claude_memory 0.1.0 → 0.2.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/.mind.mv2.aLCUZd +0 -0
  3. data/.claude/memory.sqlite3 +0 -0
  4. data/.claude/rules/claude_memory.generated.md +7 -1
  5. data/.claude/settings.json +0 -4
  6. data/.claude/settings.local.json +4 -1
  7. data/.claude-plugin/plugin.json +1 -1
  8. data/.claude.json +11 -0
  9. data/.ruby-version +1 -0
  10. data/CHANGELOG.md +62 -11
  11. data/CLAUDE.md +87 -24
  12. data/README.md +76 -159
  13. data/docs/EXAMPLES.md +436 -0
  14. data/docs/RELEASE_NOTES_v0.2.0.md +179 -0
  15. data/docs/RUBY_COMMUNITY_POST_v0.2.0.md +582 -0
  16. data/docs/SOCIAL_MEDIA_v0.2.0.md +420 -0
  17. data/docs/architecture.md +360 -0
  18. data/docs/expert_review.md +1718 -0
  19. data/docs/feature_adoption_plan.md +1241 -0
  20. data/docs/feature_adoption_plan_revised.md +2374 -0
  21. data/docs/improvements.md +1325 -0
  22. data/docs/quality_review.md +1544 -0
  23. data/docs/review_summary.md +480 -0
  24. data/lefthook.yml +10 -0
  25. data/lib/claude_memory/cli.rb +16 -844
  26. data/lib/claude_memory/commands/base_command.rb +95 -0
  27. data/lib/claude_memory/commands/changes_command.rb +39 -0
  28. data/lib/claude_memory/commands/conflicts_command.rb +37 -0
  29. data/lib/claude_memory/commands/db_init_command.rb +40 -0
  30. data/lib/claude_memory/commands/doctor_command.rb +147 -0
  31. data/lib/claude_memory/commands/explain_command.rb +65 -0
  32. data/lib/claude_memory/commands/help_command.rb +37 -0
  33. data/lib/claude_memory/commands/hook_command.rb +106 -0
  34. data/lib/claude_memory/commands/ingest_command.rb +47 -0
  35. data/lib/claude_memory/commands/init_command.rb +218 -0
  36. data/lib/claude_memory/commands/promote_command.rb +30 -0
  37. data/lib/claude_memory/commands/publish_command.rb +36 -0
  38. data/lib/claude_memory/commands/recall_command.rb +61 -0
  39. data/lib/claude_memory/commands/registry.rb +55 -0
  40. data/lib/claude_memory/commands/search_command.rb +43 -0
  41. data/lib/claude_memory/commands/serve_mcp_command.rb +16 -0
  42. data/lib/claude_memory/commands/sweep_command.rb +36 -0
  43. data/lib/claude_memory/commands/version_command.rb +13 -0
  44. data/lib/claude_memory/configuration.rb +38 -0
  45. data/lib/claude_memory/core/fact_id.rb +41 -0
  46. data/lib/claude_memory/core/null_explanation.rb +47 -0
  47. data/lib/claude_memory/core/null_fact.rb +30 -0
  48. data/lib/claude_memory/core/result.rb +143 -0
  49. data/lib/claude_memory/core/session_id.rb +37 -0
  50. data/lib/claude_memory/core/token_estimator.rb +33 -0
  51. data/lib/claude_memory/core/transcript_path.rb +37 -0
  52. data/lib/claude_memory/domain/conflict.rb +51 -0
  53. data/lib/claude_memory/domain/entity.rb +51 -0
  54. data/lib/claude_memory/domain/fact.rb +70 -0
  55. data/lib/claude_memory/domain/provenance.rb +48 -0
  56. data/lib/claude_memory/hook/exit_codes.rb +18 -0
  57. data/lib/claude_memory/hook/handler.rb +7 -2
  58. data/lib/claude_memory/index/index_query.rb +89 -0
  59. data/lib/claude_memory/index/index_query_logic.rb +41 -0
  60. data/lib/claude_memory/index/query_options.rb +67 -0
  61. data/lib/claude_memory/infrastructure/file_system.rb +29 -0
  62. data/lib/claude_memory/infrastructure/in_memory_file_system.rb +32 -0
  63. data/lib/claude_memory/ingest/content_sanitizer.rb +42 -0
  64. data/lib/claude_memory/ingest/ingester.rb +3 -0
  65. data/lib/claude_memory/ingest/privacy_tag.rb +48 -0
  66. data/lib/claude_memory/mcp/tools.rb +174 -1
  67. data/lib/claude_memory/publish.rb +29 -20
  68. data/lib/claude_memory/recall.rb +164 -16
  69. data/lib/claude_memory/resolve/resolver.rb +41 -37
  70. data/lib/claude_memory/shortcuts.rb +56 -0
  71. data/lib/claude_memory/store/store_manager.rb +35 -32
  72. data/lib/claude_memory/templates/hooks.example.json +0 -4
  73. data/lib/claude_memory/version.rb +1 -1
  74. data/lib/claude_memory.rb +59 -21
  75. metadata +55 -1
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module ClaudeMemory
6
+ module Commands
7
+ # Base class for all CLI commands.
8
+ # Provides consistent interface for commands with I/O isolation for testing.
9
+ #
10
+ # @example Implementing a command
11
+ # class MyCommand < BaseCommand
12
+ # def call(args)
13
+ # opts = parse_options(args, {verbose: false}) do |o|
14
+ # OptionParser.new do |parser|
15
+ # parser.on("-v", "--verbose") { o[:verbose] = true }
16
+ # end
17
+ # end
18
+ # return 1 if opts.nil?
19
+ #
20
+ # # ... command logic ...
21
+ # success("Done!")
22
+ # end
23
+ # end
24
+ #
25
+ # @example Testing a command
26
+ # stdout = StringIO.new
27
+ # stderr = StringIO.new
28
+ # command = MyCommand.new(stdout: stdout, stderr: stderr)
29
+ # exit_code = command.call(["--verbose"])
30
+ # expect(exit_code).to eq(0)
31
+ # expect(stdout.string).to include("Done!")
32
+ class BaseCommand
33
+ attr_reader :stdout, :stderr, :stdin
34
+
35
+ # @param stdout [IO] output stream (default: $stdout)
36
+ # @param stderr [IO] error stream (default: $stderr)
37
+ # @param stdin [IO] input stream (default: $stdin)
38
+ def initialize(stdout: $stdout, stderr: $stderr, stdin: $stdin)
39
+ @stdout = stdout
40
+ @stderr = stderr
41
+ @stdin = stdin
42
+ end
43
+
44
+ # Execute the command with given arguments
45
+ # @param args [Array<String>] command line arguments
46
+ # @return [Integer] exit code (0 for success, non-zero for failure)
47
+ def call(args)
48
+ raise NotImplementedError, "Subclass must implement #call"
49
+ end
50
+
51
+ protected
52
+
53
+ # Report successful command execution
54
+ # @param message [String, nil] optional success message
55
+ # @param exit_code [Integer] exit code (default: 0)
56
+ # @return [Integer] the exit code
57
+ def success(message = nil, exit_code: 0)
58
+ stdout.puts(message) if message
59
+ exit_code
60
+ end
61
+
62
+ # Report failed command execution
63
+ # @param message [String] error message
64
+ # @param exit_code [Integer] exit code (default: 1)
65
+ # @return [Integer] the exit code
66
+ def failure(message, exit_code: 1)
67
+ stderr.puts(message)
68
+ exit_code
69
+ end
70
+
71
+ # Parse command line options with error handling
72
+ # @param args [Array<String>] command line arguments
73
+ # @param defaults [Hash] default option values
74
+ # @yield [Hash] yields the options hash to configure the parser
75
+ # @return [Hash, nil] parsed options, or nil if parsing failed
76
+ #
77
+ # @example
78
+ # opts = parse_options(args, {verbose: false}) do |o|
79
+ # OptionParser.new do |parser|
80
+ # parser.on("-v", "--verbose") { o[:verbose] = true }
81
+ # parser.on("--name NAME") { |v| o[:name] = v }
82
+ # end
83
+ # end
84
+ def parse_options(args, defaults = {})
85
+ opts = defaults.dup
86
+ parser = yield(opts)
87
+ parser.parse(args)
88
+ opts
89
+ rescue OptionParser::InvalidOption => e
90
+ failure(e.message)
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Shows recent fact changes
6
+ class ChangesCommand < BaseCommand
7
+ def call(args)
8
+ opts = parse_options(args, {since: nil, limit: 20, scope: "all"}) do |o|
9
+ OptionParser.new do |parser|
10
+ parser.on("--since ISO", "Since timestamp") { |v| o[:since] = v }
11
+ parser.on("--limit N", Integer, "Max results") { |v| o[:limit] = v }
12
+ parser.on("--scope SCOPE", "Scope: project, global, or all") { |v| o[:scope] = v }
13
+ end
14
+ end
15
+ return 1 if opts.nil?
16
+
17
+ opts[:since] ||= (Time.now - 86400 * 7).utc.iso8601
18
+
19
+ manager = ClaudeMemory::Store::StoreManager.new
20
+ recall = ClaudeMemory::Recall.new(manager)
21
+
22
+ changes = recall.changes(since: opts[:since], limit: opts[:limit], scope: opts[:scope])
23
+ if changes.empty?
24
+ stdout.puts "No changes since #{opts[:since]}."
25
+ else
26
+ stdout.puts "Changes since #{opts[:since]} (#{changes.size}):\n\n"
27
+ changes.each do |change|
28
+ source_label = change[:source] ? " [#{change[:source]}]" : ""
29
+ stdout.puts " [#{change[:id]}] #{change[:predicate]}: #{change[:object_literal]} (#{change[:status]})#{source_label}"
30
+ stdout.puts " Created: #{change[:created_at]}"
31
+ end
32
+ end
33
+
34
+ manager.close
35
+ 0
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Shows open conflicts in memory
6
+ class ConflictsCommand < BaseCommand
7
+ def call(args)
8
+ opts = parse_options(args, {scope: "all"}) do |o|
9
+ OptionParser.new do |parser|
10
+ parser.on("--scope SCOPE", "Scope: project, global, or all") { |v| o[:scope] = v }
11
+ end
12
+ end
13
+ return 1 if opts.nil?
14
+
15
+ manager = ClaudeMemory::Store::StoreManager.new
16
+ recall = ClaudeMemory::Recall.new(manager)
17
+ conflicts = recall.conflicts(scope: opts[:scope])
18
+
19
+ if conflicts.empty?
20
+ stdout.puts "No open conflicts."
21
+ else
22
+ stdout.puts "Open conflicts (#{conflicts.size}):\n\n"
23
+ conflicts.each do |c|
24
+ source_label = c[:source] ? " [#{c[:source]}]" : ""
25
+ stdout.puts " Conflict ##{c[:id]}: Fact #{c[:fact_a_id]} vs Fact #{c[:fact_b_id]}#{source_label}"
26
+ stdout.puts " Status: #{c[:status]}, Detected: #{c[:detected_at]}"
27
+ stdout.puts " Notes: #{c[:notes]}" if c[:notes]
28
+ stdout.puts
29
+ end
30
+ end
31
+
32
+ manager.close
33
+ 0
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Initializes SQLite databases
6
+ class DbInitCommand < BaseCommand
7
+ def call(args)
8
+ opts = parse_options(args, {global: false, project: false}) do |o|
9
+ OptionParser.new do |parser|
10
+ parser.banner = "Usage: claude-memory db:init [options]"
11
+ parser.on("--global", "Initialize global database (~/.claude/memory.sqlite3)") { o[:global] = true }
12
+ parser.on("--project", "Initialize project database (.claude/memory.sqlite3)") { o[:project] = true }
13
+ end
14
+ end
15
+ return 1 if opts.nil?
16
+
17
+ # If neither flag specified, initialize both
18
+ opts[:global] = true if !opts[:global] && !opts[:project]
19
+ opts[:project] = true if !opts[:global] && !opts[:project]
20
+
21
+ manager = ClaudeMemory::Store::StoreManager.new
22
+
23
+ if opts[:global]
24
+ manager.ensure_global!
25
+ stdout.puts "Global database initialized at #{manager.global_db_path}"
26
+ stdout.puts "Schema version: #{manager.global_store.schema_version}"
27
+ end
28
+
29
+ if opts[:project]
30
+ manager.ensure_project!
31
+ stdout.puts "Project database initialized at #{manager.project_db_path}"
32
+ stdout.puts "Schema version: #{manager.project_store.schema_version}"
33
+ end
34
+
35
+ manager.close
36
+ 0
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ClaudeMemory
6
+ module Commands
7
+ # Performs system health checks for ClaudeMemory
8
+ # Checks databases, snapshots, hooks configuration
9
+ class DoctorCommand < BaseCommand
10
+ def call(_args)
11
+ issues = []
12
+ warnings = []
13
+
14
+ stdout.puts "Claude Memory Doctor\n"
15
+ stdout.puts "=" * 40
16
+
17
+ manager = ClaudeMemory::Store::StoreManager.new
18
+
19
+ stdout.puts "\n## Global Database"
20
+ check_database(manager.global_db_path, "global", issues, warnings)
21
+
22
+ stdout.puts "\n## Project Database"
23
+ check_database(manager.project_db_path, "project", issues, warnings)
24
+
25
+ manager.close
26
+
27
+ check_snapshot(warnings)
28
+ check_claude_md(warnings)
29
+ check_hooks_config(warnings)
30
+
31
+ stdout.puts
32
+
33
+ if warnings.any?
34
+ stdout.puts "Warnings:"
35
+ warnings.each { |w| stdout.puts " ⚠ #{w}" }
36
+ stdout.puts
37
+ end
38
+
39
+ if issues.any?
40
+ stdout.puts "Issues:"
41
+ issues.each { |i| stderr.puts " ✗ #{i}" }
42
+ stdout.puts
43
+ stdout.puts "Run 'claude-memory init' to set up."
44
+ return 1
45
+ end
46
+
47
+ stdout.puts "All checks passed!"
48
+ 0
49
+ end
50
+
51
+ private
52
+
53
+ def check_database(db_path, label, issues, warnings)
54
+ if File.exist?(db_path)
55
+ stdout.puts "✓ #{label.capitalize} database exists: #{db_path}"
56
+ begin
57
+ store = ClaudeMemory::Store::SQLiteStore.new(db_path)
58
+ stdout.puts " Schema version: #{store.schema_version}"
59
+
60
+ fact_count = store.facts.count
61
+ stdout.puts " Facts: #{fact_count}"
62
+
63
+ content_count = store.content_items.count
64
+ stdout.puts " Content items: #{content_count}"
65
+
66
+ conflict_count = store.conflicts.where(status: "open").count
67
+ if conflict_count > 0
68
+ warnings << "#{label}: #{conflict_count} open conflict(s) need resolution"
69
+ end
70
+ stdout.puts " Open conflicts: #{conflict_count}"
71
+
72
+ last_ingest = store.content_items.max(:ingested_at)
73
+ if last_ingest
74
+ stdout.puts " Last ingest: #{last_ingest}"
75
+ elsif label == "project"
76
+ warnings << "#{label}: No content has been ingested yet"
77
+ end
78
+
79
+ store.close
80
+ rescue => e
81
+ issues << "#{label} database error: #{e.message}"
82
+ end
83
+ elsif label == "global"
84
+ issues << "Global database not found: #{db_path}"
85
+ else
86
+ warnings << "Project database not found: #{db_path} (run 'claude-memory init')"
87
+ end
88
+ end
89
+
90
+ def check_snapshot(warnings)
91
+ if File.exist?(".claude/rules/claude_memory.generated.md")
92
+ stdout.puts "✓ Published snapshot exists"
93
+ else
94
+ warnings << "No published snapshot found. Run 'claude-memory publish'"
95
+ end
96
+ end
97
+
98
+ def check_claude_md(warnings)
99
+ if File.exist?(".claude/CLAUDE.md")
100
+ content = File.read(".claude/CLAUDE.md")
101
+ if content.include?("claude_memory.generated.md")
102
+ stdout.puts "✓ CLAUDE.md imports snapshot"
103
+ else
104
+ warnings << "CLAUDE.md does not import snapshot"
105
+ end
106
+ else
107
+ warnings << "No .claude/CLAUDE.md found"
108
+ end
109
+ end
110
+
111
+ def check_hooks_config(warnings)
112
+ settings_path = ".claude/settings.json"
113
+ local_settings_path = ".claude/settings.local.json"
114
+
115
+ hooks_found = false
116
+
117
+ [settings_path, local_settings_path].each do |path|
118
+ next unless File.exist?(path)
119
+
120
+ begin
121
+ config = JSON.parse(File.read(path))
122
+ if config["hooks"]&.any?
123
+ hooks_found = true
124
+ stdout.puts "✓ Hooks configured in #{path}"
125
+
126
+ expected_hooks = %w[Stop SessionStart PreCompact SessionEnd]
127
+ missing = expected_hooks - config["hooks"].keys
128
+ if missing.any?
129
+ warnings << "Missing recommended hooks in #{path}: #{missing.join(", ")}"
130
+ end
131
+ end
132
+ rescue JSON::ParserError
133
+ warnings << "Invalid JSON in #{path}"
134
+ end
135
+ end
136
+
137
+ unless hooks_found
138
+ warnings << "No hooks configured. Run 'claude-memory init' or configure manually."
139
+ stdout.puts "\n Manual fallback available:"
140
+ stdout.puts " claude-memory ingest --session-id <id> --transcript-path <path>"
141
+ stdout.puts " claude-memory sweep --budget 5"
142
+ stdout.puts " claude-memory publish"
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Explains a fact with provenance and relationships
6
+ class ExplainCommand < BaseCommand
7
+ def call(args)
8
+ fact_id = args.first&.to_i
9
+ unless fact_id && fact_id > 0
10
+ stderr.puts "Usage: claude-memory explain <fact_id> [--scope project|global]"
11
+ return 1
12
+ end
13
+
14
+ opts = parse_options(args[1..] || [], {scope: "project"}) do |o|
15
+ OptionParser.new do |parser|
16
+ parser.on("--scope SCOPE", "Scope: project or global") { |v| o[:scope] = v }
17
+ end
18
+ end
19
+ return 1 if opts.nil?
20
+
21
+ manager = ClaudeMemory::Store::StoreManager.new
22
+ recall = ClaudeMemory::Recall.new(manager)
23
+
24
+ explanation = recall.explain(fact_id, scope: opts[:scope])
25
+ if explanation.is_a?(ClaudeMemory::Core::NullExplanation)
26
+ stderr.puts "Fact #{fact_id} not found in #{opts[:scope]} database."
27
+ manager.close
28
+ return 1
29
+ end
30
+
31
+ stdout.puts "Fact ##{fact_id} (#{opts[:scope]}):"
32
+ print_fact(explanation[:fact])
33
+ print_receipts(explanation[:receipts])
34
+
35
+ if explanation[:supersedes].any?
36
+ stdout.puts " Supersedes: #{explanation[:supersedes].join(", ")}"
37
+ end
38
+ if explanation[:superseded_by].any?
39
+ stdout.puts " Superseded by: #{explanation[:superseded_by].join(", ")}"
40
+ end
41
+ if explanation[:conflicts].any?
42
+ stdout.puts " Conflicts: #{explanation[:conflicts].map { |c| c[:id] }.join(", ")}"
43
+ end
44
+
45
+ manager.close
46
+ 0
47
+ end
48
+
49
+ private
50
+
51
+ def print_fact(fact)
52
+ stdout.puts " #{fact[:predicate]}: #{fact[:object_literal]}"
53
+ stdout.puts " Status: #{fact[:status]}, Confidence: #{fact[:confidence]}"
54
+ end
55
+
56
+ def print_receipts(receipts)
57
+ return if receipts.empty?
58
+ stdout.puts " Receipts (#{receipts.size}):"
59
+ receipts.each do |r|
60
+ stdout.puts " - #{r[:quote] || "(no quote)"}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Displays help information for claude-memory CLI
6
+ class HelpCommand < BaseCommand
7
+ def call(_args)
8
+ stdout.puts <<~HELP
9
+ claude-memory - Long-term memory for Claude Code
10
+
11
+ Usage: claude-memory <command> [options]
12
+
13
+ Commands:
14
+ changes Show recent fact changes
15
+ conflicts Show open conflicts
16
+ db:init Initialize the SQLite database
17
+ doctor Check system health
18
+ explain Explain a fact with receipts
19
+ help Show this help message
20
+ hook Run hook entrypoints (ingest|sweep|publish)
21
+ init Initialize ClaudeMemory in a project
22
+ ingest Ingest transcript delta
23
+ promote Promote a project fact to global memory
24
+ publish Publish snapshot to Claude Code memory
25
+ recall Recall facts matching a query
26
+ search Search indexed content
27
+ serve-mcp Start MCP server
28
+ sweep Run maintenance/pruning
29
+ version Show version number
30
+
31
+ Run 'claude-memory <command> --help' for more information on a command.
32
+ HELP
33
+ 0
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ClaudeMemory
6
+ module Commands
7
+ # Handles hook entrypoints (ingest, sweep, publish)
8
+ class HookCommand < BaseCommand
9
+ def call(args)
10
+ subcommand = args.first
11
+
12
+ unless subcommand
13
+ stderr.puts "Usage: claude-memory hook <ingest|sweep|publish> [options]"
14
+ stderr.puts "\nReads hook payload JSON from stdin."
15
+ return Hook::ExitCodes::ERROR
16
+ end
17
+
18
+ unless %w[ingest sweep publish].include?(subcommand)
19
+ stderr.puts "Unknown hook command: #{subcommand}"
20
+ stderr.puts "Available: ingest, sweep, publish"
21
+ return Hook::ExitCodes::ERROR
22
+ end
23
+
24
+ opts = parse_options(args[1..] || [], {db: ClaudeMemory.project_db_path}) do |o|
25
+ OptionParser.new do |parser|
26
+ parser.on("--db PATH", "Database path") { |v| o[:db] = v }
27
+ end
28
+ end
29
+ return Hook::ExitCodes::ERROR if opts.nil?
30
+
31
+ payload = parse_hook_payload
32
+ return Hook::ExitCodes::ERROR unless payload
33
+
34
+ store = ClaudeMemory::Store::SQLiteStore.new(opts[:db])
35
+ handler = ClaudeMemory::Hook::Handler.new(store)
36
+
37
+ exit_code = case subcommand
38
+ when "ingest"
39
+ hook_ingest(handler, payload)
40
+ when "sweep"
41
+ hook_sweep(handler, payload)
42
+ when "publish"
43
+ hook_publish(handler, payload)
44
+ end
45
+
46
+ store.close
47
+ exit_code
48
+ rescue ClaudeMemory::Hook::Handler::PayloadError => e
49
+ stderr.puts "Payload error: #{e.message}"
50
+ Hook::ExitCodes::ERROR
51
+ end
52
+
53
+ private
54
+
55
+ def parse_hook_payload
56
+ input = stdin.read
57
+ JSON.parse(input)
58
+ rescue JSON::ParserError => e
59
+ stderr.puts "Invalid JSON payload: #{e.message}"
60
+ nil
61
+ end
62
+
63
+ def hook_ingest(handler, payload)
64
+ result = handler.ingest(payload)
65
+
66
+ case result[:status]
67
+ when :ingested
68
+ stdout.puts "Ingested #{result[:bytes_read]} bytes (content_id: #{result[:content_id]})"
69
+ Hook::ExitCodes::SUCCESS
70
+ when :no_change
71
+ stdout.puts "No new content to ingest"
72
+ Hook::ExitCodes::SUCCESS
73
+ when :skipped
74
+ stdout.puts "Skipped ingestion: #{result[:reason]}"
75
+ Hook::ExitCodes::WARNING
76
+ else
77
+ Hook::ExitCodes::ERROR
78
+ end
79
+ end
80
+
81
+ def hook_sweep(handler, payload)
82
+ result = handler.sweep(payload)
83
+ stats = result[:stats]
84
+
85
+ stdout.puts "Sweep complete:"
86
+ stdout.puts " Elapsed: #{stats[:elapsed_seconds].round(2)}s"
87
+ stdout.puts " Budget honored: #{stats[:budget_honored]}"
88
+
89
+ Hook::ExitCodes::SUCCESS
90
+ end
91
+
92
+ def hook_publish(handler, payload)
93
+ result = handler.publish(payload)
94
+
95
+ case result[:status]
96
+ when :updated
97
+ stdout.puts "Published snapshot to #{result[:path]}"
98
+ when :unchanged
99
+ stdout.puts "No changes - #{result[:path]} is up to date"
100
+ end
101
+
102
+ Hook::ExitCodes::SUCCESS
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Ingests transcript delta into memory database
6
+ class IngestCommand < BaseCommand
7
+ def call(args)
8
+ opts = parse_options(args, {source: "claude_code", db: ClaudeMemory.project_db_path}) do |o|
9
+ OptionParser.new do |parser|
10
+ parser.banner = "Usage: claude-memory ingest [options]"
11
+ parser.on("--source SOURCE", "Source identifier (default: claude_code)") { |v| o[:source] = v }
12
+ parser.on("--session-id ID", "Session identifier (required)") { |v| o[:session_id] = v }
13
+ parser.on("--transcript-path PATH", "Path to transcript file (required)") { |v| o[:transcript_path] = v }
14
+ parser.on("--db PATH", "Database path") { |v| o[:db] = v }
15
+ end
16
+ end
17
+
18
+ unless opts && opts[:session_id] && opts[:transcript_path]
19
+ stderr.puts "\nError: --session-id and --transcript-path are required"
20
+ return 1
21
+ end
22
+
23
+ store = ClaudeMemory::Store::SQLiteStore.new(opts[:db])
24
+ ingester = ClaudeMemory::Ingest::Ingester.new(store)
25
+
26
+ result = ingester.ingest(
27
+ source: opts[:source],
28
+ session_id: opts[:session_id],
29
+ transcript_path: opts[:transcript_path]
30
+ )
31
+
32
+ case result[:status]
33
+ when :ingested
34
+ stdout.puts "Ingested #{result[:bytes_read]} bytes (content_id: #{result[:content_id]})"
35
+ when :no_change
36
+ stdout.puts "No new content to ingest"
37
+ end
38
+
39
+ store.close
40
+ 0
41
+ rescue ClaudeMemory::Ingest::TranscriptReader::FileNotFoundError => e
42
+ stderr.puts "Error: #{e.message}"
43
+ 1
44
+ end
45
+ end
46
+ end
47
+ end