bukio-cli

bukio-cli

MCP server for agent-first double-entry bookkeeping for Dutch SMEs, supporting VAT, Peppol BIS 3.0 e-invoicing, and local-first SQLite storage with full audit logging and deterministic JSON output.

Category
访问服务器

README

<div align="center">

bukio-cli

Agent-first double-entry bookkeeping for Dutch SMEs.

VAT-optional · Peppol BIS 3.0-ready · Local-first (SQLite) · MCP-native

License: Apache-2.0 Version Node Tests Peppol MCP

</div>

bukio-cli is a double-entry bookkeeping engine and CLI that runs natively on a VPS, stores everything in one local SQLite file, and is designed so AI agents — not just humans — can operate it safely and auditably. It is built for the Dutch B2B e-invoicing mandate: every invoice ends as a compliant PDF, a Peppol BIS 3.0 UBL document, and a sendable Peppol message.

Status: Phase 5 complete (v0.8.0) — ledger → bank/VAT → invoicing & recurring → jaarrekening → agent layer (MCP). Full pipeline in the Roadmap.

Features

  • Agent-native — every command emits deterministic --json; every mutation supports --dry-run (plan mode); every action lands in an append-only audit log with actor attribution (--actor agent:<name>).
  • VAT optional — the core ledger is VAT-agnostic. The optional VAT module adds codes, the OB readout (fields 1a–5d) and KOR support when you need them. Filing always stays manual — bukio never submits anything.
  • Peppol BIS 3.0 ready — the 2027 mandate in one loop: finalize → PDF → UBL → peppol-send.
  • FX built in — book foreign-currency purchase invoices in USD, GBP, …; rates resolve from your rate store or straight from the ECB.
  • One company per database — a second company is a second SQLite file (--db or BUKIO_DB).
  • Local-first — no cloud, no lock-in. Your 7-year administration stays yours.

Quick start

# install
git clone https://github.com/erikvankempen/bukio-cli.git
cd bukio-cli && npm install && npm link        # exposes `bukio` on PATH

# create a company (dry-run first — it writes nothing)
bukio init --name "Demo BV" --kvk 12345678 --legal-form bv --vat on --dry-run
bukio init --name "Demo BV" --kvk 12345678 --legal-form bv --vat on

# post the opening capital, book an expense, check the books
bukio entry add --date 2026-08-04 --desc "Startkapitaal" \
  --postings "1100:10000.00,3000:-10000.00" --post
bukio entry add --date 2026-08-05 --desc "Kantoorartikelen" \
  --postings "4300:250.00,1100:-250.00" --post
bukio report trial-balance        # → balanced: true

Or let an agent do it — bukio mcp speaks the Model Context Protocol over stdio:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"trial_balance","arguments":{}}}' \
| bukio mcp

Screenshot

<p align="center"> <img src="screenshot.png" alt="bukio-cli demo session" width="720"> </p>

Table of Contents

  1. Features
  2. Quick start
  3. Screenshot
  4. Requirements & Install
  5. Core Concepts
  6. Command Reference
  7. Global Flags
  8. Money Format
  9. Integrity & Safety Model
  10. The Database
  11. Using Agents
  12. Project Layout
  13. Development & Testing
  14. Error Codes
  15. Common Tasks
  16. Roadmap

Requirements & Install

  • Node.js >= 20
  • Linux/macOS (developed on a Linux VPS)
git clone https://github.com/erikvankempen/bukio-cli.git
cd bukio-cli
npm install          # deps: better-sqlite3, commander
npm link             # exposes `bukio` on PATH (or: npm install -g .)
bukio --version

Uninstall: npm unlink -g bukio-cli (or npm uninstall -g bukio-cli).


Core Concepts

Double-entry bookkeeping

Every journal entry contains two or more postings (debits and credits) whose amounts sum to zero. Positive amounts are debits, negative amounts are credits. This invariant is enforced by the engine at creation time and by a database trigger when an entry is posted — an unbalanced posted entry is impossible.

Accounts and the chart of accounts

Accounts are organised in a chart of accounts with 4-digit codes and a type:

Type Normal balance Examples
asset debit 1000 Kas, 1100 Bank, 1200 Debiteuren
liability credit 2000 Crediteuren, 2100 Overige schulden
equity credit 3000 Eigen vermogen
expense debit 4000 Inkoopwaarde, 4100–4500 kosten
income credit 8000 Omzet, 8100 Overige opbrengsten

bukio init seeds a minimal default chart (14 accounts, no VAT accounts — the core is VAT-agnostic). The full RGS (Referentie Grootboekschema) taxonomy import arrives in Phase 1; account codes are RGS-compatible in structure.

Entry lifecycle

draft ──post──▶ posted ──reverse──▶ (original stays posted)
  │                                  + contra-entry posted (negated postings)
  └──reverse── (not allowed)          + audit trail
  • draft — a work-in-progress entry. Postings can be added/changed/removed (via SQL or future commands). Drafts are excluded from reports.
  • posted — final. Postings are immutable (database trigger). Posted entries appear in the trial balance.
  • reverse — reversing a posted entry posts a linked contra-entry with negated postings. The original entry stays posted — the contra-entry cancels it, so the net effect on the books is zero. Linkage: the contra-entry's reversed_from_id points at the original; the audit log records the action. Posted entries are never deleted — they are reversed.

