@litmers/cursorflow-orchestrator 0.1.14 â 0.1.15
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 +3 -0
- package/dist/cli/logs.js +42 -29
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/run.js +7 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/core/orchestrator.d.ts +3 -1
- package/dist/core/orchestrator.js +147 -8
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +4 -2
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +9 -3
- package/dist/core/runner.js +160 -55
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +4 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +3 -2
- package/dist/utils/enhanced-logger.js +87 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +6 -0
- package/dist/utils/git.js +15 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +4 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/types.d.ts +10 -0
- package/package.json +2 -1
- package/scripts/patches/test-cursor-agent.js +1 -1
- package/scripts/test-real-cursor-lifecycle.sh +289 -0
- package/src/cli/logs.ts +43 -28
- package/src/cli/run.ts +8 -0
- package/src/core/orchestrator.ts +164 -8
- package/src/core/reviewer.ts +18 -4
- package/src/core/runner.ts +174 -57
- package/src/utils/config.ts +5 -1
- package/src/utils/enhanced-logger.ts +90 -21
- package/src/utils/git.ts +15 -0
- package/src/utils/logger.ts +4 -1
- package/src/utils/types.ts +10 -0
package/src/core/runner.ts
CHANGED
|
@@ -175,7 +175,7 @@ export function validateTaskConfig(config: RunnerConfig): void {
|
|
|
175
175
|
/**
|
|
176
176
|
* Execute cursor-agent command with streaming and better error handling
|
|
177
177
|
*/
|
|
178
|
-
export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention }: {
|
|
178
|
+
export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat }: {
|
|
179
179
|
workspaceDir: string;
|
|
180
180
|
chatId: string;
|
|
181
181
|
prompt: string;
|
|
@@ -184,11 +184,14 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
184
184
|
timeout?: number;
|
|
185
185
|
/** Enable stdin piping for intervention feature (may cause buffering issues on some systems) */
|
|
186
186
|
enableIntervention?: boolean;
|
|
187
|
+
/** Output format for cursor-agent (default: 'stream-json') */
|
|
188
|
+
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
187
189
|
}): Promise<AgentSendResult> {
|
|
188
190
|
// Use stream-json format for structured output with tool calls and results
|
|
191
|
+
const format = outputFormat || 'stream-json';
|
|
189
192
|
const args = [
|
|
190
193
|
'--print',
|
|
191
|
-
'--output-format',
|
|
194
|
+
'--output-format', format,
|
|
192
195
|
'--workspace', workspaceDir,
|
|
193
196
|
...(model ? ['--model', model] : []),
|
|
194
197
|
'--resume', chatId,
|
|
@@ -426,33 +429,31 @@ export function extractDependencyRequest(text: string): { required: boolean; pla
|
|
|
426
429
|
/**
|
|
427
430
|
* Wrap prompt with dependency policy
|
|
428
431
|
*/
|
|
429
|
-
export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy): string {
|
|
430
|
-
|
|
432
|
+
export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy, options: { noGit?: boolean } = {}): string {
|
|
433
|
+
const { noGit = false } = options;
|
|
434
|
+
|
|
435
|
+
if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
|
|
431
436
|
return prompt;
|
|
432
437
|
}
|
|
433
438
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
---
|
|
454
|
-
|
|
455
|
-
${prompt}`;
|
|
439
|
+
let rules = '# Dependency Policy (MUST FOLLOW)\n\nYou are running in a restricted lane.\n\n';
|
|
440
|
+
|
|
441
|
+
rules += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
442
|
+
rules += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
|
|
443
|
+
|
|
444
|
+
if (noGit) {
|
|
445
|
+
rules += '- NO_GIT_MODE: Git is disabled. DO NOT run any git commands (commit, push, etc.). Just edit files.\n';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
rules += '\nRules:\n';
|
|
449
|
+
rules += '- BEFORE making any code changes, decide whether dependency changes are required.\n';
|
|
450
|
+
rules += '- If dependency changes are required, DO NOT change any files. Instead reply with:\n\n';
|
|
451
|
+
rules += 'DEPENDENCY_CHANGE_REQUIRED\n';
|
|
452
|
+
rules += '```json\n{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }\n```\n\n';
|
|
453
|
+
rules += 'Then STOP.\n';
|
|
454
|
+
rules += '- If dependency changes are NOT required, proceed normally.\n';
|
|
455
|
+
|
|
456
|
+
return `${rules}\n---\n\n${prompt}`;
|
|
456
457
|
}
|
|
457
458
|
|
|
458
459
|
/**
|
|
@@ -491,9 +492,11 @@ export async function runTask({
|
|
|
491
492
|
config,
|
|
492
493
|
index,
|
|
493
494
|
worktreeDir,
|
|
495
|
+
pipelineBranch,
|
|
494
496
|
taskBranch,
|
|
495
497
|
chatId,
|
|
496
498
|
runDir,
|
|
499
|
+
noGit = false,
|
|
497
500
|
}: {
|
|
498
501
|
task: Task;
|
|
499
502
|
config: RunnerConfig;
|
|
@@ -503,13 +506,18 @@ export async function runTask({
|
|
|
503
506
|
taskBranch: string;
|
|
504
507
|
chatId: string;
|
|
505
508
|
runDir: string;
|
|
509
|
+
noGit?: boolean;
|
|
506
510
|
}): Promise<TaskExecutionResult> {
|
|
507
511
|
const model = task.model || config.model || 'sonnet-4.5';
|
|
508
512
|
const convoPath = path.join(runDir, 'conversation.jsonl');
|
|
509
513
|
|
|
510
514
|
logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
|
|
511
515
|
logger.info(`Model: ${model}`);
|
|
512
|
-
|
|
516
|
+
if (noGit) {
|
|
517
|
+
logger.info('đĢ noGit mode: skipping branch operations');
|
|
518
|
+
} else {
|
|
519
|
+
logger.info(`Branch: ${taskBranch}`);
|
|
520
|
+
}
|
|
513
521
|
|
|
514
522
|
events.emit('task.started', {
|
|
515
523
|
taskName: task.name,
|
|
@@ -517,14 +525,16 @@ export async function runTask({
|
|
|
517
525
|
index,
|
|
518
526
|
});
|
|
519
527
|
|
|
520
|
-
// Checkout task branch
|
|
521
|
-
|
|
528
|
+
// Checkout task branch (skip in noGit mode)
|
|
529
|
+
if (!noGit) {
|
|
530
|
+
git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
|
|
531
|
+
}
|
|
522
532
|
|
|
523
533
|
// Apply dependency permissions
|
|
524
534
|
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
525
535
|
|
|
526
536
|
// Run prompt
|
|
527
|
-
const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy);
|
|
537
|
+
const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
|
|
528
538
|
|
|
529
539
|
appendLog(convoPath, createConversationEntry('user', prompt1, {
|
|
530
540
|
task: task.name,
|
|
@@ -547,6 +557,7 @@ export async function runTask({
|
|
|
547
557
|
signalDir: runDir,
|
|
548
558
|
timeout: config.timeout,
|
|
549
559
|
enableIntervention: config.enableIntervention,
|
|
560
|
+
outputFormat: config.agentOutputFormat,
|
|
550
561
|
});
|
|
551
562
|
|
|
552
563
|
const duration = Date.now() - startTime;
|
|
@@ -588,8 +599,10 @@ export async function runTask({
|
|
|
588
599
|
};
|
|
589
600
|
}
|
|
590
601
|
|
|
591
|
-
// Push task branch
|
|
592
|
-
|
|
602
|
+
// Push task branch (skip in noGit mode)
|
|
603
|
+
if (!noGit) {
|
|
604
|
+
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
605
|
+
}
|
|
593
606
|
|
|
594
607
|
events.emit('task.completed', {
|
|
595
608
|
taskName: task.name,
|
|
@@ -607,8 +620,13 @@ export async function runTask({
|
|
|
607
620
|
/**
|
|
608
621
|
* Run all tasks in sequence
|
|
609
622
|
*/
|
|
610
|
-
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number } = {}): Promise<TaskExecutionResult[]> {
|
|
623
|
+
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean } = {}): Promise<TaskExecutionResult[]> {
|
|
611
624
|
const startIndex = options.startIndex || 0;
|
|
625
|
+
const noGit = options.noGit || config.noGit || false;
|
|
626
|
+
|
|
627
|
+
if (noGit) {
|
|
628
|
+
logger.info('đĢ Running in noGit mode - Git operations will be skipped');
|
|
629
|
+
}
|
|
612
630
|
|
|
613
631
|
// Validate configuration before starting
|
|
614
632
|
logger.info('Validating task configuration...');
|
|
@@ -648,7 +666,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
648
666
|
|
|
649
667
|
logger.success('â Cursor authentication OK');
|
|
650
668
|
|
|
651
|
-
|
|
669
|
+
// In noGit mode, we don't need repoRoot - use current directory
|
|
670
|
+
const repoRoot = noGit ? process.cwd() : git.getRepoRoot();
|
|
652
671
|
|
|
653
672
|
// Load existing state if resuming
|
|
654
673
|
const statePath = path.join(runDir, 'state.json');
|
|
@@ -659,7 +678,10 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
659
678
|
}
|
|
660
679
|
|
|
661
680
|
const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
|
|
662
|
-
|
|
681
|
+
// In noGit mode, use a simple local directory instead of worktree
|
|
682
|
+
const worktreeDir = state?.worktreeDir || (noGit
|
|
683
|
+
? path.join(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
|
|
684
|
+
: path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch));
|
|
663
685
|
|
|
664
686
|
if (startIndex === 0) {
|
|
665
687
|
logger.section('đ Starting Pipeline');
|
|
@@ -673,10 +695,16 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
673
695
|
|
|
674
696
|
// Create worktree only if starting fresh
|
|
675
697
|
if (startIndex === 0 || !fs.existsSync(worktreeDir)) {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
698
|
+
if (noGit) {
|
|
699
|
+
// In noGit mode, just create the directory
|
|
700
|
+
logger.info(`Creating work directory: ${worktreeDir}`);
|
|
701
|
+
fs.mkdirSync(worktreeDir, { recursive: true });
|
|
702
|
+
} else {
|
|
703
|
+
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
704
|
+
baseBranch: config.baseBranch || 'main',
|
|
705
|
+
cwd: repoRoot,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
680
708
|
}
|
|
681
709
|
|
|
682
710
|
// Create chat
|
|
@@ -708,8 +736,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
708
736
|
|
|
709
737
|
saveState(statePath, state);
|
|
710
738
|
|
|
711
|
-
// Merge dependencies if any
|
|
712
|
-
if (startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
739
|
+
// Merge dependencies if any (skip in noGit mode)
|
|
740
|
+
if (!noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
713
741
|
logger.section('đ Merging Dependencies');
|
|
714
742
|
|
|
715
743
|
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
@@ -756,6 +784,50 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
756
784
|
|
|
757
785
|
// Push the merged state
|
|
758
786
|
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
787
|
+
} else if (noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
788
|
+
logger.info('â ī¸ Dependencies specified but Git is disabled - copying files instead of merging');
|
|
789
|
+
|
|
790
|
+
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
791
|
+
const lanesRoot = path.dirname(runDir);
|
|
792
|
+
|
|
793
|
+
for (const depName of config.dependsOn) {
|
|
794
|
+
const depRunDir = path.join(lanesRoot, depName);
|
|
795
|
+
const depStatePath = path.join(depRunDir, 'state.json');
|
|
796
|
+
|
|
797
|
+
if (!fs.existsSync(depStatePath)) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
|
|
803
|
+
if (depState.worktreeDir && fs.existsSync(depState.worktreeDir)) {
|
|
804
|
+
logger.info(`Copying files from dependency ${depName}: ${depState.worktreeDir} â ${worktreeDir}`);
|
|
805
|
+
|
|
806
|
+
// Use a simple recursive copy (excluding Git and internal dirs)
|
|
807
|
+
const copyFiles = (src: string, dest: string) => {
|
|
808
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
809
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
810
|
+
|
|
811
|
+
for (const entry of entries) {
|
|
812
|
+
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
|
|
813
|
+
|
|
814
|
+
const srcPath = path.join(src, entry.name);
|
|
815
|
+
const destPath = path.join(dest, entry.name);
|
|
816
|
+
|
|
817
|
+
if (entry.isDirectory()) {
|
|
818
|
+
copyFiles(srcPath, destPath);
|
|
819
|
+
} else {
|
|
820
|
+
fs.copyFileSync(srcPath, destPath);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
copyFiles(depState.worktreeDir, worktreeDir);
|
|
826
|
+
}
|
|
827
|
+
} catch (e) {
|
|
828
|
+
logger.error(`Failed to copy dependency ${depName}: ${e}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
759
831
|
}
|
|
760
832
|
|
|
761
833
|
// Run tasks
|
|
@@ -774,6 +846,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
774
846
|
taskBranch,
|
|
775
847
|
chatId,
|
|
776
848
|
runDir,
|
|
849
|
+
noGit,
|
|
777
850
|
});
|
|
778
851
|
|
|
779
852
|
results.push(result);
|
|
@@ -807,17 +880,21 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
807
880
|
process.exit(1);
|
|
808
881
|
}
|
|
809
882
|
|
|
810
|
-
// Merge into pipeline
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
883
|
+
// Merge into pipeline (skip in noGit mode)
|
|
884
|
+
if (!noGit) {
|
|
885
|
+
logger.info(`Merging ${taskBranch} â ${pipelineBranch}`);
|
|
886
|
+
git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
|
|
887
|
+
|
|
888
|
+
// Log changed files
|
|
889
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
890
|
+
if (stats) {
|
|
891
|
+
logger.info('Changed files:\n' + stats);
|
|
892
|
+
}
|
|
819
893
|
|
|
820
|
-
|
|
894
|
+
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
895
|
+
} else {
|
|
896
|
+
logger.info(`â Task ${task.name} completed (noGit mode - no branch operations)`);
|
|
897
|
+
}
|
|
821
898
|
}
|
|
822
899
|
|
|
823
900
|
// Complete
|
|
@@ -825,6 +902,41 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
825
902
|
state.endTime = Date.now();
|
|
826
903
|
saveState(statePath, state);
|
|
827
904
|
|
|
905
|
+
// Log final file summary
|
|
906
|
+
if (noGit) {
|
|
907
|
+
const getFileSummary = (dir: string): { files: number; dirs: number } => {
|
|
908
|
+
let stats = { files: 0, dirs: 0 };
|
|
909
|
+
if (!fs.existsSync(dir)) return stats;
|
|
910
|
+
|
|
911
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
912
|
+
for (const entry of entries) {
|
|
913
|
+
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
|
|
914
|
+
|
|
915
|
+
if (entry.isDirectory()) {
|
|
916
|
+
stats.dirs++;
|
|
917
|
+
const sub = getFileSummary(path.join(dir, entry.name));
|
|
918
|
+
stats.files += sub.files;
|
|
919
|
+
stats.dirs += sub.dirs;
|
|
920
|
+
} else {
|
|
921
|
+
stats.files++;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return stats;
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
const summary = getFileSummary(worktreeDir);
|
|
928
|
+
logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
|
|
929
|
+
} else {
|
|
930
|
+
try {
|
|
931
|
+
const stats = git.runGit(['diff', '--stat', config.baseBranch || 'main', pipelineBranch], { cwd: repoRoot, silent: true });
|
|
932
|
+
if (stats) {
|
|
933
|
+
logger.info('Final Workspace Summary (Git):\n' + stats);
|
|
934
|
+
}
|
|
935
|
+
} catch (e) {
|
|
936
|
+
// Ignore
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
828
940
|
logger.success('All tasks completed!');
|
|
829
941
|
return results;
|
|
830
942
|
}
|
|
@@ -844,6 +956,7 @@ if (require.main === module) {
|
|
|
844
956
|
const runDirIdx = args.indexOf('--run-dir');
|
|
845
957
|
const startIdxIdx = args.indexOf('--start-index');
|
|
846
958
|
const pipelineBranchIdx = args.indexOf('--pipeline-branch');
|
|
959
|
+
const noGit = args.includes('--no-git');
|
|
847
960
|
|
|
848
961
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
|
|
849
962
|
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
@@ -856,9 +969,10 @@ if (require.main === module) {
|
|
|
856
969
|
|
|
857
970
|
events.setRunId(runId);
|
|
858
971
|
|
|
859
|
-
// Load global config
|
|
972
|
+
// Load global config for defaults and webhooks
|
|
973
|
+
let globalConfig;
|
|
860
974
|
try {
|
|
861
|
-
|
|
975
|
+
globalConfig = loadConfig();
|
|
862
976
|
if (globalConfig.webhooks) {
|
|
863
977
|
registerWebhooks(globalConfig.webhooks);
|
|
864
978
|
}
|
|
@@ -883,14 +997,17 @@ if (require.main === module) {
|
|
|
883
997
|
process.exit(1);
|
|
884
998
|
}
|
|
885
999
|
|
|
886
|
-
// Add
|
|
1000
|
+
// Add defaults from global config or hardcoded
|
|
887
1001
|
config.dependencyPolicy = config.dependencyPolicy || {
|
|
888
|
-
allowDependencyChange: false,
|
|
889
|
-
lockfileReadOnly: true,
|
|
1002
|
+
allowDependencyChange: globalConfig?.allowDependencyChange ?? false,
|
|
1003
|
+
lockfileReadOnly: globalConfig?.lockfileReadOnly ?? true,
|
|
890
1004
|
};
|
|
891
1005
|
|
|
1006
|
+
// Add agent output format default
|
|
1007
|
+
config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'stream-json';
|
|
1008
|
+
|
|
892
1009
|
// Run tasks
|
|
893
|
-
runTasks(tasksFile, config, runDir, { startIndex })
|
|
1010
|
+
runTasks(tasksFile, config, runDir, { startIndex, noGit })
|
|
894
1011
|
.then(() => {
|
|
895
1012
|
process.exit(0);
|
|
896
1013
|
})
|
package/src/utils/config.ts
CHANGED
|
@@ -17,7 +17,9 @@ export function findProjectRoot(cwd = process.cwd()): string {
|
|
|
17
17
|
|
|
18
18
|
while (current !== path.parse(current).root) {
|
|
19
19
|
const packagePath = path.join(current, 'package.json');
|
|
20
|
-
|
|
20
|
+
const configPath = path.join(current, 'cursorflow.config.js');
|
|
21
|
+
|
|
22
|
+
if (fs.existsSync(packagePath) || fs.existsSync(configPath)) {
|
|
21
23
|
return current;
|
|
22
24
|
}
|
|
23
25
|
current = path.dirname(current);
|
|
@@ -72,6 +74,7 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
|
|
|
72
74
|
// Advanced
|
|
73
75
|
worktreePrefix: 'cursorflow-',
|
|
74
76
|
maxConcurrentLanes: 10,
|
|
77
|
+
agentOutputFormat: 'stream-json',
|
|
75
78
|
|
|
76
79
|
// Webhooks
|
|
77
80
|
webhooks: [],
|
|
@@ -194,6 +197,7 @@ export function createDefaultConfig(projectRoot: string, force = false): string
|
|
|
194
197
|
// Advanced
|
|
195
198
|
worktreePrefix: 'cursorflow-',
|
|
196
199
|
maxConcurrentLanes: 10,
|
|
200
|
+
agentOutputFormat: 'stream-json', // 'stream-json' | 'json' | 'plain'
|
|
197
201
|
|
|
198
202
|
// Webhook configuration
|
|
199
203
|
// webhooks: [
|
|
@@ -384,11 +384,13 @@ export class EnhancedLogManager {
|
|
|
384
384
|
private cleanTransform: CleanLogTransform | null = null;
|
|
385
385
|
private streamingParser: StreamingMessageParser | null = null;
|
|
386
386
|
private lineBuffer: string = '';
|
|
387
|
+
private onParsedMessage?: (msg: ParsedMessage) => void;
|
|
387
388
|
|
|
388
|
-
constructor(logDir: string, session: LogSession, config: Partial<EnhancedLogConfig> = {}) {
|
|
389
|
+
constructor(logDir: string, session: LogSession, config: Partial<EnhancedLogConfig> = {}, onParsedMessage?: (msg: ParsedMessage) => void) {
|
|
389
390
|
this.config = { ...DEFAULT_LOG_CONFIG, ...config };
|
|
390
391
|
this.session = session;
|
|
391
392
|
this.logDir = logDir;
|
|
393
|
+
this.onParsedMessage = onParsedMessage;
|
|
392
394
|
|
|
393
395
|
// Ensure log directory exists
|
|
394
396
|
fs.mkdirSync(logDir, { recursive: true });
|
|
@@ -450,6 +452,9 @@ export class EnhancedLogManager {
|
|
|
450
452
|
// Create streaming parser for readable log
|
|
451
453
|
this.streamingParser = new StreamingMessageParser((msg) => {
|
|
452
454
|
this.writeReadableMessage(msg);
|
|
455
|
+
if (this.onParsedMessage) {
|
|
456
|
+
this.onParsedMessage(msg);
|
|
457
|
+
}
|
|
453
458
|
});
|
|
454
459
|
}
|
|
455
460
|
|
|
@@ -464,41 +469,44 @@ export class EnhancedLogManager {
|
|
|
464
469
|
|
|
465
470
|
switch (msg.type) {
|
|
466
471
|
case 'system':
|
|
467
|
-
formatted =
|
|
472
|
+
formatted = `[${ts}] âī¸ SYSTEM: ${msg.content}\n`;
|
|
468
473
|
break;
|
|
469
474
|
|
|
470
475
|
case 'user':
|
|
471
|
-
// Format user prompt nicely
|
|
472
|
-
const promptPreview = msg.content.length > 200
|
|
473
|
-
? msg.content.substring(0, 200) + '...'
|
|
474
|
-
: msg.content;
|
|
475
|
-
formatted = `\n${ts}\nââ đ§ USER âââââââââââââââââââââââââââââââââââââââââââââ\n${this.indentText(promptPreview, 'â ')}\nââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n`;
|
|
476
|
-
break;
|
|
477
|
-
|
|
478
476
|
case 'assistant':
|
|
479
477
|
case 'result':
|
|
480
|
-
// Format
|
|
478
|
+
// Format with brackets and line (compact)
|
|
479
|
+
const isUser = msg.type === 'user';
|
|
481
480
|
const isResult = msg.type === 'result';
|
|
482
|
-
const
|
|
481
|
+
const headerText = isUser ? 'đ§ USER' : isResult ? 'đ¤ ASSISTANT (Final)' : 'đ¤ ASSISTANT';
|
|
483
482
|
const duration = msg.metadata?.duration_ms
|
|
484
483
|
? ` (${Math.round(msg.metadata.duration_ms / 1000)}s)`
|
|
485
484
|
: '';
|
|
486
|
-
|
|
485
|
+
|
|
486
|
+
const label = `[ ${headerText}${duration} ] `;
|
|
487
|
+
const totalWidth = 80;
|
|
488
|
+
const topBorder = `ââ${label}${'â'.repeat(Math.max(0, totalWidth - label.length - 2))}`;
|
|
489
|
+
const bottomBorder = `ââ${'â'.repeat(totalWidth - 2)}`;
|
|
490
|
+
|
|
491
|
+
const lines = msg.content.split('\n');
|
|
492
|
+
formatted = `[${ts}] ${topBorder}\n`;
|
|
493
|
+
for (const line of lines) {
|
|
494
|
+
formatted += `[${ts}] â ${line}\n`;
|
|
495
|
+
}
|
|
496
|
+
formatted += `[${ts}] ${bottomBorder}\n`;
|
|
487
497
|
break;
|
|
488
498
|
|
|
489
499
|
case 'tool':
|
|
490
|
-
|
|
491
|
-
formatted = `${ts} đ§ ${msg.content}\n`;
|
|
500
|
+
formatted = `[${ts}] đ§ TOOL: ${msg.content}\n`;
|
|
492
501
|
break;
|
|
493
502
|
|
|
494
503
|
case 'tool_result':
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
formatted = `${ts} đ ${msg.metadata?.toolName || 'Tool'}${lines}\n`;
|
|
504
|
+
const toolResultLines = msg.metadata?.lines ? ` (${msg.metadata.lines} lines)` : '';
|
|
505
|
+
formatted = `[${ts}] đ RESL: ${msg.metadata?.toolName || 'Tool'}${toolResultLines}\n`;
|
|
498
506
|
break;
|
|
499
507
|
|
|
500
508
|
default:
|
|
501
|
-
formatted =
|
|
509
|
+
formatted = `[${ts}] ${msg.content}\n`;
|
|
502
510
|
}
|
|
503
511
|
|
|
504
512
|
try {
|
|
@@ -661,9 +669,37 @@ export class EnhancedLogManager {
|
|
|
661
669
|
this.cleanTransform.write(data);
|
|
662
670
|
}
|
|
663
671
|
|
|
664
|
-
// Parse streaming JSON for readable log
|
|
672
|
+
// Parse streaming JSON for readable log (handles boxes, messages, tool calls)
|
|
665
673
|
this.parseStreamingData(text);
|
|
666
674
|
|
|
675
|
+
// Also include significant info/status lines in readable log (compact)
|
|
676
|
+
if (this.readableLogFd !== null) {
|
|
677
|
+
const lines = text.split('\n');
|
|
678
|
+
for (const line of lines) {
|
|
679
|
+
const cleanLine = stripAnsi(line).trim();
|
|
680
|
+
// Look for log lines: [ISO_DATE] [LEVEL] ...
|
|
681
|
+
if (cleanLine &&
|
|
682
|
+
!cleanLine.startsWith('{') &&
|
|
683
|
+
!this.isNoiseLog(cleanLine) &&
|
|
684
|
+
/\[\d{4}-\d{2}-\d{2}T/.test(cleanLine)) {
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
// Check if it has a level marker
|
|
688
|
+
if (/\[(INFO|WARN|ERROR|SUCCESS|DEBUG)\]/.test(cleanLine)) {
|
|
689
|
+
// Special formatting for summary
|
|
690
|
+
if (cleanLine.includes('Final Workspace Summary')) {
|
|
691
|
+
const tsMatch = cleanLine.match(/\[(\d{4}-\d{2}-\d{2}T[^\]]+)\]/);
|
|
692
|
+
const ts = tsMatch ? tsMatch[1] : new Date().toISOString();
|
|
693
|
+
fs.writeSync(this.readableLogFd, `[${ts}] đ SUMMARY: ${cleanLine.split(']').slice(2).join(']').trim()}\n`);
|
|
694
|
+
} else {
|
|
695
|
+
fs.writeSync(this.readableLogFd, `${cleanLine}\n`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
} catch {}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
667
703
|
// Write JSON entry (for significant lines only)
|
|
668
704
|
if (this.config.writeJsonLog) {
|
|
669
705
|
const cleanText = stripAnsi(text).trim();
|
|
@@ -717,6 +753,20 @@ export class EnhancedLogManager {
|
|
|
717
753
|
if (this.cleanTransform) {
|
|
718
754
|
this.cleanTransform.write(data);
|
|
719
755
|
}
|
|
756
|
+
|
|
757
|
+
// Also include error lines in readable log (compact)
|
|
758
|
+
if (this.readableLogFd !== null) {
|
|
759
|
+
const lines = text.split('\n');
|
|
760
|
+
for (const line of lines) {
|
|
761
|
+
const cleanLine = stripAnsi(line).trim();
|
|
762
|
+
if (cleanLine && !this.isNoiseLog(cleanLine)) {
|
|
763
|
+
try {
|
|
764
|
+
const ts = new Date().toISOString();
|
|
765
|
+
fs.writeSync(this.readableLogFd, `[${ts}] â STDERR: ${cleanLine}\n`);
|
|
766
|
+
} catch {}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
720
770
|
|
|
721
771
|
// Write JSON entry
|
|
722
772
|
if (this.config.writeJsonLog) {
|
|
@@ -747,6 +797,15 @@ export class EnhancedLogManager {
|
|
|
747
797
|
this.writeToRawLog(line);
|
|
748
798
|
}
|
|
749
799
|
|
|
800
|
+
// Write to readable log (compact)
|
|
801
|
+
if (this.readableLogFd !== null) {
|
|
802
|
+
const typeLabel = level === 'error' ? 'â ERROR' : level === 'info' ? 'âšī¸ INFO' : 'đ DEBUG';
|
|
803
|
+
const formatted = `${new Date().toISOString()} ${typeLabel}: ${message}\n`;
|
|
804
|
+
try {
|
|
805
|
+
fs.writeSync(this.readableLogFd, formatted);
|
|
806
|
+
} catch {}
|
|
807
|
+
}
|
|
808
|
+
|
|
750
809
|
if (this.config.writeJsonLog) {
|
|
751
810
|
this.writeJsonEntry({
|
|
752
811
|
timestamp: new Date().toISOString(),
|
|
@@ -770,6 +829,15 @@ export class EnhancedLogManager {
|
|
|
770
829
|
if (this.config.keepRawLogs) {
|
|
771
830
|
this.writeToRawLog(line);
|
|
772
831
|
}
|
|
832
|
+
|
|
833
|
+
// Write to readable log (compact)
|
|
834
|
+
if (this.readableLogFd !== null) {
|
|
835
|
+
const ts = new Date().toISOString();
|
|
836
|
+
const formatted = `[${ts}] âââ ${title} ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n`;
|
|
837
|
+
try {
|
|
838
|
+
fs.writeSync(this.readableLogFd, formatted);
|
|
839
|
+
} catch {}
|
|
840
|
+
}
|
|
773
841
|
}
|
|
774
842
|
|
|
775
843
|
/**
|
|
@@ -922,7 +990,8 @@ export class EnhancedLogManager {
|
|
|
922
990
|
export function createLogManager(
|
|
923
991
|
laneRunDir: string,
|
|
924
992
|
laneName: string,
|
|
925
|
-
config?: Partial<EnhancedLogConfig
|
|
993
|
+
config?: Partial<EnhancedLogConfig>,
|
|
994
|
+
onParsedMessage?: (msg: ParsedMessage) => void
|
|
926
995
|
): EnhancedLogManager {
|
|
927
996
|
const session: LogSession = {
|
|
928
997
|
id: `${laneName}-${Date.now().toString(36)}`,
|
|
@@ -930,7 +999,7 @@ export function createLogManager(
|
|
|
930
999
|
startTime: Date.now(),
|
|
931
1000
|
};
|
|
932
1001
|
|
|
933
|
-
return new EnhancedLogManager(laneRunDir, session, config);
|
|
1002
|
+
return new EnhancedLogManager(laneRunDir, session, config, onParsedMessage);
|
|
934
1003
|
}
|
|
935
1004
|
|
|
936
1005
|
/**
|
package/src/utils/git.ts
CHANGED
|
@@ -217,12 +217,27 @@ export function commit(message: string, options: { cwd?: string; addAll?: boolea
|
|
|
217
217
|
runGit(['commit', '-m', message], { cwd });
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Check if a remote exists
|
|
222
|
+
*/
|
|
223
|
+
export function remoteExists(remoteName = 'origin', options: { cwd?: string } = {}): boolean {
|
|
224
|
+
const result = runGitResult(['remote'], { cwd: options.cwd });
|
|
225
|
+
if (!result.success) return false;
|
|
226
|
+
return result.stdout.split('\n').map(r => r.trim()).includes(remoteName);
|
|
227
|
+
}
|
|
228
|
+
|
|
220
229
|
/**
|
|
221
230
|
* Push to remote
|
|
222
231
|
*/
|
|
223
232
|
export function push(branchName: string, options: { cwd?: string; force?: boolean; setUpstream?: boolean } = {}): void {
|
|
224
233
|
const { cwd, force = false, setUpstream = false } = options;
|
|
225
234
|
|
|
235
|
+
// Check if origin exists before pushing
|
|
236
|
+
if (!remoteExists('origin', { cwd })) {
|
|
237
|
+
// If no origin, just skip pushing (useful for local tests)
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
226
241
|
const args = ['push'];
|
|
227
242
|
|
|
228
243
|
if (force) {
|
package/src/utils/logger.ts
CHANGED
|
@@ -16,7 +16,9 @@ export const COLORS = {
|
|
|
16
16
|
green: '\x1b[32m',
|
|
17
17
|
blue: '\x1b[34m',
|
|
18
18
|
cyan: '\x1b[36m',
|
|
19
|
+
magenta: '\x1b[35m',
|
|
19
20
|
gray: '\x1b[90m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
let currentLogLevel: number = LogLevel.info;
|
|
@@ -38,7 +40,8 @@ export function setLogLevel(level: string | number): void {
|
|
|
38
40
|
function formatMessage(level: string, message: string, emoji = ''): string {
|
|
39
41
|
const timestamp = new Date().toISOString();
|
|
40
42
|
const prefix = emoji ? `${emoji} ` : '';
|
|
41
|
-
|
|
43
|
+
const lines = String(message).split('\n');
|
|
44
|
+
return lines.map(line => `[${timestamp}] [${level.toUpperCase()}] ${prefix}${line}`).join('\n');
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
/**
|