learning-agent 0.2.0 → 0.2.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/CHANGELOG.md +35 -2
- package/README.md +97 -10
- package/dist/cli.js +1381 -971
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,133 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import chalk from 'chalk';
|
|
3
2
|
import { Command } from 'commander';
|
|
4
|
-
import
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { statSync, existsSync, chmodSync, mkdirSync } from 'fs';
|
|
5
5
|
import * as fs from 'fs/promises';
|
|
6
|
-
import { mkdir, writeFile,
|
|
6
|
+
import { readFile, mkdir, writeFile, rename, appendFile } from 'fs/promises';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { join, dirname } from 'path';
|
|
9
9
|
import { createHash } from 'crypto';
|
|
10
|
-
import Database from 'better-sqlite3';
|
|
11
10
|
import { z } from 'zod';
|
|
12
|
-
import
|
|
11
|
+
import Database from 'better-sqlite3';
|
|
12
|
+
import { resolveModelFile, getLlama } from 'node-llama-cpp';
|
|
13
|
+
|
|
14
|
+
// src/cli-utils.ts
|
|
15
|
+
function formatBytes(bytes) {
|
|
16
|
+
if (bytes === 0) return "0 B";
|
|
17
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
18
|
+
const kb = bytes / 1024;
|
|
19
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
20
|
+
const mb = kb / 1024;
|
|
21
|
+
return `${mb.toFixed(1)} MB`;
|
|
22
|
+
}
|
|
23
|
+
function parseLimit(value, name) {
|
|
24
|
+
const parsed = parseInt(value, 10);
|
|
25
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
26
|
+
throw new Error(`Invalid ${name}: must be a positive integer`);
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
function getRepoRoot() {
|
|
31
|
+
return process.env["LEARNING_AGENT_ROOT"] ?? process.cwd();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/cli/shared.ts
|
|
35
|
+
var out = {
|
|
36
|
+
success: (msg) => console.log(chalk.green("[ok]"), msg),
|
|
37
|
+
error: (msg) => console.error(chalk.red("[error]"), msg),
|
|
38
|
+
info: (msg) => console.log(chalk.blue("[info]"), msg),
|
|
39
|
+
warn: (msg) => console.log(chalk.yellow("[warn]"), msg)
|
|
40
|
+
};
|
|
41
|
+
function getGlobalOpts(cmd) {
|
|
42
|
+
const opts = cmd.optsWithGlobals();
|
|
43
|
+
return {
|
|
44
|
+
verbose: opts.verbose ?? false,
|
|
45
|
+
quiet: opts.quiet ?? false
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
var DEFAULT_SEARCH_LIMIT = "10";
|
|
49
|
+
var DEFAULT_LIST_LIMIT = "20";
|
|
50
|
+
var DEFAULT_CHECK_PLAN_LIMIT = "5";
|
|
51
|
+
var ISO_DATE_PREFIX_LENGTH = 10;
|
|
52
|
+
var AVG_DECIMAL_PLACES = 1;
|
|
53
|
+
var RELEVANCE_DECIMAL_PLACES = 2;
|
|
54
|
+
var JSON_INDENT_SPACES = 2;
|
|
55
|
+
var PRE_COMMIT_MESSAGE = `Before committing, have you captured any valuable lessons from this session?
|
|
56
|
+
Consider: corrections, mistakes, or insights worth remembering.
|
|
57
|
+
|
|
58
|
+
To capture a lesson:
|
|
59
|
+
npx lna capture --trigger "what happened" --insight "what to do" --yes`;
|
|
60
|
+
var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
|
|
61
|
+
# Learning Agent pre-commit hook
|
|
62
|
+
# Reminds Claude to consider capturing lessons before commits
|
|
13
63
|
|
|
64
|
+
npx lna hooks run pre-commit
|
|
65
|
+
`;
|
|
66
|
+
var CLAUDE_HOOK_MARKER = "lna load-session";
|
|
67
|
+
var CLAUDE_HOOK_MARKER_LEGACY = "learning-agent load-session";
|
|
68
|
+
var CLAUDE_HOOK_CONFIG = {
|
|
69
|
+
matcher: "startup|resume|compact",
|
|
70
|
+
hooks: [
|
|
71
|
+
{
|
|
72
|
+
type: "command",
|
|
73
|
+
command: "npx lna load-session 2>/dev/null || true"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
var HOOK_MARKER = "# Learning Agent pre-commit hook";
|
|
78
|
+
function getClaudeSettingsPath(global) {
|
|
79
|
+
if (global) {
|
|
80
|
+
return join(homedir(), ".claude", "settings.json");
|
|
81
|
+
}
|
|
82
|
+
const repoRoot = getRepoRoot();
|
|
83
|
+
return join(repoRoot, ".claude", "settings.json");
|
|
84
|
+
}
|
|
85
|
+
async function readClaudeSettings(settingsPath) {
|
|
86
|
+
if (!existsSync(settingsPath)) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
const content = await readFile(settingsPath, "utf-8");
|
|
90
|
+
return JSON.parse(content);
|
|
91
|
+
}
|
|
92
|
+
function hasClaudeHook(settings) {
|
|
93
|
+
const hooks = settings.hooks;
|
|
94
|
+
if (!hooks?.SessionStart) return false;
|
|
95
|
+
return hooks.SessionStart.some((entry) => {
|
|
96
|
+
const hookEntry = entry;
|
|
97
|
+
return hookEntry.hooks?.some(
|
|
98
|
+
(h) => h.command?.includes(CLAUDE_HOOK_MARKER) || h.command?.includes(CLAUDE_HOOK_MARKER_LEGACY)
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function addLearningAgentHook(settings) {
|
|
103
|
+
if (!settings.hooks) {
|
|
104
|
+
settings.hooks = {};
|
|
105
|
+
}
|
|
106
|
+
const hooks = settings.hooks;
|
|
107
|
+
if (!hooks.SessionStart) {
|
|
108
|
+
hooks.SessionStart = [];
|
|
109
|
+
}
|
|
110
|
+
hooks.SessionStart.push(CLAUDE_HOOK_CONFIG);
|
|
111
|
+
}
|
|
112
|
+
function removeLearningAgentHook(settings) {
|
|
113
|
+
const hooks = settings.hooks;
|
|
114
|
+
if (!hooks?.SessionStart) return false;
|
|
115
|
+
const originalLength = hooks.SessionStart.length;
|
|
116
|
+
hooks.SessionStart = hooks.SessionStart.filter((entry) => {
|
|
117
|
+
const hookEntry = entry;
|
|
118
|
+
return !hookEntry.hooks?.some(
|
|
119
|
+
(h) => h.command?.includes(CLAUDE_HOOK_MARKER) || h.command?.includes(CLAUDE_HOOK_MARKER_LEGACY)
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
return hooks.SessionStart.length < originalLength;
|
|
123
|
+
}
|
|
124
|
+
async function writeClaudeSettings(settingsPath, settings) {
|
|
125
|
+
const dir = dirname(settingsPath);
|
|
126
|
+
await mkdir(dir, { recursive: true });
|
|
127
|
+
const tempPath = settingsPath + ".tmp";
|
|
128
|
+
await writeFile(tempPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
129
|
+
await rename(tempPath, settingsPath);
|
|
130
|
+
}
|
|
14
131
|
var SourceSchema = z.enum([
|
|
15
132
|
"user_correction",
|
|
16
133
|
"self_correction",
|
|
@@ -132,8 +249,6 @@ async function readLessons(repoRoot, options = {}) {
|
|
|
132
249
|
}
|
|
133
250
|
return { lessons: Array.from(lessons.values()), skippedCount };
|
|
134
251
|
}
|
|
135
|
-
|
|
136
|
-
// src/storage/sqlite.ts
|
|
137
252
|
var DB_PATH = ".claude/.cache/lessons.sqlite";
|
|
138
253
|
var SCHEMA_SQL = `
|
|
139
254
|
-- Main lessons table
|
|
@@ -402,257 +517,343 @@ function getRetrievalStats(repoRoot) {
|
|
|
402
517
|
lastRetrieved: row.last_retrieved
|
|
403
518
|
}));
|
|
404
519
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
var
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
520
|
+
var ARCHIVE_DIR = ".claude/lessons/archive";
|
|
521
|
+
var TOMBSTONE_THRESHOLD = 100;
|
|
522
|
+
var ARCHIVE_AGE_DAYS = 90;
|
|
523
|
+
var MS_PER_DAY = 1e3 * 60 * 60 * 24;
|
|
524
|
+
var MONTH_INDEX_OFFSET = 1;
|
|
525
|
+
var MONTH_PAD_LENGTH = 2;
|
|
526
|
+
function getArchivePath(repoRoot, date) {
|
|
527
|
+
const year = date.getFullYear();
|
|
528
|
+
const month = String(date.getMonth() + MONTH_INDEX_OFFSET).padStart(MONTH_PAD_LENGTH, "0");
|
|
529
|
+
return join(repoRoot, ARCHIVE_DIR, `${year}-${month}.jsonl`);
|
|
530
|
+
}
|
|
531
|
+
async function parseRawJsonlLines(repoRoot) {
|
|
532
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
533
|
+
let content;
|
|
534
|
+
try {
|
|
535
|
+
content = await readFile(filePath, "utf-8");
|
|
536
|
+
} catch {
|
|
537
|
+
return [];
|
|
419
538
|
}
|
|
420
|
-
const
|
|
421
|
-
for (const
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
reason: `Found similar existing lesson: "${lesson.insight.slice(0, 50)}..."`,
|
|
430
|
-
existingId: lesson.id
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
|
|
434
|
-
return {
|
|
435
|
-
novel: false,
|
|
436
|
-
reason: `Exact duplicate found`,
|
|
437
|
-
existingId: lesson.id
|
|
438
|
-
};
|
|
539
|
+
const results = [];
|
|
540
|
+
for (const line of content.split("\n")) {
|
|
541
|
+
const trimmed = line.trim();
|
|
542
|
+
if (!trimmed) continue;
|
|
543
|
+
try {
|
|
544
|
+
const parsed = JSON.parse(trimmed);
|
|
545
|
+
results.push({ line: trimmed, parsed });
|
|
546
|
+
} catch {
|
|
547
|
+
results.push({ line: trimmed, parsed: null });
|
|
439
548
|
}
|
|
440
549
|
}
|
|
441
|
-
return
|
|
550
|
+
return results;
|
|
442
551
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
/\btry to\b/i,
|
|
450
|
-
/\bdouble check\b/i
|
|
451
|
-
];
|
|
452
|
-
var GENERIC_IMPERATIVE_PATTERN = /^(always|never)\s+\w+(\s+\w+){0,2}$/i;
|
|
453
|
-
function isSpecific(insight) {
|
|
454
|
-
const words = insight.trim().split(/\s+/).filter((w) => w.length > 0);
|
|
455
|
-
if (words.length < MIN_WORD_COUNT) {
|
|
456
|
-
return { specific: false, reason: "Insight is too short to be actionable" };
|
|
457
|
-
}
|
|
458
|
-
for (const pattern of VAGUE_PATTERNS) {
|
|
459
|
-
if (pattern.test(insight)) {
|
|
460
|
-
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
552
|
+
async function countTombstones(repoRoot) {
|
|
553
|
+
const lines = await parseRawJsonlLines(repoRoot);
|
|
554
|
+
let count = 0;
|
|
555
|
+
for (const { parsed } of lines) {
|
|
556
|
+
if (parsed && parsed["deleted"] === true) {
|
|
557
|
+
count++;
|
|
461
558
|
}
|
|
462
559
|
}
|
|
463
|
-
|
|
464
|
-
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
465
|
-
}
|
|
466
|
-
return { specific: true };
|
|
560
|
+
return count;
|
|
467
561
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
562
|
+
async function needsCompaction(repoRoot) {
|
|
563
|
+
const count = await countTombstones(repoRoot);
|
|
564
|
+
return count >= TOMBSTONE_THRESHOLD;
|
|
565
|
+
}
|
|
566
|
+
async function rewriteWithoutTombstones(repoRoot) {
|
|
567
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
568
|
+
const tempPath = filePath + ".tmp";
|
|
569
|
+
const { lessons } = await readLessons(repoRoot);
|
|
570
|
+
const tombstoneCount = await countTombstones(repoRoot);
|
|
571
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
572
|
+
const lines = lessons.map((lesson) => JSON.stringify(lesson) + "\n");
|
|
573
|
+
await writeFile(tempPath, lines.join(""), "utf-8");
|
|
574
|
+
await rename(tempPath, filePath);
|
|
575
|
+
return tombstoneCount;
|
|
576
|
+
}
|
|
577
|
+
function shouldArchive(lesson, now) {
|
|
578
|
+
const created = new Date(lesson.created);
|
|
579
|
+
const ageMs = now.getTime() - created.getTime();
|
|
580
|
+
const ageDays = ageMs / MS_PER_DAY;
|
|
581
|
+
return ageDays > ARCHIVE_AGE_DAYS && (lesson.retrievalCount === void 0 || lesson.retrievalCount === 0);
|
|
582
|
+
}
|
|
583
|
+
async function archiveOldLessons(repoRoot) {
|
|
584
|
+
const { lessons } = await readLessons(repoRoot);
|
|
585
|
+
const now = /* @__PURE__ */ new Date();
|
|
586
|
+
const toArchive = [];
|
|
587
|
+
const toKeep = [];
|
|
588
|
+
for (const lesson of lessons) {
|
|
589
|
+
if (shouldArchive(lesson, now)) {
|
|
590
|
+
toArchive.push(lesson);
|
|
591
|
+
} else {
|
|
592
|
+
toKeep.push(lesson);
|
|
488
593
|
}
|
|
489
594
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
async function shouldPropose(repoRoot, insight) {
|
|
493
|
-
const specificResult = isSpecific(insight);
|
|
494
|
-
if (!specificResult.specific) {
|
|
495
|
-
return { shouldPropose: false, reason: specificResult.reason };
|
|
595
|
+
if (toArchive.length === 0) {
|
|
596
|
+
return 0;
|
|
496
597
|
}
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
return { shouldPropose: true };
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// src/capture/triggers.ts
|
|
509
|
-
var USER_CORRECTION_PATTERNS = [
|
|
510
|
-
/\bno\b[,.]?\s/i,
|
|
511
|
-
// "no, ..." or "no ..."
|
|
512
|
-
/\bwrong\b/i,
|
|
513
|
-
// "wrong"
|
|
514
|
-
/\bactually\b/i,
|
|
515
|
-
// "actually..."
|
|
516
|
-
/\bnot that\b/i,
|
|
517
|
-
// "not that"
|
|
518
|
-
/\bi meant\b/i
|
|
519
|
-
// "I meant"
|
|
520
|
-
];
|
|
521
|
-
function detectUserCorrection(signals) {
|
|
522
|
-
const { messages, context } = signals;
|
|
523
|
-
if (messages.length < 2) {
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
for (let i = 1; i < messages.length; i++) {
|
|
527
|
-
const message = messages[i];
|
|
528
|
-
if (!message) continue;
|
|
529
|
-
for (const pattern of USER_CORRECTION_PATTERNS) {
|
|
530
|
-
if (pattern.test(message)) {
|
|
531
|
-
return {
|
|
532
|
-
trigger: `User correction during ${context.intent}`,
|
|
533
|
-
correctionMessage: message,
|
|
534
|
-
context
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
return null;
|
|
540
|
-
}
|
|
541
|
-
function detectSelfCorrection(history) {
|
|
542
|
-
const { edits } = history;
|
|
543
|
-
if (edits.length < 3) {
|
|
544
|
-
return null;
|
|
598
|
+
const archiveGroups = /* @__PURE__ */ new Map();
|
|
599
|
+
for (const lesson of toArchive) {
|
|
600
|
+
const created = new Date(lesson.created);
|
|
601
|
+
const archivePath = getArchivePath(repoRoot, created);
|
|
602
|
+
const group = archiveGroups.get(archivePath) ?? [];
|
|
603
|
+
group.push(lesson);
|
|
604
|
+
archiveGroups.set(archivePath, group);
|
|
545
605
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
if (first.file === second.file && second.file === third.file && first.success && !second.success && third.success) {
|
|
552
|
-
return {
|
|
553
|
-
file: first.file,
|
|
554
|
-
trigger: `Self-correction on ${first.file}`
|
|
555
|
-
};
|
|
556
|
-
}
|
|
606
|
+
const archiveDir = join(repoRoot, ARCHIVE_DIR);
|
|
607
|
+
await mkdir(archiveDir, { recursive: true });
|
|
608
|
+
for (const [archivePath, archiveLessons] of archiveGroups) {
|
|
609
|
+
const lines2 = archiveLessons.map((l) => JSON.stringify(l) + "\n").join("");
|
|
610
|
+
await appendFile(archivePath, lines2, "utf-8");
|
|
557
611
|
}
|
|
558
|
-
|
|
612
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
613
|
+
const tempPath = filePath + ".tmp";
|
|
614
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
615
|
+
const lines = toKeep.map((lesson) => JSON.stringify(lesson) + "\n");
|
|
616
|
+
await writeFile(tempPath, lines.join(""), "utf-8");
|
|
617
|
+
await rename(tempPath, filePath);
|
|
618
|
+
return toArchive.length;
|
|
559
619
|
}
|
|
560
|
-
function
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const
|
|
620
|
+
async function compact(repoRoot) {
|
|
621
|
+
const tombstonesBefore = await countTombstones(repoRoot);
|
|
622
|
+
const archived = await archiveOldLessons(repoRoot);
|
|
623
|
+
const tombstonesAfterArchive = await countTombstones(repoRoot);
|
|
624
|
+
await rewriteWithoutTombstones(repoRoot);
|
|
625
|
+
const tombstonesRemoved = archived > 0 ? tombstonesBefore : tombstonesAfterArchive;
|
|
626
|
+
const { lessons } = await readLessons(repoRoot);
|
|
566
627
|
return {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
628
|
+
archived,
|
|
629
|
+
tombstonesRemoved,
|
|
630
|
+
lessonsRemaining: lessons.length
|
|
570
631
|
};
|
|
571
632
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
proposedInsight: result.correctionMessage
|
|
603
|
-
};
|
|
633
|
+
|
|
634
|
+
// src/cli/commands/stats.ts
|
|
635
|
+
function registerStatsCommand(program2) {
|
|
636
|
+
program2.command("stats").description("Show database health and statistics").action(async () => {
|
|
637
|
+
const repoRoot = getRepoRoot();
|
|
638
|
+
await syncIfNeeded(repoRoot);
|
|
639
|
+
const { lessons } = await readLessons(repoRoot);
|
|
640
|
+
const deletedCount = await countTombstones(repoRoot);
|
|
641
|
+
const totalLessons = lessons.length;
|
|
642
|
+
const retrievalStats = getRetrievalStats(repoRoot);
|
|
643
|
+
const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
|
|
644
|
+
const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
|
|
645
|
+
const jsonlPath = join(repoRoot, LESSONS_PATH);
|
|
646
|
+
const dbPath = join(repoRoot, DB_PATH);
|
|
647
|
+
let dataSize = 0;
|
|
648
|
+
let indexSize = 0;
|
|
649
|
+
try {
|
|
650
|
+
dataSize = statSync(jsonlPath).size;
|
|
651
|
+
} catch {
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
indexSize = statSync(dbPath).size;
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
const totalSize = dataSize + indexSize;
|
|
658
|
+
const deletedInfo = deletedCount > 0 ? ` (${deletedCount} deleted)` : "";
|
|
659
|
+
console.log(`Lessons: ${totalLessons} total${deletedInfo}`);
|
|
660
|
+
console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
|
|
661
|
+
console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
|
|
662
|
+
});
|
|
604
663
|
}
|
|
605
|
-
function
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
664
|
+
function registerListCommand(program2) {
|
|
665
|
+
program2.command("list").description("List all lessons").option("-n, --limit <number>", "Maximum results", DEFAULT_LIST_LIMIT).action(async function(options) {
|
|
666
|
+
const repoRoot = getRepoRoot();
|
|
667
|
+
const limit = parseLimit(options.limit, "limit");
|
|
668
|
+
const { verbose, quiet } = getGlobalOpts(this);
|
|
669
|
+
const { lessons, skippedCount } = await readLessons(repoRoot);
|
|
670
|
+
if (lessons.length === 0) {
|
|
671
|
+
console.log('No lessons found. Get started with: learn "Your first lesson"');
|
|
672
|
+
if (skippedCount > 0) {
|
|
673
|
+
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const toShow = lessons.slice(0, limit);
|
|
678
|
+
if (!quiet) {
|
|
679
|
+
out.info(`Showing ${toShow.length} of ${lessons.length} lesson(s):
|
|
680
|
+
`);
|
|
681
|
+
}
|
|
682
|
+
for (const lesson of toShow) {
|
|
683
|
+
console.log(`[${chalk.cyan(lesson.id)}] ${lesson.insight}`);
|
|
684
|
+
if (verbose) {
|
|
685
|
+
console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
|
|
686
|
+
console.log(` Created: ${lesson.created}`);
|
|
687
|
+
if (lesson.context) {
|
|
688
|
+
console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
|
|
692
|
+
}
|
|
693
|
+
if (lesson.tags.length > 0) {
|
|
694
|
+
console.log(` Tags: ${lesson.tags.join(", ")}`);
|
|
695
|
+
}
|
|
696
|
+
console.log();
|
|
697
|
+
}
|
|
698
|
+
if (skippedCount > 0) {
|
|
699
|
+
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
616
702
|
}
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
703
|
+
function registerSearchCommand(program2) {
|
|
704
|
+
program2.command("search <query>").description("Search lessons by keyword").option("-n, --limit <number>", "Maximum results", DEFAULT_SEARCH_LIMIT).action(async function(query, options) {
|
|
705
|
+
const repoRoot = getRepoRoot();
|
|
706
|
+
const limit = parseLimit(options.limit, "limit");
|
|
707
|
+
const { verbose, quiet } = getGlobalOpts(this);
|
|
708
|
+
await syncIfNeeded(repoRoot);
|
|
709
|
+
const results = await searchKeyword(repoRoot, query, limit);
|
|
710
|
+
if (results.length === 0) {
|
|
711
|
+
console.log('No lessons match your search. Try a different query or use "list" to see all lessons.');
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (!quiet) {
|
|
715
|
+
out.info(`Found ${results.length} lesson(s):
|
|
716
|
+
`);
|
|
717
|
+
}
|
|
718
|
+
for (const lesson of results) {
|
|
719
|
+
console.log(`[${chalk.cyan(lesson.id)}] ${lesson.insight}`);
|
|
720
|
+
console.log(` Trigger: ${lesson.trigger}`);
|
|
721
|
+
if (verbose && lesson.context) {
|
|
722
|
+
console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
723
|
+
console.log(` Created: ${lesson.created}`);
|
|
724
|
+
}
|
|
725
|
+
if (lesson.tags.length > 0) {
|
|
726
|
+
console.log(` Tags: ${lesson.tags.join(", ")}`);
|
|
727
|
+
}
|
|
728
|
+
console.log();
|
|
729
|
+
}
|
|
730
|
+
});
|
|
627
731
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
732
|
+
|
|
733
|
+
// src/cli/commands/rebuild.ts
|
|
734
|
+
function registerRebuildCommand(program2) {
|
|
735
|
+
program2.command("rebuild").description("Rebuild SQLite index from JSONL").option("-f, --force", "Force rebuild even if unchanged").action(async (options) => {
|
|
736
|
+
const repoRoot = getRepoRoot();
|
|
737
|
+
if (options.force) {
|
|
738
|
+
console.log("Forcing index rebuild...");
|
|
739
|
+
await rebuildIndex(repoRoot);
|
|
740
|
+
console.log("Index rebuilt.");
|
|
741
|
+
} else {
|
|
742
|
+
const rebuilt = await syncIfNeeded(repoRoot);
|
|
743
|
+
if (rebuilt) {
|
|
744
|
+
console.log("Index rebuilt (JSONL changed).");
|
|
745
|
+
} else {
|
|
746
|
+
console.log("Index is up to date.");
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
});
|
|
636
750
|
}
|
|
637
751
|
|
|
638
|
-
// src/cli
|
|
639
|
-
function
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
752
|
+
// src/cli/commands/export.ts
|
|
753
|
+
function registerExportCommand(program2) {
|
|
754
|
+
program2.command("export").description("Export lessons as JSON to stdout").option("--since <date>", "Only include lessons created after this date (ISO8601)").option("--tags <tags>", "Filter by tags (comma-separated, OR logic)").action(async (options) => {
|
|
755
|
+
const repoRoot = getRepoRoot();
|
|
756
|
+
const { lessons } = await readLessons(repoRoot);
|
|
757
|
+
let filtered = lessons;
|
|
758
|
+
if (options.since) {
|
|
759
|
+
const sinceDate = new Date(options.since);
|
|
760
|
+
if (Number.isNaN(sinceDate.getTime())) {
|
|
761
|
+
console.error(`Invalid date format: ${options.since}. Use ISO8601 format (e.g., 2024-01-15).`);
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
filtered = filtered.filter((lesson) => new Date(lesson.created) >= sinceDate);
|
|
765
|
+
}
|
|
766
|
+
if (options.tags) {
|
|
767
|
+
const filterTags = options.tags.split(",").map((t) => t.trim());
|
|
768
|
+
filtered = filtered.filter((lesson) => lesson.tags.some((tag) => filterTags.includes(tag)));
|
|
769
|
+
}
|
|
770
|
+
console.log(JSON.stringify(filtered, null, JSON_INDENT_SPACES));
|
|
771
|
+
});
|
|
646
772
|
}
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
773
|
+
function registerImportCommand(program2) {
|
|
774
|
+
program2.command("import <file>").description("Import lessons from a JSONL file").action(async (file) => {
|
|
775
|
+
const repoRoot = getRepoRoot();
|
|
776
|
+
let content;
|
|
777
|
+
try {
|
|
778
|
+
content = await readFile(file, "utf-8");
|
|
779
|
+
} catch (err) {
|
|
780
|
+
const code = err.code;
|
|
781
|
+
if (code === "ENOENT") {
|
|
782
|
+
console.error(`Error: File not found: ${file}`);
|
|
783
|
+
} else {
|
|
784
|
+
console.error(`Error reading file: ${err.message}`);
|
|
785
|
+
}
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
const { lessons: existingLessons } = await readLessons(repoRoot);
|
|
789
|
+
const existingIds = new Set(existingLessons.map((l) => l.id));
|
|
790
|
+
const lines = content.split("\n");
|
|
791
|
+
let imported = 0;
|
|
792
|
+
let skipped = 0;
|
|
793
|
+
let invalid = 0;
|
|
794
|
+
for (const line of lines) {
|
|
795
|
+
const trimmed = line.trim();
|
|
796
|
+
if (!trimmed) continue;
|
|
797
|
+
let parsed;
|
|
798
|
+
try {
|
|
799
|
+
parsed = JSON.parse(trimmed);
|
|
800
|
+
} catch {
|
|
801
|
+
invalid++;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const result = LessonSchema.safeParse(parsed);
|
|
805
|
+
if (!result.success) {
|
|
806
|
+
invalid++;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
const lesson = result.data;
|
|
810
|
+
if (existingIds.has(lesson.id)) {
|
|
811
|
+
skipped++;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
await appendLesson(repoRoot, lesson);
|
|
815
|
+
existingIds.add(lesson.id);
|
|
816
|
+
imported++;
|
|
817
|
+
}
|
|
818
|
+
await syncIfNeeded(repoRoot);
|
|
819
|
+
const lessonWord = imported === 1 ? "lesson" : "lessons";
|
|
820
|
+
const parts = [];
|
|
821
|
+
if (skipped > 0) parts.push(`${skipped} skipped`);
|
|
822
|
+
if (invalid > 0) parts.push(`${invalid} invalid`);
|
|
823
|
+
if (parts.length > 0) {
|
|
824
|
+
console.log(`Imported ${imported} ${lessonWord} (${parts.join(", ")})`);
|
|
825
|
+
} else {
|
|
826
|
+
console.log(`Imported ${imported} ${lessonWord}`);
|
|
827
|
+
}
|
|
828
|
+
});
|
|
653
829
|
}
|
|
654
|
-
|
|
655
|
-
|
|
830
|
+
|
|
831
|
+
// src/cli/commands/compact.ts
|
|
832
|
+
function registerCompactCommand(program2) {
|
|
833
|
+
program2.command("compact").description("Compact lessons: archive old lessons and remove tombstones").option("-f, --force", "Run compaction even if below threshold").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
|
|
834
|
+
const repoRoot = getRepoRoot();
|
|
835
|
+
const tombstones = await countTombstones(repoRoot);
|
|
836
|
+
const needs = await needsCompaction(repoRoot);
|
|
837
|
+
if (options.dryRun) {
|
|
838
|
+
console.log("Dry run - no changes will be made.\n");
|
|
839
|
+
console.log(`Tombstones found: ${tombstones}`);
|
|
840
|
+
console.log(`Compaction needed: ${needs ? "yes" : "no"}`);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (!needs && !options.force) {
|
|
844
|
+
console.log(`Compaction not needed (${tombstones} tombstones, threshold is ${TOMBSTONE_THRESHOLD}).`);
|
|
845
|
+
console.log("Use --force to compact anyway.");
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
console.log("Running compaction...");
|
|
849
|
+
const result = await compact(repoRoot);
|
|
850
|
+
console.log("\nCompaction complete:");
|
|
851
|
+
console.log(` Archived: ${result.archived} lesson(s)`);
|
|
852
|
+
console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
|
|
853
|
+
console.log(` Lessons remaining: ${result.lessonsRemaining}`);
|
|
854
|
+
await rebuildIndex(repoRoot);
|
|
855
|
+
console.log(" Index rebuilt.");
|
|
856
|
+
});
|
|
656
857
|
}
|
|
657
858
|
var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
|
|
658
859
|
var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
|
|
@@ -759,18 +960,185 @@ function rankLessons(lessons) {
|
|
|
759
960
|
})).sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
|
|
760
961
|
}
|
|
761
962
|
|
|
762
|
-
// src/
|
|
763
|
-
var
|
|
764
|
-
function
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
);
|
|
772
|
-
|
|
773
|
-
|
|
963
|
+
// src/capture/quality.ts
|
|
964
|
+
var DEFAULT_SIMILARITY_THRESHOLD = 0.8;
|
|
965
|
+
async function isNovel(repoRoot, insight, options = {}) {
|
|
966
|
+
const threshold = options.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
967
|
+
await syncIfNeeded(repoRoot);
|
|
968
|
+
const words = insight.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3).slice(0, 3);
|
|
969
|
+
if (words.length === 0) {
|
|
970
|
+
return { novel: true };
|
|
971
|
+
}
|
|
972
|
+
const searchQuery = words.join(" OR ");
|
|
973
|
+
const results = await searchKeyword(repoRoot, searchQuery, 10);
|
|
974
|
+
if (results.length === 0) {
|
|
975
|
+
return { novel: true };
|
|
976
|
+
}
|
|
977
|
+
const insightWords = new Set(insight.toLowerCase().split(/\s+/));
|
|
978
|
+
for (const lesson of results) {
|
|
979
|
+
const lessonWords = new Set(lesson.insight.toLowerCase().split(/\s+/));
|
|
980
|
+
const intersection = [...insightWords].filter((w) => lessonWords.has(w)).length;
|
|
981
|
+
const union = (/* @__PURE__ */ new Set([...insightWords, ...lessonWords])).size;
|
|
982
|
+
const similarity = union > 0 ? intersection / union : 0;
|
|
983
|
+
if (similarity >= threshold) {
|
|
984
|
+
return {
|
|
985
|
+
novel: false,
|
|
986
|
+
reason: `Found similar existing lesson: "${lesson.insight.slice(0, 50)}..."`,
|
|
987
|
+
existingId: lesson.id
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
|
|
991
|
+
return {
|
|
992
|
+
novel: false,
|
|
993
|
+
reason: `Exact duplicate found`,
|
|
994
|
+
existingId: lesson.id
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return { novel: true };
|
|
999
|
+
}
|
|
1000
|
+
var MIN_WORD_COUNT = 4;
|
|
1001
|
+
var VAGUE_PATTERNS = [
|
|
1002
|
+
/\bwrite better\b/i,
|
|
1003
|
+
/\bbe careful\b/i,
|
|
1004
|
+
/\bremember to\b/i,
|
|
1005
|
+
/\bmake sure\b/i,
|
|
1006
|
+
/\btry to\b/i,
|
|
1007
|
+
/\bdouble check\b/i
|
|
1008
|
+
];
|
|
1009
|
+
var GENERIC_IMPERATIVE_PATTERN = /^(always|never)\s+\w+(\s+\w+){0,2}$/i;
|
|
1010
|
+
function isSpecific(insight) {
|
|
1011
|
+
const words = insight.trim().split(/\s+/).filter((w) => w.length > 0);
|
|
1012
|
+
if (words.length < MIN_WORD_COUNT) {
|
|
1013
|
+
return { specific: false, reason: "Insight is too short to be actionable" };
|
|
1014
|
+
}
|
|
1015
|
+
for (const pattern of VAGUE_PATTERNS) {
|
|
1016
|
+
if (pattern.test(insight)) {
|
|
1017
|
+
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (GENERIC_IMPERATIVE_PATTERN.test(insight)) {
|
|
1021
|
+
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
1022
|
+
}
|
|
1023
|
+
return { specific: true };
|
|
1024
|
+
}
|
|
1025
|
+
var ACTION_PATTERNS = [
|
|
1026
|
+
/\buse\s+.+\s+instead\s+of\b/i,
|
|
1027
|
+
// "use X instead of Y"
|
|
1028
|
+
/\bprefer\s+.+\s+(over|to)\b/i,
|
|
1029
|
+
// "prefer X over Y" or "prefer X to Y"
|
|
1030
|
+
/\balways\s+.+\s+when\b/i,
|
|
1031
|
+
// "always X when Y"
|
|
1032
|
+
/\bnever\s+.+\s+without\b/i,
|
|
1033
|
+
// "never X without Y"
|
|
1034
|
+
/\bavoid\s+(using\s+)?\w+/i,
|
|
1035
|
+
// "avoid X" or "avoid using X"
|
|
1036
|
+
/\bcheck\s+.+\s+before\b/i,
|
|
1037
|
+
// "check X before Y"
|
|
1038
|
+
/^(run|use|add|remove|install|update|configure|set|enable|disable)\s+/i
|
|
1039
|
+
// Imperative commands at start
|
|
1040
|
+
];
|
|
1041
|
+
function isActionable(insight) {
|
|
1042
|
+
for (const pattern of ACTION_PATTERNS) {
|
|
1043
|
+
if (pattern.test(insight)) {
|
|
1044
|
+
return { actionable: true };
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return { actionable: false, reason: "Insight lacks clear action guidance" };
|
|
1048
|
+
}
|
|
1049
|
+
async function shouldPropose(repoRoot, insight) {
|
|
1050
|
+
const specificResult = isSpecific(insight);
|
|
1051
|
+
if (!specificResult.specific) {
|
|
1052
|
+
return { shouldPropose: false, reason: specificResult.reason };
|
|
1053
|
+
}
|
|
1054
|
+
const actionableResult = isActionable(insight);
|
|
1055
|
+
if (!actionableResult.actionable) {
|
|
1056
|
+
return { shouldPropose: false, reason: actionableResult.reason };
|
|
1057
|
+
}
|
|
1058
|
+
const noveltyResult = await isNovel(repoRoot, insight);
|
|
1059
|
+
if (!noveltyResult.novel) {
|
|
1060
|
+
return { shouldPropose: false, reason: noveltyResult.reason };
|
|
1061
|
+
}
|
|
1062
|
+
return { shouldPropose: true };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/capture/triggers.ts
|
|
1066
|
+
var USER_CORRECTION_PATTERNS = [
|
|
1067
|
+
/\bno\b[,.]?\s/i,
|
|
1068
|
+
// "no, ..." or "no ..."
|
|
1069
|
+
/\bwrong\b/i,
|
|
1070
|
+
// "wrong"
|
|
1071
|
+
/\bactually\b/i,
|
|
1072
|
+
// "actually..."
|
|
1073
|
+
/\bnot that\b/i,
|
|
1074
|
+
// "not that"
|
|
1075
|
+
/\bi meant\b/i
|
|
1076
|
+
// "I meant"
|
|
1077
|
+
];
|
|
1078
|
+
function detectUserCorrection(signals) {
|
|
1079
|
+
const { messages, context } = signals;
|
|
1080
|
+
if (messages.length < 2) {
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
for (let i = 1; i < messages.length; i++) {
|
|
1084
|
+
const message = messages[i];
|
|
1085
|
+
if (!message) continue;
|
|
1086
|
+
for (const pattern of USER_CORRECTION_PATTERNS) {
|
|
1087
|
+
if (pattern.test(message)) {
|
|
1088
|
+
return {
|
|
1089
|
+
trigger: `User correction during ${context.intent}`,
|
|
1090
|
+
correctionMessage: message,
|
|
1091
|
+
context
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
function detectSelfCorrection(history) {
|
|
1099
|
+
const { edits } = history;
|
|
1100
|
+
if (edits.length < 3) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
for (let i = 0; i <= edits.length - 3; i++) {
|
|
1104
|
+
const first = edits[i];
|
|
1105
|
+
const second = edits[i + 1];
|
|
1106
|
+
const third = edits[i + 2];
|
|
1107
|
+
if (!first || !second || !third) continue;
|
|
1108
|
+
if (first.file === second.file && second.file === third.file && first.success && !second.success && third.success) {
|
|
1109
|
+
return {
|
|
1110
|
+
file: first.file,
|
|
1111
|
+
trigger: `Self-correction on ${first.file}`
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
function detectTestFailure(testResult) {
|
|
1118
|
+
if (testResult.passed) {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
const lines = testResult.output.split("\n").filter((line) => line.trim().length > 0);
|
|
1122
|
+
const errorLine = lines.find((line) => /error|fail|assert/i.test(line)) ?? lines[0] ?? "";
|
|
1123
|
+
return {
|
|
1124
|
+
testFile: testResult.testFile,
|
|
1125
|
+
errorOutput: testResult.output,
|
|
1126
|
+
trigger: `Test failure in ${testResult.testFile}: ${errorLine.slice(0, 100)}`
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/retrieval/session.ts
|
|
1131
|
+
var DEFAULT_LIMIT2 = 5;
|
|
1132
|
+
function isFullLesson(lesson) {
|
|
1133
|
+
return lesson.type === "full" && lesson.severity !== void 0;
|
|
1134
|
+
}
|
|
1135
|
+
async function loadSessionLessons(repoRoot, limit = DEFAULT_LIMIT2) {
|
|
1136
|
+
const { lessons: allLessons } = await readLessons(repoRoot);
|
|
1137
|
+
const highSeverityLessons = allLessons.filter(
|
|
1138
|
+
(lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed
|
|
1139
|
+
);
|
|
1140
|
+
highSeverityLessons.sort((a, b) => {
|
|
1141
|
+
const dateA = new Date(a.created).getTime();
|
|
774
1142
|
const dateB = new Date(b.created).getTime();
|
|
775
1143
|
return dateB - dateA;
|
|
776
1144
|
});
|
|
@@ -803,163 +1171,410 @@ ${lessonLines.join("\n")}`;
|
|
|
803
1171
|
|
|
804
1172
|
// src/index.ts
|
|
805
1173
|
var VERSION = "0.1.0";
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1174
|
+
|
|
1175
|
+
// src/cli/commands/download-model.ts
|
|
1176
|
+
function registerDownloadModelCommand(program2) {
|
|
1177
|
+
program2.command("download-model").description("Download the embedding model for semantic search").option("--json", "Output as JSON").action(async (options) => {
|
|
1178
|
+
const alreadyExisted = isModelAvailable();
|
|
1179
|
+
if (alreadyExisted) {
|
|
1180
|
+
const modelPath2 = join(homedir(), ".node-llama-cpp", "models", MODEL_FILENAME);
|
|
1181
|
+
const size2 = statSync(modelPath2).size;
|
|
1182
|
+
if (options.json) {
|
|
1183
|
+
console.log(JSON.stringify({ success: true, path: modelPath2, size: size2, alreadyExisted: true }));
|
|
1184
|
+
} else {
|
|
1185
|
+
console.log("Model already exists.");
|
|
1186
|
+
console.log(`Path: ${modelPath2}`);
|
|
1187
|
+
console.log(`Size: ${formatBytes(size2)}`);
|
|
1188
|
+
}
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (!options.json) {
|
|
1192
|
+
console.log("Downloading embedding model...");
|
|
1193
|
+
}
|
|
1194
|
+
const modelPath = await resolveModel({ cli: !options.json });
|
|
1195
|
+
const size = statSync(modelPath).size;
|
|
1196
|
+
if (options.json) {
|
|
1197
|
+
console.log(JSON.stringify({ success: true, path: modelPath, size, alreadyExisted: false }));
|
|
1198
|
+
} else {
|
|
1199
|
+
console.log(`
|
|
1200
|
+
Model downloaded successfully!`);
|
|
1201
|
+
console.log(`Path: ${modelPath}`);
|
|
1202
|
+
console.log(`Size: ${formatBytes(size)}`);
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
816
1205
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1206
|
+
var SHOW_JSON_INDENT = JSON_INDENT_SPACES;
|
|
1207
|
+
function formatLessonHuman(lesson) {
|
|
1208
|
+
const lines = [];
|
|
1209
|
+
lines.push(`ID: ${lesson.id}`);
|
|
1210
|
+
lines.push(`Type: ${lesson.type}`);
|
|
1211
|
+
lines.push(`Trigger: ${lesson.trigger}`);
|
|
1212
|
+
lines.push(`Insight: ${lesson.insight}`);
|
|
1213
|
+
if (lesson.evidence) {
|
|
1214
|
+
lines.push(`Evidence: ${lesson.evidence}`);
|
|
824
1215
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const trimmed = line.trim();
|
|
828
|
-
if (!trimmed) continue;
|
|
829
|
-
try {
|
|
830
|
-
const parsed = JSON.parse(trimmed);
|
|
831
|
-
results.push({ line: trimmed, parsed });
|
|
832
|
-
} catch {
|
|
833
|
-
results.push({ line: trimmed, parsed: null });
|
|
834
|
-
}
|
|
1216
|
+
if (lesson.severity) {
|
|
1217
|
+
lines.push(`Severity: ${lesson.severity}`);
|
|
835
1218
|
}
|
|
836
|
-
|
|
1219
|
+
lines.push(`Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
|
|
1220
|
+
lines.push(`Source: ${lesson.source}`);
|
|
1221
|
+
if (lesson.context) {
|
|
1222
|
+
lines.push(`Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
1223
|
+
}
|
|
1224
|
+
lines.push(`Created: ${lesson.created}`);
|
|
1225
|
+
lines.push(`Confirmed: ${lesson.confirmed}`);
|
|
1226
|
+
if (lesson.supersedes.length > 0) {
|
|
1227
|
+
lines.push(`Supersedes: ${lesson.supersedes.join(", ")}`);
|
|
1228
|
+
}
|
|
1229
|
+
if (lesson.related.length > 0) {
|
|
1230
|
+
lines.push(`Related: ${lesson.related.join(", ")}`);
|
|
1231
|
+
}
|
|
1232
|
+
return lines.join("\n");
|
|
837
1233
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1234
|
+
function registerShowCommand(program2) {
|
|
1235
|
+
program2.command("show <id>").description("Show details of a specific lesson").option("--json", "Output as JSON").action(async (id, options) => {
|
|
1236
|
+
const repoRoot = getRepoRoot();
|
|
1237
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1238
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1239
|
+
if (!lesson) {
|
|
1240
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
1241
|
+
let wasDeleted = false;
|
|
1242
|
+
try {
|
|
1243
|
+
const content = await readFile(filePath, "utf-8");
|
|
1244
|
+
const lines = content.split("\n");
|
|
1245
|
+
for (const line of lines) {
|
|
1246
|
+
const trimmed = line.trim();
|
|
1247
|
+
if (!trimmed) continue;
|
|
1248
|
+
try {
|
|
1249
|
+
const record = JSON.parse(trimmed);
|
|
1250
|
+
if (record.id === id && record.deleted === true) {
|
|
1251
|
+
wasDeleted = true;
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
if (options.json) {
|
|
1260
|
+
console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found` }));
|
|
1261
|
+
} else {
|
|
1262
|
+
out.error(wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found`);
|
|
1263
|
+
}
|
|
1264
|
+
process.exit(1);
|
|
844
1265
|
}
|
|
845
|
-
|
|
846
|
-
|
|
1266
|
+
if (options.json) {
|
|
1267
|
+
console.log(JSON.stringify(lesson, null, SHOW_JSON_INDENT));
|
|
1268
|
+
} else {
|
|
1269
|
+
console.log(formatLessonHuman(lesson));
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
847
1272
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1273
|
+
var SHOW_JSON_INDENT2 = JSON_INDENT_SPACES;
|
|
1274
|
+
function registerUpdateCommand(program2) {
|
|
1275
|
+
program2.command("update <id>").description("Update a lesson").option("--insight <text>", "Update insight").option("--trigger <text>", "Update trigger").option("--evidence <text>", "Update evidence").option("--severity <level>", "Update severity (low/medium/high)").option("--tags <tags>", "Update tags (comma-separated)").option("--confirmed <bool>", "Update confirmed status (true/false)").option("--json", "Output as JSON").action(async (id, options) => {
|
|
1276
|
+
const repoRoot = getRepoRoot();
|
|
1277
|
+
const hasUpdates = options.insight !== void 0 || options.trigger !== void 0 || options.evidence !== void 0 || options.severity !== void 0 || options.tags !== void 0 || options.confirmed !== void 0;
|
|
1278
|
+
if (!hasUpdates) {
|
|
1279
|
+
if (options.json) {
|
|
1280
|
+
console.log(JSON.stringify({ error: "No fields to update (specify at least one: --insight, --tags, --severity, ...)" }));
|
|
1281
|
+
} else {
|
|
1282
|
+
out.error("No fields to update (specify at least one: --insight, --tags, --severity, ...)");
|
|
1283
|
+
}
|
|
1284
|
+
process.exit(1);
|
|
1285
|
+
}
|
|
1286
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1287
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1288
|
+
if (!lesson) {
|
|
1289
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
1290
|
+
let wasDeleted = false;
|
|
1291
|
+
try {
|
|
1292
|
+
const content = await readFile(filePath, "utf-8");
|
|
1293
|
+
const lines = content.split("\n");
|
|
1294
|
+
for (const line of lines) {
|
|
1295
|
+
const trimmed = line.trim();
|
|
1296
|
+
if (!trimmed) continue;
|
|
1297
|
+
try {
|
|
1298
|
+
const record = JSON.parse(trimmed);
|
|
1299
|
+
if (record.id === id && record.deleted === true) {
|
|
1300
|
+
wasDeleted = true;
|
|
1301
|
+
break;
|
|
1302
|
+
}
|
|
1303
|
+
} catch {
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
} catch {
|
|
1307
|
+
}
|
|
1308
|
+
if (options.json) {
|
|
1309
|
+
console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found` }));
|
|
1310
|
+
} else {
|
|
1311
|
+
out.error(wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found`);
|
|
1312
|
+
}
|
|
1313
|
+
process.exit(1);
|
|
1314
|
+
}
|
|
1315
|
+
if (options.severity !== void 0) {
|
|
1316
|
+
const result = SeveritySchema.safeParse(options.severity);
|
|
1317
|
+
if (!result.success) {
|
|
1318
|
+
if (options.json) {
|
|
1319
|
+
console.log(JSON.stringify({ error: `Invalid severity '${options.severity}' (must be: high, medium, low)` }));
|
|
1320
|
+
} else {
|
|
1321
|
+
out.error(`Invalid severity '${options.severity}' (must be: high, medium, low)`);
|
|
1322
|
+
}
|
|
1323
|
+
process.exit(1);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const updatedLesson = {
|
|
1327
|
+
...lesson,
|
|
1328
|
+
...options.insight !== void 0 && { insight: options.insight },
|
|
1329
|
+
...options.trigger !== void 0 && { trigger: options.trigger },
|
|
1330
|
+
...options.evidence !== void 0 && { evidence: options.evidence },
|
|
1331
|
+
...options.severity !== void 0 && { severity: options.severity },
|
|
1332
|
+
...options.tags !== void 0 && {
|
|
1333
|
+
tags: [...new Set(
|
|
1334
|
+
options.tags.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
|
|
1335
|
+
)]
|
|
1336
|
+
},
|
|
1337
|
+
...options.confirmed !== void 0 && { confirmed: options.confirmed === "true" }
|
|
1338
|
+
};
|
|
1339
|
+
const validationResult = LessonSchema.safeParse(updatedLesson);
|
|
1340
|
+
if (!validationResult.success) {
|
|
1341
|
+
if (options.json) {
|
|
1342
|
+
console.log(JSON.stringify({ error: `Schema validation failed: ${validationResult.error.message}` }));
|
|
1343
|
+
} else {
|
|
1344
|
+
out.error(`Schema validation failed: ${validationResult.error.message}`);
|
|
1345
|
+
}
|
|
1346
|
+
process.exit(1);
|
|
1347
|
+
}
|
|
1348
|
+
await appendLesson(repoRoot, updatedLesson);
|
|
1349
|
+
await syncIfNeeded(repoRoot);
|
|
1350
|
+
if (options.json) {
|
|
1351
|
+
console.log(JSON.stringify(updatedLesson, null, SHOW_JSON_INDENT2));
|
|
1352
|
+
} else {
|
|
1353
|
+
out.success(`Updated lesson ${id}`);
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
851
1356
|
}
|
|
852
|
-
async function
|
|
1357
|
+
async function wasLessonDeleted(repoRoot, id) {
|
|
853
1358
|
const filePath = join(repoRoot, LESSONS_PATH);
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1359
|
+
try {
|
|
1360
|
+
const content = await readFile(filePath, "utf-8");
|
|
1361
|
+
const lines = content.split("\n");
|
|
1362
|
+
for (const line of lines) {
|
|
1363
|
+
const trimmed = line.trim();
|
|
1364
|
+
if (!trimmed) continue;
|
|
1365
|
+
try {
|
|
1366
|
+
const record = JSON.parse(trimmed);
|
|
1367
|
+
if (record.id === id && record.deleted === true) {
|
|
1368
|
+
return true;
|
|
1369
|
+
}
|
|
1370
|
+
} catch {
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
} catch {
|
|
1374
|
+
}
|
|
1375
|
+
return false;
|
|
868
1376
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1377
|
+
function registerDeleteCommand(program2) {
|
|
1378
|
+
program2.command("delete <ids...>").description("Soft delete lessons (creates tombstone)").option("--json", "Output as JSON").action(async (ids, options) => {
|
|
1379
|
+
const repoRoot = getRepoRoot();
|
|
1380
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1381
|
+
const lessonMap = new Map(lessons.map((l) => [l.id, l]));
|
|
1382
|
+
const deleted = [];
|
|
1383
|
+
const warnings = [];
|
|
1384
|
+
for (const id of ids) {
|
|
1385
|
+
const lesson = lessonMap.get(id);
|
|
1386
|
+
if (!lesson) {
|
|
1387
|
+
const wasDeleted = await wasLessonDeleted(repoRoot, id);
|
|
1388
|
+
warnings.push({ id, message: wasDeleted ? "already deleted" : "not found" });
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
const tombstone = {
|
|
1392
|
+
...lesson,
|
|
1393
|
+
deleted: true,
|
|
1394
|
+
deletedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1395
|
+
};
|
|
1396
|
+
await appendLesson(repoRoot, tombstone);
|
|
1397
|
+
deleted.push(id);
|
|
1398
|
+
}
|
|
1399
|
+
if (deleted.length > 0) {
|
|
1400
|
+
await syncIfNeeded(repoRoot);
|
|
1401
|
+
}
|
|
1402
|
+
if (options.json) {
|
|
1403
|
+
console.log(JSON.stringify({ deleted, warnings }));
|
|
877
1404
|
} else {
|
|
878
|
-
|
|
1405
|
+
if (deleted.length > 0) {
|
|
1406
|
+
out.success(`Deleted ${deleted.length} lesson(s): ${deleted.join(", ")}`);
|
|
1407
|
+
}
|
|
1408
|
+
for (const warning of warnings) {
|
|
1409
|
+
out.warn(`${warning.id}: ${warning.message}`);
|
|
1410
|
+
}
|
|
1411
|
+
if (deleted.length === 0 && warnings.length > 0) {
|
|
1412
|
+
process.exit(1);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
function registerLearnCommand(program2) {
|
|
1418
|
+
program2.command("learn <insight>").description("Capture a new lesson").option("-t, --trigger <text>", "What triggered this lesson").option("--tags <tags>", "Comma-separated tags", "").option("-s, --severity <level>", "Lesson severity: high, medium, low").option("-y, --yes", "Skip confirmation").action(async function(insight, options) {
|
|
1419
|
+
const repoRoot = getRepoRoot();
|
|
1420
|
+
const { quiet } = getGlobalOpts(this);
|
|
1421
|
+
let severity;
|
|
1422
|
+
if (options.severity !== void 0) {
|
|
1423
|
+
const result = SeveritySchema.safeParse(options.severity);
|
|
1424
|
+
if (!result.success) {
|
|
1425
|
+
out.error(`Invalid severity value: "${options.severity}". Valid values are: high, medium, low`);
|
|
1426
|
+
process.exit(1);
|
|
1427
|
+
}
|
|
1428
|
+
severity = result.data;
|
|
879
1429
|
}
|
|
1430
|
+
const lessonType = severity !== void 0 ? "full" : "quick";
|
|
1431
|
+
const lesson = {
|
|
1432
|
+
id: generateId(insight),
|
|
1433
|
+
type: lessonType,
|
|
1434
|
+
trigger: options.trigger ?? "Manual capture",
|
|
1435
|
+
insight,
|
|
1436
|
+
tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : [],
|
|
1437
|
+
source: "manual",
|
|
1438
|
+
context: {
|
|
1439
|
+
tool: "cli",
|
|
1440
|
+
intent: "manual learning"
|
|
1441
|
+
},
|
|
1442
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1443
|
+
confirmed: true,
|
|
1444
|
+
// learn command is explicit confirmation
|
|
1445
|
+
supersedes: [],
|
|
1446
|
+
related: [],
|
|
1447
|
+
...severity !== void 0 && { severity }
|
|
1448
|
+
};
|
|
1449
|
+
await appendLesson(repoRoot, lesson);
|
|
1450
|
+
out.success(`Learned: ${insight}`);
|
|
1451
|
+
if (!quiet) {
|
|
1452
|
+
console.log(`ID: ${chalk.dim(lesson.id)}`);
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
async function detectAndPropose(repoRoot, input) {
|
|
1457
|
+
const detected = runDetector(input);
|
|
1458
|
+
if (!detected) {
|
|
1459
|
+
return null;
|
|
880
1460
|
}
|
|
881
|
-
|
|
882
|
-
|
|
1461
|
+
const { trigger, source, proposedInsight } = detected;
|
|
1462
|
+
const quality = await shouldPropose(repoRoot, proposedInsight);
|
|
1463
|
+
if (!quality.shouldPropose) {
|
|
1464
|
+
return null;
|
|
1465
|
+
}
|
|
1466
|
+
return { trigger, source, proposedInsight };
|
|
1467
|
+
}
|
|
1468
|
+
function runDetector(input) {
|
|
1469
|
+
switch (input.type) {
|
|
1470
|
+
case "user":
|
|
1471
|
+
return detectUserCorrectionFlow(input.data);
|
|
1472
|
+
case "self":
|
|
1473
|
+
return detectSelfCorrectionFlow(input.data);
|
|
1474
|
+
case "test":
|
|
1475
|
+
return detectTestFailureFlow(input.data);
|
|
883
1476
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
group.push(lesson);
|
|
890
|
-
archiveGroups.set(archivePath, group);
|
|
1477
|
+
}
|
|
1478
|
+
function detectUserCorrectionFlow(data) {
|
|
1479
|
+
const result = detectUserCorrection(data);
|
|
1480
|
+
if (!result) {
|
|
1481
|
+
return null;
|
|
891
1482
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1483
|
+
return {
|
|
1484
|
+
trigger: result.trigger,
|
|
1485
|
+
source: "user_correction",
|
|
1486
|
+
proposedInsight: result.correctionMessage
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
function detectSelfCorrectionFlow(data) {
|
|
1490
|
+
const result = detectSelfCorrection(data);
|
|
1491
|
+
if (!result) {
|
|
1492
|
+
return null;
|
|
897
1493
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
return toArchive.length;
|
|
1494
|
+
return {
|
|
1495
|
+
trigger: result.trigger,
|
|
1496
|
+
source: "self_correction",
|
|
1497
|
+
// Self-corrections need context to form useful insights
|
|
1498
|
+
proposedInsight: `Check ${result.file} for common errors before editing`
|
|
1499
|
+
};
|
|
905
1500
|
}
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const tombstonesRemoved = archived > 0 ? tombstonesBefore : tombstonesAfterArchive;
|
|
912
|
-
const { lessons } = await readLessons(repoRoot);
|
|
1501
|
+
function detectTestFailureFlow(data) {
|
|
1502
|
+
const result = detectTestFailure(data);
|
|
1503
|
+
if (!result) {
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
913
1506
|
return {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1507
|
+
trigger: result.trigger,
|
|
1508
|
+
source: "test_failure",
|
|
1509
|
+
proposedInsight: result.errorOutput
|
|
917
1510
|
};
|
|
918
1511
|
}
|
|
1512
|
+
var VALID_TYPES = /* @__PURE__ */ new Set(["user", "self", "test"]);
|
|
1513
|
+
async function parseInputFile(filePath) {
|
|
1514
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1515
|
+
const data = JSON.parse(content);
|
|
1516
|
+
if (!VALID_TYPES.has(data.type)) {
|
|
1517
|
+
throw new Error(`Invalid detection type: ${data.type}. Must be one of: user, self, test`);
|
|
1518
|
+
}
|
|
1519
|
+
return data;
|
|
1520
|
+
}
|
|
919
1521
|
|
|
920
|
-
// src/cli.ts
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1522
|
+
// src/cli/commands/detect.ts
|
|
1523
|
+
function registerDetectCommand(program2) {
|
|
1524
|
+
program2.command("detect").description("Detect learning triggers from input").requiredOption("--input <file>", "Path to JSON input file").option("--save", "Save proposed lesson (requires --yes)").option("-y, --yes", "Confirm save (required with --save)").option("--json", "Output result as JSON").action(
|
|
1525
|
+
async (options) => {
|
|
1526
|
+
const repoRoot = getRepoRoot();
|
|
1527
|
+
if (options.save && !options.yes) {
|
|
1528
|
+
if (options.json) {
|
|
1529
|
+
console.log(JSON.stringify({ error: "--save requires --yes flag for confirmation" }));
|
|
1530
|
+
} else {
|
|
1531
|
+
out.error("--save requires --yes flag for confirmation");
|
|
1532
|
+
console.log("Use: detect --input <file> --save --yes");
|
|
1533
|
+
}
|
|
1534
|
+
process.exit(1);
|
|
1535
|
+
}
|
|
1536
|
+
const input = await parseInputFile(options.input);
|
|
1537
|
+
const result = await detectAndPropose(repoRoot, input);
|
|
1538
|
+
if (!result) {
|
|
1539
|
+
if (options.json) {
|
|
1540
|
+
console.log(JSON.stringify({ detected: false }));
|
|
1541
|
+
} else {
|
|
1542
|
+
console.log("No learning trigger detected.");
|
|
1543
|
+
}
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (options.json) {
|
|
1547
|
+
console.log(JSON.stringify({ detected: true, ...result }));
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
console.log("Learning trigger detected!");
|
|
1551
|
+
console.log(` Trigger: ${result.trigger}`);
|
|
1552
|
+
console.log(` Source: ${result.source}`);
|
|
1553
|
+
console.log(` Proposed: ${result.proposedInsight}`);
|
|
1554
|
+
if (options.save && options.yes) {
|
|
1555
|
+
const lesson = {
|
|
1556
|
+
id: generateId(result.proposedInsight),
|
|
1557
|
+
type: "quick",
|
|
1558
|
+
trigger: result.trigger,
|
|
1559
|
+
insight: result.proposedInsight,
|
|
1560
|
+
tags: [],
|
|
1561
|
+
source: result.source,
|
|
1562
|
+
context: { tool: "detect", intent: "auto-capture" },
|
|
1563
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1564
|
+
confirmed: true,
|
|
1565
|
+
// --yes confirms the lesson
|
|
1566
|
+
supersedes: [],
|
|
1567
|
+
related: []
|
|
1568
|
+
};
|
|
1569
|
+
await appendLesson(repoRoot, lesson);
|
|
1570
|
+
console.log(`
|
|
1571
|
+
Saved as lesson: ${lesson.id}`);
|
|
1572
|
+
}
|
|
939
1573
|
}
|
|
940
|
-
|
|
941
|
-
};
|
|
942
|
-
var HOOK_MARKER = "# Learning Agent pre-commit hook";
|
|
943
|
-
var out = {
|
|
944
|
-
success: (msg) => console.log(chalk.green("[ok]"), msg),
|
|
945
|
-
error: (msg) => console.error(chalk.red("[error]"), msg),
|
|
946
|
-
info: (msg) => console.log(chalk.blue("[info]"), msg),
|
|
947
|
-
warn: (msg) => console.log(chalk.yellow("[warn]"), msg)
|
|
948
|
-
};
|
|
949
|
-
function getGlobalOpts(cmd) {
|
|
950
|
-
const opts = cmd.optsWithGlobals();
|
|
951
|
-
return {
|
|
952
|
-
verbose: opts.verbose ?? false,
|
|
953
|
-
quiet: opts.quiet ?? false
|
|
954
|
-
};
|
|
1574
|
+
);
|
|
955
1575
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
var DEFAULT_CHECK_PLAN_LIMIT = "5";
|
|
959
|
-
var ISO_DATE_PREFIX_LENGTH = 10;
|
|
960
|
-
var AVG_DECIMAL_PLACES = 1;
|
|
961
|
-
var RELEVANCE_DECIMAL_PLACES = 2;
|
|
962
|
-
var JSON_INDENT_SPACES = 2;
|
|
1576
|
+
|
|
1577
|
+
// src/cli/commands/capture.ts
|
|
963
1578
|
function createLessonFromFlags(trigger, insight, confirmed) {
|
|
964
1579
|
return {
|
|
965
1580
|
id: generateId(insight),
|
|
@@ -1008,68 +1623,76 @@ function createLessonFromInputFile(result, confirmed) {
|
|
|
1008
1623
|
related: []
|
|
1009
1624
|
};
|
|
1010
1625
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1626
|
+
function registerCaptureCommand(program2) {
|
|
1627
|
+
program2.command("capture").description("Capture a lesson from trigger/insight or input file").option("-t, --trigger <text>", "What triggered this lesson").option("-i, --insight <text>", "The insight or lesson learned").option("--input <file>", "Path to JSON input file (alternative to trigger/insight)").option("--json", "Output result as JSON").option("-y, --yes", "Skip confirmation and save immediately").action(async function(options) {
|
|
1628
|
+
const repoRoot = getRepoRoot();
|
|
1629
|
+
const { verbose } = getGlobalOpts(this);
|
|
1630
|
+
let lesson;
|
|
1631
|
+
if (options.input) {
|
|
1632
|
+
const input = await parseInputFile(options.input);
|
|
1633
|
+
const result = await detectAndPropose(repoRoot, input);
|
|
1634
|
+
if (!result) {
|
|
1635
|
+
options.json ? console.log(JSON.stringify({ detected: false, saved: false })) : console.log("No learning trigger detected.");
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
lesson = createLessonFromInputFile(result, options.yes ?? false);
|
|
1639
|
+
} else if (options.trigger && options.insight) {
|
|
1640
|
+
lesson = createLessonFromFlags(options.trigger, options.insight, options.yes ?? false);
|
|
1641
|
+
} else {
|
|
1642
|
+
const msg = "Provide either --trigger and --insight, or --input file.";
|
|
1643
|
+
options.json ? console.log(JSON.stringify({ error: msg, saved: false })) : out.error(msg);
|
|
1644
|
+
process.exit(1);
|
|
1017
1645
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
console.log("Relevant to your plan:\n");
|
|
1037
|
-
lessons.forEach((item, i) => {
|
|
1038
|
-
const num = i + 1;
|
|
1039
|
-
console.log(`${num}. ${chalk.bold(`[${item.lesson.id}]`)} ${item.lesson.insight}`);
|
|
1040
|
-
console.log(` - Relevance: ${item.score.toFixed(RELEVANCE_DECIMAL_PLACES)}`);
|
|
1041
|
-
console.log(` - Source: ${item.lesson.source}`);
|
|
1042
|
-
console.log();
|
|
1043
|
-
});
|
|
1044
|
-
if (!quiet) {
|
|
1045
|
-
console.log("---");
|
|
1046
|
-
console.log("Consider these lessons while implementing.");
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
function outputSessionLessonsHuman(lessons, quiet) {
|
|
1050
|
-
console.log("## Session Lessons (High Severity)\n");
|
|
1051
|
-
lessons.forEach((lesson, i) => {
|
|
1052
|
-
const num = i + 1;
|
|
1053
|
-
const date = lesson.created.slice(0, ISO_DATE_PREFIX_LENGTH);
|
|
1054
|
-
console.log(`${num}. ${chalk.bold(`[${lesson.id}]`)} ${lesson.insight}`);
|
|
1055
|
-
console.log(` - Source: ${lesson.source} (${date})`);
|
|
1056
|
-
if (lesson.tags.length > 0) {
|
|
1057
|
-
console.log(` - Tags: ${lesson.tags.join(", ")}`);
|
|
1646
|
+
if (!options.yes && !process.stdin.isTTY) {
|
|
1647
|
+
if (options.json) {
|
|
1648
|
+
console.log(JSON.stringify({ error: "--yes required in non-interactive mode", saved: false }));
|
|
1649
|
+
} else {
|
|
1650
|
+
out.error("--yes required in non-interactive mode");
|
|
1651
|
+
console.log('Use: capture --trigger "..." --insight "..." --yes');
|
|
1652
|
+
}
|
|
1653
|
+
process.exit(1);
|
|
1654
|
+
}
|
|
1655
|
+
if (options.json) {
|
|
1656
|
+
if (options.yes) await appendLesson(repoRoot, lesson);
|
|
1657
|
+
outputCaptureJson(lesson, options.yes ?? false);
|
|
1658
|
+
} else if (options.yes) {
|
|
1659
|
+
await appendLesson(repoRoot, lesson);
|
|
1660
|
+
out.success(`Lesson saved: ${lesson.id}`);
|
|
1661
|
+
if (verbose) console.log(` Type: ${lesson.type} | Trigger: ${lesson.trigger}`);
|
|
1662
|
+
} else {
|
|
1663
|
+
outputCapturePreview(lesson);
|
|
1058
1664
|
}
|
|
1059
|
-
console.log();
|
|
1060
1665
|
});
|
|
1061
|
-
const lessonWord = lessons.length === 1 ? "lesson" : "lessons";
|
|
1062
|
-
if (!quiet) {
|
|
1063
|
-
console.log("---");
|
|
1064
|
-
console.log(`${lessons.length} high-severity ${lessonWord} loaded.`);
|
|
1065
|
-
}
|
|
1066
1666
|
}
|
|
1067
1667
|
var LEARNING_AGENT_SECTION_HEADER = "## Learning Agent Integration";
|
|
1668
|
+
var HOOK_FILE_MODE = 493;
|
|
1669
|
+
var LEARNING_AGENT_HOOK_BLOCK = `
|
|
1670
|
+
# Learning Agent pre-commit hook (appended)
|
|
1671
|
+
npx lna hooks run pre-commit
|
|
1672
|
+
`;
|
|
1068
1673
|
var AGENTS_MD_TEMPLATE = `
|
|
1069
1674
|
## Learning Agent Integration
|
|
1070
1675
|
|
|
1071
1676
|
This project uses learning-agent for session memory.
|
|
1072
1677
|
|
|
1678
|
+
### \u26A0\uFE0F IMPORTANT: Never Edit JSONL Directly
|
|
1679
|
+
|
|
1680
|
+
**DO NOT** manually edit \`.claude/lessons/index.jsonl\`.
|
|
1681
|
+
|
|
1682
|
+
Always use CLI commands:
|
|
1683
|
+
|
|
1684
|
+
\`\`\`bash
|
|
1685
|
+
npx lna learn "insight" --severity high # Create lesson
|
|
1686
|
+
npx lna update <id> --insight "new text" # Update lesson
|
|
1687
|
+
npx lna delete <id> # Delete lesson
|
|
1688
|
+
npx lna list # List all lessons
|
|
1689
|
+
\`\`\`
|
|
1690
|
+
|
|
1691
|
+
Manual edits will:
|
|
1692
|
+
- **Break SQLite sync** - Index becomes stale, search fails
|
|
1693
|
+
- **Bypass schema validation** - Invalid data corrupts the database
|
|
1694
|
+
- **Cause silent failures** - Lessons won't load at session start
|
|
1695
|
+
|
|
1073
1696
|
### Retrieval Points
|
|
1074
1697
|
|
|
1075
1698
|
- **Session start**: High-severity lessons loaded automatically
|
|
@@ -1080,7 +1703,7 @@ This project uses learning-agent for session memory.
|
|
|
1080
1703
|
**BEFORE implementing any plan**, run:
|
|
1081
1704
|
|
|
1082
1705
|
\`\`\`bash
|
|
1083
|
-
npx
|
|
1706
|
+
npx lna check-plan --plan "your plan description" --json
|
|
1084
1707
|
\`\`\`
|
|
1085
1708
|
|
|
1086
1709
|
Display results as a **Lessons Check** section after your plan:
|
|
@@ -1116,14 +1739,14 @@ Before closing a session, reflect on lessons learned:
|
|
|
1116
1739
|
1. **Review**: What mistakes or corrections happened?
|
|
1117
1740
|
2. **Quality gate**: Is it novel, specific, actionable?
|
|
1118
1741
|
3. **Propose**: "Learned: [insight]. Save? [y/n]"
|
|
1119
|
-
4. **Capture**: \`npx
|
|
1742
|
+
4. **Capture**: \`npx lna capture --trigger "..." --insight "..." --yes\`
|
|
1120
1743
|
|
|
1121
1744
|
### CLI Commands
|
|
1122
1745
|
|
|
1123
1746
|
\`\`\`bash
|
|
1124
|
-
npx
|
|
1125
|
-
npx
|
|
1126
|
-
npx
|
|
1747
|
+
npx lna load-session --json # Session start
|
|
1748
|
+
npx lna check-plan --plan "..." --json # Before implementing
|
|
1749
|
+
npx lna capture --trigger "..." --insight "..." --yes
|
|
1127
1750
|
\`\`\`
|
|
1128
1751
|
|
|
1129
1752
|
See [AGENTS.md](https://github.com/Nathandela/learning_agent/blob/main/AGENTS.md) for full documentation.
|
|
@@ -1156,7 +1779,6 @@ async function updateAgentsMd(repoRoot) {
|
|
|
1156
1779
|
await writeFile(agentsPath, newContent, "utf-8");
|
|
1157
1780
|
return true;
|
|
1158
1781
|
}
|
|
1159
|
-
var HOOK_FILE_MODE = 493;
|
|
1160
1782
|
function hasLearningAgentHook(content) {
|
|
1161
1783
|
return content.includes(HOOK_MARKER);
|
|
1162
1784
|
}
|
|
@@ -1177,10 +1799,36 @@ async function getGitHooksDir(repoRoot) {
|
|
|
1177
1799
|
const defaultHooksDir = join(gitDir, "hooks");
|
|
1178
1800
|
return existsSync(defaultHooksDir) ? defaultHooksDir : null;
|
|
1179
1801
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1802
|
+
function findFirstTopLevelExitLine(lines) {
|
|
1803
|
+
let insideFunction = 0;
|
|
1804
|
+
let heredocDelimiter = null;
|
|
1805
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1806
|
+
const line = lines[i] ?? "";
|
|
1807
|
+
const trimmed = line.trim();
|
|
1808
|
+
if (heredocDelimiter !== null) {
|
|
1809
|
+
if (trimmed === heredocDelimiter) {
|
|
1810
|
+
heredocDelimiter = null;
|
|
1811
|
+
}
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
const heredocMatch = /<<-?\s*['"]?(\w+)['"]?/.exec(line);
|
|
1815
|
+
if (heredocMatch?.[1]) {
|
|
1816
|
+
heredocDelimiter = heredocMatch[1];
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
for (const char of line) {
|
|
1820
|
+
if (char === "{") insideFunction++;
|
|
1821
|
+
if (char === "}") insideFunction = Math.max(0, insideFunction - 1);
|
|
1822
|
+
}
|
|
1823
|
+
if (insideFunction > 0) {
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
if (/^\s*exit\s+(\d+|\$\w+|\$\?)\s*$/.test(trimmed)) {
|
|
1827
|
+
return i;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return -1;
|
|
1831
|
+
}
|
|
1184
1832
|
async function installPreCommitHook(repoRoot) {
|
|
1185
1833
|
const gitHooksDir = await getGitHooksDir(repoRoot);
|
|
1186
1834
|
if (!gitHooksDir) {
|
|
@@ -1193,7 +1841,16 @@ async function installPreCommitHook(repoRoot) {
|
|
|
1193
1841
|
if (hasLearningAgentHook(content)) {
|
|
1194
1842
|
return false;
|
|
1195
1843
|
}
|
|
1196
|
-
const
|
|
1844
|
+
const lines = content.split("\n");
|
|
1845
|
+
const exitLineIndex = findFirstTopLevelExitLine(lines);
|
|
1846
|
+
let newContent;
|
|
1847
|
+
if (exitLineIndex === -1) {
|
|
1848
|
+
newContent = content.trimEnd() + "\n" + LEARNING_AGENT_HOOK_BLOCK;
|
|
1849
|
+
} else {
|
|
1850
|
+
const before = lines.slice(0, exitLineIndex);
|
|
1851
|
+
const after = lines.slice(exitLineIndex);
|
|
1852
|
+
newContent = before.join("\n") + LEARNING_AGENT_HOOK_BLOCK + after.join("\n");
|
|
1853
|
+
}
|
|
1197
1854
|
await writeFile(hookPath, newContent, "utf-8");
|
|
1198
1855
|
chmodSync(hookPath, HOOK_FILE_MODE);
|
|
1199
1856
|
return true;
|
|
@@ -1202,586 +1859,339 @@ async function installPreCommitHook(repoRoot) {
|
|
|
1202
1859
|
chmodSync(hookPath, HOOK_FILE_MODE);
|
|
1203
1860
|
return true;
|
|
1204
1861
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
if (!options.skipAgents) {
|
|
1216
|
-
agentsMdUpdated = await updateAgentsMd(repoRoot);
|
|
1217
|
-
}
|
|
1218
|
-
let hooksInstalled = false;
|
|
1219
|
-
if (!options.skipHooks) {
|
|
1220
|
-
hooksInstalled = await installPreCommitHook(repoRoot);
|
|
1221
|
-
}
|
|
1222
|
-
if (options.json) {
|
|
1223
|
-
console.log(JSON.stringify({
|
|
1224
|
-
initialized: true,
|
|
1225
|
-
lessonsDir,
|
|
1226
|
-
agentsMd: agentsMdUpdated,
|
|
1227
|
-
hooks: hooksInstalled
|
|
1228
|
-
}));
|
|
1229
|
-
} else if (!quiet) {
|
|
1230
|
-
out.success("Learning agent initialized");
|
|
1231
|
-
console.log(` Lessons directory: ${lessonsDir}`);
|
|
1232
|
-
if (agentsMdUpdated) {
|
|
1233
|
-
console.log(" AGENTS.md: Updated with Learning Agent section");
|
|
1234
|
-
} else if (options.skipAgents) {
|
|
1235
|
-
console.log(" AGENTS.md: Skipped (--skip-agents)");
|
|
1236
|
-
} else {
|
|
1237
|
-
console.log(" AGENTS.md: Already has Learning Agent section");
|
|
1862
|
+
function registerInitCommand(program2) {
|
|
1863
|
+
program2.command("init").description("Initialize learning-agent in this repository").option("--skip-agents", "Skip AGENTS.md modification").option("--skip-hooks", "Skip git hooks installation").option("--skip-claude", "Skip Claude Code hooks installation").option("--json", "Output result as JSON").action(async function(options) {
|
|
1864
|
+
const repoRoot = getRepoRoot();
|
|
1865
|
+
const { quiet } = getGlobalOpts(this);
|
|
1866
|
+
await createLessonsDirectory(repoRoot);
|
|
1867
|
+
await createIndexFile(repoRoot);
|
|
1868
|
+
const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
|
|
1869
|
+
let agentsMdUpdated = false;
|
|
1870
|
+
if (!options.skipAgents) {
|
|
1871
|
+
agentsMdUpdated = await updateAgentsMd(repoRoot);
|
|
1238
1872
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
console.log(" Git hooks: Skipped (--skip-hooks)");
|
|
1243
|
-
} else {
|
|
1244
|
-
console.log(" Git hooks: Already installed or not a git repo");
|
|
1873
|
+
let hooksInstalled = false;
|
|
1874
|
+
if (!options.skipHooks) {
|
|
1875
|
+
hooksInstalled = await installPreCommitHook(repoRoot);
|
|
1245
1876
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1877
|
+
let claudeHooksInstalled = false;
|
|
1878
|
+
let claudeHooksError = null;
|
|
1879
|
+
if (!options.skipClaude) {
|
|
1880
|
+
try {
|
|
1881
|
+
const settingsPath = getClaudeSettingsPath(false);
|
|
1882
|
+
const settings = await readClaudeSettings(settingsPath);
|
|
1883
|
+
if (!hasClaudeHook(settings)) {
|
|
1884
|
+
addLearningAgentHook(settings);
|
|
1885
|
+
await writeClaudeSettings(settingsPath, settings);
|
|
1886
|
+
claudeHooksInstalled = true;
|
|
1887
|
+
}
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
claudeHooksError = err instanceof Error ? err.message : "Unknown error";
|
|
1890
|
+
}
|
|
1255
1891
|
}
|
|
1256
|
-
} else {
|
|
1257
1892
|
if (options.json) {
|
|
1258
|
-
console.log(JSON.stringify({
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
});
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
if (!hooks.SessionStart) {
|
|
1293
|
-
hooks.SessionStart = [];
|
|
1294
|
-
}
|
|
1295
|
-
hooks.SessionStart.push(CLAUDE_HOOK_CONFIG);
|
|
1296
|
-
}
|
|
1297
|
-
function removeLearningAgentHook(settings) {
|
|
1298
|
-
const hooks = settings.hooks;
|
|
1299
|
-
if (!hooks?.SessionStart) return false;
|
|
1300
|
-
const originalLength = hooks.SessionStart.length;
|
|
1301
|
-
hooks.SessionStart = hooks.SessionStart.filter((entry) => {
|
|
1302
|
-
const hookEntry = entry;
|
|
1303
|
-
return !hookEntry.hooks?.some((h) => h.command?.includes(CLAUDE_HOOK_MARKER));
|
|
1893
|
+
console.log(JSON.stringify({
|
|
1894
|
+
initialized: true,
|
|
1895
|
+
lessonsDir,
|
|
1896
|
+
agentsMd: agentsMdUpdated,
|
|
1897
|
+
hooks: hooksInstalled,
|
|
1898
|
+
claudeHooks: claudeHooksInstalled
|
|
1899
|
+
}));
|
|
1900
|
+
} else if (!quiet) {
|
|
1901
|
+
out.success("Learning agent initialized");
|
|
1902
|
+
console.log(` Lessons directory: ${lessonsDir}`);
|
|
1903
|
+
if (agentsMdUpdated) {
|
|
1904
|
+
console.log(" AGENTS.md: Updated with Learning Agent section");
|
|
1905
|
+
} else if (options.skipAgents) {
|
|
1906
|
+
console.log(" AGENTS.md: Skipped (--skip-agents)");
|
|
1907
|
+
} else {
|
|
1908
|
+
console.log(" AGENTS.md: Already has Learning Agent section");
|
|
1909
|
+
}
|
|
1910
|
+
if (hooksInstalled) {
|
|
1911
|
+
console.log(" Git hooks: pre-commit hook installed");
|
|
1912
|
+
} else if (options.skipHooks) {
|
|
1913
|
+
console.log(" Git hooks: Skipped (--skip-hooks)");
|
|
1914
|
+
} else {
|
|
1915
|
+
console.log(" Git hooks: Already installed or not a git repo");
|
|
1916
|
+
}
|
|
1917
|
+
if (claudeHooksInstalled) {
|
|
1918
|
+
console.log(" Claude Code hooks: Installed to .claude/settings.json");
|
|
1919
|
+
} else if (options.skipClaude) {
|
|
1920
|
+
console.log(" Claude Code hooks: Skipped (--skip-claude)");
|
|
1921
|
+
} else if (claudeHooksError) {
|
|
1922
|
+
console.log(` Claude Code hooks: Error - ${claudeHooksError}`);
|
|
1923
|
+
} else {
|
|
1924
|
+
console.log(" Claude Code hooks: Already installed");
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1304
1927
|
});
|
|
1305
|
-
return hooks.SessionStart.length < originalLength;
|
|
1306
|
-
}
|
|
1307
|
-
async function writeClaudeSettings(settingsPath, settings) {
|
|
1308
|
-
const dir = dirname(settingsPath);
|
|
1309
|
-
await mkdir(dir, { recursive: true });
|
|
1310
|
-
const tempPath = settingsPath + ".tmp";
|
|
1311
|
-
await writeFile(tempPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
1312
|
-
await rename(tempPath, settingsPath);
|
|
1313
1928
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1929
|
+
|
|
1930
|
+
// src/cli/commands/hooks.ts
|
|
1931
|
+
function registerHooksCommand(program2) {
|
|
1932
|
+
const hooksCommand = program2.command("hooks").description("Git hooks management");
|
|
1933
|
+
hooksCommand.command("run <hook>").description("Run a hook script (called by git hooks)").option("--json", "Output as JSON").action((hook, options) => {
|
|
1934
|
+
if (hook === "pre-commit") {
|
|
1935
|
+
if (options.json) {
|
|
1936
|
+
console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
|
|
1937
|
+
} else {
|
|
1938
|
+
console.log(PRE_COMMIT_MESSAGE);
|
|
1939
|
+
}
|
|
1324
1940
|
} else {
|
|
1325
|
-
|
|
1941
|
+
if (options.json) {
|
|
1942
|
+
console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
|
|
1943
|
+
} else {
|
|
1944
|
+
out.error(`Unknown hook: ${hook}`);
|
|
1945
|
+
}
|
|
1946
|
+
process.exit(1);
|
|
1326
1947
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
function registerSetupCommand(program2) {
|
|
1951
|
+
const setupCommand = program2.command("setup").description("Setup integrations");
|
|
1952
|
+
setupCommand.command("claude").description("Install Claude Code SessionStart hooks").option("--global", "Install to global ~/.claude/ instead of project").option("--uninstall", "Remove learning-agent hooks").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action(async (options) => {
|
|
1953
|
+
const settingsPath = getClaudeSettingsPath(options.global ?? false);
|
|
1954
|
+
const displayPath = options.global ? "~/.claude/settings.json" : ".claude/settings.json";
|
|
1955
|
+
let settings;
|
|
1956
|
+
try {
|
|
1957
|
+
settings = await readClaudeSettings(settingsPath);
|
|
1958
|
+
} catch {
|
|
1332
1959
|
if (options.json) {
|
|
1333
|
-
console.log(JSON.stringify({
|
|
1960
|
+
console.log(JSON.stringify({ error: "Failed to parse settings file" }));
|
|
1334
1961
|
} else {
|
|
1335
|
-
if
|
|
1336
|
-
|
|
1962
|
+
out.error("Failed to parse settings file. Check if JSON is valid.");
|
|
1963
|
+
}
|
|
1964
|
+
process.exit(1);
|
|
1965
|
+
}
|
|
1966
|
+
const alreadyInstalled = hasClaudeHook(settings);
|
|
1967
|
+
if (options.uninstall) {
|
|
1968
|
+
if (options.dryRun) {
|
|
1969
|
+
if (options.json) {
|
|
1970
|
+
console.log(JSON.stringify({ dryRun: true, wouldRemove: alreadyInstalled, location: displayPath }));
|
|
1337
1971
|
} else {
|
|
1338
|
-
|
|
1972
|
+
if (alreadyInstalled) {
|
|
1973
|
+
console.log(`Would remove learning-agent hooks from ${displayPath}`);
|
|
1974
|
+
} else {
|
|
1975
|
+
console.log("No learning-agent hooks to remove");
|
|
1976
|
+
}
|
|
1339
1977
|
}
|
|
1978
|
+
return;
|
|
1340
1979
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1980
|
+
const removed = removeLearningAgentHook(settings);
|
|
1981
|
+
if (removed) {
|
|
1982
|
+
await writeClaudeSettings(settingsPath, settings);
|
|
1983
|
+
if (options.json) {
|
|
1984
|
+
console.log(JSON.stringify({ installed: false, location: displayPath, action: "removed" }));
|
|
1985
|
+
} else {
|
|
1986
|
+
out.success("Learning agent hooks removed");
|
|
1987
|
+
console.log(` Location: ${displayPath}`);
|
|
1988
|
+
}
|
|
1348
1989
|
} else {
|
|
1349
|
-
|
|
1350
|
-
|
|
1990
|
+
if (options.json) {
|
|
1991
|
+
console.log(JSON.stringify({ installed: false, location: displayPath, action: "unchanged" }));
|
|
1992
|
+
} else {
|
|
1993
|
+
out.info("No learning agent hooks to remove");
|
|
1994
|
+
if (options.global) {
|
|
1995
|
+
console.log(" Hint: Try without --global to check project settings.");
|
|
1996
|
+
} else {
|
|
1997
|
+
console.log(" Hint: Try with --global flag to check global settings.");
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1351
2000
|
}
|
|
1352
|
-
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
if (options.dryRun) {
|
|
1353
2004
|
if (options.json) {
|
|
1354
|
-
console.log(JSON.stringify({
|
|
2005
|
+
console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
|
|
1355
2006
|
} else {
|
|
1356
|
-
|
|
2007
|
+
if (alreadyInstalled) {
|
|
2008
|
+
console.log("Learning agent hooks already installed");
|
|
2009
|
+
} else {
|
|
2010
|
+
console.log(`Would install learning-agent hooks to ${displayPath}`);
|
|
2011
|
+
}
|
|
1357
2012
|
}
|
|
2013
|
+
return;
|
|
1358
2014
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
2015
|
+
if (alreadyInstalled) {
|
|
2016
|
+
if (options.json) {
|
|
2017
|
+
console.log(JSON.stringify({
|
|
2018
|
+
installed: true,
|
|
2019
|
+
location: displayPath,
|
|
2020
|
+
hooks: ["SessionStart"],
|
|
2021
|
+
action: "unchanged"
|
|
2022
|
+
}));
|
|
1367
2023
|
} else {
|
|
1368
|
-
|
|
2024
|
+
out.info("Learning agent hooks already installed");
|
|
2025
|
+
console.log(` Location: ${displayPath}`);
|
|
1369
2026
|
}
|
|
2027
|
+
return;
|
|
1370
2028
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
2029
|
+
const fileExists = existsSync(settingsPath);
|
|
2030
|
+
addLearningAgentHook(settings);
|
|
2031
|
+
await writeClaudeSettings(settingsPath, settings);
|
|
1374
2032
|
if (options.json) {
|
|
1375
2033
|
console.log(JSON.stringify({
|
|
1376
2034
|
installed: true,
|
|
1377
2035
|
location: displayPath,
|
|
1378
2036
|
hooks: ["SessionStart"],
|
|
1379
|
-
action: "
|
|
2037
|
+
action: fileExists ? "updated" : "created"
|
|
1380
2038
|
}));
|
|
1381
2039
|
} else {
|
|
1382
|
-
out.
|
|
2040
|
+
out.success(options.global ? "Claude Code hooks installed (global)" : "Claude Code hooks installed (project-level)");
|
|
1383
2041
|
console.log(` Location: ${displayPath}`);
|
|
1384
|
-
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
const fileExists = existsSync(settingsPath);
|
|
1388
|
-
addLearningAgentHook(settings);
|
|
1389
|
-
await writeClaudeSettings(settingsPath, settings);
|
|
1390
|
-
if (options.json) {
|
|
1391
|
-
console.log(JSON.stringify({
|
|
1392
|
-
installed: true,
|
|
1393
|
-
location: displayPath,
|
|
1394
|
-
hooks: ["SessionStart"],
|
|
1395
|
-
action: fileExists ? "updated" : "created"
|
|
1396
|
-
}));
|
|
1397
|
-
} else {
|
|
1398
|
-
out.success(options.project ? "Claude Code hooks installed (project-level)" : "Claude Code hooks installed");
|
|
1399
|
-
console.log(` Location: ${displayPath}`);
|
|
1400
|
-
console.log(" Hook: SessionStart (startup|resume|compact)");
|
|
1401
|
-
console.log("");
|
|
1402
|
-
console.log("Lessons will be loaded automatically at session start.");
|
|
1403
|
-
if (options.project) {
|
|
2042
|
+
console.log(" Hook: SessionStart (startup|resume|compact)");
|
|
1404
2043
|
console.log("");
|
|
1405
|
-
console.log("
|
|
2044
|
+
console.log("Lessons will be loaded automatically at session start.");
|
|
2045
|
+
if (!options.global) {
|
|
2046
|
+
console.log("");
|
|
2047
|
+
console.log("Note: Project hooks override global hooks.");
|
|
2048
|
+
}
|
|
1406
2049
|
}
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
insight,
|
|
1417
|
-
tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : [],
|
|
1418
|
-
source: "manual",
|
|
1419
|
-
context: {
|
|
1420
|
-
tool: "cli",
|
|
1421
|
-
intent: "manual learning"
|
|
1422
|
-
},
|
|
1423
|
-
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1424
|
-
confirmed: true,
|
|
1425
|
-
// learn command is explicit confirmation
|
|
1426
|
-
supersedes: [],
|
|
1427
|
-
related: []
|
|
1428
|
-
};
|
|
1429
|
-
await appendLesson(repoRoot, lesson);
|
|
1430
|
-
out.success(`Learned: ${insight}`);
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// src/cli/commands/load-session.ts
|
|
2054
|
+
function formatSource(source) {
|
|
2055
|
+
return source.replace(/_/g, " ");
|
|
2056
|
+
}
|
|
2057
|
+
function outputSessionLessonsHuman(lessons, quiet) {
|
|
2058
|
+
console.log("## Lessons from Past Sessions\n");
|
|
1431
2059
|
if (!quiet) {
|
|
1432
|
-
console.log(
|
|
2060
|
+
console.log("These lessons were captured from previous corrections and should inform your work:\n");
|
|
1433
2061
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
const results = await searchKeyword(repoRoot, query, limit);
|
|
1441
|
-
if (results.length === 0) {
|
|
1442
|
-
console.log('No lessons match your search. Try a different query or use "list" to see all lessons.');
|
|
1443
|
-
return;
|
|
2062
|
+
for (let i = 0; i < lessons.length; i++) {
|
|
2063
|
+
const lesson = lessons[i];
|
|
2064
|
+
const tags = lesson.tags.length > 0 ? ` (${lesson.tags.join(", ")})` : "";
|
|
2065
|
+
console.log(`${i + 1}. **${lesson.insight}**${tags}`);
|
|
2066
|
+
console.log(` Learned: ${lesson.created.slice(0, ISO_DATE_PREFIX_LENGTH)} via ${formatSource(lesson.source)}`);
|
|
2067
|
+
console.log();
|
|
1444
2068
|
}
|
|
1445
2069
|
if (!quiet) {
|
|
1446
|
-
|
|
1447
|
-
`);
|
|
2070
|
+
console.log("Consider these lessons when planning and implementing tasks.");
|
|
1448
2071
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
2072
|
+
}
|
|
2073
|
+
function registerLoadSessionCommand(program2) {
|
|
2074
|
+
program2.command("load-session").description("Load high-severity lessons for session context").option("--json", "Output as JSON").action(async function(options) {
|
|
2075
|
+
const repoRoot = getRepoRoot();
|
|
2076
|
+
const { quiet } = getGlobalOpts(this);
|
|
2077
|
+
const lessons = await loadSessionLessons(repoRoot);
|
|
2078
|
+
if (options.json) {
|
|
2079
|
+
console.log(JSON.stringify({ lessons, count: lessons.length }));
|
|
2080
|
+
return;
|
|
1455
2081
|
}
|
|
1456
|
-
if (
|
|
1457
|
-
console.log(
|
|
2082
|
+
if (lessons.length === 0) {
|
|
2083
|
+
console.log("No high-severity lessons found.");
|
|
2084
|
+
return;
|
|
1458
2085
|
}
|
|
1459
|
-
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
console.log('No lessons found. Get started with: learn "Your first lesson"');
|
|
1469
|
-
if (skippedCount > 0) {
|
|
1470
|
-
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
2086
|
+
outputSessionLessonsHuman(lessons, quiet);
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
async function readPlanFromStdin() {
|
|
2090
|
+
const { stdin } = await import('process');
|
|
2091
|
+
if (!stdin.isTTY) {
|
|
2092
|
+
const chunks = [];
|
|
2093
|
+
for await (const chunk of stdin) {
|
|
2094
|
+
chunks.push(chunk);
|
|
1471
2095
|
}
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
const toShow = lessons.slice(0, limit);
|
|
1475
|
-
if (!quiet) {
|
|
1476
|
-
out.info(`Showing ${toShow.length} of ${lessons.length} lesson(s):
|
|
1477
|
-
`);
|
|
2096
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
1478
2097
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2098
|
+
return void 0;
|
|
2099
|
+
}
|
|
2100
|
+
function outputCheckPlanJson(lessons) {
|
|
2101
|
+
const jsonOutput = {
|
|
2102
|
+
lessons: lessons.map((l) => ({
|
|
2103
|
+
id: l.lesson.id,
|
|
2104
|
+
insight: l.lesson.insight,
|
|
2105
|
+
relevance: l.score,
|
|
2106
|
+
source: l.lesson.source
|
|
2107
|
+
})),
|
|
2108
|
+
count: lessons.length
|
|
2109
|
+
};
|
|
2110
|
+
console.log(JSON.stringify(jsonOutput));
|
|
2111
|
+
}
|
|
2112
|
+
function outputCheckPlanHuman(lessons, quiet) {
|
|
2113
|
+
console.log("## Lessons Check\n");
|
|
2114
|
+
console.log("Relevant to your plan:\n");
|
|
2115
|
+
lessons.forEach((item, i) => {
|
|
2116
|
+
const num = i + 1;
|
|
2117
|
+
console.log(`${num}. ${chalk.bold(`[${item.lesson.id}]`)} ${item.lesson.insight}`);
|
|
2118
|
+
console.log(` - Relevance: ${item.score.toFixed(RELEVANCE_DECIMAL_PLACES)}`);
|
|
2119
|
+
console.log(` - Source: ${item.lesson.source}`);
|
|
1493
2120
|
console.log();
|
|
2121
|
+
});
|
|
2122
|
+
if (!quiet) {
|
|
2123
|
+
console.log("---");
|
|
2124
|
+
console.log("Consider these lessons while implementing.");
|
|
1494
2125
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
});
|
|
1499
|
-
program.command("rebuild").description("Rebuild SQLite index from JSONL").option("-f, --force", "Force rebuild even if unchanged").action(async (options) => {
|
|
1500
|
-
const repoRoot = getRepoRoot();
|
|
1501
|
-
if (options.force) {
|
|
1502
|
-
console.log("Forcing index rebuild...");
|
|
1503
|
-
await rebuildIndex(repoRoot);
|
|
1504
|
-
console.log("Index rebuilt.");
|
|
1505
|
-
} else {
|
|
1506
|
-
const rebuilt = await syncIfNeeded(repoRoot);
|
|
1507
|
-
if (rebuilt) {
|
|
1508
|
-
console.log("Index rebuilt (JSONL changed).");
|
|
1509
|
-
} else {
|
|
1510
|
-
console.log("Index is up to date.");
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
});
|
|
1514
|
-
program.command("detect").description("Detect learning triggers from input").requiredOption("--input <file>", "Path to JSON input file").option("--save", "Save proposed lesson (requires --yes)").option("-y, --yes", "Confirm save (required with --save)").option("--json", "Output result as JSON").action(
|
|
1515
|
-
async (options) => {
|
|
2126
|
+
}
|
|
2127
|
+
function registerCheckPlanCommand(program2) {
|
|
2128
|
+
program2.command("check-plan").description("Check plan against relevant lessons").option("--plan <text>", "Plan text to check").option("--json", "Output as JSON").option("-n, --limit <number>", "Maximum results", DEFAULT_CHECK_PLAN_LIMIT).action(async function(options) {
|
|
1516
2129
|
const repoRoot = getRepoRoot();
|
|
1517
|
-
|
|
2130
|
+
const limit = parseLimit(options.limit, "limit");
|
|
2131
|
+
const { quiet } = getGlobalOpts(this);
|
|
2132
|
+
const planText = options.plan ?? await readPlanFromStdin();
|
|
2133
|
+
if (!planText) {
|
|
2134
|
+
out.error("No plan provided. Use --plan <text> or pipe text to stdin.");
|
|
2135
|
+
process.exit(1);
|
|
2136
|
+
}
|
|
2137
|
+
if (!isModelAvailable()) {
|
|
1518
2138
|
if (options.json) {
|
|
1519
|
-
console.log(JSON.stringify({
|
|
2139
|
+
console.log(JSON.stringify({
|
|
2140
|
+
error: "Embedding model not available",
|
|
2141
|
+
action: "Run: npx lna download-model"
|
|
2142
|
+
}));
|
|
1520
2143
|
} else {
|
|
1521
|
-
out.error("
|
|
1522
|
-
console.log("
|
|
2144
|
+
out.error("Embedding model not available");
|
|
2145
|
+
console.log("");
|
|
2146
|
+
console.log("Run: npx lna download-model");
|
|
1523
2147
|
}
|
|
1524
2148
|
process.exit(1);
|
|
1525
2149
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
2150
|
+
try {
|
|
2151
|
+
const result = await retrieveForPlan(repoRoot, planText, limit);
|
|
2152
|
+
if (options.json) {
|
|
2153
|
+
outputCheckPlanJson(result.lessons);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
if (result.lessons.length === 0) {
|
|
2157
|
+
console.log("No relevant lessons found for this plan.");
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
outputCheckPlanHuman(result.lessons, quiet);
|
|
2161
|
+
} catch (err) {
|
|
2162
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1529
2163
|
if (options.json) {
|
|
1530
|
-
console.log(JSON.stringify({
|
|
2164
|
+
console.log(JSON.stringify({ error: message }));
|
|
1531
2165
|
} else {
|
|
1532
|
-
|
|
2166
|
+
out.error(`Failed to check plan: ${message}`);
|
|
1533
2167
|
}
|
|
1534
|
-
return;
|
|
1535
|
-
}
|
|
1536
|
-
if (options.json) {
|
|
1537
|
-
console.log(JSON.stringify({ detected: true, ...result }));
|
|
1538
|
-
return;
|
|
1539
|
-
}
|
|
1540
|
-
console.log("Learning trigger detected!");
|
|
1541
|
-
console.log(` Trigger: ${result.trigger}`);
|
|
1542
|
-
console.log(` Source: ${result.source}`);
|
|
1543
|
-
console.log(` Proposed: ${result.proposedInsight}`);
|
|
1544
|
-
if (options.save && options.yes) {
|
|
1545
|
-
const lesson = {
|
|
1546
|
-
id: generateId(result.proposedInsight),
|
|
1547
|
-
type: "quick",
|
|
1548
|
-
trigger: result.trigger,
|
|
1549
|
-
insight: result.proposedInsight,
|
|
1550
|
-
tags: [],
|
|
1551
|
-
source: result.source,
|
|
1552
|
-
context: { tool: "detect", intent: "auto-capture" },
|
|
1553
|
-
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1554
|
-
confirmed: true,
|
|
1555
|
-
// --yes confirms the lesson
|
|
1556
|
-
supersedes: [],
|
|
1557
|
-
related: []
|
|
1558
|
-
};
|
|
1559
|
-
await appendLesson(repoRoot, lesson);
|
|
1560
|
-
console.log(`
|
|
1561
|
-
Saved as lesson: ${lesson.id}`);
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
);
|
|
1565
|
-
program.command("capture").description("Capture a lesson from trigger/insight or input file").option("-t, --trigger <text>", "What triggered this lesson").option("-i, --insight <text>", "The insight or lesson learned").option("--input <file>", "Path to JSON input file (alternative to trigger/insight)").option("--json", "Output result as JSON").option("-y, --yes", "Skip confirmation and save immediately").action(async function(options) {
|
|
1566
|
-
const repoRoot = getRepoRoot();
|
|
1567
|
-
const { verbose } = getGlobalOpts(this);
|
|
1568
|
-
let lesson;
|
|
1569
|
-
if (options.input) {
|
|
1570
|
-
const input = await parseInputFile(options.input);
|
|
1571
|
-
const result = await detectAndPropose(repoRoot, input);
|
|
1572
|
-
if (!result) {
|
|
1573
|
-
options.json ? console.log(JSON.stringify({ detected: false, saved: false })) : console.log("No learning trigger detected.");
|
|
1574
|
-
return;
|
|
1575
|
-
}
|
|
1576
|
-
lesson = createLessonFromInputFile(result, options.yes ?? false);
|
|
1577
|
-
} else if (options.trigger && options.insight) {
|
|
1578
|
-
lesson = createLessonFromFlags(options.trigger, options.insight, options.yes ?? false);
|
|
1579
|
-
} else {
|
|
1580
|
-
const msg = "Provide either --trigger and --insight, or --input file.";
|
|
1581
|
-
options.json ? console.log(JSON.stringify({ error: msg, saved: false })) : out.error(msg);
|
|
1582
|
-
process.exit(1);
|
|
1583
|
-
}
|
|
1584
|
-
if (!options.yes && !process.stdin.isTTY) {
|
|
1585
|
-
if (options.json) {
|
|
1586
|
-
console.log(JSON.stringify({ error: "--yes required in non-interactive mode", saved: false }));
|
|
1587
|
-
} else {
|
|
1588
|
-
out.error("--yes required in non-interactive mode");
|
|
1589
|
-
console.log('Use: capture --trigger "..." --insight "..." --yes');
|
|
1590
|
-
}
|
|
1591
|
-
process.exit(1);
|
|
1592
|
-
}
|
|
1593
|
-
if (options.json) {
|
|
1594
|
-
if (options.yes) await appendLesson(repoRoot, lesson);
|
|
1595
|
-
outputCaptureJson(lesson, options.yes ?? false);
|
|
1596
|
-
} else if (options.yes) {
|
|
1597
|
-
await appendLesson(repoRoot, lesson);
|
|
1598
|
-
out.success(`Lesson saved: ${lesson.id}`);
|
|
1599
|
-
if (verbose) console.log(` Type: ${lesson.type} | Trigger: ${lesson.trigger}`);
|
|
1600
|
-
} else {
|
|
1601
|
-
outputCapturePreview(lesson);
|
|
1602
|
-
}
|
|
1603
|
-
});
|
|
1604
|
-
program.command("compact").description("Compact lessons: archive old lessons and remove tombstones").option("-f, --force", "Run compaction even if below threshold").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
|
|
1605
|
-
const repoRoot = getRepoRoot();
|
|
1606
|
-
const tombstones = await countTombstones(repoRoot);
|
|
1607
|
-
const needs = await needsCompaction(repoRoot);
|
|
1608
|
-
if (options.dryRun) {
|
|
1609
|
-
console.log("Dry run - no changes will be made.\n");
|
|
1610
|
-
console.log(`Tombstones found: ${tombstones}`);
|
|
1611
|
-
console.log(`Compaction needed: ${needs ? "yes" : "no"}`);
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
if (!needs && !options.force) {
|
|
1615
|
-
console.log(`Compaction not needed (${tombstones} tombstones, threshold is ${TOMBSTONE_THRESHOLD}).`);
|
|
1616
|
-
console.log("Use --force to compact anyway.");
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
console.log("Running compaction...");
|
|
1620
|
-
const result = await compact(repoRoot);
|
|
1621
|
-
console.log("\nCompaction complete:");
|
|
1622
|
-
console.log(` Archived: ${result.archived} lesson(s)`);
|
|
1623
|
-
console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
|
|
1624
|
-
console.log(` Lessons remaining: ${result.lessonsRemaining}`);
|
|
1625
|
-
await rebuildIndex(repoRoot);
|
|
1626
|
-
console.log(" Index rebuilt.");
|
|
1627
|
-
});
|
|
1628
|
-
program.command("export").description("Export lessons as JSON to stdout").option("--since <date>", "Only include lessons created after this date (ISO8601)").option("--tags <tags>", "Filter by tags (comma-separated, OR logic)").action(async (options) => {
|
|
1629
|
-
const repoRoot = getRepoRoot();
|
|
1630
|
-
const { lessons } = await readLessons(repoRoot);
|
|
1631
|
-
let filtered = lessons;
|
|
1632
|
-
if (options.since) {
|
|
1633
|
-
const sinceDate = new Date(options.since);
|
|
1634
|
-
if (Number.isNaN(sinceDate.getTime())) {
|
|
1635
|
-
console.error(`Invalid date format: ${options.since}. Use ISO8601 format (e.g., 2024-01-15).`);
|
|
1636
2168
|
process.exit(1);
|
|
1637
2169
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
program
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
const lines = content.split("\n");
|
|
1664
|
-
let imported = 0;
|
|
1665
|
-
let skipped = 0;
|
|
1666
|
-
let invalid = 0;
|
|
1667
|
-
for (const line of lines) {
|
|
1668
|
-
const trimmed = line.trim();
|
|
1669
|
-
if (!trimmed) continue;
|
|
1670
|
-
let parsed;
|
|
1671
|
-
try {
|
|
1672
|
-
parsed = JSON.parse(trimmed);
|
|
1673
|
-
} catch {
|
|
1674
|
-
invalid++;
|
|
1675
|
-
continue;
|
|
1676
|
-
}
|
|
1677
|
-
const result = LessonSchema.safeParse(parsed);
|
|
1678
|
-
if (!result.success) {
|
|
1679
|
-
invalid++;
|
|
1680
|
-
continue;
|
|
1681
|
-
}
|
|
1682
|
-
const lesson = result.data;
|
|
1683
|
-
if (existingIds.has(lesson.id)) {
|
|
1684
|
-
skipped++;
|
|
1685
|
-
continue;
|
|
1686
|
-
}
|
|
1687
|
-
await appendLesson(repoRoot, lesson);
|
|
1688
|
-
existingIds.add(lesson.id);
|
|
1689
|
-
imported++;
|
|
1690
|
-
}
|
|
1691
|
-
const lessonWord = imported === 1 ? "lesson" : "lessons";
|
|
1692
|
-
const parts = [];
|
|
1693
|
-
if (skipped > 0) parts.push(`${skipped} skipped`);
|
|
1694
|
-
if (invalid > 0) parts.push(`${invalid} invalid`);
|
|
1695
|
-
if (parts.length > 0) {
|
|
1696
|
-
console.log(`Imported ${imported} ${lessonWord} (${parts.join(", ")})`);
|
|
1697
|
-
} else {
|
|
1698
|
-
console.log(`Imported ${imported} ${lessonWord}`);
|
|
1699
|
-
}
|
|
1700
|
-
});
|
|
1701
|
-
program.command("stats").description("Show database health and statistics").action(async () => {
|
|
1702
|
-
const repoRoot = getRepoRoot();
|
|
1703
|
-
await syncIfNeeded(repoRoot);
|
|
1704
|
-
const { lessons } = await readLessons(repoRoot);
|
|
1705
|
-
const deletedCount = await countTombstones(repoRoot);
|
|
1706
|
-
const totalLessons = lessons.length;
|
|
1707
|
-
const retrievalStats = getRetrievalStats(repoRoot);
|
|
1708
|
-
const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
|
|
1709
|
-
const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
|
|
1710
|
-
const jsonlPath = join(repoRoot, LESSONS_PATH);
|
|
1711
|
-
const dbPath = join(repoRoot, DB_PATH);
|
|
1712
|
-
let dataSize = 0;
|
|
1713
|
-
let indexSize = 0;
|
|
1714
|
-
try {
|
|
1715
|
-
dataSize = statSync(jsonlPath).size;
|
|
1716
|
-
} catch {
|
|
1717
|
-
}
|
|
1718
|
-
try {
|
|
1719
|
-
indexSize = statSync(dbPath).size;
|
|
1720
|
-
} catch {
|
|
1721
|
-
}
|
|
1722
|
-
const totalSize = dataSize + indexSize;
|
|
1723
|
-
const deletedInfo = deletedCount > 0 ? ` (${deletedCount} deleted)` : "";
|
|
1724
|
-
console.log(`Lessons: ${totalLessons} total${deletedInfo}`);
|
|
1725
|
-
console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
|
|
1726
|
-
console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
|
|
1727
|
-
});
|
|
1728
|
-
program.command("load-session").description("Load high-severity lessons for session context").option("--json", "Output as JSON").action(async function(options) {
|
|
1729
|
-
const repoRoot = getRepoRoot();
|
|
1730
|
-
const { quiet } = getGlobalOpts(this);
|
|
1731
|
-
const lessons = await loadSessionLessons(repoRoot);
|
|
1732
|
-
if (options.json) {
|
|
1733
|
-
console.log(JSON.stringify({ lessons, count: lessons.length }));
|
|
1734
|
-
return;
|
|
1735
|
-
}
|
|
1736
|
-
if (lessons.length === 0) {
|
|
1737
|
-
console.log("No high-severity lessons found.");
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
outputSessionLessonsHuman(lessons, quiet);
|
|
1741
|
-
});
|
|
1742
|
-
program.command("check-plan").description("Check plan against relevant lessons").option("--plan <text>", "Plan text to check").option("--json", "Output as JSON").option("-n, --limit <number>", "Maximum results", DEFAULT_CHECK_PLAN_LIMIT).action(async function(options) {
|
|
1743
|
-
const repoRoot = getRepoRoot();
|
|
1744
|
-
const limit = parseLimit(options.limit, "limit");
|
|
1745
|
-
const { quiet } = getGlobalOpts(this);
|
|
1746
|
-
const planText = options.plan ?? await readPlanFromStdin();
|
|
1747
|
-
if (!planText) {
|
|
1748
|
-
out.error("No plan provided. Use --plan <text> or pipe text to stdin.");
|
|
1749
|
-
process.exit(1);
|
|
1750
|
-
}
|
|
1751
|
-
if (!isModelAvailable()) {
|
|
1752
|
-
if (options.json) {
|
|
1753
|
-
console.log(JSON.stringify({
|
|
1754
|
-
error: "Embedding model not available",
|
|
1755
|
-
action: "Run: npx learning-agent download-model"
|
|
1756
|
-
}));
|
|
1757
|
-
} else {
|
|
1758
|
-
out.error("Embedding model not available");
|
|
1759
|
-
console.log("");
|
|
1760
|
-
console.log("Run: npx learning-agent download-model");
|
|
1761
|
-
}
|
|
1762
|
-
process.exit(1);
|
|
1763
|
-
}
|
|
1764
|
-
try {
|
|
1765
|
-
const result = await retrieveForPlan(repoRoot, planText, limit);
|
|
1766
|
-
if (options.json) {
|
|
1767
|
-
outputCheckPlanJson(result.lessons);
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
if (result.lessons.length === 0) {
|
|
1771
|
-
console.log("No relevant lessons found for this plan.");
|
|
1772
|
-
return;
|
|
1773
|
-
}
|
|
1774
|
-
outputCheckPlanHuman(result.lessons, quiet);
|
|
1775
|
-
} catch (err) {
|
|
1776
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1777
|
-
if (options.json) {
|
|
1778
|
-
console.log(JSON.stringify({ error: message }));
|
|
1779
|
-
} else {
|
|
1780
|
-
out.error(`Failed to check plan: ${message}`);
|
|
1781
|
-
}
|
|
1782
|
-
process.exit(1);
|
|
1783
|
-
}
|
|
1784
|
-
});
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// src/cli.ts
|
|
2174
|
+
var program = new Command();
|
|
2175
|
+
program.name("learning-agent").description("Learning system for Claude Code session memory").version(VERSION).option("-v, --verbose", "Verbose output").option("-q, --quiet", "Minimal output");
|
|
2176
|
+
registerInitCommand(program);
|
|
2177
|
+
registerLearnCommand(program);
|
|
2178
|
+
registerCaptureCommand(program);
|
|
2179
|
+
registerDetectCommand(program);
|
|
2180
|
+
registerListCommand(program);
|
|
2181
|
+
registerSearchCommand(program);
|
|
2182
|
+
registerShowCommand(program);
|
|
2183
|
+
registerUpdateCommand(program);
|
|
2184
|
+
registerDeleteCommand(program);
|
|
2185
|
+
registerLoadSessionCommand(program);
|
|
2186
|
+
registerCheckPlanCommand(program);
|
|
2187
|
+
registerStatsCommand(program);
|
|
2188
|
+
registerRebuildCommand(program);
|
|
2189
|
+
registerCompactCommand(program);
|
|
2190
|
+
registerExportCommand(program);
|
|
2191
|
+
registerImportCommand(program);
|
|
2192
|
+
registerDownloadModelCommand(program);
|
|
2193
|
+
registerSetupCommand(program);
|
|
2194
|
+
registerHooksCommand(program);
|
|
1785
2195
|
program.parse();
|
|
1786
2196
|
//# sourceMappingURL=cli.js.map
|
|
1787
2197
|
//# sourceMappingURL=cli.js.map
|