@cerefox/memory 1.6.0 → 1.7.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.6.0";
7441
+ var PKG_VERSION = "1.7.0";
7442
7442
  var init_meta = () => {};
7443
7443
 
7444
7444
  // ../../_shared/config/paths.ts
@@ -23427,6 +23427,82 @@ var init_client = __esm(() => {
23427
23427
  init_cli_core();
23428
23428
  });
23429
23429
 
23430
+ // ../../_shared/mcp-tools/_utils.ts
23431
+ function getMaxResponseBytes() {
23432
+ const raw = globalThis.process?.env?.CEREFOX_MAX_RESPONSE_BYTES;
23433
+ if (raw === undefined || raw === "")
23434
+ return MAX_RESPONSE_BYTES;
23435
+ const n = Number.parseInt(raw, 10);
23436
+ return Number.isNaN(n) || n <= 0 ? MAX_RESPONSE_BYTES : n;
23437
+ }
23438
+ function readEnv(name) {
23439
+ const g = globalThis;
23440
+ const fromProcess = g.process?.env?.[name];
23441
+ if (fromProcess !== undefined && fromProcess !== "")
23442
+ return fromProcess;
23443
+ try {
23444
+ const fromDeno = g.Deno?.env?.get(name);
23445
+ return fromDeno === "" ? undefined : fromDeno;
23446
+ } catch {
23447
+ return;
23448
+ }
23449
+ }
23450
+ function getSearchAlpha() {
23451
+ return DEFAULT_SEARCH_ALPHA;
23452
+ }
23453
+ function getMinSearchScore() {
23454
+ return readEnv("CEREFOX_EMBEDDER") === "local" ? DEFAULT_MIN_SEARCH_SCORE_LOCAL : DEFAULT_MIN_SEARCH_SCORE;
23455
+ }
23456
+ function getConfiguredMinSearchScore() {
23457
+ return;
23458
+ }
23459
+ function getConfiguredSearchAlpha() {
23460
+ return;
23461
+ }
23462
+ function getMinTermCoverage() {
23463
+ return;
23464
+ }
23465
+ function applyByteBudget(rows, maxBytes) {
23466
+ const accepted = [];
23467
+ let usedBytes = 0;
23468
+ let truncated = false;
23469
+ for (const row of rows) {
23470
+ const rowBytes = new TextEncoder().encode(JSON.stringify(row)).length;
23471
+ if (usedBytes + rowBytes > maxBytes) {
23472
+ truncated = true;
23473
+ break;
23474
+ }
23475
+ accepted.push(row);
23476
+ usedBytes += rowBytes;
23477
+ }
23478
+ return { accepted, truncated, usedBytes };
23479
+ }
23480
+ function extractConflictHashes(message) {
23481
+ return {
23482
+ expected: message.match(/expected hash ([0-9a-f]{64})/)?.[1] ?? "unknown",
23483
+ current: message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown"
23484
+ };
23485
+ }
23486
+ function isDocumentNotFoundError(error) {
23487
+ return error.code === "22023" && /not found/i.test(error.message ?? "");
23488
+ }
23489
+ function isMissingFunctionError(message, fnName) {
23490
+ return message.includes("Could not find the function") && message.includes(fnName) || message.includes("does not exist") && message.includes(fnName);
23491
+ }
23492
+ function logUsage(supabase, params) {
23493
+ Promise.resolve(supabase.rpc("cerefox_log_usage", {
23494
+ p_operation: params.operation,
23495
+ p_access_path: params.accessPath,
23496
+ p_requestor: params.requestor ?? "mcp-agent",
23497
+ p_document_id: params.document_id ?? null,
23498
+ p_project_id: params.project_id ?? null,
23499
+ p_query_text: params.query_text ?? null,
23500
+ p_result_count: params.result_count ?? null,
23501
+ p_extra: params.extra ?? {}
23502
+ })).catch(() => {});
23503
+ }
23504
+ var MAX_RESPONSE_BYTES = 200000, DEFAULT_MIN_SEARCH_SCORE = 0.5, DEFAULT_MIN_SEARCH_SCORE_LOCAL = 0.6, DEFAULT_SEARCH_ALPHA = 0.7;
23505
+
23430
23506
  // ../../_shared/server-assets/index.ts
23431
23507
  import { existsSync as existsSync5 } from "node:fs";
23432
23508
  import { dirname as dirname2, join as join5 } from "node:path";
@@ -25466,70 +25542,6 @@ var init_src = __esm(() => {
25466
25542
  src_default = Postgres;
25467
25543
  });
25468
25544
 
25469
- // ../../_shared/mcp-tools/_utils.ts
25470
- function getMaxResponseBytes() {
25471
- const raw = globalThis.process?.env?.CEREFOX_MAX_RESPONSE_BYTES;
25472
- if (raw === undefined || raw === "")
25473
- return MAX_RESPONSE_BYTES;
25474
- const n = Number.parseInt(raw, 10);
25475
- return Number.isNaN(n) || n <= 0 ? MAX_RESPONSE_BYTES : n;
25476
- }
25477
- function readEnv(name) {
25478
- const g = globalThis;
25479
- const fromProcess = g.process?.env?.[name];
25480
- if (fromProcess !== undefined && fromProcess !== "")
25481
- return fromProcess;
25482
- try {
25483
- const fromDeno = g.Deno?.env?.get(name);
25484
- return fromDeno === "" ? undefined : fromDeno;
25485
- } catch {
25486
- return;
25487
- }
25488
- }
25489
- function getSearchAlpha() {
25490
- return DEFAULT_SEARCH_ALPHA;
25491
- }
25492
- function getMinSearchScore() {
25493
- return readEnv("CEREFOX_EMBEDDER") === "local" ? DEFAULT_MIN_SEARCH_SCORE_LOCAL : DEFAULT_MIN_SEARCH_SCORE;
25494
- }
25495
- function getConfiguredMinSearchScore() {
25496
- return;
25497
- }
25498
- function getConfiguredSearchAlpha() {
25499
- return;
25500
- }
25501
- function getMinTermCoverage() {
25502
- return;
25503
- }
25504
- function applyByteBudget(rows, maxBytes) {
25505
- const accepted = [];
25506
- let usedBytes = 0;
25507
- let truncated = false;
25508
- for (const row of rows) {
25509
- const rowBytes = new TextEncoder().encode(JSON.stringify(row)).length;
25510
- if (usedBytes + rowBytes > maxBytes) {
25511
- truncated = true;
25512
- break;
25513
- }
25514
- accepted.push(row);
25515
- usedBytes += rowBytes;
25516
- }
25517
- return { accepted, truncated, usedBytes };
25518
- }
25519
- function logUsage(supabase, params) {
25520
- Promise.resolve(supabase.rpc("cerefox_log_usage", {
25521
- p_operation: params.operation,
25522
- p_access_path: params.accessPath,
25523
- p_requestor: params.requestor ?? "mcp-agent",
25524
- p_document_id: params.document_id ?? null,
25525
- p_project_id: params.project_id ?? null,
25526
- p_query_text: params.query_text ?? null,
25527
- p_result_count: params.result_count ?? null,
25528
- p_extra: params.extra ?? {}
25529
- })).catch(() => {});
25530
- }
25531
- var MAX_RESPONSE_BYTES = 200000, DEFAULT_MIN_SEARCH_SCORE = 0.5, DEFAULT_MIN_SEARCH_SCORE_LOCAL = 0.6, DEFAULT_SEARCH_ALPHA = 0.7;
25532
-
25533
25545
  // ../../_shared/mcp-tools/_projects.ts
25534
25546
  async function ensureDocumentInProject(supabase, documentId, projectName) {
25535
25547
  let projectId = null;
@@ -25731,7 +25743,7 @@ var init_bundled_docs = __esm(() => {
25731
25743
  });
25732
25744
 
25733
25745
  // ../../_shared/ef-meta/index.ts
25734
- var EF_VERSION = "1.6.0", CEREFOX_VERSION = "1.6.0", EF_LAST_CHANGED = "1.6.0";
25746
+ var EF_VERSION = "1.7.0", CEREFOX_VERSION = "1.7.0", EF_LAST_CHANGED = "1.7.0";
25735
25747
  var init_ef_meta = () => {};
25736
25748
 
25737
25749
  // ../../_shared/compatibility/index.ts
@@ -54803,6 +54815,186 @@ var init_audit_log = __esm(() => {
54803
54815
  };
54804
54816
  });
54805
54817
 
