@litmers/cursorflow-orchestrator 0.1.8 → 0.1.12

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 (66) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +113 -319
  3. package/commands/cursorflow-clean.md +24 -135
  4. package/commands/cursorflow-doctor.md +74 -18
  5. package/commands/cursorflow-init.md +33 -50
  6. package/commands/cursorflow-models.md +51 -0
  7. package/commands/cursorflow-monitor.md +56 -118
  8. package/commands/cursorflow-prepare.md +410 -108
  9. package/commands/cursorflow-resume.md +51 -148
  10. package/commands/cursorflow-review.md +38 -202
  11. package/commands/cursorflow-run.md +208 -86
  12. package/commands/cursorflow-signal.md +38 -12
  13. package/dist/cli/clean.d.ts +3 -1
  14. package/dist/cli/clean.js +145 -8
  15. package/dist/cli/clean.js.map +1 -1
  16. package/dist/cli/doctor.js +14 -1
  17. package/dist/cli/doctor.js.map +1 -1
  18. package/dist/cli/index.js +32 -21
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/init.js +5 -4
  21. package/dist/cli/init.js.map +1 -1
  22. package/dist/cli/models.d.ts +7 -0
  23. package/dist/cli/models.js +104 -0
  24. package/dist/cli/models.js.map +1 -0
  25. package/dist/cli/monitor.js +56 -1
  26. package/dist/cli/monitor.js.map +1 -1
  27. package/dist/cli/prepare.d.ts +7 -0
  28. package/dist/cli/prepare.js +748 -0
  29. package/dist/cli/prepare.js.map +1 -0
  30. package/dist/cli/resume.js +56 -0
  31. package/dist/cli/resume.js.map +1 -1
  32. package/dist/cli/run.js +30 -1
  33. package/dist/cli/run.js.map +1 -1
  34. package/dist/cli/signal.js +18 -0
  35. package/dist/cli/signal.js.map +1 -1
  36. package/dist/core/runner.d.ts +9 -1
  37. package/dist/core/runner.js +139 -23
  38. package/dist/core/runner.js.map +1 -1
  39. package/dist/utils/cursor-agent.d.ts +4 -0
  40. package/dist/utils/cursor-agent.js +58 -10
  41. package/dist/utils/cursor-agent.js.map +1 -1
  42. package/dist/utils/doctor.d.ts +10 -0
  43. package/dist/utils/doctor.js +581 -1
  44. package/dist/utils/doctor.js.map +1 -1
  45. package/dist/utils/types.d.ts +11 -0
  46. package/examples/README.md +114 -59
  47. package/examples/demo-project/README.md +61 -79
  48. package/examples/demo-project/_cursorflow/tasks/demo-test/01-create-utils.json +17 -6
  49. package/examples/demo-project/_cursorflow/tasks/demo-test/02-add-tests.json +17 -6
  50. package/examples/demo-project/_cursorflow/tasks/demo-test/README.md +66 -25
  51. package/package.json +1 -1
  52. package/scripts/patches/test-cursor-agent.js +203 -0
  53. package/src/cli/clean.ts +156 -9
  54. package/src/cli/doctor.ts +18 -2
  55. package/src/cli/index.ts +33 -21
  56. package/src/cli/init.ts +6 -4
  57. package/src/cli/models.ts +83 -0
  58. package/src/cli/monitor.ts +60 -1
  59. package/src/cli/prepare.ts +844 -0
  60. package/src/cli/resume.ts +66 -0
  61. package/src/cli/run.ts +36 -2
  62. package/src/cli/signal.ts +22 -0
  63. package/src/core/runner.ts +164 -23
  64. package/src/utils/cursor-agent.ts +62 -10
  65. package/src/utils/doctor.ts +633 -5
  66. package/src/utils/types.ts +11 -0
package/src/cli/resume.ts CHANGED
@@ -9,12 +9,31 @@ import * as logger from '../utils/logger';
9
9
  import { loadConfig, getLogsDir } from '../utils/config';
