ccmanager 2.11.6 → 3.0.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.
Files changed (34) hide show
  1. package/dist/components/Configuration.js +14 -0
  2. package/dist/components/ConfigureCustomCommand.d.ts +9 -0
  3. package/dist/components/ConfigureCustomCommand.js +44 -0
  4. package/dist/components/ConfigureOther.d.ts +6 -0
  5. package/dist/components/ConfigureOther.js +87 -0
  6. package/dist/components/ConfigureOther.test.d.ts +1 -0
  7. package/dist/components/ConfigureOther.test.js +80 -0
  8. package/dist/components/ConfigureStatusHooks.js +7 -1
  9. package/dist/components/CustomCommandSummary.d.ts +6 -0
  10. package/dist/components/CustomCommandSummary.js +10 -0
  11. package/dist/components/Menu.recent-projects.test.js +2 -0
  12. package/dist/components/Menu.test.js +2 -0
  13. package/dist/components/Session.d.ts +2 -2
  14. package/dist/components/Session.js +67 -4
  15. package/dist/constants/statusIcons.d.ts +3 -1
  16. package/dist/constants/statusIcons.js +3 -0
  17. package/dist/services/autoApprovalVerifier.d.ts +25 -0
  18. package/dist/services/autoApprovalVerifier.js +265 -0
  19. package/dist/services/autoApprovalVerifier.test.d.ts +1 -0
  20. package/dist/services/autoApprovalVerifier.test.js +120 -0
  21. package/dist/services/configurationManager.d.ts +7 -0
  22. package/dist/services/configurationManager.js +35 -0
  23. package/dist/services/sessionManager.autoApproval.test.d.ts +1 -0
  24. package/dist/services/sessionManager.autoApproval.test.js +160 -0
  25. package/dist/services/sessionManager.d.ts +5 -0
  26. package/dist/services/sessionManager.js +149 -1
  27. package/dist/services/sessionManager.statePersistence.test.js +2 -0
  28. package/dist/services/sessionManager.test.js +6 -0
  29. package/dist/types/index.d.ts +14 -1
  30. package/dist/utils/hookExecutor.test.js +8 -0
  31. package/dist/utils/logger.d.ts +83 -14
  32. package/dist/utils/logger.js +218 -17
  33. package/dist/utils/worktreeUtils.test.js +1 -0
  34. package/package.json +1 -1
@@ -9,8 +9,11 @@ import { createStateDetector } from './stateDetector.js';
9
9
  import { STATE_PERSISTENCE_DURATION_MS, STATE_CHECK_INTERVAL_MS, } from '../constants/statePersistence.js';
10
10
  import { Effect } from 'effect';
11
11
  import { ProcessError, ConfigError } from '../types/errors.js';
12
+ import { autoApprovalVerifier } from './autoApprovalVerifier.js';
13
+ import { logger } from '../utils/logger.js';
12
14
  const { Terminal } = pkg;
13
15
  const execAsync = promisify(exec);
