@supercollab/cli 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/supercollab.js +259 -1
- package/bin/supercollab.js.bak-20260624T032438Z +405 -0
- package/package.json +1 -1
package/bin/supercollab.js
CHANGED
|
@@ -3,10 +3,11 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
|
+
import * as readlineCore from 'node:readline';
|
|
6
7
|
import readline from 'node:readline/promises';
|
|
7
8
|
import { stdin as input, stdout as output } from 'node:process';
|
|
8
9
|
|
|
9
|
-
const VERSION = '0.1.
|
|
10
|
+
const VERSION = '0.1.2';
|
|
10
11
|
const DEFAULT_SERVER = process.env.SUPERCOLLAB_URL || 'https://hyper.polynode.dev';
|
|
11
12
|
const DEFAULT_CONFIG = process.env.SUPERCOLLAB_CONFIG || path.join(os.homedir(), '.supercollab', 'config.json');
|
|
12
13
|
const SESSION_TTL_SKEW = 60;
|
|
@@ -17,11 +18,14 @@ function printHelp() {
|
|
|
17
18
|
Usage:
|
|
18
19
|
supercollab register --username NAME [--password PASS] [--server URL] [--label LABEL]
|
|
19
20
|
supercollab login --username NAME [--password PASS] [--server URL]
|
|
21
|
+
supercollab menu
|
|
20
22
|
supercollab whoami
|
|
21
23
|
supercollab agent register [--label LABEL]
|
|
22
24
|
supercollab workspace list
|
|
23
25
|
supercollab workspace create --title TITLE --goal GOAL [--slug SLUG]
|
|
24
26
|
supercollab workspace invite --workspace ID [--role member]
|
|
27
|
+
supercollab workspace invites --workspace ID
|
|
28
|
+
supercollab workspace invite-revoke --workspace ID --invite ID
|
|
25
29
|
supercollab workspace join --invite TOKEN
|
|
26
30
|
supercollab workspace message --workspace ID --text TEXT
|
|
27
31
|
supercollab workspace messages --workspace ID [--limit 20]
|
|
@@ -29,6 +33,8 @@ Usage:
|
|
|
29
33
|
supercollab workspace write --workspace ID --path PATH --content TEXT
|
|
30
34
|
supercollab workspace read --workspace ID --path PATH
|
|
31
35
|
supercollab workspace git --workspace ID
|
|
36
|
+
supercollab session list
|
|
37
|
+
supercollab session revoke --session ID
|
|
32
38
|
supercollab heartbeat set --workspace ID --prompt TEXT [--interval 900]
|
|
33
39
|
supercollab heartbeat tick --workspace ID
|
|
34
40
|
supercollab mcp stdio
|
|
@@ -260,6 +266,251 @@ async function doLogin(opts) {
|
|
|
260
266
|
console.log(JSON.stringify({ ok: true, username: config.username, user_id: config.userId, config: file }, null, 2));
|
|
261
267
|
}
|
|
262
268
|
|
|
269
|
+
async function askLine(prompt, defaultValue = '') {
|
|
270
|
+
const rl = readline.createInterface({ input, output });
|
|
271
|
+
try {
|
|
272
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
273
|
+
const answer = await rl.question(`${prompt}${suffix}: `);
|
|
274
|
+
return answer.trim() || defaultValue;
|
|
275
|
+
} finally {
|
|
276
|
+
rl.close();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function pause() {
|
|
281
|
+
if (!process.stdin.isTTY) return;
|
|
282
|
+
await askLine('Press Enter to continue');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function clearScreen() {
|
|
286
|
+
if (process.stdout.isTTY) output.write('\x1b[2J\x1b[H');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function selectMenu(title, items) {
|
|
290
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
|
291
|
+
items.forEach((item, idx) => console.log(`${idx + 1}. ${item.label}`));
|
|
292
|
+
const answer = await askLine('Choose number');
|
|
293
|
+
const idx = Number(answer) - 1;
|
|
294
|
+
return items[idx] || null;
|
|
295
|
+
}
|
|
296
|
+
readlineCore.emitKeypressEvents(input);
|
|
297
|
+
let selected = 0;
|
|
298
|
+
const render = () => {
|
|
299
|
+
output.write('\x1b[?25l\x1b[2J\x1b[H');
|
|
300
|
+
output.write(`${title}\n\n`);
|
|
301
|
+
for (let i = 0; i < items.length; i++) {
|
|
302
|
+
output.write(`${i === selected ? '> ' : ' '}${items[i].label}\n`);
|
|
303
|
+
}
|
|
304
|
+
output.write('\nUp/down to move, Enter to select, q to go back.\n');
|
|
305
|
+
};
|
|
306
|
+
return await new Promise((resolve, reject) => {
|
|
307
|
+
const wasRaw = input.isRaw;
|
|
308
|
+
const cleanup = () => {
|
|
309
|
+
input.off('keypress', onKey);
|
|
310
|
+
try { input.setRawMode(Boolean(wasRaw)); } catch {}
|
|
311
|
+
output.write('\x1b[?25h');
|
|
312
|
+
};
|
|
313
|
+
const onKey = (_str, key = {}) => {
|
|
314
|
+
if (key.ctrl && key.name === 'c') {
|
|
315
|
+
cleanup();
|
|
316
|
+
reject(new Error('cancelled'));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (key.name === 'up') {
|
|
320
|
+
selected = (selected - 1 + items.length) % items.length;
|
|
321
|
+
render();
|
|
322
|
+
} else if (key.name === 'down') {
|
|
323
|
+
selected = (selected + 1) % items.length;
|
|
324
|
+
render();
|
|
325
|
+
} else if (key.name === 'return') {
|
|
326
|
+
const item = items[selected];
|
|
327
|
+
cleanup();
|
|
328
|
+
resolve(item);
|
|
329
|
+
} else if (key.name === 'q' || key.name === 'escape') {
|
|
330
|
+
cleanup();
|
|
331
|
+
resolve(null);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
input.setRawMode(true);
|
|
335
|
+
input.resume();
|
|
336
|
+
input.on('keypress', onKey);
|
|
337
|
+
render();
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function printTable(rows, columns) {
|
|
342
|
+
if (!rows.length) {
|
|
343
|
+
console.log('No rows.');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const widths = columns.map((col) => Math.max(col.label.length, ...rows.map((row) => String(col.value(row) ?? '').length)));
|
|
347
|
+
console.log(columns.map((col, i) => col.label.padEnd(widths[i])).join(' '));
|
|
348
|
+
console.log(widths.map((w) => '-'.repeat(w)).join(' '));
|
|
349
|
+
for (const row of rows) {
|
|
350
|
+
console.log(columns.map((col, i) => String(col.value(row) ?? '').padEnd(widths[i])).join(' '));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function chooseWorkspace(config) {
|
|
355
|
+
const data = await callTool(config, 'workspace_list', {});
|
|
356
|
+
const workspaces = data.workspaces || [];
|
|
357
|
+
const items = workspaces.map((ws) => ({ label: `${ws.title} (${ws.id})`, workspace: ws }));
|
|
358
|
+
items.push({ label: 'Enter workspace id manually', manual: true });
|
|
359
|
+
items.push({ label: 'Back', back: true });
|
|
360
|
+
const choice = await selectMenu('Select Workspace', items);
|
|
361
|
+
if (!choice || choice.back) return null;
|
|
362
|
+
if (choice.manual) return { id: await askLine('Workspace id') };
|
|
363
|
+
return choice.workspace;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function listInvites(config, workspaceId) {
|
|
367
|
+
const data = await apiAsAgent(config, 'GET', `/v1/workspaces/${workspaceId}/invites`);
|
|
368
|
+
printTable(data.invites || [], [
|
|
369
|
+
{ label: 'id', value: (r) => r.id },
|
|
370
|
+
{ label: 'role', value: (r) => r.role },
|
|
371
|
+
{ label: 'status', value: (r) => r.status },
|
|
372
|
+
{ label: 'expires', value: (r) => r.expires_at },
|
|
373
|
+
{ label: 'created_by', value: (r) => r.created_by_username || r.created_by_user_id },
|
|
374
|
+
]);
|
|
375
|
+
return data;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function manageInvites(config) {
|
|
379
|
+
const workspace = await chooseWorkspace(config);
|
|
380
|
+
if (!workspace) return;
|
|
381
|
+
const workspaceId = workspace.id || workspace.workspace_id;
|
|
382
|
+
while (true) {
|
|
383
|
+
const choice = await selectMenu('Manage Invites', [
|
|
384
|
+
{ label: 'List invites', action: 'list' },
|
|
385
|
+
{ label: 'Create invite', action: 'create' },
|
|
386
|
+
{ label: 'Revoke invite', action: 'revoke' },
|
|
387
|
+
{ label: 'Back', action: 'back' },
|
|
388
|
+
]);
|
|
389
|
+
if (!choice || choice.action === 'back') return;
|
|
390
|
+
clearScreen();
|
|
391
|
+
if (choice.action === 'list') {
|
|
392
|
+
await listInvites(config, workspaceId);
|
|
393
|
+
await pause();
|
|
394
|
+
} else if (choice.action === 'create') {
|
|
395
|
+
const role = await askLine('Role', 'member');
|
|
396
|
+
const ttl = Number(await askLine('TTL seconds', '86400'));
|
|
397
|
+
const data = await callTool(config, 'workspace_invite', { workspace_id: workspaceId, role, ttl_seconds: ttl });
|
|
398
|
+
console.log(JSON.stringify(data, null, 2));
|
|
399
|
+
await pause();
|
|
400
|
+
} else if (choice.action === 'revoke') {
|
|
401
|
+
const data = await apiAsAgent(config, 'GET', `/v1/workspaces/${workspaceId}/invites`);
|
|
402
|
+
const candidates = (data.invites || []).filter((invite) => invite.status === 'pending');
|
|
403
|
+
const items = candidates.map((invite) => ({ label: `${invite.id} ${invite.role} expires ${invite.expires_at}`, invite }));
|
|
404
|
+
items.push({ label: 'Enter invite id manually', manual: true });
|
|
405
|
+
items.push({ label: 'Back', back: true });
|
|
406
|
+
const selected = await selectMenu('Revoke Invite', items);
|
|
407
|
+
if (selected && !selected.back) {
|
|
408
|
+
const inviteId = selected.manual ? await askLine('Invite id') : selected.invite.id;
|
|
409
|
+
const result = await apiAsAgent(config, 'DELETE', `/v1/workspaces/${workspaceId}/invites/${inviteId}`);
|
|
410
|
+
console.log(JSON.stringify(result, null, 2));
|
|
411
|
+
await pause();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function listSessions(config) {
|
|
418
|
+
const data = await api(config, 'GET', '/v1/agent-sessions', undefined, config.userToken);
|
|
419
|
+
printTable(data.sessions || [], [
|
|
420
|
+
{ label: 'session', value: (r) => r.session_id },
|
|
421
|
+
{ label: 'agent', value: (r) => r.label || r.agent_id },
|
|
422
|
+
{ label: 'active', value: (r) => r.active ? 'yes' : 'no' },
|
|
423
|
+
{ label: 'expires', value: (r) => r.expires_at },
|
|
424
|
+
{ label: 'revoked', value: (r) => r.revoked_at || '' },
|
|
425
|
+
]);
|
|
426
|
+
return data;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function revokeSession(config, file, sessionId) {
|
|
430
|
+
const result = await api(config, 'DELETE', `/v1/agent-sessions/${sessionId}`, undefined, config.userToken);
|
|
431
|
+
delete config.agentSessionToken;
|
|
432
|
+
delete config.agentSessionExpiresAt;
|
|
433
|
+
saveConfig(config, file);
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function manageSessions(config, file) {
|
|
438
|
+
while (true) {
|
|
439
|
+
const choice = await selectMenu('Manage Sessions', [
|
|
440
|
+
{ label: 'List sessions', action: 'list' },
|
|
441
|
+
{ label: 'Revoke session', action: 'revoke' },
|
|
442
|
+
{ label: 'Back', action: 'back' },
|
|
443
|
+
]);
|
|
444
|
+
if (!choice || choice.action === 'back') return;
|
|
445
|
+
clearScreen();
|
|
446
|
+
if (choice.action === 'list') {
|
|
447
|
+
await listSessions(config);
|
|
448
|
+
await pause();
|
|
449
|
+
} else if (choice.action === 'revoke') {
|
|
450
|
+
const data = await api(config, 'GET', '/v1/agent-sessions', undefined, config.userToken);
|
|
451
|
+
const candidates = (data.sessions || []).filter((session) => session.active);
|
|
452
|
+
const items = candidates.map((session) => ({ label: `${session.session_id} ${session.label} expires ${session.expires_at}`, session }));
|
|
453
|
+
items.push({ label: 'Enter session id manually', manual: true });
|
|
454
|
+
items.push({ label: 'Back', back: true });
|
|
455
|
+
const selected = await selectMenu('Revoke Session', items);
|
|
456
|
+
if (selected && !selected.back) {
|
|
457
|
+
const sessionId = selected.manual ? await askLine('Session id') : selected.session.session_id;
|
|
458
|
+
console.log(JSON.stringify(await revokeSession(config, file, sessionId), null, 2));
|
|
459
|
+
await pause();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function runMenu(opts) {
|
|
466
|
+
const file = configPath(opts);
|
|
467
|
+
const config = loadConfig(file);
|
|
468
|
+
config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
|
|
469
|
+
while (true) {
|
|
470
|
+
const label = config.username ? `SuperCollab - ${config.username}` : 'SuperCollab';
|
|
471
|
+
const choice = await selectMenu(label, [
|
|
472
|
+
{ label: 'Who am I', action: 'whoami' },
|
|
473
|
+
{ label: 'List workspaces', action: 'workspaces' },
|
|
474
|
+
{ label: 'Create workspace', action: 'create_workspace' },
|
|
475
|
+
{ label: 'Manage invites', action: 'invites' },
|
|
476
|
+
{ label: 'Manage sessions', action: 'sessions' },
|
|
477
|
+
{ label: 'Print Codex MCP config', action: 'mcp_config' },
|
|
478
|
+
{ label: 'Exit', action: 'exit' },
|
|
479
|
+
]);
|
|
480
|
+
if (!choice || choice.action === 'exit') {
|
|
481
|
+
clearScreen();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
clearScreen();
|
|
485
|
+
if (choice.action === 'whoami') {
|
|
486
|
+
console.log(JSON.stringify(await api(config, 'GET', '/v1/me'), null, 2));
|
|
487
|
+
await pause();
|
|
488
|
+
} else if (choice.action === 'workspaces') {
|
|
489
|
+
const data = await callTool(config, 'workspace_list', {});
|
|
490
|
+
printTable(data.workspaces || [], [
|
|
491
|
+
{ label: 'id', value: (r) => r.id },
|
|
492
|
+
{ label: 'role', value: (r) => r.role },
|
|
493
|
+
{ label: 'title', value: (r) => r.title },
|
|
494
|
+
]);
|
|
495
|
+
await pause();
|
|
496
|
+
} else if (choice.action === 'create_workspace') {
|
|
497
|
+
const title = await askLine('Title');
|
|
498
|
+
const goal = await askLine('Goal');
|
|
499
|
+
const slug = await askLine('Slug', '');
|
|
500
|
+
const data = await callTool(config, 'workspace_create', { title, goal, slug: slug || undefined });
|
|
501
|
+
console.log(JSON.stringify(data, null, 2));
|
|
502
|
+
await pause();
|
|
503
|
+
} else if (choice.action === 'invites') {
|
|
504
|
+
await manageInvites(config);
|
|
505
|
+
} else if (choice.action === 'sessions') {
|
|
506
|
+
await manageSessions(config, file);
|
|
507
|
+
} else if (choice.action === 'mcp_config') {
|
|
508
|
+
printCodexConfig(opts);
|
|
509
|
+
await pause();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
263
514
|
function toolSchema(name, description, properties = {}, required = []) {
|
|
264
515
|
return { name, description, inputSchema: { type: 'object', properties, required } };
|
|
265
516
|
}
|
|
@@ -371,6 +622,7 @@ async function main() {
|
|
|
371
622
|
|
|
372
623
|
if (cmd === 'register') return doRegister(opts);
|
|
373
624
|
if (cmd === 'login') return doLogin(opts);
|
|
625
|
+
if (cmd === 'menu') return runMenu(opts);
|
|
374
626
|
if (cmd === 'whoami') return console.log(JSON.stringify(await api(config, 'GET', '/v1/me'), null, 2));
|
|
375
627
|
if (cmd === 'config' && sub === 'path') return console.log(file);
|
|
376
628
|
if (cmd === 'agent' && sub === 'register') {
|
|
@@ -380,10 +632,16 @@ async function main() {
|
|
|
380
632
|
}
|
|
381
633
|
if (cmd === 'mcp' && sub === 'stdio') return runMcp(opts);
|
|
382
634
|
if (cmd === 'mcp' && sub === 'print-config') return printCodexConfig(opts);
|
|
635
|
+
if (cmd === 'session') {
|
|
636
|
+
if (sub === 'list') return console.log(JSON.stringify(await api(config, 'GET', '/v1/agent-sessions', undefined, config.userToken), null, 2));
|
|
637
|
+
if (sub === 'revoke') return console.log(JSON.stringify(await revokeSession(config, file, requireValue(opts, 'session')), null, 2));
|
|
638
|
+
}
|
|
383
639
|
if (cmd === 'workspace') {
|
|
384
640
|
if (sub === 'list') return console.log(JSON.stringify(await callTool(config, 'workspace_list', {}), null, 2));
|
|
385
641
|
if (sub === 'create') return console.log(JSON.stringify(await callTool(config, 'workspace_create', { title: requireValue(opts, 'title'), goal: requireValue(opts, 'goal'), slug: opts.slug }), null, 2));
|
|
386
642
|
if (sub === 'invite') return console.log(JSON.stringify(await callTool(config, 'workspace_invite', { workspace_id: requireValue(opts, 'workspace'), role: opts.role || 'member', ttl_seconds: opts.ttl || 86400 }), null, 2));
|
|
643
|
+
if (sub === 'invites') return console.log(JSON.stringify(await apiAsAgent(config, 'GET', `/v1/workspaces/${requireValue(opts, 'workspace')}/invites`), null, 2));
|
|
644
|
+
if (sub === 'invite-revoke') return console.log(JSON.stringify(await apiAsAgent(config, 'DELETE', `/v1/workspaces/${requireValue(opts, 'workspace')}/invites/${requireValue(opts, 'invite')}`), null, 2));
|
|
387
645
|
if (sub === 'join') return console.log(JSON.stringify(await callTool(config, 'workspace_join', { invite_token: requireValue(opts, 'invite') }), null, 2));
|
|
388
646
|
if (sub === 'message') return console.log(JSON.stringify(await callTool(config, 'workspace_message_send', { workspace_id: requireValue(opts, 'workspace'), text: requireValue(opts, 'text') }), null, 2));
|
|
389
647
|
if (sub === 'messages') return console.log(JSON.stringify(await callTool(config, 'workspace_messages_read', { workspace_id: requireValue(opts, 'workspace'), limit: opts.limit || 20 }), null, 2));
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import readline from 'node:readline/promises';
|
|
7
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
8
|
+
|
|
9
|
+
const VERSION = '0.1.1';
|
|
10
|
+
const DEFAULT_SERVER = process.env.SUPERCOLLAB_URL || 'https://hyper.polynode.dev';
|
|
11
|
+
const DEFAULT_CONFIG = process.env.SUPERCOLLAB_CONFIG || path.join(os.homedir(), '.supercollab', 'config.json');
|
|
12
|
+
const SESSION_TTL_SKEW = 60;
|
|
13
|
+
|
|
14
|
+
function printHelp() {
|
|
15
|
+
console.log(`SuperCollab CLI ${VERSION}
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
supercollab register --username NAME [--password PASS] [--server URL] [--label LABEL]
|
|
19
|
+
supercollab login --username NAME [--password PASS] [--server URL]
|
|
20
|
+
supercollab whoami
|
|
21
|
+
supercollab agent register [--label LABEL]
|
|
22
|
+
supercollab workspace list
|
|
23
|
+
supercollab workspace create --title TITLE --goal GOAL [--slug SLUG]
|
|
24
|
+
supercollab workspace invite --workspace ID [--role member]
|
|
25
|
+
supercollab workspace join --invite TOKEN
|
|
26
|
+
supercollab workspace message --workspace ID --text TEXT
|
|
27
|
+
supercollab workspace messages --workspace ID [--limit 20]
|
|
28
|
+
supercollab workspace search --workspace ID --query TEXT
|
|
29
|
+
supercollab workspace write --workspace ID --path PATH --content TEXT
|
|
30
|
+
supercollab workspace read --workspace ID --path PATH
|
|
31
|
+
supercollab workspace git --workspace ID
|
|
32
|
+
supercollab heartbeat set --workspace ID --prompt TEXT [--interval 900]
|
|
33
|
+
supercollab heartbeat tick --workspace ID
|
|
34
|
+
supercollab mcp stdio
|
|
35
|
+
supercollab mcp print-config --client codex
|
|
36
|
+
supercollab config path
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--config PATH Config path, default ~/.supercollab/config.json
|
|
40
|
+
--server URL SuperCollab API URL, default ${DEFAULT_SERVER}
|
|
41
|
+
|
|
42
|
+
Environment:
|
|
43
|
+
SUPERCOLLAB_PASSWORD can provide password non-interactively.
|
|
44
|
+
SUPERCOLLAB_CONFIG can override config path.
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parse(argv) {
|
|
49
|
+
const positionals = [];
|
|
50
|
+
const opts = {};
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const arg = argv[i];
|
|
53
|
+
if (arg.startsWith('--')) {
|
|
54
|
+
const key = arg.slice(2);
|
|
55
|
+
const next = argv[i + 1];
|
|
56
|
+
if (!next || next.startsWith('--')) {
|
|
57
|
+
opts[key] = true;
|
|
58
|
+
} else {
|
|
59
|
+
opts[key] = next;
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
positionals.push(arg);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { positionals, opts };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function configPath(opts = {}) {
|
|
70
|
+
return opts.config || DEFAULT_CONFIG;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureConfigDir(file) {
|
|
74
|
+
const dir = path.dirname(file);
|
|
75
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
76
|
+
try { fs.chmodSync(dir, 0o700); } catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function loadConfig(file = DEFAULT_CONFIG) {
|
|
80
|
+
if (!fs.existsSync(file)) return { serverUrl: DEFAULT_SERVER };
|
|
81
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
82
|
+
return { serverUrl: DEFAULT_SERVER, ...data };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function saveConfig(config, file = DEFAULT_CONFIG) {
|
|
86
|
+
ensureConfigDir(file);
|
|
87
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
88
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
89
|
+
fs.renameSync(tmp, file);
|
|
90
|
+
try { fs.chmodSync(file, 0o600); } catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function requireValue(opts, key) {
|
|
94
|
+
if (!opts[key] || opts[key] === true) throw new Error(`missing --${key}`);
|
|
95
|
+
return String(opts[key]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readPassword(prompt = 'Password: ') {
|
|
99
|
+
if (process.env.SUPERCOLLAB_PASSWORD) return process.env.SUPERCOLLAB_PASSWORD;
|
|
100
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
|
101
|
+
throw new Error('password required; pass --password or set SUPERCOLLAB_PASSWORD');
|
|
102
|
+
}
|
|
103
|
+
return await new Promise((resolve, reject) => {
|
|
104
|
+
let password = '';
|
|
105
|
+
const stdin = process.stdin;
|
|
106
|
+
const wasRaw = stdin.isRaw;
|
|
107
|
+
const cleanup = () => {
|
|
108
|
+
stdin.off('data', onData);
|
|
109
|
+
try { stdin.setRawMode(Boolean(wasRaw)); } catch {}
|
|
110
|
+
stdin.pause();
|
|
111
|
+
};
|
|
112
|
+
const onData = (chunk) => {
|
|
113
|
+
for (const char of chunk.toString('utf8')) {
|
|
114
|
+
if (char === '\u0003') {
|
|
115
|
+
cleanup();
|
|
116
|
+
output.write('\n');
|
|
117
|
+
reject(new Error('cancelled'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (char === '\r' || char === '\n' || char === '\u0004') {
|
|
121
|
+
cleanup();
|
|
122
|
+
output.write('\n');
|
|
123
|
+
resolve(password);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (char === '\u007f' || char === '\b') {
|
|
127
|
+
if (password.length > 0) {
|
|
128
|
+
password = password.slice(0, -1);
|
|
129
|
+
output.write('\b \b');
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
password += char;
|
|
134
|
+
output.write('*');
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
output.write(prompt);
|
|
138
|
+
stdin.setEncoding('utf8');
|
|
139
|
+
stdin.setRawMode(true);
|
|
140
|
+
stdin.resume();
|
|
141
|
+
stdin.on('data', onData);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function api(config, method, endpoint, body, token) {
|
|
146
|
+
const server = (config.serverUrl || DEFAULT_SERVER).replace(/\/$/, '');
|
|
147
|
+
const headers = { 'content-type': 'application/json', 'user-agent': `supercollab-cli/${VERSION}` };
|
|
148
|
+
const auth = token || config.userToken || config.agentSessionToken;
|
|
149
|
+
if (auth) headers.authorization = `Bearer ${auth}`;
|
|
150
|
+
const res = await fetch(server + endpoint, {
|
|
151
|
+
method,
|
|
152
|
+
headers,
|
|
153
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
154
|
+
});
|
|
155
|
+
const text = await res.text();
|
|
156
|
+
let data = {};
|
|
157
|
+
if (text) {
|
|
158
|
+
try { data = JSON.parse(text); } catch { data = { text }; }
|
|
159
|
+
}
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
const detail = data.detail ? JSON.stringify(data.detail) : text;
|
|
162
|
+
throw new Error(`HTTP ${res.status} ${endpoint}: ${detail}`);
|
|
163
|
+
}
|
|
164
|
+
return data;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function generateAgentKeypair() {
|
|
168
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
169
|
+
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
|
|
170
|
+
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
171
|
+
const raw = publicKey.export({ type: 'spki', format: 'der' });
|
|
172
|
+
const digest = crypto.createHash('sha256').update(raw).digest().subarray(0, 10);
|
|
173
|
+
const fingerprint = 'ed25519:' + digest.toString('base64url').toLowerCase();
|
|
174
|
+
return { publicKeyPem, privateKeyPem, fingerprint };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function signRequest(privateKeyPem, method, endpoint, bodyString, timestamp, nonce) {
|
|
178
|
+
const bodyHash = crypto.createHash('sha256').update(bodyString).digest('hex');
|
|
179
|
+
const signing = Buffer.from(`${method.toUpperCase()}\n${endpoint}\n${bodyHash}\n${timestamp}\n${nonce}`);
|
|
180
|
+
const sig = crypto.sign(null, signing, crypto.createPrivateKey(privateKeyPem)).toString('base64url');
|
|
181
|
+
return sig + '='.repeat((4 - (sig.length % 4)) % 4);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function ensureAgentSession(config) {
|
|
185
|
+
const now = Math.floor(Date.now() / 1000);
|
|
186
|
+
if (config.agentSessionToken && config.agentSessionExpiresAt && config.agentSessionExpiresAt - SESSION_TTL_SKEW > now) {
|
|
187
|
+
return config.agentSessionToken;
|
|
188
|
+
}
|
|
189
|
+
if (!config.agentId || !config.agentPrivateKeyPem) throw new Error('no local agent registered; run supercollab agent register');
|
|
190
|
+
const endpoint = '/v1/agent-sessions';
|
|
191
|
+
const bodyString = JSON.stringify({ agent_id: config.agentId });
|
|
192
|
+
const timestamp = String(now);
|
|
193
|
+
const nonce = crypto.randomBytes(18).toString('base64url');
|
|
194
|
+
const signature = signRequest(config.agentPrivateKeyPem, 'POST', endpoint, bodyString, timestamp, nonce);
|
|
195
|
+
const server = (config.serverUrl || DEFAULT_SERVER).replace(/\/$/, '');
|
|
196
|
+
const res = await fetch(server + endpoint, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
'content-type': 'application/json',
|
|
200
|
+
'user-agent': `supercollab-cli/${VERSION}`,
|
|
201
|
+
'x-supercollab-timestamp': timestamp,
|
|
202
|
+
'x-supercollab-nonce': nonce,
|
|
203
|
+
'x-supercollab-signature': signature,
|
|
204
|
+
},
|
|
205
|
+
body: bodyString,
|
|
206
|
+
});
|
|
207
|
+
const data = await res.json();
|
|
208
|
+
if (!res.ok) throw new Error(`agent session failed: ${JSON.stringify(data)}`);
|
|
209
|
+
config.agentSessionToken = data.token;
|
|
210
|
+
config.agentSessionExpiresAt = data.expires_at;
|
|
211
|
+
return data.token;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function apiAsAgent(config, method, endpoint, body) {
|
|
215
|
+
const token = await ensureAgentSession(config);
|
|
216
|
+
return api(config, method, endpoint, body, token);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function registerAgent(config, label) {
|
|
220
|
+
if (!config.userToken) throw new Error('login/register first');
|
|
221
|
+
const keys = generateAgentKeypair();
|
|
222
|
+
const data = await api(config, 'POST', '/v1/agents/register', { label, public_key_pem: keys.publicKeyPem }, config.userToken);
|
|
223
|
+
config.agentId = data.agent_id;
|
|
224
|
+
config.agentLabel = label;
|
|
225
|
+
config.agentFingerprint = data.fingerprint;
|
|
226
|
+
config.agentPrivateKeyPem = keys.privateKeyPem;
|
|
227
|
+
delete config.agentSessionToken;
|
|
228
|
+
delete config.agentSessionExpiresAt;
|
|
229
|
+
return data;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function doRegister(opts) {
|
|
233
|
+
const file = configPath(opts);
|
|
234
|
+
const config = loadConfig(file);
|
|
235
|
+
config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
|
|
236
|
+
const username = requireValue(opts, 'username');
|
|
237
|
+
const password = opts.password ? String(opts.password) : await readPassword();
|
|
238
|
+
const data = await api(config, 'POST', '/v1/auth/register', { username, password }, null);
|
|
239
|
+
config.userId = data.user_id;
|
|
240
|
+
config.username = data.username;
|
|
241
|
+
config.userToken = data.token;
|
|
242
|
+
const agent = await registerAgent(config, String(opts.label || `${os.hostname()}-agent`));
|
|
243
|
+
saveConfig(config, file);
|
|
244
|
+
console.log(JSON.stringify({ ok: true, username: config.username, user_id: config.userId, agent_id: agent.agent_id, fingerprint: agent.fingerprint, config: file }, null, 2));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function doLogin(opts) {
|
|
248
|
+
const file = configPath(opts);
|
|
249
|
+
const config = loadConfig(file);
|
|
250
|
+
config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
|
|
251
|
+
const username = requireValue(opts, 'username');
|
|
252
|
+
const password = opts.password ? String(opts.password) : await readPassword();
|
|
253
|
+
const data = await api(config, 'POST', '/v1/auth/login', { username, password }, null);
|
|
254
|
+
config.userId = data.user_id;
|
|
255
|
+
config.username = data.username;
|
|
256
|
+
config.userToken = data.token;
|
|
257
|
+
delete config.agentSessionToken;
|
|
258
|
+
delete config.agentSessionExpiresAt;
|
|
259
|
+
saveConfig(config, file);
|
|
260
|
+
console.log(JSON.stringify({ ok: true, username: config.username, user_id: config.userId, config: file }, null, 2));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function toolSchema(name, description, properties = {}, required = []) {
|
|
264
|
+
return { name, description, inputSchema: { type: 'object', properties, required } };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function mcpTools() {
|
|
268
|
+
const s = { type: 'string' };
|
|
269
|
+
return [
|
|
270
|
+
toolSchema('supercollab_status', 'Check SuperCollab server and authenticated actor status.'),
|
|
271
|
+
toolSchema('workspace_list', 'List workspaces visible to this agent.'),
|
|
272
|
+
toolSchema('workspace_create', 'Create a goal-scoped workspace.', { title: s, goal: s, slug: s }, ['title', 'goal']),
|
|
273
|
+
toolSchema('workspace_invite', 'Create a short-lived invite token.', { workspace_id: s, role: s, ttl_seconds: { type: 'integer' } }, ['workspace_id']),
|
|
274
|
+
toolSchema('workspace_join', 'Accept an invite token.', { invite_token: s, fingerprint: s }, ['invite_token']),
|
|
275
|
+
toolSchema('workspace_message_send', 'Send a workspace message.', { workspace_id: s, text: s }, ['workspace_id', 'text']),
|
|
276
|
+
toolSchema('workspace_messages_read', 'Read workspace events/messages.', { workspace_id: s, limit: { type: 'integer' } }, ['workspace_id']),
|
|
277
|
+
toolSchema('workspace_file_list', 'List workspace files.', { workspace_id: s, path: s }, ['workspace_id']),
|
|
278
|
+
toolSchema('workspace_file_read', 'Read a workspace file.', { workspace_id: s, path: s }, ['workspace_id', 'path']),
|
|
279
|
+
toolSchema('workspace_file_write', 'Write a workspace file and commit it.', { workspace_id: s, path: s, content: s, commit_message: s }, ['workspace_id', 'path', 'content']),
|
|
280
|
+
toolSchema('workspace_git_status', 'Show workspace Git status and recent commits.', { workspace_id: s }, ['workspace_id']),
|
|
281
|
+
toolSchema('workspace_task_create', 'Create a task.', { workspace_id: s, title: s, body: s, owner: s }, ['workspace_id', 'title']),
|
|
282
|
+
toolSchema('workspace_decision_record', 'Record a decision.', { workspace_id: s, title: s, decision: s, rationale: s }, ['workspace_id', 'title', 'decision']),
|
|
283
|
+
toolSchema('workspace_memory_reindex', 'Rebuild workspace search index.', { workspace_id: s }, ['workspace_id']),
|
|
284
|
+
toolSchema('workspace_memory_search', 'Search workspace memory.', { workspace_id: s, query: s, limit: { type: 'integer' } }, ['workspace_id', 'query']),
|
|
285
|
+
toolSchema('workspace_heartbeat_get', 'Read heartbeat config.', { workspace_id: s }, ['workspace_id']),
|
|
286
|
+
toolSchema('workspace_heartbeat_set', 'Set heartbeat config.', { workspace_id: s, prompt: s, interval_seconds: { type: 'integer' }, enabled: { type: 'boolean' } }, ['workspace_id', 'prompt']),
|
|
287
|
+
toolSchema('workspace_heartbeat_tick', 'Emit heartbeat now.', { workspace_id: s }, ['workspace_id']),
|
|
288
|
+
];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function callTool(config, name, args) {
|
|
292
|
+
if (name === 'supercollab_status') return { health: await api(config, 'GET', '/health'), me: await apiAsAgent(config, 'GET', '/v1/me') };
|
|
293
|
+
if (name === 'workspace_list') return apiAsAgent(config, 'GET', '/v1/workspaces');
|
|
294
|
+
if (name === 'workspace_create') return apiAsAgent(config, 'POST', '/v1/workspaces', { title: args.title, goal: args.goal, slug: args.slug });
|
|
295
|
+
if (name === 'workspace_invite') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/invites`, { role: args.role || 'member', ttl_seconds: args.ttl_seconds || 86400 });
|
|
296
|
+
if (name === 'workspace_join') return apiAsAgent(config, 'POST', '/v1/invites/accept', { token: args.invite_token, fingerprint: args.fingerprint || config.agentFingerprint });
|
|
297
|
+
if (name === 'workspace_message_send') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/messages`, { text: args.text });
|
|
298
|
+
if (name === 'workspace_messages_read') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/messages?limit=${encodeURIComponent(args.limit || 50)}`);
|
|
299
|
+
if (name === 'workspace_file_list') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/files?path=${encodeURIComponent(args.path || '')}`);
|
|
300
|
+
if (name === 'workspace_file_read') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/files/read?path=${encodeURIComponent(args.path)}`);
|
|
301
|
+
if (name === 'workspace_file_write') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/files/write`, { path: args.path, content: args.content, commit_message: args.commit_message });
|
|
302
|
+
if (name === 'workspace_git_status') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/git/status`);
|
|
303
|
+
if (name === 'workspace_task_create') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/tasks`, { title: args.title, body: args.body || '', owner: args.owner || 'unassigned' });
|
|
304
|
+
if (name === 'workspace_decision_record') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/decisions`, { title: args.title, decision: args.decision, rationale: args.rationale || '' });
|
|
305
|
+
if (name === 'workspace_memory_reindex') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/memory/reindex`, {});
|
|
306
|
+
if (name === 'workspace_memory_search') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/memory/search?q=${encodeURIComponent(args.query)}&limit=${encodeURIComponent(args.limit || 8)}`);
|
|
307
|
+
if (name === 'workspace_heartbeat_get') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/heartbeat`);
|
|
308
|
+
if (name === 'workspace_heartbeat_set') return apiAsAgent(config, 'PUT', `/v1/workspaces/${args.workspace_id}/heartbeat`, { prompt: args.prompt, interval_seconds: args.interval_seconds || 900, enabled: args.enabled !== false });
|
|
309
|
+
if (name === 'workspace_heartbeat_tick') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/heartbeat/tick`, {});
|
|
310
|
+
throw new Error(`unknown tool: ${name}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function writeRpc(payload) {
|
|
314
|
+
const raw = Buffer.from(JSON.stringify(payload));
|
|
315
|
+
process.stdout.write(`Content-Length: ${raw.length}\r\n\r\n`);
|
|
316
|
+
process.stdout.write(raw);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function runMcp(opts) {
|
|
320
|
+
const config = loadConfig(configPath(opts));
|
|
321
|
+
let buffer = Buffer.alloc(0);
|
|
322
|
+
for await (const chunk of process.stdin) {
|
|
323
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
324
|
+
while (true) {
|
|
325
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
326
|
+
if (headerEnd < 0) break;
|
|
327
|
+
const header = buffer.subarray(0, headerEnd).toString();
|
|
328
|
+
const match = header.match(/content-length:\s*(\d+)/i);
|
|
329
|
+
if (!match) throw new Error('missing content-length');
|
|
330
|
+
const length = Number(match[1]);
|
|
331
|
+
const total = headerEnd + 4 + length;
|
|
332
|
+
if (buffer.length < total) break;
|
|
333
|
+
const body = buffer.subarray(headerEnd + 4, total).toString();
|
|
334
|
+
buffer = buffer.subarray(total);
|
|
335
|
+
const msg = JSON.parse(body);
|
|
336
|
+
const id = msg.id;
|
|
337
|
+
try {
|
|
338
|
+
let result;
|
|
339
|
+
if (msg.method === 'initialize') {
|
|
340
|
+
result = { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'supercollab', version: VERSION } };
|
|
341
|
+
} else if (msg.method === 'tools/list') {
|
|
342
|
+
result = { tools: mcpTools() };
|
|
343
|
+
} else if (msg.method === 'tools/call') {
|
|
344
|
+
const data = await callTool(config, msg.params.name, msg.params.arguments || {});
|
|
345
|
+
result = { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
346
|
+
} else if (msg.method && msg.method.startsWith('notifications/')) {
|
|
347
|
+
continue;
|
|
348
|
+
} else {
|
|
349
|
+
throw new Error(`unsupported method: ${msg.method}`);
|
|
350
|
+
}
|
|
351
|
+
if (id !== undefined) writeRpc({ jsonrpc: '2.0', id, result });
|
|
352
|
+
} catch (err) {
|
|
353
|
+
if (id !== undefined) writeRpc({ jsonrpc: '2.0', id, error: { code: -32000, message: err.message } });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function printCodexConfig(opts) {
|
|
360
|
+
const file = configPath(opts);
|
|
361
|
+
console.log(`[mcp_servers.supercollab]\ncommand = "supercollab"\nargs = ["mcp", "stdio", "--config", "${file.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"]`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function main() {
|
|
365
|
+
const { positionals, opts } = parse(process.argv.slice(2));
|
|
366
|
+
if (opts.help || positionals.length === 0) { printHelp(); return; }
|
|
367
|
+
const [cmd, sub] = positionals;
|
|
368
|
+
const file = configPath(opts);
|
|
369
|
+
const config = loadConfig(file);
|
|
370
|
+
config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
|
|
371
|
+
|
|
372
|
+
if (cmd === 'register') return doRegister(opts);
|
|
373
|
+
if (cmd === 'login') return doLogin(opts);
|
|
374
|
+
if (cmd === 'whoami') return console.log(JSON.stringify(await api(config, 'GET', '/v1/me'), null, 2));
|
|
375
|
+
if (cmd === 'config' && sub === 'path') return console.log(file);
|
|
376
|
+
if (cmd === 'agent' && sub === 'register') {
|
|
377
|
+
const data = await registerAgent(config, String(opts.label || `${os.hostname()}-agent`));
|
|
378
|
+
saveConfig(config, file);
|
|
379
|
+
return console.log(JSON.stringify({ ok: true, agent_id: data.agent_id, fingerprint: data.fingerprint, config: file }, null, 2));
|
|
380
|
+
}
|
|
381
|
+
if (cmd === 'mcp' && sub === 'stdio') return runMcp(opts);
|
|
382
|
+
if (cmd === 'mcp' && sub === 'print-config') return printCodexConfig(opts);
|
|
383
|
+
if (cmd === 'workspace') {
|
|
384
|
+
if (sub === 'list') return console.log(JSON.stringify(await callTool(config, 'workspace_list', {}), null, 2));
|
|
385
|
+
if (sub === 'create') return console.log(JSON.stringify(await callTool(config, 'workspace_create', { title: requireValue(opts, 'title'), goal: requireValue(opts, 'goal'), slug: opts.slug }), null, 2));
|
|
386
|
+
if (sub === 'invite') return console.log(JSON.stringify(await callTool(config, 'workspace_invite', { workspace_id: requireValue(opts, 'workspace'), role: opts.role || 'member', ttl_seconds: opts.ttl || 86400 }), null, 2));
|
|
387
|
+
if (sub === 'join') return console.log(JSON.stringify(await callTool(config, 'workspace_join', { invite_token: requireValue(opts, 'invite') }), null, 2));
|
|
388
|
+
if (sub === 'message') return console.log(JSON.stringify(await callTool(config, 'workspace_message_send', { workspace_id: requireValue(opts, 'workspace'), text: requireValue(opts, 'text') }), null, 2));
|
|
389
|
+
if (sub === 'messages') return console.log(JSON.stringify(await callTool(config, 'workspace_messages_read', { workspace_id: requireValue(opts, 'workspace'), limit: opts.limit || 20 }), null, 2));
|
|
390
|
+
if (sub === 'search') return console.log(JSON.stringify(await callTool(config, 'workspace_memory_search', { workspace_id: requireValue(opts, 'workspace'), query: requireValue(opts, 'query'), limit: opts.limit || 8 }), null, 2));
|
|
391
|
+
if (sub === 'write') return console.log(JSON.stringify(await callTool(config, 'workspace_file_write', { workspace_id: requireValue(opts, 'workspace'), path: requireValue(opts, 'path'), content: requireValue(opts, 'content'), commit_message: opts.message }), null, 2));
|
|
392
|
+
if (sub === 'read') return console.log(JSON.stringify(await callTool(config, 'workspace_file_read', { workspace_id: requireValue(opts, 'workspace'), path: requireValue(opts, 'path') }), null, 2));
|
|
393
|
+
if (sub === 'git') return console.log(JSON.stringify(await callTool(config, 'workspace_git_status', { workspace_id: requireValue(opts, 'workspace') }), null, 2));
|
|
394
|
+
}
|
|
395
|
+
if (cmd === 'heartbeat') {
|
|
396
|
+
if (sub === 'set') return console.log(JSON.stringify(await callTool(config, 'workspace_heartbeat_set', { workspace_id: requireValue(opts, 'workspace'), prompt: requireValue(opts, 'prompt'), interval_seconds: opts.interval || 900, enabled: opts.enabled !== 'false' }), null, 2));
|
|
397
|
+
if (sub === 'tick') return console.log(JSON.stringify(await callTool(config, 'workspace_heartbeat_tick', { workspace_id: requireValue(opts, 'workspace') }), null, 2));
|
|
398
|
+
}
|
|
399
|
+
throw new Error(`unknown command: ${positionals.join(' ')}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
main().catch((err) => {
|
|
403
|
+
console.error(`supercollab: ${err.message}`);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
});
|