claude-cli-advanced-starter-pack 1.0.16 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-cli-advanced-starter-pack",
3
- "version": "1.0.16",
3
+ "version": "1.1.0",
4
4
  "description": "Advanced Claude Code CLI toolkit - agents, hooks, skills, MCP servers, phased development, and GitHub integration",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -42,7 +42,7 @@ const OPTIONAL_FEATURES = [
42
42
  label: 'Token Budget Management',
43
43
  description: 'Monitor and manage Claude API token usage with automatic compaction warnings, archive suggestions, and respawn thresholds. Includes hooks that track usage per session.',
44
44
  commands: ['context-audit'],
45
- hooks: ['context-guardian'], // Only include hooks with templates
45
+ hooks: ['context-guardian', 'token-budget-loader', 'tool-output-cacher'],
46
46
  default: false,
47
47
  requiresPostConfig: false,
48
48
  },
@@ -51,10 +51,10 @@ const OPTIONAL_FEATURES = [
51
51
  label: 'Happy Engineering Integration',
52
52
  description: 'Integration with Happy Coder mobile app for remote session control, checkpoint management, and mobile-optimized responses.',
53
53
  commands: ['happy-start'],
54
- hooks: ['happy-checkpoint-manager'], // Only include hooks with templates
54
+ hooks: ['happy-checkpoint-manager', 'happy-title-generator', 'happy-mode-detector', 'context-injector'],
55
55
  default: false,
56
56
  requiresPostConfig: true,
57
- npmPackage: 'happy-coder', // Optional npm package to install
57
+ npmPackage: 'happy-coder',
58
58
  npmInstallPrompt: 'Install Happy Coder CLI globally? (npm i -g happy-coder)',
59
59
  },
