claude_memory 0.2.0 → 0.3.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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/.mind.mv2.o2N83S +0 -0
  3. data/.claude/CLAUDE.md +1 -0
  4. data/.claude/rules/claude_memory.generated.md +28 -9
  5. data/.claude/settings.local.json +9 -1
  6. data/.claude/skills/check-memory/SKILL.md +77 -0
  7. data/.claude/skills/improve/SKILL.md +532 -0
  8. data/.claude/skills/improve/feature-patterns.md +1221 -0
  9. data/.claude/skills/quality-update/SKILL.md +229 -0
  10. data/.claude/skills/quality-update/implementation-guide.md +346 -0
  11. data/.claude/skills/review-commit/SKILL.md +199 -0
  12. data/.claude/skills/review-for-quality/SKILL.md +154 -0
  13. data/.claude/skills/review-for-quality/expert-checklists.md +79 -0
  14. data/.claude/skills/setup-memory/SKILL.md +168 -0
  15. data/.claude/skills/study-repo/SKILL.md +307 -0
  16. data/.claude/skills/study-repo/analysis-template.md +323 -0
  17. data/.claude/skills/study-repo/focus-examples.md +327 -0
  18. data/CHANGELOG.md +133 -0
  19. data/CLAUDE.md +130 -11
  20. data/README.md +117 -10
  21. data/db/migrations/001_create_initial_schema.rb +117 -0
  22. data/db/migrations/002_add_project_scoping.rb +33 -0
  23. data/db/migrations/003_add_session_metadata.rb +42 -0
  24. data/db/migrations/004_add_fact_embeddings.rb +20 -0
  25. data/db/migrations/005_add_incremental_sync.rb +21 -0
  26. data/db/migrations/006_add_operation_tracking.rb +40 -0
  27. data/db/migrations/007_add_ingestion_metrics.rb +26 -0
  28. data/docs/.claude/mind.mv2.lock +0 -0
  29. data/docs/GETTING_STARTED.md +587 -0
  30. data/docs/RELEASE_NOTES_v0.2.0.md +0 -1
  31. data/docs/RUBY_COMMUNITY_POST_v0.2.0.md +0 -2
  32. data/docs/architecture.md +9 -8
  33. data/docs/auto_init_design.md +230 -0
  34. data/docs/improvements.md +557 -731
  35. data/docs/influence/.gitkeep +13 -0
  36. data/docs/influence/grepai.md +933 -0
  37. data/docs/influence/qmd.md +2195 -0
  38. data/docs/plugin.md +257 -11
  39. data/docs/quality_review.md +472 -1273
  40. data/docs/remaining_improvements.md +330 -0
  41. data/lefthook.yml +13 -0
  42. data/lib/claude_memory/commands/checks/claude_md_check.rb +41 -0
  43. data/lib/claude_memory/commands/checks/database_check.rb +120 -0
  44. data/lib/claude_memory/commands/checks/hooks_check.rb +112 -0
  45. data/lib/claude_memory/commands/checks/reporter.rb +110 -0
  46. data/lib/claude_memory/commands/checks/snapshot_check.rb +30 -0
  47. data/lib/claude_memory/commands/doctor_command.rb +12 -129
  48. data/lib/claude_memory/commands/help_command.rb +1 -0
  49. data/lib/claude_memory/commands/hook_command.rb +9 -2
  50. data/lib/claude_memory/commands/index_command.rb +169 -0
  51. data/lib/claude_memory/commands/ingest_command.rb +1 -1
  52. data/lib/claude_memory/commands/init_command.rb +5 -197
  53. data/lib/claude_memory/commands/initializers/database_ensurer.rb +30 -0
  54. data/lib/claude_memory/commands/initializers/global_initializer.rb +85 -0
  55. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +156 -0
  56. data/lib/claude_memory/commands/initializers/mcp_configurator.rb +56 -0
  57. data/lib/claude_memory/commands/initializers/memory_instructions_writer.rb +135 -0
  58. data/lib/claude_memory/commands/initializers/project_initializer.rb +111 -0
  59. data/lib/claude_memory/commands/recover_command.rb +75 -0
  60. data/lib/claude_memory/commands/registry.rb +5 -1
  61. data/lib/claude_memory/commands/stats_command.rb +239 -0
  62. data/lib/claude_memory/commands/uninstall_command.rb +226 -0
  63. data/lib/claude_memory/core/batch_loader.rb +32 -0
  64. data/lib/claude_memory/core/concept_ranker.rb +73 -0
  65. data/lib/claude_memory/core/embedding_candidate_builder.rb +37 -0
  66. data/lib/claude_memory/core/fact_collector.rb +51 -0
  67. data/lib/claude_memory/core/fact_query_builder.rb +154 -0
  68. data/lib/claude_memory/core/fact_ranker.rb +113 -0
  69. data/lib/claude_memory/core/result_builder.rb +54 -0
  70. data/lib/claude_memory/core/result_sorter.rb +25 -0
  71. data/lib/claude_memory/core/scope_filter.rb +61 -0
  72. data/lib/claude_memory/core/text_builder.rb +29 -0
  73. data/lib/claude_memory/embeddings/generator.rb +161 -0
  74. data/lib/claude_memory/embeddings/similarity.rb +69 -0
  75. data/lib/claude_memory/hook/handler.rb +4 -3
  76. data/lib/claude_memory/index/lexical_fts.rb +7 -2
  77. data/lib/claude_memory/infrastructure/operation_tracker.rb +158 -0
  78. data/lib/claude_memory/infrastructure/schema_validator.rb +206 -0
  79. data/lib/claude_memory/ingest/content_sanitizer.rb +6 -7
  80. data/lib/claude_memory/ingest/ingester.rb +99 -15
  81. data/lib/claude_memory/ingest/metadata_extractor.rb +57 -0
  82. data/lib/claude_memory/ingest/tool_extractor.rb +71 -0
  83. data/lib/claude_memory/mcp/response_formatter.rb +331 -0
  84. data/lib/claude_memory/mcp/server.rb +19 -0
  85. data/lib/claude_memory/mcp/setup_status_analyzer.rb +73 -0
  86. data/lib/claude_memory/mcp/tool_definitions.rb +279 -0
  87. data/lib/claude_memory/mcp/tool_helpers.rb +80 -0
  88. data/lib/claude_memory/mcp/tools.rb +330 -320
  89. data/lib/claude_memory/recall/dual_query_template.rb +63 -0
  90. data/lib/claude_memory/recall.rb +304 -237
  91. data/lib/claude_memory/resolve/resolver.rb +52 -49
  92. data/lib/claude_memory/store/sqlite_store.rb +210 -144
  93. data/lib/claude_memory/store/store_manager.rb +6 -6
  94. data/lib/claude_memory/sweep/sweeper.rb +6 -0
  95. data/lib/claude_memory/version.rb +1 -1
  96. data/lib/claude_memory.rb +35 -3
  97. metadata +71 -11
  98. data/.claude/.mind.mv2.aLCUZd +0 -0
  99. data/.claude/memory.sqlite3 +0 -0
  100. data/.mcp.json +0 -11
  101. /data/docs/{feature_adoption_plan.md → plans/feature_adoption_plan.md} +0 -0
  102. /data/docs/{feature_adoption_plan_revised.md → plans/feature_adoption_plan_revised.md} +0 -0
  103. /data/docs/{plan.md → plans/plan.md} +0 -0
  104. /data/docs/{updated_plan.md → plans/updated_plan.md} +0 -0
