mindlink 2.0.3 → 2.1.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 +3 -3
- package/dist/cli.js +485 -21
- package/dist/cli.js.map +1 -1
- package/dist/templates/hooks/claude-settings.json +1 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,9 +22,9 @@ Git gave every developer a shared version history. MindLink gives your AI team a
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
> ### ◉ Latest — v2.0
|
|
26
|
-
> **
|
|
27
|
-
> [→ Full release notes](https://github.com/404-not-found/mindlink/releases/tag/v2.0
|
|
25
|
+
> ### ◉ Latest — v2.1.0
|
|
26
|
+
> **Smart init from git history · Stack-aware templates · `mindlink recap` · `mindlink search`**
|
|
27
|
+
> [→ Full release notes](https://github.com/404-not-found/mindlink/releases/tag/v2.1.0)
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import "./chunk-2H7UOFLK.js";
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { Command as
|
|
6
|
-
import
|
|
5
|
+
import { Command as Command22 } from "commander";
|
|
6
|
+
import chalk22 from "chalk";
|
|
7
7
|
|
|
8
8
|
// src/utils/version.ts
|
|
9
|
-
var VERSION = "2.0
|
|
9
|
+
var VERSION = "2.1.0";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { Command } from "commander";
|
|
@@ -45,6 +45,7 @@ var BRAIN_DIR = ".brain";
|
|
|
45
45
|
var GLOBAL_MINDLINK_DIR = join(homedir(), ".mindlink");
|
|
46
46
|
var GLOBAL_USER_PROFILE_PATH = join(GLOBAL_MINDLINK_DIR, "USER.md");
|
|
47
47
|
var GLOBAL_WINDSURF_MCP_PATH = join(homedir(), ".codeium", "windsurf", "mcp_config.json");
|
|
48
|
+
var GLOBAL_CLINE_MCP_PATH = join(homedir(), ".cline", "data", "settings", "cline_mcp_settings.json");
|
|
48
49
|
|
|
49
50
|
// src/utils/banner.ts
|
|
50
51
|
import chalk from "chalk";
|
|
@@ -227,7 +228,113 @@ function detectProjectInfo(projectPath) {
|
|
|
227
228
|
if (dirs.length > 0) topDirs = dirs.join(", ");
|
|
228
229
|
} catch {
|
|
229
230
|
}
|
|
230
|
-
|
|
231
|
+
const git = analyzeGitHistory(projectPath);
|
|
232
|
+
return { name, description, stack, recentActivity, topDirs, date, git };
|
|
233
|
+
}
|
|
234
|
+
function analyzeGitHistory(projectPath) {
|
|
235
|
+
const exec = (cmd) => execSync(cmd, { cwd: projectPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
236
|
+
try {
|
|
237
|
+
exec("git rev-parse HEAD");
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
let recentWork = "";
|
|
242
|
+
let topModules = "";
|
|
243
|
+
let topDecisions = "";
|
|
244
|
+
let isTeam = false;
|
|
245
|
+
let projectAge = "";
|
|
246
|
+
try {
|
|
247
|
+
const messages = exec('git log --pretty=format:"%s" -30').split("\n").map((m) => m.trim()).filter((m) => m.length > 0 && m.length < 120);
|
|
248
|
+
if (messages.length > 0) {
|
|
249
|
+
recentWork = messages.slice(0, 8).join("\n");
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const diffstat = exec("git log --name-only --pretty=format: -30").split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
255
|
+
const counts = {};
|
|
256
|
+
for (const file of diffstat) {
|
|
257
|
+
const parts = file.split("/");
|
|
258
|
+
const key = parts.length > 1 ? parts[0] + "/" : file;
|
|
259
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
260
|
+
}
|
|
261
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k]) => k);
|
|
262
|
+
if (sorted.length > 0) topModules = sorted.join(", ");
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const authors = exec('git log --pretty=format:"%ae" -30').split("\n").map((e) => e.trim()).filter((e) => e.length > 0);
|
|
267
|
+
const unique = new Set(authors);
|
|
268
|
+
isTeam = unique.size > 1;
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const firstDate = exec('git log --pretty=format:"%ad" --date=short | tail -1');
|
|
273
|
+
if (firstDate) {
|
|
274
|
+
const ms = Date.now() - new Date(firstDate).getTime();
|
|
275
|
+
const days = Math.floor(ms / 864e5);
|
|
276
|
+
if (days < 7) projectAge = `${days}d old`;
|
|
277
|
+
else if (days < 30) projectAge = `${Math.floor(days / 7)}w old`;
|
|
278
|
+
else if (days < 365) projectAge = `${Math.floor(days / 30)}mo old`;
|
|
279
|
+
else projectAge = `${Math.floor(days / 365)}y old`;
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const messages = exec('git log --pretty=format:"%s" -50').split("\n").map((m) => m.trim());
|
|
285
|
+
const decisionKeywords = ["add", "switch", "replace", "migrate", "refactor", "remove", "use ", "adopt", "drop", "move"];
|
|
286
|
+
const decisions = messages.filter((m) => decisionKeywords.some((k) => m.toLowerCase().startsWith(k))).slice(0, 4).map((m) => m.slice(0, 80));
|
|
287
|
+
if (decisions.length > 0) topDecisions = decisions.join("\n");
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
if (!recentWork && !topModules && !projectAge) return null;
|
|
291
|
+
return { recentWork, topModules, topDecisions, isTeam, projectAge };
|
|
292
|
+
}
|
|
293
|
+
function buildStackHints(stack) {
|
|
294
|
+
const s = stack.toLowerCase();
|
|
295
|
+
if (s.includes("next.js") || s.includes("nextjs")) {
|
|
296
|
+
return `Pages: app/ or pages/ router
|
|
297
|
+
API routes: app/api/ or pages/api/
|
|
298
|
+
Components: components/
|
|
299
|
+
State: (fill in \u2014 context, zustand, redux?)`;
|
|
300
|
+
}
|
|
301
|
+
if (s.includes("react") && !s.includes("next")) {
|
|
302
|
+
return `Components: src/components/
|
|
303
|
+
State: (fill in \u2014 context, zustand, redux?)
|
|
304
|
+
Routing: (fill in \u2014 react-router, tanstack?)`;
|
|
305
|
+
}
|
|
306
|
+
if (s.includes("express") || s.includes("fastify") || s.includes("koa")) {
|
|
307
|
+
return `Entry: src/index.ts or server.ts
|
|
308
|
+
Routes: src/routes/
|
|
309
|
+
Middleware: src/middleware/
|
|
310
|
+
DB: (fill in)`;
|
|
311
|
+
}
|
|
312
|
+
if (s.includes("python")) {
|
|
313
|
+
return `Entry: main.py or src/
|
|
314
|
+
Dependencies: requirements.txt or pyproject.toml
|
|
315
|
+
Virtual env: .venv/ or venv/`;
|
|
316
|
+
}
|
|
317
|
+
if (s.includes("rust")) {
|
|
318
|
+
return `Entry: src/main.rs or src/lib.rs
|
|
319
|
+
Key crates: (fill in from Cargo.toml)
|
|
320
|
+
Modules: src/`;
|
|
321
|
+
}
|
|
322
|
+
if (s.includes("go")) {
|
|
323
|
+
return `Entry: main.go or cmd/
|
|
324
|
+
Packages: internal/ or pkg/
|
|
325
|
+
Dependencies: go.mod`;
|
|
326
|
+
}
|
|
327
|
+
if (s.includes("java") || s.includes("kotlin")) {
|
|
328
|
+
return `Entry: src/main/
|
|
329
|
+
Tests: src/test/
|
|
330
|
+
Build: pom.xml or build.gradle`;
|
|
331
|
+
}
|
|
332
|
+
if (s.includes("ruby")) {
|
|
333
|
+
return `Entry: app/ (Rails) or lib/
|
|
334
|
+
Dependencies: Gemfile
|
|
335
|
+
Tests: spec/ or test/`;
|
|
336
|
+
}
|
|
337
|
+
return "";
|
|
231
338
|
}
|
|
232
339
|
function memoryHasRealContent(memoryPath) {
|
|
233
340
|
try {
|
|
@@ -294,7 +401,14 @@ ${info2.stack}
|
|
|
294
401
|
}
|
|
295
402
|
const focusLines = [];
|
|
296
403
|
if (info2.topDirs) focusLines.push(`Directories: ${info2.topDirs}`);
|
|
297
|
-
if (info2.
|
|
404
|
+
if (info2.git?.recentWork) {
|
|
405
|
+
const firstLine = info2.git.recentWork.split("\n")[0];
|
|
406
|
+
focusLines.push(`Recent work: ${firstLine}`);
|
|
407
|
+
} else if (info2.recentActivity) {
|
|
408
|
+
focusLines.push(`Recent commits: ${info2.recentActivity}`);
|
|
409
|
+
}
|
|
410
|
+
if (info2.git?.projectAge) focusLines.push(`Project age: ${info2.git.projectAge}`);
|
|
411
|
+
if (info2.git?.isTeam) focusLines.push(`Team project: multiple contributors detected`);
|
|
298
412
|
const focusBlock = focusLines.length > 0 ? focusLines.join("\n") + `
|
|
299
413
|
<!-- 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 -->`;
|
|
300
414
|
content = content.replace(
|
|
@@ -302,6 +416,28 @@ ${info2.stack}
|
|
|
302
416
|
`### Current focus
|
|
303
417
|
${focusBlock}`
|
|
304
418
|
);
|
|
419
|
+
if (info2.git?.topDecisions) {
|
|
420
|
+
const decisionLines = info2.git.topDecisions.split("\n").map((d) => `- ${d} <!-- inferred from git history \u2014 verify and update -->`).join("\n");
|
|
421
|
+
content = content.replace(
|
|
422
|
+
/### Top decisions\n<!--[^]*?-->/,
|
|
423
|
+
`### Top decisions
|
|
424
|
+
${decisionLines}
|
|
425
|
+
<!-- The 3\u20135 most important locked-in choices. One line each. -->`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const stackHints = buildStackHints(info2.stack);
|
|
429
|
+
const archLines = [];
|
|
430
|
+
if (info2.git?.topModules) archLines.push(`Most active modules (from git history): ${info2.git.topModules}`);
|
|
431
|
+
if (stackHints) archLines.push(stackHints);
|
|
432
|
+
if (archLines.length > 0) {
|
|
433
|
+
content = content.replace(
|
|
434
|
+
/## Architecture[^\n]*\n\n<!--[^]*?-->/,
|
|
435
|
+
`## Architecture <!-- Read when the task involves project structure -->
|
|
436
|
+
|
|
437
|
+
` + archLines.join("\n") + `
|
|
438
|
+
<!-- High-level structure: main components and how they relate. Update when the structure changes. -->`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
305
441
|
return content;
|
|
306
442
|
}
|
|
307
443
|
var BRAIN_FILES = [
|
|
@@ -385,6 +521,32 @@ Examples:
|
|
|
385
521
|
restored.push(".mcp.json");
|
|
386
522
|
}
|
|
387
523
|
}
|
|
524
|
+
if (toRestore.includes("cline")) {
|
|
525
|
+
try {
|
|
526
|
+
mkdirSync2(dirname2(GLOBAL_CLINE_MCP_PATH), { recursive: true });
|
|
527
|
+
let existingCline = {};
|
|
528
|
+
if (existsSync2(GLOBAL_CLINE_MCP_PATH)) {
|
|
529
|
+
try {
|
|
530
|
+
existingCline = JSON.parse(readFileSync2(GLOBAL_CLINE_MCP_PATH, "utf8"));
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const mergedCline = {
|
|
535
|
+
...existingCline,
|
|
536
|
+
mcpServers: {
|
|
537
|
+
...typeof existingCline.mcpServers === "object" && existingCline.mcpServers !== null ? existingCline.mcpServers : {},
|
|
538
|
+
mindlink: {
|
|
539
|
+
command: "mindlink",
|
|
540
|
+
args: ["mcp"],
|
|
541
|
+
alwaysAllow: ["mindlink_read_memory", "mindlink_write_memory", "mindlink_session_update", "mindlink_verify"]
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
writeFileSync2(GLOBAL_CLINE_MCP_PATH, JSON.stringify(mergedCline, null, 2));
|
|
546
|
+
restored.push("~/.cline/data/settings/cline_mcp_settings.json");
|
|
547
|
+
} catch {
|
|
548
|
+
}
|
|
549
|
+
}
|
|
388
550
|
const configPath = join3(brainDir, "config.json");
|
|
389
551
|
if (!existsSync2(configPath)) {
|
|
390
552
|
const config = {
|
|
@@ -637,6 +799,32 @@ Examples:
|
|
|
637
799
|
created.push(`.kiro/settings/mcp.json${" ".repeat(13)} ${chalk2.dim("Kiro MCP server")}`);
|
|
638
800
|
}
|
|
639
801
|
}
|
|
802
|
+
if (selectedAgents.includes("cline")) {
|
|
803
|
+
try {
|
|
804
|
+
mkdirSync2(dirname2(GLOBAL_CLINE_MCP_PATH), { recursive: true });
|
|
805
|
+
let existingCline = {};
|
|
806
|
+
if (existsSync2(GLOBAL_CLINE_MCP_PATH)) {
|
|
807
|
+
try {
|
|
808
|
+
existingCline = JSON.parse(readFileSync2(GLOBAL_CLINE_MCP_PATH, "utf8"));
|
|
809
|
+
} catch {
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const mergedCline = {
|
|
813
|
+
...existingCline,
|
|
814
|
+
mcpServers: {
|
|
815
|
+
...typeof existingCline.mcpServers === "object" && existingCline.mcpServers !== null ? existingCline.mcpServers : {},
|
|
816
|
+
mindlink: {
|
|
817
|
+
command: "mindlink",
|
|
818
|
+
args: ["mcp"],
|
|
819
|
+
alwaysAllow: ["mindlink_read_memory", "mindlink_write_memory", "mindlink_session_update", "mindlink_verify"]
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
writeFileSync2(GLOBAL_CLINE_MCP_PATH, JSON.stringify(mergedCline, null, 2));
|
|
824
|
+
created.push(`~/.cline/data/settings/cline_mcp_settings.json ${chalk2.dim("Cline MCP server (global, pre-authorized)")}`);
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
827
|
+
}
|
|
640
828
|
if (selectedAgents.includes("windsurf")) {
|
|
641
829
|
try {
|
|
642
830
|
mkdirSync2(dirname2(GLOBAL_WINDSURF_MCP_PATH), { recursive: true });
|
|
@@ -689,11 +877,6 @@ Examples:
|
|
|
689
877
|
console.log("");
|
|
690
878
|
for (const err of errors) console.log(` ${chalk2.red("\u2717")} ${err}`);
|
|
691
879
|
}
|
|
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
880
|
if (!existsSync2(GLOBAL_USER_PROFILE_PATH)) {
|
|
698
881
|
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
882
|
console.log("");
|
|
@@ -734,7 +917,8 @@ function extractSection(markdown, heading) {
|
|
|
734
917
|
if (match) {
|
|
735
918
|
const level = match[1].length;
|
|
736
919
|
const title = match[2].trim();
|
|
737
|
-
|
|
920
|
+
const cleanTitle = title.replace(/<!--.*?-->/g, "").trim();
|
|
921
|
+
if (cleanTitle.toLowerCase() === heading.toLowerCase()) {
|
|
738
922
|
inSection = true;
|
|
739
923
|
headingLevel = level;
|
|
740
924
|
continue;
|
|
@@ -1399,7 +1583,7 @@ var REQUIRED_BRAIN_FILES = ["MEMORY.md", "SESSION.md", "SHARED.md", "LOG.md"];
|
|
|
1399
1583
|
async function latestVersion() {
|
|
1400
1584
|
try {
|
|
1401
1585
|
const { default: https } = await import("https");
|
|
1402
|
-
return new Promise((
|
|
1586
|
+
return new Promise((resolve19) => {
|
|
1403
1587
|
const req = https.get(
|
|
1404
1588
|
"https://registry.npmjs.org/mindlink/latest",
|
|
1405
1589
|
{ headers: { "User-Agent": "mindlink-cli" } },
|
|
@@ -1411,17 +1595,17 @@ async function latestVersion() {
|
|
|
1411
1595
|
res.on("end", () => {
|
|
1412
1596
|
try {
|
|
1413
1597
|
const parsed = JSON.parse(data);
|
|
1414
|
-
|
|
1598
|
+
resolve19(parsed.version ?? null);
|
|
1415
1599
|
} catch {
|
|
1416
|
-
|
|
1600
|
+
resolve19(null);
|
|
1417
1601
|
}
|
|
1418
1602
|
});
|
|
1419
1603
|
}
|
|
1420
1604
|
);
|
|
1421
|
-
req.on("error", () =>
|
|
1605
|
+
req.on("error", () => resolve19(null));
|
|
1422
1606
|
req.setTimeout(8e3, () => {
|
|
1423
1607
|
req.destroy();
|
|
1424
|
-
|
|
1608
|
+
resolve19(null);
|
|
1425
1609
|
});
|
|
1426
1610
|
});
|
|
1427
1611
|
} catch {
|
|
@@ -1698,6 +1882,32 @@ Examples:
|
|
|
1698
1882
|
} catch {
|
|
1699
1883
|
}
|
|
1700
1884
|
}
|
|
1885
|
+
if (agentValues.includes("cline")) {
|
|
1886
|
+
try {
|
|
1887
|
+
mkdirSync4(dirname4(GLOBAL_CLINE_MCP_PATH), { recursive: true });
|
|
1888
|
+
let existing = {};
|
|
1889
|
+
if (existsSync9(GLOBAL_CLINE_MCP_PATH)) {
|
|
1890
|
+
try {
|
|
1891
|
+
existing = JSON.parse(readFileSync9(GLOBAL_CLINE_MCP_PATH, "utf8"));
|
|
1892
|
+
} catch {
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
const merged = {
|
|
1896
|
+
...existing,
|
|
1897
|
+
mcpServers: {
|
|
1898
|
+
...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
|
|
1899
|
+
mindlink: {
|
|
1900
|
+
command: "mindlink",
|
|
1901
|
+
args: ["mcp"],
|
|
1902
|
+
alwaysAllow: ["mindlink_read_memory", "mindlink_write_memory", "mindlink_session_update", "mindlink_verify"]
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
writeFileSync6(GLOBAL_CLINE_MCP_PATH, JSON.stringify(merged, null, 2));
|
|
1907
|
+
refreshed.push("~/.cline/data/settings/cline_mcp_settings.json");
|
|
1908
|
+
} catch {
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1701
1911
|
const memoryPath = join10(projectPath, BRAIN_DIR, "MEMORY.md");
|
|
1702
1912
|
if (existsSync9(memoryPath)) {
|
|
1703
1913
|
try {
|
|
@@ -3348,8 +3558,260 @@ ${summary.trim()}
|
|
|
3348
3558
|
await server.connect(transport);
|
|
3349
3559
|
});
|
|
3350
3560
|
|
|
3561
|
+
// src/commands/recap.ts
|
|
3562
|
+
import { Command as Command20 } from "commander";
|
|
3563
|
+
import chalk20 from "chalk";
|
|
3564
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18 } from "fs";
|
|
3565
|
+
import { join as join20, resolve as resolve17 } from "path";
|
|
3566
|
+
var recapCommand = new Command20("recap").description("Summary of everything your AI learned across recent sessions").option("--days <n>", "How many days back to include", "7").option("--sessions <n>", "Max number of sessions to include").addHelpText("after", `
|
|
3567
|
+
Examples:
|
|
3568
|
+
mindlink recap
|
|
3569
|
+
mindlink recap --days 30
|
|
3570
|
+
mindlink recap --sessions 5
|
|
3571
|
+
`).action((opts) => {
|
|
3572
|
+
const projectPath = resolve17(process.cwd());
|
|
3573
|
+
const brainDir = join20(projectPath, BRAIN_DIR);
|
|
3574
|
+
if (!existsSync20(brainDir)) {
|
|
3575
|
+
console.log(` ${chalk20.red("\u2717")} No .brain/ found in this directory.`);
|
|
3576
|
+
console.log(` Run ${chalk20.cyan("mindlink init")} to get started.`);
|
|
3577
|
+
console.log("");
|
|
3578
|
+
process.exit(1);
|
|
3579
|
+
}
|
|
3580
|
+
const logPath = join20(brainDir, "LOG.md");
|
|
3581
|
+
const memoryPath = join20(brainDir, "MEMORY.md");
|
|
3582
|
+
if (!existsSync20(logPath)) {
|
|
3583
|
+
console.log(` ${chalk20.dim("No session history yet.")}`);
|
|
3584
|
+
console.log(` Start working with your AI \u2014 it will build up a log automatically.`);
|
|
3585
|
+
console.log("");
|
|
3586
|
+
process.exit(0);
|
|
3587
|
+
}
|
|
3588
|
+
const logContent = readFileSync18(logPath, "utf8");
|
|
3589
|
+
const allEntries = parseLogEntries(logContent);
|
|
3590
|
+
if (allEntries.length === 0) {
|
|
3591
|
+
console.log(` ${chalk20.dim("No sessions logged yet.")}`);
|
|
3592
|
+
console.log("");
|
|
3593
|
+
process.exit(0);
|
|
3594
|
+
}
|
|
3595
|
+
const maxDays = parseInt(opts.days ?? "7", 10);
|
|
3596
|
+
const maxSessions = opts.sessions ? parseInt(opts.sessions, 10) : void 0;
|
|
3597
|
+
const cutoff = Date.now() - maxDays * 864e5;
|
|
3598
|
+
function parseEntryDate(heading) {
|
|
3599
|
+
const clean = heading.replace(/^⭐\s*/, "").trim();
|
|
3600
|
+
const d = new Date(clean);
|
|
3601
|
+
return isNaN(d.getTime()) ? null : d;
|
|
3602
|
+
}
|
|
3603
|
+
let filtered = allEntries.filter((e) => {
|
|
3604
|
+
const d = parseEntryDate(e.heading);
|
|
3605
|
+
if (!d) return true;
|
|
3606
|
+
return d.getTime() >= cutoff;
|
|
3607
|
+
});
|
|
3608
|
+
if (maxSessions && filtered.length > maxSessions) {
|
|
3609
|
+
filtered = filtered.slice(0, maxSessions);
|
|
3610
|
+
}
|
|
3611
|
+
function extractField(body, label) {
|
|
3612
|
+
const lines = body.split("\n");
|
|
3613
|
+
const results = [];
|
|
3614
|
+
const labelLower = label.toLowerCase();
|
|
3615
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3616
|
+
const line = lines[i];
|
|
3617
|
+
const lineLower = line.toLowerCase();
|
|
3618
|
+
const inlinePatterns = [
|
|
3619
|
+
new RegExp(`\\*\\*${label}[:\\*s]*\\*\\*:?\\s*(.+)`, "i"),
|
|
3620
|
+
new RegExp(`^${label}:?\\s*(.+)`, "i")
|
|
3621
|
+
];
|
|
3622
|
+
for (const pat of inlinePatterns) {
|
|
3623
|
+
const m = line.match(pat);
|
|
3624
|
+
if (m?.[1]) {
|
|
3625
|
+
const items = m[1].split(/[·,]/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3626
|
+
results.push(...items);
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
if (lineLower.includes(labelLower + ":") && !line.includes("\u2192") && results.length === 0) {
|
|
3630
|
+
let j = i + 1;
|
|
3631
|
+
while (j < lines.length) {
|
|
3632
|
+
const next = lines[j].trim();
|
|
3633
|
+
if (!next || next.startsWith("**") || next.startsWith("##")) break;
|
|
3634
|
+
const t = next.replace(/^[-*]\s*/, "").trim();
|
|
3635
|
+
if (t.length > 0) results.push(t);
|
|
3636
|
+
j++;
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
return results;
|
|
3641
|
+
}
|
|
3642
|
+
const completedItems = [];
|
|
3643
|
+
const decisions = [];
|
|
3644
|
+
const nextItems = [];
|
|
3645
|
+
for (const entry of filtered) {
|
|
3646
|
+
completedItems.push(...extractField(entry.body, "completed"));
|
|
3647
|
+
completedItems.push(...extractField(entry.body, "what was completed"));
|
|
3648
|
+
decisions.push(...extractField(entry.body, "decisions"));
|
|
3649
|
+
nextItems.push(...extractField(entry.body, "next"));
|
|
3650
|
+
nextItems.push(...extractField(entry.body, "up next"));
|
|
3651
|
+
nextItems.push(...extractField(entry.body, "what's next"));
|
|
3652
|
+
}
|
|
3653
|
+
const dedup = (arr) => [...new Set(arr)].filter((s) => s.length > 0 && s !== "(none)");
|
|
3654
|
+
const completed = dedup(completedItems).slice(0, 8);
|
|
3655
|
+
const decidedItems = dedup(decisions).slice(0, 6);
|
|
3656
|
+
const upNext = dedup(nextItems).slice(0, 4);
|
|
3657
|
+
let memoryState = "";
|
|
3658
|
+
if (existsSync20(memoryPath)) {
|
|
3659
|
+
const mem = readFileSync18(memoryPath, "utf8");
|
|
3660
|
+
const core = extractSection(mem, "Core");
|
|
3661
|
+
const coreLines = core.split("\n").filter((l) => {
|
|
3662
|
+
const t = l.trim();
|
|
3663
|
+
return t.length > 0 && !t.startsWith("<!--") && !t.startsWith("#") && t !== "---";
|
|
3664
|
+
});
|
|
3665
|
+
if (coreLines.length > 0) memoryState = coreLines.slice(0, 4).join("\n");
|
|
3666
|
+
}
|
|
3667
|
+
console.log("");
|
|
3668
|
+
console.log(` ${chalk20.bold("\u25C9 MindLink \u2014 Session Recap")} ${chalk20.dim(`last ${maxDays} days \xB7 ${filtered.length} session${filtered.length !== 1 ? "s" : ""}`)}`);
|
|
3669
|
+
console.log("");
|
|
3670
|
+
if (memoryState) {
|
|
3671
|
+
console.log(` ${chalk20.cyan("Memory snapshot")}`);
|
|
3672
|
+
for (const line of memoryState.split("\n")) {
|
|
3673
|
+
console.log(` ${chalk20.dim(line)}`);
|
|
3674
|
+
}
|
|
3675
|
+
console.log("");
|
|
3676
|
+
}
|
|
3677
|
+
if (completed.length > 0) {
|
|
3678
|
+
console.log(` ${chalk20.green("What got done")}`);
|
|
3679
|
+
for (const item of completed) {
|
|
3680
|
+
console.log(` ${chalk20.green("\u2192")} ${item}`);
|
|
3681
|
+
}
|
|
3682
|
+
console.log("");
|
|
3683
|
+
}
|
|
3684
|
+
if (decidedItems.length > 0) {
|
|
3685
|
+
console.log(` ${chalk20.yellow("Decisions made")}`);
|
|
3686
|
+
for (const item of decidedItems) {
|
|
3687
|
+
console.log(` ${chalk20.yellow("\u2192")} ${item}`);
|
|
3688
|
+
}
|
|
3689
|
+
console.log("");
|
|
3690
|
+
}
|
|
3691
|
+
if (upNext.length > 0) {
|
|
3692
|
+
console.log(` ${chalk20.blue("Up next")}`);
|
|
3693
|
+
for (const item of upNext) {
|
|
3694
|
+
console.log(` ${chalk20.blue("\u2192")} ${item}`);
|
|
3695
|
+
}
|
|
3696
|
+
console.log("");
|
|
3697
|
+
}
|
|
3698
|
+
if (completed.length === 0 && decidedItems.length === 0 && upNext.length === 0) {
|
|
3699
|
+
console.log(` ${chalk20.dim("Sessions found but no structured content extracted.")}`);
|
|
3700
|
+
console.log(` ${chalk20.dim("Ask your AI to use the standard log format: completed / decisions / next.")}`);
|
|
3701
|
+
console.log("");
|
|
3702
|
+
}
|
|
3703
|
+
});
|
|
3704
|
+
|
|
3705
|
+
// src/commands/search.ts
|
|
3706
|
+
import { Command as Command21 } from "commander";
|
|
3707
|
+
import chalk21 from "chalk";
|
|
3708
|
+
import { existsSync as existsSync21, readFileSync as readFileSync19 } from "fs";
|
|
3709
|
+
import { join as join21, resolve as resolve18 } from "path";
|
|
3710
|
+
var MEMORY_SECTIONS = ["Core", "Architecture", "Decisions", "Conventions", "User Profile", "Important Context"];
|
|
3711
|
+
function searchFile(filePath, pattern, sectionFilter) {
|
|
3712
|
+
if (!existsSync21(filePath)) return [];
|
|
3713
|
+
const content = readFileSync19(filePath, "utf8");
|
|
3714
|
+
const results = [];
|
|
3715
|
+
const filename = filePath.split("/").pop() ?? "";
|
|
3716
|
+
if (filename === "MEMORY.md") {
|
|
3717
|
+
for (const section of MEMORY_SECTIONS) {
|
|
3718
|
+
if (sectionFilter && section.toLowerCase() !== sectionFilter.toLowerCase()) continue;
|
|
3719
|
+
const sectionContent = extractSection(content, section);
|
|
3720
|
+
const matchingLines2 = sectionContent.split("\n").filter((l) => {
|
|
3721
|
+
const t = l.trim();
|
|
3722
|
+
return t.length > 0 && !t.startsWith("<!--") && !t.startsWith("#") && t !== "---" && pattern.test(l);
|
|
3723
|
+
}).map((l) => l.trim()).slice(0, 3);
|
|
3724
|
+
if (matchingLines2.length > 0) {
|
|
3725
|
+
results.push({ section, lines: matchingLines2 });
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
return results;
|
|
3729
|
+
}
|
|
3730
|
+
const matchingLines = content.split("\n").filter((l) => pattern.test(l)).map((l) => l.trim()).filter((l) => l.length > 0).slice(0, 3);
|
|
3731
|
+
if (matchingLines.length > 0) {
|
|
3732
|
+
results.push({ section: "Log", lines: matchingLines });
|
|
3733
|
+
}
|
|
3734
|
+
return results;
|
|
3735
|
+
}
|
|
3736
|
+
var searchCommand = new Command21("search").description("Search across all project memories").argument("<query>", "Text or pattern to search for").option("--project <name>", "Limit search to a specific project path (partial match)").option("--section <name>", "Limit search to a specific MEMORY.md section (e.g. decisions, architecture)").option("--log", "Also search session logs (LOG.md)").addHelpText("after", `
|
|
3737
|
+
Examples:
|
|
3738
|
+
mindlink search "postgres"
|
|
3739
|
+
mindlink search "auth" --section decisions
|
|
3740
|
+
mindlink search "stripe" --project myapp
|
|
3741
|
+
mindlink search "api key" --log
|
|
3742
|
+
`).action((query, opts) => {
|
|
3743
|
+
const cwd = resolve18(process.cwd());
|
|
3744
|
+
const registered = getRegisteredProjects().filter((p) => existsSync21(join21(p, BRAIN_DIR, "config.json")));
|
|
3745
|
+
const projects = registered.includes(cwd) ? registered : existsSync21(join21(cwd, BRAIN_DIR, "config.json")) ? [cwd, ...registered] : registered;
|
|
3746
|
+
if (projects.length === 0) {
|
|
3747
|
+
console.log("");
|
|
3748
|
+
console.log(` ${chalk21.dim("No MindLink projects found.")}`);
|
|
3749
|
+
console.log(` Run ${chalk21.cyan("mindlink init")} in a project directory first.`);
|
|
3750
|
+
console.log("");
|
|
3751
|
+
process.exit(0);
|
|
3752
|
+
}
|
|
3753
|
+
const filteredProjects = opts.project ? projects.filter((p) => p.toLowerCase().includes(opts.project.toLowerCase())) : projects;
|
|
3754
|
+
if (filteredProjects.length === 0) {
|
|
3755
|
+
console.log("");
|
|
3756
|
+
console.log(` ${chalk21.dim(`No projects matching "${opts.project}"`)}`);
|
|
3757
|
+
console.log("");
|
|
3758
|
+
process.exit(0);
|
|
3759
|
+
}
|
|
3760
|
+
let pattern;
|
|
3761
|
+
try {
|
|
3762
|
+
pattern = new RegExp(query, "i");
|
|
3763
|
+
} catch {
|
|
3764
|
+
pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
|
3765
|
+
}
|
|
3766
|
+
const matches = [];
|
|
3767
|
+
for (const projectPath of filteredProjects) {
|
|
3768
|
+
const brainDir = join21(projectPath, BRAIN_DIR);
|
|
3769
|
+
if (!existsSync21(brainDir)) continue;
|
|
3770
|
+
const memoryMatches = searchFile(join21(brainDir, "MEMORY.md"), pattern, opts.section);
|
|
3771
|
+
for (const m of memoryMatches) {
|
|
3772
|
+
matches.push({ projectPath, file: "MEMORY.md", section: m.section, lines: m.lines });
|
|
3773
|
+
}
|
|
3774
|
+
if (opts.log) {
|
|
3775
|
+
const logMatches = searchFile(join21(brainDir, "LOG.md"), pattern, void 0);
|
|
3776
|
+
for (const m of logMatches) {
|
|
3777
|
+
matches.push({ projectPath, file: "LOG.md", section: m.section, lines: m.lines });
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
console.log("");
|
|
3782
|
+
if (matches.length === 0) {
|
|
3783
|
+
console.log(` ${chalk21.dim(`No matches for "${query}"`)}`);
|
|
3784
|
+
if (!opts.log) {
|
|
3785
|
+
console.log(` ${chalk21.dim("Add --log to also search session history.")}`);
|
|
3786
|
+
}
|
|
3787
|
+
console.log("");
|
|
3788
|
+
process.exit(0);
|
|
3789
|
+
}
|
|
3790
|
+
const byProject = {};
|
|
3791
|
+
for (const m of matches) {
|
|
3792
|
+
if (!byProject[m.projectPath]) byProject[m.projectPath] = [];
|
|
3793
|
+
byProject[m.projectPath].push(m);
|
|
3794
|
+
}
|
|
3795
|
+
const totalProjects = Object.keys(byProject).length;
|
|
3796
|
+
console.log(` ${chalk21.bold(`Found in ${totalProjects} project${totalProjects !== 1 ? "s" : ""}:`)} ${chalk21.dim(`"${query}"`)}`);
|
|
3797
|
+
console.log("");
|
|
3798
|
+
for (const [projectPath, projectMatches] of Object.entries(byProject)) {
|
|
3799
|
+
const parts = projectPath.split("/");
|
|
3800
|
+
const projectName = parts[parts.length - 1] ?? projectPath;
|
|
3801
|
+
console.log(` ${chalk21.bold(projectName)} ${chalk21.dim(projectPath)}`);
|
|
3802
|
+
for (const m of projectMatches) {
|
|
3803
|
+
console.log(` ${chalk21.cyan(m.file)} ${chalk21.dim("\u2192")} ${chalk21.cyan(m.section)}`);
|
|
3804
|
+
for (const line of m.lines) {
|
|
3805
|
+
const highlighted = line.replace(pattern, (match) => chalk21.yellow(match));
|
|
3806
|
+
console.log(` ${chalk21.dim("\xB7")} ${highlighted}`);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
console.log("");
|
|
3810
|
+
}
|
|
3811
|
+
});
|
|
3812
|
+
|
|
3351
3813
|
// src/cli.ts
|
|
3352
|
-
var program = new
|
|
3814
|
+
var program = new Command22();
|
|
3353
3815
|
program.name("mindlink").description("Give your AI a brain.").version(VERSION, "-v, --version");
|
|
3354
3816
|
program.addCommand(initCommand);
|
|
3355
3817
|
program.addCommand(statusCommand);
|
|
@@ -3370,9 +3832,11 @@ program.addCommand(verifyCommand);
|
|
|
3370
3832
|
program.addCommand(profileCommand);
|
|
3371
3833
|
program.addCommand(pruneCommand);
|
|
3372
3834
|
program.addCommand(mcpCommand);
|
|
3835
|
+
program.addCommand(recapCommand);
|
|
3836
|
+
program.addCommand(searchCommand);
|
|
3373
3837
|
program.on("command:*", (operands) => {
|
|
3374
3838
|
const unknown = operands[0];
|
|
3375
|
-
const known = ["init", "status", "log", "clear", "reset", "config", "sync", "update", "summary", "uninstall", "export", "import", "doctor", "version", "diff", "verify", "profile", "prune", "mcp"];
|
|
3839
|
+
const known = ["init", "status", "log", "clear", "reset", "config", "sync", "update", "summary", "uninstall", "export", "import", "doctor", "version", "diff", "verify", "profile", "prune", "mcp", "recap", "search"];
|
|
3376
3840
|
function levenshtein(a, b) {
|
|
3377
3841
|
const m = a.length, n = b.length;
|
|
3378
3842
|
const dp = Array.from(
|
|
@@ -3388,11 +3852,11 @@ program.on("command:*", (operands) => {
|
|
|
3388
3852
|
}
|
|
3389
3853
|
const closest = known.map((cmd) => ({ cmd, dist: levenshtein(unknown, cmd) })).sort((a, b) => a.dist - b.dist)[0];
|
|
3390
3854
|
console.log("");
|
|
3391
|
-
console.log(` ${
|
|
3855
|
+
console.log(` ${chalk22.red("\u2717")} Unknown command: ${chalk22.bold(unknown)}`);
|
|
3392
3856
|
if (closest && closest.dist <= 3) {
|
|
3393
|
-
console.log(` Did you mean ${
|
|
3857
|
+
console.log(` Did you mean ${chalk22.cyan("mindlink " + closest.cmd)}?`);
|
|
3394
3858
|
}
|
|
3395
|
-
console.log(` Run ${
|
|
3859
|
+
console.log(` Run ${chalk22.cyan("mindlink --help")} to see all commands.`);
|
|
3396
3860
|
console.log("");
|
|
3397
3861
|
process.exit(1);
|
|
3398
3862
|
});
|