@cerefox/memory 1.7.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +13 -0
- package/AGENT_QUICK_REFERENCE.md +1 -1
- package/README.md +4 -0
- package/dist/bin/cerefox.js +325 -99
- package/dist/frontend/assets/index-DWT7wZMR.js +121 -0
- package/dist/frontend/assets/index-DWT7wZMR.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/_projects.ts +76 -51
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +45 -0
- package/dist/server-assets/_shared/mcp-tools/audit-log.ts +3 -5
- package/dist/server-assets/_shared/mcp-tools/audit-ops.ts +46 -0
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +1 -1
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +6 -6
- package/dist/server-assets/db/migrations/0025_drop_orphaned_overloads.sql +2 -1
- package/dist/server-assets/db/migrations/0027_drop_archived_search_artifacts.sql +172 -0
- package/dist/server-assets/db/migrations/0028_audit_store_level_writes.sql +349 -0
- package/dist/server-assets/db/rpcs.sql +270 -64
- package/dist/server-assets/db/schema.sql +18 -4
- package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +14 -97
- package/docs/guides/cli.md +13 -2
- package/docs/guides/connect-agents.md +4 -19
- package/docs/guides/ops-scripts.md +4 -4
- package/docs/guides/upgrading.md +14 -1
- package/package.json +2 -2
- package/dist/frontend/assets/index-D8E0mTnp.js +0 -121
- package/dist/frontend/assets/index-D8E0mTnp.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.9.0";
|
|
7442
7442
|
var init_meta = () => {};
|
|
7443
7443
|
|
|
7444
7444
|
// ../../_shared/config/paths.ts
|
|
@@ -23427,6 +23427,44 @@ var init_client = __esm(() => {
|
|
|
23427
23427
|
init_cli_core();
|
|
23428
23428
|
});
|
|
23429
23429
|
|
|
23430
|
+
// ../../_shared/mcp-tools/audit-ops.ts
|
|
23431
|
+
function isStoreLevelAuditOp(operation) {
|
|
23432
|
+
return STORE_LEVEL_AUDIT_OPS.includes(operation ?? "");
|
|
23433
|
+
}
|
|
23434
|
+
function auditDocLabel(docTitle, documentId, operation) {
|
|
23435
|
+
if (docTitle)
|
|
23436
|
+
return docTitle;
|
|
23437
|
+
if (documentId)
|
|
23438
|
+
return documentId.slice(0, 8) + "…";
|
|
23439
|
+
return isStoreLevelAuditOp(operation) ? "(store)" : "(deleted)";
|
|
23440
|
+
}
|
|
23441
|
+
var STORE_LEVEL_AUDIT_OPS, AUDIT_OPERATIONS;
|
|
23442
|
+
var init_audit_ops = __esm(() => {
|
|
23443
|
+
STORE_LEVEL_AUDIT_OPS = [
|
|
23444
|
+
"config-change",
|
|
23445
|
+
"project-create",
|
|
23446
|
+
"project-edit",
|
|
23447
|
+
"project-delete"
|
|
23448
|
+
];
|
|
23449
|
+
AUDIT_OPERATIONS = [
|
|
23450
|
+
"create",
|
|
23451
|
+
"update-content",
|
|
23452
|
+
"update-metadata",
|
|
23453
|
+
"insert",
|
|
23454
|
+
"replace-section",
|
|
23455
|
+
"delete-section",
|
|
23456
|
+
"rename-section",
|
|
23457
|
+
"delete",
|
|
23458
|
+
"restore",
|
|
23459
|
+
"status-change",
|
|
23460
|
+
"archive",
|
|
23461
|
+
"unarchive",
|
|
23462
|
+
"relation-set",
|
|
23463
|
+
"relation-delete",
|
|
23464
|
+
...STORE_LEVEL_AUDIT_OPS
|
|
23465
|
+
];
|
|
23466
|
+
});
|
|
23467
|
+
|
|
23430
23468
|
// ../../_shared/mcp-tools/_utils.ts
|
|
23431
23469
|
function getMaxResponseBytes() {
|
|
23432
23470
|
const raw = globalThis.process?.env?.CEREFOX_MAX_RESPONSE_BYTES;
|
|
@@ -23486,6 +23524,21 @@ function extractConflictHashes(message) {
|
|
|
23486
23524
|
function isDocumentNotFoundError(error) {
|
|
23487
23525
|
return error.code === "22023" && /not found/i.test(error.message ?? "");
|
|
23488
23526
|
}
|
|
23527
|
+
function isAuditCheckError(message) {
|
|
23528
|
+
return /cerefox_audit_log_operation_check/.test(message);
|
|
23529
|
+
}
|
|
23530
|
+
function isDuplicateKeyError(message) {
|
|
23531
|
+
return /duplicate key|unique constraint|23505/i.test(message);
|
|
23532
|
+
}
|
|
23533
|
+
function storeWriteRemediation(message, fnName) {
|
|
23534
|
+
if (isMissingFunctionError(message, fnName)) {
|
|
23535
|
+
return "The deployed server predates schema 0.14.0 — or PostgREST's schema cache " + "is stale right after a deploy. If you just deployed, retry in a few " + "seconds; otherwise run `cerefox server deploy`.";
|
|
23536
|
+
}
|
|
23537
|
+
if (isAuditCheckError(message)) {
|
|
23538
|
+
return "The server's audit-log constraint predates migration 0028 (partial " + "deploy). Run `cerefox server deploy` to apply pending migrations, then retry.";
|
|
23539
|
+
}
|
|
23540
|
+
return null;
|
|
23541
|
+
}
|
|
23489
23542
|
function isMissingFunctionError(message, fnName) {
|
|
23490
23543
|
return message.includes("Could not find the function") && message.includes(fnName) || message.includes("does not exist") && message.includes(fnName);
|
|
23491
23544
|
}
|
|
@@ -23502,6 +23555,9 @@ function logUsage(supabase, params) {
|
|
|
23502
23555
|
})).catch(() => {});
|
|
23503
23556
|
}
|
|
23504
23557
|
var MAX_RESPONSE_BYTES = 200000, DEFAULT_MIN_SEARCH_SCORE = 0.5, DEFAULT_MIN_SEARCH_SCORE_LOCAL = 0.6, DEFAULT_SEARCH_ALPHA = 0.7;
|
|
23558
|
+
var init__utils = __esm(() => {
|
|
23559
|
+
init_audit_ops();
|
|
23560
|
+
});
|
|
23505
23561
|
|
|
23506
23562
|
// ../../_shared/server-assets/index.ts
|
|
23507
23563
|
import { existsSync as existsSync5 } from "node:fs";
|
|
@@ -25543,15 +25599,27 @@ var init_src = __esm(() => {
|
|
|
25543
25599
|
});
|
|
25544
25600
|
|
|
25545
25601
|
// ../../_shared/mcp-tools/_projects.ts
|
|
25546
|
-
async function
|
|
25547
|
-
|
|
25548
|
-
|
|
25549
|
-
|
|
25550
|
-
|
|
25551
|
-
|
|
25552
|
-
|
|
25553
|
-
|
|
25602
|
+
async function resolveOrCreateProject(supabase, projectName, audit) {
|
|
25603
|
+
const { data, error } = await supabase.rpc("cerefox_create_project", {
|
|
25604
|
+
p_name: projectName,
|
|
25605
|
+
p_description: "",
|
|
25606
|
+
p_author: audit?.author ?? "unknown",
|
|
25607
|
+
p_author_type: audit?.authorType ?? "agent",
|
|
25608
|
+
p_if_exists: "return"
|
|
25609
|
+
});
|
|
25610
|
+
if (error) {
|
|
25611
|
+
const remediation = storeWriteRemediation(error.message ?? "", "cerefox_create_project");
|
|
25612
|
+
if (remediation)
|
|
25613
|
+
throw new Error(`Project resolution failed for '${projectName}': ${remediation}`);
|
|
25614
|
+
console.warn("resolveOrCreateProject: RPC failed", error);
|
|
25615
|
+
return null;
|
|
25554
25616
|
}
|
|
25617
|
+
const row = data?.[0];
|
|
25618
|
+
return row?.project_id ? { projectId: row.project_id, projectName: row.project_name ?? projectName } : null;
|
|
25619
|
+
}
|
|
25620
|
+
async function ensureDocumentInProject(supabase, documentId, projectName, audit) {
|
|
25621
|
+
const resolved = await resolveOrCreateProject(supabase, projectName, audit);
|
|
25622
|
+
const projectId = resolved?.projectId ?? null;
|
|
25555
25623
|
if (!projectId)
|
|
25556
25624
|
return null;
|
|
25557
25625
|
const { data: existing } = await supabase.from("cerefox_document_projects").select("document_id").eq("document_id", documentId).eq("project_id", projectId).limit(1);
|
|
@@ -25563,20 +25631,14 @@ async function ensureDocumentInProject(supabase, documentId, projectName) {
|
|
|
25563
25631
|
}
|
|
25564
25632
|
return projectId;
|
|
25565
25633
|
}
|
|
25566
|
-
async function setDocumentProjectsByName(supabase, documentId, projectNames) {
|
|
25567
|
-
const
|
|
25568
|
-
|
|
25569
|
-
|
|
25570
|
-
|
|
25571
|
-
|
|
25572
|
-
if (proj?.length) {
|
|
25573
|
-
projectIds.push(proj[0].id);
|
|
25574
|
-
} else {
|
|
25575
|
-
const { data: newProj } = await supabase.from("cerefox_projects").insert({ name }).select("id");
|
|
25576
|
-
if (newProj?.[0]?.id)
|
|
25577
|
-
projectIds.push(newProj[0].id);
|
|
25578
|
-
}
|
|
25634
|
+
async function setDocumentProjectsByName(supabase, documentId, projectNames, audit) {
|
|
25635
|
+
const wanted = projectNames.filter((n) => !!n);
|
|
25636
|
+
const resolved = await Promise.all(wanted.map((n) => resolveOrCreateProject(supabase, n, audit)));
|
|
25637
|
+
const failed = wanted.filter((_, i) => !resolved[i]);
|
|
25638
|
+
if (failed.length > 0) {
|
|
25639
|
+
throw new Error(`Could not resolve project(s): ${failed.join(", ")} — memberships left unchanged.`);
|
|
25579
25640
|
}
|
|
25641
|
+
const projectIds = resolved.map((r) => r.projectId);
|
|
25580
25642
|
await supabase.from("cerefox_document_projects").delete().eq("document_id", documentId);
|
|
25581
25643
|
if (projectIds.length > 0) {
|
|
25582
25644
|
const rows = projectIds.map((pid) => ({ document_id: documentId, project_id: pid }));
|
|
@@ -25602,17 +25664,12 @@ async function replaceDocumentProjects(supabase, opts) {
|
|
|
25602
25664
|
if (!doc?.length) {
|
|
25603
25665
|
throw new Error(`Document not found (or soft-deleted): ${documentId}`);
|
|
25604
25666
|
}
|
|
25605
|
-
const
|
|
25606
|
-
|
|
25607
|
-
|
|
25608
|
-
|
|
25609
|
-
projectIds.push(proj[0].id);
|
|
25610
|
-
} else {
|
|
25611
|
-
const { data: newProj } = await supabase.from("cerefox_projects").insert({ name }).select("id");
|
|
25612
|
-
if (newProj?.[0]?.id)
|
|
25613
|
-
projectIds.push(newProj[0].id);
|
|
25614
|
-
}
|
|
25667
|
+
const resolved = await Promise.all(cleanNames.map((name) => resolveOrCreateProject(supabase, name, { author, authorType })));
|
|
25668
|
+
const failed = cleanNames.filter((_, i) => !resolved[i]);
|
|
25669
|
+
if (failed.length > 0) {
|
|
25670
|
+
throw new Error(`Could not resolve project(s): ${failed.join(", ")} — memberships left unchanged.`);
|
|
25615
25671
|
}
|
|
25672
|
+
const projectIds = resolved.map((r) => r.projectId);
|
|
25616
25673
|
await supabase.from("cerefox_document_projects").delete().eq("document_id", documentId);
|
|
25617
25674
|
if (projectIds.length > 0) {
|
|
25618
25675
|
const rows = projectIds.map((pid) => ({ document_id: documentId, project_id: pid }));
|
|
@@ -25647,7 +25704,9 @@ async function lookupProjectId(supabase, projectName) {
|
|
|
25647
25704
|
return null;
|
|
25648
25705
|
return data[0].id;
|
|
25649
25706
|
}
|
|
25650
|
-
var init__projects = () => {
|
|
25707
|
+
var init__projects = __esm(() => {
|
|
25708
|
+
init__utils();
|
|
25709
|
+
});
|
|
25651
25710
|
|
|
25652
25711
|
// src/cli/util/bundled-docs.ts
|
|
25653
25712
|
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync } from "node:fs";
|
|
@@ -25743,7 +25802,7 @@ var init_bundled_docs = __esm(() => {
|
|
|
25743
25802
|
});
|
|
25744
25803
|
|
|
25745
25804
|
// ../../_shared/ef-meta/index.ts
|
|
25746
|
-
var EF_VERSION = "1.
|
|
25805
|
+
var EF_VERSION = "1.9.0", CEREFOX_VERSION = "1.9.0", EF_LAST_CHANGED = "1.9.0";
|
|
25747
25806
|
var init_ef_meta = () => {};
|
|
25748
25807
|
|
|
25749
25808
|
// ../../_shared/compatibility/index.ts
|
|
@@ -25874,7 +25933,7 @@ async function checkServerCompatibility(opts) {
|
|
|
25874
25933
|
var COMPATIBILITY;
|
|
25875
25934
|
var init_compatibility = __esm(() => {
|
|
25876
25935
|
COMPATIBILITY = {
|
|
25877
|
-
minSchema: "0.
|
|
25936
|
+
minSchema: "0.14.0",
|
|
25878
25937
|
minEdgeFunctions: "0.6.0"
|
|
25879
25938
|
};
|
|
25880
25939
|
});
|
|
@@ -54767,7 +54826,7 @@ async function handler(supabase, args, ctx) {
|
|
|
54767
54826
|
if (!entries.length)
|
|
54768
54827
|
return "No audit log entries found.";
|
|
54769
54828
|
const lines = entries.map((e) => {
|
|
54770
|
-
const docLabel = e.doc_title
|
|
54829
|
+
const docLabel = auditDocLabel(e.doc_title, e.document_id, e.operation);
|
|
54771
54830
|
const sizeInfo = e.size_before != null && e.size_after != null ? ` | ${e.size_before} -> ${e.size_after} chars` : e.size_after != null ? ` | ${e.size_after} chars` : "";
|
|
54772
54831
|
return `${utcStamp2(e.created_at)} | ${e.operation} | ${e.author} (${e.author_type}) | ${docLabel}${sizeInfo} | ${e.description}`;
|
|
54773
54832
|
});
|
|
@@ -54778,6 +54837,7 @@ ${lines.join(`
|
|
|
54778
54837
|
}
|
|
54779
54838
|
var auditLogTool;
|
|
54780
54839
|
var init_audit_log = __esm(() => {
|
|
54840
|
+
init__utils();
|
|
54781
54841
|
auditLogTool = {
|
|
54782
54842
|
name: "cerefox_get_audit_log",
|
|
54783
54843
|
description: "Retrieve audit log entries showing who changed what and when. Supports filtering by document, author, operation type, and time range. Returns entries with document titles, author attribution, size changes, and descriptions.",
|
|
@@ -54795,7 +54855,7 @@ var init_audit_log = __esm(() => {
|
|
|
54795
54855
|
author: { type: "string", description: "Filter by author name (optional)" },
|
|
54796
54856
|
operation: {
|
|
54797
54857
|
type: "string",
|
|
54798
|
-
description:
|
|
54858
|
+
description: `Filter by operation type: ${AUDIT_OPERATIONS.join(", ")} (optional)`
|
|
54799
54859
|
},
|
|
54800
54860
|
since: {
|
|
54801
54861
|
type: "string",
|
|
@@ -54881,6 +54941,7 @@ async function handler2(supabase, args, ctx) {
|
|
|
54881
54941
|
}
|
|
54882
54942
|
var deleteDocumentTool;
|
|
54883
54943
|
var init_delete_document = __esm(() => {
|
|
54944
|
+
init__utils();
|
|
54884
54945
|
init_types3();
|
|
54885
54946
|
deleteDocumentTool = {
|
|
54886
54947
|
name: "cerefox_delete_document",
|
|
@@ -54961,6 +55022,7 @@ async function handler3(supabase, args, ctx) {
|
|
|
54961
55022
|
}
|
|
54962
55023
|
var restoreDocumentTool;
|
|
54963
55024
|
var init_restore_document = __esm(() => {
|
|
55025
|
+
init__utils();
|
|
54964
55026
|
init_types3();
|
|
54965
55027
|
restoreDocumentTool = {
|
|
54966
55028
|
name: "cerefox_restore_document",
|
|
@@ -55150,6 +55212,7 @@ ${lines.join(`
|
|
|
55150
55212
|
}
|
|
55151
55213
|
var KNOWN_TYPES = "related_to, references, supersedes, contradicts, duplicates, part_of, follows, reply_to", setRelationTool, deleteRelationTool, getRelationsTool, getNeighborsTool;
|
|
55152
55214
|
var init_relations = __esm(() => {
|
|
55215
|
+
init__utils();
|
|
55153
55216
|
init_types3();
|
|
55154
55217
|
setRelationTool = {
|
|
55155
55218
|
name: "cerefox_set_relation",
|
|
@@ -55325,6 +55388,7 @@ ${hashLine}${row.full_content ?? ""}`;
|
|
|
55325
55388
|
var getDocumentTool;
|
|
55326
55389
|
var init_get_document = __esm(() => {
|
|
55327
55390
|
init_partial_edits();
|
|
55391
|
+
init__utils();
|
|
55328
55392
|
init_types3();
|
|
55329
55393
|
getDocumentTool = {
|
|
55330
55394
|
name: "cerefox_get_document",
|
|
@@ -55590,6 +55654,7 @@ var AUDIT_OP, insertTool, editTool;
|
|
|
55590
55654
|
var init_partial_edits2 = __esm(() => {
|
|
55591
55655
|
init_partial_edits();
|
|
55592
55656
|
init__chunker();
|
|
55657
|
+
init__utils();
|
|
55593
55658
|
init_types3();
|
|
55594
55659
|
AUDIT_OP = {
|
|
55595
55660
|
insert: "insert",
|
|
@@ -55718,7 +55783,7 @@ var init_partial_edits2 = __esm(() => {
|
|
|
55718
55783
|
});
|
|
55719
55784
|
|
|
55720
55785
|
// ../../_shared/mcp-tools/get-help-content.ts
|
|
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;
|
|
55786
|
+
var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **15 core MCP tools** (14 with CLI equivalents — `cerefox_get_help` is MCP-only), plus 4 dormant relation tools that appear only when `relations_enabled` is on. 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;
|
|
55722
55787
|
var init_get_help_content = __esm(() => {
|
|
55723
55788
|
HELP_SECTIONS = {
|
|
55724
55789
|
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.",
|
|
@@ -55903,6 +55968,7 @@ var getHelpTool;
|
|
|
55903
55968
|
var init_get_help = __esm(() => {
|
|
55904
55969
|
init_ef_meta();
|
|
55905
55970
|
init_partial_edits2();
|
|
55971
|
+
init__utils();
|
|
55906
55972
|
init_get_help_content();
|
|
55907
55973
|
getHelpTool = {
|
|
55908
55974
|
name: "cerefox_get_help",
|
|
@@ -56038,9 +56104,9 @@ async function handler6(supabase, args, ctx) {
|
|
|
56038
56104
|
result_count: chunks2.length
|
|
56039
56105
|
});
|
|
56040
56106
|
if (project_names !== null) {
|
|
56041
|
-
await setDocumentProjectsByName(supabase, existingDoc.id, project_names);
|
|
56107
|
+
await setDocumentProjectsByName(supabase, existingDoc.id, project_names, { author, authorType: author_type });
|
|
56042
56108
|
} else if (project_name) {
|
|
56043
|
-
await ensureDocumentInProject(supabase, existingDoc.id, project_name);
|
|
56109
|
+
await ensureDocumentInProject(supabase, existingDoc.id, project_name, { author, authorType: author_type });
|
|
56044
56110
|
}
|
|
56045
56111
|
const note = update_if_exists ? "" : " Note: update_if_exists flag was overridden by document_id.";
|
|
56046
56112
|
return `Document updated: "${title}" (id: ${existingDoc.id}), ${chunks2.length} chunk(s), ${totalChars2} chars. New content_hash: ${contentHash2}.${note}`;
|
|
@@ -56102,9 +56168,9 @@ async function handler6(supabase, args, ctx) {
|
|
|
56102
56168
|
result_count: chunks2.length
|
|
56103
56169
|
});
|
|
56104
56170
|
if (project_names !== null) {
|
|
56105
|
-
await setDocumentProjectsByName(supabase, existingDoc.id, project_names);
|
|
56171
|
+
await setDocumentProjectsByName(supabase, existingDoc.id, project_names, { author, authorType: author_type });
|
|
56106
56172
|
} else if (project_name) {
|
|
56107
|
-
await ensureDocumentInProject(supabase, existingDoc.id, project_name);
|
|
56173
|
+
await ensureDocumentInProject(supabase, existingDoc.id, project_name, { author, authorType: author_type });
|
|
56108
56174
|
}
|
|
56109
56175
|
return `Document updated: "${existingDoc.title}" (id: ${existingDoc.id}), ${chunks2.length} chunk(s), ${totalChars2} chars. New content_hash: ${contentHash2}.`;
|
|
56110
56176
|
}
|
|
@@ -56149,9 +56215,9 @@ async function handler6(supabase, args, ctx) {
|
|
|
56149
56215
|
}
|
|
56150
56216
|
const documentId = ingestResult[0].document_id;
|
|
56151
56217
|
if (project_names !== null && project_names.length > 0) {
|
|
56152
|
-
await setDocumentProjectsByName(supabase, documentId, project_names);
|
|
56218
|
+
await setDocumentProjectsByName(supabase, documentId, project_names, { author, authorType: author_type });
|
|
56153
56219
|
} else if (project_name) {
|
|
56154
|
-
await ensureDocumentInProject(supabase, documentId, project_name);
|
|
56220
|
+
await ensureDocumentInProject(supabase, documentId, project_name, { author, authorType: author_type });
|
|
56155
56221
|
}
|
|
56156
56222
|
logUsage(supabase, {
|
|
56157
56223
|
operation: "ingest",
|
|
@@ -56167,6 +56233,7 @@ var ingestTool;
|
|
|
56167
56233
|
var init_ingest = __esm(() => {
|
|
56168
56234
|
init__chunker();
|
|
56169
56235
|
init__projects();
|
|
56236
|
+
init__utils();
|
|
56170
56237
|
init_types3();
|
|
56171
56238
|
ingestTool = {
|
|
56172
56239
|
name: "cerefox_ingest",
|
|
@@ -56239,6 +56306,7 @@ async function handler7(supabase, args, ctx) {
|
|
|
56239
56306
|
}
|
|
56240
56307
|
var listMetadataKeysTool;
|
|
56241
56308
|
var init_list_metadata_keys = __esm(() => {
|
|
56309
|
+
init__utils();
|
|
56242
56310
|
listMetadataKeysTool = {
|
|
56243
56311
|
name: "cerefox_list_metadata_keys",
|
|
56244
56312
|
description: "List all metadata keys currently in use across documents in the Cerefox knowledge base. Returns each key with its document count and up to 5 example values.",
|
|
@@ -56286,6 +56354,7 @@ ${lines.join(`
|
|
|
56286
56354
|
}
|
|
56287
56355
|
var listProjectsTool;
|
|
56288
56356
|
var init_list_projects = __esm(() => {
|
|
56357
|
+
init__utils();
|
|
56289
56358
|
listProjectsTool = {
|
|
56290
56359
|
name: "cerefox_list_projects",
|
|
56291
56360
|
description: "List all projects with their names and IDs. Use this to discover available projects before filtering by project_name in other tools.",
|
|
@@ -56340,6 +56409,7 @@ ${lines.join(`
|
|
|
56340
56409
|
}
|
|
56341
56410
|
var listVersionsTool;
|
|
56342
56411
|
var init_list_versions = __esm(() => {
|
|
56412
|
+
init__utils();
|
|
56343
56413
|
init_types3();
|
|
56344
56414
|
listVersionsTool = {
|
|
56345
56415
|
name: "cerefox_list_versions",
|
|
@@ -56438,6 +56508,7 @@ ${row.content}`;
|
|
|
56438
56508
|
}
|
|
56439
56509
|
var metadataSearchTool;
|
|
56440
56510
|
var init_metadata_search = __esm(() => {
|
|
56511
|
+
init__utils();
|
|
56441
56512
|
init__projects();
|
|
56442
56513
|
init_types3();
|
|
56443
56514
|
metadataSearchTool = {
|
|
@@ -56604,6 +56675,7 @@ ${row.full_content ?? ""}`;
|
|
|
56604
56675
|
}
|
|
56605
56676
|
var searchTool;
|
|
56606
56677
|
var init_search = __esm(() => {
|
|
56678
|
+
init__utils();
|
|
56607
56679
|
init__projects();
|
|
56608
56680
|
init_types3();
|
|
56609
56681
|
searchTool = {
|
|
@@ -56710,6 +56782,7 @@ async function handler12(supabase, args, ctx) {
|
|
|
56710
56782
|
}
|
|
56711
56783
|
var setDocumentMetadataTool;
|
|
56712
56784
|
var init_set_document_metadata = __esm(() => {
|
|
56785
|
+
init__utils();
|
|
56713
56786
|
init_types3();
|
|
56714
56787
|
setDocumentMetadataTool = {
|
|
56715
56788
|
name: "cerefox_set_document_metadata",
|
|
@@ -71370,19 +71443,32 @@ function registerConfigList(parent) {
|
|
|
71370
71443
|
|
|
71371
71444
|
// src/cli/commands/config-set.ts
|
|
71372
71445
|
init_cli_core();
|
|
71446
|
+
init__utils();
|
|
71373
71447
|
init_client();
|
|
71374
|
-
async function action5(key, value) {
|
|
71448
|
+
async function action5(key, value, options) {
|
|
71375
71449
|
const client = getClient();
|
|
71376
|
-
|
|
71377
|
-
|
|
71378
|
-
|
|
71379
|
-
|
|
71450
|
+
const author = resolveAuthor(options.author);
|
|
71451
|
+
const authorType = resolveAuthorType(options.authorType);
|
|
71452
|
+
if (author === "unknown") {
|
|
71453
|
+
warn("No --author / CEREFOX_AUTHOR_NAME set — audit log will record this change as 'unknown'.");
|
|
71454
|
+
}
|
|
71455
|
+
const { error } = await client.raw.rpc("cerefox_set_config", {
|
|
71456
|
+
p_key: key,
|
|
71457
|
+
p_value: value,
|
|
71458
|
+
p_author: author,
|
|
71459
|
+
p_author_type: authorType
|
|
71460
|
+
});
|
|
71461
|
+
if (error) {
|
|
71462
|
+
const msg = error.message ?? String(error);
|
|
71463
|
+
const remediation = storeWriteRemediation(msg, "cerefox_set_config");
|
|
71464
|
+
if (remediation)
|
|
71465
|
+
throw systemError(`Could not set ${key}.`, remediation);
|
|
71380
71466
|
throw systemError(`Could not set ${key}: ${msg}`, "The RPC validates against an allowlist of known keys; check the key spelling.");
|
|
71381
71467
|
}
|
|
71382
71468
|
println(c.green("✓ ") + `${key} = ${value}`);
|
|
71383
71469
|
}
|
|
71384
71470
|
function registerConfigSet(program2) {
|
|
71385
|
-
program2.command("config-set").description("Write a runtime config value into the cerefox_config table.").argument("<key>", "Config key.").argument("<value>", "Value to write.").action(action5);
|
|
71471
|
+
program2.command("config-set").description("Write a runtime config value into the cerefox_config table.").argument("<key>", "Config key.").argument("<value>", "Value to write.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "user | agent (default: user).").action(action5);
|
|
71386
71472
|
}
|
|
71387
71473
|
|
|
71388
71474
|
// src/cli/commands/configure-agent.ts
|
|
@@ -72730,6 +72816,7 @@ function registerConfigureAgent(program2) {
|
|
|
72730
72816
|
|
|
72731
72817
|
// src/cli/commands/delete-doc.ts
|
|
72732
72818
|
init_cli_core();
|
|
72819
|
+
init__utils();
|
|
72733
72820
|
init_client();
|
|
72734
72821
|
async function action7(documentId, options) {
|
|
72735
72822
|
const client = getClient();
|
|
@@ -72799,6 +72886,7 @@ function registerDeleteDoc(program2) {
|
|
|
72799
72886
|
|
|
72800
72887
|
// src/cli/commands/document-dead-links.ts
|
|
72801
72888
|
init_cli_core();
|
|
72889
|
+
init__utils();
|
|
72802
72890
|
init_client();
|
|
72803
72891
|
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
72892
|
async function action8(options) {
|
|
@@ -72840,6 +72928,8 @@ init_client();
|
|
|
72840
72928
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
72841
72929
|
async function action9(target, options) {
|
|
72842
72930
|
const client = getClient();
|
|
72931
|
+
const author = resolveAuthor(options.author);
|
|
72932
|
+
const authorType = resolveAuthorType(options.authorType);
|
|
72843
72933
|
const isUuid = UUID_RE.test(target);
|
|
72844
72934
|
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();
|
|
72845
72935
|
const { data: project, error } = await lookup;
|
|
@@ -72867,14 +72957,22 @@ async function action9(target, options) {
|
|
|
72867
72957
|
return;
|
|
72868
72958
|
}
|
|
72869
72959
|
}
|
|
72870
|
-
const { error: delErr } = await client.raw.
|
|
72960
|
+
const { data: delData, error: delErr } = await client.raw.rpc("cerefox_delete_project", {
|
|
72961
|
+
p_project_id: project.id,
|
|
72962
|
+
p_author: author,
|
|
72963
|
+
p_author_type: authorType
|
|
72964
|
+
});
|
|
72871
72965
|
if (delErr) {
|
|
72872
72966
|
throw systemError(`Delete failed: ${delErr.message}`);
|
|
72873
72967
|
}
|
|
72968
|
+
const delRow = delData?.[0];
|
|
72969
|
+
if (!delRow?.deleted) {
|
|
72970
|
+
throw notFound(`Project "${project.name}" was already deleted (concurrently).`);
|
|
72971
|
+
}
|
|
72874
72972
|
println(c.green(`✓ Deleted project "${project.name}" (id: ${project.id}).`));
|
|
72875
72973
|
}
|
|
72876
72974
|
function registerDeleteProject(program2) {
|
|
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);
|
|
72975
|
+
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.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "user | agent (default: user).").action(action9);
|
|
72878
72976
|
}
|
|
72879
72977
|
|
|
72880
72978
|
// src/cli/commands/deploy-server.ts
|
|
@@ -72918,7 +73016,7 @@ function buildDeploySteps(assets, opts = {}) {
|
|
|
72918
73016
|
}
|
|
72919
73017
|
steps.push({ label: "Enable extensions (uuid-ossp, vector/pgvector)", sql: EXTENSIONS_SQL }, { label: "Apply schema (tables, indexes, triggers)", sql: schemaSql }, { label: "Apply RPCs (search functions)", sql: rpcsSql });
|
|
72920
73018
|
const migrationFiles = listMigrationFiles(assets.migrationsDir);
|
|
72921
|
-
if (migrationFiles.length > 0) {
|
|
73019
|
+
if ((opts.stampMigrations ?? true) && migrationFiles.length > 0) {
|
|
72922
73020
|
const values2 = migrationFiles.map((n) => `('${n.replace(/'/g, "''")}')`).join(", ");
|
|
72923
73021
|
steps.push({
|
|
72924
73022
|
label: "Stamp migration files as already applied",
|
|
@@ -72929,14 +73027,20 @@ function buildDeploySteps(assets, opts = {}) {
|
|
|
72929
73027
|
}
|
|
72930
73028
|
async function runDbDeploy(opts) {
|
|
72931
73029
|
const log = opts.log ?? (() => {});
|
|
72932
|
-
const steps = buildDeploySteps(opts.assets, { reset: opts.reset });
|
|
72933
73030
|
if (opts.dryRun) {
|
|
72934
|
-
|
|
73031
|
+
const steps2 = buildDeploySteps(opts.assets, { reset: opts.reset });
|
|
73032
|
+
for (const step of steps2) {
|
|
72935
73033
|
log(`▶ ${step.label}… (dry-run, not executed)`);
|
|
72936
73034
|
}
|
|
72937
|
-
return { ok: true, stepsRun:
|
|
73035
|
+
return { ok: true, stepsRun: steps2.length };
|
|
72938
73036
|
}
|
|
72939
73037
|
const sql = src_default(opts.dbUrl, { prepare: false, onnotice: () => {} });
|
|
73038
|
+
const fresh = opts.reset ? true : !await detectExistingSchema(opts.dbUrl, sql);
|
|
73039
|
+
const steps = buildDeploySteps(opts.assets, { reset: opts.reset, stampMigrations: fresh });
|
|
73040
|
+
if (!fresh) {
|
|
73041
|
+
log("⚠ Existing schema detected: pending migrations are NOT stamped by a re-apply.");
|
|
73042
|
+
log(" Run `bun scripts/db_migrate.ts` (or `cerefox server deploy`) to apply them.");
|
|
73043
|
+
}
|
|
72940
73044
|
let stepsRun = 0;
|
|
72941
73045
|
try {
|
|
72942
73046
|
for (const step of steps) {
|
|
@@ -72949,11 +73053,17 @@ async function runDbDeploy(opts) {
|
|
|
72949
73053
|
return { ok: false, stepsRun, failedStep: step.label, error: message };
|
|
72950
73054
|
}
|
|
72951
73055
|
}
|
|
73056
|
+
await reloadPostgrestSchemaCache(sql);
|
|
72952
73057
|
return { ok: true, stepsRun };
|
|
72953
73058
|
} finally {
|
|
72954
73059
|
await sql.end({ timeout: 5 }).catch(() => {});
|
|
72955
73060
|
}
|
|
72956
73061
|
}
|
|
73062
|
+
async function reloadPostgrestSchemaCache(sql) {
|
|
73063
|
+
try {
|
|
73064
|
+
await sql.unsafe("NOTIFY pgrst, 'reload schema'");
|
|
73065
|
+
} catch {}
|
|
73066
|
+
}
|
|
72957
73067
|
var BOOTSTRAP_MIGRATIONS_SQL = `
|
|
72958
73068
|
CREATE TABLE IF NOT EXISTS cerefox_migrations (
|
|
72959
73069
|
id SERIAL PRIMARY KEY,
|
|
@@ -72961,13 +73071,14 @@ CREATE TABLE IF NOT EXISTS cerefox_migrations (
|
|
|
72961
73071
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
72962
73072
|
);
|
|
72963
73073
|
`;
|
|
72964
|
-
async function detectExistingSchema(dbUrl) {
|
|
72965
|
-
const sql = src_default(dbUrl, { prepare: false, onnotice: () => {} });
|
|
73074
|
+
async function detectExistingSchema(dbUrl, existing) {
|
|
73075
|
+
const sql = existing ?? src_default(dbUrl, { prepare: false, onnotice: () => {} });
|
|
72966
73076
|
try {
|
|
72967
73077
|
const rows = await sql`SELECT to_regclass('public.cerefox_documents') AS t`;
|
|
72968
73078
|
return rows[0]?.t != null;
|
|
72969
73079
|
} finally {
|
|
72970
|
-
|
|
73080
|
+
if (!existing)
|
|
73081
|
+
await sql.end({ timeout: 5 }).catch(() => {});
|
|
72971
73082
|
}
|
|
72972
73083
|
}
|
|
72973
73084
|
async function migrationStatus(opts) {
|
|
@@ -72988,7 +73099,13 @@ async function migrationStatus(opts) {
|
|
|
72988
73099
|
}
|
|
72989
73100
|
async function runDbMigrate(opts) {
|
|
72990
73101
|
const log = opts.log ?? (() => {});
|
|
72991
|
-
const sql = src_default(opts.dbUrl, {
|
|
73102
|
+
const sql = src_default(opts.dbUrl, {
|
|
73103
|
+
prepare: false,
|
|
73104
|
+
onnotice: (n) => {
|
|
73105
|
+
if (n.message)
|
|
73106
|
+
log(` ↳ ${n.message}`);
|
|
73107
|
+
}
|
|
73108
|
+
});
|
|
72992
73109
|
try {
|
|
72993
73110
|
await sql.unsafe(BOOTSTRAP_MIGRATIONS_SQL);
|
|
72994
73111
|
const allFiles = listMigrationFiles(opts.assets.migrationsDir);
|
|
@@ -73022,6 +73139,8 @@ async function runDbMigrate(opts) {
|
|
|
73022
73139
|
};
|
|
73023
73140
|
}
|
|
73024
73141
|
}
|
|
73142
|
+
if (applied.length > 0)
|
|
73143
|
+
await reloadPostgrestSchemaCache(sql);
|
|
73025
73144
|
return { ok: true, applied, pending };
|
|
73026
73145
|
} finally {
|
|
73027
73146
|
await sql.end({ timeout: 5 }).catch(() => {});
|
|
@@ -73036,6 +73155,7 @@ async function applyRpcs(opts) {
|
|
|
73036
73155
|
const sql = src_default(opts.dbUrl, { prepare: false, onnotice: () => {} });
|
|
73037
73156
|
try {
|
|
73038
73157
|
await sql.unsafe(rpcsSql);
|
|
73158
|
+
await reloadPostgrestSchemaCache(sql);
|
|
73039
73159
|
return { ok: true };
|
|
73040
73160
|
} catch (err) {
|
|
73041
73161
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -73399,6 +73519,7 @@ function registerDocumentEdit(parent) {
|
|
|
73399
73519
|
|
|
73400
73520
|
// src/cli/commands/document-restore.ts
|
|
73401
73521
|
init_cli_core();
|
|
73522
|
+
init__utils();
|
|
73402
73523
|
init_client();
|
|
73403
73524
|
async function action12(documentId, options) {
|
|
73404
73525
|
const client = getClient();
|
|
@@ -73618,20 +73739,38 @@ function registerGuides(parent) {
|
|
|
73618
73739
|
|
|
73619
73740
|
// src/cli/commands/project-create.ts
|
|
73620
73741
|
init_cli_core();
|
|
73742
|
+
init__utils();
|
|
73621
73743
|
init_client();
|
|
73622
73744
|
async function action15(name, options) {
|
|
73623
73745
|
const trimmed = name.trim();
|
|
73624
73746
|
if (!trimmed)
|
|
73625
73747
|
throw userError("Project name is required.");
|
|
73626
73748
|
const client = getClient();
|
|
73627
|
-
const
|
|
73628
|
-
|
|
73629
|
-
|
|
73749
|
+
const author = resolveAuthor(options.author);
|
|
73750
|
+
const authorType = resolveAuthorType(options.authorType);
|
|
73751
|
+
const { data, error } = await client.raw.rpc("cerefox_create_project", {
|
|
73752
|
+
p_name: trimmed,
|
|
73753
|
+
p_description: (options.description ?? "").trim(),
|
|
73754
|
+
p_author: author,
|
|
73755
|
+
p_author_type: authorType
|
|
73756
|
+
});
|
|
73757
|
+
if (error) {
|
|
73758
|
+
const msg = error.message ?? "";
|
|
73759
|
+
if (isDuplicateKeyError(msg)) {
|
|
73760
|
+
throw userError(`Project "${trimmed}" already exists.`);
|
|
73761
|
+
}
|
|
73762
|
+
const remediation = storeWriteRemediation(msg, "cerefox_create_project");
|
|
73763
|
+
if (remediation)
|
|
73764
|
+
throw systemError("Create failed.", remediation);
|
|
73765
|
+
throw systemError(`Create failed: ${msg}`);
|
|
73630
73766
|
}
|
|
73631
|
-
|
|
73767
|
+
const row = data?.[0];
|
|
73768
|
+
if (!row)
|
|
73769
|
+
throw systemError("Create failed: no row returned");
|
|
73770
|
+
println(c.green(`✓ Created project "${row.project_name}" (id: ${row.project_id}).`));
|
|
73632
73771
|
}
|
|
73633
73772
|
function registerProjectCreate(parent) {
|
|
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);
|
|
73773
|
+
parent.command("create").description("Create a new (empty) project.").argument("<name>", "Project name (must be unique).").option("--description <text>", "Optional project description.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "user | agent (default: user).").action(action15);
|
|
73635
73774
|
}
|
|
73636
73775
|
|
|
73637
73776
|
// src/cli/commands/project-edit.ts
|
|
@@ -73652,20 +73791,30 @@ async function action16(target, options) {
|
|
|
73652
73791
|
throw userError("Nothing to update — pass --name and/or --description.");
|
|
73653
73792
|
}
|
|
73654
73793
|
const client = getClient();
|
|
73794
|
+
const author = resolveAuthor(options.author);
|
|
73795
|
+
const authorType = resolveAuthorType(options.authorType);
|
|
73655
73796
|
const isUuid = UUID_RE2.test(target);
|
|
73656
73797
|
const { data: project, error: lookupErr } = await client.raw.from("cerefox_projects").select("id, name").eq(isUuid ? "id" : "name", target).maybeSingle();
|
|
73657
73798
|
if (lookupErr)
|
|
73658
73799
|
throw systemError(`Lookup failed: ${lookupErr.message}`);
|
|
73659
73800
|
if (!project)
|
|
73660
73801
|
throw notFound(`Project "${target}" not found.`);
|
|
73661
|
-
const { data, error } = await client.raw.
|
|
73662
|
-
|
|
73663
|
-
|
|
73664
|
-
|
|
73665
|
-
|
|
73802
|
+
const { data, error } = await client.raw.rpc("cerefox_update_project", {
|
|
73803
|
+
p_project_id: project.id,
|
|
73804
|
+
p_name: update.name ?? null,
|
|
73805
|
+
p_description: update.description ?? null,
|
|
73806
|
+
p_author: author,
|
|
73807
|
+
p_author_type: authorType
|
|
73808
|
+
});
|
|
73809
|
+
if (error)
|
|
73810
|
+
throw systemError(`Update failed: ${error.message}`);
|
|
73811
|
+
const row = data?.[0];
|
|
73812
|
+
if (!row)
|
|
73813
|
+
throw systemError("Update failed: no row returned");
|
|
73814
|
+
println(c.green(`✓ Updated project "${row.project_name}" (id: ${row.project_id}).`));
|
|
73666
73815
|
}
|
|
73667
73816
|
function registerProjectEdit(parent) {
|
|
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);
|
|
73817
|
+
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.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "user | agent (default: user).").action(action16);
|
|
73669
73818
|
}
|
|
73670
73819
|
|
|
73671
73820
|
// src/cli/commands/version-archive.ts
|
|
@@ -77860,6 +78009,10 @@ async function action17(options) {
|
|
|
77860
78009
|
}
|
|
77861
78010
|
if (remediation) {
|
|
77862
78011
|
println(cErr.yellow("→ " + remediation));
|
|
78012
|
+
const configDir = (process.env.CEREFOX_CONFIG_DIR ?? "").trim();
|
|
78013
|
+
if (configDir) {
|
|
78014
|
+
println(cErr.dim(` (this doctor ran against CEREFOX_CONFIG_DIR=${configDir} — ` + `prefix the command above the same way, or use your environment alias, ` + `or a bare \`cerefox\` will act on your DEFAULT environment instead)`));
|
|
78015
|
+
}
|
|
77863
78016
|
println("");
|
|
77864
78017
|
}
|
|
77865
78018
|
}
|
|
@@ -77886,6 +78039,7 @@ function registerDoctor(program2) {
|
|
|
77886
78039
|
}
|
|
77887
78040
|
|
|
77888
78041
|
// src/cli/commands/get-audit-log.ts
|
|
78042
|
+
init__utils();
|
|
77889
78043
|
init_cli_core();
|
|
77890
78044
|
init_client();
|
|
77891
78045
|
async function action18(options) {
|
|
@@ -77920,13 +78074,13 @@ async function action18(options) {
|
|
|
77920
78074
|
printTable(data.map((row) => ({
|
|
77921
78075
|
when: `${(row.created_at ?? "").slice(0, 19).replace("T", " ")}Z`,
|
|
77922
78076
|
operation: row.operation,
|
|
77923
|
-
doc: (row.doc_title
|
|
78077
|
+
doc: auditDocLabel(row.doc_title, row.document_id, row.operation).slice(0, 40),
|
|
77924
78078
|
author: (row.author ?? "") + (row.author_type ? `(${row.author_type})` : ""),
|
|
77925
78079
|
size_delta: row.size_before !== null && row.size_after !== null ? `${row.size_before} → ${row.size_after}` : ""
|
|
77926
78080
|
})));
|
|
77927
78081
|
}
|
|
77928
78082
|
function registerGetAuditLog(program2) {
|
|
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>",
|
|
78083
|
+
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: ${AUDIT_OPERATIONS.join(", ")}.`).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);
|
|
77930
78084
|
}
|
|
77931
78085
|
|
|
77932
78086
|
// src/cli/commands/relation.ts
|
|
@@ -78215,6 +78369,10 @@ async function resolveProjectIds(input, getOrCreateProject) {
|
|
|
78215
78369
|
}
|
|
78216
78370
|
return [];
|
|
78217
78371
|
}
|
|
78372
|
+
// src/ingestion/client-bridge.ts
|
|
78373
|
+
init__projects();
|
|
78374
|
+
init__utils();
|
|
78375
|
+
|
|
78218
78376
|
// src/ingestion/types.ts
|
|
78219
78377
|
class ConcurrencyConflictError extends Error {
|
|
78220
78378
|
documentId;
|
|
@@ -78384,19 +78542,15 @@ class IngestionDbBridge {
|
|
|
78384
78542
|
if (error2)
|
|
78385
78543
|
throw new Error(error2.message ?? JSON.stringify(error2));
|
|
78386
78544
|
}
|
|
78387
|
-
async getOrCreateProject(name) {
|
|
78388
|
-
const
|
|
78389
|
-
if (
|
|
78390
|
-
|
|
78391
|
-
|
|
78392
|
-
if (error2 || !data) {
|
|
78393
|
-
throw error2 ?? new Error(`getOrCreateProject(${name}) returned no data`);
|
|
78394
|
-
}
|
|
78395
|
-
return data;
|
|
78545
|
+
async getOrCreateProject(name, audit) {
|
|
78546
|
+
const resolved = await resolveOrCreateProject(this.supabase, name, audit);
|
|
78547
|
+
if (!resolved)
|
|
78548
|
+
throw new Error(`getOrCreateProject(${name}) returned no data`);
|
|
78549
|
+
return { id: resolved.projectId, name: resolved.projectName, description: null };
|
|
78396
78550
|
}
|
|
78397
78551
|
async createAuditEntry(args) {
|
|
78398
78552
|
try {
|
|
78399
|
-
await this.supabase.rpc("cerefox_create_audit_entry", {
|
|
78553
|
+
const { error: error2 } = await this.supabase.rpc("cerefox_create_audit_entry", {
|
|
78400
78554
|
p_document_id: args.documentId ?? null,
|
|
78401
78555
|
p_version_id: args.versionId ?? null,
|
|
78402
78556
|
p_operation: args.operation,
|
|
@@ -78406,7 +78560,11 @@ class IngestionDbBridge {
|
|
|
78406
78560
|
p_size_after: args.sizeAfter ?? null,
|
|
78407
78561
|
p_description: args.description ?? ""
|
|
78408
78562
|
});
|
|
78409
|
-
|
|
78563
|
+
if (error2)
|
|
78564
|
+
console.warn("createAuditEntry failed:", error2.message ?? error2);
|
|
78565
|
+
} catch (err) {
|
|
78566
|
+
console.warn("createAuditEntry failed:", err);
|
|
78567
|
+
}
|
|
78410
78568
|
}
|
|
78411
78569
|
}
|
|
78412
78570
|
|
|
@@ -78469,7 +78627,7 @@ class IngestionPipeline {
|
|
|
78469
78627
|
forceRechunk = false
|
|
78470
78628
|
} = opts;
|
|
78471
78629
|
const listFormProvided = projectIds !== undefined && projectIds !== null || projectNames !== undefined && projectNames !== null;
|
|
78472
|
-
const getOrCreate = (name) => this.db.getOrCreateProject(name);
|
|
78630
|
+
const getOrCreate = (name) => this.db.getOrCreateProject(name, { author, authorType });
|
|
78473
78631
|
if (documentId) {
|
|
78474
78632
|
const existing = await this.db.getDocumentById(documentId);
|
|
78475
78633
|
if (!existing) {
|
|
@@ -80226,6 +80384,7 @@ function registerRestore(program2) {
|
|
|
80226
80384
|
|
|
80227
80385
|
// src/cli/commands/search.ts
|
|
80228
80386
|
init_cli_core();
|
|
80387
|
+
init__utils();
|
|
80229
80388
|
init_client();
|
|
80230
80389
|
|
|
80231
80390
|
// src/cli/util/embed.ts
|
|
@@ -84289,6 +84448,7 @@ function validateConfigValue(key, value) {
|
|
|
84289
84448
|
|
|
84290
84449
|
// src/web/routes/config.ts
|
|
84291
84450
|
init_config();
|
|
84451
|
+
init__utils();
|
|
84292
84452
|
function unwrapScalarRpc(data) {
|
|
84293
84453
|
if (typeof data === "string")
|
|
84294
84454
|
return data;
|
|
@@ -84361,13 +84521,22 @@ function registerConfigRoutes(app, ctx) {
|
|
|
84361
84521
|
return c2.json({ detail: invalid }, 400);
|
|
84362
84522
|
const { error: error3 } = await ctx.supabase.rpc("cerefox_set_config", {
|
|
84363
84523
|
p_key: key,
|
|
84364
|
-
p_value: value
|
|
84524
|
+
p_value: value,
|
|
84525
|
+
p_author: "web-ui",
|
|
84526
|
+
p_author_type: "user"
|
|
84365
84527
|
});
|
|
84366
|
-
if (error3)
|
|
84528
|
+
if (error3) {
|
|
84529
|
+
const remediation = storeWriteRemediation(error3.message ?? "", "cerefox_set_config");
|
|
84530
|
+
if (remediation)
|
|
84531
|
+
return c2.json({ detail: remediation }, 503);
|
|
84367
84532
|
return c2.json({ detail: error3.message }, 500);
|
|
84533
|
+
}
|
|
84368
84534
|
return c2.json({ key, value });
|
|
84369
84535
|
});
|
|
84370
84536
|
}
|
|
84537
|
+
// src/web/routes/discovery.ts
|
|
84538
|
+
init__utils();
|
|
84539
|
+
|
|
84371
84540
|
// src/web/usage.ts
|
|
84372
84541
|
function logWebUsage(ctx, params) {
|
|
84373
84542
|
Promise.resolve(ctx.supabase.rpc("cerefox_log_usage", {
|
|
@@ -85132,6 +85301,7 @@ function registerDocumentReadRoutes(app, ctx) {
|
|
|
85132
85301
|
});
|
|
85133
85302
|
}
|
|
85134
85303
|
// src/web/routes/documents-write.ts
|
|
85304
|
+
init__utils();
|
|
85135
85305
|
async function createAuditEntry(ctx, args) {
|
|
85136
85306
|
try {
|
|
85137
85307
|
await ctx.supabase.rpc("cerefox_create_audit_entry", {
|
|
@@ -85839,6 +86009,16 @@ function registerPreferencesRoutes(app) {
|
|
|
85839
86009
|
}
|
|
85840
86010
|
|
|
85841
86011
|
// src/web/routes/projects.ts
|
|
86012
|
+
init__utils();
|
|
86013
|
+
function rpcRowToResponse(row) {
|
|
86014
|
+
return {
|
|
86015
|
+
id: row.project_id,
|
|
86016
|
+
name: row.project_name,
|
|
86017
|
+
description: row.project_description ?? null,
|
|
86018
|
+
created_at: row.created_at ?? "",
|
|
86019
|
+
updated_at: row.updated_at ?? ""
|
|
86020
|
+
};
|
|
86021
|
+
}
|
|
85842
86022
|
function projectRowToResponse(row) {
|
|
85843
86023
|
return {
|
|
85844
86024
|
id: row.id,
|
|
@@ -85867,11 +86047,25 @@ function registerProjectsRoutes(app, ctx) {
|
|
|
85867
86047
|
if (!name)
|
|
85868
86048
|
return c2.json({ detail: "Project name is required" }, 400);
|
|
85869
86049
|
const description = String(body.description ?? "").trim();
|
|
85870
|
-
const { data, error: error3 } = await ctx.supabase.
|
|
85871
|
-
|
|
85872
|
-
|
|
85873
|
-
|
|
85874
|
-
|
|
86050
|
+
const { data, error: error3 } = await ctx.supabase.rpc("cerefox_create_project", {
|
|
86051
|
+
p_name: name,
|
|
86052
|
+
p_description: description,
|
|
86053
|
+
p_author: "web-ui",
|
|
86054
|
+
p_author_type: "user"
|
|
86055
|
+
});
|
|
86056
|
+
if (error3) {
|
|
86057
|
+
const msg = error3.message ?? "";
|
|
86058
|
+
if (isDuplicateKeyError(msg))
|
|
86059
|
+
return c2.json({ detail: msg }, 409);
|
|
86060
|
+
const remediation = storeWriteRemediation(msg, "cerefox_create_project");
|
|
86061
|
+
if (remediation)
|
|
86062
|
+
return c2.json({ detail: remediation }, 503);
|
|
86063
|
+
return c2.json({ detail: msg }, 500);
|
|
86064
|
+
}
|
|
86065
|
+
const row = data?.[0];
|
|
86066
|
+
if (!row)
|
|
86067
|
+
return c2.json({ detail: "create_project returned no data" }, 500);
|
|
86068
|
+
return c2.json(rpcRowToResponse(row));
|
|
85875
86069
|
});
|
|
85876
86070
|
app.put("/api/v1/projects/:project_id", async (c2) => {
|
|
85877
86071
|
const projectId = c2.req.param("project_id");
|
|
@@ -85881,19 +86075,51 @@ function registerProjectsRoutes(app, ctx) {
|
|
|
85881
86075
|
} catch {
|
|
85882
86076
|
return c2.json({ detail: "Invalid JSON body" }, 400);
|
|
85883
86077
|
}
|
|
85884
|
-
const
|
|
85885
|
-
|
|
85886
|
-
|
|
85887
|
-
|
|
85888
|
-
|
|
86078
|
+
const update = {};
|
|
86079
|
+
if (body.name != null) {
|
|
86080
|
+
const name = String(body.name).trim();
|
|
86081
|
+
if (!name)
|
|
86082
|
+
return c2.json({ detail: "Project name cannot be empty" }, 400);
|
|
86083
|
+
update.name = name;
|
|
85889
86084
|
}
|
|
85890
|
-
|
|
86085
|
+
if (body.description != null)
|
|
86086
|
+
update.description = String(body.description).trim();
|
|
86087
|
+
if (Object.keys(update).length === 0) {
|
|
86088
|
+
return c2.json({ detail: "Nothing to update — pass name and/or description" }, 400);
|
|
86089
|
+
}
|
|
86090
|
+
const { data, error: error3 } = await ctx.supabase.rpc("cerefox_update_project", {
|
|
86091
|
+
p_project_id: projectId,
|
|
86092
|
+
p_name: update.name ?? null,
|
|
86093
|
+
p_description: update.description ?? null,
|
|
86094
|
+
p_author: "web-ui",
|
|
86095
|
+
p_author_type: "user"
|
|
86096
|
+
});
|
|
86097
|
+
if (error3) {
|
|
86098
|
+
const msg = error3.message ?? "";
|
|
86099
|
+
if (/not found/i.test(msg))
|
|
86100
|
+
return c2.json({ detail: msg }, 404);
|
|
86101
|
+
const remediation = storeWriteRemediation(msg, "cerefox_update_project");
|
|
86102
|
+
if (remediation)
|
|
86103
|
+
return c2.json({ detail: remediation }, 503);
|
|
86104
|
+
return c2.json({ detail: msg }, 500);
|
|
86105
|
+
}
|
|
86106
|
+
const row = data?.[0];
|
|
86107
|
+
if (!row)
|
|
86108
|
+
return c2.json({ detail: "update_project returned no data" }, 500);
|
|
86109
|
+
return c2.json(rpcRowToResponse(row));
|
|
85891
86110
|
});
|
|
85892
86111
|
app.delete("/api/v1/projects/:project_id", async (c2) => {
|
|
85893
86112
|
const projectId = c2.req.param("project_id");
|
|
85894
|
-
const { error: error3 } = await ctx.supabase.
|
|
86113
|
+
const { data, error: error3 } = await ctx.supabase.rpc("cerefox_delete_project", {
|
|
86114
|
+
p_project_id: projectId,
|
|
86115
|
+
p_author: "web-ui",
|
|
86116
|
+
p_author_type: "user"
|
|
86117
|
+
});
|
|
85895
86118
|
if (error3)
|
|
85896
86119
|
return c2.json({ detail: error3.message }, 500);
|
|
86120
|
+
const row = data?.[0];
|
|
86121
|
+
if (!row?.deleted)
|
|
86122
|
+
return c2.json({ detail: "Project not found" }, 404);
|
|
85897
86123
|
return c2.json({ success: true });
|
|
85898
86124
|
});
|
|
85899
86125
|
}
|