@cerefox/memory 1.0.2 → 1.0.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.
@@ -28,7 +28,8 @@ Cerefox is a persistent, shared knowledge base. You have **10 MCP tools** (9 of
28
28
  7. **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.
29
29
  8. **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.
30
30
  9. **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.
31
- 10. **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`.
31
+ 10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.
32
+ 11. **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`.
32
33
 
33
34
  ## Update Workflow (ID-based -- preferred)
34
35
 
@@ -7184,7 +7184,7 @@ var exports_meta = {};
7184
7184
  __export(exports_meta, {
7185
7185
  PKG_VERSION: () => PKG_VERSION
7186
7186
  });
7187
- var PKG_VERSION = "1.0.2";
7187
+ var PKG_VERSION = "1.0.4";
7188
7188
  var init_meta = () => {};
7189
7189
 
7190
7190
  // ../../node_modules/.bun/tslib@2.8.1/node_modules/tslib/tslib.js
@@ -25225,6 +25225,13 @@ function getMinSearchScore() {
25225
25225
  const n = Number.parseFloat(raw);
25226
25226
  return Number.isNaN(n) || n < 0 || n > 1 ? fallback : n;
25227
25227
  }
25228
+ function getMinTermCoverage() {
25229
+ const raw = globalThis.process?.env?.CEREFOX_MIN_TERM_COVERAGE;
25230
+ if (raw === undefined || raw === "")
25231
+ return;
25232
+ const n = Number.parseFloat(raw);
25233
+ return Number.isNaN(n) || n < 0 || n > 1 ? undefined : n;
25234
+ }
25228
25235
  function applyByteBudget(rows, maxBytes) {
25229
25236
  const accepted = [];
25230
25237
  let usedBytes = 0;
@@ -54966,11 +54973,11 @@ var init_get_document = __esm(() => {
54966
54973
  });
54967
54974
 
54968
54975
  // ../../_shared/mcp-tools/get-help-content.ts
54969
- 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`, …).\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;
54976
+ 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. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch("topic") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title="Same Title", content="...", document_id="abc123",\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch("topic") -> find doc (note its hash) -> modify ->\ningest(title="Same Title", content="...", update_if_exists=true,\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search "<q>" --requestor "<your-name>"` |\n| `cerefox_ingest` (paste) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --author "<your-name>" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf \'...\' \\| cerefox document ingest --paste --title "<t>" --document-id "<uuid>" --expected-content-hash "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor "<your-name>"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor "<your-name>"` |\n| `cerefox_list_projects` | `cerefox project list --requestor "<your-name>"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_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;
54970
54977
  var init_get_help_content = __esm(() => {
54971
54978
  HELP_SECTIONS = {
54972
54979
  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) |",
54973
- "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`.',
54980
+ "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. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **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`.',
54974
54981
  "Update Workflow (ID-based -- preferred)": `## Update Workflow (ID-based -- preferred)
54975
54982
 
54976
54983
  \`\`\`
@@ -55572,6 +55579,8 @@ async function handler9(supabase, args, ctx) {
55572
55579
  const mode = args.mode ?? "docs";
55573
55580
  const alpha = args.alpha ?? 0.7;
55574
55581
  const min_score = args.min_score ?? getMinSearchScore();
55582
+ const min_term_coverage = args.min_term_coverage ?? getMinTermCoverage();
55583
+ const coverageParam = min_term_coverage !== undefined ? { p_min_term_coverage: min_term_coverage } : {};
55575
55584
  const metadata_filter = args.metadata_filter ?? null;
55576
55585
  const requested_max_bytes = args.max_bytes;
55577
55586
  const ceiling = getMaxResponseBytes();
@@ -55603,7 +55612,8 @@ async function handler9(supabase, args, ctx) {
55603
55612
  p_query_text: query,
55604
55613
  p_match_count: match_count,
55605
55614
  p_project_id: projectId,
55606
- ...metaFilterParam
55615
+ ...metaFilterParam,
55616
+ ...coverageParam
55607
55617
  };
55608
55618
  } else if (mode === "hybrid") {
55609
55619
  rpcName = "cerefox_hybrid_search";
@@ -55615,7 +55625,8 @@ async function handler9(supabase, args, ctx) {
55615
55625
  p_use_upgrade: false,
55616
55626
  p_project_id: projectId,
55617
55627
  p_min_score: min_score,
55618
- ...metaFilterParam
55628
+ ...metaFilterParam,
55629
+ ...coverageParam
55619
55630
  };
55620
55631
  } else {
55621
55632
  rpcName = "cerefox_search_docs";
@@ -55626,7 +55637,8 @@ async function handler9(supabase, args, ctx) {
55626
55637
  p_alpha: alpha,
55627
55638
  p_project_id: projectId,
55628
55639
  p_min_score: min_score,
55629
- ...metaFilterParam
55640
+ ...metaFilterParam,
55641
+ ...coverageParam
55630
55642
  };
55631
55643
  }
55632
55644
  const { data, error: error2 } = await supabase.rpc(rpcName, rpcParams);
@@ -55644,10 +55656,12 @@ async function handler9(supabase, args, ctx) {
55644
55656
  if (accepted.length === 0)
55645
55657
  return "No results found.";
55646
55658
  const rows = accepted;
55659
+ const belowConfidence = rows.length > 0 && rows.every((r) => r.below_confidence === true);
55647
55660
  const parts = rows.map((row) => {
55648
55661
  const title = row.doc_title ?? "Untitled";
55649
55662
  const docId = row.document_id ? ` [id: ${row.document_id}]` : "";
55650
- const score = row.best_score != null ? ` (score: ${row.best_score.toFixed(3)})` : "";
55663
+ const rawScore = row.best_score ?? row.score;
55664
+ const score = rawScore != null ? ` (score: ${rawScore.toFixed(3)})` : "";
55651
55665
  const partial = row.is_partial ? ` -- partial (${row.chunk_count} of ${(row.total_chars ?? 0).toLocaleString()} chars)` : "";
55652
55666
  const hash = row.content_hash ? `
55653
55667
  hash: ${row.content_hash}` : "";
@@ -55660,6 +55674,11 @@ ${row.full_content ?? ""}`;
55660
55674
  ---
55661
55675
 
55662
55676
  `);
55677
+ if (belowConfidence) {
55678
+ output = `⚠ No results cleared the confidence threshold. Showing the closest ${rows.length} ` + `candidate(s) with scores — judge relevance yourself; a low score means weak signal, ` + `not necessarily absent knowledge.
55679
+
55680
+ ` + output;
55681
+ }
55663
55682
  if (truncated) {
55664
55683
  output += `
55665
55684
 
@@ -55692,6 +55711,23 @@ var init_search = __esm(() => {
55692
55711
  description: 'Optional JSONB containment filter. Only documents whose metadata contains ALL specified key-value pairs are returned. Example: {"type": "decision", "status": "active"}. Call cerefox_list_metadata_keys first to discover available keys and values. Omit to search all documents.',
55693
55712
  additionalProperties: { type: "string" }
55694
55713
  },
55714
+ mode: {
55715
+ type: "string",
55716
+ enum: ["docs", "hybrid", "fts", "semantic"],
55717
+ description: "Search mode (default: docs — full reconstructed documents). hybrid: ranked chunks; fts: keyword-only (no embedding); semantic: vector-only."
55718
+ },
55719
+ alpha: {
55720
+ type: "number",
55721
+ description: "Hybrid fusion weight 0–1 (default 0.7): 1 = pure semantic, 0 = pure keyword."
55722
+ },
55723
+ min_score: {
55724
+ type: "number",
55725
+ description: "Minimum cosine similarity for vector-side results (default: server-configured, 0.5 OpenAI / 0.6 local embedder)."
55726
+ },
55727
+ min_term_coverage: {
55728
+ type: "number",
55729
+ description: "Keyword OR-fallback confidence bar 0–1 (default 0.5): fraction of the query's meaningful terms a result must match to count as a confident hit; weaker matches return flagged below-confidence. 0 = any matching term. Needs schema ≥ 0.9.1."
55730
+ },
55695
55731
  max_bytes: {
55696
55732
  type: "integer",
55697
55733
  description: "Optional response size budget in bytes. Results are dropped whole until the budget is satisfied; a truncated flag is set when results are dropped. Defaults to the server maximum (200000). Pass a smaller value if your context window is limited. Values above the server maximum are silently capped."
@@ -69025,10 +69061,29 @@ init_cli_core();
69025
69061
 
69026
69062
  // src/cli/commands/backup.ts
69027
69063
  init_cli_core();
69028
- init_client();
69029
69064
  import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
69030
69065
  import { homedir as homedir2 } from "node:os";
69031
69066
  import { join as join2, resolve } from "node:path";
69067
+
69068
+ // ../../_shared/db-client/paginate.ts
69069
+ async function fetchAllPages(makeQuery, batchSize = 200) {
69070
+ const results = [];
69071
+ let offset = 0;
69072
+ for (;; ) {
69073
+ const { data, error } = await makeQuery(offset, offset + batchSize - 1);
69074
+ if (error)
69075
+ throw new Error(error.message ?? JSON.stringify(error));
69076
+ const page = data ?? [];
69077
+ results.push(...page);
69078
+ if (page.length < batchSize)
69079
+ break;
69080
+ offset += batchSize;
69081
+ }
69082
+ return results;
69083
+ }
69084
+
69085
+ // src/cli/commands/backup.ts
69086
+ init_client();
69032
69087
  function expandHome(path) {
69033
69088
  if (path === "~")
69034
69089
  return homedir2();
@@ -69049,21 +69104,25 @@ async function action(options) {
69049
69104
  const filename = `cerefox-${stamp}${options.label ? "-" + options.label : ""}.json`;
69050
69105
  const dest = join2(outDir, filename);
69051
69106
  const client = getClient();
69052
- const { data: docsData, error: docsErr } = await client.raw.from("cerefox_documents").select("id, title, content_hash, source, metadata, total_chars, chunk_count, " + "review_status, created_at, updated_at, deleted_at").is("deleted_at", null).order("created_at", { ascending: true });
69053
- if (docsErr)
69054
- throw systemError(`Document fetch failed: ${docsErr.message}`);
69055
- const docs = docsData ?? [];
69107
+ let docs;
69108
+ try {
69109
+ docs = await fetchAllPages((from, to) => client.raw.from("cerefox_documents").select("id, title, content_hash, source, metadata, total_chars, chunk_count, " + "review_status, created_at, updated_at, deleted_at").is("deleted_at", null).order("created_at", { ascending: true }).order("id", { ascending: true }).range(from, to));
69110
+ } catch (err) {
69111
+ throw systemError(`Document fetch failed: ${err instanceof Error ? err.message : String(err)}`);
69112
+ }
69056
69113
  let chunkTotal = 0;
69057
69114
  const enriched = [];
69058
69115
  for (let i = 0;i < docs.length; i++) {
69059
69116
  const doc = docs[i];
69060
69117
  const docId = doc.id;
69061
- const { data: chunks, error: chunkErr } = await client.raw.from("cerefox_chunks").select("*").eq("document_id", docId).is("version_id", null).order("chunk_index", { ascending: true });
69062
- if (chunkErr) {
69063
- throw systemError(`Chunk fetch failed for ${docId}: ${chunkErr.message}`);
69118
+ let chunks;
69119
+ try {
69120
+ chunks = await fetchAllPages((from, to) => client.raw.from("cerefox_chunks").select("*").eq("document_id", docId).is("version_id", null).order("chunk_index", { ascending: true }).range(from, to));
69121
+ } catch (err) {
69122
+ throw systemError(`Chunk fetch failed for ${docId}: ${err instanceof Error ? err.message : String(err)}`);
69064
69123
  }
69065
- chunkTotal += (chunks ?? []).length;
69066
- enriched.push({ ...doc, chunks: chunks ?? [] });
69124
+ chunkTotal += chunks.length;
69125
+ enriched.push({ ...doc, chunks });
69067
69126
  if (process.stdout.isTTY) {
69068
69127
  process.stderr.write(`\r Dumping documents: ${i + 1}/${docs.length} (${chunkTotal} chunks so far)…`);
69069
69128
  }
@@ -74941,7 +75000,8 @@ import { homedir as homedir6 } from "node:os";
74941
75000
  import { join as join9 } from "node:path";
74942
75001
 
74943
75002
  // ../../_shared/ef-meta/index.ts
74944
- var EF_VERSION = "1.0.2";
75003
+ var EF_VERSION = "1.0.4";
75004
+ var EF_LAST_CHANGED = "1.0.4";
74945
75005
 
74946
75006
  // src/cli/util/checks.ts
74947
75007
  init_config();
@@ -75458,6 +75518,13 @@ async function checkEdgeFunctionsCompat() {
75458
75518
  hint: "Update the Edge Functions (see remediation below)."
75459
75519
  };
75460
75520
  case "above-min-but-old":
75521
+ if (compareSemver(deployed, EF_LAST_CHANGED) >= 0) {
75522
+ return {
75523
+ name: "edge functions",
75524
+ status: "ok",
75525
+ detail: `Deployed EF v${deployed} (≥ required v${compat.edgeFunctions.min}; ` + `matches the latest server-side changes — the newer bundled label ` + `v${EF_VERSION} is cosmetic).`
75526
+ };
75527
+ }
75461
75528
  return {
75462
75529
  name: "edge functions",
75463
75530
  status: "skipped",
@@ -77224,18 +77291,22 @@ async function action27(options) {
77224
77291
  }
77225
77292
  const reindexAll = Boolean(options.all);
77226
77293
  const dryRun = Boolean(options.dryRun);
77227
- let query = supabase.from("cerefox_chunks").select("id, document_id, content, embedder_primary, cerefox_documents(title)").is("version_id", null);
77228
- if (options.documentId) {
77229
- query = query.eq("document_id", options.documentId);
77230
- }
77231
77294
  const targetModel = activeEmbedderName();
77232
- if (!reindexAll) {
77233
- query = query.neq("embedder_primary", targetModel);
77295
+ let chunks;
77296
+ try {
77297
+ chunks = await fetchAllPages((from, to) => {
77298
+ let query = supabase.from("cerefox_chunks").select("id, document_id, content, embedder_primary, cerefox_documents(title)").is("version_id", null);
77299
+ if (options.documentId) {
77300
+ query = query.eq("document_id", options.documentId);
77301
+ }
77302
+ if (!reindexAll) {
77303
+ query = query.neq("embedder_primary", targetModel);
77304
+ }
77305
+ return query.order("id", { ascending: true }).range(from, to);
77306
+ }, 1000);
77307
+ } catch (err) {
77308
+ throw systemError(`Failed to list chunks: ${err instanceof Error ? err.message : String(err)}`);
77234
77309
  }
77235
- const { data, error: error3 } = await query;
77236
- if (error3)
77237
- throw systemError(`Failed to list chunks: ${error3.message}`);
77238
- const chunks = data ?? [];
77239
77310
  if (chunks.length === 0) {
77240
77311
  println(c.dim("(nothing to reindex)"));
77241
77312
  return;
@@ -77405,6 +77476,8 @@ async function action29(query, options) {
77405
77476
  const matchCount = parsePositiveInt(options.matchCount, "--match-count", 5);
77406
77477
  const alpha = parseFloat01(options.alpha, "--alpha", 0.7);
77407
77478
  const minScore = parseFloat01(options.minScore, "--min-score", getMinSearchScore());
77479
+ const envCoverage = getMinTermCoverage();
77480
+ const coverageParam = options.minTermCoverage !== undefined ? { p_min_term_coverage: parseFloat01(options.minTermCoverage, "--min-term-coverage", envCoverage ?? 0.5) } : envCoverage !== undefined ? { p_min_term_coverage: envCoverage } : {};
77408
77481
  const maxBytes = parseNonNegativeInt(options.maxBytes, "--max-bytes", getMaxResponseBytes());
77409
77482
  const mode = options.mode ?? "docs";
77410
77483
  if (!["docs", "hybrid", "fts"].includes(mode)) {
@@ -77435,7 +77508,8 @@ async function action29(query, options) {
77435
77508
  p_query_text: query,
77436
77509
  p_match_count: matchCount,
77437
77510
  p_project_id: projectId,
77438
- ...metaFilterParam
77511
+ ...metaFilterParam,
77512
+ ...coverageParam
77439
77513
  };
77440
77514
  } else if (mode === "hybrid") {
77441
77515
  rpcName = "cerefox_hybrid_search";
@@ -77447,7 +77521,8 @@ async function action29(query, options) {
77447
77521
  p_use_upgrade: false,
77448
77522
  p_project_id: projectId,
77449
77523
  p_min_score: minScore,
77450
- ...metaFilterParam
77524
+ ...metaFilterParam,
77525
+ ...coverageParam
77451
77526
  };
77452
77527
  } else {
77453
77528
  rpcName = "cerefox_search_docs";
@@ -77458,7 +77533,8 @@ async function action29(query, options) {
77458
77533
  p_alpha: alpha,
77459
77534
  p_project_id: projectId,
77460
77535
  p_min_score: minScore,
77461
- ...metaFilterParam
77536
+ ...metaFilterParam,
77537
+ ...coverageParam
77462
77538
  };
77463
77539
  }
77464
77540
  const results = await client.rpc(rpcName, rpcParams);
@@ -77509,6 +77585,11 @@ async function action29(query, options) {
77509
77585
  println("No results found.");
77510
77586
  return;
77511
77587
  }
77588
+ const belowConfidence = accepted.length > 0 && accepted.every((r) => r.below_confidence === true);
77589
+ if (belowConfidence) {
77590
+ println(c.yellow(`⚠ No results cleared the confidence threshold — showing the closest ` + `${accepted.length} candidate(s). Judge relevance by the scores below.`));
77591
+ println("");
77592
+ }
77512
77593
  for (const row of accepted) {
77513
77594
  if (mode === "docs") {
77514
77595
  const doc2 = row;
@@ -77552,7 +77633,7 @@ async function action29(query, options) {
77552
77633
  }
77553
77634
  }
77554
77635
  function registerSearch(program2) {
77555
- program2.command("search").description("Search the knowledge base (hybrid FTS + semantic).").argument("<query>", "Natural-language search query.").option("-c, --match-count <n>", "Maximum number of documents to return.", "5").option("-p, --project-name <name>", "Filter results to a specific project.").option("-f, --metadata-filter <json>", "JSON containment filter; only docs whose metadata contains ALL pairs are returned.").option("--mode <mode>", "Search mode: docs (default), hybrid, fts.", "docs").option("--alpha <float>", "Semantic weight 0..1 (default: 0.7).", "0.7").option("--min-score <float>", "Minimum cosine similarity threshold (default: CEREFOX_MIN_SEARCH_SCORE; else 0.5, or 0.6 with the local embedder).").option("--max-bytes <n>", "Response size budget in bytes (default: CEREFOX_MAX_RESPONSE_BYTES or 200000).").option("-r, --requestor <name>", "Agent / user name (recorded in usage log).").option("--json", "Emit machine-readable JSON instead of the default text.").option("--only-metadata", "List matching docs (id, score, chunks, chars, partial/full) WITHOUT their content — like the web UI's collapsed result list. Grab a [id:…] then `cerefox document get <id>`.").action(action29);
77636
+ program2.command("search").description("Search the knowledge base (hybrid FTS + semantic).").argument("<query>", "Natural-language search query.").option("-c, --match-count <n>", "Maximum number of documents to return.", "5").option("-p, --project-name <name>", "Filter results to a specific project.").option("-f, --metadata-filter <json>", "JSON containment filter; only docs whose metadata contains ALL pairs are returned.").option("--mode <mode>", "Search mode: docs (default), hybrid, fts.", "docs").option("--alpha <float>", "Semantic weight 0..1 (default: 0.7).", "0.7").option("--min-score <float>", "Minimum cosine similarity threshold (default: CEREFOX_MIN_SEARCH_SCORE; else 0.5, or 0.6 with the local embedder).").option("--min-term-coverage <float>", "OR-fallback keyword matches must cover at least this fraction of the query's meaningful terms to count as confident hits (default: CEREFOX_MIN_TERM_COVERAGE; else the server default 0.5; needs schema ≥ 0.9.1).").option("--max-bytes <n>", "Response size budget in bytes (default: CEREFOX_MAX_RESPONSE_BYTES or 200000).").option("-r, --requestor <name>", "Agent / user name (recorded in usage log).").option("--json", "Emit machine-readable JSON instead of the default text.").option("--only-metadata", "List matching docs (id, score, chunks, chars, partial/full) WITHOUT their content — like the web UI's collapsed result list. Grab a [id:…] then `cerefox document get <id>`.").action(action29);
77556
77637
  }
77557
77638
 
77558
77639
  // src/cli/commands/self-update.ts
@@ -81424,10 +81505,8 @@ async function getProjectDocCounts(ctx, projectIds) {
81424
81505
  if (projectIds.length === 0)
81425
81506
  return { active, deleted };
81426
81507
  try {
81427
- const { data, error: error3 } = await ctx.supabase.from("cerefox_document_projects").select("project_id, cerefox_documents(deleted_at)").in("project_id", projectIds);
81428
- if (error3)
81429
- throw error3;
81430
- for (const row of data ?? []) {
81508
+ const rows = await fetchAllPages((from, to) => ctx.supabase.from("cerefox_document_projects").select("project_id, cerefox_documents(deleted_at)").in("project_id", projectIds).order("document_id", { ascending: true }).order("project_id", { ascending: true }).range(from, to));
81509
+ for (const row of rows) {
81431
81510
  const pid = row.project_id;
81432
81511
  if (!(pid in active))
81433
81512
  continue;
@@ -81471,16 +81550,13 @@ async function getRecentDocAuthors(ctx, docIds) {
81471
81550
  return out;
81472
81551
  }
81473
81552
  async function countDocumentsForProject(ctx, projectId) {
81474
- const { data, error: error3 } = await ctx.supabase.from("cerefox_document_projects").select("document_id, cerefox_documents(deleted_at)").eq("project_id", projectId);
81553
+ const { count, error: error3 } = await ctx.supabase.from("cerefox_document_projects").select("document_id, cerefox_documents!inner(deleted_at)", {
81554
+ count: "exact",
81555
+ head: true
81556
+ }).eq("project_id", projectId).is("cerefox_documents.deleted_at", null);
81475
81557
  if (error3)
81476
81558
  throw error3;
81477
- let n = 0;
81478
- for (const row of data ?? []) {
81479
- if (row.cerefox_documents && row.cerefox_documents.deleted_at === null) {
81480
- n += 1;
81481
- }
81482
- }
81483
- return n;
81559
+ return count ?? 0;
81484
81560
  }
81485
81561
  function dashboardDocFromRow(row, projectIds) {
81486
81562
  return {