codex-dev-mcp-suite 1.1.0 → 1.3.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
@@ -22,6 +22,9 @@
22
22
  # MCP_PROVIDER_CHAIN3_API_KEY=<openrouter-key>
23
23
  # MCP_PROVIDER_CHAIN3_MODEL=openai/gpt-4o-mini
24
24
 
25
+ # Cooldown (ms) before retrying a provider after a 429/5xx/timeout failure.
26
+ # MCP_PROVIDER_COOLDOWN_MS=60000
27
+
25
28
  # Base URL of an OpenAI-compatible API (must expose /v1/...)
26
29
  MCP_LLM_BASE_URL=http://localhost:11434/v1
27
30
  # API key for that endpoint (leave blank for local servers that don't need one)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0 - 2026-06-17
4
+
5
+ ### Added
6
+ - New `stats` CLI: summarize local memory storage across vault / journal / checkpoints. Shows totals, top projects by notes, most recent activity, and temp-slug cleanup candidates.
7
+ - Usage: `npx -y -p codex-dev-mcp-suite stats` (or globally `stats`)
8
+ - Flags: `--root <path>`, `--json`, `--top N`, `--help`, `--version`
9
+ - Pure-function library at `lib/stats.js` (no MCP / no stdio); tested offline.
10
+ - New top-level test runner hook: `tests/*.test.mjs` are auto-picked-up by `node run-tests.mjs`.
11
+
12
+
13
+
14
+
15
+ ### Added
16
+ - New `prune` CLI: remove temp project slugs (prefix `tmp.`) from vault, journal, and checkpoints. Default is DRY-RUN — nothing is deleted without `--yes`.
17
+ - Usage: `npx -y -p codex-dev-mcp-suite prune` (or globally `prune`)
18
+ - Flags: `--root <path>`, `--yes`, `--json`, `--help`, `--version`
19
+ - Pure-function library at `lib/prune.js`; tested offline.
20
+ - Safety: refuses to delete any slug that does not start with `tmp.`.
21
+ - New MCP tool `memory_stats` on the project-memory server: returns the same summary as the `stats` CLI. Useful for in-session introspection from an MCP client.
22
+ - Args: `root?`, `top?` (default 10), `json?` (default false).
23
+
24
+ ## 1.2.0 - 2026-06-15
25
+
26
+ ### Added
27
+ - CLI meta handling for all bins: `--help`, `--version`, and `--doctor` (config diagnostics with API keys redacted) before the stdio server starts.
28
+ - Provider diagnostics `providerChainDiagnostics()` that lists active providers with redacted keys and flags incomplete numbered slots.
29
+ - Per-provider cooldown on `429`/`5xx`/timeout/network failures via `MCP_PROVIDER_COOLDOWN_MS` (default `60000`).
30
+ - CI job that packs the tarball, installs it in a temp dir, and runs each bin `--version`.
31
+
32
+ ### Changed
33
+ - Rerank now skips providers that are cooling down and records success/failure outcomes.
34
+ - Rerank never logs API keys or response bodies.
35
+
3
36
  ## 1.1.0 - 2026-06-15
4
37
 
5
38
  ### Added