Actors

Every mutation records an actorhuman by default, or agent:<name> when an agent acts (e.g. --actor agent:hermes). Actors appear on entries (created_by) and in the audit log, so a human can always see exactly what an agent did.

The audit log

An append-only log of every mutation: actor, action, command, JSON args, outcome, and affected entry IDs. Database triggers block UPDATE and DELETE — the log cannot be rewritten after the fact. Read it with bukio audit.

Amounts

All money is stored as integer cents (amount_cents). There are no floats anywhere in financial code paths. See Money Format.


Command Reference

Global flags (--json, --db, --actor) can appear before or after the subcommand. See Global Flags.

bukio init

Initialise a company database: creates the file, the company row, and seeds the default chart of accounts.

Option Default Description
--name <name> (required) Company name
--kvk <kvk> KVK number
--legal-form <form> eenmanszaak eenmanszaak | vof | bv | nv | stichting | vereniging
--btw-id <id> BTW identification number
--iban <iban> Bank account (IBAN)
--vat <on|off> off Enable the VAT module (Phase 2)
--kor off Small business scheme — implies --vat off
--fiscal-year-end <mm-dd> 12-31 Fiscal year end
--dry-run off Show the plan without writing anything

Fails with ALREADY_INITIALISED if the database already has a company.

bukio init --name "Demo BV" --kvk 12345678 --legal-form bv --vat on --dry-run
bukio init --name "Demo BV" --kvk 12345678 --legal-form bv --vat on

bukio entry add

Create a journal entry (draft by default; --post posts it immediately).

Option Default Description
--date <yyyy-mm-dd> today Entry date (ISO)
--desc <description> (required) Description
--postings <CODE:AMOUNT> (required) Posting spec — repeat the flag or comma-separate; positive = debit, negative = credit
--source <source> manual manual | bank | invoice | agent
--source-ref <ref> Source reference (e.g. invoice number)
--post off Post immediately (draft → posted)
--dry-run off Validate and show the plan without writing
# two postings, comma-separated
bukio entry add --date 2026-08-04 --desc "Startkapitaal" \
  --postings "1100:10000.00,3000:-10000.00" --post

# equivalent: repeated flag
bukio entry add --desc "Startkapitaal" \
  --postings "1100:10000.00" --postings "3000:-10000.00"

# three postings (VAT-like split is a Phase 2 concern; 3-leg entries work today)
bukio entry add --desc "3-leg example" \
  --postings "1100:121.00,8000:-100.00,2100:-21.00" --dry-run

Validation errors (see Error Codes): INVALID_POSTING, INVALID_AMOUNT, INVALID_DATE, INVALID_DESCRIPTION, TOO_FEW_POSTINGS, UNBALANCED, ACCOUNT_NOT_FOUND, ACCOUNT_INACTIVE, INVALID_AMOUNT_CENTS, INVALID_SOURCE.

bukio entry post

Post a draft entry (draft → posted).

Option Default Description
--id <id> (required) Entry id
--dry-run off Show the plan without writing

The database trigger backstops the invariant: an entry needs >= 2 postings summing to zero before it can be posted.

bukio entry reverse

Reverse a posted entry: posts a linked contra-entry with negated postings. The original stays posted; the contra-entry cancels it (net effect zero). See Core Concepts.

Option Default Description
--id <id> (required) Entry id
--reason <text> Reason, appended to the contra-entry description
--dry-run off Show the planned contra-entry without writing

Fails with NOT_POSTED for drafts and ALREADY_REVERSED if a posted reversal already exists.

bukio entry reverse --id 2 --reason "verkeerde categorie" --dry-run
bukio entry reverse --id 2 --reason "verkeerde categorie"

bukio entry list

List journal entries (newest first).

Option Default Description
--state <state> all draft | posted | reversed
--date-from <yyyy-mm-dd> Earliest date (inclusive)
--date-to <yyyy-mm-dd> Latest date (inclusive)
--limit <n> 100 Max rows

bukio entry show

Show one entry with its full postings.

Option Default Description
--id <id> (required) Entry id

bukio report trial-balance

