@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.
@@ -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
+ };