mindlink 1.1.5 → 2.0.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/dist/cli.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import "./chunk-2H7UOFLK.js";
2
3
 
3
4
  // src/cli.ts
4
- import { Command as Command15 } from "commander";
5
- import chalk16 from "chalk";
5
+ import { Command as Command20 } from "commander";
6
+ import chalk20 from "chalk";
6
7
 
7
8
  // src/utils/version.ts
8
- var VERSION = "1.1.5";
9
+ var VERSION = "2.0.0";
9
10
 
10
11
  // src/commands/init.ts
11
12
  import { Command } from "commander";
@@ -24,13 +25,16 @@ import {
24
25
  mkdirSync as mkdirSync2,
25
26
  readFileSync as readFileSync2,
26
27
  writeFileSync as writeFileSync2,
27
- appendFileSync
28
+ appendFileSync,
29
+ readdirSync
28
30
  } from "fs";
31
+ import { execSync } from "child_process";
29
32
  import { join as join3, resolve, dirname as dirname2, basename } from "path";
30
33
 
31
34
  // src/utils/paths.ts
32
35
  import { fileURLToPath } from "url";
33
36
  import { dirname, join } from "path";
37
+ import { homedir } from "os";
34
38
  var __filename = fileURLToPath(import.meta.url);
35
39
  var __dirname = dirname(__filename);
36
40
  var TEMPLATES_DIR = join(__dirname, "templates");
@@ -38,6 +42,9 @@ var BRAIN_TEMPLATES_DIR = join(TEMPLATES_DIR, "brain");
38
42
  var AGENT_TEMPLATES_DIR = join(TEMPLATES_DIR, "agents");
39
43
  var HOOKS_TEMPLATES_DIR = join(TEMPLATES_DIR, "hooks");
40
44
  var BRAIN_DIR = ".brain";
45
+ var GLOBAL_MINDLINK_DIR = join(homedir(), ".mindlink");
46
+ var GLOBAL_USER_PROFILE_PATH = join(GLOBAL_MINDLINK_DIR, "USER.md");
47
+ var GLOBAL_WINDSURF_MCP_PATH = join(homedir(), ".codeium", "windsurf", "mcp_config.json");
41
48
 
42
49
  // src/utils/banner.ts
43
50
  import chalk from "chalk";
@@ -58,14 +65,18 @@ var AGENTS = [
58
65
  { value: "copilot", label: "GitHub Copilot", hint: ".github/copilot-instructions.md", templateFile: "copilot-instructions.md", destFile: ".github/copilot-instructions.md", selected: true },
59
66
  { value: "windsurf", label: "Windsurf", hint: ".windsurfrules", templateFile: ".windsurfrules", destFile: ".windsurfrules", selected: true },
60
67
  { value: "cline", label: "Cline", hint: ".clinerules", templateFile: ".clinerules", destFile: ".clinerules", selected: false },
61
- { value: "aider", label: "Aider", hint: "CONVENTIONS.md", templateFile: "CONVENTIONS.md", destFile: "CONVENTIONS.md", selected: false }
68
+ { value: "aider", label: "Aider", hint: "CONVENTIONS.md", templateFile: "CONVENTIONS.md", destFile: "CONVENTIONS.md", selected: false },
69
+ { value: "zed", label: "Zed", hint: ".rules", templateFile: ".rules", destFile: ".rules", selected: false },
70
+ { value: "kiro", label: "Kiro", hint: ".kiro/steering/mindlink.md", templateFile: "kiro-steering.md", destFile: ".kiro/steering/mindlink.md", selected: false },
71
+ { value: "continue", label: "Continue.dev", hint: ".continue/rules/mindlink.md", templateFile: "continue-rules.md", destFile: ".continue/rules/mindlink.md", selected: false },
72
+ { value: "trae", label: "Trae", hint: ".trae/rules/mindlink.md", templateFile: "trae-rules.md", destFile: ".trae/rules/mindlink.md", selected: false }
62
73
  ];
63
74
 
64
75
  // src/utils/registry.ts
65
76
  import { readFileSync, writeFileSync, mkdirSync } from "fs";
66
77
  import { join as join2 } from "path";
67
- import { homedir } from "os";
68
- var REGISTRY_DIR = join2(homedir(), ".mindlink");
78
+ import { homedir as homedir2 } from "os";
79
+ var REGISTRY_DIR = join2(homedir2(), ".mindlink");
69
80
  var REGISTRY_PATH = join2(REGISTRY_DIR, "projects.json");