Per-account debit/credit/net totals from posted entries, with a final BALANCED/UNBALANCED verdict. Drafts and the mirror of reversed entries behave per the lifecycle rules (drafts excluded; contra-entries included — that's what makes reversals net to zero).

Option Default Description
--year <yyyy> all years Filter by year
--format <format> human (json with --json) json | csv | xlsx | human
--out <path> stdout Output file (required for xlsx)

bukio report balans

Balance sheet as of a date, grouped by RGS hoofdgroep (Materiële vaste activa, Voorraden, Vorderingen, Liquide middelen / Eigen vermogen, Kortlopende schulden, …). Includes the computed Nog te verdelen resultaat (net result of income/expense accounts). Invariant: total assets = total liabilities + equity + result — the report says BALANCED or UNBALANCED!.

Option Default Description
--as-of <yyyy-mm-dd> today Balance date (inclusive)
--format <format> human (json with --json) json | csv | xlsx | human
--out <path> stdout Output file (required for xlsx)

bukio report pnl

Winst-en-verliesrekening for a period, grouped by RGS hoofdgroep (Omzet, Inkoopwaarde van de omzet, Personeelskosten, Afschrijvingen, Overige bedrijfskosten, Financiële baten en lasten, …). Reports revenue, costs and Netto resultaat.

Option Default Description
--year <yyyy> current year Fiscal year (sets from/to)
--from <yyyy-mm-dd> year start Period start (inclusive)
--to <yyyy-mm-dd> year end Period end (inclusive)
--format <format> human (json with --json) json | csv | xlsx | human
--out <path> stdout Output file (required for xlsx)

bukio report journal

Journal export — one row per posting with account info, for a period. Ideal for handing to your boekhouder.

Option Default Description
--year <yyyy> current year Fiscal year (sets from/to)
--from <yyyy-mm-dd> year start Period start (inclusive)
--to <yyyy-mm-dd> year end Period end (inclusive)
--format <format> human (json with --json) json | csv | xlsx | human
--out <path> stdout Output file (required for xlsx)
bukio report balans --as-of 2026-12-31
bukio report pnl --year 2026 --format xlsx --out ~/exports/pnl-2026.xlsx
bukio report journal --year 2026 --format csv --out ~/exports/journal-2026.csv

bukio account

Chart of accounts management.

Command Purpose
account add --code <c> --name <n> --type <t> --normal-balance <d|c> [--rgs-code <r>] [--dry-run] Add an account
account list [--type <t>] [--include-inactive] List accounts
account show --code <c> Show one account
account deactivate --code <c> Deactivate (blocks new postings; history stays)
account reactivate --code <c> Reactivate
account import --file <chart.csv> [--dry-run] Import a chart from CSV: code,name,type,normal_balance[,rgs_code]

The bundled default chart lives at assets/chart-nl.csv — you can import it (or your own) into any database:

bukio account import --file assets/chart-nl.csv --dry-run   # validate first
bukio account import --file assets/chart-nl.csv

bukio bank

Bank accounts, import and matching.

Command Purpose
bank add --iban <IBAN> [--name] [--account-code 1100] Register a bank account (links to a ledger account)
bank list Accounts with balance, transaction and unmatched counts
bank import --file <path> --iban <IBAN> [--format camt|csv|auto] [--dry-run] Import transactions — CAMT.053 XML or bank CSV (Rabo/ING/ABN column aliases, Dutch 1.234,56 amounts, Af/Bij sign). Idempotent via SHA-256 hash.
bank transactions [--iban] [--state unmatched|matched|ignored] [--limit] List transactions
bank match auto [--window-days 5] [--dry-run] Auto-match unmatched transactions to posted entries (exact ≤ 2 days, fuzzy ≤ window)
bank match suggest Unmatched transactions with a proposed posting (income → 8000, expense → 4300)
bank match link --tx <id> --entry <id> [--method] Link a transaction to an existing posted entry
bank match post --tx <id> --account <code> [--dry-run] Post a new entry from an unmatched transaction (bank leg + counter leg), reconciled automatically
bank ignore --tx <id> / bank unignore --tx <id> Ignore/re-open a transaction (e.g. transfers between own accounts)

The bank balance vs ledger balance check is the reconciliation test: after matching everything, bank list balance should equal the ledger account balance in the trial balance.

bukio vat

Optional VAT module (per company; KOR companies cannot enable it).

Command Purpose
vat enable [--dry-run] Enable the module: accounts 1500 (te vorderen) + 2500 (te betalen), 8 VAT codes
vat codes List VAT codes (21, 9, 0, V vrijgesteld, R/RE verlegd, M marge, P privé)
vat book --date --desc --postings "1100:121.00,8000:-100.00@21" [--post] [--dry-run] Book a VAT-aware entry. @CODE tags a posting as net; the VAT amount and the VAT ledger leg (2500/1500) are computed automatically.
vat readout --period 2026-Q2 [--mark-filed] OB-aangifte manual-filing readout — fields 1a–5d for the period (quarter YYYY-Qn or month YYYY-MM). bukio never files; you enter these amounts in Mijn Belastingdienst Zakelijk. --mark-filed records the filing.
# sale: 121.00 incl 21% -> omzet 100 + te betalen btw 21
bukio vat book --date 2026-06-01 --desc "Factuur 2026-001" \
  --postings "1100:121.00,8000:-100.00@21" --post

# purchase: 60.50 incl 21% -> kosten 50 + te vorderen btw 10.50
bukio vat book --date 2026-06-05 --desc "Kantoorartikelen" \
  --postings "4300:50.00@21,1100:-60.50" --post

# quarterly manual filing aid
bukio vat readout --period 2026-Q2

OB field mapping: 1a/1b/1c omzet (21%/9%/0%/vrijgesteld), 1d privégebruik, 3a/3b/3c inkopen, 4a/4b verlegde btw (binnenland/EU, netted via 5b), 5a verschuldigde btw, 5b voorbelasting, 5d te betalen/te ontvangen. Fields 2a/2b (exports) and 5c are not tracked in Phase 2 (shown as 0).

bukio recurring / bukio depreciation

Recurring entries & period automation (FR3A) — deterministic, dry-run first, fully audited. Templates are validated at creation; generation just replays them. bukio never generates entries on its own: the agent or a cron job triggers run --due.

Command Purpose
recurring add --name N --postings "CODE:AMT,..." --frequency monthly|quarterly|yearly --start YYYY-MM-DD [--day 1-28] [--end] [--runs] [--reverse-previous] [--dry-run] Create a recurring entry template (VAT-aware via @CODE tags; expanded at creation)
recurring add --name N --kind invoice --contact N --lines "2x DESC @ PRICE @21" --frequency monthly --start YYYY-MM-DD [--due-days 30] Create a subscription invoice template — each run generates a DRAFT invoice (never auto-finalizes; the agent finalizes)
recurring list [--status active|paused|completed|all] / show --id Inspect templates
recurring pause --id / resume --id Control scheduling
recurring preview [--as-of DATE] [--template ID] What is due (read-only plan)
recurring run [--as-of DATE] [--template ID] [--dry-run] Generate all due entries/invoice drafts — backfills missed periods, idempotent, one transaction per template (a failing template is reported and skipped, others still run)
depreciation add --name N --cost C --life-months M --start DATE [--asset 1800] [--expense 4600] [--residual 0] [--dry-run] Linear monthly depreciation with a remainder-adjusted final run (cents-exact total over the asset life)

Semantics:

  • Generated entries: source='recurring', source_ref='tpl:<id>', created_by='recurring' (the trigger actor is in the audit log). Posted, immutable, reversible like any entry.
  • --reverse-previous implements the accrual pattern: each run first reverses the previous generated entry (contra-entry dated at the original), then books the new one — monthly estimates replace cleanly, each month carries its own amount.
  • --runs / --end complete the template (status completed); a completed template cannot be re-activated.
  • First run is normalized to --day (never backwards); days 29–31 are rejected to avoid month-end clamping.
# depreciation: 5370.00 over 36 months -> 149.17/mo, final 149.05 (total exactly 5370.00)
bukio depreciation add --name "Laptop Dell" --cost 5370.00 --life-months 36 --start 2026-08-01
# accrual with auto-reversal (nog te betalen kosten, monthly estimates)
bukio recurring add --name "Nog te betalen kosten admin" \
  --postings "4310:250.00,2400:-250.00" --frequency monthly --start 2026-08-31 --day 28 --reverse-previous
# prepaid spreading: annual insurance over 12 months
bukio recurring add --name "Verzekering 12 mnd" \
  --postings "4320:100.00,1700:-100.00" --frequency monthly --start 2026-08-01 --runs 12
# the agent's month-end: preview, then run
bukio recurring preview --as-of 2026-09-30
bukio recurring run --as-of 2026-09-30
# subscription invoices: run generates DRAFT invoices, then the agent finalizes
bukio recurring add --name "SaaS abonnement" --kind invoice --contact 1 \
  --lines "2x Premium SaaS @ 99.00 @21" --frequency monthly --start 2026-08-01 --due-days 14
bukio recurring run --as-of 2026-10-31        # -> draft invoices 2026-08/09/10
bukio invoice finalize --id 1                 # -> 2026-0001, booked
bukio invoice peppol-send --id 1 --dry-run    # Peppol access-point (env creds)

bukio contact / bukio invoice

Outgoing invoicing (FR3) — compliant with the 12 verplichte factuurvereisten, lifecycle draft → sent → paid (overdue derived), credit notes, PDF + UBL export, bank payment matching.

Command Purpose
contact add --name N [--address] [--postal-code] [--city] [--vat-id] [--kvk] [--email] Add a customer (vat-id required when btw verlegd)
contact list List contacts
invoice create --contact <id> --lines "2x Consultancy @ 150.00 @21,1x Rapportage @ 400.00 @9" --date YYYY-MM-DD [--due-days 30] [--reference] [--dry-run] Create a draft invoice (line spec: [QTYx] DESC @ PRICE [@ VATCODE])
invoice finalize --id N [--dry-run] Assign the sequential number (YYYY-NNNN) and book the entry (Debiteuren / Omzet / Te betalen btw)
invoice list [--status] [--type] / show --id Inspect invoices
invoice pdf --id N [--out PATH] Render a compliant PDF via headless Chromium
invoice ubl --id N [--out PATH] Export UBL 2.1 / Peppol BIS 3.0 (EN 16931) XML
invoice credit --id N [--reason] Create a credit note (draft) from a finalized invoice
invoice pay --id N --date [--amount] Record a payment (tracking; the posting comes from the bank flow)

Compliance (validated at finalize): supplier name/KvK/btw-id/address/postal/city (set at init), invoice date, sequential number, customer name+address+city, line descriptions/quantities/prices, VAT rate + amount per rate, totals, and the customer's btw-id when a line carries @R/@RE (btw verlegd). Missing data fails with SUPPLIER_INCOMPLETE / CUSTOMER_INCOMPLETE / CUSTOMER_VAT_REQUIRED.

Payment matching: bank match auto now recognizes incoming payments against unpaid sales invoices (exact outstanding amount, oldest due first) — it marks the invoice paid, posts Bank/Debiteuren, and reconciles the transaction. The OB readout picks up invoiced sales automatically.

bukio invoice create --contact 1 --date 2026-07-10 \
  --lines "2x Consultancy @ 150.00 @21,1x Rapportage @ 400.00 @9" --reference "PO-2026-88"
bukio invoice finalize --id 1 --dry-run      # plan: number + postings
bukio invoice finalize --id 1                # -> 2026-0001, entry posted
bukio invoice pdf --id 1                     # 2026-0001.pdf
bukio invoice ubl --id 1                     # 2026-0001.xml (Peppol BIS 3.0)
# payment arrives -> the bank import matches it automatically
bukio bank import --file stmt.xml --iban NL91ABNA0417164300
bukio bank match auto                        # tx -> invoice 2026-0001 (paid)

bukio year-end / bukio jaarrekening / bukio icp

Annual close and statutory reporting (Phase 4).

Command Purpose
year-end status --year YYYY Open/closed, the year's result, per-account nets
year-end close --year YYYY [--dry-run] Close the fiscal year: reverse income/expense into 9900 (created on demand), then resultaatbestemming into 3000. Both entries source='closing', source_ref='fy:YYYY'. Guards: draft entries in the year (INCOMPLETE_YEAR), double close (ALREADY_CLOSED), no activity (EMPTY_YEAR). The P&L report excludes closing entries — the year's flow stays visible after closing; the balans then shows equity including the result
jaarrekening report --year YYYY --model micro|klein [--format json|pdf|xlsx] [--out] Statutory annual accounts in the Dutch layout (Titel 9 Boek 2 BW): balans (vaste activa / vlottende activa / eigen vermogen / voorzieningen / lang- en kortlopende schulden) + W&V (klein model). --format pdf = the KVK deposit package; xlsx for the accountant
icp readout --period YYYY-Qn ICP listing: EU btw-verlegde supplies per customer (from RE invoice lines), with their btw-ids. Fails ICP_VAT_ID_MISSING if a customer lacks one. Credit notes reduce the customer total
bukio year-end status --year 2026
bukio year-end close --year 2026 --dry-run     # plan: result 1254.15 + postings
bukio year-end close --year 2026               # entries #9 #10 posted
bukio jaarrekening report --year 2026 --model klein        # JSON
bukio jaarrekening report --year 2026 --model klein --format pdf   # jaarrekening-2026-klein.pdf (KVK)
bukio icp readout --period 2026-Q3             # EU customers + amounts

OB readout fields (Phase 4): 1a/1b/1c omzet (21%/9%/0%-vrijgesteld), 1d privégebruik (21% auto-computed on @P), 2a verlegde EU leveringen (RE), 3a inkopen binnenland (incl. verlegd @R), 3b inkopen EU (RE), 3c buiten EU, 4a/4b verlegde btw, 5a verschuldigd, 5b voorbelasting, 5d te betalen/te ontvangen. 2b and 5c are not tracked.

bukio mcp / bukio fx / bukio compliance

The agent layer (Phase 5).

Command Purpose
mcp MCP server over stdio (JSON-RPC 2.0, newline-delimited): company_info, trial_balance, balans, pnl, journal, accounts, vat_readout, icp_readout, audit, compliance, invoices (read-only) + entry_add/post/reverse, vat_book, invoice_create/finalize/credit/pay, recurring_run, year_end_close, fx_set, contact_add (mutations). Mutations are plan-only unless mode:"execute"; BUKIO_MCP_READONLY=1 blocks execution entirely. Every execute books with the caller's actor and lands in the audit log. NL query = an agent on top of these tools
fx set --currency USD --date D --rate 1.0875 Store a rate (1 EUR = N units of foreign currency, 4 decimals max). Upsert; audited
fx fetch --currency USD [--date D] Fetch the ECB reference rate (free, no key) for a currency on/before a date and store it (source ECB). Weekends/holidays fall back to the last business day; unknown currency or pre-1999 → ECB_RATE_NOT_AVAILABLE
entry add / vat book --currency USD [--rate R] Foreign-currency purchase invoices: spec amounts are in the foreign currency, converted to EUR (round-half-up) at booking; the rate is auto-looked-up (exact date, else latest on/before) when --rate is omitted. Missing rates are fetched live from the ECB and stored for reuse — one network call ever per currency/date. The ledger stores EUR; each posting keeps fx_currency/fx_amount_cents (the original amount) — reversals negate both. VAT legs are computed on the EUR amounts. BUKIO_FX_NO_FETCH=1 disables the network fallback (offline/air-gapped use)
compliance status --year YYYY OB + ICP quarterly deadlines and the jaarrekening deposit (13 months after FY end, art. 2:394 BW) with filed/open/overdue status; compliance mark --type ICP|JAARREKENING --period ... records a filing (OB uses vat readout --mark-filed)
bukio fx set --currency USD --date 2026-07-03 --rate 1.0875
bukio fx fetch --currency GBP --date 2026-08-03          # ECB reference rate, stored
bukio vat book --date 2026-08-01 --desc "Stripe (USD)" --currency USD \
  --postings "4300:895.00@21,1100:-1082.95" --post      # 779.28 EUR — rate auto-fetched from the ECB
# koersverschil at payment: book the difference on 4700 (created on demand)
bukio account add --code 4700 --name "Koersverschillen" --type expense --normal-balance debit
printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"entry_add","arguments":{"date":"2026-07-31","description":"Huur","postings":["4300:800.00","1100:-800.00"],"mode":"execute","actor":"agent:hermes"}}}' \
| bukio mcp           # or wire it into an MCP client (Hermes, Claude Code, ...)
bukio compliance status --year 2026

