ccmanager 3.2.5 → 3.2.7

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/README.md CHANGED
@@ -20,6 +20,7 @@ https://github.com/user-attachments/assets/15914a88-e288-4ac9-94d5-8127f2e19dbf
20
20
  - Configurable state detection strategies for different CLI tools
21
21
  - Status change hooks for automation and notifications
22
22
  - Devcontainer integration
23
+ - **Auto Approval (experimental)**: Automatically approve safe prompts using AI verification
23
24
 
24
25
  ## Why CCManager over Claude Squad?
25
26
 
@@ -249,6 +250,25 @@ CCManager can automatically generate worktree directory paths based on branch na
249
250
 
250
251
  For detailed configuration and examples, see [docs/worktree-auto-directory.md](docs/worktree-auto-directory.md).
251
252
 
253
+ ## Auto Approval (Experimental)
254
+
255
+ CCManager can automatically approve Claude Code prompts that don't require user permission, reducing manual intervention while maintaining safety for sensitive operations.
256
+
257
+ ### Features
258
+
259
+ - **Automatic decision**: Uses Claude (Haiku) to analyze prompts and determine if they need manual approval
260
+ - **Custom commands**: Replace the default verifier with your own script or different AI model
261
+ - **Safe fallback**: Always defaults to manual approval on errors or timeouts
262
+ - **Interruptible**: Press any key to cancel auto-approval and review manually
263
+
264
+ ### Quick Start
265
+
266
+ 1. Navigate to **Configuration** → **Other & Experimental**
267
+ 2. Enable **Auto Approval (experimental)**
268
+ 3. (Optional) Configure a custom command for verification
269
+
270
+ For detailed configuration and usage, see [docs/auto-approval.md](docs/auto-approval.md).
271
+
252
272
  ## Devcontainer Integration
253
273
 
254
274
  CCManager supports running AI assistant sessions inside devcontainers while keeping the manager itself on the host machine. This enables sandboxed development environments with restricted network access while maintaining host-level notifications and automation.
@@ -18,6 +18,15 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
18
18
  private getTerminalContent;
19
19
  private handleAutoApproval;
20
20
  private cancelAutoApprovalVerification;
21
+ /**
22
+ * Update session state with automatic status hook execution.
23
+ * This method ensures that executeStatusHook is always called when state changes.
24
+ *
25
+ * @param session - The session to update
26
+ * @param newState - The new state to set
27
+ * @param additionalUpdates - Optional additional state data updates
28
+ */
29
+ private updateSessionState;
21
30
  constructor();
22
31
  private createSessionId;
23
32
  private createTerminal;
@@ -85,15 +85,10 @@ export class SessionManager extends EventEmitter {
85
85
  if (autoApprovalResult.needsPermission) {
86
86
  // Change state to waiting_input to ask for user permission
87
87
  logger.info(`[${session.id}] Auto-approval verification determined user permission needed`);
88
- await session.stateMutex.update(data => ({
89
- ...data,
90
- state: 'waiting_input',
88
+ await this.updateSessionState(session, 'waiting_input', {
91
89
  autoApprovalFailed: true,
92
90
  autoApprovalReason: autoApprovalResult.reason,
93
- pendingState: undefined,
94
- pendingStateStart: undefined,
95
- }));
96
- this.emit('sessionStateChanged', session);
91
+ });
97
92
  }
98
93
  else {
99
94
  // Auto-approve by simulating Enter key press
@@ -101,14 +96,9 @@ export class SessionManager extends EventEmitter {
101
96
  session.process.write('\r');
102
97
  // Force state to busy to prevent endless auto-approval
103
98
  // when the state detection still sees pending_auto_approval
104
- await session.stateMutex.update(data => ({
105
- ...data,
106
- state: 'busy',
99
+ await this.updateSessionState(session, 'busy', {
107
100
  autoApprovalReason: undefined,
108
- pendingState: undefined,
109
- pendingStateStart: undefined,
110
- }));
111
- this.emit('sessionStateChanged', session);
101
+ });
112
102
  }
113
103
  })
114
104
  .catch(async (error) => {
@@ -120,16 +110,11 @@ export class SessionManager extends EventEmitter {
120
110
  logger.error(`[${session.id}] Auto-approval verification failed, requiring user permission`, error);
121
111
  const currentState = session.stateMutex.getSnapshot().state;
122
112
  if (currentState === 'pending_auto_approval') {
123
- await session.stateMutex.update(data => ({
124
- ...data,
125
- state: 'waiting_input',
113
+ await this.updateSessionState(session, 'waiting_input', {
126
114
  autoApprovalFailed: true,
127
115
  autoApprovalReason: error?.message ??
128
116
  'Auto-approval verification failed',
129
- pendingState: undefined,
130
- pendingStateStart: undefined,
131
- }));
132
- this.emit('sessionStateChanged', session);
117
+ });
133
118
  }
134
119
  })
135
120
  .finally(async () => {
@@ -157,6 +142,28 @@ export class SessionManager extends EventEmitter {
157
142
  }));
158
143
  logger.info(`[${session.id}] Cancelled auto-approval verification: ${reason}`);
159
144
  }
