@cerefox/memory 1.15.2 → 1.16.0

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.
@@ -5318,7 +5318,7 @@ var init_cli_core = __esm(() => {
5318
5318
  });
5319
5319
 
5320
5320
  // src/meta.ts
5321
- var PKG_VERSION = "1.15.2";
5321
+ var PKG_VERSION = "1.16.0";
5322
5322
  var init_meta = () => {};
5323
5323
 
5324
5324
  // ../../_shared/config/paths.ts
@@ -23799,7 +23799,7 @@ var init__utils = __esm(() => {
23799
23799
  });
23800
23800
 
23801
23801
  // ../../_shared/server-assets/index.ts
23802
- import { existsSync as existsSync5 } from "node:fs";
23802
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
23803
23803
  import { dirname as dirname2, join as join5 } from "node:path";
23804
23804
  import { fileURLToPath } from "node:url";
23805
23805
  import { cwd as processCwd2 } from "node:process";
@@ -23845,7 +23845,20 @@ function resolveServerAssets(opts = {}) {
23845
23845
  }
23846
23846
  return sourceServerAssets(join5(here, "..", ".."));
23847
23847
  }
23848
- var init_server_assets = () => {};
23848
+ function bundledSchemaVersion(opts = {}) {
23849
+ try {
23850
+ const assets = resolveServerAssets(opts);
23851
+ if (!existsSync5(assets.schemaFile))
23852
+ return null;
23853
+ return readFileSync5(assets.schemaFile, "utf8").match(SCHEMA_VERSION_RE)?.[1] ?? null;
23854
+ } catch {
23855
+ return null;
23856
+ }
23857
+ }
23858
+ var SCHEMA_VERSION_RE;
23859
+ var init_server_assets = __esm(() => {
23860
+ SCHEMA_VERSION_RE = /^--\s*@version:\s*(\S+)/m;
23861
+ });
23849
23862
 
23850
23863
  // ../../node_modules/.bun/postgres@3.4.9/node_modules/postgres/src/query.js
23851
23864
  function cachedError(xs) {
@@ -26142,7 +26155,7 @@ var init__projects = __esm(() => {
26142
26155
  });
26143
26156
 
26144
26157
  // src/cli/util/bundled-docs.ts
26145
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync } from "node:fs";
26158
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync } from "node:fs";
26146
26159
  import { dirname as dirname3, join as join7, resolve as resolve2 } from "node:path";
26147
26160
  import { fileURLToPath as fileURLToPath2 } from "node:url";