FX booking rules: amounts in posting specs are foreign currency; the rate resolves as --rate → stored rate (exact, else latest on/before) → ECB reference rate (fetched live, stored as source ECB for reuse). --rate always wins; BUKIO_FX_NO_FETCH=1 keeps bukio fully offline. The description should note the currency and the original invoice number. Outgoing invoices stay EUR-only (the 12-vereisten and UBL are EUR-based).

bukio backup / bukio restore

Command Purpose
backup [--out <path>] Consistent SQLite backup (default ~/.bukio/backups/bukio-<ts>.db)
restore --from <file> [--to <path>] [--force] Restore from a backup file (validated first)

restore refuses to overwrite an existing initialised database unless --force is given, and refuses --from/--to pointing at the same file.

bukio backup
bukio restore --from ~/.bukio/backups/bukio-2026-08-04T12-00-00.db --to ~/.bukio/restored.db

bukio audit

Read the append-only audit log (newest first).

Option Default Description
--since <iso-ts> Only entries at/after this timestamp (ISO 8601)
--by <who> all Only entries by this actor (e.g. agent:hermes)
--limit <n> 50 Max rows
bukio audit --by agent:hermes --json   # what did the agent do?
bukio audit --since 2026-08-01         # everything this month

Global Flags

Flag Env var Default Description
--json off Machine-readable JSON output (see below)
--db <path> BUKIO_DB ~/.bukio/bukio.db Database file
--actor <who> BUKIO_ACTOR human Acting entity — use agent:<name> when an agent acts

