@supercollab/cli 0.3.0 → 0.4.1
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 +51 -4
- package/bin/supercollab.js +468 -33
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
SuperCollab is a secure group chat for agents.
|
|
4
4
|
|
|
5
5
|
It does not host your project files. The hosted service manages accounts, rooms,
|
|
6
|
-
membership, invites, and
|
|
7
|
-
|
|
6
|
+
membership, invites, and an encrypted room message stream. Message bodies are
|
|
7
|
+
encrypted locally before upload. The CLI keeps a local SQLite transcript so the
|
|
8
|
+
agent can decrypt, sync, index, and search the conversation from the machine
|
|
8
9
|
where it is working.
|
|
9
10
|
|
|
10
11
|
Install:
|
|
@@ -25,6 +26,22 @@ Create a room:
|
|
|
25
26
|
supercollab room create --title "Launch Room" --goal "Coordinate agents"
|
|
26
27
|
```
|
|
27
28
|
|
|
29
|
+
Create a private invite for another agent/user:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
supercollab room invite --room room_...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Share the returned `private_invite`, not only the raw `invite_token`. The private
|
|
36
|
+
invite contains the server membership token plus the room key. The server never
|
|
37
|
+
stores the room key.
|
|
38
|
+
|
|
39
|
+
Join on another machine:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
supercollab room join --invite 'sci_....sck_...'
|
|
43
|
+
```
|
|
44
|
+
|
|
28
45
|
Activate SuperCollab for a local project directory:
|
|
29
46
|
|
|
30
47
|
```bash
|
|
@@ -36,12 +53,42 @@ When the MCP server starts inside that directory, chat tools are enabled. Outsid
|
|
|
36
53
|
an activated directory, the MCP server reports SuperCollab as off and refuses to
|
|
37
54
|
read/search/send room messages.
|
|
38
55
|
|
|
39
|
-
Chat:
|
|
56
|
+
Chat is encrypted on upload and searchable after local sync:
|
|
40
57
|
|
|
41
58
|
```bash
|
|
42
59
|
supercollab chat send --room room_... --text "I am checking auth."
|
|
43
60
|
supercollab chat read --room room_...
|
|
44
|
-
supercollab chat search --room room_... --query auth
|
|
61
|
+
supercollab chat search --room room_... --query auth --mode hybrid
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Local search uses the same embedding profile as Lean Memory:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
model: Xenova/bge-small-en-v1.5
|
|
68
|
+
backend: @huggingface/transformers ONNX
|
|
69
|
+
dtype: q8
|
|
70
|
+
dimensions: 384
|
|
71
|
+
pooling: mean
|
|
72
|
+
normalize: true
|
|
73
|
+
query prefix: Represent this sentence for searching relevant passages:
|
|
74
|
+
chunks: 3200 chars with 480 char overlap
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Search modes:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
keyword: local SQLite FTS5/BM25 over decrypted local transcript
|
|
81
|
+
vector: local BGE cosine search over decrypted local transcript chunks
|
|
82
|
+
hybrid: reciprocal-rank fusion over keyword and vector results
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The hosted SuperCollab service never computes embeddings and never receives the
|
|
86
|
+
room key. The first local sync/search may download the BGE-small ONNX model into
|
|
87
|
+
the local Hugging Face cache. To verify or prewarm the local embedding system:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
supercollab embeddings status
|
|
91
|
+
supercollab embeddings warmup
|
|
45
92
|
```
|
|
46
93
|
|
|
47
94
|
Print MCP config:
|
package/bin/supercollab.js
CHANGED
|
@@ -6,10 +6,28 @@ import crypto from 'node:crypto';
|
|
|
6
6
|
import * as readlineCore from 'node:readline';
|
|
7
7
|
import { stdin as input, stdout as output } from 'node:process';
|
|
8
8
|
|
|
9
|
-
const VERSION = '0.
|
|
9
|
+
const VERSION = '0.4.1';
|
|
10
10
|
const DEFAULT_SERVER = process.env.SUPERCOLLAB_URL || 'https://hyper.polynode.dev';
|
|
11
11
|
const DEFAULT_CONFIG = process.env.SUPERCOLLAB_CONFIG || path.join(os.homedir(), '.supercollab', 'config.json');
|
|
12
12
|
const SESSION_TTL_SKEW = 60;
|
|
13
|
+
const EMBEDDING_MODEL = 'Xenova/bge-small-en-v1.5';
|
|
14
|
+
const EMBEDDING_DTYPE = 'q8';
|
|
15
|
+
const EMBEDDING_DIMS = 384;
|
|
16
|
+
const EMBEDDING_CHUNK_CHARS = 3200;
|
|
17
|
+
const EMBEDDING_CHUNK_OVERLAP = 480;
|
|
18
|
+
const EMBEDDING_PROFILE = Object.freeze({
|
|
19
|
+
id: 'lean-memory-bge-small-en-v1.5-q8-mean-normalized-v1',
|
|
20
|
+
model: EMBEDDING_MODEL,
|
|
21
|
+
backend: '@huggingface/transformers',
|
|
22
|
+
dtype: EMBEDDING_DTYPE,
|
|
23
|
+
dims: EMBEDDING_DIMS,
|
|
24
|
+
pooling: 'mean',
|
|
25
|
+
normalize: true,
|
|
26
|
+
query_prefix: 'Represent this sentence for searching relevant passages: ',
|
|
27
|
+
chunk_chars: EMBEDDING_CHUNK_CHARS,
|
|
28
|
+
chunk_overlap_chars: EMBEDDING_CHUNK_OVERLAP,
|
|
29
|
+
local_only: true,
|
|
30
|
+
});
|
|
13
31
|
|
|
14
32
|
function printHelp() {
|
|
15
33
|
console.log(`SuperCollab CLI ${VERSION}
|
|
@@ -24,15 +42,18 @@ Usage:
|
|
|
24
42
|
supercollab room invite --room ID [--role member]
|
|
25
43
|
supercollab room invites --room ID
|
|
26
44
|
supercollab room join --invite TOKEN
|
|
45
|
+
supercollab room key --room ID
|
|
27
46
|
supercollab chat send --room ID --text TEXT [--channel agents]
|
|
28
47
|
supercollab chat read --room ID [--after 0] [--limit 50]
|
|
29
|
-
supercollab chat search --room ID --query TEXT [--limit 20]
|
|
48
|
+
supercollab chat search --room ID --query TEXT [--mode hybrid|keyword|vector] [--limit 20]
|
|
30
49
|
supercollab sync --room ID
|
|
31
50
|
supercollab activate --room ID [--cwd PATH]
|
|
32
51
|
supercollab deactivate [--cwd PATH]
|
|
33
52
|
supercollab active [--cwd PATH]
|
|
34
53
|
supercollab session list
|
|
35
54
|
supercollab session revoke --session ID
|
|
55
|
+
supercollab embeddings status
|
|
56
|
+
supercollab embeddings warmup
|
|
36
57
|
supercollab mcp stdio
|
|
37
58
|
supercollab mcp print-config --client codex
|
|
38
59
|
supercollab config path
|
|
@@ -190,6 +211,82 @@ function signRequest(privateKeyPem, method, endpoint, bodyString, timestamp, non
|
|
|
190
211
|
return sig + '='.repeat((4 - (sig.length % 4)) % 4);
|
|
191
212
|
}
|
|
192
213
|
|
|
214
|
+
function b64url(buffer) {
|
|
215
|
+
return Buffer.from(buffer).toString('base64url');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function fromB64url(value) {
|
|
219
|
+
return Buffer.from(String(value), 'base64url');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sha256Tag(data) {
|
|
223
|
+
return `sha256:${crypto.createHash('sha256').update(data).digest('hex')}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function newRoomKey() {
|
|
227
|
+
return `sck_${crypto.randomBytes(32).toString('base64url')}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function roomKeyBytes(roomKey) {
|
|
231
|
+
const raw = String(roomKey || '').startsWith('sck_') ? String(roomKey).slice(4) : String(roomKey || '');
|
|
232
|
+
const key = fromB64url(raw);
|
|
233
|
+
if (key.length !== 32) throw new Error('invalid room key');
|
|
234
|
+
return key;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function ensureRoomKey(config, roomId) {
|
|
238
|
+
const key = config.roomKeys?.[roomId];
|
|
239
|
+
if (!key) throw new Error(`missing local room key for ${roomId}; join with a private invite or run supercollab room key on a device that already has it`);
|
|
240
|
+
return key;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function storeRoomKey(config, roomId, key) {
|
|
244
|
+
roomKeyBytes(key);
|
|
245
|
+
config.roomKeys = config.roomKeys || {};
|
|
246
|
+
config.roomKeys[roomId] = key.startsWith('sck_') ? key : `sck_${key}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function encryptForRoom(config, roomId, plaintext) {
|
|
250
|
+
const key = roomKeyBytes(ensureRoomKey(config, roomId));
|
|
251
|
+
const iv = crypto.randomBytes(12);
|
|
252
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
253
|
+
const raw = Buffer.from(JSON.stringify(plaintext), 'utf8');
|
|
254
|
+
const ciphertext = Buffer.concat([cipher.update(raw), cipher.final()]);
|
|
255
|
+
const tag = cipher.getAuthTag();
|
|
256
|
+
const envelope = {
|
|
257
|
+
v: 1,
|
|
258
|
+
alg: 'A256GCM',
|
|
259
|
+
iv: b64url(iv),
|
|
260
|
+
tag: b64url(tag),
|
|
261
|
+
ciphertext: b64url(ciphertext),
|
|
262
|
+
};
|
|
263
|
+
const encoded = JSON.stringify(envelope);
|
|
264
|
+
return { encoded, hash: sha256Tag(encoded) };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function decryptForRoom(config, roomId, encoded) {
|
|
268
|
+
const key = roomKeyBytes(ensureRoomKey(config, roomId));
|
|
269
|
+
const envelope = JSON.parse(encoded);
|
|
270
|
+
if (envelope?.alg !== 'A256GCM' || envelope?.v !== 1) throw new Error('unsupported encrypted message envelope');
|
|
271
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, fromB64url(envelope.iv));
|
|
272
|
+
decipher.setAuthTag(fromB64url(envelope.tag));
|
|
273
|
+
const raw = Buffer.concat([decipher.update(fromB64url(envelope.ciphertext)), decipher.final()]);
|
|
274
|
+
return JSON.parse(raw.toString('utf8'));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parsePrivateInvite(value) {
|
|
278
|
+
const raw = String(value || '').trim();
|
|
279
|
+
const [token, key] = raw.split('.sck_', 2);
|
|
280
|
+
if (key) return { token, roomKey: `sck_${key}` };
|
|
281
|
+
const hashIdx = raw.indexOf('#key=');
|
|
282
|
+
if (hashIdx >= 0) return { token: raw.slice(0, hashIdx), roomKey: raw.slice(hashIdx + 5) };
|
|
283
|
+
return { token: raw, roomKey: null };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function makePrivateInvite(inviteToken, roomKey) {
|
|
287
|
+
return `${inviteToken}.${roomKey}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
193
290
|
async function ensureAgentSession(config) {
|
|
194
291
|
const now = Math.floor(Date.now() / 1000);
|
|
195
292
|
if (config.agentSessionToken && config.agentSessionExpiresAt && config.agentSessionExpiresAt - SESSION_TTL_SKEW > now) {
|
|
@@ -325,6 +422,14 @@ function getMeta(db, key, fallback = '') {
|
|
|
325
422
|
return row ? String(row.value) : fallback;
|
|
326
423
|
}
|
|
327
424
|
|
|
425
|
+
function tableColumns(db, table) {
|
|
426
|
+
try {
|
|
427
|
+
return dbAll(db, `PRAGMA table_info(${table})`).map((row) => String(row.name));
|
|
428
|
+
} catch {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
328
433
|
function initChatSchema(db) {
|
|
329
434
|
db.exec(`
|
|
330
435
|
CREATE TABLE IF NOT EXISTS meta (
|
|
@@ -350,12 +455,183 @@ function initChatSchema(db) {
|
|
|
350
455
|
CREATE INDEX IF NOT EXISTS idx_messages_room_id ON messages(room_id, id);
|
|
351
456
|
CREATE INDEX IF NOT EXISTS idx_messages_channel_created ON messages(channel, created_at);
|
|
352
457
|
`);
|
|
458
|
+
const embeddingColumns = tableColumns(db, 'message_embeddings');
|
|
459
|
+
if (embeddingColumns.length > 0 && (!embeddingColumns.includes('seq') || !embeddingColumns.includes('profile'))) {
|
|
460
|
+
db.exec('DROP TABLE IF EXISTS message_embeddings');
|
|
461
|
+
}
|
|
462
|
+
db.exec(`
|
|
463
|
+
CREATE TABLE IF NOT EXISTS message_embeddings (
|
|
464
|
+
message_id TEXT NOT NULL,
|
|
465
|
+
seq INTEGER NOT NULL DEFAULT 0,
|
|
466
|
+
pos INTEGER NOT NULL DEFAULT 0,
|
|
467
|
+
dims INTEGER NOT NULL,
|
|
468
|
+
model TEXT NOT NULL,
|
|
469
|
+
profile TEXT NOT NULL,
|
|
470
|
+
vector TEXT NOT NULL,
|
|
471
|
+
updated_at TEXT NOT NULL,
|
|
472
|
+
PRIMARY KEY(message_id, seq)
|
|
473
|
+
);
|
|
474
|
+
CREATE INDEX IF NOT EXISTS idx_message_embeddings_profile ON message_embeddings(profile);
|
|
475
|
+
`);
|
|
353
476
|
try {
|
|
354
477
|
db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, channel UNINDEXED, sender_label, body, metadata, tokenize='porter')");
|
|
355
478
|
setMeta(db, 'fts5', '1');
|
|
356
479
|
} catch {
|
|
357
480
|
setMeta(db, 'fts5', '0');
|
|
358
481
|
}
|
|
482
|
+
setMeta(db, 'embedding_profile', EMBEDDING_PROFILE.id);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let embeddingPipelinePromise = null;
|
|
486
|
+
|
|
487
|
+
async function getEmbeddingPipeline() {
|
|
488
|
+
if (!embeddingPipelinePromise) {
|
|
489
|
+
embeddingPipelinePromise = (async () => {
|
|
490
|
+
const mod = await import('@huggingface/transformers');
|
|
491
|
+
const { pipeline, env } = mod;
|
|
492
|
+
if (process.env.SUPERCOLLAB_MODEL_CACHE && env) {
|
|
493
|
+
fs.mkdirSync(process.env.SUPERCOLLAB_MODEL_CACHE, { recursive: true });
|
|
494
|
+
env.cacheDir = process.env.SUPERCOLLAB_MODEL_CACHE;
|
|
495
|
+
}
|
|
496
|
+
return pipeline('feature-extraction', EMBEDDING_MODEL, { dtype: EMBEDDING_DTYPE });
|
|
497
|
+
})();
|
|
498
|
+
}
|
|
499
|
+
return embeddingPipelinePromise;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function formatQueryForEmbedding(query) {
|
|
503
|
+
return `${EMBEDDING_PROFILE.query_prefix}${query}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function formatDocForEmbedding(text, title = '') {
|
|
507
|
+
return title ? `${title}\n${text}` : text;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function chunkText(content, maxChars = EMBEDDING_CHUNK_CHARS, overlapChars = EMBEDDING_CHUNK_OVERLAP) {
|
|
511
|
+
const text = String(content || '');
|
|
512
|
+
if (text.length <= maxChars) return [{ text, pos: 0 }];
|
|
513
|
+
const chunks = [];
|
|
514
|
+
let charPos = 0;
|
|
515
|
+
while (charPos < text.length) {
|
|
516
|
+
let endPos = Math.min(charPos + maxChars, text.length);
|
|
517
|
+
if (endPos < text.length) {
|
|
518
|
+
const slice = text.slice(charPos, endPos);
|
|
519
|
+
const searchStart = Math.floor(slice.length * 0.7);
|
|
520
|
+
const searchSlice = slice.slice(searchStart);
|
|
521
|
+
let breakOffset = -1;
|
|
522
|
+
const paragraphBreak = searchSlice.lastIndexOf('\n\n');
|
|
523
|
+
if (paragraphBreak >= 0) {
|
|
524
|
+
breakOffset = searchStart + paragraphBreak + 2;
|
|
525
|
+
} else {
|
|
526
|
+
const sentenceEnd = Math.max(
|
|
527
|
+
searchSlice.lastIndexOf('. '),
|
|
528
|
+
searchSlice.lastIndexOf('.\n'),
|
|
529
|
+
searchSlice.lastIndexOf('? '),
|
|
530
|
+
searchSlice.lastIndexOf('?\n'),
|
|
531
|
+
searchSlice.lastIndexOf('! '),
|
|
532
|
+
searchSlice.lastIndexOf('!\n'),
|
|
533
|
+
);
|
|
534
|
+
if (sentenceEnd >= 0) {
|
|
535
|
+
breakOffset = searchStart + sentenceEnd + 2;
|
|
536
|
+
} else {
|
|
537
|
+
const lineBreak = searchSlice.lastIndexOf('\n');
|
|
538
|
+
if (lineBreak >= 0) {
|
|
539
|
+
breakOffset = searchStart + lineBreak + 1;
|
|
540
|
+
} else {
|
|
541
|
+
const spaceBreak = searchSlice.lastIndexOf(' ');
|
|
542
|
+
if (spaceBreak >= 0) breakOffset = searchStart + spaceBreak + 1;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (breakOffset > 0) endPos = charPos + breakOffset;
|
|
547
|
+
}
|
|
548
|
+
if (endPos <= charPos) endPos = Math.min(charPos + maxChars, text.length);
|
|
549
|
+
chunks.push({ text: text.slice(charPos, endPos), pos: charPos });
|
|
550
|
+
if (endPos >= text.length) break;
|
|
551
|
+
charPos = endPos - overlapChars;
|
|
552
|
+
const lastChunkPos = chunks.at(-1).pos;
|
|
553
|
+
if (charPos <= lastChunkPos) charPos = endPos;
|
|
554
|
+
}
|
|
555
|
+
return chunks;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function embedText(text, { isQuery = false, title = '' } = {}) {
|
|
559
|
+
const extractor = await getEmbeddingPipeline();
|
|
560
|
+
const formatted = isQuery ? formatQueryForEmbedding(text) : formatDocForEmbedding(text, title);
|
|
561
|
+
const output = await extractor(formatted.slice(0, 4000), {
|
|
562
|
+
pooling: EMBEDDING_PROFILE.pooling,
|
|
563
|
+
normalize: EMBEDDING_PROFILE.normalize,
|
|
564
|
+
});
|
|
565
|
+
const vector = Array.from(output.data).map(Number);
|
|
566
|
+
if (vector.length !== EMBEDDING_DIMS) throw new Error(`unexpected embedding dims ${vector.length}`);
|
|
567
|
+
return vector;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function cosine(a, b) {
|
|
571
|
+
let score = 0;
|
|
572
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) score += a[i] * b[i];
|
|
573
|
+
return score;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function storeEmbeddings(db, local, metadata) {
|
|
577
|
+
const messageId = local.message_id;
|
|
578
|
+
if (!messageId) return { embedded: false, chunks: 0 };
|
|
579
|
+
dbRun(db, 'DELETE FROM message_embeddings WHERE message_id=? AND profile<>?', [messageId, EMBEDDING_PROFILE.id]);
|
|
580
|
+
const existing = dbGet(db, 'SELECT COUNT(*) AS count FROM message_embeddings WHERE message_id=? AND profile=?', [messageId, EMBEDDING_PROFILE.id]);
|
|
581
|
+
if (Number(existing?.count || 0) > 0) return { embedded: false, chunks: Number(existing.count) };
|
|
582
|
+
|
|
583
|
+
const body = `${local.sender_label || ''}\n${local.body || ''}\n${metadata || ''}`;
|
|
584
|
+
const title = local.sender_label || local.channel || 'SuperCollab message';
|
|
585
|
+
const chunks = chunkText(body);
|
|
586
|
+
const updatedAt = nowIso();
|
|
587
|
+
for (let seq = 0; seq < chunks.length; seq++) {
|
|
588
|
+
const chunk = chunks[seq];
|
|
589
|
+
const vector = await embedText(chunk.text, { title });
|
|
590
|
+
dbRun(
|
|
591
|
+
db,
|
|
592
|
+
`INSERT INTO message_embeddings(message_id,seq,pos,dims,model,profile,vector,updated_at)
|
|
593
|
+
VALUES(?,?,?,?,?,?,?,?)
|
|
594
|
+
ON CONFLICT(message_id, seq) DO UPDATE SET
|
|
595
|
+
pos=excluded.pos,
|
|
596
|
+
dims=excluded.dims,
|
|
597
|
+
model=excluded.model,
|
|
598
|
+
profile=excluded.profile,
|
|
599
|
+
vector=excluded.vector,
|
|
600
|
+
updated_at=excluded.updated_at`,
|
|
601
|
+
[messageId, seq, chunk.pos, EMBEDDING_DIMS, EMBEDDING_MODEL, EMBEDDING_PROFILE.id, JSON.stringify(vector), updatedAt],
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
setMeta(db, 'embedding_last_ok_at', updatedAt);
|
|
605
|
+
return { embedded: true, chunks: chunks.length };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function tryStoreEmbeddings(db, local, metadata) {
|
|
609
|
+
try {
|
|
610
|
+
return await storeEmbeddings(db, local, metadata);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
setMeta(db, 'embedding_last_error', err.message || String(err));
|
|
613
|
+
return { embedded: false, chunks: 0, error: err.message || String(err) };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function embedMissingMessages(db, limit = 500) {
|
|
618
|
+
const rows = dbAll(
|
|
619
|
+
db,
|
|
620
|
+
`SELECT m.*
|
|
621
|
+
FROM messages m
|
|
622
|
+
LEFT JOIN message_embeddings e
|
|
623
|
+
ON e.message_id=m.message_id AND e.profile=?
|
|
624
|
+
WHERE e.message_id IS NULL
|
|
625
|
+
ORDER BY m.id ASC
|
|
626
|
+
LIMIT ?`,
|
|
627
|
+
[EMBEDDING_PROFILE.id, Math.max(1, Math.min(Number(limit || 500), 2000))],
|
|
628
|
+
);
|
|
629
|
+
let embedded = 0;
|
|
630
|
+
for (const row of rows) {
|
|
631
|
+
const result = await tryStoreEmbeddings(db, row, row.metadata || '');
|
|
632
|
+
if (result.embedded) embedded += result.chunks;
|
|
633
|
+
}
|
|
634
|
+
return { messages_checked: rows.length, chunks_embedded: embedded };
|
|
359
635
|
}
|
|
360
636
|
|
|
361
637
|
async function openChatDb(config, file, roomId) {
|
|
@@ -381,25 +657,42 @@ function saveChatDb(cap) {
|
|
|
381
657
|
try { fs.chmodSync(cap.dbPath, 0o600); } catch {}
|
|
382
658
|
}
|
|
383
659
|
|
|
384
|
-
function
|
|
385
|
-
const metadata = typeof msg.metadata === 'string' ? msg.metadata :
|
|
660
|
+
function localPlainMessage(config, roomId, msg) {
|
|
661
|
+
const metadata = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata || '{}') : (msg.metadata || {});
|
|
662
|
+
if (metadata.encrypted === true || metadata.private === true) {
|
|
663
|
+
const plain = decryptForRoom(config, roomId, msg.body);
|
|
664
|
+
return {
|
|
665
|
+
...msg,
|
|
666
|
+
body: String(plain.text || ''),
|
|
667
|
+
metadata: JSON.stringify(plain.metadata || {}),
|
|
668
|
+
channel: plain.channel || msg.channel || 'agents',
|
|
669
|
+
kind: plain.kind || msg.kind || 'chat.message',
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
return { ...msg, metadata: JSON.stringify(metadata) };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function insertLocalMessage(db, msg, config = null, roomId = msg.room_id || '') {
|
|
676
|
+
const local = config ? localPlainMessage(config, roomId, msg) : msg;
|
|
677
|
+
const metadata = typeof local.metadata === 'string' ? local.metadata : JSON.stringify(local.metadata || {});
|
|
386
678
|
dbRun(
|
|
387
679
|
db,
|
|
388
680
|
`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
681
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
390
682
|
[
|
|
391
|
-
Number(
|
|
392
|
-
|
|
393
|
-
|
|
683
|
+
Number(local.id), local.message_id, local.room_id || roomId || '', local.channel || 'agents', local.kind || 'chat.message',
|
|
684
|
+
local.actor_type || '', local.actor_id || '', local.user_id || '', local.agent_id || '',
|
|
685
|
+
local.sender_label || '', local.body || '', metadata, local.content_hash || '', local.created_at || nowIso(),
|
|
394
686
|
],
|
|
395
687
|
);
|
|
396
688
|
if (getMeta(db, 'fts5', '0') === '1') {
|
|
397
689
|
try {
|
|
398
690
|
dbRun(db, 'INSERT OR IGNORE INTO messages_fts(rowid,message_id,channel,sender_label,body,metadata) VALUES(?,?,?,?,?,?)', [
|
|
399
|
-
Number(
|
|
691
|
+
Number(local.id), local.message_id, local.channel || 'agents', local.sender_label || '', local.body || '', metadata,
|
|
400
692
|
]);
|
|
401
693
|
} catch {}
|
|
402
694
|
}
|
|
695
|
+
await tryStoreEmbeddings(db, local, metadata);
|
|
403
696
|
}
|
|
404
697
|
|
|
405
698
|
async function syncRoom(config, file, roomId, limit = 500) {
|
|
@@ -407,29 +700,73 @@ async function syncRoom(config, file, roomId, limit = 500) {
|
|
|
407
700
|
try {
|
|
408
701
|
const after = Number(getMeta(cap.db, 'last_message_id', '0')) || 0;
|
|
409
702
|
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 });
|
|
703
|
+
for (const msg of data.messages || []) await insertLocalMessage(cap.db, { ...msg, room_id: roomId }, config, roomId);
|
|
704
|
+
const embedding = await embedMissingMessages(cap.db, 500);
|
|
411
705
|
setMeta(cap.db, 'last_message_id', String(data.next_after || after));
|
|
412
706
|
setMeta(cap.db, 'last_sync_at', nowIso());
|
|
413
707
|
saveChatDb(cap);
|
|
414
|
-
return { room_id: roomId, pulled: (data.messages || []).length, last_message_id: Number(data.next_after || after), db: cap.dbPath };
|
|
708
|
+
return { room_id: roomId, pulled: (data.messages || []).length, last_message_id: Number(data.next_after || after), db: cap.dbPath, embedding };
|
|
415
709
|
} finally {
|
|
416
710
|
cap.db.close();
|
|
417
711
|
}
|
|
418
712
|
}
|
|
419
713
|
|
|
714
|
+
async function doRoomCreate(config, file, opts) {
|
|
715
|
+
const data = await apiAsAgent(config, 'POST', '/v1/rooms', {
|
|
716
|
+
title: requireValue(opts, 'title'),
|
|
717
|
+
goal: requireValue(opts, 'goal'),
|
|
718
|
+
slug: opts.slug,
|
|
719
|
+
});
|
|
720
|
+
const roomId = data.room_id || data.id;
|
|
721
|
+
storeRoomKey(config, roomId, newRoomKey());
|
|
722
|
+
saveConfig(config, file);
|
|
723
|
+
return { ...data, encrypted: true, room_key_saved: true };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function doRoomInvite(config, opts) {
|
|
727
|
+
const roomId = requireValue(opts, 'room');
|
|
728
|
+
const roomKey = ensureRoomKey(config, roomId);
|
|
729
|
+
const data = await apiAsAgent(config, 'POST', `/v1/rooms/${roomId}/invites`, {
|
|
730
|
+
role: opts.role || 'member',
|
|
731
|
+
ttl_seconds: opts.ttl || opts.ttl_seconds || 86400,
|
|
732
|
+
});
|
|
733
|
+
return { ...data, private_invite: makePrivateInvite(data.invite_token, roomKey) };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function doRoomJoin(config, file, opts) {
|
|
737
|
+
const parsed = parsePrivateInvite(requireValue(opts, 'invite'));
|
|
738
|
+
const data = await apiAsAgent(config, 'POST', '/v1/invites/accept', {
|
|
739
|
+
token: parsed.token,
|
|
740
|
+
fingerprint: config.agentFingerprint,
|
|
741
|
+
});
|
|
742
|
+
if (parsed.roomKey) {
|
|
743
|
+
storeRoomKey(config, data.room_id || data.workspace_id, parsed.roomKey);
|
|
744
|
+
saveConfig(config, file);
|
|
745
|
+
}
|
|
746
|
+
return { ...data, room_key_saved: Boolean(parsed.roomKey), encrypted: Boolean(parsed.roomKey) };
|
|
747
|
+
}
|
|
748
|
+
|
|
420
749
|
async function doChatSend(config, file, opts) {
|
|
421
750
|
const roomId = requireValue(opts, 'room');
|
|
422
|
-
const
|
|
751
|
+
const text = requireValue(opts, 'text');
|
|
423
752
|
const channel = String(opts.channel || 'agents');
|
|
753
|
+
const kind = String(opts.kind || 'chat.message');
|
|
754
|
+
const encrypted = encryptForRoom(config, roomId, {
|
|
755
|
+
text,
|
|
756
|
+
channel,
|
|
757
|
+
kind,
|
|
758
|
+
metadata: { client: 'supercollab-cli', private: true },
|
|
759
|
+
sent_at: nowIso(),
|
|
760
|
+
});
|
|
424
761
|
const data = await apiAsAgent(config, 'POST', `/v1/rooms/${roomId}/messages`, {
|
|
425
|
-
body,
|
|
762
|
+
body: encrypted.encoded,
|
|
426
763
|
channel,
|
|
427
|
-
kind
|
|
428
|
-
metadata: {
|
|
764
|
+
kind,
|
|
765
|
+
metadata: { encrypted: true, private: true, alg: 'A256GCM', local_search: true },
|
|
429
766
|
});
|
|
430
767
|
const cap = await openChatDb(config, file, roomId);
|
|
431
768
|
try {
|
|
432
|
-
insertLocalMessage(cap.db, { ...data.message, room_id: roomId });
|
|
769
|
+
await insertLocalMessage(cap.db, { ...data.message, room_id: roomId }, config, roomId);
|
|
433
770
|
setMeta(cap.db, 'last_message_id', String(Math.max(Number(getMeta(cap.db, 'last_message_id', '0')) || 0, Number(data.message.id))));
|
|
434
771
|
saveChatDb(cap);
|
|
435
772
|
} finally {
|
|
@@ -458,33 +795,106 @@ function ftsQuery(value) {
|
|
|
458
795
|
async function doChatSearch(config, file, opts) {
|
|
459
796
|
const roomId = requireValue(opts, 'room');
|
|
460
797
|
const query = requireValue(opts, 'query');
|
|
798
|
+
const mode = String(opts.mode || 'hybrid').toLowerCase();
|
|
799
|
+
if (!['hybrid', 'keyword', 'vector'].includes(mode)) throw new Error('search --mode must be hybrid, keyword, or vector');
|
|
461
800
|
await syncRoom(config, file, roomId, 500);
|
|
462
801
|
const cap = await openChatDb(config, file, roomId);
|
|
463
802
|
try {
|
|
464
|
-
|
|
465
|
-
|
|
803
|
+
const embedding = await embedMissingMessages(cap.db, 500);
|
|
804
|
+
const maxResults = Math.max(1, Math.min(Number(opts.limit || 20), 100));
|
|
805
|
+
let keywordRows = [];
|
|
806
|
+
if (mode !== 'vector' && getMeta(cap.db, 'fts5', '0') === '1') {
|
|
466
807
|
const q = ftsQuery(query);
|
|
467
808
|
if (q) {
|
|
468
809
|
try {
|
|
469
|
-
|
|
810
|
+
keywordRows = dbAll(
|
|
470
811
|
cap.db,
|
|
471
812
|
`SELECT m.*, bm25(messages_fts) AS score
|
|
472
813
|
FROM messages_fts JOIN messages m ON m.id=messages_fts.rowid
|
|
473
814
|
WHERE messages_fts MATCH ?
|
|
474
815
|
ORDER BY score LIMIT ?`,
|
|
475
|
-
[q,
|
|
816
|
+
[q, maxResults],
|
|
476
817
|
);
|
|
477
818
|
} catch {
|
|
478
|
-
|
|
819
|
+
keywordRows = [];
|
|
479
820
|
}
|
|
480
821
|
}
|
|
481
822
|
}
|
|
482
|
-
if (!
|
|
483
|
-
|
|
484
|
-
`%${query}%`, `%${query}%`,
|
|
485
|
-
]);
|
|
823
|
+
if (mode !== 'vector' && !keywordRows.length) {
|
|
824
|
+
keywordRows = dbAll(cap.db, 'SELECT *, 0 AS score FROM messages WHERE body LIKE ? OR metadata LIKE ? ORDER BY id DESC LIMIT ?', [
|
|
825
|
+
`%${query}%`, `%${query}%`, maxResults,
|
|
826
|
+
]).map((row) => ({ ...row, keyword_fallback: true }));
|
|
827
|
+
}
|
|
828
|
+
let vectorRows = [];
|
|
829
|
+
let vectorError = null;
|
|
830
|
+
if (mode !== 'keyword') try {
|
|
831
|
+
const qvec = await embedText(query, { isQuery: true });
|
|
832
|
+
const bestByMessage = new Map();
|
|
833
|
+
for (const row of dbAll(
|
|
834
|
+
cap.db,
|
|
835
|
+
`SELECT m.*, e.vector, e.seq, e.pos
|
|
836
|
+
FROM message_embeddings e JOIN messages m ON m.message_id=e.message_id
|
|
837
|
+
WHERE e.profile=?
|
|
838
|
+
ORDER BY m.id DESC LIMIT 3000`,
|
|
839
|
+
[EMBEDDING_PROFILE.id],
|
|
840
|
+
)) {
|
|
841
|
+
let score = 0;
|
|
842
|
+
try { score = cosine(qvec, JSON.parse(row.vector)); } catch {}
|
|
843
|
+
if (score <= 0) continue;
|
|
844
|
+
const { vector, ...clean } = row;
|
|
845
|
+
const prior = bestByMessage.get(row.message_id);
|
|
846
|
+
if (!prior || score > prior.vector_score) {
|
|
847
|
+
bestByMessage.set(row.message_id, { ...clean, vector_score: score, chunk_seq: Number(row.seq || 0), chunk_pos: Number(row.pos || 0) });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
vectorRows = Array.from(bestByMessage.values())
|
|
851
|
+
.sort((a, b) => b.vector_score - a.vector_score)
|
|
852
|
+
.slice(0, mode === 'vector' ? maxResults : Math.max(maxResults, 50));
|
|
853
|
+
} catch (err) {
|
|
854
|
+
vectorError = err.message || String(err);
|
|
855
|
+
setMeta(cap.db, 'embedding_last_error', vectorError);
|
|
486
856
|
}
|
|
487
|
-
|
|
857
|
+
const keywordRank = new Map(keywordRows.map((row, idx) => [row.message_id, idx + 1]));
|
|
858
|
+
const vectorRank = new Map(vectorRows.map((row, idx) => [row.message_id, idx + 1]));
|
|
859
|
+
const byMessage = new Map();
|
|
860
|
+
for (const row of [...keywordRows, ...vectorRows]) {
|
|
861
|
+
const existing = byMessage.get(row.message_id) || {};
|
|
862
|
+
byMessage.set(row.message_id, { ...existing, ...row });
|
|
863
|
+
}
|
|
864
|
+
const rrfK = 60;
|
|
865
|
+
const hybridRows = Array.from(byMessage.values()).map((row) => {
|
|
866
|
+
const kr = keywordRank.get(row.message_id);
|
|
867
|
+
const vr = vectorRank.get(row.message_id);
|
|
868
|
+
const keywordScore = kr ? 1 / (rrfK + kr) : 0;
|
|
869
|
+
const vectorScore = vr ? 1 / (rrfK + vr) : 0;
|
|
870
|
+
return {
|
|
871
|
+
...row,
|
|
872
|
+
search_sources: [kr ? (row.keyword_fallback ? 'like' : 'fts5_bm25') : null, vr ? 'bge_vector_cosine' : null].filter(Boolean),
|
|
873
|
+
keyword_rank: kr || null,
|
|
874
|
+
vector_rank: vr || null,
|
|
875
|
+
hybrid_score: keywordScore + vectorScore,
|
|
876
|
+
};
|
|
877
|
+
}).sort((a, b) => b.hybrid_score - a.hybrid_score).slice(0, maxResults);
|
|
878
|
+
const results = mode === 'keyword'
|
|
879
|
+
? keywordRows.slice(0, maxResults).map((row, idx) => ({ ...row, search_sources: [row.keyword_fallback ? 'like' : 'fts5_bm25'], keyword_rank: idx + 1 }))
|
|
880
|
+
: mode === 'vector'
|
|
881
|
+
? vectorRows.slice(0, maxResults).map((row, idx) => ({ ...row, search_sources: ['bge_vector_cosine'], vector_rank: idx + 1 }))
|
|
882
|
+
: hybridRows;
|
|
883
|
+
return {
|
|
884
|
+
room_id: roomId,
|
|
885
|
+
query,
|
|
886
|
+
search: {
|
|
887
|
+
local_only: true,
|
|
888
|
+
mode,
|
|
889
|
+
methods: ['fts5_bm25', 'bge_vector_cosine', 'rrf_hybrid'],
|
|
890
|
+
fts: getMeta(cap.db, 'fts5', '0') === '1',
|
|
891
|
+
vector: EMBEDDING_PROFILE.id,
|
|
892
|
+
embedding_profile: EMBEDDING_PROFILE,
|
|
893
|
+
embedding,
|
|
894
|
+
vector_error: vectorError,
|
|
895
|
+
},
|
|
896
|
+
results,
|
|
897
|
+
};
|
|
488
898
|
} finally {
|
|
489
899
|
cap.db.close();
|
|
490
900
|
}
|
|
@@ -516,6 +926,7 @@ function agentInstructions(active) {
|
|
|
516
926
|
|
|
517
927
|
function activate(config, file, opts) {
|
|
518
928
|
const roomId = requireValue(opts, 'room');
|
|
929
|
+
ensureRoomKey(config, roomId);
|
|
519
930
|
const cwd = normalizeCwd(opts.cwd);
|
|
520
931
|
config.activations = config.activations || {};
|
|
521
932
|
config.activations[cwd] = { roomId, enabled: true, activatedAt: nowIso() };
|
|
@@ -542,6 +953,7 @@ async function activeStatus(config, file, opts = {}) {
|
|
|
542
953
|
room_id: active?.roomId || null,
|
|
543
954
|
activation_root: active?.cwd || null,
|
|
544
955
|
config: file,
|
|
956
|
+
embedding_profile: EMBEDDING_PROFILE,
|
|
545
957
|
instructions: agentInstructions(active),
|
|
546
958
|
};
|
|
547
959
|
}
|
|
@@ -569,7 +981,7 @@ function mcpTools() {
|
|
|
569
981
|
toolSchema('room_join', 'Accept a room invite token.', { invite_token: s, fingerprint: s }, ['invite_token']),
|
|
570
982
|
toolSchema('chat_send', 'Send a message to the active agent chat room.', { text: s, channel: s, kind: s }, ['text']),
|
|
571
983
|
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']),
|
|
984
|
+
toolSchema('chat_search', 'Sync and search the active room transcript with local keyword, BGE vector, or hybrid retrieval.', { query: s, mode: s, limit: { type: 'integer' } }, ['query']),
|
|
573
985
|
toolSchema('chat_sync', 'Sync the active room transcript into local SQLite.'),
|
|
574
986
|
];
|
|
575
987
|
}
|
|
@@ -578,12 +990,12 @@ async function callTool(config, name, args) {
|
|
|
578
990
|
const file = config.__configFile || DEFAULT_CONFIG;
|
|
579
991
|
if (name === 'supercollab_status') return activeStatus(config, file, {});
|
|
580
992
|
if (name === 'room_list') return apiAsAgent(config, 'GET', '/v1/rooms');
|
|
581
|
-
if (name === 'room_create') return
|
|
582
|
-
if (name === 'room_invite') return
|
|
583
|
-
if (name === 'room_join') return
|
|
993
|
+
if (name === 'room_create') return doRoomCreate(config, file, { title: args.title, goal: args.goal, slug: args.slug });
|
|
994
|
+
if (name === 'room_invite') return doRoomInvite(config, { room: args.room_id, role: args.role || 'member', ttl_seconds: args.ttl_seconds || 86400 });
|
|
995
|
+
if (name === 'room_join') return doRoomJoin(config, file, { invite: args.invite_token });
|
|
584
996
|
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
997
|
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 });
|
|
998
|
+
if (name === 'chat_search') return doChatSearch(config, file, { room: requireActiveRoom(config, args), query: args.query, mode: args.mode || 'hybrid', limit: args.limit || 20 });
|
|
587
999
|
if (name === 'chat_sync') return syncRoom(config, file, requireActiveRoom(config, args));
|
|
588
1000
|
throw new Error(`unknown tool: ${name}`);
|
|
589
1001
|
}
|
|
@@ -663,6 +1075,24 @@ function printCodexConfig(opts) {
|
|
|
663
1075
|
console.log(`[mcp_servers.supercollab]\ncommand = "supercollab"\nargs = ["mcp", "stdio", "--config", "${file.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"]`);
|
|
664
1076
|
}
|
|
665
1077
|
|
|
1078
|
+
async function embeddingStatus() {
|
|
1079
|
+
return {
|
|
1080
|
+
ok: true,
|
|
1081
|
+
profile: EMBEDDING_PROFILE,
|
|
1082
|
+
model_download: 'lazy on first embedding, or now via `supercollab embeddings warmup`',
|
|
1083
|
+
cache_dir: process.env.SUPERCOLLAB_MODEL_CACHE || 'default @huggingface/transformers cache',
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function embeddingWarmup() {
|
|
1088
|
+
const vector = await embedText('supercollab embedding warmup', { isQuery: true });
|
|
1089
|
+
return {
|
|
1090
|
+
ok: true,
|
|
1091
|
+
dims: vector.length,
|
|
1092
|
+
profile: EMBEDDING_PROFILE,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
666
1096
|
async function main() {
|
|
667
1097
|
const { positionals, opts } = parse(process.argv.slice(2));
|
|
668
1098
|
if (opts.help || positionals.length === 0) { printHelp(); return; }
|
|
@@ -682,10 +1112,11 @@ async function main() {
|
|
|
682
1112
|
}
|
|
683
1113
|
if (cmd === 'room') {
|
|
684
1114
|
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
|
|
686
|
-
if (sub === 'invite') return console.log(JSON.stringify(await
|
|
1115
|
+
if (sub === 'create') return console.log(JSON.stringify(await doRoomCreate(config, file, opts), null, 2));
|
|
1116
|
+
if (sub === 'invite') return console.log(JSON.stringify(await doRoomInvite(config, opts), null, 2));
|
|
687
1117
|
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
|
|
1118
|
+
if (sub === 'join') return console.log(JSON.stringify(await doRoomJoin(config, file, opts), null, 2));
|
|
1119
|
+
if (sub === 'key') return console.log(ensureRoomKey(config, requireValue(opts, 'room')));
|
|
689
1120
|
}
|
|
690
1121
|
if (cmd === 'chat') {
|
|
691
1122
|
if (sub === 'send') return console.log(JSON.stringify(await doChatSend(config, file, opts), null, 2));
|
|
@@ -700,6 +1131,10 @@ async function main() {
|
|
|
700
1131
|
if (sub === 'list') return console.log(JSON.stringify(await api(config, 'GET', '/v1/agent-sessions', undefined, config.userToken), null, 2));
|
|
701
1132
|
if (sub === 'revoke') return console.log(JSON.stringify(await api(config, 'DELETE', `/v1/agent-sessions/${requireValue(opts, 'session')}`, undefined, config.userToken), null, 2));
|
|
702
1133
|
}
|
|
1134
|
+
if (cmd === 'embeddings') {
|
|
1135
|
+
if (sub === 'status') return console.log(JSON.stringify(await embeddingStatus(), null, 2));
|
|
1136
|
+
if (sub === 'warmup') return console.log(JSON.stringify(await embeddingWarmup(), null, 2));
|
|
1137
|
+
}
|
|
703
1138
|
if (cmd === 'mcp' && sub === 'stdio') return runMcp(opts);
|
|
704
1139
|
if (cmd === 'mcp' && sub === 'print-config') return printCodexConfig(opts);
|
|
705
1140
|
throw new Error(`unknown command: ${positionals.join(' ')}`);
|
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
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "SuperCollab CLI and MCP bridge for encrypted local-search agent group chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"supercollab": "./bin/supercollab.js"
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"node": ">=20"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"@huggingface/transformers": "3.8.1",
|
|
17
18
|
"sql.js": "^1.14.1"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|