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.
- checksums.yaml +4 -4
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/rules/claude_memory.generated.md +94 -2
- data/.claude/settings.json +30 -52
- data/.claude/settings.local.json +3 -1
- data/.claude/skills/release/SKILL.md +168 -0
- data/.claude/skills/upgrade-dependencies/SKILL.md +154 -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 +47 -0
- data/CLAUDE.md +31 -17
- 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 +58 -20
- 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/lib/claude_memory/commands/completion_command.rb +1 -31
- data/lib/claude_memory/commands/embeddings_command.rb +198 -0
- data/lib/claude_memory/commands/help_command.rb +8 -1
- data/lib/claude_memory/commands/registry.rb +47 -34
- 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 +5 -1
- data/lib/claude_memory/commands/stats_command.rb +98 -2
- data/lib/claude_memory/configuration.rb +14 -1
- 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/embeddings/api_adapter.rb +5 -4
- data/lib/claude_memory/embeddings/fastembed_adapter.rb +43 -13
- 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 +32 -6
- data/lib/claude_memory/ingest/ingester.rb +17 -0
- data/lib/claude_memory/mcp/handlers/management_handlers.rb +24 -0
- data/lib/claude_memory/mcp/handlers/stats_handlers.rb +5 -2
- data/lib/claude_memory/mcp/instructions_builder.rb +17 -0
- data/lib/claude_memory/mcp/server.rb +30 -3
- data/lib/claude_memory/mcp/telemetry.rb +86 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +86 -3
- data/lib/claude_memory/mcp/tools.rb +10 -0
- data/lib/claude_memory/publish.rb +40 -5
- data/lib/claude_memory/recall.rb +81 -0
- data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
- data/lib/claude_memory/resolve/resolver.rb +43 -0
- data/lib/claude_memory/store/schema_manager.rb +1 -1
- data/lib/claude_memory/store/sqlite_store.rb +250 -1
- 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/version.rb +1 -1
- data/lib/claude_memory.rb +5 -0
- metadata +27 -8
- data/.claude/memory.sqlite3-shm +0 -0
- data/.claude/memory.sqlite3-wal +0 -0
|
@@ -129,13 +129,16 @@ module ClaudeMemory
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
if active_facts > 0
|
|
132
|
-
|
|
132
|
+
all_predicates = store.db[:facts]
|
|
133
133
|
.where(status: "active")
|
|
134
134
|
.group_and_count(:predicate)
|
|
135
135
|
.order(Sequel.desc(:count))
|
|
136
|
-
.limit(10)
|
|
137
136
|
.all
|
|
138
137
|
.map { |row| {predicate: row[:predicate], count: row[:count]} }
|
|
138
|
+
|
|
139
|
+
stats[:top_predicates] = all_predicates.first(10)
|
|
140
|
+
stats[:predicates_known], stats[:predicates_novel] =
|
|
141
|
+
all_predicates.partition { |row| Resolve::PredicatePolicy.known_predicates.include?(row[:predicate]) }
|
|
139
142
|
end
|
|
140
143
|
|
|
141
144
|
stats
|
|
@@ -108,9 +108,26 @@ module ClaudeMemory
|
|
|
108
108
|
|
|
109
109
|
escalation = vec ? "recall_semantic, explain, or fact_graph" : "explain or fact_graph"
|
|
110
110
|
lines << "Start with fast tools (recall, decisions, conventions) before escalating to #{escalation}."
|
|
111
|
+
|
|
112
|
+
lines << proactive_recall_guidance
|
|
111
113
|
lines.join("\n")
|
|
112
114
|
end
|
|
113
115
|
|
|
116
|
+
# Directive guidance for when Claude should proactively consult memory.
|
|
117
|
+
# Validated by A/B testing: without these directives, Claude writes code
|
|
118
|
+
# using known-dangerous patterns (e.g. Sequel.sqlite) and hallucinates
|
|
119
|
+
# file paths instead of consulting memory for the correct structure.
|
|
120
|
+
def proactive_recall_guidance
|
|
121
|
+
<<~GUIDANCE.strip
|
|
122
|
+
IMPORTANT — check memory proactively in these situations:
|
|
123
|
+
- Before writing code: call memory.conventions to verify project patterns and avoid known gotchas
|
|
124
|
+
- Before explaining architecture: call memory.architecture for structural knowledge without file traversal
|
|
125
|
+
- Before refactoring: call memory.decisions to understand why past choices were made
|
|
126
|
+
- When asked about preferences: global facts store user environment and style preferences across all projects
|
|
127
|
+
- When adding to the codebase: recall which files and patterns to follow (memory knows correct paths and relationships)
|
|
128
|
+
GUIDANCE
|
|
129
|
+
end
|
|
130
|
+
|
|
114
131
|
def count_by_predicates(store, predicates)
|
|
115
132
|
store.facts
|
|
116
133
|
.where(status: "active")
|
|
@@ -5,20 +5,30 @@ require_relative "instructions_builder"
|
|
|
5
5
|
require_relative "query_guide"
|
|
6
6
|
require_relative "text_summary"
|
|
7
7
|
require_relative "error_classifier"
|
|
8
|
+
require_relative "telemetry"
|
|
8
9
|
|
|
9
10
|
module ClaudeMemory
|
|
10
11
|
module MCP
|
|
12
|
+
# MCP JSON-RPC server over stdio.
|
|
13
|
+
# Reads newline-delimited JSON requests from input, dispatches to Tools,
|
|
14
|
+
# and writes JSON responses to output.
|
|
11
15
|
class Server
|
|
12
16
|
PROTOCOL_VERSION = "2024-11-05"
|
|
13
17
|
|
|
18
|
+
# @param store_or_manager [Store::SQLiteStore, Store::StoreManager] database backend
|
|
19
|
+
# @param input [IO] input stream for JSON-RPC requests (default: $stdin)
|
|
20
|
+
# @param output [IO] output stream for JSON-RPC responses (default: $stdout)
|
|
14
21
|
def initialize(store_or_manager, input: $stdin, output: $stdout)
|
|
15
22
|
@store_or_manager = store_or_manager
|
|
16
23
|
@tools = Tools.new(store_or_manager)
|
|
24
|
+
@telemetry = Telemetry.new(store_or_manager)
|
|
17
25
|
@input = input
|
|
18
26
|
@output = output
|
|
19
27
|
@running = false
|
|
20
28
|
end
|
|
21
29
|
|
|
30
|
+
# Start the read loop, blocking until input is exhausted or stop is called.
|
|
31
|
+
# @return [void]
|
|
22
32
|
def run
|
|
23
33
|
@running = true
|
|
24
34
|
while @running
|
|
@@ -29,12 +39,15 @@ module ClaudeMemory
|
|
|
29
39
|
end
|
|
30
40
|
end
|
|
31
41
|
|
|
42
|
+
# Signal the read loop to exit after the current message.
|
|
43
|
+
# @return [void]
|
|
32
44
|
def stop
|
|
33
45
|
@running = false
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
private
|
|
37
49
|
|
|
50
|
+
# @return [void]
|
|
38
51
|
def handle_message(line)
|
|
39
52
|
return if line.empty?
|
|
40
53
|
|
|
@@ -46,15 +59,22 @@ module ClaudeMemory
|
|
|
46
59
|
rescue JSON::ParserError => e
|
|
47
60
|
send_error(-32700, "Parse error: #{e.message}", 0)
|
|
48
61
|
rescue => e
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
# Per JSON-RPC 2.0: never respond to notifications, even on error
|
|
63
|
+
request_id = request&.fetch("id", nil)
|
|
64
|
+
send_error(-32603, "Internal error: #{e.message}", request_id) unless request_id.nil?
|
|
51
65
|
end
|
|
52
66
|
end
|
|
53
67
|
|
|
68
|
+
# @return [Hash, nil] JSON-RPC response hash, or nil for notifications
|
|
54
69
|
def process_request(request)
|
|
55
70
|
id = request["id"]
|
|
56
71
|
method = request["method"]
|
|
57
72
|
|
|
73
|
+
# Per JSON-RPC 2.0: a request without an id is a notification and
|
|
74
|
+
# MUST NOT receive a response. MCP relies on this for
|
|
75
|
+
# `notifications/initialized` after the initialize handshake.
|
|
76
|
+
return nil if id.nil?
|
|
77
|
+
|
|
58
78
|
case method
|
|
59
79
|
when "initialize"
|
|
60
80
|
handle_initialize(id, request["params"])
|
|
@@ -74,6 +94,7 @@ module ClaudeMemory
|
|
|
74
94
|
end
|
|
75
95
|
end
|
|
76
96
|
|
|
97
|
+
# @return [Hash] initialize response with capabilities and server info
|
|
77
98
|
def handle_initialize(id, _params)
|
|
78
99
|
{
|
|
79
100
|
jsonrpc: "2.0",
|
|
@@ -93,6 +114,7 @@ module ClaudeMemory
|
|
|
93
114
|
}
|
|
94
115
|
end
|
|
95
116
|
|
|
117
|
+
# @return [Hash] list of available tool definitions
|
|
96
118
|
def handle_tools_list(id)
|
|
97
119
|
{
|
|
98
120
|
jsonrpc: "2.0",
|
|
@@ -103,11 +125,14 @@ module ClaudeMemory
|
|
|
103
125
|
}
|
|
104
126
|
end
|
|
105
127
|
|
|
128
|
+
# @return [Hash] tool result with dual content/structuredContent
|
|
106
129
|
def handle_tools_call(id, params)
|
|
107
130
|
name = params["name"]
|
|
108
131
|
arguments = params["arguments"] || {}
|
|
109
132
|
|
|
110
|
-
result = @
|
|
133
|
+
result = @telemetry.record(name, arguments) do
|
|
134
|
+
@tools.call(name, arguments)
|
|
135
|
+
end
|
|
111
136
|
|
|
112
137
|
# Release database connections after each tool call
|
|
113
138
|
# This prevents lock contention with hook commands
|
|
@@ -128,6 +153,7 @@ module ClaudeMemory
|
|
|
128
153
|
}
|
|
129
154
|
end
|
|
130
155
|
|
|
156
|
+
# @return [Hash] list of available prompt definitions
|
|
131
157
|
def handle_prompts_list(id)
|
|
132
158
|
{
|
|
133
159
|
jsonrpc: "2.0",
|
|
@@ -138,6 +164,7 @@ module ClaudeMemory
|
|
|
138
164
|
}
|
|
139
165
|
end
|
|
140
166
|
|
|
167
|
+
# @return [Hash] prompt content or error if unknown
|
|
141
168
|
def handle_prompts_get(id, params)
|
|
142
169
|
name = params&.dig("name")
|
|
143
170
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module MCP
|
|
5
|
+
# Records MCP tool invocations into the project database for usage stats.
|
|
6
|
+
# Timing and error capture wrap the tool call; the insert is synchronous
|
|
7
|
+
# and best-effort — telemetry failures are swallowed so they never break
|
|
8
|
+
# a real tool response.
|
|
9
|
+
class Telemetry
|
|
10
|
+
def initialize(store_or_manager)
|
|
11
|
+
@store_or_manager = store_or_manager
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Time a tool invocation and record the outcome. Yields to the caller
|
|
15
|
+
# and returns whatever the block returns; re-raises any exception after
|
|
16
|
+
# recording it as an error.
|
|
17
|
+
def record(tool_name, arguments)
|
|
18
|
+
started = monotonic_ms
|
|
19
|
+
begin
|
|
20
|
+
result = yield
|
|
21
|
+
rescue => e
|
|
22
|
+
duration = monotonic_ms - started
|
|
23
|
+
write(
|
|
24
|
+
tool_name: tool_name,
|
|
25
|
+
duration_ms: duration,
|
|
26
|
+
result_count: nil,
|
|
27
|
+
scope: extract_scope(arguments),
|
|
28
|
+
error_class: e.class.name
|
|
29
|
+
)
|
|
30
|
+
raise
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
duration = monotonic_ms - started
|
|
34
|
+
write(
|
|
35
|
+
tool_name: tool_name,
|
|
36
|
+
duration_ms: duration,
|
|
37
|
+
result_count: extract_result_count(result),
|
|
38
|
+
scope: extract_scope(arguments),
|
|
39
|
+
error_class: nil
|
|
40
|
+
)
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def monotonic_ms
|
|
47
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def write(**row)
|
|
51
|
+
store = writable_store
|
|
52
|
+
return unless store
|
|
53
|
+
store.insert_mcp_tool_call(**row)
|
|
54
|
+
rescue Sequel::DatabaseError, Extralite::Error
|
|
55
|
+
# Telemetry is best-effort; never fail the user's tool call
|
|
56
|
+
# because stats couldn't be written.
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def writable_store
|
|
60
|
+
if @store_or_manager.is_a?(Store::StoreManager)
|
|
61
|
+
@store_or_manager.ensure_project!
|
|
62
|
+
elsif @store_or_manager.respond_to?(:insert_mcp_tool_call)
|
|
63
|
+
@store_or_manager
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def extract_scope(arguments)
|
|
68
|
+
return nil unless arguments.is_a?(Hash)
|
|
69
|
+
arguments["scope"] || arguments[:scope]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Inspect a tool result for a countable field. Most query tools
|
|
73
|
+
# return hashes with :facts, :results, :conflicts, or :changes;
|
|
74
|
+
# fall back to nil for shapes we don't recognize.
|
|
75
|
+
def extract_result_count(result)
|
|
76
|
+
return nil unless result.is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
%i[facts results conflicts changes entities items].each do |key|
|
|
79
|
+
value = result[key] || result[key.to_s]
|
|
80
|
+
return value.size if value.is_a?(Array)
|
|
81
|
+
end
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -14,6 +14,59 @@ module ClaudeMemory
|
|
|
14
14
|
# Annotations for idempotent writes (safe to retry)
|
|
15
15
|
WRITE_IDEMPOTENT = {readOnlyHint: false, idempotentHint: true, destructiveHint: false}.freeze
|
|
16
16
|
|
|
17
|
+
# Schema for {predicate, count} entries
|
|
18
|
+
PREDICATE_COUNT_SCHEMA = {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
predicate: {type: "string"},
|
|
22
|
+
count: {type: "integer"}
|
|
23
|
+
},
|
|
24
|
+
required: ["predicate", "count"]
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Schema for per-database stats block returned by memory.stats
|
|
28
|
+
DATABASE_STATS_SCHEMA = {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
exists: {type: "boolean"},
|
|
32
|
+
schema_version: {type: "integer"},
|
|
33
|
+
facts: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
total: {type: "integer"},
|
|
37
|
+
active: {type: "integer"},
|
|
38
|
+
superseded: {type: "integer"},
|
|
39
|
+
top_predicates: {
|
|
40
|
+
type: "array",
|
|
41
|
+
description: "Top 10 predicates by count (known + novel combined)",
|
|
42
|
+
items: PREDICATE_COUNT_SCHEMA
|
|
43
|
+
},
|
|
44
|
+
predicates_known: {
|
|
45
|
+
type: "array",
|
|
46
|
+
description: "Predicates with explicit cardinality policies in PredicatePolicy::POLICIES, sorted by count desc",
|
|
47
|
+
items: PREDICATE_COUNT_SCHEMA
|
|
48
|
+
},
|
|
49
|
+
predicates_novel: {
|
|
50
|
+
type: "array",
|
|
51
|
+
description: "Predicates not in PredicatePolicy::POLICIES, sorted by count desc. Novel predicates with high counts are candidates for promotion to known status with explicit cardinality policies (canonicalization signal).",
|
|
52
|
+
items: PREDICATE_COUNT_SCHEMA
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
entities: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
total: {type: "integer"},
|
|
60
|
+
by_type: {type: "array", items: {type: "object"}}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
content_items: {type: "object"},
|
|
64
|
+
provenance: {type: "object"},
|
|
65
|
+
conflicts: {type: "object"},
|
|
66
|
+
vec: {type: "object"}
|
|
67
|
+
}
|
|
68
|
+
}.freeze
|
|
69
|
+
|
|
17
70
|
# Returns array of tool definitions for MCP protocol
|
|
18
71
|
# @return [Array<Hash>] Tool definitions with name, description, and inputSchema
|
|
19
72
|
def self.all
|
|
@@ -123,13 +176,29 @@ module ClaudeMemory
|
|
|
123
176
|
},
|
|
124
177
|
{
|
|
125
178
|
name: "memory.stats",
|
|
126
|
-
description: "Get detailed statistics about the memory system (facts by predicate, entities by type, provenance coverage, conflicts, database sizes)",
|
|
179
|
+
description: "Get detailed statistics about the memory system (facts by predicate, entities by type, provenance coverage, conflicts, database sizes).",
|
|
127
180
|
inputSchema: {
|
|
128
181
|
type: "object",
|
|
129
182
|
properties: {
|
|
130
183
|
scope: {type: "string", enum: ["all", "global", "project"], description: "Show stats for: all (default), global, or project", default: "all"}
|
|
131
184
|
}
|
|
132
185
|
},
|
|
186
|
+
outputSchema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {
|
|
189
|
+
scope: {type: "string", enum: ["all", "global", "project"]},
|
|
190
|
+
databases: {
|
|
191
|
+
type: "object",
|
|
192
|
+
description: "Per-database stats. Keys are 'global', 'project', or 'legacy' depending on connection mode.",
|
|
193
|
+
properties: {
|
|
194
|
+
global: DATABASE_STATS_SCHEMA,
|
|
195
|
+
project: DATABASE_STATS_SCHEMA,
|
|
196
|
+
legacy: DATABASE_STATS_SCHEMA
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
required: ["scope", "databases"]
|
|
201
|
+
},
|
|
133
202
|
annotations: READ_ONLY
|
|
134
203
|
},
|
|
135
204
|
{
|
|
@@ -144,6 +213,20 @@ module ClaudeMemory
|
|
|
144
213
|
},
|
|
145
214
|
annotations: WRITE_IDEMPOTENT
|
|
146
215
|
},
|
|
216
|
+
{
|
|
217
|
+
name: "memory.reject_fact",
|
|
218
|
+
description: "Mark a fact as rejected (e.g. a distiller hallucination). Sets status to 'rejected' and closes any open conflicts involving the fact. Use when the user confirms a fact is wrong.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
fact_id: {type: "integer", description: "Fact ID to reject"},
|
|
223
|
+
docid: {type: "string", description: "8-char docid (alternative to fact_id)"},
|
|
224
|
+
reason: {type: "string", description: "Why the fact is wrong (recorded in conflict notes)"},
|
|
225
|
+
scope: {type: "string", enum: ["project", "global"], description: "Database scope", default: "project"}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
annotations: WRITE_IDEMPOTENT
|
|
229
|
+
},
|
|
147
230
|
{
|
|
148
231
|
name: "memory.store_extraction",
|
|
149
232
|
description: "Store extracted facts, entities, and decisions from a conversation. Call this to persist knowledge you've learned during the session.",
|
|
@@ -156,7 +239,7 @@ module ClaudeMemory
|
|
|
156
239
|
items: {
|
|
157
240
|
type: "object",
|
|
158
241
|
properties: {
|
|
159
|
-
type: {type: "string", description: "Entity type: database, framework, language, platform, repo, module, person, service"},
|
|
242
|
+
type: {type: "string", description: "Entity type. Common types: database, framework, language, platform, repo, module, person, service, tool, library, concept. You may use other types if needed."},
|
|
160
243
|
name: {type: "string", description: "Canonical name"},
|
|
161
244
|
confidence: {type: "number", description: "0.0-1.0 extraction confidence"}
|
|
162
245
|
},
|
|
@@ -170,7 +253,7 @@ module ClaudeMemory
|
|
|
170
253
|
type: "object",
|
|
171
254
|
properties: {
|
|
172
255
|
subject: {type: "string", description: "Entity name or 'repo' for project-level facts"},
|
|
173
|
-
predicate: {type: "string", description: "Relationship type:
|
|
256
|
+
predicate: {type: "string", description: "Relationship type. Known predicates: #{ClaudeMemory::Resolve::PredicatePolicy.known_predicates.join(", ")}. You may use other snake_case predicates for relations that don't fit these — be specific and reuse existing predicates when possible."},
|
|
174
257
|
object: {type: "string", description: "The value or target entity"},
|
|
175
258
|
confidence: {type: "number", description: "0.0-1.0 how confident"},
|
|
176
259
|
quote: {type: "string", description: "Source text excerpt (max 200 chars)"},
|
|
@@ -16,6 +16,9 @@ require_relative "handlers/setup_handlers"
|
|
|
16
16
|
|
|
17
17
|
module ClaudeMemory
|
|
18
18
|
module MCP
|
|
19
|
+
# Dispatcher that routes MCP tool calls to handler modules.
|
|
20
|
+
# Each handler module (QueryHandlers, ShortcutHandlers, etc.) provides
|
|
21
|
+
# the implementation for a group of related tools.
|
|
19
22
|
class Tools
|
|
20
23
|
include ToolHelpers
|
|
21
24
|
include Handlers::QueryHandlers
|
|
@@ -25,6 +28,7 @@ module ClaudeMemory
|
|
|
25
28
|
include Handlers::StatsHandlers
|
|
26
29
|
include Handlers::SetupHandlers
|
|
27
30
|
|
|
31
|
+
# @param store_or_manager [Store::SQLiteStore, Store::StoreManager] database backend
|
|
28
32
|
def initialize(store_or_manager)
|
|
29
33
|
@recall = Recall.new(store_or_manager)
|
|
30
34
|
|
|
@@ -35,10 +39,15 @@ module ClaudeMemory
|
|
|
35
39
|
end
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
# @return [Array<Hash>] MCP tool definition hashes for tools/list
|
|
38
43
|
def definitions
|
|
39
44
|
ToolDefinitions.all
|
|
40
45
|
end
|
|
41
46
|
|
|
47
|
+
# Dispatch a tool call to the appropriate handler method.
|
|
48
|
+
# @param name [String] fully-qualified tool name (e.g. "memory.recall")
|
|
49
|
+
# @param arguments [Hash] tool arguments from the MCP request
|
|
50
|
+
# @return [Hash] structured result hash for the tool response
|
|
42
51
|
def call(name, arguments)
|
|
43
52
|
case name
|
|
44
53
|
when "memory.recall" then recall(arguments)
|
|
@@ -51,6 +60,7 @@ module ClaudeMemory
|
|
|
51
60
|
when "memory.status" then status
|
|
52
61
|
when "memory.stats" then stats(arguments)
|
|
53
62
|
when "memory.promote" then promote(arguments)
|
|
63
|
+
when "memory.reject_fact" then reject_fact(arguments)
|
|
54
64
|
when "memory.store_extraction" then store_extraction(arguments)
|
|
55
65
|
when "memory.decisions" then decisions(arguments)
|
|
56
66
|
when "memory.conventions" then conventions(arguments)
|
|
@@ -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] ==
|
|
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] ==
|
|
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
|
|
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
|
|