@litmers/cursorflow-orchestrator 0.1.31 → 0.1.36
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 +27 -0
- package/README.md +182 -59
- package/commands/cursorflow-add.md +159 -0
- package/commands/cursorflow-doctor.md +45 -23
- package/commands/cursorflow-monitor.md +23 -2
- package/commands/cursorflow-new.md +87 -0
- package/commands/cursorflow-run.md +60 -111
- 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/doctor.js +47 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +34 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.js +17 -34
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +62 -65
- 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 +57 -68
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +60 -30
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/stop.js +6 -0
- package/dist/cli/stop.js.map +1 -1
- package/dist/cli/tasks.d.ts +5 -3
- package/dist/cli/tasks.js +181 -29
- 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 +215 -334
- 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/services/logging/console.js +2 -1
- package/dist/services/logging/console.js.map +1 -1
- 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.d.ts +5 -1
- package/dist/utils/config.js +15 -16
- 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/doctor.js +40 -8
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +45 -82
- package/dist/utils/enhanced-logger.js +239 -844
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/flow.d.ts +9 -0
- package/dist/utils/flow.js +73 -0
- package/dist/utils/flow.js.map +1 -0
- 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/doctor.ts +48 -4
- package/src/cli/index.ts +36 -32
- package/src/cli/logs.ts +20 -33
- package/src/cli/monitor.ts +70 -75
- package/src/cli/new.ts +235 -0
- package/src/cli/prepare.ts +98 -205
- package/src/cli/resume.ts +61 -76
- package/src/cli/run.ts +333 -306
- package/src/cli/stop.ts +8 -0
- package/src/cli/tasks.ts +200 -21
- package/src/core/failure-policy.ts +9 -0
- package/src/core/orchestrator.ts +279 -379
- 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/services/logging/console.ts +2 -1
- 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 +16 -17
- package/src/utils/dependency.ts +311 -2
- package/src/utils/doctor.ts +36 -8
- package/src/utils/enhanced-logger.ts +264 -927
- package/src/utils/flow.ts +42 -0
- 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,55 @@ 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 [L1-T2-lanename10]
|
|
135
|
+
*/
|
|
136
|
+
private getLaneTaskLabel(): string {
|
|
137
|
+
const laneNum = (this.session.laneIndex ?? 0) + 1;
|
|
138
|
+
const taskNum = (this.session.taskIndex ?? 0) + 1;
|
|
139
|
+
const shortLaneName = this.session.laneName.substring(0, 10);
|
|
140
|
+
return `L${laneNum}-T${taskNum}-${shortLaneName}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
410
143
|
/**
|
|
411
144
|
* Initialize log files and write session headers
|
|
412
145
|
*/
|
|
413
146
|
private initLogFiles(): void {
|
|
414
147
|
// 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
148
|
if (this.config.keepRawLogs) {
|
|
149
|
+
this.rotateIfNeeded(this.rawLogPath);
|
|
427
150
|
this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
|
|
428
151
|
}
|
|
429
|
-
|
|
430
|
-
if (this.config.keepAbsoluteRawLogs) {
|
|
431
|
-
this.absoluteRawLogFd = fs.openSync(this.absoluteRawLogPath, 'a');
|
|
432
|
-
}
|
|
433
152
|
|
|
434
|
-
|
|
435
|
-
this.jsonLogFd = fs.openSync(this.jsonLogPath, 'a');
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Open readable log file (for parsed streaming output)
|
|
153
|
+
this.rotateIfNeeded(this.readableLogPath);
|
|
439
154
|
this.readableLogFd = fs.openSync(this.readableLogPath, 'a');
|
|
440
155
|
|
|
441
|
-
// Get initial file
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (this.config.keepRawLogs) {
|
|
156
|
+
// Get initial file size for raw log if enabled
|
|
157
|
+
if (this.rawLogFd !== null) {
|
|
158
|
+
try {
|
|
445
159
|
this.rawLogSize = fs.statSync(this.rawLogPath).size;
|
|
160
|
+
} catch {
|
|
161
|
+
this.rawLogSize = 0;
|
|
446
162
|
}
|
|
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
163
|
}
|
|
455
164
|
|
|
456
165
|
// Write session header
|
|
457
166
|
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
167
|
}
|
|
553
168
|
|
|
554
169
|
/**
|
|
@@ -568,32 +183,14 @@ export class EnhancedLogManager {
|
|
|
568
183
|
|
|
569
184
|
`;
|
|
570
185
|
|
|
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
|
-
});
|
|
186
|
+
this.writeToRawLog(header);
|
|
187
|
+
this.writeToReadableLog(header);
|
|
591
188
|
}
|
|
592
189
|
|
|
593
190
|
/**
|
|
594
191
|
* Rotate log file if it exceeds max size
|
|
595
192
|
*/
|
|
596
|
-
private rotateIfNeeded(logPath: string
|
|
193
|
+
private rotateIfNeeded(logPath: string): void {
|
|
597
194
|
if (!fs.existsSync(logPath)) return;
|
|
598
195
|
|
|
599
196
|
try {
|
|
@@ -633,32 +230,13 @@ export class EnhancedLogManager {
|
|
|
633
230
|
fs.renameSync(logPath, rotatedPath);
|
|
634
231
|
}
|
|
635
232
|
|
|
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
233
|
/**
|
|
656
234
|
* Write to raw log with size tracking
|
|
657
235
|
*/
|
|
658
|
-
private writeToRawLog(data: string): void {
|
|
236
|
+
private writeToRawLog(data: string | Buffer): void {
|
|
659
237
|
if (this.rawLogFd === null) return;
|
|
660
238
|
|
|
661
|
-
const buffer = Buffer.from(data);
|
|
239
|
+
const buffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
662
240
|
fs.writeSync(this.rawLogFd, buffer);
|
|
663
241
|
this.rawLogSize += buffer.length;
|
|
664
242
|
|
|
@@ -672,32 +250,38 @@ export class EnhancedLogManager {
|
|
|
672
250
|
}
|
|
673
251
|
|
|
674
252
|
/**
|
|
675
|
-
* Write to
|
|
253
|
+
* Write to readable log
|
|
676
254
|
*/
|
|
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;
|
|
255
|
+
private writeToReadableLog(data: string): void {
|
|
256
|
+
if (this.readableLogFd === null) return;
|
|
683
257
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
this.absoluteRawLogFd = fs.openSync(this.absoluteRawLogPath, 'a');
|
|
689
|
-
this.absoluteRawLogSize = 0;
|
|
258
|
+
try {
|
|
259
|
+
fs.writeSync(this.readableLogFd, data);
|
|
260
|
+
} catch {
|
|
261
|
+
// Ignore write errors
|
|
690
262
|
}
|
|
691
263
|
}
|
|
692
264
|
|
|
693
265
|
/**
|
|
694
|
-
* Write a
|
|
266
|
+
* Write a parsed message to the readable log using formatMessageForConsole style
|
|
695
267
|
*/
|
|
696
|
-
|
|
697
|
-
|
|
268
|
+
public writeReadableMessage(msg: ParsedMessage): void {
|
|
269
|
+
// Use formatMessageForConsole for consistent formatting
|
|
270
|
+
// Use short lane-task label like [L01-T02]
|
|
271
|
+
const formatted = formatMessageForConsole(msg, {
|
|
272
|
+
laneLabel: `[${this.getLaneTaskLabel()}]`,
|
|
273
|
+
includeTimestamp: false, // We'll add our own short timestamp
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Strip ANSI codes and add short timestamp for file output
|
|
277
|
+
const clean = stripAnsi(formatted);
|
|
278
|
+
const ts = this.getShortTime();
|
|
279
|
+
this.writeToReadableLog(`[${ts}] ${clean}\n`);
|
|
698
280
|
|
|
699
|
-
|
|
700
|
-
|
|
281
|
+
// Callback for console output
|
|
282
|
+
if (this.onParsedMessage) {
|
|
283
|
+
this.onParsedMessage(msg);
|
|
284
|
+
}
|
|
701
285
|
}
|
|
702
286
|
|
|
703
287
|
/**
|
|
@@ -706,142 +290,144 @@ export class EnhancedLogManager {
|
|
|
706
290
|
public writeStdout(data: Buffer | string): void {
|
|
707
291
|
const text = data.toString();
|
|
708
292
|
|
|
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() || '';
|
|
293
|
+
// Write raw log (original data)
|
|
294
|
+
this.writeToRawLog(data);
|
|
728
295
|
|
|
296
|
+
// Parse JSON output and write to readable log
|
|
297
|
+
const lines = text.split('\n');
|
|
729
298
|
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
|
|
299
|
+
const trimmed = line.trim();
|
|
300
|
+
if (!trimmed) continue;
|
|
301
|
+
|
|
302
|
+
// Try to parse as JSON (cursor-agent output)
|
|
303
|
+
if (trimmed.startsWith('{')) {
|
|
304
|
+
try {
|
|
305
|
+
const json = JSON.parse(trimmed);
|
|
306
|
+
const msg = this.parseJsonToMessage(json);
|
|
307
|
+
if (msg) {
|
|
308
|
+
this.writeReadableMessage(msg);
|
|
309
|
+
continue;
|
|
783
310
|
}
|
|
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 {}
|
|
311
|
+
} catch {
|
|
312
|
+
// Not valid JSON, fall through
|
|
823
313
|
}
|
|
824
314
|
}
|
|
825
315
|
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
316
|
+
// Non-JSON line - write as-is with short timestamp and lane-task label
|
|
317
|
+
const cleanLine = stripAnsi(trimmed);
|
|
318
|
+
if (cleanLine && !this.isNoiseLog(cleanLine)) {
|
|
319
|
+
const hasTimestamp = /^\[(\d{4}-\d{2}-\d{2}T|\d{2}:\d{2}:\d{2})\]/.test(cleanLine);
|
|
320
|
+
const label = this.getLaneTaskLabel();
|
|
321
|
+
|
|
322
|
+
if (hasTimestamp) {
|
|
323
|
+
// If already has timestamp, just ensure label is present
|
|
324
|
+
const formatted = cleanLine.includes(`[${label}]`)
|
|
325
|
+
? cleanLine
|
|
326
|
+
: cleanLine.replace(/^(\[[^\]]+\])/, `$1 [${label}]`);
|
|
327
|
+
this.writeToReadableLog(`${formatted}\n`);
|
|
328
|
+
} else {
|
|
329
|
+
const ts = this.getShortTime();
|
|
330
|
+
this.writeToReadableLog(`[${ts}] [${label}] ${cleanLine}\n`);
|
|
331
|
+
}
|
|
836
332
|
}
|
|
837
333
|
}
|
|
838
334
|
}
|
|
839
|
-
|
|
335
|
+
|
|
840
336
|
/**
|
|
841
|
-
* Parse
|
|
337
|
+
* Parse cursor-agent JSON output to ParsedMessage
|
|
842
338
|
*/
|
|
843
|
-
private
|
|
844
|
-
|
|
339
|
+
private parseJsonToMessage(json: any): ParsedMessage | null {
|
|
340
|
+
const type = json.type;
|
|
341
|
+
const timestamp = json.timestamp_ms || Date.now();
|
|
342
|
+
|
|
343
|
+
switch (type) {
|
|
344
|
+
case 'system':
|
|
345
|
+
return {
|
|
346
|
+
type: 'system',
|
|
347
|
+
role: 'system',
|
|
348
|
+
content: `Model: ${json.model || 'unknown'}, Mode: ${json.permissionMode || 'default'}`,
|
|
349
|
+
timestamp,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
case 'user':
|
|
353
|
+
if (json.message?.content) {
|
|
354
|
+
const textContent = json.message.content
|
|
355
|
+
.filter((c: any) => c.type === 'text')
|
|
356
|
+
.map((c: any) => c.text)
|
|
357
|
+
.join('');
|
|
358
|
+
return {
|
|
359
|
+
type: 'user',
|
|
360
|
+
role: 'user',
|
|
361
|
+
content: textContent,
|
|
362
|
+
timestamp,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
|
|
367
|
+
case 'assistant':
|
|
368
|
+
if (json.message?.content) {
|
|
369
|
+
const textContent = json.message.content
|
|
370
|
+
.filter((c: any) => c.type === 'text')
|
|
371
|
+
.map((c: any) => c.text)
|
|
372
|
+
.join('');
|
|
373
|
+
return {
|
|
374
|
+
type: 'assistant',
|
|
375
|
+
role: 'assistant',
|
|
376
|
+
content: textContent,
|
|
377
|
+
timestamp,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
|
|
382
|
+
case 'tool_call':
|
|
383
|
+
if (json.subtype === 'started' && json.tool_call) {
|
|
384
|
+
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
385
|
+
const toolArgs = json.tool_call[toolName]?.args || {};
|
|
386
|
+
return {
|
|
387
|
+
type: 'tool',
|
|
388
|
+
role: 'tool',
|
|
389
|
+
content: `[Tool: ${toolName}] ${JSON.stringify(toolArgs)}`,
|
|
390
|
+
timestamp,
|
|
391
|
+
metadata: { callId: json.call_id, toolName },
|
|
392
|
+
};
|
|
393
|
+
} else if (json.subtype === 'completed' && json.tool_call) {
|
|
394
|
+
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
395
|
+
return {
|
|
396
|
+
type: 'tool_result',
|
|
397
|
+
role: 'tool',
|
|
398
|
+
content: `[Tool Result: ${toolName}]`,
|
|
399
|
+
timestamp,
|
|
400
|
+
metadata: { callId: json.call_id, toolName },
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
|
|
405
|
+
case 'result':
|
|
406
|
+
return {
|
|
407
|
+
type: 'result',
|
|
408
|
+
role: 'assistant',
|
|
409
|
+
content: json.result || '',
|
|
410
|
+
timestamp,
|
|
411
|
+
metadata: {
|
|
412
|
+
duration_ms: json.duration_ms,
|
|
413
|
+
is_error: json.is_error,
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
case 'thinking':
|
|
418
|
+
if (json.text) {
|
|
419
|
+
return {
|
|
420
|
+
type: 'thinking',
|
|
421
|
+
role: 'assistant',
|
|
422
|
+
content: json.text,
|
|
423
|
+
timestamp,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
|
|
428
|
+
default:
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
845
431
|
}
|
|
846
432
|
|
|
847
433
|
/**
|
|
@@ -850,87 +436,41 @@ export class EnhancedLogManager {
|
|
|
850
436
|
public writeStderr(data: Buffer | string): void {
|
|
851
437
|
const text = data.toString();
|
|
852
438
|
|
|
853
|
-
// Write absolute raw log
|
|
854
|
-
if (this.config.keepAbsoluteRawLogs) {
|
|
855
|
-
this.writeToAbsoluteRawLog(data);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
439
|
// Write raw log
|
|
859
|
-
|
|
860
|
-
this.writeToRawLog(text);
|
|
861
|
-
}
|
|
440
|
+
this.writeToRawLog(data);
|
|
862
441
|
|
|
863
|
-
//
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
442
|
+
// Write to readable log with error prefix
|
|
443
|
+
const lines = text.split('\n');
|
|
444
|
+
for (const line of lines) {
|
|
445
|
+
const cleanLine = stripAnsi(line).trim();
|
|
446
|
+
if (cleanLine && !this.isNoiseLog(cleanLine)) {
|
|
447
|
+
const hasTimestamp = /^\[(\d{4}-\d{2}-\d{2}T|\d{2}:\d{2}:\d{2})\]/.test(cleanLine);
|
|
448
|
+
const label = this.getLaneTaskLabel();
|
|
449
|
+
|
|
450
|
+
if (hasTimestamp) {
|
|
451
|
+
const formatted = cleanLine.includes(`[${label}]`)
|
|
452
|
+
? cleanLine
|
|
453
|
+
: cleanLine.replace(/^(\[[^\]]+\])/, `$1 [${label}] ❌ ERR`);
|
|
454
|
+
this.writeToReadableLog(`${formatted}\n`);
|
|
455
|
+
} else {
|
|
456
|
+
const ts = this.getShortTime();
|
|
457
|
+
this.writeToReadableLog(`[${ts}] [${label}] ❌ ERR ${cleanLine}\n`);
|
|
878
458
|
}
|
|
879
459
|
}
|
|
880
460
|
}
|
|
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
461
|
}
|
|
896
462
|
|
|
897
463
|
/**
|
|
898
464
|
* Write a custom log entry
|
|
899
465
|
*/
|
|
900
466
|
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);
|
|
467
|
+
const ts = this.getShortTime();
|
|
468
|
+
const label = this.getLaneTaskLabel();
|
|
469
|
+
const emoji = level === 'error' ? '❌' : level === 'info' ? 'ℹ️' : '🔍';
|
|
470
|
+
const line = `[${ts}] [${label}] ${emoji} ${level.toUpperCase()} ${message}\n`;
|
|
906
471
|
|
|
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
|
-
}
|
|
472
|
+
this.writeToRawLog(line);
|
|
473
|
+
this.writeToReadableLog(line);
|
|
934
474
|
}
|
|
935
475
|
|
|
936
476
|
/**
|
|
@@ -940,62 +480,37 @@ export class EnhancedLogManager {
|
|
|
940
480
|
const divider = '═'.repeat(78);
|
|
941
481
|
const line = `\n${divider}\n ${title}\n${divider}\n`;
|
|
942
482
|
|
|
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
|
-
}
|
|
483
|
+
this.writeToRawLog(line);
|
|
484
|
+
this.writeToReadableLog(line);
|
|
959
485
|
}
|
|
960
486
|
|
|
961
487
|
/**
|
|
962
488
|
* Update task context
|
|
963
489
|
*/
|
|
964
|
-
public setTask(taskName: string, model?: string): void {
|
|
490
|
+
public setTask(taskName: string, model?: string, taskIndex?: number): void {
|
|
965
491
|
this.session.taskName = taskName;
|
|
966
492
|
if (model) {
|
|
967
493
|
this.session.model = model;
|
|
968
494
|
}
|
|
495
|
+
if (taskIndex !== undefined) {
|
|
496
|
+
this.session.taskIndex = taskIndex;
|
|
497
|
+
}
|
|
969
498
|
|
|
970
499
|
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
500
|
}
|
|
984
501
|
|
|
985
502
|
/**
|
|
986
|
-
* Check if a log line is noise
|
|
503
|
+
* Check if a log line is noise
|
|
987
504
|
*/
|
|
988
505
|
private isNoiseLog(text: string): boolean {
|
|
989
|
-
// Skip empty or whitespace-only
|
|
990
506
|
if (!text.trim()) return true;
|
|
991
507
|
|
|
992
|
-
// Skip common progress/spinner patterns
|
|
993
508
|
const noisePatterns = [
|
|
994
|
-
/^[\s│├└─┌┐┘┴┬┤]+$/,
|
|
995
|
-
/^[.\s]+$/,
|
|
996
|
-
/^[=>\s-]+$/,
|
|
997
|
-
/^\d+%$/,
|
|
998
|
-
/^⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/,
|
|
509
|
+
/^[\s│├└─┌┐┘┴┬┤]+$/,
|
|
510
|
+
/^[.\s]+$/,
|
|
511
|
+
/^[=>\s-]+$/,
|
|
512
|
+
/^\d+%$/,
|
|
513
|
+
/^⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/,
|
|
999
514
|
];
|
|
1000
515
|
|
|
1001
516
|
return noisePatterns.some(p => p.test(text));
|
|
@@ -1004,12 +519,10 @@ export class EnhancedLogManager {
|
|
|
1004
519
|
/**
|
|
1005
520
|
* Get paths to all log files
|
|
1006
521
|
*/
|
|
1007
|
-
public getLogPaths(): { clean: string; raw
|
|
522
|
+
public getLogPaths(): { clean: string; raw: string; readable: string } {
|
|
1008
523
|
return {
|
|
1009
|
-
clean: this.
|
|
1010
|
-
raw: this.
|
|
1011
|
-
absoluteRaw: this.config.keepAbsoluteRawLogs ? this.absoluteRawLogPath : undefined,
|
|
1012
|
-
json: this.config.writeJsonLog ? this.jsonLogPath : undefined,
|
|
524
|
+
clean: this.readableLogPath, // For backward compatibility
|
|
525
|
+
raw: this.rawLogPath,
|
|
1013
526
|
readable: this.readableLogPath,
|
|
1014
527
|
};
|
|
1015
528
|
}
|
|
@@ -1018,29 +531,14 @@ export class EnhancedLogManager {
|
|
|
1018
531
|
* Create file descriptors for process stdio redirection
|
|
1019
532
|
*/
|
|
1020
533
|
public getFileDescriptors(): { stdout: number; stderr: number } {
|
|
1021
|
-
|
|
1022
|
-
const fd = this.rawLogFd !== null ? this.rawLogFd : this.cleanLogFd!;
|
|
534
|
+
const fd = this.rawLogFd!;
|
|
1023
535
|
return { stdout: fd, stderr: fd };
|
|
1024
536
|
}
|
|
1025
537
|
|
|
1026
538
|
/**
|
|
1027
|
-
* Close all log files
|
|
539
|
+
* Close all log files
|
|
1028
540
|
*/
|
|
1029
541
|
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
542
|
// Write session end marker
|
|
1045
543
|
const endMarker = `
|
|
1046
544
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
@@ -1050,15 +548,6 @@ export class EnhancedLogManager {
|
|
|
1050
548
|
|
|
1051
549
|
`;
|
|
1052
550
|
|
|
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
551
|
if (this.rawLogFd !== null) {
|
|
1063
552
|
try {
|
|
1064
553
|
fs.writeSync(this.rawLogFd, endMarker);
|
|
@@ -1068,34 +557,6 @@ export class EnhancedLogManager {
|
|
|
1068
557
|
this.rawLogFd = null;
|
|
1069
558
|
}
|
|
1070
559
|
|
|
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
560
|
if (this.readableLogFd !== null) {
|
|
1100
561
|
try {
|
|
1101
562
|
fs.writeSync(this.readableLogFd, endMarker);
|
|
@@ -1107,22 +568,20 @@ export class EnhancedLogManager {
|
|
|
1107
568
|
}
|
|
1108
569
|
|
|
1109
570
|
/**
|
|
1110
|
-
* Extract the last error message from the
|
|
571
|
+
* Extract the last error message from the log
|
|
1111
572
|
*/
|
|
1112
573
|
public getLastError(): string | null {
|
|
1113
574
|
try {
|
|
1114
|
-
if (!fs.existsSync(this.
|
|
1115
|
-
const content = fs.readFileSync(this.
|
|
1116
|
-
// Look for lines containing error markers
|
|
575
|
+
if (!fs.existsSync(this.readableLogPath)) return null;
|
|
576
|
+
const content = fs.readFileSync(this.readableLogPath, 'utf8');
|
|
1117
577
|
const lines = content.split('\n').filter(l =>
|
|
1118
|
-
l.includes('[ERROR]') ||
|
|
1119
578
|
l.includes('❌') ||
|
|
579
|
+
l.includes('[ERROR]') ||
|
|
1120
580
|
l.includes('error:') ||
|
|
1121
581
|
l.includes('Fatal') ||
|
|
1122
582
|
l.includes('fail')
|
|
1123
583
|
);
|
|
1124
584
|
if (lines.length === 0) {
|
|
1125
|
-
// Fallback to last 5 lines if no specific error marker found
|
|
1126
585
|
const allLines = content.split('\n').filter(l => l.trim());
|
|
1127
586
|
return allLines.slice(-5).join('\n');
|
|
1128
587
|
}
|
|
@@ -1156,75 +615,41 @@ export function createLogManager(
|
|
|
1156
615
|
laneRunDir: string,
|
|
1157
616
|
laneName: string,
|
|
1158
617
|
config?: Partial<EnhancedLogConfig>,
|
|
1159
|
-
onParsedMessage?: (msg: ParsedMessage) => void
|
|
618
|
+
onParsedMessage?: (msg: ParsedMessage) => void,
|
|
619
|
+
laneIndex?: number
|
|
1160
620
|
): EnhancedLogManager {
|
|
1161
621
|
const session: LogSession = {
|
|
1162
622
|
id: `${laneName}-${Date.now().toString(36)}`,
|
|
1163
623
|
laneName,
|
|
1164
624
|
startTime: Date.now(),
|
|
625
|
+
laneIndex: laneIndex ?? 0,
|
|
626
|
+
taskIndex: 0,
|
|
1165
627
|
};
|
|
1166
628
|
|
|
1167
629
|
return new EnhancedLogManager(laneRunDir, session, config, onParsedMessage);
|
|
1168
630
|
}
|
|
1169
631
|
|
|
1170
632
|
/**
|
|
1171
|
-
* Read and parse JSON log file
|
|
633
|
+
* Read and parse JSON log file (legacy compatibility - returns empty array)
|
|
1172
634
|
*/
|
|
1173
635
|
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
|
-
}
|
|
636
|
+
return [];
|
|
1194
637
|
}
|
|
1195
638
|
|
|
1196
639
|
/**
|
|
1197
|
-
* Export logs
|
|
640
|
+
* Export logs (legacy compatibility)
|
|
1198
641
|
*/
|
|
1199
642
|
export function exportLogs(
|
|
1200
643
|
laneRunDir: string,
|
|
1201
644
|
format: 'text' | 'json' | 'markdown' | 'html',
|
|
1202
645
|
outputPath?: string
|
|
1203
646
|
): string {
|
|
1204
|
-
const
|
|
1205
|
-
const jsonLogPath = safeJoin(laneRunDir, 'terminal.jsonl');
|
|
647
|
+
const readableLogPath = safeJoin(laneRunDir, 'terminal-readable.log');
|
|
1206
648
|
|
|
1207
649
|
let output = '';
|
|
1208
650
|
|
|
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;
|
|
651
|
+
if (fs.existsSync(readableLogPath)) {
|
|
652
|
+
output = fs.readFileSync(readableLogPath, 'utf8');
|
|
1228
653
|
}
|
|
1229
654
|
|
|
1230
655
|
if (outputPath) {
|
|
@@ -1234,101 +659,13 @@ export function exportLogs(
|
|
|
1234
659
|
return output;
|
|
1235
660
|
}
|
|
1236
661
|
|
|
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;
|
|
662
|
+
// Legacy exports for backward compatibility
|
|
663
|
+
export class StreamingMessageParser {
|
|
664
|
+
constructor(onMessage: (msg: ParsedMessage) => void) {}
|
|
665
|
+
parseLine(line: string): void {}
|
|
666
|
+
flush(): void {}
|
|
1325
667
|
}
|
|
1326
668
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
.replace(/&/g, '&')
|
|
1330
|
-
.replace(/</g, '<')
|
|
1331
|
-
.replace(/>/g, '>')
|
|
1332
|
-
.replace(/"/g, '"')
|
|
1333
|
-
.replace(/'/g, ''');
|
|
669
|
+
export class CleanLogTransform {
|
|
670
|
+
constructor(config: EnhancedLogConfig, session: LogSession) {}
|
|
1334
671
|
}
|