@litmers/cursorflow-orchestrator 0.1.5 → 0.1.8

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 (45) hide show
  1. package/CHANGELOG.md +15 -6
  2. package/README.md +33 -2
  3. package/commands/cursorflow-doctor.md +24 -0
  4. package/commands/cursorflow-signal.md +19 -0
  5. package/dist/cli/doctor.d.ts +15 -0
  6. package/dist/cli/doctor.js +139 -0
  7. package/dist/cli/doctor.js.map +1 -0
  8. package/dist/cli/index.js +5 -0
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/monitor.d.ts +1 -1
  11. package/dist/cli/monitor.js +640 -145
  12. package/dist/cli/monitor.js.map +1 -1
  13. package/dist/cli/resume.d.ts +1 -1
  14. package/dist/cli/resume.js +80 -10
  15. package/dist/cli/resume.js.map +1 -1
  16. package/dist/cli/run.js +60 -5
  17. package/dist/cli/run.js.map +1 -1
  18. package/dist/cli/setup-commands.d.ts +4 -0
  19. package/dist/cli/setup-commands.js +16 -0
  20. package/dist/cli/setup-commands.js.map +1 -1
  21. package/dist/cli/signal.d.ts +7 -0
  22. package/dist/cli/signal.js +99 -0
  23. package/dist/cli/signal.js.map +1 -0
  24. package/dist/core/orchestrator.d.ts +4 -2
  25. package/dist/core/orchestrator.js +92 -23
  26. package/dist/core/orchestrator.js.map +1 -1
  27. package/dist/core/runner.d.ts +9 -3
  28. package/dist/core/runner.js +182 -88
  29. package/dist/core/runner.js.map +1 -1
  30. package/dist/utils/doctor.d.ts +63 -0
  31. package/dist/utils/doctor.js +280 -0
  32. package/dist/utils/doctor.js.map +1 -0
  33. package/dist/utils/types.d.ts +3 -0
  34. package/package.json +1 -1
  35. package/src/cli/doctor.ts +127 -0
  36. package/src/cli/index.ts +5 -0
  37. package/src/cli/monitor.ts +693 -185
  38. package/src/cli/resume.ts +94 -12
  39. package/src/cli/run.ts +63 -7
  40. package/src/cli/setup-commands.ts +19 -0
  41. package/src/cli/signal.ts +89 -0
  42. package/src/core/orchestrator.ts +102 -27
  43. package/src/core/runner.ts +203 -99
  44. package/src/utils/doctor.ts +312 -0
  45. package/src/utils/types.ts +3 -0
@@ -6,7 +6,7 @@
6
6
 
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
- import { execSync, spawnSync } from 'child_process';
9
+ import { execSync, spawn, spawnSync } from 'child_process';
10
10
 
11
11
  import * as git from '../utils/git';
12
12
  import * as logger from '../utils/logger';
@@ -107,12 +107,16 @@ function parseJsonFromStdout(stdout: string): any {
107
107
  return null;
108
108
  }
109
109
 
