mindlink 1.1.5 → 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/README.md +26 -6
- package/commands/diff.md +115 -0
- package/commands/index.md +1 -0
- package/commands/init.md +26 -1
- package/commands/update.md +23 -5
- package/dist/cli.js +418 -55
- package/dist/cli.js.map +1 -1
- package/dist/templates/agents/.clinerules +2 -0
- package/dist/templates/agents/.rules +75 -0
- package/dist/templates/agents/.windsurfrules +2 -0
- package/dist/templates/agents/AGENTS.md +2 -0
- package/dist/templates/agents/CLAUDE.md +2 -0
- package/dist/templates/agents/CONVENTIONS.md +2 -0
- package/dist/templates/agents/CURSOR.md +2 -0
- package/dist/templates/agents/GEMINI.md +2 -0
- package/dist/templates/agents/continue-rules.md +80 -0
- package/dist/templates/agents/copilot-instructions.md +2 -0
- package/dist/templates/agents/kiro-steering.md +79 -0
- package/dist/templates/agents/trae-rules.md +80 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as
|
|
5
|
-
import
|
|
4
|
+
import { Command as Command16 } from "commander";
|
|
5
|
+
import chalk17 from "chalk";
|
|
6
6
|
|
|
7
7
|
// src/utils/version.ts
|
|
8
8
|
var VERSION = "1.1.5";
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 =
|
|
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")}`);
|
|
@@ -1019,13 +1193,14 @@ Examples:
|
|
|
1019
1193
|
import { Command as Command8 } from "commander";
|
|
1020
1194
|
import { select as select3, isCancel as isCancel4, cancel as cancel4, spinner as spinner2 } from "@clack/prompts";
|
|
1021
1195
|
import chalk9 from "chalk";
|
|
1022
|
-
import { execSync } from "child_process";
|
|
1196
|
+
import { execSync as execSync2 } from "child_process";
|
|
1023
1197
|
import { join as join10, dirname as dirname4 } from "path";
|
|
1024
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"];
|
|
1025
1200
|
async function latestVersion() {
|
|
1026
1201
|
try {
|
|
1027
1202
|
const { default: https } = await import("https");
|
|
1028
|
-
return new Promise((
|
|
1203
|
+
return new Promise((resolve14) => {
|
|
1029
1204
|
const req = https.get(
|
|
1030
1205
|
"https://registry.npmjs.org/mindlink/latest",
|
|
1031
1206
|
{ headers: { "User-Agent": "mindlink-cli" } },
|
|
@@ -1037,17 +1212,17 @@ async function latestVersion() {
|
|
|
1037
1212
|
res.on("end", () => {
|
|
1038
1213
|
try {
|
|
1039
1214
|
const parsed = JSON.parse(data);
|
|
1040
|
-
|
|
1215
|
+
resolve14(parsed.version ?? null);
|
|
1041
1216
|
} catch {
|
|
1042
|
-
|
|
1217
|
+
resolve14(null);
|
|
1043
1218
|
}
|
|
1044
1219
|
});
|
|
1045
1220
|
}
|
|
1046
1221
|
);
|
|
1047
|
-
req.on("error", () =>
|
|
1222
|
+
req.on("error", () => resolve14(null));
|
|
1048
1223
|
req.setTimeout(8e3, () => {
|
|
1049
1224
|
req.destroy();
|
|
1050
|
-
|
|
1225
|
+
resolve14(null);
|
|
1051
1226
|
});
|
|
1052
1227
|
});
|
|
1053
1228
|
} catch {
|
|
@@ -1119,7 +1294,7 @@ Examples:
|
|
|
1119
1294
|
const s2 = spinner2();
|
|
1120
1295
|
s2.start(`Installing mindlink@${latest}...`);
|
|
1121
1296
|
try {
|
|
1122
|
-
|
|
1297
|
+
execSync2(`npm install -g mindlink@${latest}`, { stdio: "pipe" });
|
|
1123
1298
|
s2.stop("Done.");
|
|
1124
1299
|
console.log("");
|
|
1125
1300
|
console.log(` ${chalk9.green("\u2713")} Updated to ${latest}.`);
|
|
@@ -1153,6 +1328,19 @@ Examples:
|
|
|
1153
1328
|
}
|
|
1154
1329
|
const agentValues = config.agents ?? AGENTS.filter((a) => a.selected).map((a) => a.value);
|
|
1155
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
|
+
}
|
|
1156
1344
|
for (const agentValue of agentValues) {
|
|
1157
1345
|
const agent = AGENTS.find((a) => a.value === agentValue);
|
|
1158
1346
|
if (!agent) continue;
|
|
@@ -1176,9 +1364,13 @@ Examples:
|
|
|
1176
1364
|
const memoryPath = join10(projectPath, BRAIN_DIR, "MEMORY.md");
|
|
1177
1365
|
if (existsSync9(memoryPath)) {
|
|
1178
1366
|
try {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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 -->
|
|
1182
1374
|
|
|
1183
1375
|
<!-- Job, company, level, years of experience, immigration status -->
|
|
1184
1376
|
<!-- Age, health, physical details -->
|
|
@@ -1190,8 +1382,20 @@ Examples:
|
|
|
1190
1382
|
|
|
1191
1383
|
---
|
|
1192
1384
|
|
|
1193
|
-
|
|
1194
|
-
|
|
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);
|
|
1195
1399
|
refreshed.push(".brain/MEMORY.md");
|
|
1196
1400
|
}
|
|
1197
1401
|
} catch {
|
|
@@ -1438,7 +1642,7 @@ Examples:
|
|
|
1438
1642
|
import { Command as Command11 } from "commander";
|
|
1439
1643
|
import { text as text2, isCancel as isCancel6, cancel as cancel6 } from "@clack/prompts";
|
|
1440
1644
|
import chalk12 from "chalk";
|
|
1441
|
-
import { existsSync as existsSync12, readdirSync as
|
|
1645
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3 } from "fs";
|
|
1442
1646
|
import { join as join13, resolve as resolve10, basename as basename3 } from "path";
|
|
1443
1647
|
import AdmZip from "adm-zip";
|
|
1444
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", `
|
|
@@ -1485,8 +1689,7 @@ Examples:
|
|
|
1485
1689
|
const answer = await text2({
|
|
1486
1690
|
message: "Where should the zip be saved?",
|
|
1487
1691
|
placeholder: projectPath,
|
|
1488
|
-
initialValue: projectPath
|
|
1489
|
-
hint: `default filename: ${defaultFilename}`
|
|
1692
|
+
initialValue: projectPath
|
|
1490
1693
|
});
|
|
1491
1694
|
if (isCancel6(answer)) {
|
|
1492
1695
|
cancel6("Cancelled.");
|
|
@@ -1514,7 +1717,7 @@ Examples:
|
|
|
1514
1717
|
skipped.push(`.brain/${file}`);
|
|
1515
1718
|
}
|
|
1516
1719
|
}
|
|
1517
|
-
const archiveFiles =
|
|
1720
|
+
const archiveFiles = readdirSync3(brainDir).filter((f) => /^LOG-\d{4}-\d{2}-\d{2}\.md$/.test(f));
|
|
1518
1721
|
for (const file of archiveFiles) {
|
|
1519
1722
|
zip.addLocalFile(join13(brainDir, file), ".brain");
|
|
1520
1723
|
included.push(`.brain/${file}`);
|
|
@@ -1661,7 +1864,7 @@ Examples:
|
|
|
1661
1864
|
// src/commands/doctor.ts
|
|
1662
1865
|
import { Command as Command13 } from "commander";
|
|
1663
1866
|
import chalk14 from "chalk";
|
|
1664
|
-
import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync3, readdirSync as
|
|
1867
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12, statSync as statSync3, readdirSync as readdirSync4 } from "fs";
|
|
1665
1868
|
import { join as join15, resolve as resolve12 } from "path";
|
|
1666
1869
|
var CORE_LINE_LIMIT = 50;
|
|
1667
1870
|
var CORE_WARN_THRESHOLD = 40;
|
|
@@ -1793,7 +1996,7 @@ Examples:
|
|
|
1793
1996
|
checks.push(ok(`LOG.md \u2014 ${entryCount} sessions logged (last: ${newestHeading}, going back to: ${oldestHeading})`));
|
|
1794
1997
|
}
|
|
1795
1998
|
}
|
|
1796
|
-
const archives =
|
|
1999
|
+
const archives = readdirSync4(brainDir).filter((f) => /^LOG-\d{4}-\d{2}\.md$/.test(f));
|
|
1797
2000
|
if (archives.length > 0) {
|
|
1798
2001
|
checks.push(info(`${archives.length} archive file${archives.length !== 1 ? "s" : ""} \u2014 old sessions are stored in LOG-*.md, not gone`));
|
|
1799
2002
|
}
|
|
@@ -1876,8 +2079,167 @@ var versionCommand = new Command14("version").description("Show the current Mind
|
|
|
1876
2079
|
console.log("");
|
|
1877
2080
|
});
|
|
1878
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
|
+
|
|
1879
2241
|
// src/cli.ts
|
|
1880
|
-
var program = new
|
|
2242
|
+
var program = new Command16();
|
|
1881
2243
|
program.name("mindlink").description("Give your AI a brain.").version(VERSION, "-v, --version");
|
|
1882
2244
|
program.addCommand(initCommand);
|
|
1883
2245
|
program.addCommand(statusCommand);
|
|
@@ -1893,9 +2255,10 @@ program.addCommand(exportCommand);
|
|
|
1893
2255
|
program.addCommand(importCommand);
|
|
1894
2256
|
program.addCommand(doctorCommand);
|
|
1895
2257
|
program.addCommand(versionCommand);
|
|
2258
|
+
program.addCommand(diffCommand);
|
|
1896
2259
|
program.on("command:*", (operands) => {
|
|
1897
2260
|
const unknown = operands[0];
|
|
1898
|
-
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"];
|
|
1899
2262
|
function levenshtein(a, b) {
|
|
1900
2263
|
const m = a.length, n = b.length;
|
|
1901
2264
|
const dp = Array.from(
|
|
@@ -1911,11 +2274,11 @@ program.on("command:*", (operands) => {
|
|
|
1911
2274
|
}
|
|
1912
2275
|
const closest = known.map((cmd) => ({ cmd, dist: levenshtein(unknown, cmd) })).sort((a, b) => a.dist - b.dist)[0];
|
|
1913
2276
|
console.log("");
|
|
1914
|
-
console.log(` ${
|
|
2277
|
+
console.log(` ${chalk17.red("\u2717")} Unknown command: ${chalk17.bold(unknown)}`);
|
|
1915
2278
|
if (closest && closest.dist <= 3) {
|
|
1916
|
-
console.log(` Did you mean ${
|
|
2279
|
+
console.log(` Did you mean ${chalk17.cyan("mindlink " + closest.cmd)}?`);
|
|
1917
2280
|
}
|
|
1918
|
-
console.log(` Run ${
|
|
2281
|
+
console.log(` Run ${chalk17.cyan("mindlink --help")} to see all commands.`);
|
|
1919
2282
|
console.log("");
|
|
1920
2283
|
process.exit(1);
|
|
1921
2284
|
});
|