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
@@ -4,15 +4,22 @@ require "digest"
4
4
  require "fileutils"
5
5
 
6
6
  module ClaudeMemory
7
+ # Generates Markdown snapshots from active facts for use as project memory.
8
+ # Publishes to .claude/rules/ (shared), a local file, or the home directory.
7
9
  class Publish
8
10
  RULES_DIR = ".claude/rules"
9
11
  GENERATED_FILE = "claude_memory.generated.md"
10
12
 
13
+ # @param store [Store::SQLiteStore] database store for reading facts
14
+ # @param file_system [Infrastructure::FileSystem] filesystem abstraction for I/O
11
15
  def initialize(store, file_system: Infrastructure::FileSystem.new)
12
16
  @store = store
13
17
  @fs = file_system
14
18
  end
15
19
 
20
+ # Generate a complete Markdown snapshot with header and body
21
+ # @param since [String, nil] ISO 8601 timestamp to include recent supersessions
22
+ # @return [String] full Markdown document
16
23
  def generate_snapshot(since: nil)
17
24
  header = <<~HEADER
18
25
  <!--
@@ -28,6 +35,12 @@ module ClaudeMemory
28
35
  header + generate_body(since: since)
29
36
  end
30
37
 
38
+ # Write snapshot to disk if content has changed
39
+ # @param mode [Symbol] output target (:shared, :local, or :home)
40
+ # @param granularity [Symbol] snapshot granularity (currently only :repo)
41
+ # @param since [String, nil] ISO 8601 timestamp for recent supersessions
42
+ # @param rules_dir [String, nil] override rules directory path
43
+ # @return [Hash] result with :status (:updated or :unchanged) and :path
31
44
  def publish!(mode: :shared, granularity: :repo, since: nil, rules_dir: nil)
32
45
  path = output_path(mode, rules_dir: rules_dir)
33
46
  body = generate_body(since: since)
@@ -97,8 +110,9 @@ module ClaudeMemory
97
110
  .all
98
111
  end
99
112
 
113
+ # @return [String] Markdown section for decision facts
100
114
  def generate_decisions_section(facts)
101
- decisions = facts.select { |f| f[:predicate] == "decision" || f[:predicate]&.start_with?("decided_") }
115
+ decisions = facts.select { |f| Resolve::PredicatePolicy.section_for(f[:predicate]) == :decisions }
102
116
  return "" if decisions.empty?
103
117
 
104
118
  lines = ["## Current Decisions\n"]
@@ -108,8 +122,9 @@ module ClaudeMemory
108
122
  lines.join("\n") + "\n"
109
123
  end
110
124
 
125
+ # @return [String] Markdown section for convention facts
111
126
  def generate_conventions_section(facts)
112
- conventions = facts.select { |f| f[:predicate] == "convention" || f[:predicate]&.include?("_convention") }
127
+ conventions = facts.select { |f| Resolve::PredicatePolicy.section_for(f[:predicate]) == :conventions }
113
128
  return "" if conventions.empty?
114
129
 
115
130
  lines = ["## Conventions\n"]
@@ -119,10 +134,9 @@ module ClaudeMemory
119
134
  lines.join("\n") + "\n"
120
135
  end
121
136
 
137
+ # @return [String] Markdown section for technical constraint facts
122
138
  def generate_constraints_section(facts)
123
- constraints = facts.select do |f|
124
- %w[uses_database uses_framework deployment_platform auth_method].include?(f[:predicate])
125
- end
139
+ constraints = facts.select { |f| Resolve::PredicatePolicy.section_for(f[:predicate]) == :constraints }
126
140
  return "" if constraints.empty?
127
141
 
128
142
  lines = ["## Technical Constraints\n"]
@@ -132,6 +146,25 @@ module ClaudeMemory
132
146
  lines.join("\n") + "\n"
133
147
  end
134
148
 
