claude_memory 0.7.1 → 0.9.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +32 -2
  4. data/.claude/settings.json +65 -15
  5. data/.claude/settings.local.json +5 -2
  6. data/.claude/skills/improve/SKILL.md +113 -25
  7. data/.claude/skills/upgrade-dependencies/SKILL.md +154 -0
  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 +2 -2
  11. data/.claude-plugin/plugin.json +3 -3
  12. data/.claude-plugin/scripts/hook-runner.sh +14 -0
  13. data/.claude-plugin/scripts/serve-mcp.sh +14 -0
  14. data/.ruby-version +1 -1
  15. data/CHANGELOG.md +90 -1
  16. data/CLAUDE.md +56 -18
  17. data/README.md +35 -0
  18. data/db/migrations/013_add_mcp_tool_calls.rb +26 -0
  19. data/db/migrations/014_canonicalize_predicates.rb +30 -0
  20. data/docs/improvements.md +74 -74
  21. data/docs/influence/claude-mem.md +1 -0
  22. data/docs/influence/claude-supermemory.md +1 -0
  23. data/docs/influence/episodic-memory.md +1 -0
  24. data/docs/influence/grepai.md +1 -0
  25. data/docs/influence/kbs.md +1 -0
  26. data/docs/influence/lossless-claw.md +1 -0
  27. data/docs/influence/qmd.md +1 -0
  28. data/docs/quality_review.md +119 -224
  29. data/hooks/hooks.json +39 -7
  30. data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
  31. data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
  32. data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
  33. data/lib/claude_memory/commands/completion_command.rb +149 -0
  34. data/lib/claude_memory/commands/doctor_command.rb +2 -0
  35. data/lib/claude_memory/commands/embeddings_command.rb +198 -0
  36. data/lib/claude_memory/commands/help_command.rb +12 -1
  37. data/lib/claude_memory/commands/hook_command.rb +2 -1
  38. data/lib/claude_memory/commands/index_command.rb +85 -78
  39. data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
  40. data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
  41. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
  42. data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
  43. data/lib/claude_memory/commands/install_skill_command.rb +78 -0
  44. data/lib/claude_memory/commands/registry.rb +47 -32
  45. data/lib/claude_memory/commands/reject_command.rb +62 -0
  46. data/lib/claude_memory/commands/restore_command.rb +77 -0
  47. data/lib/claude_memory/commands/skills/distill-transcripts.md +102 -0
  48. data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
  49. data/lib/claude_memory/commands/stats_command.rb +98 -2
  50. data/lib/claude_memory/configuration.rb +14 -1
  51. data/lib/claude_memory/core/fact_ranker.rb +2 -2
  52. data/lib/claude_memory/core/rr_fusion.rb +23 -6
  53. data/lib/claude_memory/core/snippet_extractor.rb +7 -3
  54. data/lib/claude_memory/core/text_builder.rb +11 -0
  55. data/lib/claude_memory/distill/json_schema.md +8 -4
  56. data/lib/claude_memory/distill/null_distiller.rb +2 -0
  57. data/lib/claude_memory/domain/entity.rb +13 -1
  58. data/lib/claude_memory/domain/fact.rb +26 -2
  59. data/lib/claude_memory/domain/provenance.rb +0 -1
  60. data/lib/claude_memory/embeddings/api_adapter.rb +97 -0
  61. data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
  62. data/lib/claude_memory/embeddings/fastembed_adapter.rb +46 -12
  63. data/lib/claude_memory/embeddings/generator.rb +4 -0
  64. data/lib/claude_memory/embeddings/inspector.rb +91 -0
  65. data/lib/claude_memory/embeddings/model_registry.rb +210 -0
  66. data/lib/claude_memory/embeddings/resolver.rb +44 -0
  67. data/lib/claude_memory/hook/context_injector.rb +58 -2
  68. data/lib/claude_memory/hook/distillation_runner.rb +46 -0
  69. data/lib/claude_memory/hook/handler.rb +11 -2
  70. data/lib/claude_memory/index/vector_index.rb +15 -2
  71. data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
  72. data/lib/claude_memory/ingest/ingester.rb +17 -0
  73. data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
  74. data/lib/claude_memory/mcp/handlers/management_handlers.rb +169 -0
  75. data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
  76. data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
  77. data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
  78. data/lib/claude_memory/mcp/handlers/stats_handlers.rb +205 -0
  79. data/lib/claude_memory/mcp/instructions_builder.rb +19 -1
  80. data/lib/claude_memory/mcp/query_guide.rb +10 -0
  81. data/lib/claude_memory/mcp/response_formatter.rb +1 -0
  82. data/lib/claude_memory/mcp/server.rb +22 -1
  83. data/lib/claude_memory/mcp/telemetry.rb +86 -0
  84. data/lib/claude_memory/mcp/text_summary.rb +26 -0
  85. data/lib/claude_memory/mcp/tool_definitions.rb +116 -4
  86. data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
  87. data/lib/claude_memory/mcp/tools.rb +50 -679
  88. data/lib/claude_memory/publish.rb +40 -5
  89. data/lib/claude_memory/recall/dual_engine.rb +105 -0
  90. data/lib/claude_memory/recall/legacy_engine.rb +138 -0
  91. data/lib/claude_memory/recall/query_core.rb +371 -0
  92. data/lib/claude_memory/recall.rb +121 -673
  93. data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
  94. data/lib/claude_memory/resolve/resolver.rb +43 -0
  95. data/lib/claude_memory/shortcuts.rb +4 -4
  96. data/lib/claude_memory/store/retry_handler.rb +61 -0
  97. data/lib/claude_memory/store/schema_manager.rb +68 -0
  98. data/lib/claude_memory/store/sqlite_store.rb +334 -201
  99. data/lib/claude_memory/store/store_manager.rb +50 -1
  100. data/lib/claude_memory/sweep/maintenance.rb +115 -1
  101. data/lib/claude_memory/sweep/sweeper.rb +3 -0
  102. data/lib/claude_memory/templates/hooks.example.json +26 -7
  103. data/lib/claude_memory/version.rb +1 -1
  104. data/lib/claude_memory.rb +16 -0
  105. metadata +48 -8
  106. data/.claude/memory.sqlite3-shm +0 -0
  107. data/.claude/memory.sqlite3-wal +0 -0
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module MCP
5
+ module Handlers
6
+ # Management tool handlers (store_extraction, promote, sweep, changes, conflicts)
7
+ module ManagementHandlers
8
+ def store_extraction(args)
9
+ scope = args["scope"] || "project"
10
+ store = get_store_for_scope(scope)
11
+ return {error: "Database not available"} unless store
12
+
13
+ entities = (args["entities"] || []).map { |e| symbolize_keys(e) }
14
+ facts = (args["facts"] || []).map { |f| symbolize_keys(f) }
15
+ decisions = (args["decisions"] || []).map { |d| symbolize_keys(d) }
16
+
17
+ config = Configuration.new
18
+ project_path = config.project_dir
19
+ occurred_at = Time.now.utc.iso8601
20
+
21
+ searchable_text = Core::TextBuilder.build_searchable_text(entities, facts, decisions)
22
+ content_item_id = create_synthetic_content_item(store, searchable_text, project_path, occurred_at)
23
+ index_content_item(store, content_item_id, searchable_text)
24
+
25
+ extraction = Distill::Extraction.new(
26
+ entities: entities,
27
+ facts: facts,
28
+ decisions: decisions,
29
+ signals: []
30
+ )
31
+
32
+ resolver = Resolve::Resolver.new(store)
33
+ result = resolver.apply(
34
+ extraction,
35
+ content_item_id: content_item_id,
36
+ occurred_at: occurred_at,
37
+ project_path: project_path,
38
+ scope: scope
39
+ )
40
+
41
+ {
42
+ success: true,
43
+ scope: scope,
44
+ entities_created: result[:entities_created],
45
+ facts_created: result[:facts_created],
46
+ facts_superseded: result[:facts_superseded],
47
+ conflicts_created: result[:conflicts_created]
48
+ }
49
+ end
50
+
51
+ def promote(args)
52
+ return {error: "Promote requires StoreManager"} unless @manager
53
+
54
+ fact_id = args["fact_id"]
55
+ global_fact_id = @manager.promote_fact(fact_id)
56
+
57
+ if global_fact_id
58
+ {
59
+ success: true,
60
+ project_fact_id: fact_id,
61
+ global_fact_id: global_fact_id,
62
+ message: "Fact promoted to global memory"
63
+ }
64
+ else
65
+ {error: "Fact #{fact_id} not found in project database"}
66
+ end
67
+ end
68
+
69
+ def reject_fact(args)
70
+ scope = args["scope"] || "project"
71
+ store = get_store_for_scope(scope)
72
+ return {error: "Database not available"} unless store
73
+
74
+ fact_id = args["fact_id"]
75
+ if fact_id.nil? && args["docid"]
76
+ row = store.find_fact_by_docid(args["docid"])
77
+ fact_id = row && row[:id]
78
+ end
79
+ return {error: "fact_id or docid required"} if fact_id.nil?
80
+
81
+ result = store.reject_fact(fact_id, reason: args["reason"])
82
+ return {error: "Fact #{fact_id} not found in #{scope} database"} if result.nil?
83
+
84
+ {
85
+ success: true,
86
+ scope: scope,
87
+ fact_id: fact_id,
88
+ conflicts_resolved: result[:conflicts_resolved],
89
+ message: "Fact rejected"
90
+ }
91
+ end
92
+
93
+ def sweep_now(args)
94
+ scope = args["scope"] || "project"
95
+ store = get_store_for_scope(scope)
96
+ return {error: "Database not available"} unless store
97
+
98
+ sweeper = Sweep::Sweeper.new(store)
99
+ budget = args["budget_seconds"] || 5
100
+ stats = if args["escalate"]
101
+ sweeper.run_with_escalation!(budget_seconds: budget)
102
+ else
103
+ sweeper.run!(budget_seconds: budget)
104
+ end
105
+ ResponseFormatter.format_sweep_stats(scope, stats)
106
+ end
107
+
108
+ def changes(args)
109
+ since = args["since"] || (Time.now - 86400 * 7).utc.iso8601
110
+ scope = args["scope"] || "all"
111
+ list = @recall.changes(since: since, limit: args["limit"] || 20, scope: scope)
112
+ ResponseFormatter.format_changes(since, list)
113
+ end
114
+
115
+ def conflicts(args)
116
+ scope = args["scope"] || "all"
117
+ list = @recall.conflicts(scope: scope)
118
+ ResponseFormatter.format_conflicts(list)
119
+ end
120
+
121
+ def mark_distilled(args)
122
+ content_item_id = args["content_item_id"]
123
+ facts_extracted = args["facts_extracted"] || 0
124
+
125
+ store = find_store_for_content_item(content_item_id)
126
+ return {error: "Content item #{content_item_id} not found"} unless store
127
+
128
+ store.record_ingestion_metrics(
129
+ content_item_id: content_item_id,
130
+ input_tokens: 0,
131
+ output_tokens: 0,
132
+ facts_extracted: facts_extracted
133
+ )
134
+
135
+ {
136
+ success: true,
137
+ content_item_id: content_item_id,
138
+ facts_extracted: facts_extracted
139
+ }
140
+ end
141
+
142
+ private
143
+
144
+ def create_synthetic_content_item(store, text, project_path, occurred_at)
145
+ text_hash = Digest::SHA256.hexdigest(text)
146
+ store.upsert_content_item(
147
+ source: "mcp_extraction",
148
+ session_id: "mcp-#{Time.now.to_i}",
149
+ transcript_path: nil,
150
+ project_path: project_path,
151
+ text_hash: text_hash,
152
+ byte_len: text.bytesize,
153
+ raw_text: text,
154
+ occurred_at: occurred_at
155
+ )
156
+ end
157
+
158
+ def index_content_item(store, content_item_id, text)
159
+ fts = Index::LexicalFTS.new(store)
160
+ fts.index_content_item(content_item_id, text)
161
+ end
162
+
163
+ def symbolize_keys(hash)
164
+ Core::TextBuilder.symbolize_keys(hash)
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module MCP
5
+ module Handlers
6
+ # Query and recall tool handlers
7
+ module QueryHandlers
8
+ def recall(args)
9
+ return database_not_found_error unless databases_exist?
10
+
11
+ scope = extract_scope(args)
12
+ limit = extract_limit(args)
13
+ compact = args["compact"] == true
14
+ query = args["query"]
15
+ intent = extract_intent(args)
16
+ results = @recall.query(query, limit: limit, scope: scope, include_raw_text: !compact, intent: intent)
17
+ ResponseFormatter.format_recall_results(results, compact: compact, query: query)
18
+ rescue Sequel::DatabaseError, Sequel::DatabaseConnectionError, Errno::ENOENT => e
19
+ classified_error(e, tool_name: "memory.recall")
20
+ end
21
+
22
+ def recall_index(args)
23
+ scope = extract_scope(args)
24
+ limit = extract_limit(args, default: 20)
25
+ intent = extract_intent(args)
26
+ results = @recall.query_index(args["query"], limit: limit, scope: scope, intent: intent)
27
+ ResponseFormatter.format_index_results(args["query"], scope, results)
28
+ end
29
+
30
+ def recall_details(args)
31
+ fact_ids = args["fact_ids"]
32
+ scope = args["scope"] || "project"
33
+
34
+ explanations = fact_ids.map do |fact_id|
35
+ explanation = @recall.explain(fact_id, scope: scope)
36
+ next nil if explanation.is_a?(Core::NullExplanation)
37
+
38
+ ResponseFormatter.format_detailed_explanation(explanation)
39
+ end.compact
40
+
41
+ {
42
+ fact_count: explanations.size,
43
+ facts: explanations
44
+ }
45
+ end
46
+
47
+ def explain(args)
48
+ scope = args["scope"] || "project"
49
+ explanation = @recall.explain(args["fact_id"], scope: scope)
50
+ return {error: "Fact not found in #{scope} database"} if explanation.is_a?(Core::NullExplanation)
51
+
52
+ ResponseFormatter.format_explanation(explanation, scope)
53
+ end
54
+
55
+ def recall_semantic(args)
56
+ query = args["query"]
57
+ mode = (args["mode"] || "both").to_sym
58
+ scope = extract_scope(args)
59
+ limit = extract_limit(args)
60
+ compact = args["compact"] == true
61
+ explain = args["explain"] == true
62
+ intent = extract_intent(args)
63
+
64
+ results = @recall.query_semantic(query, limit: limit, scope: scope, mode: mode, explain: explain, intent: intent)
65
+ ResponseFormatter.format_semantic_results(query, mode.to_s, scope, results, compact: compact)
66
+ end
67
+
68
+ def search_concepts(args)
69
+ concepts = args["concepts"]
70
+ scope = extract_scope(args)
71
+ limit = extract_limit(args)
72
+ compact = args["compact"] == true
73
+
74
+ return {error: "Must provide 2-5 concepts"} unless (2..5).cover?(concepts.size)
75
+
76
+ results = @recall.query_concepts(concepts, limit: limit, scope: scope)
77
+ ResponseFormatter.format_concept_results(concepts, scope, results, compact: compact)
78
+ end
79
+
80
+ def fact_graph(args)
81
+ fact_id = args["fact_id"]
82
+ depth = args["depth"] || 2
83
+ scope = args["scope"] || "project"
84
+
85
+ graph = @recall.fact_graph(fact_id, depth: depth, scope: scope)
86
+
87
+ return {error: "Fact #{fact_id} not found in #{scope} database"} if graph[:node_count] == 0
88
+
89
+ graph
90
+ end
91
+
92
+ def undistilled(args)
93
+ limit = args["limit"] || 3
94
+ min_length = args["min_length"] || 200
95
+ max_text = 2000
96
+
97
+ items = collect_undistilled_items(limit: limit, min_length: min_length)
98
+
99
+ {
100
+ count: items.size,
101
+ items: items.map { |item|
102
+ {
103
+ content_item_id: item[:id],
104
+ occurred_at: item[:occurred_at],
105
+ occurred_ago: Core::RelativeTime.format(item[:occurred_at]),
106
+ project_path: item[:project_path],
107
+ raw_text: Core::TextBuilder.truncate(item[:raw_text], max_text)
108
+ }
109
+ }
110
+ }
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module MCP
5
+ module Handlers
6
+ # Setup and discovery tool handlers
7
+ module SetupHandlers
8
+ def check_setup
9
+ issues = []
10
+ warnings = []
11
+ config = Configuration.new
12
+
13
+ global_db_exists = check_global_database(config, issues)
14
+ project_db_exists = check_project_database(config, warnings)
15
+ current_version, version_status, claude_md_exists = check_claude_md_version(warnings)
16
+ hooks_configured = check_hooks_configuration(warnings)
17
+
18
+ build_setup_result(
19
+ global_db_exists, project_db_exists, claude_md_exists,
20
+ hooks_configured, current_version, version_status,
21
+ issues, warnings
22
+ )
23
+ end
24
+
25
+ def list_projects
26
+ result = {global: nil, current_project: nil, other_projects: []}
27
+
28
+ if @manager
29
+ result[:global] = list_global_database
30
+ result[:current_project] = list_current_project
31
+ result[:other_projects] = discover_other_projects
32
+ elsif @legacy_store
33
+ result[:global] = {
34
+ exists: true,
35
+ path: @legacy_store.db.opts[:database],
36
+ facts_active: @legacy_store.facts.where(status: "active").count,
37
+ entities: @legacy_store.entities.count
38
+ }
39
+ end
40
+
41
+ result[:project_count] = 1 + result[:other_projects].size
42
+ result
43
+ end
44
+
45
+ private
46
+
47
+ def check_global_database(config, issues)
48
+ exists = File.exist?(config.global_db_path)
49
+ issues << "Global database not found at #{config.global_db_path}" unless exists
50
+ exists
51
+ end
52
+
53
+ def check_project_database(config, warnings)
54
+ exists = File.exist?(config.project_db_path)
55
+ warnings << "Project database not found at #{config.project_db_path}" unless exists
56
+ exists
57
+ end
58
+
59
+ def check_claude_md_version(warnings)
60
+ claude_md_path = ".claude/CLAUDE.md"
61
+ unless File.exist?(claude_md_path)
62
+ warnings << "No .claude/CLAUDE.md found"
63
+ return [nil, nil, false]
64
+ end
65
+
66
+ content = File.read(claude_md_path)
67
+ unless content.include?("ClaudeMemory")
68
+ warnings << "CLAUDE.md exists but no ClaudeMemory configuration found"
69
+ return [nil, nil, true]
70
+ end
71
+
72
+ current_version = SetupStatusAnalyzer.extract_version(content)
73
+ unless current_version
74
+ warnings << "CLAUDE.md has ClaudeMemory section but no version marker"
75
+ return [nil, "no_version_marker", true]
76
+ end
77
+
78
+ version_status = SetupStatusAnalyzer.determine_version_status(current_version, ClaudeMemory::VERSION)
79
+ if version_status == "outdated"
80
+ warnings << "Configuration version (v#{current_version}) is older than ClaudeMemory (v#{ClaudeMemory::VERSION}). Consider running upgrade."
81
+ end
82
+
83
+ [current_version, version_status, true]
84
+ end
85
+
86
+ def check_hooks_configuration(warnings)
87
+ settings_paths = [".claude/settings.json", ".claude/settings.local.json"]
88
+ settings_paths.each do |path|
89
+ next unless File.exist?(path)
90
+ begin
91
+ config_data = JSON.parse(File.read(path))
92
+ return true if config_data["hooks"]&.any?
93
+ rescue JSON::ParserError
94
+ warnings << "Invalid JSON in #{path}"
95
+ end
96
+ end
97
+
98
+ warnings << "No hooks configured for automatic ingestion"
99
+ false
100
+ end
101
+
102
+ def build_setup_result(global_db_exists, project_db_exists, claude_md_exists, hooks_configured, current_version, version_status, issues, warnings)
103
+ initialized = global_db_exists && claude_md_exists
104
+ status = SetupStatusAnalyzer.determine_status(global_db_exists, claude_md_exists, version_status)
105
+ recommendations = SetupStatusAnalyzer.generate_recommendations(initialized, version_status, warnings.any?)
106
+
107
+ {
108
+ status: status,
109
+ initialized: initialized,
110
+ version: {
111
+ current: current_version || "unknown",
112
+ latest: ClaudeMemory::VERSION,
113
+ status: version_status || "unknown"
114
+ },
115
+ components: {
116
+ global_database: global_db_exists,
117
+ project_database: project_db_exists,
118
+ claude_md: claude_md_exists,
119
+ hooks_configured: hooks_configured
120
+ },
121
+ issues: issues,
122
+ warnings: warnings,
123
+ recommendations: recommendations
124
+ }
125
+ end
126
+
127
+ def list_global_database
128
+ if @manager.global_exists?
129
+ @manager.ensure_global!
130
+ store = @manager.global_store
131
+ {
132
+ exists: true,
133
+ path: @manager.global_db_path,
134
+ facts_active: store.facts.where(status: "active").count,
135
+ facts_total: store.facts.count,
136
+ entities: store.entities.count
137
+ }
138
+ else
139
+ {exists: false, path: @manager.global_db_path}
140
+ end
141
+ end
142
+
143
+ def list_current_project
144
+ if @manager.project_exists?
145
+ @manager.ensure_project!
146
+ store = @manager.project_store
147
+ {
148
+ exists: true,
149
+ path: @manager.project_path,
150
+ db_path: @manager.project_db_path,
151
+ facts_active: store.facts.where(status: "active").count,
152
+ facts_total: store.facts.count,
153
+ entities: store.entities.count
154
+ }
155
+ else
156
+ {exists: false, path: @manager.project_path, db_path: @manager.project_db_path}
157
+ end
158
+ end
159
+
160
+ def discover_other_projects
161
+ return [] unless @manager.global_exists?
162
+
163
+ @manager.ensure_global!
164
+ global = @manager.global_store
165
+
166
+ promoted_paths = global.facts
167
+ .where(Sequel.like(:created_from, "promoted:%"))
168
+ .select(:created_from)
169
+ .distinct
170
+ .all
171
+ .filter_map { |f|
172
+ match = f[:created_from]&.match(/\Apromoted:(.+):\d+\z/)
173
+ match[1] if match
174
+ }
175
+ .uniq
176
+
177
+ fact_paths = global.facts
178
+ .exclude(project_path: nil)
179
+ .select(:project_path)
180
+ .distinct
181
+ .all
182
+ .map { |f| f[:project_path] }
183
+
184
+ all_paths = (promoted_paths + fact_paths).uniq
185
+ current = @manager.project_path
186
+
187
+ all_paths.filter_map { |path|
188
+ next if path == current
189
+
190
+ db_path = File.join(path, ".claude", "memory.sqlite3")
191
+ entry = {path: path, db_path: db_path, exists: File.exist?(db_path)}
192
+
193
+ if entry[:exists]
194
+ begin
195
+ temp_store = Store::SQLiteStore.new(db_path)
196
+ entry[:facts_active] = temp_store.facts.where(status: "active").count
197
+ entry[:facts_total] = temp_store.facts.count
198
+ entry[:entities] = temp_store.entities.count
199
+ temp_store.close
200
+ rescue Sequel::DatabaseError, Extralite::Error, IOError => _e
201
+ entry[:error] = "Could not read database"
202
+ end
203
+ end
204
+
205
+ entry
206
+ }
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module MCP
5
+ module Handlers
6
+ # Shortcut tool handlers (decisions, conventions, architecture)
7
+ module ShortcutHandlers
8
+ def decisions(args)
9
+ return {error: "Decisions shortcut requires StoreManager"} unless @manager
10
+
11
+ results = Recall.recent_decisions(@manager, limit: args["limit"] || 10)
12
+ format_shortcut_results(results, "decisions")
13
+ end
14
+
15
+ def conventions(args)
16
+ return {error: "Conventions shortcut requires StoreManager"} unless @manager
17
+
18
+ results = Recall.conventions(@manager, limit: args["limit"] || 20)
19
+ format_shortcut_results(results, "conventions")
20
+ end
21
+
22
+ def architecture(args)
23
+ return {error: "Architecture shortcut requires StoreManager"} unless @manager
24
+
25
+ results = Recall.architecture_choices(@manager, limit: args["limit"] || 10)
26
+ format_shortcut_results(results, "architecture")
27
+ end
28
+
29
+ private
30
+
31
+ def format_shortcut_results(results, category)
32
+ ResponseFormatter.format_shortcut_results(category, results)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end