@litmers/cursorflow-orchestrator 0.1.30 → 0.1.34
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/README.md +144 -52
- package/commands/cursorflow-add.md +159 -0
- package/commands/cursorflow-monitor.md +23 -2
- package/commands/cursorflow-new.md +87 -0
- package/dist/cli/add.d.ts +7 -0
- package/dist/cli/add.js +377 -0
- package/dist/cli/add.js.map +1 -0
- package/dist/cli/clean.js +1 -0
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/config.d.ts +7 -0
- package/dist/cli/config.js +181 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.js +34 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.js +7 -33
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +51 -62
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/new.d.ts +7 -0
- package/dist/cli/new.js +232 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/prepare.js +95 -193
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +11 -47
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +27 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tasks.js +1 -2
- package/dist/cli/tasks.js.map +1 -1
- package/dist/core/failure-policy.d.ts +9 -0
- package/dist/core/failure-policy.js +9 -0
- package/dist/core/failure-policy.js.map +1 -1
- package/dist/core/orchestrator.d.ts +20 -6
- package/dist/core/orchestrator.js +217 -331
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.d.ts +27 -0
- package/dist/core/runner/agent.js +294 -0
- package/dist/core/runner/agent.js.map +1 -0
- package/dist/core/runner/index.d.ts +5 -0
- package/dist/core/runner/index.js +22 -0
- package/dist/core/runner/index.js.map +1 -0
- package/dist/core/runner/pipeline.d.ts +9 -0
- package/dist/core/runner/pipeline.js +539 -0
- package/dist/core/runner/pipeline.js.map +1 -0
- package/dist/core/runner/prompt.d.ts +25 -0
- package/dist/core/runner/prompt.js +175 -0
- package/dist/core/runner/prompt.js.map +1 -0
- package/dist/core/runner/task.d.ts +26 -0
- package/dist/core/runner/task.js +283 -0
- package/dist/core/runner/task.js.map +1 -0
- package/dist/core/runner/utils.d.ts +37 -0
- package/dist/core/runner/utils.js +161 -0
- package/dist/core/runner/utils.js.map +1 -0
- package/dist/core/runner.d.ts +2 -96
- package/dist/core/runner.js +11 -1136
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +326 -0
- package/dist/core/stall-detection.js +781 -0
- package/dist/core/stall-detection.js.map +1 -0
- package/dist/types/config.d.ts +6 -6
- package/dist/types/flow.d.ts +84 -0
- package/dist/types/flow.js +10 -0
- package/dist/types/flow.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +3 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/lane.d.ts +0 -2
- package/dist/types/logging.d.ts +5 -1
- package/dist/types/task.d.ts +7 -11
- package/dist/utils/config.js +7 -15
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/dependency.d.ts +36 -1
- package/dist/utils/dependency.js +256 -1
- package/dist/utils/dependency.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +45 -82
- package/dist/utils/enhanced-logger.js +238 -844
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +29 -0
- package/dist/utils/git.js +115 -5
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/state.js +0 -2
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +2 -2
- package/dist/utils/task-service.js +40 -31
- package/dist/utils/task-service.js.map +1 -1
- package/package.json +4 -3
- package/src/cli/add.ts +397 -0
- package/src/cli/clean.ts +1 -0
- package/src/cli/config.ts +177 -0
- package/src/cli/index.ts +36 -32
- package/src/cli/logs.ts +7 -31
- package/src/cli/monitor.ts +55 -71
- package/src/cli/new.ts +235 -0
- package/src/cli/prepare.ts +98 -205
- package/src/cli/resume.ts +13 -56
- package/src/cli/run.ts +311 -306
- package/src/cli/tasks.ts +1 -2
- package/src/core/failure-policy.ts +9 -0
- package/src/core/orchestrator.ts +281 -375
- package/src/core/runner/agent.ts +314 -0
- package/src/core/runner/index.ts +6 -0
- package/src/core/runner/pipeline.ts +567 -0
- package/src/core/runner/prompt.ts +174 -0
- package/src/core/runner/task.ts +320 -0
- package/src/core/runner/utils.ts +142 -0
- package/src/core/runner.ts +8 -1347
- package/src/core/stall-detection.ts +936 -0
- package/src/types/config.ts +6 -6
- package/src/types/flow.ts +91 -0
- package/src/types/index.ts +15 -3
- package/src/types/lane.ts +0 -2
- package/src/types/logging.ts +5 -1
- package/src/types/task.ts +7 -11
- package/src/utils/config.ts +8 -16
- package/src/utils/dependency.ts +311 -2
- package/src/utils/enhanced-logger.ts +263 -927
- package/src/utils/git.ts +145 -5
- package/src/utils/state.ts +0 -2
- package/src/utils/task-service.ts +48 -40
- package/commands/cursorflow-review.md +0 -56
- package/commands/cursorflow-runs.md +0 -59
- package/dist/cli/runs.d.ts +0 -5
- package/dist/cli/runs.js +0 -214
- package/dist/cli/runs.js.map +0 -1
- package/dist/core/reviewer.d.ts +0 -66
- package/dist/core/reviewer.js +0 -265
- package/dist/core/reviewer.js.map +0 -1
- package/src/cli/runs.ts +0 -212
- package/src/core/reviewer.ts +0 -285
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
import * as logger from '../../utils/logger';
|
|
3
|
+
import { AgentSendResult, DependencyRequestPlan } from '../../types';
|
|
4
|
+
import { withRetry } from '../failure-policy';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { appendLog, createConversationEntry } from '../../utils/state';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute cursor-agent command with timeout and better error handling
|
|
11
|
+
*/
|
|
12
|
+
export function cursorAgentCreateChat(workspaceDir?: string): string {
|
|
13
|
+
try {
|
|
14
|
+
const args = ['create-chat'];
|
|
15
|
+
if (workspaceDir) {
|
|
16
|
+
args.push('--workspace', workspaceDir);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const res = spawnSync('cursor-agent', args, {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
stdio: 'pipe',
|
|
22
|
+
timeout: 30000, // 30 second timeout
|
|
23
|
+
cwd: workspaceDir || process.cwd(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (res.error || res.status !== 0) {
|
|
27
|
+
throw res.error || new Error(res.stderr || 'Failed to create chat');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const out = res.stdout;
|
|
31
|
+
const lines = out.split('\n').filter(Boolean);
|
|
32
|
+
const chatId = lines[lines.length - 1] || null;
|
|
33
|
+
|
|
34
|
+
if (!chatId) {
|
|
35
|
+
throw new Error('Failed to get chat ID from cursor-agent');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
logger.info(`Created chat session: ${chatId}`);
|
|
39
|
+
return chatId;
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
// Check for common errors
|
|
42
|
+
if (error.message.includes('ENOENT')) {
|
|
43
|
+
throw new Error('cursor-agent CLI not found. Install with: npm install -g @cursor/agent');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (error.message.includes('ETIMEDOUT') || error.killed) {
|
|
47
|
+
throw new Error('cursor-agent timed out. Check your internet connection and Cursor authentication.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (error.stderr) {
|
|
51
|
+
const stderr = error.stderr.toString();
|
|
52
|
+
|
|
53
|
+
// Check for authentication errors
|
|
54
|
+
if (stderr.includes('not authenticated') ||
|
|
55
|
+
stderr.includes('login') ||
|
|
56
|
+
stderr.includes('auth')) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'Cursor authentication failed. Please:\n' +
|
|
59
|
+
' 1. Open Cursor IDE\n' +
|
|
60
|
+
' 2. Sign in to your account\n' +
|
|
61
|
+
' 3. Verify you can use AI features\n' +
|
|
62
|
+
' 4. Try running cursorflow again\n\n' +
|
|
63
|
+
`Original error: ${stderr.trim()}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check for API key errors
|
|
68
|
+
if (stderr.includes('api key') || stderr.includes('API_KEY')) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
'Cursor API key error. Please check your Cursor account and subscription.\n' +
|
|
71
|
+
`Error: ${stderr.trim()}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Helper to parse JSON from stdout
|
|
82
|
+
*/
|
|
83
|
+
function parseJsonFromStdout(stdout: string): any {
|
|
84
|
+
if (!stdout) return null;
|
|
85
|
+
|
|
86
|
+
// Try to find JSON in the output (sometimes mixed with other text)
|
|
87
|
+
const lines = stdout.split('\n');
|
|
88
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
89
|
+
const line = lines[i]!.trim();
|
|
90
|
+
if (line.startsWith('{') && line.endsWith('}')) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(line);
|
|
93
|
+
} catch {
|
|
94
|
+
// Continue searching
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If no single-line JSON, try the whole stdout
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(stdout.trim());
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Execute cursor-agent command with timeout and better error handling
|
|
109
|
+
*/
|
|
110
|
+
async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat, taskName }: {
|
|
111
|
+
workspaceDir: string;
|
|
112
|
+
chatId: string;
|
|
113
|
+
prompt: string;
|
|
114
|
+
model?: string;
|
|
115
|
+
signalDir?: string;
|
|
116
|
+
timeout?: number;
|
|
117
|
+
enableIntervention?: boolean;
|
|
118
|
+
outputFormat?: 'json' | 'plain';
|
|
119
|
+
taskName?: string;
|
|
120
|
+
}): Promise<AgentSendResult> {
|
|
121
|
+
const timeoutMs = timeout || 10 * 60 * 1000; // 10 minutes default
|
|
122
|
+
const args = ['send', chatId, prompt];
|
|
123
|
+
|
|
124
|
+
if (model) {
|
|
125
|
+
args.push('--model', model);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (outputFormat === 'json') {
|
|
129
|
+
args.push('--format', 'json');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Add worktree context if provided
|
|
133
|
+
if (workspaceDir) {
|
|
134
|
+
args.push('--workspace', workspaceDir);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
logger.info(`Sending prompt to cursor-agent (timeout: ${Math.round(timeoutMs / 1000)}s)...`);
|
|
139
|
+
|
|
140
|
+
const child = spawn('cursor-agent', args, {
|
|
141
|
+
cwd: workspaceDir || process.cwd(),
|
|
142
|
+
stdio: enableIntervention ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
let fullStdout = '';
|
|
146
|
+
let fullStderr = '';
|
|
147
|
+
let timeoutHandle: NodeJS.Timeout;
|
|
148
|
+
let heartbeatInterval: NodeJS.Timeout | undefined;
|
|
149
|
+
let lastActivity = Date.now();
|
|
150
|
+
let bytesReceived = 0;
|
|
151
|
+
|
|
152
|
+
// Signal watching for intervention and timeout
|
|
153
|
+
let signalWatcher: fs.FSWatcher | null = null;
|
|
154
|
+
if (signalDir) {
|
|
155
|
+
if (!fs.existsSync(signalDir)) {
|
|
156
|
+
fs.mkdirSync(signalDir, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const interventionPath = path.join(signalDir, 'intervention.txt');
|
|
160
|
+
const timeoutPath = path.join(signalDir, 'timeout.txt');
|
|
161
|
+
|
|
162
|
+
// Watch for intervention or timeout signals from UI
|
|
163
|
+
signalWatcher = fs.watch(signalDir, (event, filename) => {
|
|
164
|
+
if (filename === 'intervention.txt' && fs.existsSync(interventionPath)) {
|
|
165
|
+
try {
|
|
166
|
+
const message = fs.readFileSync(interventionPath, 'utf8').trim();
|
|
167
|
+
if (message) {
|
|
168
|
+
logger.info(`👋 Human intervention received: ${message.substring(0, 50)}...`);
|
|
169
|
+
if (child.stdin && child.stdin.writable) {
|
|
170
|
+
child.stdin.write(message + '\n');
|
|
171
|
+
|
|
172
|
+
if (signalDir) {
|
|
173
|
+
const convoPath = path.join(signalDir, 'conversation.jsonl');
|
|
174
|
+
appendLog(convoPath, createConversationEntry('intervention', `[HUMAN INTERVENTION]: ${message}`, {
|
|
175
|
+
task: taskName || 'AGENT_TURN',
|
|
176
|
+
model: 'manual'
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
logger.warn(`Intervention requested but stdin not available: ${message}`);
|
|
181
|
+
}
|
|
182
|
+
fs.unlinkSync(interventionPath);
|
|
183
|
+
}
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
|
|
188
|
+
try {
|
|
189
|
+
const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
|
|
190
|
+
const newTimeoutMs = parseInt(newTimeoutStr);
|
|
191
|
+
if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
|
|
192
|
+
logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
|
|
193
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
194
|
+
const elapsed = Date.now() - startTime;
|
|
195
|
+
const remaining = Math.max(1000, newTimeoutMs - elapsed);
|
|
196
|
+
timeoutHandle = setTimeout(() => {
|
|
197
|
+
clearInterval(heartbeatInterval);
|
|
198
|
+
child.kill();
|
|
199
|
+
resolve({ ok: false, exitCode: -1, error: `cursor-agent timed out after updated limit.` });
|
|
200
|
+
}, remaining);
|
|
201
|
+
fs.unlinkSync(timeoutPath);
|
|
202
|
+
}
|
|
203
|
+
} catch {}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (child.stdout) {
|
|
209
|
+
child.stdout.on('data', (data) => {
|
|
210
|
+
fullStdout += data.toString();
|
|
211
|
+
bytesReceived += data.length;
|
|
212
|
+
process.stdout.write(data);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (child.stderr) {
|
|
217
|
+
child.stderr.on('data', (data) => {
|
|
218
|
+
fullStderr += data.toString();
|
|
219
|
+
process.stderr.write(data);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const startTime = Date.now();
|
|
224
|
+
timeoutHandle = setTimeout(() => {
|
|
225
|
+
clearInterval(heartbeatInterval);
|
|
226
|
+
child.kill();
|
|
227
|
+
resolve({
|
|
228
|
+
ok: false,
|
|
229
|
+
exitCode: -1,
|
|
230
|
+
error: `cursor-agent timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
|
|
231
|
+
});
|
|
232
|
+
}, timeoutMs);
|
|
233
|
+
|
|
234
|
+
child.on('close', (code) => {
|
|
235
|
+
clearTimeout(timeoutHandle);
|
|
236
|
+
clearInterval(heartbeatInterval);
|
|
237
|
+
if (signalWatcher) signalWatcher.close();
|
|
238
|
+
|
|
239
|
+
const json = parseJsonFromStdout(fullStdout);
|
|
240
|
+
|
|
241
|
+
if (code !== 0 || !json || json.type !== 'result') {
|
|
242
|
+
let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
|
|
243
|
+
resolve({ ok: false, exitCode: code ?? -1, error: errorMsg });
|
|
244
|
+
} else {
|
|
245
|
+
resolve({
|
|
246
|
+
ok: !json.is_error,
|
|
247
|
+
exitCode: code ?? 0,
|
|
248
|
+
sessionId: json.session_id || chatId,
|
|
249
|
+
resultText: json.result || '',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
child.on('error', (err) => {
|
|
255
|
+
clearTimeout(timeoutHandle);
|
|
256
|
+
if (signalWatcher) signalWatcher.close();
|
|
257
|
+
resolve({ ok: false, exitCode: -1, error: `Failed to start cursor-agent: ${err.message}` });
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Execute cursor-agent command with retries for transient errors
|
|
264
|
+
*/
|
|
265
|
+
export async function cursorAgentSend(options: {
|
|
266
|
+
workspaceDir: string;
|
|
267
|
+
chatId: string;
|
|
268
|
+
prompt: string;
|
|
269
|
+
model?: string;
|
|
270
|
+
signalDir?: string;
|
|
271
|
+
timeout?: number;
|
|
272
|
+
enableIntervention?: boolean;
|
|
273
|
+
outputFormat?: 'json' | 'plain';
|
|
274
|
+
taskName?: string;
|
|
275
|
+
}): Promise<AgentSendResult> {
|
|
276
|
+
const laneName = options.signalDir ? path.basename(path.dirname(options.signalDir)) : 'agent';
|
|
277
|
+
|
|
278
|
+
return withRetry(
|
|
279
|
+
laneName,
|
|
280
|
+
() => cursorAgentSendRaw(options),
|
|
281
|
+
(res) => ({ ok: res.ok, error: res.error }),
|
|
282
|
+
{ maxRetries: 3 }
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Extract dependency change request from agent response
|
|
288
|
+
*/
|
|
289
|
+
export function extractDependencyRequest(text: string): { required: boolean; plan?: DependencyRequestPlan; raw: string } {
|
|
290
|
+
const t = String(text || '');
|
|
291
|
+
const marker = 'DEPENDENCY_CHANGE_REQUIRED';
|
|
292
|
+
|
|
293
|
+
if (!t.includes(marker)) {
|
|
294
|
+
return { required: false, raw: t };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const after = t.split(marker).slice(1).join(marker);
|
|
298
|
+
const match = after.match(/\{[\s\S]*?\}/);
|
|
299
|
+
|
|
300
|
+
if (match) {
|
|
301
|
+
try {
|
|
302
|
+
return {
|
|
303
|
+
required: true,
|
|
304
|
+
plan: JSON.parse(match[0]!) as DependencyRequestPlan,
|
|
305
|
+
raw: t,
|
|
306
|
+
};
|
|
307
|
+
} catch {
|
|
308
|
+
return { required: true, raw: t };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { required: true, raw: t };
|
|
313
|
+
}
|
|
314
|
+
|