@cerefox/memory 1.3.0-beta.1 → 1.3.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +10 -1
- package/AGENT_QUICK_REFERENCE.md +5 -0
- package/dist/bin/cerefox.js +27 -25
- package/dist/server-assets/_shared/ef-meta/index.ts +2 -2
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +2 -2
- package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +33 -3
- package/dist/server-assets/_shared/partial-edits/index.ts +497 -0
- package/package.json +1 -1
package/AGENT_GUIDE.md
CHANGED
|
@@ -140,7 +140,7 @@ Add text to a document **without resending it**. Purely additive: this tool cann
|
|
|
140
140
|
| `text` | Yes | Markdown to insert. Blank-line separation from surrounding content is handled for you. |
|
|
141
141
|
| `position` | Yes | `end_of_document` (plain append) · `end_of_section` (add to a section's body — the most common mid-document add) · `after_heading` (lead-in text) · `before_heading` (a new block above a section). |
|
|
142
142
|
| `anchor_heading` | Unless `end_of_document` | The exact heading line (`## Intake`) or a ` > ` parent path (`## Intake > ### Notes`) when a heading appears more than once. |
|
|
143
|
-
| `section_part` | Sometimes |
|
|
143
|
+
| `section_part` | Sometimes | Required when the target section **has child sections** (whether or not it also has its own body): `own_body` (before the first child) or `subtree` (after everything nested under it). These can be far apart, so the tool refuses rather than choosing; the error lists both options. |
|
|
144
144
|
| `expected_content_hash` | **Yes** | The hash of the version you are basing this on. There is **no `last_write_wins` on this tool**. |
|
|
145
145
|
| `requestor` | No | Your agent name. |
|
|
146
146
|
|
|
@@ -163,6 +163,15 @@ Change parts of a document: **one to many operations applied atomically in a sin
|
|
|
163
163
|
|
|
164
164
|
**Put changes that belong together in ONE call.** Operations apply in order against the evolving document (op 2 sees op 1's result), and a half-applied state is impossible — so a table row and the running total it feeds cannot end up disagreeing. If any operation fails (bad anchor, ambiguity), nothing at all is written and the error names the failing operation.
|
|
165
165
|
|
|
166
|
+
**One sharp edge worth knowing.** A section runs to the next heading of the same
|
|
167
|
+
or higher level — **or to the end of the document**. So the last section owns
|
|
168
|
+
everything appended after it: an `end_of_document` insert becomes part of that
|
|
169
|
+
section's body, and a later `replace_section` or `delete_section` on that heading
|
|
170
|
+
removes it along with the rest. This is correct addressing, not a bug, but it is
|
|
171
|
+
silent. If a write reports a large shrink, that is the warning; the previous
|
|
172
|
+
content is in `cerefox_list_versions`. To append somewhere a later section edit
|
|
173
|
+
cannot swallow, give the appended material its own heading.
|
|
174
|
+
|
|
166
175
|
**To change a single line**, `replace_section` on its smallest enclosing heading and resend just that section. That is the intended granularity — line-level anchors were deliberately excluded because they silently edit the wrong place.
|
|
167
176
|
|
|
168
177
|
The audit trail records each operation distinctly (`insert` / `replace-section` / `delete-section`), so *added to*, *rewrote* and *removed* stay distinguishable from a full rewrite.
|
package/AGENT_QUICK_REFERENCE.md
CHANGED
|
@@ -50,6 +50,11 @@ diff. Use the partial-edit tools instead:
|
|
|
50
50
|
conflict means someone else changed the document; re-read and decide, do not
|
|
51
51
|
force it.
|
|
52
52
|
|
|
53
|
+
**A section runs to the next same-or-higher heading, or to the end of the
|
|
54
|
+
document.** So `end_of_document` inserts land inside the *last* section, and
|
|
55
|
+
replacing or deleting that section removes them too. A large shrink in the
|
|
56
|
+
response is your warning; `cerefox_list_versions` has the previous content.
|
|
57
|
+
|
|
53
58
|
**When an anchor is ambiguous the tool refuses and hands you the options** — a
|
|
54
59
|
repeated heading returns the qualifying paths, and a section with both its own
|
|
55
60
|
content and sub-sections returns both `section_part` choices. That is a
|
package/dist/bin/cerefox.js
CHANGED
|
@@ -7438,7 +7438,7 @@ var exports_meta = {};
|
|
|
7438
7438
|
__export(exports_meta, {
|
|
7439
7439
|
PKG_VERSION: () => PKG_VERSION
|
|
7440
7440
|
});
|
|
7441
|
-
var PKG_VERSION = "1.3.0-beta.
|
|
7441
|
+
var PKG_VERSION = "1.3.0-beta.3";
|
|
7442
7442
|
var init_meta = () => {};
|
|
7443
7443
|
|
|
7444
7444
|
// ../../_shared/config/paths.ts
|
|
@@ -26304,9 +26304,6 @@ function resolveAnchor(outline, anchorHeading) {
|
|
|
26304
26304
|
}
|
|
26305
26305
|
throw new AnchorNotFoundError(anchor, outline);
|
|
26306
26306
|
}
|
|
26307
|
-
function hasOwnBody(content, node) {
|
|
26308
|
-
return content.slice(node.bodyStart, node.ownBodyEnd).trim().length > 0;
|
|
26309
|
-
}
|
|
26310
26307
|
function firstChild(outline, node) {
|
|
26311
26308
|
for (const n of outline) {
|
|
26312
26309
|
if (n.start >= node.bodyStart && n.start < node.subtreeEnd)
|
|
@@ -26314,7 +26311,7 @@ function firstChild(outline, node) {
|
|
|
26314
26311
|
}
|
|
26315
26312
|
return null;
|
|
26316
26313
|
}
|
|
26317
|
-
function resolveSectionEnd(content, outline, node, sectionPart, opName
|
|
26314
|
+
function resolveSectionEnd(content, outline, node, sectionPart, opName) {
|
|
26318
26315
|
const child = firstChild(outline, node);
|
|
26319
26316
|
if (!child)
|
|
26320
26317
|
return node.subtreeEnd;
|
|
@@ -26322,11 +26319,6 @@ function resolveSectionEnd(content, outline, node, sectionPart, opName, destruct
|
|
|
26322
26319
|
return node.ownBodyEnd;
|
|
26323
26320
|
if (sectionPart === "subtree")
|
|
26324
26321
|
return node.subtreeEnd;
|
|
26325
|
-
if (!hasOwnBody(content, node)) {
|
|
26326
|
-
if (!destructive)
|
|
26327
|
-
return node.subtreeEnd;
|
|
26328
|
-
throw new AmbiguousPositionError(node, child.heading, opName);
|
|
26329
|
-
}
|
|
26330
26322
|
throw new AmbiguousPositionError(node, child.heading, opName);
|
|
26331
26323
|
}
|
|
26332
26324
|
function spliceBlock(content, from, to, text) {
|
|
@@ -26370,7 +26362,7 @@ function applyOne(content, operation) {
|
|
|
26370
26362
|
at = node2.bodyStart;
|
|
26371
26363
|
detail = "insert after_heading";
|
|
26372
26364
|
} else {
|
|
26373
|
-
at = resolveSectionEnd(content, outline, node2, operation.section_part, "end_of_section insert"
|
|
26365
|
+
at = resolveSectionEnd(content, outline, node2, operation.section_part, "end_of_section insert");
|
|
26374
26366
|
detail = `insert at end_of_section` + (operation.section_part ? ` (${operation.section_part})` : "");
|
|
26375
26367
|
}
|
|
26376
26368
|
return {
|
|
@@ -26380,7 +26372,7 @@ function applyOne(content, operation) {
|
|
|
26380
26372
|
}
|
|
26381
26373
|
if (operation.op === "replace_section") {
|
|
26382
26374
|
const node2 = resolveAnchor(outline, operation.anchor_heading);
|
|
26383
|
-
const to2 = resolveSectionEnd(content, outline, node2, operation.section_part, "replace_section"
|
|
26375
|
+
const to2 = resolveSectionEnd(content, outline, node2, operation.section_part, "replace_section");
|
|
26384
26376
|
return {
|
|
26385
26377
|
content: spliceBlock(content, node2.bodyStart, to2, operation.text),
|
|
26386
26378
|
applied: {
|
|
@@ -26392,7 +26384,7 @@ function applyOne(content, operation) {
|
|
|
26392
26384
|
}
|
|
26393
26385
|
const node = resolveAnchor(outline, operation.anchor_heading);
|
|
26394
26386
|
const scope = operation.scope ?? "body_only";
|
|
26395
|
-
const to = resolveSectionEnd(content, outline, node, operation.section_part, "delete_section"
|
|
26387
|
+
const to = resolveSectionEnd(content, outline, node, operation.section_part, "delete_section");
|
|
26396
26388
|
const from = scope === "heading_and_body" ? node.start : node.bodyStart;
|
|
26397
26389
|
return {
|
|
26398
26390
|
content: spliceBlock(content, from, to, ""),
|
|
@@ -26497,14 +26489,14 @@ ${outline.map((n) => ` ${n.path}`).join(`
|
|
|
26497
26489
|
const candidates = [
|
|
26498
26490
|
{
|
|
26499
26491
|
section_part: "own_body",
|
|
26500
|
-
description: `
|
|
26492
|
+
description: `just this section's own content, stopping before its first child ` + `(${firstChildHeading}) — often only a line or two below the heading`
|
|
26501
26493
|
},
|
|
26502
26494
|
{
|
|
26503
26495
|
section_part: "subtree",
|
|
26504
|
-
description: `the whole subtree,
|
|
26496
|
+
description: `the whole subtree, past everything nested under ${node.heading} — ` + `which can be a long way down`
|
|
26505
26497
|
}
|
|
26506
26498
|
];
|
|
26507
|
-
super(`Ambiguous position: "${node.path}" has
|
|
26499
|
+
super(`Ambiguous position: "${node.path}" has child sections, so "the end of ` + `the section" could mean two different places and ${opName} will not guess. ` + `No write was performed. Pass section_part to choose:
|
|
26508
26500
|
` + candidates.map((c2) => ` section_part: "${c2.section_part}" — ${c2.description}`).join(`
|
|
26509
26501
|
`));
|
|
26510
26502
|
this.name = "AmbiguousPositionError";
|
|
@@ -55069,11 +55061,11 @@ var init_get_document = __esm(() => {
|
|
|
55069
55061
|
});
|
|
55070
55062
|
|
|
55071
55063
|
// ../../_shared/mcp-tools/get-help-content.ts
|
|
55072
|
-
var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **16 MCP tools** (15 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for "How AI Agents Use Cerefox" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash | `document_id` (required), `outline` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project\'s docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc\'s project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.\n\n## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround.\n4. Both require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., "Claude Code", "archiver"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it\'s stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer\'s work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch("topic") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title="Same Title", content="...", document_id="abc123",\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch("topic") -> find doc (note its hash) -> modify ->\ningest(title="Same Title", content="...", update_if_exists=true,\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search "<q>" --requestor "<your-name>"` |\n| `cerefox_ingest` (paste) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --author "<your-name>" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --document-id "<uuid>" --expected-content-hash "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor "<your-name>"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor "<your-name>"` |\n| `cerefox_list_projects` | `cerefox project list --requestor "<your-name>"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter \'<json>\' --requestor "<your-name>"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author "<your-name>" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor "<your-name>"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author "<your-name>" --author-type agent`\n- Reads: `--requestor "<your-name>"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n', HELP_SECTIONS, HELP_SECTION_HEADINGS;
|
|
55064
|
+
var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **16 MCP tools** (15 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for "How AI Agents Use Cerefox" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash | `document_id` (required), `outline` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project\'s docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc\'s project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.\n\n## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround.\n4. Both require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**A section runs to the next same-or-higher heading, or to the end of the\ndocument.** So `end_of_document` inserts land inside the *last* section, and\nreplacing or deleting that section removes them too. A large shrink in the\nresponse is your warning; `cerefox_list_versions` has the previous content.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., "Claude Code", "archiver"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it\'s stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer\'s work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch("topic") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title="Same Title", content="...", document_id="abc123",\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch("topic") -> find doc (note its hash) -> modify ->\ningest(title="Same Title", content="...", update_if_exists=true,\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search "<q>" --requestor "<your-name>"` |\n| `cerefox_ingest` (paste) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --author "<your-name>" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --document-id "<uuid>" --expected-content-hash "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor "<your-name>"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor "<your-name>"` |\n| `cerefox_list_projects` | `cerefox project list --requestor "<your-name>"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter \'<json>\' --requestor "<your-name>"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author "<your-name>" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor "<your-name>"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author "<your-name>" --author-type agent`\n- Reads: `--requestor "<your-name>"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n', HELP_SECTIONS, HELP_SECTION_HEADINGS;
|
|
55073
55065
|
var init_get_help_content = __esm(() => {
|
|
55074
55066
|
HELP_SECTIONS = {
|
|
55075
55067
|
Tools: "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash | `document_id` (required), `outline` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.",
|
|
55076
|
-
"Editing part of a document (prefer this over re-sending)": '## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround.\n4. Both require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.',
|
|
55068
|
+
"Editing part of a document (prefer this over re-sending)": '## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround.\n4. Both require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**A section runs to the next same-or-higher heading, or to the end of the\ndocument.** So `end_of_document` inserts land inside the *last* section, and\nreplacing or deleting that section removes them too. A large shrink in the\nresponse is your warning; `cerefox_list_versions` has the previous content.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.',
|
|
55077
55069
|
"Essential Rules": '## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., "Claude Code", "archiver"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it\'s stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer\'s work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.',
|
|
55078
55070
|
"Update Workflow (ID-based -- preferred)": `## Update Workflow (ID-based -- preferred)
|
|
55079
55071
|
|
|
@@ -55454,6 +55446,16 @@ function resolveAuthorType2(ctx, args) {
|
|
|
55454
55446
|
function defaultRequestor(ctx) {
|
|
55455
55447
|
return ctx.accessPath === "cli" ? "cli-user" : "mcp-agent";
|
|
55456
55448
|
}
|
|
55449
|
+
function shrinkNote(before, after) {
|
|
55450
|
+
const lost = before - after;
|
|
55451
|
+
if (lost <= 0)
|
|
55452
|
+
return "";
|
|
55453
|
+
const pct = Math.round(lost / Math.max(before, 1) * 100);
|
|
55454
|
+
if (pct < 25)
|
|
55455
|
+
return "";
|
|
55456
|
+
return `⚠ This edit removed ${lost} characters (${pct}% smaller). If you did not ` + `intend that, note that a section runs to the next heading of the same or ` + `higher level — or to the end of the document — so replacing or deleting ` + `the LAST section also removes anything appended after it. ` + `cerefox_list_versions has the previous content.
|
|
55457
|
+
`;
|
|
55458
|
+
}
|
|
55457
55459
|
function conflictError2(documentId, expectedHash, currentHash) {
|
|
55458
55460
|
return new Error(`Conflict: document ${documentId} changed since you read it ` + `(your base hash: ${expectedHash}, current hash: ${currentHash}). No write was performed. ` + `To resolve: (1) cerefox_get_document("${documentId}", outline=true) to see the current ` + `structure and hash cheaply, or a full read if you need the text, (2) decide whether your ` + `edit still applies — another writer may have already made it, or made something it ` + `contradicts, (3) retry with expected_content_hash set to the current hash. ` + `These tools have no last-write-wins: the other writer's work is not yours to discard.`);
|
|
55459
55461
|
}
|
|
@@ -55560,8 +55562,8 @@ async function applyAndWrite(supabase, ctx, args) {
|
|
|
55560
55562
|
${summary}
|
|
55561
55563
|
|
|
55562
55564
|
` + `New content_hash: ${row?.content_hash ?? newHash}
|
|
55563
|
-
` + `Size: ${row?.total_chars ?? totalChars} chars, ${chunks.length} chunk(s).
|
|
55564
|
-
` + `Pass the new content_hash as expected_content_hash on your next edit.${warning2}`;
|
|
55565
|
+
` + `Size: ${row?.total_chars ?? totalChars} chars (was ${doc.content.length}), ${chunks.length} chunk(s).
|
|
55566
|
+
` + shrinkNote(doc.content.length, row?.total_chars ?? totalChars) + `Pass the new content_hash as expected_content_hash on your next edit.${warning2}`;
|
|
55565
55567
|
}
|
|
55566
55568
|
async function insertHandler(supabase, args, ctx) {
|
|
55567
55569
|
const documentId = args.document_id?.trim();
|
|
@@ -55654,7 +55656,7 @@ var init_partial_edits2 = __esm(() => {
|
|
|
55654
55656
|
section_part: {
|
|
55655
55657
|
type: "string",
|
|
55656
55658
|
enum: ["own_body", "subtree"],
|
|
55657
|
-
description: "Only for end_of_section when the section
|
|
55659
|
+
description: "Only for end_of_section when the target section HAS CHILD SECTIONS: own_body = before the first child, subtree = after everything nested under it. These can be far apart, so the tool refuses rather than choosing. Omit it otherwise; you will be told (with both options) whenever it is needed."
|
|
55658
55660
|
},
|
|
55659
55661
|
expected_content_hash: {
|
|
55660
55662
|
type: "string",
|
|
@@ -55710,7 +55712,7 @@ var init_partial_edits2 = __esm(() => {
|
|
|
55710
55712
|
section_part: {
|
|
55711
55713
|
type: "string",
|
|
55712
55714
|
enum: ["own_body", "subtree"],
|
|
55713
|
-
description: "Only when the target section has
|
|
55715
|
+
description: "Only when the target section has child sections. You will be told (with both options) whenever it is needed."
|
|
55714
55716
|
},
|
|
55715
55717
|
scope: {
|
|
55716
55718
|
type: "string",
|
|
@@ -76319,8 +76321,8 @@ import { homedir as homedir6 } from "node:os";
|
|
|
76319
76321
|
import { join as join9 } from "node:path";
|
|
76320
76322
|
|
|
76321
76323
|
// ../../_shared/ef-meta/index.ts
|
|
76322
|
-
var EF_VERSION = "1.3.0-beta.
|
|
76323
|
-
var EF_LAST_CHANGED = "1.3.0-beta.
|
|
76324
|
+
var EF_VERSION = "1.3.0-beta.3";
|
|
76325
|
+
var EF_LAST_CHANGED = "1.3.0-beta.3";
|
|
76324
76326
|
|
|
76325
76327
|
// src/cli/util/checks.ts
|
|
76326
76328
|
init_config();
|
|
@@ -78038,7 +78040,7 @@ async function runTool(toolName, args) {
|
|
|
78038
78040
|
}
|
|
78039
78041
|
}
|
|
78040
78042
|
function registerDocumentInsert(program2) {
|
|
78041
|
-
program2.command("insert <document-id>").description("Add text to a document without resending it (purely additive)").requiredOption("-t, --text <text>", "Markdown to insert. Use '-' for stdin or '@path' for a file.").option("-p, --position <position>", "end_of_document | end_of_section | after_heading | before_heading", "end_of_document").option("-a, --anchor-heading <heading>", "Heading line, or a ' > ' path. Required unless end_of_document.").option("--section-part <part>", "own_body | subtree —
|
|
78043
|
+
program2.command("insert <document-id>").description("Add text to a document without resending it (purely additive)").requiredOption("-t, --text <text>", "Markdown to insert. Use '-' for stdin or '@path' for a file.").option("-p, --position <position>", "end_of_document | end_of_section | after_heading | before_heading", "end_of_document").option("-a, --anchor-heading <heading>", "Heading line, or a ' > ' path. Required unless end_of_document.").option("--section-part <part>", "own_body | subtree — required when the target section has child sections").requiredOption("-e, --expected-content-hash <hash>", "content_hash you are basing this on (cerefox document get --outline shows it)").option("--requestor <name>", "Recorded in the usage log", "cli-user").option("--author-type <type>", "user (default for the CLI) or agent, when scripting on an agent's behalf", "user").action(async (documentId, options) => {
|
|
78042
78044
|
await runTool("cerefox_insert", {
|
|
78043
78045
|
document_id: documentId,
|
|
78044
78046
|
text: resolveText(options.text, "--text"),
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* doesn't touch `supabase/functions/` leaves it alone).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
export const EF_VERSION = "1.3.0-beta.
|
|
21
|
+
export const EF_VERSION = "1.3.0-beta.3";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* The most recent version whose EF-side SOURCE actually changed (#127).
|
|
@@ -28,7 +28,7 @@ export const EF_VERSION = "1.3.0-beta.1";
|
|
|
28
28
|
* `cut_release.ts` ONLY when EF source changed since the last tag; doctor
|
|
29
29
|
* uses it to stay silent on label-only drift.
|
|
30
30
|
*/
|
|
31
|
-
export const EF_LAST_CHANGED = "1.3.0-beta.
|
|
31
|
+
export const EF_LAST_CHANGED = "1.3.0-beta.3";
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* The 8 peer EFs the cerefox-mcp aggregator probes (excludes cerefox-mcp
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* docs/specs/polish-and-distribution-design.md §10d.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export const HELP_FULL = "# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **16 MCP tools** (15 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for \"How AI Agents Use Cerefox\" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash | `document_id` (required), `outline` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.\n\n## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so \"I meant to append\" cannot become \"I replaced the file\".\n3. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround.\n4. Both require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it's stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer's work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\",\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc (note its hash) -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true,\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search \"<q>\" --requestor \"<your-name>\"` |\n| `cerefox_ingest` (paste) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --document-id \"<uuid>\" --expected-content-hash \"<hash>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor \"<your-name>\"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor \"<your-name>\"` |\n| `cerefox_list_projects` | `cerefox project list --requestor \"<your-name>\"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author \"<your-name>\" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor \"<your-name>\"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author \"<your-name>\" --author-type agent`\n- Reads: `--requestor \"<your-name>\"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n";
|
|
14
|
+
export const HELP_FULL = "# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **16 MCP tools** (15 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for \"How AI Agents Use Cerefox\" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash | `document_id` (required), `outline` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.\n\n## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so \"I meant to append\" cannot become \"I replaced the file\".\n3. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround.\n4. Both require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**A section runs to the next same-or-higher heading, or to the end of the\ndocument.** So `end_of_document` inserts land inside the *last* section, and\nreplacing or deleting that section removes them too. A large shrink in the\nresponse is your warning; `cerefox_list_versions` has the previous content.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it's stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer's work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\",\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc (note its hash) -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true,\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search \"<q>\" --requestor \"<your-name>\"` |\n| `cerefox_ingest` (paste) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --document-id \"<uuid>\" --expected-content-hash \"<hash>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor \"<your-name>\"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor \"<your-name>\"` |\n| `cerefox_list_projects` | `cerefox project list --requestor \"<your-name>\"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author \"<your-name>\" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor \"<your-name>\"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author \"<your-name>\" --author-type agent`\n- Reads: `--requestor \"<your-name>\"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n";
|
|
15
15
|
|
|
16
16
|
/** Sections keyed by their H2 heading text (lower-cased for matching). */
|
|
17
17
|
export const HELP_SECTIONS: Record<string, string> = {
|
|
18
18
|
"Tools": "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash | `document_id` (required), `outline` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.",
|
|
19
|
-
"Editing part of a document (prefer this over re-sending)": "## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so \"I meant to append\" cannot become \"I replaced the file\".\n3. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround.\n4. Both require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.",
|
|
19
|
+
"Editing part of a document (prefer this over re-sending)": "## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so \"I meant to append\" cannot become \"I replaced the file\".\n3. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround.\n4. Both require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**A section runs to the next same-or-higher heading, or to the end of the\ndocument.** So `end_of_document` inserts land inside the *last* section, and\nreplacing or deleting that section removes them too. A large shrink in the\nresponse is your warning; `cerefox_list_versions` has the previous content.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.",
|
|
20
20
|
"Essential Rules": "## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it's stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer's work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.",
|
|
21
21
|
"Update Workflow (ID-based -- preferred)": "## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\",\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.",
|
|
22
22
|
"Update Workflow (title-based -- fallback)": "## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc (note its hash) -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true,\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```",
|
|
@@ -53,6 +53,35 @@ function defaultRequestor(ctx: ToolContext): string {
|
|
|
53
53
|
return ctx.accessPath === "cli" ? "cli-user" : "mcp-agent";
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Surface a large shrink in the response the agent already reads.
|
|
59
|
+
*
|
|
60
|
+
* A section extends to the next heading of equal-or-higher level **or to the
|
|
61
|
+
* end of the document**, so the last section owns everything appended after it.
|
|
62
|
+
* That means an `end_of_document` insert becomes part of the last section's
|
|
63
|
+
* body, and a later `replace_section` on that heading removes it — correctly by
|
|
64
|
+
* the addressing rules, and invisibly, because the response otherwise just says
|
|
65
|
+
* the operation applied. Observed on a real store during the v1.3.0 beta: an
|
|
66
|
+
* appended entry vanished under a routine-looking section replace.
|
|
67
|
+
*
|
|
68
|
+
* The full-document diff is deliberately not returned (§3.8), so the size delta
|
|
69
|
+
* is the cheapest honest signal available.
|
|
70
|
+
*/
|
|
71
|
+
function shrinkNote(before: number, after: number): string {
|
|
72
|
+
const lost = before - after;
|
|
73
|
+
if (lost <= 0) return "";
|
|
74
|
+
const pct = Math.round((lost / Math.max(before, 1)) * 100);
|
|
75
|
+
if (pct < 25) return "";
|
|
76
|
+
return (
|
|
77
|
+
`⚠ This edit removed ${lost} characters (${pct}% smaller). If you did not ` +
|
|
78
|
+
`intend that, note that a section runs to the next heading of the same or ` +
|
|
79
|
+
`higher level — or to the end of the document — so replacing or deleting ` +
|
|
80
|
+
`the LAST section also removes anything appended after it. ` +
|
|
81
|
+
`cerefox_list_versions has the previous content.\n`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
56
85
|
/** Audit `operation` values, matching the CHECK constraint widened by migration 0019. */
|
|
57
86
|
const AUDIT_OP: Record<AppliedOperation["op"], string> = {
|
|
58
87
|
insert: "insert",
|
|
@@ -263,7 +292,8 @@ async function applyAndWrite(
|
|
|
263
292
|
return (
|
|
264
293
|
`Applied ${applied.length} operation(s) to "${doc.title}" (id: ${documentId}):\n${summary}\n\n` +
|
|
265
294
|
`New content_hash: ${row?.content_hash ?? newHash}\n` +
|
|
266
|
-
`Size: ${row?.total_chars ?? totalChars} chars, ${chunks.length} chunk(s).\n` +
|
|
295
|
+
`Size: ${row?.total_chars ?? totalChars} chars (was ${doc.content.length}), ${chunks.length} chunk(s).\n` +
|
|
296
|
+
shrinkNote(doc.content.length, row?.total_chars ?? totalChars) +
|
|
267
297
|
`Pass the new content_hash as expected_content_hash on your next edit.${warning}`
|
|
268
298
|
);
|
|
269
299
|
}
|
|
@@ -351,7 +381,7 @@ export const insertTool: ToolDefinition = {
|
|
|
351
381
|
type: "string",
|
|
352
382
|
enum: ["own_body", "subtree"],
|
|
353
383
|
description:
|
|
354
|
-
"Only for end_of_section when the section
|
|
384
|
+
"Only for end_of_section when the target section HAS CHILD SECTIONS: own_body = before the first child, subtree = after everything nested under it. These can be far apart, so the tool refuses rather than choosing. Omit it otherwise; you will be told (with both options) whenever it is needed.",
|
|
355
385
|
},
|
|
356
386
|
expected_content_hash: {
|
|
357
387
|
type: "string",
|
|
@@ -458,7 +488,7 @@ export const editTool: ToolDefinition = {
|
|
|
458
488
|
type: "string",
|
|
459
489
|
enum: ["own_body", "subtree"],
|
|
460
490
|
description:
|
|
461
|
-
"Only when the target section has
|
|
491
|
+
"Only when the target section has child sections. You will be told (with both options) whenever it is needed.",
|
|
462
492
|
},
|
|
463
493
|
scope: {
|
|
464
494
|
type: "string",
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partial document edits — pure string layer (iteration 34).
|
|
3
|
+
*
|
|
4
|
+
* Implements the position/anchor semantics of
|
|
5
|
+
* `docs/specs/partial-document-edits-design.md` §3 exactly. No I/O, no client,
|
|
6
|
+
* no runtime dependencies: everything here is testable against strings alone
|
|
7
|
+
* and runs identically under Deno (Edge Function) and Node/Bun (local MCP,
|
|
8
|
+
* CLI). The handlers in `_shared/mcp-tools/{insert,edit}.ts` compose this with
|
|
9
|
+
* read → chunk → embed → `cerefox_ingest_document`.
|
|
10
|
+
*
|
|
11
|
+
* The one rule that governs every function: **never guess**. An absent anchor
|
|
12
|
+
* is an error, an ambiguous anchor is an error carrying the candidates that
|
|
13
|
+
* resolve it, and an ambiguous position (a section with both its own body and
|
|
14
|
+
* child headings) is an error carrying both concrete insertion points. A
|
|
15
|
+
* silent wrong-location write is strictly worse than a refusal (spec §3.7).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** One section in document order. Offsets are into the parsed string. */
|
|
19
|
+
export interface OutlineNode {
|
|
20
|
+
/** Full heading line, trimmed — e.g. `### Notes`. */
|
|
21
|
+
heading: string;
|
|
22
|
+
/** 1–6. */
|
|
23
|
+
level: number;
|
|
24
|
+
/** ` > `-joined ancestor headings + own — the §3.7 anchor path form. */
|
|
25
|
+
path: string;
|
|
26
|
+
/** Offset of the heading line's first character. */
|
|
27
|
+
start: number;
|
|
28
|
+
/** Offset just past the heading line (start of the section's own body). */
|
|
29
|
+
bodyStart: number;
|
|
30
|
+
/** End of the section's own body: before its first child heading (== subtreeEnd for leaves). */
|
|
31
|
+
ownBodyEnd: number;
|
|
32
|
+
/** Before the next heading of equal-or-higher level (end of the whole subtree). */
|
|
33
|
+
subtreeEnd: number;
|
|
34
|
+
/** subtreeEnd - start: the per-section size reported by outline mode. */
|
|
35
|
+
chars: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type InsertPosition =
|
|
39
|
+
| "end_of_document"
|
|
40
|
+
| "end_of_section"
|
|
41
|
+
| "after_heading"
|
|
42
|
+
| "before_heading";
|
|
43
|
+
|
|
44
|
+
export type SectionPart = "own_body" | "subtree";
|
|
45
|
+
|
|
46
|
+
export type EditOperation =
|
|
47
|
+
| {
|
|
48
|
+
op: "insert";
|
|
49
|
+
text: string;
|
|
50
|
+
position: InsertPosition;
|
|
51
|
+
anchor_heading?: string;
|
|
52
|
+
section_part?: SectionPart;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
op: "replace_section";
|
|
56
|
+
text: string;
|
|
57
|
+
anchor_heading: string;
|
|
58
|
+
section_part?: SectionPart;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
op: "delete_section";
|
|
62
|
+
anchor_heading: string;
|
|
63
|
+
scope?: "body_only" | "heading_and_body";
|
|
64
|
+
section_part?: SectionPart;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Echo of one applied operation, for audit labels and response text. */
|
|
68
|
+
export interface AppliedOperation {
|
|
69
|
+
op: "insert" | "replace_section" | "delete_section";
|
|
70
|
+
/** Resolved anchor path (or "(document)" for end_of_document). */
|
|
71
|
+
path: string;
|
|
72
|
+
/** Human summary: position / scope / section_part actually used. */
|
|
73
|
+
detail: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Anchor matched nothing. The write must never fall back to appending. */
|
|
77
|
+
export class AnchorNotFoundError extends Error {
|
|
78
|
+
constructor(anchor: string, outline: OutlineNode[]) {
|
|
79
|
+
const known = outline.length
|
|
80
|
+
? ` Known headings:\n${outline.map((n) => ` ${n.path}`).join("\n")}`
|
|
81
|
+
: " The document has no headings.";
|
|
82
|
+
super(
|
|
83
|
+
`Anchor not found: "${anchor}". No write was performed.${known}\n` +
|
|
84
|
+
`Anchors match a heading line exactly ("## Title") or a parent path ` +
|
|
85
|
+
`("## Parent > ### Child").`,
|
|
86
|
+
);
|
|
87
|
+
this.name = "AnchorNotFoundError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Anchor matched more than one section. Candidates resolve the retry. */
|
|
92
|
+
export class AmbiguousAnchorError extends Error {
|
|
93
|
+
readonly candidates: string[];
|
|
94
|
+
constructor(anchor: string, candidates: string[]) {
|
|
95
|
+
super(
|
|
96
|
+
`Ambiguous anchor: "${anchor}" matches ${candidates.length} sections. ` +
|
|
97
|
+
`No write was performed. Disambiguate by passing one of these paths as anchor_heading:\n` +
|
|
98
|
+
candidates.map((c) => ` ${c}`).join("\n"),
|
|
99
|
+
);
|
|
100
|
+
this.name = "AmbiguousAnchorError";
|
|
101
|
+
this.candidates = candidates;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The anchored section has both its own body and child headings, so "the
|
|
107
|
+
* section" has two defensible readings (spec §3.3). Never guessed.
|
|
108
|
+
*/
|
|
109
|
+
export class AmbiguousPositionError extends Error {
|
|
110
|
+
readonly candidates: { section_part: SectionPart; description: string }[];
|
|
111
|
+
constructor(node: OutlineNode, firstChildHeading: string, opName: string) {
|
|
112
|
+
const candidates = [
|
|
113
|
+
{
|
|
114
|
+
section_part: "own_body" as const,
|
|
115
|
+
description:
|
|
116
|
+
`just this section's own content, stopping before its first child ` +
|
|
117
|
+
`(${firstChildHeading}) — often only a line or two below the heading`,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
section_part: "subtree" as const,
|
|
121
|
+
description:
|
|
122
|
+
`the whole subtree, past everything nested under ${node.heading} — ` +
|
|
123
|
+
`which can be a long way down`,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
super(
|
|
127
|
+
`Ambiguous position: "${node.path}" has child sections, so "the end of ` +
|
|
128
|
+
`the section" could mean two different places and ${opName} will not guess. ` +
|
|
129
|
+
`No write was performed. Pass section_part to choose:\n` +
|
|
130
|
+
candidates.map((c) => ` section_part: "${c.section_part}" — ${c.description}`).join("\n"),
|
|
131
|
+
);
|
|
132
|
+
this.name = "AmbiguousPositionError";
|
|
133
|
+
this.candidates = candidates;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Structural validation failure of an operations array (before any parsing). */
|
|
138
|
+
export class InvalidOperationError extends Error {
|
|
139
|
+
constructor(index: number, message: string) {
|
|
140
|
+
super(`Invalid operation at index ${index}: ${message}. No write was performed.`);
|
|
141
|
+
this.name = "InvalidOperationError";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const ATX_HEADING = /^(#{1,6})[ \t]+(.*[^ \t#]|)[ \t]*#*[ \t]*$/;
|
|
146
|
+
const FENCE_OPEN = /^([ \t]{0,3})(`{3,}|~{3,})(.*)$/;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parse the outline of a markdown document in one pass.
|
|
150
|
+
*
|
|
151
|
+
* ATX headings only (the chunker's convention). Headings inside fenced code
|
|
152
|
+
* blocks are content, not structure: the scanner tracks the open fence's
|
|
153
|
+
* marker character and length, and only a closing fence at least as long, of
|
|
154
|
+
* the same character, closes it (CommonMark). A decision log quoting markdown
|
|
155
|
+
* WILL contain `#` lines inside fences; treating those as headings would
|
|
156
|
+
* corrupt every anchor computed after them.
|
|
157
|
+
*/
|
|
158
|
+
export function parseOutline(content: string): OutlineNode[] {
|
|
159
|
+
const nodes: OutlineNode[] = [];
|
|
160
|
+
const stack: OutlineNode[] = []; // open ancestors, strictly increasing level
|
|
161
|
+
let fence: { char: string; len: number } | null = null;
|
|
162
|
+
|
|
163
|
+
let offset = 0;
|
|
164
|
+
const lines = content.split("\n");
|
|
165
|
+
for (let i = 0; i < lines.length; i++) {
|
|
166
|
+
const line = lines[i];
|
|
167
|
+
const lineStart = offset;
|
|
168
|
+
offset += line.length + (i < lines.length - 1 ? 1 : 0);
|
|
169
|
+
|
|
170
|
+
const fenceMatch = line.match(FENCE_OPEN);
|
|
171
|
+
if (fence) {
|
|
172
|
+
if (
|
|
173
|
+
fenceMatch &&
|
|
174
|
+
fenceMatch[2][0] === fence.char &&
|
|
175
|
+
fenceMatch[2].length >= fence.len &&
|
|
176
|
+
fenceMatch[3].trim() === ""
|
|
177
|
+
) {
|
|
178
|
+
fence = null; // closing fence
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (fenceMatch) {
|
|
183
|
+
fence = { char: fenceMatch[2][0], len: fenceMatch[2].length };
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const m = line.match(ATX_HEADING);
|
|
188
|
+
if (!m) continue;
|
|
189
|
+
|
|
190
|
+
const level = m[1].length;
|
|
191
|
+
const bodyStart = lineStart + line.length + (i < lines.length - 1 ? 1 : 0);
|
|
192
|
+
|
|
193
|
+
// Close every open section at >= this level.
|
|
194
|
+
while (stack.length && stack[stack.length - 1].level >= level) {
|
|
195
|
+
const closed = stack.pop()!;
|
|
196
|
+
closed.subtreeEnd = lineStart;
|
|
197
|
+
if (closed.ownBodyEnd === -1) closed.ownBodyEnd = lineStart;
|
|
198
|
+
}
|
|
199
|
+
// This heading is the first child of the innermost still-open ancestor.
|
|
200
|
+
if (stack.length && stack[stack.length - 1].ownBodyEnd === -1) {
|
|
201
|
+
stack[stack.length - 1].ownBodyEnd = lineStart;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// CommonMark treats a trailing run of #s as decoration, so `## Title ##`
|
|
205
|
+
// and `## Title` name the same section. Store the canonical form: an agent
|
|
206
|
+
// that read the rendered document addresses it as `## Title`, and one that
|
|
207
|
+
// pasted an outline path gets the same string back (resolveAnchor
|
|
208
|
+
// canonicalises the incoming anchor too).
|
|
209
|
+
const heading = `${m[1]} ${m[2].trim()}`.trim();
|
|
210
|
+
const node: OutlineNode = {
|
|
211
|
+
heading,
|
|
212
|
+
level,
|
|
213
|
+
path: [...stack.map((a) => a.heading), heading].join(" > "),
|
|
214
|
+
start: lineStart,
|
|
215
|
+
bodyStart,
|
|
216
|
+
ownBodyEnd: -1, // resolved when the first child or the subtree end is seen
|
|
217
|
+
subtreeEnd: -1,
|
|
218
|
+
chars: 0,
|
|
219
|
+
};
|
|
220
|
+
nodes.push(node);
|
|
221
|
+
stack.push(node);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const end = content.length;
|
|
225
|
+
for (const open of stack) {
|
|
226
|
+
open.subtreeEnd = end;
|
|
227
|
+
if (open.ownBodyEnd === -1) open.ownBodyEnd = end;
|
|
228
|
+
}
|
|
229
|
+
for (const n of nodes) n.chars = n.subtreeEnd - n.start;
|
|
230
|
+
return nodes;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Resolve an anchor per spec §3.7: exact heading text, or a ` > ` parent path.
|
|
235
|
+
* 0 matches → AnchorNotFoundError; 2+ → AmbiguousAnchorError with the paths.
|
|
236
|
+
*/
|
|
237
|
+
/** `## Title ##` and `## Title` name the same section (CommonMark decoration). */
|
|
238
|
+
function canonicalHeading(text: string): string {
|
|
239
|
+
const m = text.trim().match(/^(#{1,6})[ \t]+(.*?)[ \t]*#*[ \t]*$/);
|
|
240
|
+
return m ? `${m[1]} ${m[2].trim()}`.trim() : text.trim();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function resolveAnchor(outline: OutlineNode[], anchorHeading: string): OutlineNode {
|
|
244
|
+
const anchor = canonicalHeading(anchorHeading);
|
|
245
|
+
|
|
246
|
+
// Try the LITERAL heading first, always — including when the anchor contains
|
|
247
|
+
// the path separator. Headings really do contain " > " (`## Draft > Review`,
|
|
248
|
+
// `## A > B`), and treating any such anchor as a path made those sections
|
|
249
|
+
// unaddressable by their own text: the agent would read `## A > B` from the
|
|
250
|
+
// outline, pass it back verbatim, and be told the anchor does not exist while
|
|
251
|
+
// the error listed it. Literal-first also keeps the outline's promise that
|
|
252
|
+
// what it prints can be pasted straight back.
|
|
253
|
+
const byHeading = outline.filter((n) => n.heading === anchor);
|
|
254
|
+
if (byHeading.length === 1) return byHeading[0];
|
|
255
|
+
if (byHeading.length > 1) {
|
|
256
|
+
throw new AmbiguousAnchorError(anchor, byHeading.map((n) => n.path));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// No heading matched literally: interpret it as a parent path.
|
|
260
|
+
if (anchor.includes(" > ")) {
|
|
261
|
+
const normalizedPath = anchor.split(" > ").map((seg) => canonicalHeading(seg)).join(" > ");
|
|
262
|
+
const byPath = outline.filter((n) => n.path === normalizedPath);
|
|
263
|
+
if (byPath.length === 1) return byPath[0];
|
|
264
|
+
if (byPath.length > 1) {
|
|
265
|
+
throw new AmbiguousAnchorError(anchor, byPath.map((n) => n.path));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
throw new AnchorNotFoundError(anchor, outline);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function firstChild(outline: OutlineNode[], node: OutlineNode): OutlineNode | null {
|
|
273
|
+
for (const n of outline) {
|
|
274
|
+
if (n.start >= node.bodyStart && n.start < node.subtreeEnd) return n;
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Resolve the [from, to) range a replace/delete targets, and the end offset an
|
|
281
|
+
* end_of_section insert lands at. Spec §3.3 + the freeze-pass rule: leaf →
|
|
282
|
+
* unambiguous; body+children → require section_part; children-without-body →
|
|
283
|
+
* subtree (the two readings coincide in intent: "everything under it").
|
|
284
|
+
*/
|
|
285
|
+
function resolveSectionEnd(
|
|
286
|
+
content: string,
|
|
287
|
+
outline: OutlineNode[],
|
|
288
|
+
node: OutlineNode,
|
|
289
|
+
sectionPart: SectionPart | undefined,
|
|
290
|
+
opName: string,
|
|
291
|
+
): number {
|
|
292
|
+
const child = firstChild(outline, node);
|
|
293
|
+
if (!child) return node.subtreeEnd; // leaf: unambiguous
|
|
294
|
+
if (sectionPart === "own_body") return node.ownBodyEnd;
|
|
295
|
+
if (sectionPart === "subtree") return node.subtreeEnd;
|
|
296
|
+
|
|
297
|
+
// A section with children is ambiguous, full stop — whether or not it has
|
|
298
|
+
// any body of its own, and whichever operation is asking.
|
|
299
|
+
//
|
|
300
|
+
// Two earlier versions of this function were wrong here, in opposite ways.
|
|
301
|
+
// The first returned subtreeEnd for every children-only section, so
|
|
302
|
+
// `delete_section` on a grouping heading silently removed every sub-section
|
|
303
|
+
// under it — with `scope: "body_only"`, whose whole promise is to keep the
|
|
304
|
+
// structure. A review caught that and the destructive path started refusing.
|
|
305
|
+
//
|
|
306
|
+
// The insert path kept the exemption, on the reasoning that "for an insert
|
|
307
|
+
// the two readings coincide, both landing at the section's terminus". That
|
|
308
|
+
// reasoning was simply false, and an agent editing a real document found it:
|
|
309
|
+
// for `## Parent` with children and no body of its own, `own_body` lands
|
|
310
|
+
// BEFORE the first child and `subtree` lands AFTER the last one. Those are
|
|
311
|
+
// different places — potentially pages apart — and the code was choosing
|
|
312
|
+
// silently. The agent's text went to the end of a long section, and it only
|
|
313
|
+
// discovered that by re-reading the document, which is the cost this feature
|
|
314
|
+
// exists to remove.
|
|
315
|
+
//
|
|
316
|
+
// So: no exemption. `hasOwnBody` no longer gates anything, because the
|
|
317
|
+
// presence of children is what makes "the end of this section" ambiguous.
|
|
318
|
+
throw new AmbiguousPositionError(node, child.heading, opName);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Splice `text` into `content` as a markdown block: exactly one blank line
|
|
323
|
+
* separates it from any non-empty neighbour, existing surrounding blank runs
|
|
324
|
+
* collapse rather than stack. Agents send content; the join owns whitespace.
|
|
325
|
+
*/
|
|
326
|
+
function spliceBlock(content: string, from: number, to: number, text: string): string {
|
|
327
|
+
const before = content.slice(0, from).replace(/\n+$/, "");
|
|
328
|
+
const after = content.slice(to).replace(/^\n+/, "").replace(/\n+$/, "");
|
|
329
|
+
const block = text.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
330
|
+
|
|
331
|
+
const parts: string[] = [];
|
|
332
|
+
if (before.length) parts.push(before);
|
|
333
|
+
if (block.length) parts.push(block);
|
|
334
|
+
if (after.length) parts.push(after);
|
|
335
|
+
const joined = parts.join("\n\n");
|
|
336
|
+
// Preserve a single trailing newline if the original ended with one.
|
|
337
|
+
return content.endsWith("\n") && joined.length ? joined + "\n" : joined;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function applyOne(
|
|
341
|
+
content: string,
|
|
342
|
+
operation: EditOperation,
|
|
343
|
+
): { content: string; applied: AppliedOperation } {
|
|
344
|
+
const outline = parseOutline(content);
|
|
345
|
+
|
|
346
|
+
if (operation.op === "insert") {
|
|
347
|
+
const { position, text } = operation;
|
|
348
|
+
if (position === "end_of_document") {
|
|
349
|
+
return {
|
|
350
|
+
content: spliceBlock(content, content.length, content.length, text),
|
|
351
|
+
applied: { op: "insert", path: "(document)", detail: "insert at end_of_document" },
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (!operation.anchor_heading) {
|
|
355
|
+
// Validated earlier for real callers; guarded here for direct users.
|
|
356
|
+
throw new InvalidOperationError(0, `insert with position "${position}" requires anchor_heading`);
|
|
357
|
+
}
|
|
358
|
+
const node = resolveAnchor(outline, operation.anchor_heading);
|
|
359
|
+
let at: number;
|
|
360
|
+
let detail: string;
|
|
361
|
+
if (position === "before_heading") {
|
|
362
|
+
at = node.start;
|
|
363
|
+
detail = "insert before_heading";
|
|
364
|
+
} else if (position === "after_heading") {
|
|
365
|
+
at = node.bodyStart;
|
|
366
|
+
detail = "insert after_heading";
|
|
367
|
+
} else {
|
|
368
|
+
at = resolveSectionEnd(content, outline, node, operation.section_part, "end_of_section insert");
|
|
369
|
+
detail =
|
|
370
|
+
`insert at end_of_section` +
|
|
371
|
+
(operation.section_part ? ` (${operation.section_part})` : "");
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
content: spliceBlock(content, at, at, text),
|
|
375
|
+
applied: { op: "insert", path: node.path, detail },
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (operation.op === "replace_section") {
|
|
380
|
+
const node = resolveAnchor(outline, operation.anchor_heading);
|
|
381
|
+
const to = resolveSectionEnd(content, outline, node, operation.section_part, "replace_section");
|
|
382
|
+
return {
|
|
383
|
+
content: spliceBlock(content, node.bodyStart, to, operation.text),
|
|
384
|
+
applied: {
|
|
385
|
+
op: "replace_section",
|
|
386
|
+
path: node.path,
|
|
387
|
+
detail:
|
|
388
|
+
"replace_section body" + (operation.section_part ? ` (${operation.section_part})` : ""),
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// delete_section
|
|
394
|
+
const node = resolveAnchor(outline, operation.anchor_heading);
|
|
395
|
+
const scope = operation.scope ?? "body_only";
|
|
396
|
+
const to = resolveSectionEnd(content, outline, node, operation.section_part, "delete_section");
|
|
397
|
+
const from = scope === "heading_and_body" ? node.start : node.bodyStart;
|
|
398
|
+
return {
|
|
399
|
+
content: spliceBlock(content, from, to, ""),
|
|
400
|
+
applied: {
|
|
401
|
+
op: "delete_section",
|
|
402
|
+
path: node.path,
|
|
403
|
+
detail:
|
|
404
|
+
`delete_section (${scope})` + (operation.section_part ? ` (${operation.section_part})` : ""),
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Structural validation of an operations array — index-precise, before any work. */
|
|
410
|
+
export function validateOperations(operations: unknown): EditOperation[] {
|
|
411
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
412
|
+
throw new InvalidOperationError(0, "operations must be a non-empty array");
|
|
413
|
+
}
|
|
414
|
+
return operations.map((raw, i) => {
|
|
415
|
+
if (typeof raw !== "object" || raw === null) {
|
|
416
|
+
throw new InvalidOperationError(i, "each operation must be an object");
|
|
417
|
+
}
|
|
418
|
+
const o = raw as Record<string, unknown>;
|
|
419
|
+
const op = o.op;
|
|
420
|
+
if (op === "insert") {
|
|
421
|
+
const position = o.position;
|
|
422
|
+
if (
|
|
423
|
+
position !== "end_of_document" &&
|
|
424
|
+
position !== "end_of_section" &&
|
|
425
|
+
position !== "after_heading" &&
|
|
426
|
+
position !== "before_heading"
|
|
427
|
+
) {
|
|
428
|
+
throw new InvalidOperationError(
|
|
429
|
+
i,
|
|
430
|
+
"insert requires position: end_of_document | end_of_section | after_heading | before_heading",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
if (typeof o.text !== "string" || o.text.trim() === "") {
|
|
434
|
+
throw new InvalidOperationError(i, "insert requires non-empty text");
|
|
435
|
+
}
|
|
436
|
+
if (position !== "end_of_document" && typeof o.anchor_heading !== "string") {
|
|
437
|
+
throw new InvalidOperationError(i, `insert at ${position} requires anchor_heading`);
|
|
438
|
+
}
|
|
439
|
+
if (position === "end_of_document" && o.anchor_heading !== undefined) {
|
|
440
|
+
throw new InvalidOperationError(i, "end_of_document takes no anchor_heading");
|
|
441
|
+
}
|
|
442
|
+
} else if (op === "replace_section") {
|
|
443
|
+
if (typeof o.anchor_heading !== "string" || o.anchor_heading.trim() === "") {
|
|
444
|
+
throw new InvalidOperationError(i, "replace_section requires anchor_heading");
|
|
445
|
+
}
|
|
446
|
+
if (typeof o.text !== "string" || o.text.trim() === "") {
|
|
447
|
+
throw new InvalidOperationError(i, "replace_section requires non-empty text");
|
|
448
|
+
}
|
|
449
|
+
} else if (op === "delete_section") {
|
|
450
|
+
if (typeof o.anchor_heading !== "string" || o.anchor_heading.trim() === "") {
|
|
451
|
+
throw new InvalidOperationError(i, "delete_section requires anchor_heading");
|
|
452
|
+
}
|
|
453
|
+
if (o.scope !== undefined && o.scope !== "body_only" && o.scope !== "heading_and_body") {
|
|
454
|
+
throw new InvalidOperationError(i, "scope must be body_only or heading_and_body");
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
throw new InvalidOperationError(i, "op must be insert | replace_section | delete_section");
|
|
458
|
+
}
|
|
459
|
+
if (
|
|
460
|
+
o.section_part !== undefined &&
|
|
461
|
+
o.section_part !== "own_body" &&
|
|
462
|
+
o.section_part !== "subtree"
|
|
463
|
+
) {
|
|
464
|
+
throw new InvalidOperationError(i, "section_part must be own_body or subtree");
|
|
465
|
+
}
|
|
466
|
+
return raw as EditOperation;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Apply operations in order against the evolving text (spec §3.4). All or
|
|
472
|
+
* nothing is upheld by construction: this function either returns the fully
|
|
473
|
+
* assembled result or throws before the caller writes anything — the write
|
|
474
|
+
* itself is a single ingest call downstream. Errors are re-thrown with the
|
|
475
|
+
* failing index prefixed so a batch caller can report it.
|
|
476
|
+
*/
|
|
477
|
+
export function applyOperations(
|
|
478
|
+
content: string,
|
|
479
|
+
operations: EditOperation[],
|
|
480
|
+
): { content: string; applied: AppliedOperation[] } {
|
|
481
|
+
let current = content;
|
|
482
|
+
const applied: AppliedOperation[] = [];
|
|
483
|
+
for (let i = 0; i < operations.length; i++) {
|
|
484
|
+
try {
|
|
485
|
+
const result = applyOne(current, operations[i]);
|
|
486
|
+
current = result.content;
|
|
487
|
+
applied.push(result.applied);
|
|
488
|
+
} catch (err) {
|
|
489
|
+
if (err instanceof InvalidOperationError) throw err;
|
|
490
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
491
|
+
const wrapped = new Error(`Operation ${i + 1} of ${operations.length} failed: ${msg}`);
|
|
492
|
+
wrapped.name = err instanceof Error ? err.name : "Error";
|
|
493
|
+
throw wrapped;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return { content: current, applied };
|
|
497
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerefox/memory",
|
|
3
|
-
"version": "1.3.0-beta.
|
|
3
|
+
"version": "1.3.0-beta.3",
|
|
4
4
|
"description": "Cerefox — user-owned shared memory for AI agents. CLI + stdio MCP server + web UI + ingestion for a knowledge base on your own Supabase project (or fully self-hosted with Cerefox Local).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/fstamatelopoulos/cerefox",
|