@@ -2,10 +2,16 @@
2
2
 
3
3
  require "json"
4
4
  require "digest"
5
+ require_relative "tool_helpers"
6
+ require_relative "response_formatter"
7
+ require_relative "tool_definitions"
8
+ require_relative "setup_status_analyzer"
5
9
 
6
10
  module ClaudeMemory
7
11
  module MCP
8
12
  class Tools
13
+ include ToolHelpers
14
+
9
15
  def initialize(store_or_manager)
10
16
  @recall = Recall.new(store_or_manager)
11
17
 
@@ -17,194 +23,7 @@ module ClaudeMemory
17
23
  end
18
24
 
19
25
  def definitions
20
- [
21
- {
22
- name: "memory.recall",
23
- description: "Recall facts matching a query. Searches both global and project databases.",
24
- inputSchema: {
25
- type: "object",
26
- properties: {
27
- query: {type: "string", description: "Search query"},
28
- limit: {type: "integer", description: "Max results", default: 10},
29
- scope: {type: "string", enum: ["all", "global", "project"], description: "Filter by scope: 'all' (default), 'global', or 'project'", default: "all"}
30
- },
31
- required: ["query"]
32
- }
33
- },
34
- {
35
- name: "memory.recall_index",
36
- description: "Layer 1: Search for facts and get lightweight index (IDs, previews, token counts). Use this first before fetching full details.",
37
- inputSchema: {
38
- type: "object",
39
- properties: {
40
- query: {type: "string", description: "Search query for fact discovery"},
41
- limit: {type: "integer", description: "Maximum results to return", default: 20},
42
- scope: {type: "string", enum: ["all", "global", "project"], description: "Scope: 'all' (both), 'global' (user-wide), 'project' (current only)", default: "all"}
43
- },
44
- required: ["query"]
45
- }
46
- },
47
- {
48
- name: "memory.recall_details",
49
- description: "Layer 2: Fetch full details for specific fact IDs from the index. Use after memory.recall_index to get complete information.",
50
- inputSchema: {
51
- type: "object",
52
- properties: {
53
- fact_ids: {type: "array", items: {type: "integer"}, description: "Fact IDs from memory.recall_index"},
54
- scope: {type: "string", enum: ["project", "global"], description: "Database to query", default: "project"}
55
- },
56
- required: ["fact_ids"]
57
- }
58
- },
59
- {
60
- name: "memory.explain",
61
- description: "Get detailed explanation of a fact with provenance",
62
- inputSchema: {
63
- type: "object",
64
- properties: {
65
- fact_id: {type: "integer", description: "Fact ID to explain"},
66
- scope: {type: "string", enum: ["global", "project"], description: "Which database to look in", default: "project"}
67
- },
68
- required: ["fact_id"]
69
- }
70
- },
71
- {
72
- name: "memory.changes",
73
- description: "List recent fact changes from both databases",
74
- inputSchema: {
75
- type: "object",
76
- properties: {
77
- since: {type: "string", description: "ISO timestamp"},
78
- limit: {type: "integer", default: 20},
79
- scope: {type: "string", enum: ["all", "global", "project"], default: "all"}
80
- }
81
- }
82
- },
83
- {
84
- name: "memory.conflicts",
85
- description: "List open conflicts from both databases",
86
- inputSchema: {
87
- type: "object",
88
- properties: {
89
- scope: {type: "string", enum: ["all", "global", "project"], default: "all"}
90
- }
91
- }
92
- },
93
- {
94
- name: "memory.sweep_now",
95
- description: "Run maintenance sweep on a database",
96
- inputSchema: {
97
- type: "object",
98
- properties: {
99
- budget_seconds: {type: "integer", default: 5},
100
- scope: {type: "string", enum: ["global", "project"], default: "project"}
101
- }
102
- }
103
- },
104
- {
105
- name: "memory.status",
106
- description: "Get memory system status for both databases",
107
- inputSchema: {
108
- type: "object",
109
- properties: {}
110
- }
111
- },
112
- {
113
- name: "memory.promote",
114
- description: "Promote a project fact to global memory. Use when user says a preference should apply everywhere.",
115
- inputSchema: {
116
- type: "object",
117
- properties: {
118
- fact_id: {type: "integer", description: "Project fact ID to promote to global"}
119
- },
120
- required: ["fact_id"]
121
- }
122
- },
123
- {
124
- name: "memory.store_extraction",
125
- description: "Store extracted facts, entities, and decisions from a conversation. Call this to persist knowledge you've learned during the session.",
126
- inputSchema: {
127
- type: "object",
128
- properties: {
129
- entities: {
130
- type: "array",
131
- description: "Entities mentioned (databases, frameworks, services, etc.)",
132
- items: {
133
- type: "object",
134
- properties: {
135
- type: {type: "string", description: "Entity type: database, framework, language, platform, repo, module, person, service"},
136
- name: {type: "string", description: "Canonical name"},
137
- confidence: {type: "number", description: "0.0-1.0 extraction confidence"}
138
- },
139
- required: ["type", "name"]
140
- }
141
- },
142
- facts: {
143
- type: "array",
144
- description: "Facts learned during the session",
145
- items: {
146
- type: "object",
147
- properties: {
148
- subject: {type: "string", description: "Entity name or 'repo' for project-level facts"},
149
- predicate: {type: "string", description: "Relationship type: uses_database, uses_framework, convention, decision, auth_method, deployment_platform"},
150
- object: {type: "string", description: "The value or target entity"},
151
- confidence: {type: "number", description: "0.0-1.0 how confident"},
152
- quote: {type: "string", description: "Source text excerpt (max 200 chars)"},
153
- strength: {type: "string", enum: ["stated", "inferred"], description: "Was this explicitly stated or inferred?"},
154
- scope_hint: {type: "string", enum: ["project", "global"], description: "Should this apply to just this project or globally?"}
155
- },
156
- required: ["subject", "predicate", "object"]
157
- }
158
- },
159
- decisions: {
160
- type: "array",
161
- description: "Decisions made during the session",
162
- items: {
163
- type: "object",
164
- properties: {
165
- title: {type: "string", description: "Short summary (max 100 chars)"},
166
- summary: {type: "string", description: "Full description"},
167
- status_hint: {type: "string", enum: ["accepted", "proposed", "rejected"]}
168
- },
169
- required: ["title", "summary"]
170
- }
171
- },
172
- scope: {type: "string", enum: ["global", "project"], description: "Default scope for facts", default: "project"}
173
- },
174
- required: ["facts"]
175
- }
176
- },
177
- {
178
- name: "memory.decisions",
179
- description: "Quick access to architectural decisions, constraints, and rules",
180
- inputSchema: {
181
- type: "object",
182
- properties: {
183
- limit: {type: "integer", default: 10, description: "Maximum results to return"}
184
- }
185
- }
186
- },
187
- {
188
- name: "memory.conventions",
189
- description: "Quick access to coding conventions and style preferences (global scope)",
190
- inputSchema: {
191
- type: "object",
192
- properties: {
193
- limit: {type: "integer", default: 20, description: "Maximum results to return"}
194
- }
195
- }
196
- },
197
- {
198
- name: "memory.architecture",
199
- description: "Quick access to framework choices and architectural patterns",
200
- inputSchema: {
201
- type: "object",
202
- properties: {
203
- limit: {type: "integer", default: 10, description: "Maximum results to return"}
204
- }
205
- }
206
- }
207
- ]
26
+ ToolDefinitions.all
208
27
  end
