mindlink 1.1.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command15 } from "commander";
5
- import chalk16 from "chalk";
4
+ import { Command as Command16 } from "commander";
5
+ import chalk17 from "chalk";
6
6
 
7
7
  // src/utils/version.ts
8
- var VERSION = "1.1.4";
8
+ var VERSION = "1.1.5";
9
9
 
10
10
  // src/commands/init.ts
11
11
  import { Command } from "commander";
@@ -24,8 +24,10 @@ import {
24
24
  mkdirSync as mkdirSync2,
25
25
  readFileSync as readFileSync2,
26
26
  writeFileSync as writeFileSync2,
27
- appendFileSync
27
+ appendFileSync,
28
+ readdirSync
28
29
  } from "fs";
30
+ import { execSync } from "child_process";
29
31
  import { join as join3, resolve, dirname as dirname2, basename } from "path";
30
32
 
31
33
  // src/utils/paths.ts
@@ -58,7 +60,11 @@ var AGENTS = [
58
60
  { value: "copilot", label: "GitHub Copilot", hint: ".github/copilot-instructions.md", templateFile: "copilot-instructions.md", destFile: ".github/copilot-instructions.md", selected: true },
59
61
  { value: "windsurf", label: "Windsurf", hint: ".windsurfrules", templateFile: ".windsurfrules", destFile: ".windsurfrules", selected: true },
60
62
  { 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 }
63
+ { value: "aider", label: "Aider", hint: "CONVENTIONS.md", templateFile: "CONVENTIONS.md", destFile: "CONVENTIONS.md", selected: false },
64
+ { value: "zed", label: "Zed", hint: ".rules", templateFile: ".rules", destFile: ".rules", selected: false },
65
+ { value: "kiro", label: "Kiro", hint: ".kiro/steering/mindlink.md", templateFile: "kiro-steering.md", destFile: ".kiro/steering/mindlink.md", selected: false },
66
+ { value: "continue", label: "Continue.dev", hint: ".continue/rules/mindlink.md", templateFile: "continue-rules.md", destFile: ".continue/rules/mindlink.md", selected: false },
67
+ { value: "trae", label: "Trae", hint: ".trae/rules/mindlink.md", templateFile: "trae-rules.md", destFile: ".trae/rules/mindlink.md", selected: false }
62
68
  ];
63
69
 
64
70
  // src/utils/registry.ts
@@ -100,13 +106,21 @@ function detectProjectInfo(projectPath) {
100
106
  let name = basename(projectPath);
101
107
  let description = "";
102
108
  let stack = "";
109
+ let recentActivity = "";
110
+ let topDirs = "";
103
111
  const pkgPath = join3(projectPath, "package.json");
104
112
  if (existsSync2(pkgPath)) {
105
113
  try {
106
114
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
107
115
  if (pkg.name) name = pkg.name;
108
116
  if (pkg.description) description = pkg.description;
109
- stack = "Node.js";
117
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
118
+ const layers = ["Node.js"];
119
+ if (deps["typescript"] || deps["ts-node"] || deps["tsup"]) layers.push("TypeScript");
120
+ if (deps["react"] || deps["next"]) layers.push(deps["next"] ? "Next.js" : "React");
121
+ if (deps["express"] || deps["fastify"] || deps["koa"]) layers.push("Express/Fastify");
122
+ if (deps["vite"]) layers.push("Vite");
123
+ stack = layers.join(" + ");
110
124
  } catch {
111
125
  }
112
126
  }
@@ -119,7 +133,48 @@ function detectProjectInfo(projectPath) {
119
133
  else if (existsSync2(join3(projectPath, "composer.json"))) stack = "PHP";
120
134
  else if (existsSync2(join3(projectPath, "Gemfile"))) stack = "Ruby";
121
135
  }
122
- return { name, description, stack, date };
136
+ if (!description) {
137
+ const readmePath = join3(projectPath, "README.md");
138
+ if (existsSync2(readmePath)) {
139
+ try {
140
+ const lines = readFileSync2(readmePath, "utf8").split("\n");
141
+ for (const line of lines.slice(0, 30)) {
142
+ const trimmed = line.trim();
143
+ if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("!") && !trimmed.startsWith("<") && trimmed.length > 10) {
144
+ description = trimmed.replace(/\*\*/g, "").slice(0, 120);
145
+ break;
146
+ }
147
+ }
148
+ } catch {
149
+ }
150
+ }
151
+ }
152
+ try {
153
+ const log = execSync("git log --oneline -5", { cwd: projectPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
154
+ if (log) {
155
+ recentActivity = log.split("\n").slice(0, 3).join(" \xB7 ");
156
+ }
157
+ } catch {
158
+ }
159
+ const SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".brain", "binaries", ".cache", "coverage", ".next", "out"]);
160
+ try {
161
+ const entries = readdirSync(projectPath, { withFileTypes: true });
162
+ const dirs = entries.filter((e) => e.isDirectory() && !SKIP_DIRS.has(e.name) && !e.name.startsWith(".")).map((e) => e.name).slice(0, 6);
163
+ if (dirs.length > 0) topDirs = dirs.join(", ");
164
+ } catch {
165
+ }
166
+ return { name, description, stack, recentActivity, topDirs, date };
167
+ }
168
+ function memoryHasRealContent(memoryPath) {
169
+ try {
170
+ const lines = readFileSync2(memoryPath, "utf8").split("\n");
171
+ return lines.some((line) => {
172
+ const t = line.trim();
173
+ return t.length > 0 && !t.startsWith("#") && !t.startsWith("<!--") && !t.startsWith(">") && !t.startsWith("|") && t !== "---";
174
+ });
175
+ } catch {
176
+ return false;
177
+ }
123
178
  }
