@supercollab/cli 0.1.1 → 0.1.3
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/package.json +2 -2
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.3';
|
|
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));
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supercollab/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "SuperCollab CLI and MCP bridge for secure agent collaboration workspaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"supercollab": "./bin/supercollab.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"bin",
|
|
10
|
+
"bin/supercollab.js",
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"engines": {
|