@covibes/zeroshot 5.3.0 → 5.4.0

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 (82) hide show
  1. package/README.md +94 -14
  2. package/cli/commands/providers.js +8 -9
  3. package/cli/index.js +3032 -2409
  4. package/cli/message-formatters-normal.js +28 -6
  5. package/cluster-templates/base-templates/debug-workflow.json +1 -1
  6. package/cluster-templates/base-templates/full-workflow.json +72 -188
  7. package/cluster-templates/base-templates/worker-validator.json +59 -3
  8. package/cluster-templates/conductor-bootstrap.json +4 -4
  9. package/lib/docker-config.js +8 -0
  10. package/lib/git-remote-utils.js +165 -0
  11. package/lib/id-detector.js +10 -7
  12. package/lib/provider-defaults.js +62 -0
  13. package/lib/provider-names.js +2 -1
  14. package/lib/settings/claude-auth.js +78 -0
  15. package/lib/settings.js +161 -63
  16. package/package.json +7 -2
  17. package/scripts/setup-merge-queue.sh +170 -0
  18. package/src/agent/agent-config.js +135 -82
  19. package/src/agent/agent-context-builder.js +297 -188
  20. package/src/agent/agent-hook-executor.js +310 -113
  21. package/src/agent/agent-lifecycle.js +385 -325
  22. package/src/agent/agent-stuck-detector.js +7 -7
  23. package/src/agent/agent-task-executor.js +824 -565
  24. package/src/agent/output-extraction.js +41 -24
  25. package/src/agent/output-reformatter.js +1 -1
  26. package/src/agent/schema-utils.js +108 -73
  27. package/src/agent-wrapper.js +10 -1
  28. package/src/agents/git-pusher-template.js +285 -0
  29. package/src/claude-task-runner.js +85 -34
  30. package/src/config-validator.js +922 -657
  31. package/src/input-helpers.js +65 -0
  32. package/src/isolation-manager.js +289 -199
  33. package/src/issue-providers/README.md +305 -0
  34. package/src/issue-providers/azure-devops-provider.js +273 -0
  35. package/src/issue-providers/base-provider.js +232 -0
  36. package/src/issue-providers/github-provider.js +179 -0
  37. package/src/issue-providers/gitlab-provider.js +241 -0
  38. package/src/issue-providers/index.js +196 -0
  39. package/src/issue-providers/jira-provider.js +239 -0
  40. package/src/ledger.js +22 -5
  41. package/src/lib/safe-exec.js +88 -0
  42. package/src/orchestrator.js +1107 -811
  43. package/src/preflight.js +313 -159
  44. package/src/process-metrics.js +98 -56
  45. package/src/providers/anthropic/cli-builder.js +53 -25
  46. package/src/providers/anthropic/index.js +72 -2
  47. package/src/providers/anthropic/output-parser.js +32 -14
  48. package/src/providers/base-provider.js +70 -0
  49. package/src/providers/capabilities.js +9 -0
  50. package/src/providers/google/output-parser.js +47 -38
  51. package/src/providers/index.js +19 -3
  52. package/src/providers/openai/cli-builder.js +14 -3
  53. package/src/providers/openai/index.js +1 -0
  54. package/src/providers/openai/output-parser.js +44 -30
  55. package/src/providers/opencode/cli-builder.js +42 -0
  56. package/src/providers/opencode/index.js +103 -0
  57. package/src/providers/opencode/models.js +55 -0
  58. package/src/providers/opencode/output-parser.js +122 -0
  59. package/src/schemas/sub-cluster.js +68 -39
  60. package/src/status-footer.js +94 -48
  61. package/src/sub-cluster-wrapper.js +76 -35
  62. package/src/template-resolver.js +12 -9
  63. package/src/tui/data-poller.js +123 -99
  64. package/src/tui/formatters.js +4 -3
  65. package/src/tui/keybindings.js +259 -318
  66. package/src/tui/renderer.js +11 -21
  67. package/task-lib/attachable-watcher.js +118 -81
  68. package/task-lib/claude-recovery.js +61 -24
  69. package/task-lib/commands/episodes.js +105 -0
  70. package/task-lib/commands/list.js +2 -2
  71. package/task-lib/commands/logs.js +231 -189
  72. package/task-lib/commands/schedules.js +111 -61
  73. package/task-lib/config.js +0 -2
  74. package/task-lib/runner.js +84 -27
  75. package/task-lib/scheduler.js +1 -1
  76. package/task-lib/store.js +467 -168
  77. package/task-lib/tui/formatters.js +94 -45
  78. package/task-lib/tui/renderer.js +13 -3
  79. package/task-lib/tui.js +73 -32
  80. package/task-lib/watcher.js +128 -90
  81. package/src/agents/git-pusher-agent.json +0 -20
  82. package/src/github.js +0 -139
