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.
Files changed (47) hide show
  1. package/README.md +65 -40
  2. package/dist/agent/session-handler.d.ts +1 -0
  3. package/dist/agent/session-handler.js +42 -0
  4. package/dist/agent/tmux-executor.d.ts +66 -0
  5. package/dist/agent/tmux-executor.js +368 -0
  6. package/dist/agent/tmux.d.ts +9 -1
  7. package/dist/agent/tmux.js +52 -76
  8. package/dist/agent/websocket.d.ts +2 -8
  9. package/dist/agent/websocket.js +78 -14
  10. package/dist/autopilot/index.d.ts +94 -0
  11. package/dist/autopilot/index.js +322 -0
  12. package/dist/autopilot/mission-analyzer.d.ts +27 -0
  13. package/dist/autopilot/mission-analyzer.js +232 -0
  14. package/dist/autopilot/project-detector.d.ts +12 -0
  15. package/dist/autopilot/project-detector.js +326 -0
  16. package/dist/autopilot/source-scanner.d.ts +26 -0
  17. package/dist/autopilot/source-scanner.js +285 -0
  18. package/dist/autopilot/speckit-generator.d.ts +60 -0
  19. package/dist/autopilot/speckit-generator.js +511 -0
  20. package/dist/autopilot/types.d.ts +110 -0
  21. package/dist/autopilot/types.js +6 -0
  22. package/dist/autopilot/workflow-generator.d.ts +33 -0
  23. package/dist/autopilot/workflow-generator.js +278 -0
  24. package/dist/commands/autopilot.d.ts +30 -0
  25. package/dist/commands/autopilot.js +262 -0
  26. package/dist/commands/login.d.ts +2 -1
  27. package/dist/commands/login.js +199 -8
  28. package/dist/commands/project.d.ts +1 -1
  29. package/dist/commands/project.js +4 -13
  30. package/dist/config.d.ts +20 -0
  31. package/dist/config.js +69 -2
  32. package/dist/index.js +7 -47
  33. package/dist/project/executor.d.ts +8 -53
  34. package/dist/project/executor.js +64 -520
  35. package/dist/project/manager.d.ts +0 -13
  36. package/dist/project/manager.js +0 -107
  37. package/dist/project/relay-client.d.ts +18 -68
  38. package/dist/project/relay-client.js +134 -130
  39. package/dist/project/types.d.ts +5 -0
  40. package/dist/utils/fileUtils.d.ts +28 -0
  41. package/dist/utils/fileUtils.js +159 -0
  42. package/dist/utils/oauthServer.d.ts +18 -0
  43. package/dist/utils/oauthServer.js +244 -0
  44. package/dist/utils/pkce.d.ts +16 -0
  45. package/dist/utils/pkce.js +73 -0
  46. package/package.json +5 -14
  47. package/LICENSE +0 -21
@@ -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 server if configured
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
  }