claude_memory 0.4.0 → 0.5.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/CLAUDE.md +1 -1
- data/.claude/rules/claude_memory.generated.md +14 -1
- data/.claude/skills/check-memory/SKILL.md +10 -0
- data/.claude/skills/improve/SKILL.md +12 -1
- data/.claude-plugin/plugin.json +1 -1
- data/CHANGELOG.md +70 -0
- data/db/migrations/008_add_provenance_line_range.rb +21 -0
- data/db/migrations/009_add_docid.rb +39 -0
- data/db/migrations/010_add_llm_cache.rb +30 -0
- data/docs/improvements.md +72 -1084
- data/docs/influence/claude-supermemory.md +498 -0
- data/docs/influence/qmd.md +424 -2022
- data/docs/quality_review.md +64 -705
- data/lib/claude_memory/commands/doctor_command.rb +45 -4
- data/lib/claude_memory/commands/explain_command.rb +11 -6
- data/lib/claude_memory/commands/stats_command.rb +1 -1
- data/lib/claude_memory/core/fact_graph.rb +122 -0
- data/lib/claude_memory/core/fact_query_builder.rb +34 -14
- data/lib/claude_memory/core/fact_ranker.rb +3 -20
- data/lib/claude_memory/core/relative_time.rb +45 -0
- data/lib/claude_memory/core/result_sorter.rb +2 -2
- data/lib/claude_memory/core/rr_fusion.rb +57 -0
- data/lib/claude_memory/core/snippet_extractor.rb +97 -0
- data/lib/claude_memory/domain/fact.rb +3 -1
- data/lib/claude_memory/index/index_query.rb +2 -0
- data/lib/claude_memory/index/lexical_fts.rb +18 -0
- data/lib/claude_memory/infrastructure/operation_tracker.rb +7 -21
- data/lib/claude_memory/infrastructure/schema_validator.rb +30 -25
- data/lib/claude_memory/ingest/content_sanitizer.rb +8 -1
- data/lib/claude_memory/ingest/ingester.rb +67 -56
- data/lib/claude_memory/ingest/tool_extractor.rb +1 -1
- data/lib/claude_memory/ingest/tool_filter.rb +55 -0
- data/lib/claude_memory/logging/logger.rb +112 -0
- data/lib/claude_memory/mcp/query_guide.rb +96 -0
- data/lib/claude_memory/mcp/response_formatter.rb +86 -23
- data/lib/claude_memory/mcp/server.rb +34 -4
- data/lib/claude_memory/mcp/text_summary.rb +257 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +20 -4
- data/lib/claude_memory/mcp/tools.rb +133 -120
- data/lib/claude_memory/publish.rb +12 -2
- data/lib/claude_memory/recall/expansion_detector.rb +44 -0
- data/lib/claude_memory/recall.rb +93 -41
- data/lib/claude_memory/resolve/resolver.rb +72 -40
- data/lib/claude_memory/store/sqlite_store.rb +99 -24
- data/lib/claude_memory/sweep/sweeper.rb +6 -0
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +21 -0
- metadata +14 -2
- data/docs/remaining_improvements.md +0 -330
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../core/relative_time"
|
|
4
|
+
require_relative "../core/snippet_extractor"
|
|
5
|
+
|
|
3
6
|
module ClaudeMemory
|
|
4
7
|
module MCP
|
|
5
8
|
# Pure logic for formatting domain objects into MCP tool responses
|
|
@@ -7,26 +10,32 @@ module ClaudeMemory
|
|
|
7
10
|
class ResponseFormatter
|
|
8
11
|
# Format recall query results into MCP response
|
|
9
12
|
# @param results [Array<Hash>] Recall results with :fact and :receipts
|
|
13
|
+
# @param compact [Boolean] Omit receipts for smaller responses
|
|
14
|
+
# @param query [String, nil] Original query for snippet extraction
|
|
10
15
|
# @return [Hash] MCP response with facts array
|
|
11
|
-
def self.format_recall_results(results)
|
|
16
|
+
def self.format_recall_results(results, compact: false, query: nil)
|
|
12
17
|
{
|
|
13
|
-
facts: results.map { |r| format_recall_fact(r) }
|
|
18
|
+
facts: results.map { |r| format_recall_fact(r, compact: compact, query: query) }
|
|
14
19
|
}
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
# Format single recall fact result
|
|
18
23
|
# @param result [Hash] Single result with :fact, :receipts, :source
|
|
24
|
+
# @param compact [Boolean] Omit receipts
|
|
25
|
+
# @param query [String, nil] Original query for snippet extraction
|
|
19
26
|
# @return [Hash] Formatted fact for MCP response
|
|
20
|
-
def self.format_recall_fact(result)
|
|
21
|
-
{
|
|
27
|
+
def self.format_recall_fact(result, compact: false, query: nil)
|
|
28
|
+
fact = {
|
|
22
29
|
id: result[:fact][:id],
|
|
30
|
+
docid: result[:fact][:docid],
|
|
23
31
|
subject: result[:fact][:subject_name],
|
|
24
32
|
predicate: result[:fact][:predicate],
|
|
25
33
|
object: result[:fact][:object_literal],
|
|
26
34
|
status: result[:fact][:status],
|
|
27
|
-
source: result[:source]
|
|
28
|
-
receipts: result[:receipts].map { |p| format_receipt(p) }
|
|
35
|
+
source: result[:source]
|
|
29
36
|
}
|
|
37
|
+
fact[:receipts] = result[:receipts].map { |p| format_receipt(p, query: query) } unless compact
|
|
38
|
+
fact
|
|
30
39
|
end
|
|
31
40
|
|
|
32
41
|
# Format index query results with token estimates
|
|
@@ -52,6 +61,7 @@ module ClaudeMemory
|
|
|
52
61
|
def self.format_index_fact(result)
|
|
53
62
|
{
|
|
54
63
|
id: result[:id],
|
|
64
|
+
docid: result[:docid],
|
|
55
65
|
subject: result[:subject],
|
|
56
66
|
predicate: result[:predicate],
|
|
57
67
|
object_preview: result[:object_preview],
|
|
@@ -71,11 +81,13 @@ module ClaudeMemory
|
|
|
71
81
|
{
|
|
72
82
|
fact: {
|
|
73
83
|
id: explanation[:fact][:id],
|
|
84
|
+
docid: explanation[:fact][:docid],
|
|
74
85
|
subject: explanation[:fact][:subject_name],
|
|
75
86
|
predicate: explanation[:fact][:predicate],
|
|
76
87
|
object: explanation[:fact][:object_literal],
|
|
77
88
|
status: explanation[:fact][:status],
|
|
78
89
|
valid_from: explanation[:fact][:valid_from],
|
|
90
|
+
valid_from_ago: Core::RelativeTime.format(explanation[:fact][:valid_from]),
|
|
79
91
|
valid_to: explanation[:fact][:valid_to]
|
|
80
92
|
},
|
|
81
93
|
source: scope,
|
|
@@ -93,6 +105,7 @@ module ClaudeMemory
|
|
|
93
105
|
{
|
|
94
106
|
fact: {
|
|
95
107
|
id: explanation[:fact][:id],
|
|
108
|
+
docid: explanation[:fact][:docid],
|
|
96
109
|
subject: explanation[:fact][:subject_name],
|
|
97
110
|
predicate: explanation[:fact][:predicate],
|
|
98
111
|
object: explanation[:fact][:object_literal],
|
|
@@ -100,6 +113,7 @@ module ClaudeMemory
|
|
|
100
113
|
confidence: explanation[:fact][:confidence],
|
|
101
114
|
scope: explanation[:fact][:scope],
|
|
102
115
|
valid_from: explanation[:fact][:valid_from],
|
|
116
|
+
valid_from_ago: Core::RelativeTime.format(explanation[:fact][:valid_from]),
|
|
103
117
|
valid_to: explanation[:fact][:valid_to]
|
|
104
118
|
},
|
|
105
119
|
receipts: explanation[:receipts].map { |r| format_detailed_receipt(r) },
|
|
@@ -113,21 +127,55 @@ module ClaudeMemory
|
|
|
113
127
|
|
|
114
128
|
# Format receipt (provenance) with minimal fields
|
|
115
129
|
# @param receipt [Hash] Receipt with :quote and :strength
|
|
130
|
+
# @param query [String, nil] Optional query for snippet extraction
|
|
116
131
|
# @return [Hash] Formatted receipt
|
|
117
|
-
def self.format_receipt(receipt)
|
|
118
|
-
{quote: receipt[:quote], strength: receipt[:strength]}
|
|
132
|
+
def self.format_receipt(receipt, query: nil)
|
|
133
|
+
result = {quote: receipt[:quote], strength: receipt[:strength]}
|
|
134
|
+
append_line_range(result, receipt)
|
|
135
|
+
append_snippet(result, receipt, query)
|
|
136
|
+
result
|
|
119
137
|
end
|
|
120
138
|
|
|
121
139
|
# Format detailed receipt with session and timestamp
|
|
122
140
|
# @param receipt [Hash] Receipt with full fields
|
|
141
|
+
# @param query [String, nil] Optional query for snippet extraction
|
|
123
142
|
# @return [Hash] Formatted detailed receipt
|
|
124
|
-
def self.format_detailed_receipt(receipt)
|
|
125
|
-
{
|
|
143
|
+
def self.format_detailed_receipt(receipt, query: nil)
|
|
144
|
+
result = {
|
|
126
145
|
quote: receipt[:quote],
|
|
127
146
|
strength: receipt[:strength],
|
|
128
147
|
session_id: receipt[:session_id],
|
|
129
|
-
occurred_at: receipt[:occurred_at]
|
|
148
|
+
occurred_at: receipt[:occurred_at],
|
|
149
|
+
occurred_ago: Core::RelativeTime.format(receipt[:occurred_at])
|
|
130
150
|
}
|
|
151
|
+
append_line_range(result, receipt)
|
|
152
|
+
append_snippet(result, receipt, query)
|
|
153
|
+
result
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Append stored line range from provenance if available
|
|
157
|
+
# @param result [Hash] Result hash to append to
|
|
158
|
+
# @param receipt [Hash] Receipt with optional :line_start, :line_end
|
|
159
|
+
def self.append_line_range(result, receipt)
|
|
160
|
+
return unless receipt[:line_start]
|
|
161
|
+
|
|
162
|
+
result[:line_start] = receipt[:line_start]
|
|
163
|
+
result[:line_end] = receipt[:line_end]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Extract and append snippet from raw_text if available
|
|
167
|
+
# @param result [Hash] Result hash to append to
|
|
168
|
+
# @param receipt [Hash] Receipt with optional :raw_text
|
|
169
|
+
# @param query [String, nil] Query for snippet extraction
|
|
170
|
+
def self.append_snippet(result, receipt, query)
|
|
171
|
+
return unless query && receipt[:raw_text]
|
|
172
|
+
|
|
173
|
+
extracted = Core::SnippetExtractor.extract_with_lines(receipt[:raw_text], query)
|
|
174
|
+
return unless extracted
|
|
175
|
+
|
|
176
|
+
result[:snippet] = extracted[:snippet]
|
|
177
|
+
result[:line_start] = extracted[:line_start]
|
|
178
|
+
result[:line_end] = extracted[:line_end]
|
|
131
179
|
end
|
|
132
180
|
|
|
133
181
|
# Format changes list into MCP response
|
|
@@ -147,10 +195,12 @@ module ClaudeMemory
|
|
|
147
195
|
def self.format_change(change)
|
|
148
196
|
{
|
|
149
197
|
id: change[:id],
|
|
198
|
+
docid: change[:docid],
|
|
150
199
|
predicate: change[:predicate],
|
|
151
200
|
object: change[:object_literal],
|
|
152
201
|
status: change[:status],
|
|
153
202
|
created_at: change[:created_at],
|
|
203
|
+
created_ago: Core::RelativeTime.format(change[:created_at]),
|
|
154
204
|
source: change[:source]
|
|
155
205
|
}
|
|
156
206
|
end
|
|
@@ -198,62 +248,73 @@ module ClaudeMemory
|
|
|
198
248
|
# @param mode [String] Search mode (vector, text, both)
|
|
199
249
|
# @param scope [String] Scope
|
|
200
250
|
# @param results [Array<Hash>] Results with similarity scores
|
|
251
|
+
# @param compact [Boolean] Omit receipts for smaller responses
|
|
201
252
|
# @return [Hash] Formatted semantic search response
|
|
202
|
-
def self.format_semantic_results(query, mode, scope, results)
|
|
253
|
+
def self.format_semantic_results(query, mode, scope, results, compact: false)
|
|
203
254
|
{
|
|
204
255
|
query: query,
|
|
205
256
|
mode: mode,
|
|
206
257
|
scope: scope,
|
|
207
258
|
count: results.size,
|
|
208
|
-
facts: results.map { |r| format_semantic_fact(r) }
|
|
259
|
+
facts: results.map { |r| format_semantic_fact(r, compact: compact, query: query) }
|
|
209
260
|
}
|
|
210
261
|
end
|
|
211
262
|
|
|
212
263
|
# Format single semantic search fact with similarity
|
|
213
264
|
# @param result [Hash] Result with fact, receipts, and similarity
|
|
265
|
+
# @param compact [Boolean] Omit receipts
|
|
266
|
+
# @param query [String, nil] Original query for snippet extraction
|
|
214
267
|
# @return [Hash] Formatted fact with similarity
|
|
215
|
-
def self.format_semantic_fact(result)
|
|
216
|
-
{
|
|
268
|
+
def self.format_semantic_fact(result, compact: false, query: nil)
|
|
269
|
+
fact = {
|
|
217
270
|
id: result[:fact][:id],
|
|
271
|
+
docid: result[:fact][:docid],
|
|
218
272
|
subject: result[:fact][:subject_name],
|
|
219
273
|
predicate: result[:fact][:predicate],
|
|
220
274
|
object: result[:fact][:object_literal],
|
|
221
275
|
scope: result[:fact][:scope],
|
|
222
276
|
source: result[:source],
|
|
223
|
-
similarity: result[:similarity]
|
|
224
|
-
receipts: result[:receipts].map { |r| format_receipt(r) }
|
|
277
|
+
similarity: result[:similarity]
|
|
225
278
|
}
|
|
279
|
+
fact[:receipts] = result[:receipts].map { |r| format_receipt(r, query: query) } unless compact
|
|
280
|
+
fact
|
|
226
281
|
end
|
|
227
282
|
|
|
228
283
|
# Format concept search results
|
|
229
284
|
# @param concepts [Array<String>] Concepts searched
|
|
230
285
|
# @param scope [String] Scope
|
|
231
286
|
# @param results [Array<Hash>] Results with similarity scores
|
|
287
|
+
# @param compact [Boolean] Omit receipts for smaller responses
|
|
232
288
|
# @return [Hash] Formatted concept search response
|
|
233
|
-
def self.format_concept_results(concepts, scope, results)
|
|
289
|
+
def self.format_concept_results(concepts, scope, results, compact: false)
|
|
290
|
+
query = concepts.join(" ")
|
|
234
291
|
{
|
|
235
292
|
concepts: concepts,
|
|
236
293
|
scope: scope,
|
|
237
294
|
count: results.size,
|
|
238
|
-
facts: results.map { |r| format_concept_fact(r) }
|
|
295
|
+
facts: results.map { |r| format_concept_fact(r, compact: compact, query: query) }
|
|
239
296
|
}
|
|
240
297
|
end
|
|
241
298
|
|
|
242
299
|
# Format single concept search fact with multi-concept similarity
|
|
243
300
|
# @param result [Hash] Result with average and per-concept similarities
|
|
301
|
+
# @param compact [Boolean] Omit receipts
|
|
302
|
+
# @param query [String, nil] Original query for snippet extraction
|
|
244
303
|
# @return [Hash] Formatted fact with concept similarities
|
|
245
|
-
def self.format_concept_fact(result)
|
|
246
|
-
{
|
|
304
|
+
def self.format_concept_fact(result, compact: false, query: nil)
|
|
305
|
+
fact = {
|
|
247
306
|
id: result[:fact][:id],
|
|
307
|
+
docid: result[:fact][:docid],
|
|
248
308
|
subject: result[:fact][:subject_name],
|
|
249
309
|
predicate: result[:fact][:predicate],
|
|
250
310
|
object: result[:fact][:object_literal],
|
|
251
311
|
scope: result[:fact][:scope],
|
|
252
312
|
source: result[:source],
|
|
253
313
|
average_similarity: result[:similarity],
|
|
254
|
-
concept_similarities: result[:concept_similarities]
|
|
255
|
-
receipts: result[:receipts].map { |r| format_receipt(r) }
|
|
314
|
+
concept_similarities: result[:concept_similarities]
|
|
256
315
|
}
|
|
316
|
+
fact[:receipts] = result[:receipts].map { |r| format_receipt(r, query: query) } unless compact
|
|
317
|
+
fact
|
|
257
318
|
end
|
|
258
319
|
|
|
259
320
|
# Format shortcut query results (decisions, architecture, etc.)
|
|
@@ -274,6 +335,7 @@ module ClaudeMemory
|
|
|
274
335
|
def self.format_shortcut_fact(result)
|
|
275
336
|
{
|
|
276
337
|
id: result[:fact][:id],
|
|
338
|
+
docid: result[:fact][:docid],
|
|
277
339
|
subject: result[:fact][:subject_name],
|
|
278
340
|
predicate: result[:fact][:predicate],
|
|
279
341
|
object: result[:fact][:object_literal],
|
|
@@ -318,6 +380,7 @@ module ClaudeMemory
|
|
|
318
380
|
def self.format_generic_fact(result)
|
|
319
381
|
{
|
|
320
382
|
id: result[:fact][:id],
|
|
383
|
+
docid: result[:fact][:docid],
|
|
321
384
|
subject: result[:fact][:subject_name],
|
|
322
385
|
predicate: result[:fact][:predicate],
|
|
323
386
|
object: result[:fact][:object_literal],
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "query_guide"
|
|
5
|
+
require_relative "text_summary"
|
|
4
6
|
|
|
5
7
|
module ClaudeMemory
|
|
6
8
|
module MCP
|
|
@@ -58,6 +60,10 @@ module ClaudeMemory
|
|
|
58
60
|
handle_tools_list(id)
|
|
59
61
|
when "tools/call"
|
|
60
62
|
handle_tools_call(id, request["params"])
|
|
63
|
+
when "prompts/list"
|
|
64
|
+
handle_prompts_list(id)
|
|
65
|
+
when "prompts/get"
|
|
66
|
+
handle_prompts_get(id, request["params"])
|
|
61
67
|
when "shutdown"
|
|
62
68
|
@running = false
|
|
63
69
|
{jsonrpc: "2.0", id: id, result: nil}
|
|
@@ -73,7 +79,8 @@ module ClaudeMemory
|
|
|
73
79
|
result: {
|
|
74
80
|
protocolVersion: PROTOCOL_VERSION,
|
|
75
81
|
capabilities: {
|
|
76
|
-
tools: {}
|
|
82
|
+
tools: {},
|
|
83
|
+
prompts: {}
|
|
77
84
|
},
|
|
78
85
|
serverInfo: {
|
|
79
86
|
name: "claude-memory",
|
|
@@ -104,17 +111,40 @@ module ClaudeMemory
|
|
|
104
111
|
# Connections are automatically reopened on next use
|
|
105
112
|
release_connections
|
|
106
113
|
|
|
114
|
+
text_summary = TextSummary.for_tool(name, result)
|
|
115
|
+
|
|
107
116
|
{
|
|
108
117
|
jsonrpc: "2.0",
|
|
109
118
|
id: id,
|
|
110
119
|
result: {
|
|
111
120
|
content: [
|
|
112
|
-
{type: "text", text:
|
|
113
|
-
]
|
|
121
|
+
{type: "text", text: text_summary}
|
|
122
|
+
],
|
|
123
|
+
structuredContent: result
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def handle_prompts_list(id)
|
|
129
|
+
{
|
|
130
|
+
jsonrpc: "2.0",
|
|
131
|
+
id: id,
|
|
132
|
+
result: {
|
|
133
|
+
prompts: [QueryGuide.definition]
|
|
114
134
|
}
|
|
115
135
|
}
|
|
116
136
|
end
|
|
117
137
|
|
|
138
|
+
def handle_prompts_get(id, params)
|
|
139
|
+
name = params&.dig("name")
|
|
140
|
+
|
|
141
|
+
if name == QueryGuide::PROMPT_NAME
|
|
142
|
+
{jsonrpc: "2.0", id: id, result: QueryGuide.content}
|
|
143
|
+
else
|
|
144
|
+
{jsonrpc: "2.0", id: id, error: {code: -32602, message: "Unknown prompt: #{name}"}}
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
118
148
|
def release_connections
|
|
119
149
|
if @store_or_manager.is_a?(Store::StoreManager)
|
|
120
150
|
# Release both global and project store connections
|
|
@@ -124,7 +154,7 @@ module ClaudeMemory
|
|
|
124
154
|
# Release single store connection (legacy)
|
|
125
155
|
@store_or_manager.db.disconnect
|
|
126
156
|
end
|
|
127
|
-
rescue
|
|
157
|
+
rescue Sequel::DatabaseError, Extralite::Error
|
|
128
158
|
# Silently ignore disconnect errors
|
|
129
159
|
# Connection will be reopened automatically on next use
|
|
130
160
|
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module MCP
|
|
5
|
+
# Generates human-readable text summaries from MCP tool results.
|
|
6
|
+
# Used alongside structuredContent for dual content/structuredContent pattern.
|
|
7
|
+
module TextSummary
|
|
8
|
+
def self.for_tool(name, result)
|
|
9
|
+
return result[:error] if result[:error]
|
|
10
|
+
|
|
11
|
+
case name
|
|
12
|
+
when "memory.recall" then summarize_recall(result)
|
|
13
|
+
when "memory.recall_index" then summarize_recall_index(result)
|
|
14
|
+
when "memory.recall_details" then summarize_recall_details(result)
|
|
15
|
+
when "memory.explain" then summarize_explain(result)
|
|
16
|
+
when "memory.changes" then summarize_changes(result)
|
|
17
|
+
when "memory.conflicts" then summarize_conflicts(result)
|
|
18
|
+
when "memory.sweep_now" then summarize_sweep(result)
|
|
19
|
+
when "memory.status" then summarize_status(result)
|
|
20
|
+
when "memory.stats" then summarize_stats(result)
|
|
21
|
+
when "memory.promote" then summarize_promote(result)
|
|
22
|
+
when "memory.store_extraction" then summarize_extraction(result)
|
|
23
|
+
when "memory.decisions" then summarize_shortcut(result)
|
|
24
|
+
when "memory.conventions" then summarize_shortcut(result)
|
|
25
|
+
when "memory.architecture" then summarize_shortcut(result)
|
|
26
|
+
when "memory.facts_by_tool" then summarize_tool_facts(result)
|
|
27
|
+
when "memory.facts_by_context" then summarize_context_facts(result)
|
|
28
|
+
when "memory.recall_semantic" then summarize_semantic(result)
|
|
29
|
+
when "memory.search_concepts" then summarize_concepts(result)
|
|
30
|
+
when "memory.fact_graph" then summarize_fact_graph(result)
|
|
31
|
+
when "memory.check_setup" then summarize_check_setup(result)
|
|
32
|
+
else JSON.generate(result)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.summarize_recall(result)
|
|
37
|
+
facts = result[:facts] || []
|
|
38
|
+
return "No facts found." if facts.empty?
|
|
39
|
+
|
|
40
|
+
lines = ["Found #{facts.size} fact(s):"]
|
|
41
|
+
facts.each do |f|
|
|
42
|
+
lines << "- [#{fact_label(f)}] #{f[:subject]}.#{f[:predicate]} = #{f[:object]}"
|
|
43
|
+
end
|
|
44
|
+
lines.join("\n")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.summarize_recall_index(result)
|
|
48
|
+
facts = result[:facts] || []
|
|
49
|
+
return "No matching facts for '#{result[:query]}'." if facts.empty?
|
|
50
|
+
|
|
51
|
+
lines = ["#{result[:result_count]} fact(s) matching '#{result[:query]}' (~#{result[:total_estimated_tokens]} tokens):"]
|
|
52
|
+
facts.each do |f|
|
|
53
|
+
lines << "- [#{fact_label(f)}] #{f[:subject]}.#{f[:predicate]}: #{f[:object_preview]} (#{f[:tokens]}t)"
|
|
54
|
+
end
|
|
55
|
+
lines.join("\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.summarize_recall_details(result)
|
|
59
|
+
facts = result[:facts] || []
|
|
60
|
+
return "No facts found for given IDs." if facts.empty?
|
|
61
|
+
|
|
62
|
+
lines = ["#{result[:fact_count]} fact(s):"]
|
|
63
|
+
facts.each do |f|
|
|
64
|
+
fact = f[:fact]
|
|
65
|
+
lines << "- [#{fact_label(fact)}] #{fact[:subject]}.#{fact[:predicate]} = #{fact[:object]} (#{fact[:status]}, #{fact[:scope]})"
|
|
66
|
+
end
|
|
67
|
+
lines.join("\n")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.summarize_explain(result)
|
|
71
|
+
f = result[:fact]
|
|
72
|
+
lines = ["Fact [#{fact_label(f)}]: #{f[:subject]}.#{f[:predicate]} = #{f[:object]}"]
|
|
73
|
+
lines << "Status: #{f[:status]}, valid from: #{f[:valid_from_ago] || f[:valid_from] || "unknown"}"
|
|
74
|
+
lines << "Source: #{result[:source]}"
|
|
75
|
+
|
|
76
|
+
receipts = result[:receipts] || []
|
|
77
|
+
if receipts.any?
|
|
78
|
+
lines << "Evidence: #{receipts.map { |r| r[:quote] }.join("; ")}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
lines.join("\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.summarize_changes(result)
|
|
85
|
+
changes = result[:changes] || []
|
|
86
|
+
return "No changes since #{result[:since]}." if changes.empty?
|
|
87
|
+
|
|
88
|
+
lines = ["#{changes.size} change(s) since #{result[:since]}:"]
|
|
89
|
+
changes.each do |c|
|
|
90
|
+
ago = c[:created_ago] ? " (#{c[:created_ago]})" : ""
|
|
91
|
+
lines << "- [#{fact_label(c)}] #{c[:predicate]}: #{c[:object]} [#{c[:status]}]#{ago}"
|
|
92
|
+
end
|
|
93
|
+
lines.join("\n")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.summarize_conflicts(result)
|
|
97
|
+
return "No open conflicts." if result[:count] == 0
|
|
98
|
+
|
|
99
|
+
lines = ["#{result[:count]} conflict(s):"]
|
|
100
|
+
(result[:conflicts] || []).each do |c|
|
|
101
|
+
lines << "- Conflict ##{c[:id]}: fact #{c[:fact_a]} vs fact #{c[:fact_b]} (#{c[:status]})"
|
|
102
|
+
end
|
|
103
|
+
lines.join("\n")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.summarize_sweep(result)
|
|
107
|
+
"Sweep (#{result[:scope]}): #{result[:proposed_expired]} proposed expired, " \
|
|
108
|
+
"#{result[:disputed_expired]} disputed expired, " \
|
|
109
|
+
"#{result[:orphaned_deleted]} orphaned deleted, " \
|
|
110
|
+
"#{result[:content_pruned]} content pruned " \
|
|
111
|
+
"(#{result[:elapsed_seconds]}s)"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.summarize_status(result)
|
|
115
|
+
dbs = result[:databases] || {}
|
|
116
|
+
lines = ["Memory status:"]
|
|
117
|
+
dbs.each do |name, info|
|
|
118
|
+
lines << if info[:exists] == false
|
|
119
|
+
"- #{name}: not initialized"
|
|
120
|
+
else
|
|
121
|
+
"- #{name}: #{info[:facts_active]} active facts, #{info[:open_conflicts]} conflicts (schema v#{info[:schema_version]})"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
lines.join("\n")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.summarize_stats(result)
|
|
128
|
+
dbs = result[:databases] || {}
|
|
129
|
+
lines = ["Stats (scope: #{result[:scope]}):"]
|
|
130
|
+
dbs.each do |name, info|
|
|
131
|
+
if info[:exists] == false
|
|
132
|
+
lines << "- #{name}: not initialized"
|
|
133
|
+
else
|
|
134
|
+
facts = info[:facts] || {}
|
|
135
|
+
lines << "- #{name}: #{facts[:active]}/#{facts[:total]} active facts, #{info[:entities]&.dig(:total) || 0} entities"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
lines.join("\n")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.summarize_promote(result)
|
|
142
|
+
if result[:success]
|
|
143
|
+
"Fact #{result[:project_fact_id]} promoted to global (new ID: #{result[:global_fact_id]})"
|
|
144
|
+
else
|
|
145
|
+
result[:error]
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.summarize_extraction(result)
|
|
150
|
+
if result[:success]
|
|
151
|
+
"Stored: #{result[:facts_created]} facts, #{result[:entities_created]} entities, " \
|
|
152
|
+
"#{result[:facts_superseded]} superseded, #{result[:conflicts_created]} conflicts"
|
|
153
|
+
else
|
|
154
|
+
result[:error]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.summarize_shortcut(result)
|
|
159
|
+
facts = result[:facts] || []
|
|
160
|
+
return "No #{result[:category]} found." if facts.empty?
|
|
161
|
+
|
|
162
|
+
lines = ["#{result[:count]} #{result[:category]}:"]
|
|
163
|
+
facts.each do |f|
|
|
164
|
+
lines << "- [#{fact_label(f)}] #{f[:object]}"
|
|
165
|
+
end
|
|
166
|
+
lines.join("\n")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.summarize_tool_facts(result)
|
|
170
|
+
facts = result[:facts] || []
|
|
171
|
+
return "No facts discovered via #{result[:tool_name]}." if facts.empty?
|
|
172
|
+
|
|
173
|
+
lines = ["#{result[:count]} fact(s) from #{result[:tool_name]}:"]
|
|
174
|
+
facts.each do |f|
|
|
175
|
+
lines << "- [#{fact_label(f)}] #{f[:subject]}.#{f[:predicate]} = #{f[:object]}"
|
|
176
|
+
end
|
|
177
|
+
lines.join("\n")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def self.summarize_context_facts(result)
|
|
181
|
+
facts = result[:facts] || []
|
|
182
|
+
return "No facts for #{result[:context_type]}=#{result[:context_value]}." if facts.empty?
|
|
183
|
+
|
|
184
|
+
lines = ["#{result[:count]} fact(s) for #{result[:context_type]}=#{result[:context_value]}:"]
|
|
185
|
+
facts.each do |f|
|
|
186
|
+
lines << "- [#{fact_label(f)}] #{f[:subject]}.#{f[:predicate]} = #{f[:object]}"
|
|
187
|
+
end
|
|
188
|
+
lines.join("\n")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def self.summarize_semantic(result)
|
|
192
|
+
facts = result[:facts] || []
|
|
193
|
+
return "No semantic matches for '#{result[:query]}'." if facts.empty?
|
|
194
|
+
|
|
195
|
+
lines = ["#{result[:count]} match(es) for '#{result[:query]}' (#{result[:mode]}):"]
|
|
196
|
+
facts.each do |f|
|
|
197
|
+
sim = f[:similarity] ? " (#{(f[:similarity] * 100).round}%)" : ""
|
|
198
|
+
lines << "- [#{fact_label(f)}] #{f[:subject]}.#{f[:predicate]} = #{f[:object]}#{sim}"
|
|
199
|
+
end
|
|
200
|
+
lines.join("\n")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def self.summarize_concepts(result)
|
|
204
|
+
facts = result[:facts] || []
|
|
205
|
+
concepts_str = (result[:concepts] || []).join(" + ")
|
|
206
|
+
return "No facts matching all concepts: #{concepts_str}." if facts.empty?
|
|
207
|
+
|
|
208
|
+
lines = ["#{result[:count]} fact(s) matching #{concepts_str}:"]
|
|
209
|
+
facts.each do |f|
|
|
210
|
+
sim = f[:average_similarity] ? " (#{(f[:average_similarity] * 100).round}%)" : ""
|
|
211
|
+
lines << "- [#{fact_label(f)}] #{f[:subject]}.#{f[:predicate]} = #{f[:object]}#{sim}"
|
|
212
|
+
end
|
|
213
|
+
lines.join("\n")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def self.summarize_fact_graph(result)
|
|
217
|
+
nodes = result[:nodes] || []
|
|
218
|
+
edges = result[:edges] || []
|
|
219
|
+
return "Fact #{result[:root_fact_id]} not found." if nodes.empty?
|
|
220
|
+
|
|
221
|
+
lines = ["Graph for fact ##{result[:root_fact_id]} (depth #{result[:depth]}): #{result[:node_count]} nodes, #{result[:edge_count]} edges"]
|
|
222
|
+
nodes.each do |n|
|
|
223
|
+
label = n[:docid] || n[:id]
|
|
224
|
+
lines << "- [#{label}] #{n[:subject]}.#{n[:predicate]} = #{n[:object]} (#{n[:status]})"
|
|
225
|
+
end
|
|
226
|
+
if edges.any?
|
|
227
|
+
lines << "Edges:"
|
|
228
|
+
edges.each do |e|
|
|
229
|
+
lines << " #{e[:from]} --#{e[:type]}--> #{e[:to]}"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
lines.join("\n")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def self.summarize_check_setup(result)
|
|
236
|
+
lines = ["Setup status: #{result[:status]}"]
|
|
237
|
+
lines << "Version: #{result[:version][:current]} (latest: #{result[:version][:latest]})"
|
|
238
|
+
|
|
239
|
+
components = result[:components] || {}
|
|
240
|
+
lines << "Components: global_db=#{components[:global_database]}, project_db=#{components[:project_database]}, hooks=#{components[:hooks_configured]}"
|
|
241
|
+
|
|
242
|
+
issues = result[:issues] || []
|
|
243
|
+
issues.each { |i| lines << "Issue: #{i}" }
|
|
244
|
+
|
|
245
|
+
warnings = result[:warnings] || []
|
|
246
|
+
warnings.each { |w| lines << "Warning: #{w}" }
|
|
247
|
+
|
|
248
|
+
lines.join("\n")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Format fact identifier: prefer docid if available, fall back to integer id
|
|
252
|
+
def self.fact_label(fact)
|
|
253
|
+
fact[:docid] || fact[:id]
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -17,7 +17,8 @@ module ClaudeMemory
|
|
|
17
17
|
properties: {
|
|
18
18
|
query: {type: "string", description: "Search query for existing knowledge (e.g., 'authentication flow', 'error handling', 'database setup')"},
|
|
19
19
|
limit: {type: "integer", description: "Max results", default: 10},
|
|
20
|
-
scope: {type: "string", enum: ["all", "global", "project"], description: "Filter by scope: 'all' (default), 'global', or 'project'", default: "all"}
|
|
20
|
+
scope: {type: "string", enum: ["all", "global", "project"], description: "Filter by scope: 'all' (default), 'global', or 'project'", default: "all"},
|
|
21
|
+
compact: {type: "boolean", description: "Omit provenance receipts for ~60% smaller responses", default: false}
|
|
21
22
|
},
|
|
22
23
|
required: ["query"]
|
|
23
24
|
}
|
|
@@ -53,7 +54,7 @@ module ClaudeMemory
|
|
|
53
54
|
inputSchema: {
|
|
54
55
|
type: "object",
|
|
55
56
|
properties: {
|
|
56
|
-
fact_id: {
|
|
57
|
+
fact_id: {description: "Fact ID (integer) or docid (8-char hex string) to explain"},
|
|
57
58
|
scope: {type: "string", enum: ["global", "project"], description: "Which database to look in", default: "project"}
|
|
58
59
|
},
|
|
59
60
|
required: ["fact_id"]
|
|
@@ -240,7 +241,8 @@ module ClaudeMemory
|
|
|
240
241
|
query: {type: "string", description: "Search query"},
|
|
241
242
|
mode: {type: "string", enum: ["vector", "text", "both"], default: "both", description: "Search mode: vector (embeddings), text (FTS), or both (hybrid)"},
|
|
242
243
|
limit: {type: "integer", default: 10, description: "Maximum results to return"},
|
|
243
|
-
scope: {type: "string", enum: ["all", "global", "project"], default: "all", description: "Filter by scope"}
|
|
244
|
+
scope: {type: "string", enum: ["all", "global", "project"], default: "all", description: "Filter by scope"},
|
|
245
|
+
compact: {type: "boolean", description: "Omit provenance receipts for ~60% smaller responses", default: false}
|
|
244
246
|
},
|
|
245
247
|
required: ["query"]
|
|
246
248
|
}
|
|
@@ -259,11 +261,25 @@ module ClaudeMemory
|
|
|
259
261
|
description: "2-5 concepts that must all be present"
|
|
260
262
|
},
|
|
261
263
|
limit: {type: "integer", default: 10, description: "Maximum results to return"},
|
|
262
|
-
scope: {type: "string", enum: ["all", "global", "project"], default: "all", description: "Filter by scope"}
|
|
264
|
+
scope: {type: "string", enum: ["all", "global", "project"], default: "all", description: "Filter by scope"},
|
|
265
|
+
compact: {type: "boolean", description: "Omit provenance receipts for ~60% smaller responses", default: false}
|
|
263
266
|
},
|
|
264
267
|
required: ["concepts"]
|
|
265
268
|
}
|
|
266
269
|
},
|
|
270
|
+
{
|
|
271
|
+
name: "memory.fact_graph",
|
|
272
|
+
description: "Build a dependency graph showing how facts relate through supersession and conflict links. Returns nodes (facts) and edges (supersedes/conflicts).",
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: "object",
|
|
275
|
+
properties: {
|
|
276
|
+
fact_id: {type: "integer", description: "Root fact ID to start traversal from"},
|
|
277
|
+
depth: {type: "integer", description: "Maximum BFS traversal depth (1-5)", default: 2},
|
|
278
|
+
scope: {type: "string", enum: ["global", "project"], description: "Which database to search", default: "project"}
|
|
279
|
+
},
|
|
280
|
+
required: ["fact_id"]
|
|
281
|
+
}
|
|
282
|
+
},
|
|
267
283
|
{
|
|
268
284
|
name: "memory.check_setup",
|
|
269
285
|
description: "Check ClaudeMemory initialization status. Returns version info, issues found, and recommendations.",
|