Emerald

io.github.kfireliyahu由 GitHub 账号 kfireliyahu 发布v0.3.0更新于 Nov 7, 2026

聊天之下的数据库:一个本地SQLite文件,模型从中请求数据行,而不是重读笔记。

AI 翻译由 AI 模型根据下方的原文描述翻译,可能有误。

原文描述

The database under the chat: a local SQLite file a model asks for rows, not notes to re-read.

已验证STDIO仅桌面数据库效率与工作流金融

概览

Emerald 的本地 MCP 服务;无需登录;STDIO;85 个工具。

暂无 AI 概览:以下信息来自注册中心条目和服务本身。

安装

在 SourceWeft 中

  1. 打开 控制台中的 Emerald,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

[图片]

Emerald

The database under the chat. A local, file-based database that you and an AI model fill together, where every table has real types and real checks, so what you worked out stays true. Free software, AGPL-3.0. Desktop only, no server, no account. getemerald.dev

What Emerald is

  • A document is a file. .emd, a real SQLite database on your disk, stored as SQLite bytes or as text and chosen per file. sqlite3 opens it, the schema is documented, and ordinary tools read every table, including the ones you declared.
  • Declare a table for anything you keep: a charge, a paycheck, a book, a milestone. It is a format: a real table with real types and a CHECK for every rule you state, not a template. A value that does not fit is refused when it is written, not found later.
  • Formats link, and figures are worked out across them: hours from a start and an end, a total over the rows that point here. A total never quietly skips a row it cannot read.
  • Claims are rules that have to hold: the receipt total is the card charge, every hour was paid. Each one is proved, failed or unchecked, and unchecked says why.
  • Files are read onto formats: a spreadsheet, a CSV, a JSON file of rows, a Markdown note or a folder of them. The columns are placed once, kept as a reader, and checked every time. A model may give a mapping or a position, never a value.
  • A model works in it through MCP. The server reads and writes the document directly, with shaped tools, never a general query: 28 for any document, 85 with the ledger. Every write is a dry run until you say commit; a copy is kept beside the document before any write; a document older than the server's schema is never migrated, only reported.
  • It draws in the conversation. Ask to see a sheet, a board or a roadmap and the server draws it as the app does, where the host supports MCP Apps (seen in the Claude desktop app's Code tab and in VS Code's Copilot Chat); everywhere else, the same answer as text.
  • Packs are formats somebody already worked out, with their views. Two come with Emerald: the ledger and the development pack. Making and sharing your own is next.
  • No internet, no login. The app makes no network call of its own — not for AI, not for telemetry — and a test fails if it ever does, with one exception you choose: updates, off by default. Turned on, it asks GitHub for the newest version number once a day, and on Windows downloads a newer one from its GitHub release; Help → Check for Updates Now asks once. A local model runtime on this machine can be used, and looking for one happens only on your click, over loopback.

The ledger

  • For people who want to manage their money, not be managed by it: everything they have, in one place that is theirs.
  • Reads what you already get: card statements, bank exports, payslips, fund reports.
  • Files every charge, and tells a transfer from a spend and a monthly bill from a one-off.
  • Every total opens to the rows it was summed from.
  • A year of savings-fund reports read in: one month the fund received nothing, though the payslip showed the deduction.
  • Nothing connects to your bank. You give it the files.

The development pack

  • A project's memory as rows: milestones, units, decisions, open questions, an inbox.

  • A session starts from the rows, not from a summary of a chat. A request that contradicts a decision already taken is answered with the decision.

  • One project's session leaves a note in another project's inbox, and the next session there starts with it.

  • Emerald is built with it: docs/memory.emd is this project's memory, and every session on this repository starts by asking it.

  • Setting a repository up is one command, run in its folder (from 0.2.0):

    bash
    npx -y emerald-mcp init [path/to/document.emd]

    It creates the document with the development pack (docs/memory.emd unless you name one; stored as text inside the repository, so it is committed and diffs), writes a marked section of CLAUDE.md with the questions a session starts from and the columns of every format, and — where the Claude Code plugin is not installed — the two hooks that print start_here and the inbox into a session. A second run changes nothing; after a format changes, it rewrites only its section.

Installing it

Three ways in, all available today:

  • The desktop app, for Windows and macOS, from the latest release. .emd documents get their own icon and open on a double-click. It is not code-signed: Windows shows "Windows protected your PC" (click More info, then Run anyway; the installer is per-user and asks for no administrator password), and macOS says Apple could not verify it (click Done, then System Settings → Privacy & Security, Open Anyway). A Mac copy of 0.1.2 or earlier says "Emerald is damaged" instead and has no Open Anyway: run xattr -cr /Applications/Emerald.app once in Terminal, or download the new one. Every release is built by this repository's release workflow from a tag and attested, with a SHA256SUMS file beside it: gh attestation verify <file> --repo kfireliyahu/emerald-emd asks GitHub whether the file is one the workflow built. The installers' names carry no version (Emerald-Setup.exe, Emerald-arm64.dmg, Emerald-x64.dmg), so releases/latest/download/<name> is always the newest. Updates are off by default: turn them on in About and the app asks GitHub once a day whether a newer version is out. On Windows it then downloads it in the background, checks it against the checksum the release published, and says ready quietly at the top of the window; it installs only when you press Install and restart, and opens again on the document you had open. On a Mac it says so, and Download opens the release page (the Mac updater needs a signed app). Updating yourself is automatic from 0.1.2 on; a copy older than that, download the new one once by hand.

  • The MCP server, as the npm package emerald-mcp: one line in your assistant's MCP configuration, for any document; leave off --engine-only for the ledger's tools too:

    bash
    claude mcp add emerald -- npx -y emerald-mcp --engine-only
  • The Claude Code plugin, with the server, its views drawn in the conversation, the skill for declaring a format with a person, and the hooks that bring a project's start_here and inbox into a session (what it runs):

    /plugin marketplace add kfireliyahu/emerald-emd/plugin install emerald@emerald-emd

The repository

Build and run

bash
git clone https://github.com/kfireliyahu/emerald-emd.gitcd emerald-emdnpm ci                 # npm install works too; this is the reproducible formnpm run dev            # the app, in a browser tabnpm run test           # the whole suitenpm run build          # the app -> dist/, what the desktop shell servesnpm run desktop        # build everything and open the desktop windownpm run build:plugin   # the MCP server -> dist-plugin/server.mjsnpm run build:demo     # Emerald's page and the tours -> dist-demo/npm run dist:win       # the Windows installer -> release/npm run dist:mac       # the macOS disk image -> release/   (needs a Mac)
  • Built and tested on Node 24. npm ci prints audit advisories (in the MCP SDK and the router; tracked). The first run of anything that opens the desktop window downloads Electron's binary once; that is the one time the build needs the network.
  • The screens are wide-data screens, a column mapper, an import review, a table of deposits, and there is deliberately no phone layout. In a browser, Chrome and Edge save silently through the File System Access API; Safari and Firefox save by download. The desktop app saves silently everywhere.

The MCP server from this checkout

.mcp.json points at dist-plugin/server.mjs with --engine-only, so a Claude Code session started in this directory picks it up and asks you to approve it once. From anywhere else:

