@automagik/genie 0.260202.530 → 0.260202.1833

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.
Files changed (39) hide show
  1. package/dist/claudio.js +44 -45
  2. package/dist/genie.js +58 -135
  3. package/dist/term.js +134 -67
  4. package/install.sh +43 -7
  5. package/package.json +1 -1
  6. package/src/claudio.ts +31 -21
  7. package/src/commands/launch.ts +12 -68
  8. package/src/genie-commands/doctor.ts +327 -0
  9. package/src/genie-commands/setup.ts +317 -199
  10. package/src/genie-commands/uninstall.ts +176 -0
  11. package/src/genie.ts +24 -44
  12. package/src/lib/claude-settings.ts +22 -64
  13. package/src/lib/genie-config.ts +169 -57
  14. package/src/lib/orchestrator/completion.ts +392 -0
  15. package/src/lib/orchestrator/event-monitor.ts +442 -0
  16. package/src/lib/orchestrator/index.ts +12 -0
  17. package/src/lib/orchestrator/patterns.ts +277 -0
  18. package/src/lib/orchestrator/state-detector.ts +339 -0
  19. package/src/lib/version.ts +1 -1
  20. package/src/lib/worker-registry.ts +229 -0
  21. package/src/term-commands/close.ts +221 -0
  22. package/src/term-commands/exec.ts +28 -6
  23. package/src/term-commands/kill.ts +143 -0
  24. package/src/term-commands/orchestrate.ts +844 -0
  25. package/src/term-commands/read.ts +6 -1
  26. package/src/term-commands/shortcuts.ts +14 -14
  27. package/src/term-commands/work.ts +415 -0
  28. package/src/term-commands/workers.ts +264 -0
  29. package/src/term.ts +201 -3
  30. package/src/types/genie-config.ts +49 -81
  31. package/src/genie-commands/hooks.ts +0 -317
  32. package/src/lib/hook-script.ts +0 -263
  33. package/src/lib/hooks/compose.ts +0 -72
  34. package/src/lib/hooks/index.ts +0 -163
  35. package/src/lib/hooks/presets/audited.ts +0 -191
  36. package/src/lib/hooks/presets/collaborative.ts +0 -143
  37. package/src/lib/hooks/presets/sandboxed.ts +0 -153
  38. package/src/lib/hooks/presets/supervised.ts +0 -66
  39. package/src/lib/hooks/utils/escape.ts +0 -46
