@covibes/zeroshot 5.3.0 → 5.4.1

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 (119) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -385
  3. package/cli/index.js +108 -4615
  4. package/package.json +16 -125
  5. package/CHANGELOG.md +0 -537
  6. package/cli/commands/providers.js +0 -150
  7. package/cli/lib/first-run.js +0 -211
  8. package/cli/lib/update-checker.js +0 -281
  9. package/cli/message-formatter-utils.js +0 -75
  10. package/cli/message-formatters-normal.js +0 -253
  11. package/cli/message-formatters-watch.js +0 -181
  12. package/cluster-templates/base-templates/debug-workflow.json +0 -375
  13. package/cluster-templates/base-templates/full-workflow.json +0 -697
  14. package/cluster-templates/base-templates/single-worker.json +0 -63
  15. package/cluster-templates/base-templates/worker-validator.json +0 -152
  16. package/cluster-templates/conductor-bootstrap.json +0 -124
  17. package/docker/zeroshot-cluster/Dockerfile +0 -152
  18. package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
  19. package/lib/completion.js +0 -174
  20. package/lib/docker-config.js +0 -212
  21. package/lib/id-detector.js +0 -53
  22. package/lib/provider-detection.js +0 -59
  23. package/lib/provider-names.js +0 -56
  24. package/lib/settings.js +0 -405
  25. package/lib/stream-json-parser.js +0 -10
  26. package/scripts/fix-node-pty-permissions.js +0 -75
  27. package/scripts/record-demo.sh +0 -279
  28. package/scripts/test-install.sh +0 -40
  29. package/scripts/validate-templates.js +0 -100
  30. package/src/agent/agent-config.js +0 -193
  31. package/src/agent/agent-context-builder.js +0 -335
  32. package/src/agent/agent-hook-executor.js +0 -402
  33. package/src/agent/agent-lifecycle.js +0 -683
  34. package/src/agent/agent-stuck-detector.js +0 -256
  35. package/src/agent/agent-task-executor.js +0 -1431
  36. package/src/agent/agent-trigger-evaluator.js +0 -67
  37. package/src/agent/output-extraction.js +0 -200
  38. package/src/agent/output-reformatter.js +0 -175
  39. package/src/agent/schema-utils.js +0 -111
  40. package/src/agent-wrapper.js +0 -570
  41. package/src/agents/git-pusher-agent.json +0 -20
  42. package/src/attach/attach-client.js +0 -438
  43. package/src/attach/attach-server.js +0 -543
  44. package/src/attach/index.js +0 -35
  45. package/src/attach/protocol.js +0 -220
  46. package/src/attach/ring-buffer.js +0 -121
  47. package/src/attach/socket-discovery.js +0 -267
  48. package/src/claude-task-runner.js +0 -523
  49. package/src/config-router.js +0 -80
  50. package/src/config-validator.js +0 -1660
  51. package/src/copy-worker.js +0 -43
  52. package/src/github.js +0 -139
  53. package/src/isolation-manager.js +0 -1381
  54. package/src/ledger.js +0 -614
  55. package/src/logic-engine.js +0 -223
  56. package/src/message-bus-bridge.js +0 -139
  57. package/src/message-bus.js +0 -249
  58. package/src/name-generator.js +0 -232
  59. package/src/orchestrator.js +0 -2519
  60. package/src/preflight.js +0 -485
  61. package/src/process-metrics.js +0 -566
  62. package/src/providers/anthropic/cli-builder.js +0 -45
  63. package/src/providers/anthropic/index.js +0 -134
  64. package/src/providers/anthropic/models.js +0 -23
  65. package/src/providers/anthropic/output-parser.js +0 -159
  66. package/src/providers/base-provider.js +0 -181
  67. package/src/providers/capabilities.js +0 -51
  68. package/src/providers/google/cli-builder.js +0 -55
  69. package/src/providers/google/index.js +0 -116
  70. package/src/providers/google/models.js +0 -24
  71. package/src/providers/google/output-parser.js +0 -92
  72. package/src/providers/index.js +0 -75
  73. package/src/providers/openai/cli-builder.js +0 -122
  74. package/src/providers/openai/index.js +0 -135
  75. package/src/providers/openai/models.js +0 -21
  76. package/src/providers/openai/output-parser.js +0 -129
  77. package/src/schemas/sub-cluster.js +0 -156
  78. package/src/status-footer.js +0 -980
  79. package/src/sub-cluster-wrapper.js +0 -560
  80. package/src/task-runner.js +0 -30
  81. package/src/template-resolver.js +0 -369
  82. package/src/tui/CHANGES.txt +0 -133
  83. package/src/tui/LAYOUT.md +0 -261
  84. package/src/tui/README.txt +0 -192
  85. package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
  86. package/src/tui/data-poller.js +0 -325
  87. package/src/tui/demo.js +0 -208
  88. package/src/tui/formatters.js +0 -128
  89. package/src/tui/index.js +0 -193
  90. package/src/tui/keybindings.js +0 -383
  91. package/src/tui/layout.js +0 -334
  92. package/src/tui/renderer.js +0 -194
  93. package/task-lib/attachable-watcher.js +0 -272
  94. package/task-lib/claude-recovery.js +0 -119
  95. package/task-lib/commands/clean.js +0 -50
  96. package/task-lib/commands/get-log-path.js +0 -23
  97. package/task-lib/commands/kill.js +0 -32
  98. package/task-lib/commands/list.js +0 -105
  99. package/task-lib/commands/logs.js +0 -411
  100. package/task-lib/commands/resume.js +0 -42
  101. package/task-lib/commands/run.js +0 -57
  102. package/task-lib/commands/schedule.js +0 -105
  103. package/task-lib/commands/scheduler-cmd.js +0 -96
  104. package/task-lib/commands/schedules.js +0 -98
  105. package/task-lib/commands/status.js +0 -44
  106. package/task-lib/commands/unschedule.js +0 -16
  107. package/task-lib/completion.js +0 -9
  108. package/task-lib/config.js +0 -10
  109. package/task-lib/name-generator.js +0 -230
  110. package/task-lib/package.json +0 -3
  111. package/task-lib/runner.js +0 -144
  112. package/task-lib/scheduler.js +0 -252
  113. package/task-lib/store.js +0 -230
  114. package/task-lib/tui/formatters.js +0 -166
  115. package/task-lib/tui/index.js +0 -197
  116. package/task-lib/tui/layout.js +0 -111
  117. package/task-lib/tui/renderer.js +0 -119
  118. package/task-lib/tui.js +0 -384
  119. package/task-lib/watcher.js +0 -188
