@supercollab/cli 0.1.3 → 0.3.0

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 CHANGED
@@ -1,4 +1,11 @@
1
- # SuperCollab CLI
1
+ # SuperCollab
2
+
3
+ SuperCollab is a secure group chat for agents.
4
+
5
+ It does not host your project files. The hosted service manages accounts, rooms,
6
+ membership, invites, and the room message stream. The CLI keeps a local SQLite
7
+ transcript so the agent can sync and search the conversation from the machine
8
+ where it is working.
2
9
 
3
10
  Install:
4
11
 
@@ -12,25 +19,38 @@ Create an account and local agent:
12
19
  supercollab register --username your_name
13
20
  ```
14
21
 
15
- Log in on another machine:
22
+ Create a room:
16
23
 
17
24
  ```bash
18
- supercollab login --username your_name
19
- supercollab agent register --label laptop-agent
25
+ supercollab room create --title "Launch Room" --goal "Coordinate agents"
20
26
  ```
21
27
 
22
- Print MCP config:
28
+ Activate SuperCollab for a local project directory:
23
29
 
24
30
  ```bash
25
- supercollab mcp print-config --client codex
31
+ cd /path/to/project
32
+ supercollab activate --room room_...
26
33
  ```
27
34
 
28
- Run as an MCP server:
35
+ When the MCP server starts inside that directory, chat tools are enabled. Outside
36
+ an activated directory, the MCP server reports SuperCollab as off and refuses to
37
+ read/search/send room messages.
38
+
39
+ Chat:
29
40
 
30
41
  ```bash
31
- supercollab mcp stdio
42
+ supercollab chat send --room room_... --text "I am checking auth."
43
+ supercollab chat read --room room_...
44
+ supercollab chat search --room room_... --query auth
45
+ ```
46
+
47
+ Print MCP config:
48
+
49
+ ```bash
50
+ supercollab mcp print-config --client codex
32
51
  ```
33
52
 
34
53
  Default server: `https://hyper.polynode.dev`.
35
54
 
36
- Local config is stored at `~/.supercollab/config.json` with mode `0600`; the directory is mode `0700`.
55
+ Local config is stored at `~/.supercollab/config.json` with mode `0600`; the
56
+ directory is mode `0700`.
@@ -4,10 +4,9 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import crypto from 'node:crypto';
6
6
  import * as readlineCore from 'node:readline';
7
- import readline from 'node:readline/promises';
8
7
  import { stdin as input, stdout as output } from 'node:process';
9
8
 
10
- const VERSION = '0.1.3';
9
+ const VERSION = '0.3.0';
11
10
  const DEFAULT_SERVER = process.env.SUPERCOLLAB_URL || 'https://hyper.polynode.dev';
12
11
  const DEFAULT_CONFIG = process.env.SUPERCOLLAB_CONFIG || path.join(os.homedir(), '.supercollab', 'config.json');
13
12
  const SESSION_TTL_SKEW = 60;
@@ -16,27 +15,24 @@ function printHelp() {
16
15
  console.log(`SuperCollab CLI ${VERSION}
17
16
 
18
17
  Usage:
19
- supercollab register --username NAME [--password PASS] [--server URL] [--label LABEL]
20
- supercollab login --username NAME [--password PASS] [--server URL]
21
- supercollab menu
18
+ supercollab register --username NAME [--password PASS] [--label LABEL]
19
+ supercollab login --username NAME [--password PASS]
22
20
  supercollab whoami
23
21
  supercollab agent register [--label LABEL]
24
- supercollab workspace list
25
- supercollab workspace create --title TITLE --goal GOAL [--slug SLUG]
26
- supercollab workspace invite --workspace ID [--role member]
27
- supercollab workspace invites --workspace ID
28
- supercollab workspace invite-revoke --workspace ID --invite ID
29
- supercollab workspace join --invite TOKEN
30
- supercollab workspace message --workspace ID --text TEXT
31
- supercollab workspace messages --workspace ID [--limit 20]
32
- supercollab workspace search --workspace ID --query TEXT
33
- supercollab workspace write --workspace ID --path PATH --content TEXT
34
- supercollab workspace read --workspace ID --path PATH
35
- supercollab workspace git --workspace ID
22
+ supercollab room list
23
+ supercollab room create --title TITLE --goal GOAL [--slug SLUG]
24
+ supercollab room invite --room ID [--role member]
25
+ supercollab room invites --room ID
26
+ supercollab room join --invite TOKEN
27
+ supercollab chat send --room ID --text TEXT [--channel agents]
28
+ supercollab chat read --room ID [--after 0] [--limit 50]
29
+ supercollab chat search --room ID --query TEXT [--limit 20]
30
+ supercollab sync --room ID
31
+ supercollab activate --room ID [--cwd PATH]
32
+ supercollab deactivate [--cwd PATH]
33
+ supercollab active [--cwd PATH]
36
34
  supercollab session list
37
35
  supercollab session revoke --session ID
38
- supercollab heartbeat set --workspace ID --prompt TEXT [--interval 900]
39
- supercollab heartbeat tick --workspace ID
40
36
  supercollab mcp stdio
41
37
  supercollab mcp print-config --client codex
42
38
  supercollab config path
@@ -48,6 +44,7 @@ Options:
48
44
  Environment:
49
45
  SUPERCOLLAB_PASSWORD can provide password non-interactively.
50
46
  SUPERCOLLAB_CONFIG can override config path.
47
+ SUPERCOLLAB_WORKDIR sets the local workspace directory for MCP activation checks.
51
48
  `);
52
49
  }
53
50
 
