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,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+
6
+ module ClaudeMemory
7
+ module Commands
8
+ # Initializes ClaudeMemory in a project or globally
9
+ class InitCommand < BaseCommand
10
+ def call(args)
11
+ opts = parse_options(args, {global: false}) do |o|
12
+ OptionParser.new do |parser|
13
+ parser.on("--global", "Install to global ~/.claude/ settings") { o[:global] = true }
14
+ end
15
+ end
16
+ return 1 if opts.nil?
17
+
18
+ if opts[:global]
19
+ init_global
20
+ else
21
+ init_local
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def init_local
28
+ stdout.puts "Initializing ClaudeMemory (project-local)...\n\n"
29
+
30
+ manager = ClaudeMemory::Store::StoreManager.new
31
+ manager.ensure_global!
32
+ stdout.puts "✓ Global database: #{manager.global_db_path}"
33
+ manager.ensure_project!
34
+ stdout.puts "✓ Project database: #{manager.project_db_path}"
35
+ manager.close
36
+
37
+ FileUtils.mkdir_p(".claude/rules")
38
+ stdout.puts "✓ Created .claude/rules directory"
39
+
40
+ configure_project_hooks
41
+ configure_project_mcp
42
+ install_output_style
43
+
44
+ stdout.puts "\n=== Setup Complete ===\n"
45
+ stdout.puts "ClaudeMemory is now configured for this project."
46
+ stdout.puts "\nDatabases:"
47
+ stdout.puts " Global: ~/.claude/memory.sqlite3 (user-wide knowledge)"
48
+ stdout.puts " Project: .claude/memory.sqlite3 (project-specific)"
49
+ stdout.puts "\nNext steps:"
50
+ stdout.puts " 1. Restart Claude Code to load the new configuration"
51
+ stdout.puts " 2. Use Claude Code normally - transcripts will be ingested automatically"
52
+ stdout.puts " 3. Run 'claude-memory promote <fact_id>' to move facts to global"
53
+ stdout.puts " 4. Run 'claude-memory doctor' to verify setup"
54
+
55
+ 0
56
+ end
57
+
58
+ def init_global
59
+ stdout.puts "Initializing ClaudeMemory (global only)...\n\n"
60
+
61
+ manager = ClaudeMemory::Store::StoreManager.new
62
+ manager.ensure_global!
63
+ stdout.puts "✓ Created global database: #{manager.global_db_path}"
64
+ manager.close
65
+
66
+ configure_global_hooks
67
+ configure_global_mcp
68
+ configure_global_memory
69
+
70
+ stdout.puts "\n=== Global Setup Complete ===\n"
71
+ stdout.puts "ClaudeMemory is now configured globally."
72
+ stdout.puts "\nNote: Run 'claude-memory init' in each project for project-specific memory."
73
+
74
+ 0
75
+ end
76
+
77
+ def configure_global_hooks
78
+ settings_path = File.join(Dir.home, ".claude", "settings.json")
79
+ FileUtils.mkdir_p(File.dirname(settings_path))
80
+
81
+ db_path = ClaudeMemory.global_db_path
82
+ ingest_cmd = "claude-memory hook ingest --db #{db_path}"
83
+ sweep_cmd = "claude-memory hook sweep --db #{db_path}"
84
+
85
+ hooks_config = build_hooks_config(ingest_cmd, sweep_cmd)
86
+
87
+ existing = load_json_file(settings_path)
88
+ existing["hooks"] ||= {}
89
+ existing["hooks"].merge!(hooks_config["hooks"])
90
+
91
+ File.write(settings_path, JSON.pretty_generate(existing))
92
+ stdout.puts "✓ Configured hooks in #{settings_path}"
93
+ end
94
+
95
+ def configure_global_mcp
96
+ mcp_path = File.join(Dir.home, ".claude.json")
97
+
98
+ existing = load_json_file(mcp_path)
99
+ existing["mcpServers"] ||= {}
100
+ existing["mcpServers"]["claude-memory"] = {
101
+ "type" => "stdio",
102
+ "command" => "claude-memory",
103
+ "args" => ["serve-mcp"]
104
+ }
105
+
106
+ File.write(mcp_path, JSON.pretty_generate(existing))
107
+ stdout.puts "✓ Configured MCP server in #{mcp_path}"
108
+ end
109
+
110
+ def configure_global_memory
111
+ global_claude_dir = File.join(Dir.home, ".claude")
112
+ claude_md_path = File.join(global_claude_dir, "CLAUDE.md")
113
+
114
+ memory_instruction = <<~MD
115
+ # ClaudeMemory
116
+
117
+ ClaudeMemory is installed globally. Use these MCP tools:
118
+ - `memory.recall` - Search for relevant facts
119
+ - `memory.explain` - Get detailed fact provenance
120
+ - `memory.conflicts` - Show open contradictions
121
+ - `memory.status` - Check system health
122
+ MD
123
+
124
+ FileUtils.mkdir_p(global_claude_dir)
125
+ if File.exist?(claude_md_path)
126
+ content = File.read(claude_md_path)
127
+ unless content.include?("ClaudeMemory")
128
+ File.write(claude_md_path, content + "\n\n" + memory_instruction)
129
+ end
130
+ else
131
+ File.write(claude_md_path, memory_instruction)
132
+ end
133
+
134
+ stdout.puts "✓ Updated #{claude_md_path}"
135
+ end
136
+
137
+ def configure_project_hooks
138
+ settings_path = ".claude/settings.json"
139
+ FileUtils.mkdir_p(File.dirname(settings_path))
140
+
141
+ db_path = ClaudeMemory.project_db_path
142
+ ingest_cmd = "claude-memory hook ingest --db #{db_path}"
143
+ sweep_cmd = "claude-memory hook sweep --db #{db_path}"
144
+
145
+ hooks_config = build_hooks_config(ingest_cmd, sweep_cmd)
146
+
147
+ existing = load_json_file(settings_path)
148
+ existing["hooks"] ||= {}
149
+ existing["hooks"].merge!(hooks_config["hooks"])
150
+
151
+ File.write(settings_path, JSON.pretty_generate(existing))
152
+ stdout.puts "✓ Configured hooks in #{settings_path}"
153
+ end
154
+
155
+ def configure_project_mcp
156
+ mcp_path = ".claude.json"
157
+
158
+ existing = load_json_file(mcp_path)
159
+ existing["mcpServers"] ||= {}
160
+ existing["mcpServers"]["claude-memory"] = {
161
+ "type" => "stdio",
162
+ "command" => "claude-memory",
163
+ "args" => ["serve-mcp"]
164
+ }
165
+
166
+ File.write(mcp_path, JSON.pretty_generate(existing))
167
+ stdout.puts "✓ Configured MCP server in #{mcp_path}"
168
+ end
169
+
170
+ def install_output_style
171
+ style_source = File.join(__dir__, "../../output_styles/claude_memory.json")
172
+ style_dest = ".claude/output_styles/claude_memory.json"
173
+
174
+ return unless File.exist?(style_source)
175
+
176
+ FileUtils.mkdir_p(File.dirname(style_dest))
177
+ FileUtils.cp(style_source, style_dest)
178
+ stdout.puts "✓ Installed output style at #{style_dest}"
179
+ end
180
+
181
+ def build_hooks_config(ingest_cmd, sweep_cmd)
182
+ {
183
+ "hooks" => {
184
+ "Stop" => [{
185
+ "hooks" => [
186
+ {"type" => "command", "command" => ingest_cmd, "timeout" => 10}
187
+ ]
188
+ }],
189
+ "SessionStart" => [{
190
+ "hooks" => [
191
+ {"type" => "command", "command" => ingest_cmd, "timeout" => 10}
192
+ ]
193
+ }],
194
+ "PreCompact" => [{
195
+ "hooks" => [
196
+ {"type" => "command", "command" => ingest_cmd, "timeout" => 30},
197
+ {"type" => "command", "command" => sweep_cmd, "timeout" => 30}
198
+ ]
199
+ }],
200
+ "SessionEnd" => [{
201
+ "hooks" => [
202
+ {"type" => "command", "command" => ingest_cmd, "timeout" => 30},
203
+ {"type" => "command", "command" => sweep_cmd, "timeout" => 30}
204
+ ]
205
+ }]
206
+ }
207
+ }
208
+ end
209
+
210
+ def load_json_file(path)
211
+ return {} unless File.exist?(path)
212
+ JSON.parse(File.read(path))
213
+ rescue JSON::ParserError
214
+ {}
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Promotes a project fact to global memory
6
+ class PromoteCommand < 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 promote <fact_id>"
11
+ stderr.puts "\nPromotes a project fact to the global database."
12
+ return 1
13
+ end
14
+
15
+ manager = ClaudeMemory::Store::StoreManager.new
16
+ global_fact_id = manager.promote_fact(fact_id)
17
+
18
+ if global_fact_id
19
+ stdout.puts "Promoted fact ##{fact_id} to global database as fact ##{global_fact_id}"
20
+ manager.close
21
+ 0
22
+ else
23
+ stderr.puts "Fact ##{fact_id} not found in project database."
24
+ manager.close
25
+ 1
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Publishes memory snapshot to Claude Code
6
+ class PublishCommand < BaseCommand
7
+ def call(args)
8
+ opts = parse_options(args, {mode: :shared, granularity: :repo, since: nil, scope: "project"}) do |o|
9
+ OptionParser.new do |parser|
10
+ parser.on("--mode MODE", "Mode: shared, local, or home") { |v| o[:mode] = v.to_sym }
11
+ parser.on("--granularity LEVEL", "Granularity: repo, paths, or nested") { |v| o[:granularity] = v.to_sym }
12
+ parser.on("--since ISO", "Include changes since timestamp") { |v| o[:since] = v }
13
+ parser.on("--scope SCOPE", "Scope: project or global") { |v| o[:scope] = v }
14
+ end
15
+ end
16
+ return 1 if opts.nil?
17
+
18
+ manager = ClaudeMemory::Store::StoreManager.new
19
+ store = manager.store_for_scope(opts[:scope])
20
+ publish = ClaudeMemory::Publish.new(store)
21
+
22
+ result = publish.publish!(mode: opts[:mode], granularity: opts[:granularity], since: opts[:since])
23
+
24
+ case result[:status]
25
+ when :updated
26
+ stdout.puts "Published #{opts[:scope]} snapshot to #{result[:path]}"
27
+ when :unchanged
28
+ stdout.puts "No changes - #{result[:path]} is up to date"
29
+ end
30
+
31
+ manager.close
32
+ 0
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Recalls facts matching a query
6
+ class RecallCommand < BaseCommand
7
+ def call(args)
8
+ query = args.first
9
+ unless query
10
+ stderr.puts "Usage: claude-memory recall <query> [--limit N] [--scope project|global|all]"
11
+ return 1
12
+ end
13
+
14
+ opts = parse_options(args[1..] || [], {limit: 10, scope: "all"}) do |o|
15
+ OptionParser.new do |parser|
16
+ parser.on("--limit N", Integer, "Max results") { |v| o[:limit] = v }
17
+ parser.on("--scope SCOPE", "Scope: project, global, or all") { |v| o[:scope] = v }
18
+ end
19
+ end
20
+ return 1 if opts.nil?
21
+
22
+ manager = ClaudeMemory::Store::StoreManager.new
23
+ recall = ClaudeMemory::Recall.new(manager)
24
+
25
+ results = recall.query(query, limit: opts[:limit], scope: opts[:scope])
26
+ if results.empty?
27
+ stdout.puts "No facts found."
28
+ else
29
+ stdout.puts "Found #{results.size} fact(s):\n\n"
30
+ results.each do |result|
31
+ print_fact(result[:fact], source: result[:source])
32
+ print_receipts(result[:receipts])
33
+ stdout.puts
34
+ end
35
+ end
36
+
37
+ manager.close
38
+ 0
39
+ end
40
+
41
+ private
42
+
43
+ def print_fact(fact, source: nil)
44
+ source_label = source ? " [#{source}]" : ""
45
+ stdout.puts " #{fact[:subject_name]}.#{fact[:predicate]} = #{fact[:object_literal]}#{source_label}"
46
+ stdout.puts " Status: #{fact[:status]}, Confidence: #{fact[:confidence]}"
47
+ stdout.puts " Valid: #{fact[:valid_from]} - #{fact[:valid_to] || "present"}"
48
+ end
49
+
50
+ def print_receipts(receipts)
51
+ return if receipts.empty?
52
+
53
+ stdout.puts " Receipts:"
54
+ receipts.each do |r|
55
+ quote_preview = r[:quote]&.slice(0, 80)&.gsub(/\s+/, " ")
56
+ stdout.puts " - [#{r[:strength]}] \"#{quote_preview}...\""
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Registry for CLI command lookup and dispatch
6
+ # Maps command names to command classes for dynamic dispatch
7
+ class Registry
8
+ # Map of command names to class names
9
+ # As more commands are extracted, add them here
10
+ COMMANDS = {
11
+ "help" => "HelpCommand",
12
+ "version" => "VersionCommand",
13
+ "doctor" => "DoctorCommand",
14
+ "promote" => "PromoteCommand",
15
+ "search" => "SearchCommand",
16
+ "explain" => "ExplainCommand",
17
+ "conflicts" => "ConflictsCommand",
18
+ "changes" => "ChangesCommand",
19
+ "recall" => "RecallCommand",
20
+ "sweep" => "SweepCommand",
21
+ "ingest" => "IngestCommand",
22
+ "publish" => "PublishCommand",
23
+ "db:init" => "DbInitCommand",
24
+ "init" => "InitCommand",
25
+ "serve-mcp" => "ServeMcpCommand",
26
+ "hook" => "HookCommand"
27
+ }.freeze
28
+
29
+ # Find a command class by name
30
+ # @param command_name [String] the command name (e.g., "help", "version")
31
+ # @return [Class, nil] the command class, or nil if not found
32
+ def self.find(command_name)
33
+ return nil if command_name.nil?
34
+
35
+ class_name = COMMANDS[command_name]
36
+ return nil unless class_name
37
+
38
+ Commands.const_get(class_name)
39
+ end
40
+
41
+ # Get all registered command names
42
+ # @return [Array<String>] list of command names
43
+ def self.all_commands
44
+ COMMANDS.keys
45
+ end
46
+
47
+ # Check if a command is registered
48
+ # @param command_name [String] the command name
49
+ # @return [Boolean] true if registered
50
+ def self.registered?(command_name)
51
+ COMMANDS.key?(command_name)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Searches indexed content using full-text search
6
+ class SearchCommand < BaseCommand
7
+ def call(args)
8
+ query = args.first
9
+ unless query
10
+ stderr.puts "Usage: claude-memory search <query> [--db PATH] [--limit N]"
11
+ return 1
12
+ end
13
+
14
+ opts = parse_options(args[1..] || [], {limit: 10, scope: "all"}) do |o|
15
+ OptionParser.new do |parser|
16
+ parser.on("--limit N", Integer, "Max results") { |v| o[:limit] = v }
17
+ parser.on("--scope SCOPE", "Scope: project, global, or all") { |v| o[:scope] = v }
18
+ end
19
+ end
20
+ return 1 if opts.nil?
21
+
22
+ manager = ClaudeMemory::Store::StoreManager.new
23
+ store = manager.store_for_scope((opts[:scope] == "global") ? "global" : "project")
24
+ fts = ClaudeMemory::Index::LexicalFTS.new(store)
25
+
26
+ ids = fts.search(query, limit: opts[:limit])
27
+ if ids.empty?
28
+ stdout.puts "No results found."
29
+ else
30
+ stdout.puts "Found #{ids.size} result(s):"
31
+ ids.each do |id|
32
+ text = store.content_items.where(id: id).get(:raw_text)
33
+ preview = text&.slice(0, 100)&.gsub(/\s+/, " ")
34
+ stdout.puts " [#{id}] #{preview}..."
35
+ end
36
+ end
37
+
38
+ manager.close
39
+ 0
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Starts MCP server
6
+ class ServeMcpCommand < BaseCommand
7
+ def call(_args)
8
+ manager = ClaudeMemory::Store::StoreManager.new
9
+ server = ClaudeMemory::MCP::Server.new(manager)
10
+ server.run
11
+ manager.close
12
+ 0
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Runs maintenance and pruning on memory database
6
+ class SweepCommand < BaseCommand
7
+ def call(args)
8
+ opts = parse_options(args, {budget: 5, scope: "project"}) do |o|
9
+ OptionParser.new do |parser|
10
+ parser.on("--budget SECONDS", Integer, "Time budget in seconds") { |v| o[:budget] = v }
11
+ parser.on("--scope SCOPE", "Scope: project or global") { |v| o[:scope] = v }
12
+ end
13
+ end
14
+ return 1 if opts.nil?
15
+
16
+ manager = ClaudeMemory::Store::StoreManager.new
17
+ store = manager.store_for_scope(opts[:scope])
18
+ sweeper = ClaudeMemory::Sweep::Sweeper.new(store)
19
+
20
+ stdout.puts "Running sweep on #{opts[:scope]} database with #{opts[:budget]}s budget..."
21
+ stats = sweeper.run!(budget_seconds: opts[:budget])
22
+
23
+ stdout.puts "Sweep complete:"
24
+ stdout.puts " Proposed facts expired: #{stats[:proposed_facts_expired]}"
25
+ stdout.puts " Disputed facts expired: #{stats[:disputed_facts_expired]}"
26
+ stdout.puts " Orphaned provenance deleted: #{stats[:orphaned_provenance_deleted]}"
27
+ stdout.puts " Old content pruned: #{stats[:old_content_pruned]}"
28
+ stdout.puts " Elapsed: #{stats[:elapsed_seconds].round(2)}s"
29
+ stdout.puts " Budget honored: #{stats[:budget_honored]}"
30
+
31
+ manager.close
32
+ 0
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Displays version information for claude-memory
6
+ class VersionCommand < BaseCommand
7
+ def call(_args)
8
+ stdout.puts "claude-memory #{ClaudeMemory::VERSION}"
9
+ 0
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ # Centralized configuration and ENV access
5
+ # Provides consistent access to paths and environment variables
6
+ class Configuration
7
+ attr_reader :env
8
+
9
+ def initialize(env = ENV)
10
+ @env = env
11
+ end
12
+
13
+ def home_dir
14
+ env["HOME"] || File.expand_path("~")
15
+ end
16
+
17
+ def project_dir
18
+ env["CLAUDE_PROJECT_DIR"] || Dir.pwd
19
+ end
20
+
21
+ def global_db_path
22
+ File.join(home_dir, ".claude", "memory.sqlite3")
23
+ end
24
+
25
+ def project_db_path(project_path = nil)
26
+ path = project_path || project_dir
27
+ File.join(path, ".claude", "memory.sqlite3")
28
+ end
29
+
30
+ def session_id
31
+ env["CLAUDE_SESSION_ID"]
32
+ end
33
+
34
+ def transcript_path
35
+ env["CLAUDE_TRANSCRIPT_PATH"]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Core
5
+ # Value object representing a fact identifier
6
+ # Provides type safety and validation for fact IDs (positive integers)
7
+ class FactId
8
+ attr_reader :value
9
+
10
+ def initialize(value)
11
+ @value = value.to_i
12
+ validate!
13
+ freeze
14
+ end
15
+
16
+ def to_i
17
+ value
18
+ end
19
+
20
+ def to_s
21
+ value.to_s
22
+ end
23
+
24
+ def ==(other)
25
+ other.is_a?(FactId) && other.value == value
26
+ end
27
+
28
+ alias_method :eql?, :==
29
+
30
+ def hash
31
+ value.hash
32
+ end
33
+
34
+ private
35
+
36
+ def validate!
37
+ raise ArgumentError, "Fact ID must be a positive integer" unless value.positive?
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Core
5
+ # Null object pattern for Explanation
6
+ # Represents a non-existent explanation without using nil
7
+ class NullExplanation
8
+ def present?
9
+ false
10
+ end
11
+
12
+ def fact
13
+ NullFact.new
14
+ end
15
+
16
+ def receipts
17
+ []
18
+ end
19
+
20
+ def superseded_by
21
+ []
22
+ end
23
+
24
+ def supersedes
25
+ []
26
+ end
27
+
28
+ def conflicts
29
+ []
30
+ end
31
+
32
+ def to_h
33
+ {
34
+ fact: fact.to_h,
35
+ receipts: receipts,
36
+ superseded_by: superseded_by,
37
+ supersedes: supersedes,
38
+ conflicts: conflicts
39
+ }
40
+ end
41
+
42
+ def [](key)
43
+ to_h[key]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Core
5
+ # Null object pattern for Fact
6
+ # Represents a non-existent fact without using nil
7
+ class NullFact
8
+ def present?
9
+ false
10
+ end
11
+
12
+ def to_h
13
+ {
14
+ id: nil,
15
+ subject_name: nil,
16
+ predicate: nil,
17
+ object_literal: nil,
18
+ status: "not_found",
19
+ confidence: 0.0,
20
+ valid_from: nil,
21
+ valid_to: nil
22
+ }
23
+ end
24
+
25
+ def [](key)
26
+ to_h[key]
27
+ end
28
+ end
29
+ end
30
+ end