@@ -1,252 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Scheduler daemon - runs as background process
5
- * Checks for due scheduled tasks and spawns them
6
- */
7
-
8
- import { readFileSync, writeFileSync, existsSync, appendFileSync, unlinkSync } from 'fs';
9
- import { loadSchedules, updateSchedule } from './store.js';
10
- import { spawnTask } from './runner.js';
11
- import { SCHEDULER_PID_FILE, SCHEDULER_LOG } from './config.js';
12
-
13
- const CHECK_INTERVAL = 60000; // 60 seconds
14
-
15
- /**
16
- * Parse human-readable interval to milliseconds
17
- * Supports: 30s, 5m, 2h, 1d, 1w
18
- */
19
- export function parseInterval(str) {
20
- const match = str.match(/^(\d+)(s|m|h|d|w)$/i);
21
- if (!match) return null;
22
-
23
- const value = parseInt(match[1], 10);
24
- const unit = match[2].toLowerCase();
25
-
26
- const multipliers = {
27
- s: 1000,
28
- m: 60 * 1000,
29
- h: 60 * 60 * 1000,
30
- d: 24 * 60 * 60 * 1000,
31
- w: 7 * 24 * 60 * 60 * 1000,
32
- };
33
-
34
- return value * multipliers[unit];
35
- }
36
-
37
- /**
38
- * Parse cron expression and get next run time
39
- * Simple cron parser supporting: minute hour day month weekday
40
- */
41
- export function getNextCronTime(cronExpr, fromDate = new Date()) {
42
- const parts = cronExpr.trim().split(/\s+/);
43
- if (parts.length !== 5) return null;
44
-
45
- const [minute, hour] = parts;
46
-
47
- // Simple implementation - just handle basic cases
48
- // For full cron support, use cron-parser package
49
- const next = new Date(fromDate);
50
- next.setSeconds(0);
51
- next.setMilliseconds(0);
52
-
53
- // Handle simple cases
54
- if (minute !== '*') {
55
- const mins = minute.split(',').map((m) => parseInt(m, 10));
56
- const currentMin = next.getMinutes();
57
- const nextMin = mins.find((m) => m > currentMin) ?? mins[0];
58
- if (nextMin <= currentMin) {
59
- next.setHours(next.getHours() + 1);
60
- }
61
- next.setMinutes(nextMin);
62
- }
63
-
64
- if (hour !== '*') {
65
- const hours = hour.split(',').map((h) => parseInt(h, 10));
66
- const currentHour = next.getHours();
67
- const nextHour = hours.find((h) => h > currentHour) ?? hours[0];
68
- if (nextHour <= currentHour && minute === '*') {
69
- next.setDate(next.getDate() + 1);
70
- }
71
- if (nextHour !== currentHour) {
72
- next.setMinutes(minute === '*' ? 0 : parseInt(minute, 10));
73
- }
74
- next.setHours(nextHour);
75
- }
76
-
77
- // For complex cron expressions, fall back to 1 hour from now
78
- // Full implementation would need cron-parser
79
- if (next <= fromDate) {
80
- next.setTime(fromDate.getTime() + 60 * 60 * 1000);
81
- }
82
-
83
- return next;
84
- }
85
-
86
- /**
87
- * Calculate next run time for a schedule
88
- */
89
- export function calculateNextRun(schedule) {
90
- if (schedule.interval) {
91
- return new Date(Date.now() + schedule.interval);
92
- }
93
- if (schedule.cron) {
94
- return getNextCronTime(schedule.cron);
95
- }
96
- return null;
97
- }
98
-
99
- /**
100
- * Log to scheduler log file
101
- */
102
- function log(msg) {
103
- const timestamp = new Date().toISOString();
104
- const line = `[${timestamp}] ${msg}\n`;
105
- appendFileSync(SCHEDULER_LOG, line);
106
- }
107
-
108
- /**
109
- * Check and run due schedules
110
- */
111
- async function checkSchedules() {
112
- const schedules = loadSchedules();
113
- const now = new Date();
114
-
115
- for (const schedule of Object.values(schedules)) {
116
- if (!schedule.enabled) continue;
117
-
118
- const nextRun = new Date(schedule.nextRunAt);
119
- if (nextRun > now) continue;
120
-
121
- // Schedule is due - spawn task
122
- log(`Running scheduled task: ${schedule.id} - "${schedule.prompt.slice(0, 50)}..."`);
123
-
124
- try {
125
- const task = await spawnTask(schedule.prompt, {
126
- cwd: schedule.cwd,
127
- scheduleId: schedule.id,
128
- });
129
-
130
- // Update schedule with next run time
131
- const nextRunAt = calculateNextRun(schedule);
132
- updateSchedule(schedule.id, {
133
- lastRunAt: now.toISOString(),
134
- lastTaskId: task.id,
135
- nextRunAt: nextRunAt ? nextRunAt.toISOString() : null,
136
- });
137
-
138
- log(
139
- `Spawned task ${task.id} for schedule ${schedule.id}, next run: ${nextRunAt?.toISOString() || 'none'}`
140
- );
141
- } catch (err) {
142
- log(`Error spawning task for schedule ${schedule.id}: ${err.message}`);
143
- }
144
- }
145
- }
146
-
147
- /**
148
- * Start the scheduler daemon
149
- */
150
- export function startDaemon() {
151
- // Check if already running
152
- if (existsSync(SCHEDULER_PID_FILE)) {
153
- const existingPid = parseInt(readFileSync(SCHEDULER_PID_FILE, 'utf-8').trim(), 10);
154
- try {
155
- process.kill(existingPid, 0);
156
- console.log(`Scheduler already running (PID: ${existingPid})`);
157
- return false;
158
- } catch {
159
- // Process not running, clean up stale PID file
160
- unlinkSync(SCHEDULER_PID_FILE);
161
- }
162
- }
163
-
164
- // Write PID file
165
- writeFileSync(SCHEDULER_PID_FILE, String(process.pid));
166
-
167
- log(`Scheduler daemon started (PID: ${process.pid})`);
168
- console.log(`Scheduler daemon started (PID: ${process.pid})`);
169
-
170
- // Run check loop
171
- const runLoop = async () => {
172
- while (true) {
173
- try {
174
- await checkSchedules();
175
- } catch (err) {
176
- // Log error with full stack trace - scheduler errors are critical bugs
177
- const errorMsg = `SCHEDULER ERROR: ${err.message}\nStack: ${err.stack}`;
178
- log(errorMsg);
179
- console.error(errorMsg);
180
- }
181
- await new Promise((r) => setTimeout(r, CHECK_INTERVAL));
182
- }
183
- };
184
-
185
- // Handle shutdown
186
- process.on('SIGTERM', () => {
187
- log('Scheduler daemon stopping (SIGTERM)');
188
- if (existsSync(SCHEDULER_PID_FILE)) {
189
- unlinkSync(SCHEDULER_PID_FILE);
190
- }
191
- process.exit(0);
192
- });
193
-
194
- process.on('SIGINT', () => {
195
- log('Scheduler daemon stopping (SIGINT)');
196
- if (existsSync(SCHEDULER_PID_FILE)) {
197
- unlinkSync(SCHEDULER_PID_FILE);
198
- }
199
- process.exit(0);
200
- });
201
-
202
- runLoop();
203
- return true;
204
- }
205
-
206
- /**
207
- * Stop the scheduler daemon
208
- */
209
- export function stopDaemon() {
210
- if (!existsSync(SCHEDULER_PID_FILE)) {
211
- console.log('Scheduler is not running');
212
- return false;
213
- }
214
-
215
- const pid = parseInt(readFileSync(SCHEDULER_PID_FILE, 'utf-8').trim(), 10);
216
-
217
- try {
218
- process.kill(pid, 'SIGTERM');
219
- unlinkSync(SCHEDULER_PID_FILE);
220
- console.log(`Scheduler stopped (PID: ${pid})`);
221
- log(`Scheduler daemon stopped by user (PID: ${pid})`);
222
- return true;
223
- } catch {
224
- // Process not running, clean up
225
- unlinkSync(SCHEDULER_PID_FILE);
226
- console.log('Scheduler was not running (cleaned up stale PID file)');
227
- return false;
228
- }
229
- }
230
-
231
- /**
232
- * Get daemon status
233
- */
234
- export function getDaemonStatus() {
235
- if (!existsSync(SCHEDULER_PID_FILE)) {
236
- return { running: false, pid: null };
237
- }
238
-
239
- const pid = parseInt(readFileSync(SCHEDULER_PID_FILE, 'utf-8').trim(), 10);
240
-
241
- try {
242
- process.kill(pid, 0);
243
- return { running: true, pid };
244
- } catch {
245
- return { running: false, pid: null, stale: true };
246
- }
247
- }
248
-
249
- // If run directly, start daemon
250
- if (process.argv[1]?.endsWith('scheduler.js')) {
251
- startDaemon();
252
- }
package/task-lib/store.js DELETED
@@ -1,230 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'fs';
2
- import { TASKS_DIR, TASKS_FILE, LOGS_DIR, SCHEDULES_FILE } from './config.js';
3
- import { generateName } from './name-generator.js';
4
- import lockfile from 'proper-lockfile';
5
-
6
- // Stale lock timeout - 5 seconds is plenty for JSON read/write
7
- const LOCK_STALE_MS = 5000;
8
-
9
- // Lock options with async retry support
10
- const LOCK_OPTIONS = {
11
- stale: LOCK_STALE_MS,
12
- retries: {
13
- retries: 20,
14
- minTimeout: 100,
15
- maxTimeout: 200,
16
- randomize: true,
17
- },
18
- };
19
-
20
- /**
21
- * Remove lock file if it's stale (older than LOCK_STALE_MS)
22
- */
23
- function cleanStaleLock(filePath) {
24
- const lockPath = filePath + '.lock';
25
- try {
26
- if (existsSync(lockPath)) {
27
- const age = Date.now() - statSync(lockPath).mtimeMs;
28
- if (age > LOCK_STALE_MS) {
29
- unlinkSync(lockPath);
30
- }
31
- }
32
- } catch {
33
- // Ignore - another process may have cleaned it
34
- }
35
- }
36
-
37
- export function ensureDirs() {
38
- if (!existsSync(TASKS_DIR)) mkdirSync(TASKS_DIR, { recursive: true });
39
- if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
40
- }
41
-
42
- /**
43
- * Read tasks.json (no locking - use for read-only operations)
44
- */
45
- export function loadTasks() {
46
- ensureDirs();
47
- if (!existsSync(TASKS_FILE)) return {};
48
- const content = readFileSync(TASKS_FILE, 'utf-8');
49
- try {
50
- return JSON.parse(content);
51
- } catch (error) {
52
- throw new Error(
53
- `CRITICAL: tasks.json is corrupted and cannot be parsed. Error: ${error.message}. Content: ${content.slice(0, 200)}...`
54
- );
55
- }
56
- }
57
-
58
- /**
59
- * Write tasks.json (no locking - internal use only)
60
- */
61
- export function saveTasks(tasks) {
62
- ensureDirs();
63
- writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
64
- }
65
-
66
- /**
67
- * Atomic read-modify-write with file locking
68
- * @param {Function} modifier - Function that receives tasks object and returns modified tasks
69
- * @returns {Promise<any>} - Return value from modifier function
70
- */
71
- export async function withTasksLock(modifier) {
72
- ensureDirs();
73
-
74
- // Create file if it doesn't exist (needed for locking)
75
- if (!existsSync(TASKS_FILE)) {
76
- writeFileSync(TASKS_FILE, '{}');
77
- }
78
-
79
- let release;
80
- try {
81
- // Clean stale locks from crashed processes
82
- cleanStaleLock(TASKS_FILE);
83
-
84
- // Acquire lock with async API (proper retries without CPU spin-wait)
85
- release = await lockfile.lock(TASKS_FILE, LOCK_OPTIONS);
86
-
87
- // Read current state
88
- const content = readFileSync(TASKS_FILE, 'utf-8');
89
- let tasks;
90
- try {
91
- tasks = JSON.parse(content);
92
- } catch (error) {
93
- throw new Error(`CRITICAL: tasks.json is corrupted. Error: ${error.message}`);
94
- }
95
-
96
- // Apply modification
97
- const result = modifier(tasks);
98
-
99
- // Write back
100
- writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
101
-
102
- return result;
103
- } finally {
104
- if (release) {
105
- await release();
106
- }
107
- }
108
- }
109
-
110
- export function getTask(id) {
111
- const tasks = loadTasks();
112
- return tasks[id];
113
- }
114
-
115
- export function updateTask(id, updates) {
116
- return withTasksLock((tasks) => {
117
- if (!tasks[id]) return null;
118
- tasks[id] = {
119
- ...tasks[id],
120
- ...updates,
121
- updatedAt: new Date().toISOString(),
122
- };
123
- return tasks[id];
124
- });
125
- }
126
-
127
- export function addTask(task) {
128
- return withTasksLock((tasks) => {
129
- tasks[task.id] = task;
130
- return task;
131
- });
132
- }
133
-
134
- export async function removeTask(id) {
135
- await withTasksLock((tasks) => {
136
- delete tasks[id];
137
- });
138
- }
139
-
140
- export function generateId() {
141
- return generateName('task');
142
- }
143
-
144
- export function generateScheduleId() {
145
- return generateName('sched');
146
- }
147
-
148
- // Schedule management - same pattern with locking
149
-
150
- async function withSchedulesLock(modifier) {
151
- ensureDirs();
152
-
153
- if (!existsSync(SCHEDULES_FILE)) {
154
- writeFileSync(SCHEDULES_FILE, '{}');
155
- }
156
-
157
- let release;
158
- try {
159
- // Clean stale locks from crashed processes
160
- cleanStaleLock(SCHEDULES_FILE);
161
-
162
- // Acquire lock with async API (proper retries without CPU spin-wait)
163
- release = await lockfile.lock(SCHEDULES_FILE, LOCK_OPTIONS);
164
-
165
- const content = readFileSync(SCHEDULES_FILE, 'utf-8');
166
- let schedules;
167
- try {
168
- schedules = JSON.parse(content);
169
- } catch (error) {
170
- throw new Error(`CRITICAL: schedules.json is corrupted. Error: ${error.message}`);
171
- }
172
-
173
- const result = modifier(schedules);
174
- writeFileSync(SCHEDULES_FILE, JSON.stringify(schedules, null, 2));
175
-
176
- return result;
177
- } finally {
178
- if (release) {
179
- await release();
180
- }
181
- }
182
- }
183
-
184
- export function loadSchedules() {
185
- ensureDirs();
186
- if (!existsSync(SCHEDULES_FILE)) return {};
187
- const content = readFileSync(SCHEDULES_FILE, 'utf-8');
188
- try {
189
- return JSON.parse(content);
190
- } catch (error) {
191
- throw new Error(
192
- `CRITICAL: schedules.json is corrupted and cannot be parsed. Error: ${error.message}. Content: ${content.slice(0, 200)}...`
193
- );
194
- }
195
- }
196
-
197
- export function saveSchedules(schedules) {
198
- ensureDirs();
199
- writeFileSync(SCHEDULES_FILE, JSON.stringify(schedules, null, 2));
200
- }
201
-
202
- export function getSchedule(id) {
203
- const schedules = loadSchedules();
204
- return schedules[id];
205
- }
206
-
207
- export function addSchedule(schedule) {
208
- return withSchedulesLock((schedules) => {
209
- schedules[schedule.id] = schedule;
210
- return schedule;
211
- });
212
- }
213
-
214
- export function updateSchedule(id, updates) {
215
- return withSchedulesLock((schedules) => {
216
- if (!schedules[id]) return null;
217
- schedules[id] = {
218
- ...schedules[id],
219
- ...updates,
220
- updatedAt: new Date().toISOString(),
221
- };
222
- return schedules[id];
223
- });
224
- }
225
-
226
- export async function removeSchedule(id) {
227
- await withSchedulesLock((schedules) => {
228
- delete schedules[id];
229
- });
230
- }
@@ -1,166 +0,0 @@
1
- /**
2
- * Formatting utilities for TUI display
3
- */
4
-
5
- /**
6
- * Format timestamp as human-readable relative time
7
- * @param {number} ms - Milliseconds
8
- * @returns {string} Formatted time (e.g., "2m 30s", "1h 15m")
9
- */
10
- function formatTimestamp(ms) {
11
- if (!ms || ms < 0) return '-';
12
-
13
- const seconds = Math.floor(ms / 1000);
14
- const minutes = Math.floor(seconds / 60);
15
- const hours = Math.floor(minutes / 60);
16
- const days = Math.floor(hours / 24);
17
-
18
- if (days > 0) return `${days}d ${hours % 24}h`;
19
- if (hours > 0) return `${hours}h ${minutes % 60}m`;
20
- if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
21
- return `${seconds}s`;
22
- }
23
-
24
- /**
25
- * Format bytes as human-readable size
26
- * @param {number} bytes - Bytes
27
- * @returns {string} Formatted size (e.g., "1.5 MB", "512 KB")
28
- */
29
- function formatBytes(bytes) {
30
- if (!bytes || bytes === 0) return '0 B';
31
- if (bytes < 0) return '-';
32
-
33
- const units = ['B', 'KB', 'MB', 'GB'];
34
- let size = bytes;
35
- let unitIndex = 0;
36
-
37
- while (size >= 1024 && unitIndex < units.length - 1) {
38
- size /= 1024;
39
- unitIndex++;
40
- }
41
-
42
- return `${size.toFixed(1)} ${units[unitIndex]}`;
43
- }
44
-
45
- /**
46
- * Format CPU percentage
47
- * @param {number} cpu - CPU percentage (0-100)
48
- * @returns {string} Formatted CPU (e.g., "23.5%", "0.1%")
49
- */
50
- function formatCPU(cpu) {
51
- if (cpu === undefined || cpu === null || cpu < 0) return '0.0%';
52
- return `${cpu.toFixed(1)}%`;
53
- }
54
-
55
- /**
56
- * Get state icon and color
57
- * @param {string} state - Task state (pending, running, completed, failed, etc.)
58
- * @returns {string} Colored icon
59
- */
60
- function stateIcon(state) {
61
- const icons = {
62
- pending: '○',
63
- running: '●',
64
- completed: '✓',
65
- failed: '✗',
66
- killed: '⊗',
67
- unknown: '?',
68
- };
69
-
70
- const colors = {
71
- pending: 'gray',
72
- running: 'cyan',
73
- completed: 'green',
74
- failed: 'red',
75
- killed: 'red',
76
- unknown: 'gray',
77
- };
78
-
79
- const icon = icons[state] || icons.unknown;
80
- const color = colors[state] || colors.unknown;
81
-
82
- return `{${color}-fg}${icon}{/}`;
83
- }
84
-
85
- /**
86
- * Truncate string to max length with ellipsis
87
- * @param {string} str - String to truncate
88
- * @param {number} maxLen - Maximum length
89
- * @returns {string} Truncated string
90
- */
91
- function truncate(str, maxLen) {
92
- if (!str) return '';
93
- if (str.length <= maxLen) return str;
94
- return str.substring(0, maxLen - 1) + '…';
95
- }
96
-
97
- /**
98
- * Parse event type from Claude JSON stream
99
- * @param {string} line - Raw log line
100
- * @returns {object|null} Parsed event with type, text, toolName, error
101
- */
102
- function parseEvent(line) {
103
- let trimmed = line.trim();
104
-
105
- // Strip timestamp prefix if present: [1234567890]{...} -> {...}
106
- const timestampMatch = trimmed.match(/^\[(\d+)\](.*)$/);
107
- let timestamp = Date.now();
108
- if (timestampMatch) {
109
- timestamp = parseInt(timestampMatch[1]);
110
- trimmed = timestampMatch[2];
111
- }
112
-
113
- // Keep non-JSON lines as-is
114
- if (!trimmed.startsWith('{')) {
115
- return trimmed ? { type: 'raw', text: trimmed, timestamp } : null;
116
- }
117
-
118
- // Parse JSON events
119
- try {
120
- const event = JSON.parse(trimmed);
121
-
122
- // Text delta
123
- if (event.type === 'stream_event' && event.event?.type === 'content_block_delta') {
124
- return {
125
- type: 'text',
126
- text: event.event?.delta?.text || '',
127
- timestamp,
128
- };
129
- }
130
- // Tool use
131
- else if (event.type === 'stream_event' && event.event?.type === 'content_block_start') {
132
- const block = event.event?.content_block;
133
- if (block?.type === 'tool_use' && block?.name) {
134
- return {
135
- type: 'tool',
136
- toolName: block.name,
137
- timestamp,
138
- };
139
- }
140
- }
141
- // Assistant message
142
- else if (event.type === 'assistant' && event.message?.content) {
143
- let text = '';
144
- for (const content of event.message.content) {
145
- if (content.type === 'text') {
146
- text += content.text;
147
- }
148
- }
149
- return text ? { type: 'text', text, timestamp } : null;
150
- }
151
- // Error
152
- else if (event.type === 'result' && event.is_error) {
153
- return {
154
- type: 'error',
155
- text: event.result || 'Unknown error',
156
- timestamp,
157
- };
158
- }
159
- } catch {
160
- // Parse error - skip
161
- }
162
-
163
- return null;
164
- }
165
-
166
- export { formatTimestamp, formatBytes, formatCPU, stateIcon, truncate, parseEvent };