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.
- checksums.yaml +4 -4
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/memory.sqlite3-shm +0 -0
- data/.claude/memory.sqlite3-wal +0 -0
- data/.claude/settings.json +78 -6
- data/.claude/settings.local.json +5 -2
- data/.claude/skills/improve/SKILL.md +113 -25
- data/.claude-plugin/commands/distill-transcripts.md +98 -0
- data/.claude-plugin/commands/memory-recall.md +67 -0
- data/.claude-plugin/marketplace.json +1 -1
- data/.claude-plugin/plugin.json +1 -2
- data/CHANGELOG.md +74 -1
- data/CLAUDE.md +32 -6
- data/README.md +1 -1
- data/docs/improvements.md +51 -91
- data/docs/influence/lossless-claw.md +409 -0
- data/docs/quality_review.md +119 -224
- data/hooks/hooks.json +39 -7
- data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
- data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
- data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
- data/lib/claude_memory/commands/completion_command.rb +179 -0
- data/lib/claude_memory/commands/doctor_command.rb +2 -0
- data/lib/claude_memory/commands/help_command.rb +4 -0
- data/lib/claude_memory/commands/hook_command.rb +2 -1
- data/lib/claude_memory/commands/index_command.rb +100 -65
- data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
- data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
- data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
- data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
- data/lib/claude_memory/commands/install_skill_command.rb +78 -0
- data/lib/claude_memory/commands/registry.rb +3 -1
- data/lib/claude_memory/commands/skills/distill-transcripts.md +98 -0
- data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
- data/lib/claude_memory/core/fact_ranker.rb +2 -2
- data/lib/claude_memory/core/rr_fusion.rb +23 -6
- data/lib/claude_memory/core/snippet_extractor.rb +7 -3
- data/lib/claude_memory/core/text_builder.rb +11 -0
- data/lib/claude_memory/domain/provenance.rb +0 -1
- data/lib/claude_memory/embeddings/api_adapter.rb +96 -0
- data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
- data/lib/claude_memory/embeddings/fastembed_adapter.rb +4 -0
- data/lib/claude_memory/embeddings/generator.rb +4 -0
- data/lib/claude_memory/embeddings/resolver.rb +18 -0
- data/lib/claude_memory/hook/context_injector.rb +58 -2
- data/lib/claude_memory/hook/distillation_runner.rb +46 -0
- data/lib/claude_memory/hook/handler.rb +11 -2
- data/lib/claude_memory/index/vector_index.rb +15 -2
- data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
- data/lib/claude_memory/mcp/error_classifier.rb +171 -0
- data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
- data/lib/claude_memory/mcp/handlers/management_handlers.rb +145 -0
- data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
- data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
- data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
- data/lib/claude_memory/mcp/handlers/stats_handlers.rb +202 -0
- data/lib/claude_memory/mcp/instructions_builder.rb +64 -5
- data/lib/claude_memory/mcp/query_guide.rb +51 -22
- data/lib/claude_memory/mcp/response_formatter.rb +4 -1
- data/lib/claude_memory/mcp/server.rb +1 -0
- data/lib/claude_memory/mcp/text_summary.rb +28 -1
- data/lib/claude_memory/mcp/tool_definitions.rb +33 -3
- data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
- data/lib/claude_memory/mcp/tools.rb +47 -681
- data/lib/claude_memory/recall/dual_engine.rb +105 -0
- data/lib/claude_memory/recall/legacy_engine.rb +138 -0
- data/lib/claude_memory/recall/query_core.rb +371 -0
- data/lib/claude_memory/recall.rb +29 -616
- data/lib/claude_memory/shortcuts.rb +4 -4
- data/lib/claude_memory/store/retry_handler.rb +61 -0
- data/lib/claude_memory/store/schema_manager.rb +68 -0
- data/lib/claude_memory/store/sqlite_store.rb +85 -201
- data/lib/claude_memory/sweep/maintenance.rb +126 -0
- data/lib/claude_memory/sweep/sweeper.rb +81 -75
- data/lib/claude_memory/templates/hooks.example.json +26 -7
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +12 -0
- data/v0.6.0.ANNOUNCE +32 -0
- 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
|
-
|
|
33
|
-
when "memory.
|
|
34
|
-
|
|
35
|
-
when "memory.
|
|
36
|
-
|
|
37
|
-
when "memory.
|
|
38
|
-
|
|
39
|
-
when "memory.
|
|
40
|
-
|
|
41
|
-
when "memory.
|
|
42
|
-
|
|
43
|
-
when "memory.
|
|
44
|
-
|
|
45
|
-
when "memory.
|
|
46
|
-
|
|
47
|
-
when "memory.
|
|
48
|
-
|
|
49
|
-
when "memory.
|
|
50
|
-
|
|
51
|
-
when "memory.
|
|
52
|
-
|
|
53
|
-
when "memory.
|
|
54
|
-
|
|
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: "
|
|
405
|
-
|
|
406
|
-
|
|
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
|
|
488
|
-
|
|
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
|
|
513
|
-
result = {global: nil, current_project: nil, other_projects: []}
|
|
514
|
-
|
|
97
|
+
def get_store_for_scope(scope)
|
|
515
98
|
if @manager
|
|
516
|
-
|
|
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
|
-
|
|
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
|