145
+ /**
146
+ * Update session state with automatic status hook execution.
147
+ * This method ensures that executeStatusHook is always called when state changes.
148
+ *
149
+ * @param session - The session to update
150
+ * @param newState - The new state to set
151
+ * @param additionalUpdates - Optional additional state data updates
152
+ */
153
+ async updateSessionState(session, newState, additionalUpdates = {}) {
154
+ const oldState = session.stateMutex.getSnapshot().state;
155
+ await session.stateMutex.update(data => ({
156
+ ...data,
157
+ state: newState,
158
+ pendingState: undefined,
159
+ pendingStateStart: undefined,
160
+ ...additionalUpdates,
161
+ }));
162
+ if (oldState !== newState) {
163
+ void Effect.runPromise(executeStatusHook(oldState, newState, session));
164
+ this.emit('sessionStateChanged', session);
165
+ }
166
+ }
160
167
  constructor() {
161
168
  super();
162
169
  Object.defineProperty(this, "sessions", {
@@ -393,31 +400,23 @@ export class SessionManager extends EventEmitter {
393
400
  // Check if the pending state has persisted long enough
394
401
  const duration = now - stateData.pendingStateStart;
395
402
  if (duration >= STATE_PERSISTENCE_DURATION_MS) {
396
- // Confirm the state change
397
- void session.stateMutex.update(data => {
398
- const newData = {
399
- ...data,
400
- state: detectedState,
401
- pendingState: undefined,
402
- pendingStateStart: undefined,
403
- };
404
- // If we previously blocked auto-approval and have moved out of a user prompt,
405
- // allow future auto-approval attempts.
406
- if (data.autoApprovalFailed &&
407
- detectedState !== 'waiting_input' &&
408
- detectedState !== 'pending_auto_approval') {
409
- newData.autoApprovalFailed = false;
410
- newData.autoApprovalReason = undefined;
411
- }
412
- return newData;
413
- });
403
+ // Cancel auto-approval verification if state is changing away from pending_auto_approval
414
404
  if (stateData.autoApprovalAbortController &&
415
405
  detectedState !== 'pending_auto_approval') {
416
406
  this.cancelAutoApprovalVerification(session, `state changed to ${detectedState}`);
417
407
  }
418
- // Execute status hook asynchronously (non-blocking) using Effect
419
- void Effect.runPromise(executeStatusHook(oldState, detectedState, session));
420
- this.emit('sessionStateChanged', session);
408
+ // Build additional updates for auto-approval reset
409
+ const additionalUpdates = {};
410
+ // If we previously blocked auto-approval and have moved out of a user prompt,
411
+ // allow future auto-approval attempts.
412
+ if (stateData.autoApprovalFailed &&
413
+ detectedState !== 'waiting_input' &&
414
+ detectedState !== 'pending_auto_approval') {
415
+ additionalUpdates.autoApprovalFailed = false;
416
+ additionalUpdates.autoApprovalReason = undefined;
417
+ }
418
+ // Confirm the state change with hook execution
419
+ void this.updateSessionState(session, detectedState, additionalUpdates);
421
420
  }
422
421
  }
423
422
  }
@@ -452,13 +451,7 @@ export class SessionManager extends EventEmitter {
452
451
  session.stateCheckInterval = undefined;
453
452
  }
454
453
  // Clear any pending state and update state to idle before destroying
455
- void session.stateMutex.update(data => ({
456
- ...data,
457
- state: 'idle',
458
- pendingState: undefined,
459
- pendingStateStart: undefined,
460
- }));
461
- this.emit('sessionStateChanged', session);
454
+ void this.updateSessionState(session, 'idle');
462
455
  this.destroySession(session.worktreePath);
463
456
  this.emit('sessionExit', session);
464
457
  }
@@ -490,21 +483,22 @@ export class SessionManager extends EventEmitter {
490
483
  return;
491
484
  }
492
485
  this.cancelAutoApprovalVerification(session, reason);
493
- void session.stateMutex.update(data => {
494
- const newData = {
486
+ if (stateData.state === 'pending_auto_approval') {
487
+ // State change: pending_auto_approval -> waiting_input
488
+ void this.updateSessionState(session, 'waiting_input', {
489
+ autoApprovalFailed: true,
490
+ autoApprovalReason: reason,
491
+ });
492
+ }
493
+ else {
494
+ // No state change, just update other fields
495
+ void session.stateMutex.update(data => ({
495
496
  ...data,
496
497
  autoApprovalFailed: true,
497
498
  autoApprovalReason: reason,
498
499
  pendingState: undefined,
499
500
  pendingStateStart: undefined,
500
- };
501
- if (data.state === 'pending_auto_approval') {
502
- newData.state = 'waiting_input';
503
- }
504
- return newData;
505
- });
506
- if (stateData.state === 'pending_auto_approval') {
507
- this.emit('sessionStateChanged', session);
501
+ }));
508
502
  }
509
503
  }
510
504
  destroySession(worktreePath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "3.2.5",
3
+ "version": "3.2.7",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",
@@ -41,11 +41,11 @@
41
41
  "bin"
42
42
  ],
43
43
  "optionalDependencies": {
44
- "@kodaikabasawa/ccmanager-darwin-arm64": "3.2.5",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "3.2.5",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "3.2.5",
47
- "@kodaikabasawa/ccmanager-linux-x64": "3.2.5",
48
- "@kodaikabasawa/ccmanager-win32-x64": "3.2.5"
44
+ "@kodaikabasawa/ccmanager-darwin-arm64": "3.2.7",
45
+ "@kodaikabasawa/ccmanager-darwin-x64": "3.2.7",
46
+ "@kodaikabasawa/ccmanager-linux-arm64": "3.2.7",
47
+ "@kodaikabasawa/ccmanager-linux-x64": "3.2.7",
48
+ "@kodaikabasawa/ccmanager-win32-x64": "3.2.7"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.28.0",