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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/dist/agent/api-client.d.ts +27 -0
  4. package/dist/agent/api-client.js +295 -0
  5. package/dist/agent/exec-service.d.ts +6 -0
  6. package/dist/agent/exec-service.js +126 -0
  7. package/dist/agent/index.d.ts +8 -0
  8. package/dist/agent/index.js +24 -0
  9. package/dist/agent/llm-service.d.ts +9 -0
  10. package/dist/agent/llm-service.js +156 -0
  11. package/dist/agent/runner.d.ts +16 -0
  12. package/dist/agent/runner.js +187 -0
  13. package/dist/agent/session-handler.d.ts +28 -0
  14. package/dist/agent/session-handler.js +184 -0
  15. package/dist/agent/tmux.d.ts +29 -0
  16. package/dist/agent/tmux.js +157 -0
  17. package/dist/agent/types.d.ts +72 -0
  18. package/dist/agent/types.js +2 -0
  19. package/dist/agent/websocket.d.ts +45 -0
  20. package/dist/agent/websocket.js +288 -0
  21. package/dist/api.d.ts +31 -0
  22. package/dist/api.js +78 -0
  23. package/dist/commands/agent.d.ts +5 -0
  24. package/dist/commands/agent.js +19 -0
  25. package/dist/commands/agents.d.ts +1 -0
  26. package/dist/commands/agents.js +77 -0
  27. package/dist/commands/login.d.ts +5 -0
  28. package/dist/commands/login.js +41 -0
  29. package/dist/commands/project.d.ts +33 -0
  30. package/dist/commands/project.js +359 -0
  31. package/dist/commands/sendkeys.d.ts +3 -0
  32. package/dist/commands/sendkeys.js +66 -0
  33. package/dist/commands/sessions.d.ts +1 -0
  34. package/dist/commands/sessions.js +89 -0
  35. package/dist/config.d.ts +13 -0
  36. package/dist/config.js +37 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +125 -0
  39. package/dist/project/executor.d.ts +118 -0
  40. package/dist/project/executor.js +893 -0
  41. package/dist/project/index.d.ts +4 -0
  42. package/dist/project/index.js +20 -0
  43. package/dist/project/manager.d.ts +79 -0
  44. package/dist/project/manager.js +397 -0
  45. package/dist/project/relay-client.d.ts +87 -0
  46. package/dist/project/relay-client.js +200 -0
  47. package/dist/project/types.d.ts +43 -0
  48. package/dist/project/types.js +3 -0
  49. 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;