1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
# firefly-cli
CLI tool letting an LLM agent (and the user) interact with a Firefly III
instance over its REST API. Python package, command `firefly`.
## Reference, do not modify
The Firefly III source is cloned at `../GITHUB/firefly-iii/` for reference only
(API shapes, transformers, route definitions). NEVER write to it.
## Architecture
- `firefly_cli/` package. Shared primitives: `config.py` (env over TOML file),
`client.py` (HTTP + auth + error surfacing), `errors.py` (FireflyError
hierarchy), `resolver.py` (name->id), `output.py` (JSON default, `--human`
tables), `registry.py` + `context.py`.
- Commands live in `firefly_cli/commands/`, one module per group. Each command
registers via the `@registry.command("group leaf", ...)` decorator.
- `cli.py` builds argparse subparsers from the registry. `commands/__init__.py`
imports every command module so they self-register.
## Adding a command (the expandability rule)
1. Add or edit a module in `firefly_cli/commands/`.
2. Decorate the handler: `@registry.command("budget status", help=..., args=fn)`.
`args` is `fn(subparser)` adding argparse arguments; handler is
`fn(args, ctx) -> int`. `ctx` has `.client`, `.resolver`, `.human`.
3. If it is a new module, add it to the import line in `commands/__init__.py`.
4. Write a unit test under `tests/unit/` (mock `ctx.client` / `ctx.resolver`).
5. Update `SKILL.md` (the agent-operating guide) with the new command.
6. Regenerate bash completion (it is generated, not hand-edited):
`python scripts/gen_completion.py > completions/firefly.bash`.
No other files change.
## Conventions
- Python 3.11+, standard library only. No third-party runtime deps.
- Output JSON by default (agent-first); `--human` for tables.
- Account name args resolve to IDs; ambiguous or missing names are HARD errors
listing candidates. Never silently guess an account (real money).
- Categories and tags are NOT resolved: their names pass straight to Firefly,
which auto-creates them. Accounts are never auto-created (`account create`).
- All errors are `firefly_cli.errors.FireflyError` subclasses; `cli.main`
catches them, prints `{"error": ...}` to stderr, returns exit code 1.
## Testing
- `python -m unittest discover -s tests/unit`, mocked, always run. TDD.
- Integration tests in `tests/integration/` hit a LIVE TEST ACCOUNT, gated by
`FIREFLY_TEST_URL` / `FIREFLY_TEST_TOKEN`, skipped otherwise. Write tests
create-then-delete their own records. NEVER point these at real data.
## Agent guide
`SKILL.md` documents how an agent drives the `firefly` command (the JSON/exit
contract, name resolution, task recipes). Keep it in sync when commands change.
It carries skill frontmatter and is symlinked into the Claude Code skills dir.
## Versioning
`MAJOR.MINOR.PATCH`, keyed on the tool's *contract*, not the size of the diff.
The contract is the CLI surface (command/flag names) plus the JSON output and
exit codes that an agent and SKILL.md depend on.
- **PATCH**: bug fixes and small enhancements that keep the contract (e.g.
better help text, `--human` output tweaks, a new optional flag).
- **MINOR**: new commands or functionality added without breaking existing
callers (e.g. a new `budget` group).
- **MAJOR**: a breaking change to the contract, renamed/removed command or
flag, changed JSON shape or exit codes, regardless of how small the diff is.
A large internal refactor that leaves the contract intact is NOT a major.
On every release bump the version in BOTH `pyproject.toml` and
`firefly_cli/__init__.py` (keep them equal), then create a GPG-signed tag
`vX.Y.Z` and push with `--follow-tags`.
## License
GPLv2-only. Per-file header: `Copyright (C) 2026 Danilo M. <danix@danix.xyz>`.
|