@litmers/cursorflow-orchestrator 0.1.12 → 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.
- package/CHANGELOG.md +40 -0
- package/README.md +83 -2
- package/commands/cursorflow-clean.md +20 -6
- package/commands/cursorflow-prepare.md +1 -1
- package/commands/cursorflow-resume.md +127 -6
- package/commands/cursorflow-run.md +2 -2
- package/commands/cursorflow-signal.md +11 -4
- package/dist/cli/clean.js +164 -12
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +6 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.d.ts +8 -0
- package/dist/cli/logs.js +746 -0
- package/dist/cli/logs.js.map +1 -0
- package/dist/cli/monitor.js +113 -30
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +1 -1
- package/dist/cli/resume.js +367 -18
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +2 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +34 -20
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/orchestrator.d.ts +11 -1
- package/dist/core/orchestrator.js +257 -35
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.js +20 -0
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.js +113 -13
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +34 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +9 -2
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +209 -0
- package/dist/utils/enhanced-logger.js +963 -0
- package/dist/utils/enhanced-logger.js.map +1 -0
- package/dist/utils/events.d.ts +59 -0
- package/dist/utils/events.js +37 -0
- package/dist/utils/events.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.js +25 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/types.d.ts +122 -1
- package/dist/utils/webhook.d.ts +5 -0
- package/dist/utils/webhook.js +109 -0
- package/dist/utils/webhook.js.map +1 -0
- package/examples/README.md +1 -1
- package/package.json +1 -1
- package/scripts/simple-logging-test.sh +97 -0
- package/scripts/test-real-logging.sh +289 -0
- package/scripts/test-streaming-multi-task.sh +247 -0
- package/src/cli/clean.ts +170 -13
- package/src/cli/index.ts +4 -1
- package/src/cli/logs.ts +848 -0
- package/src/cli/monitor.ts +123 -30
- package/src/cli/prepare.ts +1 -1
- package/src/cli/resume.ts +463 -22
- package/src/cli/run.ts +2 -0
- package/src/cli/signal.ts +43 -27
- package/src/core/orchestrator.ts +303 -37
- package/src/core/reviewer.ts +22 -0
- package/src/core/runner.ts +128 -12
- package/src/utils/config.ts +36 -0
- package/src/utils/doctor.ts +12 -2
- package/src/utils/enhanced-logger.ts +1097 -0
- package/src/utils/events.ts +117 -0
- package/src/utils/git.ts +25 -0
- package/src/utils/types.ts +150 -1
- 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, '&')
|
|
958
|
+
.replace(/</g, '<')
|
|
959
|
+
.replace(/>/g, '>')
|
|
960
|
+
.replace(/"/g, '"')
|
|
961
|
+
.replace(/'/g, ''');
|
|
962
|
+
}
|
|
963
|
+
//# sourceMappingURL=enhanced-logger.js.map
|