codex-dev-mcp-suite 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -1,19 +1,42 @@
1
1
  # All network features are OPTIONAL. Without them, recall falls back to keyword
2
2
  # search (fully offline). Any OpenAI-compatible endpoint works: OpenAI, Ollama,
3
- # LM Studio, OpenRouter, vLLM, LiteLLM, 9router, etc.
3
+ # LM Studio, Groq, Cerebras, OpenRouter, vLLM, LiteLLM, 9router, etc.
4
+
5
+ # Hard no-network mode. When true, disables embeddings/rerank even if keys exist.
6
+ # Accepted true values: true, 1, yes, on.
7
+ # MCP_DETERMINISTIC_FALLBACK=true
8
+
9
+ # Optional provider chain for chat/rerank. Configure only primary, or add
10
+ # numbered fallbacks. Groq/Cerebras/OpenRouter are recommended examples; any
11
+ # OpenAI-compatible base URL + API key + model ID works.
12
+ # MCP_PROVIDER_PRIMARY=groq
13
+ # MCP_PROVIDER_PRIMARY_BASE_URL=https://api.groq.com/openai/v1
14
+ # MCP_PROVIDER_PRIMARY_API_KEY=<groq-key>
15
+ # MCP_PROVIDER_PRIMARY_MODEL=llama-3.3-70b-versatile
16
+ # MCP_PROVIDER_CHAIN2=cerebras
17
+ # MCP_PROVIDER_CHAIN2_BASE_URL=https://api.cerebras.ai/v1
18
+ # MCP_PROVIDER_CHAIN2_API_KEY=<cerebras-key>
19
+ # MCP_PROVIDER_CHAIN2_MODEL=llama-3.3-70b
20
+ # MCP_PROVIDER_CHAIN3=openrouter
21
+ # MCP_PROVIDER_CHAIN3_BASE_URL=https://openrouter.ai/api/v1
22
+ # MCP_PROVIDER_CHAIN3_API_KEY=<openrouter-key>
23
+ # MCP_PROVIDER_CHAIN3_MODEL=openai/gpt-4o-mini
24
+
25
+ # Cooldown (ms) before retrying a provider after a 429/5xx/timeout failure.
26
+ # MCP_PROVIDER_COOLDOWN_MS=60000
4
27
 
5
28
  # Base URL of an OpenAI-compatible API (must expose /v1/...)
6
- LLM_BASE_URL=http://localhost:11434/v1
29
+ MCP_LLM_BASE_URL=http://localhost:11434/v1
7
30
  # API key for that endpoint (leave blank for local servers that don't need one)
8
- LLM_API_KEY=
31
+ MCP_LLM_API_KEY=
9
32
 
10
33
  # Optional: semantic embedding recall (needs /v1/embeddings)
11
- EMBED_MODEL=nomic-embed-text
34
+ MCP_EMBED_MODEL=nomic-embed-text
12
35
  EMBED_TIMEOUT_MS=12000
13
36
 
14
37
  # Optional: LLM rerank recall (needs /v1/chat/completions)
15
38
  # Used automatically when embeddings are unavailable.
16
- RERANK_MODEL=llama3.1:8b
39
+ MCP_RERANK_MODEL=llama3.1:8b
17
40
  RERANK_TIMEOUT_MS=30000
18
41
  RERANK_ENABLED=1
19
42
 
@@ -22,5 +45,6 @@ RERANK_ENABLED=1
22
45
  # JOURNAL_DIR=~/.codex/memories/journal
23
46
  # CHECKPOINT_DIR=~/.codex/memories/checkpoints
24
47
 