209
28
 
210
29
  def call(name, arguments)
@@ -225,6 +44,8 @@ module ClaudeMemory
225
44
  sweep_now(arguments)
226
45
  when "memory.status"
227
46
  status
47
+ when "memory.stats"
48
+ stats(arguments)
228
49
  when "memory.promote"
229
50
  promote(arguments)
230
51
  when "memory.store_extraction"
@@ -235,6 +56,16 @@ module ClaudeMemory
235
56
  conventions(arguments)
236
57
  when "memory.architecture"
237
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.check_setup"
68
+ check_setup
238
69
  else
239
70
  {error: "Unknown tool: #{name}"}
240
71
  end
@@ -243,48 +74,22 @@ module ClaudeMemory
243
74
  private
244
75
 
245
76
  def recall(args)
246
- scope = args["scope"] || "all"
247
- results = @recall.query(args["query"], limit: args["limit"] || 10, scope: scope)
248
- {
249
- facts: results.map do |r|
250
- {
251
- id: r[:fact][:id],
252
- subject: r[:fact][:subject_name],
253
- predicate: r[:fact][:predicate],
254
- object: r[:fact][:object_literal],
255
- status: r[:fact][:status],
256
- source: r[:source],
257
- receipts: r[:receipts].map { |p| {quote: p[:quote], strength: p[:strength]} }
258
- }
259
- end
260
- }
77
+ # Check if databases exist before querying
78
+ return database_not_found_error(StandardError.new("Database not initialized")) unless databases_exist?
79
+
80
+ scope = extract_scope(args)
81
+ limit = extract_limit(args)
82
+ results = @recall.query(args["query"], limit: limit, scope: scope)
83
+ ResponseFormatter.format_recall_results(results)
84
+ rescue Sequel::DatabaseError, Sequel::DatabaseConnectionError, SQLite3::CantOpenException, Errno::ENOENT => e
85
+ database_not_found_error(e)
261
86
  end
