compound-agent 1.3.3 → 1.4.0

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/dist/index.js CHANGED
@@ -1,15 +1,501 @@
1
1
  import { createRequire } from 'module';
2
- import { mkdir, appendFile, readFile } from 'fs/promises';
3
- import { join, dirname, relative } from 'path';
2
+ import { mkdirSync, unlinkSync, existsSync, statSync, readFileSync, readdirSync } from 'fs';
3
+ import { join, dirname, extname, relative } from 'path';
4
4
  import { createHash } from 'crypto';
5
+ import { readFile, mkdir, appendFile, readdir } from 'fs/promises';
5
6
  import { z } from 'zod';
6
- import { existsSync, mkdirSync, unlinkSync, statSync, readFileSync, readdirSync } from 'fs';
7
7
  import { getLlama, resolveModelFile } from 'node-llama-cpp';
8
8
  import { homedir } from 'os';
9
9
  import { execSync } from 'child_process';
10
+ import 'url';
10
11
  import 'chalk';
11
12
 
12
- // src/version.ts
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __esm = (fn, res) => function __init() {
15
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
16
+ };
17
+ function ensureSqliteAvailable() {
18
+ if (checked) return;
19
+ try {
20
+ const module = require2("better-sqlite3");
21
+ const Constructor = module.default || module;
22
+ const testDb = new Constructor(":memory:");
23
+ testDb.close();
24
+ DatabaseConstructor = Constructor;
25
+ checked = true;
26
+ } catch (cause) {
27
+ throw new Error(
28
+ 'better-sqlite3 failed to load.\nRun: npx ca setup (auto-configures pnpm native builds)\nOr manually add to your package.json:\n "pnpm": { "onlyBuiltDependencies": ["better-sqlite3", "node-llama-cpp"] }\nThen run: pnpm install && pnpm rebuild better-sqlite3\nFor npm/yarn, run: npm rebuild better-sqlite3',
29
+ { cause }
30
+ );
31
+ }
32
+ }
33
+ function getDatabaseConstructor() {
34
+ ensureSqliteAvailable();
35
+ return DatabaseConstructor;
36
+ }
37
+ var require2, checked, DatabaseConstructor;
38
+ var init_availability = __esm({
39
+ "src/memory/storage/sqlite/availability.ts"() {
40
+ require2 = createRequire(import.meta.url);
41
+ checked = false;
42
+ DatabaseConstructor = null;
43
+ }
44
+ });
45
+
46
+ // src/memory/storage/sqlite-knowledge/schema.ts
47
+ function createKnowledgeSchema(database) {
48
+ database.exec(SCHEMA_SQL2);
49
+ database.pragma(`user_version = ${KNOWLEDGE_SCHEMA_VERSION}`);
50
+ }
51
+ var KNOWLEDGE_SCHEMA_VERSION, SCHEMA_SQL2;
52
+ var init_schema = __esm({
53
+ "src/memory/storage/sqlite-knowledge/schema.ts"() {
54
+ KNOWLEDGE_SCHEMA_VERSION = 2;
55
+ SCHEMA_SQL2 = `
56
+ CREATE TABLE IF NOT EXISTS chunks (
57
+ id TEXT PRIMARY KEY,
58
+ file_path TEXT NOT NULL,
59
+ start_line INTEGER NOT NULL,
60
+ end_line INTEGER NOT NULL,
61
+ content_hash TEXT NOT NULL,
62
+ text TEXT NOT NULL,
63
+ embedding BLOB,
64
+ model TEXT,
65
+ updated_at TEXT NOT NULL
66
+ );
67
+
68
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
69
+ text,
70
+ content='chunks', content_rowid='rowid'
71
+ );
72
+
73
+ CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
74
+ INSERT INTO chunks_fts(rowid, text)
75
+ VALUES (new.rowid, new.text);
76
+ END;
77
+
78
+ CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
79
+ INSERT INTO chunks_fts(chunks_fts, rowid, text)
80
+ VALUES ('delete', old.rowid, old.text);
81
+ END;
82
+
83
+ CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN
84
+ INSERT INTO chunks_fts(chunks_fts, rowid, text)
85
+ VALUES ('delete', old.rowid, old.text);
86
+ INSERT INTO chunks_fts(rowid, text)
87
+ VALUES (new.rowid, new.text);
88
+ END;
89
+
90
+ CREATE INDEX IF NOT EXISTS idx_chunks_file_path ON chunks(file_path);
91
+
92
+ CREATE TABLE IF NOT EXISTS metadata (
93
+ key TEXT PRIMARY KEY,
94
+ value TEXT NOT NULL
95
+ );
96
+ `;
97
+ }
98
+ });
99
+ function openKnowledgeDb(repoRoot, options = {}) {
100
+ const { inMemory = false } = options;
101
+ const key = inMemory ? `:memory:${repoRoot}` : join(repoRoot, KNOWLEDGE_DB_PATH);
102
+ const cached = knowledgeDbMap.get(key);
103
+ if (cached) {
104
+ return cached;
105
+ }
106
+ const Database = getDatabaseConstructor();
107
+ let database;
108
+ if (inMemory) {
109
+ database = new Database(":memory:");
110
+ } else {
111
+ const dir = dirname(key);
112
+ mkdirSync(dir, { recursive: true });
113
+ database = new Database(key);
114
+ const version = database.pragma("user_version", { simple: true });
115
+ if (version !== 0 && version !== KNOWLEDGE_SCHEMA_VERSION) {
116
+ database.close();
117
+ try {
118
+ unlinkSync(key);
119
+ } catch {
120
+ }
121
+ database = new Database(key);
122
+ }
123
+ database.pragma("journal_mode = WAL");
124
+ }
125
+ createKnowledgeSchema(database);
126
+ knowledgeDbMap.set(key, database);
127
+ return database;
128
+ }
129
+ function closeKnowledgeDb() {
130
+ for (const database of knowledgeDbMap.values()) {
131
+ database.close();
132
+ }
133
+ knowledgeDbMap.clear();
134
+ }
135
+ var KNOWLEDGE_DB_PATH, knowledgeDbMap;
136
+ var init_connection = __esm({
137
+ "src/memory/storage/sqlite-knowledge/connection.ts"() {
138
+ init_availability();
139
+ init_schema();
140
+ KNOWLEDGE_DB_PATH = ".claude/.cache/knowledge.sqlite";
141
+ knowledgeDbMap = /* @__PURE__ */ new Map();
142
+ }
143
+ });
144
+ function generateChunkId(filePath, startLine, endLine) {
145
+ return createHash("sha256").update(`${filePath}:${startLine}:${endLine}`).digest("hex").slice(0, 16);
146
+ }
147
+ function chunkContentHash(text) {
148
+ return createHash("sha256").update(text).digest("hex");
149
+ }
150
+ var SUPPORTED_EXTENSIONS, CODE_EXTENSIONS;
151
+ var init_types = __esm({
152
+ "src/memory/knowledge/types.ts"() {
153
+ SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
154
+ ".md",
155
+ ".txt",
156
+ ".rst",
157
+ ".ts",
158
+ ".py",
159
+ ".js",
160
+ ".tsx",
161
+ ".jsx"
162
+ ]);
163
+ CODE_EXTENSIONS = /* @__PURE__ */ new Set([
164
+ ".ts",
165
+ ".tsx",
166
+ ".js",
167
+ ".jsx",
168
+ ".py"
169
+ ]);
170
+ }
171
+ });
172
+
173
+ // src/memory/storage/sqlite-knowledge/sync.ts
174
+ function upsertChunks(repoRoot, chunks, embeddings) {
175
+ if (chunks.length === 0) return;
176
+ const database = openKnowledgeDb(repoRoot);
177
+ const insert = database.prepare(`
178
+ INSERT OR REPLACE INTO chunks (id, file_path, start_line, end_line, content_hash, text, embedding, model, updated_at)
179
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
180
+ `);
181
+ const upsertMany = database.transaction((items) => {
182
+ for (const chunk of items) {
183
+ const embBuffer = null;
184
+ insert.run(
185
+ chunk.id,
186
+ chunk.filePath,
187
+ chunk.startLine,
188
+ chunk.endLine,
189
+ chunk.contentHash,
190
+ chunk.text,
191
+ embBuffer,
192
+ chunk.model ?? null,
193
+ chunk.updatedAt
194
+ );
195
+ }
196
+ });
197
+ upsertMany(chunks);
198
+ }
199
+ function deleteChunksByFilePath(repoRoot, filePaths) {
200
+ if (filePaths.length === 0) return;
201
+ const database = openKnowledgeDb(repoRoot);
202
+ const del = database.prepare("DELETE FROM chunks WHERE file_path = ?");
203
+ const deleteMany = database.transaction((paths) => {
204
+ for (const path2 of paths) {
205
+ del.run(path2);
206
+ }
207
+ });
208
+ deleteMany(filePaths);
209
+ }
210
+ function getIndexedFilePaths(repoRoot) {
211
+ const database = openKnowledgeDb(repoRoot);
212
+ const rows = database.prepare("SELECT DISTINCT file_path FROM chunks").all();
213
+ return rows.map((r) => r.file_path);
214
+ }
215
+ function getChunkCountByFilePath(repoRoot, filePath) {
216
+ const database = openKnowledgeDb(repoRoot);
217
+ const row = database.prepare("SELECT COUNT(*) as cnt FROM chunks WHERE file_path = ?").get(filePath);
218
+ return row.cnt;
219
+ }
220
+ function setLastIndexTime(repoRoot, time) {
221
+ const database = openKnowledgeDb(repoRoot);
222
+ database.prepare("INSERT OR REPLACE INTO metadata (key, value) VALUES ('last_index_time', ?)").run(time);
223
+ }
224
+ var init_sync = __esm({
225
+ "src/memory/storage/sqlite-knowledge/sync.ts"() {
226
+ init_connection();
227
+ }
228
+ });
229
+ function isBinary(content) {
230
+ return content.includes("\0");
231
+ }
232
+ function splitIntoSections(fileLines, ext) {
233
+ if (ext === ".md") {
234
+ return splitMarkdown(fileLines);
235
+ }
236
+ if (ext === ".rst") {
237
+ return splitParagraphs(fileLines);
238
+ }
239
+ if (CODE_EXTENSIONS.has(ext)) {
240
+ return splitCode(fileLines);
241
+ }
242
+ return splitParagraphs(fileLines);
243
+ }
244
+ function splitMarkdown(fileLines) {
245
+ const sections = [];
246
+ let current = [];
247
+ let inCodeBlock = false;
248
+ for (let i = 0; i < fileLines.length; i++) {
249
+ const line = fileLines[i];
250
+ const lineObj = { lineNumber: i + 1, text: line };
251
+ if (line.trimStart().startsWith("```")) {
252
+ inCodeBlock = !inCodeBlock;
253
+ current.push(lineObj);
254
+ continue;
255
+ }
256
+ if (!inCodeBlock && /^#{2,}\s/.test(line) && current.length > 0) {
257
+ sections.push(current);
258
+ current = [lineObj];
259
+ continue;
260
+ }
261
+ if (!inCodeBlock && line.trim() === "" && current.length > 0 && current.some((l) => l.text.trim() !== "")) {
262
+ current.push(lineObj);
263
+ sections.push(current);
264
+ current = [];
265
+ continue;
266
+ }
267
+ current.push(lineObj);
268
+ }
269
+ if (current.length > 0) {
270
+ sections.push(current);
271
+ }
272
+ return sections;
273
+ }
274
+ function splitCode(fileLines) {
275
+ const sections = [];
276
+ let current = [];
277
+ for (let i = 0; i < fileLines.length; i++) {
278
+ const line = fileLines[i];
279
+ const lineObj = { lineNumber: i + 1, text: line };
280
+ if (line.trim() === "" && current.length > 0) {
281
+ let hasNextNonBlank = false;
282
+ for (let j = i + 1; j < fileLines.length; j++) {
283
+ if (fileLines[j].trim() !== "") {
284
+ hasNextNonBlank = true;
285
+ break;
286
+ }
287
+ }
288
+ if (hasNextNonBlank) {
289
+ sections.push(current);
290
+ current = [lineObj];
291
+ continue;
292
+ }
293
+ }
294
+ current.push(lineObj);
295
+ }
296
+ if (current.length > 0) {
297
+ sections.push(current);
298
+ }
299
+ return sections;
300
+ }
301
+ function splitParagraphs(fileLines) {
302
+ const sections = [];
303
+ let current = [];
304
+ for (let i = 0; i < fileLines.length; i++) {
305
+ const line = fileLines[i];
306
+ const lineObj = { lineNumber: i + 1, text: line };
307
+ if (line.trim() === "" && current.length > 0) {
308
+ sections.push(current);
309
+ current = [lineObj];
310
+ continue;
311
+ }
312
+ current.push(lineObj);
313
+ }
314
+ if (current.length > 0) {
315
+ sections.push(current);
316
+ }
317
+ return sections;
318
+ }
319
+ function sectionText(section) {
320
+ return section.map((l) => l.text).join("\n");
321
+ }
322
+ function chunkFile(filePath, content, options) {
323
+ if (content.trim() === "") return [];
324
+ if (isBinary(content)) return [];
325
+ const targetSize = options?.targetSize ?? DEFAULT_TARGET_SIZE;
326
+ const overlapSize = options?.overlapSize ?? DEFAULT_OVERLAP_SIZE;
327
+ const fileLines = content.split("\n");
328
+ const ext = extname(filePath).toLowerCase();
329
+ const sections = splitIntoSections(fileLines, ext);
330
+ const chunks = [];
331
+ let accumulated = [];
332
+ let accumulatedLength = 0;
333
+ function emitChunk(lines, overlapLines2) {
334
+ if (lines.length === 0) return [];
335
+ const allLines = [...overlapLines2, ...lines];
336
+ const text = allLines.map((l) => l.text).join("\n");
337
+ const startLine = allLines[0].lineNumber;
338
+ const endLine = allLines[allLines.length - 1].lineNumber;
339
+ chunks.push({
340
+ id: generateChunkId(filePath, startLine, endLine),
341
+ filePath,
342
+ startLine,
343
+ endLine,
344
+ text,
345
+ contentHash: chunkContentHash(text)
346
+ });
347
+ if (overlapSize <= 0) return [];
348
+ const overlapResult = [];
349
+ let overlapLen = 0;
350
+ for (let i = lines.length - 1; i >= 0; i--) {
351
+ const lineLen = lines[i].text.length + 1;
352
+ if (overlapLen + lineLen > overlapSize && overlapResult.length > 0) break;
353
+ overlapResult.unshift(lines[i]);
354
+ overlapLen += lineLen;
355
+ }
356
+ return overlapResult;
357
+ }
358
+ let overlapLines = [];
359
+ for (const section of sections) {
360
+ const sectionLen = sectionText(section).length;
361
+ if (accumulatedLength > 0 && accumulatedLength + sectionLen > targetSize) {
362
+ overlapLines = emitChunk(accumulated, overlapLines);
363
+ accumulated = [];
364
+ accumulatedLength = 0;
365
+ }
366
+ accumulated.push(...section);
367
+ accumulatedLength += sectionLen;
368
+ if (accumulatedLength > targetSize) {
369
+ overlapLines = emitChunk(accumulated, overlapLines);
370
+ accumulated = [];
371
+ accumulatedLength = 0;
372
+ }
373
+ }
374
+ if (accumulated.length > 0) {
375
+ emitChunk(accumulated, overlapLines);
376
+ }
377
+ return chunks;
378
+ }
379
+ var DEFAULT_TARGET_SIZE, DEFAULT_OVERLAP_SIZE;
380
+ var init_chunking = __esm({
381
+ "src/memory/knowledge/chunking.ts"() {
382
+ init_types();
383
+ DEFAULT_TARGET_SIZE = 1600;
384
+ DEFAULT_OVERLAP_SIZE = 320;
385
+ }
386
+ });
387
+ function fileHash(content) {
388
+ return createHash("sha256").update(content).digest("hex");
389
+ }
390
+ function fileHashKey(relativePath) {
391
+ return "file_hash:" + relativePath;
392
+ }
393
+ function getStoredFileHash(repoRoot, relativePath) {
394
+ const db = openKnowledgeDb(repoRoot);
395
+ const row = db.prepare("SELECT value FROM metadata WHERE key = ?").get(fileHashKey(relativePath));
396
+ return row?.value ?? null;
397
+ }
398
+ function setFileHash(repoRoot, relativePath, hash) {
399
+ const db = openKnowledgeDb(repoRoot);
400
+ db.prepare("INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)").run(fileHashKey(relativePath), hash);
401
+ }
402
+ function removeFileHash(repoRoot, relativePath) {
403
+ const db = openKnowledgeDb(repoRoot);
404
+ db.prepare("DELETE FROM metadata WHERE key = ?").run(fileHashKey(relativePath));
405
+ }
406
+ async function walkSupportedFiles(baseDir, repoRoot) {
407
+ const results = [];
408
+ let entries;
409
+ try {
410
+ entries = await readdir(baseDir, { recursive: true, withFileTypes: true });
411
+ } catch {
412
+ return results;
413
+ }
414
+ for (const entry of entries) {
415
+ if (!entry.isFile()) continue;
416
+ const ext = extname(entry.name).toLowerCase();
417
+ if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
418
+ const fullPath = join(entry.parentPath ?? entry.path, entry.name);
419
+ const relPath = relative(repoRoot, fullPath);
420
+ results.push(relPath);
421
+ }
422
+ return results;
423
+ }
424
+ async function indexDocs(repoRoot, options = {}) {
425
+ const start = Date.now();
426
+ const docsDir = options.docsDir ?? "docs";
427
+ const force = options.force ?? false;
428
+ const stats = {
429
+ filesIndexed: 0,
430
+ filesSkipped: 0,
431
+ filesErrored: 0,
432
+ chunksCreated: 0,
433
+ chunksDeleted: 0,
434
+ durationMs: 0
435
+ };
436
+ const docsPath = join(repoRoot, docsDir);
437
+ const filePaths = await walkSupportedFiles(docsPath, repoRoot);
438
+ for (const relPath of filePaths) {
439
+ const fullPath = join(repoRoot, relPath);
440
+ let content;
441
+ try {
442
+ content = await readFile(fullPath, "utf-8");
443
+ } catch {
444
+ stats.filesErrored++;
445
+ continue;
446
+ }
447
+ const hash = fileHash(content);
448
+ const storedHash = getStoredFileHash(repoRoot, relPath);
449
+ if (!force && storedHash === hash) {
450
+ stats.filesSkipped++;
451
+ continue;
452
+ }
453
+ const chunks = chunkFile(relPath, content);
454
+ const now = (/* @__PURE__ */ new Date()).toISOString();
455
+ const knowledgeChunks = chunks.map((chunk) => ({
456
+ id: chunk.id,
457
+ filePath: chunk.filePath,
458
+ startLine: chunk.startLine,
459
+ endLine: chunk.endLine,
460
+ contentHash: chunk.contentHash,
461
+ text: chunk.text,
462
+ updatedAt: now
463
+ }));
464
+ const db = openKnowledgeDb(repoRoot);
465
+ db.transaction(() => {
466
+ deleteChunksByFilePath(repoRoot, [relPath]);
467
+ if (knowledgeChunks.length > 0) {
468
+ upsertChunks(repoRoot, knowledgeChunks);
469
+ }
470
+ setFileHash(repoRoot, relPath, hash);
471
+ })();
472
+ stats.filesIndexed++;
473
+ stats.chunksCreated += knowledgeChunks.length;
474
+ }
475
+ const indexedPaths = getIndexedFilePaths(repoRoot);
476
+ const currentPathSet = new Set(filePaths);
477
+ const stalePaths = indexedPaths.filter((p) => !currentPathSet.has(p));
478
+ if (stalePaths.length > 0) {
479
+ for (const path2 of stalePaths) {
480
+ stats.chunksDeleted += getChunkCountByFilePath(repoRoot, path2);
481
+ }
482
+ deleteChunksByFilePath(repoRoot, stalePaths);
483
+ for (const path2 of stalePaths) {
484
+ removeFileHash(repoRoot, path2);
485
+ }
486
+ }
487
+ setLastIndexTime(repoRoot, (/* @__PURE__ */ new Date()).toISOString());
488
+ stats.durationMs = Date.now() - start;
489
+ return stats;
490
+ }
491
+ var init_indexing = __esm({
492
+ "src/memory/knowledge/indexing.ts"() {
493
+ init_connection();
494
+ init_sync();
495
+ init_chunking();
496
+ init_types();
497
+ }
498
+ });
13
499
  var _require = createRequire(import.meta.url);