54818
+ // ../../_shared/mcp-tools/types.ts
54819
+ var McpInvalidParams;
54820
+ var init_types3 = __esm(() => {
54821
+ McpInvalidParams = class McpInvalidParams extends Error {
54822
+ constructor(message) {
54823
+ super(message);
54824
+ this.name = "McpInvalidParams";
54825
+ }
54826
+ };
54827
+ });
54828
+
54829
+ // ../../_shared/mcp-tools/delete-document.ts
54830
+ function conflictError(documentId, expectedHash, currentHash) {
54831
+ return new Error(`Conflict: document ${documentId} changed since you read it ` + `(your base hash: ${expectedHash}, current hash: ${currentHash}). ` + `Someone wrote to this document after you decided to delete it. ` + `To resolve: (1) cerefox_get_document("${documentId}") to see the current ` + `content, (2) check the deletion is still warranted — the new content may ` + `change your mind, (3) if it is, retry with expected_content_hash set to ` + `the new hash. Do not delete blindly — the change may be another writer's work.`);
54832
+ }
54833
+ async function handler2(supabase, args, ctx) {
54834
+ const document_id = args.document_id;
54835
+ const expected_content_hash = args.expected_content_hash?.trim();
54836
+ const reason = args.reason;
54837
+ if (!document_id)
54838
+ throw new McpInvalidParams("document_id is required");
54839
+ if (!expected_content_hash) {
54840
+ throw new McpInvalidParams("expected_content_hash is required: the content_hash of the document as you read it " + "(returned by cerefox_get_document, cerefox_search, and cerefox_metadata_search). " + "If you have not read the document, read it first — deletion requires knowing what " + "you are deleting.");
54841
+ }
54842
+ const author = args.author ?? args.requestor;
54843
+ const authorType = ctx.accessPath === "cli" ? "user" : "agent";
54844
+ const { data, error: error2 } = await supabase.rpc("cerefox_delete_document", {
54845
+ p_document_id: document_id,
54846
+ p_author: author ?? "unknown",
54847
+ p_author_type: authorType,
54848
+ p_expected_content_hash: expected_content_hash,
54849
+ p_reason: reason ?? null
54850
+ });
54851
+ if (error2) {
54852
+ const message = error2.message ?? String(error2);
54853
+ if (message.includes("CEREFOX_CONFLICT")) {
54854
+ const { expected, current } = extractConflictHashes(message);
54855
+ throw conflictError(document_id, expected === "unknown" ? expected_content_hash : expected, current);
54856
+ }
54857
+ if (isMissingFunctionError(message, "cerefox_delete_document")) {
54858
+ throw new Error(`This server is behind: cerefox_delete_document needs schema 0.12.0 or newer. ` + `Run \`cerefox server deploy\`, then retry. (${message})`);
54859
+ }
54860
+ if (isDocumentNotFoundError(error2)) {
54861
+ throw new McpInvalidParams(`Document ${document_id} not found.`);
54862
+ }
54863
+ throw new Error(`RPC error: ${message}`);
54864
+ }
54865
+ const row = data;
54866
+ if (!row)
54867
+ throw new Error("cerefox_delete_document returned no data");
54868
+ if (row.already_deleted) {
54869
+ return `Document ${document_id} ("${row.title ?? "untitled"}") was ALREADY soft-deleted ` + `at ${row.deleted_at}. No change was made and no audit entry was written.`;
54870
+ }
54871
+ logUsage(supabase, {
54872
+ operation: "delete",
54873
+ accessPath: ctx.accessPath,
54874
+ requestor: args.requestor,
54875
+ document_id,
54876
+ result_count: 1
54877
+ });
54878
+ return `Soft-deleted "${row.title ?? "untitled"}" (id: ${document_id}, ` + `${row.total_chars ?? "?"} chars) at ${row.deleted_at}.
54879
+ ` + `The document is excluded from search but fully recoverable: it sits in the ` + `trash until restored or purged. cerefox_restore_document undoes this if it ` + `was a mistake; permanent purge is human-only (web UI).
54880
+ ` + `Tell your user what you deleted and why, so they can review it.`;
54881
+ }
54882
+ var deleteDocumentTool;
54883
+ var init_delete_document = __esm(() => {
54884
+ init_types3();
54885
+ deleteDocumentTool = {
54886
+ name: "cerefox_delete_document",
54887
+ description: "SOFT-delete a document: it leaves search results and lands in the trash, recoverable until a human purges it. Requires expected_content_hash — the content_hash of the document AS YOU READ IT — so a delete always follows a read; if the document changed in between, the call fails with a conflict and you should re-read before deciding again. A mistaken delete can be undone with cerefox_restore_document; permanent purge is human-only (web UI). ALWAYS tell your user what you deleted and why. Pass a short reason — it is recorded in the audit log for the human reviewing the trash. Prefer this over ingesting empty/placeholder content when a document should go away.",
54888
+ annotations: {
54889
+ title: "Delete document (soft, recoverable)",
54890
+ readOnlyHint: false,
54891
+ destructiveHint: true,
54892
+ idempotentHint: true,
54893
+ openWorldHint: false
54894
+ },
54895
+ inputSchema: {
54896
+ type: "object",
54897
+ required: ["document_id", "expected_content_hash"],
54898
+ properties: {
54899
+ document_id: { type: "string", description: "UUID of the document to soft-delete." },
54900
+ expected_content_hash: {
54901
+ type: "string",
54902
+ description: "The content_hash of the document as you read it (returned by cerefox_get_document, cerefox_search, and cerefox_metadata_search). Required: a delete must follow a read. A stale hash fails with a conflict — re-read, reconsider, retry with the new hash."
54903
+ },
54904
+ reason: {
54905
+ type: "string",
54906
+ description: "Why this document is being deleted. Recorded in the audit log entry, where it is the main thing the human reviewing the trash has to go on. Short and specific beats long."
54907
+ },
54908
+ author: {
54909
+ type: "string",
54910
+ description: "Who is making this change. Recorded in the audit log."
54911
+ },
54912
+ requestor: {
54913
+ type: "string",
54914
+ description: "Name of the agent or user making this request. Recorded in the usage log."
54915
+ }
54916
+ }
54917
+ },
54918
+ handler: handler2
54919
+ };
54920
+ });
54921
+
54922
+ // ../../_shared/mcp-tools/restore-document.ts
54923
+ async function handler3(supabase, args, ctx) {
54924
+ const document_id = args.document_id;
54925
+ const reason = args.reason;
54926
+ if (!document_id)
54927
+ throw new McpInvalidParams("document_id is required");
54928
+ const author = args.author ?? args.requestor;
54929
+ const authorType = ctx.accessPath === "cli" ? "user" : "agent";
54930
+ const { data, error: error2 } = await supabase.rpc("cerefox_restore_document", {
54931
+ p_document_id: document_id,
54932
+ p_author: author ?? "unknown",
54933
+ p_author_type: authorType,
54934
+ p_reason: reason ?? null
54935
+ });
54936
+ if (error2) {
54937
+ const message = error2.message ?? String(error2);
54938
+ if (isMissingFunctionError(message, "cerefox_restore_document")) {
54939
+ throw new Error(`This server is behind: cerefox_restore_document needs schema 0.12.0 or newer. ` + `Run \`cerefox server deploy\`, then retry. (${message})`);
54940
+ }
54941
+ if (isDocumentNotFoundError(error2)) {
54942
+ throw new McpInvalidParams(`Document ${document_id} not found.`);
54943
+ }
54944
+ throw new Error(`RPC error: ${message}`);
54945
+ }
54946
+ const row = data;
54947
+ if (!row)
54948
+ throw new Error("cerefox_restore_document returned no data");
54949
+ if (!row.restored) {
54950
+ return `Document ${document_id} ("${row.title ?? "untitled"}") is NOT deleted — ` + `there is nothing to restore. No change was made.`;
54951
+ }
54952
+ logUsage(supabase, {
54953
+ operation: "restore",
54954
+ accessPath: ctx.accessPath,
54955
+ requestor: args.requestor,
54956
+ document_id,
54957
+ result_count: 1
54958
+ });
54959
+ return `Restored "${row.title ?? "untitled"}" (id: ${document_id}, ` + `${row.total_chars ?? "?"} chars) from the trash. It is searchable again ` + `and the restore is recorded in the audit log.
54960
+ ` + `Tell your user what you restored and why.`;
54961
+ }
54962
+ var restoreDocumentTool;
54963
+ var init_restore_document = __esm(() => {
54964
+ init_types3();
54965
+ restoreDocumentTool = {
54966
+ name: "cerefox_restore_document",
54967
+ description: "Restore a soft-deleted document from the trash — the inverse of cerefox_delete_document. The document becomes searchable again; the restore is recorded in the audit log with your identity. Restoring a document that is not deleted is a reported no-op. Pass a short reason — like a delete's reason, it is what the human reviewing the audit trail goes on. Permanent purge has no agent surface: once a human purges a document from the web UI, it is gone and cannot be restored.",
54968
+ annotations: {
54969
+ title: "Restore document from trash",
54970
+ readOnlyHint: false,
54971
+ destructiveHint: false,
54972
+ idempotentHint: true,
54973
+ openWorldHint: false
54974
+ },
54975
+ inputSchema: {
54976
+ type: "object",
54977
+ required: ["document_id"],
54978
+ properties: {
54979
+ document_id: { type: "string", description: "UUID of the soft-deleted document to restore." },
54980
+ reason: {
54981
+ type: "string",
54982
+ description: "Why this document is being restored. Recorded in the audit-log entry. Short and specific beats long."
54983
+ },
54984
+ author: {
54985
+ type: "string",
54986
+ description: "Who is making this change. Recorded in the audit log."
54987
+ },
54988
+ requestor: {
54989
+ type: "string",
54990
+ description: "Name of the agent or user making this request. Recorded in the usage log."
54991
+ }
54992
+ }
54993
+ },
54994
+ handler: handler3
54995
+ };
54996
+ });
54997
+
54806
54998
  // ../../_shared/mcp-tools/feature-flags.ts
54807
54999
  async function relationsEnabled(supabase) {
54808
55000
  if (cached && Date.now() - cached.at < CACHE_TTL_MS)
@@ -54833,17 +55025,6 @@ var init_feature_flags = __esm(() => {
54833
55025
  ]);
54834
55026
  });
54835
55027
 
54836
- // ../../_shared/mcp-tools/types.ts
54837
- var McpInvalidParams;
54838
- var init_types3 = __esm(() => {
54839
- McpInvalidParams = class McpInvalidParams extends Error {
54840
- constructor(message) {
54841
- super(message);
54842
- this.name = "McpInvalidParams";
54843
- }
54844
- };
54845
- });
54846
-
54847
55028
  // ../../_shared/mcp-tools/relations.ts
54848
55029
  function requireUuid2(value, field) {
54849
55030
  const s = typeof value === "string" ? value.trim() : "";
@@ -55069,7 +55250,7 @@ var init_relations = __esm(() => {
55069
55250
  });
55070
55251
 
55071
55252
  // ../../_shared/mcp-tools/get-document.ts
55072
- async function handler2(supabase, args, ctx) {
55253
+ async function handler4(supabase, args, ctx) {
55073
55254
  const document_id = args.document_id;
55074
55255
  const version_id = args.version_id ?? null;
55075
55256
  const outline = args.outline ?? false;
@@ -55182,7 +55363,7 @@ var init_get_document = __esm(() => {
55182
55363
  }
55183
55364
  }
55184
55365
  },
55185
- handler: handler2
55366
+ handler: handler4
55186
55367
  };
55187
55368
  });
55188
55369
 
@@ -55228,10 +55409,19 @@ function shrinkNote(before, afterChars, applied) {
55228
55409
  return `⚠ This edit removed ${lost} characters (${pct}% smaller). ${why}cerefox_list_versions has the previous content.
55229
55410
  `;
55230
55411
  }
