codex-dev-mcp-suite 1.5.1 → 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 CHANGED
@@ -117,3 +117,19 @@
117
117
  ### Compatibility
118
118
  - Legacy env names remain supported: `LLM_*`, `NINEROUTER_*`, `EMBED_*`, and `RERANK_*`.
119
119
  - Offline keyword search remains the default if no model endpoint is configured.
120
+
121
+ ## [1.7.0] - 2026-07-14 — Obsidian-grade vault (feat/obsidian-grade-vault)
122
+ ### Added (project-memory)
123
+ - `memory_moc` — generate/refresh Obsidian-style Map of Content (`MOC.md`): all notes as wikilinks, `#tag` index, and backlink graph. Auto-refreshed on every save/delete.
124
+ - `memory_graph` — export note graph (nodes + directed edges from `[[wikilinks]]`) as JSON for external graph views.
125
+ - Auto-bootstrap `.obsidian/` workspace (`app.json` + `workspace.json`) on first save, so the vault opens directly in the Obsidian app with graph view + tag pages.
126
+ - MOC regenerates automatically after `memory_save` and `memory_delete`.
127
+
128
+ ## [1.8.0] - 2026-07-14 — Vibecoder ergonomics (P1/P2, feat/obsidian-grade-vault)
129
+ ### Added (project-memory)
130
+ - Auto-resolve `[[wikilinks]]` + backlinks immediately on `memory_save` (graph fresh without waiting for recall).
131
+ - Per-tag index pages: `tags/<tag>.md` emitted alongside MOC, pruned when tag empties.
132
+ - `aliases` field on notes (Obsidian-style); `[[alias]]` now resolves via `resolveLink`.
133
+ ### Added (devjournal)
134
+ - `journal_daily` — Obsidian-style daily note (`daily/YYYY-MM-DD.md`); read or append mode.
135
+ - `journal_log` now auto-appends a line to today's daily note.
package/bin/prune.mjs CHANGED
@@ -35,7 +35,7 @@ function resolveRoot() {
35
35
  return parent;
36
36
  }
37
37
  }
38
- return path.join(os.homedir(), ".codex", "memories");
38
+ return path.join(os.homedir(), ".ai-shared-memory");
39
39
  }
40
40
 
41
41
  function parseArgs(argv) {
package/bin/stats.mjs CHANGED
@@ -36,7 +36,7 @@ function resolveRoot() {
36
36
  return parent;
37
37
  }
38
38
  }
39
- return path.join(os.homedir(), ".codex", "memories");
39
+ return path.join(os.homedir(), ".ai-shared-memory");
40
40
  }
41
41
 
