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 +31 -7
- package/CHANGELOG.md +46 -0
- package/README.md +59 -10
- package/bin/_meta.mjs +69 -0
- package/bin/checkpoint.mjs +8 -1
- package/bin/context-pack.mjs +8 -1
- package/bin/devjournal.mjs +8 -1
- package/bin/project-memory.mjs +8 -1
- package/devjournal/env.js +3 -0
- package/devjournal/provider-chain.js +109 -0
- package/devjournal/rerank.js +52 -21
- package/devjournal/server.js +2 -1
- package/docs/clients/claude-code.md +46 -0
- package/docs/clients/codex.md +39 -0
- package/docs/clients/generic-mcp.md +45 -0
- package/docs/configuration.md +123 -0
- package/docs/privacy.md +39 -0
- package/package.json +18 -8
- package/project-memory/embedding.js +7 -6
- package/project-memory/env.js +3 -0
- package/project-memory/provider-chain.js +109 -0
- package/project-memory/rerank.js +52 -21
- package/project-memory/server.js +4 -2
- package/_testkit/harness.mjs +0 -126
- package/checkpoint/test.mjs +0 -73
- package/context-pack/test.mjs +0 -66
- package/devjournal/test.mjs +0 -68
- package/project-memory/test.mjs +0 -72
- package/project-memory/test.rerank.mjs +0 -53
- package/run-tests.mjs +0 -42
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
|
-
|
|
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
|
-
|
|
31
|
+
MCP_LLM_API_KEY=
|
|
9
32
|
|
|
10
33
|
# Optional: semantic embedding recall (needs /v1/embeddings)
|
|
11
|
-
|
|
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
|
-
|
|
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):
|
|
26
|
-
#
|
|
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
|
-
#
|
|
1
|
+
# Dev MCP Suite
|
|
2
2
|
|
|
3
3
|
[](https://github.com/verrysimatupang99/codex-dev-mcp-suite/actions/workflows/ci.yml)
|
|
4
|
+
[](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,
|
|
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
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
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
|
|
74
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
+
}
|
package/bin/checkpoint.mjs
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "
|
|
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");
|
package/bin/context-pack.mjs
CHANGED
package/bin/devjournal.mjs
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "
|
|
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");
|
package/bin/project-memory.mjs
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "
|
|
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,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
|
+
}
|
package/devjournal/rerank.js
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LLM reranker via
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
|
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",
|
|
26
|
-
const payload = Buffer.from(JSON.stringify({ model:
|
|
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 ${
|
|
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
|
-
|
|
42
|
-
|
|
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 (!
|
|
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:`;
|
package/devjournal/server.js
CHANGED
|
@@ -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.
|