claude_memory 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/memory.sqlite3-shm +0 -0
  4. data/.claude/memory.sqlite3-wal +0 -0
  5. data/.claude/settings.json +78 -6
  6. data/.claude/settings.local.json +5 -2
  7. data/.claude/skills/improve/SKILL.md +113 -25
  8. data/.claude-plugin/commands/distill-transcripts.md +98 -0
  9. data/.claude-plugin/commands/memory-recall.md +67 -0
  10. data/.claude-plugin/marketplace.json +1 -1
  11. data/.claude-plugin/plugin.json +1 -2
  12. data/CHANGELOG.md +74 -1
  13. data/CLAUDE.md +32 -6
  14. data/README.md +1 -1
  15. data/docs/improvements.md +51 -91
  16. data/docs/influence/lossless-claw.md +409 -0
  17. data/docs/quality_review.md +119 -224
  18. data/hooks/hooks.json +39 -7
  19. data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
  20. data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
  21. data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
  22. data/lib/claude_memory/commands/completion_command.rb +179 -0
  23. data/lib/claude_memory/commands/doctor_command.rb +2 -0
  24. data/lib/claude_memory/commands/help_command.rb +4 -0
  25. data/lib/claude_memory/commands/hook_command.rb +2 -1
  26. data/lib/claude_memory/commands/index_command.rb +100 -65
  27. data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
  28. data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
  29. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
  30. data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
  31. data/lib/claude_memory/commands/install_skill_command.rb +78 -0
  32. data/lib/claude_memory/commands/registry.rb +3 -1
  33. data/lib/claude_memory/commands/skills/distill-transcripts.md +98 -0
  34. data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
  35. data/lib/claude_memory/core/fact_ranker.rb +2 -2
  36. data/lib/claude_memory/core/rr_fusion.rb +23 -6
  37. data/lib/claude_memory/core/snippet_extractor.rb +7 -3
  38. data/lib/claude_memory/core/text_builder.rb +11 -0
  39. data/lib/claude_memory/domain/provenance.rb +0 -1
  40. data/lib/claude_memory/embeddings/api_adapter.rb +96 -0
  41. data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
  42. data/lib/claude_memory/embeddings/fastembed_adapter.rb +4 -0
  43. data/lib/claude_memory/embeddings/generator.rb +4 -0
  44. data/lib/claude_memory/embeddings/resolver.rb +18 -0
  45. data/lib/claude_memory/hook/context_injector.rb +58 -2
  46. data/lib/claude_memory/hook/distillation_runner.rb +46 -0
  47. data/lib/claude_memory/hook/handler.rb +11 -2
  48. data/lib/claude_memory/index/vector_index.rb +15 -2
  49. data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
  50. data/lib/claude_memory/mcp/error_classifier.rb +171 -0
  51. data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
  52. data/lib/claude_memory/mcp/handlers/management_handlers.rb +145 -0
  53. data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
  54. data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
  55. data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
  56. data/lib/claude_memory/mcp/handlers/stats_handlers.rb +202 -0
  57. data/lib/claude_memory/mcp/instructions_builder.rb +64 -5
  58. data/lib/claude_memory/mcp/query_guide.rb +51 -22
  59. data/lib/claude_memory/mcp/response_formatter.rb +4 -1
  60. data/lib/claude_memory/mcp/server.rb +1 -0
  61. data/lib/claude_memory/mcp/text_summary.rb +28 -1
  62. data/lib/claude_memory/mcp/tool_definitions.rb +33 -3
  63. data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
  64. data/lib/claude_memory/mcp/tools.rb +47 -681
  65. data/lib/claude_memory/recall/dual_engine.rb +105 -0
  66. data/lib/claude_memory/recall/legacy_engine.rb +138 -0
  67. data/lib/claude_memory/recall/query_core.rb +371 -0
  68. data/lib/claude_memory/recall.rb +29 -616
  69. data/lib/claude_memory/shortcuts.rb +4 -4
  70. data/lib/claude_memory/store/retry_handler.rb +61 -0
  71. data/lib/claude_memory/store/schema_manager.rb +68 -0
  72. data/lib/claude_memory/store/sqlite_store.rb +85 -201
  73. data/lib/claude_memory/sweep/maintenance.rb +126 -0
  74. data/lib/claude_memory/sweep/sweeper.rb +81 -75
  75. data/lib/claude_memory/templates/hooks.example.json +26 -7
  76. data/lib/claude_memory/version.rb +1 -1
  77. data/lib/claude_memory.rb +12 -0
  78. data/v0.6.0.ANNOUNCE +32 -0
  79. metadata +27 -1