@@ -30,58 +30,70 @@ function getToolIcon(toolName) {
30
30
  }
31
31
 
32
32
  // Format tool call input for display
33
+ const TOOL_CALL_FORMATTERS = {
34
+ Bash: (input) => (input.command ? `$ ${input.command}` : ''),
35
+ Read: (input) => formatFilePathTail(input.file_path),
36
+ Write: (input) => formatFilePathTail(input.file_path, '→ '),
37
+ Edit: (input) => formatFilePathTail(input.file_path),
38
+ Glob: (input) => input.pattern || '',
39
+ Grep: (input) => (input.pattern ? `/${input.pattern}/` : ''),
40
+ WebFetch: (input) => (input.url ? input.url.substring(0, 50) : ''),
41
+ WebSearch: (input) => (input.query ? `"${input.query}"` : ''),
42
+ Task: (input) => input.description || '',
43
+ TodoWrite: (input) => formatTodoSummary(input),
44
+ AskUserQuestion: (input) => formatQuestionSummary(input),
45
+ };
46
+
33
47
  function formatToolCall(toolName, input) {
34
48
  if (!input) return '';
35
49
 
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 '';
50
+ const formatter = TOOL_CALL_FORMATTERS[toolName] || formatUnknownToolCall;
51
+ return formatter(input);
52
+ }
53
+
54
+ function formatFilePathTail(filePath, prefix = '') {
55
+ if (!filePath) return '';
56
+ return `${prefix}${filePath.split('/').slice(-2).join('/')}`;
57
+ }
58
+
59
+ function formatTodoSummary(input) {
60
+ const todos = input.todos;
61
+ if (!Array.isArray(todos)) return '';
62
+
63
+ const statusCounts = {};
64
+ for (const todo of todos) {
65
+ statusCounts[todo.status] = (statusCounts[todo.status] || 0) + 1;
66
+ }
67
+
68
+ const parts = Object.entries(statusCounts).map(
69
+ ([status, count]) => `${count} ${status.replace('_', ' ')}`
70
+ );
71
+
72
+ return `${todos.length} todo${todos.length === 1 ? '' : 's'} (${parts.join(', ')})`;
73
+ }
74
+
75
+ function formatQuestionSummary(input) {
76
+ const questions = input.questions;
77
+ if (!Array.isArray(questions) || questions.length === 0) {
78
+ return '';
79
+ }
80
+
81
+ const preview = questions[0].question.substring(0, 50);
82
+ const suffix = questions[0].question.length > 50 ? '...' : '';
83
+ return questions.length > 1
84
+ ? `${questions.length} questions: "${preview}..."`
85
+ : `"${preview}${suffix}"`;
86
+ }
87
+
88
+ function formatUnknownToolCall(input) {
89
+ const keys = Object.keys(input);
90
+ if (keys.length === 0) {
91
+ return '';
84
92
  }
93
+
94
+ const value = String(input[keys[0]]);
95
+ const preview = value.substring(0, 40);
96
+ return preview.length < value.length ? `${preview}...` : preview;
85
97
  }
86
98
 
87
99
  // Format tool result for display
@@ -98,16 +110,21 @@ function formatToolResult(content, isError, toolName, toolInput) {
98
110
  if (toolName === 'TodoWrite' && toolInput?.todos && Array.isArray(toolInput.todos)) {
99
111
  const todos = toolInput.todos;
100
112
  if (todos.length === 0) return chalk.dim('no todos');
113
+
114
+ // Helper to get status icon
115
+ const getStatusIcon = (todoStatus) => {
116
+ if (todoStatus === 'completed') return '✓';
117
+ if (todoStatus === 'in_progress') return '⧗';
118
+ return '○';
119
+ };
120
+
101
121
  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
- );
122
+ const status = getStatusIcon(todos[0].status);
123
+ const suffix = todos[0].content.length > 50 ? '...' : '';
124
+ return chalk.dim(`${status} ${todos[0].content.substring(0, 50)}${suffix}`);
107
125
  }
