s9n-devops-agent 1.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.
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/bin/cs-devops-agent +151 -0
- package/cleanup-sessions.sh +70 -0
- package/docs/PROJECT_INFO.md +115 -0
- package/docs/RELEASE_NOTES.md +189 -0
- package/docs/SESSION_MANAGEMENT.md +120 -0
- package/docs/TESTING.md +331 -0
- package/docs/houserules.md +267 -0
- package/docs/infrastructure.md +68 -0
- package/docs/testing-guide.md +224 -0
- package/package.json +68 -0
- package/src/agent-commands.js +211 -0
- package/src/claude-session-manager.js +488 -0
- package/src/close-session.js +316 -0
- package/src/cs-devops-agent-worker.js +1660 -0
- package/src/run-with-agent.js +372 -0
- package/src/session-coordinator.js +1207 -0
- package/src/setup-cs-devops-agent.js +985 -0
- package/src/worktree-manager.js +768 -0
- package/start-devops-session.sh +299 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Command Monitor
|
|
3
|
+
* Watches for special command files to trigger agent actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
class AgentCommandMonitor {
|
|
11
|
+
constructor(sessionId, worktreePath) {
|
|
12
|
+
this.sessionId = sessionId;
|
|
13
|
+
this.worktreePath = worktreePath;
|
|
14
|
+
this.commandFile = path.join(worktreePath, `.devops-command-${sessionId}`);
|
|
15
|
+
this.isClosing = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Start monitoring for commands
|
|
20
|
+
*/
|
|
21
|
+
startMonitoring() {
|
|
22
|
+
// Check for command file every 5 seconds
|
|
23
|
+
this.interval = setInterval(() => {
|
|
24
|
+
this.checkForCommands();
|
|
25
|
+
}, 5000);
|
|
26
|
+
|
|
27
|
+
console.log('[agent-commands] Monitoring for special commands...');
|
|
28
|
+
console.log(`[agent-commands] To close session, create: ${path.basename(this.commandFile)} with "CLOSE_SESSION"`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if command file exists and process it
|
|
33
|
+
*/
|
|
34
|
+
checkForCommands() {
|
|
35
|
+
if (this.isClosing) return;
|
|
36
|
+
|
|
37
|
+
if (fs.existsSync(this.commandFile)) {
|
|
38
|
+
try {
|
|
39
|
+
const command = fs.readFileSync(this.commandFile, 'utf8').trim();
|
|
40
|
+
|
|
41
|
+
// Remove the command file immediately
|
|
42
|
+
fs.unlinkSync(this.commandFile);
|
|
43
|
+
|
|
44
|
+
// Process the command
|
|
45
|
+
this.processCommand(command);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('[agent-commands] Error reading command file:', error.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Process a command
|
|
54
|
+
*/
|
|
55
|
+
processCommand(command) {
|
|
56
|
+
console.log(`[agent-commands] Received command: ${command}`);
|
|
57
|
+
|
|
58
|
+
switch (command.toUpperCase()) {
|
|
59
|
+
case 'CLOSE_SESSION':
|
|
60
|
+
case 'EXIT':
|
|
61
|
+
case 'QUIT':
|
|
62
|
+
this.handleCloseSession();
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'STATUS':
|
|
66
|
+
this.handleStatus();
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case 'PUSH':
|
|
70
|
+
this.handlePush();
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
default:
|
|
74
|
+
console.log(`[agent-commands] Unknown command: ${command}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Handle close session command
|
|
80
|
+
*/
|
|
81
|
+
handleCloseSession() {
|
|
82
|
+
if (this.isClosing) return;
|
|
83
|
+
this.isClosing = true;
|
|
84
|
+
|
|
85
|
+
console.log('[agent-commands] Initiating session cleanup...');
|
|
86
|
+
|
|
87
|
+
// Stop monitoring
|
|
88
|
+
if (this.interval) {
|
|
89
|
+
clearInterval(this.interval);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// 1. Check for uncommitted changes
|
|
94
|
+
const status = execSync(`git status --porcelain`, {
|
|
95
|
+
cwd: this.worktreePath,
|
|
96
|
+
encoding: 'utf8'
|
|
97
|
+
}).trim();
|
|
98
|
+
|
|
99
|
+
if (status) {
|
|
100
|
+
console.log('[agent-commands] Committing final changes...');
|
|
101
|
+
execSync(`git add -A`, { cwd: this.worktreePath });
|
|
102
|
+
execSync(`git commit -m "chore: session cleanup - final commit"`, {
|
|
103
|
+
cwd: this.worktreePath,
|
|
104
|
+
stdio: 'pipe'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 2. Push any unpushed commits
|
|
109
|
+
console.log('[agent-commands] Pushing changes...');
|
|
110
|
+
const branch = execSync(`git rev-parse --abbrev-ref HEAD`, {
|
|
111
|
+
cwd: this.worktreePath,
|
|
112
|
+
encoding: 'utf8'
|
|
113
|
+
}).trim();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
execSync(`git push origin ${branch}`, {
|
|
117
|
+
cwd: this.worktreePath,
|
|
118
|
+
stdio: 'pipe'
|
|
119
|
+
});
|
|
120
|
+
console.log('[agent-commands] Changes pushed successfully');
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.log('[agent-commands] Push failed or no changes to push');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 3. Create a cleanup marker file
|
|
126
|
+
const cleanupMarker = path.join(this.worktreePath, '.session-cleanup-requested');
|
|
127
|
+
fs.writeFileSync(cleanupMarker, JSON.stringify({
|
|
128
|
+
sessionId: this.sessionId,
|
|
129
|
+
timestamp: new Date().toISOString(),
|
|
130
|
+
worktree: this.worktreePath
|
|
131
|
+
}, null, 2));
|
|
132
|
+
|
|
133
|
+
console.log('[agent-commands] ✓ Session cleanup complete');
|
|
134
|
+
console.log('[agent-commands] The worktree has been prepared for removal.');
|
|
135
|
+
console.log('[agent-commands] Run "npm run devops:close" from the main repo to remove the worktree.');
|
|
136
|
+
console.log('[agent-commands] Stopping agent...');
|
|
137
|
+
|
|
138
|
+
// Exit the agent process
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}, 2000);
|
|
142
|
+
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('[agent-commands] Error during cleanup:', error.message);
|
|
145
|
+
this.isClosing = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Handle status command
|
|
151
|
+
*/
|
|
152
|
+
handleStatus() {
|
|
153
|
+
try {
|
|
154
|
+
const branch = execSync(`git rev-parse --abbrev-ref HEAD`, {
|
|
155
|
+
cwd: this.worktreePath,
|
|
156
|
+
encoding: 'utf8'
|
|
157
|
+
}).trim();
|
|
158
|
+
|
|
159
|
+
const status = execSync(`git status --short`, {
|
|
160
|
+
cwd: this.worktreePath,
|
|
161
|
+
encoding: 'utf8'
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
console.log('[agent-commands] Session Status:');
|
|
165
|
+
console.log(` Session ID: ${this.sessionId}`);
|
|
166
|
+
console.log(` Branch: ${branch}`);
|
|
167
|
+
console.log(` Working directory: ${this.worktreePath}`);
|
|
168
|
+
|
|
169
|
+
if (status.trim()) {
|
|
170
|
+
console.log(' Uncommitted changes:');
|
|
171
|
+
console.log(status);
|
|
172
|
+
} else {
|
|
173
|
+
console.log(' No uncommitted changes');
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('[agent-commands] Error getting status:', error.message);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Handle push command
|
|
182
|
+
*/
|
|
183
|
+
handlePush() {
|
|
184
|
+
try {
|
|
185
|
+
const branch = execSync(`git rev-parse --abbrev-ref HEAD`, {
|
|
186
|
+
cwd: this.worktreePath,
|
|
187
|
+
encoding: 'utf8'
|
|
188
|
+
}).trim();
|
|
189
|
+
|
|
190
|
+
console.log(`[agent-commands] Pushing branch ${branch}...`);
|
|
191
|
+
execSync(`git push origin ${branch}`, {
|
|
192
|
+
cwd: this.worktreePath,
|
|
193
|
+
stdio: 'inherit'
|
|
194
|
+
});
|
|
195
|
+
console.log('[agent-commands] Push complete');
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error('[agent-commands] Error pushing:', error.message);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Stop monitoring
|
|
203
|
+
*/
|
|
204
|
+
stop() {
|
|
205
|
+
if (this.interval) {
|
|
206
|
+
clearInterval(this.interval);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = AgentCommandMonitor;
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* CLAUDE SESSION MANAGER
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* Manages multiple Claude/Cline sessions with automatic worktree assignment.
|
|
9
|
+
* Each Claude session gets its own worktree to prevent conflicts.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* # Start a new Claude session
|
|
13
|
+
* node claude-session-manager.js start --task "feature-auth"
|
|
14
|
+
*
|
|
15
|
+
* # Get current session info
|
|
16
|
+
* node claude-session-manager.js current
|
|
17
|
+
*
|
|
18
|
+
* # List all active sessions
|
|
19
|
+
* node claude-session-manager.js list
|
|
20
|
+
*
|
|
21
|
+
* # End a session
|
|
22
|
+
* node claude-session-manager.js end --session <id>
|
|
23
|
+
*
|
|
24
|
+
* ============================================================================
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'fs';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
import { execSync } from 'child_process';
|
|
30
|
+
import { fileURLToPath } from 'url';
|
|
31
|
+
import { dirname } from 'path';
|
|
32
|
+
import crypto from 'crypto';
|
|
33
|
+
|
|
34
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
35
|
+
const __dirname = dirname(__filename);
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// CONFIGURATION
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
const CONFIG = {
|
|
42
|
+
sessionsFile: 'local_deploy/claude-sessions.json',
|
|
43
|
+
worktreesDir: 'local_deploy/worktrees',
|
|
44
|
+
sessionPrefix: 'claude-session',
|
|
45
|
+
colors: {
|
|
46
|
+
reset: '\x1b[0m',
|
|
47
|
+
bright: '\x1b[1m',
|
|
48
|
+
green: '\x1b[32m',
|
|
49
|
+
yellow: '\x1b[33m',
|
|
50
|
+
blue: '\x1b[36m',
|
|
51
|
+
red: '\x1b[31m',
|
|
52
|
+
magenta: '\x1b[35m',
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// SESSION MANAGER CLASS
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
class ClaudeSessionManager {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.repoRoot = this.getRepoRoot();
|
|
63
|
+
this.sessionsPath = path.join(this.repoRoot, CONFIG.sessionsFile);
|
|
64
|
+
this.worktreesPath = path.join(this.repoRoot, CONFIG.worktreesDir);
|
|
65
|
+
this.loadSessions();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getRepoRoot() {
|
|
69
|
+
try {
|
|
70
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('Error: Not in a git repository');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
loadSessions() {
|
|
78
|
+
if (fs.existsSync(this.sessionsPath)) {
|
|
79
|
+
const data = fs.readFileSync(this.sessionsPath, 'utf8');
|
|
80
|
+
this.sessions = JSON.parse(data);
|
|
81
|
+
} else {
|
|
82
|
+
this.sessions = {};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
saveSessions() {
|
|
87
|
+
fs.writeFileSync(this.sessionsPath, JSON.stringify(this.sessions, null, 2));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
generateSessionId() {
|
|
91
|
+
// Generate a short, unique session ID
|
|
92
|
+
const timestamp = Date.now().toString(36);
|
|
93
|
+
const random = crypto.randomBytes(2).toString('hex');
|
|
94
|
+
return `${timestamp}-${random}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Start a new Claude session with its own worktree
|
|
99
|
+
*/
|
|
100
|
+
startSession(task, options = {}) {
|
|
101
|
+
const sessionId = options.sessionId || this.generateSessionId();
|
|
102
|
+
const sessionName = `${CONFIG.sessionPrefix}-${sessionId}`;
|
|
103
|
+
const worktreeName = `${sessionName}-${task.replace(/\s+/g, '-').toLowerCase()}`;
|
|
104
|
+
const worktreePath = path.join(this.worktreesPath, worktreeName);
|
|
105
|
+
const branchName = `claude/${sessionId}/${task.replace(/\s+/g, '-').toLowerCase()}`;
|
|
106
|
+
|
|
107
|
+
console.log(`${CONFIG.colors.blue}Starting new Claude session...${CONFIG.colors.reset}`);
|
|
108
|
+
console.log(`Session ID: ${CONFIG.colors.bright}${sessionId}${CONFIG.colors.reset}`);
|
|
109
|
+
console.log(`Task: ${task}`);
|
|
110
|
+
|
|
111
|
+
// Check if session already exists
|
|
112
|
+
if (this.sessions[sessionId]) {
|
|
113
|
+
console.log(`${CONFIG.colors.yellow}Session ${sessionId} already exists${CONFIG.colors.reset}`);
|
|
114
|
+
return this.sessions[sessionId];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Create worktree
|
|
118
|
+
try {
|
|
119
|
+
if (!fs.existsSync(this.worktreesPath)) {
|
|
120
|
+
fs.mkdirSync(this.worktreesPath, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Create new branch and worktree
|
|
124
|
+
console.log(`Creating worktree at: ${worktreePath}`);
|
|
125
|
+
execSync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, { stdio: 'inherit' });
|
|
126
|
+
|
|
127
|
+
// Create session configuration
|
|
128
|
+
const session = {
|
|
129
|
+
id: sessionId,
|
|
130
|
+
name: sessionName,
|
|
131
|
+
task: task,
|
|
132
|
+
worktree: {
|
|
133
|
+
path: worktreePath,
|
|
134
|
+
branch: branchName,
|
|
135
|
+
name: worktreeName
|
|
136
|
+
},
|
|
137
|
+
created: new Date().toISOString(),
|
|
138
|
+
status: 'active',
|
|
139
|
+
pid: process.pid,
|
|
140
|
+
commitMsgFile: `.${sessionName}-commit-msg`,
|
|
141
|
+
agentConfig: {
|
|
142
|
+
AC_MSG_FILE: `.${sessionName}-commit-msg`,
|
|
143
|
+
AC_BRANCH_PREFIX: `claude_${sessionId}_`,
|
|
144
|
+
AGENT_NAME: sessionName
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Save session
|
|
149
|
+
this.sessions[sessionId] = session;
|
|
150
|
+
this.saveSessions();
|
|
151
|
+
|
|
152
|
+
// Create session config file in worktree
|
|
153
|
+
this.createWorktreeConfig(session);
|
|
154
|
+
|
|
155
|
+
// Create VS Code workspace file
|
|
156
|
+
this.createVSCodeWorkspace(session);
|
|
157
|
+
|
|
158
|
+
console.log(`${CONFIG.colors.green}✓ Session created successfully!${CONFIG.colors.reset}`);
|
|
159
|
+
console.log(`\n${CONFIG.colors.bright}To use this session:${CONFIG.colors.reset}`);
|
|
160
|
+
console.log(`1. Open VS Code: ${CONFIG.colors.blue}code "${worktreePath}"${CONFIG.colors.reset}`);
|
|
161
|
+
console.log(`2. Or navigate: ${CONFIG.colors.blue}cd "${worktreePath}"${CONFIG.colors.reset}`);
|
|
162
|
+
console.log(`3. Start agent: ${CONFIG.colors.blue}npm run agent:session ${sessionId}${CONFIG.colors.reset}`);
|
|
163
|
+
|
|
164
|
+
// Output for Claude to read
|
|
165
|
+
console.log(`\n${CONFIG.colors.magenta}[CLAUDE_SESSION_INFO]${CONFIG.colors.reset}`);
|
|
166
|
+
console.log(JSON.stringify({
|
|
167
|
+
sessionId: sessionId,
|
|
168
|
+
worktreePath: worktreePath,
|
|
169
|
+
branchName: branchName,
|
|
170
|
+
commitMsgFile: session.commitMsgFile
|
|
171
|
+
}, null, 2));
|
|
172
|
+
|
|
173
|
+
return session;
|
|
174
|
+
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(`${CONFIG.colors.red}Failed to create session: ${error.message}${CONFIG.colors.reset}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create configuration files in the worktree
|
|
183
|
+
*/
|
|
184
|
+
createWorktreeConfig(session) {
|
|
185
|
+
const configPath = path.join(session.worktree.path, '.claude-session.json');
|
|
186
|
+
fs.writeFileSync(configPath, JSON.stringify(session, null, 2));
|
|
187
|
+
|
|
188
|
+
// Create commit message file
|
|
189
|
+
const msgFilePath = path.join(session.worktree.path, session.commitMsgFile);
|
|
190
|
+
fs.writeFileSync(msgFilePath, '');
|
|
191
|
+
|
|
192
|
+
// Create .env.claude file for environment variables
|
|
193
|
+
const envContent = Object.entries(session.agentConfig)
|
|
194
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
195
|
+
.join('\n');
|
|
196
|
+
|
|
197
|
+
const envPath = path.join(session.worktree.path, '.env.claude');
|
|
198
|
+
fs.writeFileSync(envPath, envContent);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Create VS Code workspace configuration
|
|
203
|
+
*/
|
|
204
|
+
createVSCodeWorkspace(session) {
|
|
205
|
+
const vscodeDir = path.join(session.worktree.path, '.vscode');
|
|
206
|
+
|
|
207
|
+
if (!fs.existsSync(vscodeDir)) {
|
|
208
|
+
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Settings for this workspace
|
|
212
|
+
const settings = {
|
|
213
|
+
'window.title': `Claude ${session.id} - ${session.task}`,
|
|
214
|
+
'terminal.integrated.env.osx': session.agentConfig,
|
|
215
|
+
'terminal.integrated.env.linux': session.agentConfig,
|
|
216
|
+
'terminal.integrated.env.windows': session.agentConfig,
|
|
217
|
+
'files.exclude': {
|
|
218
|
+
'.claude-session.json': false,
|
|
219
|
+
[session.commitMsgFile]: false
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
fs.writeFileSync(
|
|
224
|
+
path.join(vscodeDir, 'settings.json'),
|
|
225
|
+
JSON.stringify(settings, null, 2)
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Create workspace file
|
|
229
|
+
const workspaceFile = {
|
|
230
|
+
folders: [
|
|
231
|
+
{
|
|
232
|
+
path: '.',
|
|
233
|
+
name: `Claude: ${session.task}`
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
settings: settings
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
fs.writeFileSync(
|
|
240
|
+
path.join(session.worktree.path, `${session.name}.code-workspace`),
|
|
241
|
+
JSON.stringify(workspaceFile, null, 2)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get current session based on current directory
|
|
247
|
+
*/
|
|
248
|
+
getCurrentSession() {
|
|
249
|
+
const cwd = process.cwd();
|
|
250
|
+
|
|
251
|
+
// Check if we're in a worktree
|
|
252
|
+
for (const [sessionId, session] of Object.entries(this.sessions)) {
|
|
253
|
+
if (session.worktree.path === cwd || cwd.startsWith(session.worktree.path + '/')) {
|
|
254
|
+
return session;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check for session file in current directory
|
|
259
|
+
const sessionFilePath = path.join(cwd, '.claude-session.json');
|
|
260
|
+
if (fs.existsSync(sessionFilePath)) {
|
|
261
|
+
const sessionData = JSON.parse(fs.readFileSync(sessionFilePath, 'utf8'));
|
|
262
|
+
return sessionData;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* List all active sessions
|
|
270
|
+
*/
|
|
271
|
+
listSessions() {
|
|
272
|
+
console.log(`\n${CONFIG.colors.bright}Active Claude Sessions:${CONFIG.colors.reset}`);
|
|
273
|
+
|
|
274
|
+
const activeSessions = Object.values(this.sessions).filter(s => s.status === 'active');
|
|
275
|
+
|
|
276
|
+
if (activeSessions.length === 0) {
|
|
277
|
+
console.log('No active sessions');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const session of activeSessions) {
|
|
282
|
+
const exists = fs.existsSync(session.worktree.path);
|
|
283
|
+
const status = exists ? CONFIG.colors.green + '✓' : CONFIG.colors.red + '✗';
|
|
284
|
+
|
|
285
|
+
console.log(`\n${status} ${CONFIG.colors.bright}${session.id}${CONFIG.colors.reset}`);
|
|
286
|
+
console.log(` Task: ${session.task}`);
|
|
287
|
+
console.log(` Branch: ${session.worktree.branch}`);
|
|
288
|
+
console.log(` Path: ${session.worktree.path}`);
|
|
289
|
+
console.log(` Created: ${new Date(session.created).toLocaleString()}`);
|
|
290
|
+
|
|
291
|
+
if (!exists) {
|
|
292
|
+
console.log(` ${CONFIG.colors.yellow}(Worktree missing)${CONFIG.colors.reset}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log(`\nTotal active sessions: ${activeSessions.length}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* End a session and optionally clean up
|
|
301
|
+
*/
|
|
302
|
+
endSession(sessionId, options = {}) {
|
|
303
|
+
const session = this.sessions[sessionId];
|
|
304
|
+
|
|
305
|
+
if (!session) {
|
|
306
|
+
console.error(`Session not found: ${sessionId}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log(`${CONFIG.colors.yellow}Ending session: ${sessionId}${CONFIG.colors.reset}`);
|
|
311
|
+
|
|
312
|
+
// Mark as inactive
|
|
313
|
+
session.status = 'inactive';
|
|
314
|
+
session.ended = new Date().toISOString();
|
|
315
|
+
|
|
316
|
+
if (options.cleanup) {
|
|
317
|
+
// Remove worktree
|
|
318
|
+
if (fs.existsSync(session.worktree.path)) {
|
|
319
|
+
console.log(`Removing worktree: ${session.worktree.path}`);
|
|
320
|
+
try {
|
|
321
|
+
execSync(`git worktree remove "${session.worktree.path}" --force`, { stdio: 'inherit' });
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error(`Failed to remove worktree: ${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Delete branch if requested
|
|
328
|
+
if (options.deleteBranch) {
|
|
329
|
+
console.log(`Deleting branch: ${session.worktree.branch}`);
|
|
330
|
+
try {
|
|
331
|
+
execSync(`git branch -D ${session.worktree.branch}`, { stdio: 'inherit' });
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error(`Failed to delete branch: ${error.message}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Remove from sessions
|
|
338
|
+
delete this.sessions[sessionId];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
this.saveSessions();
|
|
342
|
+
console.log(`${CONFIG.colors.green}Session ended${CONFIG.colors.reset}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Start the DevOps agent for a specific session
|
|
347
|
+
*/
|
|
348
|
+
startAgent(sessionId) {
|
|
349
|
+
const session = this.sessions[sessionId];
|
|
350
|
+
|
|
351
|
+
if (!session) {
|
|
352
|
+
console.error(`Session not found: ${sessionId}`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
console.log(`${CONFIG.colors.blue}Starting DevOps agent for session: ${sessionId}${CONFIG.colors.reset}`);
|
|
357
|
+
|
|
358
|
+
// Build environment variables
|
|
359
|
+
const env = Object.entries(session.agentConfig)
|
|
360
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
361
|
+
.join(' ');
|
|
362
|
+
|
|
363
|
+
// Command to start the agent
|
|
364
|
+
const agentScript = path.join(__dirname, 'cs-devops-agent-worker.js');
|
|
365
|
+
const command = `cd "${session.worktree.path}" && ${env} node "${agentScript}"`;
|
|
366
|
+
|
|
367
|
+
console.log(`\nRun this command to start the agent:`);
|
|
368
|
+
console.log(`${CONFIG.colors.blue}${command}${CONFIG.colors.reset}`);
|
|
369
|
+
|
|
370
|
+
return command;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ============================================================================
|
|
375
|
+
// CLI INTERFACE
|
|
376
|
+
// ============================================================================
|
|
377
|
+
|
|
378
|
+
function showHelp() {
|
|
379
|
+
console.log(`
|
|
380
|
+
${CONFIG.colors.bright}Claude Session Manager${CONFIG.colors.reset}
|
|
381
|
+
|
|
382
|
+
Usage:
|
|
383
|
+
node claude-session-manager.js <command> [options]
|
|
384
|
+
|
|
385
|
+
Commands:
|
|
386
|
+
start --task <name> Start a new Claude session with a task
|
|
387
|
+
current Show current session info
|
|
388
|
+
list List all active sessions
|
|
389
|
+
end --session <id> End a session
|
|
390
|
+
agent --session <id> Get command to start agent for session
|
|
391
|
+
help Show this help message
|
|
392
|
+
|
|
393
|
+
Options:
|
|
394
|
+
--task <name> Task or feature name
|
|
395
|
+
--session <id> Session ID
|
|
396
|
+
--cleanup Remove worktree when ending session
|
|
397
|
+
--delete-branch Delete branch when ending session
|
|
398
|
+
|
|
399
|
+
Examples:
|
|
400
|
+
# Start a new session for authentication feature
|
|
401
|
+
node claude-session-manager.js start --task "authentication-feature"
|
|
402
|
+
|
|
403
|
+
# List all active sessions
|
|
404
|
+
node claude-session-manager.js list
|
|
405
|
+
|
|
406
|
+
# End a session and clean up
|
|
407
|
+
node claude-session-manager.js end --session abc123 --cleanup --delete-branch
|
|
408
|
+
|
|
409
|
+
# Get current session info (run from within a worktree)
|
|
410
|
+
node claude-session-manager.js current
|
|
411
|
+
`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function main() {
|
|
415
|
+
const args = process.argv.slice(2);
|
|
416
|
+
const command = args[0];
|
|
417
|
+
|
|
418
|
+
if (!command || command === 'help') {
|
|
419
|
+
showHelp();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const manager = new ClaudeSessionManager();
|
|
424
|
+
|
|
425
|
+
switch (command) {
|
|
426
|
+
case 'start': {
|
|
427
|
+
const taskIdx = args.indexOf('--task');
|
|
428
|
+
if (taskIdx === -1 || !args[taskIdx + 1]) {
|
|
429
|
+
console.error('Error: --task is required for start command');
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
const task = args[taskIdx + 1];
|
|
433
|
+
manager.startSession(task);
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
case 'current': {
|
|
438
|
+
const session = manager.getCurrentSession();
|
|
439
|
+
if (session) {
|
|
440
|
+
console.log(`${CONFIG.colors.bright}Current Session:${CONFIG.colors.reset}`);
|
|
441
|
+
console.log(JSON.stringify(session, null, 2));
|
|
442
|
+
} else {
|
|
443
|
+
console.log('Not in a Claude session worktree');
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
case 'list': {
|
|
449
|
+
manager.listSessions();
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
case 'end': {
|
|
454
|
+
const sessionIdx = args.indexOf('--session');
|
|
455
|
+
if (sessionIdx === -1 || !args[sessionIdx + 1]) {
|
|
456
|
+
console.error('Error: --session is required for end command');
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
const sessionId = args[sessionIdx + 1];
|
|
460
|
+
const cleanup = args.includes('--cleanup');
|
|
461
|
+
const deleteBranch = args.includes('--delete-branch');
|
|
462
|
+
manager.endSession(sessionId, { cleanup, deleteBranch });
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
case 'agent': {
|
|
467
|
+
const sessionIdx = args.indexOf('--session');
|
|
468
|
+
if (sessionIdx === -1 || !args[sessionIdx + 1]) {
|
|
469
|
+
console.error('Error: --session is required for agent command');
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
const sessionId = args[sessionIdx + 1];
|
|
473
|
+
manager.startAgent(sessionId);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
default:
|
|
478
|
+
console.error(`Unknown command: ${command}`);
|
|
479
|
+
showHelp();
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Run the CLI
|
|
485
|
+
main().catch(err => {
|
|
486
|
+
console.error(`Error: ${err.message}`);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
});
|