@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.
- package/CHANGELOG.md +46 -0
- package/README.md +2 -0
- package/cli/index.js +151 -208
- package/cli/message-formatter-utils.js +75 -0
- package/cli/message-formatters-normal.js +214 -0
- package/cli/message-formatters-watch.js +181 -0
- package/cluster-templates/base-templates/full-workflow.json +10 -5
- package/docker/zeroshot-cluster/Dockerfile +6 -0
- package/package.json +5 -2
- package/src/agent/agent-task-executor.js +237 -112
- package/src/isolation-manager.js +94 -51
- package/src/orchestrator.js +45 -10
- package/src/preflight.js +383 -0
- package/src/process-metrics.js +546 -0
- package/src/status-footer.js +543 -0
- package/task-lib/attachable-watcher.js +202 -0
- package/task-lib/commands/clean.js +50 -0
- package/task-lib/commands/get-log-path.js +23 -0
- package/task-lib/commands/kill.js +32 -0
- package/task-lib/commands/list.js +105 -0
- package/task-lib/commands/logs.js +411 -0
- package/task-lib/commands/resume.js +41 -0
- package/task-lib/commands/run.js +48 -0
- package/task-lib/commands/schedule.js +105 -0
- package/task-lib/commands/scheduler-cmd.js +96 -0
- package/task-lib/commands/schedules.js +98 -0
- package/task-lib/commands/status.js +44 -0
- package/task-lib/commands/unschedule.js +16 -0
- package/task-lib/completion.js +9 -0
- package/task-lib/config.js +10 -0
- package/task-lib/name-generator.js +230 -0
- package/task-lib/package.json +3 -0
- package/task-lib/runner.js +123 -0
- package/task-lib/scheduler.js +252 -0
- package/task-lib/store.js +217 -0
- package/task-lib/tui/formatters.js +166 -0
- package/task-lib/tui/index.js +197 -0
- package/task-lib/tui/layout.js +111 -0
- package/task-lib/tui/renderer.js +119 -0
- package/task-lib/tui.js +384 -0
- package/task-lib/watcher.js +162 -0
- package/cluster-templates/conductor-junior-bootstrap.json +0 -69
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Attachable Watcher - spawns Claude with PTY for attach/detach support
|
|
5
|
+
*
|
|
6
|
+
* Runs detached from parent, provides Unix socket for attach clients.
|
|
7
|
+
* Uses node-pty for proper terminal emulation.
|
|
8
|
+
*
|
|
9
|
+
* Key differences from legacy watcher.js:
|
|
10
|
+
* - Uses AttachServer (node-pty) instead of child_process.spawn
|
|
11
|
+
* - Creates Unix socket at ~/.zeroshot/sockets/task-<id>.sock
|
|
12
|
+
* - Supports multiple attached clients
|
|
13
|
+
* - Still writes to log file for backward compatibility
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { homedir } from 'os';
|
|
19
|
+
import { updateTask } from './store.js';
|
|
20
|
+
|
|
21
|
+
// Import attach infrastructure from src package (CommonJS)
|
|
22
|
+
import { createRequire } from 'module';
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const { AttachServer } = require('../src/attach');
|
|
25
|
+
|
|
26
|
+
// Parse command line args (same format as legacy watcher)
|
|
27
|
+
const [, , taskId, cwd, logFile, argsJson, configJson] = process.argv;
|
|
28
|
+
const args = JSON.parse(argsJson);
|
|
29
|
+
const config = configJson ? JSON.parse(configJson) : {};
|
|
30
|
+
|
|
31
|
+
// Socket path for attach
|
|
32
|
+
const SOCKET_DIR = join(homedir(), '.zeroshot', 'sockets');
|
|
33
|
+
const socketPath = join(SOCKET_DIR, `${taskId}.sock`);
|
|
34
|
+
|
|
35
|
+
// Ensure socket directory exists
|
|
36
|
+
if (!existsSync(SOCKET_DIR)) {
|
|
37
|
+
mkdirSync(SOCKET_DIR, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function log(msg) {
|
|
41
|
+
appendFileSync(logFile, msg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Build environment - remove API key to force subscription credentials
|
|
45
|
+
const env = { ...process.env };
|
|
46
|
+
delete env.ANTHROPIC_API_KEY;
|
|
47
|
+
|
|
48
|
+
// Add model flag - priority: config.model > ANTHROPIC_MODEL env var
|
|
49
|
+
const claudeArgs = [...args];
|
|
50
|
+
const model = config.model || env.ANTHROPIC_MODEL;
|
|
51
|
+
if (model && !claudeArgs.includes('--model')) {
|
|
52
|
+
claudeArgs.unshift('--model', model);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// For JSON schema output with silent mode, track final result
|
|
56
|
+
const silentJsonMode =
|
|
57
|
+
config.outputFormat === 'json' && config.jsonSchema && config.silentJsonOutput;
|
|
58
|
+
let finalResultJson = null;
|
|
59
|
+
|
|
60
|
+
// Buffer for incomplete lines
|
|
61
|
+
let outputBuffer = '';
|
|
62
|
+
|
|
63
|
+
// Create AttachServer to spawn Claude with PTY
|
|
64
|
+
const server = new AttachServer({
|
|
65
|
+
id: taskId,
|
|
66
|
+
socketPath,
|
|
67
|
+
command: 'claude',
|
|
68
|
+
args: claudeArgs,
|
|
69
|
+
cwd,
|
|
70
|
+
env,
|
|
71
|
+
cols: 120,
|
|
72
|
+
rows: 30,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Handle output from PTY
|
|
76
|
+
server.on('output', (data) => {
|
|
77
|
+
const chunk = data.toString();
|
|
78
|
+
const timestamp = Date.now();
|
|
79
|
+
|
|
80
|
+
if (silentJsonMode) {
|
|
81
|
+
// Parse each line to find structured_output
|
|
82
|
+
outputBuffer += chunk;
|
|
83
|
+
const lines = outputBuffer.split('\n');
|
|
84
|
+
outputBuffer = lines.pop() || '';
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (!line.trim()) continue;
|
|
88
|
+
try {
|
|
89
|
+
const json = JSON.parse(line);
|
|
90
|
+
if (json.structured_output) {
|
|
91
|
+
finalResultJson = line;
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Not JSON, skip
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Normal mode - stream with timestamps
|
|
99
|
+
outputBuffer += chunk;
|
|
100
|
+
const lines = outputBuffer.split('\n');
|
|
101
|
+
outputBuffer = lines.pop() || '';
|
|
102
|
+
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
log(`[${timestamp}]${line}\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Handle process exit
|
|
110
|
+
server.on('exit', ({ exitCode, signal }) => {
|
|
111
|
+
const timestamp = Date.now();
|
|
112
|
+
const code = exitCode;
|
|
113
|
+
|
|
114
|
+
// Flush remaining buffered output
|
|
115
|
+
if (outputBuffer.trim()) {
|
|
116
|
+
if (silentJsonMode) {
|
|
117
|
+
try {
|
|
118
|
+
const json = JSON.parse(outputBuffer);
|
|
119
|
+
if (json.structured_output) {
|
|
120
|
+
finalResultJson = outputBuffer;
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Not valid JSON
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
log(`[${timestamp}]${outputBuffer}\n`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// In silent JSON mode, log ONLY the final structured_output JSON
|
|
131
|
+
if (silentJsonMode && finalResultJson) {
|
|
132
|
+
log(finalResultJson + '\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Skip footer for pure JSON output
|
|
136
|
+
if (config.outputFormat !== 'json') {
|
|
137
|
+
log(`\n${'='.repeat(50)}\n`);
|
|
138
|
+
log(`Finished: ${new Date().toISOString()}\n`);
|
|
139
|
+
log(`Exit code: ${code}, Signal: ${signal}\n`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Simple status: completed if exit 0, failed otherwise
|
|
143
|
+
const status = code === 0 ? 'completed' : 'failed';
|
|
144
|
+
updateTask(taskId, {
|
|
145
|
+
status,
|
|
146
|
+
exitCode: code,
|
|
147
|
+
error: signal ? `Killed by ${signal}` : null,
|
|
148
|
+
socketPath: null, // Clear socket path on exit
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Give clients time to receive exit message before exiting
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}, 500);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Handle errors
|
|
158
|
+
server.on('error', (err) => {
|
|
159
|
+
log(`\nError: ${err.message}\n`);
|
|
160
|
+
updateTask(taskId, { status: 'failed', error: err.message });
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Handle client attach/detach for logging
|
|
165
|
+
server.on('clientAttach', ({ clientId }) => {
|
|
166
|
+
log(`[${Date.now()}][ATTACH] Client attached: ${clientId.slice(0, 8)}...\n`);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
server.on('clientDetach', ({ clientId }) => {
|
|
170
|
+
log(`[${Date.now()}][DETACH] Client detached: ${clientId.slice(0, 8)}...\n`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Start the server
|
|
174
|
+
try {
|
|
175
|
+
await server.start();
|
|
176
|
+
|
|
177
|
+
// Update task with PID and socket path
|
|
178
|
+
updateTask(taskId, {
|
|
179
|
+
pid: server.pid,
|
|
180
|
+
socketPath,
|
|
181
|
+
attachable: true,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
log(`[${Date.now()}][SYSTEM] Started with PTY (attachable)\n`);
|
|
185
|
+
log(`[${Date.now()}][SYSTEM] Socket: ${socketPath}\n`);
|
|
186
|
+
log(`[${Date.now()}][SYSTEM] PID: ${server.pid}\n`);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
log(`\nFailed to start: ${err.message}\n`);
|
|
189
|
+
updateTask(taskId, { status: 'failed', error: err.message });
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Handle process signals for cleanup
|
|
194
|
+
process.on('SIGTERM', async () => {
|
|
195
|
+
log(`[${Date.now()}][SYSTEM] Received SIGTERM, stopping...\n`);
|
|
196
|
+
await server.stop('SIGTERM');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
process.on('SIGINT', async () => {
|
|
200
|
+
log(`[${Date.now()}][SYSTEM] Received SIGINT, stopping...\n`);
|
|
201
|
+
await server.stop('SIGINT');
|
|
202
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { unlinkSync, existsSync } from 'fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { loadTasks, saveTasks } from '../store.js';
|
|
4
|
+
|
|
5
|
+
export function cleanTasks(options = {}) {
|
|
6
|
+
const tasks = loadTasks();
|
|
7
|
+
const taskList = Object.values(tasks);
|
|
8
|
+
|
|
9
|
+
if (taskList.length === 0) {
|
|
10
|
+
console.log(chalk.dim('No tasks to clean.'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const toRemove = [];
|
|
15
|
+
|
|
16
|
+
for (const task of taskList) {
|
|
17
|
+
const shouldRemove =
|
|
18
|
+
options.all ||
|
|
19
|
+
(options.completed && task.status === 'completed') ||
|
|
20
|
+
(options.failed &&
|
|
21
|
+
(task.status === 'failed' || task.status === 'stale' || task.status === 'killed'));
|
|
22
|
+
|
|
23
|
+
if (shouldRemove) {
|
|
24
|
+
toRemove.push(task);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (toRemove.length === 0) {
|
|
29
|
+
console.log(chalk.dim('No tasks match the criteria.'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(chalk.dim(`Removing ${toRemove.length} task(s)...\n`));
|
|
34
|
+
|
|
35
|
+
for (const task of toRemove) {
|
|
36
|
+
// Delete log file
|
|
37
|
+
if (task.logFile && existsSync(task.logFile)) {
|
|
38
|
+
unlinkSync(task.logFile);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Remove from tasks
|
|
42
|
+
delete tasks[task.id];
|
|
43
|
+
|
|
44
|
+
console.log(chalk.dim(` Removed: ${task.id} [${task.status}]`));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
saveTasks(tasks);
|
|
48
|
+
|
|
49
|
+
console.log(chalk.green(`\n✓ Cleaned ${toRemove.length} task(s)`));
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getTask } from '../store.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get log file path for a task (machine-readable output)
|
|
5
|
+
* Used by cluster/agent-wrapper.js to follow task logs
|
|
6
|
+
* @param {string} taskId - Task ID
|
|
7
|
+
*/
|
|
8
|
+
export function getLogPath(taskId) {
|
|
9
|
+
const task = getTask(taskId);
|
|
10
|
+
|
|
11
|
+
if (!task) {
|
|
12
|
+
console.error(`Task not found: ${taskId}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!task.logFile) {
|
|
17
|
+
console.error(`No log file for task: ${taskId}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Output just the path (machine-readable)
|
|
22
|
+
console.log(task.logFile);
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getTask, updateTask } from '../store.js';
|
|
3
|
+
import { killTask as killProcess, isProcessRunning } from '../runner.js';
|
|
4
|
+
|
|
5
|
+
export function killTaskCommand(taskId) {
|
|
6
|
+
const task = getTask(taskId);
|
|
7
|
+
|
|
8
|
+
if (!task) {
|
|
9
|
+
console.log(chalk.red(`Task not found: ${taskId}`));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (task.status !== 'running') {
|
|
14
|
+
console.log(chalk.yellow(`Task is not running (status: ${task.status})`));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!isProcessRunning(task.pid)) {
|
|
19
|
+
console.log(chalk.yellow('Process already dead, updating status...'));
|
|
20
|
+
updateTask(taskId, { status: 'stale', error: 'Process died unexpectedly' });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const killed = killProcess(task.pid);
|
|
25
|
+
|
|
26
|
+
if (killed) {
|
|
27
|
+
console.log(chalk.green(`✓ Sent SIGTERM to task ${taskId} (PID: ${task.pid})`));
|
|
28
|
+
updateTask(taskId, { status: 'killed', error: 'Killed by user' });
|
|
29
|
+
} else {
|
|
30
|
+
console.log(chalk.red(`Failed to kill task ${taskId}`));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadTasks } from '../store.js';
|
|
3
|
+
import { isProcessRunning } from '../runner.js';
|
|
4
|
+
|
|
5
|
+
export function listTasks(options = {}) {
|
|
6
|
+
const tasks = loadTasks();
|
|
7
|
+
const taskList = Object.values(tasks);
|
|
8
|
+
|
|
9
|
+
if (taskList.length === 0) {
|
|
10
|
+
console.log(chalk.dim('No tasks found.'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Sort by creation date, newest first
|
|
15
|
+
taskList.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
16
|
+
|
|
17
|
+
// Filter by status if specified
|
|
18
|
+
let filtered = taskList;
|
|
19
|
+
if (options.status) {
|
|
20
|
+
filtered = taskList.filter((t) => t.status === options.status);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Limit results
|
|
24
|
+
const limit = options.limit || 20;
|
|
25
|
+
filtered = filtered.slice(0, limit);
|
|
26
|
+
|
|
27
|
+
// Table format (default) or verbose format
|
|
28
|
+
if (options.verbose) {
|
|
29
|
+
// Verbose format (old behavior)
|
|
30
|
+
console.log(chalk.bold(`\nClaude Tasks (${filtered.length}/${taskList.length})\n`));
|
|
31
|
+
|
|
32
|
+
for (const task of filtered) {
|
|
33
|
+
// Verify running status
|
|
34
|
+
let status = task.status;
|
|
35
|
+
if (status === 'running' && !isProcessRunning(task.pid)) {
|
|
36
|
+
status = 'stale';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const statusColor =
|
|
40
|
+
{
|
|
41
|
+
running: chalk.blue,
|
|
42
|
+
completed: chalk.green,
|
|
43
|
+
failed: chalk.red,
|
|
44
|
+
stale: chalk.yellow,
|
|
45
|
+
}[status] || chalk.dim;
|
|
46
|
+
|
|
47
|
+
const age = getAge(task.createdAt);
|
|
48
|
+
const timestamp = new Date(task.createdAt).toLocaleString();
|
|
49
|
+
|
|
50
|
+
console.log(
|
|
51
|
+
`${statusColor('●')} ${chalk.cyan(task.id)} ${statusColor(`[${status}]`)} ${chalk.dim(age + ' • ' + timestamp)}`
|
|
52
|
+
);
|
|
53
|
+
console.log(` ${chalk.dim('CWD:')} ${task.cwd}`);
|
|
54
|
+
console.log(` ${chalk.dim('Prompt:')} ${task.prompt}`);
|
|
55
|
+
if (task.pid && status === 'running') {
|
|
56
|
+
console.log(` ${chalk.dim('PID:')} ${task.pid}`);
|
|
57
|
+
}
|
|
58
|
+
if (task.error) {
|
|
59
|
+
console.log(` ${chalk.red('Error:')} ${task.error}`);
|
|
60
|
+
}
|
|
61
|
+
console.log();
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// Table format (clean, default)
|
|
65
|
+
console.log(chalk.bold(`\n=== Tasks (${filtered.length}/${taskList.length}) ===`));
|
|
66
|
+
console.log(`${'ID'.padEnd(25)} ${'Status'.padEnd(12)} ${'Age'.padEnd(10)} CWD`);
|
|
67
|
+
console.log('-'.repeat(100));
|
|
68
|
+
|
|
69
|
+
for (const task of filtered) {
|
|
70
|
+
// Verify running status
|
|
71
|
+
let status = task.status;
|
|
72
|
+
if (status === 'running' && !isProcessRunning(task.pid)) {
|
|
73
|
+
status = 'stale';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const statusColor =
|
|
77
|
+
{
|
|
78
|
+
running: chalk.blue,
|
|
79
|
+
completed: chalk.green,
|
|
80
|
+
failed: chalk.red,
|
|
81
|
+
stale: chalk.yellow,
|
|
82
|
+
}[status] || chalk.dim;
|
|
83
|
+
|
|
84
|
+
const age = getAge(task.createdAt);
|
|
85
|
+
const cwd = task.cwd.replace(process.env.HOME, '~');
|
|
86
|
+
|
|
87
|
+
console.log(
|
|
88
|
+
`${chalk.cyan(task.id.padEnd(25))} ${statusColor(status.padEnd(12))} ${chalk.dim(age.padEnd(10))} ${chalk.dim(cwd)}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getAge(dateStr) {
|
|
96
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
97
|
+
const mins = Math.floor(diff / 60000);
|
|
98
|
+
const hours = Math.floor(mins / 60);
|
|
99
|
+
const days = Math.floor(hours / 24);
|
|
100
|
+
|
|
101
|
+
if (days > 0) return `${days}d ago`;
|
|
102
|
+
if (hours > 0) return `${hours}h ago`;
|
|
103
|
+
if (mins > 0) return `${mins}m ago`;
|
|
104
|
+
return 'just now';
|
|
105
|
+
}
|