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 +3 -0
- package/CHANGELOG.md +33 -0
- package/README.md +111 -0
- 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/bin/prune.mjs +158 -0
- package/bin/stats.mjs +101 -0
- package/devjournal/provider-chain.js +63 -10
- package/devjournal/rerank.js +29 -13
- package/docs/configuration.md +14 -0
- package/lib/prune.js +114 -0
- package/lib/stats.js +269 -0
- package/package.json +9 -4
- package/project-memory/provider-chain.js +63 -10
- package/project-memory/rerank.js +29 -13
- package/project-memory/server.js +29 -0
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
|
[](https://github.com/verrysimatupang99/codex-dev-mcp-suite/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/codex-dev-mcp-suite)
|
|
5
|
+
[](https://www.npmjs.com/package/codex-dev-mcp-suite)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](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
|
+
}
|
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");
|
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
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
+
}
|