@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.
- package/LICENSE +1 -1
- package/README.md +8 -385
- package/cli/index.js +108 -4615
- package/package.json +16 -125
- package/CHANGELOG.md +0 -537
- package/cli/commands/providers.js +0 -150
- package/cli/lib/first-run.js +0 -211
- package/cli/lib/update-checker.js +0 -281
- package/cli/message-formatter-utils.js +0 -75
- package/cli/message-formatters-normal.js +0 -253
- package/cli/message-formatters-watch.js +0 -181
- package/cluster-templates/base-templates/debug-workflow.json +0 -375
- package/cluster-templates/base-templates/full-workflow.json +0 -697
- package/cluster-templates/base-templates/single-worker.json +0 -63
- package/cluster-templates/base-templates/worker-validator.json +0 -152
- package/cluster-templates/conductor-bootstrap.json +0 -124
- package/docker/zeroshot-cluster/Dockerfile +0 -152
- package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
- package/lib/completion.js +0 -174
- package/lib/docker-config.js +0 -212
- package/lib/id-detector.js +0 -53
- package/lib/provider-detection.js +0 -59
- package/lib/provider-names.js +0 -56
- package/lib/settings.js +0 -405
- package/lib/stream-json-parser.js +0 -10
- package/scripts/fix-node-pty-permissions.js +0 -75
- package/scripts/record-demo.sh +0 -279
- package/scripts/test-install.sh +0 -40
- package/scripts/validate-templates.js +0 -100
- package/src/agent/agent-config.js +0 -193
- package/src/agent/agent-context-builder.js +0 -335
- package/src/agent/agent-hook-executor.js +0 -402
- package/src/agent/agent-lifecycle.js +0 -683
- package/src/agent/agent-stuck-detector.js +0 -256
- package/src/agent/agent-task-executor.js +0 -1431
- package/src/agent/agent-trigger-evaluator.js +0 -67
- package/src/agent/output-extraction.js +0 -200
- package/src/agent/output-reformatter.js +0 -175
- package/src/agent/schema-utils.js +0 -111
- package/src/agent-wrapper.js +0 -570
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/attach/attach-client.js +0 -438
- package/src/attach/attach-server.js +0 -543
- package/src/attach/index.js +0 -35
- package/src/attach/protocol.js +0 -220
- package/src/attach/ring-buffer.js +0 -121
- package/src/attach/socket-discovery.js +0 -267
- package/src/claude-task-runner.js +0 -523
- package/src/config-router.js +0 -80
- package/src/config-validator.js +0 -1660
- package/src/copy-worker.js +0 -43
- package/src/github.js +0 -139
- package/src/isolation-manager.js +0 -1381
- package/src/ledger.js +0 -614
- package/src/logic-engine.js +0 -223
- package/src/message-bus-bridge.js +0 -139
- package/src/message-bus.js +0 -249
- package/src/name-generator.js +0 -232
- package/src/orchestrator.js +0 -2519
- package/src/preflight.js +0 -485
- package/src/process-metrics.js +0 -566
- package/src/providers/anthropic/cli-builder.js +0 -45
- package/src/providers/anthropic/index.js +0 -134
- package/src/providers/anthropic/models.js +0 -23
- package/src/providers/anthropic/output-parser.js +0 -159
- package/src/providers/base-provider.js +0 -181
- package/src/providers/capabilities.js +0 -51
- package/src/providers/google/cli-builder.js +0 -55
- package/src/providers/google/index.js +0 -116
- package/src/providers/google/models.js +0 -24
- package/src/providers/google/output-parser.js +0 -92
- package/src/providers/index.js +0 -75
- package/src/providers/openai/cli-builder.js +0 -122
- package/src/providers/openai/index.js +0 -135
- package/src/providers/openai/models.js +0 -21
- package/src/providers/openai/output-parser.js +0 -129
- package/src/schemas/sub-cluster.js +0 -156
- package/src/status-footer.js +0 -980
- package/src/sub-cluster-wrapper.js +0 -560
- package/src/task-runner.js +0 -30
- package/src/template-resolver.js +0 -369
- package/src/tui/CHANGES.txt +0 -133
- package/src/tui/LAYOUT.md +0 -261
- package/src/tui/README.txt +0 -192
- package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
- package/src/tui/data-poller.js +0 -325
- package/src/tui/demo.js +0 -208
- package/src/tui/formatters.js +0 -128
- package/src/tui/index.js +0 -193
- package/src/tui/keybindings.js +0 -383
- package/src/tui/layout.js +0 -334
- package/src/tui/renderer.js +0 -194
- package/task-lib/attachable-watcher.js +0 -272
- package/task-lib/claude-recovery.js +0 -119
- package/task-lib/commands/clean.js +0 -50
- package/task-lib/commands/get-log-path.js +0 -23
- package/task-lib/commands/kill.js +0 -32
- package/task-lib/commands/list.js +0 -105
- package/task-lib/commands/logs.js +0 -411
- package/task-lib/commands/resume.js +0 -42
- package/task-lib/commands/run.js +0 -57
- package/task-lib/commands/schedule.js +0 -105
- package/task-lib/commands/scheduler-cmd.js +0 -96
- package/task-lib/commands/schedules.js +0 -98
- package/task-lib/commands/status.js +0 -44
- package/task-lib/commands/unschedule.js +0 -16
- package/task-lib/completion.js +0 -9
- package/task-lib/config.js +0 -10
- package/task-lib/name-generator.js +0 -230
- package/task-lib/package.json +0 -3
- package/task-lib/runner.js +0 -144
- package/task-lib/scheduler.js +0 -252
- package/task-lib/store.js +0 -230
- package/task-lib/tui/formatters.js +0 -166
- package/task-lib/tui/index.js +0 -197
- package/task-lib/tui/layout.js +0 -111
- package/task-lib/tui/renderer.js +0 -119
- package/task-lib/tui.js +0 -384
- package/task-lib/watcher.js +0 -188
package/task-lib/scheduler.js
DELETED
|
@@ -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 };
|