claude_memory 0.8.0 → 0.9.1

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +94 -2
  4. data/.claude/settings.json +30 -52
  5. data/.claude/settings.local.json +3 -1
  6. data/.claude/skills/release/SKILL.md +168 -0
  7. data/.claude/skills/upgrade-dependencies/SKILL.md +154 -0
  8. data/.claude-plugin/marketplace.json +2 -2
  9. data/.claude-plugin/plugin.json +3 -3
  10. data/.claude-plugin/scripts/hook-runner.sh +14 -0
  11. data/.claude-plugin/scripts/serve-mcp.sh +14 -0
  12. data/.ruby-version +1 -1
  13. data/CHANGELOG.md +47 -0
  14. data/CLAUDE.md +31 -17
  15. data/README.md +35 -0
  16. data/db/migrations/013_add_mcp_tool_calls.rb +26 -0
  17. data/db/migrations/014_canonicalize_predicates.rb +30 -0
  18. data/docs/improvements.md +58 -20
  19. data/docs/influence/claude-mem.md +1 -0
  20. data/docs/influence/claude-supermemory.md +1 -0
  21. data/docs/influence/episodic-memory.md +1 -0
  22. data/docs/influence/grepai.md +1 -0
  23. data/docs/influence/kbs.md +1 -0
  24. data/docs/influence/lossless-claw.md +1 -0
  25. data/docs/influence/qmd.md +1 -0
  26. data/lib/claude_memory/commands/completion_command.rb +1 -31
  27. data/lib/claude_memory/commands/embeddings_command.rb +198 -0
  28. data/lib/claude_memory/commands/help_command.rb +8 -1
  29. data/lib/claude_memory/commands/registry.rb +47 -34
  30. data/lib/claude_memory/commands/reject_command.rb +62 -0
  31. data/lib/claude_memory/commands/restore_command.rb +77 -0
  32. data/lib/claude_memory/commands/skills/distill-transcripts.md +5 -1
  33. data/lib/claude_memory/commands/stats_command.rb +98 -2
  34. data/lib/claude_memory/configuration.rb +14 -1
  35. data/lib/claude_memory/distill/json_schema.md +8 -4
  36. data/lib/claude_memory/distill/null_distiller.rb +2 -0
  37. data/lib/claude_memory/domain/entity.rb +13 -1
  38. data/lib/claude_memory/domain/fact.rb +26 -2
  39. data/lib/claude_memory/embeddings/api_adapter.rb +5 -4
  40. data/lib/claude_memory/embeddings/fastembed_adapter.rb +43 -13
  41. data/lib/claude_memory/embeddings/inspector.rb +91 -0
  42. data/lib/claude_memory/embeddings/model_registry.rb +210 -0
  43. data/lib/claude_memory/embeddings/resolver.rb +32 -6
  44. data/lib/claude_memory/ingest/ingester.rb +17 -0
  45. data/lib/claude_memory/mcp/handlers/management_handlers.rb +24 -0
  46. data/lib/claude_memory/mcp/handlers/stats_handlers.rb +5 -2
  47. data/lib/claude_memory/mcp/instructions_builder.rb +17 -0
  48. data/lib/claude_memory/mcp/server.rb +30 -3
  49. data/lib/claude_memory/mcp/telemetry.rb +86 -0
  50. data/lib/claude_memory/mcp/tool_definitions.rb +86 -3
  51. data/lib/claude_memory/mcp/tools.rb +10 -0
  52. data/lib/claude_memory/publish.rb +40 -5
  53. data/lib/claude_memory/recall.rb +81 -0
  54. data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
  55. data/lib/claude_memory/resolve/resolver.rb +43 -0
  56. data/lib/claude_memory/store/schema_manager.rb +1 -1
  57. data/lib/claude_memory/store/sqlite_store.rb +250 -1
  58. data/lib/claude_memory/store/store_manager.rb +50 -1
  59. data/lib/claude_memory/sweep/maintenance.rb +115 -1
  60. data/lib/claude_memory/sweep/sweeper.rb +3 -0
  61. data/lib/claude_memory/version.rb +1 -1
  62. data/lib/claude_memory.rb +5 -0
  63. metadata +27 -8
  64. data/.claude/memory.sqlite3-shm +0 -0
  65. data/.claude/memory.sqlite3-wal +0 -0
