@cerefox/memory 1.0.6 → 1.1.0-beta.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_QUICK_REFERENCE.md +11 -2
- package/dist/bin/cerefox.js +808 -184
- package/dist/frontend/assets/index-BMFGsK0D.js +121 -0
- package/dist/frontend/assets/index-BMFGsK0D.js.map +1 -0
- package/dist/frontend/assets/index-C1JXZA9m.css +1 -0
- package/dist/frontend/index.html +2 -2
- package/dist/server-assets/_shared/ef-meta/index.ts +2 -2
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +26 -0
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +4 -4
- package/dist/server-assets/_shared/mcp-tools/index.ts +11 -0
- package/dist/server-assets/_shared/mcp-tools/relations.ts +284 -0
- package/dist/server-assets/_shared/mcp-tools/search.ts +14 -8
- package/dist/server-assets/db/migrations/0014_document_relations.sql +59 -0
- package/dist/server-assets/db/rpcs.sql +292 -14
- package/dist/server-assets/db/schema.sql +35 -2
- package/docs/guides/configuration.md +15 -0
- package/docs/guides/setup-local.md +3 -1
- package/docs/guides/upgrading.md +1 -1
- package/package.json +3 -3
- package/dist/frontend/assets/index-Asx5wD7g.css +0 -1
- package/dist/frontend/assets/index-VeqA60-v.js +0 -125
- package/dist/frontend/assets/index-VeqA60-v.js.map +0 -1
package/dist/frontend/index.html
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap"
|
|
16
16
|
/>
|
|
17
17
|
<title>Cerefox</title>
|
|
18
|
-
<script type="module" crossorigin src="/app/assets/index-
|
|
19
|
-
<link rel="stylesheet" crossorigin href="/app/assets/index-
|
|
18
|
+
<script type="module" crossorigin src="/app/assets/index-BMFGsK0D.js"></script>
|
|
19
|
+
<link rel="stylesheet" crossorigin href="/app/assets/index-C1JXZA9m.css">
|
|
20
20
|
</head>
|
|
21
21
|
<body>
|
|
22
22
|
<div id="root"></div>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* doesn't touch `supabase/functions/` leaves it alone).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
export const EF_VERSION = "1.0.
|
|
21
|
+
export const EF_VERSION = "1.1.0-beta.1";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* The most recent version whose EF-side SOURCE actually changed (#127).
|
|
@@ -28,7 +28,7 @@ export const EF_VERSION = "1.0.6";
|
|
|
28
28
|
* `cut_release.ts` ONLY when EF source changed since the last tag; doctor
|
|
29
29
|
* uses it to stay silent on label-only drift.
|
|
30
30
|
*/
|
|
31
|
-
export const EF_LAST_CHANGED = "1.0.
|
|
31
|
+
export const EF_LAST_CHANGED = "1.1.0-beta.1";
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* The 8 peer EFs the cerefox-mcp aggregator probes (excludes cerefox-mcp
|
|
@@ -89,6 +89,32 @@ export function getSearchAlpha(): number {
|
|
|
89
89
|
return readUnitInterval("CEREFOX_SEARCH_ALPHA") ?? DEFAULT_SEARCH_ALPHA;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The retrieval tunables a client should actually SEND to the RPCs (#133).
|
|
94
|
+
*
|
|
95
|
+
* Returns undefined when the operator has expressed no preference, so the
|
|
96
|
+
* parameter is omitted and the server resolves it: `cerefox_config` first,
|
|
97
|
+
* then the built-in default. That is what lets one `cerefox config set` govern
|
|
98
|
+
* every access path. When a value IS configured here it wins, preserving the
|
|
99
|
+
* chain: per-call argument > client env > deployment config > built-in.
|
|
100
|
+
*
|
|
101
|
+
* `min_search_score` has one subtlety: the local embedder needs a higher floor
|
|
102
|
+
* (nomic scores unrelated text ~0.4–0.55), so an explicitly local embedder
|
|
103
|
+
* counts as "configured" even when CEREFOX_MIN_SEARCH_SCORE is unset —
|
|
104
|
+
* otherwise omitting the parameter would silently apply the OpenAI-calibrated
|
|
105
|
+
* default to a local deployment.
|
|
106
|
+
*/
|
|
107
|
+
export function getConfiguredMinSearchScore(): number | undefined {
|
|
108
|
+
const explicit = readUnitInterval("CEREFOX_MIN_SEARCH_SCORE");
|
|
109
|
+
if (explicit !== undefined) return explicit;
|
|
110
|
+
if (readEnv("CEREFOX_EMBEDDER") === "local") return DEFAULT_MIN_SEARCH_SCORE_LOCAL;
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getConfiguredSearchAlpha(): number | undefined {
|
|
115
|
+
return readUnitInterval("CEREFOX_SEARCH_ALPHA");
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
/**
|
|
93
119
|
* Resolve the minimum cosine-similarity floor for hybrid/semantic search
|
|
94
120
|
* (vector-only matches below this are dropped; FTS matches always pass).
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
* docs/specs/polish-and-distribution-design.md §10d.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export const HELP_FULL = "# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **10 MCP tools** (9 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for \"How AI Agents Use Cerefox\" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `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";
|
|
14
|
+
export const HELP_FULL = "# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **14 MCP tools** (13 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_set_relation` | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_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. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\",\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc (note its hash) -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true,\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search \"<q>\" --requestor \"<your-name>\"` |\n| `cerefox_ingest` (paste) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --document-id \"<uuid>\" --expected-content-hash \"<hash>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor \"<your-name>\"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor \"<your-name>\"` |\n| `cerefox_list_projects` | `cerefox project list --requestor \"<your-name>\"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_set_relation` | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author \"<your-name>\" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor \"<your-name>\"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author \"<your-name>\" --author-type agent`\n- Reads: `--requestor \"<your-name>\"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n";
|
|
15
15
|
|
|
16
16
|
/** Sections keyed by their H2 heading text (lower-cased for matching). */
|
|
17
17
|
export const HELP_SECTIONS: Record<string, string> = {
|
|
18
|
-
"Tools": "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `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) |",
|
|
19
|
-
"Essential Rules": "## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **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`.",
|
|
18
|
+
"Tools": "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `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_set_relation` | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |",
|
|
19
|
+
"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. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.",
|
|
20
20
|
"Update Workflow (ID-based -- preferred)": "## 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.",
|
|
21
21
|
"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```",
|
|
22
22
|
"Catch-Up Workflow": "## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```",
|
|
23
|
-
"CLI fallback (when MCP is unavailable)": "## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\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.",
|
|
23
|
+
"CLI fallback (when MCP is unavailable)": "## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\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_set_relation` | Link two documents (`source --rel_type--> target`) | `source_id`, `target_id`, `rel_type` (required), `metadata`, `author` |\n| `cerefox_delete_relation` | Remove a relation | `source_id`, `target_id`, `rel_type` |\n| `cerefox_get_relations` | All relations touching a document, both directions | `document_id` |\n| `cerefox_get_neighbors` | Walk the graph along ONE relation type | `document_id`, `rel_type` (required), `depth`, `from_time`, `to_time`, `limit` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author \"<your-name>\" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor \"<your-name>\"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author \"<your-name>\" --author-type agent`\n- Reads: `--requestor \"<your-name>\"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.",
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export const HELP_SECTION_HEADINGS: string[] = ["Tools", "Essential Rules", "Update Workflow (ID-based -- preferred)", "Update Workflow (title-based -- fallback)", "Catch-Up Workflow", "CLI fallback (when MCP is unavailable)"];
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { auditLogTool } from "./audit-log.ts";
|
|
15
|
+
import {
|
|
16
|
+
deleteRelationTool,
|
|
17
|
+
getNeighborsTool,
|
|
18
|
+
getRelationsTool,
|
|
19
|
+
setRelationTool,
|
|
20
|
+
} from "./relations.ts";
|
|
15
21
|
import { getDocumentTool } from "./get-document.ts";
|
|
16
22
|
import { getHelpTool } from "./get-help.ts";
|
|
17
23
|
import { ingestTool } from "./ingest.ts";
|
|
@@ -34,6 +40,11 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
34
40
|
listProjectsTool,
|
|
35
41
|
setDocumentProjectsTool,
|
|
36
42
|
auditLogTool,
|
|
43
|
+
// Document relations (iteration 29): the graph surface.
|
|
44
|
+
setRelationTool,
|
|
45
|
+
deleteRelationTool,
|
|
46
|
+
getRelationsTool,
|
|
47
|
+
getNeighborsTool,
|
|
37
48
|
getHelpTool,
|
|
38
49
|
];
|
|
39
50
|
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document-relation tools (iteration 29) — the graph surface for agents.
|
|
3
|
+
*
|
|
4
|
+
* cerefox_set_relation create/update a typed edge
|
|
5
|
+
* cerefox_delete_relation remove one
|
|
6
|
+
* cerefox_get_relations everything touching a document, both directions
|
|
7
|
+
* cerefox_get_neighbors walk one relation type outward
|
|
8
|
+
*
|
|
9
|
+
* All four are thin adapters over the matching RPCs — the type dictionary
|
|
10
|
+
* (which types are symmetric, which change lifecycle status) lives in SQL so
|
|
11
|
+
* every transport behaves identically. Design:
|
|
12
|
+
* docs/research/document-relations-and-semantic-graph.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { logUsage } from "./_utils.ts";
|
|
16
|
+
import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
|
|
17
|
+
import type { MCPSupabaseClient } from "./types.ts";
|
|
18
|
+
|
|
19
|
+
/** Relation types that carry behaviour; any other string is accepted too. */
|
|
20
|
+
const KNOWN_TYPES =
|
|
21
|
+
"related_to, references, supersedes, contradicts, duplicates, part_of, follows, reply_to";
|
|
22
|
+
|
|
23
|
+
function requireUuid(value: unknown, field: string): string {
|
|
24
|
+
const s = typeof value === "string" ? value.trim() : "";
|
|
25
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s)) {
|
|
26
|
+
throw new McpInvalidParams(`${field} must be a document UUID (got: ${String(value)})`);
|
|
27
|
+
}
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── set ──────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
async function setHandler(
|
|
34
|
+
supabase: MCPSupabaseClient,
|
|
35
|
+
args: Record<string, unknown>,
|
|
36
|
+
ctx: ToolContext,
|
|
37
|
+
): Promise<string> {
|
|
38
|
+
const source = requireUuid(args.source_id, "source_id");
|
|
39
|
+
const target = requireUuid(args.target_id, "target_id");
|
|
40
|
+
const relType = typeof args.rel_type === "string" ? args.rel_type.trim() : "";
|
|
41
|
+
if (!relType) throw new McpInvalidParams("rel_type is required");
|
|
42
|
+
|
|
43
|
+
const { data, error } = await supabase.rpc("cerefox_set_relation", {
|
|
44
|
+
p_source_id: source,
|
|
45
|
+
p_target_id: target,
|
|
46
|
+
p_rel_type: relType,
|
|
47
|
+
p_author: (args.author as string | undefined) ?? "mcp-agent",
|
|
48
|
+
p_author_type: "agent",
|
|
49
|
+
p_metadata: (args.metadata as Record<string, unknown> | undefined) ?? {},
|
|
50
|
+
});
|
|
51
|
+
if (error) throw new Error(`RPC error: ${error.message}`);
|
|
52
|
+
|
|
53
|
+
const row = (Array.isArray(data) ? data[0] : data) as
|
|
54
|
+
| { is_symmetric?: boolean }
|
|
55
|
+
| undefined;
|
|
56
|
+
|
|
57
|
+
logUsage(supabase, {
|
|
58
|
+
operation: "set_relation",
|
|
59
|
+
accessPath: ctx.accessPath,
|
|
60
|
+
requestor: args.requestor as string | undefined,
|
|
61
|
+
document_id: source,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const both = row?.is_symmetric
|
|
65
|
+
? " (symmetric — the reverse edge was written too)"
|
|
66
|
+
: "";
|
|
67
|
+
const effect =
|
|
68
|
+
relType === "supersedes"
|
|
69
|
+
? "\nTarget marked superseded."
|
|
70
|
+
: relType === "contradicts"
|
|
71
|
+
? "\nBoth documents marked stale."
|
|
72
|
+
: "";
|
|
73
|
+
return `Relation set: ${source} --${relType}--> ${target}${both}.${effect}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const setRelationTool: ToolDefinition = {
|
|
77
|
+
name: "cerefox_set_relation",
|
|
78
|
+
description:
|
|
79
|
+
"Link two documents with a typed, directed relation (source → target). " +
|
|
80
|
+
`Known types with behaviour: ${KNOWN_TYPES}. Symmetric types (related_to, ` +
|
|
81
|
+
"contradicts, duplicates) write both directions. `supersedes` marks the target " +
|
|
82
|
+
"superseded; `contradicts` marks both stale. Any other type string is accepted " +
|
|
83
|
+
"and stored, just without special behaviour. Re-setting the same edge updates it.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
required: ["source_id", "target_id", "rel_type"],
|
|
87
|
+
properties: {
|
|
88
|
+
source_id: { type: "string", description: "UUID of the source document" },
|
|
89
|
+
target_id: { type: "string", description: "UUID of the target document" },
|
|
90
|
+
rel_type: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: `Relation type, e.g. one of: ${KNOWN_TYPES}. Free text is allowed.`,
|
|
93
|
+
},
|
|
94
|
+
metadata: {
|
|
95
|
+
type: "object",
|
|
96
|
+
description: "Optional JSON context for the edge (note, confidence, …)",
|
|
97
|
+
},
|
|
98
|
+
author: { type: "string", description: "Who is creating this relation" },
|
|
99
|
+
requestor: { type: "string", description: "Name of the agent making this request" },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
handler: setHandler,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ── delete ───────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
async function deleteHandler(
|
|
108
|
+
supabase: MCPSupabaseClient,
|
|
109
|
+
args: Record<string, unknown>,
|
|
110
|
+
ctx: ToolContext,
|
|
111
|
+
): Promise<string> {
|
|
112
|
+
const source = requireUuid(args.source_id, "source_id");
|
|
113
|
+
const target = requireUuid(args.target_id, "target_id");
|
|
114
|
+
const relType = typeof args.rel_type === "string" ? args.rel_type.trim() : "";
|
|
115
|
+
if (!relType) throw new McpInvalidParams("rel_type is required");
|
|
116
|
+
|
|
117
|
+
const { data, error } = await supabase.rpc("cerefox_delete_relation", {
|
|
118
|
+
p_source_id: source,
|
|
119
|
+
p_target_id: target,
|
|
120
|
+
p_rel_type: relType,
|
|
121
|
+
p_author: (args.author as string | undefined) ?? "mcp-agent",
|
|
122
|
+
p_author_type: "agent",
|
|
123
|
+
});
|
|
124
|
+
if (error) throw new Error(`RPC error: ${error.message}`);
|
|
125
|
+
|
|
126
|
+
const removed = typeof data === "number" ? data : 0;
|
|
127
|
+
logUsage(supabase, {
|
|
128
|
+
operation: "delete_relation",
|
|
129
|
+
accessPath: ctx.accessPath,
|
|
130
|
+
requestor: args.requestor as string | undefined,
|
|
131
|
+
document_id: source,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (removed === 0) return "No such relation — nothing removed.";
|
|
135
|
+
return (
|
|
136
|
+
`Removed ${removed} relation row(s): ${source} --${relType}--> ${target}. ` +
|
|
137
|
+
"Lifecycle status is left as-is (a document may be superseded by something else too)."
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const deleteRelationTool: ToolDefinition = {
|
|
142
|
+
name: "cerefox_delete_relation",
|
|
143
|
+
description:
|
|
144
|
+
"Remove a typed relation between two documents. Symmetric types remove both " +
|
|
145
|
+
"directions. Lifecycle status set by an earlier relation is NOT reverted.",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
required: ["source_id", "target_id", "rel_type"],
|
|
149
|
+
properties: {
|
|
150
|
+
source_id: { type: "string", description: "UUID of the source document" },
|
|
151
|
+
target_id: { type: "string", description: "UUID of the target document" },
|
|
152
|
+
rel_type: { type: "string", description: "Relation type to remove" },
|
|
153
|
+
author: { type: "string", description: "Who is removing this relation" },
|
|
154
|
+
requestor: { type: "string", description: "Name of the agent making this request" },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
handler: deleteHandler,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ── read ─────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
async function getRelationsHandler(
|
|
163
|
+
supabase: MCPSupabaseClient,
|
|
164
|
+
args: Record<string, unknown>,
|
|
165
|
+
ctx: ToolContext,
|
|
166
|
+
): Promise<string> {
|
|
167
|
+
const docId = requireUuid(args.document_id, "document_id");
|
|
168
|
+
const { data, error } = await supabase.rpc("cerefox_get_relations", {
|
|
169
|
+
p_document_id: docId,
|
|
170
|
+
});
|
|
171
|
+
if (error) throw new Error(`RPC error: ${error.message}`);
|
|
172
|
+
|
|
173
|
+
const rows = (data ?? []) as Array<{
|
|
174
|
+
direction: string;
|
|
175
|
+
rel_type: string;
|
|
176
|
+
other_id: string;
|
|
177
|
+
other_title: string;
|
|
178
|
+
other_lifecycle: string;
|
|
179
|
+
}>;
|
|
180
|
+
|
|
181
|
+
logUsage(supabase, {
|
|
182
|
+
operation: "get_relations",
|
|
183
|
+
accessPath: ctx.accessPath,
|
|
184
|
+
requestor: args.requestor as string | undefined,
|
|
185
|
+
document_id: docId,
|
|
186
|
+
result_count: rows.length,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (rows.length === 0) return "No relations for this document.";
|
|
190
|
+
const lines = rows.map((r) => {
|
|
191
|
+
const arrow = r.direction === "outbound" ? "→" : "←";
|
|
192
|
+
const life = r.other_lifecycle && r.other_lifecycle !== "active"
|
|
193
|
+
? ` [${r.other_lifecycle}]`
|
|
194
|
+
: "";
|
|
195
|
+
return `- ${arrow} ${r.rel_type}: ${r.other_title}${life} [id: ${r.other_id}]`;
|
|
196
|
+
});
|
|
197
|
+
return `${rows.length} relation(s):\n${lines.join("\n")}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const getRelationsTool: ToolDefinition = {
|
|
201
|
+
name: "cerefox_get_relations",
|
|
202
|
+
description:
|
|
203
|
+
"List every relation touching a document, in both directions (→ outbound, " +
|
|
204
|
+
"← inbound). Shows each neighbour's title and lifecycle status, so an agent " +
|
|
205
|
+
"can tell whether retrieved knowledge has been superseded or contradicted.",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: "object",
|
|
208
|
+
required: ["document_id"],
|
|
209
|
+
properties: {
|
|
210
|
+
document_id: { type: "string", description: "UUID of the document" },
|
|
211
|
+
requestor: { type: "string", description: "Name of the agent making this request" },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
handler: getRelationsHandler,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
async function getNeighborsHandler(
|
|
218
|
+
supabase: MCPSupabaseClient,
|
|
219
|
+
args: Record<string, unknown>,
|
|
220
|
+
ctx: ToolContext,
|
|
221
|
+
): Promise<string> {
|
|
222
|
+
const docId = requireUuid(args.document_id, "document_id");
|
|
223
|
+
const relType = typeof args.rel_type === "string" ? args.rel_type.trim() : "";
|
|
224
|
+
if (!relType) throw new McpInvalidParams("rel_type is required — pick one type to traverse");
|
|
225
|
+
const depth = Math.max(1, Math.min((args.depth as number | undefined) ?? 1, 5));
|
|
226
|
+
|
|
227
|
+
const { data, error } = await supabase.rpc("cerefox_get_neighbors", {
|
|
228
|
+
p_document_id: docId,
|
|
229
|
+
p_rel_type: relType,
|
|
230
|
+
p_depth: depth,
|
|
231
|
+
p_from_time: (args.from_time as string | undefined) ?? null,
|
|
232
|
+
p_to_time: (args.to_time as string | undefined) ?? null,
|
|
233
|
+
p_limit: Math.max(1, Math.min((args.limit as number | undefined) ?? 50, 200)),
|
|
234
|
+
});
|
|
235
|
+
if (error) throw new Error(`RPC error: ${error.message}`);
|
|
236
|
+
|
|
237
|
+
const rows = (data ?? []) as Array<{
|
|
238
|
+
document_id: string;
|
|
239
|
+
title: string;
|
|
240
|
+
lifecycle_status: string;
|
|
241
|
+
depth: number;
|
|
242
|
+
direction: string;
|
|
243
|
+
}>;
|
|
244
|
+
|
|
245
|
+
logUsage(supabase, {
|
|
246
|
+
operation: "get_neighbors",
|
|
247
|
+
accessPath: ctx.accessPath,
|
|
248
|
+
requestor: args.requestor as string | undefined,
|
|
249
|
+
document_id: docId,
|
|
250
|
+
result_count: rows.length,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (rows.length === 0) return `No documents reachable via "${relType}".`;
|
|
254
|
+
const lines = rows.map((r) => {
|
|
255
|
+
const life = r.lifecycle_status && r.lifecycle_status !== "active"
|
|
256
|
+
? ` [${r.lifecycle_status}]`
|
|
257
|
+
: "";
|
|
258
|
+
return `- depth ${r.depth} (${r.direction}): ${r.title}${life} [id: ${r.document_id}]`;
|
|
259
|
+
});
|
|
260
|
+
return `${rows.length} document(s) via "${relType}":\n${lines.join("\n")}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export const getNeighborsTool: ToolDefinition = {
|
|
264
|
+
name: "cerefox_get_neighbors",
|
|
265
|
+
description:
|
|
266
|
+
"Walk the relation graph outward from a document along ONE relation type. " +
|
|
267
|
+
"Use after cerefox_get_relations shows which types exist. depth > 1 follows " +
|
|
268
|
+
"chains (useful for follows / reply_to); cycles terminate safely. Optional " +
|
|
269
|
+
"from_time / to_time filter neighbours by their creation time.",
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: "object",
|
|
272
|
+
required: ["document_id", "rel_type"],
|
|
273
|
+
properties: {
|
|
274
|
+
document_id: { type: "string", description: "UUID of the starting document" },
|
|
275
|
+
rel_type: { type: "string", description: "The single relation type to traverse" },
|
|
276
|
+
depth: { type: "integer", description: "How many hops to follow (1–5, default 1)" },
|
|
277
|
+
from_time: { type: "string", description: "ISO-8601: only neighbours created on/after" },
|
|
278
|
+
to_time: { type: "string", description: "ISO-8601: only neighbours created on/before" },
|
|
279
|
+
limit: { type: "integer", description: "Max documents to return (default 50, max 200)" },
|
|
280
|
+
requestor: { type: "string", description: "Name of the agent making this request" },
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
handler: getNeighborsHandler,
|
|
284
|
+
};
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
import type { MCPSupabaseClient } from "./types.ts";
|
|
19
19
|
|
|
20
20
|
import { getEmbedding, resolveEmbedderKind } from "../embeddings/index.ts";
|
|
21
|
-
import { applyByteBudget,
|
|
22
|
-
|
|
21
|
+
import { applyByteBudget, getConfiguredMinSearchScore, getConfiguredSearchAlpha,
|
|
22
|
+
getMaxResponseBytes, getMinTermCoverage, logUsage } from "./_utils.ts";
|
|
23
23
|
import { lookupProjectId } from "./_projects.ts";
|
|
24
24
|
import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
|
|
25
25
|
|
|
@@ -32,14 +32,20 @@ async function handler(
|
|
|
32
32
|
const project_name = args.project_name as string | undefined;
|
|
33
33
|
const match_count = (args.match_count as number | undefined) ?? 5;
|
|
34
34
|
const mode = (args.mode as string | undefined) ?? "docs";
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// #133: omit unconfigured tunables so the server resolves them from
|
|
36
|
+
// cerefox_config (one setting governs every access path).
|
|
37
|
+
const alpha = (args.alpha as number | undefined) ?? getConfiguredSearchAlpha();
|
|
38
|
+
const min_score =
|
|
39
|
+
(args.min_score as number | undefined) ?? getConfiguredMinSearchScore();
|
|
37
40
|
// v1.0.4: coverage gate default from CEREFOX_MIN_TERM_COVERAGE; only sent
|
|
38
41
|
// when configured (see getMinTermCoverage — keeps pre-0.9.1 servers working).
|
|
39
42
|
const min_term_coverage =
|
|
40
43
|
(args.min_term_coverage as number | undefined) ?? getMinTermCoverage();
|
|
41
44
|
const coverageParam =
|
|
42
45
|
min_term_coverage !== undefined ? { p_min_term_coverage: min_term_coverage } : {};
|
|
46
|
+
// Omitted keys let the RPC apply its cerefox_config → built-in chain (#133).
|
|
47
|
+
const scoreParam = min_score !== undefined ? { p_min_score: min_score } : {};
|
|
48
|
+
const alphaParam = alpha !== undefined ? { p_alpha: alpha } : {};
|
|
43
49
|
const metadata_filter =
|
|
44
50
|
(args.metadata_filter as Record<string, string> | null | undefined) ?? null;
|
|
45
51
|
const requested_max_bytes = args.max_bytes as number | undefined;
|
|
@@ -99,10 +105,10 @@ async function handler(
|
|
|
99
105
|
p_query_text: query,
|
|
100
106
|
p_query_embedding: embedding,
|
|
101
107
|
p_match_count: match_count,
|
|
102
|
-
p_alpha: alpha,
|
|
103
108
|
p_use_upgrade: false,
|
|
104
109
|
p_project_id: projectId,
|
|
105
|
-
|
|
110
|
+
...alphaParam,
|
|
111
|
+
...scoreParam,
|
|
106
112
|
...metaFilterParam,
|
|
107
113
|
...coverageParam,
|
|
108
114
|
};
|
|
@@ -112,9 +118,9 @@ async function handler(
|
|
|
112
118
|
p_query_text: query,
|
|
113
119
|
p_query_embedding: embedding,
|
|
114
120
|
p_match_count: match_count,
|
|
115
|
-
p_alpha: alpha,
|
|
116
121
|
p_project_id: projectId,
|
|
117
|
-
|
|
122
|
+
...alphaParam,
|
|
123
|
+
...scoreParam,
|
|
118
124
|
...metaFilterParam,
|
|
119
125
|
...coverageParam,
|
|
120
126
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
-- 0014_document_relations.sql — typed edges between documents (iteration 29).
|
|
2
|
+
--
|
|
3
|
+
-- Adds the relation graph on top of the existing document model:
|
|
4
|
+
-- * cerefox_document_relations — typed, directed edges (source → target)
|
|
5
|
+
-- * cerefox_documents.lifecycle_status — 'active' | 'superseded' | 'stale' | 'archived'
|
|
6
|
+
--
|
|
7
|
+
-- Design: docs/research/document-relations-and-semantic-graph.md §2.2, §3.
|
|
8
|
+
-- Idempotent: safe to re-run.
|
|
9
|
+
|
|
10
|
+
CREATE TABLE IF NOT EXISTS cerefox_document_relations (
|
|
11
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
12
|
+
source_id UUID NOT NULL REFERENCES cerefox_documents(id) ON DELETE CASCADE,
|
|
13
|
+
target_id UUID NOT NULL REFERENCES cerefox_documents(id) ON DELETE CASCADE,
|
|
14
|
+
-- Free-text by design: agents define new types without a migration. The
|
|
15
|
+
-- type dictionary (in the RPCs) gives known types behaviour; unknown types
|
|
16
|
+
-- are stored and returned, just without special handling.
|
|
17
|
+
rel_type TEXT NOT NULL,
|
|
18
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
19
|
+
author TEXT NOT NULL DEFAULT 'unknown',
|
|
20
|
+
author_type TEXT NOT NULL DEFAULT 'agent',
|
|
21
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
22
|
+
-- One edge of a given type per ordered pair; different types may coexist.
|
|
23
|
+
UNIQUE (source_id, target_id, rel_type),
|
|
24
|
+
-- A document relating to itself is always a mistake, and self-edges would
|
|
25
|
+
-- make traversal loop.
|
|
26
|
+
CONSTRAINT cerefox_relations_no_self_edge CHECK (source_id <> target_id)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_relations_source ON cerefox_document_relations(source_id);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_relations_target ON cerefox_document_relations(target_id);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_relations_type ON cerefox_document_relations(rel_type);
|
|
32
|
+
|
|
33
|
+
-- Lifecycle status: how a document stands relative to the rest of the graph.
|
|
34
|
+
-- Distinct from review_status (editorial state) and deleted_at (existence).
|
|
35
|
+
ALTER TABLE cerefox_documents
|
|
36
|
+
ADD COLUMN IF NOT EXISTS lifecycle_status TEXT NOT NULL DEFAULT 'active';
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_docs_lifecycle
|
|
39
|
+
ON cerefox_documents(lifecycle_status)
|
|
40
|
+
WHERE lifecycle_status <> 'active';
|
|
41
|
+
|
|
42
|
+
-- Data-API grants for the new table (migration 0013 / #26: privileges are
|
|
43
|
+
-- explicit now, and a new table gets none by default).
|
|
44
|
+
DO $$
|
|
45
|
+
BEGIN
|
|
46
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
|
|
47
|
+
GRANT SELECT, INSERT, UPDATE, DELETE
|
|
48
|
+
ON TABLE public.cerefox_document_relations TO service_role;
|
|
49
|
+
END IF;
|
|
50
|
+
END
|
|
51
|
+
$$;
|
|
52
|
+
|
|
53
|
+
-- Relation writes are auditable operations; widen the audit-log constraint.
|
|
54
|
+
ALTER TABLE cerefox_audit_log DROP CONSTRAINT IF EXISTS cerefox_audit_log_operation_check;
|
|
55
|
+
ALTER TABLE cerefox_audit_log ADD CONSTRAINT cerefox_audit_log_operation_check CHECK (
|
|
56
|
+
operation IN ('create', 'update-content', 'update-metadata', 'delete',
|
|
57
|
+
'status-change', 'archive', 'unarchive', 'restore',
|
|
58
|
+
'relation-set', 'relation-delete')
|
|
59
|
+
);
|