@cerefox/memory 0.10.4 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +38 -15
- package/AGENT_QUICK_REFERENCE.md +13 -8
- package/dist/bin/cerefox.js +189 -69
- package/dist/frontend/assets/{index-DVXDQ7__.js → index-ojNhWSxm.js} +3 -3
- package/dist/frontend/assets/index-ojNhWSxm.js.map +1 -0
- package/dist/frontend/index.html +1 -1
- package/dist/server-assets/_shared/ef-meta/index.ts +1 -1
- package/dist/server-assets/_shared/mcp-tools/get-document.ts +6 -2
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +6 -6
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +75 -7
- package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +3 -1
- package/dist/server-assets/_shared/mcp-tools/search.ts +4 -1
- package/dist/server-assets/db/rpcs.sql +89 -16
- package/dist/server-assets/db/schema.sql +1 -1
- package/dist/server-assets/supabase/functions/cerefox-get-document/index.ts +5 -1
- package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +71 -2
- package/docs/guides/agent-coordination.md +14 -1
- package/docs/guides/cli.md +23 -8
- package/docs/guides/connect-agents.md +44 -3
- package/package.json +1 -1
- package/dist/frontend/assets/index-DVXDQ7__.js.map +0 -1
package/dist/bin/cerefox.js
CHANGED
|
@@ -7184,7 +7184,7 @@ var exports_meta = {};
|
|
|
7184
7184
|
__export(exports_meta, {
|
|
7185
7185
|
PKG_VERSION: () => PKG_VERSION
|
|
7186
7186
|
});
|
|
7187
|
-
var PKG_VERSION = "0.
|
|
7187
|
+
var PKG_VERSION = "0.11.1";
|
|
7188
7188
|
var init_meta = () => {};
|
|
7189
7189
|
|
|
7190
7190
|
// ../../node_modules/.bun/tslib@2.8.1/node_modules/tslib/tslib.js
|
|
@@ -54007,16 +54007,18 @@ async function handler2(supabase, args, ctx) {
|
|
|
54007
54007
|
result_count: 1
|
|
54008
54008
|
});
|
|
54009
54009
|
const label = version_id !== null ? " (archived version)" : " (current)";
|
|
54010
|
-
|
|
54010
|
+
const hashLine = row.content_hash ? `content_hash: ${row.content_hash}
|
|
54011
54011
|
|
|
54012
|
-
|
|
54012
|
+
` : "";
|
|
54013
|
+
return `# ${row.doc_title ?? "Untitled"}${label}
|
|
54014
|
+
${hashLine}${row.full_content ?? ""}`;
|
|
54013
54015
|
}
|
|
54014
54016
|
var getDocumentTool;
|
|
54015
54017
|
var init_get_document = __esm(() => {
|
|
54016
54018
|
init_types3();
|
|
54017
54019
|
getDocumentTool = {
|
|
54018
54020
|
name: "cerefox_get_document",
|
|
54019
|
-
description: "Retrieve the full reconstructed content of a document. Pass version_id to retrieve an archived version; omit it (or pass null) for the current version. Version UUIDs are returned by cerefox_list_versions.",
|
|
54021
|
+
description: "Retrieve the full reconstructed content of a document. Pass version_id to retrieve an archived version; omit it (or pass null) for the current version. Version UUIDs are returned by cerefox_list_versions. The response header includes the document's current content_hash — pass it back as expected_content_hash when updating via cerefox_ingest (optimistic concurrency).",
|
|
54020
54022
|
inputSchema: {
|
|
54021
54023
|
type: "object",
|
|
54022
54024
|
required: ["document_id"],
|
|
@@ -54037,15 +54039,23 @@ var init_get_document = __esm(() => {
|
|
|
54037
54039
|
});
|
|
54038
54040
|
|
|
54039
54041
|
// ../../_shared/mcp-tools/get-help-content.ts
|
|
54040
|
-
var 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
|
|
54042
|
+
var 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), `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_get_document` | Get full document by ID (header includes `content_hash` — the update token) | `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. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you read (shown by `cerefox_get_document`, `cerefox_search`, and `cerefox_metadata_search`) when updating a document. 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. **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`, …). 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>" --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_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', HELP_SECTIONS, HELP_SECTION_HEADINGS;
|
|
54041
54043
|
var init_get_help_content = __esm(() => {
|
|
54042
54044
|
HELP_SECTIONS = {
|
|
54043
|
-
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
|
|
54044
|
-
"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`.',
|
|
54045
|
-
"Update Workflow (ID-based -- preferred)":
|
|
54046
|
-
|
|
54045
|
+
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_get_document` | Get full document by ID (header includes `content_hash` — the update token) | `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) |",
|
|
54046
|
+
"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. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you read (shown by `cerefox_get_document`, `cerefox_search`, and `cerefox_metadata_search`) when updating a document. 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. **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`.',
|
|
54047
|
+
"Update Workflow (ID-based -- preferred)": `## Update Workflow (ID-based -- preferred)
|
|
54048
|
+
|
|
54049
|
+
\`\`\`
|
|
54050
|
+
search("topic") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->
|
|
54051
|
+
ingest(title="Same Title", content="...", document_id="abc123",
|
|
54052
|
+
expected_content_hash="<the hash you read>", author="my-agent")
|
|
54053
|
+
\`\`\`
|
|
54054
|
+
|
|
54055
|
+
On a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.`,
|
|
54056
|
+
"Update Workflow (title-based -- fallback)": '## Update Workflow (title-based -- fallback)\n\n```\nsearch("topic") -> find doc (note its hash) -> modify ->\ningest(title="Same Title", content="...", update_if_exists=true,\n expected_content_hash="<the hash you read>", author="my-agent")\n```',
|
|
54047
54057
|
"Catch-Up Workflow": '## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```',
|
|
54048
|
-
"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.'
|
|
54058
|
+
"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>" --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_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.'
|
|
54049
54059
|
};
|
|
54050
54060
|
HELP_SECTION_HEADINGS = ["Tools", "Essential Rules", "Update Workflow (ID-based -- preferred)", "Update Workflow (title-based -- fallback)", "Catch-Up Workflow", "CLI fallback (when MCP is unavailable)"];
|
|
54051
54061
|
});
|
|
@@ -54265,6 +54275,21 @@ async function sha256hex(text) {
|
|
|
54265
54275
|
var MAX_CHUNK_CHARS = 4000;
|
|
54266
54276
|
|
|
54267
54277
|
// ../../_shared/mcp-tools/ingest.ts
|
|
54278
|
+
function conflictError(documentId, expectedHash, currentHash) {
|
|
54279
|
+
return new Error(`Conflict: document ${documentId} changed since you read it ` + `(your base hash: ${expectedHash}, current hash: ${currentHash}). ` + `To resolve: (1) cerefox_get_document("${documentId}") to fetch the latest content ` + `and its content_hash, (2) merge your changes into it, (3) retry cerefox_ingest ` + `with expected_content_hash set to the new hash. Do not overwrite blindly — ` + `the current content may include another writer's work.`);
|
|
54280
|
+
}
|
|
54281
|
+
function mapIngestRpcError(message, documentId) {
|
|
54282
|
+
if (message.includes("CEREFOX_CONFLICT")) {
|
|
54283
|
+
const current = message.match(/current hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
|
|
54284
|
+
const expected = message.match(/expected hash ([0-9a-f]{64})/)?.[1] ?? "unknown";
|
|
54285
|
+
return conflictError(documentId, expected, current);
|
|
54286
|
+
}
|
|
54287
|
+
if (message.includes("CEREFOX_TOKEN_REQUIRED")) {
|
|
54288
|
+
const current = message.match(/Current hash: ([0-9a-f]{64})/)?.[1];
|
|
54289
|
+
return new Error(`Concurrency token required: content updates need expected_content_hash — ` + `the content_hash of the version you based your edit on (returned by ` + `cerefox_get_document, cerefox_search, and cerefox_metadata_search).` + (current ? ` The document's current hash is ${current}; pass it ONLY if your edit was based on the current content.` : "") + ` If you have not read the document, read it first. To deliberately overwrite ` + `regardless of concurrent changes, pass last_write_wins=true.`);
|
|
54290
|
+
}
|
|
54291
|
+
return new Error(`Ingest RPC failed: ${message}`);
|
|
54292
|
+
}
|
|
54268
54293
|
async function handler4(supabase, args, ctx) {
|
|
54269
54294
|
const title = args.title?.trim();
|
|
54270
54295
|
const content = args.content;
|
|
@@ -54272,10 +54297,12 @@ async function handler4(supabase, args, ctx) {
|
|
|
54272
54297
|
const project_name = args.project_name;
|
|
54273
54298
|
const project_names_raw = args.project_names;
|
|
54274
54299
|
const source = args.source ?? "agent";
|
|
54275
|
-
const metadata = args.metadata ??
|
|
54300
|
+
const metadata = args.metadata ?? null;
|
|
54276
54301
|
const update_if_exists = args.update_if_exists ?? false;
|
|
54277
54302
|
const author = args.author ?? "mcp-agent";
|
|
54278
54303
|
const author_type = "agent";
|
|
54304
|
+
const expected_content_hash = args.expected_content_hash?.trim() || null;
|
|
54305
|
+
const last_write_wins = args.last_write_wins ?? false;
|
|
54279
54306
|
if (!title || !content?.trim()) {
|
|
54280
54307
|
throw new McpInvalidParams("title and content are required");
|
|
54281
54308
|
}
|
|
@@ -54296,7 +54323,10 @@ async function handler4(supabase, args, ctx) {
|
|
|
54296
54323
|
const existingDoc = existing[0];
|
|
54297
54324
|
if (existingDoc.content_hash === contentHash2) {
|
|
54298
54325
|
const note2 = update_if_exists ? "" : " Note: update_if_exists flag was overridden by document_id.";
|
|
54299
|
-
return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged.${note2}`;
|
|
54326
|
+
return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged (${contentHash2}).${note2}`;
|
|
54327
|
+
}
|
|
54328
|
+
if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
|
|
54329
|
+
throw conflictError(existingDoc.id, expected_content_hash, existingDoc.content_hash);
|
|
54300
54330
|
}
|
|
54301
54331
|
const chunks2 = chunkMarkdown2(content);
|
|
54302
54332
|
if (chunks2.length === 0)
|
|
@@ -54325,10 +54355,12 @@ ${c2.content}`);
|
|
|
54325
54355
|
p_chunks: chunkData2,
|
|
54326
54356
|
p_author: author,
|
|
54327
54357
|
p_author_type: author_type,
|
|
54328
|
-
p_source_label: source
|
|
54358
|
+
p_source_label: source,
|
|
54359
|
+
p_expected_content_hash: expected_content_hash,
|
|
54360
|
+
p_last_write_wins: last_write_wins
|
|
54329
54361
|
});
|
|
54330
54362
|
if (ingestErr2)
|
|
54331
|
-
throw
|
|
54363
|
+
throw mapIngestRpcError(ingestErr2.message, existingDoc.id);
|
|
54332
54364
|
logUsage(supabase, {
|
|
54333
54365
|
operation: "ingest",
|
|
54334
54366
|
accessPath: ctx.accessPath,
|
|
@@ -54342,14 +54374,17 @@ ${c2.content}`);
|
|
|
54342
54374
|
await ensureDocumentInProject(supabase, existingDoc.id, project_name);
|
|
54343
54375
|
}
|
|
54344
54376
|
const note = update_if_exists ? "" : " Note: update_if_exists flag was overridden by document_id.";
|
|
54345
|
-
return `Document updated: "${title}" (id: ${existingDoc.id}), ${chunks2.length} chunk(s), ${totalChars2} chars.${note}`;
|
|
54377
|
+
return `Document updated: "${title}" (id: ${existingDoc.id}), ${chunks2.length} chunk(s), ${totalChars2} chars. New content_hash: ${contentHash2}.${note}`;
|
|
54346
54378
|
}
|
|
54347
54379
|
if (update_if_exists) {
|
|
54348
54380
|
const { data: existing } = await supabase.from("cerefox_documents").select("id, title, content_hash").eq("title", title).order("updated_at", { ascending: false }).limit(1);
|
|
54349
54381
|
if (existing?.length) {
|
|
54350
54382
|
const existingDoc = existing[0];
|
|
54351
54383
|
if (existingDoc.content_hash === contentHash2) {
|
|
54352
|
-
return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged.`;
|
|
54384
|
+
return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged (${contentHash2}).`;
|
|
54385
|
+
}
|
|
54386
|
+
if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
|
|
54387
|
+
throw conflictError(existingDoc.id, expected_content_hash, existingDoc.content_hash);
|
|
54353
54388
|
}
|
|
54354
54389
|
const chunks2 = chunkMarkdown2(content);
|
|
54355
54390
|
if (chunks2.length === 0)
|
|
@@ -54378,10 +54413,12 @@ ${c2.content}`);
|
|
|
54378
54413
|
p_chunks: chunkData2,
|
|
54379
54414
|
p_author: author,
|
|
54380
54415
|
p_author_type: author_type,
|
|
54381
|
-
p_source_label: source
|
|
54416
|
+
p_source_label: source,
|
|
54417
|
+
p_expected_content_hash: expected_content_hash,
|
|
54418
|
+
p_last_write_wins: last_write_wins
|
|
54382
54419
|
});
|
|
54383
54420
|
if (ingestErr2)
|
|
54384
|
-
throw
|
|
54421
|
+
throw mapIngestRpcError(ingestErr2.message, existingDoc.id);
|
|
54385
54422
|
logUsage(supabase, {
|
|
54386
54423
|
operation: "ingest",
|
|
54387
54424
|
accessPath: ctx.accessPath,
|
|
@@ -54394,7 +54431,7 @@ ${c2.content}`);
|
|
|
54394
54431
|
} else if (project_name) {
|
|
54395
54432
|
await ensureDocumentInProject(supabase, existingDoc.id, project_name);
|
|
54396
54433
|
}
|
|
54397
|
-
return `Document updated: "${existingDoc.title}" (id: ${existingDoc.id}), ${chunks2.length} chunk(s), ${totalChars2} chars.`;
|
|
54434
|
+
return `Document updated: "${existingDoc.title}" (id: ${existingDoc.id}), ${chunks2.length} chunk(s), ${totalChars2} chars. New content_hash: ${contentHash2}.`;
|
|
54398
54435
|
}
|
|
54399
54436
|
}
|
|
54400
54437
|
const { data: hashMatch } = await supabase.from("cerefox_documents").select("id, title").eq("content_hash", contentHash2).limit(1);
|
|
@@ -54479,6 +54516,14 @@ var init_ingest = __esm(() => {
|
|
|
54479
54516
|
type: "boolean",
|
|
54480
54517
|
description: "When true, update an existing document with the same title instead of creating a new one (default: false). Ignored when document_id is provided."
|
|
54481
54518
|
},
|
|
54519
|
+
expected_content_hash: {
|
|
54520
|
+
type: "string",
|
|
54521
|
+
description: "REQUIRED on content updates (optimistic concurrency): the content_hash of the document version you based your edit on, as returned by cerefox_get_document / cerefox_search / cerefox_metadata_search. If the document changed since you read it, the update fails with a conflict — re-read, merge, retry with the new hash. Not needed when creating a new document."
|
|
54522
|
+
},
|
|
54523
|
+
last_write_wins: {
|
|
54524
|
+
type: "boolean",
|
|
54525
|
+
description: "Explicitly skip the concurrency check and overwrite regardless of concurrent changes (default: false). Use ONLY when an external source of truth makes conflicts meaningless (e.g. re-syncing from files). Recorded in the audit log."
|
|
54526
|
+
},
|
|
54482
54527
|
metadata: { type: "object", description: "Arbitrary JSON metadata (optional)" },
|
|
54483
54528
|
author: {
|
|
54484
54529
|
type: "string",
|
|
@@ -54666,8 +54711,10 @@ async function handler8(supabase, args, ctx) {
|
|
|
54666
54711
|
const parts = rows.map((row) => {
|
|
54667
54712
|
const projects = row.project_names?.length ? ` | projects: ${row.project_names.join(", ")}` : "";
|
|
54668
54713
|
const meta = Object.entries(row.doc_metadata ?? {}).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
54714
|
+
const hash = row.content_hash ? `
|
|
54715
|
+
hash: ${row.content_hash}` : "";
|
|
54669
54716
|
const header = `## ${row.title} [id: ${row.document_id}]
|
|
54670
|
-
` + `${meta}${projects} | ${row.total_chars} chars | ${row.review_status} | updated ${row.updated_at?.slice(0, 10) ?? "?"}`;
|
|
54717
|
+
` + `${meta}${projects} | ${row.total_chars} chars | ${row.review_status} | updated ${row.updated_at?.slice(0, 10) ?? "?"}${hash}`;
|
|
54671
54718
|
if (include_content && row.content) {
|
|
54672
54719
|
return `${header}
|
|
54673
54720
|
|
|
@@ -54809,7 +54856,9 @@ async function handler9(supabase, args, ctx) {
|
|
|
54809
54856
|
const docId = row.document_id ? ` [id: ${row.document_id}]` : "";
|
|
54810
54857
|
const score = row.best_score != null ? ` (score: ${row.best_score.toFixed(3)})` : "";
|
|
54811
54858
|
const partial = row.is_partial ? ` -- partial (${row.chunk_count} of ${(row.total_chars ?? 0).toLocaleString()} chars)` : "";
|
|
54812
|
-
|
|
54859
|
+
const hash = row.content_hash ? `
|
|
54860
|
+
hash: ${row.content_hash}` : "";
|
|
54861
|
+
return `## ${title}${docId}${score}${partial}${hash}
|
|
54813
54862
|
|
|
54814
54863
|
${row.full_content ?? ""}`;
|
|
54815
54864
|
});
|
|
@@ -55001,6 +55050,7 @@ async function runSyncSelfDocs(options = {}) {
|
|
|
55001
55050
|
topic: doc.topic
|
|
55002
55051
|
},
|
|
55003
55052
|
update_if_exists: true,
|
|
55053
|
+
last_write_wins: true,
|
|
55004
55054
|
project_name: project,
|
|
55005
55055
|
author,
|
|
55006
55056
|
author_type: authorType
|
|
@@ -74047,7 +74097,7 @@ import { homedir as homedir5 } from "node:os";
|
|
|
74047
74097
|
import { join as join8 } from "node:path";
|
|
74048
74098
|
|
|
74049
74099
|
// ../../_shared/ef-meta/index.ts
|
|
74050
|
-
var EF_VERSION = "0.
|
|
74100
|
+
var EF_VERSION = "0.11.1";
|
|
74051
74101
|
|
|
74052
74102
|
// src/cli/util/checks.ts
|
|
74053
74103
|
init_config();
|
|
@@ -74746,6 +74796,9 @@ async function action17(documentId, options) {
|
|
|
74746
74796
|
}
|
|
74747
74797
|
println(c.bold(`# ${doc.doc_title}`));
|
|
74748
74798
|
println(c.dim(`[${doc.document_id}] · chunks: ${doc.chunk_count} · chars: ${doc.total_chars}` + (doc.is_archived ? " · archived" : "") + (doc.version_id ? ` · version: ${doc.version_id}` : "")));
|
|
74799
|
+
if (doc.content_hash) {
|
|
74800
|
+
println(c.dim(`content_hash: ${doc.content_hash}`));
|
|
74801
|
+
}
|
|
74749
74802
|
println("");
|
|
74750
74803
|
println(doc.full_content);
|
|
74751
74804
|
}
|
|
@@ -75005,6 +75058,47 @@ async function resolveProjectIds(input, getOrCreateProject) {
|
|
|
75005
75058
|
}
|
|
75006
75059
|
return [];
|
|
75007
75060
|
}
|
|
75061
|
+
// src/ingestion/types.ts
|
|
75062
|
+
class ConcurrencyConflictError extends Error {
|
|
75063
|
+
documentId;
|
|
75064
|
+
currentHash;
|
|
75065
|
+
constructor(documentId, currentHash, message) {
|
|
75066
|
+
super(message);
|
|
75067
|
+
this.name = "ConcurrencyConflictError";
|
|
75068
|
+
this.documentId = documentId;
|
|
75069
|
+
this.currentHash = currentHash;
|
|
75070
|
+
}
|
|
75071
|
+
}
|
|
75072
|
+
|
|
75073
|
+
class ConcurrencyTokenRequiredError extends Error {
|
|
75074
|
+
constructor(message) {
|
|
75075
|
+
super(message);
|
|
75076
|
+
this.name = "ConcurrencyTokenRequiredError";
|
|
75077
|
+
}
|
|
75078
|
+
}
|
|
75079
|
+
var DEFAULT_PIPELINE_SETTINGS = {
|
|
75080
|
+
maxChunkChars: 4000,
|
|
75081
|
+
minChunkChars: 100,
|
|
75082
|
+
versionRetentionHours: 48,
|
|
75083
|
+
versionCleanupEnabled: true
|
|
75084
|
+
};
|
|
75085
|
+
function loadPipelineSettings() {
|
|
75086
|
+
const env4 = globalThis.process?.env ?? {};
|
|
75087
|
+
const intMin = (raw, def, min) => {
|
|
75088
|
+
if (raw === undefined || raw === "")
|
|
75089
|
+
return def;
|
|
75090
|
+
const n = Number.parseInt(raw, 10);
|
|
75091
|
+
return Number.isNaN(n) || n < min ? def : n;
|
|
75092
|
+
};
|
|
75093
|
+
const bool = (raw, def) => raw === undefined || raw === "" ? def : !/^(false|0|no|off)$/i.test(raw.trim());
|
|
75094
|
+
return {
|
|
75095
|
+
maxChunkChars: intMin(env4.CEREFOX_MAX_CHUNK_CHARS, DEFAULT_PIPELINE_SETTINGS.maxChunkChars, 1),
|
|
75096
|
+
minChunkChars: intMin(env4.CEREFOX_MIN_CHUNK_CHARS, DEFAULT_PIPELINE_SETTINGS.minChunkChars, 0),
|
|
75097
|
+
versionRetentionHours: intMin(env4.CEREFOX_VERSION_RETENTION_HOURS, DEFAULT_PIPELINE_SETTINGS.versionRetentionHours, 0),
|
|
75098
|
+
versionCleanupEnabled: bool(env4.CEREFOX_VERSION_CLEANUP_ENABLED, DEFAULT_PIPELINE_SETTINGS.versionCleanupEnabled)
|
|
75099
|
+
};
|
|
75100
|
+
}
|
|
75101
|
+
|
|
75008
75102
|
// src/ingestion/client-bridge.ts
|
|
75009
75103
|
class IngestionDbBridge {
|
|
75010
75104
|
supabase;
|
|
@@ -75057,9 +75151,22 @@ class IngestionDbBridge {
|
|
|
75057
75151
|
params.p_retention_hours = args.retentionHours;
|
|
75058
75152
|
if (args.cleanupEnabled !== undefined)
|
|
75059
75153
|
params.p_cleanup_enabled = args.cleanupEnabled;
|
|
75154
|
+
if (args.documentId !== null) {
|
|
75155
|
+
params.p_expected_content_hash = args.expectedContentHash ?? null;
|
|
75156
|
+
params.p_last_write_wins = args.lastWriteWins ?? false;
|
|
75157
|
+
}
|
|
75060
75158
|
const { data, error: error2 } = await this.supabase.rpc("cerefox_ingest_document", params);
|
|
75061
|
-
if (error2)
|
|
75062
|
-
|
|
75159
|
+
if (error2) {
|
|
75160
|
+
const msg = error2.message ?? JSON.stringify(error2);
|
|
75161
|
+
if (msg.includes("CEREFOX_CONFLICT")) {
|
|
75162
|
+
const current = msg.match(/current hash ([0-9a-f]{64})/)?.[1] ?? null;
|
|
75163
|
+
throw new ConcurrencyConflictError(args.documentId ?? "", current, msg);
|
|
75164
|
+
}
|
|
75165
|
+
if (msg.includes("CEREFOX_TOKEN_REQUIRED")) {
|
|
75166
|
+
throw new ConcurrencyTokenRequiredError(msg);
|
|
75167
|
+
}
|
|
75168
|
+
throw new Error(msg);
|
|
75169
|
+
}
|
|
75063
75170
|
if (Array.isArray(data) && data.length > 0) {
|
|
75064
75171
|
return data[0];
|
|
75065
75172
|
}
|
|
@@ -75164,30 +75271,6 @@ async function fileToMarkdown(filename, data) {
|
|
|
75164
75271
|
return data.toString("utf8");
|
|
75165
75272
|
}
|
|
75166
75273
|
|
|
75167
|
-
// src/ingestion/types.ts
|
|
75168
|
-
var DEFAULT_PIPELINE_SETTINGS = {
|
|
75169
|
-
maxChunkChars: 4000,
|
|
75170
|
-
minChunkChars: 100,
|
|
75171
|
-
versionRetentionHours: 48,
|
|
75172
|
-
versionCleanupEnabled: true
|
|
75173
|
-
};
|
|
75174
|
-
function loadPipelineSettings() {
|
|
75175
|
-
const env4 = globalThis.process?.env ?? {};
|
|
75176
|
-
const intMin = (raw, def, min) => {
|
|
75177
|
-
if (raw === undefined || raw === "")
|
|
75178
|
-
return def;
|
|
75179
|
-
const n = Number.parseInt(raw, 10);
|
|
75180
|
-
return Number.isNaN(n) || n < min ? def : n;
|
|
75181
|
-
};
|
|
75182
|
-
const bool = (raw, def) => raw === undefined || raw === "" ? def : !/^(false|0|no|off)$/i.test(raw.trim());
|
|
75183
|
-
return {
|
|
75184
|
-
maxChunkChars: intMin(env4.CEREFOX_MAX_CHUNK_CHARS, DEFAULT_PIPELINE_SETTINGS.maxChunkChars, 1),
|
|
75185
|
-
minChunkChars: intMin(env4.CEREFOX_MIN_CHUNK_CHARS, DEFAULT_PIPELINE_SETTINGS.minChunkChars, 0),
|
|
75186
|
-
versionRetentionHours: intMin(env4.CEREFOX_VERSION_RETENTION_HOURS, DEFAULT_PIPELINE_SETTINGS.versionRetentionHours, 0),
|
|
75187
|
-
versionCleanupEnabled: bool(env4.CEREFOX_VERSION_CLEANUP_ENABLED, DEFAULT_PIPELINE_SETTINGS.versionCleanupEnabled)
|
|
75188
|
-
};
|
|
75189
|
-
}
|
|
75190
|
-
|
|
75191
75274
|
// src/ingestion/pipeline.ts
|
|
75192
75275
|
class IngestionPipeline {
|
|
75193
75276
|
db;
|
|
@@ -75214,7 +75297,9 @@ class IngestionPipeline {
|
|
|
75214
75297
|
updateExisting = false,
|
|
75215
75298
|
documentId,
|
|
75216
75299
|
author = "unknown",
|
|
75217
|
-
authorType = "user"
|
|
75300
|
+
authorType = "user",
|
|
75301
|
+
expectedContentHash,
|
|
75302
|
+
lastWriteWins = false
|
|
75218
75303
|
} = opts;
|
|
75219
75304
|
const listFormProvided = projectIds !== undefined && projectIds !== null || projectNames !== undefined && projectNames !== null;
|
|
75220
75305
|
const getOrCreate = (name) => this.db.getOrCreateProject(name);
|
|
@@ -75235,7 +75320,9 @@ class IngestionPipeline {
|
|
|
75235
75320
|
projectIds: fullSetResolved,
|
|
75236
75321
|
metadata,
|
|
75237
75322
|
author,
|
|
75238
|
-
authorType
|
|
75323
|
+
authorType,
|
|
75324
|
+
expectedContentHash,
|
|
75325
|
+
lastWriteWins
|
|
75239
75326
|
});
|
|
75240
75327
|
if (!listFormProvided && (projectId || projectName)) {
|
|
75241
75328
|
const singular = await resolveProjectIds({ projectId, projectName }, getOrCreate);
|
|
@@ -75270,7 +75357,9 @@ class IngestionPipeline {
|
|
|
75270
75357
|
projectIds: fullSetResolved,
|
|
75271
75358
|
metadata,
|
|
75272
75359
|
author,
|
|
75273
|
-
authorType
|
|
75360
|
+
authorType,
|
|
75361
|
+
expectedContentHash,
|
|
75362
|
+
lastWriteWins
|
|
75274
75363
|
});
|
|
75275
75364
|
if (!listFormProvided && (projectId || projectName)) {
|
|
75276
75365
|
const singular = await resolveProjectIds({ projectId, projectName }, getOrCreate);
|
|
@@ -75356,7 +75445,9 @@ ${c2.content}`);
|
|
|
75356
75445
|
projectIds,
|
|
75357
75446
|
metadata,
|
|
75358
75447
|
author = "unknown",
|
|
75359
|
-
authorType = "user"
|
|
75448
|
+
authorType = "user",
|
|
75449
|
+
expectedContentHash,
|
|
75450
|
+
lastWriteWins = false
|
|
75360
75451
|
} = opts;
|
|
75361
75452
|
const existing = await this.db.getDocumentById(documentId);
|
|
75362
75453
|
if (!existing) {
|
|
@@ -75365,6 +75456,9 @@ ${c2.content}`);
|
|
|
75365
75456
|
const newHash = contentHash(text);
|
|
75366
75457
|
const contentUnchanged = newHash === existing.content_hash;
|
|
75367
75458
|
if (!contentUnchanged) {
|
|
75459
|
+
if (!lastWriteWins && expectedContentHash && expectedContentHash !== existing.content_hash) {
|
|
75460
|
+
throw new ConcurrencyConflictError(documentId, existing.content_hash, `CEREFOX_CONFLICT: document ${documentId} changed since it was read ` + `(expected hash ${expectedContentHash}, current hash ${existing.content_hash}). ` + `Re-read the document, merge your changes, and retry with the new hash.`);
|
|
75461
|
+
}
|
|
75368
75462
|
const collision = await this.db.getDocumentByHash(newHash);
|
|
75369
75463
|
if (collision && collision.id !== documentId) {
|
|
75370
75464
|
throw new Error(`Identical content already exists as document ${JSON.stringify(collision.title)}. ` + "Edit that document or change the content before saving.");
|
|
@@ -75465,7 +75559,9 @@ ${c2.content}`);
|
|
|
75465
75559
|
authorType,
|
|
75466
75560
|
sourceLabel: source,
|
|
75467
75561
|
retentionHours: this.settings.versionRetentionHours,
|
|
75468
|
-
cleanupEnabled: this.settings.versionCleanupEnabled
|
|
75562
|
+
cleanupEnabled: this.settings.versionCleanupEnabled,
|
|
75563
|
+
expectedContentHash: expectedContentHash ?? null,
|
|
75564
|
+
lastWriteWins
|
|
75469
75565
|
});
|
|
75470
75566
|
let finalProjectIds;
|
|
75471
75567
|
if (newProjectIds !== null) {
|
|
@@ -75539,7 +75635,7 @@ async function action18(path, options) {
|
|
|
75539
75635
|
if (author === "unknown") {
|
|
75540
75636
|
warn("No --author / CEREFOX_AUTHOR_NAME set — audit log will record this write as 'unknown'.");
|
|
75541
75637
|
}
|
|
75542
|
-
const metadata = parseJsonObjectArg(options.metadata, "--metadata")
|
|
75638
|
+
const metadata = parseJsonObjectArg(options.metadata, "--metadata");
|
|
75543
75639
|
let projectNames;
|
|
75544
75640
|
if (options.projectNames) {
|
|
75545
75641
|
projectNames = options.projectNames.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -75566,22 +75662,26 @@ async function action18(path, options) {
|
|
|
75566
75662
|
source: options.source ?? "cli",
|
|
75567
75663
|
projectName: options.projectName ?? null,
|
|
75568
75664
|
projectNames: projectNames ?? null,
|
|
75569
|
-
metadata,
|
|
75665
|
+
metadata: metadata ?? null,
|
|
75570
75666
|
updateExisting: Boolean(options.updateIfExists),
|
|
75571
75667
|
documentId: options.documentId ?? null,
|
|
75572
75668
|
author,
|
|
75573
|
-
authorType
|
|
75669
|
+
authorType,
|
|
75670
|
+
expectedContentHash: options.expectedContentHash ?? null,
|
|
75671
|
+
lastWriteWins: Boolean(options.lastWriteWins)
|
|
75574
75672
|
}) : await pipeline.ingestText({
|
|
75575
75673
|
text: content,
|
|
75576
75674
|
title,
|
|
75577
75675
|
source: options.source ?? "cli",
|
|
75578
75676
|
projectName: options.projectName ?? null,
|
|
75579
75677
|
projectNames: projectNames ?? null,
|
|
75580
|
-
metadata,
|
|
75678
|
+
metadata: metadata ?? null,
|
|
75581
75679
|
updateExisting: Boolean(options.updateIfExists),
|
|
75582
75680
|
documentId: options.documentId ?? null,
|
|
75583
75681
|
author,
|
|
75584
|
-
authorType
|
|
75682
|
+
authorType,
|
|
75683
|
+
expectedContentHash: options.expectedContentHash ?? null,
|
|
75684
|
+
lastWriteWins: Boolean(options.lastWriteWins)
|
|
75585
75685
|
});
|
|
75586
75686
|
let verb;
|
|
75587
75687
|
if (result.action === "created") {
|
|
@@ -75600,7 +75700,7 @@ async function action18(path, options) {
|
|
|
75600
75700
|
}
|
|
75601
75701
|
}
|
|
75602
75702
|
function registerIngest(program2) {
|
|
75603
|
-
program2.command("ingest").description("Ingest a file (or stdin paste) into the knowledge base.").argument("[path]", "Path to the file to ingest. Omit when using --paste.").option("--paste", "Read content from stdin instead of a file.").option("-t, --title <title>", "Document title (required with --paste; defaults to filename without extension).").option("-p, --project-name <name>", "Single project membership (non-destructive on update).").option("-P, --project-names <names>", "Comma-separated full project membership set (destructive replace on update).").option("-m, --metadata <json>", "JSON metadata object.").option("--source <label>", "Origin label (default: cli).", "cli").option("-u, --update-if-exists", "Update an existing doc with the same title.").option("-i, --document-id <uuid>", "Update a specific document by UUID (overrides --update-if-exists).").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(action18);
|
|
75703
|
+
program2.command("ingest").description("Ingest a file (or stdin paste) into the knowledge base.").argument("[path]", "Path to the file to ingest. Omit when using --paste.").option("--paste", "Read content from stdin instead of a file.").option("-t, --title <title>", "Document title (required with --paste; defaults to filename without extension).").option("-p, --project-name <name>", "Single project membership (non-destructive on update).").option("-P, --project-names <names>", "Comma-separated full project membership set (destructive replace on update).").option("-m, --metadata <json>", "JSON metadata object.").option("--source <label>", "Origin label (default: cli).", "cli").option("-u, --update-if-exists", "Update an existing doc with the same title.").option("-i, --document-id <uuid>", "Update a specific document by UUID (overrides --update-if-exists).").option("--expected-content-hash <sha256>", "Optimistic-concurrency token: the content_hash of the version this edit is based on (shown by `document get` / `search`). Required on content updates unless --last-write-wins.").option("--last-write-wins", "Skip the concurrency check and overwrite regardless of concurrent changes (recorded in the audit log).").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").action(action18);
|
|
75604
75704
|
}
|
|
75605
75705
|
|
|
75606
75706
|
// src/cli/commands/ingest-dir.ts
|
|
@@ -75648,7 +75748,7 @@ async function action19(dir, options) {
|
|
|
75648
75748
|
if (author === "unknown") {
|
|
75649
75749
|
warn("No --author / CEREFOX_AUTHOR_NAME set — audit log will record these writes as 'unknown'.");
|
|
75650
75750
|
}
|
|
75651
|
-
const metadata = parseJsonObjectArg(options.metadata, "--metadata")
|
|
75751
|
+
const metadata = parseJsonObjectArg(options.metadata, "--metadata");
|
|
75652
75752
|
const settings = loadSettings();
|
|
75653
75753
|
if (!settings.supabaseUrl || !settings.supabaseKey) {
|
|
75654
75754
|
throw userError("Supabase credentials not configured — run `cerefox init` first.");
|
|
@@ -75679,10 +75779,11 @@ async function action19(dir, options) {
|
|
|
75679
75779
|
title: basename3(file, extname4(file)),
|
|
75680
75780
|
source: options.source ?? "cli",
|
|
75681
75781
|
projectName: options.projectName ?? null,
|
|
75682
|
-
metadata,
|
|
75782
|
+
metadata: metadata ?? null,
|
|
75683
75783
|
updateExisting: Boolean(options.updateIfExists),
|
|
75684
75784
|
author,
|
|
75685
|
-
authorType
|
|
75785
|
+
authorType,
|
|
75786
|
+
lastWriteWins: true
|
|
75686
75787
|
});
|
|
75687
75788
|
outcomes.push({
|
|
75688
75789
|
file,
|
|
@@ -76346,9 +76447,9 @@ function registerMcp(program2) {
|
|
|
76346
76447
|
init_cli_core();
|
|
76347
76448
|
init_client();
|
|
76348
76449
|
async function action26(options) {
|
|
76349
|
-
const metadataFilter = parseJsonObjectArg(options.metadataFilter, "--metadata-filter");
|
|
76350
|
-
if (
|
|
76351
|
-
throw userError("
|
|
76450
|
+
const metadataFilter = parseJsonObjectArg(options.metadataFilter, "--metadata-filter") ?? {};
|
|
76451
|
+
if (Object.keys(metadataFilter).length === 0 && !options.projectName && !options.updatedSince && !options.createdSince) {
|
|
76452
|
+
throw userError("Provide at least one of: --metadata-filter, --project-name, --updated-since, or --created-since.", `Examples: --metadata-filter '{"type":"decision-log"}' · --project-name "research" (lists that project's docs).`);
|
|
76352
76453
|
}
|
|
76353
76454
|
const limit = parsePositiveInt(options.limit, "--limit", 10);
|
|
76354
76455
|
const maxBytes = parseNonNegativeInt(options.maxBytes, "--max-bytes", 200000);
|
|
@@ -76408,7 +76509,7 @@ async function action26(options) {
|
|
|
76408
76509
|
}
|
|
76409
76510
|
}
|
|
76410
76511
|
function registerMetadataSearch(program2) {
|
|
76411
|
-
program2.command("metadata-search").description("Find documents by metadata criteria (no text query).").
|
|
76512
|
+
program2.command("metadata-search").description("Find or list documents by metadata, project, or time criteria (no text query).").option("-f, --metadata-filter <json>", "JSON object; only docs whose metadata contains ALL pairs are returned. Optional — omit to list by --project-name / time range alone (at least one criterion is required).").option("-p, --project-name <name>", "Filter to a specific project.").option("--updated-since <iso>", "Only docs updated on/after this ISO timestamp.").option("--created-since <iso>", "Only docs created on/after this ISO timestamp.").option("--include-content", "Include full document text in results.").option("-l, --limit <n>", "Maximum docs to return.", "10").option("--max-bytes <n>", "Response size budget in bytes (with --include-content).", "200000").option("-r, --requestor <name>", "Agent / user name (usage log).").option("--json", "Emit machine-readable JSON.").action(action26);
|
|
76412
76513
|
}
|
|
76413
76514
|
|
|
76414
76515
|
// src/cli/commands/reindex.ts
|
|
@@ -76728,10 +76829,12 @@ async function action29(query, options) {
|
|
|
76728
76829
|
println(c.bold(`## ${title}${docId}${score}${counts}${kind}`));
|
|
76729
76830
|
const bestMatch = doc2.best_chunk_heading_path?.length ? doc2.best_chunk_heading_path.join(" › ") : null;
|
|
76730
76831
|
const updated = doc2.doc_updated_at ? doc2.doc_updated_at.slice(0, 10) : null;
|
|
76731
|
-
|
|
76832
|
+
const hash = doc2.content_hash ? `hash: ${doc2.content_hash}` : null;
|
|
76833
|
+
if (bestMatch || updated || hash) {
|
|
76732
76834
|
const bits = [
|
|
76733
76835
|
bestMatch ? `best match: ${bestMatch}` : null,
|
|
76734
|
-
updated ? `updated ${updated}` : null
|
|
76836
|
+
updated ? `updated ${updated}` : null,
|
|
76837
|
+
hash
|
|
76735
76838
|
].filter(Boolean);
|
|
76736
76839
|
println(c.dim(` ${bits.join(" · ")}`));
|
|
76737
76840
|
}
|
|
@@ -80801,6 +80904,7 @@ function registerDocumentReadRoutes(app, ctx) {
|
|
|
80801
80904
|
created_at: meta ? meta.created_at ?? null : null,
|
|
80802
80905
|
updated_at: meta ? meta.updated_at ?? null : null,
|
|
80803
80906
|
deleted_at: meta ? meta.deleted_at ?? null : null,
|
|
80907
|
+
content_hash: meta ? meta.content_hash ?? null : null,
|
|
80804
80908
|
versions: versions3.map((v) => ({
|
|
80805
80909
|
version_id: v.version_id,
|
|
80806
80910
|
version_number: v.version_number,
|
|
@@ -80963,10 +81067,26 @@ function registerDocumentWriteRoutes(app, ctx) {
|
|
|
80963
81067
|
projectIds: Array.isArray(body.project_ids) ? body.project_ids : undefined,
|
|
80964
81068
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
80965
81069
|
author: "web-ui",
|
|
80966
|
-
authorType: "user"
|
|
81070
|
+
authorType: "user",
|
|
81071
|
+
expectedContentHash: typeof body.expected_content_hash === "string" ? body.expected_content_hash : null
|
|
80967
81072
|
});
|
|
80968
81073
|
return c2.json({ success: true, reindexed: result.reindexed });
|
|
80969
81074
|
} catch (err) {
|
|
81075
|
+
if (err instanceof ConcurrencyConflictError) {
|
|
81076
|
+
return c2.json({
|
|
81077
|
+
success: false,
|
|
81078
|
+
error: "conflict",
|
|
81079
|
+
message: "This document changed while you were editing it (another writer saved a newer version). Open it again in a new tab, merge your changes, and save from there.",
|
|
81080
|
+
current_hash: err.currentHash
|
|
81081
|
+
}, 409);
|
|
81082
|
+
}
|
|
81083
|
+
if (err instanceof ConcurrencyTokenRequiredError) {
|
|
81084
|
+
return c2.json({
|
|
81085
|
+
success: false,
|
|
81086
|
+
error: "expected_content_hash required",
|
|
81087
|
+
message: err.message
|
|
81088
|
+
}, 400);
|
|
81089
|
+
}
|
|
80970
81090
|
return c2.json({
|
|
80971
81091
|
success: false,
|
|
80972
81092
|
error: err instanceof Error ? err.message : String(err)
|