claude-conversation-memory-mcp 1.4.0 → 1.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.
Files changed (35) hide show
  1. package/README.md +230 -31
  2. package/dist/mcp-server.d.ts.map +1 -1
  3. package/dist/mcp-server.js +12 -0
  4. package/dist/mcp-server.js.map +1 -1
  5. package/dist/parsers/CodexConversationParser.d.ts +51 -0
  6. package/dist/parsers/CodexConversationParser.d.ts.map +1 -0
  7. package/dist/parsers/CodexConversationParser.js +279 -0
  8. package/dist/parsers/CodexConversationParser.js.map +1 -0
  9. package/dist/parsers/ConversationParser.d.ts +1 -0
  10. package/dist/parsers/ConversationParser.d.ts.map +1 -1
  11. package/dist/parsers/ConversationParser.js.map +1 -1
  12. package/dist/storage/ConversationStorage.d.ts.map +1 -1
  13. package/dist/storage/ConversationStorage.js +3 -3
  14. package/dist/storage/ConversationStorage.js.map +1 -1
  15. package/dist/storage/GlobalIndex.d.ts +125 -0
  16. package/dist/storage/GlobalIndex.d.ts.map +1 -0
  17. package/dist/storage/GlobalIndex.js +243 -0
  18. package/dist/storage/GlobalIndex.js.map +1 -0
  19. package/dist/storage/SQLiteManager.d.ts +4 -0
  20. package/dist/storage/SQLiteManager.d.ts.map +1 -1
  21. package/dist/storage/SQLiteManager.js +36 -2
  22. package/dist/storage/SQLiteManager.js.map +1 -1
  23. package/dist/storage/schema.sql +26 -0
  24. package/dist/tools/ToolDefinitions.d.ts +218 -0
  25. package/dist/tools/ToolDefinitions.d.ts.map +1 -1
  26. package/dist/tools/ToolDefinitions.js +223 -6
  27. package/dist/tools/ToolDefinitions.js.map +1 -1
  28. package/dist/tools/ToolHandlers.d.ts +52 -0
  29. package/dist/tools/ToolHandlers.d.ts.map +1 -1
  30. package/dist/tools/ToolHandlers.js +668 -68
  31. package/dist/tools/ToolHandlers.js.map +1 -1
  32. package/dist/types/ToolTypes.d.ts +124 -0
  33. package/dist/types/ToolTypes.d.ts.map +1 -1
  34. package/package.json +2 -1
  35. package/scripts/changelog-check.sh +62 -0
@@ -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,101 @@ 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
+ finally {
231
+ if (projectDb) {
232
+ projectDb.close();
233
+ }
234
+ }
235
+ }
236
+ allResults.sort((a, b) => b.similarity - a.similarity);
237
+ const paginatedResults = allResults.slice(offset, offset + limit);
238
+ return {
239
+ query,
240
+ results: paginatedResults,
241
+ total_found: paginatedResults.length,
242
+ has_more: offset + limit < allResults.length,
243
+ offset,
244
+ scope: 'global',
245
+ };
174
246
  }
