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