@colbymchenry/cmem 0.2.36 → 0.5.1
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/.claude-plugin/plugin.json +17 -0
- package/.mcp.json +11 -0
- package/README.md +157 -111
- package/commands/cmem.md +8 -0
- package/dist/cli.js +2841 -852
- package/dist/cli.js.map +1 -1
- package/dist/hooks/consult.js +1002 -0
- package/dist/hooks/consult.js.map +1 -0
- package/dist/hooks/sync.js +804 -0
- package/dist/hooks/sync.js.map +1 -0
- package/dist/hooks/synthesize.js +1329 -0
- package/dist/hooks/synthesize.js.map +1 -0
- package/dist/mcp/server.js +1850 -0
- package/dist/mcp/server.js.map +1 -0
- package/hooks/hooks.json +38 -0
- package/package.json +14 -7
- package/skills/memory-search/SKILL.md +12 -0
- package/scripts/postinstall.js +0 -46
|
@@ -0,0 +1,1329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/db/index.ts
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import * as sqliteVec from "sqlite-vec";
|
|
6
|
+
import { existsSync as existsSync3 } from "fs";
|
|
7
|
+
|
|
8
|
+
// src/utils/config.ts
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { join, basename, dirname } from "path";
|
|
11
|
+
import { mkdirSync, existsSync, copyFileSync } from "fs";
|
|
12
|
+
var CMEM_DIR = join(homedir(), ".cmem");
|
|
13
|
+
var DB_PATH = join(CMEM_DIR, "sessions.db");
|
|
14
|
+
var MODELS_DIR = join(CMEM_DIR, "models");
|
|
15
|
+
var BACKUPS_DIR = join(CMEM_DIR, "backups");
|
|
16
|
+
var CLAUDE_DIR = join(homedir(), ".claude");
|
|
17
|
+
var CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
|
|
18
|
+
var CLAUDE_SESSIONS_DIR = join(CLAUDE_DIR, "sessions");
|
|
19
|
+
var EMBEDDING_MODEL = "nomic-ai/nomic-embed-text-v1.5";
|
|
20
|
+
var EMBEDDING_DIMENSIONS = 768;
|
|
21
|
+
var MAX_EMBEDDING_CHARS = 8e3;
|
|
22
|
+
var DB_SIZE_ALERT_THRESHOLD = 5 * 1024 * 1024 * 1024;
|
|
23
|
+
function ensureCmemDir() {
|
|
24
|
+
if (!existsSync(CMEM_DIR)) {
|
|
25
|
+
mkdirSync(CMEM_DIR, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function ensureModelsDir() {
|
|
29
|
+
ensureCmemDir();
|
|
30
|
+
if (!existsSync(MODELS_DIR)) {
|
|
31
|
+
mkdirSync(MODELS_DIR, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/parser/index.ts
|
|
36
|
+
import { readFileSync, readdirSync, existsSync as existsSync2, statSync } from "fs";
|
|
37
|
+
function extractSessionMetadata(filepath) {
|
|
38
|
+
const content = readFileSync(filepath, "utf-8");
|
|
39
|
+
const metadata = {
|
|
40
|
+
isSidechain: false,
|
|
41
|
+
isMeta: false
|
|
42
|
+
};
|
|
43
|
+
for (const line of content.split("\n")) {
|
|
44
|
+
if (!line.trim()) continue;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(line);
|
|
47
|
+
if (parsed.type === "user" && parsed.message) {
|
|
48
|
+
if (parsed.isSidechain === true) {
|
|
49
|
+
metadata.isSidechain = true;
|
|
50
|
+
}
|
|
51
|
+
if (parsed.agentId) {
|
|
52
|
+
metadata.isSidechain = true;
|
|
53
|
+
}
|
|
54
|
+
if (parsed.isMeta === true) {
|
|
55
|
+
metadata.isMeta = true;
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return metadata;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/db/index.ts
|
|
66
|
+
var db = null;
|
|
67
|
+
function getDatabase() {
|
|
68
|
+
if (db) return db;
|
|
69
|
+
ensureCmemDir();
|
|
70
|
+
db = new Database(DB_PATH);
|
|
71
|
+
db.pragma("journal_mode = WAL");
|
|
72
|
+
sqliteVec.load(db);
|
|
73
|
+
initSchema(db);
|
|
74
|
+
return db;
|
|
75
|
+
}
|
|
76
|
+
function initSchema(database) {
|
|
77
|
+
database.exec(`
|
|
78
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
79
|
+
name TEXT PRIMARY KEY,
|
|
80
|
+
applied_at TEXT NOT NULL
|
|
81
|
+
);
|
|
82
|
+
`);
|
|
83
|
+
database.exec(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
85
|
+
id TEXT PRIMARY KEY,
|
|
86
|
+
title TEXT NOT NULL,
|
|
87
|
+
summary TEXT,
|
|
88
|
+
created_at TEXT NOT NULL,
|
|
89
|
+
updated_at TEXT NOT NULL,
|
|
90
|
+
message_count INTEGER DEFAULT 0,
|
|
91
|
+
project_path TEXT,
|
|
92
|
+
source_file TEXT,
|
|
93
|
+
raw_data TEXT NOT NULL
|
|
94
|
+
);
|
|
95
|
+
`);
|
|
96
|
+
try {
|
|
97
|
+
database.exec(`ALTER TABLE sessions ADD COLUMN source_file TEXT`);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
database.exec(`ALTER TABLE sessions ADD COLUMN is_sidechain INTEGER DEFAULT 0`);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
database.exec(`ALTER TABLE sessions ADD COLUMN is_automated INTEGER DEFAULT 0`);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
database.exec(`ALTER TABLE sessions ADD COLUMN custom_title TEXT`);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
database.exec(`
|
|
113
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
114
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
115
|
+
session_id TEXT NOT NULL,
|
|
116
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
117
|
+
content TEXT NOT NULL,
|
|
118
|
+
timestamp TEXT NOT NULL,
|
|
119
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
120
|
+
);
|
|
121
|
+
`);
|
|
122
|
+
database.exec(`
|
|
123
|
+
CREATE TABLE IF NOT EXISTS embedding_state (
|
|
124
|
+
session_id TEXT PRIMARY KEY,
|
|
125
|
+
content_length INTEGER NOT NULL,
|
|
126
|
+
file_mtime TEXT,
|
|
127
|
+
last_embedded_at TEXT NOT NULL,
|
|
128
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
129
|
+
);
|
|
130
|
+
`);
|
|
131
|
+
database.exec(`
|
|
132
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS session_embeddings USING vec0(
|
|
133
|
+
session_id TEXT PRIMARY KEY,
|
|
134
|
+
embedding FLOAT[${EMBEDDING_DIMENSIONS}]
|
|
135
|
+
);
|
|
136
|
+
`);
|
|
137
|
+
database.exec(`
|
|
138
|
+
CREATE TABLE IF NOT EXISTS favorites (
|
|
139
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
140
|
+
type TEXT NOT NULL CHECK (type IN ('session', 'folder')),
|
|
141
|
+
value TEXT NOT NULL,
|
|
142
|
+
created_at TEXT NOT NULL,
|
|
143
|
+
UNIQUE(type, value)
|
|
144
|
+
);
|
|
145
|
+
`);
|
|
146
|
+
database.exec(`
|
|
147
|
+
CREATE TABLE IF NOT EXISTS project_order (
|
|
148
|
+
path TEXT PRIMARY KEY,
|
|
149
|
+
sort_order INTEGER NOT NULL,
|
|
150
|
+
updated_at TEXT NOT NULL
|
|
151
|
+
);
|
|
152
|
+
`);
|
|
153
|
+
database.exec(`
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
155
|
+
`);
|
|
156
|
+
database.exec(`
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC);
|
|
158
|
+
`);
|
|
159
|
+
database.exec(`
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source_file);
|
|
161
|
+
`);
|
|
162
|
+
database.exec(`
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_favorites_type ON favorites(type);
|
|
164
|
+
`);
|
|
165
|
+
database.exec(`
|
|
166
|
+
CREATE TABLE IF NOT EXISTS lessons (
|
|
167
|
+
id TEXT PRIMARY KEY,
|
|
168
|
+
project_path TEXT NOT NULL,
|
|
169
|
+
category TEXT NOT NULL CHECK (category IN (
|
|
170
|
+
'architecture_decision', 'anti_pattern', 'bug_pattern',
|
|
171
|
+
'project_convention', 'dependency_knowledge', 'domain_knowledge',
|
|
172
|
+
'workflow', 'other'
|
|
173
|
+
)),
|
|
174
|
+
title TEXT NOT NULL,
|
|
175
|
+
trigger_context TEXT NOT NULL,
|
|
176
|
+
insight TEXT NOT NULL,
|
|
177
|
+
reasoning TEXT,
|
|
178
|
+
confidence REAL DEFAULT 0.5,
|
|
179
|
+
times_applied INTEGER DEFAULT 0,
|
|
180
|
+
times_validated INTEGER DEFAULT 0,
|
|
181
|
+
times_rejected INTEGER DEFAULT 0,
|
|
182
|
+
source_session_id TEXT,
|
|
183
|
+
source_type TEXT DEFAULT 'synthesized',
|
|
184
|
+
archived INTEGER DEFAULT 0,
|
|
185
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
186
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
187
|
+
last_applied_at TEXT
|
|
188
|
+
);
|
|
189
|
+
`);
|
|
190
|
+
database.exec(`
|
|
191
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS lesson_embeddings USING vec0(
|
|
192
|
+
lesson_id TEXT PRIMARY KEY,
|
|
193
|
+
embedding FLOAT[${EMBEDDING_DIMENSIONS}]
|
|
194
|
+
);
|
|
195
|
+
`);
|
|
196
|
+
database.exec(`
|
|
197
|
+
CREATE TABLE IF NOT EXISTS lesson_feedback (
|
|
198
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
199
|
+
lesson_id TEXT NOT NULL,
|
|
200
|
+
session_id TEXT,
|
|
201
|
+
feedback_type TEXT NOT NULL CHECK (feedback_type IN (
|
|
202
|
+
'validated', 'rejected', 'modified'
|
|
203
|
+
)),
|
|
204
|
+
comment TEXT,
|
|
205
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
206
|
+
FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE
|
|
207
|
+
);
|
|
208
|
+
`);
|
|
209
|
+
database.exec(`
|
|
210
|
+
CREATE TABLE IF NOT EXISTS synthesis_queue (
|
|
211
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
212
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
213
|
+
project_path TEXT NOT NULL,
|
|
214
|
+
queued_at TEXT DEFAULT (datetime('now')),
|
|
215
|
+
status TEXT DEFAULT 'pending',
|
|
216
|
+
processed_at TEXT,
|
|
217
|
+
lessons_created INTEGER DEFAULT 0,
|
|
218
|
+
error TEXT
|
|
219
|
+
);
|
|
220
|
+
`);
|
|
221
|
+
database.exec(`
|
|
222
|
+
CREATE TABLE IF NOT EXISTS session_injections (
|
|
223
|
+
session_id TEXT NOT NULL,
|
|
224
|
+
lesson_id TEXT NOT NULL,
|
|
225
|
+
injected_at TEXT DEFAULT (datetime('now')),
|
|
226
|
+
PRIMARY KEY (session_id, lesson_id)
|
|
227
|
+
);
|
|
228
|
+
`);
|
|
229
|
+
database.exec(`
|
|
230
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_project ON lessons(project_path);
|
|
231
|
+
`);
|
|
232
|
+
database.exec(`
|
|
233
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_category ON lessons(category);
|
|
234
|
+
`);
|
|
235
|
+
database.exec(`
|
|
236
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_confidence ON lessons(confidence DESC);
|
|
237
|
+
`);
|
|
238
|
+
database.exec(`
|
|
239
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_archived ON lessons(archived);
|
|
240
|
+
`);
|
|
241
|
+
database.exec(`
|
|
242
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_queue_status ON synthesis_queue(status);
|
|
243
|
+
`);
|
|
244
|
+
database.exec(`
|
|
245
|
+
CREATE INDEX IF NOT EXISTS idx_session_injections_session ON session_injections(session_id);
|
|
246
|
+
`);
|
|
247
|
+
runMigrations(database);
|
|
248
|
+
}
|
|
249
|
+
function runMigrations(database) {
|
|
250
|
+
const migrationName = "populate_session_metadata_v1";
|
|
251
|
+
const existing = database.prepare(
|
|
252
|
+
"SELECT 1 FROM migrations WHERE name = ?"
|
|
253
|
+
).get(migrationName);
|
|
254
|
+
if (existing) return;
|
|
255
|
+
const sessions = database.prepare(`
|
|
256
|
+
SELECT id, source_file FROM sessions WHERE source_file IS NOT NULL
|
|
257
|
+
`).all();
|
|
258
|
+
const updateStmt = database.prepare(`
|
|
259
|
+
UPDATE sessions SET is_sidechain = ?, is_automated = ? WHERE id = ?
|
|
260
|
+
`);
|
|
261
|
+
const transaction = database.transaction(() => {
|
|
262
|
+
for (const session of sessions) {
|
|
263
|
+
if (!existsSync3(session.source_file)) continue;
|
|
264
|
+
try {
|
|
265
|
+
const metadata = extractSessionMetadata(session.source_file);
|
|
266
|
+
const isAutomated = metadata.isSidechain || metadata.isMeta;
|
|
267
|
+
updateStmt.run(
|
|
268
|
+
metadata.isSidechain ? 1 : 0,
|
|
269
|
+
isAutomated ? 1 : 0,
|
|
270
|
+
session.id
|
|
271
|
+
);
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
database.prepare(
|
|
276
|
+
"INSERT INTO migrations (name, applied_at) VALUES (?, ?)"
|
|
277
|
+
).run(migrationName, (/* @__PURE__ */ new Date()).toISOString());
|
|
278
|
+
});
|
|
279
|
+
transaction();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/db/sessions.ts
|
|
283
|
+
function getSession(id) {
|
|
284
|
+
const db2 = getDatabase();
|
|
285
|
+
const row = db2.prepare(`
|
|
286
|
+
SELECT id, title, custom_title as customTitle, summary,
|
|
287
|
+
created_at as createdAt, updated_at as updatedAt,
|
|
288
|
+
message_count as messageCount, project_path as projectPath,
|
|
289
|
+
source_file as sourceFile, raw_data as rawData,
|
|
290
|
+
is_sidechain as isSidechain, is_automated as isAutomated
|
|
291
|
+
FROM sessions WHERE id = ?
|
|
292
|
+
`).get(id);
|
|
293
|
+
if (!row) return null;
|
|
294
|
+
return mapSessionRow(row);
|
|
295
|
+
}
|
|
296
|
+
function getSessionMessages(sessionId) {
|
|
297
|
+
const db2 = getDatabase();
|
|
298
|
+
const rows = db2.prepare(`
|
|
299
|
+
SELECT id, session_id as sessionId, role, content, timestamp
|
|
300
|
+
FROM messages WHERE session_id = ?
|
|
301
|
+
ORDER BY timestamp ASC
|
|
302
|
+
`).all(sessionId);
|
|
303
|
+
return rows;
|
|
304
|
+
}
|
|
305
|
+
function mapSessionRow(row) {
|
|
306
|
+
return {
|
|
307
|
+
...row,
|
|
308
|
+
customTitle: row.customTitle,
|
|
309
|
+
isSidechain: row.isSidechain === 1,
|
|
310
|
+
isAutomated: row.isAutomated === 1
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/db/lessons.ts
|
|
315
|
+
import { randomUUID } from "crypto";
|
|
316
|
+
function mapLessonRow(row) {
|
|
317
|
+
return {
|
|
318
|
+
id: row.id,
|
|
319
|
+
projectPath: row.project_path,
|
|
320
|
+
category: row.category,
|
|
321
|
+
title: row.title,
|
|
322
|
+
triggerContext: row.trigger_context,
|
|
323
|
+
insight: row.insight,
|
|
324
|
+
reasoning: row.reasoning ?? void 0,
|
|
325
|
+
confidence: row.confidence,
|
|
326
|
+
timesApplied: row.times_applied,
|
|
327
|
+
timesValidated: row.times_validated,
|
|
328
|
+
timesRejected: row.times_rejected,
|
|
329
|
+
sourceSessionId: row.source_session_id ?? void 0,
|
|
330
|
+
sourceType: row.source_type,
|
|
331
|
+
archived: row.archived === 1,
|
|
332
|
+
createdAt: row.created_at,
|
|
333
|
+
updatedAt: row.updated_at,
|
|
334
|
+
lastAppliedAt: row.last_applied_at ?? void 0
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function createLesson(input) {
|
|
338
|
+
const db2 = getDatabase();
|
|
339
|
+
const id = randomUUID();
|
|
340
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
341
|
+
db2.prepare(`
|
|
342
|
+
INSERT INTO lessons (
|
|
343
|
+
id, project_path, category, title, trigger_context, insight,
|
|
344
|
+
reasoning, confidence, source_session_id, source_type,
|
|
345
|
+
created_at, updated_at
|
|
346
|
+
)
|
|
347
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
348
|
+
`).run(
|
|
349
|
+
id,
|
|
350
|
+
input.projectPath,
|
|
351
|
+
input.category,
|
|
352
|
+
input.title,
|
|
353
|
+
input.triggerContext,
|
|
354
|
+
input.insight,
|
|
355
|
+
input.reasoning ?? null,
|
|
356
|
+
input.confidence ?? 0.5,
|
|
357
|
+
input.sourceSessionId ?? null,
|
|
358
|
+
input.sourceType ?? "synthesized",
|
|
359
|
+
now,
|
|
360
|
+
now
|
|
361
|
+
);
|
|
362
|
+
return getLesson(id);
|
|
363
|
+
}
|
|
364
|
+
function updateLesson(id, updates) {
|
|
365
|
+
const db2 = getDatabase();
|
|
366
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
367
|
+
const fields = ["updated_at = ?"];
|
|
368
|
+
const values = [now];
|
|
369
|
+
if (updates.category !== void 0) {
|
|
370
|
+
fields.push("category = ?");
|
|
371
|
+
values.push(updates.category);
|
|
372
|
+
}
|
|
373
|
+
if (updates.title !== void 0) {
|
|
374
|
+
fields.push("title = ?");
|
|
375
|
+
values.push(updates.title);
|
|
376
|
+
}
|
|
377
|
+
if (updates.triggerContext !== void 0) {
|
|
378
|
+
fields.push("trigger_context = ?");
|
|
379
|
+
values.push(updates.triggerContext);
|
|
380
|
+
}
|
|
381
|
+
if (updates.insight !== void 0) {
|
|
382
|
+
fields.push("insight = ?");
|
|
383
|
+
values.push(updates.insight);
|
|
384
|
+
}
|
|
385
|
+
if (updates.reasoning !== void 0) {
|
|
386
|
+
fields.push("reasoning = ?");
|
|
387
|
+
values.push(updates.reasoning);
|
|
388
|
+
}
|
|
389
|
+
if (updates.confidence !== void 0) {
|
|
390
|
+
fields.push("confidence = ?");
|
|
391
|
+
values.push(updates.confidence);
|
|
392
|
+
}
|
|
393
|
+
if (updates.archived !== void 0) {
|
|
394
|
+
fields.push("archived = ?");
|
|
395
|
+
values.push(updates.archived ? 1 : 0);
|
|
396
|
+
}
|
|
397
|
+
values.push(id);
|
|
398
|
+
db2.prepare(`
|
|
399
|
+
UPDATE lessons SET ${fields.join(", ")} WHERE id = ?
|
|
400
|
+
`).run(...values);
|
|
401
|
+
return getLesson(id);
|
|
402
|
+
}
|
|
403
|
+
function deleteLesson(id) {
|
|
404
|
+
const db2 = getDatabase();
|
|
405
|
+
const transaction = db2.transaction(() => {
|
|
406
|
+
db2.prepare("DELETE FROM lesson_embeddings WHERE lesson_id = ?").run(id);
|
|
407
|
+
db2.prepare("DELETE FROM lesson_feedback WHERE lesson_id = ?").run(id);
|
|
408
|
+
const result = db2.prepare("DELETE FROM lessons WHERE id = ?").run(id);
|
|
409
|
+
return result.changes > 0;
|
|
410
|
+
});
|
|
411
|
+
return transaction();
|
|
412
|
+
}
|
|
413
|
+
function archiveLesson(id) {
|
|
414
|
+
const db2 = getDatabase();
|
|
415
|
+
db2.prepare(`
|
|
416
|
+
UPDATE lessons SET archived = 1, updated_at = ? WHERE id = ?
|
|
417
|
+
`).run((/* @__PURE__ */ new Date()).toISOString(), id);
|
|
418
|
+
}
|
|
419
|
+
function unarchiveLesson(id) {
|
|
420
|
+
const db2 = getDatabase();
|
|
421
|
+
db2.prepare(`
|
|
422
|
+
UPDATE lessons SET archived = 0, updated_at = ? WHERE id = ?
|
|
423
|
+
`).run((/* @__PURE__ */ new Date()).toISOString(), id);
|
|
424
|
+
}
|
|
425
|
+
function getLesson(id) {
|
|
426
|
+
const db2 = getDatabase();
|
|
427
|
+
const row = db2.prepare(`
|
|
428
|
+
SELECT * FROM lessons WHERE id = ?
|
|
429
|
+
`).get(id);
|
|
430
|
+
return row ? mapLessonRow(row) : null;
|
|
431
|
+
}
|
|
432
|
+
function getLessonsByProject(projectPath, options = {}) {
|
|
433
|
+
const db2 = getDatabase();
|
|
434
|
+
let query = "SELECT * FROM lessons WHERE project_path = ?";
|
|
435
|
+
const params = [projectPath];
|
|
436
|
+
if (options.category) {
|
|
437
|
+
query += " AND category = ?";
|
|
438
|
+
params.push(options.category);
|
|
439
|
+
}
|
|
440
|
+
if (options.archived !== void 0) {
|
|
441
|
+
query += " AND archived = ?";
|
|
442
|
+
params.push(options.archived ? 1 : 0);
|
|
443
|
+
} else {
|
|
444
|
+
query += " AND archived = 0";
|
|
445
|
+
}
|
|
446
|
+
if (options.minConfidence !== void 0) {
|
|
447
|
+
query += " AND confidence >= ?";
|
|
448
|
+
params.push(options.minConfidence);
|
|
449
|
+
}
|
|
450
|
+
query += " ORDER BY confidence DESC, times_applied DESC";
|
|
451
|
+
if (options.limit) {
|
|
452
|
+
query += " LIMIT ?";
|
|
453
|
+
params.push(options.limit);
|
|
454
|
+
}
|
|
455
|
+
const rows = db2.prepare(query).all(...params);
|
|
456
|
+
return rows.map(mapLessonRow);
|
|
457
|
+
}
|
|
458
|
+
function getCoreLessons(projectPath, limit = 3) {
|
|
459
|
+
const db2 = getDatabase();
|
|
460
|
+
const rows = db2.prepare(`
|
|
461
|
+
SELECT * FROM lessons
|
|
462
|
+
WHERE project_path = ?
|
|
463
|
+
AND archived = 0
|
|
464
|
+
AND confidence >= 0.7
|
|
465
|
+
ORDER BY
|
|
466
|
+
times_validated DESC,
|
|
467
|
+
confidence DESC,
|
|
468
|
+
times_applied DESC
|
|
469
|
+
LIMIT ?
|
|
470
|
+
`).all(projectPath, limit);
|
|
471
|
+
return rows.map(mapLessonRow);
|
|
472
|
+
}
|
|
473
|
+
function getAllLessons(options = {}) {
|
|
474
|
+
const db2 = getDatabase();
|
|
475
|
+
let query = "SELECT * FROM lessons WHERE 1=1";
|
|
476
|
+
const params = [];
|
|
477
|
+
if (options.category) {
|
|
478
|
+
query += " AND category = ?";
|
|
479
|
+
params.push(options.category);
|
|
480
|
+
}
|
|
481
|
+
if (options.archived !== void 0) {
|
|
482
|
+
query += " AND archived = ?";
|
|
483
|
+
params.push(options.archived ? 1 : 0);
|
|
484
|
+
}
|
|
485
|
+
if (options.minConfidence !== void 0) {
|
|
486
|
+
query += " AND confidence >= ?";
|
|
487
|
+
params.push(options.minConfidence);
|
|
488
|
+
}
|
|
489
|
+
query += " ORDER BY updated_at DESC";
|
|
490
|
+
if (options.limit) {
|
|
491
|
+
query += " LIMIT ?";
|
|
492
|
+
params.push(options.limit);
|
|
493
|
+
}
|
|
494
|
+
const rows = db2.prepare(query).all(...params);
|
|
495
|
+
return rows.map(mapLessonRow);
|
|
496
|
+
}
|
|
497
|
+
function recordLessonApplication(id) {
|
|
498
|
+
const db2 = getDatabase();
|
|
499
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
500
|
+
db2.prepare(`
|
|
501
|
+
UPDATE lessons
|
|
502
|
+
SET times_applied = times_applied + 1,
|
|
503
|
+
last_applied_at = ?,
|
|
504
|
+
updated_at = ?
|
|
505
|
+
WHERE id = ?
|
|
506
|
+
`).run(now, now, id);
|
|
507
|
+
}
|
|
508
|
+
function recordLessonValidation(id, sessionId, comment) {
|
|
509
|
+
const db2 = getDatabase();
|
|
510
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
511
|
+
const transaction = db2.transaction(() => {
|
|
512
|
+
db2.prepare(`
|
|
513
|
+
UPDATE lessons
|
|
514
|
+
SET times_validated = times_validated + 1, updated_at = ?
|
|
515
|
+
WHERE id = ?
|
|
516
|
+
`).run(now, id);
|
|
517
|
+
db2.prepare(`
|
|
518
|
+
INSERT INTO lesson_feedback (lesson_id, session_id, feedback_type, comment)
|
|
519
|
+
VALUES (?, ?, 'validated', ?)
|
|
520
|
+
`).run(id, sessionId ?? null, comment ?? null);
|
|
521
|
+
});
|
|
522
|
+
transaction();
|
|
523
|
+
}
|
|
524
|
+
function recordLessonRejection(id, sessionId, comment) {
|
|
525
|
+
const db2 = getDatabase();
|
|
526
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
527
|
+
const transaction = db2.transaction(() => {
|
|
528
|
+
db2.prepare(`
|
|
529
|
+
UPDATE lessons
|
|
530
|
+
SET times_rejected = times_rejected + 1, updated_at = ?
|
|
531
|
+
WHERE id = ?
|
|
532
|
+
`).run(now, id);
|
|
533
|
+
db2.prepare(`
|
|
534
|
+
INSERT INTO lesson_feedback (lesson_id, session_id, feedback_type, comment)
|
|
535
|
+
VALUES (?, ?, 'rejected', ?)
|
|
536
|
+
`).run(id, sessionId ?? null, comment ?? null);
|
|
537
|
+
});
|
|
538
|
+
transaction();
|
|
539
|
+
}
|
|
540
|
+
function storeLessonEmbedding(lessonId, embedding) {
|
|
541
|
+
const db2 = getDatabase();
|
|
542
|
+
db2.prepare("DELETE FROM lesson_embeddings WHERE lesson_id = ?").run(lessonId);
|
|
543
|
+
db2.prepare(`
|
|
544
|
+
INSERT INTO lesson_embeddings (lesson_id, embedding)
|
|
545
|
+
VALUES (?, ?)
|
|
546
|
+
`).run(lessonId, JSON.stringify(embedding));
|
|
547
|
+
}
|
|
548
|
+
function searchLessonsByEmbedding(embedding, projectPath, limit = 5) {
|
|
549
|
+
return searchLessonsByEmbeddingWithDistance(embedding, projectPath, limit).map((r) => r.lesson);
|
|
550
|
+
}
|
|
551
|
+
function searchLessonsByEmbeddingWithDistance(embedding, projectPath, limit = 5) {
|
|
552
|
+
const db2 = getDatabase();
|
|
553
|
+
const rows = db2.prepare(`
|
|
554
|
+
SELECT
|
|
555
|
+
l.*,
|
|
556
|
+
le.distance
|
|
557
|
+
FROM lesson_embeddings le
|
|
558
|
+
JOIN lessons l ON l.id = le.lesson_id
|
|
559
|
+
WHERE l.project_path = ?
|
|
560
|
+
AND l.archived = 0
|
|
561
|
+
AND le.embedding MATCH ?
|
|
562
|
+
ORDER BY le.distance ASC
|
|
563
|
+
LIMIT ?
|
|
564
|
+
`).all(projectPath, JSON.stringify(embedding), limit);
|
|
565
|
+
return rows.map((row) => ({
|
|
566
|
+
lesson: mapLessonRow(row),
|
|
567
|
+
distance: row.distance
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
function getPendingSynthesis(limit = 5) {
|
|
571
|
+
const db2 = getDatabase();
|
|
572
|
+
const rows = db2.prepare(`
|
|
573
|
+
SELECT
|
|
574
|
+
id,
|
|
575
|
+
session_id as sessionId,
|
|
576
|
+
project_path as projectPath,
|
|
577
|
+
queued_at as queuedAt,
|
|
578
|
+
status,
|
|
579
|
+
processed_at as processedAt,
|
|
580
|
+
lessons_created as lessonsCreated,
|
|
581
|
+
error
|
|
582
|
+
FROM synthesis_queue
|
|
583
|
+
WHERE status = 'pending'
|
|
584
|
+
ORDER BY queued_at ASC
|
|
585
|
+
LIMIT ?
|
|
586
|
+
`).all(limit);
|
|
587
|
+
return rows;
|
|
588
|
+
}
|
|
589
|
+
function markSynthesisProcessing(id) {
|
|
590
|
+
const db2 = getDatabase();
|
|
591
|
+
db2.prepare(`
|
|
592
|
+
UPDATE synthesis_queue SET status = 'processing' WHERE id = ?
|
|
593
|
+
`).run(id);
|
|
594
|
+
}
|
|
595
|
+
function markSynthesisComplete(id, lessonsCreated) {
|
|
596
|
+
const db2 = getDatabase();
|
|
597
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
598
|
+
db2.prepare(`
|
|
599
|
+
UPDATE synthesis_queue
|
|
600
|
+
SET status = 'completed', processed_at = ?, lessons_created = ?
|
|
601
|
+
WHERE id = ?
|
|
602
|
+
`).run(now, lessonsCreated, id);
|
|
603
|
+
}
|
|
604
|
+
function markSynthesisFailed(id, error) {
|
|
605
|
+
const db2 = getDatabase();
|
|
606
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
607
|
+
db2.prepare(`
|
|
608
|
+
UPDATE synthesis_queue
|
|
609
|
+
SET status = 'failed', processed_at = ?, error = ?
|
|
610
|
+
WHERE id = ?
|
|
611
|
+
`).run(now, error, id);
|
|
612
|
+
}
|
|
613
|
+
function getSynthesisStats() {
|
|
614
|
+
const db2 = getDatabase();
|
|
615
|
+
const pending = db2.prepare(`
|
|
616
|
+
SELECT COUNT(*) as count FROM synthesis_queue WHERE status = 'pending'
|
|
617
|
+
`).get().count;
|
|
618
|
+
const processing = db2.prepare(`
|
|
619
|
+
SELECT COUNT(*) as count FROM synthesis_queue WHERE status = 'processing'
|
|
620
|
+
`).get().count;
|
|
621
|
+
const completed = db2.prepare(`
|
|
622
|
+
SELECT COUNT(*) as count FROM synthesis_queue WHERE status = 'completed'
|
|
623
|
+
`).get().count;
|
|
624
|
+
const failed = db2.prepare(`
|
|
625
|
+
SELECT COUNT(*) as count FROM synthesis_queue WHERE status = 'failed'
|
|
626
|
+
`).get().count;
|
|
627
|
+
const totalLessonsCreated = db2.prepare(`
|
|
628
|
+
SELECT COALESCE(SUM(lessons_created), 0) as total FROM synthesis_queue WHERE status = 'completed'
|
|
629
|
+
`).get().total;
|
|
630
|
+
return { pending, processing, completed, failed, totalLessonsCreated };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/utils/claude-cli.ts
|
|
634
|
+
import { spawn } from "child_process";
|
|
635
|
+
import { execSync } from "child_process";
|
|
636
|
+
function getClaudePath() {
|
|
637
|
+
try {
|
|
638
|
+
const result = execSync("which claude", { encoding: "utf-8" }).trim();
|
|
639
|
+
return result || null;
|
|
640
|
+
} catch {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function parseStreamJsonLine(line) {
|
|
645
|
+
if (!line.trim()) return null;
|
|
646
|
+
try {
|
|
647
|
+
const data = JSON.parse(line);
|
|
648
|
+
if (data.type === "assistant") {
|
|
649
|
+
if (data.message?.content && Array.isArray(data.message.content)) {
|
|
650
|
+
const textBlocks = [];
|
|
651
|
+
for (const block of data.message.content) {
|
|
652
|
+
if (block.type === "text" && block.text) {
|
|
653
|
+
textBlocks.push(block.text);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (textBlocks.length > 0) {
|
|
657
|
+
return { type: "text", content: textBlocks.join("\n") };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
} else if (data.type === "content_block_delta") {
|
|
661
|
+
if (data.delta?.type === "text_delta" && data.delta.text) {
|
|
662
|
+
return { type: "text", content: data.delta.text };
|
|
663
|
+
}
|
|
664
|
+
} else if (data.type === "result") {
|
|
665
|
+
if (data.is_error) {
|
|
666
|
+
return { type: "error", content: data.result || "Unknown error" };
|
|
667
|
+
}
|
|
668
|
+
const usage = data.usage || {};
|
|
669
|
+
const inputTokens = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
670
|
+
const outputTokens = usage.output_tokens || 0;
|
|
671
|
+
return {
|
|
672
|
+
type: "done",
|
|
673
|
+
inputTokens,
|
|
674
|
+
outputTokens,
|
|
675
|
+
costUsd: data.total_cost_usd,
|
|
676
|
+
durationMs: data.duration_ms
|
|
677
|
+
};
|
|
678
|
+
} else if (data.type === "error") {
|
|
679
|
+
return {
|
|
680
|
+
type: "error",
|
|
681
|
+
content: data.error?.message || JSON.stringify(data.error) || "Unknown error"
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
return { type: "skip" };
|
|
685
|
+
} catch {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async function runClaudePrompt(prompt, options = {}) {
|
|
690
|
+
const claudePath = getClaudePath();
|
|
691
|
+
if (!claudePath) {
|
|
692
|
+
return {
|
|
693
|
+
success: false,
|
|
694
|
+
content: "",
|
|
695
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
const { model = "haiku" } = options;
|
|
699
|
+
const args = [
|
|
700
|
+
"-p",
|
|
701
|
+
prompt,
|
|
702
|
+
"--output-format",
|
|
703
|
+
"stream-json",
|
|
704
|
+
"--verbose",
|
|
705
|
+
// Required when using stream-json with -p
|
|
706
|
+
"--model",
|
|
707
|
+
model,
|
|
708
|
+
"--permission-mode",
|
|
709
|
+
"plan"
|
|
710
|
+
// Read-only, no tools needed for summarization
|
|
711
|
+
];
|
|
712
|
+
return new Promise((resolve) => {
|
|
713
|
+
const childProcess = spawn(claudePath, args, {
|
|
714
|
+
env: {
|
|
715
|
+
...process.env,
|
|
716
|
+
CI: "true"
|
|
717
|
+
// Prevent interactive prompts
|
|
718
|
+
},
|
|
719
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
720
|
+
});
|
|
721
|
+
childProcess.stdin?.end();
|
|
722
|
+
let buffer = "";
|
|
723
|
+
const textChunks = [];
|
|
724
|
+
let finalResult = null;
|
|
725
|
+
let errorContent = "";
|
|
726
|
+
childProcess.stdout?.on("data", (data) => {
|
|
727
|
+
buffer += data.toString();
|
|
728
|
+
const lines = buffer.split("\n");
|
|
729
|
+
buffer = lines.pop() || "";
|
|
730
|
+
for (const line of lines) {
|
|
731
|
+
if (!line.trim()) continue;
|
|
732
|
+
const chunk = parseStreamJsonLine(line);
|
|
733
|
+
if (chunk) {
|
|
734
|
+
if (chunk.type === "text" && chunk.content) {
|
|
735
|
+
textChunks.push(chunk.content);
|
|
736
|
+
} else if (chunk.type === "done") {
|
|
737
|
+
finalResult = chunk;
|
|
738
|
+
} else if (chunk.type === "error" && chunk.content) {
|
|
739
|
+
errorContent = chunk.content;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
childProcess.stderr?.on("data", (data) => {
|
|
745
|
+
const text = data.toString().toLowerCase();
|
|
746
|
+
if (text.includes("error") || text.includes("failed")) {
|
|
747
|
+
errorContent = data.toString().trim();
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
childProcess.on("close", (code) => {
|
|
751
|
+
if (buffer.trim()) {
|
|
752
|
+
const chunk = parseStreamJsonLine(buffer);
|
|
753
|
+
if (chunk) {
|
|
754
|
+
if (chunk.type === "text" && chunk.content) {
|
|
755
|
+
textChunks.push(chunk.content);
|
|
756
|
+
} else if (chunk.type === "done") {
|
|
757
|
+
finalResult = chunk;
|
|
758
|
+
} else if (chunk.type === "error" && chunk.content) {
|
|
759
|
+
errorContent = chunk.content;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const content = textChunks.join("");
|
|
764
|
+
if (errorContent && !content) {
|
|
765
|
+
resolve({
|
|
766
|
+
success: false,
|
|
767
|
+
content: "",
|
|
768
|
+
error: errorContent
|
|
769
|
+
});
|
|
770
|
+
} else {
|
|
771
|
+
resolve({
|
|
772
|
+
success: code === 0 && content.length > 0,
|
|
773
|
+
content,
|
|
774
|
+
error: errorContent || void 0,
|
|
775
|
+
inputTokens: finalResult?.inputTokens,
|
|
776
|
+
outputTokens: finalResult?.outputTokens,
|
|
777
|
+
costUsd: finalResult?.costUsd,
|
|
778
|
+
durationMs: finalResult?.durationMs
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
childProcess.on("error", (err) => {
|
|
783
|
+
resolve({
|
|
784
|
+
success: false,
|
|
785
|
+
content: "",
|
|
786
|
+
error: err.message
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
setTimeout(() => {
|
|
790
|
+
childProcess.kill("SIGTERM");
|
|
791
|
+
resolve({
|
|
792
|
+
success: false,
|
|
793
|
+
content: textChunks.join(""),
|
|
794
|
+
error: "Request timed out after 60 seconds"
|
|
795
|
+
});
|
|
796
|
+
}, 6e4);
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
function isClaudeCliAvailable() {
|
|
800
|
+
return getClaudePath() !== null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/embeddings/index.ts
|
|
804
|
+
import { existsSync as existsSync4 } from "fs";
|
|
805
|
+
import { join as join2 } from "path";
|
|
806
|
+
var transformersModule = null;
|
|
807
|
+
var pipeline = null;
|
|
808
|
+
var initialized = false;
|
|
809
|
+
async function getTransformers() {
|
|
810
|
+
if (!transformersModule) {
|
|
811
|
+
transformersModule = await import("@xenova/transformers");
|
|
812
|
+
}
|
|
813
|
+
return transformersModule;
|
|
814
|
+
}
|
|
815
|
+
function isModelCached() {
|
|
816
|
+
const modelCachePath = join2(MODELS_DIR, EMBEDDING_MODEL);
|
|
817
|
+
return existsSync4(modelCachePath);
|
|
818
|
+
}
|
|
819
|
+
async function initializeEmbeddings(onProgress) {
|
|
820
|
+
if (initialized && pipeline) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
ensureModelsDir();
|
|
824
|
+
const { pipeline: createPipeline, env } = await getTransformers();
|
|
825
|
+
env.cacheDir = MODELS_DIR;
|
|
826
|
+
const cached = isModelCached();
|
|
827
|
+
if (cached) {
|
|
828
|
+
env.allowRemoteModels = false;
|
|
829
|
+
onProgress?.({ status: "loading" });
|
|
830
|
+
} else {
|
|
831
|
+
onProgress?.({ status: "downloading" });
|
|
832
|
+
}
|
|
833
|
+
pipeline = await createPipeline("feature-extraction", EMBEDDING_MODEL, {
|
|
834
|
+
progress_callback: onProgress ? (progress) => {
|
|
835
|
+
if (progress.status === "progress" && progress.file && progress.progress !== void 0) {
|
|
836
|
+
onProgress({
|
|
837
|
+
status: "downloading",
|
|
838
|
+
file: progress.file,
|
|
839
|
+
progress: progress.progress
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
} : void 0
|
|
843
|
+
});
|
|
844
|
+
initialized = true;
|
|
845
|
+
onProgress?.({ status: "ready" });
|
|
846
|
+
}
|
|
847
|
+
async function getEmbedding(text) {
|
|
848
|
+
if (!initialized) {
|
|
849
|
+
await initializeEmbeddings();
|
|
850
|
+
}
|
|
851
|
+
const truncated = text.slice(0, MAX_EMBEDDING_CHARS);
|
|
852
|
+
const output = await pipeline(truncated, {
|
|
853
|
+
pooling: "mean",
|
|
854
|
+
normalize: true
|
|
855
|
+
});
|
|
856
|
+
return Array.from(output.data);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/learning/LessonManager.ts
|
|
860
|
+
var VALIDATION_BOOST = 0.1;
|
|
861
|
+
var REJECTION_PENALTY = 0.2;
|
|
862
|
+
var MIN_CONFIDENCE = 0;
|
|
863
|
+
var MAX_CONFIDENCE = 1;
|
|
864
|
+
var AUTO_ARCHIVE_THRESHOLD = 0.1;
|
|
865
|
+
var DECAY_RATE = 0.05;
|
|
866
|
+
var LessonManager = class {
|
|
867
|
+
/**
|
|
868
|
+
* Create a new lesson and generate its embedding
|
|
869
|
+
*/
|
|
870
|
+
async create(input) {
|
|
871
|
+
const lesson = createLesson(input);
|
|
872
|
+
try {
|
|
873
|
+
const embeddingText = this.buildEmbeddingText(lesson);
|
|
874
|
+
const embedding = await getEmbedding(embeddingText);
|
|
875
|
+
storeLessonEmbedding(lesson.id, embedding);
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
return lesson;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Update a lesson
|
|
882
|
+
*/
|
|
883
|
+
update(id, updates) {
|
|
884
|
+
return updateLesson(id, updates);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Update a lesson and regenerate its embedding
|
|
888
|
+
*/
|
|
889
|
+
async updateWithEmbedding(id, updates) {
|
|
890
|
+
const lesson = updateLesson(id, updates);
|
|
891
|
+
if (lesson) {
|
|
892
|
+
if (updates.title || updates.triggerContext || updates.insight) {
|
|
893
|
+
try {
|
|
894
|
+
const embeddingText = this.buildEmbeddingText(lesson);
|
|
895
|
+
const embedding = await getEmbedding(embeddingText);
|
|
896
|
+
storeLessonEmbedding(lesson.id, embedding);
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return lesson;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Delete a lesson permanently
|
|
905
|
+
*/
|
|
906
|
+
delete(id) {
|
|
907
|
+
return deleteLesson(id);
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Archive a lesson (soft delete)
|
|
911
|
+
*/
|
|
912
|
+
archive(id) {
|
|
913
|
+
archiveLesson(id);
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Unarchive a lesson
|
|
917
|
+
*/
|
|
918
|
+
unarchive(id) {
|
|
919
|
+
unarchiveLesson(id);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Get a lesson by ID
|
|
923
|
+
*/
|
|
924
|
+
get(id) {
|
|
925
|
+
return getLesson(id);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Get lessons for a project
|
|
929
|
+
*/
|
|
930
|
+
getByProject(projectPath, options) {
|
|
931
|
+
return getLessonsByProject(projectPath, options);
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Get core lessons (high confidence, well-validated)
|
|
935
|
+
*/
|
|
936
|
+
getCore(projectPath, limit) {
|
|
937
|
+
return getCoreLessons(projectPath, limit);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Get all lessons
|
|
941
|
+
*/
|
|
942
|
+
getAll(options) {
|
|
943
|
+
return getAllLessons(options);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Record that a lesson was applied (shown to user)
|
|
947
|
+
*/
|
|
948
|
+
recordApplication(id) {
|
|
949
|
+
recordLessonApplication(id);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Record validation feedback - boosts confidence
|
|
953
|
+
*/
|
|
954
|
+
recordValidation(id, sessionId, comment) {
|
|
955
|
+
recordLessonValidation(id, sessionId, comment);
|
|
956
|
+
const lesson = getLesson(id);
|
|
957
|
+
if (lesson) {
|
|
958
|
+
const newConfidence = Math.min(MAX_CONFIDENCE, lesson.confidence + VALIDATION_BOOST);
|
|
959
|
+
updateLesson(id, { confidence: newConfidence });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Record rejection feedback - reduces confidence
|
|
964
|
+
* Auto-archives if confidence drops too low
|
|
965
|
+
*/
|
|
966
|
+
recordRejection(id, sessionId, comment) {
|
|
967
|
+
recordLessonRejection(id, sessionId, comment);
|
|
968
|
+
const lesson = getLesson(id);
|
|
969
|
+
if (lesson) {
|
|
970
|
+
const newConfidence = Math.max(MIN_CONFIDENCE, lesson.confidence - REJECTION_PENALTY);
|
|
971
|
+
if (newConfidence < AUTO_ARCHIVE_THRESHOLD) {
|
|
972
|
+
archiveLesson(id);
|
|
973
|
+
} else {
|
|
974
|
+
updateLesson(id, { confidence: newConfidence });
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Decay confidence of unused lessons
|
|
980
|
+
* Should be run periodically (e.g., weekly)
|
|
981
|
+
*/
|
|
982
|
+
decayUnusedLessons(daysThreshold = 30) {
|
|
983
|
+
const lessons = getAllLessons({ archived: false });
|
|
984
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
985
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysThreshold);
|
|
986
|
+
let decayedCount = 0;
|
|
987
|
+
for (const lesson of lessons) {
|
|
988
|
+
const lastUsed = lesson.lastAppliedAt ? new Date(lesson.lastAppliedAt) : new Date(lesson.createdAt);
|
|
989
|
+
if (lastUsed < cutoffDate) {
|
|
990
|
+
const newConfidence = Math.max(MIN_CONFIDENCE, lesson.confidence - DECAY_RATE);
|
|
991
|
+
if (newConfidence < AUTO_ARCHIVE_THRESHOLD) {
|
|
992
|
+
archiveLesson(lesson.id);
|
|
993
|
+
} else {
|
|
994
|
+
updateLesson(lesson.id, { confidence: newConfidence });
|
|
995
|
+
}
|
|
996
|
+
decayedCount++;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return decayedCount;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Build text for embedding generation
|
|
1003
|
+
*/
|
|
1004
|
+
buildEmbeddingText(lesson) {
|
|
1005
|
+
const parts = [
|
|
1006
|
+
`Title: ${lesson.title}`,
|
|
1007
|
+
`Category: ${lesson.category}`,
|
|
1008
|
+
`When to apply: ${lesson.triggerContext}`,
|
|
1009
|
+
`Insight: ${lesson.insight}`
|
|
1010
|
+
];
|
|
1011
|
+
if (lesson.reasoning) {
|
|
1012
|
+
parts.push(`Reasoning: ${lesson.reasoning}`);
|
|
1013
|
+
}
|
|
1014
|
+
return parts.join("\n\n");
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
var lessonManager = new LessonManager();
|
|
1018
|
+
|
|
1019
|
+
// src/learning/types.ts
|
|
1020
|
+
var VALID_CATEGORIES = [
|
|
1021
|
+
"architecture_decision",
|
|
1022
|
+
"anti_pattern",
|
|
1023
|
+
"bug_pattern",
|
|
1024
|
+
"project_convention",
|
|
1025
|
+
"dependency_knowledge",
|
|
1026
|
+
"domain_knowledge",
|
|
1027
|
+
"workflow",
|
|
1028
|
+
"other"
|
|
1029
|
+
];
|
|
1030
|
+
|
|
1031
|
+
// src/learning/SynthesisEngine.ts
|
|
1032
|
+
var SIMILARITY_THRESHOLD = 0.85;
|
|
1033
|
+
var MAX_MESSAGES_FOR_SYNTHESIS = 50;
|
|
1034
|
+
var MAX_MESSAGE_LENGTH = 2e3;
|
|
1035
|
+
var SynthesisEngine = class {
|
|
1036
|
+
/**
|
|
1037
|
+
* Synthesize lessons from a session
|
|
1038
|
+
*/
|
|
1039
|
+
async synthesize(session, messages) {
|
|
1040
|
+
const result = {
|
|
1041
|
+
lessonsCreated: 0,
|
|
1042
|
+
lessonsSkipped: 0,
|
|
1043
|
+
errors: []
|
|
1044
|
+
};
|
|
1045
|
+
if (!isClaudeCliAvailable()) {
|
|
1046
|
+
result.errors.push("Claude CLI not available for synthesis");
|
|
1047
|
+
return result;
|
|
1048
|
+
}
|
|
1049
|
+
if (messages.length < 3) {
|
|
1050
|
+
result.errors.push("Session too short for meaningful synthesis");
|
|
1051
|
+
return result;
|
|
1052
|
+
}
|
|
1053
|
+
try {
|
|
1054
|
+
const prompt = this.buildSynthesisPrompt(session, messages);
|
|
1055
|
+
const response = await runClaudePrompt(prompt, {
|
|
1056
|
+
model: "haiku",
|
|
1057
|
+
// Use haiku for speed and cost
|
|
1058
|
+
maxTokens: 2e3
|
|
1059
|
+
});
|
|
1060
|
+
if (!response.success) {
|
|
1061
|
+
result.errors.push(`Claude error: ${response.error}`);
|
|
1062
|
+
return result;
|
|
1063
|
+
}
|
|
1064
|
+
const rawLessons = this.parseResponse(response.content);
|
|
1065
|
+
if (rawLessons.length === 0) {
|
|
1066
|
+
return result;
|
|
1067
|
+
}
|
|
1068
|
+
const projectPath = session.projectPath || "";
|
|
1069
|
+
for (const raw of rawLessons) {
|
|
1070
|
+
try {
|
|
1071
|
+
const stored = await this.dedupeAndStore(raw, session, projectPath);
|
|
1072
|
+
if (stored) {
|
|
1073
|
+
result.lessonsCreated++;
|
|
1074
|
+
} else {
|
|
1075
|
+
result.lessonsSkipped++;
|
|
1076
|
+
}
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
result.errors.push(`Failed to store lesson: ${String(error)}`);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return result;
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
result.errors.push(`Synthesis failed: ${String(error)}`);
|
|
1084
|
+
return result;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Build the synthesis prompt for Claude
|
|
1089
|
+
*/
|
|
1090
|
+
buildSynthesisPrompt(session, messages) {
|
|
1091
|
+
const formattedMessages = this.formatMessages(messages);
|
|
1092
|
+
return `You are analyzing a Claude Code conversation session to extract reusable lessons.
|
|
1093
|
+
|
|
1094
|
+
## Session Information
|
|
1095
|
+
- Project: ${session.projectPath || "Unknown"}
|
|
1096
|
+
- Title: ${session.title}
|
|
1097
|
+
- Messages: ${messages.length}
|
|
1098
|
+
|
|
1099
|
+
## Conversation
|
|
1100
|
+
${formattedMessages}
|
|
1101
|
+
|
|
1102
|
+
## Task
|
|
1103
|
+
Extract reusable lessons from this conversation. Focus on:
|
|
1104
|
+
|
|
1105
|
+
1. **Architecture Decisions** - Design choices and their rationale
|
|
1106
|
+
2. **Anti-Patterns** - What NOT to do and why
|
|
1107
|
+
3. **Bug Patterns** - Common bugs and their root causes
|
|
1108
|
+
4. **Project Conventions** - Code style, naming, file organization
|
|
1109
|
+
5. **Dependency Knowledge** - Library quirks, version issues, APIs
|
|
1110
|
+
6. **Domain Knowledge** - Business logic, rules, requirements
|
|
1111
|
+
7. **Workflows** - Processes, commands, deployment steps
|
|
1112
|
+
|
|
1113
|
+
## Requirements
|
|
1114
|
+
- Only extract genuinely reusable lessons
|
|
1115
|
+
- Be specific and actionable, not generic
|
|
1116
|
+
- Include context for when the lesson applies
|
|
1117
|
+
- Skip trivial or one-off fixes
|
|
1118
|
+
- Focus on knowledge that would help future work
|
|
1119
|
+
|
|
1120
|
+
## Output Format
|
|
1121
|
+
Return a JSON array of lessons. Each lesson must have:
|
|
1122
|
+
\`\`\`json
|
|
1123
|
+
[
|
|
1124
|
+
{
|
|
1125
|
+
"category": "architecture_decision|anti_pattern|bug_pattern|project_convention|dependency_knowledge|domain_knowledge|workflow|other",
|
|
1126
|
+
"title": "Short descriptive title (max 60 chars)",
|
|
1127
|
+
"trigger_context": "When this lesson should be surfaced (what kind of prompt/task)",
|
|
1128
|
+
"insight": "The actual knowledge - specific and actionable",
|
|
1129
|
+
"reasoning": "Why this is true or important (optional)",
|
|
1130
|
+
"confidence": 0.3-0.7
|
|
1131
|
+
}
|
|
1132
|
+
]
|
|
1133
|
+
\`\`\`
|
|
1134
|
+
|
|
1135
|
+
Confidence guidelines:
|
|
1136
|
+
- 0.3-0.4: Observed once, might be specific to this case
|
|
1137
|
+
- 0.5: Reasonable general lesson
|
|
1138
|
+
- 0.6-0.7: Clear pattern with strong evidence
|
|
1139
|
+
|
|
1140
|
+
Return an empty array [] if no reusable lessons can be extracted.
|
|
1141
|
+
|
|
1142
|
+
Output only the JSON array, no other text.`;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Format messages for the prompt
|
|
1146
|
+
*/
|
|
1147
|
+
formatMessages(messages) {
|
|
1148
|
+
const relevantMessages = messages.slice(-MAX_MESSAGES_FOR_SYNTHESIS);
|
|
1149
|
+
return relevantMessages.map((m) => {
|
|
1150
|
+
const role = m.role === "user" ? "User" : "Assistant";
|
|
1151
|
+
const content = m.content.length > MAX_MESSAGE_LENGTH ? m.content.slice(0, MAX_MESSAGE_LENGTH) + "...[truncated]" : m.content;
|
|
1152
|
+
return `### ${role}
|
|
1153
|
+
${content}`;
|
|
1154
|
+
}).join("\n\n");
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Parse Claude's response into raw lessons
|
|
1158
|
+
*/
|
|
1159
|
+
parseResponse(content) {
|
|
1160
|
+
try {
|
|
1161
|
+
const jsonMatch = content.match(/\[[\s\S]*\]/);
|
|
1162
|
+
if (!jsonMatch) {
|
|
1163
|
+
return [];
|
|
1164
|
+
}
|
|
1165
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1166
|
+
if (!Array.isArray(parsed)) {
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
return parsed.filter((item) => this.isValidRawLesson(item)).map((item) => ({
|
|
1170
|
+
category: item.category,
|
|
1171
|
+
title: String(item.title).slice(0, 60),
|
|
1172
|
+
triggerContext: String(item.trigger_context),
|
|
1173
|
+
insight: String(item.insight),
|
|
1174
|
+
reasoning: item.reasoning ? String(item.reasoning) : void 0,
|
|
1175
|
+
confidence: Math.min(0.7, Math.max(0.3, Number(item.confidence) || 0.5))
|
|
1176
|
+
}));
|
|
1177
|
+
} catch {
|
|
1178
|
+
return [];
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Validate a raw lesson object
|
|
1183
|
+
*/
|
|
1184
|
+
isValidRawLesson(item) {
|
|
1185
|
+
if (typeof item !== "object" || item === null) {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
const obj = item;
|
|
1189
|
+
return typeof obj.category === "string" && VALID_CATEGORIES.includes(obj.category) && typeof obj.title === "string" && obj.title.length > 0 && typeof obj.trigger_context === "string" && obj.trigger_context.length > 0 && typeof obj.insight === "string" && obj.insight.length > 0;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Check for duplicates and store if unique
|
|
1193
|
+
*/
|
|
1194
|
+
async dedupeAndStore(raw, session, projectPath) {
|
|
1195
|
+
const embeddingText = `${raw.title} ${raw.triggerContext} ${raw.insight}`;
|
|
1196
|
+
try {
|
|
1197
|
+
const embedding = await getEmbedding(embeddingText);
|
|
1198
|
+
const similar = searchLessonsByEmbedding(embedding, projectPath, 1);
|
|
1199
|
+
if (similar.length > 0 && this.isTooSimilar(similar[0], raw)) {
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
const input = {
|
|
1203
|
+
projectPath,
|
|
1204
|
+
category: raw.category,
|
|
1205
|
+
title: raw.title,
|
|
1206
|
+
triggerContext: raw.triggerContext,
|
|
1207
|
+
insight: raw.insight,
|
|
1208
|
+
reasoning: raw.reasoning,
|
|
1209
|
+
confidence: raw.confidence,
|
|
1210
|
+
sourceSessionId: session.id,
|
|
1211
|
+
sourceType: "synthesized"
|
|
1212
|
+
};
|
|
1213
|
+
const lesson = await lessonManager.create(input);
|
|
1214
|
+
return lesson;
|
|
1215
|
+
} catch {
|
|
1216
|
+
const input = {
|
|
1217
|
+
projectPath,
|
|
1218
|
+
category: raw.category,
|
|
1219
|
+
title: raw.title,
|
|
1220
|
+
triggerContext: raw.triggerContext,
|
|
1221
|
+
insight: raw.insight,
|
|
1222
|
+
reasoning: raw.reasoning,
|
|
1223
|
+
confidence: raw.confidence,
|
|
1224
|
+
sourceSessionId: session.id,
|
|
1225
|
+
sourceType: "synthesized"
|
|
1226
|
+
};
|
|
1227
|
+
return lessonManager.create(input);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Check if a raw lesson is too similar to an existing lesson
|
|
1232
|
+
*/
|
|
1233
|
+
isTooSimilar(existing, raw) {
|
|
1234
|
+
const existingText = `${existing.title} ${existing.insight}`.toLowerCase();
|
|
1235
|
+
const rawText = `${raw.title} ${raw.insight}`.toLowerCase();
|
|
1236
|
+
const existingWords = new Set(existingText.split(/\s+/));
|
|
1237
|
+
const rawWords = rawText.split(/\s+/);
|
|
1238
|
+
let commonCount = 0;
|
|
1239
|
+
for (const word of rawWords) {
|
|
1240
|
+
if (existingWords.has(word)) {
|
|
1241
|
+
commonCount++;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const similarity = commonCount / Math.max(existingWords.size, rawWords.length);
|
|
1245
|
+
return similarity > SIMILARITY_THRESHOLD;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
var synthesisEngine = new SynthesisEngine();
|
|
1249
|
+
|
|
1250
|
+
// src/learning/processQueue.ts
|
|
1251
|
+
async function processSynthesisQueue(limit = 5) {
|
|
1252
|
+
const result = {
|
|
1253
|
+
processed: 0,
|
|
1254
|
+
lessonsCreated: 0,
|
|
1255
|
+
failed: 0,
|
|
1256
|
+
errors: []
|
|
1257
|
+
};
|
|
1258
|
+
const pending = getPendingSynthesis(limit);
|
|
1259
|
+
if (pending.length === 0) {
|
|
1260
|
+
return result;
|
|
1261
|
+
}
|
|
1262
|
+
for (const item of pending) {
|
|
1263
|
+
try {
|
|
1264
|
+
markSynthesisProcessing(item.id);
|
|
1265
|
+
const session = getSession(item.sessionId);
|
|
1266
|
+
if (!session) {
|
|
1267
|
+
markSynthesisFailed(item.id, "Session not found");
|
|
1268
|
+
result.failed++;
|
|
1269
|
+
result.errors.push(`Session not found: ${item.sessionId}`);
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
const messages = getSessionMessages(session.id);
|
|
1273
|
+
if (messages.length === 0) {
|
|
1274
|
+
markSynthesisFailed(item.id, "Session has no messages");
|
|
1275
|
+
result.failed++;
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
const synthesisResult = await synthesisEngine.synthesize(session, messages);
|
|
1279
|
+
if (synthesisResult.errors.length > 0) {
|
|
1280
|
+
result.errors.push(...synthesisResult.errors);
|
|
1281
|
+
}
|
|
1282
|
+
markSynthesisComplete(item.id, synthesisResult.lessonsCreated);
|
|
1283
|
+
result.processed++;
|
|
1284
|
+
result.lessonsCreated += synthesisResult.lessonsCreated;
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
markSynthesisFailed(item.id, String(error));
|
|
1287
|
+
result.failed++;
|
|
1288
|
+
result.errors.push(`Failed to process ${item.sessionId}: ${String(error)}`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return result;
|
|
1292
|
+
}
|
|
1293
|
+
function getQueueStatus() {
|
|
1294
|
+
return getSynthesisStats();
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// src/hooks/synthesize.ts
|
|
1298
|
+
async function main() {
|
|
1299
|
+
const limit = parseInt(process.argv[2], 10) || 5;
|
|
1300
|
+
const status = getQueueStatus();
|
|
1301
|
+
if (status.pending === 0) {
|
|
1302
|
+
console.log("No sessions pending synthesis.");
|
|
1303
|
+
process.exit(0);
|
|
1304
|
+
}
|
|
1305
|
+
console.log(`Processing up to ${limit} sessions...`);
|
|
1306
|
+
console.log(`Queue: ${status.pending} pending, ${status.processing} processing
|
|
1307
|
+
`);
|
|
1308
|
+
const result = await processSynthesisQueue(limit);
|
|
1309
|
+
console.log("\nSynthesis complete:");
|
|
1310
|
+
console.log(` Processed: ${result.processed}`);
|
|
1311
|
+
console.log(` Lessons created: ${result.lessonsCreated}`);
|
|
1312
|
+
console.log(` Failed: ${result.failed}`);
|
|
1313
|
+
if (result.errors.length > 0) {
|
|
1314
|
+
console.log("\nErrors:");
|
|
1315
|
+
for (const error of result.errors) {
|
|
1316
|
+
console.log(` - ${error}`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const newStatus = getQueueStatus();
|
|
1320
|
+
console.log(`
|
|
1321
|
+
Queue now: ${newStatus.pending} pending, ${newStatus.completed} completed`);
|
|
1322
|
+
console.log(`Total lessons created: ${newStatus.totalLessonsCreated}`);
|
|
1323
|
+
process.exit(result.failed > 0 ? 1 : 0);
|
|
1324
|
+
}
|
|
1325
|
+
main().catch((error) => {
|
|
1326
|
+
console.error("Synthesis error:", error);
|
|
1327
|
+
process.exit(1);
|
|
1328
|
+
});
|
|
1329
|
+
//# sourceMappingURL=synthesize.js.map
|