149
+ # @return [String] Markdown section for additional knowledge grouped by predicate
150
+ def generate_additional_section(facts)
151
+ additional = facts.select { |f| Resolve::PredicatePolicy.section_for(f[:predicate]) == :additional }
152
+ return "" if additional.empty?
153
+
154
+ grouped = additional.group_by { |f| f[:predicate] }
155
+ lines = ["## Additional Knowledge\n"]
156
+ grouped.each do |predicate, group_facts|
157
+ lines << "### #{humanize(predicate)}\n"
158
+ group_facts.each do |f|
159
+ subject = f[:subject_name] || "repo"
160
+ lines << "- #{subject}: #{f[:object_literal]}"
161
+ end
162
+ lines << ""
163
+ end
164
+ lines.join("\n") + "\n"
165
+ end
166
+
167
+ # @return [String] Markdown section for open conflicts
135
168
  def generate_conflicts_section(conflicts)
136
169
  return "" if conflicts.empty?
137
170
 
@@ -143,6 +176,7 @@ module ClaudeMemory
143
176
  lines.join("\n") + "\n"
144
177
  end
145
178
 
179
+ # @return [String] Markdown section for recently superseded facts
146
180
  def generate_supersessions_section(supersessions)
147
181
  return "" if supersessions.empty?
148
182
 
@@ -162,6 +196,7 @@ module ClaudeMemory
162
196
  sections << generate_decisions_section(facts)
163
197
  sections << generate_conventions_section(facts)
164
198
  sections << generate_constraints_section(facts)
199
+ sections << generate_additional_section(facts)
165
200
  sections << generate_conflicts_section(conflicts) if conflicts.any?
166
201
  sections << generate_supersessions_section(recent_supersessions) if recent_supersessions.any?
