@litmers/cursorflow-orchestrator 0.1.13 → 0.1.14

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 (68) hide show
  1. package/CHANGELOG.md +34 -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 +746 -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 +2 -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 +11 -1
  26. package/dist/core/orchestrator.js +257 -35
  27. package/dist/core/orchestrator.js.map +1 -1
  28. package/dist/core/reviewer.js +20 -0
  29. package/dist/core/reviewer.js.map +1 -1
  30. package/dist/core/runner.js +113 -13
  31. package/dist/core/runner.js.map +1 -1
  32. package/dist/utils/config.js +34 -0
  33. package/dist/utils/config.js.map +1 -1
  34. package/dist/utils/enhanced-logger.d.ts +209 -0
  35. package/dist/utils/enhanced-logger.js +963 -0
  36. package/dist/utils/enhanced-logger.js.map +1 -0
  37. package/dist/utils/events.d.ts +59 -0
  38. package/dist/utils/events.js +37 -0
  39. package/dist/utils/events.js.map +1 -0
  40. package/dist/utils/git.d.ts +5 -0
  41. package/dist/utils/git.js +25 -0
  42. package/dist/utils/git.js.map +1 -1
  43. package/dist/utils/types.d.ts +122 -1
  44. package/dist/utils/webhook.d.ts +5 -0
  45. package/dist/utils/webhook.js +109 -0
  46. package/dist/utils/webhook.js.map +1 -0
  47. package/examples/README.md +1 -1
  48. package/package.json +1 -1
  49. package/scripts/simple-logging-test.sh +97 -0
  50. package/scripts/test-real-logging.sh +289 -0
  51. package/scripts/test-streaming-multi-task.sh +247 -0
  52. package/src/cli/clean.ts +170 -13
  53. package/src/cli/index.ts +4 -1
  54. package/src/cli/logs.ts +848 -0
  55. package/src/cli/monitor.ts +123 -30
  56. package/src/cli/prepare.ts +1 -1
  57. package/src/cli/resume.ts +463 -22
  58. package/src/cli/run.ts +2 -0
  59. package/src/cli/signal.ts +43 -27
  60. package/src/core/orchestrator.ts +303 -37
  61. package/src/core/reviewer.ts +22 -0
  62. package/src/core/runner.ts +128 -12
  63. package/src/utils/config.ts +36 -0
  64. package/src/utils/enhanced-logger.ts +1097 -0
  65. package/src/utils/events.ts +117 -0
  66. package/src/utils/git.ts +25 -0
  67. package/src/utils/types.ts +150 -1
  68. package/src/utils/webhook.ts +85 -0
