claude_memory 0.8.0 → 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 +30 -52
- data/.claude/settings.local.json +3 -1
- 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 +41 -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 +22 -1
- 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 +26 -8
- data/.claude/memory.sqlite3-shm +0 -0
- data/.claude/memory.sqlite3-wal +0 -0
|
@@ -3,48 +3,48 @@
|
|
|
3
3
|
module ClaudeMemory
|
|
4
4
|
module Commands
|
|
5
5
|
# Registry for CLI command lookup and dispatch
|
|
6
|
-
# Maps command names to command classes
|
|
6
|
+
# Maps command names to command classes and short descriptions.
|
|
7
|
+
# Descriptions are authoritative for shell completion and help output;
|
|
8
|
+
# keep them current when adding commands.
|
|
7
9
|
class Registry
|
|
8
|
-
# Map of command names to class
|
|
9
|
-
# As more commands are extracted, add them here
|
|
10
|
+
# Map of command names to {class:, description:} entries.
|
|
11
|
+
# As more commands are extracted, add them here.
|
|
10
12
|
COMMANDS = {
|
|
11
|
-
"help" =>
|
|
12
|
-
"version" =>
|
|
13
|
-
"doctor" =>
|
|
14
|
-
"stats" =>
|
|
15
|
-
"promote" =>
|
|
16
|
-
"search" =>
|
|
17
|
-
"explain" =>
|
|
18
|
-
"conflicts" =>
|
|
19
|
-
"changes" =>
|
|
20
|
-
"recall" =>
|
|
21
|
-
"sweep" =>
|
|
22
|
-
"ingest" =>
|
|
23
|
-
"publish" =>
|
|
24
|
-
"db:init" =>
|
|
25
|
-
"init" =>
|
|
26
|
-
"uninstall" =>
|
|
27
|
-
"serve-mcp" =>
|
|
28
|
-
"hook" =>
|
|
29
|
-
"index" =>
|
|
30
|
-
"recover" =>
|
|
31
|
-
"compact" =>
|
|
32
|
-
"export" =>
|
|
33
|
-
"git-lfs" =>
|
|
34
|
-
"install-skill" =>
|
|
35
|
-
"completion" =>
|
|
13
|
+
"help" => {class: HelpCommand, description: "Show help message"},
|
|
14
|
+
"version" => {class: VersionCommand, description: "Show version"},
|
|
15
|
+
"doctor" => {class: DoctorCommand, description: "Check system health"},
|
|
16
|
+
"stats" => {class: StatsCommand, description: "Show statistics"},
|
|
17
|
+
"promote" => {class: PromoteCommand, description: "Promote fact to global"},
|
|
18
|
+
"search" => {class: SearchCommand, description: "Search indexed content"},
|
|
19
|
+
"explain" => {class: ExplainCommand, description: "Explain a fact with receipts"},
|
|
20
|
+
"conflicts" => {class: ConflictsCommand, description: "Show open conflicts"},
|
|
21
|
+
"changes" => {class: ChangesCommand, description: "Show recent fact changes"},
|
|
22
|
+
"recall" => {class: RecallCommand, description: "Recall facts matching query"},
|
|
23
|
+
"sweep" => {class: SweepCommand, description: "Run maintenance"},
|
|
24
|
+
"ingest" => {class: IngestCommand, description: "Ingest transcript delta"},
|
|
25
|
+
"publish" => {class: PublishCommand, description: "Publish snapshot"},
|
|
26
|
+
"db:init" => {class: DbInitCommand, description: "Initialize database"},
|
|
27
|
+
"init" => {class: InitCommand, description: "Initialize ClaudeMemory"},
|
|
28
|
+
"uninstall" => {class: UninstallCommand, description: "Remove configuration"},
|
|
29
|
+
"serve-mcp" => {class: ServeMcpCommand, description: "Start MCP server"},
|
|
30
|
+
"hook" => {class: HookCommand, description: "Run hook entrypoints"},
|
|
31
|
+
"index" => {class: IndexCommand, description: "Index content"},
|
|
32
|
+
"recover" => {class: RecoverCommand, description: "Recover database"},
|
|
33
|
+
"compact" => {class: CompactCommand, description: "Compact databases"},
|
|
34
|
+
"export" => {class: ExportCommand, description: "Export facts to JSON"},
|
|
35
|
+
"git-lfs" => {class: GitLfsCommand, description: "Git LFS integration"},
|
|
36
|
+
"install-skill" => {class: InstallSkillCommand, description: "Install agent skills"},
|
|
37
|
+
"completion" => {class: CompletionCommand, description: "Generate shell completions"},
|
|
38
|
+
"embeddings" => {class: EmbeddingsCommand, description: "Inspect embedding backend"},
|
|
39
|
+
"reject" => {class: RejectCommand, description: "Mark a fact as rejected"},
|
|
40
|
+
"restore" => {class: RestoreCommand, description: "Restore superseded facts from obsolete single-value classification"}
|
|
36
41
|
}.freeze
|
|
37
42
|
|
|
38
43
|
# Find a command class by name
|
|
39
44
|
# @param command_name [String] the command name (e.g., "help", "version")
|
|
40
45
|
# @return [Class, nil] the command class, or nil if not found
|
|
41
46
|
def self.find(command_name)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class_name = COMMANDS[command_name]
|
|
45
|
-
return nil unless class_name
|
|
46
|
-
|
|
47
|
-
Commands.const_get(class_name)
|
|
47
|
+
COMMANDS.dig(command_name, :class)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# Get all registered command names
|
|
@@ -59,6 +59,19 @@ module ClaudeMemory
|
|
|
59
59
|
def self.registered?(command_name)
|
|
60
60
|
COMMANDS.key?(command_name)
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
# Get the short description for a command
|
|
64
|
+
# @param command_name [String] the command name
|
|
65
|
+
# @return [String, nil] the description, or nil if not registered
|
|
66
|
+
def self.description(command_name)
|
|
67
|
+
COMMANDS.dig(command_name, :description)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Get all command descriptions as a hash
|
|
71
|
+
# @return [Hash{String => String}] command name → description
|
|
72
|
+
def self.descriptions
|
|
73
|
+
COMMANDS.transform_values { |entry| entry[:description] }
|
|
74
|
+
end
|
|
62
75
|
end
|
|
63
76
|
end
|
|
64
77
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module ClaudeMemory
|
|
6
|
+
module Commands
|
|
7
|
+
# Mark a fact as rejected (e.g. a distiller hallucination).
|
|
8
|
+
# Closes any open conflicts involving the fact.
|
|
9
|
+
class RejectCommand < BaseCommand
|
|
10
|
+
# @param args [Array<String>] command line arguments (fact_id_or_docid, --scope, --reason)
|
|
11
|
+
# @return [Integer] exit code (0 for success, 1 for failure)
|
|
12
|
+
def call(args)
|
|
13
|
+
opts = parse_options(args, {scope: "project", reason: nil}) do |o|
|
|
14
|
+
OptionParser.new do |parser|
|
|
15
|
+
parser.banner = "Usage: claude-memory reject <fact_id_or_docid> [options]"
|
|
16
|
+
parser.on("--scope SCOPE", %w[project global], "Database scope (default: project)") { |v| o[:scope] = v }
|
|
17
|
+
parser.on("--reason TEXT", "Why this fact is wrong (recorded in conflict notes)") { |v| o[:reason] = v }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
return 1 if opts.nil?
|
|
21
|
+
|
|
22
|
+
identifier = args.first
|
|
23
|
+
return failure("Usage: claude-memory reject <fact_id_or_docid> [options]") if identifier.nil? || identifier.empty?
|
|
24
|
+
|
|
25
|
+
manager = ClaudeMemory::Store::StoreManager.new
|
|
26
|
+
store = manager.store_for_scope(opts[:scope])
|
|
27
|
+
|
|
28
|
+
fact_id = resolve_fact_id(store, identifier)
|
|
29
|
+
unless fact_id
|
|
30
|
+
stderr.puts "Fact '#{identifier}' not found in #{opts[:scope]} database."
|
|
31
|
+
manager.close
|
|
32
|
+
return 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
result = store.reject_fact(fact_id, reason: opts[:reason])
|
|
36
|
+
manager.close
|
|
37
|
+
|
|
38
|
+
if result.nil?
|
|
39
|
+
stderr.puts "Fact ##{fact_id} not found."
|
|
40
|
+
return 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
stdout.puts "Rejected fact ##{fact_id} in #{opts[:scope]} database."
|
|
44
|
+
stdout.puts "Resolved #{result[:conflicts_resolved]} open conflict(s)." if result[:conflicts_resolved] > 0
|
|
45
|
+
0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Accept either a numeric fact id or an 8-char docid hex string.
|
|
51
|
+
# @param store [Store::SQLiteStore] database to look up the fact in
|
|
52
|
+
# @param identifier [String] numeric fact id or hex docid
|
|
53
|
+
# @return [Integer, nil] resolved fact id, or nil if not found
|
|
54
|
+
def resolve_fact_id(store, identifier)
|
|
55
|
+
return identifier.to_i if identifier.match?(/\A\d+\z/)
|
|
56
|
+
|
|
57
|
+
row = store.find_fact_by_docid(identifier)
|
|
58
|
+
row ? row[:id] : nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module ClaudeMemory
|
|
6
|
+
module Commands
|
|
7
|
+
# One-time recovery for facts that were superseded because of an
|
|
8
|
+
# obsolete single-value predicate classification. See
|
|
9
|
+
# Sweep::Maintenance#restore_multi_value_supersessions for the algorithm
|
|
10
|
+
# and Jaccard heuristic.
|
|
11
|
+
class RestoreCommand < BaseCommand
|
|
12
|
+
# @param args [Array<String>] command line arguments (--predicate, --scope, --dry-run)
|
|
13
|
+
# @return [Integer] exit code (0 for success, 1 for failure)
|
|
14
|
+
def call(args)
|
|
15
|
+
opts = parse_options(args, {predicate: nil, scope: "project", dry_run: false}) do |o|
|
|
16
|
+
OptionParser.new do |parser|
|
|
17
|
+
parser.banner = "Usage: claude-memory restore --predicate NAME [options]"
|
|
18
|
+
parser.on("--predicate NAME", "Predicate to restore (e.g. uses_framework)") { |v| o[:predicate] = v }
|
|
19
|
+
parser.on("--scope SCOPE", %w[project global], "Database scope (default: project)") { |v| o[:scope] = v }
|
|
20
|
+
parser.on("--dry-run", "Show what would be restored without writing") { o[:dry_run] = true }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
return 1 if opts.nil?
|
|
24
|
+
|
|
25
|
+
return failure("--predicate required (e.g. --predicate uses_framework)") if opts[:predicate].nil?
|
|
26
|
+
|
|
27
|
+
manager = ClaudeMemory::Store::StoreManager.new
|
|
28
|
+
store = manager.store_for_scope(opts[:scope])
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
result = Sweep::Maintenance.new(store).restore_multi_value_supersessions(
|
|
32
|
+
predicate: opts[:predicate],
|
|
33
|
+
dry_run: opts[:dry_run]
|
|
34
|
+
)
|
|
35
|
+
rescue ArgumentError => e
|
|
36
|
+
stderr.puts e.message
|
|
37
|
+
manager.close
|
|
38
|
+
return 1
|
|
39
|
+
ensure
|
|
40
|
+
manager.close
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
print_result(opts, result)
|
|
44
|
+
0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Print a summary of restored and skipped facts.
|
|
50
|
+
# @param opts [Hash] parsed options including :predicate, :scope, :dry_run
|
|
51
|
+
# @param result [Hash] result from Sweep::Maintenance#restore_multi_value_supersessions
|
|
52
|
+
# @return [void]
|
|
53
|
+
def print_result(opts, result)
|
|
54
|
+
mode = opts[:dry_run] ? "DRY RUN" : "RESTORE"
|
|
55
|
+
stdout.puts "#{mode}: predicate=#{opts[:predicate]} scope=#{opts[:scope]}"
|
|
56
|
+
stdout.puts "=" * 50
|
|
57
|
+
stdout.puts "Inspected: #{result[:inspected]}"
|
|
58
|
+
stdout.puts "Restored: #{result[:restored]}"
|
|
59
|
+
stdout.puts "Skipped ambiguous: #{result[:skipped_ambiguous]}"
|
|
60
|
+
|
|
61
|
+
return if result[:decisions].empty?
|
|
62
|
+
|
|
63
|
+
stdout.puts
|
|
64
|
+
stdout.puts "Decisions:"
|
|
65
|
+
result[:decisions].each do |d|
|
|
66
|
+
case d[:action]
|
|
67
|
+
when :restore
|
|
68
|
+
stdout.puts " [restore] ##{d[:fact_id]} #{d[:object]}"
|
|
69
|
+
when :skip_ambiguous
|
|
70
|
+
overlaps = d[:overlaps_with].map { |o| "'#{o}'" }.join(", ")
|
|
71
|
+
stdout.puts " [skip] ##{d[:fact_id]} #{d[:object]} (overlaps: #{overlaps})"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -30,7 +30,11 @@ For each content item, carefully read the raw_text and extract:
|
|
|
30
30
|
|
|
31
31
|
**Facts** — Knowledge learned:
|
|
32
32
|
- subject: Entity name or "repo" for project-level facts
|
|
33
|
-
- predicate:
|
|
33
|
+
- predicate: prefer a predicate from the canonical vocabulary defined in
|
|
34
|
+
`lib/claude_memory/resolve/predicate_policy.rb` (convention, decision,
|
|
35
|
+
architecture, uses_framework, uses_language, uses_database,
|
|
36
|
+
deployment_platform, auth_method). Other snake_case predicates are
|
|
37
|
+
accepted but fall through to the default multi-value policy.
|
|
34
38
|
- object: The value
|
|
35
39
|
- confidence: 0.0-1.0
|
|
36
40
|
- quote: Source excerpt (max 200 chars)
|
|
@@ -13,15 +13,21 @@ module ClaudeMemory
|
|
|
13
13
|
SCOPE_PROJECT = "project"
|
|
14
14
|
|
|
15
15
|
def call(args)
|
|
16
|
-
opts = parse_options(args, {scope: SCOPE_ALL}) do |o|
|
|
16
|
+
opts = parse_options(args, {scope: SCOPE_ALL, tools: false, since_days: nil}) do |o|
|
|
17
17
|
OptionParser.new do |parser|
|
|
18
18
|
parser.banner = "Usage: claude-memory stats [options]"
|
|
19
19
|
parser.on("--scope SCOPE", ["all", "global", "project"],
|
|
20
20
|
"Show stats for: all (default), global, or project") { |v| o[:scope] = v }
|
|
21
|
+
parser.on("--tools", "Show MCP tool-call usage stats") { o[:tools] = true }
|
|
22
|
+
parser.on("--since DAYS", Integer, "Limit --tools to last N days") { |v| o[:since_days] = v }
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
return 1 if opts.nil?
|
|
24
26
|
|
|
27
|
+
if opts[:tools]
|
|
28
|
+
return print_mcp_tool_call_stats(opts[:since_days])
|
|
29
|
+
end
|
|
30
|
+
|
|
25
31
|
manager = ClaudeMemory::Store::StoreManager.new
|
|
26
32
|
|
|
27
33
|
stdout.puts "ClaudeMemory Statistics"
|
|
@@ -42,6 +48,10 @@ module ClaudeMemory
|
|
|
42
48
|
|
|
43
49
|
private
|
|
44
50
|
|
|
51
|
+
def open_readonly(db_path)
|
|
52
|
+
Sequel.connect("extralite://#{db_path}")
|
|
53
|
+
end
|
|
54
|
+
|
|
45
55
|
def print_database_stats(label, db_path)
|
|
46
56
|
stdout.puts "## #{label} DATABASE"
|
|
47
57
|
stdout.puts
|
|
@@ -53,7 +63,7 @@ module ClaudeMemory
|
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
begin
|
|
56
|
-
db =
|
|
66
|
+
db = open_readonly(db_path)
|
|
57
67
|
|
|
58
68
|
# Facts statistics
|
|
59
69
|
print_fact_stats(db)
|
|
@@ -245,6 +255,92 @@ module ClaudeMemory
|
|
|
245
255
|
# Format number with comma separators (e.g., 1234567 => "1,234,567")
|
|
246
256
|
num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
247
257
|
end
|
|
258
|
+
|
|
259
|
+
def print_mcp_tool_call_stats(since_days)
|
|
260
|
+
manager = ClaudeMemory::Store::StoreManager.new
|
|
261
|
+
db_path = manager.project_db_path
|
|
262
|
+
|
|
263
|
+
stdout.puts "MCP Tool Call Statistics"
|
|
264
|
+
stdout.puts "=" * 50
|
|
265
|
+
|
|
266
|
+
unless File.exist?(db_path)
|
|
267
|
+
stdout.puts "Project database does not exist: #{db_path}"
|
|
268
|
+
manager.close
|
|
269
|
+
return 0
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
db = open_readonly(db_path)
|
|
273
|
+
|
|
274
|
+
unless db.table_exists?(:mcp_tool_calls)
|
|
275
|
+
stdout.puts "No telemetry recorded yet (run MCP server first)."
|
|
276
|
+
db.disconnect
|
|
277
|
+
manager.close
|
|
278
|
+
return 0
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
dataset = db[:mcp_tool_calls]
|
|
282
|
+
if since_days
|
|
283
|
+
cutoff = (Time.now - since_days * 86400).utc.iso8601
|
|
284
|
+
dataset = dataset.where { called_at >= cutoff }
|
|
285
|
+
stdout.puts "Window: last #{since_days} day#{"s" unless since_days == 1}"
|
|
286
|
+
else
|
|
287
|
+
stdout.puts "Window: all time"
|
|
288
|
+
end
|
|
289
|
+
stdout.puts
|
|
290
|
+
|
|
291
|
+
total = dataset.count
|
|
292
|
+
if total.zero?
|
|
293
|
+
stdout.puts "No tool calls recorded in window."
|
|
294
|
+
db.disconnect
|
|
295
|
+
manager.close
|
|
296
|
+
return 0
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
errors = dataset.exclude(error_class: nil).count
|
|
300
|
+
error_rate = (errors * 100.0 / total).round(1)
|
|
301
|
+
stdout.puts "Total calls: #{format_number(total)}"
|
|
302
|
+
stdout.puts "Errors: #{format_number(errors)} (#{error_rate}%)"
|
|
303
|
+
stdout.puts
|
|
304
|
+
|
|
305
|
+
print_per_tool_breakdown(dataset)
|
|
306
|
+
|
|
307
|
+
db.disconnect
|
|
308
|
+
manager.close
|
|
309
|
+
0
|
|
310
|
+
rescue Sequel::DatabaseError, Extralite::Error => e
|
|
311
|
+
stderr.puts "Error reading telemetry: #{e.message}"
|
|
312
|
+
1
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def print_per_tool_breakdown(dataset)
|
|
316
|
+
stdout.puts "Per-tool breakdown:"
|
|
317
|
+
stdout.puts " #{"Tool".ljust(28)} #{"Calls".rjust(7)} #{"Avg ms".rjust(8)} #{"P95 ms".rjust(8)} #{"Err %".rjust(6)}"
|
|
318
|
+
|
|
319
|
+
rows = dataset
|
|
320
|
+
.group_and_count(:tool_name)
|
|
321
|
+
.order(Sequel.desc(:count))
|
|
322
|
+
.all
|
|
323
|
+
|
|
324
|
+
rows.each do |row|
|
|
325
|
+
tool = row[:tool_name]
|
|
326
|
+
calls = row[:count]
|
|
327
|
+
durations = dataset.where(tool_name: tool).select_map(:duration_ms).sort
|
|
328
|
+
avg = (durations.sum.to_f / calls).round(1)
|
|
329
|
+
p95 = percentile(durations, 0.95)
|
|
330
|
+
tool_errors = dataset.where(tool_name: tool).exclude(error_class: nil).count
|
|
331
|
+
tool_err_rate = (tool_errors * 100.0 / calls).round(1)
|
|
332
|
+
|
|
333
|
+
stdout.puts " #{tool.to_s.ljust(28)} #{calls.to_s.rjust(7)} #{avg.to_s.rjust(8)} #{p95.to_s.rjust(8)} #{tool_err_rate.to_s.rjust(6)}"
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def percentile(sorted, pct)
|
|
338
|
+
return 0 if sorted.empty?
|
|
339
|
+
idx = (sorted.size * pct).ceil - 1
|
|
340
|
+
idx = 0 if idx < 0
|
|
341
|
+
idx = sorted.size - 1 if idx >= sorted.size
|
|
342
|
+
sorted[idx]
|
|
343
|
+
end
|
|
248
344
|
end
|
|
249
345
|
end
|
|
250
346
|
end
|
|
@@ -8,31 +8,44 @@ module ClaudeMemory
|
|
|
8
8
|
class Configuration
|
|
9
9
|
attr_reader :env
|
|
10
10
|
|
|
11
|
+
# @param env [Hash] environment variables (default: ENV)
|
|
11
12
|
def initialize(env = ENV)
|
|
12
13
|
@env = env
|
|
13
14
|
end
|
|
14
15
|
|
|
16
|
+
# @return [String] user home directory
|
|
15
17
|
def home_dir
|
|
16
18
|
env["HOME"] || File.expand_path("~")
|
|
17
19
|
end
|
|
18
20
|
|
|
21
|
+
# @return [String] project root directory (resolves git worktrees)
|
|
19
22
|
def project_dir
|
|
20
23
|
env["CLAUDE_PROJECT_DIR"] || resolve_project_dir
|
|
21
24
|
end
|
|
22
25
|
|
|
26
|
+
# @return [String] Claude config directory (default: ~/.claude)
|
|
27
|
+
def claude_config_dir
|
|
28
|
+
env["CLAUDE_CONFIG_DIR"] || File.join(home_dir, ".claude")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [String] path to global memory database
|
|
23
32
|
def global_db_path
|
|
24
|
-
File.join(
|
|
33
|
+
File.join(claude_config_dir, "memory.sqlite3")
|
|
25
34
|
end
|
|
26
35
|
|
|
36
|
+
# @param project_path [String, nil] override project root (defaults to project_dir)
|
|
37
|
+
# @return [String] path to project memory database
|
|
27
38
|
def project_db_path(project_path = nil)
|
|
28
39
|
path = project_path || project_dir
|
|
29
40
|
File.join(path, ".claude", "memory.sqlite3")
|
|
30
41
|
end
|
|
31
42
|
|
|
43
|
+
# @return [String, nil] current Claude session ID from CLAUDE_SESSION_ID
|
|
32
44
|
def session_id
|
|
33
45
|
env["CLAUDE_SESSION_ID"]
|
|
34
46
|
end
|
|
35
47
|
|
|
48
|
+
# @return [String, nil] path to current transcript from CLAUDE_TRANSCRIPT_PATH
|
|
36
49
|
def transcript_path
|
|
37
50
|
env["CLAUDE_TRANSCRIPT_PATH"]
|
|
38
51
|
end
|
|
@@ -66,13 +66,17 @@ This document defines the schema for extracted knowledge from transcripts.
|
|
|
66
66
|
- **conflict**: `{kind: "conflict", value: true}` - indicates contradictory information detected
|
|
67
67
|
- **time_boundary**: `{kind: "time_boundary", value: "2024-01-15"}` - temporal boundary marker
|
|
68
68
|
|
|
69
|
-
## Predicate Types
|
|
69
|
+
## Predicate Types
|
|
70
|
+
|
|
71
|
+
Canonical vocabulary defined in `lib/claude_memory/resolve/predicate_policy.rb`.
|
|
70
72
|
|
|
71
73
|
| Predicate | Cardinality | Exclusive |
|
|
72
74
|
|-----------|-------------|-----------|
|
|
73
75
|
| convention | multi | no |
|
|
74
|
-
| decision | multi
|
|
75
|
-
|
|
|
76
|
+
| decision | multi | no |
|
|
77
|
+
| architecture | multi | no |
|
|
78
|
+
| uses_framework | multi | no |
|
|
79
|
+
| uses_language | multi | no |
|
|
76
80
|
| uses_database | single | yes |
|
|
77
|
-
| uses_framework | single | yes |
|
|
78
81
|
| deployment_platform | single | yes |
|
|
82
|
+
| auth_method | single | yes |
|
|
@@ -73,6 +73,8 @@ module ClaudeMemory
|
|
|
73
73
|
facts << build_fact("uses_framework", entity[:name], text, scope_hint)
|
|
74
74
|
when "platform"
|
|
75
75
|
facts << build_fact("deployment_platform", entity[:name], text, scope_hint)
|
|
76
|
+
when "language"
|
|
77
|
+
facts << build_fact("uses_language", entity[:name], text, scope_hint)
|
|
76
78
|
end
|
|
77
79
|
end
|
|
78
80
|
|
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module ClaudeMemory
|
|
4
4
|
module Domain
|
|
5
|
-
# Domain model representing an entity (database, framework, person, etc.)
|
|
5
|
+
# Domain model representing an entity (database, framework, person, etc.).
|
|
6
|
+
# Instances are immutable (frozen).
|
|
6
7
|
class Entity
|
|
7
8
|
attr_reader :id, :type, :canonical_name, :slug, :created_at
|
|
8
9
|
|
|
10
|
+
# @param attributes [Hash] entity attributes
|
|
11
|
+
# @option attributes [Integer] :id database primary key
|
|
12
|
+
# @option attributes [String] :type entity category (required, e.g. "database", "framework", "person")
|
|
13
|
+
# @option attributes [String] :canonical_name display name (required)
|
|
14
|
+
# @option attributes [String] :slug URL-safe identifier (required)
|
|
15
|
+
# @option attributes [String] :created_at ISO 8601 creation timestamp
|
|
16
|
+
# @raise [ArgumentError] if type, canonical_name, or slug is blank
|
|
9
17
|
def initialize(attributes)
|
|
10
18
|
@id = attributes[:id]
|
|
11
19
|
@type = attributes[:type]
|
|
@@ -17,18 +25,22 @@ module ClaudeMemory
|
|
|
17
25
|
freeze
|
|
18
26
|
end
|
|
19
27
|
|
|
28
|
+
# @return [Boolean] true when type is "database"
|
|
20
29
|
def database?
|
|
21
30
|
type == "database"
|
|
22
31
|
end
|
|
23
32
|
|
|
33
|
+
# @return [Boolean] true when type is "framework"
|
|
24
34
|
def framework?
|
|
25
35
|
type == "framework"
|
|
26
36
|
end
|
|
27
37
|
|
|
38
|
+
# @return [Boolean] true when type is "person"
|
|
28
39
|
def person?
|
|
29
40
|
type == "person"
|
|
30
41
|
end
|
|
31
42
|
|
|
43
|
+
# @return [Hash] all attributes as a plain hash
|
|
32
44
|
def to_h
|
|
33
45
|
{
|
|
34
46
|
id: id,
|
|
@@ -2,13 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module ClaudeMemory
|
|
4
4
|
module Domain
|
|
5
|
-
# Domain model representing a fact in the memory system
|
|
6
|
-
# Encapsulates business logic and validation
|
|
5
|
+
# Domain model representing a fact in the memory system.
|
|
6
|
+
# Encapsulates business logic and validation. Instances are immutable (frozen).
|
|
7
7
|
class Fact
|
|
8
8
|
attr_reader :id, :docid, :subject_name, :predicate, :object_literal,
|
|
9
9
|
:status, :confidence, :scope, :project_path,
|
|
10
10
|
:valid_from, :valid_to, :created_at
|
|
11
11
|
|
|
12
|
+
# @param attributes [Hash] fact attributes
|
|
13
|
+
# @option attributes [Integer] :id database primary key
|
|
14
|
+
# @option attributes [Integer] :docid FTS document id
|
|
15
|
+
# @option attributes [String] :subject_name entity name of the subject
|
|
16
|
+
# @option attributes [String] :predicate relationship type (required)
|
|
17
|
+
# @option attributes [String] :object_literal literal value (required)
|
|
18
|
+
# @option attributes [String] :status one of "active", "superseded", "rejected", "disputed"
|
|
19
|
+
# @option attributes [Float] :confidence score between 0 and 1 (default: 1.0)
|
|
20
|
+
# @option attributes [String] :scope "project" or "global" (default: "project")
|
|
21
|
+
# @option attributes [String] :project_path path for project-scoped facts
|
|
22
|
+
# @option attributes [String] :valid_from ISO 8601 start of validity
|
|
23
|
+
# @option attributes [String] :valid_to ISO 8601 end of validity (nil if current)
|
|
24
|
+
# @option attributes [String] :created_at ISO 8601 creation timestamp
|
|
25
|
+
# @raise [ArgumentError] if predicate, object_literal, or confidence is invalid
|
|
12
26
|
def initialize(attributes)
|
|
13
27
|
@id = attributes[:id]
|
|
14
28
|
@docid = attributes[:docid]
|
|
@@ -27,22 +41,32 @@ module ClaudeMemory
|
|
|
27
41
|
freeze
|
|
28
42
|
end
|
|
29
43
|
|
|
44
|
+
# @return [Boolean] true when status is "active"
|
|
30
45
|
def active?
|
|
31
46
|
status == "active"
|
|
32
47
|
end
|
|
33
48
|
|
|
49
|
+
# @return [Boolean] true when status is "superseded"
|
|
34
50
|
def superseded?
|
|
35
51
|
status == "superseded"
|
|
36
52
|
end
|
|
37
53
|
|
|
54
|
+
# @return [Boolean] true when status is "rejected"
|
|
55
|
+
def rejected?
|
|
56
|
+
status == "rejected"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Boolean] true when scope is "global"
|
|
38
60
|
def global?
|
|
39
61
|
scope == "global"
|
|
40
62
|
end
|
|
41
63
|
|
|
64
|
+
# @return [Boolean] true when scope is "project"
|
|
42
65
|
def project?
|
|
43
66
|
scope == "project"
|
|
44
67
|
end
|
|
45
68
|
|
|
69
|
+
# @return [Hash] all attributes as a plain hash
|
|
46
70
|
def to_h
|
|
47
71
|
{
|
|
48
72
|
id: id,
|
|
@@ -22,19 +22,20 @@ module ClaudeMemory
|
|
|
22
22
|
DEFAULT_API_URL = "https://api.openai.com/v1/embeddings"
|
|
23
23
|
DEFAULT_MODEL = "text-embedding-3-small"
|
|
24
24
|
|
|
25
|
-
def initialize(env: ENV)
|
|
25
|
+
def initialize(model: nil, env: ENV)
|
|
26
26
|
@api_key = env["CLAUDE_MEMORY_EMBEDDING_API_KEY"] || env["OPENAI_API_KEY"]
|
|
27
27
|
@api_url = env["CLAUDE_MEMORY_EMBEDDING_API_URL"] || DEFAULT_API_URL
|
|
28
|
-
@model = env["CLAUDE_MEMORY_EMBEDDING_MODEL"] || DEFAULT_MODEL
|
|
28
|
+
@model = model || env["CLAUDE_MEMORY_EMBEDDING_MODEL"] || DEFAULT_MODEL
|
|
29
|
+
@known_dimensions = ModelRegistry.dimensions_for(@model)
|
|
29
30
|
|
|
30
31
|
raise ArgumentError, "Set CLAUDE_MEMORY_EMBEDDING_API_KEY or OPENAI_API_KEY" unless @api_key
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def name = "api"
|
|
34
35
|
|
|
35
|
-
# Dimensions
|
|
36
|
+
# Dimensions resolved from registry if known, otherwise lazy from first API response.
|
|
36
37
|
def dimensions
|
|
37
|
-
@dimensions ||= fetch_dimensions
|
|
38
|
+
@dimensions ||= @known_dimensions || fetch_dimensions
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
# Generate embedding for a query text.
|