package/README.md CHANGED
@@ -2,6 +2,9 @@
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
4
  [![npm version](https://img.shields.io/npm/v/codex-dev-mcp-suite.svg)](https://www.npmjs.com/package/codex-dev-mcp-suite)
5
+ [![npm downloads](https://img.shields.io/npm/dm/codex-dev-mcp-suite.svg)](https://www.npmjs.com/package/codex-dev-mcp-suite)
6
+ [![license](https://img.shields.io/npm/l/codex-dev-mcp-suite.svg)](./LICENSE)
7
+ [![node](https://img.shields.io/node/v/codex-dev-mcp-suite.svg)](https://nodejs.org)
5
8
 
6
9
  Published as `codex-dev-mcp-suite` for backward compatibility.
7
10
 
@@ -14,6 +17,51 @@ per-project** by the working directory. Works with any MCP-capable client
14
17
  (Codex CLI, Claude Code, Cursor, Cline, Gemini-compatible launchers, Hermes,
15
18
  or any stdio MCP host).
16
19
 
20
+ ## Quickstart
21
+
22
+ Try it in 10 seconds (no clone, no config):
23
+
24
+ ```bash
25
+ npx -y -p codex-dev-mcp-suite project-memory-mcp --help
26
+ ```
27
+
28
+ Local-first by default: no hosted backend, no telemetry. Works offline with
29
+ keyword recall; model/provider config is optional. For strict no-network mode,
30
+ set `MCP_DETERMINISTIC_FALLBACK=true`.
31
+
32
+ Register all four servers with an MCP client (Codex CLI shown):
33
+
34
+ ```toml
35
+ # ~/.codex/config.toml
36
+ [mcp_servers.project-memory]
37
+ command = "npx"
38
+ args = ["-y", "-p", "codex-dev-mcp-suite", "project-memory-mcp"]
39
+
40
+ [mcp_servers.devjournal]
41
+ command = "npx"
42
+ args = ["-y", "-p", "codex-dev-mcp-suite", "devjournal-mcp"]
43
+
44
+ [mcp_servers.checkpoint]
45
+ command = "npx"
46
+ args = ["-y", "-p", "codex-dev-mcp-suite", "checkpoint-mcp"]
47
+
48
+ [mcp_servers.context-pack]
49
+ command = "npx"
50
+ args = ["-y", "-p", "codex-dev-mcp-suite", "context-pack-mcp"]
51
+ ```
52
+
53
+ Inspect any server without starting it:
54
+
55
+ ```bash
56
+ project-memory-mcp --version
57
+ project-memory-mcp --doctor # config diagnostics; API keys redacted
58
+ ```
59
+
60
+ Other clients (Claude Code, Cursor, Cline, ...): see
61
+ [`docs/clients/`](docs/clients/). Full env reference:
62
+ [`docs/configuration.md`](docs/configuration.md). Data flow:
63
+ [`docs/privacy.md`](docs/privacy.md).
64
+
17
65
  ## The four servers
18
66
 
19
67
  | Server | What it does | Key tools |
@@ -138,6 +186,69 @@ node backfill-sessions-v2.mjs --dry --min-prompts 2 # preview
138
186
  node backfill-sessions-v2.mjs --min-prompts 2 # import
139
187
  ```
140
188
 
189
+ ## Stats CLI
190
+
191
+ A read-only summary of your local memory storage — totals, top projects,
192
+ recent activity, and temp-slug cleanup candidates.
193
+
194
+ ```bash
195
+ npx -y -p codex-dev-mcp-suite stats # human-readable
196
+ npx -y -p codex-dev-mcp-suite stats --json # machine-readable
197
+ npx -y -p codex-dev-mcp-suite stats --root /tmp/mem # different root
198
+ npx -y -p codex-dev-mcp-suite stats --top 5 # trim top lists
199
+ ```
200
+
201
+ Example output:
202
+
203
+ ```
204
+ Dev MCP Suite — stats
205
+ ======================
206
+ Storage root: /home/you/.codex/memories
207
+
208
+ Totals
209
+ ------
210
+ Notes: 89
211
+ Journal projects:19
212
+ Checkpoints: 1
213
+ Distinct projects: 25
214
+
215
+ Top projects by notes (top 10)
216
+ ------------------------
217
+ 24 mrtrickster99-fd1ff0fa
218
+ 14 Coding-17e063ef
219
+ ...
220
+
221
+ Temp/cleanup candidates (2)
222
+ ------------------------
223
+ tmp.itbtDn9eB3-b62798fd
224
+ tmp.iHqM2Uh5K2-8d3660a2
225
+ ```
226
+
227
+ The CLI is a thin wrapper around `lib/stats.js`, which is also importable
228
+ directly from your own scripts. The default storage root is
229
+ `~/.codex/memories`, but `--root` (or the existing `MEMORY_VAULT_DIR` /
230
+ `JOURNAL_DIR` / `CHECKPOINT_DIR` env vars) override it.
231
+
232
+
233
+ ## Prune CLI
234
+
235
+ Remove temp project slugs (prefix `tmp.`) from vault / journal / checkpoints.
236
+ Default is DRY-RUN — nothing is deleted without `--yes`.
237
+
238
+ ```bash
239
+ npx -y -p codex-dev-mcp-suite prune # dry-run report
240
+ npx -y -p codex-dev-mcp-suite prune --yes # actually delete
241
+ npx -y -p codex-dev-mcp-suite prune --json # machine-readable
242
+ npx -y -p codex-dev-mcp-suite prune --root /tmp/mem # different root
243
+ ```
244
+
245
+ Safety: refuses to delete any slug that does not start with `tmp.`. Useful
246
+ right after heavy refactors or after imports that left tmp scratch dirs.
247
+
248
+ For in-session use from an MCP client, the project-memory server exposes a
249
+ `memory_stats` tool that returns the same summary text (or JSON) as the `stats`
250
+ CLI above.
251
+
141
252
  ## Tests
142
253
 
143
254
  ```bash
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");
package/bin/prune.mjs ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bin/prune.mjs — Dev MCP Suite prune CLI.
4
+ *
5
+ * Removes temp project slugs (prefix `tmp.`) from all stores. Default is
6
+ * DRY-RUN — nothing is deleted unless you pass `--yes`.
7
+ *
8
+ * Usage:
9
+ * prune [--root <path>] [--yes] [--json]
10
+ * prune --help | --version
11
+ */
12
+ import fs from "fs";
13
+ import os from "os";
14
+ import path from "path";
15
+ import { fileURLToPath } from "url";
16
+ import { findTempSlugs, planPrune, applyPrune } from "../lib/prune.js";
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+
20
+ function pkgVersion() {
21
+ try {
22
+ return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version || "unknown";
23
+ } catch { return "unknown"; }
24
+ }
25
+
26
+ function resolveRoot() {
27
+ const fromFlag = process.env.PRUNE_ROOT;
28
+ if (fromFlag) return path.resolve(fromFlag);
29
+ for (const k of ["MEMORY_VAULT_DIR", "JOURNAL_DIR", "CHECKPOINT_DIR"]) {
30
+ const v = process.env[k];
31
+ if (v) {
32
+ const sub = k === "MEMORY_VAULT_DIR" ? "vault" : k === "JOURNAL_DIR" ? "journal" : "checkpoints";
33
+ const parent = path.dirname(v);
34
+ if (path.basename(parent) === sub) return parent;
35
+ return parent;
36
+ }
37
+ }
38
+ return path.join(os.homedir(), ".codex", "memories");
39
+ }
40
+
41
+ function parseArgs(argv) {
42
+ const opts = { root: null, yes: false, json: false, help: false, version: false };
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const a = argv[i];
45
+ if (a === "-h" || a === "--help") opts.help = true;
46
+ else if (a === "-v" || a === "--version") opts.version = true;
47
+ else if (a === "--yes" || a === "-y") opts.yes = true;
48
+ else if (a === "--json") opts.json = true;
49
+ else if (a === "--dry-run") opts.yes = false;
50
+ else if (a === "--root") opts.root = path.resolve(argv[++i]);
51
+ else if (a.startsWith("--root=")) opts.root = path.resolve(a.slice("--root=".length));
52
+ else if (!a.startsWith("-")) opts.root = path.resolve(a);
53
+ else {
54
+ process.stderr.write(`prune: unknown flag: ${a}\n`);
55
+ process.exit(2);
56
+ }
57
+ }
58
+ return opts;
59
+ }
60
+
61
+ function fmtBytes(n) {
62
+ if (n < 1024) return `${n} B`;
63
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
64
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
65
+ }
66
+
67
+ function printHelp() {
68
+ const out = [
69
+ `prune (Dev MCP Suite) v${pkgVersion()}`,
70
+ "",
71
+ "Remove temp project slugs (prefix tmp.) from vault, journal, and",
72
+ "checkpoints. Default is DRY-RUN — nothing is deleted without --yes.",
73
+ "",
74
+ "Usage:",
75
+ " prune [root] [--root <path>] [--yes] [--json]",
76
+ " prune --help | --version",
77
+ "",
78
+ "Options:",
79
+ " --yes, -y Actually delete. Without this, only a dry-run report is printed.",
80
+ " --dry-run Force dry-run mode (default).",
81
+ " --json Machine-readable JSON output.",
82
+ " --root <p> Storage root (default: ~/.codex/memories or first env var of",
83
+ " MEMORY_VAULT_DIR / JOURNAL_DIR / CHECKPOINT_DIR)",
84
+ " -h, --help",
85
+ " -v, --version",
86
+ "",
87
+ "Safety: refuses to delete any slug that does not start with `tmp.`.",
88
+ "",
89
+ "Examples:",
90
+ " prune # dry-run report",
91
+ " prune --yes # delete after seeing the plan",
92
+ " prune --yes --json # machine-readable delete report",
93
+ ];
94
+ process.stdout.write(out.join("\n") + "\n");
95
+ }
96
+
97
+ const opts = parseArgs(process.argv.slice(2));
98
+ if (opts.version) { process.stdout.write(`${pkgVersion()}\n`); process.exit(0); }
99
+ if (opts.help) { printHelp(); process.exit(0); }
100
+
101
+ const root = opts.root || resolveRoot();
102
+ const plan = planPrune({ root });
103
+
104
+ if (plan.length === 0) {
105
+ if (opts.json) {
106
+ process.stdout.write(JSON.stringify({ root, plan: [], removed: [], skipped: [], errors: [] }, null, 2) + "\n");
107
+ } else {
108
+ process.stdout.write(`No temp slugs found under ${root}.\n`);
109
+ }
110
+ process.exit(0);
111
+ }
112
+
113
+ if (!opts.yes) {
114
+ // dry-run report
115
+ if (opts.json) {
116
+ process.stdout.write(JSON.stringify({ root, dryRun: true, plan, removed: [], skipped: [], errors: [] }, null, 2) + "\n");
117
+ } else {
118
+ const lines = [];
119
+ lines.push(`Dev MCP Suite — prune (DRY RUN)`);
120
+ lines.push("=================================");
121
+ lines.push(`Storage root: ${root}`);
122
+ lines.push(`Found ${plan.length} temp slug${plan.length === 1 ? "" : "s"}:`);
123
+ let totalBytes = 0;
124
+ for (const e of plan) {
125
+ lines.push(` ${e.slug.padEnd(36)} ${fmtBytes(e.bytes).padStart(10)} stores: ${e.stores.join(",")}`);
126
+ totalBytes += e.bytes;
127
+ }
128
+ lines.push(` ${"TOTAL".padEnd(36)} ${fmtBytes(totalBytes).padStart(10)}`);
129
+ lines.push("");
130
+ lines.push("Re-run with `--yes` to actually delete.");
131
+ process.stdout.write(lines.join("\n") + "\n");
132
+ }
133
+ process.exit(0);
134
+ }
135
+
136
+ // actually delete
137
+ const slugs = plan.map((p) => p.slug);
138
+ const result = applyPrune({ root, slugs, dryRun: false });
139
+ const totalFreed = plan.reduce((a, p) => a + p.bytes, 0);
140
+
141
+ if (opts.json) {
142
+ process.stdout.write(JSON.stringify({ root, dryRun: false, plan, ...result, bytesFreed: totalFreed }, null, 2) + "\n");
143
+ } else {
144
+ const lines = [];
145
+ lines.push(`Dev MCP Suite — prune`);
146
+ lines.push("=====================");
147
+ lines.push(`Storage root: ${root}`);
148
+ lines.push(`Removed ${result.removed.length} slug${result.removed.length === 1 ? "" : "s"} (freed ~${fmtBytes(totalFreed)})`);
149
+ for (const slug of result.removed) lines.push(` ✓ ${slug}`);
150
+ if (result.errors.length) {
151
+ lines.push("");
152
+ lines.push(`Errors (${result.errors.length}):`);
153
+ for (const e of result.errors) lines.push(` ✗ ${e.slug} [${e.store}]: ${e.error}`);
154
+ }
155
+ process.stdout.write(lines.join("\n") + "\n");
156
+ }
157
+
158
+ process.exit(result.errors.length > 0 ? 1 : 0);
package/bin/stats.mjs ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bin/stats.mjs — Dev MCP Suite stats CLI.
4
+ *
5
+ * Usage:
6
+ * stats [--root <path>] [--json] [--top N] [--help] [--version]
7
+ *
8
+ * Default storage root: ~/.codex/memories (override via --root or
9
+ * MEMORY_VAULT_DIR / JOURNAL_DIR / CHECKPOINT_DIR siblings).
10
+ */
11
+ import fs from "fs";
12
+ import os from "os";
13
+ import path from "path";
14
+ import { fileURLToPath } from "url";
15
+ import { computeStats, formatText, formatJson } from "../lib/stats.js";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ function pkgVersion() {
20
+ try {
21
+ return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version || "unknown";
22
+ } catch { return "unknown"; }
23
+ }
24
+
25
+ function resolveRoot() {
26
+ // Precedence: explicit --root > vault env > journal env > checkpoint env > ~/.codex/memories
27
+ const fromFlag = process.env.STATS_ROOT;
28
+ if (fromFlag) return path.resolve(fromFlag);
29
+ for (const k of ["MEMORY_VAULT_DIR", "JOURNAL_DIR", "CHECKPOINT_DIR"]) {
30
+ const v = process.env[k];
31
+ if (v) {
32
+ // each server stores under <root>/<subdir>, so the root is the parent
33
+ const sub = k === "MEMORY_VAULT_DIR" ? "vault" : k === "JOURNAL_DIR" ? "journal" : "checkpoints";
34
+ const parent = path.dirname(v);
35
+ if (path.basename(parent) === sub) return parent;
36
+ return parent;
37
+ }
38
+ }
39
+ return path.join(os.homedir(), ".codex", "memories");
40
+ }
41
+
42
+ function parseArgs(argv) {
43
+ const opts = { root: null, json: false, top: 10, help: false, version: false };
44
+ for (let i = 0; i < argv.length; i++) {
45
+ const a = argv[i];
46
+ if (a === "-h" || a === "--help") opts.help = true;
47
+ else if (a === "-v" || a === "--version") opts.version = true;
48
+ else if (a === "--json") opts.json = true;
49
+ else if (a === "--root") opts.root = path.resolve(argv[++i]);
50
+ else if (a.startsWith("--root=")) opts.root = path.resolve(a.slice("--root=".length));
51
+ else if (a === "--top") opts.top = parseInt(argv[++i], 10) || 10;
52
+ else if (a.startsWith("--top=")) opts.top = parseInt(a.slice("--top=".length), 10) || 10;
53
+ else if (a === "--") { /* ignore rest */ break; }
54
+ else if (!a.startsWith("-")) opts.root = path.resolve(a);
55
+ else {
56
+ process.stderr.write(`stats: unknown flag: ${a}\n`);
57
+ process.exit(2);
58
+ }
59
+ }
60
+ return opts;
61
+ }
62
+
63
+ function printHelp() {
64
+ const out = [
65
+ `stats (Dev MCP Suite) v${pkgVersion()}`,
66
+ "",
67
+ "Summarize local memory storage (vault / journal / checkpoints).",
68
+ "",
69
+ "Usage:",
70
+ " stats [root] [--root <path>] [--json] [--top N]",
71
+ " stats --help | --version",
72
+ "",
73
+ "Options:",
74
+ " --root <path> Storage root (default: ~/.codex/memories or first env var of",
75
+ " MEMORY_VAULT_DIR / JOURNAL_DIR / CHECKPOINT_DIR)",
76
+ " --json Emit machine-readable JSON instead of human text",
77
+ " --top N How many entries in top-project / recent-activity lists",
78
+ " (default: 10)",
79
+ " -h, --help Show this help",
80
+ " -v, --version Print version",
81
+ "",
82
+ "Examples:",
83
+ " stats # summary of default storage",
84
+ " stats --json | jq .totals # totals only",
85
+ " stats --root /tmp/mem # use a different root",
86
+ ];
87
+ process.stdout.write(out.join("\n") + "\n");
88
+ }
89
+
90
+ const opts = parseArgs(process.argv.slice(2));
91
+ if (opts.version) { process.stdout.write(`${pkgVersion()}\n`); process.exit(0); }
92
+ if (opts.help) { printHelp(); process.exit(0); }
93
+
94
+ const root = opts.root || resolveRoot();
95
+ const stats = computeStats({ root, topLimit: opts.top });
96
+
97
+ if (opts.json) {
98
+ process.stdout.write(formatJson(stats) + "\n");
99
+ } else {
100
+ process.stdout.write(formatText(stats) + "\n");
101
+ }
@@ -7,6 +7,11 @@ function value(name) {
7
7
  return String(process.env[name] || "").trim();
8
8
  }
9
9
 
10
+ export function redactKey(key) {
11
+ const k = String(key || "");
12
+ return k ? `set (${k.length} chars)` : "not set";
13
+ }
14
+
10
15
  function providerEnv(prefix) {
11
16
  const label = value(prefix);
12
17
  const base = value(`${prefix}_BASE_URL`);
@@ -16,20 +21,34 @@ function providerEnv(prefix) {
16
21
  return { label, base, key, model };
17
22
  }
18
23
 
19
- function numberedProviders() {
20
- const providers = [];
21
- const primary = providerEnv("MCP_PROVIDER_PRIMARY");
22
- if (primary) providers.push(primary);
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
+ }
23
34
 
24
- const slots = Object.keys(process.env)
25
- .map((name) => name.match(/^MCP_PROVIDER_CHAIN(\d+)$/)?.[1])
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])
26
39
  .filter(Boolean)
27
40
  .map(Number)
28
- .filter((n) => n >= 2)
29
- .sort((a, b) => a - b);
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
+ }
30
47
 
31
- for (const n of slots) {
32
- const provider = providerEnv(`MCP_PROVIDER_CHAIN${n}`);
48
+ function numberedProviders() {
49
+ const providers = [];
50
+ for (const prefix of numberedPrefixes()) {
51
+ const provider = providerEnv(prefix);
33
52
  if (provider) providers.push(provider);
34
53
  }
35
54
  return providers;
@@ -54,3 +73,37 @@ export function providerChainConfig() {
54
73
  }
55
74
  return { providers, enabled: providers.length > 0, deterministic };
56
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
+ }