@@ -1,29 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeMemory
4
+ # Query interface for facts across dual databases (global + project).
5
+ # Delegates to DualEngine or LegacyEngine depending on the store type.
4
6
  class Recall
7
+ # @return [String] query only project-scoped facts
5
8
  SCOPE_PROJECT = "project"
9
+ # @return [String] query only global-scoped facts
6
10
  SCOPE_GLOBAL = "global"
11
+ # @return [String] query both project and global facts (default)
7
12
  SCOPE_ALL = "all"
8
13
 
9
14
  class << self
15
+ # @param manager [Store::StoreManager] dual-database manager
16
+ # @param limit [Integer] max results
17
+ # @return [Array<Hash>] recent decision facts
10
18
  def recent_decisions(manager, limit: 10)
11
19
  Shortcuts.for(:decisions, manager, limit: limit)
12
20
  end
13
21
 
22
+ # @param manager [Store::StoreManager] dual-database manager
23
+ # @param limit [Integer] max results
24
+ # @return [Array<Hash>] architecture-related facts
14
25
  def architecture_choices(manager, limit: 10)
15
26
  Shortcuts.for(:architecture, manager, limit: limit)
16
27
  end
17
28
 
29
+ # @param manager [Store::StoreManager] dual-database manager
30
+ # @param limit [Integer] max results
31
+ # @return [Array<Hash>] convention facts
18
32
  def conventions(manager, limit: 20)
19
33
  Shortcuts.for(:conventions, manager, limit: limit)
20
34
  end
21
35
 
36
+ # @param manager [Store::StoreManager] dual-database manager
37
+ # @param limit [Integer] max results
38
+ # @return [Array<Hash>] project configuration facts
22
39
  def project_config(manager, limit: 10)
23
40
  Shortcuts.for(:project_config, manager, limit: limit)
24
41
  end
25
42
  end
26
43
 
44
+ # @param store_or_manager [Store::SQLiteStore, Store::StoreManager] database store or dual-database manager
45
+ # @param fts [Index::LexicalFTS, nil] full-text search index (used only with legacy single-store)
46
+ # @param project_path [String, nil] project root path (defaults to Configuration#project_dir)
47
+ # @param env [Hash] environment variables
48
+ # @param embedding_generator [Object, nil] vector embedding generator for semantic search
27
49
  def initialize(store_or_manager, fts: nil, project_path: nil, env: ENV, embedding_generator: nil)
28
50
  config = Configuration.new(env)
29
51
  resolved_project_path = project_path || config.project_dir
@@ -45,46 +67,105 @@ module ClaudeMemory
45
67
  end
46
68
  end
47
69
 
70
+ # Search facts by text query using FTS5
71
+ # @param query_text [String] search terms
72
+ # @param limit [Integer] max results
73
+ # @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
74
+ # @param include_raw_text [Boolean] include source content text in results
75
+ # @param intent [String, nil] query intent hint for ranking
76
+ # @return [Array<Hash>] matching facts with provenance
48
77
  def query(query_text, limit: 10, scope: SCOPE_ALL, include_raw_text: false, intent: nil)
49
78
  @engine.query(query_text, limit: limit, scope: scope, include_raw_text: include_raw_text, intent: intent)
50
79
  end
51
80
 
81
+ # Search content items (not facts) via FTS5 index
82
+ # @param query_text [String] search terms
83
+ # @param limit [Integer] max results
84
+ # @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
85
+ # @param intent [String, nil] query intent hint for ranking
86
+ # @return [Array<Hash>] matching content items
52
87
  def query_index(query_text, limit: 20, scope: SCOPE_ALL, intent: nil)
53
88
  @engine.query_index(query_text, limit: limit, scope: scope, intent: intent)
54
89
  end
55
90
 
