sessioncast-cli 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 +137 -0
- package/dist/agent/api-client.d.ts +27 -0
- package/dist/agent/api-client.js +295 -0
- package/dist/agent/exec-service.d.ts +6 -0
- package/dist/agent/exec-service.js +126 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.js +24 -0
- package/dist/agent/llm-service.d.ts +9 -0
- package/dist/agent/llm-service.js +156 -0
- package/dist/agent/runner.d.ts +16 -0
- package/dist/agent/runner.js +187 -0
- package/dist/agent/session-handler.d.ts +28 -0
- package/dist/agent/session-handler.js +184 -0
- package/dist/agent/tmux.d.ts +29 -0
- package/dist/agent/tmux.js +157 -0
- package/dist/agent/types.d.ts +72 -0
- package/dist/agent/types.js +2 -0
- package/dist/agent/websocket.d.ts +45 -0
- package/dist/agent/websocket.js +288 -0
- package/dist/api.d.ts +31 -0
- package/dist/api.js +78 -0
- package/dist/commands/agent.d.ts +5 -0
- package/dist/commands/agent.js +19 -0
- package/dist/commands/agents.d.ts +1 -0
- package/dist/commands/agents.js +77 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +41 -0
- package/dist/commands/project.d.ts +33 -0
- package/dist/commands/project.js +359 -0
- package/dist/commands/sendkeys.d.ts +3 -0
- package/dist/commands/sendkeys.js +66 -0
- package/dist/commands/sessions.d.ts +1 -0
- package/dist/commands/sessions.js +89 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +125 -0
- package/dist/project/executor.d.ts +118 -0
- package/dist/project/executor.js +893 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +20 -0
- package/dist/project/manager.d.ts +79 -0
- package/dist/project/manager.js +397 -0
- package/dist/project/relay-client.d.ts +87 -0
- package/dist/project/relay-client.js +200 -0
- package/dist/project/types.d.ts +43 -0
- package/dist/project/types.js +3 -0
- package/package.json +59 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.WorkflowExecutor = void 0;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const events_1 = require("events");
|
|
41
|
+
const relay_client_1 = require("./relay-client");
|
|
42
|
+
const POLL_INTERVAL_MS = 2000;
|
|
43
|
+
const MAX_RETRIES = 3;
|
|
44
|
+
const CLAUDE_START_DELAY_MS = 1000;
|
|
45
|
+
const CLAUDE_ANALYSIS_TIMEOUT_MS = 55000; // 55 seconds (less than server's 60s timeout)
|
|
46
|
+
class WorkflowExecutor extends events_1.EventEmitter {
|
|
47
|
+
constructor(manager, options = {}) {
|
|
48
|
+
super();
|
|
49
|
+
this.workflow = null;
|
|
50
|
+
this.running = false;
|
|
51
|
+
this.pollTimer = null;
|
|
52
|
+
this.relayClient = null;
|
|
53
|
+
this.manager = manager;
|
|
54
|
+
this.options = {
|
|
55
|
+
autoLaunchClaude: options.autoLaunchClaude ?? true,
|
|
56
|
+
claudeCommand: options.claudeCommand ?? 'claude',
|
|
57
|
+
relayUrl: options.relayUrl,
|
|
58
|
+
relayToken: options.relayToken,
|
|
59
|
+
machineId: options.machineId ?? os.hostname()
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Start workflow execution
|
|
64
|
+
*/
|
|
65
|
+
async start() {
|
|
66
|
+
this.workflow = this.manager.loadWorkflow();
|
|
67
|
+
if (!this.workflow) {
|
|
68
|
+
throw new Error('No workflow found. Run project init first.');
|
|
69
|
+
}
|
|
70
|
+
this.running = true;
|
|
71
|
+
console.log(`Starting workflow: ${this.workflow.name}`);
|
|
72
|
+
// Connect to relay server if configured
|
|
73
|
+
if (this.options.relayUrl && this.options.relayToken) {
|
|
74
|
+
await this.connectToRelay();
|
|
75
|
+
}
|
|
76
|
+
// Initialize status
|
|
77
|
+
const status = {
|
|
78
|
+
workflow: this.workflow.name,
|
|
79
|
+
startedAt: new Date().toISOString(),
|
|
80
|
+
status: 'running',
|
|
81
|
+
agents: {}
|
|
82
|
+
};
|
|
83
|
+
for (const agent of this.workflow.agents) {
|
|
84
|
+
status.agents[agent.id] = { status: 'pending' };
|
|
85
|
+
}
|
|
86
|
+
this.manager.saveStatus(status);
|
|
87
|
+
// Start PM Agent first
|
|
88
|
+
await this.startPMAgent();
|
|
89
|
+
// Then start the workflow execution loop
|
|
90
|
+
this.runExecutionLoop();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Start PM Agent tmux session
|
|
94
|
+
*/
|
|
95
|
+
async startPMAgent() {
|
|
96
|
+
const sessionName = this.manager.getTmuxSessionName('pm');
|
|
97
|
+
const pmAgentPath = path.join(this.manager.getProjectPath(), 'tools', 'pm-agent');
|
|
98
|
+
console.log(`Starting PM Agent: ${sessionName}`);
|
|
99
|
+
try {
|
|
100
|
+
// Check if session already exists
|
|
101
|
+
try {
|
|
102
|
+
(0, child_process_1.execSync)(`tmux has-session -t "${sessionName}" 2>/dev/null`);
|
|
103
|
+
console.log(`PM Agent session already exists: ${sessionName}`);
|
|
104
|
+
// Update status to running even if session already exists
|
|
105
|
+
this.manager.updateAgentStatus('pm', {
|
|
106
|
+
status: 'running',
|
|
107
|
+
startedAt: new Date().toISOString()
|
|
108
|
+
});
|
|
109
|
+
this.updateRelayStatus();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Session doesn't exist, create it
|
|
114
|
+
}
|
|
115
|
+
// Create new tmux session
|
|
116
|
+
(0, child_process_1.execSync)(`tmux new-session -d -s "${sessionName}" -c "${pmAgentPath}"`, { stdio: 'pipe' });
|
|
117
|
+
console.log(`Created PM Agent session: ${sessionName}`);
|
|
118
|
+
// Auto-launch Claude Code CLI
|
|
119
|
+
if (this.options.autoLaunchClaude) {
|
|
120
|
+
await this.launchClaudeInSession(sessionName, 'pm');
|
|
121
|
+
}
|
|
122
|
+
// Update status
|
|
123
|
+
this.manager.updateAgentStatus('pm', {
|
|
124
|
+
status: 'running',
|
|
125
|
+
startedAt: new Date().toISOString()
|
|
126
|
+
});
|
|
127
|
+
this.updateRelayStatus();
|
|
128
|
+
this.emit('agent-started', { agentId: 'pm', sessionName });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error('Failed to start PM Agent:', error);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Start a work agent
|
|
137
|
+
*/
|
|
138
|
+
async startAgent(agent) {
|
|
139
|
+
const sessionName = this.manager.getTmuxSessionName(agent.id);
|
|
140
|
+
const agentPath = path.join(this.manager.getProjectPath(), agent.workDir);
|
|
141
|
+
console.log(`Starting agent: ${agent.id} (${agent.name})`);
|
|
142
|
+
try {
|
|
143
|
+
// Check if session already exists
|
|
144
|
+
try {
|
|
145
|
+
(0, child_process_1.execSync)(`tmux has-session -t "${sessionName}" 2>/dev/null`);
|
|
146
|
+
console.log(`Agent session already exists: ${sessionName}`);
|
|
147
|
+
// Update status to running even if session already exists
|
|
148
|
+
this.manager.updateAgentStatus(agent.id, {
|
|
149
|
+
status: 'running',
|
|
150
|
+
startedAt: new Date().toISOString(),
|
|
151
|
+
currentTask: agent.tasks[0] || 'Resuming...'
|
|
152
|
+
});
|
|
153
|
+
this.updateRelayStatus();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Session doesn't exist, create it
|
|
158
|
+
}
|
|
159
|
+
// Ensure work folder exists with CLAUDE.md
|
|
160
|
+
this.manager.createWorkAgent(agent.id, agent.name, agent.tasks);
|
|
161
|
+
// Create new tmux session
|
|
162
|
+
(0, child_process_1.execSync)(`tmux new-session -d -s "${sessionName}" -c "${agentPath}"`, { stdio: 'pipe' });
|
|
163
|
+
console.log(`Created agent session: ${sessionName}`);
|
|
164
|
+
// Auto-launch Claude Code CLI with task prompt
|
|
165
|
+
if (this.options.autoLaunchClaude) {
|
|
166
|
+
const taskPrompt = this.buildTaskPrompt(agent);
|
|
167
|
+
await this.launchClaudeInSession(sessionName, agent.id, taskPrompt);
|
|
168
|
+
}
|
|
169
|
+
// Update status
|
|
170
|
+
this.manager.updateAgentStatus(agent.id, {
|
|
171
|
+
status: 'running',
|
|
172
|
+
startedAt: new Date().toISOString(),
|
|
173
|
+
currentTask: agent.tasks[0] || 'Starting...'
|
|
174
|
+
});
|
|
175
|
+
this.updateRelayStatus();
|
|
176
|
+
this.emit('agent-started', { agentId: agent.id, sessionName });
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error(`Failed to start agent ${agent.id}:`, error);
|
|
180
|
+
this.manager.updateAgentStatus(agent.id, {
|
|
181
|
+
status: 'failed',
|
|
182
|
+
error: String(error)
|
|
183
|
+
});
|
|
184
|
+
this.updateRelayStatus();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Build task prompt for agent
|
|
189
|
+
*/
|
|
190
|
+
buildTaskPrompt(agent) {
|
|
191
|
+
const tasks = agent.tasks.map((t, i) => `${i + 1}. ${t}`).join('\n');
|
|
192
|
+
return `다음 태스크를 수행해주세요:\n${tasks}\n\n완료 후 반드시:\n1. output.md에 결과 기록\n2. touch DONE 실행`;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Launch Claude Code CLI in a tmux session
|
|
196
|
+
*/
|
|
197
|
+
async launchClaudeInSession(sessionName, agentId, initialPrompt) {
|
|
198
|
+
// Wait for shell to be ready
|
|
199
|
+
await this.sleep(CLAUDE_START_DELAY_MS);
|
|
200
|
+
try {
|
|
201
|
+
// Send claude command
|
|
202
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${sessionName}" "${this.options.claudeCommand}" Enter`, {
|
|
203
|
+
stdio: 'pipe'
|
|
204
|
+
});
|
|
205
|
+
console.log(`Launched Claude Code in ${agentId}`);
|
|
206
|
+
// If there's an initial prompt, send it after claude starts
|
|
207
|
+
if (initialPrompt) {
|
|
208
|
+
// Wait for claude to initialize
|
|
209
|
+
await this.sleep(3000);
|
|
210
|
+
// Send the initial prompt
|
|
211
|
+
const escapedPrompt = initialPrompt.replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
212
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${sessionName}" "${escapedPrompt}" Enter`, {
|
|
213
|
+
stdio: 'pipe'
|
|
214
|
+
});
|
|
215
|
+
console.log(`Sent initial prompt to ${agentId}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error(`Failed to launch Claude in ${agentId}:`, error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Sleep helper
|
|
224
|
+
*/
|
|
225
|
+
sleep(ms) {
|
|
226
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Main execution loop
|
|
230
|
+
*/
|
|
231
|
+
runExecutionLoop() {
|
|
232
|
+
const check = () => {
|
|
233
|
+
if (!this.running || !this.workflow) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const status = this.manager.loadStatus();
|
|
237
|
+
if (!status)
|
|
238
|
+
return;
|
|
239
|
+
// Check each agent
|
|
240
|
+
for (const agent of this.workflow.agents) {
|
|
241
|
+
const agentStatus = status.agents[agent.id];
|
|
242
|
+
// Skip completed or failed agents
|
|
243
|
+
if (agentStatus?.status === 'completed' || agentStatus?.status === 'failed') {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
// Check if dependencies are met
|
|
247
|
+
const dependenciesMet = agent.dependsOn.every(depId => {
|
|
248
|
+
return status.agents[depId]?.status === 'completed';
|
|
249
|
+
});
|
|
250
|
+
if (!dependenciesMet) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// Check if agent is pending (not started yet)
|
|
254
|
+
if (agentStatus?.status === 'pending') {
|
|
255
|
+
this.startAgent(agent);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
// Check if running agent is completed
|
|
259
|
+
if (agentStatus?.status === 'running') {
|
|
260
|
+
if (this.manager.isAgentCompleted(agent.id)) {
|
|
261
|
+
const output = this.manager.getAgentOutput(agent.id);
|
|
262
|
+
this.manager.updateAgentStatus(agent.id, {
|
|
263
|
+
status: 'completed',
|
|
264
|
+
completedAt: new Date().toISOString(),
|
|
265
|
+
output: output || undefined
|
|
266
|
+
});
|
|
267
|
+
// Append output to shared context
|
|
268
|
+
if (output) {
|
|
269
|
+
this.manager.appendToContext(`\n## ${agent.name} Output\n${output}\n`);
|
|
270
|
+
}
|
|
271
|
+
console.log(`Agent completed: ${agent.id}`);
|
|
272
|
+
this.updateRelayStatus();
|
|
273
|
+
this.emit('agent-completed', { agentId: agent.id, output });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Check if workflow is complete
|
|
278
|
+
const updatedStatus = this.manager.loadStatus();
|
|
279
|
+
if (updatedStatus) {
|
|
280
|
+
const allCompleted = this.workflow.agents.every(a => updatedStatus.agents[a.id]?.status === 'completed');
|
|
281
|
+
if (allCompleted) {
|
|
282
|
+
console.log('Workflow completed!');
|
|
283
|
+
updatedStatus.status = 'completed';
|
|
284
|
+
this.manager.saveStatus(updatedStatus);
|
|
285
|
+
this.updateRelayStatus();
|
|
286
|
+
this.emit('workflow-completed', updatedStatus);
|
|
287
|
+
this.stop();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const anyFailed = this.workflow.agents.some(a => updatedStatus.agents[a.id]?.status === 'failed');
|
|
291
|
+
if (anyFailed) {
|
|
292
|
+
console.log('Workflow failed!');
|
|
293
|
+
updatedStatus.status = 'failed';
|
|
294
|
+
this.manager.saveStatus(updatedStatus);
|
|
295
|
+
this.updateRelayStatus();
|
|
296
|
+
this.emit('workflow-failed', updatedStatus);
|
|
297
|
+
this.stop();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Continue polling
|
|
302
|
+
this.pollTimer = setTimeout(check, POLL_INTERVAL_MS);
|
|
303
|
+
};
|
|
304
|
+
check();
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Connect to relay server
|
|
308
|
+
*/
|
|
309
|
+
async connectToRelay() {
|
|
310
|
+
if (!this.options.relayUrl || !this.options.relayToken || !this.workflow) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
this.relayClient = new relay_client_1.ProjectRelayClient({
|
|
315
|
+
url: this.options.relayUrl,
|
|
316
|
+
token: this.options.relayToken,
|
|
317
|
+
machineId: this.options.machineId || os.hostname(),
|
|
318
|
+
projectId: this.manager.getProjectId(),
|
|
319
|
+
projectName: this.workflow.name,
|
|
320
|
+
mission: this.workflow.mission,
|
|
321
|
+
sources: this.manager.getSources(),
|
|
322
|
+
});
|
|
323
|
+
await this.relayClient.connect();
|
|
324
|
+
console.log('Connected to relay server');
|
|
325
|
+
this.relayClient.on('error', (error) => {
|
|
326
|
+
console.error('Relay connection error:', error.message);
|
|
327
|
+
});
|
|
328
|
+
this.relayClient.on('disconnected', () => {
|
|
329
|
+
console.log('Disconnected from relay server');
|
|
330
|
+
});
|
|
331
|
+
// Handle incoming messages from server
|
|
332
|
+
this.relayClient.on('message', (message) => {
|
|
333
|
+
this.handleRelayMessage(message);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
console.error('Failed to connect to relay server:', error);
|
|
338
|
+
// Don't throw - relay is optional
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Handle incoming messages from relay server
|
|
343
|
+
*/
|
|
344
|
+
handleRelayMessage(message) {
|
|
345
|
+
switch (message.type) {
|
|
346
|
+
case 'analyzeMission':
|
|
347
|
+
this.handleAnalyzeMission(message);
|
|
348
|
+
break;
|
|
349
|
+
case 'addSource':
|
|
350
|
+
this.handleAddSource(message);
|
|
351
|
+
break;
|
|
352
|
+
default:
|
|
353
|
+
// Ignore other message types
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Handle mission analysis request - calls Claude Code to analyze
|
|
359
|
+
*/
|
|
360
|
+
async handleAnalyzeMission(message) {
|
|
361
|
+
const requestId = message.requestId;
|
|
362
|
+
const meta = message.meta || {};
|
|
363
|
+
const mission = meta.mission || '';
|
|
364
|
+
const contextPaths = meta.contextPaths ? meta.contextPaths.split(',').filter(Boolean) : [];
|
|
365
|
+
if (!requestId) {
|
|
366
|
+
console.error('analyzeMission: missing requestId');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
console.log(`Analyzing mission: ${mission.substring(0, 50)}...`);
|
|
370
|
+
if (contextPaths.length > 0) {
|
|
371
|
+
console.log(`With context paths: ${contextPaths.join(', ')}`);
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const result = await this.callClaudeForAnalysis(mission, contextPaths);
|
|
375
|
+
this.relayClient?.sendAnalysisResponse(requestId, result.steps, result.decisions);
|
|
376
|
+
console.log(`Mission analysis completed: ${result.steps.length} steps, ${result.decisions.length} decisions`);
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
console.error('Mission analysis failed:', error);
|
|
380
|
+
this.relayClient?.sendAnalysisError(requestId, String(error));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Handle addSource request - clone/copy source into work folder
|
|
385
|
+
*/
|
|
386
|
+
async handleAddSource(message) {
|
|
387
|
+
const requestId = message.requestId;
|
|
388
|
+
const meta = message.meta || {};
|
|
389
|
+
const sourceType = meta.sourceType || 'git';
|
|
390
|
+
const sourceUrl = meta.sourceUrl || '';
|
|
391
|
+
const targetFolder = meta.targetFolder || '';
|
|
392
|
+
if (!requestId || !sourceUrl) {
|
|
393
|
+
console.error('addSource: missing requestId or sourceUrl');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
console.log(`Adding source: ${sourceType} ${sourceUrl} -> work/${targetFolder || 'auto'}`);
|
|
397
|
+
try {
|
|
398
|
+
const result = await this.executeAddSource(sourceType, sourceUrl, targetFolder);
|
|
399
|
+
this.relayClient?.sendAddSourceResult(requestId, result);
|
|
400
|
+
console.log(`Source added successfully: ${result.folder}`);
|
|
401
|
+
// Update sources list on server
|
|
402
|
+
const sources = this.manager.getSources();
|
|
403
|
+
this.relayClient?.updateSources(sources);
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
console.error('Add source failed:', error);
|
|
407
|
+
this.relayClient?.sendAddSourceError(requestId, String(error));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Execute the source addition command
|
|
412
|
+
*/
|
|
413
|
+
executeAddSource(sourceType, sourceUrl, targetFolder) {
|
|
414
|
+
// For 'prompt' type, use Claude to generate and execute the command
|
|
415
|
+
if (sourceType === 'prompt') {
|
|
416
|
+
return this.executeAddSourceWithAI(sourceUrl, targetFolder);
|
|
417
|
+
}
|
|
418
|
+
return new Promise((resolve, reject) => {
|
|
419
|
+
const workPath = path.join(this.manager.getProjectPath(), 'work');
|
|
420
|
+
// Determine target folder name
|
|
421
|
+
let folder = targetFolder;
|
|
422
|
+
if (!folder) {
|
|
423
|
+
// Auto-generate from URL
|
|
424
|
+
if (sourceType === 'git' || sourceType === 'gh') {
|
|
425
|
+
// Extract repo name from URL
|
|
426
|
+
const match = sourceUrl.match(/\/([^\/]+?)(\.git)?$/);
|
|
427
|
+
folder = match ? match[1] : 'source';
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
// For cp, use the last part of the path
|
|
431
|
+
folder = path.basename(sourceUrl);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const targetPath = path.join(workPath, folder);
|
|
435
|
+
let command;
|
|
436
|
+
switch (sourceType) {
|
|
437
|
+
case 'git':
|
|
438
|
+
command = `git clone "${sourceUrl}" "${targetPath}"`;
|
|
439
|
+
break;
|
|
440
|
+
case 'gh':
|
|
441
|
+
command = `gh repo clone "${sourceUrl}" "${targetPath}"`;
|
|
442
|
+
break;
|
|
443
|
+
case 'cp':
|
|
444
|
+
command = `cp -r "${sourceUrl}" "${targetPath}"`;
|
|
445
|
+
break;
|
|
446
|
+
default:
|
|
447
|
+
reject(new Error(`Unknown source type: ${sourceType}`));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
(0, child_process_1.exec)(command, { cwd: this.manager.getProjectPath(), maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
451
|
+
if (error) {
|
|
452
|
+
reject(new Error(`Command failed: ${error.message}\n${stderr}`));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// List files in the added folder
|
|
456
|
+
try {
|
|
457
|
+
const files = (0, child_process_1.execSync)(`ls -la "${targetPath}" | head -20`, { encoding: 'utf-8' }).split('\n').filter(Boolean);
|
|
458
|
+
resolve({ folder, files });
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
resolve({ folder, files: [] });
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Use Claude to interpret a natural language prompt and add source
|
|
468
|
+
*/
|
|
469
|
+
executeAddSourceWithAI(prompt, targetFolder) {
|
|
470
|
+
return new Promise((resolve, reject) => {
|
|
471
|
+
const workPath = path.join(this.manager.getProjectPath(), 'work');
|
|
472
|
+
const projectPath = this.manager.getProjectPath();
|
|
473
|
+
const aiPrompt = `당신은 소스 코드를 work 폴더에 추가하는 작업을 수행합니다.
|
|
474
|
+
|
|
475
|
+
사용자 요청: ${prompt}
|
|
476
|
+
${targetFolder ? `대상 폴더: work/${targetFolder}` : '대상 폴더: 자동 결정'}
|
|
477
|
+
Work 경로: ${workPath}
|
|
478
|
+
|
|
479
|
+
다음 중 적절한 명령어를 생성하고 실행하세요:
|
|
480
|
+
- git clone <url> <target_path>
|
|
481
|
+
- gh repo clone <repo> <target_path>
|
|
482
|
+
- cp -r <source> <target_path>
|
|
483
|
+
|
|
484
|
+
반드시 아래 JSON 형식으로만 응답하세요 (다른 텍스트 없이):
|
|
485
|
+
{
|
|
486
|
+
"command": "실행할 명령어",
|
|
487
|
+
"folder": "생성될 폴더명 (work/ 제외)"
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
예시:
|
|
491
|
+
- "React 보일러플레이트" → {"command": "npx create-react-app ${workPath}/react-app", "folder": "react-app"}
|
|
492
|
+
- "anthropics/claude-code 클론" → {"command": "gh repo clone anthropics/claude-code ${workPath}/claude-code", "folder": "claude-code"}`;
|
|
493
|
+
// Use echo | claude -p pattern to handle long prompts
|
|
494
|
+
const escapedPrompt = aiPrompt.replace(/'/g, "'\\''");
|
|
495
|
+
const claudeCmd = `echo '${escapedPrompt}' | env -u ANTHROPIC_API_KEY ${this.options.claudeCommand || 'claude'} -p`;
|
|
496
|
+
(0, child_process_1.exec)(claudeCmd, { cwd: projectPath, maxBuffer: 1024 * 1024, timeout: 60000, shell: '/bin/bash' }, (error, stdout, stderr) => {
|
|
497
|
+
if (error) {
|
|
498
|
+
console.error('Claude stderr:', stderr);
|
|
499
|
+
reject(new Error(`Claude analysis failed: ${error.message}\nstderr: ${stderr}`));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
// Parse Claude's response
|
|
504
|
+
const jsonMatch = stdout.match(/\{[\s\S]*\}/);
|
|
505
|
+
if (!jsonMatch) {
|
|
506
|
+
reject(new Error('Claude did not return valid JSON'));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
510
|
+
const command = result.command;
|
|
511
|
+
const folder = result.folder || 'source';
|
|
512
|
+
console.log(`AI generated command: ${command}`);
|
|
513
|
+
// Execute the generated command
|
|
514
|
+
(0, child_process_1.exec)(command, { cwd: projectPath, maxBuffer: 10 * 1024 * 1024 }, (execError, execStdout, execStderr) => {
|
|
515
|
+
if (execError) {
|
|
516
|
+
reject(new Error(`Command failed: ${execError.message}\n${execStderr}`));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
// List files in the added folder
|
|
520
|
+
const targetPath = path.join(workPath, folder);
|
|
521
|
+
try {
|
|
522
|
+
const files = (0, child_process_1.execSync)(`ls -la "${targetPath}" | head -20`, { encoding: 'utf-8' }).split('\n').filter(Boolean);
|
|
523
|
+
resolve({ folder, files });
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
resolve({ folder, files: [] });
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
catch (parseError) {
|
|
531
|
+
reject(new Error(`Failed to parse Claude response: ${parseError}`));
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Call Claude Code CLI to analyze mission and return steps and decisions
|
|
538
|
+
*/
|
|
539
|
+
callClaudeForAnalysis(mission, contextPaths = []) {
|
|
540
|
+
return new Promise((resolve, reject) => {
|
|
541
|
+
// Get project structure automatically from work folder
|
|
542
|
+
const projectStructure = this.manager.getProjectStructure();
|
|
543
|
+
// Read source file contents if contextPaths provided
|
|
544
|
+
const sourceContext = this.readSourceContext(contextPaths);
|
|
545
|
+
const prompt = this.buildAnalysisPrompt(mission, projectStructure, sourceContext);
|
|
546
|
+
const projectPath = this.manager.getProjectPath();
|
|
547
|
+
// Use echo | claude -p pattern to handle long prompts with source context
|
|
548
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
549
|
+
const claudeCmd = `echo '${escapedPrompt}' | env -u ANTHROPIC_API_KEY ${this.options.claudeCommand || 'claude'} -p`;
|
|
550
|
+
const timeout = setTimeout(() => {
|
|
551
|
+
reject(new Error('Analysis timeout'));
|
|
552
|
+
}, CLAUDE_ANALYSIS_TIMEOUT_MS);
|
|
553
|
+
(0, child_process_1.exec)(claudeCmd, { cwd: projectPath, maxBuffer: 1024 * 1024, shell: '/bin/bash' }, (error, stdout, stderr) => {
|
|
554
|
+
clearTimeout(timeout);
|
|
555
|
+
if (error) {
|
|
556
|
+
reject(new Error(`Claude CLI error: ${error.message}`));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
// Parse Claude's response to extract steps and decisions
|
|
561
|
+
const result = this.parseAnalysisResponse(stdout);
|
|
562
|
+
resolve(result);
|
|
563
|
+
}
|
|
564
|
+
catch (parseError) {
|
|
565
|
+
reject(new Error(`Failed to parse analysis: ${parseError}`));
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Read source file contents from context paths for Claude analysis
|
|
572
|
+
*/
|
|
573
|
+
readSourceContext(contextPaths) {
|
|
574
|
+
if (contextPaths.length === 0)
|
|
575
|
+
return '';
|
|
576
|
+
const projectPath = this.manager.getProjectPath();
|
|
577
|
+
const contents = [];
|
|
578
|
+
for (const ctxPath of contextPaths) {
|
|
579
|
+
const fullPath = path.join(projectPath, ctxPath);
|
|
580
|
+
try {
|
|
581
|
+
// Check if path exists
|
|
582
|
+
const stats = require('fs').statSync(fullPath);
|
|
583
|
+
if (stats.isDirectory()) {
|
|
584
|
+
// Get key files from directory (README, package.json, main source files)
|
|
585
|
+
const keyFiles = this.getKeyFilesFromDirectory(fullPath);
|
|
586
|
+
if (keyFiles.length > 0) {
|
|
587
|
+
contents.push(`\n## 소스: ${ctxPath}\n`);
|
|
588
|
+
for (const file of keyFiles) {
|
|
589
|
+
contents.push(`### ${file.name}\n\`\`\`\n${file.content}\n\`\`\`\n`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
catch (e) {
|
|
595
|
+
console.warn(`Failed to read context path: ${ctxPath}`, e);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return contents.join('\n');
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Get key files from a source directory for context
|
|
602
|
+
*/
|
|
603
|
+
getKeyFilesFromDirectory(dirPath) {
|
|
604
|
+
const fs = require('fs');
|
|
605
|
+
const keyFiles = [];
|
|
606
|
+
const keyFileNames = ['README.md', 'package.json', 'setup.py', 'Cargo.toml', 'go.mod', 'pom.xml', 'build.gradle'];
|
|
607
|
+
const maxFileSize = 5000; // Limit file content to prevent huge prompts
|
|
608
|
+
try {
|
|
609
|
+
const files = fs.readdirSync(dirPath);
|
|
610
|
+
// First, add key config/readme files
|
|
611
|
+
for (const fileName of keyFileNames) {
|
|
612
|
+
if (files.includes(fileName)) {
|
|
613
|
+
const filePath = path.join(dirPath, fileName);
|
|
614
|
+
try {
|
|
615
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
616
|
+
if (content.length > maxFileSize) {
|
|
617
|
+
content = content.substring(0, maxFileSize) + '\n... (truncated)';
|
|
618
|
+
}
|
|
619
|
+
keyFiles.push({ name: fileName, content });
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
// Skip unreadable files
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// Then, add a few source files (limited)
|
|
627
|
+
const sourceExtensions = ['.ts', '.js', '.py', '.java', '.go', '.rs'];
|
|
628
|
+
let sourceFileCount = 0;
|
|
629
|
+
const maxSourceFiles = 3;
|
|
630
|
+
for (const fileName of files) {
|
|
631
|
+
if (sourceFileCount >= maxSourceFiles)
|
|
632
|
+
break;
|
|
633
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
634
|
+
if (sourceExtensions.includes(ext)) {
|
|
635
|
+
const filePath = path.join(dirPath, fileName);
|
|
636
|
+
try {
|
|
637
|
+
const stats = fs.statSync(filePath);
|
|
638
|
+
if (stats.isFile() && stats.size < maxFileSize) {
|
|
639
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
640
|
+
if (content.length > maxFileSize) {
|
|
641
|
+
content = content.substring(0, maxFileSize) + '\n... (truncated)';
|
|
642
|
+
}
|
|
643
|
+
keyFiles.push({ name: fileName, content });
|
|
644
|
+
sourceFileCount++;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
// Skip unreadable files
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch (e) {
|
|
654
|
+
console.warn(`Failed to read directory: ${dirPath}`, e);
|
|
655
|
+
}
|
|
656
|
+
return keyFiles;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Build the prompt for Claude Code to analyze the mission
|
|
660
|
+
*/
|
|
661
|
+
buildAnalysisPrompt(mission, projectStructure, sourceContext = '') {
|
|
662
|
+
let prompt = `당신은 프로젝트 관리자입니다. 다음 미션을 분석하고 작업 스텝으로 분해해주세요.
|
|
663
|
+
|
|
664
|
+
미션: ${mission}
|
|
665
|
+
|
|
666
|
+
프로젝트 구조:
|
|
667
|
+
${projectStructure}`;
|
|
668
|
+
if (sourceContext) {
|
|
669
|
+
prompt += `
|
|
670
|
+
|
|
671
|
+
=== 소스 컨텍스트 (참고용) ===
|
|
672
|
+
${sourceContext}
|
|
673
|
+
=== 소스 컨텍스트 끝 ===
|
|
674
|
+
|
|
675
|
+
위 소스 코드를 분석하여 미션에 맞는 구체적인 작업 스텝을 생성하세요.
|
|
676
|
+
소스 코드의 구조와 내용을 고려하여 실제로 수행해야 할 작업을 상세하게 설명하세요.`;
|
|
677
|
+
}
|
|
678
|
+
prompt += `
|
|
679
|
+
|
|
680
|
+
반드시 아래 JSON 형식으로만 응답하세요 (다른 텍스트 없이):
|
|
681
|
+
{
|
|
682
|
+
"steps": [
|
|
683
|
+
{
|
|
684
|
+
"id": "step-1",
|
|
685
|
+
"title": "스텝 제목",
|
|
686
|
+
"description": "상세 설명",
|
|
687
|
+
"agent": "pm 또는 dev",
|
|
688
|
+
"dependsOn": []
|
|
689
|
+
}
|
|
690
|
+
],
|
|
691
|
+
"decisions": [
|
|
692
|
+
{
|
|
693
|
+
"id": "decision-1",
|
|
694
|
+
"question": "사용자에게 물어볼 질문",
|
|
695
|
+
"options": ["선택지1", "선택지2", "선택지3"],
|
|
696
|
+
"required": true
|
|
697
|
+
}
|
|
698
|
+
]
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
규칙:
|
|
702
|
+
- PM agent: 분석, 설계, 계획, 검토 작업 (tools/pm-agent에서 실행)
|
|
703
|
+
- DEV agent: 실제 코드 구현 작업 (work/dev에서 실행)
|
|
704
|
+
- dependsOn: 이 스텝이 의존하는 이전 스텝 ID 목록
|
|
705
|
+
- 첫 번째 스텝은 항상 PM의 요구사항 분석
|
|
706
|
+
- 마지막 스텝은 PM의 검토 및 완료
|
|
707
|
+
- 각 에이전트는 work/ 폴더 하위에 자신의 폴더에서 작업
|
|
708
|
+
- decisions: 작업 진행 전 사용자의 결정이 필요한 항목들
|
|
709
|
+
- 기술 선택, 구현 방식 등 여러 가지 선택지가 있을 때 decisions에 추가
|
|
710
|
+
- 명확한 작업만 있고 결정이 필요없으면 decisions는 빈 배열 []`;
|
|
711
|
+
return prompt;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Parse Claude's response to extract steps and decisions
|
|
715
|
+
*/
|
|
716
|
+
parseAnalysisResponse(response) {
|
|
717
|
+
// Try to find JSON object in the response (new format with steps and decisions)
|
|
718
|
+
const jsonObjectMatch = response.match(/\{[\s\S]*\}/);
|
|
719
|
+
if (jsonObjectMatch) {
|
|
720
|
+
try {
|
|
721
|
+
const parsed = JSON.parse(jsonObjectMatch[0]);
|
|
722
|
+
if (parsed.steps && Array.isArray(parsed.steps)) {
|
|
723
|
+
const steps = parsed.steps.map((step, index) => ({
|
|
724
|
+
id: step.id || `step-${index + 1}`,
|
|
725
|
+
title: step.title || `Step ${index + 1}`,
|
|
726
|
+
description: step.description || '',
|
|
727
|
+
agent: step.agent === 'dev' ? 'dev' : 'pm',
|
|
728
|
+
estimatedTime: step.estimatedTime,
|
|
729
|
+
dependsOn: Array.isArray(step.dependsOn) ? step.dependsOn : []
|
|
730
|
+
}));
|
|
731
|
+
const decisions = (parsed.decisions || []).map((decision, index) => ({
|
|
732
|
+
id: decision.id || `decision-${index + 1}`,
|
|
733
|
+
question: decision.question || '',
|
|
734
|
+
options: Array.isArray(decision.options) ? decision.options : [],
|
|
735
|
+
required: decision.required !== false
|
|
736
|
+
}));
|
|
737
|
+
return { steps, decisions };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
catch (e) {
|
|
741
|
+
// Fall through to try array format
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// Try to find JSON array in the response (legacy format)
|
|
745
|
+
const jsonArrayMatch = response.match(/\[[\s\S]*\]/);
|
|
746
|
+
if (jsonArrayMatch) {
|
|
747
|
+
try {
|
|
748
|
+
const steps = JSON.parse(jsonArrayMatch[0]);
|
|
749
|
+
// Validate and fix steps
|
|
750
|
+
return {
|
|
751
|
+
steps: steps.map((step, index) => ({
|
|
752
|
+
id: step.id || `step-${index + 1}`,
|
|
753
|
+
title: step.title || `Step ${index + 1}`,
|
|
754
|
+
description: step.description || '',
|
|
755
|
+
agent: step.agent === 'dev' ? 'dev' : 'pm',
|
|
756
|
+
estimatedTime: step.estimatedTime,
|
|
757
|
+
dependsOn: Array.isArray(step.dependsOn) ? step.dependsOn : []
|
|
758
|
+
})),
|
|
759
|
+
decisions: []
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
catch (e) {
|
|
763
|
+
console.warn('Failed to parse JSON, using default steps:', e);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// If no JSON found, create default steps
|
|
767
|
+
console.warn('No JSON found in Claude response, using default steps');
|
|
768
|
+
return { steps: this.getDefaultSteps(), decisions: [] };
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Get default steps when analysis fails
|
|
772
|
+
*/
|
|
773
|
+
getDefaultSteps() {
|
|
774
|
+
return [
|
|
775
|
+
{ id: 'step-1', title: '요구사항 분석', description: '미션을 분석하고 구체적인 요구사항 도출', agent: 'pm', dependsOn: [] },
|
|
776
|
+
{ id: 'step-2', title: '구현 계획', description: '구체적인 구현 방안 수립', agent: 'pm', dependsOn: ['step-1'] },
|
|
777
|
+
{ id: 'step-3', title: '개발', description: '기능 구현', agent: 'dev', dependsOn: ['step-2'] },
|
|
778
|
+
{ id: 'step-4', title: '검토 및 완료', description: '결과물 검토 및 마무리', agent: 'pm', dependsOn: ['step-3'] }
|
|
779
|
+
];
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Update relay with current status
|
|
783
|
+
*/
|
|
784
|
+
updateRelayStatus() {
|
|
785
|
+
if (!this.relayClient || !this.workflow)
|
|
786
|
+
return;
|
|
787
|
+
const status = this.manager.loadStatus();
|
|
788
|
+
if (!status)
|
|
789
|
+
return;
|
|
790
|
+
const agents = {};
|
|
791
|
+
for (const agent of this.workflow.agents) {
|
|
792
|
+
const agentStatus = status.agents[agent.id];
|
|
793
|
+
if (agentStatus) {
|
|
794
|
+
agents[agent.id] = {
|
|
795
|
+
status: agentStatus.status,
|
|
796
|
+
currentTask: agentStatus.currentTask
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
this.relayClient.updateStatus(status.status, agents);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Stop workflow execution
|
|
804
|
+
*/
|
|
805
|
+
stop() {
|
|
806
|
+
this.running = false;
|
|
807
|
+
if (this.pollTimer) {
|
|
808
|
+
clearTimeout(this.pollTimer);
|
|
809
|
+
this.pollTimer = null;
|
|
810
|
+
}
|
|
811
|
+
// Disconnect from relay
|
|
812
|
+
if (this.relayClient) {
|
|
813
|
+
this.relayClient.destroy();
|
|
814
|
+
this.relayClient = null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Kill all project tmux sessions
|
|
819
|
+
*/
|
|
820
|
+
killAllSessions() {
|
|
821
|
+
const projectId = this.manager.getProjectId().replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
822
|
+
const prefix = `proj_${projectId}_`;
|
|
823
|
+
try {
|
|
824
|
+
const sessions = (0, child_process_1.execSync)(`tmux ls -F "#{session_name}" 2>/dev/null || true`, {
|
|
825
|
+
encoding: 'utf-8'
|
|
826
|
+
});
|
|
827
|
+
for (const session of sessions.split('\n')) {
|
|
828
|
+
if (session.startsWith(prefix)) {
|
|
829
|
+
try {
|
|
830
|
+
(0, child_process_1.execSync)(`tmux kill-session -t "${session}"`, { stdio: 'pipe' });
|
|
831
|
+
console.log(`Killed session: ${session}`);
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
// Ignore errors
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
// Ignore errors
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Send keys to an agent session
|
|
845
|
+
*/
|
|
846
|
+
sendToAgent(agentId, keys) {
|
|
847
|
+
const sessionName = this.manager.getTmuxSessionName(agentId);
|
|
848
|
+
try {
|
|
849
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${sessionName}" "${keys.replace(/"/g, '\\"')}" Enter`, {
|
|
850
|
+
stdio: 'pipe'
|
|
851
|
+
});
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Capture agent terminal output
|
|
860
|
+
*/
|
|
861
|
+
captureAgent(agentId) {
|
|
862
|
+
const sessionName = this.manager.getTmuxSessionName(agentId);
|
|
863
|
+
try {
|
|
864
|
+
return (0, child_process_1.execSync)(`tmux capture-pane -t "${sessionName}" -p -e`, {
|
|
865
|
+
encoding: 'utf-8',
|
|
866
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* List all agent sessions for this project
|
|
875
|
+
*/
|
|
876
|
+
listSessions() {
|
|
877
|
+
const projectId = this.manager.getProjectId().replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
878
|
+
const prefix = `proj_${projectId}_`;
|
|
879
|
+
try {
|
|
880
|
+
const sessions = (0, child_process_1.execSync)(`tmux ls -F "#{session_name}" 2>/dev/null || true`, {
|
|
881
|
+
encoding: 'utf-8'
|
|
882
|
+
});
|
|
883
|
+
return sessions
|
|
884
|
+
.split('\n')
|
|
885
|
+
.filter(s => s.startsWith(prefix))
|
|
886
|
+
.map(s => s.substring(prefix.length));
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
return [];
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
exports.WorkflowExecutor = WorkflowExecutor;
|