@cerefox/memory 0.10.2 → 0.10.4

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.
@@ -18,7 +18,7 @@
18
18
  * doesn't touch `supabase/functions/` leaves it alone).
19
19
  */
20
20
 
21
- export const EF_VERSION = "0.10.1";
21
+ export const EF_VERSION = "0.10.4";
22
22
 
23
23
  /**
24
24
  * The 8 peer EFs the cerefox-mcp aggregator probes (excludes cerefox-mcp
@@ -21,7 +21,9 @@
21
21
  * prevents drift.
22
22
  */
23
23
 
24
- import type { MCPSupabaseClient } from "./types.ts";
24
+ import type { AccessPath, MCPSupabaseClient } from "./types.ts";
25
+
26
+ import { logUsage } from "./_utils.ts";
25
27
 
26
28
  /** Ensure `(documentId, project)` exists. Resolves project by name
27
29
  * (case-insensitive); creates the project if missing. Idempotent.
@@ -104,6 +106,120 @@ export async function setDocumentProjectsByName(
104
106
  return projectIds;
105
107
  }
106
108
 
109
+ export interface ReplaceDocumentProjectsResult {
110
+ documentTitle: string;
111
+ /** Names after stripping blanks + case-insensitive dedup, in input order. */
112
+ cleanNames: string[];
113
+ projectIds: string[];
114
+ }
115
+
116
+ /**
117
+ * Full-set replace of a document's project memberships, with audit + usage
118
+ * logging. The shared core behind both the `cerefox_set_document_projects`
119
+ * MCP tool and the `cerefox document set-projects` CLI command, so the two
120
+ * behave identically.
121
+ *
122
+ * Cleans the incoming names (strip blanks, preserve order, case-insensitive
123
+ * dedup), verifies the document exists and isn't soft-deleted, resolves each
124
+ * name → project_id (creating the project if absent), then DELETE-then-INSERT
125
+ * replaces the membership set. An empty (or all-blank) list clears all
126
+ * memberships. Writes an `update-metadata` audit entry (content is untouched)
127
+ * and a usage-log entry.
128
+ *
129
+ * Throws if the document is missing or soft-deleted. Callers validate
130
+ * argument *shape* (e.g. that `projectNames` is an array of strings).
131
+ */
132
+ export async function replaceDocumentProjects(
133
+ supabase: MCPSupabaseClient,
134
+ opts: {
135
+ documentId: string;
136
+ projectNames: string[];
137
+ author: string;
138
+ authorType: string;
139
+ accessPath: AccessPath;
140
+ },
141
+ ): Promise<ReplaceDocumentProjectsResult> {
142
+ const { documentId, projectNames, author, authorType, accessPath } = opts;
143
+
144
+ // Strip empties; preserve order; dedup case-insensitively.
145
+ const seenLower = new Set<string>();
146
+ const cleanNames: string[] = [];
147
+ for (const n of projectNames) {
148
+ const stripped = (n ?? "").trim();
149
+ if (!stripped) continue;
150
+ const key = stripped.toLowerCase();
151
+ if (seenLower.has(key)) continue;
152
+ seenLower.add(key);
153
+ cleanNames.push(stripped);
154
+ }
155
+
156
+ // Verify the document exists and isn't soft-deleted.
157
+ const { data: doc } = await supabase
158
+ .from("cerefox_documents")
159
+ .select("id, title")
160
+ .eq("id", documentId)
161
+ .is("deleted_at", null)
162
+ .limit(1);
163
+ if (!doc?.length) {
164
+ throw new Error(`Document not found (or soft-deleted): ${documentId}`);
165
+ }
166
+
167
+ // Resolve each name → project_id (create if absent). Preserve order.
168
+ const projectIds: string[] = [];
169
+ for (const name of cleanNames) {
170
+ const { data: proj } = await supabase
171
+ .from("cerefox_projects")
172
+ .select("id")
173
+ .ilike("name", name)
174
+ .limit(1);
175
+ if (proj?.length) {
176
+ projectIds.push(proj[0].id);
177
+ } else {
178
+ const { data: newProj } = await supabase
179
+ .from("cerefox_projects")
180
+ .insert({ name })
181
+ .select("id");
182
+ if (newProj?.[0]?.id) projectIds.push(newProj[0].id);
183
+ }
184
+ }
185
+
186
+ // DELETE-then-INSERT replace (matches Python assign_document_projects).
187
+ await supabase.from("cerefox_document_projects").delete().eq("document_id", documentId);
188
+ if (projectIds.length > 0) {
189
+ const rows = projectIds.map((pid) => ({ document_id: documentId, project_id: pid }));
190
+ await supabase.from("cerefox_document_projects").insert(rows);
191
+ }
192
+
193
+ // Audit entry — project membership is metadata, not content.
194
+ try {
195
+ await supabase.rpc("cerefox_create_audit_entry", {
196
+ p_document_id: documentId,
197
+ p_version_id: null,
198
+ p_operation: "update-metadata",
199
+ p_author: author,
200
+ p_author_type: authorType,
201
+ p_size_before: null,
202
+ p_size_after: null,
203
+ p_description:
204
+ cleanNames.length > 0
205
+ ? `Set document projects to [${cleanNames.join(", ")}]`
206
+ : "Cleared all project memberships",
207
+ });
208
+ } catch (err) {
209
+ console.warn("replaceDocumentProjects: audit entry failed", err);
210
+ }
211
+
212
+ logUsage(supabase, {
213
+ operation: "set-document-projects",
214
+ accessPath,
215
+ requestor: author,
216
+ document_id: documentId,
217
+ result_count: projectIds.length,
218
+ });
219
+
220
+ return { documentTitle: doc[0].title as string, cleanNames, projectIds };
221
+ }
222
+
107
223
  /** Resolve a project name → project_id (case-insensitive), or `null` if
108
224
  * not found. Does NOT create. Used by search / metadata-search to translate
109
225
  * `project_name` parameters to UUIDs. */
