@naarang/ccc 1.2.0-beta.9 → 2.0.0-alpha.1

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 (42) hide show
  1. package/README.md +136 -569
  2. package/bin/ccc.cjs +76 -0
  3. package/dist/index.js +2640 -1
  4. package/dist/ngrok.win32-x64-msvc-zjj4rz8c.node +0 -0
  5. package/dist/scripts/postinstall.cjs +84 -0
  6. package/package.json +32 -25
  7. package/scripts/postinstall.cjs +84 -0
  8. package/LICENSE +0 -44
  9. package/bin/ccc.js +0 -45
  10. package/dist/claude/manager.js +0 -1
  11. package/dist/claude/project-setup.js +0 -1
  12. package/dist/claude/session-manager.js +0 -1
  13. package/dist/claude/session-message-parser.js +0 -1
  14. package/dist/claude/session.js +0 -1
  15. package/dist/claude/stream-parser.js +0 -1
  16. package/dist/firebase/admin.js +0 -1
  17. package/dist/hooks/notification_hook.js +0 -306
  18. package/dist/hooks/package-lock.json +0 -550
  19. package/dist/hooks/package.json +0 -16
  20. package/dist/hooks/permissions_hook.js +0 -657
  21. package/dist/mdns/service.js +0 -1
  22. package/dist/mqtt/client.js +0 -1
  23. package/dist/mqtt-broker.js +0 -1
  24. package/dist/ngrok/manager.js +0 -1
  25. package/dist/notifications/handlers.js +0 -1
  26. package/dist/notifications/index.js +0 -1
  27. package/dist/notifications/manager.js +0 -1
  28. package/dist/notifications/preferences-manager.js +0 -1
  29. package/dist/notifications/preferences-storage.js +0 -1
  30. package/dist/notifications/sender.js +0 -1
  31. package/dist/notifications/storage.js +0 -1
  32. package/dist/notifications/types.js +0 -1
  33. package/dist/proxy/router.js +0 -1
  34. package/dist/public/terminal.html +0 -250
  35. package/dist/qr/generator.js +0 -1
  36. package/dist/terminal/server.js +0 -1
  37. package/dist/types/index.js +0 -1
  38. package/dist/utils/auto-update.js +0 -1
  39. package/dist/utils/logger.js +0 -1
  40. package/dist/utils/version.js +0 -1
  41. package/scripts/check-pty.js +0 -142
  42. package/scripts/obfuscate.js +0 -77