@@ -6,11 +6,24 @@ require_relative "tool_helpers"
6
6
  require_relative "response_formatter"
7
7
  require_relative "tool_definitions"
8
8
  require_relative "setup_status_analyzer"
9
+ require_relative "error_classifier"
10
+ require_relative "handlers/query_handlers"
11
+ require_relative "handlers/shortcut_handlers"
12
+ require_relative "handlers/context_handlers"
13
+ require_relative "handlers/management_handlers"
14
+ require_relative "handlers/stats_handlers"
15
+ require_relative "handlers/setup_handlers"
9
16
 
10
17
  module ClaudeMemory
11
18
  module MCP
12
19
  class Tools
13
20
  include ToolHelpers
21
+ include Handlers::QueryHandlers
22
+ include Handlers::ShortcutHandlers
23
+ include Handlers::ContextHandlers
24
+ include Handlers::ManagementHandlers
25
+ include Handlers::StatsHandlers
26
+ include Handlers::SetupHandlers
14
27
 
15
28
  def initialize(store_or_manager)
16
29
  @recall = Recall.new(store_or_manager)
@@ -28,370 +41,40 @@ module ClaudeMemory
28
41
 
29
42
  def call(name, arguments)
30
43
  case name
31
- when "memory.recall"
32
- recall(arguments)
33
- when "memory.recall_index"
34
- recall_index(arguments)
35
- when "memory.recall_details"
36
- recall_details(arguments)
37
- when "memory.explain"
38
- explain(arguments)
39
- when "memory.changes"
40
- changes(arguments)
41
- when "memory.conflicts"
42
- conflicts(arguments)
43
- when "memory.sweep_now"
44
- sweep_now(arguments)
45
- when "memory.status"
46
- status
47
- when "memory.stats"
48
- stats(arguments)
49
- when "memory.promote"
50
- promote(arguments)
51
- when "memory.store_extraction"
52
- store_extraction(arguments)
53
- when "memory.decisions"
54
- decisions(arguments)
55
- when "memory.conventions"
56
- conventions(arguments)
57
- when "memory.architecture"
58
- architecture(arguments)
59
- when "memory.facts_by_tool"
60
- facts_by_tool(arguments)
61
- when "memory.facts_by_context"
62
- facts_by_context(arguments)
63
- when "memory.recall_semantic"
64
- recall_semantic(arguments)
65
- when "memory.search_concepts"
66
- search_concepts(arguments)
67
- when "memory.fact_graph"
68
- fact_graph(arguments)
69
- when "memory.check_setup"
70
- check_setup
71
- when "memory.list_projects"
72
- list_projects
73
- else
74
- {error: "Unknown tool: #{name}"}
44
+ when "memory.recall" then recall(arguments)
45
+ when "memory.recall_index" then recall_index(arguments)
46
+ when "memory.recall_details" then recall_details(arguments)
47
+ when "memory.explain" then explain(arguments)
48
+ when "memory.changes" then changes(arguments)
49
+ when "memory.conflicts" then conflicts(arguments)
50
+ when "memory.sweep_now" then sweep_now(arguments)
51
+ when "memory.status" then status
52
+ when "memory.stats" then stats(arguments)
53
+ when "memory.promote" then promote(arguments)
54
+ when "memory.store_extraction" then store_extraction(arguments)
55
+ when "memory.decisions" then decisions(arguments)
56
+ when "memory.conventions" then conventions(arguments)
57
+ when "memory.architecture" then architecture(arguments)
58
+ when "memory.facts_by_tool" then facts_by_tool(arguments)
59
+ when "memory.facts_by_context" then facts_by_context(arguments)
60
+ when "memory.recall_semantic" then recall_semantic(arguments)
61
+ when "memory.search_concepts" then search_concepts(arguments)
62
+ when "memory.fact_graph" then fact_graph(arguments)
63
+ when "memory.undistilled" then undistilled(arguments)
64
+ when "memory.mark_distilled" then mark_distilled(arguments)
65
+ when "memory.check_setup" then check_setup
66
+ when "memory.list_projects" then list_projects
67
+ else {error: "Unknown tool: #{name}"}
75
68
  end