@@ -11,16 +11,16 @@
11
11
  * docs/specs/polish-and-distribution-design.md §10d.
12
12
  */
13
13
 
14
- export const HELP_FULL = "# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **10 MCP tools** (9 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), `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata`, `author` |\n| `cerefox_get_document` | Get full document by ID | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_metadata_search` | Find docs by metadata (no text query) | `metadata_filter` (required), `include_content`, `updated_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_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## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **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) -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\", author=\"my-agent\")\n```\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true, 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`, …). The legacy Python `uv run cerefox` is now a frozen husk as of v0.9 — only `uv run cerefox mcp` still works.\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>\" --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_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` |\n| `cerefox_set_document_projects` | _MCP-only; a CLI command will be added in a future release. Until then, run via MCP if available._ |\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";
14
+ export const HELP_FULL = "# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **10 MCP tools** (9 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), `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata`, `author` |\n| `cerefox_get_document` | Get full document by ID | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\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_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## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **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) -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\", author=\"my-agent\")\n```\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true, 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`, …). The legacy Python `uv run cerefox` is now a frozen husk as of v0.9 — only `uv run cerefox mcp` still works.\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>\" --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_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\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";
15
15
 
16
16
  /** Sections keyed by their H2 heading text (lower-cased for matching). */
17
17
  export const HELP_SECTIONS: Record<string, string> = {
18
- "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), `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata`, `author` |\n| `cerefox_get_document` | Get full document by ID | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_metadata_search` | Find docs by metadata (no text query) | `metadata_filter` (required), `include_content`, `updated_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_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) |",
18
+ "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), `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata`, `author` |\n| `cerefox_get_document` | Get full document by ID | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\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_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) |",
19
19
  "Essential Rules": "## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **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`.",
20
20
  "Update Workflow (ID-based -- preferred)": "## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\", author=\"my-agent\")\n```",
21
21
  "Update Workflow (title-based -- fallback)": "## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true, author=\"my-agent\")\n```",
22
22
  "Catch-Up Workflow": "## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```",
23
- "CLI fallback (when MCP is unavailable)": "## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …). The legacy Python `uv run cerefox` is now a frozen husk as of v0.9 — only `uv run cerefox mcp` still works.\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>\" --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_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` |\n| `cerefox_set_document_projects` | _MCP-only; a CLI command will be added in a future release. Until then, run via MCP if available._ |\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.",
23
+ "CLI fallback (when MCP is unavailable)": "## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …). The legacy Python `uv run cerefox` is now a frozen husk as of v0.9 — only `uv run cerefox mcp` still works.\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>\" --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_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\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.",
24
24
  };
25
25
 