JSON output contract

With --json, every command prints exactly one JSON document to stdout and exits 0 on success, 1 on failure:

// success
{ "ok": true, "data": { ... } }

// failure
{ "ok": false, "error": { "code": "UNBALANCED", "message": "postings do not sum to zero (sum = 1 cents)" } }

All amounts appear both as integer cents (amount_cents) and formatted strings (amount: "1234.56"). The schema is stable and versioned with the tool — agents can rely on it.


Money Format

  • Strict international decimal: 1234.56, max 2 decimals, no thousands separators.
  • 1234 = 123400 cents; 0.5 = 50 cents.
  • Positive = debit, negative = credit. A balanced entry's signed amounts sum to zero.
  • Thousands separators are rejected on purpose (1.234 is an error, not 1234) — ambiguity is the enemy of agents.

Integrity & Safety Model

Guarantee Enforced by
Postings sum to zero Engine (creation, in-transaction) + DB trigger (at post time)
An entry needs >= 2 postings Engine + DB trigger (at post time)
No zero-amount postings Engine + CHECK (amount_cents != 0)
Account codes are 1–6 digits Engine
Account type ↔ normal balance consistency CHECK constraint
Postings of a non-draft entry are immutable DB triggers (INSERT/UPDATE/DELETE)
Posted entries are never deleted Reversal-only workflow + triggers
Audit log is append-only DB triggers (UPDATE/DELETE abort)
Money has no floats Integer cents only, strict parser
Single company per database CHECK (id = 1) on company