25
- # Backward-compatible aliases (still honored): NINEROUTER_URL, NINEROUTER_KEY,
26
- # EMBED_URL, EMBED_KEY, RERANK_URL, RERANK_KEY
48
+ # Backward-compatible aliases (still honored): LLM_BASE_URL, LLM_API_KEY,
49
+ # NINEROUTER_URL, NINEROUTER_KEY, EMBED_MODEL, RERANK_MODEL, EMBED_URL,
50
+ # EMBED_KEY, RERANK_URL, RERANK_KEY
package/CHANGELOG.md ADDED
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ ## 1.2.0 - 2026-06-15
4
+
5
+ ### Added
6
+ - CLI meta handling for all bins: `--help`, `--version`, and `--doctor` (config diagnostics with API keys redacted) before the stdio server starts.
7
+ - Provider diagnostics `providerChainDiagnostics()` that lists active providers with redacted keys and flags incomplete numbered slots.
8
+ - Per-provider cooldown on `429`/`5xx`/timeout/network failures via `MCP_PROVIDER_COOLDOWN_MS` (default `60000`).
9
+ - CI job that packs the tarball, installs it in a temp dir, and runs each bin `--version`.
10
+
11
+ ### Changed
12
+ - Rerank now skips providers that are cooling down and records success/failure outcomes.
13
+ - Rerank never logs API keys or response bodies.
14
+
15
+ ## 1.1.0 - 2026-06-15
16
+
17
+ ### Added
18
+ - Added optional numbered provider chain for chat/rerank fallback: `MCP_PROVIDER_PRIMARY_*`, `MCP_PROVIDER_CHAIN2_*`, `MCP_PROVIDER_CHAIN3_*`, and higher.
19
+ - Added provider-chain tests for ordered fallback, incomplete slot skipping, deterministic disable, and legacy fallback.
20
+
21
+ ## 1.0.1 - 2026-06-15
22
+
23
+ ### Added
24
+ - Added neutral `MCP_*` environment variables for model configuration:
25
+ - `MCP_LLM_BASE_URL`, `MCP_LLM_API_KEY`
26
+ - `MCP_RERANK_BASE_URL`, `MCP_RERANK_API_KEY`, `MCP_RERANK_MODEL`
27
+ - `MCP_EMBED_BASE_URL`, `MCP_EMBED_API_KEY`, `MCP_EMBED_MODEL`
28
+ - Added client-agnostic docs:
29
+ - `docs/configuration.md`
30
+ - `docs/clients/codex.md`
31
+ - `docs/clients/claude-code.md`
32
+ - `docs/clients/generic-mcp.md`
33
+ - Added tests proving `MCP_*` env vars override legacy aliases.
34
+ - Added `MCP_DETERMINISTIC_FALLBACK` hard no-network mode for deterministic local-only recall.
35
+ - Added `docs/privacy.md` covering local storage, data flow, and remote provider boundaries.
36
+ - Added provider guidance for Groq, Cerebras, and OpenRouter as optional external endpoints.
37
+
38
+ ### Changed
39
+ - README now presents the project as **Dev MCP Suite** while keeping the npm package name `codex-dev-mcp-suite` for compatibility.
40
+ - Rerank and embedding helpers now describe any OpenAI-compatible endpoint, not 9Router only.
41
+ - `.env.example` now recommends `MCP_*` names for new installs.
42
+ - `memory_recall`, `journal_search`, and `memory_reindex` now explicitly respect deterministic no-network mode.
43
+
44
+ ### Compatibility
45
+ - Legacy env names remain supported: `LLM_*`, `NINEROUTER_*`, `EMBED_*`, and `RERANK_*`.
46
+ - Offline keyword search remains the default if no model endpoint is configured.
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
- # Codex Dev MCP Suite
1
+ # Dev MCP Suite
2
2
 