bash
claude mcp add emerald -- node /absolute/path/to/dist-plugin/server.mjs --engine-only

Leave --engine-only off for the ledger's tools as well. The built folder is self-contained — every package is bundled into it, and it is the npm package as it stands — so it runs from anywhere Node does. docs/plugin.md has the tool surface, the hosts tried, and the rules.

Where things are

  • docs/: the specification, architecture, data model, the engine, a note per area. docs/09-decision-log.md records why things are the way they are, and docs/memory.emd holds where the build is, as rows.
  • docs/demo.md: the page, the tours, and how another page embeds one.
  • src/domain/ is the engine and imports nothing from React, SQLite or the DOM; src/data/ is storage; src/ui/ the app; src/plugin/ the MCP server; electron/ the desktop shell; src/data/packs/ the two packs.

Issues and contributions

  • Issues and discussion are welcome.
  • Code contributions wait for a contributor agreement; LICENSING.md says why. When they open, import mappings and locale packs are the pieces that benefit most from people who live with those formats.
  • Never include real statement files. Fixtures are synthetic, and a test refuses anything else.

Disclaimers

  • It computes; it does not advise. Nothing Emerald shows is financial, tax, legal or investment advice. It adds up what you gave it and says when something does not; what to do about it is yours to decide, with whoever you would have asked anyway.
  • Check what it read. A figure read from a payslip, a statement, a fund report or a receipt is a reading of that document, by code or by a model, and either can be wrong. The document is the truth; Emerald keeps the reading beside it so you can compare.
  • A model's words are the model's. When an assistant works in a document through MCP, what it says and what it asks to write come from that assistant and its provider, under their terms, not from this project. Emerald's part is to refuse what does not fit and to show every write before it happens.
  • Your file is your file. Emerald writes the document you opened and keeps copies beside it, and it makes no other copy anywhere. Back it up as you would any file; nothing here can recover one you lost.
  • No affiliation. Emerald is not connected to, endorsed by or acting for any bank, card issuer, fund, employer, payroll provider or AI provider. The ledger reads the files those institutions give you, in whatever layout they come in.
  • As is. The software is provided without warranty of any kind, as sections 15 and 16 of the AGPL-3.0 say in full. The desktop builds are unsigned and built by this repository's workflow; use them, or build from source, at your own judgement.

Privacy

Emerald collects nothing. The policy, in full, is PRIVACY.md.

License

The code is AGPL-3.0-only (LICENSE); the notes and essays CC BY 4.0; format declarations, packs and synthetic fixtures CC0. LICENSING.md says which is which, what the name and mark are not, and how to get a commercial licence. The desktop app says the same in its About, with the third-party notices of everything it bundles a click away in Help.

来源:README.md,提交 b8460e7

工具

85

formats

Declared formats

What kinds of thing this document holds — every format declared in it, with its columns, their types, units, bounds and enum members, and the prose the author wrote around them. The columns are declared per document: `start_here` ends with one line per format, enough to write a row, and this is the whole declaration. Name a `format` for its full column list; omit it for the list of what is declared. Reads the file; never modifies it.

输入参数path必填format

declare_format

Declare a new format

Bring a **new** kind of thing into a document: a format, compiled to a real table with real types and checks. Call `formats` first. Each column has a `snake_case` `name`, a `type` — one of `text`, `integer`, `real`, `date`, `boolean`, `enum`, `money`, `link`, `quantity`, `decimal`, `month`, `instant`, `links`, `percent`, `time`, `enums`, `elsewhere`, `file` — and optionally a `unit` (`g`, `kcal`), `required`, `unique` (no two rows share it — a name a row is known by; text, integer, decimal, date, month or link), `values` for an enum or enums, `minimum`/`maximum`, a `scale` for a `decimal` (6 for a rate) and a `note` saying what it means. **A format declares its claims** — `claims`, equations over its numeric columns with a tolerance, like `4*protein_g + 4*carbs_g + 9*fat_g = kcal` within 2%; they make a row provable and never refuse one. **Ask the user whether any figures relate** before answering (a total and its parts, a price and a quantity); a list — books, contacts — makes none, needs no claims, and still gets every refusal. A figure worked out rather than typed is a `computed` column. **`money` is two columns**, minor units beside a currency, so no figure is a float. **`quantity` is two as well**, a figure beside the unit *that row* is in — only when the unit varies by row; otherwise a `real` with a `unit`. Neither takes a `unit`. A **ratio** has a `unit` and a `per` — `density`, `g` per `ml` — the only way a figure crosses dimensions: a claim writes `amount in g via ingredient.density`. No free-text check, no expression. **Across two columns of one row** is a `constraint`, one of four sentences: `isbn is required when kind is BOOK` (`required only when` adds *and empty otherwise*), `at most one of book, article, video`, `score is at most 10 when kind is QUIZ` (or `at least`), and `one row per course, week` (`when status is DONE` limits it) — a row breaking one is refused. `notes` is prose kept word for word: where the figures come from and what was decided. T

输入参数path必填notestitleclaimscommitformatcolumnsformatsrollupscomputedrule_setsaggregates另有 1 个

connect_formats

Connect two formats

Join two formats this document **already** declares: a `link` column on `format`, so each of its rows belongs to one row of `target` — each `recipe_line` belongs to one `recipe` — and, optionally, a total on `target` along it (an `aggregate`): `recipe.total_kcal`, the sum of `amount_g / 100 * ingredient.kcal` over the lines that name it. This is how a format gets a total of another declared after it, which `declare_format` cannot do. Name an existing link as `column` to add only the total. **It only adds**: a link may be empty, so rows already there lose nothing; `required: true` on a format that has rows is refused. A connection is removed with `extend_format` `remove` (the link column once its cells are emptied, or the total); re-pointing one is done at `#/connections` in the app. Returns what it would add and writes nothing until `commit: true`.

输入参数notepath必填columncommitformat必填target必填found_byrequiredaggregate

extend_format

Change a format

Change a format this document **already** declares, as its table does: add `columns`, `claims`, `computed` columns, `unique`, `constraints`, `rollups` and `aggregates` as `declare_format` takes them; `rename`, `change` or `remove` what it has; `order` the columns. A rename keeps its values. Refused, naming the rows: a `required` column on rows, removing a column holding values, a list the rows do not fit, a type or unit change over values. Writes nothing until `commit: true`.

输入参数path必填orderchangeclaimscommitformat必填removerenameuniquecolumnsrollupscomputed另有 3 个

delete_format

Remove a format and every row in it

Remove a declared format: its declaration, its table and **every row in it**. Refused while another format links to it or folds its rows. The dry run says how many rows go; show that to the user, and call again with `commit: true` only on a plain yes. To remove some rows, use `delete_format_rows` instead.

输入参数path必填commitformat必填

format_rows

Rows of a declared format

