@cerefox/memory 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7184,7 +7184,7 @@ var exports_meta = {};
7184
7184
  __export(exports_meta, {
7185
7185
  PKG_VERSION: () => PKG_VERSION
7186
7186
  });
7187
- var PKG_VERSION = "0.10.0";
7187
+ var PKG_VERSION = "0.10.2";
7188
7188
  var init_meta = () => {};
7189
7189
 
7190
7190
  // ../../node_modules/.bun/tslib@2.8.1/node_modules/tslib/tslib.js
@@ -24852,20 +24852,31 @@ var init_bundled_docs = __esm(() => {
24852
24852
  });
24853
24853
 
24854
24854
  // ../../_shared/embeddings/index.ts
24855
+ function openaiEmbeddingConfig() {
24856
+ const env4 = globalThis.process?.env ?? {};
24857
+ const base = env4.CEREFOX_OPENAI_BASE_URL?.replace(/\/+$/, "");
24858
+ const dims = Number.parseInt(env4.CEREFOX_OPENAI_EMBEDDING_DIMENSIONS ?? "", 10);
24859
+ return {
24860
+ url: base ? `${base}/embeddings` : OPENAI_EMBEDDING_URL,
24861
+ model: env4.CEREFOX_OPENAI_EMBEDDING_MODEL || OPENAI_MODEL,
24862
+ dimensions: Number.isNaN(dims) || dims <= 0 ? EMBEDDING_DIMENSIONS : dims
24863
+ };
24864
+ }
24855
24865
  async function getEmbedding(text, apiKey) {
24856
24866
  let lastError = null;
24867
+ const cfg = openaiEmbeddingConfig();
24857
24868
  for (let attempt = 0;attempt < EMBEDDING_MAX_RETRIES; attempt++) {
24858
24869
  try {
24859
- const response = await fetch(OPENAI_EMBEDDING_URL, {
24870
+ const response = await fetch(cfg.url, {
24860
24871
  method: "POST",
24861
24872
  headers: {
24862
24873
  Authorization: `Bearer ${apiKey}`,
24863
24874
  "Content-Type": "application/json"
24864
24875
  },
24865
24876
  body: JSON.stringify({
24866
- model: OPENAI_MODEL,
24877
+ model: cfg.model,
24867
24878
  input: text,
24868
- dimensions: EMBEDDING_DIMENSIONS
24879
+ dimensions: cfg.dimensions
24869
24880
  })
24870
24881
  });
24871
24882
  if (!response.ok) {
@@ -24896,18 +24907,19 @@ async function getEmbedding(text, apiKey) {
24896
24907
  }
24897
24908
  async function embedBatchSingleCall(texts, apiKey) {
24898
24909
  let lastError = null;
24910
+ const cfg = openaiEmbeddingConfig();
24899
24911
  for (let attempt = 0;attempt < EMBEDDING_MAX_RETRIES; attempt++) {
24900
24912
  try {
24901
- const response = await fetch(OPENAI_EMBEDDING_URL, {
24913
+ const response = await fetch(cfg.url, {
24902
24914
  method: "POST",
24903
24915
  headers: {
24904
24916
  Authorization: `Bearer ${apiKey}`,
24905
24917
  "Content-Type": "application/json"
24906
24918
  },
24907
24919
  body: JSON.stringify({
24908
- model: OPENAI_MODEL,
24920
+ model: cfg.model,
24909
24921
  input: texts,
24910
- dimensions: EMBEDDING_DIMENSIONS
24922
+ dimensions: cfg.dimensions
24911
24923
  })
24912
24924
  });
24913
24925
  if (!response.ok) {
@@ -53721,6 +53733,20 @@ var require_cli_progress = __commonJS((exports, module) => {
53721
53733
  });
53722
53734
 
53723
53735
  // ../../_shared/mcp-tools/_utils.ts
53736
+ function getMaxResponseBytes() {
53737
+ const raw = globalThis.process?.env?.CEREFOX_MAX_RESPONSE_BYTES;
53738
+ if (raw === undefined || raw === "")
53739
+ return MAX_RESPONSE_BYTES;
53740
+ const n = Number.parseInt(raw, 10);
53741
+ return Number.isNaN(n) || n <= 0 ? MAX_RESPONSE_BYTES : n;
53742
+ }
53743
+ function getMinSearchScore() {
53744
+ const raw = globalThis.process?.env?.CEREFOX_MIN_SEARCH_SCORE;
53745
+ if (raw === undefined || raw === "")
53746
+ return DEFAULT_MIN_SEARCH_SCORE;
53747
+ const n = Number.parseFloat(raw);
53748
+ return Number.isNaN(n) || n < 0 || n > 1 ? DEFAULT_MIN_SEARCH_SCORE : n;
53749
+ }
53724
53750
  function applyByteBudget(rows, maxBytes) {
53725
53751
  const accepted = [];
53726
53752
  let usedBytes = 0;
@@ -53748,7 +53774,7 @@ function logUsage(supabase, params) {
53748
53774
  p_extra: params.extra ?? {}
53749
53775
  })).catch(() => {});
53750
53776
  }
53751
- var MAX_RESPONSE_BYTES = 200000;
53777
+ var MAX_RESPONSE_BYTES = 200000, DEFAULT_MIN_SEARCH_SCORE = 0.5;
53752
53778
 
53753
53779
  // ../../_shared/mcp-tools/audit-log.ts
53754
53780
  async function handler(supabase, args, ctx) {
@@ -54531,7 +54557,8 @@ async function handler8(supabase, args, ctx) {
54531
54557
  if (!projectId)
54532
54558
  throw new Error(`Project not found: ${project_name}`);
54533
54559
  }
54534
- const max_bytes = include_content ? Math.min(requested_max_bytes ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES) : null;
54560
+ const ceiling = getMaxResponseBytes();
54561
+ const max_bytes = include_content ? Math.min(requested_max_bytes ?? ceiling, ceiling) : null;
54535
54562
  const params = {
54536
54563
  p_metadata_filter: metadata_filter,
54537
54564
  p_project_id: projectId,
@@ -54624,10 +54651,11 @@ async function handler9(supabase, args, ctx) {
54624
54651
  const match_count = args.match_count ?? 5;
54625
54652
  const mode = args.mode ?? "docs";
54626
54653
  const alpha = args.alpha ?? 0.7;
54627
- const min_score = args.min_score ?? 0.5;
54654
+ const min_score = args.min_score ?? getMinSearchScore();
54628
54655
  const metadata_filter = args.metadata_filter ?? null;
54629
54656
  const requested_max_bytes = args.max_bytes;
54630
- const max_bytes = Math.min(requested_max_bytes ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES);
54657
+ const ceiling = getMaxResponseBytes();
54658
+ const max_bytes = Math.min(requested_max_bytes ?? ceiling, ceiling);
54631
54659
  if (metadata_filter !== null && metadata_filter !== undefined && (typeof metadata_filter !== "object" || Array.isArray(metadata_filter))) {
54632
54660
  throw new McpInvalidParams("metadata_filter must be a JSON object or null");
54633
54661
  }
@@ -68087,7 +68115,7 @@ function utcStamp() {
68087
68115
  return d.getUTCFullYear().toString() + pad(d.getUTCMonth() + 1) + pad(d.getUTCDate()) + "T" + pad(d.getUTCHours()) + pad(d.getUTCMinutes()) + pad(d.getUTCSeconds()) + "Z";
68088
68116
  }
68089
68117
  async function action(options) {
68090
- const outDir = resolve(expandHome(options.outputDir ?? "~/.cerefox/backups"));
68118
+ const outDir = resolve(expandHome(options.outputDir ?? process.env.CEREFOX_BACKUP_DIR ?? "~/.cerefox/backups"));
68091
68119
  if (!existsSync2(outDir))
68092
68120
  mkdirSync(outDir, { recursive: true });
68093
68121
  const stamp = utcStamp();
@@ -68133,7 +68161,7 @@ async function action(options) {
68133
68161
  }
68134
68162
  }
68135
68163
  function registerBackup(program2) {
68136
- program2.command("backup").description("Write a JSON snapshot of the knowledge base.").option("-o, --output-dir <dir>", "Snapshot output directory.", "~/.cerefox/backups").option("-l, --label <label>", "Optional suffix added to the filename.").option("--include-versions", "Include archived versions in the snapshot. (v0.5: ignored — current chunks only.)").option("--git", "Commit the snapshot to the output dir as a git checkpoint. (v0.5: ignored.)").action(action);
68164
+ program2.command("backup").description("Write a JSON snapshot of the knowledge base.").option("-o, --output-dir <dir>", "Snapshot output directory (default: CEREFOX_BACKUP_DIR or ~/.cerefox/backups).").option("-l, --label <label>", "Optional suffix added to the filename.").option("--include-versions", "Include archived versions in the snapshot. (v0.5: ignored — current chunks only.)").option("--git", "Commit the snapshot to the output dir as a git checkpoint. (v0.5: ignored.)").action(action);
68137
68165
  }
68138
68166
 
68139
68167
  // src/cli/commands/completion.ts
@@ -68170,26 +68198,26 @@ function collectNodes() {
68170
68198
  nodes.sort((a, b) => a.path.localeCompare(b.path));
68171
68199
  return nodes;
68172
68200
  }
68173
- function bashScript(nodes) {
68201
+ function bashScript(nodes, prog, fn) {
68174
68202
  const candCases = nodes.map((n) => ` "${n.path}") echo "${n.candidates.join(" ")}" ;;`).join(`
68175
68203
  `);
68176
68204
  const pathPatterns = nodes.filter((n) => n.path !== "").map((n) => `"${n.path}"`).join("|");
68177
68205
  return `# Cerefox bash completion. Source from ~/.bashrc:
68178
- # source <(cerefox completion bash)
68206
+ # source <(${prog} completion bash)
68179
68207
  #
68180
- _cerefox_candidates() {
68208
+ _${fn}_candidates() {
68181
68209
  case "$1" in
68182
68210
  ${candCases}
68183
68211
  *) echo "--help" ;;
68184
68212
  esac
68185
68213
  }
68186
- _cerefox_is_path() {
68214
+ _${fn}_is_path() {
68187
68215
  case "$1" in
68188
68216
  ${pathPatterns}) return 0 ;;
68189
68217
  *) return 1 ;;
68190
68218
  esac
68191
68219
  }
68192
- _cerefox_completion() {
68220
+ _${fn}_completion() {
68193
68221
  local cur path trial w i
68194
68222
  COMPREPLY=()
68195
68223
  cur="\${COMP_WORDS[COMP_CWORD]}"
@@ -68199,39 +68227,39 @@ _cerefox_completion() {
68199
68227
  w="\${COMP_WORDS[$i]}"
68200
68228
  case "$w" in -*) break ;; esac
68201
68229
  if [ -z "$path" ]; then trial="$w"; else trial="$path $w"; fi
68202
- if _cerefox_is_path "$trial"; then
68230
+ if _${fn}_is_path "$trial"; then
68203
68231
  path="$trial"; i=$((i + 1))
68204
68232
  else
68205
68233
  break
68206
68234
  fi
68207
68235
  done
68208
- COMPREPLY=( $(compgen -W "$(_cerefox_candidates "$path")" -- "$cur") )
68236
+ COMPREPLY=( $(compgen -W "$(_${fn}_candidates "$path")" -- "$cur") )
68209
68237
  return 0
68210
68238
  }
68211
- complete -F _cerefox_completion cerefox
68239
+ complete -F _${fn}_completion ${prog}
68212
68240
  `;
68213
68241
  }
68214
- function zshScript(nodes) {
68242
+ function zshScript(nodes, prog, fn) {
68215
68243
  const candCases = nodes.map((n) => ` "${n.path}") REPLY="${n.candidates.join(" ")}" ;;`).join(`
68216
68244
  `);
68217
68245
  const pathPatterns = nodes.filter((n) => n.path !== "").map((n) => `"${n.path}"`).join("|");
68218
- return `#compdef cerefox
68246
+ return `#compdef ${prog}
68219
68247
  # Cerefox zsh completion. Save and source from ~/.zshrc:
68220
- # source <(cerefox completion zsh)
68248
+ # source <(${prog} completion zsh)
68221
68249
  #
68222
- _cerefox_candidates() {
68250
+ _${fn}_candidates() {
68223
68251
  case "$1" in
68224
68252
  ${candCases}
68225
68253
  *) REPLY="--help" ;;
68226
68254
  esac
68227
68255
  }
68228
- _cerefox_is_path() {
68256
+ _${fn}_is_path() {
68229
68257
  case "$1" in
68230
68258
  ${pathPatterns}) return 0 ;;
68231
68259
  *) return 1 ;;
68232
68260
  esac
68233
68261
  }
68234
- _cerefox() {
68262
+ _${fn}() {
68235
68263
  local path trial w i REPLY
68236
68264
  path=""
68237
68265
  i=2
@@ -68239,13 +68267,13 @@ _cerefox() {
68239
68267
  w="\${words[i]}"
68240
68268
  case "$w" in -*) break ;; esac
68241
68269
  if [[ -z "$path" ]]; then trial="$w"; else trial="$path $w"; fi
68242
- if _cerefox_is_path "$trial"; then
68270
+ if _${fn}_is_path "$trial"; then
68243
68271
  path="$trial"; (( i++ ))
68244
68272
  else
68245
68273
  break
68246
68274
  fi
68247
68275
  done
68248
- _cerefox_candidates "$path"
68276
+ _${fn}_candidates "$path"
68249
68277
  compadd -- \${=REPLY}
68250
68278
  }
68251
68279
  # Self-bootstrap the completion system if no \`compinit\` has run yet (e.g. this
@@ -68254,23 +68282,23 @@ _cerefox() {
68254
68282
  if ! whence compdef >/dev/null 2>&1; then
68255
68283
  autoload -Uz compinit && compinit
68256
68284
  fi
68257
- compdef _cerefox cerefox
68285
+ compdef _${fn} ${prog}
68258
68286
  `;
68259
68287
  }
68260
- function fishScript(nodes) {
68288
+ function fishScript(nodes, prog, fn) {
68261
68289
  const candCases = nodes.map((n) => ` case "${n.path}"
68262
68290
  echo "${n.candidates.join(" ")}"`).join(`
68263
68291
  `);
68264
68292
  const pathList = nodes.filter((n) => n.path !== "").map((n) => `"${n.path}"`).join(" ");
68265
- return `# Cerefox fish completion. Save to ~/.config/fish/completions/cerefox.fish
68266
- function __cerefox_candidates
68293
+ return `# Cerefox fish completion. Save to ~/.config/fish/completions/${prog}.fish
68294
+ function __${fn}_candidates
68267
68295
  switch "$argv[1]"
68268
68296
  ${candCases}
68269
68297
  case '*'
68270
68298
  echo "--help"
68271
68299
  end
68272
68300
  end
68273
- function __cerefox_is_path
68301
+ function __${fn}_is_path
68274
68302
  for p in ${pathList}
68275
68303
  if test "$argv[1]" = "$p"
68276
68304
  return 0
@@ -68278,7 +68306,7 @@ function __cerefox_is_path
68278
68306
  end
68279
68307
  return 1
68280
68308
  end
68281
- function __cerefox_complete
68309
+ function __${fn}_complete
68282
68310
  set -l tokens (commandline -opc)
68283
68311
  set -l path ""
68284
68312
  set -l i 2
@@ -68293,27 +68321,35 @@ function __cerefox_complete
68293
68321
  else
68294
68322
  set trial "$path $w"
68295
68323
  end
68296
- if __cerefox_is_path "$trial"
68324
+ if __${fn}_is_path "$trial"
68297
68325
  set path "$trial"
68298
68326
  set i (math $i + 1)
68299
68327
  else
68300
68328
  break
68301
68329
  end
68302
68330
  end
68303
- string split ' ' -- (__cerefox_candidates "$path")
68331
+ string split ' ' -- (__${fn}_candidates "$path")
68304
68332
  end
68305
- complete -c cerefox -f -a '(__cerefox_complete)'
68333
+ complete -c ${prog} -f -a '(__${fn}_complete)'
68306
68334
  `;
68307
68335
  }
68336
+ function progName() {
68337
+ return buildProgram().name();
68338
+ }
68339
+ function fnId(prog) {
68340
+ return prog.replace(/[^a-zA-Z0-9]/g, "_");
68341
+ }
68308
68342
  function scriptFor(shell) {
68309
68343
  const nodes = collectNodes();
68344
+ const prog = progName();
68345
+ const fn = fnId(prog);
68310
68346
  switch (shell) {
68311
68347
  case "bash":
68312
- return bashScript(nodes);
68348
+ return bashScript(nodes, prog, fn);
68313
68349
  case "zsh":
68314
- return zshScript(nodes);
68350
+ return zshScript(nodes, prog, fn);
68315
68351
  case "fish":
68316
- return fishScript(nodes);
68352
+ return fishScript(nodes, prog, fn);
68317
68353
  }
68318
68354
  }
68319
68355
  function detectShell() {
@@ -68322,8 +68358,12 @@ function detectShell() {
68322
68358
  return sh;
68323
68359
  return null;
68324
68360
  }
68325
- var RC_BEGIN = "# >>> cerefox shell completion (managed by `cerefox completion install`) >>>";
68326
- var RC_END = "# <<< cerefox shell completion <<<";
68361
+ function rcBeginFor(prog) {
68362
+ return `# >>> ${prog} shell completion (managed by \`${prog} completion install\`) >>>`;
68363
+ }
68364
+ function rcEndFor(prog) {
68365
+ return `# <<< ${prog} shell completion <<<`;
68366
+ }
68327
68367
  async function installMode(options) {
68328
68368
  const shell = options.shell ?? detectShell();
68329
68369
  if (!shell) {
@@ -68333,21 +68373,24 @@ async function installMode(options) {
68333
68373
  throw userError(`Unsupported --shell "${shell}". Use bash, zsh, or fish.`);
68334
68374
  }
68335
68375
  const home = homedir3();
68336
- const scriptPath = join3(home, `.cerefox-completion.${shell}`);
68376
+ const prog = progName();
68377
+ const rcBegin = rcBeginFor(prog);
68378
+ const rcEnd = rcEndFor(prog);
68379
+ const scriptPath = join3(home, `.${prog}-completion.${shell}`);
68337
68380
  writeFileSync2(scriptPath, scriptFor(shell), "utf8");
68338
68381
  println(c.green(`✓ Wrote completion script: ${scriptPath}`));
68339
68382
  if (shell === "fish") {
68340
- println(c.dim(" For fish, also copy it into ~/.config/fish/completions/cerefox.fish (or `source` it)."));
68383
+ println(c.dim(` For fish, also copy it into ~/.config/fish/completions/${prog}.fish (or \`source\` it).`));
68341
68384
  return;
68342
68385
  }
68343
68386
  const rcPath = join3(home, shell === "zsh" ? ".zshrc" : ".bashrc");
68344
68387
  const sourceLine = `[ -s "${scriptPath}" ] && source "${scriptPath}"`;
68345
- const block = `${RC_BEGIN}
68388
+ const block = `${rcBegin}
68346
68389
  ${sourceLine}
68347
- ${RC_END}
68390
+ ${rcEnd}
68348
68391
  `;
68349
68392
  const existing = existsSync3(rcPath) ? readFileSync3(rcPath, "utf8") : "";
68350
- if (existing.includes(RC_BEGIN)) {
68393
+ if (existing.includes(rcBegin)) {
68351
68394
  println(c.dim(` ${rcPath} already sources the completion (left as-is).`));
68352
68395
  } else {
68353
68396
  const interactive = process.stdout.isTTY && !options.yes;
@@ -69576,6 +69619,12 @@ function defaultCerefoxEntry() {
69576
69619
  args: ["-y", "--package=@cerefox/memory", "cerefox", "mcp"]
69577
69620
  };
69578
69621
  }
69622
+ function localCerefoxEntry() {
69623
+ return {
69624
+ command: process.env.CEREFOX_LOCAL_CMD || "cerefox-local",
69625
+ args: ["mcp"]
69626
+ };
69627
+ }
69579
69628
  function claudeCodeUserConfigPath() {
69580
69629
  return join4(homedir4(), ".claude.json");
69581
69630
  }
@@ -69589,8 +69638,7 @@ function claudeDesktopConfigPath() {
69589
69638
  }
69590
69639
  return join4(home, ".config", "Claude", "claude_desktop_config.json");
69591
69640
  }
69592
- function claudeCodeDelegated() {
69593
- const entry = defaultCerefoxEntry();
69641
+ function claudeCodeDelegated(entry) {
69594
69642
  return {
69595
69643
  cmd: "claude",
69596
69644
  args: ["mcp", "add", "cerefox", "--scope", "user", "--", entry.command, ...entry.args]
@@ -69656,7 +69704,7 @@ function writeMcpConfig(writer, opts = {}) {
69656
69704
  return directWrite(writer, writer.configPath, opts);
69657
69705
  }
69658
69706
  function directWrite(writer, configPath, opts) {
69659
- const entry = writer.buildServerEntry();
69707
+ const entry = opts.entry ?? writer.buildServerEntry();
69660
69708
  const format = writer.format ?? "json";
69661
69709
  if (!opts.dryRun)
69662
69710
  mkdirSync2(dirname(configPath), { recursive: true });
@@ -69697,8 +69745,8 @@ function delegatedWrite(writer, opts) {
69697
69745
  if (!writer.delegated) {
69698
69746
  throw new Error(`${writer.label}: kind=delegated but no delegated() factory`);
69699
69747
  }
69700
- const { cmd, args } = writer.delegated();
69701
- const entry = writer.buildServerEntry();
69748
+ const entry = opts.entry ?? writer.buildServerEntry();
69749
+ const { cmd, args } = writer.delegated(entry);
69702
69750
  const delegatedCommand = `${cmd} ${args.join(" ")}`;
69703
69751
  if (opts.dryRun) {
69704
69752
  return {
@@ -69743,7 +69791,8 @@ function action6(options) {
69743
69791
  const result = writeMcpConfig(writer, {
69744
69792
  customPath: options.configPath,
69745
69793
  noBackup: !options.backup,
69746
- dryRun: options.dryRun
69794
+ dryRun: options.dryRun,
69795
+ entry: options.local ? localCerefoxEntry() : undefined
69747
69796
  });
69748
69797
  if (options.json) {
69749
69798
  printJson(result);
@@ -69787,7 +69836,7 @@ function restartHint(id) {
69787
69836
  }
69788
69837
  }
69789
69838
  function registerConfigureAgent(program2) {
69790
- program2.command("configure-agent").description("Write the MCP server config for a supported client.").requiredOption("-t, --tool <client>", "Target client: claude-code, claude-desktop, cursor, codex, gemini.").option("--config-path <path>", "Override the default config-file path.").option("--no-backup", "Skip the .pre-cerefox.bak backup of any existing config.").option("--dry-run", "Print the planned write without modifying any file.").option("--json", "Emit JSON describing the result.").action(action6);
69839
+ program2.command("configure-agent").description("Write the MCP server config for a supported client.").requiredOption("-t, --tool <client>", "Target client: claude-code, claude-desktop, cursor, codex, gemini.").option("--config-path <path>", "Override the default config-file path.").option("--no-backup", "Skip the .pre-cerefox.bak backup of any existing config.").option("--dry-run", "Print the planned write without modifying any file.").option("--json", "Emit JSON describing the result.").option("--local", "Wire the local/self-hosted backend (`cerefox-local mcp`) instead of npx. Used by `cerefox-local configure-agent`.").action(action6);
69791
69840
  }
69792
69841
 
69793
69842
  // src/cli/commands/delete-doc.ts
@@ -73925,7 +73974,7 @@ import { homedir as homedir5 } from "node:os";
73925
73974
  import { join as join8 } from "node:path";
73926
73975
 
73927
73976
  // ../../_shared/ef-meta/index.ts
73928
- var EF_VERSION = "0.9.3";
73977
+ var EF_VERSION = "0.10.1";
73929
73978
 
73930
73979
  // src/cli/util/checks.ts
73931
73980
  init_config();
@@ -75049,6 +75098,22 @@ var DEFAULT_PIPELINE_SETTINGS = {
75049
75098
  versionRetentionHours: 48,
75050
75099
  versionCleanupEnabled: true
75051
75100
  };
75101
+ function loadPipelineSettings() {
75102
+ const env4 = globalThis.process?.env ?? {};
75103
+ const intMin = (raw, def, min) => {
75104
+ if (raw === undefined || raw === "")
75105
+ return def;
75106
+ const n = Number.parseInt(raw, 10);
75107
+ return Number.isNaN(n) || n < min ? def : n;
75108
+ };
75109
+ const bool = (raw, def) => raw === undefined || raw === "" ? def : !/^(false|0|no|off)$/i.test(raw.trim());
75110
+ return {
75111
+ maxChunkChars: intMin(env4.CEREFOX_MAX_CHUNK_CHARS, DEFAULT_PIPELINE_SETTINGS.maxChunkChars, 1),
75112
+ minChunkChars: intMin(env4.CEREFOX_MIN_CHUNK_CHARS, DEFAULT_PIPELINE_SETTINGS.minChunkChars, 0),
75113
+ versionRetentionHours: intMin(env4.CEREFOX_VERSION_RETENTION_HOURS, DEFAULT_PIPELINE_SETTINGS.versionRetentionHours, 0),
75114
+ versionCleanupEnabled: bool(env4.CEREFOX_VERSION_CLEANUP_ENABLED, DEFAULT_PIPELINE_SETTINGS.versionCleanupEnabled)
75115
+ };
75116
+ }
75052
75117
 
75053
75118
  // src/ingestion/pipeline.ts
75054
75119
  class IngestionPipeline {
@@ -75060,7 +75125,7 @@ class IngestionPipeline {
75060
75125
  this.db = new IngestionDbBridge(deps.supabase);
75061
75126
  this.apiKey = deps.openAiApiKey;
75062
75127
  this.embedderModel = deps.embedderModel ?? "text-embedding-3-small";
75063
- this.settings = { ...DEFAULT_PIPELINE_SETTINGS, ...deps.settings ?? {} };
75128
+ this.settings = { ...loadPipelineSettings(), ...deps.settings ?? {} };
75064
75129
  }
75065
75130
  async ingestText(opts) {
75066
75131
  const {
@@ -76474,8 +76539,8 @@ async function action28(query, options) {
76474
76539
  }
76475
76540
  const matchCount = parsePositiveInt(options.matchCount, "--match-count", 5);
76476
76541
  const alpha = parseFloat01(options.alpha, "--alpha", 0.7);
76477
- const minScore = parseFloat01(options.minScore, "--min-score", 0.5);
76478
- const maxBytes = parseNonNegativeInt(options.maxBytes, "--max-bytes", 200000);
76542
+ const minScore = parseFloat01(options.minScore, "--min-score", getMinSearchScore());
76543
+ const maxBytes = parseNonNegativeInt(options.maxBytes, "--max-bytes", getMaxResponseBytes());
76479
76544
  const mode = options.mode ?? "docs";
76480
76545
  if (!["docs", "hybrid", "fts"].includes(mode)) {
76481
76546
  throw userError(`--mode "${mode}": expected "docs", "hybrid", or "fts".`);
@@ -76620,7 +76685,7 @@ async function action28(query, options) {
76620
76685
  }
76621
76686
  }
76622
76687
  function registerSearch(program2) {
76623
- program2.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: 0.7).", "0.7").option("--min-score <float>", "Minimum cosine similarity threshold.", "0.5").option("--max-bytes <n>", "Response size budget in bytes.", "200000").option("-r, --requestor <name>", "Agent / user name (recorded in usage log).").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(action28);
76688
+ program2.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: 0.7).", "0.7").option("--min-score <float>", "Minimum cosine similarity threshold (default: CEREFOX_MIN_SEARCH_SCORE or 0.5).").option("--max-bytes <n>", "Response size budget in bytes (default: CEREFOX_MAX_RESPONSE_BYTES or 200000).").option("-r, --requestor <name>", "Agent / user name (recorded in usage log).").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(action28);
76624
76689
  }
76625
76690
 
76626
76691
  // src/cli/commands/self-update.ts
@@ -80241,7 +80306,7 @@ async function runSearch(ctx, opts) {
80241
80306
  p_alpha: 0.7,
80242
80307
  p_use_upgrade: false,
80243
80308
  p_project_id: projectId,
80244
- p_min_score: 0
80309
+ p_min_score: getMinSearchScore()
80245
80310
  };
80246
80311
  if (metadataFilter)
80247
80312
  params2.p_metadata_filter = metadataFilter;
@@ -80256,7 +80321,7 @@ async function runSearch(ctx, opts) {
80256
80321
  p_match_count: Math.min(count, 5),
80257
80322
  p_alpha: 0.7,
80258
80323
  p_project_id: projectId,
80259
- p_min_score: 0
80324
+ p_min_score: getMinSearchScore()
80260
80325
  };
80261
80326
  if (metadataFilter)
80262
80327
  params.p_metadata_filter = metadataFilter;
@@ -81939,9 +82004,9 @@ function registerRenameHusks(program2) {
81939
82004
  }
81940
82005
  }
81941
82006
  function buildProgram() {
81942
- const progName = process.env.CEREFOX_PROG_NAME || "cerefox";
81943
- const program2 = new Command(progName).description("Cerefox — user-owned shared memory for AI agents.").version(PKG_VERSION, "-v, --version", `Print the ${progName} version and exit.`).addOption(new Option("--json", "Emit machine-readable JSON on stdout instead of the default human text. " + "Available on read commands; ignored on commands without a JSON shape.").hideHelp()).showHelpAfterError(`(run \`${progName} --help\` for usage)`).enablePositionalOptions().addHelpText("after", `
81944
- Resource groups (run \`${progName} <group> --help\`):
82007
+ const progName2 = process.env.CEREFOX_PROG_NAME || "cerefox";
82008
+ const program2 = new Command(progName2).description("Cerefox — user-owned shared memory for AI agents.").version(PKG_VERSION, "-v, --version", `Print the ${progName2} version and exit.`).addOption(new Option("--json", "Emit machine-readable JSON on stdout instead of the default human text. " + "Available on read commands; ignored on commands without a JSON shape.").hideHelp()).showHelpAfterError(`(run \`${progName2} --help\` for usage)`).enablePositionalOptions().addHelpText("after", `
82009
+ Resource groups (run \`${progName2} <group> --help\`):
81945
82010
  ` + ` document get · list · edit · delete · restore · ingest · ingest-dir · version {list·archive·unarchive}
81946
82011
  ` + ` project list · create · edit · delete
81947
82012
  ` + ` metadata keys · search
@@ -81964,8 +82029,8 @@ Exit codes:
81964
82029
  ` + ` 1 user error 3 not found (document / version / project)
81965
82030
  ` + `
81966
82031
  Learn more:
81967
- ` + ` ${progName} guides list # bundled docs (offline)
81968
- ` + ` ${progName} doctor # diagnose your install
82032
+ ` + ` ${progName2} guides list # bundled docs (offline)
82033
+ ` + ` ${progName2} doctor # diagnose your install
81969
82034
  ` + ` https://github.com/fstamatelopoulos/cerefox
81970
82035
  `);
81971
82036
  registerSearch(program2);
@@ -18,7 +18,7 @@
18
18
  * doesn't touch `supabase/functions/` leaves it alone).
19
19
  */
20
20
 
21
- export const EF_VERSION = "0.9.3";
21
+ export const EF_VERSION = "0.10.1";
22
22
 
23
23
  /**
24
24
  * The 8 peer EFs the cerefox-mcp aggregator probes (excludes cerefox-mcp
@@ -17,25 +17,52 @@ export const OPENAI_EMBEDDING_URL = "https://api.openai.com/v1/embeddings";
17
17
  export const OPENAI_MODEL = "text-embedding-3-small";
18
18
  export const EMBEDDING_DIMENSIONS = 768;
19
19
 
20
+ /**
21
+ * Resolve the OpenAI embedding endpoint/model/dimensions, applying `.env`
22
+ * overrides over the built-in defaults. These were configurable in the Python
23
+ * runtime; the TS migration hardcoded them.
24
+ *
25
+ * ⚠ Overriding the MODEL or DIMENSIONS is a BREAKING change: query vectors must
26
+ * match the stored vectors and the DB column is `vector(768)`. Changing either
27
+ * requires re-embedding the whole corpus (`cerefox server reindex`) and, for a
28
+ * non-768 model, a schema change. `CEREFOX_OPENAI_BASE_URL` (proxy/gateway) is
29
+ * the only safe one to flip on an existing KB.
30
+ *
31
+ * Runtime-agnostic env read; the Deno Edge Function (no host env) keeps the
32
+ * constants — matching the EF's "model config is a constant" design.
33
+ */
34
+ export function openaiEmbeddingConfig(): { url: string; model: string; dimensions: number } {
35
+ const env =
36
+ (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {};
37
+ const base = env.CEREFOX_OPENAI_BASE_URL?.replace(/\/+$/, "");
38
+ const dims = Number.parseInt(env.CEREFOX_OPENAI_EMBEDDING_DIMENSIONS ?? "", 10);
39
+ return {
40
+ url: base ? `${base}/embeddings` : OPENAI_EMBEDDING_URL,
41
+ model: env.CEREFOX_OPENAI_EMBEDDING_MODEL || OPENAI_MODEL,
42
+ dimensions: Number.isNaN(dims) || dims <= 0 ? EMBEDDING_DIMENSIONS : dims,
43
+ };
44
+ }
45
+
20
46
  const EMBEDDING_MAX_RETRIES = 3;
21
47
  const EMBEDDING_INITIAL_BACKOFF_MS = 500; // 500ms → 1s → 2s
22
48
 
23
49
  /** Embed a single string. Used for the query vector in `cerefox_search`. */
24
50
  export async function getEmbedding(text: string, apiKey: string): Promise<number[]> {
25
51
  let lastError: Error | null = null;
52
+ const cfg = openaiEmbeddingConfig();
26
53
 
27
54
  for (let attempt = 0; attempt < EMBEDDING_MAX_RETRIES; attempt++) {
28
55
  try {
29
- const response = await fetch(OPENAI_EMBEDDING_URL, {
56
+ const response = await fetch(cfg.url, {
30
57
  method: "POST",
31
58
  headers: {
32
59
  "Authorization": `Bearer ${apiKey}`,
33
60
  "Content-Type": "application/json",
34
61
  },
35
62
  body: JSON.stringify({
36
- model: OPENAI_MODEL,
63
+ model: cfg.model,
37
64
  input: text,
38
- dimensions: EMBEDDING_DIMENSIONS,
65
+ dimensions: cfg.dimensions,
39
66
  }),
40
67
  });
41
68
 
@@ -93,19 +120,20 @@ async function embedBatchSingleCall(
93
120
  apiKey: string,
94
121
  ): Promise<number[][]> {
95
122
  let lastError: Error | null = null;
123
+ const cfg = openaiEmbeddingConfig();
96
124
 
97
125
  for (let attempt = 0; attempt < EMBEDDING_MAX_RETRIES; attempt++) {
98
126
  try {
99
- const response = await fetch(OPENAI_EMBEDDING_URL, {
127
+ const response = await fetch(cfg.url, {
100
128
  method: "POST",
101
129
  headers: {
102
130
  "Authorization": `Bearer ${apiKey}`,
103
131
  "Content-Type": "application/json",
104
132
  },
105
133
  body: JSON.stringify({
106
- model: OPENAI_MODEL,
134
+ model: cfg.model,
107
135
  input: texts,
108
- dimensions: EMBEDDING_DIMENSIONS,
136
+ dimensions: cfg.dimensions,
109
137
  }),
110
138
  });
111
139
 
@@ -14,10 +14,46 @@
14
14
 
15
15
  import type { MCPSupabaseClient } from "./types.ts";
16
16
 
17
- /** Server-enforced response-size ceiling for MCP results. Agents can request
18
- * smaller budgets via `max_bytes`; values above this are capped. */
17
+ /** Built-in default response-size ceiling for MCP/EF results. */
19
18
  export const MAX_RESPONSE_BYTES = 200_000;
20
19
 
20
+ /**
21
+ * Server-enforced response-size ceiling for MCP/Edge-Function results (agents
22
+ * can request smaller via `max_bytes`; larger is capped). Overridable via
23
+ * `CEREFOX_MAX_RESPONSE_BYTES`. Read by the Python runtime; restored after the
24
+ * TS migration. The web UI + CLI are intentionally unlimited and do not use this.
25
+ * Runtime-agnostic env read (Deno EF safely falls back to the default).
26
+ */
27
+ export function getMaxResponseBytes(): number {
28
+ const raw = (globalThis as { process?: { env?: Record<string, string | undefined> } })
29
+ .process?.env?.CEREFOX_MAX_RESPONSE_BYTES;
30
+ if (raw === undefined || raw === "") return MAX_RESPONSE_BYTES;
31
+ const n = Number.parseInt(raw, 10);
32
+ return Number.isNaN(n) || n <= 0 ? MAX_RESPONSE_BYTES : n;
33
+ }
34
+
35
+ /** Built-in default cosine-similarity floor for hybrid/semantic search. */
36
+ export const DEFAULT_MIN_SEARCH_SCORE = 0.5;
37
+
38
+ /**
39
+ * Resolve the minimum cosine-similarity floor for hybrid/semantic search
40
+ * (vector-only matches below this are dropped; FTS matches always pass).
41
+ * Overridable via the `CEREFOX_MIN_SEARCH_SCORE` env var (0.0–1.0). The Python
42
+ * runtime read this; the TS migration dropped it — restored here as the single
43
+ * default used by the CLI, local/remote MCP, and the web API.
44
+ *
45
+ * Runtime-agnostic env read: works in Node/Bun; in the Deno Edge Function
46
+ * `process` may be absent, so it falls back to the built-in default (the cloud
47
+ * EF path doesn't use the host `.env` anyway).
48
+ */
49
+ export function getMinSearchScore(): number {
50
+ const raw = (globalThis as { process?: { env?: Record<string, string | undefined> } })
51
+ .process?.env?.CEREFOX_MIN_SEARCH_SCORE;
52
+ if (raw === undefined || raw === "") return DEFAULT_MIN_SEARCH_SCORE;
53
+ const n = Number.parseFloat(raw);
54
+ return Number.isNaN(n) || n < 0 || n > 1 ? DEFAULT_MIN_SEARCH_SCORE : n;
55
+ }
56
+
21
57
  export function applyByteBudget(
22
58
  rows: unknown[],
23
59
  maxBytes: number,
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { MCPSupabaseClient } from "./types.ts";
9
9
 
10
- import { applyByteBudget, logUsage, MAX_RESPONSE_BYTES } from "./_utils.ts";
10
+ import { applyByteBudget, getMaxResponseBytes, logUsage } from "./_utils.ts";
11
11
  import { lookupProjectId } from "./_projects.ts";
12
12
  import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
13
13
 
@@ -39,8 +39,9 @@ async function handler(
39
39
  }
40
40
 
41
41
  // Enforce byte ceiling for content mode
42
+ const ceiling = getMaxResponseBytes();
42
43
  const max_bytes = include_content
43
- ? Math.min(requested_max_bytes ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES)
44
+ ? Math.min(requested_max_bytes ?? ceiling, ceiling)
44
45
  : null;
45
46
 
46
47
  const params: Record<string, unknown> = {
@@ -18,7 +18,7 @@
18
18
  import type { MCPSupabaseClient } from "./types.ts";
19
19
 
20
20
  import { getEmbedding } from "../embeddings/index.ts";
21
- import { applyByteBudget, logUsage, MAX_RESPONSE_BYTES } from "./_utils.ts";
21
+ import { applyByteBudget, getMaxResponseBytes, getMinSearchScore, logUsage } from "./_utils.ts";
22
22
  import { lookupProjectId } from "./_projects.ts";
23
23
  import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
24
24
 
@@ -32,12 +32,13 @@ async function handler(
32
32
  const match_count = (args.match_count as number | undefined) ?? 5;
33
33
  const mode = (args.mode as string | undefined) ?? "docs";
34
34
  const alpha = (args.alpha as number | undefined) ?? 0.7;
35
- const min_score = (args.min_score as number | undefined) ?? 0.5;
35
+ const min_score = (args.min_score as number | undefined) ?? getMinSearchScore();
36
36
  const metadata_filter =
37
37
  (args.metadata_filter as Record<string, string> | null | undefined) ?? null;
38
38
  const requested_max_bytes = args.max_bytes as number | undefined;
39
39
 
40
- const max_bytes = Math.min(requested_max_bytes ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES);
40
+ const ceiling = getMaxResponseBytes();
41
+ const max_bytes = Math.min(requested_max_bytes ?? ceiling, ceiling);
41
42
 
42
43
  if (
43
44
  metadata_filter !== null &&
@@ -147,6 +147,32 @@ operations.
147
147
 
148
148
  ---
149
149
 
150
+ ## Local / self-hosted (World B) — a different access model
151
+
152
+ Everything above describes the **cloud / Supabase** deployment. The **local / self-hosted**
153
+ backend ([`setup-local.md`](setup-local.md)) runs Postgres + PostgREST + the Cerefox server
154
+ in one Docker container, and its access model is deliberately simpler:
155
+
156
+ - **No Layer 1 (Edge Functions) and no anon-JWT.** There are no Edge Functions; the
157
+ `cerefox-server` inside the container exposes `/rest/v1` (a reverse-proxy to the in-container
158
+ PostgREST) plus `/app` + `/api/v1`. Remote agents over HTTP are not a goal of World B.
159
+ - **The access token never leaves the container.** db-init self-generates the PostgREST JWT
160
+ secret on boot and mints a `service_role` token into the container's runtime env. The web
161
+ UI (served by the container) and the in-container CLI/MCP read it internally — nothing on
162
+ the host holds it.
163
+ - **Agents use stdio over `docker exec`, not a network credential.** `cerefox-local mcp`
164
+ runs `cerefox mcp` inside the container via `docker exec -i`; the MCP client launches that
165
+ as a local subprocess. No URL, no bearer token in the client config.
166
+ - **The only host-side secret is `OPENAI_API_KEY`** (in `~/.cerefox/local/.env`), used for
167
+ embeddings — the same as every deployment.
168
+ - By default the container publishes on **`127.0.0.1`** (loopback only); set
169
+ `CEREFOX_LOCAL_BIND=0.0.0.0` to expose it on the LAN.
170
+
171
+ So Layers 1–3 below apply to the cloud deployment; the local backend collapses them into a
172
+ single container with an internally-held token.
173
+
174
+ ---
175
+
150
176
  ## Summary
151
177
 
152
178
  | Caller | Transport | Auth credential | Typical use |
@@ -23,6 +23,8 @@ Within a single runtime (e.g., Claude Code's agent teams feature, or a LangGraph
23
23
 
24
24
  Cerefox sits in a unique position: it is vendor-neutral, protocol-native (MCP + REST), and designed for persistent storage. Any agent that can make an HTTP call can read and write to Cerefox.
25
25
 
26
+ > **Cross-machine coordination assumes the cloud / Supabase backend** (a shared network endpoint). The default **local / self-hosted (Docker) backend is single-machine** — it binds `127.0.0.1` and agents reach it via `cerefox-local mcp` (stdio). To coordinate agents across machines with a local backend, expose it on your LAN (`CEREFOX_LOCAL_BIND=0.0.0.0`) or use the cloud backend.
27
+
26
28
  The coordination model is **asynchronous and knowledge-based**:
27
29
 
28
30
  1. **Agent A writes** a finding, decision, or task breakdown to Cerefox. It does not need to know which agent will consume it.
@@ -4,17 +4,23 @@ Comprehensive reference for every `cerefox` subcommand. For tutorials and walkth
4
4
 
5
5
  > `--help` is canonical. If anything in this document disagrees with `cerefox <subcommand> --help`, trust `--help` and file an issue against this guide.
6
6
 
7
+ > **Local / self-hosted (Docker) backend?** Every KB verb below is identical, but you run it
8
+ > as **`cerefox-local <verb>`** (it proxies into the container via `docker exec`); lifecycle
9
+ > is different (`cerefox-local init/start/stop/upgrade/uninstall/status/logs/configure-agent`).
10
+ > A local user sets **only `OPENAI_API_KEY`** — the Supabase/database vars below do **not**
11
+ > apply (the container owns them). See [`setup-local.md`](setup-local.md).
12
+
7
13
  ## Setup
8
14
 
9
- Every command reads configuration from `.env` in the working directory (or environment variables — see [`configuration.md`](configuration.md)). Required at minimum:
15
+ This section is for the **cloud / Supabase** backend. Every command reads configuration from `.env` in the working directory (or environment variables — see [`configuration.md`](configuration.md)). Required at minimum:
10
16
 
11
17
  - `CEREFOX_SUPABASE_URL` and `CEREFOX_SUPABASE_KEY` for any command that talks to Supabase
12
- - `OPENAI_API_KEY` (or `CEREFOX_FIREWORKS_API_KEY`) for any command that embeds (ingest, search)
18
+ - `OPENAI_API_KEY` for any command that embeds (ingest, search)
13
19
  - `CEREFOX_DATABASE_URL` for `cerefox server deploy` and the contributor scripts (`bun scripts/db_*.ts`)
14
20
 
15
21
  The CLI is the TypeScript `@cerefox/memory` package. Invoke any command as plain `cerefox <subcommand>` (installed via the installer or `npm install -g @cerefox/memory` — see [`quickstart.md`](quickstart.md#1-install)).
16
22
 
17
- > **v0.9 verb rename**: commands now follow a `resource verb` shape (e.g. `cerefox document get`, `cerefox project list`). The old flat verbs (`get-doc`, `list-docs`, `ingest`, `list-versions`, `config-get`, `deploy-server`, `docs`, …) are husks and have been removed — use the new forms below.
23
+ > **v0.9 verb rename**: commands now follow a `resource verb` shape (e.g. `cerefox document get`, `cerefox project list`). The old flat verbs (`get-doc`, `list-docs`, `ingest`, `list-versions`, `config-get`, `deploy-server`, `docs`, …) survive as hidden husks — they still run but print a pointer to the new form and exit non-zero (removed only at v1.0). Use the new forms below.
18
24
 
19
25
  ## Commands
20
26
 
@@ -86,7 +92,7 @@ Walks `DIRECTORY` **recursively** (always — there is no recurse toggle) and in
86
92
 
87
93
  | Flag | Type | Default | Description |
88
94
  |---|---|---|---|
89
- | `--extensions <list>` (`-e`) | comma list | `.md,.txt` | File extensions to ingest, e.g. `--extensions .md`. |
95
+ | `--extensions <list>` (`-e`) | comma list | `.md,.txt` | File extensions to ingest, e.g. `--extensions .md`. `.docx` is **not** in the default set — opt in with `--extensions .md,.txt,.docx` (converted via mammoth, same as single-file ingest). |
90
96
  | `--project-name <name>` (`-p`) | str | _none_ | Project to assign every document to. |
91
97
  | `--update-if-exists` (`-u`) | flag | off | Update existing documents by source path / title. |
92
98
  | `--metadata <json>` (`-m`) | JSON | `{}` | JSON metadata applied to every file in the run. |
@@ -141,7 +147,7 @@ cerefox search [OPTIONS] QUERY
141
147
  ```bash
142
148
  cerefox search "OAuth design"
143
149
  cerefox search "decisions" --metadata-filter '{"type":"decision-log"}' --match-count 5
144
- cerefox search "what we tried" --mode semantic --requestor "claude-code"
150
+ cerefox search "what we tried" --mode hybrid --requestor "claude-code"
145
151
  cerefox search "design docs" --only-metadata
146
152
  ```
147
153
 
@@ -24,6 +24,13 @@ CEREFOX_CONFIG_DIR=~/.cerefox-personal cerefox search "…"
24
24
 
25
25
  Full rule documented in [`docs/specs/polish-and-distribution-design.md` §7](../specs/polish-and-distribution-design.md).
26
26
 
27
+ > **Local / self-hosted (World B).** For the Docker backend (`cerefox-local`), do **not**
28
+ > set the Supabase or `CEREFOX_DATABASE_URL` vars below — the container generates and owns
29
+ > them. Put `OPENAI_API_KEY` plus any of the `CEREFOX_*` **tuning** options on this page
30
+ > (search, chunking, retrieval, versioning, embedding base-url/model, caller identity) in
31
+ > `~/.cerefox/local/.env`; the installer + `cerefox-local` forward them into the container.
32
+ > Apply changes with `cerefox-local init`. See [`setup-local.md`](setup-local.md).
33
+
27
34
  ---
28
35
 
29
36
  ## Supabase / Database
@@ -45,34 +52,27 @@ Full rule documented in [`docs/specs/polish-and-distribution-design.md` §7](../
45
52
 
46
53
  Cerefox uses cloud-based embedding APIs. Local models (mpnet, Ollama) are not supported — they require large downloads, fail on some hardware, and add installation complexity.
47
54
 
48
- | Variable | Default | Description |
49
- |----------|---------|-------------|
50
- | `CEREFOX_EMBEDDER` | `openai` | Embedding provider. Valid values: `openai`, `fireworks` |
55
+ > **TS runtime: OpenAI only (today).** The current TypeScript runtime implements the
56
+ > OpenAI embedder. `CEREFOX_EMBEDDER` and the `CEREFOX_FIREWORKS_*` variables are
57
+ > documented (they worked in the retired Python runtime) but are **not yet wired in TS** —
58
+ > they're currently no-ops, tracked for a future release.
51
59
 
52
60
  ### OpenAI (default, recommended)
53
61
 
54
62
  | Variable | Default | Description |
55
63
  |----------|---------|-------------|
56
64
  | `OPENAI_API_KEY` | `""` | OpenAI API key. Also accepted as `CEREFOX_OPENAI_API_KEY`. Get one at [platform.openai.com/api-keys](https://platform.openai.com/api-keys). |
57
- | `CEREFOX_OPENAI_BASE_URL` | `https://api.openai.com/v1` | API base URL. Override for proxies or OpenAI-compatible providers. |
58
- | `CEREFOX_OPENAI_EMBEDDING_MODEL` | `text-embedding-3-small` | OpenAI embedding model. |
59
- | `CEREFOX_OPENAI_EMBEDDING_DIMENSIONS` | `768` | Output dimensions. Must match the database schema (VECTOR(768)). |
60
-
61
- For cost estimates see `docs/guides/operational-cost.md`.
65
+ | `CEREFOX_OPENAI_BASE_URL` | `https://api.openai.com/v1` | API base URL. Safe to override for proxies or OpenAI-compatible gateways. |
66
+ | `CEREFOX_OPENAI_EMBEDDING_MODEL` | `text-embedding-3-small` | OpenAI embedding model. ⚠ see warning below. |
67
+ | `CEREFOX_OPENAI_EMBEDDING_DIMENSIONS` | `768` | Output dimensions. Must match the DB schema (`VECTOR(768)`). ⚠ see warning below. |
62
68
 
63
- ### Fireworks AI (alternative, lower cost)
69
+ > **⚠ Changing the model or dimensions is breaking.** Query vectors must match the stored
70
+ > vectors. After changing `CEREFOX_OPENAI_EMBEDDING_MODEL` you MUST re-embed the whole
71
+ > corpus (`cerefox server reindex`); changing `CEREFOX_OPENAI_EMBEDDING_DIMENSIONS` away
72
+ > from 768 also requires a schema change. `CEREFOX_OPENAI_BASE_URL` is the only one safe to
73
+ > flip on an existing knowledge base.
64
74
 
65
- | Variable | Default | Description |
66
- |----------|---------|-------------|
67
- | `CEREFOX_FIREWORKS_API_KEY` | `""` | Fireworks AI API key. |
68
- | `CEREFOX_FIREWORKS_BASE_URL` | `https://api.fireworks.ai/inference/v1` | Fireworks API base URL. |
69
- | `CEREFOX_FIREWORKS_EMBEDDING_MODEL` | `nomic-ai/nomic-embed-text-v1.5` | Fireworks model. Must natively output 768-dim vectors. |
70
-
71
- To use Fireworks:
72
- ```env
73
- CEREFOX_EMBEDDER=fireworks
74
- CEREFOX_FIREWORKS_API_KEY=fw_...
75
- ```
75
+ For cost estimates see `docs/guides/operational-cost.md`.
76
76
 
77
77
  ### Edge Functions (for agents)
78
78
 
@@ -250,16 +250,9 @@ OPENAI_API_KEY=sk-...
250
250
  # All other settings use defaults
251
251
  ```
252
252
 
253
- ## Example: Fireworks Embedder `.env`
254
-
255
- ```bash
256
- CEREFOX_SUPABASE_URL=https://abcdefghijkl.supabase.co
257
- CEREFOX_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIs...
258
- CEREFOX_DATABASE_URL=postgresql://...
259
-
260
- CEREFOX_EMBEDDER=fireworks
261
- CEREFOX_FIREWORKS_API_KEY=fw_...
262
- ```
253
+ > **Fireworks is not wired in the TS runtime yet** — `CEREFOX_EMBEDDER=fireworks` /
254
+ > `CEREFOX_FIREWORKS_*` are documented for the retired Python runtime but are currently
255
+ > no-ops (OpenAI is the only embedder implemented today). Tracked for a future release.
263
256
 
264
257
  ---
265
258
 
@@ -59,6 +59,20 @@ Three top-level paths plus a few special cases:
59
59
  > available in [`examples/mcp-configs/`](../examples/mcp-configs/). Pick the one for your
60
60
  > client, replace the placeholders, and you're connected.
61
61
 
62
+ ### Local / self-hosted (World B)
63
+
64
+ If you run the **Docker backend** ([`setup-local.md`](setup-local.md)) instead of cloud, the
65
+ MCP path is different: the server runs **inside the container**, launched per session over
66
+ `docker exec`. There's no URL or bearer token in the client config — the access token stays
67
+ in the container.
68
+
69
+ - **Easiest:** `cerefox-local configure-agent` wires it up (registers an MCP server named
70
+ `cerefox-local` with Claude Code if the `claude` CLI is present, else prints the snippet).
71
+ - **Manual:** point the client at `command: cerefox-local, args: ["mcp"]` (stdio). That proxies
72
+ to `cerefox mcp` in the container; the same 10 tools, identical behavior to every other path.
73
+ - The cloud paths above (remote Edge Function, GPT Actions) **do not apply** to a local-only
74
+ install — there are no Edge Functions.
75
+
62
76
  ---
63
77
 
64
78
  ## Prerequisites
@@ -116,12 +116,34 @@ These limits comfortably cover personal-use traffic. Check
116
116
 
117
117
  ---
118
118
 
119
+ ## Scenario C — Fully local / self-hosted (Docker)
120
+
121
+ The whole backend runs in one Docker container on your machine — Postgres + pgvector +
122
+ the Cerefox server — with **no Supabase and no Edge Functions** ([`setup-local.md`](setup-local.md)).
123
+
124
+ ```
125
+ Your machine
126
+ └── Docker container (free)
127
+ └── Postgres + pgvector + cerefox web/MCP
128
+ Embeddings via OpenAI API (pay-per-use)
129
+ ```
130
+
131
+ **Typical cost for personal use**: just the **OpenAI embedding spend** (the same pay-per-use
132
+ as every scenario — fractions of a cent per document) plus local compute/electricity. There
133
+ is **no Supabase tier and no Edge-Function invocation limit** to worry about — the binding
134
+ free-tier constraint from Scenarios A/B (500K EF calls/month) simply doesn't exist here, and
135
+ agents using the local MCP server make zero billable cloud calls. The only ongoing cost is
136
+ embeddings (and only when you ingest or run semantic/hybrid search) — see "Controlling
137
+ embedding costs" below.
138
+
139
+ ---
140
+
119
141
  ## Controlling embedding costs
120
142
 
121
143
  If you want to keep costs as low as possible:
122
144
 
123
- - **Fireworks AI** is an OpenAI-compatible alternative that offers competitive embedding prices.
124
- See `docs/guides/configuration.md` for how to switch.
145
+ - **Cheaper embedding models**: a lower-cost OpenAI-compatible provider (e.g. Fireworks AI)
146
+ is on the roadmap — **not yet wired in the TS runtime** (OpenAI only today).
125
147
  - **Batch ingest, don't re-ingest**: Cerefox deduplicates by content hash — re-ingesting the
126
148
  same file twice costs nothing. Only new or changed content triggers embedding calls.
127
149
  - **`cerefox server reindex`**: Re-embeds all existing chunks if you switch embedders. Run this once
@@ -37,7 +37,7 @@ jobs / CI / make targets that invoke them.
37
37
 
38
38
  ### TS scripts and `.env` resolution
39
39
 
40
- `bun scripts/<name>.ts` reads the same `.env` the Python CLI does. Precedence:
40
+ `bun scripts/<name>.ts` reads the same `.env` the `cerefox` CLI does. Precedence:
41
41
 
42
42
  1. `CEREFOX_CONFIG_DIR` env var (explicit override; supports `~`).
43
43
  2. `./.env` in the current working directory (dev mode).
@@ -1,9 +1,9 @@
1
1
  # Quickstart -- Zero to First Document
2
2
 
3
- Get Cerefox running on your machine via the npm install path. **No source
4
- clone, no Python required.** Once you have a Supabase project (the one
5
- prerequisite — provisioning a free one takes a few minutes), the Cerefox
6
- install and setup below is about 5 minutes.
3
+ Get Cerefox running on your machine via the npm install path (the **cloud /
4
+ Supabase** backend). **No source clone required.** Once you have a Supabase
5
+ project (the one prerequisite — provisioning a free one takes a few minutes),
6
+ the Cerefox install and setup below takes ~15 minutes.
7
7
 
8
8
  > **Upgrading from an earlier version?** See [`upgrading.md`](upgrading.md)
9
9
  > for migration steps instead.
@@ -114,8 +114,9 @@ You should see results from the bundled self-docs.
114
114
  The path above is for **end users** (no clone). If you want to hack on Cerefox,
115
115
  clone the repo, run `bun install`, and use the contributor scripts
116
116
  (`bun scripts/db_deploy.ts`, `bun scripts/db_migrate.ts`). `uv` is only needed
117
- for the legacy Python MCP fallback. See [`setup-local.md`](setup-local.md) and
118
- `CONTRIBUTING.md`.
117
+ for the legacy Python MCP fallback. See [`CONTRIBUTING.md`](../../CONTRIBUTING.md).
118
+ (Want a no-cloud install instead? That's the self-hosted Docker backend —
119
+ [`setup-local.md`](setup-local.md).)
119
120
 
120
121
  ---
121
122
 
@@ -125,7 +126,7 @@ for the legacy Python MCP fallback. See [`setup-local.md`](setup-local.md) and
125
126
  `cerefox document ingest-dir ./notes/` (recurses into sub-directories automatically)
126
127
  - **Search from the CLI**: `cerefox search "your query"`
127
128
  - **Discover all commands**: `cerefox --help`
128
- - **Run the web UI**: `cerefox web` (TypeScript — Hono backend + React SPA); see [`setup-local.md`](setup-local.md)
129
+ - **Run the web UI**: `cerefox web` (TypeScript — Hono backend + React SPA); see [`cli.md`](cli.md)
129
130
  - **Connect more AI clients** (Cursor, Codex, ChatGPT GPT Actions, etc.):
130
131
  [`connect-agents.md`](connect-agents.md)
131
132
  - **Configuration reference**: [`configuration.md`](configuration.md)
@@ -36,11 +36,13 @@ That's it. No Node, Bun, Postgres, or repo clone needed.
36
36
  ## Step 1 — Install
37
37
 
38
38
  ```bash
39
- OPENAI_API_KEY=sk-... sh -c "$(curl -fsSL https://github.com/fstamatelopoulos/cerefox/releases/latest/download/install-local.sh)"
39
+ curl -fsSL https://github.com/fstamatelopoulos/cerefox/releases/latest/download/install-local.sh | sh
40
40
  ```
41
41
 
42
42
  This pulls the published multi-arch image (`amd64` + `arm64`), starts the container, and
43
- installs a `cerefox-local` command (symlinked into `~/.local/bin`). Pick a different port
43
+ installs a `cerefox-local` command (symlinked into `~/.local/bin`). To set your OpenAI key
44
+ inline at install instead of via `cerefox-local init` (Step 2), use the command-substitution
45
+ form: `OPENAI_API_KEY=sk-... sh -c "$(curl -fsSL …/install-local.sh)"`. Pick a specific port
44
46
  with `PORT=8017 …`.
45
47
 
46
48
  > If the installer warns that `~/.local/bin` isn't on your `PATH`, add it:
@@ -48,7 +50,12 @@ with `PORT=8017 …`.
48
50
  > echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc
49
51
  > ```
50
52
 
51
- The web UI is now at **http://localhost:8000/app/** (or your chosen port).
53
+ The web UI is now at **http://localhost:8000/app/** — **or the port the installer chose**
54
+ (it auto-steps to 8010/8020/… if 8000 is busy or you also run the cloud `cerefox web`, which
55
+ defaults to 8000). The installer prints the actual URL; `cerefox-local status` shows it too.
56
+
57
+ The installer also wires **shell tab-completion** for `cerefox-local` (best-effort) — run
58
+ `exec $SHELL` or open a new terminal to activate it.
52
59
 
53
60
  **How the credential works:** the container generates its own JWT secret on first boot and
54
61
  mints the access token internally — the token never leaves the container. The only secret
@@ -47,7 +47,7 @@ You need three values from Supabase: a URL, an API key, and a direct Postgres co
47
47
 
48
48
  See the **[Supabase API keys (2026)](#supabase-api-keys-2026)** section near the end of this guide for the full picture. The short version:
49
49
 
50
- - For `CEREFOX_SUPABASE_KEY` (this guide, Python web app, CLI): use the new **secret key** (`sb_secret_…`) from **Project Settings → API Keys → Secret key**. The legacy `service_role` JWT also still works during the transition.
50
+ - For `CEREFOX_SUPABASE_KEY` (this guide, the web UI, and the CLI): use the new **secret key** (`sb_secret_…`) from **Project Settings → API Keys → Secret key**. The legacy `service_role` JWT also still works during the transition.
51
51
  - For `CEREFOX_SUPABASE_ANON_KEY` (only if you'll use Edge Functions / MCP / GPT Actions; not needed for this guide's deployment step): you must use the **legacy anon JWT** (`eyJ…`). The new `sb_publishable_…` key fails at the Edge Function gateway. See the reference section for why.
52
52
 
53
53
  Either way: keep this key secret — it bypasses Row Level Security and grants full database access.
@@ -10,6 +10,7 @@ to re-run.
10
10
  |---|---|
11
11
  | **Installer / npm** (end user, no repo clone) | `cerefox self-update` (or re-run the [installer](quickstart.md#1-install), or `bun/npm update -g @cerefox/memory`). Then `cerefox server deploy` **if the release notes flag a server-side change**. `cerefox doctor` verifies. |
12
12
  | **Source checkout** (`git clone`, contributor) | `git pull`, then `cerefox server deploy` (or the lower-level `bun scripts/db_*.ts` + `npx supabase functions deploy`). Rebuild the SPA if you run `cerefox web` from source. |
13
+ | **Local / self-hosted (Docker, World B)** | `cerefox-local upgrade` — pulls the new image and recreates the container (data persists in the volume; OpenAI key + tuning overrides preserved). **No separate `server deploy`/`reindex`**: the CLI, web, PostgREST, and schema all ship together in one versioned image, so they can't drift. See [`setup-local.md`](setup-local.md). |
13
14
 
14
15
  > **On an old pre-installer clone (0.1.x)?** The cleanest upgrade is to stop
15
16
  > running from the repo and install the package: follow the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerefox/memory",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Cerefox — user-owned shared memory for AI agents. The local TypeScript runtime: stdio MCP server in v0.4; CLI binary added in v0.5; in-process web server in v0.6; ingestion pipeline in v0.7.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/fstamatelopoulos/cerefox",