# QuidClaw

Zero-barrier personal CFO — Claude Code Plugin powered by Beancount V3.

## Architecture

### Layered Design

- `src/quidclaw/core/` — Pure business logic. Depends on Beancount, NOT on MCP. Every financial operation lives here.
- `src/quidclaw/mcp_server/` — Thin MCP adapter. Translates MCP tool calls into core function calls. No business logic here.

This separation exists so we can add other adapters (e.g., OpenClaw) without touching core logic.

### Plugin Components

QuidClaw is a Claude Code Plugin with 5 component types:

| Component | Location | Purpose |
|-----------|----------|---------|
| **Skills** | `skills/` | AI workflows — onboarding, bill import, reconciliation, reports, anomaly detection |
| **Agents** | `agents/` | Specialized subprocesses — bill parsing, document organization |
| **Hooks** | `hooks/` | Automation triggers — inbox check on session start |
| **MCP Server** | `src/quidclaw/` | Data operation tools (16 tools), resources (2), prompts (3) |
| **Plugin Manifest** | `.claude-plugin/` | Plugin metadata |

## Key Files

| File | Purpose |
|------|---------|
| `src/quidclaw/config.py` | `QuidClawConfig` dataclass — data directory paths |
| `src/quidclaw/core/ledger.py` | `Ledger` — init, load, append to .bean files |
| `src/quidclaw/core/accounts.py` | `AccountManager` — open/close/list accounts |
| `src/quidclaw/core/transactions.py` | `TransactionManager` — add transactions to monthly files |
| `src/quidclaw/core/balance.py` | `BalanceManager` — balance queries and assertions |
| `src/quidclaw/core/reports.py` | `ReportManager` — BQL queries, monthly summaries, category breakdowns |
| `src/quidclaw/core/anomaly.py` | `AnomalyDetector` — duplicates, subscriptions, outliers, unknown merchants |
| `src/quidclaw/core/prices.py` | `PriceManager` — write price directives |
| `src/quidclaw/core/init.py` | `LedgerInitializer` — default account templates |
| `src/quidclaw/core/inbox.py` | `InboxManager` — list/move inbox files, data status |
| `src/quidclaw/core/notes.py` | `NotesManager` — knowledge base: CRUD, search, tags, cross-reference |
| `src/quidclaw/mcp_server/server.py` | `create_server()` — FastMCP instance with lifespan |
| `src/quidclaw/mcp_server/tools.py` | MCP Tool definitions (16 tools) |
| `src/quidclaw/mcp_server/prompts.py` | MCP Prompts (personality, guides) |
| `src/quidclaw/mcp_server/resources.py` | MCP Resources (accounts, config) |

## MCP Tools (16 total)

### Setup (1)
| Tool | Purpose |
|------|---------|
| `set_data_dir` | Set the data directory for this session |

### Ledger Operations (10)
| Tool | Purpose |
|------|---------|
| `init_ledger` | Initialize ledger with account template |
| `add_account` | Open a new account |
| `close_account` | Close an account |
| `list_accounts` | List accounts (optionally filtered) |
| `add_transaction` | Record a transaction |
| `get_balance` | Query account balance |
| `balance_check` | Reconciliation assertion |
| `query` | Execute BQL query |
| `report` | Generate financial report |
| `fetch_prices` | Fetch and record asset prices |

### Insights & Anomaly Detection (5)
| Tool | Purpose |
|------|---------|
| `monthly_summary` | Income, expenses, savings for a month |
| `spending_by_category` | Ranked category breakdown for a month |
| `month_comparison` | Month-over-month changes with percentages |
| `largest_transactions` | Top N biggest expense transactions |
| `detect_anomalies` | Run all anomaly checks (duplicates, subscriptions, outliers, unknown) |

## Tech Stack

- Python 3.10 – 3.13
- `beancount` V3 (3.x) — accounting engine
- `beanquery` — BQL query execution
- `beanprice` — price fetching (optional)
- `mcp` — MCP Python SDK (FastMCP)
- `pyyaml` — YAML frontmatter parsing for notes

## Development

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest                            # run all tests
pytest tests/core/                # core logic tests only
pytest tests/mcp_server/          # MCP adapter tests only
pytest tests/test_integration.py  # end-to-end workflow
pytest tests/e2e/ -v -m e2e      # E2E tests (slow, uses Claude API)
```

## E2E Testing

E2E tests simulate real users by running `claude -p` with the QuidClaw plugin and verifying the resulting data state.

```bash
# Run E2E tests (requires claude CLI, uses API)
pytest tests/e2e/ -v -m e2e --timeout=180