55231
- function conflictError(documentId, expectedHash, currentHash) {
55412
+ function conflictError2(documentId, expectedHash, currentHash) {
55232
55413
  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
55414
  }
55234
55415
  async function readDocument(supabase, documentId) {
55416
+ try {
55417
+ const { data: lifecycle } = await supabase.from("cerefox_documents").select("deleted_at").eq("id", documentId).maybeSingle();
55418
+ if (lifecycle?.deleted_at) {
55419
+ throw new McpInvalidParams(`Document ${documentId} is soft-deleted (in the trash). A trashed document ` + `cannot be edited — restore it first with cerefox_restore_document, then retry.`);
55420
+ }
55421
+ } catch (e) {
55422
+ if (e instanceof McpInvalidParams)
55423
+ throw e;
55424
+ }
55235
55425
  const { data, error: error2 } = await supabase.rpc("cerefox_get_document", {
55236
55426
  p_document_id: documentId,
55237
55427
  p_version_id: null
@@ -55255,7 +55445,7 @@ async function applyAndWrite(supabase, ctx, args) {
55255
55445
  }
55256
55446
  const doc = await readDocument(supabase, documentId);
55257
55447
  if (doc.hash && expectedHash !== doc.hash) {
55258
- throw conflictError(documentId, expectedHash, doc.hash);
55448
+ throw conflictError2(documentId, expectedHash, doc.hash);
55259
55449
  }
55260
55450
  let assembled;
55261
55451
  let applied;
@@ -55306,13 +55496,19 @@ async function applyAndWrite(supabase, ctx, args) {
55306
55496
  if (error2) {
55307
55497
  const message = error2.message ?? "";
55308
55498
  if (message.includes("CEREFOX_CONFLICT")) {
55309
- const current = message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
55310
- throw conflictError(documentId, expectedHash, current);
55499
+ throw conflictError2(documentId, expectedHash, extractConflictHashes(message).current);
55311
55500
  }
55312
55501
  if (message.includes("cerefox_documents_hash_unique")) {
55313
55502
  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
55503
  }
55315
- if (message.includes("does not exist") && message.includes("cerefox_ingest_document")) {
55504
+ if (message.includes("CEREFOX_UNRESOLVED_LINKS")) {
55505
+ const ids = message.match(/do not exist: ([^.]+)\./)?.[1] ?? "(unparsed)";
55506
+ throw new Error(`Edit rejected — this edit introduces link(s) to document id(s) that do not ` + `exist: ${ids}. The UUIDs are almost certainly mangled — re-read the source ` + `you copied each link from, correct the id(s), and retry. Do not retry ` + `unchanged. Deliberate examples belong in code formatting (backticks).`);
55507
+ }
55508
+ if (message.includes("CEREFOX_DELETED")) {
55509
+ throw new Error(`Document ${documentId} was soft-deleted while this edit was in flight. ` + `Restore it first with cerefox_restore_document, then retry.`);
55510
+ }
55511
+ if (isMissingFunctionError(message, "cerefox_ingest_document")) {
55316
55512
  throw new Error(`This server is behind: partial edits need schema 0.11.0 or newer. ` + `Run \`cerefox server deploy\`, then retry. (${message})`);
55317
55513
  }
55318
55514
  throw new Error(`Edit failed: ${message}`);
@@ -55522,12 +55718,12 @@ var init_partial_edits2 = __esm(() => {
55522
55718
  });
55523
55719
 
55524
55720
  // ../../_shared/mcp-tools/get-help-content.ts
55525
- var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **17 MCP tools** (16 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_metadata` | Change tags WITHOUT resending content. **Merges** by default; a `null` value removes a key | `document_id`, `metadata` (required), `replace` (rare: set exactly this object), `author` |\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_metadata` | `cerefox document set-metadata <id> --set key=value` (also `--remove key`, `--json \'{...}\'`, `--replace`) |\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- **To change only tags, use `cerefox_set_document_metadata`, never `cerefox_ingest`.**\n Ingest replaces the whole document, so re-sending it to set one tag carries the\n full transcription risk for no reason. The metadata tool merges: the keys you\n pass are set, everything else is left alone, so you do not need to read the\n document first and cannot drop a tag another agent set. Pass `null` as a value\n to remove a key.\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. **Every `cerefox_get_help()` response\n begins with the server\'s version and the operations it registers** — you do\n not need a special topic, and the *absence* of that block is itself an answer:\n a server that does not print it predates v1.5.0. 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;
55721
+ var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **19 MCP tools** (18 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_delete_document` | **Soft**-delete a document (to trash; excluded from search; permanent purge is human-only) | `document_id`, `expected_content_hash` (**required** — a delete must follow a read), `reason` (recorded in the audit log — give one), `author` |\n| `cerefox_restore_document` | Restore a soft-deleted document from the trash (audited inverse of delete; no-op if not deleted) | `document_id` (required), `reason` (recorded in the audit log), `author` |\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_metadata` | Change tags WITHOUT resending content. **Merges** by default; a `null` value removes a key | `document_id`, `metadata` (required), `replace` (rare: set exactly this object), `author` |\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); permanent purge is web-UI-only.** `cerefox_delete_document` requires the document\'s `content_hash` as you read it (read before you delete) and takes a `reason` — give one; it is what the human reviewing the trash sees. `cerefox_restore_document` undoes a mistaken delete (also audited, also takes a `reason`). Always surface deletes AND restores to the user. Once a human purges from the web UI, the document is gone for good.\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. **The server validates `](uuid)` links on every write** (v1.7.0): a link to a nonexistent id rejects the write, naming the offender — that means you mangled the UUID; re-read the source and correct it, do not retry unchanged. Example ids go in backticks (code is not validated). `[[Wikilinks]]` may dangle. 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_insert` | `cerefox document insert <id> -t "<text>" -p <position> -a "<anchor-heading>" -e "<hash>" --requestor "<your-name>" --author-type agent` |\n| `cerefox_edit` | `cerefox document edit-parts <id> --operations \'<json>\' -e "<hash>" --requestor "<your-name>" --author-type agent` |\n| `cerefox_delete_document` | `cerefox document delete <id> --reason "<why>" --author "<your-name>" --author-type agent --yes` (confirms interactively instead of requiring the hash) |\n| `cerefox_restore_document` | `cerefox document restore <id> --reason "<why>" --author "<your-name>" --author-type agent` |\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_metadata` | `cerefox document set-metadata <id> --set key=value` (also `--remove key`, `--json \'{...}\'`, `--replace`) |\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- **To change only tags, use `cerefox_set_document_metadata`, never `cerefox_ingest`.**\n Ingest replaces the whole document, so re-sending it to set one tag carries the\n full transcription risk for no reason. The metadata tool merges: the keys you\n pass are set, everything else is left alone, so you do not need to read the\n document first and cannot drop a tag another agent set. Pass `null` as a value\n to remove a key.\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. **Every `cerefox_get_help()` response\n begins with the server\'s version and the operations it registers** — you do\n not need a special topic, and the *absence* of that block is itself an answer:\n a server that does not print it predates v1.5.0. 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;
55526
55722
  var init_get_help_content = __esm(() => {
55527
55723
  HELP_SECTIONS = {
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_metadata` | Change tags WITHOUT resending content. **Merges** by default; a `null` value removes a key | `document_id`, `metadata` (required), `replace` (rare: set exactly this object), `author` |\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.",
55724
+ 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_delete_document` | **Soft**-delete a document (to trash; excluded from search; permanent purge is human-only) | `document_id`, `expected_content_hash` (**required** — a delete must follow a read), `reason` (recorded in the audit log — give one), `author` |\n| `cerefox_restore_document` | Restore a soft-deleted document from the trash (audited inverse of delete; no-op if not deleted) | `document_id` (required), `reason` (recorded in the audit log), `author` |\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_metadata` | Change tags WITHOUT resending content. **Merges** by default; a `null` value removes a key | `document_id`, `metadata` (required), `replace` (rare: set exactly this object), `author` |\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.",
55529
55725
  "Editing part of a document (prefer this over re-sending)": '## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **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.',
55530
- "Essential Rules": '## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., "Claude Code", "archiver"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it\'s stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer\'s work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.',
55726
+ "Essential Rules": '## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., "Claude Code", "archiver"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); permanent purge is web-UI-only.** `cerefox_delete_document` requires the document\'s `content_hash` as you read it (read before you delete) and takes a `reason` — give one; it is what the human reviewing the trash sees. `cerefox_restore_document` undoes a mistaken delete (also audited, also takes a `reason`). Always surface deletes AND restores to the user. Once a human purges from the web UI, the document is gone for good.\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. **The server validates `](uuid)` links on every write** (v1.7.0): a link to a nonexistent id rejects the write, naming the offender — that means you mangled the UUID; re-read the source and correct it, do not retry unchanged. Example ids go in backticks (code is not validated). `[[Wikilinks]]` may dangle. 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`.',
55531
55727
  "Update Workflow (ID-based -- preferred)": `## Update Workflow (ID-based -- preferred)
55532
55728
 
55533
55729
  \`\`\`
@@ -55539,7 +55735,7 @@ ingest(title="Same Title", content="...", document_id="abc123",
55539
55735
  On a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.`,
55540
55736
  "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```',
55541
55737
  "Catch-Up Workflow": '## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```',
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_metadata` | `cerefox document set-metadata <id> --set key=value` (also `--remove key`, `--json \'{...}\'`, `--replace`) |\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.',
55738
+ "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_insert` | `cerefox document insert <id> -t "<text>" -p <position> -a "<anchor-heading>" -e "<hash>" --requestor "<your-name>" --author-type agent` |\n| `cerefox_edit` | `cerefox document edit-parts <id> --operations \'<json>\' -e "<hash>" --requestor "<your-name>" --author-type agent` |\n| `cerefox_delete_document` | `cerefox document delete <id> --reason "<why>" --author "<your-name>" --author-type agent --yes` (confirms interactively instead of requiring the hash) |\n| `cerefox_restore_document` | `cerefox document restore <id> --reason "<why>" --author "<your-name>" --author-type agent` |\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_metadata` | `cerefox document set-metadata <id> --set key=value` (also `--remove key`, `--json \'{...}\'`, `--replace`) |\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
55739
  "Timestamps are UTC": `## Timestamps are UTC
55544
55740
 
55545
55741
  Every timestamp Cerefox returns — \`created_at\` on audit entries, version
@@ -55652,7 +55848,7 @@ function serverIdentity() {
55652
55848
  ].join(`
55653
55849
  `);
55654
55850
  }
55655
- async function handler3(supabase, args, ctx) {
55851
+ async function handler5(supabase, args, ctx) {
55656
55852
  const topic = args.topic?.trim();
55657
55853
  logUsage(supabase, {
55658
55854
  operation: "get_help",
@@ -55730,19 +55926,26 @@ var init_get_help = __esm(() => {
55730
55926
  }
55731
55927
  }
55732
55928
  },
55733
- handler: handler3
55929
+ handler: handler5
55734
55930
  };
55735
55931
  });
55736
55932
 
55737
55933
  // ../../_shared/mcp-tools/ingest.ts
55738
- function conflictError2(documentId, expectedHash, currentHash) {
55934
+ function conflictError3(documentId, expectedHash, currentHash) {
55739
55935
  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.`);
55740
55936
  }
55741
55937
  function mapIngestRpcError(message, documentId) {
55742
55938
  if (message.includes("CEREFOX_CONFLICT")) {
55743
- const current = message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
55744
- const expected = message.match(/expected hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
55745
- return conflictError2(documentId, expected, current);
55939
+ const { expected, current } = extractConflictHashes(message);
55940
+ return conflictError3(documentId, expected, current);
55941
+ }
55942
+ if (message.includes("CEREFOX_UNRESOLVED_LINKS")) {
55943
+ const ids = message.match(/do not exist: ([^.]+)\./)?.[1] ?? "(unparsed)";
55944
+ return new Error(`Write rejected — the content links document id(s) that do not exist: ${ids}. ` + `These are almost certainly mangled UUIDs (long random ids corrupt easily when ` + `regenerated). Do NOT retry unchanged: re-read the SOURCE you copied each link ` + `from, correct the id(s), and resend. If an id is a deliberate example rather ` + `than a real link, put it in code formatting (backticks or a fence).`);
55945
+ }
55946
+ if (message.includes("CEREFOX_DELETED")) {
55947
+ const id = message.match(/document ([0-9a-f-]{36})/)?.[1] ?? documentId;
55948
+ return new Error(`Document ${id} is soft-deleted (in the trash). A trashed document cannot be ` + `updated — restore it first with cerefox_restore_document, then retry, or ` + `create a new document instead.`);
55746
55949
  }
55747
55950
  if (message.includes("CEREFOX_TOKEN_REQUIRED")) {
55748
55951
  const current = message.match(/Current hash: ([0-9a-f]{64})/)?.[1];
@@ -55750,7 +55953,7 @@ function mapIngestRpcError(message, documentId) {
55750
55953
  }
55751
55954
  return new Error(`Ingest RPC failed: ${message}`);
55752
55955
  }
55753
- async function handler4(supabase, args, ctx) {
55956
+ async function handler6(supabase, args, ctx) {
55754
55957
  const title = args.title?.trim();
55755
55958
  const content = args.content;
55756
55959
  const document_id = args.document_id ?? null;
@@ -55779,17 +55982,20 @@ async function handler4(supabase, args, ctx) {
55779
55982
  const contentHash2 = await sha256hex(normalizeContent(content));
55780
55983
  const reviewStatus = author_type === "agent" ? "pending_review" : "approved";
55781
55984
  if (document_id) {
55782
- const { data: existing } = await supabase.from("cerefox_documents").select("id, title, content_hash").eq("id", document_id).is("deleted_at", null).limit(1);
55985
+ const { data: existing } = await supabase.from("cerefox_documents").select("id, title, content_hash, deleted_at").eq("id", document_id).limit(1);
55783
55986
  if (!existing?.length) {
55784
55987
  throw new Error(`Document not found: ${document_id}`);
55785
55988
  }
55786
55989
  const existingDoc = existing[0];
55990
+ if (existingDoc.deleted_at) {
55991
+ throw new McpInvalidParams(`Document ${document_id} ("${existingDoc.title}") is soft-deleted (in the trash). ` + `A trashed document cannot be updated — restore it first with ` + `cerefox_restore_document, then retry, or create a new document instead.`);
55992
+ }
55787
55993
  if (existingDoc.content_hash === contentHash2) {
55788
55994
  const note2 = update_if_exists ? "" : " Note: update_if_exists flag was overridden by document_id.";
55789
55995
  return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged (${contentHash2}).${note2}`;
55790
55996
  }
55791
55997
  if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
55792
- throw conflictError2(existingDoc.id, expected_content_hash, existingDoc.content_hash);
55998
+ throw conflictError3(existingDoc.id, expected_content_hash, existingDoc.content_hash);
55793
55999
  }
55794
56000
  const chunks2 = chunkMarkdown(content);
55795
56001
  if (chunks2.length === 0)
@@ -55840,14 +56046,20 @@ async function handler4(supabase, args, ctx) {
55840
56046
  return `Document updated: "${title}" (id: ${existingDoc.id}), ${chunks2.length} chunk(s), ${totalChars2} chars. New content_hash: ${contentHash2}.${note}`;
55841
56047
  }
55842
56048
  if (update_if_exists) {
55843
- const { data: existing } = await supabase.from("cerefox_documents").select("id, title, content_hash").eq("title", title).order("updated_at", { ascending: false }).limit(1);
56049
+ let { data: existing } = await supabase.from("cerefox_documents").select("id, title, content_hash, deleted_at").eq("title", title).is("deleted_at", null).order("updated_at", { ascending: false }).limit(1);
56050
+ if (!existing?.length) {
56051
+ ({ data: existing } = await supabase.from("cerefox_documents").select("id, title, content_hash, deleted_at").eq("title", title).order("updated_at", { ascending: false }).limit(1));
56052
+ }
55844
56053
  if (existing?.length) {
55845
56054
  const existingDoc = existing[0];
56055
+ if (existingDoc.deleted_at) {
56056
+ throw new McpInvalidParams(`A document titled "${existingDoc.title}" is in the trash (soft-deleted, ` + `id: ${existingDoc.id}). Restore it first with cerefox_restore_document ` + `and re-ingest to update it — or have your user purge it from the web UI ` + `Trash to start fresh.`);
56057
+ }
55846
56058
  if (existingDoc.content_hash === contentHash2) {
55847
56059
  return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged (${contentHash2}).`;
55848
56060
  }
55849
56061
  if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
55850
- throw conflictError2(existingDoc.id, expected_content_hash, existingDoc.content_hash);
56062
+ throw conflictError3(existingDoc.id, expected_content_hash, existingDoc.content_hash);
55851
56063
  }
55852
56064
  const chunks2 = chunkMarkdown(content);
55853
56065
  if (chunks2.length === 0)
@@ -55897,8 +56109,11 @@ async function handler4(supabase, args, ctx) {
55897
56109
  return `Document updated: "${existingDoc.title}" (id: ${existingDoc.id}), ${chunks2.length} chunk(s), ${totalChars2} chars. New content_hash: ${contentHash2}.`;
55898
56110
  }
55899
56111
  }
55900
- const { data: hashMatch } = await supabase.from("cerefox_documents").select("id, title").eq("content_hash", contentHash2).limit(1);
56112
+ const { data: hashMatch } = await supabase.from("cerefox_documents").select("id, title, deleted_at").eq("content_hash", contentHash2).limit(1);
55901
56113
  if (hashMatch?.length) {
56114
+ if (hashMatch[0].deleted_at) {
56115
+ return `This exact content is already in the TRASH as "${hashMatch[0].title}" ` + `(soft-deleted, id: ${hashMatch[0].id}). Restore it with ` + `cerefox_restore_document instead of re-ingesting — or have your user ` + `purge it from the web UI Trash to start fresh.`;
56116
+ }
55902
56117
  return `Document already up-to-date: "${hashMatch[0].title}" (id: ${hashMatch[0].id}). Content hash unchanged (${contentHash2}) — pass it as expected_content_hash to edit it.`;
55903
56118
  }
55904
56119
  const chunks = chunkMarkdown(content);
@@ -56002,12 +56217,12 @@ var init_ingest = __esm(() => {
56002
56217
  }
56003
56218
  }
56004
56219
  },
56005
- handler: handler4
56220
+ handler: handler6
56006
56221
  };
56007
56222
  });
56008
56223
 
56009
56224
  // ../../_shared/mcp-tools/list-metadata-keys.ts
56010
- async function handler5(supabase, args, ctx) {
56225
+ async function handler7(supabase, args, ctx) {
56011
56226
  const { data, error: error2 } = await supabase.rpc("cerefox_list_metadata_keys");
56012
56227
  if (error2)
56013
56228
  throw new Error(`RPC error: ${error2.message}`);
@@ -56042,12 +56257,12 @@ var init_list_metadata_keys = __esm(() => {
56042
56257
  }
56043
56258
  }
56044
56259
  },
56045
- handler: handler5
56260
+ handler: handler7
56046
56261
  };
56047
56262
  });
56048
56263
 
56049
56264
  // ../../_shared/mcp-tools/list-projects.ts
56050
- async function handler6(supabase, args, ctx) {
56265
+ async function handler8(supabase, args, ctx) {
56051
56266
  const { data, error: error2 } = await supabase.rpc("cerefox_list_projects");
56052
56267
  if (error2)
56053
56268
  throw new Error(`RPC error: ${error2.message}`);
@@ -56089,7 +56304,7 @@ var init_list_projects = __esm(() => {
56089
56304
  }
56090
56305
  }
56091
56306
  },
56092
- handler: handler6
56307
+ handler: handler8
56093
56308
  };
56094
56309
  });
56095
56310
 
@@ -56098,7 +56313,7 @@ function utcStamp3(iso) {
56098
56313
  const trimmed = iso.slice(0, 19);
56099
56314
  return trimmed.includes("T") ? `${trimmed}Z` : `${trimmed} UTC`;
56100
56315
  }
56101
- async function handler7(supabase, args, ctx) {
56316
+ async function handler9(supabase, args, ctx) {
56102
56317
  const document_id = args.document_id;
56103
56318
  if (!document_id)
56104
56319
  throw new McpInvalidParams("document_id is required");
@@ -56149,12 +56364,12 @@ var init_list_versions = __esm(() => {
56149
56364
  }
56150
56365
  }
56151
56366
  },
56152
- handler: handler7
56367
+ handler: handler9
56153
56368
  };
56154
56369
  });
56155
56370
 
56156
56371
  // ../../_shared/mcp-tools/metadata-search.ts
56157
- async function handler8(supabase, args, ctx) {
56372
+ async function handler10(supabase, args, ctx) {
56158
56373
  const metadata_filter = args.metadata_filter;
56159
56374
  const project_name = args.project_name;
56160
56375
  const updated_since = args.updated_since;
@@ -56266,12 +56481,12 @@ var init_metadata_search = __esm(() => {
56266
56481
  }
56267
56482
  }
56268
56483
  },
56269
- handler: handler8
56484
+ handler: handler10
56270
56485
  };
56271
56486
  });