124
179
  function buildMemoryMd(templateContent, info2) {
125
180
  let content = templateContent;
@@ -138,10 +193,15 @@ ${info2.stack}
138
193
  <!-- Add layers: Frontend, Backend, Infra, etc. -->`
139
194
  );
140
195
  }
196
+ const focusLines = [];
197
+ if (info2.topDirs) focusLines.push(`Directories: ${info2.topDirs}`);
198
+ if (info2.recentActivity) focusLines.push(`Recent commits: ${info2.recentActivity}`);
199
+ const focusBlock = focusLines.length > 0 ? focusLines.join("\n") + `
200
+ <!-- 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
201
  content = content.replace(
142
202
  /### Current focus\n<!--[^]*?-->/,
143
203
  `### Current focus
144
- <!-- Initialized ${info2.date} \u2014 ask your AI to fill this in after your first session -->`
204
+ ${focusBlock}`
145
205
  );
146
206
  return content;
147
207
  }
@@ -161,41 +221,155 @@ Examples:
161
221
  const brainDir = join3(projectPath, BRAIN_DIR);
162
222
  printBanner();
163
223
  if (existsSync2(brainDir)) {
164
- if (opts.yes) {
224
+ const memoryPath = join3(brainDir, "MEMORY.md");
225
+ const hasMemory = existsSync2(memoryPath) && memoryHasRealContent(memoryPath);
226
+ if (!opts.yes && hasMemory) {
227
+ const hasAnyAgentFile = AGENTS.some((a) => existsSync2(join3(projectPath, a.destFile)));
228
+ if (!hasAnyAgentFile) {
229
+ console.log("");
230
+ console.log(` ${chalk2.cyan("\u25C9")} MindLink memory found in this project.`);
231
+ console.log(` ${chalk2.dim("MEMORY.md has content \u2014 this looks like a team project.")}`);
232
+ console.log("");
233
+ const action = await select({
234
+ message: "What would you like to do?",
235
+ options: [
236
+ { value: "restore", label: "Set up agent files", hint: "recommended for new team members \u2014 writes CLAUDE.md, .cursorrules, etc." },
237
+ { value: "reinit", label: "Full re-init", hint: "recreate everything, reconfigure settings" },
238
+ { value: "exit", label: "Cancel", hint: "" }
239
+ ]
240
+ });
241
+ if (isCancel(action) || action === "exit") {
242
+ process.exit(0);
243
+ }
244
+ if (action === "restore") {
245
+ const agentChoices = AGENTS.map((a) => ({
246
+ value: a.value,
247
+ label: `${a.label.padEnd(18)} ${chalk2.dim(a.hint)}`,
248
+ hint: a.selected ? "recommended" : void 0
249
+ }));
250
+ const agentResult = await multiselect({
251
+ message: "Which AI agents do you use?",
252
+ options: agentChoices,
253
+ initialValues: AGENTS.filter((a) => a.selected).map((a) => a.value),
254
+ required: false
255
+ });
256
+ if (isCancel(agentResult)) {
257
+ cancel("Cancelled.");
258
+ process.exit(0);
259
+ }
260
+ const toRestore = agentResult;
261
+ const restored = [];
262
+ for (const agentValue of toRestore) {
263
+ const agent = AGENTS.find((a) => a.value === agentValue);
264
+ if (!agent) continue;
265
+ const destPath = join3(projectPath, agent.destFile);
266
+ mkdirSync2(dirname2(destPath), { recursive: true });
267
+ writeFileSync2(destPath, readFileSync2(join3(AGENT_TEMPLATES_DIR, agent.templateFile), "utf8"));
268
+ restored.push(agent.destFile);
269
+ }
270
+ if (toRestore.includes("claude")) {
271
+ const hookDest = join3(projectPath, ".claude", "settings.json");
272
+ if (!existsSync2(hookDest)) {
273
+ mkdirSync2(dirname2(hookDest), { recursive: true });
274
+ writeFileSync2(hookDest, readFileSync2(join3(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
275
+ restored.push(".claude/settings.json");
276
+ }
277
+ }
278
+ const configPath = join3(brainDir, "config.json");
279
+ if (!existsSync2(configPath)) {
280
+ const config = {
281
+ gitTracking: true,
282
+ autoSync: true,
283
+ agents: toRestore,
284
+ maxLogEntries: DEFAULT_MAX_LOG_ENTRIES
285
+ };
286
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
287
+ restored.push(".brain/config.json");
288
+ }
289
+ try {
290
+ registerProject(projectPath);
291
+ } catch {
292
+ }
293
+ console.log("");
294
+ for (const f of restored) console.log(` ${chalk2.green("\u2713")} ${f}`);
295
+ console.log("");
296
+ console.log(` ${chalk2.green("\u2713")} Agent files ready. Start a new session \u2014 your AI is already briefed.`);
297
+ console.log("");
298
+ process.exit(0);
299
+ }
300
+ } else {
301
+ if (opts.yes) {
302
+ console.log(` ${chalk2.red("\u2717")} Already initialized at this path.`);
303
+ console.log(` Run ${chalk2.cyan("mindlink config")} to change settings.`);
304
+ console.log("");
305
+ process.exit(1);
306
+ }
307
+ const action = await select({
308
+ message: ".brain/ already exists at this path. What would you like to do?",
309
+ options: [
310
+ { value: "config", label: "Change settings", hint: "mindlink config" },
311
+ { value: "status", label: "View current status", hint: "mindlink status" },
312
+ { value: "exit", label: "Nothing \u2014 exit", hint: "" }
313
+ ]
314
+ });
315
+ if (isCancel(action) || action === "exit") {
316
+ process.exit(0);
317
+ }
318
+ if (action === "status") {
319
+ try {
320
+ execSync("mindlink status", { cwd: projectPath, stdio: "inherit" });
321
+ } catch {
322
+ }
323
+ }
324
+ if (action === "config") {
325
+ console.log(` Run ${chalk2.cyan("mindlink config")} to change settings.`);
326
+ }
327
+ console.log("");
328
+ process.exit(0);
329
+ }
330
+ } else if (opts.yes) {
165
331
  console.log(` ${chalk2.red("\u2717")} Already initialized at this path.`);
166
332
  console.log(` Run ${chalk2.cyan("mindlink config")} to change settings.`);
167
333
  console.log("");
168
334
  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 {
335
+ } else if (!hasMemory) {
336
+ } else {
337
+ const action = await select({
338
+ message: ".brain/ already exists at this path. What would you like to do?",
339
+ options: [
340
+ { value: "config", label: "Change settings", hint: "mindlink config" },
341
+ { value: "status", label: "View current status", hint: "mindlink status" },
342
+ { value: "exit", label: "Nothing \u2014 exit", hint: "" }
343
+ ]
344
+ });
345
+ if (isCancel(action) || action === "exit") {
346
+ process.exit(0);
186
347
  }
348
+ if (action === "status") {
349
+ try {
350
+ execSync("mindlink status", { cwd: projectPath, stdio: "inherit" });
351
+ } catch {
352
+ }
353
+ }
354
+ if (action === "config") {
355
+ console.log(` Run ${chalk2.cyan("mindlink config")} to change settings.`);
356
+ }
357
+ console.log("");
358
+ process.exit(0);
187
359
  }
188
- if (action === "config") {
189
- console.log(` Run ${chalk2.cyan("mindlink config")} to change settings.`);
190
- }
191
- console.log("");
192
- process.exit(0);
193
360
  }
194
361
  intro(chalk2.bold("Initializing memory for this project:"));
195
362
  console.log(` ${chalk2.dim(projectPath)}`);
196
363
  console.log(` ${chalk2.dim("This creates a .brain/ folder scoped to this project only.")}`);
197
364
  console.log(` ${chalk2.dim("Run mindlink init once per project \u2014 never needs to be run again.")}`);
198
365
  console.log("");
366
+ if (process.platform === "win32") {
367
+ console.log(` ${chalk2.yellow("\u26A0")} ${chalk2.bold("Windows detected")}`);
368
+ console.log(` Claude Code hooks use bash and won't run on Windows.`);
369
+ console.log(` Memory enforcement (Stop hook, session timestamps) will be disabled.`);
370
+ console.log(` All other features work normally.`);
371
+ console.log("");
372
+ }
199
373
  let selectedAgents;
200
374
  if (opts.yes) {
201
375
  selectedAgents = AGENTS.filter((a) => a.selected).map((a) => a.value);
@@ -511,7 +685,7 @@ Examples:
511
685
  // src/commands/log.ts
512
686
  import { Command as Command3 } from "commander";
513
687
  import chalk4 from "chalk";
514
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "fs";
688
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
515
689
  import { join as join5, resolve as resolve3 } from "path";
516
690
  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
691
  Examples:
@@ -568,7 +742,7 @@ Examples:
568
742
  }
569
743
  console.log("");
570
744
  }
