@cerefox/memory 1.13.0 → 1.13.1

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.
Files changed (31) hide show
  1. package/AGENT_GUIDE.md +26 -26
  2. package/AGENT_QUICK_REFERENCE.md +9 -9
  3. package/dist/bin/cerefox.js +93 -113
  4. package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
  5. package/dist/server-assets/_shared/mcp-tools/audit-log.ts +10 -8
  6. package/dist/server-assets/_shared/mcp-tools/delete-document.ts +4 -10
  7. package/dist/server-assets/_shared/mcp-tools/feature-flags.ts +4 -3
  8. package/dist/server-assets/_shared/mcp-tools/get-document.ts +3 -6
  9. package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +3 -3
  10. package/dist/server-assets/_shared/mcp-tools/get-help.ts +3 -6
  11. package/dist/server-assets/_shared/mcp-tools/identity.ts +48 -0
  12. package/dist/server-assets/_shared/mcp-tools/ingest.ts +3 -6
  13. package/dist/server-assets/_shared/mcp-tools/list-metadata-keys.ts +3 -6
  14. package/dist/server-assets/_shared/mcp-tools/list-projects.ts +3 -6
  15. package/dist/server-assets/_shared/mcp-tools/list-versions.ts +3 -6
  16. package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +3 -6
  17. package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +5 -10
  18. package/dist/server-assets/_shared/mcp-tools/relations.ts +11 -12
  19. package/dist/server-assets/_shared/mcp-tools/restore-document.ts +4 -10
  20. package/dist/server-assets/_shared/mcp-tools/search.ts +3 -6
  21. package/dist/server-assets/_shared/mcp-tools/set-document-metadata.ts +4 -10
  22. package/dist/server-assets/_shared/mcp-tools/set-document-projects.ts +3 -6
  23. package/dist/server-assets/db/migrations/0031_review_workflow_toggle.sql +4 -2
  24. package/dist/server-assets/db/rpcs.sql +19 -12
  25. package/dist/server-assets/db/schema.sql +4 -4
  26. package/dist/server-assets/supabase/functions/cerefox-mcp/index.ts +6 -6
  27. package/docs/guides/cli.md +11 -11
  28. package/docs/guides/configuration.md +33 -24
  29. package/docs/guides/connect-agents.md +6 -6
  30. package/docs/guides/upgrading.md +11 -0
  31. package/package.json +1 -1
@@ -7400,7 +7400,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
7400
7400
  });
7401
7401
 
7402
7402
  // src/meta.ts
7403
- var PKG_VERSION = "1.13.0";
7403
+ var PKG_VERSION = "1.13.1";
7404
7404
  var init_meta = () => {};
7405
7405
 
7406
7406
  // ../../_shared/config/paths.ts
@@ -25774,7 +25774,7 @@ var init_bundled_docs = __esm(() => {
25774
25774
  });
25775
25775
 
25776
25776
  // ../../_shared/ef-meta/index.ts
25777
- var EF_VERSION = "1.13.0", CEREFOX_VERSION = "1.13.0", EF_LAST_CHANGED = "1.13.0";
25777
+ var EF_VERSION = "1.13.1", CEREFOX_VERSION = "1.13.1", EF_LAST_CHANGED = "1.13.1";
25778
25778
  var init_ef_meta = () => {};
25779
25779
 
25780
25780
  // ../../_shared/compatibility/index.ts
@@ -54746,6 +54746,27 @@ var require_underline = __commonJS(function(exports2) {
54746
54746
  }
54747
54747
  });
54748
54748
 
54749
+ // ../../_shared/mcp-tools/identity.ts
54750
+ function callerIdentity(args) {
54751
+ for (const key of ["author", "requestor"]) {
54752
+ const v = args[key];
54753
+ if (typeof v === "string" && v.trim() !== "")
54754
+ return v;
54755
+ }
54756
+ return;
54757
+ }
54758
+ var DEFAULT_IDENTITY = "mcp-agent", EXAMPLE = 'e.g. "Claude Code", "archiver"', AUTHOR_PARAM_WRITE, AUTHOR_PARAM_READ;
54759
+ var init_identity = __esm(() => {
54760
+ AUTHOR_PARAM_WRITE = {
54761
+ type: "string",
54762
+ description: `Your name (agent or user), ${EXAMPLE}. Recorded as the author in the audit log and ` + `in the usage log. Defaults to "${DEFAULT_IDENTITY}" if not provided. May be enforced ` + "via server config."
54763
+ };
54764
+ AUTHOR_PARAM_READ = {
54765
+ type: "string",
54766
+ description: `Your name (agent or user), ${EXAMPLE}. Recorded in the usage log. Defaults to ` + `"${DEFAULT_IDENTITY}" if not provided. May be enforced via server config.`
54767
+ };
54768
+ });
54769
+
54749
54770
  // ../../_shared/mcp-tools/audit-log.ts
