@supercollab/cli 0.4.2 → 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 +39 -3
- package/bin/supercollab.js +1167 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -12,9 +12,45 @@ Install:
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
npm install -g @supercollab/cli
|
|
15
|
+
supercollab
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
|
|
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.
|
|
45
|
+
|
|
46
|
+
You can also run the checks directly:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
supercollab doctor
|
|
50
|
+
supercollab doctor --json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Manual account setup:
|
|
18
54
|
|
|
19
55
|
```bash
|
|
20
56
|
supercollab register --username your_name
|
|
@@ -83,8 +119,8 @@ hybrid: reciprocal-rank fusion over keyword and vector results
|
|
|
83
119
|
```
|
|
84
120
|
|
|
85
121
|
The hosted SuperCollab service never computes embeddings and never receives the
|
|
86
|
-
room key.
|
|
87
|
-
|
|
122
|
+
room key. Guided setup downloads and verifies the BGE-small ONNX model into the
|
|
123
|
+
local Hugging Face cache. To verify or prewarm the local embedding system:
|
|
88
124
|
|
|
89
125
|
```bash
|
|
90
126
|
supercollab embeddings status
|
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 { spawnSync } from 'node:child_process';
|
|
6
7
|
import * as readlineCore from 'node:readline';
|
|
7
8
|
import { stdin as input, stdout as output } from 'node:process';
|
|
8
9
|
|
|
9
|
-
const VERSION = '0.4.
|
|
10
|
+
const VERSION = '0.4.4';
|
|
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;
|
|
@@ -33,6 +34,9 @@ function printHelp() {
|
|
|
33
34
|
console.log(`SuperCollab CLI ${VERSION}
|
|
34
35
|
|
|
35
36
|
Usage:
|
|
37
|
+
supercollab menu
|
|
38
|
+
supercollab setup
|
|
39
|
+
supercollab doctor [--json] [--skip-model]
|
|
36
40
|
supercollab register --username NAME [--password PASS] [--label LABEL]
|
|
37
41
|
supercollab login --username NAME [--password PASS]
|
|
38
42
|
supercollab whoami
|
|
@@ -65,6 +69,7 @@ Options:
|
|
|
65
69
|
Environment:
|
|
66
70
|
SUPERCOLLAB_PASSWORD can provide password non-interactively.
|
|
67
71
|
SUPERCOLLAB_CONFIG can override config path.
|
|
72
|
+
SUPERCOLLAB_MODEL_CACHE can override the local Hugging Face model cache.
|
|
68
73
|
SUPERCOLLAB_WORKDIR sets the local workspace directory for MCP activation checks.
|
|
69
74
|
`);
|
|
70
75
|
}
|
|
@@ -363,6 +368,78 @@ async function doLogin(config, file, opts) {
|
|
|
363
368
|
return { ok: true, username: config.username, user_id: config.userId, config: file };
|
|
364
369
|
}
|
|
365
370
|
|
|
371
|
+
function commandExists(command, args = ['--version']) {
|
|
372
|
+
const result = spawnSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
373
|
+
return {
|
|
374
|
+
ok: result.status === 0,
|
|
375
|
+
command,
|
|
376
|
+
status: result.status,
|
|
377
|
+
output: String(result.stdout || result.stderr || '').trim().split('\n')[0] || '',
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function installAdvice(profile, checks = {}) {
|
|
382
|
+
const advice = [];
|
|
383
|
+
const platform = profile.platform;
|
|
384
|
+
if (!checks.node_supported?.ok) {
|
|
385
|
+
advice.push('Install Node.js 20 LTS or newer, then reinstall @supercollab/cli.');
|
|
386
|
+
}
|
|
387
|
+
if (!checks.native_sqlite_vec?.ok) {
|
|
388
|
+
if (profile.tools?.npm_ignore_scripts?.output === 'true') {
|
|
389
|
+
advice.push('Your npm config has `ignore-scripts=true`, which prevents native SQLite from installing. Run `npm config set ignore-scripts false`, then reinstall or run `npm rebuild -g better-sqlite3 --ignore-scripts=false`.');
|
|
390
|
+
}
|
|
391
|
+
if (platform === 'darwin') {
|
|
392
|
+
advice.push('Install Apple command line tools with `xcode-select --install`, then run `npm rebuild -g better-sqlite3`.');
|
|
393
|
+
} else if (platform === 'linux') {
|
|
394
|
+
advice.push('Install Python 3, make, and a C/C++ compiler, then run `npm rebuild -g better-sqlite3`.');
|
|
395
|
+
} else if (platform === 'win32') {
|
|
396
|
+
advice.push('Install Microsoft Visual Studio Build Tools with the C++ workload and Python, then run `npm rebuild -g better-sqlite3`.');
|
|
397
|
+
} else {
|
|
398
|
+
advice.push('Install a native build toolchain for your OS, then run `npm rebuild -g better-sqlite3`.');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!checks.bge_model?.ok && checks.bge_model?.error) {
|
|
402
|
+
advice.push('Check internet access to Hugging Face model downloads, then run `supercollab embeddings warmup`.');
|
|
403
|
+
}
|
|
404
|
+
return advice;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function detectSystemProfile() {
|
|
408
|
+
const cpus = os.cpus() || [];
|
|
409
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
410
|
+
const tools = {
|
|
411
|
+
npm: commandExists('npm', ['--version']),
|
|
412
|
+
npm_ignore_scripts: commandExists('npm', ['config', 'get', 'ignore-scripts']),
|
|
413
|
+
};
|
|
414
|
+
if (process.platform === 'darwin') {
|
|
415
|
+
tools.xcode_select = commandExists('xcode-select', ['-p']);
|
|
416
|
+
tools.clang = commandExists('clang', ['--version']);
|
|
417
|
+
} else if (process.platform === 'linux') {
|
|
418
|
+
tools.python3 = commandExists('python3', ['--version']);
|
|
419
|
+
tools.make = commandExists('make', ['--version']);
|
|
420
|
+
tools.cc = commandExists('cc', ['--version']);
|
|
421
|
+
tools.gpp = commandExists('g++', ['--version']);
|
|
422
|
+
} else if (process.platform === 'win32') {
|
|
423
|
+
tools.python = commandExists('python', ['--version']);
|
|
424
|
+
tools.node_gyp = commandExists('node-gyp', ['--version']);
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
detected_at: nowIso(),
|
|
428
|
+
cli_version: VERSION,
|
|
429
|
+
platform: process.platform,
|
|
430
|
+
arch: process.arch,
|
|
431
|
+
os_type: os.type(),
|
|
432
|
+
os_release: os.release(),
|
|
433
|
+
hostname: os.hostname(),
|
|
434
|
+
node: process.versions.node,
|
|
435
|
+
node_modules_abi: process.versions.modules,
|
|
436
|
+
node_supported: nodeMajor >= 20,
|
|
437
|
+
cpu_model: cpus[0]?.model || null,
|
|
438
|
+
cpu_count: cpus.length,
|
|
439
|
+
tools,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
366
443
|
let nativeSqlitePromise = null;
|
|
367
444
|
|
|
368
445
|
async function loadNativeSqlite() {
|
|
@@ -1094,14 +1171,14 @@ async function runMcp(opts) {
|
|
|
1094
1171
|
|
|
1095
1172
|
function printCodexConfig(opts) {
|
|
1096
1173
|
const file = configPath(opts);
|
|
1097
|
-
console.log(
|
|
1174
|
+
console.log(mcpConfigText(String(opts.client || 'codex'), file));
|
|
1098
1175
|
}
|
|
1099
1176
|
|
|
1100
1177
|
async function embeddingStatus() {
|
|
1101
1178
|
return {
|
|
1102
1179
|
ok: true,
|
|
1103
1180
|
profile: EMBEDDING_PROFILE,
|
|
1104
|
-
model_download: '
|
|
1181
|
+
model_download: 'installed during setup, or manually via `supercollab embeddings warmup`',
|
|
1105
1182
|
cache_dir: process.env.SUPERCOLLAB_MODEL_CACHE || 'default @huggingface/transformers cache',
|
|
1106
1183
|
};
|
|
1107
1184
|
}
|
|
@@ -1115,14 +1192,1100 @@ async function embeddingWarmup() {
|
|
|
1115
1192
|
};
|
|
1116
1193
|
}
|
|
1117
1194
|
|
|
1195
|
+
async function nativeEngineCheck() {
|
|
1196
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'supercollab-doctor-'));
|
|
1197
|
+
const dbPath = path.join(tempDir, 'native.sqlite');
|
|
1198
|
+
let db = null;
|
|
1199
|
+
try {
|
|
1200
|
+
const { Database, sqliteVec } = await loadNativeSqlite();
|
|
1201
|
+
db = new Database(dbPath);
|
|
1202
|
+
sqliteVec.load(db);
|
|
1203
|
+
const version = db.prepare('SELECT vec_version() AS version').get()?.version;
|
|
1204
|
+
db.exec(`CREATE VIRTUAL TABLE vec_check USING vec0(id TEXT PRIMARY KEY, embedding float[${EMBEDDING_DIMS}] distance_metric=cosine)`);
|
|
1205
|
+
db.prepare('INSERT INTO vec_check(id, embedding) VALUES(?, ?)').run('ok', new Float32Array(new Array(EMBEDDING_DIMS).fill(0).map((_, i) => i === 0 ? 1 : 0)));
|
|
1206
|
+
const rows = db.prepare('SELECT id, distance FROM vec_check WHERE embedding MATCH ? AND k = 1').all(new Float32Array(new Array(EMBEDDING_DIMS).fill(0).map((_, i) => i === 0 ? 1 : 0)));
|
|
1207
|
+
if (rows[0]?.id !== 'ok') throw new Error('sqlite-vec query did not return expected row');
|
|
1208
|
+
return {
|
|
1209
|
+
ok: true,
|
|
1210
|
+
engine: 'native-sqlite-vec',
|
|
1211
|
+
sqlite_vec_version: version,
|
|
1212
|
+
dims: EMBEDDING_DIMS,
|
|
1213
|
+
};
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
return {
|
|
1216
|
+
ok: false,
|
|
1217
|
+
engine: 'native-sqlite-vec',
|
|
1218
|
+
error: err.message || String(err),
|
|
1219
|
+
};
|
|
1220
|
+
} finally {
|
|
1221
|
+
try { db?.close(); } catch {}
|
|
1222
|
+
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch {}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async function runDoctor(config, file, opts = {}) {
|
|
1227
|
+
const profile = detectSystemProfile();
|
|
1228
|
+
const checks = {
|
|
1229
|
+
node_supported: {
|
|
1230
|
+
ok: profile.node_supported,
|
|
1231
|
+
version: profile.node,
|
|
1232
|
+
required: '>=20',
|
|
1233
|
+
},
|
|
1234
|
+
native_sqlite_vec: await nativeEngineCheck(),
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
if (!opts['skip-model']) {
|
|
1238
|
+
try {
|
|
1239
|
+
const warmed = await embeddingWarmup();
|
|
1240
|
+
checks.bge_model = {
|
|
1241
|
+
ok: warmed.ok && warmed.dims === EMBEDDING_DIMS,
|
|
1242
|
+
dims: warmed.dims,
|
|
1243
|
+
profile: EMBEDDING_PROFILE,
|
|
1244
|
+
cache_dir: process.env.SUPERCOLLAB_MODEL_CACHE || 'default @huggingface/transformers cache',
|
|
1245
|
+
};
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
checks.bge_model = {
|
|
1248
|
+
ok: false,
|
|
1249
|
+
profile: EMBEDDING_PROFILE,
|
|
1250
|
+
error: err.message || String(err),
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
} else {
|
|
1254
|
+
checks.bge_model = {
|
|
1255
|
+
ok: null,
|
|
1256
|
+
skipped: true,
|
|
1257
|
+
profile: EMBEDDING_PROFILE,
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const ok = checks.node_supported.ok && checks.native_sqlite_vec.ok && (checks.bge_model.ok === true || checks.bge_model.skipped === true);
|
|
1262
|
+
const result = {
|
|
1263
|
+
ok,
|
|
1264
|
+
checked_at: nowIso(),
|
|
1265
|
+
system: profile,
|
|
1266
|
+
checks,
|
|
1267
|
+
local_engine: {
|
|
1268
|
+
id: 'native-sqlite-vec',
|
|
1269
|
+
transcript_store: 'better-sqlite3',
|
|
1270
|
+
vector_store: 'sqlite-vec',
|
|
1271
|
+
vector_version: checks.native_sqlite_vec.sqlite_vec_version || null,
|
|
1272
|
+
embedding_profile_id: EMBEDDING_PROFILE.id,
|
|
1273
|
+
},
|
|
1274
|
+
advice: installAdvice(profile, checks),
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
config.systemProfile = profile;
|
|
1278
|
+
config.localEngine = result.local_engine;
|
|
1279
|
+
config.embeddingProfile = EMBEDDING_PROFILE;
|
|
1280
|
+
config.setupChecks = {
|
|
1281
|
+
ok,
|
|
1282
|
+
checked_at: result.checked_at,
|
|
1283
|
+
checks,
|
|
1284
|
+
advice: result.advice,
|
|
1285
|
+
};
|
|
1286
|
+
saveConfig(config, file);
|
|
1287
|
+
return result;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function printDoctor(result) {
|
|
1291
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async function loadPrompts() {
|
|
1295
|
+
return import('@clack/prompts');
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function isConfigured(config) {
|
|
1299
|
+
return Boolean(config.userToken && config.agentId && config.agentPrivateKeyPem);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function promptRequired(value) {
|
|
1303
|
+
return String(value || '').trim() ? undefined : 'Required';
|
|
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
|
+
|
|
1534
|
+
async function ensureAgentForSetup(config, file, prompts) {
|
|
1535
|
+
if (config.agentId && config.agentPrivateKeyPem) return null;
|
|
1536
|
+
const label = await prompts.text({
|
|
1537
|
+
message: 'Name this local agent',
|
|
1538
|
+
placeholder: `${os.hostname()}-agent`,
|
|
1539
|
+
defaultValue: `${os.hostname()}-agent`,
|
|
1540
|
+
validate: promptRequired,
|
|
1541
|
+
});
|
|
1542
|
+
if (prompts.isCancel(label)) throw new Error('cancelled');
|
|
1543
|
+
const agent = await registerAgent(config, String(label));
|
|
1544
|
+
saveConfig(config, file);
|
|
1545
|
+
return agent;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
async function runAuthSetup(config, file, prompts) {
|
|
1549
|
+
if (isConfigured(config)) {
|
|
1550
|
+
const reuse = await prompts.confirm({
|
|
1551
|
+
message: `Use existing login as ${config.username || 'current user'}?`,
|
|
1552
|
+
initialValue: true,
|
|
1553
|
+
});
|
|
1554
|
+
if (prompts.isCancel(reuse)) throw new Error('cancelled');
|
|
1555
|
+
if (reuse) return { reused: true };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const mode = await prompts.select({
|
|
1559
|
+
message: 'Account setup',
|
|
1560
|
+
options: [
|
|
1561
|
+
{ value: 'register', label: 'Create account', hint: 'new SuperCollab username' },
|
|
1562
|
+
{ value: 'login', label: 'Log in', hint: 'existing username' },
|
|
1563
|
+
],
|
|
1564
|
+
});
|
|
1565
|
+
if (prompts.isCancel(mode)) throw new Error('cancelled');
|
|
1566
|
+
|
|
1567
|
+
const username = await prompts.text({
|
|
1568
|
+
message: mode === 'register' ? 'Choose a username' : 'Username',
|
|
1569
|
+
validate: promptRequired,
|
|
1570
|
+
});
|
|
1571
|
+
if (prompts.isCancel(username)) throw new Error('cancelled');
|
|
1572
|
+
|
|
1573
|
+
const pass = await prompts.password({
|
|
1574
|
+
message: mode === 'register' ? 'Choose a password' : 'Password',
|
|
1575
|
+
validate: (value) => String(value || '').length >= 8 ? undefined : 'Use at least 8 characters',
|
|
1576
|
+
});
|
|
1577
|
+
if (prompts.isCancel(pass)) throw new Error('cancelled');
|
|
1578
|
+
|
|
1579
|
+
if (mode === 'register') {
|
|
1580
|
+
return doRegister(config, file, { username: String(username), password: String(pass), label: `${os.hostname()}-agent` });
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
const login = await doLogin(config, file, { username: String(username), password: String(pass) });
|
|
1584
|
+
const agent = await ensureAgentForSetup(config, file, prompts);
|
|
1585
|
+
return { ...login, agent_id: agent?.agent_id || config.agentId, fingerprint: agent?.fingerprint || config.agentFingerprint };
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
async function runRoomSetup(config, file, prompts) {
|
|
1589
|
+
const choice = await prompts.select({
|
|
1590
|
+
message: 'Room setup',
|
|
1591
|
+
options: [
|
|
1592
|
+
{ value: 'existing', label: 'Use an existing room', hint: 'pick from your rooms or type a room ID' },
|
|
1593
|
+
{ value: 'create', label: 'Create a new room', hint: 'start solo or invite agents later' },
|
|
1594
|
+
{ value: 'join', label: 'Join with private invite', hint: 'paste sci_...sck_...' },
|
|
1595
|
+
{ value: 'skip', label: 'Skip for now', hint: 'set up auth and local engine only' },
|
|
1596
|
+
],
|
|
1597
|
+
});
|
|
1598
|
+
if (prompts.isCancel(choice)) throw new Error('cancelled');
|
|
1599
|
+
if (choice === 'skip') return null;
|
|
1600
|
+
|
|
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,
|
|
1607
|
+
});
|
|
1608
|
+
return { room_id: roomId, existing: true };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (choice === 'join') {
|
|
1612
|
+
const joined = await promptJoinRoom(config, file, prompts, { quiet: true });
|
|
1613
|
+
return { room_id: joined.room_id || joined.workspace_id, joined };
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const created = await promptCreateRoom(config, file, prompts, { quiet: true });
|
|
1617
|
+
return { room_id: created.room_id || created.id, created };
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
async function runActivationSetup(config, file, roomId, prompts) {
|
|
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
|
+
}
|
|
1635
|
+
const shouldActivate = await prompts.confirm({
|
|
1636
|
+
message: 'Activate SuperCollab for a local project directory now?',
|
|
1637
|
+
initialValue: true,
|
|
1638
|
+
});
|
|
1639
|
+
if (prompts.isCancel(shouldActivate)) throw new Error('cancelled');
|
|
1640
|
+
if (!shouldActivate) return null;
|
|
1641
|
+
const cwd = await prompts.text({
|
|
1642
|
+
message: 'Project directory',
|
|
1643
|
+
defaultValue: process.cwd(),
|
|
1644
|
+
placeholder: process.cwd(),
|
|
1645
|
+
validate: (value) => {
|
|
1646
|
+
const resolved = path.resolve(String(value || ''));
|
|
1647
|
+
return fs.existsSync(resolved) ? undefined : 'Directory does not exist';
|
|
1648
|
+
},
|
|
1649
|
+
});
|
|
1650
|
+
if (prompts.isCancel(cwd)) throw new Error('cancelled');
|
|
1651
|
+
return activate(config, file, { room: roomId, cwd: String(cwd) });
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
async function runSetupSmoke(config, file, roomId, prompts) {
|
|
1655
|
+
if (!roomId) return null;
|
|
1656
|
+
const shouldSmoke = await prompts.confirm({
|
|
1657
|
+
message: 'Send a setup note and verify local BGE search?',
|
|
1658
|
+
initialValue: true,
|
|
1659
|
+
});
|
|
1660
|
+
if (prompts.isCancel(shouldSmoke)) throw new Error('cancelled');
|
|
1661
|
+
if (!shouldSmoke) return null;
|
|
1662
|
+
const marker = `setup-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1663
|
+
const text = `SuperCollab setup complete on ${os.hostname()} (${marker})`;
|
|
1664
|
+
await doChatSend(config, file, { room: roomId, text, channel: 'agents', kind: 'setup.note' });
|
|
1665
|
+
const search = await doChatSearch(config, file, { room: roomId, query: marker, mode: 'hybrid', limit: 5 });
|
|
1666
|
+
return {
|
|
1667
|
+
ok: search.results.some((row) => String(row.body || '').includes(marker)),
|
|
1668
|
+
marker,
|
|
1669
|
+
search_count: search.results.length,
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function mcpConfigText(client, file) {
|
|
1674
|
+
const escaped = file.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
1675
|
+
if (client === 'claude') {
|
|
1676
|
+
return JSON.stringify({
|
|
1677
|
+
mcpServers: {
|
|
1678
|
+
supercollab: {
|
|
1679
|
+
command: 'supercollab',
|
|
1680
|
+
args: ['mcp', 'stdio', '--config', file],
|
|
1681
|
+
},
|
|
1682
|
+
},
|
|
1683
|
+
}, null, 2);
|
|
1684
|
+
}
|
|
1685
|
+
if (client === 'codex') {
|
|
1686
|
+
return `[mcp_servers.supercollab]\ncommand = "supercollab"\nargs = ["mcp", "stdio", "--config", "${escaped}"]`;
|
|
1687
|
+
}
|
|
1688
|
+
return `supercollab mcp stdio --config "${escaped}"`;
|
|
1689
|
+
}
|
|
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
|
+
|
|
2208
|
+
async function runSetupWizard(config, file, opts = {}) {
|
|
2209
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2210
|
+
throw new Error('interactive setup requires a TTY; run `supercollab doctor --json` for non-interactive diagnostics');
|
|
2211
|
+
}
|
|
2212
|
+
const prompts = await loadPrompts();
|
|
2213
|
+
prompts.intro(`SuperCollab ${VERSION}`);
|
|
2214
|
+
try {
|
|
2215
|
+
const spin = prompts.spinner();
|
|
2216
|
+
spin.start('Checking this machine and installing the local BGE model');
|
|
2217
|
+
const doctor = await runDoctor(config, file, {});
|
|
2218
|
+
if (doctor.ok) {
|
|
2219
|
+
spin.stop(`Local engine ready: ${doctor.local_engine.id}, ${doctor.local_engine.vector_version || 'sqlite-vec'}`);
|
|
2220
|
+
} else {
|
|
2221
|
+
spin.stop('Local engine check needs attention');
|
|
2222
|
+
for (const line of doctor.advice || []) prompts.note(line, 'Fix');
|
|
2223
|
+
const cont = await prompts.confirm({ message: 'Continue setup anyway?', initialValue: false });
|
|
2224
|
+
if (prompts.isCancel(cont) || !cont) throw new Error('cancelled');
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
await runAuthSetup(config, file, prompts);
|
|
2228
|
+
const room = await runRoomSetup(config, file, prompts);
|
|
2229
|
+
const roomId = room?.room_id || null;
|
|
2230
|
+
const activation = await runActivationSetup(config, file, roomId, prompts);
|
|
2231
|
+
const smoke = await runSetupSmoke(config, file, roomId, prompts);
|
|
2232
|
+
|
|
2233
|
+
const client = await prompts.select({
|
|
2234
|
+
message: 'MCP client config',
|
|
2235
|
+
options: [
|
|
2236
|
+
{ value: 'codex', label: 'Codex', hint: 'TOML config snippet' },
|
|
2237
|
+
{ value: 'claude', label: 'Claude', hint: 'JSON config snippet' },
|
|
2238
|
+
{ value: 'manual', label: 'Manual', hint: 'stdio command' },
|
|
2239
|
+
{ value: 'skip', label: 'Skip', hint: 'show later with mcp print-config' },
|
|
2240
|
+
],
|
|
2241
|
+
});
|
|
2242
|
+
if (prompts.isCancel(client)) throw new Error('cancelled');
|
|
2243
|
+
|
|
2244
|
+
config.onboarding = {
|
|
2245
|
+
completed_at: nowIso(),
|
|
2246
|
+
cli_version: VERSION,
|
|
2247
|
+
room_id: roomId,
|
|
2248
|
+
activation_root: activation?.cwd || null,
|
|
2249
|
+
smoke,
|
|
2250
|
+
};
|
|
2251
|
+
saveConfig(config, file);
|
|
2252
|
+
|
|
2253
|
+
if (client !== 'skip') {
|
|
2254
|
+
prompts.note(mcpConfigText(client, file), `${client} MCP config`);
|
|
2255
|
+
}
|
|
2256
|
+
prompts.outro(`Ready. Config saved at ${file}`);
|
|
2257
|
+
return { ok: true, room_id: roomId, activation, smoke, config: file };
|
|
2258
|
+
} catch (err) {
|
|
2259
|
+
prompts.cancel(err.message === 'cancelled' ? 'Setup cancelled.' : `Setup stopped: ${err.message}`);
|
|
2260
|
+
throw err;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
1118
2264
|
async function main() {
|
|
1119
2265
|
const { positionals, opts } = parse(process.argv.slice(2));
|
|
1120
|
-
if (opts.help
|
|
2266
|
+
if (opts.help) { printHelp(); return; }
|
|
1121
2267
|
const [cmd, sub] = positionals;
|
|
1122
2268
|
const file = configPath(opts);
|
|
1123
2269
|
const config = attachRuntimeConfig(loadConfig(file), file);
|
|
1124
2270
|
config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
|
|
1125
2271
|
|
|
2272
|
+
if (positionals.length === 0) {
|
|
2273
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
2274
|
+
return isConfigured(config)
|
|
2275
|
+
? runMainMenu(config, file, opts)
|
|
2276
|
+
: runSetupWizard(config, file, opts);
|
|
2277
|
+
}
|
|
2278
|
+
printHelp();
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
if (cmd === 'menu') return runMainMenu(config, file, opts);
|
|
2282
|
+
if (cmd === 'setup') return runSetupWizard(config, file, opts);
|
|
2283
|
+
if (cmd === 'doctor') {
|
|
2284
|
+
const data = await runDoctor(config, file, opts);
|
|
2285
|
+
if (opts.json) return console.log(JSON.stringify(data, null, 2));
|
|
2286
|
+
printDoctor(data);
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
1126
2289
|
if (cmd === 'register') return console.log(JSON.stringify(await doRegister(config, file, opts), null, 2));
|
|
1127
2290
|
if (cmd === 'login') return console.log(JSON.stringify(await doLogin(config, file, opts), null, 2));
|
|
1128
2291
|
if (cmd === 'whoami') return console.log(JSON.stringify(await api(config, 'GET', '/v1/me'), null, 2));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supercollab/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "SuperCollab CLI and MCP bridge for encrypted local-search agent group chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"node": ">=20"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"@clack/prompts": "0.11.0",
|
|
17
18
|
"@huggingface/transformers": "3.8.1",
|
|
18
19
|
"better-sqlite3": "12.11.1",
|
|
19
20
|
"sqlite-vec": "0.1.9"
|