mindlink 2.0.4 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli.js +621 -16
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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 Command23 } from "commander";
|
|
6
|
+
import chalk23 from "chalk";
|
|
7
7
|
|
|
8
8
|
// src/utils/version.ts
|
|
9
|
-
var VERSION = "2.0
|
|
9
|
+
var VERSION = "2.2.0";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { Command } from "commander";
|
|
@@ -228,7 +228,113 @@ function detectProjectInfo(projectPath) {
|
|
|
228
228
|
if (dirs.length > 0) topDirs = dirs.join(", ");
|
|
229
229
|
} catch {
|
|
230
230
|
}
|
|
231
|
-
|
|
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 "";
|
|
232
338
|
}
|
|
233
339
|
function memoryHasRealContent(memoryPath) {
|
|
234
340
|
try {
|
|
@@ -295,7 +401,14 @@ ${info2.stack}
|
|
|
295
401
|
}
|
|
296
402
|
const focusLines = [];
|
|
297
403
|
if (info2.topDirs) focusLines.push(`Directories: ${info2.topDirs}`);
|
|
298
|
-
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`);
|
|
299
412
|
const focusBlock = focusLines.length > 0 ? focusLines.join("\n") + `
|
|
300
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 -->`;
|
|
301
414
|
content = content.replace(
|
|
@@ -303,6 +416,28 @@ ${info2.stack}
|
|
|
303
416
|
`### Current focus
|
|
304
417
|
${focusBlock}`
|
|
305
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
|
+
}
|
|
306
441
|
return content;
|
|
307
442
|
}
|
|
308
443
|
var BRAIN_FILES = [
|
|
@@ -782,7 +917,8 @@ function extractSection(markdown, heading) {
|
|
|
782
917
|
if (match) {
|
|
783
918
|
const level = match[1].length;
|
|
784
919
|
const title = match[2].trim();
|
|
785
|
-
|
|
920
|
+
const cleanTitle = title.replace(/<!--.*?-->/g, "").trim();
|
|
921
|
+
if (cleanTitle.toLowerCase() === heading.toLowerCase()) {
|
|
786
922
|
inSection = true;
|
|
787
923
|
headingLevel = level;
|
|
788
924
|
continue;
|
|
@@ -1447,7 +1583,7 @@ var REQUIRED_BRAIN_FILES = ["MEMORY.md", "SESSION.md", "SHARED.md", "LOG.md"];
|
|
|
1447
1583
|
async function latestVersion() {
|
|
1448
1584
|
try {
|
|
1449
1585
|
const { default: https } = await import("https");
|
|
1450
|
-
return new Promise((
|
|
1586
|
+
return new Promise((resolve20) => {
|
|
1451
1587
|
const req = https.get(
|
|
1452
1588
|
"https://registry.npmjs.org/mindlink/latest",
|
|
1453
1589
|
{ headers: { "User-Agent": "mindlink-cli" } },
|
|
@@ -1459,17 +1595,17 @@ async function latestVersion() {
|
|
|
1459
1595
|
res.on("end", () => {
|
|
1460
1596
|
try {
|
|
1461
1597
|
const parsed = JSON.parse(data);
|
|
1462
|
-
|
|
1598
|
+
resolve20(parsed.version ?? null);
|
|
1463
1599
|
} catch {
|
|
1464
|
-
|
|
1600
|
+
resolve20(null);
|
|
1465
1601
|
}
|
|
1466
1602
|
});
|
|
1467
1603
|
}
|
|
1468
1604
|
);
|
|
1469
|
-
req.on("error", () =>
|
|
1605
|
+
req.on("error", () => resolve20(null));
|
|
1470
1606
|
req.setTimeout(8e3, () => {
|
|
1471
1607
|
req.destroy();
|
|
1472
|
-
|
|
1608
|
+
resolve20(null);
|
|
1473
1609
|
});
|
|
1474
1610
|
});
|
|
1475
1611
|
} catch {
|
|
@@ -3422,8 +3558,474 @@ ${summary.trim()}
|
|
|
3422
3558
|
await server.connect(transport);
|
|
3423
3559
|
});
|
|
3424
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
|
+
|
|
3813
|
+
// src/commands/learn.ts
|
|
3814
|
+
import { Command as Command22 } from "commander";
|
|
3815
|
+
import chalk22 from "chalk";
|
|
3816
|
+
import { existsSync as existsSync22, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
|
|
3817
|
+
import { join as join22, resolve as resolve19, extname as extname2 } from "path";
|
|
3818
|
+
function resolveSection(hint) {
|
|
3819
|
+
if (!hint) return "Important Context";
|
|
3820
|
+
const h = hint.toLowerCase();
|
|
3821
|
+
if (h.includes("core")) return "Core";
|
|
3822
|
+
if (h.includes("arch")) return "Architecture";
|
|
3823
|
+
if (h.includes("dec")) return "Decisions";
|
|
3824
|
+
if (h.includes("conv")) return "Conventions";
|
|
3825
|
+
return "Important Context";
|
|
3826
|
+
}
|
|
3827
|
+
async function fetchUrl(url) {
|
|
3828
|
+
const { default: https } = await (url.startsWith("https") ? import("https") : import("http"));
|
|
3829
|
+
return new Promise((resolve20, reject) => {
|
|
3830
|
+
const req = https.get(url, { headers: { "User-Agent": "mindlink-learn/1.0" } }, (res) => {
|
|
3831
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
3832
|
+
fetchUrl(res.headers.location).then(resolve20).catch(reject);
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
3836
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
let data = "";
|
|
3840
|
+
res.on("data", (chunk) => {
|
|
3841
|
+
data += chunk;
|
|
3842
|
+
});
|
|
3843
|
+
res.on("end", () => resolve20(data));
|
|
3844
|
+
});
|
|
3845
|
+
req.on("error", reject);
|
|
3846
|
+
req.setTimeout(15e3, () => {
|
|
3847
|
+
req.destroy();
|
|
3848
|
+
reject(new Error("Request timed out"));
|
|
3849
|
+
});
|
|
3850
|
+
});
|
|
3851
|
+
}
|
|
3852
|
+
function stripHtml(html) {
|
|
3853
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s{2,}/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
3854
|
+
}
|
|
3855
|
+
function readLocalFile(filePath) {
|
|
3856
|
+
const ext = extname2(filePath).toLowerCase();
|
|
3857
|
+
const raw = readFileSync20(filePath, "utf8");
|
|
3858
|
+
if (ext === ".json") {
|
|
3859
|
+
try {
|
|
3860
|
+
return JSON.stringify(JSON.parse(raw), null, 2).slice(0, 8e3);
|
|
3861
|
+
} catch {
|
|
3862
|
+
return raw;
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
return raw;
|
|
3866
|
+
}
|
|
3867
|
+
function extractFacts(text3, maxLines = 60) {
|
|
3868
|
+
const SKIP_PATTERNS = [
|
|
3869
|
+
/^(cookie|privacy|terms|copyright|all rights reserved|subscribe|sign up|log in|home|menu|search|navigation)/i,
|
|
3870
|
+
/^\s*[\|<>\/\\]\s*$/,
|
|
3871
|
+
/^https?:\/\//
|
|
3872
|
+
];
|
|
3873
|
+
const lines = text3.split("\n").map((l) => l.trim()).filter(
|
|
3874
|
+
(l) => l.length > 10 && l.length < 300 && !SKIP_PATTERNS.some((p) => p.test(l))
|
|
3875
|
+
);
|
|
3876
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3877
|
+
const deduped = [];
|
|
3878
|
+
for (const line of lines) {
|
|
3879
|
+
const key = line.toLowerCase().replace(/\s+/g, " ");
|
|
3880
|
+
if (!seen.has(key)) {
|
|
3881
|
+
seen.add(key);
|
|
3882
|
+
deduped.push(line);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
return deduped.slice(0, maxLines).join("\n");
|
|
3886
|
+
}
|
|
3887
|
+
function appendToMemory(memoryPath, section, facts, source) {
|
|
3888
|
+
let content = readFileSync20(memoryPath, "utf8");
|
|
3889
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3890
|
+
const entry = `<!-- learned from: ${source} on ${date} -->
|
|
3891
|
+
${facts.trim()}`;
|
|
3892
|
+
const lines = content.split("\n");
|
|
3893
|
+
let headingIdx = -1;
|
|
3894
|
+
let nextSectionIdx = lines.length;
|
|
3895
|
+
let headingLevel = 0;
|
|
3896
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3897
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)/);
|
|
3898
|
+
if (match) {
|
|
3899
|
+
const level = match[1].length;
|
|
3900
|
+
const title = match[2].replace(/<!--.*?-->/g, "").trim();
|
|
3901
|
+
if (headingIdx < 0 && title.toLowerCase() === section.toLowerCase()) {
|
|
3902
|
+
headingIdx = i;
|
|
3903
|
+
headingLevel = level;
|
|
3904
|
+
} else if (headingIdx >= 0 && level <= headingLevel) {
|
|
3905
|
+
nextSectionIdx = i;
|
|
3906
|
+
break;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
if (headingIdx < 0) {
|
|
3911
|
+
content = content.trimEnd() + `
|
|
3912
|
+
|
|
3913
|
+
## ${section}
|
|
3914
|
+
|
|
3915
|
+
${entry}
|
|
3916
|
+
`;
|
|
3917
|
+
} else {
|
|
3918
|
+
const insertAt = nextSectionIdx > 0 && lines[nextSectionIdx - 1].trim() === "---" ? nextSectionIdx - 1 : nextSectionIdx;
|
|
3919
|
+
lines.splice(insertAt, 0, "", entry, "");
|
|
3920
|
+
content = lines.join("\n");
|
|
3921
|
+
}
|
|
3922
|
+
writeFileSync11(memoryPath, content);
|
|
3923
|
+
}
|
|
3924
|
+
var learnCommand = new Command22("learn").description("Extract facts from a file or URL and save them into project memory").argument("<source>", "File path or URL to learn from").option("--section <name>", "Target memory section (core, architecture, decisions, conventions, context)", "context").option("--preview", "Show what would be learned without writing anything").addHelpText("after", `
|
|
3925
|
+
Examples:
|
|
3926
|
+
mindlink learn ./docs/architecture.md
|
|
3927
|
+
mindlink learn https://example.com/api-docs
|
|
3928
|
+
mindlink learn ./package.json --section architecture
|
|
3929
|
+
mindlink learn ./DECISIONS.md --section decisions
|
|
3930
|
+
mindlink learn https://stripe.com/docs/webhooks --preview
|
|
3931
|
+
`).action(async (source, opts) => {
|
|
3932
|
+
const projectPath = resolve19(process.cwd());
|
|
3933
|
+
const brainDir = join22(projectPath, BRAIN_DIR);
|
|
3934
|
+
if (!existsSync22(brainDir)) {
|
|
3935
|
+
console.log(` ${chalk22.red("\u2717")} No .brain/ found in this directory.`);
|
|
3936
|
+
console.log(` Run ${chalk22.cyan("mindlink init")} to get started.`);
|
|
3937
|
+
console.log("");
|
|
3938
|
+
process.exit(1);
|
|
3939
|
+
}
|
|
3940
|
+
const memoryPath = join22(brainDir, "MEMORY.md");
|
|
3941
|
+
if (!existsSync22(memoryPath)) {
|
|
3942
|
+
console.log(` ${chalk22.red("\u2717")} MEMORY.md not found in .brain/.`);
|
|
3943
|
+
console.log(` Run ${chalk22.cyan("mindlink init")} to set up memory.`);
|
|
3944
|
+
console.log("");
|
|
3945
|
+
process.exit(1);
|
|
3946
|
+
}
|
|
3947
|
+
const section = resolveSection(opts.section);
|
|
3948
|
+
const isUrl = source.startsWith("http://") || source.startsWith("https://");
|
|
3949
|
+
console.log("");
|
|
3950
|
+
let rawText = "";
|
|
3951
|
+
if (isUrl) {
|
|
3952
|
+
process.stdout.write(` Fetching ${chalk22.dim(source)}...
|
|
3953
|
+
`);
|
|
3954
|
+
try {
|
|
3955
|
+
const html = await fetchUrl(source);
|
|
3956
|
+
rawText = stripHtml(html);
|
|
3957
|
+
} catch (err) {
|
|
3958
|
+
console.log(` ${chalk22.red("\u2717")} Could not fetch URL: ${err instanceof Error ? err.message : String(err)}`);
|
|
3959
|
+
console.log(` Check the URL is reachable and try again.`);
|
|
3960
|
+
console.log("");
|
|
3961
|
+
process.exit(1);
|
|
3962
|
+
}
|
|
3963
|
+
} else {
|
|
3964
|
+
const absPath = resolve19(projectPath, source);
|
|
3965
|
+
if (!existsSync22(absPath)) {
|
|
3966
|
+
console.log(` ${chalk22.red("\u2717")} File not found: ${absPath}`);
|
|
3967
|
+
console.log("");
|
|
3968
|
+
process.exit(1);
|
|
3969
|
+
}
|
|
3970
|
+
try {
|
|
3971
|
+
rawText = readLocalFile(absPath);
|
|
3972
|
+
} catch (err) {
|
|
3973
|
+
console.log(` ${chalk22.red("\u2717")} Could not read file: ${err instanceof Error ? err.message : String(err)}`);
|
|
3974
|
+
console.log("");
|
|
3975
|
+
process.exit(1);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
if (!rawText.trim()) {
|
|
3979
|
+
console.log(` ${chalk22.yellow("\u2192")} Source is empty \u2014 nothing to learn.`);
|
|
3980
|
+
console.log("");
|
|
3981
|
+
process.exit(0);
|
|
3982
|
+
}
|
|
3983
|
+
const facts = extractFacts(rawText);
|
|
3984
|
+
if (!facts.trim()) {
|
|
3985
|
+
console.log(` ${chalk22.yellow("\u2192")} Could not extract any meaningful content from this source.`);
|
|
3986
|
+
console.log("");
|
|
3987
|
+
process.exit(0);
|
|
3988
|
+
}
|
|
3989
|
+
if (opts.preview) {
|
|
3990
|
+
console.log(` ${chalk22.bold("Preview")} \u2014 would add to ${chalk22.cyan("## " + section)} in MEMORY.md:`);
|
|
3991
|
+
console.log("");
|
|
3992
|
+
const previewLines = facts.split("\n").slice(0, 20);
|
|
3993
|
+
for (const line of previewLines) {
|
|
3994
|
+
console.log(` ${chalk22.dim("\xB7")} ${line}`);
|
|
3995
|
+
}
|
|
3996
|
+
if (facts.split("\n").length > 20) {
|
|
3997
|
+
console.log(` ${chalk22.dim(`... and ${facts.split("\n").length - 20} more lines`)}`);
|
|
3998
|
+
}
|
|
3999
|
+
console.log("");
|
|
4000
|
+
console.log(` Run without ${chalk22.cyan("--preview")} to save.`);
|
|
4001
|
+
console.log("");
|
|
4002
|
+
process.exit(0);
|
|
4003
|
+
}
|
|
4004
|
+
try {
|
|
4005
|
+
appendToMemory(memoryPath, section, facts, source);
|
|
4006
|
+
} catch (err) {
|
|
4007
|
+
console.log(` ${chalk22.red("\u2717")} Failed to write to MEMORY.md: ${err instanceof Error ? err.message : String(err)}`);
|
|
4008
|
+
console.log("");
|
|
4009
|
+
process.exit(1);
|
|
4010
|
+
}
|
|
4011
|
+
const lineCount = facts.split("\n").filter((l) => l.trim()).length;
|
|
4012
|
+
console.log(` ${chalk22.green("\u2713")} Learned ${lineCount} lines from ${chalk22.dim(source)}`);
|
|
4013
|
+
console.log(` Added to ${chalk22.cyan("## " + section)} in MEMORY.md`);
|
|
4014
|
+
console.log("");
|
|
4015
|
+
console.log(` ${chalk22.dim("Your AI will see this context starting next session.")}`);
|
|
4016
|
+
console.log(` ${chalk22.dim("Run mindlink verify to check memory health.")}`);
|
|
4017
|
+
console.log("");
|
|
4018
|
+
const existing = extractSection(readFileSync20(memoryPath, "utf8"), section);
|
|
4019
|
+
const existingLines = existing.split("\n").filter((l) => l.trim() && !l.startsWith("<!--")).length;
|
|
4020
|
+
if (existingLines > 80) {
|
|
4021
|
+
console.log(` ${chalk22.yellow("\u2192")} ## ${section} is getting large (${existingLines} lines).`);
|
|
4022
|
+
console.log(` Run ${chalk22.cyan("mindlink prune")} to retire stale entries.`);
|
|
4023
|
+
console.log("");
|
|
4024
|
+
}
|
|
4025
|
+
});
|
|
4026
|
+
|
|
3425
4027
|
// src/cli.ts
|
|
3426
|
-
var program = new
|
|
4028
|
+
var program = new Command23();
|
|
3427
4029
|
program.name("mindlink").description("Give your AI a brain.").version(VERSION, "-v, --version");
|
|
3428
4030
|
program.addCommand(initCommand);
|
|
3429
4031
|
program.addCommand(statusCommand);
|
|
@@ -3444,9 +4046,12 @@ program.addCommand(verifyCommand);
|
|
|
3444
4046
|
program.addCommand(profileCommand);
|
|
3445
4047
|
program.addCommand(pruneCommand);
|
|
3446
4048
|
program.addCommand(mcpCommand);
|
|
4049
|
+
program.addCommand(recapCommand);
|
|
4050
|
+
program.addCommand(searchCommand);
|
|
4051
|
+
program.addCommand(learnCommand);
|
|
3447
4052
|
program.on("command:*", (operands) => {
|
|
3448
4053
|
const unknown = operands[0];
|
|
3449
|
-
const known = ["init", "status", "log", "clear", "reset", "config", "sync", "update", "summary", "uninstall", "export", "import", "doctor", "version", "diff", "verify", "profile", "prune", "mcp"];
|
|
4054
|
+
const known = ["init", "status", "log", "clear", "reset", "config", "sync", "update", "summary", "uninstall", "export", "import", "doctor", "version", "diff", "verify", "profile", "prune", "mcp", "recap", "search", "learn"];
|
|
3450
4055
|
function levenshtein(a, b) {
|
|
3451
4056
|
const m = a.length, n = b.length;
|
|
3452
4057
|
const dp = Array.from(
|
|
@@ -3462,11 +4067,11 @@ program.on("command:*", (operands) => {
|
|
|
3462
4067
|
}
|
|
3463
4068
|
const closest = known.map((cmd) => ({ cmd, dist: levenshtein(unknown, cmd) })).sort((a, b) => a.dist - b.dist)[0];
|
|
3464
4069
|
console.log("");
|
|
3465
|
-
console.log(` ${
|
|
4070
|
+
console.log(` ${chalk23.red("\u2717")} Unknown command: ${chalk23.bold(unknown)}`);
|
|
3466
4071
|
if (closest && closest.dist <= 3) {
|
|
3467
|
-
console.log(` Did you mean ${
|
|
4072
|
+
console.log(` Did you mean ${chalk23.cyan("mindlink " + closest.cmd)}?`);
|
|
3468
4073
|
}
|
|
3469
|
-
console.log(` Run ${
|
|
4074
|
+
console.log(` Run ${chalk23.cyan("mindlink --help")} to see all commands.`);
|
|
3470
4075
|
console.log("");
|
|
3471
4076
|
process.exit(1);
|
|
3472
4077
|
});
|