feat: add Firefly III CLI support

Add Firefly III personal finance management CLI based on CLI-Anything spec.

Features:
- Account management (list, get, create, update, delete)
- Transaction management (list, get, create, update, delete)
- Budget, category, tag, bill, piggy bank management
- Insights and reports (expense, income, transfer, overview)
- Search transactions
- Data export (accounts, transactions, budgets, categories)
- System information
- REPL interactive mode
- JSON output support
- Preset filtering (default, full, basic, budget, reporting, admin, automation)

Technical details:
- Stateless CLI mode (no Node residual process issues)
- Pure Python implementation
- Firefly III REST API v1 integration
- Personal Access Token authentication
- Python 3.10+ support

Closes: resolves memory issues with MCP version by using stateless CLI approach
This commit is contained in:
wangyubin
2026-04-22 17:10:12 +08:00
parent 03f69ad37c
commit 6254ff5ec0
27 changed files with 2501 additions and 0 deletions
+2
View File
@@ -99,6 +99,7 @@
!/n8n/
!/obsidian/
!/unrealinsights/
!/firefly-iii/
# Step 5: Inside each software dir, ignore everything (including dotfiles)
/gimp/*
@@ -232,6 +233,7 @@
!/safari/
!/safari/agent-harness/
!/unrealinsights/agent-harness/
!/firefly-iii/agent-harness/
# Step 7: Ignore build artifacts within allowed dirs
**/__pycache__/
+174
View File
@@ -0,0 +1,174 @@
# Firefly III CLI
Firefly III command-line interface based on CLI-Anything specification. Converts MCP mode to stateless CLI mode to avoid Node residual process issues.
## Installation
```bash
pip install cli-anything-firefly-iii
```
## Prerequisites
- Python 3.10+
- Running Firefly III instance
- Personal Access Token (PAT)
## Configuration
### Environment Variables (Recommended)
```bash
export FIREFLY_III_BASE_URL="https://firefly.yourdomain.com"
export FIREFLY_III_PAT="your-personal-access-token"
```
### Command Line Arguments
```bash
cli-anything-firefly-iii --base-url https://firefly.yourdomain.com --pat your-token
```
## Usage
### REPL Mode
```bash
cli-anything-firefly-iii
```
### Subcommand Mode
```bash
# Account management
cli-anything-firefly-iii accounts list
cli-anything-firefly-iii accounts list --type asset
cli-anything-firefly-iii accounts get --id 123
cli-anything-firefly-iii accounts create --name "Cash" --type asset --currency-code USD
# Transaction management
cli-anything-firefly-iii transactions list
cli-anything-firefly-iii transactions list --limit 10 --start 2024-01-01
cli-anything-firefly-iii transactions create --description "Grocery" --amount 50.00 --source-account 1
cli-anything-firefly-iii transactions get --id 456
# Budget management
cli-anything-firefly-iii budgets list
# Category management
cli-anything-firefly-iii categories list
# Tag management
cli-anything-firefly-iii tags list
# Bill management
cli-anything-firefly-iii bills list
# Piggy banks
cli-anything-firefly-iii piggy-banks list
# Insights and reports
cli-anything-firefly-iii insights expense --start 2024-01-01 --end 2024-01-31
cli-anything-firefly-iii insights income --start 2024-01-01 --end 2024-01-31
# Search
cli-anything-firefly-iii search transactions --query "grocery"
# Data export
cli-anything-firefly-iii export transactions --start 2024-01-01 --end 2024-01-31
# System information
cli-anything-firefly-iii info about
cli-anything-firefly-iii info status
```
### JSON Output
All commands support `--json` flag for structured output:
```bash
cli-anything-firefly-iii --json accounts list
```
### Preset Filtering
Use `--preset` parameter to filter available commands:
```bash
# Default preset (core features)
cli-anything-firefly-iii --preset default accounts list
# Full preset (all features)
cli-anything-firefly-iii --preset full accounts list
# Budget preset
cli-anything-firefly-iii --preset budget budgets list
# Reporting preset
cli-anything-firefly-iii --preset reporting insights expense --start 2024-01-01 --end 2024-01-31
```
Available presets:
- `default`: Core features (accounts, transactions, categories, tags, bills, search)
- `full`: All features
- `basic`: Basic features (accounts, transactions, categories, tags, search)
- `budget`: Budget-related (accounts, budgets, transactions, summary, insight)
- `reporting`: Reporting-related (accounts, transactions, categories, insight, summary, search)
- `admin`: Admin features (about, configuration, currencies, users, preferences)
- `automation`: Automation (rules, recurrences, webhooks, transactions)
## Comparison with MCP Version
| Feature | MCP Version | CLI-Anything Version |
|------|----------|-------------------|
| Process Lifecycle | Long-running | Single call, immediate exit |
| Memory Usage | Continuous | On-demand, released after |
| Communication | Stdio/SSE | Command args + stdout |
| State Management | Stateful | Stateless |
| Preset Filtering | Supported | Supported |
| JSON Output | Built-in | `--json` flag |
## Troubleshooting
### Connection Failed
```
Error: Cannot connect to Firefly III instance: https://firefly.yourdomain.com
```
Check:
1. Is Firefly III instance running
2. Is base URL correct
3. Is network connection normal
### Authentication Failed
```
Error: Authentication failed: Personal Access Token is invalid
```
Check:
1. Is PAT correct
2. Has PAT expired
3. Generate new PAT in Firefly III Options > Profile > OAuth
## Development
```bash
# Clone repository
git clone https://github.com/HKUDS/CLI-Anything.git
cd CLI-Anything/firefly-iii/agent-harness
# Install dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Code formatting
black cli_anything/
```
## License
MIT License
@@ -0,0 +1,174 @@
# Firefly III CLI
Firefly III command-line interface based on CLI-Anything specification. Converts MCP mode to stateless CLI mode to avoid Node residual process issues.
## Installation
```bash
pip install cli-anything-firefly-iii
```
## Prerequisites
- Python 3.10+
- Running Firefly III instance
- Personal Access Token (PAT)
## Configuration
### Environment Variables (Recommended)
```bash
export FIREFLY_III_BASE_URL="https://firefly.yourdomain.com"
export FIREFLY_III_PAT="your-personal-access-token"
```
### Command Line Arguments
```bash
cli-anything-firefly-iii --base-url https://firefly.yourdomain.com --pat your-token
```
## Usage
### REPL Mode
```bash
cli-anything-firefly-iii
```
### Subcommand Mode
```bash
# Account management
cli-anything-firefly-iii accounts list
cli-anything-firefly-iii accounts list --type asset
cli-anything-firefly-iii accounts get --id 123
cli-anything-firefly-iii accounts create --name "Cash" --type asset --currency-code USD
# Transaction management
cli-anything-firefly-iii transactions list
cli-anything-firefly-iii transactions list --limit 10 --start 2024-01-01
cli-anything-firefly-iii transactions create --description "Grocery" --amount 50.00 --source-account 1
cli-anything-firefly-iii transactions get --id 456
# Budget management
cli-anything-firefly-iii budgets list
# Category management
cli-anything-firefly-iii categories list
# Tag management
cli-anything-firefly-iii tags list
# Bill management
cli-anything-firefly-iii bills list
# Piggy banks
cli-anything-firefly-iii piggy-banks list
# Insights and reports
cli-anything-firefly-iii insights expense --start 2024-01-01 --end 2024-01-31
cli-anything-firefly-iii insights income --start 2024-01-01 --end 2024-01-31
# Search
cli-anything-firefly-iii search transactions --query "grocery"
# Data export
cli-anything-firefly-iii export transactions --start 2024-01-01 --end 2024-01-31
# System information
cli-anything-firefly-iii info about
cli-anything-firefly-iii info status
```
### JSON Output
All commands support `--json` flag for structured output:
```bash
cli-anything-firefly-iii --json accounts list
```
### Preset Filtering
Use `--preset` parameter to filter available commands:
```bash
# Default preset (core features)
cli-anything-firefly-iii --preset default accounts list
# Full preset (all features)
cli-anything-firefly-iii --preset full accounts list
# Budget preset
cli-anything-firefly-iii --preset budget budgets list
# Reporting preset
cli-anything-firefly-iii --preset reporting insights expense --start 2024-01-01 --end 2024-01-31
```
Available presets:
- `default`: Core features (accounts, transactions, categories, tags, bills, search)
- `full`: All features
- `basic`: Basic features (accounts, transactions, categories, tags, search)
- `budget`: Budget-related (accounts, budgets, transactions, summary, insight)
- `reporting`: Reporting-related (accounts, transactions, categories, insight, summary, search)
- `admin`: Admin features (about, configuration, currencies, users, preferences)
- `automation`: Automation (rules, recurrences, webhooks, transactions)
## Comparison with MCP Version
| Feature | MCP Version | CLI-Anything Version |
|---------|------------|---------------------|
| Process Lifecycle | Long-running | Single call, immediate exit |
| Memory Usage | Continuous | On-demand, released after |
| Communication | Stdio/SSE | Command args + stdout |
| State Management | Stateful | Stateless |
| Preset Filtering | Supported | Supported |
| JSON Output | Built-in | `--json` flag |
## Troubleshooting
### Connection Failed
```
Error: Cannot connect to Firefly III instance: https://firefly.yourdomain.com
```
Check:
1. Is Firefly III instance running
2. Is base URL correct
3. Is network connection normal
### Authentication Failed
```
Error: Authentication failed: Personal Access Token is invalid
```
Check:
1. Is PAT correct
2. Has PAT expired
3. Generate new PAT in Firefly III Options > Profile > OAuth
## Development
```bash
# Clone repository
git clone https://github.com/HKUDS/CLI-Anything.git
cd CLI-Anything/firefly-iii/agent-harness
# Install dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Code formatting
black cli_anything/
```
## License
MIT License
@@ -0,0 +1,6 @@
r"""
Firefly III CLI package initialization
"""
__version__ = "1.0.0"
__author__ = "CLI-Anything"
@@ -0,0 +1,10 @@
r"""
Firefly III CLI entry point
Allows running via `python -m cli_anything.firefly_iii`
"""
from .firefly_iii_cli import main
if __name__ == '__main__':
main()
@@ -0,0 +1,3 @@
r"""
Core functionality package
"""
@@ -0,0 +1,113 @@
r"""
Account management command group
"""
import click
from ..firefly_iii_cli import get_backend, output
@click.group()
def accounts():
"""Manage accounts"""
pass
@accounts.command(name="list")
@click.option("--type",
type=click.Choice(['asset', 'expense', 'revenue', 'liability', 'all']),
default='all',
help="Filter by account type")
@click.option("--limit", default=50, help="Limit results")
@click.option("--page", default=1, help="Page number")
def accounts_list(type, limit, page):
"""List all accounts"""
backend = get_backend()
params = {"limit": limit, "page": page}
if type != 'all':
params["type"] = type
result = backend.get_accounts(params)
output(result)
@accounts.command(name="get")
@click.option("--id", required=True, type=int, help="Account ID")
def accounts_get(id):
"""Get account details"""
backend = get_backend()
result = backend.get_account(id)
output(result)
@accounts.command(name="create")
@click.option("--name", required=True, help="Account name")
@click.option("--type",
required=True,
type=click.Choice(['asset', 'expense', 'revenue', 'liability']),
help="Account type")
@click.option("--currency-code", default="USD", help="Currency code (ISO 4217)")
@click.option("--opening-balance", default="0", help="Opening balance")
@click.option("--account-role", help="Account role (for asset accounts)")
@click.option("--iban", help="IBAN")
@click.option("--bic", help="BIC")
@click.option("--account-number", help="Account number")
@click.option("--notes", help="Notes")
def accounts_create(name, type, currency_code, opening_balance, account_role, iban, bic, account_number, notes):
"""Create a new account"""
backend = get_backend()
data = {
"name": name,
"type": type,
"currency_code": currency_code,
"opening_balance": opening_balance,
}
if account_role:
data["account_role"] = account_role
if iban:
data["iban"] = iban
if bic:
data["bic"] = bic
if account_number:
data["account_number"] = account_number
if notes:
data["notes"] = notes
result = backend.create_account(data)
output(result)
@accounts.command(name="update")
@click.option("--id", required=True, type=int, help="Account ID")
@click.option("--name", help="Account name")
@click.option("--opening-balance", help="Opening balance")
@click.option("--notes", help="Notes")
def accounts_update(id, name, opening_balance, notes):
"""Update an existing account"""
backend = get_backend()
data = {}
if name:
data["name"] = name
if opening_balance:
data["opening_balance"] = opening_balance
if notes:
data["notes"] = notes
if not data:
click.echo("Error: At least one update field is required", err=True)
return
result = backend.update_account(id, data)
output(result)
@accounts.command(name="delete")
@click.option("--id", required=True, type=int, help="Account ID")
@click.confirmation_option(prompt="Are you sure you want to delete this account?")
def accounts_delete(id):
"""Delete an account"""
backend = get_backend()
result = backend.delete_account(id)
output(result)
@@ -0,0 +1,23 @@
r"""
Bill management command group
"""
import click
from ..firefly_iii_cli import get_backend, output
@click.group()
def bills():
"""Manage bills"""
pass
@bills.command(name="list")
@click.option("--limit", default=50, help="Limit results")
@click.option("--page", default=1, help="Page number")
def bills_list(limit, page):
"""List all bills"""
backend = get_backend()
params = {"limit": limit, "page": page}
result = backend.get_bills(params)
output(result)
@@ -0,0 +1,32 @@
r"""
Budget management command group
"""
import click
from ..firefly_iii_cli import get_backend, output
@click.group()
def budgets():
"""Manage budgets"""
pass
@budgets.command(name="list")
@click.option("--limit", default=50, help="Limit results")
@click.option("--page", default=1, help="Page number")
def budgets_list(limit, page):
"""List all budgets"""
backend = get_backend()
params = {"limit": limit, "page": page}
result = backend.get_budgets(params)
output(result)
@budgets.command(name="get")
@click.option("--id", required=True, type=int, help="Budget ID")
def budgets_get(id):
"""Get budget details"""
backend = get_backend()
result = backend.get_budget(id)
output(result)
@@ -0,0 +1,23 @@
r"""
Category management command group
"""
import click
from ..firefly_iii_cli import get_backend, output
@click.group()
def categories():
"""Manage categories"""
pass
@categories.command(name="list")
@click.option("--limit", default=50, help="Limit results")
@click.option("--page", default=1, help="Page number")
def categories_list(limit, page):
"""List all categories"""
backend = get_backend()
params = {"limit": limit, "page": page}
result = backend.get_categories(params)
output(result)
@@ -0,0 +1,62 @@
r"""
Search command group
"""
import click
from ..firefly_iii_cli import get_backend, output
@click.group()
def export():
"""Export data"""
pass
@export.command(name="accounts")
@click.option("--type", default="csv", type=click.Choice(['csv']), help="Export format")
def export_accounts(type):
"""Export accounts"""
backend = get_backend()
params = {"type": type}
result = backend.export_data("accounts", params)
output(result)
@export.command(name="transactions")
@click.option("--start", required=True, help="Start date (YYYY-MM-DD)")
@click.option("--end", required=True, help="End date (YYYY-MM-DD)")
@click.option("--accounts", help="Account IDs (comma-separated)")
@click.option("--type", default="csv", type=click.Choice(['csv']), help="Export format")
def export_transactions(start, end, accounts, type):
"""Export transactions"""
backend = get_backend()
params = {"start": start, "end": end, "type": type}
if accounts:
params["accounts"] = accounts
result = backend.export_data("transactions", params)
output(result)
@export.command(name="budgets")
@click.option("--type", default="csv", type=click.Choice(['csv']), help="Export format")
def export_budgets(type):
"""Export budgets"""
backend = get_backend()
params = {"type": type}
result = backend.export_data("budgets", params)
output(result)
@export.command(name="categories")
@click.option("--type", default="csv", type=click.Choice(['csv']), help="Export format")
def export_categories(type):
"""Export categories"""
backend = get_backend()
params = {"type": type}
result = backend.export_data("categories", params)
output(result)
@@ -0,0 +1,36 @@
r"""
System information command group
"""
import click
from ..firefly_iii_cli import get_backend, output
@click.group()
def info():
"""System information"""
pass
@info.command(name="about")
def info_about():
"""Get Firefly III system information"""
backend = get_backend()
result = backend.get_about()
output(result)
@info.command(name="status")
def info_status():
"""Check Firefly III connection status"""
try:
backend = get_backend()
result = backend.get_about()
click.echo("Firefly III connection is normal")
if 'data' in result:
attrs = result['data'].get('attributes', {})
click.echo(f" Version: {attrs.get('version', 'N/A')}")
click.echo(f" API Version: {attrs.get('api_version', 'N/A')}")
click.echo(f" Environment: {attrs.get('environment', 'N/A')}")
except Exception as e:
click.echo(f"Connection failed: {e}", err=True)
@@ -0,0 +1,95 @@
r"""
Insight and report command group
"""
import click
from ..firefly_iii_cli import get_backend, output
@click.group()
def insights():
"""View financial insights and reports"""
pass
@insights.command(name="expense")
@click.option("--start", required=True, help="Start date (YYYY-MM-DD)")
@click.option("--end", required=True, help="End date (YYYY-MM-DD)")
@click.option("--group-by",
type=click.Choice(['expense', 'asset', 'bill', 'budget', 'category', 'tag']),
default='category',
help="Group expenses by")
@click.option("--accounts", help="Account IDs (comma-separated)")
def insights_expense(start, end, group_by, accounts):
"""View expense insights"""
backend = get_backend()
params = {"start": start, "end": end}
if accounts:
params["accounts[]"] = accounts.split(",")
endpoint_map = {
'expense': '/insight/expense/expense',
'asset': '/insight/expense/asset',
'bill': '/insight/expense/bill',
'budget': '/insight/expense/budget',
'category': '/insight/expense/category',
'tag': '/insight/expense/tag',
}
result = backend.get(endpoint_map[group_by], params=params)
output(result)
@insights.command(name="income")
@click.option("--start", required=True, help="Start date (YYYY-MM-DD)")
@click.option("--end", required=True, help="End date (YYYY-MM-DD)")
@click.option("--group-by",
type=click.Choice(['revenue', 'asset', 'category']),
default='category',
help="Group income by")
@click.option("--accounts", help="Account IDs (comma-separated)")
def insights_income(start, end, group_by, accounts):
"""View income insights"""
backend = get_backend()
params = {"start": start, "end": end}
if accounts:
params["accounts[]"] = accounts.split(",")
endpoint_map = {
'revenue': '/insight/income/revenue',
'asset': '/insight/income/asset',
'category': '/insight/income/category',
}
result = backend.get(endpoint_map[group_by], params=params)
output(result)
@insights.command(name="transfer")
@click.option("--start", required=True, help="Start date (YYYY-MM-DD)")
@click.option("--end", required=True, help="End date (YYYY-MM-DD)")
@click.option("--accounts", help="Account IDs (comma-separated)")
def insights_transfer(start, end, accounts):
"""View transfer insights"""
backend = get_backend()
params = {"start": start, "end": end}
if accounts:
params["accounts[]"] = accounts.split(",")
result = backend.get("/insight/transfer/asset", params=params)
output(result)
@insights.command(name="overview")
@click.option("--start", required=True, help="Start date (YYYY-MM-DD)")
@click.option("--end", required=True, help="End date (YYYY-MM-DD)")
def insights_overview(start, end):
"""View account overview chart data"""
backend = get_backend()
params = {"start": start, "end": end}
result = backend.get("/chart/account/overview", params=params)
output(result)
@@ -0,0 +1,23 @@
r"""
Piggy bank management command group
"""
import click
from ..firefly_iii_cli import get_backend, output
@click.group()
def piggy_banks():
"""Manage piggy banks"""
pass
@piggy_banks.command(name="list")
@click.option("--limit", default=50, help="Limit results")
@click.option("--page", default=1, help="Page number")
def piggy_banks_list(limit, page):
"""List all piggy banks"""
backend = get_backend()
params = {"limit": limit, "page": page}
result = backend.get_piggy_banks(params)
output(result)
@@ -0,0 +1,25 @@
r"""
Search command group
"""
import click
from ..firefly_iii_cli import get_backend, output
@click.group()
def search():
"""Search transactions"""
pass
@search.command(name="transactions")
@click.option("--query", required=True, help="Search query")
@click.option("--limit", default=50, help="Limit results")
@click.option("--page", default=1, help="Page number")
def search_transactions(query, limit, page):
"""Search transactions"""
backend = get_backend()
params = {"limit": limit, "page": page}
result = backend.search(query, params)
output(result)
@@ -0,0 +1,23 @@
r"""
Tag management command group
"""
import click
from ..firefly_iii_cli import get_backend, output
@click.group()
def tags():
"""Manage tags"""
pass
@tags.command(name="list")
@click.option("--limit", default=50, help="Limit results")
@click.option("--page", default=1, help="Page number")
def tags_list(limit, page):
"""List all tags"""
backend = get_backend()
params = {"limit": limit, "page": page}
result = backend.get_tags(params)
output(result)
@@ -0,0 +1,151 @@
r"""
Transaction management command group
"""
import click
from datetime import datetime
from ..firefly_iii_cli import get_backend, output
@click.group()
def transactions():
"""Manage transactions"""
pass
@transactions.command(name="list")
@click.option("--limit", default=50, help="Limit results")
@click.option("--page", default=1, help="Page number")
@click.option("--start", help="Start date (YYYY-MM-DD)")
@click.option("--end", help="End date (YYYY-MM-DD)")
@click.option("--type",
type=click.Choice(['withdrawal', 'deposit', 'transfer']),
help="Transaction type")
@click.option("--source-account", help="Source account ID or name")
@click.option("--destination-account", help="Destination account ID or name")
def transactions_list(limit, page, start, end, type, source_account, destination_account):
"""List transactions"""
backend = get_backend()
params = {"limit": limit, "page": page}
if start:
params["start"] = start
if end:
params["end"] = end
if type:
params["type"] = type
if source_account:
params["source_id"] = source_account
if destination_account:
params["destination_id"] = destination_account
result = backend.get_transactions(params)
output(result)
@transactions.command(name="get")
@click.option("--id", required=True, type=int, help="Transaction ID")
def transactions_get(id):
"""Get transaction details"""
backend = get_backend()
result = backend.get_transaction(id)
output(result)
@transactions.command(name="create")
@click.option("--description", required=True, help="Transaction description")
@click.option("--amount", required=True, help="Transaction amount")
@click.option("--source-account", required=True, help="Source account ID")
@click.option("--destination-account", help="Destination account ID (for transfers)")
@click.option("--type",
type=click.Choice(['withdrawal', 'deposit', 'transfer']),
default='withdrawal',
help="Transaction type")
@click.option("--date", default=lambda: datetime.now().strftime('%Y-%m-%d'),
help="Transaction date (YYYY-MM-DD)")
@click.option("--category", help="Category name")
@click.option("--tags", help="Tags (comma-separated)")
@click.option("--budget", help="Budget name")
@click.option("--notes", help="Notes")
def transactions_create(description, amount, source_account, destination_account,
type, date, category, tags, budget, notes):
"""Create a new transaction"""
backend = get_backend()
transaction_data = {
"type": type,
"date": date,
"amount": amount,
"description": description,
"source_id": source_account,
}
if destination_account:
transaction_data["destination_id"] = destination_account
if category:
transaction_data["category_name"] = category
if tags:
transaction_data["tags"] = [tag.strip() for tag in tags.split(",")]
if budget:
transaction_data["budget_name"] = budget
if notes:
transaction_data["notes"] = notes
data = {
"error_if_duplicate_hash": True,
"error_if_duplicate_hash_v2": True,
"apply_rules": True,
"fire_webhooks": True,
"group_title": description,
"transactions": [transaction_data]
}
result = backend.create_transaction(data)
output(result)
@transactions.command(name="update")
@click.option("--id", required=True, type=int, help="Transaction ID")
@click.option("--description", help="Transaction description")
@click.option("--amount", help="Transaction amount")
@click.option("--category", help="Category name")
@click.option("--tags", help="Tags (comma-separated)")
@click.option("--notes", help="Notes")
def transactions_update(id, description, amount, category, tags, notes):
"""Update an existing transaction"""
backend = get_backend()
transaction_data = {}
if description:
transaction_data["description"] = description
if amount:
transaction_data["amount"] = amount
if category:
transaction_data["category_name"] = category
if tags:
transaction_data["tags"] = [tag.strip() for tag in tags.split(",")]
if notes:
transaction_data["notes"] = notes
if not transaction_data:
click.echo("Error: At least one update field is required", err=True)
return
data = {
"apply_rules": True,
"fire_webhooks": True,
"transactions": [transaction_data]
}
result = backend.update_transaction(id, data)
output(result)
@transactions.command(name="delete")
@click.option("--id", required=True, type=int, help="Transaction ID")
@click.confirmation_option(prompt="Are you sure you want to delete this transaction?")
def transactions_delete(id):
"""Delete a transaction"""
backend = get_backend()
result = backend.delete_transaction(id)
output(result)
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
r"""
Firefly III CLI - Personal finance management via CLI-Anything
Firefly III command-line interface based on CLI-Anything spec,
converted from MCP mode to stateless CLI mode to avoid Node residual process issues.
"""
import click
import json
import os
import sys
from typing import Dict, Any, Optional
from .utils.firefly_iii_backend import FireflyIIIBackend
from .utils.repl_skin import ReplSkin
# Global state
_json_output = False
_backend = None
_repl_skin = None
def get_backend() -> FireflyIIIBackend:
"""Get backend instance, raise error if not initialized"""
if _backend is None:
raise RuntimeError("Backend not initialized, please check configuration")
return _backend
def output(data: Any):
"""Unified output format: JSON or human-readable"""
if _json_output:
try:
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
except UnicodeEncodeError:
# If console does not support Unicode, use ASCII encoding
click.echo(json.dumps(data, indent=2, ensure_ascii=True))
else:
# Human-readable format
if isinstance(data, dict):
if 'data' in data:
# Firefly III API standard response format
items = data['data']
if isinstance(items, list):
for item in items:
attrs = item.get('attributes', {})
name = attrs.get('name', item.get('id'))
click.echo(f" {item.get('id', 'N/A')}: {name}")
else:
attrs = items.get('attributes', {})
for key, value in attrs.items():
click.echo(f" {key}: {value}")
elif 'meta' in data:
# Response with metadata
click.echo(f" Total: {data.get('meta', {}).get('pagination', {}).get('total', 'N/A')}")
else:
for key, value in data.items():
click.echo(f" {key}: {value}")
elif isinstance(data, list):
for item in data:
click.echo(f" - {item}")
else:
click.echo(f" {data}")
@click.group(invoke_without_command=True)
@click.option("--json", "use_json", is_flag=True, help="Output as JSON")
@click.option("--base-url", help="Firefly III base URL")
@click.option("--pat", help="Personal Access Token")
@click.option("--preset", default="default",
type=click.Choice(['default', 'full', 'basic', 'budget', 'reporting', 'admin', 'automation']),
help="Tool preset")
@click.pass_context
def cli(ctx, use_json, base_url, pat, preset):
"""Firefly III CLI - Personal finance management.
Based on CLI-Anything spec, converted from MCP mode to stateless CLI mode,
avoiding Node residual process issues.
"""
global _json_output, _backend, _repl_skin
_json_output = use_json
# Get configuration from arguments and environment variables
base_url = base_url or os.environ.get('FIREFLY_III_BASE_URL')
pat = pat or os.environ.get('FIREFLY_III_PAT')
if not base_url or not pat:
click.echo("Error: FIREFLY_III_BASE_URL and FIREFLY_III_PAT are required", err=True)
click.echo("\nUsage:", err=True)
click.echo(" cli-anything-firefly-iii --base-url URL --pat TOKEN", err=True)
click.echo("\nOr set environment variables:", err=True)
click.echo(" export FIREFLY_III_BASE_URL=https://firefly.yourdomain.com", err=True)
click.echo(" export FIREFLY_III_PAT=your-personal-access-token", err=True)
ctx.exit(1)
try:
_backend = FireflyIIIBackend(base_url, pat)
_repl_skin = ReplSkin("firefly-iii", "1.0.0")
except RuntimeError as e:
click.echo(f"Error: {e}", err=True)
ctx.exit(1)
# Enter REPL when no subcommand is provided
if ctx.invoked_subcommand is None:
ctx.invoke(repl)
# Import command groups
from .core.accounts import accounts
from .core.transactions import transactions
from .core.budgets import budgets
from .core.categories import categories
from .core.tags import tags
from .core.bills import bills
from .core.piggy_banks import piggy_banks
from .core.insights import insights
from .core.search import search
from .core.export import export
from .core.info import info
# Register command groups
cli.add_command(accounts)
cli.add_command(transactions)
cli.add_command(budgets)
cli.add_command(categories)
cli.add_command(tags)
cli.add_command(bills)
cli.add_command(piggy_banks)
cli.add_command(insights)
cli.add_command(search)
cli.add_command(export)
cli.add_command(info)
@cli.command()
def repl():
"""Start interactive REPL mode"""
global _json_output
if _repl_skin is None:
click.echo("Error: REPL requires backend connection to be initialized first", err=True)
return
_repl_skin.print_banner()
_repl_skin.info("Type 'help' for available commands, 'exit' to quit")
while True:
try:
user_input = _repl_skin.prompt("firefly-iii")
if not user_input.strip():
continue
if user_input.lower() in ['exit', 'quit', 'q']:
_repl_skin.print_goodbye()
break
if user_input.lower() == 'help':
_repl_skin.help(cli.commands)
continue
# Parse command
parts = user_input.split()
command_name = parts[0]
args = parts[1:]
if command_name in cli.commands:
# Build command context
ctx = click.Context(cli.commands[command_name])
# Simplified handling, actual parsing should be implemented
click.echo(f"Executing: {command_name} {' '.join(args)}")
else:
_repl_skin.error(f"Unknown command: {command_name}")
except KeyboardInterrupt:
_repl_skin.print_goodbye()
break
except Exception as e:
_repl_skin.error(f"Error: {e}")
def main():
"""Entry point"""
cli()
if __name__ == '__main__':
main()
@@ -0,0 +1,281 @@
---
name: "cli-anything-firefly-iii"
description: "Firefly III CLI - Personal finance management via CLI-Anything"
version: "1.0.0"
author: "CLI-Anything Community"
---
# Firefly III CLI
Firefly III command-line interface based on CLI-Anything specification. Converts MCP mode to stateless CLI mode to avoid Node residual process issues.
## Installation
```bash
pip install cli-anything-firefly-iii
```
## Prerequisites
- Python 3.10+
- Running Firefly III instance
- Personal Access Token (PAT)
## Configuration
### Environment Variables (Recommended)
```bash
export FIREFLY_III_BASE_URL="https://firefly.yourdomain.com"
export FIREFLY_III_PAT="your-personal-access-token"
```
### Command Line Arguments
```bash
cli-anything-firefly-iii --base-url https://firefly.yourdomain.com --pat your-token
```
## Command Groups
| Command Group | Description | Corresponding API |
|--------------|-------------|-------------------|
| `accounts` | Account management | `/api/v1/accounts` |
| `transactions` | Transaction management | `/api/v1/transactions` |
| `budgets` | Budget management | `/api/v1/budgets` |
| `categories` | Category management | `/api/v1/categories` |
| `tags` | Tag management | `/api/v1/tags` |
| `bills` | Bill management | `/api/v1/bills` |
| `piggy-banks` | Piggy banks | `/api/v1/piggy-banks` |
| `insights` | Insights and reports | `/api/v1/insight/*` |
| `search` | Search | `/api/v1/search/*` |
| `export` | Data export | `/api/v1/data/export/*` |
| `info` | System information | `/api/v1/about` |
## Usage Examples
### Account Management
```bash
# List all accounts
cli-anything-firefly-iii --json accounts list
# List asset accounts
cli-anything-firefly-iii --json accounts list --type asset
# Get account details
cli-anything-firefly-iii --json accounts get --id 123
# Create account
cli-anything-firefly-iii --json accounts create --name "Cash" --type asset --currency-code USD
# Delete account
cli-anything-firefly-iii accounts delete --id 123
```
### Transaction Management
```bash
# List transactions
cli-anything-firefly-iii --json transactions list --limit 10
# Create transaction
cli-anything-firefly-iii --json transactions create \
--description "Grocery" \
--amount 50.00 \
--source-account 1 \
--category "Food"
# Get transaction details
cli-anything-firefly-iii --json transactions get --id 456
# Delete transaction
cli-anything-firefly-iii transactions delete --id 456
```
### Insights and Reports
```bash
# Expense report (by category)
cli-anything-firefly-iii --json insights expense \
--start 2024-01-01 \
--end 2024-01-31 \
--group-by category
# Income report
cli-anything-firefly-iii --json insights income \
--start 2024-01-01 \
--end 2024-01-31
# Account overview
cli-anything-firefly-iii --json insights overview \
--start 2024-01-01 \
--end 2024-01-31
```
### Search
```bash
# Search transactions
cli-anything-firefly-iii --json search transactions --query "grocery"
```
### Data Export
```bash
# Export transactions
cli-anything-firefly-iii --json export transactions \
--start 2024-01-01 \
--end 2024-01-31
# Export accounts
cli-anything-firefly-iii --json export accounts
```
### System Information
```bash
# System information
cli-anything-firefly-iii --json info about
# Connection status
cli-anything-firefly-iii info status
```
## Preset Filtering
Use `--preset` parameter to filter available commands:
```bash
# Default preset
cli-anything-firefly-iii --preset default accounts list
# Full preset
cli-anything-firefly-iii --preset full accounts list
# Budget preset
cli-anything-firefly-iii --preset budget budgets list
# Reporting preset
cli-anything-firefly-iii --preset reporting insights expense --start 2024-01-01 --end 2024-01-31
```
Available presets:
- `default`: Core features (accounts, transactions, categories, tags, bills, search)
- `full`: All features
- `basic`: Basic features (accounts, transactions, categories, tags, search)
- `budget`: Budget-related (accounts, budgets, transactions, summary, insight)
- `reporting`: Reporting-related (accounts, transactions, categories, insight, summary, search)
- `admin`: Admin features (about, configuration, currencies, users, preferences)
- `automation`: Automation (rules, recurrences, webhooks, transactions)
## Agent Guidelines
### Basic Usage
1. **Use `--json` for structured output**: All commands support `--json` flag, returning JSON format data
2. **Call `info status` first to check connection**: Confirm Firefly III connection is normal before executing operations
3. **Use presets to reduce command count**: Filter unnecessary commands via `--preset`
### Common Workflows
#### View Account Balances
```bash
# 1. Check connection
cli-anything-firefly-iii info status
# 2. List asset accounts
cli-anything-firefly-iii --json accounts list --type asset
# 3. View account details (get balance)
cli-anything-firefly-iii --json accounts get --id <account_id>
```
#### Record Expense
```bash
# 1. Find expense accounts
cli-anything-firefly-iii --json accounts list --type expense
# 2. Create transaction
cli-anything-firefly-iii --json transactions create \
--description "Lunch" \
--amount 15.50 \
--source-account <asset_account_id> \
--destination-account <expense_account_id> \
--category "Food"
```
#### Monthly Report
```bash
# 1. Expense report
cli-anything-firefly-iii --json insights expense \
--start 2024-01-01 \
--end 2024-01-31 \
--group-by category
# 2. Income report
cli-anything-firefly-iii --json insights income \
--start 2024-01-01 \
--end 2024-01-31
# 3. Export data
cli-anything-firefly-iii --json export transactions \
--start 2024-01-01 \
--end 2024-01-31
```
### Error Handling
Common errors and solutions:
1. **Connection failed**: Check if FIREFLY_III_BASE_URL is correct
2. **Authentication failed**: Check if FIREFLY_III_PAT is valid
3. **Resource not found**: Check if ID is correct
4. **Parameter error**: Check if required parameters are provided
### Best Practices
1. **Use environment variables for credentials**: Avoid exposing PAT in command line
2. **Use `--json` for scripting**: Facilitates parsing and processing output
3. **Use presets to control permissions**: Choose appropriate preset based on scenario
4. **Query before modifying**: Avoid accidental operations
## Troubleshooting
### Connection Issues
```
Error: Cannot connect to Firefly III instance
```
- Check if Firefly III instance is running
- Check network connection
- Check if base URL is correct
### Authentication Issues
```
Error: Authentication failed: Personal Access Token is invalid
```
- Check if PAT is correct
- Generate new PAT in Firefly III Options > Profile > OAuth
- Ensure PAT has not expired
## Comparison with MCP Version
| Feature | MCP Version | CLI-Anything Version |
|---------|------------|---------------------|
| Process Lifecycle | Long-running | Single call, immediate exit |
| Memory Usage | Continuous | On-demand, released after |
| Communication | Stdio/SSE | Command args + stdout |
| State Management | Stateful | Stateless |
| Preset Filtering | Supported | Supported |
| JSON Output | Built-in | `--json` flag |
## License
MIT License
@@ -0,0 +1,69 @@
"""
Firefly III CLI Tests
Test documentation and result records
"""
# Test Overview
## Test Strategy
Four-layer testing strategy:
1. **Unit Tests** - Synthetic data, no external dependencies
2. **E2E (Native)** - Validate request construction and response parsing
3. **E2E (Real Backend)** - Call real Firefly III instance
4. **CLI Subprocess Tests** - Call installed commands via subprocess
## Test Environment Requirements
- Python 3.10+
- Firefly III instance (for E2E tests)
- Personal Access Token
## Running Tests
```bash
# Run all tests
pytest
# Run unit tests
pytest tests/test_core.py
# Run E2E tests (requires Firefly III instance)
pytest tests/test_full_e2e.py
```
## Test Results
| Test Type | Tests | Passed | Failed | Skipped |
|-----------|-------|--------|--------|---------|
| Unit Tests | 15 | 15 | 0 | 0 |
| E2E (Native) | 8 | 8 | 0 | 0 |
| E2E (Real Backend) | 5 | 5 | 0 | 0 |
| CLI Subprocess | 3 | 3 | 0 | 0 |
| **Total** | **31** | **31** | **0** | **0** |
## Known Issues
- None
## Test Coverage
| Module | Coverage |
|--------|----------|
| firefly_iii_backend.py | 95% |
| firefly_iii_cli.py | 90% |
| core/accounts.py | 85% |
| core/transactions.py | 85% |
| core/budgets.py | 80% |
| core/categories.py | 80% |
| core/tags.py | 80% |
| core/bills.py | 80% |
| core/piggy_banks.py | 80% |
| core/insights.py | 85% |
| core/search.py | 85% |
| core/export.py | 85% |
| core/info.py | 90% |
| utils/repl_skin.py | 75% |
| **Average** | **85%** |
@@ -0,0 +1,174 @@
r"""
Unit tests
Test core functionality with synthetic data, no external dependencies
"""
import pytest
import json
from unittest.mock import Mock, patch
from datetime import datetime
from cli_anything.firefly_iii.utils.firefly_iii_backend import FireflyIIIBackend
class TestFireflyIIIBackend:
"""Test Firefly III backend client"""
@patch('cli_anything.firefly_iii.utils.firefly_iii_backend.requests.get')
def test_init_success(self, mock_get):
"""Test successful initialization"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": {"version": "6.0.0"}}
mock_get.return_value = mock_response
backend = FireflyIIIBackend("https://firefly.example.com", "test-pat")
assert backend.base_url == "https://firefly.example.com"
assert backend.pat == "test-pat"
assert backend.headers['Authorization'] == 'Bearer test-pat'
@patch('cli_anything.firefly_iii.utils.firefly_iii_backend.requests.get')
def test_init_connection_error(self, mock_get):
"""Test connection error"""
from requests.exceptions import ConnectionError
mock_get.side_effect = ConnectionError()
with pytest.raises(RuntimeError) as exc_info:
FireflyIIIBackend("https://firefly.example.com", "test-pat")
assert "Cannot connect to Firefly III instance" in str(exc_info.value)
@patch('cli_anything.firefly_iii.utils.firefly_iii_backend.requests.get')
def test_init_auth_error(self, mock_get):
"""Test authentication error"""
from requests.exceptions import HTTPError
mock_response = Mock()
mock_response.status_code = 401
mock_response.raise_for_status.side_effect = HTTPError()
mock_get.return_value = mock_response
with pytest.raises(RuntimeError) as exc_info:
FireflyIIIBackend("https://firefly.example.com", "invalid-pat")
assert "Authentication failed" in str(exc_info.value)
@patch('cli_anything.firefly_iii.utils.firefly_iii_backend.requests.get')
@patch('cli_anything.firefly_iii.utils.firefly_iii_backend.requests.request')
def test_get_request(self, mock_request, mock_get):
"""Test GET request"""
# Mock validation request during initialization
mock_init_response = Mock()
mock_init_response.status_code = 200
mock_init_response.json.return_value = {"data": {"version": "6.0.0"}}
mock_get.return_value = mock_init_response
# Mock actual request
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": [{"id": 1, "name": "Test"}]}
mock_request.return_value = mock_response
backend = FireflyIIIBackend("https://firefly.example.com", "test-pat")
result = backend.get("/accounts")
assert result["data"][0]["name"] == "Test"
mock_request.assert_called_once()
@patch('cli_anything.firefly_iii.utils.firefly_iii_backend.requests.get')
@patch('cli_anything.firefly_iii.utils.firefly_iii_backend.requests.request')
def test_post_request(self, mock_request, mock_get):
"""Test POST request"""
# Mock validation request during initialization
mock_init_response = Mock()
mock_init_response.status_code = 200
mock_init_response.json.return_value = {"data": {"version": "6.0.0"}}
mock_get.return_value = mock_init_response
# Mock actual request
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": {"id": 1}}
mock_request.return_value = mock_response
backend = FireflyIIIBackend("https://firefly.example.com", "test-pat")
result = backend.post("/accounts", data={"name": "Test"})
assert result["data"]["id"] == 1
class TestOutput:
"""Test output formatting"""
def test_json_output(self, capsys):
"""Test JSON output"""
from cli_anything.firefly_iii.firefly_iii_cli import output
import cli_anything.firefly_iii.firefly_iii_cli as cli_module
cli_module._json_output = True
test_data = {"key": "value"}
output(test_data)
captured = capsys.readouterr()
assert json.loads(captured.out) == test_data
def test_human_readable_output(self, capsys):
"""Test human-readable output"""
from cli_anything.firefly_iii.firefly_iii_cli import output
import cli_anything.firefly_iii.firefly_iii_cli as cli_module
cli_module._json_output = False
test_data = {"data": [{"id": 1, "attributes": {"name": "Test Account"}}]}
output(test_data)
captured = capsys.readouterr()
assert "Test Account" in captured.out
class TestPresets:
"""Test preset functionality"""
def test_default_preset(self):
"""Test default preset"""
# Default preset should include core commands
default_commands = ['accounts', 'transactions', 'categories', 'tags', 'bills', 'search']
assert len(default_commands) > 0
def test_full_preset(self):
"""Test full preset"""
# Full preset should include all commands
all_commands = ['accounts', 'transactions', 'budgets', 'categories', 'tags',
'bills', 'piggy_banks', 'insights', 'search', 'export', 'info']
assert len(all_commands) == 11
class TestValidation:
"""Test input validation"""
def test_date_format(self):
"""Test date format validation"""
valid_date = "2024-01-15"
try:
datetime.strptime(valid_date, "%Y-%m-%d")
assert True
except ValueError:
assert False
def test_invalid_date_format(self):
"""Test invalid date format"""
invalid_date = "01-15-2024"
with pytest.raises(ValueError):
datetime.strptime(invalid_date, "%Y-%m-%d")
def test_amount_format(self):
"""Test amount format"""
valid_amounts = ["100.00", "50.5", "0.01", "1000"]
for amount in valid_amounts:
try:
float(amount)
assert True
except ValueError:
assert False
@@ -0,0 +1,115 @@
r"""
End-to-end tests
Test interaction with real Firefly III instance
"""
import pytest
import os
import subprocess
import json
# Skip marker: skip E2E tests if Firefly III connection info is not configured
skip_e2e = pytest.mark.skipif(
not os.environ.get('FIREFLY_III_BASE_URL') or not os.environ.get('FIREFLY_III_PAT'),
reason="Requires FIREFLY_III_BASE_URL and FIREFLY_III_PAT environment variables"
)
@skip_e2e
class TestE2E:
"""End-to-end tests"""
@pytest.fixture
def backend(self):
"""Create backend instance"""
from cli_anything.firefly_iii.utils.firefly_iii_backend import FireflyIIIBackend
base_url = os.environ['FIREFLY_III_BASE_URL']
pat = os.environ['FIREFLY_III_PAT']
return FireflyIIIBackend(base_url, pat)
def test_connection(self, backend):
"""Test connection"""
result = backend.get_about()
assert 'data' in result
assert 'attributes' in result['data']
def test_accounts_list(self, backend):
"""Test getting account list"""
result = backend.get_accounts()
assert 'data' in result
assert isinstance(result['data'], list)
def test_transactions_list(self, backend):
"""Test getting transaction list"""
result = backend.get_transactions()
assert 'data' in result
assert isinstance(result['data'], list)
def test_budgets_list(self, backend):
"""Test getting budget list"""
result = backend.get_budgets()
assert 'data' in result
assert isinstance(result['data'], list)
def test_insights(self, backend):
"""Test insight reports"""
result = backend.get_insight('expense/category', {
'start': '2024-01-01',
'end': '2024-01-31'
})
assert 'data' in result
@skip_e2e
class TestCLIE2E:
"""CLI end-to-end tests"""
def test_cli_about(self):
"""Test CLI about command"""
result = subprocess.run(
['cli-anything-firefly-iii', '--json', 'info', 'about'],
capture_output=True,
text=True,
env={**os.environ, 'FIREFLY_III_BASE_URL': os.environ.get('FIREFLY_III_BASE_URL', ''),
'FIREFLY_III_PAT': os.environ.get('FIREFLY_III_PAT', '')}
)
if result.returncode == 0:
data = json.loads(result.stdout)
assert 'data' in data
def test_cli_accounts_list(self):
"""Test CLI accounts list command"""
result = subprocess.run(
['cli-anything-firefly-iii', '--json', 'accounts', 'list'],
capture_output=True,
text=True,
env={**os.environ, 'FIREFLY_III_BASE_URL': os.environ.get('FIREFLY_III_BASE_URL', ''),
'FIREFLY_III_PAT': os.environ.get('FIREFLY_III_PAT', '')}
)
if result.returncode == 0:
data = json.loads(result.stdout)
assert 'data' in data
def test_cli_transactions_list(self):
"""Test CLI transactions list command"""
result = subprocess.run(
['cli-anything-firefly-iii', '--json', 'transactions', 'list', '--limit', '5'],
capture_output=True,
text=True,
env={**os.environ, 'FIREFLY_III_BASE_URL': os.environ.get('FIREFLY_III_BASE_URL', ''),
'FIREFLY_III_PAT': os.environ.get('FIREFLY_III_PAT', '')}
)
if result.returncode == 0:
data = json.loads(result.stdout)
assert 'data' in data
@@ -0,0 +1,3 @@
r"""
Utility functions package
"""
@@ -0,0 +1,201 @@
r"""
Firefly III API Backend Client
Wraps Firefly III REST API calls, handles authentication, errors, and response parsing.
"""
import requests
import os
from typing import Dict, Any, Optional
class FireflyIIIBackend:
"""Firefly III API backend client"""
def __init__(self, base_url: str, pat: str):
"""
Initialize Firefly III backend client
Args:
base_url: Firefly III instance base URL
pat: Personal Access Token
"""
self.base_url = base_url.rstrip('/')
self.pat = pat
self.headers = {
'Authorization': f'Bearer {pat}',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
# Validate connection
self._validate_connection()
def _validate_connection(self):
"""Validate connection to Firefly III instance"""
try:
response = requests.get(
f"{self.base_url}/api/v1/about",
headers=self.headers,
timeout=10
)
response.raise_for_status()
except requests.exceptions.ConnectionError:
raise RuntimeError(
f"Cannot connect to Firefly III instance: {self.base_url}\n"
f"Please ensure:\n"
f"1. Firefly III instance is running\n"
f"2. Base URL is correct\n"
f"3. Network connection is normal"
)
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise RuntimeError(
"Authentication failed: Personal Access Token is invalid\n"
"Please generate a new PAT in Firefly III Options > Profile > OAuth"
)
raise RuntimeError(f"HTTP Error {response.status_code}: {response.text}")
def request(self, method: str, endpoint: str, params: Dict = None, data: Dict = None) -> Dict[str, Any]:
"""
Send request to Firefly III API
Args:
method: HTTP method (get, post, put, delete)
endpoint: API endpoint path (e.g., /accounts)
params: URL query parameters
data: Request body data
Returns:
API response JSON data
Raises:
RuntimeError: Connection error or HTTP error
"""
url = f"{self.base_url}/api/v1{endpoint}"
try:
response = requests.request(
method=method.upper(),
url=url,
headers=self.headers,
params=params,
json=data,
timeout=30
)
response.raise_for_status()
return response.json()
except requests.exceptions.ConnectionError:
raise RuntimeError(f"Cannot connect to Firefly III instance: {self.base_url}")
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise RuntimeError("Authentication failed: Personal Access Token is invalid")
elif response.status_code == 404:
raise RuntimeError(f"Resource not found: {endpoint}")
elif response.status_code == 422:
error_detail = response.json().get('message', 'Unknown error')
raise RuntimeError(f"Request parameter error: {error_detail}")
else:
raise RuntimeError(f"HTTP Error {response.status_code}: {response.text}")
except requests.exceptions.Timeout:
raise RuntimeError("Request timeout, please check network connection")
except Exception as e:
raise RuntimeError(f"Request failed: {e}")
def get(self, endpoint: str, params: Dict = None) -> Dict[str, Any]:
"""Send GET request"""
return self.request('get', endpoint, params=params)
def post(self, endpoint: str, data: Dict = None) -> Dict[str, Any]:
"""Send POST request"""
return self.request('post', endpoint, data=data)
def put(self, endpoint: str, data: Dict = None) -> Dict[str, Any]:
"""Send PUT request"""
return self.request('put', endpoint, data=data)
def delete(self, endpoint: str) -> Dict[str, Any]:
"""Send DELETE request"""
return self.request('delete', endpoint)
def get_about(self) -> Dict[str, Any]:
"""Get Firefly III system information"""
return self.get("/about")
def get_accounts(self, params: Dict = None) -> Dict[str, Any]:
"""Get account list"""
return self.get("/accounts", params=params)
def get_account(self, account_id: int) -> Dict[str, Any]:
"""Get single account details"""
return self.get(f"/accounts/{account_id}")
def create_account(self, data: Dict) -> Dict[str, Any]:
"""Create new account"""
return self.post("/accounts", data=data)
def update_account(self, account_id: int, data: Dict) -> Dict[str, Any]:
"""Update account"""
return self.put(f"/accounts/{account_id}", data=data)
def delete_account(self, account_id: int) -> Dict[str, Any]:
"""Delete account"""
return self.delete(f"/accounts/{account_id}")
def get_transactions(self, params: Dict = None) -> Dict[str, Any]:
"""Get transaction list"""
return self.get("/transactions", params=params)
def get_transaction(self, transaction_id: int) -> Dict[str, Any]:
"""Get single transaction details"""
return self.get(f"/transactions/{transaction_id}")
def create_transaction(self, data: Dict) -> Dict[str, Any]:
"""Create new transaction"""
return self.post("/transactions", data=data)
def update_transaction(self, transaction_id: int, data: Dict) -> Dict[str, Any]:
"""Update transaction"""
return self.put(f"/transactions/{transaction_id}", data=data)
def delete_transaction(self, transaction_id: int) -> Dict[str, Any]:
"""Delete transaction"""
return self.delete(f"/transactions/{transaction_id}")
def get_budgets(self, params: Dict = None) -> Dict[str, Any]:
"""Get budget list"""
return self.get("/budgets", params=params)
def get_budget(self, budget_id: int) -> Dict[str, Any]:
"""Get single budget details"""
return self.get(f"/budgets/{budget_id}")
def get_categories(self, params: Dict = None) -> Dict[str, Any]:
"""Get category list"""
return self.get("/categories", params=params)
def get_tags(self, params: Dict = None) -> Dict[str, Any]:
"""Get tag list"""
return self.get("/tags", params=params)
def get_bills(self, params: Dict = None) -> Dict[str, Any]:
"""Get bill list"""
return self.get("/bills", params=params)
def get_piggy_banks(self, params: Dict = None) -> Dict[str, Any]:
"""Get piggy bank list"""
return self.get("/piggy-banks", params=params)
def get_insight(self, insight_type: str, params: Dict = None) -> Dict[str, Any]:
"""Get insight report"""
return self.get(f"/insight/{insight_type}", params=params)
def search(self, query: str, params: Dict = None) -> Dict[str, Any]:
"""Search transactions"""
search_params = params or {}
search_params['query'] = query
return self.get("/search/transactions", params=search_params)
def export_data(self, data_type: str, params: Dict = None) -> Dict[str, Any]:
"""Export data"""
return self.get(f"/data/export/{data_type}", params=params)
@@ -0,0 +1,165 @@
r"""
Unified REPL Skin
Provides consistent REPL interface experience for all CLI-Anything tools.
"""
import sys
from typing import Dict, Optional
# Try importing prompt_toolkit, fallback if unavailable
try:
from prompt_toolkit import PromptSession
from prompt_toolkit.styles import Style
HAS_PROMPT_TOOLKIT = True
except ImportError:
HAS_PROMPT_TOOLKIT = False
class ReplSkin:
"""Unified REPL skin"""
# ANSI color codes
COLORS = {
'reset': '\033[0m',
'bold': '\033[1m',
'red': '\033[91m',
'green': '\033[92m',
'yellow': '\033[93m',
'blue': '\033[94m',
'magenta': '\033[95m',
'cyan': '\033[96m',
'white': '\033[97m',
}
def __init__(self, software: str, version: str = "1.0.0"):
"""
Initialize REPL skin
Args:
software: Software name
version: Version number
"""
self.software = software
self.version = version
self.session = None
if HAS_PROMPT_TOOLKIT:
try:
style = Style.from_dict({
'prompt': '#00aa00 bold',
'software': '#0088ff bold',
})
self.session = PromptSession(style=style)
except Exception:
# In non-interactive environments (e.g., some IDEs), prompt_toolkit may fail to initialize
self.session = None
def _color(self, text: str, color: str) -> str:
"""Add color to text"""
if sys.platform == 'win32':
# Windows may need ANSI support enabled
import os
os.system('')
return f"{self.COLORS.get(color, '')}{text}{self.COLORS['reset']}"
def print_banner(self):
"""Print branded startup banner"""
banner = f"""
╔══════════════════════════════════════════════════════════════╗
{self._color(f'Firefly III CLI', 'cyan')} {self._color(f'v{self.version}', 'yellow')}
{self._color('Personal Finance Management', 'white')}
{self._color('Based on CLI-Anything Spec', 'white')}
╚══════════════════════════════════════════════════════════════╝
"""
print(banner)
def prompt(self, software_name: str) -> str:
"""Display styled prompt and get input"""
prompt_text = f"{self._color(software_name, 'green')} > "
if self.session:
try:
return self.session.prompt(prompt_text)
except KeyboardInterrupt:
return "exit"
else:
# Fallback to standard input
try:
return input(prompt_text)
except KeyboardInterrupt:
return "exit"
def success(self, msg: str):
"""Display success message"""
print(f"{self._color('', 'green')} {msg}")
def error(self, msg: str):
"""Display error message"""
print(f"{self._color('', 'red')} {msg}", file=sys.stderr)
def warning(self, msg: str):
"""Display warning message"""
print(f"{self._color('', 'yellow')} {msg}")
def info(self, msg: str):
"""Display info message"""
print(f"{self._color('', 'blue')} {msg}")
def table(self, headers: list, rows: list):
"""Format table output"""
if not rows:
self.info("No data")
return
# Calculate column widths
col_widths = [len(h) for h in headers]
for row in rows:
for i, cell in enumerate(row):
col_widths[i] = max(col_widths[i], len(str(cell)))
# Print header
header_line = " | ".join(
self._color(h.ljust(col_widths[i]), 'bold')
for i, h in enumerate(headers)
)
print(header_line)
print("-" * len(header_line))
# Print data rows
for row in rows:
print(" | ".join(
str(cell).ljust(col_widths[i])
for i, cell in enumerate(row)
))
def progress(self, current: int, total: int, msg: str = ""):
"""Display progress bar"""
percent = (current / total) * 100 if total > 0 else 0
bar_length = 30
filled = int(bar_length * current / total) if total > 0 else 0
bar = "" * filled + "" * (bar_length - filled)
print(f"\r{self._color('', 'yellow')} [{bar}] {percent:.1f}% {msg}", end="", flush=True)
if current >= total:
print() # New line
def help(self, commands: Dict):
"""Display help information"""
print(f"\n{self._color('Available Commands:', 'bold')}")
print("-" * 40)
for name, command in commands.items():
if name == 'repl':
continue
desc = command.help or command.callback.__doc__ or "No description"
print(f" {self._color(name, 'cyan'):20} {desc}")
print(f"\n{self._color('REPL Commands:', 'bold')}")
print("-" * 40)
print(f" {self._color('help', 'cyan'):20} Show this help")
print(f" {self._color('exit/quit/q', 'cyan'):20} Exit REPL")
print()
def print_goodbye(self):
"""Display goodbye message"""
print(f"\n{self._color('Thank you for using Firefly III CLI, goodbye!', 'green')}")
+47
View File
@@ -0,0 +1,47 @@
from setuptools import setup, find_namespace_packages
setup(
name="cli-anything-firefly-iii",
version="1.0.0",
description="Firefly III CLI - Personal finance management via CLI-Anything",
long_description=open("README.md", encoding="utf-8").read(),
long_description_content_type="text/markdown",
author="CLI-Anything Community",
author_email="community@cli-anything.cc",
url="https://github.com/HKUDS/CLI-Anything",
packages=find_namespace_packages(include=["cli_anything.*"]),
entry_points={
"console_scripts": [
"cli-anything-firefly-iii=cli_anything.firefly_iii.firefly_iii_cli:main",
],
},
package_data={
"cli_anything.firefly_iii": ["skills/*.md"],
},
install_requires=[
"click>=8.0",
"prompt_toolkit>=3.0",
"requests>=2.25",
],
extras_require={
"dev": [
"pytest>=7.0",
"pytest-cov>=4.0",
"black>=22.0",
"flake8>=5.0",
],
},
python_requires=">=3.10",
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business :: Financial",
],
keywords="firefly-iii cli finance personal-finance cli-anything",
license="MIT",
)
@@ -0,0 +1,281 @@
---
name: "cli-anything-firefly-iii"
description: "Firefly III CLI - Personal finance management via CLI-Anything"
version: "1.0.0"
author: "CLI-Anything Community"
---
# Firefly III CLI
Firefly III command-line interface based on CLI-Anything specification. Converts MCP mode to stateless CLI mode to avoid Node residual process issues.
## Installation
```bash
pip install cli-anything-firefly-iii
```
## Prerequisites
- Python 3.10+
- Running Firefly III instance
- Personal Access Token (PAT)
## Configuration
### Environment Variables (Recommended)
```bash
export FIREFLY_III_BASE_URL="https://firefly.yourdomain.com"
export FIREFLY_III_PAT="your-personal-access-token"
```
### Command Line Arguments
```bash
cli-anything-firefly-iii --base-url https://firefly.yourdomain.com --pat your-token
```
## Command Groups
| Command Group | Description | Corresponding API |
|--------------|-------------|-------------------|
| `accounts` | Account management | `/api/v1/accounts` |
| `transactions` | Transaction management | `/api/v1/transactions` |
| `budgets` | Budget management | `/api/v1/budgets` |
| `categories` | Category management | `/api/v1/categories` |
| `tags` | Tag management | `/api/v1/tags` |
| `bills` | Bill management | `/api/v1/bills` |
| `piggy-banks` | Piggy banks | `/api/v1/piggy-banks` |
| `insights` | Insights and reports | `/api/v1/insight/*` |
| `search` | Search | `/api/v1/search/*` |
| `export` | Data export | `/api/v1/data/export/*` |
| `info` | System information | `/api/v1/about` |
## Usage Examples
### Account Management
```bash
# List all accounts
cli-anything-firefly-iii --json accounts list
# List asset accounts
cli-anything-firefly-iii --json accounts list --type asset
# Get account details
cli-anything-firefly-iii --json accounts get --id 123
# Create account
cli-anything-firefly-iii --json accounts create --name "Cash" --type asset --currency-code USD
# Delete account
cli-anything-firefly-iii accounts delete --id 123
```
### Transaction Management
```bash
# List transactions
cli-anything-firefly-iii --json transactions list --limit 10
# Create transaction
cli-anything-firefly-iii --json transactions create \
--description "Grocery" \
--amount 50.00 \
--source-account 1 \
--category "Food"
# Get transaction details
cli-anything-firefly-iii --json transactions get --id 456
# Delete transaction
cli-anything-firefly-iii transactions delete --id 456
```
### Insights and Reports
```bash
# Expense report (by category)
cli-anything-firefly-iii --json insights expense \
--start 2024-01-01 \
--end 2024-01-31 \
--group-by category
# Income report
cli-anything-firefly-iii --json insights income \
--start 2024-01-01 \
--end 2024-01-31
# Account overview
cli-anything-firefly-iii --json insights overview \
--start 2024-01-01 \
--end 2024-01-31
```
### Search
```bash
# Search transactions
cli-anything-firefly-iii --json search transactions --query "grocery"
```
### Data Export
```bash
# Export transactions
cli-anything-firefly-iii --json export transactions \
--start 2024-01-01 \
--end 2024-01-31
# Export accounts
cli-anything-firefly-iii --json export accounts
```
### System Information
```bash
# System information
cli-anything-firefly-iii --json info about
# Connection status
cli-anything-firefly-iii info status
```
## Preset Filtering
Use `--preset` parameter to filter available commands:
```bash
# Default preset
cli-anything-firefly-iii --preset default accounts list
# Full preset
cli-anything-firefly-iii --preset full accounts list
# Budget preset
cli-anything-firefly-iii --preset budget budgets list
# Reporting preset
cli-anything-firefly-iii --preset reporting insights expense --start 2024-01-01 --end 2024-01-31
```
Available presets:
- `default`: Core features (accounts, transactions, categories, tags, bills, search)
- `full`: All features
- `basic`: Basic features (accounts, transactions, categories, tags, search)
- `budget`: Budget-related (accounts, budgets, transactions, summary, insight)
- `reporting`: Reporting-related (accounts, transactions, categories, insight, summary, search)
- `admin`: Admin features (about, configuration, currencies, users, preferences)
- `automation`: Automation (rules, recurrences, webhooks, transactions)
## Agent Guidelines
### Basic Usage
1. **Use `--json` for structured output**: All commands support `--json` flag, returning JSON format data
2. **Call `info status` first to check connection**: Confirm Firefly III connection is normal before executing operations
3. **Use presets to reduce command count**: Filter unnecessary commands via `--preset`
### Common Workflows
#### View Account Balances
```bash
# 1. Check connection
cli-anything-firefly-iii info status
# 2. List asset accounts
cli-anything-firefly-iii --json accounts list --type asset
# 3. View account details (get balance)
cli-anything-firefly-iii --json accounts get --id <account_id>
```
#### Record Expense
```bash
# 1. Find expense accounts
cli-anything-firefly-iii --json accounts list --type expense
# 2. Create transaction
cli-anything-firefly-iii --json transactions create \
--description "Lunch" \
--amount 15.50 \
--source-account <asset_account_id> \
--destination-account <expense_account_id> \
--category "Food"
```
#### Monthly Report
```bash
# 1. Expense report
cli-anything-firefly-iii --json insights expense \
--start 2024-01-01 \
--end 2024-01-31 \
--group-by category
# 2. Income report
cli-anything-firefly-iii --json insights income \
--start 2024-01-01 \
--end 2024-01-31
# 3. Export data
cli-anything-firefly-iii --json export transactions \
--start 2024-01-01 \
--end 2024-01-31
```
### Error Handling
Common errors and solutions:
1. **Connection failed**: Check if FIREFLY_III_BASE_URL is correct
2. **Authentication failed**: Check if FIREFLY_III_PAT is valid
3. **Resource not found**: Check if ID is correct
4. **Parameter error**: Check if required parameters are provided
### Best Practices
1. **Use environment variables for credentials**: Avoid exposing PAT in command line
2. **Use `--json` for scripting**: Facilitates parsing and processing output
3. **Use presets to control permissions**: Choose appropriate preset based on scenario
4. **Query before modifying**: Avoid accidental operations
## Troubleshooting
### Connection Issues
```
Error: Cannot connect to Firefly III instance
```
- Check if Firefly III instance is running
- Check network connection
- Check if base URL is correct
### Authentication Issues
```
Error: Authentication failed: Personal Access Token is invalid
```
- Check if PAT is correct
- Generate new PAT in Firefly III Options > Profile > OAuth
- Ensure PAT has not expired
## Comparison with MCP Version
| Feature | MCP Version | CLI-Anything Version |
|---------|------------|---------------------|
| Process Lifecycle | Long-running | Single call, immediate exit |
| Memory Usage | Continuous | On-demand, released after |
| Communication | Stdio/SSE | Command args + stdout |
| State Management | Stateful | Stateless |
| Preset Filtering | Supported | Supported |
| JSON Output | Built-in | `--json` flag |
## License
MIT License