@cerefox/memory 1.7.0 → 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 +4 -4
- package/dist/bin/cerefox.js +202 -95
- package/dist/server-assets/_shared/ef-meta/index.ts +2 -2
- 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 +166 -18
- package/dist/server-assets/db/schema.sql +10 -2
- package/docs/guides/cli.md +21 -0
- package/package.json +1 -1
package/AGENT_GUIDE.md
CHANGED
|
@@ -478,7 +478,7 @@ Call `cerefox_list_metadata_keys` for the current list -- conventions evolve.
|
|
|
478
478
|
|
|
479
479
|
## Writing linkable content
|
|
480
480
|
|
|
481
|
-
Documents you ingest may contain markdown links to other Cerefox documents. The Cerefox web UI intercepts these links at click time and resolves them to the target document. The resolution happens entirely in the browser; the stored markdown is untouched.
|
|
481
|
+
Documents you ingest may contain markdown links to other Cerefox documents. The Cerefox web UI intercepts these links at click time and resolves them to the target document. The resolution happens entirely in the browser; the stored markdown is untouched. (User-facing overview of the whole linking system, including *why* long ids corrupt during regeneration: [`docs/guides/linking.md`](docs/guides/linking.md).)
|
|
482
482
|
|
|
483
483
|
### The rule for agents: use document UUIDs
|
|
484
484
|
|
|
@@ -502,9 +502,9 @@ same-turn-fixable error. Three things to know:
|
|
|
502
502
|
mechanism, and it is just correct markdown authoring.
|
|
503
503
|
- **Only links your write introduces are validated on updates.** A dead
|
|
504
504
|
link the document already carried (its target purged after linking) does
|
|
505
|
-
not block your unrelated edit — legacy dead links are found by
|
|
506
|
-
|
|
507
|
-
documents validate every link.
|
|
505
|
+
not block your unrelated edit — legacy dead links are found on demand by
|
|
506
|
+
the sweep (`cerefox document dead-links`, CLI), not by holding your edit
|
|
507
|
+
hostage. New documents validate every link.
|
|
508
508
|
|
|
509
509
|
`[[Wikilinks]]` are NOT validated — they remain the sanctioned form for
|
|
510
510
|
"flag a document to create later."
|
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.7.
|
|
7441
|
+
var PKG_VERSION = "1.7.1";
|
|
7442
7442
|
var init_meta = () => {};
|
|
7443
7443
|
|
|
7444
7444
|
// ../../_shared/config/paths.ts
|
|
@@ -25743,7 +25743,7 @@ var init_bundled_docs = __esm(() => {
|
|
|
25743
25743
|
});
|
|
25744
25744
|
|
|
25745
25745
|
// ../../_shared/ef-meta/index.ts
|
|
25746
|
-
var EF_VERSION = "1.7.
|
|
25746
|
+
var EF_VERSION = "1.7.1", CEREFOX_VERSION = "1.7.1", EF_LAST_CHANGED = "1.7.0";
|
|
25747
25747
|
var init_ef_meta = () => {};
|
|
25748
25748
|
|
|
25749
25749
|
// ../../_shared/compatibility/index.ts
|
|
@@ -41592,10 +41592,10 @@ var require_flate = __commonJS((exports) => {
|
|
|
41592
41592
|
var GenericWorker = require_GenericWorker();
|
|
41593
41593
|
var ARRAY_TYPE = USE_TYPEDARRAY ? "uint8array" : "array";
|
|
41594
41594
|
exports.magic = "\b\x00";
|
|
41595
|
-
function FlateWorker(
|
|
41596
|
-
GenericWorker.call(this, "FlateWorker/" +
|
|
41595
|
+
function FlateWorker(action20, options) {
|
|
41596
|
+
GenericWorker.call(this, "FlateWorker/" + action20);
|
|
41597
41597
|
this._pako = null;
|
|
41598
|
-
this._pakoAction =
|
|
41598
|
+
this._pakoAction = action20;
|
|
41599
41599
|
this._pakoOptions = options;
|
|
41600
41600
|
this.meta = {};
|
|
41601
41601
|
}
|
|
@@ -57699,11 +57699,11 @@ async function runSyncSelfDocs(options = {}) {
|
|
|
57699
57699
|
printTable(outcomes.filter((o) => o.status === "error").map((o) => ({ topic: o.topic, error: o.detail.slice(0, 100) })));
|
|
57700
57700
|
}
|
|
57701
57701
|
}
|
|
57702
|
-
async function
|
|
57702
|
+
async function action22(options) {
|
|
57703
57703
|
await runSyncSelfDocs(options);
|
|
57704
57704
|
}
|
|
57705
57705
|
function registerSyncSelfDocs(program2) {
|
|
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(
|
|
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);
|
|
57707
57707
|
}
|
|
57708
57708
|
var init_sync_self_docs = __esm(() => {
|
|
57709
57709
|
init_cli_core();
|
|
@@ -72797,11 +72797,48 @@ function registerDeleteDoc(program2) {
|
|
|
72797
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);
|
|
72798
72798
|
}
|
|
72799
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
|
+
|
|
72800
72837
|
// src/cli/commands/delete-project.ts
|
|
72801
72838
|
init_cli_core();
|
|
72802
72839
|
init_client();
|
|
72803
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;
|
|
72804
|
-
async function
|
|
72841
|
+
async function action9(target, options) {
|
|
72805
72842
|
const client = getClient();
|
|
72806
72843
|
const isUuid = UUID_RE.test(target);
|
|
72807
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();
|
|
@@ -72837,7 +72874,7 @@ async function action8(target, options) {
|
|
|
72837
72874
|
println(c.green(`✓ Deleted project "${project.name}" (id: ${project.id}).`));
|
|
72838
72875
|
}
|
|
72839
72876
|
function registerDeleteProject(program2) {
|
|
72840
|
-
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);
|
|
72841
72878
|
}
|
|
72842
72879
|
|
|
72843
72880
|
// src/cli/commands/deploy-server.ts
|
|
@@ -73043,7 +73080,7 @@ function listEdgeFunctions(functionsDir) {
|
|
|
73043
73080
|
return [];
|
|
73044
73081
|
return readdirSync2(functionsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("cerefox-")).map((e) => e.name).sort();
|
|
73045
73082
|
}
|
|
73046
|
-
async function
|
|
73083
|
+
async function action10(options) {
|
|
73047
73084
|
const settings = loadSettings();
|
|
73048
73085
|
const assets = resolveServerAssets();
|
|
73049
73086
|
const doSchema = !options.functionsOnly;
|
|
@@ -73258,7 +73295,7 @@ Proceed with deployment to Supabase?`, true);
|
|
|
73258
73295
|
println(c.dim("Verify with: cerefox doctor"));
|
|
73259
73296
|
}
|
|
73260
73297
|
function registerDeployServer(program2) {
|
|
73261
|
-
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);
|
|
73262
73299
|
}
|
|
73263
73300
|
|
|
73264
73301
|
// src/cli/commands/document-edit.ts
|
|
@@ -73278,7 +73315,7 @@ function parseMetaPair(pair) {
|
|
|
73278
73315
|
}
|
|
73279
73316
|
return [key, value];
|
|
73280
73317
|
}
|
|
73281
|
-
async function
|
|
73318
|
+
async function action11(documentId, options) {
|
|
73282
73319
|
const hasTitle = options.title !== undefined;
|
|
73283
73320
|
const sets = options.setMeta ?? [];
|
|
73284
73321
|
const unsets = options.unsetMeta ?? [];
|
|
@@ -73296,18 +73333,41 @@ async function action10(documentId, options) {
|
|
|
73296
73333
|
if (doc.deleted_at) {
|
|
73297
73334
|
throw userError(`Document ${documentId} is soft-deleted — restore it first (cerefox document restore).`);
|
|
73298
73335
|
}
|
|
73299
|
-
const
|
|
73300
|
-
for (const pair of sets) {
|
|
73301
|
-
const [k, v] = parseMetaPair(pair);
|
|
73302
|
-
metadata[k] = v;
|
|
73303
|
-
}
|
|
73304
|
-
for (const k of unsets)
|
|
73305
|
-
delete metadata[k.trim()];
|
|
73336
|
+
const metaTouched = sets.length > 0 || unsets.length > 0;
|
|
73306
73337
|
const newTitle = hasTitle ? options.title.trim() : doc.title;
|
|
73307
73338
|
const titleChanged = newTitle !== doc.title;
|
|
73308
|
-
const
|
|
73309
|
-
|
|
73310
|
-
|
|
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
|
+
}
|
|
73311
73371
|
if (titleChanged) {
|
|
73312
73372
|
const { error: ftsErr } = await client.raw.rpc("cerefox_update_chunk_fts", {
|
|
73313
73373
|
p_document_id: documentId,
|
|
@@ -73316,18 +73376,15 @@ async function action10(documentId, options) {
|
|
|
73316
73376
|
if (ftsErr)
|
|
73317
73377
|
throw systemError(`Title updated but FTS refresh failed: ${ftsErr.message}`);
|
|
73318
73378
|
}
|
|
73319
|
-
|
|
73320
|
-
|
|
73321
|
-
|
|
73322
|
-
|
|
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
|
+
});
|
|
73323
73387
|
}
|
|
73324
|
-
await client.raw.rpc("cerefox_create_audit_entry", {
|
|
73325
|
-
p_document_id: documentId,
|
|
73326
|
-
p_operation: "update-metadata",
|
|
73327
|
-
p_author: author,
|
|
73328
|
-
p_author_type: authorType,
|
|
73329
|
-
p_description: `Edited${titleChanged ? " title" : ""}` + (sets.length ? ` · set ${sets.length} meta key(s)` : "") + (unsets.length ? ` · unset ${unsets.length} meta key(s)` : "")
|
|
73330
|
-
});
|
|
73331
73388
|
println(c.green(`✓ Edited "${newTitle}" (id: ${documentId}).`));
|
|
73332
73389
|
if (titleChanged) {
|
|
73333
73390
|
println(c.dim(" Title changed: FTS refreshed; semantic embeddings update on next `cerefox server reindex`."));
|
|
@@ -73337,13 +73394,13 @@ function collect(value, prev) {
|
|
|
73337
73394
|
return [...prev, value];
|
|
73338
73395
|
}
|
|
73339
73396
|
function registerDocumentEdit(parent) {
|
|
73340
|
-
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);
|
|
73341
73398
|
}
|
|
73342
73399
|
|
|
73343
73400
|
// src/cli/commands/document-restore.ts
|
|
73344
73401
|
init_cli_core();
|
|
73345
73402
|
init_client();
|
|
73346
|
-
async function
|
|
73403
|
+
async function action12(documentId, options) {
|
|
73347
73404
|
const client = getClient();
|
|
73348
73405
|
const { data: doc, error } = await client.raw.from("cerefox_documents").select("id, title, deleted_at").eq("id", documentId).maybeSingle();
|
|
73349
73406
|
if (error)
|
|
@@ -73397,13 +73454,13 @@ async function action11(documentId, options) {
|
|
|
73397
73454
|
}
|
|
73398
73455
|
}
|
|
73399
73456
|
function registerDocumentRestore(parent) {
|
|
73400
|
-
parent.command("restore").description("Restore a soft-deleted document from the trash (inverse of `document delete`).").argument("<document-id>", "UUID of the soft-deleted document.").option("--reason <text>", "Optional reason recorded in the audit log.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(
|
|
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);
|
|
73401
73458
|
}
|
|
73402
73459
|
|
|
73403
73460
|
// src/cli/commands/document-set-metadata.ts
|
|
73404
73461
|
init_cli_core();
|
|
73405
73462
|
init_client();
|
|
73406
|
-
async function
|
|
73463
|
+
async function action13(documentId, options) {
|
|
73407
73464
|
const patch = {};
|
|
73408
73465
|
if (options.json) {
|
|
73409
73466
|
let parsed;
|
|
@@ -73475,14 +73532,14 @@ async function action12(documentId, options) {
|
|
|
73475
73532
|
println(c.dim(" Content untouched — no new version, no re-embedding."));
|
|
73476
73533
|
}
|
|
73477
73534
|
function registerDocumentSetMetadata(parent) {
|
|
73478
|
-
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);
|
|
73479
73536
|
}
|
|
73480
73537
|
|
|
73481
73538
|
// src/cli/commands/document-set-projects.ts
|
|
73482
73539
|
init_cli_core();
|
|
73483
73540
|
init__projects();
|
|
73484
73541
|
init_client();
|
|
73485
|
-
async function
|
|
73542
|
+
async function action14(documentId, projectNames, options) {
|
|
73486
73543
|
const names = projectNames ?? [];
|
|
73487
73544
|
if (options.clear && names.length > 0) {
|
|
73488
73545
|
throw userError("Pass either project names or --clear, not both.", "Use --clear on its own to remove the document from all projects.");
|
|
@@ -73512,7 +73569,7 @@ async function action13(documentId, projectNames, options) {
|
|
|
73512
73569
|
println(c.dim(" This REPLACED the previous set — any project not listed is no longer associated."));
|
|
73513
73570
|
}
|
|
73514
73571
|
function registerDocumentSetProjects(parent) {
|
|
73515
|
-
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);
|
|
73516
73573
|
}
|
|
73517
73574
|
|
|
73518
73575
|
// src/cli/commands/guides.ts
|
|
@@ -73562,7 +73619,7 @@ function registerGuides(parent) {
|
|
|
73562
73619
|
// src/cli/commands/project-create.ts
|
|
73563
73620
|
init_cli_core();
|
|
73564
73621
|
init_client();
|
|
73565
|
-
async function
|
|
73622
|
+
async function action15(name, options) {
|
|
73566
73623
|
const trimmed = name.trim();
|
|
73567
73624
|
if (!trimmed)
|
|
73568
73625
|
throw userError("Project name is required.");
|
|
@@ -73574,14 +73631,14 @@ async function action14(name, options) {
|
|
|
73574
73631
|
println(c.green(`✓ Created project "${data.name}" (id: ${data.id}).`));
|
|
73575
73632
|
}
|
|
73576
73633
|
function registerProjectCreate(parent) {
|
|
73577
|
-
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);
|
|
73578
73635
|
}
|
|
73579
73636
|
|
|
73580
73637
|
// src/cli/commands/project-edit.ts
|
|
73581
73638
|
init_cli_core();
|
|
73582
73639
|
init_client();
|
|
73583
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;
|
|
73584
|
-
async function
|
|
73641
|
+
async function action16(target, options) {
|
|
73585
73642
|
const update = {};
|
|
73586
73643
|
if (options.name !== undefined) {
|
|
73587
73644
|
const n = options.name.trim();
|
|
@@ -73608,7 +73665,7 @@ async function action15(target, options) {
|
|
|
73608
73665
|
println(c.green(`✓ Updated project "${data.name}" (id: ${data.id}).`));
|
|
73609
73666
|
}
|
|
73610
73667
|
function registerProjectEdit(parent) {
|
|
73611
|
-
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);
|
|
73612
73669
|
}
|
|
73613
73670
|
|
|
73614
73671
|
// src/cli/commands/version-archive.ts
|
|
@@ -77430,14 +77487,9 @@ async function checkEmbedderMismatch() {
|
|
|
77430
77487
|
};
|
|
77431
77488
|
}
|
|
77432
77489
|
}
|
|
77433
|
-
|
|
77434
|
-
async function checkContentFormat() {
|
|
77435
|
-
const settings = loadSettings();
|
|
77436
|
-
if (!settings.supabaseUrl || !settings.supabaseKey) {
|
|
77437
|
-
return { name: CONTENT_FORMAT_CHECK_NAME, status: "skipped", detail: "Supabase config missing; skipped." };
|
|
77438
|
-
}
|
|
77490
|
+
async function probeRpcJson(settings, fn) {
|
|
77439
77491
|
try {
|
|
77440
|
-
const url = `${settings.supabaseUrl.replace(/\/$/, "")}/rest/v1/rpc
|
|
77492
|
+
const url = `${settings.supabaseUrl.replace(/\/$/, "")}/rest/v1/rpc/${fn}`;
|
|
77441
77493
|
const resp = await fetch(url, {
|
|
77442
77494
|
method: "POST",
|
|
77443
77495
|
headers: {
|
|
@@ -77447,14 +77499,38 @@ async function checkContentFormat() {
|
|
|
77447
77499
|
},
|
|
77448
77500
|
body: "{}"
|
|
77449
77501
|
});
|
|
77450
|
-
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") {
|
|
77451
77520
|
return {
|
|
77452
77521
|
name: CONTENT_FORMAT_CHECK_NAME,
|
|
77453
77522
|
status: "skipped",
|
|
77454
|
-
detail:
|
|
77523
|
+
detail: "format stats unavailable; deploy schema 0.8.0 to enable."
|
|
77455
77524
|
};
|
|
77456
77525
|
}
|
|
77457
|
-
|
|
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;
|
|
77458
77534
|
const legacy = rows[0]?.legacy_docs ?? 0;
|
|
77459
77535
|
const total = rows[0]?.total_docs ?? 0;
|
|
77460
77536
|
if (legacy === 0) {
|
|
@@ -77470,13 +77546,39 @@ async function checkContentFormat() {
|
|
|
77470
77546
|
detail: `${legacy} of ${total} document(s) use the legacy reconstruction format (format 1).`,
|
|
77471
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`."
|
|
77472
77548
|
};
|
|
77473
|
-
}
|
|
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") {
|
|
77474
77559
|
return {
|
|
77475
|
-
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,
|
|
77476
77568
|
status: "skipped",
|
|
77477
|
-
detail: `
|
|
77569
|
+
detail: `metadata-health check skipped (${probe.detail}).`
|
|
77478
77570
|
};
|
|
77479
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
|
+
};
|
|
77480
77582
|
}
|
|
77481
77583
|
function hasCerefoxInJsonFile(path) {
|
|
77482
77584
|
if (!existsSync10(path))
|
|
@@ -77687,6 +77789,7 @@ async function runAllChecks(opts = {}) {
|
|
|
77687
77789
|
{ name: "schema + RPCs", phase: "Reading schema + RPC version", run: () => checkSchemaVersion() },
|
|
77688
77790
|
{ name: "embedder", phase: "Checking embedder consistency", run: () => checkEmbedderMismatch() },
|
|
77689
77791
|
{ name: "content format", phase: "Checking chunk reconstruction format", run: () => checkContentFormat() },
|
|
77792
|
+
{ name: "metadata health", phase: "Checking metadata well-formedness", run: () => checkMetadataHealth() },
|
|
77690
77793
|
{ name: "edge functions", phase: "Probing Edge Function versions", run: () => checkEdgeFunctionsCompat() },
|
|
77691
77794
|
{ name: "postgres", phase: "Probing Postgres DDL endpoint", run: () => checkPostgres() },
|
|
77692
77795
|
{ name: "mcp clients", phase: "Scanning MCP client configs", run: () => checkMcpConfigs() }
|
|
@@ -77715,7 +77818,7 @@ function symbol(status) {
|
|
|
77715
77818
|
return cErr.dim("ℹ");
|
|
77716
77819
|
}
|
|
77717
77820
|
}
|
|
77718
|
-
async function
|
|
77821
|
+
async function action17(options) {
|
|
77719
77822
|
const useSpinner = !options.json && process.stderr.isTTY;
|
|
77720
77823
|
const spinner = useSpinner ? ora({ text: "Starting checks…", spinner: "dots", stream: process.stderr }).start() : null;
|
|
77721
77824
|
const results = await runAllChecks({
|
|
@@ -77779,13 +77882,13 @@ async function action16(options) {
|
|
|
77779
77882
|
process.exit(1);
|
|
77780
77883
|
}
|
|
77781
77884
|
function registerDoctor(program2) {
|
|
77782
|
-
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);
|
|
77783
77886
|
}
|
|
77784
77887
|
|
|
77785
77888
|
// src/cli/commands/get-audit-log.ts
|
|
77786
77889
|
init_cli_core();
|
|
77787
77890
|
init_client();
|
|
77788
|
-
async function
|
|
77891
|
+
async function action18(options) {
|
|
77789
77892
|
const limit = parsePositiveInt(options.limit, "--limit", 50);
|
|
77790
77893
|
const client = getClient();
|
|
77791
77894
|
const data = await client.rpc("cerefox_list_audit_entries", {
|
|
@@ -77823,7 +77926,7 @@ async function action17(options) {
|
|
|
77823
77926
|
})));
|
|
77824
77927
|
}
|
|
77825
77928
|
function registerGetAuditLog(program2) {
|
|
77826
|
-
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);
|
|
77827
77930
|
}
|
|
77828
77931
|
|
|
77829
77932
|
// src/cli/commands/relation.ts
|
|
@@ -77959,7 +78062,7 @@ init_cli_core();
|
|
|
77959
78062
|
init_cli_core();
|
|
77960
78063
|
init_partial_edits();
|
|
77961
78064
|
init_client();
|
|
77962
|
-
async function
|
|
78065
|
+
async function action19(documentId, options) {
|
|
77963
78066
|
const section = (options.section ?? "").trim() || null;
|
|
77964
78067
|
if (section && options.outline) {
|
|
77965
78068
|
throw userError("Pass either --outline (the whole structure) or --section (one section's text), not both.");
|
|
@@ -78056,7 +78159,7 @@ async function action18(documentId, options) {
|
|
|
78056
78159
|
println(doc.full_content);
|
|
78057
78160
|
}
|
|
78058
78161
|
function registerGetDoc(program2) {
|
|
78059
|
-
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);
|
|
78060
78163
|
}
|
|
78061
78164
|
|
|
78062
78165
|
// src/cli/commands/ingest.ts
|
|
@@ -78720,7 +78823,7 @@ async function readContent(path, paste) {
|
|
|
78720
78823
|
const titleFromPath = basename2(path, extname3(path));
|
|
78721
78824
|
return { content, titleFromPath };
|
|
78722
78825
|
}
|
|
78723
|
-
async function
|
|
78826
|
+
async function action20(path, options) {
|
|
78724
78827
|
const { content, titleFromPath } = await readContent(path, Boolean(options.paste));
|
|
78725
78828
|
const updatingById = Boolean(options.documentId);
|
|
78726
78829
|
let title = options.title ?? (updatingById ? null : titleFromPath);
|
|
@@ -78811,7 +78914,7 @@ async function action19(path, options) {
|
|
|
78811
78914
|
}
|
|
78812
78915
|
}
|
|
78813
78916
|
function registerIngest(program2) {
|
|
78814
|
-
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);
|
|
78815
78918
|
}
|
|
78816
78919
|
|
|
78817
78920
|
// src/cli/commands/document-partial-edit.ts
|
|
@@ -78914,7 +79017,7 @@ function walk(dir, extensions) {
|
|
|
78914
79017
|
}
|
|
78915
79018
|
return files;
|
|
78916
79019
|
}
|
|
78917
|
-
async function
|
|
79020
|
+
async function action21(dir, options) {
|
|
78918
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));
|
|
78919
79022
|
const files = walk(dir, extensions);
|
|
78920
79023
|
if (files.length === 0) {
|
|
@@ -78989,7 +79092,7 @@ async function action20(dir, options) {
|
|
|
78989
79092
|
}
|
|
78990
79093
|
}
|
|
78991
79094
|
function registerIngestDir(program2) {
|
|
78992
|
-
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);
|
|
78993
79096
|
}
|
|
78994
79097
|
|
|
78995
79098
|
// src/cli/commands/init.ts
|
|
@@ -79320,7 +79423,7 @@ function writeAnswersTo(target, answers) {
|
|
|
79320
79423
|
}
|
|
79321
79424
|
}
|
|
79322
79425
|
}
|
|
79323
|
-
async function
|
|
79426
|
+
async function action23(options) {
|
|
79324
79427
|
const homeEnv = join11(homedir7(), USER_STATE_DIR_NAME, ".env");
|
|
79325
79428
|
const cwdEnv = join11(process.cwd(), ".env");
|
|
79326
79429
|
const explicitDir = (process.env.CEREFOX_CONFIG_DIR ?? "").trim();
|
|
@@ -79429,13 +79532,13 @@ async function action22(options) {
|
|
|
79429
79532
|
await postWriteLifecycle(target, options);
|
|
79430
79533
|
}
|
|
79431
79534
|
function registerInit(program2) {
|
|
79432
|
-
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);
|
|
79433
79536
|
}
|
|
79434
79537
|
|
|
79435
79538
|
// src/cli/commands/list-docs.ts
|
|
79436
79539
|
init_cli_core();
|
|
79437
79540
|
init_client();
|
|
79438
|
-
async function
|
|
79541
|
+
async function action24(options) {
|
|
79439
79542
|
const deleted = !!options.deleted;
|
|
79440
79543
|
const limit = parsePositiveInt(options.limit, "--limit", 100);
|
|
79441
79544
|
const client = getClient();
|
|
@@ -79491,13 +79594,13 @@ async function action23(options) {
|
|
|
79491
79594
|
}));
|
|
79492
79595
|
}
|
|
79493
79596
|
function registerListDocs(program2) {
|
|
79494
|
-
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);
|
|
79495
79598
|
}
|
|
79496
79599
|
|
|
79497
79600
|
// src/cli/commands/list-metadata-keys.ts
|
|
79498
79601
|
init_cli_core();
|
|
79499
79602
|
init_client();
|
|
79500
|
-
async function
|
|
79603
|
+
async function action25(options) {
|
|
79501
79604
|
const client = getClient();
|
|
79502
79605
|
const data = await client.rpc("cerefox_list_metadata_keys");
|
|
79503
79606
|
if (data === null) {
|
|
@@ -79525,13 +79628,13 @@ async function action24(options) {
|
|
|
79525
79628
|
})));
|
|
79526
79629
|
}
|
|
79527
79630
|
function registerListMetadataKeys(program2) {
|
|
79528
|
-
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);
|
|
79529
79632
|
}
|
|
79530
79633
|
|
|
79531
79634
|
// src/cli/commands/list-projects.ts
|
|
79532
79635
|
init_cli_core();
|
|
79533
79636
|
init_client();
|
|
79534
|
-
async function
|
|
79637
|
+
async function action26(options) {
|
|
79535
79638
|
const client = getClient();
|
|
79536
79639
|
const { data, error: error2 } = await client.raw.from("cerefox_projects").select("id, name, description, created_at").order("name", { ascending: true });
|
|
79537
79640
|
if (error2) {
|
|
@@ -79560,13 +79663,13 @@ async function action25(options) {
|
|
|
79560
79663
|
})), "(no projects)");
|
|
79561
79664
|
}
|
|
79562
79665
|
function registerListProjects(program2) {
|
|
79563
|
-
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);
|
|
79564
79667
|
}
|
|
79565
79668
|
|
|
79566
79669
|
// src/cli/commands/list-versions.ts
|
|
79567
79670
|
init_cli_core();
|
|
79568
79671
|
init_client();
|
|
79569
|
-
async function
|
|
79672
|
+
async function action27(documentId, options) {
|
|
79570
79673
|
const client = getClient();
|
|
79571
79674
|
const data = await client.rpc("cerefox_list_document_versions", {
|
|
79572
79675
|
p_document_id: documentId
|
|
@@ -79607,7 +79710,7 @@ async function action26(documentId, options) {
|
|
|
79607
79710
|
})));
|
|
79608
79711
|
}
|
|
79609
79712
|
function registerListVersions(program2) {
|
|
79610
|
-
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);
|
|
79611
79714
|
}
|
|
79612
79715
|
|
|
79613
79716
|
// src/cli/commands/mcp.ts
|
|
@@ -79639,7 +79742,7 @@ function registerEmbedderWarmup(program2) {
|
|
|
79639
79742
|
// src/cli/commands/metadata-search.ts
|
|
79640
79743
|
init_cli_core();
|
|
79641
79744
|
init_client();
|
|
79642
|
-
async function
|
|
79745
|
+
async function action28(options) {
|
|
79643
79746
|
const metadataFilter = parseJsonObjectArg(options.metadataFilter, "--metadata-filter") ?? {};
|
|
79644
79747
|
if (Object.keys(metadataFilter).length === 0 && !options.projectName && !options.updatedSince && !options.createdSince) {
|
|
79645
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).`);
|
|
@@ -79702,7 +79805,7 @@ async function action27(options) {
|
|
|
79702
79805
|
}
|
|
79703
79806
|
}
|
|
79704
79807
|
function registerMetadataSearch(program2) {
|
|
79705
|
-
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);
|
|
79706
79809
|
}
|
|
79707
79810
|
|
|
79708
79811
|
// src/cli/commands/reindex.ts
|
|
@@ -79728,7 +79831,7 @@ function warnLargeBulkWrite(opts) {
|
|
|
79728
79831
|
}
|
|
79729
79832
|
|
|
79730
79833
|
// src/cli/commands/reindex.ts
|
|
79731
|
-
async function
|
|
79834
|
+
async function action29(options) {
|
|
79732
79835
|
const settings = loadSettings();
|
|
79733
79836
|
if (!settings.supabaseUrl || !settings.supabaseKey) {
|
|
79734
79837
|
throw userError("Supabase credentials not configured — run `cerefox init` first.");
|
|
@@ -79818,7 +79921,7 @@ ${c2.content}`;
|
|
|
79818
79921
|
}
|
|
79819
79922
|
}
|
|
79820
79923
|
function registerReindex(program2) {
|
|
79821
|
-
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);
|
|
79822
79925
|
}
|
|
79823
79926
|
|
|
79824
79927
|
// src/cli/commands/migrate-format.ts
|
|
@@ -79826,7 +79929,7 @@ init_cli_core();
|
|
|
79826
79929
|
init_config();
|
|
79827
79930
|
init_client();
|
|
79828
79931
|
var CURRENT_FORMAT = 2;
|
|
79829
|
-
async function
|
|
79932
|
+
async function action30(options) {
|
|
79830
79933
|
const settings = loadSettings();
|
|
79831
79934
|
const client = getClient();
|
|
79832
79935
|
const supabase = client.raw;
|
|
@@ -79953,7 +80056,7 @@ async function action29(options) {
|
|
|
79953
80056
|
}
|
|
79954
80057
|
}
|
|
79955
80058
|
function registerMigrateFormat(program2) {
|
|
79956
|
-
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);
|
|
79957
80060
|
}
|
|
79958
80061
|
|
|
79959
80062
|
// src/cli/commands/restore.ts
|
|
@@ -79983,7 +80086,7 @@ function resolveBackupFile(target) {
|
|
|
79983
80086
|
}
|
|
79984
80087
|
return join12(path, candidates[0].name);
|
|
79985
80088
|
}
|
|
79986
|
-
async function
|
|
80089
|
+
async function action31(target, options) {
|
|
79987
80090
|
const file = resolveBackupFile(target);
|
|
79988
80091
|
let payload;
|
|
79989
80092
|
try {
|
|
@@ -80118,7 +80221,7 @@ async function action30(target, options) {
|
|
|
80118
80221
|
}
|
|
80119
80222
|
}
|
|
80120
80223
|
function registerRestore(program2) {
|
|
80121
|
-
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);
|
|
80122
80225
|
}
|
|
80123
80226
|
|
|
80124
80227
|
// src/cli/commands/search.ts
|
|
@@ -80143,7 +80246,7 @@ async function embedQuery(query) {
|
|
|
80143
80246
|
}
|
|
80144
80247
|
|
|
80145
80248
|
// src/cli/commands/search.ts
|
|
80146
|
-
async function
|
|
80249
|
+
async function action32(query, options) {
|
|
80147
80250
|
if (!query || query.trim() === "") {
|
|
80148
80251
|
throw userError("Empty query.");
|
|
80149
80252
|
}
|
|
@@ -80309,7 +80412,7 @@ async function action31(query, options) {
|
|
|
80309
80412
|
}
|
|
80310
80413
|
}
|
|
80311
80414
|
function registerSearch(program2) {
|
|
80312
|
-
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);
|
|
80313
80416
|
}
|
|
80314
80417
|
|
|
80315
80418
|
// src/cli/commands/self-update.ts
|
|
@@ -80356,7 +80459,7 @@ async function fetchLatestVersion() {
|
|
|
80356
80459
|
}
|
|
80357
80460
|
return body.version;
|
|
80358
80461
|
}
|
|
80359
|
-
async function
|
|
80462
|
+
async function action33(options) {
|
|
80360
80463
|
let target;
|
|
80361
80464
|
try {
|
|
80362
80465
|
target = options.version ?? await fetchLatestVersion();
|
|
@@ -80409,7 +80512,7 @@ async function action32(options) {
|
|
|
80409
80512
|
}
|
|
80410
80513
|
function registerSelfUpdate(program2) {
|
|
80411
80514
|
const desc = "Upgrade Cerefox in place. Alias: `cerefox upgrade`.";
|
|
80412
|
-
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);
|
|
80413
80516
|
declaration(program2.command("self-update"));
|
|
80414
80517
|
declaration(program2.command("upgrade"));
|
|
80415
80518
|
}
|
|
@@ -80428,7 +80531,7 @@ function symbol2(status) {
|
|
|
80428
80531
|
return cErr.dim("ℹ");
|
|
80429
80532
|
}
|
|
80430
80533
|
}
|
|
80431
|
-
async function
|
|
80534
|
+
async function action34(options) {
|
|
80432
80535
|
const useSpinner = !options.json && process.stderr.isTTY;
|
|
80433
80536
|
const spinner = useSpinner ? ora({ text: "Starting checks…", spinner: "dots", stream: process.stderr }).start() : null;
|
|
80434
80537
|
const results = await runFastChecks({
|
|
@@ -80449,7 +80552,7 @@ async function action33(options) {
|
|
|
80449
80552
|
}
|
|
80450
80553
|
}
|
|
80451
80554
|
function registerStatus(program2) {
|
|
80452
|
-
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);
|
|
80453
80556
|
}
|
|
80454
80557
|
|
|
80455
80558
|
// src/cli/commands/token.ts
|
|
@@ -80482,10 +80585,10 @@ function upsertEnvVar(path, key, value, opts = {}) {
|
|
|
80482
80585
|
}
|
|
80483
80586
|
const re = new RegExp(`^(\\s*)${escapeRegExp(key)}=.*$`, "m");
|
|
80484
80587
|
let next;
|
|
80485
|
-
let
|
|
80588
|
+
let action35;
|
|
80486
80589
|
if (re.test(original)) {
|
|
80487
80590
|
next = original.replace(re, `$1${line}`);
|
|
80488
|
-
|
|
80591
|
+
action35 = "updated";
|
|
80489
80592
|
} else {
|
|
80490
80593
|
const base = original.endsWith(`
|
|
80491
80594
|
`) ? original : `${original}
|
|
@@ -80493,10 +80596,10 @@ function upsertEnvVar(path, key, value, opts = {}) {
|
|
|
80493
80596
|
next = `${base}
|
|
80494
80597
|
${header}${line}
|
|
80495
80598
|
`;
|
|
80496
|
-
|
|
80599
|
+
action35 = "added";
|
|
80497
80600
|
}
|
|
80498
80601
|
writeFileSync5(path, next);
|
|
80499
|
-
return { path, action:
|
|
80602
|
+
return { path, action: action35, backupPath };
|
|
80500
80603
|
}
|
|
80501
80604
|
function readEnvVar(path, key) {
|
|
80502
80605
|
if (!existsSync14(path))
|
|
@@ -85069,6 +85172,9 @@ function registerDocumentWriteRoutes(app, ctx) {
|
|
|
85069
85172
|
const title = String(body.title ?? "").trim();
|
|
85070
85173
|
const content = String(body.content ?? "");
|
|
85071
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
|
+
}
|
|
85072
85178
|
const metadata = body.metadata ?? {};
|
|
85073
85179
|
const doc2 = await getCurrentDoc(ctx, documentId);
|
|
85074
85180
|
if (!doc2) {
|
|
@@ -86334,6 +86440,7 @@ Learn more:
|
|
|
86334
86440
|
moveInto(document2, registerGetDoc, "get");
|
|
86335
86441
|
moveInto(document2, registerListDocs, "list");
|
|
86336
86442
|
moveInto(document2, registerDeleteDoc, "delete");
|
|
86443
|
+
moveInto(document2, registerDocumentDeadLinks, "dead-links");
|
|
86337
86444
|
registerDocumentRestore(document2);
|
|
86338
86445
|
registerDocumentEdit(document2);
|
|
86339
86446
|
registerDocumentSetProjects(document2);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* doesn't touch `supabase/functions/` leaves it alone).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
export const EF_VERSION = "1.7.
|
|
21
|
+
export const EF_VERSION = "1.7.1";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* The Cerefox RELEASE version — what `cerefox --version` reports and what npm
|
|
@@ -36,7 +36,7 @@ export const EF_VERSION = "1.7.0";
|
|
|
36
36
|
* is imported by the Deno Edge Functions, which cannot reach into the npm
|
|
37
37
|
* package.
|
|
38
38
|
*/
|
|
39
|
-
export const CEREFOX_VERSION = "1.7.
|
|
39
|
+
export const CEREFOX_VERSION = "1.7.1";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* The most recent version whose EF-side SOURCE actually changed (#127).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- 0025_drop_orphaned_overloads.sql — remove pre-author-era 1-arg overloads.
|
|
2
|
+
--
|
|
3
|
+
-- cerefox_purge_document(UUID) and cerefox_restore_document(UUID) survived
|
|
4
|
+
-- every CREATE OR REPLACE since their signatures grew (OR REPLACE only
|
|
5
|
+
-- replaces the SAME signature), leaving long-lived databases with BOTH
|
|
6
|
+
-- overloads. A named 1-arg call is then ambiguous — PostgREST PGRST203
|
|
7
|
+
-- ("could not choose the best candidate") — which is how the first
|
|
8
|
+
-- production acceptance run failed to purge its fixtures (v1.7.0).
|
|
9
|
+
-- Fresh databases never had the old signatures and are unaffected.
|
|
10
|
+
--
|
|
11
|
+
-- Schema version 0.12.0 → 0.12.1. The DROPs also run from rpcs.sql on every
|
|
12
|
+
-- deploy; this migration makes the version advance signal the redeploy.
|
|
13
|
+
|
|
14
|
+
DROP FUNCTION IF EXISTS cerefox_purge_document(UUID);
|
|
15
|
+
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- 0026_metadata_guard_and_dead_links.sql — #212 metadata type guards +
|
|
2
|
+
-- #214 phase-2 dead-link sweep.
|
|
3
|
+
--
|
|
4
|
+
-- RPC changes ship via rpcs.sql on this deploy: cerefox_ingest_document
|
|
5
|
+
-- rejects non-object p_metadata (the MCP layer always did; now every write
|
|
6
|
+
-- path agrees), cerefox_set_document_metadata refuses to MERGE onto a
|
|
7
|
+
-- non-object stored value (|| would produce an array; only replace=true
|
|
8
|
+
-- repairs), and two read-only RPCs arrive: cerefox_find_dead_links (whole-KB
|
|
9
|
+
-- [Text](uuid) sweep) and cerefox_metadata_health (rows with non-object
|
|
10
|
+
-- metadata, surfaced by doctor).
|
|
11
|
+
--
|
|
12
|
+
-- Schema version 0.12.1 → 0.12.2. This migration also REPORTS (not repairs)
|
|
13
|
+
-- any rows already in the non-object state, so the operator sees them at
|
|
14
|
+
-- upgrade time; repair is `cerefox document set-metadata <id> --replace`.
|
|
15
|
+
|
|
16
|
+
-- Table-level backstop (round-5 review): closes every current and future
|
|
17
|
+
-- direct writer at once, not only the RPC/CLI/web paths patched in code.
|
|
18
|
+
-- NOT VALID: legacy non-object rows survive (reported below) until repaired
|
|
19
|
+
-- with `document set-metadata --replace`, which the constraint then checks.
|
|
20
|
+
DO $$
|
|
21
|
+
BEGIN
|
|
22
|
+
IF NOT EXISTS (
|
|
23
|
+
SELECT 1 FROM pg_constraint
|
|
24
|
+
WHERE conname = 'cerefox_documents_metadata_object'
|
|
25
|
+
) THEN
|
|
26
|
+
ALTER TABLE cerefox_documents
|
|
27
|
+
ADD CONSTRAINT cerefox_documents_metadata_object
|
|
28
|
+
CHECK (jsonb_typeof(metadata) = 'object') NOT VALID;
|
|
29
|
+
END IF;
|
|
30
|
+
END $$;
|
|
31
|
+
|
|
32
|
+
DO $$
|
|
33
|
+
DECLARE
|
|
34
|
+
v_count INT;
|
|
35
|
+
BEGIN
|
|
36
|
+
SELECT count(*) INTO v_count
|
|
37
|
+
FROM cerefox_documents
|
|
38
|
+
WHERE metadata IS NOT NULL AND jsonb_typeof(metadata) <> 'object';
|
|
39
|
+
IF v_count > 0 THEN
|
|
40
|
+
RAISE NOTICE
|
|
41
|
+
'Migration 0026: % document(s) hold NON-OBJECT metadata (legacy #212 state). List them with cerefox doctor (or SELECT * FROM cerefox_metadata_health()); repair each with cerefox document set-metadata <id> --replace --json ''<object>''.',
|
|
42
|
+
v_count;
|
|
43
|
+
ELSE
|
|
44
|
+
RAISE NOTICE 'Migration 0026: metadata guards + dead-link sweep arrive with rpcs.sql. No non-object metadata rows found. Schema 0.12.2.';
|
|
45
|
+
END IF;
|
|
46
|
+
END $$;
|
|
@@ -1247,6 +1247,9 @@ $$;
|
|
|
1247
1247
|
|
|
1248
1248
|
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID, TEXT, TEXT, TEXT);
|
|
1249
1249
|
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID, TEXT, TEXT);
|
|
1250
|
+
-- 0.12.1: the pre-author 1-arg overload survived every CREATE OR REPLACE
|
|
1251
|
+
-- since the signature grew (same orphan class as purge; found live on prod).
|
|
1252
|
+
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID);
|
|
1250
1253
|
CREATE FUNCTION cerefox_restore_document(
|
|
1251
1254
|
p_document_id UUID,
|
|
1252
1255
|
p_author TEXT DEFAULT 'unknown',
|
|
@@ -1309,7 +1312,14 @@ $$;
|
|
|
1309
1312
|
-- ── cerefox_purge_document ───────────────────────────────────────────────────
|
|
1310
1313
|
-- Permanently deletes a soft-deleted document (CASCADE). Only works on
|
|
1311
1314
|
-- documents that are already soft-deleted (deleted_at IS NOT NULL).
|
|
1315
|
+
--
|
|
1316
|
+
-- 0.12.1: drop the pre-author 1-arg overload. CREATE OR REPLACE never removed
|
|
1317
|
+
-- it when the signature grew, so long-lived databases carried BOTH — and a
|
|
1318
|
+
-- 1-arg named call was ambiguous there (PGRST203), which is how the first
|
|
1319
|
+
-- prod acceptance run failed to purge its fixtures. Same cleanup for
|
|
1320
|
+
-- cerefox_restore_document below.
|
|
1312
1321
|
|
|
1322
|
+
DROP FUNCTION IF EXISTS cerefox_purge_document(UUID);
|
|
1313
1323
|
CREATE OR REPLACE FUNCTION cerefox_purge_document(
|
|
1314
1324
|
p_document_id UUID,
|
|
1315
1325
|
p_author TEXT DEFAULT 'unknown',
|
|
@@ -1347,6 +1357,43 @@ END;
|
|
|
1347
1357
|
$$;
|
|
1348
1358
|
|
|
1349
1359
|
|
|
1360
|
+
-- ── cerefox_extract_doc_link_ids ─────────────────────────────────────────────
|
|
1361
|
+
-- The ONE implementation of [Text](uuid) link scanning (#214), shared by the
|
|
1362
|
+
-- write-time guard in cerefox_ingest_document and the cerefox_find_dead_links
|
|
1363
|
+
-- sweep — "same scanning rules" is enforced by this being the only copy.
|
|
1364
|
+
--
|
|
1365
|
+
-- Fences are LINE-ANCHORED and handled by SPLITTING, not by a paired-fence
|
|
1366
|
+
-- regex: Postgres AREs give the whole RE the greediness of their first
|
|
1367
|
+
-- quantified atom, which silently overrode a later .*? and made a closed
|
|
1368
|
+
-- fence strip everything to end-of-string (round-5 review, verified live) —
|
|
1369
|
+
-- blinding the scan to every link after any code block. Odd-numbered split
|
|
1370
|
+
-- segments are outside fences; an unterminated fence leaves its tail inside
|
|
1371
|
+
-- an even segment, dropped (under-validate, never false-reject). Inline code
|
|
1372
|
+
-- spans are stripped after (their regex has no greediness hazard).
|
|
1373
|
+
|
|
1374
|
+
CREATE OR REPLACE FUNCTION cerefox_extract_doc_link_ids(p_content TEXT)
|
|
1375
|
+
RETURNS TABLE (link_id TEXT)
|
|
1376
|
+
LANGUAGE sql
|
|
1377
|
+
IMMUTABLE
|
|
1378
|
+
SET search_path = public, pg_catalog
|
|
1379
|
+
AS $$
|
|
1380
|
+
WITH segments AS (
|
|
1381
|
+
SELECT seg, row_number() OVER () AS rn
|
|
1382
|
+
FROM regexp_split_to_table(COALESCE(p_content, ''), '^[ \t]*```.*$', 'n') AS seg
|
|
1383
|
+
),
|
|
1384
|
+
outside AS (
|
|
1385
|
+
SELECT string_agg(regexp_replace(seg, '`[^`]*`', ' ', 'g'), ' ') AS s
|
|
1386
|
+
FROM segments
|
|
1387
|
+
WHERE rn % 2 = 1
|
|
1388
|
+
)
|
|
1389
|
+
SELECT lower(m[1])
|
|
1390
|
+
FROM outside,
|
|
1391
|
+
LATERAL regexp_matches(
|
|
1392
|
+
outside.s,
|
|
1393
|
+
'\]\(([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\)',
|
|
1394
|
+
'g') AS m;
|
|
1395
|
+
$$;
|
|
1396
|
+
|
|
1350
1397
|
-- ── cerefox_ingest_document ──────────────────────────────────────────────────
|
|
1351
1398
|
-- Single RPC for ingesting a document (create or update). Handles:
|
|
1352
1399
|
-- - Create: insert document row, insert chunks, set review_status, create audit entry
|
|
@@ -1494,6 +1541,19 @@ BEGIN
|
|
|
1494
1541
|
USING ERRCODE = '22023'; -- invalid_parameter_value
|
|
1495
1542
|
END IF;
|
|
1496
1543
|
|
|
1544
|
+
-- ── Metadata type guard (#212, 0.12.2) ───────────────────────────────
|
|
1545
|
+
-- metadata is jsonb and accepts ANY JSON value, but every reader assumes
|
|
1546
|
+
-- an object — and `document edit`'s JS spread DECOMPOSED a stored string
|
|
1547
|
+
-- into per-character keys (13 real documents were in the vulnerable
|
|
1548
|
+
-- state). The MCP layer always validated this; the RPC now does too, so
|
|
1549
|
+
-- every write path agrees (CLI, scripts, direct PostgREST included).
|
|
1550
|
+
IF p_metadata IS NOT NULL AND jsonb_typeof(p_metadata) <> 'object' THEN
|
|
1551
|
+
RAISE EXCEPTION
|
|
1552
|
+
'cerefox_ingest_document: metadata must be a JSON object, got %. Wrap scalar values in a key ({"value": ...}).',
|
|
1553
|
+
jsonb_typeof(p_metadata)
|
|
1554
|
+
USING ERRCODE = '22023'; -- invalid_parameter_value
|
|
1555
|
+
END IF;
|
|
1556
|
+
|
|
1497
1557
|
-- ── Link integrity (#214, 0.12.0) ────────────────────────────────────
|
|
1498
1558
|
-- Validate [Text](uuid) document links against the store: agents mangle
|
|
1499
1559
|
-- long random ids when regenerating text, and a mangled id silently
|
|
@@ -1506,23 +1566,14 @@ BEGIN
|
|
|
1506
1566
|
-- See docs/specs/link-integrity-design.md.
|
|
1507
1567
|
SELECT string_agg(c->>'content', E'\n') INTO v_scannable
|
|
1508
1568
|
FROM jsonb_array_elements(p_chunks) c;
|
|
1509
|
-
|
|
1510
|
-
--
|
|
1511
|
-
--
|
|
1512
|
-
--
|
|
1513
|
-
--
|
|
1514
|
-
|
|
1515
|
-
v_scannable
|
|
1516
|
-
|
|
1517
|
-
E'(^|\\n)[ \\t]*```.*?(\\n[ \\t]*```[^\\n]*|$)', ' ', 'g');
|
|
1518
|
-
v_scannable := regexp_replace(v_scannable, '`[^`]*`', ' ', 'g');
|
|
1519
|
-
|
|
1520
|
-
SELECT array_agg(DISTINCT m[1]) INTO v_missing
|
|
1521
|
-
FROM regexp_matches(
|
|
1522
|
-
v_scannable,
|
|
1523
|
-
'\]\(([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\)',
|
|
1524
|
-
'g') m
|
|
1525
|
-
WHERE NOT EXISTS (SELECT 1 FROM cerefox_documents d WHERE d.id = m[1]::uuid);
|
|
1569
|
+
|
|
1570
|
+
-- Scanning delegated to cerefox_extract_doc_link_ids — the one copy of
|
|
1571
|
+
-- the fence/inline-code/uuid rules, shared with the dead-link sweep
|
|
1572
|
+
-- (0.12.2; the previous inline regex was defeated by ARE whole-RE
|
|
1573
|
+
-- greediness — see the helper's header).
|
|
1574
|
+
SELECT array_agg(DISTINCT l.link_id) INTO v_missing
|
|
1575
|
+
FROM cerefox_extract_doc_link_ids(v_scannable) l
|
|
1576
|
+
WHERE NOT EXISTS (SELECT 1 FROM cerefox_documents d WHERE d.id = l.link_id::uuid);
|
|
1526
1577
|
|
|
1527
1578
|
-- On UPDATE, tolerate dead links the document ALREADY carries: a target
|
|
1528
1579
|
-- purged after linking must not make the document unwritable — sync
|
|
@@ -2662,6 +2713,30 @@ BEGIN
|
|
|
2662
2713
|
USING ERRCODE = 'P0002';
|
|
2663
2714
|
END IF;
|
|
2664
2715
|
|
|
2716
|
+
-- #212: a legacy row can hold NON-OBJECT metadata (the ingest RPC did not
|
|
2717
|
+
-- validate its input until 0.12.2). Merging onto it with || would produce
|
|
2718
|
+
-- an ARRAY — Postgres treats both sides as arrays — burying the stored
|
|
2719
|
+
-- value one level deeper and leaving the row still corrupt. Only replace
|
|
2720
|
+
-- actually repairs such a row, so refuse the merge and say so. jsonb
|
|
2721
|
+
-- 'null' is treated like SQL NULL (empty), matching the CLI.
|
|
2722
|
+
IF NOT p_replace AND v_before IS NOT NULL
|
|
2723
|
+
AND jsonb_typeof(v_before) NOT IN ('object', 'null') THEN
|
|
2724
|
+
RAISE EXCEPTION
|
|
2725
|
+
'CEREFOX_BAD_METADATA: stored metadata on document % is not an object (%). A merge cannot repair it — retry with replace (CLI: --replace; MCP: replace: true), passing the full intended object.',
|
|
2726
|
+
p_document_id, jsonb_typeof(v_before)
|
|
2727
|
+
USING ERRCODE = '22023';
|
|
2728
|
+
END IF;
|
|
2729
|
+
|
|
2730
|
+
-- Normalized BEFORE value for merge and reporting: everything below must
|
|
2731
|
+
-- work when the stored value is a scalar/array/'null' and p_replace is
|
|
2732
|
+
-- true — that is the REPAIR path, and jsonb_object_keys on a scalar
|
|
2733
|
+
-- errors, which would roll back the repair itself (round-5 review,
|
|
2734
|
+
-- verified live).
|
|
2735
|
+
v_before := CASE
|
|
2736
|
+
WHEN v_before IS NULL OR jsonb_typeof(v_before) <> 'object' THEN '{}'::jsonb
|
|
2737
|
+
ELSE v_before
|
|
2738
|
+
END;
|
|
2739
|
+
|
|
2665
2740
|
-- Keys explicitly set to null are removals, never stored values.
|
|
2666
2741
|
SELECT COALESCE(array_agg(key), ARRAY[]::TEXT[]) INTO v_null_keys
|
|
2667
2742
|
FROM jsonb_each(p_metadata)
|
|
@@ -2713,6 +2788,11 @@ SET search_path = public, pg_catalog
|
|
|
2713
2788
|
AS $$
|
|
2714
2789
|
-- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
|
|
2715
2790
|
-- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
|
|
2791
|
+
-- 0.12.2 (#212, #214): metadata must be a JSON object (ingest input guard
|
|
2792
|
+
-- + set_document_metadata stored-state merge guard); cerefox_find_dead_links
|
|
2793
|
+
-- (link-integrity phase-2 sweep); cerefox_metadata_health (doctor check).
|
|
2794
|
+
-- 0.12.1: drop orphaned 1-arg overloads of purge/restore (pre-author era;
|
|
2795
|
+
-- CREATE OR REPLACE never removed them; ambiguous PGRST203 on 1-arg calls).
|
|
2716
2796
|
-- 0.12.0 (#208, #210): cerefox_delete_document — CAS
|
|
2717
2797
|
-- (p_expected_content_hash), p_reason in audit description, JSONB return,
|
|
2718
2798
|
-- idempotent re-delete; cerefox_restore_document — same rework (JSONB,
|
|
@@ -2726,7 +2806,75 @@ AS $$
|
|
|
2726
2806
|
-- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
|
|
2727
2807
|
-- the partial-edit surface, and both migrations (0019, 0020) are in the
|
|
2728
2808
|
-- sequence, so a store deploying this gets everything from both lines.
|
|
2729
|
-
SELECT '0.12.
|
|
2809
|
+
SELECT '0.12.2'::TEXT;
|
|
2810
|
+
$$;
|
|
2811
|
+
|
|
2812
|
+
-- ── cerefox_find_dead_links ──────────────────────────────────────────────────
|
|
2813
|
+
-- Phase 2 of link integrity (#214): a read-only whole-KB sweep for
|
|
2814
|
+
-- [Text](uuid) links whose target document no longer EXISTS (purged after
|
|
2815
|
+
-- linking). Complements the write-time guard, which validates only
|
|
2816
|
+
-- newly-introduced links on updates. Same scanning rules as the guard:
|
|
2817
|
+
-- line-anchored fences and inline code spans are stripped (examples are not
|
|
2818
|
+
-- links); a trashed target still exists and is NOT a dead link.
|
|
2819
|
+
-- Full chunk scan — run on demand (CLI `cerefox document dead-links`), not on
|
|
2820
|
+
-- every doctor.
|
|
2821
|
+
|
|
2822
|
+
CREATE OR REPLACE FUNCTION cerefox_find_dead_links()
|
|
2823
|
+
RETURNS TABLE (
|
|
2824
|
+
document_id UUID,
|
|
2825
|
+
document_title TEXT,
|
|
2826
|
+
dead_link_id UUID,
|
|
2827
|
+
occurrences BIGINT
|
|
2828
|
+
)
|
|
2829
|
+
LANGUAGE sql
|
|
2830
|
+
STABLE
|
|
2831
|
+
SECURITY DEFINER
|
|
2832
|
+
SET search_path = public, pg_catalog
|
|
2833
|
+
AS $$
|
|
2834
|
+
-- Scanning delegated to cerefox_extract_doc_link_ids — the one copy of
|
|
2835
|
+
-- the fence/inline-code/uuid rules, shared with the write-time guard.
|
|
2836
|
+
-- Deliberate scope: trashed LINKER documents are excluded (they are
|
|
2837
|
+
-- inert until restored; restoring one re-enters it into the next sweep).
|
|
2838
|
+
WITH doc_content AS (
|
|
2839
|
+
SELECT d.id, d.title, string_agg(c.content, E'\n' ORDER BY c.chunk_index) AS content
|
|
2840
|
+
FROM cerefox_documents d
|
|
2841
|
+
JOIN cerefox_chunks c ON c.document_id = d.id AND c.version_id IS NULL
|
|
2842
|
+
WHERE d.deleted_at IS NULL
|
|
2843
|
+
GROUP BY d.id, d.title
|
|
2844
|
+
),
|
|
2845
|
+
links AS (
|
|
2846
|
+
SELECT dc.id, dc.title, l.link_id AS target
|
|
2847
|
+
FROM doc_content dc,
|
|
2848
|
+
LATERAL cerefox_extract_doc_link_ids(dc.content) l
|
|
2849
|
+
)
|
|
2850
|
+
SELECT l.id, l.title, l.target::uuid, count(*)
|
|
2851
|
+
FROM links l
|
|
2852
|
+
WHERE NOT EXISTS (SELECT 1 FROM cerefox_documents t WHERE t.id = l.target::uuid)
|
|
2853
|
+
GROUP BY l.id, l.title, l.target
|
|
2854
|
+
ORDER BY l.title, l.target;
|
|
2855
|
+
$$;
|
|
2856
|
+
|
|
2857
|
+
-- ── cerefox_metadata_health ──────────────────────────────────────────────────
|
|
2858
|
+
-- #212: rows whose stored metadata is not a JSON object — the state the
|
|
2859
|
+
-- 0.12.2 write guards now prevent, but which legacy rows may still be in.
|
|
2860
|
+
-- Cheap (documents table only); surfaced by `cerefox doctor`. Repair:
|
|
2861
|
+
-- `cerefox document set-metadata <id> --replace --json '<object>'`.
|
|
2862
|
+
|
|
2863
|
+
CREATE OR REPLACE FUNCTION cerefox_metadata_health()
|
|
2864
|
+
RETURNS TABLE (
|
|
2865
|
+
document_id UUID,
|
|
2866
|
+
document_title TEXT,
|
|
2867
|
+
metadata_type TEXT
|
|
2868
|
+
)
|
|
2869
|
+
LANGUAGE sql
|
|
2870
|
+
STABLE
|
|
2871
|
+
SECURITY DEFINER
|
|
2872
|
+
SET search_path = public, pg_catalog
|
|
2873
|
+
AS $$
|
|
2874
|
+
SELECT d.id, d.title, jsonb_typeof(d.metadata)
|
|
2875
|
+
FROM cerefox_documents d
|
|
2876
|
+
WHERE d.metadata IS NOT NULL AND jsonb_typeof(d.metadata) <> 'object'
|
|
2877
|
+
ORDER BY d.title;
|
|
2730
2878
|
$$;
|
|
2731
2879
|
|
|
2732
2880
|
-- ── cerefox_content_format_stats ─────────────────────────────────────────────
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
-- Requires extensions: vector (pgvector), uuid-ossp
|
|
6
6
|
-- These are enabled at the top of db_deploy.py before this file is applied.
|
|
7
7
|
--
|
|
8
|
-
-- @version: 0.12.
|
|
8
|
+
-- @version: 0.12.2
|
|
9
9
|
-- The `@version` marker above is read by the schema-version-mismatch banner
|
|
10
10
|
-- (see /api/v1/schema-version). Bump it whenever schema.sql OR rpcs.sql
|
|
11
11
|
-- changes in a way that requires `cerefox server deploy` to be re-run —
|
|
@@ -42,7 +42,15 @@ CREATE TABLE IF NOT EXISTS cerefox_documents (
|
|
|
42
42
|
source_path TEXT,
|
|
43
43
|
-- SHA-256 of raw markdown content; used for deduplication
|
|
44
44
|
content_hash TEXT NOT NULL,
|
|
45
|
-
|
|
45
|
+
-- #212 (0.12.2): must be a JSON OBJECT — jsonb accepts any JSON value,
|
|
46
|
+
-- but every reader assumes an object, and non-object values were silently
|
|
47
|
+
-- destroyed by read-modify-write paths. The table-level CHECK closes all
|
|
48
|
+
-- current AND future direct writers at once; existing databases get it as
|
|
49
|
+
-- NOT VALID via migration 0026 (legacy rows survive until repaired with
|
|
50
|
+
-- `document set-metadata --replace`, which the constraint then validates).
|
|
51
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb
|
|
52
|
+
CONSTRAINT cerefox_documents_metadata_object
|
|
53
|
+
CHECK (jsonb_typeof(metadata) = 'object'),
|
|
46
54
|
chunk_count INT NOT NULL DEFAULT 0,
|
|
47
55
|
total_chars INT NOT NULL DEFAULT 0,
|
|
48
56
|
-- review_status: human governance flag. 'approved' = validated by human,
|
package/docs/guides/cli.md
CHANGED
|
@@ -226,6 +226,12 @@ cerefox document list [OPTIONS]
|
|
|
226
226
|
|
|
227
227
|
**Purpose**: update a document's title and/or metadata in place, without re-ingesting content.
|
|
228
228
|
|
|
229
|
+
> **v1.7.1 (#212)**: a title-only edit no longer writes `metadata` at all, and
|
|
230
|
+
> the command refuses to patch a document whose stored metadata is not a JSON
|
|
231
|
+
> object (a legacy state older writes could create) instead of destroying it —
|
|
232
|
+
> repair such a row with `cerefox document set-metadata <id> --replace --json
|
|
233
|
+
> '<object>'`. `cerefox doctor` lists any rows in that state.
|
|
234
|
+
|
|
229
235
|
**Synopsis**:
|
|
230
236
|
```
|
|
231
237
|
cerefox document edit [OPTIONS] DOCUMENT_ID
|
|
@@ -341,6 +347,20 @@ cerefox document set-projects <doc-id> --clear
|
|
|
341
347
|
|
|
342
348
|
---
|
|
343
349
|
|
|
350
|
+
### `cerefox document dead-links`
|
|
351
|
+
|
|
352
|
+
**Purpose**: whole-KB sweep for `[Text](uuid)` links whose target document no longer exists (phase 2 of link integrity, #214; v1.7.1, needs schema 0.12.2).
|
|
353
|
+
|
|
354
|
+
**Synopsis**: `cerefox document dead-links [--json]`
|
|
355
|
+
|
|
356
|
+
The write-time guard (v1.7.0) validates only links a write *introduces* — deliberately, so a target purged after linking cannot make its linkers unwritable. This command finds those legacy dead links on demand. A trashed target still exists and is **not** reported; trashed **linker** documents are also excluded (inert until restored — a restore re-enters them into the next sweep). Full chunk scan server-side (one RPC call); run on demand, not part of `doctor`.
|
|
357
|
+
|
|
358
|
+
**Fix each hit** by editing the linking document: correct the id, remove the link, or backtick it as an example. Full linking overview: [`linking.md`](linking.md).
|
|
359
|
+
|
|
360
|
+
**MCP equivalent**: none (maintenance verb).
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
344
364
|
### `cerefox document restore`
|
|
345
365
|
|
|
346
366
|
**Purpose**: restore a soft-deleted (trashed) document back to active.
|
|
@@ -806,6 +826,7 @@ surface).
|
|
|
806
826
|
| `document get` | `cerefox_get_document` | ✅ |
|
|
807
827
|
| `document list` | `cerefox_metadata_search` (scope by `project_name` / metadata / time) | ✅ as of this change. Unscoped whole-KB listing has no MCP path by design (scope it) |
|
|
808
828
|
| `document edit` (title / metadata in place) | — | 🔒 intentional: a human/web-parity convenience. Agents update title+metadata deterministically via `cerefox_ingest` (with `document_id`); a metadata-only edit isn't a needed agent primitive |
|
|
829
|
+
| `document dead-links` | — | 🔒 intentional: a maintenance sweep for the operator (v1.7.1, #214 phase 2); agents encounter dead links through the write-time guard instead |
|
|
809
830
|
| `document delete` (soft-delete) | `cerefox_delete_document` | ✅ v1.7.0 (#208). MCP requires the caller's read-hash; the CLI confirms interactively instead |
|
|
810
831
|
| `document restore` | `cerefox_restore_document` | ✅ v1.7.0 (#210). Permanent purge remains web-UI-only |
|
|
811
832
|
| `document version list` | `cerefox_list_versions` | ✅ |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerefox/memory",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Cerefox — user-owned shared memory for AI agents. CLI + stdio MCP server + web UI + ingestion for a knowledge base on your own Supabase project (or fully self-hosted with Cerefox Local).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/fstamatelopoulos/cerefox",
|