@covibes/zeroshot 1.0.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/CHANGELOG.md +167 -0
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/cli/index.js +3990 -0
- package/cluster-templates/base-templates/debug-workflow.json +181 -0
- package/cluster-templates/base-templates/full-workflow.json +455 -0
- package/cluster-templates/base-templates/single-worker.json +48 -0
- package/cluster-templates/base-templates/worker-validator.json +131 -0
- package/cluster-templates/conductor-bootstrap.json +122 -0
- package/cluster-templates/conductor-junior-bootstrap.json +69 -0
- package/docker/zeroshot-cluster/Dockerfile +132 -0
- package/lib/completion.js +174 -0
- package/lib/id-detector.js +53 -0
- package/lib/settings.js +97 -0
- package/lib/stream-json-parser.js +236 -0
- package/package.json +121 -0
- package/src/agent/agent-config.js +121 -0
- package/src/agent/agent-context-builder.js +241 -0
- package/src/agent/agent-hook-executor.js +329 -0
- package/src/agent/agent-lifecycle.js +555 -0
- package/src/agent/agent-stuck-detector.js +256 -0
- package/src/agent/agent-task-executor.js +1034 -0
- package/src/agent/agent-trigger-evaluator.js +67 -0
- package/src/agent-wrapper.js +459 -0
- package/src/agents/git-pusher-agent.json +20 -0
- package/src/attach/attach-client.js +438 -0
- package/src/attach/attach-server.js +543 -0
- package/src/attach/index.js +35 -0
- package/src/attach/protocol.js +220 -0
- package/src/attach/ring-buffer.js +121 -0
- package/src/attach/socket-discovery.js +242 -0
- package/src/claude-task-runner.js +468 -0
- package/src/config-router.js +80 -0
- package/src/config-validator.js +598 -0
- package/src/github.js +103 -0
- package/src/isolation-manager.js +1042 -0
- package/src/ledger.js +429 -0
- package/src/logic-engine.js +223 -0
- package/src/message-bus-bridge.js +139 -0
- package/src/message-bus.js +202 -0
- package/src/name-generator.js +232 -0
- package/src/orchestrator.js +1938 -0
- package/src/schemas/sub-cluster.js +156 -0
- package/src/sub-cluster-wrapper.js +545 -0
- package/src/task-runner.js +28 -0
- package/src/template-resolver.js +347 -0
- package/src/tui/CHANGES.txt +133 -0
- package/src/tui/LAYOUT.md +261 -0
- package/src/tui/README.txt +192 -0
- package/src/tui/TWO-LEVEL-NAVIGATION.md +186 -0
- package/src/tui/data-poller.js +325 -0
- package/src/tui/demo.js +208 -0
- package/src/tui/formatters.js +123 -0
- package/src/tui/index.js +193 -0
- package/src/tui/keybindings.js +383 -0
- package/src/tui/layout.js +317 -0
- package/src/tui/renderer.js +194 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AttachServer - PTY process manager with socket server for attach/detach
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle:
|
|
5
|
+
* 1. Created when task/agent spawns
|
|
6
|
+
* 2. Spawns command via node-pty
|
|
7
|
+
* 3. Listens on Unix socket for client connections
|
|
8
|
+
* 4. Buffers output in ring buffer (for late-joining clients)
|
|
9
|
+
* 5. Broadcasts output to all connected clients
|
|
10
|
+
* 6. Cleans up when process exits
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Multi-client support (multiple terminals can attach)
|
|
14
|
+
* - Output history replay on attach
|
|
15
|
+
* - Signal forwarding (SIGINT, SIGTERM)
|
|
16
|
+
* - Window resize support
|
|
17
|
+
* - Graceful cleanup on exit
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const net = require('net');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const EventEmitter = require('events');
|
|
24
|
+
|
|
25
|
+
const RingBuffer = require('./ring-buffer');
|
|
26
|
+
const protocol = require('./protocol');
|
|
27
|
+
const { cleanupStaleSocket } = require('./socket-discovery');
|
|
28
|
+
|
|
29
|
+
// FAIL FAST: Check for node-pty at module load time, not at spawn time
|
|
30
|
+
let pty;
|
|
31
|
+
try {
|
|
32
|
+
pty = require('node-pty');
|
|
33
|
+
} catch (e) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`AttachServer: node-pty not installed. Run: npm install node-pty\n` +
|
|
36
|
+
`Original error: ${e.message}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Default output buffer size: 1MB
|
|
41
|
+
const DEFAULT_BUFFER_SIZE = 1024 * 1024;
|
|
42
|
+
|
|
43
|
+
class AttachServer extends EventEmitter {
|
|
44
|
+
/**
|
|
45
|
+
* @param {object} options
|
|
46
|
+
* @param {string} options.id - Task or agent ID
|
|
47
|
+
* @param {string} options.socketPath - Unix socket path
|
|
48
|
+
* @param {string} options.command - Command to spawn
|
|
49
|
+
* @param {string[]} options.args - Command arguments
|
|
50
|
+
* @param {string} [options.cwd] - Working directory
|
|
51
|
+
* @param {object} [options.env] - Environment variables
|
|
52
|
+
* @param {number} [options.cols] - Terminal columns (default 120)
|
|
53
|
+
* @param {number} [options.rows] - Terminal rows (default 30)
|
|
54
|
+
* @param {number} [options.bufferSize] - Output buffer size (default 1MB)
|
|
55
|
+
*/
|
|
56
|
+
constructor(options) {
|
|
57
|
+
super();
|
|
58
|
+
|
|
59
|
+
if (!options.id) throw new Error('AttachServer: id is required');
|
|
60
|
+
if (!options.socketPath) throw new Error('AttachServer: socketPath is required');
|
|
61
|
+
if (!options.command) throw new Error('AttachServer: command is required');
|
|
62
|
+
|
|
63
|
+
this.id = options.id;
|
|
64
|
+
this.socketPath = options.socketPath;
|
|
65
|
+
this.command = options.command;
|
|
66
|
+
this.args = options.args || [];
|
|
67
|
+
this.cwd = options.cwd || process.cwd();
|
|
68
|
+
this.env = options.env || process.env;
|
|
69
|
+
this.cols = options.cols || 120;
|
|
70
|
+
this.rows = options.rows || 30;
|
|
71
|
+
|
|
72
|
+
this.outputBuffer = new RingBuffer(options.bufferSize || DEFAULT_BUFFER_SIZE);
|
|
73
|
+
this.clients = new Map(); // clientId -> { socket, decoder }
|
|
74
|
+
this.pty = null;
|
|
75
|
+
this.server = null;
|
|
76
|
+
this.state = 'stopped'; // stopped, starting, running, exiting, exited
|
|
77
|
+
this.exitCode = null;
|
|
78
|
+
this.exitSignal = null;
|
|
79
|
+
this.pid = null;
|
|
80
|
+
|
|
81
|
+
// Bind cleanup handlers
|
|
82
|
+
this._onProcessExit = this._onProcessExit.bind(this);
|
|
83
|
+
this._onServerError = this._onServerError.bind(this);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Start the PTY process and socket server
|
|
88
|
+
* @returns {Promise<void>}
|
|
89
|
+
*/
|
|
90
|
+
async start() {
|
|
91
|
+
if (this.state !== 'stopped') {
|
|
92
|
+
throw new Error(`AttachServer: Cannot start from state '${this.state}'`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.state = 'starting';
|
|
96
|
+
|
|
97
|
+
// Ensure socket directory exists
|
|
98
|
+
const socketDir = path.dirname(this.socketPath);
|
|
99
|
+
if (!fs.existsSync(socketDir)) {
|
|
100
|
+
fs.mkdirSync(socketDir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Clean up stale socket if exists
|
|
104
|
+
await cleanupStaleSocket(this.socketPath);
|
|
105
|
+
|
|
106
|
+
// Check if socket is still in use (another process)
|
|
107
|
+
if (fs.existsSync(this.socketPath)) {
|
|
108
|
+
throw new Error(`AttachServer: Socket already in use: ${this.socketPath}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Start socket server FIRST (so clients can connect immediately)
|
|
112
|
+
await this._startServer();
|
|
113
|
+
|
|
114
|
+
// Spawn PTY process
|
|
115
|
+
await this._spawnPty();
|
|
116
|
+
|
|
117
|
+
this.state = 'running';
|
|
118
|
+
this.emit('start', { id: this.id, pid: this.pid });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Stop the server and kill the PTY process
|
|
123
|
+
* @param {string} [signal='SIGTERM'] - Signal to send
|
|
124
|
+
* @returns {Promise<void>}
|
|
125
|
+
*/
|
|
126
|
+
async stop(signal = 'SIGTERM') {
|
|
127
|
+
if (this.state !== 'running') {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.state = 'exiting';
|
|
132
|
+
|
|
133
|
+
// Kill PTY process
|
|
134
|
+
if (this.pty) {
|
|
135
|
+
try {
|
|
136
|
+
this.pty.kill(signal);
|
|
137
|
+
} catch {
|
|
138
|
+
// Process may already be dead
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Wait for process to exit (with timeout)
|
|
143
|
+
await new Promise((resolve) => {
|
|
144
|
+
const timeout = setTimeout(() => {
|
|
145
|
+
// Force kill if still running
|
|
146
|
+
if (this.pty) {
|
|
147
|
+
try {
|
|
148
|
+
this.pty.kill('SIGKILL');
|
|
149
|
+
} catch {
|
|
150
|
+
// Ignore
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
resolve();
|
|
154
|
+
}, 5000);
|
|
155
|
+
|
|
156
|
+
const checkExit = () => {
|
|
157
|
+
if (this.state === 'exited') {
|
|
158
|
+
clearTimeout(timeout);
|
|
159
|
+
resolve();
|
|
160
|
+
} else {
|
|
161
|
+
setTimeout(checkExit, 100);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
checkExit();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await this._cleanup();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Send a signal to the PTY process
|
|
172
|
+
* @param {string} signal - Signal name (SIGINT, SIGTERM, etc.)
|
|
173
|
+
*/
|
|
174
|
+
sendSignal(signal) {
|
|
175
|
+
if (!this.pty || this.state !== 'running') {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
this.pty.kill(signal);
|
|
181
|
+
return true;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resize the PTY
|
|
189
|
+
* @param {number} cols - Columns
|
|
190
|
+
* @param {number} rows - Rows
|
|
191
|
+
*/
|
|
192
|
+
resize(cols, rows) {
|
|
193
|
+
if (!this.pty || this.state !== 'running') {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.cols = cols;
|
|
198
|
+
this.rows = rows;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
this.pty.resize(cols, rows);
|
|
202
|
+
} catch {
|
|
203
|
+
// Ignore resize errors
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Write to PTY stdin (for future interactive mode)
|
|
209
|
+
* @param {Buffer|string} data - Data to write
|
|
210
|
+
*/
|
|
211
|
+
write(data) {
|
|
212
|
+
if (!this.pty || this.state !== 'running') {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
this.pty.write(data);
|
|
218
|
+
return true;
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get current server state
|
|
226
|
+
* @returns {object}
|
|
227
|
+
*/
|
|
228
|
+
getState() {
|
|
229
|
+
return {
|
|
230
|
+
id: this.id,
|
|
231
|
+
state: this.state,
|
|
232
|
+
pid: this.pid,
|
|
233
|
+
exitCode: this.exitCode,
|
|
234
|
+
exitSignal: this.exitSignal,
|
|
235
|
+
clientCount: this.clients.size,
|
|
236
|
+
bufferSize: this.outputBuffer.getSize(),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─────────────────────────────────────────────────────────────────
|
|
241
|
+
// Private methods
|
|
242
|
+
// ─────────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Start the Unix socket server
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
_startServer() {
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
this.server = net.createServer((socket) => {
|
|
251
|
+
this._handleClientConnection(socket);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
this.server.on('error', this._onServerError);
|
|
255
|
+
|
|
256
|
+
this.server.listen(this.socketPath, () => {
|
|
257
|
+
// Set socket permissions (owner read/write only)
|
|
258
|
+
try {
|
|
259
|
+
fs.chmodSync(this.socketPath, 0o600);
|
|
260
|
+
} catch {
|
|
261
|
+
// Ignore permission errors
|
|
262
|
+
}
|
|
263
|
+
resolve();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this.server.on('error', (err) => {
|
|
267
|
+
if (this.state === 'starting') {
|
|
268
|
+
reject(err);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Spawn the PTY process
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
_spawnPty() {
|
|
279
|
+
this.pty = pty.spawn(this.command, this.args, {
|
|
280
|
+
name: 'xterm-256color',
|
|
281
|
+
cols: this.cols,
|
|
282
|
+
rows: this.rows,
|
|
283
|
+
cwd: this.cwd,
|
|
284
|
+
env: this.env,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
this.pid = this.pty.pid;
|
|
288
|
+
|
|
289
|
+
// Handle PTY output
|
|
290
|
+
this.pty.onData((data) => {
|
|
291
|
+
this._handlePtyOutput(data);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Handle PTY exit
|
|
295
|
+
this.pty.onExit(({ exitCode, signal }) => {
|
|
296
|
+
this._onProcessExit(exitCode, signal);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Handle PTY output
|
|
302
|
+
* @private
|
|
303
|
+
*/
|
|
304
|
+
_handlePtyOutput(data) {
|
|
305
|
+
// Buffer output for late-joining clients
|
|
306
|
+
this.outputBuffer.write(data);
|
|
307
|
+
|
|
308
|
+
// Broadcast to all connected clients
|
|
309
|
+
const message = protocol.encode(protocol.createOutputMessage(data));
|
|
310
|
+
|
|
311
|
+
for (const [clientId, client] of this.clients) {
|
|
312
|
+
try {
|
|
313
|
+
client.socket.write(message);
|
|
314
|
+
} catch {
|
|
315
|
+
// Client disconnected, will be cleaned up
|
|
316
|
+
this._removeClient(clientId);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Emit for local listeners (e.g., logging)
|
|
321
|
+
this.emit('output', data);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Handle new client connection
|
|
326
|
+
* @private
|
|
327
|
+
*/
|
|
328
|
+
_handleClientConnection(socket) {
|
|
329
|
+
const decoder = new protocol.MessageDecoder();
|
|
330
|
+
let clientId = null;
|
|
331
|
+
|
|
332
|
+
socket.on('data', (data) => {
|
|
333
|
+
try {
|
|
334
|
+
const messages = decoder.feed(data);
|
|
335
|
+
for (const msg of messages) {
|
|
336
|
+
this._handleClientMessage(socket, msg, (id) => {
|
|
337
|
+
clientId = id;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
} catch (e) {
|
|
341
|
+
// Protocol error, close connection
|
|
342
|
+
this._sendError(socket, `Protocol error: ${e.message}`);
|
|
343
|
+
socket.end();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
socket.on('close', () => {
|
|
348
|
+
if (clientId) {
|
|
349
|
+
this._removeClient(clientId);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
socket.on('error', () => {
|
|
354
|
+
if (clientId) {
|
|
355
|
+
this._removeClient(clientId);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Handle message from client
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
_handleClientMessage(socket, message, setClientId) {
|
|
365
|
+
switch (message.type) {
|
|
366
|
+
case protocol.MessageType.ATTACH: {
|
|
367
|
+
const { clientId, cols, rows } = message;
|
|
368
|
+
if (!clientId) {
|
|
369
|
+
this._sendError(socket, 'ATTACH requires clientId');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Register client
|
|
374
|
+
this.clients.set(clientId, {
|
|
375
|
+
socket,
|
|
376
|
+
decoder: new protocol.MessageDecoder(),
|
|
377
|
+
});
|
|
378
|
+
setClientId(clientId);
|
|
379
|
+
|
|
380
|
+
// Send history (buffered output)
|
|
381
|
+
const history = this.outputBuffer.read();
|
|
382
|
+
if (history.length > 0) {
|
|
383
|
+
socket.write(protocol.encode(protocol.createHistoryMessage(history)));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Send current state
|
|
387
|
+
socket.write(protocol.encode(protocol.createStateMessage(this.getState())));
|
|
388
|
+
|
|
389
|
+
// Resize PTY to client dimensions (if provided)
|
|
390
|
+
if (cols && rows) {
|
|
391
|
+
this.resize(cols, rows);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this.emit('clientAttach', { clientId });
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
case protocol.MessageType.DETACH: {
|
|
399
|
+
const { clientId } = message;
|
|
400
|
+
this._removeClient(clientId);
|
|
401
|
+
socket.end();
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
case protocol.MessageType.RESIZE: {
|
|
406
|
+
const { cols, rows } = message;
|
|
407
|
+
if (cols && rows) {
|
|
408
|
+
this.resize(cols, rows);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
case protocol.MessageType.SIGNAL: {
|
|
414
|
+
const { signal } = message;
|
|
415
|
+
this.sendSignal(signal);
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
case protocol.MessageType.STDIN: {
|
|
420
|
+
// Future interactive mode
|
|
421
|
+
const data = protocol.decodeData(message);
|
|
422
|
+
if (data) {
|
|
423
|
+
this.write(data);
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
default:
|
|
429
|
+
this._sendError(socket, `Unknown message type: ${message.type}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Remove a client
|
|
435
|
+
* @private
|
|
436
|
+
*/
|
|
437
|
+
_removeClient(clientId) {
|
|
438
|
+
const client = this.clients.get(clientId);
|
|
439
|
+
if (client) {
|
|
440
|
+
this.clients.delete(clientId);
|
|
441
|
+
this.emit('clientDetach', { clientId });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Send error message to client
|
|
447
|
+
* @private
|
|
448
|
+
*/
|
|
449
|
+
_sendError(socket, message) {
|
|
450
|
+
try {
|
|
451
|
+
socket.write(protocol.encode(protocol.createErrorMessage(message)));
|
|
452
|
+
} catch {
|
|
453
|
+
// Client disconnected
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Handle PTY process exit
|
|
459
|
+
* @private
|
|
460
|
+
*/
|
|
461
|
+
_onProcessExit(exitCode, signal) {
|
|
462
|
+
this.exitCode = exitCode;
|
|
463
|
+
this.exitSignal = signal;
|
|
464
|
+
this.state = 'exited';
|
|
465
|
+
|
|
466
|
+
// Notify all clients
|
|
467
|
+
const exitMessage = protocol.encode(protocol.createExitMessage(exitCode, signal));
|
|
468
|
+
|
|
469
|
+
for (const [, client] of this.clients) {
|
|
470
|
+
try {
|
|
471
|
+
client.socket.write(exitMessage);
|
|
472
|
+
client.socket.end();
|
|
473
|
+
} catch {
|
|
474
|
+
// Client already disconnected
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
this.emit('exit', { exitCode, signal });
|
|
479
|
+
|
|
480
|
+
// Clean up after short delay (allow clients to receive exit message)
|
|
481
|
+
setTimeout(() => {
|
|
482
|
+
this._cleanup();
|
|
483
|
+
}, 500);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Handle server error
|
|
488
|
+
* @private
|
|
489
|
+
*/
|
|
490
|
+
_onServerError(err) {
|
|
491
|
+
this.emit('error', err);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Clean up resources
|
|
496
|
+
* @private
|
|
497
|
+
*/
|
|
498
|
+
async _cleanup() {
|
|
499
|
+
// Close all client connections
|
|
500
|
+
for (const [, client] of this.clients) {
|
|
501
|
+
try {
|
|
502
|
+
client.socket.destroy();
|
|
503
|
+
} catch {
|
|
504
|
+
// Ignore
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
this.clients.clear();
|
|
508
|
+
|
|
509
|
+
// Close server
|
|
510
|
+
if (this.server) {
|
|
511
|
+
await new Promise((resolve) => {
|
|
512
|
+
this.server.close(() => resolve());
|
|
513
|
+
});
|
|
514
|
+
this.server = null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Remove socket file
|
|
518
|
+
if (fs.existsSync(this.socketPath)) {
|
|
519
|
+
try {
|
|
520
|
+
fs.unlinkSync(this.socketPath);
|
|
521
|
+
} catch {
|
|
522
|
+
// Ignore
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Clean up empty parent directory for cluster sockets
|
|
527
|
+
const socketDir = path.dirname(this.socketPath);
|
|
528
|
+
if (socketDir.includes('cluster-')) {
|
|
529
|
+
try {
|
|
530
|
+
const files = fs.readdirSync(socketDir);
|
|
531
|
+
if (files.length === 0) {
|
|
532
|
+
fs.rmdirSync(socketDir);
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
// Ignore
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
this.emit('cleanup');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
module.exports = AttachServer;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach/Detach System - tmux-style session management for zeroshot tasks and clusters
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const { AttachServer, AttachClient, socketDiscovery } = require('./attach');
|
|
6
|
+
*
|
|
7
|
+
* // Server side (in watcher.js or agent-wrapper.js)
|
|
8
|
+
* const server = new AttachServer({
|
|
9
|
+
* id: 'task-xxx',
|
|
10
|
+
* socketPath: socketDiscovery.getTaskSocketPath('task-xxx'),
|
|
11
|
+
* command: 'claude',
|
|
12
|
+
* args: ['--print', '--output-format', 'stream-json', prompt],
|
|
13
|
+
* });
|
|
14
|
+
* await server.start();
|
|
15
|
+
*
|
|
16
|
+
* // Client side (in CLI attach command)
|
|
17
|
+
* const client = new AttachClient({
|
|
18
|
+
* socketPath: socketDiscovery.getTaskSocketPath('task-xxx'),
|
|
19
|
+
* });
|
|
20
|
+
* await client.connect();
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const AttachServer = require('./attach-server');
|
|
24
|
+
const AttachClient = require('./attach-client');
|
|
25
|
+
const RingBuffer = require('./ring-buffer');
|
|
26
|
+
const protocol = require('./protocol');
|
|
27
|
+
const socketDiscovery = require('./socket-discovery');
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
AttachServer,
|
|
31
|
+
AttachClient,
|
|
32
|
+
RingBuffer,
|
|
33
|
+
protocol,
|
|
34
|
+
socketDiscovery,
|
|
35
|
+
};
|