claude-conversation-memory-mcp 1.5.0 → 1.6.4

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.
Files changed (46) hide show
  1. package/dist/embeddings/VectorStore.d.ts +10 -0
  2. package/dist/embeddings/VectorStore.d.ts.map +1 -1
  3. package/dist/embeddings/VectorStore.js +76 -0
  4. package/dist/embeddings/VectorStore.js.map +1 -1
  5. package/dist/index.js +0 -0
  6. package/dist/parsers/CodexConversationParser.d.ts +1 -1
  7. package/dist/parsers/CodexConversationParser.d.ts.map +1 -1
  8. package/dist/parsers/CodexConversationParser.js +27 -5
  9. package/dist/parsers/CodexConversationParser.js.map +1 -1
  10. package/dist/parsers/ConversationParser.d.ts +18 -0
  11. package/dist/parsers/ConversationParser.d.ts.map +1 -1
  12. package/dist/parsers/ConversationParser.js +62 -1
  13. package/dist/parsers/ConversationParser.js.map +1 -1
  14. package/dist/search/SemanticSearch.d.ts +6 -2
  15. package/dist/search/SemanticSearch.d.ts.map +1 -1
  16. package/dist/search/SemanticSearch.js +78 -35
  17. package/dist/search/SemanticSearch.js.map +1 -1
  18. package/dist/storage/ConversationStorage.d.ts +10 -0
  19. package/dist/storage/ConversationStorage.d.ts.map +1 -1
  20. package/dist/storage/ConversationStorage.js +34 -0
  21. package/dist/storage/ConversationStorage.js.map +1 -1
  22. package/dist/storage/SQLiteManager.d.ts.map +1 -1
  23. package/dist/storage/SQLiteManager.js +87 -17
  24. package/dist/storage/SQLiteManager.js.map +1 -1
  25. package/dist/storage/migrations.d.ts.map +1 -1
  26. package/dist/storage/migrations.js +41 -8
  27. package/dist/storage/migrations.js.map +1 -1
  28. package/dist/storage/schema.sql +1 -1
  29. package/dist/tools/ToolDefinitions.d.ts +116 -0
  30. package/dist/tools/ToolDefinitions.d.ts.map +1 -1
  31. package/dist/tools/ToolDefinitions.js +126 -10
  32. package/dist/tools/ToolDefinitions.js.map +1 -1
  33. package/dist/tools/ToolHandlers.d.ts +21 -0
  34. package/dist/tools/ToolHandlers.d.ts.map +1 -1
  35. package/dist/tools/ToolHandlers.js +519 -119
  36. package/dist/tools/ToolHandlers.js.map +1 -1
  37. package/dist/types/ToolTypes.d.ts +46 -0
  38. package/dist/types/ToolTypes.d.ts.map +1 -1
  39. package/dist/utils/ProjectMigration.d.ts.map +1 -1
  40. package/dist/utils/ProjectMigration.js +4 -3
  41. package/dist/utils/ProjectMigration.js.map +1 -1
  42. package/dist/utils/sanitization.d.ts +10 -0
  43. package/dist/utils/sanitization.d.ts.map +1 -1
  44. package/dist/utils/sanitization.js +33 -0
  45. package/dist/utils/sanitization.js.map +1 -1
  46. package/package.json +1 -1
@@ -29,6 +29,32 @@ import { pathToProjectFolderName } from "../utils/sanitization.js";
29
29
  import { DeletionService } from "../storage/DeletionService.js";
30
30
  import { readdirSync } from "fs";
31
31
  import { join } from "path";
32
+ /**
33
+ * Default similarity score used when semantic search is not available.
34
+ * This applies to SQL-based searches where semantic embeddings are not used.
35
+ */
36
+ const DEFAULT_SIMILARITY_SCORE = 1.0;
37
+ /**
38
+ * Pagination Patterns:
39
+ *
40
+ * This codebase uses two different pagination patterns based on data source:
41
+ *
42
+ * 1. SQL-based pagination (fetch+1):
43
+ * - Fetch limit+1 records from database
44
+ * - hasMore = results.length > limit
45
+ * - Slice to limit if hasMore is true
46
+ * - Use case: Single-database SQL queries (searchMistakes, linkCommitsToConversations)
47
+ * - Advantage: Efficient, minimal data transfer
48
+ *
49
+ * 2. In-memory pagination (slice):
50
+ * - Fetch all needed results (or limit+offset)
51
+ * - Slice to get paginated subset: results.slice(offset, offset + limit)
52
+ * - has_more = offset + limit < results.length
53
+ * - Use case: Semantic search, cross-project aggregation
54
+ * - Advantage: Allows sorting/filtering before pagination
55
+ *
56
+ * Both patterns are correct and optimized for their respective use cases.
57
+ */
32
58
  /**
33
59
  * Tool handlers for the conversation-memory MCP server.
34
60
  *
@@ -167,15 +193,106 @@ export class ToolHandlers {
167
193
  */