42
42
  function parseArgs(argv) {
@@ -24,7 +24,7 @@ import crypto from "crypto";
24
24
 
25
25
  const ROOT =
26
26
  process.env.CHECKPOINT_DIR ||
27
- path.join(os.homedir(), ".codex", "memories", "checkpoints");
27
+ path.join(os.homedir(), ".ai-shared-memory", "checkpoints");
28
28
 
29
29
  const DEFAULT_IGNORE = new Set([
30
30
  "node_modules", ".git", ".next", "dist", "build", "target", ".venv",
@@ -8,7 +8,7 @@
8
8
  * window. Detects stack, shows a pruned tree, surfaces key files, and
9
9
  * extracts top-level symbols (functions/classes/exports) heuristically.
10
10
  *
11
- * Tools: pack_overview, pack_tree, pack_outline, pack_search
11
+ * Tools: pack_overview, pack_tree, pack_outline, pack_search, pack_audit
12
12
  */
13
13
 
14
14
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -19,6 +19,7 @@ import {
19
19
  } from "@modelcontextprotocol/sdk/types.js";
20
20
  import fs from "fs/promises";
21
21
  import path from "path";
22
+ import { execSync } from "child_process";
22
23
 
23
24
  const IGNORE = new Set([
24
25
  "node_modules", ".git", ".next", "dist", "build", "target", ".venv",
@@ -34,6 +35,32 @@ const KEY_FILES = [
34
35
  ];
35
36
  const CODE_EXT = new Set([".js", ".mjs", ".ts", ".tsx", ".jsx", ".py", ".go", ".rs", ".java", ".rb", ".php", ".c", ".cpp", ".h", ".sh"]);
36
37
  const MAX_FILE_BYTES = 1_500_000;
38
+ const AUDIT_SENSITIVE_PATTERNS = [
39
+ /^\.env/i, /\.pem$/i, /\.key$/i, /\.p12$/i, /\.pfx$/i,
40
+ /id_rsa/i, /id_ed25519/i, /id_ecdsa/i, /id_dsa/i,
41
+ /known_hosts/i, /authorized_keys/i,
42
+ /credentials/i, /password/i, /secret/i, /token/i,
43
+ /\.htpasswd/i, /\.netrc/i, /npmrc$/i,
44
+ /service[_-]?account.*\.json$/i,
45
+ ];
46
+ const AUDIT_SECRET_CONTENT_RE = [
47
+ /sk-[a-zA-Z0-9]{20,}/i,
48
+ /ghp_[a-zA-Z0-9]{36}/i,
49
+ /gho_[a-zA-Z0-9]{36}/i,
50
+ /glpat-[a-zA-Z0-9\-_]{20}/i,
51
+ /AKIA[0-9A-Z]{16}/i,
52
+ /AIza[0-9A-Za-z_-]{35}/i,
53
+ /bearer\s+[a-zA-Z0-9_\-\.]{20,}/i,
54
+ /api[_-]?key\s*[:=]\s*["']?[a-zA-Z0-9_\-]{16,}/i,
55
+ /password\s*[:=]\s*["']?[^\s"']{6,}/i,
56
+ ];
57
+ const AUDIT_LARGE_WARN = 100_000;
58
+ const AUDIT_LARGE_CRIT = 1_000_000;
59
+ const AUDIT_MAX_SCAN = 16_384;
60
+
61
+ function isSensitiveName(filename) {
62
+ return AUDIT_SENSITIVE_PATTERNS.some((pattern) => pattern.test(filename));
63
+ }
37
64
 
38
65
  function resolveDir(dir) { return path.resolve(dir || process.cwd()); }
39
66
 
@@ -155,6 +182,17 @@ class ContextPackServer {
155
182
  required: ["query"],
156
183
  },
157
184
  },
185
+ {
186
+ name: "pack_audit",
187
+ description: "Audit project for security risks, leaked secrets/credentials, missing .gitignore, uncommitted sensitive files, and overly large files.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ dir: { type: "string", description: "Project directory (defaults to CWD)" },
192
+ strict: { type: "boolean", description: "Flag missing .gitignore as critical instead of warn", default: false },
193
+ },
194
+ },
195
+ },
158
196
  ],
159
197
  }));
160
198
 
@@ -166,6 +204,7 @@ class ContextPackServer {
166
204
  case "pack_tree": return await this.tree(args || {});
167
205
  case "pack_outline": return await this.outline(args || {});
168
206
  case "pack_search": return await this.search(args || {});
207
+ case "pack_audit": return await this.audit(args || {});
169
208
  default: throw new Error(`Unknown tool: ${name}`);
170
209
  }
