learning-agent 0.1.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 +56 -4
- package/README.md +130 -14
- package/dist/cli.js +1670 -518
- package/dist/cli.js.map +1 -1
- package/package.json +12 -20
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
|
|
5
|
-
import {
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { statSync, existsSync, chmodSync, mkdirSync } from 'fs';
|
|
6
5
|
import * as fs from 'fs/promises';
|
|
7
|
-
import {
|
|
6
|
+
import { readFile, mkdir, writeFile, rename, appendFile } from 'fs/promises';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
8
9
|
import { createHash } from 'crypto';
|
|
9
|
-
import Database from 'better-sqlite3';
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
-
import '
|
|
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.
|
|
13
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
|
|
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
|
|
@@ -216,6 +331,28 @@ function openDb(repoRoot) {
|
|
|
216
331
|
createSchema(db);
|
|
217
332
|
return db;
|
|
218
333
|
}
|
|
334
|
+
function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
|
|
335
|
+
const database = openDb(repoRoot);
|
|
336
|
+
const row = database.prepare("SELECT embedding, content_hash FROM lessons WHERE id = ?").get(lessonId);
|
|
337
|
+
if (!row || !row.embedding || !row.content_hash) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
if (expectedHash && row.content_hash !== expectedHash) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const float32 = new Float32Array(
|
|
344
|
+
row.embedding.buffer,
|
|
345
|
+
row.embedding.byteOffset,
|
|
346
|
+
row.embedding.byteLength / 4
|
|
347
|
+
);
|
|
348
|
+
return Array.from(float32);
|
|
349
|
+
}
|
|
350
|
+
function setCachedEmbedding(repoRoot, lessonId, embedding, hash) {
|
|
351
|
+
const database = openDb(repoRoot);
|
|
352
|
+
const float32 = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
|
|
353
|
+
const buffer = Buffer.from(float32.buffer, float32.byteOffset, float32.byteLength);
|
|
354
|
+
database.prepare("UPDATE lessons SET embedding = ?, content_hash = ? WHERE id = ?").run(buffer, hash, lessonId);
|
|
355
|
+
}
|
|
219
356
|
function rowToLesson(row) {
|
|
220
357
|
const lesson = {
|
|
221
358
|
id: row.id,
|
|
@@ -380,117 +517,559 @@ function getRetrievalStats(repoRoot) {
|
|
|
380
517
|
lastRetrieved: row.last_retrieved
|
|
381
518
|
}));
|
|
382
519
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
var
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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 [];
|
|
397
538
|
}
|
|
398
|
-
const
|
|
399
|
-
for (const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
reason: `Found similar existing lesson: "${lesson.insight.slice(0, 50)}..."`,
|
|
408
|
-
existingId: lesson.id
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
|
|
412
|
-
return {
|
|
413
|
-
novel: false,
|
|
414
|
-
reason: `Exact duplicate found`,
|
|
415
|
-
existingId: lesson.id
|
|
416
|
-
};
|
|
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 });
|
|
417
548
|
}
|
|
418
549
|
}
|
|
419
|
-
return
|
|
550
|
+
return results;
|
|
420
551
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
/\btry to\b/i,
|
|
428
|
-
/\bdouble check\b/i
|
|
429
|
-
];
|
|
430
|
-
var GENERIC_IMPERATIVE_PATTERN = /^(always|never)\s+\w+(\s+\w+){0,2}$/i;
|
|
431
|
-
function isSpecific(insight) {
|
|
432
|
-
const words = insight.trim().split(/\s+/).filter((w) => w.length > 0);
|
|
433
|
-
if (words.length < MIN_WORD_COUNT) {
|
|
434
|
-
return { specific: false, reason: "Insight is too short to be actionable" };
|
|
435
|
-
}
|
|
436
|
-
for (const pattern of VAGUE_PATTERNS) {
|
|
437
|
-
if (pattern.test(insight)) {
|
|
438
|
-
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++;
|
|
439
558
|
}
|
|
440
559
|
}
|
|
441
|
-
|
|
442
|
-
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
443
|
-
}
|
|
444
|
-
return { specific: true };
|
|
560
|
+
return count;
|
|
445
561
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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);
|
|
466
593
|
}
|
|
467
594
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
async function shouldPropose(repoRoot, insight) {
|
|
471
|
-
const specificResult = isSpecific(insight);
|
|
472
|
-
if (!specificResult.specific) {
|
|
473
|
-
return { shouldPropose: false, reason: specificResult.reason };
|
|
595
|
+
if (toArchive.length === 0) {
|
|
596
|
+
return 0;
|
|
474
597
|
}
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
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);
|
|
478
605
|
}
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
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");
|
|
482
611
|
}
|
|
483
|
-
|
|
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;
|
|
484
619
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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);
|
|
627
|
+
return {
|
|
628
|
+
archived,
|
|
629
|
+
tombstonesRemoved,
|
|
630
|
+
lessonsRemaining: lessons.length
|
|
631
|
+
};
|
|
632
|
+
}
|
|
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
|
+
});
|
|
663
|
+
}
|
|
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
|
+
});
|
|
702
|
+
}
|
|
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
|
+
});
|
|
731
|
+
}
|
|
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
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
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
|
+
});
|
|
772
|
+
}
|
|
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
|
+
});
|
|
829
|
+
}
|
|
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
|
+
});
|
|
857
|
+
}
|
|
858
|
+
var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
|
|
859
|
+
var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
|
|
860
|
+
var DEFAULT_MODEL_DIR = join(homedir(), ".node-llama-cpp", "models");
|
|
861
|
+
function isModelAvailable() {
|
|
862
|
+
return existsSync(join(DEFAULT_MODEL_DIR, MODEL_FILENAME));
|
|
863
|
+
}
|
|
864
|
+
async function resolveModel(options = {}) {
|
|
865
|
+
const { cli = true } = options;
|
|
866
|
+
return resolveModelFile(MODEL_URI, { cli });
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/embeddings/nomic.ts
|
|
870
|
+
var embeddingContext = null;
|
|
871
|
+
async function getEmbedding() {
|
|
872
|
+
if (embeddingContext) return embeddingContext;
|
|
873
|
+
const modelPath = await resolveModel({ cli: true });
|
|
874
|
+
const llama = await getLlama();
|
|
875
|
+
const model = await llama.loadModel({ modelPath });
|
|
876
|
+
embeddingContext = await model.createEmbeddingContext();
|
|
877
|
+
return embeddingContext;
|
|
878
|
+
}
|
|
879
|
+
async function embedText(text) {
|
|
880
|
+
const ctx = await getEmbedding();
|
|
881
|
+
const result = await ctx.getEmbeddingFor(text);
|
|
882
|
+
return Array.from(result.vector);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/search/vector.ts
|
|
886
|
+
function cosineSimilarity(a, b) {
|
|
887
|
+
if (a.length !== b.length) {
|
|
888
|
+
throw new Error("Vectors must have same length");
|
|
889
|
+
}
|
|
890
|
+
let dotProduct = 0;
|
|
891
|
+
let normA = 0;
|
|
892
|
+
let normB = 0;
|
|
893
|
+
for (let i = 0; i < a.length; i++) {
|
|
894
|
+
dotProduct += a[i] * b[i];
|
|
895
|
+
normA += a[i] * a[i];
|
|
896
|
+
normB += b[i] * b[i];
|
|
897
|
+
}
|
|
898
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
899
|
+
if (magnitude === 0) return 0;
|
|
900
|
+
return dotProduct / magnitude;
|
|
901
|
+
}
|
|
902
|
+
var DEFAULT_LIMIT = 10;
|
|
903
|
+
async function searchVector(repoRoot, query, options) {
|
|
904
|
+
const limit = options?.limit ?? DEFAULT_LIMIT;
|
|
905
|
+
const { lessons } = await readLessons(repoRoot);
|
|
906
|
+
if (lessons.length === 0) return [];
|
|
907
|
+
const queryVector = await embedText(query);
|
|
908
|
+
const scored = [];
|
|
909
|
+
for (const lesson of lessons) {
|
|
910
|
+
const lessonText = `${lesson.trigger} ${lesson.insight}`;
|
|
911
|
+
const hash = contentHash(lesson.trigger, lesson.insight);
|
|
912
|
+
let lessonVector = getCachedEmbedding(repoRoot, lesson.id, hash);
|
|
913
|
+
if (!lessonVector) {
|
|
914
|
+
lessonVector = await embedText(lessonText);
|
|
915
|
+
setCachedEmbedding(repoRoot, lesson.id, lessonVector, hash);
|
|
916
|
+
}
|
|
917
|
+
const score = cosineSimilarity(queryVector, lessonVector);
|
|
918
|
+
scored.push({ lesson, score });
|
|
919
|
+
}
|
|
920
|
+
scored.sort((a, b) => b.score - a.score);
|
|
921
|
+
return scored.slice(0, limit);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/search/ranking.ts
|
|
925
|
+
var RECENCY_THRESHOLD_DAYS = 30;
|
|
926
|
+
var HIGH_SEVERITY_BOOST = 1.5;
|
|
927
|
+
var MEDIUM_SEVERITY_BOOST = 1;
|
|
928
|
+
var LOW_SEVERITY_BOOST = 0.8;
|
|
929
|
+
var RECENCY_BOOST = 1.2;
|
|
930
|
+
var CONFIRMATION_BOOST = 1.3;
|
|
931
|
+
function severityBoost(lesson) {
|
|
932
|
+
switch (lesson.severity) {
|
|
933
|
+
case "high":
|
|
934
|
+
return HIGH_SEVERITY_BOOST;
|
|
935
|
+
case "medium":
|
|
936
|
+
return MEDIUM_SEVERITY_BOOST;
|
|
937
|
+
case "low":
|
|
938
|
+
return LOW_SEVERITY_BOOST;
|
|
939
|
+
default:
|
|
940
|
+
return MEDIUM_SEVERITY_BOOST;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function recencyBoost(lesson) {
|
|
944
|
+
const created = new Date(lesson.created);
|
|
945
|
+
const now = /* @__PURE__ */ new Date();
|
|
946
|
+
const ageMs = now.getTime() - created.getTime();
|
|
947
|
+
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
948
|
+
return ageDays <= RECENCY_THRESHOLD_DAYS ? RECENCY_BOOST : 1;
|
|
949
|
+
}
|
|
950
|
+
function confirmationBoost(lesson) {
|
|
951
|
+
return lesson.confirmed ? CONFIRMATION_BOOST : 1;
|
|
952
|
+
}
|
|
953
|
+
function calculateScore(lesson, vectorSimilarity) {
|
|
954
|
+
return vectorSimilarity * severityBoost(lesson) * recencyBoost(lesson) * confirmationBoost(lesson);
|
|
955
|
+
}
|
|
956
|
+
function rankLessons(lessons) {
|
|
957
|
+
return lessons.map((scored) => ({
|
|
958
|
+
...scored,
|
|
959
|
+
finalScore: calculateScore(scored.lesson, scored.score)
|
|
960
|
+
})).sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
|
|
961
|
+
}
|
|
962
|
+
|
|
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..."
|
|
494
1073
|
/\bnot that\b/i,
|
|
495
1074
|
// "not that"
|
|
496
1075
|
/\bi meant\b/i
|
|
@@ -548,39 +1127,364 @@ function detectTestFailure(testResult) {
|
|
|
548
1127
|
};
|
|
549
1128
|
}
|
|
550
1129
|
|
|
551
|
-
// src/
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
const { trigger, source, proposedInsight } = detected;
|
|
558
|
-
const quality = await shouldPropose(repoRoot, proposedInsight);
|
|
559
|
-
if (!quality.shouldPropose) {
|
|
560
|
-
return null;
|
|
561
|
-
}
|
|
562
|
-
return { trigger, source, proposedInsight };
|
|
1130
|
+
// src/retrieval/session.ts
|
|
1131
|
+
var DEFAULT_LIMIT2 = 5;
|
|
1132
|
+
function isFullLesson(lesson) {
|
|
1133
|
+
return lesson.type === "full" && lesson.severity !== void 0;
|
|
563
1134
|
}
|
|
564
|
-
function
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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();
|
|
1142
|
+
const dateB = new Date(b.created).getTime();
|
|
1143
|
+
return dateB - dateA;
|
|
1144
|
+
});
|
|
1145
|
+
return highSeverityLessons.slice(0, limit);
|
|
573
1146
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1147
|
+
|
|
1148
|
+
// src/retrieval/plan.ts
|
|
1149
|
+
var DEFAULT_LIMIT3 = 5;
|
|
1150
|
+
async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
|
|
1151
|
+
const scored = await searchVector(repoRoot, planText, { limit: limit * 2 });
|
|
1152
|
+
const ranked = rankLessons(scored);
|
|
1153
|
+
const topLessons = ranked.slice(0, limit);
|
|
1154
|
+
const message = formatLessonsCheck(topLessons);
|
|
1155
|
+
return { lessons: topLessons, message };
|
|
1156
|
+
}
|
|
1157
|
+
function formatLessonsCheck(lessons) {
|
|
1158
|
+
const header = "\u{1F4DA} Lessons Check\n" + "\u2500".repeat(40);
|
|
1159
|
+
if (lessons.length === 0) {
|
|
1160
|
+
return `${header}
|
|
1161
|
+
No relevant lessons found for this plan.`;
|
|
578
1162
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
};
|
|
1163
|
+
const lessonLines = lessons.map((l, i) => {
|
|
1164
|
+
const bullet = `${i + 1}.`;
|
|
1165
|
+
const insight = l.lesson.insight;
|
|
1166
|
+
return `${bullet} ${insight}`;
|
|
1167
|
+
});
|
|
1168
|
+
return `${header}
|
|
1169
|
+
${lessonLines.join("\n")}`;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/index.ts
|
|
1173
|
+
var VERSION = "0.1.0";
|
|
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
|
+
});
|
|
1205
|
+
}
|
|
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}`);
|
|
1215
|
+
}
|
|
1216
|
+
if (lesson.severity) {
|
|
1217
|
+
lines.push(`Severity: ${lesson.severity}`);
|
|
1218
|
+
}
|
|
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");
|
|
1233
|
+
}
|
|
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);
|
|
1265
|
+
}
|
|
1266
|
+
if (options.json) {
|
|
1267
|
+
console.log(JSON.stringify(lesson, null, SHOW_JSON_INDENT));
|
|
1268
|
+
} else {
|
|
1269
|
+
console.log(formatLessonHuman(lesson));
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
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
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
async function wasLessonDeleted(repoRoot, id) {
|
|
1358
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
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;
|
|
1376
|
+
}
|
|
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 }));
|
|
1404
|
+
} else {
|
|
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;
|
|
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;
|
|
1460
|
+
}
|
|
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);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function detectUserCorrectionFlow(data) {
|
|
1479
|
+
const result = detectUserCorrection(data);
|
|
1480
|
+
if (!result) {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
return {
|
|
1484
|
+
trigger: result.trigger,
|
|
1485
|
+
source: "user_correction",
|
|
1486
|
+
proposedInsight: result.correctionMessage
|
|
1487
|
+
};
|
|
584
1488
|
}
|
|
585
1489
|
function detectSelfCorrectionFlow(data) {
|
|
586
1490
|
const result = detectSelfCorrection(data);
|
|
@@ -615,431 +1519,679 @@ async function parseInputFile(filePath) {
|
|
|
615
1519
|
return data;
|
|
616
1520
|
}
|
|
617
1521
|
|
|
618
|
-
// src/cli
|
|
619
|
-
function
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return results;
|
|
672
|
-
}
|
|
673
|
-
async function countTombstones(repoRoot) {
|
|
674
|
-
const lines = await parseRawJsonlLines(repoRoot);
|
|
675
|
-
let count = 0;
|
|
676
|
-
for (const { parsed } of lines) {
|
|
677
|
-
if (parsed && parsed["deleted"] === true) {
|
|
678
|
-
count++;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
return count;
|
|
682
|
-
}
|
|
683
|
-
async function needsCompaction(repoRoot) {
|
|
684
|
-
const count = await countTombstones(repoRoot);
|
|
685
|
-
return count >= TOMBSTONE_THRESHOLD;
|
|
686
|
-
}
|
|
687
|
-
async function rewriteWithoutTombstones(repoRoot) {
|
|
688
|
-
const filePath = join(repoRoot, LESSONS_PATH);
|
|
689
|
-
const tempPath = filePath + ".tmp";
|
|
690
|
-
const { lessons } = await readLessons(repoRoot);
|
|
691
|
-
const tombstoneCount = await countTombstones(repoRoot);
|
|
692
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
693
|
-
const lines = lessons.map((lesson) => JSON.stringify(lesson) + "\n");
|
|
694
|
-
await writeFile(tempPath, lines.join(""), "utf-8");
|
|
695
|
-
await rename(tempPath, filePath);
|
|
696
|
-
return tombstoneCount;
|
|
697
|
-
}
|
|
698
|
-
function shouldArchive(lesson, now) {
|
|
699
|
-
const created = new Date(lesson.created);
|
|
700
|
-
const ageMs = now.getTime() - created.getTime();
|
|
701
|
-
const ageDays = ageMs / MS_PER_DAY;
|
|
702
|
-
return ageDays > ARCHIVE_AGE_DAYS && (lesson.retrievalCount === void 0 || lesson.retrievalCount === 0);
|
|
703
|
-
}
|
|
704
|
-
async function archiveOldLessons(repoRoot) {
|
|
705
|
-
const { lessons } = await readLessons(repoRoot);
|
|
706
|
-
const now = /* @__PURE__ */ new Date();
|
|
707
|
-
const toArchive = [];
|
|
708
|
-
const toKeep = [];
|
|
709
|
-
for (const lesson of lessons) {
|
|
710
|
-
if (shouldArchive(lesson, now)) {
|
|
711
|
-
toArchive.push(lesson);
|
|
712
|
-
} else {
|
|
713
|
-
toKeep.push(lesson);
|
|
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
|
+
}
|
|
714
1573
|
}
|
|
715
|
-
|
|
716
|
-
if (toArchive.length === 0) {
|
|
717
|
-
return 0;
|
|
718
|
-
}
|
|
719
|
-
const archiveGroups = /* @__PURE__ */ new Map();
|
|
720
|
-
for (const lesson of toArchive) {
|
|
721
|
-
const created = new Date(lesson.created);
|
|
722
|
-
const archivePath = getArchivePath(repoRoot, created);
|
|
723
|
-
const group = archiveGroups.get(archivePath) ?? [];
|
|
724
|
-
group.push(lesson);
|
|
725
|
-
archiveGroups.set(archivePath, group);
|
|
726
|
-
}
|
|
727
|
-
const archiveDir = join(repoRoot, ARCHIVE_DIR);
|
|
728
|
-
await mkdir(archiveDir, { recursive: true });
|
|
729
|
-
for (const [archivePath, archiveLessons] of archiveGroups) {
|
|
730
|
-
const lines2 = archiveLessons.map((l) => JSON.stringify(l) + "\n").join("");
|
|
731
|
-
await appendFile(archivePath, lines2, "utf-8");
|
|
732
|
-
}
|
|
733
|
-
const filePath = join(repoRoot, LESSONS_PATH);
|
|
734
|
-
const tempPath = filePath + ".tmp";
|
|
735
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
736
|
-
const lines = toKeep.map((lesson) => JSON.stringify(lesson) + "\n");
|
|
737
|
-
await writeFile(tempPath, lines.join(""), "utf-8");
|
|
738
|
-
await rename(tempPath, filePath);
|
|
739
|
-
return toArchive.length;
|
|
740
|
-
}
|
|
741
|
-
async function compact(repoRoot) {
|
|
742
|
-
const tombstonesBefore = await countTombstones(repoRoot);
|
|
743
|
-
const archived = await archiveOldLessons(repoRoot);
|
|
744
|
-
const tombstonesAfterArchive = await countTombstones(repoRoot);
|
|
745
|
-
await rewriteWithoutTombstones(repoRoot);
|
|
746
|
-
const tombstonesRemoved = archived > 0 ? tombstonesBefore : tombstonesAfterArchive;
|
|
747
|
-
const { lessons } = await readLessons(repoRoot);
|
|
748
|
-
return {
|
|
749
|
-
archived,
|
|
750
|
-
tombstonesRemoved,
|
|
751
|
-
lessonsRemaining: lessons.length
|
|
752
|
-
};
|
|
1574
|
+
);
|
|
753
1575
|
}
|
|
754
1576
|
|
|
755
|
-
// src/cli.ts
|
|
756
|
-
|
|
757
|
-
success: (msg) => console.log(chalk.green("[ok]"), msg),
|
|
758
|
-
error: (msg) => console.error(chalk.red("[error]"), msg),
|
|
759
|
-
info: (msg) => console.log(chalk.blue("[info]"), msg),
|
|
760
|
-
warn: (msg) => console.log(chalk.yellow("[warn]"), msg)
|
|
761
|
-
};
|
|
762
|
-
function getGlobalOpts(cmd) {
|
|
763
|
-
const opts = cmd.optsWithGlobals();
|
|
1577
|
+
// src/cli/commands/capture.ts
|
|
1578
|
+
function createLessonFromFlags(trigger, insight, confirmed) {
|
|
764
1579
|
return {
|
|
765
|
-
verbose: opts.verbose ?? false,
|
|
766
|
-
quiet: opts.quiet ?? false
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
var DEFAULT_SEARCH_LIMIT = "10";
|
|
770
|
-
var DEFAULT_LIST_LIMIT = "20";
|
|
771
|
-
var program = new Command();
|
|
772
|
-
program.option("-v, --verbose", "Show detailed output").option("-q, --quiet", "Suppress non-essential output");
|
|
773
|
-
program.name("learning-agent").description("Repository-scoped learning system for Claude Code").version(VERSION);
|
|
774
|
-
program.command("learn <insight>").description("Capture a new lesson").option("-t, --trigger <text>", "What triggered this lesson").option("--tags <tags>", "Comma-separated tags", "").option("-y, --yes", "Skip confirmation").action(async function(insight, options) {
|
|
775
|
-
const repoRoot = getRepoRoot();
|
|
776
|
-
const { quiet } = getGlobalOpts(this);
|
|
777
|
-
const lesson = {
|
|
778
1580
|
id: generateId(insight),
|
|
779
1581
|
type: "quick",
|
|
780
|
-
trigger
|
|
1582
|
+
trigger,
|
|
781
1583
|
insight,
|
|
782
|
-
tags:
|
|
1584
|
+
tags: [],
|
|
783
1585
|
source: "manual",
|
|
784
|
-
context: {
|
|
785
|
-
tool: "cli",
|
|
786
|
-
intent: "manual learning"
|
|
787
|
-
},
|
|
1586
|
+
context: { tool: "capture", intent: "manual capture" },
|
|
788
1587
|
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
789
|
-
confirmed
|
|
1588
|
+
confirmed,
|
|
790
1589
|
supersedes: [],
|
|
791
1590
|
related: []
|
|
792
1591
|
};
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1592
|
+
}
|
|
1593
|
+
function outputCaptureJson(lesson, saved) {
|
|
1594
|
+
console.log(JSON.stringify({
|
|
1595
|
+
id: lesson.id,
|
|
1596
|
+
trigger: lesson.trigger,
|
|
1597
|
+
insight: lesson.insight,
|
|
1598
|
+
type: lesson.type,
|
|
1599
|
+
saved
|
|
1600
|
+
}));
|
|
1601
|
+
}
|
|
1602
|
+
function outputCapturePreview(lesson) {
|
|
1603
|
+
console.log("Lesson captured:");
|
|
1604
|
+
console.log(` ID: ${lesson.id}`);
|
|
1605
|
+
console.log(` Trigger: ${lesson.trigger}`);
|
|
1606
|
+
console.log(` Insight: ${lesson.insight}`);
|
|
1607
|
+
console.log(` Type: ${lesson.type}`);
|
|
1608
|
+
console.log(` Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
|
|
1609
|
+
console.log("\nSave this lesson? [y/n]");
|
|
1610
|
+
}
|
|
1611
|
+
function createLessonFromInputFile(result, confirmed) {
|
|
1612
|
+
return {
|
|
1613
|
+
id: generateId(result.proposedInsight),
|
|
1614
|
+
type: "quick",
|
|
1615
|
+
trigger: result.trigger,
|
|
1616
|
+
insight: result.proposedInsight,
|
|
1617
|
+
tags: [],
|
|
1618
|
+
source: result.source,
|
|
1619
|
+
context: { tool: "capture", intent: "auto-capture" },
|
|
1620
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1621
|
+
confirmed,
|
|
1622
|
+
supersedes: [],
|
|
1623
|
+
related: []
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
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);
|
|
819
1645
|
}
|
|
820
|
-
if (
|
|
821
|
-
|
|
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);
|
|
822
1654
|
}
|
|
823
|
-
|
|
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);
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
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
|
+
`;
|
|
1673
|
+
var AGENTS_MD_TEMPLATE = `
|
|
1674
|
+
## Learning Agent Integration
|
|
1675
|
+
|
|
1676
|
+
This project uses learning-agent for session memory.
|
|
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
|
+
|
|
1696
|
+
### Retrieval Points
|
|
1697
|
+
|
|
1698
|
+
- **Session start**: High-severity lessons loaded automatically
|
|
1699
|
+
- **Plan-time**: BEFORE implementing a plan, run check-plan to retrieve relevant lessons
|
|
1700
|
+
|
|
1701
|
+
### Plan-Time Retrieval (Explicit Step)
|
|
1702
|
+
|
|
1703
|
+
**BEFORE implementing any plan**, run:
|
|
1704
|
+
|
|
1705
|
+
\`\`\`bash
|
|
1706
|
+
npx lna check-plan --plan "your plan description" --json
|
|
1707
|
+
\`\`\`
|
|
1708
|
+
|
|
1709
|
+
Display results as a **Lessons Check** section after your plan:
|
|
1710
|
+
|
|
1711
|
+
\`\`\`
|
|
1712
|
+
## Lessons Check
|
|
1713
|
+
1. [insight from lesson 1] (relevance: 0.85)
|
|
1714
|
+
2. [insight from lesson 2] (relevance: 0.72)
|
|
1715
|
+
\`\`\`
|
|
1716
|
+
|
|
1717
|
+
Consider each lesson while implementing.
|
|
1718
|
+
|
|
1719
|
+
### Proposing Lessons
|
|
1720
|
+
|
|
1721
|
+
Propose when: user correction, self-correction, test failure fix, or manual request.
|
|
1722
|
+
|
|
1723
|
+
**Quality gate (ALL must pass):**
|
|
1724
|
+
|
|
1725
|
+
- Novel (not already stored)
|
|
1726
|
+
- Specific (clear guidance)
|
|
1727
|
+
- Actionable (obvious what to do)
|
|
1728
|
+
|
|
1729
|
+
**Confirmation format:**
|
|
1730
|
+
|
|
1731
|
+
\`\`\`
|
|
1732
|
+
Learned: [insight]. Save? [y/n]
|
|
1733
|
+
\`\`\`
|
|
1734
|
+
|
|
1735
|
+
### Session-End Protocol
|
|
1736
|
+
|
|
1737
|
+
Before closing a session, reflect on lessons learned:
|
|
1738
|
+
|
|
1739
|
+
1. **Review**: What mistakes or corrections happened?
|
|
1740
|
+
2. **Quality gate**: Is it novel, specific, actionable?
|
|
1741
|
+
3. **Propose**: "Learned: [insight]. Save? [y/n]"
|
|
1742
|
+
4. **Capture**: \`npx lna capture --trigger "..." --insight "..." --yes\`
|
|
1743
|
+
|
|
1744
|
+
### CLI Commands
|
|
1745
|
+
|
|
1746
|
+
\`\`\`bash
|
|
1747
|
+
npx lna load-session --json # Session start
|
|
1748
|
+
npx lna check-plan --plan "..." --json # Before implementing
|
|
1749
|
+
npx lna capture --trigger "..." --insight "..." --yes
|
|
1750
|
+
\`\`\`
|
|
1751
|
+
|
|
1752
|
+
See [AGENTS.md](https://github.com/Nathandela/learning_agent/blob/main/AGENTS.md) for full documentation.
|
|
1753
|
+
`;
|
|
1754
|
+
function hasLearningAgentSection(content) {
|
|
1755
|
+
return content.includes(LEARNING_AGENT_SECTION_HEADER);
|
|
1756
|
+
}
|
|
1757
|
+
async function createLessonsDirectory(repoRoot) {
|
|
1758
|
+
const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
|
|
1759
|
+
await mkdir(lessonsDir, { recursive: true });
|
|
1760
|
+
}
|
|
1761
|
+
async function createIndexFile(repoRoot) {
|
|
1762
|
+
const indexPath = join(repoRoot, LESSONS_PATH);
|
|
1763
|
+
if (!existsSync(indexPath)) {
|
|
1764
|
+
await writeFile(indexPath, "", "utf-8");
|
|
824
1765
|
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
if (
|
|
834
|
-
|
|
1766
|
+
}
|
|
1767
|
+
async function updateAgentsMd(repoRoot) {
|
|
1768
|
+
const agentsPath = join(repoRoot, "AGENTS.md");
|
|
1769
|
+
let content = "";
|
|
1770
|
+
let existed = false;
|
|
1771
|
+
if (existsSync(agentsPath)) {
|
|
1772
|
+
content = await readFile(agentsPath, "utf-8");
|
|
1773
|
+
existed = true;
|
|
1774
|
+
if (hasLearningAgentSection(content)) {
|
|
1775
|
+
return false;
|
|
835
1776
|
}
|
|
836
|
-
return;
|
|
837
1777
|
}
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1778
|
+
const newContent = existed ? content.trimEnd() + "\n" + AGENTS_MD_TEMPLATE : AGENTS_MD_TEMPLATE.trim() + "\n";
|
|
1779
|
+
await writeFile(agentsPath, newContent, "utf-8");
|
|
1780
|
+
return true;
|
|
1781
|
+
}
|
|
1782
|
+
function hasLearningAgentHook(content) {
|
|
1783
|
+
return content.includes(HOOK_MARKER);
|
|
1784
|
+
}
|
|
1785
|
+
async function getGitHooksDir(repoRoot) {
|
|
1786
|
+
const gitDir = join(repoRoot, ".git");
|
|
1787
|
+
if (!existsSync(gitDir)) {
|
|
1788
|
+
return null;
|
|
842
1789
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1790
|
+
const configPath = join(gitDir, "config");
|
|
1791
|
+
if (existsSync(configPath)) {
|
|
1792
|
+
const config = await readFile(configPath, "utf-8");
|
|
1793
|
+
const match = /hooksPath\s*=\s*(.+)$/m.exec(config);
|
|
1794
|
+
if (match?.[1]) {
|
|
1795
|
+
const hooksPath = match[1].trim();
|
|
1796
|
+
return hooksPath.startsWith("/") ? hooksPath : join(repoRoot, hooksPath);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
const defaultHooksDir = join(gitDir, "hooks");
|
|
1800
|
+
return existsSync(defaultHooksDir) ? defaultHooksDir : null;
|
|
1801
|
+
}
|
|
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;
|
|
850
1811
|
}
|
|
851
|
-
|
|
852
|
-
console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
|
|
1812
|
+
continue;
|
|
853
1813
|
}
|
|
854
|
-
|
|
855
|
-
|
|
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;
|
|
856
1828
|
}
|
|
857
|
-
console.log();
|
|
858
1829
|
}
|
|
859
|
-
|
|
860
|
-
|
|
1830
|
+
return -1;
|
|
1831
|
+
}
|
|
1832
|
+
async function installPreCommitHook(repoRoot) {
|
|
1833
|
+
const gitHooksDir = await getGitHooksDir(repoRoot);
|
|
1834
|
+
if (!gitHooksDir) {
|
|
1835
|
+
return false;
|
|
861
1836
|
}
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
1837
|
+
await mkdir(gitHooksDir, { recursive: true });
|
|
1838
|
+
const hookPath = join(gitHooksDir, "pre-commit");
|
|
1839
|
+
if (existsSync(hookPath)) {
|
|
1840
|
+
const content = await readFile(hookPath, "utf-8");
|
|
1841
|
+
if (hasLearningAgentHook(content)) {
|
|
1842
|
+
return false;
|
|
1843
|
+
}
|
|
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;
|
|
873
1849
|
} else {
|
|
874
|
-
|
|
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");
|
|
875
1853
|
}
|
|
1854
|
+
await writeFile(hookPath, newContent, "utf-8");
|
|
1855
|
+
chmodSync(hookPath, HOOK_FILE_MODE);
|
|
1856
|
+
return true;
|
|
876
1857
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1858
|
+
await writeFile(hookPath, PRE_COMMIT_HOOK_TEMPLATE, "utf-8");
|
|
1859
|
+
chmodSync(hookPath, HOOK_FILE_MODE);
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
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) {
|
|
880
1864
|
const repoRoot = getRepoRoot();
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
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);
|
|
1872
|
+
}
|
|
1873
|
+
let hooksInstalled = false;
|
|
1874
|
+
if (!options.skipHooks) {
|
|
1875
|
+
hooksInstalled = await installPreCommitHook(repoRoot);
|
|
1876
|
+
}
|
|
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
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
if (options.json) {
|
|
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
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
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
|
+
}
|
|
1940
|
+
} else {
|
|
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);
|
|
1947
|
+
}
|
|
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 {
|
|
884
1959
|
if (options.json) {
|
|
885
|
-
console.log(JSON.stringify({
|
|
1960
|
+
console.log(JSON.stringify({ error: "Failed to parse settings file" }));
|
|
1961
|
+
} else {
|
|
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 }));
|
|
1971
|
+
} else {
|
|
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
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
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
|
+
}
|
|
886
1989
|
} else {
|
|
887
|
-
|
|
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
|
+
}
|
|
888
2000
|
}
|
|
889
2001
|
return;
|
|
890
2002
|
}
|
|
891
|
-
if (options.
|
|
892
|
-
|
|
2003
|
+
if (options.dryRun) {
|
|
2004
|
+
if (options.json) {
|
|
2005
|
+
console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
|
|
2006
|
+
} else {
|
|
2007
|
+
if (alreadyInstalled) {
|
|
2008
|
+
console.log("Learning agent hooks already installed");
|
|
2009
|
+
} else {
|
|
2010
|
+
console.log(`Would install learning-agent hooks to ${displayPath}`);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
893
2013
|
return;
|
|
894
2014
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
909
|
-
confirmed: false,
|
|
910
|
-
supersedes: [],
|
|
911
|
-
related: []
|
|
912
|
-
};
|
|
913
|
-
await appendLesson(repoRoot, lesson);
|
|
914
|
-
console.log(`
|
|
915
|
-
Saved as lesson: ${lesson.id}`);
|
|
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
|
+
}));
|
|
2023
|
+
} else {
|
|
2024
|
+
out.info("Learning agent hooks already installed");
|
|
2025
|
+
console.log(` Location: ${displayPath}`);
|
|
2026
|
+
}
|
|
2027
|
+
return;
|
|
916
2028
|
}
|
|
917
|
-
|
|
918
|
-
);
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
console.log(` Archived: ${result.archived} lesson(s)`);
|
|
938
|
-
console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
|
|
939
|
-
console.log(` Lessons remaining: ${result.lessonsRemaining}`);
|
|
940
|
-
await rebuildIndex(repoRoot);
|
|
941
|
-
console.log(" Index rebuilt.");
|
|
942
|
-
});
|
|
943
|
-
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) => {
|
|
944
|
-
const repoRoot = getRepoRoot();
|
|
945
|
-
const { lessons } = await readLessons(repoRoot);
|
|
946
|
-
let filtered = lessons;
|
|
947
|
-
if (options.since) {
|
|
948
|
-
const sinceDate = new Date(options.since);
|
|
949
|
-
if (Number.isNaN(sinceDate.getTime())) {
|
|
950
|
-
console.error(`Invalid date format: ${options.since}. Use ISO8601 format (e.g., 2024-01-15).`);
|
|
951
|
-
process.exit(1);
|
|
2029
|
+
const fileExists = existsSync(settingsPath);
|
|
2030
|
+
addLearningAgentHook(settings);
|
|
2031
|
+
await writeClaudeSettings(settingsPath, settings);
|
|
2032
|
+
if (options.json) {
|
|
2033
|
+
console.log(JSON.stringify({
|
|
2034
|
+
installed: true,
|
|
2035
|
+
location: displayPath,
|
|
2036
|
+
hooks: ["SessionStart"],
|
|
2037
|
+
action: fileExists ? "updated" : "created"
|
|
2038
|
+
}));
|
|
2039
|
+
} else {
|
|
2040
|
+
out.success(options.global ? "Claude Code hooks installed (global)" : "Claude Code hooks installed (project-level)");
|
|
2041
|
+
console.log(` Location: ${displayPath}`);
|
|
2042
|
+
console.log(" Hook: SessionStart (startup|resume|compact)");
|
|
2043
|
+
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
|
+
}
|
|
952
2049
|
}
|
|
953
|
-
|
|
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");
|
|
2059
|
+
if (!quiet) {
|
|
2060
|
+
console.log("These lessons were captured from previous corrections and should inform your work:\n");
|
|
954
2061
|
}
|
|
955
|
-
|
|
956
|
-
const
|
|
957
|
-
|
|
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();
|
|
958
2068
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
program.command("import <file>").description("Import lessons from a JSONL file").action(async (file) => {
|
|
962
|
-
const repoRoot = getRepoRoot();
|
|
963
|
-
let content;
|
|
964
|
-
try {
|
|
965
|
-
const { readFile: readFile4 } = await import('fs/promises');
|
|
966
|
-
content = await readFile4(file, "utf-8");
|
|
967
|
-
} catch (err) {
|
|
968
|
-
const code = err.code;
|
|
969
|
-
if (code === "ENOENT") {
|
|
970
|
-
console.error(`Error: File not found: ${file}`);
|
|
971
|
-
} else {
|
|
972
|
-
console.error(`Error reading file: ${err.message}`);
|
|
973
|
-
}
|
|
974
|
-
process.exit(1);
|
|
2069
|
+
if (!quiet) {
|
|
2070
|
+
console.log("Consider these lessons when planning and implementing tasks.");
|
|
975
2071
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
let parsed;
|
|
986
|
-
try {
|
|
987
|
-
parsed = JSON.parse(trimmed);
|
|
988
|
-
} catch {
|
|
989
|
-
invalid++;
|
|
990
|
-
continue;
|
|
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;
|
|
991
2081
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
continue;
|
|
2082
|
+
if (lessons.length === 0) {
|
|
2083
|
+
console.log("No high-severity lessons found.");
|
|
2084
|
+
return;
|
|
996
2085
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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);
|
|
1001
2095
|
}
|
|
1002
|
-
|
|
1003
|
-
existingIds.add(lesson.id);
|
|
1004
|
-
imported++;
|
|
1005
|
-
}
|
|
1006
|
-
const lessonWord = imported === 1 ? "lesson" : "lessons";
|
|
1007
|
-
const parts = [];
|
|
1008
|
-
if (skipped > 0) parts.push(`${skipped} skipped`);
|
|
1009
|
-
if (invalid > 0) parts.push(`${invalid} invalid`);
|
|
1010
|
-
if (parts.length > 0) {
|
|
1011
|
-
console.log(`Imported ${imported} ${lessonWord} (${parts.join(", ")})`);
|
|
1012
|
-
} else {
|
|
1013
|
-
console.log(`Imported ${imported} ${lessonWord}`);
|
|
1014
|
-
}
|
|
1015
|
-
});
|
|
1016
|
-
program.command("stats").description("Show database health and statistics").action(async () => {
|
|
1017
|
-
const repoRoot = getRepoRoot();
|
|
1018
|
-
await syncIfNeeded(repoRoot);
|
|
1019
|
-
const { lessons } = await readLessons(repoRoot);
|
|
1020
|
-
const deletedCount = await countTombstones(repoRoot);
|
|
1021
|
-
const totalLessons = lessons.length;
|
|
1022
|
-
const retrievalStats = getRetrievalStats(repoRoot);
|
|
1023
|
-
const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
|
|
1024
|
-
const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(1) : "0.0";
|
|
1025
|
-
const jsonlPath = join(repoRoot, LESSONS_PATH);
|
|
1026
|
-
const dbPath = join(repoRoot, DB_PATH);
|
|
1027
|
-
let dataSize = 0;
|
|
1028
|
-
let indexSize = 0;
|
|
1029
|
-
try {
|
|
1030
|
-
dataSize = statSync(jsonlPath).size;
|
|
1031
|
-
} catch {
|
|
2096
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
1032
2097
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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}`);
|
|
2120
|
+
console.log();
|
|
2121
|
+
});
|
|
2122
|
+
if (!quiet) {
|
|
2123
|
+
console.log("---");
|
|
2124
|
+
console.log("Consider these lessons while implementing.");
|
|
1036
2125
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
});
|
|
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) {
|
|
2129
|
+
const repoRoot = getRepoRoot();
|
|
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()) {
|
|
2138
|
+
if (options.json) {
|
|
2139
|
+
console.log(JSON.stringify({
|
|
2140
|
+
error: "Embedding model not available",
|
|
2141
|
+
action: "Run: npx lna download-model"
|
|
2142
|
+
}));
|
|
2143
|
+
} else {
|
|
2144
|
+
out.error("Embedding model not available");
|
|
2145
|
+
console.log("");
|
|
2146
|
+
console.log("Run: npx lna download-model");
|
|
2147
|
+
}
|
|
2148
|
+
process.exit(1);
|
|
2149
|
+
}
|
|
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";
|
|
2163
|
+
if (options.json) {
|
|
2164
|
+
console.log(JSON.stringify({ error: message }));
|
|
2165
|
+
} else {
|
|
2166
|
+
out.error(`Failed to check plan: ${message}`);
|
|
2167
|
+
}
|
|
2168
|
+
process.exit(1);
|
|
2169
|
+
}
|
|
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);
|
|
1043
2195
|
program.parse();
|
|
1044
2196
|
//# sourceMappingURL=cli.js.map
|
|
1045
2197
|
//# sourceMappingURL=cli.js.map
|