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/README.md +37 -7
- package/commands/diff.md +115 -0
- package/commands/index.md +12 -0
- package/commands/init.md +26 -1
- package/commands/mcp.md +126 -0
- package/commands/profile.md +104 -0
- package/commands/prune.md +117 -0
- package/commands/update.md +23 -5
- package/commands/verify.md +122 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/cli.js +1566 -118
- package/dist/cli.js.map +1 -1
- package/dist/templates/agents/.clinerules +13 -2
- package/dist/templates/agents/.rules +84 -0
- package/dist/templates/agents/.windsurfrules +17 -6
- package/dist/templates/agents/AGENTS.md +13 -2
- package/dist/templates/agents/CLAUDE.md +18 -7
- package/dist/templates/agents/CONVENTIONS.md +13 -2
- package/dist/templates/agents/CURSOR.md +17 -6
- package/dist/templates/agents/GEMINI.md +13 -2
- package/dist/templates/agents/continue-rules.md +89 -0
- package/dist/templates/agents/copilot-instructions.md +17 -6
- package/dist/templates/agents/kiro-steering.md +88 -0
- package/dist/templates/agents/trae-rules.md +89 -0
- package/dist/templates/hooks/claude-settings.json +6 -0
- package/dist/zod-FDOV7Y2P.js +14016 -0
- package/dist/zod-FDOV7Y2P.js.map +1 -0
- package/package.json +2 -1
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
|
|
5
|
-
import
|
|
5
|
+
import { Command as Command20 } from "commander";
|
|
6
|
+
import chalk20 from "chalk";
|
|
6
7
|
|
|
7
8
|
// src/utils/version.ts
|
|
8
|
-
var VERSION = "
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
1400
|
+
resolve17(parsed.version ?? null);
|
|
1041
1401
|
} catch {
|
|
1042
|
-
|
|
1402
|
+
resolve17(null);
|
|
1043
1403
|
}
|
|
1044
1404
|
});
|
|
1045
1405
|
}
|
|
1046
1406
|
);
|
|
1047
|
-
req.on("error", () =>
|
|
1407
|
+
req.on("error", () => resolve17(null));
|
|
1048
1408
|
req.setTimeout(8e3, () => {
|
|
1049
1409
|
req.destroy();
|
|
1050
|
-
|
|
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
|
|
1073
|
-
if (!
|
|
1433
|
+
const latest = await latestVersion();
|
|
1434
|
+
if (!latest) {
|
|
1074
1435
|
console.log(JSON.stringify({ current, latest: null, upToDate: null }));
|
|
1075
|
-
|
|
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
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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(` ${
|
|
3362
|
+
console.log(` ${chalk20.red("\u2717")} Unknown command: ${chalk20.bold(unknown)}`);
|
|
1915
3363
|
if (closest && closest.dist <= 3) {
|
|
1916
|
-
console.log(` Did you mean ${
|
|
3364
|
+
console.log(` Did you mean ${chalk20.cyan("mindlink " + closest.cmd)}?`);
|
|
1917
3365
|
}
|
|
1918
|
-
console.log(` Run ${
|
|
3366
|
+
console.log(` Run ${chalk20.cyan("mindlink --help")} to see all commands.`);
|
|
1919
3367
|
console.log("");
|
|
1920
3368
|
process.exit(1);
|
|
1921
3369
|
});
|