claude-cli-advanced-starter-pack 1.0.15 → 1.1.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.
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Happy Title Generator Hook
3
+ *
4
+ * Auto-generates session titles with format: "Issue #XX - Summary"
5
+ * Detects GitHub issue numbers from branch names.
6
+ * Notifies Happy daemon of title updates for mobile display.
7
+ *
8
+ * Event: UserPromptSubmit
9
+ *
10
+ * Configuration: Reads from .claude/config/hooks-config.json
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { execSync } = require('child_process');
16
+
17
+ // Default configuration (can be overridden by hooks-config.json)
18
+ const DEFAULT_CONFIG = {
19
+ rate_limit_ms: 30000, // Throttle title updates to once per 30 sec
20
+ max_summary_words: 4, // Maximum words in title summary
21
+ issue_pattern: 'issue-(\\d+)', // Regex to extract issue number from branch
22
+ common_prefixes: [ // Prefixes to remove from summaries
23
+ 'implement', 'add', 'fix', 'update', 'create',
24
+ 'refactor', 'build', 'remove', 'optimize', 'improve'
25
+ ],
26
+ };
27
+
28
+ // Paths
29
+ const CONFIG_PATH = path.join(process.cwd(), '.claude', 'config', 'hooks-config.json');
30
+ const STATE_PATH = path.join(process.cwd(), '.claude', 'config', 'title-generator-state.json');
31
+ const HAPPY_STATE_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.happy', 'daemon.state.json');
32
+
33
+ /**
34
+ * Load configuration with defaults
35
+ */
36
+ function loadConfig() {
37
+ try {
38
+ if (fs.existsSync(CONFIG_PATH)) {
39
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
40
+ return { ...DEFAULT_CONFIG, ...(config.title_generator || {}) };
41
+ }
42
+ } catch (e) {
43
+ // Use defaults on error
44
+ }
45
+ return DEFAULT_CONFIG;
46
+ }
47
+
48
+ /**
49
+ * Load state
50
+ */
51
+ function loadState() {
52
+ try {
53
+ if (fs.existsSync(STATE_PATH)) {
54
+ return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
55
+ }
56
+ } catch (e) {
57
+ // Return fresh state
58
+ }
59
+ return {
60
+ last_update: 0,
61
+ current_title: null,
62
+ issue_number: null,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Save state
68
+ */
69
+ function saveState(state) {
70
+ try {
71
+ const dir = path.dirname(STATE_PATH);
72
+ if (!fs.existsSync(dir)) {
73
+ fs.mkdirSync(dir, { recursive: true });
74
+ }
75
+ fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), 'utf8');
76
+ } catch (e) {
77
+ // Silent failure
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get current git branch
83
+ */
84
+ function getCurrentBranch() {
85
+ try {
86
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
87
+ encoding: 'utf8',
88
+ stdio: ['pipe', 'pipe', 'pipe'],
89
+ timeout: 5000,
90
+ }).trim();
91
+ return branch;
92
+ } catch (e) {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Extract issue number from branch name
99
+ */
100
+ function extractIssueNumber(branch, config) {
101
+ if (!branch) return null;
102
+
103
+ try {
104
+ const pattern = new RegExp(config.issue_pattern, 'i');
105
+ const match = branch.match(pattern);
106
+ if (match && match[1]) {
107
+ return parseInt(match[1], 10);
108
+ }
109
+ } catch (e) {
110
+ // Invalid regex pattern
111
+ }
112
+
113
+ // Try common patterns as fallback
114
+ const fallbackPatterns = [
115
+ /issue-(\d+)/i,
116
+ /(\d+)-/,
117
+ /#(\d+)/,
118
+ ];
119
+
120
+ for (const pattern of fallbackPatterns) {
121
+ const match = branch.match(pattern);
122
+ if (match && match[1]) {
123
+ return parseInt(match[1], 10);
124
+ }
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Generate summary from user prompt
132
+ */
133
+ function generateSummary(prompt, config) {
134
+ if (!prompt) return 'New Session';
135
+
136
+ // Clean the prompt
137
+ let summary = prompt.trim();
138
+
139
+ // Remove common prefixes
140
+ const prefixPattern = new RegExp(`^(${config.common_prefixes.join('|')})\\s+`, 'i');
141
+ summary = summary.replace(prefixPattern, '');
142
+
143
+ // Take first N words
144
+ const words = summary.split(/\s+/).slice(0, config.max_summary_words);
145
+ summary = words.join(' ');
146
+
147
+ // Capitalize first letter
148
+ if (summary.length > 0) {
149
+ summary = summary.charAt(0).toUpperCase() + summary.slice(1);
150
+ }
151
+
152
+ // Truncate if too long
153
+ if (summary.length > 50) {
154
+ summary = summary.substring(0, 47) + '...';
155
+ }
156
+
157
+ return summary || 'New Session';
158
+ }
159
+
160
+ /**
161
+ * Format title with issue number
162
+ */
163
+ function formatTitle(issueNumber, summary, branch) {
164
+ let title = '';
165
+
166
+ if (issueNumber) {
167
+ title = `Issue #${issueNumber} - ${summary}`;
168
+ } else {
169
+ title = summary;
170
+ }
171
+
172
+ // Add branch info if available
173
+ if (branch && branch !== 'main' && branch !== 'master') {
174
+ const shortBranch = branch.length > 20 ? branch.substring(0, 17) + '...' : branch;
175
+ title = `${title} | ${shortBranch}`;
176
+ }
177
+
178
+ return title;
179
+ }
180
+
181
+ /**
182
+ * Notify Happy daemon of title update
183
+ */
184
+ function notifyHappyDaemon(title) {
185
+ try {
186
+ if (!fs.existsSync(HAPPY_STATE_PATH)) {
187
+ return false; // Happy daemon not running
188
+ }
189
+
190
+ const happyState = JSON.parse(fs.readFileSync(HAPPY_STATE_PATH, 'utf8'));
191
+
192
+ // Update session title in daemon state
193
+ happyState.current_session = happyState.current_session || {};
194
+ happyState.current_session.title = title;
195
+ happyState.current_session.updated_at = new Date().toISOString();
196
+
197
+ // Write atomically using temp file
198
+ const tempPath = HAPPY_STATE_PATH + '.tmp';
199
+ fs.writeFileSync(tempPath, JSON.stringify(happyState, null, 2), 'utf8');
200
+ fs.renameSync(tempPath, HAPPY_STATE_PATH);
201
+
202
+ return true;
203
+ } catch (e) {
204
+ return false; // Silent failure - daemon may not be running
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Main hook handler
210
+ */
211
+ module.exports = async function happyTitleGenerator(context) {
212
+ // Always continue - never block
213
+ const approve = () => ({ continue: true });
214
+
215
+ try {
216
+ const config = loadConfig();
217
+ const state = loadState();
218
+ const now = Date.now();
219
+
220
+ // Rate limiting - don't update too frequently
221
+ if (state.last_update && (now - state.last_update) < config.rate_limit_ms) {
222
+ return approve();
223
+ }
224
+
225
+ // Parse hook input for user prompt
226
+ let userPrompt = '';
227
+ try {
228
+ const input = JSON.parse(process.env.CLAUDE_HOOK_INPUT || '{}');
229
+ userPrompt = input.prompt || input.message || '';
230
+ } catch (e) {
231
+ // No prompt available
232
+ }
233
+
234
+ // Get git context
235
+ const branch = getCurrentBranch();
236
+ const issueNumber = extractIssueNumber(branch, config);
237
+
238
+ // Generate title components
239
+ const summary = generateSummary(userPrompt, config);
240
+ const title = formatTitle(issueNumber, summary, branch);
241
+
242
+ // Update state
243
+ state.last_update = now;
244
+ state.current_title = title;
245
+ state.issue_number = issueNumber;
246
+ saveState(state);
247
+
248
+ // Notify Happy daemon (if running)
249
+ const notified = notifyHappyDaemon(title);
250
+
251
+ if (notified) {
252
+ console.log(`[happy-title-generator] Title updated: ${title}`);
253
+ }
254
+
255
+ return approve();
256
+ } catch (error) {
257
+ console.error(`[happy-title-generator] Error: ${error.message}`);
258
+ return approve();
259
+ }
260
+ };
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Token Budget Loader Hook
3
+ *
4
+ * Pre-calculates daily token budget at session start.
5
+ * Tracks usage across sessions and provides health status alerts.
6
+ * Saves ~5K tokens/session by pre-loading budget state.
7
+ *
8
+ * Event: UserPromptSubmit (runs once per session)
9
+ *
10
+ * Configuration: Reads from .claude/config/hooks-config.json
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ // Default configuration (can be overridden by hooks-config.json)
17
+ const DEFAULT_CONFIG = {
18
+ daily_limit: 200000, // Daily token budget
19
+ warning_percent: 60, // Warn at this percentage
20
+ critical_percent: 80, // Critical warning at this percentage
21
+ reset_hours: 24, // Budget reset cycle in hours
22
+ };
23
+
24
+ // Paths
25
+ const CONFIG_PATH = path.join(process.cwd(), '.claude', 'config', 'hooks-config.json');
26
+ const BUDGET_PATH = path.join(process.cwd(), '.claude', 'config', 'token-budget.json');
27
+ const SESSION_MARKER = path.join(process.cwd(), '.claude', 'config', '.budget-loaded');
28
+
29
+ /**
30
+ * Load configuration with defaults
31
+ */
32
+ function loadConfig() {
33
+ try {
34
+ if (fs.existsSync(CONFIG_PATH)) {
35
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
36
+ return { ...DEFAULT_CONFIG, ...(config.token_budget || {}) };
37
+ }
38
+ } catch (e) {
39
+ // Use defaults on error
40
+ }
41
+ return DEFAULT_CONFIG;
42
+ }
43
+
44
+ /**
45
+ * Load budget state
46
+ */
47
+ function loadBudget() {
48
+ try {
49
+ if (fs.existsSync(BUDGET_PATH)) {
50
+ return JSON.parse(fs.readFileSync(BUDGET_PATH, 'utf8'));
51
+ }
52
+ } catch (e) {
53
+ // Return fresh budget
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Save budget state
60
+ */
61
+ function saveBudget(budget) {
62
+ try {
63
+ const dir = path.dirname(BUDGET_PATH);
64
+ if (!fs.existsSync(dir)) {
65
+ fs.mkdirSync(dir, { recursive: true });
66
+ }
67
+ fs.writeFileSync(BUDGET_PATH, JSON.stringify(budget, null, 2), 'utf8');
68
+ } catch (e) {
69
+ console.error(`[token-budget-loader] Failed to save budget: ${e.message}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Check if we've already loaded budget this session
75
+ */
76
+ function hasLoadedThisSession() {
77
+ try {
78
+ if (fs.existsSync(SESSION_MARKER)) {
79
+ const content = fs.readFileSync(SESSION_MARKER, 'utf8');
80
+ const timestamp = parseInt(content, 10);
81
+ // Session valid for 4 hours
82
+ if (Date.now() - timestamp < 4 * 60 * 60 * 1000) {
83
+ return true;
84
+ }
85
+ }
86
+ } catch (e) {
87
+ // Continue with loading
88
+ }
89
+ return false;
90
+ }
91
+
92
+ /**
93
+ * Mark session as loaded
94
+ */
95
+ function markSessionLoaded() {
96
+ try {
97
+ const dir = path.dirname(SESSION_MARKER);
98
+ if (!fs.existsSync(dir)) {
99
+ fs.mkdirSync(dir, { recursive: true });
100
+ }
101
+ fs.writeFileSync(SESSION_MARKER, Date.now().toString(), 'utf8');
102
+ } catch (e) {
103
+ // Silent failure
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Check if budget should reset (new day)
109
+ */
110
+ function shouldResetBudget(budget, config) {
111
+ if (!budget || !budget.reset_at) {
112
+ return true;
113
+ }
114
+
115
+ const resetAt = new Date(budget.reset_at);
116
+ const now = new Date();
117
+
118
+ return now >= resetAt;
119
+ }
120
+
121
+ /**
122
+ * Calculate next reset time
123
+ */
124
+ function getNextResetTime(config) {
125
+ const now = new Date();
126
+ const resetTime = new Date(now);
127
+
128
+ // Reset at midnight UTC by default
129
+ resetTime.setUTCHours(0, 0, 0, 0);
130
+ resetTime.setTime(resetTime.getTime() + config.reset_hours * 60 * 60 * 1000);
131
+
132
+ // If we've passed today's reset, move to next cycle
133
+ while (resetTime <= now) {
134
+ resetTime.setTime(resetTime.getTime() + config.reset_hours * 60 * 60 * 1000);
135
+ }
136
+
137
+ return resetTime.toISOString();
138
+ }
139
+
140
+ /**
141
+ * Estimate tokens from characters (rough approximation)
142
+ * Claude uses ~4 chars per token on average
143
+ */
144
+ function estimateTokens(chars) {
145
+ return Math.ceil(chars / 4);
146
+ }
147
+
148
+ /**
149
+ * Get budget health status
150
+ */
151
+ function getBudgetHealth(budget, config) {
152
+ const percentUsed = (budget.used / budget.limit) * 100;
153
+
154
+ if (percentUsed >= config.critical_percent) {
155
+ return {
156
+ status: 'critical',
157
+ message: `CRITICAL: ${percentUsed.toFixed(1)}% of daily budget used`,
158
+ color: 'red',
159
+ };
160
+ } else if (percentUsed >= config.warning_percent) {
161
+ return {
162
+ status: 'warning',
163
+ message: `WARNING: ${percentUsed.toFixed(1)}% of daily budget used`,
164
+ color: 'yellow',
165
+ };
166
+ } else {
167
+ return {
168
+ status: 'healthy',
169
+ message: `Budget healthy: ${percentUsed.toFixed(1)}% used`,
170
+ color: 'green',
171
+ };
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Main hook handler
177
+ */
178
+ module.exports = async function tokenBudgetLoader(context) {
179
+ // Always continue - never block
180
+ const approve = () => ({ continue: true });
181
+
182
+ try {
183
+ // Only run once per session
184
+ if (hasLoadedThisSession()) {
185
+ return approve();
186
+ }
187
+
188
+ // Mark this session as loaded
189
+ markSessionLoaded();
190
+
191
+ const config = loadConfig();
192
+ let budget = loadBudget();
193
+
194
+ // Check if we need to reset the budget
195
+ if (shouldResetBudget(budget, config)) {
196
+ budget = {
197
+ limit: config.daily_limit,
198
+ used: 0,
199
+ reset_at: getNextResetTime(config),
200
+ sessions: [],
201
+ created_at: new Date().toISOString(),
202
+ };
203
+ saveBudget(budget);
204
+ console.log(`[token-budget-loader] Budget reset. Daily limit: ${config.daily_limit.toLocaleString()} tokens`);
205
+ }
206
+
207
+ // Calculate remaining budget
208
+ const remaining = budget.limit - budget.used;
209
+ const health = getBudgetHealth(budget, config);
210
+
211
+ // Log budget status
212
+ console.log(`[token-budget-loader] ${health.message}`);
213
+ console.log(`[token-budget-loader] Remaining: ${remaining.toLocaleString()} tokens`);
214
+ console.log(`[token-budget-loader] Resets at: ${budget.reset_at}`);
215
+
216
+ // Record this session start
217
+ budget.sessions.push({
218
+ started_at: new Date().toISOString(),
219
+ initial_remaining: remaining,
220
+ });
221
+
222
+ // Keep only last 10 sessions
223
+ if (budget.sessions.length > 10) {
224
+ budget.sessions = budget.sessions.slice(-10);
225
+ }
226
+
227
+ saveBudget(budget);
228
+
229
+ return approve();
230
+ } catch (error) {
231
+ console.error(`[token-budget-loader] Error: ${error.message}`);
232
+ return approve();
233
+ }
234
+ };
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Tool Output Cacher Hook
3
+ *
4
+ * Caches large tool outputs (>2KB) to reduce context consumption.
5
+ * Monitors cumulative session output and warns at thresholds.
6
+ * Saves ~500 tokens per large output by storing in cache files.
7
+ *
8
+ * Event: PostToolUse
9
+ *
10
+ * Configuration: Reads from .claude/config/hooks-config.json
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const crypto = require('crypto');
16
+
17
+ // Default configuration (can be overridden by hooks-config.json)
18
+ const DEFAULT_CONFIG = {
19
+ threshold_chars: 2048, // Cache outputs larger than this
20
+ warning_threshold: 100000, // Warn when session total exceeds this
21
+ critical_threshold: 150000, // Critical warning at this level
22
+ session_timeout_ms: 300000, // 5 minutes - consider new session
23
+ cacheable_tools: ['Bash', 'Read', 'Glob', 'Grep'],
24
+ bypass_tools: ['Task', 'TaskOutput'],
25
+ };
26
+
27
+ // Paths
28
+ const CONFIG_PATH = path.join(process.cwd(), '.claude', 'config', 'hooks-config.json');
29
+ const CACHE_DIR = path.join(process.cwd(), '.claude', 'cache', 'tool-outputs');
30
+ const STATE_FILE = path.join(process.cwd(), '.claude', 'config', 'tool-output-cacher-state.json');
31
+
32
+ /**
33
+ * Load configuration with defaults
34
+ */
35
+ function loadConfig() {
36
+ try {
37
+ if (fs.existsSync(CONFIG_PATH)) {
38
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
39
+ return { ...DEFAULT_CONFIG, ...(config.output_caching || {}) };
40
+ }
41
+ } catch (e) {
42
+ // Use defaults on error
43
+ }
44
+ return DEFAULT_CONFIG;
45
+ }
46
+
47
+ /**
48
+ * Load session state
49
+ */
50
+ function loadState() {
51
+ try {
52
+ if (fs.existsSync(STATE_FILE)) {
53
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
54
+ }
55
+ } catch (e) {
56
+ // Return fresh state
57
+ }
58
+ return {
59
+ session_id: null,
60
+ session_start: null,
61
+ cumulative_chars: 0,
62
+ cached_count: 0,
63
+ last_activity: null,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Save session state
69
+ */
70
+ function saveState(state) {
71
+ try {
72
+ const dir = path.dirname(STATE_FILE);
73
+ if (!fs.existsSync(dir)) {
74
+ fs.mkdirSync(dir, { recursive: true });
75
+ }
76
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), 'utf8');
77
+ } catch (e) {
78
+ // Silent failure - don't block tool execution
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Generate cache key for output
84
+ */
85
+ function generateCacheKey(toolName, output) {
86
+ const hash = crypto.createHash('md5').update(output.slice(0, 500)).digest('hex').slice(0, 8);
87
+ return `${toolName.toLowerCase()}-${Date.now()}-${hash}`;
88
+ }
89
+
90
+ /**
91
+ * Cache output to file
92
+ */
93
+ function cacheOutput(key, output, summary) {
94
+ try {
95
+ if (!fs.existsSync(CACHE_DIR)) {
96
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
97
+ }
98
+
99
+ const cachePath = path.join(CACHE_DIR, `${key}.txt`);
100
+ fs.writeFileSync(cachePath, output, 'utf8');
101
+
102
+ return {
103
+ cached: true,
104
+ path: cachePath,
105
+ summary: summary,
106
+ };
107
+ } catch (e) {
108
+ return { cached: false, error: e.message };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Generate summary of large output
114
+ */
115
+ function generateSummary(output, maxLines = 15) {
116
+ const lines = output.split('\n');
117
+ const totalLines = lines.length;
118
+ const totalChars = output.length;
119
+
120
+ // Take first N lines as preview
121
+ const preview = lines.slice(0, maxLines).join('\n');
122
+
123
+ return {
124
+ preview: preview,
125
+ total_lines: totalLines,
126
+ total_chars: totalChars,
127
+ truncated: totalLines > maxLines,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Check if tool output should be cached
133
+ */
134
+ function shouldCache(toolName, output, config) {
135
+ // Skip bypass tools
136
+ if (config.bypass_tools.includes(toolName)) {
137
+ return false;
138
+ }
139
+
140
+ // Check if tool is cacheable
141
+ const isCacheable = config.cacheable_tools.some(t =>
142
+ toolName.startsWith(t) || toolName === t
143
+ );
144
+
145
+ if (!isCacheable) {
146
+ return false;
147
+ }
148
+
149
+ // Check size threshold
150
+ return output.length > config.threshold_chars;
151
+ }
152
+
153
+ /**
154
+ * Main hook handler
155
+ */
156
+ module.exports = async function toolOutputCacher(context) {
157
+ // Always approve - this is a PostToolUse hook for monitoring only
158
+ const approve = () => ({ continue: true });
159
+
160
+ try {
161
+ // Parse hook input
162
+ const input = JSON.parse(process.env.CLAUDE_HOOK_INPUT || '{}');
163
+ const { tool_name, tool_input, tool_output } = input;
164
+
165
+ if (!tool_name || !tool_output) {
166
+ return approve();
167
+ }
168
+
169
+ const output = typeof tool_output === 'string' ? tool_output : JSON.stringify(tool_output);
170
+ const config = loadConfig();
171
+
172
+ // Load and update session state
173
+ let state = loadState();
174
+ const now = Date.now();
175
+
176
+ // Check if this is a new session
177
+ if (!state.session_start || (now - state.last_activity) > config.session_timeout_ms) {
178
+ state = {
179
+ session_id: crypto.randomBytes(4).toString('hex'),
180
+ session_start: now,
181
+ cumulative_chars: 0,
182
+ cached_count: 0,
183
+ last_activity: now,
184
+ };
185
+ }
186
+
187
+ // Update cumulative tracking
188
+ state.cumulative_chars += output.length;
189
+ state.last_activity = now;
190
+
191
+ // Check if we should cache this output
192
+ if (shouldCache(tool_name, output, config)) {
193
+ const summary = generateSummary(output);
194
+ const cacheKey = generateCacheKey(tool_name, output);
195
+ const cacheResult = cacheOutput(cacheKey, output, summary);
196
+
197
+ if (cacheResult.cached) {
198
+ state.cached_count++;
199
+ console.log(`[tool-output-cacher] Cached ${tool_name} output (${output.length} chars) -> ${cacheResult.path}`);
200
+ }
201
+ }
202
+
203
+ // Check thresholds and warn
204
+ if (state.cumulative_chars >= config.critical_threshold) {
205
+ console.log(`[tool-output-cacher] CRITICAL: Session at ${state.cumulative_chars} chars. Consider compaction.`);
206
+ } else if (state.cumulative_chars >= config.warning_threshold) {
207
+ console.log(`[tool-output-cacher] WARNING: Session at ${state.cumulative_chars} chars.`);
208
+ }
209
+
210
+ // Save state
211
+ saveState(state);
212
+
213
+ return approve();
214
+ } catch (error) {
215
+ // Always approve even on error
216
+ console.error(`[tool-output-cacher] Error: ${error.message}`);
217
+ return approve();
218
+ }
219
+ };