claude-cli-advanced-starter-pack 1.0.16 → 1.8.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.
- package/OVERVIEW.md +5 -1
- package/README.md +241 -132
- package/bin/gtask.js +53 -0
- package/package.json +1 -1
- package/src/cli/menu.js +27 -0
- package/src/commands/explore-mcp/mcp-registry.js +99 -0
- package/src/commands/init.js +309 -80
- package/src/commands/install-panel-hook.js +108 -0
- package/src/commands/install-scripts.js +232 -0
- package/src/commands/install-skill.js +220 -0
- package/src/commands/panel.js +297 -0
- package/src/commands/setup-wizard.js +4 -3
- package/src/commands/test-setup.js +4 -5
- package/src/data/releases.json +209 -0
- package/src/panel/queue.js +188 -0
- package/templates/commands/ask-claude.template.md +118 -0
- package/templates/commands/ccasp-panel.template.md +72 -0
- package/templates/commands/ccasp-setup.template.md +470 -79
- package/templates/commands/create-smoke-test.template.md +186 -0
- package/templates/commands/project-impl.template.md +9 -113
- package/templates/commands/refactor-check.template.md +112 -0
- package/templates/commands/refactor-cleanup.template.md +144 -0
- package/templates/commands/refactor-prep.template.md +192 -0
- package/templates/docs/AI_ARCHITECTURE_CONSTITUTION.template.md +198 -0
- package/templates/docs/DETAILED_GOTCHAS.template.md +347 -0
- package/templates/docs/PHASE-DEV-CHECKLIST.template.md +241 -0
- package/templates/docs/PROGRESS_JSON_TEMPLATE.json +117 -0
- package/templates/docs/background-agent.template.md +264 -0
- package/templates/hooks/autonomous-decision-logger.template.js +207 -0
- package/templates/hooks/branch-merge-checker.template.js +272 -0
- package/templates/hooks/context-injector.template.js +261 -0
- package/templates/hooks/git-commit-tracker.template.js +267 -0
- package/templates/hooks/happy-mode-detector.template.js +214 -0
- package/templates/hooks/happy-title-generator.template.js +260 -0
- package/templates/hooks/issue-completion-detector.template.js +205 -0
- package/templates/hooks/panel-queue-reader.template.js +83 -0
- package/templates/hooks/phase-validation-gates.template.js +307 -0
- package/templates/hooks/session-id-generator.template.js +236 -0
- package/templates/hooks/token-budget-loader.template.js +234 -0
- package/templates/hooks/token-usage-monitor.template.js +193 -0
- package/templates/hooks/tool-output-cacher.template.js +219 -0
- package/templates/patterns/README.md +129 -0
- package/templates/patterns/l1-l2-orchestration.md +189 -0
- package/templates/patterns/multi-phase-orchestration.md +258 -0
- package/templates/patterns/two-tier-query-pipeline.md +192 -0
- package/templates/scripts/README.md +109 -0
- package/templates/scripts/analyze-delegation-log.js +299 -0
- package/templates/scripts/autonomous-decision-logger.js +277 -0
- package/templates/scripts/git-history-analyzer.py +269 -0
- package/templates/scripts/phase-validation-gates.js +307 -0
- package/templates/scripts/poll-deployment-status.js +260 -0
- package/templates/scripts/roadmap-scanner.js +263 -0
- package/templates/scripts/validate-deployment.js +293 -0
- package/templates/skills/agent-creator/skill.json +18 -0
- package/templates/skills/agent-creator/skill.md +335 -0
- package/templates/skills/hook-creator/skill.json +18 -0
- package/templates/skills/hook-creator/skill.md +318 -0
- package/templates/skills/panel/skill.json +18 -0
- package/templates/skills/panel/skill.md +90 -0
- package/templates/skills/rag-agent-creator/skill.json +18 -0
- package/templates/skills/rag-agent-creator/skill.md +307 -0
|
@@ -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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Usage Monitor Hook
|
|
3
|
+
*
|
|
4
|
+
* Tracks cumulative token usage across the session.
|
|
5
|
+
* Auto-respawn warning at 90% of context window.
|
|
6
|
+
* Provides real-time token consumption feedback.
|
|
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
|
+
|
|
16
|
+
// Default configuration
|
|
17
|
+
const DEFAULT_CONFIG = {
|
|
18
|
+
context_limit: 200000, // Estimated context window
|
|
19
|
+
warning_threshold: 0.75, // Warn at 75%
|
|
20
|
+
critical_threshold: 0.90, // Critical at 90%
|
|
21
|
+
auto_compact_suggestion: true, // Suggest compaction at critical
|
|
22
|
+
session_timeout_ms: 300000, // 5 minutes
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Paths
|
|
26
|
+
const CONFIG_PATH = path.join(process.cwd(), '.claude', 'config', 'hooks-config.json');
|
|
27
|
+
const STATE_PATH = path.join(process.cwd(), '.claude', 'config', 'token-usage-state.json');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load configuration
|
|
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_usage || {}) };
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// Use defaults
|
|
40
|
+
}
|
|
41
|
+
return DEFAULT_CONFIG;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load state
|
|
46
|
+
*/
|
|
47
|
+
function loadState() {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(STATE_PATH)) {
|
|
50
|
+
return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// Fresh state
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
session_id: null,
|
|
57
|
+
session_start: null,
|
|
58
|
+
total_input_chars: 0,
|
|
59
|
+
total_output_chars: 0,
|
|
60
|
+
estimated_tokens: 0,
|
|
61
|
+
tool_calls: 0,
|
|
62
|
+
last_activity: null,
|
|
63
|
+
warnings_shown: 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Save state
|
|
69
|
+
*/
|
|
70
|
+
function saveState(state) {
|
|
71
|
+
try {
|
|
72
|
+
const dir = path.dirname(STATE_PATH);
|
|
73
|
+
if (!fs.existsSync(dir)) {
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), 'utf8');
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// Silent
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Estimate tokens from text (rough approximation)
|
|
84
|
+
*/
|
|
85
|
+
function estimateTokens(text) {
|
|
86
|
+
if (!text) return 0;
|
|
87
|
+
// Average ~4 characters per token for English text
|
|
88
|
+
return Math.ceil(text.length / 4);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generate session ID
|
|
93
|
+
*/
|
|
94
|
+
function generateSessionId() {
|
|
95
|
+
return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Format number with commas
|
|
100
|
+
*/
|
|
101
|
+
function formatNumber(num) {
|
|
102
|
+
return num.toLocaleString();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Main hook handler
|
|
107
|
+
*/
|
|
108
|
+
module.exports = async function tokenUsageMonitor(context) {
|
|
109
|
+
const approve = () => ({ continue: true });
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const config = loadConfig();
|
|
113
|
+
let state = loadState();
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
|
|
116
|
+
// Check for new session
|
|
117
|
+
if (!state.session_start || (now - state.last_activity) > config.session_timeout_ms) {
|
|
118
|
+
state = {
|
|
119
|
+
session_id: generateSessionId(),
|
|
120
|
+
session_start: now,
|
|
121
|
+
total_input_chars: 0,
|
|
122
|
+
total_output_chars: 0,
|
|
123
|
+
estimated_tokens: 0,
|
|
124
|
+
tool_calls: 0,
|
|
125
|
+
last_activity: now,
|
|
126
|
+
warnings_shown: 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Parse hook input
|
|
131
|
+
let inputChars = 0;
|
|
132
|
+
let outputChars = 0;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const input = JSON.parse(process.env.CLAUDE_HOOK_INPUT || '{}');
|
|
136
|
+
|
|
137
|
+
// Count input characters
|
|
138
|
+
if (input.tool_input) {
|
|
139
|
+
const inputStr = typeof input.tool_input === 'string'
|
|
140
|
+
? input.tool_input
|
|
141
|
+
: JSON.stringify(input.tool_input);
|
|
142
|
+
inputChars = inputStr.length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Count output characters
|
|
146
|
+
if (input.tool_output) {
|
|
147
|
+
const outputStr = typeof input.tool_output === 'string'
|
|
148
|
+
? input.tool_output
|
|
149
|
+
: JSON.stringify(input.tool_output);
|
|
150
|
+
outputChars = outputStr.length;
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// Skip counting on error
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Update totals
|
|
157
|
+
state.total_input_chars += inputChars;
|
|
158
|
+
state.total_output_chars += outputChars;
|
|
159
|
+
state.tool_calls++;
|
|
160
|
+
state.last_activity = now;
|
|
161
|
+
|
|
162
|
+
// Estimate tokens
|
|
163
|
+
const totalChars = state.total_input_chars + state.total_output_chars;
|
|
164
|
+
state.estimated_tokens = estimateTokens(totalChars);
|
|
165
|
+
|
|
166
|
+
// Calculate usage percentage
|
|
167
|
+
const usagePercent = state.estimated_tokens / config.context_limit;
|
|
168
|
+
|
|
169
|
+
// Check thresholds
|
|
170
|
+
if (usagePercent >= config.critical_threshold) {
|
|
171
|
+
if (state.warnings_shown < 3) { // Don't spam
|
|
172
|
+
console.log(`[token-usage-monitor] CRITICAL: ${(usagePercent * 100).toFixed(1)}% of context used`);
|
|
173
|
+
console.log(`[token-usage-monitor] Estimated tokens: ${formatNumber(state.estimated_tokens)}`);
|
|
174
|
+
if (config.auto_compact_suggestion) {
|
|
175
|
+
console.log('[token-usage-monitor] Consider using /context-audit or starting a new session');
|
|
176
|
+
}
|
|
177
|
+
state.warnings_shown++;
|
|
178
|
+
}
|
|
179
|
+
} else if (usagePercent >= config.warning_threshold) {
|
|
180
|
+
if (state.warnings_shown < 2) {
|
|
181
|
+
console.log(`[token-usage-monitor] WARNING: ${(usagePercent * 100).toFixed(1)}% of context used`);
|
|
182
|
+
state.warnings_shown++;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
saveState(state);
|
|
187
|
+
|
|
188
|
+
return approve();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(`[token-usage-monitor] Error: ${error.message}`);
|
|
191
|
+
return approve();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
@@ -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
|
+
};
|