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/_testkit/harness.mjs
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal MCP stdio test harness — zero external deps.
|
|
3
|
-
* Provides: McpClient (spawn server, initialize, callTool), and a tiny
|
|
4
|
-
* assert/test runner (describe/it/run) with colored-ish plain output.
|
|
5
|
-
*/
|
|
6
|
-
import { spawn } from "child_process";
|
|
7
|
-
|
|
8
|
-
export class McpClient {
|
|
9
|
-
constructor(serverPath, env = {}) {
|
|
10
|
-
this.serverPath = serverPath;
|
|
11
|
-
this.env = { ...process.env, ...env };
|
|
12
|
-
this.proc = null;
|
|
13
|
-
this.buf = "";
|
|
14
|
-
this.pending = new Map();
|
|
15
|
-
this.nextId = 1;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async start() {
|
|
19
|
-
this.proc = spawn("node", [this.serverPath], { env: this.env, stdio: ["pipe", "pipe", "ignore"] });
|
|
20
|
-
this.proc.stdout.on("data", (d) => this._onData(d.toString()));
|
|
21
|
-
await this._rpc("initialize", {
|
|
22
|
-
protocolVersion: "2024-11-05",
|
|
23
|
-
capabilities: {},
|
|
24
|
-
clientInfo: { name: "testkit", version: "1.0.0" },
|
|
25
|
-
});
|
|
26
|
-
this._notify("notifications/initialized");
|
|
27
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
_onData(chunk) {
|
|
31
|
-
this.buf += chunk;
|
|
32
|
-
let idx;
|
|
33
|
-
while ((idx = this.buf.indexOf("\n")) !== -1) {
|
|
34
|
-
const line = this.buf.slice(0, idx).trim();
|
|
35
|
-
this.buf = this.buf.slice(idx + 1);
|
|
36
|
-
if (!line) continue;
|
|
37
|
-
let msg;
|
|
38
|
-
try { msg = JSON.parse(line); } catch { continue; }
|
|
39
|
-
if (msg.id != null && this.pending.has(msg.id)) {
|
|
40
|
-
const { resolve } = this.pending.get(msg.id);
|
|
41
|
-
this.pending.delete(msg.id);
|
|
42
|
-
resolve(msg);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
_send(obj) { this.proc.stdin.write(JSON.stringify(obj) + "\n"); }
|
|
48
|
-
_notify(method, params) { this._send({ jsonrpc: "2.0", method, params }); }
|
|
49
|
-
|
|
50
|
-
_rpc(method, params, timeoutMs = 30000) {
|
|
51
|
-
const id = this.nextId++;
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
const timer = setTimeout(() => {
|
|
54
|
-
this.pending.delete(id);
|
|
55
|
-
reject(new Error(`RPC timeout: ${method}`));
|
|
56
|
-
}, timeoutMs);
|
|
57
|
-
this.pending.set(id, { resolve: (m) => { clearTimeout(timer); resolve(m); } });
|
|
58
|
-
this._send({ jsonrpc: "2.0", id, method, params });
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async listTools() {
|
|
63
|
-
const res = await this._rpc("tools/list", {});
|
|
64
|
-
return (res.result?.tools || []).map((t) => t.name);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async callTool(name, args = {}, timeoutMs = 30000) {
|
|
68
|
-
const res = await this._rpc("tools/call", { name, arguments: args }, timeoutMs);
|
|
69
|
-
if (res.error) throw new Error(`RPC error: ${res.error.message}`);
|
|
70
|
-
const text = res.result?.content?.[0]?.text ?? "";
|
|
71
|
-
return { text, isError: Boolean(res.result?.isError), raw: res.result };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async stop() {
|
|
75
|
-
try { this.proc?.kill(); } catch { /* ignore */ }
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ---- tiny test runner ----
|
|
80
|
-
const tests = [];
|
|
81
|
-
let currentSuite = "";
|
|
82
|
-
export function describe(name, fn) { currentSuite = name; fn(); currentSuite = ""; }
|
|
83
|
-
export function it(name, fn) { tests.push({ suite: currentSuite, name, fn }); }
|
|
84
|
-
|
|
85
|
-
export function assert(cond, msg) {
|
|
86
|
-
if (!cond) throw new Error("assertion failed: " + (msg || ""));
|
|
87
|
-
}
|
|
88
|
-
export function assertIncludes(haystack, needle, msg) {
|
|
89
|
-
if (!String(haystack).includes(needle)) {
|
|
90
|
-
throw new Error(`expected to include "${needle}"${msg ? " — " + msg : ""}\n got: ${String(haystack).slice(0, 300)}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
export function assertEqual(a, b, msg) {
|
|
94
|
-
if (a !== b) throw new Error(`expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}${msg ? " — " + msg : ""}`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export async function run() {
|
|
98
|
-
let pass = 0, fail = 0;
|
|
99
|
-
const failures = [];
|
|
100
|
-
for (const t of tests) {
|
|
101
|
-
const label = t.suite ? `${t.suite} › ${t.name}` : t.name;
|
|
102
|
-
try {
|
|
103
|
-
await t.fn();
|
|
104
|
-
pass++;
|
|
105
|
-
console.log(` ✓ ${label}`);
|
|
106
|
-
} catch (e) {
|
|
107
|
-
fail++;
|
|
108
|
-
failures.push({ label, err: e });
|
|
109
|
-
console.log(` ✗ ${label}`);
|
|
110
|
-
console.log(` ${e.message.split("\n").join("\n ")}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
console.log(`\n${pass} passed, ${fail} failed (${tests.length} total)`);
|
|
114
|
-
if (fail > 0) process.exitCode = 1;
|
|
115
|
-
return { pass, fail, failures };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// unique temp dir helper
|
|
119
|
-
import os from "os";
|
|
120
|
-
import path from "path";
|
|
121
|
-
import fs from "fs";
|
|
122
|
-
export function tmpDir(prefix = "mcp-test-") {
|
|
123
|
-
const d = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
124
|
-
return d;
|
|
125
|
-
}
|
|
126
|
-
export function rmrf(p) { try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* ignore */ } }
|
package/checkpoint/test.mjs
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { McpClient, describe, it, assert, assertIncludes, run, tmpDir, rmrf } from "../_testkit/harness.mjs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const SERVER = path.join(__dirname, "server.js");
|
|
8
|
-
const STORE = tmpDir("cp-store-");
|
|
9
|
-
const PROJ = tmpDir("cp-proj-");
|
|
10
|
-
|
|
11
|
-
// seed project files
|
|
12
|
-
fs.mkdirSync(path.join(PROJ, "src"), { recursive: true });
|
|
13
|
-
fs.writeFileSync(path.join(PROJ, "src", "a.txt"), "original A\n");
|
|
14
|
-
fs.writeFileSync(path.join(PROJ, "README.md"), "# Title\n");
|
|
15
|
-
|
|
16
|
-
const client = new McpClient(SERVER, { CHECKPOINT_DIR: STORE });
|
|
17
|
-
let cpId = "";
|
|
18
|
-
|
|
19
|
-
describe("checkpoint", () => {
|
|
20
|
-
it("lists expected tools", async () => {
|
|
21
|
-
const tools = await client.listTools();
|
|
22
|
-
for (const t of ["checkpoint_create", "checkpoint_list", "checkpoint_restore", "checkpoint_diff", "checkpoint_delete"])
|
|
23
|
-
assert(tools.includes(t), `missing ${t}`);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("creates a checkpoint", async () => {
|
|
27
|
-
const r = await client.callTool("checkpoint_create", { dir: PROJ, label: "baseline" });
|
|
28
|
-
assertIncludes(r.text, "Checkpoint");
|
|
29
|
-
assertIncludes(r.text, "Files stored: 2");
|
|
30
|
-
cpId = (r.text.match(/Checkpoint (\S+) created/) || [])[1] || "";
|
|
31
|
-
assert(cpId, "could not parse checkpoint id");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("diff detects add/modify/delete", async () => {
|
|
35
|
-
fs.writeFileSync(path.join(PROJ, "src", "a.txt"), "CHANGED A\n");
|
|
36
|
-
fs.writeFileSync(path.join(PROJ, "src", "new.txt"), "brand new\n");
|
|
37
|
-
fs.rmSync(path.join(PROJ, "README.md"));
|
|
38
|
-
const r = await client.callTool("checkpoint_diff", { dir: PROJ, id: cpId });
|
|
39
|
-
assertIncludes(r.text, "Added (1)");
|
|
40
|
-
assertIncludes(r.text, "src/new.txt");
|
|
41
|
-
assertIncludes(r.text, "Modified (1)");
|
|
42
|
-
assertIncludes(r.text, "src/a.txt");
|
|
43
|
-
assertIncludes(r.text, "Deleted (1)");
|
|
44
|
-
assertIncludes(r.text, "README.md");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("restores with clean (revert + remove new)", async () => {
|
|
48
|
-
const r = await client.callTool("checkpoint_restore", { dir: PROJ, id: cpId, clean: true });
|
|
49
|
-
assertIncludes(r.text, "Restored 2 files");
|
|
50
|
-
assert(fs.readFileSync(path.join(PROJ, "src", "a.txt"), "utf8").includes("original A"), "a.txt not reverted");
|
|
51
|
-
assert(fs.existsSync(path.join(PROJ, "README.md")), "README not restored");
|
|
52
|
-
assert(!fs.existsSync(path.join(PROJ, "src", "new.txt")), "new.txt not removed");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("errors on unknown id", async () => {
|
|
56
|
-
const r = await client.callTool("checkpoint_diff", { dir: PROJ, id: "nope-123" });
|
|
57
|
-
assert(r.isError, "expected error flag");
|
|
58
|
-
assertIncludes(r.text, "not found");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("deletes the checkpoint", async () => {
|
|
62
|
-
const r = await client.callTool("checkpoint_delete", { dir: PROJ, id: cpId });
|
|
63
|
-
assertIncludes(r.text, "Deleted checkpoint");
|
|
64
|
-
const r2 = await client.callTool("checkpoint_list", { dir: PROJ });
|
|
65
|
-
assertIncludes(r2.text, "No checkpoints");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
await client.start();
|
|
70
|
-
const { fail } = await run();
|
|
71
|
-
await client.stop();
|
|
72
|
-
rmrf(STORE); rmrf(PROJ);
|
|
73
|
-
process.exit(fail > 0 ? 1 : 0);
|
package/context-pack/test.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { McpClient, describe, it, assert, assertIncludes, run, tmpDir, rmrf } from "../_testkit/harness.mjs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const SERVER = path.join(__dirname, "server.js");
|
|
8
|
-
const PROJ = tmpDir("ctx-proj-");
|
|
9
|
-
|
|
10
|
-
fs.mkdirSync(path.join(PROJ, "src"), { recursive: true });
|
|
11
|
-
fs.writeFileSync(path.join(PROJ, "package.json"), JSON.stringify({
|
|
12
|
-
name: "sample", version: "1.0.0",
|
|
13
|
-
scripts: { dev: "vite", test: "vitest" },
|
|
14
|
-
dependencies: { react: "^18.0.0", next: "^14.0.0" },
|
|
15
|
-
}, null, 2));
|
|
16
|
-
fs.writeFileSync(path.join(PROJ, "README.md"), "# Sample Project\nDemo app.\n## Setup\nnpm install\n");
|
|
17
|
-
fs.writeFileSync(path.join(PROJ, "src", "auth.ts"),
|
|
18
|
-
"export function login(user: string) { return true; }\nexport class AuthService { verify() {} }\nfunction helperInternal() {}\n");
|
|
19
|
-
|
|
20
|
-
const client = new McpClient(SERVER, {});
|
|
21
|
-
|
|
22
|
-
describe("context-pack", () => {
|
|
23
|
-
it("lists expected tools", async () => {
|
|
24
|
-
const tools = await client.listTools();
|
|
25
|
-
for (const t of ["pack_overview", "pack_tree", "pack_outline", "pack_search"])
|
|
26
|
-
assert(tools.includes(t), `missing ${t}`);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("overview detects stack + scripts + readme", async () => {
|
|
30
|
-
const r = await client.callTool("pack_overview", { dir: PROJ });
|
|
31
|
-
assertIncludes(r.text, "Next.js");
|
|
32
|
-
assertIncludes(r.text, "React");
|
|
33
|
-
assertIncludes(r.text, "dev: vite");
|
|
34
|
-
assertIncludes(r.text, "Sample Project");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("tree shows dirs and files", async () => {
|
|
38
|
-
const r = await client.callTool("pack_tree", { dir: PROJ, max_depth: 3 });
|
|
39
|
-
assertIncludes(r.text, "src/");
|
|
40
|
-
assertIncludes(r.text, "auth.ts");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("outline extracts symbols", async () => {
|
|
44
|
-
const r = await client.callTool("pack_outline", { dir: PROJ, file: "src/auth.ts" });
|
|
45
|
-
assertIncludes(r.text, "login");
|
|
46
|
-
assertIncludes(r.text, "AuthService");
|
|
47
|
-
assertIncludes(r.text, "helperInternal");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("outline errors on missing file", async () => {
|
|
51
|
-
const r = await client.callTool("pack_outline", { dir: PROJ, file: "does/not/exist.ts" });
|
|
52
|
-
assert(r.isError, "expected error");
|
|
53
|
-
assertIncludes(r.text, "File not found");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("search matches by path substring", async () => {
|
|
57
|
-
const r = await client.callTool("pack_search", { dir: PROJ, query: "auth" });
|
|
58
|
-
assertIncludes(r.text, "src/auth.ts");
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
await client.start();
|
|
63
|
-
const { fail } = await run();
|
|
64
|
-
await client.stop();
|
|
65
|
-
rmrf(PROJ);
|
|
66
|
-
process.exit(fail > 0 ? 1 : 0);
|
package/devjournal/test.mjs
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { McpClient, describe, it, assert, assertIncludes, run, tmpDir, rmrf } from "../_testkit/harness.mjs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const SERVER = path.join(__dirname, "server.js");
|
|
7
|
-
const STORE = tmpDir("jrnl-");
|
|
8
|
-
const PROJ = "/tmp/jrnl-demo-project";
|
|
9
|
-
|
|
10
|
-
const client = new McpClient(SERVER, { JOURNAL_DIR: STORE });
|
|
11
|
-
|
|
12
|
-
describe("devjournal", () => {
|
|
13
|
-
it("lists expected tools", async () => {
|
|
14
|
-
const tools = await client.listTools();
|
|
15
|
-
for (const t of ["journal_log", "journal_handoff", "journal_resume", "journal_timeline", "journal_search", "journal_clear_handoff"])
|
|
16
|
-
assert(tools.includes(t), `missing ${t}`);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("logs entries", async () => {
|
|
20
|
-
const r1 = await client.callTool("journal_log", { dir: PROJ, title: "Set up auth", type: "done", body: "login + AuthService" });
|
|
21
|
-
assertIncludes(r1.text, "Logged [done]");
|
|
22
|
-
const r2 = await client.callTool("journal_log", { dir: PROJ, title: "Refresh undecided", type: "blocker" });
|
|
23
|
-
assertIncludes(r2.text, "Logged [blocker]");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("saves a handoff", async () => {
|
|
27
|
-
const r = await client.callTool("journal_handoff", {
|
|
28
|
-
dir: PROJ, summary: "Auth scaffolding done; wiring refresh next.",
|
|
29
|
-
next_steps: ["Implement refresh", "Add tests"],
|
|
30
|
-
open_questions: ["Cookie or header?"], active_files: ["src/auth.ts"],
|
|
31
|
-
});
|
|
32
|
-
assertIncludes(r.text, "Handoff saved");
|
|
33
|
-
assertIncludes(r.text, "Next steps: 2");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("resume returns handoff + recent entries", async () => {
|
|
37
|
-
const r = await client.callTool("journal_resume", { dir: PROJ });
|
|
38
|
-
assertIncludes(r.text, "Latest handoff");
|
|
39
|
-
assertIncludes(r.text, "Implement refresh");
|
|
40
|
-
assertIncludes(r.text, "Cookie or header?");
|
|
41
|
-
assertIncludes(r.text, "Recent entries");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("timeline filters by type", async () => {
|
|
45
|
-
const r = await client.callTool("journal_timeline", { dir: PROJ, type: "blocker" });
|
|
46
|
-
assertIncludes(r.text, "Refresh undecided");
|
|
47
|
-
assert(!r.text.includes("Set up auth"), "should not include done entries when filtering blocker");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("search finds entries by keyword (offline mode)", async () => {
|
|
51
|
-
const r = await client.callTool("journal_search", { dir: PROJ, query: "auth login refresh" });
|
|
52
|
-
assertIncludes(r.text, "[keyword]");
|
|
53
|
-
assertIncludes(r.text, "Set up auth");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("clears handoff", async () => {
|
|
57
|
-
const r = await client.callTool("journal_clear_handoff", { dir: PROJ });
|
|
58
|
-
assertIncludes(r.text, "Cleared handoff");
|
|
59
|
-
const r2 = await client.callTool("journal_resume", { dir: PROJ });
|
|
60
|
-
assert(!r2.text.includes("Latest handoff"), "handoff should be gone");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
await client.start();
|
|
65
|
-
const { fail } = await run();
|
|
66
|
-
await client.stop();
|
|
67
|
-
rmrf(STORE);
|
|
68
|
-
process.exit(fail > 0 ? 1 : 0);
|
package/project-memory/test.mjs
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { McpClient, describe, it, assert, assertIncludes, run, tmpDir, rmrf } from "../_testkit/harness.mjs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const SERVER = path.join(__dirname, "server.js");
|
|
7
|
-
const VAULT = tmpDir("pm-");
|
|
8
|
-
const DEMO = "/tmp/pm-demo-project";
|
|
9
|
-
|
|
10
|
-
// Force keyword mode (no embedding key) so the suite is deterministic/offline.
|
|
11
|
-
const client = new McpClient(SERVER, { MEMORY_VAULT_DIR: VAULT, NINEROUTER_KEY: "", EMBED_KEY: "" });
|
|
12
|
-
|
|
13
|
-
let savedId = "";
|
|
14
|
-
|
|
15
|
-
describe("project-memory", () => {
|
|
16
|
-
it("lists expected tools", async () => {
|
|
17
|
-
const tools = await client.listTools();
|
|
18
|
-
for (const t of ["memory_save", "memory_recall", "memory_list", "memory_get", "memory_delete", "memory_reindex"])
|
|
19
|
-
assert(tools.includes(t), `missing ${t}`);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("saves a note (keyword mode without embeddings)", async () => {
|
|
23
|
-
const r = await client.callTool("memory_save", {
|
|
24
|
-
dir: DEMO, title: "JWT login flow",
|
|
25
|
-
content: "login() issues JWT; refresh token in httpOnly cookie; clock skew bug on verify.",
|
|
26
|
-
tags: ["auth", "jwt"], kind: "decision",
|
|
27
|
-
});
|
|
28
|
-
assertIncludes(r.text, "Saved note");
|
|
29
|
-
assertIncludes(r.text, "keyword-only");
|
|
30
|
-
savedId = (r.text.match(/Saved note (\S+)/) || [])[1] || "";
|
|
31
|
-
assert(savedId, "could not parse note id");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("recalls by keyword and tags mode label", async () => {
|
|
35
|
-
const r = await client.callTool("memory_recall", { dir: DEMO, query: "jwt auth cookie" });
|
|
36
|
-
assertIncludes(r.text, "[keyword]");
|
|
37
|
-
assertIncludes(r.text, "JWT login flow");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("returns no-match gracefully", async () => {
|
|
41
|
-
const r = await client.callTool("memory_recall", { dir: DEMO, query: "kubernetes helm chart xyzzy" });
|
|
42
|
-
assertIncludes(r.text, "No relevant memories");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("lists saved notes", async () => {
|
|
46
|
-
const r = await client.callTool("memory_list", { dir: DEMO });
|
|
47
|
-
assertIncludes(r.text, "JWT login flow");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("gets full note by id", async () => {
|
|
51
|
-
const r = await client.callTool("memory_get", { dir: DEMO, id: savedId });
|
|
52
|
-
assertIncludes(r.text, "httpOnly cookie");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("reindex reports gracefully without embeddings", async () => {
|
|
56
|
-
const r = await client.callTool("memory_reindex", { dir: DEMO });
|
|
57
|
-
assertIncludes(r.text, "failed");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("deletes a note", async () => {
|
|
61
|
-
const r = await client.callTool("memory_delete", { dir: DEMO, id: savedId });
|
|
62
|
-
assertIncludes(r.text, "Deleted note");
|
|
63
|
-
const r2 = await client.callTool("memory_list", { dir: DEMO });
|
|
64
|
-
assertIncludes(r2.text, "No memories");
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
await client.start();
|
|
69
|
-
const { fail } = await run();
|
|
70
|
-
await client.stop();
|
|
71
|
-
rmrf(VAULT);
|
|
72
|
-
process.exit(fail > 0 ? 1 : 0);
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
// Optional online test: LLM rerank via 9router Kiro. Skips cleanly if offline.
|
|
2
|
-
import { McpClient, describe, it, assert, assertIncludes, run, tmpDir, rmrf } from "../_testkit/harness.mjs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import http from "http";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const SERVER = path.join(__dirname, "server.js");
|
|
9
|
-
const KEY = process.env.LLM_API_KEY || process.env.RERANK_KEY || process.env.NINEROUTER_KEY || "";
|
|
10
|
-
const URL_BASE = "http://localhost:20128";
|
|
11
|
-
|
|
12
|
-
function ping() {
|
|
13
|
-
return new Promise((resolve) => {
|
|
14
|
-
const req = http.request(URL_BASE + "/v1/models", { method: "GET", timeout: 4000, headers: { Authorization: `Bearer ${KEY}` } }, (r) => { r.resume(); resolve(r.statusCode === 200); });
|
|
15
|
-
req.on("error", () => resolve(false));
|
|
16
|
-
req.on("timeout", () => { req.destroy(); resolve(false); });
|
|
17
|
-
req.end();
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (!KEY) { console.log(" ⚠ rerank test skipped (no LLM_API_KEY in env)"); process.exit(0); }
|
|
22
|
-
const online = await ping();
|
|
23
|
-
if (!online) {
|
|
24
|
-
console.log(" ⚠ rerank test skipped (9router not reachable)");
|
|
25
|
-
process.exit(0);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const VAULT = tmpDir("pm-rr-");
|
|
29
|
-
const DEMO = "/tmp/pm-rr-demo";
|
|
30
|
-
const env = { MEMORY_VAULT_DIR: VAULT, LLM_BASE_URL: URL_BASE, LLM_API_KEY: KEY, EMBED_KEY: "", RERANK_MODEL: "kr/claude-haiku-4.5", RERANK_TIMEOUT_MS: "40000" };
|
|
31
|
-
const client = new McpClient(SERVER, env);
|
|
32
|
-
|
|
33
|
-
describe("project-memory (rerank/online)", () => {
|
|
34
|
-
it("seeds distinct notes", async () => {
|
|
35
|
-
await client.callTool("memory_save", { dir: DEMO, title: "JWT auth flow", content: "login issues JWT; refresh token stored in httpOnly cookie", tags: ["auth"] });
|
|
36
|
-
await client.callTool("memory_save", { dir: DEMO, title: "Dark mode toggle", content: "CSS variables and localStorage for theme switching", tags: ["ui"] });
|
|
37
|
-
await client.callTool("memory_save", { dir: DEMO, title: "Nginx docker setup", content: "Dockerfile and compose for nginx reverse proxy", tags: ["infra"] });
|
|
38
|
-
const r = await client.callTool("memory_list", { dir: DEMO });
|
|
39
|
-
assertIncludes(r.text, "(3)");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("reranks a semantic query to the auth note (no keyword overlap)", async () => {
|
|
43
|
-
const r = await client.callTool("memory_recall", { dir: DEMO, query: "how do users sign in securely with tokens", limit: 1 }, 60000);
|
|
44
|
-
assertIncludes(r.text, "[rerank]");
|
|
45
|
-
assertIncludes(r.text, "JWT auth flow");
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
await client.start();
|
|
50
|
-
const { fail } = await run();
|
|
51
|
-
await client.stop();
|
|
52
|
-
rmrf(VAULT);
|
|
53
|
-
process.exit(fail > 0 ? 1 : 0);
|
package/run-tests.mjs
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Runs every server's test.mjs and prints a combined summary.
|
|
4
|
-
* Usage: node run-tests.mjs (from ~/.codex/mcp-servers)
|
|
5
|
-
*/
|
|
6
|
-
import { spawn } from "child_process";
|
|
7
|
-
import path from "path";
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
9
|
-
|
|
10
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const SERVERS = ["project-memory", "checkpoint", "context-pack", "devjournal"];
|
|
12
|
-
|
|
13
|
-
function runOne(name) {
|
|
14
|
-
return new Promise((resolve) => {
|
|
15
|
-
const test = path.join(__dirname, name, "test.mjs");
|
|
16
|
-
const proc = spawn("node", [test], { stdio: ["ignore", "pipe", "pipe"] });
|
|
17
|
-
let out = "";
|
|
18
|
-
proc.stdout.on("data", (d) => (out += d));
|
|
19
|
-
proc.stderr.on("data", (d) => (out += d));
|
|
20
|
-
proc.on("close", (code) => resolve({ name, code, out }));
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const results = [];
|
|
25
|
-
for (const s of SERVERS) {
|
|
26
|
-
console.log(`\n=== ${s} ===`);
|
|
27
|
-
const r = await runOne(s);
|
|
28
|
-
process.stdout.write(r.out);
|
|
29
|
-
results.push(r);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
console.log("\n================ SUMMARY ================");
|
|
33
|
-
let anyFail = false;
|
|
34
|
-
for (const r of results) {
|
|
35
|
-
const ok = r.code === 0;
|
|
36
|
-
if (!ok) anyFail = true;
|
|
37
|
-
const m = r.out.match(/(\d+) passed, (\d+) failed/);
|
|
38
|
-
const stat = m ? `${m[1]} passed, ${m[2]} failed` : (ok ? "ok" : "FAILED");
|
|
39
|
-
console.log(` ${ok ? "✓" : "✗"} ${r.name.padEnd(16)} ${stat}`);
|
|
40
|
-
}
|
|
41
|
-
console.log("========================================");
|
|
42
|
-
process.exit(anyFail ? 1 : 0);
|