@litmers/cursorflow-orchestrator 0.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.
- package/CHANGELOG.md +78 -0
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/commands/cursorflow-clean.md +162 -0
- package/commands/cursorflow-init.md +67 -0
- package/commands/cursorflow-monitor.md +131 -0
- package/commands/cursorflow-prepare.md +134 -0
- package/commands/cursorflow-resume.md +181 -0
- package/commands/cursorflow-review.md +220 -0
- package/commands/cursorflow-run.md +129 -0
- package/package.json +52 -0
- package/scripts/postinstall.js +27 -0
- package/src/cli/clean.js +30 -0
- package/src/cli/index.js +93 -0
- package/src/cli/init.js +235 -0
- package/src/cli/monitor.js +29 -0
- package/src/cli/resume.js +31 -0
- package/src/cli/run.js +51 -0
- package/src/cli/setup-commands.js +210 -0
- package/src/core/orchestrator.js +185 -0
- package/src/core/reviewer.js +233 -0
- package/src/core/runner.js +343 -0
- package/src/utils/config.js +195 -0
- package/src/utils/cursor-agent.js +190 -0
- package/src/utils/git.js +311 -0
- package/src/utils/logger.js +178 -0
- package/src/utils/state.js +207 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Core Runner - Execute tasks sequentially in a lane
|
|
4
|
+
*
|
|
5
|
+
* Adapted from sequential-agent-runner.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawn } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const git = require('../utils/git');
|
|
13
|
+
const logger = require('../utils/logger');
|
|
14
|
+
const { ensureCursorAgent, checkCursorApiKey } = require('../utils/cursor-agent');
|
|
15
|
+
const { saveState, loadState, appendLog, createConversationEntry, createGitLogEntry } = require('../utils/state');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute cursor-agent command
|
|
19
|
+
*/
|
|
20
|
+
function cursorAgentCreateChat() {
|
|
21
|
+
const { execSync } = require('child_process');
|
|
22
|
+
const out = execSync('cursor-agent create-chat', {
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
stdio: 'pipe',
|
|
25
|
+
});
|
|
26
|
+
const lines = out.split('\n').filter(Boolean);
|
|
27
|
+
return lines[lines.length - 1] || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseJsonFromStdout(stdout) {
|
|
31
|
+
const text = String(stdout || '').trim();
|
|
32
|
+
if (!text) return null;
|
|
33
|
+
const lines = text.split('\n').filter(Boolean);
|
|
34
|
+
|
|
35
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
36
|
+
const line = lines[i].trim();
|
|
37
|
+
if (line.startsWith('{') && line.endsWith('}')) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(line);
|
|
40
|
+
} catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cursorAgentSend({ workspaceDir, chatId, prompt, model }) {
|
|
49
|
+
const { spawnSync } = require('child_process');
|
|
50
|
+
|
|
51
|
+
const args = [
|
|
52
|
+
'--print',
|
|
53
|
+
'--output-format', 'json',
|
|
54
|
+
'--workspace', workspaceDir,
|
|
55
|
+
...(model ? ['--model', model] : []),
|
|
56
|
+
'--resume', chatId,
|
|
57
|
+
prompt,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const res = spawnSync('cursor-agent', args, {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
stdio: 'pipe',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const json = parseJsonFromStdout(res.stdout);
|
|
66
|
+
|
|
67
|
+
if (res.status !== 0 || !json || json.type !== 'result') {
|
|
68
|
+
const msg = res.stderr?.trim() || res.stdout?.trim() || `exit=${res.status}`;
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
exitCode: res.status,
|
|
72
|
+
error: msg,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
ok: !json.is_error,
|
|
78
|
+
exitCode: res.status,
|
|
79
|
+
sessionId: json.session_id || chatId,
|
|
80
|
+
resultText: json.result || '',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Extract dependency change request from agent response
|
|
86
|
+
*/
|
|
87
|
+
function extractDependencyRequest(text) {
|
|
88
|
+
const t = String(text || '');
|
|
89
|
+
const marker = 'DEPENDENCY_CHANGE_REQUIRED';
|
|
90
|
+
|
|
91
|
+
if (!t.includes(marker)) {
|
|
92
|
+
return { required: false };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const after = t.split(marker).slice(1).join(marker);
|
|
96
|
+
const match = after.match(/\{[\s\S]*?\}/);
|
|
97
|
+
|
|
98
|
+
if (match) {
|
|
99
|
+
try {
|
|
100
|
+
return {
|
|
101
|
+
required: true,
|
|
102
|
+
plan: JSON.parse(match[0]),
|
|
103
|
+
raw: t,
|
|
104
|
+
};
|
|
105
|
+
} catch {
|
|
106
|
+
return { required: true, raw: t };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { required: true, raw: t };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Wrap prompt with dependency policy
|
|
115
|
+
*/
|
|
116
|
+
function wrapPromptForDependencyPolicy(prompt, policy) {
|
|
117
|
+
if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
|
|
118
|
+
return prompt;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return `# Dependency Policy (MUST FOLLOW)
|
|
122
|
+
|
|
123
|
+
You are running in a restricted lane.
|
|
124
|
+
|
|
125
|
+
- allowDependencyChange: ${policy.allowDependencyChange}
|
|
126
|
+
- lockfileReadOnly: ${policy.lockfileReadOnly}
|
|
127
|
+
|
|
128
|
+
Rules:
|
|
129
|
+
- BEFORE making any code changes, decide whether dependency changes are required.
|
|
130
|
+
- If dependency changes are required, DO NOT change any files. Instead reply with:
|
|
131
|
+
|
|
132
|
+
DEPENDENCY_CHANGE_REQUIRED
|
|
133
|
+
\`\`\`json
|
|
134
|
+
{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }
|
|
135
|
+
\`\`\`
|
|
136
|
+
|
|
137
|
+
Then STOP.
|
|
138
|
+
- If dependency changes are NOT required, proceed normally.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
${prompt}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Apply file permissions based on dependency policy
|
|
147
|
+
*/
|
|
148
|
+
function applyDependencyFilePermissions(worktreeDir, policy) {
|
|
149
|
+
const targets = [];
|
|
150
|
+
|
|
151
|
+
if (!policy.allowDependencyChange) {
|
|
152
|
+
targets.push('package.json');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (policy.lockfileReadOnly) {
|
|
156
|
+
targets.push('pnpm-lock.yaml', 'package-lock.json', 'yarn.lock');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const file of targets) {
|
|
160
|
+
const filePath = path.join(worktreeDir, file);
|
|
161
|
+
if (!fs.existsSync(filePath)) continue;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const stats = fs.statSync(filePath);
|
|
165
|
+
const mode = stats.mode & 0o777;
|
|
166
|
+
fs.chmodSync(filePath, mode & ~0o222); // Remove write bits
|
|
167
|
+
} catch {
|
|
168
|
+
// Best effort
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Run a single task
|
|
175
|
+
*/
|
|
176
|
+
async function runTask({
|
|
177
|
+
task,
|
|
178
|
+
config,
|
|
179
|
+
index,
|
|
180
|
+
worktreeDir,
|
|
181
|
+
pipelineBranch,
|
|
182
|
+
taskBranch,
|
|
183
|
+
chatId,
|
|
184
|
+
runDir,
|
|
185
|
+
}) {
|
|
186
|
+
const model = task.model || config.model || 'sonnet-4.5';
|
|
187
|
+
const convoPath = path.join(runDir, 'conversation.jsonl');
|
|
188
|
+
|
|
189
|
+
logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
|
|
190
|
+
logger.info(`Model: ${model}`);
|
|
191
|
+
logger.info(`Branch: ${taskBranch}`);
|
|
192
|
+
|
|
193
|
+
// Checkout task branch
|
|
194
|
+
git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
|
|
195
|
+
|
|
196
|
+
// Apply dependency permissions
|
|
197
|
+
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
198
|
+
|
|
199
|
+
// Run prompt
|
|
200
|
+
const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy);
|
|
201
|
+
|
|
202
|
+
appendLog(convoPath, createConversationEntry('user', prompt1, {
|
|
203
|
+
task: task.name,
|
|
204
|
+
model,
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
logger.info('Sending prompt to agent...');
|
|
208
|
+
const r1 = cursorAgentSend({
|
|
209
|
+
workspaceDir: worktreeDir,
|
|
210
|
+
chatId,
|
|
211
|
+
prompt: prompt1,
|
|
212
|
+
model,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error, {
|
|
216
|
+
task: task.name,
|
|
217
|
+
model,
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
if (!r1.ok) {
|
|
221
|
+
return {
|
|
222
|
+
taskName: task.name,
|
|
223
|
+
taskBranch,
|
|
224
|
+
status: 'ERROR',
|
|
225
|
+
error: r1.error,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check for dependency request
|
|
230
|
+
const depReq = extractDependencyRequest(r1.resultText);
|
|
231
|
+
if (depReq.required && !config.dependencyPolicy.allowDependencyChange) {
|
|
232
|
+
return {
|
|
233
|
+
taskName: task.name,
|
|
234
|
+
taskBranch,
|
|
235
|
+
status: 'BLOCKED_DEPENDENCY',
|
|
236
|
+
dependencyRequest: depReq.plan || null,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Push task branch
|
|
241
|
+
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
taskName: task.name,
|
|
245
|
+
taskBranch,
|
|
246
|
+
status: 'FINISHED',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Run all tasks in sequence
|
|
252
|
+
*/
|
|
253
|
+
async function runTasks(config, runDir) {
|
|
254
|
+
ensureCursorAgent();
|
|
255
|
+
|
|
256
|
+
const repoRoot = git.getRepoRoot();
|
|
257
|
+
const pipelineBranch = config.pipelineBranch || `${config.branchPrefix}${Date.now().toString(36)}`;
|
|
258
|
+
const worktreeDir = path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
|
|
259
|
+
|
|
260
|
+
logger.section('🚀 Starting Pipeline');
|
|
261
|
+
logger.info(`Pipeline Branch: ${pipelineBranch}`);
|
|
262
|
+
logger.info(`Worktree: ${worktreeDir}`);
|
|
263
|
+
logger.info(`Tasks: ${config.tasks.length}`);
|
|
264
|
+
|
|
265
|
+
// Create worktree
|
|
266
|
+
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
267
|
+
baseBranch: config.baseBranch || 'main',
|
|
268
|
+
cwd: repoRoot,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Create chat
|
|
272
|
+
const chatId = cursorAgentCreateChat();
|
|
273
|
+
|
|
274
|
+
// Save initial state
|
|
275
|
+
const state = {
|
|
276
|
+
status: 'running',
|
|
277
|
+
pipelineBranch,
|
|
278
|
+
worktreeDir,
|
|
279
|
+
chatId,
|
|
280
|
+
totalTasks: config.tasks.length,
|
|
281
|
+
currentTaskIndex: 0,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
saveState(path.join(runDir, 'state.json'), state);
|
|
285
|
+
|
|
286
|
+
// Run tasks
|
|
287
|
+
const results = [];
|
|
288
|
+
|
|
289
|
+
for (let i = 0; i < config.tasks.length; i++) {
|
|
290
|
+
const task = config.tasks[i];
|
|
291
|
+
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
292
|
+
|
|
293
|
+
const result = await runTask({
|
|
294
|
+
task,
|
|
295
|
+
config,
|
|
296
|
+
index: i,
|
|
297
|
+
worktreeDir,
|
|
298
|
+
pipelineBranch,
|
|
299
|
+
taskBranch,
|
|
300
|
+
chatId,
|
|
301
|
+
runDir,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
results.push(result);
|
|
305
|
+
|
|
306
|
+
// Update state
|
|
307
|
+
state.currentTaskIndex = i + 1;
|
|
308
|
+
saveState(path.join(runDir, 'state.json'), state);
|
|
309
|
+
|
|
310
|
+
// Handle blocked or error
|
|
311
|
+
if (result.status === 'BLOCKED_DEPENDENCY') {
|
|
312
|
+
state.status = 'blocked_dependency';
|
|
313
|
+
state.dependencyRequest = result.dependencyRequest;
|
|
314
|
+
saveState(path.join(runDir, 'state.json'), state);
|
|
315
|
+
logger.warn('Task blocked on dependency change');
|
|
316
|
+
process.exit(2);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (result.status !== 'FINISHED') {
|
|
320
|
+
state.status = 'failed';
|
|
321
|
+
saveState(path.join(runDir, 'state.json'), state);
|
|
322
|
+
logger.error(`Task failed: ${result.error}`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Merge into pipeline
|
|
327
|
+
logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
|
|
328
|
+
git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
|
|
329
|
+
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Complete
|
|
333
|
+
state.status = 'completed';
|
|
334
|
+
saveState(path.join(runDir, 'state.json'), state);
|
|
335
|
+
|
|
336
|
+
logger.success('All tasks completed!');
|
|
337
|
+
return results;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
module.exports = {
|
|
341
|
+
runTasks,
|
|
342
|
+
runTask,
|
|
343
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Configuration loader for CursorFlow
|
|
4
|
+
*
|
|
5
|
+
* Finds project root and loads user configuration with defaults
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find project root by looking for package.json
|
|
13
|
+
*/
|
|
14
|
+
function findProjectRoot(cwd = process.cwd()) {
|
|
15
|
+
let current = cwd;
|
|
16
|
+
|
|
17
|
+
while (current !== path.parse(current).root) {
|
|
18
|
+
const packagePath = path.join(current, 'package.json');
|
|
19
|
+
if (fs.existsSync(packagePath)) {
|
|
20
|
+
return current;
|
|
21
|
+
}
|
|
22
|
+
current = path.dirname(current);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw new Error('Cannot find project root with package.json');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load configuration with defaults
|
|
30
|
+
*/
|
|
31
|
+
function loadConfig(projectRoot = null) {
|
|
32
|
+
if (!projectRoot) {
|
|
33
|
+
projectRoot = findProjectRoot();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const configPath = path.join(projectRoot, 'cursorflow.config.js');
|
|
37
|
+
|
|
38
|
+
// Default configuration
|
|
39
|
+
const defaults = {
|
|
40
|
+
// Directories
|
|
41
|
+
tasksDir: '_cursorflow/tasks',
|
|
42
|
+
logsDir: '_cursorflow/logs',
|
|
43
|
+
|
|
44
|
+
// Git
|
|
45
|
+
baseBranch: 'main',
|
|
46
|
+
branchPrefix: 'feature/',
|
|
47
|
+
|
|
48
|
+
// Execution
|
|
49
|
+
executor: 'cursor-agent', // 'cursor-agent' | 'cloud'
|
|
50
|
+
pollInterval: 60, // seconds
|
|
51
|
+
|
|
52
|
+
// Dependencies
|
|
53
|
+
allowDependencyChange: false,
|
|
54
|
+
lockfileReadOnly: true,
|
|
55
|
+
|
|
56
|
+
// Review
|
|
57
|
+
enableReview: false,
|
|
58
|
+
reviewModel: 'sonnet-4.5-thinking',
|
|
59
|
+
maxReviewIterations: 3,
|
|
60
|
+
|
|
61
|
+
// Lane defaults
|
|
62
|
+
defaultLaneConfig: {
|
|
63
|
+
devPort: 3001, // 3000 + laneNumber
|
|
64
|
+
autoCreatePr: false,
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Logging
|
|
68
|
+
logLevel: 'info', // 'error' | 'warn' | 'info' | 'debug'
|
|
69
|
+
verboseGit: false,
|
|
70
|
+
|
|
71
|
+
// Advanced
|
|
72
|
+
worktreePrefix: 'cursorflow-',
|
|
73
|
+
maxConcurrentLanes: 10,
|
|
74
|
+
|
|
75
|
+
// Internal
|
|
76
|
+
projectRoot,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Try to load user config
|
|
80
|
+
if (fs.existsSync(configPath)) {
|
|
81
|
+
try {
|
|
82
|
+
const userConfig = require(configPath);
|
|
83
|
+
return { ...defaults, ...userConfig, projectRoot };
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn(`Warning: Failed to load config from ${configPath}: ${error.message}`);
|
|
86
|
+
console.warn('Using default configuration...');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return defaults;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get absolute path for tasks directory
|
|
95
|
+
*/
|
|
96
|
+
function getTasksDir(config) {
|
|
97
|
+
return path.join(config.projectRoot, config.tasksDir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get absolute path for logs directory
|
|
102
|
+
*/
|
|
103
|
+
function getLogsDir(config) {
|
|
104
|
+
return path.join(config.projectRoot, config.logsDir);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Validate configuration
|
|
109
|
+
*/
|
|
110
|
+
function validateConfig(config) {
|
|
111
|
+
const errors = [];
|
|
112
|
+
|
|
113
|
+
if (!config.tasksDir) {
|
|
114
|
+
errors.push('tasksDir is required');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!config.logsDir) {
|
|
118
|
+
errors.push('logsDir is required');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!['cursor-agent', 'cloud'].includes(config.executor)) {
|
|
122
|
+
errors.push('executor must be "cursor-agent" or "cloud"');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (config.pollInterval < 1) {
|
|
126
|
+
errors.push('pollInterval must be >= 1');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (errors.length > 0) {
|
|
130
|
+
throw new Error(`Configuration validation failed:\n${errors.join('\n')}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create default config file
|
|
138
|
+
*/
|
|
139
|
+
function createDefaultConfig(projectRoot) {
|
|
140
|
+
const configPath = path.join(projectRoot, 'cursorflow.config.js');
|
|
141
|
+
|
|
142
|
+
if (fs.existsSync(configPath)) {
|
|
143
|
+
throw new Error(`Config file already exists: ${configPath}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const template = `module.exports = {
|
|
147
|
+
// Directory configuration
|
|
148
|
+
tasksDir: '_cursorflow/tasks',
|
|
149
|
+
logsDir: '_cursorflow/logs',
|
|
150
|
+
|
|
151
|
+
// Git configuration
|
|
152
|
+
baseBranch: 'main',
|
|
153
|
+
branchPrefix: 'feature/',
|
|
154
|
+
|
|
155
|
+
// Execution configuration
|
|
156
|
+
executor: 'cursor-agent', // 'cursor-agent' | 'cloud'
|
|
157
|
+
pollInterval: 60, // seconds
|
|
158
|
+
|
|
159
|
+
// Dependency management
|
|
160
|
+
allowDependencyChange: false,
|
|
161
|
+
lockfileReadOnly: true,
|
|
162
|
+
|
|
163
|
+
// Review configuration
|
|
164
|
+
enableReview: false,
|
|
165
|
+
reviewModel: 'sonnet-4.5-thinking',
|
|
166
|
+
maxReviewIterations: 3,
|
|
167
|
+
|
|
168
|
+
// Lane configuration
|
|
169
|
+
defaultLaneConfig: {
|
|
170
|
+
devPort: 3001, // 3000 + laneNumber
|
|
171
|
+
autoCreatePr: false,
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// Logging
|
|
175
|
+
logLevel: 'info', // 'error' | 'warn' | 'info' | 'debug'
|
|
176
|
+
verboseGit: false,
|
|
177
|
+
|
|
178
|
+
// Advanced
|
|
179
|
+
worktreePrefix: 'cursorflow-',
|
|
180
|
+
maxConcurrentLanes: 10,
|
|
181
|
+
};
|
|
182
|
+
`;
|
|
183
|
+
|
|
184
|
+
fs.writeFileSync(configPath, template, 'utf8');
|
|
185
|
+
return configPath;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
findProjectRoot,
|
|
190
|
+
loadConfig,
|
|
191
|
+
getTasksDir,
|
|
192
|
+
getLogsDir,
|
|
193
|
+
validateConfig,
|
|
194
|
+
createDefaultConfig,
|
|
195
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cursor Agent CLI wrapper and utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { execSync, spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if cursor-agent CLI is installed
|
|
10
|
+
*/
|
|
11
|
+
function checkCursorAgentInstalled() {
|
|
12
|
+
try {
|
|
13
|
+
execSync('cursor-agent --version', { stdio: 'pipe' });
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get cursor-agent version
|
|
22
|
+
*/
|
|
23
|
+
function getCursorAgentVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const version = execSync('cursor-agent --version', {
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
stdio: 'pipe',
|
|
28
|
+
}).trim();
|
|
29
|
+
return version;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ensure cursor-agent is installed, exit with error message if not
|
|
37
|
+
*/
|
|
38
|
+
function ensureCursorAgent() {
|
|
39
|
+
if (!checkCursorAgentInstalled()) {
|
|
40
|
+
console.error(`
|
|
41
|
+
❌ cursor-agent CLI is not installed
|
|
42
|
+
|
|
43
|
+
Installation:
|
|
44
|
+
npm install -g @cursor/agent
|
|
45
|
+
# or
|
|
46
|
+
pnpm add -g @cursor/agent
|
|
47
|
+
# or
|
|
48
|
+
yarn global add @cursor/agent
|
|
49
|
+
|
|
50
|
+
More info: https://docs.cursor.com/agent
|
|
51
|
+
`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Print installation guide
|
|
58
|
+
*/
|
|
59
|
+
function printInstallationGuide() {
|
|
60
|
+
console.log(`
|
|
61
|
+
📦 cursor-agent CLI Installation Guide
|
|
62
|
+
|
|
63
|
+
The cursor-agent CLI is required to run CursorFlow orchestration.
|
|
64
|
+
|
|
65
|
+
Installation methods:
|
|
66
|
+
|
|
67
|
+
npm install -g @cursor/agent
|
|
68
|
+
pnpm add -g @cursor/agent
|
|
69
|
+
yarn global add @cursor/agent
|
|
70
|
+
|
|
71
|
+
Verification:
|
|
72
|
+
|
|
73
|
+
cursor-agent --version
|
|
74
|
+
|
|
75
|
+
Documentation:
|
|
76
|
+
|
|
77
|
+
https://docs.cursor.com/agent
|
|
78
|
+
|
|
79
|
+
After installation, run your command again.
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if CURSOR_API_KEY is set (for cloud execution)
|
|
85
|
+
*/
|
|
86
|
+
function checkCursorApiKey() {
|
|
87
|
+
return !!process.env.CURSOR_API_KEY;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate cursor-agent setup for given executor type
|
|
92
|
+
*/
|
|
93
|
+
function validateSetup(executor = 'cursor-agent') {
|
|
94
|
+
const errors = [];
|
|
95
|
+
|
|
96
|
+
if (executor === 'cursor-agent') {
|
|
97
|
+
if (!checkCursorAgentInstalled()) {
|
|
98
|
+
errors.push('cursor-agent CLI is not installed');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (executor === 'cloud') {
|
|
103
|
+
if (!checkCursorApiKey()) {
|
|
104
|
+
errors.push('CURSOR_API_KEY environment variable is not set');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
valid: errors.length === 0,
|
|
110
|
+
errors,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get available models (if cursor-agent supports it)
|
|
116
|
+
*/
|
|
117
|
+
function getAvailableModels() {
|
|
118
|
+
try {
|
|
119
|
+
// This is a placeholder - actual implementation depends on cursor-agent API
|
|
120
|
+
const result = execSync('cursor-agent --model invalid "test"', {
|
|
121
|
+
encoding: 'utf8',
|
|
122
|
+
stdio: 'pipe',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Parse models from error message
|
|
126
|
+
// This is an example - actual parsing depends on cursor-agent output
|
|
127
|
+
return [];
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Parse from error message
|
|
130
|
+
const output = error.stderr || error.stdout || '';
|
|
131
|
+
// Extract model names from output
|
|
132
|
+
return parseModelsFromOutput(output);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse model names from cursor-agent output
|
|
138
|
+
*/
|
|
139
|
+
function parseModelsFromOutput(output) {
|
|
140
|
+
// This is a placeholder implementation
|
|
141
|
+
// Actual parsing depends on cursor-agent CLI output format
|
|
142
|
+
const models = [];
|
|
143
|
+
|
|
144
|
+
// Example parsing logic
|
|
145
|
+
const lines = output.split('\n');
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
if (line.includes('sonnet') || line.includes('opus') || line.includes('gpt')) {
|
|
148
|
+
const match = line.match(/['"]([^'"]+)['"]/);
|
|
149
|
+
if (match) {
|
|
150
|
+
models.push(match[1]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return models;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Test cursor-agent with a simple command
|
|
160
|
+
*/
|
|
161
|
+
function testCursorAgent() {
|
|
162
|
+
try {
|
|
163
|
+
const result = spawnSync('cursor-agent', ['--help'], {
|
|
164
|
+
encoding: 'utf8',
|
|
165
|
+
stdio: 'pipe',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
success: result.status === 0,
|
|
170
|
+
output: result.stdout,
|
|
171
|
+
error: result.stderr,
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
error: error.message,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
checkCursorAgentInstalled,
|
|
183
|
+
getCursorAgentVersion,
|
|
184
|
+
ensureCursorAgent,
|
|
185
|
+
printInstallationGuide,
|
|
186
|
+
checkCursorApiKey,
|
|
187
|
+
validateSetup,
|
|
188
|
+
getAvailableModels,
|
|
189
|
+
testCursorAgent,
|
|
190
|
+
};
|