learning-agent 0.2.1 → 0.2.3

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/dist/cli.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import chalk from 'chalk';
4
- import { statSync, existsSync, chmodSync, mkdirSync } from 'fs';
5
3
  import * as fs from 'fs/promises';
6
- import { readFile, mkdir, writeFile, rename, appendFile } from 'fs/promises';
7
- import { homedir } from 'os';
4
+ import { mkdir, appendFile, readFile, writeFile, rename } from 'fs/promises';
8
5
  import { join, dirname } from 'path';
9
6
  import { createHash } from 'crypto';
10
7
  import { z } from 'zod';
11
- import Database from 'better-sqlite3';
8
+ import { createRequire } from 'module';
9
+ import { existsSync, statSync, mkdirSync, chmodSync } from 'fs';
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,9 +180,45 @@ async function readLessons(repoRoot, options = {}) {
249
180
  }
250
181
  return { lessons: Array.from(lessons.values()), skippedCount };
251
182
  }
252
- var DB_PATH = ".claude/.cache/lessons.sqlite";
183
+ var require2 = createRequire(import.meta.url);
184
+ var sqliteAvailable = null;
185
+ var sqliteWarningLogged = false;
186
+ var DatabaseConstructor = null;
187
+ function isSqliteAvailable() {
188
+ if (sqliteAvailable !== null) {
189
+ return sqliteAvailable;
190
+ }
191
+ try {
192
+ const module = require2("better-sqlite3");
193
+ const Constructor = module.default || module;
194
+ const testDb = new Constructor(":memory:");
195
+ testDb.close();
196
+ DatabaseConstructor = Constructor;
197
+ sqliteAvailable = true;
198
+ } catch {
199
+ sqliteAvailable = false;
200
+ if (!sqliteWarningLogged) {
201
+ console.warn("SQLite unavailable, running in JSONL-only mode");
202
+ sqliteWarningLogged = true;
203
+ }
204
+ }
205
+ return sqliteAvailable;
206
+ }
207
+ function logDegradationWarning() {
208
+ if (!sqliteAvailable && !sqliteWarningLogged) {
209
+ console.warn("SQLite unavailable, running in JSONL-only mode");
210
+ sqliteWarningLogged = true;
211
+ }
212
+ }
213
+ function getDatabaseConstructor() {
214
+ if (!isSqliteAvailable()) {
215
+ return null;
216
+ }
217
+ return DatabaseConstructor;
218
+ }
219
+
220
+ // src/storage/sqlite/schema.ts
253
221
  var SCHEMA_SQL = `
254
- -- Main lessons table
255
222
  CREATE TABLE IF NOT EXISTS lessons (
256
223
  id TEXT PRIMARY KEY,
257
224
  type TEXT NOT NULL,
@@ -270,32 +237,31 @@ var SCHEMA_SQL = `
270
237
  retrieval_count INTEGER NOT NULL DEFAULT 0,
271
238
  last_retrieved TEXT,
272
239
  embedding BLOB,
273
- content_hash TEXT
240
+ content_hash TEXT,
241
+ invalidated_at TEXT,
242
+ invalidation_reason TEXT,
243
+ citation_file TEXT,
244
+ citation_line INTEGER,
245
+ citation_commit TEXT,
246
+ compaction_level INTEGER DEFAULT 0,
247
+ compacted_at TEXT
274
248
  );
275
249
 
276
- -- FTS5 virtual table for full-text search
277
250
  CREATE VIRTUAL TABLE IF NOT EXISTS lessons_fts USING fts5(
278
- id,
279
- trigger,
280
- insight,
281
- tags,
282
- content='lessons',
283
- content_rowid='rowid'
251
+ id, trigger, insight, tags,
252
+ content='lessons', content_rowid='rowid'
284
253
  );
285
254
 
286
- -- Trigger to sync FTS on INSERT
287
255
  CREATE TRIGGER IF NOT EXISTS lessons_ai AFTER INSERT ON lessons BEGIN
288
256
  INSERT INTO lessons_fts(rowid, id, trigger, insight, tags)
289
257
  VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
290
258
  END;
291
259
 
292
- -- Trigger to sync FTS on DELETE
293
260
  CREATE TRIGGER IF NOT EXISTS lessons_ad AFTER DELETE ON lessons BEGIN
294
261
  INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
295
262
  VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
296
263
  END;
297
264
 
298
- -- Trigger to sync FTS on UPDATE
299
265
  CREATE TRIGGER IF NOT EXISTS lessons_au AFTER UPDATE ON lessons BEGIN
300
266
  INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
301
267
  VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
@@ -303,12 +269,10 @@ var SCHEMA_SQL = `
303
269
  VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
304
270
  END;
305
271
 
306
- -- Index for common queries
307
272
  CREATE INDEX IF NOT EXISTS idx_lessons_created ON lessons(created);
308
273
  CREATE INDEX IF NOT EXISTS idx_lessons_confirmed ON lessons(confirmed);
309
274
  CREATE INDEX IF NOT EXISTS idx_lessons_severity ON lessons(severity);
310
275
 
311
- -- Metadata table for sync tracking
312
276
  CREATE TABLE IF NOT EXISTS metadata (
313
277
  key TEXT PRIMARY KEY,
314
278
  value TEXT NOT NULL
@@ -317,22 +281,54 @@ var SCHEMA_SQL = `
317
281
  function createSchema(database) {
318
282
  database.exec(SCHEMA_SQL);
319
283
  }
284
+
285
+ // src/storage/sqlite/connection.ts
286
+ var DB_PATH = ".claude/.cache/lessons.sqlite";
320
287
  var db = null;
321
- function contentHash(trigger, insight) {
322
- return createHash("sha256").update(`${trigger} ${insight}`).digest("hex");
323
- }
324
- function openDb(repoRoot) {
325
- if (db) return db;
326
- const dbPath = join(repoRoot, DB_PATH);
327
- const dir = dirname(dbPath);
328
- mkdirSync(dir, { recursive: true });
329
- db = new Database(dbPath);
330
- db.pragma("journal_mode = WAL");
288
+ var dbIsInMemory = false;
289
+ function openDb(repoRoot, options = {}) {
290
+ if (!isSqliteAvailable()) {
291
+ return null;
292
+ }
293
+ const { inMemory = false } = options;
294
+ if (db) {
295
+ if (inMemory !== dbIsInMemory) {
296
+ closeDb();
297
+ } else {
298
+ return db;
299
+ }
300
+ }
301
+ const Database = getDatabaseConstructor();
302
+ if (inMemory) {
303
+ db = new Database(":memory:");
304
+ dbIsInMemory = true;
305
+ } else {
306
+ const dbPath = join(repoRoot, DB_PATH);
307
+ const dir = dirname(dbPath);
308
+ mkdirSync(dir, { recursive: true });
309
+ db = new Database(dbPath);
310
+ dbIsInMemory = false;
311
+ db.pragma("journal_mode = WAL");
312
+ }
331
313
  createSchema(db);
332
314
  return db;
333
315
  }
316
+ function closeDb() {
317
+ if (db) {
318
+ db.close();
319
+ db = null;
320
+ dbIsInMemory = false;
321
+ }
322
+ }
323
+ function contentHash(trigger, insight) {
324
+ return createHash("sha256").update(`${trigger} ${insight}`).digest("hex");
325
+ }
334
326
  function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
335
327
  const database = openDb(repoRoot);
328
+ if (!database) {
329
+ logDegradationWarning();
330
+ return null;
331
+ }
336
332
  const row = database.prepare("SELECT embedding, content_hash FROM lessons WHERE id = ?").get(lessonId);
337
333
  if (!row || !row.embedding || !row.content_hash) {
338
334
  return null;
@@ -349,38 +345,14 @@ function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
349
345
  }
350
346
  function setCachedEmbedding(repoRoot, lessonId, embedding, hash) {
351
347
  const database = openDb(repoRoot);
348
+ if (!database) {
349
+ logDegradationWarning();
350
+ return;
351
+ }
352
352
  const float32 = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
353
353
  const buffer = Buffer.from(float32.buffer, float32.byteOffset, float32.byteLength);
354
354
  database.prepare("UPDATE lessons SET embedding = ?, content_hash = ? WHERE id = ?").run(buffer, hash, lessonId);
355
355
  }
356
- function rowToLesson(row) {
357
- const lesson = {
358
- id: row.id,
359
- type: row.type,
360
- trigger: row.trigger,
361
- insight: row.insight,
362
- tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
363
- source: row.source,
364
- context: JSON.parse(row.context),
365
- supersedes: JSON.parse(row.supersedes),
366
- related: JSON.parse(row.related),
367
- created: row.created,
368
- confirmed: row.confirmed === 1
369
- };
370
- if (row.evidence !== null) {
371
- lesson.evidence = row.evidence;
372
- }
373
- if (row.severity !== null) {
374
- lesson.severity = row.severity;
375
- }
376
- if (row.deleted === 1) {
377
- lesson.deleted = true;
378
- }
379
- if (row.retrieval_count > 0) {
380
- lesson.retrievalCount = row.retrieval_count;
381
- }
382
- return lesson;
383
- }
384
356
  function collectCachedEmbeddings(database) {
385
357
  const cache = /* @__PURE__ */ new Map();
386
358
  const rows = database.prepare("SELECT id, embedding, content_hash FROM lessons WHERE embedding IS NOT NULL").all();
@@ -392,8 +364,8 @@ function collectCachedEmbeddings(database) {
392
364
  return cache;
393
365
  }
394
366
  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)
367
+ 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)
368
+ 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
369
  `;
398
370
  function getJsonlMtime(repoRoot) {
399
371
  const jsonlPath = join(repoRoot, LESSONS_PATH);
@@ -413,6 +385,10 @@ function setLastSyncMtime(database, mtime) {
413
385
  }
414
386
  async function rebuildIndex(repoRoot) {
415
387
  const database = openDb(repoRoot);
388
+ if (!database) {
389
+ logDegradationWarning();
390
+ return;
391
+ }
416
392
  const { lessons } = await readLessons(repoRoot);
417
393
  const cachedEmbeddings = collectCachedEmbeddings(database);
418
394
  database.exec("DELETE FROM lessons");
@@ -445,10 +421,16 @@ async function rebuildIndex(repoRoot) {
445
421
  confirmed: lesson.confirmed ? 1 : 0,
446
422
  deleted: lesson.deleted ? 1 : 0,
447
423
  retrieval_count: lesson.retrievalCount ?? 0,
448
- last_retrieved: null,
449
- // Reset on rebuild since we're rebuilding from source
424
+ last_retrieved: lesson.lastRetrieved ?? null,
450
425
  embedding: hasValidCache ? cached.embedding : null,
451
- content_hash: hasValidCache ? cached.contentHash : null
426
+ content_hash: hasValidCache ? cached.contentHash : null,
427
+ invalidated_at: lesson.invalidatedAt ?? null,
428
+ invalidation_reason: lesson.invalidationReason ?? null,
429
+ citation_file: lesson.citation?.file ?? null,
430
+ citation_line: lesson.citation?.line ?? null,
431
+ citation_commit: lesson.citation?.commit ?? null,
432
+ compaction_level: lesson.compactionLevel ?? 0,
433
+ compacted_at: lesson.compactedAt ?? null
452
434
  });
453
435
  }
454
436
  });
@@ -459,12 +441,17 @@ async function rebuildIndex(repoRoot) {
459
441
  }
460
442
  }
461
443
  async function syncIfNeeded(repoRoot, options = {}) {
444
+ if (!isSqliteAvailable()) {
445
+ logDegradationWarning();
446
+ return false;
447
+ }
462
448
  const { force = false } = options;
463
449
  const jsonlMtime = getJsonlMtime(repoRoot);
464
450
  if (jsonlMtime === null && !force) {
465
451
  return false;
466
452
  }
467
453
  const database = openDb(repoRoot);
454
+ if (!database) return false;
468
455
  const lastSyncMtime = getLastSyncMtime(database);
469
456
  const needsRebuild = force || lastSyncMtime === null || jsonlMtime !== null && jsonlMtime > lastSyncMtime;
470
457
  if (needsRebuild) {
@@ -473,27 +460,49 @@ async function syncIfNeeded(repoRoot, options = {}) {
473
460
  }
474
461
  return false;
475
462
  }
476
- async function searchKeyword(repoRoot, query, limit) {
477
- const database = openDb(repoRoot);
478
- const countResult = database.prepare("SELECT COUNT(*) as cnt FROM lessons").get();
479
- if (countResult.cnt === 0) return [];
480
- const rows = database.prepare(
481
- `
482
- SELECT l.*
483
- FROM lessons l
484
- JOIN lessons_fts fts ON l.rowid = fts.rowid
485
- WHERE lessons_fts MATCH ?
486
- LIMIT ?
487
- `
488
- ).all(query, limit);
489
- if (rows.length > 0) {
490
- incrementRetrievalCount(repoRoot, rows.map((r) => r.id));
463
+
464
+ // src/storage/sqlite/search.ts
465
+ function rowToLesson(row) {
466
+ const lesson = {
467
+ id: row.id,
468
+ type: row.type,
469
+ trigger: row.trigger,
470
+ insight: row.insight,
471
+ tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
472
+ source: row.source,
473
+ context: JSON.parse(row.context),
474
+ supersedes: JSON.parse(row.supersedes),
475
+ related: JSON.parse(row.related),
476
+ created: row.created,
477
+ confirmed: row.confirmed === 1
478
+ };
479
+ if (row.evidence !== null) lesson.evidence = row.evidence;
480
+ if (row.severity !== null) lesson.severity = row.severity;
481
+ if (row.deleted === 1) lesson.deleted = true;
482
+ if (row.retrieval_count > 0) lesson.retrievalCount = row.retrieval_count;
483
+ if (row.invalidated_at !== null) lesson.invalidatedAt = row.invalidated_at;
484
+ if (row.invalidation_reason !== null) lesson.invalidationReason = row.invalidation_reason;
485
+ if (row.citation_file !== null) {
486
+ lesson.citation = {
487
+ file: row.citation_file,
488
+ ...row.citation_line !== null && { line: row.citation_line },
489
+ ...row.citation_commit !== null && { commit: row.citation_commit }
490
+ };
491
491
  }
492
- return rows.map(rowToLesson);
492
+ if (row.compaction_level !== null && row.compaction_level !== 0) {
493
+ lesson.compactionLevel = row.compaction_level;
494
+ }
495
+ if (row.compacted_at !== null) lesson.compactedAt = row.compacted_at;
496
+ if (row.last_retrieved !== null) lesson.lastRetrieved = row.last_retrieved;
497
+ return lesson;
493
498
  }
494
499
  function incrementRetrievalCount(repoRoot, lessonIds) {
495
500
  if (lessonIds.length === 0) return;
496
501
  const database = openDb(repoRoot);
502
+ if (!database) {
503
+ logDegradationWarning();
504
+ return;
505
+ }
497
506
  const now = (/* @__PURE__ */ new Date()).toISOString();
498
507
  const update = database.prepare(`
499
508
  UPDATE lessons
@@ -508,8 +517,36 @@ function incrementRetrievalCount(repoRoot, lessonIds) {
508
517
  });
509
518
  updateMany(lessonIds);
510
519
  }