108
126
  // Multiple todos - show first one as preview
109
- const status =
110
- todos[0].status === 'completed' ? '✓' : todos[0].status === 'in_progress' ? '⧗' : '○';
127
+ const status = getStatusIcon(todos[0].status);
111
128
  return chalk.dim(
112
129
  `${status} ${todos[0].content.substring(0, 40)}... (+${todos.length - 1} more)`
113
130
  );
@@ -161,77 +178,95 @@ function accumulateText(text) {
161
178
  }
162
179
 
163
180
  // Process parsed events and output formatted content
181
+ const EVENT_HANDLERS = {
182
+ text: handleTextEvent,
183
+ thinking: handleThinkingEvent,
184
+ thinking_start: handleThinkingEvent,
185
+ tool_start: handleToolStart,
186
+ tool_call: handleToolCall,
187
+ tool_input: handleToolInput,
188
+ tool_result: handleToolResult,
189
+ result: handleResult,
190
+ block_end: handleBlockEnd,
191
+ multi: handleMulti,
192
+ };
193
+
164
194
  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;
195
+ const handler = EVENT_HANDLERS[event.type];
196
+ if (handler) {
197
+ handler(event);
198
+ }
199
+ }
200
+
201
+ function handleTextEvent(event) {
202
+ accumulateText(event.text);
203
+ }
204
+
205
+ function handleThinkingEvent(event) {
206
+ if (event.text) {
207
+ console.log(chalk.dim.italic(event.text));
208
+ return;
209
+ }
210
+
211
+ if (event.type === 'thinking_start') {
212
+ console.log(chalk.dim.italic('💭 thinking...'));
213
+ }
214
+ }
215
+
216
+ function handleToolStart() {
217
+ flushLineBuffer();
218
+ }
219
+
220
+ function handleToolCall(event) {
221
+ flushLineBuffer();
222
+ const icon = getToolIcon(event.toolName);
223
+ const toolDesc = formatToolCall(event.toolName, event.input);
224
+ console.log(`${icon} ${chalk.cyan(event.toolName)} ${chalk.dim(toolDesc)}`);
225
+ currentToolCall = { toolName: event.toolName, input: event.input };
226
+ }
227
+
228
+ function handleToolInput() {
229
+ // Streaming tool input JSON - skip (shown in tool_call)
230
+ }
231
+
232
+ function handleToolResult(event) {
233
+ const status = event.isError ? chalk.red('✗') : chalk.green('✓');
234
+ const resultDesc = formatToolResult(
235
+ event.content,
236
+ event.isError,
237
+ currentToolCall?.toolName,
238
+ currentToolCall?.input
239
+ );
240
+ console.log(` ${status} ${resultDesc}`);
241
+ }
242
+
243
+ function handleResult(event) {
244
+ flushLineBuffer();
245
+ if (event.error) {
246
+ console.log(chalk.red(`\n✗ ERROR: ${event.error}`));
247
+ return;
248
+ }
249
+
250
+ console.log(chalk.green(`\n✓ Completed`));
251
+ if (event.cost) {
252
+ console.log(chalk.dim(` Cost: $${event.cost.toFixed(4)}`));
253
+ }
254
+ if (event.duration) {
255
+ const mins = Math.floor(event.duration / 60000);
256
+ const secs = Math.floor((event.duration % 60000) / 1000);
257
+ console.log(chalk.dim(` Duration: ${mins}m ${secs}s`));
258
+ }
259
+ }
260
+
261
+ function handleBlockEnd() {
262
+ // Content block ended
263
+ }
264
+
265
+ function handleMulti(event) {
266
+ if (event.events) {
267
+ for (const subEvent of event.events) {
268
+ processEvent(subEvent);
269
+ }
235
270
  }
236
271
  }
237
272
 
@@ -296,23 +331,73 @@ export async function showLogs(taskId, options = {}) {
296
331
  }
297
332
  }
298
333
 