@@ -0,0 +1,963 @@
1
+ "use strict";
2
+ /**
3
+ * Enhanced Logger - Comprehensive terminal output capture and management
4
+ *
5
+ * Features:
6
+ * - ANSI escape sequence stripping for clean logs
7
+ * - Automatic timestamps on each line
8
+ * - Log rotation and size management
9
+ * - Session headers with context
10
+ * - Raw and clean log file options
11
+ * - Structured JSON logs for programmatic access
12
+ * - Streaming output support for real-time capture
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.EnhancedLogManager = exports.CleanLogTransform = exports.StreamingMessageParser = exports.DEFAULT_LOG_CONFIG = void 0;
49
+ exports.stripAnsi = stripAnsi;
50
+ exports.formatTimestamp = formatTimestamp;
51
+ exports.createLogManager = createLogManager;
52
+ exports.readJsonLog = readJsonLog;
53
+ exports.exportLogs = exportLogs;
54
+ const fs = __importStar(require("fs"));
55
+ const path = __importStar(require("path"));
56
+ const stream_1 = require("stream");
57
+ exports.DEFAULT_LOG_CONFIG = {
58
+ enabled: true,
59
+ stripAnsi: true,
60
+ addTimestamps: true,
61
+ maxFileSize: 50 * 1024 * 1024, // 50MB
62
+ maxFiles: 5,
63
+ keepRawLogs: true,
64
+ writeJsonLog: true,
65
+ timestampFormat: 'iso',
66
+ };
67
+ /**
68
+ * Streaming JSON Parser - Parses cursor-agent stream-json output
69
+ * and combines tokens into readable messages
70
+ */
71
+ class StreamingMessageParser {
72
+ currentMessage = '';
73
+ currentRole = '';
74
+ messageStartTime = 0;
75
+ onMessage;
76
+ constructor(onMessage) {
77
+ this.onMessage = onMessage;
78
+ }
79
+ /**
80
+ * Parse a line of JSON output from cursor-agent
81
+ */
82
+ parseLine(line) {
83
+ const trimmed = line.trim();
84
+ if (!trimmed || !trimmed.startsWith('{'))
85
+ return;
86
+ try {
87
+ const json = JSON.parse(trimmed);
88
+ this.handleJsonMessage(json);
89
+ }
90
+ catch {
91
+ // Not valid JSON, ignore
92
+ }
93
+ }
94
+ handleJsonMessage(json) {
95
+ const type = json.type;
96
+ switch (type) {
97
+ case 'system':
98
+ // System init message
99
+ this.emitMessage({
100
+ type: 'system',
101
+ role: 'system',
102
+ content: `[System] Model: ${json.model || 'unknown'}, Mode: ${json.permissionMode || 'default'}`,
103
+ timestamp: json.timestamp_ms || Date.now(),
104
+ });
105
+ break;
106
+ case 'user':
107
+ // User message - emit as complete message
108
+ if (json.message?.content) {
109
+ const textContent = json.message.content
110
+ .filter((c) => c.type === 'text')
111
+ .map((c) => c.text)
112
+ .join('');
113
+ this.emitMessage({
114
+ type: 'user',
115
+ role: 'user',
116
+ content: textContent,
117
+ timestamp: json.timestamp_ms || Date.now(),
118
+ });
119
+ }
120
+ break;
121
+ case 'assistant':
122
+ // Streaming assistant message - accumulate tokens
123
+ if (json.message?.content) {
124
+ const textContent = json.message.content
125
+ .filter((c) => c.type === 'text')
126
+ .map((c) => c.text)
127
+ .join('');
128
+ // Check if this is a new message or continuation
129
+ if (this.currentRole !== 'assistant') {
130
+ // Flush previous message if any
131
+ this.flush();
132
+ this.currentRole = 'assistant';
133
+ this.messageStartTime = json.timestamp_ms || Date.now();
134
+ }
135
+ this.currentMessage += textContent;
136
+ }
137
+ break;
138
+ case 'tool_call':
139
+ // Tool call - emit as formatted message
140
+ if (json.subtype === 'started' && json.tool_call) {
141
+ const toolName = Object.keys(json.tool_call)[0] || 'unknown';
142
+ const toolArgs = json.tool_call[toolName]?.args || {};
143
+ this.flush(); // Flush any pending assistant message
144
+ this.emitMessage({
145
+ type: 'tool',
146
+ role: 'tool',
147
+ content: `[Tool: ${toolName}] ${JSON.stringify(toolArgs)}`,
148
+ timestamp: json.timestamp_ms || Date.now(),
149
+ metadata: { callId: json.call_id, toolName },
150
+ });
151
+ }
152
+ else if (json.subtype === 'completed' && json.tool_call) {
153
+ const toolName = Object.keys(json.tool_call)[0] || 'unknown';
154
+ const result = json.tool_call[toolName]?.result;
155
+ if (result?.success) {
156
+ // Truncate large results
157
+ const content = result.success.content || '';
158
+ const truncated = content.length > 500
159
+ ? content.substring(0, 500) + '... (truncated)'
160
+ : content;
161
+ this.emitMessage({
162
+ type: 'tool_result',
163
+ role: 'tool',
164
+ content: `[Tool Result: ${toolName}] ${truncated}`,
165
+ timestamp: json.timestamp_ms || Date.now(),
166
+ metadata: { callId: json.call_id, toolName, lines: result.success.totalLines },
167
+ });
168
+ }
169
+ }
170
+ break;
171
+ case 'result':
172
+ // Final result - flush any pending and emit result
173
+ this.flush();
174
+ this.emitMessage({
175
+ type: 'result',
176
+ role: 'assistant',
177
+ content: json.result || '',
178
+ timestamp: json.timestamp_ms || Date.now(),
179
+ metadata: {
180
+ duration_ms: json.duration_ms,
181
+ is_error: json.is_error,
182
+ subtype: json.subtype,
183
+ },
184
+ });
185
+ break;
186
+ }
187
+ }
188
+ /**
189
+ * Flush accumulated message
190
+ */
191
+ flush() {
192
+ if (this.currentMessage && this.currentRole) {
193
+ this.emitMessage({
194
+ type: this.currentRole,
195
+ role: this.currentRole,
196
+ content: this.currentMessage,
197
+ timestamp: this.messageStartTime,
198
+ });
199
+ }
200
+ this.currentMessage = '';
201
+ this.currentRole = '';
202
+ this.messageStartTime = 0;
203
+ }
204
+ emitMessage(msg) {
205
+ if (msg.content.trim()) {
206
+ this.onMessage(msg);
207
+ }
208
+ }
209
+ }
210
+ exports.StreamingMessageParser = StreamingMessageParser;
211
+ /**
212
+ * ANSI escape sequence regex pattern
213
+ * Matches:
214
+ * - CSI sequences (colors, cursor movement, etc.)
215
+ * - OSC sequences (terminal titles, etc.)
216
+ * - Single-character escape codes
217
+ */
218
+ const ANSI_REGEX = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
219
+ /**
220
+ * Extended ANSI regex for more complete stripping
221
+ */
222
+ const EXTENDED_ANSI_REGEX = /(?:\x1B[@-Z\\-_]|\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07]*(?:\x07|\x1B\\)|\x1B[PX^_][^\x1B]*\x1B\\|\x1B.)/g;
223
+ /**
224
+ * Strip ANSI escape sequences from text
225
+ */
226
+ function stripAnsi(text) {
227
+ return text
228
+ .replace(EXTENDED_ANSI_REGEX, '')
229
+ .replace(ANSI_REGEX, '')
230
+ // Also remove carriage returns that overwrite lines (progress bars, etc.)
231
+ .replace(/\r[^\n]/g, '\n')
232
+ // Clean up any remaining control characters except newlines/tabs
233
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
234
+ }
235
+ /**
236
+ * Format timestamp based on format preference
237
+ */
238
+ function formatTimestamp(format, startTime) {
239
+ const now = Date.now();
240
+ switch (format) {
241
+ case 'iso':
242
+ return new Date(now).toISOString();
243
+ case 'relative':
244
+ if (startTime) {
245
+ const elapsed = now - startTime;
246
+ const seconds = Math.floor(elapsed / 1000);
247
+ const minutes = Math.floor(seconds / 60);
248
+ const hours = Math.floor(minutes / 60);
249
+ if (hours > 0) {
250
+ return `+${hours}h${minutes % 60}m${seconds % 60}s`;
251
+ }
252
+ else if (minutes > 0) {
253
+ return `+${minutes}m${seconds % 60}s`;
254
+ }
255
+ else {
256
+ return `+${seconds}s`;
257
+ }
258
+ }
259
+ return new Date(now).toISOString();
260
+ case 'short':
261
+ return new Date(now).toLocaleTimeString('en-US', { hour12: false });
262
+ default:
263
+ return new Date(now).toISOString();
264
+ }
265
+ }
266
+ /**
267
+ * Regex to detect if a line already has an ISO timestamp at the start
268
+ */
269
+ const EXISTING_TIMESTAMP_REGEX = /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
270
+ /**
271
+ * Check if a line already has a timestamp
272
+ */
273
+ function hasExistingTimestamp(line) {
274
+ return EXISTING_TIMESTAMP_REGEX.test(line.trim());
275
+ }
276
+ /**
277
+ * Transform stream that strips ANSI and adds timestamps
278
+ */
279
+ class CleanLogTransform extends stream_1.Transform {
280
+ config;
281
+ session;
282
+ buffer = '';
283
+ constructor(config, session) {
284
+ super({ encoding: 'utf8' });
285
+ this.config = config;
286
+ this.session = session;
287
+ }
288
+ _transform(chunk, encoding, callback) {
289
+ let text = chunk.toString();
290
+ // Buffer partial lines
291
+ this.buffer += text;
292
+ const lines = this.buffer.split('\n');
293
+ // Keep the last incomplete line in buffer
294
+ this.buffer = lines.pop() || '';
295
+ for (const line of lines) {
296
+ let processed = line;
297
+ // Strip ANSI if enabled
298
+ if (this.config.stripAnsi) {
299
+ processed = stripAnsi(processed);
300
+ }
301
+ // Add timestamp if enabled AND line doesn't already have one
302
+ if (this.config.addTimestamps && processed.trim() && !hasExistingTimestamp(processed)) {
303
+ const ts = formatTimestamp(this.config.timestampFormat, this.session.startTime);
304
+ processed = `[${ts}] ${processed}`;
305
+ }
306
+ this.push(processed + '\n');
307
+ }
308
+ callback();
309
+ }
310
+ _flush(callback) {
311
+ // Process any remaining buffered content
312
+ if (this.buffer.trim()) {
313
+ let processed = this.buffer;
314
+ if (this.config.stripAnsi) {
315
+ processed = stripAnsi(processed);
316
+ }
317
+ if (this.config.addTimestamps && processed.trim() && !hasExistingTimestamp(processed)) {
318
+ const ts = formatTimestamp(this.config.timestampFormat, this.session.startTime);
319
+ processed = `[${ts}] ${processed}`;
320
+ }
321
+ this.push(processed + '\n');
322
+ }
323
+ callback();
324
+ }
325
+ }
326
+ exports.CleanLogTransform = CleanLogTransform;
327
+ /**
328
+ * Enhanced Log Manager - Manages log files with rotation and multiple outputs
329
+ */
330
+ class EnhancedLogManager {
331
+ config;
332
+ session;
333
+ logDir;
334
+ cleanLogPath;
335
+ rawLogPath;
336
+ jsonLogPath;
337
+ readableLogPath;
338
+ cleanLogFd = null;
339
+ rawLogFd = null;
340
+ jsonLogFd = null;
341
+ readableLogFd = null;
342
+ cleanLogSize = 0;
343
+ rawLogSize = 0;
344
+ cleanTransform = null;
345
+ streamingParser = null;
346
+ lineBuffer = '';
347
+ constructor(logDir, session, config = {}) {
348
+ this.config = { ...exports.DEFAULT_LOG_CONFIG, ...config };
349
+ this.session = session;
350
+ this.logDir = logDir;
351
+ // Ensure log directory exists
352
+ fs.mkdirSync(logDir, { recursive: true });
353
+ // Set up log file paths
354
+ this.cleanLogPath = path.join(logDir, 'terminal.log');
355
+ this.rawLogPath = path.join(logDir, 'terminal-raw.log');
356
+ this.jsonLogPath = path.join(logDir, 'terminal.jsonl');
357
+ this.readableLogPath = path.join(logDir, 'terminal-readable.log');
358
+ // Initialize log files
359
+ this.initLogFiles();
360
+ }
361
+ /**
362
+ * Initialize log files and write session headers
363
+ */
364
+ initLogFiles() {
365
+ // Check and rotate if necessary
366
+ this.rotateIfNeeded(this.cleanLogPath, 'clean');
367
+ if (this.config.keepRawLogs) {
368
+ this.rotateIfNeeded(this.rawLogPath, 'raw');
369
+ }
370
+ // Open file descriptors
371
+ this.cleanLogFd = fs.openSync(this.cleanLogPath, 'a');
372
+ if (this.config.keepRawLogs) {
373
+ this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
374
+ }
375
+ if (this.config.writeJsonLog) {
376
+ this.jsonLogFd = fs.openSync(this.jsonLogPath, 'a');
377
+ }
378
+ // Open readable log file (for parsed streaming output)
379
+ this.readableLogFd = fs.openSync(this.readableLogPath, 'a');
380
+ // Get initial file sizes
381
+ try {
382
+ this.cleanLogSize = fs.statSync(this.cleanLogPath).size;
383
+ if (this.config.keepRawLogs) {
384
+ this.rawLogSize = fs.statSync(this.rawLogPath).size;
385
+ }
386
+ }
387
+ catch {
388
+ this.cleanLogSize = 0;
389
+ this.rawLogSize = 0;
390
+ }
391
+ // Write session header
392
+ this.writeSessionHeader();
393
+ // Create transform stream
394
+ this.cleanTransform = new CleanLogTransform(this.config, this.session);
395
+ this.cleanTransform.on('data', (data) => {
396
+ this.writeToCleanLog(data);
397
+ });
398
+ // Create streaming parser for readable log
399
+ this.streamingParser = new StreamingMessageParser((msg) => {
400
+ this.writeReadableMessage(msg);
401
+ });
402
+ }
403
+ /**
404
+ * Write a parsed message to the readable log
405
+ */
406
+ writeReadableMessage(msg) {
407
+ if (this.readableLogFd === null)
408
+ return;
409
+ const ts = new Date(msg.timestamp).toISOString();
410
+ let formatted;
411
+ switch (msg.type) {
412
+ case 'system':
413
+ formatted = `\n${ts}\n${msg.content}\n`;
414
+ break;
415
+ case 'user':
416
+ // Format user prompt nicely
417
+ const promptPreview = msg.content.length > 200
418
+ ? msg.content.substring(0, 200) + '...'
419
+ : msg.content;
420
+ formatted = `\n${ts}\n┌─ 🧑 USER ─────────────────────────────────────────────\n${this.indentText(promptPreview, '│ ')}\n└───────────────────────────────────────────────────────\n`;
421
+ break;
422
+ case 'assistant':
423
+ case 'result':
424
+ // Format assistant response
425
+ const isResult = msg.type === 'result';
426
+ const header = isResult ? '🤖 ASSISTANT (Final)' : '🤖 ASSISTANT';
427
+ const duration = msg.metadata?.duration_ms
428
+ ? ` (${Math.round(msg.metadata.duration_ms / 1000)}s)`
429
+ : '';
430
+ formatted = `\n${ts}\n┌─ ${header}${duration} ──────────────────────────────────\n${this.indentText(msg.content, '│ ')}\n└───────────────────────────────────────────────────────\n`;
431
+ break;
432
+ case 'tool':
433
+ // Format tool call
434
+ formatted = `${ts} 🔧 ${msg.content}\n`;
435
+ break;
436
+ case 'tool_result':
437
+ // Format tool result (truncated)
438
+ const lines = msg.metadata?.lines ? ` (${msg.metadata.lines} lines)` : '';
439
+ formatted = `${ts} 📄 ${msg.metadata?.toolName || 'Tool'}${lines}\n`;
440
+ break;
441
+ default:
442
+ formatted = `${ts} ${msg.content}\n`;
443
+ }
444
+ try {
445
+ fs.writeSync(this.readableLogFd, formatted);
446
+ }
447
+ catch {
448
+ // Ignore write errors
449
+ }
450
+ }
451
+ /**
452
+ * Indent text with a prefix
453
+ */
454
+ indentText(text, prefix) {
455
+ return text.split('\n').map(line => prefix + line).join('\n');
456
+ }
457
+ /**
458
+ * Write session header to logs
459
+ */
460
+ writeSessionHeader() {
461
+ const header = `
462
+ ╔══════════════════════════════════════════════════════════════════════════════╗
463
+ ║ CursorFlow Session Log ║
464
+ ╠══════════════════════════════════════════════════════════════════════════════╣
465
+ ║ Session ID: ${this.session.id.padEnd(62)}║
466
+ ║ Lane: ${this.session.laneName.padEnd(62)}║
467
+ ║ Task: ${(this.session.taskName || '-').padEnd(62)}║
468
+ ║ Model: ${(this.session.model || 'default').padEnd(62)}║
469
+ ║ Started: ${new Date(this.session.startTime).toISOString().padEnd(62)}║
470
+ ╚══════════════════════════════════════════════════════════════════════════════╝
471
+
472
+ `;
473
+ this.writeToCleanLog(header);
474
+ if (this.config.keepRawLogs && this.rawLogFd !== null) {
475
+ fs.writeSync(this.rawLogFd, header);
476
+ }
477
+ // Write JSON session entry
478
+ this.writeJsonEntry({
479
+ timestamp: new Date(this.session.startTime).toISOString(),
480
+ level: 'session',
481
+ source: 'system',
482
+ lane: this.session.laneName,
483
+ task: this.session.taskName,
484
+ message: 'Session started',
485
+ metadata: {
486
+ sessionId: this.session.id,
487
+ model: this.session.model,
488
+ ...this.session.metadata,
489
+ },
490
+ });
491
+ }
492
+ /**
493
+ * Rotate log file if it exceeds max size
494
+ */
495
+ rotateIfNeeded(logPath, type) {
496
+ if (!fs.existsSync(logPath))
497
+ return;
498
+ try {
499
+ const stats = fs.statSync(logPath);
500
+ if (stats.size >= this.config.maxFileSize) {
501
+ this.rotateLog(logPath);
502
+ }
503
+ }
504
+ catch {
505
+ // File doesn't exist or can't be read, ignore
506
+ }
507
+ }
508
+ /**
509
+ * Rotate a log file
510
+ */
511
+ rotateLog(logPath) {
512
+ const dir = path.dirname(logPath);
513
+ const ext = path.extname(logPath);
514
+ const base = path.basename(logPath, ext);
515
+ // Shift existing rotated files
516
+ for (let i = this.config.maxFiles - 1; i >= 1; i--) {
517
+ const oldPath = path.join(dir, `${base}.${i}${ext}`);
518
+ const newPath = path.join(dir, `${base}.${i + 1}${ext}`);
519
+ if (fs.existsSync(oldPath)) {
520
+ if (i === this.config.maxFiles - 1) {
521
+ fs.unlinkSync(oldPath); // Delete oldest
522
+ }
523
+ else {
524
+ fs.renameSync(oldPath, newPath);
525
+ }
526
+ }
527
+ }
528
+ // Rotate current to .1
529
+ const rotatedPath = path.join(dir, `${base}.1${ext}`);
530
+ fs.renameSync(logPath, rotatedPath);
531
+ }
532
+ /**
533
+ * Write to clean log with size tracking
534
+ */
535
+ writeToCleanLog(data) {
536
+ if (this.cleanLogFd === null)
537
+ return;
538
+ const buffer = Buffer.from(data);
539
+ fs.writeSync(this.cleanLogFd, buffer);
540
+ this.cleanLogSize += buffer.length;
541
+ // Check if rotation needed
542
+ if (this.cleanLogSize >= this.config.maxFileSize) {
543
+ fs.closeSync(this.cleanLogFd);
544
+ this.rotateLog(this.cleanLogPath);
545
+ this.cleanLogFd = fs.openSync(this.cleanLogPath, 'a');
546
+ this.cleanLogSize = 0;
547
+ }
548
+ }
549
+ /**
550
+ * Write to raw log with size tracking
551
+ */
552
+ writeToRawLog(data) {
553
+ if (this.rawLogFd === null)
554
+ return;
555
+ const buffer = Buffer.from(data);
556
+ fs.writeSync(this.rawLogFd, buffer);
557
+ this.rawLogSize += buffer.length;
558
+ // Check if rotation needed
559
+ if (this.rawLogSize >= this.config.maxFileSize) {
560
+ fs.closeSync(this.rawLogFd);
561
+ this.rotateLog(this.rawLogPath);
562
+ this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
563
+ this.rawLogSize = 0;
564
+ }
565
+ }
566
+ /**
567
+ * Write a JSON log entry
568
+ */
569
+ writeJsonEntry(entry) {
570
+ if (this.jsonLogFd === null)
571
+ return;
572
+ const line = JSON.stringify(entry) + '\n';
573
+ fs.writeSync(this.jsonLogFd, line);
574
+ }
575
+ /**
576
+ * Write stdout data
577
+ */
578
+ writeStdout(data) {
579
+ const text = data.toString();
580
+ // Write raw log
581
+ if (this.config.keepRawLogs) {
582
+ this.writeToRawLog(text);
583
+ }
584
+ // Process through transform for clean log
585
+ if (this.cleanTransform) {
586
+ this.cleanTransform.write(data);
587
+ }
588
+ // Parse streaming JSON for readable log
589
+ this.parseStreamingData(text);
590
+ // Write JSON entry (for significant lines only)
591
+ if (this.config.writeJsonLog) {
592
+ const cleanText = stripAnsi(text).trim();
593
+ if (cleanText && !this.isNoiseLog(cleanText)) {
594
+ this.writeJsonEntry({
595
+ timestamp: new Date().toISOString(),
596
+ level: 'stdout',
597
+ lane: this.session.laneName,
598
+ task: this.session.taskName,
599
+ message: cleanText.substring(0, 1000), // Truncate very long lines
600
+ raw: this.config.keepRawLogs ? undefined : text.substring(0, 1000),
601
+ });
602
+ }
603
+ }
604
+ }
605
+ /**
606
+ * Parse streaming JSON data for readable log
607
+ */
608
+ parseStreamingData(text) {
609
+ if (!this.streamingParser)
610
+ return;
611
+ // Buffer incomplete lines
612
+ this.lineBuffer += text;
613
+ const lines = this.lineBuffer.split('\n');
614
+ // Keep the last incomplete line in buffer
615
+ this.lineBuffer = lines.pop() || '';
616
+ // Parse complete lines
617
+ for (const line of lines) {
618
+ const trimmed = line.trim();
619
+ if (trimmed.startsWith('{')) {
620
+ this.streamingParser.parseLine(trimmed);
621
+ }
622
+ }
623
+ }
624
+ /**
625
+ * Write stderr data
626
+ */
627
+ writeStderr(data) {
628
+ const text = data.toString();
629
+ // Write raw log
630
+ if (this.config.keepRawLogs) {
631
+ this.writeToRawLog(text);
632
+ }
633
+ // Process through transform for clean log
634
+ if (this.cleanTransform) {
635
+ this.cleanTransform.write(data);
636
+ }
637
+ // Write JSON entry
638
+ if (this.config.writeJsonLog) {
639
+ const cleanText = stripAnsi(text).trim();
640
+ if (cleanText) {
641
+ this.writeJsonEntry({
642
+ timestamp: new Date().toISOString(),
643
+ level: 'stderr',
644
+ lane: this.session.laneName,
645
+ task: this.session.taskName,
646
+ message: cleanText.substring(0, 1000),
647
+ });
648
+ }
649
+ }
650
+ }
651
+ /**
652
+ * Write a custom log entry
653
+ */
654
+ log(level, message, metadata) {
655
+ const ts = formatTimestamp(this.config.timestampFormat, this.session.startTime);
656
+ const prefix = level.toUpperCase().padEnd(5);
657
+ const line = `[${ts}] [${prefix}] ${message}\n`;
658
+ this.writeToCleanLog(line);
659
+ if (this.config.keepRawLogs) {
660
+ this.writeToRawLog(line);
661
+ }
662
+ if (this.config.writeJsonLog) {
663
+ this.writeJsonEntry({
664
+ timestamp: new Date().toISOString(),
665
+ level,
666
+ lane: this.session.laneName,
667
+ task: this.session.taskName,
668
+ message,
669
+ metadata,
670
+ });
671
+ }
672
+ }
673
+ /**
674
+ * Add a section marker
675
+ */
676
+ section(title) {
677
+ const divider = '═'.repeat(78);
678
+ const line = `\n${divider}\n ${title}\n${divider}\n`;
679
+ this.writeToCleanLog(line);
680
+ if (this.config.keepRawLogs) {
681
+ this.writeToRawLog(line);
682
+ }
683
+ }
684
+ /**
685
+ * Update task context
686
+ */
687
+ setTask(taskName, model) {
688
+ this.session.taskName = taskName;
689
+ if (model) {
690
+ this.session.model = model;
691
+ }
692
+ this.section(`Task: ${taskName}${model ? ` (Model: ${model})` : ''}`);
693
+ if (this.config.writeJsonLog) {
694
+ this.writeJsonEntry({
695
+ timestamp: new Date().toISOString(),
696
+ level: 'info',
697
+ source: 'system',
698
+ lane: this.session.laneName,
699
+ task: taskName,
700
+ message: `Task started: ${taskName}`,
701
+ metadata: { model },
702
+ });
703
+ }
704
+ }
705
+ /**
706
+ * Check if a log line is noise (progress bars, spinners, etc.)
707
+ */
708
+ isNoiseLog(text) {
709
+ // Skip empty or whitespace-only
710
+ if (!text.trim())
711
+ return true;
712
+ // Skip common progress/spinner patterns
713
+ const noisePatterns = [
714
+ /^[\s│├└─┌┐┘┴┬┤├]+$/, // Box drawing only
715
+ /^[.\s]+$/, // Dots only
716
+ /^[=>\s-]+$/, // Progress bar characters
717
+ /^\d+%$/, // Percentage only
718
+ /^⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/, // Spinner characters
719
+ ];
720
+ return noisePatterns.some(p => p.test(text));
721
+ }
722
+ /**
723
+ * Get paths to all log files
724
+ */
725
+ getLogPaths() {
726
+ return {
727
+ clean: this.cleanLogPath,
728
+ raw: this.config.keepRawLogs ? this.rawLogPath : undefined,
729
+ json: this.config.writeJsonLog ? this.jsonLogPath : undefined,
730
+ readable: this.readableLogPath,
731
+ };
732
+ }
733
+ /**
734
+ * Create file descriptors for process stdio redirection
735
+ */
736
+ getFileDescriptors() {
737
+ // For process spawning, use the raw log fd if available, otherwise clean
738
+ const fd = this.rawLogFd !== null ? this.rawLogFd : this.cleanLogFd;
739
+ return { stdout: fd, stderr: fd };
740
+ }
741
+ /**
742
+ * Close all log files
743
+ */
744
+ close() {
745
+ // Flush transform stream
746
+ if (this.cleanTransform) {
747
+ this.cleanTransform.end();
748
+ }
749
+ // Flush streaming parser
750
+ if (this.streamingParser) {
751
+ // Parse any remaining buffered data
752
+ if (this.lineBuffer.trim()) {
753
+ this.streamingParser.parseLine(this.lineBuffer);
754
+ }
755
+ this.streamingParser.flush();
756
+ }
757
+ // Write session end marker
758
+ const endMarker = `
759
+ ╔══════════════════════════════════════════════════════════════════════════════╗
760
+ ║ Session Ended: ${new Date().toISOString().padEnd(60)}║
761
+ ║ Duration: ${this.formatDuration(Date.now() - this.session.startTime).padEnd(65)}║
762
+ ╚══════════════════════════════════════════════════════════════════════════════╝
763
+
764
+ `;
765
+ if (this.cleanLogFd !== null) {
766
+ fs.writeSync(this.cleanLogFd, endMarker);
767
+ fs.closeSync(this.cleanLogFd);
768
+ this.cleanLogFd = null;
769
+ }
770
+ if (this.rawLogFd !== null) {
771
+ fs.writeSync(this.rawLogFd, endMarker);
772
+ fs.closeSync(this.rawLogFd);
773
+ this.rawLogFd = null;
774
+ }
775
+ if (this.jsonLogFd !== null) {
776
+ this.writeJsonEntry({
777
+ timestamp: new Date().toISOString(),
778
+ level: 'session',
779
+ source: 'system',
780
+ lane: this.session.laneName,
781
+ message: 'Session ended',
782
+ metadata: {
783
+ sessionId: this.session.id,
784
+ duration: Date.now() - this.session.startTime,
785
+ },
786
+ });
787
+ fs.closeSync(this.jsonLogFd);
788
+ this.jsonLogFd = null;
789
+ }
790
+ // Close readable log
791
+ if (this.readableLogFd !== null) {
792
+ fs.writeSync(this.readableLogFd, endMarker);
793
+ fs.closeSync(this.readableLogFd);
794
+ this.readableLogFd = null;
795
+ }
796
+ }
797
+ /**
798
+ * Format duration for display
799
+ */
800
+ formatDuration(ms) {
801
+ const seconds = Math.floor((ms / 1000) % 60);
802
+ const minutes = Math.floor((ms / (1000 * 60)) % 60);
803
+ const hours = Math.floor(ms / (1000 * 60 * 60));
804
+ if (hours > 0) {
805
+ return `${hours}h ${minutes}m ${seconds}s`;
806
+ }
807
+ else if (minutes > 0) {
808
+ return `${minutes}m ${seconds}s`;
809
+ }
810
+ return `${seconds}s`;
811
+ }
812
+ }
813
+ exports.EnhancedLogManager = EnhancedLogManager;
814
+ /**
815
+ * Create a log manager for a lane
816
+ */
817
+ function createLogManager(laneRunDir, laneName, config) {
818
+ const session = {
819
+ id: `${laneName}-${Date.now().toString(36)}`,
820
+ laneName,
821
+ startTime: Date.now(),
822
+ };
823
+ return new EnhancedLogManager(laneRunDir, session, config);
824
+ }
825
+ /**
826
+ * Read and parse JSON log file
827
+ */
828
+ function readJsonLog(logPath) {
829
+ if (!fs.existsSync(logPath)) {
830
+ return [];
831
+ }
832
+ try {
833
+ const content = fs.readFileSync(logPath, 'utf8');
834
+ return content
835
+ .split('\n')
836
+ .filter(line => line.trim())
837
+ .map(line => {
838
+ try {
839
+ return JSON.parse(line);
840
+ }
841
+ catch {
842
+ return null;
843
+ }
844
+ })
845
+ .filter((entry) => entry !== null);
846
+ }
847
+ catch {
848
+ return [];
849
+ }
850
+ }
851
+ /**
852
+ * Export logs to various formats
853
+ */
854
+ function exportLogs(laneRunDir, format, outputPath) {
855
+ const cleanLogPath = path.join(laneRunDir, 'terminal.log');
856
+ const jsonLogPath = path.join(laneRunDir, 'terminal.jsonl');
857
+ let output = '';
858
+ switch (format) {
859
+ case 'text':
860
+ if (fs.existsSync(cleanLogPath)) {
861
+ output = fs.readFileSync(cleanLogPath, 'utf8');
862
+ }
863
+ break;
864
+ case 'json':
865
+ const entries = readJsonLog(jsonLogPath);
866
+ output = JSON.stringify(entries, null, 2);
867
+ break;
868
+ case 'markdown':
869
+ output = exportToMarkdown(jsonLogPath, cleanLogPath);
870
+ break;
871
+ case 'html':
872
+ output = exportToHtml(jsonLogPath, cleanLogPath);
873
+ break;
874
+ }
875
+ if (outputPath) {
876
+ fs.writeFileSync(outputPath, output, 'utf8');
877
+ }
878
+ return output;
879
+ }
880
+ /**
881
+ * Export logs to Markdown format
882
+ */
883
+ function exportToMarkdown(jsonLogPath, cleanLogPath) {
884
+ const entries = readJsonLog(jsonLogPath);
885
+ let md = '# CursorFlow Session Log\n\n';
886
+ // Find session info
887
+ const sessionStart = entries.find(e => e.level === 'session' && e.message === 'Session started');
888
+ if (sessionStart?.metadata) {
889
+ md += '## Session Info\n\n';
890
+ md += `- **Session ID**: ${sessionStart.metadata.sessionId}\n`;
891
+ md += `- **Lane**: ${sessionStart.lane}\n`;
892
+ md += `- **Model**: ${sessionStart.metadata.model || 'default'}\n`;
893
+ md += `- **Started**: ${sessionStart.timestamp}\n\n`;
894
+ }
895
+ md += '## Log Entries\n\n';
896
+ // Group by task
897
+ const byTask = new Map();
898
+ for (const entry of entries) {
899
+ const task = entry.task || '(no task)';
900
+ if (!byTask.has(task)) {
901
+ byTask.set(task, []);
902
+ }
903
+ byTask.get(task).push(entry);
904
+ }
905
+ for (const [task, taskEntries] of byTask) {
906
+ md += `### Task: ${task}\n\n`;
907
+ md += '```\n';
908
+ for (const entry of taskEntries) {
909
+ if (entry.level !== 'session') {
910
+ md += `[${entry.timestamp}] [${entry.level.toUpperCase()}] ${entry.message}\n`;
911
+ }
912
+ }
913
+ md += '```\n\n';
914
+ }
915
+ return md;
916
+ }
917
+ /**
918
+ * Export logs to HTML format
919
+ */
920
+ function exportToHtml(jsonLogPath, cleanLogPath) {
921
+ const entries = readJsonLog(jsonLogPath);
922
+ let html = `<!DOCTYPE html>
923
+ <html>
924
+ <head>
925
+ <title>CursorFlow Session Log</title>
926
+ <style>
927
+ body { font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; margin: 20px; background: #1e1e1e; color: #d4d4d4; }
928
+ h1, h2 { color: #569cd6; }
929
+ .entry { padding: 4px 8px; margin: 2px 0; border-radius: 4px; }
930
+ .entry.stdout { background: #252526; }
931
+ .entry.stderr { background: #3c1f1f; color: #f48771; }
932
+ .entry.info { background: #1e3a5f; color: #9cdcfe; }
933
+ .entry.error { background: #5f1e1e; color: #f48771; }
934
+ .entry.session { background: #1e4620; color: #6a9955; }
935
+ .timestamp { color: #808080; font-size: 0.9em; }
936
+ .level { font-weight: bold; text-transform: uppercase; }
937
+ .task { color: #dcdcaa; }
938
+ pre { white-space: pre-wrap; word-wrap: break-word; }
939
+ </style>
940
+ </head>
941
+ <body>
942
+ <h1>CursorFlow Session Log</h1>
943
+ `;
944
+ for (const entry of entries) {
945
+ html += ` <div class="entry ${entry.level}">
946
+ <span class="timestamp">${entry.timestamp}</span>
947
+ <span class="level">[${entry.level}]</span>
948
+ ${entry.task ? `<span class="task">[${entry.task}]</span>` : ''}
949
+ <pre>${escapeHtml(entry.message)}</pre>
950
+ </div>\n`;
951
+ }
952
+ html += '</body></html>';
953
+ return html;
954
+ }
955
+ function escapeHtml(text) {
956
+ return text
957
+ .replace(/&/g, '&amp;')
958
+ .replace(/</g, '&lt;')
959
+ .replace(/>/g, '&gt;')
960
+ .replace(/"/g, '&quot;')
961
+ .replace(/'/g, '&#039;');
962
+ }
963
+ //# sourceMappingURL=enhanced-logger.js.map