@kernel.chat/kbot 2.23.2 → 2.24.0
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/dist/pair.d.ts +81 -0
- package/dist/pair.d.ts.map +1 -0
- package/dist/pair.js +993 -0
- package/dist/pair.js.map +1 -0
- package/dist/plugin-sdk.d.ts +136 -0
- package/dist/plugin-sdk.d.ts.map +1 -0
- package/dist/plugin-sdk.js +946 -0
- package/dist/plugin-sdk.js.map +1 -0
- package/dist/record.d.ts +174 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +1182 -0
- package/dist/record.js.map +1 -0
- package/dist/team.d.ts +106 -0
- package/dist/team.d.ts.map +1 -0
- package/dist/team.js +917 -0
- package/dist/team.js.map +1 -0
- package/dist/tools/database.d.ts +2 -0
- package/dist/tools/database.d.ts.map +1 -0
- package/dist/tools/database.js +751 -0
- package/dist/tools/database.js.map +1 -0
- package/dist/tools/deploy.d.ts +2 -0
- package/dist/tools/deploy.d.ts.map +1 -0
- package/dist/tools/deploy.js +824 -0
- package/dist/tools/deploy.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +11 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp-marketplace.d.ts +2 -0
- package/dist/tools/mcp-marketplace.d.ts.map +1 -0
- package/dist/tools/mcp-marketplace.js +759 -0
- package/dist/tools/mcp-marketplace.js.map +1 -0
- package/package.json +25 -3
package/dist/team.js
ADDED
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
// K:BOT Team Mode — Coordinated multi-instance collaboration over local TCP
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// kbot team start # Start server + join as coordinator
|
|
5
|
+
// kbot team start --port 8000 # Custom port
|
|
6
|
+
// kbot team join --role coder # Join as coder
|
|
7
|
+
// kbot team join --role researcher # Join as researcher
|
|
8
|
+
// kbot team status # Show connected instances
|
|
9
|
+
//
|
|
10
|
+
// Architecture:
|
|
11
|
+
// - Local TCP server on localhost:7439 (configurable)
|
|
12
|
+
// - Newline-delimited JSON protocol (NDJSON)
|
|
13
|
+
// - Shared context store (any instance writes, all read)
|
|
14
|
+
// - Role-based task routing
|
|
15
|
+
//
|
|
16
|
+
// Workflow:
|
|
17
|
+
// Terminal 1: kbot team start → coordinator
|
|
18
|
+
// Terminal 2: kbot team join --role researcher
|
|
19
|
+
// Terminal 3: kbot team join --role coder
|
|
20
|
+
// Coordinator assigns "research RSC" → researcher gets the task
|
|
21
|
+
// Researcher shares findings → all instances receive context
|
|
22
|
+
// Coordinator assigns "implement RSC" → coder gets task + research context
|
|
23
|
+
import * as net from 'node:net';
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
25
|
+
import { registerTool } from './tools/index.js';
|
|
26
|
+
import { printInfo, printSuccess, printError, printWarn } from './ui.js';
|
|
27
|
+
// ── Constants ────────────────────────────────────────────────────────
|
|
28
|
+
const DEFAULT_PORT = 7439;
|
|
29
|
+
const HEARTBEAT_INTERVAL = 10_000;
|
|
30
|
+
const RECONNECT_DELAY = 2_000;
|
|
31
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
32
|
+
// ── Server State ─────────────────────────────────────────────────────
|
|
33
|
+
let _server = null;
|
|
34
|
+
let _serverPort = DEFAULT_PORT;
|
|
35
|
+
const _instances = new Map();
|
|
36
|
+
const _sharedContext = new Map();
|
|
37
|
+
const _pendingTasks = new Map();
|
|
38
|
+
// ── Client State ─────────────────────────────────────────────────────
|
|
39
|
+
let _client = null;
|
|
40
|
+
let _reconnectAttempts = 0;
|
|
41
|
+
let _onTask = null;
|
|
42
|
+
let _onContext = null;
|
|
43
|
+
let _onBroadcast = null;
|
|
44
|
+
let _onStatus = null;
|
|
45
|
+
// ── Protocol ─────────────────────────────────────────────────────────
|
|
46
|
+
/** Encode a message as NDJSON (newline-delimited JSON) */
|
|
47
|
+
function encode(msg) {
|
|
48
|
+
return JSON.stringify(msg) + '\n';
|
|
49
|
+
}
|
|
50
|
+
/** Parse incoming data buffer into messages, handling partial lines */
|
|
51
|
+
function createMessageParser() {
|
|
52
|
+
let buffer = '';
|
|
53
|
+
return (chunk) => {
|
|
54
|
+
buffer += chunk.toString('utf-8');
|
|
55
|
+
const messages = [];
|
|
56
|
+
let newlineIdx;
|
|
57
|
+
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
58
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
59
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
60
|
+
if (!line)
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
messages.push(JSON.parse(line));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Malformed line — skip
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return messages;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// ── Team Server ──────────────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Start the team coordination server.
|
|
75
|
+
* Maintains connected instances, shared context, and task routing.
|
|
76
|
+
*/
|
|
77
|
+
export async function startTeamServer(options = {}) {
|
|
78
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
79
|
+
_serverPort = port;
|
|
80
|
+
if (_server) {
|
|
81
|
+
printWarn('Team server already running');
|
|
82
|
+
return _server;
|
|
83
|
+
}
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const server = net.createServer((socket) => {
|
|
86
|
+
const parser = createMessageParser();
|
|
87
|
+
let instanceId = '';
|
|
88
|
+
socket.on('data', (data) => {
|
|
89
|
+
const messages = parser(data);
|
|
90
|
+
for (const msg of messages) {
|
|
91
|
+
handleServerMessage(msg, socket);
|
|
92
|
+
if (msg.type === 'join') {
|
|
93
|
+
instanceId = msg.instanceId;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
socket.on('error', (err) => {
|
|
98
|
+
if (instanceId) {
|
|
99
|
+
handleDisconnect(instanceId);
|
|
100
|
+
}
|
|
101
|
+
// ECONNRESET and EPIPE are expected on abrupt disconnects
|
|
102
|
+
if (err.code !== 'ECONNRESET' &&
|
|
103
|
+
err.code !== 'EPIPE') {
|
|
104
|
+
printError(`Socket error: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
socket.on('close', () => {
|
|
108
|
+
if (instanceId) {
|
|
109
|
+
handleDisconnect(instanceId);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
server.on('error', (err) => {
|
|
114
|
+
if (err.code === 'EADDRINUSE') {
|
|
115
|
+
printError(`Port ${port} is already in use. Another team server may be running.`);
|
|
116
|
+
printInfo(`Try: kbot team join --role coordinator --port ${port}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
printError(`Team server error: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
reject(err);
|
|
122
|
+
});
|
|
123
|
+
server.listen(port, '127.0.0.1', () => {
|
|
124
|
+
_server = server;
|
|
125
|
+
printSuccess(`Team server running on localhost:${port}`);
|
|
126
|
+
printInfo('Waiting for instances to join...');
|
|
127
|
+
printInfo('');
|
|
128
|
+
printInfo('In other terminals, run:');
|
|
129
|
+
printInfo(` kbot team join --role researcher`);
|
|
130
|
+
printInfo(` kbot team join --role coder`);
|
|
131
|
+
printInfo(` kbot team join --role reviewer`);
|
|
132
|
+
resolve(server);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** Handle an incoming message on the server side */
|
|
137
|
+
function handleServerMessage(msg, socket) {
|
|
138
|
+
switch (msg.type) {
|
|
139
|
+
case 'join': {
|
|
140
|
+
const instance = {
|
|
141
|
+
id: msg.instanceId,
|
|
142
|
+
role: msg.role,
|
|
143
|
+
status: 'idle',
|
|
144
|
+
socket,
|
|
145
|
+
joinedAt: new Date(),
|
|
146
|
+
};
|
|
147
|
+
_instances.set(msg.instanceId, instance);
|
|
148
|
+
printSuccess(`[+] ${msg.role} joined (${msg.instanceId.slice(0, 8)})`);
|
|
149
|
+
// Send current shared context to the new instance
|
|
150
|
+
for (const [key, value] of _sharedContext.entries()) {
|
|
151
|
+
const contextMsg = { type: 'context', key, value, from: 'server' };
|
|
152
|
+
safeSend(socket, contextMsg);
|
|
153
|
+
}
|
|
154
|
+
// Broadcast updated status to everyone
|
|
155
|
+
broadcastStatus();
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'leave': {
|
|
159
|
+
handleDisconnect(msg.instanceId);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'context': {
|
|
163
|
+
// Store and broadcast to all other instances
|
|
164
|
+
_sharedContext.set(msg.key, msg.value);
|
|
165
|
+
broadcastToAll(msg, msg.from);
|
|
166
|
+
printInfo(`[ctx] ${msg.from.slice(0, 8)} shared "${msg.key}" (${msg.value.length} chars)`);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 'task': {
|
|
170
|
+
const task = {
|
|
171
|
+
taskId: msg.taskId,
|
|
172
|
+
task: msg.task,
|
|
173
|
+
assignedTo: msg.assignTo,
|
|
174
|
+
from: msg.from,
|
|
175
|
+
createdAt: new Date(),
|
|
176
|
+
status: 'pending',
|
|
177
|
+
};
|
|
178
|
+
_pendingTasks.set(msg.taskId, task);
|
|
179
|
+
// Route to the target role
|
|
180
|
+
const target = findInstanceByRole(msg.assignTo);
|
|
181
|
+
if (target) {
|
|
182
|
+
safeSend(target.socket, msg);
|
|
183
|
+
target.status = 'working';
|
|
184
|
+
task.status = 'in_progress';
|
|
185
|
+
printInfo(`[task] "${msg.task.slice(0, 60)}" → ${msg.assignTo} (${target.id.slice(0, 8)})`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// No instance with that role — notify sender
|
|
189
|
+
const errorMsg = {
|
|
190
|
+
type: 'broadcast',
|
|
191
|
+
message: `No instance with role "${msg.assignTo}" is connected. Available roles: ${getAvailableRoles().join(', ') || 'none'}`,
|
|
192
|
+
from: 'server',
|
|
193
|
+
};
|
|
194
|
+
safeSend(socket, errorMsg);
|
|
195
|
+
printWarn(`[task] No "${msg.assignTo}" instance connected`);
|
|
196
|
+
}
|
|
197
|
+
broadcastStatus();
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case 'result': {
|
|
201
|
+
const pendingTask = _pendingTasks.get(msg.taskId);
|
|
202
|
+
if (pendingTask) {
|
|
203
|
+
pendingTask.status = 'completed';
|
|
204
|
+
pendingTask.result = msg.result;
|
|
205
|
+
// Notify the task originator
|
|
206
|
+
const originator = _instances.get(pendingTask.from);
|
|
207
|
+
if (originator) {
|
|
208
|
+
safeSend(originator.socket, msg);
|
|
209
|
+
}
|
|
210
|
+
// Mark the completer as idle
|
|
211
|
+
const completer = _instances.get(msg.from);
|
|
212
|
+
if (completer) {
|
|
213
|
+
completer.status = 'idle';
|
|
214
|
+
}
|
|
215
|
+
printSuccess(`[done] Task ${msg.taskId.slice(0, 8)} completed by ${msg.from.slice(0, 8)}`);
|
|
216
|
+
}
|
|
217
|
+
broadcastStatus();
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case 'broadcast': {
|
|
221
|
+
broadcastToAll(msg, msg.from);
|
|
222
|
+
printInfo(`[msg] ${msg.from.slice(0, 8)}: ${msg.message.slice(0, 80)}`);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
default:
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Safely send a message to a socket, handling write errors */
|
|
230
|
+
function safeSend(socket, msg) {
|
|
231
|
+
try {
|
|
232
|
+
if (!socket.destroyed) {
|
|
233
|
+
socket.write(encode(msg));
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Socket may have been destroyed between the check and the write
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
/** Broadcast a message to all connected instances except the sender */
|
|
243
|
+
function broadcastToAll(msg, excludeId) {
|
|
244
|
+
for (const [id, instance] of _instances.entries()) {
|
|
245
|
+
if (id !== excludeId && instance.status !== 'disconnected') {
|
|
246
|
+
safeSend(instance.socket, msg);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** Broadcast the current status (all instances) to everyone */
|
|
251
|
+
function broadcastStatus() {
|
|
252
|
+
const statusMsg = {
|
|
253
|
+
type: 'status',
|
|
254
|
+
instances: Array.from(_instances.values())
|
|
255
|
+
.filter(i => i.status !== 'disconnected')
|
|
256
|
+
.map(i => ({ id: i.id, role: i.role, status: i.status })),
|
|
257
|
+
};
|
|
258
|
+
for (const instance of _instances.values()) {
|
|
259
|
+
if (instance.status !== 'disconnected') {
|
|
260
|
+
safeSend(instance.socket, statusMsg);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** Handle an instance disconnect */
|
|
265
|
+
function handleDisconnect(instanceId) {
|
|
266
|
+
const instance = _instances.get(instanceId);
|
|
267
|
+
if (instance && instance.status !== 'disconnected') {
|
|
268
|
+
instance.status = 'disconnected';
|
|
269
|
+
printWarn(`[-] ${instance.role} disconnected (${instanceId.slice(0, 8)})`);
|
|
270
|
+
// Reassign any pending tasks from this instance
|
|
271
|
+
for (const [taskId, task] of _pendingTasks.entries()) {
|
|
272
|
+
if (task.assignedTo === instance.role && task.status === 'in_progress') {
|
|
273
|
+
task.status = 'pending';
|
|
274
|
+
printWarn(`[task] Task ${taskId.slice(0, 8)} unassigned (${instance.role} disconnected)`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
_instances.delete(instanceId);
|
|
278
|
+
broadcastStatus();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Find a connected instance by role (picks the first idle one, or first available) */
|
|
282
|
+
function findInstanceByRole(role) {
|
|
283
|
+
let fallback;
|
|
284
|
+
for (const instance of _instances.values()) {
|
|
285
|
+
if (instance.role === role && instance.status !== 'disconnected') {
|
|
286
|
+
if (instance.status === 'idle')
|
|
287
|
+
return instance;
|
|
288
|
+
fallback = instance;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return fallback;
|
|
292
|
+
}
|
|
293
|
+
/** Get the list of currently connected roles */
|
|
294
|
+
function getAvailableRoles() {
|
|
295
|
+
const roles = new Set();
|
|
296
|
+
for (const instance of _instances.values()) {
|
|
297
|
+
if (instance.status !== 'disconnected') {
|
|
298
|
+
roles.add(instance.role);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return Array.from(roles);
|
|
302
|
+
}
|
|
303
|
+
/** Stop the team server and disconnect all clients */
|
|
304
|
+
export async function stopTeamServer() {
|
|
305
|
+
if (!_server)
|
|
306
|
+
return;
|
|
307
|
+
// Notify all instances
|
|
308
|
+
const leaveMsg = {
|
|
309
|
+
type: 'broadcast',
|
|
310
|
+
message: 'Team server shutting down.',
|
|
311
|
+
from: 'server',
|
|
312
|
+
};
|
|
313
|
+
broadcastToAll(leaveMsg);
|
|
314
|
+
// Close all sockets
|
|
315
|
+
for (const instance of _instances.values()) {
|
|
316
|
+
try {
|
|
317
|
+
instance.socket.destroy();
|
|
318
|
+
}
|
|
319
|
+
catch { /* already closed */ }
|
|
320
|
+
}
|
|
321
|
+
_instances.clear();
|
|
322
|
+
_sharedContext.clear();
|
|
323
|
+
_pendingTasks.clear();
|
|
324
|
+
return new Promise((resolve) => {
|
|
325
|
+
_server.close(() => {
|
|
326
|
+
_server = null;
|
|
327
|
+
printInfo('Team server stopped');
|
|
328
|
+
resolve();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// ── Team Client ──────────────────────────────────────────────────────
|
|
333
|
+
/**
|
|
334
|
+
* Join an existing team as a specific role.
|
|
335
|
+
* Returns methods to interact with the team.
|
|
336
|
+
*/
|
|
337
|
+
export async function joinTeam(options) {
|
|
338
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
339
|
+
const instanceId = options.instanceId ?? randomUUID();
|
|
340
|
+
const role = options.role;
|
|
341
|
+
_reconnectAttempts = 0;
|
|
342
|
+
const client = await connectToServer(port, instanceId, role);
|
|
343
|
+
return new TeamClient(client, instanceId, role, port);
|
|
344
|
+
}
|
|
345
|
+
/** Establish a TCP connection to the team server */
|
|
346
|
+
function connectToServer(port, instanceId, role) {
|
|
347
|
+
return new Promise((resolve, reject) => {
|
|
348
|
+
const socket = net.createConnection({ host: '127.0.0.1', port }, () => {
|
|
349
|
+
// Connected — send join message
|
|
350
|
+
const joinMsg = { type: 'join', role, instanceId };
|
|
351
|
+
socket.write(encode(joinMsg));
|
|
352
|
+
_client = socket;
|
|
353
|
+
_reconnectAttempts = 0;
|
|
354
|
+
printSuccess(`Joined team as "${role}" (${instanceId.slice(0, 8)})`);
|
|
355
|
+
resolve(socket);
|
|
356
|
+
});
|
|
357
|
+
const parser = createMessageParser();
|
|
358
|
+
socket.on('data', (data) => {
|
|
359
|
+
const messages = parser(data);
|
|
360
|
+
for (const msg of messages) {
|
|
361
|
+
handleClientMessage(msg);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
socket.on('error', (err) => {
|
|
365
|
+
if (err.code === 'ECONNREFUSED') {
|
|
366
|
+
printError(`Cannot connect to team server on localhost:${port}`);
|
|
367
|
+
printInfo('Start one with: kbot team start');
|
|
368
|
+
reject(err);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
printError(`Team connection error: ${err.message}`);
|
|
372
|
+
attemptReconnect(port, instanceId, role);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
socket.on('close', () => {
|
|
376
|
+
if (_client === socket) {
|
|
377
|
+
_client = null;
|
|
378
|
+
printWarn('Disconnected from team server');
|
|
379
|
+
attemptReconnect(port, instanceId, role);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/** Attempt to reconnect after a disconnection */
|
|
385
|
+
function attemptReconnect(port, instanceId, role) {
|
|
386
|
+
if (_reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
387
|
+
printError(`Failed to reconnect after ${MAX_RECONNECT_ATTEMPTS} attempts`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
_reconnectAttempts++;
|
|
391
|
+
const delay = RECONNECT_DELAY * _reconnectAttempts;
|
|
392
|
+
printInfo(`Reconnecting in ${delay / 1000}s (attempt ${_reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);
|
|
393
|
+
setTimeout(async () => {
|
|
394
|
+
try {
|
|
395
|
+
await connectToServer(port, instanceId, role);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// connectToServer already logs errors
|
|
399
|
+
}
|
|
400
|
+
}, delay);
|
|
401
|
+
}
|
|
402
|
+
/** Handle an incoming message on the client side */
|
|
403
|
+
function handleClientMessage(msg) {
|
|
404
|
+
switch (msg.type) {
|
|
405
|
+
case 'task': {
|
|
406
|
+
printInfo(`[task] Assigned: "${msg.task.slice(0, 80)}" (from ${msg.from.slice(0, 8)})`);
|
|
407
|
+
if (_onTask) {
|
|
408
|
+
_onTask(msg.task, msg.taskId, msg.from);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case 'context': {
|
|
413
|
+
printInfo(`[ctx] ${msg.from.slice(0, 8)} shared "${msg.key}" (${msg.value.length} chars)`);
|
|
414
|
+
// Store locally
|
|
415
|
+
_sharedContext.set(msg.key, msg.value);
|
|
416
|
+
if (_onContext) {
|
|
417
|
+
_onContext(msg.key, msg.value, msg.from);
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
case 'broadcast': {
|
|
422
|
+
printInfo(`[team] ${msg.from.slice(0, 8)}: ${msg.message.slice(0, 200)}`);
|
|
423
|
+
if (_onBroadcast) {
|
|
424
|
+
_onBroadcast(msg.message, msg.from);
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
case 'result': {
|
|
429
|
+
printSuccess(`[result] Task ${msg.taskId.slice(0, 8)} completed: ${msg.result.slice(0, 100)}`);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case 'status': {
|
|
433
|
+
if (_onStatus) {
|
|
434
|
+
_onStatus(msg.instances);
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
default:
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// ── Team Client Class ────────────────────────────────────────────────
|
|
443
|
+
export class TeamClient {
|
|
444
|
+
socket;
|
|
445
|
+
instanceId;
|
|
446
|
+
role;
|
|
447
|
+
port;
|
|
448
|
+
constructor(socket, instanceId, role, port) {
|
|
449
|
+
this.socket = socket;
|
|
450
|
+
this.instanceId = instanceId;
|
|
451
|
+
this.role = role;
|
|
452
|
+
this.port = port;
|
|
453
|
+
}
|
|
454
|
+
/** Share a context value with the entire team */
|
|
455
|
+
shareContext(key, value) {
|
|
456
|
+
const msg = {
|
|
457
|
+
type: 'context',
|
|
458
|
+
key,
|
|
459
|
+
value,
|
|
460
|
+
from: this.instanceId,
|
|
461
|
+
};
|
|
462
|
+
this.send(msg);
|
|
463
|
+
}
|
|
464
|
+
/** Request a task be assigned to a specific role */
|
|
465
|
+
requestTask(role, task) {
|
|
466
|
+
const taskId = randomUUID();
|
|
467
|
+
const msg = {
|
|
468
|
+
type: 'task',
|
|
469
|
+
task,
|
|
470
|
+
assignTo: role,
|
|
471
|
+
from: this.instanceId,
|
|
472
|
+
taskId,
|
|
473
|
+
};
|
|
474
|
+
this.send(msg);
|
|
475
|
+
return taskId;
|
|
476
|
+
}
|
|
477
|
+
/** Submit a result for a completed task */
|
|
478
|
+
submitResult(taskId, result) {
|
|
479
|
+
const msg = {
|
|
480
|
+
type: 'result',
|
|
481
|
+
taskId,
|
|
482
|
+
result,
|
|
483
|
+
from: this.instanceId,
|
|
484
|
+
};
|
|
485
|
+
this.send(msg);
|
|
486
|
+
}
|
|
487
|
+
/** Broadcast a message to all team members */
|
|
488
|
+
broadcastMessage(message) {
|
|
489
|
+
const msg = {
|
|
490
|
+
type: 'broadcast',
|
|
491
|
+
message,
|
|
492
|
+
from: this.instanceId,
|
|
493
|
+
};
|
|
494
|
+
this.send(msg);
|
|
495
|
+
}
|
|
496
|
+
/** Get locally cached shared context */
|
|
497
|
+
getContext(key) {
|
|
498
|
+
return _sharedContext.get(key);
|
|
499
|
+
}
|
|
500
|
+
/** Get all shared context entries */
|
|
501
|
+
getAllContext() {
|
|
502
|
+
return new Map(_sharedContext);
|
|
503
|
+
}
|
|
504
|
+
/** Register handler for incoming tasks */
|
|
505
|
+
onTask(handler) {
|
|
506
|
+
_onTask = handler;
|
|
507
|
+
}
|
|
508
|
+
/** Register handler for incoming context updates */
|
|
509
|
+
onContext(handler) {
|
|
510
|
+
_onContext = handler;
|
|
511
|
+
}
|
|
512
|
+
/** Register handler for broadcast messages */
|
|
513
|
+
onBroadcast(handler) {
|
|
514
|
+
_onBroadcast = handler;
|
|
515
|
+
}
|
|
516
|
+
/** Register handler for status updates */
|
|
517
|
+
onStatus(handler) {
|
|
518
|
+
_onStatus = handler;
|
|
519
|
+
}
|
|
520
|
+
/** Leave the team gracefully */
|
|
521
|
+
leave() {
|
|
522
|
+
const msg = { type: 'leave', instanceId: this.instanceId };
|
|
523
|
+
this.send(msg);
|
|
524
|
+
setTimeout(() => {
|
|
525
|
+
try {
|
|
526
|
+
this.socket.destroy();
|
|
527
|
+
}
|
|
528
|
+
catch { /* already closed */ }
|
|
529
|
+
_client = null;
|
|
530
|
+
}, 100);
|
|
531
|
+
printInfo(`Left team (${this.role})`);
|
|
532
|
+
}
|
|
533
|
+
/** Check if connected */
|
|
534
|
+
get connected() {
|
|
535
|
+
return !this.socket.destroyed;
|
|
536
|
+
}
|
|
537
|
+
send(msg) {
|
|
538
|
+
if (this.socket.destroyed) {
|
|
539
|
+
printWarn('Not connected to team server');
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
this.socket.write(encode(msg));
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
printError(`Failed to send: ${err instanceof Error ? err.message : String(err)}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// ── Module-level Client Reference ────────────────────────────────────
|
|
551
|
+
// Used by the tools to access the active client without threading it through
|
|
552
|
+
let _activeClient = null;
|
|
553
|
+
export function getActiveClient() {
|
|
554
|
+
return _activeClient;
|
|
555
|
+
}
|
|
556
|
+
export function setActiveClient(client) {
|
|
557
|
+
_activeClient = client;
|
|
558
|
+
}
|
|
559
|
+
// ── Tool Registration ────────────────────────────────────────────────
|
|
560
|
+
export function registerTeamTools() {
|
|
561
|
+
registerTool({
|
|
562
|
+
name: 'team_start',
|
|
563
|
+
description: 'Start the team coordination server. Other kbot instances can then join. Optionally join as coordinator.',
|
|
564
|
+
parameters: {
|
|
565
|
+
port: { type: 'number', description: `TCP port for the team server (default: ${DEFAULT_PORT})` },
|
|
566
|
+
join_as: { type: 'string', description: 'Also join the team with this role (e.g., "coordinator")' },
|
|
567
|
+
},
|
|
568
|
+
tier: 'free',
|
|
569
|
+
async execute(args) {
|
|
570
|
+
const port = args.port ? Number(args.port) : DEFAULT_PORT;
|
|
571
|
+
try {
|
|
572
|
+
await startTeamServer({ port });
|
|
573
|
+
// Optionally join the server we just started
|
|
574
|
+
if (args.join_as) {
|
|
575
|
+
const role = String(args.join_as);
|
|
576
|
+
const client = await joinTeam({ port, role });
|
|
577
|
+
setActiveClient(client);
|
|
578
|
+
return `Team server started on localhost:${port}. Joined as "${role}".`;
|
|
579
|
+
}
|
|
580
|
+
return `Team server started on localhost:${port}. Run 'kbot team join --role <role>' in other terminals.`;
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
return `Failed to start team server: ${err instanceof Error ? err.message : String(err)}`;
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
registerTool({
|
|
588
|
+
name: 'team_join',
|
|
589
|
+
description: 'Join an existing team with a specific role (researcher, coder, reviewer, etc.).',
|
|
590
|
+
parameters: {
|
|
591
|
+
role: { type: 'string', description: 'Your role in the team (researcher, coder, reviewer, coordinator, etc.)', required: true },
|
|
592
|
+
port: { type: 'number', description: `Team server port (default: ${DEFAULT_PORT})` },
|
|
593
|
+
},
|
|
594
|
+
tier: 'free',
|
|
595
|
+
async execute(args) {
|
|
596
|
+
const role = String(args.role);
|
|
597
|
+
const port = args.port ? Number(args.port) : DEFAULT_PORT;
|
|
598
|
+
try {
|
|
599
|
+
const client = await joinTeam({ port, role });
|
|
600
|
+
setActiveClient(client);
|
|
601
|
+
return `Joined team as "${role}" on localhost:${port}. Instance ID: ${client.instanceId.slice(0, 8)}`;
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
return `Failed to join team: ${err instanceof Error ? err.message : String(err)}`;
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
registerTool({
|
|
609
|
+
name: 'team_status',
|
|
610
|
+
description: 'Show all connected team instances and their current status.',
|
|
611
|
+
parameters: {
|
|
612
|
+
port: { type: 'number', description: `Team server port (default: ${DEFAULT_PORT})` },
|
|
613
|
+
},
|
|
614
|
+
tier: 'free',
|
|
615
|
+
async execute() {
|
|
616
|
+
// If we're the server, read directly from server state
|
|
617
|
+
if (_server) {
|
|
618
|
+
const instances = Array.from(_instances.values())
|
|
619
|
+
.filter(i => i.status !== 'disconnected');
|
|
620
|
+
if (instances.length === 0) {
|
|
621
|
+
return 'Team server running, but no instances connected.';
|
|
622
|
+
}
|
|
623
|
+
const lines = instances.map(i => {
|
|
624
|
+
const uptime = Math.round((Date.now() - i.joinedAt.getTime()) / 1000);
|
|
625
|
+
return ` ${i.role.padEnd(15)} ${i.status.padEnd(10)} ${i.id.slice(0, 8)} (${uptime}s)`;
|
|
626
|
+
});
|
|
627
|
+
const contextKeys = Array.from(_sharedContext.keys());
|
|
628
|
+
const tasksSummary = Array.from(_pendingTasks.values());
|
|
629
|
+
const pending = tasksSummary.filter(t => t.status === 'pending').length;
|
|
630
|
+
const inProgress = tasksSummary.filter(t => t.status === 'in_progress').length;
|
|
631
|
+
const completed = tasksSummary.filter(t => t.status === 'completed').length;
|
|
632
|
+
return [
|
|
633
|
+
`Team Server — localhost:${_serverPort}`,
|
|
634
|
+
``,
|
|
635
|
+
`Instances (${instances.length}):`,
|
|
636
|
+
` ${'Role'.padEnd(15)} ${'Status'.padEnd(10)} ID`,
|
|
637
|
+
` ${'─'.repeat(15)} ${'─'.repeat(10)} ${'─'.repeat(8)}`,
|
|
638
|
+
...lines,
|
|
639
|
+
``,
|
|
640
|
+
`Shared Context: ${contextKeys.length} entries${contextKeys.length > 0 ? ' (' + contextKeys.join(', ') + ')' : ''}`,
|
|
641
|
+
`Tasks: ${pending} pending, ${inProgress} in progress, ${completed} completed`,
|
|
642
|
+
].join('\n');
|
|
643
|
+
}
|
|
644
|
+
// If we're a client, report what we know
|
|
645
|
+
if (_activeClient) {
|
|
646
|
+
const context = _activeClient.getAllContext();
|
|
647
|
+
return [
|
|
648
|
+
`Connected as "${_activeClient.role}" (${_activeClient.instanceId.slice(0, 8)})`,
|
|
649
|
+
`Connected: ${_activeClient.connected ? 'yes' : 'no'}`,
|
|
650
|
+
`Shared context: ${context.size} entries${context.size > 0 ? ' (' + Array.from(context.keys()).join(', ') + ')' : ''}`,
|
|
651
|
+
].join('\n');
|
|
652
|
+
}
|
|
653
|
+
return 'Not connected to any team. Run team_start or team_join first.';
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
registerTool({
|
|
657
|
+
name: 'team_share',
|
|
658
|
+
description: 'Share context with the team (e.g., research findings, code snippets, decisions). All team members receive it.',
|
|
659
|
+
parameters: {
|
|
660
|
+
key: { type: 'string', description: 'Context key (e.g., "research_findings", "api_design", "review_notes")', required: true },
|
|
661
|
+
value: { type: 'string', description: 'Context value — the actual content to share', required: true },
|
|
662
|
+
},
|
|
663
|
+
tier: 'free',
|
|
664
|
+
async execute(args) {
|
|
665
|
+
const key = String(args.key);
|
|
666
|
+
const value = String(args.value);
|
|
667
|
+
if (_activeClient) {
|
|
668
|
+
_activeClient.shareContext(key, value);
|
|
669
|
+
return `Shared "${key}" with team (${value.length} chars)`;
|
|
670
|
+
}
|
|
671
|
+
// If we're only the server (no client), store directly and broadcast
|
|
672
|
+
if (_server) {
|
|
673
|
+
_sharedContext.set(key, value);
|
|
674
|
+
const msg = { type: 'context', key, value, from: 'server' };
|
|
675
|
+
broadcastToAll(msg);
|
|
676
|
+
return `Shared "${key}" with team from server (${value.length} chars)`;
|
|
677
|
+
}
|
|
678
|
+
return 'Not connected to any team. Run team_start or team_join first.';
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
registerTool({
|
|
682
|
+
name: 'team_assign',
|
|
683
|
+
description: 'Assign a task to a specific role in the team. The instance with that role will receive the task.',
|
|
684
|
+
parameters: {
|
|
685
|
+
role: { type: 'string', description: 'Target role (researcher, coder, reviewer, etc.)', required: true },
|
|
686
|
+
task: { type: 'string', description: 'Task description — what the target should do', required: true },
|
|
687
|
+
},
|
|
688
|
+
tier: 'free',
|
|
689
|
+
async execute(args) {
|
|
690
|
+
const role = String(args.role);
|
|
691
|
+
const task = String(args.task);
|
|
692
|
+
if (_activeClient) {
|
|
693
|
+
const taskId = _activeClient.requestTask(role, task);
|
|
694
|
+
return `Task assigned to "${role}": ${task.slice(0, 100)}${task.length > 100 ? '...' : ''}\nTask ID: ${taskId.slice(0, 8)}`;
|
|
695
|
+
}
|
|
696
|
+
// Server can assign tasks directly
|
|
697
|
+
if (_server) {
|
|
698
|
+
const taskId = randomUUID();
|
|
699
|
+
const target = findInstanceByRole(role);
|
|
700
|
+
if (!target) {
|
|
701
|
+
const available = getAvailableRoles();
|
|
702
|
+
return `No instance with role "${role}" is connected. Available: ${available.join(', ') || 'none'}`;
|
|
703
|
+
}
|
|
704
|
+
const msg = { type: 'task', task, assignTo: role, from: 'server', taskId };
|
|
705
|
+
safeSend(target.socket, msg);
|
|
706
|
+
target.status = 'working';
|
|
707
|
+
const pendingTask = {
|
|
708
|
+
taskId,
|
|
709
|
+
task,
|
|
710
|
+
assignedTo: role,
|
|
711
|
+
from: 'server',
|
|
712
|
+
createdAt: new Date(),
|
|
713
|
+
status: 'in_progress',
|
|
714
|
+
};
|
|
715
|
+
_pendingTasks.set(taskId, pendingTask);
|
|
716
|
+
broadcastStatus();
|
|
717
|
+
return `Task assigned to "${role}" (${target.id.slice(0, 8)}): ${task.slice(0, 100)}${task.length > 100 ? '...' : ''}\nTask ID: ${taskId.slice(0, 8)}`;
|
|
718
|
+
}
|
|
719
|
+
return 'Not connected to any team. Run team_start or team_join first.';
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
registerTool({
|
|
723
|
+
name: 'team_broadcast',
|
|
724
|
+
description: 'Send a message to all team members. Use for announcements, status updates, or coordination.',
|
|
725
|
+
parameters: {
|
|
726
|
+
message: { type: 'string', description: 'Message to broadcast to all team members', required: true },
|
|
727
|
+
},
|
|
728
|
+
tier: 'free',
|
|
729
|
+
async execute(args) {
|
|
730
|
+
const message = String(args.message);
|
|
731
|
+
if (_activeClient) {
|
|
732
|
+
_activeClient.broadcastMessage(message);
|
|
733
|
+
return `Broadcast sent to team: ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}`;
|
|
734
|
+
}
|
|
735
|
+
if (_server) {
|
|
736
|
+
const msg = { type: 'broadcast', message, from: 'server' };
|
|
737
|
+
broadcastToAll(msg);
|
|
738
|
+
return `Broadcast sent from server: ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}`;
|
|
739
|
+
}
|
|
740
|
+
return 'Not connected to any team. Run team_start or team_join first.';
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
registerTool({
|
|
744
|
+
name: 'team_context',
|
|
745
|
+
description: 'Read shared context from the team. Returns a specific key or all shared context.',
|
|
746
|
+
parameters: {
|
|
747
|
+
key: { type: 'string', description: 'Context key to read. Omit to list all keys.' },
|
|
748
|
+
},
|
|
749
|
+
tier: 'free',
|
|
750
|
+
async execute(args) {
|
|
751
|
+
if (!_activeClient && !_server) {
|
|
752
|
+
return 'Not connected to any team. Run team_start or team_join first.';
|
|
753
|
+
}
|
|
754
|
+
if (args.key) {
|
|
755
|
+
const key = String(args.key);
|
|
756
|
+
const value = _sharedContext.get(key);
|
|
757
|
+
if (value) {
|
|
758
|
+
return `[${key}]\n${value}`;
|
|
759
|
+
}
|
|
760
|
+
return `No shared context for key "${key}". Available keys: ${Array.from(_sharedContext.keys()).join(', ') || 'none'}`;
|
|
761
|
+
}
|
|
762
|
+
// List all context
|
|
763
|
+
if (_sharedContext.size === 0) {
|
|
764
|
+
return 'No shared context yet. Team members can share context with team_share.';
|
|
765
|
+
}
|
|
766
|
+
const entries = Array.from(_sharedContext.entries()).map(([k, v]) => {
|
|
767
|
+
const preview = v.length > 120 ? v.slice(0, 120) + '...' : v;
|
|
768
|
+
return `[${k}] (${v.length} chars)\n ${preview}`;
|
|
769
|
+
});
|
|
770
|
+
return `Shared Context (${_sharedContext.size} entries):\n\n${entries.join('\n\n')}`;
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
registerTool({
|
|
774
|
+
name: 'team_result',
|
|
775
|
+
description: 'Submit a result for a task that was assigned to you.',
|
|
776
|
+
parameters: {
|
|
777
|
+
task_id: { type: 'string', description: 'Task ID (shown when task was assigned)', required: true },
|
|
778
|
+
result: { type: 'string', description: 'Task result — what you accomplished', required: true },
|
|
779
|
+
},
|
|
780
|
+
tier: 'free',
|
|
781
|
+
async execute(args) {
|
|
782
|
+
const taskId = String(args.task_id);
|
|
783
|
+
const result = String(args.result);
|
|
784
|
+
if (_activeClient) {
|
|
785
|
+
_activeClient.submitResult(taskId, result);
|
|
786
|
+
return `Result submitted for task ${taskId.slice(0, 8)}`;
|
|
787
|
+
}
|
|
788
|
+
return 'Not connected to any team. Run team_join first.';
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
registerTool({
|
|
792
|
+
name: 'team_stop',
|
|
793
|
+
description: 'Stop the team server and disconnect all instances.',
|
|
794
|
+
parameters: {},
|
|
795
|
+
tier: 'free',
|
|
796
|
+
async execute() {
|
|
797
|
+
if (_activeClient) {
|
|
798
|
+
_activeClient.leave();
|
|
799
|
+
setActiveClient(null);
|
|
800
|
+
}
|
|
801
|
+
if (_server) {
|
|
802
|
+
await stopTeamServer();
|
|
803
|
+
return 'Team server stopped. All instances disconnected.';
|
|
804
|
+
}
|
|
805
|
+
return 'No team server running on this instance.';
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
// ── CLI Integration ──────────────────────────────────────────────────
|
|
810
|
+
/**
|
|
811
|
+
* Register the `kbot team` subcommand with a Commander program.
|
|
812
|
+
* Called from cli.ts.
|
|
813
|
+
*/
|
|
814
|
+
export function registerTeamCommand(program) {
|
|
815
|
+
const teamCmd = program
|
|
816
|
+
.command('team')
|
|
817
|
+
.description('Team mode — coordinate multiple kbot instances');
|
|
818
|
+
teamCmd
|
|
819
|
+
.command('start')
|
|
820
|
+
.description('Start the team coordination server')
|
|
821
|
+
.option('--port <port>', 'TCP port', String(DEFAULT_PORT))
|
|
822
|
+
.option('--role <role>', 'Also join as this role (default: coordinator)')
|
|
823
|
+
.action(async (opts) => {
|
|
824
|
+
const port = parseInt(opts.port, 10);
|
|
825
|
+
const role = opts.role ?? 'coordinator';
|
|
826
|
+
try {
|
|
827
|
+
await startTeamServer({ port });
|
|
828
|
+
const client = await joinTeam({ port, role });
|
|
829
|
+
setActiveClient(client);
|
|
830
|
+
// Keep the process alive and handle shutdown
|
|
831
|
+
const shutdown = async () => {
|
|
832
|
+
printInfo('\nShutting down team...');
|
|
833
|
+
client.leave();
|
|
834
|
+
await stopTeamServer();
|
|
835
|
+
process.exit(0);
|
|
836
|
+
};
|
|
837
|
+
process.on('SIGINT', shutdown);
|
|
838
|
+
process.on('SIGTERM', shutdown);
|
|
839
|
+
// Heartbeat — periodically log status
|
|
840
|
+
setInterval(() => {
|
|
841
|
+
const count = _instances.size;
|
|
842
|
+
if (count > 0) {
|
|
843
|
+
const roles = getAvailableRoles();
|
|
844
|
+
printInfo(`[heartbeat] ${count} instance(s): ${roles.join(', ')}`);
|
|
845
|
+
}
|
|
846
|
+
}, HEARTBEAT_INTERVAL);
|
|
847
|
+
// Keep alive
|
|
848
|
+
await new Promise(() => { });
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
teamCmd
|
|
855
|
+
.command('join')
|
|
856
|
+
.description('Join an existing team with a role')
|
|
857
|
+
.requiredOption('--role <role>', 'Your role (researcher, coder, reviewer, etc.)')
|
|
858
|
+
.option('--port <port>', 'Team server port', String(DEFAULT_PORT))
|
|
859
|
+
.action(async (opts) => {
|
|
860
|
+
const port = parseInt(opts.port, 10);
|
|
861
|
+
try {
|
|
862
|
+
const client = await joinTeam({ port, role: opts.role });
|
|
863
|
+
setActiveClient(client);
|
|
864
|
+
// Handle shutdown
|
|
865
|
+
const shutdown = () => {
|
|
866
|
+
printInfo('\nLeaving team...');
|
|
867
|
+
client.leave();
|
|
868
|
+
process.exit(0);
|
|
869
|
+
};
|
|
870
|
+
process.on('SIGINT', shutdown);
|
|
871
|
+
process.on('SIGTERM', shutdown);
|
|
872
|
+
// Keep alive
|
|
873
|
+
await new Promise(() => { });
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
teamCmd
|
|
880
|
+
.command('status')
|
|
881
|
+
.description('Show connected team instances')
|
|
882
|
+
.option('--port <port>', 'Team server port', String(DEFAULT_PORT))
|
|
883
|
+
.action(async (opts) => {
|
|
884
|
+
const port = parseInt(opts.port, 10);
|
|
885
|
+
// Connect briefly to get status, then disconnect
|
|
886
|
+
try {
|
|
887
|
+
const client = await joinTeam({ port, role: '_status_probe', instanceId: `probe-${randomUUID()}` });
|
|
888
|
+
// Wait a moment for the status broadcast to arrive
|
|
889
|
+
await new Promise((resolve) => {
|
|
890
|
+
const timeout = setTimeout(() => {
|
|
891
|
+
resolve();
|
|
892
|
+
}, 1000);
|
|
893
|
+
client.onStatus((instances) => {
|
|
894
|
+
clearTimeout(timeout);
|
|
895
|
+
console.log('');
|
|
896
|
+
console.log(`Team — localhost:${port}`);
|
|
897
|
+
console.log(`${'Role'.padEnd(15)} ${'Status'.padEnd(10)} ID`);
|
|
898
|
+
console.log(`${'─'.repeat(15)} ${'─'.repeat(10)} ${'─'.repeat(8)}`);
|
|
899
|
+
for (const inst of instances) {
|
|
900
|
+
if (inst.role === '_status_probe')
|
|
901
|
+
continue;
|
|
902
|
+
console.log(`${inst.role.padEnd(15)} ${inst.status.padEnd(10)} ${inst.id.slice(0, 8)}`);
|
|
903
|
+
}
|
|
904
|
+
console.log(`\n${instances.filter(i => i.role !== '_status_probe').length} instance(s) connected`);
|
|
905
|
+
resolve();
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
client.leave();
|
|
909
|
+
process.exit(0);
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
printError('Cannot connect to team server. Is one running?');
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
//# sourceMappingURL=team.js.map
|