The rows this document holds of one declared format. Ask `formats` first for the column names — they belong to the document. `match` filters on exact values (text and enum members case-insensitively; a list column, the rows holding the one named), `search` a substring in every column as shown (a price as 12.90, a link by its row’s name) but yes/no, and `order` sorts by any column, computed too — by default the one a row is named by, which is its first text column. Narrow `columns` to get a table of just those — a computed column (a total, or one worked out from the row) by its name too; leave it out and each row comes back in full with its empty columns left out, **and with what its claims say about it** — proved, failed, or unchecked when the row has nothing in a column one of them names. Every row carries the id the other tools use to correct or remove it. Reads the file; never modifies it.

输入参数path必填viewlimitmatchorderformat必填offsetsearchvaluescolumnsdescending

show_format

A declared format’s rows, drawn as the app draws them

Shows the person one declared format as the app draws it — its table, its list (one line per row, a tick, grouped as `arrange_views view:list` says), or its board, roadmap or ready-to-start list when the format has one; or the agenda, every dated format’s rows from today; or a dashboard (no `format`: the document’s) — in the conversation, in a client that draws MCP Apps; the answer’s first line says whether this client said it draws them. Call it when they ask to see a format, or some of its rows: `match` and `search` filter exactly as `format_rows` does, and the view draws only those rows and says it is filtered. The text answer names every row the view shows, with its state or lane. The page is a read-only copy, drawn again as the file changes. Reads the file; never modifies it.

输入参数path必填viewmatchformatsearchcolumns

format_problems

What looks wrong in declared formats

What the engine can see is wrong in a format’s rows, without a rule anybody wrote: a link empty where most rows fill it, rows the same in every column, claims failed or unchecked. `read_onto_format` says it after every read. Reads only.

输入参数path必填formats

search_document

Search the whole document

Every row of every format, and the rest of the file, that holds any of `terms`: each hit by format, row and column, columns searched as shown. One call where `format_rows`’ `search` is one a format. Or `denylist`, a `--replace-text` file: the scan before a document is shared — hits only, a term named by its line, and the file’s bytes checked for a deleted value. Reads only.

输入参数path必填termsdenylist

start_here

What to start from

Call first. Answers the questions this document’s own views say a session starts from — what is ready to start, what is open, the latest entries — each with the `format_rows` call that lists it whole; then what a row of each format looks like, enough to write one; and where new work begins: `formats`, then `read_onto_format` (files or a folder of notes) or `declare_format`. Read-only.

输入参数path必填

format_rollup

A rollup of a declared format

A rollup a format declares, run: a figure per month, and per linked row. `formats` lists them. A month with a row that cannot answer is `unchecked`, naming it; money stays per currency. Reads only.

输入参数tofrompath必填limitformat必填rollup

apply_rules

Apply a rule set

Run a rule set over every row it sets — a write applies it by itself. A person’s value is never touched; a tie is named. With `link`, fill that link’s empty cells by its `found_by` instead; a set link is never moved, and a row with no match or two is named. Dry run until `commit: true`.

输入参数linkpath必填commitformat必填rule_set

record_format_rows

Record rows of a declared format

Write rows into a declared format — **a list of them in one call**. The columns belong to the document — `start_here` lists them, `formats` in full — and a key the format has not got refuses the batch rather than being dropped, because a figure you believe you stored and did not is worse than one you know is missing. A `money` column takes the amount in major units beside its own `<column>_currency` key, and a `quantity` column takes the figure beside a `<column>_unit` key naming a unit this document knows — `amount: 200` with `amount_unit: ml`. A `link` takes the row’s id, its name (first text column) when one row has it — `recipe: "Pancakes"` — or columns that find one: `{name: "Salt", brand: "Fine"}`, or one unique column alone: `course: {notion_id: "a1b2"}`; a row may name another of the same batch. Types, bounds and enum members are checked before anything is written and the whole batch applies or none of it does. Returns the diff and writes nothing until `commit: true`.

输入参数path必填rows必填viewcommitformat必填

update_format_rows

Correct rows of a declared format

Change columns of rows already stored — **a list of edits in one call**. Each names its row by `id` or by `where` (`{ref: "INV-0042"}`, which survives another writer; a text document renews ids on every write), and the `values` to change. `expect` (`{state: "planned"}`) refuses the batch unless the row still holds that, so two sessions cannot both take one row. Every column not named is left exactly as it was, which is the difference between this and the app: `#/formats/<name>` shows a person every column and writes every column, and this corrects three of thirty across a batch. A required column cannot be emptied. A `link` value may be the name of the row it points at, or a unique column of it (`course: {notion_id: "a1b2"}`), as in `record_format_rows`. Returns the before-and-after and writes nothing until `commit: true`.

输入参数path必填edits必填commitformat必填

delete_format_rows

Remove rows of a declared format

Remove rows from a declared format by id — **a list of them in one call**. **Refused while any other row still links to one of them** — the dry run names those rows; re-link or clear them first. The app removes one row at a time from `#/formats/<name>`; this is how thirty go at once, after printing every one of them in full. Writes nothing until `commit: true`; a copy of the document is taken first, and it is the only way back.

输入参数ids必填path必填commitformat必填

claim_units

Claim units

In a document that keeps the `development` pack, take units by `ref`: `planned` to `in_progress` with `taken_by`, the git `branch` they are taken on and `started` (default today), the batch refused unless every one is still planned. With `take_over`, take units already `in_progress` from that taker — a session that stopped mid-unit — refused unless each is held by exactly that taker; `started` is reset and the taker replaced is kept on `taken_over_from`. Writes nothing until `commit: true`.

输入参数path必填units必填branchcommitstartedtaken_by必填take_over

land_units

Land units

