claude_memory 0.1.0 → 0.2.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.aLCUZd +0 -0
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/rules/claude_memory.generated.md +7 -1
- data/.claude/settings.json +0 -4
- data/.claude/settings.local.json +4 -1
- data/.claude-plugin/plugin.json +1 -1
- data/.claude.json +11 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +62 -11
- data/CLAUDE.md +87 -24
- data/README.md +76 -159
- data/docs/EXAMPLES.md +436 -0
- data/docs/RELEASE_NOTES_v0.2.0.md +179 -0
- data/docs/RUBY_COMMUNITY_POST_v0.2.0.md +582 -0
- data/docs/SOCIAL_MEDIA_v0.2.0.md +420 -0
- data/docs/architecture.md +360 -0
- data/docs/expert_review.md +1718 -0
- data/docs/feature_adoption_plan.md +1241 -0
- data/docs/feature_adoption_plan_revised.md +2374 -0
- data/docs/improvements.md +1325 -0
- data/docs/quality_review.md +1544 -0
- data/docs/review_summary.md +480 -0
- data/lefthook.yml +10 -0
- data/lib/claude_memory/cli.rb +16 -844
- data/lib/claude_memory/commands/base_command.rb +95 -0
- data/lib/claude_memory/commands/changes_command.rb +39 -0
- data/lib/claude_memory/commands/conflicts_command.rb +37 -0
- data/lib/claude_memory/commands/db_init_command.rb +40 -0
- data/lib/claude_memory/commands/doctor_command.rb +147 -0
- data/lib/claude_memory/commands/explain_command.rb +65 -0
- data/lib/claude_memory/commands/help_command.rb +37 -0
- data/lib/claude_memory/commands/hook_command.rb +106 -0
- data/lib/claude_memory/commands/ingest_command.rb +47 -0
- data/lib/claude_memory/commands/init_command.rb +218 -0
- data/lib/claude_memory/commands/promote_command.rb +30 -0
- data/lib/claude_memory/commands/publish_command.rb +36 -0
- data/lib/claude_memory/commands/recall_command.rb +61 -0
- data/lib/claude_memory/commands/registry.rb +55 -0
- data/lib/claude_memory/commands/search_command.rb +43 -0
- data/lib/claude_memory/commands/serve_mcp_command.rb +16 -0
- data/lib/claude_memory/commands/sweep_command.rb +36 -0
- data/lib/claude_memory/commands/version_command.rb +13 -0
- data/lib/claude_memory/configuration.rb +38 -0
- data/lib/claude_memory/core/fact_id.rb +41 -0
- data/lib/claude_memory/core/null_explanation.rb +47 -0
- data/lib/claude_memory/core/null_fact.rb +30 -0
- data/lib/claude_memory/core/result.rb +143 -0
- data/lib/claude_memory/core/session_id.rb +37 -0
- data/lib/claude_memory/core/token_estimator.rb +33 -0
- data/lib/claude_memory/core/transcript_path.rb +37 -0
- data/lib/claude_memory/domain/conflict.rb +51 -0
- data/lib/claude_memory/domain/entity.rb +51 -0
- data/lib/claude_memory/domain/fact.rb +70 -0
- data/lib/claude_memory/domain/provenance.rb +48 -0
- data/lib/claude_memory/hook/exit_codes.rb +18 -0
- data/lib/claude_memory/hook/handler.rb +7 -2
- data/lib/claude_memory/index/index_query.rb +89 -0
- data/lib/claude_memory/index/index_query_logic.rb +41 -0
- data/lib/claude_memory/index/query_options.rb +67 -0
- data/lib/claude_memory/infrastructure/file_system.rb +29 -0
- data/lib/claude_memory/infrastructure/in_memory_file_system.rb +32 -0
- data/lib/claude_memory/ingest/content_sanitizer.rb +42 -0
- data/lib/claude_memory/ingest/ingester.rb +3 -0
- data/lib/claude_memory/ingest/privacy_tag.rb +48 -0
- data/lib/claude_memory/mcp/tools.rb +174 -1
- data/lib/claude_memory/publish.rb +29 -20
- data/lib/claude_memory/recall.rb +164 -16
- data/lib/claude_memory/resolve/resolver.rb +41 -37
- data/lib/claude_memory/shortcuts.rb +56 -0
- data/lib/claude_memory/store/store_manager.rb +35 -32
- data/lib/claude_memory/templates/hooks.example.json +0 -4
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +59 -21
- metadata +55 -1
|
@@ -0,0 +1,1241 @@
|
|
|
1
|
+
# ClaudeMemory Feature Adoption Plan
|
|
2
|
+
## Based on claude-mem Analysis
|
|
3
|
+
|
|
4
|
+
## Executive Summary
|
|
5
|
+
|
|
6
|
+
This plan incrementally adopts proven patterns from claude-mem (a production-grade memory system with 6+ months of real-world usage) while preserving ClaudeMemory's unique advantages (dual-database architecture, fact-based knowledge graph, truth maintenance system).
|
|
7
|
+
|
|
8
|
+
**Timeline:** 4-6 weeks across 3 phases
|
|
9
|
+
**Approach:** TDD, backward compatible, high-impact features first
|
|
10
|
+
**Risk Level:** Low
|
|
11
|
+
|
|
12
|
+
### Features Already Complete ✅
|
|
13
|
+
- **Slim Orchestrator Pattern** - CLI decomposed into 16 command classes (Phase 2 of previous refactoring)
|
|
14
|
+
- **Domain-Driven Design** - Rich domain models with business logic
|
|
15
|
+
- **Transaction Safety** - Multi-step operations wrapped in transactions
|
|
16
|
+
- **FileSystem Abstraction** - In-memory testing without disk I/O
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Phase 1: Privacy & Token Economics (Weeks 1-2)
|
|
21
|
+
### High-impact features with security and observability benefits
|
|
22
|
+
|
|
23
|
+
### 1.1 Privacy Tag System (Days 1-3)
|
|
24
|
+
|
|
25
|
+
**Priority:** HIGH - Security and user trust
|
|
26
|
+
|
|
27
|
+
**Goal:** Allow users to exclude sensitive content from storage using `<private>` tags
|
|
28
|
+
|
|
29
|
+
#### Implementation Steps
|
|
30
|
+
|
|
31
|
+
**1. Create Content Sanitizer (Day 1)**
|
|
32
|
+
|
|
33
|
+
**New file:** `lib/claude_memory/ingest/content_sanitizer.rb`
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# frozen_string_literal: true
|
|
37
|
+
|
|
38
|
+
module ClaudeMemory
|
|
39
|
+
module Ingest
|
|
40
|
+
class ContentSanitizer
|
|
41
|
+
SYSTEM_TAGS = ["claude-memory-context"].freeze
|
|
42
|
+
USER_TAGS = ["private", "no-memory", "secret"].freeze
|
|
43
|
+
MAX_TAG_COUNT = 100 # ReDoS protection
|
|
44
|
+
|
|
45
|
+
def self.strip_tags(text)
|
|
46
|
+
validate_tag_count!(text)
|
|
47
|
+
|
|
48
|
+
all_tags = SYSTEM_TAGS + USER_TAGS
|
|
49
|
+
all_tags.each do |tag|
|
|
50
|
+
# Match opening and closing tags, including multiline content
|
|
51
|
+
text = text.gsub(/<#{Regexp.escape(tag)}>.*?<\/#{Regexp.escape(tag)}>/m, "")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
text
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.validate_tag_count!(text)
|
|
58
|
+
all_tags = SYSTEM_TAGS + USER_TAGS
|
|
59
|
+
pattern = /<(?:#{all_tags.join("|")})>/
|
|
60
|
+
count = text.scan(pattern).size
|
|
61
|
+
|
|
62
|
+
raise Error, "Too many privacy tags (#{count}), possible ReDoS attack" if count > MAX_TAG_COUNT
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Tests:** `spec/claude_memory/ingest/content_sanitizer_spec.rb`
|
|
70
|
+
```ruby
|
|
71
|
+
RSpec.describe ClaudeMemory::Ingest::ContentSanitizer do
|
|
72
|
+
describe ".strip_tags" do
|
|
73
|
+
it "strips <private> tags and content" do
|
|
74
|
+
text = "Public <private>Secret</private> Public"
|
|
75
|
+
expect(described_class.strip_tags(text)).to eq("Public Public")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "strips multiple tag types" do
|
|
79
|
+
text = "A <private>X</private> B <no-memory>Y</no-memory> C"
|
|
80
|
+
expect(described_class.strip_tags(text)).to eq("A B C")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "handles nested tags" do
|
|
84
|
+
text = "Public <private>Outer <private>Inner</private></private> End"
|
|
85
|
+
expect(described_class.strip_tags(text)).to eq("Public End")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "preserves multiline content structure" do
|
|
89
|
+
text = "Line 1\n<private>Line 2\nLine 3</private>\nLine 4"
|
|
90
|
+
result = described_class.strip_tags(text)
|
|
91
|
+
expect(result).to eq("Line 1\n\nLine 4")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "raises error on excessive tags (ReDoS protection)" do
|
|
95
|
+
text = "<private>" * 101
|
|
96
|
+
expect { described_class.strip_tags(text) }.to raise_error(ClaudeMemory::Error, /Too many privacy tags/)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "strips claude-memory-context system tags" do
|
|
100
|
+
text = "Before <claude-memory-context>Context</claude-memory-context> After"
|
|
101
|
+
expect(described_class.strip_tags(text)).to eq("Before After")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe ".validate_tag_count!" do
|
|
106
|
+
it "accepts reasonable tag counts" do
|
|
107
|
+
text = "<private>x</private>" * 50
|
|
108
|
+
expect { described_class.validate_tag_count!(text) }.not_to raise_error
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "rejects excessive tag counts" do
|
|
112
|
+
text = "<private>x</private>" * 101
|
|
113
|
+
expect { described_class.validate_tag_count!(text) }.to raise_error(ClaudeMemory::Error)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Commit:** "Add ContentSanitizer for privacy tag stripping with ReDoS protection"
|
|
120
|
+
|
|
121
|
+
**2. Integrate into Ingester (Day 2)**
|
|
122
|
+
|
|
123
|
+
**Modify:** `lib/claude_memory/ingest/ingester.rb` (after line 22)
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
def ingest(source:, session_id:, transcript_path:, project_path: nil)
|
|
127
|
+
current_offset = @store.get_delta_cursor(session_id, transcript_path) || 0
|
|
128
|
+
delta, new_offset = TranscriptReader.read_delta(transcript_path, current_offset)
|
|
129
|
+
|
|
130
|
+
# NEW: Strip privacy tags before processing
|
|
131
|
+
delta = ContentSanitizer.strip_tags(delta)
|
|
132
|
+
|
|
133
|
+
return {status: :empty, message: "No content after cursor #{current_offset}"} if delta.empty?
|
|
134
|
+
|
|
135
|
+
# ... rest of method unchanged
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Tests:** Add to `spec/claude_memory/ingest/ingester_spec.rb`
|
|
140
|
+
```ruby
|
|
141
|
+
it "strips privacy tags from ingested content" do
|
|
142
|
+
File.write(transcript_path, "Public <private>Secret API key</private> Public")
|
|
143
|
+
|
|
144
|
+
ingester.ingest(
|
|
145
|
+
source: "test",
|
|
146
|
+
session_id: "sess-123",
|
|
147
|
+
transcript_path: transcript_path
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Verify stored content is sanitized
|
|
151
|
+
item = store.content_items.first
|
|
152
|
+
expect(item[:raw_text]).to eq("Public Public")
|
|
153
|
+
expect(item[:raw_text]).not_to include("Secret API key")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "strips claude-memory-context tags" do
|
|
157
|
+
File.write(transcript_path, "New <claude-memory-context>Old context</claude-memory-context> Content")
|
|
158
|
+
|
|
159
|
+
ingester.ingest(
|
|
160
|
+
source: "test",
|
|
161
|
+
session_id: "sess-123",
|
|
162
|
+
transcript_path: transcript_path
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
item = store.content_items.first
|
|
166
|
+
expect(item[:raw_text]).to eq("New Content")
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Commit:** "Integrate ContentSanitizer into Ingester"
|
|
171
|
+
|
|
172
|
+
**3. Update Documentation (Day 3)**
|
|
173
|
+
|
|
174
|
+
**Modify:** `README.md` - Add "Privacy Control" section after "Usage Examples"
|
|
175
|
+
|
|
176
|
+
```markdown
|
|
177
|
+
## Privacy Control
|
|
178
|
+
|
|
179
|
+
ClaudeMemory respects user privacy through content exclusion tags. Wrap sensitive information in `<private>` tags to prevent storage:
|
|
180
|
+
|
|
181
|
+
### Example
|
|
182
|
+
|
|
183
|
+
\`\`\`
|
|
184
|
+
API Configuration:
|
|
185
|
+
- Endpoint: https://api.example.com
|
|
186
|
+
- API Key: <private>sk-abc123def456789</private>
|
|
187
|
+
- Rate Limit: 1000/hour
|
|
188
|
+
\`\`\`
|
|
189
|
+
|
|
190
|
+
The API key will be stripped before storage, while other information is preserved.
|
|
191
|
+
|
|
192
|
+
### Supported Tags
|
|
193
|
+
|
|
194
|
+
- `<private>...</private>` - User-controlled privacy (recommended)
|
|
195
|
+
- `<no-memory>...</no-memory>` - Alternative privacy tag
|
|
196
|
+
- `<secret>...</secret>` - Alternative privacy tag
|
|
197
|
+
|
|
198
|
+
### System Tags
|
|
199
|
+
|
|
200
|
+
- `<claude-memory-context>...</claude-memory-context>` - Auto-stripped to prevent recursive storage of published memory
|
|
201
|
+
|
|
202
|
+
### Security Notes
|
|
203
|
+
|
|
204
|
+
- Tags are stripped at ingestion time (edge processing)
|
|
205
|
+
- Protected against ReDoS attacks (max 100 tags per ingestion)
|
|
206
|
+
- Content within tags is never stored or indexed
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Modify:** `CLAUDE.md` - Add to "Hook Integration" section
|
|
210
|
+
|
|
211
|
+
```markdown
|
|
212
|
+
### Privacy Tag Handling
|
|
213
|
+
|
|
214
|
+
ClaudeMemory automatically strips privacy tags during ingestion:
|
|
215
|
+
|
|
216
|
+
\`\`\`ruby
|
|
217
|
+
# User input:
|
|
218
|
+
"Database: postgresql, Password: <private>secret123</private>"
|
|
219
|
+
|
|
220
|
+
# Stored content:
|
|
221
|
+
"Database: postgresql, Password: "
|
|
222
|
+
\`\`\`
|
|
223
|
+
|
|
224
|
+
This happens at the hook layer before content reaches the database. Supported tags:
|
|
225
|
+
- `<private>` - User privacy control
|
|
226
|
+
- `<no-memory>` - Alternative syntax
|
|
227
|
+
- `<secret>` - Alternative syntax
|
|
228
|
+
- `<claude-memory-context>` - System tag (prevents recursive context injection)
|
|
229
|
+
|
|
230
|
+
ReDoS protection: Max 100 tags per ingestion.
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Commit:** "Document privacy tag system in README and CLAUDE.md"
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### 1.2 Progressive Disclosure Pattern (Days 4-7)
|
|
238
|
+
|
|
239
|
+
**Priority:** HIGH - Token efficiency and cost reduction
|
|
240
|
+
|
|
241
|
+
**Goal:** Enable 2-tier retrieval (lightweight index → detailed fetch) to reduce context waste
|
|
242
|
+
|
|
243
|
+
#### Implementation Steps
|
|
244
|
+
|
|
245
|
+
**1. Add Token Estimation (Day 4)**
|
|
246
|
+
|
|
247
|
+
**New file:** `lib/claude_memory/core/token_estimator.rb`
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# frozen_string_literal: true
|
|
251
|
+
|
|
252
|
+
module ClaudeMemory
|
|
253
|
+
module Core
|
|
254
|
+
class TokenEstimator
|
|
255
|
+
# Approximation: ~4 characters per token for English text
|
|
256
|
+
# More accurate for Claude's tokenizer than simple word count
|
|
257
|
+
CHARS_PER_TOKEN = 4.0
|
|
258
|
+
|
|
259
|
+
def self.estimate(text)
|
|
260
|
+
return 0 if text.nil? || text.empty?
|
|
261
|
+
|
|
262
|
+
# Remove extra whitespace and count characters
|
|
263
|
+
normalized = text.strip.gsub(/\s+/, " ")
|
|
264
|
+
chars = normalized.length
|
|
265
|
+
|
|
266
|
+
# Return ceiling to avoid underestimation
|
|
267
|
+
(chars / CHARS_PER_TOKEN).ceil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def self.estimate_fact(fact)
|
|
271
|
+
# Estimate tokens for a fact record
|
|
272
|
+
text = [
|
|
273
|
+
fact[:subject_name],
|
|
274
|
+
fact[:predicate],
|
|
275
|
+
fact[:object_literal]
|
|
276
|
+
].compact.join(" ")
|
|
277
|
+
|
|
278
|
+
estimate(text)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Tests:** `spec/claude_memory/core/token_estimator_spec.rb`
|
|
286
|
+
```ruby
|
|
287
|
+
RSpec.describe ClaudeMemory::Core::TokenEstimator do
|
|
288
|
+
describe ".estimate" do
|
|
289
|
+
it "estimates tokens for short text" do
|
|
290
|
+
expect(described_class.estimate("hello world")).to eq(3)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it "estimates tokens for longer text" do
|
|
294
|
+
text = "The quick brown fox jumps over the lazy dog"
|
|
295
|
+
expect(described_class.estimate(text)).to be_between(10, 12)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
it "handles empty text" do
|
|
299
|
+
expect(described_class.estimate("")).to eq(0)
|
|
300
|
+
expect(described_class.estimate(nil)).to eq(0)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it "normalizes whitespace" do
|
|
304
|
+
expect(described_class.estimate("a b c")).to eq(described_class.estimate("a b c"))
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
describe ".estimate_fact" do
|
|
309
|
+
it "estimates tokens for fact" do
|
|
310
|
+
fact = {
|
|
311
|
+
subject_name: "project",
|
|
312
|
+
predicate: "uses_database",
|
|
313
|
+
object_literal: "PostgreSQL"
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
tokens = described_class.estimate_fact(fact)
|
|
317
|
+
expect(tokens).to be > 0
|
|
318
|
+
expect(tokens).to be < 10
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Commit:** "Add TokenEstimator for progressive disclosure"
|
|
325
|
+
|
|
326
|
+
**2. Add Index Format to Recall (Day 5)**
|
|
327
|
+
|
|
328
|
+
**Modify:** `lib/claude_memory/recall.rb` - Add new method after line 28
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
# Returns lightweight index format (no full content)
|
|
332
|
+
def query_index(query_text, limit: 20, scope: SCOPE_ALL)
|
|
333
|
+
if @legacy_mode
|
|
334
|
+
query_index_legacy(query_text, limit: limit, scope: scope)
|
|
335
|
+
else
|
|
336
|
+
query_index_dual(query_text, limit: limit, scope: scope)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
private
|
|
341
|
+
|
|
342
|
+
def query_index_dual(query_text, limit:, scope:)
|
|
343
|
+
results = []
|
|
344
|
+
|
|
345
|
+
if scope == SCOPE_ALL || scope == SCOPE_PROJECT
|
|
346
|
+
@manager.ensure_project! if @manager.project_exists?
|
|
347
|
+
if @manager.project_store
|
|
348
|
+
project_results = query_index_single_store(@manager.project_store, query_text, limit: limit, source: :project)
|
|
349
|
+
results.concat(project_results)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
if scope == SCOPE_ALL || scope == SCOPE_GLOBAL
|
|
354
|
+
@manager.ensure_global! if @manager.global_exists?
|
|
355
|
+
if @manager.global_store
|
|
356
|
+
global_results = query_index_single_store(@manager.global_store, query_text, limit: limit, source: :global)
|
|
357
|
+
results.concat(global_results)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
dedupe_and_sort(results, limit)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def query_index_single_store(store, query_text, limit:, source:)
|
|
365
|
+
fts = Index::LexicalFTS.new(store)
|
|
366
|
+
content_ids = fts.search(query_text, limit: limit * 3)
|
|
367
|
+
return [] if content_ids.empty?
|
|
368
|
+
|
|
369
|
+
# Collect fact IDs (same as query_single_store)
|
|
370
|
+
seen_fact_ids = Set.new
|
|
371
|
+
ordered_fact_ids = []
|
|
372
|
+
|
|
373
|
+
content_ids.each do |content_id|
|
|
374
|
+
provenance_records = store.provenance
|
|
375
|
+
.select(:fact_id)
|
|
376
|
+
.where(content_item_id: content_id)
|
|
377
|
+
.all
|
|
378
|
+
|
|
379
|
+
provenance_records.each do |prov|
|
|
380
|
+
fact_id = prov[:fact_id]
|
|
381
|
+
next if seen_fact_ids.include?(fact_id)
|
|
382
|
+
|
|
383
|
+
seen_fact_ids.add(fact_id)
|
|
384
|
+
ordered_fact_ids << fact_id
|
|
385
|
+
break if ordered_fact_ids.size >= limit
|
|
386
|
+
end
|
|
387
|
+
break if ordered_fact_ids.size >= limit
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
return [] if ordered_fact_ids.empty?
|
|
391
|
+
|
|
392
|
+
# Batch query facts but return INDEX format (lightweight)
|
|
393
|
+
store.facts
|
|
394
|
+
.left_join(:entities, id: :subject_entity_id)
|
|
395
|
+
.select(
|
|
396
|
+
Sequel[:facts][:id],
|
|
397
|
+
Sequel[:facts][:predicate],
|
|
398
|
+
Sequel[:facts][:object_literal],
|
|
399
|
+
Sequel[:facts][:status],
|
|
400
|
+
Sequel[:entities][:canonical_name].as(:subject_name),
|
|
401
|
+
Sequel[:facts][:scope],
|
|
402
|
+
Sequel[:facts][:confidence]
|
|
403
|
+
)
|
|
404
|
+
.where(Sequel[:facts][:id] => ordered_fact_ids)
|
|
405
|
+
.all
|
|
406
|
+
.map do |fact|
|
|
407
|
+
{
|
|
408
|
+
id: fact[:id],
|
|
409
|
+
subject: fact[:subject_name],
|
|
410
|
+
predicate: fact[:predicate],
|
|
411
|
+
object_preview: fact[:object_literal]&.slice(0, 50), # Truncate for preview
|
|
412
|
+
status: fact[:status],
|
|
413
|
+
scope: fact[:scope],
|
|
414
|
+
confidence: fact[:confidence],
|
|
415
|
+
token_estimate: Core::TokenEstimator.estimate_fact(fact),
|
|
416
|
+
source: source
|
|
417
|
+
}
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Tests:** Add to `spec/claude_memory/recall_spec.rb`
|
|
423
|
+
```ruby
|
|
424
|
+
describe "#query_index" do
|
|
425
|
+
it "returns lightweight index format" do
|
|
426
|
+
fact_id = create_fact("uses_database", "PostgreSQL with extensive configuration")
|
|
427
|
+
|
|
428
|
+
results = recall.query_index("database", limit: 10, scope: :all)
|
|
429
|
+
|
|
430
|
+
expect(results).not_to be_empty
|
|
431
|
+
result = results.first
|
|
432
|
+
|
|
433
|
+
# Has essential fields
|
|
434
|
+
expect(result[:id]).to eq(fact_id)
|
|
435
|
+
expect(result[:predicate]).to eq("uses_database")
|
|
436
|
+
expect(result[:subject]).to be_present
|
|
437
|
+
|
|
438
|
+
# Has preview (truncated)
|
|
439
|
+
expect(result[:object_preview].length).to be <= 50
|
|
440
|
+
|
|
441
|
+
# Has token estimate
|
|
442
|
+
expect(result[:token_estimate]).to be > 0
|
|
443
|
+
|
|
444
|
+
# Does NOT have full provenance
|
|
445
|
+
expect(result).not_to have_key(:receipts)
|
|
446
|
+
expect(result).not_to have_key(:valid_from)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
it "includes token estimates" do
|
|
450
|
+
create_fact("uses_framework", "React")
|
|
451
|
+
|
|
452
|
+
results = recall.query_index("framework", limit: 10)
|
|
453
|
+
|
|
454
|
+
expect(results.first[:token_estimate]).to be_between(1, 10)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
**Commit:** "Add query_index method for progressive disclosure pattern"
|
|
460
|
+
|
|
461
|
+
**3. Add MCP Tools for Progressive Disclosure (Days 6-7)**
|
|
462
|
+
|
|
463
|
+
**Modify:** `lib/claude_memory/mcp/tools.rb`
|
|
464
|
+
|
|
465
|
+
Add to `#definitions` method (around line 150):
|
|
466
|
+
```ruby
|
|
467
|
+
{
|
|
468
|
+
name: "memory.recall_index",
|
|
469
|
+
description: "Layer 1: Search for facts and get lightweight index (IDs, previews, token counts). Use this first before fetching full details.",
|
|
470
|
+
inputSchema: {
|
|
471
|
+
type: "object",
|
|
472
|
+
properties: {
|
|
473
|
+
query: {
|
|
474
|
+
type: "string",
|
|
475
|
+
description: "Search query for fact discovery"
|
|
476
|
+
},
|
|
477
|
+
limit: {
|
|
478
|
+
type: "integer",
|
|
479
|
+
description: "Maximum results to return",
|
|
480
|
+
default: 20
|
|
481
|
+
},
|
|
482
|
+
scope: {
|
|
483
|
+
type: "string",
|
|
484
|
+
enum: ["all", "global", "project"],
|
|
485
|
+
default: "all",
|
|
486
|
+
description: "Scope: 'all' (both), 'global' (user-wide), 'project' (current only)"
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
required: ["query"]
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: "memory.recall_details",
|
|
494
|
+
description: "Layer 2: Fetch full details for specific fact IDs from the index. Use after memory.recall_index to get complete information.",
|
|
495
|
+
inputSchema: {
|
|
496
|
+
type: "object",
|
|
497
|
+
properties: {
|
|
498
|
+
fact_ids: {
|
|
499
|
+
type: "array",
|
|
500
|
+
items: {type: "integer"},
|
|
501
|
+
description: "Fact IDs from memory.recall_index"
|
|
502
|
+
},
|
|
503
|
+
scope: {
|
|
504
|
+
type: "string",
|
|
505
|
+
enum: ["project", "global"],
|
|
506
|
+
default: "project",
|
|
507
|
+
description: "Database to query"
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
required: ["fact_ids"]
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Add to `#call` method (around line 175):
|
|
516
|
+
```ruby
|
|
517
|
+
when "memory.recall_index"
|
|
518
|
+
recall_index(arguments)
|
|
519
|
+
when "memory.recall_details"
|
|
520
|
+
recall_details(arguments)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Add private methods (around line 360):
|
|
524
|
+
```ruby
|
|
525
|
+
def recall_index(args)
|
|
526
|
+
scope = args["scope"] || "all"
|
|
527
|
+
results = @recall.query_index(args["query"], limit: args["limit"] || 20, scope: scope)
|
|
528
|
+
|
|
529
|
+
total_tokens = results.sum { |r| r[:token_estimate] }
|
|
530
|
+
|
|
531
|
+
{
|
|
532
|
+
query: args["query"],
|
|
533
|
+
scope: scope,
|
|
534
|
+
result_count: results.size,
|
|
535
|
+
total_estimated_tokens: total_tokens,
|
|
536
|
+
facts: results.map do |r|
|
|
537
|
+
{
|
|
538
|
+
id: r[:id],
|
|
539
|
+
subject: r[:subject],
|
|
540
|
+
predicate: r[:predicate],
|
|
541
|
+
object_preview: r[:object_preview],
|
|
542
|
+
status: r[:status],
|
|
543
|
+
scope: r[:scope],
|
|
544
|
+
confidence: r[:confidence],
|
|
545
|
+
tokens: r[:token_estimate],
|
|
546
|
+
source: r[:source]
|
|
547
|
+
}
|
|
548
|
+
end
|
|
549
|
+
}
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def recall_details(args)
|
|
553
|
+
fact_ids = args["fact_ids"]
|
|
554
|
+
scope = args["scope"] || "project"
|
|
555
|
+
|
|
556
|
+
# Batch fetch detailed explanations
|
|
557
|
+
explanations = fact_ids.map do |fact_id|
|
|
558
|
+
explanation = @recall.explain(fact_id, scope: scope)
|
|
559
|
+
next nil if explanation.is_a?(Core::NullExplanation)
|
|
560
|
+
|
|
561
|
+
{
|
|
562
|
+
fact: {
|
|
563
|
+
id: explanation[:fact][:id],
|
|
564
|
+
subject: explanation[:fact][:subject_name],
|
|
565
|
+
predicate: explanation[:fact][:predicate],
|
|
566
|
+
object: explanation[:fact][:object_literal],
|
|
567
|
+
status: explanation[:fact][:status],
|
|
568
|
+
confidence: explanation[:fact][:confidence],
|
|
569
|
+
scope: explanation[:fact][:scope],
|
|
570
|
+
valid_from: explanation[:fact][:valid_from],
|
|
571
|
+
valid_to: explanation[:fact][:valid_to]
|
|
572
|
+
},
|
|
573
|
+
receipts: explanation[:receipts].map { |r|
|
|
574
|
+
{
|
|
575
|
+
quote: r[:quote],
|
|
576
|
+
strength: r[:strength],
|
|
577
|
+
session_id: r[:session_id],
|
|
578
|
+
occurred_at: r[:occurred_at]
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
relationships: {
|
|
582
|
+
supersedes: explanation[:supersedes],
|
|
583
|
+
superseded_by: explanation[:superseded_by],
|
|
584
|
+
conflicts: explanation[:conflicts].map { |c| {id: c[:id], status: c[:status]} }
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
end.compact
|
|
588
|
+
|
|
589
|
+
{
|
|
590
|
+
fact_count: explanations.size,
|
|
591
|
+
facts: explanations
|
|
592
|
+
}
|
|
593
|
+
end
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Tests:** Add to `spec/claude_memory/mcp/tools_spec.rb`
|
|
597
|
+
```ruby
|
|
598
|
+
describe "memory.recall_index" do
|
|
599
|
+
it "returns lightweight index" do
|
|
600
|
+
create_fact("uses_database", "PostgreSQL")
|
|
601
|
+
|
|
602
|
+
result = tools.call("memory.recall_index", {"query" => "database", "limit" => 10})
|
|
603
|
+
|
|
604
|
+
expect(result[:result_count]).to be > 0
|
|
605
|
+
expect(result[:total_estimated_tokens]).to be > 0
|
|
606
|
+
|
|
607
|
+
fact = result[:facts].first
|
|
608
|
+
expect(fact[:id]).to be_present
|
|
609
|
+
expect(fact[:object_preview].length).to be <= 50
|
|
610
|
+
expect(fact[:tokens]).to be > 0
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
describe "memory.recall_details" do
|
|
615
|
+
it "fetches full details for fact IDs" do
|
|
616
|
+
fact_id = create_fact("uses_framework", "React with hooks")
|
|
617
|
+
|
|
618
|
+
result = tools.call("memory.recall_details", {
|
|
619
|
+
"fact_ids" => [fact_id],
|
|
620
|
+
"scope" => "project"
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
expect(result[:fact_count]).to eq(1)
|
|
624
|
+
|
|
625
|
+
fact = result[:facts].first
|
|
626
|
+
expect(fact[:fact][:id]).to eq(fact_id)
|
|
627
|
+
expect(fact[:fact][:object]).to eq("React with hooks") # Full content
|
|
628
|
+
expect(fact[:receipts]).to be_an(Array)
|
|
629
|
+
expect(fact[:relationships]).to be_present
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
it "handles multiple fact IDs" do
|
|
633
|
+
id1 = create_fact("uses_database", "PostgreSQL")
|
|
634
|
+
id2 = create_fact("uses_framework", "Rails")
|
|
635
|
+
|
|
636
|
+
result = tools.call("memory.recall_details", {
|
|
637
|
+
"fact_ids" => [id1, id2]
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
expect(result[:fact_count]).to eq(2)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
**Commit:** "Add progressive disclosure MCP tools (recall_index, recall_details)"
|
|
646
|
+
|
|
647
|
+
**4. Update Documentation (Day 7)**
|
|
648
|
+
|
|
649
|
+
**Modify:** `README.md` - Update "MCP Tools" section
|
|
650
|
+
|
|
651
|
+
```markdown
|
|
652
|
+
### MCP Tools
|
|
653
|
+
|
|
654
|
+
When configured, these tools are available in Claude Code:
|
|
655
|
+
|
|
656
|
+
#### Progressive Disclosure Tools (Recommended)
|
|
657
|
+
|
|
658
|
+
- `memory.recall_index` - **Layer 1**: Search for facts, returns lightweight index (IDs, previews, token estimates)
|
|
659
|
+
- `memory.recall_details` - **Layer 2**: Fetch full details for specific fact IDs
|
|
660
|
+
|
|
661
|
+
**Workflow:**
|
|
662
|
+
\`\`\`
|
|
663
|
+
1. memory.recall_index("database")
|
|
664
|
+
→ Returns 10 facts with previews (~50 tokens)
|
|
665
|
+
|
|
666
|
+
2. User/Claude selects relevant IDs (e.g., [123, 456])
|
|
667
|
+
|
|
668
|
+
3. memory.recall_details([123, 456])
|
|
669
|
+
→ Returns complete information (~500 tokens)
|
|
670
|
+
\`\`\`
|
|
671
|
+
|
|
672
|
+
**Benefits:** 10x token reduction for initial search, user control over detail retrieval
|
|
673
|
+
|
|
674
|
+
#### Full-Content Tools (Legacy)
|
|
675
|
+
|
|
676
|
+
- `memory.recall` - Search for relevant facts (returns full details immediately)
|
|
677
|
+
- `memory.explain` - Get detailed fact provenance
|
|
678
|
+
- `memory.promote` - Promote a project fact to global memory
|
|
679
|
+
- `memory.store_extraction` - Store extracted facts from a conversation
|
|
680
|
+
- `memory.changes` - Recent fact updates
|
|
681
|
+
- `memory.conflicts` - Open contradictions
|
|
682
|
+
- `memory.sweep_now` - Run maintenance
|
|
683
|
+
- `memory.status` - System health check
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
**Modify:** `CLAUDE.md` - Update "MCP Integration" section
|
|
687
|
+
|
|
688
|
+
```markdown
|
|
689
|
+
## MCP Integration
|
|
690
|
+
|
|
691
|
+
### Progressive Disclosure Workflow
|
|
692
|
+
|
|
693
|
+
ClaudeMemory uses a 2-layer retrieval pattern for token efficiency:
|
|
694
|
+
|
|
695
|
+
**Layer 1 - Discovery (`memory.recall_index`)**
|
|
696
|
+
Returns lightweight index with:
|
|
697
|
+
- Fact IDs and previews (50 char max)
|
|
698
|
+
- Token estimates per fact
|
|
699
|
+
- Scope and confidence
|
|
700
|
+
- Total estimated cost for full retrieval
|
|
701
|
+
|
|
702
|
+
**Layer 2 - Detail (`memory.recall_details`)**
|
|
703
|
+
Returns complete information for selected IDs:
|
|
704
|
+
- Full fact content
|
|
705
|
+
- Complete provenance with quotes
|
|
706
|
+
- Relationship graph (supersession, conflicts)
|
|
707
|
+
- Temporal validity
|
|
708
|
+
|
|
709
|
+
Example usage in Claude Code:
|
|
710
|
+
|
|
711
|
+
\`\`\`
|
|
712
|
+
Claude: Let me search your memory for database configuration
|
|
713
|
+
Tool: memory.recall_index(query="database", limit=10)
|
|
714
|
+
Result: Found 5 facts (~150 tokens if retrieved)
|
|
715
|
+
|
|
716
|
+
Claude: I'll fetch details for the 2 most relevant facts
|
|
717
|
+
Tool: memory.recall_details(fact_ids=[123, 124])
|
|
718
|
+
Result: Full details for PostgreSQL configuration
|
|
719
|
+
\`\`\`
|
|
720
|
+
|
|
721
|
+
This reduces initial context by ~10x compared to fetching all details immediately.
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**Commit:** "Document progressive disclosure pattern in README and CLAUDE.md"
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
728
|
+
## Phase 2: Semantic Enhancements (Weeks 3-4)
|
|
729
|
+
### Improved query patterns and shortcuts
|
|
730
|
+
|
|
731
|
+
### 2.1 Semantic Shortcut Methods (Days 8-10)
|
|
732
|
+
|
|
733
|
+
**Priority:** MEDIUM - Developer convenience
|
|
734
|
+
|
|
735
|
+
**Goal:** Pre-configured queries for common use cases
|
|
736
|
+
|
|
737
|
+
#### Implementation Steps
|
|
738
|
+
|
|
739
|
+
**1. Add Shortcut Methods to Recall (Day 8)**
|
|
740
|
+
|
|
741
|
+
**Modify:** `lib/claude_memory/recall.rb` - Add class methods after line 55
|
|
742
|
+
|
|
743
|
+
```ruby
|
|
744
|
+
class << self
|
|
745
|
+
def recent_decisions(manager, limit: 10)
|
|
746
|
+
recall = new(manager)
|
|
747
|
+
recall.query("decision constraint rule requirement", limit: limit, scope: SCOPE_ALL)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def architecture_choices(manager, limit: 10)
|
|
751
|
+
recall = new(manager)
|
|
752
|
+
recall.query("uses framework implements architecture pattern", limit: limit, scope: SCOPE_ALL)
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
def conventions(manager, limit: 20)
|
|
756
|
+
recall = new(manager)
|
|
757
|
+
recall.query("convention style format pattern prefer", limit: limit, scope: SCOPE_GLOBAL)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def project_config(manager, limit: 10)
|
|
761
|
+
recall = new(manager)
|
|
762
|
+
recall.query("uses requires depends_on configuration", limit: limit, scope: SCOPE_PROJECT)
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
def recent_changes(manager, days: 7, limit: 20)
|
|
766
|
+
recall = new(manager)
|
|
767
|
+
since = Time.now - (days * 24 * 60 * 60)
|
|
768
|
+
recall.changes(since: since, limit: limit, scope: SCOPE_ALL)
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
**Tests:** Add to `spec/claude_memory/recall_spec.rb`
|
|
774
|
+
```ruby
|
|
775
|
+
describe ".recent_decisions" do
|
|
776
|
+
it "returns decision-related facts" do
|
|
777
|
+
create_fact("decision", "Use PostgreSQL for primary database")
|
|
778
|
+
create_fact("constraint", "API rate limit 1000/min")
|
|
779
|
+
|
|
780
|
+
results = described_class.recent_decisions(manager, limit: 10)
|
|
781
|
+
|
|
782
|
+
expect(results.size).to be >= 2
|
|
783
|
+
expect(results.map { |r| r[:fact][:predicate] }).to include("decision", "constraint")
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
describe ".conventions" do
|
|
788
|
+
it "returns only global scope conventions" do
|
|
789
|
+
create_global_fact("convention", "Use 4-space indentation")
|
|
790
|
+
create_project_fact("convention", "Project uses tabs")
|
|
791
|
+
|
|
792
|
+
results = described_class.conventions(manager, limit: 10)
|
|
793
|
+
|
|
794
|
+
# Should only return global convention
|
|
795
|
+
expect(results.size).to eq(1)
|
|
796
|
+
expect(results.first[:fact][:object_literal]).to eq("Use 4-space indentation")
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**Commit:** "Add semantic shortcut methods to Recall"
|
|
802
|
+
|
|
803
|
+
**2. Add MCP Tools for Shortcuts (Day 9)**
|
|
804
|
+
|
|
805
|
+
**Modify:** `lib/claude_memory/mcp/tools.rb`
|
|
806
|
+
|
|
807
|
+
Add definitions (around line 150):
|
|
808
|
+
```ruby
|
|
809
|
+
{
|
|
810
|
+
name: "memory.decisions",
|
|
811
|
+
description: "Quick access to architectural decisions, constraints, and rules",
|
|
812
|
+
inputSchema: {
|
|
813
|
+
type: "object",
|
|
814
|
+
properties: {
|
|
815
|
+
limit: {type: "integer", default: 10}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: "memory.conventions",
|
|
821
|
+
description: "Quick access to coding conventions and style preferences (global scope)",
|
|
822
|
+
inputSchema: {
|
|
823
|
+
type: "object",
|
|
824
|
+
properties: {
|
|
825
|
+
limit: {type: "integer", default: 20}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: "memory.architecture",
|
|
831
|
+
description: "Quick access to framework choices and architectural patterns",
|
|
832
|
+
inputSchema: {
|
|
833
|
+
type: "object",
|
|
834
|
+
properties: {
|
|
835
|
+
limit: {type: "integer", default: 10}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
Add handlers (around line 175):
|
|
842
|
+
```ruby
|
|
843
|
+
when "memory.decisions"
|
|
844
|
+
decisions(arguments)
|
|
845
|
+
when "memory.conventions"
|
|
846
|
+
conventions(arguments)
|
|
847
|
+
when "memory.architecture"
|
|
848
|
+
architecture(arguments)
|
|
849
|
+
|
|
850
|
+
# ... private methods (around line 400):
|
|
851
|
+
|
|
852
|
+
def decisions(args)
|
|
853
|
+
results = Recall.recent_decisions(@manager, limit: args["limit"] || 10)
|
|
854
|
+
format_shortcut_results(results, "decisions")
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
def conventions(args)
|
|
858
|
+
results = Recall.conventions(@manager, limit: args["limit"] || 20)
|
|
859
|
+
format_shortcut_results(results, "conventions")
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
def architecture(args)
|
|
863
|
+
results = Recall.architecture_choices(@manager, limit: args["limit"] || 10)
|
|
864
|
+
format_shortcut_results(results, "architecture")
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
def format_shortcut_results(results, category)
|
|
868
|
+
{
|
|
869
|
+
category: category,
|
|
870
|
+
count: results.size,
|
|
871
|
+
facts: results.map do |r|
|
|
872
|
+
{
|
|
873
|
+
id: r[:fact][:id],
|
|
874
|
+
subject: r[:fact][:subject_name],
|
|
875
|
+
predicate: r[:fact][:predicate],
|
|
876
|
+
object: r[:fact][:object_literal],
|
|
877
|
+
scope: r[:fact][:scope],
|
|
878
|
+
source: r[:source]
|
|
879
|
+
}
|
|
880
|
+
end
|
|
881
|
+
}
|
|
882
|
+
end
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
**Commit:** "Add semantic shortcut MCP tools (decisions, conventions, architecture)"
|
|
886
|
+
|
|
887
|
+
**3. Update Documentation (Day 10)**
|
|
888
|
+
|
|
889
|
+
**Modify:** `README.md` - Add "Semantic Shortcuts" section
|
|
890
|
+
|
|
891
|
+
```markdown
|
|
892
|
+
### Semantic Shortcuts
|
|
893
|
+
|
|
894
|
+
Quick access to common queries via MCP tools:
|
|
895
|
+
|
|
896
|
+
- `memory.decisions` - Architectural decisions, constraints, and rules
|
|
897
|
+
- `memory.conventions` - Coding conventions and style preferences (global scope)
|
|
898
|
+
- `memory.architecture` - Framework choices and architectural patterns
|
|
899
|
+
|
|
900
|
+
These shortcuts use optimized queries for specific use cases, reducing the need for manual query construction.
|
|
901
|
+
|
|
902
|
+
#### CLI Usage
|
|
903
|
+
|
|
904
|
+
\`\`\`bash
|
|
905
|
+
# Get all architectural decisions
|
|
906
|
+
claude-memory recall "decision constraint rule"
|
|
907
|
+
|
|
908
|
+
# Get global conventions only
|
|
909
|
+
claude-memory recall "convention style format" --scope global
|
|
910
|
+
|
|
911
|
+
# Get project architecture
|
|
912
|
+
claude-memory recall "uses framework architecture" --scope project
|
|
913
|
+
\`\`\`
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
**Commit:** "Document semantic shortcuts in README"
|
|
917
|
+
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
### 2.2 Exit Code Strategy for Hooks (Day 11)
|
|
921
|
+
|
|
922
|
+
**Priority:** MEDIUM - Better error handling for Claude Code integration
|
|
923
|
+
|
|
924
|
+
**Goal:** Define clear exit code contract for hook commands
|
|
925
|
+
|
|
926
|
+
#### Implementation Steps
|
|
927
|
+
|
|
928
|
+
**1. Create Exit Code Constants**
|
|
929
|
+
|
|
930
|
+
**New file:** `lib/claude_memory/hook/exit_codes.rb`
|
|
931
|
+
|
|
932
|
+
```ruby
|
|
933
|
+
# frozen_string_literal: true
|
|
934
|
+
|
|
935
|
+
module ClaudeMemory
|
|
936
|
+
module Hook
|
|
937
|
+
module ExitCodes
|
|
938
|
+
# Success or graceful shutdown
|
|
939
|
+
SUCCESS = 0
|
|
940
|
+
|
|
941
|
+
# Non-blocking error (shown to user, session continues)
|
|
942
|
+
# Example: Missing transcript file, database not initialized
|
|
943
|
+
WARNING = 1
|
|
944
|
+
|
|
945
|
+
# Blocking error (fed to Claude for processing)
|
|
946
|
+
# Example: Database corruption, schema mismatch
|
|
947
|
+
ERROR = 2
|
|
948
|
+
end
|
|
949
|
+
end
|
|
950
|
+
end
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
**Modify:** `lib/claude_memory/hook/handler.rb` - Add error classes
|
|
954
|
+
|
|
955
|
+
```ruby
|
|
956
|
+
class Handler
|
|
957
|
+
class NonBlockingError < StandardError; end
|
|
958
|
+
class BlockingError < StandardError; end
|
|
959
|
+
|
|
960
|
+
# ... existing code ...
|
|
961
|
+
|
|
962
|
+
def handle_error(error)
|
|
963
|
+
case error
|
|
964
|
+
when NonBlockingError
|
|
965
|
+
warn "Warning: #{error.message}"
|
|
966
|
+
ExitCodes::WARNING
|
|
967
|
+
when BlockingError
|
|
968
|
+
$stderr.puts "ERROR: #{error.message}"
|
|
969
|
+
ExitCodes::ERROR
|
|
970
|
+
else
|
|
971
|
+
# Unknown errors are blocking by default (safer)
|
|
972
|
+
$stderr.puts "ERROR: #{error.class}: #{error.message}"
|
|
973
|
+
ExitCodes::ERROR
|
|
974
|
+
end
|
|
975
|
+
end
|
|
976
|
+
end
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
**Modify:** `lib/claude_memory/commands/hook_command.rb` - Return proper exit codes
|
|
980
|
+
|
|
981
|
+
```ruby
|
|
982
|
+
def call(args)
|
|
983
|
+
# ... existing code ...
|
|
984
|
+
|
|
985
|
+
case subcommand
|
|
986
|
+
when "ingest"
|
|
987
|
+
result = handler.ingest(payload)
|
|
988
|
+
result[:status] == :skipped ? Hook::ExitCodes::WARNING : Hook::ExitCodes::SUCCESS
|
|
989
|
+
when "sweep"
|
|
990
|
+
handler.sweep(payload)
|
|
991
|
+
Hook::ExitCodes::SUCCESS
|
|
992
|
+
when "publish"
|
|
993
|
+
handler.publish(payload)
|
|
994
|
+
Hook::ExitCodes::SUCCESS
|
|
995
|
+
else
|
|
996
|
+
stderr.puts "Unknown hook subcommand: #{subcommand}"
|
|
997
|
+
Hook::ExitCodes::ERROR
|
|
998
|
+
end
|
|
999
|
+
rescue Handler::NonBlockingError => e
|
|
1000
|
+
handler.handle_error(e)
|
|
1001
|
+
rescue => e
|
|
1002
|
+
handler.handle_error(e)
|
|
1003
|
+
end
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
**Tests:** Add to `spec/claude_memory/commands/hook_command_spec.rb`
|
|
1007
|
+
```ruby
|
|
1008
|
+
it "returns SUCCESS exit code for successful ingest" do
|
|
1009
|
+
payload = {
|
|
1010
|
+
"subcommand" => "ingest",
|
|
1011
|
+
"session_id" => "sess-123",
|
|
1012
|
+
"transcript_path" => transcript_path
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
exit_code = command.call(["ingest"], stdin: StringIO.new(JSON.generate(payload)))
|
|
1016
|
+
expect(exit_code).to eq(Hook::ExitCodes::SUCCESS)
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
it "returns WARNING exit code for skipped ingest" do
|
|
1020
|
+
payload = {
|
|
1021
|
+
"subcommand" => "ingest",
|
|
1022
|
+
"session_id" => "sess-123",
|
|
1023
|
+
"transcript_path" => "/nonexistent/file"
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
exit_code = command.call(["ingest"], stdin: StringIO.new(JSON.generate(payload)))
|
|
1027
|
+
expect(exit_code).to eq(Hook::ExitCodes::WARNING)
|
|
1028
|
+
end
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
**Update:** `CLAUDE.md`
|
|
1032
|
+
|
|
1033
|
+
```markdown
|
|
1034
|
+
## Hook Exit Codes
|
|
1035
|
+
|
|
1036
|
+
ClaudeMemory hooks follow a standardized exit code contract:
|
|
1037
|
+
|
|
1038
|
+
- **0 (SUCCESS)**: Hook completed successfully or gracefully shut down
|
|
1039
|
+
- **1 (WARNING)**: Non-blocking error (shown to user, session continues)
|
|
1040
|
+
- Examples: Missing transcript file, database not initialized, empty delta
|
|
1041
|
+
- **2 (ERROR)**: Blocking error (fed to Claude for processing)
|
|
1042
|
+
- Examples: Database corruption, schema version mismatch, critical failures
|
|
1043
|
+
|
|
1044
|
+
This ensures predictable behavior when integrated with Claude Code hooks system.
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
**Commit:** "Add exit code strategy for hook commands"
|
|
1048
|
+
|
|
1049
|
+
---
|
|
1050
|
+
|
|
1051
|
+
## Phase 3: Future Enhancements (Optional)
|
|
1052
|
+
### Lower priority features for later consideration
|
|
1053
|
+
|
|
1054
|
+
### 3.1 Token Economics Tracking (Days 12-15, Optional)
|
|
1055
|
+
|
|
1056
|
+
**Priority:** LOW-MEDIUM - Observability
|
|
1057
|
+
|
|
1058
|
+
**Goal:** Track token usage metrics to demonstrate memory system efficiency
|
|
1059
|
+
|
|
1060
|
+
**Note:** This requires distiller integration (currently a stub). Skip if distiller not implemented.
|
|
1061
|
+
|
|
1062
|
+
#### High-Level Steps
|
|
1063
|
+
|
|
1064
|
+
1. Add `ingestion_metrics` table to schema
|
|
1065
|
+
2. Track tokens during distillation
|
|
1066
|
+
3. Add `stats` CLI command
|
|
1067
|
+
4. Add metrics footer to publish output
|
|
1068
|
+
|
|
1069
|
+
**Deferred:** Wait for distiller implementation
|
|
1070
|
+
|
|
1071
|
+
---
|
|
1072
|
+
|
|
1073
|
+
## Critical Files Reference
|
|
1074
|
+
|
|
1075
|
+
### Phase 1: Privacy & Token Economics
|
|
1076
|
+
|
|
1077
|
+
#### New Files
|
|
1078
|
+
- `lib/claude_memory/ingest/content_sanitizer.rb` - Privacy tag stripping
|
|
1079
|
+
- `lib/claude_memory/core/token_estimator.rb` - Token estimation
|
|
1080
|
+
- `spec/claude_memory/ingest/content_sanitizer_spec.rb` - Tests
|
|
1081
|
+
- `spec/claude_memory/core/token_estimator_spec.rb` - Tests
|
|
1082
|
+
|
|
1083
|
+
#### Modified Files
|
|
1084
|
+
- `lib/claude_memory/ingest/ingester.rb` - Integrate ContentSanitizer
|
|
1085
|
+
- `lib/claude_memory/recall.rb` - Add query_index method
|
|
1086
|
+
- `lib/claude_memory/mcp/tools.rb` - Add progressive disclosure tools
|
|
1087
|
+
- `README.md` - Document privacy tags and progressive disclosure
|
|
1088
|
+
- `CLAUDE.md` - Document privacy tags and MCP tools
|
|
1089
|
+
|
|
1090
|
+
### Phase 2: Semantic Enhancements
|
|
1091
|
+
|
|
1092
|
+
#### New Files
|
|
1093
|
+
- `lib/claude_memory/hook/exit_codes.rb` - Exit code constants
|
|
1094
|
+
|
|
1095
|
+
#### Modified Files
|
|
1096
|
+
- `lib/claude_memory/recall.rb` - Add shortcut class methods
|
|
1097
|
+
- `lib/claude_memory/mcp/tools.rb` - Add shortcut MCP tools
|
|
1098
|
+
- `lib/claude_memory/hook/handler.rb` - Use exit codes
|
|
1099
|
+
- `lib/claude_memory/commands/hook_command.rb` - Return exit codes
|
|
1100
|
+
- `README.md` - Document semantic shortcuts
|
|
1101
|
+
- `CLAUDE.md` - Document exit codes
|
|
1102
|
+
|
|
1103
|
+
---
|
|
1104
|
+
|
|
1105
|
+
## Testing Strategy
|
|
1106
|
+
|
|
1107
|
+
### Test-First Workflow
|
|
1108
|
+
1. Write failing test for new behavior
|
|
1109
|
+
2. Implement minimal code to pass
|
|
1110
|
+
3. Refactor while keeping tests green
|
|
1111
|
+
4. Commit with tests + implementation
|
|
1112
|
+
|
|
1113
|
+
### Coverage Goals
|
|
1114
|
+
- Maintain >80% coverage throughout
|
|
1115
|
+
- 100% coverage for ContentSanitizer (security-critical)
|
|
1116
|
+
- 100% coverage for TokenEstimator (accuracy-critical)
|
|
1117
|
+
|
|
1118
|
+
### Integration Testing
|
|
1119
|
+
- Test progressive disclosure end-to-end (recall_index → recall_details)
|
|
1120
|
+
- Test privacy tag stripping with various edge cases
|
|
1121
|
+
- Test exit codes in hook commands
|
|
1122
|
+
|
|
1123
|
+
---
|
|
1124
|
+
|
|
1125
|
+
## Success Metrics
|
|
1126
|
+
|
|
1127
|
+
### Phase 1 Metrics
|
|
1128
|
+
- ✅ Privacy tags stripped at ingestion (zero sensitive data stored)
|
|
1129
|
+
- ✅ Progressive disclosure reduces initial context by ~10x
|
|
1130
|
+
- ✅ New MCP tools: recall_index, recall_details
|
|
1131
|
+
- ✅ Token estimation accurate within 20%
|
|
1132
|
+
|
|
1133
|
+
### Phase 2 Metrics
|
|
1134
|
+
- ✅ Semantic shortcuts reduce query complexity
|
|
1135
|
+
- ✅ Exit codes standardized for hooks
|
|
1136
|
+
- ✅ 3 new shortcut MCP tools (decisions, conventions, architecture)
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1140
|
+
## Verification Plan
|
|
1141
|
+
|
|
1142
|
+
### After Phase 1
|
|
1143
|
+
|
|
1144
|
+
```bash
|
|
1145
|
+
# Test privacy tag stripping
|
|
1146
|
+
echo "Public <private>secret</private> text" > /tmp/test.txt
|
|
1147
|
+
./exe/claude-memory ingest --source test --session test-1 --transcript /tmp/test.txt --db /tmp/test.sqlite3
|
|
1148
|
+
# Verify "secret" not stored
|
|
1149
|
+
|
|
1150
|
+
# Test progressive disclosure
|
|
1151
|
+
./exe/claude-memory recall "database" --limit 5
|
|
1152
|
+
# Should see full results (no index format in CLI yet)
|
|
1153
|
+
|
|
1154
|
+
# Test MCP tools
|
|
1155
|
+
./exe/claude-memory serve-mcp
|
|
1156
|
+
# Send test requests for recall_index and recall_details
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
### After Phase 2
|
|
1160
|
+
|
|
1161
|
+
```bash
|
|
1162
|
+
# Test semantic shortcuts via MCP
|
|
1163
|
+
./exe/claude-memory serve-mcp
|
|
1164
|
+
# Test memory.decisions, memory.conventions, memory.architecture
|
|
1165
|
+
|
|
1166
|
+
# Test exit codes
|
|
1167
|
+
echo '{"subcommand":"ingest","session_id":"test"}' | ./exe/claude-memory hook ingest
|
|
1168
|
+
echo $? # Should be 1 (WARNING) for missing transcript
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
---
|
|
1172
|
+
|
|
1173
|
+
## Migration Path
|
|
1174
|
+
|
|
1175
|
+
### Week 1-2: Foundation (Phase 1)
|
|
1176
|
+
- Days 1-3: Privacy tag system (HIGH priority)
|
|
1177
|
+
- Days 4-7: Progressive disclosure (HIGH priority)
|
|
1178
|
+
|
|
1179
|
+
### Week 3-4: Enhancements (Phase 2)
|
|
1180
|
+
- Days 8-10: Semantic shortcuts (MEDIUM priority)
|
|
1181
|
+
- Day 11: Exit code strategy (MEDIUM priority)
|
|
1182
|
+
|
|
1183
|
+
### Week 5-6: Optional (Phase 3)
|
|
1184
|
+
- Days 12-15: Token economics tracking (LOW, requires distiller)
|
|
1185
|
+
|
|
1186
|
+
---
|
|
1187
|
+
|
|
1188
|
+
## What We're NOT Doing (And Why)
|
|
1189
|
+
|
|
1190
|
+
### ❌ Chroma Vector Database
|
|
1191
|
+
**Reason:** Adds Python dependency, embedding generation, sync overhead. SQLite FTS5 is sufficient.
|
|
1192
|
+
|
|
1193
|
+
### ❌ Background Worker Process
|
|
1194
|
+
**Reason:** MCP stdio transport works well. No need for HTTP server, PID files, port management.
|
|
1195
|
+
|
|
1196
|
+
### ❌ Web Viewer UI
|
|
1197
|
+
**Reason:** Significant effort (React, SSE, state management) for uncertain value. CLI is sufficient.
|
|
1198
|
+
|
|
1199
|
+
### ❌ Slim Orchestrator Pattern
|
|
1200
|
+
**Reason:** ALREADY COMPLETE! Previous refactoring extracted all 16 commands.
|
|
1201
|
+
|
|
1202
|
+
---
|
|
1203
|
+
|
|
1204
|
+
## Architecture Advantages We're Preserving
|
|
1205
|
+
|
|
1206
|
+
### ✅ Dual-Database Architecture (Global + Project)
|
|
1207
|
+
Better than claude-mem's single database with filtering.
|
|
1208
|
+
|
|
1209
|
+
### ✅ Fact-Based Knowledge Graph
|
|
1210
|
+
Structured triples enable richer queries vs. observation blobs.
|
|
1211
|
+
|
|
1212
|
+
### ✅ Truth Maintenance System
|
|
1213
|
+
Conflict resolution and supersession not present in claude-mem.
|
|
1214
|
+
|
|
1215
|
+
### ✅ Predicate Policies
|
|
1216
|
+
Single-value vs multi-value predicates prevent false conflicts.
|
|
1217
|
+
|
|
1218
|
+
### ✅ Ruby Ecosystem
|
|
1219
|
+
Simpler dependencies, easier install vs. Node.js + Python stack.
|
|
1220
|
+
|
|
1221
|
+
---
|
|
1222
|
+
|
|
1223
|
+
## Next Steps
|
|
1224
|
+
|
|
1225
|
+
1. **Review and approve this plan**
|
|
1226
|
+
2. **Create feature branch:** `feature/claude-mem-adoption`
|
|
1227
|
+
3. **Start Phase 1, Step 1.1:** Add ContentSanitizer for privacy tags
|
|
1228
|
+
4. **Commit early, commit often:** Small, focused changes
|
|
1229
|
+
5. **Review progress:** Weekly checkpoint after each phase
|
|
1230
|
+
|
|
1231
|
+
---
|
|
1232
|
+
|
|
1233
|
+
## Notes
|
|
1234
|
+
|
|
1235
|
+
- All features maintain backward compatibility
|
|
1236
|
+
- Tests are updated/added with each change
|
|
1237
|
+
- Code style follows Standard Ruby
|
|
1238
|
+
- Frozen string literals maintained throughout
|
|
1239
|
+
- Ruby 3.2+ idioms used where appropriate
|
|
1240
|
+
- Privacy tag stripping is non-reversible by design (security-first)
|
|
1241
|
+
- Progressive disclosure is optional (legacy recall tool still works)
|