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.
- checksums.yaml +4 -4
- data/.claude/.mind.mv2.aLCUZd +0 -0
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/rules/claude_memory.generated.md +7 -1
- data/.claude/settings.json +0 -4
- data/.claude/settings.local.json +4 -1
- data/.claude-plugin/plugin.json +1 -1
- data/.claude.json +11 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +62 -11
- data/CLAUDE.md +87 -24
- data/README.md +76 -159
- data/docs/EXAMPLES.md +436 -0
- data/docs/RELEASE_NOTES_v0.2.0.md +179 -0
- data/docs/RUBY_COMMUNITY_POST_v0.2.0.md +582 -0
- data/docs/SOCIAL_MEDIA_v0.2.0.md +420 -0
- data/docs/architecture.md +360 -0
- data/docs/expert_review.md +1718 -0
- data/docs/feature_adoption_plan.md +1241 -0
- data/docs/feature_adoption_plan_revised.md +2374 -0
- data/docs/improvements.md +1325 -0
- data/docs/quality_review.md +1544 -0
- data/docs/review_summary.md +480 -0
- data/lefthook.yml +10 -0
- data/lib/claude_memory/cli.rb +16 -844
- data/lib/claude_memory/commands/base_command.rb +95 -0
- data/lib/claude_memory/commands/changes_command.rb +39 -0
- data/lib/claude_memory/commands/conflicts_command.rb +37 -0
- data/lib/claude_memory/commands/db_init_command.rb +40 -0
- data/lib/claude_memory/commands/doctor_command.rb +147 -0
- data/lib/claude_memory/commands/explain_command.rb +65 -0
- data/lib/claude_memory/commands/help_command.rb +37 -0
- data/lib/claude_memory/commands/hook_command.rb +106 -0
- data/lib/claude_memory/commands/ingest_command.rb +47 -0
- data/lib/claude_memory/commands/init_command.rb +218 -0
- data/lib/claude_memory/commands/promote_command.rb +30 -0
- data/lib/claude_memory/commands/publish_command.rb +36 -0
- data/lib/claude_memory/commands/recall_command.rb +61 -0
- data/lib/claude_memory/commands/registry.rb +55 -0
- data/lib/claude_memory/commands/search_command.rb +43 -0
- data/lib/claude_memory/commands/serve_mcp_command.rb +16 -0
- data/lib/claude_memory/commands/sweep_command.rb +36 -0
- data/lib/claude_memory/commands/version_command.rb +13 -0
- data/lib/claude_memory/configuration.rb +38 -0
- data/lib/claude_memory/core/fact_id.rb +41 -0
- data/lib/claude_memory/core/null_explanation.rb +47 -0
- data/lib/claude_memory/core/null_fact.rb +30 -0
- data/lib/claude_memory/core/result.rb +143 -0
- data/lib/claude_memory/core/session_id.rb +37 -0
- data/lib/claude_memory/core/token_estimator.rb +33 -0
- data/lib/claude_memory/core/transcript_path.rb +37 -0
- data/lib/claude_memory/domain/conflict.rb +51 -0
- data/lib/claude_memory/domain/entity.rb +51 -0
- data/lib/claude_memory/domain/fact.rb +70 -0
- data/lib/claude_memory/domain/provenance.rb +48 -0
- data/lib/claude_memory/hook/exit_codes.rb +18 -0
- data/lib/claude_memory/hook/handler.rb +7 -2
- data/lib/claude_memory/index/index_query.rb +89 -0
- data/lib/claude_memory/index/index_query_logic.rb +41 -0
- data/lib/claude_memory/index/query_options.rb +67 -0
- data/lib/claude_memory/infrastructure/file_system.rb +29 -0
- data/lib/claude_memory/infrastructure/in_memory_file_system.rb +32 -0
- data/lib/claude_memory/ingest/content_sanitizer.rb +42 -0
- data/lib/claude_memory/ingest/ingester.rb +3 -0
- data/lib/claude_memory/ingest/privacy_tag.rb +48 -0
- data/lib/claude_memory/mcp/tools.rb +174 -1
- data/lib/claude_memory/publish.rb +29 -20
- data/lib/claude_memory/recall.rb +164 -16
- data/lib/claude_memory/resolve/resolver.rb +41 -37
- data/lib/claude_memory/shortcuts.rb +56 -0
- data/lib/claude_memory/store/store_manager.rb +35 -32
- data/lib/claude_memory/templates/hooks.example.json +0 -4
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +59 -21
- 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
|