@cerefox/memory 1.6.1 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/AGENT_GUIDE.md +80 -6
  2. package/AGENT_QUICK_REFERENCE.md +9 -7
  3. package/README.md +5 -3
  4. package/dist/bin/cerefox.js +790 -319
  5. package/dist/frontend/assets/index-D8E0mTnp.js +121 -0
  6. package/dist/frontend/assets/index-D8E0mTnp.js.map +1 -0
  7. package/dist/frontend/index.html +1 -1
  8. package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
  9. package/dist/server-assets/_shared/mcp-tools/_utils.ts +41 -0
  10. package/dist/server-assets/_shared/mcp-tools/delete-document.ts +181 -0
  11. package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +4 -4
  12. package/dist/server-assets/_shared/mcp-tools/index.ts +8 -1
  13. package/dist/server-assets/_shared/mcp-tools/ingest.ts +75 -8
  14. package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +43 -4
  15. package/dist/server-assets/_shared/mcp-tools/restore-document.ts +130 -0
  16. package/dist/server-assets/db/migrations/0024_mcp_delete_document.sql +26 -0
  17. package/dist/server-assets/db/migrations/0025_drop_orphaned_overloads.sql +15 -0
  18. package/dist/server-assets/db/migrations/0026_metadata_guard_and_dead_links.sql +46 -0
  19. package/dist/server-assets/db/rpcs.sql +355 -27
  20. package/dist/server-assets/db/schema.sql +10 -2
  21. package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +26 -0
  22. package/docs/guides/access-paths.md +24 -15
  23. package/docs/guides/cli.md +34 -7
  24. package/docs/guides/connect-agents.md +41 -17
  25. package/docs/guides/operational-cost.md +1 -1
  26. package/package.json +1 -1
  27. package/dist/frontend/assets/index-OqloGFwv.js +0 -121
  28. package/dist/frontend/assets/index-OqloGFwv.js.map +0 -1
@@ -35,7 +35,7 @@ import {
35
35
  sha256hex,
36
36
  } from "./_chunker.ts";
37
37
  import { activeEmbedderName, embedBatch, resolveEmbedderKind } from "../embeddings/index.ts";
38
- import { logUsage } from "./_utils.ts";
38
+ import { extractConflictHashes, isMissingFunctionError, logUsage } from "./_utils.ts";
39
39
  import { McpInvalidParams, type MCPSupabaseClient, type ToolContext, type ToolDefinition } from "./types.ts";
40
40
 