10
10
  import { loadState } from '../utils/state';
11
11
  import { LaneState } from '../utils/types';
12
+ import { runDoctor } from '../utils/doctor';
12
13
 
13
14
  interface ResumeOptions {
14
15
  lane: string | null;
15
16
  runDir: string | null;
16
17
  clean: boolean;
17
18
  restart: boolean;
19
+ skipDoctor: boolean;
20
+ help: boolean;
21
+ }
22
+
23
+ function printHelp(): void {
24
+ console.log(`
25
+ Usage: cursorflow resume <lane> [options]
26
+
27
+ Resume an interrupted or failed lane.
28
+
29
+ Options:
30
+ <lane> Lane name to resume
31
+ --run-dir <path> Use a specific run directory (default: latest)
32
+ --clean Clean up existing worktree before resuming
33
+ --restart Restart from the first task (index 0)
34
+ --skip-doctor Skip environment/branch checks (not recommended)
35
+ --help, -h Show help
36
+ `);
18
37
  }
19
38
 
20
39
  function parseArgs(args: string[]): ResumeOptions {
@@ -25,6 +44,8 @@ function parseArgs(args: string[]): ResumeOptions {
25
44
  runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
26
45
  clean: args.includes('--clean'),
27
46
  restart: args.includes('--restart'),
47
+ skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
48
+ help: args.includes('--help') || args.includes('-h'),
28
49
  };
29
50
  }
30
51
 