91
+ # Traverse fact relationships (supersessions, conflicts) as a graph
92
+ # @param fact_id [Integer] starting fact ID
93
+ # @param depth [Integer] traversal depth
94
+ # @param scope [String, nil] optional scope filter
95
+ # @return [Hash] graph with nodes and edges
56
96
  def fact_graph(fact_id, depth: 2, scope: nil)
57
97
  @engine.fact_graph(fact_id, depth: depth, scope: scope)
58
98
  end
59
99
 
100
+ # Show provenance chain for a fact
101
+ # @param fact_id_or_docid [Integer, String] fact ID or document ID
102
+ # @param scope [String, nil] optional scope filter
103
+ # @return [Hash] provenance details including source content
60
104
  def explain(fact_id_or_docid, scope: nil)
61
105
  @engine.explain(fact_id_or_docid, scope: scope)
62
106
  end
63
107
 
108
+ # List facts created or modified since a given time
109
+ # @param since [String] ISO 8601 timestamp
110
+ # @param limit [Integer] max results
111
+ # @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
112
+ # @return [Array<Hash>] recently changed facts
64
113
  def changes(since:, limit: 50, scope: SCOPE_ALL)
65
114
  @engine.changes(since: since, limit: limit, scope: scope)
66
115
  end
67
116
 
117
+ # List open fact conflicts
118
+ # @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
119
+ # @return [Array<Hash>] unresolved conflicts
68
120
  def conflicts(scope: SCOPE_ALL)
69
121
  @engine.conflicts(scope: scope)
70
122
  end
71
123
 
124
+ # Find facts associated with a git branch
125
+ # @param branch_name [String] git branch name
126
+ # @param limit [Integer] max results
127
+ # @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
128
+ # @return [Array<Hash>] facts from the given branch
72
129
  def facts_by_branch(branch_name, limit: 20, scope: SCOPE_ALL)
73
130
  @engine.facts_by_branch(branch_name, limit: limit, scope: scope)
74
131
  end
75
132
 
133
+ # Find facts associated with a working directory
134
+ # @param cwd [String] directory path
135
+ # @param limit [Integer] max results
136
+ # @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
137
+ # @return [Array<Hash>] facts from the given directory
76
138
  def facts_by_directory(cwd, limit: 20, scope: SCOPE_ALL)
77
139
  @engine.facts_by_directory(cwd, limit: limit, scope: scope)
78
140
  end
79
141
 
142
+ # Find facts associated with a specific tool
143
+ # @param tool_name [String] tool name (e.g., "Read", "Bash")
144
+ # @param limit [Integer] max results
145
+ # @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
146
+ # @return [Array<Hash>] facts from sessions using the given tool
80
147
  def facts_by_tool(tool_name, limit: 20, scope: SCOPE_ALL)
81
148
  @engine.facts_by_tool(tool_name, limit: limit, scope: scope)
82
149
  end
83
150
 
151
+ # Search facts using vector embeddings (semantic similarity)
152
+ # @param text [String] natural language query
153
+ # @param limit [Integer] max results
154
+ # @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
155
+ # @param mode [Symbol] :vector, :lexical, or :both (hybrid RRF)
156
+ # @param explain [Boolean] include scoring breakdown in results
157
+ # @param intent [String, nil] query intent hint for ranking
158
+ # @return [Array<Hash>] semantically similar facts
84
159
  def query_semantic(text, limit: 10, scope: SCOPE_ALL, mode: :both, explain: false, intent: nil)
85
160
  @engine.query_semantic(text, limit: limit, scope: scope, mode: mode, explain: explain, intent: intent)
86
161
  end
87
162
 
163
+ # Find facts at the intersection of multiple concepts
164
+ # @param concepts [Array<String>] 2-5 concept terms to intersect
165
+ # @param limit [Integer] max results
166
+ # @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
167
+ # @return [Array<Hash>] facts matching all given concepts
168
+ # @raise [ArgumentError] if concepts count is not 2-5
88
169
  def query_concepts(concepts, limit: 10, scope: SCOPE_ALL)