@@ -1,317 +0,0 @@
1
- /**
2
- * Genie Hooks Commands
3
- *
4
- * Commands to install, uninstall, and test hooks in Claude Code.
5
- * Bridges the gap between genie config and Claude Code settings.
6
- */
7
-
8
- import {
9
- loadClaudeSettings,
10
- saveClaudeSettings,
11
- isGenieHookInstalled,
12
- addGenieHook,
13
- removeGenieHook,
14
- getClaudeSettingsPath,
15
- contractClaudePath,
16
- } from '../lib/claude-settings.js';
17
- import {
18
- hookScriptExists,
19
- writeHookScript,
20
- removeHookScript,
21
- testHookScript,
22
- getHookScriptDisplayPath,
23
- } from '../lib/hook-script.js';
24
- import {
25
- loadGenieConfig,
26
- getGenieConfigPath,
27
- } from '../lib/genie-config.js';
28
- import { describeEnabledHooks, hasEnabledHooks } from '../lib/hooks/index.js';
29
- import { checkCommand } from '../lib/system-detect.js';
30
-
31
- /**
32
- * Print a boxed success message
33
- */
34
- function printSuccessBox(lines: string[]): void {
35
- const maxLen = Math.max(...lines.map((l) => l.length));
36
- const width = maxLen + 4;
37
-
38
- console.log();
39
- console.log('\x1b[32m' + '+' + '-'.repeat(width) + '+' + '\x1b[0m');
40
- for (const line of lines) {
41
- const padding = ' '.repeat(maxLen - line.length);
42
- console.log('\x1b[32m' + '| ' + '\x1b[0m' + line + padding + '\x1b[32m |' + '\x1b[0m');
43
- }
44
- console.log('\x1b[32m' + '+' + '-'.repeat(width) + '+' + '\x1b[0m');
45
- console.log();
46
- }
47
-
48
- /**
49
- * Check required dependencies
50
- */
51
- async function checkDependencies(): Promise<{
52
- jq: boolean;
53
- tmux: boolean;
54
- term: boolean;
55
- errors: string[];
56
- }> {
57
- const errors: string[] = [];
58
-
59
- const jqCheck = await checkCommand('jq');
60
- const tmuxCheck = await checkCommand('tmux');
61
- const termCheck = await checkCommand('term');
62
-
63
- if (!jqCheck.exists) {
64
- errors.push('jq is required but not installed. Install with: brew install jq (or apt install jq)');
65
- }
66
-
67
- return {
68
- jq: jqCheck.exists,
69
- tmux: tmuxCheck.exists,
70
- term: termCheck.exists,
71
- errors,
72
- };
73
- }
74
-
75
- /**
76
- * Install hooks into Claude Code
77
- */
78
- export async function installHooksCommand(options: { force?: boolean } = {}): Promise<void> {
79
- console.log();
80
- console.log('\x1b[1m Installing Genie Hooks\x1b[0m');
81
- console.log();
82
-
83
- // Step 1: Check dependencies
84
- console.log('\x1b[2mChecking dependencies...\x1b[0m');
85
- const deps = await checkDependencies();
86
-
87
- if (deps.jq) {
88
- console.log(' \x1b[32m+\x1b[0m jq');
89
- } else {
90
- console.log(' \x1b[31m-\x1b[0m jq (required)');
91
- }
92
-
93
- if (deps.tmux) {
94
- console.log(' \x1b[32m+\x1b[0m tmux');
95
- } else {
96
- console.log(' \x1b[33m-\x1b[0m tmux (recommended for collaborative mode)');
97
- }
98
-
99
- if (deps.term) {
100
- console.log(' \x1b[32m+\x1b[0m term');
101
- } else {
102
- console.log(' \x1b[33m-\x1b[0m term (required for collaborative mode)');
103
- }
104
-
105
- if (deps.errors.length > 0) {
106
- console.log();
107
- for (const error of deps.errors) {
108
- console.log('\x1b[31mError:\x1b[0m ' + error);
109
- }
110
- process.exit(1);
111
- }
112
-
113
- console.log();
114
-
115
- // Step 2: Load genie config and check presets
116
- console.log('\x1b[2mLoading configuration...\x1b[0m');
117
- const config = await loadGenieConfig();
118
-
119
- if (!hasEnabledHooks(config)) {
120
- console.log();
121
- console.log('\x1b[33mNo hook presets are enabled.\x1b[0m');
122
- console.log('Run \x1b[36mgenie setup\x1b[0m to configure hook presets first.');
123
- console.log();
124
- process.exit(1);
125
- }
126
-
127
- const descriptions = describeEnabledHooks(config);
128
- console.log(' \x1b[32m+\x1b[0m Enabled presets: ' + config.hooks.enabled.join(', '));
129
- console.log();
130
-
131
- // Step 3: Check if collaborative preset needs tmux
132
- if (config.hooks.enabled.includes('collaborative')) {
133
- if (!deps.tmux) {
134
- console.log('\x1b[33mWarning:\x1b[0m Collaborative mode enabled but tmux not found.');
135
- console.log('Install tmux for the best experience: brew install tmux (or apt install tmux)');
136
- console.log();
137
- }
138
- if (!deps.term) {
139
- console.log('\x1b[33mWarning:\x1b[0m Collaborative mode enabled but term not found.');
140
- console.log('The term command is required for collaborative mode to work.');
141
- console.log();
142
- }
143
- }
144
-
145
- // Step 4: Check if already installed
146
- const settings = await loadClaudeSettings();
147
- if (isGenieHookInstalled(settings) && !options.force) {
148
- console.log('\x1b[33mGenie hooks are already installed.\x1b[0m');
149
- console.log('Use \x1b[36m--force\x1b[0m to reinstall.');
150
- console.log();
151
- return;
152
- }
153
-
154
- // Step 5: Write the hook script
155
- console.log('\x1b[2mWriting hook script...\x1b[0m');
156
- await writeHookScript();
157
- console.log(' \x1b[32m+\x1b[0m ' + getHookScriptDisplayPath());
158
- console.log();
159
-
160
- // Step 6: Update Claude settings
161
- console.log('\x1b[2mUpdating Claude Code settings...\x1b[0m');
162
- const updatedSettings = addGenieHook(settings);
163
- await saveClaudeSettings(updatedSettings);
164
- console.log(' \x1b[32m+\x1b[0m Hook registered in ' + contractClaudePath(getClaudeSettingsPath()));
165
- console.log();
166
-
167
- // Step 7: Print success
168
- const sessionName = config.hooks.collaborative?.sessionName || 'genie';
169
- printSuccessBox([
170
- '\x1b[32m+\x1b[0m Installation complete!',
171
- '',
172
- 'Restart Claude Code for changes to take effect.',
173
- '',
174
- 'After restart, all Bash commands will run in tmux.',
175
- `Watch with: tmux attach -t ${sessionName}`,
176
- ]);
177
- }
178
-
179
- /**
180
- * Uninstall hooks from Claude Code
181
- */
182
- export async function uninstallHooksCommand(options: { keepScript?: boolean } = {}): Promise<void> {
183
- console.log();
184
- console.log('\x1b[1m Uninstalling Genie Hooks\x1b[0m');
185
- console.log();
186
-
187
- // Step 1: Remove from Claude settings
188
- console.log('\x1b[2mRemoving from Claude Code settings...\x1b[0m');
189
- const settings = await loadClaudeSettings();
190
-
191
- if (!isGenieHookInstalled(settings)) {
192
- console.log(' \x1b[33m-\x1b[0m No genie hooks found in settings');
193
- } else {
194
- const updatedSettings = removeGenieHook(settings);
195
- await saveClaudeSettings(updatedSettings);
196
- console.log(' \x1b[32m+\x1b[0m Hook removed from ' + contractClaudePath(getClaudeSettingsPath()));
197
- }
198
- console.log();
199
-
200
- // Step 2: Remove hook script (unless --keep-script)
201
- if (!options.keepScript) {
202
- console.log('\x1b[2mRemoving hook script...\x1b[0m');
203
- if (hookScriptExists()) {
204
- removeHookScript();
205
- console.log(' \x1b[32m+\x1b[0m Deleted ' + getHookScriptDisplayPath());
206
- } else {
207
- console.log(' \x1b[33m-\x1b[0m Hook script not found');
208
- }
209
- console.log();
210
- } else {
211
- console.log('\x1b[2mKeeping hook script (--keep-script)\x1b[0m');
212
- console.log();
213
- }
214
-
215
- // Step 3: Print success
216
- printSuccessBox([
217
- '\x1b[32m+\x1b[0m Uninstallation complete!',
218
- '',
219
- 'Restart Claude Code for changes to take effect.',
220
- ]);
221
- }
222
-
223
- /**
224
- * Test the hook script
225
- */
226
- export async function testHooksCommand(): Promise<void> {
227
- console.log();
228
- console.log('\x1b[1m Testing Genie Hooks\x1b[0m');
229
- console.log();
230
-
231
- // Check if script exists
232
- if (!hookScriptExists()) {
233
- console.log('\x1b[31mError:\x1b[0m Hook script not found.');
234
- console.log('Run \x1b[36mgenie hooks install\x1b[0m first.');
235
- console.log();
236
- process.exit(1);
237
- }
238
-
239
- console.log('Script location: ' + getHookScriptDisplayPath());
240
- console.log();
241
-
242
- // Run test
243
- console.log('\x1b[2mTesting with sample Bash input...\x1b[0m');
244
- const result = await testHookScript();
245
-
246
- if (result.success) {
247
- console.log('\x1b[32m+\x1b[0m Hook script works correctly!');
248
- console.log();
249
- console.log('\x1b[2mSample output:\x1b[0m');
250
- try {
251
- const parsed = JSON.parse(result.output || '{}');
252
- console.log(JSON.stringify(parsed, null, 2));
253
- } catch {
254
- console.log(result.output);
255
- }
256
- } else {
257
- console.log('\x1b[31m-\x1b[0m Hook script test failed!');
258
- console.log();
259
- console.log('\x1b[31mError:\x1b[0m ' + result.error);
260
- if (result.output) {
261
- console.log();
262
- console.log('\x1b[2mOutput:\x1b[0m');
263
- console.log(result.output);
264
- }
265
- process.exit(1);
266
- }
267
-
268
- console.log();
269
- }
270
-
271
- /**
272
- * Show current hook configuration (moved from setup.ts)
273
- */
274
- export async function showHooksCommand(): Promise<void> {
275
- const config = await loadGenieConfig();
276
- const descriptions = describeEnabledHooks(config);
277
-
278
- console.log();
279
- console.log('\x1b[1m Current Hook Configuration\x1b[0m');
280
- console.log(` Genie config: ${contractClaudePath(getGenieConfigPath())}`);
281
- console.log(` Claude settings: ${contractClaudePath(getClaudeSettingsPath())}`);
282
- console.log();
283
-
284
- // Show enabled presets
285
- if (descriptions.length === 0) {
286
- console.log('\x1b[33m No hook presets enabled.\x1b[0m');
287
- console.log(' Run \x1b[36mgenie setup\x1b[0m to configure hooks.');
288
- } else {
289
- console.log('\x1b[2m Enabled presets:\x1b[0m');
290
- for (const desc of descriptions) {
291
- console.log(` \x1b[32m+\x1b[0m ${desc}`);
292
- }
293
- }
294
- console.log();
295
-
296
- // Show installation status
297
- const settings = await loadClaudeSettings();
298
- const installed = isGenieHookInstalled(settings);
299
- const scriptExists = hookScriptExists();
300
-
301
- console.log('\x1b[2m Installation status:\x1b[0m');
302
- if (installed && scriptExists) {
303
- console.log(' \x1b[32m+\x1b[0m Hooks installed in Claude Code');
304
- console.log(' \x1b[32m+\x1b[0m Hook script exists');
305
- } else if (installed && !scriptExists) {
306
- console.log(' \x1b[33m!\x1b[0m Hook registered but script missing');
307
- console.log(' Run \x1b[36mgenie hooks install --force\x1b[0m to fix');
308
- } else if (!installed && scriptExists) {
309
- console.log(' \x1b[33m!\x1b[0m Script exists but hook not registered');
310
- console.log(' Run \x1b[36mgenie hooks install\x1b[0m to register');
311
- } else {
312
- console.log(' \x1b[33m-\x1b[0m Hooks not installed');
313
- console.log(' Run \x1b[36mgenie hooks install\x1b[0m to install');
314
- }
315
-
316
- console.log();
317
- }
@@ -1,263 +0,0 @@
1
- /**
2
- * Hook Script Manager
3
- *
4
- * Generates and manages the bash hook script at ~/.claude/hooks/genie-bash-hook.sh
5
- * This script intercepts Bash tool calls and rewrites them to go through term exec.
6
- */
7
-
8
- import { existsSync, unlinkSync, writeFileSync, chmodSync } from 'fs';
9
- import {
10
- ensureClaudeHooksDir,
11
- getGenieHookScriptPath,
12
- contractClaudePath,
13
- } from './claude-settings.js';
14
- import { loadGenieConfig } from './genie-config.js';
15
-
16
- /**
17
- * Check if the hook script exists
18
- */
19
- export function hookScriptExists(): boolean {
20
- return existsSync(getGenieHookScriptPath());
21
- }
22
-
23
- /**
24
- * Get the hook script path (re-export for convenience)
25
- */
26
- export function getHookScriptPath(): string {
27
- return getGenieHookScriptPath();
28
- }
29
-
30
- /**
31
- * Get the hook script path for display (with ~ for home)
32
- */
33
- export function getHookScriptDisplayPath(): string {
34
- return contractClaudePath(getGenieHookScriptPath());
35
- }
36
-
37
- /**
38
- * Generate the hook script content
39
- *
40
- * The script:
41
- * 1. Reads JSON from stdin (Claude Code sends hook event)
42
- * 2. Extracts the command from the Bash tool input
43
- * 3. Wraps the command in `term exec genie:shell '...'`
44
- * 4. Outputs JSON in Claude Code's expected format
45
- */
46
- export async function generateHookScript(): Promise<string> {
47
- // Load config to get session/window names
48
- const config = await loadGenieConfig();
49
- const sessionName = config.hooks.collaborative?.sessionName || 'genie';
50
- const windowName = config.hooks.collaborative?.windowName || 'shell';
51
- const target = `${sessionName}:${windowName}`;
52
-
53
- // The bash script that will be executed by Claude Code
54
- // Uses jq to parse and transform the JSON
55
- const script = `#!/bin/bash
56
- #
57
- # Genie Bash Hook for Claude Code
58
- #
59
- # This hook intercepts Bash tool calls and rewrites them to execute
60
- # through tmux via 'term exec', making all terminal operations visible
61
- # in a shared tmux session.
62
- #
63
- # Generated by: genie hooks install
64
- # Target session: ${target}
65
- #
66
-
67
- # Read the entire input from stdin
68
- INPUT=$(cat)
69
-
70
- # Extract the tool name to verify this is a Bash call
71
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
72
-
73
- # If not a Bash tool call, allow it to proceed unchanged
74
- if [ "$TOOL_NAME" != "Bash" ]; then
75
- echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
76
- exit 0
77
- fi
78
-
79
- # Extract the command from the tool input
80
- COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
81
-
82
- # If no command, allow it (shouldn't happen but be safe)
83
- if [ -z "$COMMAND" ]; then
84
- echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
85
- exit 0
86
- fi
87
-
88
- # Don't wrap commands that are already term exec calls (avoid infinite recursion)
89
- if [[ "$COMMAND" == *"term exec "* ]] || [[ "$COMMAND" == *"/term.js exec"* ]] || [[ "$COMMAND" == *".local/bin/term "* ]] || [[ "$COMMAND" == *".genie/bin/term"* ]]; then
90
- echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
91
- exit 0
92
- fi
93
-
94
- # Escape single quotes in the command for safe shell embedding
95
- # Replace ' with '\\'\\''
96
- ESCAPED_COMMAND=$(echo "$COMMAND" | sed "s/'/'\\\\'\\\\''/" )
97
-
98
- # Build the wrapped command that goes through term exec
99
- # Uses full path since Claude's subprocess may not have full PATH
100
- WRAPPED_COMMAND="\$HOME/.local/bin/term exec ${sessionName}:\\\${CLAUDIO_SESSION:-claude} '\${ESCAPED_COMMAND}'"
101
-
102
- # Extract other fields from the original input to preserve them
103
- TIMEOUT=$(echo "$INPUT" | jq -r '.tool_input.timeout // empty')
104
- DESCRIPTION=$(echo "$INPUT" | jq -r '.tool_input.description // empty')
105
- RUN_IN_BACKGROUND=$(echo "$INPUT" | jq -r '.tool_input.run_in_background // empty')
106
-
107
- # Build the updated input object
108
- UPDATED_INPUT=$(jq -n \\
109
- --arg cmd "$WRAPPED_COMMAND" \\
110
- --arg timeout "$TIMEOUT" \\
111
- --arg desc "$DESCRIPTION" \\
112
- --arg background "$RUN_IN_BACKGROUND" \\
113
- '{
114
- command: $cmd,
115
- timeout: (if $timeout != "" then ($timeout | tonumber) else null end),
116
- description: (if $desc != "" then ("[term] " + $desc) else null end),
117
- run_in_background: (if $background == "true" then true elif $background == "false" then false else null end)
118
- } | with_entries(select(.value != null))')
119
-
120
- # Output the hook response in the correct format
121
- jq -n \\
122
- --argjson updatedInput "$UPDATED_INPUT" \\
123
- '{
124
- "hookSpecificOutput": {
125
- "hookEventName": "PreToolUse",
126
- "permissionDecision": "allow",
127
- "updatedInput": $updatedInput,
128
- "additionalContext": "Human can observe: tmux attach -t ${sessionName}"
129
- }
130
- }'
131
- `;
132
-
133
- return script;
134
- }
135
-
136
- /**
137
- * Write the hook script to disk
138
- */
139
- export async function writeHookScript(): Promise<void> {
140
- ensureClaudeHooksDir();
141
-
142
- const scriptPath = getGenieHookScriptPath();
143
- const scriptContent = await generateHookScript();
144
-
145
- writeFileSync(scriptPath, scriptContent, 'utf-8');
146
-
147
- // Make the script executable
148
- chmodSync(scriptPath, 0o755);
149
- }
150
-
151
- /**
152
- * Remove the hook script from disk
153
- */
154
- export function removeHookScript(): void {
155
- const scriptPath = getGenieHookScriptPath();
156
- if (existsSync(scriptPath)) {
157
- unlinkSync(scriptPath);
158
- }
159
- }
160
-
161
- /**
162
- * Test the hook script with sample input
163
- * Returns { success: boolean, output?: string, error?: string }
164
- */
165
- export async function testHookScript(): Promise<{
166
- success: boolean;
167
- output?: string;
168
- error?: string;
169
- }> {
170
- const scriptPath = getGenieHookScriptPath();
171
-
172
- if (!existsSync(scriptPath)) {
173
- return {
174
- success: false,
175
- error: 'Hook script not found. Run "genie hooks install" first.',
176
- };
177
- }
178
-
179
- // Sample input that mimics what Claude Code sends
180
- const sampleInput = JSON.stringify({
181
- hook_event_name: 'PreToolUse',
182
- tool_name: 'Bash',
183
- tool_input: {
184
- command: 'echo "Hello, World!"',
185
- description: 'Test command',
186
- },
187
- });
188
-
189
- try {
190
- const { $ } = await import('bun');
191
-
192
- // Run the script with sample input
193
- const result = await $`echo ${sampleInput} | ${scriptPath}`.quiet();
194
-
195
- if (result.exitCode !== 0) {
196
- return {
197
- success: false,
198
- error: `Script exited with code ${result.exitCode}`,
199
- output: result.stderr.toString(),
200
- };
201
- }
202
-
203
- const output = result.stdout.toString().trim();
204
-
205
- // Try to parse the output as JSON
206
- try {
207
- const parsed = JSON.parse(output);
208
-
209
- // Validate the structure
210
- if (!parsed.hookSpecificOutput) {
211
- return {
212
- success: false,
213
- error: 'Output missing hookSpecificOutput field',
214
- output,
215
- };
216
- }
217
-
218
- if (parsed.hookSpecificOutput.hookEventName !== 'PreToolUse') {
219
- return {
220
- success: false,
221
- error: 'Output has wrong hookEventName',
222
- output,
223
- };
224
- }
225
-
226
- if (!['allow', 'deny', 'ask'].includes(parsed.hookSpecificOutput.permissionDecision)) {
227
- return {
228
- success: false,
229
- error: 'Output has invalid permissionDecision',
230
- output,
231
- };
232
- }
233
-
234
- // Check that the command was wrapped
235
- if (parsed.hookSpecificOutput.updatedInput?.command) {
236
- const cmd = parsed.hookSpecificOutput.updatedInput.command;
237
- if (!cmd.includes('term exec')) {
238
- return {
239
- success: false,
240
- error: 'Command was not wrapped with term exec',
241
- output,
242
- };
243
- }
244
- }
245
-
246
- return {
247
- success: true,
248
- output,
249
- };
250
- } catch (parseError: any) {
251
- return {
252
- success: false,
253
- error: `Output is not valid JSON: ${parseError.message}`,
254
- output,
255
- };
256
- }
257
- } catch (error: any) {
258
- return {
259
- success: false,
260
- error: `Failed to run hook script: ${error.message}`,
261
- };
262
- }
263
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * Hook Composition Utilities
3
- *
4
- * Functions for merging multiple hook configurations together.
5
- */
6
-
7
- import type { HookConfig } from './presets/collaborative.js';
8
-
9
- /**
10
- * Deep merge two hook configurations
11
- *
12
- * Hooks are composed by concatenating their hook arrays.
13
- * This means all hooks from both configs will run in order.
14
- */
15
- export function mergeHooks(base: HookConfig, override: HookConfig): HookConfig {
16
- const result: HookConfig = { ...base };
17
-
18
- // Merge PreToolUse hooks
19
- if (override.PreToolUse) {
20
- result.PreToolUse = [...(base.PreToolUse || []), ...override.PreToolUse];
21
- }
22
-
23
- // Merge PostToolUse hooks
24
- if (override.PostToolUse) {
25
- result.PostToolUse = [...(base.PostToolUse || []), ...override.PostToolUse];
26
- }
27
-
28
- // Merge PermissionRequest hooks
29
- if (override.PermissionRequest) {
30
- result.PermissionRequest = [...(base.PermissionRequest || []), ...override.PermissionRequest];
31
- }
32
-
33
- return result;
34
- }
35
-
36
- /**
37
- * Compose multiple hook configurations into one
38
- *
39
- * Hooks are executed in the order they appear in the array.
40
- * First preset's hooks run first.
41
- */
42
- export function composeHooks(...configs: HookConfig[]): HookConfig {
43
- return configs.reduce((acc, config) => mergeHooks(acc, config), {});
44
- }
45
-
46
- /**
47
- * Check if a hook config is empty (no hooks defined)
48
- */
49
- export function isEmptyHookConfig(config: HookConfig): boolean {
50
- return (
51
- (!config.PreToolUse || config.PreToolUse.length === 0) &&
52
- (!config.PostToolUse || config.PostToolUse.length === 0) &&
53
- (!config.PermissionRequest || config.PermissionRequest.length === 0)
54
- );
55
- }
56
-
57
- /**
58
- * Count total number of hooks in a config
59
- */
60
- export function countHooks(config: HookConfig): number {
61
- let count = 0;
62
- if (config.PreToolUse) {
63
- count += config.PreToolUse.reduce((sum, entry) => sum + entry.hooks.length, 0);
64
- }
65
- if (config.PostToolUse) {
66
- count += config.PostToolUse.reduce((sum, entry) => sum + entry.hooks.length, 0);
67
- }
68
- if (config.PermissionRequest) {
69
- count += config.PermissionRequest.reduce((sum, entry) => sum + entry.hooks.length, 0);
70
- }
71
- return count;
72
- }