262
87
 
263
88
  def recall_index(args)
264
- scope = args["scope"] || "all"
265
- results = @recall.query_index(args["query"], limit: args["limit"] || 20, scope: scope)
266
-
267
- total_tokens = results.sum { |r| r[:token_estimate] }
268
-
269
- {
270
- query: args["query"],
271
- scope: scope,
272
- result_count: results.size,
273
- total_estimated_tokens: total_tokens,
274
- facts: results.map do |r|
275
- {
276
- id: r[:id],
277
- subject: r[:subject],
278
- predicate: r[:predicate],
279
- object_preview: r[:object_preview],
280
- status: r[:status],
281
- scope: r[:scope],
282
- confidence: r[:confidence],
283
- tokens: r[:token_estimate],
284
- source: r[:source]
285
- }
286
- end
287
- }
89
+ scope = extract_scope(args)
90
+ limit = extract_limit(args, default: 20)
91
+ results = @recall.query_index(args["query"], limit: limit, scope: scope)
92
+ ResponseFormatter.format_index_results(args["query"], scope, results)
288
93
  end
289
94
 
290
95
  def recall_details(args)
@@ -296,32 +101,7 @@ module ClaudeMemory
296
101
  explanation = @recall.explain(fact_id, scope: scope)
297
102
  next nil if explanation.is_a?(Core::NullExplanation)
