@covibes/zeroshot 1.0.1 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@covibes/zeroshot",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Multi-agent orchestration engine for Claude - cluster coordinator and CLI",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -75,6 +75,7 @@
75
75
  "src/",
76
76
  "lib/",
77
77
  "cli/",
78
+ "task-lib/",
78
79
  "cluster-templates/",
79
80
  "hooks/",
80
81
  "docker/",
@@ -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
+ }