89
170
  raise ArgumentError, "Must provide 2-5 concepts" unless (2..5).cover?(concepts.size)
90
171
 
@@ -3,17 +3,77 @@
3
3
  module ClaudeMemory
4
4
  module Resolve
5
5
  class PredicatePolicy
6
+ # Canonical predicate vocabulary. Curated after a multi-project survey
7
+ # of real memory databases under ~/src — predicates with zero facts
8
+ # across every database were pruned; predicates observed in the wild
9
+ # but missing from the policy (architecture, uses_language) were added.
10
+ #
11
+ # - convention / decision: workhorse multi-value predicates
12
+ # - uses_framework / uses_language: multi-value (projects use multiple)
13
+ # - uses_database / deployment_platform / auth_method: single-value,
14
+ # correctly 1:1 per project in observed data
15
+ # - architecture: multi-value structural knowledge (was implicit)
6
16
  POLICIES = {
7
17
  "convention" => {cardinality: :multi, exclusive: false},
8
18
  "decision" => {cardinality: :multi, exclusive: false},
9
- "auth_method" => {cardinality: :single, exclusive: true},
19
+ "architecture" => {cardinality: :multi, exclusive: false},
20
+ "uses_framework" => {cardinality: :multi, exclusive: false},
21
+ "uses_language" => {cardinality: :multi, exclusive: false},
10
22
  "uses_database" => {cardinality: :single, exclusive: true},
11
- "uses_framework" => {cardinality: :single, exclusive: true},
12
- "deployment_platform" => {cardinality: :single, exclusive: true}
23
+ "deployment_platform" => {cardinality: :single, exclusive: true},
24
+ "auth_method" => {cardinality: :single, exclusive: true}
13
25
  }.freeze
14
26
 
15
27
  DEFAULT_POLICY = {cardinality: :multi, exclusive: false}.freeze
16
28
 
29
+ # Drift canonicalization. Maps predicate names the distiller has
30
+ # organically coined onto the canonical form in POLICIES. Consulted
31
+ # at insert time by the Resolver so synonym drift never fragments
32
+ # the knowledge graph.
33
+ #
34
+ # Entries observed in real project DBs:
35
+ # - has_convention (chaos_to_the_rescue): prefix-drift of convention
36
+ # - primary_language (prior policy entry): supplanted by uses_language
37
+ # which the distiller emits naturally and has multi-value semantics
38
+ SYNONYMS = {
39
+ "has_convention" => "convention",
40
+ "primary_language" => "uses_language"
41
+ }.freeze
42
+
43
+ # Section classification for the published snapshot. Keeps Publish
44
+ # from hard-coding predicate names; adding a new predicate to the
45
+ # policy and the section map in one place updates everything.
46
+ SECTION_MAP = {
47
+ "decision" => :decisions,
48
+ "convention" => :conventions,
49
+ "uses_database" => :constraints,
50
+ "uses_framework" => :constraints,
51
+ "uses_language" => :constraints,
52
+ "deployment_platform" => :constraints,
53
+ "auth_method" => :constraints
54
+ # architecture intentionally falls through to :additional for now
55
+ }.freeze
56
+
57
+ def self.known_predicates
58
+ POLICIES.keys
59
+ end
60
+
61
+ # Return the canonical form of a predicate name, applying known
62
+ # synonym mappings. Leaves unmapped predicates unchanged.
63
+ def self.canonicalize(predicate)
64
+ return predicate if predicate.nil?
65
+ SYNONYMS.fetch(predicate, predicate)
66
+ end
67
+
68
+ # Return the snapshot section a predicate belongs to.
69
+ # Respects legacy prefix/suffix patterns (decided_*, *_convention)
70
+ # that pre-date the policy.
71
+ def self.section_for(predicate)
72
+ return :decisions if predicate&.start_with?("decided_")
73
+ return :conventions if predicate&.include?("_convention")
74
+ SECTION_MAP.fetch(predicate, :additional)
75
+ end
76
+
17
77
  def self.policy_for(predicate)