3
3
  [![CI](https://github.com/verrysimatupang99/codex-dev-mcp-suite/actions/workflows/ci.yml/badge.svg)](https://github.com/verrysimatupang99/codex-dev-mcp-suite/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/codex-dev-mcp-suite.svg)](https://www.npmjs.com/package/codex-dev-mcp-suite)
5
+
6
+ Published as `codex-dev-mcp-suite` for backward compatibility.
4
7
 
5
8
  Four local, file-based MCP servers for solo developers and vibecoders who keep
6
9
  losing context when sessions hit "input too long" / get compacted / restart.
@@ -8,7 +11,8 @@ Stop re-pasting context across sessions.
8
11
 
9
12
  All servers are **local, dependency-light (only the MCP SDK), and split storage
10
13
  per-project** by the working directory. Works with any MCP-capable client
11
- (Codex CLI, Claude Code, Cursor, Cline, etc.).
14
+ (Codex CLI, Claude Code, Cursor, Cline, Gemini-compatible launchers, Hermes,
15
+ or any stdio MCP host).
12
16
 
13
17
  ## The four servers
14
18
 
@@ -28,11 +32,46 @@ Recall auto-selects the best available mode:
28
32
  3. **keyword** — always-available offline fallback
29
33
 
30
34
  All network features degrade gracefully: no endpoint = keyword mode, never an error.
35
+ Use the neutral `MCP_*` environment variables for new installs; legacy
36
+ `NINEROUTER_*` and `LLM_*` variables are still supported. See
37
+ [`docs/configuration.md`](docs/configuration.md).
38
+
39
+ Need hard local-only behavior? Set `MCP_DETERMINISTIC_FALLBACK=true` to disable
40
+ embeddings/rerank even if model keys are present; results are labeled
41
+ `[deterministic]`.
31
42
 
32
43
  ## Install
33
44
 
34
45
  Requires Node.js >= 18.
35
46
 
47
+ ### Quickest: run via npx (no clone)
48
+
49
+ Point your MCP client at the published package — no install step needed:
50
+
51
+ ```toml
52
+ # Codex CLI ~/.codex/config.toml
53
+ [mcp_servers.project-memory]
54
+ command = "npx"
55
+ args = ["-y", "-p", "codex-dev-mcp-suite", "project-memory-mcp"]
56
+
57
+ [mcp_servers.devjournal]
58
+ command = "npx"
59
+ args = ["-y", "-p", "codex-dev-mcp-suite", "devjournal-mcp"]
60
+
61
+ [mcp_servers.checkpoint]
62
+ command = "npx"
63
+ args = ["-y", "-p", "codex-dev-mcp-suite", "checkpoint-mcp"]
64
+
65
+ [mcp_servers.context-pack]
66
+ command = "npx"
67
+ args = ["-y", "-p", "codex-dev-mcp-suite", "context-pack-mcp"]
68
+ ```
69
+
70
+ Or install globally: `npm i -g codex-dev-mcp-suite` → commands
71
+ `project-memory-mcp`, `devjournal-mcp`, `checkpoint-mcp`, `context-pack-mcp`.
72
+
73
+ ### From source
74
+
36
75
  ```bash
37
76
  git clone https://github.com/<you>/codex-dev-mcp-suite.git
38
77
  cd codex-dev-mcp-suite
@@ -42,6 +81,12 @@ for s in project-memory checkpoint context-pack devjournal; do (cd "$s" && npm i
42
81
 
43
82
  ### Register with your MCP client
44
83
 
84
+ Client-specific examples:
85
+
86
+ - [Codex CLI](docs/clients/codex.md)
87
+ - [Claude Code](docs/clients/claude-code.md)
88
+ - [Generic MCP JSON](docs/clients/generic-mcp.md)
89
+
45
90
  **Codex CLI** (`~/.codex/config.toml`):
46
91
 
47
92
  ```toml
@@ -50,10 +95,10 @@ command = "node"
50
95
  args = ["/abs/path/codex-dev-mcp-suite/project-memory/server.js"]
51
96
  [mcp_servers.project-memory.env]
52
97
  MEMORY_VAULT_DIR = "~/.codex/memories/vault"
53
- # optional recall upgrades (see .env.example)
54
- # LLM_BASE_URL = "http://localhost:11434/v1" # any OpenAI-compatible endpoint
55
- # LLM_API_KEY = "..."
56
- # RERANK_MODEL = "llama3.1:8b"
98
+ # optional recall upgrades (see .env.example and docs/configuration.md)
99
+ # MCP_LLM_BASE_URL = "http://localhost:11434/v1" # any OpenAI-compatible endpoint
100
+ # MCP_LLM_API_KEY = "..."
101
+ # MCP_RERANK_MODEL = "llama3.1:8b"
57
102
 
58
103
  [mcp_servers.checkpoint]
59
104
  command = "node"
@@ -70,8 +115,10 @@ args = ["/abs/path/codex-dev-mcp-suite/devjournal/server.js"]
70
115
  JOURNAL_DIR = "~/.codex/memories/journal"
71
116
  ```
72
117
 
73
- **Claude Code / Cursor / Cline** (`mcpServers` JSON): same idea — `command: "node"`,
74
- `args: ["/abs/path/<server>/server.js"]`, optional `env`.
118
+ **Claude Code / Cursor / Cline / other MCP clients** (`mcpServers` JSON): same
119
+ idea use the npm bin commands (`project-memory-mcp`, `devjournal-mcp`,
120
+ `checkpoint-mcp`, `context-pack-mcp`) or `node /abs/path/<server>/server.js`,
121
+ with optional `env`.
75
122
 
76
123
  ## Daily workflow
77
124
 
@@ -100,8 +147,10 @@ cd project-memory && npm test
100
147
 
101
148
  ## Privacy
102
149
 
103
- Everything is stored as plain files on your machine. No data leaves your
104
- computer unless you explicitly configure an embeddings/rerank endpoint. Your
150
+ Local-first by default: no hosted backend, no built-in telemetry, no project
151
+ account system. Everything is stored as files on your machine. No data leaves
152
+ your machine unless you explicitly configure an external model/API endpoint for
153
+ rerank or embeddings. See [privacy and data flow](docs/privacy.md). Your
105
154
  personal vault/journal/checkpoints are gitignored.
106
155
 
107
156
  ## License
package/bin/_meta.mjs ADDED
@@ -0,0 +1,69 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ function pkgVersion() {
8
+ try {
9
+ const raw = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8");
10
+ return JSON.parse(raw).version || "unknown";
11
+ } catch { return "unknown"; }
12
+ }
13
+
14
+ function redactKey(name) {
15
+ const v = String(process.env[name] || "");
16
+ return v ? `set (${v.length} chars)` : "not set";
17
+ }
18
+
19
+ function doctorLines(meta) {
20
+ const lines = [];
21
+ lines.push(`${meta.title} doctor`);
22
+ lines.push(`version: ${pkgVersion()}`);
23
+ lines.push(`node: ${process.version}`);
24
+ for (const [label, name] of meta.storage || []) {
25
+ lines.push(`${label}: ${process.env[name] || meta.storageDefaults?.[name] || "(default)"}`);
26
+ }
27
+ const det = /^(1|true|yes|on)$/i.test(String(process.env.MCP_DETERMINISTIC_FALLBACK || "").trim());
28
+ lines.push(`deterministic no-network: ${det ? "on" : "off"}`);
29
+ if (meta.usesModel) {
30
+ lines.push("model/provider config (keys redacted):");
31
+ lines.push(` MCP_LLM_BASE_URL: ${process.env.MCP_LLM_BASE_URL || "(unset)"}`);
32
+ lines.push(` MCP_LLM_API_KEY: ${redactKey("MCP_LLM_API_KEY")}`);
33
+ lines.push(` MCP_PROVIDER_PRIMARY: ${process.env.MCP_PROVIDER_PRIMARY || "(unset)"}`);
34
+ lines.push(` MCP_PROVIDER_PRIMARY_API_KEY: ${redactKey("MCP_PROVIDER_PRIMARY_API_KEY")}`);
35
+ lines.push(` MCP_EMBED_API_KEY: ${redactKey("MCP_EMBED_API_KEY")}`);
36
+ lines.push(` MCP_RERANK_API_KEY: ${redactKey("MCP_RERANK_API_KEY")}`);
37
+ } else {
38
+ lines.push("model/provider config: not used by this server");
39
+ }
40
+ return lines;
41
+ }
42
+
43
+ export function handleCliMeta(meta) {
44
+ const argv = process.argv.slice(2);
45
+ if (argv.includes("-v") || argv.includes("--version")) {
46
+ process.stdout.write(`${pkgVersion()}\n`);
47
+ process.exit(0);
48
+ }
49
+ if (argv.includes("-h") || argv.includes("--help")) {
50
+ const out = [
51
+ `${meta.title} (${meta.bin}) v${pkgVersion()}`,
52
+ "",
53
+ "An MCP server that speaks JSON-RPC over stdio. Launch it from an MCP client.",
54
+ "",
55
+ "Usage:",
56
+ ` ${meta.bin} start the MCP stdio server`,
57
+ ` ${meta.bin} --version print version`,
58
+ ` ${meta.bin} --doctor print config diagnostics (API keys redacted)`,
59
+ ` ${meta.bin} --help show this help`,
60
+ ];
61
+ process.stdout.write(out.join("\n") + "\n");
62
+ process.exit(0);
63
+ }
64
+ if (argv.includes("--doctor")) {
65
+ process.stdout.write(doctorLines(meta).join("\n") + "\n");
66
+ process.exit(0);
67
+ }
68
+ return false;
69
+ }
@@ -1,2 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import "../checkpoint/server.js";
2
+ import { handleCliMeta } from "./_meta.mjs";
3
+ handleCliMeta({
4
+ bin: "checkpoint-mcp",
5
+ title: "Checkpoint MCP",
6
+ usesModel: false,
7
+ storage: [["store", "CHECKPOINT_DIR"]],
8
+ });
9
+ import("../checkpoint/server.js");
@@ -1,2 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import "../context-pack/server.js";
2
+ import { handleCliMeta } from "./_meta.mjs";
3
+ handleCliMeta({
4
+ bin: "context-pack-mcp",
5
+ title: "Context Pack MCP",
6
+ usesModel: false,
7
+ storage: [],
8
+ });
9
+ import("../context-pack/server.js");
@@ -1,2 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import "../devjournal/server.js";
2
+ import { handleCliMeta } from "./_meta.mjs";
3
+ handleCliMeta({
4
+ bin: "devjournal-mcp",
5
+ title: "Dev Journal MCP",
6
+ usesModel: true,
7
+ storage: [["store", "JOURNAL_DIR"]],
8
+ });
9
+ import("../devjournal/server.js");
@@ -1,2 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import "../project-memory/server.js";
2
+ import { handleCliMeta } from "./_meta.mjs";
3
+ handleCliMeta({
4
+ bin: "project-memory-mcp",
5
+ title: "Project Memory MCP",
6
+ usesModel: true,
7
+ storage: [["vault", "MEMORY_VAULT_DIR"]],
8
+ });
9
+ import("../project-memory/server.js");
@@ -0,0 +1,3 @@
1
+ export function deterministicEnabled() {
2
+ return /^(1|true|yes|on)$/i.test(String(process.env.MCP_DETERMINISTIC_FALLBACK || "").trim());
3
+ }
@@ -0,0 +1,109 @@
1
+ import { deterministicEnabled } from "./env.js";
2
+
3
+ const DEFAULT_BASE = "http://localhost:20128";
4
+ const DEFAULT_MODEL = "kr/claude-haiku-4.5";
5
+
6
+ function value(name) {
7
+ return String(process.env[name] || "").trim();
8
+ }
9
+
10
+ export function redactKey(key) {
11
+ const k = String(key || "");
12
+ return k ? `set (${k.length} chars)` : "not set";
13
+ }
14
+
15
+ function providerEnv(prefix) {
16
+ const label = value(prefix);
17
+ const base = value(`${prefix}_BASE_URL`);
18
+ const key = value(`${prefix}_API_KEY`);
19
+ const model = value(`${prefix}_MODEL`);
20
+ if (!label || !base || !key || !model) return null;
21
+ return { label, base, key, model };
22
+ }
23
+
24
+ function slotIssues(prefix) {
25
+ const present = ["", "_BASE_URL", "_API_KEY", "_MODEL"].some((s) => value(`${prefix}${s}`));
26
+ if (!present) return null;
27
+ const missing = [];
28
+ if (!value(prefix)) missing.push(`${prefix}`);
29
+ if (!value(`${prefix}_BASE_URL`)) missing.push(`${prefix}_BASE_URL`);
30
+ if (!value(`${prefix}_API_KEY`)) missing.push(`${prefix}_API_KEY`);
31
+ if (!value(`${prefix}_MODEL`)) missing.push(`${prefix}_MODEL`);
32
+ return missing.length ? { prefix, missing } : null;
33
+ }
34
+
35
+ function numberedPrefixes() {
36
+ const prefixes = ["MCP_PROVIDER_PRIMARY"];
37
+ const nums = Object.keys(process.env)
38
+ .map((name) => name.match(/^MCP_PROVIDER_CHAIN(\d+)/)?.[1])
39
+ .filter(Boolean)
40
+ .map(Number)
41
+ .filter((n) => n >= 2);
42
+ for (const n of [...new Set(nums)].sort((a, b) => a - b)) {
43
+ prefixes.push(`MCP_PROVIDER_CHAIN${n}`);
44
+ }
45
+ return prefixes;
46
+ }
47
+
48
+ function numberedProviders() {
49
+ const providers = [];
50
+ for (const prefix of numberedPrefixes()) {
51
+ const provider = providerEnv(prefix);
52
+ if (provider) providers.push(provider);
53
+ }
54
+ return providers;
55
+ }
56
+
57
+ function legacyProvider() {
58
+ const base = value("MCP_RERANK_BASE_URL") || value("MCP_LLM_BASE_URL") || value("LLM_BASE_URL") || value("RERANK_URL") || value("NINEROUTER_URL") || DEFAULT_BASE;
59
+ const key = value("MCP_RERANK_API_KEY") || value("MCP_LLM_API_KEY") || value("LLM_API_KEY") || value("RERANK_KEY") || value("NINEROUTER_KEY");
60
+ const model = value("MCP_RERANK_MODEL") || value("RERANK_MODEL") || DEFAULT_MODEL;
61
+ if (!key) return null;
62
+ return { label: "legacy", base, key, model };
63
+ }
64
+
65
+ export function providerChainConfig() {
66
+ const deterministic = deterministicEnabled();
67
+ const rerankEnabled = process.env.RERANK_ENABLED !== "0";
68
+ if (deterministic || !rerankEnabled) return { providers: [], enabled: false, deterministic };
69
+ const providers = numberedProviders();
70
+ if (providers.length === 0) {
71
+ const legacy = legacyProvider();
72
+ if (legacy) providers.push(legacy);
73
+ }
74
+ return { providers, enabled: providers.length > 0, deterministic };
75
+ }
76
+
77
+ export function providerChainDiagnostics() {
78
+ const cfg = providerChainConfig();
79
+ const issues = [];
80
+ for (const prefix of numberedPrefixes()) {
81
+ const issue = slotIssues(prefix);
82
+ if (issue) issues.push(issue);
83
+ }
84
+ return {
85
+ deterministic: cfg.deterministic,
86
+ enabled: cfg.enabled,
87
+ providers: cfg.providers.map((p) => ({ label: p.label, base: p.base, model: p.model, apiKey: redactKey(p.key) })),
88
+ issues,
89
+ };
90
+ }
91
+
92
+ const COOLDOWN_MS = Number(process.env.MCP_PROVIDER_COOLDOWN_MS || 60000);
93
+ const cooldowns = new Map();
94
+
95
+ export function isCoolingDown(key, now = Date.now()) {
96
+ const until = cooldowns.get(key);
97
+ if (!until) return false;
98
+ if (now >= until) { cooldowns.delete(key); return false; }
99
+ return true;
100
+ }
101
+
102
+ export function recordOutcome(key, ok, now = Date.now()) {
103
+ if (ok) { cooldowns.delete(key); return; }
104
+ cooldowns.set(key, now + COOLDOWN_MS);
105
+ }
106
+
107
+ export function _resetCooldowns() {
108
+ cooldowns.clear();
109
+ }
@@ -1,29 +1,44 @@
1
1
  /**
2
- * LLM reranker via 9router chat models (e.g. Kiro kr/claude-haiku-4.5).
3
- * Used when embeddings are unavailable: keyword prefilter -> LLM picks the
4
- * most relevant candidates. Degrades gracefully: returns null on any failure
5
- * so callers fall back to keyword ordering. Never throws.
2
+ * LLM reranker via any OpenAI-compatible /v1/chat/completions model.
3
+ * Tries provider chain slots in order, skipping any currently cooling down
4
+ * after a recent failure. Degrades gracefully: returns null on any failure so
5
+ * callers fall back to keyword ordering. Never throws. Never logs keys/bodies.
6
6
  */
7
7
  import http from "http";
8
8
  import https from "https";
9
9
  import { URL } from "url";
10
+ import { deterministicEnabled } from "./env.js";
11
+ import { providerChainConfig, isCoolingDown, recordOutcome } from "./provider-chain.js";
10
12
 
11
- const BASE = process.env.LLM_BASE_URL || process.env.RERANK_URL || process.env.NINEROUTER_URL || "http://localhost:20128";
12
- const KEY = process.env.LLM_API_KEY || process.env.RERANK_KEY || process.env.NINEROUTER_KEY || "";
13
- const MODEL = process.env.RERANK_MODEL || "kr/claude-haiku-4.5";
14
13
  const TIMEOUT_MS = Number(process.env.RERANK_TIMEOUT_MS || 30000);
15
14
  const ENABLED = process.env.RERANK_ENABLED !== "0";
16
15
 
17
16
  export function rerankConfig() {
18
- return { base: BASE, model: MODEL, enabled: Boolean(KEY) && ENABLED };
17
+ const cfg = providerChainConfig();
18
+ const first = cfg.providers[0] || {};
19
+ return {
20
+ base: first.base,
21
+ model: first.model,
22
+ providers: cfg.providers.map(({ label, base, model }) => ({ label, base, model })),
23
+ enabled: ENABLED && cfg.enabled && !deterministicEnabled(),
24
+ deterministic: deterministicEnabled(),
25
+ };
19
26
  }
20
27
 
21
- function chat(messages) {
28
+ function providerKey(provider) {
29
+ return `${provider.label}|${provider.base}|${provider.model}`;
30
+ }
31
+
32
+ /**
33
+ * Resolves { content, retryable }. retryable=true means try the next provider
34
+ * (timeout, network error, 429, or 5xx). retryable=false with null content
35
+ * means a definitive non-retryable response (e.g. 4xx auth).
36
+ */
37
+ function chatWithProvider(provider, messages) {
22
38
  return new Promise((resolve) => {
23
- if (!KEY || !ENABLED) return resolve(null);
24
39
  let u;
25
- try { u = new URL("/v1/chat/completions", BASE); } catch { return resolve(null); }
26
- const payload = Buffer.from(JSON.stringify({ model: MODEL, stream: false, temperature: 0, max_tokens: 200, messages }));
40
+ try { u = new URL("/v1/chat/completions", provider.base); } catch { return resolve({ content: null, retryable: false }); }
41
+ const payload = Buffer.from(JSON.stringify({ model: provider.model, stream: false, temperature: 0, max_tokens: 200, messages }));
27
42
  const lib = u.protocol === "https:" ? https : http;
28
43
  const req = lib.request(
29
44
  {
@@ -31,18 +46,21 @@ function chat(messages) {
31
46
  port: u.port || (u.protocol === "https:" ? 443 : 80),
32
47
  path: u.pathname,
33
48
  method: "POST",
34
- headers: { "Content-Type": "application/json", "Content-Length": payload.length, Authorization: `Bearer ${KEY}` },
49
+ headers: { "Content-Type": "application/json", "Content-Length": payload.length, Authorization: `Bearer ${provider.key}` },
35
50
  timeout: TIMEOUT_MS,
36
51
  },
37
52
  (res) => {
38
53
  let body = "";
39
54
  res.on("data", (c) => (body += c));
40
55
  res.on("end", () => {
41
- if (res.statusCode !== 200) return resolve(null);
42
- // handle plain JSON or SSE stream just in case
56
+ const status = res.statusCode || 0;
57
+ if (status !== 200) {
58
+ const retryable = status === 429 || status >= 500;
59
+ return resolve({ content: null, retryable });
60
+ }
43
61
  try {
44
62
  const j = JSON.parse(body);
45
- return resolve(j?.choices?.[0]?.message?.content ?? null);
63
+ return resolve({ content: j?.choices?.[0]?.message?.content ?? null, retryable: false });
46
64
  } catch { /* maybe SSE */ }
47
65
  try {
48
66
  let acc = "";
@@ -54,24 +72,37 @@ function chat(messages) {
54
72
  const j = JSON.parse(d);
55
73
  acc += j?.choices?.[0]?.delta?.content || j?.choices?.[0]?.message?.content || "";
56
74
  }
57
- return resolve(acc || null);
58
- } catch { return resolve(null); }
75
+ return resolve({ content: acc || null, retryable: false });
76
+ } catch { return resolve({ content: null, retryable: true }); }
59
77
  });
60
78
  }
61
79
  );
62
- req.on("error", () => resolve(null));
63
- req.on("timeout", () => { req.destroy(); resolve(null); });
80
+ req.on("error", () => resolve({ content: null, retryable: true }));
81
+ req.on("timeout", () => { req.destroy(); resolve({ content: null, retryable: true }); });
64
82
  req.write(payload);
65
83
  req.end();
66
84
  });
67
85
  }
68
86
 
87
+ async function chat(messages) {
88
+ if (!ENABLED || deterministicEnabled()) return null;
89
+ const { providers } = providerChainConfig();
90
+ for (const provider of providers) {
91
+ const key = providerKey(provider);
92
+ if (isCoolingDown(key)) continue;
93
+ const { content, retryable } = await chatWithProvider(provider, messages);
94
+ if (content) { recordOutcome(key, true); return content; }
95
+ if (retryable) recordOutcome(key, false);
96
+ }
97
+ return null;
98
+ }
99
+
69
100
  /**
70
101
  * Given a query and candidates [{id, title, snippet}], ask the model to return
71
102
  * the ids ordered by relevance. Returns an array of ids, or null on failure.
72
103
  */
73
104
  export async function rerank(query, candidates, topK = 5) {
74
- if (!KEY || !ENABLED || !candidates || candidates.length === 0) return null;
105
+ if (!ENABLED || deterministicEnabled() || !candidates || candidates.length === 0 || !providerChainConfig().enabled) return null;
75
106
  const list = candidates.map((c, i) => `[${i + 1}] (id:${c.id}) ${c.title}\n ${String(c.snippet || "").replace(/\s+/g, " ").slice(0, 200)}`).join("\n");
76
107
  const sys = "You are a search reranker. Given a user query and a numbered list of notes, return the most relevant notes ordered best-first. Respond with ONLY a comma-separated list of the bracket numbers, e.g. 3,1,5. No prose.";
77
108
  const user = `Query: ${query}\n\nNotes:\n${list}\n\nReturn the top ${topK} bracket numbers, best first, comma-separated:`;
@@ -32,6 +32,7 @@ import path from "path";
32
32
  import os from "os";
33
33
  import crypto from "crypto";
34
34
  import { rerank, rerankConfig } from "./rerank.js";
35
+ import { deterministicEnabled } from "./env.js";
35
36
 
36
37
  const ROOT =
37
38
  process.env.JOURNAL_DIR ||
@@ -272,7 +273,7 @@ class DevJournalServer {
272
273
  let ranked = entries.map((e, i) => ({ e, i, sc: score(e) }))
273
274
  .sort((a, b) => b.sc - a.sc || b.i - a.i);
274
275
  let pool = ranked.filter((x) => x.sc > 0).slice(0, 20);
275
- let mode = "keyword";
276
+ let mode = deterministicEnabled() ? "deterministic" : "keyword";
276
277
  if (pool.length === 0 && rerankConfig().enabled) {
277
278
  pool = ranked.slice(0, 20); // fall back to recent for semantic-style queries
278
279
  }
@@ -0,0 +1,46 @@
1
+ # Claude Code Setup
2
+
3
+ Install globally:
4
+
5
+ ```bash
6
+ npm i -g codex-dev-mcp-suite
7
+ ```
8
+
9
+ Add MCP servers using your Claude Code MCP configuration flow. Generic JSON shape:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "project-memory": {
15
+ "command": "project-memory-mcp",
16
+ "env": {
17
+ "MEMORY_VAULT_DIR": "~/.local/share/dev-mcp-suite/memories/vault",
18
+ "MCP_LLM_BASE_URL": "http://localhost:11434/v1",
19
+ "MCP_LLM_API_KEY": "",
20
+ "MCP_RERANK_MODEL": "llama3.1:8b",
21
+ "MCP_EMBED_MODEL": "nomic-embed-text"
22
+ }
23
+ },
24
+ "devjournal": {
25
+ "command": "devjournal-mcp",
26
+ "env": {
27
+ "JOURNAL_DIR": "~/.local/share/dev-mcp-suite/memories/journal",
28
+ "MCP_LLM_BASE_URL": "http://localhost:11434/v1",
29
+ "MCP_LLM_API_KEY": "",
30
+ "MCP_RERANK_MODEL": "llama3.1:8b"
31
+ }
32
+ },
33
+ "checkpoint": {
34
+ "command": "checkpoint-mcp",
35
+ "env": {
36
+ "CHECKPOINT_DIR": "~/.local/share/dev-mcp-suite/memories/checkpoints"
37
+ }
38
+ },
39
+ "context-pack": {
40
+ "command": "context-pack-mcp"
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ If you do not configure model env vars, `memory_recall` and `journal_search` still work in offline keyword mode.