@cerefox/memory 1.4.0 → 1.5.0

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.
@@ -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.4.0";
7441
+ var PKG_VERSION = "1.5.0";
7442
7442
  var init_meta = () => {};
7443
7443
 
7444
7444
  // ../../_shared/config/paths.ts
@@ -25730,6 +25730,10 @@ var init_bundled_docs = __esm(() => {
25730
25730
  PACKAGE_ROOT = findPackageRoot();
25731
25731
  });
25732
25732
 
25733
+ // ../../_shared/ef-meta/index.ts
25734
+ var EF_VERSION = "1.5.0", CEREFOX_VERSION = "1.5.0", EF_LAST_CHANGED = "1.5.0";
25735
+ var init_ef_meta = () => {};
25736
+
25733
25737
  // ../../_shared/compatibility/index.ts
25734
25738
  function compareSemver(a, b2) {
25735
25739
  const split = (v) => {
@@ -26318,6 +26322,19 @@ function extractSection(content, anchorHeading, sectionPart) {
26318
26322
  section_part: sectionPart ?? null
26319
26323
  };
26320
26324
  }
26325
+ function firstMeaningfulLine(text) {
26326
+ for (const line of text.split(`
26327
+ `)) {
26328
+ if (line.trim() !== "")
26329
+ return canonicalHeading(line);
26330
+ }
26331
+ return "";
26332
+ }
26333
+ function assertNoDuplicateHeading(text, node, opName) {
26334
+ if (firstMeaningfulLine(text) === node.heading) {
26335
+ throw new DuplicateHeadingError(node.heading, opName);
26336
+ }
26337
+ }
26321
26338
  function firstChild(outline, node) {
26322
26339
  for (const n of outline) {
26323
26340
  if (n.start >= node.bodyStart && n.start < node.subtreeEnd)
@@ -26373,9 +26390,11 @@ function applyOne(content, operation) {
26373
26390
  at = node2.start;
26374
26391
  detail = "insert before_heading";
26375
26392
  } else if (position === "after_heading") {
26393
+ assertNoDuplicateHeading(text, node2, "insert");
26376
26394
  at = node2.bodyStart;
26377
26395
  detail = "insert after_heading";
26378
26396
  } else {
26397
+ assertNoDuplicateHeading(text, node2, "insert");
26379
26398
  at = resolveSectionEnd(content, outline, node2, operation.section_part, "end_of_section insert");
26380
26399
  detail = `insert at end_of_section` + (operation.section_part ? ` (${operation.section_part})` : "");
26381
26400
  }
@@ -26386,6 +26405,7 @@ function applyOne(content, operation) {
26386
26405
  }
26387
26406
  if (operation.op === "replace_section") {
26388
26407
  const node2 = resolveAnchor(outline, operation.anchor_heading);
26408
+ assertNoDuplicateHeading(operation.text, node2, "replace_section");
26389
26409
  const to2 = resolveSectionEnd(content, outline, node2, operation.section_part, "replace_section");
26390
26410
  return {
26391
26411
  content: spliceBlock(content, node2.bodyStart, to2, operation.text),
@@ -26512,7 +26532,7 @@ function applyOperations(content, operations) {
26512
26532
  }
26513
26533
  return { content: current, applied };
26514
26534
  }
26515
- var AnchorNotFoundError, AmbiguousAnchorError, AmbiguousPositionError, HeadingLevelChangeError, InvalidOperationError, ATX_HEADING, FENCE_OPEN;
26535
+ var AnchorNotFoundError, AmbiguousAnchorError, AmbiguousPositionError, HeadingLevelChangeError, DuplicateHeadingError, InvalidOperationError, ATX_HEADING, FENCE_OPEN;
26516
26536
  var init_partial_edits = __esm(() => {
26517
26537
  AnchorNotFoundError = class AnchorNotFoundError extends Error {
26518
26538
  constructor(anchor, outline, reads = false) {
@@ -26562,6 +26582,12 @@ ${outline.map((n) => ` ${n.path}`).join(`
26562
26582
  this.name = "HeadingLevelChangeError";
26563
26583
  }
26564
26584
  };
26585
+ DuplicateHeadingError = class DuplicateHeadingError extends Error {
26586
+ constructor(heading, opName) {
26587
+ super(`The text you passed to ${opName} starts with the anchor heading itself ` + `(${JSON.stringify(heading)}). No write was performed. The heading is kept ` + `automatically — ${opName === "replace_section" ? "replace_section preserves it" : "insert places your text inside the section"}, ` + `so including it would produce two. Send only the new content, without that ` + `heading line. A DEEPER sub-heading inside your text is fine.`);
26588
+ this.name = "DuplicateHeadingError";
26589
+ }
26590
+ };
26565
26591
  InvalidOperationError = class InvalidOperationError extends Error {
26566
26592
  constructor(index, message) {
26567
26593
  super(`Invalid operation at index ${index}: ${message}. No write was performed.`);
@@ -54698,6 +54724,10 @@ var require_lib7 = __commonJS((exports) => {
54698
54724
  });
54699
54725
 
54700
54726
  // ../../_shared/mcp-tools/audit-log.ts
54727
+ function utcStamp2(iso) {
54728
+ const trimmed = iso.slice(0, 19);
54729
+ return trimmed.includes("T") ? `${trimmed}Z` : `${trimmed} UTC`;
54730
+ }
54701
54731
  async function handler(supabase, args, ctx) {
54702
54732
  const params = {};
54703
54733
  if (args.document_id)
@@ -54727,7 +54757,7 @@ async function handler(supabase, args, ctx) {
54727
54757
  const lines = entries.map((e) => {
54728
54758
  const docLabel = e.doc_title ?? (e.document_id ? e.document_id.slice(0, 8) + "..." : "(deleted)");
54729
54759
  const sizeInfo = e.size_before != null && e.size_after != null ? ` | ${e.size_before} -> ${e.size_after} chars` : e.size_after != null ? ` | ${e.size_after} chars` : "";
54730
- return `${e.created_at.slice(0, 19)} | ${e.operation} | ${e.author} (${e.author_type}) | ${docLabel}${sizeInfo} | ${e.description}`;
54760
+ return `${utcStamp2(e.created_at)} | ${e.operation} | ${e.author} (${e.author_type}) | ${docLabel}${sizeInfo} | ${e.description}`;
54731
54761
  });
54732
54762
  return `Audit log (${entries.length} entries, newest first):
54733
54763
 
@@ -55156,8 +55186,343 @@ var init_get_document = __esm(() => {
55156
55186
  };
55157
55187
  });
55158
55188
 
55189
+ // ../../_shared/mcp-tools/_chunker.ts
55190
+ function normalizeContent(text) {
55191
+ return text.trim().replace(/\r\n/g, `
55192
+ `).replace(/\r/g, `
55193
+ `).replace(/\n{3,}/g, `
55194
+
55195
+ `);
55196
+ }
55197
+ async function sha256hex(text) {
55198
+ const bytes = new TextEncoder().encode(text);
55199
+ const hash = await crypto.subtle.digest("SHA-256", bytes);
55200
+ return Array.from(new Uint8Array(hash)).map((b2) => b2.toString(16).padStart(2, "0")).join("");
55201
+ }
55202
+ var init__chunker = () => {};
55203
+
55204
+ // ../../_shared/mcp-tools/partial-edits.ts
55205
+ function resolveAuthorType2(ctx, args) {
55206
+ if (ctx.accessPath !== "cli")
55207
+ return "agent";
55208
+ return args.author_type === "agent" ? "agent" : "user";
55209
+ }
55210
+ function defaultRequestor(ctx) {
55211
+ return ctx.accessPath === "cli" ? "cli-user" : "mcp-agent";
55212
+ }
55213
+ function touchedTrailingSection(applied) {
55214
+ return applied.some((a) => a.reachedEnd === true);
55215
+ }
55216
+ function shrinkNote(before, afterChars, applied) {
55217
+ const beforeChars = [...before].length;
55218
+ const lost = beforeChars - afterChars;
55219
+ if (lost <= 0)
55220
+ return "";
55221
+ const pct = Math.round(lost / Math.max(beforeChars, 1) * 100);
55222
+ const trailing = touchedTrailingSection(applied);
55223
+ if (pct < 25 && !trailing) {
55224
+ return `This edit removed ${lost} characters. cerefox_list_versions has the previous content.
55225
+ `;
55226
+ }
55227
+ const why = trailing ? `You replaced or deleted the LAST section, and a section runs to the next ` + `heading of the same or higher level — or to the end of the document — so ` + `anything appended after it was inside it. ` : `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. `;
55228
+ return `⚠ This edit removed ${lost} characters (${pct}% smaller). ${why}cerefox_list_versions has the previous content.
55229
+ `;
55230
+ }
55231
+ function conflictError(documentId, expectedHash, currentHash) {
55232
+ 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.`);
55233
+ }
55234
+ async function readDocument(supabase, documentId) {
55235
+ const { data, error: error2 } = await supabase.rpc("cerefox_get_document", {
55236
+ p_document_id: documentId,
55237
+ p_version_id: null
55238
+ });
55239
+ if (error2)
55240
+ throw new Error(`Could not read document: ${error2.message}`);
55241
+ const row = data?.[0] ?? undefined;
55242
+ if (!row || row.full_content === undefined) {
55243
+ throw new McpInvalidParams(`Document not found: ${documentId}. Partial edits apply to an existing document; ` + `use cerefox_ingest to create one.`);
55244
+ }
55245
+ return {
55246
+ title: row.doc_title ?? "Untitled",
55247
+ content: row.full_content,
55248
+ hash: row.content_hash ?? ""
55249
+ };
55250
+ }
55251
+ async function applyAndWrite(supabase, ctx, args) {
55252
+ const { documentId, operations, expectedHash, requestor, toolLabel, authorType } = args;
55253
+ if (!ctx.openaiApiKey && resolveEmbedderKind() !== "local") {
55254
+ throw new Error("OpenAI API key not configured. Set OPENAI_API_KEY (Edge Function) or CEREFOX_OPENAI_API_KEY (.env, local).");
55255
+ }
55256
+ const doc = await readDocument(supabase, documentId);
55257
+ if (doc.hash && expectedHash !== doc.hash) {
55258
+ throw conflictError(documentId, expectedHash, doc.hash);
55259
+ }
55260
+ let assembled;
55261
+ let applied;
55262
+ try {
55263
+ const result = applyOperations(doc.content, operations);
55264
+ assembled = result.content;
55265
+ applied = result.applied;
55266
+ } catch (err) {
55267
+ throw new McpInvalidParams(err instanceof Error ? err.message : String(err));
55268
+ }
55269
+ if (assembled === doc.content) {
55270
+ return `No change: the ${toolLabel} produced content identical to the current document ` + `"${doc.title}" (id: ${documentId}). content_hash: ${doc.hash} (unchanged).`;
55271
+ }
55272
+ const newHash = await sha256hex(normalizeContent(assembled));
55273
+ const chunks = chunkMarkdown(assembled);
55274
+ if (chunks.length === 0) {
55275
+ throw new McpInvalidParams("The result of this edit produced no chunks (the document would be empty). " + "To remove a document use cerefox_ingest or the delete path, not a partial edit.");
55276
+ }
55277
+ const texts = chunks.map((c2) => embeddingInputFor(doc.title, c2));
55278
+ const embeddings = await embedBatch(texts, ctx.openaiApiKey ?? "");
55279
+ const totalChars = chunks.reduce((s, c2) => s + c2.char_count, 0);
55280
+ const chunkData = chunks.map((chunk, i) => ({
55281
+ chunk_index: i,
55282
+ heading_path: chunk.heading_path,
55283
+ heading_level: chunk.heading_level,
55284
+ title: chunk.title,
55285
+ content: chunk.content,
55286
+ char_count: chunk.char_count,
55287
+ embedding: embeddings[i],
55288
+ embedder: activeEmbedderName()
55289
+ }));
55290
+ const { data, error: error2 } = await supabase.rpc("cerefox_ingest_document", {
55291
+ p_document_id: documentId,
55292
+ p_title: doc.title,
55293
+ p_source: null,
55294
+ p_content_hash: newHash,
55295
+ p_metadata: null,
55296
+ p_review_status: authorType === "agent" ? "pending_review" : "approved",
55297
+ p_chunks: chunkData,
55298
+ p_author: requestor,
55299
+ p_author_type: authorType,
55300
+ p_source_label: authorType === "user" ? "manual" : "agent",
55301
+ p_expected_content_hash: expectedHash,
55302
+ p_last_write_wins: false,
55303
+ p_content_format: CONTENT_FORMAT_BLIND_STITCH,
55304
+ p_operations: applied.map((a) => ({ op: AUDIT_OP[a.op], detail: a.detail }))
55305
+ });
55306
+ if (error2) {
55307
+ const message = error2.message ?? "";
55308
+ if (message.includes("CEREFOX_CONFLICT")) {
55309
+ const current = message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
55310
+ throw conflictError(documentId, expectedHash, current);
55311
+ }
55312
+ if (message.includes("cerefox_documents_hash_unique")) {
55313
+ throw new Error(`This edit would make "${doc.title}" (id: ${documentId}) byte-identical to another ` + `document in the store, and content must be unique. No write was performed. ` + `Usually this means the two documents have converged and one should be removed or ` + `merged, or that this edit was already applied to the other one — ` + `cerefox_search for the resulting content to find it.`);
55314
+ }
55315
+ if (message.includes("does not exist") && message.includes("cerefox_ingest_document")) {
55316
+ throw new Error(`This server is behind: partial edits need schema 0.11.0 or newer. ` + `Run \`cerefox server deploy\`, then retry. (${message})`);
55317
+ }
55318
+ throw new Error(`Edit failed: ${message}`);
55319
+ }
55320
+ const row = data?.[0] ?? undefined;
55321
+ logUsage(supabase, {
55322
+ operation: toolLabel,
55323
+ accessPath: ctx.accessPath,
55324
+ requestor,
55325
+ document_id: documentId,
55326
+ result_count: applied.length
55327
+ });
55328
+ const summary = applied.map((a, i) => ` ${i + 1}. ${a.detail} → ${a.path}`).join(`
55329
+ `);
55330
+ const warning2 = row?.size_warning ? `
55331
+
55332
+ ⚠ This document has passed the configured size threshold ` + `(document_size_warning_chars). Consider splitting it.` : "";
55333
+ return `Applied ${applied.length} operation(s) to "${doc.title}" (id: ${documentId}):
55334
+ ${summary}
55335
+
55336
+ ` + `New content_hash: ${row?.content_hash ?? newHash}
55337
+ ` + `Size: ${row?.total_chars ?? totalChars} chars (was ${doc.content.length}), ${chunks.length} chunk(s).
55338
+ ` + shrinkNote(doc.content, row?.total_chars ?? totalChars, applied) + `Pass the new content_hash as expected_content_hash on your next edit.${warning2}`;
55339
+ }
55340
+ async function insertHandler(supabase, args, ctx) {
55341
+ const documentId = args.document_id?.trim();
55342
+ const text = args.text;
55343
+ const position = args.position;
55344
+ const expectedHash = args.expected_content_hash?.trim();
55345
+ if (!documentId)
55346
+ throw new McpInvalidParams("document_id is required");
55347
+ if (!text?.trim())
55348
+ throw new McpInvalidParams("text is required and cannot be empty");
55349
+ if (!expectedHash) {
55350
+ throw new McpInvalidParams("expected_content_hash is required. It is the content_hash of the version you are " + "basing this insert on — returned by cerefox_get_document (including outline mode), " + "cerefox_search, cerefox_metadata_search, and by every write. There is no " + "last-write-wins here: knowing the document changed under you is the point.");
55351
+ }
55352
+ const operations = validateOperations([
55353
+ {
55354
+ op: "insert",
55355
+ text,
55356
+ position,
55357
+ ...args.anchor_heading !== undefined ? { anchor_heading: args.anchor_heading } : {},
55358
+ ...args.section_part !== undefined ? { section_part: args.section_part } : {}
55359
+ }
55360
+ ]);
55361
+ return applyAndWrite(supabase, ctx, {
55362
+ documentId,
55363
+ operations,
55364
+ expectedHash,
55365
+ requestor: args.requestor ?? defaultRequestor(ctx),
55366
+ toolLabel: "insert",
55367
+ authorType: resolveAuthorType2(ctx, args)
55368
+ });
55369
+ }
55370
+ async function editHandler(supabase, args, ctx) {
55371
+ const documentId = args.document_id?.trim();
55372
+ const expectedHash = args.expected_content_hash?.trim();
55373
+ if (!documentId)
55374
+ throw new McpInvalidParams("document_id is required");
55375
+ if (!expectedHash) {
55376
+ throw new McpInvalidParams("expected_content_hash is required. It is the content_hash of the version you are " + "basing these edits on — returned by cerefox_get_document (including outline mode), " + "cerefox_search, cerefox_metadata_search, and by every write.");
55377
+ }
55378
+ let operations;
55379
+ try {
55380
+ operations = validateOperations(args.operations);
55381
+ } catch (err) {
55382
+ throw new McpInvalidParams(err instanceof Error ? err.message : String(err));
55383
+ }
55384
+ return applyAndWrite(supabase, ctx, {
55385
+ documentId,
55386
+ operations,
55387
+ expectedHash,
55388
+ requestor: args.requestor ?? defaultRequestor(ctx),
55389
+ toolLabel: "edit",
55390
+ authorType: resolveAuthorType2(ctx, args)
55391
+ });
55392
+ }
55393
+ var AUDIT_OP, insertTool, editTool;
55394
+ var init_partial_edits2 = __esm(() => {
55395
+ init_partial_edits();
55396
+ init__chunker();
55397
+ init_types3();
55398
+ AUDIT_OP = {
55399
+ insert: "insert",
55400
+ replace_section: "replace-section",
55401
+ delete_section: "delete-section",
55402
+ rename_section: "rename-section"
55403
+ };
55404
+ insertTool = {
55405
+ name: "cerefox_insert",
55406
+ description: "Add text to a document without resending the whole thing. Purely additive: it cannot " + "remove or overwrite existing content, so it is the safe way to append. Positions: " + "end_of_document (a plain append), end_of_section (add to the end of a section's body — " + "the most common mid-document add), after_heading (lead-in text), before_heading (a new " + "block above a section). Anchors are the exact heading line ('## Intake') or a parent path " + "('## Intake > ### Notes') when a heading appears more than once. Requires " + "expected_content_hash; returns the new hash, not the document.",
55407
+ annotations: {
55408
+ title: "Insert into document",
55409
+ readOnlyHint: false,
55410
+ destructiveHint: false,
55411
+ idempotentHint: false,
55412
+ openWorldHint: false
55413
+ },
55414
+ inputSchema: {
55415
+ type: "object",
55416
+ required: ["document_id", "text", "position", "expected_content_hash"],
55417
+ properties: {
55418
+ document_id: { type: "string", description: "UUID of the document to add to" },
55419
+ text: { type: "string", description: "Markdown to insert. Sent as-is; blank-line separation is handled for you." },
55420
+ position: {
55421
+ type: "string",
55422
+ enum: ["end_of_document", "end_of_section", "after_heading", "before_heading"],
55423
+ description: "Where to insert. end_of_document needs no anchor; the other three require anchor_heading."
55424
+ },
55425
+ anchor_heading: {
55426
+ type: "string",
55427
+ description: "Exact heading line, or a ' > ' path for a heading that appears more than once. Required unless position is end_of_document."
55428
+ },
55429
+ section_part: {
55430
+ type: "string",
55431
+ enum: ["own_body", "subtree"],
55432
+ 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."
55433
+ },
55434
+ expected_content_hash: {
55435
+ type: "string",
55436
+ description: "content_hash of the version you are basing this on. Required — no last-write-wins."
55437
+ },
55438
+ requestor: {
55439
+ type: "string",
55440
+ description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
55441
+ },
55442
+ author_type: {
55443
+ type: "string",
55444
+ enum: ["user", "agent"],
55445
+ description: "Honoured on the CLI only, where a human ran the command; over MCP the write is always recorded as an agent write regardless of what is passed."
55446
+ }
55447
+ }
55448
+ },
55449
+ handler: insertHandler
55450
+ };
55451
+ editTool = {
55452
+ name: "cerefox_edit",
55453
+ description: "Change parts of a document without resending the whole thing: one or many operations " + "applied ATOMICALLY in one write. Operations: insert (same positions as cerefox_insert), " + "replace_section (swap a section's body, heading kept), delete_section (remove a section, " + "scope body_only or heading_and_body), rename_section (change a heading's text, leaving " + "its body and position untouched — for headings that go stale, like a dated one). " + "Use one call for changes that belong together — a " + "half-applied edit is impossible, so a row and the total it feeds cannot disagree. " + "Operations apply in order and each sees the previous one's result. To change a single " + "line, replace_section on its smallest enclosing heading. Requires expected_content_hash; " + "returns the new hash, not the document.",
55454
+ annotations: {
55455
+ title: "Edit document sections",
55456
+ readOnlyHint: false,
55457
+ destructiveHint: true,
55458
+ idempotentHint: false,
55459
+ openWorldHint: false
55460
+ },
55461
+ inputSchema: {
55462
+ type: "object",
55463
+ required: ["document_id", "operations", "expected_content_hash"],
55464
+ properties: {
55465
+ document_id: { type: "string", description: "UUID of the document to edit" },
55466
+ operations: {
55467
+ type: "array",
55468
+ minItems: 1,
55469
+ description: "Operations applied in order, all-or-nothing. If any fails (bad anchor, ambiguity), nothing is written.",
55470
+ items: {
55471
+ type: "object",
55472
+ required: ["op"],
55473
+ properties: {
55474
+ op: {
55475
+ type: "string",
55476
+ enum: ["insert", "replace_section", "delete_section", "rename_section"]
55477
+ },
55478
+ text: { type: "string", description: "Markdown. Required for insert and replace_section." },
55479
+ position: {
55480
+ type: "string",
55481
+ enum: ["end_of_document", "end_of_section", "after_heading", "before_heading"],
55482
+ description: "Required for insert."
55483
+ },
55484
+ anchor_heading: {
55485
+ type: "string",
55486
+ description: "Exact heading line, or a ' > ' path when the heading is not unique. Required for replace_section, delete_section, and any insert other than end_of_document."
55487
+ },
55488
+ section_part: {
55489
+ type: "string",
55490
+ enum: ["own_body", "subtree"],
55491
+ description: "Only when the target section has child sections. You will be told (with both options) whenever it is needed."
55492
+ },
55493
+ scope: {
55494
+ type: "string",
55495
+ enum: ["body_only", "heading_and_body"],
55496
+ description: "delete_section only. Defaults to body_only, which keeps the heading."
55497
+ },
55498
+ new_heading: {
55499
+ type: "string",
55500
+ description: "rename_section only: the replacement heading LINE, at the same level (## stays ##). Changes the heading text and nothing else — the body and the section's position are untouched, which is the point: renaming via delete + insert would risk both. Use it for headings that go stale, like '## OPEN TODOs (as of 2026-08-08)'. A rename changes the anchor, so a later operation in the same call must target the NEW heading."
55501
+ }
55502
+ }
55503
+ }
55504
+ },
55505
+ expected_content_hash: {
55506
+ type: "string",
55507
+ description: "content_hash of the version you are basing these edits on. Required — no last-write-wins."
55508
+ },
55509
+ requestor: {
55510
+ type: "string",
55511
+ description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
55512
+ },
55513
+ author_type: {
55514
+ type: "string",
55515
+ enum: ["user", "agent"],
55516
+ description: "Honoured on the CLI only, where a human ran the command; over MCP the write is always recorded as an agent write regardless of what is passed."
55517
+ }
55518
+ }
55519
+ },
55520
+ handler: editHandler
55521
+ };
55522
+ });
55523
+
55159
55524
  // ../../_shared/mcp-tools/get-help-content.ts
55160
- 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`/`rename_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, or with `section: "## Heading"` one section\'s text | `document_id` (required), `outline`, `section`, `section_part` |\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. **Look before you overwrite** — `cerefox_get_document(document_id,\n section: "## Heading")` returns exactly the text a `replace_section` on that\n anchor would destroy. The outline gives you a section\'s *size*, never its\n *text*, so on a document you did not write yourself this is the difference\n between a replace and a blind overwrite.\n4. **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. To fix a stale heading (`## OPEN TODOs (as of ...)`), use\n `rename_section`: it changes the heading text and leaves the body and\n position alone.\n5. All of them 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;
55525
+ 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`/`rename_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, or with `section: "## Heading"` one section\'s text | `document_id` (required), `outline`, `section`, `section_part` |\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. **Look before you overwrite** — `cerefox_get_document(document_id,\n section: "## Heading")` returns exactly the text a `replace_section` on that\n anchor would destroy. The outline gives you a section\'s *size*, never its\n *text*, so on a document you did not write yourself this is the difference\n between a replace and a blind overwrite.\n4. **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. To fix a stale heading (`## OPEN TODOs (as of ...)`), use\n `rename_section`: it changes the heading text and leaves the body and\n position alone.\n5. All of them 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\n## Timestamps are UTC\n\nEvery timestamp Cerefox returns — `created_at` on audit entries, version\nhistory, document metadata — is **UTC**, and now carries its `Z` marker so it\ncannot be mistaken for local time.\n\n**When you write a date into a document\'s CONTENT, use your own clock, not a\nCerefox timestamp.** These are different things: a timestamp records when the\nserver stored something; a date in a log entry or a heading is authored content\nand belongs to your timezone. An agent working a Pacific afternoon read\n`2026-08-11` from version history, wrote "8/11" into its entries, and put a\nday\'s work in the future — the timestamp was correct, and copying it into\ncontent was not.\n\nCerefox deliberately does not convert to local time on the API or MCP paths.\n"Local" has no server-side meaning: the remote MCP server runs in a cloud\nfunction whose local time *is* UTC, while a local MCP server runs in yours, so\nthe same document would report two different times depending on transport. The\nweb UI converts because a browser knows the viewer\'s timezone; nothing\nserver-side does.\n\n## Mistakes that have actually happened\n\nEach of these comes from a real agent session, and each is easy to make.\n\n- **`cerefox_ingest` always replaces the ENTIRE document.** Never a section.\n Before sending, check that the tool name matches the intent: if the intent is\n "change one section", the call is `cerefox_edit` with `replace_section`. A\n section-sized edit sent as a full ingest truncated a 13,000-character index to\n a single word. It was recovered from version history within the minute, but\n only because it was noticed immediately.\n\n- **Do not include the anchor\'s own heading in your text.** `replace_section`\n keeps the heading and `insert` places your text inside the section, so\n including it produces two. This is now refused rather than silently applied,\n but the shape is worth knowing: it happened twice in one session, the second\n time while trying to repair the first. A *deeper* sub-heading inside your text\n is fine.\n\n- **Content between sections belongs to the section ABOVE it.** A section runs\n to the next heading of the same or higher level, so a `---` rule, a note, or\n any trailing text sitting just above the next heading is part of the section\n before it — even when it visually reads as belonging below. Replacing that\n section takes it too. An agent hit exactly this: a `---` that separated two\n major sections disappeared when the section above it was replaced. The write\n was correct by the addressing rules; the surprise is that "the end of this\n section" is further down the page than it looks. Note the loss warning will\n not catch it if your replacement text is longer than what it replaced, since\n there is then no net loss to report.\n\n- **Never partial-edit to fix a partial edit.** If a write leaves unexpected\n structure, stop. Use `cerefox_list_versions`, retrieve the last good version,\n and re-ingest cleanly. Repairing edits with more edits compounds the damage.\n\n- **A rejected batch is safe.** Operations in one `cerefox_edit` are\n all-or-nothing: if any is invalid, nothing is written. A refusal costs you a\n retry, not data — so prefer one call for changes that belong together, and do\n not split a batch to "make it more likely to succeed".\n\n- **Read before replacing.** `cerefox_get_document(section: "## Heading")`\n returns exactly what a `replace_section` on that anchor would overwrite. Use it\n for any section you did not write in this session. The outline gives a\n section\'s *size*, never its *text*.\n\n- **Verify after writing** — read the result back before reporting success, and\n report what the read actually shows.\n\n- **Partial edits cannot change a document\'s stored TITLE.** `rename_section`\n changes a heading inside the content; the title is a separate field and still\n needs `cerefox_ingest`.\n\n- **If a capability seems missing from one server, suspect your client first.**\n Local and remote run the same code. Call `cerefox_get_help(topic: "server")`:\n it reports the server\'s own version and the operations it registers. If that\n disagrees with your tool list, the client is holding a list it fetched before\n an upgrade — clients cache it at connect time. Ask the user to restart the\n client. Do not record a capability difference between servers as a fact; every\n such report so far has been a stale client.\n', HELP_SECTIONS, HELP_SECTION_HEADINGS;
55161
55526
  var init_get_help_content = __esm(() => {
55162
55527
  HELP_SECTIONS = {
55163
55528
  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`/`rename_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, or with `section: \"## Heading\"` one section's text | `document_id` (required), `outline`, `section`, `section_part` |\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.",
@@ -55174,12 +55539,110 @@ ingest(title="Same Title", content="...", document_id="abc123",
55174
55539
  On a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.`,
55175
55540
  "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```',
55176
55541
  "Catch-Up Workflow": '## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```',
55177
- "CLI fallback (when MCP is unavailable)": '## 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.'
55178
- };
55179
- HELP_SECTION_HEADINGS = ["Tools", "Editing part of a document (prefer this over re-sending)", "Essential Rules", "Update Workflow (ID-based -- preferred)", "Update Workflow (title-based -- fallback)", "Catch-Up Workflow", "CLI fallback (when MCP is unavailable)"];
55542
+ "CLI fallback (when MCP is unavailable)": '## 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.',
55543
+ "Timestamps are UTC": `## Timestamps are UTC
55544
+
55545
+ Every timestamp Cerefox returns — \`created_at\` on audit entries, version
55546
+ history, document metadata — is **UTC**, and now carries its \`Z\` marker so it
55547
+ cannot be mistaken for local time.
55548
+
55549
+ **When you write a date into a document's CONTENT, use your own clock, not a
55550
+ Cerefox timestamp.** These are different things: a timestamp records when the
55551
+ server stored something; a date in a log entry or a heading is authored content
55552
+ and belongs to your timezone. An agent working a Pacific afternoon read
55553
+ \`2026-08-11\` from version history, wrote "8/11" into its entries, and put a
55554
+ day's work in the future — the timestamp was correct, and copying it into
55555
+ content was not.
55556
+
55557
+ Cerefox deliberately does not convert to local time on the API or MCP paths.
55558
+ "Local" has no server-side meaning: the remote MCP server runs in a cloud
55559
+ function whose local time *is* UTC, while a local MCP server runs in yours, so
55560
+ the same document would report two different times depending on transport. The
55561
+ web UI converts because a browser knows the viewer's timezone; nothing
55562
+ server-side does.`,
55563
+ "Mistakes that have actually happened": `## Mistakes that have actually happened
55564
+
55565
+ Each of these comes from a real agent session, and each is easy to make.
55566
+
55567
+ - **\`cerefox_ingest\` always replaces the ENTIRE document.** Never a section.
55568
+ Before sending, check that the tool name matches the intent: if the intent is
55569
+ "change one section", the call is \`cerefox_edit\` with \`replace_section\`. A
55570
+ section-sized edit sent as a full ingest truncated a 13,000-character index to
55571
+ a single word. It was recovered from version history within the minute, but
55572
+ only because it was noticed immediately.
55573
+
55574
+ - **Do not include the anchor's own heading in your text.** \`replace_section\`
55575
+ keeps the heading and \`insert\` places your text inside the section, so
55576
+ including it produces two. This is now refused rather than silently applied,
55577
+ but the shape is worth knowing: it happened twice in one session, the second
55578
+ time while trying to repair the first. A *deeper* sub-heading inside your text
55579
+ is fine.
55580
+
55581
+ - **Content between sections belongs to the section ABOVE it.** A section runs
55582
+ to the next heading of the same or higher level, so a \`---\` rule, a note, or
55583
+ any trailing text sitting just above the next heading is part of the section
55584
+ before it — even when it visually reads as belonging below. Replacing that
55585
+ section takes it too. An agent hit exactly this: a \`---\` that separated two
55586
+ major sections disappeared when the section above it was replaced. The write
55587
+ was correct by the addressing rules; the surprise is that "the end of this
55588
+ section" is further down the page than it looks. Note the loss warning will
55589
+ not catch it if your replacement text is longer than what it replaced, since
55590
+ there is then no net loss to report.
55591
+
55592
+ - **Never partial-edit to fix a partial edit.** If a write leaves unexpected
55593
+ structure, stop. Use \`cerefox_list_versions\`, retrieve the last good version,
55594
+ and re-ingest cleanly. Repairing edits with more edits compounds the damage.
55595
+
55596
+ - **A rejected batch is safe.** Operations in one \`cerefox_edit\` are
55597
+ all-or-nothing: if any is invalid, nothing is written. A refusal costs you a
55598
+ retry, not data — so prefer one call for changes that belong together, and do
55599
+ not split a batch to "make it more likely to succeed".
55600
+
55601
+ - **Read before replacing.** \`cerefox_get_document(section: "## Heading")\`
55602
+ returns exactly what a \`replace_section\` on that anchor would overwrite. Use it
55603
+ for any section you did not write in this session. The outline gives a
55604
+ section's *size*, never its *text*.
55605
+
55606
+ - **Verify after writing** — read the result back before reporting success, and
55607
+ report what the read actually shows.
55608
+
55609
+ - **Partial edits cannot change a document's stored TITLE.** \`rename_section\`
55610
+ changes a heading inside the content; the title is a separate field and still
55611
+ needs \`cerefox_ingest\`.
55612
+
55613
+ - **If a capability seems missing from one server, suspect your client first.**
55614
+ Local and remote run the same code. Call \`cerefox_get_help(topic: "server")\`:
55615
+ it reports the server's own version and the operations it registers. If that
55616
+ disagrees with your tool list, the client is holding a list it fetched before
55617
+ an upgrade — clients cache it at connect time. Ask the user to restart the
55618
+ client. Do not record a capability difference between servers as a fact; every
55619
+ such report so far has been a stale client.`
55620
+ };
55621
+ HELP_SECTION_HEADINGS = ["Tools", "Editing part of a document (prefer this over re-sending)", "Essential Rules", "Update Workflow (ID-based -- preferred)", "Update Workflow (title-based -- fallback)", "Catch-Up Workflow", "CLI fallback (when MCP is unavailable)", "Timestamps are UTC", "Mistakes that have actually happened"];
55180
55622
  });
55181
55623
 
55182
55624
  // ../../_shared/mcp-tools/get-help.ts
55625
+ function editOperations() {
55626
+ const schema = editTool.inputSchema;
55627
+ return schema.properties?.operations?.items?.properties?.op?.enum ?? [];
55628
+ }
55629
+ function serverIdentity() {
55630
+ return [
55631
+ "## This server",
55632
+ "",
55633
+ `- **Version**: ${CEREFOX_VERSION}`,
55634
+ `- **cerefox_edit operations**: ${editOperations().join(", ")}`,
55635
+ "",
55636
+ "**If your tool list disagrees with this block, your CLIENT is out of date, not the server.**",
55637
+ "MCP clients fetch the tool list once when they connect and cache it, so a server",
55638
+ "upgraded mid-session is invisible until the client reconnects. Ask the user to restart",
55639
+ "the client — and if it stays missing after a restart, the client config may pin an old",
55640
+ "version of the package. Do not record a capability difference between the local and",
55641
+ "remote servers: they run the same code, and every such report so far has been a stale",
55642
+ "client."
55643
+ ].join(`
55644
+ `);
55645
+ }
55183
55646
  async function handler3(supabase, args, ctx) {
55184
55647
  const topic = args.topic?.trim();
55185
55648
  logUsage(supabase, {
@@ -55192,7 +55655,11 @@ async function handler3(supabase, args, ctx) {
55192
55655
  if (!topic) {
55193
55656
  const idx = HELP_SECTION_HEADINGS.map((h) => ` - ${h}`).join(`
55194
55657
  `);
55195
- return HELP_FULL + `
55658
+ return serverIdentity() + `
55659
+
55660
+ ---
55661
+
55662
+ ` + HELP_FULL + `
55196
55663
 
55197
55664
  ---
55198
55665
 
@@ -55202,6 +55669,8 @@ async function handler3(supabase, args, ctx) {
55202
55669
 
55203
55670
  (Topic match is case-insensitive substring on the headings above.)`;
55204
55671
  }
55672
+ if (/^(server|version|stale|client)$/i.test(topic))
55673
+ return serverIdentity();
55205
55674
  const t = topic.toLowerCase();
55206
55675
  const matched = HELP_SECTION_HEADINGS.filter((h) => h.toLowerCase().includes(t));
55207
55676
  if (matched.length === 0) {
@@ -55219,6 +55688,8 @@ async function handler3(supabase, args, ctx) {
55219
55688
  }
55220
55689
  var getHelpTool;
55221
55690
  var init_get_help = __esm(() => {
55691
+ init_ef_meta();
55692
+ init_partial_edits2();
55222
55693
  init_get_help_content();
55223
55694
  getHelpTool = {
55224
55695
  name: "cerefox_get_help",
@@ -55246,30 +55717,15 @@ var init_get_help = __esm(() => {
55246
55717
  };
55247
55718
  });
55248
55719
 
55249
- // ../../_shared/mcp-tools/_chunker.ts
55250
- function normalizeContent(text) {
55251
- return text.trim().replace(/\r\n/g, `
55252
- `).replace(/\r/g, `
55253
- `).replace(/\n{3,}/g, `
55254
-
55255
- `);
55256
- }
55257
- async function sha256hex(text) {
55258
- const bytes = new TextEncoder().encode(text);
55259
- const hash = await crypto.subtle.digest("SHA-256", bytes);
55260
- return Array.from(new Uint8Array(hash)).map((b2) => b2.toString(16).padStart(2, "0")).join("");
55261
- }
55262
- var init__chunker = () => {};
55263
-
55264
55720
  // ../../_shared/mcp-tools/ingest.ts
55265
- function conflictError(documentId, expectedHash, currentHash) {
55721
+ function conflictError2(documentId, expectedHash, currentHash) {
55266
55722
  return new Error(`Conflict: document ${documentId} changed since you read it ` + `(your base hash: ${expectedHash}, current hash: ${currentHash}). ` + `To resolve: (1) cerefox_get_document("${documentId}") to fetch the latest content ` + `and its content_hash, (2) merge your changes into it, (3) retry cerefox_ingest ` + `with expected_content_hash set to the new hash. Do not overwrite blindly — ` + `the current content may include another writer's work.`);
55267
55723
  }
55268
55724
  function mapIngestRpcError(message, documentId) {
55269
55725
  if (message.includes("CEREFOX_CONFLICT")) {
55270
55726
  const current = message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
55271
55727
  const expected = message.match(/expected hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
55272
- return conflictError(documentId, expected, current);
55728
+ return conflictError2(documentId, expected, current);
55273
55729
  }
55274
55730
  if (message.includes("CEREFOX_TOKEN_REQUIRED")) {
55275
55731
  const current = message.match(/Current hash: ([0-9a-f]{64})/)?.[1];
@@ -55316,7 +55772,7 @@ async function handler4(supabase, args, ctx) {
55316
55772
  return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged (${contentHash2}).${note2}`;
55317
55773
  }
55318
55774
  if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
55319
- throw conflictError(existingDoc.id, expected_content_hash, existingDoc.content_hash);
55775
+ throw conflictError2(existingDoc.id, expected_content_hash, existingDoc.content_hash);
55320
55776
  }
55321
55777
  const chunks2 = chunkMarkdown(content);
55322
55778
  if (chunks2.length === 0)
@@ -55374,7 +55830,7 @@ async function handler4(supabase, args, ctx) {
55374
55830
  return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged (${contentHash2}).`;
55375
55831
  }
55376
55832
  if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
55377
- throw conflictError(existingDoc.id, expected_content_hash, existingDoc.content_hash);
55833
+ throw conflictError2(existingDoc.id, expected_content_hash, existingDoc.content_hash);
55378
55834
  }
55379
55835
  const chunks2 = chunkMarkdown(content);
55380
55836
  if (chunks2.length === 0)
@@ -55533,326 +55989,6 @@ var init_ingest = __esm(() => {
55533
55989
  };
55534
55990
  });
55535
55991
 
55536
- // ../../_shared/mcp-tools/partial-edits.ts
55537
- function resolveAuthorType2(ctx, args) {
55538
- if (ctx.accessPath !== "cli")
55539
- return "agent";
55540
- return args.author_type === "agent" ? "agent" : "user";
55541
- }
55542
- function defaultRequestor(ctx) {
55543
- return ctx.accessPath === "cli" ? "cli-user" : "mcp-agent";
55544
- }
55545
- function touchedTrailingSection(applied) {
55546
- return applied.some((a) => a.reachedEnd === true);
55547
- }
55548
- function shrinkNote(before, afterChars, applied) {
55549
- const beforeChars = [...before].length;
55550
- const lost = beforeChars - afterChars;
55551
- if (lost <= 0)
55552
- return "";
55553
- const pct = Math.round(lost / Math.max(beforeChars, 1) * 100);
55554
- const trailing = touchedTrailingSection(applied);
55555
- if (pct < 25 && !trailing) {
55556
- return `This edit removed ${lost} characters. cerefox_list_versions has the previous content.
55557
- `;
55558
- }
55559
- const why = trailing ? `You replaced or deleted the LAST section, and a section runs to the next ` + `heading of the same or higher level — or to the end of the document — so ` + `anything appended after it was inside it. ` : `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. `;
55560
- return `⚠ This edit removed ${lost} characters (${pct}% smaller). ${why}cerefox_list_versions has the previous content.
55561
- `;
55562
- }
55563
- function conflictError2(documentId, expectedHash, currentHash) {
55564
- 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.`);
55565
- }
55566
- async function readDocument(supabase, documentId) {
55567
- const { data, error: error2 } = await supabase.rpc("cerefox_get_document", {
55568
- p_document_id: documentId,
55569
- p_version_id: null
55570
- });
55571
- if (error2)
55572
- throw new Error(`Could not read document: ${error2.message}`);
55573
- const row = data?.[0] ?? undefined;
55574
- if (!row || row.full_content === undefined) {
55575
- throw new McpInvalidParams(`Document not found: ${documentId}. Partial edits apply to an existing document; ` + `use cerefox_ingest to create one.`);
55576
- }
55577
- return {
55578
- title: row.doc_title ?? "Untitled",
55579
- content: row.full_content,
55580
- hash: row.content_hash ?? ""
55581
- };
55582
- }
55583
- async function applyAndWrite(supabase, ctx, args) {
55584
- const { documentId, operations, expectedHash, requestor, toolLabel, authorType } = args;
55585
- if (!ctx.openaiApiKey && resolveEmbedderKind() !== "local") {
55586
- throw new Error("OpenAI API key not configured. Set OPENAI_API_KEY (Edge Function) or CEREFOX_OPENAI_API_KEY (.env, local).");
55587
- }
55588
- const doc = await readDocument(supabase, documentId);
55589
- if (doc.hash && expectedHash !== doc.hash) {
55590
- throw conflictError2(documentId, expectedHash, doc.hash);
55591
- }
55592
- let assembled;
55593
- let applied;
55594
- try {
55595
- const result = applyOperations(doc.content, operations);
55596
- assembled = result.content;
55597
- applied = result.applied;
55598
- } catch (err) {
55599
- throw new McpInvalidParams(err instanceof Error ? err.message : String(err));
55600
- }
55601
- if (assembled === doc.content) {
55602
- return `No change: the ${toolLabel} produced content identical to the current document ` + `"${doc.title}" (id: ${documentId}). content_hash: ${doc.hash} (unchanged).`;
55603
- }
55604
- const newHash = await sha256hex(normalizeContent(assembled));
55605
- const chunks = chunkMarkdown(assembled);
55606
- if (chunks.length === 0) {
55607
- throw new McpInvalidParams("The result of this edit produced no chunks (the document would be empty). " + "To remove a document use cerefox_ingest or the delete path, not a partial edit.");
55608
- }
55609
- const texts = chunks.map((c2) => embeddingInputFor(doc.title, c2));
55610
- const embeddings = await embedBatch(texts, ctx.openaiApiKey ?? "");
55611
- const totalChars = chunks.reduce((s, c2) => s + c2.char_count, 0);
55612
- const chunkData = chunks.map((chunk, i) => ({
55613
- chunk_index: i,
55614
- heading_path: chunk.heading_path,
55615
- heading_level: chunk.heading_level,
55616
- title: chunk.title,
55617
- content: chunk.content,
55618
- char_count: chunk.char_count,
55619
- embedding: embeddings[i],
55620
- embedder: activeEmbedderName()
55621
- }));
55622
- const { data, error: error2 } = await supabase.rpc("cerefox_ingest_document", {
55623
- p_document_id: documentId,
55624
- p_title: doc.title,
55625
- p_source: null,
55626
- p_content_hash: newHash,
55627
- p_metadata: null,
55628
- p_review_status: authorType === "agent" ? "pending_review" : "approved",
55629
- p_chunks: chunkData,
55630
- p_author: requestor,
55631
- p_author_type: authorType,
55632
- p_source_label: authorType === "user" ? "manual" : "agent",
55633
- p_expected_content_hash: expectedHash,
55634
- p_last_write_wins: false,
55635
- p_content_format: CONTENT_FORMAT_BLIND_STITCH,
55636
- p_operations: applied.map((a) => ({ op: AUDIT_OP[a.op], detail: a.detail }))
55637
- });
55638
- if (error2) {
55639
- const message = error2.message ?? "";
55640
- if (message.includes("CEREFOX_CONFLICT")) {
55641
- const current = message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
55642
- throw conflictError2(documentId, expectedHash, current);
55643
- }
55644
- if (message.includes("cerefox_documents_hash_unique")) {
55645
- throw new Error(`This edit would make "${doc.title}" (id: ${documentId}) byte-identical to another ` + `document in the store, and content must be unique. No write was performed. ` + `Usually this means the two documents have converged and one should be removed or ` + `merged, or that this edit was already applied to the other one — ` + `cerefox_search for the resulting content to find it.`);
55646
- }
55647
- if (message.includes("does not exist") && message.includes("cerefox_ingest_document")) {
55648
- throw new Error(`This server is behind: partial edits need schema 0.11.0 or newer. ` + `Run \`cerefox server deploy\`, then retry. (${message})`);
55649
- }
55650
- throw new Error(`Edit failed: ${message}`);
55651
- }
55652
- const row = data?.[0] ?? undefined;
55653
- logUsage(supabase, {
55654
- operation: toolLabel,
55655
- accessPath: ctx.accessPath,
55656
- requestor,
55657
- document_id: documentId,
55658
- result_count: applied.length
55659
- });
55660
- const summary = applied.map((a, i) => ` ${i + 1}. ${a.detail} → ${a.path}`).join(`
55661
- `);
55662
- const warning2 = row?.size_warning ? `
55663
-
55664
- ⚠ This document has passed the configured size threshold ` + `(document_size_warning_chars). Consider splitting it.` : "";
55665
- return `Applied ${applied.length} operation(s) to "${doc.title}" (id: ${documentId}):
55666
- ${summary}
55667
-
55668
- ` + `New content_hash: ${row?.content_hash ?? newHash}
55669
- ` + `Size: ${row?.total_chars ?? totalChars} chars (was ${doc.content.length}), ${chunks.length} chunk(s).
55670
- ` + shrinkNote(doc.content, row?.total_chars ?? totalChars, applied) + `Pass the new content_hash as expected_content_hash on your next edit.${warning2}`;
55671
- }
55672
- async function insertHandler(supabase, args, ctx) {
55673
- const documentId = args.document_id?.trim();
55674
- const text = args.text;
55675
- const position = args.position;
55676
- const expectedHash = args.expected_content_hash?.trim();
55677
- if (!documentId)
55678
- throw new McpInvalidParams("document_id is required");
55679
- if (!text?.trim())
55680
- throw new McpInvalidParams("text is required and cannot be empty");
55681
- if (!expectedHash) {
55682
- throw new McpInvalidParams("expected_content_hash is required. It is the content_hash of the version you are " + "basing this insert on — returned by cerefox_get_document (including outline mode), " + "cerefox_search, cerefox_metadata_search, and by every write. There is no " + "last-write-wins here: knowing the document changed under you is the point.");
55683
- }
55684
- const operations = validateOperations([
55685
- {
55686
- op: "insert",
55687
- text,
55688
- position,
55689
- ...args.anchor_heading !== undefined ? { anchor_heading: args.anchor_heading } : {},
55690
- ...args.section_part !== undefined ? { section_part: args.section_part } : {}
55691
- }
55692
- ]);
55693
- return applyAndWrite(supabase, ctx, {
55694
- documentId,
55695
- operations,
55696
- expectedHash,
55697
- requestor: args.requestor ?? defaultRequestor(ctx),
55698
- toolLabel: "insert",
55699
- authorType: resolveAuthorType2(ctx, args)
55700
- });
55701
- }
55702
- async function editHandler(supabase, args, ctx) {
55703
- const documentId = args.document_id?.trim();
55704
- const expectedHash = args.expected_content_hash?.trim();
55705
- if (!documentId)
55706
- throw new McpInvalidParams("document_id is required");
55707
- if (!expectedHash) {
55708
- throw new McpInvalidParams("expected_content_hash is required. It is the content_hash of the version you are " + "basing these edits on — returned by cerefox_get_document (including outline mode), " + "cerefox_search, cerefox_metadata_search, and by every write.");
55709
- }
55710
- let operations;
55711
- try {
55712
- operations = validateOperations(args.operations);
55713
- } catch (err) {
55714
- throw new McpInvalidParams(err instanceof Error ? err.message : String(err));
55715
- }
55716
- return applyAndWrite(supabase, ctx, {
55717
- documentId,
55718
- operations,
55719
- expectedHash,
55720
- requestor: args.requestor ?? defaultRequestor(ctx),
55721
- toolLabel: "edit",
55722
- authorType: resolveAuthorType2(ctx, args)
55723
- });
55724
- }
55725
- var AUDIT_OP, insertTool, editTool;
55726
- var init_partial_edits2 = __esm(() => {
55727
- init_partial_edits();
55728
- init__chunker();
55729
- init_types3();
55730
- AUDIT_OP = {
55731
- insert: "insert",
55732
- replace_section: "replace-section",
55733
- delete_section: "delete-section",
55734
- rename_section: "rename-section"
55735
- };
55736
- insertTool = {
55737
- name: "cerefox_insert",
55738
- description: "Add text to a document without resending the whole thing. Purely additive: it cannot " + "remove or overwrite existing content, so it is the safe way to append. Positions: " + "end_of_document (a plain append), end_of_section (add to the end of a section's body — " + "the most common mid-document add), after_heading (lead-in text), before_heading (a new " + "block above a section). Anchors are the exact heading line ('## Intake') or a parent path " + "('## Intake > ### Notes') when a heading appears more than once. Requires " + "expected_content_hash; returns the new hash, not the document.",
55739
- annotations: {
55740
- title: "Insert into document",
55741
- readOnlyHint: false,
55742
- destructiveHint: false,
55743
- idempotentHint: false,
55744
- openWorldHint: false
55745
- },
55746
- inputSchema: {
55747
- type: "object",
55748
- required: ["document_id", "text", "position", "expected_content_hash"],
55749
- properties: {
55750
- document_id: { type: "string", description: "UUID of the document to add to" },
55751
- text: { type: "string", description: "Markdown to insert. Sent as-is; blank-line separation is handled for you." },
55752
- position: {
55753
- type: "string",
55754
- enum: ["end_of_document", "end_of_section", "after_heading", "before_heading"],
55755
- description: "Where to insert. end_of_document needs no anchor; the other three require anchor_heading."
55756
- },
55757
- anchor_heading: {
55758
- type: "string",
55759
- description: "Exact heading line, or a ' > ' path for a heading that appears more than once. Required unless position is end_of_document."
55760
- },
55761
- section_part: {
55762
- type: "string",
55763
- enum: ["own_body", "subtree"],
55764
- 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."
55765
- },
55766
- expected_content_hash: {
55767
- type: "string",
55768
- description: "content_hash of the version you are basing this on. Required — no last-write-wins."
55769
- },
55770
- requestor: {
55771
- type: "string",
55772
- description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
55773
- },
55774
- author_type: {
55775
- type: "string",
55776
- enum: ["user", "agent"],
55777
- description: "Honoured on the CLI only, where a human ran the command; over MCP the write is always recorded as an agent write regardless of what is passed."
55778
- }
55779
- }
55780
- },
55781
- handler: insertHandler
55782
- };
55783
- editTool = {
55784
- name: "cerefox_edit",
55785
- description: "Change parts of a document without resending the whole thing: one or many operations " + "applied ATOMICALLY in one write. Operations: insert (same positions as cerefox_insert), " + "replace_section (swap a section's body, heading kept), delete_section (remove a section, " + "scope body_only or heading_and_body), rename_section (change a heading's text, leaving " + "its body and position untouched — for headings that go stale, like a dated one). " + "Use one call for changes that belong together — a " + "half-applied edit is impossible, so a row and the total it feeds cannot disagree. " + "Operations apply in order and each sees the previous one's result. To change a single " + "line, replace_section on its smallest enclosing heading. Requires expected_content_hash; " + "returns the new hash, not the document.",
55786
- annotations: {
55787
- title: "Edit document sections",
55788
- readOnlyHint: false,
55789
- destructiveHint: true,
55790
- idempotentHint: false,
55791
- openWorldHint: false
55792
- },
55793
- inputSchema: {
55794
- type: "object",
55795
- required: ["document_id", "operations", "expected_content_hash"],
55796
- properties: {
55797
- document_id: { type: "string", description: "UUID of the document to edit" },
55798
- operations: {
55799
- type: "array",
55800
- minItems: 1,
55801
- description: "Operations applied in order, all-or-nothing. If any fails (bad anchor, ambiguity), nothing is written.",
55802
- items: {
55803
- type: "object",
55804
- required: ["op"],
55805
- properties: {
55806
- op: {
55807
- type: "string",
55808
- enum: ["insert", "replace_section", "delete_section", "rename_section"]
55809
- },
55810
- text: { type: "string", description: "Markdown. Required for insert and replace_section." },
55811
- position: {
55812
- type: "string",
55813
- enum: ["end_of_document", "end_of_section", "after_heading", "before_heading"],
55814
- description: "Required for insert."
55815
- },
55816
- anchor_heading: {
55817
- type: "string",
55818
- description: "Exact heading line, or a ' > ' path when the heading is not unique. Required for replace_section, delete_section, and any insert other than end_of_document."
55819
- },
55820
- section_part: {
55821
- type: "string",
55822
- enum: ["own_body", "subtree"],
55823
- description: "Only when the target section has child sections. You will be told (with both options) whenever it is needed."
55824
- },
55825
- scope: {
55826
- type: "string",
55827
- enum: ["body_only", "heading_and_body"],
55828
- description: "delete_section only. Defaults to body_only, which keeps the heading."
55829
- },
55830
- new_heading: {
55831
- type: "string",
55832
- description: "rename_section only: the replacement heading LINE, at the same level (## stays ##). Changes the heading text and nothing else — the body and the section's position are untouched, which is the point: renaming via delete + insert would risk both. Use it for headings that go stale, like '## OPEN TODOs (as of 2026-08-08)'. A rename changes the anchor, so a later operation in the same call must target the NEW heading."
55833
- }
55834
- }
55835
- }
55836
- },
55837
- expected_content_hash: {
55838
- type: "string",
55839
- description: "content_hash of the version you are basing these edits on. Required — no last-write-wins."
55840
- },
55841
- requestor: {
55842
- type: "string",
55843
- description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
55844
- },
55845
- author_type: {
55846
- type: "string",
55847
- enum: ["user", "agent"],
55848
- description: "Honoured on the CLI only, where a human ran the command; over MCP the write is always recorded as an agent write regardless of what is passed."
55849
- }
55850
- }
55851
- },
55852
- handler: editHandler
55853
- };
55854
- });
55855
-
55856
55992
  // ../../_shared/mcp-tools/list-metadata-keys.ts
55857
55993
  async function handler5(supabase, args, ctx) {
55858
55994
  const { data, error: error2 } = await supabase.rpc("cerefox_list_metadata_keys");
@@ -55941,6 +56077,10 @@ var init_list_projects = __esm(() => {
55941
56077
  });
55942
56078
 
55943
56079
  // ../../_shared/mcp-tools/list-versions.ts
56080
+ function utcStamp3(iso) {
56081
+ const trimmed = iso.slice(0, 19);
56082
+ return trimmed.includes("T") ? `${trimmed}Z` : `${trimmed} UTC`;
56083
+ }
55944
56084
  async function handler7(supabase, args, ctx) {
55945
56085
  const document_id = args.document_id;
55946
56086
  if (!document_id)
@@ -55960,7 +56100,7 @@ async function handler7(supabase, args, ctx) {
55960
56100
  });
55961
56101
  if (!versions.length)
55962
56102
  return "No archived versions found for this document.";
55963
- const lines = versions.map((v) => `v${v.version_number} | ${v.created_at.slice(0, 10)} | ${v.source} | ${v.chunk_count} chunks / ${v.total_chars.toLocaleString()} chars | id: ${v.version_id}`);
56103
+ const lines = versions.map((v) => `v${v.version_number} | ${utcStamp3(v.created_at)} | ${v.source} | ${v.chunk_count} chunks / ${v.total_chars.toLocaleString()} chars | id: ${v.version_id}`);
55964
56104
  return `Archived versions (newest first):
55965
56105
 
55966
56106
  ${lines.join(`
@@ -72221,7 +72361,7 @@ function action6(options) {
72221
72361
  entry: options.local ? localCerefoxEntry() : undefined
72222
72362
  });
72223
72363
  if (options.json) {
72224
- printJson(result);
72364
+ printJson({ ...result, serverName: mcpServerName() });
72225
72365
  return;
72226
72366
  }
72227
72367
  if (options.dryRun) {
@@ -76456,19 +76596,14 @@ init_cli_core();
76456
76596
 
76457
76597
  // src/cli/util/checks.ts
76458
76598
  init_meta();
76459
- import { existsSync as existsSync10, readFileSync as readFileSync7, realpathSync, statSync as statSync2 } from "node:fs";
76460
- import { homedir as homedir6 } from "node:os";
76461
- import { join as join9 } from "node:path";
76462
-
76463
- // ../../_shared/ef-meta/index.ts
76464
- var EF_VERSION = "1.4.0";
76465
- var EF_LAST_CHANGED = "1.4.0";
76466
-
76467
- // src/cli/util/checks.ts
76599
+ init_ef_meta();
76468
76600
  init_config();
76469
76601
  init_config();
76470
76602
  init_compatibility();
76471
76603
  init_server_assets();
76604
+ import { existsSync as existsSync10, readFileSync as readFileSync7, realpathSync, statSync as statSync2 } from "node:fs";
76605
+ import { homedir as homedir6 } from "node:os";
76606
+ import { join as join9 } from "node:path";
76472
76607
  function checkBinary() {
76473
76608
  return {
76474
76609
  name: "binary",
@@ -77226,7 +77361,7 @@ async function action16(options) {
77226
77361
  return;
77227
77362
  }
77228
77363
  printTable(data.map((row) => ({
77229
- when: (row.created_at ?? "").slice(0, 19).replace("T", " "),
77364
+ when: `${(row.created_at ?? "").slice(0, 19).replace("T", " ")}Z`,
77230
77365
  operation: row.operation,
77231
77366
  doc: (row.doc_title ?? (row.document_id ?? "?").slice(0, 8) + "…").slice(0, 40),
77232
77367
  author: (row.author ?? "") + (row.author_type ? `(${row.author_type})` : ""),
@@ -77371,6 +77506,13 @@ init_cli_core();
77371
77506
  init_partial_edits();
77372
77507
  init_client();
77373
77508
  async function action17(documentId, options) {
77509
+ const section = (options.section ?? "").trim() || null;
77510
+ if (section && options.outline) {
77511
+ throw userError("Pass either --outline (the whole structure) or --section (one section's text), not both.");
77512
+ }
77513
+ if (options.sectionPart && !section) {
77514
+ throw userError("--section-part only applies together with --section.");
77515
+ }
77374
77516
  const client = getClient();
77375
77517
  const rows = await client.rpc("cerefox_get_document", {
77376
77518
  p_document_id: documentId,
@@ -77418,6 +77560,35 @@ async function action17(documentId, options) {
77418
77560
  println(c.dim(` --anchor ${JSON.stringify(nodes[nodes.length - 1].path)}`));
77419
77561
  return;
77420
77562
  }
77563
+ if (section) {
77564
+ let extracted;
77565
+ try {
77566
+ extracted = extractSection(doc.full_content ?? "", section, options.sectionPart);
77567
+ } catch (err) {
77568
+ throw userError(err instanceof Error ? err.message : String(err));
77569
+ }
77570
+ const archived = Boolean(options.versionId);
77571
+ if (options.json) {
77572
+ printJson({
77573
+ title: doc.doc_title,
77574
+ heading: extracted.heading,
77575
+ path: extracted.path,
77576
+ level: extracted.level,
77577
+ section_part: extracted.section_part,
77578
+ chars: extracted.chars,
77579
+ content_hash: archived ? null : doc.content_hash ?? null,
77580
+ text: extracted.text
77581
+ });
77582
+ return;
77583
+ }
77584
+ println(c.bold(extracted.heading));
77585
+ println(c.dim(`[${doc.document_id}] · ${extracted.path} · ${extracted.chars} chars` + (extracted.section_part ? ` · ${extracted.section_part}` : "") + (archived ? " · archived" : "")));
77586
+ if (!archived && doc.content_hash)
77587
+ println(c.dim(`content_hash: ${doc.content_hash}`));
77588
+ println("");
77589
+ println(extracted.text);
77590
+ return;
77591
+ }
77421
77592
  if (options.json) {
77422
77593
  printJson(doc);
77423
77594
  return;
@@ -77431,7 +77602,7 @@ async function action17(documentId, options) {
77431
77602
  println(doc.full_content);
77432
77603
  }
77433
77604
  function registerGetDoc(program2) {
77434
- program2.command("get-doc").description("Retrieve the full content of a document by ID.").argument("<document-id>", "UUID of the document.").option("--version-id <uuid>", "Specific archived version (default: current).").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").option("--outline", "Show the heading structure, per-section sizes and content_hash instead of the content. Cheap, and the paths are the anchors the edit commands take.").action(action17);
77605
+ program2.command("get-doc").description("Retrieve the full content of a document by ID.").argument("<document-id>", "UUID of the document.").option("--version-id <uuid>", "Specific archived version (default: current).").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").option("--outline", "Show the heading structure, per-section sizes and content_hash instead of the content. Cheap, and the paths are the anchors the edit commands take.").option("--section <anchor>", "Show ONE section's text instead of the whole document: exactly what a replace_section on this anchor would overwrite. Pass the bare heading line when it is unique, or the full ' > ' path from --outline when it repeats.").option("--section-part <part>", "own_body | subtree — only when the target section has child sections, where 'the end' means two different places. You are told (with both options) whenever it is needed.").action(action17);
77435
77606
  }
77436
77607
 
77437
77608
  // src/cli/commands/ingest.ts
@@ -85149,6 +85320,7 @@ var ROOT_REDIRECT_HTML = `<!DOCTYPE html>
85149
85320
 
85150
85321
  // src/web/server.ts
85151
85322
  init_meta();
85323
+ init_ef_meta();
85152
85324
  init_cli_core();
85153
85325
  init_config();
85154
85326
  init_compatibility();