From cf5f2b36f9f3b29369b96f218fe17b11d5f8f455 Mon Sep 17 00:00:00 2001 From: Fotis Stamatelopoulos Date: Fri, 5 Jun 2026 16:35:00 -0700 Subject: [PATCH 01/10] fix+sec(local): CLI honors CEREFOX_MAX_RESPONSE_BYTES; bind container to 127.0.0.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLI `--max-bytes` now defaults to getMaxResponseBytes() (CEREFOX_MAX_RESPONSE_BYTES, 200000 fallback) — the CLI does enforce a byte budget, so it should honor the env var. Corrected CLAUDE.md: the budget applies to MCP/EF + CLI; only the web UI is unlimited. - Local container now publishes on 127.0.0.1 by default (loopback) instead of 0.0.0.0, so a single-user backend isn't exposed on the LAN. Opt back in with CEREFOX_LOCAL_BIND=0.0.0.0. install-local.sh + cerefox-local recreate()/init carry it. Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 2 +- docker/local/cerefox-local | 8 ++++++-- docker/local/install-local.sh | 6 +++++- packages/memory/src/cli/commands/search.ts | 6 +++--- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5512a13..ad28b0b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -420,4 +420,4 @@ These live in `docs/guides/` and are written for someone who has never seen the - **Agent guides**: `AGENT_GUIDE.md` (comprehensive reference for AI agents using Cerefox tools), `AGENT_QUICK_REFERENCE.md` (minimal quick reference card -- 8 tools, key rules, workflows) - **Schema**: `src/cerefox/db/schema.sql` - **Config**: `.env` file or environment variables (see `src/cerefox/config.py`) -- **Max response size**: defaults to 200000 bytes (MCP/Edge Function paths only; web UI and CLI are unlimited; configurable via `CEREFOX_MAX_RESPONSE_BYTES`) +- **Max response size**: defaults to 200000 bytes, configurable via `CEREFOX_MAX_RESPONSE_BYTES`. Enforced on the MCP / Edge Function paths **and the CLI** (the CLI also accepts a per-call `--max-bytes`). The **web UI is unlimited** (no byte budget). diff --git a/docker/local/cerefox-local b/docker/local/cerefox-local index e34b1a9..0c984a7 100755 --- a/docker/local/cerefox-local +++ b/docker/local/cerefox-local @@ -51,6 +51,7 @@ recreate() { [ -f "$ENV_FILE" ] || die "no config at $ENV_FILE — run install-local.sh first" # shellcheck disable=SC1090 PORT="$(. "$ENV_FILE"; echo "${CEREFOX_LOCAL_PORT:-8000}")" + BIND_ADDR="$(. "$ENV_FILE"; echo "${CEREFOX_LOCAL_BIND:-127.0.0.1}")" OPENAI_API_KEY="$(. "$ENV_FILE"; echo "${OPENAI_API_KEY:-}")" # Ensure the image is available BEFORE removing the running container, so a failed pull # (offline, or a tag that doesn't exist yet) can't leave us with no container at all. @@ -58,7 +59,7 @@ recreate() { || die "image '$IMAGE' not present locally and pull failed — container left running" docker rm -f "$CONTAINER" >/dev/null 2>&1 || true # shellcheck disable=SC2086 - docker run -d --name "$CONTAINER" -p "$PORT:8000" \ + docker run -d --name "$CONTAINER" -p "$BIND_ADDR:$PORT:8000" \ --restart unless-stopped \ -v "$VOLUME:/var/lib/postgresql/data" \ ${OPENAI_API_KEY:+-e OPENAI_API_KEY=$OPENAI_API_KEY} \ @@ -122,12 +123,14 @@ case "$cmd" in esac done mkdir -p "$CONFIG_DIR"; chmod 700 "$CONFIG_DIR" - cur_key=""; cur_port="8000" + cur_key=""; cur_port="8000"; cur_bind="127.0.0.1" if [ -f "$ENV_FILE" ]; then # shellcheck disable=SC1090 cur_key="$(. "$ENV_FILE"; echo "${OPENAI_API_KEY:-}")" # shellcheck disable=SC1090 cur_port="$(. "$ENV_FILE"; echo "${CEREFOX_LOCAL_PORT:-8000}")" + # shellcheck disable=SC1090 + cur_bind="$(. "$ENV_FILE"; echo "${CEREFOX_LOCAL_BIND:-127.0.0.1}")" fi if [ -z "$key" ] && [ -t 0 ]; then if [ -n "$cur_key" ]; then @@ -153,6 +156,7 @@ case "$cmd" in echo "# Cerefox LOCAL host config. The access token lives in the container, not here;" echo "# only the OpenAI key + port + optional CEREFOX_* tuning overrides are stored." echo "CEREFOX_LOCAL_PORT=$port" + echo "CEREFOX_LOCAL_BIND=$cur_bind" [ -n "$key" ] && echo "OPENAI_API_KEY=$key" [ -n "$preserved" ] && printf '%s' "$preserved" } > "$ENV_FILE" diff --git a/docker/local/install-local.sh b/docker/local/install-local.sh index 0887d5b..99587e9 100755 --- a/docker/local/install-local.sh +++ b/docker/local/install-local.sh @@ -25,6 +25,9 @@ DEFAULT_PORT=8000 # (we may auto-select a free one). Must check before applying the default. if [ -n "${PORT:-}" ]; then PORT_EXPLICIT=true; else PORT_EXPLICIT=false; fi PORT="${PORT:-$DEFAULT_PORT}" +# Bind to loopback by default — a single-user local backend shouldn't be exposed on the +# LAN. Set CEREFOX_LOCAL_BIND=0.0.0.0 to publish on all interfaces (e.g. LAN access). +BIND_ADDR="${CEREFOX_LOCAL_BIND:-127.0.0.1}" CONFIG_DIR="${CEREFOX_LOCAL_CONFIG_DIR:-$HOME/.cerefox/local}" CONTAINER="${CEREFOX_LOCAL_CONTAINER:-cerefox-local}" VOLUME="${CEREFOX_LOCAL_VOLUME:-cerefox_local_pgdata}" @@ -123,7 +126,7 @@ docker rm -f "$CONTAINER" >/dev/null 2>&1 || true # crash (a known GHC-startup segfault) — Docker re-runs the container and the 2nd boot is # clean. It does NOT override a manual `cerefox-local stop`. # shellcheck disable=SC2086 -docker run -d --name "$CONTAINER" -p "$PORT:8000" \ +docker run -d --name "$CONTAINER" -p "$BIND_ADDR:$PORT:8000" \ --restart unless-stopped \ -v "$VOLUME:/var/lib/postgresql/data" \ ${OPENAI_API_KEY:+-e OPENAI_API_KEY=$OPENAI_API_KEY} \ @@ -138,6 +141,7 @@ umask 077 echo "# only the OpenAI key + port + optional CEREFOX_* tuning overrides are stored." echo "# Add overrides (see docs/guides/configuration.md), then: cerefox-local init" echo "CEREFOX_LOCAL_PORT=$PORT" + echo "CEREFOX_LOCAL_BIND=$BIND_ADDR" [ -n "${OPENAI_API_KEY:-}" ] && echo "OPENAI_API_KEY=$OPENAI_API_KEY" [ -n "$PRESERVED_OVERRIDES" ] && printf '%s' "$PRESERVED_OVERRIDES" } > "$CONFIG_DIR/.env" diff --git a/packages/memory/src/cli/commands/search.ts b/packages/memory/src/cli/commands/search.ts index 23684a6..57b13d0 100644 --- a/packages/memory/src/cli/commands/search.ts +++ b/packages/memory/src/cli/commands/search.ts @@ -26,7 +26,7 @@ import { systemError, userError, } from "../../../../../_shared/cli-core/index.ts"; -import { getMinSearchScore } from "../../../../../_shared/mcp-tools/_utils.ts"; +import { getMaxResponseBytes, getMinSearchScore } from "../../../../../_shared/mcp-tools/_utils.ts"; import { getClient } from "../util/client.ts"; import { embedQuery } from "../util/embed.ts"; @@ -77,7 +77,7 @@ async function action( const matchCount = parsePositiveInt(options.matchCount, "--match-count", 5); const alpha = parseFloat01(options.alpha, "--alpha", 0.7); const minScore = parseFloat01(options.minScore, "--min-score", getMinSearchScore()); - const maxBytes = parseNonNegativeInt(options.maxBytes, "--max-bytes", 200_000); + const maxBytes = parseNonNegativeInt(options.maxBytes, "--max-bytes", getMaxResponseBytes()); const mode = options.mode ?? "docs"; if (!["docs", "hybrid", "fts"].includes(mode)) { throw userError(`--mode "${mode}": expected "docs", "hybrid", or "fts".`); @@ -279,7 +279,7 @@ export function registerSearch(program: Command): void { .option("--mode ", "Search mode: docs (default), hybrid, fts.", "docs") .option("--alpha ", "Semantic weight 0..1 (default: 0.7).", "0.7") .option("--min-score ", "Minimum cosine similarity threshold (default: CEREFOX_MIN_SEARCH_SCORE or 0.5).") - .option("--max-bytes ", "Response size budget in bytes.", "200000") + .option("--max-bytes ", "Response size budget in bytes (default: CEREFOX_MAX_RESPONSE_BYTES or 200000).") .option("-r, --requestor ", "Agent / user name (recorded in usage log).") .option("--json", "Emit machine-readable JSON instead of the default text.") .option( From e8d6840237facd0eda3087e3dc371e58a9de28d1 Mon Sep 17 00:00:00 2001 From: Fotis Stamatelopoulos Date: Fri, 5 Jun 2026 16:37:09 -0700 Subject: [PATCH 02/10] docs: World-B (local/self-hosted) coverage across the guides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close the cloud-only gaps the audit flagged: - upgrading.md: `cerefox-local upgrade` row (one versioned image, no server deploy/reindex). - operational-cost.md: Scenario C — fully local (no Supabase tier / EF limit; embeddings only). - access-paths.md: World-B access model (no EF/anon-JWT layer; in-container PostgREST + docker-exec MCP; token never leaves the container; 127.0.0.1 by default). - connect-agents.md: local MCP path via `cerefox-local configure-agent` / `cerefox-local mcp`. Co-Authored-By: Claude Opus 4.7 --- docs/guides/access-paths.md | 26 ++++++++++++++++++++++++++ docs/guides/connect-agents.md | 14 ++++++++++++++ docs/guides/operational-cost.md | 22 ++++++++++++++++++++++ docs/guides/upgrading.md | 1 + 4 files changed, 63 insertions(+) diff --git a/docs/guides/access-paths.md b/docs/guides/access-paths.md index 29f7a16..3289560 100644 --- a/docs/guides/access-paths.md +++ b/docs/guides/access-paths.md @@ -147,6 +147,32 @@ operations. --- +## Local / self-hosted (World B) — a different access model + +Everything above describes the **cloud / Supabase** deployment. The **local / self-hosted** +backend ([`setup-local.md`](setup-local.md)) runs Postgres + PostgREST + the Cerefox server +in one Docker container, and its access model is deliberately simpler: + +- **No Layer 1 (Edge Functions) and no anon-JWT.** There are no Edge Functions; the + `cerefox-server` inside the container exposes `/rest/v1` (a reverse-proxy to the in-container + PostgREST) plus `/app` + `/api/v1`. Remote agents over HTTP are not a goal of World B. +- **The access token never leaves the container.** db-init self-generates the PostgREST JWT + secret on boot and mints a `service_role` token into the container's runtime env. The web + UI (served by the container) and the in-container CLI/MCP read it internally — nothing on + the host holds it. +- **Agents use stdio over `docker exec`, not a network credential.** `cerefox-local mcp` + runs `cerefox mcp` inside the container via `docker exec -i`; the MCP client launches that + as a local subprocess. No URL, no bearer token in the client config. +- **The only host-side secret is `OPENAI_API_KEY`** (in `~/.cerefox/local/.env`), used for + embeddings — the same as every deployment. +- By default the container publishes on **`127.0.0.1`** (loopback only); set + `CEREFOX_LOCAL_BIND=0.0.0.0` to expose it on the LAN. + +So Layers 1–3 below apply to the cloud deployment; the local backend collapses them into a +single container with an internally-held token. + +--- + ## Summary | Caller | Transport | Auth credential | Typical use | diff --git a/docs/guides/connect-agents.md b/docs/guides/connect-agents.md index ee2c7de..7fb948f 100644 --- a/docs/guides/connect-agents.md +++ b/docs/guides/connect-agents.md @@ -59,6 +59,20 @@ Three top-level paths plus a few special cases: > available in [`examples/mcp-configs/`](../examples/mcp-configs/). Pick the one for your > client, replace the placeholders, and you're connected. +### Local / self-hosted (World B) + +If you run the **Docker backend** ([`setup-local.md`](setup-local.md)) instead of cloud, the +MCP path is different: the server runs **inside the container**, launched per session over +`docker exec`. There's no URL or bearer token in the client config — the access token stays +in the container. + +- **Easiest:** `cerefox-local configure-agent` wires it up (registers an MCP server named + `cerefox-local` with Claude Code if the `claude` CLI is present, else prints the snippet). +- **Manual:** point the client at `command: cerefox-local, args: ["mcp"]` (stdio). That proxies + to `cerefox mcp` in the container; the same 10 tools, identical behavior to every other path. +- The cloud paths above (remote Edge Function, GPT Actions) **do not apply** to a local-only + install — there are no Edge Functions. + --- ## Prerequisites diff --git a/docs/guides/operational-cost.md b/docs/guides/operational-cost.md index 551ff71..f98ec03 100644 --- a/docs/guides/operational-cost.md +++ b/docs/guides/operational-cost.md @@ -116,6 +116,28 @@ These limits comfortably cover personal-use traffic. Check --- +## Scenario C — Fully local / self-hosted (Docker) + +The whole backend runs in one Docker container on your machine — Postgres + pgvector + +the Cerefox server — with **no Supabase and no Edge Functions** ([`setup-local.md`](setup-local.md)). + +``` +Your machine +└── Docker container (free) + └── Postgres + pgvector + cerefox web/MCP + Embeddings via OpenAI API (pay-per-use) +``` + +**Typical cost for personal use**: just the **OpenAI embedding spend** (the same pay-per-use +as every scenario — fractions of a cent per document) plus local compute/electricity. There +is **no Supabase tier and no Edge-Function invocation limit** to worry about — the binding +free-tier constraint from Scenarios A/B (500K EF calls/month) simply doesn't exist here, and +agents using the local MCP server make zero billable cloud calls. The only ongoing cost is +embeddings (and only when you ingest or run semantic/hybrid search) — see "Controlling +embedding costs" below. + +--- + ## Controlling embedding costs If you want to keep costs as low as possible: diff --git a/docs/guides/upgrading.md b/docs/guides/upgrading.md index 741e754..db0336c 100644 --- a/docs/guides/upgrading.md +++ b/docs/guides/upgrading.md @@ -10,6 +10,7 @@ to re-run. |---|---| | **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. | | **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. | +| **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). | > **On an old pre-installer clone (0.1.x)?** The cleanest upgrade is to stop > running from the repo and install the package: follow the From bfb876434ef708b141fa81e4c9788e377256837e Mon Sep 17 00:00:00 2001 From: Fotis Stamatelopoulos Date: Fri, 5 Jun 2026 16:39:24 -0700 Subject: [PATCH 03/10] docs: v0.10.2 CHANGELOG + plan status (deferred completion/configure-agent polish) Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 25 ++++++++++++++++++++++++- docs/plan.md | 24 ++++++++++++++++++------ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f2a0f8..04e4387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,30 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — all ` ## [Unreleased] -Open roadmap. +### Fixed + +- **CLI honors `CEREFOX_MAX_RESPONSE_BYTES`.** The CLI enforces a response byte budget + (`--max-bytes`) but ignored the env var; its default now reads + `CEREFOX_MAX_RESPONSE_BYTES` (200000 fallback). Corrected CLAUDE.md: the budget applies + to MCP/EF **and** the CLI; only the web UI is unlimited. + +### Security + +- **Local container binds to `127.0.0.1` by default** (was `0.0.0.0`), so a single-user + self-hosted backend isn't exposed on the LAN. Opt in with `CEREFOX_LOCAL_BIND=0.0.0.0`. + +### Docs + +- World-B (local/self-hosted) coverage across the guides: `upgrading.md` + (`cerefox-local upgrade`), `operational-cost.md` (fully-local scenario — no Supabase/EF + cost), `access-paths.md` (in-container PostgREST + docker-exec MCP; token never leaves + the container), `connect-agents.md` (`cerefox-local configure-agent` / `cerefox-local mcp`). + +### Deferred (tracked for a later patch) + +- `cerefox-local` shell completion (parameterize `cerefox completion`'s root binding off + the program name) and a cleaner merged `cerefox-local --help`. +- A first-class in-bin `configure-agent --local` (docker-exec) mode for non-Claude clients. --- diff --git a/docs/plan.md b/docs/plan.md index 991ceec..fd07503 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -3390,15 +3390,27 @@ self-refreshing on `upgrade`), simplified `install-local.sh` (no host JWT mintin - ~~Fold `install-local.sh` into `install.sh` / `cerefox init`~~ — **dropped**: contradicts the two-separate-worlds framing (World B is Docker-only; there is no host `cerefox init`). -**Pending → defer to v0.10.1 (polish, not blockers):** -- [ ] `cerefox-local --help`: merge the host-verb preamble + the in-container KB `--help` - more cleanly (today it prints two sections). +**Shipped in v0.10.1** (PR #82, released 2026-06-05): the resource-verb min-search-score +regression + the full `.env`-override restoration (min-search-score, max-response-bytes, +chunking, versioning, backup-dir, OpenAI embedding base-url/model/dims), the phantom-config +guard test, port auto-select, Docker detect-and-guide, the World-B config passthrough, and +the doc quick-fixes. + +**Shipped in v0.10.2** (PR #83, on `feat/local-cerefox`): CLI `--max-bytes` honors +`CEREFOX_MAX_RESPONSE_BYTES`; **local container binds `127.0.0.1` by default** (LAN opt-in +via `CEREFOX_LOCAL_BIND`); World-B doc sections (access-paths, connect-agents, upgrading, +operational-cost). + +**Still deferred (polish — judged too fiddly/risky to do unattended):** +- [ ] Shell completion for `cerefox-local`: parameterize `cerefox completion`'s root binding + (`complete -F _cerefox_completion cerefox`, `#compdef cerefox`, fish `complete -c cerefox`) + + the `_cerefox_*` fn names off the prog name. Cross-cuts 3 shell generators + RC-install + management; do it attended to avoid breaking the working cloud completion. +- [ ] `cerefox-local --help`: merge the host-verb preamble + in-container KB `--help` more + cleanly (cosmetic; today it prints two sections, which works). - [ ] `configure-agent`: a first-class in-bin `--local` (docker-exec) mode so non-Claude clients (Cursor, Codex, Gemini) get auto-wired config, vs. today's `claude mcp add` / printed-snippet host path. -- [ ] Shell completion for `cerefox-local`: `cerefox completion` hardcodes the root - binding (`complete -F _cerefox_completion cerefox`, `#compdef cerefox`); parameterize it - off the prog name. - [ ] Local live-test wiring: point the read/write suites at the local container (extract its in-container JWT for the test) so the same suite runs against cloud **and** local. From b8653289b1d3a9ead859ec0039108ba0d830cfa2 Mon Sep 17 00:00:00 2001 From: Fotis Stamatelopoulos Date: Fri, 5 Jun 2026 20:15:24 -0700 Subject: [PATCH 04/10] fix(web): apply CEREFOX_MIN_SEARCH_SCORE in docs mode (the web default) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0.10.1 wiring was incomplete: only the `hybrid` branch in discovery.ts got getMinSearchScore() — the `docs` branch (the web UI's default mode) still passed p_min_score: 0.0, so the default web search applied no threshold. A replace_all missed it (the docs block is indented 4 spaces vs hybrid's 6). Both branches now use getMinSearchScore(). Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 6 ++++++ packages/memory/src/web/routes/discovery.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e4387..864aedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — all ` ### Fixed +- **Web search now actually applies `CEREFOX_MIN_SEARCH_SCORE`.** The v0.10.1 fix was + incomplete: the web UI defaults to `docs` mode, but only the `hybrid` branch in + `discovery.ts` was updated — the `docs` branch still passed `p_min_score: 0.0` (a + `replace_all` missed it due to a different indent). The default web search therefore + applied no threshold. Both branches now use `getMinSearchScore()`. (Note: in hybrid/docs, + the threshold filters *vector-only* matches; FTS keyword matches still pass by design.) - **CLI honors `CEREFOX_MAX_RESPONSE_BYTES`.** The CLI enforces a response byte budget (`--max-bytes`) but ignored the env var; its default now reads `CEREFOX_MAX_RESPONSE_BYTES` (200000 fallback). Corrected CLAUDE.md: the budget applies diff --git a/packages/memory/src/web/routes/discovery.ts b/packages/memory/src/web/routes/discovery.ts index 90ffb4d..ed6b5db 100644 --- a/packages/memory/src/web/routes/discovery.ts +++ b/packages/memory/src/web/routes/discovery.ts @@ -369,7 +369,7 @@ async function runSearch( p_match_count: Math.min(count, 5), p_alpha: 0.7, p_project_id: projectId, - p_min_score: 0.0, + p_min_score: getMinSearchScore(), }; if (metadataFilter) params.p_metadata_filter = metadataFilter; const { data, error } = await ctx.supabase.rpc("cerefox_search_docs", params); From 99b08c3dd0a9bf64c1cc4005050a4bb8b3b8cfb0 Mon Sep 17 00:00:00 2001 From: Fotis Stamatelopoulos Date: Fri, 5 Jun 2026 20:16:16 -0700 Subject: [PATCH 05/10] fix(local): clearer port-selection message (cloud-present vs busy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "(and/or busy)" message was ambiguous — it fired the same way whether 8000 was actually in use or just proactively skipped because a cloud install shares the 8000 default. Now it distinguishes the two and tells the user how to force 8000 if they want it. Co-Authored-By: Claude Opus 4.7 --- docker/local/install-local.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docker/local/install-local.sh b/docker/local/install-local.sh index 99587e9..968449f 100755 --- a/docker/local/install-local.sh +++ b/docker/local/install-local.sh @@ -84,10 +84,12 @@ else fi done if [ "$PORT" != "$DEFAULT_PORT" ]; then - if [ "$cloud_present" = true ]; then - echo "ℹ Port $DEFAULT_PORT is the cloud Cerefox default (and/or busy) — using $PORT for local." + if port_busy "$DEFAULT_PORT"; then + echo "ℹ Port $DEFAULT_PORT is in use — using $PORT for local." else - echo "ℹ Port $DEFAULT_PORT was busy — using $PORT for local." + echo "ℹ A cloud Cerefox install is present (~/.cerefox/.env), and \`cerefox web\` also" + echo " defaults to $DEFAULT_PORT — using $PORT for local to avoid a future collision." + echo " (Pass PORT=$DEFAULT_PORT to force $DEFAULT_PORT, or any port you prefer.)" fi fi fi From ee08e6e8aaeecd7951425886cde8319bfb4f53cc Mon Sep 17 00:00:00 2001 From: Fotis Stamatelopoulos Date: Fri, 5 Jun 2026 20:32:37 -0700 Subject: [PATCH 06/10] feat(local): cerefox-local re-checks the port at bring-up + auto-moves if taken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cerefox-local start/upgrade/init now detect when the configured host port was taken by another process since last run and step +10 to a free one (persisting it to ~/.cerefox/local/.env), instead of letting docker fail to bind quietly. Only the container-(re)starting paths do this — the proxied KB verbs (search, document, mcp, …) and stop/status/logs/uninstall never run port detection. The check happens after the container's own port is freed (rm in recreate; stopped container in start), so a healthy running setup is never needlessly port-hopped. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 7 ++++++- docker/local/cerefox-local | 41 +++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 864aedd..556bac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,12 @@ the local/World-B container: ### Changed — local backend (World B) polish - `install-local.sh` **auto-selects a free host port** (steps `+10` past a busy port, and - past `8000` when a cloud install shares that default) instead of silently colliding. + past `8000` when a cloud install shares that default) instead of silently colliding; + clearer message distinguishing "in use" from "avoiding the cloud default". +- **`cerefox-local start`/`upgrade`/`init` re-check the port at bring-up time** and step + `+10` to a free one (persisting it to `~/.cerefox/local/.env`) if the stored port was + taken since last run — so a port grabbed by something else doesn't leave the server + failing to bind. Only the container-(re)starting verbs do this; proxied KB commands don't. - Detect-and-guide when Docker is missing or its daemon is stopped (no auto-install). - World-B users can put the `CEREFOX_*` tuning overrides above in `~/.cerefox/local/.env`; they're forwarded into the container (apply with `cerefox-local init`). diff --git a/docker/local/cerefox-local b/docker/local/cerefox-local index 0c984a7..d602329 100755 --- a/docker/local/cerefox-local +++ b/docker/local/cerefox-local @@ -28,6 +28,25 @@ host_port() { # The published host port for container :8000 (so we can print the right URL). docker inspect -f '{{range $p, $c := .NetworkSettings.Ports}}{{if eq $p "8000/tcp"}}{{(index $c 0).HostPort}}{{end}}{{end}}' "$CONTAINER" 2>/dev/null || true } +# True if some process is LISTENing on TCP $1 (best-effort; needs lsof → if absent we +# can't probe and just let docker surface a bind error). +port_busy() { + command -v lsof >/dev/null 2>&1 || return 1 + lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1 +} +# Echo the first free port stepping +10 from $1 (capped to avoid an infinite loop). +free_port_from() { + _p="$1"; _n=0 + while port_busy "$_p"; do _p=$((_p + 10)); _n=$((_n + 1)); [ "$_n" -gt 50 ] && break; done + echo "$_p" +} +# Persist CEREFOX_LOCAL_PORT=<$1> in $ENV_FILE in place (portable; no `sed -i`). +set_env_port() { + [ -f "$ENV_FILE" ] || return 0 + _tmp="$ENV_FILE.tmp.$$" + awk -v p="$1" '/^CEREFOX_LOCAL_PORT=/{print "CEREFOX_LOCAL_PORT=" p; next} {print}' \ + "$ENV_FILE" > "$_tmp" && chmod 600 "$_tmp" && mv "$_tmp" "$ENV_FILE" +} # Config overrides forwarded from the host .env into the container. Deliberately # EXCLUDES container-managed vars (SUPABASE_URL/KEY, DATABASE_URL, POSTGREST_UPSTREAM, @@ -58,6 +77,15 @@ recreate() { docker image inspect "$IMAGE" >/dev/null 2>&1 || docker pull "$IMAGE" \ || die "image '$IMAGE' not present locally and pull failed — container left running" docker rm -f "$CONTAINER" >/dev/null 2>&1 || true + # Our own container's port is now freed, so a busy port here means an EXTERNAL + # conflict (e.g. cloud `cerefox web` grabbed it). Step +10 to a free one and persist. + if port_busy "$PORT"; then + _newport=$(free_port_from "$PORT") + if [ "$_newport" != "$PORT" ]; then + echo "ℹ Port $PORT is in use by another process — moving local to $_newport." + PORT="$_newport"; set_env_port "$PORT" + fi + fi # shellcheck disable=SC2086 docker run -d --name "$CONTAINER" -p "$BIND_ADDR:$PORT:8000" \ --restart unless-stopped \ @@ -165,7 +193,18 @@ case "$cmd" in recreate ;; - start|up) need_docker; docker start "$CONTAINER" >/dev/null && echo "✓ started → http://localhost:$(host_port)/app/" ;; + start|up) + need_docker + # The container is stopped (so it isn't holding its port). If the stored port is busy, + # it's an external conflict — recreate on a free port (docker start can't rebind). + _sp="$(. "$ENV_FILE" 2>/dev/null; echo "${CEREFOX_LOCAL_PORT:-8000}")" + if port_busy "$_sp"; then + echo "ℹ Stored port $_sp is in use by another process — recreating on a free port." + recreate + else + docker start "$CONTAINER" >/dev/null && echo "✓ started → http://localhost:$(host_port)/app/" + fi + ;; stop|down) need_docker; docker stop "$CONTAINER" >/dev/null && echo "✓ stopped (data kept)" ;; restart) need_docker; docker restart "$CONTAINER" >/dev/null && echo "✓ restarted → http://localhost:$(host_port)/app/" ;; status) From 1aa5f7b80ff84cf73dc482a5ae34404727a3b5d5 Mon Sep 17 00:00:00 2001 From: Fotis Stamatelopoulos Date: Fri, 5 Jun 2026 20:47:55 -0700 Subject: [PATCH 07/10] feat(local): cerefox-local completion + configure-agent for all clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the deferred v0.10.2 polish: - Shell completion is now program-name aware: `cerefox completion ` binds to the actual program name + namespaces its functions, so `cerefox-local completion ` emits a working, non-clashing script. Cloud `cerefox` output is byte-identical (no regression). - configure-agent gains `--local` (MCP entry → `cerefox-local mcp`, honoring CEREFOX_LOCAL_CMD). `cerefox-local configure-agent --tool ` now wires Claude Desktop / Cursor / Codex / Gemini too, by running the bundled config writers one-shot via `docker run --entrypoint` with only the client's config dir mounted (Claude Code still via host `claude mcp add`). Verified: the --local entry override + the one-shot-write-to-mount mechanism both work. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 16 ++-- docker/local/cerefox-local | 73 ++++++++++++---- .../memory/src/cli/commands/completion.ts | 86 +++++++++++-------- .../src/cli/commands/configure-agent.ts | 8 +- .../memory/src/cli/util/mcp-config-writers.ts | 42 ++++++--- 5 files changed, 155 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 556bac9..5d62134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,11 +34,17 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — all ` cost), `access-paths.md` (in-container PostgREST + docker-exec MCP; token never leaves the container), `connect-agents.md` (`cerefox-local configure-agent` / `cerefox-local mcp`). -### Deferred (tracked for a later patch) - -- `cerefox-local` shell completion (parameterize `cerefox completion`'s root binding off - the program name) and a cleaner merged `cerefox-local --help`. -- A first-class in-bin `configure-agent --local` (docker-exec) mode for non-Claude clients. +### Added — local backend (World B), continued + +- **`cerefox-local configure-agent --tool `** now wires non-Claude clients too + (Claude Desktop, Cursor, Codex, Gemini), not just Claude Code. It reuses the bundled + config writers via a one-shot `docker run` (the bin gains a `--local` flag that points + the MCP entry at the `cerefox-local mcp` shim); Claude Code still goes through + `claude mcp add` on the host. +- **Shell completion is program-name aware.** `cerefox completion ` now emits a + script bound to the actual program name, so `cerefox-local completion ` produces + a working `cerefox-local` completion that doesn't clash with the cloud `cerefox` one + (function names + bindings are namespaced). Cloud output is unchanged. --- diff --git a/docker/local/cerefox-local b/docker/local/cerefox-local index d602329..5d6bfd3 100755 --- a/docker/local/cerefox-local +++ b/docker/local/cerefox-local @@ -109,7 +109,7 @@ Lifecycle (handled on the host): uninstall [--purge] remove the container (--purge also DELETES the data volume) status show container state + URL logs [-f] show container logs - configure-agent wire an MCP client to this local server (Claude Code, etc.) + configure-agent [--tool X] wire an MCP client (claude-code default; also claude-desktop|cursor|codex|gemini) Knowledge base (run inside the container — same as the cloud CLI): search · document · project · metadata · audit · config · guides · mcp · … @@ -240,23 +240,60 @@ case "$cmd" in ;; configure-agent) - # Host-side: wire an MCP client to the local server. The MCP server runs INSIDE - # the container, launched per-session via `docker exec -i`. We register the host - # `cerefox-local mcp` wrapper so the client never needs the JWT. - self="$(command -v cerefox-local || echo "$CONFIG_DIR/cerefox-local")" - if command -v claude >/dev/null 2>&1; then - claude mcp add cerefox-local -- "$self" mcp && echo "✓ added MCP server 'cerefox-local' to Claude Code" - else - cat <)" ;; + esac + done + case "$tool" in + claude-code) + # Claude Code's `claude` CLI lives on the HOST, so register here (not in-container). + command -v claude >/dev/null 2>&1 \ + || die "claude CLI not found. Install Claude Code, or use --tool cursor|codex|gemini|claude-desktop." + claude mcp add cerefox-local -- "$self" mcp \ + && echo "✓ added MCP server 'cerefox-local' to Claude Code (command: $self mcp)" + ;; + cursor|codex|gemini|claude-desktop) + # Reuse the bundled bin's config writers: run it one-shot in a throwaway container + # (--entrypoint bypasses s6) with ONLY this client's config dir mounted, so the + # writer edits the HOST file. The Linux container can't compute a macOS/Windows + # path, so we compute the target here and pass --config-path. + case "$tool" in + cursor) cfg="$HOME/.cursor/mcp.json" ;; + codex) cfg="$HOME/.codex/config.toml" ;; + gemini) cfg="$HOME/.gemini/settings.json" ;; + claude-desktop) + case "$(uname -s)" in + Darwin) cfg="$HOME/Library/Application Support/Claude/claude_desktop_config.json" ;; + *) cfg="$HOME/.config/Claude/claude_desktop_config.json" ;; + esac ;; + esac + cfgdir=$(dirname "$cfg") + mkdir -p "$cfgdir" + if docker run --rm --entrypoint /usr/local/bin/cerefox \ + -e CEREFOX_LOCAL_CMD="$self" \ + -v "$cfgdir:$cfgdir" \ + "$IMAGE" \ + configure-agent --tool "$tool" --local --config-path "$cfg"; then + echo "✓ configured $tool → $cfg (command: $self mcp). Restart $tool to pick it up." + else + echo "✗ auto-config failed. Add this MCP server to $tool manually:" + echo " command: $self" + echo " args: [\"mcp\"]" + fi + ;; + *) + die "unknown --tool '$tool' (claude-code | claude-desktop | cursor | codex | gemini)" + ;; + esac ;; *) diff --git a/packages/memory/src/cli/commands/completion.ts b/packages/memory/src/cli/commands/completion.ts index 544d9e1..645caf2 100644 --- a/packages/memory/src/cli/commands/completion.ts +++ b/packages/memory/src/cli/commands/completion.ts @@ -83,7 +83,7 @@ function collectNodes(): CompletionNode[] { return nodes; } -function bashScript(nodes: CompletionNode[]): string { +function bashScript(nodes: CompletionNode[], prog: string, fn: string): string { const candCases = nodes .map((n) => ` "${n.path}") echo "${n.candidates.join(" ")}" ;;`) .join("\n"); @@ -92,21 +92,21 @@ function bashScript(nodes: CompletionNode[]): string { .map((n) => `"${n.path}"`) .join("|"); return `# Cerefox bash completion. Source from ~/.bashrc: -# source <(cerefox completion bash) +# source <(${prog} completion bash) # -_cerefox_candidates() { +_${fn}_candidates() { case "$1" in ${candCases} *) echo "--help" ;; esac } -_cerefox_is_path() { +_${fn}_is_path() { case "$1" in ${pathPatterns}) return 0 ;; *) return 1 ;; esac } -_cerefox_completion() { +_${fn}_completion() { local cur path trial w i COMPREPLY=() cur="\${COMP_WORDS[COMP_CWORD]}" @@ -116,20 +116,20 @@ _cerefox_completion() { w="\${COMP_WORDS[$i]}" case "$w" in -*) break ;; esac if [ -z "$path" ]; then trial="$w"; else trial="$path $w"; fi - if _cerefox_is_path "$trial"; then + if _${fn}_is_path "$trial"; then path="$trial"; i=$((i + 1)) else break fi done - COMPREPLY=( $(compgen -W "$(_cerefox_candidates "$path")" -- "$cur") ) + COMPREPLY=( $(compgen -W "$(_${fn}_candidates "$path")" -- "$cur") ) return 0 } -complete -F _cerefox_completion cerefox +complete -F _${fn}_completion ${prog} `; } -function zshScript(nodes: CompletionNode[]): string { +function zshScript(nodes: CompletionNode[], prog: string, fn: string): string { const candCases = nodes .map((n) => ` "${n.path}") REPLY="${n.candidates.join(" ")}" ;;`) .join("\n"); @@ -137,23 +137,23 @@ function zshScript(nodes: CompletionNode[]): string { .filter((n) => n.path !== "") .map((n) => `"${n.path}"`) .join("|"); - return `#compdef cerefox + return `#compdef ${prog} # Cerefox zsh completion. Save and source from ~/.zshrc: -# source <(cerefox completion zsh) +# source <(${prog} completion zsh) # -_cerefox_candidates() { +_${fn}_candidates() { case "$1" in ${candCases} *) REPLY="--help" ;; esac } -_cerefox_is_path() { +_${fn}_is_path() { case "$1" in ${pathPatterns}) return 0 ;; *) return 1 ;; esac } -_cerefox() { +_${fn}() { local path trial w i REPLY path="" i=2 @@ -161,13 +161,13 @@ _cerefox() { w="\${words[i]}" case "$w" in -*) break ;; esac if [[ -z "$path" ]]; then trial="$w"; else trial="$path $w"; fi - if _cerefox_is_path "$trial"; then + if _${fn}_is_path "$trial"; then path="$trial"; (( i++ )) else break fi done - _cerefox_candidates "$path" + _${fn}_candidates "$path" compadd -- \${=REPLY} } # Self-bootstrap the completion system if no \`compinit\` has run yet (e.g. this @@ -176,11 +176,11 @@ _cerefox() { if ! whence compdef >/dev/null 2>&1; then autoload -Uz compinit && compinit fi -compdef _cerefox cerefox +compdef _${fn} ${prog} `; } -function fishScript(nodes: CompletionNode[]): string { +function fishScript(nodes: CompletionNode[], prog: string, fn: string): string { const candCases = nodes .map((n) => ` case "${n.path}"\n echo "${n.candidates.join(" ")}"`) .join("\n"); @@ -188,15 +188,15 @@ function fishScript(nodes: CompletionNode[]): string { .filter((n) => n.path !== "") .map((n) => `"${n.path}"`) .join(" "); - return `# Cerefox fish completion. Save to ~/.config/fish/completions/cerefox.fish -function __cerefox_candidates + return `# Cerefox fish completion. Save to ~/.config/fish/completions/${prog}.fish +function __${fn}_candidates switch "$argv[1]" ${candCases} case '*' echo "--help" end end -function __cerefox_is_path +function __${fn}_is_path for p in ${pathList} if test "$argv[1]" = "$p" return 0 @@ -204,7 +204,7 @@ function __cerefox_is_path end return 1 end -function __cerefox_complete +function __${fn}_complete set -l tokens (commandline -opc) set -l path "" set -l i 2 @@ -219,30 +219,41 @@ function __cerefox_complete else set trial "$path $w" end - if __cerefox_is_path "$trial" + if __${fn}_is_path "$trial" set path "$trial" set i (math $i + 1) else break end end - string split ' ' -- (__cerefox_candidates "$path") + string split ' ' -- (__${fn}_candidates "$path") end -complete -c cerefox -f -a '(__cerefox_complete)' +complete -c ${prog} -f -a '(__${fn}_complete)' `; } type Shell = "bash" | "zsh" | "fish"; +/** The bin's program name (honors CEREFOX_PROG_NAME, e.g. "cerefox-local"). */ +function progName(): string { + return buildProgram().name(); +} +/** A safe shell-function identifier derived from the program name. */ +function fnId(prog: string): string { + return prog.replace(/[^a-zA-Z0-9]/g, "_"); +} + function scriptFor(shell: Shell): string { const nodes = collectNodes(); + const prog = progName(); + const fn = fnId(prog); switch (shell) { case "bash": - return bashScript(nodes); + return bashScript(nodes, prog, fn); case "zsh": - return zshScript(nodes); + return zshScript(nodes, prog, fn); case "fish": - return fishScript(nodes); + return fishScript(nodes, prog, fn); } } @@ -253,8 +264,12 @@ function detectShell(): Shell | null { return null; } -const RC_BEGIN = "# >>> cerefox shell completion (managed by `cerefox completion install`) >>>"; -const RC_END = "# <<< cerefox shell completion <<<"; +function rcBeginFor(prog: string): string { + return `# >>> ${prog} shell completion (managed by \`${prog} completion install\`) >>>`; +} +function rcEndFor(prog: string): string { + return `# <<< ${prog} shell completion <<<`; +} /** * `cerefox completion install [--shell ] [--yes]` — write the completion @@ -275,23 +290,26 @@ async function installMode(options: { shell?: string; yes?: boolean }): Promise< } const home = homedir(); - const scriptPath = join(home, `.cerefox-completion.${shell}`); + const prog = progName(); + const rcBegin = rcBeginFor(prog); + const rcEnd = rcEndFor(prog); + const scriptPath = join(home, `.${prog}-completion.${shell}`); // Always (re)write the script so upgrades pick up new commands/flags. writeFileSync(scriptPath, scriptFor(shell), "utf8"); println(c.green(`✓ Wrote completion script: ${scriptPath}`)); // fish: drop-in dir, no rc edit needed. if (shell === "fish") { - println(c.dim(" For fish, also copy it into ~/.config/fish/completions/cerefox.fish (or `source` it).")); + println(c.dim(` For fish, also copy it into ~/.config/fish/completions/${prog}.fish (or \`source\` it).`)); return; } const rcPath = join(home, shell === "zsh" ? ".zshrc" : ".bashrc"); const sourceLine = `[ -s "${scriptPath}" ] && source "${scriptPath}"`; - const block = `${RC_BEGIN}\n${sourceLine}\n${RC_END}\n`; + const block = `${rcBegin}\n${sourceLine}\n${rcEnd}\n`; const existing = existsSync(rcPath) ? readFileSync(rcPath, "utf8") : ""; - if (existing.includes(RC_BEGIN)) { + if (existing.includes(rcBegin)) { println(c.dim(` ${rcPath} already sources the completion (left as-is).`)); } else { // Editing the user's rc — confirm unless --yes or non-interactive. diff --git a/packages/memory/src/cli/commands/configure-agent.ts b/packages/memory/src/cli/commands/configure-agent.ts index a98e619..5495c41 100644 --- a/packages/memory/src/cli/commands/configure-agent.ts +++ b/packages/memory/src/cli/commands/configure-agent.ts @@ -8,7 +8,7 @@ import { printJson, userError, } from "../../../../../_shared/cli-core/index.ts"; -import { WRITERS, writeMcpConfig } from "../util/mcp-config-writers.ts"; +import { localCerefoxEntry, WRITERS, writeMcpConfig } from "../util/mcp-config-writers.ts"; interface ConfigureAgentOptions { tool: string; @@ -16,6 +16,7 @@ interface ConfigureAgentOptions { backup: boolean; dryRun?: boolean; json?: boolean; + local?: boolean; } function action(options: ConfigureAgentOptions): void { @@ -31,6 +32,10 @@ function action(options: ConfigureAgentOptions): void { customPath: options.configPath, noBackup: !options.backup, dryRun: options.dryRun, + // --local: point the client at the host `cerefox-local mcp` shim (World B) + // instead of `npx … cerefox mcp` (cloud). Usually set by `cerefox-local + // configure-agent`, which also exports CEREFOX_LOCAL_CMD with the abs path. + entry: options.local ? localCerefoxEntry() : undefined, }); if (options.json) { @@ -92,5 +97,6 @@ export function registerConfigureAgent(program: Command): void { .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(action); } diff --git a/packages/memory/src/cli/util/mcp-config-writers.ts b/packages/memory/src/cli/util/mcp-config-writers.ts index ee07417..610d98d 100644 --- a/packages/memory/src/cli/util/mcp-config-writers.ts +++ b/packages/memory/src/cli/util/mcp-config-writers.ts @@ -80,7 +80,7 @@ export interface ConfigWriter { * the server. The first element is the executable; remaining elements * are its arguments. Resolved into a full command line at run time. */ - delegated?: () => { cmd: string; args: string[] }; + delegated?: (entry: { command: string; args: string[] }) => { cmd: string; args: string[] }; } /** @@ -101,6 +101,20 @@ function defaultCerefoxEntry(): { command: string; args: string[] } { }; } +/** + * MCP server entry for the LOCAL / self-hosted (World-B) backend: launch the + * host `cerefox-local` shim, which proxies `mcp` (stdio) into the Docker + * container. The command path is overridable via `CEREFOX_LOCAL_CMD` (the + * `cerefox-local configure-agent` host path passes the resolved absolute path so + * MCP clients with a minimal PATH still find it). Used when `--local` is passed. + */ +export function localCerefoxEntry(): { command: string; args: string[] } { + return { + command: process.env.CEREFOX_LOCAL_CMD || "cerefox-local", + args: ["mcp"], + }; +} + function claudeCodeUserConfigPath(): string { // Claude Code's user-scope config — the single dot-file in $HOME. // `~/.claude/` is a directory for Claude Code's caches and history, @@ -120,8 +134,7 @@ function claudeDesktopConfigPath(): string { } /** Build the `claude mcp add ...` argv for the Claude Code delegation. */ -function claudeCodeDelegated(): { cmd: string; args: string[] } { - const entry = defaultCerefoxEntry(); +function claudeCodeDelegated(entry: { command: string; args: string[] }): { cmd: string; args: string[] } { // `claude mcp add --scope user -- [args...]` return { cmd: "claude", @@ -201,10 +214,15 @@ export interface WriteResult { * `customPath` — the override is treated as "write here, skip the * delegated CLI". This keeps the legacy test path working. */ -export function writeMcpConfig( - writer: ConfigWriter, - opts: { customPath?: string; noBackup?: boolean; dryRun?: boolean } = {}, -): WriteResult { +export interface WriteOpts { + customPath?: string; + noBackup?: boolean; + dryRun?: boolean; + /** Override the MCP server entry (e.g. the local `cerefox-local mcp` shim). */ + entry?: { command: string; args: string[] }; +} + +export function writeMcpConfig(writer: ConfigWriter, opts: WriteOpts = {}): WriteResult { // --config-path always wins. If the writer is delegated but the user // (or a test) asked for a specific file, do a direct write there. if (opts.customPath) { @@ -219,9 +237,9 @@ export function writeMcpConfig( function directWrite( writer: ConfigWriter, configPath: string, - opts: { noBackup?: boolean; dryRun?: boolean }, + opts: WriteOpts, ): WriteResult { - const entry = writer.buildServerEntry(); + const entry = opts.entry ?? writer.buildServerEntry(); const format = writer.format ?? "json"; if (!opts.dryRun) mkdirSync(dirname(configPath), { recursive: true }); @@ -294,13 +312,13 @@ function hasCerefoxEntry( */ function delegatedWrite( writer: ConfigWriter, - opts: { noBackup?: boolean; dryRun?: boolean }, + opts: WriteOpts, ): WriteResult { if (!writer.delegated) { throw new Error(`${writer.label}: kind=delegated but no delegated() factory`); } - const { cmd, args } = writer.delegated(); - const entry = writer.buildServerEntry(); + const entry = opts.entry ?? writer.buildServerEntry(); + const { cmd, args } = writer.delegated(entry); const delegatedCommand = `${cmd} ${args.join(" ")}`; if (opts.dryRun) { From 0611b57639b13f818512c8e30cf0a2b6efa6b5de Mon Sep 17 00:00:00 2001 From: Fotis Stamatelopoulos Date: Fri, 5 Jun 2026 20:53:16 -0700 Subject: [PATCH 08/10] =?UTF-8?q?docs:=20sanity=20sweep=20=E2=80=94=20fix?= =?UTF-8?q?=20stale=20info=20+=20cloud/local=20correctness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a comprehensive audit of all docs (README, CONTRIBUTING, AGENT_*, guides): - Kill Fireworks-as-working claims (README, configuration.md example block, operational-cost.md, cli.md) — OpenAI is the only TS embedder today. - CONTRIBUTING.md: stop presenting Python as a live runtime/adapter (it's a frozen MCP husk); Bun/Node-first dev setup with uv optional; .docx IS supported (only PDF dropped); embedder path → _shared/embeddings; schema version is gated by cut_release, not bumped by hand. - README: drop the Python badge. - cli.md: add a cerefox-local (World-B) callout; mark the Supabase/db env as cloud-only (local users set only OPENAI_API_KEY); note ingest-dir needs --extensions .docx opt-in. - Reconcile the install-local one-liner + the "localhost:8000" headline (installer auto-steps the port) between README and setup-local.md. - agent-coordination.md: note cross-machine coordination assumes the cloud backend (local is single-machine / loopback by default). - test: lock in configure-agent --local entry override. Co-Authored-By: Claude Opus 4.7 --- CONTRIBUTING.md | 28 +++++---- README.md | 8 +-- docs/guides/agent-coordination.md | 2 + docs/guides/cli.md | 12 +++- docs/guides/configuration.md | 13 +--- docs/guides/operational-cost.md | 4 +- docs/guides/setup-local.md | 10 ++- .../memory/test/configure-agent-local.test.ts | 61 +++++++++++++++++++ 8 files changed, 105 insertions(+), 33 deletions(-) create mode 100644 packages/memory/test/configure-agent-local.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f67fb6..f3fbb3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ The most valuable contributions fall into these categories: **Performance and security improvements**: profiling, query optimization, security hardening, input validation. -**Ingestion formats**: ingestion is Markdown/`.txt`-only as of v0.7 (PDF/DOCX converters were dropped). If you want to support new source formats (e.g., HTML, EPUB, Notion exports, Obsidian vaults), the conversion-to-Markdown step would need to be reintroduced — open an issue to discuss before starting. +**Ingestion formats**: Markdown / `.txt` / `.docx` (`.docx` is converted to Markdown via `mammoth` on ingest; fidelity varies). **PDF is not supported** (dropped in v0.7 — convert to Markdown upstream). To add new source formats (HTML, EPUB, Notion exports, Obsidian vaults), extend the conversion step in `packages/memory/src/ingestion/file-to-markdown.ts` — open an issue to discuss before starting. **Knowledge system integrations**: two-way sync with knowledge management systems (Obsidian, Logseq, Notion, etc.) is an area with significant potential. If you use Cerefox alongside another knowledge tool, an integration that keeps them in sync would be a meaningful contribution. @@ -32,11 +32,11 @@ The most valuable contributions fall into these categories: All contributions must follow Cerefox's architecture: -**Single implementation principle**: business logic lives in Postgres RPCs (`src/cerefox/db/rpcs.sql`). Python, Edge Functions, and the MCP server are thin adapters that call RPCs. Do not duplicate logic across access paths. +**Single implementation principle**: business logic lives in Postgres RPCs (`src/cerefox/db/rpcs.sql` — still the live SQL source of truth). The TS client (`packages/memory`), the Edge Functions, and the shared MCP tool handlers (`_shared/mcp-tools/`) are thin adapters that call those RPCs. Do not duplicate logic across access paths. **Markdown-first**: all content is stored as Markdown documents. Derived structures (embeddings, indexes, metadata) are regenerable from the document corpus. -**Cloud embeddings**: Cerefox uses cloud embedding APIs (OpenAI, Fireworks AI). New embedders must implement the `Embedder` protocol in `src/cerefox/embeddings/base.py` and output 768-dimensional vectors. +**Cloud embeddings**: Cerefox uses cloud embedding APIs. The live embedder is TypeScript in `_shared/embeddings/` (OpenAI `text-embedding-3-small`, 768-dim — the only one wired today; a Fireworks/OpenAI-compatible option is roadmap, not implemented). Any embedder must output **768-dim** vectors to match the `vector(768)` schema; changing the model/dimensions is a breaking change requiring `cerefox server reindex`. See `docs/solution-design.md` and `docs/research/vision.md` for the full architecture and project direction. @@ -44,21 +44,27 @@ See `docs/solution-design.md` and `docs/research/vision.md` for the full archite ## Development Setup -Cerefox is a Python + TypeScript project. As of v0.2.0, contributors need **three** runtimes installed locally: +Cerefox is a **TypeScript** project (Bun/Node). The entire runtime — CLI, MCP server, web +server, and ingestion pipeline — is TypeScript in [`@cerefox/memory`](https://www.npmjs.com/package/@cerefox/memory) +as of v0.9. Python survives **only** as a frozen, unmaintained MCP fallback (`uv run cerefox +mcp`); `uv` is optional and only needed if you want to touch that husk. | Tool | Why | Install | |---|---|---| -| **Python 3.11+** with [`uv`](https://docs.astral.sh/uv/) | Backend, CLI, MCP server, ingestion pipeline | `curl -LsSf https://astral.sh/uv/install.sh \| sh` | -| **Node 20+** with `npm` | Frontend (React + Vite), Supabase Edge Functions | [nodejs.org](https://nodejs.org/) or `nvm install 20` | -| **[Bun](https://bun.sh) 1.x** | TypeScript scripts (`scripts/*.ts`, starting with `cut_release.ts` in v0.2.0) | `curl -fsSL https://bun.sh/install \| bash` | +| **[Bun](https://bun.sh) 1.x** | The whole TS runtime + `scripts/*.ts` + tests (`bun test`) | `curl -fsSL https://bun.sh/install \| bash` | +| **Node 20+** with `npm` | Frontend (React + Vite) build + npm publish; an alternative TS runtime | [nodejs.org](https://nodejs.org/) or `nvm install 20` | +| **Python 3.11+** with [`uv`](https://docs.astral.sh/uv/) | **Optional** — only for the legacy `uv run cerefox mcp` fallback | `curl -LsSf https://astral.sh/uv/install.sh \| sh` | -The Bun requirement is new in v0.2.0 — see [Script-language policy](#script-language-policy-effective-from-v020) below. From v0.5.0 the local MCP server **and** the main CLI both ship as bins inside the npm package [`@cerefox/memory`](https://www.npmjs.com/package/@cerefox/memory); end users install via `npm`/`bun install -g` and don't need uv or a clone. Contributors still need all three runtimes (Python for the schema deploy + web server + ingestion pipeline until v0.6/v0.7, Node for the frontend + npm publish, Bun for TS scripts and `_shared/`/`packages/memory/` tests). +End users install via `npm`/`bun install -g @cerefox/memory` (or the one-liner installer) and +need neither `uv` nor a clone. The **local / self-hosted (Docker) backend** is separate again +— see [`docs/guides/setup-local.md`](docs/guides/setup-local.md). ```bash -# Clone and install +# Clone and install (TS deps for root + packages/memory + frontend) git clone https://github.com/fstamatelopoulos/cerefox.git cd cerefox -uv sync +bun install +# uv sync # OPTIONAL — only for the legacy `uv run cerefox mcp` fallback # Run tests (`bun test` is the only runner; pytest is retired) cd _shared && bun test # TS unit tests (mocked) @@ -131,7 +137,7 @@ export const COMPATIBILITY = { - **Client patch releases never raise a minimum.** A patch must run against the same server range as the minor it patches. - Each bump is intentional and reviewed at PR time — don't raise a minimum "just because" the server moved. The minimum is the *oldest server this client still works with*, not *the newest server available*. -Two versions track the server side: the **schema version** (`@version:` marker in `src/cerefox/db/schema.sql`, covers schema + RPCs since they deploy atomically) and **`EF_VERSION`** (`_shared/ef-meta/index.ts`, covers all Edge Functions). `cut_release.ts` bumps `EF_VERSION` only when EF source changed since the last tag; the schema version is bumped by hand when `schema.sql`/`rpcs.sql` change. +Two versions track the server side: the **schema version** (`@version:` marker in `src/cerefox/db/schema.sql`, covers schema + RPCs since they deploy atomically) and **`EF_VERSION`** (`_shared/ef-meta/index.ts`, covers all Edge Functions). `cut_release.ts` bumps `EF_VERSION` automatically when EF source changed since the last tag, and **gates** the schema version: it fails the cut if `schema.sql`/`rpcs.sql` changed without a matching `@version:` bump (both the `schema.sql` marker and the `cerefox_schema_version()` literal in `rpcs.sql` must move together). --- diff --git a/README.md b/README.md index d2d7610..0183d6c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ **User-owned shared memory for AI agents.** A persistent, curated knowledge layer that multiple AI tools can read and write, backed by Postgres + pgvector. [![Apache 2.0 License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) -[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://python.org) [![Node 20+](https://img.shields.io/badge/node-20+-green.svg)](https://nodejs.org) --- @@ -41,7 +40,7 @@ Cerefox is **asynchronous shared memory, not a message bus**. It solves the pers | **Metadata search** | Standalone metadata-only search (no text query needed); find documents by key-value criteria, project, and date range; optional content inclusion with byte budget; dedicated MCP tool, CLI command, and web UI page | | **Project discovery** | `cerefox_list_projects` MCP tool for agents to discover available projects; all search results include human-readable `project_names` alongside UUIDs | | **Heading-aware chunking** | Greedy section accumulation — H1/H2/H3 sections accumulate until MAX_CHUNK_CHARS; heading breadcrumb preserved per chunk | -| **Cloud embeddings** | OpenAI `text-embedding-3-small` (768-dim) via API — or swap to Fireworks AI | +| **Cloud embeddings** | OpenAI `text-embedding-3-small` (768-dim) via API (the only embedder wired in the TS runtime today) | | **Remote MCP endpoint** | `cerefox-mcp` Supabase Edge Function — MCP Streamable HTTP; connect Claude Desktop, Claude Code, or Cursor with just a URL and anon key; no Python install needed | | **Local MCP server** | `cerefox mcp` stdio server (TypeScript, from `@cerefox/memory`) -- local alternative with zero Edge Function usage, lower latency, and offline support; `npm install -g @cerefox/memory`. (A frozen Python MCP server also ships for repo-clone users: `uv run cerefox mcp`.) | | **Web UI** | React + TypeScript SPA (Mantine UI) at `/app/`; Hono (TypeScript) JSON API backend served by `cerefox web`; Markdown viewer, search with 4 modes, document editing, project management | @@ -113,7 +112,7 @@ cerefox web # web UI → http://localhost:8000/app/ ``` **Prerequisites:** Node 20+ or Bun 1.0+ · a Supabase account (free tier) · an -embedding API key (OpenAI `text-embedding-3-small` by default, or Fireworks AI). +embedding API key (OpenAI `text-embedding-3-small`). > **Full walkthrough:** [`docs/guides/quickstart.md`](docs/guides/quickstart.md) > (~15 min). Supabase specifics: [`docs/guides/setup-supabase.md`](docs/guides/setup-supabase.md). @@ -135,7 +134,8 @@ cerefox-local configure-agent # wire an MCP client (e.g. Claude Code) # 3. Use it: cerefox-local document ingest my-notes.md --title "My notes" cerefox-local search "what did I decide about auth?" -# web UI → http://localhost:8000/app/ (manage: cerefox-local status | upgrade | stop) +# web UI → http://localhost:8000/app/ (or the port the installer chose — it auto-steps +# to 8010/… if 8000 is busy; `cerefox-local status` shows the URL. Manage: status | upgrade | stop) ``` **Prerequisites:** Docker (Docker Desktop or [Colima](https://github.com/abiosoft/colima)) diff --git a/docs/guides/agent-coordination.md b/docs/guides/agent-coordination.md index 56b7979..47feb87 100644 --- a/docs/guides/agent-coordination.md +++ b/docs/guides/agent-coordination.md @@ -23,6 +23,8 @@ Within a single runtime (e.g., Claude Code's agent teams feature, or a LangGraph 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. +> **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. + The coordination model is **asynchronous and knowledge-based**: 1. **Agent A writes** a finding, decision, or task breakdown to Cerefox. It does not need to know which agent will consume it. diff --git a/docs/guides/cli.md b/docs/guides/cli.md index 0009644..4ca5c2d 100644 --- a/docs/guides/cli.md +++ b/docs/guides/cli.md @@ -4,12 +4,18 @@ Comprehensive reference for every `cerefox` subcommand. For tutorials and walkth > `--help` is canonical. If anything in this document disagrees with `cerefox --help`, trust `--help` and file an issue against this guide. +> **Local / self-hosted (Docker) backend?** Every KB verb below is identical, but you run it +> as **`cerefox-local `** (it proxies into the container via `docker exec`); lifecycle +> is different (`cerefox-local init/start/stop/upgrade/uninstall/status/logs/configure-agent`). +> A local user sets **only `OPENAI_API_KEY`** — the Supabase/database vars below do **not** +> apply (the container owns them). See [`setup-local.md`](setup-local.md). + ## Setup -Every command reads configuration from `.env` in the working directory (or environment variables — see [`configuration.md`](configuration.md)). Required at minimum: +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: - `CEREFOX_SUPABASE_URL` and `CEREFOX_SUPABASE_KEY` for any command that talks to Supabase -- `OPENAI_API_KEY` (or `CEREFOX_FIREWORKS_API_KEY`) for any command that embeds (ingest, search) +- `OPENAI_API_KEY` for any command that embeds (ingest, search) - `CEREFOX_DATABASE_URL` for `cerefox server deploy` and the contributor scripts (`bun scripts/db_*.ts`) The CLI is the TypeScript `@cerefox/memory` package. Invoke any command as plain `cerefox ` (installed via the installer or `npm install -g @cerefox/memory` — see [`quickstart.md`](quickstart.md#1-install)). @@ -86,7 +92,7 @@ Walks `DIRECTORY` **recursively** (always — there is no recurse toggle) and in | Flag | Type | Default | Description | |---|---|---|---| -| `--extensions ` (`-e`) | comma list | `.md,.txt` | File extensions to ingest, e.g. `--extensions .md`. | +| `--extensions ` (`-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). | | `--project-name ` (`-p`) | str | _none_ | Project to assign every document to. | | `--update-if-exists` (`-u`) | flag | off | Update existing documents by source path / title. | | `--metadata ` (`-m`) | JSON | `{}` | JSON metadata applied to every file in the run. | diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 4ffefca..75f562b 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -250,16 +250,9 @@ OPENAI_API_KEY=sk-... # All other settings use defaults ``` -## Example: Fireworks Embedder `.env` - -```bash -CEREFOX_SUPABASE_URL=https://abcdefghijkl.supabase.co -CEREFOX_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIs... -CEREFOX_DATABASE_URL=postgresql://... - -CEREFOX_EMBEDDER=fireworks -CEREFOX_FIREWORKS_API_KEY=fw_... -``` +> **Fireworks is not wired in the TS runtime yet** — `CEREFOX_EMBEDDER=fireworks` / +> `CEREFOX_FIREWORKS_*` are documented for the retired Python runtime but are currently +> no-ops (OpenAI is the only embedder implemented today). Tracked for a future release. --- diff --git a/docs/guides/operational-cost.md b/docs/guides/operational-cost.md index f98ec03..95539af 100644 --- a/docs/guides/operational-cost.md +++ b/docs/guides/operational-cost.md @@ -142,8 +142,8 @@ embedding costs" below. If you want to keep costs as low as possible: -- **Fireworks AI** is an OpenAI-compatible alternative that offers competitive embedding prices. - See `docs/guides/configuration.md` for how to switch. +- **Cheaper embedding models**: a lower-cost OpenAI-compatible provider (e.g. Fireworks AI) + is on the roadmap — **not yet wired in the TS runtime** (OpenAI only today). - **Batch ingest, don't re-ingest**: Cerefox deduplicates by content hash — re-ingesting the same file twice costs nothing. Only new or changed content triggers embedding calls. - **`cerefox server reindex`**: Re-embeds all existing chunks if you switch embedders. Run this once diff --git a/docs/guides/setup-local.md b/docs/guides/setup-local.md index e2711c6..9d99730 100644 --- a/docs/guides/setup-local.md +++ b/docs/guides/setup-local.md @@ -36,11 +36,13 @@ That's it. No Node, Bun, Postgres, or repo clone needed. ## Step 1 — Install ```bash -OPENAI_API_KEY=sk-... sh -c "$(curl -fsSL https://github.com/fstamatelopoulos/cerefox/releases/latest/download/install-local.sh)" +curl -fsSL https://github.com/fstamatelopoulos/cerefox/releases/latest/download/install-local.sh | sh ``` This pulls the published multi-arch image (`amd64` + `arm64`), starts the container, and -installs a `cerefox-local` command (symlinked into `~/.local/bin`). Pick a different port +installs a `cerefox-local` command (symlinked into `~/.local/bin`). To set your OpenAI key +inline at install instead of via `cerefox-local init` (Step 2), use the command-substitution +form: `OPENAI_API_KEY=sk-... sh -c "$(curl -fsSL …/install-local.sh)"`. Pick a specific port with `PORT=8017 …`. > If the installer warns that `~/.local/bin` isn't on your `PATH`, add it: @@ -48,7 +50,9 @@ with `PORT=8017 …`. > echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc > ``` -The web UI is now at **http://localhost:8000/app/** (or your chosen port). +The web UI is now at **http://localhost:8000/app/** — **or the port the installer chose** +(it auto-steps to 8010/8020/… if 8000 is busy or you also run the cloud `cerefox web`, which +defaults to 8000). The installer prints the actual URL; `cerefox-local status` shows it too. **How the credential works:** the container generates its own JWT secret on first boot and mints the access token internally — the token never leaves the container. The only secret diff --git a/packages/memory/test/configure-agent-local.test.ts b/packages/memory/test/configure-agent-local.test.ts new file mode 100644 index 0000000..0071562 --- /dev/null +++ b/packages/memory/test/configure-agent-local.test.ts @@ -0,0 +1,61 @@ +/** + * `configure-agent --local` wires the MCP entry to the `cerefox-local mcp` shim + * (World-B) instead of the npx `cerefox mcp` default, honoring CEREFOX_LOCAL_CMD. + */ + +import { afterEach, describe, expect, it } from "bun:test"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { + localCerefoxEntry, + WRITERS, + writeMcpConfig, +} from "../src/cli/util/mcp-config-writers.ts"; + +describe("localCerefoxEntry", () => { + afterEach(() => { + delete process.env.CEREFOX_LOCAL_CMD; + }); + it("defaults to `cerefox-local mcp`", () => { + expect(localCerefoxEntry()).toEqual({ command: "cerefox-local", args: ["mcp"] }); + }); + it("honors CEREFOX_LOCAL_CMD (absolute path from the host shim)", () => { + process.env.CEREFOX_LOCAL_CMD = "/Users/x/.cerefox/local/cerefox-local"; + expect(localCerefoxEntry().command).toBe("/Users/x/.cerefox/local/cerefox-local"); + }); +}); + +describe("writeMcpConfig with a --local entry override", () => { + it("writes the cerefox-local entry (not npx) into a JSON client config", () => { + const dir = mkdtempSync(join(tmpdir(), "cfx-cfg-")); + const cfg = join(dir, "mcp.json"); + try { + const res = writeMcpConfig(WRITERS.cursor, { + customPath: cfg, + noBackup: true, + entry: localCerefoxEntry(), + }); + expect(res.serverEntry).toEqual({ command: "cerefox-local", args: ["mcp"] }); + const written = JSON.parse(readFileSync(cfg, "utf8")) as { + mcpServers: { cerefox: { command: string; args: string[] } }; + }; + expect(written.mcpServers.cerefox.command).toBe("cerefox-local"); + expect(written.mcpServers.cerefox.args).toEqual(["mcp"]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("defaults to the npx entry when no override is given", () => { + const dir = mkdtempSync(join(tmpdir(), "cfx-cfg-")); + const cfg = join(dir, "mcp.json"); + try { + const res = writeMcpConfig(WRITERS.cursor, { customPath: cfg, noBackup: true }); + expect(res.serverEntry.command).toBe("npx"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); From d8e70d6dad765ac68b1806f01324cca46c62813e Mon Sep 17 00:00:00 2001 From: Fotis Stamatelopoulos Date: Fri, 5 Jun 2026 20:53:50 -0700 Subject: [PATCH 09/10] docs(plan): mark completion + configure-agent + doc-sweep done in v0.10.2 Co-Authored-By: Claude Opus 4.7 --- docs/plan.md | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/plan.md b/docs/plan.md index fd07503..2085459 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -3397,22 +3397,28 @@ guard test, port auto-select, Docker detect-and-guide, the World-B config passth the doc quick-fixes. **Shipped in v0.10.2** (PR #83, on `feat/local-cerefox`): CLI `--max-bytes` honors -`CEREFOX_MAX_RESPONSE_BYTES`; **local container binds `127.0.0.1` by default** (LAN opt-in -via `CEREFOX_LOCAL_BIND`); World-B doc sections (access-paths, connect-agents, upgrading, -operational-cost). - -**Still deferred (polish — judged too fiddly/risky to do unattended):** -- [ ] Shell completion for `cerefox-local`: parameterize `cerefox completion`'s root binding - (`complete -F _cerefox_completion cerefox`, `#compdef cerefox`, fish `complete -c cerefox`) - + the `_cerefox_*` fn names off the prog name. Cross-cuts 3 shell generators + RC-install - management; do it attended to avoid breaking the working cloud completion. -- [ ] `cerefox-local --help`: merge the host-verb preamble + in-container KB `--help` more - cleanly (cosmetic; today it prints two sections, which works). -- [ ] `configure-agent`: a first-class in-bin `--local` (docker-exec) mode so non-Claude - clients (Cursor, Codex, Gemini) get auto-wired config, vs. today's `claude mcp add` / - printed-snippet host path. +`CEREFOX_MAX_RESPONSE_BYTES`; web docs-mode `min_score` fix (the v0.10.1 fix missed the +default mode); **local container binds `127.0.0.1` by default** (LAN opt-in via +`CEREFOX_LOCAL_BIND`); **`cerefox-local` runtime port re-check + auto-step**; World-B doc +sections; **program-name-aware shell completion** (`cerefox-local completion` works, +non-clashing); **`cerefox-local configure-agent --tool X` for all clients** (bin `--local` +flag + one-shot `docker run --entrypoint` writer reuse); and a **comprehensive doc sanity +sweep** (killed Fireworks-as-working claims, de-Pythonized CONTRIBUTING, cloud-vs-local +correctness across the guides). + +**Done (were deferred polish, now completed attended in v0.10.2):** +- [x] `cerefox-local` shell completion — parameterized off the program name; cloud output + byte-identical, `cerefox-local` namespaced. (`completion.ts`) +- [x] `configure-agent --local` — bin flag + host auto-wire for Claude Desktop / Cursor / + Codex / Gemini via `docker run --entrypoint` (Claude Code via host `claude mcp add`). +- [~] `cerefox-local --help` "merge" — left as the two-section output (host verbs + the + in-container KB `--help`); it works and reads clearly, so not worth a fragile merge. + +**Still deferred (lower-value / heavier):** - [ ] Local live-test wiring: point the read/write suites at the local container (extract its in-container JWT for the test) so the same suite runs against cloud **and** local. +- [ ] `schema-version.bundled=null` image cosmetic (doctor shows a null bundled-schema in + the container context). Cosmetic only. **Testing convention (local):** use the **single** default local container (`cerefox-local` / volume `cerefox_local_pgdata`) — the local analogue of the maintainer's From 6b826eceedd458bf032dcaa48a68e26b5320483c Mon Sep 17 00:00:00 2001 From: Fotis Stamatelopoulos Date: Fri, 5 Jun 2026 21:06:29 -0700 Subject: [PATCH 10/10] feat(local): install-local.sh auto-wires cerefox-local shell completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the cloud installer: generate the program-name-namespaced completion script from the container (docker exec) and source it from the user's shell rc (idempotent, best-effort, never aborts the install) + print an "exec $shell" hint. `completion install` itself can't run for World B (proxied → writes inside the container), so the host-side wiring lives in the installer. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 12 ++++++++---- docker/local/install-local.sh | 34 ++++++++++++++++++++++++++++++++++ docs/guides/setup-local.md | 3 +++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d62134..c08769c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,10 +41,14 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — all ` config writers via a one-shot `docker run` (the bin gains a `--local` flag that points the MCP entry at the `cerefox-local mcp` shim); Claude Code still goes through `claude mcp add` on the host. -- **Shell completion is program-name aware.** `cerefox completion ` now emits a - script bound to the actual program name, so `cerefox-local completion ` produces - a working `cerefox-local` completion that doesn't clash with the cloud `cerefox` one - (function names + bindings are namespaced). Cloud output is unchanged. +- **Shell completion is program-name aware + auto-installed.** `cerefox completion ` + emits a script bound to the actual program name, so `cerefox-local completion ` + produces a working `cerefox-local` completion that doesn't clash with the cloud `cerefox` + one (functions + bindings namespaced; cloud output unchanged). `install-local.sh` now + wires it up host-side (best-effort, idempotent) — generating the script from the + container and sourcing it from your shell rc, mirroring the cloud installer + printing an + "exec $shell" hint. (The `completion install` subcommand itself can't be used for World B + — proxied into the container, it would write inside it — hence the host-side wiring.) --- diff --git a/docker/local/install-local.sh b/docker/local/install-local.sh index 968449f..1eb4bf1 100755 --- a/docker/local/install-local.sh +++ b/docker/local/install-local.sh @@ -165,6 +165,39 @@ until curl -fsS -o /dev/null "http://localhost:$PORT/api/v1/projects" 2>/dev/nul done [ "$i" -le 45 ] && echo " ready." +# 5. Shell tab-completion (best-effort, idempotent; mirrors the cloud installer). Generate +# the cerefox-local-namespaced script from the container and source it from the shell rc. +# `completion install` can't be proxied (it would write inside the container), so we do +# the host-side wiring here. Failures never abort the install. +COMPLETION_MSG="" +comp_shell="$(basename "${SHELL:-}")" +case "$comp_shell" in + bash|zsh|fish) + comp_file="$HOME/.cerefox-local-completion.$comp_shell" + if docker exec -e CEREFOX_PROG_NAME=cerefox-local "$CONTAINER" cerefox completion "$comp_shell" \ + > "$comp_file" 2>/dev/null && [ -s "$comp_file" ]; then + if [ "$comp_shell" = "fish" ]; then + fishdir="$HOME/.config/fish/completions" + if mkdir -p "$fishdir" 2>/dev/null && cp "$comp_file" "$fishdir/cerefox-local.fish" 2>/dev/null; then + COMPLETION_MSG=" ✓ shell completion (fish) installed — restart fish to activate." + fi + else + rc="$HOME/.${comp_shell}rc" + marker="# >>> cerefox-local shell completion >>>" + if [ -f "$rc" ] && grep -qF "$marker" "$rc" 2>/dev/null; then + COMPLETION_MSG=" ✓ shell completion already wired (in $rc)." + else + printf '\n%s\n[ -s "%s" ] && source "%s"\n# <<< cerefox-local shell completion <<<\n' \ + "$marker" "$comp_file" "$comp_file" >> "$rc" 2>/dev/null || true + if grep -qF "$marker" "$rc" 2>/dev/null; then + COMPLETION_MSG=" ✓ shell completion installed → activate now: exec $comp_shell" + fi + fi + fi + fi + ;; +esac + echo "✓ Cerefox Local Server → http://localhost:$PORT/app/" echo " Command: cerefox-local (installed at $BIN_DIR/cerefox-local)" echo " e.g. cerefox-local status | search \"…\" | document ingest notes.md" @@ -179,3 +212,4 @@ if [ -n "${OPENAI_API_KEY:-}" ]; then else echo " ▸ Set your OpenAI key to enable ingest + search: cerefox-local init" fi +[ -n "$COMPLETION_MSG" ] && echo "$COMPLETION_MSG" diff --git a/docs/guides/setup-local.md b/docs/guides/setup-local.md index 9d99730..5ba470c 100644 --- a/docs/guides/setup-local.md +++ b/docs/guides/setup-local.md @@ -54,6 +54,9 @@ The web UI is now at **http://localhost:8000/app/** — **or the port the instal (it auto-steps to 8010/8020/… if 8000 is busy or you also run the cloud `cerefox web`, which defaults to 8000). The installer prints the actual URL; `cerefox-local status` shows it too. +The installer also wires **shell tab-completion** for `cerefox-local` (best-effort) — run +`exec $SHELL` or open a new terminal to activate it. + **How the credential works:** the container generates its own JWT secret on first boot and mints the access token internally — the token never leaves the container. The only secret stored on your host is `OPENAI_API_KEY` (in `~/.cerefox/local/.env`), so `upgrade` can