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