@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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { existsSync, statSync, readFileSync, openSync, readSync, closeSync } from 'fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getTask } from '../store.js';
|
|
4
|
+
import { isProcessRunning } from '../runner.js';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
|
|
7
|
+
// Import cluster's stream parser (shared between task and cluster)
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { parseChunk } = require('../../lib/stream-json-parser');
|
|
10
|
+
|
|
11
|
+
// Tool icons for different tool types
|
|
12
|
+
const TOOL_ICONS = {
|
|
13
|
+
Read: 'š',
|
|
14
|
+
Write: 'š',
|
|
15
|
+
Edit: 'āļø',
|
|
16
|
+
Bash: 'š»',
|
|
17
|
+
Glob: 'š',
|
|
18
|
+
Grep: 'š',
|
|
19
|
+
WebFetch: 'š',
|
|
20
|
+
WebSearch: 'š',
|
|
21
|
+
Task: 'š¤',
|
|
22
|
+
TodoWrite: 'š',
|
|
23
|
+
AskUserQuestion: 'ā',
|
|
24
|
+
BashOutput: 'š¤',
|
|
25
|
+
KillShell: 'šŖ',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function getToolIcon(toolName) {
|
|
29
|
+
return TOOL_ICONS[toolName] || 'š§';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Format tool call input for display
|
|
33
|
+
function formatToolCall(toolName, input) {
|
|
34
|
+
if (!input) return '';
|
|
35
|
+
|
|
36
|
+
switch (toolName) {
|
|
37
|
+
case 'Bash':
|
|
38
|
+
return input.command ? `$ ${input.command}` : '';
|
|
39
|
+
case 'Read':
|
|
40
|
+
return input.file_path ? input.file_path.split('/').slice(-2).join('/') : '';
|
|
41
|
+
case 'Write':
|
|
42
|
+
return input.file_path ? `ā ${input.file_path.split('/').slice(-2).join('/')}` : '';
|
|
43
|
+
case 'Edit':
|
|
44
|
+
return input.file_path ? input.file_path.split('/').slice(-2).join('/') : '';
|
|
45
|
+
case 'Glob':
|
|
46
|
+
return input.pattern || '';
|
|
47
|
+
case 'Grep':
|
|
48
|
+
return input.pattern ? `/${input.pattern}/` : '';
|
|
49
|
+
case 'WebFetch':
|
|
50
|
+
return input.url ? input.url.substring(0, 50) : '';
|
|
51
|
+
case 'WebSearch':
|
|
52
|
+
return input.query ? `"${input.query}"` : '';
|
|
53
|
+
case 'Task':
|
|
54
|
+
return input.description || '';
|
|
55
|
+
case 'TodoWrite':
|
|
56
|
+
if (input.todos && Array.isArray(input.todos)) {
|
|
57
|
+
const statusCounts = {};
|
|
58
|
+
input.todos.forEach((todo) => {
|
|
59
|
+
statusCounts[todo.status] = (statusCounts[todo.status] || 0) + 1;
|
|
60
|
+
});
|
|
61
|
+
const parts = Object.entries(statusCounts).map(
|
|
62
|
+
([status, count]) => `${count} ${status.replace('_', ' ')}`
|
|
63
|
+
);
|
|
64
|
+
return `${input.todos.length} todo${input.todos.length === 1 ? '' : 's'} (${parts.join(', ')})`;
|
|
65
|
+
}
|
|
66
|
+
return '';
|
|
67
|
+
case 'AskUserQuestion':
|
|
68
|
+
if (input.questions && Array.isArray(input.questions)) {
|
|
69
|
+
const q = input.questions[0];
|
|
70
|
+
const preview = q.question.substring(0, 50);
|
|
71
|
+
return input.questions.length > 1
|
|
72
|
+
? `${input.questions.length} questions: "${preview}..."`
|
|
73
|
+
: `"${preview}${q.question.length > 50 ? '...' : ''}"`;
|
|
74
|
+
}
|
|
75
|
+
return '';
|
|
76
|
+
default:
|
|
77
|
+
// For unknown tools, show first key-value pair
|
|
78
|
+
const keys = Object.keys(input);
|
|
79
|
+
if (keys.length > 0) {
|
|
80
|
+
const val = String(input[keys[0]]).substring(0, 40);
|
|
81
|
+
return val.length < String(input[keys[0]]).length ? val + '...' : val;
|
|
82
|
+
}
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Format tool result for display
|
|
88
|
+
function formatToolResult(content, isError, toolName, toolInput) {
|
|
89
|
+
if (!content) return isError ? 'error' : 'done';
|
|
90
|
+
|
|
91
|
+
// For errors, show full message
|
|
92
|
+
if (isError) {
|
|
93
|
+
const firstLine = content.split('\n')[0].substring(0, 80);
|
|
94
|
+
return chalk.red(firstLine);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// For TodoWrite, show the actual todo items
|
|
98
|
+
if (toolName === 'TodoWrite' && toolInput?.todos && Array.isArray(toolInput.todos)) {
|
|
99
|
+
const todos = toolInput.todos;
|
|
100
|
+
if (todos.length === 0) return chalk.dim('no todos');
|
|
101
|
+
if (todos.length === 1) {
|
|
102
|
+
const status =
|
|
103
|
+
todos[0].status === 'completed' ? 'ā' : todos[0].status === 'in_progress' ? 'ā§' : 'ā';
|
|
104
|
+
return chalk.dim(
|
|
105
|
+
`${status} ${todos[0].content.substring(0, 50)}${todos[0].content.length > 50 ? '...' : ''}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
// Multiple todos - show first one as preview
|
|
109
|
+
const status =
|
|
110
|
+
todos[0].status === 'completed' ? 'ā' : todos[0].status === 'in_progress' ? 'ā§' : 'ā';
|
|
111
|
+
return chalk.dim(
|
|
112
|
+
`${status} ${todos[0].content.substring(0, 40)}... (+${todos.length - 1} more)`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// For success, show summary
|
|
117
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
118
|
+
if (lines.length === 0) return 'done';
|
|
119
|
+
if (lines.length === 1) {
|
|
120
|
+
const line = lines[0].substring(0, 60);
|
|
121
|
+
return chalk.dim(line.length < lines[0].length ? line + '...' : line);
|
|
122
|
+
}
|
|
123
|
+
// Multiple lines - show count
|
|
124
|
+
return chalk.dim(`${lines.length} lines`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Line buffer for accumulating text that streams without newlines
|
|
128
|
+
let lineBuffer = '';
|
|
129
|
+
let currentToolCall = null;
|
|
130
|
+
|
|
131
|
+
function resetState() {
|
|
132
|
+
lineBuffer = '';
|
|
133
|
+
currentToolCall = null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Flush pending text in buffer
|
|
137
|
+
function flushLineBuffer() {
|
|
138
|
+
if (lineBuffer.trim()) {
|
|
139
|
+
process.stdout.write(lineBuffer);
|
|
140
|
+
if (!lineBuffer.endsWith('\n')) {
|
|
141
|
+
process.stdout.write('\n');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
lineBuffer = '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Accumulate text and print complete lines
|
|
148
|
+
function accumulateText(text) {
|
|
149
|
+
lineBuffer += text;
|
|
150
|
+
|
|
151
|
+
// Print complete lines, keep incomplete in buffer
|
|
152
|
+
const lines = lineBuffer.split('\n');
|
|
153
|
+
if (lines.length > 1) {
|
|
154
|
+
// Print all complete lines
|
|
155
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
156
|
+
console.log(lines[i]);
|
|
157
|
+
}
|
|
158
|
+
// Keep incomplete line in buffer
|
|
159
|
+
lineBuffer = lines[lines.length - 1];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Process parsed events and output formatted content
|
|
164
|
+
function processEvent(event) {
|
|
165
|
+
switch (event.type) {
|
|
166
|
+
case 'text':
|
|
167
|
+
accumulateText(event.text);
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'thinking':
|
|
171
|
+
case 'thinking_start':
|
|
172
|
+
if (event.text) {
|
|
173
|
+
console.log(chalk.dim.italic(event.text));
|
|
174
|
+
} else if (event.type === 'thinking_start') {
|
|
175
|
+
console.log(chalk.dim.italic('š thinking...'));
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case 'tool_start':
|
|
180
|
+
flushLineBuffer();
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case 'tool_call':
|
|
184
|
+
flushLineBuffer();
|
|
185
|
+
const icon = getToolIcon(event.toolName);
|
|
186
|
+
const toolDesc = formatToolCall(event.toolName, event.input);
|
|
187
|
+
console.log(`${icon} ${chalk.cyan(event.toolName)} ${chalk.dim(toolDesc)}`);
|
|
188
|
+
currentToolCall = { toolName: event.toolName, input: event.input };
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case 'tool_input':
|
|
192
|
+
// Streaming tool input JSON - skip (shown in tool_call)
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case 'tool_result':
|
|
196
|
+
const status = event.isError ? chalk.red('ā') : chalk.green('ā');
|
|
197
|
+
const resultDesc = formatToolResult(
|
|
198
|
+
event.content,
|
|
199
|
+
event.isError,
|
|
200
|
+
currentToolCall?.toolName,
|
|
201
|
+
currentToolCall?.input
|
|
202
|
+
);
|
|
203
|
+
console.log(` ${status} ${resultDesc}`);
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case 'result':
|
|
207
|
+
flushLineBuffer();
|
|
208
|
+
if (event.error) {
|
|
209
|
+
console.log(chalk.red(`\nā ERROR: ${event.error}`));
|
|
210
|
+
} else {
|
|
211
|
+
console.log(chalk.green(`\nā Completed`));
|
|
212
|
+
if (event.cost) {
|
|
213
|
+
console.log(chalk.dim(` Cost: $${event.cost.toFixed(4)}`));
|
|
214
|
+
}
|
|
215
|
+
if (event.duration) {
|
|
216
|
+
const mins = Math.floor(event.duration / 60000);
|
|
217
|
+
const secs = Math.floor((event.duration % 60000) / 1000);
|
|
218
|
+
console.log(chalk.dim(` Duration: ${mins}m ${secs}s`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case 'block_end':
|
|
224
|
+
// Content block ended
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
case 'multi':
|
|
228
|
+
// Multiple events
|
|
229
|
+
if (event.events) {
|
|
230
|
+
for (const e of event.events) {
|
|
231
|
+
processEvent(e);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Parse a raw log line (may have timestamp prefix)
|
|
239
|
+
function parseLogLine(line) {
|
|
240
|
+
let trimmed = line.trim();
|
|
241
|
+
if (!trimmed) return [];
|
|
242
|
+
|
|
243
|
+
// Strip timestamp prefix if present: [1234567890]{...} -> {...}
|
|
244
|
+
const timestampMatch = trimmed.match(/^\[\d+\](.*)$/);
|
|
245
|
+
if (timestampMatch) {
|
|
246
|
+
trimmed = timestampMatch[1];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Non-JSON lines output as-is
|
|
250
|
+
if (!trimmed.startsWith('{')) {
|
|
251
|
+
return [{ type: 'text', text: trimmed + '\n' }];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Parse JSON using cluster's parser
|
|
255
|
+
return parseChunk(trimmed);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function showLogs(taskId, options = {}) {
|
|
259
|
+
const task = getTask(taskId);
|
|
260
|
+
|
|
261
|
+
if (!task) {
|
|
262
|
+
console.log(chalk.red(`Task not found: ${taskId}`));
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!existsSync(task.logFile)) {
|
|
267
|
+
console.log(chalk.yellow('Log file not found (task may still be starting).'));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const lines = options.lines || 50;
|
|
272
|
+
const follow = options.follow;
|
|
273
|
+
const watch = options.watch;
|
|
274
|
+
|
|
275
|
+
// IMPORTANT: -f should stream logs (like tail -f), -w launches interactive TUI
|
|
276
|
+
// This prevents confusion when users expect tail-like behavior
|
|
277
|
+
if (watch) {
|
|
278
|
+
// Use TUI for watch mode (interactive interface)
|
|
279
|
+
const { default: TaskLogsTUI } = await import('../tui/index.js');
|
|
280
|
+
const tui = new TaskLogsTUI({
|
|
281
|
+
taskId: task.id,
|
|
282
|
+
logFile: task.logFile,
|
|
283
|
+
taskInfo: {
|
|
284
|
+
status: task.status,
|
|
285
|
+
createdAt: task.createdAt,
|
|
286
|
+
prompt: task.prompt,
|
|
287
|
+
},
|
|
288
|
+
pid: task.pid,
|
|
289
|
+
});
|
|
290
|
+
await tui.start();
|
|
291
|
+
} else if (follow) {
|
|
292
|
+
// Stream logs continuously (like tail -f) - show last N lines first
|
|
293
|
+
await tailFollow(task.logFile, task.pid, lines);
|
|
294
|
+
} else {
|
|
295
|
+
await tailLines(task.logFile, lines);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function tailLines(file, n) {
|
|
300
|
+
resetState();
|
|
301
|
+
const rawContent = readFileSync(file, 'utf-8');
|
|
302
|
+
const rawLines = rawContent.split('\n');
|
|
303
|
+
|
|
304
|
+
// Parse and process all events
|
|
305
|
+
const allEvents = [];
|
|
306
|
+
for (const line of rawLines) {
|
|
307
|
+
const events = parseLogLine(line);
|
|
308
|
+
allEvents.push(...events);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Tail to last n events
|
|
312
|
+
const tailedEvents = allEvents.slice(-n);
|
|
313
|
+
for (const event of tailedEvents) {
|
|
314
|
+
processEvent(event);
|
|
315
|
+
}
|
|
316
|
+
flushLineBuffer();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function tailFollow(file, pid, lines = 50) {
|
|
320
|
+
resetState();
|
|
321
|
+
// First, output last N events (like tail -f behavior)
|
|
322
|
+
const rawContent = readFileSync(file, 'utf-8');
|
|
323
|
+
const rawLines = rawContent.split('\n');
|
|
324
|
+
|
|
325
|
+
// Parse all events first
|
|
326
|
+
const allEvents = [];
|
|
327
|
+
for (const line of rawLines) {
|
|
328
|
+
const events = parseLogLine(line);
|
|
329
|
+
allEvents.push(...events);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Output only the last N events
|
|
333
|
+
const tailedEvents = allEvents.slice(-lines);
|
|
334
|
+
for (const event of tailedEvents) {
|
|
335
|
+
processEvent(event);
|
|
336
|
+
}
|
|
337
|
+
flushLineBuffer();
|
|
338
|
+
|
|
339
|
+
// Poll for changes (more reliable than fs.watch)
|
|
340
|
+
let lastSize = statSync(file).size;
|
|
341
|
+
let noChangeCount = 0;
|
|
342
|
+
|
|
343
|
+
const interval = setInterval(() => {
|
|
344
|
+
try {
|
|
345
|
+
const currentSize = statSync(file).size;
|
|
346
|
+
|
|
347
|
+
if (currentSize > lastSize) {
|
|
348
|
+
// Read new content
|
|
349
|
+
const buffer = Buffer.alloc(currentSize - lastSize);
|
|
350
|
+
const fd = openSync(file, 'r');
|
|
351
|
+
readSync(fd, buffer, 0, buffer.length, lastSize);
|
|
352
|
+
closeSync(fd);
|
|
353
|
+
|
|
354
|
+
// Parse and output new lines
|
|
355
|
+
const newLines = buffer.toString().split('\n');
|
|
356
|
+
for (const line of newLines) {
|
|
357
|
+
const events = parseLogLine(line);
|
|
358
|
+
for (const event of events) {
|
|
359
|
+
processEvent(event);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
flushLineBuffer();
|
|
363
|
+
|
|
364
|
+
lastSize = currentSize;
|
|
365
|
+
noChangeCount = 0;
|
|
366
|
+
} else {
|
|
367
|
+
noChangeCount++;
|
|
368
|
+
|
|
369
|
+
// Check if process is still running after 5 seconds of no output
|
|
370
|
+
if (noChangeCount >= 10 && pid && !isProcessRunning(pid)) {
|
|
371
|
+
// Read any final content
|
|
372
|
+
const finalSize = statSync(file).size;
|
|
373
|
+
if (finalSize > lastSize) {
|
|
374
|
+
const buffer = Buffer.alloc(finalSize - lastSize);
|
|
375
|
+
const fd = openSync(file, 'r');
|
|
376
|
+
readSync(fd, buffer, 0, buffer.length, lastSize);
|
|
377
|
+
closeSync(fd);
|
|
378
|
+
|
|
379
|
+
const finalLines = buffer.toString().split('\n');
|
|
380
|
+
for (const line of finalLines) {
|
|
381
|
+
const events = parseLogLine(line);
|
|
382
|
+
for (const event of events) {
|
|
383
|
+
processEvent(event);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
flushLineBuffer();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
console.log(chalk.dim('\n--- Task completed ---'));
|
|
390
|
+
clearInterval(interval);
|
|
391
|
+
process.exit(0);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
// File might have been deleted
|
|
396
|
+
console.log(chalk.red(`\nError reading log: ${err.message}`));
|
|
397
|
+
clearInterval(interval);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
}, 500); // Poll every 500ms
|
|
401
|
+
|
|
402
|
+
// Keep alive until Ctrl+C
|
|
403
|
+
process.on('SIGINT', () => {
|
|
404
|
+
clearInterval(interval);
|
|
405
|
+
console.log(chalk.dim('\nStopped following.'));
|
|
406
|
+
process.exit(0);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Keep process running
|
|
410
|
+
await new Promise(() => {});
|
|
411
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getTask } from '../store.js';
|
|
3
|
+
import { spawnTask } from '../runner.js';
|
|
4
|
+
|
|
5
|
+
export function resumeTask(taskId, newPrompt) {
|
|
6
|
+
const task = getTask(taskId);
|
|
7
|
+
|
|
8
|
+
if (!task) {
|
|
9
|
+
console.log(chalk.red(`Task not found: ${taskId}`));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (task.status === 'running') {
|
|
14
|
+
console.log(
|
|
15
|
+
chalk.yellow(`Task is still running. Use 'zeroshot logs -f ${taskId}' to follow output.`)
|
|
16
|
+
);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const prompt = newPrompt || 'Continue from where you left off. Complete the task.';
|
|
21
|
+
|
|
22
|
+
console.log(chalk.dim(`Resuming task ${taskId}...`));
|
|
23
|
+
console.log(chalk.dim(`Original prompt: ${task.prompt}`));
|
|
24
|
+
console.log(chalk.dim(`Resume prompt: ${prompt}`));
|
|
25
|
+
|
|
26
|
+
const newTask = spawnTask(prompt, {
|
|
27
|
+
cwd: task.cwd,
|
|
28
|
+
continue: true, // Use --continue to load most recent session in that directory
|
|
29
|
+
sessionId: task.sessionId,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log(chalk.green(`\nā Resumed as new task: ${chalk.cyan(newTask.id)}`));
|
|
33
|
+
console.log(chalk.dim(` PID: ${newTask.pid}`));
|
|
34
|
+
console.log(chalk.dim(` Log: ${newTask.logFile}`));
|
|
35
|
+
|
|
36
|
+
console.log(chalk.dim('\nCommands:'));
|
|
37
|
+
console.log(chalk.dim(` zeroshot logs -f ${newTask.id} # Follow output`));
|
|
38
|
+
console.log();
|
|
39
|
+
|
|
40
|
+
return newTask;
|
|
41
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { spawnTask } from '../runner.js';
|
|
3
|
+
|
|
4
|
+
export function runTask(prompt, options = {}) {
|
|
5
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
6
|
+
console.log(chalk.red('Error: Prompt is required'));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const outputFormat = options.outputFormat || 'stream-json';
|
|
11
|
+
const jsonSchema = options.jsonSchema;
|
|
12
|
+
const silentJsonOutput = options.silentJsonOutput || false;
|
|
13
|
+
|
|
14
|
+
console.log(chalk.dim('Spawning Claude task...'));
|
|
15
|
+
if (options.model) {
|
|
16
|
+
console.log(chalk.dim(` Model: ${options.model}`));
|
|
17
|
+
}
|
|
18
|
+
if (jsonSchema && outputFormat === 'json') {
|
|
19
|
+
console.log(chalk.dim(` JSON Schema: enforced`));
|
|
20
|
+
if (silentJsonOutput) {
|
|
21
|
+
console.log(chalk.dim(` Silent mode: log contains ONLY final JSON`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const task = spawnTask(prompt, {
|
|
26
|
+
cwd: options.cwd || process.cwd(),
|
|
27
|
+
model: options.model,
|
|
28
|
+
resume: options.resume,
|
|
29
|
+
continue: options.continue,
|
|
30
|
+
outputFormat,
|
|
31
|
+
jsonSchema,
|
|
32
|
+
silentJsonOutput,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
console.log(chalk.green(`\nā Task spawned: ${chalk.cyan(task.id)}`));
|
|
36
|
+
console.log(chalk.dim(` Log: ${task.logFile}`));
|
|
37
|
+
console.log(chalk.dim(` CWD: ${task.cwd}`));
|
|
38
|
+
|
|
39
|
+
console.log(chalk.dim('\nCommands:'));
|
|
40
|
+
console.log(chalk.dim(` zeroshot attach ${task.id} # Attach to task (Ctrl+B d to detach)`));
|
|
41
|
+
console.log(chalk.dim(` zeroshot logs ${task.id} # View output`));
|
|
42
|
+
console.log(chalk.dim(` zeroshot logs -f ${task.id} # Follow output`));
|
|
43
|
+
console.log(chalk.dim(` zeroshot status ${task.id} # Check status`));
|
|
44
|
+
console.log(chalk.dim(` zeroshot kill ${task.id} # Stop task`));
|
|
45
|
+
console.log();
|
|
46
|
+
|
|
47
|
+
return task;
|
|
48
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { addSchedule, generateScheduleId, ensureDirs } from '../store.js';
|
|
3
|
+
import { parseInterval, calculateNextRun, getDaemonStatus } from '../scheduler.js';
|
|
4
|
+
import { fork } from 'child_process';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
export function createSchedule(prompt, options = {}) {
|
|
11
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
12
|
+
console.log(chalk.red('Error: Prompt is required'));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!options.every && !options.cron) {
|
|
17
|
+
console.log(chalk.red('Error: Either --every or --cron is required'));
|
|
18
|
+
console.log(chalk.dim(' Examples:'));
|
|
19
|
+
console.log(chalk.dim(' zeroshot schedule "backup" --every 1d'));
|
|
20
|
+
console.log(chalk.dim(' zeroshot schedule "cleanup" --cron "0 2 * * *"'));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ensureDirs();
|
|
25
|
+
|
|
26
|
+
let interval = null;
|
|
27
|
+
let cron = null;
|
|
28
|
+
|
|
29
|
+
if (options.every) {
|
|
30
|
+
interval = parseInterval(options.every);
|
|
31
|
+
if (!interval) {
|
|
32
|
+
console.log(chalk.red(`Error: Invalid interval format "${options.every}"`));
|
|
33
|
+
console.log(chalk.dim(' Valid formats: 30s, 5m, 2h, 1d, 1w'));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (options.cron) {
|
|
39
|
+
cron = options.cron;
|
|
40
|
+
// Basic validation
|
|
41
|
+
if (cron.trim().split(/\s+/).length !== 5) {
|
|
42
|
+
console.log(chalk.red(`Error: Invalid cron format "${options.cron}"`));
|
|
43
|
+
console.log(chalk.dim(' Format: minute hour day month weekday'));
|
|
44
|
+
console.log(chalk.dim(' Example: "0 2 * * *" (daily at 2am)'));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const schedule = {
|
|
50
|
+
id: generateScheduleId(),
|
|
51
|
+
prompt: prompt.slice(0, 200) + (prompt.length > 200 ? '...' : ''),
|
|
52
|
+
fullPrompt: prompt,
|
|
53
|
+
cwd: options.cwd || process.cwd(),
|
|
54
|
+
interval,
|
|
55
|
+
cron,
|
|
56
|
+
nextRunAt: null,
|
|
57
|
+
lastRunAt: null,
|
|
58
|
+
lastTaskId: null,
|
|
59
|
+
enabled: true,
|
|
60
|
+
createdAt: new Date().toISOString(),
|
|
61
|
+
updatedAt: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Calculate first run time
|
|
65
|
+
const nextRun = calculateNextRun(schedule);
|
|
66
|
+
schedule.nextRunAt = nextRun ? nextRun.toISOString() : null;
|
|
67
|
+
|
|
68
|
+
addSchedule(schedule);
|
|
69
|
+
|
|
70
|
+
console.log(chalk.green(`\nā Schedule created: ${chalk.cyan(schedule.id)}`));
|
|
71
|
+
console.log(chalk.dim(` Prompt: ${schedule.prompt}`));
|
|
72
|
+
console.log(chalk.dim(` CWD: ${schedule.cwd}`));
|
|
73
|
+
|
|
74
|
+
if (interval) {
|
|
75
|
+
const humanInterval = options.every;
|
|
76
|
+
console.log(chalk.dim(` Interval: every ${humanInterval}`));
|
|
77
|
+
}
|
|
78
|
+
if (cron) {
|
|
79
|
+
console.log(chalk.dim(` Cron: ${cron}`));
|
|
80
|
+
}
|
|
81
|
+
console.log(chalk.dim(` Next run: ${nextRun ? nextRun.toISOString() : 'N/A'}`));
|
|
82
|
+
|
|
83
|
+
// Check if scheduler is running, start if not
|
|
84
|
+
const status = getDaemonStatus();
|
|
85
|
+
if (!status.running) {
|
|
86
|
+
console.log(chalk.yellow('\nā Scheduler daemon is not running'));
|
|
87
|
+
console.log(chalk.dim(' Starting scheduler daemon...'));
|
|
88
|
+
|
|
89
|
+
// Fork scheduler as detached process
|
|
90
|
+
const scheduler = fork(join(__dirname, '..', 'scheduler.js'), [], {
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: 'ignore',
|
|
93
|
+
});
|
|
94
|
+
scheduler.unref();
|
|
95
|
+
|
|
96
|
+
console.log(chalk.green(' ā Scheduler daemon started'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(chalk.dim('\nCommands:'));
|
|
100
|
+
console.log(chalk.dim(` zeroshot schedules # List all schedules`));
|
|
101
|
+
console.log(chalk.dim(` zeroshot unschedule ${schedule.id} # Remove this schedule`));
|
|
102
|
+
console.log();
|
|
103
|
+
|
|
104
|
+
return schedule;
|
|
105
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { fork } from 'child_process';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { getDaemonStatus, stopDaemon } from '../scheduler.js';
|
|
6
|
+
import { SCHEDULER_LOG } from '../config.js';
|
|
7
|
+
import { existsSync, readFileSync } from 'fs';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
export function schedulerCommand(action) {
|
|
12
|
+
switch (action) {
|
|
13
|
+
case 'start':
|
|
14
|
+
startScheduler();
|
|
15
|
+
break;
|
|
16
|
+
case 'stop':
|
|
17
|
+
stopScheduler();
|
|
18
|
+
break;
|
|
19
|
+
case 'status':
|
|
20
|
+
showStatus();
|
|
21
|
+
break;
|
|
22
|
+
case 'logs':
|
|
23
|
+
showLogs();
|
|
24
|
+
break;
|
|
25
|
+
default:
|
|
26
|
+
console.log(chalk.red(`Unknown action: ${action}`));
|
|
27
|
+
console.log(chalk.dim('Available actions: start, stop, status, logs'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function startScheduler() {
|
|
33
|
+
const status = getDaemonStatus();
|
|
34
|
+
|
|
35
|
+
if (status.running) {
|
|
36
|
+
console.log(chalk.yellow(`Scheduler already running (PID: ${status.pid})`));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (status.stale) {
|
|
41
|
+
console.log(chalk.dim('Cleaning up stale PID file...'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(chalk.dim('Starting scheduler daemon...'));
|
|
45
|
+
|
|
46
|
+
const scheduler = fork(join(__dirname, '..', 'scheduler.js'), [], {
|
|
47
|
+
detached: true,
|
|
48
|
+
stdio: 'ignore',
|
|
49
|
+
});
|
|
50
|
+
scheduler.unref();
|
|
51
|
+
|
|
52
|
+
// Give it a moment to start
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
const newStatus = getDaemonStatus();
|
|
55
|
+
if (newStatus.running) {
|
|
56
|
+
console.log(chalk.green(`ā Scheduler started (PID: ${newStatus.pid})`));
|
|
57
|
+
} else {
|
|
58
|
+
console.log(chalk.red('Failed to start scheduler. Check logs:'));
|
|
59
|
+
console.log(chalk.dim(` zeroshot scheduler logs`));
|
|
60
|
+
}
|
|
61
|
+
}, 500);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function stopScheduler() {
|
|
65
|
+
const stopped = stopDaemon();
|
|
66
|
+
if (!stopped) {
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function showStatus() {
|
|
72
|
+
const status = getDaemonStatus();
|
|
73
|
+
|
|
74
|
+
if (status.running) {
|
|
75
|
+
console.log(chalk.green(`Scheduler: running`));
|
|
76
|
+
console.log(chalk.dim(` PID: ${status.pid}`));
|
|
77
|
+
console.log(chalk.dim(` Log: ${SCHEDULER_LOG}`));
|
|
78
|
+
} else if (status.stale) {
|
|
79
|
+
console.log(chalk.yellow('Scheduler: not running (stale PID file)'));
|
|
80
|
+
console.log(chalk.dim(' Run: zeroshot scheduler start'));
|
|
81
|
+
} else {
|
|
82
|
+
console.log(chalk.red('Scheduler: not running'));
|
|
83
|
+
console.log(chalk.dim(' Run: zeroshot scheduler start'));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function showLogs() {
|
|
88
|
+
if (!existsSync(SCHEDULER_LOG)) {
|
|
89
|
+
console.log(chalk.dim('No scheduler logs found.'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const content = readFileSync(SCHEDULER_LOG, 'utf-8');
|
|
94
|
+
const lines = content.split('\n').slice(-50); // Last 50 lines
|
|
95
|
+
console.log(lines.join('\n'));
|
|
96
|
+
}
|