@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,1030 @@
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
+ onParsedMessage;
348
+ constructor(logDir, session, config = {}, onParsedMessage) {
349
+ this.config = { ...exports.DEFAULT_LOG_CONFIG, ...config };
350
+ this.session = session;
351
+ this.logDir = logDir;
352
+ this.onParsedMessage = onParsedMessage;
353
+ // Ensure log directory exists
354
+ fs.mkdirSync(logDir, { recursive: true });
355
+ // Set up log file paths
356
+ this.cleanLogPath = path.join(logDir, 'terminal.log');
357
+ this.rawLogPath = path.join(logDir, 'terminal-raw.log');
358
+ this.jsonLogPath = path.join(logDir, 'terminal.jsonl');
359
+ this.readableLogPath = path.join(logDir, 'terminal-readable.log');
360
+ // Initialize log files
361
+ this.initLogFiles();
362
+ }
363
+ /**
364
+ * Initialize log files and write session headers
365
+ */
366
+ initLogFiles() {
367
+ // Check and rotate if necessary
368
+ this.rotateIfNeeded(this.cleanLogPath, 'clean');
369
+ if (this.config.keepRawLogs) {
370
+ this.rotateIfNeeded(this.rawLogPath, 'raw');
371
+ }
372
+ // Open file descriptors
373
+ this.cleanLogFd = fs.openSync(this.cleanLogPath, 'a');
374
+ if (this.config.keepRawLogs) {
375
+ this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
376
+ }
377
+ if (this.config.writeJsonLog) {
378
+ this.jsonLogFd = fs.openSync(this.jsonLogPath, 'a');
379
+ }
380
+ // Open readable log file (for parsed streaming output)
381
+ this.readableLogFd = fs.openSync(this.readableLogPath, 'a');
382
+ // Get initial file sizes
383
+ try {
384
+ this.cleanLogSize = fs.statSync(this.cleanLogPath).size;
385
+ if (this.config.keepRawLogs) {
386
+ this.rawLogSize = fs.statSync(this.rawLogPath).size;
387
+ }
388
+ }
389
+ catch {
390
+ this.cleanLogSize = 0;
391
+ this.rawLogSize = 0;
392
+ }
393
+ // Write session header
394
+ this.writeSessionHeader();
395
+ // Create transform stream
396
+ this.cleanTransform = new CleanLogTransform(this.config, this.session);
397
+ this.cleanTransform.on('data', (data) => {
398
+ this.writeToCleanLog(data);
399
+ });
400
+ // Create streaming parser for readable log
401
+ this.streamingParser = new StreamingMessageParser((msg) => {
402
+ this.writeReadableMessage(msg);
403
+ if (this.onParsedMessage) {
404
+ this.onParsedMessage(msg);
405
+ }
406
+ });
407
+ }
408
+ /**
409
+ * Write a parsed message to the readable log
410
+ */
411
+ writeReadableMessage(msg) {
412
+ if (this.readableLogFd === null)
413
+ return;
414
+ const ts = new Date(msg.timestamp).toISOString();
415
+ let formatted;
416
+ switch (msg.type) {
417
+ case 'system':
418
+ formatted = `[${ts}] ⚙️ SYSTEM: ${msg.content}\n`;
419
+ break;
420
+ case 'user':
421
+ case 'assistant':
422
+ case 'result':
423
+ // Format with brackets and line (compact)
424
+ const isUser = msg.type === 'user';
425
+ const isResult = msg.type === 'result';
426
+ const headerText = isUser ? '🧑 USER' : isResult ? '🤖 ASSISTANT (Final)' : '🤖 ASSISTANT';
427
+ const duration = msg.metadata?.duration_ms
428
+ ? ` (${Math.round(msg.metadata.duration_ms / 1000)}s)`
429
+ : '';
430
+ const label = `[ ${headerText}${duration} ] `;
431
+ const totalWidth = 80;
432
+ const topBorder = `┌─${label}${'─'.repeat(Math.max(0, totalWidth - label.length - 2))}`;
433
+ const bottomBorder = `└─${'─'.repeat(totalWidth - 2)}`;
434
+ const lines = msg.content.split('\n');
435
+ formatted = `[${ts}] ${topBorder}\n`;
436
+ for (const line of lines) {
437
+ formatted += `[${ts}] │ ${line}\n`;
438
+ }
439
+ formatted += `[${ts}] ${bottomBorder}\n`;
440
+ break;
441
+ case 'tool':
442
+ formatted = `[${ts}] 🔧 TOOL: ${msg.content}\n`;
443
+ break;
444
+ case 'tool_result':
445
+ const toolResultLines = msg.metadata?.lines ? ` (${msg.metadata.lines} lines)` : '';
446
+ formatted = `[${ts}] 📄 RESL: ${msg.metadata?.toolName || 'Tool'}${toolResultLines}\n`;
447
+ break;
448
+ default:
449
+ formatted = `[${ts}] ${msg.content}\n`;
450
+ }
451
+ try {
452
+ fs.writeSync(this.readableLogFd, formatted);
453
+ }
454
+ catch {
455
+ // Ignore write errors
456
+ }
457
+ }
458
+ /**
459
+ * Indent text with a prefix
460
+ */
461
+ indentText(text, prefix) {
462
+ return text.split('\n').map(line => prefix + line).join('\n');
463
+ }
464
+ /**
465
+ * Write session header to logs
466
+ */
467
+ writeSessionHeader() {
468
+ const header = `
469
+ ╔══════════════════════════════════════════════════════════════════════════════╗
470
+ ║ CursorFlow Session Log ║
471
+ ╠══════════════════════════════════════════════════════════════════════════════╣
472
+ ║ Session ID: ${this.session.id.padEnd(62)}║
473
+ ║ Lane: ${this.session.laneName.padEnd(62)}║
474
+ ║ Task: ${(this.session.taskName || '-').padEnd(62)}║
475
+ ║ Model: ${(this.session.model || 'default').padEnd(62)}║
476
+ ║ Started: ${new Date(this.session.startTime).toISOString().padEnd(62)}║
477
+ ╚══════════════════════════════════════════════════════════════════════════════╝
478
+
479
+ `;
480
+ this.writeToCleanLog(header);
481
+ if (this.config.keepRawLogs && this.rawLogFd !== null) {
482
+ fs.writeSync(this.rawLogFd, header);
483
+ }
484
+ // Write JSON session entry
485
+ this.writeJsonEntry({
486
+ timestamp: new Date(this.session.startTime).toISOString(),
487
+ level: 'session',
488
+ source: 'system',
489
+ lane: this.session.laneName,
490
+ task: this.session.taskName,
491
+ message: 'Session started',
492
+ metadata: {
493
+ sessionId: this.session.id,
494
+ model: this.session.model,
495
+ ...this.session.metadata,
496
+ },
497
+ });
498
+ }
499
+ /**
500
+ * Rotate log file if it exceeds max size
501
+ */
502
+ rotateIfNeeded(logPath, type) {
503
+ if (!fs.existsSync(logPath))
504
+ return;
505
+ try {
506
+ const stats = fs.statSync(logPath);
507
+ if (stats.size >= this.config.maxFileSize) {
508
+ this.rotateLog(logPath);
509
+ }
510
+ }
511
+ catch {
512
+ // File doesn't exist or can't be read, ignore
513
+ }
514
+ }
515
+ /**
516
+ * Rotate a log file
517
+ */
518
+ rotateLog(logPath) {
519
+ const dir = path.dirname(logPath);
520
+ const ext = path.extname(logPath);
521
+ const base = path.basename(logPath, ext);
522
+ // Shift existing rotated files
523
+ for (let i = this.config.maxFiles - 1; i >= 1; i--) {
524
+ const oldPath = path.join(dir, `${base}.${i}${ext}`);
525
+ const newPath = path.join(dir, `${base}.${i + 1}${ext}`);
526
+ if (fs.existsSync(oldPath)) {
527
+ if (i === this.config.maxFiles - 1) {
528
+ fs.unlinkSync(oldPath); // Delete oldest
529
+ }
530
+ else {
531
+ fs.renameSync(oldPath, newPath);
532
+ }
533
+ }
534
+ }
535
+ // Rotate current to .1
536
+ const rotatedPath = path.join(dir, `${base}.1${ext}`);
537
+ fs.renameSync(logPath, rotatedPath);
538
+ }
539
+ /**
540
+ * Write to clean log with size tracking
541
+ */
542
+ writeToCleanLog(data) {
543
+ if (this.cleanLogFd === null)
544
+ return;
545
+ const buffer = Buffer.from(data);
546
+ fs.writeSync(this.cleanLogFd, buffer);
547
+ this.cleanLogSize += buffer.length;
548
+ // Check if rotation needed
549
+ if (this.cleanLogSize >= this.config.maxFileSize) {
550
+ fs.closeSync(this.cleanLogFd);
551
+ this.rotateLog(this.cleanLogPath);
552
+ this.cleanLogFd = fs.openSync(this.cleanLogPath, 'a');
553
+ this.cleanLogSize = 0;
554
+ }
555
+ }
556
+ /**
557
+ * Write to raw log with size tracking
558
+ */
559
+ writeToRawLog(data) {
560
+ if (this.rawLogFd === null)
561
+ return;
562
+ const buffer = Buffer.from(data);
563
+ fs.writeSync(this.rawLogFd, buffer);
564
+ this.rawLogSize += buffer.length;
565
+ // Check if rotation needed
566
+ if (this.rawLogSize >= this.config.maxFileSize) {
567
+ fs.closeSync(this.rawLogFd);
568
+ this.rotateLog(this.rawLogPath);
569
+ this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
570
+ this.rawLogSize = 0;
571
+ }
572
+ }
573
+ /**
574
+ * Write a JSON log entry
575
+ */
576
+ writeJsonEntry(entry) {
577
+ if (this.jsonLogFd === null)
578
+ return;
579
+ const line = JSON.stringify(entry) + '\n';
580
+ fs.writeSync(this.jsonLogFd, line);
581
+ }
582
+ /**
583
+ * Write stdout data
584
+ */
585
+ writeStdout(data) {
586
+ const text = data.toString();
587
+ // Write raw log
588
+ if (this.config.keepRawLogs) {
589
+ this.writeToRawLog(text);
590
+ }
591
+ // Process through transform for clean log
592
+ if (this.cleanTransform) {
593
+ this.cleanTransform.write(data);
594
+ }
595
+ // Parse streaming JSON for readable log (handles boxes, messages, tool calls)
596
+ this.parseStreamingData(text);
597
+ // Also include significant info/status lines in readable log (compact)
598
+ if (this.readableLogFd !== null) {
599
+ const lines = text.split('\n');
600
+ for (const line of lines) {
601
+ const cleanLine = stripAnsi(line).trim();
602
+ // Look for log lines: [ISO_DATE] [LEVEL] ...
603
+ if (cleanLine &&
604
+ !cleanLine.startsWith('{') &&
605
+ !this.isNoiseLog(cleanLine) &&
606
+ /\[\d{4}-\d{2}-\d{2}T/.test(cleanLine)) {
607
+ try {
608
+ // Check if it has a level marker
609
+ if (/\[(INFO|WARN|ERROR|SUCCESS|DEBUG)\]/.test(cleanLine)) {
610
+ // Special formatting for summary
611
+ if (cleanLine.includes('Final Workspace Summary')) {
612
+ const tsMatch = cleanLine.match(/\[(\d{4}-\d{2}-\d{2}T[^\]]+)\]/);
613
+ const ts = tsMatch ? tsMatch[1] : new Date().toISOString();
614
+ fs.writeSync(this.readableLogFd, `[${ts}] 📊 SUMMARY: ${cleanLine.split(']').slice(2).join(']').trim()}\n`);
615
+ }
616
+ else {
617
+ fs.writeSync(this.readableLogFd, `${cleanLine}\n`);
618
+ }
619
+ }
620
+ }
621
+ catch { }
622
+ }
623
+ }
624
+ }
625
+ // Write JSON entry (for significant lines only)
626
+ if (this.config.writeJsonLog) {
627
+ const cleanText = stripAnsi(text).trim();
628
+ if (cleanText && !this.isNoiseLog(cleanText)) {
629
+ this.writeJsonEntry({
630
+ timestamp: new Date().toISOString(),
631
+ level: 'stdout',
632
+ lane: this.session.laneName,
633
+ task: this.session.taskName,
634
+ message: cleanText.substring(0, 1000), // Truncate very long lines
635
+ raw: this.config.keepRawLogs ? undefined : text.substring(0, 1000),
636
+ });
637
+ }
638
+ }
639
+ }
640
+ /**
641
+ * Parse streaming JSON data for readable log
642
+ */
643
+ parseStreamingData(text) {
644
+ if (!this.streamingParser)
645
+ return;
646
+ // Buffer incomplete lines
647
+ this.lineBuffer += text;
648
+ const lines = this.lineBuffer.split('\n');
649
+ // Keep the last incomplete line in buffer
650
+ this.lineBuffer = lines.pop() || '';
651
+ // Parse complete lines
652
+ for (const line of lines) {
653
+ const trimmed = line.trim();
654
+ if (trimmed.startsWith('{')) {
655
+ this.streamingParser.parseLine(trimmed);
656
+ }
657
+ }
658
+ }
659
+ /**
660
+ * Write stderr data
661
+ */
662
+ writeStderr(data) {
663
+ const text = data.toString();
664
+ // Write raw log
665
+ if (this.config.keepRawLogs) {
666
+ this.writeToRawLog(text);
667
+ }
668
+ // Process through transform for clean log
669
+ if (this.cleanTransform) {
670
+ this.cleanTransform.write(data);
671
+ }
672
+ // Also include error lines in readable log (compact)
673
+ if (this.readableLogFd !== null) {
674
+ const lines = text.split('\n');
675
+ for (const line of lines) {
676
+ const cleanLine = stripAnsi(line).trim();
677
+ if (cleanLine && !this.isNoiseLog(cleanLine)) {
678
+ try {
679
+ const ts = new Date().toISOString();
680
+ fs.writeSync(this.readableLogFd, `[${ts}] ❌ STDERR: ${cleanLine}\n`);
681
+ }
682
+ catch { }
683
+ }
684
+ }
685
+ }
686
+ // Write JSON entry
687
+ if (this.config.writeJsonLog) {
688
+ const cleanText = stripAnsi(text).trim();
689
+ if (cleanText) {
690
+ this.writeJsonEntry({
691
+ timestamp: new Date().toISOString(),
692
+ level: 'stderr',
693
+ lane: this.session.laneName,
694
+ task: this.session.taskName,
695
+ message: cleanText.substring(0, 1000),
696
+ });
697
+ }
698
+ }
699
+ }
700
+ /**
701
+ * Write a custom log entry
702
+ */
703
+ log(level, message, metadata) {
704
+ const ts = formatTimestamp(this.config.timestampFormat, this.session.startTime);
705
+ const prefix = level.toUpperCase().padEnd(5);
706
+ const line = `[${ts}] [${prefix}] ${message}\n`;
707
+ this.writeToCleanLog(line);
708
+ if (this.config.keepRawLogs) {
709
+ this.writeToRawLog(line);
710
+ }
711
+ // Write to readable log (compact)
712
+ if (this.readableLogFd !== null) {
713
+ const typeLabel = level === 'error' ? '❌ ERROR' : level === 'info' ? 'ℹ️ INFO' : '🔍 DEBUG';
714
+ const formatted = `${new Date().toISOString()} ${typeLabel}: ${message}\n`;
715
+ try {
716
+ fs.writeSync(this.readableLogFd, formatted);
717
+ }
718
+ catch { }
719
+ }
720
+ if (this.config.writeJsonLog) {
721
+ this.writeJsonEntry({
722
+ timestamp: new Date().toISOString(),
723
+ level,
724
+ lane: this.session.laneName,
725
+ task: this.session.taskName,
726
+ message,
727
+ metadata,
728
+ });
729
+ }
730
+ }
731
+ /**
732
+ * Add a section marker
733
+ */
734
+ section(title) {
735
+ const divider = '═'.repeat(78);
736
+ const line = `\n${divider}\n ${title}\n${divider}\n`;
737
+ this.writeToCleanLog(line);
738
+ if (this.config.keepRawLogs) {
739
+ this.writeToRawLog(line);
740
+ }
741
+ // Write to readable log (compact)
742
+ if (this.readableLogFd !== null) {
743
+ const ts = new Date().toISOString();
744
+ const formatted = `[${ts}] ━━━ ${title} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
745
+ try {
746
+ fs.writeSync(this.readableLogFd, formatted);
747
+ }
748
+ catch { }
749
+ }
750
+ }
751
+ /**
752
+ * Update task context
753
+ */
754
+ setTask(taskName, model) {
755
+ this.session.taskName = taskName;
756
+ if (model) {
757
+ this.session.model = model;
758
+ }
759
+ this.section(`Task: ${taskName}${model ? ` (Model: ${model})` : ''}`);
760
+ if (this.config.writeJsonLog) {
761
+ this.writeJsonEntry({
762
+ timestamp: new Date().toISOString(),
763
+ level: 'info',
764
+ source: 'system',
765
+ lane: this.session.laneName,
766
+ task: taskName,
767
+ message: `Task started: ${taskName}`,
768
+ metadata: { model },
769
+ });
770
+ }
771
+ }
772
+ /**
773
+ * Check if a log line is noise (progress bars, spinners, etc.)
774
+ */
775
+ isNoiseLog(text) {
776
+ // Skip empty or whitespace-only
777
+ if (!text.trim())
778
+ return true;
779
+ // Skip common progress/spinner patterns
780
+ const noisePatterns = [
781
+ /^[\s│├└─┌┐┘┴┬┤├]+$/, // Box drawing only
782
+ /^[.\s]+$/, // Dots only
783
+ /^[=>\s-]+$/, // Progress bar characters
784
+ /^\d+%$/, // Percentage only
785
+ /^⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/, // Spinner characters
786
+ ];
787
+ return noisePatterns.some(p => p.test(text));
788
+ }
789
+ /**
790
+ * Get paths to all log files
791
+ */
792
+ getLogPaths() {
793
+ return {
794
+ clean: this.cleanLogPath,
795
+ raw: this.config.keepRawLogs ? this.rawLogPath : undefined,
796
+ json: this.config.writeJsonLog ? this.jsonLogPath : undefined,
797
+ readable: this.readableLogPath,
798
+ };
799
+ }
800
+ /**
801
+ * Create file descriptors for process stdio redirection
802
+ */
803
+ getFileDescriptors() {
804
+ // For process spawning, use the raw log fd if available, otherwise clean
805
+ const fd = this.rawLogFd !== null ? this.rawLogFd : this.cleanLogFd;
806
+ return { stdout: fd, stderr: fd };
807
+ }
808
+ /**
809
+ * Close all log files
810
+ */
811
+ close() {
812
+ // Flush transform stream
813
+ if (this.cleanTransform) {
814
+ this.cleanTransform.end();
815
+ }
816
+ // Flush streaming parser
817
+ if (this.streamingParser) {
818
+ // Parse any remaining buffered data
819
+ if (this.lineBuffer.trim()) {
820
+ this.streamingParser.parseLine(this.lineBuffer);
821
+ }
822
+ this.streamingParser.flush();
823
+ }
824
+ // Write session end marker
825
+ const endMarker = `
826
+ ╔══════════════════════════════════════════════════════════════════════════════╗
827
+ ║ Session Ended: ${new Date().toISOString().padEnd(60)}║
828
+ ║ Duration: ${this.formatDuration(Date.now() - this.session.startTime).padEnd(65)}║
829
+ ╚══════════════════════════════════════════════════════════════════════════════╝
830
+
831
+ `;
832
+ if (this.cleanLogFd !== null) {
833
+ fs.writeSync(this.cleanLogFd, endMarker);
834
+ fs.closeSync(this.cleanLogFd);
835
+ this.cleanLogFd = null;
836
+ }
837
+ if (this.rawLogFd !== null) {
838
+ fs.writeSync(this.rawLogFd, endMarker);
839
+ fs.closeSync(this.rawLogFd);
840
+ this.rawLogFd = null;
841
+ }
842
+ if (this.jsonLogFd !== null) {
843
+ this.writeJsonEntry({
844
+ timestamp: new Date().toISOString(),
845
+ level: 'session',
846
+ source: 'system',
847
+ lane: this.session.laneName,
848
+ message: 'Session ended',
849
+ metadata: {
850
+ sessionId: this.session.id,
851
+ duration: Date.now() - this.session.startTime,
852
+ },
853
+ });
854
+ fs.closeSync(this.jsonLogFd);
855
+ this.jsonLogFd = null;
856
+ }
857
+ // Close readable log
858
+ if (this.readableLogFd !== null) {
859
+ fs.writeSync(this.readableLogFd, endMarker);
860
+ fs.closeSync(this.readableLogFd);
861
+ this.readableLogFd = null;
862
+ }
863
+ }
864
+ /**
865
+ * Format duration for display
866
+ */
867
+ formatDuration(ms) {
868
+ const seconds = Math.floor((ms / 1000) % 60);
869
+ const minutes = Math.floor((ms / (1000 * 60)) % 60);
870
+ const hours = Math.floor(ms / (1000 * 60 * 60));
871
+ if (hours > 0) {
872
+ return `${hours}h ${minutes}m ${seconds}s`;
873
+ }
874
+ else if (minutes > 0) {
875
+ return `${minutes}m ${seconds}s`;
876
+ }
877
+ return `${seconds}s`;
878
+ }
879
+ }
880
+ exports.EnhancedLogManager = EnhancedLogManager;
881
+ /**
882
+ * Create a log manager for a lane
883
+ */
884
+ function createLogManager(laneRunDir, laneName, config, onParsedMessage) {
885
+ const session = {
886
+ id: `${laneName}-${Date.now().toString(36)}`,
887
+ laneName,
888
+ startTime: Date.now(),
889
+ };
890
+ return new EnhancedLogManager(laneRunDir, session, config, onParsedMessage);
891
+ }
892
+ /**
893
+ * Read and parse JSON log file
894
+ */
895
+ function readJsonLog(logPath) {
896
+ if (!fs.existsSync(logPath)) {
897
+ return [];
898
+ }
899
+ try {
900
+ const content = fs.readFileSync(logPath, 'utf8');
901
+ return content
902
+ .split('\n')
903
+ .filter(line => line.trim())
904
+ .map(line => {
905
+ try {
906
+ return JSON.parse(line);
907
+ }
908
+ catch {
909
+ return null;
910
+ }
911
+ })
912
+ .filter((entry) => entry !== null);
913
+ }
914
+ catch {
915
+ return [];
916
+ }
917
+ }
918
+ /**
919
+ * Export logs to various formats
920
+ */
921
+ function exportLogs(laneRunDir, format, outputPath) {
922
+ const cleanLogPath = path.join(laneRunDir, 'terminal.log');
923
+ const jsonLogPath = path.join(laneRunDir, 'terminal.jsonl');
924
+ let output = '';
925
+ switch (format) {
926
+ case 'text':
927
+ if (fs.existsSync(cleanLogPath)) {
928
+ output = fs.readFileSync(cleanLogPath, 'utf8');
929
+ }
930
+ break;
931
+ case 'json':
932
+ const entries = readJsonLog(jsonLogPath);
933
+ output = JSON.stringify(entries, null, 2);
934
+ break;
935
+ case 'markdown':
936
+ output = exportToMarkdown(jsonLogPath, cleanLogPath);
937
+ break;
938
+ case 'html':
939
+ output = exportToHtml(jsonLogPath, cleanLogPath);
940
+ break;
941
+ }
942
+ if (outputPath) {
943
+ fs.writeFileSync(outputPath, output, 'utf8');
944
+ }
945
+ return output;
946
+ }
947
+ /**
948
+ * Export logs to Markdown format
949
+ */
950
+ function exportToMarkdown(jsonLogPath, cleanLogPath) {
951
+ const entries = readJsonLog(jsonLogPath);
952
+ let md = '# CursorFlow Session Log\n\n';
953
+ // Find session info
954
+ const sessionStart = entries.find(e => e.level === 'session' && e.message === 'Session started');
955
+ if (sessionStart?.metadata) {
956
+ md += '## Session Info\n\n';
957
+ md += `- **Session ID**: ${sessionStart.metadata.sessionId}\n`;
958
+ md += `- **Lane**: ${sessionStart.lane}\n`;
959
+ md += `- **Model**: ${sessionStart.metadata.model || 'default'}\n`;
960
+ md += `- **Started**: ${sessionStart.timestamp}\n\n`;
961
+ }
962
+ md += '## Log Entries\n\n';
963
+ // Group by task
964
+ const byTask = new Map();
965
+ for (const entry of entries) {
966
+ const task = entry.task || '(no task)';
967
+ if (!byTask.has(task)) {
968
+ byTask.set(task, []);
969
+ }
970
+ byTask.get(task).push(entry);
971
+ }
972
+ for (const [task, taskEntries] of byTask) {
973
+ md += `### Task: ${task}\n\n`;
974
+ md += '```\n';
975
+ for (const entry of taskEntries) {
976
+ if (entry.level !== 'session') {
977
+ md += `[${entry.timestamp}] [${entry.level.toUpperCase()}] ${entry.message}\n`;
978
+ }
979
+ }
980
+ md += '```\n\n';
981
+ }
982
+ return md;
983
+ }
984
+ /**
985
+ * Export logs to HTML format
986
+ */
987
+ function exportToHtml(jsonLogPath, cleanLogPath) {
988
+ const entries = readJsonLog(jsonLogPath);
989
+ let html = `<!DOCTYPE html>
990
+ <html>
991
+ <head>
992
+ <title>CursorFlow Session Log</title>
993
+ <style>
994
+ body { font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; margin: 20px; background: #1e1e1e; color: #d4d4d4; }
995
+ h1, h2 { color: #569cd6; }
996
+ .entry { padding: 4px 8px; margin: 2px 0; border-radius: 4px; }
997
+ .entry.stdout { background: #252526; }
998
+ .entry.stderr { background: #3c1f1f; color: #f48771; }
999
+ .entry.info { background: #1e3a5f; color: #9cdcfe; }
1000
+ .entry.error { background: #5f1e1e; color: #f48771; }
1001
+ .entry.session { background: #1e4620; color: #6a9955; }
1002
+ .timestamp { color: #808080; font-size: 0.9em; }
1003
+ .level { font-weight: bold; text-transform: uppercase; }
1004
+ .task { color: #dcdcaa; }
1005
+ pre { white-space: pre-wrap; word-wrap: break-word; }
1006
+ </style>
1007
+ </head>
1008
+ <body>
1009
+ <h1>CursorFlow Session Log</h1>
1010
+ `;
1011
+ for (const entry of entries) {
1012
+ html += ` <div class="entry ${entry.level}">
1013
+ <span class="timestamp">${entry.timestamp}</span>
1014
+ <span class="level">[${entry.level}]</span>
1015
+ ${entry.task ? `<span class="task">[${entry.task}]</span>` : ''}
1016
+ <pre>${escapeHtml(entry.message)}</pre>
1017
+ </div>\n`;
1018
+ }
1019
+ html += '</body></html>';
1020
+ return html;
1021
+ }
1022
+ function escapeHtml(text) {
1023
+ return text
1024
+ .replace(/&/g, '&amp;')
1025
+ .replace(/</g, '&lt;')
1026
+ .replace(/>/g, '&gt;')
1027
+ .replace(/"/g, '&quot;')
1028
+ .replace(/'/g, '&#039;');
1029
+ }
1030
+ //# sourceMappingURL=enhanced-logger.js.map