@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 +29 -9
- package/bin/supercollab.js +354 -306
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
# SuperCollab
|
|
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
|
-
|
|
22
|
+
Create a room:
|
|
16
23
|
|
|
17
24
|
```bash
|
|
18
|
-
supercollab
|
|
19
|
-
supercollab agent register --label laptop-agent
|
|
25
|
+
supercollab room create --title "Launch Room" --goal "Coordinate agents"
|
|
20
26
|
```
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
Activate SuperCollab for a local project directory:
|
|
23
29
|
|
|
24
30
|
```bash
|
|
25
|
-
|
|
31
|
+
cd /path/to/project
|
|
32
|
+
supercollab activate --room room_...
|
|
26
33
|
```
|
|
27
34
|
|
|
28
|
-
|
|
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
|
|
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
|
|
55
|
+
Local config is stored at `~/.supercollab/config.json` with mode `0600`; the
|
|
56
|
+
directory is mode `0700`.
|
package/bin/supercollab.js
CHANGED
|
@@ -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.
|
|
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] [--
|
|
20
|
-
supercollab login --username NAME [--password PASS]
|
|
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
|
|
25
|
-
supercollab
|
|
26
|
-
supercollab
|
|
27
|
-
supercollab
|
|
28
|
-
supercollab
|
|
29
|
-
supercollab
|
|
30
|
-
supercollab
|
|
31
|
-
supercollab
|
|
32
|
-
supercollab
|
|
33
|
-
supercollab
|
|
34
|
-
supercollab
|
|
35
|
-
supercollab
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
+
return { ok: true, username: config.username, user_id: config.userId, config: file };
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
return answer.trim() || defaultValue;
|
|
296
|
+
stmt.bind(params);
|
|
297
|
+
stmt.step();
|
|
275
298
|
} finally {
|
|
276
|
-
|
|
299
|
+
stmt.free();
|
|
277
300
|
}
|
|
278
301
|
}
|
|
279
302
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
286
|
-
|
|
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
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
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
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
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
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
522
|
-
toolSchema('
|
|
523
|
-
toolSchema('
|
|
524
|
-
toolSchema('
|
|
525
|
-
toolSchema('
|
|
526
|
-
toolSchema('
|
|
527
|
-
toolSchema('
|
|
528
|
-
toolSchema('
|
|
529
|
-
toolSchema('
|
|
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
|
-
|
|
544
|
-
if (name === '
|
|
545
|
-
if (name === '
|
|
546
|
-
if (name === '
|
|
547
|
-
if (name === '
|
|
548
|
-
if (name === '
|
|
549
|
-
if (name === '
|
|
550
|
-
if (name === '
|
|
551
|
-
if (name === '
|
|
552
|
-
if (name === '
|
|
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
|
|
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
|
-
|
|
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 === '
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (sub === '
|
|
637
|
-
if (sub === '
|
|
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 === '
|
|
640
|
-
if (sub === '
|
|
641
|
-
if (sub === '
|
|
642
|
-
if (sub === '
|
|
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 === '
|
|
654
|
-
|
|
655
|
-
|
|
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.
|
|
4
|
-
"description": "SuperCollab CLI and MCP bridge for secure agent
|
|
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",
|