learning-agent 0.2.1 → 0.2.2
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 +47 -1
- package/README.md +26 -101
- package/dist/cli.js +1326 -1236
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +38 -0
- package/dist/index.js +85 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
4
|
import { statSync, existsSync, chmodSync, mkdirSync } from 'fs';
|
|
5
|
-
import * as fs from 'fs/promises';
|
|
6
|
-
import { readFile, mkdir, writeFile, rename, appendFile } from 'fs/promises';
|
|
7
|
-
import { homedir } from 'os';
|
|
8
5
|
import { join, dirname } from 'path';
|
|
9
|
-
import { createHash } from 'crypto';
|
|
10
|
-
import { z } from 'zod';
|
|
11
6
|
import Database from 'better-sqlite3';
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import { readFile, mkdir, appendFile, writeFile, rename } from 'fs/promises';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import chalk from 'chalk';
|
|
12
11
|
import { resolveModelFile, getLlama } from 'node-llama-cpp';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
13
|
|
|
14
14
|
// src/cli-utils.ts
|
|
15
15
|
function formatBytes(bytes) {
|
|
@@ -30,104 +30,6 @@ function parseLimit(value, name) {
|
|
|
30
30
|
function getRepoRoot() {
|
|
31
31
|
return process.env["LEARNING_AGENT_ROOT"] ?? process.cwd();
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
// src/cli/shared.ts
|
|
35
|
-
var out = {
|
|
36
|
-
success: (msg) => console.log(chalk.green("[ok]"), msg),
|
|
37
|
-
error: (msg) => console.error(chalk.red("[error]"), msg),
|
|
38
|
-
info: (msg) => console.log(chalk.blue("[info]"), msg),
|
|
39
|
-
warn: (msg) => console.log(chalk.yellow("[warn]"), msg)
|
|
40
|
-
};
|
|
41
|
-
function getGlobalOpts(cmd) {
|
|
42
|
-
const opts = cmd.optsWithGlobals();
|
|
43
|
-
return {
|
|
44
|
-
verbose: opts.verbose ?? false,
|
|
45
|
-
quiet: opts.quiet ?? false
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
var DEFAULT_SEARCH_LIMIT = "10";
|
|
49
|
-
var DEFAULT_LIST_LIMIT = "20";
|
|
50
|
-
var DEFAULT_CHECK_PLAN_LIMIT = "5";
|
|
51
|
-
var ISO_DATE_PREFIX_LENGTH = 10;
|
|
52
|
-
var AVG_DECIMAL_PLACES = 1;
|
|
53
|
-
var RELEVANCE_DECIMAL_PLACES = 2;
|
|
54
|
-
var JSON_INDENT_SPACES = 2;
|
|
55
|
-
var PRE_COMMIT_MESSAGE = `Before committing, have you captured any valuable lessons from this session?
|
|
56
|
-
Consider: corrections, mistakes, or insights worth remembering.
|
|
57
|
-
|
|
58
|
-
To capture a lesson:
|
|
59
|
-
npx lna capture --trigger "what happened" --insight "what to do" --yes`;
|
|
60
|
-
var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
|
|
61
|
-
# Learning Agent pre-commit hook
|
|
62
|
-
# Reminds Claude to consider capturing lessons before commits
|
|
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
|
-
}
|
|
131
33
|
var SourceSchema = z.enum([
|
|
132
34
|
"user_correction",
|
|
133
35
|
"self_correction",
|
|
@@ -142,7 +44,23 @@ var PatternSchema = z.object({
|
|
|
142
44
|
bad: z.string(),
|
|
143
45
|
good: z.string()
|
|
144
46
|
});
|
|
47
|
+
var CitationSchema = z.object({
|
|
48
|
+
file: z.string().min(1),
|
|
49
|
+
// Source file path (required, non-empty)
|
|
50
|
+
line: z.number().int().positive().optional(),
|
|
51
|
+
// Line number (optional, must be positive)
|
|
52
|
+
commit: z.string().optional()
|
|
53
|
+
// Git commit hash (optional)
|
|
54
|
+
});
|
|
145
55
|
var SeveritySchema = z.enum(["high", "medium", "low"]);
|
|
56
|
+
var CompactionLevelSchema = z.union([
|
|
57
|
+
z.literal(0),
|
|
58
|
+
// Active
|
|
59
|
+
z.literal(1),
|
|
60
|
+
// Flagged (>90 days)
|
|
61
|
+
z.literal(2)
|
|
62
|
+
// Archived
|
|
63
|
+
]);
|
|
146
64
|
var LessonTypeSchema = z.enum(["quick", "full"]);
|
|
147
65
|
var LessonSchema = z.object({
|
|
148
66
|
// Core identity (required)
|
|
@@ -166,7 +84,20 @@ var LessonSchema = z.object({
|
|
|
166
84
|
pattern: PatternSchema.optional(),
|
|
167
85
|
// Lifecycle fields (optional)
|
|
168
86
|
deleted: z.boolean().optional(),
|
|
169
|
-
retrievalCount: z.number().optional()
|
|
87
|
+
retrievalCount: z.number().optional(),
|
|
88
|
+
// Provenance tracking (optional)
|
|
89
|
+
citation: CitationSchema.optional(),
|
|
90
|
+
// Age-based validity fields (optional)
|
|
91
|
+
compactionLevel: CompactionLevelSchema.optional(),
|
|
92
|
+
// 0=active, 1=flagged, 2=archived
|
|
93
|
+
compactedAt: z.string().optional(),
|
|
94
|
+
// ISO8601 when compaction happened
|
|
95
|
+
lastRetrieved: z.string().optional(),
|
|
96
|
+
// ISO8601 last retrieval time
|
|
97
|
+
// Invalidation fields (optional - for marking lessons as wrong)
|
|
98
|
+
invalidatedAt: z.string().optional(),
|
|
99
|
+
// ISO8601
|
|
100
|
+
invalidationReason: z.string().optional()
|
|
170
101
|
});
|
|
171
102
|
z.object({
|
|
172
103
|
id: z.string(),
|
|
@@ -249,6 +180,8 @@ async function readLessons(repoRoot, options = {}) {
|
|
|
249
180
|
}
|
|
250
181
|
return { lessons: Array.from(lessons.values()), skippedCount };
|
|
251
182
|
}
|
|
183
|
+
|
|
184
|
+
// src/storage/sqlite.ts
|
|
252
185
|
var DB_PATH = ".claude/.cache/lessons.sqlite";
|
|
253
186
|
var SCHEMA_SQL = `
|
|
254
187
|
-- Main lessons table
|
|
@@ -270,7 +203,15 @@ var SCHEMA_SQL = `
|
|
|
270
203
|
retrieval_count INTEGER NOT NULL DEFAULT 0,
|
|
271
204
|
last_retrieved TEXT,
|
|
272
205
|
embedding BLOB,
|
|
273
|
-
content_hash TEXT
|
|
206
|
+
content_hash TEXT,
|
|
207
|
+
-- v0.2.2 fields
|
|
208
|
+
invalidated_at TEXT,
|
|
209
|
+
invalidation_reason TEXT,
|
|
210
|
+
citation_file TEXT,
|
|
211
|
+
citation_line INTEGER,
|
|
212
|
+
citation_commit TEXT,
|
|
213
|
+
compaction_level INTEGER DEFAULT 0,
|
|
214
|
+
compacted_at TEXT
|
|
274
215
|
);
|
|
275
216
|
|
|
276
217
|
-- FTS5 virtual table for full-text search
|
|
@@ -379,6 +320,28 @@ function rowToLesson(row) {
|
|
|
379
320
|
if (row.retrieval_count > 0) {
|
|
380
321
|
lesson.retrievalCount = row.retrieval_count;
|
|
381
322
|
}
|
|
323
|
+
if (row.invalidated_at !== null) {
|
|
324
|
+
lesson.invalidatedAt = row.invalidated_at;
|
|
325
|
+
}
|
|
326
|
+
if (row.invalidation_reason !== null) {
|
|
327
|
+
lesson.invalidationReason = row.invalidation_reason;
|
|
328
|
+
}
|
|
329
|
+
if (row.citation_file !== null) {
|
|
330
|
+
lesson.citation = {
|
|
331
|
+
file: row.citation_file,
|
|
332
|
+
...row.citation_line !== null && { line: row.citation_line },
|
|
333
|
+
...row.citation_commit !== null && { commit: row.citation_commit }
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (row.compaction_level !== null && row.compaction_level !== 0) {
|
|
337
|
+
lesson.compactionLevel = row.compaction_level;
|
|
338
|
+
}
|
|
339
|
+
if (row.compacted_at !== null) {
|
|
340
|
+
lesson.compactedAt = row.compacted_at;
|
|
341
|
+
}
|
|
342
|
+
if (row.last_retrieved !== null) {
|
|
343
|
+
lesson.lastRetrieved = row.last_retrieved;
|
|
344
|
+
}
|
|
382
345
|
return lesson;
|
|
383
346
|
}
|
|
384
347
|
function collectCachedEmbeddings(database) {
|
|
@@ -392,8 +355,8 @@ function collectCachedEmbeddings(database) {
|
|
|
392
355
|
return cache;
|
|
393
356
|
}
|
|
394
357
|
var INSERT_LESSON_SQL = `
|
|
395
|
-
INSERT INTO lessons (id, type, trigger, insight, evidence, severity, tags, source, context, supersedes, related, created, confirmed, deleted, retrieval_count, last_retrieved, embedding, content_hash)
|
|
396
|
-
VALUES (@id, @type, @trigger, @insight, @evidence, @severity, @tags, @source, @context, @supersedes, @related, @created, @confirmed, @deleted, @retrieval_count, @last_retrieved, @embedding, @content_hash)
|
|
358
|
+
INSERT INTO lessons (id, type, trigger, insight, evidence, severity, tags, source, context, supersedes, related, created, confirmed, deleted, retrieval_count, last_retrieved, embedding, content_hash, invalidated_at, invalidation_reason, citation_file, citation_line, citation_commit, compaction_level, compacted_at)
|
|
359
|
+
VALUES (@id, @type, @trigger, @insight, @evidence, @severity, @tags, @source, @context, @supersedes, @related, @created, @confirmed, @deleted, @retrieval_count, @last_retrieved, @embedding, @content_hash, @invalidated_at, @invalidation_reason, @citation_file, @citation_line, @citation_commit, @compaction_level, @compacted_at)
|
|
397
360
|
`;
|
|
398
361
|
function getJsonlMtime(repoRoot) {
|
|
399
362
|
const jsonlPath = join(repoRoot, LESSONS_PATH);
|
|
@@ -445,10 +408,17 @@ async function rebuildIndex(repoRoot) {
|
|
|
445
408
|
confirmed: lesson.confirmed ? 1 : 0,
|
|
446
409
|
deleted: lesson.deleted ? 1 : 0,
|
|
447
410
|
retrieval_count: lesson.retrievalCount ?? 0,
|
|
448
|
-
last_retrieved: null,
|
|
449
|
-
// Reset on rebuild since we're rebuilding from source
|
|
411
|
+
last_retrieved: lesson.lastRetrieved ?? null,
|
|
450
412
|
embedding: hasValidCache ? cached.embedding : null,
|
|
451
|
-
content_hash: hasValidCache ? cached.contentHash : null
|
|
413
|
+
content_hash: hasValidCache ? cached.contentHash : null,
|
|
414
|
+
// v0.2.2 fields
|
|
415
|
+
invalidated_at: lesson.invalidatedAt ?? null,
|
|
416
|
+
invalidation_reason: lesson.invalidationReason ?? null,
|
|
417
|
+
citation_file: lesson.citation?.file ?? null,
|
|
418
|
+
citation_line: lesson.citation?.line ?? null,
|
|
419
|
+
citation_commit: lesson.citation?.commit ?? null,
|
|
420
|
+
compaction_level: lesson.compactionLevel ?? 0,
|
|
421
|
+
compacted_at: lesson.compactedAt ?? null
|
|
452
422
|
});
|
|
453
423
|
}
|
|
454
424
|
});
|
|
@@ -483,6 +453,7 @@ async function searchKeyword(repoRoot, query, limit) {
|
|
|
483
453
|
FROM lessons l
|
|
484
454
|
JOIN lessons_fts fts ON l.rowid = fts.rowid
|
|
485
455
|
WHERE lessons_fts MATCH ?
|
|
456
|
+
AND l.invalidated_at IS NULL
|
|
486
457
|
LIMIT ?
|
|
487
458
|
`
|
|
488
459
|
).all(query, limit);
|
|
@@ -517,90 +488,328 @@ function getRetrievalStats(repoRoot) {
|
|
|
517
488
|
lastRetrieved: row.last_retrieved
|
|
518
489
|
}));
|
|
519
490
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
var
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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 [];
|
|
491
|
+
|
|
492
|
+
// src/capture/quality.ts
|
|
493
|
+
var DEFAULT_SIMILARITY_THRESHOLD = 0.8;
|
|
494
|
+
async function isNovel(repoRoot, insight, options = {}) {
|
|
495
|
+
const threshold = options.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
496
|
+
await syncIfNeeded(repoRoot);
|
|
497
|
+
const words = insight.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3).slice(0, 3);
|
|
498
|
+
if (words.length === 0) {
|
|
499
|
+
return { novel: true };
|
|
538
500
|
}
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
501
|
+
const searchQuery = words.join(" OR ");
|
|
502
|
+
const results = await searchKeyword(repoRoot, searchQuery, 10);
|
|
503
|
+
if (results.length === 0) {
|
|
504
|
+
return { novel: true };
|
|
505
|
+
}
|
|
506
|
+
const insightWords = new Set(insight.toLowerCase().split(/\s+/));
|
|
507
|
+
for (const lesson of results) {
|
|
508
|
+
const lessonWords = new Set(lesson.insight.toLowerCase().split(/\s+/));
|
|
509
|
+
const intersection = [...insightWords].filter((w) => lessonWords.has(w)).length;
|
|
510
|
+
const union = (/* @__PURE__ */ new Set([...insightWords, ...lessonWords])).size;
|
|
511
|
+
const similarity = union > 0 ? intersection / union : 0;
|
|
512
|
+
if (similarity >= threshold) {
|
|
513
|
+
return {
|
|
514
|
+
novel: false,
|
|
515
|
+
reason: `Found similar existing lesson: "${lesson.insight.slice(0, 50)}..."`,
|
|
516
|
+
existingId: lesson.id
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
|
|
520
|
+
return {
|
|
521
|
+
novel: false,
|
|
522
|
+
reason: `Exact duplicate found`,
|
|
523
|
+
existingId: lesson.id
|
|
524
|
+
};
|
|
548
525
|
}
|
|
549
526
|
}
|
|
550
|
-
return
|
|
527
|
+
return { novel: true };
|
|
551
528
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
529
|
+
var MIN_WORD_COUNT = 4;
|
|
530
|
+
var VAGUE_PATTERNS = [
|
|
531
|
+
/\bwrite better\b/i,
|
|
532
|
+
/\bbe careful\b/i,
|
|
533
|
+
/\bremember to\b/i,
|
|
534
|
+
/\bmake sure\b/i,
|
|
535
|
+
/\btry to\b/i,
|
|
536
|
+
/\bdouble check\b/i
|
|
537
|
+
];
|
|
538
|
+
var GENERIC_IMPERATIVE_PATTERN = /^(always|never)\s+\w+(\s+\w+){0,2}$/i;
|
|
539
|
+
function isSpecific(insight) {
|
|
540
|
+
const words = insight.trim().split(/\s+/).filter((w) => w.length > 0);
|
|
541
|
+
if (words.length < MIN_WORD_COUNT) {
|
|
542
|
+
return { specific: false, reason: "Insight is too short to be actionable" };
|
|
543
|
+
}
|
|
544
|
+
for (const pattern of VAGUE_PATTERNS) {
|
|
545
|
+
if (pattern.test(insight)) {
|
|
546
|
+
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
558
547
|
}
|
|
559
548
|
}
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
|
|
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);
|
|
549
|
+
if (GENERIC_IMPERATIVE_PATTERN.test(insight)) {
|
|
550
|
+
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
551
|
+
}
|
|
552
|
+
return { specific: true };
|
|
582
553
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
554
|
+
var ACTION_PATTERNS = [
|
|
555
|
+
/\buse\s+.+\s+instead\s+of\b/i,
|
|
556
|
+
// "use X instead of Y"
|
|
557
|
+
/\bprefer\s+.+\s+(over|to)\b/i,
|
|
558
|
+
// "prefer X over Y" or "prefer X to Y"
|
|
559
|
+
/\balways\s+.+\s+when\b/i,
|
|
560
|
+
// "always X when Y"
|
|
561
|
+
/\bnever\s+.+\s+without\b/i,
|
|
562
|
+
// "never X without Y"
|
|
563
|
+
/\bavoid\s+(using\s+)?\w+/i,
|
|
564
|
+
// "avoid X" or "avoid using X"
|
|
565
|
+
/\bcheck\s+.+\s+before\b/i,
|
|
566
|
+
// "check X before Y"
|
|
567
|
+
/^(run|use|add|remove|install|update|configure|set|enable|disable)\s+/i
|
|
568
|
+
// Imperative commands at start
|
|
569
|
+
];
|
|
570
|
+
function isActionable(insight) {
|
|
571
|
+
for (const pattern of ACTION_PATTERNS) {
|
|
572
|
+
if (pattern.test(insight)) {
|
|
573
|
+
return { actionable: true };
|
|
593
574
|
}
|
|
594
575
|
}
|
|
595
|
-
|
|
596
|
-
|
|
576
|
+
return { actionable: false, reason: "Insight lacks clear action guidance" };
|
|
577
|
+
}
|
|
578
|
+
async function shouldPropose(repoRoot, insight) {
|
|
579
|
+
const specificResult = isSpecific(insight);
|
|
580
|
+
if (!specificResult.specific) {
|
|
581
|
+
return { shouldPropose: false, reason: specificResult.reason };
|
|
597
582
|
}
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
583
|
+
const actionableResult = isActionable(insight);
|
|
584
|
+
if (!actionableResult.actionable) {
|
|
585
|
+
return { shouldPropose: false, reason: actionableResult.reason };
|
|
586
|
+
}
|
|
587
|
+
const noveltyResult = await isNovel(repoRoot, insight);
|
|
588
|
+
if (!noveltyResult.novel) {
|
|
589
|
+
return { shouldPropose: false, reason: noveltyResult.reason };
|
|
590
|
+
}
|
|
591
|
+
return { shouldPropose: true };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/capture/triggers.ts
|
|
595
|
+
var USER_CORRECTION_PATTERNS = [
|
|
596
|
+
/\bno\b[,.]?\s/i,
|
|
597
|
+
// "no, ..." or "no ..."
|
|
598
|
+
/\bwrong\b/i,
|
|
599
|
+
// "wrong"
|
|
600
|
+
/\bactually\b/i,
|
|
601
|
+
// "actually..."
|
|
602
|
+
/\bnot that\b/i,
|
|
603
|
+
// "not that"
|
|
604
|
+
/\bi meant\b/i
|
|
605
|
+
// "I meant"
|
|
606
|
+
];
|
|
607
|
+
function detectUserCorrection(signals) {
|
|
608
|
+
const { messages, context } = signals;
|
|
609
|
+
if (messages.length < 2) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
for (let i = 1; i < messages.length; i++) {
|
|
613
|
+
const message = messages[i];
|
|
614
|
+
if (!message) continue;
|
|
615
|
+
for (const pattern of USER_CORRECTION_PATTERNS) {
|
|
616
|
+
if (pattern.test(message)) {
|
|
617
|
+
return {
|
|
618
|
+
trigger: `User correction during ${context.intent}`,
|
|
619
|
+
correctionMessage: message,
|
|
620
|
+
context
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
function detectSelfCorrection(history) {
|
|
628
|
+
const { edits } = history;
|
|
629
|
+
if (edits.length < 3) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
for (let i = 0; i <= edits.length - 3; i++) {
|
|
633
|
+
const first = edits[i];
|
|
634
|
+
const second = edits[i + 1];
|
|
635
|
+
const third = edits[i + 2];
|
|
636
|
+
if (!first || !second || !third) continue;
|
|
637
|
+
if (first.file === second.file && second.file === third.file && first.success && !second.success && third.success) {
|
|
638
|
+
return {
|
|
639
|
+
file: first.file,
|
|
640
|
+
trigger: `Self-correction on ${first.file}`
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function detectTestFailure(testResult) {
|
|
647
|
+
if (testResult.passed) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const lines = testResult.output.split("\n").filter((line) => line.trim().length > 0);
|
|
651
|
+
const errorLine = lines.find((line) => /error|fail|assert/i.test(line)) ?? lines[0] ?? "";
|
|
652
|
+
return {
|
|
653
|
+
testFile: testResult.testFile,
|
|
654
|
+
errorOutput: testResult.output,
|
|
655
|
+
trigger: `Test failure in ${testResult.testFile}: ${errorLine.slice(0, 100)}`
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
async function detectAndPropose(repoRoot, input) {
|
|
659
|
+
const detected = runDetector(input);
|
|
660
|
+
if (!detected) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
const { trigger, source, proposedInsight } = detected;
|
|
664
|
+
const quality = await shouldPropose(repoRoot, proposedInsight);
|
|
665
|
+
if (!quality.shouldPropose) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
return { trigger, source, proposedInsight };
|
|
669
|
+
}
|
|
670
|
+
function runDetector(input) {
|
|
671
|
+
switch (input.type) {
|
|
672
|
+
case "user":
|
|
673
|
+
return detectUserCorrectionFlow(input.data);
|
|
674
|
+
case "self":
|
|
675
|
+
return detectSelfCorrectionFlow(input.data);
|
|
676
|
+
case "test":
|
|
677
|
+
return detectTestFailureFlow(input.data);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function detectUserCorrectionFlow(data) {
|
|
681
|
+
const result = detectUserCorrection(data);
|
|
682
|
+
if (!result) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
trigger: result.trigger,
|
|
687
|
+
source: "user_correction",
|
|
688
|
+
proposedInsight: result.correctionMessage
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function detectSelfCorrectionFlow(data) {
|
|
692
|
+
const result = detectSelfCorrection(data);
|
|
693
|
+
if (!result) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
trigger: result.trigger,
|
|
698
|
+
source: "self_correction",
|
|
699
|
+
// Self-corrections need context to form useful insights
|
|
700
|
+
proposedInsight: `Check ${result.file} for common errors before editing`
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function detectTestFailureFlow(data) {
|
|
704
|
+
const result = detectTestFailure(data);
|
|
705
|
+
if (!result) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
trigger: result.trigger,
|
|
710
|
+
source: "test_failure",
|
|
711
|
+
proposedInsight: result.errorOutput
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
var VALID_TYPES = /* @__PURE__ */ new Set(["user", "self", "test"]);
|
|
715
|
+
async function parseInputFile(filePath) {
|
|
716
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
717
|
+
const data = JSON.parse(content);
|
|
718
|
+
if (!VALID_TYPES.has(data.type)) {
|
|
719
|
+
throw new Error(`Invalid detection type: ${data.type}. Must be one of: user, self, test`);
|
|
720
|
+
}
|
|
721
|
+
return data;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/utils.ts
|
|
725
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
726
|
+
function getLessonAgeDays(lesson) {
|
|
727
|
+
const created = new Date(lesson.created).getTime();
|
|
728
|
+
const now = Date.now();
|
|
729
|
+
return Math.floor((now - created) / MS_PER_DAY);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/storage/compact.ts
|
|
733
|
+
var ARCHIVE_DIR = ".claude/lessons/archive";
|
|
734
|
+
var TOMBSTONE_THRESHOLD = 100;
|
|
735
|
+
var ARCHIVE_AGE_DAYS = 90;
|
|
736
|
+
var MONTH_INDEX_OFFSET = 1;
|
|
737
|
+
var MONTH_PAD_LENGTH = 2;
|
|
738
|
+
function getArchivePath(repoRoot, date) {
|
|
739
|
+
const year = date.getFullYear();
|
|
740
|
+
const month = String(date.getMonth() + MONTH_INDEX_OFFSET).padStart(MONTH_PAD_LENGTH, "0");
|
|
741
|
+
return join(repoRoot, ARCHIVE_DIR, `${year}-${month}.jsonl`);
|
|
742
|
+
}
|
|
743
|
+
async function parseRawJsonlLines(repoRoot) {
|
|
744
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
745
|
+
let content;
|
|
746
|
+
try {
|
|
747
|
+
content = await readFile(filePath, "utf-8");
|
|
748
|
+
} catch {
|
|
749
|
+
return [];
|
|
750
|
+
}
|
|
751
|
+
const results = [];
|
|
752
|
+
for (const line of content.split("\n")) {
|
|
753
|
+
const trimmed = line.trim();
|
|
754
|
+
if (!trimmed) continue;
|
|
755
|
+
try {
|
|
756
|
+
const parsed = JSON.parse(trimmed);
|
|
757
|
+
results.push({ line: trimmed, parsed });
|
|
758
|
+
} catch {
|
|
759
|
+
results.push({ line: trimmed, parsed: null });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return results;
|
|
763
|
+
}
|
|
764
|
+
async function countTombstones(repoRoot) {
|
|
765
|
+
const lines = await parseRawJsonlLines(repoRoot);
|
|
766
|
+
let count = 0;
|
|
767
|
+
for (const { parsed } of lines) {
|
|
768
|
+
if (parsed && parsed["deleted"] === true) {
|
|
769
|
+
count++;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return count;
|
|
773
|
+
}
|
|
774
|
+
async function needsCompaction(repoRoot) {
|
|
775
|
+
const count = await countTombstones(repoRoot);
|
|
776
|
+
return count >= TOMBSTONE_THRESHOLD;
|
|
777
|
+
}
|
|
778
|
+
async function rewriteWithoutTombstones(repoRoot) {
|
|
779
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
780
|
+
const tempPath = filePath + ".tmp";
|
|
781
|
+
const { lessons } = await readLessons(repoRoot);
|
|
782
|
+
const tombstoneCount = await countTombstones(repoRoot);
|
|
783
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
784
|
+
const lines = lessons.map((lesson) => JSON.stringify(lesson) + "\n");
|
|
785
|
+
await writeFile(tempPath, lines.join(""), "utf-8");
|
|
786
|
+
await rename(tempPath, filePath);
|
|
787
|
+
return tombstoneCount;
|
|
788
|
+
}
|
|
789
|
+
function shouldArchive(lesson) {
|
|
790
|
+
const ageDays = getLessonAgeDays(lesson);
|
|
791
|
+
return ageDays > ARCHIVE_AGE_DAYS && (lesson.retrievalCount === void 0 || lesson.retrievalCount === 0);
|
|
792
|
+
}
|
|
793
|
+
async function archiveOldLessons(repoRoot) {
|
|
794
|
+
const { lessons } = await readLessons(repoRoot);
|
|
795
|
+
const toArchive = [];
|
|
796
|
+
const toKeep = [];
|
|
797
|
+
for (const lesson of lessons) {
|
|
798
|
+
if (shouldArchive(lesson)) {
|
|
799
|
+
toArchive.push(lesson);
|
|
800
|
+
} else {
|
|
801
|
+
toKeep.push(lesson);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (toArchive.length === 0) {
|
|
805
|
+
return 0;
|
|
806
|
+
}
|
|
807
|
+
const archiveGroups = /* @__PURE__ */ new Map();
|
|
808
|
+
for (const lesson of toArchive) {
|
|
809
|
+
const created = new Date(lesson.created);
|
|
810
|
+
const archivePath = getArchivePath(repoRoot, created);
|
|
811
|
+
const group = archiveGroups.get(archivePath) ?? [];
|
|
812
|
+
group.push(lesson);
|
|
604
813
|
archiveGroups.set(archivePath, group);
|
|
605
814
|
}
|
|
606
815
|
const archiveDir = join(repoRoot, ARCHIVE_DIR);
|
|
@@ -630,151 +839,393 @@ async function compact(repoRoot) {
|
|
|
630
839
|
lessonsRemaining: lessons.length
|
|
631
840
|
};
|
|
632
841
|
}
|
|
842
|
+
var out = {
|
|
843
|
+
success: (msg) => console.log(chalk.green("[ok]"), msg),
|
|
844
|
+
error: (msg) => console.error(chalk.red("[error]"), msg),
|
|
845
|
+
info: (msg) => console.log(chalk.blue("[info]"), msg),
|
|
846
|
+
warn: (msg) => console.log(chalk.yellow("[warn]"), msg)
|
|
847
|
+
};
|
|
848
|
+
function getGlobalOpts(cmd) {
|
|
849
|
+
const opts = cmd.optsWithGlobals();
|
|
850
|
+
return {
|
|
851
|
+
verbose: opts.verbose ?? false,
|
|
852
|
+
quiet: opts.quiet ?? false
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
var DEFAULT_SEARCH_LIMIT = "10";
|
|
856
|
+
var DEFAULT_LIST_LIMIT = "20";
|
|
857
|
+
var DEFAULT_CHECK_PLAN_LIMIT = "5";
|
|
858
|
+
var LESSON_COUNT_WARNING_THRESHOLD = 20;
|
|
859
|
+
var AGE_FLAG_THRESHOLD_DAYS = 90;
|
|
860
|
+
var ISO_DATE_PREFIX_LENGTH = 10;
|
|
861
|
+
var AVG_DECIMAL_PLACES = 1;
|
|
862
|
+
var RELEVANCE_DECIMAL_PLACES = 2;
|
|
863
|
+
var JSON_INDENT_SPACES = 2;
|
|
633
864
|
|
|
634
|
-
// src/
|
|
635
|
-
function
|
|
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
|
-
});
|
|
865
|
+
// src/commands/capture.ts
|
|
866
|
+
function createLessonFromFlags(trigger, insight, confirmed) {
|
|
867
|
+
return {
|
|
868
|
+
id: generateId(insight),
|
|
869
|
+
type: "quick",
|
|
870
|
+
trigger,
|
|
871
|
+
insight,
|
|
872
|
+
tags: [],
|
|
873
|
+
source: "manual",
|
|
874
|
+
context: { tool: "capture", intent: "manual capture" },
|
|
875
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
876
|
+
confirmed,
|
|
877
|
+
supersedes: [],
|
|
878
|
+
related: []
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function outputCaptureJson(lesson, saved) {
|
|
882
|
+
console.log(JSON.stringify({
|
|
883
|
+
id: lesson.id,
|
|
884
|
+
trigger: lesson.trigger,
|
|
885
|
+
insight: lesson.insight,
|
|
886
|
+
type: lesson.type,
|
|
887
|
+
saved
|
|
888
|
+
}));
|
|
889
|
+
}
|
|
890
|
+
function outputCapturePreview(lesson) {
|
|
891
|
+
console.log("Lesson captured:");
|
|
892
|
+
console.log(` ID: ${lesson.id}`);
|
|
893
|
+
console.log(` Trigger: ${lesson.trigger}`);
|
|
894
|
+
console.log(` Insight: ${lesson.insight}`);
|
|
895
|
+
console.log(` Type: ${lesson.type}`);
|
|
896
|
+
console.log(` Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
|
|
897
|
+
console.log("\nSave this lesson? [y/n]");
|
|
898
|
+
}
|
|
899
|
+
function createLessonFromInputFile(result, confirmed) {
|
|
900
|
+
return {
|
|
901
|
+
id: generateId(result.proposedInsight),
|
|
902
|
+
type: "quick",
|
|
903
|
+
trigger: result.trigger,
|
|
904
|
+
insight: result.proposedInsight,
|
|
905
|
+
tags: [],
|
|
906
|
+
source: result.source,
|
|
907
|
+
context: { tool: "capture", intent: "auto-capture" },
|
|
908
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
909
|
+
confirmed,
|
|
910
|
+
supersedes: [],
|
|
911
|
+
related: []
|
|
912
|
+
};
|
|
663
913
|
}
|
|
664
|
-
function
|
|
665
|
-
program2.command("
|
|
914
|
+
function registerCaptureCommands(program2) {
|
|
915
|
+
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").option("--citation <file:line>", "Source file (optionally with :line number)").option("--citation-commit <hash>", "Git commit hash for citation").action(async function(insight, options) {
|
|
666
916
|
const repoRoot = getRepoRoot();
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
917
|
+
const { quiet } = getGlobalOpts(this);
|
|
918
|
+
let severity;
|
|
919
|
+
if (options.severity !== void 0) {
|
|
920
|
+
const result = SeveritySchema.safeParse(options.severity);
|
|
921
|
+
if (!result.success) {
|
|
922
|
+
out.error(`Invalid severity value: "${options.severity}". Valid values are: high, medium, low`);
|
|
923
|
+
process.exit(1);
|
|
674
924
|
}
|
|
675
|
-
|
|
925
|
+
severity = result.data;
|
|
926
|
+
}
|
|
927
|
+
const lessonType = severity !== void 0 ? "full" : "quick";
|
|
928
|
+
let citation;
|
|
929
|
+
if (options.citation) {
|
|
930
|
+
const parts = options.citation.split(":");
|
|
931
|
+
const file = parts[0] ?? "";
|
|
932
|
+
const lineStr = parts[1];
|
|
933
|
+
const line = lineStr ? parseInt(lineStr, 10) : void 0;
|
|
934
|
+
citation = {
|
|
935
|
+
file,
|
|
936
|
+
...line && !isNaN(line) && { line },
|
|
937
|
+
...options.citationCommit && { commit: options.citationCommit }
|
|
938
|
+
};
|
|
676
939
|
}
|
|
677
|
-
const
|
|
940
|
+
const lesson = {
|
|
941
|
+
id: generateId(insight),
|
|
942
|
+
type: lessonType,
|
|
943
|
+
trigger: options.trigger ?? "Manual capture",
|
|
944
|
+
insight,
|
|
945
|
+
tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : [],
|
|
946
|
+
source: "manual",
|
|
947
|
+
context: {
|
|
948
|
+
tool: "cli",
|
|
949
|
+
intent: "manual learning"
|
|
950
|
+
},
|
|
951
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
952
|
+
confirmed: true,
|
|
953
|
+
// learn command is explicit confirmation
|
|
954
|
+
supersedes: [],
|
|
955
|
+
related: [],
|
|
956
|
+
...severity !== void 0 && { severity },
|
|
957
|
+
...citation && { citation }
|
|
958
|
+
};
|
|
959
|
+
await appendLesson(repoRoot, lesson);
|
|
960
|
+
const chalk3 = await import('chalk');
|
|
961
|
+
out.success(`Learned: ${insight}`);
|
|
678
962
|
if (!quiet) {
|
|
679
|
-
|
|
680
|
-
|
|
963
|
+
console.log(`ID: ${chalk3.default.dim(lesson.id)}`);
|
|
964
|
+
if (citation) {
|
|
965
|
+
console.log(`Citation: ${chalk3.default.dim(citation.file)}${citation.line ? `:${citation.line}` : ""}`);
|
|
966
|
+
}
|
|
681
967
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (
|
|
688
|
-
console.log(
|
|
968
|
+
});
|
|
969
|
+
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(
|
|
970
|
+
async (options) => {
|
|
971
|
+
const repoRoot = getRepoRoot();
|
|
972
|
+
if (options.save && !options.yes) {
|
|
973
|
+
if (options.json) {
|
|
974
|
+
console.log(JSON.stringify({ error: "--save requires --yes flag for confirmation" }));
|
|
975
|
+
} else {
|
|
976
|
+
out.error("--save requires --yes flag for confirmation");
|
|
977
|
+
console.log("Use: detect --input <file> --save --yes");
|
|
689
978
|
}
|
|
690
|
-
|
|
691
|
-
console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
|
|
979
|
+
process.exit(1);
|
|
692
980
|
}
|
|
693
|
-
|
|
694
|
-
|
|
981
|
+
const input = await parseInputFile(options.input);
|
|
982
|
+
const result = await detectAndPropose(repoRoot, input);
|
|
983
|
+
if (!result) {
|
|
984
|
+
if (options.json) {
|
|
985
|
+
console.log(JSON.stringify({ detected: false }));
|
|
986
|
+
} else {
|
|
987
|
+
console.log("No learning trigger detected.");
|
|
988
|
+
}
|
|
989
|
+
return;
|
|
695
990
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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}`);
|
|
991
|
+
if (options.json) {
|
|
992
|
+
console.log(JSON.stringify({ detected: true, ...result }));
|
|
993
|
+
return;
|
|
724
994
|
}
|
|
725
|
-
|
|
726
|
-
|
|
995
|
+
console.log("Learning trigger detected!");
|
|
996
|
+
console.log(` Trigger: ${result.trigger}`);
|
|
997
|
+
console.log(` Source: ${result.source}`);
|
|
998
|
+
console.log(` Proposed: ${result.proposedInsight}`);
|
|
999
|
+
if (options.save && options.yes) {
|
|
1000
|
+
const lesson = {
|
|
1001
|
+
id: generateId(result.proposedInsight),
|
|
1002
|
+
type: "quick",
|
|
1003
|
+
trigger: result.trigger,
|
|
1004
|
+
insight: result.proposedInsight,
|
|
1005
|
+
tags: [],
|
|
1006
|
+
source: result.source,
|
|
1007
|
+
context: { tool: "detect", intent: "auto-capture" },
|
|
1008
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1009
|
+
confirmed: true,
|
|
1010
|
+
// --yes confirms the lesson
|
|
1011
|
+
supersedes: [],
|
|
1012
|
+
related: []
|
|
1013
|
+
};
|
|
1014
|
+
await appendLesson(repoRoot, lesson);
|
|
1015
|
+
console.log(`
|
|
1016
|
+
Saved as lesson: ${lesson.id}`);
|
|
727
1017
|
}
|
|
728
|
-
console.log();
|
|
729
1018
|
}
|
|
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) => {
|
|
1019
|
+
);
|
|
1020
|
+
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) {
|
|
736
1021
|
const repoRoot = getRepoRoot();
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1022
|
+
const { verbose } = getGlobalOpts(this);
|
|
1023
|
+
let lesson;
|
|
1024
|
+
if (options.input) {
|
|
1025
|
+
const input = await parseInputFile(options.input);
|
|
1026
|
+
const result = await detectAndPropose(repoRoot, input);
|
|
1027
|
+
if (!result) {
|
|
1028
|
+
options.json ? console.log(JSON.stringify({ detected: false, saved: false })) : console.log("No learning trigger detected.");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
lesson = createLessonFromInputFile(result, options.yes ?? false);
|
|
1032
|
+
} else if (options.trigger && options.insight) {
|
|
1033
|
+
lesson = createLessonFromFlags(options.trigger, options.insight, options.yes ?? false);
|
|
741
1034
|
} else {
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
1035
|
+
const msg = "Provide either --trigger and --insight, or --input file.";
|
|
1036
|
+
options.json ? console.log(JSON.stringify({ error: msg, saved: false })) : out.error(msg);
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
if (!options.yes && !process.stdin.isTTY) {
|
|
1040
|
+
if (options.json) {
|
|
1041
|
+
console.log(JSON.stringify({ error: "--yes required in non-interactive mode", saved: false }));
|
|
745
1042
|
} else {
|
|
746
|
-
|
|
1043
|
+
out.error("--yes required in non-interactive mode");
|
|
1044
|
+
console.log('Use: capture --trigger "..." --insight "..." --yes');
|
|
747
1045
|
}
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
if (options.json) {
|
|
1049
|
+
if (options.yes) await appendLesson(repoRoot, lesson);
|
|
1050
|
+
outputCaptureJson(lesson, options.yes ?? false);
|
|
1051
|
+
} else if (options.yes) {
|
|
1052
|
+
await appendLesson(repoRoot, lesson);
|
|
1053
|
+
out.success(`Lesson saved: ${lesson.id}`);
|
|
1054
|
+
if (verbose) console.log(` Type: ${lesson.type} | Trigger: ${lesson.trigger}`);
|
|
1055
|
+
} else {
|
|
1056
|
+
outputCapturePreview(lesson);
|
|
748
1057
|
}
|
|
749
1058
|
});
|
|
750
1059
|
}
|
|
751
|
-
|
|
752
|
-
|
|
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) => {
|
|
1060
|
+
function registerManagementCommands(program2) {
|
|
1061
|
+
program2.command("wrong <id>").description("Mark a lesson as invalid/wrong").option("-r, --reason <text>", "Reason for invalidation").action(async function(id, options) {
|
|
755
1062
|
const repoRoot = getRepoRoot();
|
|
756
1063
|
const { lessons } = await readLessons(repoRoot);
|
|
757
|
-
|
|
758
|
-
if (
|
|
759
|
-
|
|
760
|
-
|
|
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);
|
|
1064
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1065
|
+
if (!lesson) {
|
|
1066
|
+
out.error(`Lesson not found: ${id}`);
|
|
1067
|
+
process.exit(1);
|
|
765
1068
|
}
|
|
766
|
-
if (
|
|
767
|
-
|
|
768
|
-
|
|
1069
|
+
if (lesson.invalidatedAt) {
|
|
1070
|
+
out.warn(`Lesson ${id} is already marked as invalid.`);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const updatedLesson = {
|
|
1074
|
+
...lesson,
|
|
1075
|
+
invalidatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1076
|
+
...options.reason !== void 0 && { invalidationReason: options.reason }
|
|
1077
|
+
};
|
|
1078
|
+
await appendLesson(repoRoot, updatedLesson);
|
|
1079
|
+
out.success(`Lesson ${id} marked as invalid.`);
|
|
1080
|
+
if (options.reason) {
|
|
1081
|
+
console.log(` Reason: ${options.reason}`);
|
|
769
1082
|
}
|
|
770
|
-
console.log(JSON.stringify(filtered, null, JSON_INDENT_SPACES));
|
|
771
1083
|
});
|
|
772
|
-
|
|
773
|
-
function registerImportCommand(program2) {
|
|
774
|
-
program2.command("import <file>").description("Import lessons from a JSONL file").action(async (file) => {
|
|
1084
|
+
program2.command("validate <id>").description("Re-enable a previously invalidated lesson").action(async function(id) {
|
|
775
1085
|
const repoRoot = getRepoRoot();
|
|
776
|
-
|
|
777
|
-
|
|
1086
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1087
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1088
|
+
if (!lesson) {
|
|
1089
|
+
out.error(`Lesson not found: ${id}`);
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
}
|
|
1092
|
+
if (!lesson.invalidatedAt) {
|
|
1093
|
+
out.info(`Lesson ${id} is not invalidated.`);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const updatedLesson = {
|
|
1097
|
+
id: lesson.id,
|
|
1098
|
+
type: lesson.type,
|
|
1099
|
+
trigger: lesson.trigger,
|
|
1100
|
+
insight: lesson.insight,
|
|
1101
|
+
tags: lesson.tags,
|
|
1102
|
+
source: lesson.source,
|
|
1103
|
+
context: lesson.context,
|
|
1104
|
+
created: lesson.created,
|
|
1105
|
+
confirmed: lesson.confirmed,
|
|
1106
|
+
supersedes: lesson.supersedes,
|
|
1107
|
+
related: lesson.related,
|
|
1108
|
+
// Include optional fields if present (excluding invalidation)
|
|
1109
|
+
...lesson.evidence !== void 0 && { evidence: lesson.evidence },
|
|
1110
|
+
...lesson.severity !== void 0 && { severity: lesson.severity },
|
|
1111
|
+
...lesson.pattern !== void 0 && { pattern: lesson.pattern },
|
|
1112
|
+
...lesson.deleted !== void 0 && { deleted: lesson.deleted },
|
|
1113
|
+
...lesson.retrievalCount !== void 0 && { retrievalCount: lesson.retrievalCount },
|
|
1114
|
+
...lesson.citation !== void 0 && { citation: lesson.citation },
|
|
1115
|
+
...lesson.compactionLevel !== void 0 && { compactionLevel: lesson.compactionLevel },
|
|
1116
|
+
...lesson.compactedAt !== void 0 && { compactedAt: lesson.compactedAt },
|
|
1117
|
+
...lesson.lastRetrieved !== void 0 && { lastRetrieved: lesson.lastRetrieved }
|
|
1118
|
+
};
|
|
1119
|
+
await appendLesson(repoRoot, updatedLesson);
|
|
1120
|
+
out.success(`Lesson ${id} re-enabled (validated).`);
|
|
1121
|
+
});
|
|
1122
|
+
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) => {
|
|
1123
|
+
const repoRoot = getRepoRoot();
|
|
1124
|
+
const tombstones = await countTombstones(repoRoot);
|
|
1125
|
+
const needs = await needsCompaction(repoRoot);
|
|
1126
|
+
if (options.dryRun) {
|
|
1127
|
+
console.log("Dry run - no changes will be made.\n");
|
|
1128
|
+
console.log(`Tombstones found: ${tombstones}`);
|
|
1129
|
+
console.log(`Compaction needed: ${needs ? "yes" : "no"}`);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (!needs && !options.force) {
|
|
1133
|
+
console.log(`Compaction not needed (${tombstones} tombstones, threshold is ${TOMBSTONE_THRESHOLD}).`);
|
|
1134
|
+
console.log("Use --force to compact anyway.");
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
console.log("Running compaction...");
|
|
1138
|
+
const result = await compact(repoRoot);
|
|
1139
|
+
console.log("\nCompaction complete:");
|
|
1140
|
+
console.log(` Archived: ${result.archived} lesson(s)`);
|
|
1141
|
+
console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
|
|
1142
|
+
console.log(` Lessons remaining: ${result.lessonsRemaining}`);
|
|
1143
|
+
await rebuildIndex(repoRoot);
|
|
1144
|
+
console.log(" Index rebuilt.");
|
|
1145
|
+
});
|
|
1146
|
+
program2.command("rebuild").description("Rebuild SQLite index from JSONL").option("-f, --force", "Force rebuild even if unchanged").action(async (options) => {
|
|
1147
|
+
const repoRoot = getRepoRoot();
|
|
1148
|
+
if (options.force) {
|
|
1149
|
+
console.log("Forcing index rebuild...");
|
|
1150
|
+
await rebuildIndex(repoRoot);
|
|
1151
|
+
console.log("Index rebuilt.");
|
|
1152
|
+
} else {
|
|
1153
|
+
const rebuilt = await syncIfNeeded(repoRoot);
|
|
1154
|
+
if (rebuilt) {
|
|
1155
|
+
console.log("Index rebuilt (JSONL changed).");
|
|
1156
|
+
} else {
|
|
1157
|
+
console.log("Index is up to date.");
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
program2.command("stats").description("Show database health and statistics").action(async () => {
|
|
1162
|
+
const repoRoot = getRepoRoot();
|
|
1163
|
+
await syncIfNeeded(repoRoot);
|
|
1164
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1165
|
+
const deletedCount = await countTombstones(repoRoot);
|
|
1166
|
+
const totalLessons = lessons.length;
|
|
1167
|
+
const retrievalStats = getRetrievalStats(repoRoot);
|
|
1168
|
+
const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
|
|
1169
|
+
const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
|
|
1170
|
+
const jsonlPath = join(repoRoot, LESSONS_PATH);
|
|
1171
|
+
const dbPath = join(repoRoot, DB_PATH);
|
|
1172
|
+
let dataSize = 0;
|
|
1173
|
+
let indexSize = 0;
|
|
1174
|
+
try {
|
|
1175
|
+
dataSize = statSync(jsonlPath).size;
|
|
1176
|
+
} catch {
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
indexSize = statSync(dbPath).size;
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
const totalSize = dataSize + indexSize;
|
|
1183
|
+
let recentCount = 0;
|
|
1184
|
+
let mediumCount = 0;
|
|
1185
|
+
let oldCount = 0;
|
|
1186
|
+
for (const lesson of lessons) {
|
|
1187
|
+
const ageDays = getLessonAgeDays(lesson);
|
|
1188
|
+
if (ageDays < 30) {
|
|
1189
|
+
recentCount++;
|
|
1190
|
+
} else if (ageDays <= AGE_FLAG_THRESHOLD_DAYS) {
|
|
1191
|
+
mediumCount++;
|
|
1192
|
+
} else {
|
|
1193
|
+
oldCount++;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const deletedInfo = deletedCount > 0 ? ` (${deletedCount} deleted)` : "";
|
|
1197
|
+
console.log(`Lessons: ${totalLessons} total${deletedInfo}`);
|
|
1198
|
+
if (totalLessons > LESSON_COUNT_WARNING_THRESHOLD) {
|
|
1199
|
+
out.warn(`High lesson count may degrade retrieval quality. Consider running \`lna compact\`.`);
|
|
1200
|
+
}
|
|
1201
|
+
if (totalLessons > 0) {
|
|
1202
|
+
console.log(`Age: ${recentCount} <30d, ${mediumCount} 30-90d, ${oldCount} >90d`);
|
|
1203
|
+
}
|
|
1204
|
+
console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
|
|
1205
|
+
console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
|
|
1206
|
+
});
|
|
1207
|
+
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) => {
|
|
1208
|
+
const repoRoot = getRepoRoot();
|
|
1209
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1210
|
+
let filtered = lessons;
|
|
1211
|
+
if (options.since) {
|
|
1212
|
+
const sinceDate = new Date(options.since);
|
|
1213
|
+
if (Number.isNaN(sinceDate.getTime())) {
|
|
1214
|
+
console.error(`Invalid date format: ${options.since}. Use ISO8601 format (e.g., 2024-01-15).`);
|
|
1215
|
+
process.exit(1);
|
|
1216
|
+
}
|
|
1217
|
+
filtered = filtered.filter((lesson) => new Date(lesson.created) >= sinceDate);
|
|
1218
|
+
}
|
|
1219
|
+
if (options.tags) {
|
|
1220
|
+
const filterTags = options.tags.split(",").map((t) => t.trim());
|
|
1221
|
+
filtered = filtered.filter((lesson) => lesson.tags.some((tag) => filterTags.includes(tag)));
|
|
1222
|
+
}
|
|
1223
|
+
console.log(JSON.stringify(filtered, null, JSON_INDENT_SPACES));
|
|
1224
|
+
});
|
|
1225
|
+
program2.command("import <file>").description("Import lessons from a JSONL file").action(async (file) => {
|
|
1226
|
+
const repoRoot = getRepoRoot();
|
|
1227
|
+
let content;
|
|
1228
|
+
try {
|
|
778
1229
|
content = await readFile(file, "utf-8");
|
|
779
1230
|
} catch (err) {
|
|
780
1231
|
const code = err.code;
|
|
@@ -815,7 +1266,6 @@ function registerImportCommand(program2) {
|
|
|
815
1266
|
existingIds.add(lesson.id);
|
|
816
1267
|
imported++;
|
|
817
1268
|
}
|
|
818
|
-
await syncIfNeeded(repoRoot);
|
|
819
1269
|
const lessonWord = imported === 1 ? "lesson" : "lessons";
|
|
820
1270
|
const parts = [];
|
|
821
1271
|
if (skipped > 0) parts.push(`${skipped} skipped`);
|
|
@@ -826,125 +1276,304 @@ function registerImportCommand(program2) {
|
|
|
826
1276
|
console.log(`Imported ${imported} ${lessonWord}`);
|
|
827
1277
|
}
|
|
828
1278
|
});
|
|
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) => {
|
|
1279
|
+
const SHOW_JSON_INDENT = 2;
|
|
1280
|
+
program2.command("show <id>").description("Show details of a specific lesson").option("--json", "Output as JSON").action(async (id, options) => {
|
|
834
1281
|
const repoRoot = getRepoRoot();
|
|
835
|
-
const
|
|
836
|
-
const
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1282
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1283
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1284
|
+
if (!lesson) {
|
|
1285
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
1286
|
+
let wasDeleted = false;
|
|
1287
|
+
try {
|
|
1288
|
+
const content = await readFile(filePath, "utf-8");
|
|
1289
|
+
const lines = content.split("\n");
|
|
1290
|
+
for (const line of lines) {
|
|
1291
|
+
const trimmed = line.trim();
|
|
1292
|
+
if (!trimmed) continue;
|
|
1293
|
+
try {
|
|
1294
|
+
const record = JSON.parse(trimmed);
|
|
1295
|
+
if (record.id === id && record.deleted === true) {
|
|
1296
|
+
wasDeleted = true;
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
} catch {
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
} catch {
|
|
1303
|
+
}
|
|
1304
|
+
if (options.json) {
|
|
1305
|
+
console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found` }));
|
|
1306
|
+
} else {
|
|
1307
|
+
out.error(wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found`);
|
|
1308
|
+
}
|
|
1309
|
+
process.exit(1);
|
|
842
1310
|
}
|
|
843
|
-
if (
|
|
844
|
-
console.log(
|
|
845
|
-
|
|
846
|
-
|
|
1311
|
+
if (options.json) {
|
|
1312
|
+
console.log(JSON.stringify(lesson, null, SHOW_JSON_INDENT));
|
|
1313
|
+
} else {
|
|
1314
|
+
console.log(formatLessonHuman(lesson));
|
|
847
1315
|
}
|
|
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
1316
|
});
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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);
|
|
1317
|
+
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) => {
|
|
1318
|
+
const repoRoot = getRepoRoot();
|
|
1319
|
+
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;
|
|
1320
|
+
if (!hasUpdates) {
|
|
1321
|
+
if (options.json) {
|
|
1322
|
+
console.log(JSON.stringify({ error: "No fields to update (specify at least one: --insight, --tags, --severity, ...)" }));
|
|
1323
|
+
} else {
|
|
1324
|
+
out.error("No fields to update (specify at least one: --insight, --tags, --severity, ...)");
|
|
1325
|
+
}
|
|
1326
|
+
process.exit(1);
|
|
916
1327
|
}
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1328
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1329
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1330
|
+
if (!lesson) {
|
|
1331
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
1332
|
+
let wasDeleted = false;
|
|
1333
|
+
try {
|
|
1334
|
+
const content = await readFile(filePath, "utf-8");
|
|
1335
|
+
const lines = content.split("\n");
|
|
1336
|
+
for (const line of lines) {
|
|
1337
|
+
const trimmed = line.trim();
|
|
1338
|
+
if (!trimmed) continue;
|
|
1339
|
+
try {
|
|
1340
|
+
const record = JSON.parse(trimmed);
|
|
1341
|
+
if (record.id === id && record.deleted === true) {
|
|
1342
|
+
wasDeleted = true;
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
} catch {
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
} catch {
|
|
1349
|
+
}
|
|
1350
|
+
if (options.json) {
|
|
1351
|
+
console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found` }));
|
|
1352
|
+
} else {
|
|
1353
|
+
out.error(wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found`);
|
|
1354
|
+
}
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1357
|
+
if (options.severity !== void 0) {
|
|
1358
|
+
const result = SeveritySchema.safeParse(options.severity);
|
|
1359
|
+
if (!result.success) {
|
|
1360
|
+
if (options.json) {
|
|
1361
|
+
console.log(JSON.stringify({ error: `Invalid severity '${options.severity}' (must be: high, medium, low)` }));
|
|
1362
|
+
} else {
|
|
1363
|
+
out.error(`Invalid severity '${options.severity}' (must be: high, medium, low)`);
|
|
1364
|
+
}
|
|
1365
|
+
process.exit(1);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const updatedLesson = {
|
|
1369
|
+
...lesson,
|
|
1370
|
+
...options.insight !== void 0 && { insight: options.insight },
|
|
1371
|
+
...options.trigger !== void 0 && { trigger: options.trigger },
|
|
1372
|
+
...options.evidence !== void 0 && { evidence: options.evidence },
|
|
1373
|
+
...options.severity !== void 0 && { severity: options.severity },
|
|
1374
|
+
...options.tags !== void 0 && {
|
|
1375
|
+
tags: [...new Set(
|
|
1376
|
+
options.tags.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
|
|
1377
|
+
)]
|
|
1378
|
+
},
|
|
1379
|
+
...options.confirmed !== void 0 && { confirmed: options.confirmed === "true" }
|
|
1380
|
+
};
|
|
1381
|
+
const validationResult = LessonSchema.safeParse(updatedLesson);
|
|
1382
|
+
if (!validationResult.success) {
|
|
1383
|
+
if (options.json) {
|
|
1384
|
+
console.log(JSON.stringify({ error: `Schema validation failed: ${validationResult.error.message}` }));
|
|
1385
|
+
} else {
|
|
1386
|
+
out.error(`Schema validation failed: ${validationResult.error.message}`);
|
|
1387
|
+
}
|
|
1388
|
+
process.exit(1);
|
|
1389
|
+
}
|
|
1390
|
+
await appendLesson(repoRoot, updatedLesson);
|
|
1391
|
+
await syncIfNeeded(repoRoot);
|
|
1392
|
+
if (options.json) {
|
|
1393
|
+
console.log(JSON.stringify(updatedLesson, null, SHOW_JSON_INDENT));
|
|
1394
|
+
} else {
|
|
1395
|
+
out.success(`Updated lesson ${id}`);
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
program2.command("delete <ids...>").description("Soft delete lessons (creates tombstone)").option("--json", "Output as JSON").action(async (ids, options) => {
|
|
1399
|
+
const repoRoot = getRepoRoot();
|
|
1400
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1401
|
+
const lessonMap = new Map(lessons.map((l) => [l.id, l]));
|
|
1402
|
+
const deleted = [];
|
|
1403
|
+
const warnings = [];
|
|
1404
|
+
for (const id of ids) {
|
|
1405
|
+
const lesson = lessonMap.get(id);
|
|
1406
|
+
if (!lesson) {
|
|
1407
|
+
const wasDeleted = await wasLessonDeleted(repoRoot, id);
|
|
1408
|
+
warnings.push({ id, message: wasDeleted ? "already deleted" : "not found" });
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
const tombstone = {
|
|
1412
|
+
...lesson,
|
|
1413
|
+
deleted: true,
|
|
1414
|
+
deletedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1415
|
+
};
|
|
1416
|
+
await appendLesson(repoRoot, tombstone);
|
|
1417
|
+
deleted.push(id);
|
|
1418
|
+
}
|
|
1419
|
+
if (deleted.length > 0) {
|
|
1420
|
+
await syncIfNeeded(repoRoot);
|
|
1421
|
+
}
|
|
1422
|
+
if (options.json) {
|
|
1423
|
+
console.log(JSON.stringify({ deleted, warnings }));
|
|
1424
|
+
} else {
|
|
1425
|
+
if (deleted.length > 0) {
|
|
1426
|
+
out.success(`Deleted ${deleted.length} lesson(s): ${deleted.join(", ")}`);
|
|
1427
|
+
}
|
|
1428
|
+
for (const warning of warnings) {
|
|
1429
|
+
out.warn(`${warning.id}: ${warning.message}`);
|
|
1430
|
+
}
|
|
1431
|
+
if (deleted.length === 0 && warnings.length > 0) {
|
|
1432
|
+
process.exit(1);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
function formatLessonHuman(lesson) {
|
|
1438
|
+
const lines = [];
|
|
1439
|
+
lines.push(`ID: ${lesson.id}`);
|
|
1440
|
+
lines.push(`Type: ${lesson.type}`);
|
|
1441
|
+
lines.push(`Trigger: ${lesson.trigger}`);
|
|
1442
|
+
lines.push(`Insight: ${lesson.insight}`);
|
|
1443
|
+
if (lesson.evidence) {
|
|
1444
|
+
lines.push(`Evidence: ${lesson.evidence}`);
|
|
1445
|
+
}
|
|
1446
|
+
if (lesson.severity) {
|
|
1447
|
+
lines.push(`Severity: ${lesson.severity}`);
|
|
1448
|
+
}
|
|
1449
|
+
lines.push(`Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
|
|
1450
|
+
lines.push(`Source: ${lesson.source}`);
|
|
1451
|
+
if (lesson.context) {
|
|
1452
|
+
lines.push(`Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
1453
|
+
}
|
|
1454
|
+
lines.push(`Created: ${lesson.created}`);
|
|
1455
|
+
lines.push(`Confirmed: ${lesson.confirmed ? "yes" : "no"}`);
|
|
1456
|
+
if (lesson.supersedes && lesson.supersedes.length > 0) {
|
|
1457
|
+
lines.push(`Supersedes: ${lesson.supersedes.join(", ")}`);
|
|
1458
|
+
}
|
|
1459
|
+
if (lesson.related && lesson.related.length > 0) {
|
|
1460
|
+
lines.push(`Related: ${lesson.related.join(", ")}`);
|
|
1461
|
+
}
|
|
1462
|
+
if (lesson.pattern) {
|
|
1463
|
+
lines.push("Pattern:");
|
|
1464
|
+
lines.push(` Bad: ${lesson.pattern.bad}`);
|
|
1465
|
+
lines.push(` Good: ${lesson.pattern.good}`);
|
|
1466
|
+
}
|
|
1467
|
+
return lines.join("\n");
|
|
1468
|
+
}
|
|
1469
|
+
async function wasLessonDeleted(repoRoot, id) {
|
|
1470
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
1471
|
+
try {
|
|
1472
|
+
const content = await readFile(filePath, "utf-8");
|
|
1473
|
+
const lines = content.split("\n");
|
|
1474
|
+
for (const line of lines) {
|
|
1475
|
+
const trimmed = line.trim();
|
|
1476
|
+
if (!trimmed) continue;
|
|
1477
|
+
try {
|
|
1478
|
+
const record = JSON.parse(trimmed);
|
|
1479
|
+
if (record.id === id && record.deleted === true) {
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
} catch {
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
} catch {
|
|
1486
|
+
}
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
|
|
1490
|
+
var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
|
|
1491
|
+
var DEFAULT_MODEL_DIR = join(homedir(), ".node-llama-cpp", "models");
|
|
1492
|
+
function isModelAvailable() {
|
|
1493
|
+
return existsSync(join(DEFAULT_MODEL_DIR, MODEL_FILENAME));
|
|
1494
|
+
}
|
|
1495
|
+
async function resolveModel(options = {}) {
|
|
1496
|
+
const { cli = true } = options;
|
|
1497
|
+
return resolveModelFile(MODEL_URI, { cli });
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/embeddings/nomic.ts
|
|
1501
|
+
var embeddingContext = null;
|
|
1502
|
+
async function getEmbedding() {
|
|
1503
|
+
if (embeddingContext) return embeddingContext;
|
|
1504
|
+
const modelPath = await resolveModel({ cli: true });
|
|
1505
|
+
const llama = await getLlama();
|
|
1506
|
+
const model = await llama.loadModel({ modelPath });
|
|
1507
|
+
embeddingContext = await model.createEmbeddingContext();
|
|
1508
|
+
return embeddingContext;
|
|
1509
|
+
}
|
|
1510
|
+
async function embedText(text) {
|
|
1511
|
+
const ctx = await getEmbedding();
|
|
1512
|
+
const result = await ctx.getEmbeddingFor(text);
|
|
1513
|
+
return Array.from(result.vector);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/search/vector.ts
|
|
1517
|
+
function cosineSimilarity(a, b) {
|
|
1518
|
+
if (a.length !== b.length) {
|
|
1519
|
+
throw new Error("Vectors must have same length");
|
|
1520
|
+
}
|
|
1521
|
+
let dotProduct = 0;
|
|
1522
|
+
let normA = 0;
|
|
1523
|
+
let normB = 0;
|
|
1524
|
+
for (let i = 0; i < a.length; i++) {
|
|
1525
|
+
dotProduct += a[i] * b[i];
|
|
1526
|
+
normA += a[i] * a[i];
|
|
1527
|
+
normB += b[i] * b[i];
|
|
1528
|
+
}
|
|
1529
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1530
|
+
if (magnitude === 0) return 0;
|
|
1531
|
+
return dotProduct / magnitude;
|
|
1532
|
+
}
|
|
1533
|
+
var DEFAULT_LIMIT = 10;
|
|
1534
|
+
async function searchVector(repoRoot, query, options) {
|
|
1535
|
+
const limit = options?.limit ?? DEFAULT_LIMIT;
|
|
1536
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1537
|
+
if (lessons.length === 0) return [];
|
|
1538
|
+
const queryVector = await embedText(query);
|
|
1539
|
+
const scored = [];
|
|
1540
|
+
for (const lesson of lessons) {
|
|
1541
|
+
if (lesson.invalidatedAt) continue;
|
|
1542
|
+
const lessonText = `${lesson.trigger} ${lesson.insight}`;
|
|
1543
|
+
const hash = contentHash(lesson.trigger, lesson.insight);
|
|
1544
|
+
let lessonVector = getCachedEmbedding(repoRoot, lesson.id, hash);
|
|
1545
|
+
if (!lessonVector) {
|
|
1546
|
+
lessonVector = await embedText(lessonText);
|
|
1547
|
+
setCachedEmbedding(repoRoot, lesson.id, lessonVector, hash);
|
|
1548
|
+
}
|
|
1549
|
+
const score = cosineSimilarity(queryVector, lessonVector);
|
|
1550
|
+
scored.push({ lesson, score });
|
|
1551
|
+
}
|
|
1552
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1553
|
+
return scored.slice(0, limit);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/search/ranking.ts
|
|
1557
|
+
var RECENCY_THRESHOLD_DAYS = 30;
|
|
1558
|
+
var HIGH_SEVERITY_BOOST = 1.5;
|
|
1559
|
+
var MEDIUM_SEVERITY_BOOST = 1;
|
|
1560
|
+
var LOW_SEVERITY_BOOST = 0.8;
|
|
1561
|
+
var RECENCY_BOOST = 1.2;
|
|
1562
|
+
var CONFIRMATION_BOOST = 1.3;
|
|
1563
|
+
function severityBoost(lesson) {
|
|
1564
|
+
switch (lesson.severity) {
|
|
1565
|
+
case "high":
|
|
1566
|
+
return HIGH_SEVERITY_BOOST;
|
|
1567
|
+
case "medium":
|
|
1568
|
+
return MEDIUM_SEVERITY_BOOST;
|
|
1569
|
+
case "low":
|
|
1570
|
+
return LOW_SEVERITY_BOOST;
|
|
1571
|
+
default:
|
|
1572
|
+
return MEDIUM_SEVERITY_BOOST;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function recencyBoost(lesson) {
|
|
1576
|
+
const ageDays = getLessonAgeDays(lesson);
|
|
948
1577
|
return ageDays <= RECENCY_THRESHOLD_DAYS ? RECENCY_BOOST : 1;
|
|
949
1578
|
}
|
|
950
1579
|
function confirmationBoost(lesson) {
|
|
@@ -960,173 +1589,6 @@ function rankLessons(lessons) {
|
|
|
960
1589
|
})).sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
|
|
961
1590
|
}
|
|
962
1591
|
|
|
963
|
-
// src/capture/quality.ts
|
|
964
|
-
var DEFAULT_SIMILARITY_THRESHOLD = 0.8;
|
|
965
|
-
async function isNovel(repoRoot, insight, options = {}) {
|
|
966
|
-
const threshold = options.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
967
|
-
await syncIfNeeded(repoRoot);
|
|
968
|
-
const words = insight.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3).slice(0, 3);
|
|
969
|
-
if (words.length === 0) {
|
|
970
|
-
return { novel: true };
|
|
971
|
-
}
|
|
972
|
-
const searchQuery = words.join(" OR ");
|
|
973
|
-
const results = await searchKeyword(repoRoot, searchQuery, 10);
|
|
974
|
-
if (results.length === 0) {
|
|
975
|
-
return { novel: true };
|
|
976
|
-
}
|
|
977
|
-
const insightWords = new Set(insight.toLowerCase().split(/\s+/));
|
|
978
|
-
for (const lesson of results) {
|
|
979
|
-
const lessonWords = new Set(lesson.insight.toLowerCase().split(/\s+/));
|
|
980
|
-
const intersection = [...insightWords].filter((w) => lessonWords.has(w)).length;
|
|
981
|
-
const union = (/* @__PURE__ */ new Set([...insightWords, ...lessonWords])).size;
|
|
982
|
-
const similarity = union > 0 ? intersection / union : 0;
|
|
983
|
-
if (similarity >= threshold) {
|
|
984
|
-
return {
|
|
985
|
-
novel: false,
|
|
986
|
-
reason: `Found similar existing lesson: "${lesson.insight.slice(0, 50)}..."`,
|
|
987
|
-
existingId: lesson.id
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
|
|
991
|
-
return {
|
|
992
|
-
novel: false,
|
|
993
|
-
reason: `Exact duplicate found`,
|
|
994
|
-
existingId: lesson.id
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
return { novel: true };
|
|
999
|
-
}
|
|
1000
|
-
var MIN_WORD_COUNT = 4;
|
|
1001
|
-
var VAGUE_PATTERNS = [
|
|
1002
|
-
/\bwrite better\b/i,
|
|
1003
|
-
/\bbe careful\b/i,
|
|
1004
|
-
/\bremember to\b/i,
|
|
1005
|
-
/\bmake sure\b/i,
|
|
1006
|
-
/\btry to\b/i,
|
|
1007
|
-
/\bdouble check\b/i
|
|
1008
|
-
];
|
|
1009
|
-
var GENERIC_IMPERATIVE_PATTERN = /^(always|never)\s+\w+(\s+\w+){0,2}$/i;
|
|
1010
|
-
function isSpecific(insight) {
|
|
1011
|
-
const words = insight.trim().split(/\s+/).filter((w) => w.length > 0);
|
|
1012
|
-
if (words.length < MIN_WORD_COUNT) {
|
|
1013
|
-
return { specific: false, reason: "Insight is too short to be actionable" };
|
|
1014
|
-
}
|
|
1015
|
-
for (const pattern of VAGUE_PATTERNS) {
|
|
1016
|
-
if (pattern.test(insight)) {
|
|
1017
|
-
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
if (GENERIC_IMPERATIVE_PATTERN.test(insight)) {
|
|
1021
|
-
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
1022
|
-
}
|
|
1023
|
-
return { specific: true };
|
|
1024
|
-
}
|
|
1025
|
-
var ACTION_PATTERNS = [
|
|
1026
|
-
/\buse\s+.+\s+instead\s+of\b/i,
|
|
1027
|
-
// "use X instead of Y"
|
|
1028
|
-
/\bprefer\s+.+\s+(over|to)\b/i,
|
|
1029
|
-
// "prefer X over Y" or "prefer X to Y"
|
|
1030
|
-
/\balways\s+.+\s+when\b/i,
|
|
1031
|
-
// "always X when Y"
|
|
1032
|
-
/\bnever\s+.+\s+without\b/i,
|
|
1033
|
-
// "never X without Y"
|
|
1034
|
-
/\bavoid\s+(using\s+)?\w+/i,
|
|
1035
|
-
// "avoid X" or "avoid using X"
|
|
1036
|
-
/\bcheck\s+.+\s+before\b/i,
|
|
1037
|
-
// "check X before Y"
|
|
1038
|
-
/^(run|use|add|remove|install|update|configure|set|enable|disable)\s+/i
|
|
1039
|
-
// Imperative commands at start
|
|
1040
|
-
];
|
|
1041
|
-
function isActionable(insight) {
|
|
1042
|
-
for (const pattern of ACTION_PATTERNS) {
|
|
1043
|
-
if (pattern.test(insight)) {
|
|
1044
|
-
return { actionable: true };
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
return { actionable: false, reason: "Insight lacks clear action guidance" };
|
|
1048
|
-
}
|
|
1049
|
-
async function shouldPropose(repoRoot, insight) {
|
|
1050
|
-
const specificResult = isSpecific(insight);
|
|
1051
|
-
if (!specificResult.specific) {
|
|
1052
|
-
return { shouldPropose: false, reason: specificResult.reason };
|
|
1053
|
-
}
|
|
1054
|
-
const actionableResult = isActionable(insight);
|
|
1055
|
-
if (!actionableResult.actionable) {
|
|
1056
|
-
return { shouldPropose: false, reason: actionableResult.reason };
|
|
1057
|
-
}
|
|
1058
|
-
const noveltyResult = await isNovel(repoRoot, insight);
|
|
1059
|
-
if (!noveltyResult.novel) {
|
|
1060
|
-
return { shouldPropose: false, reason: noveltyResult.reason };
|
|
1061
|
-
}
|
|
1062
|
-
return { shouldPropose: true };
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// src/capture/triggers.ts
|
|
1066
|
-
var USER_CORRECTION_PATTERNS = [
|
|
1067
|
-
/\bno\b[,.]?\s/i,
|
|
1068
|
-
// "no, ..." or "no ..."
|
|
1069
|
-
/\bwrong\b/i,
|
|
1070
|
-
// "wrong"
|
|
1071
|
-
/\bactually\b/i,
|
|
1072
|
-
// "actually..."
|
|
1073
|
-
/\bnot that\b/i,
|
|
1074
|
-
// "not that"
|
|
1075
|
-
/\bi meant\b/i
|
|
1076
|
-
// "I meant"
|
|
1077
|
-
];
|
|
1078
|
-
function detectUserCorrection(signals) {
|
|
1079
|
-
const { messages, context } = signals;
|
|
1080
|
-
if (messages.length < 2) {
|
|
1081
|
-
return null;
|
|
1082
|
-
}
|
|
1083
|
-
for (let i = 1; i < messages.length; i++) {
|
|
1084
|
-
const message = messages[i];
|
|
1085
|
-
if (!message) continue;
|
|
1086
|
-
for (const pattern of USER_CORRECTION_PATTERNS) {
|
|
1087
|
-
if (pattern.test(message)) {
|
|
1088
|
-
return {
|
|
1089
|
-
trigger: `User correction during ${context.intent}`,
|
|
1090
|
-
correctionMessage: message,
|
|
1091
|
-
context
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
return null;
|
|
1097
|
-
}
|
|
1098
|
-
function detectSelfCorrection(history) {
|
|
1099
|
-
const { edits } = history;
|
|
1100
|
-
if (edits.length < 3) {
|
|
1101
|
-
return null;
|
|
1102
|
-
}
|
|
1103
|
-
for (let i = 0; i <= edits.length - 3; i++) {
|
|
1104
|
-
const first = edits[i];
|
|
1105
|
-
const second = edits[i + 1];
|
|
1106
|
-
const third = edits[i + 2];
|
|
1107
|
-
if (!first || !second || !third) continue;
|
|
1108
|
-
if (first.file === second.file && second.file === third.file && first.success && !second.success && third.success) {
|
|
1109
|
-
return {
|
|
1110
|
-
file: first.file,
|
|
1111
|
-
trigger: `Self-correction on ${first.file}`
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
return null;
|
|
1116
|
-
}
|
|
1117
|
-
function detectTestFailure(testResult) {
|
|
1118
|
-
if (testResult.passed) {
|
|
1119
|
-
return null;
|
|
1120
|
-
}
|
|
1121
|
-
const lines = testResult.output.split("\n").filter((line) => line.trim().length > 0);
|
|
1122
|
-
const errorLine = lines.find((line) => /error|fail|assert/i.test(line)) ?? lines[0] ?? "";
|
|
1123
|
-
return {
|
|
1124
|
-
testFile: testResult.testFile,
|
|
1125
|
-
errorOutput: testResult.output,
|
|
1126
|
-
trigger: `Test failure in ${testResult.testFile}: ${errorLine.slice(0, 100)}`
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
1592
|
// src/retrieval/session.ts
|
|
1131
1593
|
var DEFAULT_LIMIT2 = 5;
|
|
1132
1594
|
function isFullLesson(lesson) {
|
|
@@ -1135,7 +1597,7 @@ function isFullLesson(lesson) {
|
|
|
1135
1597
|
async function loadSessionLessons(repoRoot, limit = DEFAULT_LIMIT2) {
|
|
1136
1598
|
const { lessons: allLessons } = await readLessons(repoRoot);
|
|
1137
1599
|
const highSeverityLessons = allLessons.filter(
|
|
1138
|
-
(lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed
|
|
1600
|
+
(lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed && !lesson.invalidatedAt
|
|
1139
1601
|
);
|
|
1140
1602
|
highSeverityLessons.sort((a, b) => {
|
|
1141
1603
|
const dateA = new Date(a.created).getTime();
|
|
@@ -1172,527 +1634,238 @@ ${lessonLines.join("\n")}`;
|
|
|
1172
1634
|
// src/index.ts
|
|
1173
1635
|
var VERSION = "0.1.0";
|
|
1174
1636
|
|
|
1175
|
-
// src/
|
|
1176
|
-
function
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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)}`);
|
|
1637
|
+
// src/commands/retrieval.ts
|
|
1638
|
+
async function readPlanFromStdin() {
|
|
1639
|
+
const { stdin } = await import('process');
|
|
1640
|
+
if (!stdin.isTTY) {
|
|
1641
|
+
const chunks = [];
|
|
1642
|
+
for await (const chunk of stdin) {
|
|
1643
|
+
chunks.push(chunk);
|
|
1203
1644
|
}
|
|
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}`);
|
|
1645
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
1223
1646
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1647
|
+
return void 0;
|
|
1648
|
+
}
|
|
1649
|
+
function outputCheckPlanJson(lessons) {
|
|
1650
|
+
const jsonOutput = {
|
|
1651
|
+
lessons: lessons.map((l) => ({
|
|
1652
|
+
id: l.lesson.id,
|
|
1653
|
+
insight: l.lesson.insight,
|
|
1654
|
+
relevance: l.score,
|
|
1655
|
+
source: l.lesson.source
|
|
1656
|
+
})),
|
|
1657
|
+
count: lessons.length
|
|
1658
|
+
};
|
|
1659
|
+
console.log(JSON.stringify(jsonOutput));
|
|
1660
|
+
}
|
|
1661
|
+
function outputCheckPlanHuman(lessons, quiet) {
|
|
1662
|
+
console.log("## Lessons Check\n");
|
|
1663
|
+
console.log("Relevant to your plan:\n");
|
|
1664
|
+
lessons.forEach((item, i) => {
|
|
1665
|
+
const num = i + 1;
|
|
1666
|
+
console.log(`${num}. ${chalk.bold(`[${item.lesson.id}]`)} ${item.lesson.insight}`);
|
|
1667
|
+
console.log(` - Relevance: ${item.score.toFixed(RELEVANCE_DECIMAL_PLACES)}`);
|
|
1668
|
+
console.log(` - Source: ${item.lesson.source}`);
|
|
1669
|
+
console.log();
|
|
1670
|
+
});
|
|
1671
|
+
if (!quiet) {
|
|
1672
|
+
console.log("---");
|
|
1673
|
+
console.log("Consider these lessons while implementing.");
|
|
1228
1674
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1675
|
+
}
|
|
1676
|
+
function formatSource(source) {
|
|
1677
|
+
return source.replace(/_/g, " ");
|
|
1678
|
+
}
|
|
1679
|
+
function outputSessionLessonsHuman(lessons, quiet) {
|
|
1680
|
+
console.log("## Lessons from Past Sessions\n");
|
|
1681
|
+
console.log("These lessons were captured from previous corrections and should inform your work:\n");
|
|
1682
|
+
lessons.forEach((lesson, i) => {
|
|
1683
|
+
const num = i + 1;
|
|
1684
|
+
const date = lesson.created.slice(0, ISO_DATE_PREFIX_LENGTH);
|
|
1685
|
+
const tagsDisplay = lesson.tags.length > 0 ? ` (${lesson.tags.join(", ")})` : "";
|
|
1686
|
+
console.log(`${num}. **${lesson.insight}**${tagsDisplay}`);
|
|
1687
|
+
console.log(` Learned: ${date} via ${formatSource(lesson.source)}`);
|
|
1688
|
+
console.log();
|
|
1689
|
+
});
|
|
1690
|
+
if (!quiet) {
|
|
1691
|
+
console.log("Consider these lessons when planning and implementing tasks.");
|
|
1231
1692
|
}
|
|
1232
|
-
return lines.join("\n");
|
|
1233
1693
|
}
|
|
1234
|
-
function
|
|
1235
|
-
program2.command("
|
|
1694
|
+
function registerRetrievalCommands(program2) {
|
|
1695
|
+
program2.command("search <query>").description("Search lessons by keyword").option("-n, --limit <number>", "Maximum results", DEFAULT_SEARCH_LIMIT).action(async function(query, options) {
|
|
1236
1696
|
const repoRoot = getRepoRoot();
|
|
1237
|
-
const
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
} catch {
|
|
1697
|
+
const limit = parseLimit(options.limit, "limit");
|
|
1698
|
+
const { verbose, quiet } = getGlobalOpts(this);
|
|
1699
|
+
await syncIfNeeded(repoRoot);
|
|
1700
|
+
const results = await searchKeyword(repoRoot, query, limit);
|
|
1701
|
+
if (results.length === 0) {
|
|
1702
|
+
console.log('No lessons match your search. Try a different query or use "list" to see all lessons.');
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (!quiet) {
|
|
1706
|
+
out.info(`Found ${results.length} lesson(s):
|
|
1707
|
+
`);
|
|
1708
|
+
}
|
|
1709
|
+
for (const lesson of results) {
|
|
1710
|
+
console.log(`[${chalk.cyan(lesson.id)}] ${lesson.insight}`);
|
|
1711
|
+
console.log(` Trigger: ${lesson.trigger}`);
|
|
1712
|
+
if (verbose && lesson.context) {
|
|
1713
|
+
console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
1714
|
+
console.log(` Created: ${lesson.created}`);
|
|
1258
1715
|
}
|
|
1259
|
-
if (
|
|
1260
|
-
console.log(
|
|
1261
|
-
} else {
|
|
1262
|
-
out.error(wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found`);
|
|
1716
|
+
if (lesson.tags.length > 0) {
|
|
1717
|
+
console.log(` Tags: ${lesson.tags.join(", ")}`);
|
|
1263
1718
|
}
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1266
|
-
if (options.json) {
|
|
1267
|
-
console.log(JSON.stringify(lesson, null, SHOW_JSON_INDENT));
|
|
1268
|
-
} else {
|
|
1269
|
-
console.log(formatLessonHuman(lesson));
|
|
1719
|
+
console.log();
|
|
1270
1720
|
}
|
|
1271
1721
|
});
|
|
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) => {
|
|
1722
|
+
program2.command("list").description("List all lessons").option("-n, --limit <number>", "Maximum results", DEFAULT_LIST_LIMIT).option("--invalidated", "Show only invalidated lessons").action(async function(options) {
|
|
1276
1723
|
const repoRoot = getRepoRoot();
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1724
|
+
const limit = parseLimit(options.limit, "limit");
|
|
1725
|
+
const { verbose, quiet } = getGlobalOpts(this);
|
|
1726
|
+
const { lessons, skippedCount } = await readLessons(repoRoot);
|
|
1727
|
+
const filteredLessons = options.invalidated ? lessons.filter((l) => l.invalidatedAt) : lessons;
|
|
1728
|
+
if (filteredLessons.length === 0) {
|
|
1729
|
+
if (options.invalidated) {
|
|
1730
|
+
console.log("No invalidated lessons found.");
|
|
1281
1731
|
} else {
|
|
1282
|
-
|
|
1732
|
+
console.log('No lessons found. Get started with: learn "Your first lesson"');
|
|
1283
1733
|
}
|
|
1284
|
-
|
|
1734
|
+
if (skippedCount > 0) {
|
|
1735
|
+
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
1736
|
+
}
|
|
1737
|
+
return;
|
|
1285
1738
|
}
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1739
|
+
const toShow = filteredLessons.slice(0, limit);
|
|
1740
|
+
if (!quiet) {
|
|
1741
|
+
const label = options.invalidated ? "invalidated lesson(s)" : "lesson(s)";
|
|
1742
|
+
out.info(`Showing ${toShow.length} of ${filteredLessons.length} ${label}:
|
|
1743
|
+
`);
|
|
1744
|
+
}
|
|
1745
|
+
for (const lesson of toShow) {
|
|
1746
|
+
const invalidMarker = lesson.invalidatedAt ? chalk.red("[INVALID] ") : "";
|
|
1747
|
+
console.log(`[${chalk.cyan(lesson.id)}] ${invalidMarker}${lesson.insight}`);
|
|
1748
|
+
if (verbose) {
|
|
1749
|
+
console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
|
|
1750
|
+
console.log(` Created: ${lesson.created}`);
|
|
1751
|
+
if (lesson.context) {
|
|
1752
|
+
console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
1753
|
+
}
|
|
1754
|
+
if (lesson.invalidatedAt) {
|
|
1755
|
+
console.log(` Invalidated: ${lesson.invalidatedAt}`);
|
|
1756
|
+
if (lesson.invalidationReason) {
|
|
1757
|
+
console.log(` Reason: ${lesson.invalidationReason}`);
|
|
1304
1758
|
}
|
|
1305
1759
|
}
|
|
1306
|
-
} catch {
|
|
1307
|
-
}
|
|
1308
|
-
if (options.json) {
|
|
1309
|
-
console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found` }));
|
|
1310
1760
|
} else {
|
|
1311
|
-
|
|
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);
|
|
1761
|
+
console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
|
|
1324
1762
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
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}`);
|
|
1763
|
+
if (lesson.tags.length > 0) {
|
|
1764
|
+
console.log(` Tags: ${lesson.tags.join(", ")}`);
|
|
1345
1765
|
}
|
|
1346
|
-
|
|
1766
|
+
console.log();
|
|
1347
1767
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
if (options.json) {
|
|
1351
|
-
console.log(JSON.stringify(updatedLesson, null, SHOW_JSON_INDENT2));
|
|
1352
|
-
} else {
|
|
1353
|
-
out.success(`Updated lesson ${id}`);
|
|
1768
|
+
if (skippedCount > 0) {
|
|
1769
|
+
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
1354
1770
|
}
|
|
1355
1771
|
});
|
|
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) => {
|
|
1772
|
+
program2.command("load-session").description("Load high-severity lessons for session context").option("--json", "Output as JSON").action(async function(options) {
|
|
1379
1773
|
const repoRoot = getRepoRoot();
|
|
1380
|
-
const {
|
|
1381
|
-
const
|
|
1382
|
-
const
|
|
1383
|
-
const
|
|
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
|
-
}
|
|
1774
|
+
const { quiet } = getGlobalOpts(this);
|
|
1775
|
+
const lessons = await loadSessionLessons(repoRoot);
|
|
1776
|
+
const { lessons: allLessons } = await readLessons(repoRoot);
|
|
1777
|
+
const totalCount = allLessons.length;
|
|
1402
1778
|
if (options.json) {
|
|
1403
|
-
console.log(JSON.stringify({
|
|
1404
|
-
|
|
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
|
-
}
|
|
1779
|
+
console.log(JSON.stringify({ lessons, count: lessons.length, totalCount }));
|
|
1780
|
+
return;
|
|
1414
1781
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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;
|
|
1782
|
+
if (lessons.length === 0) {
|
|
1783
|
+
console.log("No high-severity lessons found.");
|
|
1784
|
+
return;
|
|
1429
1785
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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)}`);
|
|
1786
|
+
outputSessionLessonsHuman(lessons, quiet);
|
|
1787
|
+
if (totalCount > LESSON_COUNT_WARNING_THRESHOLD) {
|
|
1788
|
+
console.log("");
|
|
1789
|
+
out.info(`${totalCount} lessons in index. Consider \`lna compact\` to reduce context pollution.`);
|
|
1453
1790
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
function detectSelfCorrectionFlow(data) {
|
|
1490
|
-
const result = detectSelfCorrection(data);
|
|
1491
|
-
if (!result) {
|
|
1492
|
-
return null;
|
|
1493
|
-
}
|
|
1494
|
-
return {
|
|
1495
|
-
trigger: result.trigger,
|
|
1496
|
-
source: "self_correction",
|
|
1497
|
-
// Self-corrections need context to form useful insights
|
|
1498
|
-
proposedInsight: `Check ${result.file} for common errors before editing`
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
function detectTestFailureFlow(data) {
|
|
1502
|
-
const result = detectTestFailure(data);
|
|
1503
|
-
if (!result) {
|
|
1504
|
-
return null;
|
|
1505
|
-
}
|
|
1506
|
-
return {
|
|
1507
|
-
trigger: result.trigger,
|
|
1508
|
-
source: "test_failure",
|
|
1509
|
-
proposedInsight: result.errorOutput
|
|
1510
|
-
};
|
|
1511
|
-
}
|
|
1512
|
-
var VALID_TYPES = /* @__PURE__ */ new Set(["user", "self", "test"]);
|
|
1513
|
-
async function parseInputFile(filePath) {
|
|
1514
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
1515
|
-
const data = JSON.parse(content);
|
|
1516
|
-
if (!VALID_TYPES.has(data.type)) {
|
|
1517
|
-
throw new Error(`Invalid detection type: ${data.type}. Must be one of: user, self, test`);
|
|
1518
|
-
}
|
|
1519
|
-
return data;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
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
|
-
}
|
|
1791
|
+
const oldLessons = lessons.filter((l) => getLessonAgeDays(l) > AGE_FLAG_THRESHOLD_DAYS);
|
|
1792
|
+
if (oldLessons.length > 0) {
|
|
1793
|
+
console.log("");
|
|
1794
|
+
out.warn(`${oldLessons.length} lesson(s) are over ${AGE_FLAG_THRESHOLD_DAYS} days old. Review for continued validity.`);
|
|
1573
1795
|
}
|
|
1574
|
-
);
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
// src/cli/commands/capture.ts
|
|
1578
|
-
function createLessonFromFlags(trigger, insight, confirmed) {
|
|
1579
|
-
return {
|
|
1580
|
-
id: generateId(insight),
|
|
1581
|
-
type: "quick",
|
|
1582
|
-
trigger,
|
|
1583
|
-
insight,
|
|
1584
|
-
tags: [],
|
|
1585
|
-
source: "manual",
|
|
1586
|
-
context: { tool: "capture", intent: "manual capture" },
|
|
1587
|
-
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1588
|
-
confirmed,
|
|
1589
|
-
supersedes: [],
|
|
1590
|
-
related: []
|
|
1591
|
-
};
|
|
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) {
|
|
1796
|
+
});
|
|
1797
|
+
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) {
|
|
1628
1798
|
const repoRoot = getRepoRoot();
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
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);
|
|
1799
|
+
const limit = parseLimit(options.limit, "limit");
|
|
1800
|
+
const { quiet } = getGlobalOpts(this);
|
|
1801
|
+
const planText = options.plan ?? await readPlanFromStdin();
|
|
1802
|
+
if (!planText) {
|
|
1803
|
+
out.error("No plan provided. Use --plan <text> or pipe text to stdin.");
|
|
1644
1804
|
process.exit(1);
|
|
1645
1805
|
}
|
|
1646
|
-
if (!
|
|
1806
|
+
if (!isModelAvailable()) {
|
|
1647
1807
|
if (options.json) {
|
|
1648
|
-
console.log(JSON.stringify({
|
|
1808
|
+
console.log(JSON.stringify({
|
|
1809
|
+
error: "Embedding model not available",
|
|
1810
|
+
action: "Run: npx learning-agent download-model"
|
|
1811
|
+
}));
|
|
1649
1812
|
} else {
|
|
1650
|
-
out.error("
|
|
1651
|
-
console.log(
|
|
1813
|
+
out.error("Embedding model not available");
|
|
1814
|
+
console.log("");
|
|
1815
|
+
console.log("Run: npx learning-agent download-model");
|
|
1652
1816
|
}
|
|
1653
1817
|
process.exit(1);
|
|
1654
1818
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
if (
|
|
1662
|
-
|
|
1663
|
-
|
|
1819
|
+
try {
|
|
1820
|
+
const result = await retrieveForPlan(repoRoot, planText, limit);
|
|
1821
|
+
if (options.json) {
|
|
1822
|
+
outputCheckPlanJson(result.lessons);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (result.lessons.length === 0) {
|
|
1826
|
+
console.log("No relevant lessons found for this plan.");
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
outputCheckPlanHuman(result.lessons, quiet);
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1832
|
+
if (options.json) {
|
|
1833
|
+
console.log(JSON.stringify({ error: message }));
|
|
1834
|
+
} else {
|
|
1835
|
+
out.error(`Failed to check plan: ${message}`);
|
|
1836
|
+
}
|
|
1837
|
+
process.exit(1);
|
|
1664
1838
|
}
|
|
1665
1839
|
});
|
|
1666
1840
|
}
|
|
1667
|
-
var
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
npx
|
|
1841
|
+
var PRE_COMMIT_MESSAGE = `Before committing, have you captured any valuable lessons from this session?
|
|
1842
|
+
Consider: corrections, mistakes, or insights worth remembering.
|
|
1843
|
+
|
|
1844
|
+
To capture a lesson:
|
|
1845
|
+
npx learning-agent capture --trigger "what happened" --insight "what to do" --yes`;
|
|
1846
|
+
var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
|
|
1847
|
+
# Learning Agent pre-commit hook
|
|
1848
|
+
# Reminds Claude to consider capturing lessons before commits
|
|
1849
|
+
|
|
1850
|
+
npx learning-agent hooks run pre-commit
|
|
1672
1851
|
`;
|
|
1852
|
+
var CLAUDE_HOOK_MARKER = "learning-agent load-session";
|
|
1853
|
+
var CLAUDE_HOOK_CONFIG = {
|
|
1854
|
+
matcher: "startup|resume|compact",
|
|
1855
|
+
hooks: [
|
|
1856
|
+
{
|
|
1857
|
+
type: "command",
|
|
1858
|
+
command: "npx learning-agent load-session 2>/dev/null || true"
|
|
1859
|
+
}
|
|
1860
|
+
]
|
|
1861
|
+
};
|
|
1862
|
+
var HOOK_MARKER = "# Learning Agent pre-commit hook";
|
|
1863
|
+
var LEARNING_AGENT_SECTION_HEADER = "## Learning Agent Integration";
|
|
1673
1864
|
var AGENTS_MD_TEMPLATE = `
|
|
1674
1865
|
## Learning Agent Integration
|
|
1675
1866
|
|
|
1676
1867
|
This project uses learning-agent for session memory.
|
|
1677
1868
|
|
|
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
1869
|
### Retrieval Points
|
|
1697
1870
|
|
|
1698
1871
|
- **Session start**: High-severity lessons loaded automatically
|
|
@@ -1703,7 +1876,7 @@ Manual edits will:
|
|
|
1703
1876
|
**BEFORE implementing any plan**, run:
|
|
1704
1877
|
|
|
1705
1878
|
\`\`\`bash
|
|
1706
|
-
npx
|
|
1879
|
+
npx learning-agent check-plan --plan "your plan description" --json
|
|
1707
1880
|
\`\`\`
|
|
1708
1881
|
|
|
1709
1882
|
Display results as a **Lessons Check** section after your plan:
|
|
@@ -1739,14 +1912,14 @@ Before closing a session, reflect on lessons learned:
|
|
|
1739
1912
|
1. **Review**: What mistakes or corrections happened?
|
|
1740
1913
|
2. **Quality gate**: Is it novel, specific, actionable?
|
|
1741
1914
|
3. **Propose**: "Learned: [insight]. Save? [y/n]"
|
|
1742
|
-
4. **Capture**: \`npx
|
|
1915
|
+
4. **Capture**: \`npx learning-agent capture --trigger "..." --insight "..." --yes\`
|
|
1743
1916
|
|
|
1744
1917
|
### CLI Commands
|
|
1745
1918
|
|
|
1746
1919
|
\`\`\`bash
|
|
1747
|
-
npx
|
|
1748
|
-
npx
|
|
1749
|
-
npx
|
|
1920
|
+
npx learning-agent load-session --json # Session start
|
|
1921
|
+
npx learning-agent check-plan --plan "..." --json # Before implementing
|
|
1922
|
+
npx learning-agent capture --trigger "..." --insight "..." --yes
|
|
1750
1923
|
\`\`\`
|
|
1751
1924
|
|
|
1752
1925
|
See [AGENTS.md](https://github.com/Nathandela/learning_agent/blob/main/AGENTS.md) for full documentation.
|
|
@@ -1779,6 +1952,7 @@ async function updateAgentsMd(repoRoot) {
|
|
|
1779
1952
|
await writeFile(agentsPath, newContent, "utf-8");
|
|
1780
1953
|
return true;
|
|
1781
1954
|
}
|
|
1955
|
+
var HOOK_FILE_MODE = 493;
|
|
1782
1956
|
function hasLearningAgentHook(content) {
|
|
1783
1957
|
return content.includes(HOOK_MARKER);
|
|
1784
1958
|
}
|
|
@@ -1799,6 +1973,10 @@ async function getGitHooksDir(repoRoot) {
|
|
|
1799
1973
|
const defaultHooksDir = join(gitDir, "hooks");
|
|
1800
1974
|
return existsSync(defaultHooksDir) ? defaultHooksDir : null;
|
|
1801
1975
|
}
|
|
1976
|
+
var LEARNING_AGENT_HOOK_BLOCK = `
|
|
1977
|
+
# Learning Agent pre-commit hook (appended)
|
|
1978
|
+
npx learning-agent hooks run pre-commit
|
|
1979
|
+
`;
|
|
1802
1980
|
function findFirstTopLevelExitLine(lines) {
|
|
1803
1981
|
let insideFunction = 0;
|
|
1804
1982
|
let heredocDelimiter = null;
|
|
@@ -1859,8 +2037,57 @@ async function installPreCommitHook(repoRoot) {
|
|
|
1859
2037
|
chmodSync(hookPath, HOOK_FILE_MODE);
|
|
1860
2038
|
return true;
|
|
1861
2039
|
}
|
|
1862
|
-
function
|
|
1863
|
-
|
|
2040
|
+
function getClaudeSettingsPath(global) {
|
|
2041
|
+
if (global) {
|
|
2042
|
+
return join(homedir(), ".claude", "settings.json");
|
|
2043
|
+
}
|
|
2044
|
+
const repoRoot = getRepoRoot();
|
|
2045
|
+
return join(repoRoot, ".claude", "settings.json");
|
|
2046
|
+
}
|
|
2047
|
+
async function readClaudeSettings(settingsPath) {
|
|
2048
|
+
if (!existsSync(settingsPath)) {
|
|
2049
|
+
return {};
|
|
2050
|
+
}
|
|
2051
|
+
const content = await readFile(settingsPath, "utf-8");
|
|
2052
|
+
return JSON.parse(content);
|
|
2053
|
+
}
|
|
2054
|
+
function hasClaudeHook(settings) {
|
|
2055
|
+
const hooks = settings.hooks;
|
|
2056
|
+
if (!hooks?.SessionStart) return false;
|
|
2057
|
+
return hooks.SessionStart.some((entry) => {
|
|
2058
|
+
const hookEntry = entry;
|
|
2059
|
+
return hookEntry.hooks?.some((h) => h.command?.includes(CLAUDE_HOOK_MARKER));
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
function addLearningAgentHook(settings) {
|
|
2063
|
+
if (!settings.hooks) {
|
|
2064
|
+
settings.hooks = {};
|
|
2065
|
+
}
|
|
2066
|
+
const hooks = settings.hooks;
|
|
2067
|
+
if (!hooks.SessionStart) {
|
|
2068
|
+
hooks.SessionStart = [];
|
|
2069
|
+
}
|
|
2070
|
+
hooks.SessionStart.push(CLAUDE_HOOK_CONFIG);
|
|
2071
|
+
}
|
|
2072
|
+
function removeLearningAgentHook(settings) {
|
|
2073
|
+
const hooks = settings.hooks;
|
|
2074
|
+
if (!hooks?.SessionStart) return false;
|
|
2075
|
+
const originalLength = hooks.SessionStart.length;
|
|
2076
|
+
hooks.SessionStart = hooks.SessionStart.filter((entry) => {
|
|
2077
|
+
const hookEntry = entry;
|
|
2078
|
+
return !hookEntry.hooks?.some((h) => h.command?.includes(CLAUDE_HOOK_MARKER));
|
|
2079
|
+
});
|
|
2080
|
+
return hooks.SessionStart.length < originalLength;
|
|
2081
|
+
}
|
|
2082
|
+
async function writeClaudeSettings(settingsPath, settings) {
|
|
2083
|
+
const dir = dirname(settingsPath);
|
|
2084
|
+
await mkdir(dir, { recursive: true });
|
|
2085
|
+
const tempPath = settingsPath + ".tmp";
|
|
2086
|
+
await writeFile(tempPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2087
|
+
await rename(tempPath, settingsPath);
|
|
2088
|
+
}
|
|
2089
|
+
function registerSetupCommands(program2) {
|
|
2090
|
+
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("--json", "Output result as JSON").action(async function(options) {
|
|
1864
2091
|
const repoRoot = getRepoRoot();
|
|
1865
2092
|
const { quiet } = getGlobalOpts(this);
|
|
1866
2093
|
await createLessonsDirectory(repoRoot);
|
|
@@ -1874,28 +2101,12 @@ function registerInitCommand(program2) {
|
|
|
1874
2101
|
if (!options.skipHooks) {
|
|
1875
2102
|
hooksInstalled = await installPreCommitHook(repoRoot);
|
|
1876
2103
|
}
|
|
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
2104
|
if (options.json) {
|
|
1893
2105
|
console.log(JSON.stringify({
|
|
1894
2106
|
initialized: true,
|
|
1895
2107
|
lessonsDir,
|
|
1896
2108
|
agentsMd: agentsMdUpdated,
|
|
1897
|
-
hooks: hooksInstalled
|
|
1898
|
-
claudeHooks: claudeHooksInstalled
|
|
2109
|
+
hooks: hooksInstalled
|
|
1899
2110
|
}));
|
|
1900
2111
|
} else if (!quiet) {
|
|
1901
2112
|
out.success("Learning agent initialized");
|
|
@@ -1914,21 +2125,8 @@ function registerInitCommand(program2) {
|
|
|
1914
2125
|
} else {
|
|
1915
2126
|
console.log(" Git hooks: Already installed or not a git repo");
|
|
1916
2127
|
}
|
|
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
2128
|
}
|
|
1927
2129
|
});
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
// src/cli/commands/hooks.ts
|
|
1931
|
-
function registerHooksCommand(program2) {
|
|
1932
2130
|
const hooksCommand = program2.command("hooks").description("Git hooks management");
|
|
1933
2131
|
hooksCommand.command("run <hook>").description("Run a hook script (called by git hooks)").option("--json", "Output as JSON").action((hook, options) => {
|
|
1934
2132
|
if (hook === "pre-commit") {
|
|
@@ -1946,8 +2144,6 @@ function registerHooksCommand(program2) {
|
|
|
1946
2144
|
process.exit(1);
|
|
1947
2145
|
}
|
|
1948
2146
|
});
|
|
1949
|
-
}
|
|
1950
|
-
function registerSetupCommand(program2) {
|
|
1951
2147
|
const setupCommand = program2.command("setup").description("Setup integrations");
|
|
1952
2148
|
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
2149
|
const settingsPath = getClaudeSettingsPath(options.global ?? false);
|
|
@@ -2048,150 +2244,44 @@ function registerSetupCommand(program2) {
|
|
|
2048
2244
|
}
|
|
2049
2245
|
}
|
|
2050
2246
|
});
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
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");
|
|
2061
|
-
}
|
|
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();
|
|
2068
|
-
}
|
|
2069
|
-
if (!quiet) {
|
|
2070
|
-
console.log("Consider these lessons when planning and implementing tasks.");
|
|
2071
|
-
}
|
|
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;
|
|
2081
|
-
}
|
|
2082
|
-
if (lessons.length === 0) {
|
|
2083
|
-
console.log("No high-severity lessons found.");
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
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);
|
|
2095
|
-
}
|
|
2096
|
-
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
2097
|
-
}
|
|
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.");
|
|
2125
|
-
}
|
|
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()) {
|
|
2247
|
+
program2.command("download-model").description("Download the embedding model for semantic search").option("--json", "Output as JSON").action(async (options) => {
|
|
2248
|
+
const alreadyExisted = isModelAvailable();
|
|
2249
|
+
if (alreadyExisted) {
|
|
2250
|
+
const modelPath2 = join(homedir(), ".node-llama-cpp", "models", MODEL_FILENAME);
|
|
2251
|
+
const size2 = statSync(modelPath2).size;
|
|
2138
2252
|
if (options.json) {
|
|
2139
|
-
console.log(JSON.stringify({
|
|
2140
|
-
error: "Embedding model not available",
|
|
2141
|
-
action: "Run: npx lna download-model"
|
|
2142
|
-
}));
|
|
2253
|
+
console.log(JSON.stringify({ success: true, path: modelPath2, size: size2, alreadyExisted: true }));
|
|
2143
2254
|
} else {
|
|
2144
|
-
|
|
2145
|
-
console.log(
|
|
2146
|
-
console.log(
|
|
2255
|
+
console.log("Model already exists.");
|
|
2256
|
+
console.log(`Path: ${modelPath2}`);
|
|
2257
|
+
console.log(`Size: ${formatBytes(size2)}`);
|
|
2147
2258
|
}
|
|
2148
|
-
|
|
2259
|
+
return;
|
|
2149
2260
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
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);
|
|
2261
|
+
if (!options.json) {
|
|
2262
|
+
console.log("Downloading embedding model...");
|
|
2263
|
+
}
|
|
2264
|
+
const modelPath = await resolveModel({ cli: !options.json });
|
|
2265
|
+
const size = statSync(modelPath).size;
|
|
2266
|
+
if (options.json) {
|
|
2267
|
+
console.log(JSON.stringify({ success: true, path: modelPath, size, alreadyExisted: false }));
|
|
2268
|
+
} else {
|
|
2269
|
+
console.log(`
|
|
2270
|
+
Model downloaded successfully!`);
|
|
2271
|
+
console.log(`Path: ${modelPath}`);
|
|
2272
|
+
console.log(`Size: ${formatBytes(size)}`);
|
|
2169
2273
|
}
|
|
2170
2274
|
});
|
|
2171
2275
|
}
|
|
2172
2276
|
|
|
2173
2277
|
// src/cli.ts
|
|
2174
2278
|
var program = new Command();
|
|
2175
|
-
program.
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
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);
|
|
2279
|
+
program.option("-v, --verbose", "Show detailed output").option("-q, --quiet", "Suppress non-essential output");
|
|
2280
|
+
program.name("learning-agent").description("Repository-scoped learning system for Claude Code").version(VERSION);
|
|
2281
|
+
registerCaptureCommands(program);
|
|
2282
|
+
registerRetrievalCommands(program);
|
|
2283
|
+
registerManagementCommands(program);
|
|
2284
|
+
registerSetupCommands(program);
|
|
2195
2285
|
program.parse();
|
|
2196
2286
|
//# sourceMappingURL=cli.js.map
|
|
2197
2287
|
//# sourceMappingURL=cli.js.map
|