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
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Handlers
|
|
6
|
+
# Status and statistics tool handlers
|
|
7
|
+
module StatsHandlers
|
|
8
|
+
def status
|
|
9
|
+
result = {databases: {}}
|
|
10
|
+
|
|
11
|
+
if @manager
|
|
12
|
+
if @manager.global_exists?
|
|
13
|
+
@manager.ensure_global!
|
|
14
|
+
result[:databases][:global] = db_stats(@manager.global_store)
|
|
15
|
+
else
|
|
16
|
+
result[:databases][:global] = {exists: false}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if @manager.project_exists?
|
|
20
|
+
@manager.ensure_project!
|
|
21
|
+
result[:databases][:project] = db_stats(@manager.project_store)
|
|
22
|
+
else
|
|
23
|
+
result[:databases][:project] = {exists: false}
|
|
24
|
+
end
|
|
25
|
+
else
|
|
26
|
+
result[:databases][:legacy] = db_stats(@legacy_store)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
result[:pending_distillation] = pending_distillation_count
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stats(args)
|
|
34
|
+
scope = args["scope"] || "all"
|
|
35
|
+
result = {scope: scope, databases: {}}
|
|
36
|
+
|
|
37
|
+
if @manager
|
|
38
|
+
if scope == "all" || scope == "global"
|
|
39
|
+
if @manager.global_exists?
|
|
40
|
+
@manager.ensure_global!
|
|
41
|
+
result[:databases][:global] = detailed_stats(@manager.global_store)
|
|
42
|
+
else
|
|
43
|
+
result[:databases][:global] = {exists: false}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if scope == "all" || scope == "project"
|
|
48
|
+
if @manager.project_exists?
|
|
49
|
+
@manager.ensure_project!
|
|
50
|
+
result[:databases][:project] = detailed_stats(@manager.project_store)
|
|
51
|
+
else
|
|
52
|
+
result[:databases][:project] = {exists: false}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
else
|
|
56
|
+
result[:databases][:legacy] = detailed_stats(@legacy_store)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def pending_distillation_count
|
|
65
|
+
stores = if @manager
|
|
66
|
+
[@manager.global_exists? ? @manager.global_store : nil,
|
|
67
|
+
@manager.project_exists? ? @manager.project_store : nil].compact
|
|
68
|
+
elsif @legacy_store
|
|
69
|
+
[@legacy_store]
|
|
70
|
+
else
|
|
71
|
+
[]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
stores.sum { |store| store.count_undistilled(min_length: 200) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def db_stats(store)
|
|
78
|
+
stats = {
|
|
79
|
+
exists: true,
|
|
80
|
+
facts_total: store.facts.count,
|
|
81
|
+
facts_active: store.facts.where(status: "active").count,
|
|
82
|
+
content_items: store.content_items.count,
|
|
83
|
+
open_conflicts: store.conflicts.where(status: "open").count,
|
|
84
|
+
schema_version: store.schema_version
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
vec_index = store.vector_index
|
|
88
|
+
stats[:vec_available] = vec_index.available?
|
|
89
|
+
stats[:vec_indexed] = vec_index.coverage_stats[:vec_indexed] if vec_index.available?
|
|
90
|
+
|
|
91
|
+
if fts_legacy?(store)
|
|
92
|
+
stats[:fts_legacy] = true
|
|
93
|
+
stats[:optimization_hint] = "Run 'claude-memory compact' to reduce database size by ~40%"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
stats
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def fts_legacy?(store)
|
|
100
|
+
row = store.db.fetch("SELECT sql FROM sqlite_master WHERE name = 'content_fts' AND type = 'table'").first
|
|
101
|
+
row && !row[:sql].to_s.include?("content=''")
|
|
102
|
+
rescue
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def detailed_stats(store)
|
|
107
|
+
active_facts = store.facts.where(status: "active").count
|
|
108
|
+
|
|
109
|
+
stats = {
|
|
110
|
+
exists: true,
|
|
111
|
+
facts: fact_stats(store, active_facts),
|
|
112
|
+
entities: entity_stats(store),
|
|
113
|
+
content_items: content_stats(store),
|
|
114
|
+
provenance: provenance_stats(store, active_facts),
|
|
115
|
+
conflicts: conflict_stats(store),
|
|
116
|
+
schema_version: store.schema_version
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
stats[:vec] = vec_stats(store, active_facts)
|
|
120
|
+
|
|
121
|
+
stats
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def fact_stats(store, active_facts)
|
|
125
|
+
stats = {
|
|
126
|
+
total: store.facts.count,
|
|
127
|
+
active: active_facts,
|
|
128
|
+
superseded: store.facts.where(status: "superseded").count
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if active_facts > 0
|
|
132
|
+
all_predicates = store.db[:facts]
|
|
133
|
+
.where(status: "active")
|
|
134
|
+
.group_and_count(:predicate)
|
|
135
|
+
.order(Sequel.desc(:count))
|
|
136
|
+
.all
|
|
137
|
+
.map { |row| {predicate: row[:predicate], count: row[:count]} }
|
|
138
|
+
|
|
139
|
+
stats[:top_predicates] = all_predicates.first(10)
|
|
140
|
+
stats[:predicates_known], stats[:predicates_novel] =
|
|
141
|
+
all_predicates.partition { |row| Resolve::PredicatePolicy.known_predicates.include?(row[:predicate]) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
stats
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def entity_stats(store)
|
|
148
|
+
{
|
|
149
|
+
total: store.entities.count,
|
|
150
|
+
by_type: store.db[:entities]
|
|
151
|
+
.group_and_count(:type)
|
|
152
|
+
.order(Sequel.desc(:count))
|
|
153
|
+
.all
|
|
154
|
+
.map { |row| {type: row[:type], count: row[:count]} }
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def content_stats(store)
|
|
159
|
+
count = store.content_items.count
|
|
160
|
+
stats = {total: count}
|
|
161
|
+
|
|
162
|
+
if count > 0
|
|
163
|
+
stats[:date_range] = {
|
|
164
|
+
first: store.content_items.min(:occurred_at),
|
|
165
|
+
last: store.content_items.max(:occurred_at)
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
stats
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def provenance_stats(store, active_facts)
|
|
173
|
+
return {facts_with_sources: 0, total_active_facts: 0, coverage_percentage: 0} if active_facts == 0
|
|
174
|
+
|
|
175
|
+
facts_with_provenance = store.db[:provenance]
|
|
176
|
+
.join(:facts, id: :fact_id)
|
|
177
|
+
.where(Sequel[:facts][:status] => "active")
|
|
178
|
+
.select(Sequel[:provenance][:fact_id])
|
|
179
|
+
.distinct
|
|
180
|
+
.count
|
|
181
|
+
|
|
182
|
+
{
|
|
183
|
+
facts_with_sources: facts_with_provenance,
|
|
184
|
+
total_active_facts: active_facts,
|
|
185
|
+
coverage_percentage: (facts_with_provenance * 100.0 / active_facts).round(1)
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def vec_stats(store, _active_facts)
|
|
190
|
+
vec_index = store.vector_index
|
|
191
|
+
result = {available: vec_index.available?}
|
|
192
|
+
result.merge!(vec_index.coverage_stats) if vec_index.available?
|
|
193
|
+
result
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def conflict_stats(store)
|
|
197
|
+
open = store.conflicts.where(status: "open").count
|
|
198
|
+
resolved = store.conflicts.where(status: "resolved").count
|
|
199
|
+
|
|
200
|
+
{open: open, resolved: resolved, total: open + resolved}
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -26,8 +26,9 @@ module ClaudeMemory
|
|
|
26
26
|
|
|
27
27
|
parts << usage_hint(store_or_manager)
|
|
28
28
|
parts.compact.join("\n\n")
|
|
29
|
-
rescue =>
|
|
29
|
+
rescue => e
|
|
30
30
|
# Never fail initialization — return minimal instructions
|
|
31
|
+
ClaudeMemory.logger.debug("InstructionsBuilder failed: #{e.message}")
|
|
31
32
|
"ClaudeMemory v#{ClaudeMemory::VERSION} — long-term memory for Claude Code."
|
|
32
33
|
end
|
|
33
34
|
|
|
@@ -107,9 +108,26 @@ module ClaudeMemory
|
|
|
107
108
|
|
|
108
109
|
escalation = vec ? "recall_semantic, explain, or fact_graph" : "explain or fact_graph"
|
|
109
110
|
lines << "Start with fast tools (recall, decisions, conventions) before escalating to #{escalation}."
|
|
111
|
+
|
|
112
|
+
lines << proactive_recall_guidance
|
|
110
113
|
lines.join("\n")
|
|
111
114
|
end
|
|
112
115
|
|
|
116
|
+
# Directive guidance for when Claude should proactively consult memory.
|
|
117
|
+
# Validated by A/B testing: without these directives, Claude writes code
|
|
118
|
+
# using known-dangerous patterns (e.g. Sequel.sqlite) and hallucinates
|
|
119
|
+
# file paths instead of consulting memory for the correct structure.
|
|
120
|
+
def proactive_recall_guidance
|
|
121
|
+
<<~GUIDANCE.strip
|
|
122
|
+
IMPORTANT — check memory proactively in these situations:
|
|
123
|
+
- Before writing code: call memory.conventions to verify project patterns and avoid known gotchas
|
|
124
|
+
- Before explaining architecture: call memory.architecture for structural knowledge without file traversal
|
|
125
|
+
- Before refactoring: call memory.decisions to understand why past choices were made
|
|
126
|
+
- When asked about preferences: global facts store user environment and style preferences across all projects
|
|
127
|
+
- When adding to the codebase: recall which files and patterns to follow (memory knows correct paths and relationships)
|
|
128
|
+
GUIDANCE
|
|
129
|
+
end
|
|
130
|
+
|
|
113
131
|
def count_by_predicates(store, predicates)
|
|
114
132
|
store.facts
|
|
115
133
|
.where(status: "active")
|
|
@@ -66,6 +66,16 @@ module ClaudeMemory
|
|
|
66
66
|
- Use when: you need facts from a specific workflow context
|
|
67
67
|
- Cost: ~300-800 tokens per call
|
|
68
68
|
|
|
69
|
+
### Tier 5: Distillation Management
|
|
70
|
+
|
|
71
|
+
**memory.undistilled** — List content items not yet deeply distilled
|
|
72
|
+
- Use when: you want to find ingested content that hasn't been processed by LLM extraction
|
|
73
|
+
- Cost: ~200-400 tokens per call
|
|
74
|
+
|
|
75
|
+
**memory.mark_distilled** — Mark a content item as distilled after extraction
|
|
76
|
+
- Use after: performing LLM-based fact extraction on undistilled content
|
|
77
|
+
- Cost: ~100 tokens per call
|
|
78
|
+
|
|
69
79
|
## Recommended Workflow
|
|
70
80
|
|
|
71
81
|
1. **Start broad**: `memory.recall` or shortcut tools (decisions/conventions/architecture)
|
|
@@ -278,6 +278,7 @@ module ClaudeMemory
|
|
|
278
278
|
source: result[:source],
|
|
279
279
|
similarity: result[:similarity]
|
|
280
280
|
}
|
|
281
|
+
fact[:score_trace] = result[:score_trace] if result[:score_trace]
|
|
281
282
|
fact[:receipts] = result[:receipts].map { |r| format_receipt(r, query: query) } unless compact
|
|
282
283
|
fact
|
|
283
284
|
end
|
|
@@ -5,20 +5,30 @@ require_relative "instructions_builder"
|
|
|
5
5
|
require_relative "query_guide"
|
|
6
6
|
require_relative "text_summary"
|
|
7
7
|
require_relative "error_classifier"
|
|
8
|
+
require_relative "telemetry"
|
|
8
9
|
|
|
9
10
|
module ClaudeMemory
|
|
10
11
|
module MCP
|
|
12
|
+
# MCP JSON-RPC server over stdio.
|
|
13
|
+
# Reads newline-delimited JSON requests from input, dispatches to Tools,
|
|
14
|
+
# and writes JSON responses to output.
|
|
11
15
|
class Server
|
|
12
16
|
PROTOCOL_VERSION = "2024-11-05"
|
|
13
17
|
|
|
18
|
+
# @param store_or_manager [Store::SQLiteStore, Store::StoreManager] database backend
|
|
19
|
+
# @param input [IO] input stream for JSON-RPC requests (default: $stdin)
|
|
20
|
+
# @param output [IO] output stream for JSON-RPC responses (default: $stdout)
|
|
14
21
|
def initialize(store_or_manager, input: $stdin, output: $stdout)
|
|
15
22
|
@store_or_manager = store_or_manager
|
|
16
23
|
@tools = Tools.new(store_or_manager)
|
|
24
|
+
@telemetry = Telemetry.new(store_or_manager)
|
|
17
25
|
@input = input
|
|
18
26
|
@output = output
|
|
19
27
|
@running = false
|
|
20
28
|
end
|
|
21
29
|
|
|
30
|
+
# Start the read loop, blocking until input is exhausted or stop is called.
|
|
31
|
+
# @return [void]
|
|
22
32
|
def run
|
|
23
33
|
@running = true
|
|
24
34
|
while @running
|
|
@@ -29,12 +39,15 @@ module ClaudeMemory
|
|
|
29
39
|
end
|
|
30
40
|
end
|
|
31
41
|
|
|
42
|
+
# Signal the read loop to exit after the current message.
|
|
43
|
+
# @return [void]
|
|
32
44
|
def stop
|
|
33
45
|
@running = false
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
private
|
|
37
49
|
|
|
50
|
+
# @return [void]
|
|
38
51
|
def handle_message(line)
|
|
39
52
|
return if line.empty?
|
|
40
53
|
|
|
@@ -51,6 +64,7 @@ module ClaudeMemory
|
|
|
51
64
|
end
|
|
52
65
|
end
|
|
53
66
|
|
|
67
|
+
# @return [Hash, nil] JSON-RPC response hash, or nil for notifications
|
|
54
68
|
def process_request(request)
|
|
55
69
|
id = request["id"]
|
|
56
70
|
method = request["method"]
|
|
@@ -74,6 +88,7 @@ module ClaudeMemory
|
|
|
74
88
|
end
|
|
75
89
|
end
|
|
76
90
|
|
|
91
|
+
# @return [Hash] initialize response with capabilities and server info
|
|
77
92
|
def handle_initialize(id, _params)
|
|
78
93
|
{
|
|
79
94
|
jsonrpc: "2.0",
|
|
@@ -93,6 +108,7 @@ module ClaudeMemory
|
|
|
93
108
|
}
|
|
94
109
|
end
|
|
95
110
|
|
|
111
|
+
# @return [Hash] list of available tool definitions
|
|
96
112
|
def handle_tools_list(id)
|
|
97
113
|
{
|
|
98
114
|
jsonrpc: "2.0",
|
|
@@ -103,11 +119,14 @@ module ClaudeMemory
|
|
|
103
119
|
}
|
|
104
120
|
end
|
|
105
121
|
|
|
122
|
+
# @return [Hash] tool result with dual content/structuredContent
|
|
106
123
|
def handle_tools_call(id, params)
|
|
107
124
|
name = params["name"]
|
|
108
125
|
arguments = params["arguments"] || {}
|
|
109
126
|
|
|
110
|
-
result = @
|
|
127
|
+
result = @telemetry.record(name, arguments) do
|
|
128
|
+
@tools.call(name, arguments)
|
|
129
|
+
end
|
|
111
130
|
|
|
112
131
|
# Release database connections after each tool call
|
|
113
132
|
# This prevents lock contention with hook commands
|
|
@@ -128,6 +147,7 @@ module ClaudeMemory
|
|
|
128
147
|
}
|
|
129
148
|
end
|
|
130
149
|
|
|
150
|
+
# @return [Hash] list of available prompt definitions
|
|
131
151
|
def handle_prompts_list(id)
|
|
132
152
|
{
|
|
133
153
|
jsonrpc: "2.0",
|
|
@@ -138,6 +158,7 @@ module ClaudeMemory
|
|
|
138
158
|
}
|
|
139
159
|
end
|
|
140
160
|
|
|
161
|
+
# @return [Hash] prompt content or error if unknown
|
|
141
162
|
def handle_prompts_get(id, params)
|
|
142
163
|
name = params&.dig("name")
|
|
143
164
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module MCP
|
|
5
|
+
# Records MCP tool invocations into the project database for usage stats.
|
|
6
|
+
# Timing and error capture wrap the tool call; the insert is synchronous
|
|
7
|
+
# and best-effort — telemetry failures are swallowed so they never break
|
|
8
|
+
# a real tool response.
|
|
9
|
+
class Telemetry
|
|
10
|
+
def initialize(store_or_manager)
|
|
11
|
+
@store_or_manager = store_or_manager
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Time a tool invocation and record the outcome. Yields to the caller
|
|
15
|
+
# and returns whatever the block returns; re-raises any exception after
|
|
16
|
+
# recording it as an error.
|
|
17
|
+
def record(tool_name, arguments)
|
|
18
|
+
started = monotonic_ms
|
|
19
|
+
begin
|
|
20
|
+
result = yield
|
|
21
|
+
rescue => e
|
|
22
|
+
duration = monotonic_ms - started
|
|
23
|
+
write(
|
|
24
|
+
tool_name: tool_name,
|
|
25
|
+
duration_ms: duration,
|
|
26
|
+
result_count: nil,
|
|
27
|
+
scope: extract_scope(arguments),
|
|
28
|
+
error_class: e.class.name
|
|
29
|
+
)
|
|
30
|
+
raise
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
duration = monotonic_ms - started
|
|
34
|
+
write(
|
|
35
|
+
tool_name: tool_name,
|
|
36
|
+
duration_ms: duration,
|
|
37
|
+
result_count: extract_result_count(result),
|
|
38
|
+
scope: extract_scope(arguments),
|
|
39
|
+
error_class: nil
|
|
40
|
+
)
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def monotonic_ms
|
|
47
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def write(**row)
|
|
51
|
+
store = writable_store
|
|
52
|
+
return unless store
|
|
53
|
+
store.insert_mcp_tool_call(**row)
|
|
54
|
+
rescue Sequel::DatabaseError, Extralite::Error
|
|
55
|
+
# Telemetry is best-effort; never fail the user's tool call
|
|
56
|
+
# because stats couldn't be written.
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def writable_store
|
|
60
|
+
if @store_or_manager.is_a?(Store::StoreManager)
|
|
61
|
+
@store_or_manager.ensure_project!
|
|
62
|
+
elsif @store_or_manager.respond_to?(:insert_mcp_tool_call)
|
|
63
|
+
@store_or_manager
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def extract_scope(arguments)
|
|
68
|
+
return nil unless arguments.is_a?(Hash)
|
|
69
|
+
arguments["scope"] || arguments[:scope]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Inspect a tool result for a countable field. Most query tools
|
|
73
|
+
# return hashes with :facts, :results, :conflicts, or :changes;
|
|
74
|
+
# fall back to nil for shapes we don't recognize.
|
|
75
|
+
def extract_result_count(result)
|
|
76
|
+
return nil unless result.is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
%i[facts results conflicts changes entities items].each do |key|
|
|
79
|
+
value = result[key] || result[key.to_s]
|
|
80
|
+
return value.size if value.is_a?(Array)
|
|
81
|
+
end
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -28,6 +28,8 @@ module ClaudeMemory
|
|
|
28
28
|
when "memory.recall_semantic" then summarize_semantic(result)
|
|
29
29
|
when "memory.search_concepts" then summarize_concepts(result)
|
|
30
30
|
when "memory.fact_graph" then summarize_fact_graph(result)
|
|
31
|
+
when "memory.undistilled" then summarize_undistilled(result)
|
|
32
|
+
when "memory.mark_distilled" then summarize_mark_distilled(result)
|
|
31
33
|
when "memory.check_setup" then summarize_check_setup(result)
|
|
32
34
|
else JSON.generate(result)
|
|
33
35
|
end
|
|
@@ -122,6 +124,10 @@ module ClaudeMemory
|
|
|
122
124
|
"- #{name}: #{info[:facts_active]} active facts, #{info[:open_conflicts]} conflicts (schema v#{info[:schema_version]})"
|
|
123
125
|
end
|
|
124
126
|
end
|
|
127
|
+
|
|
128
|
+
pending = result[:pending_distillation] || 0
|
|
129
|
+
lines << "Pending distillation: #{pending}" if pending > 0
|
|
130
|
+
|
|
125
131
|
lines.join("\n")
|
|
126
132
|
end
|
|
127
133
|
|
|
@@ -233,6 +239,26 @@ module ClaudeMemory
|
|
|
233
239
|
lines.join("\n")
|
|
234
240
|
end
|
|
235
241
|
|
|
242
|
+
def self.summarize_undistilled(result)
|
|
243
|
+
items = result[:items] || []
|
|
244
|
+
return "No undistilled content items." if items.empty?
|
|
245
|
+
|
|
246
|
+
lines = ["#{result[:count]} undistilled content item(s):"]
|
|
247
|
+
items.each do |i|
|
|
248
|
+
ago = i[:occurred_ago] || "unknown"
|
|
249
|
+
lines << "- Item ##{i[:content_item_id]} (#{ago}): #{(i[:raw_text] || "")[0, 80]}..."
|
|
250
|
+
end
|
|
251
|
+
lines.join("\n")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def self.summarize_mark_distilled(result)
|
|
255
|
+
if result[:success]
|
|
256
|
+
"Marked content item ##{result[:content_item_id]} as distilled (#{result[:facts_extracted]} facts extracted)"
|
|
257
|
+
else
|
|
258
|
+
result[:error]
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
236
262
|
def self.summarize_check_setup(result)
|
|
237
263
|
lines = ["Setup status: #{result[:status]}"]
|
|
238
264
|
lines << "Version: #{result[:version][:current]} (latest: #{result[:version][:latest]})"
|