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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/memory.sqlite3-shm +0 -0
  4. data/.claude/memory.sqlite3-wal +0 -0
  5. data/.claude/settings.json +78 -6
  6. data/.claude/settings.local.json +5 -2
  7. data/.claude/skills/improve/SKILL.md +113 -25
  8. data/.claude-plugin/commands/distill-transcripts.md +98 -0
  9. data/.claude-plugin/commands/memory-recall.md +67 -0
  10. data/.claude-plugin/marketplace.json +1 -1
  11. data/.claude-plugin/plugin.json +1 -2
  12. data/CHANGELOG.md +74 -1
  13. data/CLAUDE.md +32 -6
  14. data/README.md +1 -1
  15. data/docs/improvements.md +51 -91
  16. data/docs/influence/lossless-claw.md +409 -0
  17. data/docs/quality_review.md +119 -224
  18. data/hooks/hooks.json +39 -7
  19. data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
  20. data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
  21. data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
  22. data/lib/claude_memory/commands/completion_command.rb +179 -0
  23. data/lib/claude_memory/commands/doctor_command.rb +2 -0
  24. data/lib/claude_memory/commands/help_command.rb +4 -0
  25. data/lib/claude_memory/commands/hook_command.rb +2 -1
  26. data/lib/claude_memory/commands/index_command.rb +100 -65
  27. data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
  28. data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
  29. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
  30. data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
  31. data/lib/claude_memory/commands/install_skill_command.rb +78 -0
  32. data/lib/claude_memory/commands/registry.rb +3 -1
  33. data/lib/claude_memory/commands/skills/distill-transcripts.md +98 -0
  34. data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
  35. data/lib/claude_memory/core/fact_ranker.rb +2 -2
  36. data/lib/claude_memory/core/rr_fusion.rb +23 -6
  37. data/lib/claude_memory/core/snippet_extractor.rb +7 -3
  38. data/lib/claude_memory/core/text_builder.rb +11 -0
  39. data/lib/claude_memory/domain/provenance.rb +0 -1
  40. data/lib/claude_memory/embeddings/api_adapter.rb +96 -0
  41. data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
  42. data/lib/claude_memory/embeddings/fastembed_adapter.rb +4 -0
  43. data/lib/claude_memory/embeddings/generator.rb +4 -0
  44. data/lib/claude_memory/embeddings/resolver.rb +18 -0
  45. data/lib/claude_memory/hook/context_injector.rb +58 -2
  46. data/lib/claude_memory/hook/distillation_runner.rb +46 -0
  47. data/lib/claude_memory/hook/handler.rb +11 -2
  48. data/lib/claude_memory/index/vector_index.rb +15 -2
  49. data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
  50. data/lib/claude_memory/mcp/error_classifier.rb +171 -0
  51. data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
  52. data/lib/claude_memory/mcp/handlers/management_handlers.rb +145 -0
  53. data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
  54. data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
  55. data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
  56. data/lib/claude_memory/mcp/handlers/stats_handlers.rb +202 -0
  57. data/lib/claude_memory/mcp/instructions_builder.rb +64 -5
  58. data/lib/claude_memory/mcp/query_guide.rb +51 -22
  59. data/lib/claude_memory/mcp/response_formatter.rb +4 -1
  60. data/lib/claude_memory/mcp/server.rb +1 -0
  61. data/lib/claude_memory/mcp/text_summary.rb +28 -1
  62. data/lib/claude_memory/mcp/tool_definitions.rb +33 -3
  63. data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
  64. data/lib/claude_memory/mcp/tools.rb +47 -681
  65. data/lib/claude_memory/recall/dual_engine.rb +105 -0
  66. data/lib/claude_memory/recall/legacy_engine.rb +138 -0
  67. data/lib/claude_memory/recall/query_core.rb +371 -0
  68. data/lib/claude_memory/recall.rb +29 -616
  69. data/lib/claude_memory/shortcuts.rb +4 -4
  70. data/lib/claude_memory/store/retry_handler.rb +61 -0
  71. data/lib/claude_memory/store/schema_manager.rb +68 -0
  72. data/lib/claude_memory/store/sqlite_store.rb +85 -201
  73. data/lib/claude_memory/sweep/maintenance.rb +126 -0
  74. data/lib/claude_memory/sweep/sweeper.rb +81 -75
  75. data/lib/claude_memory/templates/hooks.example.json +26 -7
  76. data/lib/claude_memory/version.rb +1 -1
  77. data/lib/claude_memory.rb +12 -0
  78. data/v0.6.0.ANNOUNCE +32 -0
  79. 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 => _e
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::Generator.new
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", Configuration.global_db_path] if scope == SCOPE_ALL || scope == SCOPE_GLOBAL
47
- pairs << ["project", Configuration.project_db_path] if scope == SCOPE_ALL || scope == SCOPE_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
- # Check for existing progress (resumption support)
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
- store.close
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
- store.close
92
- return
106
+ return nil
93
107
  end
94
108
 
95
- # Start or continue operation tracking
96
- operation_id = checkpoint ? checkpoint[:operation_id] : tracker.start_operation(
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
- # Wrap batch processing in transaction for atomicity
114
- store.db.transaction do
115
- batch.each do |fact|
116
- # Generate text representation
117
- text = build_fact_text(fact, store)
118
-
119
- # Generate embedding
120
- embedding = generator.generate(text)
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
- # Mark operation as completed
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" => 10}
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" => ingest_cmd, "timeout" => 10}
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
- {"type" => "command", "command" => sweep_cmd, "timeout" => 30}
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
- {"type" => "command", "command" => sweep_cmd, "timeout" => 30}
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