@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,220 +0,0 @@
1
- /**
2
- * Protocol - Message framing for attach/detach IPC
3
- *
4
- * Uses length-prefixed JSON messages over Unix sockets.
5
- * Format: [4-byte length (BE)] [JSON payload]
6
- *
7
- * Message Types:
8
- *
9
- * Client → Server:
10
- * ATTACH { type: 'attach', clientId, cols, rows }
11
- * DETACH { type: 'detach', clientId }
12
- * RESIZE { type: 'resize', cols, rows }
13
- * SIGNAL { type: 'signal', signal: 'SIGINT' | 'SIGTERM' }
14
- * STDIN { type: 'stdin', data: base64 } (future interactive mode)
15
- *
16
- * Server → Client:
17
- * OUTPUT { type: 'output', data: base64, timestamp }
18
- * HISTORY { type: 'history', data: base64 }
19
- * STATE { type: 'state', status, pid, ... }
20
- * EXIT { type: 'exit', code, signal }
21
- * ERROR { type: 'error', message }
22
- */
23
-
24
- const MAX_MESSAGE_SIZE = 10 * 1024 * 1024; // 10MB max message
25
-
26
- /**
27
- * Encode a message for transmission
28
- * @param {object} message - Message object to encode
29
- * @returns {Buffer} - Length-prefixed encoded message
30
- */
31
- function encode(message) {
32
- const json = JSON.stringify(message);
33
- const payload = Buffer.from(json, 'utf8');
34
-
35
- if (payload.length > MAX_MESSAGE_SIZE) {
36
- throw new Error(`Message too large: ${payload.length} bytes (max ${MAX_MESSAGE_SIZE})`);
37
- }
38
-
39
- const frame = Buffer.alloc(4 + payload.length);
40
- frame.writeUInt32BE(payload.length, 0);
41
- payload.copy(frame, 4);
42
-
43
- return frame;
44
- }
45
-
46
- /**
47
- * MessageDecoder - Streaming decoder for framed messages
48
- *
49
- * Handles partial reads and message reassembly.
50
- */
51
- class MessageDecoder {
52
- constructor() {
53
- this.buffer = Buffer.alloc(0);
54
- }
55
-
56
- /**
57
- * Feed data into the decoder
58
- * @param {Buffer} data - Received data chunk
59
- * @returns {object[]} - Array of decoded messages (may be empty)
60
- */
61
- feed(data) {
62
- this.buffer = Buffer.concat([this.buffer, data]);
63
- const messages = [];
64
-
65
- while (this.buffer.length >= 4) {
66
- const length = this.buffer.readUInt32BE(0);
67
-
68
- if (length > MAX_MESSAGE_SIZE) {
69
- throw new Error(`Message too large: ${length} bytes (max ${MAX_MESSAGE_SIZE})`);
70
- }
71
-
72
- if (this.buffer.length < 4 + length) {
73
- // Incomplete message, wait for more data
74
- break;
75
- }
76
-
77
- const payload = this.buffer.slice(4, 4 + length);
78
- this.buffer = this.buffer.slice(4 + length);
79
-
80
- try {
81
- const message = JSON.parse(payload.toString('utf8'));
82
- messages.push(message);
83
- } catch (e) {
84
- throw new Error(`Invalid JSON in message: ${e.message}`);
85
- }
86
- }
87
-
88
- return messages;
89
- }
90
-
91
- /**
92
- * Reset decoder state
93
- */
94
- reset() {
95
- this.buffer = Buffer.alloc(0);
96
- }
97
- }
98
-
99
- // Message type constants
100
- const MessageType = {
101
- // Client → Server
102
- ATTACH: 'attach',
103
- DETACH: 'detach',
104
- RESIZE: 'resize',
105
- SIGNAL: 'signal',
106
- STDIN: 'stdin',
107
-
108
- // Server → Client
109
- OUTPUT: 'output',
110
- HISTORY: 'history',
111
- STATE: 'state',
112
- EXIT: 'exit',
113
- ERROR: 'error',
114
- };
115
-
116
- // Helper functions to create messages
117
-
118
- /**
119
- * Create an ATTACH message
120
- */
121
- function createAttachMessage(clientId, cols, rows) {
122
- return { type: MessageType.ATTACH, clientId, cols, rows };
123
- }
124
-
125
- /**
126
- * Create a DETACH message
127
- */
128
- function createDetachMessage(clientId) {
129
- return { type: MessageType.DETACH, clientId };
130
- }
131
-
132
- /**
133
- * Create a RESIZE message
134
- */
135
- function createResizeMessage(cols, rows) {
136
- return { type: MessageType.RESIZE, cols, rows };
137
- }
138
-
139
- /**
140
- * Create a SIGNAL message
141
- */
142
- function createSignalMessage(signal) {
143
- if (!['SIGINT', 'SIGTERM', 'SIGKILL', 'SIGTSTP'].includes(signal)) {
144
- throw new Error(`Invalid signal: ${signal}`);
145
- }
146
- return { type: MessageType.SIGNAL, signal };
147
- }
148
-
149
- /**
150
- * Create a STDIN message (for future interactive mode)
151
- */
152
- function createStdinMessage(data) {
153
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
154
- return { type: MessageType.STDIN, data: buf.toString('base64') };
155
- }
156
-
157
- /**
158
- * Create an OUTPUT message
159
- */
160
- function createOutputMessage(data, timestamp = Date.now()) {
161
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
162
- return { type: MessageType.OUTPUT, data: buf.toString('base64'), timestamp };
163
- }
164
-
165
- /**
166
- * Create a HISTORY message
167
- */
168
- function createHistoryMessage(data) {
169
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
170
- return { type: MessageType.HISTORY, data: buf.toString('base64') };
171
- }
172
-
173
- /**
174
- * Create a STATE message
175
- */
176
- function createStateMessage(state) {
177
- return { type: MessageType.STATE, ...state };
178
- }
179
-
180
- /**
181
- * Create an EXIT message
182
- */
183
- function createExitMessage(code, signal) {
184
- return { type: MessageType.EXIT, code, signal };
185
- }
186
-
187
- /**
188
- * Create an ERROR message
189
- */
190
- function createErrorMessage(message) {
191
- return { type: MessageType.ERROR, message };
192
- }
193
-
194
- /**
195
- * Decode base64 data field from OUTPUT/HISTORY/STDIN messages
196
- */
197
- function decodeData(message) {
198
- if (message.data) {
199
- return Buffer.from(message.data, 'base64');
200
- }
201
- return null;
202
- }
203
-
204
- module.exports = {
205
- encode,
206
- MessageDecoder,
207
- MessageType,
208
- createAttachMessage,
209
- createDetachMessage,
210
- createResizeMessage,
211
- createSignalMessage,
212
- createStdinMessage,
213
- createOutputMessage,
214
- createHistoryMessage,
215
- createStateMessage,
216
- createExitMessage,
217
- createErrorMessage,
218
- decodeData,
219
- MAX_MESSAGE_SIZE,
220
- };
@@ -1,121 +0,0 @@
1
- /**
2
- * RingBuffer - Fixed-size circular buffer for output history
3
- *
4
- * Used by AttachServer to store recent output for late-joining clients.
5
- * When buffer fills, oldest data is overwritten.
6
- *
7
- * Design:
8
- * - Fixed allocation (no dynamic resizing)
9
- * - O(1) write and read operations
10
- * - Thread-safe for single writer (Node.js is single-threaded)
11
- */
12
-
13
- class RingBuffer {
14
- /**
15
- * @param {number} maxSize - Maximum buffer size in bytes (default 1MB)
16
- */
17
- constructor(maxSize = 1024 * 1024) {
18
- if (maxSize <= 0) {
19
- throw new Error('RingBuffer maxSize must be positive');
20
- }
21
- this.buffer = Buffer.alloc(maxSize);
22
- this.maxSize = maxSize;
23
- this.writePos = 0; // Next write position
24
- this.size = 0; // Current data size (0 to maxSize)
25
- }
26
-
27
- /**
28
- * Write data to the buffer
29
- * @param {Buffer|string} data - Data to write
30
- */
31
- write(data) {
32
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
33
-
34
- if (buf.length === 0) return;
35
-
36
- // If data is larger than buffer, only keep the last maxSize bytes
37
- if (buf.length >= this.maxSize) {
38
- buf.copy(this.buffer, 0, buf.length - this.maxSize);
39
- this.writePos = 0;
40
- this.size = this.maxSize;
41
- return;
42
- }
43
-
44
- // Calculate how much wraps around
45
- const spaceToEnd = this.maxSize - this.writePos;
46
-
47
- if (buf.length <= spaceToEnd) {
48
- // No wrap needed
49
- buf.copy(this.buffer, this.writePos);
50
- this.writePos += buf.length;
51
- } else {
52
- // Wrap around
53
- buf.copy(this.buffer, this.writePos, 0, spaceToEnd);
54
- buf.copy(this.buffer, 0, spaceToEnd);
55
- this.writePos = buf.length - spaceToEnd;
56
- }
57
-
58
- // Update size (capped at maxSize)
59
- this.size = Math.min(this.size + buf.length, this.maxSize);
60
- }
61
-
62
- /**
63
- * Read all buffered data
64
- * @returns {Buffer} - All data currently in buffer
65
- */
66
- read() {
67
- if (this.size === 0) {
68
- return Buffer.alloc(0);
69
- }
70
-
71
- const result = Buffer.alloc(this.size);
72
-
73
- if (this.size < this.maxSize) {
74
- // Buffer not full yet, data starts at 0
75
- this.buffer.copy(result, 0, 0, this.size);
76
- } else {
77
- // Buffer is full, data starts at writePos (oldest data)
78
- const startPos = this.writePos;
79
- const firstChunkSize = this.maxSize - startPos;
80
-
81
- this.buffer.copy(result, 0, startPos, this.maxSize);
82
- this.buffer.copy(result, firstChunkSize, 0, startPos);
83
- }
84
-
85
- return result;
86
- }
87
-
88
- /**
89
- * Clear the buffer
90
- */
91
- clear() {
92
- this.writePos = 0;
93
- this.size = 0;
94
- }
95
-
96
- /**
97
- * Get current data size
98
- * @returns {number}
99
- */
100
- getSize() {
101
- return this.size;
102
- }
103
-
104
- /**
105
- * Check if buffer is empty
106
- * @returns {boolean}
107
- */
108
- isEmpty() {
109
- return this.size === 0;
110
- }
111
-
112
- /**
113
- * Check if buffer is full
114
- * @returns {boolean}
115
- */
116
- isFull() {
117
- return this.size === this.maxSize;
118
- }
119
- }
120
-
121
- module.exports = RingBuffer;
@@ -1,267 +0,0 @@
1
- /**
2
- * Socket Discovery - Utilities for socket path management
3
- *
4
- * Socket locations:
5
- * - Tasks: ~/.zeroshot/sockets/task-<id>.sock
6
- * - Clusters: ~/.zeroshot/sockets/cluster-<id>.sock (cluster-level, future)
7
- * - Agents: ~/.zeroshot/sockets/cluster-<id>/<agent-id>.sock
8
- */
9
-
10
- const path = require('path');
11
- const fs = require('fs');
12
- const os = require('os');
13
- const net = require('net');
14
-
15
- const ZEROSHOT_DIR = path.join(os.homedir(), '.zeroshot');
16
- const SOCKET_DIR = path.join(ZEROSHOT_DIR, 'sockets');
17
- const CLUSTERS_FILE = path.join(ZEROSHOT_DIR, 'clusters.json');
18
-
19
- /**
20
- * Check if an ID is a known cluster by looking up clusters.json
21
- * Cluster IDs don't have prefix (e.g., 'steady-pulse-42'), unlike task IDs ('task-xxx')
22
- * @param {string} id - ID to check
23
- * @returns {boolean} True if ID is a cluster
24
- */
25
- function isKnownCluster(id) {
26
- try {
27
- if (!fs.existsSync(CLUSTERS_FILE)) return false;
28
- const clusters = JSON.parse(fs.readFileSync(CLUSTERS_FILE, 'utf8'));
29
- return id in clusters;
30
- } catch {
31
- return false;
32
- }
33
- }
34
-
35
- /**
36
- * Ensure socket directory exists
37
- */
38
- function ensureSocketDir() {
39
- if (!fs.existsSync(SOCKET_DIR)) {
40
- fs.mkdirSync(SOCKET_DIR, { recursive: true });
41
- }
42
- }
43
-
44
- /**
45
- * Get socket path for a task
46
- * @param {string} taskId - Task ID (e.g., 'task-swift-falcon')
47
- * @returns {string} - Socket path
48
- */
49
- function getTaskSocketPath(taskId) {
50
- ensureSocketDir();
51
- return path.join(SOCKET_DIR, `${taskId}.sock`);
52
- }
53
-
54
- /**
55
- * Get socket path for a cluster agent
56
- * @param {string} clusterId - Cluster ID (e.g., 'cluster-bold-eagle')
57
- * @param {string} agentId - Agent ID (e.g., 'worker')
58
- * @returns {string} - Socket path
59
- */
60
- function getAgentSocketPath(clusterId, agentId) {
61
- const clusterDir = path.join(SOCKET_DIR, clusterId);
62
- if (!fs.existsSync(clusterDir)) {
63
- fs.mkdirSync(clusterDir, { recursive: true });
64
- }
65
- return path.join(clusterDir, `${agentId}.sock`);
66
- }
67
-
68
- /**
69
- * Get socket path for any ID (auto-detects task vs cluster)
70
- * @param {string} id - Task or cluster ID
71
- * @param {string} [agentId] - Optional agent ID for clusters
72
- * @returns {string} - Socket path
73
- */
74
- function getSocketPath(id, agentId = null) {
75
- if (id.startsWith('task-')) {
76
- return getTaskSocketPath(id);
77
- }
78
- // Check for explicit 'cluster-' prefix OR known cluster in registry
79
- // Cluster IDs are generated without prefix (e.g., 'steady-pulse-42')
80
- if (id.startsWith('cluster-') || isKnownCluster(id)) {
81
- if (agentId) {
82
- return getAgentSocketPath(id, agentId);
83
- }
84
- // Cluster-level socket (future use)
85
- ensureSocketDir();
86
- return path.join(SOCKET_DIR, `${id}.sock`);
87
- }
88
- // Unknown format, treat as task
89
- return getTaskSocketPath(id);
90
- }
91
-
92
- /**
93
- * Check if a socket exists and is connectable
94
- * @param {string} socketPath - Path to socket file
95
- * @returns {Promise<boolean>} - True if socket is live
96
- */
97
- function isSocketAlive(socketPath) {
98
- if (!fs.existsSync(socketPath)) {
99
- return Promise.resolve(false);
100
- }
101
-
102
- return new Promise((resolve) => {
103
- const socket = net.createConnection(socketPath);
104
- const timeout = setTimeout(() => {
105
- socket.destroy();
106
- resolve(false);
107
- }, 1000);
108
-
109
- socket.on('connect', () => {
110
- clearTimeout(timeout);
111
- socket.end();
112
- resolve(true);
113
- });
114
-
115
- socket.on('error', () => {
116
- clearTimeout(timeout);
117
- resolve(false);
118
- });
119
- });
120
- }
121
-
122
- /**
123
- * Remove stale socket file if not connectable
124
- * @param {string} socketPath - Path to socket file
125
- * @returns {Promise<boolean>} - True if socket was removed (stale)
126
- */
127
- async function cleanupStaleSocket(socketPath) {
128
- if (!fs.existsSync(socketPath)) {
129
- return false;
130
- }
131
-
132
- const alive = await isSocketAlive(socketPath);
133
- if (!alive) {
134
- try {
135
- fs.unlinkSync(socketPath);
136
- return true;
137
- } catch {
138
- // Ignore errors (file may have been removed already)
139
- }
140
- }
141
- return false;
142
- }
143
-
144
- /**
145
- * List all attachable tasks
146
- * Task sockets are .sock files directly in SOCKET_DIR (not in subdirectories)
147
- * Excludes cluster-level sockets (cluster-xxx.sock) since those aren't tasks
148
- * @returns {Promise<string[]>} - Array of task IDs with live sockets
149
- */
150
- async function listAttachableTasks() {
151
- ensureSocketDir();
152
- const entries = fs.readdirSync(SOCKET_DIR, { withFileTypes: true });
153
- const tasks = [];
154
-
155
- for (const entry of entries) {
156
- // Check socket files (Unix sockets report isSocket(), not isFile())
157
- // Also accept regular files for compatibility
158
- const isSocketFile = (entry.isSocket() || entry.isFile()) && entry.name.endsWith('.sock');
159
- if (isSocketFile && !entry.isDirectory()) {
160
- const id = entry.name.slice(0, -5); // Remove .sock
161
-
162
- // Skip cluster-level sockets (cluster-xxx.sock)
163
- if (id.startsWith('cluster-')) {
164
- continue;
165
- }
166
-
167
- const socketPath = path.join(SOCKET_DIR, entry.name);
168
- if (await isSocketAlive(socketPath)) {
169
- tasks.push(id);
170
- }
171
- }
172
- }
173
-
174
- return tasks;
175
- }
176
-
177
- /**
178
- * List all attachable agents for a cluster
179
- * @param {string} clusterId - Cluster ID
180
- * @returns {Promise<string[]>} - Array of agent IDs with live sockets
181
- */
182
- async function listAttachableAgents(clusterId) {
183
- const clusterDir = path.join(SOCKET_DIR, clusterId);
184
- if (!fs.existsSync(clusterDir)) {
185
- return [];
186
- }
187
-
188
- const files = fs.readdirSync(clusterDir);
189
- const agents = [];
190
-
191
- for (const file of files) {
192
- if (file.endsWith('.sock')) {
193
- const agentId = file.slice(0, -5); // Remove .sock
194
- const socketPath = path.join(clusterDir, file);
195
- if (await isSocketAlive(socketPath)) {
196
- agents.push(agentId);
197
- }
198
- }
199
- }
200
-
201
- return agents;
202
- }
203
-
204
- /**
205
- * List all attachable clusters
206
- * Checks both socket directories AND clusters.json for known clusters
207
- * @returns {Promise<string[]>} - Array of cluster IDs with at least one live agent socket
208
- */
209
- async function listAttachableClusters() {
210
- ensureSocketDir();
211
- const entries = fs.readdirSync(SOCKET_DIR, { withFileTypes: true });
212
- const clusters = [];
213
-
214
- // Check socket directories (both 'cluster-' prefix and plain cluster IDs)
215
- for (const entry of entries) {
216
- if (entry.isDirectory()) {
217
- // Accept directories with 'cluster-' prefix OR that are known clusters
218
- if (entry.name.startsWith('cluster-') || isKnownCluster(entry.name)) {
219
- const agents = await listAttachableAgents(entry.name);
220
- if (agents.length > 0) {
221
- clusters.push(entry.name);
222
- }
223
- }
224
- }
225
- }
226
-
227
- return clusters;
228
- }
229
-
230
- /**
231
- * Cleanup all sockets for a cluster (on cluster stop)
232
- * @param {string} clusterId - Cluster ID
233
- */
234
- function cleanupClusterSockets(clusterId) {
235
- const clusterDir = path.join(SOCKET_DIR, clusterId);
236
- if (fs.existsSync(clusterDir)) {
237
- const files = fs.readdirSync(clusterDir);
238
- for (const file of files) {
239
- try {
240
- fs.unlinkSync(path.join(clusterDir, file));
241
- } catch {
242
- // Ignore
243
- }
244
- }
245
- try {
246
- fs.rmdirSync(clusterDir);
247
- } catch {
248
- // Ignore
249
- }
250
- }
251
- }
252
-
253
- module.exports = {
254
- ZEROSHOT_DIR,
255
- SOCKET_DIR,
256
- ensureSocketDir,
257
- getTaskSocketPath,
258
- getAgentSocketPath,
259
- getSocketPath,
260
- isSocketAlive,
261
- cleanupStaleSocket,
262
- listAttachableTasks,
263
- listAttachableAgents,
264
- listAttachableClusters,
265
- cleanupClusterSockets,
266
- isKnownCluster,
267
- };