334
+ function parseEventsFromLines(lines) {
335
+ const events = [];
336
+ for (const line of lines) {
337
+ events.push(...parseLogLine(line));
338
+ }
339
+ return events;
340
+ }
341
+
342
+ function renderEvents(events) {
343
+ for (const event of events) {
344
+ processEvent(event);
345
+ }
346
+ }
347
+
348
+ function renderLines(lines) {
349
+ for (const line of lines) {
350
+ renderEvents(parseLogLine(line));
351
+ }
352
+ }
353
+
354
+ function readFileSlice(file, start, end) {
355
+ const length = Math.max(0, end - start);
356
+ if (!length) return '';
357
+
358
+ const buffer = Buffer.alloc(length);
359
+ const fd = openSync(file, 'r');
360
+ readSync(fd, buffer, 0, buffer.length, start);
361
+ closeSync(fd);
362
+
363
+ return buffer.toString();
364
+ }
365
+
366
+ function shouldStopFollowing(noChangeCount, pid) {
367
+ return noChangeCount >= 10 && pid && !isProcessRunning(pid);
368
+ }
369
+
370
+ function handleFinalContent(file, lastSize, interval) {
371
+ const finalSize = statSync(file).size;
372
+ if (finalSize > lastSize) {
373
+ const finalContent = readFileSlice(file, lastSize, finalSize);
374
+ renderLines(finalContent.split('\n'));
375
+ flushLineBuffer();
376
+ }
377
+
378
+ console.log(chalk.dim('\n--- Task completed ---'));
379
+ clearInterval(interval);
380
+ process.exit(0);
381
+ }
382
+
383
+ function handleNewContent(file, lastSize, currentSize) {
384
+ const newContent = readFileSlice(file, lastSize, currentSize);
385
+ if (newContent) {
386
+ renderLines(newContent.split('\n'));
387
+ flushLineBuffer();
388
+ }
389
+ return currentSize;
390
+ }
391
+
299
392
  function tailLines(file, n) {
300
393
  resetState();
301
394
  const rawContent = readFileSync(file, 'utf-8');
302
395
  const rawLines = rawContent.split('\n');
303
396
 
304
397
  // 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
-
398
+ const allEvents = parseEventsFromLines(rawLines);
311
399
  // Tail to last n events
312
- const tailedEvents = allEvents.slice(-n);
313
- for (const event of tailedEvents) {
314
- processEvent(event);
315
- }
400
+ renderEvents(allEvents.slice(-n));
316
401
  flushLineBuffer();
317
402
  }
318
403
 
@@ -323,17 +408,9 @@ async function tailFollow(file, pid, lines = 50) {
323
408
  const rawLines = rawContent.split('\n');
324
409
 
325
410
  // Parse all events first
326
- const allEvents = [];
327
- for (const line of rawLines) {
328
- const events = parseLogLine(line);
329
- allEvents.push(...events);
330
- }
331
-
411
+ const allEvents = parseEventsFromLines(rawLines);
332
412
  // Output only the last N events
333
- const tailedEvents = allEvents.slice(-lines);
334
- for (const event of tailedEvents) {
335
- processEvent(event);
336
- }
413
+ renderEvents(allEvents.slice(-lines));
337
414
  flushLineBuffer();
338
415
 
339
416
  // Poll for changes (more reliable than fs.watch)
@@ -345,51 +422,16 @@ async function tailFollow(file, pid, lines = 50) {
345
422
  const currentSize = statSync(file).size;
346
423
 
347
424
  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;
425
+ lastSize = handleNewContent(file, lastSize, currentSize);
365
426
  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
- }
427
+ return;
428
+ }
429
+
430
+ noChangeCount++;
431
+
432
+ // Check if process is still running after 5 seconds of no output
433
+ if (shouldStopFollowing(noChangeCount, pid)) {
434
+ handleFinalContent(file, lastSize, interval);
393
435
  }
