mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-09-01 15:36:07 +08:00
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:
@@ -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__/
|
||||
|
||||
@@ -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')}")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user