@litmers/cursorflow-orchestrator 0.1.13 → 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.
Files changed (76) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +83 -2
  3. package/commands/cursorflow-clean.md +20 -6
  4. package/commands/cursorflow-prepare.md +1 -1
  5. package/commands/cursorflow-resume.md +127 -6
  6. package/commands/cursorflow-run.md +2 -2
  7. package/commands/cursorflow-signal.md +11 -4
  8. package/dist/cli/clean.js +164 -12
  9. package/dist/cli/clean.js.map +1 -1
  10. package/dist/cli/index.d.ts +1 -0
  11. package/dist/cli/index.js +6 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/logs.d.ts +8 -0
  14. package/dist/cli/logs.js +759 -0
  15. package/dist/cli/logs.js.map +1 -0
  16. package/dist/cli/monitor.js +113 -30
  17. package/dist/cli/monitor.js.map +1 -1
  18. package/dist/cli/prepare.js +1 -1
  19. package/dist/cli/resume.js +367 -18
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +9 -0
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/signal.js +34 -20
  24. package/dist/cli/signal.js.map +1 -1
  25. package/dist/core/orchestrator.d.ts +13 -1
  26. package/dist/core/orchestrator.js +396 -35
  27. package/dist/core/orchestrator.js.map +1 -1
  28. package/dist/core/reviewer.d.ts +2 -0
  29. package/dist/core/reviewer.js +24 -2
  30. package/dist/core/reviewer.js.map +1 -1
  31. package/dist/core/runner.d.ts +9 -3
  32. package/dist/core/runner.js +266 -61
  33. package/dist/core/runner.js.map +1 -1
  34. package/dist/utils/config.js +38 -1
  35. package/dist/utils/config.js.map +1 -1
  36. package/dist/utils/enhanced-logger.d.ts +210 -0
  37. package/dist/utils/enhanced-logger.js +1030 -0
  38. package/dist/utils/enhanced-logger.js.map +1 -0
  39. package/dist/utils/events.d.ts +59 -0
  40. package/dist/utils/events.js +37 -0
  41. package/dist/utils/events.js.map +1 -0
  42. package/dist/utils/git.d.ts +11 -0
  43. package/dist/utils/git.js +40 -0
  44. package/dist/utils/git.js.map +1 -1
  45. package/dist/utils/logger.d.ts +2 -0
  46. package/dist/utils/logger.js +4 -1
  47. package/dist/utils/logger.js.map +1 -1
  48. package/dist/utils/types.d.ts +132 -1
  49. package/dist/utils/webhook.d.ts +5 -0
  50. package/dist/utils/webhook.js +109 -0
  51. package/dist/utils/webhook.js.map +1 -0
  52. package/examples/README.md +1 -1
  53. package/package.json +2 -1
  54. package/scripts/patches/test-cursor-agent.js +1 -1
  55. package/scripts/simple-logging-test.sh +97 -0
  56. package/scripts/test-real-cursor-lifecycle.sh +289 -0
  57. package/scripts/test-real-logging.sh +289 -0
  58. package/scripts/test-streaming-multi-task.sh +247 -0
  59. package/src/cli/clean.ts +170 -13
  60. package/src/cli/index.ts +4 -1
  61. package/src/cli/logs.ts +863 -0
  62. package/src/cli/monitor.ts +123 -30
  63. package/src/cli/prepare.ts +1 -1
  64. package/src/cli/resume.ts +463 -22
  65. package/src/cli/run.ts +10 -0
  66. package/src/cli/signal.ts +43 -27
  67. package/src/core/orchestrator.ts +458 -36
  68. package/src/core/reviewer.ts +40 -4
  69. package/src/core/runner.ts +293 -60
  70. package/src/utils/config.ts +41 -1
  71. package/src/utils/enhanced-logger.ts +1166 -0
  72. package/src/utils/events.ts +117 -0
  73. package/src/utils/git.ts +40 -0
  74. package/src/utils/logger.ts +4 -1
  75. package/src/utils/types.ts +160 -1
  76. package/src/utils/webhook.ts +85 -0
