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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +32 -2
  4. data/.claude/settings.json +65 -15
  5. data/.claude/settings.local.json +5 -2
  6. data/.claude/skills/improve/SKILL.md +113 -25
  7. data/.claude/skills/upgrade-dependencies/SKILL.md +154 -0
  8. data/.claude-plugin/commands/distill-transcripts.md +98 -0
  9. data/.claude-plugin/commands/memory-recall.md +67 -0
  10. data/.claude-plugin/marketplace.json +2 -2
  11. data/.claude-plugin/plugin.json +3 -3
  12. data/.claude-plugin/scripts/hook-runner.sh +14 -0
  13. data/.claude-plugin/scripts/serve-mcp.sh +14 -0
  14. data/.ruby-version +1 -1
  15. data/CHANGELOG.md +90 -1
  16. data/CLAUDE.md +56 -18
  17. data/README.md +35 -0
  18. data/db/migrations/013_add_mcp_tool_calls.rb +26 -0
  19. data/db/migrations/014_canonicalize_predicates.rb +30 -0
  20. data/docs/improvements.md +74 -74
  21. data/docs/influence/claude-mem.md +1 -0
  22. data/docs/influence/claude-supermemory.md +1 -0
  23. data/docs/influence/episodic-memory.md +1 -0
  24. data/docs/influence/grepai.md +1 -0
  25. data/docs/influence/kbs.md +1 -0
  26. data/docs/influence/lossless-claw.md +1 -0
  27. data/docs/influence/qmd.md +1 -0
  28. data/docs/quality_review.md +119 -224
  29. data/hooks/hooks.json +39 -7
  30. data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
  31. data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
  32. data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
  33. data/lib/claude_memory/commands/completion_command.rb +149 -0
  34. data/lib/claude_memory/commands/doctor_command.rb +2 -0
  35. data/lib/claude_memory/commands/embeddings_command.rb +198 -0
  36. data/lib/claude_memory/commands/help_command.rb +12 -1
  37. data/lib/claude_memory/commands/hook_command.rb +2 -1
  38. data/lib/claude_memory/commands/index_command.rb +85 -78
  39. data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
  40. data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
  41. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
  42. data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
  43. data/lib/claude_memory/commands/install_skill_command.rb +78 -0
  44. data/lib/claude_memory/commands/registry.rb +47 -32
  45. data/lib/claude_memory/commands/reject_command.rb +62 -0
  46. data/lib/claude_memory/commands/restore_command.rb +77 -0
  47. data/lib/claude_memory/commands/skills/distill-transcripts.md +102 -0
  48. data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
  49. data/lib/claude_memory/commands/stats_command.rb +98 -2
  50. data/lib/claude_memory/configuration.rb +14 -1
  51. data/lib/claude_memory/core/fact_ranker.rb +2 -2
  52. data/lib/claude_memory/core/rr_fusion.rb +23 -6
  53. data/lib/claude_memory/core/snippet_extractor.rb +7 -3
  54. data/lib/claude_memory/core/text_builder.rb +11 -0
  55. data/lib/claude_memory/distill/json_schema.md +8 -4
  56. data/lib/claude_memory/distill/null_distiller.rb +2 -0
  57. data/lib/claude_memory/domain/entity.rb +13 -1
  58. data/lib/claude_memory/domain/fact.rb +26 -2
  59. data/lib/claude_memory/domain/provenance.rb +0 -1
  60. data/lib/claude_memory/embeddings/api_adapter.rb +97 -0
  61. data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
  62. data/lib/claude_memory/embeddings/fastembed_adapter.rb +46 -12
  63. data/lib/claude_memory/embeddings/generator.rb +4 -0
  64. data/lib/claude_memory/embeddings/inspector.rb +91 -0
  65. data/lib/claude_memory/embeddings/model_registry.rb +210 -0
  66. data/lib/claude_memory/embeddings/resolver.rb +44 -0
  67. data/lib/claude_memory/hook/context_injector.rb +58 -2
  68. data/lib/claude_memory/hook/distillation_runner.rb +46 -0
  69. data/lib/claude_memory/hook/handler.rb +11 -2
  70. data/lib/claude_memory/index/vector_index.rb +15 -2
  71. data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
  72. data/lib/claude_memory/ingest/ingester.rb +17 -0
  73. data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
  74. data/lib/claude_memory/mcp/handlers/management_handlers.rb +169 -0
  75. data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
  76. data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
  77. data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
  78. data/lib/claude_memory/mcp/handlers/stats_handlers.rb +205 -0
  79. data/lib/claude_memory/mcp/instructions_builder.rb +19 -1
  80. data/lib/claude_memory/mcp/query_guide.rb +10 -0
  81. data/lib/claude_memory/mcp/response_formatter.rb +1 -0
  82. data/lib/claude_memory/mcp/server.rb +22 -1
  83. data/lib/claude_memory/mcp/telemetry.rb +86 -0
  84. data/lib/claude_memory/mcp/text_summary.rb +26 -0
  85. data/lib/claude_memory/mcp/tool_definitions.rb +116 -4
  86. data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
  87. data/lib/claude_memory/mcp/tools.rb +50 -679
  88. data/lib/claude_memory/publish.rb +40 -5
  89. data/lib/claude_memory/recall/dual_engine.rb +105 -0
  90. data/lib/claude_memory/recall/legacy_engine.rb +138 -0
  91. data/lib/claude_memory/recall/query_core.rb +371 -0
  92. data/lib/claude_memory/recall.rb +121 -673
  93. data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
  94. data/lib/claude_memory/resolve/resolver.rb +43 -0
  95. data/lib/claude_memory/shortcuts.rb +4 -4
  96. data/lib/claude_memory/store/retry_handler.rb +61 -0
  97. data/lib/claude_memory/store/schema_manager.rb +68 -0
  98. data/lib/claude_memory/store/sqlite_store.rb +334 -201
  99. data/lib/claude_memory/store/store_manager.rb +50 -1
  100. data/lib/claude_memory/sweep/maintenance.rb +115 -1
  101. data/lib/claude_memory/sweep/sweeper.rb +3 -0
  102. data/lib/claude_memory/templates/hooks.example.json +26 -7
  103. data/lib/claude_memory/version.rb +1 -1
  104. data/lib/claude_memory.rb +16 -0
  105. metadata +48 -8
  106. data/.claude/memory.sqlite3-shm +0 -0
  107. 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" => 10}
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" => ingest_cmd, "timeout" => 10}
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
- {"type" => "command", "command" => sweep_cmd, "timeout" => 30}
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
- {"type" => "command", "command" => sweep_cmd, "timeout" => 30}
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 for dynamic dispatch
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 names
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" => "HelpCommand",
12
- "version" => "VersionCommand",
13
- "doctor" => "DoctorCommand",
14
- "stats" => "StatsCommand",
15
- "promote" => "PromoteCommand",
16
- "search" => "SearchCommand",
17
- "explain" => "ExplainCommand",
18
- "conflicts" => "ConflictsCommand",
19
- "changes" => "ChangesCommand",
20
- "recall" => "RecallCommand",
21
- "sweep" => "SweepCommand",
22
- "ingest" => "IngestCommand",
23
- "publish" => "PublishCommand",
24
- "db:init" => "DbInitCommand",
25
- "init" => "InitCommand",
26
- "uninstall" => "UninstallCommand",
27
- "serve-mcp" => "ServeMcpCommand",
28
- "hook" => "HookCommand",
29
- "index" => "IndexCommand",
30
- "recover" => "RecoverCommand",
31
- "compact" => "CompactCommand",
32
- "export" => "ExportCommand",
33
- "git-lfs" => "GitLfsCommand"
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
- return nil if command_name.nil?
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