claude_memory 0.7.1 → 0.9.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/memory.sqlite3 +0 -0
- data/.claude/rules/claude_memory.generated.md +32 -2
- data/.claude/settings.json +65 -15
- data/.claude/settings.local.json +5 -2
- data/.claude/skills/improve/SKILL.md +113 -25
- data/.claude/skills/upgrade-dependencies/SKILL.md +154 -0
- data/.claude-plugin/commands/distill-transcripts.md +98 -0
- data/.claude-plugin/commands/memory-recall.md +67 -0
- data/.claude-plugin/marketplace.json +2 -2
- data/.claude-plugin/plugin.json +3 -3
- data/.claude-plugin/scripts/hook-runner.sh +14 -0
- data/.claude-plugin/scripts/serve-mcp.sh +14 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +90 -1
- data/CLAUDE.md +56 -18
- data/README.md +35 -0
- data/db/migrations/013_add_mcp_tool_calls.rb +26 -0
- data/db/migrations/014_canonicalize_predicates.rb +30 -0
- data/docs/improvements.md +74 -74
- data/docs/influence/claude-mem.md +1 -0
- data/docs/influence/claude-supermemory.md +1 -0
- data/docs/influence/episodic-memory.md +1 -0
- data/docs/influence/grepai.md +1 -0
- data/docs/influence/kbs.md +1 -0
- data/docs/influence/lossless-claw.md +1 -0
- data/docs/influence/qmd.md +1 -0
- data/docs/quality_review.md +119 -224
- data/hooks/hooks.json +39 -7
- data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
- data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
- data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
- data/lib/claude_memory/commands/completion_command.rb +149 -0
- data/lib/claude_memory/commands/doctor_command.rb +2 -0
- data/lib/claude_memory/commands/embeddings_command.rb +198 -0
- data/lib/claude_memory/commands/help_command.rb +12 -1
- data/lib/claude_memory/commands/hook_command.rb +2 -1
- data/lib/claude_memory/commands/index_command.rb +85 -78
- data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
- data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
- data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
- data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
- data/lib/claude_memory/commands/install_skill_command.rb +78 -0
- data/lib/claude_memory/commands/registry.rb +47 -32
- data/lib/claude_memory/commands/reject_command.rb +62 -0
- data/lib/claude_memory/commands/restore_command.rb +77 -0
- data/lib/claude_memory/commands/skills/distill-transcripts.md +102 -0
- data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
- data/lib/claude_memory/commands/stats_command.rb +98 -2
- data/lib/claude_memory/configuration.rb +14 -1
- data/lib/claude_memory/core/fact_ranker.rb +2 -2
- data/lib/claude_memory/core/rr_fusion.rb +23 -6
- data/lib/claude_memory/core/snippet_extractor.rb +7 -3
- data/lib/claude_memory/core/text_builder.rb +11 -0
- data/lib/claude_memory/distill/json_schema.md +8 -4
- data/lib/claude_memory/distill/null_distiller.rb +2 -0
- data/lib/claude_memory/domain/entity.rb +13 -1
- data/lib/claude_memory/domain/fact.rb +26 -2
- data/lib/claude_memory/domain/provenance.rb +0 -1
- data/lib/claude_memory/embeddings/api_adapter.rb +97 -0
- data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
- data/lib/claude_memory/embeddings/fastembed_adapter.rb +46 -12
- data/lib/claude_memory/embeddings/generator.rb +4 -0
- data/lib/claude_memory/embeddings/inspector.rb +91 -0
- data/lib/claude_memory/embeddings/model_registry.rb +210 -0
- data/lib/claude_memory/embeddings/resolver.rb +44 -0
- data/lib/claude_memory/hook/context_injector.rb +58 -2
- data/lib/claude_memory/hook/distillation_runner.rb +46 -0
- data/lib/claude_memory/hook/handler.rb +11 -2
- data/lib/claude_memory/index/vector_index.rb +15 -2
- data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
- data/lib/claude_memory/ingest/ingester.rb +17 -0
- data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
- data/lib/claude_memory/mcp/handlers/management_handlers.rb +169 -0
- data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
- data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
- data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
- data/lib/claude_memory/mcp/handlers/stats_handlers.rb +205 -0
- data/lib/claude_memory/mcp/instructions_builder.rb +19 -1
- data/lib/claude_memory/mcp/query_guide.rb +10 -0
- data/lib/claude_memory/mcp/response_formatter.rb +1 -0
- data/lib/claude_memory/mcp/server.rb +22 -1
- data/lib/claude_memory/mcp/telemetry.rb +86 -0
- data/lib/claude_memory/mcp/text_summary.rb +26 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +116 -4
- data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
- data/lib/claude_memory/mcp/tools.rb +50 -679
- data/lib/claude_memory/publish.rb +40 -5
- data/lib/claude_memory/recall/dual_engine.rb +105 -0
- data/lib/claude_memory/recall/legacy_engine.rb +138 -0
- data/lib/claude_memory/recall/query_core.rb +371 -0
- data/lib/claude_memory/recall.rb +121 -673
- data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
- data/lib/claude_memory/resolve/resolver.rb +43 -0
- data/lib/claude_memory/shortcuts.rb +4 -4
- data/lib/claude_memory/store/retry_handler.rb +61 -0
- data/lib/claude_memory/store/schema_manager.rb +68 -0
- data/lib/claude_memory/store/sqlite_store.rb +334 -201
- data/lib/claude_memory/store/store_manager.rb +50 -1
- data/lib/claude_memory/sweep/maintenance.rb +115 -1
- data/lib/claude_memory/sweep/sweeper.rb +3 -0
- data/lib/claude_memory/templates/hooks.example.json +26 -7
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +16 -0
- metadata +48 -8
- data/.claude/memory.sqlite3-shm +0 -0
- data/.claude/memory.sqlite3-wal +0 -0
|
@@ -65,6 +65,7 @@ module ClaudeMemory
|
|
|
65
65
|
@skip_hooks = true
|
|
66
66
|
else
|
|
67
67
|
@stdout.puts "\nUpdating hooks..."
|
|
68
|
+
@replace_hooks = true
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
71
|
|
|
@@ -73,7 +74,7 @@ module ClaudeMemory
|
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
def configure_hooks
|
|
76
|
-
HooksConfigurator.new(@stdout).configure_global_hooks
|
|
77
|
+
HooksConfigurator.new(@stdout).configure_global_hooks(replace: @replace_hooks || false)
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def configure_mcp
|
|
@@ -12,7 +12,7 @@ module ClaudeMemory
|
|
|
12
12
|
@stdout = stdout
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def configure_project_hooks
|
|
15
|
+
def configure_project_hooks(replace: false)
|
|
16
16
|
settings_path = ".claude/settings.json"
|
|
17
17
|
FileUtils.mkdir_p(File.dirname(settings_path))
|
|
18
18
|
|
|
@@ -24,13 +24,13 @@ module ClaudeMemory
|
|
|
24
24
|
|
|
25
25
|
existing = load_json_file(settings_path)
|
|
26
26
|
existing["hooks"] ||= {}
|
|
27
|
-
merge_hooks!(existing["hooks"], hooks_config["hooks"])
|
|
27
|
+
merge_hooks!(existing["hooks"], hooks_config["hooks"], replace: replace)
|
|
28
28
|
|
|
29
29
|
File.write(settings_path, JSON.pretty_generate(existing))
|
|
30
30
|
@stdout.puts "✓ Configured hooks in #{settings_path}"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def configure_global_hooks
|
|
33
|
+
def configure_global_hooks(replace: false)
|
|
34
34
|
settings_path = File.join(Dir.home, ".claude", "settings.json")
|
|
35
35
|
FileUtils.mkdir_p(File.dirname(settings_path))
|
|
36
36
|
|
|
@@ -42,7 +42,7 @@ module ClaudeMemory
|
|
|
42
42
|
|
|
43
43
|
existing = load_json_file(settings_path)
|
|
44
44
|
existing["hooks"] ||= {}
|
|
45
|
-
merge_hooks!(existing["hooks"], hooks_config["hooks"])
|
|
45
|
+
merge_hooks!(existing["hooks"], hooks_config["hooks"], replace: replace)
|
|
46
46
|
|
|
47
47
|
File.write(settings_path, JSON.pretty_generate(existing))
|
|
48
48
|
@stdout.puts "✓ Configured hooks in #{settings_path}"
|
|
@@ -97,28 +97,61 @@ module ClaudeMemory
|
|
|
97
97
|
private
|
|
98
98
|
|
|
99
99
|
def build_hooks_config(ingest_cmd, sweep_cmd)
|
|
100
|
+
context_cmd = "claude-memory hook context"
|
|
101
|
+
|
|
100
102
|
{
|
|
101
103
|
"hooks" => {
|
|
102
104
|
"Stop" => [{
|
|
103
105
|
"hooks" => [
|
|
104
|
-
{"type" => "command", "command" => ingest_cmd, "timeout" =>
|
|
106
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 5,
|
|
107
|
+
"statusMessage" => "Saving memory..."}
|
|
108
|
+
]
|
|
109
|
+
}],
|
|
110
|
+
"StopFailure" => [{
|
|
111
|
+
"hooks" => [
|
|
112
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 5,
|
|
113
|
+
"statusMessage" => "Saving memory..."}
|
|
105
114
|
]
|
|
106
115
|
}],
|
|
107
116
|
"SessionStart" => [{
|
|
108
117
|
"hooks" => [
|
|
109
|
-
{"type" => "command", "command" =>
|
|
118
|
+
{"type" => "command", "command" => context_cmd, "timeout" => 5,
|
|
119
|
+
"statusMessage" => "Loading memory..."}
|
|
110
120
|
]
|
|
111
121
|
}],
|
|
112
122
|
"PreCompact" => [{
|
|
113
123
|
"hooks" => [
|
|
114
|
-
{"type" => "command", "command" => ingest_cmd, "timeout" => 30
|
|
115
|
-
|
|
124
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 30,
|
|
125
|
+
"statusMessage" => "Saving memory..."},
|
|
126
|
+
{"type" => "command", "command" => sweep_cmd, "timeout" => 30,
|
|
127
|
+
"statusMessage" => "Sweeping memory..."}
|
|
116
128
|
]
|
|
117
129
|
}],
|
|
118
130
|
"SessionEnd" => [{
|
|
119
131
|
"hooks" => [
|
|
120
|
-
{"type" => "command", "command" => ingest_cmd, "timeout" => 30
|
|
121
|
-
|
|
132
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 30,
|
|
133
|
+
"statusMessage" => "Saving memory..."},
|
|
134
|
+
{"type" => "command", "command" => sweep_cmd, "timeout" => 30,
|
|
135
|
+
"statusMessage" => "Sweeping memory..."}
|
|
136
|
+
]
|
|
137
|
+
}],
|
|
138
|
+
"TaskCompleted" => [{
|
|
139
|
+
"hooks" => [
|
|
140
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 10,
|
|
141
|
+
"statusMessage" => "Saving memory..."}
|
|
142
|
+
]
|
|
143
|
+
}],
|
|
144
|
+
"TeammateIdle" => [{
|
|
145
|
+
"hooks" => [
|
|
146
|
+
{"type" => "command", "command" => ingest_cmd, "timeout" => 15,
|
|
147
|
+
"statusMessage" => "Saving memory..."}
|
|
148
|
+
]
|
|
149
|
+
}],
|
|
150
|
+
"Notification" => [{
|
|
151
|
+
"matcher" => "idle_prompt",
|
|
152
|
+
"hooks" => [
|
|
153
|
+
{"type" => "command", "command" => sweep_cmd, "timeout" => 10,
|
|
154
|
+
"statusMessage" => "Sweeping memory..."}
|
|
122
155
|
]
|
|
123
156
|
}]
|
|
124
157
|
}
|
|
@@ -132,7 +165,18 @@ module ClaudeMemory
|
|
|
132
165
|
{}
|
|
133
166
|
end
|
|
134
167
|
|
|
135
|
-
def merge_hooks!(existing_hooks, new_hooks)
|
|
168
|
+
def merge_hooks!(existing_hooks, new_hooks, replace: false)
|
|
169
|
+
if replace
|
|
170
|
+
# Remove all claude-memory hooks first, then add fresh ones
|
|
171
|
+
existing_hooks.each do |event, hook_arrays|
|
|
172
|
+
next unless hook_arrays.is_a?(Array)
|
|
173
|
+
hook_arrays.reject! do |ha|
|
|
174
|
+
ha.is_a?(Hash) && ha["hooks"]&.any? { |h| h["command"]&.include?("claude-memory") }
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
existing_hooks.delete_if { |_, v| v.is_a?(Array) && v.empty? }
|
|
178
|
+
end
|
|
179
|
+
|
|
136
180
|
new_hooks.each do |event, hook_arrays|
|
|
137
181
|
existing_hooks[event] ||= []
|
|
138
182
|
|
|
@@ -68,6 +68,7 @@ module ClaudeMemory
|
|
|
68
68
|
@skip_hooks = true
|
|
69
69
|
else
|
|
70
70
|
@stdout.puts "\nUpdating hooks..."
|
|
71
|
+
@replace_hooks = true
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
|
|
@@ -81,7 +82,7 @@ module ClaudeMemory
|
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def configure_hooks
|
|
84
|
-
HooksConfigurator.new(@stdout).configure_project_hooks
|
|
85
|
+
HooksConfigurator.new(@stdout).configure_project_hooks(replace: @replace_hooks || false)
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
def configure_mcp
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module Commands
|
|
5
|
+
# Installs embedded skill files (agent definitions) to ~/.claude/commands/
|
|
6
|
+
# for use as Claude Code slash commands.
|
|
7
|
+
class InstallSkillCommand < BaseCommand
|
|
8
|
+
SKILLS_DIR = File.expand_path("../skills", __FILE__)
|
|
9
|
+
|
|
10
|
+
AVAILABLE_SKILLS = {
|
|
11
|
+
"memory-recall" => {
|
|
12
|
+
file: "memory-recall.md",
|
|
13
|
+
description: "Memory recall agent — chains recall → explain → fact_graph"
|
|
14
|
+
},
|
|
15
|
+
"distill-transcripts" => {
|
|
16
|
+
file: "distill-transcripts.md",
|
|
17
|
+
description: "Distill transcripts — extract facts/entities/decisions from undistilled content"
|
|
18
|
+
}
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def call(args)
|
|
22
|
+
opts = parse_options(args, {list: false, force: false}) do |o|
|
|
23
|
+
OptionParser.new do |parser|
|
|
24
|
+
parser.banner = "Usage: claude-memory install-skill [SKILL_NAME] [options]"
|
|
25
|
+
parser.on("--list", "List available skills") { o[:list] = true }
|
|
26
|
+
parser.on("--force", "Overwrite existing files") { o[:force] = true }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
return 1 if opts.nil?
|
|
30
|
+
|
|
31
|
+
if opts[:list] || args.empty?
|
|
32
|
+
return list_skills
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
skill_name = args.first
|
|
36
|
+
install_skill(skill_name, force: opts[:force])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def list_skills
|
|
42
|
+
stdout.puts "Available skills:"
|
|
43
|
+
AVAILABLE_SKILLS.each do |name, info|
|
|
44
|
+
stdout.puts " #{name} — #{info[:description]}"
|
|
45
|
+
end
|
|
46
|
+
stdout.puts ""
|
|
47
|
+
stdout.puts "Install with: claude-memory install-skill <name>"
|
|
48
|
+
0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def install_skill(name, force: false)
|
|
52
|
+
skill = AVAILABLE_SKILLS[name]
|
|
53
|
+
unless skill
|
|
54
|
+
return failure("Unknown skill: #{name}. Run --list to see available skills.")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
source = File.join(SKILLS_DIR, skill[:file])
|
|
58
|
+
unless File.exist?(source)
|
|
59
|
+
return failure("Skill file not found: #{source}")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
target_dir = File.join(Dir.home, ".claude", "commands")
|
|
63
|
+
FileUtils.mkdir_p(target_dir)
|
|
64
|
+
|
|
65
|
+
target = File.join(target_dir, skill[:file])
|
|
66
|
+
|
|
67
|
+
if File.exist?(target) && !force
|
|
68
|
+
return failure("#{target} already exists. Use --force to overwrite.")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
FileUtils.cp(source, target)
|
|
72
|
+
stdout.puts "Installed #{name} to #{target}"
|
|
73
|
+
stdout.puts "Use as: /#{File.basename(name, ".md")} <query>"
|
|
74
|
+
0
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -3,46 +3,48 @@
|
|
|
3
3
|
module ClaudeMemory
|
|
4
4
|
module Commands
|
|
5
5
|
# Registry for CLI command lookup and dispatch
|
|
6
|
-
# Maps command names to command classes
|
|
6
|
+
# Maps command names to command classes and short descriptions.
|
|
7
|
+
# Descriptions are authoritative for shell completion and help output;
|
|
8
|
+
# keep them current when adding commands.
|
|
7
9
|
class Registry
|
|
8
|
-
# Map of command names to class
|
|
9
|
-
# As more commands are extracted, add them here
|
|
10
|
+
# Map of command names to {class:, description:} entries.
|
|
11
|
+
# As more commands are extracted, add them here.
|
|
10
12
|
COMMANDS = {
|
|
11
|
-
"help" =>
|
|
12
|
-
"version" =>
|
|
13
|
-
"doctor" =>
|
|
14
|
-
"stats" =>
|
|
15
|
-
"promote" =>
|
|
16
|
-
"search" =>
|
|
17
|
-
"explain" =>
|
|
18
|
-
"conflicts" =>
|
|
19
|
-
"changes" =>
|
|
20
|
-
"recall" =>
|
|
21
|
-
"sweep" =>
|
|
22
|
-
"ingest" =>
|
|
23
|
-
"publish" =>
|
|
24
|
-
"db:init" =>
|
|
25
|
-
"init" =>
|
|
26
|
-
"uninstall" =>
|
|
27
|
-
"serve-mcp" =>
|
|
28
|
-
"hook" =>
|
|
29
|
-
"index" =>
|
|
30
|
-
"recover" =>
|
|
31
|
-
"compact" =>
|
|
32
|
-
"export" =>
|
|
33
|
-
"git-lfs" =>
|
|
13
|
+
"help" => {class: HelpCommand, description: "Show help message"},
|
|
14
|
+
"version" => {class: VersionCommand, description: "Show version"},
|
|
15
|
+
"doctor" => {class: DoctorCommand, description: "Check system health"},
|
|
16
|
+
"stats" => {class: StatsCommand, description: "Show statistics"},
|
|
17
|
+
"promote" => {class: PromoteCommand, description: "Promote fact to global"},
|
|
18
|
+
"search" => {class: SearchCommand, description: "Search indexed content"},
|
|
19
|
+
"explain" => {class: ExplainCommand, description: "Explain a fact with receipts"},
|
|
20
|
+
"conflicts" => {class: ConflictsCommand, description: "Show open conflicts"},
|
|
21
|
+
"changes" => {class: ChangesCommand, description: "Show recent fact changes"},
|
|
22
|
+
"recall" => {class: RecallCommand, description: "Recall facts matching query"},
|
|
23
|
+
"sweep" => {class: SweepCommand, description: "Run maintenance"},
|
|
24
|
+
"ingest" => {class: IngestCommand, description: "Ingest transcript delta"},
|
|
25
|
+
"publish" => {class: PublishCommand, description: "Publish snapshot"},
|
|
26
|
+
"db:init" => {class: DbInitCommand, description: "Initialize database"},
|
|
27
|
+
"init" => {class: InitCommand, description: "Initialize ClaudeMemory"},
|
|
28
|
+
"uninstall" => {class: UninstallCommand, description: "Remove configuration"},
|
|
29
|
+
"serve-mcp" => {class: ServeMcpCommand, description: "Start MCP server"},
|
|
30
|
+
"hook" => {class: HookCommand, description: "Run hook entrypoints"},
|
|
31
|
+
"index" => {class: IndexCommand, description: "Index content"},
|
|
32
|
+
"recover" => {class: RecoverCommand, description: "Recover database"},
|
|
33
|
+
"compact" => {class: CompactCommand, description: "Compact databases"},
|
|
34
|
+
"export" => {class: ExportCommand, description: "Export facts to JSON"},
|
|
35
|
+
"git-lfs" => {class: GitLfsCommand, description: "Git LFS integration"},
|
|
36
|
+
"install-skill" => {class: InstallSkillCommand, description: "Install agent skills"},
|
|
37
|
+
"completion" => {class: CompletionCommand, description: "Generate shell completions"},
|
|
38
|
+
"embeddings" => {class: EmbeddingsCommand, description: "Inspect embedding backend"},
|
|
39
|
+
"reject" => {class: RejectCommand, description: "Mark a fact as rejected"},
|
|
40
|
+
"restore" => {class: RestoreCommand, description: "Restore superseded facts from obsolete single-value classification"}
|
|
34
41
|
}.freeze
|
|
35
42
|
|
|
36
43
|
# Find a command class by name
|
|
37
44
|
# @param command_name [String] the command name (e.g., "help", "version")
|
|
38
45
|
# @return [Class, nil] the command class, or nil if not found
|
|
39
46
|
def self.find(command_name)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class_name = COMMANDS[command_name]
|
|
43
|
-
return nil unless class_name
|
|
44
|
-
|
|
45
|
-
Commands.const_get(class_name)
|
|
47
|
+
COMMANDS.dig(command_name, :class)
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
# Get all registered command names
|
|
@@ -57,6 +59,19 @@ module ClaudeMemory
|
|
|
57
59
|
def self.registered?(command_name)
|
|
58
60
|
COMMANDS.key?(command_name)
|
|
59
61
|
end
|
|
62
|
+
|
|
63
|
+
# Get the short description for a command
|
|
64
|
+
# @param command_name [String] the command name
|
|
65
|
+
# @return [String, nil] the description, or nil if not registered
|
|
66
|
+
def self.description(command_name)
|
|
67
|
+
COMMANDS.dig(command_name, :description)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Get all command descriptions as a hash
|
|
71
|
+
# @return [Hash{String => String}] command name → description
|
|
72
|
+
def self.descriptions
|
|
73
|
+
COMMANDS.transform_values { |entry| entry[:description] }
|
|
74
|
+
end
|
|
60
75
|
end
|
|
61
76
|
end
|
|
62
77
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module ClaudeMemory
|
|
6
|
+
module Commands
|
|
7
|
+
# Mark a fact as rejected (e.g. a distiller hallucination).
|
|
8
|
+
# Closes any open conflicts involving the fact.
|
|
9
|
+
class RejectCommand < BaseCommand
|
|
10
|
+
# @param args [Array<String>] command line arguments (fact_id_or_docid, --scope, --reason)
|
|
11
|
+
# @return [Integer] exit code (0 for success, 1 for failure)
|
|
12
|
+
def call(args)
|
|
13
|
+
opts = parse_options(args, {scope: "project", reason: nil}) do |o|
|
|
14
|
+
OptionParser.new do |parser|
|
|
15
|
+
parser.banner = "Usage: claude-memory reject <fact_id_or_docid> [options]"
|
|
16
|
+
parser.on("--scope SCOPE", %w[project global], "Database scope (default: project)") { |v| o[:scope] = v }
|
|
17
|
+
parser.on("--reason TEXT", "Why this fact is wrong (recorded in conflict notes)") { |v| o[:reason] = v }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
return 1 if opts.nil?
|
|
21
|
+
|
|
22
|
+
identifier = args.first
|
|
23
|
+
return failure("Usage: claude-memory reject <fact_id_or_docid> [options]") if identifier.nil? || identifier.empty?
|
|
24
|
+
|
|
25
|
+
manager = ClaudeMemory::Store::StoreManager.new
|
|
26
|
+
store = manager.store_for_scope(opts[:scope])
|
|
27
|
+
|
|
28
|
+
fact_id = resolve_fact_id(store, identifier)
|
|
29
|
+
unless fact_id
|
|
30
|
+
stderr.puts "Fact '#{identifier}' not found in #{opts[:scope]} database."
|
|
31
|
+
manager.close
|
|
32
|
+
return 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
result = store.reject_fact(fact_id, reason: opts[:reason])
|
|
36
|
+
manager.close
|
|
37
|
+
|
|
38
|
+
if result.nil?
|
|
39
|
+
stderr.puts "Fact ##{fact_id} not found."
|
|
40
|
+
return 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
stdout.puts "Rejected fact ##{fact_id} in #{opts[:scope]} database."
|
|
44
|
+
stdout.puts "Resolved #{result[:conflicts_resolved]} open conflict(s)." if result[:conflicts_resolved] > 0
|
|
45
|
+
0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Accept either a numeric fact id or an 8-char docid hex string.
|
|
51
|
+
# @param store [Store::SQLiteStore] database to look up the fact in
|
|
52
|
+
# @param identifier [String] numeric fact id or hex docid
|
|
53
|
+
# @return [Integer, nil] resolved fact id, or nil if not found
|
|
54
|
+
def resolve_fact_id(store, identifier)
|
|
55
|
+
return identifier.to_i if identifier.match?(/\A\d+\z/)
|
|
56
|
+
|
|
57
|
+
row = store.find_fact_by_docid(identifier)
|
|
58
|
+
row ? row[:id] : nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module ClaudeMemory
|
|
6
|
+
module Commands
|
|
7
|
+
# One-time recovery for facts that were superseded because of an
|
|
8
|
+
# obsolete single-value predicate classification. See
|
|
9
|
+
# Sweep::Maintenance#restore_multi_value_supersessions for the algorithm
|
|
10
|
+
# and Jaccard heuristic.
|
|
11
|
+
class RestoreCommand < BaseCommand
|
|
12
|
+
# @param args [Array<String>] command line arguments (--predicate, --scope, --dry-run)
|
|
13
|
+
# @return [Integer] exit code (0 for success, 1 for failure)
|
|
14
|
+
def call(args)
|
|
15
|
+
opts = parse_options(args, {predicate: nil, scope: "project", dry_run: false}) do |o|
|
|
16
|
+
OptionParser.new do |parser|
|
|
17
|
+
parser.banner = "Usage: claude-memory restore --predicate NAME [options]"
|
|
18
|
+
parser.on("--predicate NAME", "Predicate to restore (e.g. uses_framework)") { |v| o[:predicate] = v }
|
|
19
|
+
parser.on("--scope SCOPE", %w[project global], "Database scope (default: project)") { |v| o[:scope] = v }
|
|
20
|
+
parser.on("--dry-run", "Show what would be restored without writing") { o[:dry_run] = true }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
return 1 if opts.nil?
|
|
24
|
+
|
|
25
|
+
return failure("--predicate required (e.g. --predicate uses_framework)") if opts[:predicate].nil?
|
|
26
|
+
|
|
27
|
+
manager = ClaudeMemory::Store::StoreManager.new
|
|
28
|
+
store = manager.store_for_scope(opts[:scope])
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
result = Sweep::Maintenance.new(store).restore_multi_value_supersessions(
|
|
32
|
+
predicate: opts[:predicate],
|
|
33
|
+
dry_run: opts[:dry_run]
|
|
34
|
+
)
|
|
35
|
+
rescue ArgumentError => e
|
|
36
|
+
stderr.puts e.message
|
|
37
|
+
manager.close
|
|
38
|
+
return 1
|
|
39
|
+
ensure
|
|
40
|
+
manager.close
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
print_result(opts, result)
|
|
44
|
+
0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Print a summary of restored and skipped facts.
|
|
50
|
+
# @param opts [Hash] parsed options including :predicate, :scope, :dry_run
|
|
51
|
+
# @param result [Hash] result from Sweep::Maintenance#restore_multi_value_supersessions
|
|
52
|
+
# @return [void]
|
|
53
|
+
def print_result(opts, result)
|
|
54
|
+
mode = opts[:dry_run] ? "DRY RUN" : "RESTORE"
|
|
55
|
+
stdout.puts "#{mode}: predicate=#{opts[:predicate]} scope=#{opts[:scope]}"
|
|
56
|
+
stdout.puts "=" * 50
|
|
57
|
+
stdout.puts "Inspected: #{result[:inspected]}"
|
|
58
|
+
stdout.puts "Restored: #{result[:restored]}"
|
|
59
|
+
stdout.puts "Skipped ambiguous: #{result[:skipped_ambiguous]}"
|
|
60
|
+
|
|
61
|
+
return if result[:decisions].empty?
|
|
62
|
+
|
|
63
|
+
stdout.puts
|
|
64
|
+
stdout.puts "Decisions:"
|
|
65
|
+
result[:decisions].each do |d|
|
|
66
|
+
case d[:action]
|
|
67
|
+
when :restore
|
|
68
|
+
stdout.puts " [restore] ##{d[:fact_id]} #{d[:object]}"
|
|
69
|
+
when :skip_ambiguous
|
|
70
|
+
overlaps = d[:overlaps_with].map { |o| "'#{o}'" }.join(", ")
|
|
71
|
+
stdout.puts " [skip] ##{d[:fact_id]} #{d[:object]} (overlaps: #{overlaps})"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Distill Transcripts
|
|
2
|
+
|
|
3
|
+
Extract structured knowledge (facts, entities, decisions) from undistilled transcript content and persist it to long-term memory.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/distill-transcripts
|
|
9
|
+
/distill-transcripts --limit 10
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
You are a knowledge extraction specialist. Your job is to read raw transcript content and extract structured facts, entities, and decisions, then persist them via the memory.store_extraction MCP tool.
|
|
15
|
+
|
|
16
|
+
### Step 1: Get Undistilled Content
|
|
17
|
+
|
|
18
|
+
Call `memory.undistilled` with `limit: 10` to get transcript content that hasn't been processed yet.
|
|
19
|
+
|
|
20
|
+
If no items are returned, report "No undistilled content found" and stop.
|
|
21
|
+
|
|
22
|
+
### Step 2: Extract Knowledge (per item)
|
|
23
|
+
|
|
24
|
+
For each content item, carefully read the raw_text and extract:
|
|
25
|
+
|
|
26
|
+
**Entities** — Named things mentioned:
|
|
27
|
+
- type: database, framework, language, platform, repo, module, person, service
|
|
28
|
+
- name: Canonical name (e.g., "PostgreSQL" not "postgres")
|
|
29
|
+
- confidence: 0.0-1.0
|
|
30
|
+
|
|
31
|
+
**Facts** — Knowledge learned:
|
|
32
|
+
- subject: Entity name or "repo" for project-level facts
|
|
33
|
+
- predicate: prefer a predicate from the canonical vocabulary defined in
|
|
34
|
+
`lib/claude_memory/resolve/predicate_policy.rb` (convention, decision,
|
|
35
|
+
architecture, uses_framework, uses_language, uses_database,
|
|
36
|
+
deployment_platform, auth_method). Other snake_case predicates are
|
|
37
|
+
accepted but fall through to the default multi-value policy.
|
|
38
|
+
- object: The value
|
|
39
|
+
- confidence: 0.0-1.0
|
|
40
|
+
- quote: Source excerpt (max 200 chars)
|
|
41
|
+
- strength: "stated" (explicitly said) or "inferred" (implied)
|
|
42
|
+
- scope_hint: "project" (this project only) or "global" (all projects)
|
|
43
|
+
|
|
44
|
+
**Decisions** — Choices made:
|
|
45
|
+
- title: Short summary (max 100 chars)
|
|
46
|
+
- summary: Full description
|
|
47
|
+
- status_hint: "accepted", "proposed", or "rejected"
|
|
48
|
+
|
|
49
|
+
### What to Extract
|
|
50
|
+
|
|
51
|
+
- Technology choices ("we use PostgreSQL", "switched to React")
|
|
52
|
+
- Conventions ("always use frozen_string_literal", "test files go in spec/")
|
|
53
|
+
- Architectural decisions ("API uses REST", "auth via JWT")
|
|
54
|
+
- Preferences ("prefer 4-space indent", "use Standard Ruby")
|
|
55
|
+
- Project structure ("migrations in db/migrations/", "commands in commands/")
|
|
56
|
+
|
|
57
|
+
### What to Skip
|
|
58
|
+
|
|
59
|
+
- Debugging steps and transient errors
|
|
60
|
+
- Code output and tool observations
|
|
61
|
+
- File contents that were just being read
|
|
62
|
+
- Ephemeral task details ("fix this test", "run the linter")
|
|
63
|
+
- Information already obvious from the codebase itself
|
|
64
|
+
|
|
65
|
+
### Scope Detection
|
|
66
|
+
|
|
67
|
+
Set scope_hint to "global" when the text contains signals like:
|
|
68
|
+
- "I always...", "in all my projects...", "my preference is..."
|
|
69
|
+
- "everywhere", "across all repos"
|
|
70
|
+
|
|
71
|
+
Default to "project" for everything else.
|
|
72
|
+
|
|
73
|
+
### Step 3: Persist Each Extraction
|
|
74
|
+
|
|
75
|
+
For each content item with extracted knowledge:
|
|
76
|
+
|
|
77
|
+
1. Call `memory.store_extraction` with the entities, facts, and decisions arrays
|
|
78
|
+
2. Call `memory.mark_distilled` with the content_item_id and facts_extracted count
|
|
79
|
+
3. If nothing was extracted, still call `memory.mark_distilled` with facts_extracted: 0
|
|
80
|
+
|
|
81
|
+
### Step 4: Report
|
|
82
|
+
|
|
83
|
+
Return a summary:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
## Distillation Complete
|
|
87
|
+
|
|
88
|
+
- Items processed: N
|
|
89
|
+
- Facts extracted: N
|
|
90
|
+
- Entities found: N
|
|
91
|
+
- Decisions captured: N
|
|
92
|
+
- Items skipped (nothing to extract): N
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Guidelines
|
|
96
|
+
|
|
97
|
+
- Process items one at a time to keep extractions focused
|
|
98
|
+
- Use `compact: true` on `memory.undistilled` for smaller responses
|
|
99
|
+
- Be conservative — only extract facts you're confident about (>0.7)
|
|
100
|
+
- Prefer "stated" strength over "inferred" unless clearly implied
|
|
101
|
+
- Do NOT fabricate facts — only extract what's actually in the text
|
|
102
|
+
- If text is mostly code/tool output with no conversational knowledge, mark as distilled with 0 facts
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Memory Recall Agent
|
|
2
|
+
|
|
3
|
+
Search long-term memory for facts, decisions, conventions, and architectural knowledge. Chains multiple memory tools to build comprehensive answers while saving main-agent context.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Provide a natural language query describing what you want to recall:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/memory-recall database migration strategy
|
|
11
|
+
/memory-recall authentication decisions
|
|
12
|
+
/memory-recall testing conventions
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. **Fast lookup** — Start with `memory.recall` for keyword matches
|
|
18
|
+
2. **Semantic search** — If recall returns few results, try `memory.recall_semantic` for conceptual matches
|
|
19
|
+
3. **Shortcuts** — For known categories, use `memory.decisions`, `memory.conventions`, or `memory.architecture`
|
|
20
|
+
4. **Deep dive** — For specific facts, use `memory.explain` to get provenance and `memory.fact_graph` to see relationships
|
|
21
|
+
5. **Synthesize** — Combine findings into a concise, structured answer
|
|
22
|
+
|
|
23
|
+
## Instructions
|
|
24
|
+
|
|
25
|
+
You are a memory recall specialist. Given a query, search ClaudeMemory using the available MCP tools and return a synthesized answer.
|
|
26
|
+
|
|
27
|
+
### Step 1: Initial Search
|
|
28
|
+
|
|
29
|
+
Run `memory.recall` with the user's query. If the query mentions decisions, conventions, or architecture, also run the appropriate shortcut tool in parallel.
|
|
30
|
+
|
|
31
|
+
### Step 2: Expand if Needed
|
|
32
|
+
|
|
33
|
+
If Step 1 returns fewer than 3 results:
|
|
34
|
+
- Try `memory.recall_semantic` with a rephrased version of the query
|
|
35
|
+
- Try `memory.search_concepts` with 2-3 key concepts extracted from the query
|
|
36
|
+
|
|
37
|
+
### Step 3: Enrich Key Facts
|
|
38
|
+
|
|
39
|
+
For the top 2-3 most relevant facts:
|
|
40
|
+
- Run `memory.explain` to get provenance (where the fact came from)
|
|
41
|
+
- If relationships matter, run `memory.fact_graph` to see connected facts
|
|
42
|
+
|
|
43
|
+
### Step 4: Synthesize
|
|
44
|
+
|
|
45
|
+
Return a structured response:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
## Memory Recall Results
|
|
49
|
+
|
|
50
|
+
### Key Facts
|
|
51
|
+
- [Fact 1 with provenance]
|
|
52
|
+
- [Fact 2 with provenance]
|
|
53
|
+
|
|
54
|
+
### Context
|
|
55
|
+
[How these facts relate to the query]
|
|
56
|
+
|
|
57
|
+
### Confidence
|
|
58
|
+
[High/Medium/Low based on number and freshness of supporting facts]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Guidelines
|
|
62
|
+
|
|
63
|
+
- Prefer `memory.recall` (fast, token-efficient) before escalating to semantic search
|
|
64
|
+
- Use `compact: true` on all tool calls to minimize token usage
|
|
65
|
+
- Do NOT fabricate facts — only report what memory tools return
|
|
66
|
+
- If no relevant facts found, say so clearly rather than guessing
|
|
67
|
+
- Include fact IDs so the main agent can reference them
|