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