@litmers/cursorflow-orchestrator 0.1.15 → 0.1.20
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 +23 -1
- package/README.md +26 -7
- package/commands/cursorflow-run.md +2 -0
- package/commands/cursorflow-triggers.md +250 -0
- package/dist/cli/clean.js +8 -7
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +20 -14
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +64 -47
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +27 -17
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +73 -33
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +193 -40
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +3 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +7 -7
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/orchestrator.d.ts +2 -1
- package/dist/core/orchestrator.js +54 -93
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +6 -4
- package/dist/core/reviewer.js +7 -5
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +8 -0
- package/dist/core/runner.js +219 -32
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +20 -10
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +35 -7
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +2 -2
- package/dist/utils/enhanced-logger.js +114 -43
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.js +163 -10
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/log-formatter.d.ts +16 -0
- package/dist/utils/log-formatter.js +194 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/repro-thinking-logs.d.ts +1 -0
- package/dist/utils/repro-thinking-logs.js +80 -0
- package/dist/utils/repro-thinking-logs.js.map +1 -0
- package/dist/utils/state.d.ts +4 -1
- package/dist/utils/state.js +11 -8
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +13 -0
- package/dist/utils/webhook.js +3 -0
- package/dist/utils/webhook.js.map +1 -1
- package/package.json +4 -2
- package/scripts/ai-security-check.js +3 -0
- package/scripts/local-security-gate.sh +9 -1
- package/scripts/verify-and-fix.sh +37 -0
- package/src/cli/clean.ts +8 -7
- package/src/cli/index.ts +5 -1
- package/src/cli/init.ts +19 -15
- package/src/cli/logs.ts +67 -47
- package/src/cli/monitor.ts +28 -18
- package/src/cli/prepare.ts +75 -35
- package/src/cli/resume.ts +810 -626
- package/src/cli/run.ts +3 -2
- package/src/cli/signal.ts +7 -6
- package/src/core/orchestrator.ts +68 -93
- package/src/core/reviewer.ts +14 -9
- package/src/core/runner.ts +229 -33
- package/src/utils/config.ts +19 -11
- package/src/utils/doctor.ts +38 -7
- package/src/utils/enhanced-logger.ts +117 -49
- package/src/utils/git.ts +145 -11
- package/src/utils/log-formatter.ts +162 -0
- package/src/utils/path.ts +45 -0
- package/src/utils/repro-thinking-logs.ts +54 -0
- package/src/utils/state.ts +16 -8
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +13 -0
- package/src/utils/webhook.ts +3 -0
- package/templates/basic.json +21 -0
- package/scripts/simple-logging-test.sh +0 -97
- package/scripts/test-real-cursor-lifecycle.sh +0 -289
- package/scripts/test-real-logging.sh +0 -289
- package/scripts/test-streaming-multi-task.sh +0 -247
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import {
|
|
16
|
+
import { Transform, TransformCallback } from 'stream';
|
|
17
17
|
import { EnhancedLogConfig } from './types';
|
|
18
|
+
import { safeJoin } from './path';
|
|
18
19
|
|
|
19
20
|
// Re-export for backwards compatibility
|
|
20
21
|
export { EnhancedLogConfig } from './types';
|
|
@@ -163,6 +164,23 @@ export class StreamingMessageParser {
|
|
|
163
164
|
},
|
|
164
165
|
});
|
|
165
166
|
break;
|
|
167
|
+
|
|
168
|
+
case 'thinking':
|
|
169
|
+
// Thinking message (Claude 3.7+ etc.)
|
|
170
|
+
if (json.subtype === 'delta' && json.text) {
|
|
171
|
+
// Check if this is a new message or continuation
|
|
172
|
+
if (this.currentRole !== 'thinking') {
|
|
173
|
+
// Flush previous message if any
|
|
174
|
+
this.flush();
|
|
175
|
+
this.currentRole = 'thinking';
|
|
176
|
+
this.messageStartTime = json.timestamp_ms || Date.now();
|
|
177
|
+
}
|
|
178
|
+
this.currentMessage += json.text;
|
|
179
|
+
} else if (json.subtype === 'completed') {
|
|
180
|
+
// Thinking completed - flush immediately
|
|
181
|
+
this.flush();
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
166
184
|
}
|
|
167
185
|
}
|
|
168
186
|
|
|
@@ -191,7 +209,7 @@ export class StreamingMessageParser {
|
|
|
191
209
|
}
|
|
192
210
|
|
|
193
211
|
export interface ParsedMessage {
|
|
194
|
-
type: 'system' | 'user' | 'assistant' | 'tool' | 'tool_result' | 'result';
|
|
212
|
+
type: 'system' | 'user' | 'assistant' | 'tool' | 'tool_result' | 'result' | 'thinking';
|
|
195
213
|
role: string;
|
|
196
214
|
content: string;
|
|
197
215
|
timestamp: number;
|
|
@@ -396,10 +414,10 @@ export class EnhancedLogManager {
|
|
|
396
414
|
fs.mkdirSync(logDir, { recursive: true });
|
|
397
415
|
|
|
398
416
|
// Set up log file paths
|
|
399
|
-
this.cleanLogPath =
|
|
400
|
-
this.rawLogPath =
|
|
401
|
-
this.jsonLogPath =
|
|
402
|
-
this.readableLogPath =
|
|
417
|
+
this.cleanLogPath = safeJoin(logDir, 'terminal.log');
|
|
418
|
+
this.rawLogPath = safeJoin(logDir, 'terminal-raw.log');
|
|
419
|
+
this.jsonLogPath = safeJoin(logDir, 'terminal.jsonl');
|
|
420
|
+
this.readableLogPath = safeJoin(logDir, 'terminal-readable.log');
|
|
403
421
|
|
|
404
422
|
// Initialize log files
|
|
405
423
|
this.initLogFiles();
|
|
@@ -505,6 +523,21 @@ export class EnhancedLogManager {
|
|
|
505
523
|
formatted = `[${ts}] 📄 RESL: ${msg.metadata?.toolName || 'Tool'}${toolResultLines}\n`;
|
|
506
524
|
break;
|
|
507
525
|
|
|
526
|
+
case 'thinking':
|
|
527
|
+
// Format thinking block
|
|
528
|
+
const thinkLabel = `[ 🤔 THINKING ] `;
|
|
529
|
+
const thinkWidth = 80;
|
|
530
|
+
const thinkTop = `┌─${thinkLabel}${'─'.repeat(Math.max(0, thinkWidth - thinkLabel.length - 2))}`;
|
|
531
|
+
const thinkBottom = `└─${'─'.repeat(thinkWidth - 2)}`;
|
|
532
|
+
|
|
533
|
+
const thinkLines = msg.content.trim().split('\n');
|
|
534
|
+
formatted = `[${ts}] ${thinkTop}\n`;
|
|
535
|
+
for (const line of thinkLines) {
|
|
536
|
+
formatted += `[${ts}] │ ${line}\n`;
|
|
537
|
+
}
|
|
538
|
+
formatted += `[${ts}] ${thinkBottom}\n`;
|
|
539
|
+
break;
|
|
540
|
+
|
|
508
541
|
default:
|
|
509
542
|
formatted = `[${ts}] ${msg.content}\n`;
|
|
510
543
|
}
|
|
@@ -588,8 +621,8 @@ export class EnhancedLogManager {
|
|
|
588
621
|
|
|
589
622
|
// Shift existing rotated files
|
|
590
623
|
for (let i = this.config.maxFiles - 1; i >= 1; i--) {
|
|
591
|
-
const oldPath =
|
|
592
|
-
const newPath =
|
|
624
|
+
const oldPath = safeJoin(dir, `${base}.${i}${ext}`);
|
|
625
|
+
const newPath = safeJoin(dir, `${base}.${i + 1}${ext}`);
|
|
593
626
|
|
|
594
627
|
if (fs.existsSync(oldPath)) {
|
|
595
628
|
if (i === this.config.maxFiles - 1) {
|
|
@@ -601,7 +634,7 @@ export class EnhancedLogManager {
|
|
|
601
634
|
}
|
|
602
635
|
|
|
603
636
|
// Rotate current to .1
|
|
604
|
-
const rotatedPath =
|
|
637
|
+
const rotatedPath = safeJoin(dir, `${base}.1${ext}`);
|
|
605
638
|
fs.renameSync(logPath, rotatedPath);
|
|
606
639
|
}
|
|
607
640
|
|
|
@@ -669,20 +702,73 @@ export class EnhancedLogManager {
|
|
|
669
702
|
this.cleanTransform.write(data);
|
|
670
703
|
}
|
|
671
704
|
|
|
672
|
-
//
|
|
673
|
-
this.
|
|
705
|
+
// Process lines for readable log and JSON entries
|
|
706
|
+
this.lineBuffer += text;
|
|
707
|
+
const lines = this.lineBuffer.split('\n');
|
|
708
|
+
this.lineBuffer = lines.pop() || '';
|
|
674
709
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
710
|
+
for (const line of lines) {
|
|
711
|
+
const cleanLine = stripAnsi(line).trim();
|
|
712
|
+
if (!cleanLine) continue;
|
|
713
|
+
|
|
714
|
+
// Handle streaming JSON messages (for boxes, etc. in readable log)
|
|
715
|
+
if (cleanLine.startsWith('{')) {
|
|
716
|
+
if (this.streamingParser) {
|
|
717
|
+
this.streamingParser.parseLine(cleanLine);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Special handling for terminal.jsonl entries for AI messages
|
|
721
|
+
if (this.config.writeJsonLog) {
|
|
722
|
+
try {
|
|
723
|
+
const json = JSON.parse(cleanLine);
|
|
724
|
+
let displayMsg = cleanLine;
|
|
725
|
+
let metadata = { ...json };
|
|
726
|
+
|
|
727
|
+
// Extract cleaner text for significant AI message types
|
|
728
|
+
if ((json.type === 'thinking' || json.type === 'thought') && (json.text || json.thought)) {
|
|
729
|
+
displayMsg = json.text || json.thought;
|
|
730
|
+
// Clean up any double newlines at the end of deltas
|
|
731
|
+
displayMsg = displayMsg.replace(/\n+$/, '\n');
|
|
732
|
+
} else if (json.type === 'assistant' && json.message?.content) {
|
|
733
|
+
displayMsg = json.message.content
|
|
734
|
+
.filter((c: any) => c.type === 'text')
|
|
735
|
+
.map((c: any) => c.text)
|
|
736
|
+
.join('');
|
|
737
|
+
} else if (json.type === 'user' && json.message?.content) {
|
|
738
|
+
displayMsg = json.message.content
|
|
739
|
+
.filter((c: any) => c.type === 'text')
|
|
740
|
+
.map((c: any) => c.text)
|
|
741
|
+
.join('');
|
|
742
|
+
} else if (json.type === 'tool_call' && json.subtype === 'started') {
|
|
743
|
+
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
744
|
+
const args = json.tool_call[toolName]?.args || {};
|
|
745
|
+
displayMsg = `🔧 CALL: ${toolName}(${JSON.stringify(args)})`;
|
|
746
|
+
} else if (json.type === 'tool_call' && json.subtype === 'completed') {
|
|
747
|
+
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
748
|
+
displayMsg = `📄 RESL: ${toolName}`;
|
|
749
|
+
} else if (json.type === 'result') {
|
|
750
|
+
displayMsg = json.result || 'Task completed';
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
this.writeJsonEntry({
|
|
754
|
+
timestamp: new Date().toISOString(),
|
|
755
|
+
level: 'stdout',
|
|
756
|
+
lane: this.session.laneName,
|
|
757
|
+
task: this.session.taskName,
|
|
758
|
+
message: displayMsg.substring(0, 2000), // Larger limit for AI text
|
|
759
|
+
metadata,
|
|
760
|
+
});
|
|
761
|
+
continue; // Already logged this JSON line
|
|
762
|
+
} catch {
|
|
763
|
+
// Not valid JSON or error, fall through to regular logging
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Also include significant info/status lines in readable log (compact)
|
|
769
|
+
if (this.readableLogFd !== null) {
|
|
680
770
|
// 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
|
-
|
|
771
|
+
if (!this.isNoiseLog(cleanLine) && /\[\d{4}-\d{2}-\d{2}T/.test(cleanLine)) {
|
|
686
772
|
try {
|
|
687
773
|
// Check if it has a level marker
|
|
688
774
|
if (/\[(INFO|WARN|ERROR|SUCCESS|DEBUG)\]/.test(cleanLine)) {
|
|
@@ -698,44 +784,26 @@ export class EnhancedLogManager {
|
|
|
698
784
|
} catch {}
|
|
699
785
|
}
|
|
700
786
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if (this.config.writeJsonLog) {
|
|
705
|
-
const cleanText = stripAnsi(text).trim();
|
|
706
|
-
if (cleanText && !this.isNoiseLog(cleanText)) {
|
|
787
|
+
|
|
788
|
+
// Write regular non-JSON lines to terminal.jsonl
|
|
789
|
+
if (this.config.writeJsonLog && !this.isNoiseLog(cleanLine)) {
|
|
707
790
|
this.writeJsonEntry({
|
|
708
791
|
timestamp: new Date().toISOString(),
|
|
709
792
|
level: 'stdout',
|
|
710
793
|
lane: this.session.laneName,
|
|
711
794
|
task: this.session.taskName,
|
|
712
|
-
message:
|
|
713
|
-
raw: this.config.keepRawLogs ? undefined :
|
|
795
|
+
message: cleanLine.substring(0, 1000),
|
|
796
|
+
raw: this.config.keepRawLogs ? undefined : line.substring(0, 1000),
|
|
714
797
|
});
|
|
715
798
|
}
|
|
716
799
|
}
|
|
717
800
|
}
|
|
718
801
|
|
|
719
802
|
/**
|
|
720
|
-
* Parse streaming JSON data for readable log
|
|
803
|
+
* Parse streaming JSON data for readable log - legacy, integrated into writeStdout
|
|
721
804
|
*/
|
|
722
805
|
private parseStreamingData(text: string): void {
|
|
723
|
-
if
|
|
724
|
-
|
|
725
|
-
// Buffer incomplete lines
|
|
726
|
-
this.lineBuffer += text;
|
|
727
|
-
const lines = this.lineBuffer.split('\n');
|
|
728
|
-
|
|
729
|
-
// Keep the last incomplete line in buffer
|
|
730
|
-
this.lineBuffer = lines.pop() || '';
|
|
731
|
-
|
|
732
|
-
// Parse complete lines
|
|
733
|
-
for (const line of lines) {
|
|
734
|
-
const trimmed = line.trim();
|
|
735
|
-
if (trimmed.startsWith('{')) {
|
|
736
|
-
this.streamingParser.parseLine(trimmed);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
806
|
+
// Legacy method, no longer used but kept for internal references if any
|
|
739
807
|
}
|
|
740
808
|
|
|
741
809
|
/**
|
|
@@ -873,7 +941,7 @@ export class EnhancedLogManager {
|
|
|
873
941
|
|
|
874
942
|
// Skip common progress/spinner patterns
|
|
875
943
|
const noisePatterns = [
|
|
876
|
-
/^[\s
|
|
944
|
+
/^[\s│├└─┌┐┘┴┬┤]+$/, // Box drawing only (removed duplicate ├)
|
|
877
945
|
/^[.\s]+$/, // Dots only
|
|
878
946
|
/^[=>\s-]+$/, // Progress bar characters
|
|
879
947
|
/^\d+%$/, // Percentage only
|
|
@@ -1036,8 +1104,8 @@ export function exportLogs(
|
|
|
1036
1104
|
format: 'text' | 'json' | 'markdown' | 'html',
|
|
1037
1105
|
outputPath?: string
|
|
1038
1106
|
): string {
|
|
1039
|
-
const cleanLogPath =
|
|
1040
|
-
const jsonLogPath =
|
|
1107
|
+
const cleanLogPath = safeJoin(laneRunDir, 'terminal.log');
|
|
1108
|
+
const jsonLogPath = safeJoin(laneRunDir, 'terminal.jsonl');
|
|
1041
1109
|
|
|
1042
1110
|
let output = '';
|
|
1043
1111
|
|
package/src/utils/git.ts
CHANGED
|
@@ -3,6 +3,77 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { execSync, spawnSync } from 'child_process';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { safeJoin } from './path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Acquire a file-based lock for Git operations
|
|
12
|
+
*/
|
|
13
|
+
function acquireLock(lockName: string, cwd?: string): string | null {
|
|
14
|
+
const repoRoot = cwd || getRepoRoot();
|
|
15
|
+
const lockDir = safeJoin(repoRoot, '_cursorflow', 'locks');
|
|
16
|
+
if (!fs.existsSync(lockDir)) {
|
|
17
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lockFile = safeJoin(lockDir, `${lockName}.lock`);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// wx flag ensures atomic creation
|
|
24
|
+
fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx' });
|
|
25
|
+
return lockFile;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Release a file-based lock
|
|
33
|
+
*/
|
|
34
|
+
function releaseLock(lockFile: string | null): void {
|
|
35
|
+
if (lockFile && fs.existsSync(lockFile)) {
|
|
36
|
+
try {
|
|
37
|
+
fs.unlinkSync(lockFile);
|
|
38
|
+
} catch {
|
|
39
|
+
// Ignore
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run Git command with locking
|
|
46
|
+
*/
|
|
47
|
+
async function runGitWithLock<T>(
|
|
48
|
+
lockName: string,
|
|
49
|
+
fn: () => T,
|
|
50
|
+
options: { cwd?: string; maxRetries?: number; retryDelay?: number } = {}
|
|
51
|
+
): Promise<T> {
|
|
52
|
+
const maxRetries = options.maxRetries ?? 10;
|
|
53
|
+
const retryDelay = options.retryDelay ?? 500;
|
|
54
|
+
|
|
55
|
+
let retries = 0;
|
|
56
|
+
let lockFile = null;
|
|
57
|
+
|
|
58
|
+
while (retries < maxRetries) {
|
|
59
|
+
lockFile = acquireLock(lockName, options.cwd);
|
|
60
|
+
if (lockFile) break;
|
|
61
|
+
|
|
62
|
+
retries++;
|
|
63
|
+
const delay = Math.floor(Math.random() * retryDelay) + retryDelay / 2;
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!lockFile) {
|
|
68
|
+
throw new Error(`Failed to acquire lock: ${lockName}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
return fn();
|
|
73
|
+
} finally {
|
|
74
|
+
releaseLock(lockFile);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
6
77
|
|
|
7
78
|
export interface GitRunOptions {
|
|
8
79
|
cwd?: string;
|
|
@@ -36,6 +107,25 @@ export interface CommitInfo {
|
|
|
36
107
|
subject: string;
|
|
37
108
|
}
|
|
38
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Filter out noisy git stderr messages
|
|
112
|
+
*/
|
|
113
|
+
function filterGitStderr(stderr: string): string {
|
|
114
|
+
if (!stderr) return '';
|
|
115
|
+
|
|
116
|
+
const lines = stderr.split('\n');
|
|
117
|
+
const filtered = lines.filter(line => {
|
|
118
|
+
// GitHub noise
|
|
119
|
+
if (line.includes('remote: Create a pull request')) return false;
|
|
120
|
+
if (line.trim().startsWith('remote:') && line.includes('pull/new')) return false;
|
|
121
|
+
if (line.trim() === 'remote:') return false; // Empty remote lines
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return filtered.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
39
129
|
/**
|
|
40
130
|
* Run git command and return output
|
|
41
131
|
*/
|
|
@@ -43,12 +133,21 @@ export function runGit(args: string[], options: GitRunOptions = {}): string {
|
|
|
43
133
|
const { cwd, silent = false } = options;
|
|
44
134
|
|
|
45
135
|
try {
|
|
136
|
+
const stdioMode = silent ? 'pipe' : ['inherit', 'inherit', 'pipe'];
|
|
137
|
+
|
|
46
138
|
const result = spawnSync('git', args, {
|
|
47
139
|
cwd: cwd || process.cwd(),
|
|
48
140
|
encoding: 'utf8',
|
|
49
|
-
stdio:
|
|
141
|
+
stdio: stdioMode as any,
|
|
50
142
|
});
|
|
51
143
|
|
|
144
|
+
if (!silent && result.stderr) {
|
|
145
|
+
const filteredStderr = filterGitStderr(result.stderr);
|
|
146
|
+
if (filteredStderr) {
|
|
147
|
+
process.stderr.write(filteredStderr);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
52
151
|
if (result.status !== 0 && !silent) {
|
|
53
152
|
throw new Error(`Git command failed: git ${args.join(' ')}\n${result.stderr || ''}`);
|
|
54
153
|
}
|
|
@@ -120,18 +219,53 @@ export function worktreeExists(worktreePath: string, cwd?: string): boolean {
|
|
|
120
219
|
export function createWorktree(worktreePath: string, branchName: string, options: { cwd?: string; baseBranch?: string } = {}): string {
|
|
121
220
|
const { cwd, baseBranch = 'main' } = options;
|
|
122
221
|
|
|
123
|
-
//
|
|
124
|
-
const
|
|
222
|
+
// Use a file-based lock to prevent race conditions during worktree creation
|
|
223
|
+
const lockDir = safeJoin(cwd || getRepoRoot(), '_cursorflow', 'locks');
|
|
224
|
+
if (!fs.existsSync(lockDir)) {
|
|
225
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
const lockFile = safeJoin(lockDir, 'worktree.lock');
|
|
228
|
+
|
|
229
|
+
let retries = 20;
|
|
230
|
+
let acquired = false;
|
|
231
|
+
|
|
232
|
+
while (retries > 0 && !acquired) {
|
|
233
|
+
try {
|
|
234
|
+
fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx' });
|
|
235
|
+
acquired = true;
|
|
236
|
+
} catch {
|
|
237
|
+
retries--;
|
|
238
|
+
const delay = Math.floor(Math.random() * 500) + 200;
|
|
239
|
+
// Use synchronous sleep to keep the function signature synchronous
|
|
240
|
+
const end = Date.now() + delay;
|
|
241
|
+
while (Date.now() < end) { /* wait */ }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
125
244
|
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
runGit(['worktree', 'add', worktreePath, branchName], { cwd });
|
|
129
|
-
} else {
|
|
130
|
-
// Create new branch from base
|
|
131
|
-
runGit(['worktree', 'add', '-b', branchName, worktreePath, baseBranch], { cwd });
|
|
245
|
+
if (!acquired) {
|
|
246
|
+
throw new Error('Failed to acquire worktree lock after multiple retries');
|
|
132
247
|
}
|
|
133
248
|
|
|
134
|
-
|
|
249
|
+
try {
|
|
250
|
+
// Check if branch already exists
|
|
251
|
+
const branchExists = runGitResult(['rev-parse', '--verify', branchName], { cwd }).success;
|
|
252
|
+
|
|
253
|
+
if (branchExists) {
|
|
254
|
+
// Branch exists, checkout to worktree
|
|
255
|
+
runGit(['worktree', 'add', worktreePath, branchName], { cwd });
|
|
256
|
+
} else {
|
|
257
|
+
// Create new branch from base
|
|
258
|
+
runGit(['worktree', 'add', '-b', branchName, worktreePath, baseBranch], { cwd });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return worktreePath;
|
|
262
|
+
} finally {
|
|
263
|
+
try {
|
|
264
|
+
fs.unlinkSync(lockFile);
|
|
265
|
+
} catch {
|
|
266
|
+
// Ignore
|
|
267
|
+
}
|
|
268
|
+
}
|
|
135
269
|
}
|
|
136
270
|
|
|
137
271
|
/**
|
|
@@ -362,4 +496,4 @@ export function getLastOperationStats(cwd?: string): string {
|
|
|
362
496
|
} catch (e) {
|
|
363
497
|
return '';
|
|
364
498
|
}
|
|
365
|
-
}
|
|
499
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for formatting log messages for console display
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as logger from './logger';
|
|
6
|
+
import { ParsedMessage, stripAnsi } from './enhanced-logger';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format a single parsed message into a human-readable string (compact or multi-line)
|
|
10
|
+
*/
|
|
11
|
+
export function formatMessageForConsole(
|
|
12
|
+
msg: ParsedMessage,
|
|
13
|
+
options: {
|
|
14
|
+
includeTimestamp?: boolean;
|
|
15
|
+
laneLabel?: string;
|
|
16
|
+
compact?: boolean;
|
|
17
|
+
} = {}
|
|
18
|
+
): string {
|
|
19
|
+
const { includeTimestamp = true, laneLabel = '', compact = false } = options;
|
|
20
|
+
const ts = includeTimestamp ? new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
|
|
21
|
+
const tsPrefix = ts ? `${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ` : '';
|
|
22
|
+
const labelPrefix = laneLabel ? `${logger.COLORS.magenta}${laneLabel.padEnd(12)}${logger.COLORS.reset} ` : '';
|
|
23
|
+
|
|
24
|
+
let typePrefix = '';
|
|
25
|
+
let content = msg.content;
|
|
26
|
+
|
|
27
|
+
switch (msg.type) {
|
|
28
|
+
case 'user':
|
|
29
|
+
typePrefix = `${logger.COLORS.cyan}🧑 USER${logger.COLORS.reset}`;
|
|
30
|
+
if (compact) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
|
|
31
|
+
break;
|
|
32
|
+
case 'assistant':
|
|
33
|
+
typePrefix = `${logger.COLORS.green}🤖 ASST${logger.COLORS.reset}`;
|
|
34
|
+
if (compact) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
|
|
35
|
+
break;
|
|
36
|
+
case 'tool':
|
|
37
|
+
typePrefix = `${logger.COLORS.yellow}🔧 TOOL${logger.COLORS.reset}`;
|
|
38
|
+
const toolMatch = content.match(/\[Tool: ([^\]]+)\] (.*)/);
|
|
39
|
+
if (toolMatch) {
|
|
40
|
+
const [, name, args] = toolMatch;
|
|
41
|
+
try {
|
|
42
|
+
const parsedArgs = JSON.parse(args!);
|
|
43
|
+
let argStr = '';
|
|
44
|
+
if (name === 'read_file' && parsedArgs.target_file) {
|
|
45
|
+
argStr = parsedArgs.target_file;
|
|
46
|
+
} else if (name === 'run_terminal_cmd' && parsedArgs.command) {
|
|
47
|
+
argStr = parsedArgs.command;
|
|
48
|
+
} else if (name === 'write' && parsedArgs.file_path) {
|
|
49
|
+
argStr = parsedArgs.file_path;
|
|
50
|
+
} else if (name === 'search_replace' && parsedArgs.file_path) {
|
|
51
|
+
argStr = parsedArgs.file_path;
|
|
52
|
+
} else {
|
|
53
|
+
const keys = Object.keys(parsedArgs);
|
|
54
|
+
if (keys.length > 0) {
|
|
55
|
+
argStr = String(parsedArgs[keys[0]]).substring(0, 50);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}(${argStr})`;
|
|
59
|
+
} catch {
|
|
60
|
+
content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}: ${args}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case 'tool_result':
|
|
65
|
+
typePrefix = `${logger.COLORS.gray}📄 RESL${logger.COLORS.reset}`;
|
|
66
|
+
const resMatch = content.match(/\[Tool Result: ([^\]]+)\]/);
|
|
67
|
+
content = resMatch ? `${resMatch[1]} OK` : 'result';
|
|
68
|
+
break;
|
|
69
|
+
case 'result':
|
|
70
|
+
typePrefix = `${logger.COLORS.green}✅ DONE${logger.COLORS.reset}`;
|
|
71
|
+
break;
|
|
72
|
+
case 'system':
|
|
73
|
+
typePrefix = `${logger.COLORS.gray}⚙️ SYS${logger.COLORS.reset}`;
|
|
74
|
+
break;
|
|
75
|
+
case 'thinking':
|
|
76
|
+
typePrefix = `${logger.COLORS.gray}🤔 THNK${logger.COLORS.reset}`;
|
|
77
|
+
if (compact) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!typePrefix) return `${tsPrefix}${labelPrefix}${content}`;
|
|
82
|
+
|
|
83
|
+
if (compact) {
|
|
84
|
+
return `${tsPrefix}${labelPrefix}${typePrefix} ${content}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Multi-line box format (as seen in orchestrator)
|
|
88
|
+
const lines = content.split('\n');
|
|
89
|
+
const fullPrefix = `${tsPrefix}${labelPrefix}`;
|
|
90
|
+
const header = `${typePrefix} ┌${'─'.repeat(60)}`;
|
|
91
|
+
let result = `${fullPrefix}${header}\n`;
|
|
92
|
+
|
|
93
|
+
const indent = ' '.repeat(stripAnsi(typePrefix).length);
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
result += `${fullPrefix}${indent} │ ${line}\n`;
|
|
96
|
+
}
|
|
97
|
+
result += `${fullPrefix}${indent} └${'─'.repeat(60)}`;
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Detect and format a message that might be a raw JSON string from cursor-agent
|
|
104
|
+
*/
|
|
105
|
+
export function formatPotentialJsonMessage(message: string): string {
|
|
106
|
+
const trimmed = message.trim();
|
|
107
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
108
|
+
return message;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const json = JSON.parse(trimmed);
|
|
113
|
+
if (!json.type) return message;
|
|
114
|
+
|
|
115
|
+
// Convert JSON to a ParsedMessage-like structure for formatting
|
|
116
|
+
let content = trimmed;
|
|
117
|
+
let type = 'system';
|
|
118
|
+
|
|
119
|
+
if (json.type === 'thinking' && json.text) {
|
|
120
|
+
content = json.text;
|
|
121
|
+
type = 'thinking';
|
|
122
|
+
} else if (json.type === 'assistant' && json.message?.content) {
|
|
123
|
+
content = json.message.content
|
|
124
|
+
.filter((c: any) => c.type === 'text')
|
|
125
|
+
.map((c: any) => c.text)
|
|
126
|
+
.join('');
|
|
127
|
+
type = 'assistant';
|
|
128
|
+
} else if (json.type === 'user' && json.message?.content) {
|
|
129
|
+
content = json.message.content
|
|
130
|
+
.filter((c: any) => c.type === 'text')
|
|
131
|
+
.map((c: any) => c.text)
|
|
132
|
+
.join('');
|
|
133
|
+
type = 'user';
|
|
134
|
+
} else if (json.type === 'tool_call' && json.subtype === 'started') {
|
|
135
|
+
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
136
|
+
const args = json.tool_call[toolName]?.args || {};
|
|
137
|
+
content = `[Tool: ${toolName}] ${JSON.stringify(args)}`;
|
|
138
|
+
type = 'tool';
|
|
139
|
+
} else if (json.type === 'tool_call' && json.subtype === 'completed') {
|
|
140
|
+
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
141
|
+
content = `[Tool Result: ${toolName}]`;
|
|
142
|
+
type = 'tool_result';
|
|
143
|
+
} else if (json.type === 'result') {
|
|
144
|
+
content = json.result || 'Task completed';
|
|
145
|
+
type = 'result';
|
|
146
|
+
} else {
|
|
147
|
+
// Unknown type, return as is
|
|
148
|
+
return message;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return formatMessageForConsole({
|
|
152
|
+
type: type as any,
|
|
153
|
+
role: type,
|
|
154
|
+
content,
|
|
155
|
+
timestamp: json.timestamp_ms || Date.now()
|
|
156
|
+
}, { includeTimestamp: false, compact: true });
|
|
157
|
+
|
|
158
|
+
} catch {
|
|
159
|
+
return message;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures that a path is safe and stays within a base directory.
|
|
5
|
+
* Prevents path traversal attacks.
|
|
6
|
+
*/
|
|
7
|
+
export function isSafePath(baseDir: string, ...parts: string[]): boolean {
|
|
8
|
+
const joined = path.join(baseDir, ...parts); // nosemgrep
|
|
9
|
+
const resolvedBase = path.resolve(baseDir); // nosemgrep
|
|
10
|
+
const resolvedJoined = path.resolve(joined); // nosemgrep
|
|
11
|
+
|
|
12
|
+
return resolvedJoined.startsWith(resolvedBase);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Safely joins path parts and ensures the result is within the base directory.
|
|
17
|
+
* Throws an error if path traversal is detected.
|
|
18
|
+
*
|
|
19
|
+
* @param baseDir The base directory that the resulting path must be within
|
|
20
|
+
* @param parts Path parts to join
|
|
21
|
+
* @returns The joined path
|
|
22
|
+
* @throws Error if the resulting path is outside the base directory
|
|
23
|
+
*/
|
|
24
|
+
export function safeJoin(baseDir: string, ...parts: string[]): string {
|
|
25
|
+
const joined = path.join(baseDir, ...parts); // nosemgrep
|
|
26
|
+
const resolvedBase = path.resolve(baseDir); // nosemgrep
|
|
27
|
+
const resolvedJoined = path.resolve(joined); // nosemgrep
|
|
28
|
+
|
|
29
|
+
if (!resolvedJoined.startsWith(resolvedBase)) {
|
|
30
|
+
throw new Error(`Potential path traversal detected: ${joined} is outside of ${baseDir}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return joined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalizes a path and checks if it's absolute or relative to project root.
|
|
38
|
+
*/
|
|
39
|
+
export function normalizePath(p: string, projectRoot: string): string {
|
|
40
|
+
if (path.isAbsolute(p)) {
|
|
41
|
+
return path.normalize(p);
|
|
42
|
+
}
|
|
43
|
+
return path.join(projectRoot, p); // nosemgrep
|
|
44
|
+
}
|
|
45
|
+
|