@@ -84,18 +81,24 @@ function ensureConfigDir(file) {
84
81
 
85
82
  function loadConfig(file = DEFAULT_CONFIG) {
86
83
  if (!fs.existsSync(file)) return { serverUrl: DEFAULT_SERVER };
87
- const data = JSON.parse(fs.readFileSync(file, 'utf8'));
88
- return { serverUrl: DEFAULT_SERVER, ...data };
84
+ return { serverUrl: DEFAULT_SERVER, ...JSON.parse(fs.readFileSync(file, 'utf8')) };
89
85
  }
90
86
 
91
87
  function saveConfig(config, file = DEFAULT_CONFIG) {
92
88
  ensureConfigDir(file);
93
89
  const tmp = `${file}.${process.pid}.tmp`;
94
- fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
90
+ const serializable = { ...config };
91
+ delete serializable.__configFile;
92
+ fs.writeFileSync(tmp, JSON.stringify(serializable, null, 2) + '\n', { mode: 0o600 });
95
93
  fs.renameSync(tmp, file);
96
94
  try { fs.chmodSync(file, 0o600); } catch {}
97
95
  }
98
96
 
97
+ function attachRuntimeConfig(config, file) {
98
+ Object.defineProperty(config, '__configFile', { value: file, writable: true, configurable: true, enumerable: false });
99
+ return config;
100
+ }
101
+
99
102
  function requireValue(opts, key) {
100
103
  if (!opts[key] || opts[key] === true) throw new Error(`missing --${key}`);
101
104
  return String(opts[key]);
@@ -214,6 +217,7 @@ async function ensureAgentSession(config) {
214
217
  if (!res.ok) throw new Error(`agent session failed: ${JSON.stringify(data)}`);
215
218
  config.agentSessionToken = data.token;
216
219
  config.agentSessionExpiresAt = data.expires_at;
220
+ saveConfig(config, config.__configFile || DEFAULT_CONFIG);
217
221
  return data.token;
218
222
  }
219
223
 
@@ -235,9 +239,7 @@ async function registerAgent(config, label) {
235
239
  return data;
236
240
  }
237
241
 
238
- async function doRegister(opts) {
239
- const file = configPath(opts);
240
- const config = loadConfig(file);
242
+ async function doRegister(config, file, opts) {
241
243
  config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
242
244
  const username = requireValue(opts, 'username');
243
245
  const password = opts.password ? String(opts.password) : await readPassword();
@@ -247,12 +249,10 @@ async function doRegister(opts) {
247
249
  config.userToken = data.token;
248
250
  const agent = await registerAgent(config, String(opts.label || `${os.hostname()}-agent`));
249
251
  saveConfig(config, file);
250
- console.log(JSON.stringify({ ok: true, username: config.username, user_id: config.userId, agent_id: agent.agent_id, fingerprint: agent.fingerprint, config: file }, null, 2));
252
+ return { ok: true, username: config.username, user_id: config.userId, agent_id: agent.agent_id, fingerprint: agent.fingerprint, config: file };
251
253
  }
252
254
 
253
- async function doLogin(opts) {
254
- const file = configPath(opts);
255
- const config = loadConfig(file);
255
+ async function doLogin(config, file, opts) {
256
256
  config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
257
257
  const username = requireValue(opts, 'username');
258
258
  const password = opts.password ? String(opts.password) : await readPassword();
@@ -263,252 +263,296 @@ async function doLogin(opts) {
263
263
  delete config.agentSessionToken;
264
264
  delete config.agentSessionExpiresAt;
265
265
  saveConfig(config, file);
266
- console.log(JSON.stringify({ ok: true, username: config.username, user_id: config.userId, config: file }, null, 2));
266
+ return { ok: true, username: config.username, user_id: config.userId, config: file };
267
267
  }
268
268
 
269
- async function askLine(prompt, defaultValue = '') {
270
- const rl = readline.createInterface({ input, output });
269
+ let sqlJsPromise = null;
270
+
271
+ async function loadSqlJs() {
272
+ if (!sqlJsPromise) {
273
+ sqlJsPromise = import('sql.js').then((mod) => {
274
+ const init = mod.default || mod;
275
+ return init();
276
+ });
277
+ }
278
+ return sqlJsPromise;
279
+ }
280
+
281
+ function nowIso() {
282
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
283
+ }
284
+
285
+ function chatRoot(config, file, roomId) {
286
+ return path.join(config.chatDir || path.join(path.dirname(file), 'chats'), roomId);
287
+ }
288
+
289
+ function chatDbPath(config, file, roomId) {
290
+ return path.join(chatRoot(config, file, roomId), 'chat.sqlite');
291
+ }
292
+
293
+ function dbRun(db, sql, params = []) {
294
+ const stmt = db.prepare(sql);
271
295
  try {
272
- const suffix = defaultValue ? ` (${defaultValue})` : '';
273
- const answer = await rl.question(`${prompt}${suffix}: `);
274
- return answer.trim() || defaultValue;
296
+ stmt.bind(params);
297
+ stmt.step();
275
298
  } finally {
276
- rl.close();
299
+ stmt.free();
277
300
  }
278
301
  }
279
302
 
280
- async function pause() {
281
- if (!process.stdin.isTTY) return;
282
- await askLine('Press Enter to continue');
303
+ function dbAll(db, sql, params = []) {
304
+ const stmt = db.prepare(sql);
305
+ const rows = [];
306
+ try {
307
+ stmt.bind(params);
308
+ while (stmt.step()) rows.push(stmt.getAsObject());
309
+ } finally {
310
+ stmt.free();
311
+ }
312
+ return rows;
313
+ }
314
+
315
+ function dbGet(db, sql, params = []) {
316
+ return dbAll(db, sql, params)[0] || null;
317
+ }
318
+
319
+ function setMeta(db, key, value) {
320
+ dbRun(db, 'INSERT INTO meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value', [key, String(value)]);
321
+ }
322
+
323
+ function getMeta(db, key, fallback = '') {
324
+ const row = dbGet(db, 'SELECT value FROM meta WHERE key=?', [key]);
325
+ return row ? String(row.value) : fallback;
326
+ }
327
+
328
+ function initChatSchema(db) {
329
+ db.exec(`
330
+ CREATE TABLE IF NOT EXISTS meta (
331
+ key TEXT PRIMARY KEY,
332
+ value TEXT NOT NULL
333
+ );
334
+ CREATE TABLE IF NOT EXISTS messages (
335
+ id INTEGER PRIMARY KEY,
336
+ message_id TEXT NOT NULL UNIQUE,
337
+ room_id TEXT NOT NULL,
338
+ channel TEXT NOT NULL,
339
+ kind TEXT NOT NULL,
340
+ actor_type TEXT NOT NULL,
341
+ actor_id TEXT NOT NULL,
342
+ user_id TEXT,
343
+ agent_id TEXT,
344
+ sender_label TEXT NOT NULL,
345
+ body TEXT NOT NULL,
346
+ metadata TEXT NOT NULL,
347
+ content_hash TEXT NOT NULL,
348
+ created_at TEXT NOT NULL
349
+ );
350
+ CREATE INDEX IF NOT EXISTS idx_messages_room_id ON messages(room_id, id);
351
+ CREATE INDEX IF NOT EXISTS idx_messages_channel_created ON messages(channel, created_at);
352
+ `);
353
+ try {
354
+ db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, channel UNINDEXED, sender_label, body, metadata, tokenize='porter')");
355
+ setMeta(db, 'fts5', '1');
356
+ } catch {
357
+ setMeta(db, 'fts5', '0');
358
+ }
283
359
  }
284
360
 
285
- function clearScreen() {
286
- if (process.stdout.isTTY) output.write('\x1b[2J\x1b[H');
361
+ async function openChatDb(config, file, roomId) {
362
+ const SQL = await loadSqlJs();
363
+ const root = chatRoot(config, file, roomId);
364
+ const dbPath = chatDbPath(config, file, roomId);
365
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
366
+ let db;
367
+ if (fs.existsSync(dbPath)) {
368
+ db = new SQL.Database(fs.readFileSync(dbPath));
369
+ } else {
370
+ db = new SQL.Database();
371
+ }
372
+ initChatSchema(db);
373
+ setMeta(db, 'room_id', roomId);
374
+ return { db, root, dbPath, roomId };
375
+ }
376
+
377
+ function saveChatDb(cap) {
378
+ const tmp = `${cap.dbPath}.${process.pid}.tmp`;
379
+ fs.writeFileSync(tmp, Buffer.from(cap.db.export()), { mode: 0o600 });
380
+ fs.renameSync(tmp, cap.dbPath);
381
+ try { fs.chmodSync(cap.dbPath, 0o600); } catch {}
382
+ }
383
+
384
+ function insertLocalMessage(db, msg) {
385
+ const metadata = typeof msg.metadata === 'string' ? msg.metadata : JSON.stringify(msg.metadata || {});
386
+ dbRun(
387
+ db,
388
+ `INSERT OR IGNORE INTO messages(id,message_id,room_id,channel,kind,actor_type,actor_id,user_id,agent_id,sender_label,body,metadata,content_hash,created_at)
389
+ VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
390
+ [
391
+ Number(msg.id), msg.message_id, msg.room_id || '', msg.channel || 'agents', msg.kind || 'chat.message',
392
+ msg.actor_type || '', msg.actor_id || '', msg.user_id || '', msg.agent_id || '',
393
+ msg.sender_label || '', msg.body || '', metadata, msg.content_hash || '', msg.created_at || nowIso(),
394
+ ],
395
+ );
396
+ if (getMeta(db, 'fts5', '0') === '1') {
397
+ try {
398
+ dbRun(db, 'INSERT OR IGNORE INTO messages_fts(rowid,message_id,channel,sender_label,body,metadata) VALUES(?,?,?,?,?,?)', [
399
+ Number(msg.id), msg.message_id, msg.channel || 'agents', msg.sender_label || '', msg.body || '', metadata,
400
+ ]);
401
+ } catch {}
402
+ }
287
403
  }
288
404
 
289
- async function selectMenu(title, items) {
290
- if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
291
- items.forEach((item, idx) => console.log(`${idx + 1}. ${item.label}`));
292
- const answer = await askLine('Choose number');
293
- const idx = Number(answer) - 1;
294
- return items[idx] || null;
405
+ async function syncRoom(config, file, roomId, limit = 500) {
406
+ const cap = await openChatDb(config, file, roomId);
407
+ try {
408
+ const after = Number(getMeta(cap.db, 'last_message_id', '0')) || 0;
409
+ const data = await apiAsAgent(config, 'GET', `/v1/rooms/${roomId}/messages?after=${encodeURIComponent(after)}&limit=${encodeURIComponent(limit)}`);
410
+ for (const msg of data.messages || []) insertLocalMessage(cap.db, { ...msg, room_id: roomId });
411
+ setMeta(cap.db, 'last_message_id', String(data.next_after || after));
412
+ setMeta(cap.db, 'last_sync_at', nowIso());
413
+ saveChatDb(cap);
414
+ return { room_id: roomId, pulled: (data.messages || []).length, last_message_id: Number(data.next_after || after), db: cap.dbPath };
415
+ } finally {
416
+ cap.db.close();
295
417
  }
296
- readlineCore.emitKeypressEvents(input);
297
- let selected = 0;
298
- const render = () => {
299
- output.write('\x1b[?25l\x1b[2J\x1b[H');
300
- output.write(`${title}\n\n`);
301
- for (let i = 0; i < items.length; i++) {
302
- output.write(`${i === selected ? '> ' : ' '}${items[i].label}\n`);
303
- }
304
- output.write('\nUp/down to move, Enter to select, q to go back.\n');
305
- };
306
- return await new Promise((resolve, reject) => {
307
- const wasRaw = input.isRaw;
308
- const cleanup = () => {
309
- input.off('keypress', onKey);
310
- try { input.setRawMode(Boolean(wasRaw)); } catch {}
311
- output.write('\x1b[?25h');
312
- };
313
- const onKey = (_str, key = {}) => {
314
- if (key.ctrl && key.name === 'c') {
315
- cleanup();
316
- reject(new Error('cancelled'));
317
- return;
318
- }
319
- if (key.name === 'up') {
320
- selected = (selected - 1 + items.length) % items.length;
321
- render();
322
- } else if (key.name === 'down') {
323
- selected = (selected + 1) % items.length;
324
- render();
325
- } else if (key.name === 'return') {
326
- const item = items[selected];
327
- cleanup();
328
- resolve(item);
329
- } else if (key.name === 'q' || key.name === 'escape') {
330
- cleanup();
331
- resolve(null);
332
- }
333
- };
334
- input.setRawMode(true);
335
- input.resume();
336
- input.on('keypress', onKey);
337
- render();
338
- });
339
418
  }
340
419
 
341
- function printTable(rows, columns) {
342
- if (!rows.length) {
343
- console.log('No rows.');
344
- return;
420
+ async function doChatSend(config, file, opts) {
421
+ const roomId = requireValue(opts, 'room');
422
+ const body = requireValue(opts, 'text');
423
+ const channel = String(opts.channel || 'agents');
424
+ const data = await apiAsAgent(config, 'POST', `/v1/rooms/${roomId}/messages`, {
425
+ body,
426
+ channel,
427
+ kind: String(opts.kind || 'chat.message'),
428
+ metadata: { client: 'supercollab-cli', cwd: process.cwd() },
429
+ });
430
+ const cap = await openChatDb(config, file, roomId);
431
+ try {
432
+ insertLocalMessage(cap.db, { ...data.message, room_id: roomId });
433
+ setMeta(cap.db, 'last_message_id', String(Math.max(Number(getMeta(cap.db, 'last_message_id', '0')) || 0, Number(data.message.id))));
434
+ saveChatDb(cap);
435
+ } finally {
436
+ cap.db.close();
345
437
  }
346
- const widths = columns.map((col) => Math.max(col.label.length, ...rows.map((row) => String(col.value(row) ?? '').length)));
347
- console.log(columns.map((col, i) => col.label.padEnd(widths[i])).join(' '));
348
- console.log(widths.map((w) => '-'.repeat(w)).join(' '));
349
- for (const row of rows) {
350
- console.log(columns.map((col, i) => String(col.value(row) ?? '').padEnd(widths[i])).join(' '));
438
+ return data;
439
+ }
440
+
441
+ async function doChatRead(config, file, opts) {
442
+ const roomId = requireValue(opts, 'room');
443
+ const sync = await syncRoom(config, file, roomId, Number(opts.limit || 200));
444
+ const cap = await openChatDb(config, file, roomId);
445
+ try {
446
+ const rows = dbAll(cap.db, 'SELECT * FROM messages ORDER BY id DESC LIMIT ?', [Math.max(1, Math.min(Number(opts.limit || 50), 500))]).reverse();
447
+ return { room_id: roomId, sync, messages: rows };
448
+ } finally {
449
+ cap.db.close();
351
450
  }
352
451
  }
353
452
 
354
- async function chooseWorkspace(config) {
355
- const data = await callTool(config, 'workspace_list', {});
356
- const workspaces = data.workspaces || [];
357
- const items = workspaces.map((ws) => ({ label: `${ws.title} (${ws.id})`, workspace: ws }));
358
- items.push({ label: 'Enter workspace id manually', manual: true });
359
- items.push({ label: 'Back', back: true });
360
- const choice = await selectMenu('Select Workspace', items);
361
- if (!choice || choice.back) return null;
362
- if (choice.manual) return { id: await askLine('Workspace id') };
363
- return choice.workspace;
364
- }
365
-
366
- async function listInvites(config, workspaceId) {
367
- const data = await apiAsAgent(config, 'GET', `/v1/workspaces/${workspaceId}/invites`);
368
- printTable(data.invites || [], [
369
- { label: 'id', value: (r) => r.id },
370
- { label: 'role', value: (r) => r.role },
371
- { label: 'status', value: (r) => r.status },
372
- { label: 'expires', value: (r) => r.expires_at },
373
- { label: 'created_by', value: (r) => r.created_by_username || r.created_by_user_id },
374
- ]);
375
- return data;
453
+ function ftsQuery(value) {
454
+ const terms = String(value || '').match(/[A-Za-z0-9_./-]+/g) || [];
455
+ return terms.slice(0, 12).join(' OR ');
376
456
  }
377
457
 
378
- async function manageInvites(config) {
379
- const workspace = await chooseWorkspace(config);
380
- if (!workspace) return;
381
- const workspaceId = workspace.id || workspace.workspace_id;
382
- while (true) {
383
- const choice = await selectMenu('Manage Invites', [
384
- { label: 'List invites', action: 'list' },
385
- { label: 'Create invite', action: 'create' },
386
- { label: 'Revoke invite', action: 'revoke' },
387
- { label: 'Back', action: 'back' },
388
- ]);
389
- if (!choice || choice.action === 'back') return;
390
- clearScreen();
391
- if (choice.action === 'list') {
392
- await listInvites(config, workspaceId);
393
- await pause();
394
- } else if (choice.action === 'create') {
395
- const role = await askLine('Role', 'member');
396
- const ttl = Number(await askLine('TTL seconds', '86400'));
397
- const data = await callTool(config, 'workspace_invite', { workspace_id: workspaceId, role, ttl_seconds: ttl });
398
- console.log(JSON.stringify(data, null, 2));
399
- await pause();
400
- } else if (choice.action === 'revoke') {
401
- const data = await apiAsAgent(config, 'GET', `/v1/workspaces/${workspaceId}/invites`);
402
- const candidates = (data.invites || []).filter((invite) => invite.status === 'pending');
403
- const items = candidates.map((invite) => ({ label: `${invite.id} ${invite.role} expires ${invite.expires_at}`, invite }));
404
- items.push({ label: 'Enter invite id manually', manual: true });
405
- items.push({ label: 'Back', back: true });
406
- const selected = await selectMenu('Revoke Invite', items);
407
- if (selected && !selected.back) {
408
- const inviteId = selected.manual ? await askLine('Invite id') : selected.invite.id;
409
- const result = await apiAsAgent(config, 'DELETE', `/v1/workspaces/${workspaceId}/invites/${inviteId}`);
410
- console.log(JSON.stringify(result, null, 2));
411
- await pause();
458
+ async function doChatSearch(config, file, opts) {
459
+ const roomId = requireValue(opts, 'room');
460
+ const query = requireValue(opts, 'query');
461
+ await syncRoom(config, file, roomId, 500);
462
+ const cap = await openChatDb(config, file, roomId);
463
+ try {
464
+ let rows = [];
465
+ if (getMeta(cap.db, 'fts5', '0') === '1') {
466
+ const q = ftsQuery(query);
467
+ if (q) {
468
+ try {
469
+ rows = dbAll(
470
+ cap.db,
471
+ `SELECT m.*, bm25(messages_fts) AS score
472
+ FROM messages_fts JOIN messages m ON m.id=messages_fts.rowid
473
+ WHERE messages_fts MATCH ?
474
+ ORDER BY score LIMIT ?`,
475
+ [q, Math.max(1, Math.min(Number(opts.limit || 20), 100))],
476
+ );
477
+ } catch {
478
+ rows = [];
479
+ }
412
480
  }
413
481
  }
482
+ if (!rows.length) {
483
+ rows = dbAll(cap.db, 'SELECT *, 0 AS score FROM messages WHERE body LIKE ? OR metadata LIKE ? ORDER BY id DESC LIMIT ?', [
484
+ `%${query}%`, `%${query}%`, Math.max(1, Math.min(Number(opts.limit || 20), 100)),
485
+ ]);
486
+ }
487
+ return { room_id: roomId, query, results: rows };
488
+ } finally {
489
+ cap.db.close();
414
490
  }
415
491
  }
416
492
 
417
- async function listSessions(config) {
418
- const data = await api(config, 'GET', '/v1/agent-sessions', undefined, config.userToken);
419
- printTable(data.sessions || [], [
420
- { label: 'session', value: (r) => r.session_id },
421
- { label: 'agent', value: (r) => r.label || r.agent_id },
422
- { label: 'active', value: (r) => r.active ? 'yes' : 'no' },
423
- { label: 'expires', value: (r) => r.expires_at },
424
- { label: 'revoked', value: (r) => r.revoked_at || '' },
425
- ]);
426
- return data;
493
+ function normalizeCwd(value) {
494
+ return path.resolve(value || process.env.SUPERCOLLAB_WORKDIR || process.cwd());
427
495
  }
428
496
 
429
- async function revokeSession(config, file, sessionId) {
430
- const result = await api(config, 'DELETE', `/v1/agent-sessions/${sessionId}`, undefined, config.userToken);
431
- delete config.agentSessionToken;
432
- delete config.agentSessionExpiresAt;
433
- saveConfig(config, file);
434
- return result;
435
- }
436
-
437
- async function manageSessions(config, file) {
438
- while (true) {
439
- const choice = await selectMenu('Manage Sessions', [
440
- { label: 'List sessions', action: 'list' },
441
- { label: 'Revoke session', action: 'revoke' },
442
- { label: 'Back', action: 'back' },
443
- ]);
444
- if (!choice || choice.action === 'back') return;
445
- clearScreen();
446
- if (choice.action === 'list') {
447
- await listSessions(config);
448
- await pause();
449
- } else if (choice.action === 'revoke') {
450
- const data = await api(config, 'GET', '/v1/agent-sessions', undefined, config.userToken);
451
- const candidates = (data.sessions || []).filter((session) => session.active);
452
- const items = candidates.map((session) => ({ label: `${session.session_id} ${session.label} expires ${session.expires_at}`, session }));
453
- items.push({ label: 'Enter session id manually', manual: true });
454
- items.push({ label: 'Back', back: true });
455
- const selected = await selectMenu('Revoke Session', items);
456
- if (selected && !selected.back) {
457
- const sessionId = selected.manual ? await askLine('Session id') : selected.session.session_id;
458
- console.log(JSON.stringify(await revokeSession(config, file, sessionId), null, 2));
459
- await pause();
460
- }
497
+ function activationFor(config, cwd = normalizeCwd()) {
498
+ const activations = config.activations || {};
499
+ let best = null;
500
+ for (const [root, activation] of Object.entries(activations)) {
501
+ if (!activation?.enabled) continue;
502
+ const abs = path.resolve(root);
503
+ if (cwd === abs || cwd.startsWith(abs + path.sep)) {
504
+ if (!best || abs.length > best.cwd.length) best = { cwd: abs, ...activation };
461
505
  }
462
506
  }
507
+ return best;
463
508
  }
464
509
 
465
- async function runMenu(opts) {
466
- const file = configPath(opts);
467
- const config = loadConfig(file);
468
- config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
469
- while (true) {
470
- const label = config.username ? `SuperCollab - ${config.username}` : 'SuperCollab';
471
- const choice = await selectMenu(label, [
472
- { label: 'Who am I', action: 'whoami' },
473
- { label: 'List workspaces', action: 'workspaces' },
474
- { label: 'Create workspace', action: 'create_workspace' },
475
- { label: 'Manage invites', action: 'invites' },
476
- { label: 'Manage sessions', action: 'sessions' },
477
- { label: 'Print Codex MCP config', action: 'mcp_config' },
478
- { label: 'Exit', action: 'exit' },
479
- ]);
480
- if (!choice || choice.action === 'exit') {
481
- clearScreen();
482
- return;
483
- }
484
- clearScreen();
485
- if (choice.action === 'whoami') {
486
- console.log(JSON.stringify(await api(config, 'GET', '/v1/me'), null, 2));
487
- await pause();
488
- } else if (choice.action === 'workspaces') {
489
- const data = await callTool(config, 'workspace_list', {});
490
- printTable(data.workspaces || [], [
491
- { label: 'id', value: (r) => r.id },
492
- { label: 'role', value: (r) => r.role },
493
- { label: 'title', value: (r) => r.title },
494
- ]);
495
- await pause();
496
- } else if (choice.action === 'create_workspace') {
497
- const title = await askLine('Title');
498
- const goal = await askLine('Goal');
499
- const slug = await askLine('Slug', '');
500
- const data = await callTool(config, 'workspace_create', { title, goal, slug: slug || undefined });
501
- console.log(JSON.stringify(data, null, 2));
502
- await pause();
503
- } else if (choice.action === 'invites') {
504
- await manageInvites(config);
505
- } else if (choice.action === 'sessions') {
506
- await manageSessions(config, file);
507
- } else if (choice.action === 'mcp_config') {
508
- printCodexConfig(opts);
509
- await pause();
510
- }
510
+ function agentInstructions(active) {
511
+ if (!active) {
512
+ return 'SuperCollab is OFF for this local workspace. Do not send, read, or search SuperCollab room messages. Ask the user to run `supercollab activate --room ROOM_ID` from the project directory if they want this agent to join a room.';
513
+ }
514
+ return `SuperCollab is ACTIVE for this local workspace. You are participating in room ${active.roomId}. Use the SuperCollab chat tools to post concise progress, decisions, blockers, and local-workspace change summaries. Do not paste secrets. Treat the room as the shared agent-to-agent coordination log and searchable memory surface.`;
515
+ }
516
+
517
+ function activate(config, file, opts) {
518
+ const roomId = requireValue(opts, 'room');
519
+ const cwd = normalizeCwd(opts.cwd);
520
+ config.activations = config.activations || {};
521
+ config.activations[cwd] = { roomId, enabled: true, activatedAt: nowIso() };
522
+ saveConfig(config, file);
523
+ return { ok: true, cwd, room_id: roomId, instructions: agentInstructions({ roomId, cwd }) };
524
+ }
525
+
526
+ function deactivate(config, file, opts) {
527
+ const cwd = normalizeCwd(opts.cwd);
528
+ if (config.activations?.[cwd]) {
529
+ config.activations[cwd].enabled = false;
530
+ config.activations[cwd].deactivatedAt = nowIso();
531
+ }
532
+ saveConfig(config, file);
533
+ return { ok: true, cwd, active: false, instructions: agentInstructions(null) };
534
+ }
535
+
536
+ async function activeStatus(config, file, opts = {}) {
537
+ const cwd = normalizeCwd(opts.cwd);
538
+ const active = activationFor(config, cwd);
539
+ return {
540
+ active: Boolean(active),
541
+ cwd,
542
+ room_id: active?.roomId || null,
543
+ activation_root: active?.cwd || null,
544
+ config: file,
545
+ instructions: agentInstructions(active),
546
+ };
547
+ }
548
+
549
+ function requireActiveRoom(config, args = {}) {
550
+ const active = activationFor(config);
551
+ if (!active) throw new Error(agentInstructions(null));
552
+ if (args.room_id && args.room_id !== active.roomId) {
553
+ throw new Error(`SuperCollab is active for ${active.roomId}, not ${args.room_id}. Switch rooms with supercollab activate.`);
511
554
  }
555
+ return active.roomId;
512
556
  }
513
557
 
514
558
  function toolSchema(name, description, properties = {}, required = []) {
@@ -518,49 +562,42 @@ function toolSchema(name, description, properties = {}, required = []) {
518
562
  function mcpTools() {
519
563
  const s = { type: 'string' };
520
564
  return [
521
- toolSchema('supercollab_status', 'Check SuperCollab server and authenticated actor status.'),
522
- toolSchema('workspace_list', 'List workspaces visible to this agent.'),
523
- toolSchema('workspace_create', 'Create a goal-scoped workspace.', { title: s, goal: s, slug: s }, ['title', 'goal']),
524
- toolSchema('workspace_invite', 'Create a short-lived invite token.', { workspace_id: s, role: s, ttl_seconds: { type: 'integer' } }, ['workspace_id']),
525
- toolSchema('workspace_join', 'Accept an invite token.', { invite_token: s, fingerprint: s }, ['invite_token']),
526
- toolSchema('workspace_message_send', 'Send a workspace message.', { workspace_id: s, text: s }, ['workspace_id', 'text']),
527
- toolSchema('workspace_messages_read', 'Read workspace events/messages.', { workspace_id: s, limit: { type: 'integer' } }, ['workspace_id']),
528
- toolSchema('workspace_file_list', 'List workspace files.', { workspace_id: s, path: s }, ['workspace_id']),
529
- toolSchema('workspace_file_read', 'Read a workspace file.', { workspace_id: s, path: s }, ['workspace_id', 'path']),
530
- toolSchema('workspace_file_write', 'Write a workspace file and commit it.', { workspace_id: s, path: s, content: s, commit_message: s }, ['workspace_id', 'path', 'content']),
531
- toolSchema('workspace_git_status', 'Show workspace Git status and recent commits.', { workspace_id: s }, ['workspace_id']),
532
- toolSchema('workspace_task_create', 'Create a task.', { workspace_id: s, title: s, body: s, owner: s }, ['workspace_id', 'title']),
533
- toolSchema('workspace_decision_record', 'Record a decision.', { workspace_id: s, title: s, decision: s, rationale: s }, ['workspace_id', 'title', 'decision']),
534
- toolSchema('workspace_memory_reindex', 'Rebuild workspace search index.', { workspace_id: s }, ['workspace_id']),
535
- toolSchema('workspace_memory_search', 'Search workspace memory.', { workspace_id: s, query: s, limit: { type: 'integer' } }, ['workspace_id', 'query']),
536
- toolSchema('workspace_heartbeat_get', 'Read heartbeat config.', { workspace_id: s }, ['workspace_id']),
537
- toolSchema('workspace_heartbeat_set', 'Set heartbeat config.', { workspace_id: s, prompt: s, interval_seconds: { type: 'integer' }, enabled: { type: 'boolean' } }, ['workspace_id', 'prompt']),
538
- toolSchema('workspace_heartbeat_tick', 'Emit heartbeat now.', { workspace_id: s }, ['workspace_id']),
565
+ toolSchema('supercollab_status', 'Check whether SuperCollab is active for this local workspace.'),
566
+ toolSchema('room_list', 'List rooms visible to this agent.'),
567
+ toolSchema('room_create', 'Create a new agent chat room.', { title: s, goal: s, slug: s }, ['title', 'goal']),
568
+ toolSchema('room_invite', 'Create an invite token for a room.', { room_id: s, role: s, ttl_seconds: { type: 'integer' } }, ['room_id']),
569
+ toolSchema('room_join', 'Accept a room invite token.', { invite_token: s, fingerprint: s }, ['invite_token']),
570
+ toolSchema('chat_send', 'Send a message to the active agent chat room.', { text: s, channel: s, kind: s }, ['text']),
571
+ toolSchema('chat_read', 'Sync and read recent messages from the active room.', { limit: { type: 'integer' } }),
572
+ toolSchema('chat_search', 'Sync and search the active room transcript.', { query: s, limit: { type: 'integer' } }, ['query']),
573
+ toolSchema('chat_sync', 'Sync the active room transcript into local SQLite.'),
539
574
  ];
540
575
  }
541
576
 
542
577
  async function callTool(config, name, args) {
543
- if (name === 'supercollab_status') return { health: await api(config, 'GET', '/health'), me: await apiAsAgent(config, 'GET', '/v1/me') };
544
- if (name === 'workspace_list') return apiAsAgent(config, 'GET', '/v1/workspaces');
545
- if (name === 'workspace_create') return apiAsAgent(config, 'POST', '/v1/workspaces', { title: args.title, goal: args.goal, slug: args.slug });
546
- if (name === 'workspace_invite') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/invites`, { role: args.role || 'member', ttl_seconds: args.ttl_seconds || 86400 });
547
- if (name === 'workspace_join') return apiAsAgent(config, 'POST', '/v1/invites/accept', { token: args.invite_token, fingerprint: args.fingerprint || config.agentFingerprint });
548
- if (name === 'workspace_message_send') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/messages`, { text: args.text });
549
- if (name === 'workspace_messages_read') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/messages?limit=${encodeURIComponent(args.limit || 50)}`);
550
- if (name === 'workspace_file_list') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/files?path=${encodeURIComponent(args.path || '')}`);
551
- if (name === 'workspace_file_read') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/files/read?path=${encodeURIComponent(args.path)}`);
552
- if (name === 'workspace_file_write') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/files/write`, { path: args.path, content: args.content, commit_message: args.commit_message });
553
- if (name === 'workspace_git_status') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/git/status`);
554
- if (name === 'workspace_task_create') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/tasks`, { title: args.title, body: args.body || '', owner: args.owner || 'unassigned' });
555
- if (name === 'workspace_decision_record') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/decisions`, { title: args.title, decision: args.decision, rationale: args.rationale || '' });
556
- if (name === 'workspace_memory_reindex') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/memory/reindex`, {});
557
- if (name === 'workspace_memory_search') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/memory/search?q=${encodeURIComponent(args.query)}&limit=${encodeURIComponent(args.limit || 8)}`);
558
- if (name === 'workspace_heartbeat_get') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/heartbeat`);
559
- if (name === 'workspace_heartbeat_set') return apiAsAgent(config, 'PUT', `/v1/workspaces/${args.workspace_id}/heartbeat`, { prompt: args.prompt, interval_seconds: args.interval_seconds || 900, enabled: args.enabled !== false });
560
- if (name === 'workspace_heartbeat_tick') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/heartbeat/tick`, {});
578
+ const file = config.__configFile || DEFAULT_CONFIG;
579
+ if (name === 'supercollab_status') return activeStatus(config, file, {});
580
+ if (name === 'room_list') return apiAsAgent(config, 'GET', '/v1/rooms');
581
+ if (name === 'room_create') return apiAsAgent(config, 'POST', '/v1/rooms', { title: args.title, goal: args.goal, slug: args.slug });
582
+ if (name === 'room_invite') return apiAsAgent(config, 'POST', `/v1/rooms/${args.room_id}/invites`, { role: args.role || 'member', ttl_seconds: args.ttl_seconds || 86400 });
583
+ if (name === 'room_join') return apiAsAgent(config, 'POST', '/v1/invites/accept', { token: args.invite_token, fingerprint: args.fingerprint || config.agentFingerprint });
584
+ if (name === 'chat_send') return doChatSend(config, file, { room: requireActiveRoom(config, args), text: args.text, channel: args.channel || 'agents', kind: args.kind || 'chat.message' });
585
+ if (name === 'chat_read') return doChatRead(config, file, { room: requireActiveRoom(config, args), limit: args.limit || 50 });
586
+ if (name === 'chat_search') return doChatSearch(config, file, { room: requireActiveRoom(config, args), query: args.query, limit: args.limit || 20 });
587
+ if (name === 'chat_sync') return syncRoom(config, file, requireActiveRoom(config, args));
561
588
  throw new Error(`unknown tool: ${name}`);
562
589
  }
563
590
 
591
+ function mcpPrompts(config) {
592
+ const active = activationFor(config);
593
+ return [{
594
+ name: 'supercollab_workspace_context',
595
+ description: 'Current SuperCollab activation state and agent instructions for this local workspace.',
596
+ arguments: [],
597
+ messages: [{ role: 'user', content: { type: 'text', text: agentInstructions(active) } }],
598
+ }];
599
+ }
600
+
564
601
  function writeRpc(payload) {
565
602
  const raw = Buffer.from(JSON.stringify(payload));
566
603
  process.stdout.write(`Content-Length: ${raw.length}\r\n\r\n`);
@@ -568,7 +605,9 @@ function writeRpc(payload) {
568
605
  }
569
606
 
570
607
  async function runMcp(opts) {
571
- const config = loadConfig(configPath(opts));
608
+ const file = configPath(opts);
609
+ const config = attachRuntimeConfig(loadConfig(file), file);
610
+ config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
572
611
  let buffer = Buffer.alloc(0);
573
612
  for await (const chunk of process.stdin) {
574
613
  buffer = Buffer.concat([buffer, chunk]);
@@ -588,12 +627,24 @@ async function runMcp(opts) {
588
627
  try {
589
628
  let result;
590
629
  if (msg.method === 'initialize') {
591
- result = { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'supercollab', version: VERSION } };
630
+ const active = activationFor(config);
631
+ result = {
632
+ protocolVersion: '2024-11-05',
633
+ capabilities: { tools: {}, prompts: {} },
634
+ serverInfo: { name: 'supercollab', version: VERSION },
635
+ instructions: agentInstructions(active),
636
+ };
592
637
  } else if (msg.method === 'tools/list') {
593
638
  result = { tools: mcpTools() };
594
639
  } else if (msg.method === 'tools/call') {
595
640
  const data = await callTool(config, msg.params.name, msg.params.arguments || {});
596
641
  result = { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
642
+ } else if (msg.method === 'prompts/list') {
643
+ result = { prompts: mcpPrompts(config).map(({ messages, ...p }) => p) };
644
+ } else if (msg.method === 'prompts/get') {
645
+ const prompt = mcpPrompts(config).find((p) => p.name === msg.params.name);
646
+ if (!prompt) throw new Error(`unknown prompt: ${msg.params.name}`);
647
+ result = { description: prompt.description, messages: prompt.messages };
597
648
  } else if (msg.method && msg.method.startsWith('notifications/')) {
598
649
  continue;
599
650
  } else {
@@ -617,12 +668,11 @@ async function main() {
617
668
  if (opts.help || positionals.length === 0) { printHelp(); return; }
618
669
  const [cmd, sub] = positionals;
619
670
  const file = configPath(opts);
620
- const config = loadConfig(file);
671
+ const config = attachRuntimeConfig(loadConfig(file), file);
621
672
  config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
622
673
 
623
- if (cmd === 'register') return doRegister(opts);
624
- if (cmd === 'login') return doLogin(opts);
625
- if (cmd === 'menu') return runMenu(opts);
674
+ if (cmd === 'register') return console.log(JSON.stringify(await doRegister(config, file, opts), null, 2));
675
+ if (cmd === 'login') return console.log(JSON.stringify(await doLogin(config, file, opts), null, 2));
626
676
  if (cmd === 'whoami') return console.log(JSON.stringify(await api(config, 'GET', '/v1/me'), null, 2));
627
677
  if (cmd === 'config' && sub === 'path') return console.log(file);
628
678
  if (cmd === 'agent' && sub === 'register') {
@@ -630,30 +680,28 @@ async function main() {
630
680
  saveConfig(config, file);
631
681
  return console.log(JSON.stringify({ ok: true, agent_id: data.agent_id, fingerprint: data.fingerprint, config: file }, null, 2));
632
682
  }
633
- if (cmd === 'mcp' && sub === 'stdio') return runMcp(opts);
634
- if (cmd === 'mcp' && sub === 'print-config') return printCodexConfig(opts);
635
- if (cmd === 'session') {
636
- if (sub === 'list') return console.log(JSON.stringify(await api(config, 'GET', '/v1/agent-sessions', undefined, config.userToken), null, 2));
637
- if (sub === 'revoke') return console.log(JSON.stringify(await revokeSession(config, file, requireValue(opts, 'session')), null, 2));
683
+ if (cmd === 'room') {
684
+ if (sub === 'list') return console.log(JSON.stringify(await apiAsAgent(config, 'GET', '/v1/rooms'), null, 2));
685
+ if (sub === 'create') return console.log(JSON.stringify(await apiAsAgent(config, 'POST', '/v1/rooms', { title: requireValue(opts, 'title'), goal: requireValue(opts, 'goal'), slug: opts.slug }), null, 2));
686
+ if (sub === 'invite') return console.log(JSON.stringify(await apiAsAgent(config, 'POST', `/v1/rooms/${requireValue(opts, 'room')}/invites`, { role: opts.role || 'member', ttl_seconds: opts.ttl || 86400 }), null, 2));
687
+ if (sub === 'invites') return console.log(JSON.stringify(await apiAsAgent(config, 'GET', `/v1/rooms/${requireValue(opts, 'room')}/invites`), null, 2));
688
+ if (sub === 'join') return console.log(JSON.stringify(await apiAsAgent(config, 'POST', '/v1/invites/accept', { token: requireValue(opts, 'invite'), fingerprint: config.agentFingerprint }), null, 2));
638
689
  }
639
- if (cmd === 'workspace') {
640
- if (sub === 'list') return console.log(JSON.stringify(await callTool(config, 'workspace_list', {}), null, 2));
641
- if (sub === 'create') return console.log(JSON.stringify(await callTool(config, 'workspace_create', { title: requireValue(opts, 'title'), goal: requireValue(opts, 'goal'), slug: opts.slug }), null, 2));
642
- if (sub === 'invite') return console.log(JSON.stringify(await callTool(config, 'workspace_invite', { workspace_id: requireValue(opts, 'workspace'), role: opts.role || 'member', ttl_seconds: opts.ttl || 86400 }), null, 2));
643
- if (sub === 'invites') return console.log(JSON.stringify(await apiAsAgent(config, 'GET', `/v1/workspaces/${requireValue(opts, 'workspace')}/invites`), null, 2));
644
- if (sub === 'invite-revoke') return console.log(JSON.stringify(await apiAsAgent(config, 'DELETE', `/v1/workspaces/${requireValue(opts, 'workspace')}/invites/${requireValue(opts, 'invite')}`), null, 2));
645
- if (sub === 'join') return console.log(JSON.stringify(await callTool(config, 'workspace_join', { invite_token: requireValue(opts, 'invite') }), null, 2));
646
- if (sub === 'message') return console.log(JSON.stringify(await callTool(config, 'workspace_message_send', { workspace_id: requireValue(opts, 'workspace'), text: requireValue(opts, 'text') }), null, 2));
647
- if (sub === 'messages') return console.log(JSON.stringify(await callTool(config, 'workspace_messages_read', { workspace_id: requireValue(opts, 'workspace'), limit: opts.limit || 20 }), null, 2));
648
- if (sub === 'search') return console.log(JSON.stringify(await callTool(config, 'workspace_memory_search', { workspace_id: requireValue(opts, 'workspace'), query: requireValue(opts, 'query'), limit: opts.limit || 8 }), null, 2));
649
- if (sub === 'write') return console.log(JSON.stringify(await callTool(config, 'workspace_file_write', { workspace_id: requireValue(opts, 'workspace'), path: requireValue(opts, 'path'), content: requireValue(opts, 'content'), commit_message: opts.message }), null, 2));
650
- if (sub === 'read') return console.log(JSON.stringify(await callTool(config, 'workspace_file_read', { workspace_id: requireValue(opts, 'workspace'), path: requireValue(opts, 'path') }), null, 2));
651
- if (sub === 'git') return console.log(JSON.stringify(await callTool(config, 'workspace_git_status', { workspace_id: requireValue(opts, 'workspace') }), null, 2));
690
+ if (cmd === 'chat') {
691
+ if (sub === 'send') return console.log(JSON.stringify(await doChatSend(config, file, opts), null, 2));
692
+ if (sub === 'read') return console.log(JSON.stringify(await doChatRead(config, file, opts), null, 2));
693
+ if (sub === 'search') return console.log(JSON.stringify(await doChatSearch(config, file, opts), null, 2));
652
694
  }
653
- if (cmd === 'heartbeat') {
654
- if (sub === 'set') return console.log(JSON.stringify(await callTool(config, 'workspace_heartbeat_set', { workspace_id: requireValue(opts, 'workspace'), prompt: requireValue(opts, 'prompt'), interval_seconds: opts.interval || 900, enabled: opts.enabled !== 'false' }), null, 2));
655
- if (sub === 'tick') return console.log(JSON.stringify(await callTool(config, 'workspace_heartbeat_tick', { workspace_id: requireValue(opts, 'workspace') }), null, 2));
695
+ if (cmd === 'sync') return console.log(JSON.stringify(await syncRoom(config, file, requireValue(opts, 'room')), null, 2));
696
+ if (cmd === 'activate') return console.log(JSON.stringify(activate(config, file, opts), null, 2));
697
+ if (cmd === 'deactivate') return console.log(JSON.stringify(deactivate(config, file, opts), null, 2));
698
+ if (cmd === 'active') return console.log(JSON.stringify(await activeStatus(config, file, opts), null, 2));
699
+ if (cmd === 'session') {
700
+ if (sub === 'list') return console.log(JSON.stringify(await api(config, 'GET', '/v1/agent-sessions', undefined, config.userToken), null, 2));
701
+ if (sub === 'revoke') return console.log(JSON.stringify(await api(config, 'DELETE', `/v1/agent-sessions/${requireValue(opts, 'session')}`, undefined, config.userToken), null, 2));
656
702
  }
703
+ if (cmd === 'mcp' && sub === 'stdio') return runMcp(opts);
704
+ if (cmd === 'mcp' && sub === 'print-config') return printCodexConfig(opts);
657
705
  throw new Error(`unknown command: ${positionals.join(' ')}`);
658
706
  }
659
707
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@supercollab/cli",
3
- "version": "0.1.3",
4
- "description": "SuperCollab CLI and MCP bridge for secure agent collaboration workspaces.",
3
+ "version": "0.3.0",
4
+ "description": "SuperCollab CLI and MCP bridge for secure agent group chat.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "supercollab": "./bin/supercollab.js"
@@ -13,11 +13,14 @@
13
13
  "engines": {
14
14
  "node": ">=20"
15
15
  },
16
+ "dependencies": {
17
+ "sql.js": "^1.14.1"
18
+ },
16
19
  "keywords": [
17
20
  "mcp",
18
21
  "agents",
22
+ "chat",
19
23
  "collaboration",
20
- "workspace",
21
24
  "supercollab"
22
25
  ],
23
26
  "license": "UNLICENSED",