54750
54771
  function utcStamp2(iso) {
54751
54772
  const trimmed = iso.slice(0, 19);
@@ -54755,8 +54776,8 @@ async function handler(supabase, args, ctx) {
54755
54776
  const params = {};
54756
54777
  if (args.document_id)
54757
54778
  params.p_document_id = args.document_id;
54758
- if (args.author)
54759
- params.p_author = args.author;
54779
+ if (args.by_author)
54780
+ params.p_author = args.by_author;
54760
54781
  if (args.operation)
54761
54782
  params.p_operation = args.operation;
54762
54783
  if (args.since)
@@ -54772,7 +54793,7 @@ async function handler(supabase, args, ctx) {
54772
54793
  logUsage(supabase, {
54773
54794
  operation: "get_audit_log",
54774
54795
  accessPath: ctx.accessPath,
54775
- requestor: args.requestor,
54796
+ requestor: callerIdentity(args),
54776
54797
  result_count: entries.length
54777
54798
  });
54778
54799
  if (!entries.length)
@@ -54790,6 +54811,7 @@ ${lines.join(`
54790
54811
  var auditLogTool;
54791
54812
  var init_audit_log = __esm(() => {
54792
54813
  init__utils();
54814
+ init_identity();
54793
54815
  auditLogTool = {
54794
54816
  name: "cerefox_get_audit_log",
54795
54817
  description: "Retrieve audit log entries showing who changed what and when. Supports filtering by document, author, operation type, and time range. Returns entries with document titles, author attribution, size changes, and descriptions.",
@@ -54804,7 +54826,10 @@ var init_audit_log = __esm(() => {
54804
54826
  required: [],
54805
54827
  properties: {
54806
54828
  document_id: { type: "string", description: "Filter by document UUID (optional)" },
54807
- author: { type: "string", description: "Filter by author name (optional)" },
54829
+ by_author: {
54830
+ type: "string",
54831
+ description: "Filter: only entries written by this author name (optional)"
54832
+ },
54808
54833
  operation: {
54809
54834
  type: "string",
54810
54835
  description: `Filter by operation type: ${AUDIT_OPERATIONS.join(", ")} (optional)`
@@ -54817,10 +54842,7 @@ var init_audit_log = __esm(() => {
54817
54842
  type: "integer",
54818
54843
  description: "Maximum number of entries to return (default: 50, max: 200)"
54819
54844
  },
54820
- requestor: {
54821
- type: "string",
54822
- description: 'Name of the agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent" if not provided. May be enforced via server config.'
54823
- }
54845
+ author: AUTHOR_PARAM_READ
54824
54846
  }
54825
54847
  },
54826
54848
  handler
@@ -54855,7 +54877,7 @@ async function handler2(supabase, args, ctx) {
54855
54877
  if (!expected_content_hash) {
54856
54878
  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.");
54857
54879
  }
54858
- const author = args.author ?? args.requestor;
54880
+ const author = callerIdentity(args);
54859
54881
  const authorType = ctx.accessPath === "cli" ? "user" : "agent";
54860
54882
  const { data, error } = await supabase.rpc("cerefox_delete_document", {
54861
54883
  p_document_id: document_id,
@@ -54887,7 +54909,7 @@ async function handler2(supabase, args, ctx) {
54887
54909
  logUsage(supabase, {
54888
54910
  operation: "delete",
54889
54911
  accessPath: ctx.accessPath,
54890
- requestor: args.requestor,
54912
+ requestor: author,
54891
54913
  document_id,
54892
54914
  result_count: 1
54893
54915
  });
@@ -54899,6 +54921,7 @@ var deleteDocumentTool;
54899
54921
  var init_delete_document = __esm(() => {
54900
54922
  init__utils();
54901
54923
  init_types3();
54924
+ init_identity();
54902
54925
  deleteDocumentTool = {
54903
54926
  name: "cerefox_delete_document",
54904
54927
  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.",
@@ -54922,14 +54945,7 @@ var init_delete_document = __esm(() => {
54922
54945
  type: "string",
54923
54946
  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."
54924
54947
  },
54925
- author: {
54926
- type: "string",
54927
- description: "Who is making this change. Recorded in the audit log."
54928
- },
54929
- requestor: {
54930
- type: "string",
54931
- description: "Name of the agent or user making this request. Recorded in the usage log."
54932
- }
54948
+ author: AUTHOR_PARAM_WRITE
54933
54949
  }
54934
54950
  },
54935
54951
  handler: handler2
@@ -54942,7 +54958,7 @@ async function handler3(supabase, args, ctx) {
54942
54958
  const reason = args.reason;
54943
54959
  if (!document_id)
54944
54960
  throw new McpInvalidParams("document_id is required");
54945
- const author = args.author ?? args.requestor;
54961
+ const author = callerIdentity(args);
54946
54962
  const authorType = ctx.accessPath === "cli" ? "user" : "agent";
54947
54963
  const { data, error } = await supabase.rpc("cerefox_restore_document", {
54948
54964
  p_document_id: document_id,
@@ -54969,7 +54985,7 @@ async function handler3(supabase, args, ctx) {
54969
54985
  logUsage(supabase, {
54970
54986
  operation: "restore",
54971
54987
  accessPath: ctx.accessPath,
54972
- requestor: args.requestor,
54988
+ requestor: author,
54973
54989
  document_id,
54974
54990
  result_count: 1
54975
54991
  });
@@ -54980,6 +54996,7 @@ var restoreDocumentTool;
54980
54996
  var init_restore_document = __esm(() => {
54981
54997
  init__utils();
54982
54998
  init_types3();
54999
+ init_identity();
54983
55000
  restoreDocumentTool = {
54984
55001
  name: "cerefox_restore_document",
54985
55002
  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.",
@@ -54999,14 +55016,7 @@ var init_restore_document = __esm(() => {
54999
55016
  type: "string",
55000
55017
  description: "Why this document is being restored. Recorded in the audit-log entry. Short and specific beats long."
55001
55018
  },
55002
- author: {
55003
- type: "string",
55004
- description: "Who is making this change. Recorded in the audit log."
55005
- },
55006
- requestor: {
55007
- type: "string",
55008
- description: "Name of the agent or user making this request. Recorded in the usage log."
55009
- }
55019
+ author: AUTHOR_PARAM_WRITE
55010
55020
  }
55011
55021
  },
55012
55022
  handler: handler3
@@ -55070,7 +55080,7 @@ async function setHandler(supabase, args, ctx) {
55070
55080
  p_source_id: source,
55071
55081
  p_target_id: target,
55072
55082
  p_rel_type: relType,
55073
- p_author: args.author ?? "mcp-agent",
55083
+ p_author: callerIdentity(args) ?? DEFAULT_IDENTITY,
55074
55084
  p_author_type: "agent",
55075
55085
  p_metadata: args.metadata ?? {}
55076
55086
  });
@@ -55080,7 +55090,7 @@ async function setHandler(supabase, args, ctx) {
55080
55090
  logUsage(supabase, {
55081
55091
  operation: "set_relation",
55082
55092
  accessPath: ctx.accessPath,
55083
- requestor: args.requestor,
55093
+ requestor: callerIdentity(args),
55084
55094
  document_id: source
55085
55095
  });
55086
55096
  const both = row?.is_symmetric ? " (symmetric — the reverse edge was written too)" : "";
@@ -55099,7 +55109,7 @@ async function deleteHandler(supabase, args, ctx) {
55099
55109
  p_source_id: source,
55100
55110
  p_target_id: target,
55101
55111
  p_rel_type: relType,
55102
- p_author: args.author ?? "mcp-agent",
55112
+ p_author: callerIdentity(args) ?? DEFAULT_IDENTITY,
55103
55113
  p_author_type: "agent"
55104
55114
  });
55105
55115
  if (error)
@@ -55108,7 +55118,7 @@ async function deleteHandler(supabase, args, ctx) {
55108
55118
  logUsage(supabase, {
55109
55119
  operation: "delete_relation",
55110
55120
  accessPath: ctx.accessPath,
55111
- requestor: args.requestor,
55121
+ requestor: callerIdentity(args),
55112
55122
  document_id: source
55113
55123
  });
55114
55124
  if (removed === 0)
@@ -55126,7 +55136,7 @@ async function getRelationsHandler(supabase, args, ctx) {
55126
55136
  logUsage(supabase, {
55127
55137
  operation: "get_relations",
55128
55138
  accessPath: ctx.accessPath,
55129
- requestor: args.requestor,
55139
+ requestor: callerIdentity(args),
55130
55140
  document_id: docId,
55131
55141
  result_count: rows.length
55132
55142
  });
@@ -55161,7 +55171,7 @@ async function getNeighborsHandler(supabase, args, ctx) {
55161
55171
  logUsage(supabase, {
55162
55172
  operation: "get_neighbors",
55163
55173
  accessPath: ctx.accessPath,
55164
- requestor: args.requestor,
55174
+ requestor: callerIdentity(args),
55165
55175
  document_id: docId,
55166
55176
  result_count: rows.length
55167
55177
  });
@@ -55179,6 +55189,7 @@ var KNOWN_TYPES = "related_to, references, supersedes, contradicts, duplicates,
55179
55189
  var init_relations = __esm(() => {
55180
55190
  init__utils();
55181
55191
  init_types3();
55192
+ init_identity();
55182
55193
  setRelationTool = {
55183
55194
  name: "cerefox_set_relation",
55184
55195
  description: "Link two documents with a typed, directed relation (source → target). " + `Known types with behaviour: ${KNOWN_TYPES}. Symmetric types (related_to, ` + "contradicts, duplicates) write both directions. `supersedes` marks the target " + "superseded; `contradicts` marks both stale. Any other type string is accepted " + "and stored, just without special behaviour. Re-setting the same edge updates it.",
@@ -55203,8 +55214,7 @@ var init_relations = __esm(() => {
55203
55214
  type: "object",
55204
55215
  description: "Optional JSON context for the edge (note, confidence, …)"
55205
55216
  },
55206
- author: { type: "string", description: "Who is creating this relation" },
55207
- requestor: { type: "string", description: "Name of the agent making this request" }
55217
+ author: AUTHOR_PARAM_WRITE
55208
55218
  }
55209
55219
  },
55210
55220
  handler: setHandler
@@ -55226,8 +55236,7 @@ var init_relations = __esm(() => {
55226
55236
  source_id: { type: "string", description: "UUID of the source document" },
55227
55237
  target_id: { type: "string", description: "UUID of the target document" },
55228
55238
  rel_type: { type: "string", description: "Relation type to remove" },
55229
- author: { type: "string", description: "Who is removing this relation" },
55230
- requestor: { type: "string", description: "Name of the agent making this request" }
55239
+ author: AUTHOR_PARAM_WRITE
55231
55240
  }
55232
55241
  },
55233
55242
  handler: deleteHandler
@@ -55246,7 +55255,7 @@ var init_relations = __esm(() => {
55246
55255
  required: ["document_id"],
55247
55256
  properties: {
55248
55257
  document_id: { type: "string", description: "UUID of the document" },
55249
- requestor: { type: "string", description: "Name of the agent making this request" }
55258
+ author: AUTHOR_PARAM_READ
55250
55259
  }
55251
55260
  },
55252
55261
  handler: getRelationsHandler
@@ -55270,7 +55279,7 @@ var init_relations = __esm(() => {
55270
55279
  from_time: { type: "string", description: "ISO-8601: only neighbours created on/after" },
55271
55280
  to_time: { type: "string", description: "ISO-8601: only neighbours created on/before" },
55272
55281
  limit: { type: "integer", description: "Max documents to return (default 50, max 200)" },
55273
- requestor: { type: "string", description: "Name of the agent making this request" }
55282
+ author: AUTHOR_PARAM_READ
55274
55283
  }
55275
55284
  },
55276
55285
  handler: getNeighborsHandler
@@ -55304,7 +55313,7 @@ async function handler4(supabase, args, ctx) {
55304
55313
  logUsage(supabase, {
55305
55314
  operation: "get_document",
55306
55315
  accessPath: ctx.accessPath,
55307
- requestor: args.requestor,
55316
+ requestor: callerIdentity(args),
55308
55317
  document_id,
55309
55318
  result_count: 1
55310
55319
  });
@@ -55355,6 +55364,7 @@ var init_get_document = __esm(() => {
55355
55364
  init_partial_edits();
55356
55365
  init__utils();
55357
55366
  init_types3();
55367
+ init_identity();
55358
55368
  getDocumentTool = {
55359
55369
  name: "cerefox_get_document",
55360
55370
  description: `Retrieve a document at one of three zoom levels: the whole reconstructed content (default), its heading structure with outline=true (much cheaper, and the headings are the anchors the edit tools take), or one section's text with section="## Heading" (what a replace_section on that anchor would overwrite — read it before replacing a section you did not write). Pass version_id to retrieve an archived version; omit it (or pass null) for the current version. Version UUIDs are returned by cerefox_list_versions. Every non-archived response carries the document's current content_hash — pass it back as expected_content_hash when updating (optimistic concurrency).`,
@@ -55386,10 +55396,7 @@ var init_get_document = __esm(() => {
55386
55396
  enum: ["own_body", "subtree"],
55387
55397
  description: "Only when the target section HAS CHILD SECTIONS, and it means the same here as on the edit tools: own_body = up to the first child, subtree = everything nested underneath. The read refuses without it for exactly the cases the write refuses, so that what you read is what you would replace. Omit it otherwise; you will be told (with both options) whenever it is needed."
55388
55398
  },
55389
- requestor: {
55390
- type: "string",
55391
- description: 'Name of the agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent" if not provided. May be enforced via server config.'
55392
- }
55399
+ author: AUTHOR_PARAM_READ
55393
55400
  }
55394
55401
  },
55395
55402
  handler: handler4
@@ -55588,7 +55595,7 @@ async function insertHandler(supabase, args, ctx) {
55588
55595
  documentId,
55589
55596
  operations,
55590
55597
  expectedHash,
55591
- requestor: args.requestor ?? defaultRequestor(ctx),
55598
+ requestor: callerIdentity(args) ?? defaultRequestor(ctx),
55592
55599
  toolLabel: "insert",
55593
55600
  authorType: resolveAuthorType2(ctx, args)
55594
55601
  });
@@ -55611,7 +55618,7 @@ async function editHandler(supabase, args, ctx) {
55611
55618
  documentId,
55612
55619
  operations,
55613
55620
  expectedHash,
55614
- requestor: args.requestor ?? defaultRequestor(ctx),
55621
+ requestor: callerIdentity(args) ?? defaultRequestor(ctx),
55615
55622
  toolLabel: "edit",
55616
55623
  authorType: resolveAuthorType2(ctx, args)
55617
55624
  });
@@ -55622,6 +55629,7 @@ var init_partial_edits2 = __esm(() => {
55622
55629
  init__chunker();
55623
55630
  init__utils();
55624
55631
  init_types3();
55632
+ init_identity();
55625
55633
  AUDIT_OP = {
55626
55634
  insert: "insert",
55627
55635
  replace_section: "replace-section",
@@ -55662,10 +55670,7 @@ var init_partial_edits2 = __esm(() => {
55662
55670
  type: "string",
55663
55671
  description: "content_hash of the version you are basing this on. Required — no last-write-wins."
55664
55672
  },
55665
- requestor: {
55666
- type: "string",
55667
- description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
55668
- },
55673
+ author: AUTHOR_PARAM_WRITE,
55669
55674
  author_type: {
55670
55675
  type: "string",
55671
55676
  enum: ["user", "agent"],
@@ -55733,10 +55738,7 @@ var init_partial_edits2 = __esm(() => {
55733
55738
  type: "string",
55734
55739
  description: "content_hash of the version you are basing these edits on. Required — no last-write-wins."
55735
55740
  },
55736
- requestor: {
55737
- type: "string",
55738
- description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
55739
- },
55741
+ author: AUTHOR_PARAM_WRITE,
55740
55742
  author_type: {
55741
55743
  type: "string",
55742
55744
  enum: ["user", "agent"],
@@ -55749,12 +55751,12 @@ var init_partial_edits2 = __esm(() => {
55749
55751
  });
55750
55752
 
55751
55753
  // ../../_shared/mcp-tools/get-help-content.ts
55752
- var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **15 core MCP tools** (14 with CLI equivalents — `cerefox_get_help` is MCP-only), plus 4 dormant relation tools that appear only when `relations_enabled` is on. 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- Long inline bodies can arrive with literal `\\n`/`\\"` (the author over-escaped; Cerefox stores bytes faithfully). For long or quote-dense content, ingest from a file or build incrementally with `cerefox_insert`; read back multi-line writes.\n', HELP_SECTIONS, HELP_SECTION_HEADINGS;
55754
+ var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **15 core MCP tools** (14 with CLI equivalents — `cerefox_get_help` is MCP-only), plus 4 dormant relation tools that appear only when `relations_enabled` is on. 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`, `author` |\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`, `author` |\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), `author` |\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`, `author` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required), `author` |\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`, `author` |\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), `author` |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `by_author` (filter), `operation`, `since`, `author` |\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`** to your name on every call, reads and writes alike (e.g., "Claude Code", "archiver"). Same parameter on every tool. (`requestor` is still accepted everywhere as the pre-1.13.1 alias.) On CLI, pass `--author`/`--author-type` on writes and `--requestor` on reads, 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- Long inline bodies can arrive with literal `\\n`/`\\"` (the author over-escaped; Cerefox stores bytes faithfully). For long or quote-dense content, ingest from a file or build incrementally with `cerefox_insert`; read back multi-line writes.\n', HELP_SECTIONS, HELP_SECTION_HEADINGS;
55753
55755
  var init_get_help_content = __esm(() => {
55754
55756
  HELP_SECTIONS = {
55755
- 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.",
55757
+ Tools: "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `author` |\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`, `author` |\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), `author` |\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`, `author` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required), `author` |\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`, `author` |\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), `author` |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `by_author` (filter), `operation`, `since`, `author` |\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.",
55756
55758
  "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.',
55757
- "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`.',
55759
+ "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`** to your name on every call, reads and writes alike (e.g., "Claude Code", "archiver"). Same parameter on every tool. (`requestor` is still accepted everywhere as the pre-1.13.1 alias.) On CLI, pass `--author`/`--author-type` on writes and `--requestor` on reads, 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`.',
55758
55760
  "Update Workflow (ID-based -- preferred)": `## Update Workflow (ID-based -- preferred)
55759
55761
 
55760
55762
  \`\`\`
@@ -55885,7 +55887,7 @@ async function handler5(supabase, args, ctx) {
55885
55887
  logUsage(supabase, {
55886
55888
  operation: "get_help",
55887
55889
  accessPath: ctx.accessPath,
55888
- requestor: args.requestor,
55890
+ requestor: callerIdentity(args),
55889
55891
  query_text: topic ?? null,
55890
55892
  result_count: 1
55891
55893
  });
@@ -55937,6 +55939,7 @@ var init_get_help = __esm(() => {
55937
55939
  init_partial_edits2();
55938
55940
  init__utils();
55939
55941
  init_get_help_content();
55942
+ init_identity();
55940
55943
  getHelpTool = {
55941
55944
  name: "cerefox_get_help",
55942
55945
  description: "Retrieve Cerefox's own agent-usage guidance (the AGENT_QUICK_REFERENCE.md content). Call with no arguments to get the full reference + a section index. Call with `topic` to get a single section (case-insensitive substring match against H2 headings). Use this whenever you're uncertain about Cerefox conventions — link forms, project-membership semantics, update workflows, etc.",
@@ -55953,10 +55956,7 @@ var init_get_help = __esm(() => {
55953
55956
  type: "string",
55954
55957
  description: 'Optional. Case-insensitive substring against H2 headings (e.g. "links", "update", "metadata"). Omit for the full reference.'
55955
55958
  },
55956
- requestor: {
55957
- type: "string",
55958
- description: 'Name of the agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
55959
- }
55959
+ author: AUTHOR_PARAM_READ
55960
55960
  }
55961
55961
  },
55962
55962
  handler: handler5
@@ -55998,7 +55998,7 @@ async function handler6(supabase, args, ctx) {
55998
55998
  throw new McpInvalidParams('metadata must be a JSON object of key/value pairs, e.g. {"type":"note"} — not a string, number, or array');
55999
55999
  }
56000
56000
  const update_if_exists = args.update_if_exists ?? false;
56001
- const author = args.author ?? "mcp-agent";
56001
+ const author = callerIdentity(args) ?? DEFAULT_IDENTITY;
56002
56002
  const author_type = "agent";
56003
56003
  const expected_content_hash = args.expected_content_hash?.trim() || null;
56004
56004
  const last_write_wins = args.last_write_wins ?? false;
@@ -56198,6 +56198,7 @@ var init_ingest = __esm(() => {
56198
56198
  init__projects();
56199
56199
  init__utils();
56200
56200
  init_types3();
56201
+ init_identity();
56201
56202
  ingestTool = {
56202
56203
  name: "cerefox_ingest",
56203
56204
  description: "Save a note or document to the Cerefox knowledge base. Updating an existing document REPLACES its whole content, so every unchanged character has to be reproduced exactly. Prefer cerefox_insert / cerefox_edit for any change to part of a document, and especially where the untouched content cannot be checked by reading it — document IDs, hashes, numeric tables, indexes and registries. Drifting prose is obvious on review; one wrong character in a UUID is not, and it silently breaks the reference.",
@@ -56241,10 +56242,7 @@ var init_ingest = __esm(() => {
56241
56242
  description: "Explicitly skip the concurrency check and overwrite regardless of concurrent changes (default: false). Use ONLY when an external source of truth makes conflicts meaningless (e.g. re-syncing from files). Recorded in the audit log."
56242
56243
  },
56243
56244
  metadata: { type: "object", description: "Arbitrary JSON metadata (optional)" },
56244
- author: {
56245
- type: "string",
56246
- description: 'Name of the agent or tool performing the ingestion (e.g., "Claude Code", "archiver"). Recorded in the audit log for attribution. Defaults to "mcp-agent" if not provided. May be enforced via server config.'
56247
- }
56245
+ author: AUTHOR_PARAM_WRITE
56248
56246
  }
56249
56247
  },
56250
56248
  handler: handler6
@@ -56260,7 +56258,7 @@ async function handler7(supabase, args, ctx) {
56260
56258
  logUsage(supabase, {
56261
56259
  operation: "list_metadata_keys",
56262
56260
  accessPath: ctx.accessPath,
56263
- requestor: args.requestor,
56261
+ requestor: callerIdentity(args),
56264
56262
  result_count: keys.length
56265
56263
  });
56266
56264
  if (keys.length === 0)
@@ -56270,6 +56268,7 @@ async function handler7(supabase, args, ctx) {
56270
56268
  var listMetadataKeysTool;
56271
56269
  var init_list_metadata_keys = __esm(() => {
56272
56270
  init__utils();
56271
+ init_identity();
56273
56272
  listMetadataKeysTool = {
56274
56273
  name: "cerefox_list_metadata_keys",
56275
56274
  description: "List all metadata keys currently in use across documents in the Cerefox knowledge base. Returns each key with its document count and up to 5 example values.",
@@ -56282,10 +56281,7 @@ var init_list_metadata_keys = __esm(() => {
56282
56281
  inputSchema: {
56283
56282
  type: "object",
56284
56283
  properties: {
56285
- requestor: {
56286
- type: "string",
56287
- description: 'Name of the agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent" if not provided. May be enforced via server config.'
56288
- }
56284
+ author: AUTHOR_PARAM_READ
56289
56285
  }
56290
56286
  },
56291
56287
  handler: handler7
@@ -56301,7 +56297,7 @@ async function handler8(supabase, args, ctx) {
56301
56297
  logUsage(supabase, {
56302
56298
  operation: "list_projects",
56303
56299
  accessPath: ctx.accessPath,
56304
- requestor: args.requestor,
56300
+ requestor: callerIdentity(args),
56305
56301
  result_count: projects.length
56306
56302
  });
56307
56303
  if (projects.length === 0)
@@ -56318,6 +56314,7 @@ ${lines.join(`
56318
56314
  var listProjectsTool;
56319
56315
  var init_list_projects = __esm(() => {
56320
56316
  init__utils();
56317
+ init_identity();
56321
56318
  listProjectsTool = {
56322
56319
  name: "cerefox_list_projects",
56323
56320
  description: "List all projects with their names and IDs. Use this to discover available projects before filtering by project_name in other tools.",
@@ -56330,10 +56327,7 @@ var init_list_projects = __esm(() => {
56330
56327
  inputSchema: {
56331
56328
  type: "object",
56332
56329
  properties: {
56333
- requestor: {
56334
- type: "string",
56335
- description: 'Name of the agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent" if not provided. May be enforced via server config.'
56336
- }
56330
+ author: AUTHOR_PARAM_READ
56337
56331
  }
56338
56332
  },
56339
56333
  handler: handler8
@@ -56358,7 +56352,7 @@ async function handler9(supabase, args, ctx) {
56358
56352
  logUsage(supabase, {
56359
56353
  operation: "list_versions",
56360
56354
  accessPath: ctx.accessPath,
56361
- requestor: args.requestor,
56355
+ requestor: callerIdentity(args),
56362
56356
  document_id,
56363
56357
  result_count: versions.length
56364
56358
  });
@@ -56374,6 +56368,7 @@ var listVersionsTool;
56374
56368
  var init_list_versions = __esm(() => {
56375
56369
  init__utils();
56376
56370
  init_types3();
56371
+ init_identity();
56377
56372
  listVersionsTool = {
56378
56373
  name: "cerefox_list_versions",
56379
56374
  description: "List all archived versions of a document, newest first. Returns version_id (use with cerefox_get_document), version_number, source, chunk_count, total_chars, and created_at.",
@@ -56391,10 +56386,7 @@ var init_list_versions = __esm(() => {
56391
56386
  type: "string",
56392
56387
  description: "UUID of the document whose version history to list"
56393
56388
  },
56394
- requestor: {
56395
- type: "string",
56396
- description: 'Name of the agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent" if not provided. May be enforced via server config.'
56397
- }
56389
+ author: AUTHOR_PARAM_READ
56398
56390
  }
56399
56391
  },
56400
56392
  handler: handler9
@@ -56442,7 +56434,7 @@ async function handler10(supabase, args, ctx) {
56442
56434
  logUsage(supabase, {
56443
56435
  operation: "metadata_search",
56444
56436
  accessPath: ctx.accessPath,
56445
- requestor: args.requestor,
56437
+ requestor: callerIdentity(args),
56446
56438
  query_text: JSON.stringify(metadata_filter ?? {}),
56447
56439
  project_id: projectId,
56448
56440
  result_count: rows.length
@@ -56476,6 +56468,7 @@ var init_metadata_search = __esm(() => {
56476
56468
  init__projects();
56477
56469
  init_feature_flags();
56478
56470
  init_types3();
56471
+ init_identity();
56479
56472
  metadataSearchTool = {
56480
56473
  name: "cerefox_metadata_search",
56481
56474
  description: "Find or list documents by metadata key-value criteria without a text search term. Use to discover documents tagged with specific attributes, browse by taxonomy, retrieve messages/tasks by type and status, or list all documents in a project (pass project_name alone). At least one of metadata_filter, project_name, updated_since, or created_since must be supplied; results are ordered newest-updated first.",
@@ -56511,10 +56504,7 @@ var init_metadata_search = __esm(() => {
56511
56504
  type: "integer",
56512
56505
  description: "Soft cap on total response bytes when include_content is true. Defaults to server maximum (200000)."
56513
56506
  },
56514
- requestor: {
56515
- type: "string",
56516
- description: 'Name of the agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent" if not provided. May be enforced via server config.'
56517
- }
56507
+ author: AUTHOR_PARAM_READ
56518
56508
  }
56519
56509
  },
56520
56510
  handler: handler10
@@ -56600,7 +56590,7 @@ async function handler11(supabase, args, ctx) {
56600
56590
  logUsage(supabase, {
56601
56591
  operation: "search",
56602
56592
  accessPath: ctx.accessPath,
56603
- requestor: args.requestor,
56593
+ requestor: callerIdentity(args),
56604
56594
  query_text: query,
56605
56595
  project_id: projectId,
56606
56596
  result_count: accepted.length
@@ -56643,6 +56633,7 @@ var init_search = __esm(() => {
56643
56633
  init__utils();
56644
56634
  init__projects();
56645
56635
  init_types3();
56636
+ init_identity();
56646
56637
  searchTool = {
56647
56638
  name: "cerefox_search",
56648
56639
  description: "Search the Cerefox personal knowledge base. Returns complete documents ranked by hybrid (FTS + semantic) relevance.",
@@ -56691,10 +56682,7 @@ var init_search = __esm(() => {
56691
56682
  type: "integer",
56692
56683
  description: "Optional response size budget in bytes. Results are dropped whole until the budget is satisfied; a truncated flag is set when results are dropped. Defaults to the server maximum (200000). Pass a smaller value if your context window is limited. Values above the server maximum are silently capped."
56693
56684
  },
56694
- requestor: {
56695
- type: "string",
56696
- description: 'Name of the agent or user making this request (e.g., "Claude Code", "archiver"). Recorded in the usage log for attribution. Defaults to "mcp-agent" if not provided. May be enforced via server config.'
56697
- }
56685
+ author: AUTHOR_PARAM_READ
56698
56686
  }
56699
56687
  },
56700
56688
  handler: handler11
@@ -56717,7 +56705,7 @@ async function handler12(supabase, args, ctx) {
56717
56705
  if (Object.keys(metadata).length === 0 && !replace) {
56718
56706
  throw new McpInvalidParams("metadata is empty, which would change nothing. To clear all metadata pass replace: true with {}.");
56719
56707
  }
56720
- const author = args.author ?? args.requestor;
56708
+ const author = callerIdentity(args);
56721
56709
  const authorType = ctx.accessPath === "cli" ? "user" : "agent";
56722
56710
  const { data, error } = await supabase.rpc("cerefox_set_document_metadata", {
56723
56711
  p_document_id: document_id,
@@ -56734,7 +56722,7 @@ async function handler12(supabase, args, ctx) {
56734
56722
  logUsage(supabase, {
56735
56723
  operation: "update_metadata",
56736
56724
  accessPath: ctx.accessPath,
56737
- requestor: args.requestor,
56725
+ requestor: author,
56738
56726
  document_id,
56739
56727
  result_count: 1
56740
56728
  });
@@ -56749,6 +56737,7 @@ var setDocumentMetadataTool;
56749
56737
  var init_set_document_metadata = __esm(() => {
56750
56738
  init__utils();
56751
56739
  init_types3();
56740
+ init_identity();
56752
56741
  setDocumentMetadataTool = {
56753
56742
  name: "cerefox_set_document_metadata",
56754
56743
  description: `Change a document's metadata WITHOUT resending its content. MERGES by default: the keys you pass are set, every other key is left alone — so you do not need to read the document first, and you cannot accidentally drop tags another agent set. To REMOVE a key, pass it with a null value ({"stale_key": null}). Pass replace: true to set the metadata to exactly the object given, discarding everything else (rare; the same destructive contract as cerefox_set_document_projects). Content, chunks and embeddings are untouched and no new version is created. Use this instead of cerefox_ingest whenever only the tags are changing.`,
@@ -56772,14 +56761,7 @@ var init_set_document_metadata = __esm(() => {
56772
56761
  type: "boolean",
56773
56762
  description: "Set the metadata to EXACTLY this object, discarding any key not listed. Defaults to false (merge). Use only when you mean to reset a document's tags wholesale."
56774
56763
  },
56775
- author: {
56776
- type: "string",
56777
- description: "Who is making this change. Recorded in the audit log."
56778
- },
56779
- requestor: {
56780
- type: "string",
56781
- description: "Name of the agent or user making this request. Recorded in the usage log."
56782
- }
56764
+ author: AUTHOR_PARAM_WRITE
56783
56765
  }
56784
56766
  },
56785
56767
  handler: handler12
@@ -56790,7 +56772,7 @@ var init_set_document_metadata = __esm(() => {
56790
56772
  async function handler13(supabase, args, ctx) {
56791
56773
  const document_id = args.document_id?.trim();
56792
56774
  const project_names_raw = args.project_names;
56793
- const author = args.author ?? "mcp-agent";
56775
+ const author = callerIdentity(args) ?? DEFAULT_IDENTITY;
56794
56776
  if (!document_id) {
56795
56777
  throw new McpInvalidParams("Missing required argument: document_id (UUID from a prior cerefox_search result).");
56796
56778
  }
@@ -56819,6 +56801,7 @@ var setDocumentProjectsTool;
56819
56801
  var init_set_document_projects = __esm(() => {
56820
56802
  init__projects();
56821
56803
  init_types3();
56804
+ init_identity();
56822
56805
  setDocumentProjectsTool = {
56823
56806
  name: "cerefox_set_document_projects",
56824
56807
  description: "Set the document's project memberships to EXACTLY the given list. Destructive replace: any existing memberships not in this list are removed. Pass an empty list to clear all project memberships. Projects are looked up by name (case-insensitive); missing projects are created. Logged as update-metadata in the audit log — content is untouched. Use cerefox_ingest with project_names if you want to set memberships AND update content in one call. Use this tool when you only need to change project membership without re-writing the document body.",
@@ -56842,10 +56825,7 @@ var init_set_document_projects = __esm(() => {
56842
56825
  items: { type: "string" },
56843
56826
  description: "Explicit list of project names. Each created if absent. Order is preserved. Empty list = remove from all projects."
56844
56827
  },
56845
- author: {
56846
- type: "string",
56847
- description: 'Agent or tool name recorded in the audit log. Defaults to "mcp-agent". May be enforced via server config.'
56848
- }
56828
+ author: AUTHOR_PARAM_WRITE
56849
56829
  }
56850
56830
  },
56851
56831
  handler: handler13
@@ -71580,12 +71560,12 @@ var CONFIG_CATALOG = [
71580
71560
  },
71581
71561
  {
71582
71562
  key: "review_workflow_enabled",
71583
- description: "Queue agent-authored writes as 'pending review' for a person to approve. Off: every write lands approved and no surface shows a review status. Off on a fresh install; on for stores that predate the flag.",
71563
+ description: "Show and enforce the review status of documents: agent-authored writes are marked 'pending review' for a person to approve. Off: no surface shows a review status and nothing enforces it; the status is still recorded and reappears unchanged when turned back on. Off on a fresh install; on for stores that predate the flag.",
71584
71564
  kind: "boolean",
71585
71565
  defaultValue: "false",
71586
71566
  group: "Governance",
71587
71567
  highImpact: true,
71588
- impactNote: "Turning this OFF hides the review badges, the approve control and the search filter everywhere (web, API, MCP, CLI), and every new write lands approved whoever wrote it. Nothing stored is changed: turning it back ON shows exactly what was there, including documents still marked pending."
71568
+ impactNote: "Turning this OFF hides the review badges, the approve control and the search filter everywhere (web, API, MCP, CLI). It changes nothing stored and nothing about how writes are recorded: agent writes are still marked pending behind the scenes, so turning it back ON shows exactly the statuses the store would have had all along."
71589
71569
  }
71590
71570
  ];
71591
71571
  function configKeySpec(key) {
@@ -77857,7 +77837,7 @@ async function checkReviewWorkflow() {
77857
77837
  return {
77858
77838
  name,
77859
77839
  status: "ok",
77860
- detail: on ? "ON — agent writes land pending_review; review_status is shown everywhere" : "OFF — every write lands approved; review_status is hidden everywhere",
77840
+ detail: on ? "ON — agent writes land pending_review; review_status is shown everywhere" : "OFF — review_status is hidden everywhere (still recorded from author_type)",
77861
77841
  hint: on ? undefined : "Turn it on with `cerefox config set review_workflow_enabled true`."
77862
77842
  };
77863
77843
  }