520
+ async function searchKeyword(repoRoot, query, limit) {
521
+ const database = openDb(repoRoot);
522
+ if (!database) {
523
+ throw new Error(
524
+ "Keyword search requires SQLite (FTS5 required). Install native build tools or use vector search instead."
525
+ );
526
+ }
527
+ const countResult = database.prepare("SELECT COUNT(*) as cnt FROM lessons").get();
528
+ if (countResult.cnt === 0) return [];
529
+ const rows = database.prepare(
530
+ `
531
+ SELECT l.*
532
+ FROM lessons l
533
+ JOIN lessons_fts fts ON l.rowid = fts.rowid
534
+ WHERE lessons_fts MATCH ?
535
+ AND l.invalidated_at IS NULL
536
+ LIMIT ?
537
+ `
538
+ ).all(query, limit);
539
+ if (rows.length > 0) {
540
+ incrementRetrievalCount(repoRoot, rows.map((r) => r.id));
541
+ }
542
+ return rows.map(rowToLesson);
543
+ }
511
544
  function getRetrievalStats(repoRoot) {
512
545
  const database = openDb(repoRoot);
546
+ if (!database) {
547
+ logDegradationWarning();
548
+ return [];
549
+ }
513
550
  const rows = database.prepare("SELECT id, retrieval_count, last_retrieved FROM lessons").all();
514
551
  return rows.map((row) => ({
515
552
  id: row.id,
@@ -517,10 +554,19 @@ function getRetrievalStats(repoRoot) {
517
554
  lastRetrieved: row.last_retrieved
518
555
  }));
519
556
  }
557
+
558
+ // src/utils.ts
559
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
560
+ function getLessonAgeDays(lesson) {
561
+ const created = new Date(lesson.created).getTime();
562
+ const now = Date.now();
563
+ return Math.floor((now - created) / MS_PER_DAY);
564
+ }
565
+
566
+ // src/storage/compact.ts
520
567
  var ARCHIVE_DIR = ".claude/lessons/archive";
521
568
  var TOMBSTONE_THRESHOLD = 100;
522
569
  var ARCHIVE_AGE_DAYS = 90;
523
- var MS_PER_DAY = 1e3 * 60 * 60 * 24;
524
570
  var MONTH_INDEX_OFFSET = 1;
525
571
  var MONTH_PAD_LENGTH = 2;
526
572
  function getArchivePath(repoRoot, date) {
@@ -574,19 +620,16 @@ async function rewriteWithoutTombstones(repoRoot) {
574
620
  await rename(tempPath, filePath);
575
621
  return tombstoneCount;
576
622
  }
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;
623
+ function shouldArchive(lesson) {
624
+ const ageDays = getLessonAgeDays(lesson);
581
625
  return ageDays > ARCHIVE_AGE_DAYS && (lesson.retrievalCount === void 0 || lesson.retrievalCount === 0);
582
626
  }
583
627
  async function archiveOldLessons(repoRoot) {
584
628
  const { lessons } = await readLessons(repoRoot);
585
- const now = /* @__PURE__ */ new Date();
586
629
  const toArchive = [];
587
630
  const toKeep = [];
588
631
  for (const lesson of lessons) {
589
- if (shouldArchive(lesson, now)) {
632
+ if (shouldArchive(lesson)) {
590
633
  toArchive.push(lesson);
591
634
  } else {
592
635
  toKeep.push(lesson);
@@ -631,500 +674,998 @@ async function compact(repoRoot) {
631
674
  };
632
675
  }
633
676
 
634
- // src/cli/commands/stats.ts
635
- function registerStatsCommand(program2) {
636
- program2.command("stats").description("Show database health and statistics").action(async () => {
637
- const repoRoot = getRepoRoot();
638
- await syncIfNeeded(repoRoot);
639
- const { lessons } = await readLessons(repoRoot);
640
- const deletedCount = await countTombstones(repoRoot);
641
- const totalLessons = lessons.length;
642
- const retrievalStats = getRetrievalStats(repoRoot);
643
- const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
644
- const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
645
- const jsonlPath = join(repoRoot, LESSONS_PATH);
646
- const dbPath = join(repoRoot, DB_PATH);
647
- let dataSize = 0;
648
- let indexSize = 0;
649
- try {
650
- dataSize = statSync(jsonlPath).size;
651
- } catch {
652
- }
653
- try {
654
- indexSize = statSync(dbPath).size;
655
- } catch {
656
- }
657
- const totalSize = dataSize + indexSize;
658
- const deletedInfo = deletedCount > 0 ? ` (${deletedCount} deleted)` : "";
659
- console.log(`Lessons: ${totalLessons} total${deletedInfo}`);
660
- console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
661
- console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
662
- });
663
- }
664
- function registerListCommand(program2) {
665
- program2.command("list").description("List all lessons").option("-n, --limit <number>", "Maximum results", DEFAULT_LIST_LIMIT).action(async function(options) {
666
- const repoRoot = getRepoRoot();
667
- const limit = parseLimit(options.limit, "limit");
668
- const { verbose, quiet } = getGlobalOpts(this);
669
- const { lessons, skippedCount } = await readLessons(repoRoot);
670
- if (lessons.length === 0) {
671
- console.log('No lessons found. Get started with: learn "Your first lesson"');
672
- if (skippedCount > 0) {
673
- out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
674
- }
675
- return;
676
- }
677
- const toShow = lessons.slice(0, limit);
678
- if (!quiet) {
679
- out.info(`Showing ${toShow.length} of ${lessons.length} lesson(s):
680
- `);
681
- }
682
- for (const lesson of toShow) {
683
- console.log(`[${chalk.cyan(lesson.id)}] ${lesson.insight}`);
684
- if (verbose) {
685
- console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
686
- console.log(` Created: ${lesson.created}`);
687
- if (lesson.context) {
688
- console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
689
- }
690
- } else {
691
- console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
692
- }
693
- if (lesson.tags.length > 0) {
694
- console.log(` Tags: ${lesson.tags.join(", ")}`);
695
- }
696
- console.log();
677
+ // src/capture/quality.ts
678
+ var DEFAULT_SIMILARITY_THRESHOLD = 0.8;
679
+ async function isNovel(repoRoot, insight, options = {}) {
680
+ const threshold = options.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
681
+ await syncIfNeeded(repoRoot);
682
+ const words = insight.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3).slice(0, 3);
683
+ if (words.length === 0) {
684
+ return { novel: true };
685
+ }
686
+ const searchQuery = words.join(" OR ");
687
+ const results = await searchKeyword(repoRoot, searchQuery, 10);
688
+ if (results.length === 0) {
689
+ return { novel: true };
690
+ }
691
+ const insightWords = new Set(insight.toLowerCase().split(/\s+/));
692
+ for (const lesson of results) {
693
+ const lessonWords = new Set(lesson.insight.toLowerCase().split(/\s+/));
694
+ const intersection = [...insightWords].filter((w) => lessonWords.has(w)).length;
695
+ const union = (/* @__PURE__ */ new Set([...insightWords, ...lessonWords])).size;
696
+ const similarity = union > 0 ? intersection / union : 0;
697
+ if (similarity >= threshold) {
698
+ return {
699
+ novel: false,
700
+ reason: `Found similar existing lesson: "${lesson.insight.slice(0, 50)}..."`,
701
+ existingId: lesson.id
702
+ };
697
703
  }
698
- if (skippedCount > 0) {
699
- out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
704
+ if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
705
+ return {
706
+ novel: false,
707
+ reason: `Exact duplicate found`,
708
+ existingId: lesson.id
709
+ };
700
710
  }
701
- });
711
+ }
712
+ return { novel: true };
702
713
  }
703
- function registerSearchCommand(program2) {
704
- program2.command("search <query>").description("Search lessons by keyword").option("-n, --limit <number>", "Maximum results", DEFAULT_SEARCH_LIMIT).action(async function(query, options) {
705
- const repoRoot = getRepoRoot();
706
- const limit = parseLimit(options.limit, "limit");
707
- const { verbose, quiet } = getGlobalOpts(this);
708
- await syncIfNeeded(repoRoot);
709
- const results = await searchKeyword(repoRoot, query, limit);
710
- if (results.length === 0) {
711
- console.log('No lessons match your search. Try a different query or use "list" to see all lessons.');
712
- return;
713
- }
714
- if (!quiet) {
715
- out.info(`Found ${results.length} lesson(s):
716
- `);
717
- }
718
- for (const lesson of results) {
719
- console.log(`[${chalk.cyan(lesson.id)}] ${lesson.insight}`);
720
- console.log(` Trigger: ${lesson.trigger}`);
721
- if (verbose && lesson.context) {
722
- console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
723
- console.log(` Created: ${lesson.created}`);
724
- }
725
- if (lesson.tags.length > 0) {
726
- console.log(` Tags: ${lesson.tags.join(", ")}`);
727
- }
728
- console.log();
714
+ var MIN_WORD_COUNT = 4;
715
+ var VAGUE_PATTERNS = [
716
+ /\bwrite better\b/i,
717
+ /\bbe careful\b/i,
718
+ /\bremember to\b/i,
719
+ /\bmake sure\b/i,
720
+ /\btry to\b/i,
721
+ /\bdouble check\b/i
722
+ ];
723
+ var GENERIC_IMPERATIVE_PATTERN = /^(always|never)\s+\w+(\s+\w+){0,2}$/i;
724
+ function isSpecific(insight) {
725
+ const words = insight.trim().split(/\s+/).filter((w) => w.length > 0);
726
+ if (words.length < MIN_WORD_COUNT) {
727
+ return { specific: false, reason: "Insight is too short to be actionable" };
728
+ }
729
+ for (const pattern of VAGUE_PATTERNS) {
730
+ if (pattern.test(insight)) {
731
+ return { specific: false, reason: "Insight matches a vague pattern" };
729
732
  }
730
- });
733
+ }
734
+ if (GENERIC_IMPERATIVE_PATTERN.test(insight)) {
735
+ return { specific: false, reason: "Insight matches a vague pattern" };
736
+ }
737
+ return { specific: true };
731
738
  }
732
-
733
- // src/cli/commands/rebuild.ts
734
- function registerRebuildCommand(program2) {
735
- program2.command("rebuild").description("Rebuild SQLite index from JSONL").option("-f, --force", "Force rebuild even if unchanged").action(async (options) => {
736
- const repoRoot = getRepoRoot();
737
- if (options.force) {
738
- console.log("Forcing index rebuild...");
739
- await rebuildIndex(repoRoot);
740
- console.log("Index rebuilt.");
741
- } else {
742
- const rebuilt = await syncIfNeeded(repoRoot);
743
- if (rebuilt) {
744
- console.log("Index rebuilt (JSONL changed).");
745
- } else {
746
- console.log("Index is up to date.");
747
- }
739
+ var ACTION_PATTERNS = [
740
+ /\buse\s+.+\s+instead\s+of\b/i,
741
+ // "use X instead of Y"
742
+ /\bprefer\s+.+\s+(over|to)\b/i,
743
+ // "prefer X over Y" or "prefer X to Y"
744
+ /\balways\s+.+\s+when\b/i,
745
+ // "always X when Y"
746
+ /\bnever\s+.+\s+without\b/i,
747
+ // "never X without Y"
748
+ /\bavoid\s+(using\s+)?\w+/i,
749
+ // "avoid X" or "avoid using X"
750
+ /\bcheck\s+.+\s+before\b/i,
751
+ // "check X before Y"
752
+ /^(run|use|add|remove|install|update|configure|set|enable|disable)\s+/i
753
+ // Imperative commands at start
754
+ ];
755
+ function isActionable(insight) {
756
+ for (const pattern of ACTION_PATTERNS) {
757
+ if (pattern.test(insight)) {
758
+ return { actionable: true };
748
759
  }
749
- });
760
+ }
761
+ return { actionable: false, reason: "Insight lacks clear action guidance" };
762
+ }
763
+ async function shouldPropose(repoRoot, insight) {
764
+ const specificResult = isSpecific(insight);
765
+ if (!specificResult.specific) {
766
+ return { shouldPropose: false, reason: specificResult.reason };
767
+ }
768
+ const actionableResult = isActionable(insight);
769
+ if (!actionableResult.actionable) {
770
+ return { shouldPropose: false, reason: actionableResult.reason };
771
+ }
772
+ const noveltyResult = await isNovel(repoRoot, insight);
773
+ if (!noveltyResult.novel) {
774
+ return { shouldPropose: false, reason: noveltyResult.reason };
775
+ }
776
+ return { shouldPropose: true };
750
777
  }
751
778
 
752
- // src/cli/commands/export.ts
753
- function registerExportCommand(program2) {
754
- program2.command("export").description("Export lessons as JSON to stdout").option("--since <date>", "Only include lessons created after this date (ISO8601)").option("--tags <tags>", "Filter by tags (comma-separated, OR logic)").action(async (options) => {
755
- const repoRoot = getRepoRoot();
756
- const { lessons } = await readLessons(repoRoot);
757
- let filtered = lessons;
758
- if (options.since) {
759
- const sinceDate = new Date(options.since);
760
- if (Number.isNaN(sinceDate.getTime())) {
761
- console.error(`Invalid date format: ${options.since}. Use ISO8601 format (e.g., 2024-01-15).`);
762
- process.exit(1);
779
+ // src/capture/triggers.ts
780
+ var USER_CORRECTION_PATTERNS = [
781
+ /\bno\b[,.]?\s/i,
782
+ // "no, ..." or "no ..."
783
+ /\bwrong\b/i,
784
+ // "wrong"
785
+ /\bactually\b/i,
786
+ // "actually..."
787
+ /\bnot that\b/i,
788
+ // "not that"
789
+ /\bi meant\b/i
790
+ // "I meant"
791
+ ];
792
+ function detectUserCorrection(signals) {
793
+ const { messages, context } = signals;
794
+ if (messages.length < 2) {
795
+ return null;
796
+ }
797
+ for (let i = 1; i < messages.length; i++) {
798
+ const message = messages[i];
799
+ if (!message) continue;
800
+ for (const pattern of USER_CORRECTION_PATTERNS) {
801
+ if (pattern.test(message)) {
802
+ return {
803
+ trigger: `User correction during ${context.intent}`,
804
+ correctionMessage: message,
805
+ context
806
+ };
763
807
  }
764
- filtered = filtered.filter((lesson) => new Date(lesson.created) >= sinceDate);
765
- }
766
- if (options.tags) {
767
- const filterTags = options.tags.split(",").map((t) => t.trim());
768
- filtered = filtered.filter((lesson) => lesson.tags.some((tag) => filterTags.includes(tag)));
769
808
  }
770
- console.log(JSON.stringify(filtered, null, JSON_INDENT_SPACES));
771
- });
809
+ }
810
+ return null;
772
811
  }
773
- function registerImportCommand(program2) {
774
- program2.command("import <file>").description("Import lessons from a JSONL file").action(async (file) => {
775
- const repoRoot = getRepoRoot();
776
- let content;
777
- try {
778
- content = await readFile(file, "utf-8");
779
- } catch (err) {
780
- const code = err.code;
781
- if (code === "ENOENT") {
782
- console.error(`Error: File not found: ${file}`);
783
- } else {
784
- console.error(`Error reading file: ${err.message}`);
785
- }
786
- process.exit(1);
812
+ function detectSelfCorrection(history) {
813
+ const { edits } = history;
814
+ if (edits.length < 3) {
815
+ return null;
816
+ }
817
+ for (let i = 0; i <= edits.length - 3; i++) {
818
+ const first = edits[i];
819
+ const second = edits[i + 1];
820
+ const third = edits[i + 2];
821
+ if (!first || !second || !third) continue;
822
+ if (first.file === second.file && second.file === third.file && first.success && !second.success && third.success) {
823
+ return {
824
+ file: first.file,
825
+ trigger: `Self-correction on ${first.file}`
826
+ };
787
827
  }
788
- const { lessons: existingLessons } = await readLessons(repoRoot);
789
- const existingIds = new Set(existingLessons.map((l) => l.id));
790
- const lines = content.split("\n");
791
- let imported = 0;
792
- let skipped = 0;
793
- let invalid = 0;
794
- for (const line of lines) {
795
- const trimmed = line.trim();
796
- if (!trimmed) continue;
797
- let parsed;
798
- try {
799
- parsed = JSON.parse(trimmed);
800
- } catch {
801
- invalid++;
802
- continue;
803
- }
804
- const result = LessonSchema.safeParse(parsed);
805
- if (!result.success) {
806
- invalid++;
807
- continue;
808
- }
809
- const lesson = result.data;
810
- if (existingIds.has(lesson.id)) {
811
- skipped++;
812
- continue;
813
- }
814
- await appendLesson(repoRoot, lesson);
815
- existingIds.add(lesson.id);
816
- imported++;
817
- }
818
- await syncIfNeeded(repoRoot);
819
- const lessonWord = imported === 1 ? "lesson" : "lessons";
820
- const parts = [];
821
- if (skipped > 0) parts.push(`${skipped} skipped`);
822
- if (invalid > 0) parts.push(`${invalid} invalid`);
823
- if (parts.length > 0) {
824
- console.log(`Imported ${imported} ${lessonWord} (${parts.join(", ")})`);
825
- } else {
826
- console.log(`Imported ${imported} ${lessonWord}`);
827
- }
828
- });
829
- }
830
-
831
- // src/cli/commands/compact.ts
832
- function registerCompactCommand(program2) {
833
- program2.command("compact").description("Compact lessons: archive old lessons and remove tombstones").option("-f, --force", "Run compaction even if below threshold").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
834
- const repoRoot = getRepoRoot();
835
- const tombstones = await countTombstones(repoRoot);
836
- const needs = await needsCompaction(repoRoot);
837
- if (options.dryRun) {
838
- console.log("Dry run - no changes will be made.\n");
839
- console.log(`Tombstones found: ${tombstones}`);
840
- console.log(`Compaction needed: ${needs ? "yes" : "no"}`);
841
- return;
842
- }
843
- if (!needs && !options.force) {
844
- console.log(`Compaction not needed (${tombstones} tombstones, threshold is ${TOMBSTONE_THRESHOLD}).`);
845
- console.log("Use --force to compact anyway.");
846
- return;
847
- }
848
- console.log("Running compaction...");
849
- const result = await compact(repoRoot);
850
- console.log("\nCompaction complete:");
851
- console.log(` Archived: ${result.archived} lesson(s)`);
852
- console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
853
- console.log(` Lessons remaining: ${result.lessonsRemaining}`);
854
- await rebuildIndex(repoRoot);
855
- console.log(" Index rebuilt.");
856
- });
857
- }
858
- var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
859
- var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
860
- var DEFAULT_MODEL_DIR = join(homedir(), ".node-llama-cpp", "models");
861
- function isModelAvailable() {
862
- return existsSync(join(DEFAULT_MODEL_DIR, MODEL_FILENAME));
828
+ }
829
+ return null;
863
830
  }
864
- async function resolveModel(options = {}) {
865
- const { cli = true } = options;
866
- return resolveModelFile(MODEL_URI, { cli });
831
+ function detectTestFailure(testResult) {
832
+ if (testResult.passed) {
833
+ return null;
834
+ }
835
+ const lines = testResult.output.split("\n").filter((line) => line.trim().length > 0);
836
+ const errorLine = lines.find((line) => /error|fail|assert/i.test(line)) ?? lines[0] ?? "";
837
+ return {
838
+ testFile: testResult.testFile,
839
+ errorOutput: testResult.output,
840
+ trigger: `Test failure in ${testResult.testFile}: ${errorLine.slice(0, 100)}`
841
+ };
867
842
  }
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;
843
+ async function detectAndPropose(repoRoot, input) {
844
+ const detected = runDetector(input);
845
+ if (!detected) {
846
+ return null;
847
+ }
848
+ const { trigger, source, proposedInsight } = detected;
849
+ const quality = await shouldPropose(repoRoot, proposedInsight);
850
+ if (!quality.shouldPropose) {
851
+ return null;
852
+ }
853
+ return { trigger, source, proposedInsight };
878
854
  }
879
- async function embedText(text) {
880
- const ctx = await getEmbedding();
881
- const result = await ctx.getEmbeddingFor(text);
882
- return Array.from(result.vector);
855
+ function runDetector(input) {
856
+ switch (input.type) {
857
+ case "user":
858
+ return detectUserCorrectionFlow(input.data);
859
+ case "self":
860
+ return detectSelfCorrectionFlow(input.data);
861
+ case "test":
862
+ return detectTestFailureFlow(input.data);
863
+ }
883
864
  }
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");
865
+ function detectUserCorrectionFlow(data) {
866
+ const result = detectUserCorrection(data);
867
+ if (!result) {
868
+ return null;
889
869
  }
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];
870
+ return {
871
+ trigger: result.trigger,
872
+ source: "user_correction",
873
+ proposedInsight: result.correctionMessage
874
+ };
875
+ }
876
+ function detectSelfCorrectionFlow(data) {
877
+ const result = detectSelfCorrection(data);
878
+ if (!result) {
879
+ return null;
897
880
  }
898
- const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
899
- if (magnitude === 0) return 0;
900
- return dotProduct / magnitude;
881
+ return {
882
+ trigger: result.trigger,
883
+ source: "self_correction",
884
+ // Self-corrections need context to form useful insights
885
+ proposedInsight: `Check ${result.file} for common errors before editing`
886
+ };
901
887
  }
902
- var DEFAULT_LIMIT = 10;
903
- async function searchVector(repoRoot, query, options) {
904
- const limit = options?.limit ?? DEFAULT_LIMIT;
905
- const { lessons } = await readLessons(repoRoot);
906
- if (lessons.length === 0) return [];
907
- const queryVector = await embedText(query);
908
- const scored = [];
909
- for (const lesson of lessons) {
910
- const lessonText = `${lesson.trigger} ${lesson.insight}`;
911
- const hash = contentHash(lesson.trigger, lesson.insight);
912
- let lessonVector = getCachedEmbedding(repoRoot, lesson.id, hash);
913
- if (!lessonVector) {
914
- lessonVector = await embedText(lessonText);
915
- setCachedEmbedding(repoRoot, lesson.id, lessonVector, hash);
916
- }
917
- const score = cosineSimilarity(queryVector, lessonVector);
918
- scored.push({ lesson, score });
888
+ function detectTestFailureFlow(data) {
889
+ const result = detectTestFailure(data);
890
+ if (!result) {
891
+ return null;
919
892
  }
920
- scored.sort((a, b) => b.score - a.score);
921
- return scored.slice(0, limit);
893
+ return {
894
+ trigger: result.trigger,
895
+ source: "test_failure",
896
+ proposedInsight: result.errorOutput
897
+ };
922
898
  }
923
-
924
- // src/search/ranking.ts
925
- var RECENCY_THRESHOLD_DAYS = 30;
926
- var HIGH_SEVERITY_BOOST = 1.5;
927
- var MEDIUM_SEVERITY_BOOST = 1;
928
- var LOW_SEVERITY_BOOST = 0.8;
929
- var RECENCY_BOOST = 1.2;
930
- var CONFIRMATION_BOOST = 1.3;
931
- function severityBoost(lesson) {
932
- switch (lesson.severity) {
933
- case "high":
934
- return HIGH_SEVERITY_BOOST;
935
- case "medium":
936
- return MEDIUM_SEVERITY_BOOST;
937
- case "low":
938
- return LOW_SEVERITY_BOOST;
939
- default:
940
- return MEDIUM_SEVERITY_BOOST;
899
+ var VALID_TYPES = /* @__PURE__ */ new Set(["user", "self", "test"]);
900
+ async function parseInputFile(filePath) {
901
+ const content = await fs.readFile(filePath, "utf-8");
902
+ const data = JSON.parse(content);
903
+ if (!VALID_TYPES.has(data.type)) {
904
+ throw new Error(`Invalid detection type: ${data.type}. Must be one of: user, self, test`);
941
905
  }
906
+ return data;
942
907
  }
943
- function recencyBoost(lesson) {
944
- const created = new Date(lesson.created);
945
- const now = /* @__PURE__ */ new Date();
946
- const ageMs = now.getTime() - created.getTime();
947
- const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
948
- return ageDays <= RECENCY_THRESHOLD_DAYS ? RECENCY_BOOST : 1;
908
+ var out = {
909
+ success: (msg) => console.log(chalk.green("[ok]"), msg),
910
+ error: (msg) => console.error(chalk.red("[error]"), msg),
911
+ info: (msg) => console.log(chalk.blue("[info]"), msg),
912
+ warn: (msg) => console.log(chalk.yellow("[warn]"), msg)
913
+ };
914
+ function getGlobalOpts(cmd) {
915
+ const opts = cmd.optsWithGlobals();
916
+ return {
917
+ verbose: opts.verbose ?? false,
918
+ quiet: opts.quiet ?? false
919
+ };
949
920
  }
950
- function confirmationBoost(lesson) {
951
- return lesson.confirmed ? CONFIRMATION_BOOST : 1;
921
+ var DEFAULT_SEARCH_LIMIT = "10";
922
+ var DEFAULT_LIST_LIMIT = "20";
923
+ var DEFAULT_CHECK_PLAN_LIMIT = "5";
924
+ var LESSON_COUNT_WARNING_THRESHOLD = 20;
925
+ var AGE_FLAG_THRESHOLD_DAYS = 90;
926
+ var ISO_DATE_PREFIX_LENGTH = 10;
927
+ var AVG_DECIMAL_PLACES = 1;
928
+ var RELEVANCE_DECIMAL_PLACES = 2;
929
+ var JSON_INDENT_SPACES = 2;
930
+
931
+ // src/commands/capture.ts
932
+ function createLessonFromFlags(trigger, insight, confirmed) {
933
+ return {
934
+ id: generateId(insight),
935
+ type: "quick",
936
+ trigger,
937
+ insight,
938
+ tags: [],
939
+ source: "manual",
940
+ context: { tool: "capture", intent: "manual capture" },
941
+ created: (/* @__PURE__ */ new Date()).toISOString(),
942
+ confirmed,
943
+ supersedes: [],
944
+ related: []
945
+ };
952
946
  }
953
- function calculateScore(lesson, vectorSimilarity) {
954
- return vectorSimilarity * severityBoost(lesson) * recencyBoost(lesson) * confirmationBoost(lesson);
947
+ function outputCaptureJson(lesson, saved) {
948
+ console.log(JSON.stringify({
949
+ id: lesson.id,
950
+ trigger: lesson.trigger,
951
+ insight: lesson.insight,
952
+ type: lesson.type,
953
+ saved
954
+ }));
955
955
  }
956
- function rankLessons(lessons) {
957
- return lessons.map((scored) => ({
958
- ...scored,
959
- finalScore: calculateScore(scored.lesson, scored.score)
960
- })).sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
956
+ function outputCapturePreview(lesson) {
957
+ console.log("Lesson captured:");
958
+ console.log(` ID: ${lesson.id}`);
959
+ console.log(` Trigger: ${lesson.trigger}`);
960
+ console.log(` Insight: ${lesson.insight}`);
961
+ console.log(` Type: ${lesson.type}`);
962
+ console.log(` Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
963
+ console.log("\nSave this lesson? [y/n]");
961
964
  }
962
-
963
- // src/capture/quality.ts
964
- var DEFAULT_SIMILARITY_THRESHOLD = 0.8;
965
- async function isNovel(repoRoot, insight, options = {}) {
966
- const threshold = options.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
967
- await syncIfNeeded(repoRoot);
968
- const words = insight.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3).slice(0, 3);
969
- if (words.length === 0) {
970
- return { novel: true };
965
+ function createLessonFromInputFile(result, confirmed) {
966
+ return {
967
+ id: generateId(result.proposedInsight),
968
+ type: "quick",
969
+ trigger: result.trigger,
970
+ insight: result.proposedInsight,
971
+ tags: [],
972
+ source: result.source,
973
+ context: { tool: "capture", intent: "auto-capture" },
974
+ created: (/* @__PURE__ */ new Date()).toISOString(),
975
+ confirmed,
976
+ supersedes: [],
977
+ related: []
978
+ };
979
+ }
980
+ function registerCaptureCommands(program2) {
981
+ 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) {
982
+ const repoRoot = getRepoRoot();
983
+ const { quiet } = getGlobalOpts(this);
984
+ let severity;
985
+ if (options.severity !== void 0) {
986
+ const result = SeveritySchema.safeParse(options.severity);
987
+ if (!result.success) {
988
+ out.error(`Invalid severity value: "${options.severity}". Valid values are: high, medium, low`);
989
+ process.exit(1);
990
+ }
991
+ severity = result.data;
992
+ }
993
+ const lessonType = severity !== void 0 ? "full" : "quick";
994
+ let citation;
995
+ if (options.citation) {
996
+ const parts = options.citation.split(":");
997
+ const file = parts[0] ?? "";
998
+ const lineStr = parts[1];
999
+ const line = lineStr ? parseInt(lineStr, 10) : void 0;
1000
+ citation = {
1001
+ file,
1002
+ ...line && !isNaN(line) && { line },
1003
+ ...options.citationCommit && { commit: options.citationCommit }
1004
+ };
1005
+ }
1006
+ const lesson = {
1007
+ id: generateId(insight),
1008
+ type: lessonType,
1009
+ trigger: options.trigger ?? "Manual capture",
1010
+ insight,
1011
+ tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : [],
1012
+ source: "manual",
1013
+ context: {
1014
+ tool: "cli",
1015
+ intent: "manual learning"
1016
+ },
1017
+ created: (/* @__PURE__ */ new Date()).toISOString(),
1018
+ confirmed: true,
1019
+ // learn command is explicit confirmation
1020
+ supersedes: [],
1021
+ related: [],
1022
+ ...severity !== void 0 && { severity },
1023
+ ...citation && { citation }
1024
+ };
1025
+ await appendLesson(repoRoot, lesson);
1026
+ const chalk3 = await import('chalk');
1027
+ out.success(`Learned: ${insight}`);
1028
+ if (!quiet) {
1029
+ console.log(`ID: ${chalk3.default.dim(lesson.id)}`);
1030
+ if (citation) {
1031
+ console.log(`Citation: ${chalk3.default.dim(citation.file)}${citation.line ? `:${citation.line}` : ""}`);
1032
+ }
1033
+ }
1034
+ });
1035
+ 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(
1036
+ async (options) => {
1037
+ const repoRoot = getRepoRoot();
1038
+ if (options.save && !options.yes) {
1039
+ if (options.json) {
1040
+ console.log(JSON.stringify({ error: "--save requires --yes flag for confirmation" }));
1041
+ } else {
1042
+ out.error("--save requires --yes flag for confirmation");
1043
+ console.log("Use: detect --input <file> --save --yes");
1044
+ }
1045
+ process.exit(1);
1046
+ }
1047
+ const input = await parseInputFile(options.input);
1048
+ const result = await detectAndPropose(repoRoot, input);
1049
+ if (!result) {
1050
+ if (options.json) {
1051
+ console.log(JSON.stringify({ detected: false }));
1052
+ } else {
1053
+ console.log("No learning trigger detected.");
1054
+ }
1055
+ return;
1056
+ }
1057
+ if (options.json) {
1058
+ console.log(JSON.stringify({ detected: true, ...result }));
1059
+ return;
1060
+ }
1061
+ console.log("Learning trigger detected!");
1062
+ console.log(` Trigger: ${result.trigger}`);
1063
+ console.log(` Source: ${result.source}`);
1064
+ console.log(` Proposed: ${result.proposedInsight}`);
1065
+ if (options.save && options.yes) {
1066
+ const lesson = {
1067
+ id: generateId(result.proposedInsight),
1068
+ type: "quick",
1069
+ trigger: result.trigger,
1070
+ insight: result.proposedInsight,
1071
+ tags: [],
1072
+ source: result.source,
1073
+ context: { tool: "detect", intent: "auto-capture" },
1074
+ created: (/* @__PURE__ */ new Date()).toISOString(),
1075
+ confirmed: true,
1076
+ // --yes confirms the lesson
1077
+ supersedes: [],
1078
+ related: []
1079
+ };
1080
+ await appendLesson(repoRoot, lesson);
1081
+ console.log(`
1082
+ Saved as lesson: ${lesson.id}`);
1083
+ }
1084
+ }
1085
+ );
1086
+ 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) {
1087
+ const repoRoot = getRepoRoot();
1088
+ const { verbose } = getGlobalOpts(this);
1089
+ let lesson;
1090
+ if (options.input) {
1091
+ const input = await parseInputFile(options.input);
1092
+ const result = await detectAndPropose(repoRoot, input);
1093
+ if (!result) {
1094
+ options.json ? console.log(JSON.stringify({ detected: false, saved: false })) : console.log("No learning trigger detected.");
1095
+ return;
1096
+ }
1097
+ lesson = createLessonFromInputFile(result, options.yes ?? false);
1098
+ } else if (options.trigger && options.insight) {
1099
+ lesson = createLessonFromFlags(options.trigger, options.insight, options.yes ?? false);
1100
+ } else {
1101
+ const msg = "Provide either --trigger and --insight, or --input file.";
1102
+ options.json ? console.log(JSON.stringify({ error: msg, saved: false })) : out.error(msg);
1103
+ process.exit(1);
1104
+ }
1105
+ if (!options.yes && !process.stdin.isTTY) {
1106
+ if (options.json) {
1107
+ console.log(JSON.stringify({ error: "--yes required in non-interactive mode", saved: false }));
1108
+ } else {
1109
+ out.error("--yes required in non-interactive mode");
1110
+ console.log('Use: capture --trigger "..." --insight "..." --yes');
1111
+ }
1112
+ process.exit(1);
1113
+ }
1114
+ if (options.json) {
1115
+ if (options.yes) await appendLesson(repoRoot, lesson);
1116
+ outputCaptureJson(lesson, options.yes ?? false);
1117
+ } else if (options.yes) {
1118
+ await appendLesson(repoRoot, lesson);
1119
+ out.success(`Lesson saved: ${lesson.id}`);
1120
+ if (verbose) console.log(` Type: ${lesson.type} | Trigger: ${lesson.trigger}`);
1121
+ } else {
1122
+ outputCapturePreview(lesson);
1123
+ }
1124
+ });
1125
+ }
1126
+ function formatLessonHuman(lesson) {
1127
+ const lines = [];
1128
+ lines.push(`ID: ${lesson.id}`);
1129
+ lines.push(`Type: ${lesson.type}`);
1130
+ lines.push(`Trigger: ${lesson.trigger}`);
1131
+ lines.push(`Insight: ${lesson.insight}`);
1132
+ if (lesson.evidence) {
1133
+ lines.push(`Evidence: ${lesson.evidence}`);
971
1134
  }
972
- const searchQuery = words.join(" OR ");
973
- const results = await searchKeyword(repoRoot, searchQuery, 10);
974
- if (results.length === 0) {
975
- return { novel: true };
1135
+ if (lesson.severity) {
1136
+ lines.push(`Severity: ${lesson.severity}`);
976
1137
  }
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
1138
+ lines.push(`Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
1139
+ lines.push(`Source: ${lesson.source}`);
1140
+ if (lesson.context) {
1141
+ lines.push(`Context: ${lesson.context.tool} - ${lesson.context.intent}`);
1142
+ }
1143
+ lines.push(`Created: ${lesson.created}`);
1144
+ lines.push(`Confirmed: ${lesson.confirmed ? "yes" : "no"}`);
1145
+ if (lesson.supersedes && lesson.supersedes.length > 0) {
1146
+ lines.push(`Supersedes: ${lesson.supersedes.join(", ")}`);
1147
+ }
1148
+ if (lesson.related && lesson.related.length > 0) {
1149
+ lines.push(`Related: ${lesson.related.join(", ")}`);
1150
+ }
1151
+ if (lesson.pattern) {
1152
+ lines.push("Pattern:");
1153
+ lines.push(` Bad: ${lesson.pattern.bad}`);
1154
+ lines.push(` Good: ${lesson.pattern.good}`);
1155
+ }
1156
+ return lines.join("\n");
1157
+ }
1158
+ async function wasLessonDeleted(repoRoot, id) {
1159
+ const filePath = join(repoRoot, LESSONS_PATH);
1160
+ try {
1161
+ const content = await readFile(filePath, "utf-8");
1162
+ const lines = content.split("\n");
1163
+ for (const line of lines) {
1164
+ const trimmed = line.trim();
1165
+ if (!trimmed) continue;
1166
+ try {
1167
+ const record = JSON.parse(trimmed);
1168
+ if (record.id === id && record.deleted === true) {
1169
+ return true;
1170
+ }
1171
+ } catch {
1172
+ }
1173
+ }
1174
+ } catch {
1175
+ }
1176
+ return false;
1177
+ }
1178
+
1179
+ // src/commands/management/crud.ts
1180
+ var SHOW_JSON_INDENT = 2;
1181
+ function registerCrudCommands(program2) {
1182
+ program2.command("show <id>").description("Show details of a specific lesson").option("--json", "Output as JSON").action(async (id, options) => {
1183
+ const repoRoot = getRepoRoot();
1184
+ const { lessons } = await readLessons(repoRoot);
1185
+ const lesson = lessons.find((l) => l.id === id);
1186
+ if (!lesson) {
1187
+ const wasDeleted = await wasLessonDeleted(repoRoot, id);
1188
+ if (options.json) {
1189
+ console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found` }));
1190
+ } else {
1191
+ out.error(wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found`);
1192
+ }
1193
+ process.exit(1);
1194
+ }
1195
+ if (options.json) {
1196
+ console.log(JSON.stringify(lesson, null, SHOW_JSON_INDENT));
1197
+ } else {
1198
+ console.log(formatLessonHuman(lesson));
1199
+ }
1200
+ });
1201
+ 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) => {
1202
+ const repoRoot = getRepoRoot();
1203
+ 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;
1204
+ if (!hasUpdates) {
1205
+ if (options.json) {
1206
+ console.log(JSON.stringify({ error: "No fields to update (specify at least one: --insight, --tags, --severity, ...)" }));
1207
+ } else {
1208
+ out.error("No fields to update (specify at least one: --insight, --tags, --severity, ...)");
1209
+ }
1210
+ process.exit(1);
1211
+ }
1212
+ const { lessons } = await readLessons(repoRoot);
1213
+ const lesson = lessons.find((l) => l.id === id);
1214
+ if (!lesson) {
1215
+ const wasDeleted = await wasLessonDeleted(repoRoot, id);
1216
+ if (options.json) {
1217
+ console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found` }));
1218
+ } else {
1219
+ out.error(wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found`);
1220
+ }
1221
+ process.exit(1);
1222
+ }
1223
+ if (options.severity !== void 0) {
1224
+ const result = SeveritySchema.safeParse(options.severity);
1225
+ if (!result.success) {
1226
+ if (options.json) {
1227
+ console.log(JSON.stringify({ error: `Invalid severity '${options.severity}' (must be: high, medium, low)` }));
1228
+ } else {
1229
+ out.error(`Invalid severity '${options.severity}' (must be: high, medium, low)`);
1230
+ }
1231
+ process.exit(1);
1232
+ }
1233
+ }
1234
+ const updatedLesson = {
1235
+ ...lesson,
1236
+ ...options.insight !== void 0 && { insight: options.insight },
1237
+ ...options.trigger !== void 0 && { trigger: options.trigger },
1238
+ ...options.evidence !== void 0 && { evidence: options.evidence },
1239
+ ...options.severity !== void 0 && { severity: options.severity },
1240
+ ...options.tags !== void 0 && {
1241
+ tags: [...new Set(
1242
+ options.tags.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
1243
+ )]
1244
+ },
1245
+ ...options.confirmed !== void 0 && { confirmed: options.confirmed === "true" }
1246
+ };
1247
+ const validationResult = LessonSchema.safeParse(updatedLesson);
1248
+ if (!validationResult.success) {
1249
+ if (options.json) {
1250
+ console.log(JSON.stringify({ error: `Schema validation failed: ${validationResult.error.message}` }));
1251
+ } else {
1252
+ out.error(`Schema validation failed: ${validationResult.error.message}`);
1253
+ }
1254
+ process.exit(1);
1255
+ }
1256
+ await appendLesson(repoRoot, updatedLesson);
1257
+ await syncIfNeeded(repoRoot);
1258
+ if (options.json) {
1259
+ console.log(JSON.stringify(updatedLesson, null, SHOW_JSON_INDENT));
1260
+ } else {
1261
+ out.success(`Updated lesson ${id}`);
1262
+ }
1263
+ });
1264
+ program2.command("delete <ids...>").description("Soft delete lessons (creates tombstone)").option("--json", "Output as JSON").action(async (ids, options) => {
1265
+ const repoRoot = getRepoRoot();
1266
+ const { lessons } = await readLessons(repoRoot);
1267
+ const lessonMap = new Map(lessons.map((l) => [l.id, l]));
1268
+ const deleted = [];
1269
+ const warnings = [];
1270
+ for (const id of ids) {
1271
+ const lesson = lessonMap.get(id);
1272
+ if (!lesson) {
1273
+ const wasDeleted = await wasLessonDeleted(repoRoot, id);
1274
+ warnings.push({ id, message: wasDeleted ? "already deleted" : "not found" });
1275
+ continue;
1276
+ }
1277
+ const tombstone = {
1278
+ ...lesson,
1279
+ deleted: true,
1280
+ deletedAt: (/* @__PURE__ */ new Date()).toISOString()
988
1281
  };
1282
+ await appendLesson(repoRoot, tombstone);
1283
+ deleted.push(id);
1284
+ }
1285
+ if (deleted.length > 0) {
1286
+ await syncIfNeeded(repoRoot);
1287
+ }
1288
+ if (options.json) {
1289
+ console.log(JSON.stringify({ deleted, warnings }));
1290
+ } else {
1291
+ if (deleted.length > 0) {
1292
+ out.success(`Deleted ${deleted.length} lesson(s): ${deleted.join(", ")}`);
1293
+ }
1294
+ for (const warning of warnings) {
1295
+ out.warn(`${warning.id}: ${warning.message}`);
1296
+ }
1297
+ if (deleted.length === 0 && warnings.length > 0) {
1298
+ process.exit(1);
1299
+ }
1300
+ }
1301
+ });
1302
+ }
1303
+
1304
+ // src/commands/management/invalidation.ts
1305
+ function registerInvalidationCommands(program2) {
1306
+ program2.command("wrong <id>").description("Mark a lesson as invalid/wrong").option("-r, --reason <text>", "Reason for invalidation").action(async function(id, options) {
1307
+ const repoRoot = getRepoRoot();
1308
+ const { lessons } = await readLessons(repoRoot);
1309
+ const lesson = lessons.find((l) => l.id === id);
1310
+ if (!lesson) {
1311
+ out.error(`Lesson not found: ${id}`);
1312
+ process.exit(1);
1313
+ }
1314
+ if (lesson.invalidatedAt) {
1315
+ out.warn(`Lesson ${id} is already marked as invalid.`);
1316
+ return;
1317
+ }
1318
+ const updatedLesson = {
1319
+ ...lesson,
1320
+ invalidatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1321
+ ...options.reason !== void 0 && { invalidationReason: options.reason }
1322
+ };
1323
+ await appendLesson(repoRoot, updatedLesson);
1324
+ out.success(`Lesson ${id} marked as invalid.`);
1325
+ if (options.reason) {
1326
+ console.log(` Reason: ${options.reason}`);
1327
+ }
1328
+ });
1329
+ program2.command("validate <id>").description("Re-enable a previously invalidated lesson").action(async function(id) {
1330
+ const repoRoot = getRepoRoot();
1331
+ const { lessons } = await readLessons(repoRoot);
1332
+ const lesson = lessons.find((l) => l.id === id);
1333
+ if (!lesson) {
1334
+ out.error(`Lesson not found: ${id}`);
1335
+ process.exit(1);
1336
+ }
1337
+ if (!lesson.invalidatedAt) {
1338
+ out.info(`Lesson ${id} is not invalidated.`);
1339
+ return;
1340
+ }
1341
+ const updatedLesson = {
1342
+ id: lesson.id,
1343
+ type: lesson.type,
1344
+ trigger: lesson.trigger,
1345
+ insight: lesson.insight,
1346
+ tags: lesson.tags,
1347
+ source: lesson.source,
1348
+ context: lesson.context,
1349
+ created: lesson.created,
1350
+ confirmed: lesson.confirmed,
1351
+ supersedes: lesson.supersedes,
1352
+ related: lesson.related,
1353
+ // Include optional fields if present (excluding invalidation)
1354
+ ...lesson.evidence !== void 0 && { evidence: lesson.evidence },
1355
+ ...lesson.severity !== void 0 && { severity: lesson.severity },
1356
+ ...lesson.pattern !== void 0 && { pattern: lesson.pattern },
1357
+ ...lesson.deleted !== void 0 && { deleted: lesson.deleted },
1358
+ ...lesson.retrievalCount !== void 0 && { retrievalCount: lesson.retrievalCount },
1359
+ ...lesson.citation !== void 0 && { citation: lesson.citation },
1360
+ ...lesson.compactionLevel !== void 0 && { compactionLevel: lesson.compactionLevel },
1361
+ ...lesson.compactedAt !== void 0 && { compactedAt: lesson.compactedAt },
1362
+ ...lesson.lastRetrieved !== void 0 && { lastRetrieved: lesson.lastRetrieved }
1363
+ };
1364
+ await appendLesson(repoRoot, updatedLesson);
1365
+ out.success(`Lesson ${id} re-enabled (validated).`);
1366
+ });
1367
+ }
1368
+ function registerIOCommands(program2) {
1369
+ 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) => {
1370
+ const repoRoot = getRepoRoot();
1371
+ const { lessons } = await readLessons(repoRoot);
1372
+ let filtered = lessons;
1373
+ if (options.since) {
1374
+ const sinceDate = new Date(options.since);
1375
+ if (Number.isNaN(sinceDate.getTime())) {
1376
+ console.error(`Invalid date format: ${options.since}. Use ISO8601 format (e.g., 2024-01-15).`);
1377
+ process.exit(1);
1378
+ }
1379
+ filtered = filtered.filter((lesson) => new Date(lesson.created) >= sinceDate);
1380
+ }
1381
+ if (options.tags) {
1382
+ const filterTags = options.tags.split(",").map((t) => t.trim());
1383
+ filtered = filtered.filter((lesson) => lesson.tags.some((tag) => filterTags.includes(tag)));
1384
+ }
1385
+ console.log(JSON.stringify(filtered, null, JSON_INDENT_SPACES));
1386
+ });
1387
+ program2.command("import <file>").description("Import lessons from a JSONL file").action(async (file) => {
1388
+ const repoRoot = getRepoRoot();
1389
+ let content;
1390
+ try {
1391
+ content = await readFile(file, "utf-8");
1392
+ } catch (err) {
1393
+ const code = err.code;
1394
+ if (code === "ENOENT") {
1395
+ console.error(`Error: File not found: ${file}`);
1396
+ } else {
1397
+ console.error(`Error reading file: ${err.message}`);
1398
+ }
1399
+ process.exit(1);
1400
+ }
1401
+ const { lessons: existingLessons } = await readLessons(repoRoot);
1402
+ const existingIds = new Set(existingLessons.map((l) => l.id));
1403
+ const lines = content.split("\n");
1404
+ let imported = 0;
1405
+ let skipped = 0;
1406
+ let invalid = 0;
1407
+ for (const line of lines) {
1408
+ const trimmed = line.trim();
1409
+ if (!trimmed) continue;
1410
+ let parsed;
1411
+ try {
1412
+ parsed = JSON.parse(trimmed);
1413
+ } catch {
1414
+ invalid++;
1415
+ continue;
1416
+ }
1417
+ const result = LessonSchema.safeParse(parsed);
1418
+ if (!result.success) {
1419
+ invalid++;
1420
+ continue;
1421
+ }
1422
+ const lesson = result.data;
1423
+ if (existingIds.has(lesson.id)) {
1424
+ skipped++;
1425
+ continue;
1426
+ }
1427
+ await appendLesson(repoRoot, lesson);
1428
+ existingIds.add(lesson.id);
1429
+ imported++;
1430
+ }
1431
+ const lessonWord = imported === 1 ? "lesson" : "lessons";
1432
+ const parts = [];
1433
+ if (skipped > 0) parts.push(`${skipped} skipped`);
1434
+ if (invalid > 0) parts.push(`${invalid} invalid`);
1435
+ if (parts.length > 0) {
1436
+ console.log(`Imported ${imported} ${lessonWord} (${parts.join(", ")})`);
1437
+ } else {
1438
+ console.log(`Imported ${imported} ${lessonWord}`);
1439
+ }
1440
+ });
1441
+ }
1442
+ function registerMaintenanceCommands(program2) {
1443
+ 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) => {
1444
+ const repoRoot = getRepoRoot();
1445
+ const tombstones = await countTombstones(repoRoot);
1446
+ const needs = await needsCompaction(repoRoot);
1447
+ if (options.dryRun) {
1448
+ console.log("Dry run - no changes will be made.\n");
1449
+ console.log(`Tombstones found: ${tombstones}`);
1450
+ console.log(`Compaction needed: ${needs ? "yes" : "no"}`);
1451
+ return;
1452
+ }
1453
+ if (!needs && !options.force) {
1454
+ console.log(`Compaction not needed (${tombstones} tombstones, threshold is ${TOMBSTONE_THRESHOLD}).`);
1455
+ console.log("Use --force to compact anyway.");
1456
+ return;
1457
+ }
1458
+ console.log("Running compaction...");
1459
+ const result = await compact(repoRoot);
1460
+ console.log("\nCompaction complete:");
1461
+ console.log(` Archived: ${result.archived} lesson(s)`);
1462
+ console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
1463
+ console.log(` Lessons remaining: ${result.lessonsRemaining}`);
1464
+ await rebuildIndex(repoRoot);
1465
+ console.log(" Index rebuilt.");
1466
+ });
1467
+ program2.command("rebuild").description("Rebuild SQLite index from JSONL").option("-f, --force", "Force rebuild even if unchanged").action(async (options) => {
1468
+ const repoRoot = getRepoRoot();
1469
+ if (options.force) {
1470
+ console.log("Forcing index rebuild...");
1471
+ await rebuildIndex(repoRoot);
1472
+ console.log("Index rebuilt.");
1473
+ } else {
1474
+ const rebuilt = await syncIfNeeded(repoRoot);
1475
+ if (rebuilt) {
1476
+ console.log("Index rebuilt (JSONL changed).");
1477
+ } else {
1478
+ console.log("Index is up to date.");
1479
+ }
989
1480
  }
990
- if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
991
- return {
992
- novel: false,
993
- reason: `Exact duplicate found`,
994
- existingId: lesson.id
995
- };
1481
+ });
1482
+ program2.command("stats").description("Show database health and statistics").action(async () => {
1483
+ const repoRoot = getRepoRoot();
1484
+ await syncIfNeeded(repoRoot);
1485
+ const { lessons } = await readLessons(repoRoot);
1486
+ const deletedCount = await countTombstones(repoRoot);
1487
+ const totalLessons = lessons.length;
1488
+ const retrievalStats = getRetrievalStats(repoRoot);
1489
+ const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
1490
+ const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
1491
+ const jsonlPath = join(repoRoot, LESSONS_PATH);
1492
+ const dbPath = join(repoRoot, DB_PATH);
1493
+ let dataSize = 0;
1494
+ let indexSize = 0;
1495
+ try {
1496
+ dataSize = statSync(jsonlPath).size;
1497
+ } catch {
996
1498
  }
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" };
1499
+ try {
1500
+ indexSize = statSync(dbPath).size;
1501
+ } catch {
1018
1502
  }
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 };
1503
+ const totalSize = dataSize + indexSize;
1504
+ let recentCount = 0;
1505
+ let mediumCount = 0;
1506
+ let oldCount = 0;
1507
+ for (const lesson of lessons) {
1508
+ const ageDays = getLessonAgeDays(lesson);
1509
+ if (ageDays < 30) {
1510
+ recentCount++;
1511
+ } else if (ageDays <= AGE_FLAG_THRESHOLD_DAYS) {
1512
+ mediumCount++;
1513
+ } else {
1514
+ oldCount++;
1515
+ }
1045
1516
  }
1046
- }
1047
- return { actionable: false, reason: "Insight lacks clear action guidance" };
1517
+ const deletedInfo = deletedCount > 0 ? ` (${deletedCount} deleted)` : "";
1518
+ console.log(`Lessons: ${totalLessons} total${deletedInfo}`);
1519
+ if (totalLessons > LESSON_COUNT_WARNING_THRESHOLD) {
1520
+ out.warn(`High lesson count may degrade retrieval quality. Consider running \`lna compact\`.`);
1521
+ }
1522
+ if (totalLessons > 0) {
1523
+ console.log(`Age: ${recentCount} <30d, ${mediumCount} 30-90d, ${oldCount} >90d`);
1524
+ }
1525
+ console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
1526
+ console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
1527
+ });
1048
1528
  }
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 };
1529
+
1530
+ // src/commands/management/prime.ts
1531
+ var PRIME_WORKFLOW_CONTEXT = `# Learning Agent Workflow
1532
+
1533
+ ## Core Rules
1534
+ - **NEVER** edit .claude/lessons/index.jsonl directly
1535
+ - Use CLI commands: \`lna learn\`, \`lna list\`, \`lna show\`
1536
+ - Lessons load automatically at session start
1537
+
1538
+ ## When to Capture Lessons
1539
+ - User corrects you ("no", "wrong", "actually...")
1540
+ - You self-correct after multiple attempts
1541
+ - Test fails then you fix it
1542
+
1543
+ ## Commands
1544
+ - \`lna learn "insight"\` - Capture a lesson
1545
+ - \`lna list\` - Show all lessons
1546
+ - \`lna check-plan --plan "..."\` - Get relevant lessons for plan
1547
+ - \`lna stats\` - Show database health
1548
+
1549
+ ## Quality Gate (ALL must pass before proposing)
1550
+ - Novel (not already stored)
1551
+ - Specific (clear guidance)
1552
+ - Actionable (obvious what to do)
1553
+ `;
1554
+ function registerPrimeCommand(program2) {
1555
+ program2.command("prime").description("Output workflow context for Claude Code").action(() => {
1556
+ console.log(PRIME_WORKFLOW_CONTEXT);
1557
+ });
1063
1558
  }
1064
1559
 
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;
1560
+ // src/commands/management/index.ts
1561
+ function registerManagementCommands(program2) {
1562
+ registerInvalidationCommands(program2);
1563
+ registerMaintenanceCommands(program2);
1564
+ registerIOCommands(program2);
1565
+ registerPrimeCommand(program2);
1566
+ registerCrudCommands(program2);
1567
+ }
1568
+ var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
1569
+ var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
1570
+ var DEFAULT_MODEL_DIR = join(homedir(), ".node-llama-cpp", "models");
1571
+ function isModelAvailable() {
1572
+ return existsSync(join(DEFAULT_MODEL_DIR, MODEL_FILENAME));
1573
+ }
1574
+ async function resolveModel(options = {}) {
1575
+ const { cli = true } = options;
1576
+ return resolveModelFile(MODEL_URI, { cli });
1577
+ }
1578
+
1579
+ // src/embeddings/nomic.ts
1580
+ var embeddingContext = null;
1581
+ async function getEmbedding() {
1582
+ if (embeddingContext) return embeddingContext;
1583
+ const modelPath = await resolveModel({ cli: true });
1584
+ const llama = await getLlama();
1585
+ const model = await llama.loadModel({ modelPath });
1586
+ embeddingContext = await model.createEmbeddingContext();
1587
+ return embeddingContext;
1588
+ }
1589
+ async function embedText(text) {
1590
+ const ctx = await getEmbedding();
1591
+ const result = await ctx.getEmbeddingFor(text);
1592
+ return Array.from(result.vector);
1593
+ }
1594
+
1595
+ // src/search/vector.ts
1596
+ function cosineSimilarity(a, b) {
1597
+ if (a.length !== b.length) {
1598
+ throw new Error("Vectors must have same length");
1082
1599
  }
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
- }
1600
+ let dotProduct = 0;
1601
+ let normA = 0;
1602
+ let normB = 0;
1603
+ for (let i = 0; i < a.length; i++) {
1604
+ dotProduct += a[i] * b[i];
1605
+ normA += a[i] * a[i];
1606
+ normB += b[i] * b[i];
1095
1607
  }
1096
- return null;
1608
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
1609
+ if (magnitude === 0) return 0;
1610
+ return dotProduct / magnitude;
1097
1611
  }
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
- };
1612
+ var DEFAULT_LIMIT = 10;
1613
+ async function searchVector(repoRoot, query, options) {
1614
+ const limit = options?.limit ?? DEFAULT_LIMIT;
1615
+ const { lessons } = await readLessons(repoRoot);
1616
+ if (lessons.length === 0) return [];
1617
+ const queryVector = await embedText(query);
1618
+ const scored = [];
1619
+ for (const lesson of lessons) {
1620
+ if (lesson.invalidatedAt) continue;
1621
+ const lessonText = `${lesson.trigger} ${lesson.insight}`;
1622
+ const hash = contentHash(lesson.trigger, lesson.insight);
1623
+ let lessonVector = getCachedEmbedding(repoRoot, lesson.id, hash);
1624
+ if (!lessonVector) {
1625
+ lessonVector = await embedText(lessonText);
1626
+ setCachedEmbedding(repoRoot, lesson.id, lessonVector, hash);
1113
1627
  }
1628
+ const score = cosineSimilarity(queryVector, lessonVector);
1629
+ scored.push({ lesson, score });
1114
1630
  }
1115
- return null;
1631
+ scored.sort((a, b) => b.score - a.score);
1632
+ return scored.slice(0, limit);
1116
1633
  }
1117
- function detectTestFailure(testResult) {
1118
- if (testResult.passed) {
1119
- return null;
1634
+
1635
+ // src/search/ranking.ts
1636
+ var RECENCY_THRESHOLD_DAYS = 30;
1637
+ var HIGH_SEVERITY_BOOST = 1.5;
1638
+ var MEDIUM_SEVERITY_BOOST = 1;
1639
+ var LOW_SEVERITY_BOOST = 0.8;
1640
+ var RECENCY_BOOST = 1.2;
1641
+ var CONFIRMATION_BOOST = 1.3;
1642
+ function severityBoost(lesson) {
1643
+ switch (lesson.severity) {
1644
+ case "high":
1645
+ return HIGH_SEVERITY_BOOST;
1646
+ case "medium":
1647
+ return MEDIUM_SEVERITY_BOOST;
1648
+ case "low":
1649
+ return LOW_SEVERITY_BOOST;
1650
+ default:
1651
+ return MEDIUM_SEVERITY_BOOST;
1120
1652
  }
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
- };
1653
+ }
1654
+ function recencyBoost(lesson) {
1655
+ const ageDays = getLessonAgeDays(lesson);
1656
+ return ageDays <= RECENCY_THRESHOLD_DAYS ? RECENCY_BOOST : 1;
1657
+ }
1658
+ function confirmationBoost(lesson) {
1659
+ return lesson.confirmed ? CONFIRMATION_BOOST : 1;
1660
+ }
1661
+ function calculateScore(lesson, vectorSimilarity) {
1662
+ return vectorSimilarity * severityBoost(lesson) * recencyBoost(lesson) * confirmationBoost(lesson);
1663
+ }
1664
+ function rankLessons(lessons) {
1665
+ return lessons.map((scored) => ({
1666
+ ...scored,
1667
+ finalScore: calculateScore(scored.lesson, scored.score)
1668
+ })).sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
1128
1669
  }
1129
1670
 
1130
1671
  // src/retrieval/session.ts
@@ -1135,7 +1676,7 @@ function isFullLesson(lesson) {
1135
1676
  async function loadSessionLessons(repoRoot, limit = DEFAULT_LIMIT2) {
1136
1677
  const { lessons: allLessons } = await readLessons(repoRoot);
1137
1678
  const highSeverityLessons = allLessons.filter(
1138
- (lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed
1679
+ (lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed && !lesson.invalidatedAt
1139
1680
  );
1140
1681
  highSeverityLessons.sort((a, b) => {
1141
1682
  const dateA = new Date(a.created).getTime();
@@ -1155,7 +1696,7 @@ async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
1155
1696
  return { lessons: topLessons, message };
1156
1697
  }
1157
1698
  function formatLessonsCheck(lessons) {
1158
- const header = "\u{1F4DA} Lessons Check\n" + "\u2500".repeat(40);
1699
+ const header = "Lessons Check\n" + "\u2500".repeat(40);
1159
1700
  if (lessons.length === 0) {
1160
1701
  return `${header}
1161
1702
  No relevant lessons found for this plan.`;
@@ -1170,615 +1711,772 @@ ${lessonLines.join("\n")}`;
1170
1711
  }
1171
1712
 
1172
1713
  // src/index.ts
1173
- var VERSION = "0.1.0";
1714
+ var VERSION = "0.2.3";
1174
1715
 
1175
- // src/cli/commands/download-model.ts
1176
- function registerDownloadModelCommand(program2) {
1177
- program2.command("download-model").description("Download the embedding model for semantic search").option("--json", "Output as JSON").action(async (options) => {
1178
- const alreadyExisted = isModelAvailable();
1179
- if (alreadyExisted) {
1180
- const modelPath2 = join(homedir(), ".node-llama-cpp", "models", MODEL_FILENAME);
1181
- const size2 = statSync(modelPath2).size;
1182
- if (options.json) {
1183
- console.log(JSON.stringify({ success: true, path: modelPath2, size: size2, alreadyExisted: true }));
1184
- } else {
1185
- console.log("Model already exists.");
1186
- console.log(`Path: ${modelPath2}`);
1187
- console.log(`Size: ${formatBytes(size2)}`);
1188
- }
1189
- return;
1190
- }
1191
- if (!options.json) {
1192
- console.log("Downloading embedding model...");
1193
- }
1194
- const modelPath = await resolveModel({ cli: !options.json });
1195
- const size = statSync(modelPath).size;
1196
- if (options.json) {
1197
- console.log(JSON.stringify({ success: true, path: modelPath, size, alreadyExisted: false }));
1198
- } else {
1199
- console.log(`
1200
- Model downloaded successfully!`);
1201
- console.log(`Path: ${modelPath}`);
1202
- console.log(`Size: ${formatBytes(size)}`);
1716
+ // src/commands/retrieval.ts
1717
+ async function readPlanFromStdin() {
1718
+ const { stdin } = await import('process');
1719
+ if (!stdin.isTTY) {
1720
+ const chunks = [];
1721
+ for await (const chunk of stdin) {
1722
+ chunks.push(chunk);
1203
1723
  }
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}`);
1724
+ return Buffer.concat(chunks).toString("utf-8").trim();
1223
1725
  }
1224
- lines.push(`Created: ${lesson.created}`);
1225
- lines.push(`Confirmed: ${lesson.confirmed}`);
1226
- if (lesson.supersedes.length > 0) {
1227
- lines.push(`Supersedes: ${lesson.supersedes.join(", ")}`);
1726
+ return void 0;
1727
+ }
1728
+ function outputCheckPlanJson(lessons) {
1729
+ const jsonOutput = {
1730
+ lessons: lessons.map((l) => ({
1731
+ id: l.lesson.id,
1732
+ insight: l.lesson.insight,
1733
+ relevance: l.score,
1734
+ source: l.lesson.source
1735
+ })),
1736
+ count: lessons.length
1737
+ };
1738
+ console.log(JSON.stringify(jsonOutput));
1739
+ }
1740
+ function outputCheckPlanHuman(lessons, quiet) {
1741
+ console.log("## Lessons Check\n");
1742
+ console.log("Relevant to your plan:\n");
1743
+ lessons.forEach((item, i) => {
1744
+ const num = i + 1;
1745
+ console.log(`${num}. ${chalk.bold(`[${item.lesson.id}]`)} ${item.lesson.insight}`);
1746
+ console.log(` - Relevance: ${item.score.toFixed(RELEVANCE_DECIMAL_PLACES)}`);
1747
+ console.log(` - Source: ${item.lesson.source}`);
1748
+ console.log();
1749
+ });
1750
+ if (!quiet) {
1751
+ console.log("---");
1752
+ console.log("Consider these lessons while implementing.");
1228
1753
  }
1229
- if (lesson.related.length > 0) {
1230
- lines.push(`Related: ${lesson.related.join(", ")}`);
1754
+ }
1755
+ function formatSource(source) {
1756
+ return source.replace(/_/g, " ");
1757
+ }
1758
+ function outputSessionLessonsHuman(lessons, quiet) {
1759
+ console.log("## Lessons from Past Sessions\n");
1760
+ console.log("These lessons were captured from previous corrections and should inform your work:\n");
1761
+ lessons.forEach((lesson, i) => {
1762
+ const num = i + 1;
1763
+ const date = lesson.created.slice(0, ISO_DATE_PREFIX_LENGTH);
1764
+ const tagsDisplay = lesson.tags.length > 0 ? ` (${lesson.tags.join(", ")})` : "";
1765
+ console.log(`${num}. **${lesson.insight}**${tagsDisplay}`);
1766
+ console.log(` Learned: ${date} via ${formatSource(lesson.source)}`);
1767
+ console.log();
1768
+ });
1769
+ if (!quiet) {
1770
+ console.log("Consider these lessons when planning and implementing tasks.");
1231
1771
  }
1232
- return lines.join("\n");
1233
1772
  }
1234
- function registerShowCommand(program2) {
1235
- program2.command("show <id>").description("Show details of a specific lesson").option("--json", "Output as JSON").action(async (id, options) => {
1773
+ function registerRetrievalCommands(program2) {
1774
+ program2.command("search <query>").description("Search lessons by keyword").option("-n, --limit <number>", "Maximum results", DEFAULT_SEARCH_LIMIT).action(async function(query, options) {
1236
1775
  const repoRoot = getRepoRoot();
1237
- const { lessons } = await readLessons(repoRoot);
1238
- const lesson = lessons.find((l) => l.id === id);
1239
- if (!lesson) {
1240
- const filePath = join(repoRoot, LESSONS_PATH);
1241
- let wasDeleted = false;
1242
- try {
1243
- const content = await readFile(filePath, "utf-8");
1244
- const lines = content.split("\n");
1245
- for (const line of lines) {
1246
- const trimmed = line.trim();
1247
- if (!trimmed) continue;
1248
- try {
1249
- const record = JSON.parse(trimmed);
1250
- if (record.id === id && record.deleted === true) {
1251
- wasDeleted = true;
1252
- break;
1253
- }
1254
- } catch {
1255
- }
1256
- }
1257
- } catch {
1776
+ const limit = parseLimit(options.limit, "limit");
1777
+ const { verbose, quiet } = getGlobalOpts(this);
1778
+ await syncIfNeeded(repoRoot);
1779
+ const results = await searchKeyword(repoRoot, query, limit);
1780
+ if (results.length === 0) {
1781
+ console.log('No lessons match your search. Try a different query or use "list" to see all lessons.');
1782
+ return;
1783
+ }
1784
+ if (!quiet) {
1785
+ out.info(`Found ${results.length} lesson(s):
1786
+ `);
1787
+ }
1788
+ for (const lesson of results) {
1789
+ console.log(`[${chalk.cyan(lesson.id)}] ${lesson.insight}`);
1790
+ console.log(` Trigger: ${lesson.trigger}`);
1791
+ if (verbose && lesson.context) {
1792
+ console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
1793
+ console.log(` Created: ${lesson.created}`);
1258
1794
  }
1259
- if (options.json) {
1260
- console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found` }));
1261
- } else {
1262
- out.error(wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found`);
1795
+ if (lesson.tags.length > 0) {
1796
+ console.log(` Tags: ${lesson.tags.join(", ")}`);
1263
1797
  }
1264
- process.exit(1);
1265
- }
1266
- if (options.json) {
1267
- console.log(JSON.stringify(lesson, null, SHOW_JSON_INDENT));
1268
- } else {
1269
- console.log(formatLessonHuman(lesson));
1798
+ console.log();
1270
1799
  }
1271
1800
  });
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) => {
1801
+ 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
1802
  const repoRoot = getRepoRoot();
1277
- const hasUpdates = options.insight !== void 0 || options.trigger !== void 0 || options.evidence !== void 0 || options.severity !== void 0 || options.tags !== void 0 || options.confirmed !== void 0;
1278
- if (!hasUpdates) {
1279
- if (options.json) {
1280
- console.log(JSON.stringify({ error: "No fields to update (specify at least one: --insight, --tags, --severity, ...)" }));
1281
- } else {
1282
- out.error("No fields to update (specify at least one: --insight, --tags, --severity, ...)");
1283
- }
1284
- process.exit(1);
1285
- }
1286
- const { lessons } = await readLessons(repoRoot);
1287
- const lesson = lessons.find((l) => l.id === id);
1288
- if (!lesson) {
1289
- const filePath = join(repoRoot, LESSONS_PATH);
1290
- let wasDeleted = false;
1291
- try {
1292
- const content = await readFile(filePath, "utf-8");
1293
- const lines = content.split("\n");
1294
- for (const line of lines) {
1295
- const trimmed = line.trim();
1296
- if (!trimmed) continue;
1297
- try {
1298
- const record = JSON.parse(trimmed);
1299
- if (record.id === id && record.deleted === true) {
1300
- wasDeleted = true;
1301
- break;
1302
- }
1303
- } catch {
1304
- }
1305
- }
1306
- } catch {
1307
- }
1308
- if (options.json) {
1309
- console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found` }));
1803
+ const limit = parseLimit(options.limit, "limit");
1804
+ const { verbose, quiet } = getGlobalOpts(this);
1805
+ const { lessons, skippedCount } = await readLessons(repoRoot);
1806
+ const filteredLessons = options.invalidated ? lessons.filter((l) => l.invalidatedAt) : lessons;
1807
+ if (filteredLessons.length === 0) {
1808
+ if (options.invalidated) {
1809
+ console.log("No invalidated lessons found.");
1310
1810
  } else {
1311
- out.error(wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found`);
1312
- }
1313
- process.exit(1);
1314
- }
1315
- if (options.severity !== void 0) {
1316
- const result = SeveritySchema.safeParse(options.severity);
1317
- if (!result.success) {
1318
- if (options.json) {
1319
- console.log(JSON.stringify({ error: `Invalid severity '${options.severity}' (must be: high, medium, low)` }));
1320
- } else {
1321
- out.error(`Invalid severity '${options.severity}' (must be: high, medium, low)`);
1322
- }
1323
- process.exit(1);
1811
+ console.log('No lessons found. Get started with: learn "Your first lesson"');
1324
1812
  }
1325
- }
1326
- const updatedLesson = {
1327
- ...lesson,
1328
- ...options.insight !== void 0 && { insight: options.insight },
1329
- ...options.trigger !== void 0 && { trigger: options.trigger },
1330
- ...options.evidence !== void 0 && { evidence: options.evidence },
1331
- ...options.severity !== void 0 && { severity: options.severity },
1332
- ...options.tags !== void 0 && {
1333
- tags: [...new Set(
1334
- options.tags.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
1335
- )]
1336
- },
1337
- ...options.confirmed !== void 0 && { confirmed: options.confirmed === "true" }
1338
- };
1339
- const validationResult = LessonSchema.safeParse(updatedLesson);
1340
- if (!validationResult.success) {
1341
- if (options.json) {
1342
- console.log(JSON.stringify({ error: `Schema validation failed: ${validationResult.error.message}` }));
1343
- } else {
1344
- out.error(`Schema validation failed: ${validationResult.error.message}`);
1813
+ if (skippedCount > 0) {
1814
+ out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
1345
1815
  }
1346
- process.exit(1);
1816
+ return;
1347
1817
  }
1348
- await appendLesson(repoRoot, updatedLesson);
1349
- await syncIfNeeded(repoRoot);
1350
- if (options.json) {
1351
- console.log(JSON.stringify(updatedLesson, null, SHOW_JSON_INDENT2));
1352
- } else {
1353
- out.success(`Updated lesson ${id}`);
1818
+ const toShow = filteredLessons.slice(0, limit);
1819
+ if (!quiet) {
1820
+ const label = options.invalidated ? "invalidated lesson(s)" : "lesson(s)";
1821
+ out.info(`Showing ${toShow.length} of ${filteredLessons.length} ${label}:
1822
+ `);
1354
1823
  }
1355
- });
1356
- }
1357
- async function wasLessonDeleted(repoRoot, id) {
1358
- const filePath = join(repoRoot, LESSONS_PATH);
1359
- try {
1360
- const content = await readFile(filePath, "utf-8");
1361
- const lines = content.split("\n");
1362
- for (const line of lines) {
1363
- const trimmed = line.trim();
1364
- if (!trimmed) continue;
1365
- try {
1366
- const record = JSON.parse(trimmed);
1367
- if (record.id === id && record.deleted === true) {
1368
- return true;
1824
+ for (const lesson of toShow) {
1825
+ const invalidMarker = lesson.invalidatedAt ? chalk.red("[INVALID] ") : "";
1826
+ console.log(`[${chalk.cyan(lesson.id)}] ${invalidMarker}${lesson.insight}`);
1827
+ if (verbose) {
1828
+ console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
1829
+ console.log(` Created: ${lesson.created}`);
1830
+ if (lesson.context) {
1831
+ console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
1369
1832
  }
1370
- } catch {
1371
- }
1372
- }
1373
- } catch {
1374
- }
1375
- return false;
1376
- }
1377
- function registerDeleteCommand(program2) {
1378
- program2.command("delete <ids...>").description("Soft delete lessons (creates tombstone)").option("--json", "Output as JSON").action(async (ids, options) => {
1379
- const repoRoot = getRepoRoot();
1380
- const { lessons } = await readLessons(repoRoot);
1381
- const lessonMap = new Map(lessons.map((l) => [l.id, l]));
1382
- const deleted = [];
1383
- const warnings = [];
1384
- for (const id of ids) {
1385
- const lesson = lessonMap.get(id);
1386
- if (!lesson) {
1387
- const wasDeleted = await wasLessonDeleted(repoRoot, id);
1388
- warnings.push({ id, message: wasDeleted ? "already deleted" : "not found" });
1389
- continue;
1833
+ if (lesson.invalidatedAt) {
1834
+ console.log(` Invalidated: ${lesson.invalidatedAt}`);
1835
+ if (lesson.invalidationReason) {
1836
+ console.log(` Reason: ${lesson.invalidationReason}`);
1837
+ }
1838
+ }
1839
+ } else {
1840
+ console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
1390
1841
  }
1391
- const tombstone = {
1392
- ...lesson,
1393
- deleted: true,
1394
- deletedAt: (/* @__PURE__ */ new Date()).toISOString()
1395
- };
1396
- await appendLesson(repoRoot, tombstone);
1397
- deleted.push(id);
1842
+ if (lesson.tags.length > 0) {
1843
+ console.log(` Tags: ${lesson.tags.join(", ")}`);
1844
+ }
1845
+ console.log();
1398
1846
  }
1399
- if (deleted.length > 0) {
1400
- await syncIfNeeded(repoRoot);
1847
+ if (skippedCount > 0) {
1848
+ out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
1401
1849
  }
1850
+ });
1851
+ program2.command("load-session").description("Load high-severity lessons for session context").option("--json", "Output as JSON").action(async function(options) {
1852
+ const repoRoot = getRepoRoot();
1853
+ const { quiet } = getGlobalOpts(this);
1854
+ const lessons = await loadSessionLessons(repoRoot);
1855
+ const { lessons: allLessons } = await readLessons(repoRoot);
1856
+ const totalCount = allLessons.length;
1402
1857
  if (options.json) {
1403
- console.log(JSON.stringify({ deleted, warnings }));
1404
- } else {
1405
- if (deleted.length > 0) {
1406
- out.success(`Deleted ${deleted.length} lesson(s): ${deleted.join(", ")}`);
1407
- }
1408
- for (const warning of warnings) {
1409
- out.warn(`${warning.id}: ${warning.message}`);
1410
- }
1411
- if (deleted.length === 0 && warnings.length > 0) {
1412
- process.exit(1);
1413
- }
1858
+ console.log(JSON.stringify({ lessons, count: lessons.length, totalCount }));
1859
+ return;
1860
+ }
1861
+ if (lessons.length === 0) {
1862
+ console.log("No high-severity lessons found.");
1863
+ return;
1864
+ }
1865
+ outputSessionLessonsHuman(lessons, quiet);
1866
+ if (totalCount > LESSON_COUNT_WARNING_THRESHOLD) {
1867
+ console.log("");
1868
+ out.info(`${totalCount} lessons in index. Consider \`lna compact\` to reduce context pollution.`);
1869
+ }
1870
+ const oldLessons = lessons.filter((l) => getLessonAgeDays(l) > AGE_FLAG_THRESHOLD_DAYS);
1871
+ if (oldLessons.length > 0) {
1872
+ console.log("");
1873
+ out.warn(`${oldLessons.length} lesson(s) are over ${AGE_FLAG_THRESHOLD_DAYS} days old. Review for continued validity.`);
1414
1874
  }
1415
1875
  });
1416
- }
1417
- function registerLearnCommand(program2) {
1418
- program2.command("learn <insight>").description("Capture a new lesson").option("-t, --trigger <text>", "What triggered this lesson").option("--tags <tags>", "Comma-separated tags", "").option("-s, --severity <level>", "Lesson severity: high, medium, low").option("-y, --yes", "Skip confirmation").action(async function(insight, options) {
1876
+ 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) {
1419
1877
  const repoRoot = getRepoRoot();
1878
+ const limit = parseLimit(options.limit, "limit");
1420
1879
  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);
1880
+ const planText = options.plan ?? await readPlanFromStdin();
1881
+ if (!planText) {
1882
+ out.error("No plan provided. Use --plan <text> or pipe text to stdin.");
1883
+ process.exit(1);
1884
+ }
1885
+ if (!isModelAvailable()) {
1886
+ if (options.json) {
1887
+ console.log(JSON.stringify({
1888
+ error: "Embedding model not available",
1889
+ action: "Run: npx lna download-model"
1890
+ }));
1891
+ } else {
1892
+ out.error("Embedding model not available");
1893
+ console.log("");
1894
+ console.log("Run: npx lna download-model");
1427
1895
  }
1428
- severity = result.data;
1896
+ process.exit(1);
1429
1897
  }
1430
- const lessonType = severity !== void 0 ? "full" : "quick";
1431
- const lesson = {
1432
- id: generateId(insight),
1433
- type: lessonType,
1434
- trigger: options.trigger ?? "Manual capture",
1435
- insight,
1436
- tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : [],
1437
- source: "manual",
1438
- context: {
1439
- tool: "cli",
1440
- intent: "manual learning"
1441
- },
1442
- created: (/* @__PURE__ */ new Date()).toISOString(),
1443
- confirmed: true,
1444
- // learn command is explicit confirmation
1445
- supersedes: [],
1446
- related: [],
1447
- ...severity !== void 0 && { severity }
1448
- };
1449
- await appendLesson(repoRoot, lesson);
1450
- out.success(`Learned: ${insight}`);
1451
- if (!quiet) {
1452
- console.log(`ID: ${chalk.dim(lesson.id)}`);
1898
+ try {
1899
+ const result = await retrieveForPlan(repoRoot, planText, limit);
1900
+ if (options.json) {
1901
+ outputCheckPlanJson(result.lessons);
1902
+ return;
1903
+ }
1904
+ if (result.lessons.length === 0) {
1905
+ console.log("No relevant lessons found for this plan.");
1906
+ return;
1907
+ }
1908
+ outputCheckPlanHuman(result.lessons, quiet);
1909
+ } catch (err) {
1910
+ const message = err instanceof Error ? err.message : "Unknown error";
1911
+ if (options.json) {
1912
+ console.log(JSON.stringify({ error: message }));
1913
+ } else {
1914
+ out.error(`Failed to check plan: ${message}`);
1915
+ }
1916
+ process.exit(1);
1917
+ }
1918
+ });
1919
+ }
1920
+
1921
+ // src/commands/setup/templates.ts
1922
+ var PRE_COMMIT_MESSAGE = `Before committing, have you captured any valuable lessons from this session?
1923
+ Consider: corrections, mistakes, or insights worth remembering.
1924
+
1925
+ To capture a lesson:
1926
+ npx lna capture --trigger "what happened" --insight "what to do" --yes`;
1927
+ var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
1928
+ # Learning Agent pre-commit hook
1929
+ # Reminds Claude to consider capturing lessons before commits
1930
+
1931
+ npx lna hooks run pre-commit
1932
+ `;
1933
+ var HOOK_MARKER = "# Learning Agent pre-commit hook";
1934
+ var LEARNING_AGENT_HOOK_BLOCK = `
1935
+ # Learning Agent pre-commit hook (appended)
1936
+ npx lna hooks run pre-commit
1937
+ `;
1938
+ var CLAUDE_HOOK_MARKERS = ["lna load-session", "learning-agent load-session"];
1939
+ var CLAUDE_HOOK_CONFIG = {
1940
+ matcher: "startup|resume|compact",
1941
+ hooks: [
1942
+ {
1943
+ type: "command",
1944
+ command: "npx lna load-session 2>/dev/null || true"
1453
1945
  }
1946
+ ]
1947
+ };
1948
+ var LEARNING_AGENT_SECTION_HEADER = "## Learning Agent Integration";
1949
+ var CLAUDE_REF_START_MARKER = "<!-- learning-agent:claude-ref:start -->";
1950
+ var CLAUDE_REF_END_MARKER = "<!-- learning-agent:claude-ref:end -->";
1951
+ var CLAUDE_MD_REFERENCE = `
1952
+ ${CLAUDE_REF_START_MARKER}
1953
+ ## Learning Agent
1954
+ See AGENTS.md for lesson capture workflow.
1955
+ ${CLAUDE_REF_END_MARKER}
1956
+ `;
1957
+ var AGENTS_SECTION_START_MARKER = "<!-- learning-agent:start -->";
1958
+ var AGENTS_SECTION_END_MARKER = "<!-- learning-agent:end -->";
1959
+ var AGENTS_MD_TEMPLATE = `
1960
+ ${AGENTS_SECTION_START_MARKER}
1961
+ ## Learning Agent Integration
1962
+
1963
+ This project uses learning-agent for session memory.
1964
+
1965
+ ### CRITICAL RULES
1966
+
1967
+ #### Never Edit JSONL Directly
1968
+
1969
+ **WARNING: NEVER edit .claude/lessons/index.jsonl directly.**
1970
+
1971
+ The JSONL file is the source of truth and requires:
1972
+ - Proper ID generation
1973
+ - Schema validation
1974
+ - SQLite index sync
1975
+
1976
+ Always use CLI commands:
1977
+ - \`npx lna learn "insight"\` - Add a lesson
1978
+ - \`npx lna update <id> --insight "new"\` - Modify a lesson
1979
+ - \`npx lna delete <id>\` - Remove a lesson
1980
+
1981
+ Manual edits will break validation and corrupt the SQLite sync.
1982
+
1983
+ ### Retrieval Points
1984
+
1985
+ - **Session start**: High-severity lessons loaded automatically
1986
+ - **Plan-time**: BEFORE implementing a plan, run check-plan to retrieve relevant lessons
1987
+
1988
+ ### Plan-Time Retrieval (Explicit Step)
1989
+
1990
+ **BEFORE implementing any plan**, run:
1991
+
1992
+ \`\`\`bash
1993
+ npx lna check-plan --plan "your plan description" --json
1994
+ \`\`\`
1995
+
1996
+ Display results as a **Lessons Check** section after your plan:
1997
+
1998
+ \`\`\`
1999
+ ## Lessons Check
2000
+ 1. [insight from lesson 1] (relevance: 0.85)
2001
+ 2. [insight from lesson 2] (relevance: 0.72)
2002
+ \`\`\`
2003
+
2004
+ Consider each lesson while implementing.
2005
+
2006
+ ### When to Capture Lessons (Detection Triggers)
2007
+
2008
+ Watch for these patterns and propose \`lna learn\`:
2009
+
2010
+ **User correction**: User says "no", "wrong", "actually..."
2011
+ - Action: Propose a lesson capturing the correct approach
2012
+
2013
+ **Self-correction**: You fix after multiple attempts (edit -> fail -> re-edit)
2014
+ - Action: Propose a lesson about what finally worked
2015
+
2016
+ **Test failure fix**: Test fails -> you diagnose -> fix -> passes
2017
+ - Action: Propose a lesson about the root cause and solution
2018
+
2019
+ ### Auto-Invoke Trigger Phrases
2020
+
2021
+ **Capture triggers** (propose \`lna learn\`):
2022
+ - "that worked" - User confirms a solution worked
2023
+ - "fixed it" - Problem was resolved
2024
+ - "my mistake" - User acknowledges an error
2025
+ - "actually use X" - User specifies a preference
2026
+
2027
+ **Retrieval triggers** (run \`lna check-plan\` or \`lna search\`):
2028
+ - "similar issue" - User recalls a past problem
2029
+ - "we had this before" - Reference to previous experience
2030
+ - "seen this" - Pattern recognition
2031
+ - "remember when" - Memory recall request
2032
+
2033
+ ### Proposing Lessons
2034
+
2035
+ Propose when: user correction, self-correction, test failure fix, or manual request.
2036
+
2037
+ **Quality gate (ALL must pass):**
2038
+
2039
+ - Novel (not already stored)
2040
+ - Specific (clear guidance)
2041
+ - Actionable (obvious what to do)
2042
+
2043
+ **Confirmation format:**
2044
+
2045
+ \`\`\`
2046
+ Learned: [insight]. Save? [y/n]
2047
+ \`\`\`
2048
+
2049
+ ### Session-End Protocol
2050
+
2051
+ Before closing a session, reflect on lessons learned:
2052
+
2053
+ 1. **Review**: What mistakes or corrections happened?
2054
+ 2. **Quality gate**: Is it novel, specific, actionable?
2055
+ 3. **Propose**: "Learned: [insight]. Save? [y/n]"
2056
+ 4. **Capture**: \`npx lna capture --trigger "..." --insight "..." --yes\`
2057
+
2058
+ ### CLI Commands
2059
+
2060
+ \`\`\`bash
2061
+ npx lna load-session --json # Session start
2062
+ npx lna check-plan --plan "..." --json # Before implementing
2063
+ npx lna learn "insight" # Capture a lesson
2064
+ npx lna capture --trigger "..." --insight "..." --yes
2065
+ \`\`\`
2066
+
2067
+ See [AGENTS.md](https://github.com/Nathandela/learning_agent/blob/main/AGENTS.md) for full documentation.
2068
+ ${AGENTS_SECTION_END_MARKER}
2069
+ `;
2070
+ var SLASH_COMMANDS = {
2071
+ "learn.md": `Capture a lesson from this session.
2072
+
2073
+ Usage: /learn <insight>
2074
+
2075
+ Examples:
2076
+ - /learn "Always use Polars for large CSV files"
2077
+ - /learn "API requires X-Request-ID header"
2078
+
2079
+ \`\`\`bash
2080
+ npx lna learn "$ARGUMENTS"
2081
+ \`\`\`
2082
+ `,
2083
+ "check-plan.md": `Retrieve relevant lessons for a plan before implementing.
2084
+
2085
+ Usage: /check-plan <plan description>
2086
+
2087
+ \`\`\`bash
2088
+ npx lna check-plan --plan "$ARGUMENTS" --json
2089
+ \`\`\`
2090
+ `,
2091
+ "list.md": `Show all stored lessons.
2092
+
2093
+ \`\`\`bash
2094
+ npx lna list
2095
+ \`\`\`
2096
+ `,
2097
+ "prime.md": `Load learning-agent workflow context after compaction or context loss.
2098
+
2099
+ \`\`\`bash
2100
+ npx lna prime
2101
+ \`\`\`
2102
+ `,
2103
+ "show.md": `Show details of a specific lesson.
2104
+
2105
+ Usage: /show <lesson-id>
2106
+
2107
+ \`\`\`bash
2108
+ npx lna show "$ARGUMENTS"
2109
+ \`\`\`
2110
+ `,
2111
+ "wrong.md": `Mark a lesson as incorrect or invalid.
2112
+
2113
+ Usage: /wrong <lesson-id>
2114
+
2115
+ \`\`\`bash
2116
+ npx lna wrong "$ARGUMENTS"
2117
+ \`\`\`
2118
+ `,
2119
+ "stats.md": `Show learning-agent database statistics and health.
2120
+
2121
+ \`\`\`bash
2122
+ npx lna stats
2123
+ \`\`\`
2124
+ `
2125
+ };
2126
+ var PLUGIN_MANIFEST = {
2127
+ name: "learning-agent",
2128
+ description: "Session memory for Claude Code - capture and retrieve lessons",
2129
+ version: "0.2.2",
2130
+ author: {
2131
+ name: "Nathan Delacr\xE9taz",
2132
+ url: "https://github.com/Nathandela"
2133
+ },
2134
+ repository: "https://github.com/Nathandela/learning_agent",
2135
+ license: "MIT",
2136
+ hooks: {
2137
+ SessionStart: [
2138
+ {
2139
+ matcher: "",
2140
+ hooks: [
2141
+ { type: "command", command: "npx lna prime 2>/dev/null || true" },
2142
+ { type: "command", command: "npx lna load-session 2>/dev/null || true" }
2143
+ ]
2144
+ }
2145
+ ],
2146
+ PreCompact: [
2147
+ {
2148
+ matcher: "",
2149
+ hooks: [{ type: "command", command: "npx lna prime 2>/dev/null || true" }]
2150
+ }
2151
+ ]
2152
+ }
2153
+ };
2154
+
2155
+ // src/commands/setup/claude-helpers.ts
2156
+ function getClaudeSettingsPath(global) {
2157
+ if (global) {
2158
+ return join(homedir(), ".claude", "settings.json");
2159
+ }
2160
+ const repoRoot = getRepoRoot();
2161
+ return join(repoRoot, ".claude", "settings.json");
2162
+ }
2163
+ async function readClaudeSettings(settingsPath) {
2164
+ if (!existsSync(settingsPath)) {
2165
+ return {};
2166
+ }
2167
+ const content = await readFile(settingsPath, "utf-8");
2168
+ return JSON.parse(content);
2169
+ }
2170
+ function hasClaudeHook(settings) {
2171
+ const hooks = settings.hooks;
2172
+ if (!hooks?.SessionStart) return false;
2173
+ return hooks.SessionStart.some((entry) => {
2174
+ const hookEntry = entry;
2175
+ return hookEntry.hooks?.some(
2176
+ (h) => CLAUDE_HOOK_MARKERS.some((marker) => h.command?.includes(marker))
2177
+ );
1454
2178
  });
1455
2179
  }
1456
- async function detectAndPropose(repoRoot, input) {
1457
- const detected = runDetector(input);
1458
- if (!detected) {
1459
- return null;
2180
+ function addLearningAgentHook(settings) {
2181
+ if (!settings.hooks) {
2182
+ settings.hooks = {};
1460
2183
  }
1461
- const { trigger, source, proposedInsight } = detected;
1462
- const quality = await shouldPropose(repoRoot, proposedInsight);
1463
- if (!quality.shouldPropose) {
1464
- return null;
2184
+ const hooks = settings.hooks;
2185
+ if (!hooks.SessionStart) {
2186
+ hooks.SessionStart = [];
1465
2187
  }
1466
- return { trigger, source, proposedInsight };
2188
+ hooks.SessionStart.push(CLAUDE_HOOK_CONFIG);
1467
2189
  }
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
- }
2190
+ function removeLearningAgentHook(settings) {
2191
+ const hooks = settings.hooks;
2192
+ if (!hooks?.SessionStart) return false;
2193
+ const originalLength = hooks.SessionStart.length;
2194
+ hooks.SessionStart = hooks.SessionStart.filter((entry) => {
2195
+ const hookEntry = entry;
2196
+ return !hookEntry.hooks?.some(
2197
+ (h) => CLAUDE_HOOK_MARKERS.some((marker) => h.command?.includes(marker))
2198
+ );
2199
+ });
2200
+ return hooks.SessionStart.length < originalLength;
1477
2201
  }
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
- };
2202
+ async function writeClaudeSettings(settingsPath, settings) {
2203
+ const dir = dirname(settingsPath);
2204
+ await mkdir(dir, { recursive: true });
2205
+ const tempPath = settingsPath + ".tmp";
2206
+ await writeFile(tempPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2207
+ await rename(tempPath, settingsPath);
1488
2208
  }
1489
- function detectSelfCorrectionFlow(data) {
1490
- const result = detectSelfCorrection(data);
1491
- if (!result) {
1492
- return null;
2209
+ async function installClaudeHooksForInit(repoRoot) {
2210
+ const settingsPath = join(repoRoot, ".claude", "settings.json");
2211
+ let settings;
2212
+ try {
2213
+ settings = await readClaudeSettings(settingsPath);
2214
+ } catch {
2215
+ return { installed: false, action: "error", error: "Failed to parse settings.json" };
2216
+ }
2217
+ if (hasClaudeHook(settings)) {
2218
+ return { installed: true, action: "already_installed" };
2219
+ }
2220
+ try {
2221
+ addLearningAgentHook(settings);
2222
+ await writeClaudeSettings(settingsPath, settings);
2223
+ return { installed: true, action: "installed" };
2224
+ } catch (err) {
2225
+ return { installed: false, action: "error", error: String(err) };
1493
2226
  }
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
2227
  }
1501
- function detectTestFailureFlow(data) {
1502
- const result = detectTestFailure(data);
1503
- if (!result) {
1504
- return null;
2228
+ async function removeAgentsSection(repoRoot) {
2229
+ const agentsPath = join(repoRoot, "AGENTS.md");
2230
+ if (!existsSync(agentsPath)) {
2231
+ return false;
1505
2232
  }
1506
- return {
1507
- trigger: result.trigger,
1508
- source: "test_failure",
1509
- proposedInsight: result.errorOutput
1510
- };
2233
+ const content = await readFile(agentsPath, "utf-8");
2234
+ const startIdx = content.indexOf(AGENTS_SECTION_START_MARKER);
2235
+ const endIdx = content.indexOf(AGENTS_SECTION_END_MARKER);
2236
+ if (startIdx === -1 || endIdx === -1) {
2237
+ return false;
2238
+ }
2239
+ const before = content.slice(0, startIdx);
2240
+ const after = content.slice(endIdx + AGENTS_SECTION_END_MARKER.length);
2241
+ const newContent = (before.trimEnd() + after).trim();
2242
+ if (newContent.length > 0) {
2243
+ await writeFile(agentsPath, newContent + "\n", "utf-8");
2244
+ } else {
2245
+ await writeFile(agentsPath, "", "utf-8");
2246
+ }
2247
+ return true;
1511
2248
  }
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`);
2249
+ async function removeClaudeMdReference(repoRoot) {
2250
+ const claudeMdPath = join(repoRoot, ".claude", "CLAUDE.md");
2251
+ if (!existsSync(claudeMdPath)) {
2252
+ return false;
1518
2253
  }
1519
- return data;
2254
+ const content = await readFile(claudeMdPath, "utf-8");
2255
+ const startIdx = content.indexOf(CLAUDE_REF_START_MARKER);
2256
+ const endIdx = content.indexOf(CLAUDE_REF_END_MARKER);
2257
+ if (startIdx === -1 || endIdx === -1) {
2258
+ return false;
2259
+ }
2260
+ const before = content.slice(0, startIdx);
2261
+ const after = content.slice(endIdx + CLAUDE_REF_END_MARKER.length);
2262
+ const newContent = (before.trimEnd() + after).trim();
2263
+ if (newContent.length > 0) {
2264
+ await writeFile(claudeMdPath, newContent + "\n", "utf-8");
2265
+ } else {
2266
+ await writeFile(claudeMdPath, "", "utf-8");
2267
+ }
2268
+ return true;
1520
2269
  }
1521
2270
 
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) => {
2271
+ // src/commands/setup/claude.ts
2272
+ function registerClaudeCommand(program2) {
2273
+ const setupCommand = program2.command("setup").description("Setup integrations");
2274
+ 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("--status", "Check status of Claude Code integration").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action(async (options) => {
2275
+ const settingsPath = getClaudeSettingsPath(options.global ?? false);
2276
+ const displayPath = options.global ? "~/.claude/settings.json" : ".claude/settings.json";
2277
+ let settings;
2278
+ try {
2279
+ settings = await readClaudeSettings(settingsPath);
2280
+ } catch {
2281
+ if (options.json) {
2282
+ console.log(JSON.stringify({ error: "Failed to parse settings file" }));
2283
+ } else {
2284
+ out.error("Failed to parse settings file. Check if JSON is valid.");
2285
+ }
2286
+ process.exit(1);
2287
+ }
2288
+ const alreadyInstalled = hasClaudeHook(settings);
2289
+ if (options.status) {
1526
2290
  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" }));
2291
+ const learnMdPath = join(repoRoot, ".claude", "commands", "learn.md");
2292
+ const checkPlanMdPath = join(repoRoot, ".claude", "commands", "check-plan.md");
2293
+ const learnExists = existsSync(learnMdPath);
2294
+ const checkPlanExists = existsSync(checkPlanMdPath);
2295
+ let status;
2296
+ if (alreadyInstalled && learnExists && checkPlanExists) {
2297
+ status = "connected";
2298
+ } else if (alreadyInstalled || learnExists || checkPlanExists) {
2299
+ status = "partial";
2300
+ } else {
2301
+ status = "disconnected";
2302
+ }
2303
+ const result = {
2304
+ settingsFile: displayPath,
2305
+ exists: existsSync(settingsPath),
2306
+ validJson: true,
2307
+ // We already parsed it above
2308
+ hookInstalled: alreadyInstalled,
2309
+ slashCommands: {
2310
+ learn: learnExists,
2311
+ checkPlan: checkPlanExists
2312
+ },
2313
+ status
2314
+ };
2315
+ if (options.json) {
2316
+ console.log(JSON.stringify(result, null, 2));
2317
+ } else {
2318
+ console.log("Claude Code Integration Status");
2319
+ console.log("\u2500".repeat(40));
2320
+ console.log("");
2321
+ console.log(`Settings file: ${displayPath}`);
2322
+ console.log(` ${result.exists ? "[ok]" : "[missing]"} File exists`);
2323
+ console.log(` ${result.validJson ? "[ok]" : "[error]"} Valid JSON`);
2324
+ console.log(` ${result.hookInstalled ? "[ok]" : "[warn]"} SessionStart hook installed`);
2325
+ console.log("");
2326
+ console.log("Slash commands:");
2327
+ console.log(` ${learnExists ? "[ok]" : "[warn]"} /learn command`);
2328
+ console.log(` ${checkPlanExists ? "[ok]" : "[warn]"} /check-plan command`);
2329
+ console.log("");
2330
+ if (status === "connected") {
2331
+ out.success("All checks passed. Integration is connected.");
2332
+ } else if (status === "partial") {
2333
+ out.warn("Partial setup detected.");
2334
+ console.log("");
2335
+ console.log("Run 'npx lna init' to complete setup.");
1530
2336
  } else {
1531
- out.error("--save requires --yes flag for confirmation");
1532
- console.log("Use: detect --input <file> --save --yes");
2337
+ out.error("Not connected.");
2338
+ console.log("");
2339
+ console.log("Run 'npx lna init' to set up Learning Agent.");
1533
2340
  }
1534
- process.exit(1);
1535
2341
  }
1536
- const input = await parseInputFile(options.input);
1537
- const result = await detectAndPropose(repoRoot, input);
1538
- if (!result) {
2342
+ return;
2343
+ }
2344
+ if (options.uninstall) {
2345
+ const repoRoot = getRepoRoot();
2346
+ if (options.dryRun) {
1539
2347
  if (options.json) {
1540
- console.log(JSON.stringify({ detected: false }));
2348
+ console.log(JSON.stringify({ dryRun: true, wouldRemove: alreadyInstalled, location: displayPath }));
1541
2349
  } else {
1542
- console.log("No learning trigger detected.");
2350
+ if (alreadyInstalled) {
2351
+ console.log(`Would remove learning-agent hooks from ${displayPath}`);
2352
+ } else {
2353
+ console.log("No learning-agent hooks to remove");
2354
+ }
1543
2355
  }
1544
2356
  return;
1545
2357
  }
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}`);
2358
+ const removedHook = removeLearningAgentHook(settings);
2359
+ if (removedHook) {
2360
+ await writeClaudeSettings(settingsPath, settings);
1572
2361
  }
1573
- }
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) {
1628
- const repoRoot = getRepoRoot();
1629
- const { verbose } = getGlobalOpts(this);
1630
- let lesson;
1631
- if (options.input) {
1632
- const input = await parseInputFile(options.input);
1633
- const result = await detectAndPropose(repoRoot, input);
1634
- if (!result) {
1635
- options.json ? console.log(JSON.stringify({ detected: false, saved: false })) : console.log("No learning trigger detected.");
1636
- return;
2362
+ const removedAgents = await removeAgentsSection(repoRoot);
2363
+ const removedClaudeMd = await removeClaudeMdReference(repoRoot);
2364
+ const anyRemoved = removedHook || removedAgents || removedClaudeMd;
2365
+ if (anyRemoved) {
2366
+ if (options.json) {
2367
+ console.log(JSON.stringify({
2368
+ installed: false,
2369
+ location: displayPath,
2370
+ action: "removed",
2371
+ agentsMdRemoved: removedAgents,
2372
+ claudeMdRemoved: removedClaudeMd
2373
+ }));
2374
+ } else {
2375
+ out.success("Learning agent hooks removed");
2376
+ if (removedHook) {
2377
+ console.log(` Settings: ${displayPath}`);
2378
+ }
2379
+ if (removedAgents) {
2380
+ console.log(" AGENTS.md: Learning Agent section removed");
2381
+ }
2382
+ if (removedClaudeMd) {
2383
+ console.log(" CLAUDE.md: Learning Agent reference removed");
2384
+ }
2385
+ }
2386
+ } else {
2387
+ if (options.json) {
2388
+ console.log(JSON.stringify({ installed: false, location: displayPath, action: "unchanged" }));
2389
+ } else {
2390
+ out.info("No learning agent hooks to remove");
2391
+ if (options.global) {
2392
+ console.log(" Hint: Try without --global to check project settings.");
2393
+ } else {
2394
+ console.log(" Hint: Try with --global flag to check global settings.");
2395
+ }
2396
+ }
1637
2397
  }
1638
- lesson = createLessonFromInputFile(result, options.yes ?? false);
1639
- } else if (options.trigger && options.insight) {
1640
- lesson = createLessonFromFlags(options.trigger, options.insight, options.yes ?? false);
1641
- } else {
1642
- const msg = "Provide either --trigger and --insight, or --input file.";
1643
- options.json ? console.log(JSON.stringify({ error: msg, saved: false })) : out.error(msg);
1644
- process.exit(1);
2398
+ return;
1645
2399
  }
1646
- if (!options.yes && !process.stdin.isTTY) {
2400
+ if (options.dryRun) {
1647
2401
  if (options.json) {
1648
- console.log(JSON.stringify({ error: "--yes required in non-interactive mode", saved: false }));
2402
+ console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
1649
2403
  } else {
1650
- out.error("--yes required in non-interactive mode");
1651
- console.log('Use: capture --trigger "..." --insight "..." --yes');
2404
+ if (alreadyInstalled) {
2405
+ console.log("Learning agent hooks already installed");
2406
+ } else {
2407
+ console.log(`Would install learning-agent hooks to ${displayPath}`);
2408
+ }
1652
2409
  }
1653
- process.exit(1);
2410
+ return;
2411
+ }
2412
+ if (alreadyInstalled) {
2413
+ if (options.json) {
2414
+ console.log(JSON.stringify({
2415
+ installed: true,
2416
+ location: displayPath,
2417
+ hooks: ["SessionStart"],
2418
+ action: "unchanged"
2419
+ }));
2420
+ } else {
2421
+ out.info("Learning agent hooks already installed");
2422
+ console.log(` Location: ${displayPath}`);
2423
+ }
2424
+ return;
1654
2425
  }
2426
+ const fileExists = existsSync(settingsPath);
2427
+ addLearningAgentHook(settings);
2428
+ await writeClaudeSettings(settingsPath, settings);
1655
2429
  if (options.json) {
1656
- if (options.yes) await appendLesson(repoRoot, lesson);
1657
- outputCaptureJson(lesson, options.yes ?? false);
1658
- } else if (options.yes) {
1659
- await appendLesson(repoRoot, lesson);
1660
- out.success(`Lesson saved: ${lesson.id}`);
1661
- if (verbose) console.log(` Type: ${lesson.type} | Trigger: ${lesson.trigger}`);
2430
+ console.log(JSON.stringify({
2431
+ installed: true,
2432
+ location: displayPath,
2433
+ hooks: ["SessionStart"],
2434
+ action: fileExists ? "updated" : "created"
2435
+ }));
1662
2436
  } else {
1663
- outputCapturePreview(lesson);
2437
+ out.success(options.global ? "Claude Code hooks installed (global)" : "Claude Code hooks installed (project-level)");
2438
+ console.log(` Location: ${displayPath}`);
2439
+ console.log(" Hook: SessionStart (startup|resume|compact)");
2440
+ console.log("");
2441
+ console.log("Lessons will be loaded automatically at session start.");
2442
+ if (!options.global) {
2443
+ console.log("");
2444
+ console.log("Note: Project hooks override global hooks.");
2445
+ }
1664
2446
  }
1665
2447
  });
1666
2448
  }
1667
- var LEARNING_AGENT_SECTION_HEADER = "## Learning Agent Integration";
1668
- var HOOK_FILE_MODE = 493;
1669
- var LEARNING_AGENT_HOOK_BLOCK = `
1670
- # Learning Agent pre-commit hook (appended)
1671
- npx lna hooks run pre-commit
1672
- `;
1673
- var AGENTS_MD_TEMPLATE = `
1674
- ## Learning Agent Integration
1675
-
1676
- This project uses learning-agent for session memory.
1677
-
1678
- ### \u26A0\uFE0F IMPORTANT: Never Edit JSONL Directly
1679
-
1680
- **DO NOT** manually edit \`.claude/lessons/index.jsonl\`.
1681
-
1682
- Always use CLI commands:
1683
-
1684
- \`\`\`bash
1685
- npx lna learn "insight" --severity high # Create lesson
1686
- npx lna update <id> --insight "new text" # Update lesson
1687
- npx lna delete <id> # Delete lesson
1688
- npx lna list # List all lessons
1689
- \`\`\`
1690
-
1691
- Manual edits will:
1692
- - **Break SQLite sync** - Index becomes stale, search fails
1693
- - **Bypass schema validation** - Invalid data corrupts the database
1694
- - **Cause silent failures** - Lessons won't load at session start
1695
-
1696
- ### Retrieval Points
1697
-
1698
- - **Session start**: High-severity lessons loaded automatically
1699
- - **Plan-time**: BEFORE implementing a plan, run check-plan to retrieve relevant lessons
1700
-
1701
- ### Plan-Time Retrieval (Explicit Step)
1702
-
1703
- **BEFORE implementing any plan**, run:
1704
-
1705
- \`\`\`bash
1706
- npx lna check-plan --plan "your plan description" --json
1707
- \`\`\`
1708
-
1709
- Display results as a **Lessons Check** section after your plan:
1710
-
1711
- \`\`\`
1712
- ## Lessons Check
1713
- 1. [insight from lesson 1] (relevance: 0.85)
1714
- 2. [insight from lesson 2] (relevance: 0.72)
1715
- \`\`\`
1716
-
1717
- Consider each lesson while implementing.
1718
-
1719
- ### Proposing Lessons
1720
-
1721
- Propose when: user correction, self-correction, test failure fix, or manual request.
1722
-
1723
- **Quality gate (ALL must pass):**
1724
-
1725
- - Novel (not already stored)
1726
- - Specific (clear guidance)
1727
- - Actionable (obvious what to do)
1728
-
1729
- **Confirmation format:**
1730
-
1731
- \`\`\`
1732
- Learned: [insight]. Save? [y/n]
1733
- \`\`\`
1734
-
1735
- ### Session-End Protocol
1736
-
1737
- Before closing a session, reflect on lessons learned:
1738
-
1739
- 1. **Review**: What mistakes or corrections happened?
1740
- 2. **Quality gate**: Is it novel, specific, actionable?
1741
- 3. **Propose**: "Learned: [insight]. Save? [y/n]"
1742
- 4. **Capture**: \`npx lna capture --trigger "..." --insight "..." --yes\`
1743
-
1744
- ### CLI Commands
1745
-
1746
- \`\`\`bash
1747
- npx lna load-session --json # Session start
1748
- npx lna check-plan --plan "..." --json # Before implementing
1749
- npx lna capture --trigger "..." --insight "..." --yes
1750
- \`\`\`
1751
-
1752
- See [AGENTS.md](https://github.com/Nathandela/learning_agent/blob/main/AGENTS.md) for full documentation.
1753
- `;
1754
- function hasLearningAgentSection(content) {
1755
- return content.includes(LEARNING_AGENT_SECTION_HEADER);
1756
- }
1757
- async function createLessonsDirectory(repoRoot) {
1758
- const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
1759
- await mkdir(lessonsDir, { recursive: true });
1760
- }
1761
- async function createIndexFile(repoRoot) {
1762
- const indexPath = join(repoRoot, LESSONS_PATH);
1763
- if (!existsSync(indexPath)) {
1764
- await writeFile(indexPath, "", "utf-8");
1765
- }
1766
- }
1767
- async function updateAgentsMd(repoRoot) {
1768
- const agentsPath = join(repoRoot, "AGENTS.md");
1769
- let content = "";
1770
- let existed = false;
1771
- if (existsSync(agentsPath)) {
1772
- content = await readFile(agentsPath, "utf-8");
1773
- existed = true;
1774
- if (hasLearningAgentSection(content)) {
1775
- return false;
2449
+ function registerDownloadModelCommand(program2) {
2450
+ program2.command("download-model").description("Download the embedding model for semantic search").option("--json", "Output as JSON").action(async (options) => {
2451
+ const alreadyExisted = isModelAvailable();
2452
+ if (alreadyExisted) {
2453
+ const modelPath2 = join(homedir(), ".node-llama-cpp", "models", MODEL_FILENAME);
2454
+ const size2 = statSync(modelPath2).size;
2455
+ if (options.json) {
2456
+ console.log(JSON.stringify({ success: true, path: modelPath2, size: size2, alreadyExisted: true }));
2457
+ } else {
2458
+ console.log("Model already exists.");
2459
+ console.log(`Path: ${modelPath2}`);
2460
+ console.log(`Size: ${formatBytes(size2)}`);
2461
+ }
2462
+ return;
1776
2463
  }
1777
- }
1778
- const newContent = existed ? content.trimEnd() + "\n" + AGENTS_MD_TEMPLATE : AGENTS_MD_TEMPLATE.trim() + "\n";
1779
- await writeFile(agentsPath, newContent, "utf-8");
1780
- return true;
2464
+ if (!options.json) {
2465
+ console.log("Downloading embedding model...");
2466
+ }
2467
+ const modelPath = await resolveModel({ cli: !options.json });
2468
+ const size = statSync(modelPath).size;
2469
+ if (options.json) {
2470
+ console.log(JSON.stringify({ success: true, path: modelPath, size, alreadyExisted: false }));
2471
+ } else {
2472
+ console.log(`
2473
+ Model downloaded successfully!`);
2474
+ console.log(`Path: ${modelPath}`);
2475
+ console.log(`Size: ${formatBytes(size)}`);
2476
+ }
2477
+ });
1781
2478
  }
2479
+ var HOOK_FILE_MODE = 493;
1782
2480
  function hasLearningAgentHook(content) {
1783
2481
  return content.includes(HOOK_MARKER);
1784
2482
  }
@@ -1851,14 +2549,103 @@ async function installPreCommitHook(repoRoot) {
1851
2549
  const after = lines.slice(exitLineIndex);
1852
2550
  newContent = before.join("\n") + LEARNING_AGENT_HOOK_BLOCK + after.join("\n");
1853
2551
  }
1854
- await writeFile(hookPath, newContent, "utf-8");
1855
- chmodSync(hookPath, HOOK_FILE_MODE);
1856
- return true;
2552
+ await writeFile(hookPath, newContent, "utf-8");
2553
+ chmodSync(hookPath, HOOK_FILE_MODE);
2554
+ return true;
2555
+ }
2556
+ await writeFile(hookPath, PRE_COMMIT_HOOK_TEMPLATE, "utf-8");
2557
+ chmodSync(hookPath, HOOK_FILE_MODE);
2558
+ return true;
2559
+ }
2560
+ function registerHooksCommand(program2) {
2561
+ const hooksCommand = program2.command("hooks").description("Git hooks management");
2562
+ hooksCommand.command("run <hook>").description("Run a hook script (called by git hooks)").option("--json", "Output as JSON").action((hook, options) => {
2563
+ if (hook === "pre-commit") {
2564
+ if (options.json) {
2565
+ console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
2566
+ } else {
2567
+ console.log(PRE_COMMIT_MESSAGE);
2568
+ }
2569
+ } else {
2570
+ if (options.json) {
2571
+ console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
2572
+ } else {
2573
+ out.error(`Unknown hook: ${hook}`);
2574
+ }
2575
+ process.exit(1);
2576
+ }
2577
+ });
2578
+ }
2579
+ function hasLearningAgentSection(content) {
2580
+ return content.includes(LEARNING_AGENT_SECTION_HEADER);
2581
+ }
2582
+ function hasClaudeMdReference(content) {
2583
+ return content.includes("Learning Agent") || content.includes(CLAUDE_REF_START_MARKER);
2584
+ }
2585
+ async function ensureClaudeMdReference(repoRoot) {
2586
+ const claudeMdPath = join(repoRoot, ".claude", "CLAUDE.md");
2587
+ await mkdir(join(repoRoot, ".claude"), { recursive: true });
2588
+ if (!existsSync(claudeMdPath)) {
2589
+ const content2 = `# Project Instructions
2590
+ ${CLAUDE_MD_REFERENCE}`;
2591
+ await writeFile(claudeMdPath, content2, "utf-8");
2592
+ return true;
2593
+ }
2594
+ const content = await readFile(claudeMdPath, "utf-8");
2595
+ if (hasClaudeMdReference(content)) {
2596
+ return false;
2597
+ }
2598
+ const newContent = content.trimEnd() + "\n" + CLAUDE_MD_REFERENCE;
2599
+ await writeFile(claudeMdPath, newContent, "utf-8");
2600
+ return true;
2601
+ }
2602
+ async function createLessonsDirectory(repoRoot) {
2603
+ const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
2604
+ await mkdir(lessonsDir, { recursive: true });
2605
+ }
2606
+ async function createIndexFile(repoRoot) {
2607
+ const indexPath = join(repoRoot, LESSONS_PATH);
2608
+ if (!existsSync(indexPath)) {
2609
+ await writeFile(indexPath, "", "utf-8");
2610
+ }
2611
+ }
2612
+ async function updateAgentsMd(repoRoot) {
2613
+ const agentsPath = join(repoRoot, "AGENTS.md");
2614
+ let content = "";
2615
+ let existed = false;
2616
+ if (existsSync(agentsPath)) {
2617
+ content = await readFile(agentsPath, "utf-8");
2618
+ existed = true;
2619
+ if (hasLearningAgentSection(content)) {
2620
+ return false;
2621
+ }
1857
2622
  }
1858
- await writeFile(hookPath, PRE_COMMIT_HOOK_TEMPLATE, "utf-8");
1859
- chmodSync(hookPath, HOOK_FILE_MODE);
2623
+ const newContent = existed ? content.trimEnd() + "\n" + AGENTS_MD_TEMPLATE : AGENTS_MD_TEMPLATE.trim() + "\n";
2624
+ await writeFile(agentsPath, newContent, "utf-8");
1860
2625
  return true;
1861
2626
  }
2627
+ async function createPluginManifest(repoRoot) {
2628
+ const pluginPath = join(repoRoot, ".claude", "plugin.json");
2629
+ await mkdir(join(repoRoot, ".claude"), { recursive: true });
2630
+ if (existsSync(pluginPath)) {
2631
+ return false;
2632
+ }
2633
+ await writeFile(pluginPath, JSON.stringify(PLUGIN_MANIFEST, null, 2) + "\n", "utf-8");
2634
+ return true;
2635
+ }
2636
+ async function createSlashCommands(repoRoot) {
2637
+ const commandsDir = join(repoRoot, ".claude", "commands");
2638
+ await mkdir(commandsDir, { recursive: true });
2639
+ let created = false;
2640
+ for (const [filename, content] of Object.entries(SLASH_COMMANDS)) {
2641
+ const filePath = join(commandsDir, filename);
2642
+ if (!existsSync(filePath)) {
2643
+ await writeFile(filePath, content, "utf-8");
2644
+ created = true;
2645
+ }
2646
+ }
2647
+ return created;
2648
+ }
1862
2649
  function registerInitCommand(program2) {
1863
2650
  program2.command("init").description("Initialize learning-agent in this repository").option("--skip-agents", "Skip AGENTS.md modification").option("--skip-hooks", "Skip git hooks installation").option("--skip-claude", "Skip Claude Code hooks installation").option("--json", "Output result as JSON").action(async function(options) {
1864
2651
  const repoRoot = getRepoRoot();
@@ -1870,30 +2657,31 @@ function registerInitCommand(program2) {
1870
2657
  if (!options.skipAgents) {
1871
2658
  agentsMdUpdated = await updateAgentsMd(repoRoot);
1872
2659
  }
2660
+ if (!options.skipAgents) {
2661
+ await ensureClaudeMdReference(repoRoot);
2662
+ }
2663
+ let slashCommandsCreated = false;
2664
+ if (!options.skipAgents) {
2665
+ slashCommandsCreated = await createSlashCommands(repoRoot);
2666
+ }
2667
+ if (!options.skipAgents) {
2668
+ await createPluginManifest(repoRoot);
2669
+ }
1873
2670
  let hooksInstalled = false;
1874
2671
  if (!options.skipHooks) {
1875
2672
  hooksInstalled = await installPreCommitHook(repoRoot);
1876
2673
  }
1877
- let claudeHooksInstalled = false;
1878
- let claudeHooksError = null;
2674
+ let claudeHooksResult = { action: "error", error: "skipped" };
1879
2675
  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
- }
2676
+ claudeHooksResult = await installClaudeHooksForInit(repoRoot);
1891
2677
  }
1892
2678
  if (options.json) {
2679
+ const claudeHooksInstalled = claudeHooksResult.action === "installed";
1893
2680
  console.log(JSON.stringify({
1894
2681
  initialized: true,
1895
2682
  lessonsDir,
1896
2683
  agentsMd: agentsMdUpdated,
2684
+ slashCommands: slashCommandsCreated || !options.skipAgents,
1897
2685
  hooks: hooksInstalled,
1898
2686
  claudeHooks: claudeHooksInstalled
1899
2687
  }));
@@ -1907,6 +2695,13 @@ function registerInitCommand(program2) {
1907
2695
  } else {
1908
2696
  console.log(" AGENTS.md: Already has Learning Agent section");
1909
2697
  }
2698
+ if (slashCommandsCreated) {
2699
+ console.log(" Slash commands: Created (/learn, /check-plan, /list, /prime)");
2700
+ } else if (options.skipAgents) {
2701
+ console.log(" Slash commands: Skipped (--skip-agents)");
2702
+ } else {
2703
+ console.log(" Slash commands: Already exist");
2704
+ }
1910
2705
  if (hooksInstalled) {
1911
2706
  console.log(" Git hooks: pre-commit hook installed");
1912
2707
  } else if (options.skipHooks) {
@@ -1914,284 +2709,49 @@ function registerInitCommand(program2) {
1914
2709
  } else {
1915
2710
  console.log(" Git hooks: Already installed or not a git repo");
1916
2711
  }
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");
2712
+ if (options.skipClaude) {
2713
+ console.log(" Claude hooks: Skipped (--skip-claude)");
2714
+ } else if (claudeHooksResult.action === "installed") {
2715
+ console.log(" Claude hooks: Installed to .claude/settings.json");
2716
+ } else if (claudeHooksResult.action === "already_installed") {
2717
+ console.log(" Claude hooks: Already installed");
2718
+ } else if (claudeHooksResult.error) {
2719
+ console.log(` Claude hooks: Error - ${claudeHooksResult.error}`);
1925
2720
  }
1926
2721
  }
1927
2722
  });
1928
2723
  }
1929
2724
 
1930
- // src/cli/commands/hooks.ts
1931
- function registerHooksCommand(program2) {
1932
- const hooksCommand = program2.command("hooks").description("Git hooks management");
1933
- hooksCommand.command("run <hook>").description("Run a hook script (called by git hooks)").option("--json", "Output as JSON").action((hook, options) => {
1934
- if (hook === "pre-commit") {
1935
- if (options.json) {
1936
- console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
1937
- } else {
1938
- console.log(PRE_COMMIT_MESSAGE);
1939
- }
1940
- } else {
1941
- if (options.json) {
1942
- console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
1943
- } else {
1944
- out.error(`Unknown hook: ${hook}`);
1945
- }
1946
- process.exit(1);
1947
- }
1948
- });
1949
- }
1950
- function registerSetupCommand(program2) {
1951
- const setupCommand = program2.command("setup").description("Setup integrations");
1952
- setupCommand.command("claude").description("Install Claude Code SessionStart hooks").option("--global", "Install to global ~/.claude/ instead of project").option("--uninstall", "Remove learning-agent hooks").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action(async (options) => {
1953
- const settingsPath = getClaudeSettingsPath(options.global ?? false);
1954
- const displayPath = options.global ? "~/.claude/settings.json" : ".claude/settings.json";
1955
- let settings;
1956
- try {
1957
- settings = await readClaudeSettings(settingsPath);
1958
- } catch {
1959
- if (options.json) {
1960
- console.log(JSON.stringify({ error: "Failed to parse settings file" }));
1961
- } else {
1962
- out.error("Failed to parse settings file. Check if JSON is valid.");
1963
- }
1964
- process.exit(1);
1965
- }
1966
- const alreadyInstalled = hasClaudeHook(settings);
1967
- if (options.uninstall) {
1968
- if (options.dryRun) {
1969
- if (options.json) {
1970
- console.log(JSON.stringify({ dryRun: true, wouldRemove: alreadyInstalled, location: displayPath }));
1971
- } else {
1972
- if (alreadyInstalled) {
1973
- console.log(`Would remove learning-agent hooks from ${displayPath}`);
1974
- } else {
1975
- console.log("No learning-agent hooks to remove");
1976
- }
1977
- }
1978
- return;
1979
- }
1980
- const removed = removeLearningAgentHook(settings);
1981
- if (removed) {
1982
- await writeClaudeSettings(settingsPath, settings);
1983
- if (options.json) {
1984
- console.log(JSON.stringify({ installed: false, location: displayPath, action: "removed" }));
1985
- } else {
1986
- out.success("Learning agent hooks removed");
1987
- console.log(` Location: ${displayPath}`);
1988
- }
1989
- } else {
1990
- if (options.json) {
1991
- console.log(JSON.stringify({ installed: false, location: displayPath, action: "unchanged" }));
1992
- } else {
1993
- out.info("No learning agent hooks to remove");
1994
- if (options.global) {
1995
- console.log(" Hint: Try without --global to check project settings.");
1996
- } else {
1997
- console.log(" Hint: Try with --global flag to check global settings.");
1998
- }
1999
- }
2000
- }
2001
- return;
2002
- }
2003
- if (options.dryRun) {
2004
- if (options.json) {
2005
- console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
2006
- } else {
2007
- if (alreadyInstalled) {
2008
- console.log("Learning agent hooks already installed");
2009
- } else {
2010
- console.log(`Would install learning-agent hooks to ${displayPath}`);
2011
- }
2012
- }
2013
- return;
2014
- }
2015
- if (alreadyInstalled) {
2016
- if (options.json) {
2017
- console.log(JSON.stringify({
2018
- installed: true,
2019
- location: displayPath,
2020
- hooks: ["SessionStart"],
2021
- action: "unchanged"
2022
- }));
2023
- } else {
2024
- out.info("Learning agent hooks already installed");
2025
- console.log(` Location: ${displayPath}`);
2026
- }
2027
- return;
2028
- }
2029
- const fileExists = existsSync(settingsPath);
2030
- addLearningAgentHook(settings);
2031
- await writeClaudeSettings(settingsPath, settings);
2032
- if (options.json) {
2033
- console.log(JSON.stringify({
2034
- installed: true,
2035
- location: displayPath,
2036
- hooks: ["SessionStart"],
2037
- action: fileExists ? "updated" : "created"
2038
- }));
2039
- } else {
2040
- out.success(options.global ? "Claude Code hooks installed (global)" : "Claude Code hooks installed (project-level)");
2041
- console.log(` Location: ${displayPath}`);
2042
- console.log(" Hook: SessionStart (startup|resume|compact)");
2043
- console.log("");
2044
- console.log("Lessons will be loaded automatically at session start.");
2045
- if (!options.global) {
2046
- console.log("");
2047
- console.log("Note: Project hooks override global hooks.");
2048
- }
2049
- }
2050
- });
2725
+ // src/commands/setup/index.ts
2726
+ function registerSetupCommands(program2) {
2727
+ registerInitCommand(program2);
2728
+ registerHooksCommand(program2);
2729
+ registerClaudeCommand(program2);
2730
+ registerDownloadModelCommand(program2);
2051
2731
  }
2052
2732
 
2053
- // src/cli/commands/load-session.ts
2054
- function formatSource(source) {
2055
- return source.replace(/_/g, " ");
2056
- }
2057
- function outputSessionLessonsHuman(lessons, quiet) {
2058
- console.log("## Lessons from Past Sessions\n");
2059
- if (!quiet) {
2060
- console.log("These lessons were captured from previous corrections and should inform your work:\n");
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.");
2733
+ // src/cli.ts
2734
+ function cleanup() {
2735
+ try {
2736
+ closeDb();
2737
+ } catch {
2125
2738
  }
2126
2739
  }
2127
- function registerCheckPlanCommand(program2) {
2128
- program2.command("check-plan").description("Check plan against relevant lessons").option("--plan <text>", "Plan text to check").option("--json", "Output as JSON").option("-n, --limit <number>", "Maximum results", DEFAULT_CHECK_PLAN_LIMIT).action(async function(options) {
2129
- const repoRoot = getRepoRoot();
2130
- const limit = parseLimit(options.limit, "limit");
2131
- const { quiet } = getGlobalOpts(this);
2132
- const planText = options.plan ?? await readPlanFromStdin();
2133
- if (!planText) {
2134
- out.error("No plan provided. Use --plan <text> or pipe text to stdin.");
2135
- process.exit(1);
2136
- }
2137
- if (!isModelAvailable()) {
2138
- if (options.json) {
2139
- console.log(JSON.stringify({
2140
- error: "Embedding model not available",
2141
- action: "Run: npx lna download-model"
2142
- }));
2143
- } else {
2144
- out.error("Embedding model not available");
2145
- console.log("");
2146
- console.log("Run: npx lna download-model");
2147
- }
2148
- process.exit(1);
2149
- }
2150
- try {
2151
- const result = await retrieveForPlan(repoRoot, planText, limit);
2152
- if (options.json) {
2153
- outputCheckPlanJson(result.lessons);
2154
- return;
2155
- }
2156
- if (result.lessons.length === 0) {
2157
- console.log("No relevant lessons found for this plan.");
2158
- return;
2159
- }
2160
- outputCheckPlanHuman(result.lessons, quiet);
2161
- } catch (err) {
2162
- const message = err instanceof Error ? err.message : "Unknown error";
2163
- if (options.json) {
2164
- console.log(JSON.stringify({ error: message }));
2165
- } else {
2166
- out.error(`Failed to check plan: ${message}`);
2167
- }
2168
- process.exit(1);
2169
- }
2170
- });
2171
- }
2172
-
2173
- // src/cli.ts
2740
+ process.on("SIGINT", () => {
2741
+ cleanup();
2742
+ process.exit(0);
2743
+ });
2744
+ process.on("SIGTERM", () => {
2745
+ cleanup();
2746
+ process.exit(0);
2747
+ });
2174
2748
  var program = new Command();
2175
- program.name("learning-agent").description("Learning system for Claude Code session memory").version(VERSION).option("-v, --verbose", "Verbose output").option("-q, --quiet", "Minimal output");
2176
- registerInitCommand(program);
2177
- registerLearnCommand(program);
2178
- registerCaptureCommand(program);
2179
- registerDetectCommand(program);
2180
- registerListCommand(program);
2181
- registerSearchCommand(program);
2182
- registerShowCommand(program);
2183
- registerUpdateCommand(program);
2184
- registerDeleteCommand(program);
2185
- registerLoadSessionCommand(program);
2186
- registerCheckPlanCommand(program);
2187
- registerStatsCommand(program);
2188
- registerRebuildCommand(program);
2189
- registerCompactCommand(program);
2190
- registerExportCommand(program);
2191
- registerImportCommand(program);
2192
- registerDownloadModelCommand(program);
2193
- registerSetupCommand(program);
2194
- registerHooksCommand(program);
2749
+ program.option("-v, --verbose", "Show detailed output").option("-q, --quiet", "Suppress non-essential output");
2750
+ program.name("learning-agent").description("Repository-scoped learning system for Claude Code").version(VERSION);
2751
+ registerCaptureCommands(program);
2752
+ registerRetrievalCommands(program);
2753
+ registerManagementCommands(program);
2754
+ registerSetupCommands(program);
2195
2755
  program.parse();
2196
2756
  //# sourceMappingURL=cli.js.map
2197
2757
  //# sourceMappingURL=cli.js.map