claude_memory 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/.mind.mv2.o2N83S +0 -0
- data/.claude/CLAUDE.md +1 -0
- data/.claude/rules/claude_memory.generated.md +28 -9
- data/.claude/settings.local.json +9 -1
- data/.claude/skills/check-memory/SKILL.md +77 -0
- data/.claude/skills/improve/SKILL.md +532 -0
- data/.claude/skills/improve/feature-patterns.md +1221 -0
- data/.claude/skills/quality-update/SKILL.md +229 -0
- data/.claude/skills/quality-update/implementation-guide.md +346 -0
- data/.claude/skills/review-commit/SKILL.md +199 -0
- data/.claude/skills/review-for-quality/SKILL.md +154 -0
- data/.claude/skills/review-for-quality/expert-checklists.md +79 -0
- data/.claude/skills/setup-memory/SKILL.md +168 -0
- data/.claude/skills/study-repo/SKILL.md +307 -0
- data/.claude/skills/study-repo/analysis-template.md +323 -0
- data/.claude/skills/study-repo/focus-examples.md +327 -0
- data/CHANGELOG.md +133 -0
- data/CLAUDE.md +130 -11
- data/README.md +117 -10
- data/db/migrations/001_create_initial_schema.rb +117 -0
- data/db/migrations/002_add_project_scoping.rb +33 -0
- data/db/migrations/003_add_session_metadata.rb +42 -0
- data/db/migrations/004_add_fact_embeddings.rb +20 -0
- data/db/migrations/005_add_incremental_sync.rb +21 -0
- data/db/migrations/006_add_operation_tracking.rb +40 -0
- data/db/migrations/007_add_ingestion_metrics.rb +26 -0
- data/docs/.claude/mind.mv2.lock +0 -0
- data/docs/GETTING_STARTED.md +587 -0
- data/docs/RELEASE_NOTES_v0.2.0.md +0 -1
- data/docs/RUBY_COMMUNITY_POST_v0.2.0.md +0 -2
- data/docs/architecture.md +9 -8
- data/docs/auto_init_design.md +230 -0
- data/docs/improvements.md +557 -731
- data/docs/influence/.gitkeep +13 -0
- data/docs/influence/grepai.md +933 -0
- data/docs/influence/qmd.md +2195 -0
- data/docs/plugin.md +257 -11
- data/docs/quality_review.md +472 -1273
- data/docs/remaining_improvements.md +330 -0
- data/lefthook.yml +13 -0
- data/lib/claude_memory/commands/checks/claude_md_check.rb +41 -0
- data/lib/claude_memory/commands/checks/database_check.rb +120 -0
- data/lib/claude_memory/commands/checks/hooks_check.rb +112 -0
- data/lib/claude_memory/commands/checks/reporter.rb +110 -0
- data/lib/claude_memory/commands/checks/snapshot_check.rb +30 -0
- data/lib/claude_memory/commands/doctor_command.rb +12 -129
- data/lib/claude_memory/commands/help_command.rb +1 -0
- data/lib/claude_memory/commands/hook_command.rb +9 -2
- data/lib/claude_memory/commands/index_command.rb +169 -0
- data/lib/claude_memory/commands/ingest_command.rb +1 -1
- data/lib/claude_memory/commands/init_command.rb +5 -197
- data/lib/claude_memory/commands/initializers/database_ensurer.rb +30 -0
- data/lib/claude_memory/commands/initializers/global_initializer.rb +85 -0
- data/lib/claude_memory/commands/initializers/hooks_configurator.rb +156 -0
- data/lib/claude_memory/commands/initializers/mcp_configurator.rb +56 -0
- data/lib/claude_memory/commands/initializers/memory_instructions_writer.rb +135 -0
- data/lib/claude_memory/commands/initializers/project_initializer.rb +111 -0
- data/lib/claude_memory/commands/recover_command.rb +75 -0
- data/lib/claude_memory/commands/registry.rb +5 -1
- data/lib/claude_memory/commands/stats_command.rb +239 -0
- data/lib/claude_memory/commands/uninstall_command.rb +226 -0
- data/lib/claude_memory/core/batch_loader.rb +32 -0
- data/lib/claude_memory/core/concept_ranker.rb +73 -0
- data/lib/claude_memory/core/embedding_candidate_builder.rb +37 -0
- data/lib/claude_memory/core/fact_collector.rb +51 -0
- data/lib/claude_memory/core/fact_query_builder.rb +154 -0
- data/lib/claude_memory/core/fact_ranker.rb +113 -0
- data/lib/claude_memory/core/result_builder.rb +54 -0
- data/lib/claude_memory/core/result_sorter.rb +25 -0
- data/lib/claude_memory/core/scope_filter.rb +61 -0
- data/lib/claude_memory/core/text_builder.rb +29 -0
- data/lib/claude_memory/embeddings/generator.rb +161 -0
- data/lib/claude_memory/embeddings/similarity.rb +69 -0
- data/lib/claude_memory/hook/handler.rb +4 -3
- data/lib/claude_memory/index/lexical_fts.rb +7 -2
- data/lib/claude_memory/infrastructure/operation_tracker.rb +158 -0
- data/lib/claude_memory/infrastructure/schema_validator.rb +206 -0
- data/lib/claude_memory/ingest/content_sanitizer.rb +6 -7
- data/lib/claude_memory/ingest/ingester.rb +99 -15
- data/lib/claude_memory/ingest/metadata_extractor.rb +57 -0
- data/lib/claude_memory/ingest/tool_extractor.rb +71 -0
- data/lib/claude_memory/mcp/response_formatter.rb +331 -0
- data/lib/claude_memory/mcp/server.rb +19 -0
- data/lib/claude_memory/mcp/setup_status_analyzer.rb +73 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +279 -0
- data/lib/claude_memory/mcp/tool_helpers.rb +80 -0
- data/lib/claude_memory/mcp/tools.rb +330 -320
- data/lib/claude_memory/recall/dual_query_template.rb +63 -0
- data/lib/claude_memory/recall.rb +304 -237
- data/lib/claude_memory/resolve/resolver.rb +52 -49
- data/lib/claude_memory/store/sqlite_store.rb +210 -144
- data/lib/claude_memory/store/store_manager.rb +6 -6
- data/lib/claude_memory/sweep/sweeper.rb +6 -0
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +35 -3
- metadata +71 -11
- data/.claude/.mind.mv2.aLCUZd +0 -0
- data/.claude/memory.sqlite3 +0 -0
- data/.mcp.json +0 -11
- /data/docs/{feature_adoption_plan.md → plans/feature_adoption_plan.md} +0 -0
- /data/docs/{feature_adoption_plan_revised.md → plans/feature_adoption_plan_revised.md} +0 -0
- /data/docs/{plan.md → plans/plan.md} +0 -0
- /data/docs/{updated_plan.md → plans/updated_plan.md} +0 -0
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "digest"
|
|
5
|
+
require_relative "tool_helpers"
|
|
6
|
+
require_relative "response_formatter"
|
|
7
|
+
require_relative "tool_definitions"
|
|
8
|
+
require_relative "setup_status_analyzer"
|
|
5
9
|
|
|
6
10
|
module ClaudeMemory
|
|
7
11
|
module MCP
|
|
8
12
|
class Tools
|
|
13
|
+
include ToolHelpers
|
|
14
|
+
|
|
9
15
|
def initialize(store_or_manager)
|
|
10
16
|
@recall = Recall.new(store_or_manager)
|
|
11
17
|
|
|
@@ -17,194 +23,7 @@ module ClaudeMemory
|
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
def definitions
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
name: "memory.recall",
|
|
23
|
-
description: "Recall facts matching a query. Searches both global and project databases.",
|
|
24
|
-
inputSchema: {
|
|
25
|
-
type: "object",
|
|
26
|
-
properties: {
|
|
27
|
-
query: {type: "string", description: "Search query"},
|
|
28
|
-
limit: {type: "integer", description: "Max results", default: 10},
|
|
29
|
-
scope: {type: "string", enum: ["all", "global", "project"], description: "Filter by scope: 'all' (default), 'global', or 'project'", default: "all"}
|
|
30
|
-
},
|
|
31
|
-
required: ["query"]
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: "memory.recall_index",
|
|
36
|
-
description: "Layer 1: Search for facts and get lightweight index (IDs, previews, token counts). Use this first before fetching full details.",
|
|
37
|
-
inputSchema: {
|
|
38
|
-
type: "object",
|
|
39
|
-
properties: {
|
|
40
|
-
query: {type: "string", description: "Search query for fact discovery"},
|
|
41
|
-
limit: {type: "integer", description: "Maximum results to return", default: 20},
|
|
42
|
-
scope: {type: "string", enum: ["all", "global", "project"], description: "Scope: 'all' (both), 'global' (user-wide), 'project' (current only)", default: "all"}
|
|
43
|
-
},
|
|
44
|
-
required: ["query"]
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: "memory.recall_details",
|
|
49
|
-
description: "Layer 2: Fetch full details for specific fact IDs from the index. Use after memory.recall_index to get complete information.",
|
|
50
|
-
inputSchema: {
|
|
51
|
-
type: "object",
|
|
52
|
-
properties: {
|
|
53
|
-
fact_ids: {type: "array", items: {type: "integer"}, description: "Fact IDs from memory.recall_index"},
|
|
54
|
-
scope: {type: "string", enum: ["project", "global"], description: "Database to query", default: "project"}
|
|
55
|
-
},
|
|
56
|
-
required: ["fact_ids"]
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
name: "memory.explain",
|
|
61
|
-
description: "Get detailed explanation of a fact with provenance",
|
|
62
|
-
inputSchema: {
|
|
63
|
-
type: "object",
|
|
64
|
-
properties: {
|
|
65
|
-
fact_id: {type: "integer", description: "Fact ID to explain"},
|
|
66
|
-
scope: {type: "string", enum: ["global", "project"], description: "Which database to look in", default: "project"}
|
|
67
|
-
},
|
|
68
|
-
required: ["fact_id"]
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: "memory.changes",
|
|
73
|
-
description: "List recent fact changes from both databases",
|
|
74
|
-
inputSchema: {
|
|
75
|
-
type: "object",
|
|
76
|
-
properties: {
|
|
77
|
-
since: {type: "string", description: "ISO timestamp"},
|
|
78
|
-
limit: {type: "integer", default: 20},
|
|
79
|
-
scope: {type: "string", enum: ["all", "global", "project"], default: "all"}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: "memory.conflicts",
|
|
85
|
-
description: "List open conflicts from both databases",
|
|
86
|
-
inputSchema: {
|
|
87
|
-
type: "object",
|
|
88
|
-
properties: {
|
|
89
|
-
scope: {type: "string", enum: ["all", "global", "project"], default: "all"}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
name: "memory.sweep_now",
|
|
95
|
-
description: "Run maintenance sweep on a database",
|
|
96
|
-
inputSchema: {
|
|
97
|
-
type: "object",
|
|
98
|
-
properties: {
|
|
99
|
-
budget_seconds: {type: "integer", default: 5},
|
|
100
|
-
scope: {type: "string", enum: ["global", "project"], default: "project"}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
name: "memory.status",
|
|
106
|
-
description: "Get memory system status for both databases",
|
|
107
|
-
inputSchema: {
|
|
108
|
-
type: "object",
|
|
109
|
-
properties: {}
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
name: "memory.promote",
|
|
114
|
-
description: "Promote a project fact to global memory. Use when user says a preference should apply everywhere.",
|
|
115
|
-
inputSchema: {
|
|
116
|
-
type: "object",
|
|
117
|
-
properties: {
|
|
118
|
-
fact_id: {type: "integer", description: "Project fact ID to promote to global"}
|
|
119
|
-
},
|
|
120
|
-
required: ["fact_id"]
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
name: "memory.store_extraction",
|
|
125
|
-
description: "Store extracted facts, entities, and decisions from a conversation. Call this to persist knowledge you've learned during the session.",
|
|
126
|
-
inputSchema: {
|
|
127
|
-
type: "object",
|
|
128
|
-
properties: {
|
|
129
|
-
entities: {
|
|
130
|
-
type: "array",
|
|
131
|
-
description: "Entities mentioned (databases, frameworks, services, etc.)",
|
|
132
|
-
items: {
|
|
133
|
-
type: "object",
|
|
134
|
-
properties: {
|
|
135
|
-
type: {type: "string", description: "Entity type: database, framework, language, platform, repo, module, person, service"},
|
|
136
|
-
name: {type: "string", description: "Canonical name"},
|
|
137
|
-
confidence: {type: "number", description: "0.0-1.0 extraction confidence"}
|
|
138
|
-
},
|
|
139
|
-
required: ["type", "name"]
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
facts: {
|
|
143
|
-
type: "array",
|
|
144
|
-
description: "Facts learned during the session",
|
|
145
|
-
items: {
|
|
146
|
-
type: "object",
|
|
147
|
-
properties: {
|
|
148
|
-
subject: {type: "string", description: "Entity name or 'repo' for project-level facts"},
|
|
149
|
-
predicate: {type: "string", description: "Relationship type: uses_database, uses_framework, convention, decision, auth_method, deployment_platform"},
|
|
150
|
-
object: {type: "string", description: "The value or target entity"},
|
|
151
|
-
confidence: {type: "number", description: "0.0-1.0 how confident"},
|
|
152
|
-
quote: {type: "string", description: "Source text excerpt (max 200 chars)"},
|
|
153
|
-
strength: {type: "string", enum: ["stated", "inferred"], description: "Was this explicitly stated or inferred?"},
|
|
154
|
-
scope_hint: {type: "string", enum: ["project", "global"], description: "Should this apply to just this project or globally?"}
|
|
155
|
-
},
|
|
156
|
-
required: ["subject", "predicate", "object"]
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
decisions: {
|
|
160
|
-
type: "array",
|
|
161
|
-
description: "Decisions made during the session",
|
|
162
|
-
items: {
|
|
163
|
-
type: "object",
|
|
164
|
-
properties: {
|
|
165
|
-
title: {type: "string", description: "Short summary (max 100 chars)"},
|
|
166
|
-
summary: {type: "string", description: "Full description"},
|
|
167
|
-
status_hint: {type: "string", enum: ["accepted", "proposed", "rejected"]}
|
|
168
|
-
},
|
|
169
|
-
required: ["title", "summary"]
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
scope: {type: "string", enum: ["global", "project"], description: "Default scope for facts", default: "project"}
|
|
173
|
-
},
|
|
174
|
-
required: ["facts"]
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
name: "memory.decisions",
|
|
179
|
-
description: "Quick access to architectural decisions, constraints, and rules",
|
|
180
|
-
inputSchema: {
|
|
181
|
-
type: "object",
|
|
182
|
-
properties: {
|
|
183
|
-
limit: {type: "integer", default: 10, description: "Maximum results to return"}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
name: "memory.conventions",
|
|
189
|
-
description: "Quick access to coding conventions and style preferences (global scope)",
|
|
190
|
-
inputSchema: {
|
|
191
|
-
type: "object",
|
|
192
|
-
properties: {
|
|
193
|
-
limit: {type: "integer", default: 20, description: "Maximum results to return"}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
name: "memory.architecture",
|
|
199
|
-
description: "Quick access to framework choices and architectural patterns",
|
|
200
|
-
inputSchema: {
|
|
201
|
-
type: "object",
|
|
202
|
-
properties: {
|
|
203
|
-
limit: {type: "integer", default: 10, description: "Maximum results to return"}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
]
|
|
26
|
+
ToolDefinitions.all
|
|
208
27
|
end
|
|
209
28
|
|
|
210
29
|
def call(name, arguments)
|
|
@@ -225,6 +44,8 @@ module ClaudeMemory
|
|
|
225
44
|
sweep_now(arguments)
|
|
226
45
|
when "memory.status"
|
|
227
46
|
status
|
|
47
|
+
when "memory.stats"
|
|
48
|
+
stats(arguments)
|
|
228
49
|
when "memory.promote"
|
|
229
50
|
promote(arguments)
|
|
230
51
|
when "memory.store_extraction"
|
|
@@ -235,6 +56,16 @@ module ClaudeMemory
|
|
|
235
56
|
conventions(arguments)
|
|
236
57
|
when "memory.architecture"
|
|
237
58
|
architecture(arguments)
|
|
59
|
+
when "memory.facts_by_tool"
|
|
60
|
+
facts_by_tool(arguments)
|
|
61
|
+
when "memory.facts_by_context"
|
|
62
|
+
facts_by_context(arguments)
|
|
63
|
+
when "memory.recall_semantic"
|
|
64
|
+
recall_semantic(arguments)
|
|
65
|
+
when "memory.search_concepts"
|
|
66
|
+
search_concepts(arguments)
|
|
67
|
+
when "memory.check_setup"
|
|
68
|
+
check_setup
|
|
238
69
|
else
|
|
239
70
|
{error: "Unknown tool: #{name}"}
|
|
240
71
|
end
|
|
@@ -243,48 +74,22 @@ module ClaudeMemory
|
|
|
243
74
|
private
|
|
244
75
|
|
|
245
76
|
def recall(args)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
status: r[:fact][:status],
|
|
256
|
-
source: r[:source],
|
|
257
|
-
receipts: r[:receipts].map { |p| {quote: p[:quote], strength: p[:strength]} }
|
|
258
|
-
}
|
|
259
|
-
end
|
|
260
|
-
}
|
|
77
|
+
# Check if databases exist before querying
|
|
78
|
+
return database_not_found_error(StandardError.new("Database not initialized")) unless databases_exist?
|
|
79
|
+
|
|
80
|
+
scope = extract_scope(args)
|
|
81
|
+
limit = extract_limit(args)
|
|
82
|
+
results = @recall.query(args["query"], limit: limit, scope: scope)
|
|
83
|
+
ResponseFormatter.format_recall_results(results)
|
|
84
|
+
rescue Sequel::DatabaseError, Sequel::DatabaseConnectionError, SQLite3::CantOpenException, Errno::ENOENT => e
|
|
85
|
+
database_not_found_error(e)
|
|
261
86
|
end
|
|
262
87
|
|
|
263
88
|
def recall_index(args)
|
|
264
|
-
scope = args
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
{
|
|
270
|
-
query: args["query"],
|
|
271
|
-
scope: scope,
|
|
272
|
-
result_count: results.size,
|
|
273
|
-
total_estimated_tokens: total_tokens,
|
|
274
|
-
facts: results.map do |r|
|
|
275
|
-
{
|
|
276
|
-
id: r[:id],
|
|
277
|
-
subject: r[:subject],
|
|
278
|
-
predicate: r[:predicate],
|
|
279
|
-
object_preview: r[:object_preview],
|
|
280
|
-
status: r[:status],
|
|
281
|
-
scope: r[:scope],
|
|
282
|
-
confidence: r[:confidence],
|
|
283
|
-
tokens: r[:token_estimate],
|
|
284
|
-
source: r[:source]
|
|
285
|
-
}
|
|
286
|
-
end
|
|
287
|
-
}
|
|
89
|
+
scope = extract_scope(args)
|
|
90
|
+
limit = extract_limit(args, default: 20)
|
|
91
|
+
results = @recall.query_index(args["query"], limit: limit, scope: scope)
|
|
92
|
+
ResponseFormatter.format_index_results(args["query"], scope, results)
|
|
288
93
|
end
|
|
289
94
|
|
|
290
95
|
def recall_details(args)
|
|
@@ -296,32 +101,7 @@ module ClaudeMemory
|
|
|
296
101
|
explanation = @recall.explain(fact_id, scope: scope)
|
|
297
102
|
next nil if explanation.is_a?(Core::NullExplanation)
|
|
298
103
|
|
|
299
|
-
|
|
300
|
-
fact: {
|
|
301
|
-
id: explanation[:fact][:id],
|
|
302
|
-
subject: explanation[:fact][:subject_name],
|
|
303
|
-
predicate: explanation[:fact][:predicate],
|
|
304
|
-
object: explanation[:fact][:object_literal],
|
|
305
|
-
status: explanation[:fact][:status],
|
|
306
|
-
confidence: explanation[:fact][:confidence],
|
|
307
|
-
scope: explanation[:fact][:scope],
|
|
308
|
-
valid_from: explanation[:fact][:valid_from],
|
|
309
|
-
valid_to: explanation[:fact][:valid_to]
|
|
310
|
-
},
|
|
311
|
-
receipts: explanation[:receipts].map { |r|
|
|
312
|
-
{
|
|
313
|
-
quote: r[:quote],
|
|
314
|
-
strength: r[:strength],
|
|
315
|
-
session_id: r[:session_id],
|
|
316
|
-
occurred_at: r[:occurred_at]
|
|
317
|
-
}
|
|
318
|
-
},
|
|
319
|
-
relationships: {
|
|
320
|
-
supersedes: explanation[:supersedes],
|
|
321
|
-
superseded_by: explanation[:superseded_by],
|
|
322
|
-
conflicts: explanation[:conflicts].map { |c| {id: c[:id], status: c[:status]} }
|
|
323
|
-
}
|
|
324
|
-
}
|
|
104
|
+
ResponseFormatter.format_detailed_explanation(explanation)
|
|
325
105
|
end.compact
|
|
326
106
|
|
|
327
107
|
{
|
|
@@ -335,58 +115,20 @@ module ClaudeMemory
|
|
|
335
115
|
explanation = @recall.explain(args["fact_id"], scope: scope)
|
|
336
116
|
return {error: "Fact not found in #{scope} database"} if explanation.is_a?(Core::NullExplanation)
|
|
337
117
|
|
|
338
|
-
|
|
339
|
-
fact: {
|
|
340
|
-
id: explanation[:fact][:id],
|
|
341
|
-
subject: explanation[:fact][:subject_name],
|
|
342
|
-
predicate: explanation[:fact][:predicate],
|
|
343
|
-
object: explanation[:fact][:object_literal],
|
|
344
|
-
status: explanation[:fact][:status],
|
|
345
|
-
valid_from: explanation[:fact][:valid_from],
|
|
346
|
-
valid_to: explanation[:fact][:valid_to]
|
|
347
|
-
},
|
|
348
|
-
source: scope,
|
|
349
|
-
receipts: explanation[:receipts].map { |p| {quote: p[:quote], strength: p[:strength]} },
|
|
350
|
-
supersedes: explanation[:supersedes],
|
|
351
|
-
superseded_by: explanation[:superseded_by],
|
|
352
|
-
conflicts: explanation[:conflicts].map { |c| c[:id] }
|
|
353
|
-
}
|
|
118
|
+
ResponseFormatter.format_explanation(explanation, scope)
|
|
354
119
|
end
|
|
355
120
|
|
|
356
121
|
def changes(args)
|
|
357
122
|
since = args["since"] || (Time.now - 86400 * 7).utc.iso8601
|
|
358
123
|
scope = args["scope"] || "all"
|
|
359
124
|
list = @recall.changes(since: since, limit: args["limit"] || 20, scope: scope)
|
|
360
|
-
|
|
361
|
-
since: since,
|
|
362
|
-
changes: list.map do |c|
|
|
363
|
-
{
|
|
364
|
-
id: c[:id],
|
|
365
|
-
predicate: c[:predicate],
|
|
366
|
-
object: c[:object_literal],
|
|
367
|
-
status: c[:status],
|
|
368
|
-
created_at: c[:created_at],
|
|
369
|
-
source: c[:source]
|
|
370
|
-
}
|
|
371
|
-
end
|
|
372
|
-
}
|
|
125
|
+
ResponseFormatter.format_changes(since, list)
|
|
373
126
|
end
|
|
374
127
|
|
|
375
128
|
def conflicts(args)
|
|
376
129
|
scope = args["scope"] || "all"
|
|
377
130
|
list = @recall.conflicts(scope: scope)
|
|
378
|
-
|
|
379
|
-
count: list.size,
|
|
380
|
-
conflicts: list.map do |c|
|
|
381
|
-
{
|
|
382
|
-
id: c[:id],
|
|
383
|
-
fact_a: c[:fact_a_id],
|
|
384
|
-
fact_b: c[:fact_b_id],
|
|
385
|
-
status: c[:status],
|
|
386
|
-
source: c[:source]
|
|
387
|
-
}
|
|
388
|
-
end
|
|
389
|
-
}
|
|
131
|
+
ResponseFormatter.format_conflicts(list)
|
|
390
132
|
end
|
|
391
133
|
|
|
392
134
|
def sweep_now(args)
|
|
@@ -396,14 +138,7 @@ module ClaudeMemory
|
|
|
396
138
|
|
|
397
139
|
sweeper = Sweep::Sweeper.new(store)
|
|
398
140
|
stats = sweeper.run!(budget_seconds: args["budget_seconds"] || 5)
|
|
399
|
-
|
|
400
|
-
scope: scope,
|
|
401
|
-
proposed_expired: stats[:proposed_facts_expired],
|
|
402
|
-
disputed_expired: stats[:disputed_facts_expired],
|
|
403
|
-
orphaned_deleted: stats[:orphaned_provenance_deleted],
|
|
404
|
-
content_pruned: stats[:old_content_pruned],
|
|
405
|
-
elapsed_seconds: stats[:elapsed_seconds].round(3)
|
|
406
|
-
}
|
|
141
|
+
ResponseFormatter.format_sweep_stats(scope, stats)
|
|
407
142
|
end
|
|
408
143
|
|
|
409
144
|
def status
|
|
@@ -430,6 +165,35 @@ module ClaudeMemory
|
|
|
430
165
|
result
|
|
431
166
|
end
|
|
432
167
|
|
|
168
|
+
def stats(args)
|
|
169
|
+
scope = args["scope"] || "all"
|
|
170
|
+
result = {scope: scope, databases: {}}
|
|
171
|
+
|
|
172
|
+
if @manager
|
|
173
|
+
if scope == "all" || scope == "global"
|
|
174
|
+
if @manager.global_exists?
|
|
175
|
+
@manager.ensure_global!
|
|
176
|
+
result[:databases][:global] = detailed_stats(@manager.global_store)
|
|
177
|
+
else
|
|
178
|
+
result[:databases][:global] = {exists: false}
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if scope == "all" || scope == "project"
|
|
183
|
+
if @manager.project_exists?
|
|
184
|
+
@manager.ensure_project!
|
|
185
|
+
result[:databases][:project] = detailed_stats(@manager.project_store)
|
|
186
|
+
else
|
|
187
|
+
result[:databases][:project] = {exists: false}
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
else
|
|
191
|
+
result[:databases][:legacy] = detailed_stats(@legacy_store)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
result
|
|
195
|
+
end
|
|
196
|
+
|
|
433
197
|
def promote(args)
|
|
434
198
|
return {error: "Promote requires StoreManager"} unless @manager
|
|
435
199
|
|
|
@@ -457,7 +221,8 @@ module ClaudeMemory
|
|
|
457
221
|
facts = (args["facts"] || []).map { |f| symbolize_keys(f) }
|
|
458
222
|
decisions = (args["decisions"] || []).map { |d| symbolize_keys(d) }
|
|
459
223
|
|
|
460
|
-
|
|
224
|
+
config = Configuration.new
|
|
225
|
+
project_path = config.project_dir
|
|
461
226
|
occurred_at = Time.now.utc.iso8601
|
|
462
227
|
|
|
463
228
|
searchable_text = build_searchable_text(entities, facts, decisions)
|
|
@@ -491,11 +256,7 @@ module ClaudeMemory
|
|
|
491
256
|
end
|
|
492
257
|
|
|
493
258
|
def build_searchable_text(entities, facts, decisions)
|
|
494
|
-
|
|
495
|
-
entities.each { |e| parts << "#{e[:type]}: #{e[:name]}" }
|
|
496
|
-
facts.each { |f| parts << "#{f[:subject]} #{f[:predicate]} #{f[:object]} #{f[:quote]}" }
|
|
497
|
-
decisions.each { |d| parts << "#{d[:title]} #{d[:summary]}" }
|
|
498
|
-
parts.join(" ").strip
|
|
259
|
+
Core::TextBuilder.build_searchable_text(entities, facts, decisions)
|
|
499
260
|
end
|
|
500
261
|
|
|
501
262
|
def create_synthetic_content_item(store, text, project_path, occurred_at)
|
|
@@ -518,7 +279,7 @@ module ClaudeMemory
|
|
|
518
279
|
end
|
|
519
280
|
|
|
520
281
|
def symbolize_keys(hash)
|
|
521
|
-
|
|
282
|
+
Core::TextBuilder.symbolize_keys(hash)
|
|
522
283
|
end
|
|
523
284
|
|
|
524
285
|
def get_store_for_scope(scope)
|
|
@@ -551,19 +312,174 @@ module ClaudeMemory
|
|
|
551
312
|
end
|
|
552
313
|
|
|
553
314
|
def format_shortcut_results(results, category)
|
|
315
|
+
ResponseFormatter.format_shortcut_results(category, results)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def facts_by_tool(args)
|
|
319
|
+
tool_name = args["tool_name"]
|
|
320
|
+
scope = extract_scope(args)
|
|
321
|
+
limit = extract_limit(args, default: 20)
|
|
322
|
+
|
|
323
|
+
results = @recall.facts_by_tool(tool_name, limit: limit, scope: scope)
|
|
324
|
+
ResponseFormatter.format_tool_facts(tool_name, scope, results)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def facts_by_context(args)
|
|
328
|
+
scope = extract_scope(args)
|
|
329
|
+
limit = extract_limit(args, default: 20)
|
|
330
|
+
|
|
331
|
+
if args["git_branch"]
|
|
332
|
+
results = @recall.facts_by_branch(args["git_branch"], limit: limit, scope: scope)
|
|
333
|
+
context_type = "git_branch"
|
|
334
|
+
context_value = args["git_branch"]
|
|
335
|
+
elsif args["cwd"]
|
|
336
|
+
results = @recall.facts_by_directory(args["cwd"], limit: limit, scope: scope)
|
|
337
|
+
context_type = "cwd"
|
|
338
|
+
context_value = args["cwd"]
|
|
339
|
+
else
|
|
340
|
+
return {error: "Must provide either git_branch or cwd parameter"}
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
ResponseFormatter.format_context_facts(context_type, context_value, scope, results)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def recall_semantic(args)
|
|
347
|
+
query = args["query"]
|
|
348
|
+
mode = (args["mode"] || "both").to_sym
|
|
349
|
+
scope = extract_scope(args)
|
|
350
|
+
limit = extract_limit(args)
|
|
351
|
+
|
|
352
|
+
results = @recall.query_semantic(query, limit: limit, scope: scope, mode: mode)
|
|
353
|
+
ResponseFormatter.format_semantic_results(query, mode.to_s, scope, results)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def search_concepts(args)
|
|
357
|
+
concepts = args["concepts"]
|
|
358
|
+
scope = extract_scope(args)
|
|
359
|
+
limit = extract_limit(args)
|
|
360
|
+
|
|
361
|
+
return {error: "Must provide 2-5 concepts"} unless (2..5).cover?(concepts.size)
|
|
362
|
+
|
|
363
|
+
results = @recall.query_concepts(concepts, limit: limit, scope: scope)
|
|
364
|
+
ResponseFormatter.format_concept_results(concepts, scope, results)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def databases_exist?
|
|
368
|
+
if @manager
|
|
369
|
+
# For dual-database mode, at least global database should exist
|
|
370
|
+
config = Configuration.new
|
|
371
|
+
File.exist?(config.global_db_path)
|
|
372
|
+
elsif @legacy_store
|
|
373
|
+
# For legacy mode, check if the database file exists
|
|
374
|
+
# Extract the database path from the store's connection
|
|
375
|
+
db_path = @legacy_store.db.opts[:database]
|
|
376
|
+
db_path && File.exist?(db_path)
|
|
377
|
+
else
|
|
378
|
+
false
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def database_not_found_error(error)
|
|
554
383
|
{
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
384
|
+
error: "Database not found or not accessible",
|
|
385
|
+
message: "ClaudeMemory may not be initialized. Run memory.check_setup for detailed status.",
|
|
386
|
+
details: error.message,
|
|
387
|
+
recommendations: [
|
|
388
|
+
"Run memory.check_setup to diagnose the issue",
|
|
389
|
+
"If not initialized, run: claude-memory init",
|
|
390
|
+
"For help: claude-memory doctor"
|
|
391
|
+
]
|
|
392
|
+
}
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def check_setup
|
|
396
|
+
issues = []
|
|
397
|
+
warnings = []
|
|
398
|
+
config = Configuration.new
|
|
399
|
+
|
|
400
|
+
# Check global database
|
|
401
|
+
global_db_exists = File.exist?(config.global_db_path)
|
|
402
|
+
unless global_db_exists
|
|
403
|
+
issues << "Global database not found at #{config.global_db_path}"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Check project database
|
|
407
|
+
project_db_exists = File.exist?(config.project_db_path)
|
|
408
|
+
unless project_db_exists
|
|
409
|
+
warnings << "Project database not found at #{config.project_db_path}"
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Check for CLAUDE.md and version
|
|
413
|
+
claude_md_path = ".claude/CLAUDE.md"
|
|
414
|
+
claude_md_exists = File.exist?(claude_md_path)
|
|
415
|
+
current_version = nil
|
|
416
|
+
version_status = nil
|
|
417
|
+
|
|
418
|
+
if claude_md_exists
|
|
419
|
+
content = File.read(claude_md_path)
|
|
420
|
+
if content.include?("ClaudeMemory")
|
|
421
|
+
current_version = SetupStatusAnalyzer.extract_version(content)
|
|
422
|
+
if current_version
|
|
423
|
+
version_status = SetupStatusAnalyzer.determine_version_status(current_version, ClaudeMemory::VERSION)
|
|
424
|
+
if version_status == "outdated"
|
|
425
|
+
warnings << "Configuration version (v#{current_version}) is older than ClaudeMemory (v#{ClaudeMemory::VERSION}). Consider running upgrade."
|
|
426
|
+
end
|
|
427
|
+
else
|
|
428
|
+
version_status = "no_version_marker"
|
|
429
|
+
warnings << "CLAUDE.md has ClaudeMemory section but no version marker"
|
|
430
|
+
end
|
|
431
|
+
else
|
|
432
|
+
warnings << "CLAUDE.md exists but no ClaudeMemory configuration found"
|
|
433
|
+
end
|
|
434
|
+
else
|
|
435
|
+
warnings << "No .claude/CLAUDE.md found"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Check hooks configuration
|
|
439
|
+
hooks_configured = false
|
|
440
|
+
settings_paths = [".claude/settings.json", ".claude/settings.local.json"]
|
|
441
|
+
settings_paths.each do |path|
|
|
442
|
+
if File.exist?(path)
|
|
443
|
+
begin
|
|
444
|
+
config_data = JSON.parse(File.read(path))
|
|
445
|
+
if config_data["hooks"]&.any?
|
|
446
|
+
hooks_configured = true
|
|
447
|
+
break
|
|
448
|
+
end
|
|
449
|
+
rescue JSON::ParserError
|
|
450
|
+
warnings << "Invalid JSON in #{path}"
|
|
451
|
+
end
|
|
566
452
|
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
unless hooks_configured
|
|
456
|
+
warnings << "No hooks configured for automatic ingestion"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Determine overall status using analyzer
|
|
460
|
+
initialized = global_db_exists && claude_md_exists
|
|
461
|
+
status = SetupStatusAnalyzer.determine_status(global_db_exists, claude_md_exists, version_status)
|
|
462
|
+
|
|
463
|
+
# Generate recommendations using analyzer
|
|
464
|
+
recommendations = SetupStatusAnalyzer.generate_recommendations(initialized, version_status, warnings.any?)
|
|
465
|
+
|
|
466
|
+
{
|
|
467
|
+
status: status,
|
|
468
|
+
initialized: initialized,
|
|
469
|
+
version: {
|
|
470
|
+
current: current_version || "unknown",
|
|
471
|
+
latest: ClaudeMemory::VERSION,
|
|
472
|
+
status: version_status || "unknown"
|
|
473
|
+
},
|
|
474
|
+
components: {
|
|
475
|
+
global_database: global_db_exists,
|
|
476
|
+
project_database: project_db_exists,
|
|
477
|
+
claude_md: claude_md_exists,
|
|
478
|
+
hooks_configured: hooks_configured
|
|
479
|
+
},
|
|
480
|
+
issues: issues,
|
|
481
|
+
warnings: warnings,
|
|
482
|
+
recommendations: recommendations
|
|
567
483
|
}
|
|
568
484
|
end
|
|
569
485
|
|
|
@@ -577,6 +493,100 @@ module ClaudeMemory
|
|
|
577
493
|
schema_version: store.schema_version
|
|
578
494
|
}
|
|
579
495
|
end
|
|
496
|
+
|
|
497
|
+
def detailed_stats(store)
|
|
498
|
+
result = {exists: true}
|
|
499
|
+
|
|
500
|
+
# Facts statistics
|
|
501
|
+
total_facts = store.facts.count
|
|
502
|
+
active_facts = store.facts.where(status: "active").count
|
|
503
|
+
superseded_facts = store.facts.where(status: "superseded").count
|
|
504
|
+
|
|
505
|
+
result[:facts] = {
|
|
506
|
+
total: total_facts,
|
|
507
|
+
active: active_facts,
|
|
508
|
+
superseded: superseded_facts
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
# Top predicates
|
|
512
|
+
if active_facts > 0
|
|
513
|
+
top_predicates = store.db[:facts]
|
|
514
|
+
.where(status: "active")
|
|
515
|
+
.group_and_count(:predicate)
|
|
516
|
+
.order(Sequel.desc(:count))
|
|
517
|
+
.limit(10)
|
|
518
|
+
.all
|
|
519
|
+
.map { |row| {predicate: row[:predicate], count: row[:count]} }
|
|
520
|
+
|
|
521
|
+
result[:facts][:top_predicates] = top_predicates
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Entities by type
|
|
525
|
+
entity_counts = store.db[:entities]
|
|
526
|
+
.group_and_count(:type)
|
|
527
|
+
.order(Sequel.desc(:count))
|
|
528
|
+
.all
|
|
529
|
+
.map { |row| {type: row[:type], count: row[:count]} }
|
|
530
|
+
|
|
531
|
+
result[:entities] = {
|
|
532
|
+
total: store.entities.count,
|
|
533
|
+
by_type: entity_counts
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# Content items
|
|
537
|
+
content_count = store.content_items.count
|
|
538
|
+
result[:content_items] = {
|
|
539
|
+
total: content_count
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if content_count > 0
|
|
543
|
+
first_date = store.content_items.min(:occurred_at)
|
|
544
|
+
last_date = store.content_items.max(:occurred_at)
|
|
545
|
+
result[:content_items][:date_range] = {
|
|
546
|
+
first: first_date,
|
|
547
|
+
last: last_date
|
|
548
|
+
}
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Provenance coverage
|
|
552
|
+
if active_facts > 0
|
|
553
|
+
facts_with_provenance = store.db[:provenance]
|
|
554
|
+
.join(:facts, id: :fact_id)
|
|
555
|
+
.where(Sequel[:facts][:status] => "active")
|
|
556
|
+
.select(Sequel[:provenance][:fact_id])
|
|
557
|
+
.distinct
|
|
558
|
+
.count
|
|
559
|
+
|
|
560
|
+
coverage_percentage = (facts_with_provenance * 100.0 / active_facts).round(1)
|
|
561
|
+
|
|
562
|
+
result[:provenance] = {
|
|
563
|
+
facts_with_sources: facts_with_provenance,
|
|
564
|
+
total_active_facts: active_facts,
|
|
565
|
+
coverage_percentage: coverage_percentage
|
|
566
|
+
}
|
|
567
|
+
else
|
|
568
|
+
result[:provenance] = {
|
|
569
|
+
facts_with_sources: 0,
|
|
570
|
+
total_active_facts: 0,
|
|
571
|
+
coverage_percentage: 0
|
|
572
|
+
}
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Conflicts
|
|
576
|
+
open_conflicts = store.conflicts.where(status: "open").count
|
|
577
|
+
resolved_conflicts = store.conflicts.where(status: "resolved").count
|
|
578
|
+
|
|
579
|
+
result[:conflicts] = {
|
|
580
|
+
open: open_conflicts,
|
|
581
|
+
resolved: resolved_conflicts,
|
|
582
|
+
total: open_conflicts + resolved_conflicts
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
# Schema version
|
|
586
|
+
result[:schema_version] = store.schema_version
|
|
587
|
+
|
|
588
|
+
result
|
|
589
|
+
end
|
|
580
590
|
end
|
|
581
591
|
end
|
|
582
592
|
end
|