60
60
  {
@@ -1,5 +1,50 @@
1
1
  {
2
2
  "releases": [
3
+ {
4
+ "version": "1.1.0",
5
+ "date": "2026-01-30",
6
+ "summary": "Feature: Phase 1 of 85 Recommendations - 5 New Hook Templates",
7
+ "highlights": [
8
+ "Added tool-output-cacher.template.js - Caches large outputs, saves ~500 tokens per output",
9
+ "Added token-budget-loader.template.js - Pre-calculates daily budget, ~5K tokens/session savings",
10
+ "Added happy-title-generator.template.js - Auto-generates session titles with issue numbers",
11
+ "Added happy-mode-detector.template.js - Detects Happy daemon environment",
12
+ "Added context-injector.template.js - Injects prior session context for continuity",
13
+ "All hooks now configurable via .claude/config/hooks-config.json",
14
+ "Updated tokenManagement feature to include token-budget-loader and tool-output-cacher",
15
+ "Updated happyMode feature to include all Happy-related hooks"
16
+ ],
17
+ "newFeatures": {
18
+ "commands": [],
19
+ "agents": [],
20
+ "skills": [],
21
+ "hooks": [
22
+ {
23
+ "name": "tool-output-cacher",
24
+ "description": "Caches large tool outputs to reduce context consumption"
25
+ },
26
+ {
27
+ "name": "token-budget-loader",
28
+ "description": "Pre-calculates daily token budget at session start"
29
+ },
30
+ {
31
+ "name": "happy-title-generator",
32
+ "description": "Auto-generates session titles for Happy daemon"
33
+ },
34
+ {
35
+ "name": "happy-mode-detector",
36
+ "description": "Detects Happy daemon environment and enables mobile mode"
37
+ },
38
+ {
39
+ "name": "context-injector",
40
+ "description": "Injects prior session context for seamless resumption"
41
+ }
42
+ ],
43
+ "other": []
44
+ },
45
+ "breaking": [],
46
+ "deprecated": []
47
+ },
3
48
  {
4
49
  "version": "1.0.16",
5
50
  "date": "2026-01-30",
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Context Injector Hook
3
+ *
4
+ * Injects prior session context for seamless resumption.
5
+ * Loads checkpoints, recent progress, and active features.
6
+ * Enables continuity across multi-day projects.
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
+ recent_completed_tasks: 3, // Number of completed tasks to show
19
+ next_pending_tasks: 2, // Number of pending tasks to show
20
+ active_features_limit: 3, // Number of active features to show
21
+ recent_agents_limit: 5, // Number of recent agents to show
22
+ max_checkpoint_age_hours: 48, // Ignore checkpoints older than this
23
+ };
24
+
25
+ // Paths
26
+ const CONFIG_PATH = path.join(process.cwd(), '.claude', 'config', 'hooks-config.json');
27
+ const CHECKPOINT_PATH = path.join(process.cwd(), '.claude', 'checkpoints', 'latest.json');
28
+ const FEATURE_TRACKING_PATH = path.join(process.cwd(), '.claude', 'config', 'feature-tracking.json');
29
+ const AGENT_LOG_PATH = path.join(process.cwd(), '.claude', 'logs', 'agent-activity.json');
30
+ const SESSION_MARKER = path.join(process.cwd(), '.claude', 'config', '.context-injected');
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.context_injector || {}) };
40
+ }
41
+ } catch (e) {
42
+ // Use defaults on error
43
+ }
44
+ return DEFAULT_CONFIG;
45
+ }
46
+
47
+ /**
48
+ * Check if we've already injected context this session
49
+ */
50
+ function hasInjectedThisSession() {
51
+ try {
52
+ if (fs.existsSync(SESSION_MARKER)) {
53
+ const content = fs.readFileSync(SESSION_MARKER, 'utf8');
54
+ const timestamp = parseInt(content, 10);
55
+ // Session valid for 4 hours
56
+ if (Date.now() - timestamp < 4 * 60 * 60 * 1000) {
57
+ return true;
58
+ }
59
+ }
60
+ } catch (e) {
61
+ // Continue with injection
62
+ }
63
+ return false;
64
+ }
65
+
66
+ /**
67
+ * Mark session as injected
68
+ */
69
+ function markSessionInjected() {
70
+ try {
71
+ const dir = path.dirname(SESSION_MARKER);
72
+ if (!fs.existsSync(dir)) {
73
+ fs.mkdirSync(dir, { recursive: true });
74
+ }
75
+ fs.writeFileSync(SESSION_MARKER, Date.now().toString(), 'utf8');
76
+ } catch (e) {
77
+ // Silent failure
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Load latest checkpoint
83
+ */
84
+ function loadCheckpoint(config) {
85
+ try {
86
+ if (!fs.existsSync(CHECKPOINT_PATH)) {
87
+ return null;
88
+ }
89
+
90
+ const checkpoint = JSON.parse(fs.readFileSync(CHECKPOINT_PATH, 'utf8'));
91
+
92
+ // Check age
93
+ if (checkpoint.created_at) {
94
+ const age = Date.now() - new Date(checkpoint.created_at).getTime();
95
+ const maxAge = config.max_checkpoint_age_hours * 60 * 60 * 1000;
96
+ if (age > maxAge) {
97
+ return null; // Checkpoint too old
98
+ }
99
+ }
100
+
101
+ return checkpoint;
102
+ } catch (e) {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Load feature tracking
109
+ */
110
+ function loadFeatureTracking() {
111
+ try {
112
+ if (fs.existsSync(FEATURE_TRACKING_PATH)) {
113
+ return JSON.parse(fs.readFileSync(FEATURE_TRACKING_PATH, 'utf8'));
114
+ }
115
+ } catch (e) {
116
+ // No feature tracking
117
+ }
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Load recent agent activity
123
+ */
124
+ function loadAgentActivity(config) {
125
+ try {
126
+ if (fs.existsSync(AGENT_LOG_PATH)) {
127
+ const activity = JSON.parse(fs.readFileSync(AGENT_LOG_PATH, 'utf8'));
128
+ // Return most recent agents
129
+ if (Array.isArray(activity)) {
130
+ return activity.slice(-config.recent_agents_limit);
131
+ }
132
+ }
133
+ } catch (e) {
134
+ // No agent activity
135
+ }
136
+ return [];
137
+ }
138
+
139
+ /**
140
+ * Format task list for display
141
+ */
142
+ function formatTaskList(tasks, limit, status) {
143
+ if (!tasks || !Array.isArray(tasks)) return '';
144
+
145
+ const filtered = tasks.filter(t => t.status === status).slice(0, limit);
146
+ if (filtered.length === 0) return '';
147
+
148
+ return filtered.map(t => ` - ${t.title || t.name || 'Unknown task'}`).join('\n');
149
+ }
150
+
151
+ /**
152
+ * Format feature list for display
153
+ */
154
+ function formatFeatureList(features, limit) {
155
+ if (!features || !Array.isArray(features)) return '';
156
+
157
+ const active = features.filter(f => f.status === 'in_progress').slice(0, limit);
158
+ if (active.length === 0) return '';
159
+
160
+ return active.map(f => ` - ${f.name}: ${f.progress || 0}%`).join('\n');
161
+ }
162
+
163
+ /**
164
+ * Format agent list for display
165
+ */
166
+ function formatAgentList(agents) {
167
+ if (!agents || agents.length === 0) return '';
168
+
169
+ return agents.map(a => {
170
+ const name = a.name || a.type || 'agent';
171
+ const status = a.success ? 'completed' : 'failed';
172
+ return ` - ${name}: ${status}`;
173
+ }).join('\n');
174
+ }
175
+
176
+ /**
177
+ * Build context message
178
+ */
179
+ function buildContextMessage(checkpoint, features, agents, config) {
180
+ const sections = [];
181
+
182
+ // Add checkpoint progress
183
+ if (checkpoint) {
184
+ const completed = formatTaskList(checkpoint.tasks, config.recent_completed_tasks, 'completed');
185
+ const pending = formatTaskList(checkpoint.tasks, config.next_pending_tasks, 'pending');
186
+
187
+ if (completed) {
188
+ sections.push(`**Recent Progress:**\n${completed}`);
189
+ }
190
+ if (pending) {
191
+ sections.push(`**Next Tasks:**\n${pending}`);
192
+ }
193
+ if (checkpoint.current_phase) {
194
+ sections.push(`**Current Phase:** ${checkpoint.current_phase}`);
195
+ }
196
+ }
197
+
198
+ // Add active features
199
+ if (features) {
200
+ const featureList = formatFeatureList(features.features || features, config.active_features_limit);
201
+ if (featureList) {
202
+ sections.push(`**Active Features:**\n${featureList}`);
203
+ }
204
+ }
205
+
206
+ // Add recent agents
207
+ if (agents && agents.length > 0) {
208
+ const agentList = formatAgentList(agents);
209
+ if (agentList) {
210
+ sections.push(`**Recent Agents:**\n${agentList}`);
211
+ }
212
+ }
213
+
214
+ return sections.join('\n\n');
215
+ }
216
+
217
+ /**
218
+ * Main hook handler
219
+ */
220
+ module.exports = async function contextInjector(context) {
221
+ // Always continue - never block
222
+ const approve = () => ({ continue: true });
223
+
224
+ try {
225
+ // Check if already injected this session
226
+ if (hasInjectedThisSession()) {
227
+ return approve();
228
+ }
229
+
230
+ // Mark session as injected
231
+ markSessionInjected();
232
+
233
+ const config = loadConfig();
234
+
235
+ // Load context sources
236
+ const checkpoint = loadCheckpoint(config);
237
+ const features = loadFeatureTracking();
238
+ const agents = loadAgentActivity(config);
239
+
240
+ // Check if we have any context to inject
241
+ if (!checkpoint && !features && agents.length === 0) {
242
+ console.log('[context-injector] No prior context found');
243
+ return approve();
244
+ }
245
+
246
+ // Build context message
247
+ const message = buildContextMessage(checkpoint, features, agents, config);
248
+
249
+ if (message) {
250
+ console.log('[context-injector] Session context loaded:');
251
+ console.log('---');
252
+ console.log(message);
253
+ console.log('---');
254
+ }
255
+
256
+ return approve();
257
+ } catch (error) {
258
+ console.error(`[context-injector] Error: ${error.message}`);
259
+ return approve();
260
+ }
261
+ };
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Happy Mode Detector Hook
3
+ *
4
+ * Auto-detects Happy daemon environment and enables mobile-optimized mode.
5
+ * Sets appropriate verbosity and response formatting for mobile clients.
6
+ * Supports multiple detection methods: env var, daemon state, manual config.
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
+ enabled: false, // Manual override
19
+ auto_detect: true, // Attempt auto-detection
20
+ verbosity: 'condensed', // 'verbose', 'condensed', 'compact'
21
+ show_file_stats: true, // Show file operation statistics
22
+ max_grep_results: 5, // Limit grep results for mobile
23
+ checkpoint_interval_minutes: 10, // Auto-checkpoint frequency
24
+ };
25
+
26
+ // Paths
27
+ const CONFIG_PATH = path.join(process.cwd(), '.claude', 'config', 'hooks-config.json');
28
+ const HAPPY_MODE_PATH = path.join(process.cwd(), '.claude', 'config', 'happy-mode.json');
29
+ const HAPPY_STATE_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.happy', 'daemon.state.json');
30
+ const SESSION_MARKER = path.join(process.cwd(), '.claude', 'config', '.happy-detected');
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.happy_mode || {}) };
40
+ }
41
+ } catch (e) {
42
+ // Use defaults on error
43
+ }
44
+ return DEFAULT_CONFIG;
45
+ }
46
+
47
+ /**
48
+ * Check if we've already detected Happy mode this session
49
+ */
50
+ function hasDetectedThisSession() {
51
+ try {
52
+ if (fs.existsSync(SESSION_MARKER)) {
53
+ const content = fs.readFileSync(SESSION_MARKER, 'utf8');
54
+ const data = JSON.parse(content);
55
+ // Session valid for 4 hours
56
+ if (Date.now() - data.timestamp < 4 * 60 * 60 * 1000) {
57
+ return data;
58
+ }
59
+ }
60
+ } catch (e) {
61
+ // Continue with detection
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Mark session as detected
68
+ */
69
+ function markSessionDetected(result) {
70
+ try {
71
+ const dir = path.dirname(SESSION_MARKER);
72
+ if (!fs.existsSync(dir)) {
73
+ fs.mkdirSync(dir, { recursive: true });
74
+ }
75
+ fs.writeFileSync(SESSION_MARKER, JSON.stringify({
76
+ timestamp: Date.now(),
77
+ ...result,
78
+ }, null, 2), 'utf8');
79
+ } catch (e) {
80
+ // Silent failure
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Detect Happy mode via environment variable
86
+ */
87
+ function detectViaEnvVar() {
88
+ return process.env.HAPPY_SESSION === 'true';
89
+ }
90
+
91
+ /**
92
+ * Detect Happy mode via daemon state file
93
+ */
94
+ function detectViaDaemonState() {
95
+ try {
96
+ if (fs.existsSync(HAPPY_STATE_PATH)) {
97
+ const state = JSON.parse(fs.readFileSync(HAPPY_STATE_PATH, 'utf8'));
98
+ // Check if daemon is running and has active session
99
+ if (state.running && state.current_session) {
100
+ return true;
101
+ }
102
+ }
103
+ } catch (e) {
104
+ // Daemon not running or state invalid
105
+ }
106
+ return false;
107
+ }
108
+
109
+ /**
110
+ * Detect Happy mode via manual configuration
111
+ */
112
+ function detectViaManualConfig() {
113
+ try {
114
+ if (fs.existsSync(HAPPY_MODE_PATH)) {
115
+ const config = JSON.parse(fs.readFileSync(HAPPY_MODE_PATH, 'utf8'));
116
+ return config.enabled === true;
117
+ }
118
+ } catch (e) {
119
+ // No manual config
120
+ }
121
+ return false;
122
+ }
123
+
124
+ /**
125
+ * Get detection method description
126
+ */
127
+ function getDetectionMethod(envVar, daemon, manual) {
128
+ if (manual) return 'manual_config';
129
+ if (envVar) return 'env_var';
130
+ if (daemon) return 'daemon_state';
131
+ return 'none';
132
+ }
133
+
134
+ /**
135
+ * Save Happy mode state for other hooks to use
136
+ */
137
+ function saveHappyModeState(result) {
138
+ try {
139
+ const dir = path.dirname(HAPPY_MODE_PATH);
140
+ if (!fs.existsSync(dir)) {
141
+ fs.mkdirSync(dir, { recursive: true });
142
+ }
143
+ fs.writeFileSync(HAPPY_MODE_PATH, JSON.stringify(result, null, 2), 'utf8');
144
+ } catch (e) {
145
+ // Silent failure
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Main hook handler
151
+ */
152
+ module.exports = async function happyModeDetector(context) {
153
+ // Always continue - never block
154
+ const approve = () => ({ continue: true });
155
+
156
+ try {
157
+ // Check if already detected this session
158
+ const cached = hasDetectedThisSession();
159
+ if (cached) {
160
+ return approve();
161
+ }
162
+
163
+ const config = loadConfig();
164
+
165
+ // Skip detection if auto-detect is disabled
166
+ if (!config.auto_detect) {
167
+ const result = {
168
+ happy_mode: config.enabled,
169
+ detection_method: config.enabled ? 'manual_override' : 'disabled',
170
+ config: config,
171
+ };
172
+ markSessionDetected(result);
173
+ return approve();
174
+ }
175
+
176
+ // Try all detection methods
177
+ const viaEnvVar = detectViaEnvVar();
178
+ const viaDaemon = detectViaDaemonState();
179
+ const viaManual = detectViaManualConfig();
180
+
181
+ const isHappyMode = viaEnvVar || viaDaemon || viaManual;
182
+ const detectionMethod = getDetectionMethod(viaEnvVar, viaDaemon, viaManual);
183
+
184
+ // Build result object
185
+ const result = {
186
+ happy_mode: isHappyMode,
187
+ detection_method: detectionMethod,
188
+ detected_at: new Date().toISOString(),
189
+ config: {
190
+ verbosity: config.verbosity,
191
+ show_file_stats: config.show_file_stats,
192
+ max_grep_results: config.max_grep_results,
193
+ checkpoint_interval_minutes: config.checkpoint_interval_minutes,
194
+ },
195
+ };
196
+
197
+ // Save state for other hooks
198
+ saveHappyModeState(result);
199
+ markSessionDetected(result);
200
+
201
+ // Log detection result
202
+ if (isHappyMode) {
203
+ console.log(`[happy-mode-detector] Happy mode ENABLED (via ${detectionMethod})`);
204
+ console.log(`[happy-mode-detector] Verbosity: ${config.verbosity}`);
205
+ } else {
206
+ console.log('[happy-mode-detector] Happy mode not detected');
207
+ }
208
+
209
+ return approve();
210
+ } catch (error) {
211
+ console.error(`[happy-mode-detector] Error: ${error.message}`);
212
+ return approve();
213
+ }
214
+ };
@@ -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
+ };