@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,1850 @@
|
|
|
1
|
+
// src/mcp/server.ts
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
|
|
9
|
+
// src/db/index.ts
|
|
10
|
+
import Database from "better-sqlite3";
|
|
11
|
+
import * as sqliteVec from "sqlite-vec";
|
|
12
|
+
import { existsSync as existsSync3 } from "fs";
|
|
13
|
+
|
|
14
|
+
// src/utils/config.ts
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import { join, basename, dirname } from "path";
|
|
17
|
+
import { mkdirSync, existsSync, copyFileSync } from "fs";
|
|
18
|
+
var CMEM_DIR = join(homedir(), ".cmem");
|
|
19
|
+
var DB_PATH = join(CMEM_DIR, "sessions.db");
|
|
20
|
+
var MODELS_DIR = join(CMEM_DIR, "models");
|
|
21
|
+
var BACKUPS_DIR = join(CMEM_DIR, "backups");
|
|
22
|
+
var CLAUDE_DIR = join(homedir(), ".claude");
|
|
23
|
+
var CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
|
|
24
|
+
var CLAUDE_SESSIONS_DIR = join(CLAUDE_DIR, "sessions");
|
|
25
|
+
var EMBEDDING_MODEL = "nomic-ai/nomic-embed-text-v1.5";
|
|
26
|
+
var EMBEDDING_DIMENSIONS = 768;
|
|
27
|
+
var MAX_EMBEDDING_CHARS = 8e3;
|
|
28
|
+
var DB_SIZE_ALERT_THRESHOLD = 5 * 1024 * 1024 * 1024;
|
|
29
|
+
function ensureCmemDir() {
|
|
30
|
+
if (!existsSync(CMEM_DIR)) {
|
|
31
|
+
mkdirSync(CMEM_DIR, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function ensureModelsDir() {
|
|
35
|
+
ensureCmemDir();
|
|
36
|
+
if (!existsSync(MODELS_DIR)) {
|
|
37
|
+
mkdirSync(MODELS_DIR, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/parser/index.ts
|
|
42
|
+
import { readFileSync, readdirSync, existsSync as existsSync2, statSync } from "fs";
|
|
43
|
+
function extractSessionMetadata(filepath) {
|
|
44
|
+
const content = readFileSync(filepath, "utf-8");
|
|
45
|
+
const metadata = {
|
|
46
|
+
isSidechain: false,
|
|
47
|
+
isMeta: false
|
|
48
|
+
};
|
|
49
|
+
for (const line of content.split("\n")) {
|
|
50
|
+
if (!line.trim()) continue;
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(line);
|
|
53
|
+
if (parsed.type === "user" && parsed.message) {
|
|
54
|
+
if (parsed.isSidechain === true) {
|
|
55
|
+
metadata.isSidechain = true;
|
|
56
|
+
}
|
|
57
|
+
if (parsed.agentId) {
|
|
58
|
+
metadata.isSidechain = true;
|
|
59
|
+
}
|
|
60
|
+
if (parsed.isMeta === true) {
|
|
61
|
+
metadata.isMeta = true;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return metadata;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/db/index.ts
|
|
72
|
+
var db = null;
|
|
73
|
+
function getDatabase() {
|
|
74
|
+
if (db) return db;
|
|
75
|
+
ensureCmemDir();
|
|
76
|
+
db = new Database(DB_PATH);
|
|
77
|
+
db.pragma("journal_mode = WAL");
|
|
78
|
+
sqliteVec.load(db);
|
|
79
|
+
initSchema(db);
|
|
80
|
+
return db;
|
|
81
|
+
}
|
|
82
|
+
function initSchema(database) {
|
|
83
|
+
database.exec(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
85
|
+
name TEXT PRIMARY KEY,
|
|
86
|
+
applied_at TEXT NOT NULL
|
|
87
|
+
);
|
|
88
|
+
`);
|
|
89
|
+
database.exec(`
|
|
90
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
91
|
+
id TEXT PRIMARY KEY,
|
|
92
|
+
title TEXT NOT NULL,
|
|
93
|
+
summary TEXT,
|
|
94
|
+
created_at TEXT NOT NULL,
|
|
95
|
+
updated_at TEXT NOT NULL,
|
|
96
|
+
message_count INTEGER DEFAULT 0,
|
|
97
|
+
project_path TEXT,
|
|
98
|
+
source_file TEXT,
|
|
99
|
+
raw_data TEXT NOT NULL
|
|
100
|
+
);
|
|
101
|
+
`);
|
|
102
|
+
try {
|
|
103
|
+
database.exec(`ALTER TABLE sessions ADD COLUMN source_file TEXT`);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
database.exec(`ALTER TABLE sessions ADD COLUMN is_sidechain INTEGER DEFAULT 0`);
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
database.exec(`ALTER TABLE sessions ADD COLUMN is_automated INTEGER DEFAULT 0`);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
database.exec(`ALTER TABLE sessions ADD COLUMN custom_title TEXT`);
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
database.exec(`
|
|
119
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
120
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
121
|
+
session_id TEXT NOT NULL,
|
|
122
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
123
|
+
content TEXT NOT NULL,
|
|
124
|
+
timestamp TEXT NOT NULL,
|
|
125
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
126
|
+
);
|
|
127
|
+
`);
|
|
128
|
+
database.exec(`
|
|
129
|
+
CREATE TABLE IF NOT EXISTS embedding_state (
|
|
130
|
+
session_id TEXT PRIMARY KEY,
|
|
131
|
+
content_length INTEGER NOT NULL,
|
|
132
|
+
file_mtime TEXT,
|
|
133
|
+
last_embedded_at TEXT NOT NULL,
|
|
134
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
135
|
+
);
|
|
136
|
+
`);
|
|
137
|
+
database.exec(`
|
|
138
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS session_embeddings USING vec0(
|
|
139
|
+
session_id TEXT PRIMARY KEY,
|
|
140
|
+
embedding FLOAT[${EMBEDDING_DIMENSIONS}]
|
|
141
|
+
);
|
|
142
|
+
`);
|
|
143
|
+
database.exec(`
|
|
144
|
+
CREATE TABLE IF NOT EXISTS favorites (
|
|
145
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
146
|
+
type TEXT NOT NULL CHECK (type IN ('session', 'folder')),
|
|
147
|
+
value TEXT NOT NULL,
|
|
148
|
+
created_at TEXT NOT NULL,
|
|
149
|
+
UNIQUE(type, value)
|
|
150
|
+
);
|
|
151
|
+
`);
|
|
152
|
+
database.exec(`
|
|
153
|
+
CREATE TABLE IF NOT EXISTS project_order (
|
|
154
|
+
path TEXT PRIMARY KEY,
|
|
155
|
+
sort_order INTEGER NOT NULL,
|
|
156
|
+
updated_at TEXT NOT NULL
|
|
157
|
+
);
|
|
158
|
+
`);
|
|
159
|
+
database.exec(`
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
161
|
+
`);
|
|
162
|
+
database.exec(`
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC);
|
|
164
|
+
`);
|
|
165
|
+
database.exec(`
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source_file);
|
|
167
|
+
`);
|
|
168
|
+
database.exec(`
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_favorites_type ON favorites(type);
|
|
170
|
+
`);
|
|
171
|
+
database.exec(`
|
|
172
|
+
CREATE TABLE IF NOT EXISTS lessons (
|
|
173
|
+
id TEXT PRIMARY KEY,
|
|
174
|
+
project_path TEXT NOT NULL,
|
|
175
|
+
category TEXT NOT NULL CHECK (category IN (
|
|
176
|
+
'architecture_decision', 'anti_pattern', 'bug_pattern',
|
|
177
|
+
'project_convention', 'dependency_knowledge', 'domain_knowledge',
|
|
178
|
+
'workflow', 'other'
|
|
179
|
+
)),
|
|
180
|
+
title TEXT NOT NULL,
|
|
181
|
+
trigger_context TEXT NOT NULL,
|
|
182
|
+
insight TEXT NOT NULL,
|
|
183
|
+
reasoning TEXT,
|
|
184
|
+
confidence REAL DEFAULT 0.5,
|
|
185
|
+
times_applied INTEGER DEFAULT 0,
|
|
186
|
+
times_validated INTEGER DEFAULT 0,
|
|
187
|
+
times_rejected INTEGER DEFAULT 0,
|
|
188
|
+
source_session_id TEXT,
|
|
189
|
+
source_type TEXT DEFAULT 'synthesized',
|
|
190
|
+
archived INTEGER DEFAULT 0,
|
|
191
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
192
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
193
|
+
last_applied_at TEXT
|
|
194
|
+
);
|
|
195
|
+
`);
|
|
196
|
+
database.exec(`
|
|
197
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS lesson_embeddings USING vec0(
|
|
198
|
+
lesson_id TEXT PRIMARY KEY,
|
|
199
|
+
embedding FLOAT[${EMBEDDING_DIMENSIONS}]
|
|
200
|
+
);
|
|
201
|
+
`);
|
|
202
|
+
database.exec(`
|
|
203
|
+
CREATE TABLE IF NOT EXISTS lesson_feedback (
|
|
204
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
205
|
+
lesson_id TEXT NOT NULL,
|
|
206
|
+
session_id TEXT,
|
|
207
|
+
feedback_type TEXT NOT NULL CHECK (feedback_type IN (
|
|
208
|
+
'validated', 'rejected', 'modified'
|
|
209
|
+
)),
|
|
210
|
+
comment TEXT,
|
|
211
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
212
|
+
FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE
|
|
213
|
+
);
|
|
214
|
+
`);
|
|
215
|
+
database.exec(`
|
|
216
|
+
CREATE TABLE IF NOT EXISTS synthesis_queue (
|
|
217
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
218
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
219
|
+
project_path TEXT NOT NULL,
|
|
220
|
+
queued_at TEXT DEFAULT (datetime('now')),
|
|
221
|
+
status TEXT DEFAULT 'pending',
|
|
222
|
+
processed_at TEXT,
|
|
223
|
+
lessons_created INTEGER DEFAULT 0,
|
|
224
|
+
error TEXT
|
|
225
|
+
);
|
|
226
|
+
`);
|
|
227
|
+
database.exec(`
|
|
228
|
+
CREATE TABLE IF NOT EXISTS session_injections (
|
|
229
|
+
session_id TEXT NOT NULL,
|
|
230
|
+
lesson_id TEXT NOT NULL,
|
|
231
|
+
injected_at TEXT DEFAULT (datetime('now')),
|
|
232
|
+
PRIMARY KEY (session_id, lesson_id)
|
|
233
|
+
);
|
|
234
|
+
`);
|
|
235
|
+
database.exec(`
|
|
236
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_project ON lessons(project_path);
|
|
237
|
+
`);
|
|
238
|
+
database.exec(`
|
|
239
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_category ON lessons(category);
|
|
240
|
+
`);
|
|
241
|
+
database.exec(`
|
|
242
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_confidence ON lessons(confidence DESC);
|
|
243
|
+
`);
|
|
244
|
+
database.exec(`
|
|
245
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_archived ON lessons(archived);
|
|
246
|
+
`);
|
|
247
|
+
database.exec(`
|
|
248
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_queue_status ON synthesis_queue(status);
|
|
249
|
+
`);
|
|
250
|
+
database.exec(`
|
|
251
|
+
CREATE INDEX IF NOT EXISTS idx_session_injections_session ON session_injections(session_id);
|
|
252
|
+
`);
|
|
253
|
+
runMigrations(database);
|
|
254
|
+
}
|
|
255
|
+
function runMigrations(database) {
|
|
256
|
+
const migrationName = "populate_session_metadata_v1";
|
|
257
|
+
const existing = database.prepare(
|
|
258
|
+
"SELECT 1 FROM migrations WHERE name = ?"
|
|
259
|
+
).get(migrationName);
|
|
260
|
+
if (existing) return;
|
|
261
|
+
const sessions = database.prepare(`
|
|
262
|
+
SELECT id, source_file FROM sessions WHERE source_file IS NOT NULL
|
|
263
|
+
`).all();
|
|
264
|
+
const updateStmt = database.prepare(`
|
|
265
|
+
UPDATE sessions SET is_sidechain = ?, is_automated = ? WHERE id = ?
|
|
266
|
+
`);
|
|
267
|
+
const transaction = database.transaction(() => {
|
|
268
|
+
for (const session of sessions) {
|
|
269
|
+
if (!existsSync3(session.source_file)) continue;
|
|
270
|
+
try {
|
|
271
|
+
const metadata = extractSessionMetadata(session.source_file);
|
|
272
|
+
const isAutomated = metadata.isSidechain || metadata.isMeta;
|
|
273
|
+
updateStmt.run(
|
|
274
|
+
metadata.isSidechain ? 1 : 0,
|
|
275
|
+
isAutomated ? 1 : 0,
|
|
276
|
+
session.id
|
|
277
|
+
);
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
database.prepare(
|
|
282
|
+
"INSERT INTO migrations (name, applied_at) VALUES (?, ?)"
|
|
283
|
+
).run(migrationName, (/* @__PURE__ */ new Date()).toISOString());
|
|
284
|
+
});
|
|
285
|
+
transaction();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/db/sessions.ts
|
|
289
|
+
function getSession(id) {
|
|
290
|
+
const db2 = getDatabase();
|
|
291
|
+
const row = db2.prepare(`
|
|
292
|
+
SELECT id, title, custom_title as customTitle, summary,
|
|
293
|
+
created_at as createdAt, updated_at as updatedAt,
|
|
294
|
+
message_count as messageCount, project_path as projectPath,
|
|
295
|
+
source_file as sourceFile, raw_data as rawData,
|
|
296
|
+
is_sidechain as isSidechain, is_automated as isAutomated
|
|
297
|
+
FROM sessions WHERE id = ?
|
|
298
|
+
`).get(id);
|
|
299
|
+
if (!row) return null;
|
|
300
|
+
return mapSessionRow(row);
|
|
301
|
+
}
|
|
302
|
+
function getSessionByIdPrefix(idPrefix) {
|
|
303
|
+
const db2 = getDatabase();
|
|
304
|
+
const row = db2.prepare(`
|
|
305
|
+
SELECT id, title, custom_title as customTitle, summary,
|
|
306
|
+
created_at as createdAt, updated_at as updatedAt,
|
|
307
|
+
message_count as messageCount, project_path as projectPath,
|
|
308
|
+
source_file as sourceFile, raw_data as rawData,
|
|
309
|
+
is_sidechain as isSidechain, is_automated as isAutomated
|
|
310
|
+
FROM sessions WHERE id LIKE ? || '%'
|
|
311
|
+
LIMIT 1
|
|
312
|
+
`).get(idPrefix);
|
|
313
|
+
if (!row) return null;
|
|
314
|
+
return mapSessionRow(row);
|
|
315
|
+
}
|
|
316
|
+
function getSessionMessages(sessionId) {
|
|
317
|
+
const db2 = getDatabase();
|
|
318
|
+
const rows = db2.prepare(`
|
|
319
|
+
SELECT id, session_id as sessionId, role, content, timestamp
|
|
320
|
+
FROM messages WHERE session_id = ?
|
|
321
|
+
ORDER BY timestamp ASC
|
|
322
|
+
`).all(sessionId);
|
|
323
|
+
return rows;
|
|
324
|
+
}
|
|
325
|
+
function mapSessionRow(row) {
|
|
326
|
+
return {
|
|
327
|
+
...row,
|
|
328
|
+
customTitle: row.customTitle,
|
|
329
|
+
isSidechain: row.isSidechain === 1,
|
|
330
|
+
isAutomated: row.isAutomated === 1
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function listSessions(limit = 100) {
|
|
334
|
+
const db2 = getDatabase();
|
|
335
|
+
const rows = db2.prepare(`
|
|
336
|
+
SELECT id, title, custom_title as customTitle, summary,
|
|
337
|
+
created_at as createdAt, updated_at as updatedAt,
|
|
338
|
+
message_count as messageCount, project_path as projectPath,
|
|
339
|
+
source_file as sourceFile, raw_data as rawData,
|
|
340
|
+
is_sidechain as isSidechain, is_automated as isAutomated
|
|
341
|
+
FROM sessions
|
|
342
|
+
ORDER BY updated_at DESC
|
|
343
|
+
LIMIT ?
|
|
344
|
+
`).all(limit);
|
|
345
|
+
return rows.map(mapSessionRow);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/db/vectors.ts
|
|
349
|
+
function searchSessions(queryEmbedding, limit = 10) {
|
|
350
|
+
const db2 = getDatabase();
|
|
351
|
+
const embeddingJson = JSON.stringify(queryEmbedding);
|
|
352
|
+
const rows = db2.prepare(`
|
|
353
|
+
SELECT s.id, s.title, s.summary, s.created_at as createdAt, s.updated_at as updatedAt,
|
|
354
|
+
s.message_count as messageCount, s.project_path as projectPath, s.raw_data as rawData,
|
|
355
|
+
vec_distance_L2(e.embedding, ?) as distance
|
|
356
|
+
FROM session_embeddings e
|
|
357
|
+
JOIN sessions s ON e.session_id = s.id
|
|
358
|
+
ORDER BY distance ASC
|
|
359
|
+
LIMIT ?
|
|
360
|
+
`).all(embeddingJson, limit);
|
|
361
|
+
return rows;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/embeddings/index.ts
|
|
365
|
+
import { existsSync as existsSync4 } from "fs";
|
|
366
|
+
import { join as join2 } from "path";
|
|
367
|
+
var transformersModule = null;
|
|
368
|
+
var pipeline = null;
|
|
369
|
+
var initialized = false;
|
|
370
|
+
async function getTransformers() {
|
|
371
|
+
if (!transformersModule) {
|
|
372
|
+
transformersModule = await import("@xenova/transformers");
|
|
373
|
+
}
|
|
374
|
+
return transformersModule;
|
|
375
|
+
}
|
|
376
|
+
function isModelCached() {
|
|
377
|
+
const modelCachePath = join2(MODELS_DIR, EMBEDDING_MODEL);
|
|
378
|
+
return existsSync4(modelCachePath);
|
|
379
|
+
}
|
|
380
|
+
async function initializeEmbeddings(onProgress) {
|
|
381
|
+
if (initialized && pipeline) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
ensureModelsDir();
|
|
385
|
+
const { pipeline: createPipeline, env } = await getTransformers();
|
|
386
|
+
env.cacheDir = MODELS_DIR;
|
|
387
|
+
const cached = isModelCached();
|
|
388
|
+
if (cached) {
|
|
389
|
+
env.allowRemoteModels = false;
|
|
390
|
+
onProgress?.({ status: "loading" });
|
|
391
|
+
} else {
|
|
392
|
+
onProgress?.({ status: "downloading" });
|
|
393
|
+
}
|
|
394
|
+
pipeline = await createPipeline("feature-extraction", EMBEDDING_MODEL, {
|
|
395
|
+
progress_callback: onProgress ? (progress) => {
|
|
396
|
+
if (progress.status === "progress" && progress.file && progress.progress !== void 0) {
|
|
397
|
+
onProgress({
|
|
398
|
+
status: "downloading",
|
|
399
|
+
file: progress.file,
|
|
400
|
+
progress: progress.progress
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
} : void 0
|
|
404
|
+
});
|
|
405
|
+
initialized = true;
|
|
406
|
+
onProgress?.({ status: "ready" });
|
|
407
|
+
}
|
|
408
|
+
function isReady() {
|
|
409
|
+
return initialized && pipeline !== null;
|
|
410
|
+
}
|
|
411
|
+
async function getEmbedding(text) {
|
|
412
|
+
if (!initialized) {
|
|
413
|
+
await initializeEmbeddings();
|
|
414
|
+
}
|
|
415
|
+
const truncated = text.slice(0, MAX_EMBEDDING_CHARS);
|
|
416
|
+
const output = await pipeline(truncated, {
|
|
417
|
+
pooling: "mean",
|
|
418
|
+
normalize: true
|
|
419
|
+
});
|
|
420
|
+
return Array.from(output.data);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/utils/format.ts
|
|
424
|
+
function formatTimeAgo(timestamp) {
|
|
425
|
+
const date = new Date(timestamp);
|
|
426
|
+
const now = /* @__PURE__ */ new Date();
|
|
427
|
+
const diffMs = now.getTime() - date.getTime();
|
|
428
|
+
const diffSecs = Math.floor(diffMs / 1e3);
|
|
429
|
+
const diffMins = Math.floor(diffSecs / 60);
|
|
430
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
431
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
432
|
+
const diffWeeks = Math.floor(diffDays / 7);
|
|
433
|
+
const diffMonths = Math.floor(diffDays / 30);
|
|
434
|
+
if (diffSecs < 60) return "just now";
|
|
435
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
436
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
437
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
438
|
+
if (diffWeeks < 4) return `${diffWeeks}w ago`;
|
|
439
|
+
return `${diffMonths}mo ago`;
|
|
440
|
+
}
|
|
441
|
+
function truncate(text, maxLength) {
|
|
442
|
+
if (text.length <= maxLength) return text;
|
|
443
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/utils/claude-cli.ts
|
|
447
|
+
import { spawn } from "child_process";
|
|
448
|
+
import { execSync } from "child_process";
|
|
449
|
+
function getClaudePath() {
|
|
450
|
+
try {
|
|
451
|
+
const result = execSync("which claude", { encoding: "utf-8" }).trim();
|
|
452
|
+
return result || null;
|
|
453
|
+
} catch {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function parseStreamJsonLine(line) {
|
|
458
|
+
if (!line.trim()) return null;
|
|
459
|
+
try {
|
|
460
|
+
const data = JSON.parse(line);
|
|
461
|
+
if (data.type === "assistant") {
|
|
462
|
+
if (data.message?.content && Array.isArray(data.message.content)) {
|
|
463
|
+
const textBlocks = [];
|
|
464
|
+
for (const block of data.message.content) {
|
|
465
|
+
if (block.type === "text" && block.text) {
|
|
466
|
+
textBlocks.push(block.text);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (textBlocks.length > 0) {
|
|
470
|
+
return { type: "text", content: textBlocks.join("\n") };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} else if (data.type === "content_block_delta") {
|
|
474
|
+
if (data.delta?.type === "text_delta" && data.delta.text) {
|
|
475
|
+
return { type: "text", content: data.delta.text };
|
|
476
|
+
}
|
|
477
|
+
} else if (data.type === "result") {
|
|
478
|
+
if (data.is_error) {
|
|
479
|
+
return { type: "error", content: data.result || "Unknown error" };
|
|
480
|
+
}
|
|
481
|
+
const usage = data.usage || {};
|
|
482
|
+
const inputTokens = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
483
|
+
const outputTokens = usage.output_tokens || 0;
|
|
484
|
+
return {
|
|
485
|
+
type: "done",
|
|
486
|
+
inputTokens,
|
|
487
|
+
outputTokens,
|
|
488
|
+
costUsd: data.total_cost_usd,
|
|
489
|
+
durationMs: data.duration_ms
|
|
490
|
+
};
|
|
491
|
+
} else if (data.type === "error") {
|
|
492
|
+
return {
|
|
493
|
+
type: "error",
|
|
494
|
+
content: data.error?.message || JSON.stringify(data.error) || "Unknown error"
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
return { type: "skip" };
|
|
498
|
+
} catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function runClaudePrompt(prompt, options = {}) {
|
|
503
|
+
const claudePath = getClaudePath();
|
|
504
|
+
if (!claudePath) {
|
|
505
|
+
return {
|
|
506
|
+
success: false,
|
|
507
|
+
content: "",
|
|
508
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const { model = "haiku" } = options;
|
|
512
|
+
const args = [
|
|
513
|
+
"-p",
|
|
514
|
+
prompt,
|
|
515
|
+
"--output-format",
|
|
516
|
+
"stream-json",
|
|
517
|
+
"--verbose",
|
|
518
|
+
// Required when using stream-json with -p
|
|
519
|
+
"--model",
|
|
520
|
+
model,
|
|
521
|
+
"--permission-mode",
|
|
522
|
+
"plan"
|
|
523
|
+
// Read-only, no tools needed for summarization
|
|
524
|
+
];
|
|
525
|
+
return new Promise((resolve) => {
|
|
526
|
+
const childProcess = spawn(claudePath, args, {
|
|
527
|
+
env: {
|
|
528
|
+
...process.env,
|
|
529
|
+
CI: "true"
|
|
530
|
+
// Prevent interactive prompts
|
|
531
|
+
},
|
|
532
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
533
|
+
});
|
|
534
|
+
childProcess.stdin?.end();
|
|
535
|
+
let buffer = "";
|
|
536
|
+
const textChunks = [];
|
|
537
|
+
let finalResult = null;
|
|
538
|
+
let errorContent = "";
|
|
539
|
+
childProcess.stdout?.on("data", (data) => {
|
|
540
|
+
buffer += data.toString();
|
|
541
|
+
const lines = buffer.split("\n");
|
|
542
|
+
buffer = lines.pop() || "";
|
|
543
|
+
for (const line of lines) {
|
|
544
|
+
if (!line.trim()) continue;
|
|
545
|
+
const chunk = parseStreamJsonLine(line);
|
|
546
|
+
if (chunk) {
|
|
547
|
+
if (chunk.type === "text" && chunk.content) {
|
|
548
|
+
textChunks.push(chunk.content);
|
|
549
|
+
} else if (chunk.type === "done") {
|
|
550
|
+
finalResult = chunk;
|
|
551
|
+
} else if (chunk.type === "error" && chunk.content) {
|
|
552
|
+
errorContent = chunk.content;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
childProcess.stderr?.on("data", (data) => {
|
|
558
|
+
const text = data.toString().toLowerCase();
|
|
559
|
+
if (text.includes("error") || text.includes("failed")) {
|
|
560
|
+
errorContent = data.toString().trim();
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
childProcess.on("close", (code) => {
|
|
564
|
+
if (buffer.trim()) {
|
|
565
|
+
const chunk = parseStreamJsonLine(buffer);
|
|
566
|
+
if (chunk) {
|
|
567
|
+
if (chunk.type === "text" && chunk.content) {
|
|
568
|
+
textChunks.push(chunk.content);
|
|
569
|
+
} else if (chunk.type === "done") {
|
|
570
|
+
finalResult = chunk;
|
|
571
|
+
} else if (chunk.type === "error" && chunk.content) {
|
|
572
|
+
errorContent = chunk.content;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const content = textChunks.join("");
|
|
577
|
+
if (errorContent && !content) {
|
|
578
|
+
resolve({
|
|
579
|
+
success: false,
|
|
580
|
+
content: "",
|
|
581
|
+
error: errorContent
|
|
582
|
+
});
|
|
583
|
+
} else {
|
|
584
|
+
resolve({
|
|
585
|
+
success: code === 0 && content.length > 0,
|
|
586
|
+
content,
|
|
587
|
+
error: errorContent || void 0,
|
|
588
|
+
inputTokens: finalResult?.inputTokens,
|
|
589
|
+
outputTokens: finalResult?.outputTokens,
|
|
590
|
+
costUsd: finalResult?.costUsd,
|
|
591
|
+
durationMs: finalResult?.durationMs
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
childProcess.on("error", (err) => {
|
|
596
|
+
resolve({
|
|
597
|
+
success: false,
|
|
598
|
+
content: "",
|
|
599
|
+
error: err.message
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
setTimeout(() => {
|
|
603
|
+
childProcess.kill("SIGTERM");
|
|
604
|
+
resolve({
|
|
605
|
+
success: false,
|
|
606
|
+
content: textChunks.join(""),
|
|
607
|
+
error: "Request timed out after 60 seconds"
|
|
608
|
+
});
|
|
609
|
+
}, 6e4);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
function isClaudeCliAvailable() {
|
|
613
|
+
return getClaudePath() !== null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/db/lessons.ts
|
|
617
|
+
import { randomUUID } from "crypto";
|
|
618
|
+
function mapLessonRow(row) {
|
|
619
|
+
return {
|
|
620
|
+
id: row.id,
|
|
621
|
+
projectPath: row.project_path,
|
|
622
|
+
category: row.category,
|
|
623
|
+
title: row.title,
|
|
624
|
+
triggerContext: row.trigger_context,
|
|
625
|
+
insight: row.insight,
|
|
626
|
+
reasoning: row.reasoning ?? void 0,
|
|
627
|
+
confidence: row.confidence,
|
|
628
|
+
timesApplied: row.times_applied,
|
|
629
|
+
timesValidated: row.times_validated,
|
|
630
|
+
timesRejected: row.times_rejected,
|
|
631
|
+
sourceSessionId: row.source_session_id ?? void 0,
|
|
632
|
+
sourceType: row.source_type,
|
|
633
|
+
archived: row.archived === 1,
|
|
634
|
+
createdAt: row.created_at,
|
|
635
|
+
updatedAt: row.updated_at,
|
|
636
|
+
lastAppliedAt: row.last_applied_at ?? void 0
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function createLesson(input) {
|
|
640
|
+
const db2 = getDatabase();
|
|
641
|
+
const id = randomUUID();
|
|
642
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
643
|
+
db2.prepare(`
|
|
644
|
+
INSERT INTO lessons (
|
|
645
|
+
id, project_path, category, title, trigger_context, insight,
|
|
646
|
+
reasoning, confidence, source_session_id, source_type,
|
|
647
|
+
created_at, updated_at
|
|
648
|
+
)
|
|
649
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
650
|
+
`).run(
|
|
651
|
+
id,
|
|
652
|
+
input.projectPath,
|
|
653
|
+
input.category,
|
|
654
|
+
input.title,
|
|
655
|
+
input.triggerContext,
|
|
656
|
+
input.insight,
|
|
657
|
+
input.reasoning ?? null,
|
|
658
|
+
input.confidence ?? 0.5,
|
|
659
|
+
input.sourceSessionId ?? null,
|
|
660
|
+
input.sourceType ?? "synthesized",
|
|
661
|
+
now,
|
|
662
|
+
now
|
|
663
|
+
);
|
|
664
|
+
return getLesson(id);
|
|
665
|
+
}
|
|
666
|
+
function updateLesson(id, updates) {
|
|
667
|
+
const db2 = getDatabase();
|
|
668
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
669
|
+
const fields = ["updated_at = ?"];
|
|
670
|
+
const values = [now];
|
|
671
|
+
if (updates.category !== void 0) {
|
|
672
|
+
fields.push("category = ?");
|
|
673
|
+
values.push(updates.category);
|
|
674
|
+
}
|
|
675
|
+
if (updates.title !== void 0) {
|
|
676
|
+
fields.push("title = ?");
|
|
677
|
+
values.push(updates.title);
|
|
678
|
+
}
|
|
679
|
+
if (updates.triggerContext !== void 0) {
|
|
680
|
+
fields.push("trigger_context = ?");
|
|
681
|
+
values.push(updates.triggerContext);
|
|
682
|
+
}
|
|
683
|
+
if (updates.insight !== void 0) {
|
|
684
|
+
fields.push("insight = ?");
|
|
685
|
+
values.push(updates.insight);
|
|
686
|
+
}
|
|
687
|
+
if (updates.reasoning !== void 0) {
|
|
688
|
+
fields.push("reasoning = ?");
|
|
689
|
+
values.push(updates.reasoning);
|
|
690
|
+
}
|
|
691
|
+
if (updates.confidence !== void 0) {
|
|
692
|
+
fields.push("confidence = ?");
|
|
693
|
+
values.push(updates.confidence);
|
|
694
|
+
}
|
|
695
|
+
if (updates.archived !== void 0) {
|
|
696
|
+
fields.push("archived = ?");
|
|
697
|
+
values.push(updates.archived ? 1 : 0);
|
|
698
|
+
}
|
|
699
|
+
values.push(id);
|
|
700
|
+
db2.prepare(`
|
|
701
|
+
UPDATE lessons SET ${fields.join(", ")} WHERE id = ?
|
|
702
|
+
`).run(...values);
|
|
703
|
+
return getLesson(id);
|
|
704
|
+
}
|
|
705
|
+
function deleteLesson(id) {
|
|
706
|
+
const db2 = getDatabase();
|
|
707
|
+
const transaction = db2.transaction(() => {
|
|
708
|
+
db2.prepare("DELETE FROM lesson_embeddings WHERE lesson_id = ?").run(id);
|
|
709
|
+
db2.prepare("DELETE FROM lesson_feedback WHERE lesson_id = ?").run(id);
|
|
710
|
+
const result = db2.prepare("DELETE FROM lessons WHERE id = ?").run(id);
|
|
711
|
+
return result.changes > 0;
|
|
712
|
+
});
|
|
713
|
+
return transaction();
|
|
714
|
+
}
|
|
715
|
+
function archiveLesson(id) {
|
|
716
|
+
const db2 = getDatabase();
|
|
717
|
+
db2.prepare(`
|
|
718
|
+
UPDATE lessons SET archived = 1, updated_at = ? WHERE id = ?
|
|
719
|
+
`).run((/* @__PURE__ */ new Date()).toISOString(), id);
|
|
720
|
+
}
|
|
721
|
+
function unarchiveLesson(id) {
|
|
722
|
+
const db2 = getDatabase();
|
|
723
|
+
db2.prepare(`
|
|
724
|
+
UPDATE lessons SET archived = 0, updated_at = ? WHERE id = ?
|
|
725
|
+
`).run((/* @__PURE__ */ new Date()).toISOString(), id);
|
|
726
|
+
}
|
|
727
|
+
function getLesson(id) {
|
|
728
|
+
const db2 = getDatabase();
|
|
729
|
+
const row = db2.prepare(`
|
|
730
|
+
SELECT * FROM lessons WHERE id = ?
|
|
731
|
+
`).get(id);
|
|
732
|
+
return row ? mapLessonRow(row) : null;
|
|
733
|
+
}
|
|
734
|
+
function getLessonsByProject(projectPath, options = {}) {
|
|
735
|
+
const db2 = getDatabase();
|
|
736
|
+
let query = "SELECT * FROM lessons WHERE project_path = ?";
|
|
737
|
+
const params = [projectPath];
|
|
738
|
+
if (options.category) {
|
|
739
|
+
query += " AND category = ?";
|
|
740
|
+
params.push(options.category);
|
|
741
|
+
}
|
|
742
|
+
if (options.archived !== void 0) {
|
|
743
|
+
query += " AND archived = ?";
|
|
744
|
+
params.push(options.archived ? 1 : 0);
|
|
745
|
+
} else {
|
|
746
|
+
query += " AND archived = 0";
|
|
747
|
+
}
|
|
748
|
+
if (options.minConfidence !== void 0) {
|
|
749
|
+
query += " AND confidence >= ?";
|
|
750
|
+
params.push(options.minConfidence);
|
|
751
|
+
}
|
|
752
|
+
query += " ORDER BY confidence DESC, times_applied DESC";
|
|
753
|
+
if (options.limit) {
|
|
754
|
+
query += " LIMIT ?";
|
|
755
|
+
params.push(options.limit);
|
|
756
|
+
}
|
|
757
|
+
const rows = db2.prepare(query).all(...params);
|
|
758
|
+
return rows.map(mapLessonRow);
|
|
759
|
+
}
|
|
760
|
+
function getCoreLessons(projectPath, limit = 3) {
|
|
761
|
+
const db2 = getDatabase();
|
|
762
|
+
const rows = db2.prepare(`
|
|
763
|
+
SELECT * FROM lessons
|
|
764
|
+
WHERE project_path = ?
|
|
765
|
+
AND archived = 0
|
|
766
|
+
AND confidence >= 0.7
|
|
767
|
+
ORDER BY
|
|
768
|
+
times_validated DESC,
|
|
769
|
+
confidence DESC,
|
|
770
|
+
times_applied DESC
|
|
771
|
+
LIMIT ?
|
|
772
|
+
`).all(projectPath, limit);
|
|
773
|
+
return rows.map(mapLessonRow);
|
|
774
|
+
}
|
|
775
|
+
function getAllLessons(options = {}) {
|
|
776
|
+
const db2 = getDatabase();
|
|
777
|
+
let query = "SELECT * FROM lessons WHERE 1=1";
|
|
778
|
+
const params = [];
|
|
779
|
+
if (options.category) {
|
|
780
|
+
query += " AND category = ?";
|
|
781
|
+
params.push(options.category);
|
|
782
|
+
}
|
|
783
|
+
if (options.archived !== void 0) {
|
|
784
|
+
query += " AND archived = ?";
|
|
785
|
+
params.push(options.archived ? 1 : 0);
|
|
786
|
+
}
|
|
787
|
+
if (options.minConfidence !== void 0) {
|
|
788
|
+
query += " AND confidence >= ?";
|
|
789
|
+
params.push(options.minConfidence);
|
|
790
|
+
}
|
|
791
|
+
query += " ORDER BY updated_at DESC";
|
|
792
|
+
if (options.limit) {
|
|
793
|
+
query += " LIMIT ?";
|
|
794
|
+
params.push(options.limit);
|
|
795
|
+
}
|
|
796
|
+
const rows = db2.prepare(query).all(...params);
|
|
797
|
+
return rows.map(mapLessonRow);
|
|
798
|
+
}
|
|
799
|
+
function recordLessonApplication(id) {
|
|
800
|
+
const db2 = getDatabase();
|
|
801
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
802
|
+
db2.prepare(`
|
|
803
|
+
UPDATE lessons
|
|
804
|
+
SET times_applied = times_applied + 1,
|
|
805
|
+
last_applied_at = ?,
|
|
806
|
+
updated_at = ?
|
|
807
|
+
WHERE id = ?
|
|
808
|
+
`).run(now, now, id);
|
|
809
|
+
}
|
|
810
|
+
function recordLessonValidation(id, sessionId, comment) {
|
|
811
|
+
const db2 = getDatabase();
|
|
812
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
813
|
+
const transaction = db2.transaction(() => {
|
|
814
|
+
db2.prepare(`
|
|
815
|
+
UPDATE lessons
|
|
816
|
+
SET times_validated = times_validated + 1, updated_at = ?
|
|
817
|
+
WHERE id = ?
|
|
818
|
+
`).run(now, id);
|
|
819
|
+
db2.prepare(`
|
|
820
|
+
INSERT INTO lesson_feedback (lesson_id, session_id, feedback_type, comment)
|
|
821
|
+
VALUES (?, ?, 'validated', ?)
|
|
822
|
+
`).run(id, sessionId ?? null, comment ?? null);
|
|
823
|
+
});
|
|
824
|
+
transaction();
|
|
825
|
+
}
|
|
826
|
+
function recordLessonRejection(id, sessionId, comment) {
|
|
827
|
+
const db2 = getDatabase();
|
|
828
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
829
|
+
const transaction = db2.transaction(() => {
|
|
830
|
+
db2.prepare(`
|
|
831
|
+
UPDATE lessons
|
|
832
|
+
SET times_rejected = times_rejected + 1, updated_at = ?
|
|
833
|
+
WHERE id = ?
|
|
834
|
+
`).run(now, id);
|
|
835
|
+
db2.prepare(`
|
|
836
|
+
INSERT INTO lesson_feedback (lesson_id, session_id, feedback_type, comment)
|
|
837
|
+
VALUES (?, ?, 'rejected', ?)
|
|
838
|
+
`).run(id, sessionId ?? null, comment ?? null);
|
|
839
|
+
});
|
|
840
|
+
transaction();
|
|
841
|
+
}
|
|
842
|
+
function storeLessonEmbedding(lessonId, embedding) {
|
|
843
|
+
const db2 = getDatabase();
|
|
844
|
+
db2.prepare("DELETE FROM lesson_embeddings WHERE lesson_id = ?").run(lessonId);
|
|
845
|
+
db2.prepare(`
|
|
846
|
+
INSERT INTO lesson_embeddings (lesson_id, embedding)
|
|
847
|
+
VALUES (?, ?)
|
|
848
|
+
`).run(lessonId, JSON.stringify(embedding));
|
|
849
|
+
}
|
|
850
|
+
function searchLessonsByEmbedding(embedding, projectPath, limit = 5) {
|
|
851
|
+
return searchLessonsByEmbeddingWithDistance(embedding, projectPath, limit).map((r) => r.lesson);
|
|
852
|
+
}
|
|
853
|
+
function searchLessonsByEmbeddingWithDistance(embedding, projectPath, limit = 5) {
|
|
854
|
+
const db2 = getDatabase();
|
|
855
|
+
const rows = db2.prepare(`
|
|
856
|
+
SELECT
|
|
857
|
+
l.*,
|
|
858
|
+
le.distance
|
|
859
|
+
FROM lesson_embeddings le
|
|
860
|
+
JOIN lessons l ON l.id = le.lesson_id
|
|
861
|
+
WHERE l.project_path = ?
|
|
862
|
+
AND l.archived = 0
|
|
863
|
+
AND le.embedding MATCH ?
|
|
864
|
+
ORDER BY le.distance ASC
|
|
865
|
+
LIMIT ?
|
|
866
|
+
`).all(projectPath, JSON.stringify(embedding), limit);
|
|
867
|
+
return rows.map((row) => ({
|
|
868
|
+
lesson: mapLessonRow(row),
|
|
869
|
+
distance: row.distance
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
function getLessonStats() {
|
|
873
|
+
const db2 = getDatabase();
|
|
874
|
+
const totalLessons = db2.prepare(`
|
|
875
|
+
SELECT COUNT(*) as count FROM lessons
|
|
876
|
+
`).get().count;
|
|
877
|
+
const activeLessons = db2.prepare(`
|
|
878
|
+
SELECT COUNT(*) as count FROM lessons WHERE archived = 0
|
|
879
|
+
`).get().count;
|
|
880
|
+
const archivedLessons = totalLessons - activeLessons;
|
|
881
|
+
const categoryRows = db2.prepare(`
|
|
882
|
+
SELECT category, COUNT(*) as count
|
|
883
|
+
FROM lessons
|
|
884
|
+
WHERE archived = 0
|
|
885
|
+
GROUP BY category
|
|
886
|
+
`).all();
|
|
887
|
+
const byCategory = {
|
|
888
|
+
architecture_decision: 0,
|
|
889
|
+
anti_pattern: 0,
|
|
890
|
+
bug_pattern: 0,
|
|
891
|
+
project_convention: 0,
|
|
892
|
+
dependency_knowledge: 0,
|
|
893
|
+
domain_knowledge: 0,
|
|
894
|
+
workflow: 0,
|
|
895
|
+
other: 0
|
|
896
|
+
};
|
|
897
|
+
for (const row of categoryRows) {
|
|
898
|
+
byCategory[row.category] = row.count;
|
|
899
|
+
}
|
|
900
|
+
const avgConfidence = db2.prepare(`
|
|
901
|
+
SELECT COALESCE(AVG(confidence), 0) as avg FROM lessons WHERE archived = 0
|
|
902
|
+
`).get().avg;
|
|
903
|
+
return {
|
|
904
|
+
totalLessons,
|
|
905
|
+
activeLessons,
|
|
906
|
+
archivedLessons,
|
|
907
|
+
byCategory,
|
|
908
|
+
avgConfidence
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/learning/LessonManager.ts
|
|
913
|
+
var VALIDATION_BOOST = 0.1;
|
|
914
|
+
var REJECTION_PENALTY = 0.2;
|
|
915
|
+
var MIN_CONFIDENCE = 0;
|
|
916
|
+
var MAX_CONFIDENCE = 1;
|
|
917
|
+
var AUTO_ARCHIVE_THRESHOLD = 0.1;
|
|
918
|
+
var DECAY_RATE = 0.05;
|
|
919
|
+
var LessonManager = class {
|
|
920
|
+
/**
|
|
921
|
+
* Create a new lesson and generate its embedding
|
|
922
|
+
*/
|
|
923
|
+
async create(input) {
|
|
924
|
+
const lesson = createLesson(input);
|
|
925
|
+
try {
|
|
926
|
+
const embeddingText = this.buildEmbeddingText(lesson);
|
|
927
|
+
const embedding = await getEmbedding(embeddingText);
|
|
928
|
+
storeLessonEmbedding(lesson.id, embedding);
|
|
929
|
+
} catch {
|
|
930
|
+
}
|
|
931
|
+
return lesson;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Update a lesson
|
|
935
|
+
*/
|
|
936
|
+
update(id, updates) {
|
|
937
|
+
return updateLesson(id, updates);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Update a lesson and regenerate its embedding
|
|
941
|
+
*/
|
|
942
|
+
async updateWithEmbedding(id, updates) {
|
|
943
|
+
const lesson = updateLesson(id, updates);
|
|
944
|
+
if (lesson) {
|
|
945
|
+
if (updates.title || updates.triggerContext || updates.insight) {
|
|
946
|
+
try {
|
|
947
|
+
const embeddingText = this.buildEmbeddingText(lesson);
|
|
948
|
+
const embedding = await getEmbedding(embeddingText);
|
|
949
|
+
storeLessonEmbedding(lesson.id, embedding);
|
|
950
|
+
} catch {
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return lesson;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Delete a lesson permanently
|
|
958
|
+
*/
|
|
959
|
+
delete(id) {
|
|
960
|
+
return deleteLesson(id);
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Archive a lesson (soft delete)
|
|
964
|
+
*/
|
|
965
|
+
archive(id) {
|
|
966
|
+
archiveLesson(id);
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Unarchive a lesson
|
|
970
|
+
*/
|
|
971
|
+
unarchive(id) {
|
|
972
|
+
unarchiveLesson(id);
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Get a lesson by ID
|
|
976
|
+
*/
|
|
977
|
+
get(id) {
|
|
978
|
+
return getLesson(id);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Get lessons for a project
|
|
982
|
+
*/
|
|
983
|
+
getByProject(projectPath, options) {
|
|
984
|
+
return getLessonsByProject(projectPath, options);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Get core lessons (high confidence, well-validated)
|
|
988
|
+
*/
|
|
989
|
+
getCore(projectPath, limit) {
|
|
990
|
+
return getCoreLessons(projectPath, limit);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Get all lessons
|
|
994
|
+
*/
|
|
995
|
+
getAll(options) {
|
|
996
|
+
return getAllLessons(options);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Record that a lesson was applied (shown to user)
|
|
1000
|
+
*/
|
|
1001
|
+
recordApplication(id) {
|
|
1002
|
+
recordLessonApplication(id);
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Record validation feedback - boosts confidence
|
|
1006
|
+
*/
|
|
1007
|
+
recordValidation(id, sessionId, comment) {
|
|
1008
|
+
recordLessonValidation(id, sessionId, comment);
|
|
1009
|
+
const lesson = getLesson(id);
|
|
1010
|
+
if (lesson) {
|
|
1011
|
+
const newConfidence = Math.min(MAX_CONFIDENCE, lesson.confidence + VALIDATION_BOOST);
|
|
1012
|
+
updateLesson(id, { confidence: newConfidence });
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Record rejection feedback - reduces confidence
|
|
1017
|
+
* Auto-archives if confidence drops too low
|
|
1018
|
+
*/
|
|
1019
|
+
recordRejection(id, sessionId, comment) {
|
|
1020
|
+
recordLessonRejection(id, sessionId, comment);
|
|
1021
|
+
const lesson = getLesson(id);
|
|
1022
|
+
if (lesson) {
|
|
1023
|
+
const newConfidence = Math.max(MIN_CONFIDENCE, lesson.confidence - REJECTION_PENALTY);
|
|
1024
|
+
if (newConfidence < AUTO_ARCHIVE_THRESHOLD) {
|
|
1025
|
+
archiveLesson(id);
|
|
1026
|
+
} else {
|
|
1027
|
+
updateLesson(id, { confidence: newConfidence });
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Decay confidence of unused lessons
|
|
1033
|
+
* Should be run periodically (e.g., weekly)
|
|
1034
|
+
*/
|
|
1035
|
+
decayUnusedLessons(daysThreshold = 30) {
|
|
1036
|
+
const lessons = getAllLessons({ archived: false });
|
|
1037
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
1038
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysThreshold);
|
|
1039
|
+
let decayedCount = 0;
|
|
1040
|
+
for (const lesson of lessons) {
|
|
1041
|
+
const lastUsed = lesson.lastAppliedAt ? new Date(lesson.lastAppliedAt) : new Date(lesson.createdAt);
|
|
1042
|
+
if (lastUsed < cutoffDate) {
|
|
1043
|
+
const newConfidence = Math.max(MIN_CONFIDENCE, lesson.confidence - DECAY_RATE);
|
|
1044
|
+
if (newConfidence < AUTO_ARCHIVE_THRESHOLD) {
|
|
1045
|
+
archiveLesson(lesson.id);
|
|
1046
|
+
} else {
|
|
1047
|
+
updateLesson(lesson.id, { confidence: newConfidence });
|
|
1048
|
+
}
|
|
1049
|
+
decayedCount++;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return decayedCount;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Build text for embedding generation
|
|
1056
|
+
*/
|
|
1057
|
+
buildEmbeddingText(lesson) {
|
|
1058
|
+
const parts = [
|
|
1059
|
+
`Title: ${lesson.title}`,
|
|
1060
|
+
`Category: ${lesson.category}`,
|
|
1061
|
+
`When to apply: ${lesson.triggerContext}`,
|
|
1062
|
+
`Insight: ${lesson.insight}`
|
|
1063
|
+
];
|
|
1064
|
+
if (lesson.reasoning) {
|
|
1065
|
+
parts.push(`Reasoning: ${lesson.reasoning}`);
|
|
1066
|
+
}
|
|
1067
|
+
return parts.join("\n\n");
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
var lessonManager = new LessonManager();
|
|
1071
|
+
|
|
1072
|
+
// src/learning/types.ts
|
|
1073
|
+
var VALID_CATEGORIES = [
|
|
1074
|
+
"architecture_decision",
|
|
1075
|
+
"anti_pattern",
|
|
1076
|
+
"bug_pattern",
|
|
1077
|
+
"project_convention",
|
|
1078
|
+
"dependency_knowledge",
|
|
1079
|
+
"domain_knowledge",
|
|
1080
|
+
"workflow",
|
|
1081
|
+
"other"
|
|
1082
|
+
];
|
|
1083
|
+
|
|
1084
|
+
// src/mcp/server.ts
|
|
1085
|
+
function createMcpServer() {
|
|
1086
|
+
const server = new Server(
|
|
1087
|
+
{
|
|
1088
|
+
name: "cmem",
|
|
1089
|
+
version: "0.1.0"
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
capabilities: {
|
|
1093
|
+
tools: {}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1098
|
+
return {
|
|
1099
|
+
tools: [
|
|
1100
|
+
{
|
|
1101
|
+
name: "search_sessions",
|
|
1102
|
+
description: "Semantic search across all saved Claude Code conversation sessions. Use this to find past conversations about specific topics, projects, or problems. Returns matching sessions ranked by relevance.",
|
|
1103
|
+
inputSchema: {
|
|
1104
|
+
type: "object",
|
|
1105
|
+
properties: {
|
|
1106
|
+
query: {
|
|
1107
|
+
type: "string",
|
|
1108
|
+
description: 'Natural language search query. Examples: "React hooks discussion", "database migration", "authentication implementation"'
|
|
1109
|
+
},
|
|
1110
|
+
limit: {
|
|
1111
|
+
type: "number",
|
|
1112
|
+
description: "Maximum number of results to return (default: 5)"
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
required: ["query"]
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
name: "list_sessions",
|
|
1120
|
+
description: "List all saved Claude Code conversation sessions, ordered by most recently updated. Use this to browse available sessions or find recent conversations.",
|
|
1121
|
+
inputSchema: {
|
|
1122
|
+
type: "object",
|
|
1123
|
+
properties: {
|
|
1124
|
+
limit: {
|
|
1125
|
+
type: "number",
|
|
1126
|
+
description: "Maximum number of sessions to return (default: 10)"
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
name: "get_session",
|
|
1133
|
+
description: "Get detailed information about a specific conversation session, including its full message history. Use this after finding a session via search or list.",
|
|
1134
|
+
inputSchema: {
|
|
1135
|
+
type: "object",
|
|
1136
|
+
properties: {
|
|
1137
|
+
sessionId: {
|
|
1138
|
+
type: "string",
|
|
1139
|
+
description: "The session ID to retrieve"
|
|
1140
|
+
},
|
|
1141
|
+
includeMessages: {
|
|
1142
|
+
type: "boolean",
|
|
1143
|
+
description: "Whether to include the full message history (default: true)"
|
|
1144
|
+
},
|
|
1145
|
+
messageLimit: {
|
|
1146
|
+
type: "number",
|
|
1147
|
+
description: "Maximum number of messages to include (default: 50, from most recent)"
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
1150
|
+
required: ["sessionId"]
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
name: "get_session_context",
|
|
1155
|
+
description: "Get a formatted context summary from a past session that can be used to continue or reference that conversation. Returns key information and recent messages in a readable format.",
|
|
1156
|
+
inputSchema: {
|
|
1157
|
+
type: "object",
|
|
1158
|
+
properties: {
|
|
1159
|
+
sessionId: {
|
|
1160
|
+
type: "string",
|
|
1161
|
+
description: "The session ID to get context from"
|
|
1162
|
+
},
|
|
1163
|
+
messageCount: {
|
|
1164
|
+
type: "number",
|
|
1165
|
+
description: "Number of recent messages to include (default: 10)"
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
required: ["sessionId"]
|
|
1169
|
+
}
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
name: "search_and_summarize",
|
|
1173
|
+
description: "Search past Claude Code sessions and get an AI-generated summary tailored to your specific question. This spawns a separate Claude instance to read and synthesize the relevant sessions, keeping your main context clean. Use this when you need insights from past conversations.",
|
|
1174
|
+
inputSchema: {
|
|
1175
|
+
type: "object",
|
|
1176
|
+
properties: {
|
|
1177
|
+
query: {
|
|
1178
|
+
type: "string",
|
|
1179
|
+
description: 'Your question or topic to search for. Examples: "What did we decide about the database schema?", "How did we implement authentication?", "What was the bug fix for the login issue?"'
|
|
1180
|
+
},
|
|
1181
|
+
sessionLimit: {
|
|
1182
|
+
type: "number",
|
|
1183
|
+
description: "Maximum number of sessions to analyze (default: 3)"
|
|
1184
|
+
},
|
|
1185
|
+
model: {
|
|
1186
|
+
type: "string",
|
|
1187
|
+
enum: ["haiku", "sonnet", "opus"],
|
|
1188
|
+
description: "Model to use for summarization (default: haiku for speed)"
|
|
1189
|
+
}
|
|
1190
|
+
},
|
|
1191
|
+
required: ["query"]
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
// ==================== LESSON TOOLS ====================
|
|
1195
|
+
{
|
|
1196
|
+
name: "search_lessons",
|
|
1197
|
+
description: "Search learned lessons for a project using semantic search. Returns lessons that match the query context.",
|
|
1198
|
+
inputSchema: {
|
|
1199
|
+
type: "object",
|
|
1200
|
+
properties: {
|
|
1201
|
+
query: {
|
|
1202
|
+
type: "string",
|
|
1203
|
+
description: "Search query describing what you want to find"
|
|
1204
|
+
},
|
|
1205
|
+
projectPath: {
|
|
1206
|
+
type: "string",
|
|
1207
|
+
description: "Project path to search lessons for (defaults to current directory)"
|
|
1208
|
+
},
|
|
1209
|
+
limit: {
|
|
1210
|
+
type: "number",
|
|
1211
|
+
description: "Maximum number of results (default: 5)"
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
required: ["query"]
|
|
1215
|
+
}
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
name: "get_lesson",
|
|
1219
|
+
description: "Get details of a specific learned lesson by ID.",
|
|
1220
|
+
inputSchema: {
|
|
1221
|
+
type: "object",
|
|
1222
|
+
properties: {
|
|
1223
|
+
lessonId: {
|
|
1224
|
+
type: "string",
|
|
1225
|
+
description: "The lesson ID to retrieve"
|
|
1226
|
+
}
|
|
1227
|
+
},
|
|
1228
|
+
required: ["lessonId"]
|
|
1229
|
+
}
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
name: "save_lesson",
|
|
1233
|
+
description: "Manually save a new lesson. Use this when you learn something important about a project that should be remembered.",
|
|
1234
|
+
inputSchema: {
|
|
1235
|
+
type: "object",
|
|
1236
|
+
properties: {
|
|
1237
|
+
projectPath: {
|
|
1238
|
+
type: "string",
|
|
1239
|
+
description: "Project path this lesson applies to"
|
|
1240
|
+
},
|
|
1241
|
+
category: {
|
|
1242
|
+
type: "string",
|
|
1243
|
+
enum: VALID_CATEGORIES,
|
|
1244
|
+
description: "Category of the lesson"
|
|
1245
|
+
},
|
|
1246
|
+
title: {
|
|
1247
|
+
type: "string",
|
|
1248
|
+
description: "Short title (max 60 chars)"
|
|
1249
|
+
},
|
|
1250
|
+
triggerContext: {
|
|
1251
|
+
type: "string",
|
|
1252
|
+
description: "When this lesson should be surfaced"
|
|
1253
|
+
},
|
|
1254
|
+
insight: {
|
|
1255
|
+
type: "string",
|
|
1256
|
+
description: "The actual knowledge (specific, actionable)"
|
|
1257
|
+
},
|
|
1258
|
+
reasoning: {
|
|
1259
|
+
type: "string",
|
|
1260
|
+
description: "Why this is true (optional)"
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
required: ["projectPath", "category", "title", "triggerContext", "insight"]
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
name: "validate_lesson",
|
|
1268
|
+
description: "Mark a lesson as helpful/correct. Boosts its confidence score.",
|
|
1269
|
+
inputSchema: {
|
|
1270
|
+
type: "object",
|
|
1271
|
+
properties: {
|
|
1272
|
+
lessonId: {
|
|
1273
|
+
type: "string",
|
|
1274
|
+
description: "The lesson ID to validate"
|
|
1275
|
+
},
|
|
1276
|
+
comment: {
|
|
1277
|
+
type: "string",
|
|
1278
|
+
description: "Optional comment about why it was helpful"
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
required: ["lessonId"]
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
name: "reject_lesson",
|
|
1286
|
+
description: "Mark a lesson as unhelpful/incorrect. Reduces its confidence score.",
|
|
1287
|
+
inputSchema: {
|
|
1288
|
+
type: "object",
|
|
1289
|
+
properties: {
|
|
1290
|
+
lessonId: {
|
|
1291
|
+
type: "string",
|
|
1292
|
+
description: "The lesson ID to reject"
|
|
1293
|
+
},
|
|
1294
|
+
comment: {
|
|
1295
|
+
type: "string",
|
|
1296
|
+
description: "Optional comment about why it was wrong"
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
required: ["lessonId"]
|
|
1300
|
+
}
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
name: "list_lessons",
|
|
1304
|
+
description: "List all lessons for a project.",
|
|
1305
|
+
inputSchema: {
|
|
1306
|
+
type: "object",
|
|
1307
|
+
properties: {
|
|
1308
|
+
projectPath: {
|
|
1309
|
+
type: "string",
|
|
1310
|
+
description: "Project path to list lessons for"
|
|
1311
|
+
},
|
|
1312
|
+
category: {
|
|
1313
|
+
type: "string",
|
|
1314
|
+
enum: VALID_CATEGORIES,
|
|
1315
|
+
description: "Filter by category"
|
|
1316
|
+
},
|
|
1317
|
+
includeArchived: {
|
|
1318
|
+
type: "boolean",
|
|
1319
|
+
description: "Include archived lessons (default: false)"
|
|
1320
|
+
},
|
|
1321
|
+
limit: {
|
|
1322
|
+
type: "number",
|
|
1323
|
+
description: "Maximum number of results (default: 20)"
|
|
1324
|
+
}
|
|
1325
|
+
},
|
|
1326
|
+
required: ["projectPath"]
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
]
|
|
1330
|
+
};
|
|
1331
|
+
});
|
|
1332
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1333
|
+
const { name, arguments: args } = request.params;
|
|
1334
|
+
try {
|
|
1335
|
+
switch (name) {
|
|
1336
|
+
case "search_sessions":
|
|
1337
|
+
return await handleSearchSessions(args);
|
|
1338
|
+
case "list_sessions":
|
|
1339
|
+
return await handleListSessions(args);
|
|
1340
|
+
case "get_session":
|
|
1341
|
+
return await handleGetSession(
|
|
1342
|
+
args
|
|
1343
|
+
);
|
|
1344
|
+
case "get_session_context":
|
|
1345
|
+
return await handleGetSessionContext(
|
|
1346
|
+
args
|
|
1347
|
+
);
|
|
1348
|
+
case "search_and_summarize":
|
|
1349
|
+
return await handleSearchAndSummarize(
|
|
1350
|
+
args
|
|
1351
|
+
);
|
|
1352
|
+
// ==================== LESSON HANDLERS ====================
|
|
1353
|
+
case "search_lessons":
|
|
1354
|
+
return await handleSearchLessons(
|
|
1355
|
+
args
|
|
1356
|
+
);
|
|
1357
|
+
case "get_lesson":
|
|
1358
|
+
return await handleGetLesson(args);
|
|
1359
|
+
case "save_lesson":
|
|
1360
|
+
return await handleSaveLesson(
|
|
1361
|
+
args
|
|
1362
|
+
);
|
|
1363
|
+
case "validate_lesson":
|
|
1364
|
+
return await handleValidateLesson(
|
|
1365
|
+
args
|
|
1366
|
+
);
|
|
1367
|
+
case "reject_lesson":
|
|
1368
|
+
return await handleRejectLesson(
|
|
1369
|
+
args
|
|
1370
|
+
);
|
|
1371
|
+
case "list_lessons":
|
|
1372
|
+
return await handleListLessons(
|
|
1373
|
+
args
|
|
1374
|
+
);
|
|
1375
|
+
default:
|
|
1376
|
+
return {
|
|
1377
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
1378
|
+
isError: true
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
return {
|
|
1383
|
+
content: [{ type: "text", text: `Error: ${String(error)}` }],
|
|
1384
|
+
isError: true
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
return server;
|
|
1389
|
+
}
|
|
1390
|
+
async function handleSearchSessions(args) {
|
|
1391
|
+
const { query, limit = 5 } = args;
|
|
1392
|
+
if (!isReady()) {
|
|
1393
|
+
try {
|
|
1394
|
+
await initializeEmbeddings();
|
|
1395
|
+
} catch {
|
|
1396
|
+
const allSessions = listSessions(100);
|
|
1397
|
+
const queryLower = query.toLowerCase();
|
|
1398
|
+
const filtered = allSessions.filter(
|
|
1399
|
+
(s) => s.title.toLowerCase().includes(queryLower) || s.summary && s.summary.toLowerCase().includes(queryLower)
|
|
1400
|
+
).slice(0, limit);
|
|
1401
|
+
return {
|
|
1402
|
+
content: [
|
|
1403
|
+
{
|
|
1404
|
+
type: "text",
|
|
1405
|
+
text: formatSessionList(filtered, `Text search results for "${query}" (embeddings unavailable)`)
|
|
1406
|
+
}
|
|
1407
|
+
]
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const queryEmbedding = await getEmbedding(query);
|
|
1412
|
+
const results = searchSessions(queryEmbedding, limit);
|
|
1413
|
+
return {
|
|
1414
|
+
content: [
|
|
1415
|
+
{
|
|
1416
|
+
type: "text",
|
|
1417
|
+
text: formatSessionList(results, `Semantic search results for "${query}"`)
|
|
1418
|
+
}
|
|
1419
|
+
]
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
async function handleListSessions(args) {
|
|
1423
|
+
const { limit = 10 } = args;
|
|
1424
|
+
const sessions = listSessions(limit);
|
|
1425
|
+
return {
|
|
1426
|
+
content: [
|
|
1427
|
+
{
|
|
1428
|
+
type: "text",
|
|
1429
|
+
text: formatSessionList(sessions, "Recent sessions")
|
|
1430
|
+
}
|
|
1431
|
+
]
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
async function handleGetSession(args) {
|
|
1435
|
+
const { sessionId, includeMessages = true, messageLimit = 50 } = args;
|
|
1436
|
+
let session = getSession(sessionId) || getSessionByIdPrefix(sessionId);
|
|
1437
|
+
if (!session) {
|
|
1438
|
+
return {
|
|
1439
|
+
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
1440
|
+
isError: true
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
const lines = [
|
|
1444
|
+
`# Session: ${session.title}`,
|
|
1445
|
+
"",
|
|
1446
|
+
`**ID:** ${session.id}`,
|
|
1447
|
+
`**Created:** ${session.createdAt}`,
|
|
1448
|
+
`**Updated:** ${session.updatedAt} (${formatTimeAgo(session.updatedAt)})`,
|
|
1449
|
+
`**Messages:** ${session.messageCount}`
|
|
1450
|
+
];
|
|
1451
|
+
if (session.projectPath) {
|
|
1452
|
+
lines.push(`**Project:** ${session.projectPath}`);
|
|
1453
|
+
}
|
|
1454
|
+
if (session.summary) {
|
|
1455
|
+
lines.push("", "## Summary", session.summary);
|
|
1456
|
+
}
|
|
1457
|
+
if (includeMessages) {
|
|
1458
|
+
const messages = getSessionMessages(session.id);
|
|
1459
|
+
const recentMessages = messages.slice(-messageLimit);
|
|
1460
|
+
lines.push("", "## Messages", "");
|
|
1461
|
+
for (const msg of recentMessages) {
|
|
1462
|
+
lines.push(`### ${msg.role === "user" ? "User" : "Assistant"}`);
|
|
1463
|
+
lines.push(msg.content);
|
|
1464
|
+
lines.push("");
|
|
1465
|
+
}
|
|
1466
|
+
if (messages.length > messageLimit) {
|
|
1467
|
+
lines.push(`_...${messages.length - messageLimit} earlier messages omitted_`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return {
|
|
1471
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
async function handleGetSessionContext(args) {
|
|
1475
|
+
const { sessionId, messageCount = 10 } = args;
|
|
1476
|
+
let session = getSession(sessionId) || getSessionByIdPrefix(sessionId);
|
|
1477
|
+
if (!session) {
|
|
1478
|
+
return {
|
|
1479
|
+
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
1480
|
+
isError: true
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
const messages = getSessionMessages(session.id);
|
|
1484
|
+
const recentMessages = messages.slice(-messageCount);
|
|
1485
|
+
const lines = [
|
|
1486
|
+
"# Context from Previous Session",
|
|
1487
|
+
"",
|
|
1488
|
+
`**Topic:** ${session.title}`
|
|
1489
|
+
];
|
|
1490
|
+
if (session.projectPath) {
|
|
1491
|
+
lines.push(`**Project:** ${session.projectPath}`);
|
|
1492
|
+
}
|
|
1493
|
+
if (session.summary) {
|
|
1494
|
+
lines.push("", "## Summary", session.summary);
|
|
1495
|
+
}
|
|
1496
|
+
lines.push(
|
|
1497
|
+
"",
|
|
1498
|
+
"## Recent Conversation",
|
|
1499
|
+
`_Last ${recentMessages.length} of ${messages.length} messages_`,
|
|
1500
|
+
""
|
|
1501
|
+
);
|
|
1502
|
+
for (const msg of recentMessages) {
|
|
1503
|
+
const role = msg.role === "user" ? "**User:**" : "**Assistant:**";
|
|
1504
|
+
lines.push(role);
|
|
1505
|
+
lines.push(truncate(msg.content, 2e3));
|
|
1506
|
+
lines.push("");
|
|
1507
|
+
}
|
|
1508
|
+
return {
|
|
1509
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
async function handleSearchAndSummarize(args) {
|
|
1513
|
+
const { query, sessionLimit = 3, model = "haiku" } = args;
|
|
1514
|
+
if (!isClaudeCliAvailable()) {
|
|
1515
|
+
return {
|
|
1516
|
+
content: [
|
|
1517
|
+
{
|
|
1518
|
+
type: "text",
|
|
1519
|
+
text: "Claude CLI not found. This tool requires Claude Code CLI to be installed."
|
|
1520
|
+
}
|
|
1521
|
+
],
|
|
1522
|
+
isError: true
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
if (!isReady()) {
|
|
1526
|
+
try {
|
|
1527
|
+
await initializeEmbeddings();
|
|
1528
|
+
} catch {
|
|
1529
|
+
return {
|
|
1530
|
+
content: [
|
|
1531
|
+
{
|
|
1532
|
+
type: "text",
|
|
1533
|
+
text: "Embedding model not available. Please run `cmem setup` first."
|
|
1534
|
+
}
|
|
1535
|
+
],
|
|
1536
|
+
isError: true
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
const queryEmbedding = await getEmbedding(query);
|
|
1541
|
+
const sessions = searchSessions(queryEmbedding, sessionLimit);
|
|
1542
|
+
if (sessions.length === 0) {
|
|
1543
|
+
return {
|
|
1544
|
+
content: [
|
|
1545
|
+
{
|
|
1546
|
+
type: "text",
|
|
1547
|
+
text: `No relevant sessions found for: "${query}"`
|
|
1548
|
+
}
|
|
1549
|
+
]
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
const sessionContents = [];
|
|
1553
|
+
for (const session of sessions) {
|
|
1554
|
+
const messages = getSessionMessages(session.id);
|
|
1555
|
+
if (messages.length === 0) continue;
|
|
1556
|
+
const sessionText = [
|
|
1557
|
+
`## Session: ${session.title}`,
|
|
1558
|
+
`Project: ${session.projectPath || "Unknown"}`,
|
|
1559
|
+
`Date: ${session.updatedAt}`,
|
|
1560
|
+
"",
|
|
1561
|
+
...messages.slice(-20).map((m) => `**${m.role}:** ${truncate(m.content, 1500)}`)
|
|
1562
|
+
].join("\n");
|
|
1563
|
+
sessionContents.push(sessionText);
|
|
1564
|
+
}
|
|
1565
|
+
if (sessionContents.length === 0) {
|
|
1566
|
+
return {
|
|
1567
|
+
content: [
|
|
1568
|
+
{
|
|
1569
|
+
type: "text",
|
|
1570
|
+
text: `Found ${sessions.length} sessions but they appear to be empty.`
|
|
1571
|
+
}
|
|
1572
|
+
]
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const prompt = `You are analyzing past Claude Code conversation sessions to answer a user's question.
|
|
1576
|
+
|
|
1577
|
+
<user_question>
|
|
1578
|
+
${query}
|
|
1579
|
+
</user_question>
|
|
1580
|
+
|
|
1581
|
+
<past_sessions>
|
|
1582
|
+
${sessionContents.join("\n\n---\n\n")}
|
|
1583
|
+
</past_sessions>
|
|
1584
|
+
|
|
1585
|
+
Based on the past sessions above, provide a concise and helpful answer to the user's question. Focus on:
|
|
1586
|
+
1. Directly answering their question with specific details from the conversations
|
|
1587
|
+
2. Mentioning which session(s) the information came from
|
|
1588
|
+
3. Highlighting any relevant decisions, code snippets, or conclusions
|
|
1589
|
+
|
|
1590
|
+
If the sessions don't contain relevant information to answer the question, say so clearly.
|
|
1591
|
+
|
|
1592
|
+
Keep your response concise but complete.`;
|
|
1593
|
+
const response = await runClaudePrompt(prompt, { model });
|
|
1594
|
+
if (!response.success) {
|
|
1595
|
+
return {
|
|
1596
|
+
content: [
|
|
1597
|
+
{
|
|
1598
|
+
type: "text",
|
|
1599
|
+
text: `Error generating summary: ${response.error || "Unknown error"}`
|
|
1600
|
+
}
|
|
1601
|
+
],
|
|
1602
|
+
isError: true
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
const resultLines = [
|
|
1606
|
+
response.content,
|
|
1607
|
+
"",
|
|
1608
|
+
"---",
|
|
1609
|
+
`*Analyzed ${sessions.length} session(s) | Model: ${model}${response.durationMs ? ` | ${(response.durationMs / 1e3).toFixed(1)}s` : ""}${response.outputTokens ? ` | ${response.outputTokens} tokens` : ""}*`
|
|
1610
|
+
];
|
|
1611
|
+
return {
|
|
1612
|
+
content: [{ type: "text", text: resultLines.join("\n") }]
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
function formatSessionList(sessions, header) {
|
|
1616
|
+
if (sessions.length === 0) {
|
|
1617
|
+
return `${header}
|
|
1618
|
+
|
|
1619
|
+
No sessions found.`;
|
|
1620
|
+
}
|
|
1621
|
+
const lines = [header, ""];
|
|
1622
|
+
for (const session of sessions) {
|
|
1623
|
+
lines.push(`### ${session.title}`);
|
|
1624
|
+
lines.push(`- **ID:** \`${session.id.slice(0, 8)}\` (use this to get full session)`);
|
|
1625
|
+
lines.push(`- **Messages:** ${session.messageCount}`);
|
|
1626
|
+
lines.push(`- **Updated:** ${formatTimeAgo(session.updatedAt)}`);
|
|
1627
|
+
if (session.projectPath) {
|
|
1628
|
+
lines.push(`- **Project:** ${session.projectPath}`);
|
|
1629
|
+
}
|
|
1630
|
+
if (session.summary) {
|
|
1631
|
+
lines.push(`- **Summary:** ${truncate(session.summary, 150)}`);
|
|
1632
|
+
}
|
|
1633
|
+
lines.push("");
|
|
1634
|
+
}
|
|
1635
|
+
return lines.join("\n");
|
|
1636
|
+
}
|
|
1637
|
+
async function handleSearchLessons(args) {
|
|
1638
|
+
const { query, projectPath = process.cwd(), limit = 5 } = args;
|
|
1639
|
+
if (!isReady()) {
|
|
1640
|
+
try {
|
|
1641
|
+
await initializeEmbeddings();
|
|
1642
|
+
} catch {
|
|
1643
|
+
return {
|
|
1644
|
+
content: [
|
|
1645
|
+
{
|
|
1646
|
+
type: "text",
|
|
1647
|
+
text: "Embedding model not available. Cannot perform semantic search."
|
|
1648
|
+
}
|
|
1649
|
+
],
|
|
1650
|
+
isError: true
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const queryEmbedding = await getEmbedding(query);
|
|
1655
|
+
const lessons = searchLessonsByEmbedding(queryEmbedding, projectPath, limit);
|
|
1656
|
+
if (lessons.length === 0) {
|
|
1657
|
+
return {
|
|
1658
|
+
content: [
|
|
1659
|
+
{
|
|
1660
|
+
type: "text",
|
|
1661
|
+
text: `No lessons found for query: "${query}" in project: ${projectPath}`
|
|
1662
|
+
}
|
|
1663
|
+
]
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
const lines = [`# Lessons matching "${query}"`, ""];
|
|
1667
|
+
for (const lesson of lessons) {
|
|
1668
|
+
lines.push(`### ${lesson.title}`);
|
|
1669
|
+
lines.push(`- **ID:** \`${lesson.id.slice(0, 8)}\``);
|
|
1670
|
+
lines.push(`- **Category:** ${lesson.category}`);
|
|
1671
|
+
lines.push(`- **Confidence:** ${(lesson.confidence * 100).toFixed(0)}%`);
|
|
1672
|
+
lines.push(`- **When to apply:** ${lesson.triggerContext}`);
|
|
1673
|
+
lines.push("");
|
|
1674
|
+
lines.push(`**Insight:** ${lesson.insight}`);
|
|
1675
|
+
if (lesson.reasoning) {
|
|
1676
|
+
lines.push(`**Reasoning:** ${lesson.reasoning}`);
|
|
1677
|
+
}
|
|
1678
|
+
lines.push("");
|
|
1679
|
+
}
|
|
1680
|
+
return {
|
|
1681
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
async function handleGetLesson(args) {
|
|
1685
|
+
const { lessonId } = args;
|
|
1686
|
+
const lesson = getLesson(lessonId);
|
|
1687
|
+
if (!lesson) {
|
|
1688
|
+
return {
|
|
1689
|
+
content: [{ type: "text", text: `Lesson not found: ${lessonId}` }],
|
|
1690
|
+
isError: true
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
const lines = [
|
|
1694
|
+
`# ${lesson.title}`,
|
|
1695
|
+
"",
|
|
1696
|
+
`**ID:** ${lesson.id}`,
|
|
1697
|
+
`**Category:** ${lesson.category}`,
|
|
1698
|
+
`**Project:** ${lesson.projectPath}`,
|
|
1699
|
+
`**Confidence:** ${(lesson.confidence * 100).toFixed(0)}%`,
|
|
1700
|
+
`**Times Applied:** ${lesson.timesApplied}`,
|
|
1701
|
+
`**Times Validated:** ${lesson.timesValidated}`,
|
|
1702
|
+
`**Times Rejected:** ${lesson.timesRejected}`,
|
|
1703
|
+
`**Archived:** ${lesson.archived ? "Yes" : "No"}`,
|
|
1704
|
+
`**Created:** ${lesson.createdAt}`,
|
|
1705
|
+
`**Updated:** ${lesson.updatedAt}`,
|
|
1706
|
+
"",
|
|
1707
|
+
"## When to Apply",
|
|
1708
|
+
lesson.triggerContext,
|
|
1709
|
+
"",
|
|
1710
|
+
"## Insight",
|
|
1711
|
+
lesson.insight
|
|
1712
|
+
];
|
|
1713
|
+
if (lesson.reasoning) {
|
|
1714
|
+
lines.push("", "## Reasoning", lesson.reasoning);
|
|
1715
|
+
}
|
|
1716
|
+
if (lesson.sourceSessionId) {
|
|
1717
|
+
lines.push("", `**Source Session:** ${lesson.sourceSessionId}`);
|
|
1718
|
+
}
|
|
1719
|
+
return {
|
|
1720
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
async function handleSaveLesson(args) {
|
|
1724
|
+
const input = {
|
|
1725
|
+
projectPath: args.projectPath,
|
|
1726
|
+
category: args.category,
|
|
1727
|
+
title: args.title,
|
|
1728
|
+
triggerContext: args.triggerContext,
|
|
1729
|
+
insight: args.insight,
|
|
1730
|
+
reasoning: args.reasoning,
|
|
1731
|
+
sourceType: "manual",
|
|
1732
|
+
confidence: 0.6
|
|
1733
|
+
// Manual lessons start with moderate confidence
|
|
1734
|
+
};
|
|
1735
|
+
try {
|
|
1736
|
+
const lesson = await lessonManager.create(input);
|
|
1737
|
+
return {
|
|
1738
|
+
content: [
|
|
1739
|
+
{
|
|
1740
|
+
type: "text",
|
|
1741
|
+
text: `Lesson saved successfully!
|
|
1742
|
+
|
|
1743
|
+
**ID:** ${lesson.id}
|
|
1744
|
+
**Title:** ${lesson.title}`
|
|
1745
|
+
}
|
|
1746
|
+
]
|
|
1747
|
+
};
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
return {
|
|
1750
|
+
content: [
|
|
1751
|
+
{
|
|
1752
|
+
type: "text",
|
|
1753
|
+
text: `Failed to save lesson: ${String(error)}`
|
|
1754
|
+
}
|
|
1755
|
+
],
|
|
1756
|
+
isError: true
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
async function handleValidateLesson(args) {
|
|
1761
|
+
const { lessonId, comment } = args;
|
|
1762
|
+
const lesson = getLesson(lessonId);
|
|
1763
|
+
if (!lesson) {
|
|
1764
|
+
return {
|
|
1765
|
+
content: [{ type: "text", text: `Lesson not found: ${lessonId}` }],
|
|
1766
|
+
isError: true
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
lessonManager.recordValidation(lessonId, void 0, comment);
|
|
1770
|
+
const updated = getLesson(lessonId);
|
|
1771
|
+
return {
|
|
1772
|
+
content: [
|
|
1773
|
+
{
|
|
1774
|
+
type: "text",
|
|
1775
|
+
text: `Lesson validated: "${lesson.title}"
|
|
1776
|
+
New confidence: ${((updated?.confidence ?? 0) * 100).toFixed(0)}%`
|
|
1777
|
+
}
|
|
1778
|
+
]
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
async function handleRejectLesson(args) {
|
|
1782
|
+
const { lessonId, comment } = args;
|
|
1783
|
+
const lesson = getLesson(lessonId);
|
|
1784
|
+
if (!lesson) {
|
|
1785
|
+
return {
|
|
1786
|
+
content: [{ type: "text", text: `Lesson not found: ${lessonId}` }],
|
|
1787
|
+
isError: true
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
lessonManager.recordRejection(lessonId, void 0, comment);
|
|
1791
|
+
const updated = getLesson(lessonId);
|
|
1792
|
+
let message = `Lesson rejected: "${lesson.title}"`;
|
|
1793
|
+
if (updated?.archived) {
|
|
1794
|
+
message += "\nLesson has been auto-archived due to low confidence.";
|
|
1795
|
+
} else if (updated) {
|
|
1796
|
+
message += `
|
|
1797
|
+
New confidence: ${(updated.confidence * 100).toFixed(0)}%`;
|
|
1798
|
+
}
|
|
1799
|
+
return {
|
|
1800
|
+
content: [{ type: "text", text: message }]
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
async function handleListLessons(args) {
|
|
1804
|
+
const { projectPath, category, includeArchived = false, limit = 20 } = args;
|
|
1805
|
+
const lessons = getLessonsByProject(projectPath, {
|
|
1806
|
+
category,
|
|
1807
|
+
archived: includeArchived ? void 0 : false,
|
|
1808
|
+
limit
|
|
1809
|
+
});
|
|
1810
|
+
if (lessons.length === 0) {
|
|
1811
|
+
return {
|
|
1812
|
+
content: [
|
|
1813
|
+
{
|
|
1814
|
+
type: "text",
|
|
1815
|
+
text: `No lessons found for project: ${projectPath}`
|
|
1816
|
+
}
|
|
1817
|
+
]
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
const stats = getLessonStats();
|
|
1821
|
+
const lines = [
|
|
1822
|
+
`# Lessons for ${projectPath}`,
|
|
1823
|
+
"",
|
|
1824
|
+
`**Total:** ${lessons.length} | **Active:** ${stats.activeLessons} | **Archived:** ${stats.archivedLessons}`,
|
|
1825
|
+
""
|
|
1826
|
+
];
|
|
1827
|
+
for (const lesson of lessons) {
|
|
1828
|
+
const archived = lesson.archived ? " [ARCHIVED]" : "";
|
|
1829
|
+
lines.push(`### ${lesson.title}${archived}`);
|
|
1830
|
+
lines.push(`- **ID:** \`${lesson.id.slice(0, 8)}\``);
|
|
1831
|
+
lines.push(`- **Category:** ${lesson.category}`);
|
|
1832
|
+
lines.push(`- **Confidence:** ${(lesson.confidence * 100).toFixed(0)}%`);
|
|
1833
|
+
lines.push(`- **Applied:** ${lesson.timesApplied} | **Validated:** ${lesson.timesValidated} | **Rejected:** ${lesson.timesRejected}`);
|
|
1834
|
+
lines.push(`- **Insight:** ${truncate(lesson.insight, 100)}`);
|
|
1835
|
+
lines.push("");
|
|
1836
|
+
}
|
|
1837
|
+
return {
|
|
1838
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
async function startMcpServer() {
|
|
1842
|
+
const server = createMcpServer();
|
|
1843
|
+
const transport = new StdioServerTransport();
|
|
1844
|
+
await server.connect(transport);
|
|
1845
|
+
}
|
|
1846
|
+
export {
|
|
1847
|
+
createMcpServer,
|
|
1848
|
+
startMcpServer
|
|
1849
|
+
};
|
|
1850
|
+
//# sourceMappingURL=server.js.map
|