41
41
  /**
@@ -175,6 +175,28 @@ async function readDocument(
175
175
  supabase: MCPSupabaseClient,
176
176
  documentId: string,
177
177
  ): Promise<{ title: string; content: string; hash: string }> {
178
+ // Trashed check BEFORE anything else: a partial edit of a trashed document
179
+ // would otherwise pay for embeddings and then hit the RPC's CEREFOX_DELETED
180
+ // refusal as an unshaped error, on every retry (round-4 review). ADVISORY
181
+ // only — the RPC guard is authoritative — so a client without the .from()
182
+ // surface (test doubles) skips it rather than failing.
183
+ try {
184
+ const { data: lifecycle } = await supabase
185
+ .from("cerefox_documents")
186
+ .select("deleted_at")
187
+ .eq("id", documentId)
188
+ .maybeSingle();
189
+ if (lifecycle?.deleted_at) {
190
+ throw new McpInvalidParams(
191
+ `Document ${documentId} is soft-deleted (in the trash). A trashed document ` +
192
+ `cannot be edited — restore it first with cerefox_restore_document, then retry.`,
193
+ );
194
+ }
195
+ } catch (e) {
196
+ if (e instanceof McpInvalidParams) throw e;
197
+ // fall through — the RPC's CEREFOX_DELETED guard still refuses the write
198
+ }
199
+
178
200
  const { data, error } = await supabase.rpc("cerefox_get_document", {
179
201
  p_document_id: documentId,
180
202
  p_version_id: null,
@@ -303,8 +325,7 @@ async function applyAndWrite(
303
325
  if (error) {
304
326
  const message = error.message ?? "";
305
327
  if (message.includes("CEREFOX_CONFLICT")) {
306
- const current = message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
307
- throw conflictError(documentId, expectedHash, current);
328
+ throw conflictError(documentId, expectedHash, extractConflictHashes(message).current);
308
329
  }
309
330
  if (message.includes("cerefox_documents_hash_unique")) {
310
331
  // content_hash is UNIQUE store-wide, so an edit whose result matches
@@ -318,7 +339,25 @@ async function applyAndWrite(
318
339
  `cerefox_search for the resulting content to find it.`,
319
340
  );
320
341
  }
321
- if (message.includes("does not exist") && message.includes("cerefox_ingest_document")) {
342
+ if (message.includes("CEREFOX_UNRESOLVED_LINKS")) {
343
+ // Only ids INTRODUCED by this write reject (the RPC tolerates dead
344
+ // links the document already carried), so the diagnosis is specific.
345
+ const ids = message.match(/do not exist: ([^.]+)\./)?.[1] ?? "(unparsed)";
346
+ throw new Error(
347
+ `Edit rejected — this edit introduces link(s) to document id(s) that do not ` +
348
+ `exist: ${ids}. The UUIDs are almost certainly mangled — re-read the source ` +
349
+ `you copied each link from, correct the id(s), and retry. Do not retry ` +
350
+ `unchanged. Deliberate examples belong in code formatting (backticks).`,
351
+ );
352
+ }
353
+ if (message.includes("CEREFOX_DELETED")) {
354
+ // Race backstop: the doc was trashed between our read and the write.
355
+ throw new Error(
356
+ `Document ${documentId} was soft-deleted while this edit was in flight. ` +
357
+ `Restore it first with cerefox_restore_document, then retry.`,
358
+ );
359
+ }
360
+ if (isMissingFunctionError(message, "cerefox_ingest_document")) {
322
361
  throw new Error(
323
362
  `This server is behind: partial edits need schema 0.11.0 or newer. ` +
324
363
  `Run \`cerefox server deploy\`, then retry. (${message})`,
@@ -0,0 +1,130 @@
1
+ /**
2
+ * `cerefox_restore_document` — bring a soft-deleted document back (#210).
3
+ *
4
+ * The counterpart to `cerefox_delete_document`, added by maintainer decision
5
+ * (2026-08-13) reversing the earlier "restore is human-only" posture: every
6
+ * restore is audited with author attribution, restoring cannot destroy
7
+ * content, and CLI/MCP parity (`cerefox document restore` existed all along)
8
+ * beats a boundary the audit surface had already outgrown.
9
+ *
10
+ * What still cannot happen from here: **permanent purge**, which remains
11
+ * web-UI-only — the one action that actually destroys data keeps its
12
+ * human-in-the-loop confirmation.
13
+ *
14
+ * No `expected_content_hash`, matching the CLI contract: a trashed document
15
+ * cannot be rewritten while in the trash (cerefox_ingest_document refuses
16
+ * updates to soft-deleted documents as of 0.12.0), so what was reviewed in
17
+ * the trash is what comes back — there is no read-freshness to prove.
18
+ */
19
+
20
+ import type { MCPSupabaseClient } from "./types.ts";
21
+
22
+ import { isDocumentNotFoundError, isMissingFunctionError, logUsage } from "./_utils.ts";
23
+ import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
24
+
25
+ async function handler(
26
+ supabase: MCPSupabaseClient,
27
+ args: Record<string, unknown>,
28
+ ctx: ToolContext,
29
+ ): Promise<string> {
30
+ const document_id = args.document_id as string | undefined;
31
+ const reason = args.reason as string | undefined;
32
+
33
+ if (!document_id) throw new McpInvalidParams("document_id is required");
34
+
35
+ const author = (args.author as string | undefined) ?? (args.requestor as string | undefined);
36
+ // Derived from the transport, never taken from the caller: an agent must not
37
+ // be able to record itself as a user. Matches the delete handler.
38
+ const authorType = ctx.accessPath === "cli" ? "user" : "agent";
39
+
40
+ const { data, error } = await supabase.rpc("cerefox_restore_document", {
41
+ p_document_id: document_id,
42
+ p_author: author ?? "unknown",
43
+ p_author_type: authorType,
44
+ p_reason: reason ?? null,
45
+ });
46
+
47
+ if (error) {
48
+ const message = error.message ?? String(error);
49
+ if (isMissingFunctionError(message, "cerefox_restore_document")) {
50
+ throw new Error(
51
+ `This server is behind: cerefox_restore_document needs schema 0.12.0 or newer. ` +
52
+ `Run \`cerefox server deploy\`, then retry. (${message})`,
53
+ );
54
+ }
55
+ if (isDocumentNotFoundError(error)) {
56
+ throw new McpInvalidParams(`Document ${document_id} not found.`);
57
+ }
58
+ throw new Error(`RPC error: ${message}`);
59
+ }
60
+
61
+ const row = data as
62
+ | {
63
+ document_id?: string;
64
+ title?: string;
65
+ total_chars?: number;
66
+ restored?: boolean;
67
+ was_deleted?: boolean;
68
+ }
69
+ | undefined;
70
+ if (!row) throw new Error("cerefox_restore_document returned no data");
71
+
72
+ if (!row.restored) {
73
+ // Report what happened, not what was asked for: the document was never in
74
+ // the trash, nothing changed, no audit entry was written.
75
+ return (
76
+ `Document ${document_id} ("${row.title ?? "untitled"}") is NOT deleted — ` +
77
+ `there is nothing to restore. No change was made.`
78
+ );
79
+ }
80
+
81
+ logUsage(supabase, {
82
+ operation: "restore",
83
+ accessPath: ctx.accessPath,
84
+ requestor: args.requestor as string | undefined,
85
+ document_id,
86
+ result_count: 1,
87
+ });
88
+
89
+ return (
90
+ `Restored "${row.title ?? "untitled"}" (id: ${document_id}, ` +
91
+ `${row.total_chars ?? "?"} chars) from the trash. It is searchable again ` +
92
+ `and the restore is recorded in the audit log.\n` +
93
+ `Tell your user what you restored and why.`
94
+ );
95
+ }
96
+
97
+ export const restoreDocumentTool: ToolDefinition = {
98
+ name: "cerefox_restore_document",
99
+ description:
100
+ "Restore a soft-deleted document from the trash — the inverse of cerefox_delete_document. The document becomes searchable again; the restore is recorded in the audit log with your identity. Restoring a document that is not deleted is a reported no-op. Pass a short reason — like a delete's reason, it is what the human reviewing the audit trail goes on. Permanent purge has no agent surface: once a human purges a document from the web UI, it is gone and cannot be restored.",
101
+ annotations: {
102
+ title: "Restore document from trash",
103
+ readOnlyHint: false,
104
+ // Recovery only: it can bring content back, never remove it.
105
+ destructiveHint: false,
106
+ idempotentHint: true,
107
+ openWorldHint: false,
108
+ },
109
+ inputSchema: {
110
+ type: "object",
111
+ required: ["document_id"],
112
+ properties: {
113
+ document_id: { type: "string", description: "UUID of the soft-deleted document to restore." },
114
+ reason: {
115
+ type: "string",
116
+ description:
117
+ "Why this document is being restored. Recorded in the audit-log entry. Short and specific beats long.",
118
+ },
119
+ author: {
120
+ type: "string",
121
+ description: "Who is making this change. Recorded in the audit log.",
122
+ },
123
+ requestor: {
124
+ type: "string",
125
+ description: "Name of the agent or user making this request. Recorded in the usage log.",
126
+ },
127
+ },
128
+ },
129
+ handler,
130
+ };
@@ -0,0 +1,26 @@
1
+ -- 0024_mcp_delete_document.sql — MCP soft-delete parity (#208).
2
+ --
3
+ -- Reworks cerefox_delete_document: optional CAS via p_expected_content_hash
4
+ -- (CEREFOX_CONFLICT / PT409 on mismatch, same pattern as the ingest CAS),
5
+ -- p_reason appended to the audit description, JSONB return instead of VOID,
6
+ -- and idempotent re-delete (original deleted_at preserved, no duplicate audit
7
+ -- entry). No table DDL: the function itself ships in rpcs.sql, which
8
+ -- `cerefox server deploy` re-applies wholesale. This migration exists so the
9
+ -- schema version advances in step, which is what tells an existing deployment
10
+ -- it needs that redeploy.
11
+ --
12
+ -- Schema version 0.11.3 → 0.12.0. Restore moves OUT of the web-UI-only tier
13
+ -- (#210, maintainer decision): both delete and restore are agent-reachable
14
+ -- and audited; permanent purge is now the single web-UI-only action.
15
+ -- cerefox_ingest_document additionally refuses to rewrite a soft-deleted
16
+ -- document (restore first), which is what makes restore safe without a
17
+ -- freshness token.
18
+
19
+ DO $$
20
+ BEGIN
21
+ RAISE NOTICE
22
+ 'Migration 0024: cerefox_delete_document + cerefox_restore_document '
23
+ 'rework arrives with rpcs.sql on this deploy — CAS on delete, p_reason, '
24
+ 'JSONB returns, honest no-ops. Backs the new cerefox_delete_document '
25
+ 'and cerefox_restore_document MCP tools (#208, #210). Schema 0.12.0.';
26
+ END $$;
@@ -0,0 +1,15 @@
1
+ -- 0025_drop_orphaned_overloads.sql — remove pre-author-era 1-arg overloads.
2
+ --
3
+ -- cerefox_purge_document(UUID) and cerefox_restore_document(UUID) survived
4
+ -- every CREATE OR REPLACE since their signatures grew (OR REPLACE only
5
+ -- replaces the SAME signature), leaving long-lived databases with BOTH
6
+ -- overloads. A named 1-arg call is then ambiguous — PostgREST PGRST203
7
+ -- ("could not choose the best candidate") — which is how the first
8
+ -- production acceptance run failed to purge its fixtures (v1.7.0).
9
+ -- Fresh databases never had the old signatures and are unaffected.
10
+ --
11
+ -- Schema version 0.12.0 → 0.12.1. The DROPs also run from rpcs.sql on every
12
+ -- deploy; this migration makes the version advance signal the redeploy.
13
+
14
+ DROP FUNCTION IF EXISTS cerefox_purge_document(UUID);
15
+ DROP FUNCTION IF EXISTS cerefox_restore_document(UUID);
@@ -0,0 +1,46 @@
1
+ -- 0026_metadata_guard_and_dead_links.sql — #212 metadata type guards +
2
+ -- #214 phase-2 dead-link sweep.
3
+ --
4
+ -- RPC changes ship via rpcs.sql on this deploy: cerefox_ingest_document
5
+ -- rejects non-object p_metadata (the MCP layer always did; now every write
6
+ -- path agrees), cerefox_set_document_metadata refuses to MERGE onto a
7
+ -- non-object stored value (|| would produce an array; only replace=true
8
+ -- repairs), and two read-only RPCs arrive: cerefox_find_dead_links (whole-KB
9
+ -- [Text](uuid) sweep) and cerefox_metadata_health (rows with non-object
10
+ -- metadata, surfaced by doctor).
11
+ --
12
+ -- Schema version 0.12.1 → 0.12.2. This migration also REPORTS (not repairs)
13
+ -- any rows already in the non-object state, so the operator sees them at
14
+ -- upgrade time; repair is `cerefox document set-metadata <id> --replace`.
15
+
16
+ -- Table-level backstop (round-5 review): closes every current and future
17
+ -- direct writer at once, not only the RPC/CLI/web paths patched in code.
18
+ -- NOT VALID: legacy non-object rows survive (reported below) until repaired
19
+ -- with `document set-metadata --replace`, which the constraint then checks.
20
+ DO $$
21
+ BEGIN
22
+ IF NOT EXISTS (
23
+ SELECT 1 FROM pg_constraint
24
+ WHERE conname = 'cerefox_documents_metadata_object'
25
+ ) THEN
26
+ ALTER TABLE cerefox_documents
27
+ ADD CONSTRAINT cerefox_documents_metadata_object
28
+ CHECK (jsonb_typeof(metadata) = 'object') NOT VALID;
29
+ END IF;
30
+ END $$;
31
+
32
+ DO $$
33
+ DECLARE
34
+ v_count INT;
35
+ BEGIN
36
+ SELECT count(*) INTO v_count
37
+ FROM cerefox_documents
38
+ WHERE metadata IS NOT NULL AND jsonb_typeof(metadata) <> 'object';
39
+ IF v_count > 0 THEN
40
+ RAISE NOTICE
41
+ 'Migration 0026: % document(s) hold NON-OBJECT metadata (legacy #212 state). List them with cerefox doctor (or SELECT * FROM cerefox_metadata_health()); repair each with cerefox document set-metadata <id> --replace --json ''<object>''.',
42
+ v_count;
43
+ ELSE
44
+ RAISE NOTICE 'Migration 0026: metadata guards + dead-link sweep arrive with rpcs.sql. No non-object metadata rows found. Schema 0.12.2.';
45
+ END IF;
46
+ END $$;