@claudetools/tools 0.7.7 → 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.
@@ -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 facts and patterns from response
629
- FACTS=$(echo "$CONTEXT" | jq -r '.facts // []' 2>/dev/null)
630
- PATTERNS=$(echo "$CONTEXT" | jq -r '.patterns // []' 2>/dev/null)
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
- # Session Context: $PROJECT_NAME
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
- ## Relevant Facts
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 "$FACTS" | jq -r '.[] | "- \\(.entity1) \\(.relationship) \\(.entity2): \\(.context)"' 2>/dev/null || echo "- No facts stored yet")
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
- ## Detected Patterns
650
+ $(if [ "$INDEX_COUNT" != "0" ] && [ "$INDEX_COUNT" != "" ]; then
651
+ echo "---
644
652
 
645
- $(echo "$PATTERNS" | jq -r '.[] | "- **\\(.name)**: \\(.description)"' 2>/dev/null || echo "- No patterns detected yet")
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 file is regenerated each session. Do not edit manually.*
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
- # Automatically injects relevant memory context before each prompt
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
- # Inject context (silent fail)
717
- RESULT=$(curl -s -X POST "$API_URL/api/v1/context/inject" \\
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 if available
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
- echo "$CONTEXT"
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.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudetools/tools",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "Persistent AI memory, task management, and codebase intelligence for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",