26
26
  export const HELP_SECTION_HEADINGS: string[] = ["Tools", "Essential Rules", "Update Workflow (ID-based -- preferred)", "Update Workflow (title-based -- fallback)", "Catch-Up Workflow", "CLI fallback (when MCP is unavailable)"];
@@ -24,11 +24,18 @@ async function handler(
24
24
  const include_content = (args.include_content as boolean | undefined) ?? false;
25
25
  const requested_max_bytes = args.max_bytes as number | undefined;
26
26
 
27
- if (!metadata_filter || typeof metadata_filter !== "object" || Array.isArray(metadata_filter)) {
28
- throw new McpInvalidParams("metadata_filter is required and must be a JSON object");
27
+ if (metadata_filter !== undefined && (typeof metadata_filter !== "object" || Array.isArray(metadata_filter))) {
28
+ throw new McpInvalidParams("metadata_filter must be a JSON object when provided");
29
29
  }
30
- if (Object.keys(metadata_filter).length === 0) {
31
- throw new McpInvalidParams("metadata_filter must contain at least one key-value pair");
30
+ const has_metadata = !!metadata_filter && Object.keys(metadata_filter).length > 0;
31
+ // metadata_filter is optional, but at least one narrowing criterion is
32
+ // required so this can never become an unbounded whole-KB dump. An empty
33
+ // filter + project_name lists a project's documents: the RPC's
34
+ // `metadata @> '{}'` matches every row and the project predicate narrows it.
35
+ if (!has_metadata && !project_name && !updated_since && !created_since) {
36
+ throw new McpInvalidParams(
37
+ "Provide at least one of: metadata_filter, project_name, updated_since, or created_since.",
38
+ );
32
39
  }
33
40
 
34
41
  // Resolve project name to UUID if provided
@@ -45,7 +52,7 @@ async function handler(
45
52
  : null;
46
53
 
47
54
  const params: Record<string, unknown> = {
48
- p_metadata_filter: metadata_filter,
55
+ p_metadata_filter: metadata_filter ?? {},
49
56
  p_project_id: projectId,
50
57
  p_updated_since: updated_since ?? null,
51
58
  p_created_since: created_since ?? null,
@@ -78,12 +85,12 @@ async function handler(
78
85
  operation: "metadata_search",
79
86
  accessPath: ctx.accessPath,
80
87
  requestor: args.requestor as string | undefined,
81
- query_text: JSON.stringify(metadata_filter),
88
+ query_text: JSON.stringify(metadata_filter ?? {}),
82
89
  project_id: projectId,
83
90
  result_count: rows.length,
84
91
  });
85
92
 
86
- if (rows.length === 0) return "No documents match the metadata filter.";
93
+ if (rows.length === 0) return "No documents match the given criteria.";
87
94
 
88
95
  // Note: when include_content is true the RPC already respects p_max_bytes
89
96
  // server-side. The applyByteBudget helper is retained here only for
@@ -114,18 +121,17 @@ async function handler(
114
121
  export const metadataSearchTool: ToolDefinition = {
115
122
  name: "cerefox_metadata_search",
116
123
  description:
117
- "Find documents by metadata key-value criteria without a text search term. Use to discover documents tagged with specific attributes, browse by taxonomy, or retrieve messages/tasks by type and status.",
124
+ "Find or list documents by metadata key-value criteria without a text search term. Use to discover documents tagged with specific attributes, browse by taxonomy, retrieve messages/tasks by type and status, or list all documents in a project (pass project_name alone). At least one of metadata_filter, project_name, updated_since, or created_since must be supplied; results are ordered newest-updated first.",
118
125
  inputSchema: {
119
126
  type: "object",
120
- required: ["metadata_filter"],
121
127
  properties: {
122
128
  metadata_filter: {
123
129
  type: "object",
124
130
  description:
125
- 'Key-value pairs; ALL must match (AND semantics). Example: {"type": "decision", "status": "active"}. Call cerefox_list_metadata_keys first to discover available keys.',
131
+ 'Key-value pairs; ALL must match (AND semantics). Example: {"type": "decision", "status": "active"}. Call cerefox_list_metadata_keys first to discover available keys. Optional — omit (or pass {}) to list by project_name / time range alone.',
126
132
  additionalProperties: { type: "string" },
127
133
  },
128
- project_name: { type: "string", description: "Restrict to a project by name (optional)" },
134
+ project_name: { type: "string", description: "Restrict to a project by name. Sufficient on its own to list that project's documents (optional)." },
129
135
  updated_since: {
130
136
  type: "string",
131
137
  description: "ISO-8601 timestamp; only docs updated on/after (optional)",
@@ -11,7 +11,7 @@
11
11
 
12
12
  import type { MCPSupabaseClient } from "./types.ts";
13
13
 
14
- import { logUsage } from "./_utils.ts";
14
+ import { replaceDocumentProjects } from "./_projects.ts";
15
15
  import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
16
16
 
17
17
  async function handler(
@@ -42,80 +42,15 @@ async function handler(
42
42
  throw new McpInvalidParams("project_names must contain only strings.");
43
43
  }
44
44
 
45
- // Strip empties; preserve order; dedup case-insensitively.
46
- const seenLower = new Set<string>();
47
- const cleanNames: string[] = [];
48
- for (const n of project_names_raw as string[]) {
49
- const stripped = n.trim();
50
- if (!stripped) continue;
51
- const key = stripped.toLowerCase();
52
- if (seenLower.has(key)) continue;
53
- seenLower.add(key);
54
- cleanNames.push(stripped);
55
- }
56
-
57
- // Verify the document exists and isn't soft-deleted.
58
- const { data: doc } = await supabase
59
- .from("cerefox_documents")
60
- .select("id, title")
61
- .eq("id", document_id)
62
- .is("deleted_at", null)
63
- .limit(1);
64
- if (!doc?.length) {
65
- throw new Error(`Document not found (or soft-deleted): ${document_id}`);
66
- }
67
-
68
- // Resolve each name → project_id (create if absent). Preserve order.
69
- const projectIds: string[] = [];
70
- for (const name of cleanNames) {
71
- const { data: proj } = await supabase
72
- .from("cerefox_projects")
73
- .select("id")
74
- .ilike("name", name)
75
- .limit(1);
76
- if (proj?.length) {
77
- projectIds.push(proj[0].id);
78
- } else {
79
- const { data: newProj } = await supabase
80
- .from("cerefox_projects")
81
- .insert({ name })
82
- .select("id");
83
- if (newProj?.[0]?.id) projectIds.push(newProj[0].id);
84
- }
85
- }
86
-
87
- // DELETE-then-INSERT replace (matches Python assign_document_projects).
88
- await supabase.from("cerefox_document_projects").delete().eq("document_id", document_id);
89
- if (projectIds.length > 0) {
90
- const rows = projectIds.map((pid) => ({ document_id, project_id: pid }));
91
- await supabase.from("cerefox_document_projects").insert(rows);
92
- }
93
-
94
- // Audit entry — project membership is metadata, not content.
95
- try {
96
- await supabase.rpc("cerefox_create_audit_entry", {
97
- p_document_id: document_id,
98
- p_version_id: null,
99
- p_operation: "update-metadata",
100
- p_author: author,
101
- p_author_type: "agent",
102
- p_size_before: null,
103
- p_size_after: null,
104
- p_description:
105
- cleanNames.length > 0
106
- ? `Set document projects to [${cleanNames.join(", ")}]`
107
- : "Cleared all project memberships",
108
- });
109
- } catch (err) {
110
- console.warn("set-document-projects: audit entry failed", err);
111
- }
112
-
113
- logUsage(supabase, {
114
- operation: "set-document-projects",
45
+ // The clean/dedup, existence check, name→id resolution, DELETE-then-INSERT
46
+ // replace, audit entry, and usage log all live in the shared core so the
47
+ // `cerefox document set-projects` CLI command behaves identically.
48
+ const { cleanNames, projectIds } = await replaceDocumentProjects(supabase, {
49
+ documentId: document_id,
50
+ projectNames: project_names_raw as string[],
51
+ author,
52
+ authorType: "agent",
115
53
  accessPath: ctx.accessPath,
116
- requestor: author,
117
- document_id,
118
- result_count: projectIds.length,
119
54
  });
120
55
 
121
56
  if (cleanNames.length === 0) {
@@ -15,8 +15,11 @@ import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/inde
15
15
  * Note: cerefox-mcp calls the RPC directly (not this Edge Function).
16
16
  *
17
17
  * Request body (JSON):
18
- * metadata_filter object required Key-value pairs (AND semantics)
18
+ * metadata_filter object optional Key-value pairs (AND semantics)
19
19
  * project_id string optional Project UUID filter
20
+ *
21
+ * At least one of metadata_filter / project_id / updated_since / created_since
22
+ * must be supplied (an empty filter + project_id lists that project's docs).
20
23
  * updated_since string optional ISO-8601 lower bound for updated_at
21
24
  * created_since string optional ISO-8601 lower bound for created_at
22
25
  * limit number optional Max results (default: 10)
@@ -53,13 +56,12 @@ Deno.serve(async (req: Request): Promise<Response> => {
53
56
  const metadata_filter = body.metadata_filter;
54
57
 
55
58
  if (
56
- !metadata_filter ||
57
- typeof metadata_filter !== "object" ||
58
- Array.isArray(metadata_filter) ||
59
- Object.keys(metadata_filter).length === 0
59
+ metadata_filter !== undefined &&
60
+ metadata_filter !== null &&
61
+ (typeof metadata_filter !== "object" || Array.isArray(metadata_filter))
60
62
  ) {
61
63
  return new Response(
62
- JSON.stringify({ error: "metadata_filter is required and must be a non-empty JSON object" }),
64
+ JSON.stringify({ error: "metadata_filter must be a JSON object when provided" }),
63
65
  { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
64
66
  );
65
67
  }
@@ -67,6 +69,22 @@ Deno.serve(async (req: Request): Promise<Response> => {
67
69
  const project_id = body.project_id ?? null;
68
70
  const updated_since = body.updated_since ?? null;
69
71
  const created_since = body.created_since ?? null;
72
+
73
+ // metadata_filter is optional, but at least one narrowing criterion is
74
+ // required so this never becomes an unbounded whole-KB dump. An empty
75
+ // filter + project_id lists a project's documents (the RPC's
76
+ // `metadata @> '{}'` matches every row; the project predicate narrows it).
77
+ const has_metadata =
78
+ metadata_filter && typeof metadata_filter === "object" &&
79
+ Object.keys(metadata_filter).length > 0;
80
+ if (!has_metadata && !project_id && !updated_since && !created_since) {
81
+ return new Response(
82
+ JSON.stringify({
83
+ error: "Provide at least one of: metadata_filter, project_id, updated_since, or created_since.",
84
+ }),
85
+ { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
86
+ );
87
+ }
70
88
  const limit = body.limit ?? 10;
71
89
  const include_content = body.include_content ?? false;
72
90
  const requested_max_bytes = body.max_bytes;
@@ -102,7 +120,7 @@ Deno.serve(async (req: Request): Promise<Response> => {
102
120
  }
103
121
 
104
122
  const params: Record<string, unknown> = {
105
- p_metadata_filter: metadata_filter,
123
+ p_metadata_filter: has_metadata ? metadata_filter : {},
106
124
  p_project_id: project_id,
107
125
  p_updated_since: updated_since,
108
126
  p_created_since: created_since,
@@ -209,7 +209,7 @@ paths.
209
209
  | Tier | Operations | Reversible? | Where exposed |
210
210
  |---|---|---|---|
211
211
  | 1. Reads + soft mutations | search, get, list-*, ingest (create/update), metadata-search, get-audit-log | n/a (reads) / yes (versioned) | All paths — MCP, Edge Functions, CLI, web UI |
212
- | 2. Soft-destructive | `delete_document` (soft delete to trash), `set_review_status` | yes — restorable via web UI | All paths (CLI: `cerefox document delete`; web UI; Python; **not** MCP or Edge Functions today) |
212
+ | 2. Soft-destructive | `delete_document` (soft delete to trash), `set_review_status` | yes — restorable via web UI | All paths (CLI: `cerefox document delete`; web UI; **not** MCP or Edge Functions today) |
213
213
  | 3. **Hard-destructive** | `purge_document` (permanent), `restore_document` (un-trash), `set_version_archived` (toggle version retention) | no (purge) / yes (restore, but recovers from a destructive action) | **Web UI only** |
214
214
 
215
215
  ### Why purge / restore are web-UI-only
@@ -53,7 +53,7 @@ A living document where agents record decisions, experiment outcomes, and lesson
53
53
 
54
54
  **Example**: A coding agent working on a project records "Chose PostgreSQL RPC approach over application-level logic because..." in a decision log document. Next week, a different agent working on a related feature searches Cerefox, finds the decision log, and understands the rationale without re-deriving it.
55
55
 
56
- **How it works**: Create a document with a structured format (date, context, decision, outcome). Use a consistent title or project tag so agents can find it. Use `update_if_exists: true` to append new entries.
56
+ **How it works**: Create a document with a structured format (date, context, decision, outcome). Use a consistent title or project tag so agents can find it. To add entries over time, re-ingest with `update_if_exists: true` (or `document_id`) — this replaces the document in place, so build the new full content by appending to the prior content you fetched.
57
57
 
58
58
  **Best for**: Project-level institutional memory, avoiding repeated decisions, onboarding new agent sessions.
59
59
 
@@ -207,7 +207,9 @@ cerefox document list [OPTIONS]
207
207
  | `--deleted` | flag | off | List soft-deleted (trashed) documents instead of active ones, newest-deleted first. Pair the ids with `cerefox document restore` / `cerefox document delete`. |
208
208
  | `--json` | flag | off | Machine-readable JSON output. |
209
209
 
210
- **Output**: tabular `id | title | source | status | updated_at` listing (or `deleted_at` with `--deleted`). CLI-only — there is no MCP equivalent.
210
+ **Output**: tabular `id | title | source | status | updated_at` listing (or `deleted_at` with `--deleted`).
211
+
212
+ **MCP equivalent**: scope-by-project / metadata / time listing maps to [`cerefox_metadata_search`](../../AGENT_GUIDE.md) — e.g. `cerefox_metadata_search(project_name="research")` lists that project's documents (the `metadata_filter` may be empty when another scope is supplied). The `--deleted` (trash) view and unscoped whole-KB listing remain CLI-only.
211
213
 
212
214
  ---
213
215
 
@@ -240,6 +242,43 @@ cerefox document edit <doc-id> --set-meta status=archived --unset-meta draft
240
242
 
241
243
  ---
242
244
 
245
+ ### `cerefox document set-projects`
246
+
247
+ **Purpose**: replace a document's project memberships with **exactly** the given set (full-set replace — any project not listed is removed). This is the CLI equivalent of the `cerefox_set_document_projects` MCP tool; both share one membership-replace core, so they behave identically. Content is untouched; the change is logged as an `update-metadata` audit entry.
248
+
249
+ **Synopsis**:
250
+ ```
251
+ cerefox document set-projects [OPTIONS] DOCUMENT_ID [PROJECT_NAMES...]
252
+ ```
253
+
254
+ **Options**:
255
+
256
+ | Flag | Type | Default | Description |
257
+ |---|---|---|---|
258
+ | `[project-names...]` | variadic args | _none_ | One or more project names. Each is created if missing; order preserved; case-insensitively de-duplicated. |
259
+ | `--clear` | flag | off | Remove the document from **all** projects. Mutually exclusive with passing names. |
260
+ | `--author <name>` (`-a`) | str | `CEREFOX_AUTHOR_NAME` or `unknown` | Identity recorded in the audit log. |
261
+ | `--author-type <type>` | `user`\|`agent` | `user` | Caller type recorded in the audit log. |
262
+
263
+ To set memberships **and** update content in one shot, use `cerefox document ingest --document-id <id> --project-name …` instead. Use this command when you only need to change membership.
264
+
265
+ **Examples**:
266
+ ```bash
267
+ # Set the document to belong to exactly these two projects (replaces any others)
268
+ cerefox document set-projects <doc-id> research archive
269
+
270
+ # Remove the document from all projects
271
+ cerefox document set-projects <doc-id> --clear
272
+ ```
273
+
274
+ **Output**: a confirmation line with the document title and the resulting project set (or a "cleared all memberships" line), plus a reminder that the previous set was replaced.
275
+
276
+ **Exit codes**: `0` on success; `1` on validation error (no names and no `--clear`, or both) or if the document is missing / soft-deleted.
277
+
278
+ **MCP equivalent**: [`cerefox_set_document_projects`](../../AGENT_GUIDE.md).
279
+
280
+ ---
281
+
243
282
  ### `cerefox document restore`
244
283
 
245
284
  **Purpose**: restore a soft-deleted (trashed) document back to active.
@@ -599,10 +638,59 @@ Every MCP parameter has an exact-name CLI flag (kebab-cased). Short forms exist
599
638
  | `cerefox_get_document(document_id, version_id, requestor)` | `cerefox document get <id> --version-id <vid> --requestor <name>` |
600
639
  | `cerefox_list_versions(document_id, requestor)` | `cerefox document version list <id> --requestor <name>` |
601
640
  | `cerefox_list_projects(requestor)` | `cerefox project list --requestor <name>` |
641
+ | `cerefox_set_document_projects(document_id, project_names, author)` | `cerefox document set-projects <id> <name...> --author <a> --author-type <t>` (or `--clear` to remove all) |
602
642
  | `cerefox_list_metadata_keys()` | `cerefox metadata keys` |
603
643
  | `cerefox_metadata_search(metadata_filter, project_name, updated_since, created_since, limit, include_content, requestor)` | `cerefox metadata search --metadata-filter '<json>' --project-name <n> --updated-since <iso> --created-since <iso> --limit N --include-content --requestor <name>` |
604
644
  | `cerefox_get_audit_log(document_id, author, operation, since, until, limit, requestor)` | `cerefox audit list --document-id <id> --author <a> --operation <op> --since <iso> --until <iso> --limit N --requestor <name>` |
605
645
 
646
+ ## CLI ↔ MCP parity matrix
647
+
648
+ The table above is MCP-first (it lists the tools that *have* a CLI form). This
649
+ one is **CLI-first** — every `cerefox` command, with its MCP equivalent or an
650
+ explicit reason it has none. It exists to make parity gaps visible. Legend:
651
+ **✅ mapped** · **⚠️ gap** (a capability one surface has and the other lacks,
652
+ arguably worth closing) · **🔒 intentional** (deliberately not on the MCP/agent
653
+ surface).
654
+
655
+ | CLI command | MCP equivalent | Status |
656
+ |---|---|---|
657
+ | `document ingest` | `cerefox_ingest` | ✅ |
658
+ | `document ingest-dir` | — (agents loop `cerefox_ingest`) | 🔒 bulk filesystem walk; no server-side dir access from MCP |
659
+ | `search` | `cerefox_search` | ✅ (CLI adds `--mode`/`--alpha`/`--min-score`/`--only-metadata`) |
660
+ | `document get` | `cerefox_get_document` | ✅ |
661
+ | `document list` | `cerefox_metadata_search` (scope by `project_name` / metadata / time) | ✅ as of this change. Unscoped whole-KB listing has no MCP path by design (scope it) |
662
+ | `document edit` (title / metadata in place) | — | 🔒 intentional: a human/web-parity convenience. Agents update title+metadata deterministically via `cerefox_ingest` (with `document_id`); a metadata-only edit isn't a needed agent primitive |
663
+ | `document delete` (soft-delete) | — | 🔒 destructive; trust model keeps delete/restore on CLI + web only |
664
+ | `document restore` | — | 🔒 trust model (CLI + web only) |
665
+ | `document version list` | `cerefox_list_versions` | ✅ |
666
+ | `document version archive` / `unarchive` | — | 🔒 intentional: version-retention protection is exposed only to CLI + web (a maintenance concern, not an agent primitive) |
667
+ | `document set-projects` | `cerefox_set_document_projects` | ✅ full-set replace of a document's project memberships (shared core; `--clear` to remove all) |
668
+ | `project list` | `cerefox_list_projects` | ✅ |
669
+ | `project create` / `edit` / `delete` | — | 🔒 project mutations CLI + web only |
670
+ | `metadata keys` | `cerefox_list_metadata_keys` | ✅ |
671
+ | `metadata search` | `cerefox_metadata_search` | ✅ |
672
+ | `audit list` | `cerefox_get_audit_log` | ✅ |
673
+ | `guides list` / `show` / `open` / `ingest` | `cerefox_get_help` (partial) | ✅~ `get_help` returns the bundled quick-reference; `guides` is the richer CLI form |
674
+ | `server deploy` / `server reindex` | — | 🔒 operator/deploy surface |
675
+ | `config list` / `get` / `set` | — | 🔒 runtime config; operator surface |
676
+ | `web` / `mcp` | — | 🔒 lifecycle (`mcp` *is* the MCP server) |
677
+ | `init` / `doctor` / `status` / `configure-agent` / `self-update` / `completion` / `backup *` | — | 🔒 install / health / ops |
678
+
679
+ **Gap status** (the 🔒 rows are deliberate and out of scope):
680
+
681
+ 1. `document list` → **closed**: project/metadata/time-scoped listing now routes
682
+ through `cerefox_metadata_search` (it accepts an empty `metadata_filter` when
683
+ another scope is supplied).
684
+ 2. `cerefox_set_document_projects` → **closed**: added `cerefox document
685
+ set-projects` (full-set replace, `--clear` to remove all), sharing the
686
+ membership-replace core with the MCP tool.
687
+ 3. `document edit` (metadata/title-only edit) → **intentional non-gap**: a
688
+ human/web-parity convenience; agents use `cerefox_ingest` for content+metadata
689
+ updates. Revisit only if a concrete agent workflow needs metadata-only edits.
690
+ 4. `document version archive` / `unarchive` → **intentional non-gap**: version-retention
691
+ protection is exposed only to CLI + web (a maintenance concern, deliberately not on
692
+ the MCP/agent surface).
693
+
606
694
  ## Known issues
607
695
 
608
696
  None outstanding as of v0.1.17 (cerefox#27 — the `cerefox search` NameError — is resolved). When new bugs surface, they are tracked in the GitHub issues list; check there before relying on a behaviour the docs imply.
@@ -604,7 +604,7 @@ In the action editor, paste this schema (replace `<your-project-ref>`):
604
604
  openapi: 3.1.0
605
605
  info:
606
606
  title: Cerefox Knowledge Base
607
- version: 1.8.0
607
+ version: 1.9.0
608
608
  servers:
609
609
  - url: https://<your-project-ref>.supabase.co/functions/v1
610
610
  paths:
@@ -895,15 +895,17 @@ paths:
895
895
  post:
896
896
  operationId: metadataSearch
897
897
  summary: >
898
- Find documents by metadata key-value criteria without a text search term.
899
- Use to discover documents tagged with specific attributes or browse by taxonomy.
898
+ Find or list documents by metadata key-value criteria without a text
899
+ search term. Use to discover documents tagged with specific attributes,
900
+ browse by taxonomy, or list a project's documents (pass project_id alone).
901
+ At least one of metadata_filter, project_id, updated_since, or
902
+ created_since must be supplied.
900
903
  requestBody:
901
904
  required: true
902
905
  content:
903
906
  application/json:
904
907
  schema:
905
908
  type: object
906
- required: [metadata_filter]
907
909
  properties:
908
910
  metadata_filter:
909
911
  type: object
@@ -912,10 +914,14 @@ paths:
912
914
  description: >
913
915
  Key-value pairs; ALL must match (AND semantics).
914
916
  Example: {"type": "decision", "status": "active"}.
917
+ Optional — omit (or pass {}) to list by project_id / time
918
+ range alone. At least one filter (metadata_filter, project_id,
919
+ updated_since, or created_since) is required.
915
920
  project_id:
916
921
  type: string
917
922
  description: >
918
- Filter by project UUID (optional). NOTE: this is the project
923
+ Filter by project UUID (optional). Sufficient on its own to
924
+ list that project's documents. NOTE: this is the project
919
925
  UUID, not its name — unlike searchKnowledgeBase / ingestNote
920
926
  which take project_name. Get UUIDs from listProjects.
921
927
  updated_since:
@@ -1148,6 +1154,7 @@ The agent docs are written around MCP tool names. **CLI flag names match MCP par
1148
1154
  | `cerefox_get_document` | `cerefox document get <document-id> --version-id <vid> --requestor <name>` |
1149
1155
  | `cerefox_list_versions` | `cerefox document version list <document-id> --requestor <name>` |
1150
1156
  | `cerefox_list_projects` | `cerefox project list --requestor <name>` |
1157
+ | `cerefox_set_document_projects` | `cerefox document set-projects <document-id> <name...> --author <a> --author-type user\|agent` (or `--clear`) |
1151
1158
  | `cerefox_list_metadata_keys` | `cerefox metadata keys` |
1152
1159
  | `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --project-name <n> --requestor <name>` |
1153
1160
  | `cerefox_get_audit_log` | `cerefox audit list --document-id <id> --author <a> --operation <op> --since <iso> --until <iso> --limit N --json --requestor <name>` |