@covibes/zeroshot 1.0.1 → 1.1.3

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +2 -0
  3. package/cli/index.js +151 -208
  4. package/cli/message-formatter-utils.js +75 -0
  5. package/cli/message-formatters-normal.js +214 -0
  6. package/cli/message-formatters-watch.js +181 -0
  7. package/cluster-templates/base-templates/full-workflow.json +10 -5
  8. package/docker/zeroshot-cluster/Dockerfile +6 -0
  9. package/package.json +5 -2
  10. package/src/agent/agent-task-executor.js +237 -112
  11. package/src/isolation-manager.js +94 -51
  12. package/src/orchestrator.js +45 -10
  13. package/src/preflight.js +383 -0
  14. package/src/process-metrics.js +546 -0
  15. package/src/status-footer.js +543 -0
  16. package/task-lib/attachable-watcher.js +202 -0
  17. package/task-lib/commands/clean.js +50 -0
  18. package/task-lib/commands/get-log-path.js +23 -0
  19. package/task-lib/commands/kill.js +32 -0
  20. package/task-lib/commands/list.js +105 -0
  21. package/task-lib/commands/logs.js +411 -0
  22. package/task-lib/commands/resume.js +41 -0
  23. package/task-lib/commands/run.js +48 -0
  24. package/task-lib/commands/schedule.js +105 -0
  25. package/task-lib/commands/scheduler-cmd.js +96 -0
  26. package/task-lib/commands/schedules.js +98 -0
  27. package/task-lib/commands/status.js +44 -0
  28. package/task-lib/commands/unschedule.js +16 -0
  29. package/task-lib/completion.js +9 -0
  30. package/task-lib/config.js +10 -0
  31. package/task-lib/name-generator.js +230 -0
  32. package/task-lib/package.json +3 -0
  33. package/task-lib/runner.js +123 -0
  34. package/task-lib/scheduler.js +252 -0
  35. package/task-lib/store.js +217 -0
  36. package/task-lib/tui/formatters.js +166 -0
  37. package/task-lib/tui/index.js +197 -0
  38. package/task-lib/tui/layout.js +111 -0
  39. package/task-lib/tui/renderer.js +119 -0
  40. package/task-lib/tui.js +384 -0
  41. package/task-lib/watcher.js +162 -0
  42. package/cluster-templates/conductor-junior-bootstrap.json +0 -69
@@ -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
+ }