claude_memory 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/memory.sqlite3-shm +0 -0
- data/.claude/memory.sqlite3-wal +0 -0
- data/.claude/settings.json +78 -6
- data/.claude/settings.local.json +2 -1
- data/.claude/skills/improve/SKILL.md +113 -25
- data/.claude-plugin/commands/distill-transcripts.md +98 -0
- data/.claude-plugin/commands/memory-recall.md +67 -0
- data/.claude-plugin/marketplace.json +1 -1
- data/.claude-plugin/plugin.json +1 -1
- data/CHANGELOG.md +49 -1
- data/CLAUDE.md +29 -5
- data/docs/improvements.md +18 -56
- data/docs/quality_review.md +119 -224
- data/hooks/hooks.json +39 -7
- data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
- data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
- data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
- data/lib/claude_memory/commands/completion_command.rb +179 -0
- data/lib/claude_memory/commands/doctor_command.rb +2 -0
- data/lib/claude_memory/commands/help_command.rb +4 -0
- data/lib/claude_memory/commands/hook_command.rb +2 -1
- data/lib/claude_memory/commands/index_command.rb +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 +3 -1
- data/lib/claude_memory/commands/skills/distill-transcripts.md +98 -0
- data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
- data/lib/claude_memory/core/fact_ranker.rb +2 -2
- data/lib/claude_memory/core/rr_fusion.rb +23 -6
- data/lib/claude_memory/core/snippet_extractor.rb +7 -3
- data/lib/claude_memory/core/text_builder.rb +11 -0
- data/lib/claude_memory/domain/provenance.rb +0 -1
- data/lib/claude_memory/embeddings/api_adapter.rb +96 -0
- data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
- data/lib/claude_memory/embeddings/fastembed_adapter.rb +4 -0
- data/lib/claude_memory/embeddings/generator.rb +4 -0
- data/lib/claude_memory/embeddings/resolver.rb +18 -0
- data/lib/claude_memory/hook/context_injector.rb +58 -2
- data/lib/claude_memory/hook/distillation_runner.rb +46 -0
- data/lib/claude_memory/hook/handler.rb +11 -2
- data/lib/claude_memory/index/vector_index.rb +15 -2
- data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
- data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
- data/lib/claude_memory/mcp/handlers/management_handlers.rb +145 -0
- data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
- data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
- data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
- data/lib/claude_memory/mcp/handlers/stats_handlers.rb +202 -0
- data/lib/claude_memory/mcp/instructions_builder.rb +2 -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/text_summary.rb +26 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +30 -1
- data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
- data/lib/claude_memory/mcp/tools.rb +39 -678
- data/lib/claude_memory/recall/dual_engine.rb +105 -0
- data/lib/claude_memory/recall/legacy_engine.rb +138 -0
- data/lib/claude_memory/recall/query_core.rb +371 -0
- data/lib/claude_memory/recall.rb +29 -662
- data/lib/claude_memory/shortcuts.rb +4 -4
- data/lib/claude_memory/store/retry_handler.rb +61 -0
- data/lib/claude_memory/store/schema_manager.rb +68 -0
- data/lib/claude_memory/store/sqlite_store.rb +85 -201
- data/lib/claude_memory/templates/hooks.example.json +26 -7
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +11 -0
- metadata +23 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Handlers
|
|
6
|
+
# Setup and discovery tool handlers
|
|
7
|
+
module SetupHandlers
|
|
8
|
+
def check_setup
|
|
9
|
+
issues = []
|
|
10
|
+
warnings = []
|
|
11
|
+
config = Configuration.new
|
|
12
|
+
|
|
13
|
+
global_db_exists = check_global_database(config, issues)
|
|
14
|
+
project_db_exists = check_project_database(config, warnings)
|
|
15
|
+
current_version, version_status, claude_md_exists = check_claude_md_version(warnings)
|
|
16
|
+
hooks_configured = check_hooks_configuration(warnings)
|
|
17
|
+
|
|
18
|
+
build_setup_result(
|
|
19
|
+
global_db_exists, project_db_exists, claude_md_exists,
|
|
20
|
+
hooks_configured, current_version, version_status,
|
|
21
|
+
issues, warnings
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def list_projects
|
|
26
|
+
result = {global: nil, current_project: nil, other_projects: []}
|
|
27
|
+
|
|
28
|
+
if @manager
|
|
29
|
+
result[:global] = list_global_database
|
|
30
|
+
result[:current_project] = list_current_project
|
|
31
|
+
result[:other_projects] = discover_other_projects
|
|
32
|
+
elsif @legacy_store
|
|
33
|
+
result[:global] = {
|
|
34
|
+
exists: true,
|
|
35
|
+
path: @legacy_store.db.opts[:database],
|
|
36
|
+
facts_active: @legacy_store.facts.where(status: "active").count,
|
|
37
|
+
entities: @legacy_store.entities.count
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
result[:project_count] = 1 + result[:other_projects].size
|
|
42
|
+
result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def check_global_database(config, issues)
|
|
48
|
+
exists = File.exist?(config.global_db_path)
|
|
49
|
+
issues << "Global database not found at #{config.global_db_path}" unless exists
|
|
50
|
+
exists
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def check_project_database(config, warnings)
|
|
54
|
+
exists = File.exist?(config.project_db_path)
|
|
55
|
+
warnings << "Project database not found at #{config.project_db_path}" unless exists
|
|
56
|
+
exists
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check_claude_md_version(warnings)
|
|
60
|
+
claude_md_path = ".claude/CLAUDE.md"
|
|
61
|
+
unless File.exist?(claude_md_path)
|
|
62
|
+
warnings << "No .claude/CLAUDE.md found"
|
|
63
|
+
return [nil, nil, false]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
content = File.read(claude_md_path)
|
|
67
|
+
unless content.include?("ClaudeMemory")
|
|
68
|
+
warnings << "CLAUDE.md exists but no ClaudeMemory configuration found"
|
|
69
|
+
return [nil, nil, true]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
current_version = SetupStatusAnalyzer.extract_version(content)
|
|
73
|
+
unless current_version
|
|
74
|
+
warnings << "CLAUDE.md has ClaudeMemory section but no version marker"
|
|
75
|
+
return [nil, "no_version_marker", true]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
version_status = SetupStatusAnalyzer.determine_version_status(current_version, ClaudeMemory::VERSION)
|
|
79
|
+
if version_status == "outdated"
|
|
80
|
+
warnings << "Configuration version (v#{current_version}) is older than ClaudeMemory (v#{ClaudeMemory::VERSION}). Consider running upgrade."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
[current_version, version_status, true]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def check_hooks_configuration(warnings)
|
|
87
|
+
settings_paths = [".claude/settings.json", ".claude/settings.local.json"]
|
|
88
|
+
settings_paths.each do |path|
|
|
89
|
+
next unless File.exist?(path)
|
|
90
|
+
begin
|
|
91
|
+
config_data = JSON.parse(File.read(path))
|
|
92
|
+
return true if config_data["hooks"]&.any?
|
|
93
|
+
rescue JSON::ParserError
|
|
94
|
+
warnings << "Invalid JSON in #{path}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
warnings << "No hooks configured for automatic ingestion"
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_setup_result(global_db_exists, project_db_exists, claude_md_exists, hooks_configured, current_version, version_status, issues, warnings)
|
|
103
|
+
initialized = global_db_exists && claude_md_exists
|
|
104
|
+
status = SetupStatusAnalyzer.determine_status(global_db_exists, claude_md_exists, version_status)
|
|
105
|
+
recommendations = SetupStatusAnalyzer.generate_recommendations(initialized, version_status, warnings.any?)
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
status: status,
|
|
109
|
+
initialized: initialized,
|
|
110
|
+
version: {
|
|
111
|
+
current: current_version || "unknown",
|
|
112
|
+
latest: ClaudeMemory::VERSION,
|
|
113
|
+
status: version_status || "unknown"
|
|
114
|
+
},
|
|
115
|
+
components: {
|
|
116
|
+
global_database: global_db_exists,
|
|
117
|
+
project_database: project_db_exists,
|
|
118
|
+
claude_md: claude_md_exists,
|
|
119
|
+
hooks_configured: hooks_configured
|
|
120
|
+
},
|
|
121
|
+
issues: issues,
|
|
122
|
+
warnings: warnings,
|
|
123
|
+
recommendations: recommendations
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def list_global_database
|
|
128
|
+
if @manager.global_exists?
|
|
129
|
+
@manager.ensure_global!
|
|
130
|
+
store = @manager.global_store
|
|
131
|
+
{
|
|
132
|
+
exists: true,
|
|
133
|
+
path: @manager.global_db_path,
|
|
134
|
+
facts_active: store.facts.where(status: "active").count,
|
|
135
|
+
facts_total: store.facts.count,
|
|
136
|
+
entities: store.entities.count
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
{exists: false, path: @manager.global_db_path}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def list_current_project
|
|
144
|
+
if @manager.project_exists?
|
|
145
|
+
@manager.ensure_project!
|
|
146
|
+
store = @manager.project_store
|
|
147
|
+
{
|
|
148
|
+
exists: true,
|
|
149
|
+
path: @manager.project_path,
|
|
150
|
+
db_path: @manager.project_db_path,
|
|
151
|
+
facts_active: store.facts.where(status: "active").count,
|
|
152
|
+
facts_total: store.facts.count,
|
|
153
|
+
entities: store.entities.count
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
{exists: false, path: @manager.project_path, db_path: @manager.project_db_path}
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def discover_other_projects
|
|
161
|
+
return [] unless @manager.global_exists?
|
|
162
|
+
|
|
163
|
+
@manager.ensure_global!
|
|
164
|
+
global = @manager.global_store
|
|
165
|
+
|
|
166
|
+
promoted_paths = global.facts
|
|
167
|
+
.where(Sequel.like(:created_from, "promoted:%"))
|
|
168
|
+
.select(:created_from)
|
|
169
|
+
.distinct
|
|
170
|
+
.all
|
|
171
|
+
.filter_map { |f|
|
|
172
|
+
match = f[:created_from]&.match(/\Apromoted:(.+):\d+\z/)
|
|
173
|
+
match[1] if match
|
|
174
|
+
}
|
|
175
|
+
.uniq
|
|
176
|
+
|
|
177
|
+
fact_paths = global.facts
|
|
178
|
+
.exclude(project_path: nil)
|
|
179
|
+
.select(:project_path)
|
|
180
|
+
.distinct
|
|
181
|
+
.all
|
|
182
|
+
.map { |f| f[:project_path] }
|
|
183
|
+
|
|
184
|
+
all_paths = (promoted_paths + fact_paths).uniq
|
|
185
|
+
current = @manager.project_path
|
|
186
|
+
|
|
187
|
+
all_paths.filter_map { |path|
|
|
188
|
+
next if path == current
|
|
189
|
+
|
|
190
|
+
db_path = File.join(path, ".claude", "memory.sqlite3")
|
|
191
|
+
entry = {path: path, db_path: db_path, exists: File.exist?(db_path)}
|
|
192
|
+
|
|
193
|
+
if entry[:exists]
|
|
194
|
+
begin
|
|
195
|
+
temp_store = Store::SQLiteStore.new(db_path)
|
|
196
|
+
entry[:facts_active] = temp_store.facts.where(status: "active").count
|
|
197
|
+
entry[:facts_total] = temp_store.facts.count
|
|
198
|
+
entry[:entities] = temp_store.entities.count
|
|
199
|
+
temp_store.close
|
|
200
|
+
rescue Sequel::DatabaseError, Extralite::Error, IOError => _e
|
|
201
|
+
entry[:error] = "Could not read database"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
entry
|
|
206
|
+
}
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Handlers
|
|
6
|
+
# Shortcut tool handlers (decisions, conventions, architecture)
|
|
7
|
+
module ShortcutHandlers
|
|
8
|
+
def decisions(args)
|
|
9
|
+
return {error: "Decisions shortcut requires StoreManager"} unless @manager
|
|
10
|
+
|
|
11
|
+
results = Recall.recent_decisions(@manager, limit: args["limit"] || 10)
|
|
12
|
+
format_shortcut_results(results, "decisions")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def conventions(args)
|
|
16
|
+
return {error: "Conventions shortcut requires StoreManager"} unless @manager
|
|
17
|
+
|
|
18
|
+
results = Recall.conventions(@manager, limit: args["limit"] || 20)
|
|
19
|
+
format_shortcut_results(results, "conventions")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def architecture(args)
|
|
23
|
+
return {error: "Architecture shortcut requires StoreManager"} unless @manager
|
|
24
|
+
|
|
25
|
+
results = Recall.architecture_choices(@manager, limit: args["limit"] || 10)
|
|
26
|
+
format_shortcut_results(results, "architecture")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def format_shortcut_results(results, category)
|
|
32
|
+
ResponseFormatter.format_shortcut_results(category, results)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
stats[:top_predicates] = store.db[:facts]
|
|
133
|
+
.where(status: "active")
|
|
134
|
+
.group_and_count(:predicate)
|
|
135
|
+
.order(Sequel.desc(:count))
|
|
136
|
+
.limit(10)
|
|
137
|
+
.all
|
|
138
|
+
.map { |row| {predicate: row[:predicate], count: row[:count]} }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
stats
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def entity_stats(store)
|
|
145
|
+
{
|
|
146
|
+
total: store.entities.count,
|
|
147
|
+
by_type: store.db[:entities]
|
|
148
|
+
.group_and_count(:type)
|
|
149
|
+
.order(Sequel.desc(:count))
|
|
150
|
+
.all
|
|
151
|
+
.map { |row| {type: row[:type], count: row[:count]} }
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def content_stats(store)
|
|
156
|
+
count = store.content_items.count
|
|
157
|
+
stats = {total: count}
|
|
158
|
+
|
|
159
|
+
if count > 0
|
|
160
|
+
stats[:date_range] = {
|
|
161
|
+
first: store.content_items.min(:occurred_at),
|
|
162
|
+
last: store.content_items.max(:occurred_at)
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
stats
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def provenance_stats(store, active_facts)
|
|
170
|
+
return {facts_with_sources: 0, total_active_facts: 0, coverage_percentage: 0} if active_facts == 0
|
|
171
|
+
|
|
172
|
+
facts_with_provenance = store.db[:provenance]
|
|
173
|
+
.join(:facts, id: :fact_id)
|
|
174
|
+
.where(Sequel[:facts][:status] => "active")
|
|
175
|
+
.select(Sequel[:provenance][:fact_id])
|
|
176
|
+
.distinct
|
|
177
|
+
.count
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
facts_with_sources: facts_with_provenance,
|
|
181
|
+
total_active_facts: active_facts,
|
|
182
|
+
coverage_percentage: (facts_with_provenance * 100.0 / active_facts).round(1)
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def vec_stats(store, _active_facts)
|
|
187
|
+
vec_index = store.vector_index
|
|
188
|
+
result = {available: vec_index.available?}
|
|
189
|
+
result.merge!(vec_index.coverage_stats) if vec_index.available?
|
|
190
|
+
result
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def conflict_stats(store)
|
|
194
|
+
open = store.conflicts.where(status: "open").count
|
|
195
|
+
resolved = store.conflicts.where(status: "resolved").count
|
|
196
|
+
|
|
197
|
+
{open: open, resolved: resolved, total: open + resolved}
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
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
|
|
|
@@ -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
|
|
@@ -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]})"
|
|
@@ -25,6 +25,7 @@ module ClaudeMemory
|
|
|
25
25
|
type: "object",
|
|
26
26
|
properties: {
|
|
27
27
|
query: {type: "string", description: "Search query for existing knowledge (e.g., 'authentication flow', 'error handling', 'database setup')"},
|
|
28
|
+
intent: {type: "string", description: "Optional intent to disambiguate the query (e.g., 'migration' or 'performance' when query is 'database'). Steers search without replacing the query."},
|
|
28
29
|
limit: {type: "integer", description: "Max results", default: 10},
|
|
29
30
|
scope: {type: "string", enum: ["all", "global", "project"], description: "Filter by scope: 'all' (default), 'global', or 'project'", default: "all"},
|
|
30
31
|
compact: {type: "boolean", description: "Omit provenance receipts for ~60% smaller responses (~800 → ~300 tokens/result)", default: false}
|
|
@@ -40,6 +41,7 @@ module ClaudeMemory
|
|
|
40
41
|
type: "object",
|
|
41
42
|
properties: {
|
|
42
43
|
query: {type: "string", description: "Search query for existing knowledge (e.g., 'client errors', 'database choice')"},
|
|
44
|
+
intent: {type: "string", description: "Optional intent to disambiguate the query (e.g., 'schema' or 'optimization' when query is 'database'). Steers search without replacing the query."},
|
|
43
45
|
limit: {type: "integer", description: "Maximum results to return", default: 20},
|
|
44
46
|
scope: {type: "string", enum: ["all", "global", "project"], description: "Scope: 'all' (both), 'global' (user-wide), 'project' (current only)", default: "all"}
|
|
45
47
|
},
|
|
@@ -265,10 +267,12 @@ module ClaudeMemory
|
|
|
265
267
|
type: "object",
|
|
266
268
|
properties: {
|
|
267
269
|
query: {type: "string", description: "Search query"},
|
|
270
|
+
intent: {type: "string", description: "Optional intent to disambiguate the query (e.g., 'security' when query is 'authentication'). Disables BM25 shortcut to ensure vector search runs."},
|
|
268
271
|
mode: {type: "string", enum: ["vector", "text", "both"], default: "both", description: "Search mode: vector (embeddings), text (FTS), or both (hybrid)"},
|
|
269
272
|
limit: {type: "integer", default: 10, description: "Maximum results to return"},
|
|
270
273
|
scope: {type: "string", enum: ["all", "global", "project"], default: "all", description: "Filter by scope"},
|
|
271
|
-
compact: {type: "boolean", description: "Omit provenance receipts for ~60% smaller responses (~800 → ~300 tokens/result)", default: false}
|
|
274
|
+
compact: {type: "boolean", description: "Omit provenance receipts for ~60% smaller responses (~800 → ~300 tokens/result)", default: false},
|
|
275
|
+
explain: {type: "boolean", description: "Include per-result score traces showing FTS rank, vector similarity, and RRF contribution", default: false}
|
|
272
276
|
},
|
|
273
277
|
required: ["query"]
|
|
274
278
|
},
|
|
@@ -309,6 +313,31 @@ module ClaudeMemory
|
|
|
309
313
|
},
|
|
310
314
|
annotations: READ_ONLY
|
|
311
315
|
},
|
|
316
|
+
{
|
|
317
|
+
name: "memory.undistilled",
|
|
318
|
+
description: "List content items not yet deeply distilled. Returns raw transcript text for knowledge extraction.",
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: {
|
|
322
|
+
limit: {type: "integer", default: 3, description: "Max items to return"},
|
|
323
|
+
min_length: {type: "integer", default: 200, description: "Min text length (skip tiny deltas)"}
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
annotations: READ_ONLY
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "memory.mark_distilled",
|
|
330
|
+
description: "Mark a content item as distilled after extracting facts from it.",
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: "object",
|
|
333
|
+
properties: {
|
|
334
|
+
content_item_id: {type: "integer", description: "ID of the distilled content item"},
|
|
335
|
+
facts_extracted: {type: "integer", default: 0, description: "Number of facts extracted"}
|
|
336
|
+
},
|
|
337
|
+
required: ["content_item_id"]
|
|
338
|
+
},
|
|
339
|
+
annotations: WRITE_IDEMPOTENT
|
|
340
|
+
},
|
|
312
341
|
{
|
|
313
342
|
name: "memory.check_setup",
|
|
314
343
|
description: "Check ClaudeMemory initialization status. Returns version info, issues found, and recommendations.",
|
|
@@ -75,6 +75,49 @@ module ClaudeMemory
|
|
|
75
75
|
def extract_limit(args, default: 10)
|
|
76
76
|
args["limit"] || default
|
|
77
77
|
end
|
|
78
|
+
|
|
79
|
+
# Extract optional intent parameter for query disambiguation
|
|
80
|
+
# @param args [Hash] Tool arguments
|
|
81
|
+
# @return [String, nil] Intent string or nil if not provided/blank
|
|
82
|
+
def extract_intent(args)
|
|
83
|
+
intent = args["intent"]
|
|
84
|
+
(intent.nil? || intent.to_s.strip.empty?) ? nil : intent.to_s.strip
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Collect undistilled content items from both stores (or legacy store)
|
|
88
|
+
# @param limit [Integer] Maximum items to return
|
|
89
|
+
# @param min_length [Integer] Minimum byte_len to include
|
|
90
|
+
# @return [Array<Hash>] Undistilled items sorted by recency
|
|
91
|
+
def collect_undistilled_items(limit:, min_length: 200)
|
|
92
|
+
if @manager
|
|
93
|
+
stores = []
|
|
94
|
+
stores << @manager.project_store if @manager.project_exists?
|
|
95
|
+
stores << @manager.global_store if @manager.global_exists?
|
|
96
|
+
items = stores.flat_map { |s| s.undistilled_content_items(limit: limit, min_length: min_length) }
|
|
97
|
+
items.sort_by { |i| i[:occurred_at] || "" }.reverse.first(limit)
|
|
98
|
+
elsif @legacy_store
|
|
99
|
+
@legacy_store.undistilled_content_items(limit: limit, min_length: min_length)
|
|
100
|
+
else
|
|
101
|
+
[]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Find the store containing a given content item
|
|
106
|
+
# @param content_item_id [Integer] Content item ID to locate
|
|
107
|
+
# @return [Store::SQLiteStore, nil] The store containing the item, or nil
|
|
108
|
+
def find_store_for_content_item(content_item_id)
|
|
109
|
+
if @manager
|
|
110
|
+
if @manager.project_store&.content_items&.where(id: content_item_id)&.any?
|
|
111
|
+
@manager.project_store
|
|
112
|
+
elsif @manager.global_store&.content_items&.where(id: content_item_id)&.any?
|
|
113
|
+
@manager.global_store
|
|
114
|
+
end
|
|
115
|
+
elsif @legacy_store
|
|
116
|
+
if @legacy_store.content_items.where(id: content_item_id).any?
|
|
117
|
+
@legacy_store
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
78
121
|
end
|
|
79
122
|
end
|
|
80
123
|
end
|