learning-agent 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -2
- package/README.md +17 -5
- package/dist/cli.js +1250 -750
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +40 -2
- package/dist/index.js +85 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import chalk from 'chalk';
|
|
3
2
|
import { Command } from 'commander';
|
|
4
|
-
import { existsSync, statSync, chmodSync, mkdirSync } from 'fs';
|
|
5
|
-
import * as fs from 'fs/promises';
|
|
6
|
-
import { mkdir, writeFile, readFile, rename, appendFile } from 'fs/promises';
|
|
7
|
-
import { homedir } from 'os';
|
|
8
|
-
import { join, dirname } from 'path';
|
|
9
3
|
import { createHash } from 'crypto';
|
|
4
|
+
import { statSync, existsSync, chmodSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
10
6
|
import Database from 'better-sqlite3';
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import { readFile, mkdir, appendFile, writeFile, rename } from 'fs/promises';
|
|
11
9
|
import { z } from 'zod';
|
|
12
|
-
import
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { resolveModelFile, getLlama } from 'node-llama-cpp';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
13
|
|
|
14
|
+
// src/cli-utils.ts
|
|
15
|
+
function formatBytes(bytes) {
|
|
16
|
+
if (bytes === 0) return "0 B";
|
|
17
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
18
|
+
const kb = bytes / 1024;
|
|
19
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
20
|
+
const mb = kb / 1024;
|
|
21
|
+
return `${mb.toFixed(1)} MB`;
|
|
22
|
+
}
|
|
23
|
+
function parseLimit(value, name) {
|
|
24
|
+
const parsed = parseInt(value, 10);
|
|
25
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
26
|
+
throw new Error(`Invalid ${name}: must be a positive integer`);
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
function getRepoRoot() {
|
|
31
|
+
return process.env["LEARNING_AGENT_ROOT"] ?? process.cwd();
|
|
32
|
+
}
|
|
14
33
|
var SourceSchema = z.enum([
|
|
15
34
|
"user_correction",
|
|
16
35
|
"self_correction",
|
|
@@ -25,7 +44,23 @@ var PatternSchema = z.object({
|
|
|
25
44
|
bad: z.string(),
|
|
26
45
|
good: z.string()
|
|
27
46
|
});
|
|
47
|
+
var CitationSchema = z.object({
|
|
48
|
+
file: z.string().min(1),
|
|
49
|
+
// Source file path (required, non-empty)
|
|
50
|
+
line: z.number().int().positive().optional(),
|
|
51
|
+
// Line number (optional, must be positive)
|
|
52
|
+
commit: z.string().optional()
|
|
53
|
+
// Git commit hash (optional)
|
|
54
|
+
});
|
|
28
55
|
var SeveritySchema = z.enum(["high", "medium", "low"]);
|
|
56
|
+
var CompactionLevelSchema = z.union([
|
|
57
|
+
z.literal(0),
|
|
58
|
+
// Active
|
|
59
|
+
z.literal(1),
|
|
60
|
+
// Flagged (>90 days)
|
|
61
|
+
z.literal(2)
|
|
62
|
+
// Archived
|
|
63
|
+
]);
|
|
29
64
|
var LessonTypeSchema = z.enum(["quick", "full"]);
|
|
30
65
|
var LessonSchema = z.object({
|
|
31
66
|
// Core identity (required)
|
|
@@ -49,7 +84,20 @@ var LessonSchema = z.object({
|
|
|
49
84
|
pattern: PatternSchema.optional(),
|
|
50
85
|
// Lifecycle fields (optional)
|
|
51
86
|
deleted: z.boolean().optional(),
|
|
52
|
-
retrievalCount: z.number().optional()
|
|
87
|
+
retrievalCount: z.number().optional(),
|
|
88
|
+
// Provenance tracking (optional)
|
|
89
|
+
citation: CitationSchema.optional(),
|
|
90
|
+
// Age-based validity fields (optional)
|
|
91
|
+
compactionLevel: CompactionLevelSchema.optional(),
|
|
92
|
+
// 0=active, 1=flagged, 2=archived
|
|
93
|
+
compactedAt: z.string().optional(),
|
|
94
|
+
// ISO8601 when compaction happened
|
|
95
|
+
lastRetrieved: z.string().optional(),
|
|
96
|
+
// ISO8601 last retrieval time
|
|
97
|
+
// Invalidation fields (optional - for marking lessons as wrong)
|
|
98
|
+
invalidatedAt: z.string().optional(),
|
|
99
|
+
// ISO8601
|
|
100
|
+
invalidationReason: z.string().optional()
|
|
53
101
|
});
|
|
54
102
|
z.object({
|
|
55
103
|
id: z.string(),
|
|
@@ -155,7 +203,15 @@ var SCHEMA_SQL = `
|
|
|
155
203
|
retrieval_count INTEGER NOT NULL DEFAULT 0,
|
|
156
204
|
last_retrieved TEXT,
|
|
157
205
|
embedding BLOB,
|
|
158
|
-
content_hash TEXT
|
|
206
|
+
content_hash TEXT,
|
|
207
|
+
-- v0.2.2 fields
|
|
208
|
+
invalidated_at TEXT,
|
|
209
|
+
invalidation_reason TEXT,
|
|
210
|
+
citation_file TEXT,
|
|
211
|
+
citation_line INTEGER,
|
|
212
|
+
citation_commit TEXT,
|
|
213
|
+
compaction_level INTEGER DEFAULT 0,
|
|
214
|
+
compacted_at TEXT
|
|
159
215
|
);
|
|
160
216
|
|
|
161
217
|
-- FTS5 virtual table for full-text search
|
|
@@ -264,6 +320,28 @@ function rowToLesson(row) {
|
|
|
264
320
|
if (row.retrieval_count > 0) {
|
|
265
321
|
lesson.retrievalCount = row.retrieval_count;
|
|
266
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
|
+
}
|
|
267
345
|
return lesson;
|
|
268
346
|
}
|
|
269
347
|
function collectCachedEmbeddings(database) {
|
|
@@ -277,8 +355,8 @@ function collectCachedEmbeddings(database) {
|
|
|
277
355
|
return cache;
|
|
278
356
|
}
|
|
279
357
|
var INSERT_LESSON_SQL = `
|
|
280
|
-
INSERT INTO lessons (id, type, trigger, insight, evidence, severity, tags, source, context, supersedes, related, created, confirmed, deleted, retrieval_count, last_retrieved, embedding, content_hash)
|
|
281
|
-
VALUES (@id, @type, @trigger, @insight, @evidence, @severity, @tags, @source, @context, @supersedes, @related, @created, @confirmed, @deleted, @retrieval_count, @last_retrieved, @embedding, @content_hash)
|
|
358
|
+
INSERT INTO lessons (id, type, trigger, insight, evidence, severity, tags, source, context, supersedes, related, created, confirmed, deleted, retrieval_count, last_retrieved, embedding, content_hash, invalidated_at, invalidation_reason, citation_file, citation_line, citation_commit, compaction_level, compacted_at)
|
|
359
|
+
VALUES (@id, @type, @trigger, @insight, @evidence, @severity, @tags, @source, @context, @supersedes, @related, @created, @confirmed, @deleted, @retrieval_count, @last_retrieved, @embedding, @content_hash, @invalidated_at, @invalidation_reason, @citation_file, @citation_line, @citation_commit, @compaction_level, @compacted_at)
|
|
282
360
|
`;
|
|
283
361
|
function getJsonlMtime(repoRoot) {
|
|
284
362
|
const jsonlPath = join(repoRoot, LESSONS_PATH);
|
|
@@ -330,10 +408,17 @@ async function rebuildIndex(repoRoot) {
|
|
|
330
408
|
confirmed: lesson.confirmed ? 1 : 0,
|
|
331
409
|
deleted: lesson.deleted ? 1 : 0,
|
|
332
410
|
retrieval_count: lesson.retrievalCount ?? 0,
|
|
333
|
-
last_retrieved: null,
|
|
334
|
-
// Reset on rebuild since we're rebuilding from source
|
|
411
|
+
last_retrieved: lesson.lastRetrieved ?? null,
|
|
335
412
|
embedding: hasValidCache ? cached.embedding : null,
|
|
336
|
-
content_hash: hasValidCache ? cached.contentHash : null
|
|
413
|
+
content_hash: hasValidCache ? cached.contentHash : null,
|
|
414
|
+
// v0.2.2 fields
|
|
415
|
+
invalidated_at: lesson.invalidatedAt ?? null,
|
|
416
|
+
invalidation_reason: lesson.invalidationReason ?? null,
|
|
417
|
+
citation_file: lesson.citation?.file ?? null,
|
|
418
|
+
citation_line: lesson.citation?.line ?? null,
|
|
419
|
+
citation_commit: lesson.citation?.commit ?? null,
|
|
420
|
+
compaction_level: lesson.compactionLevel ?? 0,
|
|
421
|
+
compacted_at: lesson.compactedAt ?? null
|
|
337
422
|
});
|
|
338
423
|
}
|
|
339
424
|
});
|
|
@@ -368,6 +453,7 @@ async function searchKeyword(repoRoot, query, limit) {
|
|
|
368
453
|
FROM lessons l
|
|
369
454
|
JOIN lessons_fts fts ON l.rowid = fts.rowid
|
|
370
455
|
WHERE lessons_fts MATCH ?
|
|
456
|
+
AND l.invalidated_at IS NULL
|
|
371
457
|
LIMIT ?
|
|
372
458
|
`
|
|
373
459
|
).all(query, limit);
|
|
@@ -635,178 +721,18 @@ async function parseInputFile(filePath) {
|
|
|
635
721
|
return data;
|
|
636
722
|
}
|
|
637
723
|
|
|
638
|
-
// src/
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
const mb = kb / 1024;
|
|
645
|
-
return `${mb.toFixed(1)} MB`;
|
|
646
|
-
}
|
|
647
|
-
function parseLimit(value, name) {
|
|
648
|
-
const parsed = parseInt(value, 10);
|
|
649
|
-
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
650
|
-
throw new Error(`Invalid ${name}: must be a positive integer`);
|
|
651
|
-
}
|
|
652
|
-
return parsed;
|
|
653
|
-
}
|
|
654
|
-
function getRepoRoot() {
|
|
655
|
-
return process.env["LEARNING_AGENT_ROOT"] ?? process.cwd();
|
|
656
|
-
}
|
|
657
|
-
var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
|
|
658
|
-
var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
|
|
659
|
-
var DEFAULT_MODEL_DIR = join(homedir(), ".node-llama-cpp", "models");
|
|
660
|
-
function isModelAvailable() {
|
|
661
|
-
return existsSync(join(DEFAULT_MODEL_DIR, MODEL_FILENAME));
|
|
662
|
-
}
|
|
663
|
-
async function resolveModel(options = {}) {
|
|
664
|
-
const { cli = true } = options;
|
|
665
|
-
return resolveModelFile(MODEL_URI, { cli });
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// src/embeddings/nomic.ts
|
|
669
|
-
var embeddingContext = null;
|
|
670
|
-
async function getEmbedding() {
|
|
671
|
-
if (embeddingContext) return embeddingContext;
|
|
672
|
-
const modelPath = await resolveModel({ cli: true });
|
|
673
|
-
const llama = await getLlama();
|
|
674
|
-
const model = await llama.loadModel({ modelPath });
|
|
675
|
-
embeddingContext = await model.createEmbeddingContext();
|
|
676
|
-
return embeddingContext;
|
|
677
|
-
}
|
|
678
|
-
async function embedText(text) {
|
|
679
|
-
const ctx = await getEmbedding();
|
|
680
|
-
const result = await ctx.getEmbeddingFor(text);
|
|
681
|
-
return Array.from(result.vector);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// src/search/vector.ts
|
|
685
|
-
function cosineSimilarity(a, b) {
|
|
686
|
-
if (a.length !== b.length) {
|
|
687
|
-
throw new Error("Vectors must have same length");
|
|
688
|
-
}
|
|
689
|
-
let dotProduct = 0;
|
|
690
|
-
let normA = 0;
|
|
691
|
-
let normB = 0;
|
|
692
|
-
for (let i = 0; i < a.length; i++) {
|
|
693
|
-
dotProduct += a[i] * b[i];
|
|
694
|
-
normA += a[i] * a[i];
|
|
695
|
-
normB += b[i] * b[i];
|
|
696
|
-
}
|
|
697
|
-
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
698
|
-
if (magnitude === 0) return 0;
|
|
699
|
-
return dotProduct / magnitude;
|
|
700
|
-
}
|
|
701
|
-
var DEFAULT_LIMIT = 10;
|
|
702
|
-
async function searchVector(repoRoot, query, options) {
|
|
703
|
-
const limit = options?.limit ?? DEFAULT_LIMIT;
|
|
704
|
-
const { lessons } = await readLessons(repoRoot);
|
|
705
|
-
if (lessons.length === 0) return [];
|
|
706
|
-
const queryVector = await embedText(query);
|
|
707
|
-
const scored = [];
|
|
708
|
-
for (const lesson of lessons) {
|
|
709
|
-
const lessonText = `${lesson.trigger} ${lesson.insight}`;
|
|
710
|
-
const hash = contentHash(lesson.trigger, lesson.insight);
|
|
711
|
-
let lessonVector = getCachedEmbedding(repoRoot, lesson.id, hash);
|
|
712
|
-
if (!lessonVector) {
|
|
713
|
-
lessonVector = await embedText(lessonText);
|
|
714
|
-
setCachedEmbedding(repoRoot, lesson.id, lessonVector, hash);
|
|
715
|
-
}
|
|
716
|
-
const score = cosineSimilarity(queryVector, lessonVector);
|
|
717
|
-
scored.push({ lesson, score });
|
|
718
|
-
}
|
|
719
|
-
scored.sort((a, b) => b.score - a.score);
|
|
720
|
-
return scored.slice(0, limit);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// src/search/ranking.ts
|
|
724
|
-
var RECENCY_THRESHOLD_DAYS = 30;
|
|
725
|
-
var HIGH_SEVERITY_BOOST = 1.5;
|
|
726
|
-
var MEDIUM_SEVERITY_BOOST = 1;
|
|
727
|
-
var LOW_SEVERITY_BOOST = 0.8;
|
|
728
|
-
var RECENCY_BOOST = 1.2;
|
|
729
|
-
var CONFIRMATION_BOOST = 1.3;
|
|
730
|
-
function severityBoost(lesson) {
|
|
731
|
-
switch (lesson.severity) {
|
|
732
|
-
case "high":
|
|
733
|
-
return HIGH_SEVERITY_BOOST;
|
|
734
|
-
case "medium":
|
|
735
|
-
return MEDIUM_SEVERITY_BOOST;
|
|
736
|
-
case "low":
|
|
737
|
-
return LOW_SEVERITY_BOOST;
|
|
738
|
-
default:
|
|
739
|
-
return MEDIUM_SEVERITY_BOOST;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
function recencyBoost(lesson) {
|
|
743
|
-
const created = new Date(lesson.created);
|
|
744
|
-
const now = /* @__PURE__ */ new Date();
|
|
745
|
-
const ageMs = now.getTime() - created.getTime();
|
|
746
|
-
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
747
|
-
return ageDays <= RECENCY_THRESHOLD_DAYS ? RECENCY_BOOST : 1;
|
|
748
|
-
}
|
|
749
|
-
function confirmationBoost(lesson) {
|
|
750
|
-
return lesson.confirmed ? CONFIRMATION_BOOST : 1;
|
|
751
|
-
}
|
|
752
|
-
function calculateScore(lesson, vectorSimilarity) {
|
|
753
|
-
return vectorSimilarity * severityBoost(lesson) * recencyBoost(lesson) * confirmationBoost(lesson);
|
|
754
|
-
}
|
|
755
|
-
function rankLessons(lessons) {
|
|
756
|
-
return lessons.map((scored) => ({
|
|
757
|
-
...scored,
|
|
758
|
-
finalScore: calculateScore(scored.lesson, scored.score)
|
|
759
|
-
})).sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// src/retrieval/session.ts
|
|
763
|
-
var DEFAULT_LIMIT2 = 5;
|
|
764
|
-
function isFullLesson(lesson) {
|
|
765
|
-
return lesson.type === "full" && lesson.severity !== void 0;
|
|
766
|
-
}
|
|
767
|
-
async function loadSessionLessons(repoRoot, limit = DEFAULT_LIMIT2) {
|
|
768
|
-
const { lessons: allLessons } = await readLessons(repoRoot);
|
|
769
|
-
const highSeverityLessons = allLessons.filter(
|
|
770
|
-
(lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed
|
|
771
|
-
);
|
|
772
|
-
highSeverityLessons.sort((a, b) => {
|
|
773
|
-
const dateA = new Date(a.created).getTime();
|
|
774
|
-
const dateB = new Date(b.created).getTime();
|
|
775
|
-
return dateB - dateA;
|
|
776
|
-
});
|
|
777
|
-
return highSeverityLessons.slice(0, limit);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// src/retrieval/plan.ts
|
|
781
|
-
var DEFAULT_LIMIT3 = 5;
|
|
782
|
-
async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
|
|
783
|
-
const scored = await searchVector(repoRoot, planText, { limit: limit * 2 });
|
|
784
|
-
const ranked = rankLessons(scored);
|
|
785
|
-
const topLessons = ranked.slice(0, limit);
|
|
786
|
-
const message = formatLessonsCheck(topLessons);
|
|
787
|
-
return { lessons: topLessons, message };
|
|
788
|
-
}
|
|
789
|
-
function formatLessonsCheck(lessons) {
|
|
790
|
-
const header = "\u{1F4DA} Lessons Check\n" + "\u2500".repeat(40);
|
|
791
|
-
if (lessons.length === 0) {
|
|
792
|
-
return `${header}
|
|
793
|
-
No relevant lessons found for this plan.`;
|
|
794
|
-
}
|
|
795
|
-
const lessonLines = lessons.map((l, i) => {
|
|
796
|
-
const bullet = `${i + 1}.`;
|
|
797
|
-
const insight = l.lesson.insight;
|
|
798
|
-
return `${bullet} ${insight}`;
|
|
799
|
-
});
|
|
800
|
-
return `${header}
|
|
801
|
-
${lessonLines.join("\n")}`;
|
|
724
|
+
// src/utils.ts
|
|
725
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
726
|
+
function getLessonAgeDays(lesson) {
|
|
727
|
+
const created = new Date(lesson.created).getTime();
|
|
728
|
+
const now = Date.now();
|
|
729
|
+
return Math.floor((now - created) / MS_PER_DAY);
|
|
802
730
|
}
|
|
803
731
|
|
|
804
|
-
// src/
|
|
805
|
-
var VERSION = "0.1.0";
|
|
732
|
+
// src/storage/compact.ts
|
|
806
733
|
var ARCHIVE_DIR = ".claude/lessons/archive";
|
|
807
734
|
var TOMBSTONE_THRESHOLD = 100;
|
|
808
735
|
var ARCHIVE_AGE_DAYS = 90;
|
|
809
|
-
var MS_PER_DAY = 1e3 * 60 * 60 * 24;
|
|
810
736
|
var MONTH_INDEX_OFFSET = 1;
|
|
811
737
|
var MONTH_PAD_LENGTH = 2;
|
|
812
738
|
function getArchivePath(repoRoot, date) {
|
|
@@ -860,19 +786,16 @@ async function rewriteWithoutTombstones(repoRoot) {
|
|
|
860
786
|
await rename(tempPath, filePath);
|
|
861
787
|
return tombstoneCount;
|
|
862
788
|
}
|
|
863
|
-
function shouldArchive(lesson
|
|
864
|
-
const
|
|
865
|
-
const ageMs = now.getTime() - created.getTime();
|
|
866
|
-
const ageDays = ageMs / MS_PER_DAY;
|
|
789
|
+
function shouldArchive(lesson) {
|
|
790
|
+
const ageDays = getLessonAgeDays(lesson);
|
|
867
791
|
return ageDays > ARCHIVE_AGE_DAYS && (lesson.retrievalCount === void 0 || lesson.retrievalCount === 0);
|
|
868
792
|
}
|
|
869
793
|
async function archiveOldLessons(repoRoot) {
|
|
870
794
|
const { lessons } = await readLessons(repoRoot);
|
|
871
|
-
const now = /* @__PURE__ */ new Date();
|
|
872
795
|
const toArchive = [];
|
|
873
796
|
const toKeep = [];
|
|
874
797
|
for (const lesson of lessons) {
|
|
875
|
-
if (shouldArchive(lesson
|
|
798
|
+
if (shouldArchive(lesson)) {
|
|
876
799
|
toArchive.push(lesson);
|
|
877
800
|
} else {
|
|
878
801
|
toKeep.push(lesson);
|
|
@@ -916,30 +839,6 @@ async function compact(repoRoot) {
|
|
|
916
839
|
lessonsRemaining: lessons.length
|
|
917
840
|
};
|
|
918
841
|
}
|
|
919
|
-
|
|
920
|
-
// src/cli.ts
|
|
921
|
-
var PRE_COMMIT_MESSAGE = `Before committing, have you captured any valuable lessons from this session?
|
|
922
|
-
Consider: corrections, mistakes, or insights worth remembering.
|
|
923
|
-
|
|
924
|
-
To capture a lesson:
|
|
925
|
-
npx learning-agent capture --trigger "what happened" --insight "what to do" --yes`;
|
|
926
|
-
var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
|
|
927
|
-
# Learning Agent pre-commit hook
|
|
928
|
-
# Reminds Claude to consider capturing lessons before commits
|
|
929
|
-
|
|
930
|
-
npx learning-agent hooks run pre-commit
|
|
931
|
-
`;
|
|
932
|
-
var CLAUDE_HOOK_MARKER = "learning-agent load-session";
|
|
933
|
-
var CLAUDE_HOOK_CONFIG = {
|
|
934
|
-
matcher: "startup|resume|compact",
|
|
935
|
-
hooks: [
|
|
936
|
-
{
|
|
937
|
-
type: "command",
|
|
938
|
-
command: "npx learning-agent load-session 2>/dev/null || true"
|
|
939
|
-
}
|
|
940
|
-
]
|
|
941
|
-
};
|
|
942
|
-
var HOOK_MARKER = "# Learning Agent pre-commit hook";
|
|
943
842
|
var out = {
|
|
944
843
|
success: (msg) => console.log(chalk.green("[ok]"), msg),
|
|
945
844
|
error: (msg) => console.error(chalk.red("[error]"), msg),
|
|
@@ -956,10 +855,14 @@ function getGlobalOpts(cmd) {
|
|
|
956
855
|
var DEFAULT_SEARCH_LIMIT = "10";
|
|
957
856
|
var DEFAULT_LIST_LIMIT = "20";
|
|
958
857
|
var DEFAULT_CHECK_PLAN_LIMIT = "5";
|
|
858
|
+
var LESSON_COUNT_WARNING_THRESHOLD = 20;
|
|
859
|
+
var AGE_FLAG_THRESHOLD_DAYS = 90;
|
|
959
860
|
var ISO_DATE_PREFIX_LENGTH = 10;
|
|
960
861
|
var AVG_DECIMAL_PLACES = 1;
|
|
961
862
|
var RELEVANCE_DECIMAL_PLACES = 2;
|
|
962
863
|
var JSON_INDENT_SPACES = 2;
|
|
864
|
+
|
|
865
|
+
// src/commands/capture.ts
|
|
963
866
|
function createLessonFromFlags(trigger, insight, confirmed) {
|
|
964
867
|
return {
|
|
965
868
|
id: generateId(insight),
|
|
@@ -1008,62 +911,955 @@ function createLessonFromInputFile(result, confirmed) {
|
|
|
1008
911
|
related: []
|
|
1009
912
|
};
|
|
1010
913
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
914
|
+
function registerCaptureCommands(program2) {
|
|
915
|
+
program2.command("learn <insight>").description("Capture a new lesson").option("-t, --trigger <text>", "What triggered this lesson").option("--tags <tags>", "Comma-separated tags", "").option("-s, --severity <level>", "Lesson severity: high, medium, low").option("-y, --yes", "Skip confirmation").option("--citation <file:line>", "Source file (optionally with :line number)").option("--citation-commit <hash>", "Git commit hash for citation").action(async function(insight, options) {
|
|
916
|
+
const repoRoot = getRepoRoot();
|
|
917
|
+
const { quiet } = getGlobalOpts(this);
|
|
918
|
+
let severity;
|
|
919
|
+
if (options.severity !== void 0) {
|
|
920
|
+
const result = SeveritySchema.safeParse(options.severity);
|
|
921
|
+
if (!result.success) {
|
|
922
|
+
out.error(`Invalid severity value: "${options.severity}". Valid values are: high, medium, low`);
|
|
923
|
+
process.exit(1);
|
|
924
|
+
}
|
|
925
|
+
severity = result.data;
|
|
1017
926
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
927
|
+
const lessonType = severity !== void 0 ? "full" : "quick";
|
|
928
|
+
let citation;
|
|
929
|
+
if (options.citation) {
|
|
930
|
+
const parts = options.citation.split(":");
|
|
931
|
+
const file = parts[0] ?? "";
|
|
932
|
+
const lineStr = parts[1];
|
|
933
|
+
const line = lineStr ? parseInt(lineStr, 10) : void 0;
|
|
934
|
+
citation = {
|
|
935
|
+
file,
|
|
936
|
+
...line && !isNaN(line) && { line },
|
|
937
|
+
...options.citationCommit && { commit: options.citationCommit }
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
const lesson = {
|
|
941
|
+
id: generateId(insight),
|
|
942
|
+
type: lessonType,
|
|
943
|
+
trigger: options.trigger ?? "Manual capture",
|
|
944
|
+
insight,
|
|
945
|
+
tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : [],
|
|
946
|
+
source: "manual",
|
|
947
|
+
context: {
|
|
948
|
+
tool: "cli",
|
|
949
|
+
intent: "manual learning"
|
|
950
|
+
},
|
|
951
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
952
|
+
confirmed: true,
|
|
953
|
+
// learn command is explicit confirmation
|
|
954
|
+
supersedes: [],
|
|
955
|
+
related: [],
|
|
956
|
+
...severity !== void 0 && { severity },
|
|
957
|
+
...citation && { citation }
|
|
958
|
+
};
|
|
959
|
+
await appendLesson(repoRoot, lesson);
|
|
960
|
+
const chalk3 = await import('chalk');
|
|
961
|
+
out.success(`Learned: ${insight}`);
|
|
962
|
+
if (!quiet) {
|
|
963
|
+
console.log(`ID: ${chalk3.default.dim(lesson.id)}`);
|
|
964
|
+
if (citation) {
|
|
965
|
+
console.log(`Citation: ${chalk3.default.dim(citation.file)}${citation.line ? `:${citation.line}` : ""}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
program2.command("detect").description("Detect learning triggers from input").requiredOption("--input <file>", "Path to JSON input file").option("--save", "Save proposed lesson (requires --yes)").option("-y, --yes", "Confirm save (required with --save)").option("--json", "Output result as JSON").action(
|
|
970
|
+
async (options) => {
|
|
971
|
+
const repoRoot = getRepoRoot();
|
|
972
|
+
if (options.save && !options.yes) {
|
|
973
|
+
if (options.json) {
|
|
974
|
+
console.log(JSON.stringify({ error: "--save requires --yes flag for confirmation" }));
|
|
975
|
+
} else {
|
|
976
|
+
out.error("--save requires --yes flag for confirmation");
|
|
977
|
+
console.log("Use: detect --input <file> --save --yes");
|
|
978
|
+
}
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
const input = await parseInputFile(options.input);
|
|
982
|
+
const result = await detectAndPropose(repoRoot, input);
|
|
983
|
+
if (!result) {
|
|
984
|
+
if (options.json) {
|
|
985
|
+
console.log(JSON.stringify({ detected: false }));
|
|
986
|
+
} else {
|
|
987
|
+
console.log("No learning trigger detected.");
|
|
988
|
+
}
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (options.json) {
|
|
992
|
+
console.log(JSON.stringify({ detected: true, ...result }));
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
console.log("Learning trigger detected!");
|
|
996
|
+
console.log(` Trigger: ${result.trigger}`);
|
|
997
|
+
console.log(` Source: ${result.source}`);
|
|
998
|
+
console.log(` Proposed: ${result.proposedInsight}`);
|
|
999
|
+
if (options.save && options.yes) {
|
|
1000
|
+
const lesson = {
|
|
1001
|
+
id: generateId(result.proposedInsight),
|
|
1002
|
+
type: "quick",
|
|
1003
|
+
trigger: result.trigger,
|
|
1004
|
+
insight: result.proposedInsight,
|
|
1005
|
+
tags: [],
|
|
1006
|
+
source: result.source,
|
|
1007
|
+
context: { tool: "detect", intent: "auto-capture" },
|
|
1008
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1009
|
+
confirmed: true,
|
|
1010
|
+
// --yes confirms the lesson
|
|
1011
|
+
supersedes: [],
|
|
1012
|
+
related: []
|
|
1013
|
+
};
|
|
1014
|
+
await appendLesson(repoRoot, lesson);
|
|
1015
|
+
console.log(`
|
|
1016
|
+
Saved as lesson: ${lesson.id}`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
);
|
|
1020
|
+
program2.command("capture").description("Capture a lesson from trigger/insight or input file").option("-t, --trigger <text>", "What triggered this lesson").option("-i, --insight <text>", "The insight or lesson learned").option("--input <file>", "Path to JSON input file (alternative to trigger/insight)").option("--json", "Output result as JSON").option("-y, --yes", "Skip confirmation and save immediately").action(async function(options) {
|
|
1021
|
+
const repoRoot = getRepoRoot();
|
|
1022
|
+
const { verbose } = getGlobalOpts(this);
|
|
1023
|
+
let lesson;
|
|
1024
|
+
if (options.input) {
|
|
1025
|
+
const input = await parseInputFile(options.input);
|
|
1026
|
+
const result = await detectAndPropose(repoRoot, input);
|
|
1027
|
+
if (!result) {
|
|
1028
|
+
options.json ? console.log(JSON.stringify({ detected: false, saved: false })) : console.log("No learning trigger detected.");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
lesson = createLessonFromInputFile(result, options.yes ?? false);
|
|
1032
|
+
} else if (options.trigger && options.insight) {
|
|
1033
|
+
lesson = createLessonFromFlags(options.trigger, options.insight, options.yes ?? false);
|
|
1034
|
+
} else {
|
|
1035
|
+
const msg = "Provide either --trigger and --insight, or --input file.";
|
|
1036
|
+
options.json ? console.log(JSON.stringify({ error: msg, saved: false })) : out.error(msg);
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
if (!options.yes && !process.stdin.isTTY) {
|
|
1040
|
+
if (options.json) {
|
|
1041
|
+
console.log(JSON.stringify({ error: "--yes required in non-interactive mode", saved: false }));
|
|
1042
|
+
} else {
|
|
1043
|
+
out.error("--yes required in non-interactive mode");
|
|
1044
|
+
console.log('Use: capture --trigger "..." --insight "..." --yes');
|
|
1045
|
+
}
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
if (options.json) {
|
|
1049
|
+
if (options.yes) await appendLesson(repoRoot, lesson);
|
|
1050
|
+
outputCaptureJson(lesson, options.yes ?? false);
|
|
1051
|
+
} else if (options.yes) {
|
|
1052
|
+
await appendLesson(repoRoot, lesson);
|
|
1053
|
+
out.success(`Lesson saved: ${lesson.id}`);
|
|
1054
|
+
if (verbose) console.log(` Type: ${lesson.type} | Trigger: ${lesson.trigger}`);
|
|
1055
|
+
} else {
|
|
1056
|
+
outputCapturePreview(lesson);
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
function registerManagementCommands(program2) {
|
|
1061
|
+
program2.command("wrong <id>").description("Mark a lesson as invalid/wrong").option("-r, --reason <text>", "Reason for invalidation").action(async function(id, options) {
|
|
1062
|
+
const repoRoot = getRepoRoot();
|
|
1063
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1064
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1065
|
+
if (!lesson) {
|
|
1066
|
+
out.error(`Lesson not found: ${id}`);
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
if (lesson.invalidatedAt) {
|
|
1070
|
+
out.warn(`Lesson ${id} is already marked as invalid.`);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const updatedLesson = {
|
|
1074
|
+
...lesson,
|
|
1075
|
+
invalidatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1076
|
+
...options.reason !== void 0 && { invalidationReason: options.reason }
|
|
1077
|
+
};
|
|
1078
|
+
await appendLesson(repoRoot, updatedLesson);
|
|
1079
|
+
out.success(`Lesson ${id} marked as invalid.`);
|
|
1080
|
+
if (options.reason) {
|
|
1081
|
+
console.log(` Reason: ${options.reason}`);
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
program2.command("validate <id>").description("Re-enable a previously invalidated lesson").action(async function(id) {
|
|
1085
|
+
const repoRoot = getRepoRoot();
|
|
1086
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1087
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1088
|
+
if (!lesson) {
|
|
1089
|
+
out.error(`Lesson not found: ${id}`);
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
}
|
|
1092
|
+
if (!lesson.invalidatedAt) {
|
|
1093
|
+
out.info(`Lesson ${id} is not invalidated.`);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const updatedLesson = {
|
|
1097
|
+
id: lesson.id,
|
|
1098
|
+
type: lesson.type,
|
|
1099
|
+
trigger: lesson.trigger,
|
|
1100
|
+
insight: lesson.insight,
|
|
1101
|
+
tags: lesson.tags,
|
|
1102
|
+
source: lesson.source,
|
|
1103
|
+
context: lesson.context,
|
|
1104
|
+
created: lesson.created,
|
|
1105
|
+
confirmed: lesson.confirmed,
|
|
1106
|
+
supersedes: lesson.supersedes,
|
|
1107
|
+
related: lesson.related,
|
|
1108
|
+
// Include optional fields if present (excluding invalidation)
|
|
1109
|
+
...lesson.evidence !== void 0 && { evidence: lesson.evidence },
|
|
1110
|
+
...lesson.severity !== void 0 && { severity: lesson.severity },
|
|
1111
|
+
...lesson.pattern !== void 0 && { pattern: lesson.pattern },
|
|
1112
|
+
...lesson.deleted !== void 0 && { deleted: lesson.deleted },
|
|
1113
|
+
...lesson.retrievalCount !== void 0 && { retrievalCount: lesson.retrievalCount },
|
|
1114
|
+
...lesson.citation !== void 0 && { citation: lesson.citation },
|
|
1115
|
+
...lesson.compactionLevel !== void 0 && { compactionLevel: lesson.compactionLevel },
|
|
1116
|
+
...lesson.compactedAt !== void 0 && { compactedAt: lesson.compactedAt },
|
|
1117
|
+
...lesson.lastRetrieved !== void 0 && { lastRetrieved: lesson.lastRetrieved }
|
|
1118
|
+
};
|
|
1119
|
+
await appendLesson(repoRoot, updatedLesson);
|
|
1120
|
+
out.success(`Lesson ${id} re-enabled (validated).`);
|
|
1121
|
+
});
|
|
1122
|
+
program2.command("compact").description("Compact lessons: archive old lessons and remove tombstones").option("-f, --force", "Run compaction even if below threshold").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
|
|
1123
|
+
const repoRoot = getRepoRoot();
|
|
1124
|
+
const tombstones = await countTombstones(repoRoot);
|
|
1125
|
+
const needs = await needsCompaction(repoRoot);
|
|
1126
|
+
if (options.dryRun) {
|
|
1127
|
+
console.log("Dry run - no changes will be made.\n");
|
|
1128
|
+
console.log(`Tombstones found: ${tombstones}`);
|
|
1129
|
+
console.log(`Compaction needed: ${needs ? "yes" : "no"}`);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (!needs && !options.force) {
|
|
1133
|
+
console.log(`Compaction not needed (${tombstones} tombstones, threshold is ${TOMBSTONE_THRESHOLD}).`);
|
|
1134
|
+
console.log("Use --force to compact anyway.");
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
console.log("Running compaction...");
|
|
1138
|
+
const result = await compact(repoRoot);
|
|
1139
|
+
console.log("\nCompaction complete:");
|
|
1140
|
+
console.log(` Archived: ${result.archived} lesson(s)`);
|
|
1141
|
+
console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
|
|
1142
|
+
console.log(` Lessons remaining: ${result.lessonsRemaining}`);
|
|
1143
|
+
await rebuildIndex(repoRoot);
|
|
1144
|
+
console.log(" Index rebuilt.");
|
|
1145
|
+
});
|
|
1146
|
+
program2.command("rebuild").description("Rebuild SQLite index from JSONL").option("-f, --force", "Force rebuild even if unchanged").action(async (options) => {
|
|
1147
|
+
const repoRoot = getRepoRoot();
|
|
1148
|
+
if (options.force) {
|
|
1149
|
+
console.log("Forcing index rebuild...");
|
|
1150
|
+
await rebuildIndex(repoRoot);
|
|
1151
|
+
console.log("Index rebuilt.");
|
|
1152
|
+
} else {
|
|
1153
|
+
const rebuilt = await syncIfNeeded(repoRoot);
|
|
1154
|
+
if (rebuilt) {
|
|
1155
|
+
console.log("Index rebuilt (JSONL changed).");
|
|
1156
|
+
} else {
|
|
1157
|
+
console.log("Index is up to date.");
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
program2.command("stats").description("Show database health and statistics").action(async () => {
|
|
1162
|
+
const repoRoot = getRepoRoot();
|
|
1163
|
+
await syncIfNeeded(repoRoot);
|
|
1164
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1165
|
+
const deletedCount = await countTombstones(repoRoot);
|
|
1166
|
+
const totalLessons = lessons.length;
|
|
1167
|
+
const retrievalStats = getRetrievalStats(repoRoot);
|
|
1168
|
+
const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
|
|
1169
|
+
const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
|
|
1170
|
+
const jsonlPath = join(repoRoot, LESSONS_PATH);
|
|
1171
|
+
const dbPath = join(repoRoot, DB_PATH);
|
|
1172
|
+
let dataSize = 0;
|
|
1173
|
+
let indexSize = 0;
|
|
1174
|
+
try {
|
|
1175
|
+
dataSize = statSync(jsonlPath).size;
|
|
1176
|
+
} catch {
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
indexSize = statSync(dbPath).size;
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
const totalSize = dataSize + indexSize;
|
|
1183
|
+
let recentCount = 0;
|
|
1184
|
+
let mediumCount = 0;
|
|
1185
|
+
let oldCount = 0;
|
|
1186
|
+
for (const lesson of lessons) {
|
|
1187
|
+
const ageDays = getLessonAgeDays(lesson);
|
|
1188
|
+
if (ageDays < 30) {
|
|
1189
|
+
recentCount++;
|
|
1190
|
+
} else if (ageDays <= AGE_FLAG_THRESHOLD_DAYS) {
|
|
1191
|
+
mediumCount++;
|
|
1192
|
+
} else {
|
|
1193
|
+
oldCount++;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const deletedInfo = deletedCount > 0 ? ` (${deletedCount} deleted)` : "";
|
|
1197
|
+
console.log(`Lessons: ${totalLessons} total${deletedInfo}`);
|
|
1198
|
+
if (totalLessons > LESSON_COUNT_WARNING_THRESHOLD) {
|
|
1199
|
+
out.warn(`High lesson count may degrade retrieval quality. Consider running \`lna compact\`.`);
|
|
1200
|
+
}
|
|
1201
|
+
if (totalLessons > 0) {
|
|
1202
|
+
console.log(`Age: ${recentCount} <30d, ${mediumCount} 30-90d, ${oldCount} >90d`);
|
|
1203
|
+
}
|
|
1204
|
+
console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
|
|
1205
|
+
console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
|
|
1206
|
+
});
|
|
1207
|
+
program2.command("export").description("Export lessons as JSON to stdout").option("--since <date>", "Only include lessons created after this date (ISO8601)").option("--tags <tags>", "Filter by tags (comma-separated, OR logic)").action(async (options) => {
|
|
1208
|
+
const repoRoot = getRepoRoot();
|
|
1209
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1210
|
+
let filtered = lessons;
|
|
1211
|
+
if (options.since) {
|
|
1212
|
+
const sinceDate = new Date(options.since);
|
|
1213
|
+
if (Number.isNaN(sinceDate.getTime())) {
|
|
1214
|
+
console.error(`Invalid date format: ${options.since}. Use ISO8601 format (e.g., 2024-01-15).`);
|
|
1215
|
+
process.exit(1);
|
|
1216
|
+
}
|
|
1217
|
+
filtered = filtered.filter((lesson) => new Date(lesson.created) >= sinceDate);
|
|
1218
|
+
}
|
|
1219
|
+
if (options.tags) {
|
|
1220
|
+
const filterTags = options.tags.split(",").map((t) => t.trim());
|
|
1221
|
+
filtered = filtered.filter((lesson) => lesson.tags.some((tag) => filterTags.includes(tag)));
|
|
1222
|
+
}
|
|
1223
|
+
console.log(JSON.stringify(filtered, null, JSON_INDENT_SPACES));
|
|
1224
|
+
});
|
|
1225
|
+
program2.command("import <file>").description("Import lessons from a JSONL file").action(async (file) => {
|
|
1226
|
+
const repoRoot = getRepoRoot();
|
|
1227
|
+
let content;
|
|
1228
|
+
try {
|
|
1229
|
+
content = await readFile(file, "utf-8");
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
const code = err.code;
|
|
1232
|
+
if (code === "ENOENT") {
|
|
1233
|
+
console.error(`Error: File not found: ${file}`);
|
|
1234
|
+
} else {
|
|
1235
|
+
console.error(`Error reading file: ${err.message}`);
|
|
1236
|
+
}
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
const { lessons: existingLessons } = await readLessons(repoRoot);
|
|
1240
|
+
const existingIds = new Set(existingLessons.map((l) => l.id));
|
|
1241
|
+
const lines = content.split("\n");
|
|
1242
|
+
let imported = 0;
|
|
1243
|
+
let skipped = 0;
|
|
1244
|
+
let invalid = 0;
|
|
1245
|
+
for (const line of lines) {
|
|
1246
|
+
const trimmed = line.trim();
|
|
1247
|
+
if (!trimmed) continue;
|
|
1248
|
+
let parsed;
|
|
1249
|
+
try {
|
|
1250
|
+
parsed = JSON.parse(trimmed);
|
|
1251
|
+
} catch {
|
|
1252
|
+
invalid++;
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
const result = LessonSchema.safeParse(parsed);
|
|
1256
|
+
if (!result.success) {
|
|
1257
|
+
invalid++;
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
const lesson = result.data;
|
|
1261
|
+
if (existingIds.has(lesson.id)) {
|
|
1262
|
+
skipped++;
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
await appendLesson(repoRoot, lesson);
|
|
1266
|
+
existingIds.add(lesson.id);
|
|
1267
|
+
imported++;
|
|
1268
|
+
}
|
|
1269
|
+
const lessonWord = imported === 1 ? "lesson" : "lessons";
|
|
1270
|
+
const parts = [];
|
|
1271
|
+
if (skipped > 0) parts.push(`${skipped} skipped`);
|
|
1272
|
+
if (invalid > 0) parts.push(`${invalid} invalid`);
|
|
1273
|
+
if (parts.length > 0) {
|
|
1274
|
+
console.log(`Imported ${imported} ${lessonWord} (${parts.join(", ")})`);
|
|
1275
|
+
} else {
|
|
1276
|
+
console.log(`Imported ${imported} ${lessonWord}`);
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
const SHOW_JSON_INDENT = 2;
|
|
1280
|
+
program2.command("show <id>").description("Show details of a specific lesson").option("--json", "Output as JSON").action(async (id, options) => {
|
|
1281
|
+
const repoRoot = getRepoRoot();
|
|
1282
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1283
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1284
|
+
if (!lesson) {
|
|
1285
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
1286
|
+
let wasDeleted = false;
|
|
1287
|
+
try {
|
|
1288
|
+
const content = await readFile(filePath, "utf-8");
|
|
1289
|
+
const lines = content.split("\n");
|
|
1290
|
+
for (const line of lines) {
|
|
1291
|
+
const trimmed = line.trim();
|
|
1292
|
+
if (!trimmed) continue;
|
|
1293
|
+
try {
|
|
1294
|
+
const record = JSON.parse(trimmed);
|
|
1295
|
+
if (record.id === id && record.deleted === true) {
|
|
1296
|
+
wasDeleted = true;
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
} catch {
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
} catch {
|
|
1303
|
+
}
|
|
1304
|
+
if (options.json) {
|
|
1305
|
+
console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found` }));
|
|
1306
|
+
} else {
|
|
1307
|
+
out.error(wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found`);
|
|
1308
|
+
}
|
|
1309
|
+
process.exit(1);
|
|
1310
|
+
}
|
|
1311
|
+
if (options.json) {
|
|
1312
|
+
console.log(JSON.stringify(lesson, null, SHOW_JSON_INDENT));
|
|
1313
|
+
} else {
|
|
1314
|
+
console.log(formatLessonHuman(lesson));
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
program2.command("update <id>").description("Update a lesson").option("--insight <text>", "Update insight").option("--trigger <text>", "Update trigger").option("--evidence <text>", "Update evidence").option("--severity <level>", "Update severity (low/medium/high)").option("--tags <tags>", "Update tags (comma-separated)").option("--confirmed <bool>", "Update confirmed status (true/false)").option("--json", "Output as JSON").action(async (id, options) => {
|
|
1318
|
+
const repoRoot = getRepoRoot();
|
|
1319
|
+
const hasUpdates = options.insight !== void 0 || options.trigger !== void 0 || options.evidence !== void 0 || options.severity !== void 0 || options.tags !== void 0 || options.confirmed !== void 0;
|
|
1320
|
+
if (!hasUpdates) {
|
|
1321
|
+
if (options.json) {
|
|
1322
|
+
console.log(JSON.stringify({ error: "No fields to update (specify at least one: --insight, --tags, --severity, ...)" }));
|
|
1323
|
+
} else {
|
|
1324
|
+
out.error("No fields to update (specify at least one: --insight, --tags, --severity, ...)");
|
|
1325
|
+
}
|
|
1326
|
+
process.exit(1);
|
|
1327
|
+
}
|
|
1328
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1329
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
1330
|
+
if (!lesson) {
|
|
1331
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
1332
|
+
let wasDeleted = false;
|
|
1333
|
+
try {
|
|
1334
|
+
const content = await readFile(filePath, "utf-8");
|
|
1335
|
+
const lines = content.split("\n");
|
|
1336
|
+
for (const line of lines) {
|
|
1337
|
+
const trimmed = line.trim();
|
|
1338
|
+
if (!trimmed) continue;
|
|
1339
|
+
try {
|
|
1340
|
+
const record = JSON.parse(trimmed);
|
|
1341
|
+
if (record.id === id && record.deleted === true) {
|
|
1342
|
+
wasDeleted = true;
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
} catch {
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
} catch {
|
|
1349
|
+
}
|
|
1350
|
+
if (options.json) {
|
|
1351
|
+
console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found` }));
|
|
1352
|
+
} else {
|
|
1353
|
+
out.error(wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found`);
|
|
1354
|
+
}
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1357
|
+
if (options.severity !== void 0) {
|
|
1358
|
+
const result = SeveritySchema.safeParse(options.severity);
|
|
1359
|
+
if (!result.success) {
|
|
1360
|
+
if (options.json) {
|
|
1361
|
+
console.log(JSON.stringify({ error: `Invalid severity '${options.severity}' (must be: high, medium, low)` }));
|
|
1362
|
+
} else {
|
|
1363
|
+
out.error(`Invalid severity '${options.severity}' (must be: high, medium, low)`);
|
|
1364
|
+
}
|
|
1365
|
+
process.exit(1);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const updatedLesson = {
|
|
1369
|
+
...lesson,
|
|
1370
|
+
...options.insight !== void 0 && { insight: options.insight },
|
|
1371
|
+
...options.trigger !== void 0 && { trigger: options.trigger },
|
|
1372
|
+
...options.evidence !== void 0 && { evidence: options.evidence },
|
|
1373
|
+
...options.severity !== void 0 && { severity: options.severity },
|
|
1374
|
+
...options.tags !== void 0 && {
|
|
1375
|
+
tags: [...new Set(
|
|
1376
|
+
options.tags.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
|
|
1377
|
+
)]
|
|
1378
|
+
},
|
|
1379
|
+
...options.confirmed !== void 0 && { confirmed: options.confirmed === "true" }
|
|
1380
|
+
};
|
|
1381
|
+
const validationResult = LessonSchema.safeParse(updatedLesson);
|
|
1382
|
+
if (!validationResult.success) {
|
|
1383
|
+
if (options.json) {
|
|
1384
|
+
console.log(JSON.stringify({ error: `Schema validation failed: ${validationResult.error.message}` }));
|
|
1385
|
+
} else {
|
|
1386
|
+
out.error(`Schema validation failed: ${validationResult.error.message}`);
|
|
1387
|
+
}
|
|
1388
|
+
process.exit(1);
|
|
1389
|
+
}
|
|
1390
|
+
await appendLesson(repoRoot, updatedLesson);
|
|
1391
|
+
await syncIfNeeded(repoRoot);
|
|
1392
|
+
if (options.json) {
|
|
1393
|
+
console.log(JSON.stringify(updatedLesson, null, SHOW_JSON_INDENT));
|
|
1394
|
+
} else {
|
|
1395
|
+
out.success(`Updated lesson ${id}`);
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
program2.command("delete <ids...>").description("Soft delete lessons (creates tombstone)").option("--json", "Output as JSON").action(async (ids, options) => {
|
|
1399
|
+
const repoRoot = getRepoRoot();
|
|
1400
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1401
|
+
const lessonMap = new Map(lessons.map((l) => [l.id, l]));
|
|
1402
|
+
const deleted = [];
|
|
1403
|
+
const warnings = [];
|
|
1404
|
+
for (const id of ids) {
|
|
1405
|
+
const lesson = lessonMap.get(id);
|
|
1406
|
+
if (!lesson) {
|
|
1407
|
+
const wasDeleted = await wasLessonDeleted(repoRoot, id);
|
|
1408
|
+
warnings.push({ id, message: wasDeleted ? "already deleted" : "not found" });
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
const tombstone = {
|
|
1412
|
+
...lesson,
|
|
1413
|
+
deleted: true,
|
|
1414
|
+
deletedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1415
|
+
};
|
|
1416
|
+
await appendLesson(repoRoot, tombstone);
|
|
1417
|
+
deleted.push(id);
|
|
1418
|
+
}
|
|
1419
|
+
if (deleted.length > 0) {
|
|
1420
|
+
await syncIfNeeded(repoRoot);
|
|
1421
|
+
}
|
|
1422
|
+
if (options.json) {
|
|
1423
|
+
console.log(JSON.stringify({ deleted, warnings }));
|
|
1424
|
+
} else {
|
|
1425
|
+
if (deleted.length > 0) {
|
|
1426
|
+
out.success(`Deleted ${deleted.length} lesson(s): ${deleted.join(", ")}`);
|
|
1427
|
+
}
|
|
1428
|
+
for (const warning of warnings) {
|
|
1429
|
+
out.warn(`${warning.id}: ${warning.message}`);
|
|
1430
|
+
}
|
|
1431
|
+
if (deleted.length === 0 && warnings.length > 0) {
|
|
1432
|
+
process.exit(1);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
function formatLessonHuman(lesson) {
|
|
1438
|
+
const lines = [];
|
|
1439
|
+
lines.push(`ID: ${lesson.id}`);
|
|
1440
|
+
lines.push(`Type: ${lesson.type}`);
|
|
1441
|
+
lines.push(`Trigger: ${lesson.trigger}`);
|
|
1442
|
+
lines.push(`Insight: ${lesson.insight}`);
|
|
1443
|
+
if (lesson.evidence) {
|
|
1444
|
+
lines.push(`Evidence: ${lesson.evidence}`);
|
|
1445
|
+
}
|
|
1446
|
+
if (lesson.severity) {
|
|
1447
|
+
lines.push(`Severity: ${lesson.severity}`);
|
|
1448
|
+
}
|
|
1449
|
+
lines.push(`Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
|
|
1450
|
+
lines.push(`Source: ${lesson.source}`);
|
|
1451
|
+
if (lesson.context) {
|
|
1452
|
+
lines.push(`Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
1453
|
+
}
|
|
1454
|
+
lines.push(`Created: ${lesson.created}`);
|
|
1455
|
+
lines.push(`Confirmed: ${lesson.confirmed ? "yes" : "no"}`);
|
|
1456
|
+
if (lesson.supersedes && lesson.supersedes.length > 0) {
|
|
1457
|
+
lines.push(`Supersedes: ${lesson.supersedes.join(", ")}`);
|
|
1458
|
+
}
|
|
1459
|
+
if (lesson.related && lesson.related.length > 0) {
|
|
1460
|
+
lines.push(`Related: ${lesson.related.join(", ")}`);
|
|
1461
|
+
}
|
|
1462
|
+
if (lesson.pattern) {
|
|
1463
|
+
lines.push("Pattern:");
|
|
1464
|
+
lines.push(` Bad: ${lesson.pattern.bad}`);
|
|
1465
|
+
lines.push(` Good: ${lesson.pattern.good}`);
|
|
1466
|
+
}
|
|
1467
|
+
return lines.join("\n");
|
|
1468
|
+
}
|
|
1469
|
+
async function wasLessonDeleted(repoRoot, id) {
|
|
1470
|
+
const filePath = join(repoRoot, LESSONS_PATH);
|
|
1471
|
+
try {
|
|
1472
|
+
const content = await readFile(filePath, "utf-8");
|
|
1473
|
+
const lines = content.split("\n");
|
|
1474
|
+
for (const line of lines) {
|
|
1475
|
+
const trimmed = line.trim();
|
|
1476
|
+
if (!trimmed) continue;
|
|
1477
|
+
try {
|
|
1478
|
+
const record = JSON.parse(trimmed);
|
|
1479
|
+
if (record.id === id && record.deleted === true) {
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
} catch {
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
} catch {
|
|
1486
|
+
}
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
|
|
1490
|
+
var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
|
|
1491
|
+
var DEFAULT_MODEL_DIR = join(homedir(), ".node-llama-cpp", "models");
|
|
1492
|
+
function isModelAvailable() {
|
|
1493
|
+
return existsSync(join(DEFAULT_MODEL_DIR, MODEL_FILENAME));
|
|
1494
|
+
}
|
|
1495
|
+
async function resolveModel(options = {}) {
|
|
1496
|
+
const { cli = true } = options;
|
|
1497
|
+
return resolveModelFile(MODEL_URI, { cli });
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/embeddings/nomic.ts
|
|
1501
|
+
var embeddingContext = null;
|
|
1502
|
+
async function getEmbedding() {
|
|
1503
|
+
if (embeddingContext) return embeddingContext;
|
|
1504
|
+
const modelPath = await resolveModel({ cli: true });
|
|
1505
|
+
const llama = await getLlama();
|
|
1506
|
+
const model = await llama.loadModel({ modelPath });
|
|
1507
|
+
embeddingContext = await model.createEmbeddingContext();
|
|
1508
|
+
return embeddingContext;
|
|
1509
|
+
}
|
|
1510
|
+
async function embedText(text) {
|
|
1511
|
+
const ctx = await getEmbedding();
|
|
1512
|
+
const result = await ctx.getEmbeddingFor(text);
|
|
1513
|
+
return Array.from(result.vector);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/search/vector.ts
|
|
1517
|
+
function cosineSimilarity(a, b) {
|
|
1518
|
+
if (a.length !== b.length) {
|
|
1519
|
+
throw new Error("Vectors must have same length");
|
|
1520
|
+
}
|
|
1521
|
+
let dotProduct = 0;
|
|
1522
|
+
let normA = 0;
|
|
1523
|
+
let normB = 0;
|
|
1524
|
+
for (let i = 0; i < a.length; i++) {
|
|
1525
|
+
dotProduct += a[i] * b[i];
|
|
1526
|
+
normA += a[i] * a[i];
|
|
1527
|
+
normB += b[i] * b[i];
|
|
1528
|
+
}
|
|
1529
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1530
|
+
if (magnitude === 0) return 0;
|
|
1531
|
+
return dotProduct / magnitude;
|
|
1532
|
+
}
|
|
1533
|
+
var DEFAULT_LIMIT = 10;
|
|
1534
|
+
async function searchVector(repoRoot, query, options) {
|
|
1535
|
+
const limit = options?.limit ?? DEFAULT_LIMIT;
|
|
1536
|
+
const { lessons } = await readLessons(repoRoot);
|
|
1537
|
+
if (lessons.length === 0) return [];
|
|
1538
|
+
const queryVector = await embedText(query);
|
|
1539
|
+
const scored = [];
|
|
1540
|
+
for (const lesson of lessons) {
|
|
1541
|
+
if (lesson.invalidatedAt) continue;
|
|
1542
|
+
const lessonText = `${lesson.trigger} ${lesson.insight}`;
|
|
1543
|
+
const hash = contentHash(lesson.trigger, lesson.insight);
|
|
1544
|
+
let lessonVector = getCachedEmbedding(repoRoot, lesson.id, hash);
|
|
1545
|
+
if (!lessonVector) {
|
|
1546
|
+
lessonVector = await embedText(lessonText);
|
|
1547
|
+
setCachedEmbedding(repoRoot, lesson.id, lessonVector, hash);
|
|
1548
|
+
}
|
|
1549
|
+
const score = cosineSimilarity(queryVector, lessonVector);
|
|
1550
|
+
scored.push({ lesson, score });
|
|
1551
|
+
}
|
|
1552
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1553
|
+
return scored.slice(0, limit);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/search/ranking.ts
|
|
1557
|
+
var RECENCY_THRESHOLD_DAYS = 30;
|
|
1558
|
+
var HIGH_SEVERITY_BOOST = 1.5;
|
|
1559
|
+
var MEDIUM_SEVERITY_BOOST = 1;
|
|
1560
|
+
var LOW_SEVERITY_BOOST = 0.8;
|
|
1561
|
+
var RECENCY_BOOST = 1.2;
|
|
1562
|
+
var CONFIRMATION_BOOST = 1.3;
|
|
1563
|
+
function severityBoost(lesson) {
|
|
1564
|
+
switch (lesson.severity) {
|
|
1565
|
+
case "high":
|
|
1566
|
+
return HIGH_SEVERITY_BOOST;
|
|
1567
|
+
case "medium":
|
|
1568
|
+
return MEDIUM_SEVERITY_BOOST;
|
|
1569
|
+
case "low":
|
|
1570
|
+
return LOW_SEVERITY_BOOST;
|
|
1571
|
+
default:
|
|
1572
|
+
return MEDIUM_SEVERITY_BOOST;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function recencyBoost(lesson) {
|
|
1576
|
+
const ageDays = getLessonAgeDays(lesson);
|
|
1577
|
+
return ageDays <= RECENCY_THRESHOLD_DAYS ? RECENCY_BOOST : 1;
|
|
1578
|
+
}
|
|
1579
|
+
function confirmationBoost(lesson) {
|
|
1580
|
+
return lesson.confirmed ? CONFIRMATION_BOOST : 1;
|
|
1581
|
+
}
|
|
1582
|
+
function calculateScore(lesson, vectorSimilarity) {
|
|
1583
|
+
return vectorSimilarity * severityBoost(lesson) * recencyBoost(lesson) * confirmationBoost(lesson);
|
|
1584
|
+
}
|
|
1585
|
+
function rankLessons(lessons) {
|
|
1586
|
+
return lessons.map((scored) => ({
|
|
1587
|
+
...scored,
|
|
1588
|
+
finalScore: calculateScore(scored.lesson, scored.score)
|
|
1589
|
+
})).sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/retrieval/session.ts
|
|
1593
|
+
var DEFAULT_LIMIT2 = 5;
|
|
1594
|
+
function isFullLesson(lesson) {
|
|
1595
|
+
return lesson.type === "full" && lesson.severity !== void 0;
|
|
1596
|
+
}
|
|
1597
|
+
async function loadSessionLessons(repoRoot, limit = DEFAULT_LIMIT2) {
|
|
1598
|
+
const { lessons: allLessons } = await readLessons(repoRoot);
|
|
1599
|
+
const highSeverityLessons = allLessons.filter(
|
|
1600
|
+
(lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed && !lesson.invalidatedAt
|
|
1601
|
+
);
|
|
1602
|
+
highSeverityLessons.sort((a, b) => {
|
|
1603
|
+
const dateA = new Date(a.created).getTime();
|
|
1604
|
+
const dateB = new Date(b.created).getTime();
|
|
1605
|
+
return dateB - dateA;
|
|
1606
|
+
});
|
|
1607
|
+
return highSeverityLessons.slice(0, limit);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/retrieval/plan.ts
|
|
1611
|
+
var DEFAULT_LIMIT3 = 5;
|
|
1612
|
+
async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
|
|
1613
|
+
const scored = await searchVector(repoRoot, planText, { limit: limit * 2 });
|
|
1614
|
+
const ranked = rankLessons(scored);
|
|
1615
|
+
const topLessons = ranked.slice(0, limit);
|
|
1616
|
+
const message = formatLessonsCheck(topLessons);
|
|
1617
|
+
return { lessons: topLessons, message };
|
|
1618
|
+
}
|
|
1619
|
+
function formatLessonsCheck(lessons) {
|
|
1620
|
+
const header = "\u{1F4DA} Lessons Check\n" + "\u2500".repeat(40);
|
|
1621
|
+
if (lessons.length === 0) {
|
|
1622
|
+
return `${header}
|
|
1623
|
+
No relevant lessons found for this plan.`;
|
|
1624
|
+
}
|
|
1625
|
+
const lessonLines = lessons.map((l, i) => {
|
|
1626
|
+
const bullet = `${i + 1}.`;
|
|
1627
|
+
const insight = l.lesson.insight;
|
|
1628
|
+
return `${bullet} ${insight}`;
|
|
1629
|
+
});
|
|
1630
|
+
return `${header}
|
|
1631
|
+
${lessonLines.join("\n")}`;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// src/index.ts
|
|
1635
|
+
var VERSION = "0.1.0";
|
|
1636
|
+
|
|
1637
|
+
// src/commands/retrieval.ts
|
|
1638
|
+
async function readPlanFromStdin() {
|
|
1639
|
+
const { stdin } = await import('process');
|
|
1640
|
+
if (!stdin.isTTY) {
|
|
1641
|
+
const chunks = [];
|
|
1642
|
+
for await (const chunk of stdin) {
|
|
1643
|
+
chunks.push(chunk);
|
|
1644
|
+
}
|
|
1645
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
1646
|
+
}
|
|
1647
|
+
return void 0;
|
|
1648
|
+
}
|
|
1649
|
+
function outputCheckPlanJson(lessons) {
|
|
1650
|
+
const jsonOutput = {
|
|
1651
|
+
lessons: lessons.map((l) => ({
|
|
1652
|
+
id: l.lesson.id,
|
|
1653
|
+
insight: l.lesson.insight,
|
|
1654
|
+
relevance: l.score,
|
|
1655
|
+
source: l.lesson.source
|
|
1656
|
+
})),
|
|
1657
|
+
count: lessons.length
|
|
1658
|
+
};
|
|
1659
|
+
console.log(JSON.stringify(jsonOutput));
|
|
1660
|
+
}
|
|
1661
|
+
function outputCheckPlanHuman(lessons, quiet) {
|
|
1662
|
+
console.log("## Lessons Check\n");
|
|
1663
|
+
console.log("Relevant to your plan:\n");
|
|
1664
|
+
lessons.forEach((item, i) => {
|
|
1665
|
+
const num = i + 1;
|
|
1666
|
+
console.log(`${num}. ${chalk.bold(`[${item.lesson.id}]`)} ${item.lesson.insight}`);
|
|
1667
|
+
console.log(` - Relevance: ${item.score.toFixed(RELEVANCE_DECIMAL_PLACES)}`);
|
|
1668
|
+
console.log(` - Source: ${item.lesson.source}`);
|
|
1669
|
+
console.log();
|
|
1043
1670
|
});
|
|
1044
1671
|
if (!quiet) {
|
|
1045
1672
|
console.log("---");
|
|
1046
1673
|
console.log("Consider these lessons while implementing.");
|
|
1047
1674
|
}
|
|
1048
1675
|
}
|
|
1676
|
+
function formatSource(source) {
|
|
1677
|
+
return source.replace(/_/g, " ");
|
|
1678
|
+
}
|
|
1049
1679
|
function outputSessionLessonsHuman(lessons, quiet) {
|
|
1050
|
-
console.log("##
|
|
1680
|
+
console.log("## Lessons from Past Sessions\n");
|
|
1681
|
+
console.log("These lessons were captured from previous corrections and should inform your work:\n");
|
|
1051
1682
|
lessons.forEach((lesson, i) => {
|
|
1052
1683
|
const num = i + 1;
|
|
1053
1684
|
const date = lesson.created.slice(0, ISO_DATE_PREFIX_LENGTH);
|
|
1054
|
-
|
|
1055
|
-
console.log(
|
|
1056
|
-
|
|
1057
|
-
console.log(` - Tags: ${lesson.tags.join(", ")}`);
|
|
1058
|
-
}
|
|
1685
|
+
const tagsDisplay = lesson.tags.length > 0 ? ` (${lesson.tags.join(", ")})` : "";
|
|
1686
|
+
console.log(`${num}. **${lesson.insight}**${tagsDisplay}`);
|
|
1687
|
+
console.log(` Learned: ${date} via ${formatSource(lesson.source)}`);
|
|
1059
1688
|
console.log();
|
|
1060
1689
|
});
|
|
1061
|
-
const lessonWord = lessons.length === 1 ? "lesson" : "lessons";
|
|
1062
1690
|
if (!quiet) {
|
|
1063
|
-
console.log("
|
|
1064
|
-
console.log(`${lessons.length} high-severity ${lessonWord} loaded.`);
|
|
1691
|
+
console.log("Consider these lessons when planning and implementing tasks.");
|
|
1065
1692
|
}
|
|
1066
1693
|
}
|
|
1694
|
+
function registerRetrievalCommands(program2) {
|
|
1695
|
+
program2.command("search <query>").description("Search lessons by keyword").option("-n, --limit <number>", "Maximum results", DEFAULT_SEARCH_LIMIT).action(async function(query, options) {
|
|
1696
|
+
const repoRoot = getRepoRoot();
|
|
1697
|
+
const limit = parseLimit(options.limit, "limit");
|
|
1698
|
+
const { verbose, quiet } = getGlobalOpts(this);
|
|
1699
|
+
await syncIfNeeded(repoRoot);
|
|
1700
|
+
const results = await searchKeyword(repoRoot, query, limit);
|
|
1701
|
+
if (results.length === 0) {
|
|
1702
|
+
console.log('No lessons match your search. Try a different query or use "list" to see all lessons.');
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (!quiet) {
|
|
1706
|
+
out.info(`Found ${results.length} lesson(s):
|
|
1707
|
+
`);
|
|
1708
|
+
}
|
|
1709
|
+
for (const lesson of results) {
|
|
1710
|
+
console.log(`[${chalk.cyan(lesson.id)}] ${lesson.insight}`);
|
|
1711
|
+
console.log(` Trigger: ${lesson.trigger}`);
|
|
1712
|
+
if (verbose && lesson.context) {
|
|
1713
|
+
console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
1714
|
+
console.log(` Created: ${lesson.created}`);
|
|
1715
|
+
}
|
|
1716
|
+
if (lesson.tags.length > 0) {
|
|
1717
|
+
console.log(` Tags: ${lesson.tags.join(", ")}`);
|
|
1718
|
+
}
|
|
1719
|
+
console.log();
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
program2.command("list").description("List all lessons").option("-n, --limit <number>", "Maximum results", DEFAULT_LIST_LIMIT).option("--invalidated", "Show only invalidated lessons").action(async function(options) {
|
|
1723
|
+
const repoRoot = getRepoRoot();
|
|
1724
|
+
const limit = parseLimit(options.limit, "limit");
|
|
1725
|
+
const { verbose, quiet } = getGlobalOpts(this);
|
|
1726
|
+
const { lessons, skippedCount } = await readLessons(repoRoot);
|
|
1727
|
+
const filteredLessons = options.invalidated ? lessons.filter((l) => l.invalidatedAt) : lessons;
|
|
1728
|
+
if (filteredLessons.length === 0) {
|
|
1729
|
+
if (options.invalidated) {
|
|
1730
|
+
console.log("No invalidated lessons found.");
|
|
1731
|
+
} else {
|
|
1732
|
+
console.log('No lessons found. Get started with: learn "Your first lesson"');
|
|
1733
|
+
}
|
|
1734
|
+
if (skippedCount > 0) {
|
|
1735
|
+
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
1736
|
+
}
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
const toShow = filteredLessons.slice(0, limit);
|
|
1740
|
+
if (!quiet) {
|
|
1741
|
+
const label = options.invalidated ? "invalidated lesson(s)" : "lesson(s)";
|
|
1742
|
+
out.info(`Showing ${toShow.length} of ${filteredLessons.length} ${label}:
|
|
1743
|
+
`);
|
|
1744
|
+
}
|
|
1745
|
+
for (const lesson of toShow) {
|
|
1746
|
+
const invalidMarker = lesson.invalidatedAt ? chalk.red("[INVALID] ") : "";
|
|
1747
|
+
console.log(`[${chalk.cyan(lesson.id)}] ${invalidMarker}${lesson.insight}`);
|
|
1748
|
+
if (verbose) {
|
|
1749
|
+
console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
|
|
1750
|
+
console.log(` Created: ${lesson.created}`);
|
|
1751
|
+
if (lesson.context) {
|
|
1752
|
+
console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
1753
|
+
}
|
|
1754
|
+
if (lesson.invalidatedAt) {
|
|
1755
|
+
console.log(` Invalidated: ${lesson.invalidatedAt}`);
|
|
1756
|
+
if (lesson.invalidationReason) {
|
|
1757
|
+
console.log(` Reason: ${lesson.invalidationReason}`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
} else {
|
|
1761
|
+
console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
|
|
1762
|
+
}
|
|
1763
|
+
if (lesson.tags.length > 0) {
|
|
1764
|
+
console.log(` Tags: ${lesson.tags.join(", ")}`);
|
|
1765
|
+
}
|
|
1766
|
+
console.log();
|
|
1767
|
+
}
|
|
1768
|
+
if (skippedCount > 0) {
|
|
1769
|
+
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
program2.command("load-session").description("Load high-severity lessons for session context").option("--json", "Output as JSON").action(async function(options) {
|
|
1773
|
+
const repoRoot = getRepoRoot();
|
|
1774
|
+
const { quiet } = getGlobalOpts(this);
|
|
1775
|
+
const lessons = await loadSessionLessons(repoRoot);
|
|
1776
|
+
const { lessons: allLessons } = await readLessons(repoRoot);
|
|
1777
|
+
const totalCount = allLessons.length;
|
|
1778
|
+
if (options.json) {
|
|
1779
|
+
console.log(JSON.stringify({ lessons, count: lessons.length, totalCount }));
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (lessons.length === 0) {
|
|
1783
|
+
console.log("No high-severity lessons found.");
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
outputSessionLessonsHuman(lessons, quiet);
|
|
1787
|
+
if (totalCount > LESSON_COUNT_WARNING_THRESHOLD) {
|
|
1788
|
+
console.log("");
|
|
1789
|
+
out.info(`${totalCount} lessons in index. Consider \`lna compact\` to reduce context pollution.`);
|
|
1790
|
+
}
|
|
1791
|
+
const oldLessons = lessons.filter((l) => getLessonAgeDays(l) > AGE_FLAG_THRESHOLD_DAYS);
|
|
1792
|
+
if (oldLessons.length > 0) {
|
|
1793
|
+
console.log("");
|
|
1794
|
+
out.warn(`${oldLessons.length} lesson(s) are over ${AGE_FLAG_THRESHOLD_DAYS} days old. Review for continued validity.`);
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
program2.command("check-plan").description("Check plan against relevant lessons").option("--plan <text>", "Plan text to check").option("--json", "Output as JSON").option("-n, --limit <number>", "Maximum results", DEFAULT_CHECK_PLAN_LIMIT).action(async function(options) {
|
|
1798
|
+
const repoRoot = getRepoRoot();
|
|
1799
|
+
const limit = parseLimit(options.limit, "limit");
|
|
1800
|
+
const { quiet } = getGlobalOpts(this);
|
|
1801
|
+
const planText = options.plan ?? await readPlanFromStdin();
|
|
1802
|
+
if (!planText) {
|
|
1803
|
+
out.error("No plan provided. Use --plan <text> or pipe text to stdin.");
|
|
1804
|
+
process.exit(1);
|
|
1805
|
+
}
|
|
1806
|
+
if (!isModelAvailable()) {
|
|
1807
|
+
if (options.json) {
|
|
1808
|
+
console.log(JSON.stringify({
|
|
1809
|
+
error: "Embedding model not available",
|
|
1810
|
+
action: "Run: npx learning-agent download-model"
|
|
1811
|
+
}));
|
|
1812
|
+
} else {
|
|
1813
|
+
out.error("Embedding model not available");
|
|
1814
|
+
console.log("");
|
|
1815
|
+
console.log("Run: npx learning-agent download-model");
|
|
1816
|
+
}
|
|
1817
|
+
process.exit(1);
|
|
1818
|
+
}
|
|
1819
|
+
try {
|
|
1820
|
+
const result = await retrieveForPlan(repoRoot, planText, limit);
|
|
1821
|
+
if (options.json) {
|
|
1822
|
+
outputCheckPlanJson(result.lessons);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (result.lessons.length === 0) {
|
|
1826
|
+
console.log("No relevant lessons found for this plan.");
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
outputCheckPlanHuman(result.lessons, quiet);
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1832
|
+
if (options.json) {
|
|
1833
|
+
console.log(JSON.stringify({ error: message }));
|
|
1834
|
+
} else {
|
|
1835
|
+
out.error(`Failed to check plan: ${message}`);
|
|
1836
|
+
}
|
|
1837
|
+
process.exit(1);
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
var PRE_COMMIT_MESSAGE = `Before committing, have you captured any valuable lessons from this session?
|
|
1842
|
+
Consider: corrections, mistakes, or insights worth remembering.
|
|
1843
|
+
|
|
1844
|
+
To capture a lesson:
|
|
1845
|
+
npx learning-agent capture --trigger "what happened" --insight "what to do" --yes`;
|
|
1846
|
+
var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
|
|
1847
|
+
# Learning Agent pre-commit hook
|
|
1848
|
+
# Reminds Claude to consider capturing lessons before commits
|
|
1849
|
+
|
|
1850
|
+
npx learning-agent hooks run pre-commit
|
|
1851
|
+
`;
|
|
1852
|
+
var CLAUDE_HOOK_MARKER = "learning-agent load-session";
|
|
1853
|
+
var CLAUDE_HOOK_CONFIG = {
|
|
1854
|
+
matcher: "startup|resume|compact",
|
|
1855
|
+
hooks: [
|
|
1856
|
+
{
|
|
1857
|
+
type: "command",
|
|
1858
|
+
command: "npx learning-agent load-session 2>/dev/null || true"
|
|
1859
|
+
}
|
|
1860
|
+
]
|
|
1861
|
+
};
|
|
1862
|
+
var HOOK_MARKER = "# Learning Agent pre-commit hook";
|
|
1067
1863
|
var LEARNING_AGENT_SECTION_HEADER = "## Learning Agent Integration";
|
|
1068
1864
|
var AGENTS_MD_TEMPLATE = `
|
|
1069
1865
|
## Learning Agent Integration
|
|
@@ -1181,6 +1977,36 @@ var LEARNING_AGENT_HOOK_BLOCK = `
|
|
|
1181
1977
|
# Learning Agent pre-commit hook (appended)
|
|
1182
1978
|
npx learning-agent hooks run pre-commit
|
|
1183
1979
|
`;
|
|
1980
|
+
function findFirstTopLevelExitLine(lines) {
|
|
1981
|
+
let insideFunction = 0;
|
|
1982
|
+
let heredocDelimiter = null;
|
|
1983
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1984
|
+
const line = lines[i] ?? "";
|
|
1985
|
+
const trimmed = line.trim();
|
|
1986
|
+
if (heredocDelimiter !== null) {
|
|
1987
|
+
if (trimmed === heredocDelimiter) {
|
|
1988
|
+
heredocDelimiter = null;
|
|
1989
|
+
}
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
const heredocMatch = /<<-?\s*['"]?(\w+)['"]?/.exec(line);
|
|
1993
|
+
if (heredocMatch?.[1]) {
|
|
1994
|
+
heredocDelimiter = heredocMatch[1];
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
for (const char of line) {
|
|
1998
|
+
if (char === "{") insideFunction++;
|
|
1999
|
+
if (char === "}") insideFunction = Math.max(0, insideFunction - 1);
|
|
2000
|
+
}
|
|
2001
|
+
if (insideFunction > 0) {
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
if (/^\s*exit\s+(\d+|\$\w+|\$\?)\s*$/.test(trimmed)) {
|
|
2005
|
+
return i;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return -1;
|
|
2009
|
+
}
|
|
1184
2010
|
async function installPreCommitHook(repoRoot) {
|
|
1185
2011
|
const gitHooksDir = await getGitHooksDir(repoRoot);
|
|
1186
2012
|
if (!gitHooksDir) {
|
|
@@ -1193,7 +2019,16 @@ async function installPreCommitHook(repoRoot) {
|
|
|
1193
2019
|
if (hasLearningAgentHook(content)) {
|
|
1194
2020
|
return false;
|
|
1195
2021
|
}
|
|
1196
|
-
const
|
|
2022
|
+
const lines = content.split("\n");
|
|
2023
|
+
const exitLineIndex = findFirstTopLevelExitLine(lines);
|
|
2024
|
+
let newContent;
|
|
2025
|
+
if (exitLineIndex === -1) {
|
|
2026
|
+
newContent = content.trimEnd() + "\n" + LEARNING_AGENT_HOOK_BLOCK;
|
|
2027
|
+
} else {
|
|
2028
|
+
const before = lines.slice(0, exitLineIndex);
|
|
2029
|
+
const after = lines.slice(exitLineIndex);
|
|
2030
|
+
newContent = before.join("\n") + LEARNING_AGENT_HOOK_BLOCK + after.join("\n");
|
|
2031
|
+
}
|
|
1197
2032
|
await writeFile(hookPath, newContent, "utf-8");
|
|
1198
2033
|
chmodSync(hookPath, HOOK_FILE_MODE);
|
|
1199
2034
|
return true;
|
|
@@ -1202,72 +2037,12 @@ async function installPreCommitHook(repoRoot) {
|
|
|
1202
2037
|
chmodSync(hookPath, HOOK_FILE_MODE);
|
|
1203
2038
|
return true;
|
|
1204
2039
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
program.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) {
|
|
1209
|
-
const repoRoot = getRepoRoot();
|
|
1210
|
-
const { quiet } = getGlobalOpts(this);
|
|
1211
|
-
await createLessonsDirectory(repoRoot);
|
|
1212
|
-
await createIndexFile(repoRoot);
|
|
1213
|
-
const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
|
|
1214
|
-
let agentsMdUpdated = false;
|
|
1215
|
-
if (!options.skipAgents) {
|
|
1216
|
-
agentsMdUpdated = await updateAgentsMd(repoRoot);
|
|
1217
|
-
}
|
|
1218
|
-
let hooksInstalled = false;
|
|
1219
|
-
if (!options.skipHooks) {
|
|
1220
|
-
hooksInstalled = await installPreCommitHook(repoRoot);
|
|
1221
|
-
}
|
|
1222
|
-
if (options.json) {
|
|
1223
|
-
console.log(JSON.stringify({
|
|
1224
|
-
initialized: true,
|
|
1225
|
-
lessonsDir,
|
|
1226
|
-
agentsMd: agentsMdUpdated,
|
|
1227
|
-
hooks: hooksInstalled
|
|
1228
|
-
}));
|
|
1229
|
-
} else if (!quiet) {
|
|
1230
|
-
out.success("Learning agent initialized");
|
|
1231
|
-
console.log(` Lessons directory: ${lessonsDir}`);
|
|
1232
|
-
if (agentsMdUpdated) {
|
|
1233
|
-
console.log(" AGENTS.md: Updated with Learning Agent section");
|
|
1234
|
-
} else if (options.skipAgents) {
|
|
1235
|
-
console.log(" AGENTS.md: Skipped (--skip-agents)");
|
|
1236
|
-
} else {
|
|
1237
|
-
console.log(" AGENTS.md: Already has Learning Agent section");
|
|
1238
|
-
}
|
|
1239
|
-
if (hooksInstalled) {
|
|
1240
|
-
console.log(" Git hooks: pre-commit hook installed");
|
|
1241
|
-
} else if (options.skipHooks) {
|
|
1242
|
-
console.log(" Git hooks: Skipped (--skip-hooks)");
|
|
1243
|
-
} else {
|
|
1244
|
-
console.log(" Git hooks: Already installed or not a git repo");
|
|
1245
|
-
}
|
|
2040
|
+
function getClaudeSettingsPath(global) {
|
|
2041
|
+
if (global) {
|
|
2042
|
+
return join(homedir(), ".claude", "settings.json");
|
|
1246
2043
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
hooksCommand.command("run <hook>").description("Run a hook script (called by git hooks)").option("--json", "Output as JSON").action((hook, options) => {
|
|
1250
|
-
if (hook === "pre-commit") {
|
|
1251
|
-
if (options.json) {
|
|
1252
|
-
console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
|
|
1253
|
-
} else {
|
|
1254
|
-
console.log(PRE_COMMIT_MESSAGE);
|
|
1255
|
-
}
|
|
1256
|
-
} else {
|
|
1257
|
-
if (options.json) {
|
|
1258
|
-
console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
|
|
1259
|
-
} else {
|
|
1260
|
-
out.error(`Unknown hook: ${hook}`);
|
|
1261
|
-
}
|
|
1262
|
-
process.exit(1);
|
|
1263
|
-
}
|
|
1264
|
-
});
|
|
1265
|
-
function getClaudeSettingsPath(project) {
|
|
1266
|
-
if (project) {
|
|
1267
|
-
const repoRoot = getRepoRoot();
|
|
1268
|
-
return join(repoRoot, ".claude", "settings.json");
|
|
1269
|
-
}
|
|
1270
|
-
return join(homedir(), ".claude", "settings.json");
|
|
2044
|
+
const repoRoot = getRepoRoot();
|
|
2045
|
+
return join(repoRoot, ".claude", "settings.json");
|
|
1271
2046
|
}
|
|
1272
2047
|
async function readClaudeSettings(settingsPath) {
|
|
1273
2048
|
if (!existsSync(settingsPath)) {
|
|
@@ -1311,477 +2086,202 @@ async function writeClaudeSettings(settingsPath, settings) {
|
|
|
1311
2086
|
await writeFile(tempPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
1312
2087
|
await rename(tempPath, settingsPath);
|
|
1313
2088
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
if (options.
|
|
1323
|
-
|
|
1324
|
-
} else {
|
|
1325
|
-
out.error("Failed to parse settings file. Check if JSON is valid.");
|
|
2089
|
+
function registerSetupCommands(program2) {
|
|
2090
|
+
program2.command("init").description("Initialize learning-agent in this repository").option("--skip-agents", "Skip AGENTS.md modification").option("--skip-hooks", "Skip git hooks installation").option("--json", "Output result as JSON").action(async function(options) {
|
|
2091
|
+
const repoRoot = getRepoRoot();
|
|
2092
|
+
const { quiet } = getGlobalOpts(this);
|
|
2093
|
+
await createLessonsDirectory(repoRoot);
|
|
2094
|
+
await createIndexFile(repoRoot);
|
|
2095
|
+
const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
|
|
2096
|
+
let agentsMdUpdated = false;
|
|
2097
|
+
if (!options.skipAgents) {
|
|
2098
|
+
agentsMdUpdated = await updateAgentsMd(repoRoot);
|
|
1326
2099
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
if (options.
|
|
1332
|
-
|
|
1333
|
-
|
|
2100
|
+
let hooksInstalled = false;
|
|
2101
|
+
if (!options.skipHooks) {
|
|
2102
|
+
hooksInstalled = await installPreCommitHook(repoRoot);
|
|
2103
|
+
}
|
|
2104
|
+
if (options.json) {
|
|
2105
|
+
console.log(JSON.stringify({
|
|
2106
|
+
initialized: true,
|
|
2107
|
+
lessonsDir,
|
|
2108
|
+
agentsMd: agentsMdUpdated,
|
|
2109
|
+
hooks: hooksInstalled
|
|
2110
|
+
}));
|
|
2111
|
+
} else if (!quiet) {
|
|
2112
|
+
out.success("Learning agent initialized");
|
|
2113
|
+
console.log(` Lessons directory: ${lessonsDir}`);
|
|
2114
|
+
if (agentsMdUpdated) {
|
|
2115
|
+
console.log(" AGENTS.md: Updated with Learning Agent section");
|
|
2116
|
+
} else if (options.skipAgents) {
|
|
2117
|
+
console.log(" AGENTS.md: Skipped (--skip-agents)");
|
|
1334
2118
|
} else {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
2119
|
+
console.log(" AGENTS.md: Already has Learning Agent section");
|
|
2120
|
+
}
|
|
2121
|
+
if (hooksInstalled) {
|
|
2122
|
+
console.log(" Git hooks: pre-commit hook installed");
|
|
2123
|
+
} else if (options.skipHooks) {
|
|
2124
|
+
console.log(" Git hooks: Skipped (--skip-hooks)");
|
|
2125
|
+
} else {
|
|
2126
|
+
console.log(" Git hooks: Already installed or not a git repo");
|
|
1340
2127
|
}
|
|
1341
|
-
return;
|
|
1342
2128
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
2129
|
+
});
|
|
2130
|
+
const hooksCommand = program2.command("hooks").description("Git hooks management");
|
|
2131
|
+
hooksCommand.command("run <hook>").description("Run a hook script (called by git hooks)").option("--json", "Output as JSON").action((hook, options) => {
|
|
2132
|
+
if (hook === "pre-commit") {
|
|
1346
2133
|
if (options.json) {
|
|
1347
|
-
console.log(JSON.stringify({
|
|
2134
|
+
console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
|
|
1348
2135
|
} else {
|
|
1349
|
-
|
|
1350
|
-
console.log(` Location: ${displayPath}`);
|
|
2136
|
+
console.log(PRE_COMMIT_MESSAGE);
|
|
1351
2137
|
}
|
|
1352
2138
|
} else {
|
|
1353
2139
|
if (options.json) {
|
|
1354
|
-
console.log(JSON.stringify({
|
|
2140
|
+
console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
|
|
1355
2141
|
} else {
|
|
1356
|
-
out.
|
|
2142
|
+
out.error(`Unknown hook: ${hook}`);
|
|
1357
2143
|
}
|
|
2144
|
+
process.exit(1);
|
|
1358
2145
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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" }));
|
|
1367
2157
|
} else {
|
|
1368
|
-
|
|
2158
|
+
out.error("Failed to parse settings file. Check if JSON is valid.");
|
|
1369
2159
|
}
|
|
2160
|
+
process.exit(1);
|
|
1370
2161
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
}
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
const fileExists = existsSync(settingsPath);
|
|
1388
|
-
addLearningAgentHook(settings);
|
|
1389
|
-
await writeClaudeSettings(settingsPath, settings);
|
|
1390
|
-
if (options.json) {
|
|
1391
|
-
console.log(JSON.stringify({
|
|
1392
|
-
installed: true,
|
|
1393
|
-
location: displayPath,
|
|
1394
|
-
hooks: ["SessionStart"],
|
|
1395
|
-
action: fileExists ? "updated" : "created"
|
|
1396
|
-
}));
|
|
1397
|
-
} else {
|
|
1398
|
-
out.success(options.project ? "Claude Code hooks installed (project-level)" : "Claude Code hooks installed");
|
|
1399
|
-
console.log(` Location: ${displayPath}`);
|
|
1400
|
-
console.log(" Hook: SessionStart (startup|resume|compact)");
|
|
1401
|
-
console.log("");
|
|
1402
|
-
console.log("Lessons will be loaded automatically at session start.");
|
|
1403
|
-
if (options.project) {
|
|
1404
|
-
console.log("");
|
|
1405
|
-
console.log("Note: Project hooks override global hooks.");
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
});
|
|
1409
|
-
program.command("learn <insight>").description("Capture a new lesson").option("-t, --trigger <text>", "What triggered this lesson").option("--tags <tags>", "Comma-separated tags", "").option("-y, --yes", "Skip confirmation").action(async function(insight, options) {
|
|
1410
|
-
const repoRoot = getRepoRoot();
|
|
1411
|
-
const { quiet } = getGlobalOpts(this);
|
|
1412
|
-
const lesson = {
|
|
1413
|
-
id: generateId(insight),
|
|
1414
|
-
type: "quick",
|
|
1415
|
-
trigger: options.trigger ?? "Manual capture",
|
|
1416
|
-
insight,
|
|
1417
|
-
tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : [],
|
|
1418
|
-
source: "manual",
|
|
1419
|
-
context: {
|
|
1420
|
-
tool: "cli",
|
|
1421
|
-
intent: "manual learning"
|
|
1422
|
-
},
|
|
1423
|
-
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1424
|
-
confirmed: true,
|
|
1425
|
-
// learn command is explicit confirmation
|
|
1426
|
-
supersedes: [],
|
|
1427
|
-
related: []
|
|
1428
|
-
};
|
|
1429
|
-
await appendLesson(repoRoot, lesson);
|
|
1430
|
-
out.success(`Learned: ${insight}`);
|
|
1431
|
-
if (!quiet) {
|
|
1432
|
-
console.log(`ID: ${chalk.dim(lesson.id)}`);
|
|
1433
|
-
}
|
|
1434
|
-
});
|
|
1435
|
-
program.command("search <query>").description("Search lessons by keyword").option("-n, --limit <number>", "Maximum results", DEFAULT_SEARCH_LIMIT).action(async function(query, options) {
|
|
1436
|
-
const repoRoot = getRepoRoot();
|
|
1437
|
-
const limit = parseLimit(options.limit, "limit");
|
|
1438
|
-
const { verbose, quiet } = getGlobalOpts(this);
|
|
1439
|
-
await syncIfNeeded(repoRoot);
|
|
1440
|
-
const results = await searchKeyword(repoRoot, query, limit);
|
|
1441
|
-
if (results.length === 0) {
|
|
1442
|
-
console.log('No lessons match your search. Try a different query or use "list" to see all lessons.');
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
if (!quiet) {
|
|
1446
|
-
out.info(`Found ${results.length} lesson(s):
|
|
1447
|
-
`);
|
|
1448
|
-
}
|
|
1449
|
-
for (const lesson of results) {
|
|
1450
|
-
console.log(`[${chalk.cyan(lesson.id)}] ${lesson.insight}`);
|
|
1451
|
-
console.log(` Trigger: ${lesson.trigger}`);
|
|
1452
|
-
if (verbose && lesson.context) {
|
|
1453
|
-
console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
1454
|
-
console.log(` Created: ${lesson.created}`);
|
|
1455
|
-
}
|
|
1456
|
-
if (lesson.tags.length > 0) {
|
|
1457
|
-
console.log(` Tags: ${lesson.tags.join(", ")}`);
|
|
1458
|
-
}
|
|
1459
|
-
console.log();
|
|
1460
|
-
}
|
|
1461
|
-
});
|
|
1462
|
-
program.command("list").description("List all lessons").option("-n, --limit <number>", "Maximum results", DEFAULT_LIST_LIMIT).action(async function(options) {
|
|
1463
|
-
const repoRoot = getRepoRoot();
|
|
1464
|
-
const limit = parseLimit(options.limit, "limit");
|
|
1465
|
-
const { verbose, quiet } = getGlobalOpts(this);
|
|
1466
|
-
const { lessons, skippedCount } = await readLessons(repoRoot);
|
|
1467
|
-
if (lessons.length === 0) {
|
|
1468
|
-
console.log('No lessons found. Get started with: learn "Your first lesson"');
|
|
1469
|
-
if (skippedCount > 0) {
|
|
1470
|
-
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
1471
|
-
}
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
const toShow = lessons.slice(0, limit);
|
|
1475
|
-
if (!quiet) {
|
|
1476
|
-
out.info(`Showing ${toShow.length} of ${lessons.length} lesson(s):
|
|
1477
|
-
`);
|
|
1478
|
-
}
|
|
1479
|
-
for (const lesson of toShow) {
|
|
1480
|
-
console.log(`[${chalk.cyan(lesson.id)}] ${lesson.insight}`);
|
|
1481
|
-
if (verbose) {
|
|
1482
|
-
console.log(` Type: ${lesson.type} | Source: ${lesson.source}`);
|
|
1483
|
-
console.log(` Created: ${lesson.created}`);
|
|
1484
|
-
if (lesson.context) {
|
|
1485
|
-
console.log(` Context: ${lesson.context.tool} - ${lesson.context.intent}`);
|
|
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;
|
|
1486
2175
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
out.warn(`${skippedCount} corrupted lesson(s) skipped.`);
|
|
1497
|
-
}
|
|
1498
|
-
});
|
|
1499
|
-
program.command("rebuild").description("Rebuild SQLite index from JSONL").option("-f, --force", "Force rebuild even if unchanged").action(async (options) => {
|
|
1500
|
-
const repoRoot = getRepoRoot();
|
|
1501
|
-
if (options.force) {
|
|
1502
|
-
console.log("Forcing index rebuild...");
|
|
1503
|
-
await rebuildIndex(repoRoot);
|
|
1504
|
-
console.log("Index rebuilt.");
|
|
1505
|
-
} else {
|
|
1506
|
-
const rebuilt = await syncIfNeeded(repoRoot);
|
|
1507
|
-
if (rebuilt) {
|
|
1508
|
-
console.log("Index rebuilt (JSONL changed).");
|
|
1509
|
-
} else {
|
|
1510
|
-
console.log("Index is up to date.");
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
});
|
|
1514
|
-
program.command("detect").description("Detect learning triggers from input").requiredOption("--input <file>", "Path to JSON input file").option("--save", "Save proposed lesson (requires --yes)").option("-y, --yes", "Confirm save (required with --save)").option("--json", "Output result as JSON").action(
|
|
1515
|
-
async (options) => {
|
|
1516
|
-
const repoRoot = getRepoRoot();
|
|
1517
|
-
if (options.save && !options.yes) {
|
|
1518
|
-
if (options.json) {
|
|
1519
|
-
console.log(JSON.stringify({ error: "--save requires --yes flag for confirmation" }));
|
|
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
|
+
}
|
|
1520
2185
|
} else {
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
+
}
|
|
1523
2196
|
}
|
|
1524
|
-
|
|
2197
|
+
return;
|
|
1525
2198
|
}
|
|
1526
|
-
|
|
1527
|
-
const result = await detectAndPropose(repoRoot, input);
|
|
1528
|
-
if (!result) {
|
|
2199
|
+
if (options.dryRun) {
|
|
1529
2200
|
if (options.json) {
|
|
1530
|
-
console.log(JSON.stringify({
|
|
2201
|
+
console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
|
|
1531
2202
|
} else {
|
|
1532
|
-
|
|
2203
|
+
if (alreadyInstalled) {
|
|
2204
|
+
console.log("Learning agent hooks already installed");
|
|
2205
|
+
} else {
|
|
2206
|
+
console.log(`Would install learning-agent hooks to ${displayPath}`);
|
|
2207
|
+
}
|
|
1533
2208
|
}
|
|
1534
2209
|
return;
|
|
1535
2210
|
}
|
|
1536
|
-
if (
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
trigger: result.trigger,
|
|
1549
|
-
insight: result.proposedInsight,
|
|
1550
|
-
tags: [],
|
|
1551
|
-
source: result.source,
|
|
1552
|
-
context: { tool: "detect", intent: "auto-capture" },
|
|
1553
|
-
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1554
|
-
confirmed: true,
|
|
1555
|
-
// --yes confirms the lesson
|
|
1556
|
-
supersedes: [],
|
|
1557
|
-
related: []
|
|
1558
|
-
};
|
|
1559
|
-
await appendLesson(repoRoot, lesson);
|
|
1560
|
-
console.log(`
|
|
1561
|
-
Saved as lesson: ${lesson.id}`);
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
);
|
|
1565
|
-
program.command("capture").description("Capture a lesson from trigger/insight or input file").option("-t, --trigger <text>", "What triggered this lesson").option("-i, --insight <text>", "The insight or lesson learned").option("--input <file>", "Path to JSON input file (alternative to trigger/insight)").option("--json", "Output result as JSON").option("-y, --yes", "Skip confirmation and save immediately").action(async function(options) {
|
|
1566
|
-
const repoRoot = getRepoRoot();
|
|
1567
|
-
const { verbose } = getGlobalOpts(this);
|
|
1568
|
-
let lesson;
|
|
1569
|
-
if (options.input) {
|
|
1570
|
-
const input = await parseInputFile(options.input);
|
|
1571
|
-
const result = await detectAndPropose(repoRoot, input);
|
|
1572
|
-
if (!result) {
|
|
1573
|
-
options.json ? console.log(JSON.stringify({ detected: false, saved: false })) : console.log("No learning trigger detected.");
|
|
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
|
+
}
|
|
1574
2223
|
return;
|
|
1575
2224
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
} else {
|
|
1580
|
-
const msg = "Provide either --trigger and --insight, or --input file.";
|
|
1581
|
-
options.json ? console.log(JSON.stringify({ error: msg, saved: false })) : out.error(msg);
|
|
1582
|
-
process.exit(1);
|
|
1583
|
-
}
|
|
1584
|
-
if (!options.yes && !process.stdin.isTTY) {
|
|
1585
|
-
if (options.json) {
|
|
1586
|
-
console.log(JSON.stringify({ error: "--yes required in non-interactive mode", saved: false }));
|
|
1587
|
-
} else {
|
|
1588
|
-
out.error("--yes required in non-interactive mode");
|
|
1589
|
-
console.log('Use: capture --trigger "..." --insight "..." --yes');
|
|
1590
|
-
}
|
|
1591
|
-
process.exit(1);
|
|
1592
|
-
}
|
|
1593
|
-
if (options.json) {
|
|
1594
|
-
if (options.yes) await appendLesson(repoRoot, lesson);
|
|
1595
|
-
outputCaptureJson(lesson, options.yes ?? false);
|
|
1596
|
-
} else if (options.yes) {
|
|
1597
|
-
await appendLesson(repoRoot, lesson);
|
|
1598
|
-
out.success(`Lesson saved: ${lesson.id}`);
|
|
1599
|
-
if (verbose) console.log(` Type: ${lesson.type} | Trigger: ${lesson.trigger}`);
|
|
1600
|
-
} else {
|
|
1601
|
-
outputCapturePreview(lesson);
|
|
1602
|
-
}
|
|
1603
|
-
});
|
|
1604
|
-
program.command("compact").description("Compact lessons: archive old lessons and remove tombstones").option("-f, --force", "Run compaction even if below threshold").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
|
|
1605
|
-
const repoRoot = getRepoRoot();
|
|
1606
|
-
const tombstones = await countTombstones(repoRoot);
|
|
1607
|
-
const needs = await needsCompaction(repoRoot);
|
|
1608
|
-
if (options.dryRun) {
|
|
1609
|
-
console.log("Dry run - no changes will be made.\n");
|
|
1610
|
-
console.log(`Tombstones found: ${tombstones}`);
|
|
1611
|
-
console.log(`Compaction needed: ${needs ? "yes" : "no"}`);
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
if (!needs && !options.force) {
|
|
1615
|
-
console.log(`Compaction not needed (${tombstones} tombstones, threshold is ${TOMBSTONE_THRESHOLD}).`);
|
|
1616
|
-
console.log("Use --force to compact anyway.");
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
console.log("Running compaction...");
|
|
1620
|
-
const result = await compact(repoRoot);
|
|
1621
|
-
console.log("\nCompaction complete:");
|
|
1622
|
-
console.log(` Archived: ${result.archived} lesson(s)`);
|
|
1623
|
-
console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
|
|
1624
|
-
console.log(` Lessons remaining: ${result.lessonsRemaining}`);
|
|
1625
|
-
await rebuildIndex(repoRoot);
|
|
1626
|
-
console.log(" Index rebuilt.");
|
|
1627
|
-
});
|
|
1628
|
-
program.command("export").description("Export lessons as JSON to stdout").option("--since <date>", "Only include lessons created after this date (ISO8601)").option("--tags <tags>", "Filter by tags (comma-separated, OR logic)").action(async (options) => {
|
|
1629
|
-
const repoRoot = getRepoRoot();
|
|
1630
|
-
const { lessons } = await readLessons(repoRoot);
|
|
1631
|
-
let filtered = lessons;
|
|
1632
|
-
if (options.since) {
|
|
1633
|
-
const sinceDate = new Date(options.since);
|
|
1634
|
-
if (Number.isNaN(sinceDate.getTime())) {
|
|
1635
|
-
console.error(`Invalid date format: ${options.since}. Use ISO8601 format (e.g., 2024-01-15).`);
|
|
1636
|
-
process.exit(1);
|
|
1637
|
-
}
|
|
1638
|
-
filtered = filtered.filter((lesson) => new Date(lesson.created) >= sinceDate);
|
|
1639
|
-
}
|
|
1640
|
-
if (options.tags) {
|
|
1641
|
-
const filterTags = options.tags.split(",").map((t) => t.trim());
|
|
1642
|
-
filtered = filtered.filter((lesson) => lesson.tags.some((tag) => filterTags.includes(tag)));
|
|
1643
|
-
}
|
|
1644
|
-
console.log(JSON.stringify(filtered, null, JSON_INDENT_SPACES));
|
|
1645
|
-
});
|
|
1646
|
-
program.command("import <file>").description("Import lessons from a JSONL file").action(async (file) => {
|
|
1647
|
-
const repoRoot = getRepoRoot();
|
|
1648
|
-
let content;
|
|
1649
|
-
try {
|
|
1650
|
-
const { readFile: readFile5 } = await import('fs/promises');
|
|
1651
|
-
content = await readFile5(file, "utf-8");
|
|
1652
|
-
} catch (err) {
|
|
1653
|
-
const code = err.code;
|
|
1654
|
-
if (code === "ENOENT") {
|
|
1655
|
-
console.error(`Error: File not found: ${file}`);
|
|
1656
|
-
} else {
|
|
1657
|
-
console.error(`Error reading file: ${err.message}`);
|
|
1658
|
-
}
|
|
1659
|
-
process.exit(1);
|
|
1660
|
-
}
|
|
1661
|
-
const { lessons: existingLessons } = await readLessons(repoRoot);
|
|
1662
|
-
const existingIds = new Set(existingLessons.map((l) => l.id));
|
|
1663
|
-
const lines = content.split("\n");
|
|
1664
|
-
let imported = 0;
|
|
1665
|
-
let skipped = 0;
|
|
1666
|
-
let invalid = 0;
|
|
1667
|
-
for (const line of lines) {
|
|
1668
|
-
const trimmed = line.trim();
|
|
1669
|
-
if (!trimmed) continue;
|
|
1670
|
-
let parsed;
|
|
1671
|
-
try {
|
|
1672
|
-
parsed = JSON.parse(trimmed);
|
|
1673
|
-
} catch {
|
|
1674
|
-
invalid++;
|
|
1675
|
-
continue;
|
|
1676
|
-
}
|
|
1677
|
-
const result = LessonSchema.safeParse(parsed);
|
|
1678
|
-
if (!result.success) {
|
|
1679
|
-
invalid++;
|
|
1680
|
-
continue;
|
|
1681
|
-
}
|
|
1682
|
-
const lesson = result.data;
|
|
1683
|
-
if (existingIds.has(lesson.id)) {
|
|
1684
|
-
skipped++;
|
|
1685
|
-
continue;
|
|
1686
|
-
}
|
|
1687
|
-
await appendLesson(repoRoot, lesson);
|
|
1688
|
-
existingIds.add(lesson.id);
|
|
1689
|
-
imported++;
|
|
1690
|
-
}
|
|
1691
|
-
const lessonWord = imported === 1 ? "lesson" : "lessons";
|
|
1692
|
-
const parts = [];
|
|
1693
|
-
if (skipped > 0) parts.push(`${skipped} skipped`);
|
|
1694
|
-
if (invalid > 0) parts.push(`${invalid} invalid`);
|
|
1695
|
-
if (parts.length > 0) {
|
|
1696
|
-
console.log(`Imported ${imported} ${lessonWord} (${parts.join(", ")})`);
|
|
1697
|
-
} else {
|
|
1698
|
-
console.log(`Imported ${imported} ${lessonWord}`);
|
|
1699
|
-
}
|
|
1700
|
-
});
|
|
1701
|
-
program.command("stats").description("Show database health and statistics").action(async () => {
|
|
1702
|
-
const repoRoot = getRepoRoot();
|
|
1703
|
-
await syncIfNeeded(repoRoot);
|
|
1704
|
-
const { lessons } = await readLessons(repoRoot);
|
|
1705
|
-
const deletedCount = await countTombstones(repoRoot);
|
|
1706
|
-
const totalLessons = lessons.length;
|
|
1707
|
-
const retrievalStats = getRetrievalStats(repoRoot);
|
|
1708
|
-
const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
|
|
1709
|
-
const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
|
|
1710
|
-
const jsonlPath = join(repoRoot, LESSONS_PATH);
|
|
1711
|
-
const dbPath = join(repoRoot, DB_PATH);
|
|
1712
|
-
let dataSize = 0;
|
|
1713
|
-
let indexSize = 0;
|
|
1714
|
-
try {
|
|
1715
|
-
dataSize = statSync(jsonlPath).size;
|
|
1716
|
-
} catch {
|
|
1717
|
-
}
|
|
1718
|
-
try {
|
|
1719
|
-
indexSize = statSync(dbPath).size;
|
|
1720
|
-
} catch {
|
|
1721
|
-
}
|
|
1722
|
-
const totalSize = dataSize + indexSize;
|
|
1723
|
-
const deletedInfo = deletedCount > 0 ? ` (${deletedCount} deleted)` : "";
|
|
1724
|
-
console.log(`Lessons: ${totalLessons} total${deletedInfo}`);
|
|
1725
|
-
console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
|
|
1726
|
-
console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
|
|
1727
|
-
});
|
|
1728
|
-
program.command("load-session").description("Load high-severity lessons for session context").option("--json", "Output as JSON").action(async function(options) {
|
|
1729
|
-
const repoRoot = getRepoRoot();
|
|
1730
|
-
const { quiet } = getGlobalOpts(this);
|
|
1731
|
-
const lessons = await loadSessionLessons(repoRoot);
|
|
1732
|
-
if (options.json) {
|
|
1733
|
-
console.log(JSON.stringify({ lessons, count: lessons.length }));
|
|
1734
|
-
return;
|
|
1735
|
-
}
|
|
1736
|
-
if (lessons.length === 0) {
|
|
1737
|
-
console.log("No high-severity lessons found.");
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
outputSessionLessonsHuman(lessons, quiet);
|
|
1741
|
-
});
|
|
1742
|
-
program.command("check-plan").description("Check plan against relevant lessons").option("--plan <text>", "Plan text to check").option("--json", "Output as JSON").option("-n, --limit <number>", "Maximum results", DEFAULT_CHECK_PLAN_LIMIT).action(async function(options) {
|
|
1743
|
-
const repoRoot = getRepoRoot();
|
|
1744
|
-
const limit = parseLimit(options.limit, "limit");
|
|
1745
|
-
const { quiet } = getGlobalOpts(this);
|
|
1746
|
-
const planText = options.plan ?? await readPlanFromStdin();
|
|
1747
|
-
if (!planText) {
|
|
1748
|
-
out.error("No plan provided. Use --plan <text> or pipe text to stdin.");
|
|
1749
|
-
process.exit(1);
|
|
1750
|
-
}
|
|
1751
|
-
if (!isModelAvailable()) {
|
|
2225
|
+
const fileExists = existsSync(settingsPath);
|
|
2226
|
+
addLearningAgentHook(settings);
|
|
2227
|
+
await writeClaudeSettings(settingsPath, settings);
|
|
1752
2228
|
if (options.json) {
|
|
1753
2229
|
console.log(JSON.stringify({
|
|
1754
|
-
|
|
1755
|
-
|
|
2230
|
+
installed: true,
|
|
2231
|
+
location: displayPath,
|
|
2232
|
+
hooks: ["SessionStart"],
|
|
2233
|
+
action: fileExists ? "updated" : "created"
|
|
1756
2234
|
}));
|
|
1757
2235
|
} else {
|
|
1758
|
-
out.
|
|
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)");
|
|
1759
2239
|
console.log("");
|
|
1760
|
-
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
|
+
}
|
|
1761
2245
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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)}`);
|
|
2258
|
+
}
|
|
1768
2259
|
return;
|
|
1769
2260
|
}
|
|
1770
|
-
if (
|
|
1771
|
-
console.log("
|
|
1772
|
-
return;
|
|
2261
|
+
if (!options.json) {
|
|
2262
|
+
console.log("Downloading embedding model...");
|
|
1773
2263
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2264
|
+
const modelPath = await resolveModel({ cli: !options.json });
|
|
2265
|
+
const size = statSync(modelPath).size;
|
|
1777
2266
|
if (options.json) {
|
|
1778
|
-
console.log(JSON.stringify({
|
|
2267
|
+
console.log(JSON.stringify({ success: true, path: modelPath, size, alreadyExisted: false }));
|
|
1779
2268
|
} else {
|
|
1780
|
-
|
|
2269
|
+
console.log(`
|
|
2270
|
+
Model downloaded successfully!`);
|
|
2271
|
+
console.log(`Path: ${modelPath}`);
|
|
2272
|
+
console.log(`Size: ${formatBytes(size)}`);
|
|
1781
2273
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// src/cli.ts
|
|
2278
|
+
var program = new Command();
|
|
2279
|
+
program.option("-v, --verbose", "Show detailed output").option("-q, --quiet", "Suppress non-essential output");
|
|
2280
|
+
program.name("learning-agent").description("Repository-scoped learning system for Claude Code").version(VERSION);
|
|
2281
|
+
registerCaptureCommands(program);
|
|
2282
|
+
registerRetrievalCommands(program);
|
|
2283
|
+
registerManagementCommands(program);
|
|
2284
|
+
registerSetupCommands(program);
|
|
1785
2285
|
program.parse();
|
|
1786
2286
|
//# sourceMappingURL=cli.js.map
|
|
1787
2287
|
//# sourceMappingURL=cli.js.map
|