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