18
78
  POLICIES.fetch(predicate, DEFAULT_POLICY)
19
79
  end
@@ -2,11 +2,32 @@
2
2
 
3
3
  module ClaudeMemory
4
4
  module Resolve
5
+ # Truth maintenance engine that processes distilled extractions into stored facts.
6
+ # Wraps entity resolution, fact insertion, supersession, and conflict detection
7
+ # in a single database transaction.
8
+ #
9
+ # @example
10
+ # resolver = Resolver.new(store)
11
+ # result = resolver.apply(extraction, content_item_id: 42, scope: "project")
12
+ # result[:facts_created] #=> 3
13
+ # result[:facts_superseded] #=> 1
5
14
  class Resolver
15
+ # @param store [Store::SQLiteStore] backing database for reads and writes
6
16
  def initialize(store)
7
17
  @store = store
8
18
  end
9
19
 
20
+ # Apply a distilled extraction, resolving each fact against existing knowledge.
21
+ # Facts may be inserted, reinforce an existing fact, supersede old facts, or
22
+ # create a conflict when the resolution is ambiguous.
23
+ #
24
+ # @param extraction [#entities, #facts] distilled extraction with entities and facts
25
+ # @param content_item_id [Integer, nil] source content item for provenance
26
+ # @param occurred_at [String, nil] ISO 8601 timestamp (defaults to now)
27
+ # @param project_path [String, nil] project path for scoped facts
28
+ # @param scope [String] default scope for facts ("project" or "global")
29
+ # @return [Hash] counts keyed by :entities_created, :facts_created,
30
+ # :facts_superseded, :conflicts_created, :provenance_created
10
31
  def apply(extraction, content_item_id: nil, occurred_at: nil, project_path: nil, scope: "project")
11
32
  occurred_at ||= Time.now.utc.iso8601
12
33
 
@@ -49,6 +70,21 @@ module ClaudeMemory
49
70
  end
50
71
 
51
72
  def resolve_fact(fact_data, entity_ids, content_item_id, occurred_at, project_path:, scope:)
73
+ # Canonicalize drift-prone predicate synonyms (has_convention →
74
+ # convention, primary_language → uses_language) before anything
75
+ # else looks at the predicate.
76
+ original_predicate = fact_data[:predicate]
77
+ canonical = PredicatePolicy.canonicalize(original_predicate)
78
+ if canonical != original_predicate
79
+ ClaudeMemory.logger.debug("resolve",
80
+ message: "Canonicalized predicate",
81
+ from: original_predicate,
82
+ to: canonical)
83
+ fact_data = fact_data.merge(predicate: canonical)
84
+ end
85
+
86
+ log_novel_predicate(canonical) unless PredicatePolicy.known_predicates.include?(canonical)
87
+
52
88
  subject_id = resolve_subject(fact_data, entity_ids)
53
89
  existing_facts = @store.facts_for_slot(subject_id, fact_data[:predicate])
54
90
  resolution = determine_resolution(existing_facts, fact_data, entity_ids)
@@ -57,6 +93,13 @@ module ClaudeMemory
57
93
  project_path: project_path, scope: scope)
58
94
  end
59
95
 
96
+ def log_novel_predicate(predicate)
97
+ ClaudeMemory.logger.warn("resolve",
98
+ message: "Novel predicate encountered",
99
+ predicate: predicate,
100
+ hint: "add to PredicatePolicy::POLICIES or PredicatePolicy::SYNONYMS to canonicalize")
101
+ end
102
+
60
103
  def resolve_subject(fact_data, entity_ids)
61
104
  entity_ids[fact_data[:subject]] ||
62
105
  @store.find_or_create_entity(type: "repo", name: fact_data[:subject])
@@ -5,7 +5,7 @@ module ClaudeMemory
5
5
  # Schema migration and version management for SQLiteStore.
6
6
  # Handles Sequel migrations, legacy version syncing, and initial setup.
7
7
  module SchemaManager
8
- SCHEMA_VERSION = 12
8
+ SCHEMA_VERSION = 14
9
9
 
10
10
  private
11
11