@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26
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 +25 -0
- package/README.md +25 -7
- package/commands/cursorflow-clean.md +19 -0
- package/commands/cursorflow-runs.md +59 -0
- package/commands/cursorflow-stop.md +55 -0
- package/dist/cli/clean.js +178 -6
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +8 -7
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +126 -77
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -0
- package/dist/cli/monitor.js +1021 -202
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +39 -21
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +268 -163
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +11 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runs.d.ts +5 -0
- package/dist/cli/runs.js +214 -0
- package/dist/cli/runs.js.map +1 -0
- package/dist/cli/setup-commands.js +0 -0
- package/dist/cli/signal.js +8 -8
- package/dist/cli/signal.js.map +1 -1
- package/dist/cli/stop.d.ts +5 -0
- package/dist/cli/stop.js +215 -0
- package/dist/cli/stop.js.map +1 -0
- package/dist/cli/tasks.d.ts +10 -0
- package/dist/cli/tasks.js +165 -0
- package/dist/cli/tasks.js.map +1 -0
- package/dist/core/auto-recovery.d.ts +212 -0
- package/dist/core/auto-recovery.js +737 -0
- package/dist/core/auto-recovery.js.map +1 -0
- package/dist/core/failure-policy.d.ts +156 -0
- package/dist/core/failure-policy.js +488 -0
- package/dist/core/failure-policy.js.map +1 -0
- package/dist/core/orchestrator.d.ts +16 -2
- package/dist/core/orchestrator.js +439 -105
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +2 -0
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +33 -10
- package/dist/core/runner.js +374 -164
- package/dist/core/runner.js.map +1 -1
- package/dist/services/logging/buffer.d.ts +67 -0
- package/dist/services/logging/buffer.js +309 -0
- package/dist/services/logging/buffer.js.map +1 -0
- package/dist/services/logging/console.d.ts +89 -0
- package/dist/services/logging/console.js +169 -0
- package/dist/services/logging/console.js.map +1 -0
- package/dist/services/logging/file-writer.d.ts +71 -0
- package/dist/services/logging/file-writer.js +516 -0
- package/dist/services/logging/file-writer.js.map +1 -0
- package/dist/services/logging/formatter.d.ts +39 -0
- package/dist/services/logging/formatter.js +227 -0
- package/dist/services/logging/formatter.js.map +1 -0
- package/dist/services/logging/index.d.ts +11 -0
- package/dist/services/logging/index.js +30 -0
- package/dist/services/logging/index.js.map +1 -0
- package/dist/services/logging/parser.d.ts +31 -0
- package/dist/services/logging/parser.js +222 -0
- package/dist/services/logging/parser.js.map +1 -0
- package/dist/services/process/index.d.ts +59 -0
- package/dist/services/process/index.js +257 -0
- package/dist/services/process/index.js.map +1 -0
- package/dist/types/agent.d.ts +20 -0
- package/dist/types/agent.js +6 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/config.d.ts +65 -0
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/events.d.ts +125 -0
- package/dist/types/events.js +6 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.js +37 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/lane.d.ts +43 -0
- package/dist/types/lane.js +6 -0
- package/dist/types/lane.js.map +1 -0
- package/dist/types/logging.d.ts +71 -0
- package/dist/types/logging.js +16 -0
- package/dist/types/logging.js.map +1 -0
- package/dist/types/review.d.ts +17 -0
- package/dist/types/review.js +6 -0
- package/dist/types/review.js.map +1 -0
- package/dist/types/run.d.ts +32 -0
- package/dist/types/run.js +6 -0
- package/dist/types/run.js.map +1 -0
- package/dist/types/task.d.ts +71 -0
- package/dist/types/task.js +6 -0
- package/dist/types/task.js.map +1 -0
- package/dist/ui/components.d.ts +134 -0
- package/dist/ui/components.js +389 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/log-viewer.d.ts +49 -0
- package/dist/ui/log-viewer.js +449 -0
- package/dist/ui/log-viewer.js.map +1 -0
- package/dist/utils/checkpoint.d.ts +87 -0
- package/dist/utils/checkpoint.js +317 -0
- package/dist/utils/checkpoint.js.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.js +18 -8
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/cursor-agent.js.map +1 -1
- package/dist/utils/dependency.d.ts +74 -0
- package/dist/utils/dependency.js +420 -0
- package/dist/utils/dependency.js.map +1 -0
- package/dist/utils/doctor.js +17 -11
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +10 -33
- package/dist/utils/enhanced-logger.js +108 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +121 -0
- package/dist/utils/git.js +484 -11
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/health.d.ts +91 -0
- package/dist/utils/health.js +556 -0
- package/dist/utils/health.js.map +1 -0
- package/dist/utils/lock.d.ts +95 -0
- package/dist/utils/lock.js +332 -0
- package/dist/utils/lock.js.map +1 -0
- package/dist/utils/log-buffer.d.ts +17 -0
- package/dist/utils/log-buffer.js +14 -0
- package/dist/utils/log-buffer.js.map +1 -0
- package/dist/utils/log-constants.d.ts +23 -0
- package/dist/utils/log-constants.js +28 -0
- package/dist/utils/log-constants.js.map +1 -0
- package/dist/utils/log-formatter.d.ts +25 -0
- package/dist/utils/log-formatter.js +237 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/log-service.d.ts +19 -0
- package/dist/utils/log-service.js +47 -0
- package/dist/utils/log-service.js.map +1 -0
- package/dist/utils/logger.d.ts +46 -27
- package/dist/utils/logger.js +82 -60
- package/dist/utils/logger.js.map +1 -1
- 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/process-manager.d.ts +21 -0
- package/dist/utils/process-manager.js +138 -0
- package/dist/utils/process-manager.js.map +1 -0
- package/dist/utils/retry.d.ts +121 -0
- package/dist/utils/retry.js +374 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/run-service.d.ts +88 -0
- package/dist/utils/run-service.js +412 -0
- package/dist/utils/run-service.js.map +1 -0
- package/dist/utils/state.d.ts +62 -3
- package/dist/utils/state.js +317 -11
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +82 -0
- package/dist/utils/task-service.js +348 -0
- package/dist/utils/task-service.js.map +1 -0
- 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 +2 -271
- package/dist/utils/types.js +16 -0
- package/dist/utils/types.js.map +1 -1
- package/package.json +38 -23
- package/scripts/ai-security-check.js +0 -1
- package/scripts/local-security-gate.sh +0 -0
- package/scripts/monitor-lanes.sh +94 -0
- package/scripts/patches/test-cursor-agent.js +0 -1
- package/scripts/release.sh +0 -0
- package/scripts/setup-security.sh +0 -0
- package/scripts/stream-logs.sh +72 -0
- package/scripts/verify-and-fix.sh +0 -0
- package/src/cli/clean.ts +187 -6
- package/src/cli/index.ts +12 -1
- package/src/cli/init.ts +8 -7
- package/src/cli/logs.ts +124 -77
- package/src/cli/monitor.ts +1815 -898
- package/src/cli/prepare.ts +41 -21
- package/src/cli/resume.ts +753 -626
- package/src/cli/run.ts +12 -5
- package/src/cli/runs.ts +212 -0
- package/src/cli/setup-commands.ts +0 -0
- package/src/cli/signal.ts +8 -7
- package/src/cli/stop.ts +209 -0
- package/src/cli/tasks.ts +154 -0
- package/src/core/auto-recovery.ts +909 -0
- package/src/core/failure-policy.ts +592 -0
- package/src/core/orchestrator.ts +1131 -704
- package/src/core/reviewer.ts +4 -0
- package/src/core/runner.ts +444 -180
- package/src/services/logging/buffer.ts +326 -0
- package/src/services/logging/console.ts +193 -0
- package/src/services/logging/file-writer.ts +526 -0
- package/src/services/logging/formatter.ts +268 -0
- package/src/services/logging/index.ts +16 -0
- package/src/services/logging/parser.ts +232 -0
- package/src/services/process/index.ts +261 -0
- package/src/types/agent.ts +24 -0
- package/src/types/config.ts +79 -0
- package/src/types/events.ts +156 -0
- package/src/types/index.ts +29 -0
- package/src/types/lane.ts +56 -0
- package/src/types/logging.ts +96 -0
- package/src/types/review.ts +20 -0
- package/src/types/run.ts +37 -0
- package/src/types/task.ts +79 -0
- package/src/ui/components.ts +430 -0
- package/src/ui/log-viewer.ts +485 -0
- package/src/utils/checkpoint.ts +374 -0
- package/src/utils/config.ts +18 -8
- package/src/utils/cursor-agent.ts +1 -1
- package/src/utils/dependency.ts +482 -0
- package/src/utils/doctor.ts +18 -11
- package/src/utils/enhanced-logger.ts +122 -60
- package/src/utils/git.ts +517 -11
- package/src/utils/health.ts +596 -0
- package/src/utils/lock.ts +346 -0
- package/src/utils/log-buffer.ts +28 -0
- package/src/utils/log-constants.ts +26 -0
- package/src/utils/log-formatter.ts +245 -0
- package/src/utils/log-service.ts +49 -0
- package/src/utils/logger.ts +100 -51
- package/src/utils/path.ts +45 -0
- package/src/utils/process-manager.ts +100 -0
- package/src/utils/retry.ts +413 -0
- package/src/utils/run-service.ts +433 -0
- package/src/utils/state.ts +385 -11
- package/src/utils/task-service.ts +370 -0
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +2 -314
- package/templates/basic.json +21 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced file-based locking with async support and stale lock detection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { safeJoin } from './path';
|
|
8
|
+
|
|
9
|
+
export interface LockInfo {
|
|
10
|
+
pid: number;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
operation: string;
|
|
13
|
+
hostname?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LockOptions {
|
|
17
|
+
/** Maximum time to wait for lock in milliseconds */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
/** Delay between retry attempts in milliseconds */
|
|
20
|
+
retryDelayMs?: number;
|
|
21
|
+
/** Lock is considered stale after this many milliseconds */
|
|
22
|
+
staleTimeoutMs?: number;
|
|
23
|
+
/** Description of the operation for logging */
|
|
24
|
+
operation?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_LOCK_OPTIONS: Required<LockOptions> = {
|
|
28
|
+
timeoutMs: 30000,
|
|
29
|
+
retryDelayMs: 100,
|
|
30
|
+
staleTimeoutMs: 60000,
|
|
31
|
+
operation: 'unknown',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if a process is still running
|
|
36
|
+
*/
|
|
37
|
+
export function isProcessRunning(pid: number): boolean {
|
|
38
|
+
try {
|
|
39
|
+
// Sending signal 0 checks if process exists without actually signaling it
|
|
40
|
+
process.kill(pid, 0);
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the lock directory for a given base path
|
|
49
|
+
*/
|
|
50
|
+
export function getLockDir(basePath: string): string {
|
|
51
|
+
return safeJoin(basePath, '_cursorflow', 'locks');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Ensure lock directory exists
|
|
56
|
+
*/
|
|
57
|
+
export function ensureLockDir(basePath: string): string {
|
|
58
|
+
const lockDir = getLockDir(basePath);
|
|
59
|
+
if (!fs.existsSync(lockDir)) {
|
|
60
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
return lockDir;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read lock file info
|
|
67
|
+
*/
|
|
68
|
+
export function readLockInfo(lockFile: string): LockInfo | null {
|
|
69
|
+
try {
|
|
70
|
+
if (!fs.existsSync(lockFile)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const content = fs.readFileSync(lockFile, 'utf8');
|
|
74
|
+
return JSON.parse(content) as LockInfo;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if a lock is stale
|
|
82
|
+
*/
|
|
83
|
+
export function isLockStale(lockInfo: LockInfo, staleTimeoutMs: number): boolean {
|
|
84
|
+
// Check if process is dead
|
|
85
|
+
if (!isProcessRunning(lockInfo.pid)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if lock has expired
|
|
90
|
+
if (Date.now() - lockInfo.timestamp > staleTimeoutMs) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Try to acquire a lock synchronously (for backward compatibility)
|
|
99
|
+
*/
|
|
100
|
+
export function tryAcquireLockSync(lockFile: string, options: LockOptions = {}): boolean {
|
|
101
|
+
const opts = { ...DEFAULT_LOCK_OPTIONS, ...options };
|
|
102
|
+
|
|
103
|
+
// Check for existing lock
|
|
104
|
+
const existingLock = readLockInfo(lockFile);
|
|
105
|
+
if (existingLock) {
|
|
106
|
+
if (!isLockStale(existingLock, opts.staleTimeoutMs)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
// Stale lock - remove it
|
|
110
|
+
try {
|
|
111
|
+
fs.unlinkSync(lockFile);
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Try to create lock atomically
|
|
118
|
+
const lockInfo: LockInfo = {
|
|
119
|
+
pid: process.pid,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
operation: opts.operation,
|
|
122
|
+
hostname: require('os').hostname(),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
fs.writeFileSync(lockFile, JSON.stringify(lockInfo, null, 2), { flag: 'wx' });
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Release a lock synchronously
|
|
135
|
+
*/
|
|
136
|
+
export function releaseLockSync(lockFile: string): void {
|
|
137
|
+
try {
|
|
138
|
+
const lockInfo = readLockInfo(lockFile);
|
|
139
|
+
// Only release if we own the lock
|
|
140
|
+
if (lockInfo && lockInfo.pid === process.pid) {
|
|
141
|
+
fs.unlinkSync(lockFile);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Ignore errors
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Acquire a lock with async waiting
|
|
150
|
+
*/
|
|
151
|
+
export async function acquireLock(lockFile: string, options: LockOptions = {}): Promise<boolean> {
|
|
152
|
+
const opts = { ...DEFAULT_LOCK_OPTIONS, ...options };
|
|
153
|
+
const startTime = Date.now();
|
|
154
|
+
|
|
155
|
+
while (Date.now() - startTime < opts.timeoutMs) {
|
|
156
|
+
if (tryAcquireLockSync(lockFile, opts)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Wait before retrying with jitter
|
|
161
|
+
const jitter = Math.random() * opts.retryDelayMs * 0.5;
|
|
162
|
+
await new Promise(resolve => setTimeout(resolve, opts.retryDelayMs + jitter));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Release a lock
|
|
170
|
+
*/
|
|
171
|
+
export async function releaseLock(lockFile: string): Promise<void> {
|
|
172
|
+
releaseLockSync(lockFile);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Execute a function while holding a lock
|
|
177
|
+
*/
|
|
178
|
+
export async function withLock<T>(
|
|
179
|
+
lockFile: string,
|
|
180
|
+
fn: () => Promise<T>,
|
|
181
|
+
options: LockOptions = {}
|
|
182
|
+
): Promise<T> {
|
|
183
|
+
const acquired = await acquireLock(lockFile, options);
|
|
184
|
+
if (!acquired) {
|
|
185
|
+
throw new Error(`Failed to acquire lock: ${lockFile} (timeout: ${options.timeoutMs || DEFAULT_LOCK_OPTIONS.timeoutMs}ms)`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
return await fn();
|
|
190
|
+
} finally {
|
|
191
|
+
await releaseLock(lockFile);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Execute a synchronous function while holding a lock
|
|
197
|
+
*/
|
|
198
|
+
export function withLockSync<T>(
|
|
199
|
+
lockFile: string,
|
|
200
|
+
fn: () => T,
|
|
201
|
+
options: LockOptions = {}
|
|
202
|
+
): T {
|
|
203
|
+
const opts = { ...DEFAULT_LOCK_OPTIONS, ...options };
|
|
204
|
+
const startTime = Date.now();
|
|
205
|
+
|
|
206
|
+
// Busy wait for lock (synchronous)
|
|
207
|
+
while (Date.now() - startTime < opts.timeoutMs) {
|
|
208
|
+
if (tryAcquireLockSync(lockFile, opts)) {
|
|
209
|
+
try {
|
|
210
|
+
return fn();
|
|
211
|
+
} finally {
|
|
212
|
+
releaseLockSync(lockFile);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Sync sleep
|
|
217
|
+
const end = Date.now() + opts.retryDelayMs;
|
|
218
|
+
while (Date.now() < end) { /* busy wait */ }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
throw new Error(`Failed to acquire lock: ${lockFile} (timeout: ${opts.timeoutMs}ms)`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Clean up stale locks in a directory
|
|
226
|
+
*/
|
|
227
|
+
export function cleanStaleLocks(lockDir: string, staleTimeoutMs: number = DEFAULT_LOCK_OPTIONS.staleTimeoutMs): number {
|
|
228
|
+
if (!fs.existsSync(lockDir)) {
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let cleaned = 0;
|
|
233
|
+
const files = fs.readdirSync(lockDir);
|
|
234
|
+
|
|
235
|
+
for (const file of files) {
|
|
236
|
+
if (!file.endsWith('.lock')) continue;
|
|
237
|
+
|
|
238
|
+
const lockFile = safeJoin(lockDir, file);
|
|
239
|
+
const lockInfo = readLockInfo(lockFile);
|
|
240
|
+
|
|
241
|
+
if (lockInfo && isLockStale(lockInfo, staleTimeoutMs)) {
|
|
242
|
+
try {
|
|
243
|
+
fs.unlinkSync(lockFile);
|
|
244
|
+
cleaned++;
|
|
245
|
+
} catch {
|
|
246
|
+
// Ignore
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return cleaned;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get status of all locks in a directory
|
|
256
|
+
*/
|
|
257
|
+
export function getLockStatus(lockDir: string): Array<{ file: string; info: LockInfo; stale: boolean }> {
|
|
258
|
+
if (!fs.existsSync(lockDir)) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const result: Array<{ file: string; info: LockInfo; stale: boolean }> = [];
|
|
263
|
+
const files = fs.readdirSync(lockDir);
|
|
264
|
+
|
|
265
|
+
for (const file of files) {
|
|
266
|
+
if (!file.endsWith('.lock')) continue;
|
|
267
|
+
|
|
268
|
+
const lockFile = safeJoin(lockDir, file);
|
|
269
|
+
const lockInfo = readLockInfo(lockFile);
|
|
270
|
+
|
|
271
|
+
if (lockInfo) {
|
|
272
|
+
result.push({
|
|
273
|
+
file,
|
|
274
|
+
info: lockInfo,
|
|
275
|
+
stale: isLockStale(lockInfo, DEFAULT_LOCK_OPTIONS.staleTimeoutMs),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Named lock manager for managing multiple locks
|
|
285
|
+
*/
|
|
286
|
+
export class LockManager {
|
|
287
|
+
private readonly basePath: string;
|
|
288
|
+
private readonly heldLocks: Set<string> = new Set();
|
|
289
|
+
|
|
290
|
+
constructor(basePath: string) {
|
|
291
|
+
this.basePath = basePath;
|
|
292
|
+
ensureLockDir(basePath);
|
|
293
|
+
|
|
294
|
+
// Register cleanup on process exit
|
|
295
|
+
process.on('exit', () => this.releaseAll());
|
|
296
|
+
process.on('SIGINT', () => {
|
|
297
|
+
this.releaseAll();
|
|
298
|
+
process.exit(130);
|
|
299
|
+
});
|
|
300
|
+
process.on('SIGTERM', () => {
|
|
301
|
+
this.releaseAll();
|
|
302
|
+
process.exit(143);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private getLockPath(name: string): string {
|
|
307
|
+
return safeJoin(getLockDir(this.basePath), `${name}.lock`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async acquire(name: string, options: LockOptions = {}): Promise<boolean> {
|
|
311
|
+
const lockPath = this.getLockPath(name);
|
|
312
|
+
const acquired = await acquireLock(lockPath, options);
|
|
313
|
+
if (acquired) {
|
|
314
|
+
this.heldLocks.add(name);
|
|
315
|
+
}
|
|
316
|
+
return acquired;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async release(name: string): Promise<void> {
|
|
320
|
+
const lockPath = this.getLockPath(name);
|
|
321
|
+
await releaseLock(lockPath);
|
|
322
|
+
this.heldLocks.delete(name);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
releaseAll(): void {
|
|
326
|
+
for (const name of this.heldLocks) {
|
|
327
|
+
const lockPath = this.getLockPath(name);
|
|
328
|
+
releaseLockSync(lockPath);
|
|
329
|
+
}
|
|
330
|
+
this.heldLocks.clear();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async withLock<T>(name: string, fn: () => Promise<T>, options: LockOptions = {}): Promise<T> {
|
|
334
|
+
const lockPath = this.getLockPath(name);
|
|
335
|
+
return withLock(lockPath, fn, options);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
cleanStale(staleTimeoutMs?: number): number {
|
|
339
|
+
return cleanStaleLocks(getLockDir(this.basePath), staleTimeoutMs);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
getStatus(): Array<{ file: string; info: LockInfo; stale: boolean }> {
|
|
343
|
+
return getLockStatus(getLockDir(this.basePath));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogBufferService - Scrollable log buffer for TUI monitor
|
|
3
|
+
*
|
|
4
|
+
* This file is kept for backward compatibility.
|
|
5
|
+
* New code should import from '../services/logging/buffer' directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export everything from the new services/logging/buffer module
|
|
9
|
+
export {
|
|
10
|
+
LogBufferService,
|
|
11
|
+
createLogBuffer,
|
|
12
|
+
type LogBufferOptions,
|
|
13
|
+
type LogFilter,
|
|
14
|
+
type LogBufferState,
|
|
15
|
+
} from '../services/logging/buffer';
|
|
16
|
+
|
|
17
|
+
// Re-export types from types/logging for backward compatibility
|
|
18
|
+
export type { JsonLogEntry, BufferedLogEntry } from '../types/logging';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Log viewport interface for backward compatibility
|
|
22
|
+
*/
|
|
23
|
+
export interface LogViewport {
|
|
24
|
+
entries: import('../types/logging').BufferedLogEntry[];
|
|
25
|
+
totalCount: number;
|
|
26
|
+
offset: number;
|
|
27
|
+
visibleCount: number;
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared log constants to avoid circular dependencies
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const COLORS = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
yellow: '\x1b[33m',
|
|
9
|
+
green: '\x1b[32m',
|
|
10
|
+
blue: '\x1b[34m',
|
|
11
|
+
cyan: '\x1b[36m',
|
|
12
|
+
magenta: '\x1b[35m',
|
|
13
|
+
gray: '\x1b[90m',
|
|
14
|
+
bold: '\x1b[1m',
|
|
15
|
+
white: '\x1b[37m',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export enum LogLevel {
|
|
19
|
+
error = 0,
|
|
20
|
+
warn = 1,
|
|
21
|
+
info = 2,
|
|
22
|
+
success = 2,
|
|
23
|
+
progress = 2,
|
|
24
|
+
debug = 3,
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for formatting log messages for console display
|
|
3
|
+
*
|
|
4
|
+
* Format: [HH:MM:SS] [lane-task] ICON TYPE content
|
|
5
|
+
*
|
|
6
|
+
* Rules:
|
|
7
|
+
* - Box format only for: user, assistant, system, result
|
|
8
|
+
* - Compact format for: tool, tool_result, thinking (gray/dim)
|
|
9
|
+
* - Tool names simplified: ShellToolCall → Shell
|
|
10
|
+
* - Lane labels max 16 chars: [01-types-tests]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { COLORS } from './log-constants';
|
|
14
|
+
import { ParsedMessage, stripAnsi } from './enhanced-logger';
|
|
15
|
+
|
|
16
|
+
// Types that should use box format
|
|
17
|
+
const BOX_TYPES = new Set(['user', 'assistant', 'system', 'result']);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Simplify tool names (ShellToolCall → Shell, etc.)
|
|
21
|
+
*/
|
|
22
|
+
function simplifyToolName(name: string): string {
|
|
23
|
+
// Remove common suffixes
|
|
24
|
+
return name
|
|
25
|
+
.replace(/ToolCall$/i, '')
|
|
26
|
+
.replace(/Tool$/i, '')
|
|
27
|
+
.replace(/^run_terminal_cmd$/i, 'shell')
|
|
28
|
+
.replace(/^search_replace$/i, 'edit');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format a single parsed message into a human-readable string (compact or multi-line)
|
|
33
|
+
*/
|
|
34
|
+
export function formatMessageForConsole(
|
|
35
|
+
msg: ParsedMessage,
|
|
36
|
+
options: {
|
|
37
|
+
includeTimestamp?: boolean;
|
|
38
|
+
laneLabel?: string;
|
|
39
|
+
compact?: boolean;
|
|
40
|
+
context?: string;
|
|
41
|
+
} = {}
|
|
42
|
+
): string {
|
|
43
|
+
const { includeTimestamp = true, laneLabel = '', compact = false, context = '' } = options;
|
|
44
|
+
const ts = includeTimestamp ? new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
|
|
45
|
+
const tsPrefix = ts ? `${COLORS.gray}[${ts}]${COLORS.reset} ` : '';
|
|
46
|
+
|
|
47
|
+
// Handle context (e.g. from logger.info) - max 16 chars
|
|
48
|
+
const effectiveLaneLabel = laneLabel || (context ? `[${context}]` : '');
|
|
49
|
+
const truncatedLabel = effectiveLaneLabel.length > 16
|
|
50
|
+
? effectiveLaneLabel.substring(0, 16)
|
|
51
|
+
: effectiveLaneLabel;
|
|
52
|
+
const labelPrefix = truncatedLabel ? `${COLORS.magenta}${truncatedLabel.padEnd(16)}${COLORS.reset} ` : '';
|
|
53
|
+
|
|
54
|
+
let typePrefix = '';
|
|
55
|
+
let content = msg.content;
|
|
56
|
+
|
|
57
|
+
// Determine if we should use box format
|
|
58
|
+
// Box format only for: user, assistant, system, result (and only when not compact)
|
|
59
|
+
const useBox = !compact && BOX_TYPES.has(msg.type);
|
|
60
|
+
|
|
61
|
+
// Clean up wrapped prompts for user messages to hide internal instructions
|
|
62
|
+
if (msg.type === 'user') {
|
|
63
|
+
const contextMarker = '### 🛠 Environment & Context';
|
|
64
|
+
const instructionsMarker = '### 📝 Final Instructions';
|
|
65
|
+
|
|
66
|
+
if (content.includes(contextMarker)) {
|
|
67
|
+
const parts = content.split('---\n');
|
|
68
|
+
if (parts.length >= 3) {
|
|
69
|
+
content = parts[1]!.trim();
|
|
70
|
+
} else {
|
|
71
|
+
content = content.split(contextMarker).pop() || content;
|
|
72
|
+
content = content.split(instructionsMarker)[0] || content;
|
|
73
|
+
content = content.replace(/^.*---\n/s, '').trim();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// For thinking: collapse multiple newlines into single space
|
|
79
|
+
if (msg.type === 'thinking') {
|
|
80
|
+
content = content.replace(/\n\s*\n/g, ' ').replace(/\n/g, ' ').trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
switch (msg.type) {
|
|
84
|
+
case 'user':
|
|
85
|
+
typePrefix = `${COLORS.cyan}🧑 USER${COLORS.reset}`;
|
|
86
|
+
if (!useBox) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
|
|
87
|
+
break;
|
|
88
|
+
case 'assistant':
|
|
89
|
+
typePrefix = `${COLORS.green}🤖 ASST${COLORS.reset}`;
|
|
90
|
+
if (!useBox) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
|
|
91
|
+
break;
|
|
92
|
+
case 'tool':
|
|
93
|
+
// Tool calls are always compact and gray
|
|
94
|
+
typePrefix = `${COLORS.gray}🔧 TOOL${COLORS.reset}`;
|
|
95
|
+
const toolMatch = content.match(/\[Tool: ([^\]]+)\] (.*)/);
|
|
96
|
+
if (toolMatch) {
|
|
97
|
+
const [, rawName, args] = toolMatch;
|
|
98
|
+
const name = simplifyToolName(rawName!);
|
|
99
|
+
try {
|
|
100
|
+
const parsedArgs = JSON.parse(args!);
|
|
101
|
+
let argStr = '';
|
|
102
|
+
if (rawName === 'read_file' && parsedArgs.target_file) {
|
|
103
|
+
argStr = parsedArgs.target_file;
|
|
104
|
+
} else if (rawName === 'run_terminal_cmd' && parsedArgs.command) {
|
|
105
|
+
argStr = parsedArgs.command;
|
|
106
|
+
} else if (rawName === 'write' && parsedArgs.file_path) {
|
|
107
|
+
argStr = parsedArgs.file_path;
|
|
108
|
+
} else if (rawName === 'search_replace' && parsedArgs.file_path) {
|
|
109
|
+
argStr = parsedArgs.file_path;
|
|
110
|
+
} else {
|
|
111
|
+
const keys = Object.keys(parsedArgs);
|
|
112
|
+
if (keys.length > 0) {
|
|
113
|
+
argStr = String(parsedArgs[keys[0]]).substring(0, 50);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
content = `${COLORS.gray}${name}${COLORS.reset}(${COLORS.gray}${argStr}${COLORS.reset})`;
|
|
117
|
+
} catch {
|
|
118
|
+
content = `${COLORS.gray}${name}${COLORS.reset}: ${args}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
case 'tool_result':
|
|
123
|
+
// Tool results are always compact and gray
|
|
124
|
+
typePrefix = `${COLORS.gray}📄 RESL${COLORS.reset}`;
|
|
125
|
+
const resMatch = content.match(/\[Tool Result: ([^\]]+)\]/);
|
|
126
|
+
if (resMatch) {
|
|
127
|
+
const simpleName = simplifyToolName(resMatch[1]!);
|
|
128
|
+
content = `${COLORS.gray}${simpleName} OK${COLORS.reset}`;
|
|
129
|
+
} else {
|
|
130
|
+
content = `${COLORS.gray}result${COLORS.reset}`;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
case 'result':
|
|
134
|
+
case 'success':
|
|
135
|
+
typePrefix = `${COLORS.green}✅ DONE${COLORS.reset}`;
|
|
136
|
+
break;
|
|
137
|
+
case 'system':
|
|
138
|
+
typePrefix = `${COLORS.gray}⚙️ SYS${COLORS.reset}`;
|
|
139
|
+
break;
|
|
140
|
+
case 'thinking':
|
|
141
|
+
// Thinking is always compact and gray
|
|
142
|
+
typePrefix = `${COLORS.gray}🤔 THNK${COLORS.reset}`;
|
|
143
|
+
content = `${COLORS.gray}${content.substring(0, 100)}${content.length > 100 ? '...' : ''}${COLORS.reset}`;
|
|
144
|
+
break;
|
|
145
|
+
case 'info':
|
|
146
|
+
typePrefix = `${COLORS.cyan}ℹ️ INFO${COLORS.reset}`;
|
|
147
|
+
break;
|
|
148
|
+
case 'warn':
|
|
149
|
+
typePrefix = `${COLORS.yellow}⚠️ WARN${COLORS.reset}`;
|
|
150
|
+
break;
|
|
151
|
+
case 'error':
|
|
152
|
+
typePrefix = `${COLORS.red}❌ ERR${COLORS.reset}`;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!typePrefix) return `${tsPrefix}${labelPrefix}${content}`;
|
|
157
|
+
|
|
158
|
+
// Compact format (single line)
|
|
159
|
+
if (!useBox) {
|
|
160
|
+
return `${tsPrefix}${labelPrefix}${typePrefix.padEnd(12)} ${content}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Multi-line box format (only for user, assistant, system, result)
|
|
164
|
+
// Emoji width is 2, so we need to account for that in indent calculation
|
|
165
|
+
const lines = content.split('\n');
|
|
166
|
+
const fullPrefix = `${tsPrefix}${labelPrefix}`;
|
|
167
|
+
const strippedPrefix = stripAnsi(typePrefix);
|
|
168
|
+
// Count emojis (they take 2 terminal columns but 1-2 chars in string)
|
|
169
|
+
const emojiCount = (strippedPrefix.match(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2300}-\u{23FF}]|[\u{2B50}-\u{2B55}]|[\u{231A}-\u{231B}]|[\u{23E9}-\u{23F3}]|[\u{23F8}-\u{23FA}]|✅|❌|⚙️|ℹ️|⚠️|🔧|📄|🤔|🧑|🤖/gu) || []).length;
|
|
170
|
+
const visualWidth = strippedPrefix.length + emojiCount; // emoji adds 1 extra width
|
|
171
|
+
|
|
172
|
+
const boxWidth = 60;
|
|
173
|
+
const header = `${typePrefix}┌${'─'.repeat(boxWidth)}`;
|
|
174
|
+
let result = `${fullPrefix}${header}\n`;
|
|
175
|
+
|
|
176
|
+
const indent = ' '.repeat(visualWidth);
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
result += `${fullPrefix}${indent}│ ${line}\n`;
|
|
179
|
+
}
|
|
180
|
+
result += `${fullPrefix}${indent}└${'─'.repeat(boxWidth)}`;
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Detect and format a message that might be a raw JSON string from cursor-agent
|
|
187
|
+
*/
|
|
188
|
+
export function formatPotentialJsonMessage(message: string): string {
|
|
189
|
+
const trimmed = message.trim();
|
|
190
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
191
|
+
return message;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const json = JSON.parse(trimmed);
|
|
196
|
+
if (!json.type) return message;
|
|
197
|
+
|
|
198
|
+
// Convert JSON to a ParsedMessage-like structure for formatting
|
|
199
|
+
let content: string;
|
|
200
|
+
let type: string;
|
|
201
|
+
|
|
202
|
+
if (json.type === 'thinking' && json.text) {
|
|
203
|
+
content = json.text;
|
|
204
|
+
type = 'thinking';
|
|
205
|
+
} else if (json.type === 'assistant' && json.message?.content) {
|
|
206
|
+
content = json.message.content
|
|
207
|
+
.filter((c: any) => c.type === 'text')
|
|
208
|
+
.map((c: any) => c.text)
|
|
209
|
+
.join('');
|
|
210
|
+
type = 'assistant';
|
|
211
|
+
} else if (json.type === 'user' && json.message?.content) {
|
|
212
|
+
content = json.message.content
|
|
213
|
+
.filter((c: any) => c.type === 'text')
|
|
214
|
+
.map((c: any) => c.text)
|
|
215
|
+
.join('');
|
|
216
|
+
type = 'user';
|
|
217
|
+
} else if (json.type === 'tool_call' && json.subtype === 'started') {
|
|
218
|
+
const rawToolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
219
|
+
const args = json.tool_call[rawToolName]?.args || {};
|
|
220
|
+
// Tool name will be simplified in formatMessageForConsole
|
|
221
|
+
content = `[Tool: ${rawToolName}] ${JSON.stringify(args)}`;
|
|
222
|
+
type = 'tool';
|
|
223
|
+
} else if (json.type === 'tool_call' && json.subtype === 'completed') {
|
|
224
|
+
const rawToolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
225
|
+
content = `[Tool Result: ${rawToolName}]`;
|
|
226
|
+
type = 'tool_result';
|
|
227
|
+
} else if (json.type === 'result') {
|
|
228
|
+
content = json.result || 'Task completed';
|
|
229
|
+
type = 'result';
|
|
230
|
+
} else {
|
|
231
|
+
// Unknown type, return as is
|
|
232
|
+
return message;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return formatMessageForConsole({
|
|
236
|
+
type: type as any,
|
|
237
|
+
role: type,
|
|
238
|
+
content,
|
|
239
|
+
timestamp: json.timestamp_ms || Date.now()
|
|
240
|
+
}, { includeTimestamp: false, compact: true });
|
|
241
|
+
|
|
242
|
+
} catch {
|
|
243
|
+
return message;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Service - Helpers for log processing and filtering
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { JsonLogEntry } from './enhanced-logger';
|
|
6
|
+
import { LogImportance } from './types';
|
|
7
|
+
|
|
8
|
+
export interface MergedLogEntry extends JsonLogEntry {
|
|
9
|
+
laneName: string;
|
|
10
|
+
laneColor: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class LogService {
|
|
14
|
+
/**
|
|
15
|
+
* Determine importance level of a log entry
|
|
16
|
+
*/
|
|
17
|
+
static getLogImportance(entry: JsonLogEntry): LogImportance {
|
|
18
|
+
if (entry.level === 'error') return LogImportance.CRITICAL;
|
|
19
|
+
if (entry.level === 'stderr') return LogImportance.HIGH;
|
|
20
|
+
|
|
21
|
+
const msg = (entry.message || '').toLowerCase();
|
|
22
|
+
if (msg.includes('error') || msg.includes('fail')) return LogImportance.HIGH;
|
|
23
|
+
if (msg.includes('warn')) return LogImportance.MEDIUM;
|
|
24
|
+
if (msg.includes('success') || msg.includes('done') || msg.includes('completed')) return LogImportance.LOW;
|
|
25
|
+
|
|
26
|
+
if (entry.level === 'debug') return LogImportance.DEBUG;
|
|
27
|
+
return LogImportance.INFO;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if an entry meets the minimum importance level
|
|
32
|
+
*/
|
|
33
|
+
static meetsImportanceLevel(entry: JsonLogEntry, minLevel: LogImportance): boolean {
|
|
34
|
+
const entryLevel = this.getLogImportance(entry);
|
|
35
|
+
const levels = [
|
|
36
|
+
LogImportance.DEBUG,
|
|
37
|
+
LogImportance.INFO,
|
|
38
|
+
LogImportance.LOW,
|
|
39
|
+
LogImportance.MEDIUM,
|
|
40
|
+
LogImportance.HIGH,
|
|
41
|
+
LogImportance.CRITICAL
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const entryIdx = levels.indexOf(entryLevel);
|
|
45
|
+
const minIdx = levels.indexOf(minLevel);
|
|
46
|
+
|
|
47
|
+
return entryIdx >= minIdx;
|
|
48
|
+
}
|
|
49
|
+
}
|