ccmanager 0.0.2 → 0.0.4
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 +10 -20
- package/dist/services/sessionManager.test.js +46 -22
- 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
|
|
@@ -43,17 +43,11 @@ export class SessionManager extends EventEmitter {
|
|
|
43
43
|
}
|
|
44
44
|
else if (currentState === 'waiting_input' &&
|
|
45
45
|
hasBottomBorder &&
|
|
46
|
-
!hasWaitingPrompt
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
// First time seeing bottom border, keep waiting state
|
|
54
|
-
newState = 'waiting_input';
|
|
55
|
-
this.waitingWithBottomBorder.set(sessionId, true);
|
|
56
|
-
}
|
|
46
|
+
!hasWaitingPrompt &&
|
|
47
|
+
!wasWaitingWithBottomBorder) {
|
|
48
|
+
// Keep the waiting state and mark that we've seen the bottom border
|
|
49
|
+
newState = 'waiting_input';
|
|
50
|
+
this.waitingWithBottomBorder.set(sessionId, true);
|
|
57
51
|
// Clear any pending busy timer
|
|
58
52
|
const existingTimer = this.busyTimers.get(sessionId);
|
|
59
53
|
if (existingTimer) {
|
|
@@ -90,14 +84,6 @@ export class SessionManager extends EventEmitter {
|
|
|
90
84
|
// Keep current busy state for now
|
|
91
85
|
newState = 'busy';
|
|
92
86
|
}
|
|
93
|
-
else if (!hasWaitingPrompt && !hasEscToInterrupt && !hasBottomBorder) {
|
|
94
|
-
// No special prompts or indicators, transition to idle
|
|
95
|
-
newState = 'idle';
|
|
96
|
-
// Clear the waiting flag when transitioning to idle
|
|
97
|
-
if (currentState === 'waiting_input') {
|
|
98
|
-
this.waitingWithBottomBorder.set(sessionId, false);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
87
|
return newState;
|
|
102
88
|
}
|
|
103
89
|
constructor() {
|
|
@@ -131,7 +117,11 @@ export class SessionManager extends EventEmitter {
|
|
|
131
117
|
const id = `session-${Date.now()}-${Math.random()
|
|
132
118
|
.toString(36)
|
|
133
119
|
.substr(2, 9)}`;
|
|
134
|
-
|
|
120
|
+
// Parse Claude command arguments from environment variable
|
|
121
|
+
const claudeArgs = process.env['CCMANAGER_CLAUDE_ARGS']
|
|
122
|
+
? process.env['CCMANAGER_CLAUDE_ARGS'].split(' ')
|
|
123
|
+
: [];
|
|
124
|
+
const ptyProcess = spawn('claude', claudeArgs, {
|
|
135
125
|
name: 'xterm-color',
|
|
136
126
|
cols: process.stdout.columns || 80,
|
|
137
127
|
rows: process.stdout.rows || 24,
|
|
@@ -62,16 +62,27 @@ describe('SessionManager', () => {
|
|
|
62
62
|
const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
|
|
63
63
|
expect(newState).toBe('busy');
|
|
64
64
|
});
|
|
65
|
-
it('should not change from waiting_input when bottom border was already seen', () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
// it('should not change from waiting_input when bottom border was already seen', () => {
|
|
66
|
+
// const cleanData = '└───────────────────────┘';
|
|
67
|
+
// const currentState: SessionState = 'waiting_input';
|
|
68
|
+
// vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(true);
|
|
69
|
+
//
|
|
70
|
+
// // First, simulate seeing waiting prompt with bottom border
|
|
71
|
+
// sessionManager.detectSessionState(
|
|
72
|
+
// '│ Do you want to continue?\n└───────────────────────┘',
|
|
73
|
+
// 'idle',
|
|
74
|
+
// mockSessionId,
|
|
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
|
+
// });
|
|
75
86
|
it('should clear waitingWithBottomBorder flag when transitioning to busy', () => {
|
|
76
87
|
const cleanData = 'Processing... Press ESC to interrupt';
|
|
77
88
|
const currentState = 'waiting_input';
|
|
@@ -84,18 +95,29 @@ describe('SessionManager', () => {
|
|
|
84
95
|
const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
|
|
85
96
|
expect(newState).toBe('busy');
|
|
86
97
|
});
|
|
87
|
-
it('should clear waitingWithBottomBorder flag when transitioning to idle', () => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
// it('should clear waitingWithBottomBorder flag when transitioning to idle', () => {
|
|
99
|
+
// const cleanData = 'Task completed successfully';
|
|
100
|
+
// const currentState: SessionState = 'waiting_input';
|
|
101
|
+
// vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(false);
|
|
102
|
+
//
|
|
103
|
+
// // First set up waiting state with bottom border
|
|
104
|
+
// vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(true);
|
|
105
|
+
// sessionManager.detectSessionState(
|
|
106
|
+
// '│ Do you want to continue?\n└───────────────────────┘',
|
|
107
|
+
// 'idle',
|
|
108
|
+
// mockSessionId,
|
|
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
|
+
// });
|
|
99
121
|
it('should transition from busy to idle after 500ms timer when no "esc to interrupt"', async () => {
|
|
100
122
|
// Create a mock session for the timer test
|
|
101
123
|
const mockWorktreePath = '/test/worktree';
|
|
@@ -103,6 +125,7 @@ describe('SessionManager', () => {
|
|
|
103
125
|
id: mockSessionId,
|
|
104
126
|
worktreePath: mockWorktreePath,
|
|
105
127
|
state: 'busy',
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
129
|
process: {},
|
|
107
130
|
output: [],
|
|
108
131
|
outputHistory: [],
|
|
@@ -131,6 +154,7 @@ describe('SessionManager', () => {
|
|
|
131
154
|
id: mockSessionId,
|
|
132
155
|
worktreePath: mockWorktreePath,
|
|
133
156
|
state: 'busy',
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
158
|
process: {},
|
|
135
159
|
output: [],
|
|
136
160
|
outputHistory: [],
|
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
|
-
}
|