56272
56487
 
56273
56488
  // ../../_shared/mcp-tools/search.ts
56274
- async function handler9(supabase, args, ctx) {
56489
+ async function handler11(supabase, args, ctx) {
56275
56490
  const query = args.query;
56276
56491
  const project_name = args.project_name;
56277
56492
  const match_count = args.match_count ?? 5;
@@ -56445,12 +56660,12 @@ var init_search = __esm(() => {
56445
56660
  }
56446
56661
  }
56447
56662
  },
56448
- handler: handler9
56663
+ handler: handler11
56449
56664
  };
56450
56665
  });
56451
56666
 
56452
56667
  // ../../_shared/mcp-tools/set-document-metadata.ts
56453
- async function handler10(supabase, args, ctx) {
56668
+ async function handler12(supabase, args, ctx) {
56454
56669
  const document_id = args.document_id;
56455
56670
  const metadata = args.metadata;
56456
56671
  const replace = args.replace ?? false;
@@ -56529,12 +56744,12 @@ var init_set_document_metadata = __esm(() => {
56529
56744
  }
56530
56745
  }
56531
56746
  },
56532
- handler: handler10
56747
+ handler: handler12
56533
56748
  };
56534
56749
  });
56535
56750
 
56536
56751
  // ../../_shared/mcp-tools/set-document-projects.ts
56537
- async function handler11(supabase, args, ctx) {
56752
+ async function handler13(supabase, args, ctx) {
56538
56753
  const document_id = args.document_id?.trim();
56539
56754
  const project_names_raw = args.project_names;
56540
56755
  const author = args.author ?? "mcp-agent";
@@ -56595,7 +56810,7 @@ var init_set_document_projects = __esm(() => {
56595
56810
  }
56596
56811
  }
56597
56812
  },
56598
- handler: handler11
56813
+ handler: handler13
56599
56814
  };
56600
56815
  });
56601
56816
 