Backup: the database is a single SQLite file (WAL mode). Copy it while the CLI is not writing, or use the .backup API / sqlite3 .backup:

sqlite3 ~/.bukio/bukio.db ".backup ~/backups/bukio-$(date +%F).db"

A built-in bukio backup/restore lands in Phase 1.


The Database

  • Engine: SQLite (via better-sqlite3), WAL mode, foreign keys on.
  • Location: ~/.bukio/bukio.db by default; override with --db or BUKIO_DB.
  • Migrations: numbered .sql files in migrations/, applied in order, tracked via PRAGMA user_version.

Schema summary (see migrations/001_initial.sql for the authoritative DDL):

company           — one row (id must be 1): name, kvk, legal_form, btw_id, iban,
                    vat_module, kor_flag, fiscal_year_end
accounts          — chart of accounts: code, name, type, rgs_code, normal_balance, active
journal_entries   — date, description, source, source_ref, state, reversed_from_id,
                    created_by, created_at, posted_at
postings          — entry_id, account_id, amount_cents, document_id
audit_log         — ts, actor, action, command, args_json, outcome, entry_ids

Using Agents

bukio-cli is built for agents. The companion file AGENTS.md in the repo root is the agent's manual: invariants, exact command/JSON contracts, error codes, and worked examples (opening the month, correcting mistakes). Agents should read AGENTS.md before driving the tool, and follow the house rules:

  1. Always --dry-run before mutating. Show the plan, then apply.
  2. Always pass --actor agent:<your-name> so the audit trail attributes your work.
  3. Prefer --json for parsing; keep human-readable output for humans.
  4. Never edit the SQLite file directly. Use the CLI/engine — the triggers and audit log exist for a reason.
  5. Never delete a posted entry. Reverse it.
  6. Verify after every mutation (e.g. report trial-balance --json must say balanced: true).