70
81
  function load() {
71
82
  try {
@@ -94,19 +105,86 @@ function pruneRegistry(isValid) {
94
105
  if (pruned.length !== paths.length) save(pruned);
95
106
  }
96
107
 
108
+ // src/utils/content.ts
109
+ function sectionHasRealContent(markdown, heading) {
110
+ const lines = markdown.split("\n");
111
+ let inSection = false;
112
+ let headingLevel = 0;
113
+ for (const line of lines) {
114
+ const match = line.match(/^(#{1,6})\s+(.+)/);
115
+ if (match) {
116
+ const level = match[1].length;
117
+ const title = match[2].replace(/<!--.*?-->/g, "").trim();
118
+ if (title.toLowerCase() === heading.toLowerCase()) {
119
+ inSection = true;
120
+ headingLevel = level;
121
+ continue;
122
+ }
123
+ if (inSection && level <= headingLevel) {
124
+ break;
125
+ }
126
+ }
127
+ if (inSection) {
128
+ const t = line.trim();
129
+ if (t.length > 0 && !t.startsWith("#") && !t.startsWith("<!--") && !t.startsWith(">") && !t.startsWith("|") && t !== "---") {
130
+ return true;
131
+ }
132
+ }
133
+ }
134
+ return false;
135
+ }
136
+ function replaceSection(markdown, heading, newBody) {
137
+ const lines = markdown.split("\n");
138
+ let headingIdx = -1;
139
+ let nextSectionIdx = lines.length;
140
+ let headingLevel = 0;
141
+ for (let i = 0; i < lines.length; i++) {
142
+ const match = lines[i].match(/^(#{1,6})\s+(.+)/);
143
+ if (match) {
144
+ const level = match[1].length;
145
+ const title = match[2].replace(/<!--.*?-->/g, "").trim();
146
+ if (headingIdx < 0 && title.toLowerCase() === heading.toLowerCase()) {
147
+ headingIdx = i;
148
+ headingLevel = level;
149
+ } else if (headingIdx >= 0 && level <= headingLevel) {
150
+ nextSectionIdx = i;
151
+ break;
152
+ }
153
+ }
154
+ }
155
+ if (headingIdx < 0) return markdown;
156
+ const before = lines.slice(0, headingIdx + 1);
157
+ const after = lines.slice(nextSectionIdx);
158
+ return [...before, "", newBody.trim(), "", ...after].join("\n");
159
+ }
160
+ function countRealLines(markdown) {
161
+ return markdown.split("\n").filter((line) => {
162
+ const t = line.trim();
163
+ return t.length > 0 && !t.startsWith("#") && !t.startsWith("<!--") && !t.startsWith(">") && t !== "---";
164
+ }).length;
165
+ }
166
+
97
167
  // src/commands/init.ts
98
168
  function detectProjectInfo(projectPath) {
99
169
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
100
170
  let name = basename(projectPath);
101
171
  let description = "";
102
172
  let stack = "";
173
+ let recentActivity = "";
174
+ let topDirs = "";
103
175
  const pkgPath = join3(projectPath, "package.json");
104
176
  if (existsSync2(pkgPath)) {
105
177
  try {
106
178
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
107
179
  if (pkg.name) name = pkg.name;
108
180
  if (pkg.description) description = pkg.description;
109
- stack = "Node.js";
181
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
182
+ const layers = ["Node.js"];
183
+ if (deps["typescript"] || deps["ts-node"] || deps["tsup"]) layers.push("TypeScript");
184
+ if (deps["react"] || deps["next"]) layers.push(deps["next"] ? "Next.js" : "React");
185
+ if (deps["express"] || deps["fastify"] || deps["koa"]) layers.push("Express/Fastify");
186
+ if (deps["vite"]) layers.push("Vite");
187
+ stack = layers.join(" + ");
110
188
  } catch {
111
189
  }
112
190
  }
@@ -119,7 +197,83 @@ function detectProjectInfo(projectPath) {
119
197
  else if (existsSync2(join3(projectPath, "composer.json"))) stack = "PHP";
120
198
  else if (existsSync2(join3(projectPath, "Gemfile"))) stack = "Ruby";
121
199
  }
122
- return { name, description, stack, date };
200
+ if (!description) {
201
+ const readmePath = join3(projectPath, "README.md");
202
+ if (existsSync2(readmePath)) {
203
+ try {
204
+ const lines = readFileSync2(readmePath, "utf8").split("\n");
205
+ for (const line of lines.slice(0, 30)) {
206
+ const trimmed = line.trim();
207
+ if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("!") && !trimmed.startsWith("<") && trimmed.length > 10) {
208
+ description = trimmed.replace(/\*\*/g, "").slice(0, 120);
209
+ break;
210
+ }
211
+ }
212
+ } catch {
213
+ }
214
+ }
215
+ }
216
+ try {
217
+ const log = execSync("git log --oneline -5", { cwd: projectPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
218
+ if (log) {
219
+ recentActivity = log.split("\n").slice(0, 3).join(" \xB7 ");
220
+ }
221
+ } catch {
222
+ }
223
+ const SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".brain", "binaries", ".cache", "coverage", ".next", "out"]);
224
+ try {
225
+ const entries = readdirSync(projectPath, { withFileTypes: true });
226
+ const dirs = entries.filter((e) => e.isDirectory() && !SKIP_DIRS.has(e.name) && !e.name.startsWith(".")).map((e) => e.name).slice(0, 6);
227
+ if (dirs.length > 0) topDirs = dirs.join(", ");
228
+ } catch {
229
+ }
230
+ return { name, description, stack, recentActivity, topDirs, date };
231
+ }
232
+ function memoryHasRealContent(memoryPath) {
233
+ try {
234
+ const lines = readFileSync2(memoryPath, "utf8").split("\n");
235
+ return lines.some((line) => {
236
+ const t = line.trim();
237
+ return t.length > 0 && !t.startsWith("#") && !t.startsWith("<!--") && !t.startsWith(">") && !t.startsWith("|") && t !== "---";
238
+ });
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+ function injectUserProfile(memoryContent, profileContent) {
244
+ const profileLines = profileContent.split("\n");
245
+ const contentStart = profileLines.findIndex((l) => {
246
+ const t = l.trim();
247
+ return t.length > 0 && !t.startsWith("#") && !t.startsWith(">") && !t.startsWith("<!--");
248
+ });
249
+ const profileBody = contentStart >= 0 ? profileLines.slice(contentStart).join("\n").trim() : "";
250
+ if (!profileBody) return memoryContent;
251
+ const lines = memoryContent.split("\n");
252
+ let insertAt = -1;
253
+ let inProfile = false;
254
+ let profileHeadingLevel = 0;
255
+ for (let i = 0; i < lines.length; i++) {
256
+ const match = lines[i].match(/^(#{1,6})\s+(.+)/);
257
+ if (match) {
258
+ const level = match[1].length;
259
+ const title = match[2].replace(/<!--.*?-->/g, "").trim();
260
+ if (title.toLowerCase() === "user profile") {
261
+ inProfile = true;
262
+ profileHeadingLevel = level;
263
+ continue;
264
+ }
265
+ if (inProfile && level <= profileHeadingLevel) {
266
+ insertAt = i;
267
+ break;
268
+ }
269
+ }
270
+ if (inProfile && insertAt < 0 && i === lines.length - 1) {
271
+ insertAt = lines.length;
272
+ }
273
+ }
274
+ if (insertAt < 0) return memoryContent;
275
+ lines.splice(insertAt, 0, profileBody, "");
276
+ return lines.join("\n");
123
277
  }
124
278
  function buildMemoryMd(templateContent, info2) {
125
279
  let content = templateContent;
@@ -138,10 +292,15 @@ ${info2.stack}
138
292
  <!-- Add layers: Frontend, Backend, Infra, etc. -->`
139
293
  );
140
294
  }
295
+ const focusLines = [];
296
+ if (info2.topDirs) focusLines.push(`Directories: ${info2.topDirs}`);
297
+ if (info2.recentActivity) focusLines.push(`Recent commits: ${info2.recentActivity}`);
298
+ const focusBlock = focusLines.length > 0 ? focusLines.join("\n") + `
299
+ <!-- Initialized ${info2.date} \u2014 update to reflect the current active focus -->` : `<!-- Initialized ${info2.date} \u2014 ask your AI to fill this in after your first session -->`;
141
300
  content = content.replace(
142
301
  /### Current focus\n<!--[^]*?-->/,
143
302
  `### Current focus
144
- <!-- Initialized ${info2.date} \u2014 ask your AI to fill this in after your first session -->`
303
+ ${focusBlock}`
145
304
  );
146
305
  return content;
147
306
  }
@@ -161,41 +320,159 @@ Examples:
161
320
  const brainDir = join3(projectPath, BRAIN_DIR);
162
321
  printBanner();
163
322
  if (existsSync2(brainDir)) {
164
- if (opts.yes) {
323
+ const memoryPath = join3(brainDir, "MEMORY.md");
324
+ const hasMemory = existsSync2(memoryPath) && memoryHasRealContent(memoryPath);
325
+ if (!opts.yes && hasMemory) {
326
+ const hasAnyAgentFile = AGENTS.some((a) => existsSync2(join3(projectPath, a.destFile)));
327
+ if (!hasAnyAgentFile) {
328
+ console.log("");
329
+ console.log(` ${chalk2.cyan("\u25C9")} MindLink memory found in this project.`);
330
+ console.log(` ${chalk2.dim("MEMORY.md has content \u2014 this looks like a team project.")}`);
331
+ console.log("");
332
+ const action = await select({
333
+ message: "What would you like to do?",
334
+ options: [
335
+ { value: "restore", label: "Set up agent files", hint: "recommended for new team members \u2014 writes CLAUDE.md, .cursorrules, etc." },
336
+ { value: "reinit", label: "Full re-init", hint: "recreate everything, reconfigure settings" },
337
+ { value: "exit", label: "Cancel", hint: "" }
338
+ ]
339
+ });
340
+ if (isCancel(action) || action === "exit") {
341
+ process.exit(0);
342
+ }
343
+ if (action === "restore") {
344
+ const agentChoices = AGENTS.map((a) => ({
345
+ value: a.value,
346
+ label: `${a.label.padEnd(18)} ${chalk2.dim(a.hint)}`,
347
+ hint: a.selected ? "recommended" : void 0
348
+ }));
349
+ const agentResult = await multiselect({
350
+ message: "Which AI agents do you use?",
351
+ options: agentChoices,
352
+ initialValues: AGENTS.filter((a) => a.selected).map((a) => a.value),
353
+ required: false
354
+ });
355
+ if (isCancel(agentResult)) {
356
+ cancel("Cancelled.");
357
+ process.exit(0);
358
+ }
359
+ const toRestore = agentResult;
360
+ const restored = [];
361
+ for (const agentValue of toRestore) {
362
+ const agent = AGENTS.find((a) => a.value === agentValue);
363
+ if (!agent) continue;
364
+ const destPath = join3(projectPath, agent.destFile);
365
+ mkdirSync2(dirname2(destPath), { recursive: true });
366
+ writeFileSync2(destPath, readFileSync2(join3(AGENT_TEMPLATES_DIR, agent.templateFile), "utf8"));
367
+ restored.push(agent.destFile);
368
+ }
369
+ if (toRestore.includes("claude")) {
370
+ const hookDest = join3(projectPath, ".claude", "settings.json");
371
+ if (!existsSync2(hookDest)) {
372
+ mkdirSync2(dirname2(hookDest), { recursive: true });
373
+ const settings = JSON.parse(readFileSync2(join3(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
374
+ settings.mcpServers = {
375
+ mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
376
+ };
377
+ writeFileSync2(hookDest, JSON.stringify(settings, null, 2));
378
+ restored.push(".claude/settings.json");
379
+ }
380
+ }
381
+ const configPath = join3(brainDir, "config.json");
382
+ if (!existsSync2(configPath)) {
383
+ const config = {
384
+ gitTracking: true,
385
+ autoSync: true,
386
+ agents: toRestore,
387
+ maxLogEntries: DEFAULT_MAX_LOG_ENTRIES
388
+ };
389
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
390
+ restored.push(".brain/config.json");
391
+ }
392
+ try {
393
+ registerProject(projectPath);
394
+ } catch {
395
+ }
396
+ console.log("");
397
+ for (const f of restored) console.log(` ${chalk2.green("\u2713")} ${f}`);
398
+ console.log("");
399
+ console.log(` ${chalk2.green("\u2713")} Agent files ready. Start a new session \u2014 your AI is already briefed.`);
400
+ console.log("");
401
+ process.exit(0);
402
+ }
403
+ } else {
404
+ if (opts.yes) {
405
+ console.log(` ${chalk2.red("\u2717")} Already initialized at this path.`);
406
+ console.log(` Run ${chalk2.cyan("mindlink config")} to change settings.`);
407
+ console.log("");
408
+ process.exit(1);
409
+ }
410
+ const action = await select({
411
+ message: ".brain/ already exists at this path. What would you like to do?",
412
+ options: [
413
+ { value: "config", label: "Change settings", hint: "mindlink config" },
414
+ { value: "status", label: "View current status", hint: "mindlink status" },
415
+ { value: "exit", label: "Nothing \u2014 exit", hint: "" }
416
+ ]
417
+ });
418
+ if (isCancel(action) || action === "exit") {
419
+ process.exit(0);
420
+ }
421
+ if (action === "status") {
422
+ try {
423
+ execSync("mindlink status", { cwd: projectPath, stdio: "inherit" });
424
+ } catch {
425
+ }
426
+ }
427
+ if (action === "config") {
428
+ console.log(` Run ${chalk2.cyan("mindlink config")} to change settings.`);
429
+ }
430
+ console.log("");
431
+ process.exit(0);
432
+ }
433
+ } else if (opts.yes) {
165
434
  console.log(` ${chalk2.red("\u2717")} Already initialized at this path.`);
166
435
  console.log(` Run ${chalk2.cyan("mindlink config")} to change settings.`);
167
436
  console.log("");
168
437
  process.exit(1);
169
- }
170
- const action = await select({
171
- message: ".brain/ already exists at this path. What would you like to do?",
172
- options: [
173
- { value: "config", label: "Change settings", hint: "mindlink config" },
174
- { value: "status", label: "View current status", hint: "mindlink status" },
175
- { value: "exit", label: "Nothing \u2014 exit", hint: "" }
176
- ]
177
- });
178
- if (isCancel(action) || action === "exit") {
179
- process.exit(0);
180
- }
181
- if (action === "status") {
182
- const { execSync: execSync2 } = await import("child_process");
183
- try {
184
- execSync2("mindlink status", { stdio: "inherit" });
185
- } catch {
438
+ } else if (!hasMemory) {
439
+ } else {
440
+ const action = await select({
441
+ message: ".brain/ already exists at this path. What would you like to do?",
442
+ options: [
443
+ { value: "config", label: "Change settings", hint: "mindlink config" },
444
+ { value: "status", label: "View current status", hint: "mindlink status" },
445
+ { value: "exit", label: "Nothing \u2014 exit", hint: "" }
446
+ ]
447
+ });
448
+ if (isCancel(action) || action === "exit") {
449
+ process.exit(0);
186
450
  }
451
+ if (action === "status") {
452
+ try {
453
+ execSync("mindlink status", { cwd: projectPath, stdio: "inherit" });
454
+ } catch {
455
+ }
456
+ }
457
+ if (action === "config") {
458
+ console.log(` Run ${chalk2.cyan("mindlink config")} to change settings.`);
459
+ }
460
+ console.log("");
461
+ process.exit(0);
187
462
  }
188
- if (action === "config") {
189
- console.log(` Run ${chalk2.cyan("mindlink config")} to change settings.`);
190
- }
191
- console.log("");
192
- process.exit(0);
193
463
  }
194
464
  intro(chalk2.bold("Initializing memory for this project:"));
195
465
  console.log(` ${chalk2.dim(projectPath)}`);
196
466
  console.log(` ${chalk2.dim("This creates a .brain/ folder scoped to this project only.")}`);
197
467
  console.log(` ${chalk2.dim("Run mindlink init once per project \u2014 never needs to be run again.")}`);
198
468
  console.log("");
469
+ if (process.platform === "win32") {
470
+ console.log(` ${chalk2.yellow("\u26A0")} ${chalk2.bold("Windows detected")}`);
471
+ console.log(` Claude Code hooks use bash and won't run on Windows.`);
472
+ console.log(` Memory enforcement (Stop hook, session timestamps) will be disabled.`);
473
+ console.log(` All other features work normally.`);
474
+ console.log("");
475
+ }
199
476
  let selectedAgents;
200
477
  if (opts.yes) {
201
478
  selectedAgents = AGENTS.filter((a) => a.selected).map((a) => a.value);
@@ -278,6 +555,18 @@ Examples:
278
555
  writeFileSync2(dest, content);
279
556
  created.push(`${file.label.padEnd(32)} ${chalk2.dim(file.desc)}`);
280
557
  }
558
+ const memoryDest = join3(brainDir, "MEMORY.md");
559
+ if (existsSync2(GLOBAL_USER_PROFILE_PATH)) {
560
+ const profileContent = readFileSync2(GLOBAL_USER_PROFILE_PATH, "utf8");
561
+ if (sectionHasRealContent(profileContent, "MindLink \u2014 Global User Profile") || profileContent.split("\n").some((l) => {
562
+ const t = l.trim();
563
+ return t.length > 0 && !t.startsWith("#") && !t.startsWith(">") && !t.startsWith("<!--");
564
+ })) {
565
+ const injected = injectUserProfile(readFileSync2(memoryDest, "utf8"), profileContent);
566
+ writeFileSync2(memoryDest, injected);
567
+ created.push(`User Profile imported from ~/.mindlink/USER.md`);
568
+ }
569
+ }
281
570
  for (const agentValue of selectedAgents) {
282
571
  const agent = AGENTS.find((a) => a.value === agentValue);
283
572
  if (!agent) continue;
@@ -290,8 +579,70 @@ Examples:
290
579
  const hookDest = join3(projectPath, ".claude", "settings.json");
291
580
  if (!existsSync2(hookDest)) {
292
581
  mkdirSync2(dirname2(hookDest), { recursive: true });
293
- writeFileSync2(hookDest, readFileSync2(join3(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
294
- created.push(`.claude/settings.json${" ".repeat(14)} ${chalk2.dim("Claude Code compact hook")}`);
582
+ const settings = JSON.parse(readFileSync2(join3(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
583
+ settings.mcpServers = {
584
+ mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
585
+ };
586
+ writeFileSync2(hookDest, JSON.stringify(settings, null, 2));
587
+ created.push(`.claude/settings.json${" ".repeat(14)} ${chalk2.dim("Claude Code hooks + MCP server")}`);
588
+ }
589
+ }
590
+ if (selectedAgents.includes("cursor")) {
591
+ const cursorMcpDest = join3(projectPath, ".cursor", "mcp.json");
592
+ if (!existsSync2(cursorMcpDest)) {
593
+ mkdirSync2(join3(projectPath, ".cursor"), { recursive: true });
594
+ const cursorMcp = { mcpServers: { mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } } } };
595
+ writeFileSync2(cursorMcpDest, JSON.stringify(cursorMcp, null, 2));
596
+ created.push(`.cursor/mcp.json${" ".repeat(20)} ${chalk2.dim("Cursor MCP server")}`);
597
+ }
598
+ }
599
+ if (selectedAgents.includes("continue")) {
600
+ const continueMcpDest = join3(projectPath, ".continue", "mcpServers", "mindlink.json");
601
+ if (!existsSync2(continueMcpDest)) {
602
+ mkdirSync2(join3(projectPath, ".continue", "mcpServers"), { recursive: true });
603
+ const continueMcp = { mcpServers: { mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } } } };
604
+ writeFileSync2(continueMcpDest, JSON.stringify(continueMcp, null, 2));
605
+ created.push(`.continue/mcpServers/mindlink.json ${chalk2.dim("Continue MCP server")}`);
606
+ }
607
+ }
608
+ if (selectedAgents.includes("copilot")) {
609
+ const copilotMcpDest = join3(projectPath, ".vscode", "mcp.json");
610
+ if (!existsSync2(copilotMcpDest)) {
611
+ mkdirSync2(join3(projectPath, ".vscode"), { recursive: true });
612
+ const copilotMcp = { mcpServers: { mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } } } };
613
+ writeFileSync2(copilotMcpDest, JSON.stringify(copilotMcp, null, 2));
614
+ created.push(`.vscode/mcp.json${" ".repeat(19)} ${chalk2.dim("GitHub Copilot MCP server")}`);
615
+ }
616
+ }
617
+ if (selectedAgents.includes("kiro")) {
618
+ const kiroMcpDest = join3(projectPath, ".kiro", "settings", "mcp.json");
619
+ if (!existsSync2(kiroMcpDest)) {
620
+ mkdirSync2(join3(projectPath, ".kiro", "settings"), { recursive: true });
621
+ const kiroMcp = { mcpServers: { mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } } } };
622
+ writeFileSync2(kiroMcpDest, JSON.stringify(kiroMcp, null, 2));
623
+ created.push(`.kiro/settings/mcp.json${" ".repeat(13)} ${chalk2.dim("Kiro MCP server")}`);
624
+ }
625
+ }
626
+ if (selectedAgents.includes("windsurf")) {
627
+ try {
628
+ mkdirSync2(dirname2(GLOBAL_WINDSURF_MCP_PATH), { recursive: true });
629
+ let existingWindsurf = {};
630
+ if (existsSync2(GLOBAL_WINDSURF_MCP_PATH)) {
631
+ try {
632
+ existingWindsurf = JSON.parse(readFileSync2(GLOBAL_WINDSURF_MCP_PATH, "utf8"));
633
+ } catch {
634
+ }
635
+ }
636
+ const mergedWindsurf = {
637
+ ...existingWindsurf,
638
+ mcpServers: {
639
+ ...typeof existingWindsurf.mcpServers === "object" && existingWindsurf.mcpServers !== null ? existingWindsurf.mcpServers : {},
640
+ mindlink: { command: "mindlink", args: ["mcp"] }
641
+ }
642
+ };
643
+ writeFileSync2(GLOBAL_WINDSURF_MCP_PATH, JSON.stringify(mergedWindsurf, null, 2));
644
+ created.push(`~/.codeium/windsurf/mcp_config.json ${chalk2.dim("Windsurf MCP server (global)")}`);
645
+ } catch {
295
646
  }
296
647
  }
297
648
  if (!gitTracking) {
@@ -324,7 +675,15 @@ Examples:
324
675
  console.log("");
325
676
  for (const err of errors) console.log(` ${chalk2.red("\u2717")} ${err}`);
326
677
  }
327
- console.log("");
678
+ if (selectedAgents.includes("cline")) {
679
+ console.log(` ${chalk2.yellow("\u2192")} Cline: add the MCP server manually in Cline's settings UI (MCP Servers tab)`);
680
+ console.log(` ${chalk2.dim("Command: mindlink Args: mcp Env: MINDLINK_PROJECT_PATH=" + projectPath)}`);
681
+ console.log("");
682
+ }
683
+ if (!existsSync2(GLOBAL_USER_PROFILE_PATH)) {
684
+ console.log(` ${chalk2.dim("\u2192")} Run ${chalk2.cyan("mindlink profile")} to set up a global user profile \u2014 imported into every new project automatically.`);
685
+ console.log("");
686
+ }
328
687
  note(
329
688
  `Your AI finally has a brain.
330
689
 
@@ -511,7 +870,7 @@ Examples:
511
870
  // src/commands/log.ts
512
871
  import { Command as Command3 } from "commander";
513
872
  import chalk4 from "chalk";
514
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "fs";
873
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
515
874
  import { join as join5, resolve as resolve3 } from "path";
516
875
  var logCommand = new Command3("log").description("Print session history").option("--all", "Show full history").option("--limit <n>", "Show last N sessions", "10").option("--since <date>", "Show sessions from a date (matched against heading text)").option("--json", "Output as JSON").addHelpText("after", `
517
876
  Examples:
@@ -568,7 +927,7 @@ Examples:
568
927
  }
569
928
  console.log("");
570
929
  }
571
- const archiveFiles = readdirSync(brainDir).filter((f) => /^LOG-\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
930
+ const archiveFiles = readdirSync2(brainDir).filter((f) => /^LOG-\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
572
931
  if (archiveFiles.length > 0) {
573
932
  console.log(` ${chalk4.dim("\u2500".repeat(44))}`);
574
933
  console.log(` ${chalk4.dim("Like all human brains, MindLink forgets old sessions")}`);
@@ -1019,13 +1378,14 @@ Examples:
1019
1378
  import { Command as Command8 } from "commander";
1020
1379
  import { select as select3, isCancel as isCancel4, cancel as cancel4, spinner as spinner2 } from "@clack/prompts";
1021
1380
  import chalk9 from "chalk";
1022
- import { execSync } from "child_process";
1381
+ import { execSync as execSync2 } from "child_process";
1023
1382
  import { join as join10, dirname as dirname4 } from "path";
1024
1383
  import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
1384
+ var REQUIRED_BRAIN_FILES = ["MEMORY.md", "SESSION.md", "SHARED.md", "LOG.md"];
1025
1385
  async function latestVersion() {
1026
1386
  try {
1027
1387
  const { default: https } = await import("https");
1028
- return new Promise((resolve13) => {
1388
+ return new Promise((resolve17) => {
1029
1389
  const req = https.get(
1030
1390
  "https://registry.npmjs.org/mindlink/latest",
1031
1391
  { headers: { "User-Agent": "mindlink-cli" } },
@@ -1037,17 +1397,17 @@ async function latestVersion() {
1037
1397
  res.on("end", () => {
1038
1398
  try {
1039
1399
  const parsed = JSON.parse(data);
1040
- resolve13(parsed.version ?? null);
1400
+ resolve17(parsed.version ?? null);
1041
1401
  } catch {
1042
- resolve13(null);
1402
+ resolve17(null);
1043
1403
  }
1044
1404
  });
1045
1405
  }
1046
1406
  );
1047
- req.on("error", () => resolve13(null));
1407
+ req.on("error", () => resolve17(null));
1048
1408
  req.setTimeout(8e3, () => {
1049
1409
  req.destroy();
1050
- resolve13(null);
1410
+ resolve17(null);
1051
1411
  });
1052
1412
  });
1053
1413
  } catch {
@@ -1068,71 +1428,73 @@ Examples:
1068
1428
  mindlink update
1069
1429
  `).action(async () => {
1070
1430
  const current = VERSION;
1431
+ let nonTtyExitCode = null;
1071
1432
  if (!process.stdin.isTTY) {
1072
- const latest2 = await latestVersion();
1073
- if (!latest2) {
1433
+ const latest = await latestVersion();
1434
+ if (!latest) {
1074
1435
  console.log(JSON.stringify({ current, latest: null, upToDate: null }));
1075
- process.exit(1);
1436
+ nonTtyExitCode = 1;
1437
+ } else {
1438
+ const upToDate = !semverGt(latest, current);
1439
+ console.log(JSON.stringify({ current, latest, upToDate }));
1440
+ if (!upToDate) nonTtyExitCode = 2;
1076
1441
  }
1077
- const upToDate = !semverGt(latest2, current);
1078
- console.log(JSON.stringify({ current, latest: latest2, upToDate }));
1079
- if (!upToDate) process.exit(2);
1080
- return;
1081
- }
1082
- const s = spinner2();
1083
- s.start("Checking for updates...");
1084
- const latest = await latestVersion();
1085
- if (!latest) {
1086
- s.stop("Could not reach npm registry.");
1087
- console.log("");
1088
- console.log(` ${chalk9.red("\u2717")} Could not check for updates. Check your internet connection.`);
1089
- console.log(` ${chalk9.dim("Latest releases: github.com/404-not-found/mindlink/releases")}`);
1090
- console.log("");
1091
- process.exit(1);
1092
- }
1093
- s.stop("Done.");
1094
- console.log("");
1095
- console.log(` Current version : ${chalk9.dim(current)}`);
1096
- console.log(` Latest version : ${semverGt(latest, current) ? chalk9.green(latest) : chalk9.dim(latest)}`);
1097
- console.log("");
1098
- if (!semverGt(latest, current)) {
1099
- console.log(` ${chalk9.green("\u2713")} You're on the latest version (${current}).`);
1100
- console.log("");
1101
1442
  } else {
1102
- const action = await select3({
1103
- message: `Update to ${latest}?`,
1104
- options: [
1105
- { value: "update", label: `Update to ${latest}` },
1106
- { value: "skip", label: "Skip this version" },
1107
- { value: "cancel", label: "Cancel" }
1108
- ]
1109
- });
1110
- if (isCancel4(action) || action === "cancel" || action === "skip") {
1111
- if (action === "skip") {
1112
- console.log(` ${chalk9.dim("Skipped. Run mindlink update again to install later.")}`);
1113
- } else {
1114
- cancel4("Cancelled.");
1115
- }
1443
+ const s = spinner2();
1444
+ s.start("Checking for updates...");
1445
+ const latest = await latestVersion();
1446
+ if (!latest) {
1447
+ s.stop("Could not reach npm registry.");
1116
1448
  console.log("");
1117
- return;
1118
- }
1119
- const s2 = spinner2();
1120
- s2.start(`Installing mindlink@${latest}...`);
1121
- try {
1122
- execSync(`npm install -g mindlink@${latest}`, { stdio: "pipe" });
1123
- s2.stop("Done.");
1449
+ console.log(` ${chalk9.red("\u2717")} Could not check for updates. Check your internet connection.`);
1450
+ console.log(` ${chalk9.dim("Latest releases: github.com/404-not-found/mindlink/releases")}`);
1124
1451
  console.log("");
1125
- console.log(` ${chalk9.green("\u2713")} Updated to ${latest}.`);
1126
- console.log(` ${chalk9.dim("See what's new: github.com/404-not-found/mindlink/releases")}`);
1127
- } catch (err) {
1128
- s2.stop("Failed.");
1452
+ process.exit(1);
1453
+ }
1454
+ s.stop("Done.");
1455
+ console.log("");
1456
+ console.log(` Current version : ${chalk9.dim(current)}`);
1457
+ console.log(` Latest version : ${semverGt(latest, current) ? chalk9.green(latest) : chalk9.dim(latest)}`);
1458
+ console.log("");
1459
+ if (!semverGt(latest, current)) {
1460
+ console.log(` ${chalk9.green("\u2713")} You're on the latest version (${current}).`);
1129
1461
  console.log("");
1130
- console.log(` ${chalk9.red("\u2717")} Update failed.`);
1131
- console.log(` ${chalk9.dim("Try: npm install -g mindlink@" + latest)}`);
1132
- if (err instanceof Error && err.message.includes("EACCES")) {
1133
- console.log(` ${chalk9.dim("Permission error \u2014 try: sudo npm install -g mindlink@" + latest)}`);
1462
+ } else {
1463
+ const action = await select3({
1464
+ message: `Update to ${latest}?`,
1465
+ options: [
1466
+ { value: "update", label: `Update to ${latest}` },
1467
+ { value: "skip", label: "Skip this version" },
1468
+ { value: "cancel", label: "Cancel" }
1469
+ ]
1470
+ });
1471
+ if (isCancel4(action) || action === "cancel" || action === "skip") {
1472
+ if (action === "skip") {
1473
+ console.log(` ${chalk9.dim("Skipped. Run mindlink update again to install later.")}`);
1474
+ } else {
1475
+ cancel4("Cancelled.");
1476
+ }
1477
+ console.log("");
1478
+ return;
1479
+ }
1480
+ const s2 = spinner2();
1481
+ s2.start(`Installing mindlink@${latest}...`);
1482
+ try {
1483
+ execSync2(`npm install -g mindlink@${latest}`, { stdio: "pipe" });
1484
+ s2.stop("Done.");
1485
+ console.log("");
1486
+ console.log(` ${chalk9.green("\u2713")} Updated to ${latest}.`);
1487
+ console.log(` ${chalk9.dim("See what's new: github.com/404-not-found/mindlink/releases")}`);
1488
+ } catch (err) {
1489
+ s2.stop("Failed.");
1490
+ console.log("");
1491
+ console.log(` ${chalk9.red("\u2717")} Update failed.`);
1492
+ console.log(` ${chalk9.dim("Try: npm install -g mindlink@" + latest)}`);
1493
+ if (err instanceof Error && err.message.includes("EACCES")) {
1494
+ console.log(` ${chalk9.dim("Permission error \u2014 try: sudo npm install -g mindlink@" + latest)}`);
1495
+ }
1496
+ process.exit(1);
1134
1497
  }
1135
- process.exit(1);
1136
1498
  }
1137
1499
  }
1138
1500
  const cwd = process.cwd();
@@ -1153,6 +1515,19 @@ Examples:
1153
1515
  }
1154
1516
  const agentValues = config.agents ?? AGENTS.filter((a) => a.selected).map((a) => a.value);
1155
1517
  const refreshed = [];
1518
+ for (const brainFile of REQUIRED_BRAIN_FILES) {
1519
+ const dest = join10(projectPath, BRAIN_DIR, brainFile);
1520
+ if (!existsSync9(dest)) {
1521
+ try {
1522
+ const template = join10(BRAIN_TEMPLATES_DIR, brainFile);
1523
+ if (existsSync9(template)) {
1524
+ writeFileSync6(dest, readFileSync9(template, "utf8"));
1525
+ refreshed.push(`.brain/${brainFile}`);
1526
+ }
1527
+ } catch {
1528
+ }
1529
+ }
1530
+ }
1156
1531
  for (const agentValue of agentValues) {
1157
1532
  const agent = AGENTS.find((a) => a.value === agentValue);
1158
1533
  if (!agent) continue;
@@ -1168,17 +1543,142 @@ Examples:
1168
1543
  const hookDest = join10(projectPath, ".claude", "settings.json");
1169
1544
  try {
1170
1545
  mkdirSync4(join10(projectPath, ".claude"), { recursive: true });
1171
- writeFileSync6(hookDest, readFileSync9(join10(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
1546
+ const template = JSON.parse(readFileSync9(join10(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
1547
+ let existing = {};
1548
+ if (existsSync9(hookDest)) {
1549
+ try {
1550
+ existing = JSON.parse(readFileSync9(hookDest, "utf8"));
1551
+ } catch {
1552
+ }
1553
+ }
1554
+ const merged = {
1555
+ ...template,
1556
+ ...existing,
1557
+ // Always refresh hooks and permissions from template
1558
+ hooks: template.hooks,
1559
+ permissions: template.permissions,
1560
+ // Merge mcpServers: keep user's other servers, update mindlink entry
1561
+ mcpServers: {
1562
+ ...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
1563
+ mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
1564
+ }
1565
+ };
1566
+ writeFileSync6(hookDest, JSON.stringify(merged, null, 2));
1172
1567
  refreshed.push(".claude/settings.json");
1173
1568
  } catch {
1174
1569
  }
1175
1570
  }
1571
+ if (agentValues.includes("cursor")) {
1572
+ const cursorMcpDest = join10(projectPath, ".cursor", "mcp.json");
1573
+ try {
1574
+ mkdirSync4(join10(projectPath, ".cursor"), { recursive: true });
1575
+ let existing = {};
1576
+ if (existsSync9(cursorMcpDest)) {
1577
+ try {
1578
+ existing = JSON.parse(readFileSync9(cursorMcpDest, "utf8"));
1579
+ } catch {
1580
+ }
1581
+ }
1582
+ const merged = {
1583
+ ...existing,
1584
+ mcpServers: {
1585
+ ...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
1586
+ mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
1587
+ }
1588
+ };
1589
+ writeFileSync6(cursorMcpDest, JSON.stringify(merged, null, 2));
1590
+ refreshed.push(".cursor/mcp.json");
1591
+ } catch {
1592
+ }
1593
+ }
1594
+ if (agentValues.includes("continue")) {
1595
+ const continueMcpDest = join10(projectPath, ".continue", "mcpServers", "mindlink.json");
1596
+ try {
1597
+ mkdirSync4(join10(projectPath, ".continue", "mcpServers"), { recursive: true });
1598
+ const continueMcp = { mcpServers: { mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } } } };
1599
+ writeFileSync6(continueMcpDest, JSON.stringify(continueMcp, null, 2));
1600
+ refreshed.push(".continue/mcpServers/mindlink.json");
1601
+ } catch {
1602
+ }
1603
+ }
1604
+ if (agentValues.includes("copilot")) {
1605
+ const copilotMcpDest = join10(projectPath, ".vscode", "mcp.json");
1606
+ try {
1607
+ mkdirSync4(join10(projectPath, ".vscode"), { recursive: true });
1608
+ let existing = {};
1609
+ if (existsSync9(copilotMcpDest)) {
1610
+ try {
1611
+ existing = JSON.parse(readFileSync9(copilotMcpDest, "utf8"));
1612
+ } catch {
1613
+ }
1614
+ }
1615
+ const merged = {
1616
+ ...existing,
1617
+ mcpServers: {
1618
+ ...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
1619
+ mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
1620
+ }
1621
+ };
1622
+ writeFileSync6(copilotMcpDest, JSON.stringify(merged, null, 2));
1623
+ refreshed.push(".vscode/mcp.json");
1624
+ } catch {
1625
+ }
1626
+ }
1627
+ if (agentValues.includes("kiro")) {
1628
+ const kiroMcpDest = join10(projectPath, ".kiro", "settings", "mcp.json");
1629
+ try {
1630
+ mkdirSync4(join10(projectPath, ".kiro", "settings"), { recursive: true });
1631
+ let existing = {};
1632
+ if (existsSync9(kiroMcpDest)) {
1633
+ try {
1634
+ existing = JSON.parse(readFileSync9(kiroMcpDest, "utf8"));
1635
+ } catch {
1636
+ }
1637
+ }
1638
+ const merged = {
1639
+ ...existing,
1640
+ mcpServers: {
1641
+ ...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
1642
+ mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
1643
+ }
1644
+ };
1645
+ writeFileSync6(kiroMcpDest, JSON.stringify(merged, null, 2));
1646
+ refreshed.push(".kiro/settings/mcp.json");
1647
+ } catch {
1648
+ }
1649
+ }
1650
+ if (agentValues.includes("windsurf")) {
1651
+ try {
1652
+ mkdirSync4(dirname4(GLOBAL_WINDSURF_MCP_PATH), { recursive: true });
1653
+ let existing = {};
1654
+ if (existsSync9(GLOBAL_WINDSURF_MCP_PATH)) {
1655
+ try {
1656
+ existing = JSON.parse(readFileSync9(GLOBAL_WINDSURF_MCP_PATH, "utf8"));
1657
+ } catch {
1658
+ }
1659
+ }
1660
+ const merged = {
1661
+ ...existing,
1662
+ mcpServers: {
1663
+ ...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
1664
+ mindlink: { command: "mindlink", args: ["mcp"] }
1665
+ }
1666
+ };
1667
+ writeFileSync6(GLOBAL_WINDSURF_MCP_PATH, JSON.stringify(merged, null, 2));
1668
+ refreshed.push("~/.codeium/windsurf/mcp_config.json");
1669
+ } catch {
1670
+ }
1671
+ }
1176
1672
  const memoryPath = join10(projectPath, BRAIN_DIR, "MEMORY.md");
1177
1673
  if (existsSync9(memoryPath)) {
1178
1674
  try {
1179
- const content = readFileSync9(memoryPath, "utf8");
1180
- if (!content.includes("## User Profile") && content.includes("## Important Context")) {
1181
- const userProfileBlock = `## User Profile <!-- READ EVERY SESSION \u2014 personal facts about the user -->
1675
+ let content = readFileSync9(memoryPath, "utf8");
1676
+ let memoryChanged = false;
1677
+ const migrations = [
1678
+ {
1679
+ // v1.1.5: Add ## User Profile section
1680
+ marker: "## User Profile",
1681
+ block: `## User Profile <!-- READ EVERY SESSION \u2014 personal facts about the user -->
1182
1682
 
1183
1683
  <!-- Job, company, level, years of experience, immigration status -->
1184
1684
  <!-- Age, health, physical details -->
@@ -1190,13 +1690,45 @@ Examples:
1190
1690
 
1191
1691
  ---
1192
1692
 
1193
- `;
1194
- writeFileSync6(memoryPath, content.replace("## Important Context", userProfileBlock + "## Important Context"));
1693
+ `,
1694
+ before: "## Important Context"
1695
+ }
1696
+ // Future migrations go here — same pattern:
1697
+ // { marker: '## New Section', block: '## New Section\n\n<!-- ... -->\n\n\n---\n\n', before: '## Some Existing Section' },
1698
+ ];
1699
+ for (const m of migrations) {
1700
+ if (!content.includes(m.marker) && content.includes(m.before)) {
1701
+ content = content.replace(m.before, m.block + m.before);
1702
+ memoryChanged = true;
1703
+ }
1704
+ }
1705
+ if (memoryChanged) {
1706
+ writeFileSync6(memoryPath, content);
1195
1707
  refreshed.push(".brain/MEMORY.md");
1196
1708
  }
1197
1709
  } catch {
1198
1710
  }
1199
1711
  }
1712
+ if (existsSync9(GLOBAL_USER_PROFILE_PATH) && existsSync9(memoryPath)) {
1713
+ try {
1714
+ const profileRaw = readFileSync9(GLOBAL_USER_PROFILE_PATH, "utf8");
1715
+ const profileLines = profileRaw.split("\n");
1716
+ const bodyStart = profileLines.findIndex((l) => {
1717
+ const t = l.trim();
1718
+ return t.length > 0 && !t.startsWith("#") && !t.startsWith(">") && !t.startsWith("<!--");
1719
+ });
1720
+ const profileBody = bodyStart >= 0 ? profileLines.slice(bodyStart).join("\n").trim() : "";
1721
+ if (profileBody) {
1722
+ const memContent = readFileSync9(memoryPath, "utf8");
1723
+ const updated = replaceSection(memContent, "User Profile", profileBody);
1724
+ if (updated !== memContent) {
1725
+ writeFileSync6(memoryPath, updated);
1726
+ if (!refreshed.includes(".brain/MEMORY.md")) refreshed.push(".brain/MEMORY.md (profile synced)");
1727
+ }
1728
+ }
1729
+ } catch {
1730
+ }
1731
+ }
1200
1732
  console.log(` ${chalk9.bold(projectPath)}`);
1201
1733
  for (const f of refreshed) {
1202
1734
  console.log(` ${chalk9.green("\u2713")} ${f}`);
@@ -1206,6 +1738,7 @@ Examples:
1206
1738
  console.log(` ${chalk9.dim("All agent files are up to date.")}`);
1207
1739
  }
1208
1740
  console.log("");
1741
+ if (nonTtyExitCode !== null) process.exit(nonTtyExitCode);
1209
1742
  });
1210
1743
 
1211
1744
  // src/commands/summary.ts
@@ -1438,7 +1971,7 @@ Examples:
1438
1971
  import { Command as Command11 } from "commander";
1439
1972
  import { text as text2, isCancel as isCancel6, cancel as cancel6 } from "@clack/prompts";
1440
1973
  import chalk12 from "chalk";
1441
- import { existsSync as existsSync12, readdirSync as readdirSync2 } from "fs";
1974
+ import { existsSync as existsSync12, readdirSync as readdirSync3 } from "fs";
1442
1975
  import { join as join13, resolve as resolve10, basename as basename3 } from "path";
1443
1976
  import AdmZip from "adm-zip";
1444
1977
  var exportCommand = new Command11("export").description("Export .brain/ memory to a shareable zip file").option("--output <path>", "Directory or full path to save the zip file").option("--include-agents", "Also include agent instruction files (CLAUDE.md, CURSOR.md, etc.)").addHelpText("after", `
@@ -1485,8 +2018,7 @@ Examples:
1485
2018
  const answer = await text2({
1486
2019
  message: "Where should the zip be saved?",
1487
2020
  placeholder: projectPath,
1488
- initialValue: projectPath,
1489
- hint: `default filename: ${defaultFilename}`
2021
+ initialValue: projectPath
1490
2022
  });
1491
2023
  if (isCancel6(answer)) {
1492
2024
  cancel6("Cancelled.");
@@ -1514,7 +2046,7 @@ Examples:
1514
2046
  skipped.push(`.brain/${file}`);
1515
2047
  }
1516
2048
  }
1517
- const archiveFiles = readdirSync2(brainDir).filter((f) => /^LOG-\d{4}-\d{2}-\d{2}\.md$/.test(f));
2049
+ const archiveFiles = readdirSync3(brainDir).filter((f) => /^LOG-\d{4}-\d{2}-\d{2}\.md$/.test(f));
1518
2050
  for (const file of archiveFiles) {
1519
2051
  zip.addLocalFile(join13(brainDir, file), ".brain");
1520
2052
  included.push(`.brain/${file}`);
@@ -1661,7 +2193,7 @@ Examples:
1661
2193
  // src/commands/doctor.ts
1662
2194
  import { Command as Command13 } from "commander";
1663
2195
  import chalk14 from "chalk";
1664
- import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync3, readdirSync as readdirSync3 } from "fs";
2196
+ import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync3, readdirSync as readdirSync4 } from "fs";
1665
2197
  import { join as join15, resolve as resolve12 } from "path";
1666
2198
  var CORE_LINE_LIMIT = 50;
1667
2199
  var CORE_WARN_THRESHOLD = 40;
@@ -1793,7 +2325,7 @@ Examples:
1793
2325
  checks.push(ok(`LOG.md \u2014 ${entryCount} sessions logged (last: ${newestHeading}, going back to: ${oldestHeading})`));
1794
2326
  }
1795
2327
  }
1796
- const archives = readdirSync3(brainDir).filter((f) => /^LOG-\d{4}-\d{2}\.md$/.test(f));
2328
+ const archives = readdirSync4(brainDir).filter((f) => /^LOG-\d{4}-\d{2}\.md$/.test(f));
1797
2329
  if (archives.length > 0) {
1798
2330
  checks.push(info(`${archives.length} archive file${archives.length !== 1 ? "s" : ""} \u2014 old sessions are stored in LOG-*.md, not gone`));
1799
2331
  }
@@ -1876,8 +2408,919 @@ var versionCommand = new Command14("version").description("Show the current Mind
1876
2408
  console.log("");
1877
2409
  });
1878
2410
 
2411
+ // src/commands/diff.ts
2412
+ import { Command as Command15 } from "commander";
2413
+ import chalk16 from "chalk";
2414
+ import { existsSync as existsSync15, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
2415
+ import { join as join16, resolve as resolve13 } from "path";
2416
+ import { execSync as execSync3 } from "child_process";
2417
+ var BRAIN_FILES3 = ["MEMORY.md", "SESSION.md", "LOG.md", "SHARED.md"];
2418
+ function relativeTime2(ms) {
2419
+ const secs = Math.floor(ms / 1e3);
2420
+ if (secs < 60) return `${secs}s ago`;
2421
+ const mins = Math.floor(secs / 60);
2422
+ if (mins < 60) return `${mins}m ago`;
2423
+ const hours = Math.floor(mins / 60);
2424
+ if (hours < 24) return `${hours}h ago`;
2425
+ return `${Math.floor(hours / 24)}d ago`;
2426
+ }
2427
+ function getGitDiff(filePath, since) {
2428
+ try {
2429
+ const dir = resolve13(filePath, "..");
2430
+ execSync3("git rev-parse --is-inside-work-tree", { cwd: dir, stdio: "pipe" });
2431
+ const ref = since || "HEAD~1";
2432
+ const diff = execSync3(`git diff ${ref} -- "${filePath}"`, {
2433
+ cwd: dir,
2434
+ encoding: "utf8",
2435
+ stdio: ["pipe", "pipe", "pipe"]
2436
+ });
2437
+ return diff || null;
2438
+ } catch {
2439
+ return null;
2440
+ }
2441
+ }
2442
+ function parseDiffSummary(diff) {
2443
+ const added = [];
2444
+ const removed = [];
2445
+ for (const line of diff.split("\n")) {
2446
+ if (line.startsWith("+") && !line.startsWith("+++")) {
2447
+ const t = line.slice(1).trim();
2448
+ if (t && !t.startsWith("<!--") && !t.startsWith("#") && t !== "---") added.push(t);
2449
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2450
+ const t = line.slice(1).trim();
2451
+ if (t && !t.startsWith("<!--") && !t.startsWith("#") && t !== "---") removed.push(t);
2452
+ }
2453
+ }
2454
+ return { added, removed };
2455
+ }
2456
+ var diffCommand = new Command15("diff").description("Show what changed in .brain/ since last session").option("--since <ref>", "Git ref or date to diff against (default: last mindlink clear or HEAD~1)").option("--json", "Output as JSON").addHelpText("after", `
2457
+ What it does:
2458
+ Shows what changed in each .brain/ file since your last session.
2459
+ Uses git diff when .brain/ is tracked; falls back to file modification times.
2460
+
2461
+ Examples:
2462
+ mindlink diff
2463
+ mindlink diff --since HEAD~3
2464
+ mindlink diff --since "2026-04-10"
2465
+ `).action((opts) => {
2466
+ const projectPath = resolve13(process.cwd());
2467
+ const brainDir = join16(projectPath, BRAIN_DIR);
2468
+ if (!existsSync15(brainDir)) {
2469
+ console.log("");
2470
+ console.log(` ${chalk16.red("\u2717")} No .brain/ found in this directory.`);
2471
+ console.log(` Run ${chalk16.cyan("mindlink init")} to get started.`);
2472
+ console.log("");
2473
+ process.exit(1);
2474
+ }
2475
+ const results = {};
2476
+ const sessionTsPath = join16(brainDir, ".session_ts");
2477
+ const sessionTs = existsSync15(sessionTsPath) ? parseInt(readFileSync13(sessionTsPath, "utf8").trim(), 10) * 1e3 : null;
2478
+ const now = Date.now();
2479
+ const isGitTracked = (() => {
2480
+ try {
2481
+ execSync3("git rev-parse --is-inside-work-tree", { cwd: projectPath, stdio: "pipe" });
2482
+ const tracked = execSync3("git ls-files --error-unmatch .brain/MEMORY.md", {
2483
+ cwd: projectPath,
2484
+ stdio: "pipe"
2485
+ });
2486
+ return true;
2487
+ } catch {
2488
+ return false;
2489
+ }
2490
+ })();
2491
+ for (const file of BRAIN_FILES3) {
2492
+ const filePath = join16(brainDir, file);
2493
+ if (!existsSync15(filePath)) {
2494
+ results[file] = { exists: false };
2495
+ continue;
2496
+ }
2497
+ const stat = statSync4(filePath);
2498
+ const mtime = stat.mtimeMs;
2499
+ const content = readFileSync13(filePath, "utf8");
2500
+ const sizeLines = content.split("\n").length;
2501
+ let diff = null;
2502
+ let added = [];
2503
+ let removed = [];
2504
+ if (isGitTracked) {
2505
+ diff = getGitDiff(filePath, opts.since);
2506
+ if (diff) {
2507
+ const summary = parseDiffSummary(diff);
2508
+ added = summary.added.slice(0, 5);
2509
+ removed = summary.removed.slice(0, 5);
2510
+ }
2511
+ }
2512
+ results[file] = { exists: true, mtime, diff, added, removed, sizeLines };
2513
+ }
2514
+ if (opts.json) {
2515
+ console.log(JSON.stringify(results, null, 2));
2516
+ return;
2517
+ }
2518
+ console.log("");
2519
+ console.log(` ${chalk16.bold(".brain/ changes")}`);
2520
+ if (sessionTs) {
2521
+ console.log(` ${chalk16.dim(`Session started ${relativeTime2(now - sessionTs)}`)}`);
2522
+ }
2523
+ if (isGitTracked) {
2524
+ const sinceRef = opts.since || "HEAD~1";
2525
+ console.log(` ${chalk16.dim(`Git diff against: ${sinceRef}`)}`);
2526
+ } else {
2527
+ console.log(` ${chalk16.dim(".brain/ is not git-tracked \u2014 showing modification times only")}`);
2528
+ }
2529
+ console.log("");
2530
+ for (const file of BRAIN_FILES3) {
2531
+ const r = results[file];
2532
+ if (!r.exists) {
2533
+ console.log(` ${chalk16.dim("\u2014")} ${file} ${chalk16.dim("(not found)")}`);
2534
+ continue;
2535
+ }
2536
+ const age = r.mtime ? relativeTime2(now - r.mtime) : "";
2537
+ const changedThisSession = sessionTs && r.mtime ? r.mtime > sessionTs : false;
2538
+ const indicator = changedThisSession ? chalk16.green("\u25CF") : chalk16.dim("\u25CB");
2539
+ const label = changedThisSession ? chalk16.green(file) : chalk16.dim(file);
2540
+ console.log(` ${indicator} ${label} ${chalk16.dim(`${r.sizeLines} lines \xB7 modified ${age}`)}`);
2541
+ if (isGitTracked && r.diff) {
2542
+ for (const line of (r.added ?? []).slice(0, 3)) {
2543
+ console.log(` ${chalk16.green("+")} ${line.length > 80 ? line.slice(0, 80) + "\u2026" : line}`);
2544
+ }
2545
+ for (const line of (r.removed ?? []).slice(0, 3)) {
2546
+ console.log(` ${chalk16.red("-")} ${line.length > 80 ? line.slice(0, 80) + "\u2026" : line}`);
2547
+ }
2548
+ } else if (!isGitTracked && changedThisSession) {
2549
+ console.log(` ${chalk16.dim("(content diff unavailable \u2014 commit .brain/ to git for line-level diff)")}`);
2550
+ } else if (isGitTracked && !r.diff) {
2551
+ console.log(` ${chalk16.dim("no changes since last commit")}`);
2552
+ }
2553
+ }
2554
+ console.log("");
2555
+ const changedCount = BRAIN_FILES3.filter((f) => {
2556
+ const r = results[f];
2557
+ return r.exists && sessionTs && r.mtime && r.mtime > sessionTs;
2558
+ }).length;
2559
+ if (changedCount === 0) {
2560
+ console.log(` ${chalk16.dim("No .brain/ files were modified this session.")}`);
2561
+ if (sessionTs) {
2562
+ console.log(` ${chalk16.dim("If the session just started, this is expected.")}`);
2563
+ }
2564
+ } else {
2565
+ console.log(` ${chalk16.green("\u2713")} ${changedCount} file${changedCount !== 1 ? "s" : ""} updated this session.`);
2566
+ }
2567
+ console.log("");
2568
+ });
2569
+
2570
+ // src/commands/verify.ts
2571
+ import { Command as Command16 } from "commander";
2572
+ import chalk17 from "chalk";
2573
+ import { existsSync as existsSync16, readFileSync as readFileSync14, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
2574
+ import { dirname as dirname5 } from "path";
2575
+ import { join as join17, resolve as resolve14 } from "path";
2576
+ var SESSION_WARN_DAYS = 3;
2577
+ var SESSION_FAIL_DAYS = 7;
2578
+ var MEMORY_WARN_LINES = 100;
2579
+ var MEMORY_FAIL_LINES = 200;
2580
+ function pass(id, label, message) {
2581
+ return { id, label, status: "pass", message, fixable: false };
2582
+ }
2583
+ function warn2(id, label, message, fixable = false) {
2584
+ return { id, label, status: "warn", message, fixable };
2585
+ }
2586
+ function fail2(id, label, message, fixable = false) {
2587
+ return { id, label, status: "fail", message, fixable };
2588
+ }
2589
+ function icon2(status) {
2590
+ switch (status) {
2591
+ case "pass":
2592
+ return chalk17.green("\u2713");
2593
+ case "warn":
2594
+ return chalk17.yellow("\u26A0");
2595
+ case "fail":
2596
+ return chalk17.red("\u2717");
2597
+ }
2598
+ }
2599
+ function runChecks(projectPath) {
2600
+ const brainDir = join17(projectPath, BRAIN_DIR);
2601
+ const results = [];
2602
+ if (!existsSync16(brainDir)) {
2603
+ results.push(fail2("brain_missing", ".brain/ missing", `Run ${chalk17.cyan("mindlink init")} to set up memory.`));
2604
+ return results;
2605
+ }
2606
+ const configPath = join17(brainDir, "config.json");
2607
+ let config = {};
2608
+ if (existsSync16(configPath)) {
2609
+ try {
2610
+ config = JSON.parse(readFileSync14(configPath, "utf8"));
2611
+ } catch {
2612
+ }
2613
+ }
2614
+ const memoryPath = join17(brainDir, "MEMORY.md");
2615
+ if (!existsSync16(memoryPath)) {
2616
+ results.push(fail2("core", "Core section", `MEMORY.md is missing \u2014 run ${chalk17.cyan("mindlink init")}.`));
2617
+ } else {
2618
+ const memMd = readFileSync14(memoryPath, "utf8");
2619
+ if (!sectionHasRealContent(memMd, "Core")) {
2620
+ results.push(fail2("core", "Core section \u2014 empty", "Start a session and tell your AI to fill in the Core section."));
2621
+ } else {
2622
+ results.push(pass("core", "Core section \u2014 filled", ""));
2623
+ }
2624
+ }
2625
+ if (existsSync16(memoryPath)) {
2626
+ const memMd = readFileSync14(memoryPath, "utf8");
2627
+ if (!sectionHasRealContent(memMd, "User Profile")) {
2628
+ results.push(fail2("user_profile", "User Profile \u2014 empty", `Run ${chalk17.cyan("mindlink profile")} to set up your global profile and import it here.`));
2629
+ } else {
2630
+ results.push(pass("user_profile", "User Profile \u2014 filled", ""));
2631
+ }
2632
+ }
2633
+ const sessionPath = join17(brainDir, "SESSION.md");
2634
+ if (!existsSync16(sessionPath)) {
2635
+ results.push(fail2("session_fresh", "SESSION.md \u2014 missing", `Run ${chalk17.cyan("mindlink init")} to recreate it.`));
2636
+ } else {
2637
+ const ageDays = (Date.now() - statSync5(sessionPath).mtime.getTime()) / 864e5;
2638
+ const hasContent = readFileSync14(sessionPath, "utf8").split("\n").some((l) => l.trim().length > 0 && !l.startsWith("#") && !l.startsWith("<!--"));
2639
+ if (!hasContent) {
2640
+ results.push(warn2("session_fresh", "SESSION.md \u2014 no content yet", "Start a session \u2014 your AI will fill this in automatically."));
2641
+ } else if (ageDays > SESSION_FAIL_DAYS) {
2642
+ const days = Math.floor(ageDays);
2643
+ results.push(fail2("session_fresh", `SESSION.md \u2014 last updated ${days} days ago`, "SESSION.md has not been updated in a week. Your AI may not be writing session state. Check that it is completing the REQUIRED session-end steps."));
2644
+ } else if (ageDays > SESSION_WARN_DAYS) {
2645
+ const days = Math.floor(ageDays);
2646
+ results.push(warn2("session_fresh", `SESSION.md \u2014 last updated ${days} days ago`, "SESSION.md is getting stale. If you have active sessions, your AI should be updating this after each response."));
2647
+ } else {
2648
+ const mins = Math.floor((Date.now() - statSync5(sessionPath).mtime.getTime()) / 6e4);
2649
+ const age = mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.floor(mins / 60)}h ago` : `${Math.floor(mins / 1440)}d ago`;
2650
+ results.push(pass("session_fresh", `SESSION.md \u2014 updated ${age}`, ""));
2651
+ }
2652
+ }
2653
+ const logPath = join17(brainDir, "LOG.md");
2654
+ if (!existsSync16(logPath)) {
2655
+ results.push(fail2("log_present", "LOG.md \u2014 missing", `Run ${chalk17.cyan("mindlink init")} to recreate it.`));
2656
+ } else {
2657
+ const entries = (readFileSync14(logPath, "utf8").match(/^##\s+/gm) ?? []).length;
2658
+ if (entries === 0) {
2659
+ results.push(pass("log_present", "LOG.md \u2014 no sessions yet", ""));
2660
+ } else {
2661
+ results.push(pass("log_present", `LOG.md \u2014 ${entries} session${entries !== 1 ? "s" : ""} logged`, ""));
2662
+ }
2663
+ }
2664
+ if (existsSync16(memoryPath)) {
2665
+ const memMd = readFileSync14(memoryPath, "utf8");
2666
+ const lines = countRealLines(memMd);
2667
+ if (lines > MEMORY_FAIL_LINES) {
2668
+ results.push(fail2("memory_size", `MEMORY.md \u2014 ${lines} lines (target: under ${MEMORY_FAIL_LINES})`, `Run ${chalk17.cyan("mindlink prune")} to consolidate stale entries.`));
2669
+ } else if (lines > MEMORY_WARN_LINES) {
2670
+ results.push(warn2("memory_size", `MEMORY.md \u2014 ${lines} lines (getting long)`, `Consider running ${chalk17.cyan("mindlink prune")} to retire old entries.`));
2671
+ } else {
2672
+ results.push(pass("memory_size", `MEMORY.md \u2014 ${lines} line${lines !== 1 ? "s" : ""} (healthy)`, ""));
2673
+ }
2674
+ }
2675
+ const configuredAgents = config.agents ?? [];
2676
+ if (configuredAgents.length === 0) {
2677
+ results.push(warn2("agent_files", "No agents configured", `Run ${chalk17.cyan("mindlink config")} \u2192 Agent instruction files.`, false));
2678
+ } else {
2679
+ const missing = [];
2680
+ for (const agentValue of configuredAgents) {
2681
+ const agent = AGENTS.find((a) => a.value === agentValue);
2682
+ if (!agent) continue;
2683
+ if (!existsSync16(join17(projectPath, agent.destFile))) missing.push(agent.destFile);
2684
+ }
2685
+ if (missing.length === configuredAgents.length) {
2686
+ results.push(fail2("agent_files", "Agent files \u2014 none present", `All configured agent files are missing. Run ${chalk17.cyan("mindlink verify --fix")} to regenerate.`, true));
2687
+ } else if (missing.length > 0) {
2688
+ results.push(warn2("agent_files", `Agent files \u2014 ${missing.length} missing: ${missing.join(", ")}`, `Run ${chalk17.cyan("mindlink verify --fix")} to regenerate.`, true));
2689
+ } else {
2690
+ results.push(pass("agent_files", `Agent files \u2014 all ${configuredAgents.length} present`, ""));
2691
+ }
2692
+ }
2693
+ return results;
2694
+ }
2695
+ function applyFix(projectPath, results) {
2696
+ const brainDir = join17(projectPath, BRAIN_DIR);
2697
+ const configPath = join17(brainDir, "config.json");
2698
+ let config = {};
2699
+ if (existsSync16(configPath)) {
2700
+ try {
2701
+ config = JSON.parse(readFileSync14(configPath, "utf8"));
2702
+ } catch {
2703
+ }
2704
+ }
2705
+ const fixable = results.filter((r) => r.fixable && r.status !== "pass");
2706
+ if (fixable.length === 0) {
2707
+ console.log(` ${chalk17.dim("Nothing to auto-fix. Address the issues above manually.")}`);
2708
+ return;
2709
+ }
2710
+ let fixed = 0;
2711
+ for (const r of fixable) {
2712
+ if (r.id === "agent_files") {
2713
+ const configuredAgents = config.agents ?? [];
2714
+ for (const agentValue of configuredAgents) {
2715
+ const agent = AGENTS.find((a) => a.value === agentValue);
2716
+ if (!agent) continue;
2717
+ const destPath = join17(projectPath, agent.destFile);
2718
+ if (!existsSync16(destPath)) {
2719
+ try {
2720
+ mkdirSync6(dirname5(destPath), { recursive: true });
2721
+ writeFileSync7(destPath, readFileSync14(join17(AGENT_TEMPLATES_DIR, agent.templateFile), "utf8"));
2722
+ console.log(` ${chalk17.green("\u2713")} Regenerated ${agent.destFile}`);
2723
+ fixed++;
2724
+ } catch (err) {
2725
+ console.log(` ${chalk17.red("\u2717")} Failed to regenerate ${agent.destFile}: ${err instanceof Error ? err.message : err}`);
2726
+ }
2727
+ }
2728
+ }
2729
+ if (configuredAgents.includes("claude")) {
2730
+ const hookPath = join17(projectPath, ".claude", "settings.json");
2731
+ if (!existsSync16(hookPath)) {
2732
+ try {
2733
+ mkdirSync6(dirname5(hookPath), { recursive: true });
2734
+ writeFileSync7(hookPath, readFileSync14(join17(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
2735
+ console.log(` ${chalk17.green("\u2713")} Regenerated .claude/settings.json`);
2736
+ fixed++;
2737
+ } catch {
2738
+ }
2739
+ }
2740
+ }
2741
+ }
2742
+ }
2743
+ for (const r of results) {
2744
+ if (r.status === "fail" || r.status === "warn") {
2745
+ if (r.id === "core" || r.id === "user_profile") {
2746
+ console.log(` ${chalk17.dim("\u2192")} ${r.id === "core" ? "Core" : "User Profile"}: start a session and tell your AI to fill this in.`);
2747
+ } else if (r.id === "memory_size") {
2748
+ console.log(` ${chalk17.dim("\u2192")} MEMORY.md too large: run ${chalk17.cyan("mindlink prune")} to consolidate.`);
2749
+ }
2750
+ }
2751
+ }
2752
+ if (fixed > 0) console.log("");
2753
+ }
2754
+ var verifyCommand = new Command16("verify").description("Check that .brain/ memory is healthy and up-to-date").option("--json", "Output results as JSON").option("--fix", "Auto-fix recoverable issues (regenerate missing agent files)").addHelpText("after", `
2755
+ What is checked:
2756
+ Core \u2014 MEMORY.md Core section has real content
2757
+ User Profile \u2014 MEMORY.md User Profile has real content
2758
+ SESSION.md \u2014 was updated recently (warn >3 days, fail >7 days)
2759
+ LOG.md \u2014 present and readable
2760
+ MEMORY.md size \u2014 line count (warn >100, fail >200)
2761
+ Agent files \u2014 all files from config.json are present on disk
2762
+
2763
+ Examples:
2764
+ mindlink verify
2765
+ mindlink verify --json
2766
+ mindlink verify --fix
2767
+ `).action((opts) => {
2768
+ const projectPath = resolve14(process.cwd());
2769
+ const results = runChecks(projectPath);
2770
+ if (opts.json) {
2771
+ console.log(JSON.stringify({ ok: results.every((r) => r.status === "pass"), checks: results }, null, 2));
2772
+ const hasFailure = results.some((r) => r.status === "fail");
2773
+ process.exit(hasFailure ? 1 : 0);
2774
+ }
2775
+ console.log("");
2776
+ console.log(` ${chalk17.bold("\u25C9 MindLink Verify")}`);
2777
+ console.log(` ${chalk17.dim(projectPath)}`);
2778
+ console.log("");
2779
+ for (const r of results) {
2780
+ console.log(` ${icon2(r.status)} ${r.label}`);
2781
+ if (r.message) console.log(` ${chalk17.dim(r.message)}`);
2782
+ }
2783
+ console.log("");
2784
+ const failCount = results.filter((r) => r.status === "fail").length;
2785
+ const warnCount = results.filter((r) => r.status === "warn").length;
2786
+ const fixableCount = results.filter((r) => r.fixable && r.status !== "pass").length;
2787
+ if (opts.fix) {
2788
+ applyFix(projectPath, results);
2789
+ }
2790
+ if (failCount > 0) {
2791
+ console.log(` ${chalk17.red.bold(`${failCount} error${failCount !== 1 ? "s" : ""}`)}, ${warnCount} warning${warnCount !== 1 ? "s" : ""}.`);
2792
+ if (!opts.fix && fixableCount > 0) {
2793
+ console.log(` ${chalk17.dim(`Run ${chalk17.cyan("mindlink verify --fix")} to auto-repair ${fixableCount} issue${fixableCount !== 1 ? "s" : ""}.`)}`);
2794
+ }
2795
+ } else if (warnCount > 0) {
2796
+ console.log(` ${chalk17.yellow.bold(`${warnCount} warning${warnCount !== 1 ? "s" : ""}`)}, no critical errors.`);
2797
+ } else {
2798
+ console.log(` ${chalk17.green.bold("All good.")} Your AI's memory is healthy.`);
2799
+ }
2800
+ console.log("");
2801
+ if (failCount > 0) process.exit(1);
2802
+ });
2803
+
2804
+ // src/commands/profile.ts
2805
+ import { Command as Command17 } from "commander";
2806
+ import chalk18 from "chalk";
2807
+ import { existsSync as existsSync17, readFileSync as readFileSync15, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
2808
+ import { execSync as execSync4 } from "child_process";
2809
+ var PROFILE_TEMPLATE = `# MindLink \u2014 Global User Profile
2810
+
2811
+ > This file is imported into every new project's MEMORY.md on \`mindlink init\`.
2812
+ > Edit it with \`mindlink profile\`. Run \`mindlink update\` to sync changes to all projects.
2813
+
2814
+ <!-- Role, company, title, level, years of experience -->
2815
+ <!-- Primary languages and tools -->
2816
+ <!-- Communication style and preferences -->
2817
+ <!-- Editor, OS, shell setup -->
2818
+ <!-- Added: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)} -->
2819
+ `;
2820
+ function ensureProfileExists() {
2821
+ if (!existsSync17(GLOBAL_MINDLINK_DIR)) {
2822
+ mkdirSync7(GLOBAL_MINDLINK_DIR, { recursive: true });
2823
+ }
2824
+ if (!existsSync17(GLOBAL_USER_PROFILE_PATH)) {
2825
+ writeFileSync8(GLOBAL_USER_PROFILE_PATH, PROFILE_TEMPLATE);
2826
+ }
2827
+ }
2828
+ var profileCommand = new Command17("profile").description("Manage your global user profile (imported into every new project)").option("--show", "Print current profile to stdout").option("--path", "Print profile file path only").addHelpText("after", `
2829
+ Your profile is stored at ~/.mindlink/USER.md and auto-imported into
2830
+ MEMORY.md when you run \`mindlink init\` on a new project.
2831
+
2832
+ Run \`mindlink update\` to sync profile changes to all registered projects.
2833
+
2834
+ Examples:
2835
+ mindlink profile # open profile in $EDITOR
2836
+ mindlink profile --show # print profile to stdout
2837
+ mindlink profile --path # print file path
2838
+ `).action((opts) => {
2839
+ if (opts.path) {
2840
+ console.log(GLOBAL_USER_PROFILE_PATH);
2841
+ return;
2842
+ }
2843
+ if (opts.show) {
2844
+ if (!existsSync17(GLOBAL_USER_PROFILE_PATH)) {
2845
+ console.log("");
2846
+ console.log(` ${chalk18.yellow("\u26A0")} No global profile yet.`);
2847
+ console.log(` Run ${chalk18.cyan("mindlink profile")} to create one.`);
2848
+ console.log("");
2849
+ return;
2850
+ }
2851
+ console.log("");
2852
+ console.log(readFileSync15(GLOBAL_USER_PROFILE_PATH, "utf8"));
2853
+ return;
2854
+ }
2855
+ ensureProfileExists();
2856
+ const editor = process.env.EDITOR || process.env.VISUAL || "nano";
2857
+ console.log("");
2858
+ console.log(` ${chalk18.dim(`Opening ${GLOBAL_USER_PROFILE_PATH} in ${editor}...`)}`);
2859
+ console.log(` ${chalk18.dim("Run mindlink update after saving to sync to all registered projects.")}`);
2860
+ console.log("");
2861
+ try {
2862
+ execSync4(`${editor} "${GLOBAL_USER_PROFILE_PATH}"`, { stdio: "inherit" });
2863
+ console.log("");
2864
+ console.log(` ${chalk18.green("\u2713")} Profile saved.`);
2865
+ console.log(` ${chalk18.dim("\u2192 Run mindlink update to sync to all registered projects.")}`);
2866
+ console.log("");
2867
+ } catch {
2868
+ console.log("");
2869
+ }
2870
+ });
2871
+
2872
+ // src/commands/prune.ts
2873
+ import { Command as Command18 } from "commander";
2874
+ import { select as select6, isCancel as isCancel8 } from "@clack/prompts";
2875
+ import chalk19 from "chalk";
2876
+ import { existsSync as existsSync18, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
2877
+ import { join as join18, resolve as resolve15 } from "path";
2878
+ var STALENESS_THRESHOLDS = {
2879
+ "current focus": 14,
2880
+ "decisions": 180,
2881
+ "conventions": 180,
2882
+ "architecture": 365
2883
+ // 'user profile' and 'important context' have no expiry
2884
+ };
2885
+ var TIMESTAMP_RE = /<!--\s*added:\s*(\d{4}-\d{2}-\d{2})\s*-->/;
2886
+ var SECTION_RE = /^(#{1,6})\s+(.+)/;
2887
+ function parseTimestampedEntries(content) {
2888
+ const lines = content.split("\n");
2889
+ const entries = [];
2890
+ let currentSection = "";
2891
+ let currentHeadingLevel = 0;
2892
+ let entryLines = [];
2893
+ let entryStart = -1;
2894
+ function flushEntry(lineEnd) {
2895
+ if (entryStart < 0 || entryLines.length === 0) return;
2896
+ const fullText = entryLines.join("\n");
2897
+ const timestampMatch = fullText.match(TIMESTAMP_RE);
2898
+ if (!timestampMatch) {
2899
+ entryLines = [];
2900
+ entryStart = -1;
2901
+ return;
2902
+ }
2903
+ const addedDate = /* @__PURE__ */ new Date(timestampMatch[1] + "T00:00:00Z");
2904
+ const ageInDays = (Date.now() - addedDate.getTime()) / 864e5;
2905
+ const sectionKey = currentSection.toLowerCase().replace(/\s*<!--.*?-->\s*/g, "").trim();
2906
+ const threshold = STALENESS_THRESHOLDS[sectionKey] ?? Infinity;
2907
+ const isStale = isFinite(threshold) && ageInDays > threshold;
2908
+ const trimmedLines = entryLines.filter((l) => l.trim().length > 0);
2909
+ const displayText = trimmedLines.join("\n");
2910
+ entries.push({
2911
+ section: currentSection.replace(/<!--.*?-->/g, "").trim(),
2912
+ text: displayText,
2913
+ addedDate,
2914
+ lineStart: entryStart,
2915
+ lineEnd,
2916
+ ageInDays,
2917
+ threshold,
2918
+ isStale
2919
+ });
2920
+ entryLines = [];
2921
+ entryStart = -1;
2922
+ }
2923
+ for (let i = 0; i < lines.length; i++) {
2924
+ const line = lines[i];
2925
+ const sectionMatch = line.match(SECTION_RE);
2926
+ if (sectionMatch) {
2927
+ const level = sectionMatch[1].length;
2928
+ const title = sectionMatch[2];
2929
+ flushEntry(i);
2930
+ if (level <= 2) {
2931
+ currentSection = title;
2932
+ currentHeadingLevel = level;
2933
+ }
2934
+ continue;
2935
+ }
2936
+ if (line.trim() === "---") {
2937
+ flushEntry(i);
2938
+ continue;
2939
+ }
2940
+ if (line.trim().startsWith("<!--") && line.trim().endsWith("-->")) {
2941
+ if (entryStart < 0) continue;
2942
+ }
2943
+ if (TIMESTAMP_RE.test(line)) {
2944
+ if (entryStart < 0) entryStart = i;
2945
+ entryLines.push(line);
2946
+ flushEntry(i + 1);
2947
+ continue;
2948
+ }
2949
+ if (line.trim().length > 0 && !line.trim().startsWith(">")) {
2950
+ if (entryStart < 0) entryStart = i;
2951
+ entryLines.push(line);
2952
+ } else if (entryStart >= 0) {
2953
+ entryLines.push(line);
2954
+ }
2955
+ }
2956
+ flushEntry(lines.length);
2957
+ return entries;
2958
+ }
2959
+ function removeLines(content, lineStart, lineEnd) {
2960
+ const lines = content.split("\n");
2961
+ lines.splice(lineStart, lineEnd - lineStart);
2962
+ return lines.join("\n");
2963
+ }
2964
+ function appendToArchive(content, entryText, pruneDate) {
2965
+ const archiveHeading = "## Archive";
2966
+ const archiveEntry = `${entryText} <!-- archived: ${pruneDate} -->`;
2967
+ if (content.includes(archiveHeading)) {
2968
+ return content.replace(
2969
+ /(## Archive\n(?:<!--[^>]*-->\n)*)/,
2970
+ `$1
2971
+ ${archiveEntry}
2972
+ `
2973
+ );
2974
+ } else {
2975
+ return content.trimEnd() + `
2976
+
2977
+ ${archiveHeading}
2978
+
2979
+ <!-- Entries moved here by mindlink prune \u2014 kept for reference -->
2980
+
2981
+ ${archiveEntry}
2982
+ `;
2983
+ }
2984
+ }
2985
+ function formatAge(days) {
2986
+ if (days < 30) return `${Math.round(days)} days`;
2987
+ if (days < 365) return `${Math.round(days / 30)} months`;
2988
+ return `${(days / 365).toFixed(1)} years`;
2989
+ }
2990
+ var pruneCommand = new Command18("prune").description("Review and retire stale MEMORY.md entries interactively").option("--dry-run", "Show stale entries without making any changes").option("--all", "Show all timestamped entries regardless of age").addHelpText("after", `
2991
+ Scans MEMORY.md for entries with <!-- added: YYYY-MM-DD --> timestamps.
2992
+ Entries older than their section's staleness threshold are flagged for review.
2993
+
2994
+ Staleness thresholds:
2995
+ Current Focus \u2014 14 days
2996
+ Decisions \u2014 180 days
2997
+ Conventions \u2014 180 days
2998
+ Architecture \u2014 365 days
2999
+ User Profile \u2014 never expires
3000
+
3001
+ Archived entries are moved to ## Archive at the bottom of MEMORY.md.
3002
+ They are never permanently deleted unless you choose "Delete".
3003
+
3004
+ Examples:
3005
+ mindlink prune
3006
+ mindlink prune --dry-run
3007
+ mindlink prune --all
3008
+ `).action(async (opts) => {
3009
+ const projectPath = resolve15(process.cwd());
3010
+ const brainDir = join18(projectPath, BRAIN_DIR);
3011
+ const memoryPath = join18(brainDir, "MEMORY.md");
3012
+ console.log("");
3013
+ console.log(` ${chalk19.bold("\u25C9 MindLink Prune")}`);
3014
+ console.log(` ${chalk19.dim(memoryPath)}`);
3015
+ console.log("");
3016
+ if (!existsSync18(memoryPath)) {
3017
+ console.log(` ${chalk19.red("\u2717")} MEMORY.md not found. Run ${chalk19.cyan("mindlink init")} first.`);
3018
+ console.log("");
3019
+ process.exit(1);
3020
+ }
3021
+ const allEntries = parseTimestampedEntries(readFileSync16(memoryPath, "utf8"));
3022
+ const toReview = opts.all ? allEntries.filter((e) => e.addedDate !== null) : allEntries.filter((e) => e.isStale);
3023
+ if (toReview.length === 0) {
3024
+ if (allEntries.length === 0) {
3025
+ console.log(` ${chalk19.dim("No timestamped entries found in MEMORY.md.")}`);
3026
+ console.log(` ${chalk19.dim("Entries are timestamped when your AI writes them (<!-- added: YYYY-MM-DD -->).")}`);
3027
+ } else {
3028
+ console.log(` ${chalk19.green("\u2713")} No stale entries found. ${allEntries.length} entry${allEntries.length !== 1 ? "ies" : "y"} all within threshold.`);
3029
+ console.log(` ${chalk19.dim("Run mindlink prune --all to review all timestamped entries.")}`);
3030
+ }
3031
+ console.log("");
3032
+ return;
3033
+ }
3034
+ console.log(` Scanning MEMORY.md for ${opts.all ? "timestamped" : "stale"} entries...`);
3035
+ console.log(` Found ${toReview.length} entr${toReview.length !== 1 ? "ies" : "y"} to review.`);
3036
+ console.log("");
3037
+ if (opts.dryRun) {
3038
+ for (const entry of toReview) {
3039
+ const age = entry.ageInDays !== null ? formatAge(entry.ageInDays) : "undated";
3040
+ const thresholdStr = isFinite(entry.threshold) ? ` \u2014 threshold: ${entry.threshold} days` : "";
3041
+ console.log(` ${chalk19.yellow("\u26A0")} [${entry.section}] ${entry.text.split("\n")[0].slice(0, 80)}`);
3042
+ console.log(` ${chalk19.dim(`Added: ${entry.addedDate?.toISOString().slice(0, 10) ?? "unknown"} (${age} ago)${thresholdStr}`)}`);
3043
+ console.log("");
3044
+ }
3045
+ console.log(` ${chalk19.dim("Dry run \u2014 no changes made.")}`);
3046
+ console.log("");
3047
+ return;
3048
+ }
3049
+ const pruneDate = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3050
+ let content = readFileSync16(memoryPath, "utf8");
3051
+ let archived = 0;
3052
+ let deleted = 0;
3053
+ let kept = 0;
3054
+ let skippedAll = false;
3055
+ const sorted = [...toReview].sort((a, b) => b.lineStart - a.lineStart);
3056
+ for (const entry of sorted) {
3057
+ if (skippedAll) {
3058
+ kept++;
3059
+ continue;
3060
+ }
3061
+ const age = entry.ageInDays !== null ? formatAge(entry.ageInDays) : "undated";
3062
+ const thresholdStr = isFinite(entry.threshold) ? ` \u2014 threshold: ${entry.threshold} days` : "";
3063
+ const displayLines = entry.text.split("\n").slice(0, 3).join("\n");
3064
+ console.log(` ${chalk19.bold("\u2500".repeat(55))}`);
3065
+ console.log(` ${chalk19.bold("Section:")} ${entry.section}`);
3066
+ console.log(` ${chalk19.bold("Entry:")}`);
3067
+ for (const l of displayLines.split("\n")) console.log(` ${chalk19.dim(l)}`);
3068
+ console.log(` ${chalk19.bold("Added:")} ${entry.addedDate?.toISOString().slice(0, 10) ?? "unknown"} (${age} ago)${thresholdStr}`);
3069
+ console.log("");
3070
+ const action = await select6({
3071
+ message: "What would you like to do?",
3072
+ options: [
3073
+ { value: "keep", label: "Keep", hint: "leave as-is" },
3074
+ { value: "archive", label: "Archive", hint: "move to ## Archive section" },
3075
+ { value: "delete", label: "Delete", hint: "remove permanently" },
3076
+ { value: "skip_all", label: "Skip remaining", hint: "keep all remaining entries unchanged" }
3077
+ ]
3078
+ });
3079
+ if (isCancel8(action)) {
3080
+ console.log("");
3081
+ break;
3082
+ }
3083
+ if (action === "skip_all") {
3084
+ skippedAll = true;
3085
+ kept++;
3086
+ continue;
3087
+ }
3088
+ if (action === "archive") {
3089
+ const entryText = content.split("\n").slice(entry.lineStart, entry.lineEnd).join("\n").trim();
3090
+ content = removeLines(content, entry.lineStart, entry.lineEnd);
3091
+ content = appendToArchive(content, entryText, pruneDate);
3092
+ archived++;
3093
+ } else if (action === "delete") {
3094
+ content = removeLines(content, entry.lineStart, entry.lineEnd);
3095
+ deleted++;
3096
+ } else {
3097
+ kept++;
3098
+ }
3099
+ console.log("");
3100
+ }
3101
+ if (archived > 0 || deleted > 0) {
3102
+ writeFileSync9(memoryPath, content);
3103
+ }
3104
+ console.log(` ${chalk19.bold("\u2500".repeat(55))}`);
3105
+ if (archived > 0) console.log(` ${chalk19.green("\u2713")} ${archived} entry${archived !== 1 ? "ies" : ""} archived`);
3106
+ if (deleted > 0) console.log(` ${chalk19.green("\u2713")} ${deleted} entry${deleted !== 1 ? "ies" : ""} deleted`);
3107
+ if (kept > 0) console.log(` ${chalk19.dim("\xB7")} ${kept} entry${kept !== 1 ? "ies" : ""} kept`);
3108
+ if (archived > 0 || deleted > 0) {
3109
+ console.log(` ${chalk19.green("\u2713")} MEMORY.md updated.`);
3110
+ }
3111
+ console.log("");
3112
+ });
3113
+
3114
+ // src/commands/mcp.ts
3115
+ import { Command as Command19 } from "commander";
3116
+ import { existsSync as existsSync19, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
3117
+ import { join as join19, resolve as resolve16, dirname as dirname6 } from "path";
3118
+ function resolveProjectPath() {
3119
+ const envPath = process.env.MINDLINK_PROJECT_PATH;
3120
+ if (envPath && existsSync19(join19(envPath, BRAIN_DIR))) return envPath;
3121
+ let current = resolve16(process.cwd());
3122
+ for (let i = 0; i < 10; i++) {
3123
+ if (existsSync19(join19(current, BRAIN_DIR))) return current;
3124
+ const parent = dirname6(current);
3125
+ if (parent === current) break;
3126
+ current = parent;
3127
+ }
3128
+ return null;
3129
+ }
3130
+ function readMemorySection(memoryPath, section) {
3131
+ const content = readFileSync17(memoryPath, "utf8");
3132
+ if (!section) {
3133
+ const core = extractSection(content, "Core");
3134
+ const profile = extractSection(content, "User Profile");
3135
+ return `## Core
3136
+
3137
+ ${core}
3138
+
3139
+ ## User Profile
3140
+
3141
+ ${profile}`.trim();
3142
+ }
3143
+ return extractSection(content, section);
3144
+ }
3145
+ function appendToSection(memoryPath, section, newContent) {
3146
+ let content = readFileSync17(memoryPath, "utf8");
3147
+ const sectionBody = extractSection(content, section);
3148
+ const lines = content.split("\n");
3149
+ let headingIdx = -1;
3150
+ let nextSectionIdx = lines.length;
3151
+ let headingLevel = 0;
3152
+ for (let i = 0; i < lines.length; i++) {
3153
+ const match = lines[i].match(/^(#{1,6})\s+(.+)/);
3154
+ if (match) {
3155
+ const level = match[1].length;
3156
+ const title = match[2].replace(/<!--.*?-->/g, "").trim();
3157
+ if (headingIdx < 0 && title.toLowerCase() === section.toLowerCase()) {
3158
+ headingIdx = i;
3159
+ headingLevel = level;
3160
+ } else if (headingIdx >= 0 && level <= headingLevel) {
3161
+ nextSectionIdx = i;
3162
+ break;
3163
+ }
3164
+ }
3165
+ }
3166
+ if (headingIdx < 0) {
3167
+ content = content.trimEnd() + `
3168
+
3169
+ ## ${section}
3170
+
3171
+ ${newContent.trim()}
3172
+ `;
3173
+ } else {
3174
+ const insertAt = nextSectionIdx > 0 && lines[nextSectionIdx - 1].trim() === "---" ? nextSectionIdx - 1 : nextSectionIdx;
3175
+ lines.splice(insertAt, 0, "", newContent.trim(), "");
3176
+ content = lines.join("\n");
3177
+ }
3178
+ writeFileSync10(memoryPath, content);
3179
+ void sectionBody;
3180
+ }
3181
+ var mcpCommand = new Command19("mcp").description("Start the MindLink MCP server (stdio transport for AI tool integration)").addHelpText("after", `
3182
+ The MCP server runs as a local process launched by Claude Code.
3183
+ It exposes 4 tools for auditable, schema-validated memory reads and writes.
3184
+
3185
+ Tools:
3186
+ mindlink_read_memory(section?) \u2014 read a section of MEMORY.md
3187
+ mindlink_write_memory(section, content) \u2014 append to a MEMORY.md section
3188
+ mindlink_session_update(summary) \u2014 overwrite SESSION.md
3189
+ mindlink_verify() \u2014 run health check, return JSON
3190
+
3191
+ Configure in .claude/settings.json (done automatically by mindlink init):
3192
+ { "mcpServers": { "mindlink": { "command": "mindlink", "args": ["mcp"] } } }
3193
+
3194
+ Examples:
3195
+ mindlink mcp # (launched by Claude Code, not by hand)
3196
+ `).action(async () => {
3197
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
3198
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
3199
+ const { z } = await import("./zod-FDOV7Y2P.js");
3200
+ const server = new McpServer({ name: "mindlink", version: VERSION });
3201
+ server.tool(
3202
+ "mindlink_read_memory",
3203
+ "Read a section of this project's MEMORY.md. If section is omitted, returns Core + User Profile only.",
3204
+ {
3205
+ section: z.enum(["Core", "Architecture", "Decisions", "Conventions", "User Profile", "Important Context"]).optional().describe("Section to read. Omit for Core + User Profile (recommended default).")
3206
+ },
3207
+ async ({ section }) => {
3208
+ const projectPath = resolveProjectPath();
3209
+ if (!projectPath) {
3210
+ return {
3211
+ content: [{ type: "text", text: "Error: No MindLink project found at this path. Run mindlink init first." }],
3212
+ isError: true
3213
+ };
3214
+ }
3215
+ const memoryPath = join19(projectPath, BRAIN_DIR, "MEMORY.md");
3216
+ if (!existsSync19(memoryPath)) {
3217
+ return {
3218
+ content: [{ type: "text", text: "Error: MEMORY.md not found. Run mindlink init to create it." }],
3219
+ isError: true
3220
+ };
3221
+ }
3222
+ try {
3223
+ const content = readMemorySection(memoryPath, section);
3224
+ return { content: [{ type: "text", text: content || "(section is empty)" }] };
3225
+ } catch (err) {
3226
+ return {
3227
+ content: [{ type: "text", text: `Error reading MEMORY.md: ${err instanceof Error ? err.message : err}` }],
3228
+ isError: true
3229
+ };
3230
+ }
3231
+ }
3232
+ );
3233
+ server.tool(
3234
+ "mindlink_write_memory",
3235
+ "Append a fact or decision to a section of MEMORY.md. Never overwrites existing content. Always include a <!-- added: YYYY-MM-DD --> timestamp.",
3236
+ {
3237
+ section: z.enum(["Core", "Architecture", "Decisions", "Conventions", "User Profile", "Important Context"]).describe("Section to append to."),
3238
+ content: z.string().describe("The markdown content to append. Include <!-- added: YYYY-MM-DD --> timestamp.")
3239
+ },
3240
+ async ({ section, content }) => {
3241
+ const projectPath = resolveProjectPath();
3242
+ if (!projectPath) {
3243
+ return {
3244
+ content: [{ type: "text", text: "Error: No MindLink project found. Run mindlink init first." }],
3245
+ isError: true
3246
+ };
3247
+ }
3248
+ const memoryPath = join19(projectPath, BRAIN_DIR, "MEMORY.md");
3249
+ if (!existsSync19(memoryPath)) {
3250
+ return {
3251
+ content: [{ type: "text", text: "Error: MEMORY.md not found. Run mindlink init to create it." }],
3252
+ isError: true
3253
+ };
3254
+ }
3255
+ try {
3256
+ appendToSection(memoryPath, section, content);
3257
+ return { content: [{ type: "text", text: `\u2713 Appended to ## ${section} in MEMORY.md.` }] };
3258
+ } catch (err) {
3259
+ return {
3260
+ content: [{ type: "text", text: `Error writing MEMORY.md: ${err instanceof Error ? err.message : err}` }],
3261
+ isError: true
3262
+ };
3263
+ }
3264
+ }
3265
+ );
3266
+ server.tool(
3267
+ "mindlink_session_update",
3268
+ "Update SESSION.md with the current session summary. Call this as the last action of every response.",
3269
+ {
3270
+ summary: z.string().describe("Current task state \u2014 what was asked, what you answered, any decisions made, what's next.")
3271
+ },
3272
+ async ({ summary }) => {
3273
+ const projectPath = resolveProjectPath();
3274
+ if (!projectPath) {
3275
+ return {
3276
+ content: [{ type: "text", text: "Error: No MindLink project found. Run mindlink init first." }],
3277
+ isError: true
3278
+ };
3279
+ }
3280
+ const sessionPath = join19(projectPath, BRAIN_DIR, "SESSION.md");
3281
+ try {
3282
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3283
+ const content = `# Session State
3284
+
3285
+ <!-- Last updated: ${date} -->
3286
+
3287
+ ${summary.trim()}
3288
+ `;
3289
+ writeFileSync10(sessionPath, content);
3290
+ return { content: [{ type: "text", text: "\u2713 SESSION.md updated." }] };
3291
+ } catch (err) {
3292
+ return {
3293
+ content: [{ type: "text", text: `Error writing SESSION.md: ${err instanceof Error ? err.message : err}` }],
3294
+ isError: true
3295
+ };
3296
+ }
3297
+ }
3298
+ );
3299
+ server.tool(
3300
+ "mindlink_verify",
3301
+ "Run a health check on .brain/. Returns pass/warn/fail status for each check. Call this to verify your memory writes succeeded.",
3302
+ {},
3303
+ async () => {
3304
+ const projectPath = resolveProjectPath();
3305
+ if (!projectPath) {
3306
+ return {
3307
+ content: [{ type: "text", text: JSON.stringify({ ok: false, error: "No MindLink project found. Run mindlink init first." }) }],
3308
+ isError: true
3309
+ };
3310
+ }
3311
+ const checks = runChecks(projectPath);
3312
+ const ok2 = checks.every((c) => c.status === "pass");
3313
+ return {
3314
+ content: [{ type: "text", text: JSON.stringify({ ok: ok2, checks }, null, 2) }]
3315
+ };
3316
+ }
3317
+ );
3318
+ const transport = new StdioServerTransport();
3319
+ await server.connect(transport);
3320
+ });
3321
+
1879
3322
  // src/cli.ts
1880
- var program = new Command15();
3323
+ var program = new Command20();
1881
3324
  program.name("mindlink").description("Give your AI a brain.").version(VERSION, "-v, --version");
1882
3325
  program.addCommand(initCommand);
1883
3326
  program.addCommand(statusCommand);
@@ -1893,9 +3336,14 @@ program.addCommand(exportCommand);
1893
3336
  program.addCommand(importCommand);
1894
3337
  program.addCommand(doctorCommand);
1895
3338
  program.addCommand(versionCommand);
3339
+ program.addCommand(diffCommand);
3340
+ program.addCommand(verifyCommand);
3341
+ program.addCommand(profileCommand);
3342
+ program.addCommand(pruneCommand);
3343
+ program.addCommand(mcpCommand);
1896
3344
  program.on("command:*", (operands) => {
1897
3345
  const unknown = operands[0];
1898
- const known = ["init", "status", "log", "clear", "reset", "config", "sync", "update", "summary", "uninstall", "export", "import", "doctor", "version"];
3346
+ const known = ["init", "status", "log", "clear", "reset", "config", "sync", "update", "summary", "uninstall", "export", "import", "doctor", "version", "diff", "verify", "profile", "prune", "mcp"];
1899
3347
  function levenshtein(a, b) {
1900
3348
  const m = a.length, n = b.length;
1901
3349
  const dp = Array.from(
@@ -1911,11 +3359,11 @@ program.on("command:*", (operands) => {
1911
3359
  }
1912
3360
  const closest = known.map((cmd) => ({ cmd, dist: levenshtein(unknown, cmd) })).sort((a, b) => a.dist - b.dist)[0];
1913
3361
  console.log("");
1914
- console.log(` ${chalk16.red("\u2717")} Unknown command: ${chalk16.bold(unknown)}`);
3362
+ console.log(` ${chalk20.red("\u2717")} Unknown command: ${chalk20.bold(unknown)}`);
1915
3363
  if (closest && closest.dist <= 3) {
1916
- console.log(` Did you mean ${chalk16.cyan("mindlink " + closest.cmd)}?`);
3364
+ console.log(` Did you mean ${chalk20.cyan("mindlink " + closest.cmd)}?`);
1917
3365
  }
1918
- console.log(` Run ${chalk16.cyan("mindlink --help")} to see all commands.`);
3366
+ console.log(` Run ${chalk20.cyan("mindlink --help")} to see all commands.`);
1919
3367
  console.log("");
1920
3368
  process.exit(1);
1921
3369
  });