@cerefox/memory 1.16.1 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +6 -4
- package/AGENT_QUICK_REFERENCE.md +1 -1
- package/dist/bin/cerefox.js +186 -81
- package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
- package/dist/server-assets/_shared/embeddings/onnx-embedder.ts +149 -6
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +3 -32
- package/dist/server-assets/_shared/mcp-tools/escape-heuristic.ts +23 -0
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +2 -2
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +5 -4
- package/dist/server-assets/_shared/mcp-tools/search.ts +1 -1
- package/dist/server-assets/db/rpcs.sql +72 -20
- package/dist/server-assets/db/schema.sql +1 -1
- package/docs/guides/configuration.md +22 -5
- package/docs/guides/linking.md +2 -2
- package/docs/guides/ops-scripts.md +45 -0
- package/docs/guides/setup-local.md +4 -2
- package/docs/guides/upgrading.md +7 -0
- package/package.json +1 -1
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.17.1";
|
|
5322
5322
|
var init_meta = () => {};
|
|
5323
5323
|
|
|
5324
5324
|
// ../../_shared/config/paths.ts
|
|
@@ -23719,24 +23719,6 @@ function getMaxResponseBytes() {
|
|
|
23719
23719
|
const n = Number.parseInt(raw, 10);
|
|
23720
23720
|
return Number.isNaN(n) || n <= 0 ? MAX_RESPONSE_BYTES : n;
|
|
23721
23721
|
}
|
|
23722
|
-
function readEnv(name) {
|
|
23723
|
-
const g = globalThis;
|
|
23724
|
-
const fromProcess = g.process?.env?.[name];
|
|
23725
|
-
if (fromProcess !== undefined && fromProcess !== "")
|
|
23726
|
-
return fromProcess;
|
|
23727
|
-
try {
|
|
23728
|
-
const fromDeno = g.Deno?.env?.get(name);
|
|
23729
|
-
return fromDeno === "" ? undefined : fromDeno;
|
|
23730
|
-
} catch {
|
|
23731
|
-
return;
|
|
23732
|
-
}
|
|
23733
|
-
}
|
|
23734
|
-
function getSearchAlpha() {
|
|
23735
|
-
return DEFAULT_SEARCH_ALPHA;
|
|
23736
|
-
}
|
|
23737
|
-
function getMinSearchScore() {
|
|
23738
|
-
return readEnv("CEREFOX_EMBEDDER") === "local" ? DEFAULT_MIN_SEARCH_SCORE_LOCAL : DEFAULT_MIN_SEARCH_SCORE;
|
|
23739
|
-
}
|
|
23740
23722
|
function getConfiguredMinSearchScore() {
|
|
23741
23723
|
return;
|
|
23742
23724
|
}
|
|
@@ -23793,7 +23775,7 @@ function resolveByteBudget(requested, ceiling) {
|
|
|
23793
23775
|
return ceiling;
|
|
23794
23776
|
return Math.min(Math.max(n, 1), ceiling);
|
|
23795
23777
|
}
|
|
23796
|
-
var MAX_RESPONSE_BYTES = 200000
|
|
23778
|
+
var MAX_RESPONSE_BYTES = 200000;
|
|
23797
23779
|
var init__utils = __esm(() => {
|
|
23798
23780
|
init_audit_ops();
|
|
23799
23781
|
});
|
|
@@ -26248,7 +26230,7 @@ var init_bundled_docs = __esm(() => {
|
|
|
26248
26230
|
});
|
|
26249
26231
|
|
|
26250
26232
|
// ../../_shared/ef-meta/index.ts
|
|
26251
|
-
var EF_VERSION = "1.
|
|
26233
|
+
var EF_VERSION = "1.17.1", CEREFOX_VERSION = "1.17.1", EF_LAST_CHANGED = "1.17.1";
|
|
26252
26234
|
var init_ef_meta = () => {};
|
|
26253
26235
|
|
|
26254
26236
|
// ../../_shared/compatibility/index.ts
|
|
@@ -26387,20 +26369,22 @@ var init_compatibility = __esm(() => {
|
|
|
26387
26369
|
// ../../_shared/embeddings/onnx-embedder.ts
|
|
26388
26370
|
var exports_onnx_embedder = {};
|
|
26389
26371
|
__export(exports_onnx_embedder, {
|
|
26372
|
+
EmbedderDownloadError: () => EmbedderDownloadError,
|
|
26390
26373
|
ONNX_MODEL_APPROX_MB: () => ONNX_MODEL_APPROX_MB,
|
|
26391
26374
|
ONNX_MODEL_DIM: () => ONNX_MODEL_DIM,
|
|
26392
26375
|
ONNX_MODEL_DTYPE: () => ONNX_MODEL_DTYPE,
|
|
26393
26376
|
ONNX_MODEL_ID: () => ONNX_MODEL_ID,
|
|
26394
26377
|
ONNX_MODEL_NAME: () => ONNX_MODEL_NAME,
|
|
26395
26378
|
buildPrefixedInputs: () => buildPrefixedInputs,
|
|
26379
|
+
downloadModelFiles: () => downloadModelFiles,
|
|
26396
26380
|
isModelCached: () => isModelCached,
|
|
26397
26381
|
nomicPrefix: () => nomicPrefix,
|
|
26398
26382
|
onnxEmbed: () => onnxEmbed,
|
|
26399
26383
|
warmup: () => warmup
|
|
26400
26384
|
});
|
|
26401
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
|
|
26385
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, renameSync, statSync as statSync2, writeFileSync as writeFileSync5, appendFileSync } from "node:fs";
|
|
26402
26386
|
import { homedir as homedir6 } from "node:os";
|
|
26403
|
-
import { join as join9 } from "node:path";
|
|
26387
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
26404
26388
|
function nomicPrefix(role) {
|
|
26405
26389
|
return role === "query" ? "search_query: " : "search_document: ";
|
|
26406
26390
|
}
|
|
@@ -26408,6 +26392,86 @@ function buildPrefixedInputs(texts, role) {
|
|
|
26408
26392
|
const p = nomicPrefix(role);
|
|
26409
26393
|
return texts.map((t) => p + t);
|
|
26410
26394
|
}
|
|
26395
|
+
async function fetchRange(url, start, end, timeoutMs) {
|
|
26396
|
+
const res = await fetch(url, { headers: { Range: `bytes=${start}-${end}` }, signal: AbortSignal.timeout(timeoutMs) });
|
|
26397
|
+
if (res.status === 200) {
|
|
26398
|
+
await res.body?.cancel();
|
|
26399
|
+
return "no-range";
|
|
26400
|
+
}
|
|
26401
|
+
const total = Number(/\/(\d+)\s*$/.exec(res.headers.get("content-range") ?? "")?.[1] ?? 0);
|
|
26402
|
+
if (res.status === 416 && total > 0) {
|
|
26403
|
+
await res.body?.cancel();
|
|
26404
|
+
return { bytes: new Uint8Array(0), total };
|
|
26405
|
+
}
|
|
26406
|
+
if (res.status !== 206)
|
|
26407
|
+
throw new Error(`HTTP ${res.status}`);
|
|
26408
|
+
if (total === 0)
|
|
26409
|
+
throw new Error("partial response without a total size (Content-Range)");
|
|
26410
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
26411
|
+
const want = Math.min(end, total > 0 ? total - 1 : end) - start + 1;
|
|
26412
|
+
if (bytes.byteLength !== want)
|
|
26413
|
+
throw new Error(`segment ${start}-${end}: got ${bytes.byteLength} bytes, expected ${want}`);
|
|
26414
|
+
return { bytes, total };
|
|
26415
|
+
}
|
|
26416
|
+
async function downloadModelFiles(remoteHost, onProgress, onDone, opts = {}) {
|
|
26417
|
+
const attempts = opts.attempts ?? DOWNLOAD_ATTEMPTS;
|
|
26418
|
+
const backoffMs = opts.backoffMs ?? ((attempt) => Math.min(30, 2 ** attempt) * 1000);
|
|
26419
|
+
const segment = opts.segmentBytes ?? SEGMENT_BYTES;
|
|
26420
|
+
const timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
26421
|
+
const dir = getCacheDir();
|
|
26422
|
+
const base = remoteHost.endsWith("/") ? remoteHost : `${remoteHost}/`;
|
|
26423
|
+
for (const rel of MODEL_REPO_FILES) {
|
|
26424
|
+
const dest = join9(dir, ONNX_MODEL_ID, rel);
|
|
26425
|
+
if (existsSync10(dest))
|
|
26426
|
+
continue;
|
|
26427
|
+
mkdirSync4(dirname4(dest), { recursive: true });
|
|
26428
|
+
const part = `${dest}.part`;
|
|
26429
|
+
const url = `${base}${ONNX_MODEL_ID}/resolve/main/${rel}`;
|
|
26430
|
+
let failures = 0;
|
|
26431
|
+
let lastError = "";
|
|
26432
|
+
let total = 0;
|
|
26433
|
+
for (;; ) {
|
|
26434
|
+
const have = existsSync10(part) ? statSync2(part).size : 0;
|
|
26435
|
+
if (total > 0 && have >= total)
|
|
26436
|
+
break;
|
|
26437
|
+
try {
|
|
26438
|
+
const got = await fetchRange(url, have, have + segment - 1, timeoutMs);
|
|
26439
|
+
if (got === "no-range") {
|
|
26440
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(Math.max(timeoutMs, ONNX_MODEL_APPROX_MB * 1e5)) });
|
|
26441
|
+
if (!res.ok)
|
|
26442
|
+
throw new Error(`HTTP ${res.status}`);
|
|
26443
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
26444
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
26445
|
+
if (declared > 0 && bytes.byteLength !== declared) {
|
|
26446
|
+
throw new Error(`got ${bytes.byteLength} bytes, expected ${declared}`);
|
|
26447
|
+
}
|
|
26448
|
+
writeFileSync5(part, bytes);
|
|
26449
|
+
total = bytes.byteLength;
|
|
26450
|
+
} else {
|
|
26451
|
+
total = got.total;
|
|
26452
|
+
if (have > total) {
|
|
26453
|
+
writeFileSync5(part, new Uint8Array(0));
|
|
26454
|
+
continue;
|
|
26455
|
+
}
|
|
26456
|
+
appendFileSync(part, got.bytes);
|
|
26457
|
+
}
|
|
26458
|
+
failures = 0;
|
|
26459
|
+
onProgress(rel, statSync2(part).size, total);
|
|
26460
|
+
} catch (err) {
|
|
26461
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
26462
|
+
failures++;
|
|
26463
|
+
if (failures >= attempts)
|
|
26464
|
+
throw new EmbedderDownloadError(`${rel}: ${lastError}`);
|
|
26465
|
+
const wait = backoffMs(failures);
|
|
26466
|
+
process.stderr.write(`[cerefox-embed] ${rel}: ${lastError}; retrying in ${Math.round(wait / 1000)}s (${failures}/${attempts - 1})
|
|
26467
|
+
`);
|
|
26468
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
26469
|
+
}
|
|
26470
|
+
}
|
|
26471
|
+
renameSync(part, dest);
|
|
26472
|
+
onDone(rel, statSync2(dest).size);
|
|
26473
|
+
}
|
|
26474
|
+
}
|
|
26411
26475
|
function isModelCached(dir = getCacheDir()) {
|
|
26412
26476
|
return MODEL_FILES.every((f) => existsSync10(join9(dir, f)));
|
|
26413
26477
|
}
|
|
@@ -26536,9 +26600,9 @@ async function ensurePipeline() {
|
|
|
26536
26600
|
progressState.set(file, { ...prior, done: true });
|
|
26537
26601
|
}
|
|
26538
26602
|
};
|
|
26603
|
+
await downloadModelFiles(String(transformers.env.remoteHost ?? "https://huggingface.co/"), (file, loaded, total) => progressCallback({ status: "progress", file, loaded, total }), (file, size) => progressCallback({ status: "done", file, total: size }));
|
|
26539
26604
|
const pipe = await transformers.pipeline("feature-extraction", ONNX_MODEL_ID, {
|
|
26540
|
-
dtype: ONNX_MODEL_DTYPE
|
|
26541
|
-
progress_callback: progressCallback
|
|
26605
|
+
dtype: ONNX_MODEL_DTYPE
|
|
26542
26606
|
});
|
|
26543
26607
|
process.stderr.write(`[cerefox-embed] embedder ready.
|
|
26544
26608
|
`);
|
|
@@ -26582,14 +26646,23 @@ async function onnxEmbed(texts, role) {
|
|
|
26582
26646
|
}
|
|
26583
26647
|
return vectors;
|
|
26584
26648
|
}
|
|
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;
|
|
26649
|
+
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_REPO_FILES, MODEL_FILES, EmbedderDownloadError, DOWNLOAD_ATTEMPTS = 6, SEGMENT_BYTES, REQUEST_TIMEOUT_MS = 120000, transformersModule = null, pipelinePromise = null, DEFAULT_ONNX_BATCH = 4;
|
|
26586
26650
|
var init_onnx_embedder = __esm(() => {
|
|
26587
|
-
|
|
26651
|
+
MODEL_REPO_FILES = [
|
|
26588
26652
|
"config.json",
|
|
26589
26653
|
"tokenizer.json",
|
|
26590
26654
|
"tokenizer_config.json",
|
|
26591
26655
|
`onnx/model_quantized.onnx`
|
|
26592
|
-
]
|
|
26656
|
+
];
|
|
26657
|
+
MODEL_FILES = MODEL_REPO_FILES.map((f) => join9(ONNX_MODEL_ID, f));
|
|
26658
|
+
EmbedderDownloadError = class EmbedderDownloadError extends Error {
|
|
26659
|
+
status = 503;
|
|
26660
|
+
constructor(detail) {
|
|
26661
|
+
super(`The local embedding model (${ONNX_MODEL_NAME}, ~${ONNX_MODEL_APPROX_MB} MB) could not be downloaded: ${detail}. ` + "What was downloaded is kept, and the next request resumes it. Check the network and try again.");
|
|
26662
|
+
this.name = "EmbedderDownloadError";
|
|
26663
|
+
}
|
|
26664
|
+
};
|
|
26665
|
+
SEGMENT_BYTES = 4 * 1024 * 1024;
|
|
26593
26666
|
});
|
|
26594
26667
|
|
|
26595
26668
|
// ../../_shared/embeddings/index.ts
|
|
@@ -27138,6 +27211,16 @@ function escapedContentNote(content, channel = "mcp") {
|
|
|
27138
27211
|
const remedy = channel === "cli" ? `fix the escaping in the source and re-ingest.` : `re-send with actual characters. For long content, prefer ` + `ingesting from a file or building the document incrementally with ` + `cerefox_insert/cerefox_edit.`;
|
|
27139
27212
|
return ` Note: content contains ${parts.join(" and ")} sequence(s) against ` + `${m.realNewlines} real newline(s) — if line breaks or quotes were ` + `intended, ` + remedy;
|
|
27140
27213
|
}
|
|
27214
|
+
function escapedTitleNote(title) {
|
|
27215
|
+
const found = [...new Set((title.match(HTML_ENTITY) ?? []).map((e) => e.toLowerCase()))];
|
|
27216
|
+
if (found.length === 0)
|
|
27217
|
+
return "";
|
|
27218
|
+
return ` Note: the title contains HTML entities (${found.join(", ")}), stored literally. ` + `If you meant the characters themselves (e.g. "&" for "&", "<>" for "<>"), ` + `update the title with the actual characters.`;
|
|
27219
|
+
}
|
|
27220
|
+
var HTML_ENTITY;
|
|
27221
|
+
var init_escape_heuristic = __esm(() => {
|
|
27222
|
+
HTML_ENTITY = /&(?:amp|lt|gt|quot|apos|nbsp|#\d{1,6}|#x[0-9a-f]{1,6});/gi;
|
|
27223
|
+
});
|
|
27141
27224
|
|
|
27142
27225
|
// ../../_shared/ingest/chunker.ts
|
|
27143
27226
|
function cpLen(s) {
|
|
@@ -51310,6 +51393,7 @@ async function editHandler(supabase, args, ctx) {
|
|
|
51310
51393
|
}
|
|
51311
51394
|
var AUDIT_OP, insertTool, editTool;
|
|
51312
51395
|
var init_partial_edits2 = __esm(() => {
|
|
51396
|
+
init_escape_heuristic();
|
|
51313
51397
|
init_partial_edits();
|
|
51314
51398
|
init__chunker();
|
|
51315
51399
|
init__utils();
|
|
@@ -51436,12 +51520,12 @@ var init_partial_edits2 = __esm(() => {
|
|
|
51436
51520
|
});
|
|
51437
51521
|
|
|
51438
51522
|
// ../../_shared/mcp-tools/get-help-content.ts
|
|
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;
|
|
51523
|
+
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. Write the characters themselves: `&` and `<>`, never `&` or `<>` (entities are stored literally and the write reply will say so). 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;
|
|
51440
51524
|
var init_get_help_content = __esm(() => {
|
|
51441
51525
|
HELP_SECTIONS = {
|
|
51442
51526
|
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.",
|
|
51443
51527
|
"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.',
|
|
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`.',
|
|
51528
|
+
"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. Write the characters themselves: `&` and `<>`, never `&` or `<>` (entities are stored literally and the write reply will say so). 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`.',
|
|
51445
51529
|
"Update Workflow (ID-based -- preferred)": `## Update Workflow (ID-based -- preferred)
|
|
51446
51530
|
|
|
51447
51531
|
\`\`\`
|
|
@@ -51760,7 +51844,7 @@ async function handler6(supabase, args, ctx) {
|
|
|
51760
51844
|
await ensureDocumentInProject(supabase, existingDoc.id, project_name, { author, authorType: author_type });
|
|
51761
51845
|
}
|
|
51762
51846
|
const note = overrideNote;
|
|
51763
|
-
return `Document updated: "${title}" (id: ${existingDoc.id}), ${chunks.length} chunk(s), ${totalChars} chars. New content_hash: ${contentHash}.${note}${escapedContentNote(content)}`;
|
|
51847
|
+
return `Document updated: "${title}" (id: ${existingDoc.id}), ${chunks.length} chunk(s), ${totalChars} chars. New content_hash: ${contentHash}.${note}${escapedContentNote(content)}${escapedTitleNote(title)}`;
|
|
51764
51848
|
}
|
|
51765
51849
|
if (update_if_exists) {
|
|
51766
51850
|
let { data: existing } = await supabase.from("cerefox_documents").select("id, title, content_hash, deleted_at").eq("title", title).is("deleted_at", null).order("updated_at", { ascending: false }).limit(1);
|
|
@@ -51822,7 +51906,7 @@ async function handler6(supabase, args, ctx) {
|
|
|
51822
51906
|
} else if (project_name) {
|
|
51823
51907
|
await ensureDocumentInProject(supabase, existingDoc.id, project_name, { author, authorType: author_type });
|
|
51824
51908
|
}
|
|
51825
|
-
return `Document updated: "${existingDoc.title}" (id: ${existingDoc.id}), ${chunks.length} chunk(s), ${totalChars} chars. New content_hash: ${contentHash}.${escapedContentNote(content)}`;
|
|
51909
|
+
return `Document updated: "${existingDoc.title}" (id: ${existingDoc.id}), ${chunks.length} chunk(s), ${totalChars} chars. New content_hash: ${contentHash}.${escapedContentNote(content)}${escapedTitleNote(title)}`;
|
|
51826
51910
|
}
|
|
51827
51911
|
}
|
|
51828
51912
|
const { data: hashMatch } = await supabase.from("cerefox_documents").select("id, title, deleted_at").eq("content_hash", contentHash).limit(1);
|
|
@@ -51876,10 +51960,11 @@ async function handler6(supabase, args, ctx) {
|
|
|
51876
51960
|
result_count: chunks.length
|
|
51877
51961
|
});
|
|
51878
51962
|
const projectInfo = project_name ? `, project: "${project_name}"` : "";
|
|
51879
|
-
return `Document saved: "${title}" (id: ${documentId}), ${chunks.length} chunk(s), ${totalChars} chars${projectInfo}. ` + `content_hash: ${contentHash} — pass it as expected_content_hash on your next edit.` + escapedContentNote(content);
|
|
51963
|
+
return `Document saved: "${title}" (id: ${documentId}), ${chunks.length} chunk(s), ${totalChars} chars${projectInfo}. ` + `content_hash: ${contentHash} — pass it as expected_content_hash on your next edit.` + escapedContentNote(content) + escapedTitleNote(title);
|
|
51880
51964
|
}
|
|
51881
51965
|
var ingestTool;
|
|
51882
51966
|
var init_ingest = __esm(() => {
|
|
51967
|
+
init_escape_heuristic();
|
|
51883
51968
|
init__chunker();
|
|
51884
51969
|
init__projects();
|
|
51885
51970
|
init__utils();
|
|
@@ -52525,7 +52610,7 @@ var init_search = __esm(() => {
|
|
|
52525
52610
|
},
|
|
52526
52611
|
min_term_coverage: {
|
|
52527
52612
|
type: "number",
|
|
52528
|
-
description: "Keyword OR-fallback confidence bar 0–1 (default 0.
|
|
52613
|
+
description: "Keyword OR-fallback confidence bar 0–1 (default: server-configured, else 0.67): fraction of the query's meaningful terms a result must match to count as a confident hit; weaker matches return flagged below-confidence. 0 = any matching term. Needs schema ≥ 0.9.1."
|
|
52529
52614
|
},
|
|
52530
52615
|
max_bytes: {
|
|
52531
52616
|
type: "integer",
|
|
@@ -65705,9 +65790,10 @@ var CONFIG_CATALOG = [
|
|
|
65705
65790
|
},
|
|
65706
65791
|
{
|
|
65707
65792
|
key: "min_search_score",
|
|
65708
|
-
description: "Minimum cosine similarity for vector-side results.
|
|
65793
|
+
description: "Minimum cosine similarity for vector-side results. Unset, it follows the store's embedder: 0.5 OpenAI, 0.6 local (nomic).",
|
|
65709
65794
|
kind: "number",
|
|
65710
65795
|
defaultValue: "0.5",
|
|
65796
|
+
defaultRpc: "cerefox_default_min_search_score",
|
|
65711
65797
|
min: 0,
|
|
65712
65798
|
max: 1,
|
|
65713
65799
|
group: "Retrieval"
|
|
@@ -65716,7 +65802,7 @@ var CONFIG_CATALOG = [
|
|
|
65716
65802
|
key: "min_term_coverage",
|
|
65717
65803
|
description: "Fraction of a query's meaningful terms a keyword OR-fallback match must cover to count as confident.",
|
|
65718
65804
|
kind: "number",
|
|
65719
|
-
defaultValue: "0.
|
|
65805
|
+
defaultValue: "0.67",
|
|
65720
65806
|
min: 0,
|
|
65721
65807
|
max: 1,
|
|
65722
65808
|
group: "Retrieval"
|
|
@@ -71778,7 +71864,7 @@ init_config();
|
|
|
71778
71864
|
init_config();
|
|
71779
71865
|
init_compatibility();
|
|
71780
71866
|
init_server_assets();
|
|
71781
|
-
import { existsSync as existsSync11, readFileSync as readFileSync9, realpathSync, statSync as
|
|
71867
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9, realpathSync, statSync as statSync3 } from "node:fs";
|
|
71782
71868
|
import { homedir as homedir7 } from "node:os";
|
|
71783
71869
|
import { join as join10 } from "node:path";
|
|
71784
71870
|
|
|
@@ -72019,7 +72105,7 @@ function checkConfig() {
|
|
|
72019
72105
|
let modeDetail = "";
|
|
72020
72106
|
if (process.platform !== "win32") {
|
|
72021
72107
|
try {
|
|
72022
|
-
const mode =
|
|
72108
|
+
const mode = statSync3(envPath).mode & 511;
|
|
72023
72109
|
if (mode & 63) {
|
|
72024
72110
|
return {
|
|
72025
72111
|
name: "config",
|
|
@@ -72376,6 +72462,17 @@ async function checkEmbedderMismatch() {
|
|
|
72376
72462
|
const recorded = [...new Set(rows.map((r) => r.embedder_primary))];
|
|
72377
72463
|
const stale = recorded.filter((r) => r !== active);
|
|
72378
72464
|
if (stale.length === 0) {
|
|
72465
|
+
if (resolveEmbedderKind() === "local") {
|
|
72466
|
+
await Promise.resolve().then(() => init_onnx_embedder());
|
|
72467
|
+
if (!isModelCached()) {
|
|
72468
|
+
return {
|
|
72469
|
+
name: EMBEDDER_CHECK_NAME,
|
|
72470
|
+
status: "warn",
|
|
72471
|
+
detail: `configured "${active}", but the model is not downloaded yet; the first search or write will fetch it (~130 MB).`,
|
|
72472
|
+
hint: "Fetch it now with `cerefox embedder-warmup` (Cerefox Local: `cerefox-local embedder-warmup`); it resumes if the connection drops."
|
|
72473
|
+
};
|
|
72474
|
+
}
|
|
72475
|
+
}
|
|
72379
72476
|
return {
|
|
72380
72477
|
name: EMBEDDER_CHECK_NAME,
|
|
72381
72478
|
status: "ok",
|
|
@@ -73146,6 +73243,7 @@ function registerGetDoc(program) {
|
|
|
73146
73243
|
|
|
73147
73244
|
// src/cli/commands/ingest.ts
|
|
73148
73245
|
init_dist4();
|
|
73246
|
+
init_escape_heuristic();
|
|
73149
73247
|
init_cli_core();
|
|
73150
73248
|
init_config();
|
|
73151
73249
|
import { readFileSync as readFileSync11 } from "node:fs";
|
|
@@ -74034,7 +74132,7 @@ init_dist4();
|
|
|
74034
74132
|
init_cli_core();
|
|
74035
74133
|
init_config();
|
|
74036
74134
|
var import_cli_progress = __toESM(require_cli_progress(), 1);
|
|
74037
|
-
import { readdirSync as readdirSync4, statSync as
|
|
74135
|
+
import { readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
|
|
74038
74136
|
import { basename as basename3, extname as extname4, join as join11 } from "node:path";
|
|
74039
74137
|
function walk(dir, extensions) {
|
|
74040
74138
|
let entries;
|
|
@@ -74048,7 +74146,7 @@ function walk(dir, extensions) {
|
|
|
74048
74146
|
const full = join11(dir, name);
|
|
74049
74147
|
let stat;
|
|
74050
74148
|
try {
|
|
74051
|
-
stat =
|
|
74149
|
+
stat = statSync4(full);
|
|
74052
74150
|
} catch {
|
|
74053
74151
|
continue;
|
|
74054
74152
|
}
|
|
@@ -74151,10 +74249,10 @@ import {
|
|
|
74151
74249
|
existsSync as existsSync12,
|
|
74152
74250
|
mkdirSync as mkdirSync5,
|
|
74153
74251
|
readFileSync as readFileSync14,
|
|
74154
|
-
writeFileSync as
|
|
74252
|
+
writeFileSync as writeFileSync6
|
|
74155
74253
|
} from "node:fs";
|
|
74156
74254
|
import { homedir as homedir8 } from "node:os";
|
|
74157
|
-
import { dirname as
|
|
74255
|
+
import { dirname as dirname5, join as join12 } from "node:path";
|
|
74158
74256
|
async function readConfigFile(path) {
|
|
74159
74257
|
if (!existsSync12(path)) {
|
|
74160
74258
|
throw userError(`--config file not found: ${path}`);
|
|
@@ -74458,8 +74556,8 @@ async function postWriteLifecycle(envPath, options) {
|
|
|
74458
74556
|
println(c.dim(` Config in effect: ${envPath}`));
|
|
74459
74557
|
}
|
|
74460
74558
|
function writeAnswersTo(target, answers) {
|
|
74461
|
-
mkdirSync5(
|
|
74462
|
-
|
|
74559
|
+
mkdirSync5(dirname5(target), { recursive: true });
|
|
74560
|
+
writeFileSync6(target, buildEnvFile(answers), "utf8");
|
|
74463
74561
|
if (process.platform !== "win32") {
|
|
74464
74562
|
try {
|
|
74465
74563
|
chmodSync(target, 384);
|
|
@@ -74522,7 +74620,7 @@ async function action23(options) {
|
|
|
74522
74620
|
const ch = await promptMigrationChoice();
|
|
74523
74621
|
println("");
|
|
74524
74622
|
if (ch === "c") {
|
|
74525
|
-
mkdirSync5(
|
|
74623
|
+
mkdirSync5(dirname5(homeEnv), { recursive: true });
|
|
74526
74624
|
copyFileSync2(cwdEnv, homeEnv);
|
|
74527
74625
|
if (process.platform !== "win32") {
|
|
74528
74626
|
try {
|
|
@@ -75139,7 +75237,7 @@ function registerMigrateFormat(program) {
|
|
|
75139
75237
|
// src/cli/commands/restore.ts
|
|
75140
75238
|
init_cli_core();
|
|
75141
75239
|
init_client();
|
|
75142
|
-
import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync5, statSync as
|
|
75240
|
+
import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
|
|
75143
75241
|
import { homedir as homedir9 } from "node:os";
|
|
75144
75242
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
75145
75243
|
function expandHome2(path) {
|
|
@@ -75154,10 +75252,10 @@ function resolveBackupFile(target) {
|
|
|
75154
75252
|
if (!existsSync13(path)) {
|
|
75155
75253
|
throw userError(`Backup path not found: ${target}`);
|
|
75156
75254
|
}
|
|
75157
|
-
const stat =
|
|
75255
|
+
const stat = statSync5(path);
|
|
75158
75256
|
if (stat.isFile())
|
|
75159
75257
|
return path;
|
|
75160
|
-
const candidates = readdirSync5(path).filter((n) => n.endsWith(".json") && n.startsWith("cerefox-")).map((n) => ({ name: n, mtime:
|
|
75258
|
+
const candidates = readdirSync5(path).filter((n) => n.endsWith(".json") && n.startsWith("cerefox-")).map((n) => ({ name: n, mtime: statSync5(join13(path, n)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
75161
75259
|
if (candidates.length === 0) {
|
|
75162
75260
|
throw userError(`No cerefox-*.json files in ${path}`);
|
|
75163
75261
|
}
|
|
@@ -75491,7 +75589,7 @@ async function action32(query, options) {
|
|
|
75491
75589
|
}
|
|
75492
75590
|
}
|
|
75493
75591
|
function registerSearch(program) {
|
|
75494
|
-
program.command("search").description("Search the knowledge base (hybrid FTS + semantic).").argument("<query>", "Natural-language search query.").option("-c, --match-count <n>", "Maximum number of documents to return.", "5").option("-p, --project-name <name>", "Filter results to a specific project.").option("-f, --metadata-filter <json>", "JSON containment filter; only docs whose metadata contains ALL pairs are returned.").option("--mode <mode>", "Search mode: docs (default), hybrid, fts.", "docs").option("--alpha <float>", "Semantic weight 0..1 (default: CEREFOX_SEARCH_ALPHA; else 0.7).").option("--min-score <float>", "Minimum cosine similarity threshold (default:
|
|
75592
|
+
program.command("search").description("Search the knowledge base (hybrid FTS + semantic).").argument("<query>", "Natural-language search query.").option("-c, --match-count <n>", "Maximum number of documents to return.", "5").option("-p, --project-name <name>", "Filter results to a specific project.").option("-f, --metadata-filter <json>", "JSON containment filter; only docs whose metadata contains ALL pairs are returned.").option("--mode <mode>", "Search mode: docs (default), hybrid, fts.", "docs").option("--alpha <float>", "Semantic weight 0..1 (default: CEREFOX_SEARCH_ALPHA; else 0.7).").option("--min-score <float>", "Minimum cosine similarity threshold (default: the store's min_search_score; else 0.5, or 0.6 on a local-embedder store).").option("--min-term-coverage <float>", "OR-fallback keyword matches must cover at least this fraction of the query's meaningful terms to count as confident hits (default: the store's min_term_coverage; else 0.67; needs schema ≥ 0.9.1).").option("--max-bytes <n>", "Response size budget in bytes (default: CEREFOX_MAX_RESPONSE_BYTES or 200000).").addOption(authorOption("read")).addOption(requestorAliasOption()).option("--json", "Emit machine-readable JSON instead of the default text.").option("--only-metadata", "List matching docs (id, score, chunks, chars, partial/full) WITHOUT their content — like the web UI's collapsed result list. Grab a [id:…] then `cerefox document get <id>`.").action(action32);
|
|
75495
75593
|
}
|
|
75496
75594
|
|
|
75497
75595
|
// src/cli/commands/self-update.ts
|
|
@@ -75642,8 +75740,8 @@ init_config();
|
|
|
75642
75740
|
import { randomBytes } from "node:crypto";
|
|
75643
75741
|
|
|
75644
75742
|
// src/cli/util/env-file.ts
|
|
75645
|
-
import { copyFileSync as copyFileSync3, existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as
|
|
75646
|
-
import { dirname as
|
|
75743
|
+
import { copyFileSync as copyFileSync3, existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "node:fs";
|
|
75744
|
+
import { dirname as dirname6 } from "node:path";
|
|
75647
75745
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
75648
75746
|
function escapeRegExp(s) {
|
|
75649
75747
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -75653,7 +75751,7 @@ function upsertEnvVar(path, key, value, opts = {}) {
|
|
|
75653
75751
|
const header = opts.comment ? `# ${opts.comment}
|
|
75654
75752
|
` : "";
|
|
75655
75753
|
if (!existsSync14(path)) {
|
|
75656
|
-
|
|
75754
|
+
writeFileSync7(path, `${header}${line}
|
|
75657
75755
|
`, { mode: 384 });
|
|
75658
75756
|
return { path, action: "created" };
|
|
75659
75757
|
}
|
|
@@ -75678,7 +75776,7 @@ ${header}${line}
|
|
|
75678
75776
|
`;
|
|
75679
75777
|
action = "added";
|
|
75680
75778
|
}
|
|
75681
|
-
|
|
75779
|
+
writeFileSync7(path, next);
|
|
75682
75780
|
return { path, action, backupPath };
|
|
75683
75781
|
}
|
|
75684
75782
|
function readEnvVar(path, key) {
|
|
@@ -75689,7 +75787,7 @@ function readEnvVar(path, key) {
|
|
|
75689
75787
|
}
|
|
75690
75788
|
function envGitignoreWarning(path) {
|
|
75691
75789
|
try {
|
|
75692
|
-
const dir =
|
|
75790
|
+
const dir = dirname6(path);
|
|
75693
75791
|
const inTree = spawnSync6("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], {
|
|
75694
75792
|
encoding: "utf8"
|
|
75695
75793
|
});
|
|
@@ -77649,7 +77747,7 @@ var _baseMimes = {
|
|
|
77649
77747
|
var baseMimes = _baseMimes;
|
|
77650
77748
|
|
|
77651
77749
|
// ../../node_modules/.bun/@hono+node-server@2.1.1+2ac783cc5e75a70c/node_modules/@hono/node-server/dist/serve-static.mjs
|
|
77652
|
-
import { createReadStream, existsSync as existsSync16, statSync as
|
|
77750
|
+
import { createReadStream, existsSync as existsSync16, statSync as statSync6 } from "node:fs";
|
|
77653
77751
|
import { join as join14 } from "node:path";
|
|
77654
77752
|
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;
|
|
77655
77753
|
var ENCODINGS = {
|
|
@@ -77661,7 +77759,7 @@ var ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS);
|
|
|
77661
77759
|
var getStats = (path) => {
|
|
77662
77760
|
let stats;
|
|
77663
77761
|
try {
|
|
77664
|
-
stats =
|
|
77762
|
+
stats = statSync6(path);
|
|
77665
77763
|
} catch {}
|
|
77666
77764
|
return stats;
|
|
77667
77765
|
};
|
|
@@ -77807,7 +77905,7 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
77807
77905
|
|
|
77808
77906
|
// src/web/server.ts
|
|
77809
77907
|
import { existsSync as existsSync20 } from "node:fs";
|
|
77810
|
-
import { readFileSync as readFileSync19, statSync as
|
|
77908
|
+
import { readFileSync as readFileSync19, statSync as statSync9 } from "node:fs";
|
|
77811
77909
|
import { join as join18 } from "node:path";
|
|
77812
77910
|
|
|
77813
77911
|
// ../../node_modules/.bun/hono@4.13.7/node_modules/hono/dist/compose.js
|
|
@@ -79584,15 +79682,22 @@ function registerConfigRoutes(app, ctx) {
|
|
|
79584
79682
|
p_key: spec.key
|
|
79585
79683
|
});
|
|
79586
79684
|
const stored = error ? null : unwrapScalarRpc(data);
|
|
79685
|
+
let builtIn = spec.defaultValue;
|
|
79686
|
+
if (stored == null && spec.defaultRpc) {
|
|
79687
|
+
const d = await ctx.supabase.rpc(spec.defaultRpc, {});
|
|
79688
|
+
const v = d.error ? null : unwrapScalarRpc(d.data);
|
|
79689
|
+
if (v != null)
|
|
79690
|
+
builtIn = String(v);
|
|
79691
|
+
}
|
|
79587
79692
|
const envVar = RETIRED_ENV_VARS2[spec.key];
|
|
79588
79693
|
const envValue = envVar ? (process.env[envVar] ?? "").trim() : "";
|
|
79589
79694
|
return {
|
|
79590
79695
|
key: spec.key,
|
|
79591
79696
|
value: stored,
|
|
79592
|
-
effective: stored ??
|
|
79697
|
+
effective: stored ?? builtIn,
|
|
79593
79698
|
description: spec.description,
|
|
79594
79699
|
kind: spec.kind,
|
|
79595
|
-
default:
|
|
79700
|
+
default: builtIn,
|
|
79596
79701
|
min: spec.min ?? null,
|
|
79597
79702
|
max: spec.max ?? null,
|
|
79598
79703
|
group: spec.group,
|
|
@@ -79651,7 +79756,6 @@ function registerConfigRoutes(app, ctx) {
|
|
|
79651
79756
|
});
|
|
79652
79757
|
}
|
|
79653
79758
|
// src/web/routes/discovery.ts
|
|
79654
|
-
init__utils();
|
|
79655
79759
|
init_feature_flags();
|
|
79656
79760
|
|
|
79657
79761
|
// src/web/usage.ts
|
|
@@ -79880,10 +79984,8 @@ async function runSearch(ctx, opts) {
|
|
|
79880
79984
|
p_query_text: query,
|
|
79881
79985
|
p_query_embedding: embedding,
|
|
79882
79986
|
p_match_count: count,
|
|
79883
|
-
p_alpha: getSearchAlpha(),
|
|
79884
79987
|
p_use_upgrade: false,
|
|
79885
|
-
p_project_id: projectId
|
|
79886
|
-
p_min_score: getMinSearchScore()
|
|
79988
|
+
p_project_id: projectId
|
|
79887
79989
|
};
|
|
79888
79990
|
if (metadataFilter)
|
|
79889
79991
|
params.p_metadata_filter = metadataFilter;
|
|
@@ -79898,9 +80000,7 @@ async function runSearch(ctx, opts) {
|
|
|
79898
80000
|
p_query_text: query,
|
|
79899
80001
|
p_query_embedding: embedding,
|
|
79900
80002
|
p_match_count: Math.min(count, 5),
|
|
79901
|
-
|
|
79902
|
-
p_project_id: projectId,
|
|
79903
|
-
p_min_score: getMinSearchScore()
|
|
80003
|
+
p_project_id: projectId
|
|
79904
80004
|
};
|
|
79905
80005
|
if (metadataFilter)
|
|
79906
80006
|
params.p_metadata_filter = metadataFilter;
|
|
@@ -79995,6 +80095,9 @@ function registerDiscoveryRoutes(app, ctx) {
|
|
|
79995
80095
|
if (err instanceof HttpError) {
|
|
79996
80096
|
return c.json({ detail: err.message }, err.status);
|
|
79997
80097
|
}
|
|
80098
|
+
if (err instanceof Error && err.name === "EmbedderDownloadError") {
|
|
80099
|
+
return c.json({ detail: err.message }, 503);
|
|
80100
|
+
}
|
|
79998
80101
|
throw err;
|
|
79999
80102
|
}
|
|
80000
80103
|
const who = resolveCallerIdentity(c);
|
|
@@ -80794,6 +80897,8 @@ function notReady(error) {
|
|
|
80794
80897
|
return { success: false, error, detail: error };
|
|
80795
80898
|
}
|
|
80796
80899
|
function ingestFailure(err) {
|
|
80900
|
+
if (err instanceof Error && err.name === "EmbedderDownloadError")
|
|
80901
|
+
return [notReady(err.message), 503];
|
|
80797
80902
|
if (err instanceof ConcurrencyConflictError) {
|
|
80798
80903
|
return [
|
|
80799
80904
|
{
|
|
@@ -81060,7 +81165,7 @@ function registerIngestRoutes(app, ctx) {
|
|
|
81060
81165
|
init_server_assets();
|
|
81061
81166
|
init_meta();
|
|
81062
81167
|
import { execFileSync } from "node:child_process";
|
|
81063
|
-
import { dirname as
|
|
81168
|
+
import { dirname as dirname8 } from "node:path";
|
|
81064
81169
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
81065
81170
|
|
|
81066
81171
|
// src/web/docs.ts
|
|
@@ -81068,9 +81173,9 @@ import {
|
|
|
81068
81173
|
existsSync as existsSync17,
|
|
81069
81174
|
readFileSync as readFileSync17,
|
|
81070
81175
|
readdirSync as readdirSync6,
|
|
81071
|
-
statSync as
|
|
81176
|
+
statSync as statSync7
|
|
81072
81177
|
} from "node:fs";
|
|
81073
|
-
import { basename as basename5, dirname as
|
|
81178
|
+
import { basename as basename5, dirname as dirname7, join as join15, resolve as resolve5 } from "node:path";
|
|
81074
81179
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
81075
81180
|
var TOP_LEVEL_DOCS = [
|
|
81076
81181
|
{ filename: "README.md", path: "README.md", category: "readme" },
|
|
@@ -81086,7 +81191,7 @@ var TOP_LEVEL_DOCS = [
|
|
|
81086
81191
|
}
|
|
81087
81192
|
];
|
|
81088
81193
|
function moduleDir2() {
|
|
81089
|
-
return
|
|
81194
|
+
return dirname7(fileURLToPath3(import.meta.url));
|
|
81090
81195
|
}
|
|
81091
81196
|
function resolveDocsRoots() {
|
|
81092
81197
|
const here = moduleDir2();
|
|
@@ -81098,7 +81203,7 @@ function resolveDocsRoots() {
|
|
|
81098
81203
|
let pkgTopLevel = null;
|
|
81099
81204
|
for (const pkg of pkgRootCandidates) {
|
|
81100
81205
|
const guides = join15(pkg, "docs", "guides");
|
|
81101
|
-
if (existsSync17(guides) &&
|
|
81206
|
+
if (existsSync17(guides) && statSync7(guides).isDirectory()) {
|
|
81102
81207
|
pkgGuides = guides;
|
|
81103
81208
|
pkgTopLevel = pkg;
|
|
81104
81209
|
break;
|
|
@@ -81164,7 +81269,7 @@ function readDoc(docPath) {
|
|
|
81164
81269
|
if (!candidate.startsWith(rootResolved + "/") && candidate !== rootResolved) {
|
|
81165
81270
|
return null;
|
|
81166
81271
|
}
|
|
81167
|
-
if (existsSync17(candidate) &&
|
|
81272
|
+
if (existsSync17(candidate) && statSync7(candidate).isFile()) {
|
|
81168
81273
|
try {
|
|
81169
81274
|
return readFileSync17(candidate, "utf8");
|
|
81170
81275
|
} catch {
|
|
@@ -81190,7 +81295,7 @@ function readOpenApiDocument() {
|
|
|
81190
81295
|
|
|
81191
81296
|
// src/web/routes/meta.ts
|
|
81192
81297
|
init_compatibility();
|
|
81193
|
-
function resolveGitCommitShort(dir =
|
|
81298
|
+
function resolveGitCommitShort(dir = dirname8(fileURLToPath4(import.meta.url))) {
|
|
81194
81299
|
const env = process.env.CEREFOX_GIT_COMMIT;
|
|
81195
81300
|
if (env)
|
|
81196
81301
|
return env.slice(0, 7);
|
|
@@ -81322,7 +81427,7 @@ function registerPostgrestProxy(app) {
|
|
|
81322
81427
|
|
|
81323
81428
|
// src/web/routes/preferences.ts
|
|
81324
81429
|
init_config();
|
|
81325
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as
|
|
81430
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "node:fs";
|
|
81326
81431
|
import { join as join16 } from "node:path";
|
|
81327
81432
|
function isTheme(v) {
|
|
81328
81433
|
return v === "auto" || v === "light" || v === "dark";
|
|
@@ -81350,7 +81455,7 @@ function registerPreferencesRoutes(app) {
|
|
|
81350
81455
|
const dir = userStateDir();
|
|
81351
81456
|
if (!existsSync18(dir))
|
|
81352
81457
|
mkdirSync6(dir, { recursive: true });
|
|
81353
|
-
|
|
81458
|
+
writeFileSync8(prefsFile(), `${JSON.stringify(next, null, 2)}
|
|
81354
81459
|
`);
|
|
81355
81460
|
} catch (err) {
|
|
81356
81461
|
return c.json({ detail: err instanceof Error ? err.message : String(err) }, 500);
|
|
@@ -81485,14 +81590,14 @@ function registerProjectsRoutes(app, ctx) {
|
|
|
81485
81590
|
}
|
|
81486
81591
|
|
|
81487
81592
|
// src/web/static.ts
|
|
81488
|
-
import { existsSync as existsSync19, statSync as
|
|
81489
|
-
import { dirname as
|
|
81593
|
+
import { existsSync as existsSync19, statSync as statSync8 } from "node:fs";
|
|
81594
|
+
import { dirname as dirname9, join as join17 } from "node:path";
|
|
81490
81595
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
81491
81596
|
function moduleDir3() {
|
|
81492
|
-
return
|
|
81597
|
+
return dirname9(fileURLToPath5(import.meta.url));
|
|
81493
81598
|
}
|
|
81494
81599
|
function isUsableSpaDir(dir) {
|
|
81495
|
-
return existsSync19(dir) &&
|
|
81600
|
+
return existsSync19(dir) && statSync8(dir).isDirectory() && existsSync19(join17(dir, "index.html"));
|
|
81496
81601
|
}
|
|
81497
81602
|
function resolveSpaDist() {
|
|
81498
81603
|
const here = moduleDir3();
|
|
@@ -81514,7 +81619,7 @@ function resolveStaticDir() {
|
|
|
81514
81619
|
join17(here, "..", "..", "..", "..", "web", "static")
|
|
81515
81620
|
];
|
|
81516
81621
|
for (const c of candidates) {
|
|
81517
|
-
if (existsSync19(c) &&
|
|
81622
|
+
if (existsSync19(c) && statSync8(c).isDirectory())
|
|
81518
81623
|
return c;
|
|
81519
81624
|
}
|
|
81520
81625
|
return null;
|
|
@@ -81615,7 +81720,7 @@ function buildApp(ctx = buildWebContext()) {
|
|
|
81615
81720
|
let cached = null;
|
|
81616
81721
|
const readIndex = () => {
|
|
81617
81722
|
try {
|
|
81618
|
-
const { mtimeMs, size } =
|
|
81723
|
+
const { mtimeMs, size } = statSync9(indexPath);
|
|
81619
81724
|
if (!cached || cached.mtimeMs !== mtimeMs || cached.size !== size) {
|
|
81620
81725
|
cached = { mtimeMs, size, html: readFileSync19(indexPath, "utf8") };
|
|
81621
81726
|
}
|