Project Layout

bukio-cli/
├── bin/bukio.js           # CLI entry point
├── src/
│   ├── cli/               # commander commands (init, entry, report, audit, util)
│   ├── core/              # db, accounts, chart, entries (posting engine), money
│   ├── audit/             # append-only audit log
│   └── report/            # trial balance
├── migrations/            # numbered SQL migrations (001_initial.sql)
├── test/                  # node:test suites (unit + CLI end-to-end)
├── AGENTS.md              # agent manual — read before driving the tool
└── README.md

Development & Testing

npm test          # node --test — discovers test/*.test.js

The suite covers: money parsing, posting engine invariants, reversal semantics, DB triggers (balance, immutability, append-only audit), trial balance math, and end-to-end CLI flows against temporary databases.


Error Codes

Code Meaning
NO_DATABASE No database at the path — run bukio init first
ALREADY_INITIALISED The database already has a company
INVALID_LEGAL_FORM Unknown legal form for init
INVALID_FISCAL_YEAR_END Fiscal year end must be mm-dd
INVALID_RGS_CODE RGS code does not match the expected format (e.g. BMVA.02)
INVALID_CSV_HEADER / EMPTY_CSV Chart CSV missing required columns or empty
ALREADY_ACTIVE / ALREADY_INACTIVE Account already in that state
INVALID_AMOUNT Amount string not parseable (see Money Format)
INVALID_AMOUNT_CENTS Posting amount is not a non-zero integer
INVALID_POSTING Posting spec is not CODE:AMOUNT
INVALID_DATE Date is not yyyy-mm-dd or not a real calendar date
INVALID_DESCRIPTION Description is empty
INVALID_SOURCE Unknown source (manual/bank/invoice/agent only)
INVALID_ACTOR Actor is empty
TOO_FEW_POSTINGS Fewer than 2 postings
UNBALANCED Postings do not sum to zero
ACCOUNT_NOT_FOUND Account code does not exist
ACCOUNT_INACTIVE Account exists but is inactive
ACCOUNT_EXISTS Account code already exists (account creation, Phase 1)
INVALID_CODE / INVALID_NAME / INVALID_TYPE / INVALID_NORMAL_BALANCE / INVALID_COMBINATION Account validation (Phase 1 surface)
NOT_FOUND Entry id does not exist
ALREADY_POSTED Entry is already posted
NOT_POSTED Entry must be posted first (reversal)
ALREADY_REVERSED A posted reversal already exists for this entry
OUT_REQUIRED --out <path> is required for xlsx output
FILE_NOT_FOUND Backup file does not exist
INVALID_BACKUP File is not a valid bukio database
RESTORE_EXISTS Target already has a company — pass --force
SAME_FILE Restore source and target are the same file
INVALID_IBAN IBAN is malformed
INVALID_CAMT / EMPTY_STATEMENT CAMT.053 XML invalid or empty
INVALID_CSV_HEADER / EMPTY_CSV Bank/chart CSV missing required columns or empty
INVALID_FORMAT Unknown --format for bank import
NOT_FOUND (bank) Bank transaction does not exist
ALREADY_MATCHED Bank transaction already matched/ignored
VAT_MODULE_OFF VAT module not enabled for this company (vat enable first)
KOR_ACTIVE KOR company cannot enable the VAT module
VAT_CODE_NOT_FOUND @CODE references an unknown VAT code
VAT_MARGIN_NOT_SUPPORTED Margeregeling cannot be split automatically
INVALID_PERIOD Period must be YYYY-Qn or YYYY-MM
INVALID_FREQUENCY / INVALID_DATE / INVALID_RANGE Recurring template schedule invalid
INVALID_RUNS / INVALID_COST / INVALID_RESIDUAL / INVALID_LIFE Depreciation parameters invalid
ALREADY_COMPLETED A completed recurring template cannot be re-activated
RECURRING_ERROR A template failed during recurring run (reported per-template, others continue)
SUPPLIER_INCOMPLETE / CUSTOMER_INCOMPLETE Invoice missing supplier/customer vereisten — set them at init / contact add
CUSTOMER_VAT_REQUIRED btw verlegd line needs the customer's btw-id
INVALID_LINE / NO_LINES / CONTACT_NOT_FOUND Invoice line/contact validation
ALREADY_FINALIZED / NOT_FINALIZED Invoice lifecycle violations
OVERPAYMENT / NOT_PAYABLE / CREDIT_NOT_PAYABLE Payment validation
PDF_UNAVAILABLE Playwright/Chromium could not render the invoice PDF
PEPPOL_NOT_CONFIGURED / PEPPOL_SEND_FAILED Peppol provider missing (env BUKIO_PEPPOL_ENDPOINT) or rejected the document
INVALID_KIND / INVALID_REVERSE Recurring template kind errors (reverse-previous is entry-only)
INCOMPLETE_YEAR / ALREADY_CLOSED / EMPTY_YEAR / INVALID_YEAR Year-end close guards
INVALID_MODEL jaarrekening model must be micro or klein
ICP_VAT_ID_MISSING EU customer without a btw-id — the ICP listing cannot be completed
FX_RATE_NOT_FOUND / INVALID_RATE / INVALID_CURRENCY / INVALID_FX_AMOUNT / INVALID_FX_CURRENCY FX booking errors (missing rate, malformed rate/currency/amount)
ECB_FETCH_FAILED / ECB_RATE_NOT_AVAILABLE ECB unreachable, or no reference rate for the currency/date (unknown currency, pre-1999)
MCP_READONLY A mutation was attempted on a read-only MCP server (BUKIO_MCP_READONLY=1)
INVALID_TYPE / INVALID_PERIOD compliance mark errors
SQLITE_CONSTRAINT_TRIGGER A database trigger aborted the operation (e.g. editing a posted entry, rewriting the audit log)

