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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module ClaudeMemory
|
|
6
|
+
module Commands
|
|
7
|
+
module Initializers
|
|
8
|
+
# Orchestrates project-local ClaudeMemory initialization
|
|
9
|
+
class ProjectInitializer
|
|
10
|
+
def initialize(stdout, stderr, stdin)
|
|
11
|
+
@stdout = stdout
|
|
12
|
+
@stderr = stderr
|
|
13
|
+
@stdin = stdin
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize_memory
|
|
17
|
+
@stdout.puts "Initializing ClaudeMemory (project-local)...\n\n"
|
|
18
|
+
|
|
19
|
+
# Check for existing hooks and offer options
|
|
20
|
+
hooks_config = HooksConfigurator.new(@stdout)
|
|
21
|
+
if hooks_config.has_claude_memory_hooks?(".claude/settings.json")
|
|
22
|
+
handle_existing_hooks(hooks_config)
|
|
23
|
+
return 0 if @skip_initialization
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
ensure_databases
|
|
27
|
+
ensure_directories
|
|
28
|
+
configure_hooks unless @skip_hooks
|
|
29
|
+
configure_mcp
|
|
30
|
+
configure_memory_instructions
|
|
31
|
+
install_output_style
|
|
32
|
+
|
|
33
|
+
print_completion_message
|
|
34
|
+
0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def handle_existing_hooks(hooks_config)
|
|
40
|
+
@stdout.puts "⚠️ Existing claude-memory hooks detected in .claude/settings.json"
|
|
41
|
+
@stdout.puts "\nOptions:"
|
|
42
|
+
@stdout.puts " 1. Update to current version (recommended)"
|
|
43
|
+
@stdout.puts " 2. Remove hooks (uninstall)"
|
|
44
|
+
@stdout.puts " 3. Leave as-is (skip)"
|
|
45
|
+
@stdout.print "\nChoice [1]: "
|
|
46
|
+
|
|
47
|
+
choice = @stdin.gets.to_s.strip
|
|
48
|
+
choice = "1" if choice.empty?
|
|
49
|
+
|
|
50
|
+
case choice
|
|
51
|
+
when "2"
|
|
52
|
+
@stdout.puts "\nRemoving hooks..."
|
|
53
|
+
hooks_config.remove_hooks_from_file(".claude/settings.json")
|
|
54
|
+
@stdout.puts "✓ Hooks removed. Run 'claude-memory uninstall' for full cleanup."
|
|
55
|
+
@skip_initialization = true
|
|
56
|
+
when "3"
|
|
57
|
+
@stdout.puts "\nSkipping hook configuration."
|
|
58
|
+
@skip_hooks = true
|
|
59
|
+
else
|
|
60
|
+
@stdout.puts "\nUpdating hooks..."
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def ensure_databases
|
|
65
|
+
DatabaseEnsurer.new(@stdout).ensure_project_databases
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ensure_directories
|
|
69
|
+
FileUtils.mkdir_p(".claude/rules")
|
|
70
|
+
@stdout.puts "✓ Created .claude/rules directory"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def configure_hooks
|
|
74
|
+
HooksConfigurator.new(@stdout).configure_project_hooks
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def configure_mcp
|
|
78
|
+
McpConfigurator.new(@stdout).configure_project_mcp
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def configure_memory_instructions
|
|
82
|
+
MemoryInstructionsWriter.new(@stdout).write_project_instructions
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def install_output_style
|
|
86
|
+
style_source = File.join(__dir__, "../../../output_styles/claude_memory.json")
|
|
87
|
+
style_dest = ".claude/output_styles/claude_memory.json"
|
|
88
|
+
|
|
89
|
+
return unless File.exist?(style_source)
|
|
90
|
+
|
|
91
|
+
FileUtils.mkdir_p(File.dirname(style_dest))
|
|
92
|
+
FileUtils.cp(style_source, style_dest)
|
|
93
|
+
@stdout.puts "✓ Installed output style at #{style_dest}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def print_completion_message
|
|
97
|
+
@stdout.puts "\n=== Setup Complete ===\n"
|
|
98
|
+
@stdout.puts "ClaudeMemory is now configured for this project."
|
|
99
|
+
@stdout.puts "\nDatabases:"
|
|
100
|
+
@stdout.puts " Global: ~/.claude/memory.sqlite3 (user-wide knowledge)"
|
|
101
|
+
@stdout.puts " Project: .claude/memory.sqlite3 (project-specific)"
|
|
102
|
+
@stdout.puts "\nNext steps:"
|
|
103
|
+
@stdout.puts " 1. Restart Claude Code to load the new configuration"
|
|
104
|
+
@stdout.puts " 2. Use Claude Code normally - transcripts will be ingested automatically"
|
|
105
|
+
@stdout.puts " 3. Run 'claude-memory promote <fact_id>' to move facts to global"
|
|
106
|
+
@stdout.puts " 4. Run 'claude-memory doctor' to verify setup"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module Commands
|
|
5
|
+
# Recovers from stuck operations by resetting them
|
|
6
|
+
class RecoverCommand < BaseCommand
|
|
7
|
+
def call(args)
|
|
8
|
+
opts = parse_options(args, {operation: nil, scope: nil}) do |o|
|
|
9
|
+
OptionParser.new do |parser|
|
|
10
|
+
parser.banner = "Usage: claude-memory recover [options]"
|
|
11
|
+
parser.on("--operation TYPE", "Filter by operation type") { |v| o[:operation] = v }
|
|
12
|
+
parser.on("--scope SCOPE", "Filter by scope (global/project)") { |v| o[:scope] = v }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
return 1 if opts.nil?
|
|
16
|
+
|
|
17
|
+
manager = Store::StoreManager.new
|
|
18
|
+
|
|
19
|
+
total_reset = 0
|
|
20
|
+
|
|
21
|
+
# Reset stuck operations in global database
|
|
22
|
+
if opts[:scope].nil? || opts[:scope] == "global"
|
|
23
|
+
if File.exist?(manager.global_db_path)
|
|
24
|
+
count = reset_stuck_operations(
|
|
25
|
+
manager.global_store,
|
|
26
|
+
"global",
|
|
27
|
+
opts[:operation]
|
|
28
|
+
)
|
|
29
|
+
total_reset += count
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Reset stuck operations in project database
|
|
34
|
+
if opts[:scope].nil? || opts[:scope] == "project"
|
|
35
|
+
if File.exist?(manager.project_db_path)
|
|
36
|
+
count = reset_stuck_operations(
|
|
37
|
+
manager.project_store,
|
|
38
|
+
"project",
|
|
39
|
+
opts[:operation]
|
|
40
|
+
)
|
|
41
|
+
total_reset += count
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
manager.close
|
|
46
|
+
|
|
47
|
+
if total_reset.zero?
|
|
48
|
+
stdout.puts "No stuck operations found."
|
|
49
|
+
else
|
|
50
|
+
stdout.puts "Reset #{total_reset} stuck operation(s)."
|
|
51
|
+
stdout.puts "You can now re-run the failed operation."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def reset_stuck_operations(store, scope_label, operation_type_filter)
|
|
60
|
+
tracker = Infrastructure::OperationTracker.new(store)
|
|
61
|
+
|
|
62
|
+
count = tracker.reset_stuck_operations(
|
|
63
|
+
operation_type: operation_type_filter,
|
|
64
|
+
scope: scope_label
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if count > 0
|
|
68
|
+
stdout.puts "#{scope_label.capitalize}: Reset #{count} stuck operation(s)"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
count
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -11,6 +11,7 @@ module ClaudeMemory
|
|
|
11
11
|
"help" => "HelpCommand",
|
|
12
12
|
"version" => "VersionCommand",
|
|
13
13
|
"doctor" => "DoctorCommand",
|
|
14
|
+
"stats" => "StatsCommand",
|
|
14
15
|
"promote" => "PromoteCommand",
|
|
15
16
|
"search" => "SearchCommand",
|
|
16
17
|
"explain" => "ExplainCommand",
|
|
@@ -22,8 +23,11 @@ module ClaudeMemory
|
|
|
22
23
|
"publish" => "PublishCommand",
|
|
23
24
|
"db:init" => "DbInitCommand",
|
|
24
25
|
"init" => "InitCommand",
|
|
26
|
+
"uninstall" => "UninstallCommand",
|
|
25
27
|
"serve-mcp" => "ServeMcpCommand",
|
|
26
|
-
"hook" => "HookCommand"
|
|
28
|
+
"hook" => "HookCommand",
|
|
29
|
+
"index" => "IndexCommand",
|
|
30
|
+
"recover" => "RecoverCommand"
|
|
27
31
|
}.freeze
|
|
28
32
|
|
|
29
33
|
# Find a command class by name
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module ClaudeMemory
|
|
6
|
+
module Commands
|
|
7
|
+
# Displays detailed statistics about the memory system
|
|
8
|
+
# Shows facts by status and predicate, entities by type, content items,
|
|
9
|
+
# provenance coverage, conflicts, and database sizes
|
|
10
|
+
class StatsCommand < BaseCommand
|
|
11
|
+
SCOPE_ALL = "all"
|
|
12
|
+
SCOPE_GLOBAL = "global"
|
|
13
|
+
SCOPE_PROJECT = "project"
|
|
14
|
+
|
|
15
|
+
def call(args)
|
|
16
|
+
opts = parse_options(args, {scope: SCOPE_ALL}) do |o|
|
|
17
|
+
OptionParser.new do |parser|
|
|
18
|
+
parser.banner = "Usage: claude-memory stats [options]"
|
|
19
|
+
parser.on("--scope SCOPE", ["all", "global", "project"],
|
|
20
|
+
"Show stats for: all (default), global, or project") { |v| o[:scope] = v }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
return 1 if opts.nil?
|
|
24
|
+
|
|
25
|
+
manager = ClaudeMemory::Store::StoreManager.new
|
|
26
|
+
|
|
27
|
+
stdout.puts "ClaudeMemory Statistics"
|
|
28
|
+
stdout.puts "=" * 50
|
|
29
|
+
stdout.puts
|
|
30
|
+
|
|
31
|
+
if opts[:scope] == SCOPE_ALL || opts[:scope] == SCOPE_GLOBAL
|
|
32
|
+
print_database_stats("GLOBAL", manager.global_db_path)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if opts[:scope] == SCOPE_ALL || opts[:scope] == SCOPE_PROJECT
|
|
36
|
+
print_database_stats("PROJECT", manager.project_db_path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
manager.close
|
|
40
|
+
0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def print_database_stats(label, db_path)
|
|
46
|
+
stdout.puts "## #{label} DATABASE"
|
|
47
|
+
stdout.puts
|
|
48
|
+
|
|
49
|
+
unless File.exist?(db_path)
|
|
50
|
+
stdout.puts "Database does not exist: #{db_path}"
|
|
51
|
+
stdout.puts
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
db = Sequel.sqlite(db_path, readonly: true)
|
|
57
|
+
|
|
58
|
+
# Facts statistics
|
|
59
|
+
print_fact_stats(db)
|
|
60
|
+
stdout.puts
|
|
61
|
+
|
|
62
|
+
# Entities statistics
|
|
63
|
+
print_entity_stats(db)
|
|
64
|
+
stdout.puts
|
|
65
|
+
|
|
66
|
+
# Content items statistics
|
|
67
|
+
print_content_stats(db)
|
|
68
|
+
stdout.puts
|
|
69
|
+
|
|
70
|
+
# Provenance coverage
|
|
71
|
+
print_provenance_stats(db)
|
|
72
|
+
stdout.puts
|
|
73
|
+
|
|
74
|
+
# Conflicts
|
|
75
|
+
print_conflict_stats(db)
|
|
76
|
+
stdout.puts
|
|
77
|
+
|
|
78
|
+
# ROI Metrics (if available)
|
|
79
|
+
print_roi_metrics(db)
|
|
80
|
+
stdout.puts
|
|
81
|
+
|
|
82
|
+
# Database size
|
|
83
|
+
print_database_size(db_path)
|
|
84
|
+
stdout.puts
|
|
85
|
+
|
|
86
|
+
db.disconnect
|
|
87
|
+
rescue => e
|
|
88
|
+
stderr.puts "Error reading database: #{e.message}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def print_fact_stats(db)
|
|
93
|
+
total = db[:facts].count
|
|
94
|
+
active = db[:facts].where(status: "active").count
|
|
95
|
+
superseded = db[:facts].where(status: "superseded").count
|
|
96
|
+
|
|
97
|
+
stdout.puts "Facts:"
|
|
98
|
+
stdout.puts " Total: #{total} (#{active} active, #{superseded} superseded)"
|
|
99
|
+
|
|
100
|
+
if total > 0
|
|
101
|
+
stdout.puts
|
|
102
|
+
stdout.puts " Top Predicates:"
|
|
103
|
+
|
|
104
|
+
predicate_counts = db[:facts]
|
|
105
|
+
.where(status: "active")
|
|
106
|
+
.group_and_count(:predicate)
|
|
107
|
+
.order(Sequel.desc(:count))
|
|
108
|
+
.limit(10)
|
|
109
|
+
.all
|
|
110
|
+
|
|
111
|
+
predicate_counts.each do |row|
|
|
112
|
+
stdout.puts " #{row[:count].to_s.rjust(4)} - #{row[:predicate]}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def print_entity_stats(db)
|
|
118
|
+
total = db[:entities].count
|
|
119
|
+
|
|
120
|
+
stdout.puts "Entities: #{total}"
|
|
121
|
+
|
|
122
|
+
if total > 0
|
|
123
|
+
type_counts = db[:entities]
|
|
124
|
+
.group_and_count(:type)
|
|
125
|
+
.order(Sequel.desc(:count))
|
|
126
|
+
.all
|
|
127
|
+
|
|
128
|
+
type_counts.each do |row|
|
|
129
|
+
stdout.puts " #{row[:count].to_s.rjust(4)} - #{row[:type]}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def print_content_stats(db)
|
|
135
|
+
total = db[:content_items].count
|
|
136
|
+
|
|
137
|
+
stdout.puts "Content Items: #{total}"
|
|
138
|
+
|
|
139
|
+
if total > 0
|
|
140
|
+
first_date = db[:content_items].min(:occurred_at)
|
|
141
|
+
last_date = db[:content_items].max(:occurred_at)
|
|
142
|
+
|
|
143
|
+
if first_date && last_date
|
|
144
|
+
first_formatted = format_date(first_date)
|
|
145
|
+
last_formatted = format_date(last_date)
|
|
146
|
+
stdout.puts " Date Range: #{first_formatted} - #{last_formatted}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def print_provenance_stats(db)
|
|
152
|
+
total_active_facts = db[:facts].where(status: "active").count
|
|
153
|
+
facts_with_provenance = db[:provenance]
|
|
154
|
+
.join(:facts, id: :fact_id)
|
|
155
|
+
.where(Sequel[:facts][:status] => "active")
|
|
156
|
+
.select(Sequel[:provenance][:fact_id])
|
|
157
|
+
.distinct
|
|
158
|
+
.count
|
|
159
|
+
|
|
160
|
+
if total_active_facts > 0
|
|
161
|
+
percentage = (facts_with_provenance * 100.0 / total_active_facts).round(1)
|
|
162
|
+
stdout.puts "Provenance: #{facts_with_provenance}/#{total_active_facts} facts have sources (#{percentage}%)"
|
|
163
|
+
else
|
|
164
|
+
stdout.puts "Provenance: 0/0 facts have sources"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def print_conflict_stats(db)
|
|
169
|
+
open = db[:conflicts].where(status: "open").count
|
|
170
|
+
resolved = db[:conflicts].where(status: "resolved").count
|
|
171
|
+
total = open + resolved
|
|
172
|
+
|
|
173
|
+
stdout.puts "Conflicts: #{open} open, #{resolved} resolved (#{total} total)"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def print_roi_metrics(db)
|
|
177
|
+
# Check if ingestion_metrics table exists (schema v7+)
|
|
178
|
+
return unless db.table_exists?(:ingestion_metrics)
|
|
179
|
+
|
|
180
|
+
# standard:disable Performance/Detect (Sequel DSL requires .select{}.first)
|
|
181
|
+
result = db[:ingestion_metrics]
|
|
182
|
+
.select {
|
|
183
|
+
[
|
|
184
|
+
sum(:input_tokens).as(:total_input),
|
|
185
|
+
sum(:output_tokens).as(:total_output),
|
|
186
|
+
sum(:facts_extracted).as(:total_facts),
|
|
187
|
+
count(:id).as(:total_ops)
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
.first
|
|
191
|
+
# standard:enable Performance/Detect
|
|
192
|
+
|
|
193
|
+
return if result.nil? || result[:total_ops].to_i.zero?
|
|
194
|
+
|
|
195
|
+
total_input = result[:total_input].to_i
|
|
196
|
+
total_output = result[:total_output].to_i
|
|
197
|
+
total_facts = result[:total_facts].to_i
|
|
198
|
+
total_ops = result[:total_ops].to_i
|
|
199
|
+
|
|
200
|
+
efficiency = total_input.zero? ? 0.0 : (total_facts.to_f / total_input * 1000).round(2)
|
|
201
|
+
|
|
202
|
+
stdout.puts "Token Economics (Distillation ROI):"
|
|
203
|
+
stdout.puts " Input Tokens: #{format_number(total_input)}"
|
|
204
|
+
stdout.puts " Output Tokens: #{format_number(total_output)}"
|
|
205
|
+
stdout.puts " Facts Extracted: #{format_number(total_facts)}"
|
|
206
|
+
stdout.puts " Operations: #{format_number(total_ops)}"
|
|
207
|
+
stdout.puts " Efficiency: #{efficiency} facts per 1,000 input tokens"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def print_database_size(db_path)
|
|
211
|
+
size_bytes = File.size(db_path)
|
|
212
|
+
size_kb = (size_bytes / 1024.0).round(1)
|
|
213
|
+
size_mb = (size_bytes / (1024.0 * 1024.0)).round(2)
|
|
214
|
+
|
|
215
|
+
if size_mb >= 1.0
|
|
216
|
+
stdout.puts "Database Size: #{size_mb} MB"
|
|
217
|
+
else
|
|
218
|
+
stdout.puts "Database Size: #{size_kb} KB"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def format_date(iso8601_string)
|
|
223
|
+
# Extract just the date part (YYYY-MM-DD) from ISO8601 timestamp
|
|
224
|
+
return iso8601_string unless iso8601_string
|
|
225
|
+
|
|
226
|
+
date_part = iso8601_string.split("T").first
|
|
227
|
+
return date_part if date_part
|
|
228
|
+
|
|
229
|
+
# Fallback to first 10 chars
|
|
230
|
+
iso8601_string[0...10]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def format_number(num)
|
|
234
|
+
# Format number with comma separators (e.g., 1234567 => "1,234,567")
|
|
235
|
+
num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module ClaudeMemory
|
|
7
|
+
module Commands
|
|
8
|
+
# Removes ClaudeMemory configuration from a project or globally
|
|
9
|
+
class UninstallCommand < BaseCommand
|
|
10
|
+
def call(args)
|
|
11
|
+
opts = parse_options(args, {global: false, full: false}) do |o|
|
|
12
|
+
OptionParser.new do |parser|
|
|
13
|
+
parser.banner = "Usage: claude-memory uninstall [options]"
|
|
14
|
+
parser.on("--global", "Uninstall from global ~/.claude/ settings") { o[:global] = true }
|
|
15
|
+
parser.on("--full", "Remove databases and all configuration (not just hooks)") { o[:full] = true }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
return 1 if opts.nil?
|
|
19
|
+
|
|
20
|
+
if opts[:global]
|
|
21
|
+
uninstall_global(opts[:full])
|
|
22
|
+
else
|
|
23
|
+
uninstall_local(opts[:full])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def uninstall_local(full)
|
|
30
|
+
stdout.puts "Uninstalling ClaudeMemory (project-local)...\n\n"
|
|
31
|
+
|
|
32
|
+
removed_items = []
|
|
33
|
+
|
|
34
|
+
# Remove hooks from settings.json
|
|
35
|
+
if remove_hooks_from_file(".claude/settings.json")
|
|
36
|
+
removed_items << "Hooks from .claude/settings.json"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Remove MCP server from .claude.json
|
|
40
|
+
if remove_mcp_server(".claude.json")
|
|
41
|
+
removed_items << "MCP server from .claude.json"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Remove ClaudeMemory section from .claude/CLAUDE.md
|
|
45
|
+
if remove_claude_md_section(".claude/CLAUDE.md")
|
|
46
|
+
removed_items << "ClaudeMemory section from .claude/CLAUDE.md"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if full
|
|
50
|
+
# Remove databases and generated files
|
|
51
|
+
files_to_remove = [
|
|
52
|
+
".claude/memory.sqlite3",
|
|
53
|
+
".claude/rules/claude_memory.generated.md",
|
|
54
|
+
".claude/output_styles/claude_memory.json"
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
files_to_remove.each do |file|
|
|
58
|
+
if File.exist?(file)
|
|
59
|
+
FileUtils.rm_f(file)
|
|
60
|
+
removed_items << "Removed #{file}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
stdout.puts "\n=== Uninstall Complete ===\n"
|
|
66
|
+
if removed_items.any?
|
|
67
|
+
stdout.puts "Removed:"
|
|
68
|
+
removed_items.each { |item| stdout.puts " ✓ #{item}" }
|
|
69
|
+
else
|
|
70
|
+
stdout.puts "No ClaudeMemory configuration found."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if full
|
|
74
|
+
stdout.puts "\nNote: Global database (~/.claude/memory.sqlite3) was NOT removed."
|
|
75
|
+
stdout.puts " Run 'claude-memory uninstall --global --full' to remove it."
|
|
76
|
+
else
|
|
77
|
+
stdout.puts "\nNote: Project database (.claude/memory.sqlite3) was NOT removed."
|
|
78
|
+
stdout.puts " Run with --full flag to remove all files."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
stdout.puts "\nRestart Claude Code to complete uninstallation."
|
|
82
|
+
|
|
83
|
+
0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def uninstall_global(full)
|
|
87
|
+
stdout.puts "Uninstalling ClaudeMemory (global)...\n\n"
|
|
88
|
+
|
|
89
|
+
removed_items = []
|
|
90
|
+
|
|
91
|
+
# Remove hooks from ~/.claude/settings.json
|
|
92
|
+
global_settings = File.join(Dir.home, ".claude", "settings.json")
|
|
93
|
+
if remove_hooks_from_file(global_settings)
|
|
94
|
+
removed_items << "Hooks from #{global_settings}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Remove MCP server from ~/.claude.json
|
|
98
|
+
global_mcp = File.join(Dir.home, ".claude.json")
|
|
99
|
+
if remove_mcp_server(global_mcp)
|
|
100
|
+
removed_items << "MCP server from #{global_mcp}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Remove ClaudeMemory section from ~/.claude/CLAUDE.md
|
|
104
|
+
global_claude_md = File.join(Dir.home, ".claude", "CLAUDE.md")
|
|
105
|
+
if remove_claude_md_section(global_claude_md)
|
|
106
|
+
removed_items << "ClaudeMemory section from #{global_claude_md}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if full
|
|
110
|
+
# Remove global database
|
|
111
|
+
global_db = ClaudeMemory.global_db_path
|
|
112
|
+
if File.exist?(global_db)
|
|
113
|
+
FileUtils.rm_f(global_db)
|
|
114
|
+
removed_items << "Removed #{global_db}"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
stdout.puts "\n=== Global Uninstall Complete ===\n"
|
|
119
|
+
if removed_items.any?
|
|
120
|
+
stdout.puts "Removed:"
|
|
121
|
+
removed_items.each { |item| stdout.puts " ✓ #{item}" }
|
|
122
|
+
else
|
|
123
|
+
stdout.puts "No ClaudeMemory configuration found."
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if full
|
|
127
|
+
stdout.puts "\nWarning: Global database was removed. All user-wide knowledge is deleted."
|
|
128
|
+
else
|
|
129
|
+
stdout.puts "\nNote: Global database (~/.claude/memory.sqlite3) was NOT removed."
|
|
130
|
+
stdout.puts " Run with --full flag to remove all files."
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
stdout.puts "\nRestart Claude Code to complete uninstallation."
|
|
134
|
+
|
|
135
|
+
0
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Removes claude-memory hooks from a settings.json file
|
|
139
|
+
# Returns true if hooks were removed, false otherwise
|
|
140
|
+
def remove_hooks_from_file(settings_path)
|
|
141
|
+
return false unless File.exist?(settings_path)
|
|
142
|
+
|
|
143
|
+
begin
|
|
144
|
+
config = JSON.parse(File.read(settings_path))
|
|
145
|
+
rescue JSON::ParserError
|
|
146
|
+
stderr.puts "Warning: Could not parse #{settings_path}, skipping hook removal"
|
|
147
|
+
return false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
return false unless config["hooks"]
|
|
151
|
+
|
|
152
|
+
modified = false
|
|
153
|
+
config["hooks"].each do |event, hook_arrays|
|
|
154
|
+
next unless hook_arrays.is_a?(Array)
|
|
155
|
+
|
|
156
|
+
# Filter out hook arrays that contain claude-memory commands
|
|
157
|
+
original_count = hook_arrays.size
|
|
158
|
+
hook_arrays.reject! do |hook_array|
|
|
159
|
+
next false unless hook_array["hooks"].is_a?(Array)
|
|
160
|
+
|
|
161
|
+
hook_array["hooks"].any? { |h| h["command"]&.include?("claude-memory") }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
modified = true if hook_arrays.size < original_count
|
|
165
|
+
|
|
166
|
+
# Remove empty event keys
|
|
167
|
+
config["hooks"].delete(event) if hook_arrays.empty?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if modified
|
|
171
|
+
# Remove hooks key entirely if empty
|
|
172
|
+
config.delete("hooks") if config["hooks"].empty?
|
|
173
|
+
|
|
174
|
+
File.write(settings_path, JSON.pretty_generate(config))
|
|
175
|
+
true
|
|
176
|
+
else
|
|
177
|
+
false
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Removes claude-memory MCP server from .claude.json
|
|
182
|
+
# Returns true if server was removed, false otherwise
|
|
183
|
+
def remove_mcp_server(mcp_path)
|
|
184
|
+
return false unless File.exist?(mcp_path)
|
|
185
|
+
|
|
186
|
+
begin
|
|
187
|
+
config = JSON.parse(File.read(mcp_path))
|
|
188
|
+
rescue JSON::ParserError
|
|
189
|
+
stderr.puts "Warning: Could not parse #{mcp_path}, skipping MCP removal"
|
|
190
|
+
return false
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
return false unless config["mcpServers"]&.key?("claude-memory")
|
|
194
|
+
|
|
195
|
+
config["mcpServers"].delete("claude-memory")
|
|
196
|
+
config.delete("mcpServers") if config["mcpServers"].empty?
|
|
197
|
+
|
|
198
|
+
File.write(mcp_path, JSON.pretty_generate(config))
|
|
199
|
+
true
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Removes ClaudeMemory section from CLAUDE.md
|
|
203
|
+
# Returns true if section was removed, false otherwise
|
|
204
|
+
def remove_claude_md_section(claude_md_path)
|
|
205
|
+
return false unless File.exist?(claude_md_path)
|
|
206
|
+
|
|
207
|
+
content = File.read(claude_md_path)
|
|
208
|
+
return false unless content.include?("ClaudeMemory")
|
|
209
|
+
|
|
210
|
+
# Remove ClaudeMemory section (marked by HTML comment through end of section)
|
|
211
|
+
# Match from HTML comment through content until we hit another HTML comment or ## header
|
|
212
|
+
modified_content = content.gsub(/<!-- ClaudeMemory v.*?-->.*?(?=^##|\n<!--|\Z)/m, "")
|
|
213
|
+
|
|
214
|
+
# Clean up extra blank lines
|
|
215
|
+
modified_content = modified_content.gsub(/\n{3,}/, "\n\n").strip
|
|
216
|
+
|
|
217
|
+
if modified_content != content
|
|
218
|
+
File.write(claude_md_path, modified_content)
|
|
219
|
+
true
|
|
220
|
+
else
|
|
221
|
+
false
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module Core
|
|
5
|
+
# Utility for batch loading records from Sequel datasets
|
|
6
|
+
# Eliminates duplication of batch query patterns
|
|
7
|
+
class BatchLoader
|
|
8
|
+
# Load multiple records by IDs and organize them by a key
|
|
9
|
+
#
|
|
10
|
+
# @param dataset [Sequel::Dataset] The dataset to query
|
|
11
|
+
# @param ids [Array] The IDs to load
|
|
12
|
+
# @param group_by [Symbol] How to organize results (:single for hash by ID, or column name for grouping)
|
|
13
|
+
# @return [Hash] Results organized by the specified key
|
|
14
|
+
def self.load_many(dataset, ids, group_by: :id)
|
|
15
|
+
return {} if ids.empty?
|
|
16
|
+
|
|
17
|
+
results = dataset.where(id: ids).all
|
|
18
|
+
|
|
19
|
+
case group_by
|
|
20
|
+
when :single
|
|
21
|
+
# Single record per ID (hash by ID)
|
|
22
|
+
results.each_with_object({}) { |row, hash| hash[row[:id]] = row }
|
|
23
|
+
when Symbol
|
|
24
|
+
# Multiple records per key (grouped)
|
|
25
|
+
results.group_by { |row| row[group_by] }
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "Invalid group_by: #{group_by.inspect}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|