76
69
  end
77
70
 
78
71
  private
79
72
 
80
- def recall(args)
81
- # Check if databases exist before querying
82
- return database_not_found_error(StandardError.new("Database not initialized")) unless databases_exist?
83
-
84
- scope = extract_scope(args)
85
- limit = extract_limit(args)
86
- compact = args["compact"] == true
87
- query = args["query"]
88
- results = @recall.query(query, limit: limit, scope: scope, include_raw_text: !compact)
89
- ResponseFormatter.format_recall_results(results, compact: compact, query: query)
90
- rescue Sequel::DatabaseError, Sequel::DatabaseConnectionError, SQLite3::CantOpenException, Errno::ENOENT => e
91
- database_not_found_error(e)
92
- end
93
-
94
- def recall_index(args)
95
- scope = extract_scope(args)
96
- limit = extract_limit(args, default: 20)
97
- results = @recall.query_index(args["query"], limit: limit, scope: scope)
98
- ResponseFormatter.format_index_results(args["query"], scope, results)
99
- end
100
-
101
- def recall_details(args)
102
- fact_ids = args["fact_ids"]
103
- scope = args["scope"] || "project"
104
-
105
- # Batch fetch detailed explanations
106
- explanations = fact_ids.map do |fact_id|
107
- explanation = @recall.explain(fact_id, scope: scope)
108
- next nil if explanation.is_a?(Core::NullExplanation)
109
-
110
- ResponseFormatter.format_detailed_explanation(explanation)
111
- end.compact
112
-
113
- {
114
- fact_count: explanations.size,
115
- facts: explanations
116
- }
117
- end
118
-
119
- def explain(args)
120
- scope = args["scope"] || "project"
121
- explanation = @recall.explain(args["fact_id"], scope: scope)
122
- return {error: "Fact not found in #{scope} database"} if explanation.is_a?(Core::NullExplanation)
123
-
124
- ResponseFormatter.format_explanation(explanation, scope)
125
- end
126
-
127
- def changes(args)
128
- since = args["since"] || (Time.now - 86400 * 7).utc.iso8601
129
- scope = args["scope"] || "all"
130
- list = @recall.changes(since: since, limit: args["limit"] || 20, scope: scope)
131
- ResponseFormatter.format_changes(since, list)
132
- end
133
-
134
- def conflicts(args)
135
- scope = args["scope"] || "all"
136
- list = @recall.conflicts(scope: scope)
137
- ResponseFormatter.format_conflicts(list)
138
- end
139
-
140
- def sweep_now(args)
141
- scope = args["scope"] || "project"
142
- store = get_store_for_scope(scope)
143
- return {error: "Database not available"} unless store
144
-
145
- sweeper = Sweep::Sweeper.new(store)
146
- stats = sweeper.run!(budget_seconds: args["budget_seconds"] || 5)
147
- ResponseFormatter.format_sweep_stats(scope, stats)
148
- end
149
-
150
- def status
151
- result = {databases: {}}
152
-
153
- if @manager
154
- if @manager.global_exists?
155
- @manager.ensure_global!
156
- result[:databases][:global] = db_stats(@manager.global_store)
157
- else
158
- result[:databases][:global] = {exists: false}
159
- end
160
-
161
- if @manager.project_exists?
162
- @manager.ensure_project!
163
- result[:databases][:project] = db_stats(@manager.project_store)
164
- else
165
- result[:databases][:project] = {exists: false}
166
- end
167
- else
168
- result[:databases][:legacy] = db_stats(@legacy_store)
169
- end
170
-
171
- result
172
- end
173
-
174
- def stats(args)
175
- scope = args["scope"] || "all"
176
- result = {scope: scope, databases: {}}
177
-
178
- if @manager
179
- if scope == "all" || scope == "global"
180
- if @manager.global_exists?
181
- @manager.ensure_global!
182
- result[:databases][:global] = detailed_stats(@manager.global_store)
183
- else
184
- result[:databases][:global] = {exists: false}
185
- end
186
- end
187
-
188
- if scope == "all" || scope == "project"
189
- if @manager.project_exists?
190
- @manager.ensure_project!
191
- result[:databases][:project] = detailed_stats(@manager.project_store)
192
- else
193
- result[:databases][:project] = {exists: false}
194
- end
195
- end
196
- else
197
- result[:databases][:legacy] = detailed_stats(@legacy_store)
198
- end
199
-
200
- result
201
- end
202
-
203
- def promote(args)
204
- return {error: "Promote requires StoreManager"} unless @manager
205
-
206
- fact_id = args["fact_id"]
207
- global_fact_id = @manager.promote_fact(fact_id)
208
-
209
- if global_fact_id
210
- {
211
- success: true,
212
- project_fact_id: fact_id,
213
- global_fact_id: global_fact_id,
214
- message: "Fact promoted to global memory"
215
- }
216
- else
217
- {error: "Fact #{fact_id} not found in project database"}
218
- end
219
- end
220
-
221
- def store_extraction(args)
222
- scope = args["scope"] || "project"
223
- store = get_store_for_scope(scope)
224
- return {error: "Database not available"} unless store
225
-
226
- entities = (args["entities"] || []).map { |e| symbolize_keys(e) }
227
- facts = (args["facts"] || []).map { |f| symbolize_keys(f) }
228
- decisions = (args["decisions"] || []).map { |d| symbolize_keys(d) }
229
-
230
- config = Configuration.new
231
- project_path = config.project_dir
232
- occurred_at = Time.now.utc.iso8601
233
-
234
- searchable_text = build_searchable_text(entities, facts, decisions)
235
- content_item_id = create_synthetic_content_item(store, searchable_text, project_path, occurred_at)
236
- index_content_item(store, content_item_id, searchable_text)
237
-
238
- extraction = Distill::Extraction.new(
239
- entities: entities,
240
- facts: facts,
241
- decisions: decisions,
242
- signals: []
243
- )
244
-
245
- resolver = Resolve::Resolver.new(store)
246
- result = resolver.apply(
247
- extraction,
248
- content_item_id: content_item_id,
249
- occurred_at: occurred_at,
250
- project_path: project_path,
251
- scope: scope
252
- )
253
-
254
- {
255
- success: true,
256
- scope: scope,
257
- entities_created: result[:entities_created],
258
- facts_created: result[:facts_created],
259
- facts_superseded: result[:facts_superseded],
260
- conflicts_created: result[:conflicts_created]
261
- }
262
- end
263
-
264
- def build_searchable_text(entities, facts, decisions)
265
- Core::TextBuilder.build_searchable_text(entities, facts, decisions)
266
- end
267
-
268
- def create_synthetic_content_item(store, text, project_path, occurred_at)
269
- text_hash = Digest::SHA256.hexdigest(text)
270
- store.upsert_content_item(
271
- source: "mcp_extraction",
272
- session_id: "mcp-#{Time.now.to_i}",
273
- transcript_path: nil,
274
- project_path: project_path,
275
- text_hash: text_hash,
276
- byte_len: text.bytesize,
277
- raw_text: text,
278
- occurred_at: occurred_at
279
- )
280
- end
281
-
282
- def index_content_item(store, content_item_id, text)
283
- fts = Index::LexicalFTS.new(store)
284
- fts.index_content_item(content_item_id, text)
285
- end
286
-
287
- def symbolize_keys(hash)
288
- Core::TextBuilder.symbolize_keys(hash)
289
- end
290
-
291
- def get_store_for_scope(scope)
292
- if @manager
293
- @manager.store_for_scope(scope)
294
- else
295
- @legacy_store
296
- end
297
- end
298
-
299
- def decisions(args)
300
- return {error: "Decisions shortcut requires StoreManager"} unless @manager
301
-
302
- results = Recall.recent_decisions(@manager, limit: args["limit"] || 10)
303
- format_shortcut_results(results, "decisions")
304
- end
305
-
306
- def conventions(args)
307
- return {error: "Conventions shortcut requires StoreManager"} unless @manager
308
-
309
- results = Recall.conventions(@manager, limit: args["limit"] || 20)
310
- format_shortcut_results(results, "conventions")
311
- end
312
-
313
- def architecture(args)
314
- return {error: "Architecture shortcut requires StoreManager"} unless @manager
315
-
316
- results = Recall.architecture_choices(@manager, limit: args["limit"] || 10)
317
- format_shortcut_results(results, "architecture")
318
- end
319
-
320
- def format_shortcut_results(results, category)
321
- ResponseFormatter.format_shortcut_results(category, results)
322
- end
323
-
324
- def facts_by_tool(args)
325
- tool_name = args["tool_name"]
326
- scope = extract_scope(args)
327
- limit = extract_limit(args, default: 20)
328
-
329
- results = @recall.facts_by_tool(tool_name, limit: limit, scope: scope)
330
- ResponseFormatter.format_tool_facts(tool_name, scope, results)
331
- end
332
-
333
- def facts_by_context(args)
334
- scope = extract_scope(args)
335
- limit = extract_limit(args, default: 20)
336
-
337
- if args["git_branch"]
338
- results = @recall.facts_by_branch(args["git_branch"], limit: limit, scope: scope)
339
- context_type = "git_branch"
340
- context_value = args["git_branch"]
341
- elsif args["cwd"]
342
- results = @recall.facts_by_directory(args["cwd"], limit: limit, scope: scope)
343
- context_type = "cwd"
344
- context_value = args["cwd"]
345
- else
346
- return {error: "Must provide either git_branch or cwd parameter"}
347
- end
348
-
349
- ResponseFormatter.format_context_facts(context_type, context_value, scope, results)
350
- end
351
-
352
- def recall_semantic(args)
353
- query = args["query"]
354
- mode = (args["mode"] || "both").to_sym
355
- scope = extract_scope(args)
356
- limit = extract_limit(args)
357
- compact = args["compact"] == true
358
-
359
- results = @recall.query_semantic(query, limit: limit, scope: scope, mode: mode)
360
- ResponseFormatter.format_semantic_results(query, mode.to_s, scope, results, compact: compact)
361
- end
362
-
363
- def search_concepts(args)
364
- concepts = args["concepts"]
365
- scope = extract_scope(args)
366
- limit = extract_limit(args)
367
- compact = args["compact"] == true
368
-
369
- return {error: "Must provide 2-5 concepts"} unless (2..5).cover?(concepts.size)
370
-
371
- results = @recall.query_concepts(concepts, limit: limit, scope: scope)
372
- ResponseFormatter.format_concept_results(concepts, scope, results, compact: compact)
373
- end
374
-
375
- def fact_graph(args)
376
- fact_id = args["fact_id"]
377
- depth = args["depth"] || 2
378
- scope = args["scope"] || "project"
379
-
380
- graph = @recall.fact_graph(fact_id, depth: depth, scope: scope)
381
-
382
- return {error: "Fact #{fact_id} not found in #{scope} database"} if graph[:node_count] == 0
383
-
384
- graph
385
- end
386
-
387
73
  def databases_exist?
