codex-dev-mcp-suite 1.2.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/CHANGELOG.md +21 -0
- package/README.md +111 -0
- package/bin/prune.mjs +158 -0
- package/bin/stats.mjs +101 -0
- package/lib/prune.js +114 -0
- package/lib/stats.js +269 -0
- package/package.json +8 -4
- package/project-memory/server.js +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
3
24
|
## 1.2.0 - 2026-06-15
|
|
4
25
|
|
|
5
26
|
### 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/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
|
+
}
|
package/lib/prune.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev MCP Suite — prune library.
|
|
3
|
+
*
|
|
4
|
+
* Identifies and removes "temp" project slugs (prefix `tmp.`) from all three
|
|
5
|
+
* stores. Used by `bin/prune.mjs`. No MCP, no stdio — safe to import from tests.
|
|
6
|
+
*
|
|
7
|
+
* Storage layout (matches the rest of the suite):
|
|
8
|
+
* <root>/vault/<slug>/
|
|
9
|
+
* <root>/journal/<slug>/
|
|
10
|
+
* <root>/checkpoints/<slug>/
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
|
|
16
|
+
export const STORES = ["vault", "journal", "checkpoints"];
|
|
17
|
+
const TEMP_PREFIX = "tmp.";
|
|
18
|
+
|
|
19
|
+
/** Returns the list of slug names matching temp pattern across all stores. */
|
|
20
|
+
export function findTempSlugs({ root }) {
|
|
21
|
+
const set = new Set();
|
|
22
|
+
for (const store of STORES) {
|
|
23
|
+
let entries = [];
|
|
24
|
+
try {
|
|
25
|
+
entries = fs.readdirSync(path.join(root, store), { withFileTypes: true });
|
|
26
|
+
} catch { continue; }
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
if (e.isDirectory() && e.name.startsWith(TEMP_PREFIX)) set.add(e.name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...set].sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Recursively sum bytes under each slug's directories across stores. */
|
|
35
|
+
export function estimateBytes({ root, slugs }) {
|
|
36
|
+
let total = 0;
|
|
37
|
+
for (const slug of slugs) {
|
|
38
|
+
for (const store of STORES) {
|
|
39
|
+
total += dirSize(path.join(root, store, slug));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return total;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function dirSize(p) {
|
|
46
|
+
let total = 0;
|
|
47
|
+
let stack = [p];
|
|
48
|
+
while (stack.length) {
|
|
49
|
+
const cur = stack.pop();
|
|
50
|
+
let stat;
|
|
51
|
+
try { stat = fs.lstatSync(cur); } catch { continue; }
|
|
52
|
+
if (stat.isDirectory()) {
|
|
53
|
+
let children = [];
|
|
54
|
+
try { children = fs.readdirSync(cur); } catch { continue; }
|
|
55
|
+
for (const c of children) stack.push(path.join(cur, c));
|
|
56
|
+
} else {
|
|
57
|
+
total += stat.size;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return total;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Validate slugs only contain temp prefix — refuse to delete otherwise. */
|
|
64
|
+
function assertTempSlugs(slugs) {
|
|
65
|
+
for (const s of slugs) {
|
|
66
|
+
if (!s.startsWith(TEMP_PREFIX)) {
|
|
67
|
+
throw new Error(`refusing to remove non-temp slug: "${s}" (must start with "${TEMP_PREFIX}")`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Compute a plan: per-slug list of stores where the dir exists + total bytes. */
|
|
73
|
+
export function planPrune({ root }) {
|
|
74
|
+
const slugs = findTempSlugs({ root });
|
|
75
|
+
return slugs.map((slug) => {
|
|
76
|
+
const stores = [];
|
|
77
|
+
for (const store of STORES) {
|
|
78
|
+
const p = path.join(root, store, slug);
|
|
79
|
+
try {
|
|
80
|
+
if (fs.existsSync(p)) stores.push(store);
|
|
81
|
+
} catch { /* ignore */ }
|
|
82
|
+
}
|
|
83
|
+
const bytes = estimateBytes({ root, slugs: [slug] });
|
|
84
|
+
return { slug, stores, bytes };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Remove temp slugs from all stores. Default is dry-run (no deletion).
|
|
90
|
+
* @param {{ root: string, slugs: string[], dryRun?: boolean }} opts
|
|
91
|
+
* @returns {{ removed: string[], skipped: string[], errors: Array<{slug:string,store:string,error:string}> }}
|
|
92
|
+
*/
|
|
93
|
+
export function applyPrune({ root, slugs, dryRun = true }) {
|
|
94
|
+
assertTempSlugs(slugs);
|
|
95
|
+
const removed = [];
|
|
96
|
+
const skipped = [];
|
|
97
|
+
const errors = [];
|
|
98
|
+
for (const slug of slugs) {
|
|
99
|
+
let anyRemoved = false;
|
|
100
|
+
for (const store of STORES) {
|
|
101
|
+
const p = path.join(root, store, slug);
|
|
102
|
+
if (!fs.existsSync(p)) continue;
|
|
103
|
+
if (dryRun) { skipped.push(slug); continue; }
|
|
104
|
+
try {
|
|
105
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
106
|
+
anyRemoved = true;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
errors.push({ slug, store, error: String(e && e.message || e) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (anyRemoved) removed.push(slug);
|
|
112
|
+
}
|
|
113
|
+
return { removed, skipped, errors };
|
|
114
|
+
}
|
package/lib/stats.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev MCP Suite — stats aggregation library.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for summarizing local memory storage across the three stores
|
|
5
|
+
* (vault / journal / checkpoints). No MCP, no stdio — used by bin/stats.mjs
|
|
6
|
+
* and safe to import from tests.
|
|
7
|
+
*
|
|
8
|
+
* Storage layout assumed (see each MCP server's server.js for source of truth):
|
|
9
|
+
* <root>/vault/<slug>/notes/*.md (one .md per memory_save note)
|
|
10
|
+
* <root>/journal/<slug>/journal.jsonl (append-only entries)
|
|
11
|
+
* <root>/journal/<slug>/handoff.json (optional)
|
|
12
|
+
* <root>/checkpoints/<slug>/snapshots/<id>/ (one dir per snapshot)
|
|
13
|
+
* <root>/checkpoints/<slug>/manifest.json (checkpoint index)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
|
|
19
|
+
const VAULT_DIRNAME = "vault";
|
|
20
|
+
const JOURNAL_DIRNAME = "journal";
|
|
21
|
+
const CHECKPOINT_DIRNAME = "checkpoints";
|
|
22
|
+
|
|
23
|
+
function safeListDirs(parent) {
|
|
24
|
+
try {
|
|
25
|
+
return fs
|
|
26
|
+
.readdirSync(parent, { withFileTypes: true })
|
|
27
|
+
.filter((d) => d.isDirectory())
|
|
28
|
+
.map((d) => d.name);
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safeListFiles(parent) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.readdirSync(parent);
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function newestMtimeMs(dir) {
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.statSync(dir);
|
|
45
|
+
return stat.mtimeMs;
|
|
46
|
+
} catch {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isoFromMs(ms) {
|
|
52
|
+
if (!ms) return null;
|
|
53
|
+
return new Date(ms).toISOString();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isTempSlug(slug) {
|
|
57
|
+
return /^tmp\./.test(slug);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function summarizeProject(slug, { root }) {
|
|
61
|
+
const vaultDir = path.join(root, VAULT_DIRNAME, slug);
|
|
62
|
+
const journalDir = path.join(root, JOURNAL_DIRNAME, slug);
|
|
63
|
+
const cpDir = path.join(root, CHECKPOINT_DIRNAME, slug);
|
|
64
|
+
|
|
65
|
+
const notesDir = path.join(vaultDir, "notes");
|
|
66
|
+
const notes = safeListFiles(notesDir).filter((f) => f.endsWith(".md")).length;
|
|
67
|
+
|
|
68
|
+
let journalEntries = 0;
|
|
69
|
+
let lastEntryTs = null;
|
|
70
|
+
const jl = path.join(journalDir, "journal.jsonl");
|
|
71
|
+
if (fs.existsSync(jl)) {
|
|
72
|
+
try {
|
|
73
|
+
const lines = fs.readFileSync(jl, "utf8").split("\n").filter(Boolean);
|
|
74
|
+
journalEntries = lines.length;
|
|
75
|
+
// journal entries are append-only and roughly time-ordered; parse first + last
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
try {
|
|
78
|
+
const obj = JSON.parse(line);
|
|
79
|
+
if (obj && obj.ts) {
|
|
80
|
+
if (!lastEntryTs || obj.ts > lastEntryTs) lastEntryTs = obj.ts;
|
|
81
|
+
}
|
|
82
|
+
} catch { /* skip malformed */ }
|
|
83
|
+
}
|
|
84
|
+
} catch { /* ignore read errors */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// checkpoints: count snapshot dirs
|
|
88
|
+
let checkpoints = 0;
|
|
89
|
+
const snapsDir = path.join(cpDir, "snapshots");
|
|
90
|
+
if (fs.existsSync(snapsDir)) {
|
|
91
|
+
checkpoints = safeListDirs(snapsDir).length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// last activity across all artifacts (mtime fallback)
|
|
95
|
+
let lastActivityMs = 0;
|
|
96
|
+
for (const d of [vaultDir, journalDir, cpDir]) {
|
|
97
|
+
const m = newestMtimeMs(d);
|
|
98
|
+
if (m > lastActivityMs) lastActivityMs = m;
|
|
99
|
+
}
|
|
100
|
+
// prefer parsed entry ts if newer than filesystem mtime
|
|
101
|
+
if (lastEntryTs) {
|
|
102
|
+
const t = Date.parse(lastEntryTs);
|
|
103
|
+
if (!Number.isNaN(t) && t > lastActivityMs) lastActivityMs = t;
|
|
104
|
+
}
|
|
105
|
+
const lastActivityTs = isoFromMs(lastActivityMs);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
slug,
|
|
109
|
+
notes,
|
|
110
|
+
journalEntries,
|
|
111
|
+
checkpoints,
|
|
112
|
+
lastActivityTs,
|
|
113
|
+
temp: isTempSlug(slug),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Compute aggregate stats for a memory root.
|
|
119
|
+
* @param {object} opts
|
|
120
|
+
* @param {string} opts.root - absolute path to memories root (containing vault/, journal/, checkpoints/)
|
|
121
|
+
* @param {number} [opts.topLimit=10] - max entries in topByNotes / recentActivity
|
|
122
|
+
* @returns {{
|
|
123
|
+
* root: string,
|
|
124
|
+
* totals: { notes: number, journalProjects: number, checkpoints: number },
|
|
125
|
+
* projectCount: number,
|
|
126
|
+
* topByNotes: Array<{ slug: string, notes: number }>,
|
|
127
|
+
* recentActivity: Array<{ slug: string, ts: string }>,
|
|
128
|
+
* tempSlugs: string[],
|
|
129
|
+
* perProject: Array<{ slug: string, notes: number, journalEntries: number, checkpoints: number, lastActivityTs: string|null, temp: boolean }>,
|
|
130
|
+
* }}
|
|
131
|
+
*/
|
|
132
|
+
export function computeStats({ root, topLimit = 10 } = {}) {
|
|
133
|
+
if (!root) throw new Error("computeStats: `root` is required");
|
|
134
|
+
|
|
135
|
+
// union of slugs across all three stores
|
|
136
|
+
const slugSet = new Set();
|
|
137
|
+
for (const store of [VAULT_DIRNAME, JOURNAL_DIRNAME, CHECKPOINT_DIRNAME]) {
|
|
138
|
+
for (const slug of safeListDirs(path.join(root, store))) slugSet.add(slug);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const perProject = [...slugSet]
|
|
142
|
+
.map((slug) => summarizeProject(slug, { root }))
|
|
143
|
+
.sort((a, b) => {
|
|
144
|
+
if (a.temp !== b.temp) return a.temp ? 1 : -1; // real first
|
|
145
|
+
return a.slug.localeCompare(b.slug);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const totals = perProject.reduce(
|
|
149
|
+
(acc, p) => ({
|
|
150
|
+
notes: acc.notes + p.notes,
|
|
151
|
+
journalProjects: acc.journalProjects + (p.journalEntries > 0 ? 1 : 0),
|
|
152
|
+
checkpoints: acc.checkpoints + p.checkpoints,
|
|
153
|
+
}),
|
|
154
|
+
{ notes: 0, journalProjects: 0, checkpoints: 0 },
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const topByNotes = [...perProject]
|
|
158
|
+
.filter((p) => p.notes > 0)
|
|
159
|
+
.sort((a, b) => b.notes - a.notes || a.slug.localeCompare(b.slug))
|
|
160
|
+
.slice(0, topLimit)
|
|
161
|
+
.map((p) => ({ slug: p.slug, notes: p.notes }));
|
|
162
|
+
|
|
163
|
+
const recentActivity = [...perProject]
|
|
164
|
+
.filter((p) => p.lastActivityTs && !p.temp)
|
|
165
|
+
.sort((a, b) => (a.lastActivityTs < b.lastActivityTs ? 1 : -1))
|
|
166
|
+
.slice(0, topLimit)
|
|
167
|
+
.map((p) => ({ slug: p.slug, ts: p.lastActivityTs }));
|
|
168
|
+
|
|
169
|
+
const tempSlugs = perProject.filter((p) => p.temp).map((p) => p.slug).sort();
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
root,
|
|
173
|
+
totals,
|
|
174
|
+
projectCount: perProject.length,
|
|
175
|
+
topByNotes,
|
|
176
|
+
recentActivity,
|
|
177
|
+
tempSlugs,
|
|
178
|
+
perProject,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Right-pad a slug to a fixed display width. */
|
|
183
|
+
function pad(s, w) {
|
|
184
|
+
s = String(s);
|
|
185
|
+
if (s.length >= w) return s + " ";
|
|
186
|
+
return s + " ".repeat(w - s.length);
|
|
187
|
+
}
|
|
188
|
+
function rpadNum(n, w) {
|
|
189
|
+
const s = String(n);
|
|
190
|
+
return s.length >= w ? s : " ".repeat(w - s.length) + s;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatLocal(ts) {
|
|
194
|
+
if (!ts) return "—";
|
|
195
|
+
try {
|
|
196
|
+
return new Date(ts).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
197
|
+
} catch {
|
|
198
|
+
return ts;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Human-readable text summary.
|
|
204
|
+
* @param {ReturnType<typeof computeStats>} stats
|
|
205
|
+
* @param {object} [opts]
|
|
206
|
+
* @param {string} [opts.timezone="UTC"] - display-only label, not used to convert
|
|
207
|
+
*/
|
|
208
|
+
export function formatText(stats, opts = {}) {
|
|
209
|
+
const lines = [];
|
|
210
|
+
lines.push("Dev MCP Suite — stats");
|
|
211
|
+
lines.push("======================");
|
|
212
|
+
lines.push(`Storage root: ${stats.root}`);
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push("Totals");
|
|
215
|
+
lines.push("------");
|
|
216
|
+
lines.push(` Notes: ${stats.totals.notes}`);
|
|
217
|
+
lines.push(` Journal projects:${rpadNum(stats.totals.journalProjects, 2)}`);
|
|
218
|
+
lines.push(` Checkpoints: ${stats.totals.checkpoints}`);
|
|
219
|
+
lines.push(` Distinct projects: ${stats.projectCount}`);
|
|
220
|
+
lines.push("");
|
|
221
|
+
|
|
222
|
+
if (stats.topByNotes.length) {
|
|
223
|
+
lines.push(`Top projects by notes (top ${stats.topByNotes.length})`);
|
|
224
|
+
lines.push("------------------------");
|
|
225
|
+
const slugW = Math.max(8, ...stats.topByNotes.map((p) => p.slug.length));
|
|
226
|
+
for (const p of stats.topByNotes) {
|
|
227
|
+
lines.push(` ${rpadNum(p.notes, 4)} ${pad(p.slug, slugW)}`);
|
|
228
|
+
}
|
|
229
|
+
lines.push("");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (stats.recentActivity.length) {
|
|
233
|
+
lines.push("Most recent activity");
|
|
234
|
+
lines.push("--------------------");
|
|
235
|
+
const slugW = Math.max(8, ...stats.recentActivity.map((p) => p.slug.length));
|
|
236
|
+
for (const p of stats.recentActivity) {
|
|
237
|
+
lines.push(` ${pad(p.slug, slugW)} ${formatLocal(p.ts)}`);
|
|
238
|
+
}
|
|
239
|
+
lines.push("");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (stats.tempSlugs.length) {
|
|
243
|
+
lines.push(`Temp/cleanup candidates (${stats.tempSlugs.length})`);
|
|
244
|
+
lines.push("------------------------");
|
|
245
|
+
for (const slug of stats.tempSlugs) lines.push(` ${slug}`);
|
|
246
|
+
lines.push("");
|
|
247
|
+
} else {
|
|
248
|
+
lines.push("Temp/cleanup candidates: none");
|
|
249
|
+
lines.push("");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
lines.push(`Generated at ${new Date().toISOString()}`);
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Stable JSON serialization (sorted keys for top-level + totals). */
|
|
257
|
+
export function formatJson(stats) {
|
|
258
|
+
const out = {
|
|
259
|
+
root: stats.root,
|
|
260
|
+
totals: stats.totals,
|
|
261
|
+
projectCount: stats.projectCount,
|
|
262
|
+
topByNotes: stats.topByNotes,
|
|
263
|
+
recentActivity: stats.recentActivity,
|
|
264
|
+
tempSlugs: stats.tempSlugs,
|
|
265
|
+
perProject: stats.perProject,
|
|
266
|
+
generatedAt: new Date().toISOString(),
|
|
267
|
+
};
|
|
268
|
+
return JSON.stringify(out, null, 2);
|
|
269
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-dev-mcp-suite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Four local, file-based MCP servers for solo devs/vibecoders: persistent project memory, session handoff/resume, git-independent file checkpoints, and token-efficient project briefings. Works with any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,10 +32,13 @@
|
|
|
32
32
|
"project-memory-mcp": "bin/project-memory.mjs",
|
|
33
33
|
"devjournal-mcp": "bin/devjournal.mjs",
|
|
34
34
|
"checkpoint-mcp": "bin/checkpoint.mjs",
|
|
35
|
-
"context-pack-mcp": "bin/context-pack.mjs"
|
|
35
|
+
"context-pack-mcp": "bin/context-pack.mjs",
|
|
36
|
+
"stats": "bin/stats.mjs",
|
|
37
|
+
"prune": "bin/prune.mjs"
|
|
36
38
|
},
|
|
37
39
|
"scripts": {
|
|
38
|
-
"test": "node run-tests.mjs"
|
|
40
|
+
"test": "node run-tests.mjs",
|
|
41
|
+
"test:stats": "node tests/stats.test.mjs"
|
|
39
42
|
},
|
|
40
43
|
"dependencies": {
|
|
41
44
|
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
@@ -70,6 +73,7 @@
|
|
|
70
73
|
"CHANGELOG.md",
|
|
71
74
|
"README.md",
|
|
72
75
|
"LICENSE",
|
|
73
|
-
".env.example"
|
|
76
|
+
".env.example",
|
|
77
|
+
"lib/*.js"
|
|
74
78
|
]
|
|
75
79
|
}
|
package/project-memory/server.js
CHANGED
|
@@ -29,6 +29,7 @@ import crypto from "crypto";
|
|
|
29
29
|
import { embed, embedOne, cosine, embeddingConfig } from "./embedding.js";
|
|
30
30
|
import { rerank, rerankConfig } from "./rerank.js";
|
|
31
31
|
import { deterministicEnabled } from "./env.js";
|
|
32
|
+
import { computeStats, formatText, formatJson } from "../lib/stats.js";
|
|
32
33
|
|
|
33
34
|
const VAULT_ROOT =
|
|
34
35
|
process.env.MEMORY_VAULT_DIR ||
|
|
@@ -220,6 +221,18 @@ class ProjectMemoryServer {
|
|
|
220
221
|
},
|
|
221
222
|
},
|
|
222
223
|
},
|
|
224
|
+
{
|
|
225
|
+
name: "memory_stats",
|
|
226
|
+
description: "Summarize local memory storage across vault / journal / checkpoints: totals, top projects, recent activity, and temp-slug cleanup candidates. Returns the same text as the `stats` CLI.",
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
root: { type: "string", description: "Storage root (default: ~/.codex/memories or first env var of MEMORY_VAULT_DIR / JOURNAL_DIR / CHECKPOINT_DIR)" },
|
|
231
|
+
top: { type: "number", description: "How many entries in top-project / recent-activity lists (default 10)", default: 10 },
|
|
232
|
+
json: { type: "boolean", description: "Return machine-readable JSON instead of human text", default: false },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
223
236
|
],
|
|
224
237
|
}));
|
|
225
238
|
|
|
@@ -233,6 +246,7 @@ class ProjectMemoryServer {
|
|
|
233
246
|
case "memory_get": return await this.get(args || {});
|
|
234
247
|
case "memory_delete": return await this.del(args || {});
|
|
235
248
|
case "memory_reindex": return await this.reindex(args || {});
|
|
249
|
+
case "memory_stats": return await this.stats(args || {});
|
|
236
250
|
default: throw new Error(`Unknown tool: ${name}`);
|
|
237
251
|
}
|
|
238
252
|
} catch (error) {
|
|
@@ -439,6 +453,21 @@ class ProjectMemoryServer {
|
|
|
439
453
|
return { content: [{ type: "text", text: `Reindex ${p.slug}: embedded ${done}, failed ${failed} (embeddings ${embeddingConfig().enabled ? "configured" : "not configured"}). ${failed ? "Failures usually mean the embedding model is unavailable right now." : ""}` }] };
|
|
440
454
|
}
|
|
441
455
|
|
|
456
|
+
async stats({ root: explicitRoot, top = 10, json = false } = {}) {
|
|
457
|
+
const root = explicitRoot
|
|
458
|
+
? path.resolve(explicitRoot)
|
|
459
|
+
: (process.env.MEMORY_VAULT_DIR
|
|
460
|
+
? path.dirname(process.env.MEMORY_VAULT_DIR)
|
|
461
|
+
: process.env.JOURNAL_DIR
|
|
462
|
+
? path.dirname(process.env.JOURNAL_DIR)
|
|
463
|
+
: process.env.CHECKPOINT_DIR
|
|
464
|
+
? path.dirname(process.env.CHECKPOINT_DIR)
|
|
465
|
+
: path.join(os.homedir(), ".codex", "memories"));
|
|
466
|
+
const stats = computeStats({ root, topLimit: top });
|
|
467
|
+
const text = json ? formatJson(stats) : formatText(stats);
|
|
468
|
+
return { content: [{ type: "text", text }] };
|
|
469
|
+
}
|
|
470
|
+
|
|
442
471
|
async run() {
|
|
443
472
|
const transport = new StdioServerTransport();
|
|
444
473
|
await this.server.connect(transport);
|