14
500
  var _pkg = _require("../package.json");
15
501
  var VERSION = _pkg.version;
@@ -224,29 +710,9 @@ async function readLessons(repoRoot, options = {}) {
224
710
  const lessons = result.items.filter((item) => item.type === "lesson");
225
711
  return { lessons, skippedCount: result.skippedCount };
226
712
  }
227
- var require2 = createRequire(import.meta.url);
228
- var checked = false;
229
- var DatabaseConstructor = null;
230
- function ensureSqliteAvailable() {
231
- if (checked) return;
232
- try {
233
- const module = require2("better-sqlite3");
234
- const Constructor = module.default || module;
235
- const testDb = new Constructor(":memory:");
236
- testDb.close();
237
- DatabaseConstructor = Constructor;
238
- checked = true;
239
- } catch (cause) {
240
- throw new Error(
241
- 'better-sqlite3 failed to load.\nRun: npx ca setup (auto-configures pnpm native builds)\nOr manually add to your package.json:\n "pnpm": { "onlyBuiltDependencies": ["better-sqlite3", "node-llama-cpp"] }\nThen run: pnpm install && pnpm rebuild better-sqlite3\nFor npm/yarn, run: npm rebuild better-sqlite3',
242
- { cause }
243
- );
244
- }
245
- }
246
- function getDatabaseConstructor() {
247
- ensureSqliteAvailable();
248
- return DatabaseConstructor;
249
- }
713
+
714
+ // src/memory/storage/sqlite/connection.ts
715
+ init_availability();
250
716
 