388
74
  if @manager
389
- # For dual-database mode, check if either database exists
390
75
  config = Configuration.new
391
76
  File.exist?(config.global_db_path) || File.exist?(config.project_db_path)
392
77
  elsif @legacy_store
393
- # For legacy mode, check if the database file exists
394
- # Extract the database path from the store's connection
395
78
  db_path = @legacy_store.db.opts[:database]
396
79
  db_path && File.exist?(db_path)
397
80
  else
@@ -399,341 +82,24 @@ module ClaudeMemory
399
82
  end
400
83
  end
401
84
 
402
- def database_not_found_error(error)
403
- {
404
- error: "Database not found or not accessible",
405
- message: "ClaudeMemory may not be initialized. Run memory.check_setup for detailed status.",
406
- details: error.message,
407
- recommendations: [
408
- "Run memory.check_setup to diagnose the issue",
409
- "If not initialized, run: claude-memory init",
410
- "For help: claude-memory doctor"
411
- ]
412
- }
413
- end
414
-
415
- def check_setup
416
- issues = []
417
- warnings = []
418
- config = Configuration.new
419
-
420
- global_db_exists = check_global_database(config, issues)
421
- project_db_exists = check_project_database(config, warnings)
422
- current_version, version_status, claude_md_exists = check_claude_md_version(warnings)
423
- hooks_configured = check_hooks_configuration(warnings)
424
-
425
- build_setup_result(
426
- global_db_exists, project_db_exists, claude_md_exists,
427
- hooks_configured, current_version, version_status,
428
- issues, warnings
429
- )
430
- end
431
-
432
- def check_global_database(config, issues)
433
- exists = File.exist?(config.global_db_path)
434
- issues << "Global database not found at #{config.global_db_path}" unless exists
435
- exists
436
- end
437
-
438
- def check_project_database(config, warnings)
439
- exists = File.exist?(config.project_db_path)
440
- warnings << "Project database not found at #{config.project_db_path}" unless exists
441
- exists
442
- end
443
-
444
- def check_claude_md_version(warnings)
445
- claude_md_path = ".claude/CLAUDE.md"
446
- unless File.exist?(claude_md_path)
447
- warnings << "No .claude/CLAUDE.md found"
448
- return [nil, nil, false]
449
- end
450
-
451
- content = File.read(claude_md_path)
452
- unless content.include?("ClaudeMemory")
453
- warnings << "CLAUDE.md exists but no ClaudeMemory configuration found"
454
- return [nil, nil, true]
455
- end
456
-
457
- current_version = SetupStatusAnalyzer.extract_version(content)
458
- unless current_version
459
- warnings << "CLAUDE.md has ClaudeMemory section but no version marker"
460
- return [nil, "no_version_marker", true]
461
- end
462
-
463
- version_status = SetupStatusAnalyzer.determine_version_status(current_version, ClaudeMemory::VERSION)
464
- if version_status == "outdated"
465
- warnings << "Configuration version (v#{current_version}) is older than ClaudeMemory (v#{ClaudeMemory::VERSION}). Consider running upgrade."
466
- end
467
-
468
- [current_version, version_status, true]
469
- end
470
-
471
- def check_hooks_configuration(warnings)
472
- settings_paths = [".claude/settings.json", ".claude/settings.local.json"]
473
- settings_paths.each do |path|
474
- next unless File.exist?(path)
475
- begin
476
- config_data = JSON.parse(File.read(path))
477
- return true if config_data["hooks"]&.any?
478
- rescue JSON::ParserError
479
- warnings << "Invalid JSON in #{path}"
480
- end
85
+ def database_not_found_error(error = nil)
86
+ if error
87
+ ErrorClassifier.build_error_response(error, tool_name: "recall")
88
+ else
89
+ ErrorClassifier.build_benign_response(:not_initialized, tool_name: "recall")
481
90
  end