26148
26161
  function findPackageRoot() {
@@ -26151,7 +26164,7 @@ function findPackageRoot() {
26151
26164
  const pkgJson = join7(dir, "package.json");
26152
26165
  if (existsSync8(pkgJson)) {
26153
26166
  try {
26154
- const parsed = JSON.parse(readFileSync6(pkgJson, "utf8"));
26167
+ const parsed = JSON.parse(readFileSync7(pkgJson, "utf8"));
26155
26168
  if (parsed.name === "@cerefox/memory")
26156
26169
  return dir;
26157
26170
  } catch {}
@@ -26226,7 +26239,7 @@ function readBundledDoc(topic) {
26226
26239
  return {
26227
26240
  topic: entry.topic,
26228
26241
  path: entry.path,
26229
- content: readFileSync6(entry.path, "utf8")
26242
+ content: readFileSync7(entry.path, "utf8")
26230
26243
  };
26231
26244
  }
26232
26245
  var PACKAGE_ROOT;
@@ -26235,7 +26248,7 @@ var init_bundled_docs = __esm(() => {
26235
26248
  });
26236
26249
 
26237
26250
  // ../../_shared/ef-meta/index.ts
26238
- var EF_VERSION = "1.15.2", CEREFOX_VERSION = "1.15.2", EF_LAST_CHANGED = "1.15.2";
26251
+ var EF_VERSION = "1.16.0", CEREFOX_VERSION = "1.16.0", EF_LAST_CHANGED = "1.16.0";
26239
26252
  var init_ef_meta = () => {};
26240
26253
 
26241
26254
  // ../../_shared/compatibility/index.ts
@@ -26380,6 +26393,7 @@ __export(exports_onnx_embedder, {
26380
26393
  ONNX_MODEL_ID: () => ONNX_MODEL_ID,
26381
26394
  ONNX_MODEL_NAME: () => ONNX_MODEL_NAME,
26382
26395
  buildPrefixedInputs: () => buildPrefixedInputs,
26396
+ isModelCached: () => isModelCached,
26383
26397
  nomicPrefix: () => nomicPrefix,
26384
26398
  onnxEmbed: () => onnxEmbed,
26385
26399
  warmup: () => warmup
@@ -26394,6 +26408,9 @@ function buildPrefixedInputs(texts, role) {
26394
26408
  const p = nomicPrefix(role);
26395
26409
  return texts.map((t) => p + t);
26396
26410
  }
26411
+ function isModelCached(dir = getCacheDir()) {
26412
+ return MODEL_FILES.every((f) => existsSync10(join9(dir, f)));
26413
+ }
26397
26414
  function getCacheDir() {
26398
26415
  const env = globalThis.process?.env ?? {};
26399
26416
  if (env.CEREFOX_MODELS_DIR)
@@ -26436,6 +26453,12 @@ async function ensurePipeline() {
26436
26453
  return pipelinePromise;
26437
26454
  pipelinePromise = (async () => {
26438
26455
  const transformers = await loadTransformers();
26456
+ if (isModelCached()) {
26457
+ const pipe = await transformers.pipeline("feature-extraction", ONNX_MODEL_ID, {
26458
+ dtype: ONNX_MODEL_DTYPE
26459
+ });
26460
+ return pipe;
26461
+ }
26439
26462
  const mb = ONNX_MODEL_APPROX_MB;
26440
26463
  const fmt = (s) => s < 60 ? `${s}s` : `${Math.round(s / 60)}m`;
26441
26464
  process.stderr.write(`[cerefox-embed] loading "${ONNX_MODEL_NAME}" from HuggingFace (~${mb} MB; est. ${fmt(Math.round(mb * 8 / 50))}-${fmt(Math.round(mb * 8 / 10))} at 50-10 Mbps; first-run only)…
@@ -26559,8 +26582,15 @@ async function onnxEmbed(texts, role) {
26559
26582
  }
26560
26583
  return vectors;
26561
26584
  }
26562
- var ONNX_MODEL_ID = "nomic-ai/nomic-embed-text-v1.5", ONNX_MODEL_NAME = "nomic-embed-text-v1.5", ONNX_MODEL_DTYPE = "q8", ONNX_MODEL_DIM = 768, ONNX_MODEL_APPROX_MB = 130, transformersModule = null, pipelinePromise = null, DEFAULT_ONNX_BATCH = 4;
26563
- var init_onnx_embedder = () => {};
26585
+ var ONNX_MODEL_ID = "nomic-ai/nomic-embed-text-v1.5", ONNX_MODEL_NAME = "nomic-embed-text-v1.5", ONNX_MODEL_DTYPE = "q8", ONNX_MODEL_DIM = 768, ONNX_MODEL_APPROX_MB = 130, MODEL_FILES, transformersModule = null, pipelinePromise = null, DEFAULT_ONNX_BATCH = 4;
26586
+ var init_onnx_embedder = __esm(() => {
26587
+ MODEL_FILES = [
26588
+ "config.json",
26589
+ "tokenizer.json",
26590
+ "tokenizer_config.json",
26591
+ `onnx/model_quantized.onnx`
26592
+ ].map((f) => join9(ONNX_MODEL_ID, f));
26593
+ });
26564
26594
 
26565
26595
  // ../../_shared/embeddings/index.ts
26566
26596
  function openaiEmbeddingConfig() {
@@ -50568,8 +50598,9 @@ async function handler2(supabase, args, ctx) {
50568
50598
  result_count: 1
50569
50599
  });
50570
50600
  return `Soft-deleted "${row.title ?? "untitled"}" (id: ${document_id}, ` + `${row.total_chars ?? "?"} chars) at ${row.deleted_at}.
50571
- ` + `The document is excluded from search but fully recoverable: it sits in the ` + `trash until restored or purged. cerefox_restore_document undoes this if it ` + `was a mistake; permanent purge is human-only (web UI).
50572
- ` + `Tell your user what you deleted and why, so they can review it.`;
50601
+ ` + `The document is excluded from search but fully recoverable: it sits in the ` + `trash until restored or purged. cerefox_restore_document undoes this if it ` + `was a mistake; permanent purge is human-only (web UI), or automatic after the ` + `trash retention period if the operator has turned trash auto-purge on.
50602
+ ` + (row.auto_purged ? `Trash auto-purge (an operator setting): this delete also permanently purged ` + `${row.auto_purged} document(s) that had been in the trash longer than the ` + `retention period.
50603
+ ` : "") + `Tell your user what you deleted and why, so they can review it.`;
50573
50604
  }
50574
50605
  var deleteDocumentTool;
50575
50606
  var init_delete_document = __esm(() => {
@@ -50578,7 +50609,7 @@ var init_delete_document = __esm(() => {
50578
50609
  init_identity();
50579
50610
  deleteDocumentTool = {
50580
50611
  name: "cerefox_delete_document",
50581
- description: "SOFT-delete a document: it leaves search results and lands in the trash, recoverable until a human purges it. Requires expected_content_hash — the content_hash of the document AS YOU READ IT — so a delete always follows a read; if the document changed in between, the call fails with a conflict and you should re-read before deciding again. A mistaken delete can be undone with cerefox_restore_document; permanent purge is human-only (web UI). ALWAYS tell your user what you deleted and why. Pass a short reason — it is recorded in the audit log for the human reviewing the trash. Prefer this over ingesting empty/placeholder content when a document should go away.",
50612
+ description: "SOFT-delete a document: it leaves search results and lands in the trash, recoverable until a human purges it (or, if the operator has turned on trash auto-purge, until it has been in the trash longer than the retention period; a delete reports any such purges it triggered). Requires expected_content_hash — the content_hash of the document AS YOU READ IT — so a delete always follows a read; if the document changed in between, the call fails with a conflict and you should re-read before deciding again. A mistaken delete can be undone with cerefox_restore_document; permanent purge is human-only (web UI). ALWAYS tell your user what you deleted and why. Pass a short reason — it is recorded in the audit log for the human reviewing the trash. Prefer this over ingesting empty/placeholder content when a document should go away.",
50582
50613
  annotations: {
50583
50614
  title: "Delete document (soft, recoverable)",
50584
50615
  readOnlyHint: false,
@@ -51405,12 +51436,12 @@ var init_partial_edits2 = __esm(() => {
51405
51436
  });
51406
51437
 
51407
51438
  // ../../_shared/mcp-tools/get-help-content.ts
51408
- var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **15 core MCP tools** (14 with CLI equivalents — `cerefox_get_help` is MCP-only), plus 4 dormant relation tools that appear only when `relations_enabled` is on. 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`, `author` |\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 10), `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_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part`, `author` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`/`rename_section`), `expected_content_hash` (required), `author` |\n| `cerefox_delete_document` | **Soft**-delete a document (to trash; excluded from search; permanent purge is human-only) | `document_id`, `expected_content_hash` (**required** — a delete must follow a read), `reason` (recorded in the audit log — give one), `author` |\n| `cerefox_restore_document` | Restore a soft-deleted document from the trash (audited inverse of delete; no-op if not deleted) | `document_id` (required), `reason` (recorded in the audit log), `author` |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash, or with `section: "## Heading"` one section\'s text | `document_id` (required), `outline`, `section`, `section_part`, `author` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required), `author` |\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`, `author` |\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_metadata` | Change tags WITHOUT resending content. **Merges** by default; a `null` value removes a key | `document_id`, `metadata` (required), `replace` (rare: set exactly this object), `author` |\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), `author` |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `by_author` (filter), `operation`, `since`, `author` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.\n\n## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Look before you overwrite** — `cerefox_get_document(document_id,\n section: "## Heading")` returns exactly the text a `replace_section` on that\n anchor would destroy. The outline gives you a section\'s *size*, never its\n *text*, so on a document you did not write yourself this is the difference\n between a replace and a blind overwrite.\n4. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround. To fix a stale heading (`## OPEN TODOs (as of ...)`), use\n `rename_section`: it changes the heading text and leaves the body and\n position alone.\n5. All of them require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**A section runs to the next same-or-higher heading, or to the end of the\ndocument.** So `end_of_document` inserts land inside the *last* section, and\nreplacing or deleting that section removes them too. A large shrink in the\nresponse is your warning; `cerefox_list_versions` has the previous content.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.\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`** to your name on every call, reads and writes alike (e.g., "Claude Code", "archiver"). Same parameter on every tool. (`requestor` is still accepted everywhere as the pre-1.13.1 alias.) On the CLI it is `--author` on every command too (plus `--author-type` on writes); or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **`max_bytes` has to fit a whole document.** Search returns COMPLETE documents, so a budget smaller than the top hit fits nothing. You get a header-only list saying so (never "No results found." — that means the store really has nothing). Raise `max_bytes`, or read one document with `cerefox_get_document` using `outline: true` or `section`.\n6. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n7. **Write for retrieval: the title does real work.** The document title is indexed at the **highest weight** *and* prepended to every chunk before it is embedded, so it steers both halves of hybrid search (keyword and semantic) for every chunk in the document. Name the document with the distinctive terms someone would actually search for -- "Cerefox Response Size Limits", not "Notes" or "Update 3". Renaming re-indexes and re-embeds, so correcting a vague title is cheap and takes effect immediately. Inside the content, write structured Markdown with H1/H2/H3 headings: the chunker follows them, and a chunk\'s own heading carries the same weight as the title.\n8. **Deletes are soft (recoverable); permanent purge is web-UI-only.** `cerefox_delete_document` requires the document\'s `content_hash` as you read it (read before you delete) and takes a `reason` — give one; it is what the human reviewing the trash sees. `cerefox_restore_document` undoes a mistaken delete (also audited, also takes a `reason`). Always surface deletes AND restores to the user. Once a human purges from the web UI, the document is gone for good.\n9. **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. **The server validates `](uuid)` links on every write** (v1.7.0): a link to a nonexistent id rejects the write, naming the offender — that means you mangled the UUID; re-read the source and correct it, do not retry unchanged. Example ids go in backticks (code is not validated). `[[Wikilinks]]` may dangle. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n10. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. 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.\n11. **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.\n12. **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.\n13. **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`). 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>" --author "<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> --author "<your-name>"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --author "<your-name>"` |\n| `cerefox_list_projects` | `cerefox project list --author "<your-name>"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_insert` | `cerefox document insert <id> -t "<text>" -p <position> -a "<anchor-heading>" -e "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_edit` | `cerefox document edit-parts <id> --operations \'<json>\' -e "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_delete_document` | `cerefox document delete <id> --reason "<why>" --author "<your-name>" --author-type agent --yes` (confirms interactively instead of requiring the hash) |\n| `cerefox_restore_document` | `cerefox document restore <id> --reason "<why>" --author "<your-name>" --author-type agent` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter \'<json>\' --author "<your-name>"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_metadata` | `cerefox document set-metadata <id> --set key=value` (also `--remove key`, `--json \'{...}\'`, `--replace`) |\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 --author "<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: `--author "<your-name>"` (the same flag; `--requestor` still works as a hidden alias)\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n\n## Timestamps are UTC\n\nEvery timestamp Cerefox returns — `created_at` on audit entries, version\nhistory, document metadata — is **UTC**, and now carries its `Z` marker so it\ncannot be mistaken for local time.\n\n**When you write a date into a document\'s CONTENT, use your own clock, not a\nCerefox timestamp.** These are different things: a timestamp records when the\nserver stored something; a date in a log entry or a heading is authored content\nand belongs to your timezone. An agent working a Pacific afternoon read\n`2026-08-11` from version history, wrote "8/11" into its entries, and put a\nday\'s work in the future — the timestamp was correct, and copying it into\ncontent was not.\n\nCerefox deliberately does not convert to local time on the API or MCP paths.\n"Local" has no server-side meaning: the remote MCP server runs in a cloud\nfunction whose local time *is* UTC, while a local MCP server runs in yours, so\nthe same document would report two different times depending on transport. The\nweb UI converts because a browser knows the viewer\'s timezone; nothing\nserver-side does.\n\n## Mistakes that have actually happened\n\nEach of these comes from a real agent session, and each is easy to make.\n\n- **`cerefox_ingest` always replaces the ENTIRE document.** Never a section.\n Before sending, check that the tool name matches the intent: if the intent is\n "change one section", the call is `cerefox_edit` with `replace_section`. A\n section-sized edit sent as a full ingest truncated a 13,000-character index to\n a single word. It was recovered from version history within the minute, but\n only because it was noticed immediately.\n\n- **Do not include the anchor\'s own heading in your text.** `replace_section`\n keeps the heading and `insert` places your text inside the section, so\n including it produces two. This is now refused rather than silently applied,\n but the shape is worth knowing: it happened twice in one session, the second\n time while trying to repair the first. A *deeper* sub-heading inside your text\n is fine.\n\n- **Content between sections belongs to the section ABOVE it.** A section runs\n to the next heading of the same or higher level, so a `---` rule, a note, or\n any trailing text sitting just above the next heading is part of the section\n before it — even when it visually reads as belonging below. Replacing that\n section takes it too. An agent hit exactly this: a `---` that separated two\n major sections disappeared when the section above it was replaced. The write\n was correct by the addressing rules; the surprise is that "the end of this\n section" is further down the page than it looks. Note the loss warning will\n not catch it if your replacement text is longer than what it replaced, since\n there is then no net loss to report.\n\n- **To change only tags, use `cerefox_set_document_metadata`, never `cerefox_ingest`.**\n Ingest replaces the whole document, so re-sending it to set one tag carries the\n full transcription risk for no reason. The metadata tool merges: the keys you\n pass are set, everything else is left alone, so you do not need to read the\n document first and cannot drop a tag another agent set. Pass `null` as a value\n to remove a key.\n\n- **Never partial-edit to fix a partial edit.** If a write leaves unexpected\n structure, stop. Use `cerefox_list_versions`, retrieve the last good version,\n and re-ingest cleanly. Repairing edits with more edits compounds the damage.\n\n- **A rejected batch is safe.** Operations in one `cerefox_edit` are\n all-or-nothing: if any is invalid, nothing is written. A refusal costs you a\n retry, not data — so prefer one call for changes that belong together, and do\n not split a batch to "make it more likely to succeed".\n\n- **Read before replacing.** `cerefox_get_document(section: "## Heading")`\n returns exactly what a `replace_section` on that anchor would overwrite. Use it\n for any section you did not write in this session. The outline gives a\n section\'s *size*, never its *text*.\n\n- **Verify after writing** — read the result back before reporting success, and\n report what the read actually shows.\n\n- **Partial edits cannot change a document\'s stored TITLE.** `rename_section`\n changes a heading inside the content; the title is a separate field and still\n needs `cerefox_ingest`.\n\n- **If a capability seems missing from one server, suspect your client first.**\n Local and remote run the same code. **Every `cerefox_get_help()` response\n begins with the server\'s version and the operations it registers** — you do\n not need a special topic, and the *absence* of that block is itself an answer:\n a server that does not print it predates v1.5.0. If that\n disagrees with your tool list, the client is holding a list it fetched before\n an upgrade — clients cache it at connect time. Ask the user to restart the\n client. Do not record a capability difference between servers as a fact; every\n such report so far has been a stale client.\n- Long inline bodies can arrive with literal `\\n`/`\\"` (the author over-escaped; Cerefox stores bytes faithfully). For long or quote-dense content, ingest from a file or build incrementally with `cerefox_insert`; read back multi-line writes.\n', HELP_SECTIONS, HELP_SECTION_HEADINGS;
51439
+ var HELP_FULL = '# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **15 core MCP tools** (14 with CLI equivalents — `cerefox_get_help` is MCP-only), plus 4 dormant relation tools that appear only when `relations_enabled` is on. 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`, `author` |\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 10), `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_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part`, `author` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`/`rename_section`), `expected_content_hash` (required), `author` |\n| `cerefox_delete_document` | **Soft**-delete a document (to trash; excluded from search; permanent purge is human-only) | `document_id`, `expected_content_hash` (**required** — a delete must follow a read), `reason` (recorded in the audit log — give one), `author` |\n| `cerefox_restore_document` | Restore a soft-deleted document from the trash (audited inverse of delete; no-op if not deleted) | `document_id` (required), `reason` (recorded in the audit log), `author` |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash, or with `section: "## Heading"` one section\'s text | `document_id` (required), `outline`, `section`, `section_part`, `author` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required), `author` |\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`, `author` |\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_metadata` | Change tags WITHOUT resending content. **Merges** by default; a `null` value removes a key | `document_id`, `metadata` (required), `replace` (rare: set exactly this object), `author` |\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), `author` |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `by_author` (filter), `operation`, `since`, `author` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.\n\n## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Look before you overwrite** — `cerefox_get_document(document_id,\n section: "## Heading")` returns exactly the text a `replace_section` on that\n anchor would destroy. The outline gives you a section\'s *size*, never its\n *text*, so on a document you did not write yourself this is the difference\n between a replace and a blind overwrite.\n4. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround. To fix a stale heading (`## OPEN TODOs (as of ...)`), use\n `rename_section`: it changes the heading text and leaves the body and\n position alone.\n5. All of them require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**A section runs to the next same-or-higher heading, or to the end of the\ndocument.** So `end_of_document` inserts land inside the *last* section, and\nreplacing or deleting that section removes them too. A large shrink in the\nresponse is your warning; `cerefox_list_versions` has the previous content.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.\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`** to your name on every call, reads and writes alike (e.g., "Claude Code", "archiver"). Same parameter on every tool. (`requestor` is still accepted everywhere as the pre-1.13.1 alias.) On the CLI it is `--author` on every command too (plus `--author-type` on writes); or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **`max_bytes` has to fit a whole document.** Search returns COMPLETE documents, so a budget smaller than the top hit fits nothing. You get a header-only list saying so (never "No results found." — that means the store really has nothing). Raise `max_bytes`, or read one document with `cerefox_get_document` using `outline: true` or `section`.\n6. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n7. **Write for retrieval: the title does real work.** The document title is indexed at the **highest weight** *and* prepended to every chunk before it is embedded, so it steers both halves of hybrid search (keyword and semantic) for every chunk in the document. Name the document with the distinctive terms someone would actually search for -- "Cerefox Response Size Limits", not "Notes" or "Update 3". Renaming re-indexes and re-embeds, so correcting a vague title is cheap and takes effect immediately. Inside the content, write structured Markdown with H1/H2/H3 headings: the chunker follows them, and a chunk\'s own heading carries the same weight as the title.\n8. **Deletes are soft (recoverable); permanent purge is web-UI-only.** `cerefox_delete_document` requires the document\'s `content_hash` as you read it (read before you delete) and takes a `reason` — give one; it is what the human reviewing the trash sees. `cerefox_restore_document` undoes a mistaken delete (also audited, also takes a `reason`). Always surface deletes AND restores to the user. Once a human purges from the web UI, the document is gone for good; so is one auto-purged after `trash_retention_days`, if the operator has turned trash auto-purge on.\n9. **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. **The server validates `](uuid)` links on every write** (v1.7.0): a link to a nonexistent id rejects the write, naming the offender — that means you mangled the UUID; re-read the source and correct it, do not retry unchanged. Example ids go in backticks (code is not validated). `[[Wikilinks]]` may dangle. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n10. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. 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.\n11. **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.\n12. **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.\n13. **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`). 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>" --author "<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> --author "<your-name>"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --author "<your-name>"` |\n| `cerefox_list_projects` | `cerefox project list --author "<your-name>"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_insert` | `cerefox document insert <id> -t "<text>" -p <position> -a "<anchor-heading>" -e "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_edit` | `cerefox document edit-parts <id> --operations \'<json>\' -e "<hash>" --author "<your-name>" --author-type agent` |\n| `cerefox_delete_document` | `cerefox document delete <id> --reason "<why>" --author "<your-name>" --author-type agent --yes` (confirms interactively instead of requiring the hash) |\n| `cerefox_restore_document` | `cerefox document restore <id> --reason "<why>" --author "<your-name>" --author-type agent` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter \'<json>\' --author "<your-name>"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_metadata` | `cerefox document set-metadata <id> --set key=value` (also `--remove key`, `--json \'{...}\'`, `--replace`) |\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 --author "<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: `--author "<your-name>"` (the same flag; `--requestor` still works as a hidden alias)\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n\n## Timestamps are UTC\n\nEvery timestamp Cerefox returns — `created_at` on audit entries, version\nhistory, document metadata — is **UTC**, and now carries its `Z` marker so it\ncannot be mistaken for local time.\n\n**When you write a date into a document\'s CONTENT, use your own clock, not a\nCerefox timestamp.** These are different things: a timestamp records when the\nserver stored something; a date in a log entry or a heading is authored content\nand belongs to your timezone. An agent working a Pacific afternoon read\n`2026-08-11` from version history, wrote "8/11" into its entries, and put a\nday\'s work in the future — the timestamp was correct, and copying it into\ncontent was not.\n\nCerefox deliberately does not convert to local time on the API or MCP paths.\n"Local" has no server-side meaning: the remote MCP server runs in a cloud\nfunction whose local time *is* UTC, while a local MCP server runs in yours, so\nthe same document would report two different times depending on transport. The\nweb UI converts because a browser knows the viewer\'s timezone; nothing\nserver-side does.\n\n## Mistakes that have actually happened\n\nEach of these comes from a real agent session, and each is easy to make.\n\n- **`cerefox_ingest` always replaces the ENTIRE document.** Never a section.\n Before sending, check that the tool name matches the intent: if the intent is\n "change one section", the call is `cerefox_edit` with `replace_section`. A\n section-sized edit sent as a full ingest truncated a 13,000-character index to\n a single word. It was recovered from version history within the minute, but\n only because it was noticed immediately.\n\n- **Do not include the anchor\'s own heading in your text.** `replace_section`\n keeps the heading and `insert` places your text inside the section, so\n including it produces two. This is now refused rather than silently applied,\n but the shape is worth knowing: it happened twice in one session, the second\n time while trying to repair the first. A *deeper* sub-heading inside your text\n is fine.\n\n- **Content between sections belongs to the section ABOVE it.** A section runs\n to the next heading of the same or higher level, so a `---` rule, a note, or\n any trailing text sitting just above the next heading is part of the section\n before it — even when it visually reads as belonging below. Replacing that\n section takes it too. An agent hit exactly this: a `---` that separated two\n major sections disappeared when the section above it was replaced. The write\n was correct by the addressing rules; the surprise is that "the end of this\n section" is further down the page than it looks. Note the loss warning will\n not catch it if your replacement text is longer than what it replaced, since\n there is then no net loss to report.\n\n- **To change only tags, use `cerefox_set_document_metadata`, never `cerefox_ingest`.**\n Ingest replaces the whole document, so re-sending it to set one tag carries the\n full transcription risk for no reason. The metadata tool merges: the keys you\n pass are set, everything else is left alone, so you do not need to read the\n document first and cannot drop a tag another agent set. Pass `null` as a value\n to remove a key.\n\n- **Never partial-edit to fix a partial edit.** If a write leaves unexpected\n structure, stop. Use `cerefox_list_versions`, retrieve the last good version,\n and re-ingest cleanly. Repairing edits with more edits compounds the damage.\n\n- **A rejected batch is safe.** Operations in one `cerefox_edit` are\n all-or-nothing: if any is invalid, nothing is written. A refusal costs you a\n retry, not data — so prefer one call for changes that belong together, and do\n not split a batch to "make it more likely to succeed".\n\n- **Read before replacing.** `cerefox_get_document(section: "## Heading")`\n returns exactly what a `replace_section` on that anchor would overwrite. Use it\n for any section you did not write in this session. The outline gives a\n section\'s *size*, never its *text*.\n\n- **Verify after writing** — read the result back before reporting success, and\n report what the read actually shows.\n\n- **Partial edits cannot change a document\'s stored TITLE.** `rename_section`\n changes a heading inside the content; the title is a separate field and still\n needs `cerefox_ingest`.\n\n- **If a capability seems missing from one server, suspect your client first.**\n Local and remote run the same code. **Every `cerefox_get_help()` response\n begins with the server\'s version and the operations it registers** — you do\n not need a special topic, and the *absence* of that block is itself an answer:\n a server that does not print it predates v1.5.0. If that\n disagrees with your tool list, the client is holding a list it fetched before\n an upgrade — clients cache it at connect time. Ask the user to restart the\n client. Do not record a capability difference between servers as a fact; every\n such report so far has been a stale client.\n- Long inline bodies can arrive with literal `\\n`/`\\"` (the author over-escaped; Cerefox stores bytes faithfully). For long or quote-dense content, ingest from a file or build incrementally with `cerefox_insert`; read back multi-line writes.\n', HELP_SECTIONS, HELP_SECTION_HEADINGS;
51409
51440
  var init_get_help_content = __esm(() => {
51410
51441
  HELP_SECTIONS = {
51411
51442
  Tools: "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `author` |\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 10), `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_insert` | **Add** to a document without resending it. Cannot destroy content. | `document_id`, `text`, `position` (`end_of_document`/`end_of_section`/`after_heading`/`before_heading`), `expected_content_hash` (required), `anchor_heading` (unless `end_of_document`), `section_part`, `author` |\n| `cerefox_edit` | **Change** parts of a document: 1..n operations applied atomically | `document_id`, `operations` (`insert`/`replace_section`/`delete_section`/`rename_section`), `expected_content_hash` (required), `author` |\n| `cerefox_delete_document` | **Soft**-delete a document (to trash; excluded from search; permanent purge is human-only) | `document_id`, `expected_content_hash` (**required** — a delete must follow a read), `reason` (recorded in the audit log — give one), `author` |\n| `cerefox_restore_document` | Restore a soft-deleted document from the trash (audited inverse of delete; no-op if not deleted) | `document_id` (required), `reason` (recorded in the audit log), `author` |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token), or with `outline: true` just its heading paths, sizes and hash, or with `section: \"## Heading\"` one section's text | `document_id` (required), `outline`, `section`, `section_part`, `author` |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required), `author` |\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`, `author` |\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_metadata` | Change tags WITHOUT resending content. **Merges** by default; a `null` value removes a key | `document_id`, `metadata` (required), `replace` (rare: set exactly this object), `author` |\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), `author` |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `by_author` (filter), `operation`, `since`, `author` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n⚑ **Opt-in — usually absent.** The four relation tools are hidden unless the\noperator enables them (`relations_enabled`). **Trust your own tool list**: if\nthey are not in it, the feature is switched off for this deployment. That is\nnormal, not an error, and not something to work around.",
51412
51443
  "Editing part of a document (prefer this over re-sending)": '## Editing part of a document (prefer this over re-sending)\n\n**Re-sending a whole document to change part of it is the main way agents lose\ndata.** You have to reproduce the untouched remainder verbatim, and any drift\nsilently rewrites content nobody asked you to touch — which the caller cannot\ndiff. Use the partial-edit tools instead:\n\n1. **Learn the anchors** — `cerefox_get_document(document_id, outline: true)`.\n Returns heading paths, per-section sizes and the `content_hash`, without the\n body. The paths it returns are exactly what `anchor_heading` accepts.\n2. **Add** → `cerefox_insert`. `end_of_document` is a plain append;\n `end_of_section` adds inside a named section. It is structurally incapable of\n removing anything, so "I meant to append" cannot become "I replaced the file".\n3. **Look before you overwrite** — `cerefox_get_document(document_id,\n section: "## Heading")` returns exactly the text a `replace_section` on that\n anchor would destroy. The outline gives you a section\'s *size*, never its\n *text*, so on a document you did not write yourself this is the difference\n between a replace and a blind overwrite.\n4. **Change or remove** → `cerefox_edit`. Put changes that belong together in\n ONE call: they apply atomically, so a table row and the total it feeds cannot\n end up disagreeing. To change a single line, `replace_section` on its\n smallest enclosing heading — that is the intended granularity, not a\n workaround. To fix a stale heading (`## OPEN TODOs (as of ...)`), use\n `rename_section`: it changes the heading text and leaves the body and\n position alone.\n5. All of them require `expected_content_hash` and **have no last-write-wins**. A\n conflict means someone else changed the document; re-read and decide, do not\n force it.\n\n**A section runs to the next same-or-higher heading, or to the end of the\ndocument.** So `end_of_document` inserts land inside the *last* section, and\nreplacing or deleting that section removes them too. A large shrink in the\nresponse is your warning; `cerefox_list_versions` has the previous content.\n\n**When an anchor is ambiguous the tool refuses and hands you the options** — a\nrepeated heading returns the qualifying paths, and a section with both its own\ncontent and sub-sections returns both `section_part` choices. That is a\nrecoverable answer, not a failure: retry with what it gave you.',
51413
- "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`** to your name on every call, reads and writes alike (e.g., "Claude Code", "archiver"). Same parameter on every tool. (`requestor` is still accepted everywhere as the pre-1.13.1 alias.) On the CLI it is `--author` on every command too (plus `--author-type` on writes); or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **`max_bytes` has to fit a whole document.** Search returns COMPLETE documents, so a budget smaller than the top hit fits nothing. You get a header-only list saying so (never "No results found." — that means the store really has nothing). Raise `max_bytes`, or read one document with `cerefox_get_document` using `outline: true` or `section`.\n6. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n7. **Write for retrieval: the title does real work.** The document title is indexed at the **highest weight** *and* prepended to every chunk before it is embedded, so it steers both halves of hybrid search (keyword and semantic) for every chunk in the document. Name the document with the distinctive terms someone would actually search for -- "Cerefox Response Size Limits", not "Notes" or "Update 3". Renaming re-indexes and re-embeds, so correcting a vague title is cheap and takes effect immediately. Inside the content, write structured Markdown with H1/H2/H3 headings: the chunker follows them, and a chunk\'s own heading carries the same weight as the title.\n8. **Deletes are soft (recoverable); permanent purge is web-UI-only.** `cerefox_delete_document` requires the document\'s `content_hash` as you read it (read before you delete) and takes a `reason` — give one; it is what the human reviewing the trash sees. `cerefox_restore_document` undoes a mistaken delete (also audited, also takes a `reason`). Always surface deletes AND restores to the user. Once a human purges from the web UI, the document is gone for good.\n9. **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. **The server validates `](uuid)` links on every write** (v1.7.0): a link to a nonexistent id rejects the write, naming the offender — that means you mangled the UUID; re-read the source and correct it, do not retry unchanged. Example ids go in backticks (code is not validated). `[[Wikilinks]]` may dangle. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n10. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. 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.\n11. **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.\n12. **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.\n13. **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`.',
51444
+ "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`** to your name on every call, reads and writes alike (e.g., "Claude Code", "archiver"). Same parameter on every tool. (`requestor` is still accepted everywhere as the pre-1.13.1 alias.) On the CLI it is `--author` on every command too (plus `--author-type` on writes); or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE` env vars set in the user\'s `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **`max_bytes` has to fit a whole document.** Search returns COMPLETE documents, so a budget smaller than the top hit fits nothing. You get a header-only list saying so (never "No results found." — that means the store really has nothing). Raise `max_bytes`, or read one document with `cerefox_get_document` using `outline: true` or `section`.\n6. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n7. **Write for retrieval: the title does real work.** The document title is indexed at the **highest weight** *and* prepended to every chunk before it is embedded, so it steers both halves of hybrid search (keyword and semantic) for every chunk in the document. Name the document with the distinctive terms someone would actually search for -- "Cerefox Response Size Limits", not "Notes" or "Update 3". Renaming re-indexes and re-embeds, so correcting a vague title is cheap and takes effect immediately. Inside the content, write structured Markdown with H1/H2/H3 headings: the chunker follows them, and a chunk\'s own heading carries the same weight as the title.\n8. **Deletes are soft (recoverable); permanent purge is web-UI-only.** `cerefox_delete_document` requires the document\'s `content_hash` as you read it (read before you delete) and takes a `reason` — give one; it is what the human reviewing the trash sees. `cerefox_restore_document` undoes a mistaken delete (also audited, also takes a `reason`). Always surface deletes AND restores to the user. Once a human purges from the web UI, the document is gone for good; so is one auto-purged after `trash_retention_days`, if the operator has turned trash auto-purge on.\n9. **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. **The server validates `](uuid)` links on every write** (v1.7.0): a link to a nonexistent id rejects the write, naming the offender — that means you mangled the UUID; re-read the source and correct it, do not retry unchanged. Example ids go in backticks (code is not validated). `[[Wikilinks]]` may dangle. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n10. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you last saw — every read shows one (`cerefox_get_document` incl. outline mode, `cerefox_search`, `cerefox_metadata_search`) and **every write returns the new one, including create** (v1.3.0, #189), so after writing you already hold the token for your next edit; no re-read needed. 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.\n11. **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.\n12. **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.\n13. **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`.',
51414
51445
  "Update Workflow (ID-based -- preferred)": `## Update Workflow (ID-based -- preferred)
51415
51446
 
51416
51447
  \`\`\`
@@ -51652,6 +51683,7 @@ async function handler6(supabase, args, ctx) {
51652
51683
  throw new McpInvalidParams('metadata must be a JSON object of key/value pairs, e.g. {"type":"note"} — not a string, number, or array');
51653
51684
  }
51654
51685
  const update_if_exists = args.update_if_exists ?? false;
51686
+ const overrideNote = args.update_if_exists === false ? " Note: update_if_exists flag was overridden by document_id." : "";
51655
51687
  const author = callerIdentity(args) ?? DEFAULT_IDENTITY;
51656
51688
  const author_type = "agent";
51657
51689
  const expected_content_hash = args.expected_content_hash?.trim() || null;
@@ -51677,7 +51709,7 @@ async function handler6(supabase, args, ctx) {
51677
51709
  throw new McpInvalidParams(`Document ${document_id} ("${existingDoc.title}") is soft-deleted (in the trash). ` + `A trashed document cannot be updated — restore it first with ` + `cerefox_restore_document, then retry, or create a new document instead.`);
51678
51710
  }
51679
51711
  if (existingDoc.content_hash === contentHash) {
51680
- const note = update_if_exists ? "" : " Note: update_if_exists flag was overridden by document_id.";
51712
+ const note = overrideNote;
51681
51713
  return `Document already up-to-date: "${existingDoc.title}" (id: ${existingDoc.id}). Content hash unchanged (${contentHash}).${note}`;
51682
51714
  }
51683
51715
  if (!last_write_wins && expected_content_hash && expected_content_hash !== existingDoc.content_hash) {
@@ -51727,7 +51759,7 @@ async function handler6(supabase, args, ctx) {
51727
51759
  } else if (project_name) {
51728
51760
  await ensureDocumentInProject(supabase, existingDoc.id, project_name, { author, authorType: author_type });
51729
51761
  }
51730
- const note = update_if_exists ? "" : " Note: update_if_exists flag was overridden by document_id.";
51762
+ const note = overrideNote;
51731
51763
  return `Document updated: "${title}" (id: ${existingDoc.id}), ${chunks.length} chunk(s), ${totalChars} chars. New content_hash: ${contentHash}.${note}${escapedContentNote(content)}`;
51732
51764
  }
51733
51765
  if (update_if_exists) {
@@ -53467,7 +53499,7 @@ var require_cli_progress = __commonJS(function(exports, module) {
53467
53499
  });
53468
53500
 
53469
53501
  // src/cli/commands/sync-self-docs.ts
53470
- import { readFileSync as readFileSync12 } from "node:fs";
53502
+ import { readFileSync as readFileSync13 } from "node:fs";
53471
53503
  import { basename as basename4, extname as extname5 } from "node:path";
53472
53504
  async function runSyncSelfDocs(options = {}) {
53473
53505
  const project = options.project ?? "_cerefox-self-docs";
@@ -53494,7 +53526,7 @@ async function runSyncSelfDocs(options = {}) {
53494
53526
  const authorType = resolveAuthorType("agent");
53495
53527
  const outcomes = [];
53496
53528
  for (const doc of docs) {
53497
- const content = readFileSync12(doc.path, "utf8");
53529
+ const content = readFileSync13(doc.path, "utf8");
53498
53530
  const m = content.match(/^#\s+(.+)$/m);
53499
53531
  const title = m ? m[1].trim() : basename4(doc.path, extname5(doc.path));
53500
53532
  try {
@@ -58571,7 +58603,7 @@ var require_data = __commonJS(function(exports, module) {
58571
58603
  };
58572
58604
  });
58573
58605
 
58574
- // ../../node_modules/.bun/fast-uri@3.1.7/node_modules/fast-uri/lib/utils.js
58606
+ // ../../node_modules/.bun/fast-uri@3.1.8/node_modules/fast-uri/lib/utils.js
58575
58607
  var require_utils2 = __commonJS(function(exports, module) {
58576
58608
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
58577
58609
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
@@ -59088,7 +59120,7 @@ var require_utils2 = __commonJS(function(exports, module) {
59088
59120
  };
59089
59121
  });
59090
59122
 
59091
- // ../../node_modules/.bun/fast-uri@3.1.7/node_modules/fast-uri/lib/schemes.js
59123
+ // ../../node_modules/.bun/fast-uri@3.1.8/node_modules/fast-uri/lib/schemes.js
59092
59124
  var require_schemes = __commonJS(function(exports, module) {
59093
59125
  var { isUUID } = require_utils2();
59094
59126
  var URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu;
@@ -59263,7 +59295,7 @@ var require_schemes = __commonJS(function(exports, module) {
59263
59295
  };
59264
59296
  });
59265
59297
 
59266
- // ../../node_modules/.bun/fast-uri@3.1.7/node_modules/fast-uri/index.js
59298
+ // ../../node_modules/.bun/fast-uri@3.1.8/node_modules/fast-uri/index.js
59267
59299
  var require_fast_uri = __commonJS(function(exports, module) {
59268
59300
  var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils2();
59269
59301
  var { SCHEMES, getSchemeHandler } = require_schemes();
@@ -59593,13 +59625,14 @@ var require_fast_uri = __commonJS(function(exports, module) {
59593
59625
  if (!malformedIPLiteral) {
59594
59626
  malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
59595
59627
  }
59596
- if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
59597
- if (uri.indexOf("%") !== -1) {
59598
- if (parsed.host !== undefined && !malformedIPLiteral) {
59599
- const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
59600
- parsed.host = reescapeHostDelimiters(host, isIP);
59601
- }
59628
+ if (uri.indexOf("%") !== -1 && parsed.host !== undefined && !malformedIPLiteral) {
59629
+ let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
59630
+ if (!isIP) {
59631
+ host = normalizePercentEncoding(host.toLowerCase());
59602
59632
  }
59633
+ parsed.host = reescapeHostDelimiters(host, isIP);
59634
+ }
59635
+ if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
59603
59636
  if (parsed.path) {
59604
59637
  parsed.path = normalizePathEncoding(parsed.path);
59605
59638
  }
@@ -63058,7 +63091,6 @@ var init_stdio2 = __esm(() => {
63058
63091
  });
63059
63092
 
63060
63093
  // src/server.ts
63061
- import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
63062
63094
  function buildServer() {
63063
63095
  const settings = loadSettings();
63064
63096
  if (!settings.supabaseUrl || !settings.supabaseKey) {
@@ -63109,14 +63141,7 @@ async function warnIfSchemaVersionMismatch(supabase) {
63109
63141
  const deployed = typeof data === "string" ? data : null;
63110
63142
  if (!deployed)
63111
63143
  return;
63112
- let bundled = null;
63113
- try {
63114
- const assets = resolveServerAssets();
63115
- if (existsSync13(assets.schemaFile)) {
63116
- const m = readFileSync14(assets.schemaFile, "utf8").match(SCHEMA_VERSION_RE2);
63117
- bundled = m ? m[1] : null;
63118
- }
63119
- } catch {}
63144
+ const bundled = bundledSchemaVersion();
63120
63145
  if (!bundled)
63121
63146
  return;
63122
63147
  if (compareSemver(deployed, bundled) < 0) {
@@ -63125,7 +63150,7 @@ async function warnIfSchemaVersionMismatch(supabase) {
63125
63150
  }
63126
63151
  } catch {}
63127
63152
  }
63128
- var SERVER_NAME = "cerefox", SCHEMA_VERSION_RE2;
63153
+ var SERVER_NAME = "cerefox";
63129
63154
  var init_server3 = __esm(() => {
63130
63155
  init_server2();
63131
63156
  init_stdio2();
@@ -63136,7 +63161,6 @@ var init_server3 = __esm(() => {
63136
63161
  init_server_assets();
63137
63162
  init_mcp_tools();
63138
63163
  init_meta();
63139
- SCHEMA_VERSION_RE2 = /^--\s*@version:\s*(\S+)/m;
63140
63164
  });
63141
63165
 
63142
63166
  // src/bin/cerefox.ts
@@ -65723,6 +65747,27 @@ var CONFIG_CATALOG = [
65723
65747
  highImpact: true,
65724
65748
  impactNote: "Turning this OFF keeps every version of every document forever. Versions carry embeddings, so storage grows without bound — on a busy store that is the largest table. Turning it back ON prunes on the next save, which permanently deletes versions outside the window (the newest and any archived ones survive)."
65725
65749
  },
65750
+ {
65751
+ key: "trash_auto_purge_enabled",
65752
+ description: "Permanently purge documents that have been in the trash longer than the trash retention period. Swept by the next document delete (there is no scheduler): a document becomes eligible after the period and is removed by the next delete after that.",
65753
+ kind: "boolean",
65754
+ defaultValue: "false",
65755
+ group: "Retention",
65756
+ highImpact: true,
65757
+ impactNote: "Turning this ON makes every document delete also purge, permanently, the documents that have been in the trash longer than the retention period, up to 100 per delete, including ones already in the trash today. Purged documents cannot be restored. Each purge is recorded in the audit log as 'trash-retention'."
65758
+ },
65759
+ {
65760
+ key: "trash_retention_days",
65761
+ description: "Days a document stays in the trash before auto-purge may remove it (only while trash auto-purge is on). This is the window in which a trashed document can still be restored.",
65762
+ kind: "number",
65763
+ defaultValue: "60",
65764
+ min: 1,
65765
+ max: 3650,
65766
+ integer: true,
65767
+ group: "Retention",
65768
+ highImpact: true,
65769
+ impactNote: "Shortening this while auto-purge is on makes more of the current trash eligible: the next delete purges documents older than the new period, permanently."
65770
+ },
65726
65771
  {
65727
65772
  key: "document_size_warning_chars",
65728
65773
  description: "Flag writes that push a document past this many characters (0 = off). Partial edits make writes cheap, so an agent that only ever inserts never assembles the document and never sees it grow; this puts the fact in the write's response. A signal only — writes are never blocked.",
@@ -65766,6 +65811,9 @@ function validateConfigValue(key, value) {
65766
65811
  if (!Number.isFinite(n)) {
65767
65812
  return `${key} must be a number (got ${JSON.stringify(value)}).`;
65768
65813
  }
65814
+ if (spec.integer && !Number.isInteger(n)) {
65815
+ return `${key} must be a whole number (got ${n}).`;
65816
+ }
65769
65817
  if (spec.min !== undefined && n < spec.min) {
65770
65818
  return `${key} must be ≥ ${spec.min} (got ${n}).`;
65771
65819
  }
@@ -67333,6 +67381,9 @@ async function action7(documentId, options) {
67333
67381
  if (options.reason) {
67334
67382
  println(c.dim(` Reason (recorded in the audit log): ${options.reason}`));
67335
67383
  }
67384
+ if (result?.auto_purged) {
67385
+ println(c.yellow(` Trash auto-purge: this delete also permanently purged ${result.auto_purged} document(s) ` + `that had been in the trash longer than trash_retention_days.`));
67386
+ }
67336
67387
  }
67337
67388
  function registerDeleteDoc(program) {
67338
67389
  program.command("delete-doc").description("Soft-delete a document (recoverable via the web UI trash).").argument("<document-id>", "UUID of the document to delete.").option("--reason <text>", "Optional reason recorded in the audit log.").option("-a, --author <name>", "Caller identity (audit log).").option("--author-type <type>", "'user' or 'agent' (default: user).", "user").option("--yes", "Skip the confirmation prompt.").action(action7);
@@ -67439,7 +67490,7 @@ import { readdirSync as readdirSync2 } from "node:fs";
67439
67490
 
67440
67491
  // ../../_shared/db-deploy/index.ts
67441
67492
  init_src();
67442
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync } from "node:fs";
67493
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync } from "node:fs";
67443
67494
  import { join as join6 } from "node:path";
67444
67495
  var RESET_SQL = `
67445
67496
  DROP TABLE IF EXISTS cerefox_chunks CASCADE;
@@ -67462,8 +67513,8 @@ function listMigrationFiles(migrationsDir) {
67462
67513
  return readdirSync(migrationsDir).filter((n) => n.endsWith(".sql")).sort();
67463
67514
  }
67464
67515
  function buildDeploySteps(assets, opts = {}) {
67465
- const schemaSql = readFileSync5(assets.schemaFile, "utf8");
67466
- const rpcsSql = readFileSync5(assets.rpcsFile, "utf8");
67516
+ const schemaSql = readFileSync6(assets.schemaFile, "utf8");
67517
+ const rpcsSql = readFileSync6(assets.rpcsFile, "utf8");
67467
67518
  const steps = [];
67468
67519
  if (opts.reset) {
67469
67520
  steps.push({ label: "Reset: drop existing Cerefox objects", sql: RESET_SQL });
@@ -67575,7 +67626,7 @@ async function runDbMigrate(opts) {
67575
67626
  }
67576
67627
  const applied = [];
67577
67628
  for (const f of pending) {
67578
- const body = readFileSync5(join6(opts.assets.migrationsDir, f), "utf8");
67629
+ const body = readFileSync6(join6(opts.assets.migrationsDir, f), "utf8");
67579
67630
  log(`Applying ${f}…`);
67580
67631
  try {
67581
67632
  await sql.begin(async (tx) => {
@@ -67605,7 +67656,7 @@ async function applyRpcs(opts) {
67605
67656
  log("Refresh RPCs (rpcs.sql)…");
67606
67657
  if (opts.dryRun)
67607
67658
  return { ok: true };
67608
- const rpcsSql = readFileSync5(opts.assets.rpcsFile, "utf8");
67659
+ const rpcsSql = readFileSync6(opts.assets.rpcsFile, "utf8");
67609
67660
  const sql = src_default(opts.dbUrl, { prepare: false, onnotice: () => {} });
67610
67661
  try {
67611
67662
  await sql.unsafe(rpcsSql);
@@ -71723,7 +71774,7 @@ init_config();
71723
71774
  init_config();
71724
71775
  init_compatibility();
71725
71776
  init_server_assets();
71726
- import { existsSync as existsSync11, readFileSync as readFileSync8, realpathSync, statSync as statSync2 } from "node:fs";
71777
+ import { existsSync as existsSync11, readFileSync as readFileSync9, realpathSync, statSync as statSync2 } from "node:fs";
71727
71778
  import { homedir as homedir7 } from "node:os";
71728
71779
  import { join as join10 } from "node:path";
71729
71780
 
@@ -71733,7 +71784,7 @@ import {
71733
71784
  existsSync as existsSync9,
71734
71785
  mkdirSync as mkdirSync3,
71735
71786
  openSync,
71736
- readFileSync as readFileSync7,
71787
+ readFileSync as readFileSync8,
71737
71788
  rmSync,
71738
71789
  writeFileSync as writeFileSync4
71739
71790
  } from "node:fs";
@@ -71757,7 +71808,7 @@ function readPidFile() {
71757
71808
  if (!existsSync9(PID_FILE))
71758
71809
  return null;
71759
71810
  try {
71760
- const parsed = JSON.parse(readFileSync7(PID_FILE, "utf8"));
71811
+ const parsed = JSON.parse(readFileSync8(PID_FILE, "utf8"));
71761
71812
  if (typeof parsed.pid !== "number")
71762
71813
  return null;
71763
71814
  return {
@@ -72153,18 +72204,6 @@ async function checkOpenAI() {
72153
72204
  };
72154
72205
  }
72155
72206
  }
72156
- var SCHEMA_VERSION_RE = /^--\s*@version:\s*(\S+)/m;
72157
- function readBundledSchemaVersion() {
72158
- try {
72159
- const assets = resolveServerAssets();
72160
- if (!existsSync11(assets.schemaFile))
72161
- return null;
72162
- const m = readFileSync8(assets.schemaFile, "utf8").match(SCHEMA_VERSION_RE);
72163
- return m ? m[1] : null;
72164
- } catch {
72165
- return null;
72166
- }
72167
- }
72168
72207
  var SCHEMA_CHECK_NAME = "schema + RPCs";
72169
72208
  async function checkSchemaVersion() {
72170
72209
  const settings = loadSettings();
@@ -72195,7 +72234,7 @@ async function checkSchemaVersion() {
72195
72234
  };
72196
72235
  }
72197
72236
  const deployed = await resp.json();
72198
- const bundled = readBundledSchemaVersion();
72237
+ const bundled = bundledSchemaVersion();
72199
72238
  const level = classifyCompat(deployed, COMPATIBILITY.minSchema, bundled);
72200
72239
  switch (level) {
72201
72240
  case "below-min":
@@ -72269,6 +72308,51 @@ async function checkReviewWorkflow() {
72269
72308
  hint: on ? undefined : "Turn it on with `cerefox config set review_workflow_enabled true`."
72270
72309
  };
72271
72310
  }
72311
+ async function checkTrashAutoPurge() {
72312
+ const name = "trash auto-purge";
72313
+ const settings = loadSettings();
72314
+ if (!settings.supabaseUrl || !settings.supabaseKey) {
72315
+ return { name, status: "skipped", detail: "no Supabase credentials" };
72316
+ }
72317
+ const read = async (key) => {
72318
+ const resp = await fetch(`${settings.supabaseUrl.replace(/\/$/, "")}/rest/v1/rpc/cerefox_get_config`, {
72319
+ method: "POST",
72320
+ headers: {
72321
+ "Content-Type": "application/json",
72322
+ apikey: settings.supabaseKey,
72323
+ Authorization: `Bearer ${settings.supabaseKey}`
72324
+ },
72325
+ body: JSON.stringify({ p_key: key })
72326
+ });
72327
+ if (!resp.ok)
72328
+ throw new Error(`HTTP ${resp.status}`);
72329
+ const body = await resp.json();
72330
+ return typeof body === "string" ? body : null;
72331
+ };
72332
+ let enabled;
72333
+ let days;
72334
+ try {
72335
+ [enabled, days] = await Promise.all([read("trash_auto_purge_enabled"), read("trash_retention_days")]);
72336
+ } catch (err) {
72337
+ return { name, status: "skipped", detail: `could not read config: ${err.message}` };
72338
+ }
72339
+ if (enabled == null) {
72340
+ return { name, status: "ok", detail: "not available on this schema (needs 0.17.0; the trash is never auto-purged)" };
72341
+ }
72342
+ if (enabled.trim() !== "true") {
72343
+ return {
72344
+ name,
72345
+ status: "ok",
72346
+ detail: "off — the trash is only emptied by hand",
72347
+ hint: "Opt in with `cerefox config set trash_auto_purge_enabled true` (period: trash_retention_days)."
72348
+ };
72349
+ }
72350
+ return {
72351
+ name,
72352
+ status: "ok",
72353
+ detail: `ON — each delete also purges documents trashed more than ${days ?? "?"} day(s) ago (up to 100 per delete)`
72354
+ };
72355
+ }
72272
72356
  async function checkEmbedderMismatch() {
72273
72357
  const settings = loadSettings();
72274
72358
  if (!settings.supabaseUrl || !settings.supabaseKey) {
@@ -72405,7 +72489,7 @@ function hasCerefoxInJsonFile(path) {
72405
72489
  if (!existsSync11(path))
72406
72490
  return false;
72407
72491
  try {
72408
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
72492
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
72409
72493
  const mcpServers = parsed.mcpServers;
72410
72494
  return Boolean(mcpServers && typeof mcpServers === "object" && "cerefox" in mcpServers);
72411
72495
  } catch {
@@ -72658,6 +72742,7 @@ async function runAllChecks(opts = {}) {
72658
72742
  { name: "openai", phase: "Probing OpenAI embeddings", run: () => checkOpenAI() },
72659
72743
  { name: "schema + RPCs", phase: "Reading schema + RPC version", run: () => checkSchemaVersion() },
72660
72744
  { name: "review workflow", phase: "Reading review workflow flag", run: () => checkReviewWorkflow() },
72745
+ { name: "trash auto-purge", phase: "Reading trash auto-purge settings", run: () => checkTrashAutoPurge() },
72661
72746
  { name: "embedder", phase: "Checking embedder consistency", run: () => checkEmbedderMismatch() },
72662
72747
  { name: "content format", phase: "Checking chunk reconstruction format", run: () => checkContentFormat() },
72663
72748
  { name: "metadata health", phase: "Checking metadata well-formedness", run: () => checkMetadataHealth() },
@@ -73059,11 +73144,11 @@ function registerGetDoc(program) {
73059
73144
  init_dist4();
73060
73145
  init_cli_core();
73061
73146
  init_config();
73062
- import { readFileSync as readFileSync10 } from "node:fs";
73147
+ import { readFileSync as readFileSync11 } from "node:fs";
73063
73148
  import { basename as basename2, extname as extname3 } from "node:path";
73064
73149
 
73065
73150
  // src/ingestion/pipeline.ts
73066
- import { readFileSync as readFileSync9 } from "node:fs";
73151
+ import { readFileSync as readFileSync10 } from "node:fs";
73067
73152
  import { basename, extname as extname2, resolve as resolve3 } from "node:path";
73068
73153
  // ../../_shared/ingest/pipeline-helpers.ts
73069
73154
  import { createHash } from "node:crypto";
@@ -73389,6 +73474,10 @@ async function fileToMarkdown(filename, data) {
73389
73474
  }
73390
73475
 
73391
73476
  // src/ingestion/pipeline.ts
73477
+ function updateByIdNote(updateExisting) {
73478
+ return updateExisting === false ? "document_id provided; update_if_exists flag was overridden" : undefined;
73479
+ }
73480
+
73392
73481
  class IngestionPipeline {
73393
73482
  db;
73394
73483
  apiKey;
@@ -73453,9 +73542,9 @@ class IngestionPipeline {
73453
73542
  result.projectIds = await this.db.getDocumentProjectIds(documentId);
73454
73543
  }
73455
73544
  }
73456
- if (!updateExisting) {
73457
- result.note = "document_id provided; update_if_exists flag was overridden";
73458
- }
73545
+ const note = updateByIdNote(opts.updateExisting);
73546
+ if (note)
73547
+ result.note = note;
73459
73548
  return result;
73460
73549
  }
73461
73550
  if (updateExisting) {
@@ -73729,7 +73818,7 @@ class IngestionPipeline {
73729
73818
  };
73730
73819
  }
73731
73820
  async ingestFile(path, opts = {}) {
73732
- const text = await fileToMarkdown(path, readFileSync9(path));
73821
+ const text = await fileToMarkdown(path, readFileSync10(path));
73733
73822
  const absPath = resolve3(path);
73734
73823
  const stem = basename(absPath, extname2(absPath));
73735
73824
  return this.ingestText({
@@ -73764,7 +73853,7 @@ async function readContent(path, paste) {
73764
73853
  }
73765
73854
  let content;
73766
73855
  try {
73767
- content = readFileSync10(path, "utf8");
73856
+ content = readFileSync11(path, "utf8");
73768
73857
  } catch (err) {
73769
73858
  const msg = err instanceof Error ? err.message : String(err);
73770
73859
  throw userError(`Cannot read ${path}: ${msg}`);
@@ -73822,7 +73911,7 @@ async function action20(path, options) {
73822
73911
  projectName: options.projectName ?? null,
73823
73912
  projectNames: projectNames ?? null,
73824
73913
  metadata: metadata ?? null,
73825
- updateExisting: Boolean(options.updateIfExists),
73914
+ updateExisting: options.updateIfExists ? true : undefined,
73826
73915
  documentId: options.documentId ?? null,
73827
73916
  author,
73828
73917
  authorType,
@@ -73836,7 +73925,7 @@ async function action20(path, options) {
73836
73925
  projectName: options.projectName ?? null,
73837
73926
  projectNames: projectNames ?? null,
73838
73927
  metadata: metadata ?? null,
73839
- updateExisting: Boolean(options.updateIfExists),
73928
+ updateExisting: options.updateIfExists ? true : undefined,
73840
73929
  documentId: options.documentId ?? null,
73841
73930
  author,
73842
73931
  authorType,
@@ -73874,14 +73963,14 @@ init_cli_core();
73874
73963
  init_mcp_tools();
73875
73964
  init_config();
73876
73965
  init_client();
73877
- import { readFileSync as readFileSync11 } from "node:fs";
73966
+ import { readFileSync as readFileSync12 } from "node:fs";
73878
73967
  function resolveText(value, what) {
73879
73968
  if (value === undefined)
73880
73969
  throw userError(`${what} is required.`);
73881
73970
  if (value === "-")
73882
- return readFileSync11(0, "utf8");
73971
+ return readFileSync12(0, "utf8");
73883
73972
  if (value.startsWith("@"))
73884
- return readFileSync11(value.slice(1), "utf8");
73973
+ return readFileSync12(value.slice(1), "utf8");
73885
73974
  return value;
73886
73975
  }
73887
73976
  function context() {
@@ -74014,7 +74103,7 @@ async function action21(dir, options) {
74014
74103
  sourceOnCreate: "cli",
74015
74104
  projectName: options.projectName ?? null,
74016
74105
  metadata: metadata ?? null,
74017
- updateExisting: Boolean(options.updateIfExists),
74106
+ updateExisting: options.updateIfExists ? true : undefined,
74018
74107
  author,
74019
74108
  authorType,
74020
74109
  lastWriteWins: true
@@ -74057,7 +74146,7 @@ import {
74057
74146
  copyFileSync as copyFileSync2,
74058
74147
  existsSync as existsSync12,
74059
74148
  mkdirSync as mkdirSync5,
74060
- readFileSync as readFileSync13,
74149
+ readFileSync as readFileSync14,
74061
74150
  writeFileSync as writeFileSync5
74062
74151
  } from "node:fs";
74063
74152
  import { homedir as homedir8 } from "node:os";
@@ -74068,7 +74157,7 @@ async function readConfigFile(path) {
74068
74157
  }
74069
74158
  let parsed;
74070
74159
  try {
74071
- parsed = JSON.parse(readFileSync13(path, "utf8"));
74160
+ parsed = JSON.parse(readFileSync14(path, "utf8"));
74072
74161
  } catch (err) {
74073
74162
  throw userError(`--config: invalid JSON in ${path}: ${err instanceof Error ? err.message : String(err)}`);
74074
74163
  }
@@ -74110,7 +74199,7 @@ function parseDotEnvFile(content) {
74110
74199
  return map;
74111
74200
  }
74112
74201
  function answersFromEnvFile(path) {
74113
- const parsed = parseDotEnvFile(readFileSync13(path, "utf8"));
74202
+ const parsed = parseDotEnvFile(readFileSync14(path, "utf8"));
74114
74203
  const required = ["CEREFOX_SUPABASE_URL", "CEREFOX_SUPABASE_KEY", "OPENAI_API_KEY"];
74115
74204
  for (const key of required) {
74116
74205
  if (!parsed[key] || parsed[key].trim() === "") {
@@ -75046,7 +75135,7 @@ function registerMigrateFormat(program) {
75046
75135
  // src/cli/commands/restore.ts
75047
75136
  init_cli_core();
75048
75137
  init_client();
75049
- import { existsSync as existsSync14, readFileSync as readFileSync15, readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
75138
+ import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
75050
75139
  import { homedir as homedir9 } from "node:os";
75051
75140
  import { join as join13, resolve as resolve4 } from "node:path";
75052
75141
  function expandHome2(path) {
@@ -75058,7 +75147,7 @@ function expandHome2(path) {
75058
75147
  }
75059
75148
  function resolveBackupFile(target) {
75060
75149
  const path = resolve4(expandHome2(target));
75061
- if (!existsSync14(path)) {
75150
+ if (!existsSync13(path)) {
75062
75151
  throw userError(`Backup path not found: ${target}`);
75063
75152
  }
75064
75153
  const stat = statSync4(path);
@@ -75549,7 +75638,7 @@ init_config();
75549
75638
  import { randomBytes } from "node:crypto";
75550
75639
 
75551
75640
  // src/cli/util/env-file.ts
75552
- import { copyFileSync as copyFileSync3, existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "node:fs";
75641
+ import { copyFileSync as copyFileSync3, existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "node:fs";
75553
75642
  import { dirname as dirname5 } from "node:path";
75554
75643
  import { spawnSync as spawnSync6 } from "node:child_process";
75555
75644
  function escapeRegExp(s) {
@@ -75559,7 +75648,7 @@ function upsertEnvVar(path, key, value, opts = {}) {
75559
75648
  const line = `${key}=${value}`;
75560
75649
  const header = opts.comment ? `# ${opts.comment}
75561
75650
  ` : "";
75562
- if (!existsSync15(path)) {
75651
+ if (!existsSync14(path)) {
75563
75652
  writeFileSync6(path, `${header}${line}
75564
75653
  `, { mode: 384 });
75565
75654
  return { path, action: "created" };
@@ -75589,7 +75678,7 @@ ${header}${line}
75589
75678
  return { path, action, backupPath };
75590
75679
  }
75591
75680
  function readEnvVar(path, key) {
75592
- if (!existsSync15(path))
75681
+ if (!existsSync14(path))
75593
75682
  return null;
75594
75683
  const m = readFileSync16(path, "utf8").match(new RegExp(`^\\s*${escapeRegExp(key)}=(.*)$`, "m"));
75595
75684
  return m ? m[1].trim() : null;
@@ -75612,7 +75701,7 @@ function envGitignoreWarning(path) {
75612
75701
  }
75613
75702
 
75614
75703
  // src/web/auth.ts
75615
- import { existsSync as existsSync16 } from "node:fs";
75704
+ import { existsSync as existsSync15 } from "node:fs";
75616
75705
 
75617
75706
  // ../../node_modules/.bun/@hono+node-server@2.1.1+2ac783cc5e75a70c/node_modules/@hono/node-server/dist/conninfo.mjs
75618
75707
  var getConnInfo = (c) => {
@@ -75677,7 +75766,7 @@ var LOOPBACK_ADDRESSES = new Set([
75677
75766
  ]);
75678
75767
  function isContainerised() {
75679
75768
  try {
75680
- return existsSync16("/.dockerenv");
75769
+ return existsSync15("/.dockerenv");
75681
75770
  } catch {
75682
75771
  return false;
75683
75772
  }
@@ -77556,7 +77645,7 @@ var _baseMimes = {
77556
77645
  var baseMimes = _baseMimes;
77557
77646
 
77558
77647
  // ../../node_modules/.bun/@hono+node-server@2.1.1+2ac783cc5e75a70c/node_modules/@hono/node-server/dist/serve-static.mjs
77559
- import { createReadStream, existsSync as existsSync17, statSync as statSync5 } from "node:fs";
77648
+ import { createReadStream, existsSync as existsSync16, statSync as statSync5 } from "node:fs";
77560
77649
  import { join as join14 } from "node:path";
77561
77650
  var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
77562
77651
  var ENCODINGS = {
@@ -77630,7 +77719,7 @@ var tryDecodeURI = (str) => tryDecode(str, decodeURI);
77630
77719
  var serveStatic = (options = { root: "" }) => {
77631
77720
  const root = options.root || "";
77632
77721
  const optionPath = options.path;
77633
- if (root !== "" && !existsSync17(root))
77722
+ if (root !== "" && !existsSync16(root))
77634
77723
  console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`);
77635
77724
  return async (c, next) => {
77636
77725
  if (c.finalized)
@@ -77713,7 +77802,7 @@ var serveStatic = (options = { root: "" }) => {
77713
77802
  };
77714
77803
 
77715
77804
  // src/web/server.ts
77716
- import { existsSync as existsSync21 } from "node:fs";
77805
+ import { existsSync as existsSync20 } from "node:fs";
77717
77806
  import { readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
77718
77807
  import { join as join18 } from "node:path";
77719
77808
 
@@ -80066,9 +80155,14 @@ function registerDiscoveryRoutes(app, ctx) {
80066
80155
  });
80067
80156
  app.get("/api/v1/documents/trash", async (c) => {
80068
80157
  const limit = Math.min(Math.max(Number.parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 500);
80158
+ const deletedBefore = c.req.query("deleted_before");
80159
+ if (deletedBefore !== undefined && Number.isNaN(Date.parse(deletedBefore))) {
80160
+ return c.json({ detail: "deleted_before must be an ISO-8601 timestamp" }, 400);
80161
+ }
80162
+ const cutoff = deletedBefore !== undefined ? new Date(deletedBefore).toISOString() : "infinity";
80069
80163
  const [listed, counted] = await Promise.all([
80070
- ctx.supabase.from("cerefox_documents").select("id, title, source, chunk_count, total_chars, review_status, deleted_at, updated_at").not("deleted_at", "is", null).order("deleted_at", { ascending: false }).limit(limit),
80071
- ctx.supabase.from("cerefox_documents").select("id", { count: "exact", head: true }).not("deleted_at", "is", null)
80164
+ ctx.supabase.from("cerefox_documents").select("id, title, source, chunk_count, total_chars, review_status, deleted_at, updated_at").not("deleted_at", "is", null).lt("deleted_at", cutoff).order("deleted_at", { ascending: false }).limit(limit),
80165
+ ctx.supabase.from("cerefox_documents").select("id", { count: "exact", head: true }).not("deleted_at", "is", null).lt("deleted_at", cutoff)
80072
80166
  ]);
80073
80167
  const { data, error } = listed;
80074
80168
  if (error)
@@ -80574,7 +80668,11 @@ function registerDocumentWriteRoutes(app, ctx) {
80574
80668
  return c.json({ detail: error.message }, 500);
80575
80669
  }
80576
80670
  const row = data;
80577
- return c.json({ success: true, ...row ? { already_deleted: row.already_deleted ?? false } : {} });
80671
+ return c.json({
80672
+ success: true,
80673
+ ...row ? { already_deleted: row.already_deleted ?? false } : {},
80674
+ ...typeof row?.auto_purged === "number" ? { auto_purged: row.auto_purged } : {}
80675
+ });
80578
80676
  });
80579
80677
  app.post("/api/v1/documents/:document_id/restore", async (c) => {
80580
80678
  const documentId = c.req.param("document_id");
@@ -80790,7 +80888,7 @@ function registerIngestRoutes(app, ctx) {
80790
80888
  projectNames: Array.isArray(body.project_names) ? body.project_names : null,
80791
80889
  projectName: typeof body.project_name === "string" ? body.project_name : null,
80792
80890
  metadata: body.metadata ?? null,
80793
- updateExisting: body.update_existing === true || body.update_if_exists === true,
80891
+ updateExisting: body.update_existing == null && body.update_if_exists == null ? undefined : body.update_existing === true || body.update_if_exists === true,
80794
80892
  documentId: body.document_id ?? null,
80795
80893
  author: who.identity.author,
80796
80894
  authorType: who.identity.authorType,
@@ -80955,12 +81053,15 @@ function registerIngestRoutes(app, ctx) {
80955
81053
  }
80956
81054
 
80957
81055
  // src/web/routes/meta.ts
81056
+ init_server_assets();
80958
81057
  init_meta();
80959
81058
  import { execFileSync } from "node:child_process";
81059
+ import { dirname as dirname7 } from "node:path";
81060
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
80960
81061
 
80961
81062
  // src/web/docs.ts
80962
81063
  import {
80963
- existsSync as existsSync18,
81064
+ existsSync as existsSync17,
80964
81065
  readFileSync as readFileSync17,
80965
81066
  readdirSync as readdirSync6,
80966
81067
  statSync as statSync6
@@ -80993,7 +81094,7 @@ function resolveDocsRoots() {
80993
81094
  let pkgTopLevel = null;
80994
81095
  for (const pkg of pkgRootCandidates) {
80995
81096
  const guides = join15(pkg, "docs", "guides");
80996
- if (existsSync18(guides) && statSync6(guides).isDirectory()) {
81097
+ if (existsSync17(guides) && statSync6(guides).isDirectory()) {
80997
81098
  pkgGuides = guides;
80998
81099
  pkgTopLevel = pkg;
80999
81100
  break;
@@ -81005,8 +81106,8 @@ function resolveDocsRoots() {
81005
81106
  return {
81006
81107
  pkgGuides,
81007
81108
  pkgTopLevel,
81008
- repoGuides: existsSync18(repoGuides) ? repoGuides : null,
81009
- repoTopLevel: existsSync18(join15(repoTopLevel, "README.md")) ? repoTopLevel : null
81109
+ repoGuides: existsSync17(repoGuides) ? repoGuides : null,
81110
+ repoTopLevel: existsSync17(join15(repoTopLevel, "README.md")) ? repoTopLevel : null
81010
81111
  };
81011
81112
  }
81012
81113
  function readH1(filePath) {
@@ -81032,7 +81133,7 @@ function listBundledDocs2() {
81032
81133
  if (topRoot) {
81033
81134
  for (const t of TOP_LEVEL_DOCS) {
81034
81135
  const abs = join15(topRoot, t.filename);
81035
- if (existsSync18(abs)) {
81136
+ if (existsSync17(abs)) {
81036
81137
  entries.push(entryForFile(abs, t.path, t.category));
81037
81138
  }
81038
81139
  }
@@ -81059,7 +81160,7 @@ function readDoc(docPath) {
81059
81160
  if (!candidate.startsWith(rootResolved + "/") && candidate !== rootResolved) {
81060
81161
  return null;
81061
81162
  }
81062
- if (existsSync18(candidate) && statSync6(candidate).isFile()) {
81163
+ if (existsSync17(candidate) && statSync6(candidate).isFile()) {
81063
81164
  try {
81064
81165
  return readFileSync17(candidate, "utf8");
81065
81166
  } catch {
@@ -81068,15 +81169,30 @@ function readDoc(docPath) {
81068
81169
  }
81069
81170
  return null;
81070
81171
  }
81172
+ function readOpenApiDocument() {
81173
+ const here = moduleDir2();
81174
+ const fromSource = here.endsWith(join15("src", "web"));
81175
+ const repoCopy = join15(here, "..", "..", "..", "..", "docs", "api", "openapi.json");
81176
+ const candidates = fromSource ? [repoCopy] : [
81177
+ join15(here, "..", "..", "docs", "api", "openapi.json"),
81178
+ repoCopy
81179
+ ];
81180
+ for (const p of candidates) {
81181
+ if (existsSync17(p))
81182
+ return readFileSync17(p, "utf8");
81183
+ }
81184
+ return null;
81185
+ }
81071
81186
 
81072
81187
  // src/web/routes/meta.ts
81073
81188
  init_compatibility();
81074
- function resolveGitCommitShort() {
81189
+ function resolveGitCommitShort(dir = dirname7(fileURLToPath4(import.meta.url))) {
81075
81190
  const env = process.env.CEREFOX_GIT_COMMIT;
81076
81191
  if (env)
81077
81192
  return env.slice(0, 7);
81078
81193
  try {
81079
81194
  const out = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
81195
+ cwd: dir,
81080
81196
  timeout: 2000,
81081
81197
  stdio: ["ignore", "pipe", "ignore"]
81082
81198
  }).toString().trim();
@@ -81090,7 +81206,6 @@ var VERSION_INFO = {
81090
81206
  git_commit_short: resolveGitCommitShort(),
81091
81207
  build_date: process.env.CEREFOX_BUILD_DATE ?? null
81092
81208
  };
81093
- var SCHEMA_VERSION_RE3 = /^--\s*@version:\s*(\S+)/m;
81094
81209
  function registerMetaRoutes(app, ctx) {
81095
81210
  app.get("/api/v1/version", (c) => {
81096
81211
  const label = (process.env.CEREFOX_ENV_LABEL ?? "").trim();
@@ -81107,28 +81222,15 @@ function registerMetaRoutes(app, ctx) {
81107
81222
  "Content-Type": "text/markdown; charset=utf-8"
81108
81223
  });
81109
81224
  });
81110
- app.get("/api/v1/schema-version", async (c) => {
81111
- let bundled = null;
81112
- try {
81113
- const { readFileSync, existsSync } = await import("node:fs");
81114
- const { fileURLToPath } = await import("node:url");
81115
- const { dirname, join } = await import("node:path");
81116
- const here = dirname(fileURLToPath(import.meta.url));
81117
- const candidates = [
81118
- join(here, "..", "..", "..", "db", "schema.sql"),
81119
- join(here, "..", "..", "..", "..", "..", "src", "cerefox", "db", "schema.sql")
81120
- ];
81121
- for (const path of candidates) {
81122
- if (existsSync(path)) {
81123
- const sql = readFileSync(path, "utf8");
81124
- const match = sql.match(SCHEMA_VERSION_RE3);
81125
- bundled = match ? match[1] : null;
81126
- break;
81127
- }
81128
- }
81129
- } catch {
81130
- bundled = null;
81225
+ app.get("/api/v1/openapi.json", (c) => {
81226
+ const doc = readOpenApiDocument();
81227
+ if (doc === null) {
81228
+ return c.json({ detail: "The OpenAPI document is not bundled with this build." }, 404);
81131
81229
  }
81230
+ return c.body(doc, 200, { "Content-Type": "application/json; charset=utf-8" });
81231
+ });
81232
+ app.get("/api/v1/schema-version", async (c) => {
81233
+ const bundled = bundledSchemaVersion();
81132
81234
  let deployed = null;
81133
81235
  if (ctx) {
81134
81236
  try {
@@ -81157,8 +81259,8 @@ function registerMetaRoutes(app, ctx) {
81157
81259
  }
81158
81260
  } catch {}
81159
81261
  }
81160
- const mismatch = Boolean(bundled && deployed && bundled !== deployed);
81161
81262
  const level = classifyCompat(deployed, COMPATIBILITY.minSchema, bundled);
81263
+ const mismatch = level === "above-min-but-old";
81162
81264
  return c.json({
81163
81265
  bundled,
81164
81266
  deployed,
@@ -81216,7 +81318,7 @@ function registerPostgrestProxy(app) {
81216
81318
 
81217
81319
  // src/web/routes/preferences.ts
81218
81320
  init_config();
81219
- import { existsSync as existsSync19, mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync7 } from "node:fs";
81321
+ import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync7 } from "node:fs";
81220
81322
  import { join as join16 } from "node:path";
81221
81323
  function isTheme(v) {
81222
81324
  return v === "auto" || v === "light" || v === "dark";
@@ -81242,7 +81344,7 @@ function registerPreferencesRoutes(app) {
81242
81344
  const next = { ...readPrefs(), theme: body.theme };
81243
81345
  try {
81244
81346
  const dir = userStateDir();
81245
- if (!existsSync19(dir))
81347
+ if (!existsSync18(dir))
81246
81348
  mkdirSync6(dir, { recursive: true });
81247
81349
  writeFileSync7(prefsFile(), `${JSON.stringify(next, null, 2)}
81248
81350
  `);
@@ -81379,14 +81481,14 @@ function registerProjectsRoutes(app, ctx) {
81379
81481
  }
81380
81482
 
81381
81483
  // src/web/static.ts
81382
- import { existsSync as existsSync20, statSync as statSync7 } from "node:fs";
81383
- import { dirname as dirname7, join as join17 } from "node:path";
81384
- import { fileURLToPath as fileURLToPath4 } from "node:url";
81484
+ import { existsSync as existsSync19, statSync as statSync7 } from "node:fs";
81485
+ import { dirname as dirname8, join as join17 } from "node:path";
81486
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
81385
81487
  function moduleDir3() {
81386
- return dirname7(fileURLToPath4(import.meta.url));
81488
+ return dirname8(fileURLToPath5(import.meta.url));
81387
81489
  }
81388
81490
  function isUsableSpaDir(dir) {
81389
- return existsSync20(dir) && statSync7(dir).isDirectory() && existsSync20(join17(dir, "index.html"));
81491
+ return existsSync19(dir) && statSync7(dir).isDirectory() && existsSync19(join17(dir, "index.html"));
81390
81492
  }
81391
81493
  function resolveSpaDist() {
81392
81494
  const here = moduleDir3();
@@ -81408,7 +81510,7 @@ function resolveStaticDir() {
81408
81510
  join17(here, "..", "..", "..", "..", "web", "static")
81409
81511
  ];
81410
81512
  for (const c of candidates) {
81411
- if (existsSync20(c) && statSync7(c).isDirectory())
81513
+ if (existsSync19(c) && statSync7(c).isDirectory())
81412
81514
  return c;
81413
81515
  }
81414
81516
  return null;
@@ -81455,11 +81557,16 @@ init_ef_meta();
81455
81557
  init_cli_core();
81456
81558
  init_config();
81457
81559
  init_compatibility();
81560
+ var OPENAPI_PATH = "/api/v1/openapi.json";
81458
81561
  function buildApp(ctx = buildWebContext()) {
81459
81562
  const app = new Hono2;
81460
81563
  if (true) {
81461
81564
  app.use(logger((message, ...rest) => console.log(`${localTimestamp()} ${message}`, ...rest)));
81462
81565
  }
81566
+ app.use("/api/v1/*", async (c, next) => {
81567
+ await next();
81568
+ c.res.headers.set("Link", `<${OPENAPI_PATH}>; rel="service-desc"`);
81569
+ });
81463
81570
  app.use("/api/v1/*", apiAuth());
81464
81571
  app.use("/rest/v1/*", apiAuth());
81465
81572
  registerMetaRoutes(app, ctx);
@@ -81500,7 +81607,7 @@ function buildApp(ctx = buildWebContext()) {
81500
81607
  }));
81501
81608
  app.get("/app/assets/*", (c) => c.text("Not found", 404));
81502
81609
  const indexPath = join18(spaDist, "index.html");
81503
- if (existsSync21(indexPath)) {
81610
+ if (existsSync20(indexPath)) {
81504
81611
  let cached = null;
81505
81612
  const readIndex = () => {
81506
81613
  try {