@covibes/zeroshot 5.4.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 (136) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -465
  3. package/cli/index.js +101 -5231
  4. package/package.json +16 -130
  5. package/CHANGELOG.md +0 -537
  6. package/cli/commands/providers.js +0 -149
  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 -275
  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 -581
  14. package/cluster-templates/base-templates/single-worker.json +0 -63
  15. package/cluster-templates/base-templates/worker-validator.json +0 -208
  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 -220
  21. package/lib/git-remote-utils.js +0 -165
  22. package/lib/id-detector.js +0 -56
  23. package/lib/provider-defaults.js +0 -62
  24. package/lib/provider-detection.js +0 -59
  25. package/lib/provider-names.js +0 -57
  26. package/lib/settings/claude-auth.js +0 -78
  27. package/lib/settings.js +0 -503
  28. package/lib/stream-json-parser.js +0 -10
  29. package/scripts/fix-node-pty-permissions.js +0 -75
  30. package/scripts/record-demo.sh +0 -279
  31. package/scripts/setup-merge-queue.sh +0 -170
  32. package/scripts/test-install.sh +0 -40
  33. package/scripts/validate-templates.js +0 -100
  34. package/src/agent/agent-config.js +0 -246
  35. package/src/agent/agent-context-builder.js +0 -444
  36. package/src/agent/agent-hook-executor.js +0 -599
  37. package/src/agent/agent-lifecycle.js +0 -743
  38. package/src/agent/agent-stuck-detector.js +0 -256
  39. package/src/agent/agent-task-executor.js +0 -1690
  40. package/src/agent/agent-trigger-evaluator.js +0 -67
  41. package/src/agent/output-extraction.js +0 -217
  42. package/src/agent/output-reformatter.js +0 -175
  43. package/src/agent/schema-utils.js +0 -146
  44. package/src/agent-wrapper.js +0 -579
  45. package/src/agents/git-pusher-template.js +0 -285
  46. package/src/attach/attach-client.js +0 -438
  47. package/src/attach/attach-server.js +0 -543
  48. package/src/attach/index.js +0 -35
  49. package/src/attach/protocol.js +0 -220
  50. package/src/attach/ring-buffer.js +0 -121
  51. package/src/attach/socket-discovery.js +0 -267
  52. package/src/claude-task-runner.js +0 -574
  53. package/src/config-router.js +0 -80
  54. package/src/config-validator.js +0 -1925
  55. package/src/copy-worker.js +0 -43
  56. package/src/input-helpers.js +0 -65
  57. package/src/isolation-manager.js +0 -1471
  58. package/src/issue-providers/README.md +0 -305
  59. package/src/issue-providers/azure-devops-provider.js +0 -273
  60. package/src/issue-providers/base-provider.js +0 -232
  61. package/src/issue-providers/github-provider.js +0 -179
  62. package/src/issue-providers/gitlab-provider.js +0 -241
  63. package/src/issue-providers/index.js +0 -196
  64. package/src/issue-providers/jira-provider.js +0 -239
  65. package/src/ledger.js +0 -631
  66. package/src/lib/safe-exec.js +0 -88
  67. package/src/logic-engine.js +0 -223
  68. package/src/message-bus-bridge.js +0 -139
  69. package/src/message-bus.js +0 -249
  70. package/src/name-generator.js +0 -232
  71. package/src/orchestrator.js +0 -2815
  72. package/src/preflight.js +0 -639
  73. package/src/process-metrics.js +0 -608
  74. package/src/providers/anthropic/cli-builder.js +0 -73
  75. package/src/providers/anthropic/index.js +0 -204
  76. package/src/providers/anthropic/models.js +0 -23
  77. package/src/providers/anthropic/output-parser.js +0 -177
  78. package/src/providers/base-provider.js +0 -251
  79. package/src/providers/capabilities.js +0 -60
  80. package/src/providers/google/cli-builder.js +0 -55
  81. package/src/providers/google/index.js +0 -116
  82. package/src/providers/google/models.js +0 -24
  83. package/src/providers/google/output-parser.js +0 -101
  84. package/src/providers/index.js +0 -91
  85. package/src/providers/openai/cli-builder.js +0 -133
  86. package/src/providers/openai/index.js +0 -136
  87. package/src/providers/openai/models.js +0 -21
  88. package/src/providers/openai/output-parser.js +0 -143
  89. package/src/providers/opencode/cli-builder.js +0 -42
  90. package/src/providers/opencode/index.js +0 -103
  91. package/src/providers/opencode/models.js +0 -55
  92. package/src/providers/opencode/output-parser.js +0 -122
  93. package/src/schemas/sub-cluster.js +0 -185
  94. package/src/status-footer.js +0 -1026
  95. package/src/sub-cluster-wrapper.js +0 -601
  96. package/src/task-runner.js +0 -30
  97. package/src/template-resolver.js +0 -372
  98. package/src/tui/CHANGES.txt +0 -133
  99. package/src/tui/LAYOUT.md +0 -261
  100. package/src/tui/README.txt +0 -192
  101. package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
  102. package/src/tui/data-poller.js +0 -349
  103. package/src/tui/demo.js +0 -208
  104. package/src/tui/formatters.js +0 -129
  105. package/src/tui/index.js +0 -193
  106. package/src/tui/keybindings.js +0 -324
  107. package/src/tui/layout.js +0 -334
  108. package/src/tui/renderer.js +0 -184
  109. package/task-lib/attachable-watcher.js +0 -309
  110. package/task-lib/claude-recovery.js +0 -156
  111. package/task-lib/commands/clean.js +0 -50
  112. package/task-lib/commands/episodes.js +0 -105
  113. package/task-lib/commands/get-log-path.js +0 -23
  114. package/task-lib/commands/kill.js +0 -32
  115. package/task-lib/commands/list.js +0 -105
  116. package/task-lib/commands/logs.js +0 -453
  117. package/task-lib/commands/resume.js +0 -42
  118. package/task-lib/commands/run.js +0 -57
  119. package/task-lib/commands/schedule.js +0 -105
  120. package/task-lib/commands/scheduler-cmd.js +0 -96
  121. package/task-lib/commands/schedules.js +0 -148
  122. package/task-lib/commands/status.js +0 -44
  123. package/task-lib/commands/unschedule.js +0 -16
  124. package/task-lib/completion.js +0 -9
  125. package/task-lib/config.js +0 -8
  126. package/task-lib/name-generator.js +0 -230
  127. package/task-lib/package.json +0 -3
  128. package/task-lib/runner.js +0 -201
  129. package/task-lib/scheduler.js +0 -252
  130. package/task-lib/store.js +0 -529
  131. package/task-lib/tui/formatters.js +0 -215
  132. package/task-lib/tui/index.js +0 -197
  133. package/task-lib/tui/layout.js +0 -111
  134. package/task-lib/tui/renderer.js +0 -129
  135. package/task-lib/tui.js +0 -425
  136. package/task-lib/watcher.js +0 -226
@@ -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
- };