171
210
  } catch (e) {
@@ -246,6 +285,139 @@ class ContextPackServer {
246
285
  return { content: [{ type: "text", text: hits.length ? `Matches for "${query}" (${hits.length}):\n` + hits.map((h) => ` ${h}`).join("\n") : `No path matches for "${query}" in ${root}` }] };
247
286
  }
248
287
 
288
+
289
+
290
+ async audit({ dir, strict = false }) {
291
+ const root = resolveDir(dir);
292
+ const findings = [];
293
+
294
+ function add(sev, msg, fix) {
295
+ findings.push({ severity: sev, message: msg, fix });
296
+ }
297
+
298
+ // walk
299
+ let all = [];
300
+ try { all = await walk(root, "", [], 0, 6); } catch { /* ignore */ }
301
+ const files = all.filter((f) => !f.dir);
302
+
303
+ // git presence and status
304
+ let gitRoot = null;
305
+ try {
306
+ const g = execSync("git rev-parse --show-toplevel", { cwd: root, encoding: "utf8", timeout: 3000 }).trim();
307
+ gitRoot = g;
308
+ } catch { /* not a git repo */ }
309
+
310
+ let gitStatus = [];
311
+ if (gitRoot) {
312
+ try {
313
+ const out = execSync("git status --porcelain", { cwd: root, encoding: "utf8", timeout: 5000 });
314
+ gitStatus = out.trim().split("\n").filter(Boolean).map((line) => {
315
+ const code = line.slice(0, 2);
316
+ const file = line.slice(3);
317
+ return { code, file, untracked: code === "??" };
318
+ });
319
+ } catch { /* ignore */ }
320
+ }
321
+
322
+ const statusMap = new Map(gitStatus.map((s) => [s.file, s]));
323
+
324
+ // Helpers
325
+ const isGitTracked = (rel) => {
326
+ const s = statusMap.get(rel);
327
+ if (!s) return true; // assume tracked if no status (could be clean tracked)
328
+ return !s.untracked;
329
+ };
330
+ const isIgnored = (rel) => {
331
+ try {
332
+ execSync(`git check-ignore -q -- "${rel}"`, { cwd: root, timeout: 2000 });
333
+ return true;
334
+ } catch { return false; }
335
+ };
336
+
337
+ // --- Missing critical files ---
338
+ const hasGitignore = files.some((f) => f.rel === ".gitignore" || path.basename(f.rel) === ".gitignore");
339
+ const hasEnvExample = files.some((f) => f.rel === ".env.example" || f.rel === ".env.sample" || f.rel === ".env.template");
340
+ if (gitRoot && !hasGitignore) add(strict ? "critical" : "warn", "No .gitignore found in a git repository.", "Add a .gitignore to prevent accidental commits of build artifacts, secrets, and OS files.");
341
+ if (!hasEnvExample) add("info", "No .env.example / .env.sample found.", "Add .env.example with dummy values so contributors know which env vars are required.");
342
+
343
+ // --- File-by-file scan ---
344
+ for (const f of files) {
345
+ const basename = path.basename(f.rel);
346
+ const abs = path.join(root, f.rel);
347
+ let stat;
348
+ try { stat = await fs.stat(abs); } catch { continue; }
349
+ const size = stat.size;
350
+ const sensitiveName = isSensitiveName(basename);
351
+
352
+ // large files
353
+ if (size > AUDIT_LARGE_CRIT) {
354
+ add("critical", `Large file: ${f.rel} (${(size/1e6).toFixed(1)} MB)`, "Consider adding to .gitignore, using Git LFS, or splitting.");
355
+ } else if (size > AUDIT_LARGE_WARN) {
356
+ add("warn", `Large file: ${f.rel} (${(size/1e3).toFixed(0)} KB)`, "Consider adding to .gitignore, using Git LFS, or splitting.");
357
+ }
358
+
359
+ // exposed sensitive files
360
+ if (sensitiveName) {
361
+ if (gitRoot) {
362
+ const tracked = isGitTracked(f.rel);
363
+ const ignored = isIgnored(f.rel);
364
+ if (tracked && !ignored) {
365
+ add("critical", `Sensitive file committed to git: ${f.rel}`, "Remove from git history (e.g. git-filter-repo), add to .gitignore, rotate any exposed secrets.");
366
+ } else if (!tracked) {
367
+ add("warn", `Sensitive file untracked: ${f.rel}`, "Ensure it is listed in .gitignore and never committed.");
368
+ } else if (ignored) {
369
+ add("info", `Sensitive file present but gitignored: ${f.rel}`, "OK — ensure secrets are rotated if previously committed.");
370
+ }
371
+ } else {
372
+ add("warn", `Sensitive file found (not a git repo): ${f.rel}`, "Review permissions and consider encrypting or moving to a secret manager.");
373
+ }
374
+ }
375
+
376
+ // secret content scan (small text-ish files only)
377
+ if (!f.dir && (CODE_EXT.has(path.extname(basename)) || [".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env", ".sh"].includes(path.extname(basename).toLowerCase()) || basename.startsWith(".env"))) {
378
+ if (size > 0 && size < AUDIT_MAX_SCAN * 4) {
379
+ let content = "";
380
+ try {
381
+ const fd = await fs.open(abs, "r");
382
+ const buf = Buffer.alloc(Math.min(size, AUDIT_MAX_SCAN));
383
+ await fd.read(buf, 0, buf.length, 0);
384
+ await fd.close();
385
+ content = buf.toString("utf8");
386
+ } catch { continue; }
387
+ for (const re of AUDIT_SECRET_CONTENT_RE) {
388
+ if (re.test(content)) {
389
+ add("critical", `Possible secret/hardcoded credential in ${f.rel}`, "Move to environment vars, secret manager, or encrypted vault. Rotate leaked value immediately.");
390
+ break;
391
+ }
392
+ }
393
+ }
394
+ }
395
+ }
396
+
397
+ // --- duplicate / inconsistent configs ---
398
+ const envFiles = files.filter((f) => path.basename(f.rel).startsWith(".env"));
399
+ if (envFiles.length > 2) {
400
+ add("info", `Multiple env files found (${envFiles.map((f) => f.rel).join(", ")})`, "Consolidate variants into .env, .env.local, .env.production. Ensure .gitignore covers all .env* except .env.example.");
401
+ }
402
+
403
+ // --- render ---
404
+ const icon = { critical: "🔴", warn: "🟡", info: "🟢" };
405
+ const groups = { critical: [], warn: [], info: [] };
406
+ for (const f of findings) groups[f.severity].push(f);
407
+ const lines = [`# Audit: ${path.basename(root)}`, `Path: ${root}`, `Git repo: ${gitRoot ? gitRoot : "no"}`, ``];
408
+ for (const sev of ["critical", "warn", "info"]) {
409
+ if (!groups[sev].length) continue;
410
+ lines.push(`## ${icon[sev]} ${sev.toUpperCase()} (${groups[sev].length})`);
411
+ for (const f of groups[sev]) {
412
+ lines.push(`- ${f.message}`);
413
+ if (f.fix) lines.push(` → Fix: ${f.fix}`);
414
+ }
415
+ lines.push("");
416
+ }
417
+ if (!findings.length) lines.push("🟢 No issues detected.");
418
+ return { content: [{ type: "text", text: lines.join("\n") }] };
419
+ }
420
+
249
421
  async run() {
250
422
  const t = new StdioServerTransport();
251
423
  await this.server.connect(t);
@@ -36,7 +36,7 @@ import { deterministicEnabled } from "./env.js";
36
36
 
37
37
  const ROOT =
38
38
  process.env.JOURNAL_DIR ||
39
- path.join(os.homedir(), ".codex", "memories", "journal");
39
+ path.join(os.homedir(), ".ai-shared-memory", "journal");
40
40
 
41
41
  const MAX_TEXT = 20_000;
42
42
 
@@ -170,6 +170,28 @@ class DevJournalServer {
170
170
  properties: { dir: { type: "string", description: "Project directory (defaults to CWD)" } },
171
171
  },
172
172
  },
173
+ {
174
+ name: "journal_daily",
175
+ description: "Read or create today's daily note for the project (Obsidian-style YYYY-MM-DD.md under daily/). Append mode adds a line; read mode returns the note. Great for a vibecoder's daily log that survives compaction.",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ dir: { type: "string", description: "Project directory (defaults to CWD)" },
180
+ date: { type: "string", description: "Optional YYYY-MM-DD (defaults to today)" },
181
+ append: { type: "string", description: "If provided, append this line to the daily note" },
182
+ },
183
+ },
184
+ },
185
+ {
186
+ name: "initialize_agent_session",
187
+ description: "Initialize the current agent session. Run this ONCE when the agent starts in a new project to perform a handshake and get contextual handoff.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ dir: { type: "string", description: "Project directory (defaults to CWD)" },
192
+ },
193
+ },
194
+ },
173
195
  ],