168
194
  async searchConversations(args) {
169
195
  const typedArgs = args;
170
- const { query, limit = 10, date_range } = typedArgs;
171
- const filter = {};
172
- if (date_range) {
173
- filter.date_range = date_range;
196
+ const { query, limit = 10, offset = 0, date_range, scope = 'all', conversation_id } = typedArgs;
197
+ // Handle global scope by delegating to searchAllConversations
198
+ if (scope === 'global') {
199
+ const { GlobalIndex } = await import("../storage/GlobalIndex.js");
200
+ const { SQLiteManager } = await import("../storage/SQLiteManager.js");
201
+ const { SemanticSearch } = await import("../search/SemanticSearch.js");
202
+ const globalIndex = new GlobalIndex();
203
+ const projects = globalIndex.getAllProjects();
204
+ const allResults = [];
205
+ for (const project of projects) {
206
+ let projectDb = null;
207
+ try {
208
+ projectDb = new SQLiteManager({ dbPath: project.db_path, readOnly: true });
209
+ const semanticSearch = new SemanticSearch(projectDb);
210
+ const localResults = await semanticSearch.searchConversations(query, limit + offset);
211
+ const filteredResults = date_range
212
+ ? localResults.filter((r) => {
213
+ const timestamp = r.message.timestamp;
214
+ return timestamp >= date_range[0] && timestamp <= date_range[1];
215
+ })
216
+ : localResults;
217
+ for (const result of filteredResults) {
218
+ allResults.push({
219
+ conversation_id: result.conversation.id,
220
+ message_id: result.message.id,
221
+ timestamp: new Date(result.message.timestamp).toISOString(),
222
+ similarity: result.similarity,
223
+ snippet: result.snippet,
224
+ git_branch: result.conversation.git_branch,
225
+ message_type: result.message.message_type,
226
+ role: result.message.role,
227
+ });
228
+ }
229
+ }
230
+ catch (_error) {
231
+ // Skip projects that fail to search (embedding errors, etc.)
232
+ // Continue to next project
233
+ continue;
234
+ }
235
+ finally {
236
+ if (projectDb) {
237
+ projectDb.close();
238
+ }
239
+ }
240
+ }
241
+ allResults.sort((a, b) => b.similarity - a.similarity);
242
+ const paginatedResults = allResults.slice(offset, offset + limit);
243
+ return {
244
+ query,
245
+ results: paginatedResults,
246
+ total_found: paginatedResults.length,
247
+ has_more: offset + limit < allResults.length,
248
+ offset,
249
+ scope: 'global',
250
+ };
174
251
  }
175
- const results = await this.memory.search(query, limit);
252
+ // Handle current session scope
253
+ if (scope === 'current') {
254
+ if (!conversation_id) {
255
+ throw new Error("conversation_id is required when scope='current'");
256
+ }
257
+ const results = await this.memory.search(query, limit + offset);
258
+ const filteredResults = results.filter(r => r.conversation.id === conversation_id);
259
+ const dateFilteredResults = date_range
260
+ ? filteredResults.filter(r => {
261
+ const timestamp = r.message.timestamp;
262
+ return timestamp >= date_range[0] && timestamp <= date_range[1];
263
+ })
264
+ : filteredResults;
265
+ const paginatedResults = dateFilteredResults.slice(offset, offset + limit);
266
+ return {
267
+ query,
268
+ results: paginatedResults.map((r) => ({
269
+ conversation_id: r.conversation.id,
270
+ message_id: r.message.id,
271
+ timestamp: new Date(r.message.timestamp).toISOString(),
272
+ similarity: r.similarity,
273
+ snippet: r.snippet,
274
+ git_branch: r.conversation.git_branch,
275
+ message_type: r.message.message_type,
276
+ role: r.message.role,
277
+ })),
278
+ total_found: paginatedResults.length,
279
+ has_more: offset + limit < dateFilteredResults.length,
280
+ offset,
281
+ scope: 'current',
282
+ };
283
+ }
284
+ // Handle 'all' scope (default) - all sessions in current project
285
+ const results = await this.memory.search(query, limit + offset);
286
+ const filteredResults = date_range
287
+ ? results.filter(r => {
288
+ const timestamp = r.message.timestamp;
289
+ return timestamp >= date_range[0] && timestamp <= date_range[1];
290
+ })
291
+ : results;
292
+ const paginatedResults = filteredResults.slice(offset, offset + limit);
176
293
  return {
177
294
  query,
178
- results: results.map((r) => ({
295
+ results: paginatedResults.map((r) => ({
179
296
  conversation_id: r.conversation.id,
180
297
  message_id: r.message.id,
181
298
  timestamp: new Date(r.message.timestamp).toISOString(),
@@ -185,7 +302,10 @@ export class ToolHandlers {
185
302
  message_type: r.message.message_type,
186
303
  role: r.message.role,
187
304
  })),
188
- total_found: results.length,
305
+ total_found: paginatedResults.length,
306
+ has_more: offset + limit < filteredResults.length,
307
+ offset,
308
+ scope: 'all',
189
309
  };
190
310
  }
191
311
  /**
@@ -231,17 +351,49 @@ export class ToolHandlers {
231
351
  */
232
352
  async getDecisions(args) {
233
353
  const typedArgs = args;
234
- const { query, file_path, limit = 10 } = typedArgs;
235
- const results = await this.memory.searchDecisions(query, limit);
354
+ const { query, file_path, limit = 10, offset = 0, scope = 'all', conversation_id } = typedArgs;
355
+ // Handle global scope
356
+ if (scope === 'global') {
357
+ const globalResponse = await this.getAllDecisions({ query, file_path, limit, offset, source_type: 'all' });
358
+ return {
359
+ query,
360
+ file_path,
361
+ decisions: globalResponse.decisions.map(d => ({
362
+ decision_id: d.decision_id,
363
+ decision_text: d.decision_text,
364
+ rationale: d.rationale,
365
+ alternatives_considered: d.alternatives_considered,
366
+ rejected_reasons: d.rejected_reasons,
367
+ context: d.context,
368
+ related_files: d.related_files,
369
+ related_commits: d.related_commits,
370
+ timestamp: d.timestamp,
371
+ similarity: d.similarity,
372
+ })),
373
+ total_found: globalResponse.total_found,
374
+ has_more: globalResponse.has_more,
375
+ offset: globalResponse.offset,
376
+ scope: 'global',
377
+ };
378
+ }
379
+ const results = await this.memory.searchDecisions(query, limit + offset);
236
380
  // Filter by file if specified
237
381
  let filteredResults = results;
238
382
  if (file_path) {
239
383
  filteredResults = results.filter((r) => r.decision.related_files.includes(file_path));
240
384
  }
385
+ // Filter by conversation_id if scope is 'current'
386
+ if (scope === 'current') {
387
+ if (!conversation_id) {
388
+ throw new Error("conversation_id is required when scope='current'");
389
+ }
390
+ filteredResults = filteredResults.filter((r) => r.decision.conversation_id === conversation_id);
391
+ }
392
+ const paginatedResults = filteredResults.slice(offset, offset + limit);
241
393
  return {
242
394
  query,
243
395
  file_path,
244
- decisions: filteredResults.map((r) => ({
396
+ decisions: paginatedResults.map((r) => ({
245
397
  decision_id: r.decision.id,
246
398
  decision_text: r.decision.decision_text,
247
399
  rationale: r.decision.rationale,
@@ -253,7 +405,10 @@ export class ToolHandlers {
253
405
  timestamp: new Date(r.decision.timestamp).toISOString(),
254
406
  similarity: r.similarity,
255
407
  })),
256
- total_found: filteredResults.length,
408
+ total_found: paginatedResults.length,
409
+ has_more: offset + limit < filteredResults.length,
410
+ offset,
411
+ scope,
257
412
  };
258
413
  }
259
414
  /**
@@ -356,7 +511,7 @@ export class ToolHandlers {
356
511
  */
357
512
  async getFileEvolution(args) {
358
513
  const typedArgs = args;
359
- const { file_path, include_decisions = true, include_commits = true } = typedArgs;
514
+ const { file_path, include_decisions = true, include_commits = true, limit = 50, offset = 0 } = typedArgs;
360
515
  const timeline = this.memory.getFileTimeline(file_path);
361
516
  const events = [];
362
517
  timeline.edits.forEach((edit) => {
@@ -396,10 +551,13 @@ export class ToolHandlers {
396
551
  }
397
552
  // Sort by timestamp (descending - most recent first)
398
553
  events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
554
+ // Apply pagination
555
+ const paginatedEvents = events.slice(offset, offset + limit);
399
556
  return {
400
557
  file_path,
401
558
  total_edits: timeline.edits.length,
402
- timeline: events,
559
+ timeline: paginatedEvents,
560
+ has_more: offset + limit < events.length,
403
561
  };
404
562
  }
405
563
  /**
@@ -442,24 +600,35 @@ export class ToolHandlers {
442
600
  */
443
601
  async linkCommitsToConversations(args) {
444
602
  const typedArgs = args;
445
- const { query, conversation_id, limit = 20 } = typedArgs;
603
+ const { query, conversation_id, limit = 20, offset = 0, scope = 'all' } = typedArgs;
604
+ // Global scope not supported for git commits (project-specific)
605
+ if (scope === 'global') {
606
+ throw new Error("Global scope is not supported for linkCommitsToConversations (git commits are project-specific)");
607
+ }
446
608
  let sql = "SELECT * FROM git_commits WHERE 1=1";
447
609
  const params = [];
448
- if (conversation_id) {
610
+ if (conversation_id || scope === 'current') {
611
+ const targetId = conversation_id || typedArgs.conversation_id;
612
+ if (!targetId) {
613
+ throw new Error("conversation_id is required when scope='current'");
614
+ }
449
615
  sql += " AND conversation_id = ?";
450
- params.push(conversation_id);
616
+ params.push(targetId);
451
617
  }
452
618
  if (query) {
453
619
  sql += " AND message LIKE ?";
454
620
  params.push(`%${query}%`);
455
621
  }
456
- sql += " ORDER BY timestamp DESC LIMIT ?";
457
- params.push(limit);
622
+ sql += ` ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
623
+ params.push(limit + 1); // Fetch one extra to determine has_more
624
+ params.push(offset);
458
625
  const commits = this.db.prepare(sql).all(...params);
626
+ const hasMore = commits.length > limit;
627
+ const results = hasMore ? commits.slice(0, limit) : commits;
459
628
  return {
460
629
  query,
461
630
  conversation_id,
462
- commits: commits.map((c) => ({
631
+ commits: results.map((c) => ({
463
632
  hash: c.hash.substring(0, 7),
464
633
  full_hash: c.hash,
465
634
  message: c.message,
@@ -469,7 +638,10 @@ export class ToolHandlers {
469
638
  files_changed: JSON.parse(c.files_changed || "[]"),
470
639
  conversation_id: c.conversation_id,
471
640
  })),
472
- total_found: commits.length,
641
+ total_found: results.length,
642
+ has_more: hasMore,
643
+ offset,
644
+ scope,
473
645
  };
474
646
  }
475
647
  /**
@@ -511,7 +683,28 @@ export class ToolHandlers {
511
683
  */
512
684
  async searchMistakes(args) {
513
685
  const typedArgs = args;
514
- const { query, mistake_type, limit = 10 } = typedArgs;
686
+ const { query, mistake_type, limit = 10, offset = 0, scope = 'all', conversation_id } = typedArgs;
687
+ // Handle global scope
688
+ if (scope === 'global') {
689
+ const globalResponse = await this.searchAllMistakes({ query, mistake_type, limit, offset, source_type: 'all' });
690
+ return {
691
+ query,
692
+ mistake_type,
693
+ mistakes: globalResponse.mistakes.map(m => ({
694
+ mistake_id: m.mistake_id,
695
+ mistake_type: m.mistake_type,
696
+ what_went_wrong: m.what_went_wrong,
697
+ correction: m.correction,
698
+ user_correction_message: m.user_correction_message,
699
+ files_affected: m.files_affected,
700
+ timestamp: m.timestamp,
701
+ })),
702
+ total_found: globalResponse.total_found,
703
+ has_more: globalResponse.has_more,
704
+ offset: globalResponse.offset,
705
+ scope: 'global',
706
+ };
707
+ }
515
708
  const sanitized = sanitizeForLike(query);
516
709
  let sql = "SELECT * FROM mistakes WHERE what_went_wrong LIKE ? ESCAPE '\\'";
517
710
  const params = [`%${sanitized}%`];
@@ -519,13 +712,24 @@ export class ToolHandlers {
519
712
  sql += " AND mistake_type = ?";
520
713
  params.push(mistake_type);
521
714
  }
522
- sql += " ORDER BY timestamp DESC LIMIT ?";
523
- params.push(limit);
715
+ // Filter by conversation_id if scope is 'current'
716
+ if (scope === 'current') {
717
+ if (!conversation_id) {
718
+ throw new Error("conversation_id is required when scope='current'");
719
+ }
720
+ sql += " AND conversation_id = ?";
721
+ params.push(conversation_id);
722
+ }
723
+ sql += ` ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
724
+ params.push(limit + 1); // Fetch one extra to determine has_more
725
+ params.push(offset);
524
726
  const mistakes = this.db.prepare(sql).all(...params);
727
+ const hasMore = mistakes.length > limit;
728
+ const results = hasMore ? mistakes.slice(0, limit) : mistakes;
525
729
  return {
526
730
  query,
527
731
  mistake_type,
528
- mistakes: mistakes.map((m) => ({
732
+ mistakes: results.map((m) => ({
529
733
  mistake_id: m.id,
530
734
  mistake_type: m.mistake_type,
531
735
  what_went_wrong: m.what_went_wrong,
@@ -534,7 +738,10 @@ export class ToolHandlers {
534
738
  files_affected: JSON.parse(m.files_affected || "[]"),
535
739
  timestamp: new Date(m.timestamp).toISOString(),
536
740
  })),
537
- total_found: mistakes.length,
741
+ total_found: results.length,
742
+ has_more: hasMore,
743
+ offset,
744
+ scope,
538
745
  };
539
746
  }
540
747
  /**
@@ -793,8 +1000,13 @@ export class ToolHandlers {
793
1000
  */
794
1001
  async findSimilarSessions(args) {
795
1002
  const typedArgs = args;
796
- const { query, limit = 5 } = typedArgs;
797
- const results = await this.memory.search(query, limit * 3); // Get more to group by conversation
1003
+ const { query, limit = 5, offset = 0, scope = 'all', conversation_id: _conversation_id } = typedArgs;
1004
+ // Note: scope='global' and scope='current' have limited usefulness for finding similar SESSIONS
1005
+ // but we implement them for API consistency
1006
+ if (scope === 'current') {
1007
+ throw new Error("scope='current' is not supported for findSimilarSessions (it finds sessions, not messages within a session)");
1008
+ }
1009
+ const results = await this.memory.search(query, (limit + offset) * 3); // Get more to group by conversation
798
1010
  // Group by conversation
799
1011
  const conversationMap = new Map();
800
1012
  for (const result of results) {
@@ -819,13 +1031,16 @@ export class ToolHandlers {
819
1031
  });
820
1032
  }
821
1033
  }
822
- const sessions = Array.from(conversationMap.values())
823
- .sort((a, b) => b.relevance_score - a.relevance_score)
824
- .slice(0, limit);
1034
+ const allSessions = Array.from(conversationMap.values())
1035
+ .sort((a, b) => b.relevance_score - a.relevance_score);
1036
+ const sessions = allSessions.slice(offset, offset + limit);
825
1037
  return {
826
1038
  query,
827
1039
  sessions,
828
1040
  total_found: sessions.length,
1041
+ has_more: offset + limit < allSessions.length,
1042
+ offset,
1043
+ scope,
829
1044
  };
830
1045
  }
831
1046
  /**
@@ -869,20 +1084,24 @@ export class ToolHandlers {
869
1084
  */
870
1085
  async recallAndApply(args) {
871
1086
  const typedArgs = args;
872
- const { query, context_types = ["conversations", "decisions", "mistakes", "file_changes", "commits"], file_path, date_range, limit = 5 } = typedArgs;
1087
+ const { query, context_types = ["conversations", "decisions", "mistakes", "file_changes", "commits"], file_path, date_range, limit = 5, offset = 0, scope = 'all', conversation_id } = typedArgs;
873
1088
  const recalled = {};
874
1089
  let totalItems = 0;
875
1090
  const suggestions = [];
876
1091
  // 1. Recall conversations if requested
877
1092
  if (context_types.includes("conversations")) {
878
- const searchResults = await this.memory.search(query, limit);
879
- // Apply date filter if provided
880
- const filteredResults = date_range
881
- ? searchResults.filter(r => r.message.timestamp >= date_range[0] && r.message.timestamp <= date_range[1])
882
- : searchResults;
883
- recalled.conversations = filteredResults.map(result => ({
884
- session_id: result.conversation.id || "unknown",
885
- timestamp: new Date(result.message.timestamp).toISOString(),
1093
+ // Use searchConversations with scope support
1094
+ const convResponse = await this.searchConversations({
1095
+ query,
1096
+ limit,
1097
+ offset,
1098
+ date_range,
1099
+ scope,
1100
+ conversation_id,
1101
+ });
1102
+ recalled.conversations = convResponse.results.map(result => ({
1103
+ session_id: result.conversation_id,
1104
+ timestamp: result.timestamp,
886
1105
  snippet: result.snippet,
887
1106
  relevance_score: result.similarity,
888
1107
  }));
@@ -893,25 +1112,24 @@ export class ToolHandlers {
893
1112
  }
894
1113
  // 2. Recall decisions if requested
895
1114
  if (context_types.includes("decisions")) {
896
- const decisions = this.db.getDatabase()
897
- .prepare(`
898
- SELECT id, decision_text, rationale, alternatives_considered, rejected_reasons, context, related_files, timestamp
899
- FROM decisions
900
- WHERE decision_text LIKE ? ${file_path ? 'AND related_files LIKE ?' : ''}
901
- ${date_range ? 'AND timestamp BETWEEN ? AND ?' : ''}
902
- ORDER BY timestamp DESC
903
- LIMIT ?
904
- `)
905
- .all(`%${sanitizeForLike(query)}%`, ...(file_path ? [`%${sanitizeForLike(file_path)}%`] : []), ...(date_range ? [date_range[0], date_range[1]] : []), limit);
906
- recalled.decisions = decisions.map(d => ({
907
- decision_id: d.id,
908
- type: d.context,
1115
+ // Use getDecisions with scope support
1116
+ const decisionsResponse = await this.getDecisions({
1117
+ query,
1118
+ file_path,
1119
+ limit,
1120
+ offset,
1121
+ scope,
1122
+ conversation_id,
1123
+ });
1124
+ recalled.decisions = decisionsResponse.decisions.map(d => ({
1125
+ decision_id: d.decision_id,
1126
+ type: d.context || 'unknown',
909
1127
  description: d.decision_text,
910
1128
  rationale: d.rationale || undefined,
911
- alternatives: d.alternatives_considered ? JSON.parse(d.alternatives_considered) : undefined,
912
- rejected_approaches: d.rejected_reasons ? JSON.parse(d.rejected_reasons) : undefined,
913
- affects_components: JSON.parse(d.related_files),
914
- timestamp: new Date(d.timestamp).toISOString(),
1129
+ alternatives: d.alternatives_considered,
1130
+ rejected_approaches: Object.values(d.rejected_reasons),
1131
+ affects_components: d.related_files,
1132
+ timestamp: d.timestamp,
915
1133
  }));
916
1134
  totalItems += recalled.decisions.length;
917
1135
  if (recalled.decisions.length > 0) {
@@ -920,25 +1138,23 @@ export class ToolHandlers {
920
1138
  }
921
1139
  // 3. Recall mistakes if requested
922
1140
  if (context_types.includes("mistakes")) {
923
- const mistakes = this.db.getDatabase()
924
- .prepare(`
925
- SELECT id, mistake_type, what_went_wrong, correction, user_correction_message, files_affected, timestamp
926
- FROM mistakes
927
- WHERE what_went_wrong LIKE ? ${file_path ? 'AND files_affected LIKE ?' : ''}
928
- ${date_range ? 'AND timestamp BETWEEN ? AND ?' : ''}
929
- ORDER BY timestamp DESC
930
- LIMIT ?
931
- `)
932
- .all(`%${sanitizeForLike(query)}%`, ...(file_path ? [`%${sanitizeForLike(file_path)}%`] : []), ...(date_range ? [date_range[0], date_range[1]] : []), limit);
933
- recalled.mistakes = mistakes.map(m => ({
934
- mistake_id: m.id,
1141
+ // Use searchMistakes with scope support
1142
+ const mistakesResponse = await this.searchMistakes({
1143
+ query,
1144
+ limit,
1145
+ offset,
1146
+ scope,
1147
+ conversation_id,
1148
+ });
1149
+ recalled.mistakes = mistakesResponse.mistakes.map(m => ({
1150
+ mistake_id: m.mistake_id,
935
1151
  type: m.mistake_type,
936
1152
  description: m.what_went_wrong,
937
1153
  what_happened: m.what_went_wrong,
938
1154
  how_fixed: m.correction || undefined,
939
1155
  lesson_learned: m.user_correction_message || undefined,
940
- files_affected: JSON.parse(m.files_affected),
941
- timestamp: new Date(m.timestamp).toISOString(),
1156
+ files_affected: m.files_affected,
1157
+ timestamp: m.timestamp,
942
1158
  }));
943
1159
  totalItems += recalled.mistakes.length;
944
1160
  if (recalled.mistakes.length > 0) {
@@ -1425,7 +1641,7 @@ export class ToolHandlers {
1425
1641
  const { join } = await import("path");
1426
1642
  const { existsSync, readdirSync } = await import("fs");
1427
1643
  const typedArgs = args;
1428
- const { include_codex = true, include_claude_code = true, codex_path = join(homedir(), ".codex"), claude_projects_path = join(homedir(), ".claude", "projects"), } = typedArgs;
1644
+ const { include_codex = true, include_claude_code = true, codex_path = join(homedir(), ".codex"), claude_projects_path = join(homedir(), ".claude", "projects"), incremental = true, } = typedArgs;
1429
1645
  const globalIndex = new GlobalIndex();
1430
1646
  try {
1431
1647
  const projects = [];
@@ -1440,13 +1656,24 @@ export class ToolHandlers {
1440
1656
  const { CodexConversationParser } = await import("../parsers/CodexConversationParser.js");
1441
1657
  const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1442
1658
  const { ConversationStorage } = await import("../storage/ConversationStorage.js");
1659
+ const { SemanticSearch } = await import("../search/SemanticSearch.js");
1660
+ const { DecisionExtractor } = await import("../parsers/DecisionExtractor.js");
1661
+ const { MistakeExtractor } = await import("../parsers/MistakeExtractor.js");
1443
1662
  // Create dedicated database for Codex
1444
1663
  const codexDbPath = join(codex_path, ".codex-conversations-memory.db");
1445
1664
  const codexDb = new SQLiteManager({ dbPath: codexDbPath });
1446
1665
  const codexStorage = new ConversationStorage(codexDb);
1666
+ // Get last indexed time for incremental mode
1667
+ let codexLastIndexedMs;
1668
+ if (incremental) {
1669
+ const existingProject = globalIndex.getProject(codex_path);
1670
+ if (existingProject) {
1671
+ codexLastIndexedMs = existingProject.last_indexed;
1672
+ }
1673
+ }
1447
1674
  // Parse Codex sessions
1448
1675
  const parser = new CodexConversationParser();
1449
- const parseResult = parser.parseSession(codex_path);
1676
+ const parseResult = parser.parseSession(codex_path, undefined, codexLastIndexedMs);
1450
1677
  // Store all parsed data
1451
1678
  await codexStorage.storeConversations(parseResult.conversations);
1452
1679
  await codexStorage.storeMessages(parseResult.messages);
@@ -1454,6 +1681,25 @@ export class ToolHandlers {
1454
1681
  await codexStorage.storeToolResults(parseResult.tool_results);
1455
1682
  await codexStorage.storeFileEdits(parseResult.file_edits);
1456
1683
  await codexStorage.storeThinkingBlocks(parseResult.thinking_blocks);
1684
+ // Extract and store decisions
1685
+ const decisionExtractor = new DecisionExtractor();
1686
+ const decisions = decisionExtractor.extractDecisions(parseResult.messages, parseResult.thinking_blocks);
1687
+ await codexStorage.storeDecisions(decisions);
1688
+ // Extract and store mistakes
1689
+ const mistakeExtractor = new MistakeExtractor();
1690
+ const mistakes = mistakeExtractor.extractMistakes(parseResult.messages, parseResult.tool_results);
1691
+ await codexStorage.storeMistakes(mistakes);
1692
+ // Generate embeddings for semantic search
1693
+ try {
1694
+ const semanticSearch = new SemanticSearch(codexDb);
1695
+ await semanticSearch.indexMessages(parseResult.messages, incremental);
1696
+ await semanticSearch.indexDecisions(decisions, incremental);
1697
+ console.log(`✓ Generated embeddings for Codex project`);
1698
+ }
1699
+ catch (embedError) {
1700
+ console.warn("⚠️ Embedding generation failed for Codex:", embedError.message);
1701
+ console.warn(" FTS fallback will be used for search");
1702
+ }
1457
1703
  // Get stats from the database
1458
1704
  const stats = codexDb.getDatabase()
1459
1705
  .prepare("SELECT COUNT(*) as count FROM conversations")
@@ -1503,38 +1749,102 @@ export class ToolHandlers {
1503
1749
  // Index Claude Code projects if requested
1504
1750
  if (include_claude_code && existsSync(claude_projects_path)) {
1505
1751
  try {
1752
+ const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1753
+ const { ConversationStorage } = await import("../storage/ConversationStorage.js");
1754
+ const { ConversationParser } = await import("../parsers/ConversationParser.js");
1755
+ const { DecisionExtractor } = await import("../parsers/DecisionExtractor.js");
1756
+ const { MistakeExtractor } = await import("../parsers/MistakeExtractor.js");
1757
+ const { statSync } = await import("fs");
1506
1758
  const projectFolders = readdirSync(claude_projects_path);
1507
1759
  for (const folder of projectFolders) {
1508
1760
  const folderPath = join(claude_projects_path, folder);
1509
1761
  try {
1510
- // Try to determine the original project path from folder name
1511
- const projectPath = folderPath; // Simplified for now
1512
- // Index this project
1513
- const indexResult = await this.indexConversations({
1514
- project_path: projectPath,
1515
- });
1516
- if (indexResult.success) {
1517
- // Register in global index
1518
- globalIndex.registerProject({
1519
- project_path: projectPath,
1520
- source_type: "claude-code",
1521
- db_path: this.db.getDbPath(),
1522
- message_count: indexResult.stats.messages.count,
1523
- conversation_count: indexResult.stats.conversations.count,
1524
- decision_count: indexResult.stats.decisions.count,
1525
- mistake_count: indexResult.stats.mistakes.count,
1526
- });
1527
- projects.push({
1528
- project_path: projectPath,
1529
- source_type: "claude-code",
1530
- message_count: indexResult.stats.messages.count,
1531
- conversation_count: indexResult.stats.conversations.count,
1532
- });
1533
- totalMessages += indexResult.stats.messages.count;
1534
- totalConversations += indexResult.stats.conversations.count;
1535
- totalDecisions += indexResult.stats.decisions.count;
1536
- totalMistakes += indexResult.stats.mistakes.count;
1762
+ // Skip if not a directory
1763
+ if (!statSync(folderPath).isDirectory()) {
1764
+ continue;
1765
+ }
1766
+ // Create dedicated database for this Claude Code project
1767
+ const projectDbPath = join(folderPath, ".claude-conversations-memory.db");
1768
+ const projectDb = new SQLiteManager({ dbPath: projectDbPath });
1769
+ const projectStorage = new ConversationStorage(projectDb);
1770
+ // Get last indexed time for incremental mode
1771
+ let lastIndexedMs;
1772
+ if (incremental) {
1773
+ const existingProject = globalIndex.getProject(folderPath);
1774
+ if (existingProject) {
1775
+ lastIndexedMs = existingProject.last_indexed;
1776
+ }
1777
+ }
1778
+ // Parse Claude Code conversations directly from this folder
1779
+ const parser = new ConversationParser();
1780
+ const parseResult = parser.parseFromFolder(folderPath, undefined, lastIndexedMs);
1781
+ // Skip empty projects
1782
+ if (parseResult.messages.length === 0) {
1783
+ projectDb.close();
1784
+ continue;
1785
+ }
1786
+ // Store all parsed data
1787
+ await projectStorage.storeConversations(parseResult.conversations);
1788
+ await projectStorage.storeMessages(parseResult.messages);
1789
+ await projectStorage.storeToolUses(parseResult.tool_uses);
1790
+ await projectStorage.storeToolResults(parseResult.tool_results);
1791
+ await projectStorage.storeFileEdits(parseResult.file_edits);
1792
+ await projectStorage.storeThinkingBlocks(parseResult.thinking_blocks);
1793
+ // Extract and store decisions
1794
+ const decisionExtractor = new DecisionExtractor();
1795
+ const decisions = decisionExtractor.extractDecisions(parseResult.messages, parseResult.thinking_blocks);
1796
+ await projectStorage.storeDecisions(decisions);
1797
+ // Extract and store mistakes
1798
+ const mistakeExtractor = new MistakeExtractor();
1799
+ const mistakes = mistakeExtractor.extractMistakes(parseResult.messages, parseResult.tool_results);
1800
+ await projectStorage.storeMistakes(mistakes);
1801
+ // Generate embeddings for semantic search
1802
+ try {
1803
+ const { SemanticSearch } = await import("../search/SemanticSearch.js");
1804
+ const semanticSearch = new SemanticSearch(projectDb);
1805
+ await semanticSearch.indexMessages(parseResult.messages, incremental);
1806
+ await semanticSearch.indexDecisions(decisions, incremental);
1807
+ console.log(`✓ Generated embeddings for project: ${folder}`);
1808
+ }
1809
+ catch (embedError) {
1810
+ console.warn(`⚠️ Embedding generation failed for ${folder}:`, embedError.message);
1811
+ console.warn(" FTS fallback will be used for search");
1537
1812
  }
1813
+ // Get stats from the database
1814
+ const stats = projectDb.getDatabase()
1815
+ .prepare("SELECT COUNT(*) as count FROM conversations")
1816
+ .get();
1817
+ const messageStats = projectDb.getDatabase()
1818
+ .prepare("SELECT COUNT(*) as count FROM messages")
1819
+ .get();
1820
+ const decisionStats = projectDb.getDatabase()
1821
+ .prepare("SELECT COUNT(*) as count FROM decisions")
1822
+ .get();
1823
+ const mistakeStats = projectDb.getDatabase()
1824
+ .prepare("SELECT COUNT(*) as count FROM mistakes")
1825
+ .get();
1826
+ // Register in global index with the project-specific database path
1827
+ globalIndex.registerProject({
1828
+ project_path: folderPath,
1829
+ source_type: "claude-code",
1830
+ db_path: projectDbPath,
1831
+ message_count: messageStats.count,
1832
+ conversation_count: stats.count,
1833
+ decision_count: decisionStats.count,
1834
+ mistake_count: mistakeStats.count,
1835
+ });
1836
+ projects.push({
1837
+ project_path: folderPath,
1838
+ source_type: "claude-code",
1839
+ message_count: messageStats.count,
1840
+ conversation_count: stats.count,
1841
+ });
1842
+ totalMessages += messageStats.count;
1843
+ totalConversations += stats.count;
1844
+ totalDecisions += decisionStats.count;
1845
+ totalMistakes += mistakeStats.count;
1846
+ // Close the project database
1847
+ projectDb.close();
1538
1848
  }
1539
1849
  catch (error) {
1540
1850
  errors.push({
@@ -1583,7 +1893,7 @@ export class ToolHandlers {
1583
1893
  const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1584
1894
  const { SemanticSearch } = await import("../search/SemanticSearch.js");
1585
1895
  const typedArgs = args;
1586
- const { query, limit = 20, date_range, source_type = "all" } = typedArgs;
1896
+ const { query, limit = 20, offset = 0, date_range, source_type = "all" } = typedArgs;
1587
1897
  const globalIndex = new GlobalIndex();
1588
1898
  try {
1589
1899
  const projects = globalIndex.getAllProjects(source_type === "all" ? undefined : source_type);
@@ -1638,18 +1948,21 @@ export class ToolHandlers {
1638
1948
  }
1639
1949
  }
1640
1950
  }
1641
- // Sort by similarity and limit
1642
- const sortedResults = allResults.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
1951
+ // Sort by similarity and paginate
1952
+ const sortedResults = allResults.sort((a, b) => b.similarity - a.similarity);
1953
+ const paginatedResults = sortedResults.slice(offset, offset + limit);
1643
1954
  return {
1644
1955
  query,
1645
- results: sortedResults,
1646
- total_found: sortedResults.length,
1956
+ results: paginatedResults,
1957
+ total_found: paginatedResults.length,
1958
+ has_more: offset + limit < sortedResults.length,
1959
+ offset,
1647
1960
  projects_searched: projects.length,
1648
1961
  search_stats: {
1649
1962
  claude_code_results: claudeCodeResults,
1650
1963
  codex_results: codexResults,
1651
1964
  },
1652
- message: `Found ${sortedResults.length} result(s) across ${projects.length} project(s)`,
1965
+ message: `Found ${paginatedResults.length} result(s) across ${projects.length} project(s)`,
1653
1966
  };
1654
1967
  }
1655
1968
  finally {
@@ -1665,19 +1978,64 @@ export class ToolHandlers {
1665
1978
  */
1666
1979
  async getAllDecisions(args) {
1667
1980
  const { GlobalIndex } = await import("../storage/GlobalIndex.js");
1981
+ const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1668
1982
  const typedArgs = args;
1669
- const { query, file_path: _file_path } = typedArgs;
1983
+ const { query, file_path, limit = 20, offset = 0, source_type = 'all' } = typedArgs;
1670
1984
  const globalIndex = new GlobalIndex();
1671
1985
  try {
1672
- const projects = globalIndex.getAllProjects();
1673
- // TODO: Implement cross-project decision search
1674
- // For now, return empty results with proper structure
1986
+ const projects = globalIndex.getAllProjects(source_type === "all" ? undefined : source_type);
1987
+ const allDecisions = [];
1988
+ for (const project of projects) {
1989
+ let projectDb = null;
1990
+ try {
1991
+ projectDb = new SQLiteManager({ dbPath: project.db_path, readOnly: true });
1992
+ const sanitized = sanitizeForLike(query);
1993
+ let sql = "SELECT * FROM decisions WHERE decision_text LIKE ? ESCAPE '\\\\'";
1994
+ const params = [`%${sanitized}%`];
1995
+ if (file_path) {
1996
+ sql += " AND related_files LIKE ?";
1997
+ params.push(`%${file_path}%`);
1998
+ }
1999
+ sql += " ORDER BY timestamp DESC LIMIT ?";
2000
+ params.push(limit + offset + 1);
2001
+ const decisions = projectDb.prepare(sql).all(...params);
2002
+ for (const d of decisions) {
2003
+ allDecisions.push({
2004
+ decision_id: d.id,
2005
+ decision_text: d.decision_text,
2006
+ rationale: d.rationale,
2007
+ alternatives_considered: JSON.parse(d.alternatives_considered || "[]"),
2008
+ rejected_reasons: JSON.parse(d.rejected_reasons || "{}"),
2009
+ context: d.context,
2010
+ related_files: JSON.parse(d.related_files || "[]"),
2011
+ related_commits: JSON.parse(d.related_commits || "[]"),
2012
+ timestamp: new Date(d.timestamp).toISOString(),
2013
+ similarity: DEFAULT_SIMILARITY_SCORE,
2014
+ project_path: project.project_path,
2015
+ source_type: project.source_type,
2016
+ });
2017
+ }
2018
+ }
2019
+ catch (_error) {
2020
+ continue;
2021
+ }
2022
+ finally {
2023
+ if (projectDb) {
2024
+ projectDb.close();
2025
+ }
2026
+ }
2027
+ }
2028
+ // Sort by timestamp and paginate
2029
+ const sortedDecisions = allDecisions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
2030
+ const paginatedDecisions = sortedDecisions.slice(offset, offset + limit);
1675
2031
  return {
1676
2032
  query,
1677
- decisions: [],
1678
- total_found: 0,
2033
+ decisions: paginatedDecisions,
2034
+ total_found: paginatedDecisions.length,
2035
+ has_more: offset + limit < sortedDecisions.length,
2036
+ offset,
1679
2037
  projects_searched: projects.length,
1680
- message: `Cross-project decision search not yet implemented (would search ${projects.length} project(s))`,
2038
+ message: `Found ${paginatedDecisions.length} decision(s) across ${projects.length} project(s)`,
1681
2039
  };
1682
2040
  }
1683
2041
  finally {
@@ -1692,19 +2050,61 @@ export class ToolHandlers {
1692
2050
  */
1693
2051
  async searchAllMistakes(args) {
1694
2052
  const { GlobalIndex } = await import("../storage/GlobalIndex.js");
2053
+ const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1695
2054
  const typedArgs = args;
1696
- const { query, mistake_type: _mistake_type } = typedArgs;
2055
+ const { query, mistake_type, limit = 20, offset = 0, source_type = 'all' } = typedArgs;
1697
2056
  const globalIndex = new GlobalIndex();
1698
2057
  try {
1699
- const projects = globalIndex.getAllProjects();
1700
- // TODO: Implement cross-project mistake search
1701
- // For now, return empty results with proper structure
2058
+ const projects = globalIndex.getAllProjects(source_type === "all" ? undefined : source_type);
2059
+ const allMistakes = [];
2060
+ for (const project of projects) {
2061
+ let projectDb = null;
2062
+ try {
2063
+ projectDb = new SQLiteManager({ dbPath: project.db_path, readOnly: true });
2064
+ const sanitized = sanitizeForLike(query);
2065
+ let sql = "SELECT * FROM mistakes WHERE what_went_wrong LIKE ? ESCAPE '\\\\'";
2066
+ const params = [`%${sanitized}%`];
2067
+ if (mistake_type) {
2068
+ sql += " AND mistake_type = ?";
2069
+ params.push(mistake_type);
2070
+ }
2071
+ sql += " ORDER BY timestamp DESC LIMIT ?";
2072
+ params.push(limit + offset + 1);
2073
+ const mistakes = projectDb.prepare(sql).all(...params);
2074
+ for (const m of mistakes) {
2075
+ allMistakes.push({
2076
+ mistake_id: m.id,
2077
+ mistake_type: m.mistake_type,
2078
+ what_went_wrong: m.what_went_wrong,
2079
+ correction: m.correction,
2080
+ user_correction_message: m.user_correction_message,
2081
+ files_affected: JSON.parse(m.files_affected || "[]"),
2082
+ timestamp: new Date(m.timestamp).toISOString(),
2083
+ project_path: project.project_path,
2084
+ source_type: project.source_type,
2085
+ });
2086
+ }
2087
+ }
2088
+ catch (_error) {
2089
+ continue;
2090
+ }
2091
+ finally {
2092
+ if (projectDb) {
2093
+ projectDb.close();
2094
+ }
2095
+ }
2096
+ }
2097
+ // Sort by timestamp and paginate
2098
+ const sortedMistakes = allMistakes.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
2099
+ const paginatedMistakes = sortedMistakes.slice(offset, offset + limit);
1702
2100
  return {
1703
2101
  query,
1704
- mistakes: [],
1705
- total_found: 0,
2102
+ mistakes: paginatedMistakes,
2103
+ total_found: paginatedMistakes.length,
2104
+ has_more: offset + limit < sortedMistakes.length,
2105
+ offset,
1706
2106
  projects_searched: projects.length,
1707
- message: `Cross-project mistake search not yet implemented (would search ${projects.length} project(s))`,
2107
+ message: `Found ${paginatedMistakes.length} mistake(s) across ${projects.length} project(s)`,
1708
2108
  };
1709
2109
  }
1710
2110
  finally {