claude_memory 0.11.0 → 0.12.1

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +54 -85
  4. data/.claude/skills/release/SKILL.md +44 -6
  5. data/.claude/skills/study-repo/SKILL.md +15 -0
  6. data/.claude-plugin/commands/audit-memory.md +68 -0
  7. data/.claude-plugin/marketplace.json +1 -1
  8. data/.claude-plugin/plugin.json +2 -4
  9. data/CHANGELOG.md +50 -0
  10. data/CLAUDE.md +11 -4
  11. data/README.md +40 -1
  12. data/db/migrations/018_add_otel_telemetry.rb +81 -0
  13. data/docs/1_0_punchlist.md +318 -66
  14. data/docs/api_stability.md +346 -0
  15. data/docs/audit_runbook.md +209 -0
  16. data/docs/claude_monitoring.md +956 -0
  17. data/docs/improvements.md +148 -9
  18. data/docs/influence/ai-memory-systems-2026.md +403 -0
  19. data/docs/memory_audit_2026-05-21.md +303 -0
  20. data/docs/plugin.md +1 -1
  21. data/docs/soak/audit_2026-06-03_agent-training-program.json +53 -0
  22. data/docs/soak/audit_2026-06-03_agentic.json +31 -0
  23. data/docs/soak/audit_2026-06-03_ai-software-architect.json +19 -0
  24. data/docs/soak/audit_2026-06-03_chaos_to_the_rescue.json +60 -0
  25. data/docs/soak/audit_2026-06-03_claude_memory.json +55 -0
  26. data/docs/soak/audit_2026-06-03_daily-vibe.json +59 -0
  27. data/docs/soak/audit_2026-06-03_minerva-sky.json +19 -0
  28. data/docs/soak/audit_2026-06-03_nowreading.dev.json +19 -0
  29. data/docs/soak/audit_2026-06-03_ups.dev.json +55 -0
  30. data/docs/soak/baseline_2026-06-03.md +145 -0
  31. data/lib/claude_memory/audit/checks.rb +239 -0
  32. data/lib/claude_memory/audit/finding.rb +33 -0
  33. data/lib/claude_memory/audit/runner.rb +73 -0
  34. data/lib/claude_memory/commands/audit_command.rb +117 -0
  35. data/lib/claude_memory/commands/checks/embeddings_check.rb +97 -0
  36. data/lib/claude_memory/commands/dashboard_command.rb +2 -1
  37. data/lib/claude_memory/commands/doctor_command.rb +1 -0
  38. data/lib/claude_memory/commands/import_auto_memory_command.rb +180 -0
  39. data/lib/claude_memory/commands/otel_command.rb +240 -0
  40. data/lib/claude_memory/commands/registry.rb +5 -1
  41. data/lib/claude_memory/commands/setup_vectors_command.rb +182 -0
  42. data/lib/claude_memory/configuration.rb +60 -0
  43. data/lib/claude_memory/core/fact_query_builder.rb +1 -0
  44. data/lib/claude_memory/dashboard/api.rb +8 -0
  45. data/lib/claude_memory/dashboard/index.html +140 -1
  46. data/lib/claude_memory/dashboard/prompt_journey.rb +48 -0
  47. data/lib/claude_memory/dashboard/server.rb +86 -0
  48. data/lib/claude_memory/dashboard/telemetry.rb +156 -0
  49. data/lib/claude_memory/deprecations.rb +106 -0
  50. data/lib/claude_memory/distill/reference_material_detector.rb +37 -4
  51. data/lib/claude_memory/hook/auto_memory_mirror.rb +7 -3
  52. data/lib/claude_memory/hook/context_injector.rb +11 -2
  53. data/lib/claude_memory/mcp/tool_definitions.rb +3 -3
  54. data/lib/claude_memory/otel/attributes.rb +118 -0
  55. data/lib/claude_memory/otel/constants.rb +32 -0
  56. data/lib/claude_memory/otel/ingestor.rb +54 -0
  57. data/lib/claude_memory/otel/otlp_json_envelope.rb +254 -0
  58. data/lib/claude_memory/otel/prompt_scope.rb +108 -0
  59. data/lib/claude_memory/otel/settings_writer.rb +122 -0
  60. data/lib/claude_memory/otel/status.rb +58 -0
  61. data/lib/claude_memory/recall/staleness_annotator.rb +73 -0
  62. data/lib/claude_memory/resolve/predicate_policy.rb +17 -1
  63. data/lib/claude_memory/resolve/resolver.rb +30 -3
  64. data/lib/claude_memory/shortcuts.rb +61 -18
  65. data/lib/claude_memory/store/prompt_journey_query.rb +87 -0
  66. data/lib/claude_memory/store/schema_manager.rb +1 -1
  67. data/lib/claude_memory/store/sqlite_store.rb +136 -0
  68. data/lib/claude_memory/sweep/maintenance.rb +31 -1
  69. data/lib/claude_memory/sweep/sweeper.rb +6 -0
  70. data/lib/claude_memory/version.rb +1 -1
  71. data/lib/claude_memory.rb +20 -0
  72. metadata +38 -1
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ module Checks
6
+ # Surfaces the active embedding provider, model, and dimension
7
+ # alignment between provider and stored vectors.
8
+ #
9
+ # Doctor previously had VecCheck (sqlite-vec extension + index
10
+ # coverage) but no signal about which provider was actually in use —
11
+ # so a user could see "sqlite-vec available ✓" while silently
12
+ # running on tfidf default when fastembed was loadable. This check
13
+ # closes that visibility gap and points users at
14
+ # `claude-memory setup-vectors` to opt into fastembed.
15
+ class EmbeddingsCheck
16
+ FASTEMBED_HINT = "Set CLAUDE_MEMORY_EMBEDDING_PROVIDER=fastembed for higher-quality semantic recall (fastembed is loadable on this system). " \
17
+ "Run 'claude-memory setup-vectors' to configure."
18
+
19
+ FASTEMBED_INSTALL_HINT = "fastembed is not installed; semantic recall is using tfidf (lower quality). " \
20
+ "Run 'claude-memory setup-vectors' to install fastembed and switch."
21
+
22
+ def call
23
+ provider = Embeddings.resolve
24
+ provider_name = provider.name
25
+ warnings = []
26
+
27
+ # Hint when user is on default tfidf — different message
28
+ # depending on whether fastembed is even loadable.
29
+ if provider_name == "tfidf"
30
+ warnings << (fastembed_loadable? ? FASTEMBED_HINT : FASTEMBED_INSTALL_HINT)
31
+ end
32
+
33
+ dim_mismatches = check_dimension_alignment(provider)
34
+ warnings.concat(dim_mismatches)
35
+
36
+ {
37
+ status: warnings.any? ? :warning : :ok,
38
+ label: "embeddings",
39
+ message: "Embedding provider: #{provider_name}, dimensions: #{provider.dimensions}",
40
+ details: {
41
+ provider: provider_name,
42
+ dimensions: provider.dimensions,
43
+ fastembed_loadable: fastembed_loadable?
44
+ },
45
+ warnings: warnings
46
+ }
47
+ rescue => e
48
+ {
49
+ status: :warning,
50
+ label: "embeddings",
51
+ message: "Embedding provider check failed: #{e.message}",
52
+ details: {},
53
+ warnings: []
54
+ }
55
+ end
56
+
57
+ private
58
+
59
+ def fastembed_loadable?
60
+ return @fastembed_loadable if defined?(@fastembed_loadable)
61
+ @fastembed_loadable = begin
62
+ require "fastembed"
63
+ true
64
+ rescue LoadError
65
+ false
66
+ end
67
+ end
68
+
69
+ def check_dimension_alignment(provider)
70
+ config = Configuration.new
71
+ mismatches = []
72
+
73
+ [config.global_db_path, config.project_db_path].each do |db_path|
74
+ next unless File.exist?(db_path)
75
+
76
+ store = nil
77
+ begin
78
+ store = Store::SQLiteStore.new(db_path)
79
+ result = Embeddings::DimensionCheck.call(store, provider)
80
+ next unless result.status == :mismatch
81
+
82
+ mismatches << "Dimension mismatch in #{File.basename(File.dirname(db_path))} DB: " \
83
+ "stored=#{result.stored} but current provider produces #{result.current}. " \
84
+ "Run 'claude-memory index --force' to re-embed under the current provider."
85
+ rescue => e
86
+ ClaudeMemory.logger.debug("EmbeddingsCheck dimension check failed for #{db_path}: #{e.message}")
87
+ ensure
88
+ store&.close
89
+ end
90
+ end
91
+
92
+ mismatches
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -36,7 +36,8 @@ module ClaudeMemory
36
36
  manager.ensure_global! if manager.global_exists?
