@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,252 @@
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
+ 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 = 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
+ }
@@ -0,0 +1,217 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } 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
+ // Lock options for sync API (no retries allowed)
7
+ const LOCK_OPTIONS = {
8
+ stale: 30000, // Consider lock stale after 30s
9
+ };
10
+
11
+ // Retry wrapper for sync lock acquisition
12
+ function lockWithRetry(file, options, maxRetries = 100, delayMs = 100) {
13
+ for (let i = 0; i < maxRetries; i++) {
14
+ try {
15
+ return lockfile.lockSync(file, options);
16
+ } catch (err) {
17
+ if (err.code === 'ELOCKED' && i < maxRetries - 1) {
18
+ // File is locked, wait and retry
19
+ const start = Date.now();
20
+ while (Date.now() - start < delayMs) {
21
+ // Busy wait (sync)
22
+ }
23
+ continue;
24
+ }
25
+ throw err;
26
+ }
27
+ }
28
+ throw new Error(`Failed to acquire lock after ${maxRetries} retries`);
29
+ }
30
+
31
+ export function ensureDirs() {
32
+ if (!existsSync(TASKS_DIR)) mkdirSync(TASKS_DIR, { recursive: true });
33
+ if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
34
+ }
35
+
36
+ /**
37
+ * Read tasks.json (no locking - use for read-only operations)
38
+ */
39
+ export function loadTasks() {
40
+ ensureDirs();
41
+ if (!existsSync(TASKS_FILE)) return {};
42
+ const content = readFileSync(TASKS_FILE, 'utf-8');
43
+ try {
44
+ return JSON.parse(content);
45
+ } catch (error) {
46
+ throw new Error(
47
+ `CRITICAL: tasks.json is corrupted and cannot be parsed. Error: ${error.message}. Content: ${content.slice(0, 200)}...`
48
+ );
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Write tasks.json (no locking - internal use only)
54
+ */
55
+ export function saveTasks(tasks) {
56
+ ensureDirs();
57
+ writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
58
+ }
59
+
60
+ /**
61
+ * Atomic read-modify-write with file locking
62
+ * @param {Function} modifier - Function that receives tasks object and returns modified tasks
63
+ * @returns {any} - Return value from modifier function
64
+ */
65
+ export function withTasksLock(modifier) {
66
+ ensureDirs();
67
+
68
+ // Create file if it doesn't exist (needed for locking)
69
+ if (!existsSync(TASKS_FILE)) {
70
+ writeFileSync(TASKS_FILE, '{}');
71
+ }
72
+
73
+ let release;
74
+ try {
75
+ // Acquire lock (blocks until available)
76
+ release = lockWithRetry(TASKS_FILE, LOCK_OPTIONS);
77
+
78
+ // Read current state
79
+ const content = readFileSync(TASKS_FILE, 'utf-8');
80
+ let tasks;
81
+ try {
82
+ tasks = JSON.parse(content);
83
+ } catch (error) {
84
+ throw new Error(`CRITICAL: tasks.json is corrupted. Error: ${error.message}`);
85
+ }
86
+
87
+ // Apply modification
88
+ const result = modifier(tasks);
89
+
90
+ // Write back
91
+ writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
92
+
93
+ return result;
94
+ } finally {
95
+ if (release) {
96
+ release();
97
+ }
98
+ }
99
+ }
100
+
101
+ export function getTask(id) {
102
+ const tasks = loadTasks();
103
+ return tasks[id];
104
+ }
105
+
106
+ export function updateTask(id, updates) {
107
+ return withTasksLock((tasks) => {
108
+ if (!tasks[id]) return null;
109
+ tasks[id] = {
110
+ ...tasks[id],
111
+ ...updates,
112
+ updatedAt: new Date().toISOString(),
113
+ };
114
+ return tasks[id];
115
+ });
116
+ }
117
+
118
+ export function addTask(task) {
119
+ return withTasksLock((tasks) => {
120
+ tasks[task.id] = task;
121
+ return task;
122
+ });
123
+ }
124
+
125
+ export function removeTask(id) {
126
+ withTasksLock((tasks) => {
127
+ delete tasks[id];
128
+ });
129
+ }
130
+
131
+ export function generateId() {
132
+ return generateName('task');
133
+ }
134
+
135
+ export function generateScheduleId() {
136
+ return generateName('sched');
137
+ }
138
+
139
+ // Schedule management - same pattern with locking
140
+
141
+ function withSchedulesLock(modifier) {
142
+ ensureDirs();
143
+
144
+ if (!existsSync(SCHEDULES_FILE)) {
145
+ writeFileSync(SCHEDULES_FILE, '{}');
146
+ }
147
+
148
+ let release;
149
+ try {
150
+ release = lockWithRetry(SCHEDULES_FILE, LOCK_OPTIONS);
151
+
152
+ const content = readFileSync(SCHEDULES_FILE, 'utf-8');
153
+ let schedules;
154
+ try {
155
+ schedules = JSON.parse(content);
156
+ } catch (error) {
157
+ throw new Error(`CRITICAL: schedules.json is corrupted. Error: ${error.message}`);
158
+ }
159
+
160
+ const result = modifier(schedules);
161
+ writeFileSync(SCHEDULES_FILE, JSON.stringify(schedules, null, 2));
162
+
163
+ return result;
164
+ } finally {
165
+ if (release) {
166
+ release();
167
+ }
168
+ }
169
+ }
170
+
171
+ export function loadSchedules() {
172
+ ensureDirs();
173
+ if (!existsSync(SCHEDULES_FILE)) return {};
174
+ const content = readFileSync(SCHEDULES_FILE, 'utf-8');
175
+ try {
176
+ return JSON.parse(content);
177
+ } catch (error) {
178
+ throw new Error(
179
+ `CRITICAL: schedules.json is corrupted and cannot be parsed. Error: ${error.message}. Content: ${content.slice(0, 200)}...`
180
+ );
181
+ }
182
+ }
183
+
184
+ export function saveSchedules(schedules) {
185
+ ensureDirs();
186
+ writeFileSync(SCHEDULES_FILE, JSON.stringify(schedules, null, 2));
187
+ }
188
+
189
+ export function getSchedule(id) {
190
+ const schedules = loadSchedules();
191
+ return schedules[id];
192
+ }
193
+
194
+ export function addSchedule(schedule) {
195
+ return withSchedulesLock((schedules) => {
196
+ schedules[schedule.id] = schedule;
197
+ return schedule;
198
+ });
199
+ }
200
+
201
+ export function updateSchedule(id, updates) {
202
+ return withSchedulesLock((schedules) => {
203
+ if (!schedules[id]) return null;
204
+ schedules[id] = {
205
+ ...schedules[id],
206
+ ...updates,
207
+ updatedAt: new Date().toISOString(),
208
+ };
209
+ return schedules[id];
210
+ });
211
+ }
212
+
213
+ export function removeSchedule(id) {
214
+ withSchedulesLock((schedules) => {
215
+ delete schedules[id];
216
+ });
217
+ }
@@ -0,0 +1,166 @@
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 };