175
- const results = await this.memory.search(query, limit);
247
+ // Handle current session scope
248
+ if (scope === 'current') {
249
+ if (!conversation_id) {
250
+ throw new Error("conversation_id is required when scope='current'");
251
+ }
252
+ const results = await this.memory.search(query, limit + offset);
253
+ const filteredResults = results.filter(r => r.conversation.id === conversation_id);
254
+ const dateFilteredResults = date_range
255
+ ? filteredResults.filter(r => {
256
+ const timestamp = r.message.timestamp;
257
+ return timestamp >= date_range[0] && timestamp <= date_range[1];
258
+ })
259
+ : filteredResults;
260
+ const paginatedResults = dateFilteredResults.slice(offset, offset + limit);
261
+ return {
262
+ query,
263
+ results: paginatedResults.map((r) => ({
264
+ conversation_id: r.conversation.id,
265
+ message_id: r.message.id,
266
+ timestamp: new Date(r.message.timestamp).toISOString(),
267
+ similarity: r.similarity,
268
+ snippet: r.snippet,
269
+ git_branch: r.conversation.git_branch,
270
+ message_type: r.message.message_type,
271
+ role: r.message.role,
272
+ })),
273
+ total_found: paginatedResults.length,
274
+ has_more: offset + limit < dateFilteredResults.length,
275
+ offset,
276
+ scope: 'current',
277
+ };
278
+ }
279
+ // Handle 'all' scope (default) - all sessions in current project
280
+ const results = await this.memory.search(query, limit + offset);
281
+ const filteredResults = date_range
282
+ ? results.filter(r => {
283
+ const timestamp = r.message.timestamp;
284
+ return timestamp >= date_range[0] && timestamp <= date_range[1];
285
+ })
286
+ : results;
287
+ const paginatedResults = filteredResults.slice(offset, offset + limit);
176
288
  return {
177
289
  query,
178
- results: results.map((r) => ({
290
+ results: paginatedResults.map((r) => ({
179
291
  conversation_id: r.conversation.id,
180
292
  message_id: r.message.id,
181
293
  timestamp: new Date(r.message.timestamp).toISOString(),
@@ -185,7 +297,10 @@ export class ToolHandlers {
185
297
  message_type: r.message.message_type,
186
298
  role: r.message.role,
187
299
  })),
188
- total_found: results.length,
300
+ total_found: paginatedResults.length,
301
+ has_more: offset + limit < filteredResults.length,
302
+ offset,
303
+ scope: 'all',
189
304
  };
190
305
  }
191
306
  /**
@@ -231,17 +346,49 @@ export class ToolHandlers {
231
346
  */
232
347
  async getDecisions(args) {
233
348
  const typedArgs = args;
234
- const { query, file_path, limit = 10 } = typedArgs;
235
- const results = await this.memory.searchDecisions(query, limit);
349
+ const { query, file_path, limit = 10, offset = 0, scope = 'all', conversation_id } = typedArgs;
350
+ // Handle global scope
351
+ if (scope === 'global') {
352
+ const globalResponse = await this.getAllDecisions({ query, file_path, limit, offset, source_type: 'all' });
353
+ return {
354
+ query,
355
+ file_path,
356
+ decisions: globalResponse.decisions.map(d => ({
357
+ decision_id: d.decision_id,
358
+ decision_text: d.decision_text,
359
+ rationale: d.rationale,
360
+ alternatives_considered: d.alternatives_considered,
361
+ rejected_reasons: d.rejected_reasons,
362
+ context: d.context,
363
+ related_files: d.related_files,
364
+ related_commits: d.related_commits,
365
+ timestamp: d.timestamp,
366
+ similarity: d.similarity,
367
+ })),
368
+ total_found: globalResponse.total_found,
369
+ has_more: globalResponse.has_more,
370
+ offset: globalResponse.offset,
371
+ scope: 'global',
372
+ };
373
+ }
374
+ const results = await this.memory.searchDecisions(query, limit + offset);
236
375
  // Filter by file if specified
237
376
  let filteredResults = results;
238
377
  if (file_path) {
239
378
  filteredResults = results.filter((r) => r.decision.related_files.includes(file_path));
240
379
  }
380
+ // Filter by conversation_id if scope is 'current'
381
+ if (scope === 'current') {
382
+ if (!conversation_id) {
383
+ throw new Error("conversation_id is required when scope='current'");
384
+ }
385
+ filteredResults = filteredResults.filter((r) => r.decision.conversation_id === conversation_id);
386
+ }
387
+ const paginatedResults = filteredResults.slice(offset, offset + limit);
241
388
  return {
242
389
  query,
243
390
  file_path,
244
- decisions: filteredResults.map((r) => ({
391
+ decisions: paginatedResults.map((r) => ({
245
392
  decision_id: r.decision.id,
246
393
  decision_text: r.decision.decision_text,
247
394
  rationale: r.decision.rationale,
@@ -253,7 +400,10 @@ export class ToolHandlers {
253
400
  timestamp: new Date(r.decision.timestamp).toISOString(),
254
401
  similarity: r.similarity,
255
402
  })),
256
- total_found: filteredResults.length,
403
+ total_found: paginatedResults.length,
404
+ has_more: offset + limit < filteredResults.length,
405
+ offset,
406
+ scope,
257
407
  };
258
408
  }
259
409
  /**
@@ -442,24 +592,35 @@ export class ToolHandlers {
442
592
  */
443
593
  async linkCommitsToConversations(args) {
444
594
  const typedArgs = args;
445
- const { query, conversation_id, limit = 20 } = typedArgs;
595
+ const { query, conversation_id, limit = 20, offset = 0, scope = 'all' } = typedArgs;
596
+ // Global scope not supported for git commits (project-specific)
597
+ if (scope === 'global') {
598
+ throw new Error("Global scope is not supported for linkCommitsToConversations (git commits are project-specific)");
599
+ }
446
600
  let sql = "SELECT * FROM git_commits WHERE 1=1";
447
601
  const params = [];
448
- if (conversation_id) {
602
+ if (conversation_id || scope === 'current') {
603
+ const targetId = conversation_id || typedArgs.conversation_id;
604
+ if (!targetId) {
605
+ throw new Error("conversation_id is required when scope='current'");
606
+ }
449
607
  sql += " AND conversation_id = ?";
450
- params.push(conversation_id);
608
+ params.push(targetId);
451
609
  }
452
610
  if (query) {
453
611
  sql += " AND message LIKE ?";
454
612
  params.push(`%${query}%`);
455
613
  }
456
- sql += " ORDER BY timestamp DESC LIMIT ?";
457
- params.push(limit);
614
+ sql += ` ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
615
+ params.push(limit + 1); // Fetch one extra to determine has_more
616
+ params.push(offset);
458
617
  const commits = this.db.prepare(sql).all(...params);
618
+ const hasMore = commits.length > limit;
619
+ const results = hasMore ? commits.slice(0, limit) : commits;
459
620
  return {
460
621
  query,
461
622
  conversation_id,
462
- commits: commits.map((c) => ({
623
+ commits: results.map((c) => ({
463
624
  hash: c.hash.substring(0, 7),
464
625
  full_hash: c.hash,
465
626
  message: c.message,
@@ -469,7 +630,10 @@ export class ToolHandlers {
469
630
  files_changed: JSON.parse(c.files_changed || "[]"),
470
631
  conversation_id: c.conversation_id,
471
632
  })),
472
- total_found: commits.length,
633
+ total_found: results.length,
634
+ has_more: hasMore,
635
+ offset,
636
+ scope,
473
637
  };
474
638
  }
475
639
  /**
@@ -511,7 +675,28 @@ export class ToolHandlers {
511
675
  */
512
676
  async searchMistakes(args) {
513
677
  const typedArgs = args;
514
- const { query, mistake_type, limit = 10 } = typedArgs;
678
+ const { query, mistake_type, limit = 10, offset = 0, scope = 'all', conversation_id } = typedArgs;
679
+ // Handle global scope
680
+ if (scope === 'global') {
681
+ const globalResponse = await this.searchAllMistakes({ query, mistake_type, limit, offset, source_type: 'all' });
682
+ return {
683
+ query,
684
+ mistake_type,
685
+ mistakes: globalResponse.mistakes.map(m => ({
686
+ mistake_id: m.mistake_id,
687
+ mistake_type: m.mistake_type,
688
+ what_went_wrong: m.what_went_wrong,
689
+ correction: m.correction,
690
+ user_correction_message: m.user_correction_message,
691
+ files_affected: m.files_affected,
692
+ timestamp: m.timestamp,
693
+ })),
694
+ total_found: globalResponse.total_found,
695
+ has_more: globalResponse.has_more,
696
+ offset: globalResponse.offset,
697
+ scope: 'global',
698
+ };
699
+ }
515
700
  const sanitized = sanitizeForLike(query);
516
701
  let sql = "SELECT * FROM mistakes WHERE what_went_wrong LIKE ? ESCAPE '\\'";
517
702
  const params = [`%${sanitized}%`];
@@ -519,13 +704,24 @@ export class ToolHandlers {
519
704
  sql += " AND mistake_type = ?";
520
705
  params.push(mistake_type);
521
706
  }
522
- sql += " ORDER BY timestamp DESC LIMIT ?";
523
- params.push(limit);
707
+ // Filter by conversation_id if scope is 'current'
708
+ if (scope === 'current') {
709
+ if (!conversation_id) {
710
+ throw new Error("conversation_id is required when scope='current'");
711
+ }
712
+ sql += " AND conversation_id = ?";
713
+ params.push(conversation_id);
714
+ }
715
+ sql += ` ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
716
+ params.push(limit + 1); // Fetch one extra to determine has_more
717
+ params.push(offset);
524
718
  const mistakes = this.db.prepare(sql).all(...params);
719
+ const hasMore = mistakes.length > limit;
720
+ const results = hasMore ? mistakes.slice(0, limit) : mistakes;
525
721
  return {
526
722
  query,
527
723
  mistake_type,
528
- mistakes: mistakes.map((m) => ({
724
+ mistakes: results.map((m) => ({
529
725
  mistake_id: m.id,
530
726
  mistake_type: m.mistake_type,
531
727
  what_went_wrong: m.what_went_wrong,
@@ -534,7 +730,10 @@ export class ToolHandlers {
534
730
  files_affected: JSON.parse(m.files_affected || "[]"),
535
731
  timestamp: new Date(m.timestamp).toISOString(),
536
732
  })),
537
- total_found: mistakes.length,
733
+ total_found: results.length,
734
+ has_more: hasMore,
735
+ offset,
736
+ scope,
538
737
  };
539
738
  }
540
739
  /**
@@ -793,8 +992,13 @@ export class ToolHandlers {
793
992
  */
794
993
  async findSimilarSessions(args) {
795
994
  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
995
+ const { query, limit = 5, offset = 0, scope = 'all', conversation_id: _conversation_id } = typedArgs;
996
+ // Note: scope='global' and scope='current' have limited usefulness for finding similar SESSIONS
997
+ // but we implement them for API consistency
998
+ if (scope === 'current') {
999
+ throw new Error("scope='current' is not supported for findSimilarSessions (it finds sessions, not messages within a session)");
1000
+ }
1001
+ const results = await this.memory.search(query, (limit + offset) * 3); // Get more to group by conversation
798
1002
  // Group by conversation
799
1003
  const conversationMap = new Map();
800
1004
  for (const result of results) {
@@ -819,13 +1023,16 @@ export class ToolHandlers {
819
1023
  });
820
1024
  }
821
1025
  }
822
- const sessions = Array.from(conversationMap.values())
823
- .sort((a, b) => b.relevance_score - a.relevance_score)
824
- .slice(0, limit);
1026
+ const allSessions = Array.from(conversationMap.values())
1027
+ .sort((a, b) => b.relevance_score - a.relevance_score);
1028
+ const sessions = allSessions.slice(offset, offset + limit);
825
1029
  return {
826
1030
  query,
827
1031
  sessions,
828
1032
  total_found: sessions.length,
1033
+ has_more: offset + limit < allSessions.length,
1034
+ offset,
1035
+ scope,
829
1036
  };
830
1037
  }
831
1038
  /**
@@ -869,20 +1076,24 @@ export class ToolHandlers {
869
1076
  */
870
1077
  async recallAndApply(args) {
871
1078
  const typedArgs = args;
872
- const { query, context_types = ["conversations", "decisions", "mistakes", "file_changes", "commits"], file_path, date_range, limit = 5 } = typedArgs;
1079
+ const { query, context_types = ["conversations", "decisions", "mistakes", "file_changes", "commits"], file_path, date_range, limit = 5, offset = 0, scope = 'all', conversation_id } = typedArgs;
873
1080
  const recalled = {};
874
1081
  let totalItems = 0;
875
1082
  const suggestions = [];
876
1083
  // 1. Recall conversations if requested
877
1084
  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(),
1085
+ // Use searchConversations with scope support
1086
+ const convResponse = await this.searchConversations({
1087
+ query,
1088
+ limit,
1089
+ offset,
1090
+ date_range,
1091
+ scope,
1092
+ conversation_id,
1093
+ });
1094
+ recalled.conversations = convResponse.results.map(result => ({
1095
+ session_id: result.conversation_id,
1096
+ timestamp: result.timestamp,
886
1097
  snippet: result.snippet,
887
1098
  relevance_score: result.similarity,
888
1099
  }));
@@ -893,25 +1104,24 @@ export class ToolHandlers {
893
1104
  }
894
1105
  // 2. Recall decisions if requested
895
1106
  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,
1107
+ // Use getDecisions with scope support
1108
+ const decisionsResponse = await this.getDecisions({
1109
+ query,
1110
+ file_path,
1111
+ limit,
1112
+ offset,
1113
+ scope,
1114
+ conversation_id,
1115
+ });
1116
+ recalled.decisions = decisionsResponse.decisions.map(d => ({
1117
+ decision_id: d.decision_id,
1118
+ type: d.context || 'unknown',
909
1119
  description: d.decision_text,
910
1120
  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(),
1121
+ alternatives: d.alternatives_considered,
1122
+ rejected_approaches: Object.values(d.rejected_reasons),
1123
+ affects_components: d.related_files,
1124
+ timestamp: d.timestamp,
915
1125
  }));
916
1126
  totalItems += recalled.decisions.length;
917
1127
  if (recalled.decisions.length > 0) {
@@ -920,25 +1130,23 @@ export class ToolHandlers {
920
1130
  }
921
1131
  // 3. Recall mistakes if requested
922
1132
  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,
1133
+ // Use searchMistakes with scope support
1134
+ const mistakesResponse = await this.searchMistakes({
1135
+ query,
1136
+ limit,
1137
+ offset,
1138
+ scope,
1139
+ conversation_id,
1140
+ });
1141
+ recalled.mistakes = mistakesResponse.mistakes.map(m => ({
1142
+ mistake_id: m.mistake_id,
935
1143
  type: m.mistake_type,
936
1144
  description: m.what_went_wrong,
937
1145
  what_happened: m.what_went_wrong,
938
1146
  how_fixed: m.correction || undefined,
939
1147
  lesson_learned: m.user_correction_message || undefined,
940
- files_affected: JSON.parse(m.files_affected),
941
- timestamp: new Date(m.timestamp).toISOString(),
1148
+ files_affected: m.files_affected,
1149
+ timestamp: m.timestamp,
942
1150
  }));
943
1151
  totalItems += recalled.mistakes.length;
944
1152
  if (recalled.mistakes.length > 0) {
@@ -1409,5 +1617,397 @@ export class ToolHandlers {
1409
1617
  };
1410
1618
  }
1411
1619
  }
1620
+ // ==================== Global Cross-Project Tools ====================
1621
+ /**
1622
+ * Index all projects (Claude Code + Codex).
1623
+ *
1624
+ * Discovers and indexes all projects from both Claude Code and Codex,
1625
+ * registering them in a global index for cross-project search.
1626
+ *
1627
+ * @param args - Indexing arguments
1628
+ * @returns Summary of all indexed projects
1629
+ */
1630
+ async indexAllProjects(args) {
1631
+ const { GlobalIndex } = await import("../storage/GlobalIndex.js");
1632
+ const { homedir } = await import("os");
1633
+ const { join } = await import("path");
1634
+ const { existsSync, readdirSync } = await import("fs");
1635
+ const typedArgs = args;
1636
+ const { include_codex = true, include_claude_code = true, codex_path = join(homedir(), ".codex"), claude_projects_path = join(homedir(), ".claude", "projects"), } = typedArgs;
1637
+ const globalIndex = new GlobalIndex();
1638
+ try {
1639
+ const projects = [];
1640
+ const errors = [];
1641
+ let totalMessages = 0;
1642
+ let totalConversations = 0;
1643
+ let totalDecisions = 0;
1644
+ let totalMistakes = 0;
1645
+ // Index Codex if requested
1646
+ if (include_codex && existsSync(codex_path)) {
1647
+ try {
1648
+ const { CodexConversationParser } = await import("../parsers/CodexConversationParser.js");
1649
+ const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1650
+ const { ConversationStorage } = await import("../storage/ConversationStorage.js");
1651
+ // Create dedicated database for Codex
1652
+ const codexDbPath = join(codex_path, ".codex-conversations-memory.db");
1653
+ const codexDb = new SQLiteManager({ dbPath: codexDbPath });
1654
+ const codexStorage = new ConversationStorage(codexDb);
1655
+ // Parse Codex sessions
1656
+ const parser = new CodexConversationParser();
1657
+ const parseResult = parser.parseSession(codex_path);
1658
+ // Store all parsed data
1659
+ await codexStorage.storeConversations(parseResult.conversations);
1660
+ await codexStorage.storeMessages(parseResult.messages);
1661
+ await codexStorage.storeToolUses(parseResult.tool_uses);
1662
+ await codexStorage.storeToolResults(parseResult.tool_results);
1663
+ await codexStorage.storeFileEdits(parseResult.file_edits);
1664
+ await codexStorage.storeThinkingBlocks(parseResult.thinking_blocks);
1665
+ // Get stats from the database
1666
+ const stats = codexDb.getDatabase()
1667
+ .prepare("SELECT COUNT(*) as count FROM conversations")
1668
+ .get();
1669
+ const messageStats = codexDb.getDatabase()
1670
+ .prepare("SELECT COUNT(*) as count FROM messages")
1671
+ .get();
1672
+ const decisionStats = codexDb.getDatabase()
1673
+ .prepare("SELECT COUNT(*) as count FROM decisions")
1674
+ .get();
1675
+ const mistakeStats = codexDb.getDatabase()
1676
+ .prepare("SELECT COUNT(*) as count FROM mistakes")
1677
+ .get();
1678
+ // Register in global index
1679
+ globalIndex.registerProject({
1680
+ project_path: codex_path,
1681
+ source_type: "codex",
1682
+ db_path: codexDbPath,
1683
+ message_count: messageStats.count,
1684
+ conversation_count: stats.count,
1685
+ decision_count: decisionStats.count,
1686
+ mistake_count: mistakeStats.count,
1687
+ metadata: {
1688
+ indexed_folders: parseResult.indexed_folders || [],
1689
+ },
1690
+ });
1691
+ projects.push({
1692
+ project_path: codex_path,
1693
+ source_type: "codex",
1694
+ message_count: messageStats.count,
1695
+ conversation_count: stats.count,
1696
+ });
1697
+ totalMessages += messageStats.count;
1698
+ totalConversations += stats.count;
1699
+ totalDecisions += decisionStats.count;
1700
+ totalMistakes += mistakeStats.count;
1701
+ // Close the Codex database
1702
+ codexDb.close();
1703
+ }
1704
+ catch (error) {
1705
+ errors.push({
1706
+ project_path: codex_path,
1707
+ error: error.message,
1708
+ });
1709
+ }
1710
+ }
1711
+ // Index Claude Code projects if requested
1712
+ if (include_claude_code && existsSync(claude_projects_path)) {
1713
+ try {
1714
+ const projectFolders = readdirSync(claude_projects_path);
1715
+ for (const folder of projectFolders) {
1716
+ const folderPath = join(claude_projects_path, folder);
1717
+ try {
1718
+ // Try to determine the original project path from folder name
1719
+ const projectPath = folderPath; // Simplified for now
1720
+ // Index this project
1721
+ const indexResult = await this.indexConversations({
1722
+ project_path: projectPath,
1723
+ });
1724
+ if (indexResult.success) {
1725
+ // Register in global index
1726
+ globalIndex.registerProject({
1727
+ project_path: projectPath,
1728
+ source_type: "claude-code",
1729
+ db_path: this.db.getDbPath(),
1730
+ message_count: indexResult.stats.messages.count,
1731
+ conversation_count: indexResult.stats.conversations.count,
1732
+ decision_count: indexResult.stats.decisions.count,
1733
+ mistake_count: indexResult.stats.mistakes.count,
1734
+ });
1735
+ projects.push({
1736
+ project_path: projectPath,
1737
+ source_type: "claude-code",
1738
+ message_count: indexResult.stats.messages.count,
1739
+ conversation_count: indexResult.stats.conversations.count,
1740
+ });
1741
+ totalMessages += indexResult.stats.messages.count;
1742
+ totalConversations += indexResult.stats.conversations.count;
1743
+ totalDecisions += indexResult.stats.decisions.count;
1744
+ totalMistakes += indexResult.stats.mistakes.count;
1745
+ }
1746
+ }
1747
+ catch (error) {
1748
+ errors.push({
1749
+ project_path: folder,
1750
+ error: error.message,
1751
+ });
1752
+ }
1753
+ }
1754
+ }
1755
+ catch (error) {
1756
+ errors.push({
1757
+ project_path: claude_projects_path,
1758
+ error: error.message,
1759
+ });
1760
+ }
1761
+ }
1762
+ const stats = globalIndex.getGlobalStats();
1763
+ return {
1764
+ success: true,
1765
+ global_index_path: globalIndex.getDbPath(),
1766
+ projects_indexed: projects.length,
1767
+ claude_code_projects: stats.claude_code_projects,
1768
+ codex_projects: stats.codex_projects,
1769
+ total_messages: totalMessages,
1770
+ total_conversations: totalConversations,
1771
+ total_decisions: totalDecisions,
1772
+ total_mistakes: totalMistakes,
1773
+ projects,
1774
+ errors,
1775
+ message: `Indexed ${projects.length} project(s): ${stats.claude_code_projects} Claude Code + ${stats.codex_projects} Codex`,
1776
+ };
1777
+ }
1778
+ finally {
1779
+ // Ensure GlobalIndex is always closed
1780
+ globalIndex.close();
1781
+ }
1782
+ }
1783
+ /**
1784
+ * Search across all indexed projects.
1785
+ *
1786
+ * @param args - Search arguments
1787
+ * @returns Search results from all projects
1788
+ */
1789
+ async searchAllConversations(args) {
1790
+ const { GlobalIndex } = await import("../storage/GlobalIndex.js");
1791
+ const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1792
+ const { SemanticSearch } = await import("../search/SemanticSearch.js");
1793
+ const typedArgs = args;
1794
+ const { query, limit = 20, offset = 0, date_range, source_type = "all" } = typedArgs;
1795
+ const globalIndex = new GlobalIndex();
1796
+ try {
1797
+ const projects = globalIndex.getAllProjects(source_type === "all" ? undefined : source_type);
1798
+ const allResults = [];
1799
+ let claudeCodeResults = 0;
1800
+ let codexResults = 0;
1801
+ for (const project of projects) {
1802
+ let projectDb = null;
1803
+ try {
1804
+ // Open this project's database
1805
+ projectDb = new SQLiteManager({ dbPath: project.db_path, readOnly: true });
1806
+ const semanticSearch = new SemanticSearch(projectDb);
1807
+ // Search this specific project's database
1808
+ const localResults = await semanticSearch.searchConversations(query, limit);
1809
+ // Filter by date range if specified
1810
+ const filteredResults = date_range
1811
+ ? localResults.filter((r) => {
1812
+ const timestamp = r.message.timestamp;
1813
+ return timestamp >= date_range[0] && timestamp <= date_range[1];
1814
+ })
1815
+ : localResults;
1816
+ // Enrich results with project info
1817
+ for (const result of filteredResults) {
1818
+ allResults.push({
1819
+ conversation_id: result.conversation.id,
1820
+ message_id: result.message.id,
1821
+ timestamp: new Date(result.message.timestamp).toISOString(),
1822
+ similarity: result.similarity,
1823
+ snippet: result.snippet,
1824
+ git_branch: result.conversation.git_branch,
1825
+ message_type: result.message.message_type,
1826
+ role: result.message.role,
1827
+ project_path: project.project_path,
1828
+ source_type: project.source_type,
1829
+ });
1830
+ if (project.source_type === "claude-code") {
1831
+ claudeCodeResults++;
1832
+ }
1833
+ else {
1834
+ codexResults++;
1835
+ }
1836
+ }
1837
+ }
1838
+ catch (_error) {
1839
+ // Skip projects that fail to search
1840
+ continue;
1841
+ }
1842
+ finally {
1843
+ // Close project database
1844
+ if (projectDb) {
1845
+ projectDb.close();
1846
+ }
1847
+ }
1848
+ }
1849
+ // Sort by similarity and paginate
1850
+ const sortedResults = allResults.sort((a, b) => b.similarity - a.similarity);
1851
+ const paginatedResults = sortedResults.slice(offset, offset + limit);
1852
+ return {
1853
+ query,
1854
+ results: paginatedResults,
1855
+ total_found: paginatedResults.length,
1856
+ has_more: offset + limit < sortedResults.length,
1857
+ offset,
1858
+ projects_searched: projects.length,
1859
+ search_stats: {
1860
+ claude_code_results: claudeCodeResults,
1861
+ codex_results: codexResults,
1862
+ },
1863
+ message: `Found ${paginatedResults.length} result(s) across ${projects.length} project(s)`,
1864
+ };
1865
+ }
1866
+ finally {
1867
+ // Ensure GlobalIndex is always closed
1868
+ globalIndex.close();
1869
+ }
1870
+ }
1871
+ /**
1872
+ * Get decisions from all indexed projects.
1873
+ *
1874
+ * @param args - Query arguments
1875
+ * @returns Decisions from all projects
1876
+ */
1877
+ async getAllDecisions(args) {
1878
+ const { GlobalIndex } = await import("../storage/GlobalIndex.js");
1879
+ const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1880
+ const typedArgs = args;
1881
+ const { query, file_path, limit = 20, offset = 0, source_type = 'all' } = typedArgs;
1882
+ const globalIndex = new GlobalIndex();
1883
+ try {
1884
+ const projects = globalIndex.getAllProjects(source_type === "all" ? undefined : source_type);
1885
+ const allDecisions = [];
1886
+ for (const project of projects) {
1887
+ let projectDb = null;
1888
+ try {
1889
+ projectDb = new SQLiteManager({ dbPath: project.db_path, readOnly: true });
1890
+ const sanitized = sanitizeForLike(query);
1891
+ let sql = "SELECT * FROM decisions WHERE decision_text LIKE ? ESCAPE '\\\\'";
1892
+ const params = [`%${sanitized}%`];
1893
+ if (file_path) {
1894
+ sql += " AND related_files LIKE ?";
1895
+ params.push(`%${file_path}%`);
1896
+ }
1897
+ sql += " ORDER BY timestamp DESC LIMIT ?";
1898
+ params.push(limit + offset + 1);
1899
+ const decisions = projectDb.prepare(sql).all(...params);
1900
+ for (const d of decisions) {
1901
+ allDecisions.push({
1902
+ decision_id: d.id,
1903
+ decision_text: d.decision_text,
1904
+ rationale: d.rationale,
1905
+ alternatives_considered: JSON.parse(d.alternatives_considered || "[]"),
1906
+ rejected_reasons: JSON.parse(d.rejected_reasons || "{}"),
1907
+ context: d.context,
1908
+ related_files: JSON.parse(d.related_files || "[]"),
1909
+ related_commits: JSON.parse(d.related_commits || "[]"),
1910
+ timestamp: new Date(d.timestamp).toISOString(),
1911
+ similarity: DEFAULT_SIMILARITY_SCORE,
1912
+ project_path: project.project_path,
1913
+ source_type: project.source_type,
1914
+ });
1915
+ }
1916
+ }
1917
+ catch (_error) {
1918
+ continue;
1919
+ }
1920
+ finally {
1921
+ if (projectDb) {
1922
+ projectDb.close();
1923
+ }
1924
+ }
1925
+ }
1926
+ // Sort by timestamp and paginate
1927
+ const sortedDecisions = allDecisions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1928
+ const paginatedDecisions = sortedDecisions.slice(offset, offset + limit);
1929
+ return {
1930
+ query,
1931
+ decisions: paginatedDecisions,
1932
+ total_found: paginatedDecisions.length,
1933
+ has_more: offset + limit < sortedDecisions.length,
1934
+ offset,
1935
+ projects_searched: projects.length,
1936
+ message: `Found ${paginatedDecisions.length} decision(s) across ${projects.length} project(s)`,
1937
+ };
1938
+ }
1939
+ finally {
1940
+ globalIndex.close();
1941
+ }
1942
+ }
1943
+ /**
1944
+ * Search mistakes across all indexed projects.
1945
+ *
1946
+ * @param args - Search arguments
1947
+ * @returns Mistakes from all projects
1948
+ */
1949
+ async searchAllMistakes(args) {
1950
+ const { GlobalIndex } = await import("../storage/GlobalIndex.js");
1951
+ const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1952
+ const typedArgs = args;
1953
+ const { query, mistake_type, limit = 20, offset = 0, source_type = 'all' } = typedArgs;
1954
+ const globalIndex = new GlobalIndex();
1955
+ try {
1956
+ const projects = globalIndex.getAllProjects(source_type === "all" ? undefined : source_type);
1957
+ const allMistakes = [];
1958
+ for (const project of projects) {
1959
+ let projectDb = null;
1960
+ try {
1961
+ projectDb = new SQLiteManager({ dbPath: project.db_path, readOnly: true });
1962
+ const sanitized = sanitizeForLike(query);
1963
+ let sql = "SELECT * FROM mistakes WHERE what_went_wrong LIKE ? ESCAPE '\\\\'";
1964
+ const params = [`%${sanitized}%`];
1965
+ if (mistake_type) {
1966
+ sql += " AND mistake_type = ?";
1967
+ params.push(mistake_type);
1968
+ }
1969
+ sql += " ORDER BY timestamp DESC LIMIT ?";
1970
+ params.push(limit + offset + 1);
1971
+ const mistakes = projectDb.prepare(sql).all(...params);
1972
+ for (const m of mistakes) {
1973
+ allMistakes.push({
1974
+ mistake_id: m.id,
1975
+ mistake_type: m.mistake_type,
1976
+ what_went_wrong: m.what_went_wrong,
1977
+ correction: m.correction,
1978
+ user_correction_message: m.user_correction_message,
1979
+ files_affected: JSON.parse(m.files_affected || "[]"),
1980
+ timestamp: new Date(m.timestamp).toISOString(),
1981
+ project_path: project.project_path,
1982
+ source_type: project.source_type,
1983
+ });
1984
+ }
1985
+ }
1986
+ catch (_error) {
1987
+ continue;
1988
+ }
1989
+ finally {
1990
+ if (projectDb) {
1991
+ projectDb.close();
1992
+ }
1993
+ }
1994
+ }
1995
+ // Sort by timestamp and paginate
1996
+ const sortedMistakes = allMistakes.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1997
+ const paginatedMistakes = sortedMistakes.slice(offset, offset + limit);
1998
+ return {
1999
+ query,
2000
+ mistakes: paginatedMistakes,
2001
+ total_found: paginatedMistakes.length,
2002
+ has_more: offset + limit < sortedMistakes.length,
2003
+ offset,
2004
+ projects_searched: projects.length,
2005
+ message: `Found ${paginatedMistakes.length} mistake(s) across ${projects.length} project(s)`,
2006
+ };
2007
+ }
2008
+ finally {
2009
+ globalIndex.close();
2010
+ }
2011
+ }
1412
2012
  }
1413
2013
  //# sourceMappingURL=ToolHandlers.js.map