@covibes/zeroshot 1.0.1 → 1.0.2
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/package.json +2 -1
- package/task-lib/attachable-watcher.js +202 -0
- package/task-lib/commands/clean.js +50 -0
- package/task-lib/commands/get-log-path.js +23 -0
- package/task-lib/commands/kill.js +32 -0
- package/task-lib/commands/list.js +105 -0
- package/task-lib/commands/logs.js +411 -0
- package/task-lib/commands/resume.js +41 -0
- package/task-lib/commands/run.js +48 -0
- package/task-lib/commands/schedule.js +105 -0
- package/task-lib/commands/scheduler-cmd.js +96 -0
- package/task-lib/commands/schedules.js +98 -0
- package/task-lib/commands/status.js +44 -0
- package/task-lib/commands/unschedule.js +16 -0
- package/task-lib/completion.js +9 -0
- package/task-lib/config.js +10 -0
- package/task-lib/name-generator.js +230 -0
- package/task-lib/package.json +3 -0
- package/task-lib/runner.js +123 -0
- package/task-lib/scheduler.js +252 -0
- package/task-lib/store.js +217 -0
- package/task-lib/tui/formatters.js +166 -0
- package/task-lib/tui/index.js +197 -0
- package/task-lib/tui/layout.js +111 -0
- package/task-lib/tui/renderer.js +119 -0
- package/task-lib/tui.js +384 -0
- package/task-lib/watcher.js +162 -0
package/task-lib/tui.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task TUI - Interactive task viewer
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - List all tasks
|
|
6
|
+
* - Navigate with arrow keys
|
|
7
|
+
* - Press Enter to view logs
|
|
8
|
+
* - Press Esc to go back to list
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import blessed from 'blessed';
|
|
12
|
+
import { loadTasks } from './store.js';
|
|
13
|
+
import { isProcessRunning } from './runner.js';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import os from 'os';
|
|
17
|
+
|
|
18
|
+
// Parse a single log line from JSON stream format
|
|
19
|
+
function parseLogLine(line) {
|
|
20
|
+
let trimmed = line.trim();
|
|
21
|
+
|
|
22
|
+
// Strip timestamp prefix if present: [1234567890]{...} -> {...}
|
|
23
|
+
const timestampMatch = trimmed.match(/^\[\d+\](.*)$/);
|
|
24
|
+
if (timestampMatch) {
|
|
25
|
+
trimmed = timestampMatch[1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Keep non-JSON lines
|
|
29
|
+
if (!trimmed.startsWith('{')) {
|
|
30
|
+
return trimmed ? trimmed + '\n' : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Parse JSON and extract relevant info
|
|
34
|
+
try {
|
|
35
|
+
const event = JSON.parse(trimmed);
|
|
36
|
+
|
|
37
|
+
// Extract text from content_block_delta
|
|
38
|
+
if (event.type === 'stream_event' && event.event?.type === 'content_block_delta') {
|
|
39
|
+
return event.event?.delta?.text || '';
|
|
40
|
+
}
|
|
41
|
+
// Extract tool use info
|
|
42
|
+
else if (event.type === 'stream_event' && event.event?.type === 'content_block_start') {
|
|
43
|
+
const block = event.event?.content_block;
|
|
44
|
+
if (block?.type === 'tool_use' && block?.name) {
|
|
45
|
+
return `\n[Tool: ${block.name}]\n`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Extract assistant messages
|
|
49
|
+
else if (event.type === 'assistant' && event.message?.content) {
|
|
50
|
+
let output = '';
|
|
51
|
+
for (const content of event.message.content) {
|
|
52
|
+
if (content.type === 'text') {
|
|
53
|
+
output += content.text;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
58
|
+
// Extract final result
|
|
59
|
+
else if (event.type === 'result') {
|
|
60
|
+
if (event.is_error) {
|
|
61
|
+
return `\n[ERROR] ${event.result || 'Unknown error'}\n`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Not JSON or parse error - skip
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class TaskTUI {
|
|
72
|
+
constructor(options = {}) {
|
|
73
|
+
this.tasks = [];
|
|
74
|
+
this.selectedIndex = 0;
|
|
75
|
+
this.viewMode = 'list'; // 'list' or 'detail'
|
|
76
|
+
this.selectedTask = null;
|
|
77
|
+
this.initialScrollDone = false;
|
|
78
|
+
this.refreshRate = options.refreshRate || 1000;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
start() {
|
|
82
|
+
// Create screen
|
|
83
|
+
this.screen = blessed.screen({
|
|
84
|
+
smartCSR: true,
|
|
85
|
+
title: 'Vibe Task Watch',
|
|
86
|
+
dockBorders: true,
|
|
87
|
+
fullUnicode: true,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Create main list view
|
|
91
|
+
this.listBox = blessed.list({
|
|
92
|
+
parent: this.screen,
|
|
93
|
+
top: 0,
|
|
94
|
+
left: 0,
|
|
95
|
+
width: '100%',
|
|
96
|
+
height: '100%-2',
|
|
97
|
+
keys: true,
|
|
98
|
+
vi: true,
|
|
99
|
+
mouse: true,
|
|
100
|
+
border: {
|
|
101
|
+
type: 'line',
|
|
102
|
+
},
|
|
103
|
+
style: {
|
|
104
|
+
selected: {
|
|
105
|
+
bg: 'blue',
|
|
106
|
+
fg: 'white',
|
|
107
|
+
bold: true,
|
|
108
|
+
},
|
|
109
|
+
border: {
|
|
110
|
+
fg: 'cyan',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
tags: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Create detail view (hidden initially)
|
|
117
|
+
this.detailBox = blessed.box({
|
|
118
|
+
parent: this.screen,
|
|
119
|
+
top: 0,
|
|
120
|
+
left: 0,
|
|
121
|
+
width: '100%',
|
|
122
|
+
height: '100%-2',
|
|
123
|
+
border: {
|
|
124
|
+
type: 'line',
|
|
125
|
+
},
|
|
126
|
+
style: {
|
|
127
|
+
border: {
|
|
128
|
+
fg: 'cyan',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
scrollable: true,
|
|
132
|
+
alwaysScroll: true,
|
|
133
|
+
keys: true,
|
|
134
|
+
vi: true,
|
|
135
|
+
mouse: true,
|
|
136
|
+
scrollbar: {
|
|
137
|
+
ch: '│',
|
|
138
|
+
style: {
|
|
139
|
+
bg: 'cyan',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
tags: false,
|
|
143
|
+
hidden: true,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Status bar
|
|
147
|
+
this.statusBar = blessed.box({
|
|
148
|
+
parent: this.screen,
|
|
149
|
+
bottom: 0,
|
|
150
|
+
left: 0,
|
|
151
|
+
width: '100%',
|
|
152
|
+
height: 2,
|
|
153
|
+
content: '',
|
|
154
|
+
tags: true,
|
|
155
|
+
style: {
|
|
156
|
+
bg: 'blue',
|
|
157
|
+
fg: 'white',
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Setup keybindings
|
|
162
|
+
this.setupKeybindings();
|
|
163
|
+
|
|
164
|
+
// Initial render
|
|
165
|
+
this.refreshData();
|
|
166
|
+
this.render();
|
|
167
|
+
|
|
168
|
+
// Start polling
|
|
169
|
+
this.pollInterval = setInterval(() => {
|
|
170
|
+
this.refreshData();
|
|
171
|
+
this.render();
|
|
172
|
+
}, this.refreshRate);
|
|
173
|
+
|
|
174
|
+
// Render screen
|
|
175
|
+
this.screen.render();
|
|
176
|
+
|
|
177
|
+
// Focus on list after layout is established
|
|
178
|
+
this.listBox.focus();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setupKeybindings() {
|
|
182
|
+
// Quit
|
|
183
|
+
this.screen.key(['q', 'C-c'], () => {
|
|
184
|
+
if (this.pollInterval) {
|
|
185
|
+
clearInterval(this.pollInterval);
|
|
186
|
+
}
|
|
187
|
+
process.exit(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Enter - view details
|
|
191
|
+
this.listBox.key(['enter', 'space'], () => {
|
|
192
|
+
if (this.tasks.length > 0) {
|
|
193
|
+
this.selectedTask = this.tasks[this.selectedIndex];
|
|
194
|
+
this.viewMode = 'detail';
|
|
195
|
+
this.initialScrollDone = false;
|
|
196
|
+
this.listBox.hide();
|
|
197
|
+
this.detailBox.show();
|
|
198
|
+
this.screen.render();
|
|
199
|
+
setImmediate(() => {
|
|
200
|
+
this.detailBox.focus();
|
|
201
|
+
this.render();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Escape - back to list
|
|
207
|
+
this.screen.key(['escape'], () => {
|
|
208
|
+
if (this.viewMode === 'detail') {
|
|
209
|
+
this.viewMode = 'list';
|
|
210
|
+
this.detailBox.hide();
|
|
211
|
+
this.listBox.show();
|
|
212
|
+
this.screen.render();
|
|
213
|
+
setImmediate(() => {
|
|
214
|
+
this.listBox.focus();
|
|
215
|
+
this.render();
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Arrow keys for list
|
|
221
|
+
this.listBox.key(['up', 'k'], () => {
|
|
222
|
+
if (this.selectedIndex > 0) {
|
|
223
|
+
this.selectedIndex--;
|
|
224
|
+
this.listBox.select(this.selectedIndex);
|
|
225
|
+
this.render();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
this.listBox.key(['down', 'j'], () => {
|
|
230
|
+
if (this.selectedIndex < this.tasks.length - 1) {
|
|
231
|
+
this.selectedIndex++;
|
|
232
|
+
this.listBox.select(this.selectedIndex);
|
|
233
|
+
this.render();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Scroll in detail view
|
|
238
|
+
this.detailBox.key(['up', 'k'], () => {
|
|
239
|
+
this.detailBox.scroll(-1);
|
|
240
|
+
this.screen.render();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
this.detailBox.key(['down', 'j'], () => {
|
|
244
|
+
this.detailBox.scroll(1);
|
|
245
|
+
this.screen.render();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
this.detailBox.key(['pageup', 'u'], () => {
|
|
249
|
+
this.detailBox.scroll(-10);
|
|
250
|
+
this.screen.render();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
this.detailBox.key(['pagedown', 'd'], () => {
|
|
254
|
+
this.detailBox.scroll(10);
|
|
255
|
+
this.screen.render();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
refreshData() {
|
|
260
|
+
const tasks = loadTasks();
|
|
261
|
+
this.tasks = Object.values(tasks);
|
|
262
|
+
|
|
263
|
+
// Sort by creation date, newest first
|
|
264
|
+
this.tasks.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
265
|
+
|
|
266
|
+
// Verify running status
|
|
267
|
+
for (const task of this.tasks) {
|
|
268
|
+
if (task.status === 'running' && !isProcessRunning(task.pid)) {
|
|
269
|
+
task.status = 'stale';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
render() {
|
|
275
|
+
if (this.viewMode === 'list') {
|
|
276
|
+
this.renderList();
|
|
277
|
+
} else {
|
|
278
|
+
this.renderDetail();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
renderList() {
|
|
283
|
+
const items = this.tasks.map((task) => {
|
|
284
|
+
const statusIcon =
|
|
285
|
+
{
|
|
286
|
+
running: '{blue-fg}●{/}',
|
|
287
|
+
completed: '{green-fg}●{/}',
|
|
288
|
+
failed: '{red-fg}●{/}',
|
|
289
|
+
stale: '{yellow-fg}●{/}',
|
|
290
|
+
}[task.status] || '{gray-fg}●{/}';
|
|
291
|
+
|
|
292
|
+
const age = this.getAge(task.createdAt);
|
|
293
|
+
const cwd = task.cwd.replace(os.homedir(), '~');
|
|
294
|
+
|
|
295
|
+
return `${statusIcon} {cyan-fg}${task.id.padEnd(25)}{/} {gray-fg}${task.status.padEnd(10)} ${age.padEnd(10)} ${cwd}{/}`;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
this.listBox.setItems(items);
|
|
299
|
+
this.listBox.setLabel(
|
|
300
|
+
` Tasks (${this.tasks.length}) - ↑↓ navigate, Enter to view logs, q to quit `
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// Update status bar
|
|
304
|
+
if (this.tasks.length > 0) {
|
|
305
|
+
const task = this.tasks[this.selectedIndex];
|
|
306
|
+
this.statusBar.setContent(
|
|
307
|
+
` Selected: {cyan-fg}${task.id}{/} | Status: ${this.getStatusColor(task.status)} | Press Enter to view logs`
|
|
308
|
+
);
|
|
309
|
+
} else {
|
|
310
|
+
this.statusBar.setContent(' No tasks found');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this.screen.render();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
renderDetail() {
|
|
317
|
+
if (!this.selectedTask) return;
|
|
318
|
+
|
|
319
|
+
const task = this.selectedTask;
|
|
320
|
+
|
|
321
|
+
// Load and parse log file
|
|
322
|
+
const logPath = path.join(os.homedir(), '.claude-zeroshot', 'logs', `${task.id}.log`);
|
|
323
|
+
let content = '';
|
|
324
|
+
|
|
325
|
+
if (fs.existsSync(logPath)) {
|
|
326
|
+
try {
|
|
327
|
+
const rawContent = fs.readFileSync(logPath, 'utf8');
|
|
328
|
+
const lines = rawContent.split('\n');
|
|
329
|
+
|
|
330
|
+
// Parse JSON stream and extract human-readable content
|
|
331
|
+
for (const line of lines) {
|
|
332
|
+
const parsed = parseLogLine(line);
|
|
333
|
+
if (parsed) content += parsed;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Strip ANSI codes for clean display
|
|
337
|
+
// eslint-disable-next-line no-control-regex
|
|
338
|
+
content = content.replace(/\x1b\[[0-9;]*m/g, '');
|
|
339
|
+
} catch (error) {
|
|
340
|
+
content = `Error reading log: ${error.message}`;
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
content = 'No log file found';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.detailBox.setContent(content);
|
|
347
|
+
this.detailBox.setLabel(` ${task.id} | ${task.status} | ↑↓ scroll, Esc back, q quit `);
|
|
348
|
+
|
|
349
|
+
// Update status bar
|
|
350
|
+
this.statusBar.setContent(` ${task.id} | Esc to go back`);
|
|
351
|
+
|
|
352
|
+
// Scroll to bottom only on first view
|
|
353
|
+
if (!this.initialScrollDone) {
|
|
354
|
+
this.detailBox.setScrollPerc(100);
|
|
355
|
+
this.initialScrollDone = true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this.screen.render();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
getAge(dateStr) {
|
|
362
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
363
|
+
const mins = Math.floor(diff / 60000);
|
|
364
|
+
const hours = Math.floor(mins / 60);
|
|
365
|
+
const days = Math.floor(hours / 24);
|
|
366
|
+
|
|
367
|
+
if (days > 0) return `${days}d ago`;
|
|
368
|
+
if (hours > 0) return `${hours}h ago`;
|
|
369
|
+
if (mins > 0) return `${mins}m ago`;
|
|
370
|
+
return 'just now';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
getStatusColor(status) {
|
|
374
|
+
const colors = {
|
|
375
|
+
running: '{blue-fg}running{/}',
|
|
376
|
+
completed: '{green-fg}completed{/}',
|
|
377
|
+
failed: '{red-fg}failed{/}',
|
|
378
|
+
stale: '{yellow-fg}stale{/}',
|
|
379
|
+
};
|
|
380
|
+
return colors[status] || status;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export default TaskTUI;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Watcher process - spawns and monitors a claude process
|
|
5
|
+
* Runs detached from parent, updates task status on completion
|
|
6
|
+
*
|
|
7
|
+
* Uses regular spawn (not PTY) - Claude CLI with --print is non-interactive
|
|
8
|
+
* PTY causes EIO errors when processes are killed/OOM'd
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { appendFileSync } from 'fs';
|
|
13
|
+
import { dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { updateTask } from './store.js';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
const [, , taskId, cwd, logFile, argsJson, configJson] = process.argv;
|
|
20
|
+
const args = JSON.parse(argsJson);
|
|
21
|
+
const config = configJson ? JSON.parse(configJson) : {};
|
|
22
|
+
|
|
23
|
+
function log(msg) {
|
|
24
|
+
appendFileSync(logFile, msg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Build environment - remove API key to force subscription credentials
|
|
28
|
+
const env = { ...process.env };
|
|
29
|
+
delete env.ANTHROPIC_API_KEY;
|
|
30
|
+
|
|
31
|
+
// Add model flag - priority: config.model > ANTHROPIC_MODEL env var
|
|
32
|
+
const claudeArgs = [...args];
|
|
33
|
+
const model = config.model || env.ANTHROPIC_MODEL;
|
|
34
|
+
if (model && !claudeArgs.includes('--model')) {
|
|
35
|
+
claudeArgs.unshift('--model', model);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Spawn claude using regular child_process (not PTY)
|
|
39
|
+
// --print mode is non-interactive, PTY adds overhead and causes EIO on OOM
|
|
40
|
+
const child = spawn('claude', claudeArgs, {
|
|
41
|
+
cwd,
|
|
42
|
+
env,
|
|
43
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Update task with PID
|
|
47
|
+
updateTask(taskId, { pid: child.pid });
|
|
48
|
+
|
|
49
|
+
// For JSON schema output with silent mode, capture ONLY the structured_output JSON
|
|
50
|
+
const silentJsonMode =
|
|
51
|
+
config.outputFormat === 'json' && config.jsonSchema && config.silentJsonOutput;
|
|
52
|
+
let finalResultJson = null;
|
|
53
|
+
|
|
54
|
+
// Buffer for incomplete lines (need complete lines to add timestamps)
|
|
55
|
+
let stdoutBuffer = '';
|
|
56
|
+
|
|
57
|
+
// Process stdout data
|
|
58
|
+
// CRITICAL: Prepend timestamp to each line for real-time tracking in cluster
|
|
59
|
+
// Format: [1733301234567]{json...} - consumers parse timestamp for accurate timing
|
|
60
|
+
child.stdout.on('data', (data) => {
|
|
61
|
+
const chunk = data.toString();
|
|
62
|
+
const timestamp = Date.now();
|
|
63
|
+
|
|
64
|
+
if (silentJsonMode) {
|
|
65
|
+
// Parse each line to find the one with structured_output
|
|
66
|
+
stdoutBuffer += chunk;
|
|
67
|
+
const lines = stdoutBuffer.split('\n');
|
|
68
|
+
stdoutBuffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
69
|
+
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (!line.trim()) continue;
|
|
72
|
+
try {
|
|
73
|
+
const json = JSON.parse(line);
|
|
74
|
+
if (json.structured_output) {
|
|
75
|
+
finalResultJson = line;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Not JSON or incomplete, skip
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// Normal mode - stream with timestamps on each complete line
|
|
83
|
+
stdoutBuffer += chunk;
|
|
84
|
+
const lines = stdoutBuffer.split('\n');
|
|
85
|
+
stdoutBuffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
// Timestamp each line: [epochMs]originalContent
|
|
89
|
+
log(`[${timestamp}]${line}\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Buffer for stderr incomplete lines
|
|
95
|
+
let stderrBuffer = '';
|
|
96
|
+
|
|
97
|
+
// Stream stderr to log with timestamps
|
|
98
|
+
child.stderr.on('data', (data) => {
|
|
99
|
+
const chunk = data.toString();
|
|
100
|
+
const timestamp = Date.now();
|
|
101
|
+
|
|
102
|
+
stderrBuffer += chunk;
|
|
103
|
+
const lines = stderrBuffer.split('\n');
|
|
104
|
+
stderrBuffer = lines.pop() || '';
|
|
105
|
+
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
log(`[${timestamp}]${line}\n`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Handle process exit
|
|
112
|
+
child.on('close', (code, signal) => {
|
|
113
|
+
const timestamp = Date.now();
|
|
114
|
+
|
|
115
|
+
// Flush any remaining buffered stdout
|
|
116
|
+
if (stdoutBuffer.trim()) {
|
|
117
|
+
if (silentJsonMode) {
|
|
118
|
+
try {
|
|
119
|
+
const json = JSON.parse(stdoutBuffer);
|
|
120
|
+
if (json.structured_output) {
|
|
121
|
+
finalResultJson = stdoutBuffer;
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Not valid JSON
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
log(`[${timestamp}]${stdoutBuffer}\n`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Flush any remaining buffered stderr
|
|
132
|
+
if (stderrBuffer.trim()) {
|
|
133
|
+
log(`[${timestamp}]${stderrBuffer}\n`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// In silent JSON mode, log ONLY the final structured_output JSON
|
|
137
|
+
if (silentJsonMode && finalResultJson) {
|
|
138
|
+
log(finalResultJson + '\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Skip footer for pure JSON output
|
|
142
|
+
if (config.outputFormat !== 'json') {
|
|
143
|
+
log(`\n${'='.repeat(50)}\n`);
|
|
144
|
+
log(`Finished: ${new Date().toISOString()}\n`);
|
|
145
|
+
log(`Exit code: ${code}, Signal: ${signal}\n`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Simple status: completed if exit 0, failed otherwise
|
|
149
|
+
const status = code === 0 ? 'completed' : 'failed';
|
|
150
|
+
updateTask(taskId, {
|
|
151
|
+
status,
|
|
152
|
+
exitCode: code,
|
|
153
|
+
error: signal ? `Killed by ${signal}` : null,
|
|
154
|
+
});
|
|
155
|
+
process.exit(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
child.on('error', (err) => {
|
|
159
|
+
log(`\nError: ${err.message}\n`);
|
|
160
|
+
updateTask(taskId, { status: 'failed', error: err.message });
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|