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.
- checksums.yaml +4 -4
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/rules/claude_memory.generated.md +32 -2
- data/.claude/settings.json +65 -15
- data/.claude/settings.local.json +5 -2
- data/.claude/skills/improve/SKILL.md +113 -25
- data/.claude/skills/upgrade-dependencies/SKILL.md +154 -0
- data/.claude-plugin/commands/distill-transcripts.md +98 -0
- data/.claude-plugin/commands/memory-recall.md +67 -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 +90 -1
- data/CLAUDE.md +56 -18
- 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 +74 -74
- 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/docs/quality_review.md +119 -224
- data/hooks/hooks.json +39 -7
- data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
- data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
- data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
- data/lib/claude_memory/commands/completion_command.rb +149 -0
- data/lib/claude_memory/commands/doctor_command.rb +2 -0
- data/lib/claude_memory/commands/embeddings_command.rb +198 -0
- data/lib/claude_memory/commands/help_command.rb +12 -1
- data/lib/claude_memory/commands/hook_command.rb +2 -1
- data/lib/claude_memory/commands/index_command.rb +85 -78
- data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
- data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
- data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
- data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
- data/lib/claude_memory/commands/install_skill_command.rb +78 -0
- data/lib/claude_memory/commands/registry.rb +47 -32
- 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 +102 -0
- data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
- data/lib/claude_memory/commands/stats_command.rb +98 -2
- data/lib/claude_memory/configuration.rb +14 -1
- data/lib/claude_memory/core/fact_ranker.rb +2 -2
- data/lib/claude_memory/core/rr_fusion.rb +23 -6
- data/lib/claude_memory/core/snippet_extractor.rb +7 -3
- data/lib/claude_memory/core/text_builder.rb +11 -0
- 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/domain/provenance.rb +0 -1
- data/lib/claude_memory/embeddings/api_adapter.rb +97 -0
- data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
- data/lib/claude_memory/embeddings/fastembed_adapter.rb +46 -12
- data/lib/claude_memory/embeddings/generator.rb +4 -0
- 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 +44 -0
- data/lib/claude_memory/hook/context_injector.rb +58 -2
- data/lib/claude_memory/hook/distillation_runner.rb +46 -0
- data/lib/claude_memory/hook/handler.rb +11 -2
- data/lib/claude_memory/index/vector_index.rb +15 -2
- data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
- data/lib/claude_memory/ingest/ingester.rb +17 -0
- data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
- data/lib/claude_memory/mcp/handlers/management_handlers.rb +169 -0
- data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
- data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
- data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
- data/lib/claude_memory/mcp/handlers/stats_handlers.rb +205 -0
- data/lib/claude_memory/mcp/instructions_builder.rb +19 -1
- data/lib/claude_memory/mcp/query_guide.rb +10 -0
- data/lib/claude_memory/mcp/response_formatter.rb +1 -0
- data/lib/claude_memory/mcp/server.rb +22 -1
- data/lib/claude_memory/mcp/telemetry.rb +86 -0
- data/lib/claude_memory/mcp/text_summary.rb +26 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +116 -4
- data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
- data/lib/claude_memory/mcp/tools.rb +50 -679
- data/lib/claude_memory/publish.rb +40 -5
- data/lib/claude_memory/recall/dual_engine.rb +105 -0
- data/lib/claude_memory/recall/legacy_engine.rb +138 -0
- data/lib/claude_memory/recall/query_core.rb +371 -0
- data/lib/claude_memory/recall.rb +121 -673
- data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
- data/lib/claude_memory/resolve/resolver.rb +43 -0
- data/lib/claude_memory/shortcuts.rb +4 -4
- data/lib/claude_memory/store/retry_handler.rb +61 -0
- data/lib/claude_memory/store/schema_manager.rb +68 -0
- data/lib/claude_memory/store/sqlite_store.rb +334 -201
- 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/templates/hooks.example.json +26 -7
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +16 -0
- metadata +48 -8
- data/.claude/memory.sqlite3-shm +0 -0
- data/.claude/memory.sqlite3-wal +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Handlers
|
|
6
|
+
# Management tool handlers (store_extraction, promote, sweep, changes, conflicts)
|
|
7
|
+
module ManagementHandlers
|
|
8
|
+
def store_extraction(args)
|
|
9
|
+
scope = args["scope"] || "project"
|
|
10
|
+
store = get_store_for_scope(scope)
|
|
11
|
+
return {error: "Database not available"} unless store
|
|
12
|
+
|
|
13
|
+
entities = (args["entities"] || []).map { |e| symbolize_keys(e) }
|
|
14
|
+
facts = (args["facts"] || []).map { |f| symbolize_keys(f) }
|
|
15
|
+
decisions = (args["decisions"] || []).map { |d| symbolize_keys(d) }
|
|
16
|
+
|
|
17
|
+
config = Configuration.new
|
|
18
|
+
project_path = config.project_dir
|
|
19
|
+
occurred_at = Time.now.utc.iso8601
|
|
20
|
+
|
|
21
|
+
searchable_text = Core::TextBuilder.build_searchable_text(entities, facts, decisions)
|
|
22
|
+
content_item_id = create_synthetic_content_item(store, searchable_text, project_path, occurred_at)
|
|
23
|
+
index_content_item(store, content_item_id, searchable_text)
|
|
24
|
+
|
|
25
|
+
extraction = Distill::Extraction.new(
|
|
26
|
+
entities: entities,
|
|
27
|
+
facts: facts,
|
|
28
|
+
decisions: decisions,
|
|
29
|
+
signals: []
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
resolver = Resolve::Resolver.new(store)
|
|
33
|
+
result = resolver.apply(
|
|
34
|
+
extraction,
|
|
35
|
+
content_item_id: content_item_id,
|
|
36
|
+
occurred_at: occurred_at,
|
|
37
|
+
project_path: project_path,
|
|
38
|
+
scope: scope
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
success: true,
|
|
43
|
+
scope: scope,
|
|
44
|
+
entities_created: result[:entities_created],
|
|
45
|
+
facts_created: result[:facts_created],
|
|
46
|
+
facts_superseded: result[:facts_superseded],
|
|
47
|
+
conflicts_created: result[:conflicts_created]
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def promote(args)
|
|
52
|
+
return {error: "Promote requires StoreManager"} unless @manager
|
|
53
|
+
|
|
54
|
+
fact_id = args["fact_id"]
|
|
55
|
+
global_fact_id = @manager.promote_fact(fact_id)
|
|
56
|
+
|
|
57
|
+
if global_fact_id
|
|
58
|
+
{
|
|
59
|
+
success: true,
|
|
60
|
+
project_fact_id: fact_id,
|
|
61
|
+
global_fact_id: global_fact_id,
|
|
62
|
+
message: "Fact promoted to global memory"
|
|
63
|
+
}
|
|
64
|
+
else
|
|
65
|
+
{error: "Fact #{fact_id} not found in project database"}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def reject_fact(args)
|
|
70
|
+
scope = args["scope"] || "project"
|
|
71
|
+
store = get_store_for_scope(scope)
|
|
72
|
+
return {error: "Database not available"} unless store
|
|
73
|
+
|
|
74
|
+
fact_id = args["fact_id"]
|
|
75
|
+
if fact_id.nil? && args["docid"]
|
|
76
|
+
row = store.find_fact_by_docid(args["docid"])
|
|
77
|
+
fact_id = row && row[:id]
|
|
78
|
+
end
|
|
79
|
+
return {error: "fact_id or docid required"} if fact_id.nil?
|
|
80
|
+
|
|
81
|
+
result = store.reject_fact(fact_id, reason: args["reason"])
|
|
82
|
+
return {error: "Fact #{fact_id} not found in #{scope} database"} if result.nil?
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
success: true,
|
|
86
|
+
scope: scope,
|
|
87
|
+
fact_id: fact_id,
|
|
88
|
+
conflicts_resolved: result[:conflicts_resolved],
|
|
89
|
+
message: "Fact rejected"
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def sweep_now(args)
|
|
94
|
+
scope = args["scope"] || "project"
|
|
95
|
+
store = get_store_for_scope(scope)
|
|
96
|
+
return {error: "Database not available"} unless store
|
|
97
|
+
|
|
98
|
+
sweeper = Sweep::Sweeper.new(store)
|
|
99
|
+
budget = args["budget_seconds"] || 5
|
|
100
|
+
stats = if args["escalate"]
|
|
101
|
+
sweeper.run_with_escalation!(budget_seconds: budget)
|
|
102
|
+
else
|
|
103
|
+
sweeper.run!(budget_seconds: budget)
|
|
104
|
+
end
|
|
105
|
+
ResponseFormatter.format_sweep_stats(scope, stats)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def changes(args)
|
|
109
|
+
since = args["since"] || (Time.now - 86400 * 7).utc.iso8601
|
|
110
|
+
scope = args["scope"] || "all"
|
|
111
|
+
list = @recall.changes(since: since, limit: args["limit"] || 20, scope: scope)
|
|
112
|
+
ResponseFormatter.format_changes(since, list)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def conflicts(args)
|
|
116
|
+
scope = args["scope"] || "all"
|
|
117
|
+
list = @recall.conflicts(scope: scope)
|
|
118
|
+
ResponseFormatter.format_conflicts(list)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def mark_distilled(args)
|
|
122
|
+
content_item_id = args["content_item_id"]
|
|
123
|
+
facts_extracted = args["facts_extracted"] || 0
|
|
124
|
+
|
|
125
|
+
store = find_store_for_content_item(content_item_id)
|
|
126
|
+
return {error: "Content item #{content_item_id} not found"} unless store
|
|
127
|
+
|
|
128
|
+
store.record_ingestion_metrics(
|
|
129
|
+
content_item_id: content_item_id,
|
|
130
|
+
input_tokens: 0,
|
|
131
|
+
output_tokens: 0,
|
|
132
|
+
facts_extracted: facts_extracted
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
success: true,
|
|
137
|
+
content_item_id: content_item_id,
|
|
138
|
+
facts_extracted: facts_extracted
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def create_synthetic_content_item(store, text, project_path, occurred_at)
|
|
145
|
+
text_hash = Digest::SHA256.hexdigest(text)
|
|
146
|
+
store.upsert_content_item(
|
|
147
|
+
source: "mcp_extraction",
|
|
148
|
+
session_id: "mcp-#{Time.now.to_i}",
|
|
149
|
+
transcript_path: nil,
|
|
150
|
+
project_path: project_path,
|
|
151
|
+
text_hash: text_hash,
|
|
152
|
+
byte_len: text.bytesize,
|
|
153
|
+
raw_text: text,
|
|
154
|
+
occurred_at: occurred_at
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def index_content_item(store, content_item_id, text)
|
|
159
|
+
fts = Index::LexicalFTS.new(store)
|
|
160
|
+
fts.index_content_item(content_item_id, text)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def symbolize_keys(hash)
|
|
164
|
+
Core::TextBuilder.symbolize_keys(hash)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Handlers
|
|
6
|
+
# Query and recall tool handlers
|
|
7
|
+
module QueryHandlers
|
|
8
|
+
def recall(args)
|
|
9
|
+
return database_not_found_error unless databases_exist?
|
|
10
|
+
|
|
11
|
+
scope = extract_scope(args)
|
|
12
|
+
limit = extract_limit(args)
|
|
13
|
+
compact = args["compact"] == true
|
|
14
|
+
query = args["query"]
|
|
15
|
+
intent = extract_intent(args)
|
|
16
|
+
results = @recall.query(query, limit: limit, scope: scope, include_raw_text: !compact, intent: intent)
|
|
17
|
+
ResponseFormatter.format_recall_results(results, compact: compact, query: query)
|
|
18
|
+
rescue Sequel::DatabaseError, Sequel::DatabaseConnectionError, Errno::ENOENT => e
|
|
19
|
+
classified_error(e, tool_name: "memory.recall")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def recall_index(args)
|
|
23
|
+
scope = extract_scope(args)
|
|
24
|
+
limit = extract_limit(args, default: 20)
|
|
25
|
+
intent = extract_intent(args)
|
|
26
|
+
results = @recall.query_index(args["query"], limit: limit, scope: scope, intent: intent)
|
|
27
|
+
ResponseFormatter.format_index_results(args["query"], scope, results)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def recall_details(args)
|
|
31
|
+
fact_ids = args["fact_ids"]
|
|
32
|
+
scope = args["scope"] || "project"
|
|
33
|
+
|
|
34
|
+
explanations = fact_ids.map do |fact_id|
|
|
35
|
+
explanation = @recall.explain(fact_id, scope: scope)
|
|
36
|
+
next nil if explanation.is_a?(Core::NullExplanation)
|
|
37
|
+
|
|
38
|
+
ResponseFormatter.format_detailed_explanation(explanation)
|
|
39
|
+
end.compact
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
fact_count: explanations.size,
|
|
43
|
+
facts: explanations
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def explain(args)
|
|
48
|
+
scope = args["scope"] || "project"
|
|
49
|
+
explanation = @recall.explain(args["fact_id"], scope: scope)
|
|
50
|
+
return {error: "Fact not found in #{scope} database"} if explanation.is_a?(Core::NullExplanation)
|
|
51
|
+
|
|
52
|
+
ResponseFormatter.format_explanation(explanation, scope)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def recall_semantic(args)
|
|
56
|
+
query = args["query"]
|
|
57
|
+
mode = (args["mode"] || "both").to_sym
|
|
58
|
+
scope = extract_scope(args)
|
|
59
|
+
limit = extract_limit(args)
|
|
60
|
+
compact = args["compact"] == true
|
|
61
|
+
explain = args["explain"] == true
|
|
62
|
+
intent = extract_intent(args)
|
|
63
|
+
|
|
64
|
+
results = @recall.query_semantic(query, limit: limit, scope: scope, mode: mode, explain: explain, intent: intent)
|
|
65
|
+
ResponseFormatter.format_semantic_results(query, mode.to_s, scope, results, compact: compact)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def search_concepts(args)
|
|
69
|
+
concepts = args["concepts"]
|
|
70
|
+
scope = extract_scope(args)
|
|
71
|
+
limit = extract_limit(args)
|
|
72
|
+
compact = args["compact"] == true
|
|
73
|
+
|
|
74
|
+
return {error: "Must provide 2-5 concepts"} unless (2..5).cover?(concepts.size)
|
|
75
|
+
|
|
76
|
+
results = @recall.query_concepts(concepts, limit: limit, scope: scope)
|
|
77
|
+
ResponseFormatter.format_concept_results(concepts, scope, results, compact: compact)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def fact_graph(args)
|
|
81
|
+
fact_id = args["fact_id"]
|
|
82
|
+
depth = args["depth"] || 2
|
|
83
|
+
scope = args["scope"] || "project"
|
|
84
|
+
|
|
85
|
+
graph = @recall.fact_graph(fact_id, depth: depth, scope: scope)
|
|
86
|
+
|
|
87
|
+
return {error: "Fact #{fact_id} not found in #{scope} database"} if graph[:node_count] == 0
|
|
88
|
+
|
|
89
|
+
graph
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def undistilled(args)
|
|
93
|
+
limit = args["limit"] || 3
|
|
94
|
+
min_length = args["min_length"] || 200
|
|
95
|
+
max_text = 2000
|
|
96
|
+
|
|
97
|
+
items = collect_undistilled_items(limit: limit, min_length: min_length)
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
count: items.size,
|
|
101
|
+
items: items.map { |item|
|
|
102
|
+
{
|
|
103
|
+
content_item_id: item[:id],
|
|
104
|
+
occurred_at: item[:occurred_at],
|
|
105
|
+
occurred_ago: Core::RelativeTime.format(item[:occurred_at]),
|
|
106
|
+
project_path: item[:project_path],
|
|
107
|
+
raw_text: Core::TextBuilder.truncate(item[:raw_text], max_text)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Handlers
|
|
6
|
+
# Setup and discovery tool handlers
|
|
7
|
+
module SetupHandlers
|
|
8
|
+
def check_setup
|
|
9
|
+
issues = []
|
|
10
|
+
warnings = []
|
|
11
|
+
config = Configuration.new
|
|
12
|
+
|
|
13
|
+
global_db_exists = check_global_database(config, issues)
|
|
14
|
+
project_db_exists = check_project_database(config, warnings)
|
|
15
|
+
current_version, version_status, claude_md_exists = check_claude_md_version(warnings)
|
|
16
|
+
hooks_configured = check_hooks_configuration(warnings)
|
|
17
|
+
|
|
18
|
+
build_setup_result(
|
|
19
|
+
global_db_exists, project_db_exists, claude_md_exists,
|
|
20
|
+
hooks_configured, current_version, version_status,
|
|
21
|
+
issues, warnings
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def list_projects
|
|
26
|
+
result = {global: nil, current_project: nil, other_projects: []}
|
|
27
|
+
|
|
28
|
+
if @manager
|
|
29
|
+
result[:global] = list_global_database
|
|
30
|
+
result[:current_project] = list_current_project
|
|
31
|
+
result[:other_projects] = discover_other_projects
|
|
32
|
+
elsif @legacy_store
|
|
33
|
+
result[:global] = {
|
|
34
|
+
exists: true,
|
|
35
|
+
path: @legacy_store.db.opts[:database],
|
|
36
|
+
facts_active: @legacy_store.facts.where(status: "active").count,
|
|
37
|
+
entities: @legacy_store.entities.count
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
result[:project_count] = 1 + result[:other_projects].size
|
|
42
|
+
result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def check_global_database(config, issues)
|
|
48
|
+
exists = File.exist?(config.global_db_path)
|
|
49
|
+
issues << "Global database not found at #{config.global_db_path}" unless exists
|
|
50
|
+
exists
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def check_project_database(config, warnings)
|
|
54
|
+
exists = File.exist?(config.project_db_path)
|
|
55
|
+
warnings << "Project database not found at #{config.project_db_path}" unless exists
|
|
56
|
+
exists
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check_claude_md_version(warnings)
|
|
60
|
+
claude_md_path = ".claude/CLAUDE.md"
|
|
61
|
+
unless File.exist?(claude_md_path)
|
|
62
|
+
warnings << "No .claude/CLAUDE.md found"
|
|
63
|
+
return [nil, nil, false]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
content = File.read(claude_md_path)
|
|
67
|
+
unless content.include?("ClaudeMemory")
|
|
68
|
+
warnings << "CLAUDE.md exists but no ClaudeMemory configuration found"
|
|
69
|
+
return [nil, nil, true]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
current_version = SetupStatusAnalyzer.extract_version(content)
|
|
73
|
+
unless current_version
|
|
74
|
+
warnings << "CLAUDE.md has ClaudeMemory section but no version marker"
|
|
75
|
+
return [nil, "no_version_marker", true]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
version_status = SetupStatusAnalyzer.determine_version_status(current_version, ClaudeMemory::VERSION)
|
|
79
|
+
if version_status == "outdated"
|
|
80
|
+
warnings << "Configuration version (v#{current_version}) is older than ClaudeMemory (v#{ClaudeMemory::VERSION}). Consider running upgrade."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
[current_version, version_status, true]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def check_hooks_configuration(warnings)
|
|
87
|
+
settings_paths = [".claude/settings.json", ".claude/settings.local.json"]
|
|
88
|
+
settings_paths.each do |path|
|
|
89
|
+
next unless File.exist?(path)
|
|
90
|
+
begin
|
|
91
|
+
config_data = JSON.parse(File.read(path))
|
|
92
|
+
return true if config_data["hooks"]&.any?
|
|
93
|
+
rescue JSON::ParserError
|
|
94
|
+
warnings << "Invalid JSON in #{path}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
warnings << "No hooks configured for automatic ingestion"
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_setup_result(global_db_exists, project_db_exists, claude_md_exists, hooks_configured, current_version, version_status, issues, warnings)
|
|
103
|
+
initialized = global_db_exists && claude_md_exists
|
|
104
|
+
status = SetupStatusAnalyzer.determine_status(global_db_exists, claude_md_exists, version_status)
|
|
105
|
+
recommendations = SetupStatusAnalyzer.generate_recommendations(initialized, version_status, warnings.any?)
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
status: status,
|
|
109
|
+
initialized: initialized,
|
|
110
|
+
version: {
|
|
111
|
+
current: current_version || "unknown",
|
|
112
|
+
latest: ClaudeMemory::VERSION,
|
|
113
|
+
status: version_status || "unknown"
|
|
114
|
+
},
|
|
115
|
+
components: {
|
|
116
|
+
global_database: global_db_exists,
|
|
117
|
+
project_database: project_db_exists,
|
|
118
|
+
claude_md: claude_md_exists,
|
|
119
|
+
hooks_configured: hooks_configured
|
|
120
|
+
},
|
|
121
|
+
issues: issues,
|
|
122
|
+
warnings: warnings,
|
|
123
|
+
recommendations: recommendations
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def list_global_database
|
|
128
|
+
if @manager.global_exists?
|
|
129
|
+
@manager.ensure_global!
|
|
130
|
+
store = @manager.global_store
|
|
131
|
+
{
|
|
132
|
+
exists: true,
|
|
133
|
+
path: @manager.global_db_path,
|
|
134
|
+
facts_active: store.facts.where(status: "active").count,
|
|
135
|
+
facts_total: store.facts.count,
|
|
136
|
+
entities: store.entities.count
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
{exists: false, path: @manager.global_db_path}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def list_current_project
|
|
144
|
+
if @manager.project_exists?
|
|
145
|
+
@manager.ensure_project!
|
|
146
|
+
store = @manager.project_store
|
|
147
|
+
{
|
|
148
|
+
exists: true,
|
|
149
|
+
path: @manager.project_path,
|
|
150
|
+
db_path: @manager.project_db_path,
|
|
151
|
+
facts_active: store.facts.where(status: "active").count,
|
|
152
|
+
facts_total: store.facts.count,
|
|
153
|
+
entities: store.entities.count
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
{exists: false, path: @manager.project_path, db_path: @manager.project_db_path}
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def discover_other_projects
|
|
161
|
+
return [] unless @manager.global_exists?
|
|
162
|
+
|
|
163
|
+
@manager.ensure_global!
|
|
164
|
+
global = @manager.global_store
|
|
165
|
+
|
|
166
|
+
promoted_paths = global.facts
|
|
167
|
+
.where(Sequel.like(:created_from, "promoted:%"))
|
|
168
|
+
.select(:created_from)
|
|
169
|
+
.distinct
|
|
170
|
+
.all
|
|
171
|
+
.filter_map { |f|
|
|
172
|
+
match = f[:created_from]&.match(/\Apromoted:(.+):\d+\z/)
|
|
173
|
+
match[1] if match
|
|
174
|
+
}
|
|
175
|
+
.uniq
|
|
176
|
+
|
|
177
|
+
fact_paths = global.facts
|
|
178
|
+
.exclude(project_path: nil)
|
|
179
|
+
.select(:project_path)
|
|
180
|
+
.distinct
|
|
181
|
+
.all
|
|
182
|
+
.map { |f| f[:project_path] }
|
|
183
|
+
|
|
184
|
+
all_paths = (promoted_paths + fact_paths).uniq
|
|
185
|
+
current = @manager.project_path
|
|
186
|
+
|
|
187
|
+
all_paths.filter_map { |path|
|
|
188
|
+
next if path == current
|
|
189
|
+
|
|
190
|
+
db_path = File.join(path, ".claude", "memory.sqlite3")
|
|
191
|
+
entry = {path: path, db_path: db_path, exists: File.exist?(db_path)}
|
|
192
|
+
|
|
193
|
+
if entry[:exists]
|
|
194
|
+
begin
|
|
195
|
+
temp_store = Store::SQLiteStore.new(db_path)
|
|
196
|
+
entry[:facts_active] = temp_store.facts.where(status: "active").count
|
|
197
|
+
entry[:facts_total] = temp_store.facts.count
|
|
198
|
+
entry[:entities] = temp_store.entities.count
|
|
199
|
+
temp_store.close
|
|
200
|
+
rescue Sequel::DatabaseError, Extralite::Error, IOError => _e
|
|
201
|
+
entry[:error] = "Could not read database"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
entry
|
|
206
|
+
}
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Handlers
|
|
6
|
+
# Shortcut tool handlers (decisions, conventions, architecture)
|
|
7
|
+
module ShortcutHandlers
|
|
8
|
+
def decisions(args)
|
|
9
|
+
return {error: "Decisions shortcut requires StoreManager"} unless @manager
|
|
10
|
+
|
|
11
|
+
results = Recall.recent_decisions(@manager, limit: args["limit"] || 10)
|
|
12
|
+
format_shortcut_results(results, "decisions")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def conventions(args)
|
|
16
|
+
return {error: "Conventions shortcut requires StoreManager"} unless @manager
|
|
17
|
+
|
|
18
|
+
results = Recall.conventions(@manager, limit: args["limit"] || 20)
|
|
19
|
+
format_shortcut_results(results, "conventions")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def architecture(args)
|
|
23
|
+
return {error: "Architecture shortcut requires StoreManager"} unless @manager
|
|
24
|
+
|
|
25
|
+
results = Recall.architecture_choices(@manager, limit: args["limit"] || 10)
|
|
26
|
+
format_shortcut_results(results, "architecture")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def format_shortcut_results(results, category)
|
|
32
|
+
ResponseFormatter.format_shortcut_results(category, results)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|