482
-
483
- warnings << "No hooks configured for automatic ingestion"
484
- false
485
91
  end
486
92
 
487
- def build_setup_result(global_db_exists, project_db_exists, claude_md_exists, hooks_configured, current_version, version_status, issues, warnings)
488
- initialized = global_db_exists && claude_md_exists
489
- status = SetupStatusAnalyzer.determine_status(global_db_exists, claude_md_exists, version_status)
490
- recommendations = SetupStatusAnalyzer.generate_recommendations(initialized, version_status, warnings.any?)
491
-
492
- {
493
- status: status,
494
- initialized: initialized,
495
- version: {
496
- current: current_version || "unknown",
497
- latest: ClaudeMemory::VERSION,
498
- status: version_status || "unknown"
499
- },
500
- components: {
501
- global_database: global_db_exists,
502
- project_database: project_db_exists,
503
- claude_md: claude_md_exists,
504
- hooks_configured: hooks_configured
505
- },
506
- issues: issues,
507
- warnings: warnings,
508
- recommendations: recommendations
509
- }
93
+ def classified_error(error, tool_name: nil)
94
+ ErrorClassifier.build_error_response(error, tool_name: tool_name)
510
95
  end
511
96
 
512
- def list_projects
513
- result = {global: nil, current_project: nil, other_projects: []}
514
-
97
+ def get_store_for_scope(scope)
515
98
  if @manager