298
103
 
299
- {
300
- fact: {
301
- id: explanation[:fact][:id],
302
- subject: explanation[:fact][:subject_name],
303
- predicate: explanation[:fact][:predicate],
304
- object: explanation[:fact][:object_literal],
305
- status: explanation[:fact][:status],
306
- confidence: explanation[:fact][:confidence],
307
- scope: explanation[:fact][:scope],
308
- valid_from: explanation[:fact][:valid_from],
309
- valid_to: explanation[:fact][:valid_to]
310
- },
311
- receipts: explanation[:receipts].map { |r|
312
- {
313
- quote: r[:quote],
314
- strength: r[:strength],
315
- session_id: r[:session_id],
316
- occurred_at: r[:occurred_at]
317
- }
318
- },
319
- relationships: {
320
- supersedes: explanation[:supersedes],
321
- superseded_by: explanation[:superseded_by],
322
- conflicts: explanation[:conflicts].map { |c| {id: c[:id], status: c[:status]} }
323
- }
324
- }
104
+ ResponseFormatter.format_detailed_explanation(explanation)
325
105
  end.compact
326
106
 
327
107
  {
@@ -335,58 +115,20 @@ module ClaudeMemory
335
115
  explanation = @recall.explain(args["fact_id"], scope: scope)
336
116
  return {error: "Fact not found in #{scope} database"} if explanation.is_a?(Core::NullExplanation)
337
117
 
338
- {
339
- fact: {
340
- id: explanation[:fact][:id],
341
- subject: explanation[:fact][:subject_name],
342
- predicate: explanation[:fact][:predicate],
343
- object: explanation[:fact][:object_literal],
344
- status: explanation[:fact][:status],
345
- valid_from: explanation[:fact][:valid_from],
346
- valid_to: explanation[:fact][:valid_to]
347
- },
348
- source: scope,
349
- receipts: explanation[:receipts].map { |p| {quote: p[:quote], strength: p[:strength]} },
350
- supersedes: explanation[:supersedes],
351
- superseded_by: explanation[:superseded_by],
352
- conflicts: explanation[:conflicts].map { |c| c[:id] }
353
- }
118
+ ResponseFormatter.format_explanation(explanation, scope)
354
119
  end
355
120
 
356
121
  def changes(args)
357
122
  since = args["since"] || (Time.now - 86400 * 7).utc.iso8601
358
123
  scope = args["scope"] || "all"
359
124
  list = @recall.changes(since: since, limit: args["limit"] || 20, scope: scope)
360
- {
361
- since: since,
362
- changes: list.map do |c|
363
- {
364
- id: c[:id],
365
- predicate: c[:predicate],
366
- object: c[:object_literal],
367
- status: c[:status],
368
- created_at: c[:created_at],
369
- source: c[:source]
370
- }
371
- end
372
- }
125
+ ResponseFormatter.format_changes(since, list)
373
126
  end
374
127
 
375
128
  def conflicts(args)
376
129
  scope = args["scope"] || "all"
377
130
  list = @recall.conflicts(scope: scope)
378
- {
379
- count: list.size,
380
- conflicts: list.map do |c|
381
- {
382
- id: c[:id],
383
- fact_a: c[:fact_a_id],
384
- fact_b: c[:fact_b_id],
385
- status: c[:status],
386
- source: c[:source]
387
- }
388
- end
389
- }
131
+ ResponseFormatter.format_conflicts(list)
390
132
  end
391
133
 
392
134
  def sweep_now(args)
@@ -396,14 +138,7 @@ module ClaudeMemory
396
138
 
397
139
  sweeper = Sweep::Sweeper.new(store)
398
140
  stats = sweeper.run!(budget_seconds: args["budget_seconds"] || 5)
399
- {
400
- scope: scope,
401
- proposed_expired: stats[:proposed_facts_expired],
402
- disputed_expired: stats[:disputed_facts_expired],
403
- orphaned_deleted: stats[:orphaned_provenance_deleted],
404
- content_pruned: stats[:old_content_pruned],
405
- elapsed_seconds: stats[:elapsed_seconds].round(3)
406
- }
141
+ ResponseFormatter.format_sweep_stats(scope, stats)
407
142
  end
408
143
 
409
144
  def status
@@ -430,6 +165,35 @@ module ClaudeMemory
430
165
  result
431
166
  end
432
167
 
168
+ def stats(args)
169
+ scope = args["scope"] || "all"
170
+ result = {scope: scope, databases: {}}
171
+
172
+ if @manager
173
+ if scope == "all" || scope == "global"
174
+ if @manager.global_exists?
175
+ @manager.ensure_global!
176
+ result[:databases][:global] = detailed_stats(@manager.global_store)
177
+ else
178
+ result[:databases][:global] = {exists: false}
179
+ end
180
+ end
181
+
182
+ if scope == "all" || scope == "project"
183
+ if @manager.project_exists?
184
+ @manager.ensure_project!
185
+ result[:databases][:project] = detailed_stats(@manager.project_store)
186
+ else
187
+ result[:databases][:project] = {exists: false}
188
+ end
189
+ end
190
+ else
191
+ result[:databases][:legacy] = detailed_stats(@legacy_store)
192
+ end
193
+
194
+ result
195
+ end
196
+
433
197
  def promote(args)
434
198
  return {error: "Promote requires StoreManager"} unless @manager
435
199
 
@@ -457,7 +221,8 @@ module ClaudeMemory
457
221
  facts = (args["facts"] || []).map { |f| symbolize_keys(f) }
458
222
  decisions = (args["decisions"] || []).map { |d| symbolize_keys(d) }
459
223
 
460
- project_path = ENV["CLAUDE_PROJECT_DIR"] || Dir.pwd
224
+ config = Configuration.new
225
+ project_path = config.project_dir
461
226
  occurred_at = Time.now.utc.iso8601
462
227
 
463
228
  searchable_text = build_searchable_text(entities, facts, decisions)
@@ -491,11 +256,7 @@ module ClaudeMemory
491
256
  end
492
257
 
493
258
  def build_searchable_text(entities, facts, decisions)
494
- parts = []
495
- entities.each { |e| parts << "#{e[:type]}: #{e[:name]}" }
496
- facts.each { |f| parts << "#{f[:subject]} #{f[:predicate]} #{f[:object]} #{f[:quote]}" }
497
- decisions.each { |d| parts << "#{d[:title]} #{d[:summary]}" }
498
- parts.join(" ").strip
259
+ Core::TextBuilder.build_searchable_text(entities, facts, decisions)
499
260
  end
500
261
 
501
262
  def create_synthetic_content_item(store, text, project_path, occurred_at)
@@ -518,7 +279,7 @@ module ClaudeMemory
518
279
  end
519
280
 
520
281
  def symbolize_keys(hash)
521
- hash.transform_keys(&:to_sym)
282
+ Core::TextBuilder.symbolize_keys(hash)
522
283
  end
523
284
 
524
285
  def get_store_for_scope(scope)
@@ -551,19 +312,174 @@ module ClaudeMemory
551
312
  end
552
313
 
553
314
  def format_shortcut_results(results, category)
315
+ ResponseFormatter.format_shortcut_results(category, results)
316
+ end
317
+
318
+ def facts_by_tool(args)
319
+ tool_name = args["tool_name"]
320
+ scope = extract_scope(args)
321
+ limit = extract_limit(args, default: 20)
322
+
323
+ results = @recall.facts_by_tool(tool_name, limit: limit, scope: scope)
324
+ ResponseFormatter.format_tool_facts(tool_name, scope, results)
325
+ end
326
+
327
+ def facts_by_context(args)
328
+ scope = extract_scope(args)
329
+ limit = extract_limit(args, default: 20)
330
+
331
+ if args["git_branch"]
332
+ results = @recall.facts_by_branch(args["git_branch"], limit: limit, scope: scope)
333
+ context_type = "git_branch"
334
+ context_value = args["git_branch"]
335
+ elsif args["cwd"]
336
+ results = @recall.facts_by_directory(args["cwd"], limit: limit, scope: scope)
337
+ context_type = "cwd"
338
+ context_value = args["cwd"]
339
+ else
340
+ return {error: "Must provide either git_branch or cwd parameter"}
341
+ end
342
+
343
+ ResponseFormatter.format_context_facts(context_type, context_value, scope, results)
344
+ end
345
+
346
+ def recall_semantic(args)
347
+ query = args["query"]
348
+ mode = (args["mode"] || "both").to_sym
349
+ scope = extract_scope(args)
350
+ limit = extract_limit(args)
351
+
352
+ results = @recall.query_semantic(query, limit: limit, scope: scope, mode: mode)
353
+ ResponseFormatter.format_semantic_results(query, mode.to_s, scope, results)
354
+ end
355
+
356
+ def search_concepts(args)
357
+ concepts = args["concepts"]
358
+ scope = extract_scope(args)
359
+ limit = extract_limit(args)
360
+
361
+ return {error: "Must provide 2-5 concepts"} unless (2..5).cover?(concepts.size)
362
+
363
+ results = @recall.query_concepts(concepts, limit: limit, scope: scope)
364
+ ResponseFormatter.format_concept_results(concepts, scope, results)
365
+ end
366
+
367
+ def databases_exist?
368
+ if @manager
369
+ # For dual-database mode, at least global database should exist
370
+ config = Configuration.new
371
+ File.exist?(config.global_db_path)
372
+ elsif @legacy_store
373
+ # For legacy mode, check if the database file exists
374
+ # Extract the database path from the store's connection
375
+ db_path = @legacy_store.db.opts[:database]
376
+ db_path && File.exist?(db_path)
377
+ else
378
+ false
379
+ end
380
+ end
381
+
382
+ def database_not_found_error(error)
554
383
  {
555
- category: category,
556
- count: results.size,
557
- facts: results.map do |r|
558
- {
559
- id: r[:fact][:id],
560
- subject: r[:fact][:subject_name],
561
- predicate: r[:fact][:predicate],
562
- object: r[:fact][:object_literal],
563
- scope: r[:fact][:scope],
564
- source: r[:source]
565
- }
384
+ error: "Database not found or not accessible",
385
+ message: "ClaudeMemory may not be initialized. Run memory.check_setup for detailed status.",
386
+ details: error.message,
387
+ recommendations: [
388
+ "Run memory.check_setup to diagnose the issue",
389
+ "If not initialized, run: claude-memory init",
390
+ "For help: claude-memory doctor"
391
+ ]
392
+ }
393
+ end
394
+
395
+ def check_setup
396
+ issues = []
397
+ warnings = []
398
+ config = Configuration.new
399
+
400
+ # Check global database
401
+ global_db_exists = File.exist?(config.global_db_path)
402
+ unless global_db_exists
403
+ issues << "Global database not found at #{config.global_db_path}"
404
+ end
405
+
406
+ # Check project database
407
+ project_db_exists = File.exist?(config.project_db_path)
408
+ unless project_db_exists
409
+ warnings << "Project database not found at #{config.project_db_path}"
410
+ end
411
+
412
+ # Check for CLAUDE.md and version
413
+ claude_md_path = ".claude/CLAUDE.md"
414
+ claude_md_exists = File.exist?(claude_md_path)
415
+ current_version = nil
416
+ version_status = nil
417
+
418
+ if claude_md_exists
419
+ content = File.read(claude_md_path)
420
+ if content.include?("ClaudeMemory")
421
+ current_version = SetupStatusAnalyzer.extract_version(content)
422
+ if current_version
423
+ version_status = SetupStatusAnalyzer.determine_version_status(current_version, ClaudeMemory::VERSION)
424
+ if version_status == "outdated"
425
+ warnings << "Configuration version (v#{current_version}) is older than ClaudeMemory (v#{ClaudeMemory::VERSION}). Consider running upgrade."
426
+ end
427
+ else
428
+ version_status = "no_version_marker"
429
+ warnings << "CLAUDE.md has ClaudeMemory section but no version marker"
430
+ end
431
+ else
432
+ warnings << "CLAUDE.md exists but no ClaudeMemory configuration found"
433
+ end
434
+ else
435
+ warnings << "No .claude/CLAUDE.md found"
436
+ end
437
+
438
+ # Check hooks configuration
439
+ hooks_configured = false
440
+ settings_paths = [".claude/settings.json", ".claude/settings.local.json"]
441
+ settings_paths.each do |path|
442
+ if File.exist?(path)
443
+ begin
444
+ config_data = JSON.parse(File.read(path))
445
+ if config_data["hooks"]&.any?
446
+ hooks_configured = true
447
+ break
448
+ end
449
+ rescue JSON::ParserError
450
+ warnings << "Invalid JSON in #{path}"
451
+ end
566
452
  end
453
+ end
454
+
455
+ unless hooks_configured
456
+ warnings << "No hooks configured for automatic ingestion"
457
+ end
458
+
459
+ # Determine overall status using analyzer
460
+ initialized = global_db_exists && claude_md_exists
461
+ status = SetupStatusAnalyzer.determine_status(global_db_exists, claude_md_exists, version_status)
462
+
463
+ # Generate recommendations using analyzer
464
+ recommendations = SetupStatusAnalyzer.generate_recommendations(initialized, version_status, warnings.any?)
465
+
466
+ {
467
+ status: status,
468
+ initialized: initialized,
469
+ version: {
470
+ current: current_version || "unknown",
471
+ latest: ClaudeMemory::VERSION,
472
+ status: version_status || "unknown"
473
+ },
474
+ components: {
475
+ global_database: global_db_exists,
476
+ project_database: project_db_exists,
477
+ claude_md: claude_md_exists,
478
+ hooks_configured: hooks_configured
479
+ },
480
+ issues: issues,
481
+ warnings: warnings,
482
+ recommendations: recommendations
567
483
  }
568
484
  end
569
485
 
@@ -577,6 +493,100 @@ module ClaudeMemory
577
493
  schema_version: store.schema_version
578
494
  }