In a document that keeps the `development` pack: a unit’s closing pass in one call, for a list of units by `ref`: their new `decisions` (date, milestone, scope and `state: live` default to the unit’s), the unit to `built` with `landed` (default today), `files` and `decision` (default its first new one), its `review_entries` (`state: open` default), `close_milestone`, its git `commit` (refused without one unless `same_commit: true`) and `branch` (another than the claim’s refused unless `other_branch`), and the `report` of the session that built it. Or `inbox`: ids of a child’s proposals — an inbox row, `state: new`, `refs` its unit, its `body` one ```json block of one unit’s landing (no `close_milestone`), written to the document path the hooks printed — each landed as written and moved to `applied`. Each step is `record_format_rows` or `update_format_rows`, judged as they are, run in that order; all of it applies or none. Diff by format; writes nothing until `commit: true`.

输入参数path必填inboxunitscommit

dispatch_plan

Dispatch plan

In a document that keeps the `development` pack, before handing units out: for unit refs (every ready unit if left out), which can run at once and which share a path in their `paths` — one per line — and so go one after another, beside any unit in progress; a unit with no paths is unknown, never safe. Then each one’s brief, the units it waits on and the decisions it names. Read-only.

输入参数path必填units

record_units

Add or correct a unit of measure, or set the currency

Teach this document a unit it does not know — `slice`, `IU`, a tablespoon that is 20 ml in this kitchen. A unit is what a declared column says its figures are in and what a claim's `in` converts into, so a conversion the document cannot make is a conversion this fixes. **The engine converts inside a dimension and never across one**: grams to kilograms is arithmetic, millilitres to grams needs a density, and a density is a column of the format rather than a unit. A symbol the document already has is corrected — its factor and its note, never what it measures. `currency` sets the document’s currency — what a new amount on a screen starts in; a row still names its own. `week_starts_on` is what `this week` counts from (Monday). `formats` lists them. Returns the diff and writes nothing until `commit: true`.

输入参数path必填unitscommitcurrencyweek_starts_on

read_onto_format

Read a file onto a declared format

Read what the user already keeps onto a declared format: any .csv, .xlsx (or .xls) or .json of rows, a file row per table row; or **a folder of Markdown notes**, a note per row: `title`, `file`, each frontmatter key, `inline tags`, `body links` and `body`, with a link read from `[[name]]` or `[text](file.md)`; a body link naming no row is left out and named, the note still read; a hidden file is not read; links between notes of one folder are filled; `items` reads list items or table rows onto another format, each linked to its note. **Dry-runs by default**: the reader with each key’s header cell and first value, the rows as stored, the rows that would not type and why, and **what the format claims, checked before a row exists**. Writes on `commit: true`. Keys come from `formats`. **The user maps the columns; this only guesses** — show the reader, correct it with `columns`. `fixed` gives a key one value for the file (a currency, a unit, the row every line belongs to). A column fills a `link` only by the exact unique name of its row, and a line naming none is refused; a link is guessed only from a header that names it. `header` writes the file’s total, period or card as one row of another format that every line links to. **A source read again updates its rows** by note file name, and any file by a unique column (so a later export updates what an earlier one wrote): changed, added and gone named; gone kept unless `remove_gone`. `supersede_reading` takes a reading back. **Several files in one call**: `files`, each `{ file, format }` with its own columns — read in the order their links need, so a line names a row from a file above it; one dry run for the lot, written whole or not at all.

输入参数filepath必填filesfixeditemssheetundercommitformatheadercolumnsrecursive另有 4 个

supersede_reading

Take back what one file wrote

Take back the rows one file wrote onto a declared format — **exactly those rows and no others**. Called with no `reading`, it lists every file read onto the format with when, and how many rows each still has. **Dry-runs by default** and writes nothing until `commit: true`. **The reader is kept** (forgetting one is `readers`), so supersede → correct the file → read it again is one call each. This is how a file read twice stops being two copies; a file is matched by its bytes, never row by row.

输入参数path必填commitformat必填reading

readers

A format’s readers

List a declared format’s readers — one per kind of file it has read — or name and forget them. Dry-runs until `commit: true`. Forgetting takes back no row.

输入参数path必填commitforgetformat必填rename

keep_copy

Back up a document now

Backs the document up now: a copy as it is, in Emerald’s folder, named for what it comes before (`work.before-20261006T120512-cleanup.emd`), and says where. A read or batch of 100+ rows keeps one by itself; ask before anything else worth undoing. The last 3 are kept, apart from the rolling copies.

输入参数path必填before

store_document_as

Store a document as text or as SQLite bytes

Writes the document again as `text` (diffs in git) or as `sqlite` bytes, and every later write keeps that shape — the app’s Save as text. Refused when the text would not carry it — a document keeping the ledger, for one. Writes nothing until `commit: true`.

输入参数as必填path必填commit

arrange_views

Arrange a format’s views

How the app shows a format: on its entry, a column’s `show_as`, `header` line, `hidden`, `new_entry` and the `order`; a board’s lanes; a table column’s `width`; a column’s `setting` in a list, a roadmap, a ready list or a start-here question; `view: saved`, a `name` and what it keeps (`today` stays that phrase) — on one saved, only what is given changes; `match:{}`, `search:""`, `order:""`, `columns:{}` empty one — or `rename`, or `delete: true`; `kind: input` takes `fields`, `defaults`, `default`; `view: dashboard` `add`s, `size`s, `order`s and `remove`s blocks (no `format`: the document’s). `formats` with a `format` shows the arrangement and each column’s ways. Dry-runs until `commit: true`.

输入参数path必填commitsettings必填

create_document

Create an Emerald document

Create a new .emd document at a path that does not exist yet; never overwrites. It keeps the `packs` named and no other, and **none when omitted**: a document starts empty and holds what the person declares. Name a pack only when the work is what it is for — `development` for a project’s memory, `ledger` for their finances. With no path the user may be shown a form; otherwise ask them.

输入参数pathpacks

keep_packs

Keep packs in a document

Keep packs in an existing document (the Packs screen’s *Keep … here*): declares the pack’s formats it lacks, adds missing optional columns to those it holds, renames a column the pack renamed with its values, changes no row; a pack kept already is brought up to date this way, its formats’ and columns’ notes and views’ settings too wherever the document still holds an earlier build’s — one a person changed is kept, and the dry run names it. Refused, naming the column, where a format of the same name is not the pack’s. Dry run until `commit: true`.

输入参数path必填packs必填commit

document_info

Emerald document info

Describe an Emerald document (a .emd file): whether this plugin will serve it, the formats it declares with how many rows each holds, and — when it keeps the ledger — its accounts, transactions, categories and reporting currency. Call it when a tool refuses a document — every tool refuses one whose schema does not match, and this is the one that says so in a sentence the user can act on. Reads the file; never modifies it.

输入参数path必填

category_tree

Category tree

The document category taxonomy: every category full path (Living → Utilities → Electricity), its id, its kind and its Plan bucket. Read this before suggesting a category for anything — the ids here are what the write tools take, and two categories may share a short name.

输入参数path必填include_archived

uncategorized_merchants

Uncategorized merchants

The uncategorized backlog, grouped by merchant: how many rows each merchant has, and what the user has already categorized other rows of the same merchant under. Ordered by the largest single-currency outflow, which is not printed. Start a "help me categorize" conversation here.

输入参数tofrompath必填limitsearchaccount_idinclude_amounts

transactions

Transactions

A filtered page of the ledger, plus the totals over every matching row — not just the page. Filter by date range, account, category, uncategorized-only, merchant search and amount bounds; sort by date, charge date, merchant, category or amount. Use this to answer "what did I spend at X" and to show the evidence behind any figure.

输入参数tomaxminfrompath必填sortlimitsearchdirectionaccount_idcategory_iduncategorized另有 1 个

month_summary

Month summary

One month already added up: income, expenses, balance, savings rate; how each compares with last month, the three-month average and the same month last year; the category breakdown rolled up to parents; the fixed-versus-variable split; and where the income came from. Defaults to the current month. Every figure comes from the same call the app own Insights screen makes, so they cannot disagree.

输入参数path必填month

year_summary

Year summary

One year already added up: income, expenses, balance and savings rate; averages over the complete months only; the best and worst month by balance; a month-by-month table where a month with no data reads as nothing rather than as zero; and what the paychecks say about gross, tax and payroll savings. Defaults to the current year.

输入参数path必填year

fixed_expenses

Bills

The bills the document tracks: what each is expected to cost, its amortized monthly equivalent, its cadence, whether the amount is fixed or variable, what it is matched against in the ledger, and whether the user marked it cuttable. Active ones by default.

输入参数path必填include_inactive

debts

Debts

Every debt with its principal, what has been paid, what is outstanding, how far along it is and when it projects to be paid off — plus the totals and what card installments still commit the user to. Open debts by default.

输入参数path必填include_closed

savings

Savings

Goals and where the money is: net worth — the one figure Overview shows, with its parts (in the bank, in accounts with holdings, locked, pension and hishtalmut funds, owed) — each goal balance broken into the four ways money got there, and the monthly expense figure the emergency-fund target is measured against. Open goals by default.

输入参数path必填include_closed

goal_projection

Goal projection

What each open goal will hold in N months, by plain addition of the amount Plan puts toward it each month (no interest counted), and what a month would have to be to reach it on its date. The same view as the Goals screen.

输入参数path必填months

debt_projection

Debt projection

What each open debt will have left in N months, by plain addition of its monthly payment and the extra Plan puts toward it (no interest counted), and when it is paid off with and without the extra. The same view as the Debts screen.

输入参数path必填months

fixed_expense_candidates

Recurring charges the ledger suggests

What looks like a recurring bill in the imported transactions but has not been confirmed as one: the merchant, the typical charge and its amortized monthly share, the cadence, how many months it was seen in, how much the amount and the day move, and how confident the detector is. Merchants already tracked or previously dismissed are excluded. This is the question `fixed_expenses` answers the other half of — that one lists what is already committed. Reads the document; changes nothing.

输入参数path必填ending_atwindow_monthsinclude_dismissed

track_fixed_expenses

Track recurring charges as bills

Turn recurring charges into tracked bills — **several in one call**. An entry either names a `merchant` from fixed_expense_candidates, in which case the charge, cadence and category come from the detector unless overridden, or gives a `name` and an `expected_amount` for a bill the ledger cannot see, like an annual premium paid by standing order elsewhere. **Dry-runs by default**: it returns what it would track and writes nothing until called again with commit true. Confirming the same merchant twice updates the row already covering it rather than adding a second. The document is copied to a timestamped .bak before anything is written.

输入参数path必填commitexpenses必填ending_atwindow_months

dismiss_fixed_expense_candidates

Say no to suggested recurring charges

Record that these merchants are not recurring bills, so the detector stops offering them — to this conversation and to every later one. A dismissal lasts until it is cleared; pass restore to clear it and let the merchant be suggested again. Writes immediately: it changes no transaction, no category and no bill.

输入参数path必填restoremerchants必填

plans

Plans: bill choices and buckets

The Plan screen as rows: each plan with what every bill does to it (out of net, part of the split, or ignored) and its buckets — what each is for (a goal, a debt, a category, a fund, an account or nothing) and how it is sized. Ids are here for `set_plan`, `set_fixed_expense_roles` and `edit_fixed_expenses`. The worked-out figures for a month are `plan_month`.

输入参数path必填plan_id

plan_month

Work a plan out for a month, and use it

One plan worked out for one month: net pay, the bills out of net, what is left to split, each bucket’s amount and share, what it actually got lately, when a goal is reached at this rate, money with no job to do — and what *using* the plan for that month would write (a planned deposit into a goal, a planned payment on a debt). **Dry-runs by default**, which is also how you read it; `commit: true` writes those suggestions and gives the month this plan. `months` instead gives a list of months this plan and writes nothing else, and `back_to_default` puts months of this plan back on the default — the Plan screen’s month checkboxes. A month nobody gave a plan is on the default. `only_buckets` writes just those buckets’ suggestions. A fund or an account is pointed at and writes nothing. The document is copied to a timestamped .bak first.

输入参数path必填monthcommitmonthsplan_idonly_bucketsback_to_default

set_fixed_expense_roles

Say what each bill does to a plan

For each bill, per plan: `net` (comes out of net pay before the split), `split` (is one share of the split, drawn on the bar) or `ignore` (this plan leaves it out). **A list in one call.** A bill the paycheck already took (a pension deduction) is outside the choice and refused. An expense with no choice made is ignored. This is the Plan screen’s switch on every row. **Dry-runs by default**: it shows each change and writes nothing until `commit: true`; the document is copied to a timestamped .bak first. Ids come from `plans`.

输入参数path必填roles必填commit

set_plan

Create, change or delete plans

What the Plan screen does to a plan — **a list of entries in one call**. Per entry: `plan_id` for an existing plan, or `create: true` with `name` (and `copy_of`, a plan id, to start from its sizes and its choices for the bills); then any of `name`, `make_default`, `basis` ({kind: LAST_PAYCHECK | AVERAGE_LAST_N | SPECIFIC_PAYCHECK | CUSTOM_AMOUNT, amount, months, paycheck_id}) — a bonus month is a plan whose basis is an amount — and `buckets`, **the plan’s whole list of buckets** as the screen’s bar has it: each {bucket_id (existing) or name (new), points_at: {kind: goal | debt | category | fund | account | none, id}, mode: percent | amount | rest, percent (0-100), amount (major units), locked}. A bucket is for one thing, and a bucket sized “rest” takes what is left after the others and the bills that are part of the split; at most one. Or, alone, `delete: true` (the default plan too; another becomes the default), or `remove_buckets` to take buckets out and hand each share to its right. What each bill does to a plan is `set_fixed_expense_roles`. **Dry-runs by default**; `commit: true` writes. The document is copied to a timestamped .bak first. Ids come from `plans`.

输入参数path必填plans必填commit

edit_fixed_expenses

Edit, end or stop tracking bills

Change bills that exist — **a list in one call**. Per entry: `name`, `expected_amount` (the whole charge, major units — a yearly premium is the year’s figure), `cadence`, `category_id` (null clears), `is_discretionary`, `day_of_month` (null clears), `ended_on` (YYYY-MM-DD or "today": it keeps its history and drops out of every plan); or, alone, `stop_tracking: true`, which removes it and what every plan said about it — its charges stay on Transactions. How it is recognised in the ledger is not changed here. Adding is `track_fixed_expenses`; what it does to a plan is `set_fixed_expense_roles`. **Dry-runs by default**; `commit: true` writes. The document is copied to a timestamped .bak first. Ids come from `plans` or `fixed_expenses`.

输入参数path必填edits必填commit

paychecks

Paychecks

Every paycheck recorded in the document — gross, net, the employer’s own contributions, total compensation, the effective tax rate, what payroll put away, and whether each itemization reconciles to its net. Plus the salary months with no paycheck at all, per-year totals, and **the deduction-type vocabulary with its ids**, which is what a line of a new paycheck has to name. Reads the document; changes nothing.

输入参数path必填limitinclude_lines

record_paychecks

Record paychecks

Write paychecks into the document — **several in one call**, one entry per paycheck. Each needs the salary month, the payment date, gross and net; an itemization is optional, and each of its lines names a deduction type by id from `paychecks`. Read a paycheck PDF or photo yourself and this is where what it says goes. **Dry-runs by default**: it returns what it would record, including whether each paycheck’s itemization reconciles to its net, and writes nothing until called again with commit true. Nothing here deduplicates — a second paycheck for a salary month is allowed, because a 13th salary is exactly that. The document is copied to a timestamped .bak before anything is written.

输入参数path必填commitpaychecks必填

set_deduction_types

Make, rename, order and remove deduction types, and say what each counts as and pays into

Everything the deduction types screen does, for **several types in one call**: **make** a type (its name, what the paycheck calls it, its kind), **rename** one, change its kind, **remove** one, put the list in **order**, and say what each type **counts as** (out of net, saving — the two flags every paycheck figure turns on) and what it **pays into** — a fund or a goal for a saving line, a debt (a payroll loan) for a loan repayment. **This link is how a fund finds its paycheck lines**: a pension line is a deposit into a fund only because its type names the fund, and `funds` checks those deposits against the fund’s own report. A new type starts as its kind’s guess, and a kind change carries the flags along while they are still the guess; one that differs is shown as set by hand, and `guess` puts it back. One destination per type — naming one clears the others, and `pays_into: null` links nothing. Refused by name: a target of the wrong kind (a fund or a goal takes a line that counts as saving, a debt a loan repayment), a name another type has, and removing a built-in type, a type a stored paycheck line names, or one another row links to. Types and their ids are listed by `paychecks`; funds by `funds`, goals by `savings`, debts by `debts`. **Dry-runs by default**; the batch applies whole or not at all, and the document is copied to a timestamped .bak first.

输入参数path必填ordertypescommit

funds

Pension and hishtalmut funds

Every pension, study fund, provident fund and managers’ insurance the document tracks: the latest transcribed balance and how old it is, the management fee as stated and as measured against what was actually charged, what the dormant ones cost a year, and — the point of the feature — **whether the paychecks and the fund’s own report agree about what was deposited**. A fund finds its paycheck lines through the deduction types that pay into it — `set_deduction_types` sets that link. Name one with fund_id to also get its readings, its return window by window, and the working behind that check. Reads the document; changes nothing.

输入参数path必填fund_idinclude_archived

record_fund_readings

Record fund balances from a report

Transcribe what a pension, hishtalmut, provident or managers’-insurance report says into the document — **several readings in one call**. Each entry names a fund by fund_id or by name, and carries one reading: the balance and its date, plus whatever else the report printed. A name that matches no fund creates it, once per batch, so four quarterly reports for one fund are four entries naming the same fund. **Leave out anything the report did not state** — an empty field means "not stated", and a 0 would be read as a stated zero. **Dry-runs by default**: it returns what it would write and writes nothing until called again with commit true. The document is copied to a timestamped .bak first. Editing a fund or correcting a reading already written is app-only.

输入参数path必填commitreadings必填

create_goals

Create goals

Add goals — **several in one call**. A goal has a name; a target amount and a target date are optional, and an open-ended goal is a real answer. Its kind is optional and drives nothing (EARMARKED when left out, as the goal form makes it); how many months of expenses the bank should hold is the cushion (`set_cushion`). **Dry-runs by default**; the document is copied to a timestamped .bak before anything is written. **The goal form’s one question is `default_holding`**: a bank account’s Cash, a place kept by hand or a new one takes `already_there`; a holding a broker’s rows buy is followed by its rows, with nothing to type; a bank account’s Cash can count the transfers into it (`from_transfers`). Editing, closing and deleting a goal are app-only, except its default holding (`set_goal_deposit_categories`).

输入参数path必填goals必填commit

record_deposits

Record savings movements

Record money moving in a goal — **several in one call**. Six modes: DEPOSIT, WITHDRAWAL, TRANSFER between holdings, OPENING for what was already there when tracking started, and GAIN / LOSS for what the market did. **The mode carries the direction; the amount is always positive.** OPENING, GAIN and LOSS are in the balance but are not money the user set aside, so they are kept out of the savings rate. **Dry-runs by default**; the document is copied to a timestamped .bak first. Editing and deleting a movement are app-only. A DEPOSIT here is money **set aside** by hand, still inside the bank; deposits **read from the bank** come from the Money moved category a goal reads (`set_goal_deposit_categories`).

输入参数path必填commitdeposits必填

set_place_accounts

Name the account a place’s money is shown by

Say which account each place on Net worth is — the bank account, the broker, the savings account whose statements show its money — **several in one call**. A place *links* to an account and is not merged with it: a deposit or cash at home has no statements and names none (`account: null` takes a link away). A card cannot be named, and an account is named by one place at most. The account gets its own cash holding in the place, made with the link — or `cash_holding` picks one already there, such as the holding goal deposits already land in. Unlinking keeps the holding. A bank account or cash has a place named as it and its own cash from the moment it is made; naming it from another place moves it there while its own place holds nothing. A place that names one account is not re-pointed to another in one call: set it to `null` first. Nothing here moves a figure. Places and holdings are listed by `savings`; accounts by `document_info`. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填links必填commit

record_holding_readings

Record what a statement says a holding is worth

Write what a statement printed each holding was worth on a day — **a list in one call**. Transcribed, never fetched: there is no price feed. `value` is the worth as printed, in major units; `quantity`, `cost_basis` and `price` are optional and each is a check: the cost against what the rows that name the holding put in by that day, the quantity against their quantities, quantity × `price` against the value (a price printed in agorot is divided by 100 first — the check catches it when it is not). An account’s own cash holding takes a value only, checked against the account’s running balance. A goal fed by the holding is then worth its latest reading, with what went in beside it, and the difference is growth. The same figures for the same day again change nothing; other figures for a day that has a reading are refused unless `correct: true`; `remove: true` takes a day’s reading away. Holdings and their ids are listed by `savings`. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填commitreadings必填

set_goal_deposit_categories

Point goals at their deposits

Say what feeds each goal, **several in one call**. **One feed per goal, never both**, so nothing counts twice: either a **Money moved category** — a transaction categorized under it is a deposit into the goal, read from the bank — or, with `from_holding: true`, **the rows that name its default holding**: every buy or sale of that holding inside an account that holds things (`set_transaction_holdings`) is a deposit, and the bank transfer that brought the cash there feeds nothing — or, with `from_transfers: true`, **the confirmed transfers into the bank or cash account whose own Cash is its default holding**: each transfer pair (`answer_transfers`) arriving there is a deposit and one leaving it a withdrawal, after its Was already there; off unless asked, since a main account takes transfers that are not for a goal. A broker’s cash is no such feed. Categorizing is the only step, and a row re-categorized elsewhere takes its deposit with it. One goal per category, per holding and per account; the category must be a Money moved one (a transfer or a saving, not a spending category), and the goal needs a default holding for the money to sit in: `default_holding` sets it, in the same entry or on its own, as the Goals form’s "Where is the money?" does. `category: null` stops either feed, and removes the deposits it had derived. Money you set aside by hand (`record_deposits`) and what was already there are never touched. `savings` shows what a holding’s rows put in, against the cost basis a reading printed. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填goals必填commit

set_cushion

Set the cushion

Set the cushion. **What is in the bank** is the latest statement balance of the bank accounts that count — **every bank account and cash, until `accounts` unticks it**; **each goal saved in one of them holds its part** (its deposits into that account’s own cash holding), and **the cushion is what is left**, against a target of `months` of bills plus the Plan `buckets` (Groceries, say) counted beside the bills. No goal is the cushion and setting it makes none. Goals holding more than an account, or than the whole, is a failed check, never shrunk to fit. Every answer ends with the split as it stands. Wealth › Net worth and Plan both show it. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填commitmonthsbucketsaccounts

pause_goals

Pause goals

Pause goals for a number of months, or start them again, **several in one call**. A paused goal is skipped by the projection and gives its monthly amount back to what is left in Plan for those months; it is not deleted and keeps its history. `months` counts this month: 1 skips this month only. 0 starts the goal again. Plan's own bucket is left alone. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填goals必填commit

settle_set_aside

Is it still set aside?

Answer, for a goal, whether money **set aside** (promised to it but still inside the bank) is still there. A set-aside amount nobody has confirmed for more than 90 days is the one that is asked about; `goal_projection` lists them. `still_there: true` records today against them so they are not asked about again for 90 days. `false` withdraws them from the goal, with the reason written, so the balance stops counting money that is gone. Several goals in one call. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填goals必填commit

suggest_goal_amounts

Suggest a split

Suggest each goal's monthly amount: goals by nearest date (no kind of goal goes first), each taking what gets it there on its date until what Plan has for goals runs out (what they take now plus what is left after every bucket). A goal with no date is sized as a year of what is missing; a reached, target-less or paused goal gets nothing. The preview is the suggestion; `commit: true` writes it through the same rule a typed amount on Goals uses, so the rest of the split moves with it. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填commit

set_goal_amounts

Set what goes toward goals and debts each month

Set the monthly amount toward a goal, or the extra on a debt: the Plan bucket for it, in the default plan. The difference comes from (or goes back to) Plan's Everything else, as on the Goals and Debts screens; a goal or debt with no bucket gets one. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填commitamounts必填

set_opening_balances

Set accounts' opening balances

Say what each bank or cash account **held before its first statement**, and the day that stands for everything before — **several in one call**. A document cannot read this off a file. The answer shows the check it buys: opening, plus what the statements moved since, has to equal the balance the last statement ended on, and the row says by how much when it does not. `clear: true` forgets one. A credit card has none. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填commitaccounts必填

create_debts

Create debts

Add debts — **several in one call**. A debt is the original principal plus a stream of payments; **the balance is derived, never stored**, so there is nothing here that sets one. Optionally set how payments are discovered: a merchant pattern for a standing order, or a category. **Dry-runs by default**; the document is copied to a timestamped .bak first. Editing, closing and deleting a debt are app-only, as is promoting a card installment group.

输入参数path必填debts必填commit

record_debt_payments

Record debt payments

Record payments against a debt — **several in one call**. The amount is a magnitude; the direction is known. A payment is **all principal** unless it names an interest portion, because v1 has no amortization schedule. Paying more than is outstanding is allowed and is flagged. **Dry-runs by default**; the document is copied to a timestamped .bak first. Payments the app discovered at import are a separate thing and are confirmed in the app.

输入参数path必填commitpayments必填

guide_status

Guide: what to set up, and what is done

In a document that keeps the ledger: the places worth setting up, in order — reading statements and paychecks, making the list of categories yours, giving each merchant a category, the bills, the plan, the opening balance and the cushion — with which are done and what the document shows for each, and the tool that does each one. The same list and the same checks as the app’s Guide me. Call it after create_document, or when the user asks what is left to set up. Reads the document; changes nothing.

输入参数path必填

update_account

Rename an account or set its last 4 digits

Give an account a real name and the last 4 digits of its card or number. The digits are what matches an imported statement to the right account, and what identifies a card charge on a bank statement, so they are worth asking for. Account ids come from document_info.

输入参数kindnamepath必填last4account_id必填

choose_categories

Archive or restore categories

Put categories away, or bring them back. Give `archive_ids` and `restore_ids` to say exactly which — these work at any time. On a document that holds no account and no transaction yet, `keep_ids` is also accepted and archives everything not listed, which is the fast way to trim the shipped list; it is refused afterwards, when one forgotten id would archive a category out from under a year of transactions. Archiving is not deleting — the history stays and comes back with it — and system categories are never touched. Ids come from category_tree. On a document that already holds work it takes a timestamped .bak first.

输入参数path必填keep_idsarchive_idsrestore_ids

create_category

Create categories

Add categories the document does not have — **a list of them in one call**. `choose_categories` only keeps or archives the ones already there, so this is what answers "none of these fit". Give a `parent_id` or `parent_name` to put it under another, in which case it takes the parent’s kind and `kind` is ignored. Categories nest three levels at most (Living → Utilities → Electricity): a top-level category is a heading — Living, Lifestyle, Income — and a Plan bucket for it counts everything under it. Entries are applied in order, so a parent and its children go in the same call. If a category of the same name already sits in the same place nothing is created: a live one is reported back with its id, an archived one is brought back rather than twinned. Writes immediately — the change is additive, so there is nothing to preview.

输入参数path必填categories

edit_categories

Rename, colour, move, merge, archive or delete categories

Change categories that exist — **a list of edits in one call**. Per entry: `name`, `parent_id` (any category of its kind with room — three levels at most — or null for the top level), `move` (up or down), `archived`, `monthly_target` (major units, null clears — the category’s Plan bucket in the plan in use: one figure, shared with Plan), `color` (category-1 … category-11, null for automatic), `icon`, or, alone in the entry, `merge_into` (a category id) or `delete: true` — with `move_to` (a category id of the same kind) when rows, rules, subcategories, Plan buckets, bills, debts or goals still use it. This is the Categories screen’s rename, colour, move, merge, archive and delete, and its Plan bucket. A heading (Living, Income…) is a top-level category, renamed and moved like any other. Adding categories is `create_category`. **Dry-runs by default**: it says what each entry would do and which it refuses (a taken name, a fourth level, a delete with nowhere to move what uses it) and writes nothing until you call again with `commit: true`. The document is copied to a timestamped .bak first. Ids come from `category_tree`.

输入参数path必填editscommit

set_excluded_from_totals

Leave rows out of the totals, or count them again

Leave transactions out of every total — **a list of ids in one call** — or count them again with `excluded: false`. A row left out stays in the ledger and is shown marked; it is simply not summed. This is what the Transactions screen’s row menu does: a card bill the bank statement shows as one charge is the same money as the card statement’s rows, and counting both doubles the month. **Dry-runs by default** and says how much it would take off the totals; commit with `commit: true` after the user has said yes. The document is copied to a timestamped .bak first. Ids come from `transactions`.

输入参数path必填commitexcludedtransaction_ids必填

set_transaction_notes

Add, edit or clear notes on transactions

Write the note on a transaction — **a list in one call**. An empty `note` clears it. This is the row menu’s *Add a note*: a line about one charge that is worth remembering. **Dry-runs by default**; commit with `commit: true` after the user has seen the table. Ids come from `transactions`.

输入参数path必填notes必填commit

set_transaction_holdings

Say what a transaction’s cash turned into

Name the holding a transaction bought or sold, and how much of it — **a list in one call**. Such a row is cash turning into a holding inside one account (a broker buying a fund, a savings account opening a deposit): it counts in neither came in, went out nor moved, and the holding’s quantity is the sum of its rows’ quantities. `quantity` is a count of the thing, never money — bought is positive, sold negative, up to six places. `holding: null` clears both. The holding must sit at a place that names the row’s own account, or at one that names none, and is never an account’s own cash. Holdings and their ids are listed by `savings`; transaction ids by `transactions`. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填rows必填commit

transfers

Transfers between the person’s own accounts

Lists the transfers between two of the person’s own accounts that imports have found: a row leaving one account and the same amount arriving in another within a few days. Undecided ones by default; `answer` reads the transfers or the rows that are not one. An answered transfer is neither income nor spending. `answer_transfers` answers them.

输入参数path必填answer

answer_transfers

Answer transfers between the person’s own accounts

Answers the pairs `transfers` lists — **a list in one call**. `transfer`: one transfer between two of the person’s accounts, so neither row is income or spending (the end on an ordinary account counts as money moved, the end on an account with holdings is in no total). `not_a_transfer`: two rows that only looked like one; both stay what they were and are never suggested again. `undecided` takes an answer back. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填pairs必填commit

count_card_charges

Count a card payment with no statement as spending

The card payments in a bank statement for a card this document has no statement of — that month of card spending is otherwise missing. Without `cards`, lists them. With `cards` (the last four digits, or `unknown`), counts each card’s payments as the spending they stand for, so Home stops asking; the rows already count, so no figure moves. Reading the card’s statement later still makes them card payments to answer. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填cardscommit

set_no_pay_months

Say a month had no pay

Says months had no pay — between jobs, unpaid leave — so no paycheck is missing for them: Home stops asking "No paycheck for …" and `paychecks` stops listing them as gaps. `no_pay: false` takes it back. Stores the answer only; no paycheck and no figure. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数path必填commitmonths必填no_pay

set_card_payers

Say which bank rows pay a card

Say that a bank merchant pays a credit card — **a list in one call**. Give the bank `transaction_id` and the `card_account_id` it pays (ids from `document_info`); omit the card to take the answer back. This is *This pays a card* on a Transactions row and on a By merchant row. Optionally `counts` says whether those bank rows count in the totals: false leaves them out, because the card’s own rows already hold that money. **Dry-runs by default**; commit with `commit: true` once the user has said yes.

输入参数path必填commitpayments必填

skip_merchants

Skip merchants for now, or put them back

Set merchants aside in the By merchant queue — **a list in one call** — or put them back with `skip: false`. "I do not know what this is" is a real answer to a cryptic bank transfer; a skip writes nothing to any transaction. Names are the normalized ones `uncategorized_merchants` prints. **Dry-runs by default**; commit with `commit: true`.

输入参数path必填skipcommitmerchants必填

assign_category

Assign categories to transactions

Categorize the transactions with no category — **many merchants in one call**. Send every merchant-to-category decision you have worked out as one `assignments` list rather than calling this once per merchant. **Dry-runs by default**: it returns what the whole batch would change and writes nothing until you call it again with commit true, so show the user the table and let them answer once. Rows categorized by hand are never touched, rows assigned by a rule are left to the rule, and every row it does change is marked AI so the batch stays findable. The document is copied to a timestamped .bak before anything is written. A row covered by two assignments refuses the batch rather than picking one.

输入参数path必填commitassignments必填

note_merchant_suggestions

Note what was suggested about merchants

Remember what this conversation suggested for merchants that were **not** categorized — a category the user declined, or a name nobody could identify — so the next conversation does not ask about the same names again. Shown in the Asked about column of uncategorized_merchants, cleared automatically when a merchant is categorized. Writes immediately: it changes no transaction, no category and no rule.

输入参数path必填suggestions必填

create_rule

Create a categorization rule

Create a standing rule that categorizes matching transactions into a category, now and on every future import. **This is the widest write here** — it reaches the whole history — so it needs two calls: the first returns the transactions the rule would match plus a confirmation token, and the second must quote that token back. Show the user the matches before you commit; the token expires the moment the match set changes. Rows categorized by hand are never touched.

输入参数kindnamepath必填commitpatternaccount_idamount_maxamount_mincategory_id必填confirmationapply_to_existing

statement_preview

Look at a statement file

Read a card or bank statement (.xlsx, .xls, .csv) and report what is in it: which sheet, where the header row is, what each column looks like and what the app guesses it is, the first rows verbatim, and whether a reader already recognises these columns. Changes nothing — call this before import_statement so the user can confirm the columns. **The user maps the columns; the app only guesses**, so show them this and ask.

输入参数file必填path必填sheetcolumnsheader_rowamount_convention

import_statement

Import a statement

Import a card or bank statement into an account, through the app’s own pipeline: the same parse, the same dedupe by natural key, the same rules applied on commit. **Dry-runs by default** — it reports what it would import and how the parse compares with the total the file prints for itself, and writes nothing until commit is true. **A file whose printed total does not reconcile is refused**, because importing over it puts numbers in the ledger the file itself says are wrong. Re-importing the same statement is safe: duplicates are matched and skipped.

输入参数file必填path必填sheetcommitcolumnsaccount_idheader_rownew_accountsave_mapping_asamount_conventionignore_printed_total

record_holdings_statement

Record a statement of an account with holdings

Write what a broker’s (or any account with holdings’) statement printed — **the whole file in one call**: its movements onto the account, and its holdings table as dated readings. **You read the file** — a PDF, usually — and send what it printed; nothing here parses it. Each row is the **value date**, the description as printed, the signed `amount` (a buy negative, a deposit or a sale positive; a fee printed in its own column is already inside the amount), and `cash_after`, the cash balance the statement printed after it. A buy or a sale also names the `holding` and the `quantity` (bought positive, sold negative). **The balances are a check**: each must be the one before it plus the row, and the batch is refused at the first that is not — a misread figure. **They are also each row’s key** (date, amount, balance after), so an overlapping file — a half-year export repeating the months — adds only what is new, and the same file again adds nothing. A row of amount 0 naming no holding (the broker’s tax bookkeeping) is left out. `holdings` is the holdings table: per holding its `value`, and when printed its `quantity`, `cost_basis` and `price` (one unit, in major units — a price printed in agorot is divided by 100); `cash` is the cash line. Each is checked: the cost against what the holding’s rows put in by `as_of`, the quantity against theirs, quantity × price against the value, and the cash against the account’s rows. Holdings are named by id or name (`savings` lists them); a name the place does not have is made there. **Dry-runs by default**; the document is copied to a timestamped .bak first.

输入参数cashfile必填frompath必填rowsas_of必填placecommitcurrencyholdingsaccount_idnew_account

frame_copy

The document, for a view drawn in the conversation

Not for a model: a view drawn in the conversation calls it with its own key to fetch its document, and any other call is refused. Read rows with `format_rows`.

输入参数key必填path必填since

版本历史

6
  1. v0.3.0最新Nov 3, 2026
  2. v0.2.0Oct 23, 2026
  3. v0.1.6Oct 20, 2026
  4. v0.1.5Oct 19, 2026
  5. v0.1.2Oct 18, 2026
  6. v0.1.1Oct 18, 2026

评分

已满足适用检查项的 90%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分
  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具必需15 分
  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分