@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.
@@ -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