167
202
 
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ class Recall
5
+ # Query engine for dual-database mode (StoreManager with global + project DBs).
6
+ # Wraps DualQueryTemplate around shared QueryCore methods.
7
+ class DualEngine
8
+ include QueryCore
9
+
10
+ def initialize(manager, embedding_generator:, project_path:)
11
+ @manager = manager
12
+ @embedding_generator = embedding_generator
13
+ @project_path = project_path
14
+ end
15
+
16
+ def query(query_text, limit:, scope:, include_raw_text: false, intent: nil)
17
+ effective_query = intent_augmented_query(query_text, intent)
18
+ results = dual_execute(scope: scope, limit: limit) do |store, source|
19
+ query_single_store(store, effective_query, limit: limit, source: source, include_raw_text: include_raw_text)
20
+ end
21
+ dedupe_and_sort(results, limit)
22
+ end
23
+
24
+ def query_index(query_text, limit:, scope:, intent: nil)
25
+ effective_query = intent_augmented_query(query_text, intent)
26
+ results = dual_execute(scope: scope, limit: limit) do |store, source|
27
+ query_index_single_store(store, effective_query, limit: limit, source: source)
28
+ end
29
+ dedupe_and_sort_index(results, limit)
30
+ end
31
+
32
+ def fact_graph(fact_id, depth:, scope:)
33
+ scope ||= SCOPE_PROJECT
34
+ store = @manager.store_for_scope(scope)
35
+ Core::FactGraph.build(store, fact_id, depth: depth)
36
+ end
37
+
38
+ def explain(fact_id_or_docid, scope:)
39
+ scope ||= SCOPE_PROJECT
40
+ store = @manager.store_for_scope(scope)
41
+ fact_id = resolve_fact_identifier(store, fact_id_or_docid)
42
+ explain_from_store(store, fact_id)
43
+ end
44
+
45
+ def changes(since:, limit:, scope:)
46
+ results = dual_execute(scope: scope, limit: limit) do |store, source|
47
+ changes = fetch_changes(store, since, limit)
48
+ Core::ResultSorter.annotate_source(changes, source)
49
+ end
50
+ Core::ResultSorter.sort_by_timestamp(results, limit)
51
+ end
52
+
53
+ def conflicts(scope:)
54
+ dual_execute(scope: scope) do |store, source|
55
+ conflicts = store.open_conflicts
56
+ Core::ResultSorter.annotate_source(conflicts, source)
57
+ end
58
+ end
59
+
60
+ def facts_by_branch(branch_name, limit:, scope:)
61
+ results = dual_execute(scope: scope, limit: limit) do |store, source|
62
+ facts_by_context_single(store, :git_branch, branch_name, limit: limit, source: source)
63
+ end
64
+ dedupe_and_sort(results, limit)
65
+ end
66
+
67
+ def facts_by_directory(cwd, limit:, scope:)
68
+ results = dual_execute(scope: scope, limit: limit) do |store, source|
69
+ facts_by_context_single(store, :cwd, cwd, limit: limit, source: source)
70
+ end
71
+ dedupe_and_sort(results, limit)
72
+ end
73
+
74
+ def facts_by_tool(tool_name, limit:, scope:)
75
+ results = dual_execute(scope: scope, limit: limit) do |store, source|
76
+ facts_by_tool_single(store, tool_name, limit: limit, source: source)
77
+ end
78
+ dedupe_and_sort(results, limit)
79
+ end
80
+
81
+ def query_semantic(text, limit:, scope:, mode:, explain: false, intent: nil)
82
+ effective_text = intent_augmented_query(text, intent)
83
+ results = dual_execute(scope: scope, limit: limit) do |store, source|
84
+ query_semantic_single(store, effective_text, limit: limit * 3, mode: mode, source: source, explain: explain,
85
+ skip_fts_shortcut: !intent.nil?)
86
+ end
87
+ dedupe_by_fact_id(results, limit)
88
+ end
89
+
90
+ def query_concepts(concepts, limit:, scope:)
91
+ results = dual_execute(scope: scope, limit: limit) do |store, source|
92
+ query_concepts_single(store, concepts, limit: limit * 2, source: source)
93
+ end
94
+ dedupe_by_fact_id(results, limit)
95
+ end
96
+
97
+ private
98
+
99
+ def dual_execute(scope:, limit: nil, &operation)
100
+ template = DualQueryTemplate.new(@manager)
101
+ template.execute(scope: scope, limit: limit, &operation)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ class Recall
5
+ # Query engine for legacy single-store mode.
6
+ # Operates directly on one SQLiteStore with local scope filtering.
7
+ class LegacyEngine
8
+ include QueryCore
9
+
10
+ def initialize(store, fts:, embedding_generator:, project_path:)
11
+ @store = store
12
+ @fts = fts
13
+ @embedding_generator = embedding_generator
14
+ @project_path = project_path
15
+ end
16
+
17
+ def query(query_text, limit:, scope:, include_raw_text: false, intent: nil)
18
+ effective_query = intent_augmented_query(query_text, intent)
19
+ content_ids = @fts.search(effective_query, limit: limit * 3)
20
+ return [] if content_ids.empty?
21
+
22
+ provenance_by_content = @store.provenance
23
+ .select(:fact_id, :content_item_id)
24
+ .where(content_item_id: content_ids)
25
+ .all
26
+ .group_by { |p| p[:content_item_id] }
27
+
28
+ all_fact_ids = []
29
+ seen_fact_ids = Set.new
30
+ content_ids.each do |content_id|
31
+ (provenance_by_content[content_id] || []).each do |prov|
32
+ next if seen_fact_ids.include?(prov[:fact_id])
33
+ seen_fact_ids.add(prov[:fact_id])
34
+ all_fact_ids << prov[:fact_id]
35
+ end
36
+ end
37
+
38
+ return [] if all_fact_ids.empty?
39
+
40
+ facts_by_id = batch_find_facts(@store, all_fact_ids)
41
+
42
+ selected_fact_ids = []
43
+ all_fact_ids.each do |fact_id|
44
+ fact = facts_by_id[fact_id]
45
+ next unless fact
46
+ next unless fact_matches_scope?(fact, scope)
47
+ selected_fact_ids << fact_id
48
+ break if selected_fact_ids.size >= limit
49
+ end
50
+
51
+ return [] if selected_fact_ids.empty?
52
+
53
+ receipts_by_fact_id = batch_find_receipts(@store, selected_fact_ids)
54
+
55
+ facts_with_provenance = selected_fact_ids.map do |fact_id|
56
+ {
57
+ fact: facts_by_id[fact_id],
58
+ receipts: receipts_by_fact_id[fact_id] || []
59
+ }
60
+ end
61
+
62
+ sort_by_scope_priority(facts_with_provenance)
63
+ end
64
+
65
+ def query_index(query_text, limit:, scope:, intent: nil)
66
+ effective_query = intent_augmented_query(query_text, intent)
67
+ options = Index::QueryOptions.new(
68
+ query_text: effective_query,
69
+ limit: limit,
70
+ scope: :all,
71
+ source: :legacy
72
+ )
73
+
74
+ query = Index::IndexQuery.new(@store, options)
75
+ results = query.execute
76
+
77
+ results.select do |result|
78
+ fact = Core::FactQueryBuilder.find_fact(@store, result[:id])
79
+ fact && fact_matches_scope?(fact, scope)
80
+ end
81
+ end
82
+
83
+ def fact_graph(fact_id, depth:, scope:)
84
+ Core::FactGraph.build(@store, fact_id, depth: depth)
85
+ end
86
+
87
+ def explain(fact_id_or_docid, scope:)
88
+ fact_id = resolve_fact_identifier(@store, fact_id_or_docid)
89
+ explain_from_store(@store, fact_id)
90
+ end
91
+
92
+ def changes(since:, limit:, scope:)
93
+ ds = @store.facts
94
+ .select(:id, :docid, :subject_entity_id, :predicate, :object_literal, :status, :created_at, :scope, :project_path)
95
+ .where { created_at >= since }
96
+ .order(Sequel.desc(:created_at))
97
+ .limit(limit)
98
+
99
+ ds = apply_scope_filter(ds, scope)
100
+ ds.all
101
+ end
102
+
103
+ def conflicts(scope:)
104
+ all_conflicts = @store.open_conflicts
105
+ return all_conflicts if scope == SCOPE_ALL
106
+
107
+ all_conflicts.select do |conflict|
108
+ fact_a = Core::FactQueryBuilder.find_fact(@store, conflict[:fact_a_id])
109
+ fact_b = Core::FactQueryBuilder.find_fact(@store, conflict[:fact_b_id])
110
+
111
+ fact_matches_scope?(fact_a, scope) || fact_matches_scope?(fact_b, scope)
112
+ end
113
+ end
114
+
115
+ def facts_by_branch(branch_name, limit:, scope:)
116
+ facts_by_context_single(@store, :git_branch, branch_name, limit: limit, source: :legacy)
117
+ end
118
+
119
+ def facts_by_directory(cwd, limit:, scope:)
120
+ facts_by_context_single(@store, :cwd, cwd, limit: limit, source: :legacy)
121
+ end
122
+
123
+ def facts_by_tool(tool_name, limit:, scope:)
124
+ facts_by_tool_single(@store, tool_name, limit: limit, source: :legacy)
125
+ end
126
+
127
+ def query_semantic(text, limit:, scope:, mode:, explain: false, intent: nil)
128
+ effective_text = intent_augmented_query(text, intent)
129
+ query_semantic_single(@store, effective_text, limit: limit, mode: mode, source: :legacy, explain: explain,
130
+ skip_fts_shortcut: !intent.nil?)
131
+ end
132
+
133
+ def query_concepts(concepts, limit:, scope:)
134
+ query_concepts_single(@store, concepts, limit: limit, source: :legacy)
135
+ end
136
+ end
137
+ end
138
+ end