@@ -0,0 +1,863 @@
1
+ /**
2
+ * CursorFlow logs command - View and export logs
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as logger from '../utils/logger';
8
+ import { loadConfig } from '../utils/config';
9
+ import {
10
+ readJsonLog,
11
+ exportLogs,
12
+ stripAnsi,
13
+ JsonLogEntry
14
+ } from '../utils/enhanced-logger';
15
+
16
+ interface LogsOptions {
17
+ runDir?: string;
18
+ lane?: string;
19
+ all: boolean; // View all lanes merged
20
+ format: 'text' | 'json' | 'markdown' | 'html';
21
+ output?: string;
22
+ tail?: number;
23
+ follow: boolean;
24
+ filter?: string;
25
+ level?: string;
26
+ clean: boolean;
27
+ raw: boolean;
28
+ readable: boolean; // Show readable parsed log
29
+ help: boolean;
30
+ }
31
+
32
+ function printHelp(): void {
33
+ console.log(`
34
+ Usage: cursorflow logs [run-dir] [options]
35
+
36
+ View and export lane logs.
37
+
38
+ Options:
39
+ [run-dir] Run directory (default: latest)
40
+ --lane <name> Filter to specific lane
41
+ --all, -a View all lanes merged (sorted by timestamp)
42
+ --format <fmt> Output format: text, json, markdown, html (default: text)
43
+ --output <path> Write output to file instead of stdout
44
+ --tail <n> Show last n lines/entries (default: all)
45
+ --follow, -f Follow log output in real-time
46
+ --filter <pattern> Filter entries by regex pattern
47
+ --level <level> Filter by log level: stdout, stderr, info, error, debug
48
+ --readable, -r Show readable log (parsed AI output) (default)
49
+ --clean Show clean terminal logs without ANSI codes
50
+ --raw Show raw terminal logs with ANSI codes
51
+ --help, -h Show help
52
+
53
+ Examples:
54
+ cursorflow logs # View latest run logs summary
55
+ cursorflow logs --lane api-setup # View readable parsed log (default)
56
+ cursorflow logs --lane api-setup --clean # View clean terminal logs
57
+ cursorflow logs --all # View all lanes merged by time
58
+ cursorflow logs --all --follow # Follow all lanes in real-time
59
+ cursorflow logs --all --format json # Export all lanes as JSON
60
+ cursorflow logs --all --filter "error" # Filter all lanes for errors
61
+ cursorflow logs --format json --output out.json # Export as JSON
62
+ `);
63
+ }
64
+
65
+ function parseArgs(args: string[]): LogsOptions {
66
+ const laneIdx = args.indexOf('--lane');
67
+ const formatIdx = args.indexOf('--format');
68
+ const outputIdx = args.indexOf('--output');
69
+ const tailIdx = args.indexOf('--tail');
70
+ const filterIdx = args.indexOf('--filter');
71
+ const levelIdx = args.indexOf('--level');
72
+
73
+ // Find run directory (first non-option argument)
74
+ const runDir = args.find((arg, i) => {
75
+ if (arg.startsWith('--') || arg.startsWith('-')) return false;
76
+ // Skip values for options
77
+ const prevArg = args[i - 1];
78
+ if (prevArg && ['--lane', '--format', '--output', '--tail', '--filter', '--level'].includes(prevArg)) {
79
+ return false;
80
+ }
81
+ return true;
82
+ });
83
+
84
+ const raw = args.includes('--raw');
85
+ const clean = args.includes('--clean');
86
+ const readable = args.includes('--readable') || args.includes('-r');
87
+
88
+ return {
89
+ runDir,
90
+ lane: laneIdx >= 0 ? args[laneIdx + 1] : undefined,
91
+ all: args.includes('--all') || args.includes('-a'),
92
+ format: (formatIdx >= 0 ? args[formatIdx + 1] : 'text') as LogsOptions['format'],
93
+ output: outputIdx >= 0 ? args[outputIdx + 1] : undefined,
94
+ tail: tailIdx >= 0 ? parseInt(args[tailIdx + 1] || '50') : undefined,
95
+ follow: args.includes('--follow') || args.includes('-f'),
96
+ filter: filterIdx >= 0 ? args[filterIdx + 1] : undefined,
97
+ level: levelIdx >= 0 ? args[levelIdx + 1] : undefined,
98
+ raw,
99
+ clean,
100
+ // Default to readable if no other format is specified
101
+ readable: readable || (!raw && !clean),
102
+ help: args.includes('--help') || args.includes('-h'),
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Find the latest run directory
108
+ */
109
+ function findLatestRunDir(logsDir: string): string | null {
110
+ const runsDir = path.join(logsDir, 'runs');
111
+ if (!fs.existsSync(runsDir)) return null;
112
+
113
+ const runs = fs.readdirSync(runsDir)
114
+ .filter(d => d.startsWith('run-'))
115
+ .map(d => ({
116
+ name: d,
117
+ path: path.join(runsDir, d),
118
+ mtime: fs.statSync(path.join(runsDir, d)).mtime.getTime()
119
+ }))
120
+ .sort((a, b) => b.mtime - a.mtime);
121
+
122
+ return runs.length > 0 ? runs[0]!.path : null;
123
+ }
124
+
125
+ /**
126
+ * List lanes in a run directory
127
+ */
128
+ function listLanes(runDir: string): string[] {
129
+ const lanesDir = path.join(runDir, 'lanes');
130
+ if (!fs.existsSync(lanesDir)) return [];
131
+
132
+ return fs.readdirSync(lanesDir)
133
+ .filter(d => fs.statSync(path.join(lanesDir, d)).isDirectory());
134
+ }
135
+
136
+ /**
137
+ * Read and display text logs
138
+ */
139
+ function displayTextLogs(
140
+ laneDir: string,
141
+ options: LogsOptions
142
+ ): void {
143
+ let logFile: string;
144
+ const readableLog = path.join(laneDir, 'terminal-readable.log');
145
+ const rawLog = path.join(laneDir, 'terminal-raw.log');
146
+ const cleanLog = path.join(laneDir, 'terminal.log');
147
+
148
+ if (options.raw) {
149
+ logFile = rawLog;
150
+ } else if (options.clean) {
151
+ logFile = cleanLog;
152
+ } else if (options.readable && fs.existsSync(readableLog)) {
153
+ logFile = readableLog;
154
+ } else {
155
+ // Default or fallback to clean log
156
+ logFile = cleanLog;
157
+ }
158
+
159
+ if (!fs.existsSync(logFile)) {
160
+ console.log('No log file found.');
161
+ return;
162
+ }
163
+
164
+ let content = fs.readFileSync(logFile, 'utf8');
165
+ let lines = content.split('\n');
166
+
167
+ // Apply filter
168
+ if (options.filter) {
169
+ const regex = new RegExp(options.filter, 'i');
170
+ lines = lines.filter(line => regex.test(line));
171
+ }
172
+
173
+ // Apply tail
174
+ if (options.tail && lines.length > options.tail) {
175
+ lines = lines.slice(-options.tail);
176
+ }
177
+
178
+ // Clean ANSI if needed (for clean mode or default fallback)
179
+ if (!options.raw) {
180
+ lines = lines.map(line => stripAnsi(line));
181
+ }
182
+
183
+ console.log(lines.join('\n'));
184
+ }
185
+
186
+ /**
187
+ * Read and display JSON logs
188
+ */
189
+ function displayJsonLogs(
190
+ laneDir: string,
191
+ options: LogsOptions
192
+ ): void {
193
+ const logFile = path.join(laneDir, 'terminal.jsonl');
194
+
195
+ if (!fs.existsSync(logFile)) {
196
+ console.log('No JSON log file found.');
197
+ return;
198
+ }
199
+
200
+ let entries = readJsonLog(logFile);
201
+
202
+ // Apply level filter
203
+ if (options.level) {
204
+ entries = entries.filter(e => e.level === options.level);
205
+ }
206
+
207
+ // Apply regex filter
208
+ if (options.filter) {
209
+ const regex = new RegExp(options.filter, 'i');
210
+ entries = entries.filter(e => regex.test(e.message) || regex.test(e.task || ''));
211
+ }
212
+
213
+ // Apply tail
214
+ if (options.tail && entries.length > options.tail) {
215
+ entries = entries.slice(-options.tail);
216
+ }
217
+
218
+ if (options.format === 'json') {
219
+ console.log(JSON.stringify(entries, null, 2));
220
+ } else {
221
+ // Display as formatted text
222
+ for (const entry of entries) {
223
+ const levelColor = getLevelColor(entry.level);
224
+ const ts = new Date(entry.timestamp).toLocaleTimeString();
225
+ console.log(`${levelColor}[${ts}] [${entry.level.toUpperCase().padEnd(6)}]${logger.COLORS.reset} ${entry.message}`);
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Get color for log level
232
+ */
233
+ function getLevelColor(level: string): string {
234
+ switch (level) {
235
+ case 'error':
236
+ return logger.COLORS.red;
237
+ case 'stderr':
238
+ return logger.COLORS.yellow;
239
+ case 'info':
240
+ case 'session':
241
+ return logger.COLORS.cyan;
242
+ case 'debug':
243
+ return logger.COLORS.gray;
244
+ default:
245
+ return logger.COLORS.reset;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Lane color palette for distinguishing lanes in merged view
251
+ */
252
+ const LANE_COLORS = [
253
+ '\x1b[38;5;39m', // Blue
254
+ '\x1b[38;5;208m', // Orange
255
+ '\x1b[38;5;156m', // Light Green
256
+ '\x1b[38;5;213m', // Pink
257
+ '\x1b[38;5;87m', // Cyan
258
+ '\x1b[38;5;228m', // Yellow
259
+ '\x1b[38;5;183m', // Light Purple
260
+ '\x1b[38;5;121m', // Sea Green
261
+ ];
262
+
263
+ /**
264
+ * Get consistent color for a lane name
265
+ */
266
+ function getLaneColor(laneName: string, laneIndex: number): string {
267
+ return LANE_COLORS[laneIndex % LANE_COLORS.length]!;
268
+ }
269
+
270
+ /**
271
+ * Extended JSON log entry with lane info
272
+ */
273
+ interface MergedLogEntry extends JsonLogEntry {
274
+ laneName: string;
275
+ laneColor: string;
276
+ }
277
+
278
+ /**
279
+ * Read and merge all lane logs
280
+ */
281
+ function readAllLaneLogs(runDir: string): MergedLogEntry[] {
282
+ const lanes = listLanes(runDir);
283
+ const allEntries: MergedLogEntry[] = [];
284
+
285
+ lanes.forEach((laneName, index) => {
286
+ const laneDir = path.join(runDir, 'lanes', laneName);
287
+ const jsonLogPath = path.join(laneDir, 'terminal.jsonl');
288
+
289
+ if (fs.existsSync(jsonLogPath)) {
290
+ const entries = readJsonLog(jsonLogPath);
291
+ const laneColor = getLaneColor(laneName, index);
292
+
293
+ for (const entry of entries) {
294
+ allEntries.push({
295
+ ...entry,
296
+ laneName,
297
+ laneColor,
298
+ });
299
+ }
300
+ }
301
+ });
302
+
303
+ // Sort by timestamp
304
+ allEntries.sort((a, b) => {
305
+ const timeA = new Date(a.timestamp).getTime();
306
+ const timeB = new Date(b.timestamp).getTime();
307
+ return timeA - timeB;
308
+ });
309
+
310
+ return allEntries;
311
+ }
312
+
313
+ /**
314
+ * Display merged logs from all lanes
315
+ */
316
+ function displayMergedLogs(runDir: string, options: LogsOptions): void {
317
+ let entries = readAllLaneLogs(runDir);
318
+
319
+ if (entries.length === 0) {
320
+ console.log('No log entries found in any lane.');
321
+ return;
322
+ }
323
+
324
+ // Apply level filter
325
+ if (options.level) {
326
+ entries = entries.filter(e => e.level === options.level);
327
+ }
328
+
329
+ // Apply regex filter
330
+ if (options.filter) {
331
+ const regex = new RegExp(options.filter, 'i');
332
+ entries = entries.filter(e =>
333
+ regex.test(e.message) ||
334
+ regex.test(e.task || '') ||
335
+ regex.test(e.laneName)
336
+ );
337
+ }
338
+
339
+ // Apply tail
340
+ if (options.tail && entries.length > options.tail) {
341
+ entries = entries.slice(-options.tail);
342
+ }
343
+
344
+ // Get unique lanes for legend
345
+ const lanes = [...new Set(entries.map(e => e.laneName))];
346
+
347
+ // Print header
348
+ console.log(`\n${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}`);
349
+ console.log(`${logger.COLORS.cyan} 🔀 Merged Logs - ${path.basename(runDir)} (${entries.length} entries from ${lanes.length} lanes)${logger.COLORS.reset}`);
350
+ console.log(`${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}`);
351
+
352
+ // Print lane legend
353
+ console.log('\n Lanes: ' + lanes.map((lane, i) => {
354
+ const color = getLaneColor(lane, lanes.indexOf(lane));
355
+ return `${color}■${logger.COLORS.reset} ${lane}`;
356
+ }).join(' '));
357
+ console.log('');
358
+
359
+ // Format output based on format option
360
+ if (options.format === 'json') {
361
+ console.log(JSON.stringify(entries, null, 2));
362
+ return;
363
+ }
364
+
365
+ // Display entries
366
+ for (const entry of entries) {
367
+ const ts = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
368
+ const levelColor = getLevelColor(entry.level);
369
+ const laneColor = entry.laneColor;
370
+ const lanePad = entry.laneName.substring(0, 12).padEnd(12);
371
+ const levelPad = entry.level.toUpperCase().padEnd(6);
372
+
373
+ // Skip session entries for cleaner output unless they're important
374
+ if (entry.level === 'session' && entry.message === 'Session started') {
375
+ console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${laneColor}[${lanePad}]${logger.COLORS.reset} ${logger.COLORS.cyan}── Session Started ──${logger.COLORS.reset}`);
376
+ continue;
377
+ }
378
+ if (entry.level === 'session' && entry.message === 'Session ended') {
379
+ console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${laneColor}[${lanePad}]${logger.COLORS.reset} ${logger.COLORS.cyan}── Session Ended ──${logger.COLORS.reset}`);
380
+ continue;
381
+ }
382
+
383
+ console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${laneColor}[${lanePad}]${logger.COLORS.reset} ${levelColor}[${levelPad}]${logger.COLORS.reset} ${entry.message}`);
384
+ }
385
+
386
+ console.log(`\n${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}`);
387
+ }
388
+
389
+ /**
390
+ * Follow all lanes in real-time
391
+ */
392
+ function followAllLogs(runDir: string, options: LogsOptions): void {
393
+ const lanes = listLanes(runDir);
394
+
395
+ if (lanes.length === 0) {
396
+ console.log('No lanes found.');
397
+ return;
398
+ }
399
+
400
+ // Track last read position for each lane
401
+ const lastPositions: Record<string, number> = {};
402
+ const laneColors: Record<string, string> = {};
403
+
404
+ lanes.forEach((lane, index) => {
405
+ lastPositions[lane] = 0;
406
+ laneColors[lane] = getLaneColor(lane, index);
407
+ });
408
+
409
+ // Print header
410
+ console.log(`\n${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}`);
411
+ console.log(`${logger.COLORS.cyan} 🔴 Following All Lanes - ${path.basename(runDir)} (Ctrl+C to stop)${logger.COLORS.reset}`);
412
+ console.log(`${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}`);
413
+ console.log('\n Lanes: ' + lanes.map((lane, i) => {
414
+ return `${laneColors[lane]}■${logger.COLORS.reset} ${lane}`;
415
+ }).join(' '));
416
+ console.log('');
417
+
418
+ const checkInterval = setInterval(() => {
419
+ const newEntries: MergedLogEntry[] = [];
420
+
421
+ for (const lane of lanes) {
422
+ const laneDir = path.join(runDir, 'lanes', lane);
423
+ const jsonLogPath = path.join(laneDir, 'terminal.jsonl');
424
+
425
+ if (!fs.existsSync(jsonLogPath)) continue;
426
+
427
+ try {
428
+ const stats = fs.statSync(jsonLogPath);
429
+ if (stats.size > lastPositions[lane]!) {
430
+ const fd = fs.openSync(jsonLogPath, 'r');
431
+ const buffer = Buffer.alloc(stats.size - lastPositions[lane]!);
432
+ fs.readSync(fd, buffer, 0, buffer.length, lastPositions[lane]!);
433
+ fs.closeSync(fd);
434
+
435
+ const content = buffer.toString();
436
+ const lines = content.split('\n').filter(l => l.trim());
437
+
438
+ for (const line of lines) {
439
+ try {
440
+ const entry = JSON.parse(line) as JsonLogEntry;
441
+ newEntries.push({
442
+ ...entry,
443
+ laneName: lane,
444
+ laneColor: laneColors[lane]!,
445
+ });
446
+ } catch {
447
+ // Skip invalid lines
448
+ }
449
+ }
450
+
451
+ lastPositions[lane] = stats.size;
452
+ }
453
+ } catch {
454
+ // Ignore errors
455
+ }
456
+ }
457
+
458
+ // Sort new entries by timestamp
459
+ newEntries.sort((a, b) => {
460
+ const timeA = new Date(a.timestamp).getTime();
461
+ const timeB = new Date(b.timestamp).getTime();
462
+ return timeA - timeB;
463
+ });
464
+
465
+ // Apply filters and display
466
+ for (let entry of newEntries) {
467
+ // Apply level filter
468
+ if (options.level && entry.level !== options.level) continue;
469
+
470
+ // Apply regex filter
471
+ if (options.filter) {
472
+ const regex = new RegExp(options.filter, 'i');
473
+ if (!regex.test(entry.message) && !regex.test(entry.task || '') && !regex.test(entry.laneName)) {
474
+ continue;
475
+ }
476
+ }
477
+
478
+ const ts = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
479
+ const levelColor = getLevelColor(entry.level);
480
+ const lanePad = entry.laneName.substring(0, 12).padEnd(12);
481
+ const levelPad = entry.level.toUpperCase().padEnd(6);
482
+
483
+ // Skip verbose session entries
484
+ if (entry.level === 'session') {
485
+ if (entry.message === 'Session started') {
486
+ console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${entry.laneColor}[${lanePad}]${logger.COLORS.reset} ${logger.COLORS.cyan}── Session Started ──${logger.COLORS.reset}`);
487
+ } else if (entry.message === 'Session ended') {
488
+ console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${entry.laneColor}[${lanePad}]${logger.COLORS.reset} ${logger.COLORS.cyan}── Session Ended ──${logger.COLORS.reset}`);
489
+ }
490
+ continue;
491
+ }
492
+
493
+ console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${entry.laneColor}[${lanePad}]${logger.COLORS.reset} ${levelColor}[${levelPad}]${logger.COLORS.reset} ${entry.message}`);
494
+ }
495
+ }, 100);
496
+
497
+ // Handle Ctrl+C
498
+ process.on('SIGINT', () => {
499
+ clearInterval(checkInterval);
500
+ console.log('\n\nStopped following logs.');
501
+ process.exit(0);
502
+ });
503
+ }
504
+
505
+ /**
506
+ * Export merged logs to various formats
507
+ */
508
+ function exportMergedLogs(runDir: string, format: string, outputPath?: string): string {
509
+ let entries = readAllLaneLogs(runDir);
510
+ let output = '';
511
+
512
+ switch (format) {
513
+ case 'json':
514
+ output = JSON.stringify(entries, null, 2);
515
+ break;
516
+
517
+ case 'markdown':
518
+ output = exportMergedToMarkdown(entries, runDir);
519
+ break;
520
+
521
+ case 'html':
522
+ output = exportMergedToHtml(entries, runDir);
523
+ break;
524
+
525
+ default:
526
+ // Text format
527
+ for (const entry of entries) {
528
+ const ts = new Date(entry.timestamp).toISOString();
529
+ output += `[${ts}] [${entry.laneName}] [${entry.level.toUpperCase()}] ${entry.message}\n`;
530
+ }
531
+ }
532
+
533
+ if (outputPath) {
534
+ fs.writeFileSync(outputPath, output, 'utf8');
535
+ }
536
+
537
+ return output;
538
+ }
539
+
540
+ /**
541
+ * Export merged logs to Markdown
542
+ */
543
+ function exportMergedToMarkdown(entries: MergedLogEntry[], runDir: string): string {
544
+ const lanes = [...new Set(entries.map(e => e.laneName))];
545
+
546
+ let md = `# CursorFlow Merged Logs\n\n`;
547
+ md += `**Run:** ${path.basename(runDir)}\n`;
548
+ md += `**Lanes:** ${lanes.join(', ')}\n`;
549
+ md += `**Entries:** ${entries.length}\n\n`;
550
+
551
+ md += `## Timeline\n\n`;
552
+ md += '| Time | Lane | Level | Message |\n';
553
+ md += '|------|------|-------|--------|\n';
554
+
555
+ for (const entry of entries) {
556
+ const ts = new Date(entry.timestamp).toLocaleTimeString();
557
+ const message = entry.message.replace(/\|/g, '\\|').substring(0, 80);
558
+ md += `| ${ts} | ${entry.laneName} | ${entry.level} | ${message} |\n`;
559
+ }
560
+
561
+ return md;
562
+ }
563
+
564
+ /**
565
+ * Export merged logs to HTML
566
+ */
567
+ function exportMergedToHtml(entries: MergedLogEntry[], runDir: string): string {
568
+ const lanes = [...new Set(entries.map(e => e.laneName))];
569
+
570
+ let html = `<!DOCTYPE html>
571
+ <html>
572
+ <head>
573
+ <title>CursorFlow Merged Logs - ${path.basename(runDir)}</title>
574
+ <style>
575
+ body { font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; margin: 20px; background: #1e1e1e; color: #d4d4d4; }
576
+ h1, h2 { color: #569cd6; }
577
+ .legend { margin: 10px 0; padding: 10px; background: #252526; border-radius: 4px; }
578
+ .legend-item { display: inline-block; margin-right: 15px; }
579
+ .legend-color { display: inline-block; width: 12px; height: 12px; margin-right: 5px; border-radius: 2px; }
580
+ .entry { padding: 4px 8px; margin: 2px 0; border-radius: 4px; display: flex; }
581
+ .entry .time { color: #808080; width: 80px; flex-shrink: 0; }
582
+ .entry .lane { width: 120px; flex-shrink: 0; font-weight: bold; }
583
+ .entry .level { width: 70px; flex-shrink: 0; }
584
+ .entry .message { flex: 1; white-space: pre-wrap; }
585
+ .entry.stdout { background: #252526; }
586
+ .entry.stderr { background: #3c1f1f; }
587
+ .entry.stderr .level { color: #f48771; }
588
+ .entry.info { background: #1e3a5f; }
589
+ .entry.error { background: #5f1e1e; }
590
+ .entry.session { background: #1e4620; color: #6a9955; }
591
+ </style>
592
+ </head>
593
+ <body>
594
+ <h1>🔀 CursorFlow Merged Logs</h1>
595
+ <p><strong>Run:</strong> ${path.basename(runDir)} | <strong>Entries:</strong> ${entries.length}</p>
596
+
597
+ <div class="legend">
598
+ <strong>Lanes:</strong> `;
599
+
600
+ const colors = ['#5dade2', '#f39c12', '#58d68d', '#af7ac5', '#48c9b0', '#f7dc6f', '#bb8fce', '#76d7c4'];
601
+ lanes.forEach((lane, i) => {
602
+ const color = colors[i % colors.length];
603
+ html += `<span class="legend-item"><span class="legend-color" style="background: ${color}"></span>${lane}</span>`;
604
+ });
605
+
606
+ html += `</div>\n`;
607
+
608
+ for (const entry of entries) {
609
+ const ts = new Date(entry.timestamp).toLocaleTimeString();
610
+ const laneIndex = lanes.indexOf(entry.laneName);
611
+ const color = colors[laneIndex % colors.length];
612
+
613
+ html += ` <div class="entry ${entry.level}">
614
+ <span class="time">${ts}</span>
615
+ <span class="lane" style="color: ${color}">${entry.laneName}</span>
616
+ <span class="level">[${entry.level.toUpperCase()}]</span>
617
+ <span class="message">${escapeHtml(entry.message)}</span>
618
+ </div>\n`;
619
+ }
620
+
621
+ html += '</body></html>';
622
+ return html;
623
+ }
624
+
625
+ function escapeHtml(text: string): string {
626
+ return text
627
+ .replace(/&/g, '&amp;')
628
+ .replace(/</g, '&lt;')
629
+ .replace(/>/g, '&gt;')
630
+ .replace(/"/g, '&quot;')
631
+ .replace(/'/g, '&#039;');
632
+ }
633
+
634
+ /**
635
+ * Follow logs in real-time
636
+ */
637
+ function followLogs(laneDir: string, options: LogsOptions): void {
638
+ let logFile: string;
639
+ const readableLog = path.join(laneDir, 'terminal-readable.log');
640
+ const rawLog = path.join(laneDir, 'terminal-raw.log');
641
+ const cleanLog = path.join(laneDir, 'terminal.log');
642
+
643
+ if (options.raw) {
644
+ logFile = rawLog;
645
+ } else if (options.clean) {
646
+ logFile = cleanLog;
647
+ } else if (options.readable && fs.existsSync(readableLog)) {
648
+ logFile = readableLog;
649
+ } else {
650
+ // Default or fallback to clean log
651
+ logFile = cleanLog;
652
+ }
653
+
654
+ if (!fs.existsSync(logFile)) {
655
+ console.log('Waiting for log file...');
656
+ }
657
+
658
+ let lastSize = 0;
659
+ try {
660
+ lastSize = fs.existsSync(logFile) ? fs.statSync(logFile).size : 0;
661
+ } catch {
662
+ // Ignore
663
+ }
664
+
665
+ console.log(`${logger.COLORS.cyan}Following ${logFile}... (Ctrl+C to stop)${logger.COLORS.reset}\n`);
666
+
667
+ const checkInterval = setInterval(() => {
668
+ try {
669
+ if (!fs.existsSync(logFile)) return;
670
+
671
+ const stats = fs.statSync(logFile);
672
+ if (stats.size > lastSize) {
673
+ const fd = fs.openSync(logFile, 'r');
674
+ const buffer = Buffer.alloc(stats.size - lastSize);
675
+ fs.readSync(fd, buffer, 0, buffer.length, lastSize);
676
+ fs.closeSync(fd);
677
+
678
+ let content = buffer.toString();
679
+
680
+ // Apply filter
681
+ if (options.filter) {
682
+ const regex = new RegExp(options.filter, 'i');
683
+ const lines = content.split('\n');
684
+ content = lines.filter(line => regex.test(line)).join('\n');
685
+ }
686
+
687
+ // Clean ANSI if needed (unless raw mode)
688
+ if (!options.raw) {
689
+ content = stripAnsi(content);
690
+ }
691
+
692
+ if (content.trim()) {
693
+ process.stdout.write(content);
694
+ }
695
+
696
+ lastSize = stats.size;
697
+ }
698
+ } catch (e) {
699
+ // Ignore errors (file might be rotating)
700
+ }
701
+ }, 100);
702
+
703
+ // Handle Ctrl+C
704
+ process.on('SIGINT', () => {
705
+ clearInterval(checkInterval);
706
+ console.log('\n\nStopped following logs.');
707
+ process.exit(0);
708
+ });
709
+ }
710
+
711
+ /**
712
+ * Display logs summary for all lanes
713
+ */
714
+ function displaySummary(runDir: string): void {
715
+ const lanes = listLanes(runDir);
716
+
717
+ console.log(`\n${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}`);
718
+ console.log(`${logger.COLORS.cyan} 📋 Logs Summary - ${path.basename(runDir)}${logger.COLORS.reset}`);
719
+ console.log(`${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}\n`);
720
+
721
+ if (lanes.length === 0) {
722
+ console.log(' No lanes found.');
723
+ return;
724
+ }
725
+
726
+ for (const lane of lanes) {
727
+ const laneDir = path.join(runDir, 'lanes', lane);
728
+ const cleanLog = path.join(laneDir, 'terminal.log');
729
+ const rawLog = path.join(laneDir, 'terminal-raw.log');
730
+ const jsonLog = path.join(laneDir, 'terminal.jsonl');
731
+ const readableLog = path.join(laneDir, 'terminal-readable.log');
732
+
733
+ console.log(` ${logger.COLORS.green}📁 ${lane}${logger.COLORS.reset}`);
734
+
735
+ if (fs.existsSync(cleanLog)) {
736
+ const stats = fs.statSync(cleanLog);
737
+ console.log(` └─ terminal.log ${formatSize(stats.size)}`);
738
+ }
739
+
740
+ if (fs.existsSync(rawLog)) {
741
+ const stats = fs.statSync(rawLog);
742
+ console.log(` └─ terminal-raw.log ${formatSize(stats.size)}`);
743
+ }
744
+
745
+ if (fs.existsSync(readableLog)) {
746
+ const stats = fs.statSync(readableLog);
747
+ console.log(` └─ terminal-readable.log ${formatSize(stats.size)} ${logger.COLORS.yellow}(parsed AI output)${logger.COLORS.reset}`);
748
+ }
749
+
750
+ if (fs.existsSync(jsonLog)) {
751
+ const stats = fs.statSync(jsonLog);
752
+ const entries = readJsonLog(jsonLog);
753
+ const errors = entries.filter(e => e.level === 'error' || e.level === 'stderr').length;
754
+ console.log(` └─ terminal.jsonl ${formatSize(stats.size)} (${entries.length} entries, ${errors} errors)`);
755
+ }
756
+
757
+ console.log('');
758
+ }
759
+ }
760
+
761
+ /**
762
+ * Format file size
763
+ */
764
+ function formatSize(bytes: number): string {
765
+ if (bytes < 1024) return `${bytes} B`;
766
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
767
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
768
+ }
769
+
770
+ /**
771
+ * Main logs command
772
+ */
773
+ async function logs(args: string[]): Promise<void> {
774
+ const options = parseArgs(args);
775
+
776
+ if (options.help) {
777
+ printHelp();
778
+ return;
779
+ }
780
+
781
+ const config = loadConfig();
782
+
783
+ // Find run directory
784
+ let runDir = options.runDir;
785
+ if (!runDir || runDir === 'latest') {
786
+ runDir = findLatestRunDir(config.logsDir) || undefined;
787
+ if (!runDir) {
788
+ throw new Error('No run directories found');
789
+ }
790
+ }
791
+
792
+ if (!fs.existsSync(runDir)) {
793
+ throw new Error(`Run directory not found: ${runDir}`);
794
+ }
795
+
796
+ // Handle --all option (view all lanes merged)
797
+ if (options.all) {
798
+ // Handle follow mode for all lanes
799
+ if (options.follow) {
800
+ followAllLogs(runDir, options);
801
+ return;
802
+ }
803
+
804
+ // Handle export for all lanes
805
+ if (options.output) {
806
+ exportMergedLogs(runDir, options.format, options.output);
807
+ console.log(`Exported merged logs to: ${options.output}`);
808
+ return;
809
+ }
810
+
811
+ // Display merged logs
812
+ if (options.format === 'json') {
813
+ const entries = readAllLaneLogs(runDir);
814
+ console.log(JSON.stringify(entries, null, 2));
815
+ } else if (options.format === 'markdown' || options.format === 'html') {
816
+ const content = exportMergedLogs(runDir, options.format);
817
+ console.log(content);
818
+ } else {
819
+ displayMergedLogs(runDir, options);
820
+ }
821
+ return;
822
+ }
823
+
824
+ // If no lane specified, show summary
825
+ if (!options.lane) {
826
+ displaySummary(runDir);
827
+ console.log(`${logger.COLORS.gray}Use --lane <name> to view logs (default: readable), --clean for terminal logs, or --all to view all lanes merged${logger.COLORS.reset}`);
828
+ return;
829
+ }
830
+
831
+ // Find lane directory
832
+ const laneDir = path.join(runDir, 'lanes', options.lane);
833
+ if (!fs.existsSync(laneDir)) {
834
+ const lanes = listLanes(runDir);
835
+ throw new Error(`Lane not found: ${options.lane}\nAvailable lanes: ${lanes.join(', ')}`);
836
+ }
837
+
838
+ // Handle follow mode
839
+ if (options.follow) {
840
+ followLogs(laneDir, options);
841
+ return;
842
+ }
843
+
844
+ // Handle export
845
+ if (options.output) {
846
+ const content = exportLogs(laneDir, options.format, options.output);
847
+ console.log(`Exported logs to: ${options.output}`);
848
+ return;
849
+ }
850
+
851
+ // Display logs
852
+ if (options.format === 'json' || options.level) {
853
+ displayJsonLogs(laneDir, options);
854
+ } else if (options.format === 'markdown' || options.format === 'html') {
855
+ const content = exportLogs(laneDir, options.format);
856
+ console.log(content);
857
+ } else {
858
+ displayTextLogs(laneDir, options);
859
+ }
860
+ }
861
+
862
+ export = logs;
863
+