16
+ const TERMINAL_CONTENT_MAX_LINES = 300;
14
17
  export class SessionManager extends EventEmitter {
15
18
  async spawn(command, args, worktreePath) {
16
19
  const spawnOptions = {
@@ -26,7 +29,104 @@ export class SessionManager extends EventEmitter {
26
29
  // Create a detector based on the session's detection strategy
27
30
  const strategy = session.detectionStrategy || 'claude';
28
31
  const detector = createStateDetector(strategy);
29
- return detector.detectState(session.terminal, session.state);
32
+ const detectedState = detector.detectState(session.terminal, session.state);
33
+ // If auto-approval is enabled and state is waiting_input, convert to pending_auto_approval
34
+ if (detectedState === 'waiting_input' &&
35
+ configurationManager.isAutoApprovalEnabled() &&
36
+ !session.autoApprovalFailed) {
37
+ return 'pending_auto_approval';
38
+ }
39
+ return detectedState;
40
+ }
41
+ getTerminalContent(session) {
42
+ const buffer = session.terminal.buffer.active;
43
+ const lines = [];
44
+ // Start from the bottom and work our way up
45
+ for (let i = buffer.length - 1; i >= 0 && lines.length < TERMINAL_CONTENT_MAX_LINES; i--) {
46
+ const line = buffer.getLine(i);
47
+ if (line) {
48
+ const text = line.translateToString(true);
49
+ // Skip empty lines at the bottom
50
+ if (lines.length > 0 || text.trim() !== '') {
51
+ lines.unshift(text);
52
+ }
53
+ }
54
+ }
55
+ return lines.join('\n');
56
+ }
57
+ handleAutoApproval(session) {
58
+ // Cancel any existing verification before starting a new one
59
+ this.cancelAutoApprovalVerification(session, 'Restarting verification for pending auto-approval state');
60
+ const abortController = new AbortController();
61
+ session.autoApprovalAbortController = abortController;
62
+ session.autoApprovalReason = undefined;
63
+ // Get terminal content for verification
64
+ const terminalContent = this.getTerminalContent(session);
65
+ // Verify if permission is needed
66
+ void Effect.runPromise(autoApprovalVerifier.verifyNeedsPermission(terminalContent, {
67
+ signal: abortController.signal,
68
+ }))
69
+ .then(autoApprovalResult => {
70
+ if (abortController.signal.aborted) {
71
+ logger.debug(`[${session.id}] Auto-approval verification aborted before completion`);
72
+ return;
73
+ }
74
+ // If state already moved away, skip handling
75
+ if (session.state !== 'pending_auto_approval') {
76
+ logger.debug(`[${session.id}] Skipping auto-approval handling; current state is ${session.state}`);
77
+ return;
78
+ }
79
+ if (autoApprovalResult.needsPermission) {
80
+ // Change state to waiting_input to ask for user permission
81
+ logger.info(`[${session.id}] Auto-approval verification determined user permission needed`);
82
+ session.state = 'waiting_input';
83
+ session.autoApprovalFailed = true;
84
+ session.autoApprovalReason = autoApprovalResult.reason;
85
+ session.pendingState = undefined;
86
+ session.pendingStateStart = undefined;
87
+ this.emit('sessionStateChanged', session);
88
+ }
89
+ else {
90
+ // Auto-approve by simulating Enter key press
91
+ logger.info(`[${session.id}] Auto-approval granted, simulating user permission`);
92
+ session.autoApprovalReason = undefined;
93
+ session.process.write('\r');
94
+ }
95
+ })
96
+ .catch((error) => {
97
+ if (abortController.signal.aborted) {
98
+ logger.debug(`[${session.id}] Auto-approval verification aborted (${error?.message ?? 'aborted'})`);
99
+ return;
100
+ }
101
+ // On failure, fall back to requiring explicit permission
102
+ logger.error(`[${session.id}] Auto-approval verification failed, requiring user permission`, error);
103
+ if (session.state === 'pending_auto_approval') {
104
+ session.state = 'waiting_input';
105
+ session.autoApprovalFailed = true;
106
+ session.autoApprovalReason =
107
+ error?.message ??
108
+ 'Auto-approval verification failed';
109
+ session.pendingState = undefined;
110
+ session.pendingStateStart = undefined;
111
+ this.emit('sessionStateChanged', session);
112
+ }
113
+ })
114
+ .finally(() => {
115
+ if (session.autoApprovalAbortController === abortController) {
116
+ session.autoApprovalAbortController = undefined;
117
+ }
118
+ });
119
+ }
120
+ cancelAutoApprovalVerification(session, reason) {
121
+ const controller = session.autoApprovalAbortController;
122
+ if (!controller) {
123
+ return;
124
+ }
125
+ if (!controller.signal.aborted) {
126
+ controller.abort();
127
+ }
128
+ session.autoApprovalAbortController = undefined;
129
+ logger.info(`[${session.id}] Cancelled auto-approval verification: ${reason}`);
30
130
  }
31
131
  constructor() {
32
132
  super();
@@ -81,6 +181,9 @@ export class SessionManager extends EventEmitter {
81
181
  devcontainerConfig: options.devcontainerConfig ?? undefined,
82
182
  pendingState: undefined,
83
183
  pendingStateStart: undefined,
184
+ autoApprovalFailed: false,
185
+ autoApprovalReason: undefined,
186
+ autoApprovalAbortController: undefined,
84
187
  };
85
188
  // Set up persistent background data handler for state detection
86
189
  this.setupBackgroundHandler(session);
@@ -266,6 +369,22 @@ export class SessionManager extends EventEmitter {
266
369
  session.state = detectedState;
267
370
  session.pendingState = undefined;
268
371
  session.pendingStateStart = undefined;
372
+ if (session.autoApprovalAbortController &&
373
+ detectedState !== 'pending_auto_approval') {
374
+ this.cancelAutoApprovalVerification(session, `state changed to ${detectedState}`);
375
+ }
376
+ // If we previously blocked auto-approval and have moved out of a user prompt,
377
+ // allow future auto-approval attempts.
378
+ if (session.autoApprovalFailed &&
379
+ detectedState !== 'waiting_input' &&
380
+ detectedState !== 'pending_auto_approval') {
381
+ session.autoApprovalFailed = false;
382
+ session.autoApprovalReason = undefined;
383
+ }
384
+ // Handle auto-approval if state is pending_auto_approval
385
+ if (detectedState === 'pending_auto_approval') {
386
+ this.handleAutoApproval(session);
387
+ }
269
388
  // Execute status hook asynchronously (non-blocking) using Effect
270
389
  void Effect.runPromise(executeStatusHook(oldState, detectedState, session));
271
390
  this.emit('sessionStateChanged', session);
@@ -282,6 +401,9 @@ export class SessionManager extends EventEmitter {
282
401
  this.setupExitHandler(session);
283
402
  }
284
403
  cleanupSession(session) {
404
+ if (session.autoApprovalAbortController) {
405
+ this.cancelAutoApprovalVerification(session, 'Session cleanup');
406
+ }
285
407
  // Clear the state check interval
286
408
  if (session.stateCheckInterval) {
287
409
  clearInterval(session.stateCheckInterval);
@@ -313,9 +435,31 @@ export class SessionManager extends EventEmitter {
313
435
  }
314
436
  }
315
437
  }
438
+ cancelAutoApproval(worktreePath, reason = 'User input received') {
439
+ const session = this.sessions.get(worktreePath);
440
+ if (!session) {
441
+ return;
442
+ }
443
+ if (session.state !== 'pending_auto_approval' &&
444
+ !session.autoApprovalAbortController) {
445
+ return;
446
+ }
447
+ this.cancelAutoApprovalVerification(session, reason);
448
+ session.autoApprovalFailed = true;
449
+ session.autoApprovalReason = reason;
450
+ session.pendingState = undefined;
451
+ session.pendingStateStart = undefined;
452
+ if (session.state === 'pending_auto_approval') {
453
+ session.state = 'waiting_input';
454
+ this.emit('sessionStateChanged', session);
455
+ }
456
+ }
316
457
  destroySession(worktreePath) {
317
458
  const session = this.sessions.get(worktreePath);
318
459
  if (session) {
460
+ if (session.autoApprovalAbortController) {
461
+ this.cancelAutoApprovalVerification(session, 'Session destroyed');
462
+ }
319
463
  // Clear the state check interval
320
464
  if (session.stateCheckInterval) {
321
465
  clearInterval(session.stateCheckInterval);
@@ -493,6 +637,7 @@ export class SessionManager extends EventEmitter {
493
637
  idle: 0,
494
638
  busy: 0,
495
639
  waiting_input: 0,
640
+ pending_auto_approval: 0,
496
641
  total: sessions.length,
497
642
  };
498
643
  sessions.forEach(session => {
@@ -506,6 +651,9 @@ export class SessionManager extends EventEmitter {
506
651
  case 'waiting_input':
507
652
  counts.waiting_input++;
508
653
  break;
654
+ case 'pending_auto_approval':
655
+ counts.pending_auto_approval++;
656
+ break;
509
657
  }
510
658
  });
511
659
  return counts;
@@ -36,6 +36,8 @@ vi.mock('./configurationManager.js', () => ({
36
36
  setWorktreeLastOpened: vi.fn(),
37
37
  getWorktreeLastOpenedTime: vi.fn(),
38
38
  getWorktreeLastOpened: vi.fn(() => ({})),
39
+ isAutoApprovalEnabled: vi.fn(() => false),
40
+ setAutoApprovalEnabled: vi.fn(),
39
41
  },
40
42
  }));
41
43
  describe('SessionManager - State Persistence', () => {
@@ -22,6 +22,8 @@ vi.mock('./configurationManager.js', () => ({
22
22
  setWorktreeLastOpened: vi.fn(),
23
23
  getWorktreeLastOpenedTime: vi.fn(),
24
24
  getWorktreeLastOpened: vi.fn(() => ({})),
25
+ isAutoApprovalEnabled: vi.fn(() => false),
26
+ setAutoApprovalEnabled: vi.fn(),
25
27
  },
26
28
  }));
27
29
  // Mock Terminal
@@ -743,6 +745,7 @@ describe('SessionManager', () => {
743
745
  idle: 1,
744
746
  busy: 2,
745
747
  waiting_input: 1,
748
+ pending_auto_approval: 0,
746
749
  total: 4,
747
750
  };
748
751
  const formatted = SessionManager.formatSessionCounts(counts);
@@ -753,6 +756,7 @@ describe('SessionManager', () => {
753
756
  idle: 2,
754
757
  busy: 0,
755
758
  waiting_input: 1,
759
+ pending_auto_approval: 0,
756
760
  total: 3,
757
761
  };
758
762
  const formatted = SessionManager.formatSessionCounts(counts);
@@ -763,6 +767,7 @@ describe('SessionManager', () => {
763
767
  idle: 0,
764
768
  busy: 3,
765
769
  waiting_input: 0,
770
+ pending_auto_approval: 0,
766
771
  total: 3,
767
772
  };
768
773
  const formatted = SessionManager.formatSessionCounts(counts);
@@ -773,6 +778,7 @@ describe('SessionManager', () => {
773
778
  idle: 0,
774
779
  busy: 0,
775
780
  waiting_input: 0,
781
+ pending_auto_approval: 0,
776
782
  total: 0,
777
783
  };
778
784
  const formatted = SessionManager.formatSessionCounts(counts);
@@ -2,7 +2,7 @@ import { IPty } from 'node-pty';
2
2
  import type pkg from '@xterm/headless';
3
3
  import { GitStatus } from '../utils/gitStatus.js';
4
4
  export type Terminal = InstanceType<typeof pkg.Terminal>;
5
- export type SessionState = 'idle' | 'busy' | 'waiting_input';
5
+ export type SessionState = 'idle' | 'busy' | 'waiting_input' | 'pending_auto_approval';
6
6
  export type StateDetectionStrategy = 'claude' | 'gemini' | 'codex' | 'cursor' | 'github-copilot' | 'cline';
7
7
  export interface Worktree {
8
8
  path: string;
@@ -29,12 +29,20 @@ export interface Session {
29
29
  devcontainerConfig: DevcontainerConfig | undefined;
30
30
  pendingState: SessionState | undefined;
31
31
  pendingStateStart: number | undefined;
32
+ autoApprovalFailed: boolean;
33
+ autoApprovalReason?: string;
34
+ autoApprovalAbortController?: AbortController;
35
+ }
36
+ export interface AutoApprovalResponse {
37
+ needsPermission: boolean;
38
+ reason?: string;
32
39
  }
33
40
  export interface SessionManager {
34
41
  sessions: Map<string, Session>;
35
42
  getSession(worktreePath: string): Session | undefined;
36
43
  destroySession(worktreePath: string): void;
37
44
  getAllSessions(): Session[];
45
+ cancelAutoApproval(worktreePath: string, reason?: string): void;
38
46
  }
39
47
  export interface ShortcutKey {
40
48
  ctrl?: boolean;
@@ -55,6 +63,7 @@ export interface StatusHookConfig {
55
63
  idle?: StatusHook;
56
64
  busy?: StatusHook;
57
65
  waiting_input?: StatusHook;
66
+ pending_auto_approval?: StatusHook;
58
67
  }
59
68
  export interface WorktreeHook {
60
69
  command: string;
@@ -98,6 +107,10 @@ export interface ConfigurationData {
98
107
  worktree?: WorktreeConfig;
99
108
  command?: CommandConfig;
100
109
  commandPresets?: CommandPresetsConfig;
110
+ autoApproval?: {
111
+ enabled: boolean;
112
+ customCommand?: string;
113
+ };
101
114
  }
102
115
  export interface GitProject {
103
116
  name: string;
@@ -272,6 +272,7 @@ describe('hookExecutor Integration Tests', () => {
272
272
  pendingStateStart: undefined,
273
273
  lastActivity: new Date(),
274
274
  isActive: true,
275
+ autoApprovalFailed: false,
275
276
  };
276
277
  // Mock WorktreeService to return a worktree with the tmpDir path
277
278
  vi.mocked(WorktreeService).mockImplementation(() => ({
@@ -292,6 +293,7 @@ describe('hookExecutor Integration Tests', () => {
292
293
  },
293
294
  idle: { enabled: false, command: '' },
294
295
  waiting_input: { enabled: false, command: '' },
296
+ pending_auto_approval: { enabled: false, command: '' },
295
297
  });
296
298
  try {
297
299
  // Act - execute the hook and await it
@@ -325,6 +327,7 @@ describe('hookExecutor Integration Tests', () => {
325
327
  pendingStateStart: undefined,
326
328
  lastActivity: new Date(),
327
329
  isActive: true,
330
+ autoApprovalFailed: false,
328
331
  };
329
332
  // Mock WorktreeService to return a worktree with the tmpDir path
330
333
  vi.mocked(WorktreeService).mockImplementation(() => ({
@@ -345,6 +348,7 @@ describe('hookExecutor Integration Tests', () => {
345
348
  },
346
349
  idle: { enabled: false, command: '' },
347
350
  waiting_input: { enabled: false, command: '' },
351
+ pending_auto_approval: { enabled: false, command: '' },
348
352
  });
349
353
  try {
350
354
  // Act & Assert - should not throw even when hook fails
@@ -376,6 +380,7 @@ describe('hookExecutor Integration Tests', () => {
376
380
  pendingStateStart: undefined,
377
381
  lastActivity: new Date(),
378
382
  isActive: true,
383
+ autoApprovalFailed: false,
379
384
  };
380
385
  // Mock WorktreeService to return a worktree with the tmpDir path
381
386
  vi.mocked(WorktreeService).mockImplementation(() => ({
@@ -396,6 +401,7 @@ describe('hookExecutor Integration Tests', () => {
396
401
  },
397
402
  idle: { enabled: false, command: '' },
398
403
  waiting_input: { enabled: false, command: '' },
404
+ pending_auto_approval: { enabled: false, command: '' },
399
405
  });
400
406
  try {
401
407
  // Act
@@ -429,6 +435,7 @@ describe('hookExecutor Integration Tests', () => {
429
435
  pendingStateStart: undefined,
430
436
  lastActivity: new Date(),
431
437
  isActive: true,
438
+ autoApprovalFailed: false,
432
439
  };
433
440
  // Mock WorktreeService to fail with GitError
434
441
  vi.mocked(WorktreeService).mockImplementation(() => ({
@@ -446,6 +453,7 @@ describe('hookExecutor Integration Tests', () => {
446
453
  },
447
454
  idle: { enabled: false, command: '' },
448
455
  waiting_input: { enabled: false, command: '' },
456
+ pending_auto_approval: { enabled: false, command: '' },
449
457
  });
450
458
  try {
451
459
  // Act - should not throw even when getWorktreesEffect fails
@@ -1,14 +1,83 @@
1
- export declare const log: {
2
- log: (...args: unknown[]) => void;
3
- info: (...args: unknown[]) => void;
4
- warn: (...args: unknown[]) => void;
5
- error: (...args: unknown[]) => void;
6
- debug: (...args: unknown[]) => void;
7
- };
8
- export declare const logger: {
9
- log: (...args: unknown[]) => void;
10
- info: (...args: unknown[]) => void;
11
- warn: (...args: unknown[]) => void;
12
- error: (...args: unknown[]) => void;
13
- debug: (...args: unknown[]) => void;
14
- };
1
+ /**
2
+ * Logger configuration with size management and log rotation
3
+ */
4
+ interface LoggerConfig {
5
+ /** Maximum log file size in bytes (default: 5MB) */
6
+ maxSizeBytes: number;
7
+ /** Number of old logs to keep (default: 3) */
8
+ maxRotatedFiles: number;
9
+ /** Enable console output for errors (default: true) */
10
+ logErrorsToConsole: boolean;
11
+ }
12
+ /**
13
+ * CLI-optimized logger with size management and rotation
14
+ *
15
+ * Features:
16
+ * - Automatic log rotation when file exceeds max size
17
+ * - Configurable retention (3 rotated files by default)
18
+ * - Atomic write operations to prevent corruption
19
+ * - Platform-aware log location (respects XDG_STATE_HOME on Linux)
20
+ * - Detailed timestamps and structured log lines
21
+ * - Sensitive information filtering on console output
22
+ */
23
+ declare class Logger {
24
+ private readonly logFile;
25
+ private readonly config;
26
+ private writeQueue;
27
+ private isWriting;
28
+ constructor(config?: Partial<LoggerConfig>);
29
+ /**
30
+ * Resolve log file path following XDG Base Directory specification
31
+ * and respecting environment overrides for testing
32
+ */
33
+ private resolveLogPath;
34
+ /**
35
+ * Initialize log file and ensure directory exists
36
+ */
37
+ private initializeLogFile;
38
+ /**
39
+ * Check if log file exceeds size limit and rotate if needed
40
+ */
41
+ private rotateLogIfNeeded;
42
+ /**
43
+ * Queue write operation to prevent concurrent writes
44
+ * This ensures log file integrity with atomic operations
45
+ */
46
+ private queueWrite;
47
+ /**
48
+ * Process write queue sequentially to prevent concurrent writes
49
+ */
50
+ private processQueue;
51
+ /**
52
+ * Write log entry with level and formatted message
53
+ */
54
+ private writeLog;
55
+ /**
56
+ * Get the path to the current log file
57
+ * Useful for users to locate and inspect logs
58
+ */
59
+ getLogPath(): string;
60
+ /**
61
+ * Log entry at LOG level (general information)
62
+ */
63
+ log(...args: unknown[]): void;
64
+ /**
65
+ * Log entry at INFO level (significant events)
66
+ */
67
+ info(...args: unknown[]): void;
68
+ /**
69
+ * Log entry at WARN level (potentially harmful situations)
70
+ */
71
+ warn(...args: unknown[]): void;
72
+ /**
73
+ * Log entry at ERROR level (error conditions)
74
+ */
75
+ error(...args: unknown[]): void;
76
+ /**
77
+ * Log entry at DEBUG level (detailed diagnostic information)
78
+ * Only written to file, not to console
79
+ */
80
+ debug(...args: unknown[]): void;
81
+ }
82
+ export declare const logger: Logger;
83
+ export {};