394
436
  } catch (err) {
395
437
  // File might have been deleted
@@ -10,14 +10,32 @@ export function listSchedules(_options = {}) {
10
10
  const daemonStatus = getDaemonStatus();
11
11
 
12
12
  if (scheduleList.length === 0) {
13
- console.log(chalk.dim('No schedules found.'));
14
- console.log(chalk.dim('\nCreate a schedule:'));
15
- console.log(chalk.dim(' zeroshot schedule "your prompt" --every 1h'));
16
- console.log(chalk.dim(' zeroshot schedule "your prompt" --cron "0 * * * *"'));
13
+ printNoSchedules();
17
14
  return;
18
15
  }
19
16
 
20
- // Show daemon status
17
+ printDaemonStatus(daemonStatus);
18
+
19
+ // Sort by next run time
20
+ sortSchedulesByNextRun(scheduleList);
21
+
22
+ printScheduleHeader();
23
+
24
+ for (const schedule of scheduleList) {
25
+ printSchedule(schedule);
26
+ }
27
+
28
+ console.log(chalk.dim(`Total: ${scheduleList.length} schedule(s)`));
29
+ }
30
+
31
+ function printNoSchedules() {
32
+ console.log(chalk.dim('No schedules found.'));
33
+ console.log(chalk.dim('\nCreate a schedule:'));
34
+ console.log(chalk.dim(' zeroshot schedule "your prompt" --every 1h'));
35
+ console.log(chalk.dim(' zeroshot schedule "your prompt" --cron "0 * * * *"'));
36
+ }
37
+
38
+ function printDaemonStatus(daemonStatus) {
21
39
  if (daemonStatus.running) {
22
40
  console.log(chalk.green(`Scheduler: running (PID: ${daemonStatus.pid})`));
23
41
  } else if (daemonStatus.stale) {
@@ -27,72 +45,104 @@ export function listSchedules(_options = {}) {
27
45
  console.log(chalk.dim(' Run: zeroshot scheduler start'));
28
46
  }
29
47
  console.log();
48
+ }
30
49
 
31
- // Sort by next run time
50
+ function sortSchedulesByNextRun(scheduleList) {
32
51
  scheduleList.sort((a, b) => {
33
52
  if (!a.nextRunAt) return 1;
34
53
  if (!b.nextRunAt) return -1;
35
54
  return new Date(a.nextRunAt) - new Date(b.nextRunAt);
36
55
  });
56
+ }
37
57
 
58
+ function printScheduleHeader() {
38
59
  console.log(chalk.bold('Scheduled Tasks:'));
39
60
  console.log();
61
+ }
40
62
 
41
- for (const schedule of scheduleList) {
42
- const statusColor = schedule.enabled ? chalk.green : chalk.red;
43
- const statusText = schedule.enabled ? '●' : '○';
44
-
45
- console.log(`${statusColor(statusText)} ${chalk.cyan(schedule.id)}`);
46
- console.log(chalk.dim(` Prompt: ${schedule.prompt}`));
47
-
48
- if (schedule.interval) {
49
- const ms = schedule.interval;
50
- let human;
51
- if (ms >= 7 * 24 * 60 * 60 * 1000) human = `${ms / (7 * 24 * 60 * 60 * 1000)}w`;
52
- else if (ms >= 24 * 60 * 60 * 1000) human = `${ms / (24 * 60 * 60 * 1000)}d`;
53
- else if (ms >= 60 * 60 * 1000) human = `${ms / (60 * 60 * 1000)}h`;
54
- else if (ms >= 60 * 1000) human = `${ms / (60 * 1000)}m`;
55
- else human = `${ms / 1000}s`;
56
- console.log(chalk.dim(` Interval: every ${human}`));
57
- }
58
- if (schedule.cron) {
59
- console.log(chalk.dim(` Cron: ${schedule.cron}`));
60
- }
61
-
62
- if (schedule.nextRunAt) {
63
- const nextRun = new Date(schedule.nextRunAt);
64
- const now = new Date();
65
- const diff = nextRun - now;
66
-
67
- let timeUntil;
68
- if (diff < 0) {
69
- timeUntil = 'overdue';
70
- } else if (diff < 60000) {
71
- timeUntil = 'in < 1m';
72
- } else if (diff < 3600000) {
73
- timeUntil = `in ${Math.round(diff / 60000)}m`;
74
- } else if (diff < 86400000) {
75
- timeUntil = `in ${Math.round(diff / 3600000)}h`;
76
- } else {
77
- timeUntil = `in ${Math.round(diff / 86400000)}d`;
78
- }
79
-
80
- console.log(chalk.dim(` Next run: ${nextRun.toISOString()} (${timeUntil})`));
81
- }
82
-
83
- if (schedule.lastRunAt) {
84
- console.log(chalk.dim(` Last run: ${schedule.lastRunAt}`));
85
- if (schedule.lastTaskId) {
86
- console.log(chalk.dim(` Last task: ${schedule.lastTaskId}`));
87
- }
88
- }
89
-
90
- if (schedule.verify) {
91
- console.log(chalk.dim(` Verify: enabled`));
92
- }
93
-
94
- console.log();
63
+ function printSchedule(schedule) {
64
+ printScheduleHeaderLine(schedule);
65
+ printSchedulePrompt(schedule);
66
+ printScheduleInterval(schedule);
67
+ printScheduleCron(schedule);
68
+ printScheduleNextRun(schedule);
69
+ printScheduleLastRun(schedule);
70
+ printScheduleVerify(schedule);
71
+ console.log();
72
+ }
73
+
74
+ function printScheduleHeaderLine(schedule) {
75
+ const statusColor = schedule.enabled ? chalk.green : chalk.red;
76
+ const statusText = schedule.enabled ? '●' : '○';
77
+ console.log(`${statusColor(statusText)} ${chalk.cyan(schedule.id)}`);
78
+ }
79
+
80
+ function printSchedulePrompt(schedule) {
81
+ console.log(chalk.dim(` Prompt: ${schedule.prompt}`));
82
+ }
83
+
84
+ function printScheduleInterval(schedule) {
85
+ if (!schedule.interval) {
86
+ return;
95
87
  }
88
+ const human = formatInterval(schedule.interval);
89
+ console.log(chalk.dim(` Interval: every ${human}`));
90
+ }
96
91
 
97
- console.log(chalk.dim(`Total: ${scheduleList.length} schedule(s)`));
92
+ function printScheduleCron(schedule) {
93
+ if (schedule.cron) {
94
+ console.log(chalk.dim(` Cron: ${schedule.cron}`));
95
+ }
96
+ }
97
+
98
+ function printScheduleNextRun(schedule) {
99
+ if (!schedule.nextRunAt) {
100
+ return;
101
+ }
102
+ const nextRun = new Date(schedule.nextRunAt);
103
+ const timeUntil = formatTimeUntil(nextRun);
104
+ console.log(chalk.dim(` Next run: ${nextRun.toISOString()} (${timeUntil})`));
105
+ }
106
+
107
+ function printScheduleLastRun(schedule) {
108
+ if (!schedule.lastRunAt) {
109
+ return;
110
+ }
111
+ console.log(chalk.dim(` Last run: ${schedule.lastRunAt}`));
112
+ if (schedule.lastTaskId) {
113
+ console.log(chalk.dim(` Last task: ${schedule.lastTaskId}`));
114
+ }
115
+ }
116
+
117
+ function printScheduleVerify(schedule) {
118
+ if (schedule.verify) {
119
+ console.log(chalk.dim(` Verify: enabled`));
120
+ }
121
+ }
122
+
123
+ function formatInterval(ms) {
124
+ if (ms >= 7 * 24 * 60 * 60 * 1000) return `${ms / (7 * 24 * 60 * 60 * 1000)}w`;
125
+ if (ms >= 24 * 60 * 60 * 1000) return `${ms / (24 * 60 * 60 * 1000)}d`;
126
+ if (ms >= 60 * 60 * 1000) return `${ms / (60 * 60 * 1000)}h`;
127
+ if (ms >= 60 * 1000) return `${ms / (60 * 1000)}m`;
128
+ return `${ms / 1000}s`;
129
+ }
130
+
131
+ function formatTimeUntil(nextRun) {
132
+ const now = new Date();
133
+ const diff = nextRun - now;
134
+
135
+ if (diff < 0) {
136
+ return 'overdue';
137
+ }
138
+ if (diff < 60000) {
139
+ return 'in < 1m';
140
+ }
141
+ if (diff < 3600000) {
142
+ return `in ${Math.round(diff / 60000)}m`;
143
+ }
144
+ if (diff < 86400000) {
145
+ return `in ${Math.round(diff / 3600000)}h`;
146
+ }
147
+ return `in ${Math.round(diff / 86400000)}d`;
98
148
  }