@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.
- package/AGENT_GUIDE.md +11 -0
- package/AGENT_QUICK_REFERENCE.md +9 -8
- package/dist/bin/cerefox.js +423 -280
- package/dist/frontend/assets/{index-BC_iqEHZ.js → index-DUY9wimN.js} +29 -29
- package/dist/frontend/assets/index-DUY9wimN.js.map +1 -0
- package/dist/frontend/index.html +1 -1
- package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +8 -3
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +2 -2
- package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +29 -7
- package/dist/server-assets/_shared/mcp-tools/search.ts +79 -17
- package/dist/server-assets/supabase/functions/cerefox-search/index.ts +26 -24
- package/docs/guides/connect-agents.md +8 -2
- package/package.json +1 -1
- package/dist/frontend/assets/index-BC_iqEHZ.js.map +0 -1
package/dist/bin/cerefox.js
CHANGED
|
@@ -7400,7 +7400,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
7400
7400
|
});
|
|
7401
7401
|
|
|
7402
7402
|
// src/meta.ts
|
|
7403
|
-
var PKG_VERSION = "1.14.
|
|
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
|
-
|
|
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.
|
|
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
|
|
25927
|
-
import { homedir as
|
|
25928
|
-
import { join as
|
|
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
|
|
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 (!
|
|
25949
|
-
|
|
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").\
|
|
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
|
|
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:
|
|
56653
|
+
result_count: matched.length,
|
|
56654
|
+
...truncated ? { extra: { returned: accepted.length, truncated: true } } : {}
|
|
56607
56655
|
});
|
|
56608
|
-
if (
|
|
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
|
-
[
|
|
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
|
|
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 =
|
|
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
|
|
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 (
|
|
70929
|
-
const m =
|
|
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
|
|
77478
|
-
import { homedir as
|
|
77479
|
-
import { join as
|
|
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 (!
|
|
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 (!
|
|
77961
|
+
if (!existsSync11(assets.schemaFile))
|
|
77743
77962
|
return null;
|
|
77744
|
-
const m =
|
|
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 (!
|
|
78206
|
+
if (!existsSync11(path))
|
|
77988
78207
|
return false;
|
|
77989
78208
|
try {
|
|
77990
|
-
const parsed = JSON.parse(
|
|
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 =
|
|
77999
|
-
const claudeCodeUser =
|
|
78000
|
-
const claudeCodeProj =
|
|
78001
|
-
const claudeDesktop = process.platform === "darwin" ?
|
|
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 =
|
|
78026
|
-
if (!
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
|
79680
|
+
return readFileSync11(0, "utf8");
|
|
79413
79681
|
if (value.startsWith("@"))
|
|
79414
|
-
return
|
|
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
|
|
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 =
|
|
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
|
|
79589
|
-
mkdirSync as
|
|
79590
|
-
readFileSync as
|
|
79591
|
-
writeFileSync as
|
|
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
|
|
79594
|
-
import { dirname as dirname4, join as
|
|
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 (!
|
|
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(
|
|
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(
|
|
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
|
-
|
|
79899
|
-
|
|
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 =
|
|
79910
|
-
const cwdEnv =
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
80558
|
-
import { homedir as
|
|
80559
|
-
import { join as
|
|
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
|
|
80830
|
+
return homedir9();
|
|
80563
80831
|
if (path.startsWith("~/"))
|
|
80564
|
-
return
|
|
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 (!
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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 (!
|
|
81064
|
-
|
|
81337
|
+
if (!existsSync15(path)) {
|
|
81338
|
+
writeFileSync6(path, `${header}${line}
|
|
81065
81339
|
`, { mode: 384 });
|
|
81066
81340
|
return { path, action: "created" };
|
|
81067
81341
|
}
|
|
81068
|
-
const original =
|
|
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
|
-
|
|
81363
|
+
writeFileSync6(path, next);
|
|
81090
81364
|
return { path, action, backupPath };
|
|
81091
81365
|
}
|
|
81092
81366
|
function readEnvVar(path, key) {
|
|
81093
|
-
if (!
|
|
81367
|
+
if (!existsSync15(path))
|
|
81094
81368
|
return null;
|
|
81095
|
-
const 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
|
|
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
|
|
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
|
|
82906
|
-
import { join as
|
|
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 !== "" && !
|
|
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 =
|
|
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 =
|
|
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
|
|
83063
|
-
import { readFileSync as
|
|
83064
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
|
86238
|
-
readFileSync as
|
|
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
|
|
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
|
-
|
|
86264
|
-
|
|
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 =
|
|
86270
|
-
if (
|
|
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 =
|
|
86277
|
-
const repoGuides =
|
|
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:
|
|
86283
|
-
repoTopLevel:
|
|
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 =
|
|
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 =
|
|
86309
|
-
if (
|
|
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 =
|
|
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 (
|
|
86610
|
+
if (existsSync18(candidate) && statSync6(candidate).isFile()) {
|
|
86337
86611
|
try {
|
|
86338
|
-
return
|
|
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
|
|
86494
|
-
import { join as
|
|
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
|
|
86773
|
+
return join16(userStateDir(), "web-prefs.json");
|
|
86500
86774
|
}
|
|
86501
86775
|
function readPrefs() {
|
|
86502
86776
|
try {
|
|
86503
|
-
const raw = JSON.parse(
|
|
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 (!
|
|
86520
|
-
|
|
86521
|
-
|
|
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
|
|
86657
|
-
import { dirname as dirname7, join as
|
|
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
|
|
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
|
-
|
|
86669
|
-
|
|
86670
|
-
|
|
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
|
-
|
|
86682
|
-
|
|
86955
|
+
join17(here, "..", "static"),
|
|
86956
|
+
join17(here, "..", "..", "..", "..", "web", "static")
|
|
86683
86957
|
];
|
|
86684
86958
|
for (const c of candidates) {
|
|
86685
|
-
if (
|
|
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
|
-
|
|
86776
|
-
|
|
86777
|
-
|
|
86778
|
-
|
|
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}`));
|