@cerefox/memory 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +11 -8
- package/AGENT_QUICK_REFERENCE.md +12 -5
- package/dist/bin/cerefox.js +214 -58
- package/dist/server-assets/_shared/ef-meta/index.ts +2 -2
- package/dist/server-assets/_shared/mcp-tools/get-document.ts +67 -3
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +3 -3
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +2 -1
- package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +82 -14
- package/dist/server-assets/_shared/partial-edits/index.ts +173 -13
- package/dist/server-assets/db/migrations/0021_rename_section_audit_op.sql +36 -0
- package/dist/server-assets/db/rpcs.sql +2 -1
- package/dist/server-assets/db/schema.sql +5 -2
- package/docs/guides/configuration.md +1 -1
- package/docs/guides/connect-agents.md +14 -1
- package/package.json +1 -1
package/dist/bin/cerefox.js
CHANGED
|
@@ -7438,7 +7438,7 @@ var exports_meta = {};
|
|
|
7438
7438
|
__export(exports_meta, {
|
|
7439
7439
|
PKG_VERSION: () => PKG_VERSION
|
|
7440
7440
|
});
|
|
7441
|
-
var PKG_VERSION = "1.
|
|
7441
|
+
var PKG_VERSION = "1.4.0";
|
|
7442
7442
|
var init_meta = () => {};
|
|
7443
7443
|
|
|
7444
7444
|
// ../../_shared/config/paths.ts
|
|
@@ -26285,13 +26285,13 @@ function canonicalHeading(text) {
|
|
|
26285
26285
|
const m = text.trim().match(/^(#{1,6})[ \t]+(.*?)[ \t]*#*[ \t]*$/);
|
|
26286
26286
|
return m ? `${m[1]} ${m[2].trim()}`.trim() : text.trim();
|
|
26287
26287
|
}
|
|
26288
|
-
function resolveAnchor(outline, anchorHeading) {
|
|
26288
|
+
function resolveAnchor(outline, anchorHeading, reads = false) {
|
|
26289
26289
|
const anchor = canonicalHeading(anchorHeading);
|
|
26290
26290
|
const byHeading = outline.filter((n) => n.heading === anchor);
|
|
26291
26291
|
if (byHeading.length === 1)
|
|
26292
26292
|
return byHeading[0];
|
|
26293
26293
|
if (byHeading.length > 1) {
|
|
26294
|
-
throw new AmbiguousAnchorError(anchor, byHeading.map((n) => n.path));
|
|
26294
|
+
throw new AmbiguousAnchorError(anchor, byHeading.map((n) => n.path), reads);
|
|
26295
26295
|
}
|
|
26296
26296
|
if (anchor.includes(" > ")) {
|
|
26297
26297
|
const normalizedPath = anchor.split(" > ").map((seg) => canonicalHeading(seg)).join(" > ");
|
|
@@ -26299,10 +26299,24 @@ function resolveAnchor(outline, anchorHeading) {
|
|
|
26299
26299
|
if (byPath.length === 1)
|
|
26300
26300
|
return byPath[0];
|
|
26301
26301
|
if (byPath.length > 1) {
|
|
26302
|
-
throw new AmbiguousAnchorError(anchor, byPath.map((n) => n.path));
|
|
26302
|
+
throw new AmbiguousAnchorError(anchor, byPath.map((n) => n.path), reads);
|
|
26303
26303
|
}
|
|
26304
26304
|
}
|
|
26305
|
-
throw new AnchorNotFoundError(anchor, outline);
|
|
26305
|
+
throw new AnchorNotFoundError(anchor, outline, reads);
|
|
26306
|
+
}
|
|
26307
|
+
function extractSection(content, anchorHeading, sectionPart) {
|
|
26308
|
+
const outline = parseOutline(content);
|
|
26309
|
+
const node = resolveAnchor(outline, anchorHeading, true);
|
|
26310
|
+
const to = resolveSectionEnd(content, outline, node, sectionPart, "the section read", true);
|
|
26311
|
+
const text = content.slice(node.bodyStart, to);
|
|
26312
|
+
return {
|
|
26313
|
+
heading: node.heading,
|
|
26314
|
+
path: node.path,
|
|
26315
|
+
level: node.level,
|
|
26316
|
+
text,
|
|
26317
|
+
chars: text.length,
|
|
26318
|
+
section_part: sectionPart ?? null
|
|
26319
|
+
};
|
|
26306
26320
|
}
|
|
26307
26321
|
function firstChild(outline, node) {
|
|
26308
26322
|
for (const n of outline) {
|
|
@@ -26311,7 +26325,7 @@ function firstChild(outline, node) {
|
|
|
26311
26325
|
}
|
|
26312
26326
|
return null;
|
|
26313
26327
|
}
|
|
26314
|
-
function resolveSectionEnd(content, outline, node, sectionPart, opName) {
|
|
26328
|
+
function resolveSectionEnd(content, outline, node, sectionPart, opName, reads = false) {
|
|
26315
26329
|
const child = firstChild(outline, node);
|
|
26316
26330
|
if (!child)
|
|
26317
26331
|
return node.subtreeEnd;
|
|
@@ -26319,7 +26333,7 @@ function resolveSectionEnd(content, outline, node, sectionPart, opName) {
|
|
|
26319
26333
|
return node.ownBodyEnd;
|
|
26320
26334
|
if (sectionPart === "subtree")
|
|
26321
26335
|
return node.subtreeEnd;
|
|
26322
|
-
throw new AmbiguousPositionError(node, child.heading, opName);
|
|
26336
|
+
throw new AmbiguousPositionError(node, child.heading, opName, reads);
|
|
26323
26337
|
}
|
|
26324
26338
|
function spliceBlock(content, from, to, text) {
|
|
26325
26339
|
const before = content.slice(0, from).replace(/\n+$/, "");
|
|
@@ -26378,7 +26392,30 @@ function applyOne(content, operation) {
|
|
|
26378
26392
|
applied: {
|
|
26379
26393
|
op: "replace_section",
|
|
26380
26394
|
path: node2.path,
|
|
26381
|
-
detail: "replace_section body" + (operation.section_part ? ` (${operation.section_part})` : "")
|
|
26395
|
+
detail: "replace_section body" + (operation.section_part ? ` (${operation.section_part})` : ""),
|
|
26396
|
+
reachedEnd: to2 >= content.trimEnd().length
|
|
26397
|
+
}
|
|
26398
|
+
};
|
|
26399
|
+
}
|
|
26400
|
+
if (operation.op === "rename_section") {
|
|
26401
|
+
const node2 = resolveAnchor(outline, operation.anchor_heading);
|
|
26402
|
+
const next = canonicalHeading(operation.new_heading);
|
|
26403
|
+
const m = next.match(ATX_HEADING);
|
|
26404
|
+
if (!m) {
|
|
26405
|
+
throw new InvalidOperationError(0, `new_heading must be a markdown heading line like "## Title", got ${JSON.stringify(operation.new_heading)}`);
|
|
26406
|
+
}
|
|
26407
|
+
if (m[1].length !== node2.level)
|
|
26408
|
+
throw new HeadingLevelChangeError(node2.heading, next);
|
|
26409
|
+
const hadNewline = content[node2.bodyStart - 1] === `
|
|
26410
|
+
`;
|
|
26411
|
+
const replacement = next + (hadNewline ? `
|
|
26412
|
+
` : "");
|
|
26413
|
+
return {
|
|
26414
|
+
content: content.slice(0, node2.start) + replacement + content.slice(node2.bodyStart),
|
|
26415
|
+
applied: {
|
|
26416
|
+
op: "rename_section",
|
|
26417
|
+
path: node2.path,
|
|
26418
|
+
detail: `rename_section to ${next}`
|
|
26382
26419
|
}
|
|
26383
26420
|
};
|
|
26384
26421
|
}
|
|
@@ -26391,7 +26428,8 @@ function applyOne(content, operation) {
|
|
|
26391
26428
|
applied: {
|
|
26392
26429
|
op: "delete_section",
|
|
26393
26430
|
path: node.path,
|
|
26394
|
-
detail: `delete_section (${scope})` + (operation.section_part ? ` (${operation.section_part})` : "")
|
|
26431
|
+
detail: `delete_section (${scope})` + (operation.section_part ? ` (${operation.section_part})` : ""),
|
|
26432
|
+
reachedEnd: to >= content.trimEnd().length
|
|
26395
26433
|
}
|
|
26396
26434
|
};
|
|
26397
26435
|
}
|
|
@@ -26433,8 +26471,21 @@ function validateOperations(operations) {
|
|
|
26433
26471
|
if (o.scope !== undefined && o.scope !== "body_only" && o.scope !== "heading_and_body") {
|
|
26434
26472
|
throw new InvalidOperationError(i, "scope must be body_only or heading_and_body");
|
|
26435
26473
|
}
|
|
26474
|
+
} else if (op === "rename_section") {
|
|
26475
|
+
if (typeof o.anchor_heading !== "string" || o.anchor_heading.trim() === "") {
|
|
26476
|
+
throw new InvalidOperationError(i, "rename_section requires anchor_heading");
|
|
26477
|
+
}
|
|
26478
|
+
if (typeof o.new_heading !== "string" || o.new_heading.trim() === "") {
|
|
26479
|
+
throw new InvalidOperationError(i, "rename_section requires non-empty new_heading");
|
|
26480
|
+
}
|
|
26481
|
+
if (o.text !== undefined) {
|
|
26482
|
+
throw new InvalidOperationError(i, "rename_section changes only the heading and takes no text — use replace_section for the body, or both operations in one call");
|
|
26483
|
+
}
|
|
26484
|
+
if (o.section_part !== undefined) {
|
|
26485
|
+
throw new InvalidOperationError(i, "rename_section takes no section_part: it replaces the heading line, so no extent is involved");
|
|
26486
|
+
}
|
|
26436
26487
|
} else {
|
|
26437
|
-
throw new InvalidOperationError(i, "op must be insert | replace_section | delete_section");
|
|
26488
|
+
throw new InvalidOperationError(i, "op must be insert | replace_section | delete_section | rename_section");
|
|
26438
26489
|
}
|
|
26439
26490
|
if (o.section_part !== undefined && o.section_part !== "own_body" && o.section_part !== "subtree") {
|
|
26440
26491
|
throw new InvalidOperationError(i, "section_part must be own_body or subtree");
|
|
@@ -26461,22 +26512,22 @@ function applyOperations(content, operations) {
|
|
|
26461
26512
|
}
|
|
26462
26513
|
return { content: current, applied };
|
|
26463
26514
|
}
|
|
26464
|
-
var AnchorNotFoundError, AmbiguousAnchorError, AmbiguousPositionError, InvalidOperationError, ATX_HEADING, FENCE_OPEN;
|
|
26515
|
+
var AnchorNotFoundError, AmbiguousAnchorError, AmbiguousPositionError, HeadingLevelChangeError, InvalidOperationError, ATX_HEADING, FENCE_OPEN;
|
|
26465
26516
|
var init_partial_edits = __esm(() => {
|
|
26466
26517
|
AnchorNotFoundError = class AnchorNotFoundError extends Error {
|
|
26467
|
-
constructor(anchor, outline) {
|
|
26518
|
+
constructor(anchor, outline, reads = false) {
|
|
26468
26519
|
const known = outline.length ? ` Known headings:
|
|
26469
26520
|
${outline.map((n) => ` ${n.path}`).join(`
|
|
26470
26521
|
`)}` : " The document has no headings.";
|
|
26471
|
-
super(`Anchor not found: "${anchor}"
|
|
26522
|
+
super(`Anchor not found: "${anchor}".${reads ? "" : " No write was performed."}${known}
|
|
26472
26523
|
` + `Anchors match a heading line exactly ("## Title") or a parent path ` + `("## Parent > ### Child").`);
|
|
26473
26524
|
this.name = "AnchorNotFoundError";
|
|
26474
26525
|
}
|
|
26475
26526
|
};
|
|
26476
26527
|
AmbiguousAnchorError = class AmbiguousAnchorError extends Error {
|
|
26477
26528
|
candidates;
|
|
26478
|
-
constructor(anchor, candidates) {
|
|
26479
|
-
super(`Ambiguous anchor: "${anchor}" matches ${candidates.length} sections. ` +
|
|
26529
|
+
constructor(anchor, candidates, reads = false) {
|
|
26530
|
+
super(`Ambiguous anchor: "${anchor}" matches ${candidates.length} sections. ` + `${reads ? "" : "No write was performed. "}Disambiguate by passing one of these paths as anchor_heading:
|
|
26480
26531
|
` + candidates.map((c2) => ` ${c2}`).join(`
|
|
26481
26532
|
`));
|
|
26482
26533
|
this.name = "AmbiguousAnchorError";
|
|
@@ -26485,7 +26536,7 @@ ${outline.map((n) => ` ${n.path}`).join(`
|
|
|
26485
26536
|
};
|
|
26486
26537
|
AmbiguousPositionError = class AmbiguousPositionError extends Error {
|
|
26487
26538
|
candidates;
|
|
26488
|
-
constructor(node, firstChildHeading, opName) {
|
|
26539
|
+
constructor(node, firstChildHeading, opName, reads = false) {
|
|
26489
26540
|
const candidates = [
|
|
26490
26541
|
{
|
|
26491
26542
|
section_part: "own_body",
|
|
@@ -26496,13 +26547,21 @@ ${outline.map((n) => ` ${n.path}`).join(`
|
|
|
26496
26547
|
description: `the whole subtree, past everything nested under ${node.heading} — ` + `which can be a long way down`
|
|
26497
26548
|
}
|
|
26498
26549
|
];
|
|
26499
|
-
super(`Ambiguous position: "${node.path}" has child sections, so "the end of ` + `the section" could mean two different places and ${opName} will not guess. ` +
|
|
26550
|
+
super(`Ambiguous position: "${node.path}" has child sections, so "the end of ` + `the section" could mean two different places and ${opName} will not guess. ` + `${reads ? "" : "No write was performed. "}Pass section_part to choose:
|
|
26500
26551
|
` + candidates.map((c2) => ` section_part: "${c2.section_part}" — ${c2.description}`).join(`
|
|
26501
26552
|
`));
|
|
26502
26553
|
this.name = "AmbiguousPositionError";
|
|
26503
26554
|
this.candidates = candidates;
|
|
26504
26555
|
}
|
|
26505
26556
|
};
|
|
26557
|
+
HeadingLevelChangeError = class HeadingLevelChangeError extends Error {
|
|
26558
|
+
constructor(fromHeading, toHeading) {
|
|
26559
|
+
const from = (fromHeading.match(/^#+/) ?? [""])[0].length;
|
|
26560
|
+
const to = (toHeading.match(/^#+/) ?? [""])[0].length;
|
|
26561
|
+
super(`rename_section changes heading TEXT, not depth: "${fromHeading}" is level ` + `${from} and "${toHeading}" is level ${to}. No write was performed. ` + `Changing the level would re-parent every section nested under it. ` + `Pass a level-${from} heading (${"#".repeat(from)} ...), or restructure ` + `explicitly with delete_section + insert if that is what you meant.`);
|
|
26562
|
+
this.name = "HeadingLevelChangeError";
|
|
26563
|
+
}
|
|
26564
|
+
};
|
|
26506
26565
|
InvalidOperationError = class InvalidOperationError extends Error {
|
|
26507
26566
|
constructor(index, message) {
|
|
26508
26567
|
super(`Invalid operation at index ${index}: ${message}. No write was performed.`);
|
|
@@ -54984,8 +55043,16 @@ async function handler2(supabase, args, ctx) {
|
|
|
54984
55043
|
const document_id = args.document_id;
|
|
54985
55044
|
const version_id = args.version_id ?? null;
|
|
54986
55045
|
const outline = args.outline ?? false;
|
|
55046
|
+
const section = (args.section ?? "").trim() || null;
|
|
55047
|
+
const section_part = args.section_part ?? undefined;
|
|
54987
55048
|
if (!document_id)
|
|
54988
55049
|
throw new McpInvalidParams("document_id is required");
|
|
55050
|
+
if (section && outline) {
|
|
55051
|
+
throw new McpInvalidParams("Pass either outline (the whole structure) or section (one section's text), not both.");
|
|
55052
|
+
}
|
|
55053
|
+
if (section_part && !section) {
|
|
55054
|
+
throw new McpInvalidParams("section_part only applies together with section.");
|
|
55055
|
+
}
|
|
54989
55056
|
const { data, error: error2 } = await supabase.rpc("cerefox_get_document", {
|
|
54990
55057
|
p_document_id: document_id,
|
|
54991
55058
|
p_version_id: version_id
|
|
@@ -55017,6 +55084,26 @@ async function handler2(supabase, args, ctx) {
|
|
|
55017
55084
|
note: archived ? "This is an ARCHIVED version's structure, so no content_hash is returned: these anchors describe the old version and must not be used to edit the current one. Re-read without version_id to edit." : nodes.length === 0 ? "This document has no headings, so it has no anchors: only end_of_document inserts apply." : "Use a path as anchor_heading in cerefox_insert / cerefox_edit; content_hash is your expected_content_hash."
|
|
55018
55085
|
}, null, 2);
|
|
55019
55086
|
}
|
|
55087
|
+
if (section) {
|
|
55088
|
+
let extracted;
|
|
55089
|
+
try {
|
|
55090
|
+
extracted = extractSection(row.full_content ?? "", section, section_part);
|
|
55091
|
+
} catch (err) {
|
|
55092
|
+
throw new McpInvalidParams(err instanceof Error ? err.message : String(err));
|
|
55093
|
+
}
|
|
55094
|
+
const archived = version_id !== null;
|
|
55095
|
+
return JSON.stringify({
|
|
55096
|
+
title: row.doc_title ?? "Untitled",
|
|
55097
|
+
heading: extracted.heading,
|
|
55098
|
+
path: extracted.path,
|
|
55099
|
+
level: extracted.level,
|
|
55100
|
+
section_part: extracted.section_part,
|
|
55101
|
+
chars: extracted.chars,
|
|
55102
|
+
content_hash: archived ? null : row.content_hash ?? null,
|
|
55103
|
+
text: extracted.text,
|
|
55104
|
+
note: archived ? "This is an ARCHIVED version's section, so no content_hash is returned: it must not be used to edit the current document." : "This is exactly the text a replace_section on this anchor would overwrite (the heading itself is kept). content_hash is your expected_content_hash."
|
|
55105
|
+
}, null, 2);
|
|
55106
|
+
}
|
|
55020
55107
|
const label = version_id !== null ? " (archived version)" : " (current)";
|
|
55021
55108
|
const hashLine = row.content_hash ? `content_hash: ${row.content_hash}
|
|
55022
55109
|
|
|
@@ -55030,7 +55117,7 @@ var init_get_document = __esm(() => {
|
|
|
55030
55117
|
init_types3();
|
|
55031
55118
|
getDocumentTool = {
|
|
55032
55119
|
name: "cerefox_get_document",
|
|
55033
|
-
description:
|
|
55120
|
+
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).`,
|
|
55034
55121
|
annotations: {
|
|
55035
55122
|
title: "Read document",
|
|
55036
55123
|
readOnlyHint: true,
|
|
@@ -55048,7 +55135,16 @@ var init_get_document = __esm(() => {
|
|
|
55048
55135
|
},
|
|
55049
55136
|
outline: {
|
|
55050
55137
|
type: "boolean",
|
|
55051
|
-
description: "Return the document's STRUCTURE instead of its content: heading paths, levels and per-section sizes, plus content_hash and total size. Far cheaper than a full read
|
|
55138
|
+
description: "Return the document's STRUCTURE instead of its content: heading paths, levels and per-section sizes, plus content_hash and total size. Far cheaper than a full read. Every heading listed is addressable by cerefox_insert / cerefox_edit: pass the bare heading line (e.g. '## Daily Logs') when it occurs once in the document, and only the full ' > ' path shown here when the same heading text repeats. Use this before editing a document you have not read."
|
|
55139
|
+
},
|
|
55140
|
+
section: {
|
|
55141
|
+
type: "string",
|
|
55142
|
+
description: "Return ONE section's text instead of the whole document: the anchor heading, exactly as cerefox_insert / cerefox_edit take it (bare heading line when unique, ' > ' path when it repeats). What comes back is precisely the text a replace_section on this anchor would overwrite, so read it before replacing a section you did not write. The heading itself is returned separately, because replace_section keeps it. Cannot be combined with outline."
|
|
55143
|
+
},
|
|
55144
|
+
section_part: {
|
|
55145
|
+
type: "string",
|
|
55146
|
+
enum: ["own_body", "subtree"],
|
|
55147
|
+
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."
|
|
55052
55148
|
},
|
|
55053
55149
|
requestor: {
|
|
55054
55150
|
type: "string",
|
|
@@ -55061,11 +55157,11 @@ var init_get_document = __esm(() => {
|
|
|
55061
55157
|
});
|
|
55062
55158
|
|
|
55063
55159
|
// ../../_shared/mcp-tools/get-help-content.ts
|
|
55064
|
-
var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **16 MCP tools** (15 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for "How AI Agents Use Cerefox" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash | `document_id` (required), `outline` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project\'s docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc\'s project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.\n\n## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround.\n4. Both require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**A section runs to the next same-or-higher heading, or to the end of the\ndocument.** So `end_of_document` inserts land inside the *last* section, and\nreplacing or deleting that section removes them too. A large shrink in the\nresponse is your warning; `cerefox_list_versions` has the previous content.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., "Claude Code", "archiver"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it\'s stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer\'s work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch("topic") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title="Same Title", content="...", document_id="abc123",\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch("topic") -> find doc (note its hash) -> modify ->\ningest(title="Same Title", content="...", update_if_exists=true,\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search "<q>" --requestor "<your-name>"` |\n| `cerefox_ingest` (paste) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --author "<your-name>" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --document-id "<uuid>" --expected-content-hash "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor "<your-name>"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor "<your-name>"` |\n| `cerefox_list_projects` | `cerefox project list --requestor "<your-name>"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter \'<json>\' --requestor "<your-name>"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author "<your-name>" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor "<your-name>"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author "<your-name>" --author-type agent`\n- Reads: `--requestor "<your-name>"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n', HELP_SECTIONS, HELP_SECTION_HEADINGS;
|
|
55160
|
+
var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **16 MCP tools** (15 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for "How AI Agents Use Cerefox" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`/`rename_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash, or with `section: "## Heading"` one section\'s text | `document_id` (required), `outline`, `section`, `section_part` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project\'s docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc\'s project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.\n\n## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Look before you overwrite** — `cerefox_get_document(document_id,\n section: "## Heading")` returns exactly the text a `replace_section` on that\n anchor would destroy. The outline gives you a section\'s *size*, never its\n *text*, so on a document you did not write yourself this is the difference\n between a replace and a blind overwrite.\n4. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround. To fix a stale heading (`## OPEN TODOs (as of ...)`), use\n `rename_section`: it changes the heading text and leaves the body and\n position alone.\n5. All of them require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**A section runs to the next same-or-higher heading, or to the end of the\ndocument.** So `end_of_document` inserts land inside the *last* section, and\nreplacing or deleting that section removes them too. A large shrink in the\nresponse is your warning; `cerefox_list_versions` has the previous content.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., "Claude Code", "archiver"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it\'s stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer\'s work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch("topic") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title="Same Title", content="...", document_id="abc123",\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch("topic") -> find doc (note its hash) -> modify ->\ningest(title="Same Title", content="...", update_if_exists=true,\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search "<q>" --requestor "<your-name>"` |\n| `cerefox_ingest` (paste) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --author "<your-name>" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --document-id "<uuid>" --expected-content-hash "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor "<your-name>"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor "<your-name>"` |\n| `cerefox_list_projects` | `cerefox project list --requestor "<your-name>"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter \'<json>\' --requestor "<your-name>"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author "<your-name>" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor "<your-name>"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author "<your-name>" --author-type agent`\n- Reads: `--requestor "<your-name>"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n', HELP_SECTIONS, HELP_SECTION_HEADINGS;
|
|
55065
55161
|
var init_get_help_content = __esm(() => {
|
|
55066
55162
|
HELP_SECTIONS = {
|
|
55067
|
-
Tools: "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash | `document_id` (required), `outline` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.",
|
|
55068
|
-
"Editing part of a document (prefer this over re-sending)": '## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround.\
|
|
55163
|
+
Tools: "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`/`rename_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash, or with `section: \"## Heading\"` one section's text | `document_id` (required), `outline`, `section`, `section_part` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.",
|
|
55164
|
+
"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.',
|
|
55069
55165
|
"Essential Rules": '## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., "Claude Code", "archiver"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it\'s stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer\'s work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.',
|
|
55070
55166
|
"Update Workflow (ID-based -- preferred)": `## Update Workflow (ID-based -- preferred)
|
|
55071
55167
|
|
|
@@ -55386,7 +55482,7 @@ var init_ingest = __esm(() => {
|
|
|
55386
55482
|
init_types3();
|
|
55387
55483
|
ingestTool = {
|
|
55388
55484
|
name: "cerefox_ingest",
|
|
55389
|
-
description: "Save a note or document to the Cerefox knowledge base.",
|
|
55485
|
+
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.",
|
|
55390
55486
|
annotations: {
|
|
55391
55487
|
title: "Save or update a document",
|
|
55392
55488
|
readOnlyHint: false,
|
|
@@ -55446,14 +55542,22 @@ function resolveAuthorType2(ctx, args) {
|
|
|
55446
55542
|
function defaultRequestor(ctx) {
|
|
55447
55543
|
return ctx.accessPath === "cli" ? "cli-user" : "mcp-agent";
|
|
55448
55544
|
}
|
|
55449
|
-
function
|
|
55450
|
-
|
|
55545
|
+
function touchedTrailingSection(applied) {
|
|
55546
|
+
return applied.some((a) => a.reachedEnd === true);
|
|
55547
|
+
}
|
|
55548
|
+
function shrinkNote(before, afterChars, applied) {
|
|
55549
|
+
const beforeChars = [...before].length;
|
|
55550
|
+
const lost = beforeChars - afterChars;
|
|
55451
55551
|
if (lost <= 0)
|
|
55452
55552
|
return "";
|
|
55453
|
-
const pct = Math.round(lost / Math.max(
|
|
55454
|
-
|
|
55455
|
-
|
|
55456
|
-
|
|
55553
|
+
const pct = Math.round(lost / Math.max(beforeChars, 1) * 100);
|
|
55554
|
+
const trailing = touchedTrailingSection(applied);
|
|
55555
|
+
if (pct < 25 && !trailing) {
|
|
55556
|
+
return `This edit removed ${lost} characters. cerefox_list_versions has the previous content.
|
|
55557
|
+
`;
|
|
55558
|
+
}
|
|
55559
|
+
const why = trailing ? `You replaced or deleted the LAST section, and a section runs to the next ` + `heading of the same or higher level — or to the end of the document — so ` + `anything appended after it was inside it. ` : `If you did not intend that, note that a section runs to the next heading ` + `of the same or higher level — or to the end of the document — so replacing ` + `or deleting the LAST section also removes anything appended after it. `;
|
|
55560
|
+
return `⚠ This edit removed ${lost} characters (${pct}% smaller). ${why}cerefox_list_versions has the previous content.
|
|
55457
55561
|
`;
|
|
55458
55562
|
}
|
|
55459
55563
|
function conflictError2(documentId, expectedHash, currentHash) {
|
|
@@ -55563,7 +55667,7 @@ ${summary}
|
|
|
55563
55667
|
|
|
55564
55668
|
` + `New content_hash: ${row?.content_hash ?? newHash}
|
|
55565
55669
|
` + `Size: ${row?.total_chars ?? totalChars} chars (was ${doc.content.length}), ${chunks.length} chunk(s).
|
|
55566
|
-
` + shrinkNote(doc.content
|
|
55670
|
+
` + shrinkNote(doc.content, row?.total_chars ?? totalChars, applied) + `Pass the new content_hash as expected_content_hash on your next edit.${warning2}`;
|
|
55567
55671
|
}
|
|
55568
55672
|
async function insertHandler(supabase, args, ctx) {
|
|
55569
55673
|
const documentId = args.document_id?.trim();
|
|
@@ -55626,7 +55730,8 @@ var init_partial_edits2 = __esm(() => {
|
|
|
55626
55730
|
AUDIT_OP = {
|
|
55627
55731
|
insert: "insert",
|
|
55628
55732
|
replace_section: "replace-section",
|
|
55629
|
-
delete_section: "delete-section"
|
|
55733
|
+
delete_section: "delete-section",
|
|
55734
|
+
rename_section: "rename-section"
|
|
55630
55735
|
};
|
|
55631
55736
|
insertTool = {
|
|
55632
55737
|
name: "cerefox_insert",
|
|
@@ -55677,7 +55782,7 @@ var init_partial_edits2 = __esm(() => {
|
|
|
55677
55782
|
};
|
|
55678
55783
|
editTool = {
|
|
55679
55784
|
name: "cerefox_edit",
|
|
55680
|
-
description: "Change parts of a document without resending the whole thing: one or many operations " + "applied ATOMICALLY in one write. Operations: insert (same positions as cerefox_insert), " + "replace_section (swap a section's body, heading kept), delete_section (remove a section, " + "scope body_only or heading_and_body). Use one call for changes that belong together — a " + "half-applied edit is impossible, so a row and the total it feeds cannot disagree. " + "Operations apply in order and each sees the previous one's result. To change a single " + "line, replace_section on its smallest enclosing heading. Requires expected_content_hash; " + "returns the new hash, not the document.",
|
|
55785
|
+
description: "Change parts of a document without resending the whole thing: one or many operations " + "applied ATOMICALLY in one write. Operations: insert (same positions as cerefox_insert), " + "replace_section (swap a section's body, heading kept), delete_section (remove a section, " + "scope body_only or heading_and_body), rename_section (change a heading's text, leaving " + "its body and position untouched — for headings that go stale, like a dated one). " + "Use one call for changes that belong together — a " + "half-applied edit is impossible, so a row and the total it feeds cannot disagree. " + "Operations apply in order and each sees the previous one's result. To change a single " + "line, replace_section on its smallest enclosing heading. Requires expected_content_hash; " + "returns the new hash, not the document.",
|
|
55681
55786
|
annotations: {
|
|
55682
55787
|
title: "Edit document sections",
|
|
55683
55788
|
readOnlyHint: false,
|
|
@@ -55698,7 +55803,10 @@ var init_partial_edits2 = __esm(() => {
|
|
|
55698
55803
|
type: "object",
|
|
55699
55804
|
required: ["op"],
|
|
55700
55805
|
properties: {
|
|
55701
|
-
op: {
|
|
55806
|
+
op: {
|
|
55807
|
+
type: "string",
|
|
55808
|
+
enum: ["insert", "replace_section", "delete_section", "rename_section"]
|
|
55809
|
+
},
|
|
55702
55810
|
text: { type: "string", description: "Markdown. Required for insert and replace_section." },
|
|
55703
55811
|
position: {
|
|
55704
55812
|
type: "string",
|
|
@@ -55718,6 +55826,10 @@ var init_partial_edits2 = __esm(() => {
|
|
|
55718
55826
|
type: "string",
|
|
55719
55827
|
enum: ["body_only", "heading_and_body"],
|
|
55720
55828
|
description: "delete_section only. Defaults to body_only, which keeps the heading."
|
|
55829
|
+
},
|
|
55830
|
+
new_heading: {
|
|
55831
|
+
type: "string",
|
|
55832
|
+
description: "rename_section only: the replacement heading LINE, at the same level (## stays ##). Changes the heading text and nothing else — the body and the section's position are untouched, which is the point: renaming via delete + insert would risk both. Use it for headings that go stale, like '## OPEN TODOs (as of 2026-08-08)'. A rename changes the anchor, so a later operation in the same call must target the NEW heading."
|
|
55721
55833
|
}
|
|
55722
55834
|
}
|
|
55723
55835
|
}
|
|
@@ -71898,16 +72010,27 @@ function stringify(obj2, { maxDepth = 1000, numbersAsFloat = false } = {}) {
|
|
|
71898
72010
|
*/
|
|
71899
72011
|
|
|
71900
72012
|
// src/cli/util/mcp-config-writers.ts
|
|
72013
|
+
init_config();
|
|
72014
|
+
function envForEntry() {
|
|
72015
|
+
const label = (process.env.CEREFOX_ENV_LABEL ?? "").trim();
|
|
72016
|
+
if (!label || mcpServerName() === "cerefox")
|
|
72017
|
+
return;
|
|
72018
|
+
return { CEREFOX_CONFIG_DIR: resolveConfigDir(), CEREFOX_ENV_LABEL: label };
|
|
72019
|
+
}
|
|
71901
72020
|
function defaultCerefoxEntry() {
|
|
72021
|
+
const env3 = envForEntry();
|
|
71902
72022
|
return {
|
|
71903
72023
|
command: "npx",
|
|
71904
|
-
args: ["-y", "--package=@cerefox/memory", "cerefox", "mcp"]
|
|
72024
|
+
args: ["-y", "--package=@cerefox/memory", "cerefox", "mcp"],
|
|
72025
|
+
...env3 ? { env: env3 } : {}
|
|
71905
72026
|
};
|
|
71906
72027
|
}
|
|
71907
72028
|
function localCerefoxEntry() {
|
|
72029
|
+
const env3 = envForEntry();
|
|
71908
72030
|
return {
|
|
71909
72031
|
command: process.env.CEREFOX_LOCAL_CMD || "cerefox-local",
|
|
71910
|
-
args: ["mcp"]
|
|
72032
|
+
args: ["mcp"],
|
|
72033
|
+
...env3 ? { env: env3 } : {}
|
|
71911
72034
|
};
|
|
71912
72035
|
}
|
|
71913
72036
|
function claudeCodeUserConfigPath() {
|
|
@@ -71924,9 +72047,20 @@ function claudeDesktopConfigPath() {
|
|
|
71924
72047
|
return join4(home, ".config", "Claude", "claude_desktop_config.json");
|
|
71925
72048
|
}
|
|
71926
72049
|
function claudeCodeDelegated(entry) {
|
|
72050
|
+
const envFlags = Object.entries(entry.env ?? {}).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
|
|
71927
72051
|
return {
|
|
71928
72052
|
cmd: "claude",
|
|
71929
|
-
args: [
|
|
72053
|
+
args: [
|
|
72054
|
+
"mcp",
|
|
72055
|
+
"add",
|
|
72056
|
+
mcpServerName(),
|
|
72057
|
+
"--scope",
|
|
72058
|
+
"user",
|
|
72059
|
+
...envFlags,
|
|
72060
|
+
"--",
|
|
72061
|
+
entry.command,
|
|
72062
|
+
...entry.args
|
|
72063
|
+
]
|
|
71930
72064
|
};
|
|
71931
72065
|
}
|
|
71932
72066
|
function cursorConfigPath() {
|
|
@@ -72011,7 +72145,7 @@ function directWrite(writer, configPath, opts) {
|
|
|
72011
72145
|
}
|
|
72012
72146
|
const serversKey = format === "toml" ? "mcp_servers" : "mcpServers";
|
|
72013
72147
|
const servers = (existing[serversKey] && typeof existing[serversKey] === "object" ? existing[serversKey] : {}) ?? {};
|
|
72014
|
-
servers
|
|
72148
|
+
servers[mcpServerName()] = entry;
|
|
72015
72149
|
existing[serversKey] = servers;
|
|
72016
72150
|
if (!opts.dryRun) {
|
|
72017
72151
|
const body = format === "toml" ? stringify(existing) + `
|
|
@@ -72021,10 +72155,17 @@ function directWrite(writer, configPath, opts) {
|
|
|
72021
72155
|
}
|
|
72022
72156
|
return { configPath, backupPath, action: action6, serverEntry: entry };
|
|
72023
72157
|
}
|
|
72158
|
+
function mcpServerName() {
|
|
72159
|
+
const label = (process.env.CEREFOX_ENV_LABEL ?? "").trim();
|
|
72160
|
+
if (!label)
|
|
72161
|
+
return "cerefox";
|
|
72162
|
+
const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
72163
|
+
return slug ? `cerefox-${slug}` : "cerefox";
|
|
72164
|
+
}
|
|
72024
72165
|
function hasCerefoxEntry(existing, format) {
|
|
72025
72166
|
const key = format === "toml" ? "mcp_servers" : "mcpServers";
|
|
72026
72167
|
const servers = existing[key];
|
|
72027
|
-
return typeof servers === "object" && servers !== null && servers
|
|
72168
|
+
return typeof servers === "object" && servers !== null && servers[mcpServerName()] !== undefined;
|
|
72028
72169
|
}
|
|
72029
72170
|
function delegatedWrite(writer, opts) {
|
|
72030
72171
|
if (!writer.delegated) {
|
|
@@ -72093,6 +72234,7 @@ function action6(options) {
|
|
|
72093
72234
|
println(c.dim(` backup: ${result.backupPath}`));
|
|
72094
72235
|
}
|
|
72095
72236
|
println(c.dim(` action: ${result.action}`));
|
|
72237
|
+
println(c.dim(` server name: ${mcpServerName()}`));
|
|
72096
72238
|
if (result.delegatedCommand) {
|
|
72097
72239
|
println(c.dim(` invoked: ${result.delegatedCommand}`));
|
|
72098
72240
|
}
|
|
@@ -76319,8 +76461,8 @@ import { homedir as homedir6 } from "node:os";
|
|
|
76319
76461
|
import { join as join9 } from "node:path";
|
|
76320
76462
|
|
|
76321
76463
|
// ../../_shared/ef-meta/index.ts
|
|
76322
|
-
var EF_VERSION = "1.
|
|
76323
|
-
var EF_LAST_CHANGED = "1.
|
|
76464
|
+
var EF_VERSION = "1.4.0";
|
|
76465
|
+
var EF_LAST_CHANGED = "1.4.0";
|
|
76324
76466
|
|
|
76325
76467
|
// src/cli/util/checks.ts
|
|
76326
76468
|
init_config();
|
|
@@ -76812,7 +76954,8 @@ async function checkPostgres() {
|
|
|
76812
76954
|
}
|
|
76813
76955
|
let postgres2;
|
|
76814
76956
|
try {
|
|
76815
|
-
|
|
76957
|
+
const mod = await Promise.resolve().then(() => (init_src(), exports_src));
|
|
76958
|
+
postgres2 = mod.default;
|
|
76816
76959
|
} catch (err) {
|
|
76817
76960
|
return {
|
|
76818
76961
|
name: "postgres",
|
|
@@ -77576,6 +77719,7 @@ class IngestionPipeline {
|
|
|
77576
77719
|
text,
|
|
77577
77720
|
title,
|
|
77578
77721
|
source = "paste",
|
|
77722
|
+
sourceOnCreate,
|
|
77579
77723
|
sourceLabel,
|
|
77580
77724
|
sourcePath: sourcePathOpt,
|
|
77581
77725
|
projectName,
|
|
@@ -77678,7 +77822,8 @@ class IngestionPipeline {
|
|
|
77678
77822
|
action: "skipped",
|
|
77679
77823
|
reindexed: false,
|
|
77680
77824
|
projectIds: existingProjectIds,
|
|
77681
|
-
note: ""
|
|
77825
|
+
note: "",
|
|
77826
|
+
contentHash: existingByHash.content_hash ?? hash
|
|
77682
77827
|
};
|
|
77683
77828
|
}
|
|
77684
77829
|
const chunks = chunkMarkdown(text, this.settings.maxChunkChars, this.settings.minChunkChars);
|
|
@@ -77703,7 +77848,7 @@ class IngestionPipeline {
|
|
|
77703
77848
|
const rpcResult = await this.db.ingestDocumentRpc({
|
|
77704
77849
|
documentId: null,
|
|
77705
77850
|
title,
|
|
77706
|
-
source,
|
|
77851
|
+
source: source ?? sourceOnCreate ?? null,
|
|
77707
77852
|
sourcePath,
|
|
77708
77853
|
contentHash: hash,
|
|
77709
77854
|
metadata: validatedMeta,
|
|
@@ -77725,7 +77870,8 @@ class IngestionPipeline {
|
|
|
77725
77870
|
action: "created",
|
|
77726
77871
|
reindexed: false,
|
|
77727
77872
|
projectIds: resolvedIds,
|
|
77728
|
-
note: ""
|
|
77873
|
+
note: "",
|
|
77874
|
+
contentHash: hash
|
|
77729
77875
|
};
|
|
77730
77876
|
}
|
|
77731
77877
|
async updateDocument(opts) {
|
|
@@ -77817,7 +77963,8 @@ class IngestionPipeline {
|
|
|
77817
77963
|
action: "updated",
|
|
77818
77964
|
reindexed: false,
|
|
77819
77965
|
projectIds: finalProjectIds2,
|
|
77820
|
-
note: ""
|
|
77966
|
+
note: "",
|
|
77967
|
+
contentHash: existing.content_hash ?? newHash
|
|
77821
77968
|
};
|
|
77822
77969
|
}
|
|
77823
77970
|
const chunks = chunkMarkdown(text, this.settings.maxChunkChars, this.settings.minChunkChars);
|
|
@@ -77851,7 +77998,7 @@ class IngestionPipeline {
|
|
|
77851
77998
|
contentFormat: CONTENT_FORMAT_BLIND_STITCH,
|
|
77852
77999
|
author,
|
|
77853
78000
|
authorType,
|
|
77854
|
-
sourceLabel: sourceLabel ?? source,
|
|
78001
|
+
sourceLabel: sourceLabel ?? source ?? "manual",
|
|
77855
78002
|
expectedContentHash: expectedContentHash ?? (forceRechunk && contentUnchanged ? existing.content_hash : null),
|
|
77856
78003
|
lastWriteWins
|
|
77857
78004
|
});
|
|
@@ -77870,7 +78017,8 @@ class IngestionPipeline {
|
|
|
77870
78017
|
action: "updated",
|
|
77871
78018
|
reindexed: true,
|
|
77872
78019
|
projectIds: finalProjectIds,
|
|
77873
|
-
note: ""
|
|
78020
|
+
note: "",
|
|
78021
|
+
contentHash: newHash
|
|
77874
78022
|
};
|
|
77875
78023
|
}
|
|
77876
78024
|
async ingestFile(path, opts = {}) {
|
|
@@ -77881,7 +78029,8 @@ class IngestionPipeline {
|
|
|
77881
78029
|
...opts,
|
|
77882
78030
|
text,
|
|
77883
78031
|
title: opts.title ?? stem,
|
|
77884
|
-
source: opts.source
|
|
78032
|
+
source: opts.source === undefined ? "file" : opts.source,
|
|
78033
|
+
sourceOnCreate: opts.sourceOnCreate ?? "file",
|
|
77885
78034
|
sourcePath: absPath
|
|
77886
78035
|
});
|
|
77887
78036
|
}
|
|
@@ -77953,6 +78102,7 @@ async function action18(path, options) {
|
|
|
77953
78102
|
title = data.title;
|
|
77954
78103
|
println(c.dim(` (keeping existing title: ${JSON.stringify(title)})`));
|
|
77955
78104
|
}
|
|
78105
|
+
const resolvedSource = options.source ?? null;
|
|
77956
78106
|
const pipeline = new IngestionPipeline({
|
|
77957
78107
|
supabase,
|
|
77958
78108
|
openAiApiKey: settings.openaiApiKey
|
|
@@ -77960,7 +78110,8 @@ async function action18(path, options) {
|
|
|
77960
78110
|
try {
|
|
77961
78111
|
const result = path && !options.paste ? await pipeline.ingestFile(path, {
|
|
77962
78112
|
title,
|
|
77963
|
-
source:
|
|
78113
|
+
source: resolvedSource,
|
|
78114
|
+
sourceOnCreate: "cli",
|
|
77964
78115
|
projectName: options.projectName ?? null,
|
|
77965
78116
|
projectNames: projectNames ?? null,
|
|
77966
78117
|
metadata: metadata ?? null,
|
|
@@ -77973,7 +78124,8 @@ async function action18(path, options) {
|
|
|
77973
78124
|
}) : await pipeline.ingestText({
|
|
77974
78125
|
text: content,
|
|
77975
78126
|
title,
|
|
77976
|
-
source:
|
|
78127
|
+
source: resolvedSource,
|
|
78128
|
+
sourceOnCreate: "cli",
|
|
77977
78129
|
projectName: options.projectName ?? null,
|
|
77978
78130
|
projectNames: projectNames ?? null,
|
|
77979
78131
|
metadata: metadata ?? null,
|
|
@@ -77995,13 +78147,16 @@ async function action18(path, options) {
|
|
|
77995
78147
|
const projects = result.projectIds.length > 0 ? ` [projects: ${result.projectIds.length}]` : "";
|
|
77996
78148
|
const note = result.note ? ` (${result.note})` : "";
|
|
77997
78149
|
println(c.green("✓ ") + `${verb}: ${JSON.stringify(result.title)} (id: ${result.documentId}), ` + `${result.chunkCount} chunk(s), ${result.totalChars} chars.${projects}${note}`);
|
|
78150
|
+
if (result.contentHash) {
|
|
78151
|
+
println(c.dim(` content_hash: ${result.contentHash}`));
|
|
78152
|
+
}
|
|
77998
78153
|
} catch (err) {
|
|
77999
78154
|
const msg = err instanceof Error ? err.message : String(err);
|
|
78000
78155
|
throw systemError(`Ingest failed: ${msg}`);
|
|
78001
78156
|
}
|
|
78002
78157
|
}
|
|
78003
78158
|
function registerIngest(program2) {
|
|
78004
|
-
program2.command("ingest").description("Ingest a file (or stdin paste) into the knowledge base.").argument("[path]", "Path to the file to ingest. Omit when using --paste.").option("--paste", "Read content from stdin instead of a file.").option("-t, --title <title>", "Document title (required with --paste; defaults to filename without extension).").option("-p, --project-name <name>", "Single project membership (non-destructive on update).").option("-P, --project-names <names>", "Comma-separated full project membership set (destructive replace on update).").option("-m, --metadata <json>", "JSON metadata object.").option("--source <label>",
|
|
78159
|
+
program2.command("ingest").description("Ingest a file (or stdin paste) into the knowledge base.").argument("[path]", "Path to the file to ingest. Omit when using --paste.").option("--paste", "Read content from stdin instead of a file.").option("-t, --title <title>", "Document title (required with --paste; defaults to filename without extension).").option("-p, --project-name <name>", "Single project membership (non-destructive on update).").option("-P, --project-names <names>", "Comma-separated full project membership set (destructive replace on update).").option("-m, --metadata <json>", "JSON metadata object.").option("--source <label>", 'Origin label. Omit it on an update and the document keeps the source it already has (#193); omit it on a create and it is recorded as "cli".').option("-u, --update-if-exists", "Update an existing doc with the same title.").option("-i, --document-id <uuid>", "Update a specific document by UUID (overrides --update-if-exists).").option("--expected-content-hash <sha256>", "Optimistic-concurrency token: the content_hash of the version this edit is based on (shown by `document get` / `search`). Required on content updates unless --last-write-wins.").option("--last-write-wins", "Skip the concurrency check and overwrite regardless of concurrent changes (recorded in the audit log).").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(action18);
|
|
78005
78160
|
}
|
|
78006
78161
|
|
|
78007
78162
|
// src/cli/commands/document-partial-edit.ts
|
|
@@ -78145,7 +78300,8 @@ async function action19(dir, options) {
|
|
|
78145
78300
|
try {
|
|
78146
78301
|
const result = await pipeline.ingestFile(file, {
|
|
78147
78302
|
title: basename3(file, extname4(file)),
|
|
78148
|
-
source: options.source ??
|
|
78303
|
+
source: options.source ?? null,
|
|
78304
|
+
sourceOnCreate: "cli",
|
|
78149
78305
|
projectName: options.projectName ?? null,
|
|
78150
78306
|
metadata: metadata ?? null,
|
|
78151
78307
|
updateExisting: Boolean(options.updateIfExists),
|
|
@@ -78178,7 +78334,7 @@ async function action19(dir, options) {
|
|
|
78178
78334
|
}
|
|
78179
78335
|
}
|
|
78180
78336
|
function registerIngestDir(program2) {
|
|
78181
|
-
program2.command("ingest-dir").description("Recursively ingest a directory of markdown / text files.").argument("<dir>", "Root directory to walk.").option("-p, --project-name <name>", "Project membership for all ingested docs.").option("-m, --metadata <json>", "JSON metadata applied to every doc.").option("--source <label>",
|
|
78337
|
+
program2.command("ingest-dir").description("Recursively ingest a directory of markdown / text files.").argument("<dir>", "Root directory to walk.").option("-p, --project-name <name>", "Project membership for all ingested docs.").option("-m, --metadata <json>", "JSON metadata applied to every doc.").option("--source <label>", 'Origin label. Omit it and each matched document keeps the source it already has (#193); newly created ones are recorded as "cli".').option("-u, --update-if-exists", "Update an existing doc with the same title.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").option("-e, --extensions <list>", "Comma-separated file extensions to ingest.", ".md,.txt").action(action19);
|
|
78182
78338
|
}
|
|
78183
78339
|
|
|
78184
78340
|
// src/cli/commands/init.ts
|
|
@@ -83652,7 +83808,7 @@ async function runSearch(ctx, opts) {
|
|
|
83652
83808
|
if (!ctx.openAiApiKey && resolveEmbedderKind() !== "local") {
|
|
83653
83809
|
throw new HttpError(503, "Embedder not available");
|
|
83654
83810
|
}
|
|
83655
|
-
const embedding = await getEmbedding(query, ctx.openAiApiKey);
|
|
83811
|
+
const embedding = await getEmbedding(query, ctx.openAiApiKey ?? "");
|
|
83656
83812
|
if (mode === "semantic") {
|
|
83657
83813
|
const params2 = {
|
|
83658
83814
|
p_query_embedding: embedding,
|
|
@@ -84249,7 +84405,7 @@ function registerDocumentWriteRoutes(app, ctx) {
|
|
|
84249
84405
|
try {
|
|
84250
84406
|
const pipeline2 = new IngestionPipeline({
|
|
84251
84407
|
supabase: ctx.supabase,
|
|
84252
|
-
openAiApiKey: ctx.openAiApiKey
|
|
84408
|
+
openAiApiKey: ctx.openAiApiKey ?? ""
|
|
84253
84409
|
});
|
|
84254
84410
|
const result = await pipeline2.updateDocument({
|
|
84255
84411
|
documentId,
|
|
@@ -84420,7 +84576,7 @@ function registerIngestRoutes(app, ctx) {
|
|
|
84420
84576
|
try {
|
|
84421
84577
|
const pipeline2 = new IngestionPipeline({
|
|
84422
84578
|
supabase: ctx.supabase,
|
|
84423
|
-
openAiApiKey: ctx.openAiApiKey
|
|
84579
|
+
openAiApiKey: ctx.openAiApiKey ?? ""
|
|
84424
84580
|
});
|
|
84425
84581
|
const result = await pipeline2.ingestText({
|
|
84426
84582
|
text: content.trim(),
|
|
@@ -84485,7 +84641,7 @@ function registerIngestRoutes(app, ctx) {
|
|
|
84485
84641
|
try {
|
|
84486
84642
|
const pipeline2 = new IngestionPipeline({
|
|
84487
84643
|
supabase: ctx.supabase,
|
|
84488
|
-
openAiApiKey: ctx.openAiApiKey
|
|
84644
|
+
openAiApiKey: ctx.openAiApiKey ?? ""
|
|
84489
84645
|
});
|
|
84490
84646
|
const result = await pipeline2.ingestText({
|
|
84491
84647
|
text,
|
|
@@ -84538,7 +84694,7 @@ function registerIngestRoutes(app, ctx) {
|
|
|
84538
84694
|
try {
|
|
84539
84695
|
const pipeline2 = new IngestionPipeline({
|
|
84540
84696
|
supabase: ctx.supabase,
|
|
84541
|
-
openAiApiKey: ctx.openAiApiKey
|
|
84697
|
+
openAiApiKey: ctx.openAiApiKey ?? ""
|
|
84542
84698
|
});
|
|
84543
84699
|
const result = await pipeline2.updateDocument({
|
|
84544
84700
|
documentId,
|