251
717
  // src/memory/storage/sqlite/schema.ts
252
718
  var SCHEMA_VERSION = 3;
@@ -342,7 +808,10 @@ function openDb(repoRoot, options = {}) {
342
808
  database = new Database(key);
343
809
  if (!hasExpectedVersion(database)) {
344
810
  database.close();
345
- unlinkSync(key);
811
+ try {
812
+ unlinkSync(key);
813
+ } catch {
814
+ }
346
815
  database = new Database(key);
347
816
  }
348
817
  database.pragma("journal_mode = WAL");
@@ -483,7 +952,65 @@ async function syncIfNeeded(repoRoot, options = {}) {
483
952
  return false;
484
953
  }
485
954
 
955
+ // src/memory/search/hybrid.ts
956
+ var DEFAULT_VECTOR_WEIGHT = 0.7;
957
+ var DEFAULT_TEXT_WEIGHT = 0.3;
958
+ var CANDIDATE_MULTIPLIER = 4;
959
+ var MIN_HYBRID_SCORE = 0.35;
960
+ function normalizeBm25Rank(rank) {
961
+ if (!Number.isFinite(rank)) return 0;
962
+ const abs = Math.abs(rank);
963
+ return abs / (1 + abs);
964
+ }
965
+ function mergeHybridScores(vectorResults, keywordResults, getId, options) {
966
+ if (vectorResults.length === 0 && keywordResults.length === 0) return [];
967
+ const rawVecW = options?.vectorWeight ?? DEFAULT_VECTOR_WEIGHT;
968
+ const rawTxtW = options?.textWeight ?? DEFAULT_TEXT_WEIGHT;
969
+ const total = rawVecW + rawTxtW;
970
+ if (total <= 0) return [];
971
+ const vecW = rawVecW / total;
972
+ const txtW = rawTxtW / total;
973
+ const limit = options?.limit;
974
+ const minScore = options?.minScore;
975
+ const merged = /* @__PURE__ */ new Map();
976
+ for (const v of vectorResults) {
977
+ merged.set(getId(v.item), { item: v.item, vecScore: v.score, txtScore: 0 });
978
+ }
979
+ for (const k of keywordResults) {
980
+ const id = getId(k.item);
981
+ const existing = merged.get(id);
982
+ if (existing) {
983
+ existing.txtScore = k.score;
984
+ } else {
985
+ merged.set(id, { item: k.item, vecScore: 0, txtScore: k.score });
986
+ }
987
+ }
988
+ const results = [];
989
+ for (const entry of merged.values()) {
990
+ results.push({
991
+ item: entry.item,
992
+ score: vecW * entry.vecScore + txtW * entry.txtScore
993
+ });
994
+ }
995
+ results.sort((a, b) => b.score - a.score);
996
+ const filtered = minScore !== void 0 ? results.filter((r) => r.score >= minScore) : results;
997
+ return limit !== void 0 ? filtered.slice(0, limit) : filtered;
998
+ }
999
+ function mergeHybridResults(vectorResults, keywordResults, options) {
1000
+ const genericVec = vectorResults.map((v) => ({ item: v.lesson, score: v.score }));
1001
+ const genericKw = keywordResults.map((k) => ({ item: k.lesson, score: k.score }));
1002
+ const merged = mergeHybridScores(genericVec, genericKw, (item) => item.id, options);
1003
+ return merged.map((m) => ({ lesson: m.item, score: m.score }));
1004
+ }
1005
+
486
1006
  // src/memory/storage/sqlite/search.ts
1007
+ function safeJsonParse(value, fallback) {
1008
+ try {
1009
+ return JSON.parse(value);
1010
+ } catch {
1011
+ return fallback;
1012
+ }
1013
+ }
487
1014
  function rowToMemoryItem(row) {
488
1015
  const item = {
489
1016
  id: row.id,
@@ -492,9 +1019,9 @@ function rowToMemoryItem(row) {
492
1019
  insight: row.insight,
493
1020
  tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
494
1021
  source: row.source,
495
- context: JSON.parse(row.context),
496
- supersedes: JSON.parse(row.supersedes),
497
- related: JSON.parse(row.related),
1022
+ context: safeJsonParse(row.context, {}),
1023
+ supersedes: safeJsonParse(row.supersedes, []),
1024
+ related: safeJsonParse(row.related, []),
498
1025
  created: row.created,
499
1026
  confirmed: row.confirmed === 1
500
1027
  };
@@ -525,7 +1052,7 @@ function rowToMemoryItem(row) {
525
1052
  }
526
1053
  var FTS_OPERATORS = /* @__PURE__ */ new Set(["AND", "OR", "NOT", "NEAR"]);
527
1054
  function sanitizeFtsQuery(query) {
528
- const stripped = query.replace(/["*^+-]/g, "");
1055
+ const stripped = query.replace(/["*^+\-():{}]/g, "");
529
1056
  const tokens = stripped.split(/\s+/).filter((t) => t.length > 0 && !FTS_OPERATORS.has(t));
530
1057
  return tokens.join(" ");
531
1058
  }
@@ -546,44 +1073,47 @@ function incrementRetrievalCount(repoRoot, lessonIds) {
546
1073
  });
547
1074
  updateMany(lessonIds);
548
1075
  }
549
- async function searchKeyword(repoRoot, query, limit, typeFilter) {
1076
+ function executeFtsQuery(repoRoot, query, limit, options) {
550
1077
  const database = openDb(repoRoot);
551
- const countResult = database.prepare("SELECT COUNT(*) as cnt FROM lessons").get();
552
- if (countResult.cnt === 0) return [];
553
1078
  const sanitized = sanitizeFtsQuery(query);
554
1079
  if (sanitized === "") return [];
1080
+ const selectCols = options.includeRank ? "l.*, fts.rank" : "l.*";
1081
+ const orderClause = options.includeRank ? "ORDER BY fts.rank" : "";
1082
+ const typeClause = options.typeFilter ? "AND l.type = ?" : "";
1083
+ const sql = `
1084
+ SELECT ${selectCols}
1085
+ FROM lessons l
1086
+ JOIN lessons_fts fts ON l.rowid = fts.rowid
1087
+ WHERE lessons_fts MATCH ?
1088
+ AND l.invalidated_at IS NULL
1089
+ ${typeClause}
1090
+ ${orderClause}
1091
+ LIMIT ?
1092
+ `;
1093
+ const params = options.typeFilter ? [sanitized, options.typeFilter, limit] : [sanitized, limit];
555
1094
  try {
556
- if (typeFilter) {
557
- const rows2 = database.prepare(
558
- `
559
- SELECT l.*
560
- FROM lessons l
561
- JOIN lessons_fts fts ON l.rowid = fts.rowid
562
- WHERE lessons_fts MATCH ?
563
- AND l.invalidated_at IS NULL
564
- AND l.type = ?
565
- LIMIT ?
566
- `
567
- ).all(sanitized, typeFilter, limit);
568
- return rows2.map(rowToMemoryItem).filter((x) => x !== null);
569
- }
570
- const rows = database.prepare(
571
- `
572
- SELECT l.*
573
- FROM lessons l
574
- JOIN lessons_fts fts ON l.rowid = fts.rowid
575
- WHERE lessons_fts MATCH ?
576
- AND l.invalidated_at IS NULL
577
- LIMIT ?
578
- `
579
- ).all(sanitized, limit);
580
- return rows.map(rowToMemoryItem).filter((x) => x !== null);
1095
+ return database.prepare(sql).all(...params);
581
1096
  } catch (err) {
582
1097
  const message = err instanceof Error ? err.message : "Unknown FTS5 error";
583
1098
  console.error(`[compound-agent] search error: ${message}`);
584
1099
  return [];
585
1100
  }
586
1101
  }
1102
+ async function searchKeyword(repoRoot, query, limit, typeFilter) {
1103
+ const rows = executeFtsQuery(repoRoot, query, limit, { includeRank: false, typeFilter });
1104
+ return rows.map(rowToMemoryItem).filter((x) => x !== null);
1105
+ }
1106
+ async function searchKeywordScored(repoRoot, query, limit, typeFilter) {
1107
+ const rows = executeFtsQuery(repoRoot, query, limit, { includeRank: true, typeFilter });
1108
+ const results = [];
1109
+ for (const row of rows) {
1110
+ const lesson = rowToMemoryItem(row);
1111
+ if (lesson) {
1112
+ results.push({ lesson, score: normalizeBm25Rank(row.rank) });
1113
+ }
1114
+ }
1115
+ return results;
1116
+ }
587
1117
 
588
1118
  // src/utils.ts
589
1119
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
@@ -1161,8 +1691,13 @@ async function loadSessionLessons(repoRoot, limit = DEFAULT_LIMIT2) {
1161
1691
  // src/memory/retrieval/plan.ts
1162
1692
  var DEFAULT_LIMIT3 = 5;
1163
1693
  async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
1164
- const scored = await searchVector(repoRoot, planText, { limit: limit * 2 });
1165
- const ranked = rankLessons(scored);
1694
+ const candidateLimit = limit * CANDIDATE_MULTIPLIER;
1695
+ const [vectorResults, keywordResults] = await Promise.all([
1696
+ searchVector(repoRoot, planText, { limit: candidateLimit }),
1697
+ searchKeywordScored(repoRoot, planText, candidateLimit)
1698
+ ]);
1699
+ const merged = mergeHybridResults(vectorResults, keywordResults, { minScore: MIN_HYBRID_SCORE });
1700
+ const ranked = rankLessons(merged);
1166
1701
  const topLessons = ranked.slice(0, limit);
1167
1702
  if (topLessons.length > 0) {
1168
1703
  incrementRetrievalCount(repoRoot, topLessons.map((item) => item.lesson.id));
@@ -1185,12 +1720,173 @@ No relevant lessons found for this plan.`;
1185
1720
  ${lessonLines.join("\n")}`;
1186
1721
  }
1187
1722
 
1723
+ // src/memory/storage/sqlite-knowledge/index.ts
1724
+ init_connection();
1725
+ init_schema();
1726
+
1727
+ // src/memory/storage/sqlite-knowledge/cache.ts
1728
+ init_connection();
1729
+ init_types();
1730
+ function getCachedChunkEmbedding(repoRoot, chunkId, expectedHash) {
1731
+ const database = openKnowledgeDb(repoRoot);
1732
+ const row = database.prepare("SELECT embedding, content_hash FROM chunks WHERE id = ?").get(chunkId);
1733
+ if (!row || !row.embedding || !row.content_hash) {
1734
+ return null;
1735
+ }
1736
+ if (expectedHash && row.content_hash !== expectedHash) {
1737
+ return null;
1738
+ }
1739
+ return new Float32Array(
1740
+ row.embedding.buffer,
1741
+ row.embedding.byteOffset,
1742
+ row.embedding.byteLength / 4
1743
+ );
1744
+ }
1745
+ function setCachedChunkEmbedding(repoRoot, chunkId, embedding, hash) {
1746
+ const database = openKnowledgeDb(repoRoot);
1747
+ const float32 = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
1748
+ const buffer = Buffer.from(float32.buffer, float32.byteOffset, float32.byteLength);
1749
+ database.prepare("UPDATE chunks SET embedding = ?, content_hash = ? WHERE id = ?").run(buffer, hash, chunkId);
1750
+ }
1751
+ function collectCachedChunkEmbeddings(database) {
1752
+ const cache = /* @__PURE__ */ new Map();
1753
+ const rows = database.prepare("SELECT id, embedding, content_hash FROM chunks WHERE embedding IS NOT NULL").all();
1754
+ for (const row of rows) {
1755
+ if (row.embedding && row.content_hash) {
1756
+ cache.set(row.id, { embedding: row.embedding, contentHash: row.content_hash });
1757
+ }
1758
+ }
1759
+ return cache;
1760
+ }
1761
+
1762
+ // src/memory/storage/sqlite-knowledge/search.ts
1763
+ init_connection();
1764
+ function rowToChunk(row) {
1765
+ const chunk = {
1766
+ id: row.id,
1767
+ filePath: row.file_path,
1768
+ startLine: row.start_line,
1769
+ endLine: row.end_line,
1770
+ contentHash: row.content_hash,
1771
+ text: row.text,
1772
+ updatedAt: row.updated_at
1773
+ };
1774
+ if (row.model !== null) {
1775
+ chunk.model = row.model;
1776
+ }
1777
+ return chunk;
1778
+ }
1779
+ function searchChunksKeywordScored(repoRoot, query, limit) {
1780
+ const database = openKnowledgeDb(repoRoot);
1781
+ const sanitized = sanitizeFtsQuery(query);
1782
+ if (sanitized === "") return [];
1783
+ try {
1784
+ const rows = database.prepare(
1785
+ `SELECT c.*, fts.rank
1786
+ FROM chunks c
1787
+ JOIN chunks_fts fts ON c.rowid = fts.rowid
1788
+ WHERE chunks_fts MATCH ?
1789
+ ORDER BY fts.rank
1790
+ LIMIT ?`
1791
+ ).all(sanitized, limit);
1792
+ return rows.map((row) => ({
1793
+ chunk: rowToChunk(row),
1794
+ score: normalizeBm25Rank(row.rank)
1795
+ }));
1796
+ } catch (err) {
1797
+ const message = err instanceof Error ? err.message : "Unknown FTS5 error";
1798
+ console.error(`[compound-agent] knowledge scored search error: ${message}`);
1799
+ return [];
1800
+ }
1801
+ }
1802
+
1803
+ // src/memory/storage/sqlite-knowledge/index.ts
1804
+ init_sync();
1805
+
1806
+ // src/index.ts
1807
+ init_chunking();
1808
+ init_indexing();
1809
+
1810
+ // src/memory/knowledge/search.ts
1811
+ init_connection();
1812
+ var DEFAULT_KNOWLEDGE_LIMIT = 6;
1813
+ async function searchKnowledgeVector(repoRoot, query, options) {
1814
+ const limit = options?.limit ?? DEFAULT_KNOWLEDGE_LIMIT;
1815
+ const database = openKnowledgeDb(repoRoot);
1816
+ const embRows = database.prepare("SELECT id, embedding FROM chunks WHERE embedding IS NOT NULL").all();
1817
+ if (embRows.length === 0) return [];
1818
+ const queryVector = await embedText(query);
1819
+ const scored = [];
1820
+ for (const row of embRows) {
1821
+ const embFloat = new Float32Array(
1822
+ row.embedding.buffer,
1823
+ row.embedding.byteOffset,
1824
+ row.embedding.byteLength / 4
1825
+ );
1826
+ scored.push({ id: row.id, score: cosineSimilarity(queryVector, embFloat) });
1827
+ }
1828
+ scored.sort((a, b) => b.score - a.score);
1829
+ const topK = scored.slice(0, limit);
1830
+ if (topK.length === 0) return [];
1831
+ const placeholders = topK.map(() => "?").join(",");
1832
+ const sql = `SELECT id, file_path, start_line, end_line, content_hash, text, model, updated_at FROM chunks WHERE id IN (${placeholders})`;
1833
+ const dataRows = database.prepare(sql).all(...topK.map((r) => r.id));
1834
+ const dataMap = new Map(dataRows.map((r) => [r.id, r]));
1835
+ const results = [];
1836
+ for (const { id, score } of topK) {
1837
+ const row = dataMap.get(id);
1838
+ if (!row) continue;
1839
+ const chunk = {
1840
+ id: row.id,
1841
+ filePath: row.file_path,
1842
+ startLine: row.start_line,
1843
+ endLine: row.end_line,
1844
+ contentHash: row.content_hash,
1845
+ text: row.text,
1846
+ updatedAt: row.updated_at
1847
+ };
1848
+ if (row.model !== null) {
1849
+ chunk.model = row.model;
1850
+ }
1851
+ results.push({ item: chunk, score });
1852
+ }
1853
+ return results;
1854
+ }
1855
+ async function searchKnowledge(repoRoot, query, options) {
1856
+ const limit = options?.limit ?? DEFAULT_KNOWLEDGE_LIMIT;
1857
+ const candidateLimit = limit * CANDIDATE_MULTIPLIER;
1858
+ const usability = await isModelUsable();
1859
+ if (usability.usable) {
1860
+ const [vectorResults, keywordResults2] = await Promise.all([
1861
+ searchKnowledgeVector(repoRoot, query, { limit: candidateLimit }),
1862
+ Promise.resolve(searchChunksKeywordScored(repoRoot, query, candidateLimit))
1863
+ ]);
1864
+ if (vectorResults.length === 0) {
1865
+ return keywordResults2.map((k) => ({ item: k.chunk, score: k.score })).slice(0, limit);
1866
+ }
1867
+ const genericKw = keywordResults2.map((k) => ({
1868
+ item: k.chunk,
1869
+ score: k.score
1870
+ }));
1871
+ const merged = mergeHybridScores(
1872
+ vectorResults,
1873
+ genericKw,
1874
+ (item) => item.id,
1875
+ { limit, minScore: MIN_HYBRID_SCORE }
1876
+ );
1877
+ return merged;
1878
+ }
1879
+ const keywordResults = searchChunksKeywordScored(repoRoot, query, limit);
1880
+ return keywordResults.map((k) => ({ item: k.chunk, score: k.score }));
1881
+ }
1882
+
1188
1883
  // src/cli-utils.ts
1189
1884
  function getRepoRoot() {
1190
- return process.env["COMPOUND_AGENT_ROOT"] ?? process.cwd();
1885
+ return process.env["COMPOUND_AGENT_ROOT"] || process.cwd();
1191
1886
  }
1192
1887
  var STATE_DIR = ".claude";
1193
1888
  var STATE_FILE = ".ca-phase-state.json";
1889
+ var PHASE_STATE_MAX_AGE_MS = 72 * 60 * 60 * 1e3;
1194
1890
  var PHASES = ["brainstorm", "plan", "work", "review", "compound"];
1195
1891
  var GATES = ["post-plan", "gate-3", "gate-4", "final"];
1196
1892
  function getStatePath(repoRoot) {
@@ -1220,11 +1916,24 @@ function getPhaseState(repoRoot) {
1220
1916
  if (!existsSync(path2)) return null;
1221
1917
  const raw = readFileSync(path2, "utf-8");
1222
1918
  const parsed = JSON.parse(raw);
1223
- return validatePhaseState(parsed) ? parsed : null;
1919
+ if (!validatePhaseState(parsed)) return null;
1920
+ const age = Date.now() - new Date(parsed.started_at).getTime();
1921
+ if (age > PHASE_STATE_MAX_AGE_MS) {
1922
+ cleanPhaseState(repoRoot);
1923
+ return null;
1924
+ }
1925
+ return parsed;
1224
1926
  } catch {
1225
1927
  return null;
1226
1928
  }
1227
1929
  }
1930
+ function cleanPhaseState(repoRoot) {
1931
+ try {
1932
+ const path2 = getStatePath(repoRoot);
1933
+ if (existsSync(path2)) unlinkSync(path2);
1934
+ } catch {
1935
+ }
1936
+ }
1228
1937
 
1229
1938
  // src/commands/management-prime.ts
1230
1939
  var TRUST_LANGUAGE_TEMPLATE = `# Compound Agent Active
@@ -1238,6 +1947,7 @@ var TRUST_LANGUAGE_TEMPLATE = `# Compound Agent Active
1238
1947
  | Command | Purpose |
1239
1948
  |---------|---------|
1240
1949
  | \`npx ca search "query"\` | Search lessons - call BEFORE architectural decisions |
1950
+ | \`npx ca knowledge "query"\` | Search docs knowledge - use for architecture context |
1241
1951
  | \`npx ca learn "insight"\` | Capture lessons - call AFTER corrections or discoveries |
1242
1952
 
1243
1953
  ## Core Constraints
@@ -1250,7 +1960,7 @@ var TRUST_LANGUAGE_TEMPLATE = `# Compound Agent Active
1250
1960
 
1251
1961
  ## Retrieval Protocol
1252
1962
 
1253
- You MUST call \`npx ca search\` BEFORE:
1963
+ You MUST call \`npx ca search\` and \`npx ca knowledge\` BEFORE:
1254
1964
  - Architectural decisions or complex planning
1255
1965
  - Implementing patterns you've done before in this repo
1256
1966
 
@@ -1629,6 +2339,11 @@ var AuditReportSchema = z.object({
1629
2339
  timestamp: z.string()
1630
2340
  });
1631
2341
 
1632
- export { AuditFindingSchema, AuditReportSchema, CCT_PATTERNS_PATH, CctPatternSchema, DB_PATH, LESSONS_PATH, LessonItemSchema, LessonSchema, MODEL_FILENAME, MODEL_URI, MemoryItemRecordSchema, MemoryItemSchema, MemoryItemTypeSchema, PatternItemSchema, PreferenceItemSchema, SolutionItemSchema, VERSION, appendLesson, appendMemoryItem, buildSimilarityMatrix, calculateScore, closeDb, clusterBySimilarity, confirmationBoost, cosineSimilarity, detectSelfCorrection, detectTestFailure, detectUserCorrection, embedText, embedTexts, formatLessonsCheck, generateId, getEmbedding, getPrimeContext, isActionable, isModelAvailable, isModelUsable, isNovel, isSpecific, loadSessionLessons, rankLessons, readCctPatterns, readLessons, readMemoryItems, rebuildIndex, recencyBoost, resolveModel, retrieveForPlan, runAudit, searchKeyword, searchVector, severityBoost, shouldPropose, synthesizePattern, unloadEmbedding, writeCctPatterns };
2342
+ // src/memory/knowledge/index.ts
2343
+ init_chunking();
2344
+ init_types();
2345
+ init_indexing();
2346
+
2347
+ export { AuditFindingSchema, AuditReportSchema, CANDIDATE_MULTIPLIER, CCT_PATTERNS_PATH, CctPatternSchema, DB_PATH, DEFAULT_TEXT_WEIGHT, DEFAULT_VECTOR_WEIGHT, KNOWLEDGE_DB_PATH, KNOWLEDGE_SCHEMA_VERSION, LESSONS_PATH, LessonItemSchema, LessonSchema, MODEL_FILENAME, MODEL_URI, MemoryItemRecordSchema, MemoryItemSchema, MemoryItemTypeSchema, PatternItemSchema, PreferenceItemSchema, SolutionItemSchema, VERSION, appendLesson, appendMemoryItem, buildSimilarityMatrix, calculateScore, chunkFile, closeDb, closeKnowledgeDb, clusterBySimilarity, collectCachedChunkEmbeddings, confirmationBoost, cosineSimilarity, detectSelfCorrection, detectTestFailure, detectUserCorrection, embedText, embedTexts, formatLessonsCheck, generateId, getCachedChunkEmbedding, getEmbedding, getPrimeContext, indexDocs, isActionable, isModelAvailable, isModelUsable, isNovel, isSpecific, loadSessionLessons, mergeHybridResults, normalizeBm25Rank, openKnowledgeDb, rankLessons, readCctPatterns, readLessons, readMemoryItems, rebuildIndex, recencyBoost, resolveModel, retrieveForPlan, runAudit, searchChunksKeywordScored, searchKeyword, searchKnowledge, searchKnowledgeVector, searchVector, setCachedChunkEmbedding, severityBoost, shouldPropose, synthesizePattern, unloadEmbedding, writeCctPatterns };
1633
2348
  //# sourceMappingURL=index.js.map
1634
2349
  //# sourceMappingURL=index.js.map