@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.
- package/CHANGELOG.md +55 -0
- package/README.md +113 -319
- package/commands/cursorflow-clean.md +24 -135
- package/commands/cursorflow-doctor.md +74 -18
- package/commands/cursorflow-init.md +33 -50
- package/commands/cursorflow-models.md +51 -0
- package/commands/cursorflow-monitor.md +56 -118
- package/commands/cursorflow-prepare.md +410 -108
- package/commands/cursorflow-resume.md +51 -148
- package/commands/cursorflow-review.md +38 -202
- package/commands/cursorflow-run.md +208 -86
- package/commands/cursorflow-signal.md +38 -12
- package/dist/cli/clean.d.ts +3 -1
- package/dist/cli/clean.js +145 -8
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/doctor.js +14 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +32 -21
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +5 -4
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/models.d.ts +7 -0
- package/dist/cli/models.js +104 -0
- package/dist/cli/models.js.map +1 -0
- package/dist/cli/monitor.js +56 -1
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.d.ts +7 -0
- package/dist/cli/prepare.js +748 -0
- package/dist/cli/prepare.js.map +1 -0
- package/dist/cli/resume.js +56 -0
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +30 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +18 -0
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/runner.d.ts +9 -1
- package/dist/core/runner.js +139 -23
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/cursor-agent.d.ts +4 -0
- package/dist/utils/cursor-agent.js +58 -10
- package/dist/utils/cursor-agent.js.map +1 -1
- package/dist/utils/doctor.d.ts +10 -0
- package/dist/utils/doctor.js +581 -1
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/types.d.ts +11 -0
- package/examples/README.md +114 -59
- package/examples/demo-project/README.md +61 -79
- package/examples/demo-project/_cursorflow/tasks/demo-test/01-create-utils.json +17 -6
- package/examples/demo-project/_cursorflow/tasks/demo-test/02-add-tests.json +17 -6
- package/examples/demo-project/_cursorflow/tasks/demo-test/README.md +66 -25
- package/package.json +1 -1
- package/scripts/patches/test-cursor-agent.js +203 -0
- package/src/cli/clean.ts +156 -9
- package/src/cli/doctor.ts +18 -2
- package/src/cli/index.ts +33 -21
- package/src/cli/init.ts +6 -4
- package/src/cli/models.ts +83 -0
- package/src/cli/monitor.ts +60 -1
- package/src/cli/prepare.ts +844 -0
- package/src/cli/resume.ts +66 -0
- package/src/cli/run.ts +36 -2
- package/src/cli/signal.ts +22 -0
- package/src/core/runner.ts +164 -23
- package/src/utils/cursor-agent.ts +62 -10
- package/src/utils/doctor.ts +633 -5
- 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
|
|
package/src/core/runner.ts
CHANGED
|
@@ -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
|
-
|
|
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: [
|
|
134
|
-
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
|
-
|
|
151
|
-
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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:
|
|
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
|
-
},
|
|
307
|
+
}, timeoutMs);
|
|
182
308
|
|
|
183
309
|
child.on('close', (code) => {
|
|
184
|
-
clearTimeout(
|
|
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(
|
|
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
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
139
|
+
if (discoveredModels.length > 0) {
|
|
140
|
+
return [...new Set([...knownModels, ...discoveredModels])];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return knownModels;
|
|
125
144
|
} catch (error: any) {
|
|
126
|
-
|
|
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;
|