@@ -56614,6 +56829,8 @@ async function assertToolEnabled(supabase, name) {
56614
56829
  var ALL_TOOLS, TOOLS_BY_NAME;
56615
56830
  var init_mcp_tools = __esm(() => {
56616
56831
  init_audit_log();
56832
+ init_delete_document();
56833
+ init_restore_document();
56617
56834
  init_feature_flags();
56618
56835
  init_relations();
56619
56836
  init_get_document();
@@ -56634,6 +56851,8 @@ var init_mcp_tools = __esm(() => {
56634
56851
  ingestTool,
56635
56852
  insertTool,
56636
56853
  editTool,
56854
+ deleteDocumentTool,
56855
+ restoreDocumentTool,
56637
56856
  getDocumentTool,
56638
56857
  listVersionsTool,
56639
56858
  metadataSearchTool,
@@ -62713,25 +62932,25 @@ class Protocol {
62713
62932
  const error3 = McpError.fromError(ErrorCode.ConnectionClosed, "Connection closed");
62714
62933
  this._transport = undefined;
62715
62934
  this.onclose?.();
62716
- for (const handler12 of responseHandlers.values()) {
62717
- handler12(error3);
62935
+ for (const handler14 of responseHandlers.values()) {
62936
+ handler14(error3);
62718
62937
  }
62719
62938
  }
62720
62939
  _onerror(error3) {
62721
62940
  this.onerror?.(error3);
62722
62941
  }
62723
62942
  _onnotification(notification) {
62724
- const handler12 = this._notificationHandlers.get(notification.method) ?? this.fallbackNotificationHandler;
62725
- if (handler12 === undefined) {
62943
+ const handler14 = this._notificationHandlers.get(notification.method) ?? this.fallbackNotificationHandler;
62944
+ if (handler14 === undefined) {
62726
62945
  return;
62727
62946
  }
62728
- Promise.resolve().then(() => handler12(notification)).catch((error3) => this._onerror(new Error(`Uncaught error in notification handler: ${error3}`)));
62947
+ Promise.resolve().then(() => handler14(notification)).catch((error3) => this._onerror(new Error(`Uncaught error in notification handler: ${error3}`)));
62729
62948
  }
62730
62949
  _onrequest(request, extra) {
62731
- const handler12 = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
62950
+ const handler14 = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
62732
62951
  const capturedTransport = this._transport;
62733
62952
  const relatedTaskId = request.params?._meta?.[RELATED_TASK_META_KEY]?.taskId;
62734
- if (handler12 === undefined) {
62953
+ if (handler14 === undefined) {
62735
62954
  const errorResponse = {
62736
62955
  jsonrpc: "2.0",
62737
62956
  id: request.id,
@@ -62795,7 +63014,7 @@ class Protocol {
62795
63014
  if (taskCreationParams) {
62796
63015
  this.assertTaskHandlerCapability(request.method);
62797
63016
  }
62798
- }).then(() => handler12(request, fullExtra)).then(async (result) => {
63017
+ }).then(() => handler14(request, fullExtra)).then(async (result) => {
62799
63018
  if (abortController.signal.aborted) {
62800
63019
  return;
62801
63020
  }
@@ -62844,8 +63063,8 @@ class Protocol {
62844
63063
  _onprogress(notification) {
62845
63064
  const { progressToken, ...params } = notification.params;
62846
63065
  const messageId = Number(progressToken);
62847
- const handler12 = this._progressHandlers.get(messageId);
62848
- if (!handler12) {
63066
+ const handler14 = this._progressHandlers.get(messageId);
63067
+ if (!handler14) {
62849
63068
  this._onerror(new Error(`Received a progress notification for an unknown token: ${JSON.stringify(notification)}`));
62850
63069
  return;
62851
63070
  }
@@ -62862,7 +63081,7 @@ class Protocol {
62862
63081
  return;
62863
63082
  }
62864
63083
  }
62865
- handler12(params);
63084
+ handler14(params);
62866
63085
  }
62867
63086
  _onresponse(response) {
62868
63087
  const messageId = Number(response.id);
@@ -62877,8 +63096,8 @@ class Protocol {
62877
63096
  }
62878
63097
  return;
62879
63098
  }
62880
- const handler12 = this._responseHandlers.get(messageId);
62881
- if (handler12 === undefined) {
63099
+ const handler14 = this._responseHandlers.get(messageId);
63100
+ if (handler14 === undefined) {
62882
63101
  this._onerror(new Error(`Received a response for an unknown message ID: ${JSON.stringify(response)}`));
62883
63102
  return;
62884
63103
  }
@@ -62899,10 +63118,10 @@ class Protocol {
62899
63118
  this._progressHandlers.delete(messageId);
62900
63119
  }
62901
63120
  if (isJSONRPCResultResponse(response)) {
62902
- handler12(response);
63121
+ handler14(response);
62903
63122
  } else {
62904
63123
  const error3 = McpError.fromError(response.error.code, response.error.message, response.error.data);
62905
- handler12(error3);
63124
+ handler14(error3);
62906
63125
  }
62907
63126
  }
62908
63127
  get transport() {
@@ -63065,9 +63284,9 @@ class Protocol {
63065
63284
  const relatedTaskId = relatedTask?.taskId;
63066
63285
  if (relatedTaskId) {
63067
63286
  const responseResolver = (response) => {
63068
- const handler12 = this._responseHandlers.get(messageId);
63069
- if (handler12) {
63070
- handler12(response);
63287
+ const handler14 = this._responseHandlers.get(messageId);
63288
+ if (handler14) {
63289
+ handler14(response);
63071
63290
  } else {
63072
63291
  this._onerror(new Error(`Response handler missing for side-channeled request ${messageId}`));
63073
63292
  }
@@ -63176,12 +63395,12 @@ class Protocol {
63176
63395
  }
63177
63396
  await this._transport.send(jsonrpcNotification, options);
63178
63397
  }
63179
- setRequestHandler(requestSchema, handler12) {
63398
+ setRequestHandler(requestSchema, handler14) {
63180
63399
  const method = getMethodLiteral(requestSchema);
63181
63400
  this.assertRequestHandlerCapability(method);
63182
63401
  this._requestHandlers.set(method, (request, extra) => {
63183
63402
  const parsed = parseWithCompat(requestSchema, request);
63184
- return Promise.resolve(handler12(parsed, extra));
63403
+ return Promise.resolve(handler14(parsed, extra));
63185
63404
  });
63186
63405
  }
63187
63406
  removeRequestHandler(method) {
@@ -63192,11 +63411,11 @@ class Protocol {
63192
63411
  throw new Error(`A request handler for ${method} already exists, which would be overridden`);
63193
63412
  }
63194
63413
  }
63195
- setNotificationHandler(notificationSchema, handler12) {
63414
+ setNotificationHandler(notificationSchema, handler14) {
63196
63415
  const method = getMethodLiteral(notificationSchema);
63197
63416
  this._notificationHandlers.set(method, (notification) => {
63198
63417
  const parsed = parseWithCompat(notificationSchema, notification);
63199
- return Promise.resolve(handler12(parsed));
63418
+ return Promise.resolve(handler14(parsed));
63200
63419
  });
63201
63420
  }
63202
63421
  removeNotificationHandler(method) {
@@ -70138,7 +70357,7 @@ var init_server2 = __esm(() => {
70138
70357
  }
70139
70358
  this._capabilities = mergeCapabilities(this._capabilities, capabilities);
70140
70359
  }
70141
- setRequestHandler(requestSchema, handler12) {
70360
+ setRequestHandler(requestSchema, handler14) {
70142
70361
  const shape = getObjectShape(requestSchema);
70143
70362
  const methodSchema = shape?.method;
70144
70363
  if (!methodSchema) {
@@ -70157,7 +70376,7 @@ var init_server2 = __esm(() => {
70157
70376
  throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
70158
70377
  }
70159
70378
  const { params } = validatedRequest.data;
70160
- const result = await Promise.resolve(handler12(request, extra));
70379
+ const result = await Promise.resolve(handler14(request, extra));
70161
70380
  if (params.task) {
70162
70381
  const taskValidationResult = safeParse2(CreateTaskResultSchema, result);
70163
70382
  if (!taskValidationResult.success) {
@@ -70175,7 +70394,7 @@ var init_server2 = __esm(() => {
70175
70394
  };
70176
70395
  return super.setRequestHandler(requestSchema, wrappedHandler);
70177
70396
  }
70178
- return super.setRequestHandler(requestSchema, handler12);
70397
+ return super.setRequestHandler(requestSchema, handler14);
70179
70398
  }
70180
70399
  assertCapabilityForMethod(method) {
70181
70400
  switch (method) {
@@ -72540,14 +72759,38 @@ async function action7(documentId, options) {
72540
72759
  return;
72541
72760
  }
72542
72761
  }
72543
- await client.rpc("cerefox_delete_document", {
72544
- p_document_id: documentId,
72545
- p_author: author,
72546
- p_author_type: authorType
72547
- });
72762
+ let result;
72763
+ try {
72764
+ result = await client.rpc("cerefox_delete_document", {
72765
+ p_document_id: documentId,
72766
+ p_author: author,
72767
+ p_author_type: authorType,
72768
+ ...options.reason ? { p_reason: options.reason } : {}
72769
+ });
72770
+ } catch (e) {
72771
+ const message = e instanceof Error ? e.message : String(e);
72772
+ if (isMissingFunctionError(message, "cerefox_delete_document")) {
72773
+ throw systemError(options.reason ? `This server is behind: \`--reason\` needs schema 0.12.0 or newer. ` + `Run \`cerefox server deploy\` and retry, or retry without --reason.` : `The delete did not run: the server has no matching cerefox_delete_document ` + `(mid-deploy window, or an old schema). Run \`cerefox server deploy\` and retry.`);
72774
+ }
72775
+ throw e;
72776
+ }
72777
+ if (result === null) {
72778
+ const { data: check, error: checkError } = await client.raw.from("cerefox_documents").select("deleted_at").eq("id", documentId).maybeSingle();
72779
+ if (checkError) {
72780
+ warn(`Delete submitted, but the follow-up verification read failed (${checkError.message}). ` + `Confirm with: cerefox document get ${documentId}`);
72781
+ return;
72782
+ }
72783
+ if (!check?.deleted_at) {
72784
+ throw systemError(`The delete did not take effect — the server has no matching ` + `cerefox_delete_document (mid-deploy window, or an old schema). ` + `Run \`cerefox server deploy\` and retry.`);
72785
+ }
72786
+ }
72787
+ if (result?.already_deleted) {
72788
+ println(c.dim(`Document ${documentId} ("${doc.title}") was already soft-deleted at ${result.deleted_at} ` + `by the time the delete ran. No change was made and no reason was recorded.`));
72789
+ return;
72790
+ }
72548
72791
  println(c.green(`✓ Soft-deleted "${doc.title}" (id: ${documentId}). Recoverable from the Cerefox web UI trash.`));
72549
72792
  if (options.reason) {
72550
- println(c.dim(` Reason (informational only): ${options.reason}`));
72793
+ println(c.dim(` Reason (recorded in the audit log): ${options.reason}`));
72551
72794
  }
72552
72795
  }
72553
72796
  function registerDeleteDoc(program2) {
@@ -73116,15 +73359,45 @@ async function action11(documentId, options) {
73116
73359
  if (author === "unknown") {
73117
73360
  warn("No --author / CEREFOX_AUTHOR_NAME set — audit log will record this restore as 'unknown'.");
73118
73361
  }
73119
- await client.rpc("cerefox_restore_document", {
73120
- p_document_id: documentId,
73121
- p_author: author,
73122
- p_author_type: authorType
73123
- });
73362
+ let result;
73363
+ try {
73364
+ result = await client.rpc("cerefox_restore_document", {
73365
+ p_document_id: documentId,
73366
+ p_author: author,
73367
+ p_author_type: authorType,
73368
+ ...options.reason ? { p_reason: options.reason } : {}
73369
+ });
73370
+ } catch (e) {
73371
+ const message = e instanceof Error ? e.message : String(e);
73372
+ if (isMissingFunctionError(message, "cerefox_restore_document")) {
73373
+ throw systemError(options.reason ? `This server is behind: \`--reason\` needs schema 0.12.0 or newer. ` + `Run \`cerefox server deploy\` and retry, or retry without --reason.` : `The restore did not run: the server has no matching cerefox_restore_document ` + `(mid-deploy window, or an old schema). Run \`cerefox server deploy\` and retry.`);
73374
+ }
73375
+ throw e;
73376
+ }
73377
+ if (result === null) {
73378
+ const { data: check, error: checkError } = await client.raw.from("cerefox_documents").select("deleted_at").eq("id", documentId).maybeSingle();
73379
+ if (checkError) {
73380
+ warn(`Restore submitted, but the follow-up verification read failed (${checkError.message}). ` + `Confirm with: cerefox document get ${documentId}`);
73381
+ return;
73382
+ }
73383
+ if (!check) {
73384
+ throw systemError(`Document ${documentId} no longer exists — it may have been purged concurrently. Nothing was restored.`);
73385
+ }
73386
+ if (check.deleted_at) {
73387
+ throw systemError(`The restore did not take effect — the server has no matching ` + `cerefox_restore_document (mid-deploy window, or an old schema). ` + `Run \`cerefox server deploy\` and retry.`);
73388
+ }
73389
+ }
73390
+ if (result && result.restored === false) {
73391
+ println(c.dim(`Document ${documentId} ("${doc.title}") was already restored by the time this ran. No change was made.`));
73392
+ return;
73393
+ }
73124
73394
  println(c.green(`✓ Restored "${doc.title}" (id: ${documentId}) from the trash.`));
73395
+ if (options.reason) {
73396
+ println(c.dim(` Reason (recorded in the audit log): ${options.reason}`));
73397
+ }
73125
73398
  }
73126
73399
  function registerDocumentRestore(parent) {
73127
- parent.command("restore").description("Restore a soft-deleted document from the trash (inverse of `document delete`).").argument("<document-id>", "UUID of the soft-deleted document.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(action11);
73400
+ parent.command("restore").description("Restore a soft-deleted document from the trash (inverse of `document delete`).").argument("<document-id>", "UUID of the soft-deleted document.").option("--reason <text>", "Optional reason recorded in the audit log.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(action11);
73128
73401
  }
73129
73402
 
73130
73403
  // src/cli/commands/document-set-metadata.ts
@@ -77895,15 +78168,20 @@ class IngestionDbBridge {
77895
78168
  const rows = data ?? [];
77896
78169
  return rows.length > 0 ? rows[0] : null;
77897
78170
  }
78171
+ async findPreferLive(column, value) {
78172
+ const live = await this.supabase.from("cerefox_documents").select("*").eq(column, value).is("deleted_at", null).order("updated_at", { ascending: false }).limit(1);
78173
+ const liveRows = live.data ?? [];
78174
+ if (liveRows.length > 0)
78175
+ return liveRows[0];
78176
+ const any = await this.supabase.from("cerefox_documents").select("*").eq(column, value).order("updated_at", { ascending: false }).limit(1);
78177
+ const anyRows = any.data ?? [];
78178
+ return anyRows.length > 0 ? anyRows[0] : null;
78179
+ }
77898
78180
  async findDocumentByTitle(title) {
77899
- const { data } = await this.supabase.from("cerefox_documents").select("*").eq("title", title).order("updated_at", { ascending: false }).limit(1);
77900
- const rows = data ?? [];
77901
- return rows.length > 0 ? rows[0] : null;
78181
+ return this.findPreferLive("title", title);
77902
78182
  }
77903
78183
  async findDocumentBySourcePath(sourcePath) {
77904
- const { data } = await this.supabase.from("cerefox_documents").select("*").eq("source_path", sourcePath).order("updated_at", { ascending: false }).limit(1);
77905
- const rows = data ?? [];
77906
- return rows.length > 0 ? rows[0] : null;
78184
+ return this.findPreferLive("source_path", sourcePath);
77907
78185
  }
77908
78186
  async listChunksForDocument(documentId) {
77909
78187
  const data = await fetchAllPages((from, to) => this.supabase.from("cerefox_chunks").select("id, document_id, chunk_index, heading_path, heading_level, title, content, char_count").eq("document_id", documentId).is("version_id", null).order("chunk_index").range(from, to));
@@ -77942,8 +78220,8 @@ class IngestionDbBridge {
77942
78220
  if (error2) {
77943
78221
  const msg = error2.message ?? JSON.stringify(error2);
77944
78222
  if (msg.includes("CEREFOX_CONFLICT")) {
77945
- const current = msg.match(/current hash ([0-9a-f]{64})/)?.[1] ?? null;
77946
- throw new ConcurrencyConflictError(args.documentId ?? "", current, msg);
78223
+ const { current } = extractConflictHashes(msg);
78224
+ throw new ConcurrencyConflictError(args.documentId ?? "", current === "unknown" ? null : current, msg);
77947
78225
  }
77948
78226
  if (msg.includes("CEREFOX_TOKEN_REQUIRED")) {
77949
78227
  throw new ConcurrencyTokenRequiredError(msg);
@@ -78126,12 +78404,33 @@ class IngestionPipeline {
78126
78404
  }
78127
78405
  if (updateExisting) {
78128
78406
  let existingDoc = null;
78407
+ let matchedBySourcePath = false;
78129
78408
  if (sourcePathOpt) {
78130
78409
  existingDoc = await this.db.findDocumentBySourcePath(sourcePathOpt);
78410
+ matchedBySourcePath = existingDoc !== null;
78131
78411
  }
78132
78412
  if (!existingDoc) {
78133
78413
  existingDoc = await this.db.findDocumentByTitle(title);
78134
78414
  }
78415
+ if (existingDoc?.deleted_at) {
78416
+ if (matchedBySourcePath && lastWriteWins) {
78417
+ const projIds = await this.db.getDocumentProjectIds(existingDoc.id);
78418
+ return {
78419
+ documentId: existingDoc.id,
78420
+ title: existingDoc.title ?? title,
78421
+ chunkCount: existingDoc.chunk_count ?? 0,
78422
+ totalChars: existingDoc.total_chars ?? 0,
78423
+ action: "skipped",
78424
+ reindexed: false,
78425
+ projectIds: projIds,
78426
+ note: `"${existingDoc.title}" is in the trash (soft-deleted ` + `${existingDoc.deleted_at.slice(0, 10)}) — skipped, deletion respected. ` + `To resume syncing this file, restore the document ` + `(\`cerefox document restore ${existingDoc.id}\`) or purge it from the web UI Trash.`,
78427
+ contentHash: existingDoc.content_hash ?? ""
78428
+ };
78429
+ }
78430
+ if (!matchedBySourcePath) {
78431
+ existingDoc = null;
78432
+ }
78433
+ }
78135
78434
  if (existingDoc) {
78136
78435
  let fullSetResolved = null;
78137
78436
  if (listFormProvided) {
@@ -78174,7 +78473,7 @@ class IngestionPipeline {
78174
78473
  action: "skipped",
78175
78474
  reindexed: false,
78176
78475
  projectIds: existingProjectIds,
78177
- note: "",
78476
+ note: existingByHash.deleted_at ? `Identical content is in the TRASH as "${existingByHash.title}" ` + `(soft-deleted ${existingByHash.deleted_at.slice(0, 10)}). Restore it ` + `(\`cerefox document restore ${existingByHash.id}\` or the web UI Trash page) ` + `instead of re-ingesting, or purge it first to start fresh.` : "",
78178
78477
  contentHash: existingByHash.content_hash ?? hash
78179
78478
  };
78180
78479
  }
@@ -78246,11 +78545,15 @@ class IngestionPipeline {
78246
78545
  if (!existing) {
78247
78546
  throw new Error(`Document ${JSON.stringify(documentId)} not found`);
78248
78547
  }
78548
+ if (existing.deleted_at) {
78549
+ throw new Error(`Document ${documentId} ("${existing.title}") is soft-deleted (in the trash). ` + `A trashed document cannot be updated — restore it first ` + `(\`cerefox document restore ${documentId}\`, the web UI Trash page, or ` + `cerefox_restore_document over MCP), then re-ingest.`);
78550
+ }
78249
78551
  const newHash = contentHash(text);
78250
78552
  const contentUnchanged = newHash === existing.content_hash;
78553
+ const expectedHashTrimmed = expectedContentHash?.trim() || null;
78251
78554
  if (!contentUnchanged) {
78252
- if (!lastWriteWins && expectedContentHash && expectedContentHash !== existing.content_hash) {
78253
- throw new ConcurrencyConflictError(documentId, existing.content_hash, `CEREFOX_CONFLICT: document ${documentId} changed since it was read ` + `(expected hash ${expectedContentHash}, current hash ${existing.content_hash}). ` + `Re-read the document, merge your changes, and retry with the new hash.`);
78555
+ if (!lastWriteWins && expectedHashTrimmed && expectedHashTrimmed !== existing.content_hash) {
78556
+ throw new ConcurrencyConflictError(documentId, existing.content_hash, `CEREFOX_CONFLICT: document ${documentId} changed since it was read ` + `(expected hash ${expectedHashTrimmed}, current hash ${existing.content_hash}). ` + `Re-read the document, merge your changes, and retry with the new hash.`);
78254
78557
  }
78255
78558
  const collision = await this.db.getDocumentByHash(newHash);
78256
78559
  if (collision && collision.id !== documentId) {
@@ -78351,7 +78654,7 @@ class IngestionPipeline {
78351
78654
  author,
78352
78655
  authorType,
78353
78656
  sourceLabel: sourceLabel ?? source ?? "manual",
78354
- expectedContentHash: expectedContentHash ?? (forceRechunk && contentUnchanged ? existing.content_hash : null),
78657
+ expectedContentHash: expectedHashTrimmed ?? (forceRechunk && contentUnchanged ? existing.content_hash : null),
78355
78658
  lastWriteWins
78356
78659
  });
78357
78660
  let finalProjectIds;
@@ -78664,7 +78967,7 @@ async function action20(dir, options) {
78664
78967
  outcomes.push({
78665
78968
  file,
78666
78969
  status: "ok",
78667
- detail: `${result.action}: ${result.chunkCount} chunks`
78970
+ detail: `${result.action}: ${result.chunkCount} chunks${result.note ? ` (${result.note})` : ""}`
78668
78971
  });
78669
78972
  } catch (err) {
78670
78973
  const msg = err instanceof Error ? err.message : String(err);
@@ -80371,13 +80674,13 @@ var WSContext = class {
80371
80674
  this.#init.close(code, reason);
80372
80675
  }
80373
80676
  };
80374
- var defineWebSocketHelper = (handler12) => {
80677
+ var defineWebSocketHelper = (handler14) => {
80375
80678
  return (...args) => {
80376
80679
  if (typeof args[0] === "function") {
80377
80680
  const [createEvents, options] = args;
80378
80681
  return async function upgradeWebSocket(c2, next) {
80379
80682
  const events = await createEvents(c2);
80380
- const result = await handler12(c2, events, options);
80683
+ const result = await handler14(c2, events, options);
80381
80684
  if (result) {
80382
80685
  return result;
80383
80686
  }
@@ -80386,7 +80689,7 @@ var defineWebSocketHelper = (handler12) => {
80386
80689
  } else {
80387
80690
  const [c2, events, options] = args;
80388
80691
  return (async () => {
80389
- const upgraded = await handler12(c2, events, options);
80692
+ const upgraded = await handler14(c2, events, options);
80390
80693
  if (!upgraded) {
80391
80694
  throw new Error("Failed to upgrade WebSocket");
80392
80695
  }
@@ -81979,16 +82282,16 @@ var compose = (middleware, onError, onNotFound) => {
81979
82282
  index = i;
81980
82283
  let res;
81981
82284
  let isError = false;
81982
- let handler12;
82285
+ let handler14;
81983
82286
  if (middleware[i]) {
81984
- handler12 = middleware[i][0][0];
82287
+ handler14 = middleware[i][0][0];
81985
82288
  context2.req.routeIndex = i;
81986
82289
  } else {
81987
- handler12 = i === middleware.length && next || undefined;
82290
+ handler14 = i === middleware.length && next || undefined;
81988
82291
  }
81989
- if (handler12) {
82292
+ if (handler14) {
81990
82293
  try {
81991
- res = await handler12(context2, () => dispatch(i + 1));
82294
+ res = await handler14(context2, () => dispatch(i + 1));
81992
82295
  } catch (err) {
81993
82296
  if (err instanceof Error && onError) {
81994
82297
  context2.error = err;
@@ -82672,8 +82975,8 @@ var Hono = class _Hono {
82672
82975
  } else {
82673
82976
  this.#addRoute(method, this.#path, args1);
82674
82977
  }
82675
- args.forEach((handler12) => {
82676
- this.#addRoute(method, this.#path, handler12);
82978
+ args.forEach((handler14) => {
82979
+ this.#addRoute(method, this.#path, handler14);
82677
82980
  });
82678
82981
  return this;
82679
82982
  };
@@ -82682,8 +82985,8 @@ var Hono = class _Hono {
82682
82985
  for (const p of [path].flat()) {
82683
82986
  this.#path = p;
82684
82987
  for (const m of [method].flat()) {
82685
- handlers.map((handler12) => {
82686
- this.#addRoute(m.toUpperCase(), this.#path, handler12);
82988
+ handlers.map((handler14) => {
82989
+ this.#addRoute(m.toUpperCase(), this.#path, handler14);
82687
82990
  });
82688
82991
  }
82689
82992
  }
@@ -82696,8 +82999,8 @@ var Hono = class _Hono {
82696
82999
  this.#path = "*";
82697
83000
  handlers.unshift(arg1);
82698
83001
  }
82699
- handlers.forEach((handler12) => {
82700
- this.#addRoute(METHOD_NAME_ALL, this.#path, handler12);
83002
+ handlers.forEach((handler14) => {
83003
+ this.#addRoute(METHOD_NAME_ALL, this.#path, handler14);
82701
83004
  });
82702
83005
  return this;
82703
83006
  };
@@ -82720,14 +83023,14 @@ var Hono = class _Hono {
82720
83023
  route(path, app) {
82721
83024
  const subApp = this.basePath(path);
82722
83025
  app.routes.map((r) => {
82723
- let handler12;
83026
+ let handler14;
82724
83027
  if (app.errorHandler === errorHandler) {
82725
- handler12 = r.handler;
83028
+ handler14 = r.handler;
82726
83029
  } else {
82727
- handler12 = async (c2, next) => (await compose([], app.errorHandler)(c2, () => r.handler(c2, next))).res;
82728
- handler12[COMPOSED_HANDLER] = r.handler;
83030
+ handler14 = async (c2, next) => (await compose([], app.errorHandler)(c2, () => r.handler(c2, next))).res;
83031
+ handler14[COMPOSED_HANDLER] = r.handler;
82729
83032
  }
82730
- subApp.#addRoute(r.method, r.path, handler12, r.basePath);
83033
+ subApp.#addRoute(r.method, r.path, handler14, r.basePath);
82731
83034
  });
82732
83035
  return this;
82733
83036
  }
@@ -82736,12 +83039,12 @@ var Hono = class _Hono {
82736
83039
  subApp._basePath = mergePath(this._basePath, path);
82737
83040
  return subApp;
82738
83041
  }
82739
- onError = (handler12) => {
82740
- this.errorHandler = handler12;
83042
+ onError = (handler14) => {
83043
+ this.errorHandler = handler14;
82741
83044
  return this;
82742
83045
  };
82743
- notFound = (handler12) => {
82744
- this.#notFoundHandler = handler12;
83046
+ notFound = (handler14) => {
83047
+ this.#notFoundHandler = handler14;
82745
83048
  return this;
82746
83049
  };
82747
83050
  mount(path, applicationHandler, options) {
@@ -82778,26 +83081,26 @@ var Hono = class _Hono {
82778
83081
  return new Request(url, request);
82779
83082
  };
82780
83083
  })();
82781
- const handler12 = async (c2, next) => {
83084
+ const handler14 = async (c2, next) => {
82782
83085
  const res = await applicationHandler(replaceRequest(c2.req.raw), ...getOptions(c2));
82783
83086
  if (res) {
82784
83087
  return res;
82785
83088
  }
82786
83089
  await next();
82787
83090
  };
82788
- this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"), handler12);
83091
+ this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"), handler14);
82789
83092
  return this;
82790
83093
  }
82791
- #addRoute(method, path, handler12, baseRoutePath) {
83094
+ #addRoute(method, path, handler14, baseRoutePath) {
82792
83095
  method = method.toUpperCase();
82793
83096
  path = mergePath(this._basePath, path);
82794
83097
  const r = {
82795
83098
  basePath: baseRoutePath !== undefined ? mergePath(this._basePath, baseRoutePath) : this._basePath,
82796
83099
  path,
82797
83100
  method,
82798
- handler: handler12
83101
+ handler: handler14
82799
83102
  };
82800
- this.router.add(method, path, [handler12, r]);
83103
+ this.router.add(method, path, [handler14, r]);
82801
83104
  this.routes.push(r);
82802
83105
  }
82803
83106
  #handleError(err, c2) {
@@ -83122,7 +83425,7 @@ var RegExpRouter = class {
83122
83425
  this.#middleware = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
83123
83426
  this.#routes = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
83124
83427
  }
83125
- add(method, path, handler12) {
83428
+ add(method, path, handler14) {
83126
83429
  const middleware = this.#middleware;
83127
83430
  const routes = this.#routes;
83128
83431
  if (!middleware || !routes) {
@@ -83152,13 +83455,13 @@ var RegExpRouter = class {
83152
83455
  Object.keys(middleware).forEach((m) => {
83153
83456
  if (method === METHOD_NAME_ALL || method === m) {
83154
83457
  Object.keys(middleware[m]).forEach((p) => {
83155
- re.test(p) && middleware[m][p].push([handler12, paramCount]);
83458
+ re.test(p) && middleware[m][p].push([handler14, paramCount]);
83156
83459
  });
83157
83460
  }
83158
83461
  });
83159
83462
  Object.keys(routes).forEach((m) => {
83160
83463
  if (method === METHOD_NAME_ALL || method === m) {
83161
- Object.keys(routes[m]).forEach((p) => re.test(p) && routes[m][p].push([handler12, paramCount]));
83464
+ Object.keys(routes[m]).forEach((p) => re.test(p) && routes[m][p].push([handler14, paramCount]));
83162
83465
  }
83163
83466
  });
83164
83467
  return;
@@ -83171,7 +83474,7 @@ var RegExpRouter = class {
83171
83474
  routes[m][path2] ||= [
83172
83475
  ...findMiddleware(middleware[m], path2) || findMiddleware(middleware[METHOD_NAME_ALL], path2) || []
83173
83476
  ];
83174
- routes[m][path2].push([handler12, paramCount - len + i + 1]);
83477
+ routes[m][path2].push([handler14, paramCount - len + i + 1]);
83175
83478
  }
83176
83479
  });
83177
83480
  }
@@ -83220,21 +83523,21 @@ var PreparedRegExpRouter = class {
83220
83523
  matcher[1].forEach((list) => list && list.push(handlerData));
83221
83524
  Object.values(matcher[2]).forEach((list) => list[0].push(handlerData));
83222
83525
  }
83223
- #addPath(method, path, handler12, indexes, map2) {
83526
+ #addPath(method, path, handler14, indexes, map2) {
83224
83527
  const matcher = this.#matchers[method];
83225
83528
  if (!map2) {
83226
- matcher[2][path][0].push([handler12, {}]);
83529
+ matcher[2][path][0].push([handler14, {}]);
83227
83530
  } else {
83228
83531
  indexes.forEach((index) => {
83229
83532
  if (typeof index === "number") {
83230
- matcher[1][index].push([handler12, map2]);
83533
+ matcher[1][index].push([handler14, map2]);
83231
83534
  } else {
83232
- matcher[2][index || path][0].push([handler12, map2]);
83535
+ matcher[2][index || path][0].push([handler14, map2]);
83233
83536
  }
83234
83537
  });
83235
83538
  }
83236
83539
  }
83237
- add(method, path, handler12) {
83540
+ add(method, path, handler14) {
83238
83541
  if (!this.#matchers[method]) {
83239
83542
  const all = this.#matchers[METHOD_NAME_ALL];
83240
83543
  const staticMap = {};
@@ -83248,7 +83551,7 @@ var PreparedRegExpRouter = class {
83248
83551
  ];
83249
83552
  }
83250
83553
  if (path === "/*" || path === "*") {
83251
- const handlerData = [handler12, {}];
83554
+ const handlerData = [handler14, {}];
83252
83555
  if (method === METHOD_NAME_ALL) {
83253
83556
  for (const m in this.#matchers) {
83254
83557
  this.#addWildcard(m, handlerData);
@@ -83265,10 +83568,10 @@ var PreparedRegExpRouter = class {
83265
83568
  for (const [indexes, map2] of data) {
83266
83569
  if (method === METHOD_NAME_ALL) {
83267
83570
  for (const m in this.#matchers) {
83268
- this.#addPath(m, path, handler12, indexes, map2);
83571
+ this.#addPath(m, path, handler14, indexes, map2);
83269
83572
  }
83270
83573
  } else {
83271
- this.#addPath(method, path, handler12, indexes, map2);
83574
+ this.#addPath(method, path, handler14, indexes, map2);
83272
83575
  }
83273
83576
  }
83274
83577
  }
@@ -83286,11 +83589,11 @@ var SmartRouter = class {
83286
83589
  constructor(init) {
83287
83590
  this.#routers = init.routers;
83288
83591
  }
83289
- add(method, path, handler12) {
83592
+ add(method, path, handler14) {
83290
83593
  if (!this.#routes) {
83291
83594
  throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
83292
83595
  }
83293
- this.#routes.push([method, path, handler12]);
83596
+ this.#routes.push([method, path, handler14]);
83294
83597
  }
83295
83598
  match(method, path) {
83296
83599
  if (!this.#routes) {
@@ -83347,17 +83650,17 @@ var Node2 = class _Node2 {
83347
83650
  #patterns;
83348
83651
  #order = 0;
83349
83652
  #params = emptyParams;
83350
- constructor(method, handler12, children) {
83653
+ constructor(method, handler14, children) {
83351
83654
  this.#children = children || /* @__PURE__ */ Object.create(null);
83352
83655
  this.#methods = [];
83353
- if (method && handler12) {
83656
+ if (method && handler14) {
83354
83657
  const m = /* @__PURE__ */ Object.create(null);
83355
- m[method] = { handler: handler12, possibleKeys: [], score: 0 };
83658
+ m[method] = { handler: handler14, possibleKeys: [], score: 0 };
83356
83659
  this.#methods = [m];
83357
83660
  }
83358
83661
  this.#patterns = [];
83359
83662
  }
83360
- insert(method, path, handler12) {
83663
+ insert(method, path, handler14) {
83361
83664
  this.#order = ++this.#order;
83362
83665
  let curNode = this;
83363
83666
  const parts = splitRoutingPath(path);
@@ -83383,7 +83686,7 @@ var Node2 = class _Node2 {
83383
83686
  }
83384
83687
  curNode.#methods.push({
83385
83688
  [method]: {
83386
- handler: handler12,
83689
+ handler: handler14,
83387
83690
  possibleKeys: possibleKeys.filter((v, i, a) => a.indexOf(v) === i),
83388
83691
  score: this.#order
83389
83692
  }
@@ -83501,7 +83804,7 @@ var Node2 = class _Node2 {
83501
83804
  return a.score - b2.score;
83502
83805
  });
83503
83806
  }
83504
- return [handlerSets.map(({ handler: handler12, params }) => [handler12, params])];
83807
+ return [handlerSets.map(({ handler: handler14, params }) => [handler14, params])];
83505
83808
  }
83506
83809
  };
83507
83810
 
@@ -83512,15 +83815,15 @@ var TrieRouter = class {
83512
83815
  constructor() {
83513
83816
  this.#node = new Node2;
83514
83817
  }
83515
- add(method, path, handler12) {
83818
+ add(method, path, handler14) {
83516
83819
  const results = checkOptionalParameter(path);
83517
83820
  if (results) {
83518
83821
  for (let i = 0, len = results.length;i < len; i++) {
83519
- this.#node.insert(method, results[i], handler12);
83822
+ this.#node.insert(method, results[i], handler14);
83520
83823
  }
83521
83824
  return;
83522
83825
  }
83523
- this.#node.insert(method, path, handler12);
83826
+ this.#node.insert(method, path, handler14);
83524
83827
  }
83525
83828
  match(method, path) {
83526
83829
  return this.#node.search(method, path);
@@ -84353,6 +84656,33 @@ function registerDiscoveryRoutes(app, ctx) {
84353
84656
  content: row.content ?? null
84354
84657
  })));
84355
84658
  });
84659
+ app.get("/api/v1/dashboard/recent-docs", async (c2) => {
84660
+ const projectId = c2.req.query("project_id") || null;
84661
+ if (projectId && !UUID_RE4.test(projectId)) {
84662
+ return c2.json({ detail: "project_id must be a UUID" }, 400);
84663
+ }
84664
+ const [recentDocs, projects] = await Promise.all([
84665
+ listDocuments(ctx, { projectId, limit: 10 }),
84666
+ listAllProjects(ctx)
84667
+ ]);
84668
+ const docIds = recentDocs.map((d) => String(d.id));
84669
+ const [docProjectsMap, authors] = await Promise.all([
84670
+ getProjectsForDocuments(ctx, docIds, projects),
84671
+ getRecentDocAuthors(ctx, docIds)
84672
+ ]);
84673
+ return c2.json({
84674
+ recent_docs: recentDocs.map((d) => {
84675
+ const id = String(d.id);
84676
+ const pids = (docProjectsMap[id] ?? []).map((p) => String(p.id));
84677
+ const a = authors[id];
84678
+ return {
84679
+ ...dashboardDocFromRow(d, pids),
84680
+ author: a?.author ?? null,
84681
+ author_type: a?.author_type ?? null
84682
+ };
84683
+ })
84684
+ });
84685
+ });
84356
84686
  app.get("/api/v1/dashboard", async (c2) => {
84357
84687
  const [recentDocs, projects, docCount, totals] = await Promise.all([
84358
84688
  listDocuments(ctx, { limit: 10 }),
@@ -84744,6 +85074,13 @@ function registerDocumentWriteRoutes(app, ctx) {
84744
85074
  if (!doc2) {
84745
85075
  return c2.json({ success: false, error: "Document not found" }, 404);
84746
85076
  }
85077
+ if (doc2.deleted_at) {
85078
+ return c2.json({
85079
+ success: false,
85080
+ error: "document is in the trash",
85081
+ message: "This document was moved to the trash while you were editing. " + "Restore it from the Trash page first, then save again."
85082
+ }, 409);
85083
+ }
84747
85084
  const currentHash = doc2.content_hash;
84748
85085
  const proposedHash = content.trim() ? contentHash(content) : null;
84749
85086
  const contentChanged = proposedHash !== null && currentHash !== null && proposedHash !== currentHash;
@@ -84787,10 +85124,23 @@ function registerDocumentWriteRoutes(app, ctx) {
84787
85124
  message: err.message
84788
85125
  }, 400);
84789
85126
  }
84790
- return c2.json({
84791
- success: false,
84792
- error: err instanceof Error ? err.message : String(err)
84793
- }, 500);
85127
+ const msg = err instanceof Error ? err.message : String(err);
85128
+ if (msg.includes("CEREFOX_UNRESOLVED_LINKS")) {
85129
+ const ids = msg.match(/do not exist: ([^.]+)\./)?.[1] ?? "";
85130
+ return c2.json({
85131
+ success: false,
85132
+ error: "unresolved document links",
85133
+ message: `This content links document id(s) that don't exist${ids ? `: ${ids}` : ""}. ` + `Fix or remove the broken link(s), or wrap example ids in backticks.`
85134
+ }, 422);
85135
+ }
85136
+ if (msg.includes("soft-deleted")) {
85137
+ return c2.json({
85138
+ success: false,
85139
+ error: "document is in the trash",
85140
+ message: "This document was moved to the trash while you were editing. " + "Restore it from the Trash page first, then save again."
85141
+ }, 409);
85142
+ }
85143
+ return c2.json({ success: false, error: msg }, 500);
84794
85144
  }
84795
85145
  }
84796
85146
  const updates = {};
@@ -84823,25 +85173,35 @@ function registerDocumentWriteRoutes(app, ctx) {
84823
85173
  });
84824
85174
  app.delete("/api/v1/documents/:document_id", async (c2) => {
84825
85175
  const documentId = c2.req.param("document_id");
84826
- const { error: error3 } = await ctx.supabase.rpc("cerefox_delete_document", {
85176
+ const { data, error: error3 } = await ctx.supabase.rpc("cerefox_delete_document", {
84827
85177
  p_document_id: documentId,
84828
85178
  p_author: "web-ui",
84829
85179
  p_author_type: "user"
84830
85180
  });
84831
- if (error3)
85181
+ if (error3) {
85182
+ if (isDocumentNotFoundError(error3)) {
85183
+ return c2.json({ detail: `Document ${documentId} not found` }, 404);
85184
+ }
84832
85185
  return c2.json({ detail: error3.message }, 500);
84833
- return c2.json({ success: true });
85186
+ }
85187
+ const row = data;
85188
+ return c2.json({ success: true, ...row ? { already_deleted: row.already_deleted ?? false } : {} });
84834
85189
  });
84835
85190
  app.post("/api/v1/documents/:document_id/restore", async (c2) => {
84836
85191
  const documentId = c2.req.param("document_id");
84837
- const { error: error3 } = await ctx.supabase.rpc("cerefox_restore_document", {
85192
+ const { data, error: error3 } = await ctx.supabase.rpc("cerefox_restore_document", {
84838
85193
  p_document_id: documentId,
84839
85194
  p_author: "web-ui",
84840
85195
  p_author_type: "user"
84841
85196
  });
84842
- if (error3)
85197
+ if (error3) {
85198
+ if (isDocumentNotFoundError(error3)) {
85199
+ return c2.json({ detail: `Document ${documentId} not found` }, 404);
85200
+ }
84843
85201
  return c2.json({ detail: error3.message }, 500);
84844
- return c2.json({ success: true });
85202
+ }
85203
+ const row = data;
85204
+ return c2.json({ success: true, ...row ? { restored: row.restored ?? false } : {} });
84845
85205
  });
84846
85206
  app.delete("/api/v1/documents/:document_id/purge", async (c2) => {
84847
85207
  const documentId = c2.req.param("document_id");
@@ -84867,6 +85227,9 @@ function registerDocumentWriteRoutes(app, ctx) {
84867
85227
  return c2.json({ detail: `Invalid status: ${JSON.stringify(status)}` }, 400);
84868
85228
  }
84869
85229
  const old = await getCurrentDoc(ctx, documentId);
85230
+ if (old?.deleted_at) {
85231
+ return c2.json({ detail: "This document is in the trash — restore it before changing its review status." }, 409);
85232
+ }
84870
85233
  const oldStatus = old?.review_status ?? "unknown";
84871
85234
  const { error: error3 } = await ctx.supabase.from("cerefox_documents").update({ review_status: status, updated_at: new Date().toISOString() }).eq("id", documentId);
84872
85235
  if (error3)
@@ -85013,7 +85376,8 @@ function registerIngestRoutes(app, ctx) {
85013
85376
  document_id: result.documentId,
85014
85377
  title: result.title,
85015
85378
  skipped,
85016
- updated: result.reindexed
85379
+ updated: result.reindexed,
85380
+ ...result.note ? { note: result.note } : {}
85017
85381
  }, 200);
85018
85382
  } catch (err) {
85019
85383
  return c2.json(notReady(err instanceof Error ? err.message : String(err)), 200);