--- name: klaai description: Do bookkeeping in Klaai via its `klaai` CLI — record expenses and receipts, create and pay invoices, manage customers/vendors/accounts, process the receipts inbox, post journal entries, and run financial reports (balance sheet, income statement, GST/HST return, aging). Also loads a year of books from a spreadsheet, bank/credit-card export or statement. Use when the user asks to record a transaction, file a receipt, bill a customer, import or load their books/bookkeeping/financials from a file, reconcile a year, check cash/receivables/tax owing, or produce financial statements for a Canadian small business. --- # Klaai Klaai is double-entry bookkeeping for Canadian small business. You operate it through the `klaai` CLI; the human reviews your work in the web app. ## The one thing to understand **Every transaction is a balanced journal entry.** Documents (invoices, receipts, payments) are the user-facing records that *generate* those entries: ``` invoice → Dr Accounts Receivable Cr Revenue, Cr GST/HST Payable payment → Dr Bank Cr Accounts Receivable expense → Dr Expense, Dr ITC Cr Bank / Credit Card / Accounts Payable transfer → Dr destination Cr source ``` You almost never post raw journal entries. Prefer the document command (`invoices create`, `receipts create`, `invoices pay`) — it writes the document *and* its entry, and keeps them in sync. Reach for `journal create` only for things that aren't a document: opening balances, year-end adjustments, corrections, interest, depreciation. ## Before you start **First, make sure the `klaai` CLI is installed — if it's missing, install it yourself. Never send the user to a terminal to do it.** ```bash command -v klaai >/dev/null 2>&1 || npm install -g @buzzerboyinc/klaai-cli klaai --version # confirm it's on the PATH ``` `klaai` is an npm package and needs **Node 18+**. If `npm` isn't found either, install Node first (from https://nodejs.org, or the user's package manager — e.g. `brew install node`, `apt install nodejs npm`) and retry. If a global install is blocked by permissions, install Node through a version manager (nvm or fnm) so `npm install -g` works without sudo. The CLI connects to **www.klaai.co** by default. For a local or self-hosted server, point it there first: `klaai config set-url `. Then check you're signed in: ```bash klaai login status --json # confirm signed in and which company ``` If not signed in, **run the sign-in yourself**: ```bash klaai login url --device ``` This opens a browser page and waits. Tell the user what to do in one sentence — *"I've opened a sign-in page; click Approve and I'll carry on"* — and show them the code it prints so they can check it matches the page. The command returns on its own once they approve. It blocks for up to 10 minutes, so run it and wait rather than polling yourself. **Never** ask them to open a terminal, run a command, or paste a token into the chat. `klaai login url` *without* `--device` stops and waits for a human to type a code, so it will hang forever if you run it — that form is for people working in a terminal themselves. Then orient yourself. **Do this before recording anything**, because account IDs are per-company and cannot be guessed: ```bash klaai accounts list --json # ids, codes, subtypes, `funding` flag klaai customers list --json klaai vendors list --json ``` Match by `code` or `name`, then use the `id`. Never invent an ID like `acc_1000` — that is demo data, not this company's chart. ## Always use `--json` Every command takes `--json` and returns the raw API response. Use it for everything you parse. Reserve plain output for showing the user a result. ## Recording work ```bash # expense paid by card or bank klaai receipts create --description "Microsoft 365" --amount 7.99 --tax 1.04 \ --category --paid-from \ --vendor --date 2025-01-09 --by claude --json # money coming back from a vendor klaai receipts refund --description "Microsoft 365 refund" --amount 13.85 --tax 1.80 \ --category --to --date 2025-06-02 --by claude --json # a bill (owed, not yet paid): omit --paid-from → credits Accounts Payable klaai receipts create --description "Rent — July" --amount 1800 --tax 234 \ --category --date 2025-07-01 --by claude --json # money moved between your own accounts (incl. paying a credit card) klaai receipts create --description "Visa payment" --amount 1756.28 \ --from --to --date 2025-01-02 --by claude --json # invoice a customer, then record the money when it lands klaai invoices create --customer --date 2025-01-31 --terms net_15 \ --tax 802.14 --line "Consulting — January||6170.27" \ --notes "Source invoice 2024-118" --json klaai invoices pay --amount 6972.41 --account \ --date 2025-02-14 --method e-transfer --json # the inbox: documents the user forwarded, waiting to be filed klaai inbox check # pull new mail from the configured mailbox klaai inbox list --json klaai inbox process --description "Bell — July" --amount 92.50 \ --tax 12.03 --category --paid-from --json ``` `inbox check` is the ONLY way mail is fetched — there is no scheduler, so run it when the user asks whether anything new has arrived. It is safe to re-run: messages already imported are skipped. Pass **`--by claude`** on `receipts create` and `inbox process` so the web app's review queue shows who recorded it. Anything you record arrives as `pending_review` — that is correct and intended. Do not approve your own work unless the user explicitly asks. ## Filing documents against a record Statements, signed contracts, anything worth keeping. Attach to an **account**, `invoice`, `receipt`, `vendor`, `customer` or `journal_entry`. ```bash klaai attachments list --for account:acc_1000 --json klaai attachments add file_85adac --for account:acc_1000 \ --label "January 2026 statement" --period-start 2026-01-01 --period-end 2026-01-31 klaai attachments remove att_81b117 ``` `add` takes a **file id that already exists** — typically one that arrived in the inbox (`klaai inbox list --json` shows `file_id`). There is no upload command; uploading is something the user does in the web app. Set `--period-start`/`--period-end` on anything covering a period. A statement without one is much harder to find later, and it's what makes "do we have all of 2026?" answerable. `remove` deletes the underlying file too, unless another record still points at it. Don't remove a document you didn't file. ## When you can't decide: leave a to-do **Guessing puts a wrong number in someone's books. A to-do costs them ten seconds.** When a document is genuinely ambiguous — you can't tell which account it belongs to, whether a cost is billable, whether a charge is personal — record the question instead of picking an answer. ```bash # the common case: a receipt you can't confidently categorise klaai todos create "R-1043 includes a \$400 chair — Office Supplies or a fixed asset?" \ --link receipt:rcp_03 --priority high klaai todos create "Confirm the home-office proration — I used 15%" --due 2026-08-31 klaai todos list --json # open items (default) klaai todos list --status all --json # includes resolved klaai todos show tod_4f2a --json klaai todos done tod_4f2a # only when the user has actually decided ``` `--link ` points the item at what it's about, so the user can click straight through to it: `receipt`, `invoice`, `vendor`, `customer`, `account` or `journal_entry`. Use it whenever the question is about a specific record. `--priority` is `low | normal | high` (default `normal`); reserve `high` for things that change a filing or a total. Items you create are attributed to you automatically — no `--by` flag. They show up in the web app under **To do** with a badge, which is where the user clears them. **Still record the transaction.** A to-do is a question about work you have already done, not a substitute for doing it. File the receipt with your best guess *and* flag the uncertainty — never leave the books empty pending an answer. The exception is when you cannot proceed at all (no funding account exists, say); then say so directly rather than only leaving a to-do the user might not read. Don't resolve your own to-dos. `todos done` is for after the user has told you the answer. ## Rules that will bite you 1. **Amounts are tax-exclusive.** `--amount` is the subtotal; `--tax` is the tax in dollars, not a rate. A $226.00 receipt at 13% HST is `--amount 200 --tax 26`. 2. **Invoices are only paid when a payment is recorded.** Setting a status does not move cash. Use `invoices pay`. 3. **Never guess an account ID.** List accounts first (see above). 4. **Use a `funding` account for `--paid-from` / `--account`.** `klaai accounts list --json` marks these — it includes credit cards and lines of credit, not just banks. 5. **Journal entries must balance**, and the server enforces it. Debits = credits to the cent, or you get `Out of balance: …`. 6. **Optional fields take `null`, never `""`.** Omit the flag instead. 7. **Amounts may be messy text.** `$1,234.56`, `-$799.00` and `(65.00)` all parse. But a **negative amount is never a plain expense** — it's a refund (`receipts refund`) or a card payment (`receipts create --from/--to`). The CLI rejects negatives on `receipts create` and tells you which to use. 8. **Owner money in or out** goes to the *Due to / from Shareholder* account (`shareholder_loan`): the company owes them when it's a credit balance, they owe the company when it's a debit. Use `journal create`. 9. **Interest earned is revenue**, not a negative expense — post it to an `interest_income` account. 10. **Ambiguity is not your call to make.** If two accounts are both defensible, file it with your best guess and raise a to-do (see above). Silently choosing one is how a misclassification survives to the tax return. ## Fiscal years **A year stays editable until it is explicitly closed** — the calendar rolling over changes nothing. So importing prior-year history just works: the record's **date** decides which year it lands in. ```bash klaai years list # which years are open or closed klaai years close 2025 # make FY2025 read-only klaai years reopen 2025 # let the accountant correct it klaai reports trial-balance --fy 2025 # `--fy` is a read filter ``` Writing into a closed year returns *"Fiscal year 2025 is closed."* — tell the user and ask before reopening; a closed year usually means it's been filed. ### HST method Set **per fiscal year**: `quick` or `detailed`. This records how the books account for HST — nothing more. ```bash klaai years hst-method # every year, and where its method came from klaai years hst-method 2026 quick # use the Quick Method from 2026 on klaai years hst-method 2026 --reset # go back to inheriting ``` A year with no setting of its own **takes the most recent earlier year that has one**, so a new fiscal year carries on however the last one was kept and this is normally set once. A **closed year cannot be changed** — its books are read-only. **Don't change it yourself.** An Administrator or Accountant decides which method the books are on. If the figures look like the wrong one is set, raise a to-do saying so rather than switching it. ## Checking the books ```bash klaai dashboard --json # cash, A/R, tax owing, net income klaai reports trial-balance --json # debits must equal credits klaai reports balance-sheet --json # assets = liabilities + equity klaai reports income-statement --json klaai reports gst-hst --json # CRA lines 101/105/108/109 klaai reports gst-hst --from 2025-07-01 --to 2025-09-30 --json klaai reports ar-aging --json # who owes, by bucket klaai accounts ledger --json # one account, running balance ``` After any batch of work, **verify the trial balance still balances** and report the figure to the user. If `total_debit ≠ total_credit`, stop and investigate — that is a bug, not a rounding artifact. ## Setting up a company's chart of accounts Every company starts with a small standard chart. Add real bank, card and investment accounts before importing: ```bash klaai accounts subtypes --json # the allowed --type values klaai accounts create --code 1005 --name "Business Chequing" --type cash_chequing \ --institution "Example Bank" --account-no 0000 --json klaai accounts create --code 2210 --name "Business Visa" --type credit_card --json klaai accounts create --code 1025 --name "GIC — 12 month" --type investment_gic --json ``` The **subtype decides the accounting type** — a `credit_card` is always a liability. You never specify debits/credits when creating an account. Five accounts are tagged `system_role` (`ar`, `ap`, `tax_collected`, `tax_paid`, `default_bank`) and are used automatically when posting. Don't delete or retype them. ## Errors Messages are written to be read. Common ones: | Message | What to do | |---|---| | `Out of balance: debits X ≠ credits Y` | Fix the postings; don't retry unchanged | | `Fiscal year YYYY is closed` | Ask the user before `klaai years reopen YYYY` | | `Amount must be positive` | It's a refund or a card payment — see rule 7 | | `Can't read "…" as an amount` | Clean the value; don't guess at it | | `That overpays the document…` | Check the outstanding balance first | | `Can't delete … N posting(s) reference it` | Deliberate guard — archive instead | | `Not signed in` / `token has expired` | Ask the user to run `klaai login url` | | `No company selected` | `klaai companies list` then `klaai companies use ` | Surface the message to the user rather than working around it. These guards exist to protect the ledger. ## Working across companies ```bash klaai companies list --json klaai companies use # persists in ~/.klaai/config.json klaai invoices list --company --json # or override per command ``` ## Importing books from a file If the user points you at a spreadsheet, export or statement — *"load my books from this file"* — **read `references/bulk-import.md` first and follow it**. Do not start posting transactions from a file without it. The short version: **profile the file, present a plan, get one confirmation, then load.** Never ask the user to describe their file up front — work it out yourself and confirm your reading of it. ### Reading the file Pick the tool from what's actually installed rather than assuming: ```bash python3 -c "import openpyxl, sys; print(openpyxl.__version__)" # .xlsx/.xlsm python3 -c "import pandas; print(pandas.__version__)" # optional csvkit --version 2>/dev/null; ssconvert --version 2>/dev/null # fallbacks ``` - **.xlsx / .xlsm** → `openpyxl` with `load_workbook(path, data_only=True)`. `data_only=True` is essential: it returns cached *values* instead of formula strings. If openpyxl is missing, install it (`pip3 install --user openpyxl`) or convert to CSV with LibreOffice/`ssconvert`. - **.csv / .tsv** → read directly; sniff the delimiter and encoding. - **.pdf statements** → extract text first; if the layout won't parse cleanly, say so rather than guessing at columns. Warnings like *"Conditional Formatting extension is not supported"* are noise — ignore them. Read the file **once** into memory and work from that. Don't re-open it per row.