@cerefox/memory 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +90 -8
- package/AGENT_QUICK_REFERENCE.md +91 -5
- package/dist/bin/cerefox.js +715 -387
- package/dist/frontend/assets/{index-B1pgikxA.js → index-D9z5yV9u.js} +30 -30
- package/dist/frontend/assets/index-D9z5yV9u.js.map +1 -0
- package/dist/frontend/index.html +1 -1
- package/dist/server-assets/_shared/ef-meta/index.ts +20 -2
- package/dist/server-assets/_shared/mcp-tools/audit-log.ts +14 -1
- package/dist/server-assets/_shared/mcp-tools/get-document.ts +67 -3
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +6 -4
- package/dist/server-assets/_shared/mcp-tools/get-help.ts +56 -0
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +2 -1
- package/dist/server-assets/_shared/mcp-tools/list-versions.ts +12 -1
- package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +82 -14
- package/dist/server-assets/_shared/partial-edits/index.ts +222 -13
- package/dist/server-assets/db/migrations/0021_rename_section_audit_op.sql +36 -0
- package/dist/server-assets/db/migrations/0022_rls_on_document_relations.sql +38 -0
- package/dist/server-assets/db/rpcs.sql +4 -1
- package/dist/server-assets/db/schema.sql +12 -2
- package/docs/guides/configuration.md +1 -1
- package/docs/guides/connect-agents.md +14 -1
- package/package.json +1 -1
- package/dist/frontend/assets/index-B1pgikxA.js.map +0 -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.5.0";
|
|
7442
7442
|
var init_meta = () => {};
|
|
7443
7443
|
|
|
7444
7444
|
// ../../_shared/config/paths.ts
|
|
@@ -25730,6 +25730,10 @@ var init_bundled_docs = __esm(() => {
|
|
|
25730
25730
|
PACKAGE_ROOT = findPackageRoot();
|
|
25731
25731
|
});
|
|
25732
25732
|
|
|
25733
|
+
// ../../_shared/ef-meta/index.ts
|
|
25734
|
+
var EF_VERSION = "1.5.0", CEREFOX_VERSION = "1.5.0", EF_LAST_CHANGED = "1.5.0";
|
|
25735
|
+
var init_ef_meta = () => {};
|
|
25736
|
+
|
|
25733
25737
|
// ../../_shared/compatibility/index.ts
|
|
25734
25738
|
function compareSemver(a, b2) {
|
|
25735
25739
|
const split = (v) => {
|
|
@@ -26285,13 +26289,13 @@ function canonicalHeading(text) {
|
|
|
26285
26289
|
const m = text.trim().match(/^(#{1,6})[ \t]+(.*?)[ \t]*#*[ \t]*$/);
|
|
26286
26290
|
return m ? `${m[1]} ${m[2].trim()}`.trim() : text.trim();
|
|
26287
26291
|
}
|
|
26288
|
-
function resolveAnchor(outline, anchorHeading) {
|
|
26292
|
+
function resolveAnchor(outline, anchorHeading, reads = false) {
|
|
26289
26293
|
const anchor = canonicalHeading(anchorHeading);
|
|
26290
26294
|
const byHeading = outline.filter((n) => n.heading === anchor);
|
|
26291
26295
|
if (byHeading.length === 1)
|
|
26292
26296
|
return byHeading[0];
|
|
26293
26297
|
if (byHeading.length > 1) {
|
|
26294
|
-
throw new AmbiguousAnchorError(anchor, byHeading.map((n) => n.path));
|
|
26298
|
+
throw new AmbiguousAnchorError(anchor, byHeading.map((n) => n.path), reads);
|
|
26295
26299
|
}
|
|
26296
26300
|
if (anchor.includes(" > ")) {
|
|
26297
26301
|
const normalizedPath = anchor.split(" > ").map((seg) => canonicalHeading(seg)).join(" > ");
|
|
@@ -26299,10 +26303,37 @@ function resolveAnchor(outline, anchorHeading) {
|
|
|
26299
26303
|
if (byPath.length === 1)
|
|
26300
26304
|
return byPath[0];
|
|
26301
26305
|
if (byPath.length > 1) {
|
|
26302
|
-
throw new AmbiguousAnchorError(anchor, byPath.map((n) => n.path));
|
|
26306
|
+
throw new AmbiguousAnchorError(anchor, byPath.map((n) => n.path), reads);
|
|
26303
26307
|
}
|
|
26304
26308
|
}
|
|
26305
|
-
throw new AnchorNotFoundError(anchor, outline);
|
|
26309
|
+
throw new AnchorNotFoundError(anchor, outline, reads);
|
|
26310
|
+
}
|
|
26311
|
+
function extractSection(content, anchorHeading, sectionPart) {
|
|
26312
|
+
const outline = parseOutline(content);
|
|
26313
|
+
const node = resolveAnchor(outline, anchorHeading, true);
|
|
26314
|
+
const to = resolveSectionEnd(content, outline, node, sectionPart, "the section read", true);
|
|
26315
|
+
const text = content.slice(node.bodyStart, to);
|
|
26316
|
+
return {
|
|
26317
|
+
heading: node.heading,
|
|
26318
|
+
path: node.path,
|
|
26319
|
+
level: node.level,
|
|
26320
|
+
text,
|
|
26321
|
+
chars: text.length,
|
|
26322
|
+
section_part: sectionPart ?? null
|
|
26323
|
+
};
|
|
26324
|
+
}
|
|
26325
|
+
function firstMeaningfulLine(text) {
|
|
26326
|
+
for (const line of text.split(`
|
|
26327
|
+
`)) {
|
|
26328
|
+
if (line.trim() !== "")
|
|
26329
|
+
return canonicalHeading(line);
|
|
26330
|
+
}
|
|
26331
|
+
return "";
|
|
26332
|
+
}
|
|
26333
|
+
function assertNoDuplicateHeading(text, node, opName) {
|
|
26334
|
+
if (firstMeaningfulLine(text) === node.heading) {
|
|
26335
|
+
throw new DuplicateHeadingError(node.heading, opName);
|
|
26336
|
+
}
|
|
26306
26337
|
}
|
|
26307
26338
|
function firstChild(outline, node) {
|
|
26308
26339
|
for (const n of outline) {
|
|
@@ -26311,7 +26342,7 @@ function firstChild(outline, node) {
|
|
|
26311
26342
|
}
|
|
26312
26343
|
return null;
|
|
26313
26344
|
}
|
|
26314
|
-
function resolveSectionEnd(content, outline, node, sectionPart, opName) {
|
|
26345
|
+
function resolveSectionEnd(content, outline, node, sectionPart, opName, reads = false) {
|
|
26315
26346
|
const child = firstChild(outline, node);
|
|
26316
26347
|
if (!child)
|
|
26317
26348
|
return node.subtreeEnd;
|
|
@@ -26319,7 +26350,7 @@ function resolveSectionEnd(content, outline, node, sectionPart, opName) {
|
|
|
26319
26350
|
return node.ownBodyEnd;
|
|
26320
26351
|
if (sectionPart === "subtree")
|
|
26321
26352
|
return node.subtreeEnd;
|
|
26322
|
-
throw new AmbiguousPositionError(node, child.heading, opName);
|
|
26353
|
+
throw new AmbiguousPositionError(node, child.heading, opName, reads);
|
|
26323
26354
|
}
|
|
26324
26355
|
function spliceBlock(content, from, to, text) {
|
|
26325
26356
|
const before = content.slice(0, from).replace(/\n+$/, "");
|
|
@@ -26359,9 +26390,11 @@ function applyOne(content, operation) {
|
|
|
26359
26390
|
at = node2.start;
|
|
26360
26391
|
detail = "insert before_heading";
|
|
26361
26392
|
} else if (position === "after_heading") {
|
|
26393
|
+
assertNoDuplicateHeading(text, node2, "insert");
|
|
26362
26394
|
at = node2.bodyStart;
|
|
26363
26395
|
detail = "insert after_heading";
|
|
26364
26396
|
} else {
|
|
26397
|
+
assertNoDuplicateHeading(text, node2, "insert");
|
|
26365
26398
|
at = resolveSectionEnd(content, outline, node2, operation.section_part, "end_of_section insert");
|
|
26366
26399
|
detail = `insert at end_of_section` + (operation.section_part ? ` (${operation.section_part})` : "");
|
|
26367
26400
|
}
|
|
@@ -26372,13 +26405,37 @@ function applyOne(content, operation) {
|
|
|
26372
26405
|
}
|
|
26373
26406
|
if (operation.op === "replace_section") {
|
|
26374
26407
|
const node2 = resolveAnchor(outline, operation.anchor_heading);
|
|
26408
|
+
assertNoDuplicateHeading(operation.text, node2, "replace_section");
|
|
26375
26409
|
const to2 = resolveSectionEnd(content, outline, node2, operation.section_part, "replace_section");
|
|
26376
26410
|
return {
|
|
26377
26411
|
content: spliceBlock(content, node2.bodyStart, to2, operation.text),
|
|
26378
26412
|
applied: {
|
|
26379
26413
|
op: "replace_section",
|
|
26380
26414
|
path: node2.path,
|
|
26381
|
-
detail: "replace_section body" + (operation.section_part ? ` (${operation.section_part})` : "")
|
|
26415
|
+
detail: "replace_section body" + (operation.section_part ? ` (${operation.section_part})` : ""),
|
|
26416
|
+
reachedEnd: to2 >= content.trimEnd().length
|
|
26417
|
+
}
|
|
26418
|
+
};
|
|
26419
|
+
}
|
|
26420
|
+
if (operation.op === "rename_section") {
|
|
26421
|
+
const node2 = resolveAnchor(outline, operation.anchor_heading);
|
|
26422
|
+
const next = canonicalHeading(operation.new_heading);
|
|
26423
|
+
const m = next.match(ATX_HEADING);
|
|
26424
|
+
if (!m) {
|
|
26425
|
+
throw new InvalidOperationError(0, `new_heading must be a markdown heading line like "## Title", got ${JSON.stringify(operation.new_heading)}`);
|
|
26426
|
+
}
|
|
26427
|
+
if (m[1].length !== node2.level)
|
|
26428
|
+
throw new HeadingLevelChangeError(node2.heading, next);
|
|
26429
|
+
const hadNewline = content[node2.bodyStart - 1] === `
|
|
26430
|
+
`;
|
|
26431
|
+
const replacement = next + (hadNewline ? `
|
|
26432
|
+
` : "");
|
|
26433
|
+
return {
|
|
26434
|
+
content: content.slice(0, node2.start) + replacement + content.slice(node2.bodyStart),
|
|
26435
|
+
applied: {
|
|
26436
|
+
op: "rename_section",
|
|
26437
|
+
path: node2.path,
|
|
26438
|
+
detail: `rename_section to ${next}`
|
|
26382
26439
|
}
|
|
26383
26440
|
};
|
|
26384
26441
|
}
|
|
@@ -26391,7 +26448,8 @@ function applyOne(content, operation) {
|
|
|
26391
26448
|
applied: {
|
|
26392
26449
|
op: "delete_section",
|
|
26393
26450
|
path: node.path,
|
|
26394
|
-
detail: `delete_section (${scope})` + (operation.section_part ? ` (${operation.section_part})` : "")
|
|
26451
|
+
detail: `delete_section (${scope})` + (operation.section_part ? ` (${operation.section_part})` : ""),
|
|
26452
|
+
reachedEnd: to >= content.trimEnd().length
|
|
26395
26453
|
}
|
|
26396
26454
|
};
|
|
26397
26455
|
}
|
|
@@ -26433,8 +26491,21 @@ function validateOperations(operations) {
|
|
|
26433
26491
|
if (o.scope !== undefined && o.scope !== "body_only" && o.scope !== "heading_and_body") {
|
|
26434
26492
|
throw new InvalidOperationError(i, "scope must be body_only or heading_and_body");
|
|
26435
26493
|
}
|
|
26494
|
+
} else if (op === "rename_section") {
|
|
26495
|
+
if (typeof o.anchor_heading !== "string" || o.anchor_heading.trim() === "") {
|
|
26496
|
+
throw new InvalidOperationError(i, "rename_section requires anchor_heading");
|
|
26497
|
+
}
|
|
26498
|
+
if (typeof o.new_heading !== "string" || o.new_heading.trim() === "") {
|
|
26499
|
+
throw new InvalidOperationError(i, "rename_section requires non-empty new_heading");
|
|
26500
|
+
}
|
|
26501
|
+
if (o.text !== undefined) {
|
|
26502
|
+
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");
|
|
26503
|
+
}
|
|
26504
|
+
if (o.section_part !== undefined) {
|
|
26505
|
+
throw new InvalidOperationError(i, "rename_section takes no section_part: it replaces the heading line, so no extent is involved");
|
|
26506
|
+
}
|
|
26436
26507
|
} else {
|
|
26437
|
-
throw new InvalidOperationError(i, "op must be insert | replace_section | delete_section");
|
|
26508
|
+
throw new InvalidOperationError(i, "op must be insert | replace_section | delete_section | rename_section");
|
|
26438
26509
|
}
|
|
26439
26510
|
if (o.section_part !== undefined && o.section_part !== "own_body" && o.section_part !== "subtree") {
|
|
26440
26511
|
throw new InvalidOperationError(i, "section_part must be own_body or subtree");
|
|
@@ -26461,22 +26532,22 @@ function applyOperations(content, operations) {
|
|
|
26461
26532
|
}
|
|
26462
26533
|
return { content: current, applied };
|
|
26463
26534
|
}
|
|
26464
|
-
var AnchorNotFoundError, AmbiguousAnchorError, AmbiguousPositionError, InvalidOperationError, ATX_HEADING, FENCE_OPEN;
|
|
26535
|
+
var AnchorNotFoundError, AmbiguousAnchorError, AmbiguousPositionError, HeadingLevelChangeError, DuplicateHeadingError, InvalidOperationError, ATX_HEADING, FENCE_OPEN;
|
|
26465
26536
|
var init_partial_edits = __esm(() => {
|
|
26466
26537
|
AnchorNotFoundError = class AnchorNotFoundError extends Error {
|
|
26467
|
-
constructor(anchor, outline) {
|
|
26538
|
+
constructor(anchor, outline, reads = false) {
|
|
26468
26539
|
const known = outline.length ? ` Known headings:
|
|
26469
26540
|
${outline.map((n) => ` ${n.path}`).join(`
|
|
26470
26541
|
`)}` : " The document has no headings.";
|
|
26471
|
-
super(`Anchor not found: "${anchor}"
|
|
26542
|
+
super(`Anchor not found: "${anchor}".${reads ? "" : " No write was performed."}${known}
|
|
26472
26543
|
` + `Anchors match a heading line exactly ("## Title") or a parent path ` + `("## Parent > ### Child").`);
|
|
26473
26544
|
this.name = "AnchorNotFoundError";
|
|
26474
26545
|
}
|
|
26475
26546
|
};
|
|
26476
26547
|
AmbiguousAnchorError = class AmbiguousAnchorError extends Error {
|
|
26477
26548
|
candidates;
|
|
26478
|
-
constructor(anchor, candidates) {
|
|
26479
|
-
super(`Ambiguous anchor: "${anchor}" matches ${candidates.length} sections. ` +
|
|
26549
|
+
constructor(anchor, candidates, reads = false) {
|
|
26550
|
+
super(`Ambiguous anchor: "${anchor}" matches ${candidates.length} sections. ` + `${reads ? "" : "No write was performed. "}Disambiguate by passing one of these paths as anchor_heading:
|
|
26480
26551
|
` + candidates.map((c2) => ` ${c2}`).join(`
|
|
26481
26552
|
`));
|
|
26482
26553
|
this.name = "AmbiguousAnchorError";
|
|
@@ -26485,7 +26556,7 @@ ${outline.map((n) => ` ${n.path}`).join(`
|
|
|
26485
26556
|
};
|
|
26486
26557
|
AmbiguousPositionError = class AmbiguousPositionError extends Error {
|
|
26487
26558
|
candidates;
|
|
26488
|
-
constructor(node, firstChildHeading, opName) {
|
|
26559
|
+
constructor(node, firstChildHeading, opName, reads = false) {
|
|
26489
26560
|
const candidates = [
|
|
26490
26561
|
{
|
|
26491
26562
|
section_part: "own_body",
|
|
@@ -26496,13 +26567,27 @@ ${outline.map((n) => ` ${n.path}`).join(`
|
|
|
26496
26567
|
description: `the whole subtree, past everything nested under ${node.heading} — ` + `which can be a long way down`
|
|
26497
26568
|
}
|
|
26498
26569
|
];
|
|
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. ` +
|
|
26570
|
+
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
26571
|
` + candidates.map((c2) => ` section_part: "${c2.section_part}" — ${c2.description}`).join(`
|
|
26501
26572
|
`));
|
|
26502
26573
|
this.name = "AmbiguousPositionError";
|
|
26503
26574
|
this.candidates = candidates;
|
|
26504
26575
|
}
|
|
26505
26576
|
};
|
|
26577
|
+
HeadingLevelChangeError = class HeadingLevelChangeError extends Error {
|
|
26578
|
+
constructor(fromHeading, toHeading) {
|
|
26579
|
+
const from = (fromHeading.match(/^#+/) ?? [""])[0].length;
|
|
26580
|
+
const to = (toHeading.match(/^#+/) ?? [""])[0].length;
|
|
26581
|
+
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.`);
|
|
26582
|
+
this.name = "HeadingLevelChangeError";
|
|
26583
|
+
}
|
|
26584
|
+
};
|
|
26585
|
+
DuplicateHeadingError = class DuplicateHeadingError extends Error {
|
|
26586
|
+
constructor(heading, opName) {
|
|
26587
|
+
super(`The text you passed to ${opName} starts with the anchor heading itself ` + `(${JSON.stringify(heading)}). No write was performed. The heading is kept ` + `automatically — ${opName === "replace_section" ? "replace_section preserves it" : "insert places your text inside the section"}, ` + `so including it would produce two. Send only the new content, without that ` + `heading line. A DEEPER sub-heading inside your text is fine.`);
|
|
26588
|
+
this.name = "DuplicateHeadingError";
|
|
26589
|
+
}
|
|
26590
|
+
};
|
|
26506
26591
|
InvalidOperationError = class InvalidOperationError extends Error {
|
|
26507
26592
|
constructor(index, message) {
|
|
26508
26593
|
super(`Invalid operation at index ${index}: ${message}. No write was performed.`);
|
|
@@ -54639,6 +54724,10 @@ var require_lib7 = __commonJS((exports) => {
|
|
|
54639
54724
|
});
|
|
54640
54725
|
|
|
54641
54726
|
// ../../_shared/mcp-tools/audit-log.ts
|
|
54727
|
+
function utcStamp2(iso) {
|
|
54728
|
+
const trimmed = iso.slice(0, 19);
|
|
54729
|
+
return trimmed.includes("T") ? `${trimmed}Z` : `${trimmed} UTC`;
|
|
54730
|
+
}
|
|
54642
54731
|
async function handler(supabase, args, ctx) {
|
|
54643
54732
|
const params = {};
|
|
54644
54733
|
if (args.document_id)
|
|
@@ -54668,7 +54757,7 @@ async function handler(supabase, args, ctx) {
|
|
|
54668
54757
|
const lines = entries.map((e) => {
|
|
54669
54758
|
const docLabel = e.doc_title ?? (e.document_id ? e.document_id.slice(0, 8) + "..." : "(deleted)");
|
|
54670
54759
|
const sizeInfo = e.size_before != null && e.size_after != null ? ` | ${e.size_before} -> ${e.size_after} chars` : e.size_after != null ? ` | ${e.size_after} chars` : "";
|
|
54671
|
-
return `${e.created_at
|
|
54760
|
+
return `${utcStamp2(e.created_at)} | ${e.operation} | ${e.author} (${e.author_type}) | ${docLabel}${sizeInfo} | ${e.description}`;
|
|
54672
54761
|
});
|
|
54673
54762
|
return `Audit log (${entries.length} entries, newest first):
|
|
54674
54763
|
|
|
@@ -54984,8 +55073,16 @@ async function handler2(supabase, args, ctx) {
|
|
|
54984
55073
|
const document_id = args.document_id;
|
|
54985
55074
|
const version_id = args.version_id ?? null;
|
|
54986
55075
|
const outline = args.outline ?? false;
|
|
55076
|
+
const section = (args.section ?? "").trim() || null;
|
|
55077
|
+
const section_part = args.section_part ?? undefined;
|
|
54987
55078
|
if (!document_id)
|
|
54988
55079
|
throw new McpInvalidParams("document_id is required");
|
|
55080
|
+
if (section && outline) {
|
|
55081
|
+
throw new McpInvalidParams("Pass either outline (the whole structure) or section (one section's text), not both.");
|
|
55082
|
+
}
|
|
55083
|
+
if (section_part && !section) {
|
|
55084
|
+
throw new McpInvalidParams("section_part only applies together with section.");
|
|
55085
|
+
}
|
|
54989
55086
|
const { data, error: error2 } = await supabase.rpc("cerefox_get_document", {
|
|
54990
55087
|
p_document_id: document_id,
|
|
54991
55088
|
p_version_id: version_id
|
|
@@ -55017,6 +55114,26 @@ async function handler2(supabase, args, ctx) {
|
|
|
55017
55114
|
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
55115
|
}, null, 2);
|
|
55019
55116
|
}
|
|
55117
|
+
if (section) {
|
|
55118
|
+
let extracted;
|
|
55119
|
+
try {
|
|
55120
|
+
extracted = extractSection(row.full_content ?? "", section, section_part);
|
|
55121
|
+
} catch (err) {
|
|
55122
|
+
throw new McpInvalidParams(err instanceof Error ? err.message : String(err));
|
|
55123
|
+
}
|
|
55124
|
+
const archived = version_id !== null;
|
|
55125
|
+
return JSON.stringify({
|
|
55126
|
+
title: row.doc_title ?? "Untitled",
|
|
55127
|
+
heading: extracted.heading,
|
|
55128
|
+
path: extracted.path,
|
|
55129
|
+
level: extracted.level,
|
|
55130
|
+
section_part: extracted.section_part,
|
|
55131
|
+
chars: extracted.chars,
|
|
55132
|
+
content_hash: archived ? null : row.content_hash ?? null,
|
|
55133
|
+
text: extracted.text,
|
|
55134
|
+
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."
|
|
55135
|
+
}, null, 2);
|
|
55136
|
+
}
|
|
55020
55137
|
const label = version_id !== null ? " (archived version)" : " (current)";
|
|
55021
55138
|
const hashLine = row.content_hash ? `content_hash: ${row.content_hash}
|
|
55022
55139
|
|
|
@@ -55030,7 +55147,7 @@ var init_get_document = __esm(() => {
|
|
|
55030
55147
|
init_types3();
|
|
55031
55148
|
getDocumentTool = {
|
|
55032
55149
|
name: "cerefox_get_document",
|
|
55033
|
-
description:
|
|
55150
|
+
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
55151
|
annotations: {
|
|
55035
55152
|
title: "Read document",
|
|
55036
55153
|
readOnlyHint: true,
|
|
@@ -55048,7 +55165,16 @@ var init_get_document = __esm(() => {
|
|
|
55048
55165
|
},
|
|
55049
55166
|
outline: {
|
|
55050
55167
|
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
|
|
55168
|
+
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."
|
|
55169
|
+
},
|
|
55170
|
+
section: {
|
|
55171
|
+
type: "string",
|
|
55172
|
+
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."
|
|
55173
|
+
},
|
|
55174
|
+
section_part: {
|
|
55175
|
+
type: "string",
|
|
55176
|
+
enum: ["own_body", "subtree"],
|
|
55177
|
+
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
55178
|
},
|
|
55053
55179
|
requestor: {
|
|
55054
55180
|
type: "string",
|
|
@@ -55060,12 +55186,347 @@ var init_get_document = __esm(() => {
|
|
|
55060
55186
|
};
|
|
55061
55187
|
});
|
|
55062
55188
|
|
|
55189
|
+
// ../../_shared/mcp-tools/_chunker.ts
|
|
55190
|
+
function normalizeContent(text) {
|
|
55191
|
+
return text.trim().replace(/\r\n/g, `
|
|
55192
|
+
`).replace(/\r/g, `
|
|
55193
|
+
`).replace(/\n{3,}/g, `
|
|
55194
|
+
|
|
55195
|
+
`);
|
|
55196
|
+
}
|
|
55197
|
+
async function sha256hex(text) {
|
|
55198
|
+
const bytes = new TextEncoder().encode(text);
|
|
55199
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
55200
|
+
return Array.from(new Uint8Array(hash)).map((b2) => b2.toString(16).padStart(2, "0")).join("");
|
|
55201
|
+
}
|
|
55202
|
+
var init__chunker = () => {};
|
|
55203
|
+
|
|
55204
|
+
// ../../_shared/mcp-tools/partial-edits.ts
|
|
55205
|
+
function resolveAuthorType2(ctx, args) {
|
|
55206
|
+
if (ctx.accessPath !== "cli")
|
|
55207
|
+
return "agent";
|
|
55208
|
+
return args.author_type === "agent" ? "agent" : "user";
|
|
55209
|
+
}
|
|
55210
|
+
function defaultRequestor(ctx) {
|
|
55211
|
+
return ctx.accessPath === "cli" ? "cli-user" : "mcp-agent";
|
|
55212
|
+
}
|
|
55213
|
+
function touchedTrailingSection(applied) {
|
|
55214
|
+
return applied.some((a) => a.reachedEnd === true);
|
|
55215
|
+
}
|
|
55216
|
+
function shrinkNote(before, afterChars, applied) {
|
|
55217
|
+
const beforeChars = [...before].length;
|
|
55218
|
+
const lost = beforeChars - afterChars;
|
|
55219
|
+
if (lost <= 0)
|
|
55220
|
+
return "";
|
|
55221
|
+
const pct = Math.round(lost / Math.max(beforeChars, 1) * 100);
|
|
55222
|
+
const trailing = touchedTrailingSection(applied);
|
|
55223
|
+
if (pct < 25 && !trailing) {
|
|
55224
|
+
return `This edit removed ${lost} characters. cerefox_list_versions has the previous content.
|
|
55225
|
+
`;
|
|
55226
|
+
}
|
|
55227
|
+
const why = trailing ? `You replaced or deleted the LAST section, and a section runs to the next ` + `heading of the same or higher level — or to the end of the document — so ` + `anything appended after it was inside it. ` : `If you did not intend that, note that a section runs to the next heading ` + `of the same or higher level — or to the end of the document — so replacing ` + `or deleting the LAST section also removes anything appended after it. `;
|
|
55228
|
+
return `⚠ This edit removed ${lost} characters (${pct}% smaller). ${why}cerefox_list_versions has the previous content.
|
|
55229
|
+
`;
|
|
55230
|
+
}
|
|
55231
|
+
function conflictError(documentId, expectedHash, currentHash) {
|
|
55232
|
+
return new Error(`Conflict: document ${documentId} changed since you read it ` + `(your base hash: ${expectedHash}, current hash: ${currentHash}). No write was performed. ` + `To resolve: (1) cerefox_get_document("${documentId}", outline=true) to see the current ` + `structure and hash cheaply, or a full read if you need the text, (2) decide whether your ` + `edit still applies — another writer may have already made it, or made something it ` + `contradicts, (3) retry with expected_content_hash set to the current hash. ` + `These tools have no last-write-wins: the other writer's work is not yours to discard.`);
|
|
55233
|
+
}
|
|
55234
|
+
async function readDocument(supabase, documentId) {
|
|
55235
|
+
const { data, error: error2 } = await supabase.rpc("cerefox_get_document", {
|
|
55236
|
+
p_document_id: documentId,
|
|
55237
|
+
p_version_id: null
|
|
55238
|
+
});
|
|
55239
|
+
if (error2)
|
|
55240
|
+
throw new Error(`Could not read document: ${error2.message}`);
|
|
55241
|
+
const row = data?.[0] ?? undefined;
|
|
55242
|
+
if (!row || row.full_content === undefined) {
|
|
55243
|
+
throw new McpInvalidParams(`Document not found: ${documentId}. Partial edits apply to an existing document; ` + `use cerefox_ingest to create one.`);
|
|
55244
|
+
}
|
|
55245
|
+
return {
|
|
55246
|
+
title: row.doc_title ?? "Untitled",
|
|
55247
|
+
content: row.full_content,
|
|
55248
|
+
hash: row.content_hash ?? ""
|
|
55249
|
+
};
|
|
55250
|
+
}
|
|
55251
|
+
async function applyAndWrite(supabase, ctx, args) {
|
|
55252
|
+
const { documentId, operations, expectedHash, requestor, toolLabel, authorType } = args;
|
|
55253
|
+
if (!ctx.openaiApiKey && resolveEmbedderKind() !== "local") {
|
|
55254
|
+
throw new Error("OpenAI API key not configured. Set OPENAI_API_KEY (Edge Function) or CEREFOX_OPENAI_API_KEY (.env, local).");
|
|
55255
|
+
}
|
|
55256
|
+
const doc = await readDocument(supabase, documentId);
|
|
55257
|
+
if (doc.hash && expectedHash !== doc.hash) {
|
|
55258
|
+
throw conflictError(documentId, expectedHash, doc.hash);
|
|
55259
|
+
}
|
|
55260
|
+
let assembled;
|
|
55261
|
+
let applied;
|
|
55262
|
+
try {
|
|
55263
|
+
const result = applyOperations(doc.content, operations);
|
|
55264
|
+
assembled = result.content;
|
|
55265
|
+
applied = result.applied;
|
|
55266
|
+
} catch (err) {
|
|
55267
|
+
throw new McpInvalidParams(err instanceof Error ? err.message : String(err));
|
|
55268
|
+
}
|
|
55269
|
+
if (assembled === doc.content) {
|
|
55270
|
+
return `No change: the ${toolLabel} produced content identical to the current document ` + `"${doc.title}" (id: ${documentId}). content_hash: ${doc.hash} (unchanged).`;
|
|
55271
|
+
}
|
|
55272
|
+
const newHash = await sha256hex(normalizeContent(assembled));
|
|
55273
|
+
const chunks = chunkMarkdown(assembled);
|
|
55274
|
+
if (chunks.length === 0) {
|
|
55275
|
+
throw new McpInvalidParams("The result of this edit produced no chunks (the document would be empty). " + "To remove a document use cerefox_ingest or the delete path, not a partial edit.");
|
|
55276
|
+
}
|
|
55277
|
+
const texts = chunks.map((c2) => embeddingInputFor(doc.title, c2));
|
|
55278
|
+
const embeddings = await embedBatch(texts, ctx.openaiApiKey ?? "");
|
|
55279
|
+
const totalChars = chunks.reduce((s, c2) => s + c2.char_count, 0);
|
|
55280
|
+
const chunkData = chunks.map((chunk, i) => ({
|
|
55281
|
+
chunk_index: i,
|
|
55282
|
+
heading_path: chunk.heading_path,
|
|
55283
|
+
heading_level: chunk.heading_level,
|
|
55284
|
+
title: chunk.title,
|
|
55285
|
+
content: chunk.content,
|
|
55286
|
+
char_count: chunk.char_count,
|
|
55287
|
+
embedding: embeddings[i],
|
|
55288
|
+
embedder: activeEmbedderName()
|
|
55289
|
+
}));
|
|
55290
|
+
const { data, error: error2 } = await supabase.rpc("cerefox_ingest_document", {
|
|
55291
|
+
p_document_id: documentId,
|
|
55292
|
+
p_title: doc.title,
|
|
55293
|
+
p_source: null,
|
|
55294
|
+
p_content_hash: newHash,
|
|
55295
|
+
p_metadata: null,
|
|
55296
|
+
p_review_status: authorType === "agent" ? "pending_review" : "approved",
|
|
55297
|
+
p_chunks: chunkData,
|
|
55298
|
+
p_author: requestor,
|
|
55299
|
+
p_author_type: authorType,
|
|
55300
|
+
p_source_label: authorType === "user" ? "manual" : "agent",
|
|
55301
|
+
p_expected_content_hash: expectedHash,
|
|
55302
|
+
p_last_write_wins: false,
|
|
55303
|
+
p_content_format: CONTENT_FORMAT_BLIND_STITCH,
|
|
55304
|
+
p_operations: applied.map((a) => ({ op: AUDIT_OP[a.op], detail: a.detail }))
|
|
55305
|
+
});
|
|
55306
|
+
if (error2) {
|
|
55307
|
+
const message = error2.message ?? "";
|
|
55308
|
+
if (message.includes("CEREFOX_CONFLICT")) {
|
|
55309
|
+
const current = message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
|
|
55310
|
+
throw conflictError(documentId, expectedHash, current);
|
|
55311
|
+
}
|
|
55312
|
+
if (message.includes("cerefox_documents_hash_unique")) {
|
|
55313
|
+
throw new Error(`This edit would make "${doc.title}" (id: ${documentId}) byte-identical to another ` + `document in the store, and content must be unique. No write was performed. ` + `Usually this means the two documents have converged and one should be removed or ` + `merged, or that this edit was already applied to the other one — ` + `cerefox_search for the resulting content to find it.`);
|
|
55314
|
+
}
|
|
55315
|
+
if (message.includes("does not exist") && message.includes("cerefox_ingest_document")) {
|
|
55316
|
+
throw new Error(`This server is behind: partial edits need schema 0.11.0 or newer. ` + `Run \`cerefox server deploy\`, then retry. (${message})`);
|
|
55317
|
+
}
|
|
55318
|
+
throw new Error(`Edit failed: ${message}`);
|
|
55319
|
+
}
|
|
55320
|
+
const row = data?.[0] ?? undefined;
|
|
55321
|
+
logUsage(supabase, {
|
|
55322
|
+
operation: toolLabel,
|
|
55323
|
+
accessPath: ctx.accessPath,
|
|
55324
|
+
requestor,
|
|
55325
|
+
document_id: documentId,
|
|
55326
|
+
result_count: applied.length
|
|
55327
|
+
});
|
|
55328
|
+
const summary = applied.map((a, i) => ` ${i + 1}. ${a.detail} → ${a.path}`).join(`
|
|
55329
|
+
`);
|
|
55330
|
+
const warning2 = row?.size_warning ? `
|
|
55331
|
+
|
|
55332
|
+
⚠ This document has passed the configured size threshold ` + `(document_size_warning_chars). Consider splitting it.` : "";
|
|
55333
|
+
return `Applied ${applied.length} operation(s) to "${doc.title}" (id: ${documentId}):
|
|
55334
|
+
${summary}
|
|
55335
|
+
|
|
55336
|
+
` + `New content_hash: ${row?.content_hash ?? newHash}
|
|
55337
|
+
` + `Size: ${row?.total_chars ?? totalChars} chars (was ${doc.content.length}), ${chunks.length} chunk(s).
|
|
55338
|
+
` + shrinkNote(doc.content, row?.total_chars ?? totalChars, applied) + `Pass the new content_hash as expected_content_hash on your next edit.${warning2}`;
|
|
55339
|
+
}
|
|
55340
|
+
async function insertHandler(supabase, args, ctx) {
|
|
55341
|
+
const documentId = args.document_id?.trim();
|
|
55342
|
+
const text = args.text;
|
|
55343
|
+
const position = args.position;
|
|
55344
|
+
const expectedHash = args.expected_content_hash?.trim();
|
|
55345
|
+
if (!documentId)
|
|
55346
|
+
throw new McpInvalidParams("document_id is required");
|
|
55347
|
+
if (!text?.trim())
|
|
55348
|
+
throw new McpInvalidParams("text is required and cannot be empty");
|
|
55349
|
+
if (!expectedHash) {
|
|
55350
|
+
throw new McpInvalidParams("expected_content_hash is required. It is the content_hash of the version you are " + "basing this insert on — returned by cerefox_get_document (including outline mode), " + "cerefox_search, cerefox_metadata_search, and by every write. There is no " + "last-write-wins here: knowing the document changed under you is the point.");
|
|
55351
|
+
}
|
|
55352
|
+
const operations = validateOperations([
|
|
55353
|
+
{
|
|
55354
|
+
op: "insert",
|
|
55355
|
+
text,
|
|
55356
|
+
position,
|
|
55357
|
+
...args.anchor_heading !== undefined ? { anchor_heading: args.anchor_heading } : {},
|
|
55358
|
+
...args.section_part !== undefined ? { section_part: args.section_part } : {}
|
|
55359
|
+
}
|
|
55360
|
+
]);
|
|
55361
|
+
return applyAndWrite(supabase, ctx, {
|
|
55362
|
+
documentId,
|
|
55363
|
+
operations,
|
|
55364
|
+
expectedHash,
|
|
55365
|
+
requestor: args.requestor ?? defaultRequestor(ctx),
|
|
55366
|
+
toolLabel: "insert",
|
|
55367
|
+
authorType: resolveAuthorType2(ctx, args)
|
|
55368
|
+
});
|
|
55369
|
+
}
|
|
55370
|
+
async function editHandler(supabase, args, ctx) {
|
|
55371
|
+
const documentId = args.document_id?.trim();
|
|
55372
|
+
const expectedHash = args.expected_content_hash?.trim();
|
|
55373
|
+
if (!documentId)
|
|
55374
|
+
throw new McpInvalidParams("document_id is required");
|
|
55375
|
+
if (!expectedHash) {
|
|
55376
|
+
throw new McpInvalidParams("expected_content_hash is required. It is the content_hash of the version you are " + "basing these edits on — returned by cerefox_get_document (including outline mode), " + "cerefox_search, cerefox_metadata_search, and by every write.");
|
|
55377
|
+
}
|
|
55378
|
+
let operations;
|
|
55379
|
+
try {
|
|
55380
|
+
operations = validateOperations(args.operations);
|
|
55381
|
+
} catch (err) {
|
|
55382
|
+
throw new McpInvalidParams(err instanceof Error ? err.message : String(err));
|
|
55383
|
+
}
|
|
55384
|
+
return applyAndWrite(supabase, ctx, {
|
|
55385
|
+
documentId,
|
|
55386
|
+
operations,
|
|
55387
|
+
expectedHash,
|
|
55388
|
+
requestor: args.requestor ?? defaultRequestor(ctx),
|
|
55389
|
+
toolLabel: "edit",
|
|
55390
|
+
authorType: resolveAuthorType2(ctx, args)
|
|
55391
|
+
});
|
|
55392
|
+
}
|
|
55393
|
+
var AUDIT_OP, insertTool, editTool;
|
|
55394
|
+
var init_partial_edits2 = __esm(() => {
|
|
55395
|
+
init_partial_edits();
|
|
55396
|
+
init__chunker();
|
|
55397
|
+
init_types3();
|
|
55398
|
+
AUDIT_OP = {
|
|
55399
|
+
insert: "insert",
|
|
55400
|
+
replace_section: "replace-section",
|
|
55401
|
+
delete_section: "delete-section",
|
|
55402
|
+
rename_section: "rename-section"
|
|
55403
|
+
};
|
|
55404
|
+
insertTool = {
|
|
55405
|
+
name: "cerefox_insert",
|
|
55406
|
+
description: "Add text to a document without resending the whole thing. Purely additive: it cannot " + "remove or overwrite existing content, so it is the safe way to append. Positions: " + "end_of_document (a plain append), end_of_section (add to the end of a section's body — " + "the most common mid-document add), after_heading (lead-in text), before_heading (a new " + "block above a section). Anchors are the exact heading line ('## Intake') or a parent path " + "('## Intake > ### Notes') when a heading appears more than once. Requires " + "expected_content_hash; returns the new hash, not the document.",
|
|
55407
|
+
annotations: {
|
|
55408
|
+
title: "Insert into document",
|
|
55409
|
+
readOnlyHint: false,
|
|
55410
|
+
destructiveHint: false,
|
|
55411
|
+
idempotentHint: false,
|
|
55412
|
+
openWorldHint: false
|
|
55413
|
+
},
|
|
55414
|
+
inputSchema: {
|
|
55415
|
+
type: "object",
|
|
55416
|
+
required: ["document_id", "text", "position", "expected_content_hash"],
|
|
55417
|
+
properties: {
|
|
55418
|
+
document_id: { type: "string", description: "UUID of the document to add to" },
|
|
55419
|
+
text: { type: "string", description: "Markdown to insert. Sent as-is; blank-line separation is handled for you." },
|
|
55420
|
+
position: {
|
|
55421
|
+
type: "string",
|
|
55422
|
+
enum: ["end_of_document", "end_of_section", "after_heading", "before_heading"],
|
|
55423
|
+
description: "Where to insert. end_of_document needs no anchor; the other three require anchor_heading."
|
|
55424
|
+
},
|
|
55425
|
+
anchor_heading: {
|
|
55426
|
+
type: "string",
|
|
55427
|
+
description: "Exact heading line, or a ' > ' path for a heading that appears more than once. Required unless position is end_of_document."
|
|
55428
|
+
},
|
|
55429
|
+
section_part: {
|
|
55430
|
+
type: "string",
|
|
55431
|
+
enum: ["own_body", "subtree"],
|
|
55432
|
+
description: "Only for end_of_section when the target section HAS CHILD SECTIONS: own_body = before the first child, subtree = after everything nested under it. These can be far apart, so the tool refuses rather than choosing. Omit it otherwise; you will be told (with both options) whenever it is needed."
|
|
55433
|
+
},
|
|
55434
|
+
expected_content_hash: {
|
|
55435
|
+
type: "string",
|
|
55436
|
+
description: "content_hash of the version you are basing this on. Required — no last-write-wins."
|
|
55437
|
+
},
|
|
55438
|
+
requestor: {
|
|
55439
|
+
type: "string",
|
|
55440
|
+
description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
|
|
55441
|
+
},
|
|
55442
|
+
author_type: {
|
|
55443
|
+
type: "string",
|
|
55444
|
+
enum: ["user", "agent"],
|
|
55445
|
+
description: "Honoured on the CLI only, where a human ran the command; over MCP the write is always recorded as an agent write regardless of what is passed."
|
|
55446
|
+
}
|
|
55447
|
+
}
|
|
55448
|
+
},
|
|
55449
|
+
handler: insertHandler
|
|
55450
|
+
};
|
|
55451
|
+
editTool = {
|
|
55452
|
+
name: "cerefox_edit",
|
|
55453
|
+
description: "Change parts of a document without resending the whole thing: one or many operations " + "applied ATOMICALLY in one write. Operations: insert (same positions as cerefox_insert), " + "replace_section (swap a section's body, heading kept), delete_section (remove a section, " + "scope body_only or heading_and_body), rename_section (change a heading's text, leaving " + "its body and position untouched — for headings that go stale, like a dated one). " + "Use one call for changes that belong together — a " + "half-applied edit is impossible, so a row and the total it feeds cannot disagree. " + "Operations apply in order and each sees the previous one's result. To change a single " + "line, replace_section on its smallest enclosing heading. Requires expected_content_hash; " + "returns the new hash, not the document.",
|
|
55454
|
+
annotations: {
|
|
55455
|
+
title: "Edit document sections",
|
|
55456
|
+
readOnlyHint: false,
|
|
55457
|
+
destructiveHint: true,
|
|
55458
|
+
idempotentHint: false,
|
|
55459
|
+
openWorldHint: false
|
|
55460
|
+
},
|
|
55461
|
+
inputSchema: {
|
|
55462
|
+
type: "object",
|
|
55463
|
+
required: ["document_id", "operations", "expected_content_hash"],
|
|
55464
|
+
properties: {
|
|
55465
|
+
document_id: { type: "string", description: "UUID of the document to edit" },
|
|
55466
|
+
operations: {
|
|
55467
|
+
type: "array",
|
|
55468
|
+
minItems: 1,
|
|
55469
|
+
description: "Operations applied in order, all-or-nothing. If any fails (bad anchor, ambiguity), nothing is written.",
|
|
55470
|
+
items: {
|
|
55471
|
+
type: "object",
|
|
55472
|
+
required: ["op"],
|
|
55473
|
+
properties: {
|
|
55474
|
+
op: {
|
|
55475
|
+
type: "string",
|
|
55476
|
+
enum: ["insert", "replace_section", "delete_section", "rename_section"]
|
|
55477
|
+
},
|
|
55478
|
+
text: { type: "string", description: "Markdown. Required for insert and replace_section." },
|
|
55479
|
+
position: {
|
|
55480
|
+
type: "string",
|
|
55481
|
+
enum: ["end_of_document", "end_of_section", "after_heading", "before_heading"],
|
|
55482
|
+
description: "Required for insert."
|
|
55483
|
+
},
|
|
55484
|
+
anchor_heading: {
|
|
55485
|
+
type: "string",
|
|
55486
|
+
description: "Exact heading line, or a ' > ' path when the heading is not unique. Required for replace_section, delete_section, and any insert other than end_of_document."
|
|
55487
|
+
},
|
|
55488
|
+
section_part: {
|
|
55489
|
+
type: "string",
|
|
55490
|
+
enum: ["own_body", "subtree"],
|
|
55491
|
+
description: "Only when the target section has child sections. You will be told (with both options) whenever it is needed."
|
|
55492
|
+
},
|
|
55493
|
+
scope: {
|
|
55494
|
+
type: "string",
|
|
55495
|
+
enum: ["body_only", "heading_and_body"],
|
|
55496
|
+
description: "delete_section only. Defaults to body_only, which keeps the heading."
|
|
55497
|
+
},
|
|
55498
|
+
new_heading: {
|
|
55499
|
+
type: "string",
|
|
55500
|
+
description: "rename_section only: the replacement heading LINE, at the same level (## stays ##). Changes the heading text and nothing else — the body and the section's position are untouched, which is the point: renaming via delete + insert would risk both. Use it for headings that go stale, like '## OPEN TODOs (as of 2026-08-08)'. A rename changes the anchor, so a later operation in the same call must target the NEW heading."
|
|
55501
|
+
}
|
|
55502
|
+
}
|
|
55503
|
+
}
|
|
55504
|
+
},
|
|
55505
|
+
expected_content_hash: {
|
|
55506
|
+
type: "string",
|
|
55507
|
+
description: "content_hash of the version you are basing these edits on. Required — no last-write-wins."
|
|
55508
|
+
},
|
|
55509
|
+
requestor: {
|
|
55510
|
+
type: "string",
|
|
55511
|
+
description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
|
|
55512
|
+
},
|
|
55513
|
+
author_type: {
|
|
55514
|
+
type: "string",
|
|
55515
|
+
enum: ["user", "agent"],
|
|
55516
|
+
description: "Honoured on the CLI only, where a human ran the command; over MCP the write is always recorded as an agent write regardless of what is passed."
|
|
55517
|
+
}
|
|
55518
|
+
}
|
|
55519
|
+
},
|
|
55520
|
+
handler: editHandler
|
|
55521
|
+
};
|
|
55522
|
+
});
|
|
55523
|
+
|
|
55063
55524
|
// ../../_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;
|
|
55525
|
+
var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **16 MCP tools** (15 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for "How AI Agents Use Cerefox" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`/`rename_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash, or with `section: "## Heading"` one section\'s text | `document_id` (required), `outline`, `section`, `section_part` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project\'s docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc\'s project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.\n\n## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Look before you overwrite** — `cerefox_get_document(document_id,\n section: "## Heading")` returns exactly the text a `replace_section` on that\n anchor would destroy. The outline gives you a section\'s *size*, never its\n *text*, so on a document you did not write yourself this is the difference\n between a replace and a blind overwrite.\n4. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround. To fix a stale heading (`## OPEN TODOs (as of ...)`), use\n `rename_section`: it changes the heading text and leaves the body and\n position alone.\n5. All of them require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**A section runs to the next same-or-higher heading, or to the end of the\ndocument.** So `end_of_document` inserts land inside the *last* section, and\nreplacing or deleting that section removes them too. A large shrink in the\nresponse is your warning; `cerefox_list_versions` has the previous content.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., "Claude Code", "archiver"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it\'s stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer\'s work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch("topic") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title="Same Title", content="...", document_id="abc123",\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch("topic") -> find doc (note its hash) -> modify ->\ningest(title="Same Title", content="...", update_if_exists=true,\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search "<q>" --requestor "<your-name>"` |\n| `cerefox_ingest` (paste) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --author "<your-name>" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --document-id "<uuid>" --expected-content-hash "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor "<your-name>"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor "<your-name>"` |\n| `cerefox_list_projects` | `cerefox project list --requestor "<your-name>"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter \'<json>\' --requestor "<your-name>"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author "<your-name>" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor "<your-name>"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author "<your-name>" --author-type agent`\n- Reads: `--requestor "<your-name>"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n\n## Timestamps are UTC\n\nEvery timestamp Cerefox returns — `created_at` on audit entries, version\nhistory, document metadata — is **UTC**, and now carries its `Z` marker so it\ncannot be mistaken for local time.\n\n**When you write a date into a document\'s CONTENT, use your own clock, not a\nCerefox timestamp.** These are different things: a timestamp records when the\nserver stored something; a date in a log entry or a heading is authored content\nand belongs to your timezone. An agent working a Pacific afternoon read\n`2026-08-11` from version history, wrote "8/11" into its entries, and put a\nday\'s work in the future — the timestamp was correct, and copying it into\ncontent was not.\n\nCerefox deliberately does not convert to local time on the API or MCP paths.\n"Local" has no server-side meaning: the remote MCP server runs in a cloud\nfunction whose local time *is* UTC, while a local MCP server runs in yours, so\nthe same document would report two different times depending on transport. The\nweb UI converts because a browser knows the viewer\'s timezone; nothing\nserver-side does.\n\n## Mistakes that have actually happened\n\nEach of these comes from a real agent session, and each is easy to make.\n\n- **`cerefox_ingest` always replaces the ENTIRE document.** Never a section.\n Before sending, check that the tool name matches the intent: if the intent is\n "change one section", the call is `cerefox_edit` with `replace_section`. A\n section-sized edit sent as a full ingest truncated a 13,000-character index to\n a single word. It was recovered from version history within the minute, but\n only because it was noticed immediately.\n\n- **Do not include the anchor\'s own heading in your text.** `replace_section`\n keeps the heading and `insert` places your text inside the section, so\n including it produces two. This is now refused rather than silently applied,\n but the shape is worth knowing: it happened twice in one session, the second\n time while trying to repair the first. A *deeper* sub-heading inside your text\n is fine.\n\n- **Content between sections belongs to the section ABOVE it.** A section runs\n to the next heading of the same or higher level, so a `---` rule, a note, or\n any trailing text sitting just above the next heading is part of the section\n before it — even when it visually reads as belonging below. Replacing that\n section takes it too. An agent hit exactly this: a `---` that separated two\n major sections disappeared when the section above it was replaced. The write\n was correct by the addressing rules; the surprise is that "the end of this\n section" is further down the page than it looks. Note the loss warning will\n not catch it if your replacement text is longer than what it replaced, since\n there is then no net loss to report.\n\n- **Never partial-edit to fix a partial edit.** If a write leaves unexpected\n structure, stop. Use `cerefox_list_versions`, retrieve the last good version,\n and re-ingest cleanly. Repairing edits with more edits compounds the damage.\n\n- **A rejected batch is safe.** Operations in one `cerefox_edit` are\n all-or-nothing: if any is invalid, nothing is written. A refusal costs you a\n retry, not data — so prefer one call for changes that belong together, and do\n not split a batch to "make it more likely to succeed".\n\n- **Read before replacing.** `cerefox_get_document(section: "## Heading")`\n returns exactly what a `replace_section` on that anchor would overwrite. Use it\n for any section you did not write in this session. The outline gives a\n section\'s *size*, never its *text*.\n\n- **Verify after writing** — read the result back before reporting success, and\n report what the read actually shows.\n\n- **Partial edits cannot change a document\'s stored TITLE.** `rename_section`\n changes a heading inside the content; the title is a separate field and still\n needs `cerefox_ingest`.\n\n- **If a capability seems missing from one server, suspect your client first.**\n Local and remote run the same code. Call `cerefox_get_help(topic: "server")`:\n it reports the server\'s own version and the operations it registers. If that\n disagrees with your tool list, the client is holding a list it fetched before\n an upgrade — clients cache it at connect time. Ask the user to restart the\n client. Do not record a capability difference between servers as a fact; every\n such report so far has been a stale client.\n', HELP_SECTIONS, HELP_SECTION_HEADINGS;
|
|
55065
55526
|
var init_get_help_content = __esm(() => {
|
|
55066
55527
|
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.\
|
|
55528
|
+
Tools: "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`/`rename_section`), `expected_content_hash` (required) |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash, or with `section: \"## Heading\"` one section's text | `document_id` (required), `outline`, `section`, `section_part` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.",
|
|
55529
|
+
"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
55530
|
"Essential Rules": '## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., "Claude Code", "archiver"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. If it\'s stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer\'s work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.',
|
|
55070
55531
|
"Update Workflow (ID-based -- preferred)": `## Update Workflow (ID-based -- preferred)
|
|
55071
55532
|
|
|
@@ -55078,12 +55539,110 @@ ingest(title="Same Title", content="...", document_id="abc123",
|
|
|
55078
55539
|
On a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.`,
|
|
55079
55540
|
"Update Workflow (title-based -- fallback)": '## Update Workflow (title-based -- fallback)\n\n```\nsearch("topic") -> find doc (note its hash) -> modify ->\ningest(title="Same Title", content="...", update_if_exists=true,\n expected_content_hash="<the hash you read>", author="my-agent")\n```',
|
|
55080
55541
|
"Catch-Up Workflow": '## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```',
|
|
55081
|
-
"CLI fallback (when MCP is unavailable)": '## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search "<q>" --requestor "<your-name>"` |\n| `cerefox_ingest` (paste) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --author "<your-name>" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --document-id "<uuid>" --expected-content-hash "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor "<your-name>"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor "<your-name>"` |\n| `cerefox_list_projects` | `cerefox project list --requestor "<your-name>"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter \'<json>\' --requestor "<your-name>"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author "<your-name>" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor "<your-name>"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author "<your-name>" --author-type agent`\n- Reads: `--requestor "<your-name>"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.'
|
|
55082
|
-
|
|
55083
|
-
|
|
55542
|
+
"CLI fallback (when MCP is unavailable)": '## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search "<q>" --requestor "<your-name>"` |\n| `cerefox_ingest` (paste) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --author "<your-name>" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --document-id "<uuid>" --expected-content-hash "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor "<your-name>"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor "<your-name>"` |\n| `cerefox_list_projects` | `cerefox project list --requestor "<your-name>"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` ⚑ | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` ⚑ | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` ⚑ | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` ⚑ | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter \'<json>\' --requestor "<your-name>"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author "<your-name>" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor "<your-name>"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author "<your-name>" --author-type agent`\n- Reads: `--requestor "<your-name>"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.',
|
|
55543
|
+
"Timestamps are UTC": `## Timestamps are UTC
|
|
55544
|
+
|
|
55545
|
+
Every timestamp Cerefox returns — \`created_at\` on audit entries, version
|
|
55546
|
+
history, document metadata — is **UTC**, and now carries its \`Z\` marker so it
|
|
55547
|
+
cannot be mistaken for local time.
|
|
55548
|
+
|
|
55549
|
+
**When you write a date into a document's CONTENT, use your own clock, not a
|
|
55550
|
+
Cerefox timestamp.** These are different things: a timestamp records when the
|
|
55551
|
+
server stored something; a date in a log entry or a heading is authored content
|
|
55552
|
+
and belongs to your timezone. An agent working a Pacific afternoon read
|
|
55553
|
+
\`2026-08-11\` from version history, wrote "8/11" into its entries, and put a
|
|
55554
|
+
day's work in the future — the timestamp was correct, and copying it into
|
|
55555
|
+
content was not.
|
|
55556
|
+
|
|
55557
|
+
Cerefox deliberately does not convert to local time on the API or MCP paths.
|
|
55558
|
+
"Local" has no server-side meaning: the remote MCP server runs in a cloud
|
|
55559
|
+
function whose local time *is* UTC, while a local MCP server runs in yours, so
|
|
55560
|
+
the same document would report two different times depending on transport. The
|
|
55561
|
+
web UI converts because a browser knows the viewer's timezone; nothing
|
|
55562
|
+
server-side does.`,
|
|
55563
|
+
"Mistakes that have actually happened": `## Mistakes that have actually happened
|
|
55564
|
+
|
|
55565
|
+
Each of these comes from a real agent session, and each is easy to make.
|
|
55566
|
+
|
|
55567
|
+
- **\`cerefox_ingest\` always replaces the ENTIRE document.** Never a section.
|
|
55568
|
+
Before sending, check that the tool name matches the intent: if the intent is
|
|
55569
|
+
"change one section", the call is \`cerefox_edit\` with \`replace_section\`. A
|
|
55570
|
+
section-sized edit sent as a full ingest truncated a 13,000-character index to
|
|
55571
|
+
a single word. It was recovered from version history within the minute, but
|
|
55572
|
+
only because it was noticed immediately.
|
|
55573
|
+
|
|
55574
|
+
- **Do not include the anchor's own heading in your text.** \`replace_section\`
|
|
55575
|
+
keeps the heading and \`insert\` places your text inside the section, so
|
|
55576
|
+
including it produces two. This is now refused rather than silently applied,
|
|
55577
|
+
but the shape is worth knowing: it happened twice in one session, the second
|
|
55578
|
+
time while trying to repair the first. A *deeper* sub-heading inside your text
|
|
55579
|
+
is fine.
|
|
55580
|
+
|
|
55581
|
+
- **Content between sections belongs to the section ABOVE it.** A section runs
|
|
55582
|
+
to the next heading of the same or higher level, so a \`---\` rule, a note, or
|
|
55583
|
+
any trailing text sitting just above the next heading is part of the section
|
|
55584
|
+
before it — even when it visually reads as belonging below. Replacing that
|
|
55585
|
+
section takes it too. An agent hit exactly this: a \`---\` that separated two
|
|
55586
|
+
major sections disappeared when the section above it was replaced. The write
|
|
55587
|
+
was correct by the addressing rules; the surprise is that "the end of this
|
|
55588
|
+
section" is further down the page than it looks. Note the loss warning will
|
|
55589
|
+
not catch it if your replacement text is longer than what it replaced, since
|
|
55590
|
+
there is then no net loss to report.
|
|
55591
|
+
|
|
55592
|
+
- **Never partial-edit to fix a partial edit.** If a write leaves unexpected
|
|
55593
|
+
structure, stop. Use \`cerefox_list_versions\`, retrieve the last good version,
|
|
55594
|
+
and re-ingest cleanly. Repairing edits with more edits compounds the damage.
|
|
55595
|
+
|
|
55596
|
+
- **A rejected batch is safe.** Operations in one \`cerefox_edit\` are
|
|
55597
|
+
all-or-nothing: if any is invalid, nothing is written. A refusal costs you a
|
|
55598
|
+
retry, not data — so prefer one call for changes that belong together, and do
|
|
55599
|
+
not split a batch to "make it more likely to succeed".
|
|
55600
|
+
|
|
55601
|
+
- **Read before replacing.** \`cerefox_get_document(section: "## Heading")\`
|
|
55602
|
+
returns exactly what a \`replace_section\` on that anchor would overwrite. Use it
|
|
55603
|
+
for any section you did not write in this session. The outline gives a
|
|
55604
|
+
section's *size*, never its *text*.
|
|
55605
|
+
|
|
55606
|
+
- **Verify after writing** — read the result back before reporting success, and
|
|
55607
|
+
report what the read actually shows.
|
|
55608
|
+
|
|
55609
|
+
- **Partial edits cannot change a document's stored TITLE.** \`rename_section\`
|
|
55610
|
+
changes a heading inside the content; the title is a separate field and still
|
|
55611
|
+
needs \`cerefox_ingest\`.
|
|
55612
|
+
|
|
55613
|
+
- **If a capability seems missing from one server, suspect your client first.**
|
|
55614
|
+
Local and remote run the same code. Call \`cerefox_get_help(topic: "server")\`:
|
|
55615
|
+
it reports the server's own version and the operations it registers. If that
|
|
55616
|
+
disagrees with your tool list, the client is holding a list it fetched before
|
|
55617
|
+
an upgrade — clients cache it at connect time. Ask the user to restart the
|
|
55618
|
+
client. Do not record a capability difference between servers as a fact; every
|
|
55619
|
+
such report so far has been a stale client.`
|
|
55620
|
+
};
|
|
55621
|
+
HELP_SECTION_HEADINGS = ["Tools", "Editing part of a document (prefer this over re-sending)", "Essential Rules", "Update Workflow (ID-based -- preferred)", "Update Workflow (title-based -- fallback)", "Catch-Up Workflow", "CLI fallback (when MCP is unavailable)", "Timestamps are UTC", "Mistakes that have actually happened"];
|
|
55084
55622
|
});
|
|
55085
55623
|
|
|
55086
55624
|
// ../../_shared/mcp-tools/get-help.ts
|
|
55625
|
+
function editOperations() {
|
|
55626
|
+
const schema = editTool.inputSchema;
|
|
55627
|
+
return schema.properties?.operations?.items?.properties?.op?.enum ?? [];
|
|
55628
|
+
}
|
|
55629
|
+
function serverIdentity() {
|
|
55630
|
+
return [
|
|
55631
|
+
"## This server",
|
|
55632
|
+
"",
|
|
55633
|
+
`- **Version**: ${CEREFOX_VERSION}`,
|
|
55634
|
+
`- **cerefox_edit operations**: ${editOperations().join(", ")}`,
|
|
55635
|
+
"",
|
|
55636
|
+
"**If your tool list disagrees with this block, your CLIENT is out of date, not the server.**",
|
|
55637
|
+
"MCP clients fetch the tool list once when they connect and cache it, so a server",
|
|
55638
|
+
"upgraded mid-session is invisible until the client reconnects. Ask the user to restart",
|
|
55639
|
+
"the client — and if it stays missing after a restart, the client config may pin an old",
|
|
55640
|
+
"version of the package. Do not record a capability difference between the local and",
|
|
55641
|
+
"remote servers: they run the same code, and every such report so far has been a stale",
|
|
55642
|
+
"client."
|
|
55643
|
+
].join(`
|
|
55644
|
+
`);
|
|
55645
|
+
}
|
|
55087
55646
|
async function handler3(supabase, args, ctx) {
|
|
55088
55647
|
const topic = args.topic?.trim();
|
|
55089
55648
|
logUsage(supabase, {
|
|
@@ -55096,7 +55655,11 @@ async function handler3(supabase, args, ctx) {
|
|
|
55096
55655
|
if (!topic) {
|
|
55097
55656
|
const idx = HELP_SECTION_HEADINGS.map((h) => ` - ${h}`).join(`
|
|
55098
55657
|
`);
|
|
55099
|
-
return
|
|
55658
|
+
return serverIdentity() + `
|
|
55659
|
+
|
|
55660
|
+
---
|
|
55661
|
+
|
|
55662
|
+
` + HELP_FULL + `
|
|
55100
55663
|
|
|
55101
55664
|
---
|
|
55102
55665
|
|
|
@@ -55106,6 +55669,8 @@ async function handler3(supabase, args, ctx) {
|
|
|
55106
55669
|
|
|
55107
55670
|
(Topic match is case-insensitive substring on the headings above.)`;
|
|
55108
55671
|
}
|
|
55672
|
+
if (/^(server|version|stale|client)$/i.test(topic))
|
|
55673
|
+
return serverIdentity();
|
|
55109
55674
|
const t = topic.toLowerCase();
|
|
55110
55675
|
const matched = HELP_SECTION_HEADINGS.filter((h) => h.toLowerCase().includes(t));
|
|
55111
55676
|
if (matched.length === 0) {
|
|
@@ -55123,6 +55688,8 @@ async function handler3(supabase, args, ctx) {
|
|
|
55123
55688
|
}
|
|
55124
55689
|
var getHelpTool;
|
|
55125
55690
|
var init_get_help = __esm(() => {
|
|
55691
|
+
init_ef_meta();
|
|
55692
|
+
init_partial_edits2();
|
|
55126
55693
|
init_get_help_content();
|
|
55127
55694
|
getHelpTool = {
|
|
55128
55695
|
name: "cerefox_get_help",
|
|
@@ -55150,30 +55717,15 @@ var init_get_help = __esm(() => {
|
|
|
55150
55717
|
};
|
|
55151
55718
|
});
|
|
55152
55719
|
|
|
55153
|
-
// ../../_shared/mcp-tools/_chunker.ts
|
|
55154
|
-
function normalizeContent(text) {
|
|
55155
|
-
return text.trim().replace(/\r\n/g, `
|
|
55156
|
-
`).replace(/\r/g, `
|
|
55157
|
-
`).replace(/\n{3,}/g, `
|
|
55158
|
-
|
|
55159
|
-
`);
|
|
55160
|
-
}
|
|
55161
|
-
async function sha256hex(text) {
|
|
55162
|
-
const bytes = new TextEncoder().encode(text);
|
|
55163
|
-
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
55164
|
-
return Array.from(new Uint8Array(hash)).map((b2) => b2.toString(16).padStart(2, "0")).join("");
|
|
55165
|
-
}
|
|
55166
|
-
var init__chunker = () => {};
|
|
55167
|
-
|
|
55168
55720
|
// ../../_shared/mcp-tools/ingest.ts
|
|
55169
|
-
function
|
|
55721
|
+
function conflictError2(documentId, expectedHash, currentHash) {
|
|
55170
55722
|
return new Error(`Conflict: document ${documentId} changed since you read it ` + `(your base hash: ${expectedHash}, current hash: ${currentHash}). ` + `To resolve: (1) cerefox_get_document("${documentId}") to fetch the latest content ` + `and its content_hash, (2) merge your changes into it, (3) retry cerefox_ingest ` + `with expected_content_hash set to the new hash. Do not overwrite blindly — ` + `the current content may include another writer's work.`);
|
|
55171
55723
|
}
|
|
55172
55724
|
function mapIngestRpcError(message, documentId) {
|
|
55173
55725
|
if (message.includes("CEREFOX_CONFLICT")) {
|
|
55174
55726
|
const current = message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
|
|
55175
55727
|
const expected = message.match(/expected hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
|
|
55176
|
-
return
|
|
55728
|
+
return conflictError2(documentId, expected, current);
|
|
55177
55729
|
}
|
|
55178
55730
|
if (message.includes("CEREFOX_TOKEN_REQUIRED")) {
|
|
55179
55731
|
const current = message.match(/Current hash: ([0-9a-f]{64})/)?.[1];
|
|
@@ -55220,7 +55772,7 @@ async function handler4(supabase, args, ctx) {
|
|
|
55220
55772
|
return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged (${contentHash2}).${note2}`;
|
|
55221
55773
|
}
|
|
55222
55774
|
if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
|
|
55223
|
-
throw
|
|
55775
|
+
throw conflictError2(existingDoc.id, expected_content_hash, existingDoc.content_hash);
|
|
55224
55776
|
}
|
|
55225
55777
|
const chunks2 = chunkMarkdown(content);
|
|
55226
55778
|
if (chunks2.length === 0)
|
|
@@ -55278,7 +55830,7 @@ async function handler4(supabase, args, ctx) {
|
|
|
55278
55830
|
return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged (${contentHash2}).`;
|
|
55279
55831
|
}
|
|
55280
55832
|
if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
|
|
55281
|
-
throw
|
|
55833
|
+
throw conflictError2(existingDoc.id, expected_content_hash, existingDoc.content_hash);
|
|
55282
55834
|
}
|
|
55283
55835
|
const chunks2 = chunkMarkdown(content);
|
|
55284
55836
|
if (chunks2.length === 0)
|
|
@@ -55386,7 +55938,7 @@ var init_ingest = __esm(() => {
|
|
|
55386
55938
|
init_types3();
|
|
55387
55939
|
ingestTool = {
|
|
55388
55940
|
name: "cerefox_ingest",
|
|
55389
|
-
description: "Save a note or document to the Cerefox knowledge base.",
|
|
55941
|
+
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
55942
|
annotations: {
|
|
55391
55943
|
title: "Save or update a document",
|
|
55392
55944
|
readOnlyHint: false,
|
|
@@ -55437,310 +55989,6 @@ var init_ingest = __esm(() => {
|
|
|
55437
55989
|
};
|
|
55438
55990
|
});
|
|
55439
55991
|
|
|
55440
|
-
// ../../_shared/mcp-tools/partial-edits.ts
|
|
55441
|
-
function resolveAuthorType2(ctx, args) {
|
|
55442
|
-
if (ctx.accessPath !== "cli")
|
|
55443
|
-
return "agent";
|
|
55444
|
-
return args.author_type === "agent" ? "agent" : "user";
|
|
55445
|
-
}
|
|
55446
|
-
function defaultRequestor(ctx) {
|
|
55447
|
-
return ctx.accessPath === "cli" ? "cli-user" : "mcp-agent";
|
|
55448
|
-
}
|
|
55449
|
-
function shrinkNote(before, after) {
|
|
55450
|
-
const lost = before - after;
|
|
55451
|
-
if (lost <= 0)
|
|
55452
|
-
return "";
|
|
55453
|
-
const pct = Math.round(lost / Math.max(before, 1) * 100);
|
|
55454
|
-
if (pct < 25)
|
|
55455
|
-
return "";
|
|
55456
|
-
return `⚠ This edit removed ${lost} characters (${pct}% smaller). If you did not ` + `intend that, note that a section runs to the next heading of the same or ` + `higher level — or to the end of the document — so replacing or deleting ` + `the LAST section also removes anything appended after it. ` + `cerefox_list_versions has the previous content.
|
|
55457
|
-
`;
|
|
55458
|
-
}
|
|
55459
|
-
function conflictError2(documentId, expectedHash, currentHash) {
|
|
55460
|
-
return new Error(`Conflict: document ${documentId} changed since you read it ` + `(your base hash: ${expectedHash}, current hash: ${currentHash}). No write was performed. ` + `To resolve: (1) cerefox_get_document("${documentId}", outline=true) to see the current ` + `structure and hash cheaply, or a full read if you need the text, (2) decide whether your ` + `edit still applies — another writer may have already made it, or made something it ` + `contradicts, (3) retry with expected_content_hash set to the current hash. ` + `These tools have no last-write-wins: the other writer's work is not yours to discard.`);
|
|
55461
|
-
}
|
|
55462
|
-
async function readDocument(supabase, documentId) {
|
|
55463
|
-
const { data, error: error2 } = await supabase.rpc("cerefox_get_document", {
|
|
55464
|
-
p_document_id: documentId,
|
|
55465
|
-
p_version_id: null
|
|
55466
|
-
});
|
|
55467
|
-
if (error2)
|
|
55468
|
-
throw new Error(`Could not read document: ${error2.message}`);
|
|
55469
|
-
const row = data?.[0] ?? undefined;
|
|
55470
|
-
if (!row || row.full_content === undefined) {
|
|
55471
|
-
throw new McpInvalidParams(`Document not found: ${documentId}. Partial edits apply to an existing document; ` + `use cerefox_ingest to create one.`);
|
|
55472
|
-
}
|
|
55473
|
-
return {
|
|
55474
|
-
title: row.doc_title ?? "Untitled",
|
|
55475
|
-
content: row.full_content,
|
|
55476
|
-
hash: row.content_hash ?? ""
|
|
55477
|
-
};
|
|
55478
|
-
}
|
|
55479
|
-
async function applyAndWrite(supabase, ctx, args) {
|
|
55480
|
-
const { documentId, operations, expectedHash, requestor, toolLabel, authorType } = args;
|
|
55481
|
-
if (!ctx.openaiApiKey && resolveEmbedderKind() !== "local") {
|
|
55482
|
-
throw new Error("OpenAI API key not configured. Set OPENAI_API_KEY (Edge Function) or CEREFOX_OPENAI_API_KEY (.env, local).");
|
|
55483
|
-
}
|
|
55484
|
-
const doc = await readDocument(supabase, documentId);
|
|
55485
|
-
if (doc.hash && expectedHash !== doc.hash) {
|
|
55486
|
-
throw conflictError2(documentId, expectedHash, doc.hash);
|
|
55487
|
-
}
|
|
55488
|
-
let assembled;
|
|
55489
|
-
let applied;
|
|
55490
|
-
try {
|
|
55491
|
-
const result = applyOperations(doc.content, operations);
|
|
55492
|
-
assembled = result.content;
|
|
55493
|
-
applied = result.applied;
|
|
55494
|
-
} catch (err) {
|
|
55495
|
-
throw new McpInvalidParams(err instanceof Error ? err.message : String(err));
|
|
55496
|
-
}
|
|
55497
|
-
if (assembled === doc.content) {
|
|
55498
|
-
return `No change: the ${toolLabel} produced content identical to the current document ` + `"${doc.title}" (id: ${documentId}). content_hash: ${doc.hash} (unchanged).`;
|
|
55499
|
-
}
|
|
55500
|
-
const newHash = await sha256hex(normalizeContent(assembled));
|
|
55501
|
-
const chunks = chunkMarkdown(assembled);
|
|
55502
|
-
if (chunks.length === 0) {
|
|
55503
|
-
throw new McpInvalidParams("The result of this edit produced no chunks (the document would be empty). " + "To remove a document use cerefox_ingest or the delete path, not a partial edit.");
|
|
55504
|
-
}
|
|
55505
|
-
const texts = chunks.map((c2) => embeddingInputFor(doc.title, c2));
|
|
55506
|
-
const embeddings = await embedBatch(texts, ctx.openaiApiKey ?? "");
|
|
55507
|
-
const totalChars = chunks.reduce((s, c2) => s + c2.char_count, 0);
|
|
55508
|
-
const chunkData = chunks.map((chunk, i) => ({
|
|
55509
|
-
chunk_index: i,
|
|
55510
|
-
heading_path: chunk.heading_path,
|
|
55511
|
-
heading_level: chunk.heading_level,
|
|
55512
|
-
title: chunk.title,
|
|
55513
|
-
content: chunk.content,
|
|
55514
|
-
char_count: chunk.char_count,
|
|
55515
|
-
embedding: embeddings[i],
|
|
55516
|
-
embedder: activeEmbedderName()
|
|
55517
|
-
}));
|
|
55518
|
-
const { data, error: error2 } = await supabase.rpc("cerefox_ingest_document", {
|
|
55519
|
-
p_document_id: documentId,
|
|
55520
|
-
p_title: doc.title,
|
|
55521
|
-
p_source: null,
|
|
55522
|
-
p_content_hash: newHash,
|
|
55523
|
-
p_metadata: null,
|
|
55524
|
-
p_review_status: authorType === "agent" ? "pending_review" : "approved",
|
|
55525
|
-
p_chunks: chunkData,
|
|
55526
|
-
p_author: requestor,
|
|
55527
|
-
p_author_type: authorType,
|
|
55528
|
-
p_source_label: authorType === "user" ? "manual" : "agent",
|
|
55529
|
-
p_expected_content_hash: expectedHash,
|
|
55530
|
-
p_last_write_wins: false,
|
|
55531
|
-
p_content_format: CONTENT_FORMAT_BLIND_STITCH,
|
|
55532
|
-
p_operations: applied.map((a) => ({ op: AUDIT_OP[a.op], detail: a.detail }))
|
|
55533
|
-
});
|
|
55534
|
-
if (error2) {
|
|
55535
|
-
const message = error2.message ?? "";
|
|
55536
|
-
if (message.includes("CEREFOX_CONFLICT")) {
|
|
55537
|
-
const current = message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
|
|
55538
|
-
throw conflictError2(documentId, expectedHash, current);
|
|
55539
|
-
}
|
|
55540
|
-
if (message.includes("cerefox_documents_hash_unique")) {
|
|
55541
|
-
throw new Error(`This edit would make "${doc.title}" (id: ${documentId}) byte-identical to another ` + `document in the store, and content must be unique. No write was performed. ` + `Usually this means the two documents have converged and one should be removed or ` + `merged, or that this edit was already applied to the other one — ` + `cerefox_search for the resulting content to find it.`);
|
|
55542
|
-
}
|
|
55543
|
-
if (message.includes("does not exist") && message.includes("cerefox_ingest_document")) {
|
|
55544
|
-
throw new Error(`This server is behind: partial edits need schema 0.11.0 or newer. ` + `Run \`cerefox server deploy\`, then retry. (${message})`);
|
|
55545
|
-
}
|
|
55546
|
-
throw new Error(`Edit failed: ${message}`);
|
|
55547
|
-
}
|
|
55548
|
-
const row = data?.[0] ?? undefined;
|
|
55549
|
-
logUsage(supabase, {
|
|
55550
|
-
operation: toolLabel,
|
|
55551
|
-
accessPath: ctx.accessPath,
|
|
55552
|
-
requestor,
|
|
55553
|
-
document_id: documentId,
|
|
55554
|
-
result_count: applied.length
|
|
55555
|
-
});
|
|
55556
|
-
const summary = applied.map((a, i) => ` ${i + 1}. ${a.detail} → ${a.path}`).join(`
|
|
55557
|
-
`);
|
|
55558
|
-
const warning2 = row?.size_warning ? `
|
|
55559
|
-
|
|
55560
|
-
⚠ This document has passed the configured size threshold ` + `(document_size_warning_chars). Consider splitting it.` : "";
|
|
55561
|
-
return `Applied ${applied.length} operation(s) to "${doc.title}" (id: ${documentId}):
|
|
55562
|
-
${summary}
|
|
55563
|
-
|
|
55564
|
-
` + `New content_hash: ${row?.content_hash ?? newHash}
|
|
55565
|
-
` + `Size: ${row?.total_chars ?? totalChars} chars (was ${doc.content.length}), ${chunks.length} chunk(s).
|
|
55566
|
-
` + shrinkNote(doc.content.length, row?.total_chars ?? totalChars) + `Pass the new content_hash as expected_content_hash on your next edit.${warning2}`;
|
|
55567
|
-
}
|
|
55568
|
-
async function insertHandler(supabase, args, ctx) {
|
|
55569
|
-
const documentId = args.document_id?.trim();
|
|
55570
|
-
const text = args.text;
|
|
55571
|
-
const position = args.position;
|
|
55572
|
-
const expectedHash = args.expected_content_hash?.trim();
|
|
55573
|
-
if (!documentId)
|
|
55574
|
-
throw new McpInvalidParams("document_id is required");
|
|
55575
|
-
if (!text?.trim())
|
|
55576
|
-
throw new McpInvalidParams("text is required and cannot be empty");
|
|
55577
|
-
if (!expectedHash) {
|
|
55578
|
-
throw new McpInvalidParams("expected_content_hash is required. It is the content_hash of the version you are " + "basing this insert on — returned by cerefox_get_document (including outline mode), " + "cerefox_search, cerefox_metadata_search, and by every write. There is no " + "last-write-wins here: knowing the document changed under you is the point.");
|
|
55579
|
-
}
|
|
55580
|
-
const operations = validateOperations([
|
|
55581
|
-
{
|
|
55582
|
-
op: "insert",
|
|
55583
|
-
text,
|
|
55584
|
-
position,
|
|
55585
|
-
...args.anchor_heading !== undefined ? { anchor_heading: args.anchor_heading } : {},
|
|
55586
|
-
...args.section_part !== undefined ? { section_part: args.section_part } : {}
|
|
55587
|
-
}
|
|
55588
|
-
]);
|
|
55589
|
-
return applyAndWrite(supabase, ctx, {
|
|
55590
|
-
documentId,
|
|
55591
|
-
operations,
|
|
55592
|
-
expectedHash,
|
|
55593
|
-
requestor: args.requestor ?? defaultRequestor(ctx),
|
|
55594
|
-
toolLabel: "insert",
|
|
55595
|
-
authorType: resolveAuthorType2(ctx, args)
|
|
55596
|
-
});
|
|
55597
|
-
}
|
|
55598
|
-
async function editHandler(supabase, args, ctx) {
|
|
55599
|
-
const documentId = args.document_id?.trim();
|
|
55600
|
-
const expectedHash = args.expected_content_hash?.trim();
|
|
55601
|
-
if (!documentId)
|
|
55602
|
-
throw new McpInvalidParams("document_id is required");
|
|
55603
|
-
if (!expectedHash) {
|
|
55604
|
-
throw new McpInvalidParams("expected_content_hash is required. It is the content_hash of the version you are " + "basing these edits on — returned by cerefox_get_document (including outline mode), " + "cerefox_search, cerefox_metadata_search, and by every write.");
|
|
55605
|
-
}
|
|
55606
|
-
let operations;
|
|
55607
|
-
try {
|
|
55608
|
-
operations = validateOperations(args.operations);
|
|
55609
|
-
} catch (err) {
|
|
55610
|
-
throw new McpInvalidParams(err instanceof Error ? err.message : String(err));
|
|
55611
|
-
}
|
|
55612
|
-
return applyAndWrite(supabase, ctx, {
|
|
55613
|
-
documentId,
|
|
55614
|
-
operations,
|
|
55615
|
-
expectedHash,
|
|
55616
|
-
requestor: args.requestor ?? defaultRequestor(ctx),
|
|
55617
|
-
toolLabel: "edit",
|
|
55618
|
-
authorType: resolveAuthorType2(ctx, args)
|
|
55619
|
-
});
|
|
55620
|
-
}
|
|
55621
|
-
var AUDIT_OP, insertTool, editTool;
|
|
55622
|
-
var init_partial_edits2 = __esm(() => {
|
|
55623
|
-
init_partial_edits();
|
|
55624
|
-
init__chunker();
|
|
55625
|
-
init_types3();
|
|
55626
|
-
AUDIT_OP = {
|
|
55627
|
-
insert: "insert",
|
|
55628
|
-
replace_section: "replace-section",
|
|
55629
|
-
delete_section: "delete-section"
|
|
55630
|
-
};
|
|
55631
|
-
insertTool = {
|
|
55632
|
-
name: "cerefox_insert",
|
|
55633
|
-
description: "Add text to a document without resending the whole thing. Purely additive: it cannot " + "remove or overwrite existing content, so it is the safe way to append. Positions: " + "end_of_document (a plain append), end_of_section (add to the end of a section's body — " + "the most common mid-document add), after_heading (lead-in text), before_heading (a new " + "block above a section). Anchors are the exact heading line ('## Intake') or a parent path " + "('## Intake > ### Notes') when a heading appears more than once. Requires " + "expected_content_hash; returns the new hash, not the document.",
|
|
55634
|
-
annotations: {
|
|
55635
|
-
title: "Insert into document",
|
|
55636
|
-
readOnlyHint: false,
|
|
55637
|
-
destructiveHint: false,
|
|
55638
|
-
idempotentHint: false,
|
|
55639
|
-
openWorldHint: false
|
|
55640
|
-
},
|
|
55641
|
-
inputSchema: {
|
|
55642
|
-
type: "object",
|
|
55643
|
-
required: ["document_id", "text", "position", "expected_content_hash"],
|
|
55644
|
-
properties: {
|
|
55645
|
-
document_id: { type: "string", description: "UUID of the document to add to" },
|
|
55646
|
-
text: { type: "string", description: "Markdown to insert. Sent as-is; blank-line separation is handled for you." },
|
|
55647
|
-
position: {
|
|
55648
|
-
type: "string",
|
|
55649
|
-
enum: ["end_of_document", "end_of_section", "after_heading", "before_heading"],
|
|
55650
|
-
description: "Where to insert. end_of_document needs no anchor; the other three require anchor_heading."
|
|
55651
|
-
},
|
|
55652
|
-
anchor_heading: {
|
|
55653
|
-
type: "string",
|
|
55654
|
-
description: "Exact heading line, or a ' > ' path for a heading that appears more than once. Required unless position is end_of_document."
|
|
55655
|
-
},
|
|
55656
|
-
section_part: {
|
|
55657
|
-
type: "string",
|
|
55658
|
-
enum: ["own_body", "subtree"],
|
|
55659
|
-
description: "Only for end_of_section when the target section HAS CHILD SECTIONS: own_body = before the first child, subtree = after everything nested under it. These can be far apart, so the tool refuses rather than choosing. Omit it otherwise; you will be told (with both options) whenever it is needed."
|
|
55660
|
-
},
|
|
55661
|
-
expected_content_hash: {
|
|
55662
|
-
type: "string",
|
|
55663
|
-
description: "content_hash of the version you are basing this on. Required — no last-write-wins."
|
|
55664
|
-
},
|
|
55665
|
-
requestor: {
|
|
55666
|
-
type: "string",
|
|
55667
|
-
description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
|
|
55668
|
-
},
|
|
55669
|
-
author_type: {
|
|
55670
|
-
type: "string",
|
|
55671
|
-
enum: ["user", "agent"],
|
|
55672
|
-
description: "Honoured on the CLI only, where a human ran the command; over MCP the write is always recorded as an agent write regardless of what is passed."
|
|
55673
|
-
}
|
|
55674
|
-
}
|
|
55675
|
-
},
|
|
55676
|
-
handler: insertHandler
|
|
55677
|
-
};
|
|
55678
|
-
editTool = {
|
|
55679
|
-
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.",
|
|
55681
|
-
annotations: {
|
|
55682
|
-
title: "Edit document sections",
|
|
55683
|
-
readOnlyHint: false,
|
|
55684
|
-
destructiveHint: true,
|
|
55685
|
-
idempotentHint: false,
|
|
55686
|
-
openWorldHint: false
|
|
55687
|
-
},
|
|
55688
|
-
inputSchema: {
|
|
55689
|
-
type: "object",
|
|
55690
|
-
required: ["document_id", "operations", "expected_content_hash"],
|
|
55691
|
-
properties: {
|
|
55692
|
-
document_id: { type: "string", description: "UUID of the document to edit" },
|
|
55693
|
-
operations: {
|
|
55694
|
-
type: "array",
|
|
55695
|
-
minItems: 1,
|
|
55696
|
-
description: "Operations applied in order, all-or-nothing. If any fails (bad anchor, ambiguity), nothing is written.",
|
|
55697
|
-
items: {
|
|
55698
|
-
type: "object",
|
|
55699
|
-
required: ["op"],
|
|
55700
|
-
properties: {
|
|
55701
|
-
op: { type: "string", enum: ["insert", "replace_section", "delete_section"] },
|
|
55702
|
-
text: { type: "string", description: "Markdown. Required for insert and replace_section." },
|
|
55703
|
-
position: {
|
|
55704
|
-
type: "string",
|
|
55705
|
-
enum: ["end_of_document", "end_of_section", "after_heading", "before_heading"],
|
|
55706
|
-
description: "Required for insert."
|
|
55707
|
-
},
|
|
55708
|
-
anchor_heading: {
|
|
55709
|
-
type: "string",
|
|
55710
|
-
description: "Exact heading line, or a ' > ' path when the heading is not unique. Required for replace_section, delete_section, and any insert other than end_of_document."
|
|
55711
|
-
},
|
|
55712
|
-
section_part: {
|
|
55713
|
-
type: "string",
|
|
55714
|
-
enum: ["own_body", "subtree"],
|
|
55715
|
-
description: "Only when the target section has child sections. You will be told (with both options) whenever it is needed."
|
|
55716
|
-
},
|
|
55717
|
-
scope: {
|
|
55718
|
-
type: "string",
|
|
55719
|
-
enum: ["body_only", "heading_and_body"],
|
|
55720
|
-
description: "delete_section only. Defaults to body_only, which keeps the heading."
|
|
55721
|
-
}
|
|
55722
|
-
}
|
|
55723
|
-
}
|
|
55724
|
-
},
|
|
55725
|
-
expected_content_hash: {
|
|
55726
|
-
type: "string",
|
|
55727
|
-
description: "content_hash of the version you are basing these edits on. Required — no last-write-wins."
|
|
55728
|
-
},
|
|
55729
|
-
requestor: {
|
|
55730
|
-
type: "string",
|
|
55731
|
-
description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".'
|
|
55732
|
-
},
|
|
55733
|
-
author_type: {
|
|
55734
|
-
type: "string",
|
|
55735
|
-
enum: ["user", "agent"],
|
|
55736
|
-
description: "Honoured on the CLI only, where a human ran the command; over MCP the write is always recorded as an agent write regardless of what is passed."
|
|
55737
|
-
}
|
|
55738
|
-
}
|
|
55739
|
-
},
|
|
55740
|
-
handler: editHandler
|
|
55741
|
-
};
|
|
55742
|
-
});
|
|
55743
|
-
|
|
55744
55992
|
// ../../_shared/mcp-tools/list-metadata-keys.ts
|
|
55745
55993
|
async function handler5(supabase, args, ctx) {
|
|
55746
55994
|
const { data, error: error2 } = await supabase.rpc("cerefox_list_metadata_keys");
|
|
@@ -55829,6 +56077,10 @@ var init_list_projects = __esm(() => {
|
|
|
55829
56077
|
});
|
|
55830
56078
|
|
|
55831
56079
|
// ../../_shared/mcp-tools/list-versions.ts
|
|
56080
|
+
function utcStamp3(iso) {
|
|
56081
|
+
const trimmed = iso.slice(0, 19);
|
|
56082
|
+
return trimmed.includes("T") ? `${trimmed}Z` : `${trimmed} UTC`;
|
|
56083
|
+
}
|
|
55832
56084
|
async function handler7(supabase, args, ctx) {
|
|
55833
56085
|
const document_id = args.document_id;
|
|
55834
56086
|
if (!document_id)
|
|
@@ -55848,7 +56100,7 @@ async function handler7(supabase, args, ctx) {
|
|
|
55848
56100
|
});
|
|
55849
56101
|
if (!versions.length)
|
|
55850
56102
|
return "No archived versions found for this document.";
|
|
55851
|
-
const lines = versions.map((v) => `v${v.version_number} | ${v.created_at
|
|
56103
|
+
const lines = versions.map((v) => `v${v.version_number} | ${utcStamp3(v.created_at)} | ${v.source} | ${v.chunk_count} chunks / ${v.total_chars.toLocaleString()} chars | id: ${v.version_id}`);
|
|
55852
56104
|
return `Archived versions (newest first):
|
|
55853
56105
|
|
|
55854
56106
|
${lines.join(`
|
|
@@ -71898,16 +72150,27 @@ function stringify(obj2, { maxDepth = 1000, numbersAsFloat = false } = {}) {
|
|
|
71898
72150
|
*/
|
|
71899
72151
|
|
|
71900
72152
|
// src/cli/util/mcp-config-writers.ts
|
|
72153
|
+
init_config();
|
|
72154
|
+
function envForEntry() {
|
|
72155
|
+
const label = (process.env.CEREFOX_ENV_LABEL ?? "").trim();
|
|
72156
|
+
if (!label || mcpServerName() === "cerefox")
|
|
72157
|
+
return;
|
|
72158
|
+
return { CEREFOX_CONFIG_DIR: resolveConfigDir(), CEREFOX_ENV_LABEL: label };
|
|
72159
|
+
}
|
|
71901
72160
|
function defaultCerefoxEntry() {
|
|
72161
|
+
const env3 = envForEntry();
|
|
71902
72162
|
return {
|
|
71903
72163
|
command: "npx",
|
|
71904
|
-
args: ["-y", "--package=@cerefox/memory", "cerefox", "mcp"]
|
|
72164
|
+
args: ["-y", "--package=@cerefox/memory", "cerefox", "mcp"],
|
|
72165
|
+
...env3 ? { env: env3 } : {}
|
|
71905
72166
|
};
|
|
71906
72167
|
}
|
|
71907
72168
|
function localCerefoxEntry() {
|
|
72169
|
+
const env3 = envForEntry();
|
|
71908
72170
|
return {
|
|
71909
72171
|
command: process.env.CEREFOX_LOCAL_CMD || "cerefox-local",
|
|
71910
|
-
args: ["mcp"]
|
|
72172
|
+
args: ["mcp"],
|
|
72173
|
+
...env3 ? { env: env3 } : {}
|
|
71911
72174
|
};
|
|
71912
72175
|
}
|
|
71913
72176
|
function claudeCodeUserConfigPath() {
|
|
@@ -71924,9 +72187,20 @@ function claudeDesktopConfigPath() {
|
|
|
71924
72187
|
return join4(home, ".config", "Claude", "claude_desktop_config.json");
|
|
71925
72188
|
}
|
|
71926
72189
|
function claudeCodeDelegated(entry) {
|
|
72190
|
+
const envFlags = Object.entries(entry.env ?? {}).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
|
|
71927
72191
|
return {
|
|
71928
72192
|
cmd: "claude",
|
|
71929
|
-
args: [
|
|
72193
|
+
args: [
|
|
72194
|
+
"mcp",
|
|
72195
|
+
"add",
|
|
72196
|
+
mcpServerName(),
|
|
72197
|
+
"--scope",
|
|
72198
|
+
"user",
|
|
72199
|
+
...envFlags,
|
|
72200
|
+
"--",
|
|
72201
|
+
entry.command,
|
|
72202
|
+
...entry.args
|
|
72203
|
+
]
|
|
71930
72204
|
};
|
|
71931
72205
|
}
|
|
71932
72206
|
function cursorConfigPath() {
|
|
@@ -72011,7 +72285,7 @@ function directWrite(writer, configPath, opts) {
|
|
|
72011
72285
|
}
|
|
72012
72286
|
const serversKey = format === "toml" ? "mcp_servers" : "mcpServers";
|
|
72013
72287
|
const servers = (existing[serversKey] && typeof existing[serversKey] === "object" ? existing[serversKey] : {}) ?? {};
|
|
72014
|
-
servers
|
|
72288
|
+
servers[mcpServerName()] = entry;
|
|
72015
72289
|
existing[serversKey] = servers;
|
|
72016
72290
|
if (!opts.dryRun) {
|
|
72017
72291
|
const body = format === "toml" ? stringify(existing) + `
|
|
@@ -72021,10 +72295,17 @@ function directWrite(writer, configPath, opts) {
|
|
|
72021
72295
|
}
|
|
72022
72296
|
return { configPath, backupPath, action: action6, serverEntry: entry };
|
|
72023
72297
|
}
|
|
72298
|
+
function mcpServerName() {
|
|
72299
|
+
const label = (process.env.CEREFOX_ENV_LABEL ?? "").trim();
|
|
72300
|
+
if (!label)
|
|
72301
|
+
return "cerefox";
|
|
72302
|
+
const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
72303
|
+
return slug ? `cerefox-${slug}` : "cerefox";
|
|
72304
|
+
}
|
|
72024
72305
|
function hasCerefoxEntry(existing, format) {
|
|
72025
72306
|
const key = format === "toml" ? "mcp_servers" : "mcpServers";
|
|
72026
72307
|
const servers = existing[key];
|
|
72027
|
-
return typeof servers === "object" && servers !== null && servers
|
|
72308
|
+
return typeof servers === "object" && servers !== null && servers[mcpServerName()] !== undefined;
|
|
72028
72309
|
}
|
|
72029
72310
|
function delegatedWrite(writer, opts) {
|
|
72030
72311
|
if (!writer.delegated) {
|
|
@@ -72080,7 +72361,7 @@ function action6(options) {
|
|
|
72080
72361
|
entry: options.local ? localCerefoxEntry() : undefined
|
|
72081
72362
|
});
|
|
72082
72363
|
if (options.json) {
|
|
72083
|
-
printJson(result);
|
|
72364
|
+
printJson({ ...result, serverName: mcpServerName() });
|
|
72084
72365
|
return;
|
|
72085
72366
|
}
|
|
72086
72367
|
if (options.dryRun) {
|
|
@@ -72093,6 +72374,7 @@ function action6(options) {
|
|
|
72093
72374
|
println(c.dim(` backup: ${result.backupPath}`));
|
|
72094
72375
|
}
|
|
72095
72376
|
println(c.dim(` action: ${result.action}`));
|
|
72377
|
+
println(c.dim(` server name: ${mcpServerName()}`));
|
|
72096
72378
|
if (result.delegatedCommand) {
|
|
72097
72379
|
println(c.dim(` invoked: ${result.delegatedCommand}`));
|
|
72098
72380
|
}
|
|
@@ -76314,19 +76596,14 @@ init_cli_core();
|
|
|
76314
76596
|
|
|
76315
76597
|
// src/cli/util/checks.ts
|
|
76316
76598
|
init_meta();
|
|
76317
|
-
|
|
76318
|
-
import { homedir as homedir6 } from "node:os";
|
|
76319
|
-
import { join as join9 } from "node:path";
|
|
76320
|
-
|
|
76321
|
-
// ../../_shared/ef-meta/index.ts
|
|
76322
|
-
var EF_VERSION = "1.3.0";
|
|
76323
|
-
var EF_LAST_CHANGED = "1.3.0-beta.4";
|
|
76324
|
-
|
|
76325
|
-
// src/cli/util/checks.ts
|
|
76599
|
+
init_ef_meta();
|
|
76326
76600
|
init_config();
|
|
76327
76601
|
init_config();
|
|
76328
76602
|
init_compatibility();
|
|
76329
76603
|
init_server_assets();
|
|
76604
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, realpathSync, statSync as statSync2 } from "node:fs";
|
|
76605
|
+
import { homedir as homedir6 } from "node:os";
|
|
76606
|
+
import { join as join9 } from "node:path";
|
|
76330
76607
|
function checkBinary() {
|
|
76331
76608
|
return {
|
|
76332
76609
|
name: "binary",
|
|
@@ -76812,7 +77089,8 @@ async function checkPostgres() {
|
|
|
76812
77089
|
}
|
|
76813
77090
|
let postgres2;
|
|
76814
77091
|
try {
|
|
76815
|
-
|
|
77092
|
+
const mod = await Promise.resolve().then(() => (init_src(), exports_src));
|
|
77093
|
+
postgres2 = mod.default;
|
|
76816
77094
|
} catch (err) {
|
|
76817
77095
|
return {
|
|
76818
77096
|
name: "postgres",
|
|
@@ -77083,7 +77361,7 @@ async function action16(options) {
|
|
|
77083
77361
|
return;
|
|
77084
77362
|
}
|
|
77085
77363
|
printTable(data.map((row) => ({
|
|
77086
|
-
when: (row.created_at ?? "").slice(0, 19).replace("T", " ")
|
|
77364
|
+
when: `${(row.created_at ?? "").slice(0, 19).replace("T", " ")}Z`,
|
|
77087
77365
|
operation: row.operation,
|
|
77088
77366
|
doc: (row.doc_title ?? (row.document_id ?? "?").slice(0, 8) + "…").slice(0, 40),
|
|
77089
77367
|
author: (row.author ?? "") + (row.author_type ? `(${row.author_type})` : ""),
|
|
@@ -77228,6 +77506,13 @@ init_cli_core();
|
|
|
77228
77506
|
init_partial_edits();
|
|
77229
77507
|
init_client();
|
|
77230
77508
|
async function action17(documentId, options) {
|
|
77509
|
+
const section = (options.section ?? "").trim() || null;
|
|
77510
|
+
if (section && options.outline) {
|
|
77511
|
+
throw userError("Pass either --outline (the whole structure) or --section (one section's text), not both.");
|
|
77512
|
+
}
|
|
77513
|
+
if (options.sectionPart && !section) {
|
|
77514
|
+
throw userError("--section-part only applies together with --section.");
|
|
77515
|
+
}
|
|
77231
77516
|
const client = getClient();
|
|
77232
77517
|
const rows = await client.rpc("cerefox_get_document", {
|
|
77233
77518
|
p_document_id: documentId,
|
|
@@ -77275,6 +77560,35 @@ async function action17(documentId, options) {
|
|
|
77275
77560
|
println(c.dim(` --anchor ${JSON.stringify(nodes[nodes.length - 1].path)}`));
|
|
77276
77561
|
return;
|
|
77277
77562
|
}
|
|
77563
|
+
if (section) {
|
|
77564
|
+
let extracted;
|
|
77565
|
+
try {
|
|
77566
|
+
extracted = extractSection(doc.full_content ?? "", section, options.sectionPart);
|
|
77567
|
+
} catch (err) {
|
|
77568
|
+
throw userError(err instanceof Error ? err.message : String(err));
|
|
77569
|
+
}
|
|
77570
|
+
const archived = Boolean(options.versionId);
|
|
77571
|
+
if (options.json) {
|
|
77572
|
+
printJson({
|
|
77573
|
+
title: doc.doc_title,
|
|
77574
|
+
heading: extracted.heading,
|
|
77575
|
+
path: extracted.path,
|
|
77576
|
+
level: extracted.level,
|
|
77577
|
+
section_part: extracted.section_part,
|
|
77578
|
+
chars: extracted.chars,
|
|
77579
|
+
content_hash: archived ? null : doc.content_hash ?? null,
|
|
77580
|
+
text: extracted.text
|
|
77581
|
+
});
|
|
77582
|
+
return;
|
|
77583
|
+
}
|
|
77584
|
+
println(c.bold(extracted.heading));
|
|
77585
|
+
println(c.dim(`[${doc.document_id}] · ${extracted.path} · ${extracted.chars} chars` + (extracted.section_part ? ` · ${extracted.section_part}` : "") + (archived ? " · archived" : "")));
|
|
77586
|
+
if (!archived && doc.content_hash)
|
|
77587
|
+
println(c.dim(`content_hash: ${doc.content_hash}`));
|
|
77588
|
+
println("");
|
|
77589
|
+
println(extracted.text);
|
|
77590
|
+
return;
|
|
77591
|
+
}
|
|
77278
77592
|
if (options.json) {
|
|
77279
77593
|
printJson(doc);
|
|
77280
77594
|
return;
|
|
@@ -77288,7 +77602,7 @@ async function action17(documentId, options) {
|
|
|
77288
77602
|
println(doc.full_content);
|
|
77289
77603
|
}
|
|
77290
77604
|
function registerGetDoc(program2) {
|
|
77291
|
-
program2.command("get-doc").description("Retrieve the full content of a document by ID.").argument("<document-id>", "UUID of the document.").option("--version-id <uuid>", "Specific archived version (default: current).").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").option("--outline", "Show the heading structure, per-section sizes and content_hash instead of the content. Cheap, and the paths are the anchors the edit commands take.").action(action17);
|
|
77605
|
+
program2.command("get-doc").description("Retrieve the full content of a document by ID.").argument("<document-id>", "UUID of the document.").option("--version-id <uuid>", "Specific archived version (default: current).").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").option("--outline", "Show the heading structure, per-section sizes and content_hash instead of the content. Cheap, and the paths are the anchors the edit commands take.").option("--section <anchor>", "Show ONE section's text instead of the whole document: exactly what a replace_section on this anchor would overwrite. Pass the bare heading line when it is unique, or the full ' > ' path from --outline when it repeats.").option("--section-part <part>", "own_body | subtree — only when the target section has child sections, where 'the end' means two different places. You are told (with both options) whenever it is needed.").action(action17);
|
|
77292
77606
|
}
|
|
77293
77607
|
|
|
77294
77608
|
// src/cli/commands/ingest.ts
|
|
@@ -77576,6 +77890,7 @@ class IngestionPipeline {
|
|
|
77576
77890
|
text,
|
|
77577
77891
|
title,
|
|
77578
77892
|
source = "paste",
|
|
77893
|
+
sourceOnCreate,
|
|
77579
77894
|
sourceLabel,
|
|
77580
77895
|
sourcePath: sourcePathOpt,
|
|
77581
77896
|
projectName,
|
|
@@ -77678,7 +77993,8 @@ class IngestionPipeline {
|
|
|
77678
77993
|
action: "skipped",
|
|
77679
77994
|
reindexed: false,
|
|
77680
77995
|
projectIds: existingProjectIds,
|
|
77681
|
-
note: ""
|
|
77996
|
+
note: "",
|
|
77997
|
+
contentHash: existingByHash.content_hash ?? hash
|
|
77682
77998
|
};
|
|
77683
77999
|
}
|
|
77684
78000
|
const chunks = chunkMarkdown(text, this.settings.maxChunkChars, this.settings.minChunkChars);
|
|
@@ -77703,7 +78019,7 @@ class IngestionPipeline {
|
|
|
77703
78019
|
const rpcResult = await this.db.ingestDocumentRpc({
|
|
77704
78020
|
documentId: null,
|
|
77705
78021
|
title,
|
|
77706
|
-
source,
|
|
78022
|
+
source: source ?? sourceOnCreate ?? null,
|
|
77707
78023
|
sourcePath,
|
|
77708
78024
|
contentHash: hash,
|
|
77709
78025
|
metadata: validatedMeta,
|
|
@@ -77725,7 +78041,8 @@ class IngestionPipeline {
|
|
|
77725
78041
|
action: "created",
|
|
77726
78042
|
reindexed: false,
|
|
77727
78043
|
projectIds: resolvedIds,
|
|
77728
|
-
note: ""
|
|
78044
|
+
note: "",
|
|
78045
|
+
contentHash: hash
|
|
77729
78046
|
};
|
|
77730
78047
|
}
|
|
77731
78048
|
async updateDocument(opts) {
|
|
@@ -77817,7 +78134,8 @@ class IngestionPipeline {
|
|
|
77817
78134
|
action: "updated",
|
|
77818
78135
|
reindexed: false,
|
|
77819
78136
|
projectIds: finalProjectIds2,
|
|
77820
|
-
note: ""
|
|
78137
|
+
note: "",
|
|
78138
|
+
contentHash: existing.content_hash ?? newHash
|
|
77821
78139
|
};
|
|
77822
78140
|
}
|
|
77823
78141
|
const chunks = chunkMarkdown(text, this.settings.maxChunkChars, this.settings.minChunkChars);
|
|
@@ -77851,7 +78169,7 @@ class IngestionPipeline {
|
|
|
77851
78169
|
contentFormat: CONTENT_FORMAT_BLIND_STITCH,
|
|
77852
78170
|
author,
|
|
77853
78171
|
authorType,
|
|
77854
|
-
sourceLabel: sourceLabel ?? source,
|
|
78172
|
+
sourceLabel: sourceLabel ?? source ?? "manual",
|
|
77855
78173
|
expectedContentHash: expectedContentHash ?? (forceRechunk && contentUnchanged ? existing.content_hash : null),
|
|
77856
78174
|
lastWriteWins
|
|
77857
78175
|
});
|
|
@@ -77870,7 +78188,8 @@ class IngestionPipeline {
|
|
|
77870
78188
|
action: "updated",
|
|
77871
78189
|
reindexed: true,
|
|
77872
78190
|
projectIds: finalProjectIds,
|
|
77873
|
-
note: ""
|
|
78191
|
+
note: "",
|
|
78192
|
+
contentHash: newHash
|
|
77874
78193
|
};
|
|
77875
78194
|
}
|
|
77876
78195
|
async ingestFile(path, opts = {}) {
|
|
@@ -77881,7 +78200,8 @@ class IngestionPipeline {
|
|
|
77881
78200
|
...opts,
|
|
77882
78201
|
text,
|
|
77883
78202
|
title: opts.title ?? stem,
|
|
77884
|
-
source: opts.source
|
|
78203
|
+
source: opts.source === undefined ? "file" : opts.source,
|
|
78204
|
+
sourceOnCreate: opts.sourceOnCreate ?? "file",
|
|
77885
78205
|
sourcePath: absPath
|
|
77886
78206
|
});
|
|
77887
78207
|
}
|
|
@@ -77953,6 +78273,7 @@ async function action18(path, options) {
|
|
|
77953
78273
|
title = data.title;
|
|
77954
78274
|
println(c.dim(` (keeping existing title: ${JSON.stringify(title)})`));
|
|
77955
78275
|
}
|
|
78276
|
+
const resolvedSource = options.source ?? null;
|
|
77956
78277
|
const pipeline = new IngestionPipeline({
|
|
77957
78278
|
supabase,
|
|
77958
78279
|
openAiApiKey: settings.openaiApiKey
|
|
@@ -77960,7 +78281,8 @@ async function action18(path, options) {
|
|
|
77960
78281
|
try {
|
|
77961
78282
|
const result = path && !options.paste ? await pipeline.ingestFile(path, {
|
|
77962
78283
|
title,
|
|
77963
|
-
source:
|
|
78284
|
+
source: resolvedSource,
|
|
78285
|
+
sourceOnCreate: "cli",
|
|
77964
78286
|
projectName: options.projectName ?? null,
|
|
77965
78287
|
projectNames: projectNames ?? null,
|
|
77966
78288
|
metadata: metadata ?? null,
|
|
@@ -77973,7 +78295,8 @@ async function action18(path, options) {
|
|
|
77973
78295
|
}) : await pipeline.ingestText({
|
|
77974
78296
|
text: content,
|
|
77975
78297
|
title,
|
|
77976
|
-
source:
|
|
78298
|
+
source: resolvedSource,
|
|
78299
|
+
sourceOnCreate: "cli",
|
|
77977
78300
|
projectName: options.projectName ?? null,
|
|
77978
78301
|
projectNames: projectNames ?? null,
|
|
77979
78302
|
metadata: metadata ?? null,
|
|
@@ -77995,13 +78318,16 @@ async function action18(path, options) {
|
|
|
77995
78318
|
const projects = result.projectIds.length > 0 ? ` [projects: ${result.projectIds.length}]` : "";
|
|
77996
78319
|
const note = result.note ? ` (${result.note})` : "";
|
|
77997
78320
|
println(c.green("✓ ") + `${verb}: ${JSON.stringify(result.title)} (id: ${result.documentId}), ` + `${result.chunkCount} chunk(s), ${result.totalChars} chars.${projects}${note}`);
|
|
78321
|
+
if (result.contentHash) {
|
|
78322
|
+
println(c.dim(` content_hash: ${result.contentHash}`));
|
|
78323
|
+
}
|
|
77998
78324
|
} catch (err) {
|
|
77999
78325
|
const msg = err instanceof Error ? err.message : String(err);
|
|
78000
78326
|
throw systemError(`Ingest failed: ${msg}`);
|
|
78001
78327
|
}
|
|
78002
78328
|
}
|
|
78003
78329
|
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>",
|
|
78330
|
+
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
78331
|
}
|
|
78006
78332
|
|
|
78007
78333
|
// src/cli/commands/document-partial-edit.ts
|
|
@@ -78145,7 +78471,8 @@ async function action19(dir, options) {
|
|
|
78145
78471
|
try {
|
|
78146
78472
|
const result = await pipeline.ingestFile(file, {
|
|
78147
78473
|
title: basename3(file, extname4(file)),
|
|
78148
|
-
source: options.source ??
|
|
78474
|
+
source: options.source ?? null,
|
|
78475
|
+
sourceOnCreate: "cli",
|
|
78149
78476
|
projectName: options.projectName ?? null,
|
|
78150
78477
|
metadata: metadata ?? null,
|
|
78151
78478
|
updateExisting: Boolean(options.updateIfExists),
|
|
@@ -78178,7 +78505,7 @@ async function action19(dir, options) {
|
|
|
78178
78505
|
}
|
|
78179
78506
|
}
|
|
78180
78507
|
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>",
|
|
78508
|
+
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
78509
|
}
|
|
78183
78510
|
|
|
78184
78511
|
// src/cli/commands/init.ts
|
|
@@ -83652,7 +83979,7 @@ async function runSearch(ctx, opts) {
|
|
|
83652
83979
|
if (!ctx.openAiApiKey && resolveEmbedderKind() !== "local") {
|
|
83653
83980
|
throw new HttpError(503, "Embedder not available");
|
|
83654
83981
|
}
|
|
83655
|
-
const embedding = await getEmbedding(query, ctx.openAiApiKey);
|
|
83982
|
+
const embedding = await getEmbedding(query, ctx.openAiApiKey ?? "");
|
|
83656
83983
|
if (mode === "semantic") {
|
|
83657
83984
|
const params2 = {
|
|
83658
83985
|
p_query_embedding: embedding,
|
|
@@ -84249,7 +84576,7 @@ function registerDocumentWriteRoutes(app, ctx) {
|
|
|
84249
84576
|
try {
|
|
84250
84577
|
const pipeline2 = new IngestionPipeline({
|
|
84251
84578
|
supabase: ctx.supabase,
|
|
84252
|
-
openAiApiKey: ctx.openAiApiKey
|
|
84579
|
+
openAiApiKey: ctx.openAiApiKey ?? ""
|
|
84253
84580
|
});
|
|
84254
84581
|
const result = await pipeline2.updateDocument({
|
|
84255
84582
|
documentId,
|
|
@@ -84420,7 +84747,7 @@ function registerIngestRoutes(app, ctx) {
|
|
|
84420
84747
|
try {
|
|
84421
84748
|
const pipeline2 = new IngestionPipeline({
|
|
84422
84749
|
supabase: ctx.supabase,
|
|
84423
|
-
openAiApiKey: ctx.openAiApiKey
|
|
84750
|
+
openAiApiKey: ctx.openAiApiKey ?? ""
|
|
84424
84751
|
});
|
|
84425
84752
|
const result = await pipeline2.ingestText({
|
|
84426
84753
|
text: content.trim(),
|
|
@@ -84485,7 +84812,7 @@ function registerIngestRoutes(app, ctx) {
|
|
|
84485
84812
|
try {
|
|
84486
84813
|
const pipeline2 = new IngestionPipeline({
|
|
84487
84814
|
supabase: ctx.supabase,
|
|
84488
|
-
openAiApiKey: ctx.openAiApiKey
|
|
84815
|
+
openAiApiKey: ctx.openAiApiKey ?? ""
|
|
84489
84816
|
});
|
|
84490
84817
|
const result = await pipeline2.ingestText({
|
|
84491
84818
|
text,
|
|
@@ -84538,7 +84865,7 @@ function registerIngestRoutes(app, ctx) {
|
|
|
84538
84865
|
try {
|
|
84539
84866
|
const pipeline2 = new IngestionPipeline({
|
|
84540
84867
|
supabase: ctx.supabase,
|
|
84541
|
-
openAiApiKey: ctx.openAiApiKey
|
|
84868
|
+
openAiApiKey: ctx.openAiApiKey ?? ""
|
|
84542
84869
|
});
|
|
84543
84870
|
const result = await pipeline2.updateDocument({
|
|
84544
84871
|
documentId,
|
|
@@ -84993,6 +85320,7 @@ var ROOT_REDIRECT_HTML = `<!DOCTYPE html>
|
|
|
84993
85320
|
|
|
84994
85321
|
// src/web/server.ts
|
|
84995
85322
|
init_meta();
|
|
85323
|
+
init_ef_meta();
|
|
84996
85324
|
init_cli_core();
|
|
84997
85325
|
init_config();
|
|
84998
85326
|
init_compatibility();
|