learning-agent 0.2.2 → 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/CHANGELOG.md +46 -1
- package/README.md +103 -0
- package/dist/cli.js +1341 -871
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +28 -45
- package/dist/index.js +161 -103
- package/dist/index.js.map +1 -1
- package/package.json +21 -10
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
4
|
-
import { statSync, existsSync, chmodSync, mkdirSync } from 'fs';
|
|
5
|
-
import { join, dirname } from 'path';
|
|
6
|
-
import Database from 'better-sqlite3';
|
|
7
3
|
import * as fs from 'fs/promises';
|
|
8
|
-
import {
|
|
4
|
+
import { mkdir, appendFile, readFile, writeFile, rename } from 'fs/promises';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
9
7
|
import { z } from 'zod';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import { existsSync, statSync, mkdirSync, chmodSync } from 'fs';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { resolveModelFile, getLlama } from 'node-llama-cpp';
|
|
12
12
|
import { homedir } from 'os';
|
|
@@ -180,11 +180,45 @@ async function readLessons(repoRoot, options = {}) {
|
|
|
180
180
|
}
|
|
181
181
|
return { lessons: Array.from(lessons.values()), skippedCount };
|
|
182
182
|
}
|
|
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
|
+
}
|
|
183
219
|
|
|
184
|
-
// src/storage/sqlite.ts
|
|
185
|
-
var DB_PATH = ".claude/.cache/lessons.sqlite";
|
|
220
|
+
// src/storage/sqlite/schema.ts
|
|
186
221
|
var SCHEMA_SQL = `
|
|
187
|
-
-- Main lessons table
|
|
188
222
|
CREATE TABLE IF NOT EXISTS lessons (
|
|
189
223
|
id TEXT PRIMARY KEY,
|
|
190
224
|
type TEXT NOT NULL,
|
|
@@ -204,7 +238,6 @@ var SCHEMA_SQL = `
|
|
|
204
238
|
last_retrieved TEXT,
|
|
205
239
|
embedding BLOB,
|
|
206
240
|
content_hash TEXT,
|
|
207
|
-
-- v0.2.2 fields
|
|
208
241
|
invalidated_at TEXT,
|
|
209
242
|
invalidation_reason TEXT,
|
|
210
243
|
citation_file TEXT,
|
|
@@ -214,29 +247,21 @@ var SCHEMA_SQL = `
|
|
|
214
247
|
compacted_at TEXT
|
|
215
248
|
);
|
|
216
249
|
|
|
217
|
-
-- FTS5 virtual table for full-text search
|
|
218
250
|
CREATE VIRTUAL TABLE IF NOT EXISTS lessons_fts USING fts5(
|
|
219
|
-
id,
|
|
220
|
-
|
|
221
|
-
insight,
|
|
222
|
-
tags,
|
|
223
|
-
content='lessons',
|
|
224
|
-
content_rowid='rowid'
|
|
251
|
+
id, trigger, insight, tags,
|
|
252
|
+
content='lessons', content_rowid='rowid'
|
|
225
253
|
);
|
|
226
254
|
|
|
227
|
-
-- Trigger to sync FTS on INSERT
|
|
228
255
|
CREATE TRIGGER IF NOT EXISTS lessons_ai AFTER INSERT ON lessons BEGIN
|
|
229
256
|
INSERT INTO lessons_fts(rowid, id, trigger, insight, tags)
|
|
230
257
|
VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
|
|
231
258
|
END;
|
|
232
259
|
|
|
233
|
-
-- Trigger to sync FTS on DELETE
|
|
234
260
|
CREATE TRIGGER IF NOT EXISTS lessons_ad AFTER DELETE ON lessons BEGIN
|
|
235
261
|
INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
|
|
236
262
|
VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
|
|
237
263
|
END;
|
|
238
264
|
|
|
239
|
-
-- Trigger to sync FTS on UPDATE
|
|
240
265
|
CREATE TRIGGER IF NOT EXISTS lessons_au AFTER UPDATE ON lessons BEGIN
|
|
241
266
|
INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
|
|
242
267
|
VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
|
|
@@ -244,12 +269,10 @@ var SCHEMA_SQL = `
|
|
|
244
269
|
VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
|
|
245
270
|
END;
|
|
246
271
|
|
|
247
|
-
-- Index for common queries
|
|
248
272
|
CREATE INDEX IF NOT EXISTS idx_lessons_created ON lessons(created);
|
|
249
273
|
CREATE INDEX IF NOT EXISTS idx_lessons_confirmed ON lessons(confirmed);
|
|
250
274
|
CREATE INDEX IF NOT EXISTS idx_lessons_severity ON lessons(severity);
|
|
251
275
|
|
|
252
|
-
-- Metadata table for sync tracking
|
|
253
276
|
CREATE TABLE IF NOT EXISTS metadata (
|
|
254
277
|
key TEXT PRIMARY KEY,
|
|
255
278
|
value TEXT NOT NULL
|
|
@@ -258,22 +281,54 @@ var SCHEMA_SQL = `
|
|
|
258
281
|
function createSchema(database) {
|
|
259
282
|
database.exec(SCHEMA_SQL);
|
|
260
283
|
}
|
|
284
|
+
|
|
285
|
+
// src/storage/sqlite/connection.ts
|
|
286
|
+
var DB_PATH = ".claude/.cache/lessons.sqlite";
|
|
261
287
|
var db = null;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
}
|
|
272
313
|
createSchema(db);
|
|
273
314
|
return db;
|
|
274
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
|
+
}
|
|
275
326
|
function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
|
|
276
327
|
const database = openDb(repoRoot);
|
|
328
|
+
if (!database) {
|
|
329
|
+
logDegradationWarning();
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
277
332
|
const row = database.prepare("SELECT embedding, content_hash FROM lessons WHERE id = ?").get(lessonId);
|
|
278
333
|
if (!row || !row.embedding || !row.content_hash) {
|
|
279
334
|
return null;
|
|
@@ -290,60 +345,14 @@ function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
|
|
|
290
345
|
}
|
|
291
346
|
function setCachedEmbedding(repoRoot, lessonId, embedding, hash) {
|
|
292
347
|
const database = openDb(repoRoot);
|
|
348
|
+
if (!database) {
|
|
349
|
+
logDegradationWarning();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
293
352
|
const float32 = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
|
|
294
353
|
const buffer = Buffer.from(float32.buffer, float32.byteOffset, float32.byteLength);
|
|
295
354
|
database.prepare("UPDATE lessons SET embedding = ?, content_hash = ? WHERE id = ?").run(buffer, hash, lessonId);
|
|
296
355
|
}
|
|
297
|
-
function rowToLesson(row) {
|
|
298
|
-
const lesson = {
|
|
299
|
-
id: row.id,
|
|
300
|
-
type: row.type,
|
|
301
|
-
trigger: row.trigger,
|
|
302
|
-
insight: row.insight,
|
|
303
|
-
tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
|
|
304
|
-
source: row.source,
|
|
305
|
-
context: JSON.parse(row.context),
|
|
306
|
-
supersedes: JSON.parse(row.supersedes),
|
|
307
|
-
related: JSON.parse(row.related),
|
|
308
|
-
created: row.created,
|
|
309
|
-
confirmed: row.confirmed === 1
|
|
310
|
-
};
|
|
311
|
-
if (row.evidence !== null) {
|
|
312
|
-
lesson.evidence = row.evidence;
|
|
313
|
-
}
|
|
314
|
-
if (row.severity !== null) {
|
|
315
|
-
lesson.severity = row.severity;
|
|
316
|
-
}
|
|
317
|
-
if (row.deleted === 1) {
|
|
318
|
-
lesson.deleted = true;
|
|
319
|
-
}
|
|
320
|
-
if (row.retrieval_count > 0) {
|
|
321
|
-
lesson.retrievalCount = row.retrieval_count;
|
|
322
|
-
}
|
|
323
|
-
if (row.invalidated_at !== null) {
|
|
324
|
-
lesson.invalidatedAt = row.invalidated_at;
|
|
325
|
-
}
|
|
326
|
-
if (row.invalidation_reason !== null) {
|
|
327
|
-
lesson.invalidationReason = row.invalidation_reason;
|
|
328
|
-
}
|
|
329
|
-
if (row.citation_file !== null) {
|
|
330
|
-
lesson.citation = {
|
|
331
|
-
file: row.citation_file,
|
|
332
|
-
...row.citation_line !== null && { line: row.citation_line },
|
|
333
|
-
...row.citation_commit !== null && { commit: row.citation_commit }
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
if (row.compaction_level !== null && row.compaction_level !== 0) {
|
|
337
|
-
lesson.compactionLevel = row.compaction_level;
|
|
338
|
-
}
|
|
339
|
-
if (row.compacted_at !== null) {
|
|
340
|
-
lesson.compactedAt = row.compacted_at;
|
|
341
|
-
}
|
|
342
|
-
if (row.last_retrieved !== null) {
|
|
343
|
-
lesson.lastRetrieved = row.last_retrieved;
|
|
344
|
-
}
|
|
345
|
-
return lesson;
|
|
346
|
-
}
|
|
347
356
|
function collectCachedEmbeddings(database) {
|
|
348
357
|
const cache = /* @__PURE__ */ new Map();
|
|
349
358
|
const rows = database.prepare("SELECT id, embedding, content_hash FROM lessons WHERE embedding IS NOT NULL").all();
|
|
@@ -376,6 +385,10 @@ function setLastSyncMtime(database, mtime) {
|
|
|
376
385
|
}
|
|
377
386
|
async function rebuildIndex(repoRoot) {
|
|
378
387
|
const database = openDb(repoRoot);
|
|
388
|
+
if (!database) {
|
|
389
|
+
logDegradationWarning();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
379
392
|
const { lessons } = await readLessons(repoRoot);
|
|
380
393
|
const cachedEmbeddings = collectCachedEmbeddings(database);
|
|
381
394
|
database.exec("DELETE FROM lessons");
|
|
@@ -411,7 +424,6 @@ async function rebuildIndex(repoRoot) {
|
|
|
411
424
|
last_retrieved: lesson.lastRetrieved ?? null,
|
|
412
425
|
embedding: hasValidCache ? cached.embedding : null,
|
|
413
426
|
content_hash: hasValidCache ? cached.contentHash : null,
|
|
414
|
-
// v0.2.2 fields
|
|
415
427
|
invalidated_at: lesson.invalidatedAt ?? null,
|
|
416
428
|
invalidation_reason: lesson.invalidationReason ?? null,
|
|
417
429
|
citation_file: lesson.citation?.file ?? null,
|
|
@@ -429,12 +441,17 @@ async function rebuildIndex(repoRoot) {
|
|
|
429
441
|
}
|
|
430
442
|
}
|
|
431
443
|
async function syncIfNeeded(repoRoot, options = {}) {
|
|
444
|
+
if (!isSqliteAvailable()) {
|
|
445
|
+
logDegradationWarning();
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
432
448
|
const { force = false } = options;
|
|
433
449
|
const jsonlMtime = getJsonlMtime(repoRoot);
|
|
434
450
|
if (jsonlMtime === null && !force) {
|
|
435
451
|
return false;
|
|
436
452
|
}
|
|
437
453
|
const database = openDb(repoRoot);
|
|
454
|
+
if (!database) return false;
|
|
438
455
|
const lastSyncMtime = getLastSyncMtime(database);
|
|
439
456
|
const needsRebuild = force || lastSyncMtime === null || jsonlMtime !== null && jsonlMtime > lastSyncMtime;
|
|
440
457
|
if (needsRebuild) {
|
|
@@ -443,28 +460,49 @@ async function syncIfNeeded(repoRoot, options = {}) {
|
|
|
443
460
|
}
|
|
444
461
|
return false;
|
|
445
462
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
+
};
|
|
462
491
|
}
|
|
463
|
-
|
|
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;
|
|
464
498
|
}
|
|
465
499
|
function incrementRetrievalCount(repoRoot, lessonIds) {
|
|
466
500
|
if (lessonIds.length === 0) return;
|
|
467
501
|
const database = openDb(repoRoot);
|
|
502
|
+
if (!database) {
|
|
503
|
+
logDegradationWarning();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
468
506
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
469
507
|
const update = database.prepare(`
|
|
470
508
|
UPDATE lessons
|
|
@@ -479,8 +517,36 @@ function incrementRetrievalCount(repoRoot, lessonIds) {
|
|
|
479
517
|
});
|
|
480
518
|
updateMany(lessonIds);
|
|
481
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
|
+
}
|
|
482
544
|
function getRetrievalStats(repoRoot) {
|
|
483
545
|
const database = openDb(repoRoot);
|
|
546
|
+
if (!database) {
|
|
547
|
+
logDegradationWarning();
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
484
550
|
const rows = database.prepare("SELECT id, retrieval_count, last_retrieved FROM lessons").all();
|
|
485
551
|
return rows.map((row) => ({
|
|
486
552
|
id: row.id,
|
|
@@ -489,83 +555,202 @@ function getRetrievalStats(repoRoot) {
|
|
|
489
555
|
}));
|
|
490
556
|
}
|
|
491
557
|
|
|
492
|
-
// src/
|
|
493
|
-
var
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
|
567
|
+
var ARCHIVE_DIR = ".claude/lessons/archive";
|
|
568
|
+
var TOMBSTONE_THRESHOLD = 100;
|
|
569
|
+
var ARCHIVE_AGE_DAYS = 90;
|
|
570
|
+
var MONTH_INDEX_OFFSET = 1;
|
|
571
|
+
var MONTH_PAD_LENGTH = 2;
|
|
572
|
+
function getArchivePath(repoRoot, date) {
|
|
573
|
+
const year = date.getFullYear();
|
|
574
|
+
const month = String(date.getMonth() + MONTH_INDEX_OFFSET).padStart(MONTH_PAD_LENGTH, "0");
|
|
575
|
+
return join(repoRoot, ARCHIVE_DIR, `${year}-${month}.jsonl`);
|
|
576
|
+
}
|
|
577
|
+
async function parseRawJsonlLines(repoRoot) {
|
|
578
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
579
|
+
let content;
|
|
580
|
+
try {
|
|
581
|
+
content = await readFile(filePath, "utf-8");
|
|
582
|
+
} catch {
|
|
583
|
+
return [];
|
|
505
584
|
}
|
|
506
|
-
const
|
|
507
|
-
for (const
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
reason: `Found similar existing lesson: "${lesson.insight.slice(0, 50)}..."`,
|
|
516
|
-
existingId: lesson.id
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
|
|
520
|
-
return {
|
|
521
|
-
novel: false,
|
|
522
|
-
reason: `Exact duplicate found`,
|
|
523
|
-
existingId: lesson.id
|
|
524
|
-
};
|
|
585
|
+
const results = [];
|
|
586
|
+
for (const line of content.split("\n")) {
|
|
587
|
+
const trimmed = line.trim();
|
|
588
|
+
if (!trimmed) continue;
|
|
589
|
+
try {
|
|
590
|
+
const parsed = JSON.parse(trimmed);
|
|
591
|
+
results.push({ line: trimmed, parsed });
|
|
592
|
+
} catch {
|
|
593
|
+
results.push({ line: trimmed, parsed: null });
|
|
525
594
|
}
|
|
526
595
|
}
|
|
527
|
-
return
|
|
596
|
+
return results;
|
|
528
597
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
/\btry to\b/i,
|
|
536
|
-
/\bdouble check\b/i
|
|
537
|
-
];
|
|
538
|
-
var GENERIC_IMPERATIVE_PATTERN = /^(always|never)\s+\w+(\s+\w+){0,2}$/i;
|
|
539
|
-
function isSpecific(insight) {
|
|
540
|
-
const words = insight.trim().split(/\s+/).filter((w) => w.length > 0);
|
|
541
|
-
if (words.length < MIN_WORD_COUNT) {
|
|
542
|
-
return { specific: false, reason: "Insight is too short to be actionable" };
|
|
543
|
-
}
|
|
544
|
-
for (const pattern of VAGUE_PATTERNS) {
|
|
545
|
-
if (pattern.test(insight)) {
|
|
546
|
-
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
598
|
+
async function countTombstones(repoRoot) {
|
|
599
|
+
const lines = await parseRawJsonlLines(repoRoot);
|
|
600
|
+
let count = 0;
|
|
601
|
+
for (const { parsed } of lines) {
|
|
602
|
+
if (parsed && parsed["deleted"] === true) {
|
|
603
|
+
count++;
|
|
547
604
|
}
|
|
548
605
|
}
|
|
549
|
-
|
|
550
|
-
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
551
|
-
}
|
|
552
|
-
return { specific: true };
|
|
606
|
+
return count;
|
|
553
607
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
608
|
+
async function needsCompaction(repoRoot) {
|
|
609
|
+
const count = await countTombstones(repoRoot);
|
|
610
|
+
return count >= TOMBSTONE_THRESHOLD;
|
|
611
|
+
}
|
|
612
|
+
async function rewriteWithoutTombstones(repoRoot) {
|
|
613
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
614
|
+
const tempPath = filePath + ".tmp";
|
|
615
|
+
const { lessons } = await readLessons(repoRoot);
|
|
616
|
+
const tombstoneCount = await countTombstones(repoRoot);
|
|
617
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
618
|
+
const lines = lessons.map((lesson) => JSON.stringify(lesson) + "\n");
|
|
619
|
+
await writeFile(tempPath, lines.join(""), "utf-8");
|
|
620
|
+
await rename(tempPath, filePath);
|
|
621
|
+
return tombstoneCount;
|
|
622
|
+
}
|
|
623
|
+
function shouldArchive(lesson) {
|
|
624
|
+
const ageDays = getLessonAgeDays(lesson);
|
|
625
|
+
return ageDays > ARCHIVE_AGE_DAYS && (lesson.retrievalCount === void 0 || lesson.retrievalCount === 0);
|
|
626
|
+
}
|
|
627
|
+
async function archiveOldLessons(repoRoot) {
|
|
628
|
+
const { lessons } = await readLessons(repoRoot);
|
|
629
|
+
const toArchive = [];
|
|
630
|
+
const toKeep = [];
|
|
631
|
+
for (const lesson of lessons) {
|
|
632
|
+
if (shouldArchive(lesson)) {
|
|
633
|
+
toArchive.push(lesson);
|
|
634
|
+
} else {
|
|
635
|
+
toKeep.push(lesson);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (toArchive.length === 0) {
|
|
639
|
+
return 0;
|
|
640
|
+
}
|
|
641
|
+
const archiveGroups = /* @__PURE__ */ new Map();
|
|
642
|
+
for (const lesson of toArchive) {
|
|
643
|
+
const created = new Date(lesson.created);
|
|
644
|
+
const archivePath = getArchivePath(repoRoot, created);
|
|
645
|
+
const group = archiveGroups.get(archivePath) ?? [];
|
|
646
|
+
group.push(lesson);
|
|
647
|
+
archiveGroups.set(archivePath, group);
|
|
648
|
+
}
|
|
649
|
+
const archiveDir = join(repoRoot, ARCHIVE_DIR);
|
|
650
|
+
await mkdir(archiveDir, { recursive: true });
|
|
651
|
+
for (const [archivePath, archiveLessons] of archiveGroups) {
|
|
652
|
+
const lines2 = archiveLessons.map((l) => JSON.stringify(l) + "\n").join("");
|
|
653
|
+
await appendFile(archivePath, lines2, "utf-8");
|
|
654
|
+
}
|
|
655
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
656
|
+
const tempPath = filePath + ".tmp";
|
|
657
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
658
|
+
const lines = toKeep.map((lesson) => JSON.stringify(lesson) + "\n");
|
|
659
|
+
await writeFile(tempPath, lines.join(""), "utf-8");
|
|
660
|
+
await rename(tempPath, filePath);
|
|
661
|
+
return toArchive.length;
|
|
662
|
+
}
|
|
663
|
+
async function compact(repoRoot) {
|
|
664
|
+
const tombstonesBefore = await countTombstones(repoRoot);
|
|
665
|
+
const archived = await archiveOldLessons(repoRoot);
|
|
666
|
+
const tombstonesAfterArchive = await countTombstones(repoRoot);
|
|
667
|
+
await rewriteWithoutTombstones(repoRoot);
|
|
668
|
+
const tombstonesRemoved = archived > 0 ? tombstonesBefore : tombstonesAfterArchive;
|
|
669
|
+
const { lessons } = await readLessons(repoRoot);
|
|
670
|
+
return {
|
|
671
|
+
archived,
|
|
672
|
+
tombstonesRemoved,
|
|
673
|
+
lessonsRemaining: lessons.length
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
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
|
+
};
|
|
703
|
+
}
|
|
704
|
+
if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
|
|
705
|
+
return {
|
|
706
|
+
novel: false,
|
|
707
|
+
reason: `Exact duplicate found`,
|
|
708
|
+
existingId: lesson.id
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return { novel: true };
|
|
713
|
+
}
|
|
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" };
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (GENERIC_IMPERATIVE_PATTERN.test(insight)) {
|
|
735
|
+
return { specific: false, reason: "Insight matches a vague pattern" };
|
|
736
|
+
}
|
|
737
|
+
return { specific: true };
|
|
738
|
+
}
|
|
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
|
|
569
754
|
];
|
|
570
755
|
function isActionable(insight) {
|
|
571
756
|
for (const pattern of ACTION_PATTERNS) {
|
|
@@ -720,181 +905,62 @@ async function parseInputFile(filePath) {
|
|
|
720
905
|
}
|
|
721
906
|
return data;
|
|
722
907
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
+
};
|
|
730
920
|
}
|
|
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;
|
|
731
930
|
|
|
732
|
-
// src/
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
content = await readFile(filePath, "utf-8");
|
|
748
|
-
} catch {
|
|
749
|
-
return [];
|
|
750
|
-
}
|
|
751
|
-
const results = [];
|
|
752
|
-
for (const line of content.split("\n")) {
|
|
753
|
-
const trimmed = line.trim();
|
|
754
|
-
if (!trimmed) continue;
|
|
755
|
-
try {
|
|
756
|
-
const parsed = JSON.parse(trimmed);
|
|
757
|
-
results.push({ line: trimmed, parsed });
|
|
758
|
-
} catch {
|
|
759
|
-
results.push({ line: trimmed, parsed: null });
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
return results;
|
|
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
|
+
};
|
|
763
946
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
return count;
|
|
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
|
+
}));
|
|
773
955
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const tombstoneCount = await countTombstones(repoRoot);
|
|
783
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
784
|
-
const lines = lessons.map((lesson) => JSON.stringify(lesson) + "\n");
|
|
785
|
-
await writeFile(tempPath, lines.join(""), "utf-8");
|
|
786
|
-
await rename(tempPath, filePath);
|
|
787
|
-
return tombstoneCount;
|
|
788
|
-
}
|
|
789
|
-
function shouldArchive(lesson) {
|
|
790
|
-
const ageDays = getLessonAgeDays(lesson);
|
|
791
|
-
return ageDays > ARCHIVE_AGE_DAYS && (lesson.retrievalCount === void 0 || lesson.retrievalCount === 0);
|
|
792
|
-
}
|
|
793
|
-
async function archiveOldLessons(repoRoot) {
|
|
794
|
-
const { lessons } = await readLessons(repoRoot);
|
|
795
|
-
const toArchive = [];
|
|
796
|
-
const toKeep = [];
|
|
797
|
-
for (const lesson of lessons) {
|
|
798
|
-
if (shouldArchive(lesson)) {
|
|
799
|
-
toArchive.push(lesson);
|
|
800
|
-
} else {
|
|
801
|
-
toKeep.push(lesson);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
if (toArchive.length === 0) {
|
|
805
|
-
return 0;
|
|
806
|
-
}
|
|
807
|
-
const archiveGroups = /* @__PURE__ */ new Map();
|
|
808
|
-
for (const lesson of toArchive) {
|
|
809
|
-
const created = new Date(lesson.created);
|
|
810
|
-
const archivePath = getArchivePath(repoRoot, created);
|
|
811
|
-
const group = archiveGroups.get(archivePath) ?? [];
|
|
812
|
-
group.push(lesson);
|
|
813
|
-
archiveGroups.set(archivePath, group);
|
|
814
|
-
}
|
|
815
|
-
const archiveDir = join(repoRoot, ARCHIVE_DIR);
|
|
816
|
-
await mkdir(archiveDir, { recursive: true });
|
|
817
|
-
for (const [archivePath, archiveLessons] of archiveGroups) {
|
|
818
|
-
const lines2 = archiveLessons.map((l) => JSON.stringify(l) + "\n").join("");
|
|
819
|
-
await appendFile(archivePath, lines2, "utf-8");
|
|
820
|
-
}
|
|
821
|
-
const filePath = join(repoRoot, LESSONS_PATH);
|
|
822
|
-
const tempPath = filePath + ".tmp";
|
|
823
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
824
|
-
const lines = toKeep.map((lesson) => JSON.stringify(lesson) + "\n");
|
|
825
|
-
await writeFile(tempPath, lines.join(""), "utf-8");
|
|
826
|
-
await rename(tempPath, filePath);
|
|
827
|
-
return toArchive.length;
|
|
828
|
-
}
|
|
829
|
-
async function compact(repoRoot) {
|
|
830
|
-
const tombstonesBefore = await countTombstones(repoRoot);
|
|
831
|
-
const archived = await archiveOldLessons(repoRoot);
|
|
832
|
-
const tombstonesAfterArchive = await countTombstones(repoRoot);
|
|
833
|
-
await rewriteWithoutTombstones(repoRoot);
|
|
834
|
-
const tombstonesRemoved = archived > 0 ? tombstonesBefore : tombstonesAfterArchive;
|
|
835
|
-
const { lessons } = await readLessons(repoRoot);
|
|
836
|
-
return {
|
|
837
|
-
archived,
|
|
838
|
-
tombstonesRemoved,
|
|
839
|
-
lessonsRemaining: lessons.length
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
var out = {
|
|
843
|
-
success: (msg) => console.log(chalk.green("[ok]"), msg),
|
|
844
|
-
error: (msg) => console.error(chalk.red("[error]"), msg),
|
|
845
|
-
info: (msg) => console.log(chalk.blue("[info]"), msg),
|
|
846
|
-
warn: (msg) => console.log(chalk.yellow("[warn]"), msg)
|
|
847
|
-
};
|
|
848
|
-
function getGlobalOpts(cmd) {
|
|
849
|
-
const opts = cmd.optsWithGlobals();
|
|
850
|
-
return {
|
|
851
|
-
verbose: opts.verbose ?? false,
|
|
852
|
-
quiet: opts.quiet ?? false
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
var DEFAULT_SEARCH_LIMIT = "10";
|
|
856
|
-
var DEFAULT_LIST_LIMIT = "20";
|
|
857
|
-
var DEFAULT_CHECK_PLAN_LIMIT = "5";
|
|
858
|
-
var LESSON_COUNT_WARNING_THRESHOLD = 20;
|
|
859
|
-
var AGE_FLAG_THRESHOLD_DAYS = 90;
|
|
860
|
-
var ISO_DATE_PREFIX_LENGTH = 10;
|
|
861
|
-
var AVG_DECIMAL_PLACES = 1;
|
|
862
|
-
var RELEVANCE_DECIMAL_PLACES = 2;
|
|
863
|
-
var JSON_INDENT_SPACES = 2;
|
|
864
|
-
|
|
865
|
-
// src/commands/capture.ts
|
|
866
|
-
function createLessonFromFlags(trigger, insight, confirmed) {
|
|
867
|
-
return {
|
|
868
|
-
id: generateId(insight),
|
|
869
|
-
type: "quick",
|
|
870
|
-
trigger,
|
|
871
|
-
insight,
|
|
872
|
-
tags: [],
|
|
873
|
-
source: "manual",
|
|
874
|
-
context: { tool: "capture", intent: "manual capture" },
|
|
875
|
-
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
876
|
-
confirmed,
|
|
877
|
-
supersedes: [],
|
|
878
|
-
related: []
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
function outputCaptureJson(lesson, saved) {
|
|
882
|
-
console.log(JSON.stringify({
|
|
883
|
-
id: lesson.id,
|
|
884
|
-
trigger: lesson.trigger,
|
|
885
|
-
insight: lesson.insight,
|
|
886
|
-
type: lesson.type,
|
|
887
|
-
saved
|
|
888
|
-
}));
|
|
889
|
-
}
|
|
890
|
-
function outputCapturePreview(lesson) {
|
|
891
|
-
console.log("Lesson captured:");
|
|
892
|
-
console.log(` ID: ${lesson.id}`);
|
|
893
|
-
console.log(` Trigger: ${lesson.trigger}`);
|
|
894
|
-
console.log(` Insight: ${lesson.insight}`);
|
|
895
|
-
console.log(` Type: ${lesson.type}`);
|
|
896
|
-
console.log(` Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
|
|
897
|
-
console.log("\nSave this lesson? [y/n]");
|
|
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]");
|
|
898
964
|
}
|
|
899
965
|
function createLessonFromInputFile(result, confirmed) {
|
|
900
966
|
return {
|
|
@@ -1057,153 +1123,249 @@ Saved as lesson: ${lesson.id}`);
|
|
|
1057
1123
|
}
|
|
1058
1124
|
});
|
|
1059
1125
|
}
|
|
1060
|
-
function
|
|
1061
|
-
|
|
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}`);
|
|
1134
|
+
}
|
|
1135
|
+
if (lesson.severity) {
|
|
1136
|
+
lines.push(`Severity: ${lesson.severity}`);
|
|
1137
|
+
}
|
|
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) => {
|
|
1062
1183
|
const repoRoot = getRepoRoot();
|
|
1063
1184
|
const { lessons } = await readLessons(repoRoot);
|
|
1064
1185
|
const lesson = lessons.find((l) => l.id === id);
|
|
1065
1186
|
if (!lesson) {
|
|
1066
|
-
|
|
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
|
+
}
|
|
1067
1193
|
process.exit(1);
|
|
1068
1194
|
}
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
const updatedLesson = {
|
|
1074
|
-
...lesson,
|
|
1075
|
-
invalidatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1076
|
-
...options.reason !== void 0 && { invalidationReason: options.reason }
|
|
1077
|
-
};
|
|
1078
|
-
await appendLesson(repoRoot, updatedLesson);
|
|
1079
|
-
out.success(`Lesson ${id} marked as invalid.`);
|
|
1080
|
-
if (options.reason) {
|
|
1081
|
-
console.log(` Reason: ${options.reason}`);
|
|
1195
|
+
if (options.json) {
|
|
1196
|
+
console.log(JSON.stringify(lesson, null, SHOW_JSON_INDENT));
|
|
1197
|
+
} else {
|
|
1198
|
+
console.log(formatLessonHuman(lesson));
|
|
1082
1199
|
}
|
|
1083
1200
|
});
|
|
1084
|
-
program2.command("
|
|
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) => {
|
|
1085
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
|
+
}
|
|
1086
1212
|
const { lessons } = await readLessons(repoRoot);
|
|
1087
1213
|
const lesson = lessons.find((l) => l.id === id);
|
|
1088
1214
|
if (!lesson) {
|
|
1089
|
-
|
|
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
|
+
}
|
|
1090
1221
|
process.exit(1);
|
|
1091
1222
|
}
|
|
1092
|
-
if (
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
+
}
|
|
1095
1233
|
}
|
|
1096
1234
|
const updatedLesson = {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
trigger:
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
// Include optional fields if present (excluding invalidation)
|
|
1109
|
-
...lesson.evidence !== void 0 && { evidence: lesson.evidence },
|
|
1110
|
-
...lesson.severity !== void 0 && { severity: lesson.severity },
|
|
1111
|
-
...lesson.pattern !== void 0 && { pattern: lesson.pattern },
|
|
1112
|
-
...lesson.deleted !== void 0 && { deleted: lesson.deleted },
|
|
1113
|
-
...lesson.retrievalCount !== void 0 && { retrievalCount: lesson.retrievalCount },
|
|
1114
|
-
...lesson.citation !== void 0 && { citation: lesson.citation },
|
|
1115
|
-
...lesson.compactionLevel !== void 0 && { compactionLevel: lesson.compactionLevel },
|
|
1116
|
-
...lesson.compactedAt !== void 0 && { compactedAt: lesson.compactedAt },
|
|
1117
|
-
...lesson.lastRetrieved !== void 0 && { lastRetrieved: lesson.lastRetrieved }
|
|
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" }
|
|
1118
1246
|
};
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
console.log("Dry run - no changes will be made.\n");
|
|
1128
|
-
console.log(`Tombstones found: ${tombstones}`);
|
|
1129
|
-
console.log(`Compaction needed: ${needs ? "yes" : "no"}`);
|
|
1130
|
-
return;
|
|
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);
|
|
1131
1255
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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}`);
|
|
1136
1262
|
}
|
|
1137
|
-
console.log("Running compaction...");
|
|
1138
|
-
const result = await compact(repoRoot);
|
|
1139
|
-
console.log("\nCompaction complete:");
|
|
1140
|
-
console.log(` Archived: ${result.archived} lesson(s)`);
|
|
1141
|
-
console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
|
|
1142
|
-
console.log(` Lessons remaining: ${result.lessonsRemaining}`);
|
|
1143
|
-
await rebuildIndex(repoRoot);
|
|
1144
|
-
console.log(" Index rebuilt.");
|
|
1145
1263
|
});
|
|
1146
|
-
program2.command("
|
|
1264
|
+
program2.command("delete <ids...>").description("Soft delete lessons (creates tombstone)").option("--json", "Output as JSON").action(async (ids, options) => {
|
|
1147
1265
|
const repoRoot = getRepoRoot();
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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()
|
|
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 }));
|
|
1152
1290
|
} else {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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);
|
|
1158
1299
|
}
|
|
1159
1300
|
}
|
|
1160
1301
|
});
|
|
1161
|
-
|
|
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) {
|
|
1162
1307
|
const repoRoot = getRepoRoot();
|
|
1163
|
-
await syncIfNeeded(repoRoot);
|
|
1164
1308
|
const { lessons } = await readLessons(repoRoot);
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
|
|
1170
|
-
const jsonlPath = join(repoRoot, LESSONS_PATH);
|
|
1171
|
-
const dbPath = join(repoRoot, DB_PATH);
|
|
1172
|
-
let dataSize = 0;
|
|
1173
|
-
let indexSize = 0;
|
|
1174
|
-
try {
|
|
1175
|
-
dataSize = statSync(jsonlPath).size;
|
|
1176
|
-
} catch {
|
|
1309
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1310
|
+
if (!lesson) {
|
|
1311
|
+
out.error(`Lesson not found: ${id}`);
|
|
1312
|
+
process.exit(1);
|
|
1177
1313
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1314
|
+
if (lesson.invalidatedAt) {
|
|
1315
|
+
out.warn(`Lesson ${id} is already marked as invalid.`);
|
|
1316
|
+
return;
|
|
1181
1317
|
}
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
mediumCount++;
|
|
1192
|
-
} else {
|
|
1193
|
-
oldCount++;
|
|
1194
|
-
}
|
|
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}`);
|
|
1195
1327
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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);
|
|
1200
1336
|
}
|
|
1201
|
-
if (
|
|
1202
|
-
|
|
1337
|
+
if (!lesson.invalidatedAt) {
|
|
1338
|
+
out.info(`Lesson ${id} is not invalidated.`);
|
|
1339
|
+
return;
|
|
1203
1340
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
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).`);
|
|
1206
1366
|
});
|
|
1367
|
+
}
|
|
1368
|
+
function registerIOCommands(program2) {
|
|
1207
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) => {
|
|
1208
1370
|
const repoRoot = getRepoRoot();
|
|
1209
1371
|
const { lessons } = await readLessons(repoRoot);
|
|
@@ -1276,215 +1438,132 @@ function registerManagementCommands(program2) {
|
|
|
1276
1438
|
console.log(`Imported ${imported} ${lessonWord}`);
|
|
1277
1439
|
}
|
|
1278
1440
|
});
|
|
1279
|
-
|
|
1280
|
-
|
|
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) => {
|
|
1281
1444
|
const repoRoot = getRepoRoot();
|
|
1282
|
-
const
|
|
1283
|
-
const
|
|
1284
|
-
if (
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
const lines = content.split("\n");
|
|
1290
|
-
for (const line of lines) {
|
|
1291
|
-
const trimmed = line.trim();
|
|
1292
|
-
if (!trimmed) continue;
|
|
1293
|
-
try {
|
|
1294
|
-
const record = JSON.parse(trimmed);
|
|
1295
|
-
if (record.id === id && record.deleted === true) {
|
|
1296
|
-
wasDeleted = true;
|
|
1297
|
-
break;
|
|
1298
|
-
}
|
|
1299
|
-
} catch {
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
} catch {
|
|
1303
|
-
}
|
|
1304
|
-
if (options.json) {
|
|
1305
|
-
console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found` }));
|
|
1306
|
-
} else {
|
|
1307
|
-
out.error(wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found`);
|
|
1308
|
-
}
|
|
1309
|
-
process.exit(1);
|
|
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;
|
|
1310
1452
|
}
|
|
1311
|
-
if (options.
|
|
1312
|
-
console.log(
|
|
1313
|
-
|
|
1314
|
-
|
|
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;
|
|
1315
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.");
|
|
1316
1466
|
});
|
|
1317
|
-
program2.command("
|
|
1467
|
+
program2.command("rebuild").description("Rebuild SQLite index from JSONL").option("-f, --force", "Force rebuild even if unchanged").action(async (options) => {
|
|
1318
1468
|
const repoRoot = getRepoRoot();
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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).");
|
|
1323
1477
|
} else {
|
|
1324
|
-
|
|
1478
|
+
console.log("Index is up to date.");
|
|
1325
1479
|
}
|
|
1326
|
-
process.exit(1);
|
|
1327
1480
|
}
|
|
1481
|
+
});
|
|
1482
|
+
program2.command("stats").description("Show database health and statistics").action(async () => {
|
|
1483
|
+
const repoRoot = getRepoRoot();
|
|
1484
|
+
await syncIfNeeded(repoRoot);
|
|
1328
1485
|
const { lessons } = await readLessons(repoRoot);
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if (record.id === id && record.deleted === true) {
|
|
1342
|
-
wasDeleted = true;
|
|
1343
|
-
break;
|
|
1344
|
-
}
|
|
1345
|
-
} catch {
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
} catch {
|
|
1349
|
-
}
|
|
1350
|
-
if (options.json) {
|
|
1351
|
-
console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found` }));
|
|
1352
|
-
} else {
|
|
1353
|
-
out.error(wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found`);
|
|
1354
|
-
}
|
|
1355
|
-
process.exit(1);
|
|
1356
|
-
}
|
|
1357
|
-
if (options.severity !== void 0) {
|
|
1358
|
-
const result = SeveritySchema.safeParse(options.severity);
|
|
1359
|
-
if (!result.success) {
|
|
1360
|
-
if (options.json) {
|
|
1361
|
-
console.log(JSON.stringify({ error: `Invalid severity '${options.severity}' (must be: high, medium, low)` }));
|
|
1362
|
-
} else {
|
|
1363
|
-
out.error(`Invalid severity '${options.severity}' (must be: high, medium, low)`);
|
|
1364
|
-
}
|
|
1365
|
-
process.exit(1);
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
const updatedLesson = {
|
|
1369
|
-
...lesson,
|
|
1370
|
-
...options.insight !== void 0 && { insight: options.insight },
|
|
1371
|
-
...options.trigger !== void 0 && { trigger: options.trigger },
|
|
1372
|
-
...options.evidence !== void 0 && { evidence: options.evidence },
|
|
1373
|
-
...options.severity !== void 0 && { severity: options.severity },
|
|
1374
|
-
...options.tags !== void 0 && {
|
|
1375
|
-
tags: [...new Set(
|
|
1376
|
-
options.tags.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
|
|
1377
|
-
)]
|
|
1378
|
-
},
|
|
1379
|
-
...options.confirmed !== void 0 && { confirmed: options.confirmed === "true" }
|
|
1380
|
-
};
|
|
1381
|
-
const validationResult = LessonSchema.safeParse(updatedLesson);
|
|
1382
|
-
if (!validationResult.success) {
|
|
1383
|
-
if (options.json) {
|
|
1384
|
-
console.log(JSON.stringify({ error: `Schema validation failed: ${validationResult.error.message}` }));
|
|
1385
|
-
} else {
|
|
1386
|
-
out.error(`Schema validation failed: ${validationResult.error.message}`);
|
|
1387
|
-
}
|
|
1388
|
-
process.exit(1);
|
|
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 {
|
|
1389
1498
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
console.log(JSON.stringify(updatedLesson, null, SHOW_JSON_INDENT));
|
|
1394
|
-
} else {
|
|
1395
|
-
out.success(`Updated lesson ${id}`);
|
|
1499
|
+
try {
|
|
1500
|
+
indexSize = statSync(dbPath).size;
|
|
1501
|
+
} catch {
|
|
1396
1502
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
continue;
|
|
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++;
|
|
1410
1515
|
}
|
|
1411
|
-
const tombstone = {
|
|
1412
|
-
...lesson,
|
|
1413
|
-
deleted: true,
|
|
1414
|
-
deletedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1415
|
-
};
|
|
1416
|
-
await appendLesson(repoRoot, tombstone);
|
|
1417
|
-
deleted.push(id);
|
|
1418
1516
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
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\`.`);
|
|
1421
1521
|
}
|
|
1422
|
-
if (
|
|
1423
|
-
console.log(
|
|
1424
|
-
} else {
|
|
1425
|
-
if (deleted.length > 0) {
|
|
1426
|
-
out.success(`Deleted ${deleted.length} lesson(s): ${deleted.join(", ")}`);
|
|
1427
|
-
}
|
|
1428
|
-
for (const warning of warnings) {
|
|
1429
|
-
out.warn(`${warning.id}: ${warning.message}`);
|
|
1430
|
-
}
|
|
1431
|
-
if (deleted.length === 0 && warnings.length > 0) {
|
|
1432
|
-
process.exit(1);
|
|
1433
|
-
}
|
|
1522
|
+
if (totalLessons > 0) {
|
|
1523
|
+
console.log(`Age: ${recentCount} <30d, ${mediumCount} 30-90d, ${oldCount} >90d`);
|
|
1434
1524
|
}
|
|
1525
|
+
console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
|
|
1526
|
+
console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
|
|
1435
1527
|
});
|
|
1436
1528
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1467
|
-
return lines.join("\n");
|
|
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
|
+
});
|
|
1468
1558
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
try {
|
|
1478
|
-
const record = JSON.parse(trimmed);
|
|
1479
|
-
if (record.id === id && record.deleted === true) {
|
|
1480
|
-
return true;
|
|
1481
|
-
}
|
|
1482
|
-
} catch {
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
} catch {
|
|
1486
|
-
}
|
|
1487
|
-
return false;
|
|
1559
|
+
|
|
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);
|
|
1488
1567
|
}
|
|
1489
1568
|
var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
|
|
1490
1569
|
var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
|
|
@@ -1617,7 +1696,7 @@ async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
|
|
|
1617
1696
|
return { lessons: topLessons, message };
|
|
1618
1697
|
}
|
|
1619
1698
|
function formatLessonsCheck(lessons) {
|
|
1620
|
-
const header = "
|
|
1699
|
+
const header = "Lessons Check\n" + "\u2500".repeat(40);
|
|
1621
1700
|
if (lessons.length === 0) {
|
|
1622
1701
|
return `${header}
|
|
1623
1702
|
No relevant lessons found for this plan.`;
|
|
@@ -1632,7 +1711,7 @@ ${lessonLines.join("\n")}`;
|
|
|
1632
1711
|
}
|
|
1633
1712
|
|
|
1634
1713
|
// src/index.ts
|
|
1635
|
-
var VERSION = "0.
|
|
1714
|
+
var VERSION = "0.2.3";
|
|
1636
1715
|
|
|
1637
1716
|
// src/commands/retrieval.ts
|
|
1638
1717
|
async function readPlanFromStdin() {
|
|
@@ -1807,12 +1886,12 @@ function registerRetrievalCommands(program2) {
|
|
|
1807
1886
|
if (options.json) {
|
|
1808
1887
|
console.log(JSON.stringify({
|
|
1809
1888
|
error: "Embedding model not available",
|
|
1810
|
-
action: "Run: npx
|
|
1889
|
+
action: "Run: npx lna download-model"
|
|
1811
1890
|
}));
|
|
1812
1891
|
} else {
|
|
1813
1892
|
out.error("Embedding model not available");
|
|
1814
1893
|
console.log("");
|
|
1815
|
-
console.log("Run: npx
|
|
1894
|
+
console.log("Run: npx lna download-model");
|
|
1816
1895
|
}
|
|
1817
1896
|
process.exit(1);
|
|
1818
1897
|
}
|
|
@@ -1838,34 +1917,69 @@ function registerRetrievalCommands(program2) {
|
|
|
1838
1917
|
}
|
|
1839
1918
|
});
|
|
1840
1919
|
}
|
|
1920
|
+
|
|
1921
|
+
// src/commands/setup/templates.ts
|
|
1841
1922
|
var PRE_COMMIT_MESSAGE = `Before committing, have you captured any valuable lessons from this session?
|
|
1842
1923
|
Consider: corrections, mistakes, or insights worth remembering.
|
|
1843
1924
|
|
|
1844
1925
|
To capture a lesson:
|
|
1845
|
-
npx
|
|
1926
|
+
npx lna capture --trigger "what happened" --insight "what to do" --yes`;
|
|
1846
1927
|
var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
|
|
1847
1928
|
# Learning Agent pre-commit hook
|
|
1848
1929
|
# Reminds Claude to consider capturing lessons before commits
|
|
1849
1930
|
|
|
1850
|
-
npx
|
|
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
|
|
1851
1937
|
`;
|
|
1852
|
-
var
|
|
1938
|
+
var CLAUDE_HOOK_MARKERS = ["lna load-session", "learning-agent load-session"];
|
|
1853
1939
|
var CLAUDE_HOOK_CONFIG = {
|
|
1854
1940
|
matcher: "startup|resume|compact",
|
|
1855
1941
|
hooks: [
|
|
1856
1942
|
{
|
|
1857
1943
|
type: "command",
|
|
1858
|
-
command: "npx
|
|
1944
|
+
command: "npx lna load-session 2>/dev/null || true"
|
|
1859
1945
|
}
|
|
1860
1946
|
]
|
|
1861
1947
|
};
|
|
1862
|
-
var HOOK_MARKER = "# Learning Agent pre-commit hook";
|
|
1863
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 -->";
|
|
1864
1959
|
var AGENTS_MD_TEMPLATE = `
|
|
1960
|
+
${AGENTS_SECTION_START_MARKER}
|
|
1865
1961
|
## Learning Agent Integration
|
|
1866
1962
|
|
|
1867
1963
|
This project uses learning-agent for session memory.
|
|
1868
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
|
+
|
|
1869
1983
|
### Retrieval Points
|
|
1870
1984
|
|
|
1871
1985
|
- **Session start**: High-severity lessons loaded automatically
|
|
@@ -1876,7 +1990,7 @@ This project uses learning-agent for session memory.
|
|
|
1876
1990
|
**BEFORE implementing any plan**, run:
|
|
1877
1991
|
|
|
1878
1992
|
\`\`\`bash
|
|
1879
|
-
npx
|
|
1993
|
+
npx lna check-plan --plan "your plan description" --json
|
|
1880
1994
|
\`\`\`
|
|
1881
1995
|
|
|
1882
1996
|
Display results as a **Lessons Check** section after your plan:
|
|
@@ -1889,6 +2003,33 @@ Display results as a **Lessons Check** section after your plan:
|
|
|
1889
2003
|
|
|
1890
2004
|
Consider each lesson while implementing.
|
|
1891
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
|
+
|
|
1892
2033
|
### Proposing Lessons
|
|
1893
2034
|
|
|
1894
2035
|
Propose when: user correction, self-correction, test failure fix, or manual request.
|
|
@@ -1912,45 +2053,428 @@ Before closing a session, reflect on lessons learned:
|
|
|
1912
2053
|
1. **Review**: What mistakes or corrections happened?
|
|
1913
2054
|
2. **Quality gate**: Is it novel, specific, actionable?
|
|
1914
2055
|
3. **Propose**: "Learned: [insight]. Save? [y/n]"
|
|
1915
|
-
4. **Capture**: \`npx
|
|
2056
|
+
4. **Capture**: \`npx lna capture --trigger "..." --insight "..." --yes\`
|
|
1916
2057
|
|
|
1917
2058
|
### CLI Commands
|
|
1918
2059
|
|
|
1919
2060
|
\`\`\`bash
|
|
1920
|
-
npx
|
|
1921
|
-
npx
|
|
1922
|
-
npx
|
|
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
|
|
1923
2065
|
\`\`\`
|
|
1924
2066
|
|
|
1925
2067
|
See [AGENTS.md](https://github.com/Nathandela/learning_agent/blob/main/AGENTS.md) for full documentation.
|
|
2068
|
+
${AGENTS_SECTION_END_MARKER}
|
|
1926
2069
|
`;
|
|
1927
|
-
|
|
1928
|
-
|
|
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");
|
|
1929
2162
|
}
|
|
1930
|
-
async function
|
|
1931
|
-
|
|
1932
|
-
|
|
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);
|
|
1933
2169
|
}
|
|
1934
|
-
|
|
1935
|
-
const
|
|
1936
|
-
if (!
|
|
1937
|
-
|
|
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
|
+
);
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
function addLearningAgentHook(settings) {
|
|
2181
|
+
if (!settings.hooks) {
|
|
2182
|
+
settings.hooks = {};
|
|
1938
2183
|
}
|
|
2184
|
+
const hooks = settings.hooks;
|
|
2185
|
+
if (!hooks.SessionStart) {
|
|
2186
|
+
hooks.SessionStart = [];
|
|
2187
|
+
}
|
|
2188
|
+
hooks.SessionStart.push(CLAUDE_HOOK_CONFIG);
|
|
1939
2189
|
}
|
|
1940
|
-
|
|
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;
|
|
2201
|
+
}
|
|
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);
|
|
2208
|
+
}
|
|
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) };
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
async function removeAgentsSection(repoRoot) {
|
|
1941
2229
|
const agentsPath = join(repoRoot, "AGENTS.md");
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2230
|
+
if (!existsSync(agentsPath)) {
|
|
2231
|
+
return false;
|
|
2232
|
+
}
|
|
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;
|
|
2248
|
+
}
|
|
2249
|
+
async function removeClaudeMdReference(repoRoot) {
|
|
2250
|
+
const claudeMdPath = join(repoRoot, ".claude", "CLAUDE.md");
|
|
2251
|
+
if (!existsSync(claudeMdPath)) {
|
|
2252
|
+
return false;
|
|
2253
|
+
}
|
|
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;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
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) {
|
|
2290
|
+
const repoRoot = getRepoRoot();
|
|
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.");
|
|
2336
|
+
} else {
|
|
2337
|
+
out.error("Not connected.");
|
|
2338
|
+
console.log("");
|
|
2339
|
+
console.log("Run 'npx lna init' to set up Learning Agent.");
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
if (options.uninstall) {
|
|
2345
|
+
const repoRoot = getRepoRoot();
|
|
2346
|
+
if (options.dryRun) {
|
|
2347
|
+
if (options.json) {
|
|
2348
|
+
console.log(JSON.stringify({ dryRun: true, wouldRemove: alreadyInstalled, location: displayPath }));
|
|
2349
|
+
} else {
|
|
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
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
const removedHook = removeLearningAgentHook(settings);
|
|
2359
|
+
if (removedHook) {
|
|
2360
|
+
await writeClaudeSettings(settingsPath, settings);
|
|
2361
|
+
}
|
|
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
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
if (options.dryRun) {
|
|
2401
|
+
if (options.json) {
|
|
2402
|
+
console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
|
|
2403
|
+
} else {
|
|
2404
|
+
if (alreadyInstalled) {
|
|
2405
|
+
console.log("Learning agent hooks already installed");
|
|
2406
|
+
} else {
|
|
2407
|
+
console.log(`Would install learning-agent hooks to ${displayPath}`);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
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;
|
|
2425
|
+
}
|
|
2426
|
+
const fileExists = existsSync(settingsPath);
|
|
2427
|
+
addLearningAgentHook(settings);
|
|
2428
|
+
await writeClaudeSettings(settingsPath, settings);
|
|
2429
|
+
if (options.json) {
|
|
2430
|
+
console.log(JSON.stringify({
|
|
2431
|
+
installed: true,
|
|
2432
|
+
location: displayPath,
|
|
2433
|
+
hooks: ["SessionStart"],
|
|
2434
|
+
action: fileExists ? "updated" : "created"
|
|
2435
|
+
}));
|
|
2436
|
+
} else {
|
|
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
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
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;
|
|
2463
|
+
}
|
|
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)}`);
|
|
1949
2476
|
}
|
|
1950
|
-
}
|
|
1951
|
-
const newContent = existed ? content.trimEnd() + "\n" + AGENTS_MD_TEMPLATE : AGENTS_MD_TEMPLATE.trim() + "\n";
|
|
1952
|
-
await writeFile(agentsPath, newContent, "utf-8");
|
|
1953
|
-
return true;
|
|
2477
|
+
});
|
|
1954
2478
|
}
|
|
1955
2479
|
var HOOK_FILE_MODE = 493;
|
|
1956
2480
|
function hasLearningAgentHook(content) {
|
|
@@ -1973,10 +2497,6 @@ async function getGitHooksDir(repoRoot) {
|
|
|
1973
2497
|
const defaultHooksDir = join(gitDir, "hooks");
|
|
1974
2498
|
return existsSync(defaultHooksDir) ? defaultHooksDir : null;
|
|
1975
2499
|
}
|
|
1976
|
-
var LEARNING_AGENT_HOOK_BLOCK = `
|
|
1977
|
-
# Learning Agent pre-commit hook (appended)
|
|
1978
|
-
npx learning-agent hooks run pre-commit
|
|
1979
|
-
`;
|
|
1980
2500
|
function findFirstTopLevelExitLine(lines) {
|
|
1981
2501
|
let insideFunction = 0;
|
|
1982
2502
|
let heredocDelimiter = null;
|
|
@@ -2037,57 +2557,97 @@ async function installPreCommitHook(repoRoot) {
|
|
|
2037
2557
|
chmodSync(hookPath, HOOK_FILE_MODE);
|
|
2038
2558
|
return true;
|
|
2039
2559
|
}
|
|
2040
|
-
function
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
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
|
+
});
|
|
2046
2578
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
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;
|
|
2050
2593
|
}
|
|
2051
|
-
const content = await readFile(
|
|
2052
|
-
|
|
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;
|
|
2053
2601
|
}
|
|
2054
|
-
function
|
|
2055
|
-
const
|
|
2056
|
-
|
|
2057
|
-
return hooks.SessionStart.some((entry) => {
|
|
2058
|
-
const hookEntry = entry;
|
|
2059
|
-
return hookEntry.hooks?.some((h) => h.command?.includes(CLAUDE_HOOK_MARKER));
|
|
2060
|
-
});
|
|
2602
|
+
async function createLessonsDirectory(repoRoot) {
|
|
2603
|
+
const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
|
|
2604
|
+
await mkdir(lessonsDir, { recursive: true });
|
|
2061
2605
|
}
|
|
2062
|
-
function
|
|
2063
|
-
|
|
2064
|
-
|
|
2606
|
+
async function createIndexFile(repoRoot) {
|
|
2607
|
+
const indexPath = join(repoRoot, LESSONS_PATH);
|
|
2608
|
+
if (!existsSync(indexPath)) {
|
|
2609
|
+
await writeFile(indexPath, "", "utf-8");
|
|
2065
2610
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
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
|
+
}
|
|
2069
2622
|
}
|
|
2070
|
-
|
|
2623
|
+
const newContent = existed ? content.trimEnd() + "\n" + AGENTS_MD_TEMPLATE : AGENTS_MD_TEMPLATE.trim() + "\n";
|
|
2624
|
+
await writeFile(agentsPath, newContent, "utf-8");
|
|
2625
|
+
return true;
|
|
2071
2626
|
}
|
|
2072
|
-
function
|
|
2073
|
-
const
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
return hooks.SessionStart.length < originalLength;
|
|
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;
|
|
2081
2635
|
}
|
|
2082
|
-
async function
|
|
2083
|
-
const
|
|
2084
|
-
await mkdir(
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
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;
|
|
2088
2648
|
}
|
|
2089
|
-
function
|
|
2090
|
-
program2.command("init").description("Initialize learning-agent in this repository").option("--skip-agents", "Skip AGENTS.md modification").option("--skip-hooks", "Skip git hooks installation").option("--json", "Output result as JSON").action(async function(options) {
|
|
2649
|
+
function registerInitCommand(program2) {
|
|
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) {
|
|
2091
2651
|
const repoRoot = getRepoRoot();
|
|
2092
2652
|
const { quiet } = getGlobalOpts(this);
|
|
2093
2653
|
await createLessonsDirectory(repoRoot);
|
|
@@ -2097,16 +2657,33 @@ function registerSetupCommands(program2) {
|
|
|
2097
2657
|
if (!options.skipAgents) {
|
|
2098
2658
|
agentsMdUpdated = await updateAgentsMd(repoRoot);
|
|
2099
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
|
+
}
|
|
2100
2670
|
let hooksInstalled = false;
|
|
2101
2671
|
if (!options.skipHooks) {
|
|
2102
2672
|
hooksInstalled = await installPreCommitHook(repoRoot);
|
|
2103
2673
|
}
|
|
2674
|
+
let claudeHooksResult = { action: "error", error: "skipped" };
|
|
2675
|
+
if (!options.skipClaude) {
|
|
2676
|
+
claudeHooksResult = await installClaudeHooksForInit(repoRoot);
|
|
2677
|
+
}
|
|
2104
2678
|
if (options.json) {
|
|
2679
|
+
const claudeHooksInstalled = claudeHooksResult.action === "installed";
|
|
2105
2680
|
console.log(JSON.stringify({
|
|
2106
2681
|
initialized: true,
|
|
2107
2682
|
lessonsDir,
|
|
2108
2683
|
agentsMd: agentsMdUpdated,
|
|
2109
|
-
|
|
2684
|
+
slashCommands: slashCommandsCreated || !options.skipAgents,
|
|
2685
|
+
hooks: hooksInstalled,
|
|
2686
|
+
claudeHooks: claudeHooksInstalled
|
|
2110
2687
|
}));
|
|
2111
2688
|
} else if (!quiet) {
|
|
2112
2689
|
out.success("Learning agent initialized");
|
|
@@ -2118,6 +2695,13 @@ function registerSetupCommands(program2) {
|
|
|
2118
2695
|
} else {
|
|
2119
2696
|
console.log(" AGENTS.md: Already has Learning Agent section");
|
|
2120
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
|
+
}
|
|
2121
2705
|
if (hooksInstalled) {
|
|
2122
2706
|
console.log(" Git hooks: pre-commit hook installed");
|
|
2123
2707
|
} else if (options.skipHooks) {
|
|
@@ -2125,156 +2709,42 @@ function registerSetupCommands(program2) {
|
|
|
2125
2709
|
} else {
|
|
2126
2710
|
console.log(" Git hooks: Already installed or not a git repo");
|
|
2127
2711
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
console.log(PRE_COMMIT_MESSAGE);
|
|
2137
|
-
}
|
|
2138
|
-
} else {
|
|
2139
|
-
if (options.json) {
|
|
2140
|
-
console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
|
|
2141
|
-
} else {
|
|
2142
|
-
out.error(`Unknown hook: ${hook}`);
|
|
2143
|
-
}
|
|
2144
|
-
process.exit(1);
|
|
2145
|
-
}
|
|
2146
|
-
});
|
|
2147
|
-
const setupCommand = program2.command("setup").description("Setup integrations");
|
|
2148
|
-
setupCommand.command("claude").description("Install Claude Code SessionStart hooks").option("--global", "Install to global ~/.claude/ instead of project").option("--uninstall", "Remove learning-agent hooks").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action(async (options) => {
|
|
2149
|
-
const settingsPath = getClaudeSettingsPath(options.global ?? false);
|
|
2150
|
-
const displayPath = options.global ? "~/.claude/settings.json" : ".claude/settings.json";
|
|
2151
|
-
let settings;
|
|
2152
|
-
try {
|
|
2153
|
-
settings = await readClaudeSettings(settingsPath);
|
|
2154
|
-
} catch {
|
|
2155
|
-
if (options.json) {
|
|
2156
|
-
console.log(JSON.stringify({ error: "Failed to parse settings file" }));
|
|
2157
|
-
} else {
|
|
2158
|
-
out.error("Failed to parse settings file. Check if JSON is valid.");
|
|
2159
|
-
}
|
|
2160
|
-
process.exit(1);
|
|
2161
|
-
}
|
|
2162
|
-
const alreadyInstalled = hasClaudeHook(settings);
|
|
2163
|
-
if (options.uninstall) {
|
|
2164
|
-
if (options.dryRun) {
|
|
2165
|
-
if (options.json) {
|
|
2166
|
-
console.log(JSON.stringify({ dryRun: true, wouldRemove: alreadyInstalled, location: displayPath }));
|
|
2167
|
-
} else {
|
|
2168
|
-
if (alreadyInstalled) {
|
|
2169
|
-
console.log(`Would remove learning-agent hooks from ${displayPath}`);
|
|
2170
|
-
} else {
|
|
2171
|
-
console.log("No learning-agent hooks to remove");
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
return;
|
|
2175
|
-
}
|
|
2176
|
-
const removed = removeLearningAgentHook(settings);
|
|
2177
|
-
if (removed) {
|
|
2178
|
-
await writeClaudeSettings(settingsPath, settings);
|
|
2179
|
-
if (options.json) {
|
|
2180
|
-
console.log(JSON.stringify({ installed: false, location: displayPath, action: "removed" }));
|
|
2181
|
-
} else {
|
|
2182
|
-
out.success("Learning agent hooks removed");
|
|
2183
|
-
console.log(` Location: ${displayPath}`);
|
|
2184
|
-
}
|
|
2185
|
-
} else {
|
|
2186
|
-
if (options.json) {
|
|
2187
|
-
console.log(JSON.stringify({ installed: false, location: displayPath, action: "unchanged" }));
|
|
2188
|
-
} else {
|
|
2189
|
-
out.info("No learning agent hooks to remove");
|
|
2190
|
-
if (options.global) {
|
|
2191
|
-
console.log(" Hint: Try without --global to check project settings.");
|
|
2192
|
-
} else {
|
|
2193
|
-
console.log(" Hint: Try with --global flag to check global settings.");
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
return;
|
|
2198
|
-
}
|
|
2199
|
-
if (options.dryRun) {
|
|
2200
|
-
if (options.json) {
|
|
2201
|
-
console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
|
|
2202
|
-
} else {
|
|
2203
|
-
if (alreadyInstalled) {
|
|
2204
|
-
console.log("Learning agent hooks already installed");
|
|
2205
|
-
} else {
|
|
2206
|
-
console.log(`Would install learning-agent hooks to ${displayPath}`);
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
return;
|
|
2210
|
-
}
|
|
2211
|
-
if (alreadyInstalled) {
|
|
2212
|
-
if (options.json) {
|
|
2213
|
-
console.log(JSON.stringify({
|
|
2214
|
-
installed: true,
|
|
2215
|
-
location: displayPath,
|
|
2216
|
-
hooks: ["SessionStart"],
|
|
2217
|
-
action: "unchanged"
|
|
2218
|
-
}));
|
|
2219
|
-
} else {
|
|
2220
|
-
out.info("Learning agent hooks already installed");
|
|
2221
|
-
console.log(` Location: ${displayPath}`);
|
|
2222
|
-
}
|
|
2223
|
-
return;
|
|
2224
|
-
}
|
|
2225
|
-
const fileExists = existsSync(settingsPath);
|
|
2226
|
-
addLearningAgentHook(settings);
|
|
2227
|
-
await writeClaudeSettings(settingsPath, settings);
|
|
2228
|
-
if (options.json) {
|
|
2229
|
-
console.log(JSON.stringify({
|
|
2230
|
-
installed: true,
|
|
2231
|
-
location: displayPath,
|
|
2232
|
-
hooks: ["SessionStart"],
|
|
2233
|
-
action: fileExists ? "updated" : "created"
|
|
2234
|
-
}));
|
|
2235
|
-
} else {
|
|
2236
|
-
out.success(options.global ? "Claude Code hooks installed (global)" : "Claude Code hooks installed (project-level)");
|
|
2237
|
-
console.log(` Location: ${displayPath}`);
|
|
2238
|
-
console.log(" Hook: SessionStart (startup|resume|compact)");
|
|
2239
|
-
console.log("");
|
|
2240
|
-
console.log("Lessons will be loaded automatically at session start.");
|
|
2241
|
-
if (!options.global) {
|
|
2242
|
-
console.log("");
|
|
2243
|
-
console.log("Note: Project hooks override global hooks.");
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
});
|
|
2247
|
-
program2.command("download-model").description("Download the embedding model for semantic search").option("--json", "Output as JSON").action(async (options) => {
|
|
2248
|
-
const alreadyExisted = isModelAvailable();
|
|
2249
|
-
if (alreadyExisted) {
|
|
2250
|
-
const modelPath2 = join(homedir(), ".node-llama-cpp", "models", MODEL_FILENAME);
|
|
2251
|
-
const size2 = statSync(modelPath2).size;
|
|
2252
|
-
if (options.json) {
|
|
2253
|
-
console.log(JSON.stringify({ success: true, path: modelPath2, size: size2, alreadyExisted: true }));
|
|
2254
|
-
} else {
|
|
2255
|
-
console.log("Model already exists.");
|
|
2256
|
-
console.log(`Path: ${modelPath2}`);
|
|
2257
|
-
console.log(`Size: ${formatBytes(size2)}`);
|
|
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}`);
|
|
2258
2720
|
}
|
|
2259
|
-
return;
|
|
2260
|
-
}
|
|
2261
|
-
if (!options.json) {
|
|
2262
|
-
console.log("Downloading embedding model...");
|
|
2263
|
-
}
|
|
2264
|
-
const modelPath = await resolveModel({ cli: !options.json });
|
|
2265
|
-
const size = statSync(modelPath).size;
|
|
2266
|
-
if (options.json) {
|
|
2267
|
-
console.log(JSON.stringify({ success: true, path: modelPath, size, alreadyExisted: false }));
|
|
2268
|
-
} else {
|
|
2269
|
-
console.log(`
|
|
2270
|
-
Model downloaded successfully!`);
|
|
2271
|
-
console.log(`Path: ${modelPath}`);
|
|
2272
|
-
console.log(`Size: ${formatBytes(size)}`);
|
|
2273
2721
|
}
|
|
2274
2722
|
});
|
|
2275
2723
|
}
|
|
2276
2724
|
|
|
2725
|
+
// src/commands/setup/index.ts
|
|
2726
|
+
function registerSetupCommands(program2) {
|
|
2727
|
+
registerInitCommand(program2);
|
|
2728
|
+
registerHooksCommand(program2);
|
|
2729
|
+
registerClaudeCommand(program2);
|
|
2730
|
+
registerDownloadModelCommand(program2);
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2277
2733
|
// src/cli.ts
|
|
2734
|
+
function cleanup() {
|
|
2735
|
+
try {
|
|
2736
|
+
closeDb();
|
|
2737
|
+
} catch {
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
process.on("SIGINT", () => {
|
|
2741
|
+
cleanup();
|
|
2742
|
+
process.exit(0);
|
|
2743
|
+
});
|
|
2744
|
+
process.on("SIGTERM", () => {
|
|
2745
|
+
cleanup();
|
|
2746
|
+
process.exit(0);
|
|
2747
|
+
});
|
|
2278
2748
|
var program = new Command();
|
|
2279
2749
|
program.option("-v, --verbose", "Show detailed output").option("-q, --quiet", "Suppress non-essential output");
|
|
2280
2750
|
program.name("learning-agent").description("Repository-scoped learning system for Claude Code").version(VERSION);
|