516
- result[:global] = list_global_database
517
- result[:current_project] = list_current_project
518
- result[:other_projects] = discover_other_projects
519
- elsif @legacy_store
520
- result[:global] = {
521
- exists: true,
522
- path: @legacy_store.db.opts[:database],
523
- facts_active: @legacy_store.facts.where(status: "active").count,
524
- entities: @legacy_store.entities.count
525
- }
526
- end
527
-
528
- result[:project_count] = 1 + result[:other_projects].size
529
- result
530
- end
531
-
532
- def list_global_database
533
- if @manager.global_exists?
534
- @manager.ensure_global!
535
- store = @manager.global_store
536
- {
537
- exists: true,
538
- path: @manager.global_db_path,
539
- facts_active: store.facts.where(status: "active").count,
540
- facts_total: store.facts.count,
541
- entities: store.entities.count
542
- }
543
- else
544
- {exists: false, path: @manager.global_db_path}
545
- end
546
- end
547
-
548
- def list_current_project
549
- if @manager.project_exists?
550
- @manager.ensure_project!
551
- store = @manager.project_store
552
- {
553
- exists: true,
554
- path: @manager.project_path,
555
- db_path: @manager.project_db_path,
556
- facts_active: store.facts.where(status: "active").count,
557
- facts_total: store.facts.count,
558
- entities: store.entities.count
559
- }
99
+ @manager.store_for_scope(scope)
560
100
  else