579
495
  end
496
+
497
+ def detailed_stats(store)
498
+ result = {exists: true}
499
+
500
+ # Facts statistics
501
+ total_facts = store.facts.count
502
+ active_facts = store.facts.where(status: "active").count
503
+ superseded_facts = store.facts.where(status: "superseded").count
504
+
505
+ result[:facts] = {
506
+ total: total_facts,
507
+ active: active_facts,
508
+ superseded: superseded_facts
509
+ }
510
+
511
+ # Top predicates
512
+ if active_facts > 0
513
+ top_predicates = store.db[:facts]
514
+ .where(status: "active")
515
+ .group_and_count(:predicate)
516
+ .order(Sequel.desc(:count))
517
+ .limit(10)
518
+ .all
519
+ .map { |row| {predicate: row[:predicate], count: row[:count]} }
520
+
521
+ result[:facts][:top_predicates] = top_predicates
522
+ end
523
+
524
+ # Entities by type
525
+ entity_counts = store.db[:entities]
526
+ .group_and_count(:type)
527
+ .order(Sequel.desc(:count))
528
+ .all
529
+ .map { |row| {type: row[:type], count: row[:count]} }
530
+
531
+ result[:entities] = {
532
+ total: store.entities.count,
533
+ by_type: entity_counts
534
+ }
535
+
536
+ # Content items
537
+ content_count = store.content_items.count
538
+ result[:content_items] = {
539
+ total: content_count
540
+ }
541
+
542
+ if content_count > 0
543
+ first_date = store.content_items.min(:occurred_at)
544
+ last_date = store.content_items.max(:occurred_at)
545
+ result[:content_items][:date_range] = {
546
+ first: first_date,
547
+ last: last_date
548
+ }
549
+ end
550
+
551
+ # Provenance coverage
552
+ if active_facts > 0
553
+ facts_with_provenance = store.db[:provenance]
554
+ .join(:facts, id: :fact_id)
555
+ .where(Sequel[:facts][:status] => "active")
556
+ .select(Sequel[:provenance][:fact_id])
557
+ .distinct
558
+ .count
559
+
560
+ coverage_percentage = (facts_with_provenance * 100.0 / active_facts).round(1)
561
+
562
+ result[:provenance] = {
563
+ facts_with_sources: facts_with_provenance,
564
+ total_active_facts: active_facts,
565
+ coverage_percentage: coverage_percentage
566
+ }
567
+ else
568
+ result[:provenance] = {
569
+ facts_with_sources: 0,
570
+ total_active_facts: 0,
571
+ coverage_percentage: 0
572
+ }
573
+ end
574
+
575
+ # Conflicts
576
+ open_conflicts = store.conflicts.where(status: "open").count
577
+ resolved_conflicts = store.conflicts.where(status: "resolved").count
578
+
579
+ result[:conflicts] = {
580
+ open: open_conflicts,
581
+ resolved: resolved_conflicts,
582
+ total: open_conflicts + resolved_conflicts
583
+ }
584
+
585
+ # Schema version
586
+ result[:schema_version] = store.schema_version
587
+
588
+ result
589
+ end
580
590
  end
581
591
  end
582
592
  end