@claudetools/tools 0.7.6 → 0.7.8
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.
- package/dist/handlers/tool-handlers.js +134 -1
- package/dist/helpers/api-client.d.ts +90 -0
- package/dist/helpers/api-client.js +32 -0
- package/dist/setup.js +145 -22
- package/dist/tools.js +105 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { mcpLogger } from '../logger.js';
|
|
|
6
6
|
import { getDefaultProjectId, DEFAULT_USER_ID, lastContextUsed, setLastContextUsed, API_BASE_URL } from '../helpers/config.js';
|
|
7
7
|
import { EXPERT_WORKERS, matchTaskToWorker, buildWorkerPrompt } from '../helpers/workers.js';
|
|
8
8
|
import { validateTaskDescription } from '../templates/orchestrator-prompt.js';
|
|
9
|
-
import { searchMemory, addMemory, storeFact, getContext, getSummary, getEntities, injectContext, apiRequest, listCachedDocs, getCachedDocs, cacheDocs } from '../helpers/api-client.js';
|
|
9
|
+
import { searchMemory, addMemory, storeFact, getContext, getSummary, getEntities, injectContext, apiRequest, listCachedDocs, getCachedDocs, cacheDocs, getMemoryIndex, getMemoryDetail, getDisclosureAnalytics } from '../helpers/api-client.js';
|
|
10
10
|
import { queryDependencies, analyzeImpact } from '../helpers/dependencies.js';
|
|
11
11
|
import { checkPatterns } from '../helpers/patterns.js';
|
|
12
12
|
import { formatContextForClaude } from '../helpers/formatter.js';
|
|
@@ -180,6 +180,139 @@ export function registerToolHandlers(server) {
|
|
|
180
180
|
],
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
|
+
// =========================================================================
|
|
184
|
+
// PROGRESSIVE DISCLOSURE HANDLERS (memory_index / memory_detail)
|
|
185
|
+
// =========================================================================
|
|
186
|
+
case 'memory_index': {
|
|
187
|
+
// Get lightweight index of available memories
|
|
188
|
+
const query = args?.query;
|
|
189
|
+
const limit = args?.limit || 30;
|
|
190
|
+
const includeCategories = args?.include_categories;
|
|
191
|
+
const minRelevance = args?.min_relevance || 0.3;
|
|
192
|
+
mcpLogger.searchQuery(query || '(index scan)', 'index');
|
|
193
|
+
const indexTimer = mcpLogger.startTimer();
|
|
194
|
+
const result = await getMemoryIndex(projectId, {
|
|
195
|
+
query,
|
|
196
|
+
limit,
|
|
197
|
+
include_categories: includeCategories,
|
|
198
|
+
min_relevance: minRelevance,
|
|
199
|
+
});
|
|
200
|
+
mcpLogger.toolResult(name, true, timer(), `${result.index.length} entries, ${result.metadata.critical_count} critical`);
|
|
201
|
+
// Format as compact index table
|
|
202
|
+
let output = `# Memory Index\n\n`;
|
|
203
|
+
output += `**Total Available:** ${result.metadata.total_available}\n`;
|
|
204
|
+
output += `**Returned:** ${result.metadata.returned}\n`;
|
|
205
|
+
output += `**Critical Facts:** ${result.metadata.critical_count}\n`;
|
|
206
|
+
output += `**Total Token Cost:** ~${result.metadata.total_token_cost} tokens\n`;
|
|
207
|
+
output += `**Query:** ${result.metadata.query || '(none)'}\n`;
|
|
208
|
+
output += `**Time:** ${result.metadata.retrieval_time_ms}ms\n\n`;
|
|
209
|
+
if (result.index.length === 0) {
|
|
210
|
+
output += `No memories found matching the criteria.\n`;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
output += `## Entries\n\n`;
|
|
214
|
+
output += `| # | ID | Summary | Category | Relevance | Tokens | Critical |\n`;
|
|
215
|
+
output += `|---|----|---------|---------:|---------:|---------:|:---------:|\n`;
|
|
216
|
+
result.index.forEach((entry, i) => {
|
|
217
|
+
const critical = entry.is_critical ? '🔴' : '';
|
|
218
|
+
const summary = entry.summary.length > 50 ? entry.summary.slice(0, 47) + '...' : entry.summary;
|
|
219
|
+
output += `| ${i + 1} | \`${entry.id.slice(0, 8)}\` | ${summary} | ${entry.category} | ${(entry.relevance * 100).toFixed(0)}% | ~${entry.token_cost} | ${critical} |\n`;
|
|
220
|
+
});
|
|
221
|
+
output += `\n**Usage:** Call \`memory_detail(memory_ids=["id1", "id2", ...])\` to fetch full content for specific entries.\n`;
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
content: [{ type: 'text', text: output }],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
case 'memory_detail': {
|
|
228
|
+
// Fetch full content for specific memory IDs
|
|
229
|
+
const memoryIds = args?.memory_ids;
|
|
230
|
+
if (!memoryIds || memoryIds.length === 0) {
|
|
231
|
+
return {
|
|
232
|
+
content: [{
|
|
233
|
+
type: 'text',
|
|
234
|
+
text: 'Error: memory_ids array is required. Use memory_index first to discover IDs.',
|
|
235
|
+
}],
|
|
236
|
+
isError: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const detailTimer = mcpLogger.startTimer();
|
|
240
|
+
const result = await getMemoryDetail(projectId, memoryIds);
|
|
241
|
+
mcpLogger.toolResult(name, true, timer(), `${result.metadata.found}/${result.metadata.requested} found`);
|
|
242
|
+
let output = `# Memory Details\n\n`;
|
|
243
|
+
output += `**Requested:** ${result.metadata.requested}\n`;
|
|
244
|
+
output += `**Found:** ${result.metadata.found}\n`;
|
|
245
|
+
output += `**Time:** ${result.metadata.retrieval_time_ms}ms\n\n`;
|
|
246
|
+
if (result.metadata.not_found.length > 0) {
|
|
247
|
+
output += `**Not Found:** ${result.metadata.not_found.join(', ')}\n\n`;
|
|
248
|
+
}
|
|
249
|
+
if (result.memories.length === 0) {
|
|
250
|
+
output += `No memories found for the requested IDs.\n`;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
for (const memory of result.memories) {
|
|
254
|
+
output += `---\n\n`;
|
|
255
|
+
output += `## ${memory.source_entity} → ${memory.relation_type} → ${memory.target_entity}\n\n`;
|
|
256
|
+
output += `**ID:** \`${memory.id}\`\n`;
|
|
257
|
+
output += `**Created:** ${new Date(memory.created_at).toLocaleDateString()}\n`;
|
|
258
|
+
output += `**Valid:** ${memory.valid ? '✅' : '❌'}\n\n`;
|
|
259
|
+
output += `**Fact:**\n${memory.fact}\n\n`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
content: [{ type: 'text', text: output }],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
case 'disclosure_analytics': {
|
|
267
|
+
// Get disclosure analytics for memory usage optimization
|
|
268
|
+
const days = args?.days || 30;
|
|
269
|
+
const limit = args?.limit || 20;
|
|
270
|
+
const analyticsTimer = mcpLogger.startTimer();
|
|
271
|
+
const result = await getDisclosureAnalytics(projectId, { days, limit });
|
|
272
|
+
mcpLogger.toolResult(name, true, timer(), `${result.summary.unique_memories} memories tracked`);
|
|
273
|
+
let output = `# Disclosure Analytics\n\n`;
|
|
274
|
+
output += `## Summary (Last ${result.summary.period_days} days)\n\n`;
|
|
275
|
+
output += `| Metric | Value |\n`;
|
|
276
|
+
output += `|:-------|------:|\n`;
|
|
277
|
+
output += `| Total Indexed | ${result.summary.total_indexed} |\n`;
|
|
278
|
+
output += `| Total Fetched | ${result.summary.total_fetched} |\n`;
|
|
279
|
+
output += `| Total Injected | ${result.summary.total_injected} |\n`;
|
|
280
|
+
output += `| Unique Memories | ${result.summary.unique_memories} |\n`;
|
|
281
|
+
output += `| Overall Hit Rate | ${(result.summary.overall_hit_rate * 100).toFixed(1)}% |\n\n`;
|
|
282
|
+
if (result.event_counts.length > 0) {
|
|
283
|
+
output += `## Event Counts\n\n`;
|
|
284
|
+
output += `| Event Type | Count |\n`;
|
|
285
|
+
output += `|:-----------|------:|\n`;
|
|
286
|
+
for (const ec of result.event_counts) {
|
|
287
|
+
output += `| ${ec.event_type} | ${ec.count} |\n`;
|
|
288
|
+
}
|
|
289
|
+
output += `\n`;
|
|
290
|
+
}
|
|
291
|
+
if (result.top_fetched.length > 0) {
|
|
292
|
+
output += `## Top Fetched Memories\n\n`;
|
|
293
|
+
output += `These memories are frequently accessed via memory_detail:\n\n`;
|
|
294
|
+
output += `| Memory ID | Index | Fetch | Inject | Hit Rate |\n`;
|
|
295
|
+
output += `|:----------|------:|------:|-------:|---------:|\n`;
|
|
296
|
+
for (const tf of result.top_fetched) {
|
|
297
|
+
output += `| \`${tf.memory_id.slice(0, 12)}\` | ${tf.index_count} | ${tf.detail_count} | ${tf.inject_count} | ${(tf.hit_rate * 100).toFixed(0)}% |\n`;
|
|
298
|
+
}
|
|
299
|
+
output += `\n`;
|
|
300
|
+
}
|
|
301
|
+
if (result.critical_candidates.length > 0) {
|
|
302
|
+
output += `## 🔴 Critical Candidates\n\n`;
|
|
303
|
+
output += `These memories have high hit rates and may benefit from being marked as critical:\n\n`;
|
|
304
|
+
output += `| Memory ID | Index | Fetch | Hit Rate |\n`;
|
|
305
|
+
output += `|:----------|------:|------:|---------:|\n`;
|
|
306
|
+
for (const cc of result.critical_candidates) {
|
|
307
|
+
output += `| \`${cc.memory_id.slice(0, 12)}\` | ${cc.index_count} | ${cc.detail_count} | ${(cc.hit_rate * 100).toFixed(0)}% |\n`;
|
|
308
|
+
}
|
|
309
|
+
output += `\n`;
|
|
310
|
+
output += `**Tip:** Mark frequently-fetched memories as architecture/decision facts for auto-injection.\n`;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: 'text', text: output }],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
183
316
|
case 'query_dependencies': {
|
|
184
317
|
const functionName = args?.function_name;
|
|
185
318
|
const direction = args?.direction || 'both';
|
|
@@ -75,6 +75,96 @@ export declare function listCachedDocs(_projectId?: string): Promise<DocsCacheLi
|
|
|
75
75
|
*/
|
|
76
76
|
export declare function getCachedDocs(_projectId: string, // Unused - docs cache is global
|
|
77
77
|
libraryId: string, topic?: string): Promise<CachedDoc | null>;
|
|
78
|
+
export interface MemoryIndexEntry {
|
|
79
|
+
id: string;
|
|
80
|
+
summary: string;
|
|
81
|
+
category: 'architecture' | 'pattern' | 'decision' | 'preference' | 'fact';
|
|
82
|
+
relevance: number;
|
|
83
|
+
token_cost: number;
|
|
84
|
+
is_critical: boolean;
|
|
85
|
+
created_at: string;
|
|
86
|
+
}
|
|
87
|
+
export interface MemoryIndexResponse {
|
|
88
|
+
index: MemoryIndexEntry[];
|
|
89
|
+
metadata: {
|
|
90
|
+
total_available: number;
|
|
91
|
+
returned: number;
|
|
92
|
+
query?: string;
|
|
93
|
+
retrieval_time_ms: number;
|
|
94
|
+
critical_count: number;
|
|
95
|
+
total_token_cost: number;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export interface MemoryDetailEntry {
|
|
99
|
+
id: string;
|
|
100
|
+
source_entity: string;
|
|
101
|
+
target_entity: string;
|
|
102
|
+
relation_type: string;
|
|
103
|
+
fact: string;
|
|
104
|
+
context?: string;
|
|
105
|
+
created_at: string;
|
|
106
|
+
valid: boolean;
|
|
107
|
+
}
|
|
108
|
+
export interface MemoryDetailResponse {
|
|
109
|
+
memories: MemoryDetailEntry[];
|
|
110
|
+
metadata: {
|
|
111
|
+
requested: number;
|
|
112
|
+
found: number;
|
|
113
|
+
not_found: string[];
|
|
114
|
+
retrieval_time_ms: number;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get a lightweight index of available memories without full content.
|
|
119
|
+
* Returns metadata (topic, token cost, relevance) for Claude to scan and decide what to fetch.
|
|
120
|
+
*/
|
|
121
|
+
export declare function getMemoryIndex(projectId: string, options?: {
|
|
122
|
+
query?: string;
|
|
123
|
+
limit?: number;
|
|
124
|
+
include_categories?: ('architecture' | 'pattern' | 'decision' | 'preference' | 'fact')[];
|
|
125
|
+
min_relevance?: number;
|
|
126
|
+
}, userId?: string): Promise<MemoryIndexResponse>;
|
|
127
|
+
/**
|
|
128
|
+
* Fetch full content for specific memory IDs.
|
|
129
|
+
* Use after scanning the index to retrieve only needed details.
|
|
130
|
+
*/
|
|
131
|
+
export declare function getMemoryDetail(projectId: string, memoryIds: string[], userId?: string): Promise<MemoryDetailResponse>;
|
|
132
|
+
export interface DisclosureAnalytics {
|
|
133
|
+
summary: {
|
|
134
|
+
period_days: number;
|
|
135
|
+
total_indexed: number;
|
|
136
|
+
total_fetched: number;
|
|
137
|
+
total_injected: number;
|
|
138
|
+
unique_memories: number;
|
|
139
|
+
overall_hit_rate: number;
|
|
140
|
+
};
|
|
141
|
+
event_counts: Array<{
|
|
142
|
+
event_type: string;
|
|
143
|
+
count: number;
|
|
144
|
+
}>;
|
|
145
|
+
top_fetched: Array<{
|
|
146
|
+
memory_id: string;
|
|
147
|
+
index_count: number;
|
|
148
|
+
detail_count: number;
|
|
149
|
+
inject_count: number;
|
|
150
|
+
hit_rate: number;
|
|
151
|
+
last_indexed_at: string;
|
|
152
|
+
last_fetched_at: string;
|
|
153
|
+
}>;
|
|
154
|
+
critical_candidates: Array<{
|
|
155
|
+
memory_id: string;
|
|
156
|
+
index_count: number;
|
|
157
|
+
detail_count: number;
|
|
158
|
+
hit_rate: number;
|
|
159
|
+
}>;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get disclosure analytics for a project
|
|
163
|
+
*/
|
|
164
|
+
export declare function getDisclosureAnalytics(projectId: string, options?: {
|
|
165
|
+
limit?: number;
|
|
166
|
+
days?: number;
|
|
167
|
+
}, userId?: string): Promise<DisclosureAnalytics>;
|
|
78
168
|
/**
|
|
79
169
|
* Ensure documentation is cached (fetches from Context7 if needed)
|
|
80
170
|
*/
|
|
@@ -105,6 +105,38 @@ libraryId, topic) {
|
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Get a lightweight index of available memories without full content.
|
|
110
|
+
* Returns metadata (topic, token cost, relevance) for Claude to scan and decide what to fetch.
|
|
111
|
+
*/
|
|
112
|
+
export async function getMemoryIndex(projectId, options = {}, userId = DEFAULT_USER_ID) {
|
|
113
|
+
const response = await apiRequest(`/api/v1/memory/${userId}/${projectId}/index`, 'POST', options);
|
|
114
|
+
return response.data;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Fetch full content for specific memory IDs.
|
|
118
|
+
* Use after scanning the index to retrieve only needed details.
|
|
119
|
+
*/
|
|
120
|
+
export async function getMemoryDetail(projectId, memoryIds, userId = DEFAULT_USER_ID) {
|
|
121
|
+
const response = await apiRequest(`/api/v1/memory/${userId}/${projectId}/detail`, 'POST', { memory_ids: memoryIds });
|
|
122
|
+
return response.data;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get disclosure analytics for a project
|
|
126
|
+
*/
|
|
127
|
+
export async function getDisclosureAnalytics(projectId, options = {}, userId = DEFAULT_USER_ID) {
|
|
128
|
+
const params = new URLSearchParams();
|
|
129
|
+
if (options.limit)
|
|
130
|
+
params.set('limit', String(options.limit));
|
|
131
|
+
if (options.days)
|
|
132
|
+
params.set('days', String(options.days));
|
|
133
|
+
const queryString = params.toString();
|
|
134
|
+
const response = await apiRequest(`/api/v1/memory/${userId}/${projectId}/analytics${queryString ? `?${queryString}` : ''}`);
|
|
135
|
+
return response.data;
|
|
136
|
+
}
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// Documentation Cache Operations
|
|
139
|
+
// =============================================================================
|
|
108
140
|
/**
|
|
109
141
|
* Ensure documentation is cached (fetches from Context7 if needed)
|
|
110
142
|
*/
|
package/dist/setup.js
CHANGED
|
@@ -625,27 +625,43 @@ if [ -n "$PROJECT_ID" ]; then
|
|
|
625
625
|
-d "{\\"project_id\\": \\"$PROJECT_ID\\"}" 2>/dev/null)
|
|
626
626
|
|
|
627
627
|
if [ -n "$CONTEXT" ] && [ "$CONTEXT" != "null" ]; then
|
|
628
|
-
# Extract
|
|
629
|
-
|
|
630
|
-
|
|
628
|
+
# Extract progressive disclosure data from response
|
|
629
|
+
CRITICAL=$(echo "$CONTEXT" | jq -r '.data.critical_facts // []' 2>/dev/null)
|
|
630
|
+
INDEX=$(echo "$CONTEXT" | jq -r '.data.memory_index // []' 2>/dev/null)
|
|
631
|
+
INSTRUCTIONS=$(echo "$CONTEXT" | jq -r '.data.instructions // ""' 2>/dev/null)
|
|
632
|
+
CRITICAL_COUNT=$(echo "$CONTEXT" | jq -r '.data.metadata.critical_count // 0' 2>/dev/null)
|
|
633
|
+
INDEX_COUNT=$(echo "$CONTEXT" | jq -r '.data.metadata.index_count // 0' 2>/dev/null)
|
|
631
634
|
|
|
632
|
-
# Generate session context file
|
|
635
|
+
# Generate session context file with progressive disclosure format
|
|
633
636
|
cat > "$CONTEXT_FILE" << CTXEOF
|
|
634
|
-
|
|
637
|
+
🚀 **SESSION CONTEXT: $PROJECT_NAME**
|
|
635
638
|
|
|
636
639
|
**Project ID:** $PROJECT_ID
|
|
637
640
|
**Generated:** $(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
638
641
|
|
|
639
|
-
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## 📌 Project Knowledge (Auto-Injected from Memory)
|
|
645
|
+
|
|
646
|
+
**IMPORTANT:** The facts below are stored in your memory system and represent critical patterns, mistakes to avoid, and architectural decisions. READ THESE before making changes.
|
|
640
647
|
|
|
641
|
-
$(echo "$
|
|
648
|
+
$(echo "$CRITICAL" | jq -r '.[] | "### \\(.category | ascii_upcase): \\(.source) → \\(.target)\n\\(.fact)\n"' 2>/dev/null || echo "No critical facts stored yet.")
|
|
642
649
|
|
|
643
|
-
|
|
650
|
+
$(if [ "$INDEX_COUNT" != "0" ] && [ "$INDEX_COUNT" != "" ]; then
|
|
651
|
+
echo "---
|
|
644
652
|
|
|
645
|
-
|
|
653
|
+
## 📋 Memory Index ($INDEX_COUNT entries available)
|
|
654
|
+
|
|
655
|
+
**Quick reference** - use \\\`memory_detail({ memory_ids: [\"id\"] })\\\` to fetch full details.
|
|
656
|
+
|
|
657
|
+
| ID | Summary | Category |
|
|
658
|
+
|:---|:--------|:---------|"
|
|
659
|
+
echo "$INDEX" | jq -r '.[] | "| \(.id) | \(.summary) | \(.category) |"' 2>/dev/null
|
|
660
|
+
fi)
|
|
646
661
|
|
|
647
662
|
---
|
|
648
|
-
*This
|
|
663
|
+
*This context is automatically generated each session. Facts come from the claudetools memory system.*
|
|
664
|
+
*To add new facts: use memory_store_fact(entity1, relationship, entity2, context)*
|
|
649
665
|
CTXEOF
|
|
650
666
|
fi
|
|
651
667
|
else
|
|
@@ -671,9 +687,10 @@ fi
|
|
|
671
687
|
writeFileSync(sessionStartPath, sessionStartHook, { mode: 0o755 });
|
|
672
688
|
success('Installed session-start.sh hook');
|
|
673
689
|
// User prompt submit hook - injects context before each message
|
|
690
|
+
// This hook receives the user's prompt via stdin and performs semantic search
|
|
674
691
|
const userPromptHook = `#!/bin/bash
|
|
675
|
-
# ClaudeTools Context Injection Hook
|
|
676
|
-
#
|
|
692
|
+
# ClaudeTools Per-Prompt Context Injection Hook
|
|
693
|
+
# Reads user's prompt from stdin, performs semantic search, injects relevant context
|
|
677
694
|
|
|
678
695
|
# Prevent recursion
|
|
679
696
|
LOCK_FILE="/tmp/claude-prompt-hook.lock"
|
|
@@ -684,6 +701,21 @@ trap "rm -f $LOCK_FILE" EXIT
|
|
|
684
701
|
# Skip if disabled
|
|
685
702
|
if [ "$CLAUDE_DISABLE_HOOKS" = "1" ]; then exit 0; fi
|
|
686
703
|
|
|
704
|
+
# Read the user's prompt from stdin (Claude Code passes it as JSON)
|
|
705
|
+
INPUT=$(timeout 1 cat 2>/dev/null || echo "")
|
|
706
|
+
USER_QUERY=""
|
|
707
|
+
if [ -n "$INPUT" ]; then
|
|
708
|
+
# Extract the prompt text from the JSON input
|
|
709
|
+
# Claude Code sends: {"prompt": "user message...", ...}
|
|
710
|
+
USER_QUERY=$(echo "$INPUT" | jq -r '.prompt // .message // .content // empty' 2>/dev/null)
|
|
711
|
+
fi
|
|
712
|
+
|
|
713
|
+
# If no query, skip injection (nothing to search for)
|
|
714
|
+
if [ -z "$USER_QUERY" ]; then exit 0; fi
|
|
715
|
+
|
|
716
|
+
# Skip very short queries (likely not worth searching)
|
|
717
|
+
if [ \${#USER_QUERY} -lt 10 ]; then exit 0; fi
|
|
718
|
+
|
|
687
719
|
# Read config
|
|
688
720
|
CONFIG_FILE="$HOME/.claudetools/config.json"
|
|
689
721
|
if [ ! -f "$CONFIG_FILE" ]; then exit 0; fi
|
|
@@ -706,25 +738,28 @@ fi
|
|
|
706
738
|
|
|
707
739
|
# Priority 2: Fall back to projects.json cache
|
|
708
740
|
if [ -z "$PROJECT_ID" ] && [ -f "$PROJECT_FILE" ]; then
|
|
709
|
-
# Try to find project by path prefix (use variable binding for jq startswith)
|
|
710
741
|
PROJECT_ID=$(jq -r --arg cwd "$CWD" '
|
|
711
742
|
.bindings[]? | select(.local_path) |
|
|
712
743
|
. as $b | select($cwd | startswith($b.local_path)) |
|
|
713
744
|
.project_id' "$PROJECT_FILE" 2>/dev/null | head -1)
|
|
714
745
|
fi
|
|
715
746
|
|
|
716
|
-
#
|
|
717
|
-
|
|
747
|
+
# Escape the query for JSON (handle quotes and newlines)
|
|
748
|
+
ESCAPED_QUERY=$(echo "$USER_QUERY" | head -c 500 | jq -Rs '.')
|
|
749
|
+
|
|
750
|
+
# Inject context with semantic search based on the user's prompt
|
|
751
|
+
RESULT=$(curl -s --max-time 2 -X POST "$API_URL/api/v1/context/inject" \\
|
|
718
752
|
-H "Authorization: Bearer $API_KEY" \\
|
|
719
753
|
-H "Content-Type: application/json" \\
|
|
720
|
-
-d "{\\"project_id\\": \\"$PROJECT_ID\\", \\"cwd\\": \\"$CWD\\"}" \\
|
|
754
|
+
-d "{\\"query\\": $ESCAPED_QUERY, \\"project_id\\": \\"$PROJECT_ID\\", \\"cwd\\": \\"$CWD\\"}" \\
|
|
721
755
|
2>/dev/null)
|
|
722
756
|
|
|
723
|
-
# Output context
|
|
757
|
+
# Output context as additionalContext JSON for Claude Code
|
|
724
758
|
if [ -n "$RESULT" ] && [ "$RESULT" != "null" ]; then
|
|
725
|
-
CONTEXT=$(echo "$RESULT" | jq -r '.context // empty' 2>/dev/null)
|
|
726
|
-
if [ -n "$CONTEXT" ]; then
|
|
727
|
-
|
|
759
|
+
CONTEXT=$(echo "$RESULT" | jq -r '.data.context // empty' 2>/dev/null)
|
|
760
|
+
if [ -n "$CONTEXT" ] && [ "$CONTEXT" != "" ]; then
|
|
761
|
+
# Output as JSON with additionalContext for Claude Code to inject
|
|
762
|
+
echo "{\\"additionalContext\\": $CONTEXT}"
|
|
728
763
|
fi
|
|
729
764
|
fi
|
|
730
765
|
`;
|
|
@@ -777,6 +812,77 @@ curl -s -X POST "$API_URL/api/v1/tools/log" \\
|
|
|
777
812
|
}
|
|
778
813
|
writeFileSync(postToolPath, postToolHook, { mode: 0o755 });
|
|
779
814
|
success('Installed post-tool-use.sh hook');
|
|
815
|
+
// PreCompact hook - auto-saves session knowledge before compaction
|
|
816
|
+
// Fires before Claude's auto-compact summarizes away conversation details
|
|
817
|
+
const preCompactHook = `#!/bin/bash
|
|
818
|
+
# ClaudeTools PreCompact Knowledge Extraction Hook
|
|
819
|
+
# Automatically extracts and saves important session knowledge before compaction
|
|
820
|
+
|
|
821
|
+
# Skip if disabled
|
|
822
|
+
if [ "$CLAUDE_DISABLE_HOOKS" = "1" ]; then exit 0; fi
|
|
823
|
+
|
|
824
|
+
# Read the transcript from stdin (Claude Code passes JSON with transcript)
|
|
825
|
+
INPUT=$(timeout 5 cat 2>/dev/null || echo "")
|
|
826
|
+
if [ -z "$INPUT" ]; then exit 0; fi
|
|
827
|
+
|
|
828
|
+
# Extract trigger type (auto or manual)
|
|
829
|
+
TRIGGER=$(echo "$INPUT" | jq -r '.trigger // "unknown"' 2>/dev/null)
|
|
830
|
+
|
|
831
|
+
# Only process auto compaction (most valuable to preserve)
|
|
832
|
+
# Manual compaction the user controls what gets kept
|
|
833
|
+
if [ "$TRIGGER" != "auto" ]; then exit 0; fi
|
|
834
|
+
|
|
835
|
+
# Read config
|
|
836
|
+
CONFIG_FILE="$HOME/.claudetools/config.json"
|
|
837
|
+
if [ ! -f "$CONFIG_FILE" ]; then exit 0; fi
|
|
838
|
+
|
|
839
|
+
API_URL=$(jq -r '.apiUrl // "https://api.claudetools.dev"' "$CONFIG_FILE")
|
|
840
|
+
API_KEY=$(jq -r '.apiKey // empty' "$CONFIG_FILE")
|
|
841
|
+
|
|
842
|
+
if [ -z "$API_KEY" ]; then exit 0; fi
|
|
843
|
+
|
|
844
|
+
# Get current project
|
|
845
|
+
CWD=$(pwd)
|
|
846
|
+
PROJECT_ID=""
|
|
847
|
+
|
|
848
|
+
CLAUDE_MD="$CWD/.claude/CLAUDE.md"
|
|
849
|
+
if [ -f "$CLAUDE_MD" ]; then
|
|
850
|
+
PROJECT_ID=$(grep "Project ID" "$CLAUDE_MD" 2>/dev/null | sed -n "s/.*\\\`\\([a-z0-9_]*\\)\\\`.*/\\1/p" | head -1)
|
|
851
|
+
fi
|
|
852
|
+
|
|
853
|
+
if [ -z "$PROJECT_ID" ]; then
|
|
854
|
+
PROJECT_FILE="$HOME/.claudetools/projects.json"
|
|
855
|
+
if [ -f "$PROJECT_FILE" ]; then
|
|
856
|
+
PROJECT_ID=$(jq -r --arg cwd "$CWD" '
|
|
857
|
+
.bindings[]? | select(.local_path) |
|
|
858
|
+
. as $b | select($cwd | startswith($b.local_path)) |
|
|
859
|
+
.project_id' "$PROJECT_FILE" 2>/dev/null | head -1)
|
|
860
|
+
fi
|
|
861
|
+
fi
|
|
862
|
+
|
|
863
|
+
if [ -z "$PROJECT_ID" ]; then exit 0; fi
|
|
864
|
+
|
|
865
|
+
# Extract the transcript/conversation from the input
|
|
866
|
+
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript // .conversation // .messages // empty' 2>/dev/null)
|
|
867
|
+
|
|
868
|
+
# Send to API for knowledge extraction (async, don't block compaction)
|
|
869
|
+
curl -s --max-time 10 -X POST "$API_URL/api/v1/context/extract" \\
|
|
870
|
+
-H "Authorization: Bearer $API_KEY" \\
|
|
871
|
+
-H "Content-Type: application/json" \\
|
|
872
|
+
-d "{\\"project_id\\": \\"$PROJECT_ID\\", \\"trigger\\": \\"$TRIGGER\\", \\"transcript\\": $TRANSCRIPT}" \\
|
|
873
|
+
2>/dev/null &
|
|
874
|
+
|
|
875
|
+
# Don't wait for extraction - let compaction proceed
|
|
876
|
+
exit 0
|
|
877
|
+
`;
|
|
878
|
+
const preCompactPath = join(HOOKS_DIR, 'pre-compact.sh');
|
|
879
|
+
if (existsSync(preCompactPath)) {
|
|
880
|
+
const backup = backupFile(preCompactPath);
|
|
881
|
+
if (backup)
|
|
882
|
+
info(`Backed up existing hook to ${basename(backup)}`);
|
|
883
|
+
}
|
|
884
|
+
writeFileSync(preCompactPath, preCompactHook, { mode: 0o755 });
|
|
885
|
+
success('Installed pre-compact.sh hook');
|
|
780
886
|
}
|
|
781
887
|
async function configureSettings() {
|
|
782
888
|
header('Claude Code Settings');
|
|
@@ -850,6 +956,23 @@ async function configureSettings() {
|
|
|
850
956
|
});
|
|
851
957
|
success('Added PostToolUse hook to settings');
|
|
852
958
|
}
|
|
959
|
+
// Add PreCompact hook (auto-saves session knowledge before compaction)
|
|
960
|
+
if (!hooks.PreCompact) {
|
|
961
|
+
hooks.PreCompact = [];
|
|
962
|
+
}
|
|
963
|
+
const preCompactHooks = hooks.PreCompact;
|
|
964
|
+
const hasPreCompactHook = preCompactHooks.some(h => h.hooks?.some(hk => hk.command?.includes('pre-compact.sh')));
|
|
965
|
+
if (!hasPreCompactHook) {
|
|
966
|
+
preCompactHooks.push({
|
|
967
|
+
matcher: '',
|
|
968
|
+
hooks: [{
|
|
969
|
+
type: 'command',
|
|
970
|
+
command: join(HOOKS_DIR, 'pre-compact.sh'),
|
|
971
|
+
timeout: 15, // Give extraction more time
|
|
972
|
+
}],
|
|
973
|
+
});
|
|
974
|
+
success('Added PreCompact hook to settings');
|
|
975
|
+
}
|
|
853
976
|
// Write settings
|
|
854
977
|
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
855
978
|
success(`Saved settings to ${SETTINGS_PATH}`);
|
|
@@ -967,7 +1090,7 @@ async function verifySetup(config) {
|
|
|
967
1090
|
error('MCP config not found');
|
|
968
1091
|
}
|
|
969
1092
|
// Check hooks installed
|
|
970
|
-
const requiredHooks = ['session-start.sh', 'user-prompt-submit.sh', 'post-tool-use.sh'];
|
|
1093
|
+
const requiredHooks = ['session-start.sh', 'user-prompt-submit.sh', 'post-tool-use.sh', 'pre-compact.sh'];
|
|
971
1094
|
const installedHooks = requiredHooks.filter(h => existsSync(join(HOOKS_DIR, h)));
|
|
972
1095
|
if (installedHooks.length === requiredHooks.length) {
|
|
973
1096
|
success('All hooks installed');
|
|
@@ -1151,7 +1274,7 @@ export async function runUninstall() {
|
|
|
1151
1274
|
}
|
|
1152
1275
|
}
|
|
1153
1276
|
// Remove hook scripts
|
|
1154
|
-
const hooks = ['session-start.sh', 'user-prompt-submit.sh', 'post-tool-use.sh'];
|
|
1277
|
+
const hooks = ['session-start.sh', 'user-prompt-submit.sh', 'post-tool-use.sh', 'pre-compact.sh'];
|
|
1155
1278
|
for (const hook of hooks) {
|
|
1156
1279
|
const hookPath = join(HOOKS_DIR, hook);
|
|
1157
1280
|
if (existsSync(hookPath)) {
|
package/dist/tools.js
CHANGED
|
@@ -180,6 +180,111 @@ EXAMPLES:
|
|
|
180
180
|
},
|
|
181
181
|
},
|
|
182
182
|
},
|
|
183
|
+
// =========================================================================
|
|
184
|
+
// PROGRESSIVE DISCLOSURE TOOLS (Layer 1 & 2)
|
|
185
|
+
// =========================================================================
|
|
186
|
+
{
|
|
187
|
+
name: 'memory_index',
|
|
188
|
+
description: `Get a lightweight index of available memories WITHOUT fetching full content. Use this FIRST to scan what context is available before deciding what to retrieve.
|
|
189
|
+
|
|
190
|
+
Returns for each memory:
|
|
191
|
+
- id: Unique identifier for fetching details
|
|
192
|
+
- topic: Short topic/subject description
|
|
193
|
+
- relevance: Score (0-1) indicating how relevant to query
|
|
194
|
+
- token_cost: Estimated tokens if fetched
|
|
195
|
+
- importance: "critical" | "high" | "normal" - Critical facts are auto-injected
|
|
196
|
+
- category: "architecture" | "pattern" | "decision" | "preference" | "fact"
|
|
197
|
+
- last_accessed: When this memory was last used
|
|
198
|
+
|
|
199
|
+
WORKFLOW:
|
|
200
|
+
1. Call memory_index with your query to see what's available
|
|
201
|
+
2. Review the index - critical items are auto-injected
|
|
202
|
+
3. Call memory_detail for specific IDs you want full content for
|
|
203
|
+
|
|
204
|
+
This saves tokens by letting you selectively fetch only relevant memories.`,
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
query: {
|
|
209
|
+
type: 'string',
|
|
210
|
+
description: 'Search query to find relevant memories (optional - returns all if empty)',
|
|
211
|
+
},
|
|
212
|
+
project_id: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: 'Project ID (optional, uses default if not provided)',
|
|
215
|
+
},
|
|
216
|
+
limit: {
|
|
217
|
+
type: 'number',
|
|
218
|
+
description: 'Maximum number of index entries to return (default: 50)',
|
|
219
|
+
},
|
|
220
|
+
include_categories: {
|
|
221
|
+
type: 'array',
|
|
222
|
+
items: { type: 'string', enum: ['architecture', 'pattern', 'decision', 'preference', 'fact'] },
|
|
223
|
+
description: 'Filter by memory categories (optional)',
|
|
224
|
+
},
|
|
225
|
+
min_relevance: {
|
|
226
|
+
type: 'number',
|
|
227
|
+
description: 'Minimum relevance score (0-1) to include (default: 0)',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'memory_detail',
|
|
234
|
+
description: `Fetch full content for specific memories by ID. Use after memory_index to retrieve details for memories you want.
|
|
235
|
+
|
|
236
|
+
Returns full memory content including:
|
|
237
|
+
- Complete fact/context text
|
|
238
|
+
- Related entities
|
|
239
|
+
- Source information
|
|
240
|
+
- Temporal context
|
|
241
|
+
|
|
242
|
+
Use sparingly - each fetch costs tokens. Let memory_index guide which memories to fetch.`,
|
|
243
|
+
inputSchema: {
|
|
244
|
+
type: 'object',
|
|
245
|
+
properties: {
|
|
246
|
+
memory_ids: {
|
|
247
|
+
type: 'array',
|
|
248
|
+
items: { type: 'string' },
|
|
249
|
+
description: 'Array of memory IDs to fetch full content for',
|
|
250
|
+
},
|
|
251
|
+
project_id: {
|
|
252
|
+
type: 'string',
|
|
253
|
+
description: 'Project ID (optional, uses default if not provided)',
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
required: ['memory_ids'],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'disclosure_analytics',
|
|
261
|
+
description: `View analytics for memory disclosure patterns. Shows which memories are indexed vs fetched, hit rates, and candidates for marking as critical.
|
|
262
|
+
|
|
263
|
+
Use this to optimize memory retrieval:
|
|
264
|
+
- See which memories are frequently fetched (may need auto-injection)
|
|
265
|
+
- Calculate overall hit rate (fetched / indexed ratio)
|
|
266
|
+
- Identify memories that should be marked as critical/architecture
|
|
267
|
+
- Track usage trends over time
|
|
268
|
+
|
|
269
|
+
High hit rate memories (frequently fetched after being indexed) are good candidates for promotion to critical status, which enables auto-injection and saves tokens.`,
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: {
|
|
273
|
+
project_id: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
description: 'Project ID (optional, uses default if not provided)',
|
|
276
|
+
},
|
|
277
|
+
days: {
|
|
278
|
+
type: 'number',
|
|
279
|
+
description: 'Number of days to analyze (default: 30)',
|
|
280
|
+
},
|
|
281
|
+
limit: {
|
|
282
|
+
type: 'number',
|
|
283
|
+
description: 'Maximum number of top memories to return (default: 20)',
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
183
288
|
{
|
|
184
289
|
name: 'query_dependencies',
|
|
185
290
|
description: 'Query function call dependencies from the code graph. Find what functions a given function calls (forward) or what functions call it (reverse). Week 4: Dependency Query Tool.',
|