ccmanager 0.0.3 → 0.0.5
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 +18 -1
- package/dist/services/sessionManager.js +32 -1
- package/dist/services/sessionManager.test.js +22 -44
- package/package.json +1 -1
- package/dist/components/StatusBar.d.ts +0 -9
- package/dist/components/StatusBar.js +0 -51
- package/dist/hooks/useSession.d.ts +0 -9
- package/dist/hooks/useSession.js +0 -44
- package/dist/hooks/useWorktree.d.ts +0 -7
- package/dist/hooks/useWorktree.js +0 -31
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -5
- package/dist/services/logger.d.ts +0 -1
- package/dist/services/logger.js +0 -10
- package/dist/services/stateDetector.d.ts +0 -9
- package/dist/services/stateDetector.js +0 -82
- package/dist/utils/debug.d.ts +0 -1
- package/dist/utils/debug.js +0 -109
- package/dist/utils/waitingPatternDetector.d.ts +0 -1
- package/dist/utils/waitingPatternDetector.js +0 -16
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# CCManager - Claude Code
|
|
1
|
+
# CCManager - Claude Code Session Manager
|
|
2
2
|
|
|
3
3
|
CCManager is a TUI application for managing multiple Claude Code sessions across Git worktrees.
|
|
4
4
|
|
|
@@ -48,6 +48,23 @@ $ npm start
|
|
|
48
48
|
$ npx ccmanager
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
## Environment Variables
|
|
52
|
+
|
|
53
|
+
### CCMANAGER_CLAUDE_ARGS
|
|
54
|
+
|
|
55
|
+
You can pass additional arguments to Claude Code sessions by setting the `CCMANAGER_CLAUDE_ARGS` environment variable:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Start Claude Code with specific arguments for all sessions
|
|
59
|
+
export CCMANAGER_CLAUDE_ARGS="--resume"
|
|
60
|
+
npx ccmanager
|
|
61
|
+
|
|
62
|
+
# Or set it inline
|
|
63
|
+
CCMANAGER_CLAUDE_ARGS="--resume" npx ccmanager
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The arguments are applied to all Claude Code sessions started by CCManager.
|
|
67
|
+
|
|
51
68
|
## Keyboard Shortcuts
|
|
52
69
|
|
|
53
70
|
### Default Shortcuts
|
|
@@ -55,6 +55,21 @@ export class SessionManager extends EventEmitter {
|
|
|
55
55
|
this.busyTimers.delete(sessionId);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
else if (currentState === 'waiting_input' &&
|
|
59
|
+
hasBottomBorder &&
|
|
60
|
+
!hasWaitingPrompt &&
|
|
61
|
+
wasWaitingWithBottomBorder) {
|
|
62
|
+
// We've already seen the bottom border for this waiting prompt,
|
|
63
|
+
// so transition to idle
|
|
64
|
+
newState = 'idle';
|
|
65
|
+
this.waitingWithBottomBorder.set(sessionId, false);
|
|
66
|
+
// Clear any pending busy timer
|
|
67
|
+
const existingTimer = this.busyTimers.get(sessionId);
|
|
68
|
+
if (existingTimer) {
|
|
69
|
+
clearTimeout(existingTimer);
|
|
70
|
+
this.busyTimers.delete(sessionId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
58
73
|
else if (hasEscToInterrupt) {
|
|
59
74
|
// If "esc to interrupt" is present, set state to busy
|
|
60
75
|
newState = 'busy';
|
|
@@ -84,6 +99,18 @@ export class SessionManager extends EventEmitter {
|
|
|
84
99
|
// Keep current busy state for now
|
|
85
100
|
newState = 'busy';
|
|
86
101
|
}
|
|
102
|
+
else if (currentState === 'waiting_input') {
|
|
103
|
+
// If we're in waiting_input but no special patterns detected,
|
|
104
|
+
// transition to idle and clear the flag
|
|
105
|
+
newState = 'idle';
|
|
106
|
+
this.waitingWithBottomBorder.set(sessionId, false);
|
|
107
|
+
// Clear any pending busy timer
|
|
108
|
+
const existingTimer = this.busyTimers.get(sessionId);
|
|
109
|
+
if (existingTimer) {
|
|
110
|
+
clearTimeout(existingTimer);
|
|
111
|
+
this.busyTimers.delete(sessionId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
87
114
|
return newState;
|
|
88
115
|
}
|
|
89
116
|
constructor() {
|
|
@@ -117,7 +144,11 @@ export class SessionManager extends EventEmitter {
|
|
|
117
144
|
const id = `session-${Date.now()}-${Math.random()
|
|
118
145
|
.toString(36)
|
|
119
146
|
.substr(2, 9)}`;
|
|
120
|
-
|
|
147
|
+
// Parse Claude command arguments from environment variable
|
|
148
|
+
const claudeArgs = process.env['CCMANAGER_CLAUDE_ARGS']
|
|
149
|
+
? process.env['CCMANAGER_CLAUDE_ARGS'].split(' ')
|
|
150
|
+
: [];
|
|
151
|
+
const ptyProcess = spawn('claude', claudeArgs, {
|
|
121
152
|
name: 'xterm-color',
|
|
122
153
|
cols: process.stdout.columns || 80,
|
|
123
154
|
rows: process.stdout.rows || 24,
|
|
@@ -62,27 +62,16 @@ describe('SessionManager', () => {
|
|
|
62
62
|
const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
|
|
63
63
|
expect(newState).toBe('busy');
|
|
64
64
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// );
|
|
76
|
-
//
|
|
77
|
-
// // Now another bottom border appears
|
|
78
|
-
// const newState = sessionManager.detectSessionState(
|
|
79
|
-
// cleanData,
|
|
80
|
-
// currentState,
|
|
81
|
-
// mockSessionId,
|
|
82
|
-
// );
|
|
83
|
-
//
|
|
84
|
-
// expect(newState).toBe('idle'); // Should change to idle since we already saw the bottom border
|
|
85
|
-
// });
|
|
65
|
+
it('should not change from waiting_input when bottom border was already seen', () => {
|
|
66
|
+
const cleanData = '└───────────────────────┘';
|
|
67
|
+
const currentState = 'waiting_input';
|
|
68
|
+
vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(true);
|
|
69
|
+
// First, simulate seeing waiting prompt with bottom border
|
|
70
|
+
sessionManager.detectSessionState('│ Do you want to continue?\n└───────────────────────┘', 'idle', mockSessionId);
|
|
71
|
+
// Now another bottom border appears
|
|
72
|
+
const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
|
|
73
|
+
expect(newState).toBe('idle'); // Should change to idle since we already saw the bottom border
|
|
74
|
+
});
|
|
86
75
|
it('should clear waitingWithBottomBorder flag when transitioning to busy', () => {
|
|
87
76
|
const cleanData = 'Processing... Press ESC to interrupt';
|
|
88
77
|
const currentState = 'waiting_input';
|
|
@@ -95,29 +84,18 @@ describe('SessionManager', () => {
|
|
|
95
84
|
const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
|
|
96
85
|
expect(newState).toBe('busy');
|
|
97
86
|
});
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
// // Now transition to idle
|
|
112
|
-
// vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(false);
|
|
113
|
-
// const newState = sessionManager.detectSessionState(
|
|
114
|
-
// cleanData,
|
|
115
|
-
// currentState,
|
|
116
|
-
// mockSessionId,
|
|
117
|
-
// );
|
|
118
|
-
//
|
|
119
|
-
// expect(newState).toBe('idle');
|
|
120
|
-
// });
|
|
87
|
+
it('should clear waitingWithBottomBorder flag when transitioning to idle', () => {
|
|
88
|
+
const cleanData = 'Task completed successfully';
|
|
89
|
+
const currentState = 'waiting_input';
|
|
90
|
+
vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(false);
|
|
91
|
+
// First set up waiting state with bottom border
|
|
92
|
+
vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(true);
|
|
93
|
+
sessionManager.detectSessionState('│ Do you want to continue?\n└───────────────────────┘', 'idle', mockSessionId);
|
|
94
|
+
// Now transition to idle
|
|
95
|
+
vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(false);
|
|
96
|
+
const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
|
|
97
|
+
expect(newState).toBe('idle');
|
|
98
|
+
});
|
|
121
99
|
it('should transition from busy to idle after 500ms timer when no "esc to interrupt"', async () => {
|
|
122
100
|
// Create a mock session for the timer test
|
|
123
101
|
const mockWorktreePath = '/test/worktree';
|
package/package.json
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { SessionState, ViewType, Worktree } from '../types/index.js';
|
|
3
|
-
interface StatusBarProps {
|
|
4
|
-
view: ViewType;
|
|
5
|
-
sessionState?: SessionState;
|
|
6
|
-
worktree?: Worktree;
|
|
7
|
-
}
|
|
8
|
-
export declare const StatusBar: React.FC<StatusBarProps>;
|
|
9
|
-
export {};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { SessionState, ViewType } from '../types/index.js';
|
|
4
|
-
export const StatusBar = ({ view, sessionState, worktree, }) => {
|
|
5
|
-
const getStateText = () => {
|
|
6
|
-
if (!sessionState)
|
|
7
|
-
return '';
|
|
8
|
-
switch (sessionState) {
|
|
9
|
-
case SessionState.Processing:
|
|
10
|
-
return 'Processing...';
|
|
11
|
-
case SessionState.WaitingForInput:
|
|
12
|
-
return 'Ready';
|
|
13
|
-
case SessionState.NeedsInteraction:
|
|
14
|
-
return 'Needs input';
|
|
15
|
-
case SessionState.Error:
|
|
16
|
-
return 'Error';
|
|
17
|
-
case SessionState.Terminated:
|
|
18
|
-
return 'Terminated';
|
|
19
|
-
default:
|
|
20
|
-
return 'Idle';
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
const getStateColor = () => {
|
|
24
|
-
if (!sessionState)
|
|
25
|
-
return 'gray';
|
|
26
|
-
switch (sessionState) {
|
|
27
|
-
case SessionState.Processing:
|
|
28
|
-
return 'yellow';
|
|
29
|
-
case SessionState.WaitingForInput:
|
|
30
|
-
return 'green';
|
|
31
|
-
case SessionState.NeedsInteraction:
|
|
32
|
-
return 'cyan';
|
|
33
|
-
case SessionState.Error:
|
|
34
|
-
return 'red';
|
|
35
|
-
case SessionState.Terminated:
|
|
36
|
-
return 'gray';
|
|
37
|
-
default:
|
|
38
|
-
return 'gray';
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
return (React.createElement(Box, { borderStyle: "single", borderTop: true, paddingLeft: 1, paddingRight: 1, justifyContent: "space-between" },
|
|
42
|
-
React.createElement(Box, null, worktree && (React.createElement(Text, null,
|
|
43
|
-
React.createElement(Text, { bold: true }, worktree.branch),
|
|
44
|
-
React.createElement(Text, { dimColor: true },
|
|
45
|
-
" (",
|
|
46
|
-
worktree.path,
|
|
47
|
-
")")))),
|
|
48
|
-
React.createElement(Box, { gap: 2 },
|
|
49
|
-
sessionState && React.createElement(Text, { color: getStateColor() }, getStateText()),
|
|
50
|
-
React.createElement(Text, { dimColor: true }, view === ViewType.Session ? 'Ctrl+E Return to Menu' : 'Q Quit'))));
|
|
51
|
-
};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { Session } from '../types/index.js';
|
|
2
|
-
export declare const useSession: () => {
|
|
3
|
-
sessions: Session[];
|
|
4
|
-
createOrActivateSession: (worktreePath: string) => Session;
|
|
5
|
-
writeToSession: (sessionId: string, data: string) => void;
|
|
6
|
-
resizeSession: (sessionId: string, cols: number, rows: number) => void;
|
|
7
|
-
terminateSession: (sessionId: string) => void;
|
|
8
|
-
getSession: (id: string) => Session | undefined;
|
|
9
|
-
};
|
package/dist/hooks/useSession.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { SessionManager } from '../services/sessionManager.js';
|
|
3
|
-
export const useSession = () => {
|
|
4
|
-
const [sessions, setSessions] = useState([]);
|
|
5
|
-
const [sessionManager] = useState(() => new SessionManager());
|
|
6
|
-
const refreshSessions = useCallback(() => {
|
|
7
|
-
setSessions(sessionManager.getAllSessions());
|
|
8
|
-
}, [sessionManager]);
|
|
9
|
-
const createOrActivateSession = useCallback((worktreePath) => {
|
|
10
|
-
let session = sessionManager.findSessionByWorktree(worktreePath);
|
|
11
|
-
if (!session) {
|
|
12
|
-
session = sessionManager.createSession(worktreePath);
|
|
13
|
-
sessionManager.startSession(session.id);
|
|
14
|
-
}
|
|
15
|
-
else if (!session.process) {
|
|
16
|
-
sessionManager.startSession(session.id);
|
|
17
|
-
}
|
|
18
|
-
refreshSessions();
|
|
19
|
-
return session;
|
|
20
|
-
}, [sessionManager, refreshSessions]);
|
|
21
|
-
const writeToSession = useCallback((sessionId, data) => {
|
|
22
|
-
sessionManager.writeToSession(sessionId, data);
|
|
23
|
-
}, [sessionManager]);
|
|
24
|
-
const resizeSession = useCallback((sessionId, cols, rows) => {
|
|
25
|
-
sessionManager.resizeSession(sessionId, cols, rows);
|
|
26
|
-
}, [sessionManager]);
|
|
27
|
-
const terminateSession = useCallback((sessionId) => {
|
|
28
|
-
sessionManager.terminateSession(sessionId);
|
|
29
|
-
refreshSessions();
|
|
30
|
-
}, [sessionManager, refreshSessions]);
|
|
31
|
-
// Update sessions when they change
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
const interval = setInterval(refreshSessions, 100);
|
|
34
|
-
return () => clearInterval(interval);
|
|
35
|
-
}, [refreshSessions]);
|
|
36
|
-
return {
|
|
37
|
-
sessions,
|
|
38
|
-
createOrActivateSession,
|
|
39
|
-
writeToSession,
|
|
40
|
-
resizeSession,
|
|
41
|
-
terminateSession,
|
|
42
|
-
getSession: (id) => sessionManager.getSession(id),
|
|
43
|
-
};
|
|
44
|
-
};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { WorktreeService } from '../services/worktreeService.js';
|
|
3
|
-
export const useWorktree = () => {
|
|
4
|
-
const [worktrees, setWorktrees] = useState([]);
|
|
5
|
-
const [loading, setLoading] = useState(true);
|
|
6
|
-
const [error, setError] = useState(null);
|
|
7
|
-
const worktreeService = new WorktreeService();
|
|
8
|
-
const refreshWorktrees = async () => {
|
|
9
|
-
try {
|
|
10
|
-
setLoading(true);
|
|
11
|
-
setError(null);
|
|
12
|
-
const trees = await worktreeService.listWorktrees();
|
|
13
|
-
setWorktrees(trees);
|
|
14
|
-
}
|
|
15
|
-
catch (err) {
|
|
16
|
-
setError(err instanceof Error ? err.message : 'Failed to load worktrees');
|
|
17
|
-
}
|
|
18
|
-
finally {
|
|
19
|
-
setLoading(false);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
refreshWorktrees();
|
|
24
|
-
}, []);
|
|
25
|
-
return {
|
|
26
|
-
worktrees,
|
|
27
|
-
loading,
|
|
28
|
-
error,
|
|
29
|
-
refreshWorktrees,
|
|
30
|
-
};
|
|
31
|
-
};
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function log(...args: any[]): void;
|
package/dist/services/logger.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
const logFile = path.join(process.cwd(), 'ccmanager.log');
|
|
4
|
-
// Clear log file on module load
|
|
5
|
-
fs.writeFileSync(logFile, '');
|
|
6
|
-
export function log(...args) {
|
|
7
|
-
const timestamp = new Date().toISOString();
|
|
8
|
-
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
|
|
9
|
-
fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`);
|
|
10
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { SessionState } from '../types/index.js';
|
|
2
|
-
export declare class StateDetector {
|
|
3
|
-
private readonly promptPatterns;
|
|
4
|
-
private readonly errorPatterns;
|
|
5
|
-
private readonly processingPatterns;
|
|
6
|
-
detectState(output: string, currentState: SessionState): SessionState;
|
|
7
|
-
private stripAnsi;
|
|
8
|
-
private matchesAny;
|
|
9
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { SessionState } from '../types/index.js';
|
|
2
|
-
export class StateDetector {
|
|
3
|
-
constructor() {
|
|
4
|
-
Object.defineProperty(this, "promptPatterns", {
|
|
5
|
-
enumerable: true,
|
|
6
|
-
configurable: true,
|
|
7
|
-
writable: true,
|
|
8
|
-
value: [
|
|
9
|
-
/>\s*$/,
|
|
10
|
-
/\?\s*$/,
|
|
11
|
-
/:\s*$/,
|
|
12
|
-
/Press Enter to continue/i,
|
|
13
|
-
/\(y\/n\)/i,
|
|
14
|
-
/\[y\/N\]/i,
|
|
15
|
-
/Enter your choice/i,
|
|
16
|
-
]
|
|
17
|
-
});
|
|
18
|
-
Object.defineProperty(this, "errorPatterns", {
|
|
19
|
-
enumerable: true,
|
|
20
|
-
configurable: true,
|
|
21
|
-
writable: true,
|
|
22
|
-
value: [
|
|
23
|
-
/error:/i,
|
|
24
|
-
/failed:/i,
|
|
25
|
-
/exception:/i,
|
|
26
|
-
/traceback/i,
|
|
27
|
-
/command not found/i,
|
|
28
|
-
]
|
|
29
|
-
});
|
|
30
|
-
Object.defineProperty(this, "processingPatterns", {
|
|
31
|
-
enumerable: true,
|
|
32
|
-
configurable: true,
|
|
33
|
-
writable: true,
|
|
34
|
-
value: [
|
|
35
|
-
/processing/i,
|
|
36
|
-
/loading/i,
|
|
37
|
-
/running/i,
|
|
38
|
-
/executing/i,
|
|
39
|
-
/analyzing/i,
|
|
40
|
-
/searching/i,
|
|
41
|
-
/\.\.\./,
|
|
42
|
-
/in progress/i,
|
|
43
|
-
]
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
detectState(output, currentState) {
|
|
47
|
-
// Clean ANSI codes for better pattern matching
|
|
48
|
-
const cleanOutput = this.stripAnsi(output);
|
|
49
|
-
// Check for errors first
|
|
50
|
-
if (this.matchesAny(cleanOutput, this.errorPatterns)) {
|
|
51
|
-
return SessionState.Error;
|
|
52
|
-
}
|
|
53
|
-
// Check if processing
|
|
54
|
-
if (this.matchesAny(cleanOutput, this.processingPatterns)) {
|
|
55
|
-
return SessionState.Processing;
|
|
56
|
-
}
|
|
57
|
-
// Check for prompts that need interaction
|
|
58
|
-
if (this.matchesAny(cleanOutput, this.promptPatterns)) {
|
|
59
|
-
// Check if it's a yes/no prompt or choice prompt
|
|
60
|
-
if (/\(y\/n\)/i.test(cleanOutput) ||
|
|
61
|
-
/\[y\/N\]/i.test(cleanOutput) ||
|
|
62
|
-
/Enter your choice/i.test(cleanOutput)) {
|
|
63
|
-
return SessionState.NeedsInteraction;
|
|
64
|
-
}
|
|
65
|
-
return SessionState.WaitingForInput;
|
|
66
|
-
}
|
|
67
|
-
// If we're getting output but no clear patterns, assume processing
|
|
68
|
-
if (cleanOutput.trim().length > 0 &&
|
|
69
|
-
currentState === SessionState.WaitingForInput) {
|
|
70
|
-
return SessionState.Processing;
|
|
71
|
-
}
|
|
72
|
-
// Default to current state
|
|
73
|
-
return currentState;
|
|
74
|
-
}
|
|
75
|
-
stripAnsi(str) {
|
|
76
|
-
// eslint-disable-next-line no-control-regex
|
|
77
|
-
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
78
|
-
}
|
|
79
|
-
matchesAny(text, patterns) {
|
|
80
|
-
return patterns.some(pattern => pattern.test(text));
|
|
81
|
-
}
|
|
82
|
-
}
|
package/dist/utils/debug.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function debugLog(location: string, message: string, data?: any): void;
|
package/dist/utils/debug.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
class DebugLogger {
|
|
5
|
-
constructor() {
|
|
6
|
-
Object.defineProperty(this, "logFile", {
|
|
7
|
-
enumerable: true,
|
|
8
|
-
configurable: true,
|
|
9
|
-
writable: true,
|
|
10
|
-
value: void 0
|
|
11
|
-
});
|
|
12
|
-
Object.defineProperty(this, "stream", {
|
|
13
|
-
enumerable: true,
|
|
14
|
-
configurable: true,
|
|
15
|
-
writable: true,
|
|
16
|
-
value: null
|
|
17
|
-
});
|
|
18
|
-
// Create log file in user's home directory
|
|
19
|
-
const logDir = path.join(homedir(), '.ccmanager');
|
|
20
|
-
if (!fs.existsSync(logDir)) {
|
|
21
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
this.logFile = path.join(logDir, 'debug.log');
|
|
24
|
-
this.initStream();
|
|
25
|
-
}
|
|
26
|
-
initStream() {
|
|
27
|
-
this.stream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
|
28
|
-
// Add separator when starting new session
|
|
29
|
-
this.stream.write('\n' + '='.repeat(80) + '\n');
|
|
30
|
-
this.stream.write(`Session started at ${new Date().toISOString()}\n`);
|
|
31
|
-
this.stream.write('='.repeat(80) + '\n\n');
|
|
32
|
-
}
|
|
33
|
-
stripAnsi(str) {
|
|
34
|
-
// Remove ANSI escape sequences
|
|
35
|
-
return str.replace(/\x1B\[[0-9;]*m/g, '') // Color codes
|
|
36
|
-
.replace(/\x1B\[\?[0-9]+[hl]/g, '') // Cursor visibility
|
|
37
|
-
.replace(/\x1B\[[0-9]*[ABCDEFGHJKST]/g, '') // Cursor movement
|
|
38
|
-
.replace(/\x1B\[[0-9]*K/g, '') // Clear line
|
|
39
|
-
.replace(/\r/g, '') // Carriage returns
|
|
40
|
-
.replace(/\x1B\[2004[hl]/g, '') // Bracketed paste mode
|
|
41
|
-
.replace(/\x1B\[1004[hl]/g, ''); // Focus tracking mode
|
|
42
|
-
}
|
|
43
|
-
formatData(data) {
|
|
44
|
-
if (typeof data === 'string') {
|
|
45
|
-
const cleaned = this.stripAnsi(data);
|
|
46
|
-
// Only show non-empty, meaningful content
|
|
47
|
-
if (cleaned.trim()) {
|
|
48
|
-
// Truncate very long outputs
|
|
49
|
-
if (cleaned.length > 200) {
|
|
50
|
-
return cleaned.substring(0, 200) + '... [truncated]';
|
|
51
|
-
}
|
|
52
|
-
return cleaned;
|
|
53
|
-
}
|
|
54
|
-
return '[empty or control sequences only]';
|
|
55
|
-
}
|
|
56
|
-
else if (typeof data === 'object' && data !== null) {
|
|
57
|
-
// Pretty print objects
|
|
58
|
-
return JSON.stringify(data, null, 2);
|
|
59
|
-
}
|
|
60
|
-
return String(data);
|
|
61
|
-
}
|
|
62
|
-
log(location, message, data) {
|
|
63
|
-
if (!this.stream)
|
|
64
|
-
return;
|
|
65
|
-
const timestampParts = new Date().toISOString().split('T');
|
|
66
|
-
const timestamp = timestampParts[1] ? timestampParts[1].replace('Z', '') : new Date().toTimeString().split(' ')[0];
|
|
67
|
-
// Format the log entry in a human-readable way
|
|
68
|
-
this.stream.write(`[${timestamp}] ${location}\n`);
|
|
69
|
-
this.stream.write(` ${message}:\n`);
|
|
70
|
-
if (data !== undefined) {
|
|
71
|
-
const formattedData = this.formatData(data);
|
|
72
|
-
const lines = formattedData.split('\n');
|
|
73
|
-
lines.forEach(line => {
|
|
74
|
-
if (this.stream) {
|
|
75
|
-
this.stream.write(` ${line}\n`);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
if (this.stream) {
|
|
80
|
-
this.stream.write('\n');
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
close() {
|
|
84
|
-
if (this.stream) {
|
|
85
|
-
this.stream.write(`\nSession ended at ${new Date().toISOString()}\n`);
|
|
86
|
-
this.stream.write('='.repeat(80) + '\n');
|
|
87
|
-
this.stream.end();
|
|
88
|
-
this.stream = null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
// Singleton instance
|
|
93
|
-
const debugLogger = new DebugLogger();
|
|
94
|
-
// Export a simple function for logging
|
|
95
|
-
export function debugLog(location, message, data) {
|
|
96
|
-
debugLogger.log(location, message, data);
|
|
97
|
-
}
|
|
98
|
-
// Clean up on process exit
|
|
99
|
-
process.on('exit', () => {
|
|
100
|
-
debugLogger.close();
|
|
101
|
-
});
|
|
102
|
-
process.on('SIGINT', () => {
|
|
103
|
-
debugLogger.close();
|
|
104
|
-
process.exit();
|
|
105
|
-
});
|
|
106
|
-
process.on('SIGTERM', () => {
|
|
107
|
-
debugLogger.close();
|
|
108
|
-
process.exit();
|
|
109
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function isWaitingForInput(output: string): boolean;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export function isWaitingForInput(output) {
|
|
2
|
-
// Don't trim - we need to check end patterns
|
|
3
|
-
if (!output) {
|
|
4
|
-
return false;
|
|
5
|
-
}
|
|
6
|
-
// Check if output ends with Claude prompt ("> " at the end)
|
|
7
|
-
if (output.trimEnd().endsWith('>')) {
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
// Check for user interaction
|
|
11
|
-
// │ Do you want to proceed?
|
|
12
|
-
if (output.includes('│ Do you want')) {
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
return false;
|
|
16
|
-
}
|