claude_memory 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/memory.sqlite3-shm +0 -0
- data/.claude/memory.sqlite3-wal +0 -0
- data/.claude/settings.json +78 -6
- data/.claude/settings.local.json +5 -2
- data/.claude/skills/improve/SKILL.md +113 -25
- data/.claude-plugin/commands/distill-transcripts.md +98 -0
- data/.claude-plugin/commands/memory-recall.md +67 -0
- data/.claude-plugin/marketplace.json +1 -1
- data/.claude-plugin/plugin.json +1 -2
- data/CHANGELOG.md +74 -1
- data/CLAUDE.md +32 -6
- data/README.md +1 -1
- data/docs/improvements.md +51 -91
- data/docs/influence/lossless-claw.md +409 -0
- data/docs/quality_review.md +119 -224
- data/hooks/hooks.json +39 -7
- data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
- data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
- data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
- data/lib/claude_memory/commands/completion_command.rb +179 -0
- data/lib/claude_memory/commands/doctor_command.rb +2 -0
- data/lib/claude_memory/commands/help_command.rb +4 -0
- data/lib/claude_memory/commands/hook_command.rb +2 -1
- data/lib/claude_memory/commands/index_command.rb +100 -65
- data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
- data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
- data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
- data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
- data/lib/claude_memory/commands/install_skill_command.rb +78 -0
- data/lib/claude_memory/commands/registry.rb +3 -1
- data/lib/claude_memory/commands/skills/distill-transcripts.md +98 -0
- data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
- data/lib/claude_memory/core/fact_ranker.rb +2 -2
- data/lib/claude_memory/core/rr_fusion.rb +23 -6
- data/lib/claude_memory/core/snippet_extractor.rb +7 -3
- data/lib/claude_memory/core/text_builder.rb +11 -0
- data/lib/claude_memory/domain/provenance.rb +0 -1
- data/lib/claude_memory/embeddings/api_adapter.rb +96 -0
- data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
- data/lib/claude_memory/embeddings/fastembed_adapter.rb +4 -0
- data/lib/claude_memory/embeddings/generator.rb +4 -0
- data/lib/claude_memory/embeddings/resolver.rb +18 -0
- data/lib/claude_memory/hook/context_injector.rb +58 -2
- data/lib/claude_memory/hook/distillation_runner.rb +46 -0
- data/lib/claude_memory/hook/handler.rb +11 -2
- data/lib/claude_memory/index/vector_index.rb +15 -2
- data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
- data/lib/claude_memory/mcp/error_classifier.rb +171 -0
- data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
- data/lib/claude_memory/mcp/handlers/management_handlers.rb +145 -0
- data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
- data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
- data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
- data/lib/claude_memory/mcp/handlers/stats_handlers.rb +202 -0
- data/lib/claude_memory/mcp/instructions_builder.rb +64 -5
- data/lib/claude_memory/mcp/query_guide.rb +51 -22
- data/lib/claude_memory/mcp/response_formatter.rb +4 -1
- data/lib/claude_memory/mcp/server.rb +1 -0
- data/lib/claude_memory/mcp/text_summary.rb +28 -1
- data/lib/claude_memory/mcp/tool_definitions.rb +33 -3
- data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
- data/lib/claude_memory/mcp/tools.rb +47 -681
- data/lib/claude_memory/recall/dual_engine.rb +105 -0
- data/lib/claude_memory/recall/legacy_engine.rb +138 -0
- data/lib/claude_memory/recall/query_core.rb +371 -0
- data/lib/claude_memory/recall.rb +29 -616
- data/lib/claude_memory/shortcuts.rb +4 -4
- data/lib/claude_memory/store/retry_handler.rb +61 -0
- data/lib/claude_memory/store/schema_manager.rb +68 -0
- data/lib/claude_memory/store/sqlite_store.rb +85 -201
- data/lib/claude_memory/sweep/maintenance.rb +126 -0
- data/lib/claude_memory/sweep/sweeper.rb +81 -75
- data/lib/claude_memory/templates/hooks.example.json +26 -7
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +12 -0
- data/v0.6.0.ANNOUNCE +32 -0
- metadata +27 -1
|
@@ -52,7 +52,8 @@ module ClaudeMemory
|
|
|
52
52
|
stats = store.vector_index.coverage_stats
|
|
53
53
|
totals[:with_embedding] += stats[:with_embedding]
|
|
54
54
|
totals[:vec_indexed] += stats[:vec_indexed]
|
|
55
|
-
rescue =>
|
|
55
|
+
rescue => e
|
|
56
|
+
ClaudeMemory.logger.debug("VecCheck failed for #{db_path}: #{e.message}")
|
|
56
57
|
next
|
|
57
58
|
ensure
|
|
58
59
|
store&.close
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module Commands
|
|
5
|
+
# Generates shell completion scripts for bash and zsh.
|
|
6
|
+
# Outputs completion script to stdout for eval or redirection.
|
|
7
|
+
class CompletionCommand < BaseCommand
|
|
8
|
+
def call(args)
|
|
9
|
+
opts = parse_options(args, {shell: detect_shell}) do |o|
|
|
10
|
+
OptionParser.new do |parser|
|
|
11
|
+
parser.banner = "Usage: claude-memory completion [options]"
|
|
12
|
+
parser.on("--shell SHELL", %w[bash zsh], "Shell type: bash or zsh (auto-detected)") { |v| o[:shell] = v }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
return 1 if opts.nil?
|
|
16
|
+
|
|
17
|
+
case opts[:shell]
|
|
18
|
+
when "zsh"
|
|
19
|
+
stdout.puts zsh_completion
|
|
20
|
+
when "bash"
|
|
21
|
+
stdout.puts bash_completion
|
|
22
|
+
else
|
|
23
|
+
return failure("Unknown shell: #{opts[:shell]}. Use --shell bash or --shell zsh")
|
|
24
|
+
end
|
|
25
|
+
0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def detect_shell
|
|
31
|
+
shell = ENV.fetch("SHELL", "/bin/bash")
|
|
32
|
+
File.basename(shell)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def command_names
|
|
36
|
+
Registry.all_commands.sort
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def zsh_completion
|
|
40
|
+
commands_with_desc = command_descriptions.map { |name, desc|
|
|
41
|
+
" '#{name}:#{desc}'"
|
|
42
|
+
}.join("\n")
|
|
43
|
+
|
|
44
|
+
<<~ZSH
|
|
45
|
+
#compdef claude-memory
|
|
46
|
+
|
|
47
|
+
_claude_memory() {
|
|
48
|
+
local -a commands
|
|
49
|
+
commands=(
|
|
50
|
+
#{commands_with_desc}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_arguments -C \\
|
|
54
|
+
'1:command:->command' \\
|
|
55
|
+
'*::arg:->args'
|
|
56
|
+
|
|
57
|
+
case $state in
|
|
58
|
+
command)
|
|
59
|
+
_describe 'command' commands
|
|
60
|
+
;;
|
|
61
|
+
args)
|
|
62
|
+
case $words[1] in
|
|
63
|
+
recall|search|explain)
|
|
64
|
+
_arguments '*:query:'
|
|
65
|
+
;;
|
|
66
|
+
promote)
|
|
67
|
+
_arguments '*:fact_id:'
|
|
68
|
+
;;
|
|
69
|
+
hook)
|
|
70
|
+
local -a subcommands
|
|
71
|
+
subcommands=('ingest:Ingest transcript' 'sweep:Run maintenance' 'publish:Publish snapshot' 'context:Inject context')
|
|
72
|
+
_describe 'subcommand' subcommands
|
|
73
|
+
;;
|
|
74
|
+
compact|export|changes|stats|sweep|conflicts)
|
|
75
|
+
_arguments '--scope[Scope]:scope:(all global project)'
|
|
76
|
+
;;
|
|
77
|
+
index)
|
|
78
|
+
_arguments '--vec[Build vector index]' '--rebuild[Rebuild from scratch]'
|
|
79
|
+
;;
|
|
80
|
+
completion)
|
|
81
|
+
_arguments '--shell[Shell type]:shell:(bash zsh)'
|
|
82
|
+
;;
|
|
83
|
+
install-skill)
|
|
84
|
+
local -a skills
|
|
85
|
+
skills=(#{skill_names.map { |s| "'#{s}'" }.join(" ")})
|
|
86
|
+
_arguments '--list[List available skills]' '--force[Overwrite existing]' '1:skill:($skills)'
|
|
87
|
+
;;
|
|
88
|
+
esac
|
|
89
|
+
;;
|
|
90
|
+
esac
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_claude_memory "$@"
|
|
94
|
+
ZSH
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def bash_completion
|
|
98
|
+
<<~BASH
|
|
99
|
+
# bash completion for claude-memory
|
|
100
|
+
|
|
101
|
+
_claude_memory() {
|
|
102
|
+
local cur prev commands
|
|
103
|
+
COMPREPLY=()
|
|
104
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
105
|
+
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
106
|
+
commands="#{command_names.join(" ")}"
|
|
107
|
+
|
|
108
|
+
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
109
|
+
COMPREPLY=( $(compgen -W "${commands}" -- "${cur}") )
|
|
110
|
+
return 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
case "${COMP_WORDS[1]}" in
|
|
114
|
+
hook)
|
|
115
|
+
COMPREPLY=( $(compgen -W "ingest sweep publish context" -- "${cur}") )
|
|
116
|
+
;;
|
|
117
|
+
compact|export|changes|stats|sweep|conflicts)
|
|
118
|
+
if [[ "${prev}" == "--scope" ]]; then
|
|
119
|
+
COMPREPLY=( $(compgen -W "all global project" -- "${cur}") )
|
|
120
|
+
else
|
|
121
|
+
COMPREPLY=( $(compgen -W "--scope" -- "${cur}") )
|
|
122
|
+
fi
|
|
123
|
+
;;
|
|
124
|
+
install-skill)
|
|
125
|
+
if [[ "${prev}" == "install-skill" ]]; then
|
|
126
|
+
COMPREPLY=( $(compgen -W "#{skill_names.join(" ")} --list --force" -- "${cur}") )
|
|
127
|
+
fi
|
|
128
|
+
;;
|
|
129
|
+
completion)
|
|
130
|
+
if [[ "${prev}" == "--shell" ]]; then
|
|
131
|
+
COMPREPLY=( $(compgen -W "bash zsh" -- "${cur}") )
|
|
132
|
+
else
|
|
133
|
+
COMPREPLY=( $(compgen -W "--shell" -- "${cur}") )
|
|
134
|
+
fi
|
|
135
|
+
;;
|
|
136
|
+
esac
|
|
137
|
+
return 0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
complete -F _claude_memory claude-memory
|
|
141
|
+
BASH
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def command_descriptions
|
|
145
|
+
{
|
|
146
|
+
"changes" => "Show recent fact changes",
|
|
147
|
+
"compact" => "Compact databases",
|
|
148
|
+
"completion" => "Generate shell completions",
|
|
149
|
+
"conflicts" => "Show open conflicts",
|
|
150
|
+
"db:init" => "Initialize database",
|
|
151
|
+
"doctor" => "Check system health",
|
|
152
|
+
"explain" => "Explain a fact with receipts",
|
|
153
|
+
"export" => "Export facts to JSON",
|
|
154
|
+
"git-lfs" => "Git LFS integration",
|
|
155
|
+
"help" => "Show help message",
|
|
156
|
+
"hook" => "Run hook entrypoints",
|
|
157
|
+
"index" => "Index content",
|
|
158
|
+
"ingest" => "Ingest transcript delta",
|
|
159
|
+
"init" => "Initialize ClaudeMemory",
|
|
160
|
+
"install-skill" => "Install agent skills",
|
|
161
|
+
"promote" => "Promote fact to global",
|
|
162
|
+
"publish" => "Publish snapshot",
|
|
163
|
+
"recall" => "Recall facts matching query",
|
|
164
|
+
"recover" => "Recover database",
|
|
165
|
+
"search" => "Search indexed content",
|
|
166
|
+
"serve-mcp" => "Start MCP server",
|
|
167
|
+
"stats" => "Show statistics",
|
|
168
|
+
"sweep" => "Run maintenance",
|
|
169
|
+
"uninstall" => "Remove configuration",
|
|
170
|
+
"version" => "Show version"
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def skill_names
|
|
175
|
+
InstallSkillCommand::AVAILABLE_SKILLS.keys
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -20,6 +20,8 @@ module ClaudeMemory
|
|
|
20
20
|
checks = [
|
|
21
21
|
Checks::DatabaseCheck.new(manager.global_db_path, "global"),
|
|
22
22
|
Checks::DatabaseCheck.new(manager.project_db_path, "project"),
|
|
23
|
+
Checks::DistillCheck.new(manager.global_db_path, "global"),
|
|
24
|
+
Checks::DistillCheck.new(manager.project_db_path, "project"),
|
|
23
25
|
Checks::VecCheck.new,
|
|
24
26
|
Checks::SnapshotCheck.new,
|
|
25
27
|
Checks::ClaudeMdCheck.new,
|
|
@@ -31,6 +31,10 @@ module ClaudeMemory
|
|
|
31
31
|
uninstall Remove ClaudeMemory configuration
|
|
32
32
|
version Show version number
|
|
33
33
|
|
|
34
|
+
Utilities:
|
|
35
|
+
completion Generate shell completions (bash/zsh)
|
|
36
|
+
install-skill Install agent skills to ~/.claude/commands/
|
|
37
|
+
|
|
34
38
|
Run 'claude-memory <command> --help' for more information on a command.
|
|
35
39
|
HELP
|
|
36
40
|
0
|
|
@@ -171,13 +171,14 @@ module ClaudeMemory
|
|
|
171
171
|
|
|
172
172
|
def hook_context(payload, db_path)
|
|
173
173
|
project_path = payload["project_path"] || payload["cwd"]
|
|
174
|
+
source = payload["source"]
|
|
174
175
|
manager = ClaudeMemory::Store::StoreManager.new(
|
|
175
176
|
project_db_path: db_path,
|
|
176
177
|
project_path: project_path
|
|
177
178
|
)
|
|
178
179
|
manager.ensure_both!
|
|
179
180
|
|
|
180
|
-
injector = ClaudeMemory::Hook::ContextInjector.new(manager)
|
|
181
|
+
injector = ClaudeMemory::Hook::ContextInjector.new(manager, source: source)
|
|
181
182
|
context_text = injector.generate_context
|
|
182
183
|
|
|
183
184
|
if context_text
|
|
@@ -9,13 +9,14 @@ module ClaudeMemory
|
|
|
9
9
|
SCOPE_PROJECT = "project"
|
|
10
10
|
|
|
11
11
|
def call(args)
|
|
12
|
-
opts = parse_options(args, {scope: SCOPE_ALL, batch_size: 100, force: false, vec: false}) do |o|
|
|
12
|
+
opts = parse_options(args, {scope: SCOPE_ALL, batch_size: 100, force: false, vec: false, provider: nil}) do |o|
|
|
13
13
|
OptionParser.new do |parser|
|
|
14
14
|
parser.banner = "Usage: claude-memory index [options]"
|
|
15
15
|
parser.on("--scope SCOPE", "Scope: global, project, or all (default: all)") { |v| o[:scope] = v }
|
|
16
16
|
parser.on("--batch-size SIZE", Integer, "Batch size (default: 100)") { |v| o[:batch_size] = v }
|
|
17
17
|
parser.on("--force", "Re-index facts that already have embeddings") { o[:force] = true }
|
|
18
18
|
parser.on("--vec", "Backfill vec0 index from existing embeddings (no regeneration)") { o[:vec] = true }
|
|
19
|
+
parser.on("--provider NAME", "Embedding provider: tfidf, fastembed, api") { |v| o[:provider] = v }
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
return 1 if opts.nil?
|
|
@@ -30,7 +31,7 @@ module ClaudeMemory
|
|
|
30
31
|
return vec_backfill(opts)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
generator = Embeddings
|
|
34
|
+
generator = Embeddings.resolve(opts[:provider])
|
|
34
35
|
|
|
35
36
|
scopes_for(opts[:scope]).each do |label, db_path|
|
|
36
37
|
index_database(label, db_path, generator, opts)
|
|
@@ -42,9 +43,10 @@ module ClaudeMemory
|
|
|
42
43
|
private
|
|
43
44
|
|
|
44
45
|
def scopes_for(scope)
|
|
46
|
+
config = Configuration.new
|
|
45
47
|
pairs = []
|
|
46
|
-
pairs << ["global",
|
|
47
|
-
pairs << ["project",
|
|
48
|
+
pairs << ["global", config.global_db_path] if scope == SCOPE_ALL || scope == SCOPE_GLOBAL
|
|
49
|
+
pairs << ["project", config.project_db_path] if scope == SCOPE_ALL || scope == SCOPE_PROJECT
|
|
48
50
|
pairs
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -55,96 +57,85 @@ module ClaudeMemory
|
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
store = Store::SQLiteStore.new(db_path)
|
|
60
|
+
handle_dimension_mismatch(store, generator, label)
|
|
58
61
|
tracker = Infrastructure::OperationTracker.new(store)
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
facts, checkpoint = find_facts_to_index(store, tracker, label, opts)
|
|
64
|
+
unless facts
|
|
65
|
+
store.close
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
operation_id = checkpoint ? checkpoint[:operation_id] : tracker.start_operation(
|
|
70
|
+
operation_type: "index_embeddings",
|
|
71
|
+
scope: label,
|
|
72
|
+
total_items: facts.size,
|
|
73
|
+
checkpoint_data: {last_fact_id: nil}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
stdout.puts "#{label.capitalize} database: Indexing #{facts.size} facts..."
|
|
77
|
+
run_indexing(store, facts, generator, tracker, operation_id, checkpoint, opts)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def handle_dimension_mismatch(store, generator, label)
|
|
81
|
+
check = Embeddings::DimensionCheck.call(store, generator)
|
|
82
|
+
return unless check.status == :mismatch
|
|
83
|
+
|
|
84
|
+
stdout.puts "#{label.capitalize}: Embedding dimensions changed (#{check.stored} → #{check.current}), clearing stale embeddings..."
|
|
85
|
+
clear_stale_embeddings(store)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def find_facts_to_index(store, tracker, label, opts)
|
|
61
89
|
checkpoint = tracker.get_checkpoint(operation_type: "index_embeddings", scope: label)
|
|
90
|
+
|
|
62
91
|
if checkpoint && !opts[:force]
|
|
63
92
|
stdout.puts "#{label.capitalize} database: Resuming from previous run (processed #{checkpoint[:processed_items]} facts)..."
|
|
64
93
|
resume_from_fact_id = checkpoint[:checkpoint_data][:last_fact_id]
|
|
65
|
-
else
|
|
66
|
-
resume_from_fact_id = nil
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Find facts to index
|
|
70
|
-
facts_dataset = if opts[:force]
|
|
71
|
-
store.facts
|
|
72
|
-
else
|
|
73
|
-
store.facts.where(embedding_json: nil)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# If resuming, skip facts we've already processed
|
|
77
|
-
if resume_from_fact_id
|
|
78
|
-
facts_dataset = facts_dataset.where(Sequel.lit("id > ?", resume_from_fact_id))
|
|
79
94
|
end
|
|
80
95
|
|
|
96
|
+
facts_dataset = opts[:force] ? store.facts : store.facts.where(embedding_json: nil)
|
|
97
|
+
facts_dataset = facts_dataset.where(Sequel.lit("id > ?", resume_from_fact_id)) if resume_from_fact_id
|
|
81
98
|
facts = facts_dataset.order(:id).all
|
|
82
99
|
|
|
83
100
|
if facts.empty? && !checkpoint
|
|
84
101
|
stdout.puts "#{label.capitalize} database: All facts already indexed"
|
|
85
|
-
|
|
86
|
-
return
|
|
102
|
+
return nil
|
|
87
103
|
elsif facts.empty? && checkpoint
|
|
88
|
-
# Resume found nothing left to do - mark as completed
|
|
89
104
|
tracker.complete_operation(checkpoint[:operation_id])
|
|
90
105
|
stdout.puts "#{label.capitalize} database: Resumed operation completed (nothing left to index)"
|
|
91
|
-
|
|
92
|
-
return
|
|
106
|
+
return nil
|
|
93
107
|
end
|
|
94
108
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
operation_type: "index_embeddings",
|
|
98
|
-
scope: label,
|
|
99
|
-
total_items: facts.size,
|
|
100
|
-
checkpoint_data: {last_fact_id: nil}
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
stdout.puts "#{label.capitalize} database: Indexing #{facts.size} facts..."
|
|
109
|
+
[facts, checkpoint]
|
|
110
|
+
end
|
|
104
111
|
|
|
112
|
+
def run_indexing(store, facts, generator, tracker, operation_id, checkpoint, opts)
|
|
105
113
|
vec_index = store.vector_index
|
|
106
|
-
if vec_index.available?
|
|
107
|
-
stdout.puts " sqlite-vec available, dual-writing to vec0 index"
|
|
108
|
-
end
|
|
114
|
+
stdout.puts " sqlite-vec available, dual-writing to vec0 index" if vec_index.available?
|
|
109
115
|
|
|
116
|
+
embedding_cache = build_embedding_cache(store)
|
|
117
|
+
cache_hits = 0
|
|
110
118
|
processed = checkpoint ? checkpoint[:processed_items] : 0
|
|
119
|
+
|
|
111
120
|
begin
|
|
112
121
|
facts.each_slice(opts[:batch_size]) do |batch|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# Store embedding (JSON column)
|
|
123
|
-
store.update_fact_embedding(fact[:id], embedding)
|
|
124
|
-
|
|
125
|
-
# Dual-write to vec0 if available (insert_embedding manages vec_indexed_at)
|
|
126
|
-
vec_index.insert_embedding(fact[:id], embedding) if vec_index.available?
|
|
127
|
-
|
|
128
|
-
processed += 1
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Update checkpoint after batch commits
|
|
132
|
-
last_fact_id = batch.last[:id]
|
|
133
|
-
tracker.update_progress(
|
|
134
|
-
operation_id,
|
|
135
|
-
processed_items: processed,
|
|
136
|
-
checkpoint_data: {last_fact_id: last_fact_id}
|
|
137
|
-
)
|
|
138
|
-
end
|
|
139
|
-
|
|
122
|
+
cache_hits += process_batch(store, batch, generator, vec_index, embedding_cache)
|
|
123
|
+
processed += batch.size
|
|
124
|
+
|
|
125
|
+
tracker.update_progress(
|
|
126
|
+
operation_id,
|
|
127
|
+
processed_items: processed,
|
|
128
|
+
checkpoint_data: {last_fact_id: batch.last[:id]}
|
|
129
|
+
)
|
|
140
130
|
stdout.puts " Processed #{processed} facts..."
|
|
141
131
|
end
|
|
142
132
|
|
|
143
|
-
|
|
133
|
+
report_dedup_stats(processed, cache_hits)
|
|
134
|
+
store.set_meta("embedding_dimensions", generator.dimensions.to_s)
|
|
135
|
+
store.set_meta("embedding_provider", generator.name)
|
|
144
136
|
tracker.complete_operation(operation_id)
|
|
145
137
|
stdout.puts " Done!"
|
|
146
138
|
rescue => e
|
|
147
|
-
# Mark operation as failed
|
|
148
139
|
tracker.fail_operation(operation_id, e.message)
|
|
149
140
|
stderr.puts " Failed: #{e.message}"
|
|
150
141
|
raise
|
|
@@ -153,6 +144,32 @@ module ClaudeMemory
|
|
|
153
144
|
end
|
|
154
145
|
end
|
|
155
146
|
|
|
147
|
+
def process_batch(store, batch, generator, vec_index, embedding_cache)
|
|
148
|
+
cache_hits = 0
|
|
149
|
+
store.db.transaction do
|
|
150
|
+
batch.each do |fact|
|
|
151
|
+
text = build_fact_text(fact, store)
|
|
152
|
+
embedding = embedding_cache[text]
|
|
153
|
+
if embedding
|
|
154
|
+
cache_hits += 1
|
|
155
|
+
else
|
|
156
|
+
embedding = generator.generate(text)
|
|
157
|
+
embedding_cache[text] = embedding
|
|
158
|
+
end
|
|
159
|
+
store.update_fact_embedding(fact[:id], embedding)
|
|
160
|
+
vec_index.insert_embedding(fact[:id], embedding) if vec_index.available?
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
cache_hits
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def report_dedup_stats(processed, cache_hits)
|
|
167
|
+
return unless processed > 0
|
|
168
|
+
|
|
169
|
+
pct = (cache_hits > 0) ? "#{(cache_hits * 100.0 / processed).round(1)}%" : "0%"
|
|
170
|
+
stdout.puts " Cache hits: #{cache_hits}/#{processed} (#{pct} dedup)"
|
|
171
|
+
end
|
|
172
|
+
|
|
156
173
|
def vec_backfill(opts)
|
|
157
174
|
scopes_for(opts[:scope]).each do |label, db_path|
|
|
158
175
|
unless File.exist?(db_path)
|
|
@@ -192,6 +209,19 @@ module ClaudeMemory
|
|
|
192
209
|
0
|
|
193
210
|
end
|
|
194
211
|
|
|
212
|
+
def build_embedding_cache(store)
|
|
213
|
+
cache = {}
|
|
214
|
+
store.facts
|
|
215
|
+
.where(status: "active")
|
|
216
|
+
.where(Sequel.~(embedding_json: nil))
|
|
217
|
+
.select(:id, :subject_entity_id, :predicate, :object_entity_id, :object_literal, :embedding_json)
|
|
218
|
+
.each do |fact|
|
|
219
|
+
text = build_fact_text(fact, store)
|
|
220
|
+
cache[text] ||= JSON.parse(fact[:embedding_json])
|
|
221
|
+
end
|
|
222
|
+
cache
|
|
223
|
+
end
|
|
224
|
+
|
|
195
225
|
def build_fact_text(fact, store)
|
|
196
226
|
# Build rich text representation for embedding
|
|
197
227
|
parts = []
|
|
@@ -216,6 +246,11 @@ module ClaudeMemory
|
|
|
216
246
|
parts.join(" ")
|
|
217
247
|
end
|
|
218
248
|
|
|
249
|
+
def clear_stale_embeddings(store)
|
|
250
|
+
store.facts.where(Sequel.~(embedding_json: nil)).update(embedding_json: nil, vec_indexed_at: nil)
|
|
251
|
+
store.vector_index.clear!
|
|
252
|
+
end
|
|
253
|
+
|
|
219
254
|
def valid_scope?(scope)
|
|
220
255
|
[SCOPE_ALL, SCOPE_GLOBAL, SCOPE_PROJECT].include?(scope)
|
|
221
256
|
end
|
|
@@ -15,6 +15,10 @@ module ClaudeMemory
|
|
|
15
15
|
@stdout.puts "✓ Global database: #{manager.global_db_path}"
|
|
16
16
|
manager.ensure_project!
|
|
17
17
|
@stdout.puts "✓ Project database: #{manager.project_db_path}"
|
|
18
|
+
|
|
19
|
+
backfill_distillation_metrics(manager.global_store, "global")
|
|
20
|
+
backfill_distillation_metrics(manager.project_store, "project")
|
|
21
|
+
|
|
18
22
|
manager.close
|
|
19
23
|
end
|
|
20
24
|
|
|
@@ -22,8 +26,20 @@ module ClaudeMemory
|
|
|
22
26
|
manager = ClaudeMemory::Store::StoreManager.new
|
|
23
27
|
manager.ensure_global!
|
|
24
28
|
@stdout.puts "✓ Created global database: #{manager.global_db_path}"
|
|
29
|
+
|
|
30
|
+
backfill_distillation_metrics(manager.global_store, "global")
|
|
31
|
+
|
|
25
32
|
manager.close
|
|
26
33
|
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def backfill_distillation_metrics(store, label)
|
|
38
|
+
backfilled = store.backfill_distillation_metrics!
|
|
39
|
+
if backfilled > 0
|
|
40
|
+
@stdout.puts "✓ Marked #{backfilled} pre-existing content items as distilled (#{label})"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
27
43
|
end
|
|
28
44
|
end
|
|
29
45
|
end
|
|
@@ -65,6 +65,7 @@ module ClaudeMemory
|
|
|
65
65
|
@skip_hooks = true
|
|
66
66
|
else
|
|
67
67
|
@stdout.puts "\nUpdating hooks..."
|
|
68
|
+
@replace_hooks = true
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
71
|
|
|
@@ -73,7 +74,7 @@ module ClaudeMemory
|
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
def configure_hooks
|
|
76
|
-
HooksConfigurator.new(@stdout).configure_global_hooks
|
|
77
|
+
HooksConfigurator.new(@stdout).configure_global_hooks(replace: @replace_hooks || false)
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def configure_mcp
|
|
@@ -12,7 +12,7 @@ module ClaudeMemory
|
|
|
12
12
|
@stdout = stdout
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def configure_project_hooks
|
|
15
|
+
def configure_project_hooks(replace: false)
|
|
16
16
|
settings_path = ".claude/settings.json"
|
|
17
17
|
FileUtils.mkdir_p(File.dirname(settings_path))
|
|
18
18
|
|
|
@@ -24,13 +24,13 @@ module ClaudeMemory
|
|
|
24
24
|
|
|
25
25
|
existing = load_json_file(settings_path)
|
|
26
26
|
existing["hooks"] ||= {}
|
|
27
|
-
merge_hooks!(existing["hooks"], hooks_config["hooks"])
|
|
27
|
+
merge_hooks!(existing["hooks"], hooks_config["hooks"], replace: replace)
|
|
28
28
|
|
|
29
29
|
File.write(settings_path, JSON.pretty_generate(existing))
|
|
30
30
|
@stdout.puts "✓ Configured hooks in #{settings_path}"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def configure_global_hooks
|
|
33
|
+
def configure_global_hooks(replace: false)
|
|
34
34
|
settings_path = File.join(Dir.home, ".claude", "settings.json")
|
|
35
35
|
FileUtils.mkdir_p(File.dirname(settings_path))
|
|
36
36
|
|
|
@@ -42,7 +42,7 @@ module ClaudeMemory
|
|
|
42
42
|
|
|
43
43
|
existing = load_json_file(settings_path)
|
|
44
44
|
existing["hooks"] ||= {}
|
|
45
|
-
merge_hooks!(existing["hooks"], hooks_config["hooks"])
|
|
45
|
+
merge_hooks!(existing["hooks"], hooks_config["hooks"], replace: replace)
|
|
46
46
|
|
|
47
47
|
File.write(settings_path, JSON.pretty_generate(existing))
|
|
48
48
|
@stdout.puts "✓ Configured hooks in #{settings_path}"
|
|
@@ -97,28 +97,61 @@ module ClaudeMemory
|
|
|
97
97
|
private
|
|
98
98
|
|
|
99
99
|
def build_hooks_config(ingest_cmd, sweep_cmd)
|
|
100
|
+
context_cmd = "claude-memory hook context"
|
|
101
|
+
|
|
100
102
|
{
|
|
101
103
|
"hooks" => {
|
|
102
104
|
"Stop" => [{
|
|
103
105
|
"hooks" => [
|
|
104
|
-
{"type" => "command", "command" => ingest_cmd, "timeout" =>
|
|
106
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 5,
|
|
107
|
+
"statusMessage" => "Saving memory..."}
|
|
108
|
+
]
|
|
109
|
+
}],
|
|
110
|
+
"StopFailure" => [{
|
|
111
|
+
"hooks" => [
|
|
112
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 5,
|
|
113
|
+
"statusMessage" => "Saving memory..."}
|
|
105
114
|
]
|
|
106
115
|
}],
|
|
107
116
|
"SessionStart" => [{
|
|
108
117
|
"hooks" => [
|
|
109
|
-
{"type" => "command", "command" =>
|
|
118
|
+
{"type" => "command", "command" => context_cmd, "timeout" => 5,
|
|
119
|
+
"statusMessage" => "Loading memory..."}
|
|
110
120
|
]
|
|
111
121
|
}],
|
|
112
122
|
"PreCompact" => [{
|
|
113
123
|
"hooks" => [
|
|
114
|
-
{"type" => "command", "command" => ingest_cmd, "timeout" => 30
|
|
115
|
-
|
|
124
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 30,
|
|
125
|
+
"statusMessage" => "Saving memory..."},
|
|
126
|
+
{"type" => "command", "command" => sweep_cmd, "timeout" => 30,
|
|
127
|
+
"statusMessage" => "Sweeping memory..."}
|
|
116
128
|
]
|
|
117
129
|
}],
|
|
118
130
|
"SessionEnd" => [{
|
|
119
131
|
"hooks" => [
|
|
120
|
-
{"type" => "command", "command" => ingest_cmd, "timeout" => 30
|
|
121
|
-
|
|
132
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 30,
|
|
133
|
+
"statusMessage" => "Saving memory..."},
|
|
134
|
+
{"type" => "command", "command" => sweep_cmd, "timeout" => 30,
|
|
135
|
+
"statusMessage" => "Sweeping memory..."}
|
|
136
|
+
]
|
|
137
|
+
}],
|
|
138
|
+
"TaskCompleted" => [{
|
|
139
|
+
"hooks" => [
|
|
140
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 10,
|
|
141
|
+
"statusMessage" => "Saving memory..."}
|
|
142
|
+
]
|
|
143
|
+
}],
|
|
144
|
+
"TeammateIdle" => [{
|
|
145
|
+
"hooks" => [
|
|
146
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 15,
|
|
147
|
+
"statusMessage" => "Saving memory..."}
|
|
148
|
+
]
|
|
149
|
+
}],
|
|
150
|
+
"Notification" => [{
|
|
151
|
+
"matcher" => "idle_prompt",
|
|
152
|
+
"hooks" => [
|
|
153
|
+
{"type" => "command", "command" => sweep_cmd, "timeout" => 10,
|
|
154
|
+
"statusMessage" => "Sweeping memory..."}
|
|
122
155
|
]
|
|
123
156
|
}]
|
|
124
157
|
}
|
|
@@ -132,7 +165,18 @@ module ClaudeMemory
|
|
|
132
165
|
{}
|
|
133
166
|
end
|
|
134
167
|
|
|
135
|
-
def merge_hooks!(existing_hooks, new_hooks)
|
|
168
|
+
def merge_hooks!(existing_hooks, new_hooks, replace: false)
|
|
169
|
+
if replace
|
|
170
|
+
# Remove all claude-memory hooks first, then add fresh ones
|
|
171
|
+
existing_hooks.each do |event, hook_arrays|
|
|
172
|
+
next unless hook_arrays.is_a?(Array)
|
|
173
|
+
hook_arrays.reject! do |ha|
|
|
174
|
+
ha.is_a?(Hash) && ha["hooks"]&.any? { |h| h["command"]&.include?("claude-memory") }
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
existing_hooks.delete_if { |_, v| v.is_a?(Array) && v.empty? }
|
|
178
|
+
end
|
|
179
|
+
|
|
136
180
|
new_hooks.each do |event, hook_arrays|
|
|
137
181
|
existing_hooks[event] ||= []
|
|
138
182
|
|
|
@@ -68,6 +68,7 @@ module ClaudeMemory
|
|
|
68
68
|
@skip_hooks = true
|
|
69
69
|
else
|
|
70
70
|
@stdout.puts "\nUpdating hooks..."
|
|
71
|
+
@replace_hooks = true
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
|
|
@@ -81,7 +82,7 @@ module ClaudeMemory
|
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def configure_hooks
|
|
84
|
-
HooksConfigurator.new(@stdout).configure_project_hooks
|
|
85
|
+
HooksConfigurator.new(@stdout).configure_project_hooks(replace: @replace_hooks || false)
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
def configure_mcp
|