571
- const archiveFiles = readdirSync(brainDir).filter((f) => /^LOG-\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
745
+ const archiveFiles = readdirSync2(brainDir).filter((f) => /^LOG-\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
572
746
  if (archiveFiles.length > 0) {
573
747
  console.log(` ${chalk4.dim("\u2500".repeat(44))}`);
574
748
  console.log(` ${chalk4.dim("Like all human brains, MindLink forgets old sessions")}`);
@@ -582,7 +756,7 @@ Examples:
582
756
  // src/commands/clear.ts
583
757
  import { Command as Command4 } from "commander";
584
758
  import chalk5 from "chalk";
585
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
759
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, unlinkSync } from "fs";
586
760
  import { join as join6, resolve as resolve4 } from "path";
587
761
  var clearCommand = new Command4("clear").description("Reset SESSION.md for a fresh session start").option("-y, --yes", "Skip confirmation prompt").addHelpText("after", `
588
762
  What it does:
@@ -611,6 +785,8 @@ Examples:
611
785
  const templatePath = join6(BRAIN_TEMPLATES_DIR, "SESSION.md");
612
786
  const destPath = join6(brainDir, "SESSION.md");
613
787
  writeFileSync3(destPath, readFileSync5(templatePath, "utf8"));
788
+ const tsDest = join6(brainDir, ".session_ts");
789
+ if (existsSync5(tsDest)) unlinkSync(tsDest);
614
790
  } catch (err) {
615
791
  console.log(` ${chalk5.red("\u2717")} ${err instanceof Error ? err.message : String(err)}`);
616
792
  console.log("");
@@ -699,7 +875,7 @@ import {
699
875
  writeFileSync as writeFileSync5,
700
876
  appendFileSync as appendFileSync2,
701
877
  mkdirSync as mkdirSync3,
702
- unlinkSync
878
+ unlinkSync as unlinkSync2
703
879
  } from "fs";
704
880
  import { join as join8, resolve as resolve6, dirname as dirname3 } from "path";
705
881
  function readConfig(brainDir) {
@@ -739,7 +915,7 @@ function removeAgentFile(projectPath, agentValue) {
739
915
  if (!agent) return null;
740
916
  const destPath = join8(projectPath, agent.destFile);
741
917
  if (!existsSync7(destPath)) return null;
742
- unlinkSync(destPath);
918
+ unlinkSync2(destPath);
743
919
  return agent.destFile;
744
920
  }
745
921
  function addClaudeHook(projectPath) {
@@ -1017,13 +1193,14 @@ Examples:
1017
1193
  import { Command as Command8 } from "commander";
1018
1194
  import { select as select3, isCancel as isCancel4, cancel as cancel4, spinner as spinner2 } from "@clack/prompts";
1019
1195
  import chalk9 from "chalk";
1020
- import { execSync } from "child_process";
1196
+ import { execSync as execSync2 } from "child_process";
1021
1197
  import { join as join10, dirname as dirname4 } from "path";
1022
1198
  import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
1199
+ var REQUIRED_BRAIN_FILES = ["MEMORY.md", "SESSION.md", "SHARED.md", "LOG.md"];
1023
1200
  async function latestVersion() {
1024
1201
  try {
1025
1202
  const { default: https } = await import("https");
1026
- return new Promise((resolve13) => {
1203
+ return new Promise((resolve14) => {
1027
1204
  const req = https.get(
1028
1205
  "https://registry.npmjs.org/mindlink/latest",
1029
1206
  { headers: { "User-Agent": "mindlink-cli" } },
@@ -1035,17 +1212,17 @@ async function latestVersion() {
1035
1212
  res.on("end", () => {
1036
1213
  try {
1037
1214
  const parsed = JSON.parse(data);
1038
- resolve13(parsed.version ?? null);
1215
+ resolve14(parsed.version ?? null);
1039
1216
  } catch {
1040
- resolve13(null);
1217
+ resolve14(null);
1041
1218
  }
1042
1219
  });
1043
1220
  }
1044
1221
  );
1045
- req.on("error", () => resolve13(null));
1222
+ req.on("error", () => resolve14(null));
1046
1223
  req.setTimeout(8e3, () => {
1047
1224
  req.destroy();
1048
- resolve13(null);
1225
+ resolve14(null);
1049
1226
  });
1050
1227
  });
1051
1228
  } catch {
@@ -1117,7 +1294,7 @@ Examples:
1117
1294
  const s2 = spinner2();
1118
1295
  s2.start(`Installing mindlink@${latest}...`);
1119
1296
  try {
1120
- execSync(`npm install -g mindlink@${latest}`, { stdio: "pipe" });
1297
+ execSync2(`npm install -g mindlink@${latest}`, { stdio: "pipe" });
1121
1298
  s2.stop("Done.");
1122
1299
  console.log("");
1123
1300
  console.log(` ${chalk9.green("\u2713")} Updated to ${latest}.`);
@@ -1151,6 +1328,19 @@ Examples:
1151
1328
  }
1152
1329
  const agentValues = config.agents ?? AGENTS.filter((a) => a.selected).map((a) => a.value);
1153
1330
  const refreshed = [];
1331
+ for (const brainFile of REQUIRED_BRAIN_FILES) {
1332
+ const dest = join10(projectPath, BRAIN_DIR, brainFile);
1333
+ if (!existsSync9(dest)) {
1334
+ try {
1335
+ const template = join10(BRAIN_TEMPLATES_DIR, brainFile);
1336
+ if (existsSync9(template)) {
1337
+ writeFileSync6(dest, readFileSync9(template, "utf8"));
1338
+ refreshed.push(`.brain/${brainFile}`);
1339
+ }
1340
+ } catch {
1341
+ }
1342
+ }
1343
+ }
1154
1344
  for (const agentValue of agentValues) {
1155
1345
  const agent = AGENTS.find((a) => a.value === agentValue);
1156
1346
  if (!agent) continue;
@@ -1171,6 +1361,46 @@ Examples:
1171
1361
  } catch {
1172
1362
  }
1173
1363
  }
1364
+ const memoryPath = join10(projectPath, BRAIN_DIR, "MEMORY.md");
1365
+ if (existsSync9(memoryPath)) {
1366
+ try {
1367
+ let content = readFileSync9(memoryPath, "utf8");
1368
+ let memoryChanged = false;
1369
+ const migrations = [
1370
+ {
1371
+ // v1.1.5: Add ## User Profile section
1372
+ marker: "## User Profile",
1373
+ block: `## User Profile <!-- READ EVERY SESSION \u2014 personal facts about the user -->
1374
+
1375
+ <!-- Job, company, level, years of experience, immigration status -->
1376
+ <!-- Age, health, physical details -->
1377
+ <!-- Family, relationships, major life events -->
1378
+ <!-- Long-term goals: career, financial, personal -->
1379
+ <!-- Strong opinions, values, preferences -->
1380
+ <!-- Update in place \u2014 do not append; consolidate when it grows -->
1381
+
1382
+
1383
+ ---
1384
+
1385
+ `,
1386
+ before: "## Important Context"
1387
+ }
1388
+ // Future migrations go here — same pattern:
1389
+ // { marker: '## New Section', block: '## New Section\n\n<!-- ... -->\n\n\n---\n\n', before: '## Some Existing Section' },
1390
+ ];
1391
+ for (const m of migrations) {
1392
+ if (!content.includes(m.marker) && content.includes(m.before)) {
1393
+ content = content.replace(m.before, m.block + m.before);
1394
+ memoryChanged = true;
1395
+ }
1396
+ }
1397
+ if (memoryChanged) {
1398
+ writeFileSync6(memoryPath, content);
1399
+ refreshed.push(".brain/MEMORY.md");
1400
+ }
1401
+ } catch {
1402
+ }
1403
+ }
1174
1404
  console.log(` ${chalk9.bold(projectPath)}`);
1175
1405
  for (const f of refreshed) {
1176
1406
  console.log(` ${chalk9.green("\u2713")} ${f}`);
@@ -1301,7 +1531,7 @@ to get a full briefing on the current project state.
1301
1531
  import { Command as Command10 } from "commander";
1302
1532
  import { select as select4, isCancel as isCancel5, cancel as cancel5 } from "@clack/prompts";
1303
1533
  import chalk11 from "chalk";
1304
- import { existsSync as existsSync11, readFileSync as readFileSync11, rmSync, unlinkSync as unlinkSync2 } from "fs";
1534
+ import { existsSync as existsSync11, readFileSync as readFileSync11, rmSync, unlinkSync as unlinkSync3 } from "fs";
1305
1535
  import { join as join12, resolve as resolve9 } from "path";
1306
1536
  var uninstallCommand = new Command10("uninstall").description("Remove MindLink from the current project").option("-y, --yes", "Skip confirmation and remove all project files").addHelpText("after", `
1307
1537
  What gets removed:
@@ -1383,7 +1613,7 @@ Examples:
1383
1613
  const destPath = join12(projectPath, agent.destFile);
1384
1614
  if (existsSync11(destPath)) {
1385
1615
  try {
1386
- unlinkSync2(destPath);
1616
+ unlinkSync3(destPath);
1387
1617
  removed.push(agent.destFile);
1388
1618
  } catch (err) {
1389
1619
  errors.push(`${agent.destFile}: ${err instanceof Error ? err.message : String(err)}`);
@@ -1394,7 +1624,7 @@ Examples:
1394
1624
  const hookPath = join12(projectPath, ".claude", "settings.json");
1395
1625
  if (existsSync11(hookPath)) {
1396
1626
  try {
1397
- unlinkSync2(hookPath);
1627
+ unlinkSync3(hookPath);
1398
1628
  removed.push(".claude/settings.json");
1399
1629
  } catch {
1400
1630
  }
@@ -1412,7 +1642,7 @@ Examples:
1412
1642
  import { Command as Command11 } from "commander";
1413
1643
  import { text as text2, isCancel as isCancel6, cancel as cancel6 } from "@clack/prompts";
1414
1644
  import chalk12 from "chalk";
1415
- import { existsSync as existsSync12, readdirSync as readdirSync2 } from "fs";
1645
+ import { existsSync as existsSync12, readdirSync as readdirSync3 } from "fs";
1416
1646
  import { join as join13, resolve as resolve10, basename as basename3 } from "path";
1417
1647
  import AdmZip from "adm-zip";
1418
1648
  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", `
@@ -1459,8 +1689,7 @@ Examples:
1459
1689
  const answer = await text2({
1460
1690
  message: "Where should the zip be saved?",
1461
1691
  placeholder: projectPath,
1462
- initialValue: projectPath,
1463
- hint: `default filename: ${defaultFilename}`
1692
+ initialValue: projectPath
1464
1693
  });
1465
1694
  if (isCancel6(answer)) {
1466
1695
  cancel6("Cancelled.");
@@ -1488,7 +1717,7 @@ Examples:
1488
1717
  skipped.push(`.brain/${file}`);
1489
1718
  }
1490
1719
  }
1491
- const archiveFiles = readdirSync2(brainDir).filter((f) => /^LOG-\d{4}-\d{2}-\d{2}\.md$/.test(f));
1720
+ const archiveFiles = readdirSync3(brainDir).filter((f) => /^LOG-\d{4}-\d{2}-\d{2}\.md$/.test(f));
1492
1721
  for (const file of archiveFiles) {
1493
1722
  zip.addLocalFile(join13(brainDir, file), ".brain");
1494
1723
  included.push(`.brain/${file}`);
@@ -1635,7 +1864,7 @@ Examples:
1635
1864
  // src/commands/doctor.ts
1636
1865
  import { Command as Command13 } from "commander";
1637
1866
  import chalk14 from "chalk";
1638
- import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync3, readdirSync as readdirSync3 } from "fs";
1867
+ import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync3, readdirSync as readdirSync4 } from "fs";
1639
1868
  import { join as join15, resolve as resolve12 } from "path";
1640
1869
  var CORE_LINE_LIMIT = 50;
1641
1870
  var CORE_WARN_THRESHOLD = 40;
@@ -1767,7 +1996,7 @@ Examples:
1767
1996
  checks.push(ok(`LOG.md \u2014 ${entryCount} sessions logged (last: ${newestHeading}, going back to: ${oldestHeading})`));
1768
1997
  }
1769
1998
  }
1770
- const archives = readdirSync3(brainDir).filter((f) => /^LOG-\d{4}-\d{2}\.md$/.test(f));
1999
+ const archives = readdirSync4(brainDir).filter((f) => /^LOG-\d{4}-\d{2}\.md$/.test(f));
1771
2000
  if (archives.length > 0) {
1772
2001
  checks.push(info(`${archives.length} archive file${archives.length !== 1 ? "s" : ""} \u2014 old sessions are stored in LOG-*.md, not gone`));
1773
2002
  }
@@ -1850,8 +2079,167 @@ var versionCommand = new Command14("version").description("Show the current Mind
1850
2079
  console.log("");
1851
2080
  });
1852
2081
 
2082
+ // src/commands/diff.ts
2083
+ import { Command as Command15 } from "commander";
2084
+ import chalk16 from "chalk";
2085
+ import { existsSync as existsSync15, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
2086
+ import { join as join16, resolve as resolve13 } from "path";
2087
+ import { execSync as execSync3 } from "child_process";
2088
+ var BRAIN_FILES3 = ["MEMORY.md", "SESSION.md", "LOG.md", "SHARED.md"];
2089
+ function relativeTime2(ms) {
2090
+ const secs = Math.floor(ms / 1e3);
2091
+ if (secs < 60) return `${secs}s ago`;
2092
+ const mins = Math.floor(secs / 60);
2093
+ if (mins < 60) return `${mins}m ago`;
2094
+ const hours = Math.floor(mins / 60);
2095
+ if (hours < 24) return `${hours}h ago`;
2096
+ return `${Math.floor(hours / 24)}d ago`;
2097
+ }
2098
+ function getGitDiff(filePath, since) {
2099
+ try {
2100
+ const dir = resolve13(filePath, "..");
2101
+ execSync3("git rev-parse --is-inside-work-tree", { cwd: dir, stdio: "pipe" });
2102
+ const ref = since || "HEAD~1";
2103
+ const diff = execSync3(`git diff ${ref} -- "${filePath}"`, {
2104
+ cwd: dir,
2105
+ encoding: "utf8",
2106
+ stdio: ["pipe", "pipe", "pipe"]
2107
+ });
2108
+ return diff || null;
2109
+ } catch {
2110
+ return null;
2111
+ }
2112
+ }
2113
+ function parseDiffSummary(diff) {
2114
+ const added = [];
2115
+ const removed = [];
2116
+ for (const line of diff.split("\n")) {
2117
+ if (line.startsWith("+") && !line.startsWith("+++")) {
2118
+ const t = line.slice(1).trim();
2119
+ if (t && !t.startsWith("<!--") && !t.startsWith("#") && t !== "---") added.push(t);
2120
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2121
+ const t = line.slice(1).trim();
2122
+ if (t && !t.startsWith("<!--") && !t.startsWith("#") && t !== "---") removed.push(t);
2123
+ }
2124
+ }
2125
+ return { added, removed };
2126
+ }
2127
+ 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", `
2128
+ What it does:
2129
+ Shows what changed in each .brain/ file since your last session.
2130
+ Uses git diff when .brain/ is tracked; falls back to file modification times.
2131
+
2132
+ Examples:
2133
+ mindlink diff
2134
+ mindlink diff --since HEAD~3
2135
+ mindlink diff --since "2026-04-10"
2136
+ `).action((opts) => {
2137
+ const projectPath = resolve13(process.cwd());
2138
+ const brainDir = join16(projectPath, BRAIN_DIR);
2139
+ if (!existsSync15(brainDir)) {
2140
+ console.log("");
2141
+ console.log(` ${chalk16.red("\u2717")} No .brain/ found in this directory.`);
2142
+ console.log(` Run ${chalk16.cyan("mindlink init")} to get started.`);
2143
+ console.log("");
2144
+ process.exit(1);
2145
+ }
2146
+ const results = {};
2147
+ const sessionTsPath = join16(brainDir, ".session_ts");
2148
+ const sessionTs = existsSync15(sessionTsPath) ? parseInt(readFileSync13(sessionTsPath, "utf8").trim(), 10) * 1e3 : null;
2149
+ const now = Date.now();
2150
+ const isGitTracked = (() => {
2151
+ try {
2152
+ execSync3("git rev-parse --is-inside-work-tree", { cwd: projectPath, stdio: "pipe" });
2153
+ const tracked = execSync3("git ls-files --error-unmatch .brain/MEMORY.md", {
2154
+ cwd: projectPath,
2155
+ stdio: "pipe"
2156
+ });
2157
+ return true;
2158
+ } catch {
2159
+ return false;
2160
+ }
2161
+ })();
2162
+ for (const file of BRAIN_FILES3) {
2163
+ const filePath = join16(brainDir, file);
2164
+ if (!existsSync15(filePath)) {
2165
+ results[file] = { exists: false };
2166
+ continue;
2167
+ }
2168
+ const stat = statSync4(filePath);
2169
+ const mtime = stat.mtimeMs;
2170
+ const content = readFileSync13(filePath, "utf8");
2171
+ const sizeLines = content.split("\n").length;
2172
+ let diff = null;
2173
+ let added = [];
2174
+ let removed = [];
2175
+ if (isGitTracked) {
2176
+ diff = getGitDiff(filePath, opts.since);
2177
+ if (diff) {
2178
+ const summary = parseDiffSummary(diff);
2179
+ added = summary.added.slice(0, 5);
2180
+ removed = summary.removed.slice(0, 5);
2181
+ }
2182
+ }
2183
+ results[file] = { exists: true, mtime, diff, added, removed, sizeLines };
2184
+ }
2185
+ if (opts.json) {
2186
+ console.log(JSON.stringify(results, null, 2));
2187
+ return;
2188
+ }
2189
+ console.log("");
2190
+ console.log(` ${chalk16.bold(".brain/ changes")}`);
2191
+ if (sessionTs) {
2192
+ console.log(` ${chalk16.dim(`Session started ${relativeTime2(now - sessionTs)}`)}`);
2193
+ }
2194
+ if (isGitTracked) {
2195
+ const sinceRef = opts.since || "HEAD~1";
2196
+ console.log(` ${chalk16.dim(`Git diff against: ${sinceRef}`)}`);
2197
+ } else {
2198
+ console.log(` ${chalk16.dim(".brain/ is not git-tracked \u2014 showing modification times only")}`);
2199
+ }
2200
+ console.log("");
2201
+ for (const file of BRAIN_FILES3) {
2202
+ const r = results[file];
2203
+ if (!r.exists) {
2204
+ console.log(` ${chalk16.dim("\u2014")} ${file} ${chalk16.dim("(not found)")}`);
2205
+ continue;
2206
+ }
2207
+ const age = r.mtime ? relativeTime2(now - r.mtime) : "";
2208
+ const changedThisSession = sessionTs && r.mtime ? r.mtime > sessionTs : false;
2209
+ const indicator = changedThisSession ? chalk16.green("\u25CF") : chalk16.dim("\u25CB");
2210
+ const label = changedThisSession ? chalk16.green(file) : chalk16.dim(file);
2211
+ console.log(` ${indicator} ${label} ${chalk16.dim(`${r.sizeLines} lines \xB7 modified ${age}`)}`);
2212
+ if (isGitTracked && r.diff) {
2213
+ for (const line of (r.added ?? []).slice(0, 3)) {
2214
+ console.log(` ${chalk16.green("+")} ${line.length > 80 ? line.slice(0, 80) + "\u2026" : line}`);
2215
+ }
2216
+ for (const line of (r.removed ?? []).slice(0, 3)) {
2217
+ console.log(` ${chalk16.red("-")} ${line.length > 80 ? line.slice(0, 80) + "\u2026" : line}`);
2218
+ }
2219
+ } else if (!isGitTracked && changedThisSession) {
2220
+ console.log(` ${chalk16.dim("(content diff unavailable \u2014 commit .brain/ to git for line-level diff)")}`);
2221
+ } else if (isGitTracked && !r.diff) {
2222
+ console.log(` ${chalk16.dim("no changes since last commit")}`);
2223
+ }
2224
+ }
2225
+ console.log("");
2226
+ const changedCount = BRAIN_FILES3.filter((f) => {
2227
+ const r = results[f];
2228
+ return r.exists && sessionTs && r.mtime && r.mtime > sessionTs;
2229
+ }).length;
2230
+ if (changedCount === 0) {
2231
+ console.log(` ${chalk16.dim("No .brain/ files were modified this session.")}`);
2232
+ if (sessionTs) {
2233
+ console.log(` ${chalk16.dim("If the session just started, this is expected.")}`);
2234
+ }
2235
+ } else {
2236
+ console.log(` ${chalk16.green("\u2713")} ${changedCount} file${changedCount !== 1 ? "s" : ""} updated this session.`);
2237
+ }
2238
+ console.log("");
2239
+ });
2240
+
1853
2241
  // src/cli.ts
1854
- var program = new Command15();
2242
+ var program = new Command16();
1855
2243
  program.name("mindlink").description("Give your AI a brain.").version(VERSION, "-v, --version");
1856
2244
  program.addCommand(initCommand);
1857
2245
  program.addCommand(statusCommand);
@@ -1867,9 +2255,10 @@ program.addCommand(exportCommand);
1867
2255
  program.addCommand(importCommand);
1868
2256
  program.addCommand(doctorCommand);
1869
2257
  program.addCommand(versionCommand);
2258
+ program.addCommand(diffCommand);
1870
2259
  program.on("command:*", (operands) => {
1871
2260
  const unknown = operands[0];
1872
- const known = ["init", "status", "log", "clear", "reset", "config", "sync", "update", "summary", "uninstall", "export", "import", "doctor", "version"];
2261
+ const known = ["init", "status", "log", "clear", "reset", "config", "sync", "update", "summary", "uninstall", "export", "import", "doctor", "version", "diff"];
1873
2262
  function levenshtein(a, b) {
1874
2263
  const m = a.length, n = b.length;
1875
2264
  const dp = Array.from(
@@ -1885,11 +2274,11 @@ program.on("command:*", (operands) => {
1885
2274
  }
1886
2275
  const closest = known.map((cmd) => ({ cmd, dist: levenshtein(unknown, cmd) })).sort((a, b) => a.dist - b.dist)[0];
1887
2276
  console.log("");
1888
- console.log(` ${chalk16.red("\u2717")} Unknown command: ${chalk16.bold(unknown)}`);
2277
+ console.log(` ${chalk17.red("\u2717")} Unknown command: ${chalk17.bold(unknown)}`);
1889
2278
  if (closest && closest.dist <= 3) {
1890
- console.log(` Did you mean ${chalk16.cyan("mindlink " + closest.cmd)}?`);
2279
+ console.log(` Did you mean ${chalk17.cyan("mindlink " + closest.cmd)}?`);
1891
2280
  }
1892
- console.log(` Run ${chalk16.cyan("mindlink --help")} to see all commands.`);
2281
+ console.log(` Run ${chalk17.cyan("mindlink --help")} to see all commands.`);
1893
2282
  console.log("");
1894
2283
  process.exit(1);
1895
2284
  });