@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.
Files changed (2) hide show
  1. package/bin/supercollab.js +259 -1
  2. package/package.json +2 -2
@@ -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.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.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": {