@cerefox/memory 1.14.1 → 1.14.2

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.
@@ -7400,7 +7400,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
7400
7400
  });
7401
7401
 
7402
7402
  // src/meta.ts
7403
- var PKG_VERSION = "1.14.1";
7403
+ var PKG_VERSION = "1.14.2";
7404
7404
  var init_meta = () => {};
7405
7405
 
7406
7406
  // ../../_shared/config/paths.ts
@@ -23282,16 +23282,18 @@ function applyByteBudget(rows, maxBytes) {
23282
23282
  const accepted = [];
23283
23283
  let usedBytes = 0;
23284
23284
  let truncated = false;
23285
- for (const row of rows) {
23285
+ let cut = rows.length;
23286
+ for (const [i, row] of rows.entries()) {
23286
23287
  const rowBytes = new TextEncoder().encode(JSON.stringify(row)).length;
23287
23288
  if (usedBytes + rowBytes > maxBytes) {
23288
23289
  truncated = true;
23290
+ cut = i;
23289
23291
  break;
23290
23292
  }
23291
23293
  accepted.push(row);
23292
23294
  usedBytes += rowBytes;
23293
23295
  }
23294
- return { accepted, truncated, usedBytes };
23296
+ return { accepted, dropped: rows.slice(cut), truncated, usedBytes };
23295
23297
  }
23296
23298
  function extractConflictHashes(message) {
23297
23299
  return {
@@ -25774,7 +25776,7 @@ var init_bundled_docs = __esm(() => {
25774
25776
  });
25775
25777
 
25776
25778
  // ../../_shared/ef-meta/index.ts
25777
- var EF_VERSION = "1.14.1", CEREFOX_VERSION = "1.14.1", EF_LAST_CHANGED = "1.14.0";
25779
+ var EF_VERSION = "1.14.2", CEREFOX_VERSION = "1.14.2", EF_LAST_CHANGED = "1.14.2";
25778
25780
  var init_ef_meta = () => {};
25779
25781
 
25780
25782
  // ../../_shared/compatibility/index.ts
@@ -25923,9 +25925,9 @@ __export(exports_onnx_embedder, {
25923
25925
  onnxEmbed: () => onnxEmbed,
25924
25926
  warmup: () => warmup
25925
25927
  });
25926
- import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
25927
- import { homedir as homedir5 } from "node:os";
25928
- import { join as join8 } from "node:path";
25928
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
25929
+ import { homedir as homedir6 } from "node:os";
25930
+ import { join as join9 } from "node:path";
25929
25931
  function nomicPrefix(role) {
25930
25932
  return role === "query" ? "search_query: " : "search_document: ";
25931
25933
  }
@@ -25937,7 +25939,7 @@ function getCacheDir() {
25937
25939
  const env2 = globalThis.process?.env ?? {};
25938
25940
  if (env2.CEREFOX_MODELS_DIR)
25939
25941
  return env2.CEREFOX_MODELS_DIR;
25940
- return join8(homedir5(), ".cerefox", "models");
25942
+ return join9(homedir6(), ".cerefox", "models");
25941
25943
  }
25942
25944
  async function loadTransformers() {
25943
25945
  if (transformersModule)
@@ -25945,8 +25947,8 @@ async function loadTransformers() {
25945
25947
  const spec = "@huggingface/transformers";
25946
25948
  transformersModule = await import(spec);
25947
25949
  const dir = getCacheDir();
25948
- if (!existsSync9(dir))
25949
- mkdirSync3(dir, { recursive: true });
25950
+ if (!existsSync10(dir))
25951
+ mkdirSync4(dir, { recursive: true });
25950
25952
  transformersModule.env.cacheDir = dir;
25951
25953
  transformersModule.env.allowLocalModels = true;
25952
25954
  transformersModule.env.allowRemoteModels = true;
@@ -55761,12 +55763,12 @@ var init_partial_edits2 = __esm(() => {
55761
55763
  });
55762
55764
 
55763
55765
  // ../../_shared/mcp-tools/get-help-content.ts
55764
- 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 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_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. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); 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.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. **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.\n9. **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.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch("topic") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title="Same Title", content="...", document_id="abc123",\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch("topic") -> find doc (note its hash) -> modify ->\ningest(title="Same Title", content="...", update_if_exists=true,\n expected_content_hash="<the hash you read>", author="my-agent")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={"type": "decision-log"}, updated_since="2026-03-28T00:00:00Z")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`). 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;
55766
+ 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 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_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 structured Markdown** with H1/H2/H3 headings for good chunking and search.\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;
55765
55767
  var init_get_help_content = __esm(() => {
55766
55768
  HELP_SECTIONS = {
55767
55769
  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 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_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.",
55768
55770
  "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.',
55769
- "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. **Add metadata** -- at minimum `type` ("decision-log", "research", "design-doc") and `status` ("active", "draft").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); 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.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don\'t write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don\'t construct manually. **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.\n9. **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.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates\' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Relations express how documents relate; lifecycle tells you if knowledge is still good.** Use `cerefox_set_relation` when one document supersedes, contradicts, references, or continues another. `supersedes` marks the target **superseded**; `contradicts` marks **both** stale; `related_to`/`duplicates`/`contradicts` are symmetric (both directions written). Any other type string is accepted without special behaviour. When a search result or `cerefox_get_relations` shows a neighbour marked `[superseded]` or `[stale]`, say so rather than presenting it as current.\n12. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc\'s full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean "add" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.',
55771
+ "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 structured Markdown** with H1/H2/H3 headings for good chunking and search.\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`.',
55770
55772
  "Update Workflow (ID-based -- preferred)": `## Update Workflow (ID-based -- preferred)
55771
55773
 
55772
55774
  \`\`\`
@@ -56449,8 +56451,23 @@ async function handler10(supabase, args, ctx) {
56449
56451
  project_id: projectId,
56450
56452
  result_count: rows.length
56451
56453
  });
56452
- if (rows.length === 0)
56454
+ if (rows.length === 0) {
56455
+ if (include_content && max_bytes !== null) {
56456
+ const { data: headers } = await supabase.rpc("cerefox_metadata_search", {
56457
+ ...params,
56458
+ p_include_content: false,
56459
+ p_max_bytes: null
56460
+ });
56461
+ const headerRows = headers ?? [];
56462
+ if (headerRows.length > 0) {
56463
+ return `⚠ ${headerRows.length} document(s) match, but none fit max_bytes=${max_bytes} ` + `with include_content. This is NOT an empty result. Listing them without ` + `content — raise max_bytes, or read one with cerefox_get_document ` + `(outline: true for structure).
56464
+
56465
+ ` + headerRows.map((r) => `## ${r.title} [id: ${r.document_id}]`).join(`
56466
+ `);
56467
+ }
56468
+ }
56453
56469
  return "No documents match the given criteria.";
56470
+ }
56454
56471
  const showReview = await reviewWorkflowEnabled(supabase);
56455
56472
  const parts = rows.map((row) => {
56456
56473
  const projects = row.project_names?.length ? ` | projects: ${row.project_names.join(", ")}` : "";
@@ -56522,6 +56539,35 @@ var init_metadata_search = __esm(() => {
56522
56539
  });
56523
56540
 
56524
56541
  // ../../_shared/mcp-tools/search.ts
56542
+ function headerLine(row) {
56543
+ const title = row.doc_title ?? "Untitled";
56544
+ const docId = row.document_id ? ` [id: ${row.document_id}]` : "";
56545
+ const raw = row.best_score ?? row.score;
56546
+ const score = raw != null ? ` (score: ${raw.toFixed(3)})` : "";
56547
+ const size = row.total_chars != null ? ` -- ${row.total_chars.toLocaleString()} chars` : "";
56548
+ const hash = row.content_hash ? `
56549
+ hash: ${row.content_hash}` : "";
56550
+ return `## ${title}${docId}${score}${size}${hash}`;
56551
+ }
56552
+ function degradedToHeaders(matched, maxBytes) {
56553
+ const biggest = Math.max(...matched.map((r) => new TextEncoder().encode(JSON.stringify(r)).length));
56554
+ const lead = `⚠ ${matched.length} result(s) matched, but none fit max_bytes=${maxBytes} ` + `(the largest is ${biggest.toLocaleString()} bytes). This is NOT an empty ` + `knowledge base. Listing what matched, without content — raise max_bytes to ` + `read it, or read one document with cerefox_get_document (outline: true for ` + `structure, or section: "## Heading" for one part).`;
56555
+ const lines = [];
56556
+ let used = new TextEncoder().encode(lead).length;
56557
+ for (const row of matched) {
56558
+ const line = headerLine(row);
56559
+ const size = new TextEncoder().encode(line).length + 2;
56560
+ if (used + size > maxBytes)
56561
+ break;
56562
+ lines.push(line);
56563
+ used += size;
56564
+ }
56565
+ return lines.length > 0 ? `${lead}
56566
+
56567
+ ${lines.join(`
56568
+
56569
+ `)}` : lead;
56570
+ }
56525
56571
  async function handler11(supabase, args, ctx) {
56526
56572
  const query = args.query;
56527
56573
  const project_name = args.project_name;
@@ -56596,17 +56642,22 @@ async function handler11(supabase, args, ctx) {
56596
56642
  const { data, error } = await supabase.rpc(rpcName, rpcParams);
56597
56643
  if (error)
56598
56644
  throw new Error(`RPC error: ${error.message}`);
56599
- const { accepted, truncated, usedBytes } = applyByteBudget(data ?? [], max_bytes);
56645
+ const matched = data ?? [];
56646
+ const { accepted, dropped, truncated, usedBytes } = applyByteBudget(matched, max_bytes);
56600
56647
  logUsage(supabase, {
56601
56648
  operation: "search",
56602
56649
  accessPath: ctx.accessPath,
56603
56650
  requestor: callerIdentity(args),
56604
56651
  query_text: query,
56605
56652
  project_id: projectId,
56606
- result_count: accepted.length
56653
+ result_count: matched.length,
56654
+ ...truncated ? { extra: { returned: accepted.length, truncated: true } } : {}
56607
56655
  });
56608
- if (accepted.length === 0)
56656
+ if (matched.length === 0)
56609
56657
  return "No results found.";
56658
+ if (accepted.length === 0) {
56659
+ return degradedToHeaders(matched, max_bytes);
56660
+ }
56610
56661
  const rows = accepted;
56611
56662
  const belowConfidence = rows.length > 0 && rows.every((r) => r.below_confidence === true);
56612
56663
  const parts = rows.map((row) => {
@@ -56634,7 +56685,7 @@ ${row.full_content ?? ""}`;
56634
56685
  if (truncated) {
56635
56686
  output += `
56636
56687
 
56637
- [Results truncated at ${usedBytes} bytes. Use a more specific query or a smaller match_count to see more.]`;
56688
+ [${accepted.length} of ${matched.length} result(s) shown; truncated at ` + `${usedBytes} bytes. ${dropped.length} did not fit: ` + `${dropped.map((r) => r.doc_title ?? "Untitled").join(", ")}. ` + `Raise max_bytes, narrow the query, or lower match_count.]`;
56638
56689
  }
56639
56690
  return output;
56640
56691
  }
@@ -57660,7 +57711,7 @@ var require_cli_progress = __commonJS(function(exports2, module2) {
57660
57711
  });
57661
57712
 
57662
57713
  // src/cli/commands/sync-self-docs.ts
57663
- import { readFileSync as readFileSync11 } from "node:fs";
57714
+ import { readFileSync as readFileSync12 } from "node:fs";
57664
57715
  import { basename as basename4, extname as extname5 } from "node:path";
57665
57716
  async function runSyncSelfDocs(options = {}) {
57666
57717
  const project = options.project ?? "_cerefox-self-docs";
@@ -57687,7 +57738,7 @@ async function runSyncSelfDocs(options = {}) {
57687
57738
  const authorType = resolveAuthorType("agent");
57688
57739
  const outcomes = [];
57689
57740
  for (const doc of docs) {
57690
- const content = readFileSync11(doc.path, "utf8");
57741
+ const content = readFileSync12(doc.path, "utf8");
57691
57742
  const m = content.match(/^#\s+(.+)$/m);
57692
57743
  const title = m ? m[1].trim() : basename4(doc.path, extname5(doc.path));
57693
57744
  try {
@@ -70871,7 +70922,7 @@ var init_stdio2 = __esm(() => {
70871
70922
  });
70872
70923
 
70873
70924
  // src/server.ts
70874
- import { existsSync as existsSync12, readFileSync as readFileSync13 } from "node:fs";
70925
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
70875
70926
  function buildServer() {
70876
70927
  const settings = loadSettings();
70877
70928
  if (!settings.supabaseUrl || !settings.supabaseKey) {
@@ -70925,8 +70976,8 @@ async function warnIfSchemaVersionMismatch(supabase) {
70925
70976
  let bundled = null;
70926
70977
  try {
70927
70978
  const assets = resolveServerAssets();
70928
- if (existsSync12(assets.schemaFile)) {
70929
- const m = readFileSync13(assets.schemaFile, "utf8").match(SCHEMA_VERSION_RE2);
70979
+ if (existsSync13(assets.schemaFile)) {
70980
+ const m = readFileSync14(assets.schemaFile, "utf8").match(SCHEMA_VERSION_RE2);
70930
70981
  bundled = m ? m[1] : null;
70931
70982
  }
70932
70983
  } catch {}
@@ -77474,9 +77525,177 @@ init_config();
77474
77525
  init_config();
77475
77526
  init_compatibility();
77476
77527
  init_server_assets();
77477
- import { existsSync as existsSync10, readFileSync as readFileSync7, realpathSync, statSync as statSync2 } from "node:fs";
77478
- import { homedir as homedir6 } from "node:os";
77479
- import { join as join9 } from "node:path";
77528
+ import { existsSync as existsSync11, readFileSync as readFileSync8, realpathSync, statSync as statSync2 } from "node:fs";
77529
+ import { homedir as homedir7 } from "node:os";
77530
+ import { join as join10 } from "node:path";
77531
+
77532
+ // src/web/daemon.ts
77533
+ import { spawn } from "node:child_process";
77534
+ import {
77535
+ existsSync as existsSync9,
77536
+ mkdirSync as mkdirSync3,
77537
+ openSync,
77538
+ readFileSync as readFileSync7,
77539
+ rmSync,
77540
+ writeFileSync as writeFileSync4
77541
+ } from "node:fs";
77542
+ import { homedir as homedir5 } from "node:os";
77543
+ import { join as join8 } from "node:path";
77544
+ function resolveStateDir(override = process.env.CEREFOX_CONFIG_DIR, home = homedir5()) {
77545
+ override = (override ?? "").trim();
77546
+ if (!override)
77547
+ return join8(home, ".cerefox");
77548
+ return override === "~" || override.startsWith("~/") ? join8(home, override.slice(2)) : override;
77549
+ }
77550
+ var STATE_DIR = resolveStateDir();
77551
+ var PID_FILE = join8(STATE_DIR, "web.pid");
77552
+ var LOG_FILE = join8(STATE_DIR, "web.log");
77553
+ var daemonPaths = { stateDir: STATE_DIR, pidFile: PID_FILE, logFile: LOG_FILE };
77554
+ function ensureStateDir() {
77555
+ if (!existsSync9(STATE_DIR))
77556
+ mkdirSync3(STATE_DIR, { recursive: true });
77557
+ }
77558
+ function readPidFile() {
77559
+ if (!existsSync9(PID_FILE))
77560
+ return null;
77561
+ try {
77562
+ const parsed = JSON.parse(readFileSync7(PID_FILE, "utf8"));
77563
+ if (typeof parsed.pid !== "number")
77564
+ return null;
77565
+ return {
77566
+ pid: parsed.pid,
77567
+ port: typeof parsed.port === "number" ? parsed.port : 8000,
77568
+ host: typeof parsed.host === "string" ? parsed.host : "127.0.0.1",
77569
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : "unknown"
77570
+ };
77571
+ } catch {
77572
+ return null;
77573
+ }
77574
+ }
77575
+ function writePidFile(info) {
77576
+ ensureStateDir();
77577
+ writeFileSync4(PID_FILE, JSON.stringify(info, null, 2) + `
77578
+ `, "utf8");
77579
+ }
77580
+ function removePidFile() {
77581
+ rmSync(PID_FILE, { force: true });
77582
+ }
77583
+ function isProcessAlive(pid) {
77584
+ try {
77585
+ process.kill(pid, 0);
77586
+ return true;
77587
+ } catch (err) {
77588
+ return err.code === "EPERM";
77589
+ }
77590
+ }
77591
+ async function probeVersion(host, port, timeoutMs = 1500) {
77592
+ const probeHost = host === "0.0.0.0" ? "127.0.0.1" : host;
77593
+ try {
77594
+ const ctrl = new AbortController;
77595
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
77596
+ try {
77597
+ const resp = await fetch(`http://${probeHost}:${port}/api/v1/version`, {
77598
+ signal: ctrl.signal
77599
+ });
77600
+ if (!resp.ok)
77601
+ return { responding: false, version: null };
77602
+ const body = await resp.json().catch(() => null);
77603
+ return {
77604
+ responding: true,
77605
+ version: typeof body?.version === "string" ? body.version : null
77606
+ };
77607
+ } finally {
77608
+ clearTimeout(timer);
77609
+ }
77610
+ } catch {
77611
+ return { responding: false, version: null };
77612
+ }
77613
+ }
77614
+ async function isResponding(host, port, timeoutMs = 1500) {
77615
+ return (await probeVersion(host, port, timeoutMs)).responding;
77616
+ }
77617
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
77618
+ function assertUnix() {
77619
+ if (process.platform === "win32") {
77620
+ throw new Error("Daemon mode (`cerefox web start/stop/status`) is not supported on Windows yet. " + "Run `cerefox web` in the foreground, or set up a Windows service manually.");
77621
+ }
77622
+ }
77623
+ async function startDaemon(opts) {
77624
+ assertUnix();
77625
+ ensureStateDir();
77626
+ const existing = readPidFile();
77627
+ if (existing && isProcessAlive(existing.pid)) {
77628
+ return existing.port === opts.port ? { kind: "already-running", info: existing } : { kind: "port-conflict", info: existing };
77629
+ }
77630
+ if (existing)
77631
+ removePidFile();
77632
+ const logFd = openSync(LOG_FILE, "a");
77633
+ const child = spawn(opts.runtime, [opts.scriptPath, "web", "--host", opts.host, "--port", String(opts.port)], { detached: true, stdio: ["ignore", logFd, logFd] });
77634
+ child.unref();
77635
+ if (typeof child.pid !== "number") {
77636
+ throw new Error("Failed to spawn the web server process (no pid).");
77637
+ }
77638
+ writePidFile({
77639
+ pid: child.pid,
77640
+ port: opts.port,
77641
+ host: opts.host,
77642
+ startedAt: new Date().toISOString()
77643
+ });
77644
+ let responding = false;
77645
+ for (let i = 0;i < 20; i++) {
77646
+ if (!isProcessAlive(child.pid))
77647
+ break;
77648
+ if (await isResponding(opts.host, opts.port)) {
77649
+ responding = true;
77650
+ break;
77651
+ }
77652
+ await sleep2(250);
77653
+ }
77654
+ return { kind: "started", pid: child.pid, responding };
77655
+ }
77656
+ async function stopDaemon() {
77657
+ assertUnix();
77658
+ const info = readPidFile();
77659
+ if (!info || !isProcessAlive(info.pid)) {
77660
+ removePidFile();
77661
+ return { kind: "not-running" };
77662
+ }
77663
+ try {
77664
+ process.kill(info.pid, "SIGTERM");
77665
+ } catch {}
77666
+ let forced = false;
77667
+ let alive = true;
77668
+ for (let i = 0;i < 12; i++) {
77669
+ await sleep2(250);
77670
+ if (!isProcessAlive(info.pid)) {
77671
+ alive = false;
77672
+ break;
77673
+ }
77674
+ }
77675
+ if (alive) {
77676
+ try {
77677
+ process.kill(info.pid, "SIGKILL");
77678
+ forced = true;
77679
+ } catch {}
77680
+ }
77681
+ removePidFile();
77682
+ return { kind: "stopped", pid: info.pid, forced };
77683
+ }
77684
+ function restartCommand(host, port) {
77685
+ const flags = `${host === "127.0.0.1" ? "" : ` --host ${host}`}${port === 8000 ? "" : ` --port ${port}`}`;
77686
+ return `cerefox web stop && cerefox web start${flags}`;
77687
+ }
77688
+ async function statusDaemon() {
77689
+ const info = readPidFile();
77690
+ if (!info)
77691
+ return { kind: "stopped" };
77692
+ if (!isProcessAlive(info.pid))
77693
+ return { kind: "stale", info };
77694
+ const { responding, version } = await probeVersion(info.host, info.port);
77695
+ return { kind: "running", info, responding, version };
77696
+ }
77697
+
77698
+ // src/cli/util/checks.ts
77480
77699
  function checkBinary() {
77481
77700
  return {
77482
77701
  name: "binary",
@@ -77527,7 +77746,7 @@ function checkConfig() {
77527
77746
  hint: "Run `cerefox init` to bootstrap."
77528
77747
  };
77529
77748
  }
77530
- if (!existsSync10(envPath)) {
77749
+ if (!existsSync11(envPath)) {
77531
77750
  const settings = loadSettings();
77532
77751
  if (settings.supabaseUrl && settings.supabaseKey) {
77533
77752
  return {
@@ -77739,9 +77958,9 @@ var SCHEMA_VERSION_RE = /^--\s*@version:\s*(\S+)/m;
77739
77958
  function readBundledSchemaVersion() {
77740
77959
  try {
77741
77960
  const assets = resolveServerAssets();
77742
- if (!existsSync10(assets.schemaFile))
77961
+ if (!existsSync11(assets.schemaFile))
77743
77962
  return null;
77744
- const m = readFileSync7(assets.schemaFile, "utf8").match(SCHEMA_VERSION_RE);
77963
+ const m = readFileSync8(assets.schemaFile, "utf8").match(SCHEMA_VERSION_RE);
77745
77964
  return m ? m[1] : null;
77746
77965
  } catch {
77747
77966
  return null;
@@ -77984,21 +78203,69 @@ async function checkMetadataHealth() {
77984
78203
  };
77985
78204
  }
77986
78205
  function hasCerefoxInJsonFile(path) {
77987
- if (!existsSync10(path))
78206
+ if (!existsSync11(path))
77988
78207
  return false;
77989
78208
  try {
77990
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
78209
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
77991
78210
  const mcpServers = parsed.mcpServers;
77992
78211
  return Boolean(mcpServers && typeof mcpServers === "object" && "cerefox" in mcpServers);
77993
78212
  } catch {
77994
78213
  return false;
77995
78214
  }
77996
78215
  }
78216
+ async function checkWebDaemon() {
78217
+ let status;
78218
+ try {
78219
+ status = await statusDaemon();
78220
+ } catch {
78221
+ return { name: "web server", status: "skipped", detail: "could not read the daemon pidfile" };
78222
+ }
78223
+ if (status.kind === "stopped") {
78224
+ return { name: "web server", status: "skipped", detail: "no background daemon running" };
78225
+ }
78226
+ if (status.kind === "stale") {
78227
+ return {
78228
+ name: "web server",
78229
+ status: "skipped",
78230
+ detail: `no daemon running (stale pidfile for process ${status.info.pid}).`,
78231
+ hint: "Clean it up: cerefox web stop"
78232
+ };
78233
+ }
78234
+ if (!status.responding) {
78235
+ return {
78236
+ name: "web server",
78237
+ status: "warn",
78238
+ detail: `process ${status.info.pid} is alive but not answering on :${status.info.port}.`,
78239
+ hint: `Check the log, then: ${restartCommand(status.info.host, status.info.port)}`
78240
+ };
78241
+ }
78242
+ if (status.version && status.version !== PKG_VERSION) {
78243
+ return {
78244
+ name: "web server",
78245
+ status: "warn",
78246
+ detail: `running v${status.version} on :${status.info.port}, but this client is v${PKG_VERSION} ` + `— it is still serving the previous build.`,
78247
+ hint: `Restart it: ${restartCommand(status.info.host, status.info.port)}`
78248
+ };
78249
+ }
78250
+ if (!status.version) {
78251
+ return {
78252
+ name: "web server",
78253
+ status: "warn",
78254
+ detail: `responding on :${status.info.port} (pid ${status.info.pid}) but did not report a version.`,
78255
+ hint: `Confirm it is Cerefox: curl http://${status.info.host}:${status.info.port}/api/v1/version`
78256
+ };
78257
+ }
78258
+ return {
78259
+ name: "web server",
78260
+ status: "ok",
78261
+ detail: `v${status.version} on :${status.info.port} (pid ${status.info.pid})`
78262
+ };
78263
+ }
77997
78264
  function checkMcpConfigs() {
77998
- const home = homedir6();
77999
- const claudeCodeUser = join9(home, ".claude.json");
78000
- const claudeCodeProj = join9(process.cwd(), ".mcp.json");
78001
- const claudeDesktop = process.platform === "darwin" ? join9(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : process.platform === "win32" ? join9(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json") : join9(home, ".config", "Claude", "claude_desktop_config.json");
78265
+ const home = homedir7();
78266
+ const claudeCodeUser = join10(home, ".claude.json");
78267
+ const claudeCodeProj = join10(process.cwd(), ".mcp.json");
78268
+ const claudeDesktop = process.platform === "darwin" ? join10(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : process.platform === "win32" ? join10(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json") : join10(home, ".config", "Claude", "claude_desktop_config.json");
78002
78269
  const found = [];
78003
78270
  if (hasCerefoxInJsonFile(claudeCodeUser))
78004
78271
  found.push("Claude Code (user)");
@@ -78022,8 +78289,8 @@ function checkMcpConfigs() {
78022
78289
  }
78023
78290
  function checkLegacyShadowEnv(opts = {}) {
78024
78291
  const activeEnv = resolveEnvFile(opts);
78025
- const cwdEnv = join9(opts.cwd ?? process.cwd(), ".env");
78026
- if (!existsSync10(activeEnv) || !existsSync10(cwdEnv))
78292
+ const cwdEnv = join10(opts.cwd ?? process.cwd(), ".env");
78293
+ if (!existsSync11(activeEnv) || !existsSync11(cwdEnv))
78027
78294
  return null;
78028
78295
  try {
78029
78296
  if (realpathSync(activeEnv) === realpathSync(cwdEnv))
@@ -78197,6 +78464,7 @@ async function runAllChecks(opts = {}) {
78197
78464
  { name: "metadata health", phase: "Checking metadata well-formedness", run: () => checkMetadataHealth() },
78198
78465
  { name: "edge functions", phase: "Probing Edge Function versions", run: () => checkEdgeFunctionsCompat() },
78199
78466
  { name: "postgres", phase: "Probing Postgres DDL endpoint", run: () => checkPostgres() },
78467
+ { name: "web server", phase: "Probing the web daemon", run: () => checkWebDaemon() },
78200
78468
  { name: "mcp clients", phase: "Scanning MCP client configs", run: () => checkMcpConfigs() }
78201
78469
  ];
78202
78470
  return runSteps(steps, opts);
@@ -78592,11 +78860,11 @@ function registerGetDoc(program) {
78592
78860
  init_dist4();
78593
78861
  init_cli_core();
78594
78862
  init_config();
78595
- import { readFileSync as readFileSync9 } from "node:fs";
78863
+ import { readFileSync as readFileSync10 } from "node:fs";
78596
78864
  import { basename as basename2, extname as extname3 } from "node:path";
78597
78865
 
78598
78866
  // src/ingestion/pipeline.ts
78599
- import { readFileSync as readFileSync8 } from "node:fs";
78867
+ import { readFileSync as readFileSync9 } from "node:fs";
78600
78868
  import { basename, extname as extname2, resolve as resolve3 } from "node:path";
78601
78869
  // ../../_shared/ingest/pipeline-helpers.ts
78602
78870
  import { createHash } from "node:crypto";
@@ -79259,7 +79527,7 @@ class IngestionPipeline {
79259
79527
  };
79260
79528
  }
79261
79529
  async ingestFile(path, opts = {}) {
79262
- const text = await fileToMarkdown(path, readFileSync8(path));
79530
+ const text = await fileToMarkdown(path, readFileSync9(path));
79263
79531
  const absPath = resolve3(path);
79264
79532
  const stem = basename(absPath, extname2(absPath));
79265
79533
  return this.ingestText({
@@ -79294,7 +79562,7 @@ async function readContent(path, paste) {
79294
79562
  }
79295
79563
  let content;
79296
79564
  try {
79297
- content = readFileSync9(path, "utf8");
79565
+ content = readFileSync10(path, "utf8");
79298
79566
  } catch (err) {
79299
79567
  const msg = err instanceof Error ? err.message : String(err);
79300
79568
  throw userError(`Cannot read ${path}: ${msg}`);
@@ -79404,14 +79672,14 @@ init_cli_core();
79404
79672
  init_mcp_tools();
79405
79673
  init_config();
79406
79674
  init_client();
79407
- import { readFileSync as readFileSync10 } from "node:fs";
79675
+ import { readFileSync as readFileSync11 } from "node:fs";
79408
79676
  function resolveText(value, what) {
79409
79677
  if (value === undefined)
79410
79678
  throw userError(`${what} is required.`);
79411
79679
  if (value === "-")
79412
- return readFileSync10(0, "utf8");
79680
+ return readFileSync11(0, "utf8");
79413
79681
  if (value.startsWith("@"))
79414
- return readFileSync10(value.slice(1), "utf8");
79682
+ return readFileSync11(value.slice(1), "utf8");
79415
79683
  return value;
79416
79684
  }
79417
79685
  function context() {
@@ -79472,7 +79740,7 @@ init_cli_core();
79472
79740
  init_config();
79473
79741
  var import_cli_progress = __toESM(require_cli_progress(), 1);
79474
79742
  import { readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
79475
- import { basename as basename3, extname as extname4, join as join10 } from "node:path";
79743
+ import { basename as basename3, extname as extname4, join as join11 } from "node:path";
79476
79744
  function walk(dir, extensions) {
79477
79745
  let entries;
79478
79746
  try {
@@ -79482,7 +79750,7 @@ function walk(dir, extensions) {
79482
79750
  }
79483
79751
  const files = [];
79484
79752
  for (const name of entries) {
79485
- const full = join10(dir, name);
79753
+ const full = join11(dir, name);
79486
79754
  let stat;
79487
79755
  try {
79488
79756
  stat = statSync3(full);
@@ -79585,20 +79853,20 @@ import { spawnSync as spawnSync4 } from "node:child_process";
79585
79853
  import {
79586
79854
  chmodSync,
79587
79855
  copyFileSync as copyFileSync2,
79588
- existsSync as existsSync11,
79589
- mkdirSync as mkdirSync4,
79590
- readFileSync as readFileSync12,
79591
- writeFileSync as writeFileSync4
79856
+ existsSync as existsSync12,
79857
+ mkdirSync as mkdirSync5,
79858
+ readFileSync as readFileSync13,
79859
+ writeFileSync as writeFileSync5
79592
79860
  } from "node:fs";
79593
- import { homedir as homedir7 } from "node:os";
79594
- import { dirname as dirname4, join as join11 } from "node:path";
79861
+ import { homedir as homedir8 } from "node:os";
79862
+ import { dirname as dirname4, join as join12 } from "node:path";
79595
79863
  async function readConfigFile(path) {
79596
- if (!existsSync11(path)) {
79864
+ if (!existsSync12(path)) {
79597
79865
  throw userError(`--config file not found: ${path}`);
79598
79866
  }
79599
79867
  let parsed;
79600
79868
  try {
79601
- parsed = JSON.parse(readFileSync12(path, "utf8"));
79869
+ parsed = JSON.parse(readFileSync13(path, "utf8"));
79602
79870
  } catch (err) {
79603
79871
  throw userError(`--config: invalid JSON in ${path}: ${err instanceof Error ? err.message : String(err)}`);
79604
79872
  }
@@ -79640,7 +79908,7 @@ function parseDotEnvFile(content) {
79640
79908
  return map;
79641
79909
  }
79642
79910
  function answersFromEnvFile(path) {
79643
- const parsed = parseDotEnvFile(readFileSync12(path, "utf8"));
79911
+ const parsed = parseDotEnvFile(readFileSync13(path, "utf8"));
79644
79912
  const required = ["CEREFOX_SUPABASE_URL", "CEREFOX_SUPABASE_KEY", "OPENAI_API_KEY"];
79645
79913
  for (const key of required) {
79646
79914
  if (!parsed[key] || parsed[key].trim() === "") {
@@ -79895,8 +80163,8 @@ async function postWriteLifecycle(envPath, options) {
79895
80163
  println(c.dim(` Config in effect: ${envPath}`));
79896
80164
  }
79897
80165
  function writeAnswersTo(target, answers) {
79898
- mkdirSync4(dirname4(target), { recursive: true });
79899
- writeFileSync4(target, buildEnvFile(answers), "utf8");
80166
+ mkdirSync5(dirname4(target), { recursive: true });
80167
+ writeFileSync5(target, buildEnvFile(answers), "utf8");
79900
80168
  if (process.platform !== "win32") {
79901
80169
  try {
79902
80170
  chmodSync(target, 384);
@@ -79906,12 +80174,12 @@ function writeAnswersTo(target, answers) {
79906
80174
  }
79907
80175
  }
79908
80176
  async function action23(options) {
79909
- const homeEnv = join11(homedir7(), USER_STATE_DIR_NAME, ".env");
79910
- const cwdEnv = join11(process.cwd(), ".env");
80177
+ const homeEnv = join12(homedir8(), USER_STATE_DIR_NAME, ".env");
80178
+ const cwdEnv = join12(process.cwd(), ".env");
79911
80179
  const explicitDir = (process.env.CEREFOX_CONFIG_DIR ?? "").trim();
79912
80180
  if (explicitDir) {
79913
80181
  const target = resolveEnvFile();
79914
- if (existsSync11(target) && !options.force) {
80182
+ if (existsSync12(target) && !options.force) {
79915
80183
  println(c.yellow(`⚠ Config already exists at ${target}.`));
79916
80184
  const ok = await confirm("Overwrite?", true);
79917
80185
  if (!ok) {
@@ -79933,7 +80201,7 @@ async function action23(options) {
79933
80201
  await postWriteLifecycle(target, options);
79934
80202
  return;
79935
80203
  }
79936
- if (existsSync11(homeEnv) && !options.force) {
80204
+ if (existsSync12(homeEnv) && !options.force) {
79937
80205
  println(c.yellow(`⚠ Config already exists at ${homeEnv}.`));
79938
80206
  const ok = await confirm("Overwrite?", true);
79939
80207
  if (!ok) {
@@ -79954,12 +80222,12 @@ async function action23(options) {
79954
80222
  await postWriteLifecycle(homeEnv, options);
79955
80223
  return;
79956
80224
  }
79957
- if (existsSync11(cwdEnv) && !options.force && !options.config) {
80225
+ if (existsSync12(cwdEnv) && !options.force && !options.config) {
79958
80226
  printMigrationMenu(cwdEnv, homeEnv);
79959
80227
  const ch = await promptMigrationChoice();
79960
80228
  println("");
79961
80229
  if (ch === "c") {
79962
- mkdirSync4(dirname4(homeEnv), { recursive: true });
80230
+ mkdirSync5(dirname4(homeEnv), { recursive: true });
79963
80231
  copyFileSync2(cwdEnv, homeEnv);
79964
80232
  if (process.platform !== "win32") {
79965
80233
  try {
@@ -80554,35 +80822,35 @@ function registerMigrateFormat(program) {
80554
80822
  // src/cli/commands/restore.ts
80555
80823
  init_cli_core();
80556
80824
  init_client();
80557
- import { existsSync as existsSync13, readFileSync as readFileSync14, readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
80558
- import { homedir as homedir8 } from "node:os";
80559
- import { join as join12, resolve as resolve4 } from "node:path";
80825
+ import { existsSync as existsSync14, readFileSync as readFileSync15, readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
80826
+ import { homedir as homedir9 } from "node:os";
80827
+ import { join as join13, resolve as resolve4 } from "node:path";
80560
80828
  function expandHome2(path) {
80561
80829
  if (path === "~")
80562
- return homedir8();
80830
+ return homedir9();
80563
80831
  if (path.startsWith("~/"))
80564
- return join12(homedir8(), path.slice(2));
80832
+ return join13(homedir9(), path.slice(2));
80565
80833
  return path;
80566
80834
  }
80567
80835
  function resolveBackupFile(target) {
80568
80836
  const path = resolve4(expandHome2(target));
80569
- if (!existsSync13(path)) {
80837
+ if (!existsSync14(path)) {
80570
80838
  throw userError(`Backup path not found: ${target}`);
80571
80839
  }
80572
80840
  const stat = statSync4(path);
80573
80841
  if (stat.isFile())
80574
80842
  return path;
80575
- const candidates = readdirSync5(path).filter((n) => n.endsWith(".json") && n.startsWith("cerefox-")).map((n) => ({ name: n, mtime: statSync4(join12(path, n)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
80843
+ const candidates = readdirSync5(path).filter((n) => n.endsWith(".json") && n.startsWith("cerefox-")).map((n) => ({ name: n, mtime: statSync4(join13(path, n)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
80576
80844
  if (candidates.length === 0) {
80577
80845
  throw userError(`No cerefox-*.json files in ${path}`);
80578
80846
  }
80579
- return join12(path, candidates[0].name);
80847
+ return join13(path, candidates[0].name);
80580
80848
  }
80581
80849
  async function action31(target, options) {
80582
80850
  const file = resolveBackupFile(target);
80583
80851
  let payload;
80584
80852
  try {
80585
- payload = JSON.parse(readFileSync14(file, "utf8"));
80853
+ payload = JSON.parse(readFileSync15(file, "utf8"));
80586
80854
  } catch (err) {
80587
80855
  throw userError(`Could not parse backup file ${file}: ${err instanceof Error ? err.message : String(err)}`);
80588
80856
  }
@@ -80993,6 +81261,12 @@ async function action33(options) {
80993
81261
  }
80994
81262
  println("");
80995
81263
  println(c.green(`✓ Upgraded to ${target}.`));
81264
+ const daemon = await statusDaemon().catch(() => null);
81265
+ if (daemon?.kind === "running" && daemon.version !== target) {
81266
+ println("");
81267
+ println(c.yellow(`⚠ The web server is still running the previous build ` + `(pid ${daemon.info.pid} on :${daemon.info.port}` + `${daemon.version ? `, v${daemon.version}` : ""}).`));
81268
+ println(" Restart it: " + c.bold(restartCommand(daemon.info.host, daemon.info.port)));
81269
+ }
80996
81270
  println("");
80997
81271
  println("Next steps:");
80998
81272
  println(" 1. " + c.bold("cerefox server deploy") + " apply this release's schema/RPC/EF updates");
@@ -81050,7 +81324,7 @@ init_config();
81050
81324
  import { randomBytes } from "node:crypto";
81051
81325
 
81052
81326
  // src/cli/util/env-file.ts
81053
- import { copyFileSync as copyFileSync3, existsSync as existsSync14, readFileSync as readFileSync15, writeFileSync as writeFileSync5 } from "node:fs";
81327
+ import { copyFileSync as copyFileSync3, existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "node:fs";
81054
81328
  import { dirname as dirname5 } from "node:path";
81055
81329
  import { spawnSync as spawnSync6 } from "node:child_process";
81056
81330
  function escapeRegExp(s) {
@@ -81060,12 +81334,12 @@ function upsertEnvVar(path, key, value, opts = {}) {
81060
81334
  const line = `${key}=${value}`;
81061
81335
  const header = opts.comment ? `# ${opts.comment}
81062
81336
  ` : "";
81063
- if (!existsSync14(path)) {
81064
- writeFileSync5(path, `${header}${line}
81337
+ if (!existsSync15(path)) {
81338
+ writeFileSync6(path, `${header}${line}
81065
81339
  `, { mode: 384 });
81066
81340
  return { path, action: "created" };
81067
81341
  }
81068
- const original = readFileSync15(path, "utf8");
81342
+ const original = readFileSync16(path, "utf8");
81069
81343
  let backupPath;
81070
81344
  if (!opts.noBackup) {
81071
81345
  backupPath = `${path}.pre-cerefox.bak`;
@@ -81086,13 +81360,13 @@ ${header}${line}
81086
81360
  `;
81087
81361
  action = "added";
81088
81362
  }
81089
- writeFileSync5(path, next);
81363
+ writeFileSync6(path, next);
81090
81364
  return { path, action, backupPath };
81091
81365
  }
81092
81366
  function readEnvVar(path, key) {
81093
- if (!existsSync14(path))
81367
+ if (!existsSync15(path))
81094
81368
  return null;
81095
- const m = readFileSync15(path, "utf8").match(new RegExp(`^\\s*${escapeRegExp(key)}=(.*)$`, "m"));
81369
+ const m = readFileSync16(path, "utf8").match(new RegExp(`^\\s*${escapeRegExp(key)}=(.*)$`, "m"));
81096
81370
  return m ? m[1].trim() : null;
81097
81371
  }
81098
81372
  function envGitignoreWarning(path) {
@@ -81113,7 +81387,7 @@ function envGitignoreWarning(path) {
81113
81387
  }
81114
81388
 
81115
81389
  // src/web/auth.ts
81116
- import { existsSync as existsSync15 } from "node:fs";
81390
+ import { existsSync as existsSync16 } from "node:fs";
81117
81391
 
81118
81392
  // ../../node_modules/.bun/@hono+node-server@2.0.12+19b9adbcbc3b652f/node_modules/@hono/node-server/dist/conninfo.mjs
81119
81393
  var getConnInfo = (c) => {
@@ -81178,7 +81452,7 @@ var LOOPBACK_ADDRESSES = new Set([
81178
81452
  ]);
81179
81453
  function isContainerised() {
81180
81454
  try {
81181
- return existsSync15("/.dockerenv");
81455
+ return existsSync16("/.dockerenv");
81182
81456
  } catch {
81183
81457
  return false;
81184
81458
  }
@@ -82902,8 +83176,8 @@ var _baseMimes = {
82902
83176
  var baseMimes = _baseMimes;
82903
83177
 
82904
83178
  // ../../node_modules/.bun/@hono+node-server@2.0.12+19b9adbcbc3b652f/node_modules/@hono/node-server/dist/serve-static.mjs
82905
- import { createReadStream, existsSync as existsSync16, statSync as statSync5 } from "node:fs";
82906
- import { join as join13 } from "node:path";
83179
+ import { createReadStream, existsSync as existsSync17, statSync as statSync5 } from "node:fs";
83180
+ import { join as join14 } from "node:path";
82907
83181
  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;
82908
83182
  var ENCODINGS = {
82909
83183
  br: ".br",
@@ -82976,7 +83250,7 @@ var tryDecodeURI = (str) => tryDecode(str, decodeURI);
82976
83250
  var serveStatic = (options = { root: "" }) => {
82977
83251
  const root = options.root || "";
82978
83252
  const optionPath = options.path;
82979
- if (root !== "" && !existsSync16(root))
83253
+ if (root !== "" && !existsSync17(root))
82980
83254
  console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`);
82981
83255
  return async (c, next) => {
82982
83256
  if (c.finalized)
@@ -82993,11 +83267,11 @@ var serveStatic = (options = { root: "" }) => {
82993
83267
  await options.onNotFound?.(c.req.path, c);
82994
83268
  return next();
82995
83269
  }
82996
- let path = join13(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename);
83270
+ let path = join14(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename);
82997
83271
  let stats = getStats(path);
82998
83272
  if (stats && stats.isDirectory()) {
82999
83273
  const indexFile = options.index ?? "index.html";
83000
- path = join13(path, indexFile);
83274
+ path = join14(path, indexFile);
83001
83275
  stats = getStats(path);
83002
83276
  }
83003
83277
  if (!stats) {
@@ -83059,9 +83333,9 @@ var serveStatic = (options = { root: "" }) => {
83059
83333
  };
83060
83334
 
83061
83335
  // src/web/server.ts
83062
- import { existsSync as existsSync20 } from "node:fs";
83063
- import { readFileSync as readFileSync18 } from "node:fs";
83064
- import { join as join17 } from "node:path";
83336
+ import { existsSync as existsSync21 } from "node:fs";
83337
+ import { readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
83338
+ import { join as join18 } from "node:path";
83065
83339
 
83066
83340
  // ../../node_modules/.bun/hono@4.12.34/node_modules/hono/dist/compose.js
83067
83341
  var compose = (middleware, onError, onNotFound) => {
@@ -84781,7 +85055,7 @@ function registerAuditUsageRoutes(app, ctx) {
84781
85055
  }
84782
85056
 
84783
85057
  // src/web/routes/config.ts
84784
- import { homedir as homedir9 } from "node:os";
85058
+ import { homedir as homedir10 } from "node:os";
84785
85059
  import { sep } from "node:path";
84786
85060
  init_config();
84787
85061
 
@@ -84902,7 +85176,7 @@ function registerConfigRoutes(app, ctx) {
84902
85176
  let configFile = null;
84903
85177
  try {
84904
85178
  const abs = resolveEnvFile();
84905
- const home = homedir9();
85179
+ const home = homedir10();
84906
85180
  configFile = abs === home || abs.startsWith(home + sep) ? `~${abs.slice(home.length)}` : abs;
84907
85181
  } catch {}
84908
85182
  return c.json({ keys: entries, config_file: configFile });
@@ -86234,12 +86508,12 @@ import { execFileSync } from "node:child_process";
86234
86508
 
86235
86509
  // src/web/docs.ts
86236
86510
  import {
86237
- existsSync as existsSync17,
86238
- readFileSync as readFileSync16,
86511
+ existsSync as existsSync18,
86512
+ readFileSync as readFileSync17,
86239
86513
  readdirSync as readdirSync6,
86240
86514
  statSync as statSync6
86241
86515
  } from "node:fs";
86242
- import { basename as basename5, dirname as dirname6, join as join14, resolve as resolve5 } from "node:path";
86516
+ import { basename as basename5, dirname as dirname6, join as join15, resolve as resolve5 } from "node:path";
86243
86517
  import { fileURLToPath as fileURLToPath3 } from "node:url";
86244
86518
  var TOP_LEVEL_DOCS = [
86245
86519
  { filename: "README.md", path: "README.md", category: "readme" },
@@ -86260,32 +86534,32 @@ function moduleDir2() {
86260
86534
  function resolveDocsRoots() {
86261
86535
  const here = moduleDir2();
86262
86536
  const pkgRootCandidates = [
86263
- join14(here, "..", ".."),
86264
- join14(here, "..", "..", "..", "..")
86537
+ join15(here, "..", ".."),
86538
+ join15(here, "..", "..", "..", "..")
86265
86539
  ];
86266
86540
  let pkgGuides = null;
86267
86541
  let pkgTopLevel = null;
86268
86542
  for (const pkg of pkgRootCandidates) {
86269
- const guides = join14(pkg, "docs", "guides");
86270
- if (existsSync17(guides) && statSync6(guides).isDirectory()) {
86543
+ const guides = join15(pkg, "docs", "guides");
86544
+ if (existsSync18(guides) && statSync6(guides).isDirectory()) {
86271
86545
  pkgGuides = guides;
86272
86546
  pkgTopLevel = pkg;
86273
86547
  break;
86274
86548
  }
86275
86549
  }
86276
- const repoCandidate = join14(here, "..", "..", "..", "..");
86277
- const repoGuides = join14(repoCandidate, "docs", "guides");
86550
+ const repoCandidate = join15(here, "..", "..", "..", "..");
86551
+ const repoGuides = join15(repoCandidate, "docs", "guides");
86278
86552
  const repoTopLevel = repoCandidate;
86279
86553
  return {
86280
86554
  pkgGuides,
86281
86555
  pkgTopLevel,
86282
- repoGuides: existsSync17(repoGuides) ? repoGuides : null,
86283
- repoTopLevel: existsSync17(join14(repoTopLevel, "README.md")) ? repoTopLevel : null
86556
+ repoGuides: existsSync18(repoGuides) ? repoGuides : null,
86557
+ repoTopLevel: existsSync18(join15(repoTopLevel, "README.md")) ? repoTopLevel : null
86284
86558
  };
86285
86559
  }
86286
86560
  function readH1(filePath) {
86287
86561
  try {
86288
- const content = readFileSync16(filePath, "utf8");
86562
+ const content = readFileSync17(filePath, "utf8");
86289
86563
  const match = content.match(/^#\s+(.+?)\s*$/m);
86290
86564
  return match ? match[1] : null;
86291
86565
  } catch {
@@ -86305,8 +86579,8 @@ function listBundledDocs2() {
86305
86579
  const topRoot = pkgTopLevel ?? repoTopLevel;
86306
86580
  if (topRoot) {
86307
86581
  for (const t of TOP_LEVEL_DOCS) {
86308
- const abs = join14(topRoot, t.filename);
86309
- if (existsSync17(abs)) {
86582
+ const abs = join15(topRoot, t.filename);
86583
+ if (existsSync18(abs)) {
86310
86584
  entries.push(entryForFile(abs, t.path, t.category));
86311
86585
  }
86312
86586
  }
@@ -86315,7 +86589,7 @@ function listBundledDocs2() {
86315
86589
  if (guidesRoot) {
86316
86590
  const names = readdirSync6(guidesRoot).filter((n) => n.endsWith(".md")).sort();
86317
86591
  for (const name of names) {
86318
- const abs = join14(guidesRoot, name);
86592
+ const abs = join15(guidesRoot, name);
86319
86593
  entries.push(entryForFile(abs, `guides/${name}`, "guide"));
86320
86594
  }
86321
86595
  }
@@ -86333,9 +86607,9 @@ function readDoc(docPath) {
86333
86607
  if (!candidate.startsWith(rootResolved + "/") && candidate !== rootResolved) {
86334
86608
  return null;
86335
86609
  }
86336
- if (existsSync17(candidate) && statSync6(candidate).isFile()) {
86610
+ if (existsSync18(candidate) && statSync6(candidate).isFile()) {
86337
86611
  try {
86338
- return readFileSync16(candidate, "utf8");
86612
+ return readFileSync17(candidate, "utf8");
86339
86613
  } catch {
86340
86614
  return null;
86341
86615
  }
@@ -86490,17 +86764,17 @@ function registerPostgrestProxy(app) {
86490
86764
 
86491
86765
  // src/web/routes/preferences.ts
86492
86766
  init_config();
86493
- import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync17, writeFileSync as writeFileSync6 } from "node:fs";
86494
- import { join as join15 } from "node:path";
86767
+ import { existsSync as existsSync19, mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync7 } from "node:fs";
86768
+ import { join as join16 } from "node:path";
86495
86769
  function isTheme(v) {
86496
86770
  return v === "auto" || v === "light" || v === "dark";
86497
86771
  }
86498
86772
  function prefsFile() {
86499
- return join15(userStateDir(), "web-prefs.json");
86773
+ return join16(userStateDir(), "web-prefs.json");
86500
86774
  }
86501
86775
  function readPrefs() {
86502
86776
  try {
86503
- const raw = JSON.parse(readFileSync17(prefsFile(), "utf8"));
86777
+ const raw = JSON.parse(readFileSync18(prefsFile(), "utf8"));
86504
86778
  if (isTheme(raw.theme))
86505
86779
  return { theme: raw.theme };
86506
86780
  } catch {}
@@ -86516,9 +86790,9 @@ function registerPreferencesRoutes(app) {
86516
86790
  const next = { ...readPrefs(), theme: body.theme };
86517
86791
  try {
86518
86792
  const dir = userStateDir();
86519
- if (!existsSync18(dir))
86520
- mkdirSync5(dir, { recursive: true });
86521
- writeFileSync6(prefsFile(), `${JSON.stringify(next, null, 2)}
86793
+ if (!existsSync19(dir))
86794
+ mkdirSync6(dir, { recursive: true });
86795
+ writeFileSync7(prefsFile(), `${JSON.stringify(next, null, 2)}
86522
86796
  `);
86523
86797
  } catch (err) {
86524
86798
  return c.json({ detail: err instanceof Error ? err.message : String(err) }, 500);
@@ -86653,21 +86927,21 @@ function registerProjectsRoutes(app, ctx) {
86653
86927
  }
86654
86928
 
86655
86929
  // src/web/static.ts
86656
- import { existsSync as existsSync19, statSync as statSync7 } from "node:fs";
86657
- import { dirname as dirname7, join as join16 } from "node:path";
86930
+ import { existsSync as existsSync20, statSync as statSync7 } from "node:fs";
86931
+ import { dirname as dirname7, join as join17 } from "node:path";
86658
86932
  import { fileURLToPath as fileURLToPath4 } from "node:url";
86659
86933
  function moduleDir3() {
86660
86934
  return dirname7(fileURLToPath4(import.meta.url));
86661
86935
  }
86662
86936
  function isUsableSpaDir(dir) {
86663
- return existsSync19(dir) && statSync7(dir).isDirectory() && existsSync19(join16(dir, "index.html"));
86937
+ return existsSync20(dir) && statSync7(dir).isDirectory() && existsSync20(join17(dir, "index.html"));
86664
86938
  }
86665
86939
  function resolveSpaDist() {
86666
86940
  const here = moduleDir3();
86667
86941
  const candidates = [
86668
- join16(here, "..", "frontend"),
86669
- join16(here, "..", "..", "..", "..", "frontend", "dist"),
86670
- join16(here, "..", "..", "dist", "frontend")
86942
+ join17(here, "..", "frontend"),
86943
+ join17(here, "..", "..", "..", "..", "frontend", "dist"),
86944
+ join17(here, "..", "..", "dist", "frontend")
86671
86945
  ];
86672
86946
  for (const c of candidates) {
86673
86947
  if (isUsableSpaDir(c))
@@ -86678,11 +86952,11 @@ function resolveSpaDist() {
86678
86952
  function resolveStaticDir() {
86679
86953
  const here = moduleDir3();
86680
86954
  const candidates = [
86681
- join16(here, "..", "static"),
86682
- join16(here, "..", "..", "..", "..", "web", "static")
86955
+ join17(here, "..", "static"),
86956
+ join17(here, "..", "..", "..", "..", "web", "static")
86683
86957
  ];
86684
86958
  for (const c of candidates) {
86685
- if (existsSync19(c) && statSync7(c).isDirectory())
86959
+ if (existsSync20(c) && statSync7(c).isDirectory())
86686
86960
  return c;
86687
86961
  }
86688
86962
  return null;
@@ -86772,10 +87046,27 @@ function buildApp(ctx = buildWebContext()) {
86772
87046
  root: spaDist,
86773
87047
  rewriteRequestPath: (path) => path.replace(/^\/app/, "") || "/"
86774
87048
  }));
86775
- const indexPath = join17(spaDist, "index.html");
86776
- if (existsSync20(indexPath)) {
86777
- const indexHtml = readFileSync18(indexPath, "utf8");
86778
- app.get("/app/*", (c) => c.html(indexHtml));
87049
+ app.get("/app/assets/*", (c) => c.text("Not found", 404));
87050
+ const indexPath = join18(spaDist, "index.html");
87051
+ if (existsSync21(indexPath)) {
87052
+ let cached = null;
87053
+ const readIndex = () => {
87054
+ try {
87055
+ const { mtimeMs, size } = statSync8(indexPath);
87056
+ if (!cached || cached.mtimeMs !== mtimeMs || cached.size !== size) {
87057
+ cached = { mtimeMs, size, html: readFileSync19(indexPath, "utf8") };
87058
+ }
87059
+ } catch {}
87060
+ return cached?.html ?? null;
87061
+ };
87062
+ readIndex();
87063
+ app.get("/app/*", (c) => {
87064
+ const html = readIndex();
87065
+ if (html === null) {
87066
+ return c.text("The web UI could not be read from disk. Restart the server.", 503);
87067
+ }
87068
+ return c.html(html);
87069
+ });
86779
87070
  }
86780
87071
  }
86781
87072
  app.get("/", (c) => c.html(ROOT_REDIRECT_HTML));
@@ -86840,160 +87131,8 @@ async function buildWebServer(options = {}) {
86840
87131
  };
86841
87132
  }
86842
87133
 
86843
- // src/web/daemon.ts
86844
- import { spawn } from "node:child_process";
86845
- import {
86846
- existsSync as existsSync21,
86847
- mkdirSync as mkdirSync6,
86848
- openSync,
86849
- readFileSync as readFileSync19,
86850
- rmSync,
86851
- writeFileSync as writeFileSync7
86852
- } from "node:fs";
86853
- import { homedir as homedir10 } from "node:os";
86854
- import { join as join18 } from "node:path";
86855
- function resolveStateDir(override = process.env.CEREFOX_CONFIG_DIR, home = homedir10()) {
86856
- override = (override ?? "").trim();
86857
- if (!override)
86858
- return join18(home, ".cerefox");
86859
- return override === "~" || override.startsWith("~/") ? join18(home, override.slice(2)) : override;
86860
- }
86861
- var STATE_DIR = resolveStateDir();
86862
- var PID_FILE = join18(STATE_DIR, "web.pid");
86863
- var LOG_FILE = join18(STATE_DIR, "web.log");
86864
- var daemonPaths = { stateDir: STATE_DIR, pidFile: PID_FILE, logFile: LOG_FILE };
86865
- function ensureStateDir() {
86866
- if (!existsSync21(STATE_DIR))
86867
- mkdirSync6(STATE_DIR, { recursive: true });
86868
- }
86869
- function readPidFile() {
86870
- if (!existsSync21(PID_FILE))
86871
- return null;
86872
- try {
86873
- const parsed = JSON.parse(readFileSync19(PID_FILE, "utf8"));
86874
- if (typeof parsed.pid !== "number")
86875
- return null;
86876
- return {
86877
- pid: parsed.pid,
86878
- port: typeof parsed.port === "number" ? parsed.port : 8000,
86879
- host: typeof parsed.host === "string" ? parsed.host : "127.0.0.1",
86880
- startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : "unknown"
86881
- };
86882
- } catch {
86883
- return null;
86884
- }
86885
- }
86886
- function writePidFile(info) {
86887
- ensureStateDir();
86888
- writeFileSync7(PID_FILE, JSON.stringify(info, null, 2) + `
86889
- `, "utf8");
86890
- }
86891
- function removePidFile() {
86892
- rmSync(PID_FILE, { force: true });
86893
- }
86894
- function isProcessAlive(pid) {
86895
- try {
86896
- process.kill(pid, 0);
86897
- return true;
86898
- } catch (err) {
86899
- return err.code === "EPERM";
86900
- }
86901
- }
86902
- async function isResponding(host, port, timeoutMs = 1500) {
86903
- const probeHost = host === "0.0.0.0" ? "127.0.0.1" : host;
86904
- try {
86905
- const ctrl = new AbortController;
86906
- const timer = setTimeout(() => ctrl.abort(), timeoutMs);
86907
- try {
86908
- const resp = await fetch(`http://${probeHost}:${port}/api/v1/version`, {
86909
- signal: ctrl.signal
86910
- });
86911
- return resp.ok;
86912
- } finally {
86913
- clearTimeout(timer);
86914
- }
86915
- } catch {
86916
- return false;
86917
- }
86918
- }
86919
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
86920
- function assertUnix() {
86921
- if (process.platform === "win32") {
86922
- throw new Error("Daemon mode (`cerefox web start/stop/status`) is not supported on Windows yet. " + "Run `cerefox web` in the foreground, or set up a Windows service manually.");
86923
- }
86924
- }
86925
- async function startDaemon(opts) {
86926
- assertUnix();
86927
- ensureStateDir();
86928
- const existing = readPidFile();
86929
- if (existing && isProcessAlive(existing.pid)) {
86930
- return existing.port === opts.port ? { kind: "already-running", info: existing } : { kind: "port-conflict", info: existing };
86931
- }
86932
- if (existing)
86933
- removePidFile();
86934
- const logFd = openSync(LOG_FILE, "a");
86935
- const child = spawn(opts.runtime, [opts.scriptPath, "web", "--host", opts.host, "--port", String(opts.port)], { detached: true, stdio: ["ignore", logFd, logFd] });
86936
- child.unref();
86937
- if (typeof child.pid !== "number") {
86938
- throw new Error("Failed to spawn the web server process (no pid).");
86939
- }
86940
- writePidFile({
86941
- pid: child.pid,
86942
- port: opts.port,
86943
- host: opts.host,
86944
- startedAt: new Date().toISOString()
86945
- });
86946
- let responding = false;
86947
- for (let i = 0;i < 20; i++) {
86948
- if (!isProcessAlive(child.pid))
86949
- break;
86950
- if (await isResponding(opts.host, opts.port)) {
86951
- responding = true;
86952
- break;
86953
- }
86954
- await sleep2(250);
86955
- }
86956
- return { kind: "started", pid: child.pid, responding };
86957
- }
86958
- async function stopDaemon() {
86959
- assertUnix();
86960
- const info = readPidFile();
86961
- if (!info || !isProcessAlive(info.pid)) {
86962
- removePidFile();
86963
- return { kind: "not-running" };
86964
- }
86965
- try {
86966
- process.kill(info.pid, "SIGTERM");
86967
- } catch {}
86968
- let forced = false;
86969
- let alive = true;
86970
- for (let i = 0;i < 12; i++) {
86971
- await sleep2(250);
86972
- if (!isProcessAlive(info.pid)) {
86973
- alive = false;
86974
- break;
86975
- }
86976
- }
86977
- if (alive) {
86978
- try {
86979
- process.kill(info.pid, "SIGKILL");
86980
- forced = true;
86981
- } catch {}
86982
- }
86983
- removePidFile();
86984
- return { kind: "stopped", pid: info.pid, forced };
86985
- }
86986
- async function statusDaemon() {
86987
- const info = readPidFile();
86988
- if (!info)
86989
- return { kind: "stopped" };
86990
- if (!isProcessAlive(info.pid))
86991
- return { kind: "stale", info };
86992
- const responding = await isResponding(info.host, info.port);
86993
- return { kind: "running", info, responding };
86994
- }
86995
-
86996
87134
  // src/cli/commands/web.ts
87135
+ init_meta();
86997
87136
  function parsePort(raw) {
86998
87137
  const port = Number.parseInt(raw, 10);
86999
87138
  if (!Number.isFinite(port) || port < 1 || port > 65535) {
@@ -87113,6 +87252,10 @@ Full log: ${daemonPaths.logFile}`);
87113
87252
  case "running":
87114
87253
  if (status.responding) {
87115
87254
  println(c.green(`Cerefox web: running on :${status.info.port} (pid ${status.info.pid}, since ${status.info.startedAt}).`));
87255
+ if (status.version && status.version !== PKG_VERSION) {
87256
+ println(c.yellow(` ⚠ It is serving v${status.version}; this CLI is v${PKG_VERSION}.`));
87257
+ println(c.dim(" Restart to pick up the new build: " + restartCommand(status.info.host, status.info.port)));
87258
+ }
87116
87259
  } else {
87117
87260
  println(c.yellow(`Cerefox web: process ${status.info.pid} alive but not responding on :${status.info.port}.`));
87118
87261
  println(c.dim(` Check the log: ${daemonPaths.logFile}`));