# Run specific scenario
pytest tests/e2e/test_dedup.py -v --timeout=180
```

E2E test structure:
- `tests/e2e/fixtures/` — Simulated bank statements and receipts (CSV, text)
- `tests/e2e/helpers.py` — `run_claude()`, `load_ledger()`, assertion helpers
- `tests/e2e/conftest.py` — `data_dir` and `data_dir_with_accounts` fixtures
- `tests/e2e/test_*.py` — 6 test groups: import, dedup, reconcile, query, onboarding, organize

Key principle: **Don't check what AI says. Check what AI does.** Verify .bean file entries, directory structure, and balances — not AI text output.

## Data Directory

No global default. QuidClaw uses the current working directory as the data directory (directory-as-project model, like git). Override: `QUIDCLAW_DATA_DIR` env var.

To use a custom directory, set the env var before starting Claude Code:
```bash
export QUIDCLAW_DATA_DIR=~/my-finances
```

```
my-finances/
├── ledger/                # Beancount ledger (clean, structured)
│   ├── main.bean          #   includes all other files
│   ├── accounts.bean      #   Open/Close directives
│   ├── prices.bean        #   Price directives
│   └── YYYY/YYYY-MM.bean  #   Transactions by month
├── inbox/                 # Drop zone — user puts files here
├── documents/             # Organized archive (AI-managed)
│   └── YYYY/MM/           #   Filed by year and month
├── notes/                 # Financial knowledge base (AI-managed)
│   ├── profile.md         #   User profile (living)
│   ├── calendar.md        #   Payment calendar (living)
│   ├── assets/            #   Property, vehicles, investments (living)
│   ├── liabilities/       #   Loans, debts (living)
│   ├── insurance/         #   Policy details (living)
│   ├── accounts/          #   Bank/card details (living)
│   ├── subscriptions/     #   Recurring charges (living)
│   ├── income/            #   Income sources (living)
│   ├── decisions/         #   Decision log (append-only)
│   └── journal/           #   Conversation captures (append-only)
└── reports/               # Generated reports (AI-managed)
```

## Notes: Two Types

- **Living documents** (`profile.md`, `assets/*.md`, `calendar.md`, etc.) — always reflect current state. Update "Current Status" section, append to "History" section.
- **Append-only logs** (`decisions/*.md`, `journal/*.md`) — historical record, only grows. Each entry starts with date.

## How to Add a New MCP Tool

1. Add core logic in `src/quidclaw/core/<module>.py` with tests in `tests/core/`
2. Add tool wrapper in `src/quidclaw/mcp_server/tools.py` inside `register_tools()`
3. Pattern:
   ```python
   @mcp.tool()
   async def my_tool(ctx: Context, param: str) -> str:
       app = ctx.request_context.lifespan_context  # -> AppContext
       mgr = SomeManager(app.config)  # or app.ledger
       result = mgr.some_method(param)
       return format_result(result)
   ```

## How to Add a New Skill

1. Create `skills/<skill-name>/SKILL.md`
2. Add frontmatter with `name` and `description` (description controls when AI auto-triggers it)
3. Write the workflow instructions in the body

## How to Add a New Agent

1. Create `agents/<agent-name>.md`
2. Add frontmatter with `name` and `description`
3. Write the agent's system prompt in the body

## Conventions

- Core classes that operate on ledger data take a `Ledger` instance in their constructor
- Core classes that operate on files (inbox, notes) take a `QuidClawConfig` instance
- Beancount directives are written as plain text strings appended to .bean files
- Transactions go into monthly files (`ledger/YYYY/YYYY-MM.bean`), auto-included in `main.bean`
- Multi-currency open directives use comma-separated currencies: `2026-01-01 open Assets:Bank CNY,USD`
- Account naming: use distinguishing identifiers for all accounts (users may have multiple per platform)
  - Banks: last 4 digits — `Assets:Bank:CMB:1234`, `Liabilities:CreditCard:BOC:5678`
  - Payment apps: phone last 4 or nickname — `Assets:WeChat:1234`, `Assets:Alipay:Main`
  - Investments: platform + identifier — `Assets:Investment:Futu:1234`
  - Always ask "do you have more than one?" before assuming single account
- Tests use `tmp_path` fixture for isolated data directories
- Use `ToolError` (from `mcp.server.fastmcp.exceptions`) for user-facing errors in MCP tools
- FastMCP uses `instructions=` not `description=` in constructor
- Document naming convention: `{Source}-{Type}-{YYYY-MM}.{ext}`

## Beancount V3 API Notes

```python
from beancount import loader
entries, errors, options = loader.load_file("path.bean")

from beancount.core import data
# data.Open, data.Close, data.Transaction, data.Balance, data.Price

from beancount.core import realization
real_root = realization.realize(entries)
real_account = realization.get(real_root, "Assets:Bank:BOC")
# real_account.balance -> Inventory of positions
# realization.iter_children(real_root) -> yields RealAccount objects (not tuples)

from beanquery.query import run_query
columns, rows = run_query(entries, options, "SELECT ...")
# columns: tuple of Column objects with .name attribute
# rows: list of tuples
# BQL queries operate at posting level — use DISTINCT to deduplicate
```