561
- {exists: false, path: @manager.project_path, db_path: @manager.project_db_path}
562
- end
563
- end
564
-
565
- def discover_other_projects
566
- return [] unless @manager.global_exists?
567
-
568
- @manager.ensure_global!
569
- global = @manager.global_store
570
-
571
- # Find project paths from promoted facts
572
- promoted_paths = global.facts
573
- .where(Sequel.like(:created_from, "promoted:%"))
574
- .select(:created_from)
575
- .distinct
576
- .all
577
- .filter_map { |f|
578
- match = f[:created_from]&.match(/\Apromoted:(.+):\d+\z/)
579
- match[1] if match
580
- }
581
- .uniq
582
-
583
- # Also check for project_path values on facts
584
- fact_paths = global.facts
585
- .exclude(project_path: nil)
586
- .select(:project_path)
587
- .distinct
588
- .all
589
- .map { |f| f[:project_path] }
590
-
591
- all_paths = (promoted_paths + fact_paths).uniq
592
- current = @manager.project_path
593
-
594
- all_paths.filter_map { |path|
595
- next if path == current
596
-
597
- db_path = File.join(path, ".claude", "memory.sqlite3")
598
- entry = {path: path, db_path: db_path, exists: File.exist?(db_path)}
599
-
600
- if entry[:exists]
601
- begin
602
- temp_store = Store::SQLiteStore.new(db_path)
603
- entry[:facts_active] = temp_store.facts.where(status: "active").count
604
- entry[:facts_total] = temp_store.facts.count
605
- entry[:entities] = temp_store.entities.count
606
- temp_store.close
607
- rescue Sequel::DatabaseError, Extralite::Error, IOError => _e
608
- entry[:error] = "Could not read database"
609
- end
610
- end
611
-
612
- entry
613
- }
614
- end
615
-
616
- def db_stats(store)
617
- stats = {
618
- exists: true,
619
- facts_total: store.facts.count,
620
- facts_active: store.facts.where(status: "active").count,
621
- content_items: store.content_items.count,
622
- open_conflicts: store.conflicts.where(status: "open").count,
623
- schema_version: store.schema_version
624
- }
625
-
626
- vec_index = store.vector_index
627
- stats[:vec_available] = vec_index.available?
628
- stats[:vec_indexed] = vec_index.coverage_stats[:vec_indexed] if vec_index.available?
629
-
630
- if fts_legacy?(store)
631
- stats[:fts_legacy] = true
632
- stats[:optimization_hint] = "Run 'claude-memory compact' to reduce database size by ~40%"
633
- end
634
-
635
- stats
636
- end
637
-
638
- def fts_legacy?(store)
639
- row = store.db.fetch("SELECT sql FROM sqlite_master WHERE name = 'content_fts' AND type = 'table'").first
640
- row && !row[:sql].to_s.include?("content=''")
641
- rescue
642
- false
643
- end
644
-
645
- def detailed_stats(store)
646
- active_facts = store.facts.where(status: "active").count
647
-
648
- stats = {
649
- exists: true,
650
- facts: fact_stats(store, active_facts),
651
- entities: entity_stats(store),
652
- content_items: content_stats(store),
653
- provenance: provenance_stats(store, active_facts),
654
- conflicts: conflict_stats(store),
655
- schema_version: store.schema_version
656
- }
657
-
658
- stats[:vec] = vec_stats(store, active_facts)
659
-
660
- stats
661
- end
662
-
663
- def fact_stats(store, active_facts)
664
- stats = {
665
- total: store.facts.count,
666
- active: active_facts,
667
- superseded: store.facts.where(status: "superseded").count
668
- }
669
-
670
- if active_facts > 0
671
- stats[:top_predicates] = store.db[:facts]
672
- .where(status: "active")
673
- .group_and_count(:predicate)
674
- .order(Sequel.desc(:count))
675
- .limit(10)
676
- .all
677
- .map { |row| {predicate: row[:predicate], count: row[:count]} }
678
- end
679
-
680
- stats
681
- end
682
-
683
- def entity_stats(store)
684
- {
685
- total: store.entities.count,
686
- by_type: store.db[:entities]
687
- .group_and_count(:type)
688
- .order(Sequel.desc(:count))
689
- .all
690
- .map { |row| {type: row[:type], count: row[:count]} }
691
- }
692
- end
693
-
694
- def content_stats(store)
695
- count = store.content_items.count
696
- stats = {total: count}
697
-
698
- if count > 0
699
- stats[:date_range] = {
700
- first: store.content_items.min(:occurred_at),
701
- last: store.content_items.max(:occurred_at)
702
- }
101
+ @legacy_store
703
102
  end
704
-
705
- stats
706
- end
707
-
708
- def provenance_stats(store, active_facts)
709
- return {facts_with_sources: 0, total_active_facts: 0, coverage_percentage: 0} if active_facts == 0
710
-
711
- facts_with_provenance = store.db[:provenance]
712
- .join(:facts, id: :fact_id)
713
- .where(Sequel[:facts][:status] => "active")
714
- .select(Sequel[:provenance][:fact_id])
715
- .distinct
716
- .count
717
-
718
- {
719
- facts_with_sources: facts_with_provenance,
720
- total_active_facts: active_facts,
721
- coverage_percentage: (facts_with_provenance * 100.0 / active_facts).round(1)
722
- }
723
- end
724
-
725
- def vec_stats(store, _active_facts)
726
- vec_index = store.vector_index
727
- result = {available: vec_index.available?}
728
- result.merge!(vec_index.coverage_stats) if vec_index.available?
729
- result
730
- end
731
-
732
- def conflict_stats(store)
733
- open = store.conflicts.where(status: "open").count
734
- resolved = store.conflicts.where(status: "resolved").count
735
-
736
- {open: open, resolved: resolved, total: open + resolved}
737
103
  end
738
104
  end
739
105
  end