mindlink 1.2.0 → 2.0.1
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 +22 -12
- package/commands/index.md +11 -0
- package/commands/mcp.md +126 -0
- package/commands/profile.md +104 -0
- package/commands/prune.md +117 -0
- 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 +1191 -77
- package/dist/cli.js.map +1 -1
- package/dist/templates/agents/.clinerules +11 -2
- package/dist/templates/agents/.rules +11 -2
- package/dist/templates/agents/.windsurfrules +15 -6
- package/dist/templates/agents/AGENTS.md +11 -2
- package/dist/templates/agents/CLAUDE.md +16 -7
- package/dist/templates/agents/CONVENTIONS.md +11 -2
- package/dist/templates/agents/CURSOR.md +15 -6
- package/dist/templates/agents/GEMINI.md +11 -2
- package/dist/templates/agents/continue-rules.md +15 -6
- package/dist/templates/agents/copilot-instructions.md +15 -6
- package/dist/templates/agents/kiro-steering.md +15 -6
- package/dist/templates/agents/trae-rules.md +11 -2
- 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.1";
|
|
9
10
|
|
|
10
11
|
// src/commands/init.ts
|
|
11
12
|
import { Command } from "commander";
|
|
@@ -33,6 +34,7 @@ import { join as join3, resolve, dirname as dirname2, basename } from "path";
|
|
|
33
34
|
// src/utils/paths.ts
|
|
34
35
|
import { fileURLToPath } from "url";
|
|
35
36
|
import { dirname, join } from "path";
|
|
37
|
+
import { homedir } from "os";
|
|
36
38
|
var __filename = fileURLToPath(import.meta.url);
|
|
37
39
|
var __dirname = dirname(__filename);
|
|
38
40
|
var TEMPLATES_DIR = join(__dirname, "templates");
|
|
@@ -40,6 +42,9 @@ var BRAIN_TEMPLATES_DIR = join(TEMPLATES_DIR, "brain");
|
|
|
40
42
|
var AGENT_TEMPLATES_DIR = join(TEMPLATES_DIR, "agents");
|
|
41
43
|
var HOOKS_TEMPLATES_DIR = join(TEMPLATES_DIR, "hooks");
|
|
42
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");
|
|
43
48
|
|
|
44
49
|
// src/utils/banner.ts
|
|
45
50
|
import chalk from "chalk";
|
|
@@ -70,8 +75,8 @@ var AGENTS = [
|
|
|
70
75
|
// src/utils/registry.ts
|
|
71
76
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
72
77
|
import { join as join2 } from "path";
|
|
73
|
-
import { homedir } from "os";
|
|
74
|
-
var REGISTRY_DIR = join2(
|
|
78
|
+
import { homedir as homedir2 } from "os";
|
|
79
|
+
var REGISTRY_DIR = join2(homedir2(), ".mindlink");
|
|
75
80
|
var REGISTRY_PATH = join2(REGISTRY_DIR, "projects.json");
|
|
76
81
|
function load() {
|
|
77
82
|
try {
|
|
@@ -100,6 +105,65 @@ function pruneRegistry(isValid) {
|
|
|
100
105
|
if (pruned.length !== paths.length) save(pruned);
|
|
101
106
|
}
|
|
102
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
|
+
|
|
103
167
|
// src/commands/init.ts
|
|
104
168
|
function detectProjectInfo(projectPath) {
|
|
105
169
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
@@ -176,6 +240,41 @@ function memoryHasRealContent(memoryPath) {
|
|
|
176
240
|
return false;
|
|
177
241
|
}
|
|
178
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");
|
|
277
|
+
}
|
|
179
278
|
function buildMemoryMd(templateContent, info2) {
|
|
180
279
|
let content = templateContent;
|
|
181
280
|
const whatLine = info2.description ? `**${info2.name}** \u2014 ${info2.description}` : `**${info2.name}**`;
|
|
@@ -271,9 +370,20 @@ Examples:
|
|
|
271
370
|
const hookDest = join3(projectPath, ".claude", "settings.json");
|
|
272
371
|
if (!existsSync2(hookDest)) {
|
|
273
372
|
mkdirSync2(dirname2(hookDest), { recursive: true });
|
|
274
|
-
|
|
373
|
+
const settings = JSON.parse(readFileSync2(join3(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
|
|
374
|
+
writeFileSync2(hookDest, JSON.stringify(settings, null, 2));
|
|
275
375
|
restored.push(".claude/settings.json");
|
|
276
376
|
}
|
|
377
|
+
const mcpJsonDest = join3(projectPath, ".mcp.json");
|
|
378
|
+
if (!existsSync2(mcpJsonDest)) {
|
|
379
|
+
const mcpJson = {
|
|
380
|
+
mcpServers: {
|
|
381
|
+
mindlink: { type: "stdio", command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
writeFileSync2(mcpJsonDest, JSON.stringify(mcpJson, null, 2));
|
|
385
|
+
restored.push(".mcp.json");
|
|
386
|
+
}
|
|
277
387
|
}
|
|
278
388
|
const configPath = join3(brainDir, "config.json");
|
|
279
389
|
if (!existsSync2(configPath)) {
|
|
@@ -452,6 +562,18 @@ Examples:
|
|
|
452
562
|
writeFileSync2(dest, content);
|
|
453
563
|
created.push(`${file.label.padEnd(32)} ${chalk2.dim(file.desc)}`);
|
|
454
564
|
}
|
|
565
|
+
const memoryDest = join3(brainDir, "MEMORY.md");
|
|
566
|
+
if (existsSync2(GLOBAL_USER_PROFILE_PATH)) {
|
|
567
|
+
const profileContent = readFileSync2(GLOBAL_USER_PROFILE_PATH, "utf8");
|
|
568
|
+
if (sectionHasRealContent(profileContent, "MindLink \u2014 Global User Profile") || profileContent.split("\n").some((l) => {
|
|
569
|
+
const t = l.trim();
|
|
570
|
+
return t.length > 0 && !t.startsWith("#") && !t.startsWith(">") && !t.startsWith("<!--");
|
|
571
|
+
})) {
|
|
572
|
+
const injected = injectUserProfile(readFileSync2(memoryDest, "utf8"), profileContent);
|
|
573
|
+
writeFileSync2(memoryDest, injected);
|
|
574
|
+
created.push(`User Profile imported from ~/.mindlink/USER.md`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
455
577
|
for (const agentValue of selectedAgents) {
|
|
456
578
|
const agent = AGENTS.find((a) => a.value === agentValue);
|
|
457
579
|
if (!agent) continue;
|
|
@@ -464,8 +586,77 @@ Examples:
|
|
|
464
586
|
const hookDest = join3(projectPath, ".claude", "settings.json");
|
|
465
587
|
if (!existsSync2(hookDest)) {
|
|
466
588
|
mkdirSync2(dirname2(hookDest), { recursive: true });
|
|
467
|
-
|
|
468
|
-
|
|
589
|
+
const settings = JSON.parse(readFileSync2(join3(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
|
|
590
|
+
writeFileSync2(hookDest, JSON.stringify(settings, null, 2));
|
|
591
|
+
created.push(`.claude/settings.json${" ".repeat(14)} ${chalk2.dim("Claude Code hooks")}`);
|
|
592
|
+
}
|
|
593
|
+
const mcpJsonDest = join3(projectPath, ".mcp.json");
|
|
594
|
+
if (!existsSync2(mcpJsonDest)) {
|
|
595
|
+
const mcpJson = {
|
|
596
|
+
mcpServers: {
|
|
597
|
+
mindlink: { type: "stdio", command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
writeFileSync2(mcpJsonDest, JSON.stringify(mcpJson, null, 2));
|
|
601
|
+
created.push(`.mcp.json${" ".repeat(24)} ${chalk2.dim("Claude Code MCP server")}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (selectedAgents.includes("cursor")) {
|
|
605
|
+
const cursorMcpDest = join3(projectPath, ".cursor", "mcp.json");
|
|
606
|
+
if (!existsSync2(cursorMcpDest)) {
|
|
607
|
+
mkdirSync2(join3(projectPath, ".cursor"), { recursive: true });
|
|
608
|
+
const cursorMcp = { mcpServers: { mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } } } };
|
|
609
|
+
writeFileSync2(cursorMcpDest, JSON.stringify(cursorMcp, null, 2));
|
|
610
|
+
created.push(`.cursor/mcp.json${" ".repeat(20)} ${chalk2.dim("Cursor MCP server")}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (selectedAgents.includes("continue")) {
|
|
614
|
+
const continueMcpDest = join3(projectPath, ".continue", "mcpServers", "mindlink.json");
|
|
615
|
+
if (!existsSync2(continueMcpDest)) {
|
|
616
|
+
mkdirSync2(join3(projectPath, ".continue", "mcpServers"), { recursive: true });
|
|
617
|
+
const continueMcp = { mcpServers: { mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } } } };
|
|
618
|
+
writeFileSync2(continueMcpDest, JSON.stringify(continueMcp, null, 2));
|
|
619
|
+
created.push(`.continue/mcpServers/mindlink.json ${chalk2.dim("Continue MCP server")}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (selectedAgents.includes("copilot")) {
|
|
623
|
+
const copilotMcpDest = join3(projectPath, ".vscode", "mcp.json");
|
|
624
|
+
if (!existsSync2(copilotMcpDest)) {
|
|
625
|
+
mkdirSync2(join3(projectPath, ".vscode"), { recursive: true });
|
|
626
|
+
const copilotMcp = { mcpServers: { mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } } } };
|
|
627
|
+
writeFileSync2(copilotMcpDest, JSON.stringify(copilotMcp, null, 2));
|
|
628
|
+
created.push(`.vscode/mcp.json${" ".repeat(19)} ${chalk2.dim("GitHub Copilot MCP server")}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (selectedAgents.includes("kiro")) {
|
|
632
|
+
const kiroMcpDest = join3(projectPath, ".kiro", "settings", "mcp.json");
|
|
633
|
+
if (!existsSync2(kiroMcpDest)) {
|
|
634
|
+
mkdirSync2(join3(projectPath, ".kiro", "settings"), { recursive: true });
|
|
635
|
+
const kiroMcp = { mcpServers: { mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } } } };
|
|
636
|
+
writeFileSync2(kiroMcpDest, JSON.stringify(kiroMcp, null, 2));
|
|
637
|
+
created.push(`.kiro/settings/mcp.json${" ".repeat(13)} ${chalk2.dim("Kiro MCP server")}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (selectedAgents.includes("windsurf")) {
|
|
641
|
+
try {
|
|
642
|
+
mkdirSync2(dirname2(GLOBAL_WINDSURF_MCP_PATH), { recursive: true });
|
|
643
|
+
let existingWindsurf = {};
|
|
644
|
+
if (existsSync2(GLOBAL_WINDSURF_MCP_PATH)) {
|
|
645
|
+
try {
|
|
646
|
+
existingWindsurf = JSON.parse(readFileSync2(GLOBAL_WINDSURF_MCP_PATH, "utf8"));
|
|
647
|
+
} catch {
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const mergedWindsurf = {
|
|
651
|
+
...existingWindsurf,
|
|
652
|
+
mcpServers: {
|
|
653
|
+
...typeof existingWindsurf.mcpServers === "object" && existingWindsurf.mcpServers !== null ? existingWindsurf.mcpServers : {},
|
|
654
|
+
mindlink: { command: "mindlink", args: ["mcp"] }
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
writeFileSync2(GLOBAL_WINDSURF_MCP_PATH, JSON.stringify(mergedWindsurf, null, 2));
|
|
658
|
+
created.push(`~/.codeium/windsurf/mcp_config.json ${chalk2.dim("Windsurf MCP server (global)")}`);
|
|
659
|
+
} catch {
|
|
469
660
|
}
|
|
470
661
|
}
|
|
471
662
|
if (!gitTracking) {
|
|
@@ -498,7 +689,15 @@ Examples:
|
|
|
498
689
|
console.log("");
|
|
499
690
|
for (const err of errors) console.log(` ${chalk2.red("\u2717")} ${err}`);
|
|
500
691
|
}
|
|
501
|
-
|
|
692
|
+
if (selectedAgents.includes("cline")) {
|
|
693
|
+
console.log(` ${chalk2.yellow("\u2192")} Cline: add the MCP server manually in Cline's settings UI (MCP Servers tab)`);
|
|
694
|
+
console.log(` ${chalk2.dim("Command: mindlink Args: mcp Env: MINDLINK_PROJECT_PATH=" + projectPath)}`);
|
|
695
|
+
console.log("");
|
|
696
|
+
}
|
|
697
|
+
if (!existsSync2(GLOBAL_USER_PROFILE_PATH)) {
|
|
698
|
+
console.log(` ${chalk2.dim("\u2192")} Run ${chalk2.cyan("mindlink profile")} to set up a global user profile \u2014 imported into every new project automatically.`);
|
|
699
|
+
console.log("");
|
|
700
|
+
}
|
|
502
701
|
note(
|
|
503
702
|
`Your AI finally has a brain.
|
|
504
703
|
|
|
@@ -1200,7 +1399,7 @@ var REQUIRED_BRAIN_FILES = ["MEMORY.md", "SESSION.md", "SHARED.md", "LOG.md"];
|
|
|
1200
1399
|
async function latestVersion() {
|
|
1201
1400
|
try {
|
|
1202
1401
|
const { default: https } = await import("https");
|
|
1203
|
-
return new Promise((
|
|
1402
|
+
return new Promise((resolve17) => {
|
|
1204
1403
|
const req = https.get(
|
|
1205
1404
|
"https://registry.npmjs.org/mindlink/latest",
|
|
1206
1405
|
{ headers: { "User-Agent": "mindlink-cli" } },
|
|
@@ -1212,17 +1411,17 @@ async function latestVersion() {
|
|
|
1212
1411
|
res.on("end", () => {
|
|
1213
1412
|
try {
|
|
1214
1413
|
const parsed = JSON.parse(data);
|
|
1215
|
-
|
|
1414
|
+
resolve17(parsed.version ?? null);
|
|
1216
1415
|
} catch {
|
|
1217
|
-
|
|
1416
|
+
resolve17(null);
|
|
1218
1417
|
}
|
|
1219
1418
|
});
|
|
1220
1419
|
}
|
|
1221
1420
|
);
|
|
1222
|
-
req.on("error", () =>
|
|
1421
|
+
req.on("error", () => resolve17(null));
|
|
1223
1422
|
req.setTimeout(8e3, () => {
|
|
1224
1423
|
req.destroy();
|
|
1225
|
-
|
|
1424
|
+
resolve17(null);
|
|
1226
1425
|
});
|
|
1227
1426
|
});
|
|
1228
1427
|
} catch {
|
|
@@ -1243,71 +1442,73 @@ Examples:
|
|
|
1243
1442
|
mindlink update
|
|
1244
1443
|
`).action(async () => {
|
|
1245
1444
|
const current = VERSION;
|
|
1445
|
+
let nonTtyExitCode = null;
|
|
1246
1446
|
if (!process.stdin.isTTY) {
|
|
1247
|
-
const
|
|
1248
|
-
if (!
|
|
1447
|
+
const latest = await latestVersion();
|
|
1448
|
+
if (!latest) {
|
|
1249
1449
|
console.log(JSON.stringify({ current, latest: null, upToDate: null }));
|
|
1250
|
-
|
|
1450
|
+
nonTtyExitCode = 1;
|
|
1451
|
+
} else {
|
|
1452
|
+
const upToDate = !semverGt(latest, current);
|
|
1453
|
+
console.log(JSON.stringify({ current, latest, upToDate }));
|
|
1454
|
+
if (!upToDate) nonTtyExitCode = 2;
|
|
1251
1455
|
}
|
|
1252
|
-
const upToDate = !semverGt(latest2, current);
|
|
1253
|
-
console.log(JSON.stringify({ current, latest: latest2, upToDate }));
|
|
1254
|
-
if (!upToDate) process.exit(2);
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
const s = spinner2();
|
|
1258
|
-
s.start("Checking for updates...");
|
|
1259
|
-
const latest = await latestVersion();
|
|
1260
|
-
if (!latest) {
|
|
1261
|
-
s.stop("Could not reach npm registry.");
|
|
1262
|
-
console.log("");
|
|
1263
|
-
console.log(` ${chalk9.red("\u2717")} Could not check for updates. Check your internet connection.`);
|
|
1264
|
-
console.log(` ${chalk9.dim("Latest releases: github.com/404-not-found/mindlink/releases")}`);
|
|
1265
|
-
console.log("");
|
|
1266
|
-
process.exit(1);
|
|
1267
|
-
}
|
|
1268
|
-
s.stop("Done.");
|
|
1269
|
-
console.log("");
|
|
1270
|
-
console.log(` Current version : ${chalk9.dim(current)}`);
|
|
1271
|
-
console.log(` Latest version : ${semverGt(latest, current) ? chalk9.green(latest) : chalk9.dim(latest)}`);
|
|
1272
|
-
console.log("");
|
|
1273
|
-
if (!semverGt(latest, current)) {
|
|
1274
|
-
console.log(` ${chalk9.green("\u2713")} You're on the latest version (${current}).`);
|
|
1275
|
-
console.log("");
|
|
1276
1456
|
} else {
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
{ value: "cancel", label: "Cancel" }
|
|
1283
|
-
]
|
|
1284
|
-
});
|
|
1285
|
-
if (isCancel4(action) || action === "cancel" || action === "skip") {
|
|
1286
|
-
if (action === "skip") {
|
|
1287
|
-
console.log(` ${chalk9.dim("Skipped. Run mindlink update again to install later.")}`);
|
|
1288
|
-
} else {
|
|
1289
|
-
cancel4("Cancelled.");
|
|
1290
|
-
}
|
|
1457
|
+
const s = spinner2();
|
|
1458
|
+
s.start("Checking for updates...");
|
|
1459
|
+
const latest = await latestVersion();
|
|
1460
|
+
if (!latest) {
|
|
1461
|
+
s.stop("Could not reach npm registry.");
|
|
1291
1462
|
console.log("");
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
const s2 = spinner2();
|
|
1295
|
-
s2.start(`Installing mindlink@${latest}...`);
|
|
1296
|
-
try {
|
|
1297
|
-
execSync2(`npm install -g mindlink@${latest}`, { stdio: "pipe" });
|
|
1298
|
-
s2.stop("Done.");
|
|
1463
|
+
console.log(` ${chalk9.red("\u2717")} Could not check for updates. Check your internet connection.`);
|
|
1464
|
+
console.log(` ${chalk9.dim("Latest releases: github.com/404-not-found/mindlink/releases")}`);
|
|
1299
1465
|
console.log("");
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1466
|
+
process.exit(1);
|
|
1467
|
+
}
|
|
1468
|
+
s.stop("Done.");
|
|
1469
|
+
console.log("");
|
|
1470
|
+
console.log(` Current version : ${chalk9.dim(current)}`);
|
|
1471
|
+
console.log(` Latest version : ${semverGt(latest, current) ? chalk9.green(latest) : chalk9.dim(latest)}`);
|
|
1472
|
+
console.log("");
|
|
1473
|
+
if (!semverGt(latest, current)) {
|
|
1474
|
+
console.log(` ${chalk9.green("\u2713")} You're on the latest version (${current}).`);
|
|
1304
1475
|
console.log("");
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1476
|
+
} else {
|
|
1477
|
+
const action = await select3({
|
|
1478
|
+
message: `Update to ${latest}?`,
|
|
1479
|
+
options: [
|
|
1480
|
+
{ value: "update", label: `Update to ${latest}` },
|
|
1481
|
+
{ value: "skip", label: "Skip this version" },
|
|
1482
|
+
{ value: "cancel", label: "Cancel" }
|
|
1483
|
+
]
|
|
1484
|
+
});
|
|
1485
|
+
if (isCancel4(action) || action === "cancel" || action === "skip") {
|
|
1486
|
+
if (action === "skip") {
|
|
1487
|
+
console.log(` ${chalk9.dim("Skipped. Run mindlink update again to install later.")}`);
|
|
1488
|
+
} else {
|
|
1489
|
+
cancel4("Cancelled.");
|
|
1490
|
+
}
|
|
1491
|
+
console.log("");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const s2 = spinner2();
|
|
1495
|
+
s2.start(`Installing mindlink@${latest}...`);
|
|
1496
|
+
try {
|
|
1497
|
+
execSync2(`npm install -g mindlink@${latest}`, { stdio: "pipe" });
|
|
1498
|
+
s2.stop("Done.");
|
|
1499
|
+
console.log("");
|
|
1500
|
+
console.log(` ${chalk9.green("\u2713")} Updated to ${latest}.`);
|
|
1501
|
+
console.log(` ${chalk9.dim("See what's new: github.com/404-not-found/mindlink/releases")}`);
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
s2.stop("Failed.");
|
|
1504
|
+
console.log("");
|
|
1505
|
+
console.log(` ${chalk9.red("\u2717")} Update failed.`);
|
|
1506
|
+
console.log(` ${chalk9.dim("Try: npm install -g mindlink@" + latest)}`);
|
|
1507
|
+
if (err instanceof Error && err.message.includes("EACCES")) {
|
|
1508
|
+
console.log(` ${chalk9.dim("Permission error \u2014 try: sudo npm install -g mindlink@" + latest)}`);
|
|
1509
|
+
}
|
|
1510
|
+
process.exit(1);
|
|
1309
1511
|
}
|
|
1310
|
-
process.exit(1);
|
|
1311
1512
|
}
|
|
1312
1513
|
}
|
|
1313
1514
|
const cwd = process.cwd();
|
|
@@ -1356,10 +1557,146 @@ Examples:
|
|
|
1356
1557
|
const hookDest = join10(projectPath, ".claude", "settings.json");
|
|
1357
1558
|
try {
|
|
1358
1559
|
mkdirSync4(join10(projectPath, ".claude"), { recursive: true });
|
|
1359
|
-
|
|
1560
|
+
const template = JSON.parse(readFileSync9(join10(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
|
|
1561
|
+
let existing = {};
|
|
1562
|
+
if (existsSync9(hookDest)) {
|
|
1563
|
+
try {
|
|
1564
|
+
existing = JSON.parse(readFileSync9(hookDest, "utf8"));
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
const { mcpServers: _removed, ...existingWithoutMcp } = existing;
|
|
1569
|
+
void _removed;
|
|
1570
|
+
const merged = {
|
|
1571
|
+
...existingWithoutMcp,
|
|
1572
|
+
hooks: template.hooks,
|
|
1573
|
+
permissions: template.permissions
|
|
1574
|
+
};
|
|
1575
|
+
writeFileSync6(hookDest, JSON.stringify(merged, null, 2));
|
|
1360
1576
|
refreshed.push(".claude/settings.json");
|
|
1361
1577
|
} catch {
|
|
1362
1578
|
}
|
|
1579
|
+
const mcpJsonDest = join10(projectPath, ".mcp.json");
|
|
1580
|
+
try {
|
|
1581
|
+
let existing = {};
|
|
1582
|
+
if (existsSync9(mcpJsonDest)) {
|
|
1583
|
+
try {
|
|
1584
|
+
existing = JSON.parse(readFileSync9(mcpJsonDest, "utf8"));
|
|
1585
|
+
} catch {
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
const merged = {
|
|
1589
|
+
...existing,
|
|
1590
|
+
mcpServers: {
|
|
1591
|
+
...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
|
|
1592
|
+
mindlink: { type: "stdio", command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
writeFileSync6(mcpJsonDest, JSON.stringify(merged, null, 2));
|
|
1596
|
+
refreshed.push(".mcp.json");
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (agentValues.includes("cursor")) {
|
|
1601
|
+
const cursorMcpDest = join10(projectPath, ".cursor", "mcp.json");
|
|
1602
|
+
try {
|
|
1603
|
+
mkdirSync4(join10(projectPath, ".cursor"), { recursive: true });
|
|
1604
|
+
let existing = {};
|
|
1605
|
+
if (existsSync9(cursorMcpDest)) {
|
|
1606
|
+
try {
|
|
1607
|
+
existing = JSON.parse(readFileSync9(cursorMcpDest, "utf8"));
|
|
1608
|
+
} catch {
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const merged = {
|
|
1612
|
+
...existing,
|
|
1613
|
+
mcpServers: {
|
|
1614
|
+
...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
|
|
1615
|
+
mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
writeFileSync6(cursorMcpDest, JSON.stringify(merged, null, 2));
|
|
1619
|
+
refreshed.push(".cursor/mcp.json");
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (agentValues.includes("continue")) {
|
|
1624
|
+
const continueMcpDest = join10(projectPath, ".continue", "mcpServers", "mindlink.json");
|
|
1625
|
+
try {
|
|
1626
|
+
mkdirSync4(join10(projectPath, ".continue", "mcpServers"), { recursive: true });
|
|
1627
|
+
const continueMcp = { mcpServers: { mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } } } };
|
|
1628
|
+
writeFileSync6(continueMcpDest, JSON.stringify(continueMcp, null, 2));
|
|
1629
|
+
refreshed.push(".continue/mcpServers/mindlink.json");
|
|
1630
|
+
} catch {
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
if (agentValues.includes("copilot")) {
|
|
1634
|
+
const copilotMcpDest = join10(projectPath, ".vscode", "mcp.json");
|
|
1635
|
+
try {
|
|
1636
|
+
mkdirSync4(join10(projectPath, ".vscode"), { recursive: true });
|
|
1637
|
+
let existing = {};
|
|
1638
|
+
if (existsSync9(copilotMcpDest)) {
|
|
1639
|
+
try {
|
|
1640
|
+
existing = JSON.parse(readFileSync9(copilotMcpDest, "utf8"));
|
|
1641
|
+
} catch {
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const merged = {
|
|
1645
|
+
...existing,
|
|
1646
|
+
mcpServers: {
|
|
1647
|
+
...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
|
|
1648
|
+
mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
writeFileSync6(copilotMcpDest, JSON.stringify(merged, null, 2));
|
|
1652
|
+
refreshed.push(".vscode/mcp.json");
|
|
1653
|
+
} catch {
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
if (agentValues.includes("kiro")) {
|
|
1657
|
+
const kiroMcpDest = join10(projectPath, ".kiro", "settings", "mcp.json");
|
|
1658
|
+
try {
|
|
1659
|
+
mkdirSync4(join10(projectPath, ".kiro", "settings"), { recursive: true });
|
|
1660
|
+
let existing = {};
|
|
1661
|
+
if (existsSync9(kiroMcpDest)) {
|
|
1662
|
+
try {
|
|
1663
|
+
existing = JSON.parse(readFileSync9(kiroMcpDest, "utf8"));
|
|
1664
|
+
} catch {
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const merged = {
|
|
1668
|
+
...existing,
|
|
1669
|
+
mcpServers: {
|
|
1670
|
+
...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
|
|
1671
|
+
mindlink: { command: "mindlink", args: ["mcp"], env: { MINDLINK_PROJECT_PATH: projectPath } }
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
writeFileSync6(kiroMcpDest, JSON.stringify(merged, null, 2));
|
|
1675
|
+
refreshed.push(".kiro/settings/mcp.json");
|
|
1676
|
+
} catch {
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (agentValues.includes("windsurf")) {
|
|
1680
|
+
try {
|
|
1681
|
+
mkdirSync4(dirname4(GLOBAL_WINDSURF_MCP_PATH), { recursive: true });
|
|
1682
|
+
let existing = {};
|
|
1683
|
+
if (existsSync9(GLOBAL_WINDSURF_MCP_PATH)) {
|
|
1684
|
+
try {
|
|
1685
|
+
existing = JSON.parse(readFileSync9(GLOBAL_WINDSURF_MCP_PATH, "utf8"));
|
|
1686
|
+
} catch {
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
const merged = {
|
|
1690
|
+
...existing,
|
|
1691
|
+
mcpServers: {
|
|
1692
|
+
...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
|
|
1693
|
+
mindlink: { command: "mindlink", args: ["mcp"] }
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
writeFileSync6(GLOBAL_WINDSURF_MCP_PATH, JSON.stringify(merged, null, 2));
|
|
1697
|
+
refreshed.push("~/.codeium/windsurf/mcp_config.json");
|
|
1698
|
+
} catch {
|
|
1699
|
+
}
|
|
1363
1700
|
}
|
|
1364
1701
|
const memoryPath = join10(projectPath, BRAIN_DIR, "MEMORY.md");
|
|
1365
1702
|
if (existsSync9(memoryPath)) {
|
|
@@ -1401,6 +1738,26 @@ Examples:
|
|
|
1401
1738
|
} catch {
|
|
1402
1739
|
}
|
|
1403
1740
|
}
|
|
1741
|
+
if (existsSync9(GLOBAL_USER_PROFILE_PATH) && existsSync9(memoryPath)) {
|
|
1742
|
+
try {
|
|
1743
|
+
const profileRaw = readFileSync9(GLOBAL_USER_PROFILE_PATH, "utf8");
|
|
1744
|
+
const profileLines = profileRaw.split("\n");
|
|
1745
|
+
const bodyStart = profileLines.findIndex((l) => {
|
|
1746
|
+
const t = l.trim();
|
|
1747
|
+
return t.length > 0 && !t.startsWith("#") && !t.startsWith(">") && !t.startsWith("<!--");
|
|
1748
|
+
});
|
|
1749
|
+
const profileBody = bodyStart >= 0 ? profileLines.slice(bodyStart).join("\n").trim() : "";
|
|
1750
|
+
if (profileBody) {
|
|
1751
|
+
const memContent = readFileSync9(memoryPath, "utf8");
|
|
1752
|
+
const updated = replaceSection(memContent, "User Profile", profileBody);
|
|
1753
|
+
if (updated !== memContent) {
|
|
1754
|
+
writeFileSync6(memoryPath, updated);
|
|
1755
|
+
if (!refreshed.includes(".brain/MEMORY.md")) refreshed.push(".brain/MEMORY.md (profile synced)");
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
} catch {
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1404
1761
|
console.log(` ${chalk9.bold(projectPath)}`);
|
|
1405
1762
|
for (const f of refreshed) {
|
|
1406
1763
|
console.log(` ${chalk9.green("\u2713")} ${f}`);
|
|
@@ -1410,6 +1767,7 @@ Examples:
|
|
|
1410
1767
|
console.log(` ${chalk9.dim("All agent files are up to date.")}`);
|
|
1411
1768
|
}
|
|
1412
1769
|
console.log("");
|
|
1770
|
+
if (nonTtyExitCode !== null) process.exit(nonTtyExitCode);
|
|
1413
1771
|
});
|
|
1414
1772
|
|
|
1415
1773
|
// src/commands/summary.ts
|
|
@@ -2238,8 +2596,760 @@ Examples:
|
|
|
2238
2596
|
console.log("");
|
|
2239
2597
|
});
|
|
2240
2598
|
|
|
2599
|
+
// src/commands/verify.ts
|
|
2600
|
+
import { Command as Command16 } from "commander";
|
|
2601
|
+
import chalk17 from "chalk";
|
|
2602
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
2603
|
+
import { dirname as dirname5 } from "path";
|
|
2604
|
+
import { join as join17, resolve as resolve14 } from "path";
|
|
2605
|
+
var SESSION_WARN_DAYS = 3;
|
|
2606
|
+
var SESSION_FAIL_DAYS = 7;
|
|
2607
|
+
var MEMORY_WARN_LINES = 100;
|
|
2608
|
+
var MEMORY_FAIL_LINES = 200;
|
|
2609
|
+
function pass(id, label, message) {
|
|
2610
|
+
return { id, label, status: "pass", message, fixable: false };
|
|
2611
|
+
}
|
|
2612
|
+
function warn2(id, label, message, fixable = false) {
|
|
2613
|
+
return { id, label, status: "warn", message, fixable };
|
|
2614
|
+
}
|
|
2615
|
+
function fail2(id, label, message, fixable = false) {
|
|
2616
|
+
return { id, label, status: "fail", message, fixable };
|
|
2617
|
+
}
|
|
2618
|
+
function icon2(status) {
|
|
2619
|
+
switch (status) {
|
|
2620
|
+
case "pass":
|
|
2621
|
+
return chalk17.green("\u2713");
|
|
2622
|
+
case "warn":
|
|
2623
|
+
return chalk17.yellow("\u26A0");
|
|
2624
|
+
case "fail":
|
|
2625
|
+
return chalk17.red("\u2717");
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
function runChecks(projectPath) {
|
|
2629
|
+
const brainDir = join17(projectPath, BRAIN_DIR);
|
|
2630
|
+
const results = [];
|
|
2631
|
+
if (!existsSync16(brainDir)) {
|
|
2632
|
+
results.push(fail2("brain_missing", ".brain/ missing", `Run ${chalk17.cyan("mindlink init")} to set up memory.`));
|
|
2633
|
+
return results;
|
|
2634
|
+
}
|
|
2635
|
+
const configPath = join17(brainDir, "config.json");
|
|
2636
|
+
let config = {};
|
|
2637
|
+
if (existsSync16(configPath)) {
|
|
2638
|
+
try {
|
|
2639
|
+
config = JSON.parse(readFileSync14(configPath, "utf8"));
|
|
2640
|
+
} catch {
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
const memoryPath = join17(brainDir, "MEMORY.md");
|
|
2644
|
+
if (!existsSync16(memoryPath)) {
|
|
2645
|
+
results.push(fail2("core", "Core section", `MEMORY.md is missing \u2014 run ${chalk17.cyan("mindlink init")}.`));
|
|
2646
|
+
} else {
|
|
2647
|
+
const memMd = readFileSync14(memoryPath, "utf8");
|
|
2648
|
+
if (!sectionHasRealContent(memMd, "Core")) {
|
|
2649
|
+
results.push(fail2("core", "Core section \u2014 empty", "Start a session and tell your AI to fill in the Core section."));
|
|
2650
|
+
} else {
|
|
2651
|
+
results.push(pass("core", "Core section \u2014 filled", ""));
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
if (existsSync16(memoryPath)) {
|
|
2655
|
+
const memMd = readFileSync14(memoryPath, "utf8");
|
|
2656
|
+
if (!sectionHasRealContent(memMd, "User Profile")) {
|
|
2657
|
+
results.push(fail2("user_profile", "User Profile \u2014 empty", `Run ${chalk17.cyan("mindlink profile")} to set up your global profile and import it here.`));
|
|
2658
|
+
} else {
|
|
2659
|
+
results.push(pass("user_profile", "User Profile \u2014 filled", ""));
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
const sessionPath = join17(brainDir, "SESSION.md");
|
|
2663
|
+
if (!existsSync16(sessionPath)) {
|
|
2664
|
+
results.push(fail2("session_fresh", "SESSION.md \u2014 missing", `Run ${chalk17.cyan("mindlink init")} to recreate it.`));
|
|
2665
|
+
} else {
|
|
2666
|
+
const ageDays = (Date.now() - statSync5(sessionPath).mtime.getTime()) / 864e5;
|
|
2667
|
+
const hasContent = readFileSync14(sessionPath, "utf8").split("\n").some((l) => l.trim().length > 0 && !l.startsWith("#") && !l.startsWith("<!--"));
|
|
2668
|
+
if (!hasContent) {
|
|
2669
|
+
results.push(warn2("session_fresh", "SESSION.md \u2014 no content yet", "Start a session \u2014 your AI will fill this in automatically."));
|
|
2670
|
+
} else if (ageDays > SESSION_FAIL_DAYS) {
|
|
2671
|
+
const days = Math.floor(ageDays);
|
|
2672
|
+
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."));
|
|
2673
|
+
} else if (ageDays > SESSION_WARN_DAYS) {
|
|
2674
|
+
const days = Math.floor(ageDays);
|
|
2675
|
+
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."));
|
|
2676
|
+
} else {
|
|
2677
|
+
const mins = Math.floor((Date.now() - statSync5(sessionPath).mtime.getTime()) / 6e4);
|
|
2678
|
+
const age = mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.floor(mins / 60)}h ago` : `${Math.floor(mins / 1440)}d ago`;
|
|
2679
|
+
results.push(pass("session_fresh", `SESSION.md \u2014 updated ${age}`, ""));
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
const logPath = join17(brainDir, "LOG.md");
|
|
2683
|
+
if (!existsSync16(logPath)) {
|
|
2684
|
+
results.push(fail2("log_present", "LOG.md \u2014 missing", `Run ${chalk17.cyan("mindlink init")} to recreate it.`));
|
|
2685
|
+
} else {
|
|
2686
|
+
const entries = (readFileSync14(logPath, "utf8").match(/^##\s+/gm) ?? []).length;
|
|
2687
|
+
if (entries === 0) {
|
|
2688
|
+
results.push(pass("log_present", "LOG.md \u2014 no sessions yet", ""));
|
|
2689
|
+
} else {
|
|
2690
|
+
results.push(pass("log_present", `LOG.md \u2014 ${entries} session${entries !== 1 ? "s" : ""} logged`, ""));
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
if (existsSync16(memoryPath)) {
|
|
2694
|
+
const memMd = readFileSync14(memoryPath, "utf8");
|
|
2695
|
+
const lines = countRealLines(memMd);
|
|
2696
|
+
if (lines > MEMORY_FAIL_LINES) {
|
|
2697
|
+
results.push(fail2("memory_size", `MEMORY.md \u2014 ${lines} lines (target: under ${MEMORY_FAIL_LINES})`, `Run ${chalk17.cyan("mindlink prune")} to consolidate stale entries.`));
|
|
2698
|
+
} else if (lines > MEMORY_WARN_LINES) {
|
|
2699
|
+
results.push(warn2("memory_size", `MEMORY.md \u2014 ${lines} lines (getting long)`, `Consider running ${chalk17.cyan("mindlink prune")} to retire old entries.`));
|
|
2700
|
+
} else {
|
|
2701
|
+
results.push(pass("memory_size", `MEMORY.md \u2014 ${lines} line${lines !== 1 ? "s" : ""} (healthy)`, ""));
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
const configuredAgents = config.agents ?? [];
|
|
2705
|
+
if (configuredAgents.length === 0) {
|
|
2706
|
+
results.push(warn2("agent_files", "No agents configured", `Run ${chalk17.cyan("mindlink config")} \u2192 Agent instruction files.`, false));
|
|
2707
|
+
} else {
|
|
2708
|
+
const missing = [];
|
|
2709
|
+
for (const agentValue of configuredAgents) {
|
|
2710
|
+
const agent = AGENTS.find((a) => a.value === agentValue);
|
|
2711
|
+
if (!agent) continue;
|
|
2712
|
+
if (!existsSync16(join17(projectPath, agent.destFile))) missing.push(agent.destFile);
|
|
2713
|
+
}
|
|
2714
|
+
if (missing.length === configuredAgents.length) {
|
|
2715
|
+
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));
|
|
2716
|
+
} else if (missing.length > 0) {
|
|
2717
|
+
results.push(warn2("agent_files", `Agent files \u2014 ${missing.length} missing: ${missing.join(", ")}`, `Run ${chalk17.cyan("mindlink verify --fix")} to regenerate.`, true));
|
|
2718
|
+
} else {
|
|
2719
|
+
results.push(pass("agent_files", `Agent files \u2014 all ${configuredAgents.length} present`, ""));
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return results;
|
|
2723
|
+
}
|
|
2724
|
+
function applyFix(projectPath, results) {
|
|
2725
|
+
const brainDir = join17(projectPath, BRAIN_DIR);
|
|
2726
|
+
const configPath = join17(brainDir, "config.json");
|
|
2727
|
+
let config = {};
|
|
2728
|
+
if (existsSync16(configPath)) {
|
|
2729
|
+
try {
|
|
2730
|
+
config = JSON.parse(readFileSync14(configPath, "utf8"));
|
|
2731
|
+
} catch {
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
const fixable = results.filter((r) => r.fixable && r.status !== "pass");
|
|
2735
|
+
if (fixable.length === 0) {
|
|
2736
|
+
console.log(` ${chalk17.dim("Nothing to auto-fix. Address the issues above manually.")}`);
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
let fixed = 0;
|
|
2740
|
+
for (const r of fixable) {
|
|
2741
|
+
if (r.id === "agent_files") {
|
|
2742
|
+
const configuredAgents = config.agents ?? [];
|
|
2743
|
+
for (const agentValue of configuredAgents) {
|
|
2744
|
+
const agent = AGENTS.find((a) => a.value === agentValue);
|
|
2745
|
+
if (!agent) continue;
|
|
2746
|
+
const destPath = join17(projectPath, agent.destFile);
|
|
2747
|
+
if (!existsSync16(destPath)) {
|
|
2748
|
+
try {
|
|
2749
|
+
mkdirSync6(dirname5(destPath), { recursive: true });
|
|
2750
|
+
writeFileSync7(destPath, readFileSync14(join17(AGENT_TEMPLATES_DIR, agent.templateFile), "utf8"));
|
|
2751
|
+
console.log(` ${chalk17.green("\u2713")} Regenerated ${agent.destFile}`);
|
|
2752
|
+
fixed++;
|
|
2753
|
+
} catch (err) {
|
|
2754
|
+
console.log(` ${chalk17.red("\u2717")} Failed to regenerate ${agent.destFile}: ${err instanceof Error ? err.message : err}`);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
if (configuredAgents.includes("claude")) {
|
|
2759
|
+
const hookPath = join17(projectPath, ".claude", "settings.json");
|
|
2760
|
+
if (!existsSync16(hookPath)) {
|
|
2761
|
+
try {
|
|
2762
|
+
mkdirSync6(dirname5(hookPath), { recursive: true });
|
|
2763
|
+
writeFileSync7(hookPath, readFileSync14(join17(HOOKS_TEMPLATES_DIR, "claude-settings.json"), "utf8"));
|
|
2764
|
+
console.log(` ${chalk17.green("\u2713")} Regenerated .claude/settings.json`);
|
|
2765
|
+
fixed++;
|
|
2766
|
+
} catch {
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
for (const r of results) {
|
|
2773
|
+
if (r.status === "fail" || r.status === "warn") {
|
|
2774
|
+
if (r.id === "core" || r.id === "user_profile") {
|
|
2775
|
+
console.log(` ${chalk17.dim("\u2192")} ${r.id === "core" ? "Core" : "User Profile"}: start a session and tell your AI to fill this in.`);
|
|
2776
|
+
} else if (r.id === "memory_size") {
|
|
2777
|
+
console.log(` ${chalk17.dim("\u2192")} MEMORY.md too large: run ${chalk17.cyan("mindlink prune")} to consolidate.`);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
if (fixed > 0) console.log("");
|
|
2782
|
+
}
|
|
2783
|
+
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", `
|
|
2784
|
+
What is checked:
|
|
2785
|
+
Core \u2014 MEMORY.md Core section has real content
|
|
2786
|
+
User Profile \u2014 MEMORY.md User Profile has real content
|
|
2787
|
+
SESSION.md \u2014 was updated recently (warn >3 days, fail >7 days)
|
|
2788
|
+
LOG.md \u2014 present and readable
|
|
2789
|
+
MEMORY.md size \u2014 line count (warn >100, fail >200)
|
|
2790
|
+
Agent files \u2014 all files from config.json are present on disk
|
|
2791
|
+
|
|
2792
|
+
Examples:
|
|
2793
|
+
mindlink verify
|
|
2794
|
+
mindlink verify --json
|
|
2795
|
+
mindlink verify --fix
|
|
2796
|
+
`).action((opts) => {
|
|
2797
|
+
const projectPath = resolve14(process.cwd());
|
|
2798
|
+
const results = runChecks(projectPath);
|
|
2799
|
+
if (opts.json) {
|
|
2800
|
+
console.log(JSON.stringify({ ok: results.every((r) => r.status === "pass"), checks: results }, null, 2));
|
|
2801
|
+
const hasFailure = results.some((r) => r.status === "fail");
|
|
2802
|
+
process.exit(hasFailure ? 1 : 0);
|
|
2803
|
+
}
|
|
2804
|
+
console.log("");
|
|
2805
|
+
console.log(` ${chalk17.bold("\u25C9 MindLink Verify")}`);
|
|
2806
|
+
console.log(` ${chalk17.dim(projectPath)}`);
|
|
2807
|
+
console.log("");
|
|
2808
|
+
for (const r of results) {
|
|
2809
|
+
console.log(` ${icon2(r.status)} ${r.label}`);
|
|
2810
|
+
if (r.message) console.log(` ${chalk17.dim(r.message)}`);
|
|
2811
|
+
}
|
|
2812
|
+
console.log("");
|
|
2813
|
+
const failCount = results.filter((r) => r.status === "fail").length;
|
|
2814
|
+
const warnCount = results.filter((r) => r.status === "warn").length;
|
|
2815
|
+
const fixableCount = results.filter((r) => r.fixable && r.status !== "pass").length;
|
|
2816
|
+
if (opts.fix) {
|
|
2817
|
+
applyFix(projectPath, results);
|
|
2818
|
+
}
|
|
2819
|
+
if (failCount > 0) {
|
|
2820
|
+
console.log(` ${chalk17.red.bold(`${failCount} error${failCount !== 1 ? "s" : ""}`)}, ${warnCount} warning${warnCount !== 1 ? "s" : ""}.`);
|
|
2821
|
+
if (!opts.fix && fixableCount > 0) {
|
|
2822
|
+
console.log(` ${chalk17.dim(`Run ${chalk17.cyan("mindlink verify --fix")} to auto-repair ${fixableCount} issue${fixableCount !== 1 ? "s" : ""}.`)}`);
|
|
2823
|
+
}
|
|
2824
|
+
} else if (warnCount > 0) {
|
|
2825
|
+
console.log(` ${chalk17.yellow.bold(`${warnCount} warning${warnCount !== 1 ? "s" : ""}`)}, no critical errors.`);
|
|
2826
|
+
} else {
|
|
2827
|
+
console.log(` ${chalk17.green.bold("All good.")} Your AI's memory is healthy.`);
|
|
2828
|
+
}
|
|
2829
|
+
console.log("");
|
|
2830
|
+
if (failCount > 0) process.exit(1);
|
|
2831
|
+
});
|
|
2832
|
+
|
|
2833
|
+
// src/commands/profile.ts
|
|
2834
|
+
import { Command as Command17 } from "commander";
|
|
2835
|
+
import chalk18 from "chalk";
|
|
2836
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
2837
|
+
import { execSync as execSync4 } from "child_process";
|
|
2838
|
+
var PROFILE_TEMPLATE = `# MindLink \u2014 Global User Profile
|
|
2839
|
+
|
|
2840
|
+
> This file is imported into every new project's MEMORY.md on \`mindlink init\`.
|
|
2841
|
+
> Edit it with \`mindlink profile\`. Run \`mindlink update\` to sync changes to all projects.
|
|
2842
|
+
|
|
2843
|
+
<!-- Role, company, title, level, years of experience -->
|
|
2844
|
+
<!-- Primary languages and tools -->
|
|
2845
|
+
<!-- Communication style and preferences -->
|
|
2846
|
+
<!-- Editor, OS, shell setup -->
|
|
2847
|
+
<!-- Added: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)} -->
|
|
2848
|
+
`;
|
|
2849
|
+
function ensureProfileExists() {
|
|
2850
|
+
if (!existsSync17(GLOBAL_MINDLINK_DIR)) {
|
|
2851
|
+
mkdirSync7(GLOBAL_MINDLINK_DIR, { recursive: true });
|
|
2852
|
+
}
|
|
2853
|
+
if (!existsSync17(GLOBAL_USER_PROFILE_PATH)) {
|
|
2854
|
+
writeFileSync8(GLOBAL_USER_PROFILE_PATH, PROFILE_TEMPLATE);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
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", `
|
|
2858
|
+
Your profile is stored at ~/.mindlink/USER.md and auto-imported into
|
|
2859
|
+
MEMORY.md when you run \`mindlink init\` on a new project.
|
|
2860
|
+
|
|
2861
|
+
Run \`mindlink update\` to sync profile changes to all registered projects.
|
|
2862
|
+
|
|
2863
|
+
Examples:
|
|
2864
|
+
mindlink profile # open profile in $EDITOR
|
|
2865
|
+
mindlink profile --show # print profile to stdout
|
|
2866
|
+
mindlink profile --path # print file path
|
|
2867
|
+
`).action((opts) => {
|
|
2868
|
+
if (opts.path) {
|
|
2869
|
+
console.log(GLOBAL_USER_PROFILE_PATH);
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
if (opts.show) {
|
|
2873
|
+
if (!existsSync17(GLOBAL_USER_PROFILE_PATH)) {
|
|
2874
|
+
console.log("");
|
|
2875
|
+
console.log(` ${chalk18.yellow("\u26A0")} No global profile yet.`);
|
|
2876
|
+
console.log(` Run ${chalk18.cyan("mindlink profile")} to create one.`);
|
|
2877
|
+
console.log("");
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
console.log("");
|
|
2881
|
+
console.log(readFileSync15(GLOBAL_USER_PROFILE_PATH, "utf8"));
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
ensureProfileExists();
|
|
2885
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "nano";
|
|
2886
|
+
console.log("");
|
|
2887
|
+
console.log(` ${chalk18.dim(`Opening ${GLOBAL_USER_PROFILE_PATH} in ${editor}...`)}`);
|
|
2888
|
+
console.log(` ${chalk18.dim("Run mindlink update after saving to sync to all registered projects.")}`);
|
|
2889
|
+
console.log("");
|
|
2890
|
+
try {
|
|
2891
|
+
execSync4(`${editor} "${GLOBAL_USER_PROFILE_PATH}"`, { stdio: "inherit" });
|
|
2892
|
+
console.log("");
|
|
2893
|
+
console.log(` ${chalk18.green("\u2713")} Profile saved.`);
|
|
2894
|
+
console.log(` ${chalk18.dim("\u2192 Run mindlink update to sync to all registered projects.")}`);
|
|
2895
|
+
console.log("");
|
|
2896
|
+
} catch {
|
|
2897
|
+
console.log("");
|
|
2898
|
+
}
|
|
2899
|
+
});
|
|
2900
|
+
|
|
2901
|
+
// src/commands/prune.ts
|
|
2902
|
+
import { Command as Command18 } from "commander";
|
|
2903
|
+
import { select as select6, isCancel as isCancel8 } from "@clack/prompts";
|
|
2904
|
+
import chalk19 from "chalk";
|
|
2905
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
|
|
2906
|
+
import { join as join18, resolve as resolve15 } from "path";
|
|
2907
|
+
var STALENESS_THRESHOLDS = {
|
|
2908
|
+
"current focus": 14,
|
|
2909
|
+
"decisions": 180,
|
|
2910
|
+
"conventions": 180,
|
|
2911
|
+
"architecture": 365
|
|
2912
|
+
// 'user profile' and 'important context' have no expiry
|
|
2913
|
+
};
|
|
2914
|
+
var TIMESTAMP_RE = /<!--\s*added:\s*(\d{4}-\d{2}-\d{2})\s*-->/;
|
|
2915
|
+
var SECTION_RE = /^(#{1,6})\s+(.+)/;
|
|
2916
|
+
function parseTimestampedEntries(content) {
|
|
2917
|
+
const lines = content.split("\n");
|
|
2918
|
+
const entries = [];
|
|
2919
|
+
let currentSection = "";
|
|
2920
|
+
let currentHeadingLevel = 0;
|
|
2921
|
+
let entryLines = [];
|
|
2922
|
+
let entryStart = -1;
|
|
2923
|
+
function flushEntry(lineEnd) {
|
|
2924
|
+
if (entryStart < 0 || entryLines.length === 0) return;
|
|
2925
|
+
const fullText = entryLines.join("\n");
|
|
2926
|
+
const timestampMatch = fullText.match(TIMESTAMP_RE);
|
|
2927
|
+
if (!timestampMatch) {
|
|
2928
|
+
entryLines = [];
|
|
2929
|
+
entryStart = -1;
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
const addedDate = /* @__PURE__ */ new Date(timestampMatch[1] + "T00:00:00Z");
|
|
2933
|
+
const ageInDays = (Date.now() - addedDate.getTime()) / 864e5;
|
|
2934
|
+
const sectionKey = currentSection.toLowerCase().replace(/\s*<!--.*?-->\s*/g, "").trim();
|
|
2935
|
+
const threshold = STALENESS_THRESHOLDS[sectionKey] ?? Infinity;
|
|
2936
|
+
const isStale = isFinite(threshold) && ageInDays > threshold;
|
|
2937
|
+
const trimmedLines = entryLines.filter((l) => l.trim().length > 0);
|
|
2938
|
+
const displayText = trimmedLines.join("\n");
|
|
2939
|
+
entries.push({
|
|
2940
|
+
section: currentSection.replace(/<!--.*?-->/g, "").trim(),
|
|
2941
|
+
text: displayText,
|
|
2942
|
+
addedDate,
|
|
2943
|
+
lineStart: entryStart,
|
|
2944
|
+
lineEnd,
|
|
2945
|
+
ageInDays,
|
|
2946
|
+
threshold,
|
|
2947
|
+
isStale
|
|
2948
|
+
});
|
|
2949
|
+
entryLines = [];
|
|
2950
|
+
entryStart = -1;
|
|
2951
|
+
}
|
|
2952
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2953
|
+
const line = lines[i];
|
|
2954
|
+
const sectionMatch = line.match(SECTION_RE);
|
|
2955
|
+
if (sectionMatch) {
|
|
2956
|
+
const level = sectionMatch[1].length;
|
|
2957
|
+
const title = sectionMatch[2];
|
|
2958
|
+
flushEntry(i);
|
|
2959
|
+
if (level <= 2) {
|
|
2960
|
+
currentSection = title;
|
|
2961
|
+
currentHeadingLevel = level;
|
|
2962
|
+
}
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
if (line.trim() === "---") {
|
|
2966
|
+
flushEntry(i);
|
|
2967
|
+
continue;
|
|
2968
|
+
}
|
|
2969
|
+
if (line.trim().startsWith("<!--") && line.trim().endsWith("-->")) {
|
|
2970
|
+
if (entryStart < 0) continue;
|
|
2971
|
+
}
|
|
2972
|
+
if (TIMESTAMP_RE.test(line)) {
|
|
2973
|
+
if (entryStart < 0) entryStart = i;
|
|
2974
|
+
entryLines.push(line);
|
|
2975
|
+
flushEntry(i + 1);
|
|
2976
|
+
continue;
|
|
2977
|
+
}
|
|
2978
|
+
if (line.trim().length > 0 && !line.trim().startsWith(">")) {
|
|
2979
|
+
if (entryStart < 0) entryStart = i;
|
|
2980
|
+
entryLines.push(line);
|
|
2981
|
+
} else if (entryStart >= 0) {
|
|
2982
|
+
entryLines.push(line);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
flushEntry(lines.length);
|
|
2986
|
+
return entries;
|
|
2987
|
+
}
|
|
2988
|
+
function removeLines(content, lineStart, lineEnd) {
|
|
2989
|
+
const lines = content.split("\n");
|
|
2990
|
+
lines.splice(lineStart, lineEnd - lineStart);
|
|
2991
|
+
return lines.join("\n");
|
|
2992
|
+
}
|
|
2993
|
+
function appendToArchive(content, entryText, pruneDate) {
|
|
2994
|
+
const archiveHeading = "## Archive";
|
|
2995
|
+
const archiveEntry = `${entryText} <!-- archived: ${pruneDate} -->`;
|
|
2996
|
+
if (content.includes(archiveHeading)) {
|
|
2997
|
+
return content.replace(
|
|
2998
|
+
/(## Archive\n(?:<!--[^>]*-->\n)*)/,
|
|
2999
|
+
`$1
|
|
3000
|
+
${archiveEntry}
|
|
3001
|
+
`
|
|
3002
|
+
);
|
|
3003
|
+
} else {
|
|
3004
|
+
return content.trimEnd() + `
|
|
3005
|
+
|
|
3006
|
+
${archiveHeading}
|
|
3007
|
+
|
|
3008
|
+
<!-- Entries moved here by mindlink prune \u2014 kept for reference -->
|
|
3009
|
+
|
|
3010
|
+
${archiveEntry}
|
|
3011
|
+
`;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
function formatAge(days) {
|
|
3015
|
+
if (days < 30) return `${Math.round(days)} days`;
|
|
3016
|
+
if (days < 365) return `${Math.round(days / 30)} months`;
|
|
3017
|
+
return `${(days / 365).toFixed(1)} years`;
|
|
3018
|
+
}
|
|
3019
|
+
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", `
|
|
3020
|
+
Scans MEMORY.md for entries with <!-- added: YYYY-MM-DD --> timestamps.
|
|
3021
|
+
Entries older than their section's staleness threshold are flagged for review.
|
|
3022
|
+
|
|
3023
|
+
Staleness thresholds:
|
|
3024
|
+
Current Focus \u2014 14 days
|
|
3025
|
+
Decisions \u2014 180 days
|
|
3026
|
+
Conventions \u2014 180 days
|
|
3027
|
+
Architecture \u2014 365 days
|
|
3028
|
+
User Profile \u2014 never expires
|
|
3029
|
+
|
|
3030
|
+
Archived entries are moved to ## Archive at the bottom of MEMORY.md.
|
|
3031
|
+
They are never permanently deleted unless you choose "Delete".
|
|
3032
|
+
|
|
3033
|
+
Examples:
|
|
3034
|
+
mindlink prune
|
|
3035
|
+
mindlink prune --dry-run
|
|
3036
|
+
mindlink prune --all
|
|
3037
|
+
`).action(async (opts) => {
|
|
3038
|
+
const projectPath = resolve15(process.cwd());
|
|
3039
|
+
const brainDir = join18(projectPath, BRAIN_DIR);
|
|
3040
|
+
const memoryPath = join18(brainDir, "MEMORY.md");
|
|
3041
|
+
console.log("");
|
|
3042
|
+
console.log(` ${chalk19.bold("\u25C9 MindLink Prune")}`);
|
|
3043
|
+
console.log(` ${chalk19.dim(memoryPath)}`);
|
|
3044
|
+
console.log("");
|
|
3045
|
+
if (!existsSync18(memoryPath)) {
|
|
3046
|
+
console.log(` ${chalk19.red("\u2717")} MEMORY.md not found. Run ${chalk19.cyan("mindlink init")} first.`);
|
|
3047
|
+
console.log("");
|
|
3048
|
+
process.exit(1);
|
|
3049
|
+
}
|
|
3050
|
+
const allEntries = parseTimestampedEntries(readFileSync16(memoryPath, "utf8"));
|
|
3051
|
+
const toReview = opts.all ? allEntries.filter((e) => e.addedDate !== null) : allEntries.filter((e) => e.isStale);
|
|
3052
|
+
if (toReview.length === 0) {
|
|
3053
|
+
if (allEntries.length === 0) {
|
|
3054
|
+
console.log(` ${chalk19.dim("No timestamped entries found in MEMORY.md.")}`);
|
|
3055
|
+
console.log(` ${chalk19.dim("Entries are timestamped when your AI writes them (<!-- added: YYYY-MM-DD -->).")}`);
|
|
3056
|
+
} else {
|
|
3057
|
+
console.log(` ${chalk19.green("\u2713")} No stale entries found. ${allEntries.length} entry${allEntries.length !== 1 ? "ies" : "y"} all within threshold.`);
|
|
3058
|
+
console.log(` ${chalk19.dim("Run mindlink prune --all to review all timestamped entries.")}`);
|
|
3059
|
+
}
|
|
3060
|
+
console.log("");
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
console.log(` Scanning MEMORY.md for ${opts.all ? "timestamped" : "stale"} entries...`);
|
|
3064
|
+
console.log(` Found ${toReview.length} entr${toReview.length !== 1 ? "ies" : "y"} to review.`);
|
|
3065
|
+
console.log("");
|
|
3066
|
+
if (opts.dryRun) {
|
|
3067
|
+
for (const entry of toReview) {
|
|
3068
|
+
const age = entry.ageInDays !== null ? formatAge(entry.ageInDays) : "undated";
|
|
3069
|
+
const thresholdStr = isFinite(entry.threshold) ? ` \u2014 threshold: ${entry.threshold} days` : "";
|
|
3070
|
+
console.log(` ${chalk19.yellow("\u26A0")} [${entry.section}] ${entry.text.split("\n")[0].slice(0, 80)}`);
|
|
3071
|
+
console.log(` ${chalk19.dim(`Added: ${entry.addedDate?.toISOString().slice(0, 10) ?? "unknown"} (${age} ago)${thresholdStr}`)}`);
|
|
3072
|
+
console.log("");
|
|
3073
|
+
}
|
|
3074
|
+
console.log(` ${chalk19.dim("Dry run \u2014 no changes made.")}`);
|
|
3075
|
+
console.log("");
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
const pruneDate = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3079
|
+
let content = readFileSync16(memoryPath, "utf8");
|
|
3080
|
+
let archived = 0;
|
|
3081
|
+
let deleted = 0;
|
|
3082
|
+
let kept = 0;
|
|
3083
|
+
let skippedAll = false;
|
|
3084
|
+
const sorted = [...toReview].sort((a, b) => b.lineStart - a.lineStart);
|
|
3085
|
+
for (const entry of sorted) {
|
|
3086
|
+
if (skippedAll) {
|
|
3087
|
+
kept++;
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
const age = entry.ageInDays !== null ? formatAge(entry.ageInDays) : "undated";
|
|
3091
|
+
const thresholdStr = isFinite(entry.threshold) ? ` \u2014 threshold: ${entry.threshold} days` : "";
|
|
3092
|
+
const displayLines = entry.text.split("\n").slice(0, 3).join("\n");
|
|
3093
|
+
console.log(` ${chalk19.bold("\u2500".repeat(55))}`);
|
|
3094
|
+
console.log(` ${chalk19.bold("Section:")} ${entry.section}`);
|
|
3095
|
+
console.log(` ${chalk19.bold("Entry:")}`);
|
|
3096
|
+
for (const l of displayLines.split("\n")) console.log(` ${chalk19.dim(l)}`);
|
|
3097
|
+
console.log(` ${chalk19.bold("Added:")} ${entry.addedDate?.toISOString().slice(0, 10) ?? "unknown"} (${age} ago)${thresholdStr}`);
|
|
3098
|
+
console.log("");
|
|
3099
|
+
const action = await select6({
|
|
3100
|
+
message: "What would you like to do?",
|
|
3101
|
+
options: [
|
|
3102
|
+
{ value: "keep", label: "Keep", hint: "leave as-is" },
|
|
3103
|
+
{ value: "archive", label: "Archive", hint: "move to ## Archive section" },
|
|
3104
|
+
{ value: "delete", label: "Delete", hint: "remove permanently" },
|
|
3105
|
+
{ value: "skip_all", label: "Skip remaining", hint: "keep all remaining entries unchanged" }
|
|
3106
|
+
]
|
|
3107
|
+
});
|
|
3108
|
+
if (isCancel8(action)) {
|
|
3109
|
+
console.log("");
|
|
3110
|
+
break;
|
|
3111
|
+
}
|
|
3112
|
+
if (action === "skip_all") {
|
|
3113
|
+
skippedAll = true;
|
|
3114
|
+
kept++;
|
|
3115
|
+
continue;
|
|
3116
|
+
}
|
|
3117
|
+
if (action === "archive") {
|
|
3118
|
+
const entryText = content.split("\n").slice(entry.lineStart, entry.lineEnd).join("\n").trim();
|
|
3119
|
+
content = removeLines(content, entry.lineStart, entry.lineEnd);
|
|
3120
|
+
content = appendToArchive(content, entryText, pruneDate);
|
|
3121
|
+
archived++;
|
|
3122
|
+
} else if (action === "delete") {
|
|
3123
|
+
content = removeLines(content, entry.lineStart, entry.lineEnd);
|
|
3124
|
+
deleted++;
|
|
3125
|
+
} else {
|
|
3126
|
+
kept++;
|
|
3127
|
+
}
|
|
3128
|
+
console.log("");
|
|
3129
|
+
}
|
|
3130
|
+
if (archived > 0 || deleted > 0) {
|
|
3131
|
+
writeFileSync9(memoryPath, content);
|
|
3132
|
+
}
|
|
3133
|
+
console.log(` ${chalk19.bold("\u2500".repeat(55))}`);
|
|
3134
|
+
if (archived > 0) console.log(` ${chalk19.green("\u2713")} ${archived} entry${archived !== 1 ? "ies" : ""} archived`);
|
|
3135
|
+
if (deleted > 0) console.log(` ${chalk19.green("\u2713")} ${deleted} entry${deleted !== 1 ? "ies" : ""} deleted`);
|
|
3136
|
+
if (kept > 0) console.log(` ${chalk19.dim("\xB7")} ${kept} entry${kept !== 1 ? "ies" : ""} kept`);
|
|
3137
|
+
if (archived > 0 || deleted > 0) {
|
|
3138
|
+
console.log(` ${chalk19.green("\u2713")} MEMORY.md updated.`);
|
|
3139
|
+
}
|
|
3140
|
+
console.log("");
|
|
3141
|
+
});
|
|
3142
|
+
|
|
3143
|
+
// src/commands/mcp.ts
|
|
3144
|
+
import { Command as Command19 } from "commander";
|
|
3145
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
|
|
3146
|
+
import { join as join19, resolve as resolve16, dirname as dirname6 } from "path";
|
|
3147
|
+
function resolveProjectPath() {
|
|
3148
|
+
const envPath = process.env.MINDLINK_PROJECT_PATH;
|
|
3149
|
+
if (envPath && existsSync19(join19(envPath, BRAIN_DIR))) return envPath;
|
|
3150
|
+
let current = resolve16(process.cwd());
|
|
3151
|
+
for (let i = 0; i < 10; i++) {
|
|
3152
|
+
if (existsSync19(join19(current, BRAIN_DIR))) return current;
|
|
3153
|
+
const parent = dirname6(current);
|
|
3154
|
+
if (parent === current) break;
|
|
3155
|
+
current = parent;
|
|
3156
|
+
}
|
|
3157
|
+
return null;
|
|
3158
|
+
}
|
|
3159
|
+
function readMemorySection(memoryPath, section) {
|
|
3160
|
+
const content = readFileSync17(memoryPath, "utf8");
|
|
3161
|
+
if (!section) {
|
|
3162
|
+
const core = extractSection(content, "Core");
|
|
3163
|
+
const profile = extractSection(content, "User Profile");
|
|
3164
|
+
return `## Core
|
|
3165
|
+
|
|
3166
|
+
${core}
|
|
3167
|
+
|
|
3168
|
+
## User Profile
|
|
3169
|
+
|
|
3170
|
+
${profile}`.trim();
|
|
3171
|
+
}
|
|
3172
|
+
return extractSection(content, section);
|
|
3173
|
+
}
|
|
3174
|
+
function appendToSection(memoryPath, section, newContent) {
|
|
3175
|
+
let content = readFileSync17(memoryPath, "utf8");
|
|
3176
|
+
const sectionBody = extractSection(content, section);
|
|
3177
|
+
const lines = content.split("\n");
|
|
3178
|
+
let headingIdx = -1;
|
|
3179
|
+
let nextSectionIdx = lines.length;
|
|
3180
|
+
let headingLevel = 0;
|
|
3181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3182
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)/);
|
|
3183
|
+
if (match) {
|
|
3184
|
+
const level = match[1].length;
|
|
3185
|
+
const title = match[2].replace(/<!--.*?-->/g, "").trim();
|
|
3186
|
+
if (headingIdx < 0 && title.toLowerCase() === section.toLowerCase()) {
|
|
3187
|
+
headingIdx = i;
|
|
3188
|
+
headingLevel = level;
|
|
3189
|
+
} else if (headingIdx >= 0 && level <= headingLevel) {
|
|
3190
|
+
nextSectionIdx = i;
|
|
3191
|
+
break;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
if (headingIdx < 0) {
|
|
3196
|
+
content = content.trimEnd() + `
|
|
3197
|
+
|
|
3198
|
+
## ${section}
|
|
3199
|
+
|
|
3200
|
+
${newContent.trim()}
|
|
3201
|
+
`;
|
|
3202
|
+
} else {
|
|
3203
|
+
const insertAt = nextSectionIdx > 0 && lines[nextSectionIdx - 1].trim() === "---" ? nextSectionIdx - 1 : nextSectionIdx;
|
|
3204
|
+
lines.splice(insertAt, 0, "", newContent.trim(), "");
|
|
3205
|
+
content = lines.join("\n");
|
|
3206
|
+
}
|
|
3207
|
+
writeFileSync10(memoryPath, content);
|
|
3208
|
+
void sectionBody;
|
|
3209
|
+
}
|
|
3210
|
+
var mcpCommand = new Command19("mcp").description("Start the MindLink MCP server (stdio transport for AI tool integration)").addHelpText("after", `
|
|
3211
|
+
The MCP server runs as a local process launched by Claude Code.
|
|
3212
|
+
It exposes 4 tools for auditable, schema-validated memory reads and writes.
|
|
3213
|
+
|
|
3214
|
+
Tools:
|
|
3215
|
+
mindlink_read_memory(section?) \u2014 read a section of MEMORY.md
|
|
3216
|
+
mindlink_write_memory(section, content) \u2014 append to a MEMORY.md section
|
|
3217
|
+
mindlink_session_update(summary) \u2014 overwrite SESSION.md
|
|
3218
|
+
mindlink_verify() \u2014 run health check, return JSON
|
|
3219
|
+
|
|
3220
|
+
Configure in .claude/settings.json (done automatically by mindlink init):
|
|
3221
|
+
{ "mcpServers": { "mindlink": { "command": "mindlink", "args": ["mcp"] } } }
|
|
3222
|
+
|
|
3223
|
+
Examples:
|
|
3224
|
+
mindlink mcp # (launched by Claude Code, not by hand)
|
|
3225
|
+
`).action(async () => {
|
|
3226
|
+
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
3227
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3228
|
+
const { z } = await import("./zod-FDOV7Y2P.js");
|
|
3229
|
+
const server = new McpServer({ name: "mindlink", version: VERSION });
|
|
3230
|
+
server.tool(
|
|
3231
|
+
"mindlink_read_memory",
|
|
3232
|
+
"Read a section of this project's MEMORY.md. If section is omitted, returns Core + User Profile only.",
|
|
3233
|
+
{
|
|
3234
|
+
section: z.enum(["Core", "Architecture", "Decisions", "Conventions", "User Profile", "Important Context"]).optional().describe("Section to read. Omit for Core + User Profile (recommended default).")
|
|
3235
|
+
},
|
|
3236
|
+
async ({ section }) => {
|
|
3237
|
+
const projectPath = resolveProjectPath();
|
|
3238
|
+
if (!projectPath) {
|
|
3239
|
+
return {
|
|
3240
|
+
content: [{ type: "text", text: "Error: No MindLink project found at this path. Run mindlink init first." }],
|
|
3241
|
+
isError: true
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
const memoryPath = join19(projectPath, BRAIN_DIR, "MEMORY.md");
|
|
3245
|
+
if (!existsSync19(memoryPath)) {
|
|
3246
|
+
return {
|
|
3247
|
+
content: [{ type: "text", text: "Error: MEMORY.md not found. Run mindlink init to create it." }],
|
|
3248
|
+
isError: true
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
try {
|
|
3252
|
+
const content = readMemorySection(memoryPath, section);
|
|
3253
|
+
return { content: [{ type: "text", text: content || "(section is empty)" }] };
|
|
3254
|
+
} catch (err) {
|
|
3255
|
+
return {
|
|
3256
|
+
content: [{ type: "text", text: `Error reading MEMORY.md: ${err instanceof Error ? err.message : err}` }],
|
|
3257
|
+
isError: true
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
);
|
|
3262
|
+
server.tool(
|
|
3263
|
+
"mindlink_write_memory",
|
|
3264
|
+
"Append a fact or decision to a section of MEMORY.md. Never overwrites existing content. Always include a <!-- added: YYYY-MM-DD --> timestamp.",
|
|
3265
|
+
{
|
|
3266
|
+
section: z.enum(["Core", "Architecture", "Decisions", "Conventions", "User Profile", "Important Context"]).describe("Section to append to."),
|
|
3267
|
+
content: z.string().describe("The markdown content to append. Include <!-- added: YYYY-MM-DD --> timestamp.")
|
|
3268
|
+
},
|
|
3269
|
+
async ({ section, content }) => {
|
|
3270
|
+
const projectPath = resolveProjectPath();
|
|
3271
|
+
if (!projectPath) {
|
|
3272
|
+
return {
|
|
3273
|
+
content: [{ type: "text", text: "Error: No MindLink project found. Run mindlink init first." }],
|
|
3274
|
+
isError: true
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
const memoryPath = join19(projectPath, BRAIN_DIR, "MEMORY.md");
|
|
3278
|
+
if (!existsSync19(memoryPath)) {
|
|
3279
|
+
return {
|
|
3280
|
+
content: [{ type: "text", text: "Error: MEMORY.md not found. Run mindlink init to create it." }],
|
|
3281
|
+
isError: true
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
try {
|
|
3285
|
+
appendToSection(memoryPath, section, content);
|
|
3286
|
+
return { content: [{ type: "text", text: `\u2713 Appended to ## ${section} in MEMORY.md.` }] };
|
|
3287
|
+
} catch (err) {
|
|
3288
|
+
return {
|
|
3289
|
+
content: [{ type: "text", text: `Error writing MEMORY.md: ${err instanceof Error ? err.message : err}` }],
|
|
3290
|
+
isError: true
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
);
|
|
3295
|
+
server.tool(
|
|
3296
|
+
"mindlink_session_update",
|
|
3297
|
+
"Update SESSION.md with the current session summary. Call this as the last action of every response.",
|
|
3298
|
+
{
|
|
3299
|
+
summary: z.string().describe("Current task state \u2014 what was asked, what you answered, any decisions made, what's next.")
|
|
3300
|
+
},
|
|
3301
|
+
async ({ summary }) => {
|
|
3302
|
+
const projectPath = resolveProjectPath();
|
|
3303
|
+
if (!projectPath) {
|
|
3304
|
+
return {
|
|
3305
|
+
content: [{ type: "text", text: "Error: No MindLink project found. Run mindlink init first." }],
|
|
3306
|
+
isError: true
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
const sessionPath = join19(projectPath, BRAIN_DIR, "SESSION.md");
|
|
3310
|
+
try {
|
|
3311
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3312
|
+
const content = `# Session State
|
|
3313
|
+
|
|
3314
|
+
<!-- Last updated: ${date} -->
|
|
3315
|
+
|
|
3316
|
+
${summary.trim()}
|
|
3317
|
+
`;
|
|
3318
|
+
writeFileSync10(sessionPath, content);
|
|
3319
|
+
return { content: [{ type: "text", text: "\u2713 SESSION.md updated." }] };
|
|
3320
|
+
} catch (err) {
|
|
3321
|
+
return {
|
|
3322
|
+
content: [{ type: "text", text: `Error writing SESSION.md: ${err instanceof Error ? err.message : err}` }],
|
|
3323
|
+
isError: true
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
);
|
|
3328
|
+
server.tool(
|
|
3329
|
+
"mindlink_verify",
|
|
3330
|
+
"Run a health check on .brain/. Returns pass/warn/fail status for each check. Call this to verify your memory writes succeeded.",
|
|
3331
|
+
{},
|
|
3332
|
+
async () => {
|
|
3333
|
+
const projectPath = resolveProjectPath();
|
|
3334
|
+
if (!projectPath) {
|
|
3335
|
+
return {
|
|
3336
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: "No MindLink project found. Run mindlink init first." }) }],
|
|
3337
|
+
isError: true
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
const checks = runChecks(projectPath);
|
|
3341
|
+
const ok2 = checks.every((c) => c.status === "pass");
|
|
3342
|
+
return {
|
|
3343
|
+
content: [{ type: "text", text: JSON.stringify({ ok: ok2, checks }, null, 2) }]
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
);
|
|
3347
|
+
const transport = new StdioServerTransport();
|
|
3348
|
+
await server.connect(transport);
|
|
3349
|
+
});
|
|
3350
|
+
|
|
2241
3351
|
// src/cli.ts
|
|
2242
|
-
var program = new
|
|
3352
|
+
var program = new Command20();
|
|
2243
3353
|
program.name("mindlink").description("Give your AI a brain.").version(VERSION, "-v, --version");
|
|
2244
3354
|
program.addCommand(initCommand);
|
|
2245
3355
|
program.addCommand(statusCommand);
|
|
@@ -2256,9 +3366,13 @@ program.addCommand(importCommand);
|
|
|
2256
3366
|
program.addCommand(doctorCommand);
|
|
2257
3367
|
program.addCommand(versionCommand);
|
|
2258
3368
|
program.addCommand(diffCommand);
|
|
3369
|
+
program.addCommand(verifyCommand);
|
|
3370
|
+
program.addCommand(profileCommand);
|
|
3371
|
+
program.addCommand(pruneCommand);
|
|
3372
|
+
program.addCommand(mcpCommand);
|
|
2259
3373
|
program.on("command:*", (operands) => {
|
|
2260
3374
|
const unknown = operands[0];
|
|
2261
|
-
const known = ["init", "status", "log", "clear", "reset", "config", "sync", "update", "summary", "uninstall", "export", "import", "doctor", "version", "diff"];
|
|
3375
|
+
const known = ["init", "status", "log", "clear", "reset", "config", "sync", "update", "summary", "uninstall", "export", "import", "doctor", "version", "diff", "verify", "profile", "prune", "mcp"];
|
|
2262
3376
|
function levenshtein(a, b) {
|
|
2263
3377
|
const m = a.length, n = b.length;
|
|
2264
3378
|
const dp = Array.from(
|
|
@@ -2274,11 +3388,11 @@ program.on("command:*", (operands) => {
|
|
|
2274
3388
|
}
|
|
2275
3389
|
const closest = known.map((cmd) => ({ cmd, dist: levenshtein(unknown, cmd) })).sort((a, b) => a.dist - b.dist)[0];
|
|
2276
3390
|
console.log("");
|
|
2277
|
-
console.log(` ${
|
|
3391
|
+
console.log(` ${chalk20.red("\u2717")} Unknown command: ${chalk20.bold(unknown)}`);
|
|
2278
3392
|
if (closest && closest.dist <= 3) {
|
|
2279
|
-
console.log(` Did you mean ${
|
|
3393
|
+
console.log(` Did you mean ${chalk20.cyan("mindlink " + closest.cmd)}?`);
|
|
2280
3394
|
}
|
|
2281
|
-
console.log(` Run ${
|
|
3395
|
+
console.log(` Run ${chalk20.cyan("mindlink --help")} to see all commands.`);
|
|
2282
3396
|
console.log("");
|
|
2283
3397
|
process.exit(1);
|
|
2284
3398
|
});
|