@cerefox/memory 1.6.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +80 -6
- package/AGENT_QUICK_REFERENCE.md +9 -7
- package/README.md +5 -3
- package/dist/bin/cerefox.js +790 -319
- package/dist/frontend/assets/index-D8E0mTnp.js +121 -0
- package/dist/frontend/assets/index-D8E0mTnp.js.map +1 -0
- package/dist/frontend/index.html +1 -1
- 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/migrations/0025_drop_orphaned_overloads.sql +15 -0
- package/dist/server-assets/db/migrations/0026_metadata_guard_and_dead_links.sql +46 -0
- package/dist/server-assets/db/rpcs.sql +355 -27
- package/dist/server-assets/db/schema.sql +10 -2
- 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 +34 -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-OqloGFwv.js +0 -121
- package/dist/frontend/assets/index-OqloGFwv.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.1";
|
|
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.1", CEREFOX_VERSION = "1.7.1", EF_LAST_CHANGED = "1.7.0";
|
|
25735
25747
|
var init_ef_meta = () => {};
|
|
25736
25748
|
|
|
25737
25749
|
// ../../_shared/compatibility/index.ts
|
|
@@ -41580,10 +41592,10 @@ var require_flate = __commonJS((exports) => {
|
|
|
41580
41592
|
var GenericWorker = require_GenericWorker();
|
|
41581
41593
|
var ARRAY_TYPE = USE_TYPEDARRAY ? "uint8array" : "array";
|
|
41582
41594
|
exports.magic = "\b\x00";
|
|
41583
|
-
function FlateWorker(
|
|
41584
|
-
GenericWorker.call(this, "FlateWorker/" +
|
|
41595
|
+
function FlateWorker(action20, options) {
|
|
41596
|
+
GenericWorker.call(this, "FlateWorker/" + action20);
|
|
41585
41597
|
this._pako = null;
|
|
41586
|
-
this._pakoAction =
|
|
41598
|
+
this._pakoAction = action20;
|
|
41587
41599
|
this._pakoOptions = options;
|
|
41588
41600
|
this.meta = {};
|
|
41589
41601
|
}
|
|
@@ -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,
|
|
@@ -57480,11 +57699,11 @@ async function runSyncSelfDocs(options = {}) {
|
|
|
57480
57699
|
printTable(outcomes.filter((o) => o.status === "error").map((o) => ({ topic: o.topic, error: o.detail.slice(0, 100) })));
|
|
57481
57700
|
}
|
|
57482
57701
|
}
|
|
57483
|
-
async function
|
|
57702
|
+
async function action22(options) {
|
|
57484
57703
|
await runSyncSelfDocs(options);
|
|
57485
57704
|
}
|
|
57486
57705
|
function registerSyncSelfDocs(program2) {
|
|
57487
|
-
program2.command("sync-self-docs").description("Ingest bundled Cerefox docs under the _cerefox-self-docs project.").option("--dry-run", "List what would be ingested without writing.").option("--project <name>", "Override the target project name.", "_cerefox-self-docs").action(
|
|
57706
|
+
program2.command("sync-self-docs").description("Ingest bundled Cerefox docs under the _cerefox-self-docs project.").option("--dry-run", "List what would be ingested without writing.").option("--project <name>", "Override the target project name.", "_cerefox-self-docs").action(action22);
|
|
57488
57707
|
}
|
|
57489
57708
|
var init_sync_self_docs = __esm(() => {
|
|
57490
57709
|
init_cli_core();
|
|
@@ -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,25 +72759,86 @@ 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) {
|
|
72554
72797
|
program2.command("delete-doc").description("Soft-delete a document (recoverable via the web UI trash).").argument("<document-id>", "UUID of the document to delete.").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").option("--yes", "Skip the confirmation prompt.").action(action7);
|
|
72555
72798
|
}
|
|
72556
72799
|
|
|
72800
|
+
// src/cli/commands/document-dead-links.ts
|
|
72801
|
+
init_cli_core();
|
|
72802
|
+
init_client();
|
|
72803
|
+
var SERVER_BEHIND = "The sweep did not run: this server has no cerefox_find_dead_links (needs schema 0.12.2). " + "Run `cerefox server deploy`, then retry.";
|
|
72804
|
+
async function action8(options) {
|
|
72805
|
+
const client = getClient();
|
|
72806
|
+
let rows;
|
|
72807
|
+
try {
|
|
72808
|
+
rows = await client.rpc("cerefox_find_dead_links", {});
|
|
72809
|
+
} catch (e) {
|
|
72810
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
72811
|
+
if (isMissingFunctionError(message, "cerefox_find_dead_links")) {
|
|
72812
|
+
throw systemError(SERVER_BEHIND);
|
|
72813
|
+
}
|
|
72814
|
+
throw e;
|
|
72815
|
+
}
|
|
72816
|
+
if (rows === null)
|
|
72817
|
+
throw systemError(SERVER_BEHIND);
|
|
72818
|
+
if (options.json) {
|
|
72819
|
+
printJson(rows);
|
|
72820
|
+
return;
|
|
72821
|
+
}
|
|
72822
|
+
if (rows.length === 0) {
|
|
72823
|
+
println(c.green("✓ No dead document links found."));
|
|
72824
|
+
return;
|
|
72825
|
+
}
|
|
72826
|
+
println(c.yellow(`${rows.length} dead link(s) across ${new Set(rows.map((r) => r.document_id)).size} document(s):`));
|
|
72827
|
+
for (const r of rows) {
|
|
72828
|
+
println(` ${r.document_title} (${r.document_id})`);
|
|
72829
|
+
println(c.dim(` → [Text](${r.dead_link_id}) ×${r.occurrences} — target no longer exists`));
|
|
72830
|
+
}
|
|
72831
|
+
println(c.dim(`Fix each by editing the linking document (correct the id, remove the link, or backtick it as an example). ` + `The write-time guard prevents NEW dead links; these predate it or lost their target to a purge.`));
|
|
72832
|
+
}
|
|
72833
|
+
function registerDocumentDeadLinks(program2) {
|
|
72834
|
+
program2.command("dead-links").description("Find [Text](uuid) links whose target document no longer exists (#214 phase 2).").option("--json", "Machine-readable output.").action(action8);
|
|
72835
|
+
}
|
|
72836
|
+
|
|
72557
72837
|
// src/cli/commands/delete-project.ts
|
|
72558
72838
|
init_cli_core();
|
|
72559
72839
|
init_client();
|
|
72560
72840
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
72561
|
-
async function
|
|
72841
|
+
async function action9(target, options) {
|
|
72562
72842
|
const client = getClient();
|
|
72563
72843
|
const isUuid = UUID_RE.test(target);
|
|
72564
72844
|
const lookup = isUuid ? client.raw.from("cerefox_projects").select("id, name, description").eq("id", target).maybeSingle() : client.raw.from("cerefox_projects").select("id, name, description").eq("name", target).maybeSingle();
|
|
@@ -72594,7 +72874,7 @@ async function action8(target, options) {
|
|
|
72594
72874
|
println(c.green(`✓ Deleted project "${project.name}" (id: ${project.id}).`));
|
|
72595
72875
|
}
|
|
72596
72876
|
function registerDeleteProject(program2) {
|
|
72597
|
-
program2.command("delete-project").description("Delete an empty project (use --force to remove a non-empty one).").argument("<name-or-id>", "Project name (exact match) or UUID.").option("--yes", "Skip the confirmation prompt.").option("--force", "Allow deletion when documents are still linked to the project.").action(
|
|
72877
|
+
program2.command("delete-project").description("Delete an empty project (use --force to remove a non-empty one).").argument("<name-or-id>", "Project name (exact match) or UUID.").option("--yes", "Skip the confirmation prompt.").option("--force", "Allow deletion when documents are still linked to the project.").action(action9);
|
|
72598
72878
|
}
|
|
72599
72879
|
|
|
72600
72880
|
// src/cli/commands/deploy-server.ts
|
|
@@ -72800,7 +73080,7 @@ function listEdgeFunctions(functionsDir) {
|
|
|
72800
73080
|
return [];
|
|
72801
73081
|
return readdirSync2(functionsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("cerefox-")).map((e) => e.name).sort();
|
|
72802
73082
|
}
|
|
72803
|
-
async function
|
|
73083
|
+
async function action10(options) {
|
|
72804
73084
|
const settings = loadSettings();
|
|
72805
73085
|
const assets = resolveServerAssets();
|
|
72806
73086
|
const doSchema = !options.functionsOnly;
|
|
@@ -73015,7 +73295,7 @@ Proceed with deployment to Supabase?`, true);
|
|
|
73015
73295
|
println(c.dim("Verify with: cerefox doctor"));
|
|
73016
73296
|
}
|
|
73017
73297
|
function registerDeployServer(program2) {
|
|
73018
|
-
program2.command("deploy-server").description("Deploy/update the Cerefox server side (schema + RPCs + Edge Functions) on Supabase.").option("--dry-run", "Print the plan + pre-flight without deploying.").option("--schema-only", "Deploy/update only the schema + RPCs (skip Edge Functions).").option("--functions-only", "Deploy only the Edge Functions (skip the schema/RPCs).").option("--project-ref <ref>", "Supabase project ref for Edge Function deploys (default: derived from CEREFOX_SUPABASE_URL).").option("--yes", "Non-interactive (skip the deployment confirmation).").action(
|
|
73298
|
+
program2.command("deploy-server").description("Deploy/update the Cerefox server side (schema + RPCs + Edge Functions) on Supabase.").option("--dry-run", "Print the plan + pre-flight without deploying.").option("--schema-only", "Deploy/update only the schema + RPCs (skip Edge Functions).").option("--functions-only", "Deploy only the Edge Functions (skip the schema/RPCs).").option("--project-ref <ref>", "Supabase project ref for Edge Function deploys (default: derived from CEREFOX_SUPABASE_URL).").option("--yes", "Non-interactive (skip the deployment confirmation).").action(action10);
|
|
73019
73299
|
}
|
|
73020
73300
|
|
|
73021
73301
|
// src/cli/commands/document-edit.ts
|
|
@@ -73035,7 +73315,7 @@ function parseMetaPair(pair) {
|
|
|
73035
73315
|
}
|
|
73036
73316
|
return [key, value];
|
|
73037
73317
|
}
|
|
73038
|
-
async function
|
|
73318
|
+
async function action11(documentId, options) {
|
|
73039
73319
|
const hasTitle = options.title !== undefined;
|
|
73040
73320
|
const sets = options.setMeta ?? [];
|
|
73041
73321
|
const unsets = options.unsetMeta ?? [];
|
|
@@ -73053,18 +73333,41 @@ async function action10(documentId, options) {
|
|
|
73053
73333
|
if (doc.deleted_at) {
|
|
73054
73334
|
throw userError(`Document ${documentId} is soft-deleted — restore it first (cerefox document restore).`);
|
|
73055
73335
|
}
|
|
73056
|
-
const
|
|
73057
|
-
for (const pair of sets) {
|
|
73058
|
-
const [k, v] = parseMetaPair(pair);
|
|
73059
|
-
metadata[k] = v;
|
|
73060
|
-
}
|
|
73061
|
-
for (const k of unsets)
|
|
73062
|
-
delete metadata[k.trim()];
|
|
73336
|
+
const metaTouched = sets.length > 0 || unsets.length > 0;
|
|
73063
73337
|
const newTitle = hasTitle ? options.title.trim() : doc.title;
|
|
73064
73338
|
const titleChanged = newTitle !== doc.title;
|
|
73065
|
-
const
|
|
73066
|
-
|
|
73067
|
-
|
|
73339
|
+
const author = resolveAuthor(options.author);
|
|
73340
|
+
const authorType = resolveAuthorType(options.authorType);
|
|
73341
|
+
if (author === "unknown") {
|
|
73342
|
+
warn("No --author / CEREFOX_AUTHOR_NAME set — audit log will record this write as 'unknown'.");
|
|
73343
|
+
}
|
|
73344
|
+
if (metaTouched) {
|
|
73345
|
+
const patch = {};
|
|
73346
|
+
for (const pair of sets) {
|
|
73347
|
+
const [k, v] = parseMetaPair(pair);
|
|
73348
|
+
patch[k] = v;
|
|
73349
|
+
}
|
|
73350
|
+
for (const k of unsets)
|
|
73351
|
+
patch[k.trim()] = null;
|
|
73352
|
+
const { error: metaErr } = await client.raw.rpc("cerefox_set_document_metadata", {
|
|
73353
|
+
p_document_id: documentId,
|
|
73354
|
+
p_metadata: patch,
|
|
73355
|
+
p_replace: false,
|
|
73356
|
+
p_author: author,
|
|
73357
|
+
p_author_type: authorType
|
|
73358
|
+
});
|
|
73359
|
+
if (metaErr) {
|
|
73360
|
+
if (metaErr.message?.includes("CEREFOX_BAD_METADATA")) {
|
|
73361
|
+
throw userError(`Document ${documentId} has non-object metadata; a patch cannot repair it. ` + `Repair it first with: cerefox document set-metadata ${documentId} --replace --json '<the intended object>'`);
|
|
73362
|
+
}
|
|
73363
|
+
throw systemError(`Metadata update failed: ${metaErr.message}`);
|
|
73364
|
+
}
|
|
73365
|
+
}
|
|
73366
|
+
if (hasTitle) {
|
|
73367
|
+
const { error: updErr } = await client.raw.from("cerefox_documents").update({ title: newTitle, updated_at: new Date().toISOString() }).eq("id", documentId);
|
|
73368
|
+
if (updErr)
|
|
73369
|
+
throw systemError(`Update failed: ${updErr.message}`);
|
|
73370
|
+
}
|
|
73068
73371
|
if (titleChanged) {
|
|
73069
73372
|
const { error: ftsErr } = await client.raw.rpc("cerefox_update_chunk_fts", {
|
|
73070
73373
|
p_document_id: documentId,
|
|
@@ -73073,18 +73376,15 @@ async function action10(documentId, options) {
|
|
|
73073
73376
|
if (ftsErr)
|
|
73074
73377
|
throw systemError(`Title updated but FTS refresh failed: ${ftsErr.message}`);
|
|
73075
73378
|
}
|
|
73076
|
-
|
|
73077
|
-
|
|
73078
|
-
|
|
73079
|
-
|
|
73379
|
+
if (titleChanged) {
|
|
73380
|
+
await client.raw.rpc("cerefox_create_audit_entry", {
|
|
73381
|
+
p_document_id: documentId,
|
|
73382
|
+
p_operation: "update-metadata",
|
|
73383
|
+
p_author: author,
|
|
73384
|
+
p_author_type: authorType,
|
|
73385
|
+
p_description: "Edited title"
|
|
73386
|
+
});
|
|
73080
73387
|
}
|
|
73081
|
-
await client.raw.rpc("cerefox_create_audit_entry", {
|
|
73082
|
-
p_document_id: documentId,
|
|
73083
|
-
p_operation: "update-metadata",
|
|
73084
|
-
p_author: author,
|
|
73085
|
-
p_author_type: authorType,
|
|
73086
|
-
p_description: `Edited${titleChanged ? " title" : ""}` + (sets.length ? ` · set ${sets.length} meta key(s)` : "") + (unsets.length ? ` · unset ${unsets.length} meta key(s)` : "")
|
|
73087
|
-
});
|
|
73088
73388
|
println(c.green(`✓ Edited "${newTitle}" (id: ${documentId}).`));
|
|
73089
73389
|
if (titleChanged) {
|
|
73090
73390
|
println(c.dim(" Title changed: FTS refreshed; semantic embeddings update on next `cerefox server reindex`."));
|
|
@@ -73094,13 +73394,13 @@ function collect(value, prev) {
|
|
|
73094
73394
|
return [...prev, value];
|
|
73095
73395
|
}
|
|
73096
73396
|
function registerDocumentEdit(parent) {
|
|
73097
|
-
parent.command("edit").description("Edit a document's title and/or metadata (non-destructive patch). Content edits: `document ingest --document-id <id> --update`.").argument("<document-id>", "UUID of the document.").option("--title <title>", "New title (refreshes FTS; re-embed on next reindex).").option("--set-meta <key=value>", "Set/overwrite a metadata key (repeatable). Value is JSON-parsed when possible.", collect, []).option("--unset-meta <key>", "Remove a metadata key (repeatable).", collect, []).option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(
|
|
73397
|
+
parent.command("edit").description("Edit a document's title and/or metadata (non-destructive patch). Content edits: `document ingest --document-id <id> --update`.").argument("<document-id>", "UUID of the document.").option("--title <title>", "New title (refreshes FTS; re-embed on next reindex).").option("--set-meta <key=value>", "Set/overwrite a metadata key (repeatable). Value is JSON-parsed when possible.", collect, []).option("--unset-meta <key>", "Remove a metadata key (repeatable).", collect, []).option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(action11);
|
|
73098
73398
|
}
|
|
73099
73399
|
|
|
73100
73400
|
// src/cli/commands/document-restore.ts
|
|
73101
73401
|
init_cli_core();
|
|
73102
73402
|
init_client();
|
|
73103
|
-
async function
|
|
73403
|
+
async function action12(documentId, options) {
|
|
73104
73404
|
const client = getClient();
|
|
73105
73405
|
const { data: doc, error } = await client.raw.from("cerefox_documents").select("id, title, deleted_at").eq("id", documentId).maybeSingle();
|
|
73106
73406
|
if (error)
|
|
@@ -73116,21 +73416,51 @@ async function action11(documentId, options) {
|
|
|
73116
73416
|
if (author === "unknown") {
|
|
73117
73417
|
warn("No --author / CEREFOX_AUTHOR_NAME set — audit log will record this restore as 'unknown'.");
|
|
73118
73418
|
}
|
|
73119
|
-
|
|
73120
|
-
|
|
73121
|
-
|
|
73122
|
-
|
|
73123
|
-
|
|
73419
|
+
let result;
|
|
73420
|
+
try {
|
|
73421
|
+
result = await client.rpc("cerefox_restore_document", {
|
|
73422
|
+
p_document_id: documentId,
|
|
73423
|
+
p_author: author,
|
|
73424
|
+
p_author_type: authorType,
|
|
73425
|
+
...options.reason ? { p_reason: options.reason } : {}
|
|
73426
|
+
});
|
|
73427
|
+
} catch (e) {
|
|
73428
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
73429
|
+
if (isMissingFunctionError(message, "cerefox_restore_document")) {
|
|
73430
|
+
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.`);
|
|
73431
|
+
}
|
|
73432
|
+
throw e;
|
|
73433
|
+
}
|
|
73434
|
+
if (result === null) {
|
|
73435
|
+
const { data: check, error: checkError } = await client.raw.from("cerefox_documents").select("deleted_at").eq("id", documentId).maybeSingle();
|
|
73436
|
+
if (checkError) {
|
|
73437
|
+
warn(`Restore submitted, but the follow-up verification read failed (${checkError.message}). ` + `Confirm with: cerefox document get ${documentId}`);
|
|
73438
|
+
return;
|
|
73439
|
+
}
|
|
73440
|
+
if (!check) {
|
|
73441
|
+
throw systemError(`Document ${documentId} no longer exists — it may have been purged concurrently. Nothing was restored.`);
|
|
73442
|
+
}
|
|
73443
|
+
if (check.deleted_at) {
|
|
73444
|
+
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.`);
|
|
73445
|
+
}
|
|
73446
|
+
}
|
|
73447
|
+
if (result && result.restored === false) {
|
|
73448
|
+
println(c.dim(`Document ${documentId} ("${doc.title}") was already restored by the time this ran. No change was made.`));
|
|
73449
|
+
return;
|
|
73450
|
+
}
|
|
73124
73451
|
println(c.green(`✓ Restored "${doc.title}" (id: ${documentId}) from the trash.`));
|
|
73452
|
+
if (options.reason) {
|
|
73453
|
+
println(c.dim(` Reason (recorded in the audit log): ${options.reason}`));
|
|
73454
|
+
}
|
|
73125
73455
|
}
|
|
73126
73456
|
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(
|
|
73457
|
+
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(action12);
|
|
73128
73458
|
}
|
|
73129
73459
|
|
|
73130
73460
|
// src/cli/commands/document-set-metadata.ts
|
|
73131
73461
|
init_cli_core();
|
|
73132
73462
|
init_client();
|
|
73133
|
-
async function
|
|
73463
|
+
async function action13(documentId, options) {
|
|
73134
73464
|
const patch = {};
|
|
73135
73465
|
if (options.json) {
|
|
73136
73466
|
let parsed;
|
|
@@ -73202,14 +73532,14 @@ async function action12(documentId, options) {
|
|
|
73202
73532
|
println(c.dim(" Content untouched — no new version, no re-embedding."));
|
|
73203
73533
|
}
|
|
73204
73534
|
function registerDocumentSetMetadata(parent) {
|
|
73205
|
-
parent.command("set-metadata").description("Change a document's metadata without resending its content (merges by default).").argument("<document-id>", "UUID of the document.").option("-s, --set <key=value...>", "Set a key. Repeatable. Values are stored as JSON strings; quote to force JSON parsing.").option("-r, --remove <key...>", "Remove a key. Repeatable. (Sends a JSON null.)").option("--json <object>", "A JSON object of keys to set; a null value removes that key.").option("--replace", "Set the metadata to EXACTLY what was given, discarding every key not listed. Default is merge.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").option("--json-out", "Emit the result as JSON.").action(
|
|
73535
|
+
parent.command("set-metadata").description("Change a document's metadata without resending its content (merges by default).").argument("<document-id>", "UUID of the document.").option("-s, --set <key=value...>", "Set a key. Repeatable. Values are stored as JSON strings; quote to force JSON parsing.").option("-r, --remove <key...>", "Remove a key. Repeatable. (Sends a JSON null.)").option("--json <object>", "A JSON object of keys to set; a null value removes that key.").option("--replace", "Set the metadata to EXACTLY what was given, discarding every key not listed. Default is merge.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").option("--json-out", "Emit the result as JSON.").action(action13);
|
|
73206
73536
|
}
|
|
73207
73537
|
|
|
73208
73538
|
// src/cli/commands/document-set-projects.ts
|
|
73209
73539
|
init_cli_core();
|
|
73210
73540
|
init__projects();
|
|
73211
73541
|
init_client();
|
|
73212
|
-
async function
|
|
73542
|
+
async function action14(documentId, projectNames, options) {
|
|
73213
73543
|
const names = projectNames ?? [];
|
|
73214
73544
|
if (options.clear && names.length > 0) {
|
|
73215
73545
|
throw userError("Pass either project names or --clear, not both.", "Use --clear on its own to remove the document from all projects.");
|
|
@@ -73239,7 +73569,7 @@ async function action13(documentId, projectNames, options) {
|
|
|
73239
73569
|
println(c.dim(" This REPLACED the previous set — any project not listed is no longer associated."));
|
|
73240
73570
|
}
|
|
73241
73571
|
function registerDocumentSetProjects(parent) {
|
|
73242
|
-
parent.command("set-projects").description("Replace a document's project memberships with exactly the given set (or --clear to remove all).").argument("<document-id>", "UUID of the document.").argument("[project-names...]", "Project names to set (created if missing). Omit and pass --clear to remove all.").option("--clear", "Remove the document from all projects.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(
|
|
73572
|
+
parent.command("set-projects").description("Replace a document's project memberships with exactly the given set (or --clear to remove all).").argument("<document-id>", "UUID of the document.").argument("[project-names...]", "Project names to set (created if missing). Omit and pass --clear to remove all.").option("--clear", "Remove the document from all projects.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(action14);
|
|
73243
73573
|
}
|
|
73244
73574
|
|
|
73245
73575
|
// src/cli/commands/guides.ts
|
|
@@ -73289,7 +73619,7 @@ function registerGuides(parent) {
|
|
|
73289
73619
|
// src/cli/commands/project-create.ts
|
|
73290
73620
|
init_cli_core();
|
|
73291
73621
|
init_client();
|
|
73292
|
-
async function
|
|
73622
|
+
async function action15(name, options) {
|
|
73293
73623
|
const trimmed = name.trim();
|
|
73294
73624
|
if (!trimmed)
|
|
73295
73625
|
throw userError("Project name is required.");
|
|
@@ -73301,14 +73631,14 @@ async function action14(name, options) {
|
|
|
73301
73631
|
println(c.green(`✓ Created project "${data.name}" (id: ${data.id}).`));
|
|
73302
73632
|
}
|
|
73303
73633
|
function registerProjectCreate(parent) {
|
|
73304
|
-
parent.command("create").description("Create a new (empty) project.").argument("<name>", "Project name (must be unique).").option("--description <text>", "Optional project description.").action(
|
|
73634
|
+
parent.command("create").description("Create a new (empty) project.").argument("<name>", "Project name (must be unique).").option("--description <text>", "Optional project description.").action(action15);
|
|
73305
73635
|
}
|
|
73306
73636
|
|
|
73307
73637
|
// src/cli/commands/project-edit.ts
|
|
73308
73638
|
init_cli_core();
|
|
73309
73639
|
init_client();
|
|
73310
73640
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
73311
|
-
async function
|
|
73641
|
+
async function action16(target, options) {
|
|
73312
73642
|
const update = {};
|
|
73313
73643
|
if (options.name !== undefined) {
|
|
73314
73644
|
const n = options.name.trim();
|
|
@@ -73335,7 +73665,7 @@ async function action15(target, options) {
|
|
|
73335
73665
|
println(c.green(`✓ Updated project "${data.name}" (id: ${data.id}).`));
|
|
73336
73666
|
}
|
|
73337
73667
|
function registerProjectEdit(parent) {
|
|
73338
|
-
parent.command("edit").description("Rename a project and/or change its description.").argument("<name-or-id>", "Project name (exact match) or UUID.").option("--name <new-name>", "New project name.").option("--description <text>", "New project description.").action(
|
|
73668
|
+
parent.command("edit").description("Rename a project and/or change its description.").argument("<name-or-id>", "Project name (exact match) or UUID.").option("--name <new-name>", "New project name.").option("--description <text>", "New project description.").action(action16);
|
|
73339
73669
|
}
|
|
73340
73670
|
|
|
73341
73671
|
// src/cli/commands/version-archive.ts
|
|
@@ -77157,14 +77487,9 @@ async function checkEmbedderMismatch() {
|
|
|
77157
77487
|
};
|
|
77158
77488
|
}
|
|
77159
77489
|
}
|
|
77160
|
-
|
|
77161
|
-
async function checkContentFormat() {
|
|
77162
|
-
const settings = loadSettings();
|
|
77163
|
-
if (!settings.supabaseUrl || !settings.supabaseKey) {
|
|
77164
|
-
return { name: CONTENT_FORMAT_CHECK_NAME, status: "skipped", detail: "Supabase config missing; skipped." };
|
|
77165
|
-
}
|
|
77490
|
+
async function probeRpcJson(settings, fn) {
|
|
77166
77491
|
try {
|
|
77167
|
-
const url = `${settings.supabaseUrl.replace(/\/$/, "")}/rest/v1/rpc
|
|
77492
|
+
const url = `${settings.supabaseUrl.replace(/\/$/, "")}/rest/v1/rpc/${fn}`;
|
|
77168
77493
|
const resp = await fetch(url, {
|
|
77169
77494
|
method: "POST",
|
|
77170
77495
|
headers: {
|
|
@@ -77174,14 +77499,38 @@ async function checkContentFormat() {
|
|
|
77174
77499
|
},
|
|
77175
77500
|
body: "{}"
|
|
77176
77501
|
});
|
|
77177
|
-
if (
|
|
77502
|
+
if (resp.status === 404)
|
|
77503
|
+
return { kind: "absent" };
|
|
77504
|
+
if (!resp.ok)
|
|
77505
|
+
return { kind: "error", detail: `HTTP ${resp.status}` };
|
|
77506
|
+
return { kind: "ok", rows: await resp.json() };
|
|
77507
|
+
} catch (err) {
|
|
77508
|
+
return { kind: "error", detail: err instanceof Error ? err.message : String(err) };
|
|
77509
|
+
}
|
|
77510
|
+
}
|
|
77511
|
+
var CONTENT_FORMAT_CHECK_NAME = "content format";
|
|
77512
|
+
async function checkContentFormat() {
|
|
77513
|
+
const settings = loadSettings();
|
|
77514
|
+
if (!settings.supabaseUrl || !settings.supabaseKey) {
|
|
77515
|
+
return { name: CONTENT_FORMAT_CHECK_NAME, status: "skipped", detail: "Supabase config missing; skipped." };
|
|
77516
|
+
}
|
|
77517
|
+
{
|
|
77518
|
+
const probe = await probeRpcJson(settings, "cerefox_content_format_stats");
|
|
77519
|
+
if (probe.kind === "absent") {
|
|
77178
77520
|
return {
|
|
77179
77521
|
name: CONTENT_FORMAT_CHECK_NAME,
|
|
77180
77522
|
status: "skipped",
|
|
77181
|
-
detail:
|
|
77523
|
+
detail: "format stats unavailable; deploy schema 0.8.0 to enable."
|
|
77182
77524
|
};
|
|
77183
77525
|
}
|
|
77184
|
-
|
|
77526
|
+
if (probe.kind === "error") {
|
|
77527
|
+
return {
|
|
77528
|
+
name: CONTENT_FORMAT_CHECK_NAME,
|
|
77529
|
+
status: "skipped",
|
|
77530
|
+
detail: `content-format check skipped (${probe.detail}).`
|
|
77531
|
+
};
|
|
77532
|
+
}
|
|
77533
|
+
const rows = probe.rows;
|
|
77185
77534
|
const legacy = rows[0]?.legacy_docs ?? 0;
|
|
77186
77535
|
const total = rows[0]?.total_docs ?? 0;
|
|
77187
77536
|
if (legacy === 0) {
|
|
@@ -77197,13 +77546,39 @@ async function checkContentFormat() {
|
|
|
77197
77546
|
detail: `${legacy} of ${total} document(s) use the legacy reconstruction format (format 1).`,
|
|
77198
77547
|
hint: "Harmless — they auto-convert on next edit. To convert them all now: `cerefox server migrate-format` (re-embeds, so try `--dry-run` first). To read what chunk formats are: `cerefox guides show content-format`."
|
|
77199
77548
|
};
|
|
77200
|
-
}
|
|
77549
|
+
}
|
|
77550
|
+
}
|
|
77551
|
+
var METADATA_HEALTH_CHECK_NAME = "metadata health";
|
|
77552
|
+
async function checkMetadataHealth() {
|
|
77553
|
+
const settings = loadSettings();
|
|
77554
|
+
if (!settings.supabaseUrl || !settings.supabaseKey) {
|
|
77555
|
+
return { name: METADATA_HEALTH_CHECK_NAME, status: "skipped", detail: "Supabase config missing; skipped." };
|
|
77556
|
+
}
|
|
77557
|
+
const probe = await probeRpcJson(settings, "cerefox_metadata_health");
|
|
77558
|
+
if (probe.kind === "absent") {
|
|
77201
77559
|
return {
|
|
77202
|
-
name:
|
|
77560
|
+
name: METADATA_HEALTH_CHECK_NAME,
|
|
77561
|
+
status: "skipped",
|
|
77562
|
+
detail: "metadata-health RPC not deployed; deploy schema 0.12.2 to enable."
|
|
77563
|
+
};
|
|
77564
|
+
}
|
|
77565
|
+
if (probe.kind === "error") {
|
|
77566
|
+
return {
|
|
77567
|
+
name: METADATA_HEALTH_CHECK_NAME,
|
|
77203
77568
|
status: "skipped",
|
|
77204
|
-
detail: `
|
|
77569
|
+
detail: `metadata-health check skipped (${probe.detail}).`
|
|
77205
77570
|
};
|
|
77206
77571
|
}
|
|
77572
|
+
if (probe.rows.length === 0) {
|
|
77573
|
+
return { name: METADATA_HEALTH_CHECK_NAME, status: "ok", detail: "all document metadata is well-formed" };
|
|
77574
|
+
}
|
|
77575
|
+
const sample = probe.rows.slice(0, 3).map((r) => `"${r.document_title}" (${r.metadata_type})`).join(", ");
|
|
77576
|
+
return {
|
|
77577
|
+
name: METADATA_HEALTH_CHECK_NAME,
|
|
77578
|
+
status: "skipped",
|
|
77579
|
+
detail: `${probe.rows.length} document(s) hold non-object metadata: ${sample}${probe.rows.length > 3 ? ", …" : ""}.`,
|
|
77580
|
+
hint: "Writes that would merge onto these rows are refused (#212). Repair each with `cerefox document set-metadata <id> --replace --json '<the intended object>'`."
|
|
77581
|
+
};
|
|
77207
77582
|
}
|
|
77208
77583
|
function hasCerefoxInJsonFile(path) {
|
|
77209
77584
|
if (!existsSync10(path))
|
|
@@ -77414,6 +77789,7 @@ async function runAllChecks(opts = {}) {
|
|
|
77414
77789
|
{ name: "schema + RPCs", phase: "Reading schema + RPC version", run: () => checkSchemaVersion() },
|
|
77415
77790
|
{ name: "embedder", phase: "Checking embedder consistency", run: () => checkEmbedderMismatch() },
|
|
77416
77791
|
{ name: "content format", phase: "Checking chunk reconstruction format", run: () => checkContentFormat() },
|
|
77792
|
+
{ name: "metadata health", phase: "Checking metadata well-formedness", run: () => checkMetadataHealth() },
|
|
77417
77793
|
{ name: "edge functions", phase: "Probing Edge Function versions", run: () => checkEdgeFunctionsCompat() },
|
|
77418
77794
|
{ name: "postgres", phase: "Probing Postgres DDL endpoint", run: () => checkPostgres() },
|
|
77419
77795
|
{ name: "mcp clients", phase: "Scanning MCP client configs", run: () => checkMcpConfigs() }
|
|
@@ -77442,7 +77818,7 @@ function symbol(status) {
|
|
|
77442
77818
|
return cErr.dim("ℹ");
|
|
77443
77819
|
}
|
|
77444
77820
|
}
|
|
77445
|
-
async function
|
|
77821
|
+
async function action17(options) {
|
|
77446
77822
|
const useSpinner = !options.json && process.stderr.isTTY;
|
|
77447
77823
|
const spinner = useSpinner ? ora({ text: "Starting checks…", spinner: "dots", stream: process.stderr }).start() : null;
|
|
77448
77824
|
const results = await runAllChecks({
|
|
@@ -77506,13 +77882,13 @@ async function action16(options) {
|
|
|
77506
77882
|
process.exit(1);
|
|
77507
77883
|
}
|
|
77508
77884
|
function registerDoctor(program2) {
|
|
77509
|
-
program2.command("doctor").description("Run diagnostic checks against the installed Cerefox.").option("--json", "Emit machine-readable JSON (no colours, structured output).").option("--strict", "Exit non-zero when any check warns (default: only errors fail).").action(
|
|
77885
|
+
program2.command("doctor").description("Run diagnostic checks against the installed Cerefox.").option("--json", "Emit machine-readable JSON (no colours, structured output).").option("--strict", "Exit non-zero when any check warns (default: only errors fail).").action(action17);
|
|
77510
77886
|
}
|
|
77511
77887
|
|
|
77512
77888
|
// src/cli/commands/get-audit-log.ts
|
|
77513
77889
|
init_cli_core();
|
|
77514
77890
|
init_client();
|
|
77515
|
-
async function
|
|
77891
|
+
async function action18(options) {
|
|
77516
77892
|
const limit = parsePositiveInt(options.limit, "--limit", 50);
|
|
77517
77893
|
const client = getClient();
|
|
77518
77894
|
const data = await client.rpc("cerefox_list_audit_entries", {
|
|
@@ -77550,7 +77926,7 @@ async function action17(options) {
|
|
|
77550
77926
|
})));
|
|
77551
77927
|
}
|
|
77552
77928
|
function registerGetAuditLog(program2) {
|
|
77553
|
-
program2.command("get-audit-log").description("Query the audit log with optional filters.").option("-d, --document-id <uuid>", "Filter by document.").option("-a, --author <name>", "Filter by author.").option("-o, --operation <type>", "Filter by operation: create, update-content, update-metadata, delete, restore.").option("--since <iso>", "Lower-bound ISO timestamp.").option("--until <iso>", "Upper-bound ISO timestamp.").option("-l, --limit <n>", "Maximum entries (max 200).", "50").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(
|
|
77929
|
+
program2.command("get-audit-log").description("Query the audit log with optional filters.").option("-d, --document-id <uuid>", "Filter by document.").option("-a, --author <name>", "Filter by author.").option("-o, --operation <type>", "Filter by operation: create, update-content, update-metadata, delete, restore.").option("--since <iso>", "Lower-bound ISO timestamp.").option("--until <iso>", "Upper-bound ISO timestamp.").option("-l, --limit <n>", "Maximum entries (max 200).", "50").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(action18);
|
|
77554
77930
|
}
|
|
77555
77931
|
|
|
77556
77932
|
// src/cli/commands/relation.ts
|
|
@@ -77686,7 +78062,7 @@ init_cli_core();
|
|
|
77686
78062
|
init_cli_core();
|
|
77687
78063
|
init_partial_edits();
|
|
77688
78064
|
init_client();
|
|
77689
|
-
async function
|
|
78065
|
+
async function action19(documentId, options) {
|
|
77690
78066
|
const section = (options.section ?? "").trim() || null;
|
|
77691
78067
|
if (section && options.outline) {
|
|
77692
78068
|
throw userError("Pass either --outline (the whole structure) or --section (one section's text), not both.");
|
|
@@ -77783,7 +78159,7 @@ async function action18(documentId, options) {
|
|
|
77783
78159
|
println(doc.full_content);
|
|
77784
78160
|
}
|
|
77785
78161
|
function registerGetDoc(program2) {
|
|
77786
|
-
program2.command("get-doc").description("Retrieve the full content of a document by ID.").argument("<document-id>", "UUID of the document.").option("--version-id <uuid>", "Specific archived version (default: current).").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").option("--outline", "Show the heading structure, per-section sizes and content_hash instead of the content. Cheap, and the paths are the anchors the edit commands take.").option("--section <anchor>", "Show ONE section's text instead of the whole document: exactly what a replace_section on this anchor would overwrite. Pass the bare heading line when it is unique, or the full ' > ' path from --outline when it repeats.").option("--section-part <part>", "own_body | subtree — only when the target section has child sections, where 'the end' means two different places. You are told (with both options) whenever it is needed.").action(
|
|
78162
|
+
program2.command("get-doc").description("Retrieve the full content of a document by ID.").argument("<document-id>", "UUID of the document.").option("--version-id <uuid>", "Specific archived version (default: current).").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").option("--outline", "Show the heading structure, per-section sizes and content_hash instead of the content. Cheap, and the paths are the anchors the edit commands take.").option("--section <anchor>", "Show ONE section's text instead of the whole document: exactly what a replace_section on this anchor would overwrite. Pass the bare heading line when it is unique, or the full ' > ' path from --outline when it repeats.").option("--section-part <part>", "own_body | subtree — only when the target section has child sections, where 'the end' means two different places. You are told (with both options) whenever it is needed.").action(action19);
|
|
77787
78163
|
}
|
|
77788
78164
|
|
|
77789
78165
|
// src/cli/commands/ingest.ts
|
|
@@ -77895,15 +78271,20 @@ class IngestionDbBridge {
|
|
|
77895
78271
|
const rows = data ?? [];
|
|
77896
78272
|
return rows.length > 0 ? rows[0] : null;
|
|
77897
78273
|
}
|
|
78274
|
+
async findPreferLive(column, value) {
|
|
78275
|
+
const live = await this.supabase.from("cerefox_documents").select("*").eq(column, value).is("deleted_at", null).order("updated_at", { ascending: false }).limit(1);
|
|
78276
|
+
const liveRows = live.data ?? [];
|
|
78277
|
+
if (liveRows.length > 0)
|
|
78278
|
+
return liveRows[0];
|
|
78279
|
+
const any = await this.supabase.from("cerefox_documents").select("*").eq(column, value).order("updated_at", { ascending: false }).limit(1);
|
|
78280
|
+
const anyRows = any.data ?? [];
|
|
78281
|
+
return anyRows.length > 0 ? anyRows[0] : null;
|
|
78282
|
+
}
|
|
77898
78283
|
async findDocumentByTitle(title) {
|
|
77899
|
-
|
|
77900
|
-
const rows = data ?? [];
|
|
77901
|
-
return rows.length > 0 ? rows[0] : null;
|
|
78284
|
+
return this.findPreferLive("title", title);
|
|
77902
78285
|
}
|
|
77903
78286
|
async findDocumentBySourcePath(sourcePath) {
|
|
77904
|
-
|
|
77905
|
-
const rows = data ?? [];
|
|
77906
|
-
return rows.length > 0 ? rows[0] : null;
|
|
78287
|
+
return this.findPreferLive("source_path", sourcePath);
|
|
77907
78288
|
}
|
|
77908
78289
|
async listChunksForDocument(documentId) {
|
|
77909
78290
|
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 +78323,8 @@ class IngestionDbBridge {
|
|
|
77942
78323
|
if (error2) {
|
|
77943
78324
|
const msg = error2.message ?? JSON.stringify(error2);
|
|
77944
78325
|
if (msg.includes("CEREFOX_CONFLICT")) {
|
|
77945
|
-
const current = msg
|
|
77946
|
-
throw new ConcurrencyConflictError(args.documentId ?? "", current, msg);
|
|
78326
|
+
const { current } = extractConflictHashes(msg);
|
|
78327
|
+
throw new ConcurrencyConflictError(args.documentId ?? "", current === "unknown" ? null : current, msg);
|
|
77947
78328
|
}
|
|
77948
78329
|
if (msg.includes("CEREFOX_TOKEN_REQUIRED")) {
|
|
77949
78330
|
throw new ConcurrencyTokenRequiredError(msg);
|
|
@@ -78126,12 +78507,33 @@ class IngestionPipeline {
|
|
|
78126
78507
|
}
|
|
78127
78508
|
if (updateExisting) {
|
|
78128
78509
|
let existingDoc = null;
|
|
78510
|
+
let matchedBySourcePath = false;
|
|
78129
78511
|
if (sourcePathOpt) {
|
|
78130
78512
|
existingDoc = await this.db.findDocumentBySourcePath(sourcePathOpt);
|
|
78513
|
+
matchedBySourcePath = existingDoc !== null;
|
|
78131
78514
|
}
|
|
78132
78515
|
if (!existingDoc) {
|
|
78133
78516
|
existingDoc = await this.db.findDocumentByTitle(title);
|
|
78134
78517
|
}
|
|
78518
|
+
if (existingDoc?.deleted_at) {
|
|
78519
|
+
if (matchedBySourcePath && lastWriteWins) {
|
|
78520
|
+
const projIds = await this.db.getDocumentProjectIds(existingDoc.id);
|
|
78521
|
+
return {
|
|
78522
|
+
documentId: existingDoc.id,
|
|
78523
|
+
title: existingDoc.title ?? title,
|
|
78524
|
+
chunkCount: existingDoc.chunk_count ?? 0,
|
|
78525
|
+
totalChars: existingDoc.total_chars ?? 0,
|
|
78526
|
+
action: "skipped",
|
|
78527
|
+
reindexed: false,
|
|
78528
|
+
projectIds: projIds,
|
|
78529
|
+
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.`,
|
|
78530
|
+
contentHash: existingDoc.content_hash ?? ""
|
|
78531
|
+
};
|
|
78532
|
+
}
|
|
78533
|
+
if (!matchedBySourcePath) {
|
|
78534
|
+
existingDoc = null;
|
|
78535
|
+
}
|
|
78536
|
+
}
|
|
78135
78537
|
if (existingDoc) {
|
|
78136
78538
|
let fullSetResolved = null;
|
|
78137
78539
|
if (listFormProvided) {
|
|
@@ -78174,7 +78576,7 @@ class IngestionPipeline {
|
|
|
78174
78576
|
action: "skipped",
|
|
78175
78577
|
reindexed: false,
|
|
78176
78578
|
projectIds: existingProjectIds,
|
|
78177
|
-
note: "",
|
|
78579
|
+
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
78580
|
contentHash: existingByHash.content_hash ?? hash
|
|
78179
78581
|
};
|
|
78180
78582
|
}
|
|
@@ -78246,11 +78648,15 @@ class IngestionPipeline {
|
|
|
78246
78648
|
if (!existing) {
|
|
78247
78649
|
throw new Error(`Document ${JSON.stringify(documentId)} not found`);
|
|
78248
78650
|
}
|
|
78651
|
+
if (existing.deleted_at) {
|
|
78652
|
+
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.`);
|
|
78653
|
+
}
|
|
78249
78654
|
const newHash = contentHash(text);
|
|
78250
78655
|
const contentUnchanged = newHash === existing.content_hash;
|
|
78656
|
+
const expectedHashTrimmed = expectedContentHash?.trim() || null;
|
|
78251
78657
|
if (!contentUnchanged) {
|
|
78252
|
-
if (!lastWriteWins &&
|
|
78253
|
-
throw new ConcurrencyConflictError(documentId, existing.content_hash, `CEREFOX_CONFLICT: document ${documentId} changed since it was read ` + `(expected hash ${
|
|
78658
|
+
if (!lastWriteWins && expectedHashTrimmed && expectedHashTrimmed !== existing.content_hash) {
|
|
78659
|
+
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
78660
|
}
|
|
78255
78661
|
const collision = await this.db.getDocumentByHash(newHash);
|
|
78256
78662
|
if (collision && collision.id !== documentId) {
|
|
@@ -78351,7 +78757,7 @@ class IngestionPipeline {
|
|
|
78351
78757
|
author,
|
|
78352
78758
|
authorType,
|
|
78353
78759
|
sourceLabel: sourceLabel ?? source ?? "manual",
|
|
78354
|
-
expectedContentHash:
|
|
78760
|
+
expectedContentHash: expectedHashTrimmed ?? (forceRechunk && contentUnchanged ? existing.content_hash : null),
|
|
78355
78761
|
lastWriteWins
|
|
78356
78762
|
});
|
|
78357
78763
|
let finalProjectIds;
|
|
@@ -78417,7 +78823,7 @@ async function readContent(path, paste) {
|
|
|
78417
78823
|
const titleFromPath = basename2(path, extname3(path));
|
|
78418
78824
|
return { content, titleFromPath };
|
|
78419
78825
|
}
|
|
78420
|
-
async function
|
|
78826
|
+
async function action20(path, options) {
|
|
78421
78827
|
const { content, titleFromPath } = await readContent(path, Boolean(options.paste));
|
|
78422
78828
|
const updatingById = Boolean(options.documentId);
|
|
78423
78829
|
let title = options.title ?? (updatingById ? null : titleFromPath);
|
|
@@ -78508,7 +78914,7 @@ async function action19(path, options) {
|
|
|
78508
78914
|
}
|
|
78509
78915
|
}
|
|
78510
78916
|
function registerIngest(program2) {
|
|
78511
|
-
program2.command("ingest").description("Ingest a file (or stdin paste) into the knowledge base.").argument("[path]", "Path to the file to ingest. Omit when using --paste.").option("--paste", "Read content from stdin instead of a file.").option("-t, --title <title>", "Document title (required with --paste; defaults to filename without extension).").option("-p, --project-name <name>", "Single project membership (non-destructive on update).").option("-P, --project-names <names>", "Comma-separated full project membership set (destructive replace on update).").option("-m, --metadata <json>", "JSON metadata object.").option("--source <label>", 'Origin label. Omit it on an update and the document keeps the source it already has (#193); omit it on a create and it is recorded as "cli".').option("-u, --update-if-exists", "Update an existing doc with the same title.").option("-i, --document-id <uuid>", "Update a specific document by UUID (overrides --update-if-exists).").option("--expected-content-hash <sha256>", "Optimistic-concurrency token: the content_hash of the version this edit is based on (shown by `document get` / `search`). Required on content updates unless --last-write-wins.").option("--last-write-wins", "Skip the concurrency check and overwrite regardless of concurrent changes (recorded in the audit log).").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(
|
|
78917
|
+
program2.command("ingest").description("Ingest a file (or stdin paste) into the knowledge base.").argument("[path]", "Path to the file to ingest. Omit when using --paste.").option("--paste", "Read content from stdin instead of a file.").option("-t, --title <title>", "Document title (required with --paste; defaults to filename without extension).").option("-p, --project-name <name>", "Single project membership (non-destructive on update).").option("-P, --project-names <names>", "Comma-separated full project membership set (destructive replace on update).").option("-m, --metadata <json>", "JSON metadata object.").option("--source <label>", 'Origin label. Omit it on an update and the document keeps the source it already has (#193); omit it on a create and it is recorded as "cli".').option("-u, --update-if-exists", "Update an existing doc with the same title.").option("-i, --document-id <uuid>", "Update a specific document by UUID (overrides --update-if-exists).").option("--expected-content-hash <sha256>", "Optimistic-concurrency token: the content_hash of the version this edit is based on (shown by `document get` / `search`). Required on content updates unless --last-write-wins.").option("--last-write-wins", "Skip the concurrency check and overwrite regardless of concurrent changes (recorded in the audit log).").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(action20);
|
|
78512
78918
|
}
|
|
78513
78919
|
|
|
78514
78920
|
// src/cli/commands/document-partial-edit.ts
|
|
@@ -78611,7 +79017,7 @@ function walk(dir, extensions) {
|
|
|
78611
79017
|
}
|
|
78612
79018
|
return files;
|
|
78613
79019
|
}
|
|
78614
|
-
async function
|
|
79020
|
+
async function action21(dir, options) {
|
|
78615
79021
|
const extensions = new Set((options.extensions ?? ".md,.txt").split(",").map((e) => e.trim().toLowerCase()).map((e) => e.startsWith(".") ? e : "." + e).filter((e) => e.length > 0));
|
|
78616
79022
|
const files = walk(dir, extensions);
|
|
78617
79023
|
if (files.length === 0) {
|
|
@@ -78664,7 +79070,7 @@ async function action20(dir, options) {
|
|
|
78664
79070
|
outcomes.push({
|
|
78665
79071
|
file,
|
|
78666
79072
|
status: "ok",
|
|
78667
|
-
detail: `${result.action}: ${result.chunkCount} chunks`
|
|
79073
|
+
detail: `${result.action}: ${result.chunkCount} chunks${result.note ? ` (${result.note})` : ""}`
|
|
78668
79074
|
});
|
|
78669
79075
|
} catch (err) {
|
|
78670
79076
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -78686,7 +79092,7 @@ async function action20(dir, options) {
|
|
|
78686
79092
|
}
|
|
78687
79093
|
}
|
|
78688
79094
|
function registerIngestDir(program2) {
|
|
78689
|
-
program2.command("ingest-dir").description("Recursively ingest a directory of markdown / text files.").argument("<dir>", "Root directory to walk.").option("-p, --project-name <name>", "Project membership for all ingested docs.").option("-m, --metadata <json>", "JSON metadata applied to every doc.").option("--source <label>", 'Origin label. Omit it and each matched document keeps the source it already has (#193); newly created ones are recorded as "cli".').option("-u, --update-if-exists", "Update an existing doc with the same title.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").option("-e, --extensions <list>", "Comma-separated file extensions to ingest.", ".md,.txt").action(
|
|
79095
|
+
program2.command("ingest-dir").description("Recursively ingest a directory of markdown / text files.").argument("<dir>", "Root directory to walk.").option("-p, --project-name <name>", "Project membership for all ingested docs.").option("-m, --metadata <json>", "JSON metadata applied to every doc.").option("--source <label>", 'Origin label. Omit it and each matched document keeps the source it already has (#193); newly created ones are recorded as "cli".').option("-u, --update-if-exists", "Update an existing doc with the same title.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").option("-e, --extensions <list>", "Comma-separated file extensions to ingest.", ".md,.txt").action(action21);
|
|
78690
79096
|
}
|
|
78691
79097
|
|
|
78692
79098
|
// src/cli/commands/init.ts
|
|
@@ -79017,7 +79423,7 @@ function writeAnswersTo(target, answers) {
|
|
|
79017
79423
|
}
|
|
79018
79424
|
}
|
|
79019
79425
|
}
|
|
79020
|
-
async function
|
|
79426
|
+
async function action23(options) {
|
|
79021
79427
|
const homeEnv = join11(homedir7(), USER_STATE_DIR_NAME, ".env");
|
|
79022
79428
|
const cwdEnv = join11(process.cwd(), ".env");
|
|
79023
79429
|
const explicitDir = (process.env.CEREFOX_CONFIG_DIR ?? "").trim();
|
|
@@ -79126,13 +79532,13 @@ async function action22(options) {
|
|
|
79126
79532
|
await postWriteLifecycle(target, options);
|
|
79127
79533
|
}
|
|
79128
79534
|
function registerInit(program2) {
|
|
79129
|
-
program2.command("init").description("Interactive first-run setup (config, schema deploy stub, optional MCP wiring).").option("-c, --config <file>", "Non-interactive mode: read answers from a JSON file.").option("--force", "Overwrite existing configuration without prompting.").option("--skip-schema", "Skip the schema deploy step.").option("--skip-self-docs", "Skip the bundled self-doc ingest.").option("--skip-agent-config", "Skip the optional MCP agent wiring.").action(
|
|
79535
|
+
program2.command("init").description("Interactive first-run setup (config, schema deploy stub, optional MCP wiring).").option("-c, --config <file>", "Non-interactive mode: read answers from a JSON file.").option("--force", "Overwrite existing configuration without prompting.").option("--skip-schema", "Skip the schema deploy step.").option("--skip-self-docs", "Skip the bundled self-doc ingest.").option("--skip-agent-config", "Skip the optional MCP agent wiring.").action(action23);
|
|
79130
79536
|
}
|
|
79131
79537
|
|
|
79132
79538
|
// src/cli/commands/list-docs.ts
|
|
79133
79539
|
init_cli_core();
|
|
79134
79540
|
init_client();
|
|
79135
|
-
async function
|
|
79541
|
+
async function action24(options) {
|
|
79136
79542
|
const deleted = !!options.deleted;
|
|
79137
79543
|
const limit = parsePositiveInt(options.limit, "--limit", 100);
|
|
79138
79544
|
const client = getClient();
|
|
@@ -79188,13 +79594,13 @@ async function action23(options) {
|
|
|
79188
79594
|
}));
|
|
79189
79595
|
}
|
|
79190
79596
|
function registerListDocs(program2) {
|
|
79191
|
-
program2.command("list-docs").description("List documents in the knowledge base.").option("-p, --project <name>", "Filter to a specific project.").option("-l, --limit <n>", "Maximum docs to return.", "100").option("--deleted", "List soft-deleted (trashed) documents instead of active ones.").option("--json", "Emit machine-readable JSON.").action(
|
|
79597
|
+
program2.command("list-docs").description("List documents in the knowledge base.").option("-p, --project <name>", "Filter to a specific project.").option("-l, --limit <n>", "Maximum docs to return.", "100").option("--deleted", "List soft-deleted (trashed) documents instead of active ones.").option("--json", "Emit machine-readable JSON.").action(action24);
|
|
79192
79598
|
}
|
|
79193
79599
|
|
|
79194
79600
|
// src/cli/commands/list-metadata-keys.ts
|
|
79195
79601
|
init_cli_core();
|
|
79196
79602
|
init_client();
|
|
79197
|
-
async function
|
|
79603
|
+
async function action25(options) {
|
|
79198
79604
|
const client = getClient();
|
|
79199
79605
|
const data = await client.rpc("cerefox_list_metadata_keys");
|
|
79200
79606
|
if (data === null) {
|
|
@@ -79222,13 +79628,13 @@ async function action24(options) {
|
|
|
79222
79628
|
})));
|
|
79223
79629
|
}
|
|
79224
79630
|
function registerListMetadataKeys(program2) {
|
|
79225
|
-
program2.command("list-metadata-keys").description("List all metadata keys with document counts and example values.").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(
|
|
79631
|
+
program2.command("list-metadata-keys").description("List all metadata keys with document counts and example values.").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(action25);
|
|
79226
79632
|
}
|
|
79227
79633
|
|
|
79228
79634
|
// src/cli/commands/list-projects.ts
|
|
79229
79635
|
init_cli_core();
|
|
79230
79636
|
init_client();
|
|
79231
|
-
async function
|
|
79637
|
+
async function action26(options) {
|
|
79232
79638
|
const client = getClient();
|
|
79233
79639
|
const { data, error: error2 } = await client.raw.from("cerefox_projects").select("id, name, description, created_at").order("name", { ascending: true });
|
|
79234
79640
|
if (error2) {
|
|
@@ -79257,13 +79663,13 @@ async function action25(options) {
|
|
|
79257
79663
|
})), "(no projects)");
|
|
79258
79664
|
}
|
|
79259
79665
|
function registerListProjects(program2) {
|
|
79260
|
-
program2.command("list-projects").description("List all projects in the knowledge base.").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(
|
|
79666
|
+
program2.command("list-projects").description("List all projects in the knowledge base.").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(action26);
|
|
79261
79667
|
}
|
|
79262
79668
|
|
|
79263
79669
|
// src/cli/commands/list-versions.ts
|
|
79264
79670
|
init_cli_core();
|
|
79265
79671
|
init_client();
|
|
79266
|
-
async function
|
|
79672
|
+
async function action27(documentId, options) {
|
|
79267
79673
|
const client = getClient();
|
|
79268
79674
|
const data = await client.rpc("cerefox_list_document_versions", {
|
|
79269
79675
|
p_document_id: documentId
|
|
@@ -79304,7 +79710,7 @@ async function action26(documentId, options) {
|
|
|
79304
79710
|
})));
|
|
79305
79711
|
}
|
|
79306
79712
|
function registerListVersions(program2) {
|
|
79307
|
-
program2.command("list-versions").description("List archived versions of a document.").argument("<document-id>", "UUID of the document.").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(
|
|
79713
|
+
program2.command("list-versions").description("List archived versions of a document.").argument("<document-id>", "UUID of the document.").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(action27);
|
|
79308
79714
|
}
|
|
79309
79715
|
|
|
79310
79716
|
// src/cli/commands/mcp.ts
|
|
@@ -79336,7 +79742,7 @@ function registerEmbedderWarmup(program2) {
|
|
|
79336
79742
|
// src/cli/commands/metadata-search.ts
|
|
79337
79743
|
init_cli_core();
|
|
79338
79744
|
init_client();
|
|
79339
|
-
async function
|
|
79745
|
+
async function action28(options) {
|
|
79340
79746
|
const metadataFilter = parseJsonObjectArg(options.metadataFilter, "--metadata-filter") ?? {};
|
|
79341
79747
|
if (Object.keys(metadataFilter).length === 0 && !options.projectName && !options.updatedSince && !options.createdSince) {
|
|
79342
79748
|
throw userError("Provide at least one of: --metadata-filter, --project-name, --updated-since, or --created-since.", `Examples: --metadata-filter '{"type":"decision-log"}' · --project-name "research" (lists that project's docs).`);
|
|
@@ -79399,7 +79805,7 @@ async function action27(options) {
|
|
|
79399
79805
|
}
|
|
79400
79806
|
}
|
|
79401
79807
|
function registerMetadataSearch(program2) {
|
|
79402
|
-
program2.command("metadata-search").description("Find or list documents by metadata, project, or time criteria (no text query).").option("-f, --metadata-filter <json>", "JSON object; only docs whose metadata contains ALL pairs are returned. Optional — omit to list by --project-name / time range alone (at least one criterion is required).").option("-p, --project-name <name>", "Filter to a specific project.").option("--updated-since <iso>", "Only docs updated on/after this ISO timestamp.").option("--created-since <iso>", "Only docs created on/after this ISO timestamp.").option("--include-content", "Include full document text in results.").option("-l, --limit <n>", "Maximum docs to return.", "10").option("--max-bytes <n>", "Response size budget in bytes (with --include-content).", "200000").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(
|
|
79808
|
+
program2.command("metadata-search").description("Find or list documents by metadata, project, or time criteria (no text query).").option("-f, --metadata-filter <json>", "JSON object; only docs whose metadata contains ALL pairs are returned. Optional — omit to list by --project-name / time range alone (at least one criterion is required).").option("-p, --project-name <name>", "Filter to a specific project.").option("--updated-since <iso>", "Only docs updated on/after this ISO timestamp.").option("--created-since <iso>", "Only docs created on/after this ISO timestamp.").option("--include-content", "Include full document text in results.").option("-l, --limit <n>", "Maximum docs to return.", "10").option("--max-bytes <n>", "Response size budget in bytes (with --include-content).", "200000").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(action28);
|
|
79403
79809
|
}
|
|
79404
79810
|
|
|
79405
79811
|
// src/cli/commands/reindex.ts
|
|
@@ -79425,7 +79831,7 @@ function warnLargeBulkWrite(opts) {
|
|
|
79425
79831
|
}
|
|
79426
79832
|
|
|
79427
79833
|
// src/cli/commands/reindex.ts
|
|
79428
|
-
async function
|
|
79834
|
+
async function action29(options) {
|
|
79429
79835
|
const settings = loadSettings();
|
|
79430
79836
|
if (!settings.supabaseUrl || !settings.supabaseKey) {
|
|
79431
79837
|
throw userError("Supabase credentials not configured — run `cerefox init` first.");
|
|
@@ -79515,7 +79921,7 @@ ${c2.content}`;
|
|
|
79515
79921
|
}
|
|
79516
79922
|
}
|
|
79517
79923
|
function registerReindex(program2) {
|
|
79518
|
-
program2.command("reindex").description("Re-embed existing document chunks (v0.7+).").option("--all", "Reindex every chunk regardless of embedder.").option("--batch <n>", "Chunks per OpenAI batch call. Capped at 96 internally.", "32").option("--dry-run", "Show counts without re-embedding.").option("-i, --document-id <uuid>", "Limit reindex to a single document.").action(
|
|
79924
|
+
program2.command("reindex").description("Re-embed existing document chunks (v0.7+).").option("--all", "Reindex every chunk regardless of embedder.").option("--batch <n>", "Chunks per OpenAI batch call. Capped at 96 internally.", "32").option("--dry-run", "Show counts without re-embedding.").option("-i, --document-id <uuid>", "Limit reindex to a single document.").action(action29);
|
|
79519
79925
|
}
|
|
79520
79926
|
|
|
79521
79927
|
// src/cli/commands/migrate-format.ts
|
|
@@ -79523,7 +79929,7 @@ init_cli_core();
|
|
|
79523
79929
|
init_config();
|
|
79524
79930
|
init_client();
|
|
79525
79931
|
var CURRENT_FORMAT = 2;
|
|
79526
|
-
async function
|
|
79932
|
+
async function action30(options) {
|
|
79527
79933
|
const settings = loadSettings();
|
|
79528
79934
|
const client = getClient();
|
|
79529
79935
|
const supabase = client.raw;
|
|
@@ -79650,7 +80056,7 @@ async function action29(options) {
|
|
|
79650
80056
|
}
|
|
79651
80057
|
}
|
|
79652
80058
|
function registerMigrateFormat(program2) {
|
|
79653
|
-
program2.command("migrate-format").description("Convert legacy-format documents to the current chunk format (re-chunks + re-embeds).").option("--dry-run", "Report how many documents would be converted; write nothing.").option("-l, --limit <n>", "Convert at most N documents (re-run to continue).").option("--document-id <uuid>", "Convert a single document.").option("--author <name>", "Recorded in the audit log for each conversion.").action(
|
|
80059
|
+
program2.command("migrate-format").description("Convert legacy-format documents to the current chunk format (re-chunks + re-embeds).").option("--dry-run", "Report how many documents would be converted; write nothing.").option("-l, --limit <n>", "Convert at most N documents (re-run to continue).").option("--document-id <uuid>", "Convert a single document.").option("--author <name>", "Recorded in the audit log for each conversion.").action(action30);
|
|
79654
80060
|
}
|
|
79655
80061
|
|
|
79656
80062
|
// src/cli/commands/restore.ts
|
|
@@ -79680,7 +80086,7 @@ function resolveBackupFile(target) {
|
|
|
79680
80086
|
}
|
|
79681
80087
|
return join12(path, candidates[0].name);
|
|
79682
80088
|
}
|
|
79683
|
-
async function
|
|
80089
|
+
async function action31(target, options) {
|
|
79684
80090
|
const file = resolveBackupFile(target);
|
|
79685
80091
|
let payload;
|
|
79686
80092
|
try {
|
|
@@ -79815,7 +80221,7 @@ async function action30(target, options) {
|
|
|
79815
80221
|
}
|
|
79816
80222
|
}
|
|
79817
80223
|
function registerRestore(program2) {
|
|
79818
|
-
program2.command("restore").description("Restore a JSON-snapshot backup into the knowledge base.").argument("<snapshot>", "Backup file (or directory; most recent is picked) produced by `cerefox backup`.").option("--dry-run", "Print what would be restored without writing.").option("-p, --project-name <name>", "Reserved for future use; currently ignored. Project memberships are restored from the backup itself (format 2+).").action(
|
|
80224
|
+
program2.command("restore").description("Restore a JSON-snapshot backup into the knowledge base.").argument("<snapshot>", "Backup file (or directory; most recent is picked) produced by `cerefox backup`.").option("--dry-run", "Print what would be restored without writing.").option("-p, --project-name <name>", "Reserved for future use; currently ignored. Project memberships are restored from the backup itself (format 2+).").action(action31);
|
|
79819
80225
|
}
|
|
79820
80226
|
|
|
79821
80227
|
// src/cli/commands/search.ts
|
|
@@ -79840,7 +80246,7 @@ async function embedQuery(query) {
|
|
|
79840
80246
|
}
|
|
79841
80247
|
|
|
79842
80248
|
// src/cli/commands/search.ts
|
|
79843
|
-
async function
|
|
80249
|
+
async function action32(query, options) {
|
|
79844
80250
|
if (!query || query.trim() === "") {
|
|
79845
80251
|
throw userError("Empty query.");
|
|
79846
80252
|
}
|
|
@@ -80006,7 +80412,7 @@ async function action31(query, options) {
|
|
|
80006
80412
|
}
|
|
80007
80413
|
}
|
|
80008
80414
|
function registerSearch(program2) {
|
|
80009
|
-
program2.command("search").description("Search the knowledge base (hybrid FTS + semantic).").argument("<query>", "Natural-language search query.").option("-c, --match-count <n>", "Maximum number of documents to return.", "5").option("-p, --project-name <name>", "Filter results to a specific project.").option("-f, --metadata-filter <json>", "JSON containment filter; only docs whose metadata contains ALL pairs are returned.").option("--mode <mode>", "Search mode: docs (default), hybrid, fts.", "docs").option("--alpha <float>", "Semantic weight 0..1 (default: CEREFOX_SEARCH_ALPHA; else 0.7).").option("--min-score <float>", "Minimum cosine similarity threshold (default: CEREFOX_MIN_SEARCH_SCORE; else 0.5, or 0.6 with the local embedder).").option("--min-term-coverage <float>", "OR-fallback keyword matches must cover at least this fraction of the query's meaningful terms to count as confident hits (default: CEREFOX_MIN_TERM_COVERAGE; else the server default 0.5; needs schema ≥ 0.9.1).").option("--max-bytes <n>", "Response size budget in bytes (default: CEREFOX_MAX_RESPONSE_BYTES or 200000).").option("-r, --requestor <name>", "Agent / user name (recorded in usage log).").option("--json", "Emit machine-readable JSON instead of the default text.").option("--only-metadata", "List matching docs (id, score, chunks, chars, partial/full) WITHOUT their content — like the web UI's collapsed result list. Grab a [id:…] then `cerefox document get <id>`.").action(
|
|
80415
|
+
program2.command("search").description("Search the knowledge base (hybrid FTS + semantic).").argument("<query>", "Natural-language search query.").option("-c, --match-count <n>", "Maximum number of documents to return.", "5").option("-p, --project-name <name>", "Filter results to a specific project.").option("-f, --metadata-filter <json>", "JSON containment filter; only docs whose metadata contains ALL pairs are returned.").option("--mode <mode>", "Search mode: docs (default), hybrid, fts.", "docs").option("--alpha <float>", "Semantic weight 0..1 (default: CEREFOX_SEARCH_ALPHA; else 0.7).").option("--min-score <float>", "Minimum cosine similarity threshold (default: CEREFOX_MIN_SEARCH_SCORE; else 0.5, or 0.6 with the local embedder).").option("--min-term-coverage <float>", "OR-fallback keyword matches must cover at least this fraction of the query's meaningful terms to count as confident hits (default: CEREFOX_MIN_TERM_COVERAGE; else the server default 0.5; needs schema ≥ 0.9.1).").option("--max-bytes <n>", "Response size budget in bytes (default: CEREFOX_MAX_RESPONSE_BYTES or 200000).").option("-r, --requestor <name>", "Agent / user name (recorded in usage log).").option("--json", "Emit machine-readable JSON instead of the default text.").option("--only-metadata", "List matching docs (id, score, chunks, chars, partial/full) WITHOUT their content — like the web UI's collapsed result list. Grab a [id:…] then `cerefox document get <id>`.").action(action32);
|
|
80010
80416
|
}
|
|
80011
80417
|
|
|
80012
80418
|
// src/cli/commands/self-update.ts
|
|
@@ -80053,7 +80459,7 @@ async function fetchLatestVersion() {
|
|
|
80053
80459
|
}
|
|
80054
80460
|
return body.version;
|
|
80055
80461
|
}
|
|
80056
|
-
async function
|
|
80462
|
+
async function action33(options) {
|
|
80057
80463
|
let target;
|
|
80058
80464
|
try {
|
|
80059
80465
|
target = options.version ?? await fetchLatestVersion();
|
|
@@ -80106,7 +80512,7 @@ async function action32(options) {
|
|
|
80106
80512
|
}
|
|
80107
80513
|
function registerSelfUpdate(program2) {
|
|
80108
80514
|
const desc = "Upgrade Cerefox in place. Alias: `cerefox upgrade`.";
|
|
80109
|
-
const declaration = (cmd) => cmd.description(desc).option("--check", "Print current vs latest; do nothing.").option("--yes", "Non-interactive (skip confirmation).").option("--version <version>", "Pin a specific version (e.g. 0.5.1 or 0.6.0-rc.1).").action(
|
|
80515
|
+
const declaration = (cmd) => cmd.description(desc).option("--check", "Print current vs latest; do nothing.").option("--yes", "Non-interactive (skip confirmation).").option("--version <version>", "Pin a specific version (e.g. 0.5.1 or 0.6.0-rc.1).").action(action33);
|
|
80110
80516
|
declaration(program2.command("self-update"));
|
|
80111
80517
|
declaration(program2.command("upgrade"));
|
|
80112
80518
|
}
|
|
@@ -80125,7 +80531,7 @@ function symbol2(status) {
|
|
|
80125
80531
|
return cErr.dim("ℹ");
|
|
80126
80532
|
}
|
|
80127
80533
|
}
|
|
80128
|
-
async function
|
|
80534
|
+
async function action34(options) {
|
|
80129
80535
|
const useSpinner = !options.json && process.stderr.isTTY;
|
|
80130
80536
|
const spinner = useSpinner ? ora({ text: "Starting checks…", spinner: "dots", stream: process.stderr }).start() : null;
|
|
80131
80537
|
const results = await runFastChecks({
|
|
@@ -80146,7 +80552,7 @@ async function action33(options) {
|
|
|
80146
80552
|
}
|
|
80147
80553
|
}
|
|
80148
80554
|
function registerStatus(program2) {
|
|
80149
|
-
program2.command("status").description("Quick sanity check (fast subset of `cerefox doctor`).").option("--json", "Emit machine-readable JSON.").action(
|
|
80555
|
+
program2.command("status").description("Quick sanity check (fast subset of `cerefox doctor`).").option("--json", "Emit machine-readable JSON.").action(action34);
|
|
80150
80556
|
}
|
|
80151
80557
|
|
|
80152
80558
|
// src/cli/commands/token.ts
|
|
@@ -80179,10 +80585,10 @@ function upsertEnvVar(path, key, value, opts = {}) {
|
|
|
80179
80585
|
}
|
|
80180
80586
|
const re = new RegExp(`^(\\s*)${escapeRegExp(key)}=.*$`, "m");
|
|
80181
80587
|
let next;
|
|
80182
|
-
let
|
|
80588
|
+
let action35;
|
|
80183
80589
|
if (re.test(original)) {
|
|
80184
80590
|
next = original.replace(re, `$1${line}`);
|
|
80185
|
-
|
|
80591
|
+
action35 = "updated";
|
|
80186
80592
|
} else {
|
|
80187
80593
|
const base = original.endsWith(`
|
|
80188
80594
|
`) ? original : `${original}
|
|
@@ -80190,10 +80596,10 @@ function upsertEnvVar(path, key, value, opts = {}) {
|
|
|
80190
80596
|
next = `${base}
|
|
80191
80597
|
${header}${line}
|
|
80192
80598
|
`;
|
|
80193
|
-
|
|
80599
|
+
action35 = "added";
|
|
80194
80600
|
}
|
|
80195
80601
|
writeFileSync5(path, next);
|
|
80196
|
-
return { path, action:
|
|
80602
|
+
return { path, action: action35, backupPath };
|
|
80197
80603
|
}
|
|
80198
80604
|
function readEnvVar(path, key) {
|
|
80199
80605
|
if (!existsSync14(path))
|
|
@@ -80371,13 +80777,13 @@ var WSContext = class {
|
|
|
80371
80777
|
this.#init.close(code, reason);
|
|
80372
80778
|
}
|
|
80373
80779
|
};
|
|
80374
|
-
var defineWebSocketHelper = (
|
|
80780
|
+
var defineWebSocketHelper = (handler14) => {
|
|
80375
80781
|
return (...args) => {
|
|
80376
80782
|
if (typeof args[0] === "function") {
|
|
80377
80783
|
const [createEvents, options] = args;
|
|
80378
80784
|
return async function upgradeWebSocket(c2, next) {
|
|
80379
80785
|
const events = await createEvents(c2);
|
|
80380
|
-
const result = await
|
|
80786
|
+
const result = await handler14(c2, events, options);
|
|
80381
80787
|
if (result) {
|
|
80382
80788
|
return result;
|
|
80383
80789
|
}
|
|
@@ -80386,7 +80792,7 @@ var defineWebSocketHelper = (handler12) => {
|
|
|
80386
80792
|
} else {
|
|
80387
80793
|
const [c2, events, options] = args;
|
|
80388
80794
|
return (async () => {
|
|
80389
|
-
const upgraded = await
|
|
80795
|
+
const upgraded = await handler14(c2, events, options);
|
|
80390
80796
|
if (!upgraded) {
|
|
80391
80797
|
throw new Error("Failed to upgrade WebSocket");
|
|
80392
80798
|
}
|
|
@@ -81979,16 +82385,16 @@ var compose = (middleware, onError, onNotFound) => {
|
|
|
81979
82385
|
index = i;
|
|
81980
82386
|
let res;
|
|
81981
82387
|
let isError = false;
|
|
81982
|
-
let
|
|
82388
|
+
let handler14;
|
|
81983
82389
|
if (middleware[i]) {
|
|
81984
|
-
|
|
82390
|
+
handler14 = middleware[i][0][0];
|
|
81985
82391
|
context2.req.routeIndex = i;
|
|
81986
82392
|
} else {
|
|
81987
|
-
|
|
82393
|
+
handler14 = i === middleware.length && next || undefined;
|
|
81988
82394
|
}
|
|
81989
|
-
if (
|
|
82395
|
+
if (handler14) {
|
|
81990
82396
|
try {
|
|
81991
|
-
res = await
|
|
82397
|
+
res = await handler14(context2, () => dispatch(i + 1));
|
|
81992
82398
|
} catch (err) {
|
|
81993
82399
|
if (err instanceof Error && onError) {
|
|
81994
82400
|
context2.error = err;
|
|
@@ -82672,8 +83078,8 @@ var Hono = class _Hono {
|
|
|
82672
83078
|
} else {
|
|
82673
83079
|
this.#addRoute(method, this.#path, args1);
|
|
82674
83080
|
}
|
|
82675
|
-
args.forEach((
|
|
82676
|
-
this.#addRoute(method, this.#path,
|
|
83081
|
+
args.forEach((handler14) => {
|
|
83082
|
+
this.#addRoute(method, this.#path, handler14);
|
|
82677
83083
|
});
|
|
82678
83084
|
return this;
|
|
82679
83085
|
};
|
|
@@ -82682,8 +83088,8 @@ var Hono = class _Hono {
|
|
|
82682
83088
|
for (const p of [path].flat()) {
|
|
82683
83089
|
this.#path = p;
|
|
82684
83090
|
for (const m of [method].flat()) {
|
|
82685
|
-
handlers.map((
|
|
82686
|
-
this.#addRoute(m.toUpperCase(), this.#path,
|
|
83091
|
+
handlers.map((handler14) => {
|
|
83092
|
+
this.#addRoute(m.toUpperCase(), this.#path, handler14);
|
|
82687
83093
|
});
|
|
82688
83094
|
}
|
|
82689
83095
|
}
|
|
@@ -82696,8 +83102,8 @@ var Hono = class _Hono {
|
|
|
82696
83102
|
this.#path = "*";
|
|
82697
83103
|
handlers.unshift(arg1);
|
|
82698
83104
|
}
|
|
82699
|
-
handlers.forEach((
|
|
82700
|
-
this.#addRoute(METHOD_NAME_ALL, this.#path,
|
|
83105
|
+
handlers.forEach((handler14) => {
|
|
83106
|
+
this.#addRoute(METHOD_NAME_ALL, this.#path, handler14);
|
|
82701
83107
|
});
|
|
82702
83108
|
return this;
|
|
82703
83109
|
};
|
|
@@ -82720,14 +83126,14 @@ var Hono = class _Hono {
|
|
|
82720
83126
|
route(path, app) {
|
|
82721
83127
|
const subApp = this.basePath(path);
|
|
82722
83128
|
app.routes.map((r) => {
|
|
82723
|
-
let
|
|
83129
|
+
let handler14;
|
|
82724
83130
|
if (app.errorHandler === errorHandler) {
|
|
82725
|
-
|
|
83131
|
+
handler14 = r.handler;
|
|
82726
83132
|
} else {
|
|
82727
|
-
|
|
82728
|
-
|
|
83133
|
+
handler14 = async (c2, next) => (await compose([], app.errorHandler)(c2, () => r.handler(c2, next))).res;
|
|
83134
|
+
handler14[COMPOSED_HANDLER] = r.handler;
|
|
82729
83135
|
}
|
|
82730
|
-
subApp.#addRoute(r.method, r.path,
|
|
83136
|
+
subApp.#addRoute(r.method, r.path, handler14, r.basePath);
|
|
82731
83137
|
});
|
|
82732
83138
|
return this;
|
|
82733
83139
|
}
|
|
@@ -82736,12 +83142,12 @@ var Hono = class _Hono {
|
|
|
82736
83142
|
subApp._basePath = mergePath(this._basePath, path);
|
|
82737
83143
|
return subApp;
|
|
82738
83144
|
}
|
|
82739
|
-
onError = (
|
|
82740
|
-
this.errorHandler =
|
|
83145
|
+
onError = (handler14) => {
|
|
83146
|
+
this.errorHandler = handler14;
|
|
82741
83147
|
return this;
|
|
82742
83148
|
};
|
|
82743
|
-
notFound = (
|
|
82744
|
-
this.#notFoundHandler =
|
|
83149
|
+
notFound = (handler14) => {
|
|
83150
|
+
this.#notFoundHandler = handler14;
|
|
82745
83151
|
return this;
|
|
82746
83152
|
};
|
|
82747
83153
|
mount(path, applicationHandler, options) {
|
|
@@ -82778,26 +83184,26 @@ var Hono = class _Hono {
|
|
|
82778
83184
|
return new Request(url, request);
|
|
82779
83185
|
};
|
|
82780
83186
|
})();
|
|
82781
|
-
const
|
|
83187
|
+
const handler14 = async (c2, next) => {
|
|
82782
83188
|
const res = await applicationHandler(replaceRequest(c2.req.raw), ...getOptions(c2));
|
|
82783
83189
|
if (res) {
|
|
82784
83190
|
return res;
|
|
82785
83191
|
}
|
|
82786
83192
|
await next();
|
|
82787
83193
|
};
|
|
82788
|
-
this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"),
|
|
83194
|
+
this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"), handler14);
|
|
82789
83195
|
return this;
|
|
82790
83196
|
}
|
|
82791
|
-
#addRoute(method, path,
|
|
83197
|
+
#addRoute(method, path, handler14, baseRoutePath) {
|
|
82792
83198
|
method = method.toUpperCase();
|
|
82793
83199
|
path = mergePath(this._basePath, path);
|
|
82794
83200
|
const r = {
|
|
82795
83201
|
basePath: baseRoutePath !== undefined ? mergePath(this._basePath, baseRoutePath) : this._basePath,
|
|
82796
83202
|
path,
|
|
82797
83203
|
method,
|
|
82798
|
-
handler:
|
|
83204
|
+
handler: handler14
|
|
82799
83205
|
};
|
|
82800
|
-
this.router.add(method, path, [
|
|
83206
|
+
this.router.add(method, path, [handler14, r]);
|
|
82801
83207
|
this.routes.push(r);
|
|
82802
83208
|
}
|
|
82803
83209
|
#handleError(err, c2) {
|
|
@@ -83122,7 +83528,7 @@ var RegExpRouter = class {
|
|
|
83122
83528
|
this.#middleware = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
|
|
83123
83529
|
this.#routes = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
|
|
83124
83530
|
}
|
|
83125
|
-
add(method, path,
|
|
83531
|
+
add(method, path, handler14) {
|
|
83126
83532
|
const middleware = this.#middleware;
|
|
83127
83533
|
const routes = this.#routes;
|
|
83128
83534
|
if (!middleware || !routes) {
|
|
@@ -83152,13 +83558,13 @@ var RegExpRouter = class {
|
|
|
83152
83558
|
Object.keys(middleware).forEach((m) => {
|
|
83153
83559
|
if (method === METHOD_NAME_ALL || method === m) {
|
|
83154
83560
|
Object.keys(middleware[m]).forEach((p) => {
|
|
83155
|
-
re.test(p) && middleware[m][p].push([
|
|
83561
|
+
re.test(p) && middleware[m][p].push([handler14, paramCount]);
|
|
83156
83562
|
});
|
|
83157
83563
|
}
|
|
83158
83564
|
});
|
|
83159
83565
|
Object.keys(routes).forEach((m) => {
|
|
83160
83566
|
if (method === METHOD_NAME_ALL || method === m) {
|
|
83161
|
-
Object.keys(routes[m]).forEach((p) => re.test(p) && routes[m][p].push([
|
|
83567
|
+
Object.keys(routes[m]).forEach((p) => re.test(p) && routes[m][p].push([handler14, paramCount]));
|
|
83162
83568
|
}
|
|
83163
83569
|
});
|
|
83164
83570
|
return;
|
|
@@ -83171,7 +83577,7 @@ var RegExpRouter = class {
|
|
|
83171
83577
|
routes[m][path2] ||= [
|
|
83172
83578
|
...findMiddleware(middleware[m], path2) || findMiddleware(middleware[METHOD_NAME_ALL], path2) || []
|
|
83173
83579
|
];
|
|
83174
|
-
routes[m][path2].push([
|
|
83580
|
+
routes[m][path2].push([handler14, paramCount - len + i + 1]);
|
|
83175
83581
|
}
|
|
83176
83582
|
});
|
|
83177
83583
|
}
|
|
@@ -83220,21 +83626,21 @@ var PreparedRegExpRouter = class {
|
|
|
83220
83626
|
matcher[1].forEach((list) => list && list.push(handlerData));
|
|
83221
83627
|
Object.values(matcher[2]).forEach((list) => list[0].push(handlerData));
|
|
83222
83628
|
}
|
|
83223
|
-
#addPath(method, path,
|
|
83629
|
+
#addPath(method, path, handler14, indexes, map2) {
|
|
83224
83630
|
const matcher = this.#matchers[method];
|
|
83225
83631
|
if (!map2) {
|
|
83226
|
-
matcher[2][path][0].push([
|
|
83632
|
+
matcher[2][path][0].push([handler14, {}]);
|
|
83227
83633
|
} else {
|
|
83228
83634
|
indexes.forEach((index) => {
|
|
83229
83635
|
if (typeof index === "number") {
|
|
83230
|
-
matcher[1][index].push([
|
|
83636
|
+
matcher[1][index].push([handler14, map2]);
|
|
83231
83637
|
} else {
|
|
83232
|
-
matcher[2][index || path][0].push([
|
|
83638
|
+
matcher[2][index || path][0].push([handler14, map2]);
|
|
83233
83639
|
}
|
|
83234
83640
|
});
|
|
83235
83641
|
}
|
|
83236
83642
|
}
|
|
83237
|
-
add(method, path,
|
|
83643
|
+
add(method, path, handler14) {
|
|
83238
83644
|
if (!this.#matchers[method]) {
|
|
83239
83645
|
const all = this.#matchers[METHOD_NAME_ALL];
|
|
83240
83646
|
const staticMap = {};
|
|
@@ -83248,7 +83654,7 @@ var PreparedRegExpRouter = class {
|
|
|
83248
83654
|
];
|
|
83249
83655
|
}
|
|
83250
83656
|
if (path === "/*" || path === "*") {
|
|
83251
|
-
const handlerData = [
|
|
83657
|
+
const handlerData = [handler14, {}];
|
|
83252
83658
|
if (method === METHOD_NAME_ALL) {
|
|
83253
83659
|
for (const m in this.#matchers) {
|
|
83254
83660
|
this.#addWildcard(m, handlerData);
|
|
@@ -83265,10 +83671,10 @@ var PreparedRegExpRouter = class {
|
|
|
83265
83671
|
for (const [indexes, map2] of data) {
|
|
83266
83672
|
if (method === METHOD_NAME_ALL) {
|
|
83267
83673
|
for (const m in this.#matchers) {
|
|
83268
|
-
this.#addPath(m, path,
|
|
83674
|
+
this.#addPath(m, path, handler14, indexes, map2);
|
|
83269
83675
|
}
|
|
83270
83676
|
} else {
|
|
83271
|
-
this.#addPath(method, path,
|
|
83677
|
+
this.#addPath(method, path, handler14, indexes, map2);
|
|
83272
83678
|
}
|
|
83273
83679
|
}
|
|
83274
83680
|
}
|
|
@@ -83286,11 +83692,11 @@ var SmartRouter = class {
|
|
|
83286
83692
|
constructor(init) {
|
|
83287
83693
|
this.#routers = init.routers;
|
|
83288
83694
|
}
|
|
83289
|
-
add(method, path,
|
|
83695
|
+
add(method, path, handler14) {
|
|
83290
83696
|
if (!this.#routes) {
|
|
83291
83697
|
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
|
|
83292
83698
|
}
|
|
83293
|
-
this.#routes.push([method, path,
|
|
83699
|
+
this.#routes.push([method, path, handler14]);
|
|
83294
83700
|
}
|
|
83295
83701
|
match(method, path) {
|
|
83296
83702
|
if (!this.#routes) {
|
|
@@ -83347,17 +83753,17 @@ var Node2 = class _Node2 {
|
|
|
83347
83753
|
#patterns;
|
|
83348
83754
|
#order = 0;
|
|
83349
83755
|
#params = emptyParams;
|
|
83350
|
-
constructor(method,
|
|
83756
|
+
constructor(method, handler14, children) {
|
|
83351
83757
|
this.#children = children || /* @__PURE__ */ Object.create(null);
|
|
83352
83758
|
this.#methods = [];
|
|
83353
|
-
if (method &&
|
|
83759
|
+
if (method && handler14) {
|
|
83354
83760
|
const m = /* @__PURE__ */ Object.create(null);
|
|
83355
|
-
m[method] = { handler:
|
|
83761
|
+
m[method] = { handler: handler14, possibleKeys: [], score: 0 };
|
|
83356
83762
|
this.#methods = [m];
|
|
83357
83763
|
}
|
|
83358
83764
|
this.#patterns = [];
|
|
83359
83765
|
}
|
|
83360
|
-
insert(method, path,
|
|
83766
|
+
insert(method, path, handler14) {
|
|
83361
83767
|
this.#order = ++this.#order;
|
|
83362
83768
|
let curNode = this;
|
|
83363
83769
|
const parts = splitRoutingPath(path);
|
|
@@ -83383,7 +83789,7 @@ var Node2 = class _Node2 {
|
|
|
83383
83789
|
}
|
|
83384
83790
|
curNode.#methods.push({
|
|
83385
83791
|
[method]: {
|
|
83386
|
-
handler:
|
|
83792
|
+
handler: handler14,
|
|
83387
83793
|
possibleKeys: possibleKeys.filter((v, i, a) => a.indexOf(v) === i),
|
|
83388
83794
|
score: this.#order
|
|
83389
83795
|
}
|
|
@@ -83501,7 +83907,7 @@ var Node2 = class _Node2 {
|
|
|
83501
83907
|
return a.score - b2.score;
|
|
83502
83908
|
});
|
|
83503
83909
|
}
|
|
83504
|
-
return [handlerSets.map(({ handler:
|
|
83910
|
+
return [handlerSets.map(({ handler: handler14, params }) => [handler14, params])];
|
|
83505
83911
|
}
|
|
83506
83912
|
};
|
|
83507
83913
|
|
|
@@ -83512,15 +83918,15 @@ var TrieRouter = class {
|
|
|
83512
83918
|
constructor() {
|
|
83513
83919
|
this.#node = new Node2;
|
|
83514
83920
|
}
|
|
83515
|
-
add(method, path,
|
|
83921
|
+
add(method, path, handler14) {
|
|
83516
83922
|
const results = checkOptionalParameter(path);
|
|
83517
83923
|
if (results) {
|
|
83518
83924
|
for (let i = 0, len = results.length;i < len; i++) {
|
|
83519
|
-
this.#node.insert(method, results[i],
|
|
83925
|
+
this.#node.insert(method, results[i], handler14);
|
|
83520
83926
|
}
|
|
83521
83927
|
return;
|
|
83522
83928
|
}
|
|
83523
|
-
this.#node.insert(method, path,
|
|
83929
|
+
this.#node.insert(method, path, handler14);
|
|
83524
83930
|
}
|
|
83525
83931
|
match(method, path) {
|
|
83526
83932
|
return this.#node.search(method, path);
|
|
@@ -84353,6 +84759,33 @@ function registerDiscoveryRoutes(app, ctx) {
|
|
|
84353
84759
|
content: row.content ?? null
|
|
84354
84760
|
})));
|
|
84355
84761
|
});
|
|
84762
|
+
app.get("/api/v1/dashboard/recent-docs", async (c2) => {
|
|
84763
|
+
const projectId = c2.req.query("project_id") || null;
|
|
84764
|
+
if (projectId && !UUID_RE4.test(projectId)) {
|
|
84765
|
+
return c2.json({ detail: "project_id must be a UUID" }, 400);
|
|
84766
|
+
}
|
|
84767
|
+
const [recentDocs, projects] = await Promise.all([
|
|
84768
|
+
listDocuments(ctx, { projectId, limit: 10 }),
|
|
84769
|
+
listAllProjects(ctx)
|
|
84770
|
+
]);
|
|
84771
|
+
const docIds = recentDocs.map((d) => String(d.id));
|
|
84772
|
+
const [docProjectsMap, authors] = await Promise.all([
|
|
84773
|
+
getProjectsForDocuments(ctx, docIds, projects),
|
|
84774
|
+
getRecentDocAuthors(ctx, docIds)
|
|
84775
|
+
]);
|
|
84776
|
+
return c2.json({
|
|
84777
|
+
recent_docs: recentDocs.map((d) => {
|
|
84778
|
+
const id = String(d.id);
|
|
84779
|
+
const pids = (docProjectsMap[id] ?? []).map((p) => String(p.id));
|
|
84780
|
+
const a = authors[id];
|
|
84781
|
+
return {
|
|
84782
|
+
...dashboardDocFromRow(d, pids),
|
|
84783
|
+
author: a?.author ?? null,
|
|
84784
|
+
author_type: a?.author_type ?? null
|
|
84785
|
+
};
|
|
84786
|
+
})
|
|
84787
|
+
});
|
|
84788
|
+
});
|
|
84356
84789
|
app.get("/api/v1/dashboard", async (c2) => {
|
|
84357
84790
|
const [recentDocs, projects, docCount, totals] = await Promise.all([
|
|
84358
84791
|
listDocuments(ctx, { limit: 10 }),
|
|
@@ -84739,11 +85172,21 @@ function registerDocumentWriteRoutes(app, ctx) {
|
|
|
84739
85172
|
const title = String(body.title ?? "").trim();
|
|
84740
85173
|
const content = String(body.content ?? "");
|
|
84741
85174
|
const projectIds = Array.isArray(body.project_ids) ? body.project_ids : [];
|
|
85175
|
+
if (body.metadata !== undefined && body.metadata !== null && (typeof body.metadata !== "object" || Array.isArray(body.metadata))) {
|
|
85176
|
+
return c2.json({ success: false, error: "metadata must be a JSON object of key/value pairs" }, 400);
|
|
85177
|
+
}
|
|
84742
85178
|
const metadata = body.metadata ?? {};
|
|
84743
85179
|
const doc2 = await getCurrentDoc(ctx, documentId);
|
|
84744
85180
|
if (!doc2) {
|
|
84745
85181
|
return c2.json({ success: false, error: "Document not found" }, 404);
|
|
84746
85182
|
}
|
|
85183
|
+
if (doc2.deleted_at) {
|
|
85184
|
+
return c2.json({
|
|
85185
|
+
success: false,
|
|
85186
|
+
error: "document is in the trash",
|
|
85187
|
+
message: "This document was moved to the trash while you were editing. " + "Restore it from the Trash page first, then save again."
|
|
85188
|
+
}, 409);
|
|
85189
|
+
}
|
|
84747
85190
|
const currentHash = doc2.content_hash;
|
|
84748
85191
|
const proposedHash = content.trim() ? contentHash(content) : null;
|
|
84749
85192
|
const contentChanged = proposedHash !== null && currentHash !== null && proposedHash !== currentHash;
|
|
@@ -84787,10 +85230,23 @@ function registerDocumentWriteRoutes(app, ctx) {
|
|
|
84787
85230
|
message: err.message
|
|
84788
85231
|
}, 400);
|
|
84789
85232
|
}
|
|
84790
|
-
|
|
84791
|
-
|
|
84792
|
-
|
|
84793
|
-
|
|
85233
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
85234
|
+
if (msg.includes("CEREFOX_UNRESOLVED_LINKS")) {
|
|
85235
|
+
const ids = msg.match(/do not exist: ([^.]+)\./)?.[1] ?? "";
|
|
85236
|
+
return c2.json({
|
|
85237
|
+
success: false,
|
|
85238
|
+
error: "unresolved document links",
|
|
85239
|
+
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.`
|
|
85240
|
+
}, 422);
|
|
85241
|
+
}
|
|
85242
|
+
if (msg.includes("soft-deleted")) {
|
|
85243
|
+
return c2.json({
|
|
85244
|
+
success: false,
|
|
85245
|
+
error: "document is in the trash",
|
|
85246
|
+
message: "This document was moved to the trash while you were editing. " + "Restore it from the Trash page first, then save again."
|
|
85247
|
+
}, 409);
|
|
85248
|
+
}
|
|
85249
|
+
return c2.json({ success: false, error: msg }, 500);
|
|
84794
85250
|
}
|
|
84795
85251
|
}
|
|
84796
85252
|
const updates = {};
|
|
@@ -84823,25 +85279,35 @@ function registerDocumentWriteRoutes(app, ctx) {
|
|
|
84823
85279
|
});
|
|
84824
85280
|
app.delete("/api/v1/documents/:document_id", async (c2) => {
|
|
84825
85281
|
const documentId = c2.req.param("document_id");
|
|
84826
|
-
const { error: error3 } = await ctx.supabase.rpc("cerefox_delete_document", {
|
|
85282
|
+
const { data, error: error3 } = await ctx.supabase.rpc("cerefox_delete_document", {
|
|
84827
85283
|
p_document_id: documentId,
|
|
84828
85284
|
p_author: "web-ui",
|
|
84829
85285
|
p_author_type: "user"
|
|
84830
85286
|
});
|
|
84831
|
-
if (error3)
|
|
85287
|
+
if (error3) {
|
|
85288
|
+
if (isDocumentNotFoundError(error3)) {
|
|
85289
|
+
return c2.json({ detail: `Document ${documentId} not found` }, 404);
|
|
85290
|
+
}
|
|
84832
85291
|
return c2.json({ detail: error3.message }, 500);
|
|
84833
|
-
|
|
85292
|
+
}
|
|
85293
|
+
const row = data;
|
|
85294
|
+
return c2.json({ success: true, ...row ? { already_deleted: row.already_deleted ?? false } : {} });
|
|
84834
85295
|
});
|
|
84835
85296
|
app.post("/api/v1/documents/:document_id/restore", async (c2) => {
|
|
84836
85297
|
const documentId = c2.req.param("document_id");
|
|
84837
|
-
const { error: error3 } = await ctx.supabase.rpc("cerefox_restore_document", {
|
|
85298
|
+
const { data, error: error3 } = await ctx.supabase.rpc("cerefox_restore_document", {
|
|
84838
85299
|
p_document_id: documentId,
|
|
84839
85300
|
p_author: "web-ui",
|
|
84840
85301
|
p_author_type: "user"
|
|
84841
85302
|
});
|
|
84842
|
-
if (error3)
|
|
85303
|
+
if (error3) {
|
|
85304
|
+
if (isDocumentNotFoundError(error3)) {
|
|
85305
|
+
return c2.json({ detail: `Document ${documentId} not found` }, 404);
|
|
85306
|
+
}
|
|
84843
85307
|
return c2.json({ detail: error3.message }, 500);
|
|
84844
|
-
|
|
85308
|
+
}
|
|
85309
|
+
const row = data;
|
|
85310
|
+
return c2.json({ success: true, ...row ? { restored: row.restored ?? false } : {} });
|
|
84845
85311
|
});
|
|
84846
85312
|
app.delete("/api/v1/documents/:document_id/purge", async (c2) => {
|
|
84847
85313
|
const documentId = c2.req.param("document_id");
|
|
@@ -84867,6 +85333,9 @@ function registerDocumentWriteRoutes(app, ctx) {
|
|
|
84867
85333
|
return c2.json({ detail: `Invalid status: ${JSON.stringify(status)}` }, 400);
|
|
84868
85334
|
}
|
|
84869
85335
|
const old = await getCurrentDoc(ctx, documentId);
|
|
85336
|
+
if (old?.deleted_at) {
|
|
85337
|
+
return c2.json({ detail: "This document is in the trash — restore it before changing its review status." }, 409);
|
|
85338
|
+
}
|
|
84870
85339
|
const oldStatus = old?.review_status ?? "unknown";
|
|
84871
85340
|
const { error: error3 } = await ctx.supabase.from("cerefox_documents").update({ review_status: status, updated_at: new Date().toISOString() }).eq("id", documentId);
|
|
84872
85341
|
if (error3)
|
|
@@ -85013,7 +85482,8 @@ function registerIngestRoutes(app, ctx) {
|
|
|
85013
85482
|
document_id: result.documentId,
|
|
85014
85483
|
title: result.title,
|
|
85015
85484
|
skipped,
|
|
85016
|
-
updated: result.reindexed
|
|
85485
|
+
updated: result.reindexed,
|
|
85486
|
+
...result.note ? { note: result.note } : {}
|
|
85017
85487
|
}, 200);
|
|
85018
85488
|
} catch (err) {
|
|
85019
85489
|
return c2.json(notReady(err instanceof Error ? err.message : String(err)), 200);
|
|
@@ -85970,6 +86440,7 @@ Learn more:
|
|
|
85970
86440
|
moveInto(document2, registerGetDoc, "get");
|
|
85971
86441
|
moveInto(document2, registerListDocs, "list");
|
|
85972
86442
|
moveInto(document2, registerDeleteDoc, "delete");
|
|
86443
|
+
moveInto(document2, registerDocumentDeadLinks, "dead-links");
|
|
85973
86444
|
registerDocumentRestore(document2);
|
|
85974
86445
|
registerDocumentEdit(document2);
|
|
85975
86446
|
registerDocumentSetProjects(document2);
|