174
196
  }));
175
197
 
@@ -183,6 +205,8 @@ class DevJournalServer {
183
205
  case "journal_timeline": return await this.timeline(args || {});
184
206
  case "journal_search": return await this.search(args || {});
185
207
  case "journal_clear_handoff": return await this.clearHandoff(args || {});
208
+ case "journal_daily": return await this.daily(args || {});
209
+ case "initialize_agent_session": return await this.initializeAgent(args || {});
186
210
  default: throw new Error(`Unknown tool: ${name}`);
187
211
  }
188
212
  } catch (e) {
@@ -198,9 +222,34 @@ class DevJournalServer {
198
222
  const ts = (tsArg && /^\d{4}-\d{2}-\d{2}/.test(String(tsArg))) ? String(tsArg) : nowIso();
199
223
  const entry = { ts, type, title, body };
200
224
  await this.append(p, entry);
225
+ await this.appendDaily(p, ts.slice(0, 10), `- [${type}] ${title}${body ? ` — ${body.split("\n")[0].slice(0, 160)}` : ""}`);
201
226
  return { content: [{ type: "text", text: `Logged [${type}] "${title}" to ${p.slug}` }] };
202
227
  }
203
228
 
229
+ // Obsidian-style daily note: one file per date under daily/. Read or append.
230
+ async appendDaily(p, date, line) {
231
+ const dir = path.join(p.dir, "daily");
232
+ await fs.mkdir(dir, { recursive: true });
233
+ const file = path.join(dir, `${date}.md`);
234
+ const stamp = new Date().toISOString().slice(11, 19);
235
+ await fs.appendFile(file, `- ${stamp} ${line}\n`);
236
+ }
237
+
238
+ async daily({ dir, date, append } = {}) {
239
+ const p = this.paths(dir);
240
+ const day = (date && /^\d{4}-\d{2}-\d{2}$/.test(String(date))) ? String(date) : nowIso().slice(0, 10);
241
+ const dirPath = path.join(p.dir, "daily");
242
+ const file = path.join(dirPath, `${day}.md`);
243
+ if (append && String(append).trim()) {
244
+ await this.appendDaily(p, day, String(append).trim());
245
+ return { content: [{ type: "text", text: `Appended to daily note ${day}.md for ${p.slug}` }] };
246
+ }
247
+ let text;
248
+ try { text = await fs.readFile(file, "utf8"); }
249
+ catch { return { content: [{ type: "text", text: `No daily note for ${day} in ${p.slug} yet.` }] }; }
250
+ return { content: [{ type: "text", text: `# Daily — ${day}\n\n${text}` }] };
251
+ }
252
+
204
253
  async doHandoff({ summary, next_steps, open_questions, active_files, dir }) {
205
254
  summary = clip(summary, "summary", MAX_TEXT);
206
255
  const p = this.paths(dir);
@@ -301,6 +350,30 @@ class DevJournalServer {
301
350
  return { content: [{ type: "text", text: `Cleared handoff for ${p.slug}` }] };
302
351
  }
303
352
 
353
+ async initializeAgent({ dir }) {
354
+ const agentName = process.env.MCP_AGENT_NAME || 'Unknown-Agent';
355
+ const resumeData = await this.resume({ dir, recent: 5 });
356
+
357
+ let rules = "";
358
+ try {
359
+ const p = this.paths(dir);
360
+ const rulePath = path.join(p.root, ".ai", "AGENTS.md");
361
+ const r = await fs.readFile(rulePath, "utf8");
362
+ rules = `\n\n## Project Rules (.ai/AGENTS.md)\n${r}`;
363
+ } catch {
364
+ try {
365
+ const rootRule = path.join(this.paths(dir).root, "AGENTS.md");
366
+ const rr = await fs.readFile(rootRule, "utf8");
367
+ rules = `\n\n## Project Rules (AGENTS.md)\n${rr}`;
368
+ } catch {}
369
+ }
370
+
371
+ const greeting = `# Universal MCP Handshake\nAgent: ${agentName}\n\n`;
372
+ const content = greeting + resumeData.content[0].text + rules;
373
+
374
+ return { content: [{ type: "text", text: content }] };
375
+ }
376
+
304
377
  async run() {
305
378
  const t = new StdioServerTransport();
306
379
  await this.server.connect(t);
@@ -0,0 +1,74 @@
1
+ # 🚀 Rencana Kerja & Roadmap Masa Depan — codex-dev-mcp-suite
2
+
3
+ > **Repository:** [verrysimatupang99/codex-dev-mcp-suite](https://github.com/verrysimatupang99/codex-dev-mcp-suite)
4
+ > **Versi Terbaru:** `v1.7.0` (Obsidian Vault Grade & Codebase Security Audit)
5
+ > **Lisensi:** MIT
6
+
7
+ ---
8
+
9
+ ## 📌 Status Terkini (v1.7.0)
10
+
11
+ Rilis `v1.7.0` baru saja digabung (*merged*) ke `master` & dipublikasikan dengan pencapaian:
12
+ - ✅ **Obsidian Vault Parity**: Dukungan `.obsidian` setup otomatis, `memory_moc` (Map of Content), `memory_graph`, dan format Markdown standar Obsidian (`[[WikiLinks]]`).
13
+ - ✅ **Codebase Security Audit (`pack_audit`)**: Tool otomatis untuk deteksi kebocoran file sensitif (`.env`, `.pem`, `id_rsa`), deteksi kunci API ter-hardcode, file besar, dan kelengkapan `.gitignore`.
14
+ - ✅ **Stabilitas & Test**: **97 / 97 unit test (100% PASS)** di seluruh 4 server MCP + 3 utility CLI.
15
+
16
+ ---
17
+
18
+ ## 🎯 Roadmap Pengembangan Kedepan
19
+
20
+ ```mermaid
21
+ graph TD
22
+ v17["v1.7.0 (Current)
23
+ Obsidian Vault & Security Audit"] --> v18["v1.8.0 (Q3 2026)
24
+ Local Offline Embeddings"]
25
+ v18 --> v19["v1.9.0 (Q4 2026)
26
+ Multi-Agent & Cross-Project"]
27
+ v19 --> v20["v2.0.0 (Q1 2027)
28
+ Obsidian Live Sync & Web Dashboard"]
29
+ ```
30
+
31
+ ---
32
+
33
+ ### 1. 🟢 v1.8.0 — Local Offline Semantic Search & ONNX Embedding
34
+ > **Fokus:** Menjadikan `project-memory` 100% mandiri tanpa butuh API key eksternal untuk pencarian semantik.
35
+
36
+ - [ ] **Engine Embedding Lokal (WASM/ONNX)**:
37
+ - Integrasi `@xenova/transformers` / ONNX Runtime Web dengan model ringan (`all-MiniLM-L6-v2` atau `bge-small-en-v1.5`).
38
+ - Pencarian semantik otomatis berfungsi saat laptop *offline* / tanpa jaringan internet.
39
+ - [ ] **Fast Keyword-Semantic Hybrid Search**:
40
+ - Penggabungan BM25 / TF-IDF keyword search dengan cosine similarity vektor lokal.
41
+ - [ ] **Smart Cache Vector Storage**:
42
+ - Menyimpan cache vektor di `.project-memory/vectors.json` tanpa membuat ketergantungan database eksternal.
43
+
44
+ ---
45
+
46
+ ### 2. 🔵 v1.9.0 — Multi-Agent Sync & Cross-Project Memory
47
+ > **Fokus:** Koordinasi memori antar sub-agent AI dan pengelolaan memori lintas proyek.
48
+
49
+ - [ ] **Multi-Agent Timeline Stream (`devjournal_stream`)**:
50
+ - Live event stream untuk melihat log & status pengerjaan subagent secara real-time.
51
+ - [ ] **Cross-Project Memory Link (`memory_crosslink`)**:
52
+ - Menghubungkan dokumentasi/solusi bug dari proyek A ke proyek B (misal: solusi konfigurasi server/VPS bisa dipakai ulang di proyek aplikasi).
53
+ - [ ] **Subagent Handoff Lock (`handoff_claim`)**:
54
+ - Mencegah dua subagent menimpa *handoff log* yang sama secara bersamaan.
55
+
56
+ ---
57
+
58
+ ### 3. 🟣 v2.0.0 — Native IDE GUI & Obsidian Live 2-Way Sync
59
+ > **Fokus:** Pengalaman pengguna (*Developer Ergonomics*) tingkat lanjut & visualisasi grafik.
60
+
61
+ - [ ] **Obsidian 2-Way Live Sync**:
62
+ - Sinkronisasi otomatis secara dua arah: editan di Obsidian Desktop langsung ter-update di `project-memory` AI, dan sebaliknya.
63
+ - [ ] **Web Dashboard & GUI (`npx codex-dev-mcp-suite ui`)**:
64
+ - Dashboard web lokal berbasis Svelte/React ringan untuk melihat grafik proyek, statistik memori, checkpoint diff, dan timeline devjournal.
65
+ - [ ] **Git Pre-commit Hook Integration**:
66
+ - Integrasi `pack_audit` langsung ke `.git/hooks/pre-commit` untuk mencegah developer sengaja/tidak sengaja memuat `.env` atau *secret key* ke repository.
67
+
68
+ ---
69
+
70
+ ### 🟡 Peningkatan Berkelanjutan (Ongoing Maintenance)
71
+ - [ ] **Templates Kredensial & Preset MCP**:
72
+ - Konfigurasi siap pakai untuk Cursor, Claude Code, Windsurf, VS Code, dan Antigravity.
73
+ - [ ] **CI/CD Auto-Publish**:
74
+ - GitHub Actions pipeline untuk auto-build & test saat ada `tag` baru.
@@ -0,0 +1,13 @@
1
+ # Provider smoke matrix
2
+
3
+ _Generated 2026-06-17T01:58:41.412Z_
4
+
5
+ | Provider | Kind | Status | Latency | HTTP | Sample / dim | Error |
6
+ |---|---|---|---|---|---|---|
7
+ | groq | chat | ✓ | 430ms | 200 | `OK` | |
8
+ | cerebras | chat | ✓ | 554ms | 200 | — | |
9
+ | openai | chat | ✗ | 158ms | 401 | — | {"error":{"message":"unauthorized client detected, contact support for assistance at https://discord.com/invite/V6kaP6Rg44"},"message":"UNAUTHENTICATED","success":false,"type":"unauthorized_client_err |
10
+ | openai | embed | ✗ | 147ms | 401 | — | {"error":{"message":"unauthorized client detected, contact support for assistance at https://discord.com/invite/V6kaP6Rg44"},"message":"UNAUTHENTICATED","success":false,"type":"unauthorized_client_err |
11
+ | anthropic | chat | ✓ | 5.83s | 200 | — | |
12
+ | cloudflare | chat | ✗ | 907ms | 400 | — | {"errors":[{"message":"AiError: Bad input: Error: oneOf at '/' not met, 0 matches: required properties at '/' are 'text', required properties at '/' are 'requests' (f3f58c5f-7207-461a-96e1-d809e15ede3 |
13
+ | cloudflare | embed | ✓ | 194ms | 200 | dim=768 | |
@@ -0,0 +1,60 @@
1
+ # Hardening v1.2 Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add CLI UX (help/version/doctor), secret redaction, provider validation, per-provider timeout + 429/5xx cooldown, and a packaged-tarball CI smoke test, without breaking existing behavior.
6
+
7
+ **Architecture:** Add a shared bin meta-handler that intercepts `--help/--version/--doctor` before starting the stdio server. Add redaction + provider diagnostics + cooldown helpers in the provider layer. Wire rerank chat calls through cooldown-aware provider iteration. Extend CI with a tarball install smoke job.
8
+
9
+ **Tech Stack:** Node.js ESM, existing MCP SDK servers, built-in `http`/`https`, offline test harness, GitHub Actions.
10
+
11
+ ---
12
+
13
+ ### Task 1: Bin CLI Meta (help/version/doctor)
14
+
15
+ **Files:**
16
+ - Create: `bin/_meta.mjs`
17
+ - Modify: `bin/project-memory.mjs`, `bin/devjournal.mjs`, `bin/checkpoint.mjs`, `bin/context-pack.mjs`
18
+
19
+ - [ ] Add `handleCliMeta({ bin, title, serverDir })` that handles `-h/--help`, `-v/--version`, `--doctor` then exits; returns false otherwise.
20
+ - [ ] Read version from root `package.json`.
21
+ - [ ] `--doctor` prints storage + model/provider config with API keys redacted (never raw values).
22
+ - [ ] Each bin calls meta first, then imports its server only when not handled.
23
+
24
+ ### Task 2: Redaction + Provider Diagnostics + Cooldown
25
+
26
+ **Files:**
27
+ - Modify: `project-memory/provider-chain.js`, `devjournal/provider-chain.js`
28
+
29
+ - [ ] Add `redactKey(value)` returning `set (<n> chars)` / `not set`, never the value.
30
+ - [ ] Add `providerChainDiagnostics()` listing active providers (redacted) and incomplete-slot issues.
31
+ - [ ] Add pure cooldown helpers `recordOutcome(key, ok, now)` and `isCoolingDown(key, now)` using `MCP_PROVIDER_COOLDOWN_MS` (default 60000).
32
+
33
+ ### Task 3: Cooldown-Aware Rerank
34
+
35
+ **Files:**
36
+ - Modify: `project-memory/rerank.js`, `devjournal/rerank.js`
37
+
38
+ - [ ] Make provider chat return status so 429/5xx/network failures are retryable.
39
+ - [ ] Skip providers currently cooling down; record outcomes.
40
+ - [ ] Stop after first success; fall back to keyword if all fail. Never log keys/bodies.
41
+
42
+ ### Task 4: Tests
43
+
44
+ **Files:**
45
+ - Modify: `project-memory/test.mjs`, `devjournal/test.mjs`
46
+
47
+ - [ ] Test `providerChainDiagnostics()` flags an incomplete slot and redacts keys.
48
+ - [ ] Test cooldown helpers: failure starts cooldown, expiry clears it, success clears it.
49
+ - [ ] Test bin `--version` prints package version and `--doctor` output excludes raw key value.
50
+
51
+ ### Task 5: CI Tarball Smoke + Verify
52
+
53
+ **Files:**
54
+ - Modify: `.github/workflows/ci.yml`
55
+ - Modify: `CHANGELOG.md`, `package.json`, `docs/configuration.md`, `.env.example`
56
+
57
+ - [ ] Add CI job: `npm pack`, install tarball in temp dir, run each bin `--version`.
58
+ - [ ] Document `--doctor` and `MCP_PROVIDER_COOLDOWN_MS`.
59
+ - [ ] Bump version to 1.2.0 and add changelog entry.
60
+ - [ ] Run `npm test` and `npm pack --dry-run`; expect all pass.
@@ -0,0 +1,65 @@
1
+ # Provider Chain v1.1 Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add optional numbered OpenAI-compatible provider fallback for rerank/chat calls while preserving deterministic no-network mode and legacy env compatibility.
6
+
7
+ **Architecture:** Add focused provider-chain helpers to `project-memory` and `devjournal`, then route existing rerank chat calls through those helpers. Provider slots are built from `MCP_PROVIDER_PRIMARY_*`, `MCP_PROVIDER_CHAIN2_*`, `MCP_PROVIDER_CHAIN3_*`, etc.; if absent, existing `MCP_RERANK_*`/`MCP_LLM_*`/legacy config is used.
8
+
9
+ **Tech Stack:** Node.js ESM, existing MCP SDK servers, built-in `http`/`https`, existing offline test harness.
10
+
11
+ ---
12
+
13
+ ### Task 1: Provider Chain Helpers
14
+
15
+ **Files:**
16
+ - Create: `project-memory/provider-chain.js`
17
+ - Create: `devjournal/provider-chain.js`
18
+ - Test: `project-memory/test.mjs`
19
+ - Test: `devjournal/test.mjs`
20
+
21
+ - [ ] Create a `providerEnv(slotPrefix)` helper that reads `NAME`, `BASE_URL`, `API_KEY`, and `MODEL` from a prefix such as `MCP_PROVIDER_PRIMARY`.
22
+ - [ ] Create `providerChainConfig()` returning `{ providers, enabled, deterministic }`.
23
+ - [ ] Include `MCP_PROVIDER_PRIMARY` first when complete.
24
+ - [ ] Include every complete numbered slot matching `MCP_PROVIDER_CHAIN<N>` in numeric order for `N >= 2`.
25
+ - [ ] Fall back to one legacy provider from existing rerank/LLM envs when no numbered slots exist.
26
+ - [ ] Return disabled config if deterministic mode is on.
27
+ - [ ] Add tests for primary-only, primary+chain2+chain3 ordering, arbitrary chain4, incomplete slot skip, deterministic disable, and legacy fallback.
28
+
29
+ ### Task 2: Rerank Uses Provider Chain
30
+
31
+ **Files:**
32
+ - Modify: `project-memory/rerank.js`
33
+ - Modify: `devjournal/rerank.js`
34
+ - Test: existing `npm test`
35
+
36
+ - [ ] Replace single `BASE`/`KEY`/`MODEL` request selection with `providerChainConfig()`.
37
+ - [ ] Try providers in order for each chat request.
38
+ - [ ] Return first valid OpenAI-compatible response.
39
+ - [ ] On non-200, timeout, network error, invalid JSON, or SSE parse failure, try the next provider.
40
+ - [ ] Keep returning `null` if every provider fails so keyword fallback still works.
41
+ - [ ] Never log API keys or response bodies.
42
+
43
+ ### Task 3: Docs and Examples
44
+
45
+ **Files:**
46
+ - Modify: `docs/configuration.md`
47
+ - Modify: `docs/privacy.md`
48
+ - Modify: `.env.example`
49
+ - Modify: `CHANGELOG.md`
50
+ - Modify: `README.md` if needed
51
+
52
+ - [ ] Document numbered provider slots.
53
+ - [ ] Document Groq/Cerebras/OpenRouter as recommended examples, not required providers.
54
+ - [ ] Clarify users can use any OpenAI-compatible base URL, token, and model ID.
55
+ - [ ] Clarify deterministic mode overrides provider chain.
56
+ - [ ] Add changelog entry for v1.1 unreleased.
57
+
58
+ ### Task 4: Verification
59
+
60
+ **Files:**
61
+ - No source changes.
62
+
63
+ - [ ] Run `npm test` and expect 33+ tests all pass.
64
+ - [ ] Run `npm pack --dry-run` and confirm runtime provider-chain helpers are included.
65
+ - [ ] Run secret scan on changed/staged files and confirm no real secrets.
@@ -0,0 +1,41 @@
1
+ # Provider- and Client-Agnostic Config Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make the MCP suite configurable for any OpenAI-compatible gateway and document usage beyond Codex.
6
+
7
+ **Architecture:** Keep backward compatibility with existing `NINEROUTER_*` env vars while introducing neutral `MCP_*` env names. Documentation separates generic install, provider config, and client-specific config examples.
8
+
9
+ **Tech Stack:** Node.js MCP servers, OpenAI-compatible HTTP APIs for embeddings/rerank, Markdown docs.
10
+
11
+ ---
12
+
13
+ ### Task 1: Neutral Provider Env Aliases
14
+
15
+ **Files:**
16
+ - Modify: `project-memory/embedding.js`
17
+ - Modify: `project-memory/rerank.js`
18
+ - Modify: `devjournal/rerank.js`
19
+ - Test: existing `npm test`
20
+
21
+ - [ ] Add `MCP_EMBED_BASE_URL`, `MCP_EMBED_API_KEY`, `MCP_EMBED_MODEL` with fallback to existing `NINEROUTER_URL`, `NINEROUTER_KEY`, `EMBED_MODEL`.
22
+ - [ ] Add `MCP_LLM_BASE_URL`, `MCP_LLM_API_KEY`, `MCP_RERANK_MODEL` with fallback to existing `NINEROUTER_URL`, `NINEROUTER_KEY`, `RERANK_MODEL`.
23
+ - [ ] Keep default disabled behavior when no key/url/model is configured.
24
+ - [ ] Run `npm test` and expect all tests pass.
25
+
26
+ ### Task 2: Client-Agnostic Docs
27
+
28
+ **Files:**
29
+ - Create: `docs/configuration.md`
30
+ - Create: `docs/clients/codex.md`
31
+ - Create: `docs/clients/claude-code.md`
32
+ - Create: `docs/clients/generic-mcp.md`
33
+ - Modify: `.env.example`
34
+ - Modify: `README.md`
35
+
36
+ - [ ] Document neutral env names and legacy aliases.
37
+ - [ ] Provide Codex TOML config using npm bin commands.
38
+ - [ ] Provide Claude Code MCP JSON config using npm bin commands.
39
+ - [ ] Provide generic MCP JSON config.
40
+ - [ ] Update README to call the suite “Dev MCP Suite” while preserving package name.
41
+ - [ ] Run tests again after docs changes.