110
- export function cursorAgentSend({ workspaceDir, chatId, prompt, model }: {
110
+ /**
111
+ * Execute cursor-agent command with streaming and better error handling
112
+ */
113
+ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir }: {
111
114
  workspaceDir: string;
112
115
  chatId: string;
113
116
  prompt: string;
114
117
  model?: string;
115
- }): AgentSendResult {
118
+ signalDir?: string;
119
+ }): Promise<AgentSendResult> {
116
120
  const args = [
117
121
  '--print',
118
122
  '--output-format', 'json',
@@ -124,74 +128,100 @@ export function cursorAgentSend({ workspaceDir, chatId, prompt, model }: {
124
128
 
125
129
  logger.info('Executing cursor-agent...');
126
130
 
127
- const res = spawnSync('cursor-agent', args, {
128
- encoding: 'utf8',
129
- stdio: 'pipe',
130
- timeout: 300000, // 5 minute timeout for LLM response
131
- });
132
-
133
- // Check for timeout
134
- if (res.error) {
135
- if ((res.error as any).code === 'ETIMEDOUT') {
136
- return {
131
+ return new Promise((resolve) => {
132
+ const child = spawn('cursor-agent', args, {
133
+ stdio: ['pipe', 'pipe', 'pipe'], // Enable stdin piping
134
+ env: process.env,
135
+ });
136
+
137
+ let fullStdout = '';
138
+ let fullStderr = '';
139
+
140
+ // Watch for "intervention.txt" signal file if any
141
+ const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
142
+ let interventionWatcher: fs.FSWatcher | null = null;
143
+
144
+ if (interventionPath && fs.existsSync(path.dirname(interventionPath))) {
145
+ interventionWatcher = fs.watch(path.dirname(interventionPath), (event, filename) => {
146
+ if (filename === 'intervention.txt' && fs.existsSync(interventionPath)) {
147
+ try {
148
+ const message = fs.readFileSync(interventionPath, 'utf8').trim();
149
+ if (message) {
150
+ logger.info(`Injecting intervention: ${message}`);
151
+ child.stdin.write(message + '\n');
152
+ fs.unlinkSync(interventionPath); // Clear it
153
+ }
154
+ } catch (e) {
155
+ logger.warn('Failed to read intervention file');
156
+ }
157
+ }
158
+ });
159
+ }
160
+
161
+ child.stdout.on('data', (data) => {
162
+ const str = data.toString();
163
+ fullStdout += str;
164
+ // Also pipe to our own stdout so it goes to terminal.log
165
+ process.stdout.write(data);
166
+ });
167
+
168
+ child.stderr.on('data', (data) => {
169
+ fullStderr += data.toString();
170
+ // Pipe to our own stderr so it goes to terminal.log
171
+ process.stderr.write(data);
172
+ });
173
+
174
+ const timeout = setTimeout(() => {
175
+ child.kill();
176
+ resolve({
137
177
  ok: false,
138
178
  exitCode: -1,
139
179
  error: 'cursor-agent timed out after 5 minutes. The LLM request may be taking too long or there may be network issues.',
140
- };
141
- }
142
-
143
- return {
144
- ok: false,
145
- exitCode: -1,
146
- error: `cursor-agent error: ${res.error.message}`,
147
- };
148
- }
149
-
150
- const json = parseJsonFromStdout(res.stdout);
151
-
152
- if (res.status !== 0 || !json || json.type !== 'result') {
153
- let errorMsg = res.stderr?.trim() || res.stdout?.trim() || `exit=${res.status}`;
154
-
155
- // Check for authentication errors
156
- if (errorMsg.includes('not authenticated') ||
157
- errorMsg.includes('login') ||
158
- errorMsg.includes('auth')) {
159
- errorMsg = 'Authentication error. Please:\n' +
160
- ' 1. Open Cursor IDE\n' +
161
- ' 2. Sign in to your account\n' +
162
- ' 3. Verify AI features are working\n' +
163
- ' 4. Try again\n\n' +
164
- `Details: ${errorMsg}`;
165
- }
166
-
167
- // Check for rate limit errors
168
- if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
169
- errorMsg = 'API rate limit or quota exceeded. Please:\n' +
170
- ' 1. Check your Cursor subscription\n' +
171
- ' 2. Wait a few minutes and try again\n\n' +
172
- `Details: ${errorMsg}`;
173
- }
174
-
175
- // Check for model errors
176
- if (errorMsg.includes('model')) {
177
- errorMsg = `Model error (requested: ${model || 'default'}). ` +
178
- 'Please check if the model is available in your Cursor subscription.\n\n' +
179
- `Details: ${errorMsg}`;
180
- }
181
-
182
- return {
183
- ok: false,
184
- exitCode: res.status ?? -1,
185
- error: errorMsg,
186
- };
187
- }
188
-
189
- return {
190
- ok: !json.is_error,
191
- exitCode: res.status ?? 0,
192
- sessionId: json.session_id || chatId,
193
- resultText: json.result || '',
194
- };
180
+ });
181
+ }, 300000);
182
+
183
+ child.on('close', (code) => {
184
+ clearTimeout(timeout);
185
+ if (interventionWatcher) interventionWatcher.close();
186
+
187
+ const json = parseJsonFromStdout(fullStdout);
188
+
189
+ if (code !== 0 || !json || json.type !== 'result') {
190
+ let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
191
+
192
+ // Check for common errors
193
+ if (errorMsg.includes('not authenticated') || errorMsg.includes('login') || errorMsg.includes('auth')) {
194
+ errorMsg = 'Authentication error. Please sign in to Cursor IDE.';
195
+ } else if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
196
+ errorMsg = 'API rate limit or quota exceeded.';
197
+ } else if (errorMsg.includes('model')) {
198
+ errorMsg = `Model error (requested: ${model || 'default'}). Check your subscription.`;
199
+ }
200
+
201
+ resolve({
202
+ ok: false,
203
+ exitCode: code ?? -1,
204
+ error: errorMsg,
205
+ });
206
+ } else {
207
+ resolve({
208
+ ok: !json.is_error,
209
+ exitCode: code ?? 0,
210
+ sessionId: json.session_id || chatId,
211
+ resultText: json.result || '',
212
+ });
213
+ }
214
+ });
215
+
216
+ child.on('error', (err) => {
217
+ clearTimeout(timeout);
218
+ resolve({
219
+ ok: false,
220
+ exitCode: -1,
221
+ error: `Failed to start cursor-agent: ${err.message}`,
222
+ });
223
+ });
224
+ });
195
225
  }
196
226
 
197
227
  /**
@@ -326,11 +356,12 @@ export async function runTask({
326
356
  }));
327
357
 
328
358
  logger.info('Sending prompt to agent...');
329
- const r1 = cursorAgentSend({
359
+ const r1 = await cursorAgentSend({
330
360
  workspaceDir: worktreeDir,
331
361
  chatId,
332
362
  prompt: prompt1,
333
363
  model,
364
+ signalDir: runDir
334
365
  });
335
366
 
336
367
  appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error || 'No response', {
@@ -371,7 +402,9 @@ export async function runTask({
371
402
  /**
372
403
  * Run all tasks in sequence
373
404
  */
374
- export async function runTasks(config: RunnerConfig, runDir: string): Promise<TaskExecutionResult[]> {
405
+ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number } = {}): Promise<TaskExecutionResult[]> {
406
+ const startIndex = options.startIndex || 0;
407
+
375
408
  // Ensure cursor-agent is installed
376
409
  ensureCursorAgent();
377
410
 
@@ -400,44 +433,113 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
400
433
  logger.success('✓ Cursor authentication OK');
401
434
 
402
435
  const repoRoot = git.getRepoRoot();
403
- const pipelineBranch = config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
404
- const worktreeDir = path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
405
436
 
406
- logger.section('🚀 Starting Pipeline');
437
+ // Load existing state if resuming
438
+ const statePath = path.join(runDir, 'state.json');
439
+ let state: LaneState | null = null;
440
+
441
+ if (startIndex > 0 && fs.existsSync(statePath)) {
442
+ state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
443
+ }
444
+
445
+ const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
446
+ const worktreeDir = state?.worktreeDir || path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
447
+
448
+ if (startIndex === 0) {
449
+ logger.section('🚀 Starting Pipeline');
450
+ } else {
451
+ logger.section(`🔁 Resuming Pipeline from task ${startIndex + 1}`);
452
+ }
453
+
407
454
  logger.info(`Pipeline Branch: ${pipelineBranch}`);
408
455
  logger.info(`Worktree: ${worktreeDir}`);
409
456
  logger.info(`Tasks: ${config.tasks.length}`);
410
457
 
411
- // Create worktree
412
- git.createWorktree(worktreeDir, pipelineBranch, {
413
- baseBranch: config.baseBranch || 'main',
414
- cwd: repoRoot,
415
- });
458
+ // Create worktree only if starting fresh
459
+ if (startIndex === 0 || !fs.existsSync(worktreeDir)) {
460
+ git.createWorktree(worktreeDir, pipelineBranch, {
461
+ baseBranch: config.baseBranch || 'main',
462
+ cwd: repoRoot,
463
+ });
464
+ }
416
465
 
417
466
  // Create chat
418
467
  logger.info('Creating chat session...');
419
468
  const chatId = cursorAgentCreateChat();
420
469
 
421
- // Save initial state
422
- const state: LaneState = {
423
- status: 'running',
424
- pipelineBranch,
425
- worktreeDir,
426
- totalTasks: config.tasks.length,
427
- currentTaskIndex: 0,
428
- label: pipelineBranch,
429
- startTime: Date.now(),
430
- endTime: null,
431
- error: null,
432
- dependencyRequest: null,
433
- };
470
+ // Initialize state if not loaded
471
+ if (!state) {
472
+ state = {
473
+ status: 'running',
474
+ pipelineBranch,
475
+ worktreeDir,
476
+ totalTasks: config.tasks.length,
477
+ currentTaskIndex: 0,
478
+ label: pipelineBranch,
479
+ startTime: Date.now(),
480
+ endTime: null,
481
+ error: null,
482
+ dependencyRequest: null,
483
+ tasksFile, // Store tasks file for resume
484
+ dependsOn: config.dependsOn || [],
485
+ };
486
+ } else {
487
+ state.status = 'running';
488
+ state.error = null;
489
+ state.dependencyRequest = null;
490
+ state.dependsOn = config.dependsOn || [];
491
+ }
492
+
493
+ saveState(statePath, state);
434
494
 
435
- saveState(path.join(runDir, 'state.json'), state);
495
+ // Merge dependencies if any
496
+ if (startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
497
+ logger.section('🔗 Merging Dependencies');
498
+
499
+ // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
500
+ const lanesRoot = path.dirname(runDir);
501
+
502
+ for (const depName of config.dependsOn) {
503
+ const depRunDir = path.join(lanesRoot, depName);
504
+ const depStatePath = path.join(depRunDir, 'state.json');
505
+
506
+ if (!fs.existsSync(depStatePath)) {
507
+ logger.warn(`Dependency state not found for ${depName} at ${depStatePath}`);
508
+ continue;
509
+ }
510
+
511
+ try {
512
+ const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
513
+ if (depState.status !== 'completed') {
514
+ logger.warn(`Dependency ${depName} is in status ${depState.status}, merge might be incomplete`);
515
+ }
516
+
517
+ if (depState.pipelineBranch) {
518
+ logger.info(`Merging dependency branch: ${depState.pipelineBranch} (${depName})`);
519
+
520
+ // Fetch first to ensure we have the branch
521
+ git.runGit(['fetch', 'origin', depState.pipelineBranch], { cwd: worktreeDir, silent: true });
522
+
523
+ // Merge
524
+ git.merge(depState.pipelineBranch, {
525
+ cwd: worktreeDir,
526
+ noFf: true,
527
+ message: `chore: merge dependency ${depName} (${depState.pipelineBranch})`
528
+ });
529
+ }
530
+ } catch (e) {
531
+ logger.error(`Failed to merge dependency ${depName}: ${e}`);
532
+ }
533
+ }
534
+
535
+ // Push the merged state
536
+ git.push(pipelineBranch, { cwd: worktreeDir });
537
+ }
436
538
 
437
539
  // Run tasks
438
540
  const results: TaskExecutionResult[] = [];
439
541
 
440
- for (let i = 0; i < config.tasks.length; i++) {
542
+ for (let i = startIndex; i < config.tasks.length; i++) {
441
543
  const task = config.tasks[i]!;
442
544
  const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
443
545
 
@@ -456,13 +558,13 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
456
558
 
457
559
  // Update state
458
560
  state.currentTaskIndex = i + 1;
459
- saveState(path.join(runDir, 'state.json'), state);
561
+ saveState(statePath, state);
460
562
 
461
563
  // Handle blocked or error
462
564
  if (result.status === 'BLOCKED_DEPENDENCY') {
463
- state.status = 'failed'; // Or blocked if we had a blocked status in LaneState
565
+ state.status = 'failed';
464
566
  state.dependencyRequest = result.dependencyRequest || null;
465
- saveState(path.join(runDir, 'state.json'), state);
567
+ saveState(statePath, state);
466
568
  logger.warn('Task blocked on dependency change');
467
569
  process.exit(2);
468
570
  }
@@ -470,7 +572,7 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
470
572
  if (result.status !== 'FINISHED') {
471
573
  state.status = 'failed';
472
574
  state.error = result.error || 'Unknown error';
473
- saveState(path.join(runDir, 'state.json'), state);
575
+ saveState(statePath, state);
474
576
  logger.error(`Task failed: ${result.error}`);
475
577
  process.exit(1);
476
578
  }
@@ -484,7 +586,7 @@ export async function runTasks(config: RunnerConfig, runDir: string): Promise<Ta
484
586
  // Complete
485
587
  state.status = 'completed';
486
588
  state.endTime = Date.now();
487
- saveState(path.join(runDir, 'state.json'), state);
589
+ saveState(statePath, state);
488
590
 
489
591
  logger.success('All tasks completed!');
490
592
  return results;
@@ -503,9 +605,11 @@ if (require.main === module) {
503
605
 
504
606
  const tasksFile = args[0]!;
505
607
  const runDirIdx = args.indexOf('--run-dir');
608
+ const startIdxIdx = args.indexOf('--start-index');
506
609
  // const executorIdx = args.indexOf('--executor');
507
610
 
508
611
  const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
612
+ const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
509
613
  // const executor = executorIdx >= 0 ? args[executorIdx + 1] : 'cursor-agent';
510
614
 
511
615
  if (!fs.existsSync(tasksFile)) {
@@ -529,7 +633,7 @@ if (require.main === module) {
529
633
  };
530
634
 
531
635
  // Run tasks
532
- runTasks(config, runDir)
636
+ runTasks(tasksFile, config, runDir, { startIndex })
533
637
  .then(() => {
534
638
  process.exit(0);
535
639
  })