@supercollab/cli 0.4.3 → 0.4.4
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/README.md +27 -5
- package/bin/supercollab.js +780 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,11 +15,33 @@ npm install -g @supercollab/cli
|
|
|
15
15
|
supercollab
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Running `supercollab` opens
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
Running `supercollab` opens guided setup the first time, then opens the full
|
|
19
|
+
interactive menu after your account and local agent are configured. You can also
|
|
20
|
+
open the menu directly:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
supercollab menu
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The menu is designed so normal use does not require memorizing command-line
|
|
27
|
+
flags. It includes scrollable/selectable flows for:
|
|
28
|
+
|
|
29
|
+
- creating a room
|
|
30
|
+
- joining with a private invite
|
|
31
|
+
- choosing an existing room
|
|
32
|
+
- typing a room ID manually when needed
|
|
33
|
+
- activating a room for a local project directory
|
|
34
|
+
- creating private invites
|
|
35
|
+
- sending, reading, syncing, and searching room messages
|
|
36
|
+
- running the local system check and BGE model install/warmup
|
|
37
|
+
- viewing MCP config snippets
|
|
38
|
+
- managing sessions and server settings
|
|
39
|
+
|
|
40
|
+
Guided setup detects your OS/CPU/Node runtime, verifies native SQLite and
|
|
41
|
+
sqlite-vec, downloads and warms the BGE model locally, writes the selected local
|
|
42
|
+
engine into `~/.supercollab/config.json`, creates or logs into your account,
|
|
43
|
+
registers the local agent, creates/joins/selects a room, activates a project
|
|
44
|
+
directory, and prints MCP config.
|
|
23
45
|
|
|
24
46
|
You can also run the checks directly:
|
|
25
47
|
|
package/bin/supercollab.js
CHANGED
|
@@ -7,7 +7,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
7
7
|
import * as readlineCore from 'node:readline';
|
|
8
8
|
import { stdin as input, stdout as output } from 'node:process';
|
|
9
9
|
|
|
10
|
-
const VERSION = '0.4.
|
|
10
|
+
const VERSION = '0.4.4';
|
|
11
11
|
const DEFAULT_SERVER = process.env.SUPERCOLLAB_URL || 'https://hyper.polynode.dev';
|
|
12
12
|
const DEFAULT_CONFIG = process.env.SUPERCOLLAB_CONFIG || path.join(os.homedir(), '.supercollab', 'config.json');
|
|
13
13
|
const SESSION_TTL_SKEW = 60;
|
|
@@ -34,6 +34,7 @@ function printHelp() {
|
|
|
34
34
|
console.log(`SuperCollab CLI ${VERSION}
|
|
35
35
|
|
|
36
36
|
Usage:
|
|
37
|
+
supercollab menu
|
|
37
38
|
supercollab setup
|
|
38
39
|
supercollab doctor [--json] [--skip-model]
|
|
39
40
|
supercollab register --username NAME [--password PASS] [--label LABEL]
|
|
@@ -1302,6 +1303,234 @@ function promptRequired(value) {
|
|
|
1302
1303
|
return String(value || '').trim() ? undefined : 'Required';
|
|
1303
1304
|
}
|
|
1304
1305
|
|
|
1306
|
+
function trimmed(value) {
|
|
1307
|
+
return String(value || '').trim();
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function shorten(value, max = 80) {
|
|
1311
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
1312
|
+
if (text.length <= max) return text;
|
|
1313
|
+
return `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function roomIdFrom(room) {
|
|
1317
|
+
return trimmed(room?.room_id || room?.id || room?.workspace_id);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function roomTitleFrom(room) {
|
|
1321
|
+
return trimmed(room?.title || room?.name || room?.slug || room?.goal || roomIdFrom(room));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function roomsFromResponse(data) {
|
|
1325
|
+
if (Array.isArray(data)) return data;
|
|
1326
|
+
if (Array.isArray(data?.rooms)) return data.rooms;
|
|
1327
|
+
if (Array.isArray(data?.workspaces)) return data.workspaces;
|
|
1328
|
+
if (Array.isArray(data?.items)) return data.items;
|
|
1329
|
+
return [];
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function hasLocalRoomKey(config, roomId) {
|
|
1333
|
+
return Boolean(config.roomKeys?.[roomId]);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function roomLabel(room, config) {
|
|
1337
|
+
const roomId = roomIdFrom(room);
|
|
1338
|
+
const title = roomTitleFrom(room);
|
|
1339
|
+
const prefix = title && title !== roomId ? `${shorten(title, 42)} ` : '';
|
|
1340
|
+
const key = hasLocalRoomKey(config, roomId) ? 'key saved' : 'key missing';
|
|
1341
|
+
return `${prefix}(${roomId})`;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function roomHint(room, config) {
|
|
1345
|
+
const roomId = roomIdFrom(room);
|
|
1346
|
+
const pieces = [];
|
|
1347
|
+
const goal = trimmed(room?.goal || room?.description);
|
|
1348
|
+
if (goal) pieces.push(shorten(goal, 48));
|
|
1349
|
+
pieces.push(hasLocalRoomKey(config, roomId) ? 'encrypted local search ready' : 'join/import key to decrypt');
|
|
1350
|
+
return pieces.join(' - ');
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
async function listRoomsForMenu(config) {
|
|
1354
|
+
const data = await apiAsAgent(config, 'GET', '/v1/rooms');
|
|
1355
|
+
return roomsFromResponse(data).filter((room) => roomIdFrom(room));
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
async function promptManualRoomId(prompts, message = 'Room ID') {
|
|
1359
|
+
const roomId = await prompts.text({
|
|
1360
|
+
message,
|
|
1361
|
+
placeholder: 'room_...',
|
|
1362
|
+
validate: promptRequired,
|
|
1363
|
+
});
|
|
1364
|
+
if (prompts.isCancel(roomId)) throw new Error('cancelled');
|
|
1365
|
+
return trimmed(roomId);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
async function ensureRoomKeyFromMenu(config, file, prompts, roomId) {
|
|
1369
|
+
try {
|
|
1370
|
+
ensureRoomKey(config, roomId);
|
|
1371
|
+
return true;
|
|
1372
|
+
} catch {
|
|
1373
|
+
const choice = await prompts.select({
|
|
1374
|
+
message: `No local room key for ${roomId}`,
|
|
1375
|
+
options: [
|
|
1376
|
+
{ value: 'invite', label: 'Join with private invite', hint: 'recommended if another member invited you' },
|
|
1377
|
+
{ value: 'key', label: 'Paste room key', hint: 'sck_... from a trusted device' },
|
|
1378
|
+
{ value: 'choose', label: 'Choose another room' },
|
|
1379
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
1380
|
+
],
|
|
1381
|
+
});
|
|
1382
|
+
if (prompts.isCancel(choice) || choice === 'cancel') throw new Error('cancelled');
|
|
1383
|
+
if (choice === 'choose') return false;
|
|
1384
|
+
if (choice === 'invite') {
|
|
1385
|
+
const joined = await promptJoinRoom(config, file, prompts, { quiet: true });
|
|
1386
|
+
return Boolean(joined?.room_id === roomId || joined?.workspace_id === roomId || hasLocalRoomKey(config, roomId));
|
|
1387
|
+
}
|
|
1388
|
+
const key = await prompts.text({
|
|
1389
|
+
message: 'Paste room key',
|
|
1390
|
+
placeholder: 'sck_...',
|
|
1391
|
+
validate: (value) => {
|
|
1392
|
+
try {
|
|
1393
|
+
roomKeyBytes(trimmed(value));
|
|
1394
|
+
return undefined;
|
|
1395
|
+
} catch {
|
|
1396
|
+
return 'Invalid room key';
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1399
|
+
});
|
|
1400
|
+
if (prompts.isCancel(key)) throw new Error('cancelled');
|
|
1401
|
+
storeRoomKey(config, roomId, trimmed(key));
|
|
1402
|
+
saveConfig(config, file);
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
async function selectRoom(config, file, prompts, options = {}) {
|
|
1408
|
+
const {
|
|
1409
|
+
message = 'Choose room',
|
|
1410
|
+
requireKey = true,
|
|
1411
|
+
includeCreate = false,
|
|
1412
|
+
includeJoin = false,
|
|
1413
|
+
includeManual = true,
|
|
1414
|
+
} = options;
|
|
1415
|
+
|
|
1416
|
+
while (true) {
|
|
1417
|
+
let rooms = [];
|
|
1418
|
+
try {
|
|
1419
|
+
rooms = await listRoomsForMenu(config);
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
prompts.note(err.message || String(err), 'Could not load rooms');
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const choices = rooms.map((room) => ({
|
|
1425
|
+
value: roomIdFrom(room),
|
|
1426
|
+
label: roomLabel(room, config),
|
|
1427
|
+
hint: roomHint(room, config),
|
|
1428
|
+
}));
|
|
1429
|
+
if (includeCreate) choices.push({ value: '__create', label: 'Create new room', hint: 'set title and goal now' });
|
|
1430
|
+
if (includeJoin) choices.push({ value: '__join', label: 'Join with private invite', hint: 'paste sci_...sck_...' });
|
|
1431
|
+
if (includeManual) choices.push({ value: '__manual', label: 'Type room ID manually', hint: 'room_...' });
|
|
1432
|
+
choices.push({ value: '__back', label: 'Back' });
|
|
1433
|
+
|
|
1434
|
+
const selected = await prompts.select({ message, options: choices });
|
|
1435
|
+
if (prompts.isCancel(selected) || selected === '__back') throw new Error('cancelled');
|
|
1436
|
+
|
|
1437
|
+
if (selected === '__create') {
|
|
1438
|
+
const created = await promptCreateRoom(config, file, prompts, { quiet: true });
|
|
1439
|
+
if (created?.room_id) return created.room_id;
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
if (selected === '__join') {
|
|
1443
|
+
const joined = await promptJoinRoom(config, file, prompts, { quiet: true });
|
|
1444
|
+
const joinedRoomId = joined?.room_id || joined?.workspace_id;
|
|
1445
|
+
if (joinedRoomId) return joinedRoomId;
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const roomId = selected === '__manual'
|
|
1450
|
+
? await promptManualRoomId(prompts)
|
|
1451
|
+
: String(selected);
|
|
1452
|
+
|
|
1453
|
+
if (!requireKey) return roomId;
|
|
1454
|
+
const ok = await ensureRoomKeyFromMenu(config, file, prompts, roomId);
|
|
1455
|
+
if (ok) return roomId;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function formatMessagesForNote(messages, maxRows = 12) {
|
|
1460
|
+
const rows = (messages || []).slice(-maxRows);
|
|
1461
|
+
if (!rows.length) return 'No messages yet.';
|
|
1462
|
+
return rows.map((row) => {
|
|
1463
|
+
const when = trimmed(row.created_at).replace('T', ' ').replace('Z', '');
|
|
1464
|
+
return `[${when}] ${row.sender_label || row.actor_id || 'agent'}\n${shorten(row.body, 260)}`;
|
|
1465
|
+
}).join('\n\n');
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function formatSearchForNote(results, maxRows = 8) {
|
|
1469
|
+
const rows = (results || []).slice(0, maxRows);
|
|
1470
|
+
if (!rows.length) return 'No matching messages.';
|
|
1471
|
+
return rows.map((row, idx) => {
|
|
1472
|
+
const sources = Array.isArray(row.search_sources) ? row.search_sources.join('+') : 'match';
|
|
1473
|
+
return `${idx + 1}. ${row.sender_label || row.actor_id || 'agent'} - ${sources}\n${shorten(row.body, 260)}`;
|
|
1474
|
+
}).join('\n\n');
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
async function promptCreateRoom(config, file, prompts, options = {}) {
|
|
1478
|
+
const title = await prompts.text({
|
|
1479
|
+
message: 'Room title',
|
|
1480
|
+
placeholder: 'Launch Room',
|
|
1481
|
+
validate: promptRequired,
|
|
1482
|
+
});
|
|
1483
|
+
if (prompts.isCancel(title)) throw new Error('cancelled');
|
|
1484
|
+
const goal = await prompts.text({
|
|
1485
|
+
message: 'Room goal',
|
|
1486
|
+
placeholder: 'Coordinate agents on this project',
|
|
1487
|
+
validate: promptRequired,
|
|
1488
|
+
});
|
|
1489
|
+
if (prompts.isCancel(goal)) throw new Error('cancelled');
|
|
1490
|
+
const slug = await prompts.text({
|
|
1491
|
+
message: 'Optional short slug',
|
|
1492
|
+
placeholder: 'press Enter to skip',
|
|
1493
|
+
});
|
|
1494
|
+
if (prompts.isCancel(slug)) throw new Error('cancelled');
|
|
1495
|
+
const spin = prompts.spinner();
|
|
1496
|
+
spin.start('Creating encrypted room');
|
|
1497
|
+
try {
|
|
1498
|
+
const created = await doRoomCreate(config, file, {
|
|
1499
|
+
title: trimmed(title),
|
|
1500
|
+
goal: trimmed(goal),
|
|
1501
|
+
slug: trimmed(slug) || undefined,
|
|
1502
|
+
});
|
|
1503
|
+
const roomId = created.room_id || created.id;
|
|
1504
|
+
spin.stop(`Room created: ${roomId}`);
|
|
1505
|
+
if (!options.quiet) prompts.note(`Room ID: ${roomId}\nLocal room key: saved on this machine`, 'Room created');
|
|
1506
|
+
return { ...created, room_id: roomId };
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
spin.stop('Room creation failed');
|
|
1509
|
+
throw err;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
async function promptJoinRoom(config, file, prompts, options = {}) {
|
|
1514
|
+
const invite = await prompts.text({
|
|
1515
|
+
message: 'Paste private invite',
|
|
1516
|
+
placeholder: 'sci_....sck_...',
|
|
1517
|
+
validate: promptRequired,
|
|
1518
|
+
});
|
|
1519
|
+
if (prompts.isCancel(invite)) throw new Error('cancelled');
|
|
1520
|
+
const spin = prompts.spinner();
|
|
1521
|
+
spin.start('Joining room and saving local room key');
|
|
1522
|
+
try {
|
|
1523
|
+
const joined = await doRoomJoin(config, file, { invite: trimmed(invite) });
|
|
1524
|
+
const roomId = joined.room_id || joined.workspace_id;
|
|
1525
|
+
spin.stop(`Joined room: ${roomId}`);
|
|
1526
|
+
if (!options.quiet) prompts.note(`Room ID: ${roomId}\nLocal room key saved: ${joined.room_key_saved ? 'yes' : 'no'}`, 'Joined');
|
|
1527
|
+
return { ...joined, room_id: roomId };
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
spin.stop('Join failed');
|
|
1530
|
+
throw err;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1305
1534
|
async function ensureAgentForSetup(config, file, prompts) {
|
|
1306
1535
|
if (config.agentId && config.agentPrivateKeyPem) return null;
|
|
1307
1536
|
const label = await prompts.text({
|
|
@@ -1360,6 +1589,7 @@ async function runRoomSetup(config, file, prompts) {
|
|
|
1360
1589
|
const choice = await prompts.select({
|
|
1361
1590
|
message: 'Room setup',
|
|
1362
1591
|
options: [
|
|
1592
|
+
{ value: 'existing', label: 'Use an existing room', hint: 'pick from your rooms or type a room ID' },
|
|
1363
1593
|
{ value: 'create', label: 'Create a new room', hint: 'start solo or invite agents later' },
|
|
1364
1594
|
{ value: 'join', label: 'Join with private invite', hint: 'paste sci_...sck_...' },
|
|
1365
1595
|
{ value: 'skip', label: 'Skip for now', hint: 'set up auth and local engine only' },
|
|
@@ -1368,35 +1598,40 @@ async function runRoomSetup(config, file, prompts) {
|
|
|
1368
1598
|
if (prompts.isCancel(choice)) throw new Error('cancelled');
|
|
1369
1599
|
if (choice === 'skip') return null;
|
|
1370
1600
|
|
|
1371
|
-
if (choice === '
|
|
1372
|
-
const
|
|
1373
|
-
message: '
|
|
1374
|
-
|
|
1375
|
-
|
|
1601
|
+
if (choice === 'existing') {
|
|
1602
|
+
const roomId = await selectRoom(config, file, prompts, {
|
|
1603
|
+
message: 'Choose room to use',
|
|
1604
|
+
requireKey: true,
|
|
1605
|
+
includeCreate: true,
|
|
1606
|
+
includeJoin: true,
|
|
1376
1607
|
});
|
|
1377
|
-
|
|
1378
|
-
|
|
1608
|
+
return { room_id: roomId, existing: true };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (choice === 'join') {
|
|
1612
|
+
const joined = await promptJoinRoom(config, file, prompts, { quiet: true });
|
|
1379
1613
|
return { room_id: joined.room_id || joined.workspace_id, joined };
|
|
1380
1614
|
}
|
|
1381
1615
|
|
|
1382
|
-
const
|
|
1383
|
-
message: 'Room title',
|
|
1384
|
-
placeholder: 'Launch Room',
|
|
1385
|
-
validate: promptRequired,
|
|
1386
|
-
});
|
|
1387
|
-
if (prompts.isCancel(title)) throw new Error('cancelled');
|
|
1388
|
-
const goal = await prompts.text({
|
|
1389
|
-
message: 'Room goal',
|
|
1390
|
-
placeholder: 'Coordinate agents on this project',
|
|
1391
|
-
validate: promptRequired,
|
|
1392
|
-
});
|
|
1393
|
-
if (prompts.isCancel(goal)) throw new Error('cancelled');
|
|
1394
|
-
const created = await doRoomCreate(config, file, { title: String(title), goal: String(goal) });
|
|
1616
|
+
const created = await promptCreateRoom(config, file, prompts, { quiet: true });
|
|
1395
1617
|
return { room_id: created.room_id || created.id, created };
|
|
1396
1618
|
}
|
|
1397
1619
|
|
|
1398
1620
|
async function runActivationSetup(config, file, roomId, prompts) {
|
|
1399
|
-
if (!roomId)
|
|
1621
|
+
if (!roomId) {
|
|
1622
|
+
const pick = await prompts.confirm({
|
|
1623
|
+
message: 'Activate an existing room for this project directory?',
|
|
1624
|
+
initialValue: false,
|
|
1625
|
+
});
|
|
1626
|
+
if (prompts.isCancel(pick)) throw new Error('cancelled');
|
|
1627
|
+
if (!pick) return null;
|
|
1628
|
+
roomId = await selectRoom(config, file, prompts, {
|
|
1629
|
+
message: 'Choose room to activate',
|
|
1630
|
+
requireKey: true,
|
|
1631
|
+
includeCreate: true,
|
|
1632
|
+
includeJoin: true,
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1400
1635
|
const shouldActivate = await prompts.confirm({
|
|
1401
1636
|
message: 'Activate SuperCollab for a local project directory now?',
|
|
1402
1637
|
initialValue: true,
|
|
@@ -1453,6 +1688,523 @@ function mcpConfigText(client, file) {
|
|
|
1453
1688
|
return `supercollab mcp stdio --config "${escaped}"`;
|
|
1454
1689
|
}
|
|
1455
1690
|
|
|
1691
|
+
async function promptSystemCheck(config, file, prompts) {
|
|
1692
|
+
const spin = prompts.spinner();
|
|
1693
|
+
spin.start('Checking this machine and warming the local BGE model');
|
|
1694
|
+
const doctor = await runDoctor(config, file, {});
|
|
1695
|
+
spin.stop(doctor.ok ? 'Local engine ready' : 'Local engine needs attention');
|
|
1696
|
+
const checks = [
|
|
1697
|
+
`Node: ${doctor.checks.node_supported.ok ? 'ok' : 'needs Node >=20'} (${doctor.system.node})`,
|
|
1698
|
+
`SQLite vector engine: ${doctor.checks.native_sqlite_vec.ok ? 'ok' : 'failed'}${doctor.checks.native_sqlite_vec.sqlite_vec_version ? ` (${doctor.checks.native_sqlite_vec.sqlite_vec_version})` : ''}`,
|
|
1699
|
+
`BGE model: ${doctor.checks.bge_model.ok ? 'ok' : doctor.checks.bge_model.skipped ? 'skipped' : 'failed'}`,
|
|
1700
|
+
`Embedding profile: ${doctor.local_engine.embedding_profile_id}`,
|
|
1701
|
+
];
|
|
1702
|
+
if (doctor.advice?.length) checks.push('', ...doctor.advice);
|
|
1703
|
+
prompts.note(checks.join('\n'), doctor.ok ? 'Doctor passed' : 'Doctor');
|
|
1704
|
+
return doctor;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
async function promptActivateRoom(config, file, prompts, options = {}) {
|
|
1708
|
+
const roomId = options.roomId || await selectRoom(config, file, prompts, {
|
|
1709
|
+
message: 'Choose room to activate',
|
|
1710
|
+
requireKey: true,
|
|
1711
|
+
includeCreate: true,
|
|
1712
|
+
includeJoin: true,
|
|
1713
|
+
});
|
|
1714
|
+
const cwd = await prompts.text({
|
|
1715
|
+
message: 'Project directory',
|
|
1716
|
+
defaultValue: options.cwd || process.cwd(),
|
|
1717
|
+
placeholder: options.cwd || process.cwd(),
|
|
1718
|
+
validate: (value) => {
|
|
1719
|
+
const resolved = path.resolve(String(value || ''));
|
|
1720
|
+
return fs.existsSync(resolved) ? undefined : 'Directory does not exist';
|
|
1721
|
+
},
|
|
1722
|
+
});
|
|
1723
|
+
if (prompts.isCancel(cwd)) throw new Error('cancelled');
|
|
1724
|
+
const activation = activate(config, file, { room: roomId, cwd: String(cwd) });
|
|
1725
|
+
prompts.note(`${activation.instructions}\n\nActivation root: ${activation.cwd}`, 'Workspace active');
|
|
1726
|
+
return activation;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
async function promptDeactivateRoom(config, file, prompts, options = {}) {
|
|
1730
|
+
const cwd = await prompts.text({
|
|
1731
|
+
message: 'Directory to deactivate',
|
|
1732
|
+
defaultValue: options.cwd || process.cwd(),
|
|
1733
|
+
placeholder: options.cwd || process.cwd(),
|
|
1734
|
+
validate: (value) => fs.existsSync(path.resolve(String(value || ''))) ? undefined : 'Directory does not exist',
|
|
1735
|
+
});
|
|
1736
|
+
if (prompts.isCancel(cwd)) throw new Error('cancelled');
|
|
1737
|
+
const result = deactivate(config, file, { cwd: String(cwd) });
|
|
1738
|
+
prompts.note(`SuperCollab is off for ${result.cwd}`, 'Workspace deactivated');
|
|
1739
|
+
return result;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
async function promptCreateInvite(config, file, prompts, options = {}) {
|
|
1743
|
+
const roomId = options.roomId || await selectRoom(config, file, prompts, {
|
|
1744
|
+
message: 'Choose room to invite into',
|
|
1745
|
+
requireKey: true,
|
|
1746
|
+
includeCreate: true,
|
|
1747
|
+
includeJoin: false,
|
|
1748
|
+
});
|
|
1749
|
+
const role = await prompts.select({
|
|
1750
|
+
message: 'Invite role',
|
|
1751
|
+
options: [
|
|
1752
|
+
{ value: 'member', label: 'Member', hint: 'normal agent/user access' },
|
|
1753
|
+
],
|
|
1754
|
+
});
|
|
1755
|
+
if (prompts.isCancel(role)) throw new Error('cancelled');
|
|
1756
|
+
const ttl = await prompts.select({
|
|
1757
|
+
message: 'Invite expiry',
|
|
1758
|
+
options: [
|
|
1759
|
+
{ value: 86400, label: '24 hours' },
|
|
1760
|
+
{ value: 3600, label: '1 hour' },
|
|
1761
|
+
{ value: 604800, label: '7 days' },
|
|
1762
|
+
],
|
|
1763
|
+
});
|
|
1764
|
+
if (prompts.isCancel(ttl)) throw new Error('cancelled');
|
|
1765
|
+
const spin = prompts.spinner();
|
|
1766
|
+
spin.start('Creating private invite');
|
|
1767
|
+
try {
|
|
1768
|
+
const data = await doRoomInvite(config, { room: roomId, role, ttl_seconds: ttl });
|
|
1769
|
+
spin.stop('Private invite created');
|
|
1770
|
+
prompts.note(data.private_invite, 'Share this private invite');
|
|
1771
|
+
return data;
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
spin.stop('Invite failed');
|
|
1774
|
+
throw err;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
async function promptSendMessage(config, file, prompts, options = {}) {
|
|
1779
|
+
const roomId = options.roomId || await selectRoom(config, file, prompts, {
|
|
1780
|
+
message: 'Choose room to message',
|
|
1781
|
+
requireKey: true,
|
|
1782
|
+
includeCreate: true,
|
|
1783
|
+
includeJoin: true,
|
|
1784
|
+
});
|
|
1785
|
+
let channel = await prompts.select({
|
|
1786
|
+
message: 'Channel',
|
|
1787
|
+
options: [
|
|
1788
|
+
{ value: 'agents', label: 'Agents', hint: 'default coordination chat' },
|
|
1789
|
+
{ value: 'progress', label: 'Progress', hint: 'status notes' },
|
|
1790
|
+
{ value: 'decisions', label: 'Decisions', hint: 'architecture/product decisions' },
|
|
1791
|
+
{ value: 'blockers', label: 'Blockers', hint: 'things that need attention' },
|
|
1792
|
+
{ value: '__custom', label: 'Type custom channel' },
|
|
1793
|
+
],
|
|
1794
|
+
});
|
|
1795
|
+
if (prompts.isCancel(channel)) throw new Error('cancelled');
|
|
1796
|
+
if (channel === '__custom') {
|
|
1797
|
+
channel = await prompts.text({ message: 'Channel name', validate: promptRequired });
|
|
1798
|
+
if (prompts.isCancel(channel)) throw new Error('cancelled');
|
|
1799
|
+
}
|
|
1800
|
+
const kind = await prompts.select({
|
|
1801
|
+
message: 'Message type',
|
|
1802
|
+
options: [
|
|
1803
|
+
{ value: 'chat.message', label: 'Chat message' },
|
|
1804
|
+
{ value: 'progress.note', label: 'Progress note' },
|
|
1805
|
+
{ value: 'decision.note', label: 'Decision note' },
|
|
1806
|
+
{ value: 'blocker.note', label: 'Blocker note' },
|
|
1807
|
+
],
|
|
1808
|
+
});
|
|
1809
|
+
if (prompts.isCancel(kind)) throw new Error('cancelled');
|
|
1810
|
+
const text = await prompts.text({
|
|
1811
|
+
message: 'Message',
|
|
1812
|
+
placeholder: 'Concise agent-to-agent note',
|
|
1813
|
+
validate: promptRequired,
|
|
1814
|
+
});
|
|
1815
|
+
if (prompts.isCancel(text)) throw new Error('cancelled');
|
|
1816
|
+
const spin = prompts.spinner();
|
|
1817
|
+
spin.start('Encrypting, uploading, and indexing locally');
|
|
1818
|
+
try {
|
|
1819
|
+
const data = await doChatSend(config, file, {
|
|
1820
|
+
room: roomId,
|
|
1821
|
+
text: String(text),
|
|
1822
|
+
channel: trimmed(channel) || 'agents',
|
|
1823
|
+
kind,
|
|
1824
|
+
});
|
|
1825
|
+
spin.stop('Message sent');
|
|
1826
|
+
prompts.note(`Room: ${roomId}\nMessage ID: ${data.message?.message_id || data.message?.id || 'saved'}`, 'Sent');
|
|
1827
|
+
return data;
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
spin.stop('Send failed');
|
|
1830
|
+
throw err;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
async function promptReadMessages(config, file, prompts, options = {}) {
|
|
1835
|
+
const roomId = options.roomId || await selectRoom(config, file, prompts, {
|
|
1836
|
+
message: 'Choose room to read',
|
|
1837
|
+
requireKey: true,
|
|
1838
|
+
includeCreate: false,
|
|
1839
|
+
includeJoin: true,
|
|
1840
|
+
});
|
|
1841
|
+
const limit = await prompts.select({
|
|
1842
|
+
message: 'How many recent messages?',
|
|
1843
|
+
options: [
|
|
1844
|
+
{ value: 20, label: '20 messages' },
|
|
1845
|
+
{ value: 50, label: '50 messages' },
|
|
1846
|
+
{ value: 100, label: '100 messages' },
|
|
1847
|
+
{ value: 200, label: '200 messages' },
|
|
1848
|
+
],
|
|
1849
|
+
});
|
|
1850
|
+
if (prompts.isCancel(limit)) throw new Error('cancelled');
|
|
1851
|
+
const spin = prompts.spinner();
|
|
1852
|
+
spin.start('Syncing and decrypting transcript locally');
|
|
1853
|
+
try {
|
|
1854
|
+
const data = await doChatRead(config, file, { room: roomId, limit });
|
|
1855
|
+
spin.stop(`Read ${data.messages.length} local messages`);
|
|
1856
|
+
prompts.note(formatMessagesForNote(data.messages), `Recent messages (${roomId})`);
|
|
1857
|
+
return data;
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
spin.stop('Read failed');
|
|
1860
|
+
throw err;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
async function promptSearchMessages(config, file, prompts, options = {}) {
|
|
1865
|
+
const roomId = options.roomId || await selectRoom(config, file, prompts, {
|
|
1866
|
+
message: 'Choose room to search',
|
|
1867
|
+
requireKey: true,
|
|
1868
|
+
includeCreate: false,
|
|
1869
|
+
includeJoin: true,
|
|
1870
|
+
});
|
|
1871
|
+
const query = await prompts.text({
|
|
1872
|
+
message: 'Search query',
|
|
1873
|
+
placeholder: 'auth decisions, current blocker, setup note...',
|
|
1874
|
+
validate: promptRequired,
|
|
1875
|
+
});
|
|
1876
|
+
if (prompts.isCancel(query)) throw new Error('cancelled');
|
|
1877
|
+
const mode = await prompts.select({
|
|
1878
|
+
message: 'Search mode',
|
|
1879
|
+
options: [
|
|
1880
|
+
{ value: 'hybrid', label: 'Hybrid', hint: 'keyword + BGE vector' },
|
|
1881
|
+
{ value: 'keyword', label: 'Keyword', hint: 'SQLite FTS/BM25' },
|
|
1882
|
+
{ value: 'vector', label: 'Vector', hint: 'local BGE cosine search' },
|
|
1883
|
+
],
|
|
1884
|
+
});
|
|
1885
|
+
if (prompts.isCancel(mode)) throw new Error('cancelled');
|
|
1886
|
+
const limit = await prompts.select({
|
|
1887
|
+
message: 'Result limit',
|
|
1888
|
+
options: [
|
|
1889
|
+
{ value: 10, label: '10 results' },
|
|
1890
|
+
{ value: 20, label: '20 results' },
|
|
1891
|
+
{ value: 50, label: '50 results' },
|
|
1892
|
+
],
|
|
1893
|
+
});
|
|
1894
|
+
if (prompts.isCancel(limit)) throw new Error('cancelled');
|
|
1895
|
+
const spin = prompts.spinner();
|
|
1896
|
+
spin.start('Syncing, embedding locally, and searching');
|
|
1897
|
+
try {
|
|
1898
|
+
const data = await doChatSearch(config, file, { room: roomId, query: String(query), mode, limit });
|
|
1899
|
+
spin.stop(`Found ${data.results.length} result(s)`);
|
|
1900
|
+
prompts.note(formatSearchForNote(data.results), `Search results (${mode})`);
|
|
1901
|
+
return data;
|
|
1902
|
+
} catch (err) {
|
|
1903
|
+
spin.stop('Search failed');
|
|
1904
|
+
throw err;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
async function promptSyncRoom(config, file, prompts, options = {}) {
|
|
1909
|
+
const roomId = options.roomId || await selectRoom(config, file, prompts, {
|
|
1910
|
+
message: 'Choose room to sync',
|
|
1911
|
+
requireKey: true,
|
|
1912
|
+
includeCreate: false,
|
|
1913
|
+
includeJoin: true,
|
|
1914
|
+
});
|
|
1915
|
+
const spin = prompts.spinner();
|
|
1916
|
+
spin.start('Syncing encrypted room transcript into local SQLite');
|
|
1917
|
+
try {
|
|
1918
|
+
const data = await syncRoom(config, file, roomId);
|
|
1919
|
+
spin.stop(`Pulled ${data.pulled} message(s)`);
|
|
1920
|
+
prompts.note(`Local DB: ${data.db}\nLast message ID: ${data.last_message_id}\nChunks embedded: ${data.embedding?.chunks_embedded || 0}`, 'Sync complete');
|
|
1921
|
+
return data;
|
|
1922
|
+
} catch (err) {
|
|
1923
|
+
spin.stop('Sync failed');
|
|
1924
|
+
throw err;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
async function promptRoomActions(config, file, prompts, roomId) {
|
|
1929
|
+
while (true) {
|
|
1930
|
+
const action = await prompts.select({
|
|
1931
|
+
message: `Room ${roomId}`,
|
|
1932
|
+
options: [
|
|
1933
|
+
{ value: 'activate', label: 'Activate for a project directory' },
|
|
1934
|
+
{ value: 'invite', label: 'Create private invite' },
|
|
1935
|
+
{ value: 'send', label: 'Send message' },
|
|
1936
|
+
{ value: 'read', label: 'Read recent messages' },
|
|
1937
|
+
{ value: 'search', label: 'Search transcript' },
|
|
1938
|
+
{ value: 'sync', label: 'Sync locally' },
|
|
1939
|
+
{ value: 'back', label: 'Back' },
|
|
1940
|
+
],
|
|
1941
|
+
});
|
|
1942
|
+
if (prompts.isCancel(action) || action === 'back') return;
|
|
1943
|
+
try {
|
|
1944
|
+
if (action === 'activate') await promptActivateRoom(config, file, prompts, { roomId });
|
|
1945
|
+
if (action === 'invite') await promptCreateInvite(config, file, prompts, { roomId });
|
|
1946
|
+
if (action === 'send') await promptSendMessage(config, file, prompts, { roomId });
|
|
1947
|
+
if (action === 'read') await promptReadMessages(config, file, prompts, { roomId });
|
|
1948
|
+
if (action === 'search') await promptSearchMessages(config, file, prompts, { roomId });
|
|
1949
|
+
if (action === 'sync') await promptSyncRoom(config, file, prompts, { roomId });
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
if (err.message !== 'cancelled') prompts.note(err.message || String(err), 'Action failed');
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
async function promptBrowseRooms(config, file, prompts) {
|
|
1957
|
+
const roomId = await selectRoom(config, file, prompts, {
|
|
1958
|
+
message: 'Choose room',
|
|
1959
|
+
requireKey: false,
|
|
1960
|
+
includeCreate: true,
|
|
1961
|
+
includeJoin: true,
|
|
1962
|
+
includeManual: true,
|
|
1963
|
+
});
|
|
1964
|
+
const ok = await ensureRoomKeyFromMenu(config, file, prompts, roomId);
|
|
1965
|
+
if (!ok) return promptBrowseRooms(config, file, prompts);
|
|
1966
|
+
return promptRoomActions(config, file, prompts, roomId);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
async function promptAccountStatus(config, file, prompts) {
|
|
1970
|
+
let me = null;
|
|
1971
|
+
try {
|
|
1972
|
+
me = config.userToken ? await api(config, 'GET', '/v1/me', undefined, config.userToken) : null;
|
|
1973
|
+
} catch {}
|
|
1974
|
+
const active = await activeStatus(config, file, {});
|
|
1975
|
+
const rooms = await listRoomsForMenu(config).catch(() => []);
|
|
1976
|
+
const lines = [
|
|
1977
|
+
`Server: ${config.serverUrl || DEFAULT_SERVER}`,
|
|
1978
|
+
`User: ${config.username || me?.username || 'not logged in'}`,
|
|
1979
|
+
`Agent: ${config.agentLabel || config.agentId || 'not registered'}`,
|
|
1980
|
+
`Fingerprint: ${config.agentFingerprint || 'none'}`,
|
|
1981
|
+
`Rooms visible: ${rooms.length}`,
|
|
1982
|
+
`Current directory: ${active.active ? `active in ${active.room_id}` : 'SuperCollab off'}`,
|
|
1983
|
+
`Config: ${file}`,
|
|
1984
|
+
];
|
|
1985
|
+
prompts.note(lines.join('\n'), 'Account and config');
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
async function promptMcpConfig(config, file, prompts) {
|
|
1989
|
+
const client = await prompts.select({
|
|
1990
|
+
message: 'MCP client config',
|
|
1991
|
+
options: [
|
|
1992
|
+
{ value: 'codex', label: 'Codex', hint: 'TOML config snippet' },
|
|
1993
|
+
{ value: 'claude', label: 'Claude', hint: 'JSON config snippet' },
|
|
1994
|
+
{ value: 'manual', label: 'Manual', hint: 'stdio command' },
|
|
1995
|
+
{ value: 'back', label: 'Back' },
|
|
1996
|
+
],
|
|
1997
|
+
});
|
|
1998
|
+
if (prompts.isCancel(client) || client === 'back') return;
|
|
1999
|
+
prompts.note(mcpConfigText(client, file), `${client} MCP config`);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function sessionsFromResponse(data) {
|
|
2003
|
+
if (Array.isArray(data)) return data;
|
|
2004
|
+
if (Array.isArray(data?.sessions)) return data.sessions;
|
|
2005
|
+
if (Array.isArray(data?.items)) return data.items;
|
|
2006
|
+
return [];
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function sessionIdFrom(session) {
|
|
2010
|
+
return trimmed(session?.session_id || session?.id || session?.token_id);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
async function promptManageSessions(config, prompts) {
|
|
2014
|
+
const data = await api(config, 'GET', '/v1/agent-sessions', undefined, config.userToken);
|
|
2015
|
+
const sessions = sessionsFromResponse(data).filter((session) => sessionIdFrom(session));
|
|
2016
|
+
if (!sessions.length) {
|
|
2017
|
+
prompts.note('No active sessions returned by the server.', 'Sessions');
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
const selected = await prompts.select({
|
|
2021
|
+
message: 'Choose session to revoke',
|
|
2022
|
+
options: [
|
|
2023
|
+
...sessions.map((session) => {
|
|
2024
|
+
const id = sessionIdFrom(session);
|
|
2025
|
+
const label = `${shorten(session.agent_label || session.label || id, 36)} (${id})`;
|
|
2026
|
+
const hint = [session.created_at, session.expires_at ? `expires ${session.expires_at}` : null].filter(Boolean).join(' - ');
|
|
2027
|
+
return { value: id, label, hint };
|
|
2028
|
+
}),
|
|
2029
|
+
{ value: '__manual', label: 'Type session ID manually' },
|
|
2030
|
+
{ value: '__back', label: 'Back' },
|
|
2031
|
+
],
|
|
2032
|
+
});
|
|
2033
|
+
if (prompts.isCancel(selected) || selected === '__back') return;
|
|
2034
|
+
const sessionId = selected === '__manual'
|
|
2035
|
+
? await prompts.text({ message: 'Session ID', validate: promptRequired })
|
|
2036
|
+
: selected;
|
|
2037
|
+
if (prompts.isCancel(sessionId)) throw new Error('cancelled');
|
|
2038
|
+
const confirm = await prompts.confirm({ message: `Revoke session ${sessionId}?`, initialValue: false });
|
|
2039
|
+
if (prompts.isCancel(confirm) || !confirm) return;
|
|
2040
|
+
await api(config, 'DELETE', `/v1/agent-sessions/${encodeURIComponent(String(sessionId))}`, undefined, config.userToken);
|
|
2041
|
+
prompts.note(`Revoked ${sessionId}`, 'Session revoked');
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
async function promptSetServerUrl(config, file, prompts) {
|
|
2045
|
+
const server = await prompts.text({
|
|
2046
|
+
message: 'SuperCollab server URL',
|
|
2047
|
+
defaultValue: config.serverUrl || DEFAULT_SERVER,
|
|
2048
|
+
placeholder: DEFAULT_SERVER,
|
|
2049
|
+
validate: (value) => {
|
|
2050
|
+
try {
|
|
2051
|
+
const parsed = new URL(String(value || ''));
|
|
2052
|
+
return parsed.protocol === 'https:' || parsed.hostname === 'localhost' ? undefined : 'Use https:// for remote servers';
|
|
2053
|
+
} catch {
|
|
2054
|
+
return 'Enter a valid URL';
|
|
2055
|
+
}
|
|
2056
|
+
},
|
|
2057
|
+
});
|
|
2058
|
+
if (prompts.isCancel(server)) throw new Error('cancelled');
|
|
2059
|
+
config.serverUrl = trimmed(server).replace(/\/$/, '');
|
|
2060
|
+
saveConfig(config, file);
|
|
2061
|
+
prompts.note(config.serverUrl, 'Server URL saved');
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
async function runRoomsMenu(config, file, prompts) {
|
|
2065
|
+
while (true) {
|
|
2066
|
+
const action = await prompts.select({
|
|
2067
|
+
message: 'Rooms',
|
|
2068
|
+
options: [
|
|
2069
|
+
{ value: 'browse', label: 'Browse/select room', hint: 'scroll rooms or type room ID' },
|
|
2070
|
+
{ value: 'create', label: 'Create room', hint: 'sets up backend room and local key' },
|
|
2071
|
+
{ value: 'join', label: 'Join with private invite', hint: 'saves room key locally' },
|
|
2072
|
+
{ value: 'invite', label: 'Create private invite' },
|
|
2073
|
+
{ value: 'back', label: 'Back' },
|
|
2074
|
+
],
|
|
2075
|
+
});
|
|
2076
|
+
if (prompts.isCancel(action) || action === 'back') return;
|
|
2077
|
+
try {
|
|
2078
|
+
if (action === 'browse') await promptBrowseRooms(config, file, prompts);
|
|
2079
|
+
if (action === 'create') {
|
|
2080
|
+
const created = await promptCreateRoom(config, file, prompts);
|
|
2081
|
+
if (created?.room_id) await promptRoomActions(config, file, prompts, created.room_id);
|
|
2082
|
+
}
|
|
2083
|
+
if (action === 'join') {
|
|
2084
|
+
const joined = await promptJoinRoom(config, file, prompts);
|
|
2085
|
+
if (joined?.room_id) await promptRoomActions(config, file, prompts, joined.room_id);
|
|
2086
|
+
}
|
|
2087
|
+
if (action === 'invite') await promptCreateInvite(config, file, prompts);
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
if (err.message !== 'cancelled') prompts.note(err.message || String(err), 'Room action failed');
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
async function runChatMenu(config, file, prompts) {
|
|
2095
|
+
while (true) {
|
|
2096
|
+
const action = await prompts.select({
|
|
2097
|
+
message: 'Chat',
|
|
2098
|
+
options: [
|
|
2099
|
+
{ value: 'send', label: 'Send message/note' },
|
|
2100
|
+
{ value: 'read', label: 'Read recent messages' },
|
|
2101
|
+
{ value: 'search', label: 'Search transcript' },
|
|
2102
|
+
{ value: 'sync', label: 'Sync room locally' },
|
|
2103
|
+
{ value: 'back', label: 'Back' },
|
|
2104
|
+
],
|
|
2105
|
+
});
|
|
2106
|
+
if (prompts.isCancel(action) || action === 'back') return;
|
|
2107
|
+
try {
|
|
2108
|
+
if (action === 'send') await promptSendMessage(config, file, prompts);
|
|
2109
|
+
if (action === 'read') await promptReadMessages(config, file, prompts);
|
|
2110
|
+
if (action === 'search') await promptSearchMessages(config, file, prompts);
|
|
2111
|
+
if (action === 'sync') await promptSyncRoom(config, file, prompts);
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
if (err.message !== 'cancelled') prompts.note(err.message || String(err), 'Chat action failed');
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
async function runWorkspaceMenu(config, file, prompts) {
|
|
2119
|
+
while (true) {
|
|
2120
|
+
const active = await activeStatus(config, file, {});
|
|
2121
|
+
const action = await prompts.select({
|
|
2122
|
+
message: `Workspace activation (${active.active ? `active: ${active.room_id}` : 'off'})`,
|
|
2123
|
+
options: [
|
|
2124
|
+
{ value: 'status', label: 'Show current status' },
|
|
2125
|
+
{ value: 'activate', label: 'Activate this directory' },
|
|
2126
|
+
{ value: 'activate_other', label: 'Activate another directory' },
|
|
2127
|
+
{ value: 'deactivate', label: 'Deactivate directory' },
|
|
2128
|
+
{ value: 'back', label: 'Back' },
|
|
2129
|
+
],
|
|
2130
|
+
});
|
|
2131
|
+
if (prompts.isCancel(action) || action === 'back') return;
|
|
2132
|
+
try {
|
|
2133
|
+
if (action === 'status') prompts.note(active.instructions, 'Current workspace');
|
|
2134
|
+
if (action === 'activate') await promptActivateRoom(config, file, prompts, { cwd: process.cwd() });
|
|
2135
|
+
if (action === 'activate_other') await promptActivateRoom(config, file, prompts);
|
|
2136
|
+
if (action === 'deactivate') await promptDeactivateRoom(config, file, prompts, { cwd: process.cwd() });
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
if (err.message !== 'cancelled') prompts.note(err.message || String(err), 'Workspace action failed');
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
async function runSettingsMenu(config, file, prompts) {
|
|
2144
|
+
while (true) {
|
|
2145
|
+
const action = await prompts.select({
|
|
2146
|
+
message: 'Settings',
|
|
2147
|
+
options: [
|
|
2148
|
+
{ value: 'doctor', label: 'System check / install BGE model' },
|
|
2149
|
+
{ value: 'account', label: 'Account and config status' },
|
|
2150
|
+
{ value: 'mcp', label: 'Show MCP config' },
|
|
2151
|
+
{ value: 'sessions', label: 'Manage sessions' },
|
|
2152
|
+
{ value: 'server', label: 'Set server URL' },
|
|
2153
|
+
{ value: 'back', label: 'Back' },
|
|
2154
|
+
],
|
|
2155
|
+
});
|
|
2156
|
+
if (prompts.isCancel(action) || action === 'back') return;
|
|
2157
|
+
try {
|
|
2158
|
+
if (action === 'doctor') await promptSystemCheck(config, file, prompts);
|
|
2159
|
+
if (action === 'account') await promptAccountStatus(config, file, prompts);
|
|
2160
|
+
if (action === 'mcp') await promptMcpConfig(config, file, prompts);
|
|
2161
|
+
if (action === 'sessions') await promptManageSessions(config, prompts);
|
|
2162
|
+
if (action === 'server') await promptSetServerUrl(config, file, prompts);
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
if (err.message !== 'cancelled') prompts.note(err.message || String(err), 'Settings action failed');
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
async function runMainMenu(config, file, opts = {}) {
|
|
2170
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2171
|
+
throw new Error('interactive menu requires a TTY');
|
|
2172
|
+
}
|
|
2173
|
+
if (!isConfigured(config)) {
|
|
2174
|
+
return runSetupWizard(config, file, opts);
|
|
2175
|
+
}
|
|
2176
|
+
const prompts = await loadPrompts();
|
|
2177
|
+
prompts.intro(`SuperCollab ${VERSION}`);
|
|
2178
|
+
try {
|
|
2179
|
+
while (true) {
|
|
2180
|
+
const active = await activeStatus(config, file, {});
|
|
2181
|
+
const action = await prompts.select({
|
|
2182
|
+
message: `Main menu (${config.username || 'user'} - ${active.active ? `active ${active.room_id}` : 'workspace off'})`,
|
|
2183
|
+
options: [
|
|
2184
|
+
{ value: 'rooms', label: 'Rooms', hint: 'create, join, browse, invite' },
|
|
2185
|
+
{ value: 'chat', label: 'Chat', hint: 'send, read, search, sync' },
|
|
2186
|
+
{ value: 'workspace', label: 'Workspace activation', hint: 'turn SuperCollab on/off for directories' },
|
|
2187
|
+
{ value: 'settings', label: 'Settings', hint: 'doctor, MCP config, sessions, server URL' },
|
|
2188
|
+
{ value: 'setup', label: 'Run onboarding again' },
|
|
2189
|
+
{ value: 'exit', label: 'Exit' },
|
|
2190
|
+
],
|
|
2191
|
+
});
|
|
2192
|
+
if (prompts.isCancel(action) || action === 'exit') {
|
|
2193
|
+
prompts.outro('Done.');
|
|
2194
|
+
return { ok: true };
|
|
2195
|
+
}
|
|
2196
|
+
if (action === 'rooms') await runRoomsMenu(config, file, prompts);
|
|
2197
|
+
if (action === 'chat') await runChatMenu(config, file, prompts);
|
|
2198
|
+
if (action === 'workspace') await runWorkspaceMenu(config, file, prompts);
|
|
2199
|
+
if (action === 'settings') await runSettingsMenu(config, file, prompts);
|
|
2200
|
+
if (action === 'setup') return runSetupWizard(config, file, opts);
|
|
2201
|
+
}
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
prompts.cancel(err.message === 'cancelled' ? 'Menu closed.' : `Menu stopped: ${err.message}`);
|
|
2204
|
+
throw err;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
1456
2208
|
async function runSetupWizard(config, file, opts = {}) {
|
|
1457
2209
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1458
2210
|
throw new Error('interactive setup requires a TTY; run `supercollab doctor --json` for non-interactive diagnostics');
|
|
@@ -1518,10 +2270,15 @@ async function main() {
|
|
|
1518
2270
|
config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
|
|
1519
2271
|
|
|
1520
2272
|
if (positionals.length === 0) {
|
|
1521
|
-
if (process.stdin.isTTY && process.stdout.isTTY)
|
|
2273
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
2274
|
+
return isConfigured(config)
|
|
2275
|
+
? runMainMenu(config, file, opts)
|
|
2276
|
+
: runSetupWizard(config, file, opts);
|
|
2277
|
+
}
|
|
1522
2278
|
printHelp();
|
|
1523
2279
|
return;
|
|
1524
2280
|
}
|
|
2281
|
+
if (cmd === 'menu') return runMainMenu(config, file, opts);
|
|
1525
2282
|
if (cmd === 'setup') return runSetupWizard(config, file, opts);
|
|
1526
2283
|
if (cmd === 'doctor') {
|
|
1527
2284
|
const data = await runDoctor(config, file, opts);
|