@@ -1,657 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * PreToolUse Hook for Claude Code
5
- *
6
- * This hook intercepts tool calls and requests user permission via MQTT
7
- * before allowing Claude to execute them.
8
- *
9
- * Communication Flow:
10
- * 1. Receives tool call details from Claude via stdin
11
- * 2. Reads session-to-project mapping from sessions.json
12
- * 3. Publishes permission request to MQTT (permissions/{projectId}/request)
13
- * 4. Waits for user response from frontend (permissions/{projectId}/response)
14
- * 5. Returns decision to Claude via stdout
15
- */
16
-
17
- const mqtt = require('mqtt');
18
- const fs = require('fs');
19
- const path = require('path');
20
-
21
- // Load environment variables from .env file in hooks folder (silent mode)
22
- require('dotenv').config({ path: path.join(__dirname, '.env'), quiet: true });
23
-
24
- // ============================================================================
25
- // Configuration
26
- // ============================================================================
27
-
28
- const MQTT_CONFIG = {
29
- host: process.env.MQTT_HOST || 'localhost',
30
- port: parseInt(process.env.MQTT_PORT || '8883', 10),
31
- username: process.env.MQTT_USERNAME || undefined,
32
- password: process.env.MQTT_PASSWORD || undefined,
33
- clientId: `claude-hook-${Math.random().toString(16).substring(2, 10)}`,
34
- qos: parseInt(process.env.MQTT_QOS || '1', 10),
35
- };
36
-
37
- const TIMEOUT_MS = 300000; // 5 minutes default timeout
38
- // Sessions.json is in .claude folder (same folder as hooks folder)
39
- // Hook runs from $CLAUDE_PROJECT_DIR/.claude/hooks, so go up 1 level to .claude
40
- const SESSION_MAPPING_FILE = path.join(__dirname, '..', 'sessions.json');
41
- const SESSION_CONFIG_FILE = path.join(__dirname, '..', 'session-config.json');
42
-
43
- // Tools that modify code/files - only these should trigger acceptEdits mode
44
- const CODE_MODIFYING_TOOLS = ['Edit', 'MultiEdit', 'Write', 'NotebookEdit'];
45
-
46
- // ============================================================================
47
- // Helper Functions
48
- // ============================================================================
49
-
50
- /**
51
- * Logs to stderr (so it doesn't interfere with stdout JSON output)
52
- */
53
- function log(message, data = null) {
54
- const timestamp = new Date().toISOString();
55
- const logMsg = data
56
- ? `[${timestamp}] [PermissionsHook] ${message}: ${JSON.stringify(data)}`
57
- : `[${timestamp}] [PermissionsHook] ${message}`;
58
- console.error(logMsg);
59
- }
60
-
61
- /**
62
- * Reads session-to-project mapping from sessions.json
63
- * Returns projectId for given sessionId
64
- */
65
- function getProjectIdFromSession(sessionId) {
66
- try {
67
- if (!fs.existsSync(SESSION_MAPPING_FILE)) {
68
- log('Session mapping file not found', { path: SESSION_MAPPING_FILE });
69
- return null;
70
- }
71
-
72
- const data = fs.readFileSync(SESSION_MAPPING_FILE, 'utf8');
73
- const mapping = JSON.parse(data);
74
-
75
- if (mapping[sessionId]) {
76
- log('Found project ID for session', { sessionId, projectId: mapping[sessionId] });
77
- return mapping[sessionId];
78
- }
79
-
80
- log('Session ID not found in mapping', { sessionId });
81
- return null;
82
-
83
- } catch (error) {
84
- log('Error reading session mapping', { error: error.message });
85
- return null;
86
- }
87
- }
88
-
89
- /**
90
- * Reads permission mode for given session from session-config.json
91
- * Returns permission mode ('default', 'acceptEdits', 'plan', 'bypassPermissions')
92
- */
93
- function getPermissionModeForSession(sessionId) {
94
- try {
95
- if (!fs.existsSync(SESSION_CONFIG_FILE)) {
96
- log('Session config file not found, using default mode', { path: SESSION_CONFIG_FILE });
97
- return 'default';
98
- }
99
-
100
- const data = fs.readFileSync(SESSION_CONFIG_FILE, 'utf8');
101
- const config = JSON.parse(data);
102
-
103
- if (config[sessionId] && config[sessionId].permissionMode) {
104
- const mode = config[sessionId].permissionMode;
105
- log('Found permission mode for session', { sessionId, permissionMode: mode });
106
- return mode;
107
- }
108
-
109
- log('Session ID not found in config, using default mode', { sessionId });
110
- return 'default';
111
-
112
- } catch (error) {
113
- log('Error reading session config, using default mode', { error: error.message });
114
- return 'default';
115
- }
116
- }
117
-
118
- /**
119
- * Reads the current working directory for a session from session-config.json
120
- * Returns the cwd or null if not found
121
- */
122
- function getCwdForSession(sessionId) {
123
- try {
124
- if (!fs.existsSync(SESSION_CONFIG_FILE)) {
125
- log('Session config file not found, no cwd available', { path: SESSION_CONFIG_FILE });
126
- return null;
127
- }
128
-
129
- const data = fs.readFileSync(SESSION_CONFIG_FILE, 'utf8');
130
- const config = JSON.parse(data);
131
-
132
- if (config[sessionId] && config[sessionId].cwd) {
133
- const cwd = config[sessionId].cwd;
134
- log('Found cwd for session', { sessionId, cwd });
135
- return cwd;
136
- }
137
-
138
- log('No cwd found for session', { sessionId });
139
- return null;
140
-
141
- } catch (error) {
142
- log('Error reading cwd from session config', { error: error.message });
143
- return null;
144
- }
145
- }
146
-
147
- /**
148
- * Checks if a file path is outside the project's current working directory
149
- * Returns true if the file is outside the project directory
150
- */
151
- function isFileOutsideProjectDirectory(filePath, projectCwd) {
152
- try {
153
- if (!filePath || !projectCwd) {
154
- return false;
155
- }
156
-
157
- // Resolve both paths to absolute paths
158
- const absoluteFilePath = path.resolve(filePath);
159
- const absoluteProjectCwd = path.resolve(projectCwd);
160
-
161
- // Check if the file path starts with the project cwd
162
- const isInside = absoluteFilePath.startsWith(absoluteProjectCwd + path.sep) ||
163
- absoluteFilePath === absoluteProjectCwd;
164
-
165
- log('Checking if file is outside project directory', {
166
- filePath: absoluteFilePath,
167
- projectCwd: absoluteProjectCwd,
168
- isOutside: !isInside
169
- });
170
-
171
- return !isInside;
172
-
173
- } catch (error) {
174
- log('Error checking file path', { error: error.message });
175
- // On error, be conservative and ask for permission
176
- return true;
177
- }
178
- }
179
-
180
- /**
181
- * Reads JSON input from stdin
182
- */
183
- function readStdin() {
184
- return new Promise((resolve, reject) => {
185
- let data = '';
186
-
187
- process.stdin.setEncoding('utf8');
188
-
189
- process.stdin.on('data', (chunk) => {
190
- data += chunk;
191
- });
192
-
193
- process.stdin.on('end', () => {
194
- try {
195
- const parsed = JSON.parse(data);
196
- resolve(parsed);
197
- } catch (error) {
198
- reject(new Error(`Failed to parse stdin JSON: ${error.message}`));
199
- }
200
- });
201
-
202
- process.stdin.on('error', (error) => {
203
- reject(new Error(`Error reading stdin: ${error.message}`));
204
- });
205
- });
206
- }
207
-
208
- /**
209
- * Outputs the decision to stdout in the format Claude expects
210
- */
211
- function outputDecision(decision, reason = '') {
212
- const output = {
213
- hookSpecificOutput: {
214
- hookEventName: 'PreToolUse',
215
- permissionDecision: decision,
216
- permissionDecisionReason: reason
217
- }
218
- };
219
-
220
- console.log(JSON.stringify(output));
221
- }
222
-
223
- /**
224
- * Generates a permission string for Claude Code's settings.local.json format
225
- * Format: "ToolName(parameter)" - e.g., "Bash(npm install)", "Edit(/path/to/file.ts)"
226
- */
227
- function generatePermissionString(toolName, toolInput) {
228
- try {
229
- if (!toolInput) {
230
- return toolName;
231
- }
232
-
233
- switch (toolName) {
234
- case 'Bash':
235
- return toolInput.command ? `Bash(${toolInput.command})` : 'Bash';
236
- case 'Edit':
237
- return toolInput.file_path ? `Edit(${toolInput.file_path})` : 'Edit';
238
- case 'Write':
239
- return toolInput.file_path ? `Write(${toolInput.file_path})` : 'Write';
240
- case 'Read':
241
- return toolInput.file_path ? `Read(${toolInput.file_path})` : 'Read';
242
- case 'WebFetch':
243
- return toolInput.url ? `WebFetch(${toolInput.url})` : 'WebFetch';
244
- case 'WebSearch':
245
- return toolInput.query ? `WebSearch(${toolInput.query})` : 'WebSearch';
246
- case 'NotebookEdit':
247
- return toolInput.notebook_path ? `NotebookEdit(${toolInput.notebook_path})` : 'NotebookEdit';
248
- case 'BashOutput':
249
- return 'BashOutput';
250
- default:
251
- return toolName;
252
- }
253
- } catch (error) {
254
- log('Error generating permission string', { error: error.message });
255
- return toolName;
256
- }
257
- }
258
-
259
- /**
260
- * Checks if a tool+command is in the allowed list (settings.local.json)
261
- * Returns true if permission is granted via allowed list
262
- */
263
- function checkAllowedTools(toolName, toolInput) {
264
- try {
265
- const settingsPath = path.join(__dirname, '..', 'settings.local.json');
266
-
267
- if (!fs.existsSync(settingsPath)) {
268
- return false;
269
- }
270
-
271
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
272
-
273
- if (!settings.permissions || !settings.permissions.allow) {
274
- return false;
275
- }
276
-
277
- // Generate the same permission string format used when adding
278
- const permissionString = generatePermissionString(toolName, toolInput);
279
-
280
- // Check if this exact permission exists
281
- const isAllowed = settings.permissions.allow.includes(permissionString);
282
-
283
- if (isAllowed) {
284
- log('Tool+command found in allowed list', { permissionString });
285
- return true;
286
- }
287
-
288
- return false;
289
- } catch (error) {
290
- log('Error checking allowed tools', { error: error.message });
291
- return false;
292
- }
293
- }
294
-
295
- // ============================================================================
296
- // MQTT Communication
297
- // ============================================================================
298
-
299
- /**
300
- * Connects to MQTT broker and waits for permission response
301
- */
302
- async function requestPermission(hookInput, projectId) {
303
- const { tool_name, tool_input, session_id, cwd } = hookInput;
304
-
305
- log('Processing permission request', { tool_name, projectId, session_id });
306
-
307
- return new Promise((resolve, reject) => {
308
- // Use WebSocket protocol for MQTT connection (embedded broker uses ws://)
309
- const connectUrl = `ws://${MQTT_CONFIG.host}:${MQTT_CONFIG.port}`;
310
-
311
- const connectOptions = {
312
- clientId: MQTT_CONFIG.clientId,
313
- clean: true,
314
- reconnectPeriod: 0, // Don't auto-reconnect (we need fast failure)
315
- connectTimeout: 10000,
316
- };
317
-
318
- // Add credentials if provided
319
- if (MQTT_CONFIG.username && MQTT_CONFIG.password) {
320
- connectOptions.username = MQTT_CONFIG.username;
321
- connectOptions.password = MQTT_CONFIG.password;
322
- }
323
-
324
- log('Connecting to MQTT broker', { host: MQTT_CONFIG.host, port: MQTT_CONFIG.port });
325
-
326
- const client = mqtt.connect(connectUrl, connectOptions);
327
- let resolved = false;
328
- let timeoutHandle = null;
329
-
330
- // Topics for communication
331
- const requestTopic = `permissions/${projectId}/request`;
332
- const responseTopic = `permissions/${projectId}/response`;
333
-
334
- /**
335
- * Cleanup and resolve/reject helper
336
- */
337
- function cleanup(callback) {
338
- if (resolved) return;
339
- resolved = true;
340
-
341
- if (timeoutHandle) {
342
- clearTimeout(timeoutHandle);
343
- }
344
-
345
- // Clean disconnect
346
- client.end(true, {}, () => {
347
- callback();
348
- });
349
- }
350
-
351
- // Set timeout for user response
352
- timeoutHandle = setTimeout(() => {
353
- log('Permission request timed out', { timeout: TIMEOUT_MS });
354
- cleanup(() => {
355
- resolve({
356
- decision: 'deny',
357
- reason: `Permission request timed out after ${TIMEOUT_MS / 1000} seconds. No response from user.`
358
- });
359
- });
360
- }, TIMEOUT_MS);
361
-
362
- // Handle connection
363
- client.on('connect', () => {
364
- log('Connected to MQTT broker');
365
-
366
- // Subscribe to response topic
367
- client.subscribe(responseTopic, { qos: MQTT_CONFIG.qos }, (err) => {
368
- if (err) {
369
- log('Failed to subscribe to response topic', { error: err.message });
370
- cleanup(() => {
371
- reject(new Error(`Failed to subscribe: ${err.message}`));
372
- });
373
- return;
374
- }
375
-
376
- log('Subscribed to response topic', { topic: responseTopic });
377
-
378
- // Publish permission request
379
- const permissionRequest = {
380
- tool_name,
381
- tool_input,
382
- session_id,
383
- cwd,
384
- timestamp: Date.now(),
385
- project_id: projectId,
386
- };
387
-
388
- const payload = JSON.stringify(permissionRequest);
389
-
390
- client.publish(requestTopic, payload, { qos: MQTT_CONFIG.qos, retain: false }, (err) => {
391
- if (err) {
392
- log('Failed to publish permission request', { error: err.message });
393
- cleanup(() => {
394
- reject(new Error(`Failed to publish: ${err.message}`));
395
- });
396
- return;
397
- }
398
-
399
- log('Published permission request', { topic: requestTopic });
400
- });
401
- });
402
- });
403
-
404
- // Handle incoming messages (response from frontend)
405
- client.on('message', (topic, payload) => {
406
- if (topic !== responseTopic) return;
407
-
408
- try {
409
- const response = JSON.parse(payload.toString());
410
- log('Received permission response', response);
411
-
412
- // Validate response format
413
- if (!response.decision || !['allow', 'deny'].includes(response.decision)) {
414
- log('Invalid response format', response);
415
- cleanup(() => {
416
- resolve({
417
- decision: 'deny',
418
- reason: 'Invalid response format from frontend'
419
- });
420
- });
421
- return;
422
- }
423
-
424
- // If permission mode is updated in response, save it
425
- if (response.permissionMode) {
426
- log('Updating permission mode from response', {
427
- sessionId: session_id,
428
- newMode: response.permissionMode
429
- });
430
-
431
- try {
432
- const configPath = path.join(__dirname, '..', 'session-config.json');
433
- let config = {};
434
-
435
- if (fs.existsSync(configPath)) {
436
- config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
437
- }
438
-
439
- config[session_id] = { permissionMode: response.permissionMode };
440
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
441
-
442
- log('Permission mode updated successfully', { sessionId: session_id });
443
- } catch (error) {
444
- log('Failed to update permission mode', { error: error.message });
445
- }
446
- }
447
-
448
- // If user clicked "don't ask again", add to allowed tools in settings.local.json
449
- if (response.addToAllowedTools) {
450
- // Generate permission string from the tool_name and tool_input we received earlier
451
- const permissionString = generatePermissionString(tool_name, tool_input);
452
-
453
- log('Adding permission to settings.local.json', {
454
- toolName: tool_name,
455
- permissionString
456
- });
457
-
458
- try {
459
- const settingsPath = path.join(__dirname, '..', 'settings.local.json');
460
- let settings = {};
461
-
462
- // Read existing settings
463
- if (fs.existsSync(settingsPath)) {
464
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
465
- }
466
-
467
- // Initialize permissions structure if needed
468
- if (!settings.permissions) {
469
- settings.permissions = {};
470
- }
471
- if (!settings.permissions.allow) {
472
- settings.permissions.allow = [];
473
- }
474
-
475
- // Add permission string if not already present
476
- if (!settings.permissions.allow.includes(permissionString)) {
477
- settings.permissions.allow.push(permissionString);
478
-
479
- // Write back atomically
480
- const tempPath = `${settingsPath}.tmp`;
481
- fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2), 'utf8');
482
- fs.renameSync(tempPath, settingsPath);
483
-
484
- log('Added permission to settings.local.json', {
485
- permissionString,
486
- totalAllowed: settings.permissions.allow.length
487
- });
488
- } else {
489
- log('Permission already exists in settings.local.json', { permissionString });
490
- }
491
- } catch (error) {
492
- log('Failed to update settings.local.json', { error: error.message });
493
- }
494
- }
495
-
496
- // Valid response received
497
- cleanup(() => {
498
- resolve({
499
- decision: response.decision,
500
- reason: response.reason || `User ${response.decision}ed the tool use`
501
- });
502
- });
503
-
504
- } catch (error) {
505
- log('Failed to parse response', { error: error.message });
506
- cleanup(() => {
507
- resolve({
508
- decision: 'deny',
509
- reason: 'Failed to parse permission response'
510
- });
511
- });
512
- }
513
- });
514
-
515
- // Handle connection errors
516
- client.on('error', (error) => {
517
- log('MQTT connection error', { error: error.message });
518
- cleanup(() => {
519
- reject(new Error(`MQTT error: ${error.message}`));
520
- });
521
- });
522
-
523
- // Handle unexpected disconnection
524
- client.on('close', () => {
525
- if (!resolved) {
526
- log('MQTT connection closed unexpectedly');
527
- cleanup(() => {
528
- reject(new Error('MQTT connection closed before receiving response'));
529
- });
530
- }
531
- });
532
- });
533
- }
534
-
535
- // ============================================================================
536
- // Main Execution
537
- // ============================================================================
538
-
539
- async function main() {
540
- try {
541
- log('Hook started');
542
-
543
- // Read hook input from stdin
544
- const hookInput = await readStdin();
545
- log('Received hook input', hookInput);
546
-
547
- // Validate required fields
548
- if (!hookInput.tool_name || !hookInput.session_id || !hookInput.cwd) {
549
- throw new Error('Missing required fields in hook input');
550
- }
551
-
552
- // Get project ID from session mapping file
553
- let projectId = getProjectIdFromSession(hookInput.session_id);
554
-
555
- // CRITICAL: If session is not in sessions.json, this is a native Claude Code session
556
- // (not managed by CCC backend). Pass through without MQTT check to avoid hanging.
557
- if (!projectId) {
558
- log('Session not found in sessions.json - this is a native Claude Code session');
559
- log('Passing through to use Claude Code\'s native permission system');
560
- process.exit(0);
561
- }
562
-
563
- log('Resolved project ID', { projectId });
564
-
565
- // Get permission mode for this session
566
- const permissionMode = getPermissionModeForSession(hookInput.session_id);
567
- const toolName = hookInput.tool_name;
568
-
569
- log('Permission mode check', { sessionId: hookInput.session_id, permissionMode, toolName });
570
-
571
- // Auto-approval logic based on permission mode
572
- if (permissionMode === 'bypassPermissions') {
573
- log('Auto-approving (bypass mode)');
574
- outputDecision('allow', 'User has approved the tool use (bypass mode)');
575
- process.exit(0);
576
- }
577
-
578
- if (permissionMode === 'acceptEdits') {
579
- // Only auto-approve actual code-modifying tools
580
- if (CODE_MODIFYING_TOOLS.includes(toolName)) {
581
- log('Auto-approving (acceptEdits mode)', { toolName });
582
- outputDecision('allow', `User has approved code-modifying tools`);
583
- process.exit(0);
584
- }
585
- log('Tool not auto-approved in acceptEdits mode, asking user', { toolName });
586
- }
587
-
588
- // Plan mode - will handle separately with special UI
589
- // For now, ask user for all tools in plan mode
590
- if (permissionMode === 'plan') {
591
- log('Plan mode detected - will ask user with special UI');
592
- // TODO: Plan mode will need special handling in frontend
593
- // For now, ask user normally
594
- }
595
-
596
- // Special handling for Read tool - check if file is outside project directory
597
- if (toolName === 'Read') {
598
- const projectCwd = getCwdForSession(hookInput.session_id);
599
- const filePath = hookInput.tool_input?.file_path;
600
-
601
- if (projectCwd && filePath) {
602
- const isOutside = isFileOutsideProjectDirectory(filePath, projectCwd);
603
-
604
- if (!isOutside) {
605
- // File is inside project directory, auto-approve
606
- log('Auto-approving Read (file inside project directory)', { filePath, projectCwd });
607
- outputDecision('allow', 'File is within project directory');
608
- process.exit(0);
609
- } else {
610
- // File is outside project directory, will ask user below
611
- log('Read operation outside project directory, will ask user', { filePath, projectCwd });
612
- }
613
- }
614
- }
615
-
616
- // Check if this specific tool+command is in allowed list
617
- if (checkAllowedTools(toolName, hookInput.tool_input)) {
618
- log('Auto-approving (found in allowed list)');
619
- outputDecision('allow', `User has allowed this tool use previously for auto-approval`);
620
- process.exit(0);
621
- }
622
-
623
- // Default mode or plan mode - ask user via MQTT
624
- log('Requesting permission via MQTT', { mode: permissionMode });
625
- const result = await requestPermission(hookInput, projectId);
626
- log('Permission decision made', result);
627
-
628
- // Output decision to Claude
629
- outputDecision(result.decision, result.reason);
630
-
631
- // Exit with appropriate code
632
- process.exit(0);
633
-
634
- } catch (error) {
635
- log('Hook execution failed', { error: error.message, stack: error.stack });
636
-
637
- // On error, deny the tool use
638
- outputDecision('deny', `Hook error: ${error.message}`);
639
- process.exit(0); // Exit 0 even on error, but with deny decision
640
- }
641
- }
642
-
643
- // Handle process signals
644
- process.on('SIGINT', () => {
645
- log('Received SIGINT, exiting');
646
- outputDecision('deny', 'Hook interrupted by signal');
647
- process.exit(0);
648
- });
649
-
650
- process.on('SIGTERM', () => {
651
- log('Received SIGTERM, exiting');
652
- outputDecision('deny', 'Hook terminated by signal');
653
- process.exit(0);
654
- });
655
-
656
- // Run the hook
657
- main();