claude-conversation-memory-mcp 1.5.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.
@@ -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) {
@@ -1583,7 +1791,7 @@ export class ToolHandlers {
1583
1791
  const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1584
1792
  const { SemanticSearch } = await import("../search/SemanticSearch.js");
1585
1793
  const typedArgs = args;
1586
- const { query, limit = 20, date_range, source_type = "all" } = typedArgs;
1794
+ const { query, limit = 20, offset = 0, date_range, source_type = "all" } = typedArgs;
1587
1795
  const globalIndex = new GlobalIndex();
1588
1796
  try {
1589
1797
  const projects = globalIndex.getAllProjects(source_type === "all" ? undefined : source_type);
@@ -1638,18 +1846,21 @@ export class ToolHandlers {
1638
1846
  }
1639
1847
  }
1640
1848
  }
1641
- // Sort by similarity and limit
1642
- const sortedResults = allResults.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
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);
1643
1852
  return {
1644
1853
  query,
1645
- results: sortedResults,
1646
- total_found: sortedResults.length,
1854
+ results: paginatedResults,
1855
+ total_found: paginatedResults.length,
1856
+ has_more: offset + limit < sortedResults.length,
1857
+ offset,
1647
1858
  projects_searched: projects.length,
1648
1859
  search_stats: {
1649
1860
  claude_code_results: claudeCodeResults,
1650
1861
  codex_results: codexResults,
1651
1862
  },
1652
- message: `Found ${sortedResults.length} result(s) across ${projects.length} project(s)`,
1863
+ message: `Found ${paginatedResults.length} result(s) across ${projects.length} project(s)`,
1653
1864
  };
1654
1865
  }
1655
1866
  finally {
@@ -1665,19 +1876,64 @@ export class ToolHandlers {
1665
1876
  */
1666
1877
  async getAllDecisions(args) {
1667
1878
  const { GlobalIndex } = await import("../storage/GlobalIndex.js");
1879
+ const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1668
1880
  const typedArgs = args;
1669
- const { query, file_path: _file_path } = typedArgs;
1881
+ const { query, file_path, limit = 20, offset = 0, source_type = 'all' } = typedArgs;
1670
1882
  const globalIndex = new GlobalIndex();
1671
1883
  try {
1672
- const projects = globalIndex.getAllProjects();
1673
- // TODO: Implement cross-project decision search
1674
- // For now, return empty results with proper structure
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);
1675
1929
  return {
1676
1930
  query,
1677
- decisions: [],
1678
- total_found: 0,
1931
+ decisions: paginatedDecisions,
1932
+ total_found: paginatedDecisions.length,
1933
+ has_more: offset + limit < sortedDecisions.length,
1934
+ offset,
1679
1935
  projects_searched: projects.length,
1680
- message: `Cross-project decision search not yet implemented (would search ${projects.length} project(s))`,
1936
+ message: `Found ${paginatedDecisions.length} decision(s) across ${projects.length} project(s)`,
1681
1937
  };
1682
1938
  }
1683
1939
  finally {
@@ -1692,19 +1948,61 @@ export class ToolHandlers {
1692
1948
  */
1693
1949
  async searchAllMistakes(args) {
1694
1950
  const { GlobalIndex } = await import("../storage/GlobalIndex.js");
1951
+ const { SQLiteManager } = await import("../storage/SQLiteManager.js");
1695
1952
  const typedArgs = args;
1696
- const { query, mistake_type: _mistake_type } = typedArgs;
1953
+ const { query, mistake_type, limit = 20, offset = 0, source_type = 'all' } = typedArgs;
1697
1954
  const globalIndex = new GlobalIndex();
1698
1955
  try {
1699
- const projects = globalIndex.getAllProjects();
1700
- // TODO: Implement cross-project mistake search
1701
- // For now, return empty results with proper structure
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);
1702
1998
  return {
1703
1999
  query,
1704
- mistakes: [],
1705
- total_found: 0,
2000
+ mistakes: paginatedMistakes,
2001
+ total_found: paginatedMistakes.length,
2002
+ has_more: offset + limit < sortedMistakes.length,
2003
+ offset,
1706
2004
  projects_searched: projects.length,
1707
- message: `Cross-project mistake search not yet implemented (would search ${projects.length} project(s))`,
2005
+ message: `Found ${paginatedMistakes.length} mistake(s) across ${projects.length} project(s)`,
1708
2006
  };
1709
2007
  }
1710
2008
  finally {