mnemos-capture 1.0.6 → 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.
@@ -0,0 +1,303 @@
1
+ import { homedir, tmpdir } from "os";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { execSync } from "child_process";
5
+ const HOSTED_URL = "https://mnemos-capture.vercel.app/api/mcp";
6
+ const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
7
+ // Matches both legacy ("mnemos-capture inbox-check") and modern
8
+ // ("mnemos-capture@latest inbox-check") hook command forms.
9
+ function isMnemosHook(cmd) {
10
+ return cmd.includes("mnemos-capture") && cmd.includes("inbox-check");
11
+ }
12
+ function isMnemosPreToolHook(cmd) {
13
+ return cmd.includes("mnemos-capture") && cmd.includes("vault-check");
14
+ }
15
+ function readSettings() {
16
+ if (!existsSync(CLAUDE_SETTINGS_PATH))
17
+ return {};
18
+ try {
19
+ return JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ function writeSettings(settings) {
26
+ const dir = join(homedir(), ".claude");
27
+ if (!existsSync(dir))
28
+ mkdirSync(dir, { recursive: true });
29
+ writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
30
+ }
31
+ // ── Session state for vault deduplication ──
32
+ function sessionStateFile() {
33
+ const sessionId = process.env["CLAUDE_SESSION_ID"] ?? new Date().toISOString().split("T")[0] ?? "default";
34
+ return join(tmpdir(), `mnemos-vault-${sessionId}.json`);
35
+ }
36
+ function readSessionSurfaced() {
37
+ try {
38
+ const file = sessionStateFile();
39
+ if (!existsSync(file))
40
+ return [];
41
+ return JSON.parse(readFileSync(file, "utf-8"));
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ function appendSessionSurfaced(filenames) {
48
+ try {
49
+ const file = sessionStateFile();
50
+ const existing = readSessionSurfaced();
51
+ const updated = [...new Set([...existing, ...filenames])];
52
+ writeFileSync(file, JSON.stringify(updated));
53
+ }
54
+ catch {
55
+ // best-effort
56
+ }
57
+ }
58
+ // ── Stdin reader for hook context ──
59
+ async function readStdin() {
60
+ return new Promise((resolve) => {
61
+ if (process.stdin.isTTY) {
62
+ resolve("");
63
+ return;
64
+ }
65
+ let data = "";
66
+ process.stdin.setEncoding("utf-8");
67
+ process.stdin.on("data", (chunk) => { data += chunk; });
68
+ process.stdin.on("end", () => resolve(data));
69
+ setTimeout(() => resolve(data), 2000);
70
+ });
71
+ }
72
+ export function setupHooks(apiKey, opts = {}) {
73
+ const settings = readSettings();
74
+ const hooks = settings.hooks ?? {};
75
+ const sessionStart = hooks.SessionStart ?? [];
76
+ // Inspect any existing mnemos hook so we can either skip, migrate, or replace
77
+ let existingCmd = null;
78
+ for (const group of sessionStart) {
79
+ for (const h of group.hooks ?? []) {
80
+ if (typeof h.command === "string" && isMnemosHook(h.command)) {
81
+ existingCmd = h.command;
82
+ break;
83
+ }
84
+ }
85
+ if (existingCmd)
86
+ break;
87
+ }
88
+ // skipIfExists (serve-mcp auto-install): preserve user-configured hooks but
89
+ // silently migrate legacy hooks that don't use @latest so they always stay fresh.
90
+ if (existingCmd && opts.skipIfExists) {
91
+ const alreadyLatest = existingCmd.includes("mnemos-capture@latest");
92
+ if (alreadyLatest)
93
+ return;
94
+ // Migration: preserve the existing --briefing flag when upgrading to @latest
95
+ opts = { ...opts, briefing: existingCmd.includes("--briefing") };
96
+ }
97
+ const command = opts.briefing
98
+ ? `npx -y mnemos-capture@latest inbox-check --key ${apiKey} --briefing`
99
+ : `npx -y mnemos-capture@latest inbox-check --key ${apiKey}`;
100
+ // Remove any existing mnemos inbox-check hook (handles key rotation too)
101
+ const filtered = sessionStart
102
+ .map((group) => ({
103
+ ...group,
104
+ hooks: (group.hooks ?? []).filter((h) => !(typeof h.command === "string" && isMnemosHook(h.command))),
105
+ }))
106
+ .filter((group) => (group.hooks ?? []).length > 0);
107
+ filtered.push({ hooks: [{ type: "command", command }] });
108
+ settings.hooks = { ...hooks, SessionStart: filtered };
109
+ writeSettings(settings);
110
+ if (!opts.silent) {
111
+ console.log("Mnemos inbox hook installed.");
112
+ console.log(`Location: ${CLAUDE_SETTINGS_PATH}`);
113
+ if (opts.briefing) {
114
+ console.log("At the start of each Claude Code session you'll see a full project briefing.");
115
+ }
116
+ else {
117
+ console.log("At the start of each Claude Code session you'll see your inbox count.");
118
+ }
119
+ }
120
+ if (opts.vault) {
121
+ setupPreToolHook(apiKey, { silent: opts.silent, skipIfExists: opts.skipIfExists });
122
+ }
123
+ }
124
+ export function setupPreToolHook(apiKey, opts = {}) {
125
+ const settings = readSettings();
126
+ const hooks = settings.hooks ?? {};
127
+ const preToolCall = hooks.PreToolCall ?? [];
128
+ let existingCmd = null;
129
+ for (const group of preToolCall) {
130
+ for (const h of group.hooks ?? []) {
131
+ if (typeof h.command === "string" && isMnemosPreToolHook(h.command)) {
132
+ existingCmd = h.command;
133
+ break;
134
+ }
135
+ }
136
+ if (existingCmd)
137
+ break;
138
+ }
139
+ if (existingCmd && opts.skipIfExists) {
140
+ if (existingCmd.includes("mnemos-capture@latest"))
141
+ return;
142
+ }
143
+ const command = `npx -y mnemos-capture@latest vault-check --key ${apiKey}`;
144
+ const filtered = preToolCall
145
+ .map((group) => ({
146
+ ...group,
147
+ hooks: (group.hooks ?? []).filter((h) => !(typeof h.command === "string" && isMnemosPreToolHook(h.command))),
148
+ }))
149
+ .filter((group) => (group.hooks ?? []).length > 0);
150
+ filtered.push({ hooks: [{ type: "command", command }] });
151
+ settings.hooks = { ...hooks, PreToolCall: filtered };
152
+ writeSettings(settings);
153
+ if (!opts.silent) {
154
+ console.log("Mnemos vault hook installed (PreToolCall).");
155
+ console.log("Your knowledge vault will surface relevant captures as you edit files.");
156
+ }
157
+ }
158
+ function run(cmd) {
159
+ return execSync(cmd, { timeout: 2000, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
160
+ }
161
+ function detectProjectContext() {
162
+ const parts = [];
163
+ // Repo name
164
+ try {
165
+ const remote = run("git remote get-url origin");
166
+ const match = remote.match(/\/([^/]+?)(?:\.git)?$/);
167
+ const repo = match?.[1] ?? run("git rev-parse --show-toplevel").split("/").pop();
168
+ if (repo)
169
+ parts.push(`Project: ${repo}`);
170
+ }
171
+ catch {
172
+ const dir = process.cwd().split("/").pop();
173
+ if (dir)
174
+ parts.push(`Project: ${dir}`);
175
+ }
176
+ // Current branch
177
+ try {
178
+ const branch = run("git branch --show-current");
179
+ if (branch)
180
+ parts.push(`Branch: ${branch}`);
181
+ }
182
+ catch { }
183
+ // Recent commits (last 5)
184
+ try {
185
+ const log = run("git log --oneline -5");
186
+ if (log)
187
+ parts.push(`Recent commits:\n${log}`);
188
+ }
189
+ catch { }
190
+ // CLAUDE.md — first 800 chars gives the model real project intent
191
+ try {
192
+ const root = run("git rev-parse --show-toplevel");
193
+ const claudeMd = readFileSync(join(root, "CLAUDE.md"), "utf-8").slice(0, 800).trim();
194
+ if (claudeMd)
195
+ parts.push(`Project instructions (CLAUDE.md excerpt):\n${claudeMd}`);
196
+ }
197
+ catch { }
198
+ return parts.length > 0 ? parts.join("\n\n") : "current project";
199
+ }
200
+ export async function callMcp(apiKey, toolName, toolArgs, timeoutMs = 15000) {
201
+ const res = await fetch(HOSTED_URL, {
202
+ method: "POST",
203
+ headers: {
204
+ "Content-Type": "application/json",
205
+ Authorization: `Bearer ${apiKey}`,
206
+ },
207
+ body: JSON.stringify({
208
+ jsonrpc: "2.0",
209
+ method: "tools/call",
210
+ params: { name: toolName, arguments: toolArgs },
211
+ id: 1,
212
+ }),
213
+ signal: AbortSignal.timeout(timeoutMs),
214
+ });
215
+ if (!res.ok)
216
+ return null;
217
+ const data = (await res.json());
218
+ return data.result?.content?.[0]?.text ?? null;
219
+ }
220
+ export async function vaultCheck(apiKey) {
221
+ try {
222
+ const inputJson = await readStdin();
223
+ if (!inputJson.trim())
224
+ return;
225
+ let hookData;
226
+ try {
227
+ hookData = JSON.parse(inputJson);
228
+ }
229
+ catch {
230
+ return;
231
+ }
232
+ // Only act on file-editing tool calls
233
+ const editingTools = ["Edit", "Write", "MultiEdit"];
234
+ if (!hookData.tool_name || !editingTools.includes(hookData.tool_name))
235
+ return;
236
+ const filePath = hookData.tool_input?.["file_path"] ??
237
+ hookData.tool_input?.["path"];
238
+ if (!filePath)
239
+ return;
240
+ const projectCtx = detectProjectContext();
241
+ const activityContext = `Editing file: ${filePath}\nProject context:\n${projectCtx}`;
242
+ const sessionSurfaced = readSessionSurfaced();
243
+ const text = await callMcp(apiKey, "vault_scan", {
244
+ activity_context: activityContext,
245
+ session_surfaced: sessionSurfaced,
246
+ }, 8000);
247
+ if (!text?.trim())
248
+ return;
249
+ // Track surfaced filenames to avoid repetition within the session
250
+ const filenameMatches = text.match(/(?:inbox|applied|archived)\/[\w-]+\.md/g) ?? [];
251
+ if (filenameMatches.length > 0)
252
+ appendSessionSurfaced(filenameMatches);
253
+ process.stdout.write(text.trim() + "\n");
254
+ }
255
+ catch {
256
+ // Silent fail — never interrupt a tool call
257
+ }
258
+ }
259
+ export async function inboxCheck(apiKey, opts = {}) {
260
+ try {
261
+ if (opts.briefing) {
262
+ const projectContext = detectProjectContext();
263
+ const text = await callMcp(apiKey, "briefing", { project_context: projectContext });
264
+ if (!text?.trim())
265
+ return;
266
+ const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/);
267
+ let applyNow = [];
268
+ if (jsonMatch?.[1]) {
269
+ try {
270
+ const all = JSON.parse(jsonMatch[1]);
271
+ applyNow = all.filter((s) => s.applyNow);
272
+ }
273
+ catch {
274
+ applyNow = [];
275
+ }
276
+ }
277
+ // Strip the JSON block — display only the Markdown narrative
278
+ const displayText = text.replace(/```json[\s\S]*?```\n?/, "").trim();
279
+ process.stdout.write(displayText + "\n");
280
+ if (applyNow.length > 0) {
281
+ const filenames = applyNow.map((s) => `'${s.filename}'`).join(", ");
282
+ process.stdout.write(`\n---\nMnemos: ${applyNow.length} insight${applyNow.length === 1 ? "" : "s"} ready to apply now.\n` +
283
+ `Run generate_plan with selected_captures: [${filenames}] to get a full implementation plan.\n`);
284
+ }
285
+ return;
286
+ }
287
+ // Default: fast count via list_inbox (no LLM call)
288
+ const text = await callMcp(apiKey, "list_inbox", {});
289
+ if (!text)
290
+ return;
291
+ // list_inbox returns "N capture(s) in inbox:" or "Inbox is empty."
292
+ const match = text.match(/^(\d+)\s+capture/);
293
+ if (!match)
294
+ return;
295
+ const count = parseInt(match[1], 10);
296
+ if (count > 0) {
297
+ process.stdout.write(`Mnemos: ${count} capture${count === 1 ? "" : "s"} in inbox — run list_inbox to review\n`);
298
+ }
299
+ }
300
+ catch {
301
+ // Silent fail — never interrupt a session startup
302
+ }
303
+ }
package/dist/cli/index.js CHANGED
@@ -8,11 +8,58 @@ async function main() {
8
8
  return;
9
9
  }
10
10
  if (command === "serve-mcp") {
11
- // MCP server mode — proxies to hosted instance
12
11
  const { serveMcp } = await import("./mcp-server.js");
13
12
  await serveMcp();
14
13
  return;
15
14
  }
15
+ if (command === "setup-hooks") {
16
+ const { setupHooks } = await import("./hooks.js");
17
+ const keyIdx = args.indexOf("--key");
18
+ const apiKey = keyIdx !== -1 ? args[keyIdx + 1] : undefined;
19
+ if (!apiKey) {
20
+ console.error("Usage: npx mnemos-capture setup-hooks --key YOUR_API_KEY [--briefing] [--vault]");
21
+ process.exit(1);
22
+ }
23
+ setupHooks(apiKey, { briefing: args.includes("--briefing"), vault: args.includes("--vault") });
24
+ return;
25
+ }
26
+ if (command === "inbox-check") {
27
+ const { inboxCheck } = await import("./hooks.js");
28
+ const keyIdx = args.indexOf("--key");
29
+ const apiKey = keyIdx !== -1 ? args[keyIdx + 1] : undefined;
30
+ if (!apiKey)
31
+ process.exit(0); // silent fail in hook context
32
+ await inboxCheck(apiKey, { briefing: args.includes("--briefing") });
33
+ return;
34
+ }
35
+ if (command === "vault-check") {
36
+ const { vaultCheck } = await import("./hooks.js");
37
+ const keyIdx = args.indexOf("--key");
38
+ const apiKey = keyIdx !== -1 ? args[keyIdx + 1] : undefined;
39
+ if (!apiKey)
40
+ process.exit(0); // silent fail in hook context
41
+ await vaultCheck(apiKey);
42
+ return;
43
+ }
44
+ if (command === "config") {
45
+ const { runConfigCommand } = await import("./config.js");
46
+ runConfigCommand(args.slice(1));
47
+ return;
48
+ }
49
+ if (command === "kos") {
50
+ const { kos } = await import("./kos.js");
51
+ const { readConfig } = await import("./config.js");
52
+ const keyIdx = args.indexOf("--key");
53
+ const apiKey = keyIdx !== -1 ? args[keyIdx + 1] : readConfig().key;
54
+ if (!apiKey) {
55
+ console.error("No API key. Pass --key YOUR_KEY or store it: mnemos config set key YOUR_KEY");
56
+ process.exit(1);
57
+ }
58
+ const planIdx = args.indexOf("--plan");
59
+ const plan = planIdx !== -1 ? args[planIdx + 1] : undefined;
60
+ await kos(apiKey, { plan });
61
+ return;
62
+ }
16
63
  // Default: open the hosted app
17
64
  console.log("");
18
65
  console.log(" Mnemos — Knowledge capture for agentic workflows\n");
@@ -21,7 +68,7 @@ async function main() {
21
68
  console.log(" First time? Sign in with GitHub — setup takes 30 seconds.");
22
69
  console.log("");
23
70
  console.log(" Connect to Claude Code:");
24
- console.log(" claude mcp add mnemos -- npx mnemos-capture serve-mcp --key YOUR_API_KEY");
71
+ console.log(" claude mcp add mnemos -- npx -y mnemos-capture@latest serve-mcp --key YOUR_API_KEY");
25
72
  console.log("");
26
73
  // Try to open the URL in the default browser
27
74
  const { exec } = await import("child_process");
@@ -33,9 +80,30 @@ function printHelp() {
33
80
  Mnemos — Knowledge capture for agentic workflows
34
81
 
35
82
  Usage:
36
- npx mnemos-capture Open Mnemos in your browser
37
- npx mnemos-capture serve-mcp Start the MCP server for Claude Code
38
- npx mnemos-capture help Show this help
83
+ npx mnemos-capture Open Mnemos in your browser
84
+ npx mnemos-capture serve-mcp --key KEY Start the MCP server for Claude Code
85
+ npx mnemos-capture setup-hooks --key KEY Install inbox count hook (fast)
86
+ npx mnemos-capture setup-hooks --key KEY --briefing Install full briefing hook (uses LLM)
87
+ npx mnemos-capture setup-hooks --key KEY --vault Install vault hook (PreToolCall)
88
+ npx mnemos-capture setup-hooks --key KEY --briefing --vault Install both hooks
89
+ npx mnemos-capture config set agent "claude -p" Set which AI assistant kos drives
90
+ npx mnemos-capture kos --key KEY [--plan FILE] Implement a plan in an isolated worktree
91
+ npx mnemos-capture help Show this help
92
+
93
+ MCP tools (via your AI assistant):
94
+ briefing — Session-start briefing with ranked insights to apply
95
+ generate_plan — Turn selected captures into a full implementation plan
96
+ list_plans — List or read saved implementation plans
97
+ vault_scan — Scan all captures for relevance to current activity
98
+ curate — Validate URLs and flag stale captures
99
+
100
+ kos — implement plans with your own AI assistant (model-agnostic):
101
+ 1. Tell Mnemos which assistant to drive (once):
102
+ mnemos config set agent "claude -p" # or "codex exec", "aider --yes --message"
103
+ 2. Generate a plan via the generate_plan MCP tool.
104
+ 3. Run: mnemos kos --key YOUR_KEY
105
+ kos creates an isolated git worktree, hands the plan to your assistant,
106
+ and reports the branch + the plan's Verification Checklist when done.
39
107
 
40
108
  Get started:
41
109
  1. Run: npx mnemos-capture
@@ -44,7 +112,18 @@ function printHelp() {
44
112
  4. Start capturing!
45
113
 
46
114
  Connect to Claude Code:
47
- claude mcp add mnemos -- npx mnemos-capture serve-mcp --key YOUR_API_KEY
115
+ claude mcp add mnemos -- npx -y mnemos-capture@latest serve-mcp --key YOUR_API_KEY
116
+
117
+ The @latest tag ensures every Claude Code session uses the most recent
118
+ published version. serve-mcp also self-upgrades on startup as a safety net
119
+ for users who registered without @latest.
120
+
121
+ The hook is installed automatically when you start the MCP server.
122
+ To upgrade to a full project briefing at session start:
123
+ npx mnemos-capture setup-hooks --key YOUR_API_KEY --briefing
124
+
125
+ To enable the vault (surfaces captures as you edit files — opt-in):
126
+ npx mnemos-capture setup-hooks --key YOUR_API_KEY --vault
48
127
  `);
49
128
  }
50
129
  main().catch((err) => {
@@ -0,0 +1,123 @@
1
+ /**
2
+ * kos — Mnemos' Knowledge Operations orchestrator.
3
+ *
4
+ * Drives whatever AI assistant the user configured (`mnemos config set agent ...`)
5
+ * to implement a generated plan in an isolated git worktree. Mnemos owns the
6
+ * harness-independent parts: fetching the plan, the worktree, and reporting the
7
+ * Verification Checklist. The assistant is a black box — model-agnostic by design.
8
+ */
9
+ import { tmpdir } from "os";
10
+ import { join } from "path";
11
+ import { writeFileSync, existsSync, appendFileSync, readFileSync } from "fs";
12
+ import { execSync } from "child_process";
13
+ import { callMcp } from "./hooks.js";
14
+ import { readConfig } from "./config.js";
15
+ // ── Pure helpers (exported for testing) ──
16
+ /** Extract `**filename.md**` plan names from a list_plans response. */
17
+ export function parsePlanFilenames(listText) {
18
+ const matches = listText.match(/\*\*([^*]+\.md)\*\*/g) ?? [];
19
+ return matches.map((m) => m.replace(/\*\*/g, ""));
20
+ }
21
+ /** Pick the newest plan. Filenames are date-prefixed (YYYY-MM-DD-...), so lexical desc = newest. */
22
+ export function latestPlanFilename(listText) {
23
+ const names = parsePlanFilenames(listText);
24
+ if (names.length === 0)
25
+ return null;
26
+ return [...names].sort((a, b) => b.localeCompare(a))[0] ?? null;
27
+ }
28
+ /** Pull the Verification Checklist items out of a plan's Markdown. */
29
+ export function parseChecklist(planText) {
30
+ const section = planText.match(/##\s*Verification Checklist\s*\n([\s\S]*?)(?=\n##\s|$)/i);
31
+ if (!section?.[1])
32
+ return [];
33
+ return section[1]
34
+ .split("\n")
35
+ .map((l) => l.trim())
36
+ .filter((l) => /^- \[[ x]\]/i.test(l))
37
+ .map((l) => l.replace(/^- \[[ x]\]\s*/i, ""));
38
+ }
39
+ /** Build a unique, sortable branch name for a kos run. */
40
+ export function buildKosBranch(now = new Date()) {
41
+ const stamp = now.toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
42
+ return `mnemos/kos-${stamp}`;
43
+ }
44
+ /** The instruction handed to the assistant. The plan file is the real contract. */
45
+ export const KOS_PROMPT = "Implement the plan in MNEMOS_PLAN.md in this repository. " +
46
+ "Follow the Codebase Mapping table exactly — change only the files it lists. " +
47
+ "For each step apply the thinking effort named in its Effort column " +
48
+ "(low = quick/mechanical, medium = careful, high = deep reasoning). " +
49
+ "When finished, make sure every item in the Verification Checklist is satisfied.";
50
+ // ── Orchestrator ──
51
+ function run(cmd, cwd) {
52
+ return execSync(cmd, { cwd, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
53
+ }
54
+ export async function kos(apiKey, opts = {}) {
55
+ const cfg = readConfig();
56
+ if (!cfg.agent) {
57
+ console.error("No agent configured. Tell Mnemos which AI assistant to drive:\n");
58
+ console.error(' mnemos config set agent "claude -p" # Claude Code');
59
+ console.error(' mnemos config set agent "codex exec" # Codex');
60
+ console.error(' mnemos config set agent "aider --yes --message" # Aider');
61
+ process.exit(1);
62
+ }
63
+ // 1. Resolve the plan — explicit filename, or the newest saved plan
64
+ const planFile = opts.plan ?? latestPlanFilename((await callMcp(apiKey, "list_plans", {}, 15000)) ?? "");
65
+ if (!planFile) {
66
+ console.error("No plans found. Generate one first with the generate_plan MCP tool.");
67
+ process.exit(1);
68
+ }
69
+ const planText = await callMcp(apiKey, "list_plans", { filename: planFile }, 15000);
70
+ if (!planText || !planText.trim()) {
71
+ console.error(`Could not read plan: ${planFile}`);
72
+ process.exit(1);
73
+ }
74
+ // 2. Must be inside a git repo
75
+ let repoRoot;
76
+ try {
77
+ repoRoot = run("git rev-parse --show-toplevel");
78
+ }
79
+ catch {
80
+ console.error("mnemos kos must be run inside a git repository.");
81
+ process.exit(1);
82
+ return;
83
+ }
84
+ // 3. Isolated worktree on a fresh branch
85
+ const branch = buildKosBranch();
86
+ const worktreePath = join(tmpdir(), branch.replace(/\//g, "-"));
87
+ console.log(`Creating isolated worktree on branch ${branch}...`);
88
+ run(`git worktree add -b ${branch} "${worktreePath}"`, repoRoot);
89
+ // 4. Drop the plan in and keep git from tracking it (local exclude, not committed)
90
+ writeFileSync(join(worktreePath, "MNEMOS_PLAN.md"), planText);
91
+ try {
92
+ const excludePath = join(repoRoot, ".git", "info", "exclude");
93
+ if (existsSync(excludePath)) {
94
+ const current = readFileSync(excludePath, "utf-8");
95
+ if (!current.includes("MNEMOS_PLAN.md"))
96
+ appendFileSync(excludePath, "\nMNEMOS_PLAN.md\n");
97
+ }
98
+ }
99
+ catch {
100
+ // best-effort
101
+ }
102
+ // 5. Hand off to the user's assistant, running inside the worktree
103
+ console.log(`Launching: ${cfg.agent}\n`);
104
+ try {
105
+ execSync(`${cfg.agent} ${JSON.stringify(KOS_PROMPT)}`, { cwd: worktreePath, stdio: "inherit" });
106
+ }
107
+ catch {
108
+ console.error("\nThe agent exited with an error. The worktree is preserved for inspection.");
109
+ }
110
+ // 6. Report — branch + the plan's own checklist as the review gate
111
+ const checklist = parseChecklist(planText);
112
+ console.log(`\n${"─".repeat(60)}`);
113
+ console.log(`kos finished. Plan: ${planFile}`);
114
+ console.log(`Branch: ${branch}`);
115
+ console.log(`Worktree: ${worktreePath}`);
116
+ if (checklist.length > 0) {
117
+ console.log(`\nVerify before merging:`);
118
+ for (const item of checklist)
119
+ console.log(` [ ] ${item}`);
120
+ }
121
+ console.log(`\nReview the branch, then merge. Clean up the worktree with:`);
122
+ console.log(` git worktree remove "${worktreePath}"`);
123
+ }
@@ -33,10 +33,30 @@ export async function serveMcp() {
33
33
  const apiKey = keyIdx !== -1 ? args[keyIdx + 1] : undefined;
34
34
  if (!apiKey) {
35
35
  process.stderr.write("\nMnemos MCP server requires an API key.\n");
36
- process.stderr.write("Usage: npx mnemos-capture serve-mcp --key <your-api-key>\n\n");
36
+ process.stderr.write("Usage: npx -y mnemos-capture@latest serve-mcp --key <your-api-key>\n\n");
37
37
  process.stderr.write("Get your API key at: https://mnemos-capture.vercel.app/onboard\n\n");
38
38
  process.exit(1);
39
39
  }
40
+ // Self-upgrade: if a newer version exists on npm, respawn via npx @latest.
41
+ // Handled before anything else so stdio isn't yet owned by the MCP loop.
42
+ try {
43
+ const { maybeSelfUpgrade } = await import("./version-check.js");
44
+ const upgrading = await maybeSelfUpgrade();
45
+ if (upgrading)
46
+ return; // child has taken over; parent will exit when child does
47
+ }
48
+ catch {
49
+ // Version check must never block startup
50
+ }
51
+ // Auto-install the inbox hook the first time the MCP server is registered.
52
+ // Uses silent mode so it never prints to stdout (which is reserved for MCP protocol).
53
+ try {
54
+ const { setupHooks } = await import("./hooks.js");
55
+ setupHooks(apiKey, { silent: true, skipIfExists: true });
56
+ }
57
+ catch {
58
+ // Non-fatal — hook setup should never block the MCP server from starting
59
+ }
40
60
  process.stderr.write("Mnemos MCP server starting (proxying to hosted instance)...\n");
41
61
  let buffer = "";
42
62
  process.stdin.setEncoding("utf-8");
@@ -0,0 +1,79 @@
1
+ // Self-update enforcement: ensures users always run the latest mnemos-capture.
2
+ //
3
+ // Strategy: on serve-mcp startup, query npm for the latest published version.
4
+ // If behind, respawn the process via `npx -y mnemos-capture@latest` and proxy
5
+ // stdio. A MNEMOS_UPGRADED=1 env var prevents infinite recursion.
6
+ import { readFileSync } from "fs";
7
+ import { fileURLToPath } from "url";
8
+ import { dirname, join } from "path";
9
+ import { spawn } from "child_process";
10
+ const NPM_REGISTRY = "https://registry.npmjs.org/mnemos-capture/latest";
11
+ const CHECK_TIMEOUT_MS = 2000;
12
+ export function getCurrentVersion() {
13
+ try {
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ // dist/cli/version-check.js → ../../package.json
16
+ const pkg = JSON.parse(readFileSync(join(here, "../../package.json"), "utf-8"));
17
+ return pkg.version ?? "0.0.0";
18
+ }
19
+ catch {
20
+ return "0.0.0";
21
+ }
22
+ }
23
+ export function isNewer(latest, current) {
24
+ const l = latest.split(".").map((n) => parseInt(n, 10) || 0);
25
+ const c = current.split(".").map((n) => parseInt(n, 10) || 0);
26
+ for (let i = 0; i < 3; i++) {
27
+ const lv = l[i] ?? 0;
28
+ const cv = c[i] ?? 0;
29
+ if (lv > cv)
30
+ return true;
31
+ if (lv < cv)
32
+ return false;
33
+ }
34
+ return false;
35
+ }
36
+ async function getLatestVersion() {
37
+ try {
38
+ const res = await fetch(NPM_REGISTRY, { signal: AbortSignal.timeout(CHECK_TIMEOUT_MS) });
39
+ if (!res.ok)
40
+ return null;
41
+ const data = (await res.json());
42
+ return data.version ?? null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * If a newer version exists on npm, respawn via `npx -y mnemos-capture@latest`
50
+ * with the original argv, piping stdio. Returns true if the caller should stop
51
+ * (because the child is taking over). Returns false otherwise — caller should
52
+ * continue with normal startup.
53
+ */
54
+ export async function maybeSelfUpgrade() {
55
+ // Recursion guard: the child spawned below will inherit this env var.
56
+ if (process.env.MNEMOS_UPGRADED === "1")
57
+ return false;
58
+ const current = getCurrentVersion();
59
+ const latest = await getLatestVersion();
60
+ if (!latest || !isNewer(latest, current))
61
+ return false;
62
+ process.stderr.write(`Mnemos: upgrading to v${latest} (was v${current})...\n`);
63
+ const child = spawn("npx", ["-y", `mnemos-capture@${latest}`, ...process.argv.slice(2)], {
64
+ stdio: "inherit",
65
+ env: { ...process.env, MNEMOS_UPGRADED: "1" },
66
+ });
67
+ // Keep parent alive until the child exits, then exit with its code.
68
+ await new Promise((resolve) => {
69
+ child.on("exit", (code) => {
70
+ process.exit(code ?? 0);
71
+ resolve();
72
+ });
73
+ child.on("error", () => {
74
+ // If spawn itself fails, fall through and let the parent continue
75
+ resolve();
76
+ });
77
+ });
78
+ return true;
79
+ }