@litmers/cursorflow-orchestrator 0.1.31 → 0.1.34
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/README.md +144 -52
- package/commands/cursorflow-add.md +159 -0
- package/commands/cursorflow-monitor.md +23 -2
- package/commands/cursorflow-new.md +87 -0
- package/dist/cli/add.d.ts +7 -0
- package/dist/cli/add.js +377 -0
- package/dist/cli/add.js.map +1 -0
- package/dist/cli/clean.js +1 -0
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/config.d.ts +7 -0
- package/dist/cli/config.js +181 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.js +34 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.js +7 -33
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +51 -62
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/new.d.ts +7 -0
- package/dist/cli/new.js +232 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/prepare.js +95 -193
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +11 -47
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +27 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tasks.js +1 -2
- package/dist/cli/tasks.js.map +1 -1
- package/dist/core/failure-policy.d.ts +9 -0
- package/dist/core/failure-policy.js +9 -0
- package/dist/core/failure-policy.js.map +1 -1
- package/dist/core/orchestrator.d.ts +20 -6
- package/dist/core/orchestrator.js +213 -333
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.d.ts +27 -0
- package/dist/core/runner/agent.js +294 -0
- package/dist/core/runner/agent.js.map +1 -0
- package/dist/core/runner/index.d.ts +5 -0
- package/dist/core/runner/index.js +22 -0
- package/dist/core/runner/index.js.map +1 -0
- package/dist/core/runner/pipeline.d.ts +9 -0
- package/dist/core/runner/pipeline.js +539 -0
- package/dist/core/runner/pipeline.js.map +1 -0
- package/dist/core/runner/prompt.d.ts +25 -0
- package/dist/core/runner/prompt.js +175 -0
- package/dist/core/runner/prompt.js.map +1 -0
- package/dist/core/runner/task.d.ts +26 -0
- package/dist/core/runner/task.js +283 -0
- package/dist/core/runner/task.js.map +1 -0
- package/dist/core/runner/utils.d.ts +37 -0
- package/dist/core/runner/utils.js +161 -0
- package/dist/core/runner/utils.js.map +1 -0
- package/dist/core/runner.d.ts +2 -96
- package/dist/core/runner.js +11 -1136
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +326 -0
- package/dist/core/stall-detection.js +781 -0
- package/dist/core/stall-detection.js.map +1 -0
- package/dist/types/config.d.ts +6 -6
- package/dist/types/flow.d.ts +84 -0
- package/dist/types/flow.js +10 -0
- package/dist/types/flow.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +3 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/lane.d.ts +0 -2
- package/dist/types/logging.d.ts +5 -1
- package/dist/types/task.d.ts +7 -11
- package/dist/utils/config.js +7 -15
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/dependency.d.ts +36 -1
- package/dist/utils/dependency.js +256 -1
- package/dist/utils/dependency.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +45 -82
- package/dist/utils/enhanced-logger.js +238 -844
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +29 -0
- package/dist/utils/git.js +115 -5
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/state.js +0 -2
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +2 -2
- package/dist/utils/task-service.js +40 -31
- package/dist/utils/task-service.js.map +1 -1
- package/package.json +4 -3
- package/src/cli/add.ts +397 -0
- package/src/cli/clean.ts +1 -0
- package/src/cli/config.ts +177 -0
- package/src/cli/index.ts +36 -32
- package/src/cli/logs.ts +7 -31
- package/src/cli/monitor.ts +55 -71
- package/src/cli/new.ts +235 -0
- package/src/cli/prepare.ts +98 -205
- package/src/cli/resume.ts +13 -56
- package/src/cli/run.ts +311 -306
- package/src/cli/tasks.ts +1 -2
- package/src/core/failure-policy.ts +9 -0
- package/src/core/orchestrator.ts +277 -378
- package/src/core/runner/agent.ts +314 -0
- package/src/core/runner/index.ts +6 -0
- package/src/core/runner/pipeline.ts +567 -0
- package/src/core/runner/prompt.ts +174 -0
- package/src/core/runner/task.ts +320 -0
- package/src/core/runner/utils.ts +142 -0
- package/src/core/runner.ts +8 -1347
- package/src/core/stall-detection.ts +936 -0
- package/src/types/config.ts +6 -6
- package/src/types/flow.ts +91 -0
- package/src/types/index.ts +15 -3
- package/src/types/lane.ts +0 -2
- package/src/types/logging.ts +5 -1
- package/src/types/task.ts +7 -11
- package/src/utils/config.ts +8 -16
- package/src/utils/dependency.ts +311 -2
- package/src/utils/enhanced-logger.ts +263 -927
- package/src/utils/git.ts +145 -5
- package/src/utils/state.ts +0 -2
- package/src/utils/task-service.ts +48 -40
- package/commands/cursorflow-review.md +0 -56
- package/commands/cursorflow-runs.md +0 -59
- package/dist/cli/runs.d.ts +0 -5
- package/dist/cli/runs.js +0 -214
- package/dist/cli/runs.js.map +0 -1
- package/dist/core/reviewer.d.ts +0 -66
- package/dist/core/reviewer.js +0 -265
- package/dist/core/reviewer.js.map +0 -1
- package/src/cli/runs.ts +0 -212
- package/src/core/reviewer.ts +0 -285
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enhanced Logger -
|
|
2
|
+
* Enhanced Logger - Simplified terminal output capture
|
|
3
3
|
*
|
|
4
4
|
* Features:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* - Log rotation and size management
|
|
8
|
-
* - Session headers with context
|
|
9
|
-
* - Raw and clean log file options
|
|
10
|
-
* - Structured JSON logs for programmatic access
|
|
11
|
-
* - Streaming output support for real-time capture
|
|
5
|
+
* - Raw log: Original output as-is
|
|
6
|
+
* - Readable log: Formatted with formatMessageForConsole style
|
|
12
7
|
*/
|
|
13
8
|
|
|
14
9
|
import * as fs from 'fs';
|
|
15
10
|
import * as path from 'path';
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import { EnhancedLogConfig, ParsedMessage, JsonLogEntry, LogSession } from '../types';
|
|
19
|
-
export { EnhancedLogConfig, ParsedMessage, JsonLogEntry, LogSession };
|
|
11
|
+
import { EnhancedLogConfig, ParsedMessage, LogSession } from '../types';
|
|
12
|
+
import { formatMessageForConsole } from './log-formatter';
|
|
20
13
|
import { safeJoin } from './path';
|
|
21
14
|
|
|
15
|
+
export { EnhancedLogConfig, ParsedMessage, LogSession };
|
|
16
|
+
|
|
17
|
+
// Re-export JsonLogEntry for backward compatibility (empty type)
|
|
18
|
+
export interface JsonLogEntry {
|
|
19
|
+
timestamp: string;
|
|
20
|
+
level: string;
|
|
21
|
+
lane?: string;
|
|
22
|
+
task?: string;
|
|
23
|
+
message: string;
|
|
24
|
+
metadata?: Record<string, any>;
|
|
25
|
+
source?: string;
|
|
26
|
+
raw?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
export const DEFAULT_LOG_CONFIG: EnhancedLogConfig = {
|
|
23
30
|
enabled: true,
|
|
24
31
|
stripAnsi: true,
|
|
@@ -28,199 +35,14 @@ export const DEFAULT_LOG_CONFIG: EnhancedLogConfig = {
|
|
|
28
35
|
keepRawLogs: true,
|
|
29
36
|
keepAbsoluteRawLogs: false,
|
|
30
37
|
raw: false,
|
|
31
|
-
writeJsonLog:
|
|
38
|
+
writeJsonLog: false, // Disabled by default now
|
|
32
39
|
timestampFormat: 'iso',
|
|
33
40
|
};
|
|
34
41
|
|
|
35
|
-
/**
|
|
36
|
-
* Streaming JSON Parser - Parses cursor-agent stream-json output
|
|
37
|
-
* and combines tokens into readable messages
|
|
38
|
-
*/
|
|
39
|
-
export class StreamingMessageParser {
|
|
40
|
-
private currentMessage: string = '';
|
|
41
|
-
private currentRole: string = '';
|
|
42
|
-
private messageStartTime: number = 0;
|
|
43
|
-
private onMessage: (msg: ParsedMessage) => void;
|
|
44
|
-
|
|
45
|
-
constructor(onMessage: (msg: ParsedMessage) => void) {
|
|
46
|
-
this.onMessage = onMessage;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Parse a line of JSON output from cursor-agent
|
|
51
|
-
*/
|
|
52
|
-
parseLine(line: string): void {
|
|
53
|
-
const trimmed = line.trim();
|
|
54
|
-
if (!trimmed || !trimmed.startsWith('{')) return;
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const json = JSON.parse(trimmed);
|
|
58
|
-
this.handleJsonMessage(json);
|
|
59
|
-
} catch {
|
|
60
|
-
// Not valid JSON, ignore
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private handleJsonMessage(json: any): void {
|
|
65
|
-
const type = json.type;
|
|
66
|
-
|
|
67
|
-
switch (type) {
|
|
68
|
-
case 'system':
|
|
69
|
-
// System init message
|
|
70
|
-
this.emitMessage({
|
|
71
|
-
type: 'system',
|
|
72
|
-
role: 'system',
|
|
73
|
-
content: `[System] Model: ${json.model || 'unknown'}, Mode: ${json.permissionMode || 'default'}`,
|
|
74
|
-
timestamp: json.timestamp_ms || Date.now(),
|
|
75
|
-
});
|
|
76
|
-
break;
|
|
77
|
-
|
|
78
|
-
case 'user':
|
|
79
|
-
// User message - emit as complete message
|
|
80
|
-
if (json.message?.content) {
|
|
81
|
-
const textContent = json.message.content
|
|
82
|
-
.filter((c: any) => c.type === 'text')
|
|
83
|
-
.map((c: any) => c.text)
|
|
84
|
-
.join('');
|
|
85
|
-
|
|
86
|
-
this.emitMessage({
|
|
87
|
-
type: 'user',
|
|
88
|
-
role: 'user',
|
|
89
|
-
content: textContent,
|
|
90
|
-
timestamp: json.timestamp_ms || Date.now(),
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
break;
|
|
94
|
-
|
|
95
|
-
case 'assistant':
|
|
96
|
-
// Streaming assistant message - accumulate tokens
|
|
97
|
-
if (json.message?.content) {
|
|
98
|
-
const textContent = json.message.content
|
|
99
|
-
.filter((c: any) => c.type === 'text')
|
|
100
|
-
.map((c: any) => c.text)
|
|
101
|
-
.join('');
|
|
102
|
-
|
|
103
|
-
// Check if this is a new message or continuation
|
|
104
|
-
if (this.currentRole !== 'assistant') {
|
|
105
|
-
// Flush previous message if any
|
|
106
|
-
this.flush();
|
|
107
|
-
this.currentRole = 'assistant';
|
|
108
|
-
this.messageStartTime = json.timestamp_ms || Date.now();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
this.currentMessage += textContent;
|
|
112
|
-
}
|
|
113
|
-
break;
|
|
114
|
-
|
|
115
|
-
case 'tool_call':
|
|
116
|
-
// Tool call - emit as formatted message
|
|
117
|
-
if (json.subtype === 'started' && json.tool_call) {
|
|
118
|
-
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
119
|
-
const toolArgs = json.tool_call[toolName]?.args || {};
|
|
120
|
-
|
|
121
|
-
this.flush(); // Flush any pending assistant message
|
|
122
|
-
|
|
123
|
-
this.emitMessage({
|
|
124
|
-
type: 'tool',
|
|
125
|
-
role: 'tool',
|
|
126
|
-
content: `[Tool: ${toolName}] ${JSON.stringify(toolArgs)}`,
|
|
127
|
-
timestamp: json.timestamp_ms || Date.now(),
|
|
128
|
-
metadata: { callId: json.call_id, toolName },
|
|
129
|
-
});
|
|
130
|
-
} else if (json.subtype === 'completed' && json.tool_call) {
|
|
131
|
-
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
132
|
-
const result = json.tool_call[toolName]?.result;
|
|
133
|
-
|
|
134
|
-
if (result?.success) {
|
|
135
|
-
// Truncate large results
|
|
136
|
-
const content = result.success.content || '';
|
|
137
|
-
const truncated = content.length > 500
|
|
138
|
-
? content.substring(0, 500) + '... (truncated)'
|
|
139
|
-
: content;
|
|
140
|
-
|
|
141
|
-
this.emitMessage({
|
|
142
|
-
type: 'tool_result',
|
|
143
|
-
role: 'tool',
|
|
144
|
-
content: `[Tool Result: ${toolName}] ${truncated}`,
|
|
145
|
-
timestamp: json.timestamp_ms || Date.now(),
|
|
146
|
-
metadata: { callId: json.call_id, toolName, lines: result.success.totalLines },
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
break;
|
|
151
|
-
|
|
152
|
-
case 'result':
|
|
153
|
-
// Final result - flush any pending and emit result
|
|
154
|
-
this.flush();
|
|
155
|
-
|
|
156
|
-
this.emitMessage({
|
|
157
|
-
type: 'result',
|
|
158
|
-
role: 'assistant',
|
|
159
|
-
content: json.result || '',
|
|
160
|
-
timestamp: json.timestamp_ms || Date.now(),
|
|
161
|
-
metadata: {
|
|
162
|
-
duration_ms: json.duration_ms,
|
|
163
|
-
is_error: json.is_error,
|
|
164
|
-
subtype: json.subtype,
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
break;
|
|
168
|
-
|
|
169
|
-
case 'thinking':
|
|
170
|
-
// Thinking message (Claude 3.7+ etc.)
|
|
171
|
-
if (json.subtype === 'delta' && json.text) {
|
|
172
|
-
// Check if this is a new message or continuation
|
|
173
|
-
if (this.currentRole !== 'thinking') {
|
|
174
|
-
// Flush previous message if any
|
|
175
|
-
this.flush();
|
|
176
|
-
this.currentRole = 'thinking';
|
|
177
|
-
this.messageStartTime = json.timestamp_ms || Date.now();
|
|
178
|
-
}
|
|
179
|
-
this.currentMessage += json.text;
|
|
180
|
-
} else if (json.subtype === 'completed') {
|
|
181
|
-
// Thinking completed - flush immediately
|
|
182
|
-
this.flush();
|
|
183
|
-
}
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Flush accumulated message
|
|
190
|
-
*/
|
|
191
|
-
flush(): void {
|
|
192
|
-
if (this.currentMessage && this.currentRole) {
|
|
193
|
-
this.emitMessage({
|
|
194
|
-
type: this.currentRole as any,
|
|
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
|
-
|
|
205
|
-
private emitMessage(msg: ParsedMessage): void {
|
|
206
|
-
if (msg.content.trim()) {
|
|
207
|
-
this.onMessage(msg);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
42
|
/**
|
|
213
43
|
* ANSI escape sequence regex pattern
|
|
214
|
-
* Matches:
|
|
215
|
-
* - CSI sequences (colors, cursor movement, etc.)
|
|
216
|
-
* - OSC sequences (terminal titles, etc.)
|
|
217
|
-
* - Single-character escape codes
|
|
218
44
|
*/
|
|
219
45
|
const ANSI_REGEX = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Extended ANSI regex for more complete stripping
|
|
223
|
-
*/
|
|
224
46
|
const EXTENDED_ANSI_REGEX = /(?:\x1B[@-Z\\-_]|\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07]*(?:\x07|\x1B\\)|\x1B[PX^_][^\x1B]*\x1B\\|\x1B.)/g;
|
|
225
47
|
|
|
226
48
|
/**
|
|
@@ -230,14 +52,12 @@ export function stripAnsi(text: string): string {
|
|
|
230
52
|
return text
|
|
231
53
|
.replace(EXTENDED_ANSI_REGEX, '')
|
|
232
54
|
.replace(ANSI_REGEX, '')
|
|
233
|
-
// Also remove carriage returns that overwrite lines (progress bars, etc.)
|
|
234
55
|
.replace(/\r[^\n]/g, '\n')
|
|
235
|
-
// Clean up any remaining control characters except newlines/tabs
|
|
236
56
|
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
237
57
|
}
|
|
238
58
|
|
|
239
59
|
/**
|
|
240
|
-
* Format timestamp
|
|
60
|
+
* Format timestamp
|
|
241
61
|
*/
|
|
242
62
|
export function formatTimestamp(format: 'iso' | 'relative' | 'short', startTime?: number): string {
|
|
243
63
|
const now = Date.now();
|
|
@@ -269,122 +89,25 @@ export function formatTimestamp(format: 'iso' | 'relative' | 'short', startTime?
|
|
|
269
89
|
}
|
|
270
90
|
|
|
271
91
|
/**
|
|
272
|
-
*
|
|
273
|
-
* Matches:
|
|
274
|
-
* - ISO format: [2024-01-01T12:34:56...]
|
|
275
|
-
* - Short format: [12:34:56]
|
|
276
|
-
*/
|
|
277
|
-
const EXISTING_TIMESTAMP_REGEX = /^\[(\d{4}-\d{2}-\d{2}T)?\d{2}:\d{2}:\d{2}/;
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Check if a line already has a timestamp
|
|
281
|
-
*/
|
|
282
|
-
function hasExistingTimestamp(line: string): boolean {
|
|
283
|
-
return EXISTING_TIMESTAMP_REGEX.test(line.trim());
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Transform stream that strips ANSI and adds timestamps
|
|
288
|
-
*/
|
|
289
|
-
export class CleanLogTransform extends Transform {
|
|
290
|
-
private config: EnhancedLogConfig;
|
|
291
|
-
private session: LogSession;
|
|
292
|
-
private buffer: string = '';
|
|
293
|
-
|
|
294
|
-
constructor(config: EnhancedLogConfig, session: LogSession) {
|
|
295
|
-
super({ encoding: 'utf8' });
|
|
296
|
-
this.config = config;
|
|
297
|
-
this.session = session;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
_transform(chunk: Buffer, encoding: BufferEncoding, callback: TransformCallback): void {
|
|
301
|
-
let text = chunk.toString();
|
|
302
|
-
|
|
303
|
-
// Buffer partial lines
|
|
304
|
-
this.buffer += text;
|
|
305
|
-
const lines = this.buffer.split('\n');
|
|
306
|
-
|
|
307
|
-
// Keep the last incomplete line in buffer
|
|
308
|
-
this.buffer = lines.pop() || '';
|
|
309
|
-
|
|
310
|
-
for (const line of lines) {
|
|
311
|
-
let processed = line;
|
|
312
|
-
|
|
313
|
-
// Strip ANSI if enabled
|
|
314
|
-
if (this.config.stripAnsi) {
|
|
315
|
-
processed = stripAnsi(processed);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Add timestamp if enabled AND line doesn't already have one
|
|
319
|
-
if (this.config.addTimestamps && processed.trim() && !hasExistingTimestamp(processed)) {
|
|
320
|
-
const ts = formatTimestamp(this.config.timestampFormat, this.session.startTime);
|
|
321
|
-
processed = `[${ts}] ${processed}`;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
this.push(processed + '\n');
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
callback();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
_flush(callback: TransformCallback): void {
|
|
331
|
-
// Process any remaining buffered content
|
|
332
|
-
if (this.buffer.trim()) {
|
|
333
|
-
let processed = this.buffer;
|
|
334
|
-
|
|
335
|
-
if (this.config.stripAnsi) {
|
|
336
|
-
processed = stripAnsi(processed);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (this.config.addTimestamps && processed.trim() && !hasExistingTimestamp(processed)) {
|
|
340
|
-
const ts = formatTimestamp(this.config.timestampFormat, this.session.startTime);
|
|
341
|
-
processed = `[${ts}] ${processed}`;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
this.push(processed + '\n');
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
callback();
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Enhanced Log Manager - Manages log files with rotation and multiple outputs
|
|
92
|
+
* Simplified Log Manager - Only raw and readable logs
|
|
353
93
|
*/
|
|
354
94
|
export class EnhancedLogManager {
|
|
355
95
|
private config: EnhancedLogConfig;
|
|
356
96
|
private session: LogSession;
|
|
357
97
|
private logDir: string;
|
|
358
98
|
|
|
359
|
-
private cleanLogPath: string;
|
|
360
99
|
private rawLogPath: string;
|
|
361
|
-
private absoluteRawLogPath: string;
|
|
362
|
-
private jsonLogPath: string;
|
|
363
100
|
private readableLogPath: string;
|
|
364
101
|
|
|
365
|
-
private cleanLogFd: number | null = null;
|
|
366
102
|
private rawLogFd: number | null = null;
|
|
367
|
-
private absoluteRawLogFd: number | null = null;
|
|
368
|
-
private jsonLogFd: number | null = null;
|
|
369
103
|
private readableLogFd: number | null = null;
|
|
370
104
|
|
|
371
|
-
private cleanLogSize: number = 0;
|
|
372
105
|
private rawLogSize: number = 0;
|
|
373
|
-
private absoluteRawLogSize: number = 0;
|
|
374
106
|
|
|
375
|
-
private cleanTransform: CleanLogTransform | null = null;
|
|
376
|
-
private streamingParser: StreamingMessageParser | null = null;
|
|
377
|
-
private lineBuffer: string = '';
|
|
378
107
|
private onParsedMessage?: (msg: ParsedMessage) => void;
|
|
379
108
|
|
|
380
109
|
constructor(logDir: string, session: LogSession, config: Partial<EnhancedLogConfig> = {}, onParsedMessage?: (msg: ParsedMessage) => void) {
|
|
381
110
|
this.config = { ...DEFAULT_LOG_CONFIG, ...config };
|
|
382
|
-
|
|
383
|
-
// Support 'raw' as alias for 'keepAbsoluteRawLogs'
|
|
384
|
-
if (this.config.raw) {
|
|
385
|
-
this.config.keepAbsoluteRawLogs = true;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
111
|
this.session = session;
|
|
389
112
|
this.logDir = logDir;
|
|
390
113
|
this.onParsedMessage = onParsedMessage;
|
|
@@ -392,163 +115,54 @@ export class EnhancedLogManager {
|
|
|
392
115
|
// Ensure log directory exists
|
|
393
116
|
fs.mkdirSync(logDir, { recursive: true });
|
|
394
117
|
|
|
395
|
-
// Set up log file paths
|
|
396
|
-
this.cleanLogPath = safeJoin(logDir, 'terminal.log');
|
|
118
|
+
// Set up log file paths (simplified)
|
|
397
119
|
this.rawLogPath = safeJoin(logDir, 'terminal-raw.log');
|
|
398
|
-
this.absoluteRawLogPath = safeJoin(logDir, 'terminal-absolute-raw.log');
|
|
399
|
-
this.jsonLogPath = safeJoin(logDir, 'terminal.jsonl');
|
|
400
120
|
this.readableLogPath = safeJoin(logDir, 'terminal-readable.log');
|
|
401
|
-
|
|
402
|
-
if (this.config.raw) {
|
|
403
|
-
logger.info(`[${session.laneName}] 📄 Raw data capture enabled: ${this.absoluteRawLogPath}`);
|
|
404
|
-
}
|
|
405
121
|
|
|
406
122
|
// Initialize log files
|
|
407
123
|
this.initLogFiles();
|
|
408
124
|
}
|
|
409
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Get short time format (HH:MM:SS)
|
|
128
|
+
*/
|
|
129
|
+
private getShortTime(): string {
|
|
130
|
+
return new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get lane-task label like [L01-T02]
|
|
135
|
+
*/
|
|
136
|
+
private getLaneTaskLabel(): string {
|
|
137
|
+
const laneNum = (this.session.laneIndex ?? 0) + 1;
|
|
138
|
+
const taskNum = (this.session.taskIndex ?? 0) + 1;
|
|
139
|
+
return `L${laneNum.toString().padStart(2, '0')}-T${taskNum.toString().padStart(2, '0')}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
410
142
|
/**
|
|
411
143
|
* Initialize log files and write session headers
|
|
412
144
|
*/
|
|
413
145
|
private initLogFiles(): void {
|
|
414
146
|
// Check and rotate if necessary
|
|
415
|
-
this.rotateIfNeeded(this.cleanLogPath, 'clean');
|
|
416
|
-
if (this.config.keepRawLogs) {
|
|
417
|
-
this.rotateIfNeeded(this.rawLogPath, 'raw');
|
|
418
|
-
}
|
|
419
|
-
if (this.config.keepAbsoluteRawLogs) {
|
|
420
|
-
this.rotateIfNeeded(this.absoluteRawLogPath, 'raw');
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Open file descriptors
|
|
424
|
-
this.cleanLogFd = fs.openSync(this.cleanLogPath, 'a');
|
|
425
|
-
|
|
426
147
|
if (this.config.keepRawLogs) {
|
|
148
|
+
this.rotateIfNeeded(this.rawLogPath);
|
|
427
149
|
this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
|
|
428
150
|
}
|
|
429
|
-
|
|
430
|
-
if (this.config.keepAbsoluteRawLogs) {
|
|
431
|
-
this.absoluteRawLogFd = fs.openSync(this.absoluteRawLogPath, 'a');
|
|
432
|
-
}
|
|
433
151
|
|
|
434
|
-
|
|
435
|
-
this.jsonLogFd = fs.openSync(this.jsonLogPath, 'a');
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Open readable log file (for parsed streaming output)
|
|
152
|
+
this.rotateIfNeeded(this.readableLogPath);
|
|
439
153
|
this.readableLogFd = fs.openSync(this.readableLogPath, 'a');
|
|
440
154
|
|
|
441
|
-
// Get initial file
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (this.config.keepRawLogs) {
|
|
155
|
+
// Get initial file size for raw log if enabled
|
|
156
|
+
if (this.rawLogFd !== null) {
|
|
157
|
+
try {
|
|
445
158
|
this.rawLogSize = fs.statSync(this.rawLogPath).size;
|
|
159
|
+
} catch {
|
|
160
|
+
this.rawLogSize = 0;
|
|
446
161
|
}
|
|
447
|
-
if (this.config.keepAbsoluteRawLogs) {
|
|
448
|
-
this.absoluteRawLogSize = fs.statSync(this.absoluteRawLogPath).size;
|
|
449
|
-
}
|
|
450
|
-
} catch {
|
|
451
|
-
this.cleanLogSize = 0;
|
|
452
|
-
this.rawLogSize = 0;
|
|
453
|
-
this.absoluteRawLogSize = 0;
|
|
454
162
|
}
|
|
455
163
|
|
|
456
164
|
// Write session header
|
|
457
165
|
this.writeSessionHeader();
|
|
458
|
-
|
|
459
|
-
// Create transform stream
|
|
460
|
-
this.cleanTransform = new CleanLogTransform(this.config, this.session);
|
|
461
|
-
this.cleanTransform.on('data', (data: string) => {
|
|
462
|
-
this.writeToCleanLog(data);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
// Create streaming parser for readable log
|
|
466
|
-
this.streamingParser = new StreamingMessageParser((msg) => {
|
|
467
|
-
this.writeReadableMessage(msg);
|
|
468
|
-
if (this.onParsedMessage) {
|
|
469
|
-
this.onParsedMessage(msg);
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Write a parsed message to the readable log
|
|
476
|
-
*/
|
|
477
|
-
private writeReadableMessage(msg: ParsedMessage): void {
|
|
478
|
-
if (this.readableLogFd === null) return;
|
|
479
|
-
|
|
480
|
-
const ts = new Date(msg.timestamp).toISOString();
|
|
481
|
-
let formatted: string;
|
|
482
|
-
|
|
483
|
-
switch (msg.type) {
|
|
484
|
-
case 'system':
|
|
485
|
-
formatted = `[${ts}] ⚙️ SYSTEM: ${msg.content}\n`;
|
|
486
|
-
break;
|
|
487
|
-
|
|
488
|
-
case 'user':
|
|
489
|
-
case 'assistant':
|
|
490
|
-
case 'result':
|
|
491
|
-
// Format with brackets and line (compact)
|
|
492
|
-
const isUser = msg.type === 'user';
|
|
493
|
-
const isResult = msg.type === 'result';
|
|
494
|
-
const headerText = isUser ? '🧑 USER' : isResult ? '🤖 ASSISTANT (Final)' : '🤖 ASSISTANT';
|
|
495
|
-
const duration = msg.metadata?.duration_ms
|
|
496
|
-
? ` (${Math.round(msg.metadata.duration_ms / 1000)}s)`
|
|
497
|
-
: '';
|
|
498
|
-
|
|
499
|
-
const label = `[ ${headerText}${duration} ] `;
|
|
500
|
-
const totalWidth = 80;
|
|
501
|
-
const topBorder = `┌─${label}${'─'.repeat(Math.max(0, totalWidth - label.length - 2))}`;
|
|
502
|
-
const bottomBorder = `└─${'─'.repeat(totalWidth - 2)}`;
|
|
503
|
-
|
|
504
|
-
const lines = msg.content.split('\n');
|
|
505
|
-
formatted = `[${ts}] ${topBorder}\n`;
|
|
506
|
-
for (const line of lines) {
|
|
507
|
-
formatted += `[${ts}] │ ${line}\n`;
|
|
508
|
-
}
|
|
509
|
-
formatted += `[${ts}] ${bottomBorder}\n`;
|
|
510
|
-
break;
|
|
511
|
-
|
|
512
|
-
case 'tool':
|
|
513
|
-
formatted = `[${ts}] 🔧 TOOL: ${msg.content}\n`;
|
|
514
|
-
break;
|
|
515
|
-
|
|
516
|
-
case 'tool_result':
|
|
517
|
-
const toolResultLines = msg.metadata?.lines ? ` (${msg.metadata.lines} lines)` : '';
|
|
518
|
-
formatted = `[${ts}] 📄 RESL: ${msg.metadata?.toolName || 'Tool'}${toolResultLines}\n`;
|
|
519
|
-
break;
|
|
520
|
-
|
|
521
|
-
case 'thinking':
|
|
522
|
-
// Format thinking block
|
|
523
|
-
const thinkLabel = `[ 🤔 THINKING ] `;
|
|
524
|
-
const thinkWidth = 80;
|
|
525
|
-
const thinkTop = `┌─${thinkLabel}${'─'.repeat(Math.max(0, thinkWidth - thinkLabel.length - 2))}`;
|
|
526
|
-
const thinkBottom = `└─${'─'.repeat(thinkWidth - 2)}`;
|
|
527
|
-
|
|
528
|
-
const thinkLines = msg.content.trim().split('\n');
|
|
529
|
-
formatted = `[${ts}] ${thinkTop}\n`;
|
|
530
|
-
for (const line of thinkLines) {
|
|
531
|
-
formatted += `[${ts}] │ ${line}\n`;
|
|
532
|
-
}
|
|
533
|
-
formatted += `[${ts}] ${thinkBottom}\n`;
|
|
534
|
-
break;
|
|
535
|
-
|
|
536
|
-
default:
|
|
537
|
-
formatted = `[${ts}] ${msg.content}\n`;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
try {
|
|
541
|
-
fs.writeSync(this.readableLogFd, formatted);
|
|
542
|
-
} catch {
|
|
543
|
-
// Ignore write errors
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Indent text with a prefix
|
|
549
|
-
*/
|
|
550
|
-
private indentText(text: string, prefix: string): string {
|
|
551
|
-
return text.split('\n').map(line => prefix + line).join('\n');
|
|
552
166
|
}
|
|
553
167
|
|
|
554
168
|
/**
|
|
@@ -568,32 +182,14 @@ export class EnhancedLogManager {
|
|
|
568
182
|
|
|
569
183
|
`;
|
|
570
184
|
|
|
571
|
-
this.
|
|
572
|
-
|
|
573
|
-
if (this.config.keepRawLogs && this.rawLogFd !== null) {
|
|
574
|
-
fs.writeSync(this.rawLogFd, header);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Write JSON session entry
|
|
578
|
-
this.writeJsonEntry({
|
|
579
|
-
timestamp: new Date(this.session.startTime).toISOString(),
|
|
580
|
-
level: 'session',
|
|
581
|
-
source: 'system',
|
|
582
|
-
lane: this.session.laneName,
|
|
583
|
-
task: this.session.taskName,
|
|
584
|
-
message: 'Session started',
|
|
585
|
-
metadata: {
|
|
586
|
-
sessionId: this.session.id,
|
|
587
|
-
model: this.session.model,
|
|
588
|
-
...this.session.metadata,
|
|
589
|
-
},
|
|
590
|
-
});
|
|
185
|
+
this.writeToRawLog(header);
|
|
186
|
+
this.writeToReadableLog(header);
|
|
591
187
|
}
|
|
592
188
|
|
|
593
189
|
/**
|
|
594
190
|
* Rotate log file if it exceeds max size
|
|
595
191
|
*/
|
|
596
|
-
private rotateIfNeeded(logPath: string
|
|
192
|
+
private rotateIfNeeded(logPath: string): void {
|
|
597
193
|
if (!fs.existsSync(logPath)) return;
|
|
598
194
|
|
|
599
195
|
try {
|
|
@@ -633,32 +229,13 @@ export class EnhancedLogManager {
|
|
|
633
229
|
fs.renameSync(logPath, rotatedPath);
|
|
634
230
|
}
|
|
635
231
|
|
|
636
|
-
/**
|
|
637
|
-
* Write to clean log with size tracking
|
|
638
|
-
*/
|
|
639
|
-
private writeToCleanLog(data: string): void {
|
|
640
|
-
if (this.cleanLogFd === null) return;
|
|
641
|
-
|
|
642
|
-
const buffer = Buffer.from(data);
|
|
643
|
-
fs.writeSync(this.cleanLogFd, buffer);
|
|
644
|
-
this.cleanLogSize += buffer.length;
|
|
645
|
-
|
|
646
|
-
// Check if rotation needed
|
|
647
|
-
if (this.cleanLogSize >= this.config.maxFileSize) {
|
|
648
|
-
fs.closeSync(this.cleanLogFd);
|
|
649
|
-
this.rotateLog(this.cleanLogPath);
|
|
650
|
-
this.cleanLogFd = fs.openSync(this.cleanLogPath, 'a');
|
|
651
|
-
this.cleanLogSize = 0;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
232
|
/**
|
|
656
233
|
* Write to raw log with size tracking
|
|
657
234
|
*/
|
|
658
|
-
private writeToRawLog(data: string): void {
|
|
235
|
+
private writeToRawLog(data: string | Buffer): void {
|
|
659
236
|
if (this.rawLogFd === null) return;
|
|
660
237
|
|
|
661
|
-
const buffer = Buffer.from(data);
|
|
238
|
+
const buffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
662
239
|
fs.writeSync(this.rawLogFd, buffer);
|
|
663
240
|
this.rawLogSize += buffer.length;
|
|
664
241
|
|
|
@@ -672,32 +249,38 @@ export class EnhancedLogManager {
|
|
|
672
249
|
}
|
|
673
250
|
|
|
674
251
|
/**
|
|
675
|
-
* Write to
|
|
252
|
+
* Write to readable log
|
|
676
253
|
*/
|
|
677
|
-
private
|
|
678
|
-
if (this.
|
|
679
|
-
|
|
680
|
-
const buffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
681
|
-
fs.writeSync(this.absoluteRawLogFd, buffer);
|
|
682
|
-
this.absoluteRawLogSize += buffer.length;
|
|
254
|
+
private writeToReadableLog(data: string): void {
|
|
255
|
+
if (this.readableLogFd === null) return;
|
|
683
256
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
this.absoluteRawLogFd = fs.openSync(this.absoluteRawLogPath, 'a');
|
|
689
|
-
this.absoluteRawLogSize = 0;
|
|
257
|
+
try {
|
|
258
|
+
fs.writeSync(this.readableLogFd, data);
|
|
259
|
+
} catch {
|
|
260
|
+
// Ignore write errors
|
|
690
261
|
}
|
|
691
262
|
}
|
|
692
263
|
|
|
693
264
|
/**
|
|
694
|
-
* Write a
|
|
265
|
+
* Write a parsed message to the readable log using formatMessageForConsole style
|
|
695
266
|
*/
|
|
696
|
-
|
|
697
|
-
|
|
267
|
+
public writeReadableMessage(msg: ParsedMessage): void {
|
|
268
|
+
// Use formatMessageForConsole for consistent formatting
|
|
269
|
+
// Use short lane-task label like [L01-T02]
|
|
270
|
+
const formatted = formatMessageForConsole(msg, {
|
|
271
|
+
laneLabel: `[${this.getLaneTaskLabel()}]`,
|
|
272
|
+
includeTimestamp: false, // We'll add our own short timestamp
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Strip ANSI codes and add short timestamp for file output
|
|
276
|
+
const clean = stripAnsi(formatted);
|
|
277
|
+
const ts = this.getShortTime();
|
|
278
|
+
this.writeToReadableLog(`[${ts}] ${clean}\n`);
|
|
698
279
|
|
|
699
|
-
|
|
700
|
-
|
|
280
|
+
// Callback for console output
|
|
281
|
+
if (this.onParsedMessage) {
|
|
282
|
+
this.onParsedMessage(msg);
|
|
283
|
+
}
|
|
701
284
|
}
|
|
702
285
|
|
|
703
286
|
/**
|
|
@@ -706,142 +289,144 @@ export class EnhancedLogManager {
|
|
|
706
289
|
public writeStdout(data: Buffer | string): void {
|
|
707
290
|
const text = data.toString();
|
|
708
291
|
|
|
709
|
-
// Write
|
|
710
|
-
|
|
711
|
-
this.writeToAbsoluteRawLog(data);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// Write raw log
|
|
715
|
-
if (this.config.keepRawLogs) {
|
|
716
|
-
this.writeToRawLog(text);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Process through transform for clean log
|
|
720
|
-
if (this.cleanTransform) {
|
|
721
|
-
this.cleanTransform.write(data);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// Process lines for readable log and JSON entries
|
|
725
|
-
this.lineBuffer += text;
|
|
726
|
-
const lines = this.lineBuffer.split('\n');
|
|
727
|
-
this.lineBuffer = lines.pop() || '';
|
|
292
|
+
// Write raw log (original data)
|
|
293
|
+
this.writeToRawLog(data);
|
|
728
294
|
|
|
295
|
+
// Parse JSON output and write to readable log
|
|
296
|
+
const lines = text.split('\n');
|
|
729
297
|
for (const line of lines) {
|
|
730
|
-
const
|
|
731
|
-
if (!
|
|
732
|
-
|
|
733
|
-
//
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
try {
|
|
742
|
-
const json = JSON.parse(cleanLine);
|
|
743
|
-
let displayMsg = cleanLine;
|
|
744
|
-
let metadata = { ...json };
|
|
745
|
-
|
|
746
|
-
// Extract cleaner text for significant AI message types
|
|
747
|
-
if ((json.type === 'thinking' || json.type === 'thought') && (json.text || json.thought)) {
|
|
748
|
-
displayMsg = json.text || json.thought;
|
|
749
|
-
// Clean up any double newlines at the end of deltas
|
|
750
|
-
displayMsg = displayMsg.replace(/\n+$/, '\n');
|
|
751
|
-
} else if (json.type === 'assistant' && json.message?.content) {
|
|
752
|
-
displayMsg = json.message.content
|
|
753
|
-
.filter((c: any) => c.type === 'text')
|
|
754
|
-
.map((c: any) => c.text)
|
|
755
|
-
.join('');
|
|
756
|
-
} else if (json.type === 'user' && json.message?.content) {
|
|
757
|
-
displayMsg = json.message.content
|
|
758
|
-
.filter((c: any) => c.type === 'text')
|
|
759
|
-
.map((c: any) => c.text)
|
|
760
|
-
.join('');
|
|
761
|
-
} else if (json.type === 'tool_call' && json.subtype === 'started') {
|
|
762
|
-
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
763
|
-
const args = json.tool_call[toolName]?.args || {};
|
|
764
|
-
displayMsg = `🔧 CALL: ${toolName}(${JSON.stringify(args)})`;
|
|
765
|
-
} else if (json.type === 'tool_call' && json.subtype === 'completed') {
|
|
766
|
-
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
767
|
-
displayMsg = `📄 RESL: ${toolName}`;
|
|
768
|
-
} else if (json.type === 'result') {
|
|
769
|
-
displayMsg = json.result || 'Task completed';
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
this.writeJsonEntry({
|
|
773
|
-
timestamp: new Date().toISOString(),
|
|
774
|
-
level: 'stdout',
|
|
775
|
-
lane: this.session.laneName,
|
|
776
|
-
task: this.session.taskName,
|
|
777
|
-
message: displayMsg.substring(0, 2000), // Larger limit for AI text
|
|
778
|
-
metadata,
|
|
779
|
-
});
|
|
780
|
-
continue; // Already logged this JSON line
|
|
781
|
-
} catch {
|
|
782
|
-
// Not valid JSON or error, fall through to regular logging
|
|
298
|
+
const trimmed = line.trim();
|
|
299
|
+
if (!trimmed) continue;
|
|
300
|
+
|
|
301
|
+
// Try to parse as JSON (cursor-agent output)
|
|
302
|
+
if (trimmed.startsWith('{')) {
|
|
303
|
+
try {
|
|
304
|
+
const json = JSON.parse(trimmed);
|
|
305
|
+
const msg = this.parseJsonToMessage(json);
|
|
306
|
+
if (msg) {
|
|
307
|
+
this.writeReadableMessage(msg);
|
|
308
|
+
continue;
|
|
783
309
|
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Parse standard text logs into ParsedMessage
|
|
787
|
-
// Format: [HH:MM:SS] [LANE] ℹ️ INFO message
|
|
788
|
-
const textLogRegex = /^\[(\d{2}:\d{2}:\d{2})\]\s+\[(.*?)\]\s+(.*?)\s+(INFO|WARN|ERROR|SUCCESS|DONE|PROGRESS)\s+(.*)/;
|
|
789
|
-
const match = cleanLine.match(textLogRegex);
|
|
790
|
-
|
|
791
|
-
if (match && this.onParsedMessage) {
|
|
792
|
-
const [, time, , emoji, level, content] = match;
|
|
793
|
-
// Convert HH:MM:SS to a timestamp for today
|
|
794
|
-
const [h, m, s] = time!.split(':').map(Number);
|
|
795
|
-
const timestamp = new Date().setHours(h!, m!, s!, 0);
|
|
796
|
-
|
|
797
|
-
this.onParsedMessage({
|
|
798
|
-
type: level!.toLowerCase().replace('done', 'result').replace('success', 'result') as any,
|
|
799
|
-
role: 'system',
|
|
800
|
-
content: `${emoji} ${content}`,
|
|
801
|
-
timestamp,
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Also include significant info/status lines in readable log (compact)
|
|
807
|
-
if (this.readableLogFd !== null) {
|
|
808
|
-
// Look for log lines: [ISO_DATE] [LEVEL] ...
|
|
809
|
-
if (!this.isNoiseLog(cleanLine) && /\[\d{4}-\d{2}-\d{2}T/.test(cleanLine)) {
|
|
810
|
-
try {
|
|
811
|
-
// Check if it has a level marker
|
|
812
|
-
if (/\[(INFO|WARN|ERROR|SUCCESS|DEBUG)\]/.test(cleanLine)) {
|
|
813
|
-
// Special formatting for summary
|
|
814
|
-
if (cleanLine.includes('Final Workspace Summary')) {
|
|
815
|
-
const tsMatch = cleanLine.match(/\[(\d{4}-\d{2}-\d{2}T[^\]]+)\]/);
|
|
816
|
-
const ts = tsMatch ? tsMatch[1] : new Date().toISOString();
|
|
817
|
-
fs.writeSync(this.readableLogFd, `[${ts}] 📊 SUMMARY: ${cleanLine.split(']').slice(2).join(']').trim()}\n`);
|
|
818
|
-
} else {
|
|
819
|
-
fs.writeSync(this.readableLogFd, `${cleanLine}\n`);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
} catch {}
|
|
310
|
+
} catch {
|
|
311
|
+
// Not valid JSON, fall through
|
|
823
312
|
}
|
|
824
313
|
}
|
|
825
314
|
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
315
|
+
// Non-JSON line - write as-is with short timestamp and lane-task label
|
|
316
|
+
const cleanLine = stripAnsi(trimmed);
|
|
317
|
+
if (cleanLine && !this.isNoiseLog(cleanLine)) {
|
|
318
|
+
const hasTimestamp = /^\[(\d{4}-\d{2}-\d{2}T|\d{2}:\d{2}:\d{2})\]/.test(cleanLine);
|
|
319
|
+
const label = this.getLaneTaskLabel();
|
|
320
|
+
|
|
321
|
+
if (hasTimestamp) {
|
|
322
|
+
// If already has timestamp, just ensure label is present
|
|
323
|
+
const formatted = cleanLine.includes(`[${label}]`)
|
|
324
|
+
? cleanLine
|
|
325
|
+
: cleanLine.replace(/^(\[[^\]]+\])/, `$1 [${label}]`);
|
|
326
|
+
this.writeToReadableLog(`${formatted}\n`);
|
|
327
|
+
} else {
|
|
328
|
+
const ts = this.getShortTime();
|
|
329
|
+
this.writeToReadableLog(`[${ts}] [${label}] ${cleanLine}\n`);
|
|
330
|
+
}
|
|
836
331
|
}
|
|
837
332
|
}
|
|
838
333
|
}
|
|
839
|
-
|
|
334
|
+
|
|
840
335
|
/**
|
|
841
|
-
* Parse
|
|
336
|
+
* Parse cursor-agent JSON output to ParsedMessage
|
|
842
337
|
*/
|
|
843
|
-
private
|
|
844
|
-
|
|
338
|
+
private parseJsonToMessage(json: any): ParsedMessage | null {
|
|
339
|
+
const type = json.type;
|
|
340
|
+
const timestamp = json.timestamp_ms || Date.now();
|
|
341
|
+
|
|
342
|
+
switch (type) {
|
|
343
|
+
case 'system':
|
|
344
|
+
return {
|
|
345
|
+
type: 'system',
|
|
346
|
+
role: 'system',
|
|
347
|
+
content: `Model: ${json.model || 'unknown'}, Mode: ${json.permissionMode || 'default'}`,
|
|
348
|
+
timestamp,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
case 'user':
|
|
352
|
+
if (json.message?.content) {
|
|
353
|
+
const textContent = json.message.content
|
|
354
|
+
.filter((c: any) => c.type === 'text')
|
|
355
|
+
.map((c: any) => c.text)
|
|
356
|
+
.join('');
|
|
357
|
+
return {
|
|
358
|
+
type: 'user',
|
|
359
|
+
role: 'user',
|
|
360
|
+
content: textContent,
|
|
361
|
+
timestamp,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
|
|
366
|
+
case 'assistant':
|
|
367
|
+
if (json.message?.content) {
|
|
368
|
+
const textContent = json.message.content
|
|
369
|
+
.filter((c: any) => c.type === 'text')
|
|
370
|
+
.map((c: any) => c.text)
|
|
371
|
+
.join('');
|
|
372
|
+
return {
|
|
373
|
+
type: 'assistant',
|
|
374
|
+
role: 'assistant',
|
|
375
|
+
content: textContent,
|
|
376
|
+
timestamp,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
|
|
381
|
+
case 'tool_call':
|
|
382
|
+
if (json.subtype === 'started' && json.tool_call) {
|
|
383
|
+
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
384
|
+
const toolArgs = json.tool_call[toolName]?.args || {};
|
|
385
|
+
return {
|
|
386
|
+
type: 'tool',
|
|
387
|
+
role: 'tool',
|
|
388
|
+
content: `[Tool: ${toolName}] ${JSON.stringify(toolArgs)}`,
|
|
389
|
+
timestamp,
|
|
390
|
+
metadata: { callId: json.call_id, toolName },
|
|
391
|
+
};
|
|
392
|
+
} else if (json.subtype === 'completed' && json.tool_call) {
|
|
393
|
+
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
394
|
+
return {
|
|
395
|
+
type: 'tool_result',
|
|
396
|
+
role: 'tool',
|
|
397
|
+
content: `[Tool Result: ${toolName}]`,
|
|
398
|
+
timestamp,
|
|
399
|
+
metadata: { callId: json.call_id, toolName },
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
|
|
404
|
+
case 'result':
|
|
405
|
+
return {
|
|
406
|
+
type: 'result',
|
|
407
|
+
role: 'assistant',
|
|
408
|
+
content: json.result || '',
|
|
409
|
+
timestamp,
|
|
410
|
+
metadata: {
|
|
411
|
+
duration_ms: json.duration_ms,
|
|
412
|
+
is_error: json.is_error,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
case 'thinking':
|
|
417
|
+
if (json.text) {
|
|
418
|
+
return {
|
|
419
|
+
type: 'thinking',
|
|
420
|
+
role: 'assistant',
|
|
421
|
+
content: json.text,
|
|
422
|
+
timestamp,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
|
|
427
|
+
default:
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
845
430
|
}
|
|
846
431
|
|
|
847
432
|
/**
|
|
@@ -850,87 +435,41 @@ export class EnhancedLogManager {
|
|
|
850
435
|
public writeStderr(data: Buffer | string): void {
|
|
851
436
|
const text = data.toString();
|
|
852
437
|
|
|
853
|
-
// Write absolute raw log
|
|
854
|
-
if (this.config.keepAbsoluteRawLogs) {
|
|
855
|
-
this.writeToAbsoluteRawLog(data);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
438
|
// Write raw log
|
|
859
|
-
|
|
860
|
-
this.writeToRawLog(text);
|
|
861
|
-
}
|
|
439
|
+
this.writeToRawLog(data);
|
|
862
440
|
|
|
863
|
-
//
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
441
|
+
// Write to readable log with error prefix
|
|
442
|
+
const lines = text.split('\n');
|
|
443
|
+
for (const line of lines) {
|
|
444
|
+
const cleanLine = stripAnsi(line).trim();
|
|
445
|
+
if (cleanLine && !this.isNoiseLog(cleanLine)) {
|
|
446
|
+
const hasTimestamp = /^\[(\d{4}-\d{2}-\d{2}T|\d{2}:\d{2}:\d{2})\]/.test(cleanLine);
|
|
447
|
+
const label = this.getLaneTaskLabel();
|
|
448
|
+
|
|
449
|
+
if (hasTimestamp) {
|
|
450
|
+
const formatted = cleanLine.includes(`[${label}]`)
|
|
451
|
+
? cleanLine
|
|
452
|
+
: cleanLine.replace(/^(\[[^\]]+\])/, `$1 [${label}] ❌ ERR`);
|
|
453
|
+
this.writeToReadableLog(`${formatted}\n`);
|
|
454
|
+
} else {
|
|
455
|
+
const ts = this.getShortTime();
|
|
456
|
+
this.writeToReadableLog(`[${ts}] [${label}] ❌ ERR ${cleanLine}\n`);
|
|
878
457
|
}
|
|
879
458
|
}
|
|
880
459
|
}
|
|
881
|
-
|
|
882
|
-
// Write JSON entry
|
|
883
|
-
if (this.config.writeJsonLog) {
|
|
884
|
-
const cleanText = stripAnsi(text).trim();
|
|
885
|
-
if (cleanText) {
|
|
886
|
-
this.writeJsonEntry({
|
|
887
|
-
timestamp: new Date().toISOString(),
|
|
888
|
-
level: 'stderr',
|
|
889
|
-
lane: this.session.laneName,
|
|
890
|
-
task: this.session.taskName,
|
|
891
|
-
message: cleanText.substring(0, 1000),
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
460
|
}
|
|
896
461
|
|
|
897
462
|
/**
|
|
898
463
|
* Write a custom log entry
|
|
899
464
|
*/
|
|
900
465
|
public log(level: 'info' | 'error' | 'debug', message: string, metadata?: Record<string, any>): void {
|
|
901
|
-
const ts =
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
const line = `[${ts}] [${
|
|
905
|
-
this.writeToCleanLog(line);
|
|
466
|
+
const ts = this.getShortTime();
|
|
467
|
+
const label = this.getLaneTaskLabel();
|
|
468
|
+
const emoji = level === 'error' ? '❌' : level === 'info' ? 'ℹ️' : '🔍';
|
|
469
|
+
const line = `[${ts}] [${label}] ${emoji} ${level.toUpperCase()} ${message}\n`;
|
|
906
470
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (this.config.keepAbsoluteRawLogs) {
|
|
912
|
-
this.writeToAbsoluteRawLog(line);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// Write to readable log (compact)
|
|
916
|
-
if (this.readableLogFd !== null) {
|
|
917
|
-
const typeLabel = level === 'error' ? '❌ ERROR' : level === 'info' ? 'ℹ️ INFO' : '🔍 DEBUG';
|
|
918
|
-
const formatted = `${new Date().toISOString()} ${typeLabel}: ${message}\n`;
|
|
919
|
-
try {
|
|
920
|
-
fs.writeSync(this.readableLogFd, formatted);
|
|
921
|
-
} catch {}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (this.config.writeJsonLog) {
|
|
925
|
-
this.writeJsonEntry({
|
|
926
|
-
timestamp: new Date().toISOString(),
|
|
927
|
-
level,
|
|
928
|
-
lane: this.session.laneName,
|
|
929
|
-
task: this.session.taskName,
|
|
930
|
-
message,
|
|
931
|
-
metadata,
|
|
932
|
-
});
|
|
933
|
-
}
|
|
471
|
+
this.writeToRawLog(line);
|
|
472
|
+
this.writeToReadableLog(line);
|
|
934
473
|
}
|
|
935
474
|
|
|
936
475
|
/**
|
|
@@ -940,62 +479,37 @@ export class EnhancedLogManager {
|
|
|
940
479
|
const divider = '═'.repeat(78);
|
|
941
480
|
const line = `\n${divider}\n ${title}\n${divider}\n`;
|
|
942
481
|
|
|
943
|
-
this.
|
|
944
|
-
|
|
945
|
-
this.writeToRawLog(line);
|
|
946
|
-
}
|
|
947
|
-
if (this.config.keepAbsoluteRawLogs) {
|
|
948
|
-
this.writeToAbsoluteRawLog(line);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// Write to readable log (compact)
|
|
952
|
-
if (this.readableLogFd !== null) {
|
|
953
|
-
const ts = new Date().toISOString();
|
|
954
|
-
const formatted = `[${ts}] ━━━ ${title} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
955
|
-
try {
|
|
956
|
-
fs.writeSync(this.readableLogFd, formatted);
|
|
957
|
-
} catch {}
|
|
958
|
-
}
|
|
482
|
+
this.writeToRawLog(line);
|
|
483
|
+
this.writeToReadableLog(line);
|
|
959
484
|
}
|
|
960
485
|
|
|
961
486
|
/**
|
|
962
487
|
* Update task context
|
|
963
488
|
*/
|
|
964
|
-
public setTask(taskName: string, model?: string): void {
|
|
489
|
+
public setTask(taskName: string, model?: string, taskIndex?: number): void {
|
|
965
490
|
this.session.taskName = taskName;
|
|
966
491
|
if (model) {
|
|
967
492
|
this.session.model = model;
|
|
968
493
|
}
|
|
494
|
+
if (taskIndex !== undefined) {
|
|
495
|
+
this.session.taskIndex = taskIndex;
|
|
496
|
+
}
|
|
969
497
|
|
|
970
498
|
this.section(`Task: ${taskName}${model ? ` (Model: ${model})` : ''}`);
|
|
971
|
-
|
|
972
|
-
if (this.config.writeJsonLog) {
|
|
973
|
-
this.writeJsonEntry({
|
|
974
|
-
timestamp: new Date().toISOString(),
|
|
975
|
-
level: 'info',
|
|
976
|
-
source: 'system',
|
|
977
|
-
lane: this.session.laneName,
|
|
978
|
-
task: taskName,
|
|
979
|
-
message: `Task started: ${taskName}`,
|
|
980
|
-
metadata: { model },
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
499
|
}
|
|
984
500
|
|
|
985
501
|
/**
|
|
986
|
-
* Check if a log line is noise
|
|
502
|
+
* Check if a log line is noise
|
|
987
503
|
*/
|
|
988
504
|
private isNoiseLog(text: string): boolean {
|
|
989
|
-
// Skip empty or whitespace-only
|
|
990
505
|
if (!text.trim()) return true;
|
|
991
506
|
|
|
992
|
-
// Skip common progress/spinner patterns
|
|
993
507
|
const noisePatterns = [
|
|
994
|
-
/^[\s│├└─┌┐┘┴┬┤]+$/,
|
|
995
|
-
/^[.\s]+$/,
|
|
996
|
-
/^[=>\s-]+$/,
|
|
997
|
-
/^\d+%$/,
|
|
998
|
-
/^⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/,
|
|
508
|
+
/^[\s│├└─┌┐┘┴┬┤]+$/,
|
|
509
|
+
/^[.\s]+$/,
|
|
510
|
+
/^[=>\s-]+$/,
|
|
511
|
+
/^\d+%$/,
|
|
512
|
+
/^⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/,
|
|
999
513
|
];
|
|
1000
514
|
|
|
1001
515
|
return noisePatterns.some(p => p.test(text));
|
|
@@ -1004,12 +518,10 @@ export class EnhancedLogManager {
|
|
|
1004
518
|
/**
|
|
1005
519
|
* Get paths to all log files
|
|
1006
520
|
*/
|
|
1007
|
-
public getLogPaths(): { clean: string; raw
|
|
521
|
+
public getLogPaths(): { clean: string; raw: string; readable: string } {
|
|
1008
522
|
return {
|
|
1009
|
-
clean: this.
|
|
1010
|
-
raw: this.
|
|
1011
|
-
absoluteRaw: this.config.keepAbsoluteRawLogs ? this.absoluteRawLogPath : undefined,
|
|
1012
|
-
json: this.config.writeJsonLog ? this.jsonLogPath : undefined,
|
|
523
|
+
clean: this.readableLogPath, // For backward compatibility
|
|
524
|
+
raw: this.rawLogPath,
|
|
1013
525
|
readable: this.readableLogPath,
|
|
1014
526
|
};
|
|
1015
527
|
}
|
|
@@ -1018,29 +530,14 @@ export class EnhancedLogManager {
|
|
|
1018
530
|
* Create file descriptors for process stdio redirection
|
|
1019
531
|
*/
|
|
1020
532
|
public getFileDescriptors(): { stdout: number; stderr: number } {
|
|
1021
|
-
|
|
1022
|
-
const fd = this.rawLogFd !== null ? this.rawLogFd : this.cleanLogFd!;
|
|
533
|
+
const fd = this.rawLogFd!;
|
|
1023
534
|
return { stdout: fd, stderr: fd };
|
|
1024
535
|
}
|
|
1025
536
|
|
|
1026
537
|
/**
|
|
1027
|
-
* Close all log files
|
|
538
|
+
* Close all log files
|
|
1028
539
|
*/
|
|
1029
540
|
public close(): void {
|
|
1030
|
-
// Flush transform stream
|
|
1031
|
-
if (this.cleanTransform) {
|
|
1032
|
-
this.cleanTransform.end();
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Flush streaming parser
|
|
1036
|
-
if (this.streamingParser) {
|
|
1037
|
-
// Parse any remaining buffered data
|
|
1038
|
-
if (this.lineBuffer.trim()) {
|
|
1039
|
-
this.streamingParser.parseLine(this.lineBuffer);
|
|
1040
|
-
}
|
|
1041
|
-
this.streamingParser.flush();
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
541
|
// Write session end marker
|
|
1045
542
|
const endMarker = `
|
|
1046
543
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
@@ -1050,15 +547,6 @@ export class EnhancedLogManager {
|
|
|
1050
547
|
|
|
1051
548
|
`;
|
|
1052
549
|
|
|
1053
|
-
if (this.cleanLogFd !== null) {
|
|
1054
|
-
try {
|
|
1055
|
-
fs.writeSync(this.cleanLogFd, endMarker);
|
|
1056
|
-
fs.fsyncSync(this.cleanLogFd);
|
|
1057
|
-
fs.closeSync(this.cleanLogFd);
|
|
1058
|
-
} catch {}
|
|
1059
|
-
this.cleanLogFd = null;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
550
|
if (this.rawLogFd !== null) {
|
|
1063
551
|
try {
|
|
1064
552
|
fs.writeSync(this.rawLogFd, endMarker);
|
|
@@ -1068,34 +556,6 @@ export class EnhancedLogManager {
|
|
|
1068
556
|
this.rawLogFd = null;
|
|
1069
557
|
}
|
|
1070
558
|
|
|
1071
|
-
if (this.absoluteRawLogFd !== null) {
|
|
1072
|
-
try {
|
|
1073
|
-
fs.fsyncSync(this.absoluteRawLogFd);
|
|
1074
|
-
fs.closeSync(this.absoluteRawLogFd);
|
|
1075
|
-
} catch {}
|
|
1076
|
-
this.absoluteRawLogFd = null;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (this.jsonLogFd !== null) {
|
|
1080
|
-
try {
|
|
1081
|
-
this.writeJsonEntry({
|
|
1082
|
-
timestamp: new Date().toISOString(),
|
|
1083
|
-
level: 'session',
|
|
1084
|
-
source: 'system',
|
|
1085
|
-
lane: this.session.laneName,
|
|
1086
|
-
message: 'Session ended',
|
|
1087
|
-
metadata: {
|
|
1088
|
-
sessionId: this.session.id,
|
|
1089
|
-
duration: Date.now() - this.session.startTime,
|
|
1090
|
-
},
|
|
1091
|
-
});
|
|
1092
|
-
fs.fsyncSync(this.jsonLogFd);
|
|
1093
|
-
fs.closeSync(this.jsonLogFd);
|
|
1094
|
-
} catch {}
|
|
1095
|
-
this.jsonLogFd = null;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// Close readable log
|
|
1099
559
|
if (this.readableLogFd !== null) {
|
|
1100
560
|
try {
|
|
1101
561
|
fs.writeSync(this.readableLogFd, endMarker);
|
|
@@ -1107,22 +567,20 @@ export class EnhancedLogManager {
|
|
|
1107
567
|
}
|
|
1108
568
|
|
|
1109
569
|
/**
|
|
1110
|
-
* Extract the last error message from the
|
|
570
|
+
* Extract the last error message from the log
|
|
1111
571
|
*/
|
|
1112
572
|
public getLastError(): string | null {
|
|
1113
573
|
try {
|
|
1114
|
-
if (!fs.existsSync(this.
|
|
1115
|
-
const content = fs.readFileSync(this.
|
|
1116
|
-
// Look for lines containing error markers
|
|
574
|
+
if (!fs.existsSync(this.readableLogPath)) return null;
|
|
575
|
+
const content = fs.readFileSync(this.readableLogPath, 'utf8');
|
|
1117
576
|
const lines = content.split('\n').filter(l =>
|
|
1118
|
-
l.includes('[ERROR]') ||
|
|
1119
577
|
l.includes('❌') ||
|
|
578
|
+
l.includes('[ERROR]') ||
|
|
1120
579
|
l.includes('error:') ||
|
|
1121
580
|
l.includes('Fatal') ||
|
|
1122
581
|
l.includes('fail')
|
|
1123
582
|
);
|
|
1124
583
|
if (lines.length === 0) {
|
|
1125
|
-
// Fallback to last 5 lines if no specific error marker found
|
|
1126
584
|
const allLines = content.split('\n').filter(l => l.trim());
|
|
1127
585
|
return allLines.slice(-5).join('\n');
|
|
1128
586
|
}
|
|
@@ -1156,75 +614,41 @@ export function createLogManager(
|
|
|
1156
614
|
laneRunDir: string,
|
|
1157
615
|
laneName: string,
|
|
1158
616
|
config?: Partial<EnhancedLogConfig>,
|
|
1159
|
-
onParsedMessage?: (msg: ParsedMessage) => void
|
|
617
|
+
onParsedMessage?: (msg: ParsedMessage) => void,
|
|
618
|
+
laneIndex?: number
|
|
1160
619
|
): EnhancedLogManager {
|
|
1161
620
|
const session: LogSession = {
|
|
1162
621
|
id: `${laneName}-${Date.now().toString(36)}`,
|
|
1163
622
|
laneName,
|
|
1164
623
|
startTime: Date.now(),
|
|
624
|
+
laneIndex: laneIndex ?? 0,
|
|
625
|
+
taskIndex: 0,
|
|
1165
626
|
};
|
|
1166
627
|
|
|
1167
628
|
return new EnhancedLogManager(laneRunDir, session, config, onParsedMessage);
|
|
1168
629
|
}
|
|
1169
630
|
|
|
1170
631
|
/**
|
|
1171
|
-
* Read and parse JSON log file
|
|
632
|
+
* Read and parse JSON log file (legacy compatibility - returns empty array)
|
|
1172
633
|
*/
|
|
1173
634
|
export function readJsonLog(logPath: string): JsonLogEntry[] {
|
|
1174
|
-
|
|
1175
|
-
return [];
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
try {
|
|
1179
|
-
const content = fs.readFileSync(logPath, 'utf8');
|
|
1180
|
-
return content
|
|
1181
|
-
.split('\n')
|
|
1182
|
-
.filter(line => line.trim())
|
|
1183
|
-
.map(line => {
|
|
1184
|
-
try {
|
|
1185
|
-
return JSON.parse(line) as JsonLogEntry;
|
|
1186
|
-
} catch {
|
|
1187
|
-
return null;
|
|
1188
|
-
}
|
|
1189
|
-
})
|
|
1190
|
-
.filter((entry): entry is JsonLogEntry => entry !== null);
|
|
1191
|
-
} catch {
|
|
1192
|
-
return [];
|
|
1193
|
-
}
|
|
635
|
+
return [];
|
|
1194
636
|
}
|
|
1195
637
|
|
|
1196
638
|
/**
|
|
1197
|
-
* Export logs
|
|
639
|
+
* Export logs (legacy compatibility)
|
|
1198
640
|
*/
|
|
1199
641
|
export function exportLogs(
|
|
1200
642
|
laneRunDir: string,
|
|
1201
643
|
format: 'text' | 'json' | 'markdown' | 'html',
|
|
1202
644
|
outputPath?: string
|
|
1203
645
|
): string {
|
|
1204
|
-
const
|
|
1205
|
-
const jsonLogPath = safeJoin(laneRunDir, 'terminal.jsonl');
|
|
646
|
+
const readableLogPath = safeJoin(laneRunDir, 'terminal-readable.log');
|
|
1206
647
|
|
|
1207
648
|
let output = '';
|
|
1208
649
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
if (fs.existsSync(cleanLogPath)) {
|
|
1212
|
-
output = fs.readFileSync(cleanLogPath, 'utf8');
|
|
1213
|
-
}
|
|
1214
|
-
break;
|
|
1215
|
-
|
|
1216
|
-
case 'json':
|
|
1217
|
-
const entries = readJsonLog(jsonLogPath);
|
|
1218
|
-
output = JSON.stringify(entries, null, 2);
|
|
1219
|
-
break;
|
|
1220
|
-
|
|
1221
|
-
case 'markdown':
|
|
1222
|
-
output = exportToMarkdown(jsonLogPath, cleanLogPath);
|
|
1223
|
-
break;
|
|
1224
|
-
|
|
1225
|
-
case 'html':
|
|
1226
|
-
output = exportToHtml(jsonLogPath, cleanLogPath);
|
|
1227
|
-
break;
|
|
650
|
+
if (fs.existsSync(readableLogPath)) {
|
|
651
|
+
output = fs.readFileSync(readableLogPath, 'utf8');
|
|
1228
652
|
}
|
|
1229
653
|
|
|
1230
654
|
if (outputPath) {
|
|
@@ -1234,101 +658,13 @@ export function exportLogs(
|
|
|
1234
658
|
return output;
|
|
1235
659
|
}
|
|
1236
660
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
let md = '# CursorFlow Session Log\n\n';
|
|
1244
|
-
|
|
1245
|
-
// Find session info
|
|
1246
|
-
const sessionStart = entries.find(e => e.level === 'session' && e.message === 'Session started');
|
|
1247
|
-
if (sessionStart?.metadata) {
|
|
1248
|
-
md += '## Session Info\n\n';
|
|
1249
|
-
md += `- **Session ID**: ${sessionStart.metadata.sessionId}\n`;
|
|
1250
|
-
md += `- **Lane**: ${sessionStart.lane}\n`;
|
|
1251
|
-
md += `- **Model**: ${sessionStart.metadata.model || 'default'}\n`;
|
|
1252
|
-
md += `- **Started**: ${sessionStart.timestamp}\n\n`;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
md += '## Log Entries\n\n';
|
|
1256
|
-
|
|
1257
|
-
// Group by task
|
|
1258
|
-
const byTask = new Map<string, JsonLogEntry[]>();
|
|
1259
|
-
for (const entry of entries) {
|
|
1260
|
-
const task = entry.task || '(no task)';
|
|
1261
|
-
if (!byTask.has(task)) {
|
|
1262
|
-
byTask.set(task, []);
|
|
1263
|
-
}
|
|
1264
|
-
byTask.get(task)!.push(entry);
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
for (const [task, taskEntries] of byTask) {
|
|
1268
|
-
md += `### Task: ${task}\n\n`;
|
|
1269
|
-
md += '```\n';
|
|
1270
|
-
for (const entry of taskEntries) {
|
|
1271
|
-
const level = entry.level || 'info';
|
|
1272
|
-
const message = entry.message || '';
|
|
1273
|
-
if (level !== 'session') {
|
|
1274
|
-
md += `[${entry.timestamp}] [${level.toUpperCase()}] ${message}\n`;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
md += '```\n\n';
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
return md;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
/**
|
|
1284
|
-
* Export logs to HTML format
|
|
1285
|
-
*/
|
|
1286
|
-
function exportToHtml(jsonLogPath: string, cleanLogPath: string): string {
|
|
1287
|
-
const entries = readJsonLog(jsonLogPath);
|
|
1288
|
-
|
|
1289
|
-
let html = `<!DOCTYPE html>
|
|
1290
|
-
<html>
|
|
1291
|
-
<head>
|
|
1292
|
-
<title>CursorFlow Session Log</title>
|
|
1293
|
-
<style>
|
|
1294
|
-
body { font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; margin: 20px; background: #1e1e1e; color: #d4d4d4; }
|
|
1295
|
-
h1, h2 { color: #569cd6; }
|
|
1296
|
-
.entry { padding: 4px 8px; margin: 2px 0; border-radius: 4px; }
|
|
1297
|
-
.entry.stdout { background: #252526; }
|
|
1298
|
-
.entry.stderr { background: #3c1f1f; color: #f48771; }
|
|
1299
|
-
.entry.info { background: #1e3a5f; color: #9cdcfe; }
|
|
1300
|
-
.entry.error { background: #5f1e1e; color: #f48771; }
|
|
1301
|
-
.entry.session { background: #1e4620; color: #6a9955; }
|
|
1302
|
-
.timestamp { color: #808080; font-size: 0.9em; }
|
|
1303
|
-
.level { font-weight: bold; text-transform: uppercase; }
|
|
1304
|
-
.task { color: #dcdcaa; }
|
|
1305
|
-
pre { white-space: pre-wrap; word-wrap: break-word; }
|
|
1306
|
-
</style>
|
|
1307
|
-
</head>
|
|
1308
|
-
<body>
|
|
1309
|
-
<h1>CursorFlow Session Log</h1>
|
|
1310
|
-
`;
|
|
1311
|
-
|
|
1312
|
-
for (const entry of entries) {
|
|
1313
|
-
const level = entry.level || 'info';
|
|
1314
|
-
const message = entry.message || '';
|
|
1315
|
-
html += ` <div class="entry ${level}">
|
|
1316
|
-
<span class="timestamp">${entry.timestamp}</span>
|
|
1317
|
-
<span class="level">[${level}]</span>
|
|
1318
|
-
${entry.task ? `<span class="task">[${entry.task}]</span>` : ''}
|
|
1319
|
-
<pre>${escapeHtml(message)}</pre>
|
|
1320
|
-
</div>\n`;
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
html += '</body></html>';
|
|
1324
|
-
return html;
|
|
661
|
+
// Legacy exports for backward compatibility
|
|
662
|
+
export class StreamingMessageParser {
|
|
663
|
+
constructor(onMessage: (msg: ParsedMessage) => void) {}
|
|
664
|
+
parseLine(line: string): void {}
|
|
665
|
+
flush(): void {}
|
|
1325
666
|
}
|
|
1326
667
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
.replace(/&/g, '&')
|
|
1330
|
-
.replace(/</g, '<')
|
|
1331
|
-
.replace(/>/g, '>')
|
|
1332
|
-
.replace(/"/g, '"')
|
|
1333
|
-
.replace(/'/g, ''');
|
|
668
|
+
export class CleanLogTransform {
|
|
669
|
+
constructor(config: EnhancedLogConfig, session: LogSession) {}
|
|
1334
670
|
}
|