sessioncast-cli 1.0.0 → 1.1.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/README.md +65 -40
- package/dist/agent/session-handler.d.ts +1 -0
- package/dist/agent/session-handler.js +42 -0
- package/dist/agent/tmux-executor.d.ts +66 -0
- package/dist/agent/tmux-executor.js +368 -0
- package/dist/agent/tmux.d.ts +9 -1
- package/dist/agent/tmux.js +52 -76
- package/dist/agent/websocket.d.ts +2 -8
- package/dist/agent/websocket.js +78 -14
- package/dist/autopilot/index.d.ts +94 -0
- package/dist/autopilot/index.js +322 -0
- package/dist/autopilot/mission-analyzer.d.ts +27 -0
- package/dist/autopilot/mission-analyzer.js +232 -0
- package/dist/autopilot/project-detector.d.ts +12 -0
- package/dist/autopilot/project-detector.js +326 -0
- package/dist/autopilot/source-scanner.d.ts +26 -0
- package/dist/autopilot/source-scanner.js +285 -0
- package/dist/autopilot/speckit-generator.d.ts +60 -0
- package/dist/autopilot/speckit-generator.js +511 -0
- package/dist/autopilot/types.d.ts +110 -0
- package/dist/autopilot/types.js +6 -0
- package/dist/autopilot/workflow-generator.d.ts +33 -0
- package/dist/autopilot/workflow-generator.js +278 -0
- package/dist/commands/autopilot.d.ts +30 -0
- package/dist/commands/autopilot.js +262 -0
- package/dist/commands/login.d.ts +2 -1
- package/dist/commands/login.js +199 -8
- package/dist/commands/project.d.ts +1 -1
- package/dist/commands/project.js +4 -13
- package/dist/config.d.ts +20 -0
- package/dist/config.js +69 -2
- package/dist/index.js +7 -47
- package/dist/project/executor.d.ts +8 -53
- package/dist/project/executor.js +64 -520
- package/dist/project/manager.d.ts +0 -13
- package/dist/project/manager.js +0 -107
- package/dist/project/relay-client.d.ts +18 -68
- package/dist/project/relay-client.js +134 -130
- package/dist/project/types.d.ts +5 -0
- package/dist/utils/fileUtils.d.ts +28 -0
- package/dist/utils/fileUtils.js +159 -0
- package/dist/utils/oauthServer.d.ts +18 -0
- package/dist/utils/oauthServer.js +244 -0
- package/dist/utils/pkce.d.ts +16 -0
- package/dist/utils/pkce.js +73 -0
- package/package.json +5 -14
- package/LICENSE +0 -21
package/dist/project/executor.js
CHANGED
|
@@ -36,13 +36,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.WorkflowExecutor = void 0;
|
|
37
37
|
const child_process_1 = require("child_process");
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
-
const os = __importStar(require("os"));
|
|
40
39
|
const events_1 = require("events");
|
|
41
40
|
const relay_client_1 = require("./relay-client");
|
|
42
41
|
const POLL_INTERVAL_MS = 2000;
|
|
43
42
|
const MAX_RETRIES = 3;
|
|
44
43
|
const CLAUDE_START_DELAY_MS = 1000;
|
|
45
|
-
const CLAUDE_ANALYSIS_TIMEOUT_MS = 55000; // 55 seconds (less than server's 60s timeout)
|
|
46
44
|
class WorkflowExecutor extends events_1.EventEmitter {
|
|
47
45
|
constructor(manager, options = {}) {
|
|
48
46
|
super();
|
|
@@ -55,8 +53,7 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
55
53
|
autoLaunchClaude: options.autoLaunchClaude ?? true,
|
|
56
54
|
claudeCommand: options.claudeCommand ?? 'claude',
|
|
57
55
|
relayUrl: options.relayUrl,
|
|
58
|
-
relayToken: options.relayToken
|
|
59
|
-
machineId: options.machineId ?? os.hostname()
|
|
56
|
+
relayToken: options.relayToken
|
|
60
57
|
};
|
|
61
58
|
}
|
|
62
59
|
/**
|
|
@@ -69,7 +66,7 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
69
66
|
}
|
|
70
67
|
this.running = true;
|
|
71
68
|
console.log(`Starting workflow: ${this.workflow.name}`);
|
|
72
|
-
// Connect to relay
|
|
69
|
+
// Connect to relay if configured
|
|
73
70
|
if (this.options.relayUrl && this.options.relayToken) {
|
|
74
71
|
await this.connectToRelay();
|
|
75
72
|
}
|
|
@@ -84,11 +81,72 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
84
81
|
status.agents[agent.id] = { status: 'pending' };
|
|
85
82
|
}
|
|
86
83
|
this.manager.saveStatus(status);
|
|
84
|
+
// Update relay with initial agents list
|
|
85
|
+
this.updateRelayAgents();
|
|
87
86
|
// Start PM Agent first
|
|
88
87
|
await this.startPMAgent();
|
|
89
88
|
// Then start the workflow execution loop
|
|
90
89
|
this.runExecutionLoop();
|
|
91
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Connect to relay server and register project
|
|
93
|
+
*/
|
|
94
|
+
async connectToRelay() {
|
|
95
|
+
if (!this.options.relayUrl || !this.options.relayToken) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const project = this.manager.load();
|
|
99
|
+
this.relayClient = new relay_client_1.ProjectRelayClient({
|
|
100
|
+
relayUrl: this.options.relayUrl,
|
|
101
|
+
token: this.options.relayToken,
|
|
102
|
+
projectId: this.manager.getProjectId(),
|
|
103
|
+
projectName: project?.name || this.manager.getProjectId(),
|
|
104
|
+
projectPath: this.manager.getProjectPath()
|
|
105
|
+
});
|
|
106
|
+
// Setup event handlers
|
|
107
|
+
this.relayClient.on('connected', () => {
|
|
108
|
+
console.log('[Relay] Connected and project registered');
|
|
109
|
+
this.relayClient?.updateStatus('running');
|
|
110
|
+
});
|
|
111
|
+
this.relayClient.on('addSource', (meta) => {
|
|
112
|
+
console.log('[Relay] Add source request:', meta);
|
|
113
|
+
this.emit('addSourceRequest', meta);
|
|
114
|
+
});
|
|
115
|
+
this.relayClient.on('analyzeMission', (meta) => {
|
|
116
|
+
console.log('[Relay] Analyze mission request:', meta);
|
|
117
|
+
this.emit('analyzeMissionRequest', meta);
|
|
118
|
+
});
|
|
119
|
+
this.relayClient.on('startWorkflow', (meta) => {
|
|
120
|
+
console.log('[Relay] Start workflow request:', meta);
|
|
121
|
+
this.emit('startWorkflowRequest', meta);
|
|
122
|
+
});
|
|
123
|
+
this.relayClient.on('error', (error) => {
|
|
124
|
+
console.error('[Relay] Error:', error.message);
|
|
125
|
+
});
|
|
126
|
+
this.relayClient.connect();
|
|
127
|
+
// Wait a bit for connection
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Update relay with current agents list
|
|
132
|
+
*/
|
|
133
|
+
updateRelayAgents() {
|
|
134
|
+
if (!this.relayClient || !this.workflow)
|
|
135
|
+
return;
|
|
136
|
+
const status = this.manager.loadStatus();
|
|
137
|
+
const agents = this.workflow.agents.map(agent => ({
|
|
138
|
+
id: agent.id,
|
|
139
|
+
name: agent.name,
|
|
140
|
+
status: status?.agents[agent.id]?.status || 'pending'
|
|
141
|
+
}));
|
|
142
|
+
// Add PM agent
|
|
143
|
+
agents.unshift({
|
|
144
|
+
id: 'pm',
|
|
145
|
+
name: 'PM',
|
|
146
|
+
status: status?.agents['pm']?.status || 'pending'
|
|
147
|
+
});
|
|
148
|
+
this.relayClient.updateAgents(agents);
|
|
149
|
+
}
|
|
92
150
|
/**
|
|
93
151
|
* Start PM Agent tmux session
|
|
94
152
|
*/
|
|
@@ -101,12 +159,6 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
101
159
|
try {
|
|
102
160
|
(0, child_process_1.execSync)(`tmux has-session -t "${sessionName}" 2>/dev/null`);
|
|
103
161
|
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
162
|
return;
|
|
111
163
|
}
|
|
112
164
|
catch {
|
|
@@ -124,7 +176,6 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
124
176
|
status: 'running',
|
|
125
177
|
startedAt: new Date().toISOString()
|
|
126
178
|
});
|
|
127
|
-
this.updateRelayStatus();
|
|
128
179
|
this.emit('agent-started', { agentId: 'pm', sessionName });
|
|
129
180
|
}
|
|
130
181
|
catch (error) {
|
|
@@ -144,13 +195,6 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
144
195
|
try {
|
|
145
196
|
(0, child_process_1.execSync)(`tmux has-session -t "${sessionName}" 2>/dev/null`);
|
|
146
197
|
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
198
|
return;
|
|
155
199
|
}
|
|
156
200
|
catch {
|
|
@@ -172,7 +216,6 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
172
216
|
startedAt: new Date().toISOString(),
|
|
173
217
|
currentTask: agent.tasks[0] || 'Starting...'
|
|
174
218
|
});
|
|
175
|
-
this.updateRelayStatus();
|
|
176
219
|
this.emit('agent-started', { agentId: agent.id, sessionName });
|
|
177
220
|
}
|
|
178
221
|
catch (error) {
|
|
@@ -181,7 +224,6 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
181
224
|
status: 'failed',
|
|
182
225
|
error: String(error)
|
|
183
226
|
});
|
|
184
|
-
this.updateRelayStatus();
|
|
185
227
|
}
|
|
186
228
|
}
|
|
187
229
|
/**
|
|
@@ -269,7 +311,6 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
269
311
|
this.manager.appendToContext(`\n## ${agent.name} Output\n${output}\n`);
|
|
270
312
|
}
|
|
271
313
|
console.log(`Agent completed: ${agent.id}`);
|
|
272
|
-
this.updateRelayStatus();
|
|
273
314
|
this.emit('agent-completed', { agentId: agent.id, output });
|
|
274
315
|
}
|
|
275
316
|
}
|
|
@@ -282,7 +323,6 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
282
323
|
console.log('Workflow completed!');
|
|
283
324
|
updatedStatus.status = 'completed';
|
|
284
325
|
this.manager.saveStatus(updatedStatus);
|
|
285
|
-
this.updateRelayStatus();
|
|
286
326
|
this.emit('workflow-completed', updatedStatus);
|
|
287
327
|
this.stop();
|
|
288
328
|
return;
|
|
@@ -292,7 +332,6 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
292
332
|
console.log('Workflow failed!');
|
|
293
333
|
updatedStatus.status = 'failed';
|
|
294
334
|
this.manager.saveStatus(updatedStatus);
|
|
295
|
-
this.updateRelayStatus();
|
|
296
335
|
this.emit('workflow-failed', updatedStatus);
|
|
297
336
|
this.stop();
|
|
298
337
|
return;
|
|
@@ -303,502 +342,6 @@ class WorkflowExecutor extends events_1.EventEmitter {
|
|
|
303
342
|
};
|
|
304
343
|
check();
|
|
305
344
|
}
|
|
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
345
|
/**
|
|
803
346
|
* Stop workflow execution
|
|
804
347
|
*/
|
|
@@ -810,6 +353,7 @@ ${sourceContext}
|
|
|
810
353
|
}
|
|
811
354
|
// Disconnect from relay
|
|
812
355
|
if (this.relayClient) {
|
|
356
|
+
this.relayClient.updateStatus('completed');
|
|
813
357
|
this.relayClient.destroy();
|
|
814
358
|
this.relayClient = null;
|
|
815
359
|
}
|