codex-dev-mcp-suite 1.6.0 → 1.7.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 +16 -0
- package/context-pack/server.js +173 -1
- package/devjournal/server.js +38 -0
- package/docs/ROADMAP.md +74 -0
- package/docs/providers.md +13 -0
- package/docs/superpowers/plans/2026-06-15-hardening-v1-2-implementation.md +60 -0
- package/docs/superpowers/plans/2026-06-15-provider-chain-v1-1-implementation.md +65 -0
- package/docs/superpowers/plans/2026-06-15-provider-client-agnostic-config.md +41 -0
- package/docs/superpowers/plans/2026-06-15-trust-release-implementation.md +62 -0
- package/docs/superpowers/plans/2026-06-18-v1-5-0-knowledge-graph.md +792 -0
- package/docs/superpowers/specs/2026-06-15-provider-chain-v1-1-design.md +89 -0
- package/docs/superpowers/specs/2026-06-15-trust-release-design.md +164 -0
- package/docs/superpowers/specs/2026-06-18-dev-mcp-suite-v1-5-0-knowledge-graph-design.md +386 -0
- package/package.json +5 -18
- package/project-memory/graph.js +8 -1
- package/project-memory/server.js +162 -4
- package/run-tests.mjs +54 -0
package/project-memory/server.js
CHANGED
|
@@ -155,6 +155,103 @@ class ProjectMemoryServer {
|
|
|
155
155
|
await fs.writeFile(p.indexFile, JSON.stringify(index, null, 2));
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// Ensure an Obsidian-friendly workspace exists so the vault can be opened
|
|
159
|
+
// directly in the Obsidian app (graph view, tag pages, MOC navigation).
|
|
160
|
+
async ensureObsidianFolder(p) {
|
|
161
|
+
const obsDir = path.join(p.projectDir, ".obsidian");
|
|
162
|
+
try { await fs.access(obsDir); return; } catch {}
|
|
163
|
+
await fs.mkdir(obsDir, { recursive: true });
|
|
164
|
+
await fs.writeFile(path.join(obsDir, "app.json"), JSON.stringify({
|
|
165
|
+
alwaysUpdateLinks: true,
|
|
166
|
+
newLinkFormat: "shortest",
|
|
167
|
+
useMarkdownLinks: false,
|
|
168
|
+
showViewHeader: true,
|
|
169
|
+
}, null, 2));
|
|
170
|
+
await fs.writeFile(path.join(obsDir, "workspace.json"), JSON.stringify({
|
|
171
|
+
_comment: "Auto-managed by codex-dev-mcp-suite (project-memory).",
|
|
172
|
+
}, null, 2));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Build an Obsidian-style Map of Content (MOC) listing every note, its
|
|
176
|
+
// outgoing links, backlinks, and a #tag index. Refreshed on save/delete.
|
|
177
|
+
async writeMoc(p, index) {
|
|
178
|
+
const notes = Object.values(index.notes || {})
|
|
179
|
+
.sort((a, b) => (a.created < b.created ? 1 : -1));
|
|
180
|
+
if (notes.length === 0) return;
|
|
181
|
+
const tagMap = new Map();
|
|
182
|
+
for (const n of notes) {
|
|
183
|
+
for (const t of n.tags || []) {
|
|
184
|
+
if (!tagMap.has(t)) tagMap.set(t, []);
|
|
185
|
+
tagMap.get(t).push(n);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const lines = [
|
|
189
|
+
"# MOC — Map of Content",
|
|
190
|
+
"",
|
|
191
|
+
`> Auto-generated by project-memory for project \`${p.slug}\`. Do not edit by hand; it is regenerated on every save/delete.`,
|
|
192
|
+
"",
|
|
193
|
+
`**${notes.length} notes**`,
|
|
194
|
+
"",
|
|
195
|
+
"## Notes",
|
|
196
|
+
];
|
|
197
|
+
for (const n of notes) {
|
|
198
|
+
const link = `[[${n.title}]]`;
|
|
199
|
+
const tags = (n.tags || []).length ? ` #${(n.tags || []).join(" #")}` : "";
|
|
200
|
+
lines.push(`- ${link} — *${n.kind || "note"}*${tags}`);
|
|
201
|
+
}
|
|
202
|
+
lines.push("", "## By tag");
|
|
203
|
+
if (tagMap.size === 0) lines.push("- (no tags yet)");
|
|
204
|
+
for (const [tag, ns] of [...tagMap.entries()].sort()) {
|
|
205
|
+
lines.push(`- #${tag}: ${ns.map((n) => `[[${n.title}]]`).join(", ")}`);
|
|
206
|
+
}
|
|
207
|
+
lines.push("", "## Graph (backlinks)");
|
|
208
|
+
for (const n of notes) {
|
|
209
|
+
const bl = (n.backlinks || []).map((b) => `[[${b.title}]]`).join(", ");
|
|
210
|
+
if (bl) lines.push(`- [[${n.title}]] <- ${bl}`);
|
|
211
|
+
}
|
|
212
|
+
lines.push("");
|
|
213
|
+
await fs.writeFile(path.join(p.projectDir, "MOC.md"), lines.join("\n"));
|
|
214
|
+
// Also emit a standalone #tag index page per tag (Obsidian-style tag pages).
|
|
215
|
+
await this.writeTagPages(p, tagMap);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Emit one `tags/<tag>.md` page per tag, listing all notes carrying it.
|
|
219
|
+
async writeTagPages(p, tagMap) {
|
|
220
|
+
const tagDir = path.join(p.projectDir, "tags");
|
|
221
|
+
await fs.mkdir(tagDir, { recursive: true });
|
|
222
|
+
for (const [tag, ns] of tagMap.entries()) {
|
|
223
|
+
const body = [
|
|
224
|
+
`# Tag: ${tag}`,
|
|
225
|
+
"",
|
|
226
|
+
ns.map((n) => `- [[${n.title}]] — *${n.kind || "note"}*`).join("\n"),
|
|
227
|
+
"",
|
|
228
|
+
].join("\n");
|
|
229
|
+
await fs.writeFile(path.join(tagDir, `${String(tag).replace(/[^a-zA-Z0-9._-]/g, "-")}.md`), body);
|
|
230
|
+
}
|
|
231
|
+
// prune tag pages with no notes left
|
|
232
|
+
let files = [];
|
|
233
|
+
try { files = await fs.readdir(tagDir); } catch { return; }
|
|
234
|
+
for (const f of files) {
|
|
235
|
+
if (!f.endsWith(".md")) continue;
|
|
236
|
+
const t = f.slice(0, -3);
|
|
237
|
+
if (!tagMap.has(t)) await fs.unlink(path.join(tagDir, f)).catch(() => {});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Export the note graph (nodes + directed edges) for external graph views.
|
|
242
|
+
buildGraph(index) {
|
|
243
|
+
const nodes = Object.values(index.notes || {}).map((n) => ({
|
|
244
|
+
id: n.id, title: n.title, kind: n.kind || "note", tags: n.tags || [],
|
|
245
|
+
}));
|
|
246
|
+
const edges = [];
|
|
247
|
+
for (const n of Object.values(index.notes || {})) {
|
|
248
|
+
for (const link of n.links || []) {
|
|
249
|
+
edges.push({ from: n.id, raw: link.raw, ref: link.ref, kind: link.kind });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { nodes, edges };
|
|
253
|
+
}
|
|
254
|
+
|
|
158
255
|
setupHandlers() {
|
|
159
256
|
this.server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
160
257
|
tools: [
|
|
@@ -170,6 +267,7 @@ class ProjectMemoryServer {
|
|
|
170
267
|
dir: { type: "string", description: "Project directory (defaults to CWD)" },
|
|
171
268
|
tags: { type: "array", items: { type: "string" }, description: "Optional tags" },
|
|
172
269
|
kind: { type: "string", description: "note | decision | task | log | snippet", default: "note" },
|
|
270
|
+
aliases: { type: "array", items: { type: "string" }, description: "Optional Obsidian-style aliases for the note (alt titles wikilinks can reference)" },
|
|
173
271
|
created: { type: "string", description: "Optional ISO timestamp to backdate the note (e.g. original session time)" },
|
|
174
272
|
},
|
|
175
273
|
required: ["title", "content"],
|
|
@@ -288,6 +386,26 @@ class ProjectMemoryServer {
|
|
|
288
386
|
},
|
|
289
387
|
},
|
|
290
388
|
},
|
|
389
|
+
{
|
|
390
|
+
name: "memory_moc",
|
|
391
|
+
description: "Generate / refresh the Obsidian-style Map of Content (MOC.md) for the project: lists every note with wikilinks, a #tag index, and backlink graph. Run after saves, or on demand to open the vault in Obsidian.",
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: "object",
|
|
394
|
+
properties: {
|
|
395
|
+
dir: { type: "string", description: "Project directory (defaults to CWD)" },
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "memory_graph",
|
|
401
|
+
description: "Export the note graph (nodes + directed edges from [[wikilinks]]) as JSON, for external graph views or analysis. Backlinks are resolved via graph state.",
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: "object",
|
|
404
|
+
properties: {
|
|
405
|
+
dir: { type: "string", description: "Project directory (defaults to CWD)" },
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
},
|
|
291
409
|
],
|
|
292
410
|
}));
|
|
293
411
|
|
|
@@ -305,6 +423,8 @@ class ProjectMemoryServer {
|
|
|
305
423
|
case "memory_global_recall": return await this.globalRecall(args || {});
|
|
306
424
|
case "memory_dedup": return await this.dedup(args || {});
|
|
307
425
|
case "memory_stats": return await this.stats(args || {});
|
|
426
|
+
case "memory_moc": return await this.moc(args || {});
|
|
427
|
+
case "memory_graph": return await this.graph(args || {});
|
|
308
428
|
default: throw new Error(`Unknown tool: ${name}`);
|
|
309
429
|
}
|
|
310
430
|
} catch (error) {
|
|
@@ -349,22 +469,23 @@ class ProjectMemoryServer {
|
|
|
349
469
|
});
|
|
350
470
|
}
|
|
351
471
|
|
|
352
|
-
async save({ title, content, dir, tags, kind = "note", created: createdArg }) {
|
|
472
|
+
async save({ title, content, dir, tags, kind = "note", aliases, created: createdArg }) {
|
|
353
473
|
title = limit(title, "title", MAX_TITLE);
|
|
354
474
|
content = limit(content, "content", MAX_CONTENT);
|
|
355
475
|
const tagList = Array.isArray(tags) ? tags.map((t) => String(t).trim()).filter(Boolean) : [];
|
|
476
|
+
const aliasList = Array.isArray(aliases) ? aliases.map((t) => String(t).trim()).filter(Boolean) : [];
|
|
356
477
|
const p = this.paths(dir);
|
|
357
478
|
await fs.mkdir(p.notesDir, { recursive: true });
|
|
358
479
|
|
|
359
480
|
const id = genId();
|
|
360
481
|
const created = (createdArg && /^\d{4}-\d{2}-\d{2}/.test(String(createdArg))) ? String(createdArg) : nowIso();
|
|
361
|
-
const meta = { id, title, kind, tags: tagList, created, dir: path.resolve(dir || process.cwd()) };
|
|
482
|
+
const meta = { id, title, kind, tags: tagList, aliases: aliasList, created, dir: path.resolve(dir || process.cwd()) };
|
|
362
483
|
const file = path.join(p.notesDir, `${id}.md`);
|
|
363
484
|
await fs.writeFile(file, buildFrontmatter(meta) + `# ${title}\n\n${content}\n`);
|
|
364
485
|
|
|
365
486
|
const index = await this.loadIndex(p);
|
|
366
487
|
const keywords = [...new Set([...tokenize(title), ...tokenize(content), ...tagList.map((t) => t.toLowerCase())])];
|
|
367
|
-
const note = { id, title, kind, tags: tagList, created, keywords, file: path.relative(p.projectDir, file) };
|
|
488
|
+
const note = { id, title, kind, tags: tagList, aliases: aliasList, created, keywords, file: path.relative(p.projectDir, file) };
|
|
368
489
|
|
|
369
490
|
const vec = await embedOne(`${title}\n\n${content}`);
|
|
370
491
|
let embedded = false;
|
|
@@ -372,8 +493,19 @@ class ProjectMemoryServer {
|
|
|
372
493
|
|
|
373
494
|
index.notes[id] = note;
|
|
374
495
|
await this.saveIndex(p, index);
|
|
496
|
+
await this.ensureObsidianFolder(p);
|
|
497
|
+
// Resolve [[wikilinks]] + backlinks immediately so the graph is fresh on save.
|
|
498
|
+
const ensured = await ensureGraphState({
|
|
499
|
+
vaultRoot: VAULT_ROOT,
|
|
500
|
+
projectDir: p.projectDir,
|
|
501
|
+
slug: p.slug,
|
|
502
|
+
index,
|
|
503
|
+
noteLoader: loadNoteBody,
|
|
504
|
+
});
|
|
505
|
+
if (ensured.changed) await this.saveIndex(p, ensured.index);
|
|
506
|
+
await this.writeMoc(p, ensured.index);
|
|
375
507
|
|
|
376
|
-
return { content: [{ type: "text", text: `Saved note ${id} → ${p.slug}\nFile: ${file}\nKeywords indexed: ${keywords.length}${embedded ? " (semantic embedding stored)" : " (keyword-only; embeddings unavailable)"}` }] };
|
|
508
|
+
return { content: [{ type: "text", text: `Saved note ${id} → ${p.slug}\nFile: ${file}\nKeywords indexed: ${keywords.length}${embedded ? " (semantic embedding stored)" : " (keyword-only; embeddings unavailable)"}\nMOC refreshed: ${path.join(p.projectDir, "MOC.md")}` }] };
|
|
377
509
|
}
|
|
378
510
|
|
|
379
511
|
async recall({ query, dir, limit: lim = 5, full = false, mode: requestedMode = "auto" }) {
|
|
@@ -572,6 +704,7 @@ class ProjectMemoryServer {
|
|
|
572
704
|
await fs.unlink(path.join(p.projectDir, meta.file)).catch(() => {});
|
|
573
705
|
delete index.notes[id];
|
|
574
706
|
await this.saveIndex(p, index);
|
|
707
|
+
await this.writeMoc(p, index);
|
|
575
708
|
return { content: [{ type: "text", text: `Deleted note ${id} from ${p.slug}` }] };
|
|
576
709
|
}
|
|
577
710
|
|
|
@@ -653,6 +786,31 @@ class ProjectMemoryServer {
|
|
|
653
786
|
return { content: [{ type: "text", text }] };
|
|
654
787
|
}
|
|
655
788
|
|
|
789
|
+
async moc({ dir } = {}) {
|
|
790
|
+
const p = this.paths(dir);
|
|
791
|
+
await this.ensureObsidianFolder(p);
|
|
792
|
+
const index = await this.loadIndex(p);
|
|
793
|
+
await this.writeMoc(p, index);
|
|
794
|
+
const mocFile = path.join(p.projectDir, "MOC.md");
|
|
795
|
+
const count = Object.keys(index.notes || {}).length;
|
|
796
|
+
return { content: [{ type: "text", text: `MOC refreshed for ${p.slug}: ${count} notes → ${mocFile}\nOpen the project folder in Obsidian to see the graph + tag pages.` }] };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async graph({ dir } = {}) {
|
|
800
|
+
const p = this.paths(dir);
|
|
801
|
+
const index = await this.loadIndex(p);
|
|
802
|
+
const ensured = await ensureGraphState({
|
|
803
|
+
vaultRoot: VAULT_ROOT,
|
|
804
|
+
projectDir: p.projectDir,
|
|
805
|
+
slug: p.slug,
|
|
806
|
+
index,
|
|
807
|
+
noteLoader: loadNoteBody,
|
|
808
|
+
});
|
|
809
|
+
if (ensured.changed) await this.saveIndex(p, ensured.index);
|
|
810
|
+
const g = this.buildGraph(ensured.index);
|
|
811
|
+
return { content: [{ type: "text", text: JSON.stringify(g, null, 2) }] };
|
|
812
|
+
}
|
|
813
|
+
|
|
656
814
|
async run() {
|
|
657
815
|
const transport = new StdioServerTransport();
|
|
658
816
|
await this.server.connect(transport);
|
package/run-tests.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Runs every server's test.mjs and any top-level tests/*.test.mjs,
|
|
4
|
+
* then prints a combined summary. Usage: node run-tests.mjs
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const SERVERS = ["project-memory", "checkpoint", "context-pack", "devjournal"];
|
|
13
|
+
const EXTRA_TESTS = fs.existsSync("tests")
|
|
14
|
+
? fs.readdirSync("tests").filter((f) => f.endsWith(".test.mjs")).map((f) => `tests/${f}`)
|
|
15
|
+
: [];
|
|
16
|
+
|
|
17
|
+
/** Run a single test file by absolute path. */
|
|
18
|
+
function runFile(absPath) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const proc = spawn("node", [absPath], { stdio: ["ignore", "pipe", "pipe"] });
|
|
21
|
+
let out = "";
|
|
22
|
+
proc.stdout.on("data", (d) => (out += d));
|
|
23
|
+
proc.stderr.on("data", (d) => (out += d));
|
|
24
|
+
proc.on("close", (code) => resolve({ name: path.basename(path.dirname(absPath)) || absPath, code, out }));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const results = [];
|
|
29
|
+
|
|
30
|
+
for (const s of SERVERS) {
|
|
31
|
+
console.log(`\n=== ${s} ===`);
|
|
32
|
+
const r = await runFile(path.join(__dirname, s, "test.mjs"));
|
|
33
|
+
process.stdout.write(r.out);
|
|
34
|
+
results.push({ ...r, name: s });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const t of EXTRA_TESTS) {
|
|
38
|
+
console.log(`\n=== ${t} ===`);
|
|
39
|
+
const r = await runFile(path.join(__dirname, t));
|
|
40
|
+
process.stdout.write(r.out);
|
|
41
|
+
results.push({ ...r, name: t });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log("\n================ SUMMARY ================");
|
|
45
|
+
let anyFail = false;
|
|
46
|
+
for (const r of results) {
|
|
47
|
+
const ok = r.code === 0;
|
|
48
|
+
if (!ok) anyFail = true;
|
|
49
|
+
const m = r.out.match(/(\d+) passed, (\d+) failed/);
|
|
50
|
+
const stat = m ? `${m[1]} passed, ${m[2]} failed` : (ok ? "ok" : "FAILED");
|
|
51
|
+
console.log(` ${ok ? "✓" : "✗"} ${r.name.padEnd(28)} ${stat}`);
|
|
52
|
+
}
|
|
53
|
+
console.log("========================================");
|
|
54
|
+
process.exit(anyFail ? 1 : 0);
|