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.
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
</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 (
--dborBUKIO_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
- Features
- Quick start
- Screenshot
- Requirements & Install
- Core Concepts
- Command Reference
- Global Flags
- Money Format
- Integrity & Safety Model
- The Database
- Using Agents
- Project Layout
- Development & Testing
- Error Codes
- Common Tasks
- 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_idpoints at the original; the audit log records the action. Posted entries are never deleted — they are reversed.
Actors
Every mutation records an actor — human 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-previousimplements 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/--endcomplete the template (statuscompleted); 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.234is 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.dbby default; override with--dborBUKIO_DB. - Migrations: numbered
.sqlfiles inmigrations/, applied in order, tracked viaPRAGMA 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:
- Always
--dry-runbefore mutating. Show the plan, then apply. - Always pass
--actor agent:<your-name>so the audit trail attributes your work. - Prefer
--jsonfor parsing; keep human-readable output for humans. - Never edit the SQLite file directly. Use the CLI/engine — the triggers and audit log exist for a reason.
- Never delete a posted entry. Reverse it.
- Verify after every mutation (e.g.
report trial-balance --jsonmust saybalanced: 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
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。