37
37
  manager.ensure_project! if manager.project_exists?
38
38
 
39
- stdout.puts "Starting ClaudeMemory dashboard on http://localhost:#{opts[:port]}"
39
+ stdout.puts "Starting ClaudeMemory dashboard on http://127.0.0.1:#{opts[:port]}"
40
+ stdout.puts "OTel receiver listening at http://127.0.0.1:#{opts[:port]}/v1/{metrics,logs,traces}"
40
41
  stdout.puts "Press Ctrl+C to stop."
41
42
 
42
43
  server = Dashboard::Server.new(
@@ -23,6 +23,7 @@ module ClaudeMemory
23
23
  Checks::DistillCheck.new(manager.global_db_path, "global"),
24
24
  Checks::DistillCheck.new(manager.project_db_path, "project"),
25
25
  Checks::VecCheck.new,
26
+ Checks::EmbeddingsCheck.new,
26
27
  Checks::SnapshotCheck.new,
27
28
  Checks::ClaudeMdCheck.new,
28
29
  Checks::HooksCheck.new
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "optparse"
5
+
6
+ module ClaudeMemory
7
+ module Commands
8
+ # Imports Claude Code auto-memory files (~/.claude/projects/<slug>/memory/*.md)
9
+ # into the project database as durable facts. Before this command, those
10
+ # markdown files were only surfaced transiently via `Hook::AutoMemoryMirror`
11
+ # at SessionStart — they were invisible to `memory.recall` and the
12
+ # shortcut tools. Importing them as facts (predicate=convention for
13
+ # gotcha/feedback/project files, predicate=reference for reference
14
+ # type) makes that knowledge first-class queryable knowledge.
15
+ #
16
+ # Idempotent on object_literal prefix: re-running skips files whose
17
+ # body text is already present.
18
+ class ImportAutoMemoryCommand < BaseCommand
19
+ def call(args)
20
+ opts = parse_opts(args)
21
+ return 1 if opts.nil?
22
+
23
+ auto_dir = resolve_auto_dir
24
+ files = list_files(auto_dir)
25
+ if files.empty?
26
+ stdout.puts "No auto-memory files found in #{auto_dir}"
27
+ return 0
28
+ end
29
+
30
+ manager = Store::StoreManager.new
31
+ manager.ensure_project!
32
+ store = manager.project_store
33
+
34
+ imported = 0
35
+ skipped = 0
36
+ files.each do |path|
37
+ fact_data = parse_file(path)
38
+ next if fact_data.nil?
39
+
40
+ if already_imported?(store, fact_data[:object_literal])
41
+ skipped += 1
42
+ next
43
+ end
44
+
45
+ if opts[:dry_run]
46
+ stdout.puts "[DRY] #{File.basename(path)} → #{fact_data[:predicate]}"
47
+ else
48
+ insert(store, fact_data, path)
49
+ stdout.puts "Imported: #{File.basename(path)} → #{fact_data[:predicate]}"
50
+ end
51
+ imported += 1
52
+ end
53
+
54
+ stdout.puts ""
55
+ verb = opts[:dry_run] ? "Would import" : "Imported"
56
+ stdout.puts "#{verb}: #{imported} Skipped (already present): #{skipped}"
57
+ manager.close
58
+ 0
59
+ end
60
+
61
+ private
62
+
63
+ def parse_opts(args)
64
+ options = {dry_run: false}
65
+ parser = OptionParser.new do |o|
66
+ o.banner = "Usage: claude-memory import-auto-memory [--dry-run]"
67
+ o.on("--dry-run", "Show what would be imported without writing") { options[:dry_run] = true }
68
+ end
69
+ parser.parse!(args.dup)
70
+ options
71
+ rescue OptionParser::InvalidOption => e
72
+ stderr.puts e.message
73
+ nil
74
+ end
75
+
76
+ def resolve_auto_dir
77
+ config = Configuration.new
78
+ Hook::AutoMemoryMirror.default_dir(config.project_dir, config.claude_config_dir)
79
+ end
80
+
81
+ def list_files(dir)
82
+ return [] unless Dir.exist?(dir)
83
+ Dir.glob(File.join(dir, "*.md")).reject { |f| File.basename(f) == "MEMORY.md" }.sort
84
+ end
85
+
86
+ def parse_file(path)
87
+ text = File.read(path)
88
+ return nil unless (match = text.match(/\A---\n(.*?)\n---\n(.*)\z/m))
89
+
90
+ frontmatter = match[1]
91
+ body = match[2].strip
92
+
93
+ name = frontmatter[/^name:\s*(.+)/, 1]&.strip
94
+ type = frontmatter[/^type:\s*(.+)/, 1]&.strip
95
+ description = frontmatter[/^description:\s*(.+)/, 1]&.strip
96
+ return nil if name.nil? || type.nil?
97
+
98
+ predicate, subject, scope = map_type(type)
99
+ object = build_object(name, description, body)
100
+
101
+ {
102
+ subject: subject,
103
+ predicate: predicate,
104
+ object_literal: object,
105
+ scope: scope
106
+ }
107
+ end
108
+
109
+ def map_type(type)
110
+ case type
111
+ when "feedback", "user"
112
+ ["convention", "user", "global"]
113
+ when "reference"
114
+ ["reference", "repo", "project"]
115
+ else
116
+ # gotcha, project, anything else
117
+ ["convention", "repo", "project"]
118
+ end
119
+ end
120
+
121
+ # Build an object string that carries a reason clause so
122
+ # BareConclusionDetector does not flag the fact as bare. Auto-memory
123
+ # files conventionally include a **Why:** section; we surface the first
124
+ # 400 chars of the body alongside the name as the object text.
125
+ def build_object(name, description, body)
126
+ first_para = body.split("\n\n").first.to_s.strip
127
+ first_para = first_para[0..400] + "..." if first_para.length > 400
128
+
129
+ parts = [name]
130
+ parts << description if description && !description.empty? && description != name
131
+ parts << first_para unless first_para.empty?
132
+ text = parts.join(" — ").gsub(/\s+/, " ").strip
133
+
134
+ # If no reason clause is present, attach a stable suffix so the fact
135
+ # is structurally distinguishable from bare conclusions.
136
+ text += " (imported from project auto-memory; see source file for full reasoning)" unless reason_present?(text)
137
+ text
138
+ end
139
+
140
+ def reason_present?(text)
141
+ text.match?(/\b(because|so that|so the|so we|in order to|to avoid|to ensure|to prevent|prevents|otherwise|caused by|breaks when)\b/i)
142
+ end
143
+
144
+ def already_imported?(store, object_text)
145
+ needle = object_text[0..80].gsub(/[%_]/) { |c| "\\#{c}" }
146
+ !store.facts.where(Sequel.like(:object_literal, "#{needle}%")).limit(1).all.empty?
147
+ end
148
+
149
+ def insert(store, fact_data, path)
150
+ store.db.transaction do
151
+ subject_type = (fact_data[:subject] == "user") ? "person" : "repo"
152
+ subject_id = store.find_or_create_entity(type: subject_type, name: fact_data[:subject])
153
+
154
+ fact_id = store.insert_fact(
155
+ subject_entity_id: subject_id,
156
+ predicate: fact_data[:predicate],
157
+ object_literal: fact_data[:object_literal],
158
+ scope: fact_data[:scope],
159
+ project_path: (fact_data[:scope] == "global") ? nil : Configuration.new.project_dir
160
+ )
161
+
162
+ content_id = store.upsert_content_item(
163
+ source: "auto_memory_import",
164
+ session_id: nil,
165
+ text_hash: Digest::SHA256.hexdigest(path + fact_data[:object_literal]),
166
+ byte_len: fact_data[:object_literal].bytesize,
167
+ raw_text: fact_data[:object_literal]
168
+ )
169
+
170
+ store.insert_provenance(
171
+ fact_id: fact_id,
172
+ content_item_id: content_id,
173
+ quote: fact_data[:object_literal][0..200],
174
+ strength: "stated"
175
+ )
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "net/http"
5
+ require "uri"
6
+ require "json"
7
+
8
+ module ClaudeMemory
9
+ module Commands
10
+ # CLI shell for the OTel ingestion feature. Subcommands flip flags in
11
+ # `.claude/settings.json` (delegated to OTel::SettingsWriter) or report
12
+ # status (delegated to OTel::Status). The command itself contains no
13
+ # domain logic.
14
+ #
15
+ # claude-memory otel # default --status
16
+ # claude-memory otel --status
17
+ # claude-memory otel --enable
18
+ # claude-memory otel --disable
19
+ # claude-memory otel --enable-traces
20
+ # claude-memory otel --disable-traces
21
+ # claude-memory otel --capture-prompts # opt-in: OTEL_LOG_USER_PROMPTS=1
22
+ # claude-memory otel --no-capture-prompts
23
+ # claude-memory otel --verify # POST a fixture and confirm round-trip
24
+ class OtelCommand < BaseCommand
25
+ def call(args)
26
+ opts = parse_options(args, default_options) do |o|
27
+ OptionParser.new do |parser|
28
+ parser.banner = "Usage: claude-memory otel [options]"
29
+ parser.on("--status", "Show ingestion status (default)") { o[:action] = :status }
30
+ parser.on("--enable", "Configure Claude Code to export telemetry") { o[:action] = :enable }
31
+ parser.on("--disable", "Remove telemetry env from settings.json") { o[:action] = :disable }
32
+ parser.on("--enable-traces", "Opt in to trace ingestion") { o[:action] = :enable_traces }
33
+ parser.on("--disable-traces", "Tell Claude Code to skip trace export") { o[:action] = :disable_traces }
34
+ parser.on("--capture-prompts", "Opt in to OTEL_LOG_USER_PROMPTS=1") { o[:action] = :capture_prompts }
35
+ parser.on("--no-capture-prompts", "Stop capturing prompt content") { o[:action] = :disable_capture_prompts }
36
+ parser.on("--verify", "Send a sample envelope and confirm it persisted") { o[:action] = :verify }
37
+ parser.on("--backfill", "Tag historical activity_events from prior OTel events") { o[:action] = :backfill }
38
+ parser.on("--port PORT", Integer, "Receiver port for the dashboard (default 3377)") { |v| o[:port] = v }
39
+ parser.on("--batch-size N", Integer, "Backfill batch size (default 500)") { |v| o[:batch_size] = v }
40
+ end
41
+ end
42
+ return 1 if opts.nil?
43
+
44
+ case opts[:action]
45
+ when :enable then run_enable(opts)
46
+ when :disable then run_disable(opts)
47
+ when :enable_traces then run_settings_change(opts) { |w| w.enable_traces! }
48
+ when :disable_traces then run_settings_change(opts) { |w| w.disable_traces! }
49
+ when :capture_prompts then run_capture_prompts(opts)
50
+ when :disable_capture_prompts then run_settings_change(opts) { |w| w.disable_capture_prompts! }
51
+ when :verify then run_verify(opts)
52
+ when :backfill then run_backfill(opts)
53
+ else run_status(opts)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ DEFAULT_BACKFILL_BATCH_SIZE = 500
60
+
61
+ def default_options
62
+ {action: :status, port: OTel::SettingsWriter::DEFAULT_PORT, batch_size: DEFAULT_BACKFILL_BATCH_SIZE}
63
+ end
64
+
65
+ def writer(opts)
66
+ OTel::SettingsWriter.new(claude_dir, port: opts[:port])
67
+ end
68
+
69
+ def claude_dir
70
+ File.join(ClaudeMemory::Configuration.new.project_dir, ".claude")
71
+ end
72
+
73
+ def run_status(opts)
74
+ manager = Store::StoreManager.new
75
+ manager.ensure_global! if manager.global_exists?
76
+ store = manager.global_store
77
+ status = OTel::Status.new(store, settings_writer: writer(opts)).snapshot
78
+
79
+ stdout.puts "OTel telemetry status:"
80
+ stdout.puts " metrics ingested: #{status[:metric_count]}"
81
+ stdout.puts " events ingested: #{status[:event_count]}"
82
+ stdout.puts " traces ingested: #{status[:trace_count]} (enabled: #{status[:traces_enabled]})"
83
+ stdout.puts " last metric: #{status[:last_metric_at] || "never"}"
84
+ stdout.puts " configured endpoint: #{status[:endpoint] || "(not configured — run --enable)"}"
85
+ manager.close
86
+ 0
87
+ end
88
+
89
+ def run_enable(opts)
90
+ result = writer(opts).enable!
91
+ return failure("could not enable telemetry: #{result.error}") if result.failure?
92
+ stdout.puts "Enabled OTel telemetry. Restart any active claude sessions for changes to take effect."
93
+ stdout.puts " endpoint: http://127.0.0.1:#{opts[:port]}"
94
+ stdout.puts " protocol: http/json"
95
+ stdout.puts ""
96
+ stdout.puts "Traces are off by default. Opt in with `claude-memory otel --enable-traces`."
97
+ 0
98
+ end
99
+
100
+ def run_disable(opts)
101
+ result = writer(opts).disable!
102
+ return failure("could not disable telemetry: #{result.error}") if result.failure?
103
+ stdout.puts "Disabled OTel telemetry. Removed env keys from settings.json."
104
+ 0
105
+ end
106
+
107
+ def run_settings_change(opts)
108
+ result = yield(writer(opts))
109
+ return failure("settings update failed: #{result.error}") if result.failure?
110
+ stdout.puts "Settings updated:"
111
+ result.value.each { |key, value| stdout.puts " #{key}=#{value}" }
112
+ 0
113
+ end
114
+
115
+ def run_capture_prompts(opts)
116
+ stdout.puts "WARNING: enabling OTEL_LOG_USER_PROMPTS=1 will cause Claude Code"
117
+ stdout.puts "to send your verbatim prompts (and the conversation history) to"
118
+ stdout.puts "this dashboard's local SQLite. Type 'yes' to continue."
119
+ stdout.print "> "
120
+ answer = stdin.gets&.strip
121
+ unless answer == "yes"
122
+ stdout.puts "Aborted. No changes made."
123
+ return 0
124
+ end
125
+ run_settings_change(opts) { |w| w.capture_prompts! }
126
+ end
127
+
128
+ # One-time pass: stream every otel_events row with a prompt_id from
129
+ # the global store and run it through PromptScope so historical
130
+ # activity_events get tagged. Subsequent runs are cheap because
131
+ # PromptScope is idempotent (already-tagged rows are excluded by
132
+ # the `prompt_id: nil` filter).
133
+ def run_backfill(opts)
134
+ manager = Store::StoreManager.new
135
+ unless manager.global_exists?
136
+ return failure("Global memory DB not found — nothing to backfill against.")
137
+ end
138
+ manager.ensure_global!
139
+ manager.ensure_project! if manager.project_exists?
140
+
141
+ store = manager.global_store
142
+ unless store.db.table_exists?(:otel_events)
143
+ manager.close
144
+ return failure("otel_events table missing — run `claude-memory dashboard` first to migrate.")
145
+ end
146
+
147
+ total_events = store.otel_events.exclude(prompt_id: nil).count
148
+ if total_events.zero?
149
+ stdout.puts "No OTel events with prompt_id to backfill from."
150
+ manager.close
151
+ return 0
152
+ end
153
+
154
+ stdout.puts "Backfilling #{total_events} OTel event(s) into activity_events…"
155
+ scope = OTel::PromptScope.new(manager)
156
+ tagged = groups = batches = 0
157
+ offset = 0
158
+ batch_size = opts[:batch_size]
159
+
160
+ loop do
161
+ rows = store.otel_events
162
+ .exclude(prompt_id: nil)
163
+ .order(:occurred_at, :id)
164
+ .limit(batch_size)
165
+ .offset(offset)
166
+ .select(:event_name, :session_id, :prompt_id, :occurred_at)
167
+ .all
168
+ break if rows.empty?
169
+
170
+ events = rows.map { |r|
171
+ {event_name: r[:event_name], session_id: r[:session_id],
172
+ prompt_id: r[:prompt_id], occurred_at: r[:occurred_at]}
173
+ }
174
+ result = scope.tag(events)
175
+ tagged += result[:tagged].to_i
176
+ groups += result[:groups].to_i
177
+ batches += 1
178
+ offset += rows.size
179
+ stdout.print "."
180
+ stdout.flush
181
+ end
182
+
183
+ stdout.puts ""
184
+ stdout.puts "Backfill complete: tagged #{tagged} activity_event(s) across #{groups} prompt group(s) in #{batches} batch(es)."
185
+ manager.close
186
+ 0
187
+ end
188
+
189
+ def run_verify(opts)
190
+ url = URI.parse("http://127.0.0.1:#{opts[:port]}/v1/metrics")
191
+ body = JSON.generate(sample_metrics_envelope)
192
+ response = post_json(url, body)
193
+ if response.is_a?(Net::HTTPSuccess)
194
+ stdout.puts "Verify OK: dashboard accepted the sample metrics envelope."
195
+ 0
196
+ else
197
+ failure("Verify failed: dashboard returned #{response&.code || "no response"}")
198
+ end
199
+ rescue Errno::ECONNREFUSED, SocketError => e
200
+ failure("Verify failed: #{e.message}. Is the dashboard running on port #{opts[:port]}?")
201
+ end
202
+
203
+ def post_json(url, body)
204
+ Net::HTTP.start(url.host, url.port, open_timeout: 2, read_timeout: 5) do |http|
205
+ req = Net::HTTP::Post.new(url.path, "Content-Type" => "application/json")
206
+ req.body = body
207
+ http.request(req)
208
+ end
209
+ end
210
+
211
+ # Smallest valid OTLP/HTTP/JSON metrics envelope — one counter point.
212
+ # Used only for the --verify subcommand so users can confirm
213
+ # end-to-end wiring without running a real claude session.
214
+ def sample_metrics_envelope
215
+ nano = (Time.now.to_f * 1_000_000_000).to_i.to_s
216
+ {
217
+ "resourceMetrics" => [{
218
+ "resource" => {"attributes" => [
219
+ {"key" => "service.name", "value" => {"stringValue" => "claude-memory-verify"}}
220
+ ]},
221
+ "scopeMetrics" => [{
222
+ "scope" => {"name" => "claude-memory.verify"},
223
+ "metrics" => [{
224
+ "name" => "claude_memory.verify",
225
+ "unit" => "count",
226
+ "sum" => {
227
+ "dataPoints" => [{
228
+ "asInt" => "1",
229
+ "timeUnixNano" => nano,
230
+ "attributes" => []
231
+ }]
232
+ }
233
+ }]
234
+ }]
235
+ }]
236
+ }
237
+ end
238
+ end
239
+ end
240
+ end
@@ -43,7 +43,11 @@ module ClaudeMemory
43
43
  "census" => {class: CensusCommand, description: "Aggregate predicate usage across project databases"},
44
44
  "dashboard" => {class: DashboardCommand, description: "Open debugging dashboard"},
45
45
  "digest" => {class: DigestCommand, description: "Render a weekly markdown digest of memory activity"},
46
- "show" => {class: ShowCommand, description: "Print what memory would inject at the next SessionStart"}
46
+ "show" => {class: ShowCommand, description: "Print what memory would inject at the next SessionStart"},
47
+ "otel" => {class: OtelCommand, description: "Configure or inspect OpenTelemetry ingestion from Claude Code"},
48
+ "setup-vectors" => {class: SetupVectorsCommand, description: "Opt into vector recall — write provider env to .claude/settings.json + re-index"},
49
+ "import-auto-memory" => {class: ImportAutoMemoryCommand, description: "Import Claude Code auto-memory .md files into project DB as facts"},
50
+ "audit" => {class: AuditCommand, description: "Run memory health audit; report inconsistencies and optimizations"}
47
51
  }.freeze
48
52
 
49
53
  # Find a command class by name