@@ -45,6 +66,12 @@ function findLatestRunDir(logsDir: string): string | null {
45
66
 
46
67
  async function resume(args: string[]): Promise<void> {
47
68
  const options = parseArgs(args);
69
+
70
+ if (options.help) {
71
+ printHelp();
72
+ return;
73
+ }
74
+
48
75
  const config = loadConfig();
49
76
  const logsDir = getLogsDir(config);
50
77
 
@@ -77,6 +104,45 @@ async function resume(args: string[]): Promise<void> {
77
104
  throw new Error(`Original tasks file not found: ${state.tasksFile}. Resume impossible without task definition.`);
78
105
  }
79
106
 
107
+ // Run doctor check before resuming (check branches, etc.)
108
+ if (!options.skipDoctor) {
109
+ const tasksDir = path.dirname(state.tasksFile);
110
+ logger.info('Running pre-flight checks...');
111
+
112
+ const report = runDoctor({
113
+ cwd: process.cwd(),
114
+ tasksDir,
115
+ includeCursorAgentChecks: false, // Skip agent checks for resume
116
+ });
117
+
118
+ // Only show blocking errors for resume
119
+ const blockingIssues = report.issues.filter(i =>
120
+ i.severity === 'error' &&
121
+ (i.id.startsWith('branch.') || i.id.startsWith('git.'))
122
+ );
123
+
124
+ if (blockingIssues.length > 0) {
125
+ logger.section('🛑 Pre-resume check found issues');
126
+ for (const issue of blockingIssues) {
127
+ logger.error(`${issue.title} (${issue.id})`, '❌');
128
+ console.log(` ${issue.message}`);
129
+ if (issue.details) console.log(` Details: ${issue.details}`);
130
+ if (issue.fixes?.length) {
131
+ console.log(' Fix:');
132
+ for (const fix of issue.fixes) console.log(` - ${fix}`);
133
+ }
134
+ console.log('');
135
+ }
136
+ throw new Error('Pre-resume checks failed. Use --skip-doctor to bypass (not recommended).');
137
+ }
138
+
139
+ // Show warnings but don't block
140
+ const warnings = report.issues.filter(i => i.severity === 'warn' && i.id.startsWith('branch.'));
141
+ if (warnings.length > 0) {
142
+ logger.warn(`${warnings.length} warning(s) found. Run 'cursorflow doctor' for details.`);
143
+ }
144
+ }
145
+
80
146
  logger.section(`🔁 Resuming Lane: ${options.lane}`);
81
147
  logger.info(`Run: ${path.basename(runDir)}`);
82
148
  logger.info(`Tasks: ${state.tasksFile}`);
package/src/cli/run.ts CHANGED
@@ -7,31 +7,57 @@ import * as fs from 'fs';
7
7
  import * as logger from '../utils/logger';
8
8
  import { orchestrate } from '../core/orchestrator';
9
9
  import { getLogsDir, loadConfig } from '../utils/config';
10
- import { runDoctor } from '../utils/doctor';
10
+ import { runDoctor, getDoctorStatus } from '../utils/doctor';
11
11
  import { areCommandsInstalled, setupCommands } from './setup-commands';
12
12
 
13
13
  interface RunOptions {
14
14
  tasksDir?: string;
15
15
  dryRun: boolean;
16
16
  executor: string | null;
17
+ maxConcurrent: number | null;
17
18
  skipDoctor: boolean;
19
+ help: boolean;
20
+ }
21
+
22
+ function printHelp(): void {
23
+ console.log(`
24
+ Usage: cursorflow run <tasks-dir> [options]
25
+
26
+ Run task orchestration based on dependency graph.
27
+
28
+ Options:
29
+ <tasks-dir> Directory containing task JSON files
30
+ --max-concurrent <num> Limit parallel agents (overrides config)
31
+ --executor <type> cursor-agent | cloud
32
+ --skip-doctor Skip environment checks (not recommended)
33
+ --dry-run Show execution plan without starting agents
34
+ --help, -h Show help
35
+ `);
18
36
  }
19
37
 
20
38
  function parseArgs(args: string[]): RunOptions {
21
39
  const tasksDir = args.find(a => !a.startsWith('--'));
22
40
  const executorIdx = args.indexOf('--executor');
41
+ const maxConcurrentIdx = args.indexOf('--max-concurrent');
23
42
 
24
43
  return {
25
44
  tasksDir,
26
45
  dryRun: args.includes('--dry-run'),
27
46
  executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
47
+ maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '0') || null : null,
28
48
  skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
49
+ help: args.includes('--help') || args.includes('-h'),
29
50
  };
30
51
  }
31
52
 
32
53
  async function run(args: string[]): Promise<void> {
33
54
  const options = parseArgs(args);
34
55
 
56
+ if (options.help) {
57
+ printHelp();
58
+ return;
59
+ }
60
+
35
61
  // Auto-setup Cursor commands if missing or outdated
36
62
  if (!areCommandsInstalled()) {
37
63
  logger.info('Installing missing or outdated Cursor IDE commands...');
@@ -64,6 +90,14 @@ async function run(args: string[]): Promise<void> {
64
90
  throw new Error(`Tasks directory not found: ${tasksDir}`);
65
91
  }
66
92
 
93
+ // Check if doctor has been run at least once
94
+ const doctorStatus = getDoctorStatus(config.projectRoot);
95
+ if (!doctorStatus) {
96
+ logger.warn('It looks like you haven\'t run `cursorflow doctor` yet.');
97
+ logger.warn('Running doctor is highly recommended to catch environment issues early.');
98
+ console.log(' Run: cursorflow doctor\n');
99
+ }
100
+
67
101
  // Preflight checks (doctor)
68
102
  if (!options.skipDoctor) {
69
103
  const report = runDoctor({
@@ -99,7 +133,7 @@ async function run(args: string[]): Promise<void> {
99
133
  executor: options.executor || config.executor,
100
134
  pollInterval: config.pollInterval * 1000,
101
135
  runDir: path.join(logsDir, 'runs', `run-${Date.now()}`),
102
- maxConcurrentLanes: config.maxConcurrentLanes,
136
+ maxConcurrentLanes: options.maxConcurrent || config.maxConcurrentLanes,
103
137
  });
104
138
  } catch (error: any) {
105
139
  // Re-throw to be handled by the main entry point
package/src/cli/signal.ts CHANGED
@@ -14,6 +14,21 @@ interface SignalOptions {
14
14
  lane: string | null;
15
15
  message: string | null;
16
16
  runDir: string | null;
17
+ help: boolean;
18
+ }
19
+
20
+ function printHelp(): void {
21
+ console.log(`
22
+ Usage: cursorflow signal <lane> "<message>" [options]
23
+
24
+ Directly intervene in a running lane by sending a message to the agent.
25
+
26
+ Options:
27
+ <lane> Lane name to signal
28
+ "<message>" Message text to send
29
+ --run-dir <path> Use a specific run directory (default: latest)
30
+ --help, -h Show help
31
+ `);
17
32
  }
18
33
 
19
34
  function parseArgs(args: string[]): SignalOptions {
@@ -26,6 +41,7 @@ function parseArgs(args: string[]): SignalOptions {
26
41
  lane: nonOptions[0] || null,
27
42
  message: nonOptions.slice(1).join(' ') || null,
28
43
  runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
44
+ help: args.includes('--help') || args.includes('-h'),
29
45
  };
30
46
  }
31
47
 
@@ -43,6 +59,12 @@ function findLatestRunDir(logsDir: string): string | null {
43
59
 
44
60
  async function signal(args: string[]): Promise<void> {
45
61
  const options = parseArgs(args);
62
+
63
+ if (options.help) {
64
+ printHelp();
65
+ return;
66
+ }
67
+
46
68
  const config = loadConfig();
47
69
  const logsDir = getLogsDir(config);
48
70
 
@@ -107,15 +107,79 @@ function parseJsonFromStdout(stdout: string): any {
107
107
  return null;
108
108
  }
109
109
 
110
+ /** Default timeout: 5 minutes */
111
+ const DEFAULT_TIMEOUT_MS = 300000;
112
+
113
+ /** Heartbeat interval: 30 seconds */
114
+ const HEARTBEAT_INTERVAL_MS = 30000;
115
+
116
+ /**
117
+ * Validate task configuration
118
+ * @throws Error if validation fails
119
+ */
120
+ export function validateTaskConfig(config: RunnerConfig): void {
121
+ if (!config.tasks || !Array.isArray(config.tasks)) {
122
+ throw new Error('Invalid config: "tasks" must be an array');
123
+ }
124
+
125
+ if (config.tasks.length === 0) {
126
+ throw new Error('Invalid config: "tasks" array is empty');
127
+ }
128
+
129
+ for (let i = 0; i < config.tasks.length; i++) {
130
+ const task = config.tasks[i];
131
+ const taskNum = i + 1;
132
+
133
+ if (!task) {
134
+ throw new Error(`Invalid config: Task ${taskNum} is null or undefined`);
135
+ }
136
+
137
+ if (!task.name || typeof task.name !== 'string') {
138
+ throw new Error(
139
+ `Invalid config: Task ${taskNum} missing required "name" field.\n` +
140
+ ` Found: ${JSON.stringify(task, null, 2).substring(0, 200)}...\n` +
141
+ ` Expected: { "name": "task-name", "prompt": "..." }`
142
+ );
143
+ }
144
+
145
+ if (!task.prompt || typeof task.prompt !== 'string') {
146
+ throw new Error(
147
+ `Invalid config: Task "${task.name}" (${taskNum}) missing required "prompt" field`
148
+ );
149
+ }
150
+
151
+ // Validate task name format (no spaces, special chars that could break branch names)
152
+ if (!/^[a-zA-Z0-9_-]+$/.test(task.name)) {
153
+ throw new Error(
154
+ `Invalid config: Task name "${task.name}" contains invalid characters.\n` +
155
+ ` Task names must only contain: letters, numbers, underscore (_), hyphen (-)`
156
+ );
157
+ }
158
+ }
159
+
160
+ // Validate timeout if provided
161
+ if (config.timeout !== undefined) {
162
+ if (typeof config.timeout !== 'number' || config.timeout <= 0) {
163
+ throw new Error(
164
+ `Invalid config: "timeout" must be a positive number (milliseconds).\n` +
165
+ ` Found: ${config.timeout}`
166
+ );
167
+ }
168
+ }
169
+ }
170
+
110
171
  /**
111
172
  * Execute cursor-agent command with streaming and better error handling
112
173
  */
113
- export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir }: {
174
+ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention }: {
114
175
  workspaceDir: string;
115
176
  chatId: string;
116
177
  prompt: string;
117
178
  model?: string;
118
179
  signalDir?: string;
180
+ timeout?: number;
181
+ /** Enable stdin piping for intervention feature (may cause buffering issues on some systems) */
182
+ enableIntervention?: boolean;
119
183
  }): Promise<AgentSendResult> {
120
184
  const args = [
121
185
  '--print',
@@ -126,17 +190,67 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
126
190
  prompt,
127
191
  ];
128
192
 
129
- logger.info('Executing cursor-agent...');
193
+ const timeoutMs = timeout || DEFAULT_TIMEOUT_MS;
194
+ logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
195
+
196
+ // Determine stdio mode based on intervention setting
197
+ // When intervention is enabled, we pipe stdin for message injection
198
+ // When disabled (default), we ignore stdin to avoid buffering issues
199
+ const stdinMode = enableIntervention ? 'pipe' : 'ignore';
200
+
201
+ if (enableIntervention) {
202
+ logger.info('Intervention mode enabled (stdin piped)');
203
+ }
130
204
 
131
205
  return new Promise((resolve) => {
206
+ // Build environment, preserving user's NODE_OPTIONS but disabling problematic flags
207
+ const childEnv = { ...process.env };
208
+
209
+ // Only filter out specific problematic NODE_OPTIONS, don't clear entirely
210
+ if (childEnv.NODE_OPTIONS) {
211
+ // Remove flags that might interfere with cursor-agent
212
+ const filtered = childEnv.NODE_OPTIONS
213
+ .split(' ')
214
+ .filter(opt => !opt.includes('--inspect') && !opt.includes('--debug'))
215
+ .join(' ');
216
+ childEnv.NODE_OPTIONS = filtered;
217
+ }
218
+
219
+ // Disable Python buffering in case cursor-agent uses Python
220
+ childEnv.PYTHONUNBUFFERED = '1';
221
+
132
222
  const child = spawn('cursor-agent', args, {
133
- stdio: ['pipe', 'pipe', 'pipe'], // Enable stdin piping
134
- env: process.env,
223
+ stdio: [stdinMode, 'pipe', 'pipe'],
224
+ env: childEnv,
135
225
  });
136
226
 
227
+ // Save PID to state if possible
228
+ if (child.pid && signalDir) {
229
+ try {
230
+ const statePath = path.join(signalDir, 'state.json');
231
+ if (fs.existsSync(statePath)) {
232
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
233
+ state.pid = child.pid;
234
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
235
+ }
236
+ } catch (e) {
237
+ // Best effort
238
+ }
239
+ }
240
+
137
241
  let fullStdout = '';
138
242
  let fullStderr = '';
139
243
 
244
+ // Heartbeat logging to show progress
245
+ let lastHeartbeat = Date.now();
246
+ let bytesReceived = 0;
247
+ const heartbeatInterval = setInterval(() => {
248
+ const elapsed = Math.round((Date.now() - lastHeartbeat) / 1000);
249
+ const totalElapsed = Math.round((Date.now() - startTime) / 1000);
250
+ logger.info(`⏱ Heartbeat: ${totalElapsed}s elapsed, ${bytesReceived} bytes received`);
251
+ }, HEARTBEAT_INTERVAL_MS);
252
+ const startTime = Date.now();
253
+
140
254
  // Watch for "intervention.txt" signal file if any
141
255
  const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
142
256
  let interventionWatcher: fs.FSWatcher | null = null;
@@ -147,8 +261,13 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
147
261
  try {
148
262
  const message = fs.readFileSync(interventionPath, 'utf8').trim();
149
263
  if (message) {
150
- logger.info(`Injecting intervention: ${message}`);
151
- child.stdin.write(message + '\n');
264
+ if (enableIntervention && child.stdin) {
265
+ logger.info(`Injecting intervention: ${message}`);
266
+ child.stdin.write(message + '\n');
267
+ } else {
268
+ logger.warn(`Intervention requested but stdin not available: ${message}`);
269
+ logger.warn('To enable intervention, set enableIntervention: true in config');
270
+ }
152
271
  fs.unlinkSync(interventionPath); // Clear it
153
272
  }
154
273
  } catch (e) {
@@ -158,30 +277,38 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
158
277
  });
159
278
  }
160
279
 
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
- });
280
+ if (child.stdout) {
281
+ child.stdout.on('data', (data) => {
282
+ const str = data.toString();
283
+ fullStdout += str;
284
+ bytesReceived += data.length;
285
+ // Also pipe to our own stdout so it goes to terminal.log
286
+ process.stdout.write(data);
287
+ });
288
+ }
167
289
 
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
- });
290
+ if (child.stderr) {
291
+ child.stderr.on('data', (data) => {
292
+ fullStderr += data.toString();
293
+ // Pipe to our own stderr so it goes to terminal.log
294
+ process.stderr.write(data);
295
+ });
296
+ }
173
297
 
174
- const timeout = setTimeout(() => {
298
+ const timeoutHandle = setTimeout(() => {
299
+ clearInterval(heartbeatInterval);
175
300
  child.kill();
301
+ const timeoutSec = Math.round(timeoutMs / 1000);
176
302
  resolve({
177
303
  ok: false,
178
304
  exitCode: -1,
179
- error: 'cursor-agent timed out after 5 minutes. The LLM request may be taking too long or there may be network issues.',
305
+ error: `cursor-agent timed out after ${timeoutSec} seconds. The LLM request may be taking too long or there may be network issues.`,
180
306
  });
181
- }, 300000);
307
+ }, timeoutMs);
182
308
 
183
309
  child.on('close', (code) => {
184
- clearTimeout(timeout);
310
+ clearTimeout(timeoutHandle);
311
+ clearInterval(heartbeatInterval);
185
312
  if (interventionWatcher) interventionWatcher.close();
186
313
 
187
314
  const json = parseJsonFromStdout(fullStdout);
@@ -214,7 +341,8 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
214
341
  });
215
342
 
216
343
  child.on('error', (err) => {
217
- clearTimeout(timeout);
344
+ clearTimeout(timeoutHandle);
345
+ clearInterval(heartbeatInterval);
218
346
  resolve({
219
347
  ok: false,
220
348
  exitCode: -1,
@@ -361,7 +489,9 @@ export async function runTask({
361
489
  chatId,
362
490
  prompt: prompt1,
363
491
  model,
364
- signalDir: runDir
492
+ signalDir: runDir,
493
+ timeout: config.timeout,
494
+ enableIntervention: config.enableIntervention,
365
495
  });
366
496
 
367
497
  appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error || 'No response', {
@@ -405,6 +535,17 @@ export async function runTask({
405
535
  export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number } = {}): Promise<TaskExecutionResult[]> {
406
536
  const startIndex = options.startIndex || 0;
407
537
 
538
+ // Validate configuration before starting
539
+ logger.info('Validating task configuration...');
540
+ try {
541
+ validateTaskConfig(config);
542
+ logger.success('✓ Configuration valid');
543
+ } catch (validationError: any) {
544
+ logger.error('❌ Configuration validation failed');
545
+ logger.error(` ${validationError.message}`);
546
+ throw validationError;
547
+ }
548
+
408
549
  // Ensure cursor-agent is installed
409
550
  ensureCursorAgent();
410
551
 
@@ -114,19 +114,35 @@ export function validateSetup(executor = 'cursor-agent'): { valid: boolean; erro
114
114
  * Get available models (if cursor-agent supports it)
115
115
  */
116
116
  export function getAvailableModels(): string[] {
117
+ // Known models in the current version of cursor-agent
118
+ const knownModels = [
119
+ 'sonnet-4.5',
120
+ 'sonnet-4.5-thinking',
121
+ 'opus-4.5',
122
+ 'opus-4.5-thinking',
123
+ 'gpt-5.2',
124
+ 'gpt-5.2-high',
125
+ ];
126
+
117
127
  try {
118
- // This is a placeholder - actual implementation depends on cursor-agent API
119
- // execSync('cursor-agent --model invalid "test"', {
120
- // encoding: 'utf8',
121
- // stdio: 'pipe',
122
- // });
128
+ // Try to trigger a model list by using an invalid model with --print
129
+ // Some versions of cursor-agent output valid models when an invalid one is used.
130
+ const result = spawnSync('cursor-agent', ['--print', '--model', 'list-available-models', 'test'], {
131
+ encoding: 'utf8',
132
+ stdio: 'pipe',
133
+ timeout: 5000,
134
+ });
135
+
136
+ const output = (result.stderr || result.stdout || '').toString();
137
+ const discoveredModels = parseModelsFromOutput(output);
123
138
 
124
- return [];
139
+ if (discoveredModels.length > 0) {
140
+ return [...new Set([...knownModels, ...discoveredModels])];
141
+ }
142
+
143
+ return knownModels;
125
144
  } catch (error: any) {
126
- // Parse from error message
127
- const output = (error.stderr || error.stdout || '').toString();
128
- // Extract model names from output
129
- return parseModelsFromOutput(output);
145
+ return knownModels;
130
146
  }
131
147
  }
132
148
 
@@ -175,6 +191,42 @@ export function testCursorAgent(): { success: boolean; output?: string; error?:
175
191
  }
176
192
  }
177
193
 
194
+ /**
195
+ * Run interactive agent test to prime permissions (MCP, user approval, etc.)
196
+ */
197
+ export function runInteractiveAgentTest(): boolean {
198
+ const { spawnSync } = require('child_process');
199
+
200
+ console.log('\n' + '━'.repeat(60));
201
+ console.log('🤖 Interactive Agent Priming Test');
202
+ console.log('━'.repeat(60));
203
+ console.log('\nThis will start cursor-agent in interactive mode (NOT --print).');
204
+ console.log('Use this to approve MCP permissions or initial setup requests.\n');
205
+ console.log('MISSION: Just say hello and confirm MCP connectivity.');
206
+ console.log('ACTION: Once the agent responds and finishes, you can exit.');
207
+ console.log('\n' + '─'.repeat(60) + '\n');
208
+
209
+ try {
210
+ // Run WITHOUT --print to allow interactive user input and UI popups
211
+ const result = spawnSync('cursor-agent', ['Hello, verify MCP and system access.'], {
212
+ stdio: 'inherit', // Crucial for interactivity
213
+ env: process.env,
214
+ });
215
+
216
+ console.log('\n' + '─'.repeat(60));
217
+ if (result.status === 0) {
218
+ console.log('✅ Interactive test completed successfully!');
219
+ return true;
220
+ } else {
221
+ console.log('❌ Interactive test exited with code: ' + result.status);
222
+ return false;
223
+ }
224
+ } catch (error: any) {
225
+ console.log('❌ Failed to run interactive test: ' + error.message);
226
+ return false;
227
+ }
228
+ }
229
+
178
230
  export interface AuthCheckResult {
179
231
  authenticated: boolean;
180
232
  message: string;