means.cli: Command-Line Tools#
The cli subpackage implements the console scripts declared in pyproject.toml, one module per command.
Each module exposes a main() entry point (the target of its [project.scripts] entry) plus the domain functions that do the work.
Remote telemetry is structural and content-free: operation names are stable means.* identifiers, and attributes are limited to bounded kinds, counts, and error codes.
Account and user names, file paths, memos, dates, amounts, report contents, model responses, and ledger command output remain absent from telemetry.
tests/test_telemetry.py enforces that boundary across every direct Logfire call in the means package; shared-library and exporter behavior is covered in my-basis and the fleet observability contract.
Supplying a Logfire token is the explicit opt-in that enables these structural events in local or development runs.
I load — Parse Configured Ledgers#
- means.cli.load.load_ledgers(directory: Annotated[Path, PathType(path_type=dir)]) list[Ledger]#
Set up the means system and parse every configured user’s ledger.
Users that share a ledger file with an earlier (parent) user are appended to that ledger’s
userslist instead of producing a duplicateLedger.- Parameters:
directory – The means data directory containing parameters and ledger files.
- Returns:
One
Ledgerper distinct ledger file.
- means.cli.load.parse_args() Namespace#
Parse command-line arguments for the
loadcommand.
- means.cli.load.main()#
Run the
loadcommand: parse arguments and load all configured ledgers.
II grow — Regenerate the Account Tree#
- means.cli.grow.process_seed(parent: Account, tree: dict | None) Generator[Account, None, None]#
Yield the parent account and, recursively, every descendant in the seed tree.
- Parameters:
parent – The account path accumulated so far.
tree – The subtree of child names below
parent, or None at a leaf.
- Yields:
Each account path in depth-first order, starting with
parentitself.
- means.cli.grow.load_seed(source: Annotated[Path, PathType(path_type=file)], users: list[str]) tuple[dict, dict]#
Load the seed file and split it into mode and per-user override trees.
- Parameters:
source – The
accounts_seed.yamlfile to parse.users – All user uids, each of which must be present in the seed data.
- Returns:
A tuple of the mode tree and the per-user override tree.
- means.cli.grow.grow_user_section(mode: str, user: str, tree: dict, utree: dict) list[str]#
Render the account declarations for one mode/user pair.
- Parameters:
mode – The top-level account mode (e.g.
funds,costs).user – The user uid this section belongs to.
tree – The shared seed tree for this mode.
utree – The per-user override tree.
- Returns:
The lines of the section, starting with a wrapped comment header.
- means.cli.grow.grow(source: Annotated[Path, PathType(path_type=file)] | None = None, target: Annotated[Path, PathType(path_type=file)] | None = None) None#
Grow the accounts tree in
accounts.ledgerfrom the seed file.Expands the dictionary content of
accounts_seed.yamlinto explicitaccountdeclarations, ensuring consistency across all users.- Parameters:
source – The seed file to read; defaults to
Account.SEED_FILE.target – The accounts file to write; defaults to
Account.ACCT_FILE.
- means.cli.grow.parse_args() Namespace#
Parse command-line arguments for the
growcommand.
- means.cli.grow.main()#
Run the
growcommand: load ledgers, then regrow the accounts file.
III ingest — Import Bank Statements#
- means.cli.ingest.ingest_file(ledger: Ledger, file: Annotated[Path, PathType(path_type=file)], uid: str) list[Transaction]#
Ingest a single
.csvfile into a list of transactions.- Parameters:
ledger – The ledger that owns the file.
file – The downloaded
.csvfile to ingest.uid – The uid of the
BankAccountthe file belongs to.
- Returns:
The cleaned transactions parsed from the file.
- means.cli.ingest.ingest(ledger: Ledger, keep: bool = False) None#
Ingest every new
.csvfile for a ledger, then merge, write, and archive.- Parameters:
ledger – The ledger to ingest new files into.
keep – If True, leave the original
.csvfiles in place instead of archiving them.
- means.cli.ingest.parse_args() Namespace#
Parse command-line arguments for the
ingestcommand.
- means.cli.ingest.main()#
Run the
ingestcommand: load ledgers, optionally grow accounts, then ingest.
IV categorize — Resolve Unknown Transactions#
- means.cli.categorize.categorize_ledger(ledger: Ledger) None#
Start an interactive program to categorize any remaining unknowns in the ledger.
- means.cli.categorize.categorize_transaction(ledger: Ledger, tr: Transaction, bank: dict[str, list[Account]]) bool#
Interactively categorize a transaction using an fzf fuzzy-finder.
- Parameters:
ledger – The ledger the transaction belongs to.
tr – The unknown transaction to categorize.
bank – A mutable mapping of cleaned memos to previously chosen accounts.
- Returns:
False if the user chose to exit the loop, True otherwise.
- means.cli.categorize.parse_args() Namespace#
Parse command line arguments.
- means.cli.categorize.main()#
The main function for the “categorize” command, which starts an interactive program.
V suggest — LLM-Assisted Templates#
suggest is an explicitly remote, advisory layer over the deterministic template system.
Install the optional SDK and provide the standard Anthropic API key environment variable:
uv tool install 'my-means[llms]'
export ANTHROPIC_API_KEY=your-api-key
export MEANS_LLM_MODEL=claude-haiku-4-5-20251001 # optional default
Preview proposals without changing templates.yaml, then run again and confirm a reviewed merge:
suggest --directory /path/to/means --dry-run
suggest --directory /path/to/means
The request contains uncategorized memo strings, current templates, and the account structure.
It contains no API key and no telemetry content.
--dry-run prevents the local write, not the remote request.
Before confirmation, proposals must use known account paths, add rather than replace patterns, and contain bounded literal pattern text without regex metacharacters plus text labels.
Existing hand-authored regex templates remain supported; the restriction applies only to untrusted model additions.
The default Claude Haiku 4.5 model is economical and can be replaced through MEANS_LLM_MODEL or --model without changing code.
- means.cli.suggest.build_prompt(memos: list[str], templates_text: str, accounts_text: str) str#
Build the LLM prompt asking for new template entries.
- Parameters:
memos – The uncategorized memo strings to be classified.
templates_text – The raw text of the existing
templates.yaml.accounts_text – The raw text of the account structure file.
- Returns:
The full prompt to send to the model.
- means.cli.suggest.suggest(root_ledger: Path, templates_file: Path, accounts_file: Path, dry_run: bool = False, *, model: str | None = None) None#
Ask an LLM for new template entries and optionally apply them.
Streams the model’s reasoning to stdout, extracts its fenced YAML block, and — after user confirmation — deep-merges the new entries into
templates.yaml.- Parameters:
root_ledger – The root ledger file to query for unknown payees.
templates_file – The
templates.yamlfile to update.accounts_file – The account structure file given to the model as context.
dry_run – If True, print suggestions without prompting to apply them.
model – Anthropic model ID. Defaults to
MEANS_LLM_MODEL, then Claude Haiku 4.5.
- Raises:
ValueError – If the model configuration or proposed template data is invalid.
RuntimeError – If the optional SDK is missing or a local input changes during review.
- means.cli.suggest.parse_args() Namespace#
Parse command-line arguments for the
suggestcommand.
- means.cli.suggest.main() None#
Run the
suggestcommand against the configured means directory.
VI means-plan — Conditional Cashflow and Debt Planning#
means-plan projects explicit YAML or JSON assumptions without reading or mutating a Ledger.
It is a conditional deterministic projection, not a prediction or financial advice.
Project one strategy or compare every implemented strategy:
means-plan project --scenario fictional-plan.yaml --strategy avalanche --json
means-plan compare --scenario fictional-plan.yaml --csv --output comparison.csv
JSON is the default machine-readable output; CSV is a rectangular spreadsheet-oriented subset.
Reports go to stdout unless --output names an explicit owner-only file.
The comparison’s recommended field is a rule-selected strategy under the disclosed comparison objective, not advice.
Zero warnings means no implemented rule fired, not that the scenario is safe or complete.
The complete fictional scenario schema, exact-cent and date rules, warning meanings, selection objective, privacy boundary, and limitations are in the conditional planning guide.
- means.cli.plan.parse_args(argv: list[str] | None = None) Namespace#
Parse the planning command line.
- means.cli.plan.run(args: Namespace) str#
Calculate and render the selected planning report.
- means.cli.plan.main(argv: list[str] | None = None) int#
Run the planning command.
VII report — Tabular Finance Reports#
- class means.cli.report.Editor(*, directory: ~typing.Annotated[~pathlib.Path, ~pydantic.types.PathType(path_type=dir)], reports: dict[str, ~typing.Annotated[~pandas.DataFrame, ~pydantic.types.GetPydanticSchema(get_pydantic_core_schema=~my.utils.SyntaxUtils.SyntaxUtils.pyd_schemify.<locals>.<lambda>, get_pydantic_json_schema=None)]] = {}, sheet_id: str = '')#
Generate reports on the state of finances across one or more ledger files.
- REGISTRY: ClassVar[dict[str, Reporter]] = {'overview': <function Editor.overview>, 'raw': <function Editor.raw>, 'shared': <function Editor.shared>}#
- directory: pyd.DirectoryPath#
- reports: dict[str, DataFrameField]#
- sheet_id: str#
- report(fn_name: str) dict[str, DataFrame]#
Generate a report by calling the specified function.
- property transactions: Iterator[Transaction]#
Yield all transactions across all ledgers.
- static export_repayment_map(**kwargs: dict[str, dict[str, float]]) DataFrame#
Export a nested dict of repayments into a dataframe.
- Returns:
A dataframe with columns
type,from_user,to_user, andamount.
Prepare a dataframe of shared costs for further processing.
Calculate the total amount contributed by each user per month to each cost account.
Calculate the total amount contributed by each user per month.
- split(transactions: Iterable[Transaction]) Iterable[Transaction]#
Split transactions with more than 2 posts into multiple transactions with 2 posts.
- build_tr_dataframe(transactions: Iterable[Transaction]) DataFrame#
Create a dataframe with one row per account pair.
- build_post_dataframe(transactions: Iterable[Transaction]) DataFrame#
Create a dataframe with one row per post (i.e. account), rather than per transaction.
- calculate_contributions(df: DataFrame, users: list[str]) dict[str, dict[str, float]]#
Calculate the net contributions between all users, including the shared account
sh.- Parameters:
df – DataFrame with columns
u0,u1,amnt, andtypeusers – List of all users involved, excluding
sh
- Returns:
A nested mapping of each user to the net amount contributed to every other user, including the shared
shaccount. Each self-contribution is zero.
- settle_debts(edges: dict[str, dict[str, float]], users: list[str]) DataFrame#
Determine how much each user owes to every other user in order to settle all debts.
- Parameters:
edges – Output of
calculate_contributions(), describing the net contributions between all users, includingsh.users – List of all users involved, excluding
sh. This is necessary to determine the split of shared costs, and to separate debtors from creditors.
- overview() dict[str, DataFrame]#
Index and validate all ledgers, reporting a simple summary of transactions & balances.
Generate a report describing the state of “shared” finances between a pair of users.
Works by finding relevant transactions, and then balancing them separately to determine whether any money is owed by either party to the other.
In this situation, relevant transactions are both those that involve the
costs:sh:accounts, and those that are direct transfers between thefunds:accounts of the participantsIt is assumed that the split of costs is even, i.e.
0.5.
- raw() dict[str, DataFrame]#
Output raw transaction histories for each ledger file.
- upload() None#
Upload all .csv files written since last upload to the specified Google Sheet.
- means.cli.report.register(fn: R) R#
Decorator to register a report function in the Reporter registry.
- means.cli.report.parse_args() Namespace#
Parse command-line arguments for the reporting script.
- means.cli.report.main()#
Generate and upload reports.
VIII means-bank — Stage Read-Only Provider Changes#
means-bank separates the networked fetch boundary from deterministic planning and explicit local apply.
Provider credentials and access tokens are injected through environment variables; they are never command-line options.
The generated sidecar is the only Ledger file the command may replace.
The first connection is an explicit human-gated setup operation.
Prepare an owner-only Proton Pass environment manifest containing PLAID_CLIENT_ID, PLAID_SECRET, and PLAID_ENV, then run the wrapper through pass-cli run (pp is an optional shell alias):
pass-cli run --env-file ~/.local/state/means/bank-sync.env.pp -- \
means-bank-pp-connect \
--directory ~/my/self/_0_resources/means \
--connection fictional-primary \
--access-token-env PLAID_ACCESS_FICTIONAL \
--vault Personal \
--item-title plaid_fictional_primary
The wrapper opens Hosted Link, waits for the operator to finish authorization, writes the exchanged access token directly to a new Proton Pass custom item over standard input, and records only its pass:// reference and protected connection identity in owner-only local state.
It refuses reconnect-in-place so a different provider Item cannot silently inherit old source history.
The personal means directory declares only non-secret local policy in parameters/parameters.yaml.
unsupported_accounts is the explicit gap ledger; every account that cannot use the active provider remains visible in status rather than disappearing from coverage:
bank_sync:
state: ~/.local/state/means/bank-sync.sqlite3
manual_ledger: robb.ledger
root_ledger: root.ledger
sidecar: generated/bank-sync.ledger
since: 2026-04-22
unsupported_accounts:
- debts:r:unsupported_card
The recurring workflow is deliberately staged:
Inspect connection readiness and explicit gaps without injecting credentials.
means-bank --directory ~/my/self/_0_resources/means status --jsonA new connection reports
fetch_required; only a historically complete committed fetch makes every configured connectionready.Inject references and fetch provider source changes, then inspect status again.
pass-cli run --env-file ~/.local/state/means/bank-sync.env.pp -- \ means-bank --directory ~/my/self/_0_resources/means fetch --json means-bank --directory ~/my/self/_0_resources/means status --json
List locally observed accounts by opaque handle and bind each one to an existing Ledger account.
means-bank --directory ~/my/self/_0_resources/means accounts --json means-bank --directory ~/my/self/_0_resources/means bind \ --account account-opaque-example \ --ledger-account funds:r:checking
Create an owner-read-only preview without changing the live sidecar.
means-bank --directory ~/my/self/_0_resources/means plan --jsonInspect the returned
preview_path, then apply its exact immutableplan_id.means-bank --directory ~/my/self/_0_resources/means apply \ --plan plan-opaque-example --json
Applying performs no network calls. It rejects unresolved account, transfer, currency, or manual-ledger overlap blockers; changed source or ledger hashes; altered previews; invalid Ledger output; and unknown crash-recovery state. A repeated plan and apply over unchanged source is a no-op.
- class means.cli.bank.ExitCode(*values)#
Stable process exit codes for automation.
- OK = 0#
- USAGE = 2#
- AUTH = 10#
- NETWORK = 11#
- BLOCKED = 12#
- STALE_PLAN = 13#
- VALIDATION = 14#
- LOCKED = 15#
- RECOVERY = 16#
- class means.cli.bank.BankSyncPaths(*, directory: Path, state: Path, manual_ledger: Path, root_ledger: Path, sidecar: Path, since: date, gaps: tuple[str, ...] = ())#
Resolved local paths and projection cutoff.
- directory: Path#
- state: Path#
- manual_ledger: Path#
- root_ledger: Path#
- sidecar: Path#
- since: date#
- gaps: tuple[str, ...]#
- classmethod from_directory(directory: Path) BankSyncPaths#
Load non-secret bank-sync policy from the personal means directory.
- means.cli.bank.build_parser() ArgumentParser#
Build the complete bank-sync command parser.
- means.cli.bank.parse_args(argv: list[str] | None = None) Namespace#
Parse and validate command-line arguments.
- means.cli.bank.connect(args: Namespace, paths: BankSyncPaths) int#
Run the human-gated Plaid Hosted Link bootstrap.
- means.cli.bank.status(args: Namespace, paths: BankSyncPaths) int#
Report counts-only coverage, freshness, and recovery state.
- means.cli.bank.accounts(args: Namespace, paths: BankSyncPaths) int#
List locally stored account metadata without provider identifiers or balances.
- means.cli.bank.bind_account(args: Namespace, paths: BankSyncPaths) int#
Bind one opaque provider account to an existing Ledger account.
- means.cli.bank.fetch(args: Namespace, paths: BankSyncPaths) int#
Fetch every configured connection into the source journal.
- means.cli.bank.plan(args: Namespace, paths: BankSyncPaths) ReconciliationPlan#
Build and persist a network-free reconciliation plan.
- means.cli.bank.apply(args: Namespace, paths: BankSyncPaths) ApplyResult#
Apply one immutable plan without network access.
- means.cli.bank.recover(args: Namespace, paths: BankSyncPaths) list[ApplyResult]#
Recover interrupted sidecar replacements.
- means.cli.bank.main(argv: list[str] | None = None) int#
Run the selected bank-sync operation.
- means.cli.bank_pp.build_parser() ArgumentParser#
Build the explicit Proton Pass secret-bootstrap parser.
- means.cli.bank_pp.main(argv: list[str] | None = None) int#
Authorize one Item, save its token, and record only non-secret local configuration.