Common Tasks

Open a company's books

bukio init --name "Demo BV" --kvk 12345678 --legal-form bv --vat on
bukio entry add --desc "Startkapitaal" --postings "1100:10000.00,3000:-10000.00" --post

Book an expense (paid from the bank account)

bukio entry add --desc "Kantoorartikelen" --postings "4300:250.00,1100:-250.00" --post

Book sales (money received, income)

bukio entry add --desc "Factuur 2026-001" --postings "1100:1210.00,8000:-1210.00" --post

Correct a mistake — reverse, then book correctly:

bukio entry reverse --id 2 --reason "verkeerde categorie"
bukio entry add --desc "Kantoorartikelen (gecorrigeerd)" --postings "4200:250.00,1100:-250.00" --post

Month-end sanity check

bukio report trial-balance --year 2026 --json   # must be balanced: true
bukio report balans --as-of 2026-12-31          # must say BALANCED
bukio report pnl --year 2026                    # result = revenue - costs
bukio audit --since 2026-08-01 --by agent:hermes

Hand the year to your boekhouder

bukio report journal --year 2026 --format xlsx --out ~/exports/journal-2026.xlsx
bukio report balans --as-of 2026-12-31 --format csv --out ~/exports/balans-2026.csv
bukio report pnl --year 2026 --format xlsx --out ~/exports/pnl-2026.xlsx

Month-end close with bank + VAT (the real workflow)

# 1. import the bank statement (idempotent — safe to re-run)
bukio bank import --file ~/exports/rabo-2026-06.camt.xml --iban NL91ABNA0417164300
# 2. dry-run the auto-match, then apply
bukio bank match auto --dry-run
bukio bank match auto
# 3. handle the leftovers: suggest -> post or link
bukio bank match suggest
bukio bank match post --tx 17 --account 4300
# 4. the balance check: bank balance must equal the ledger balance
bukio bank list
bukio report trial-balance --json          # must be balanced: true
# 5. VAT quarter: read the OB fields, file manually in Mijn Belastingdienst
bukio vat readout --period 2026-Q2
bukio vat readout --period 2026-Q2 --mark-filed

Protect the books

bukio backup                              # ~/.bukio/backups/bukio-<ts>.db
bukio restore --from ~/.bukio/backups/bukio-....db --to ~/.bukio/test-restore.db

Extend the chart of accounts

bukio account add --code 4350 --name "Reiskosten" --type expense --normal-balance debit --rgs-code WBED.42
bukio account import --file assets/chart-nl.csv --dry-run

Run two companies — separate databases:

bukio --db ~/.bukio/bv-a.db init --name "BV A" --legal-form bv
bukio --db ~/.bukio/bv-b.db init --name "BV B" --legal-form bv

KOR / non-VAT entity — simply omit the VAT module; the ledger never exposes VAT concepts:

bukio init --name "Mijn ZZP" --kor

Roadmap

Phase Scope Status
0 Foundation: ledger, posting engine, audit, trial balance, --json/--dry-run ✅ done
1 Accounts CRUD + CSV import, RGS-mapped chart, balans + W&V, CSV/XLSX export, backup/restore ✅ done
2 Bank import (CAMT.053/CSV), matching; optional VAT module (codes, OB readout, KOR) ✅ done
3 Invoicing: factuurvereisten, PDF (Playwright), UBL/Peppol BIS 3.0, credit notes, payment matching, recurring entries + recurring invoices + Peppol send Compliant invoice PDF + UBL per invoice; due entries generated & posted on time
4 Jaarrekening micro/klein models, closing entries, KVK package, ICP readout Jaarrekening package for a micro BV — ✅ done (v0.7.0, 178 tests green)
5 Agent layer: MCP server, permissions/approval gates, NL query, AI categorization suggestions, compliance calendar, FX translation Agent closes a month end-to-end with zero unsupervised mutations — ✅ done (v0.8.0, 199 tests green)
6 Optional: Ponto live feeds, Peppol send/receive, OCR, SQLCipher optional

Design principles persist across phases: agent-native from day one, VAT optional, no automated tax filing, single company per database, local-first.


Part of the Bukio venture. PRD: ~/memos/bukio/bukio-cli-prd.md. Separate product line from the Bukio web platform — shared brand and philosophy, no shared code.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选