@supercollab/cli 0.4.4 → 0.4.5
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 +16 -0
- package/bin/supercollab.js +118 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,6 +133,22 @@ Print MCP config:
|
|
|
133
133
|
supercollab mcp print-config --client codex
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
For Claude Desktop on macOS, generate the config from the project directory you
|
|
137
|
+
want the agent to use:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
cd /path/to/project
|
|
141
|
+
supercollab mcp print-config --client claude
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The Claude config uses absolute Node and CLI paths plus an explicit `PATH` so it
|
|
145
|
+
does not depend on Homebrew, nvm, zsh, or GUI app shell startup behavior. Before
|
|
146
|
+
opening Claude, verify the local MCP handshake:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
supercollab mcp smoke
|
|
150
|
+
```
|
|
151
|
+
|
|
136
152
|
Default server: `https://hyper.polynode.dev`.
|
|
137
153
|
|
|
138
154
|
Local config is stored at `~/.supercollab/config.json` with mode `0600`; the
|
package/bin/supercollab.js
CHANGED
|
@@ -3,11 +3,13 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import * as readlineCore from 'node:readline';
|
|
8
9
|
import { stdin as input, stdout as output } from 'node:process';
|
|
9
10
|
|
|
10
|
-
const VERSION = '0.4.
|
|
11
|
+
const VERSION = '0.4.5';
|
|
12
|
+
const CLI_ENTRY = fileURLToPath(import.meta.url);
|
|
11
13
|
const DEFAULT_SERVER = process.env.SUPERCOLLAB_URL || 'https://hyper.polynode.dev';
|
|
12
14
|
const DEFAULT_CONFIG = process.env.SUPERCOLLAB_CONFIG || path.join(os.homedir(), '.supercollab', 'config.json');
|
|
13
15
|
const SESSION_TTL_SKEW = 60;
|
|
@@ -60,6 +62,7 @@ Usage:
|
|
|
60
62
|
supercollab embeddings warmup
|
|
61
63
|
supercollab mcp stdio
|
|
62
64
|
supercollab mcp print-config --client codex
|
|
65
|
+
supercollab mcp smoke [--timeout 5000]
|
|
63
66
|
supercollab config path
|
|
64
67
|
|
|
65
68
|
Options:
|
|
@@ -1169,9 +1172,96 @@ async function runMcp(opts) {
|
|
|
1169
1172
|
}
|
|
1170
1173
|
}
|
|
1171
1174
|
|
|
1175
|
+
function encodeMcpMessage(msg) {
|
|
1176
|
+
const raw = Buffer.from(JSON.stringify(msg));
|
|
1177
|
+
return Buffer.concat([Buffer.from(`Content-Length: ${raw.length}\r\n\r\n`), raw]);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function parseMcpMessages(state) {
|
|
1181
|
+
const messages = [];
|
|
1182
|
+
while (true) {
|
|
1183
|
+
const headerEnd = state.buffer.indexOf('\r\n\r\n');
|
|
1184
|
+
if (headerEnd < 0) break;
|
|
1185
|
+
const header = state.buffer.subarray(0, headerEnd).toString();
|
|
1186
|
+
const match = header.match(/content-length:\s*(\d+)/i);
|
|
1187
|
+
if (!match) {
|
|
1188
|
+
state.parseError = `missing content-length in MCP output: ${header.slice(0, 200)}`;
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
const length = Number(match[1]);
|
|
1192
|
+
const total = headerEnd + 4 + length;
|
|
1193
|
+
if (state.buffer.length < total) break;
|
|
1194
|
+
const body = state.buffer.subarray(headerEnd + 4, total).toString();
|
|
1195
|
+
state.buffer = state.buffer.subarray(total);
|
|
1196
|
+
try {
|
|
1197
|
+
messages.push(JSON.parse(body));
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
state.parseError = err.message || String(err);
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return messages;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function runMcpSmoke(opts) {
|
|
1207
|
+
const file = configPath(opts);
|
|
1208
|
+
const timeoutMs = Math.max(1000, Math.min(Number(opts.timeout || 5000), 30000));
|
|
1209
|
+
const args = [CLI_ENTRY, 'mcp', 'stdio', '--config', file];
|
|
1210
|
+
return await new Promise((resolve, reject) => {
|
|
1211
|
+
const child = spawn(process.execPath, args, {
|
|
1212
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1213
|
+
env: {
|
|
1214
|
+
...process.env,
|
|
1215
|
+
PATH: defaultPathEnv(),
|
|
1216
|
+
SUPERCOLLAB_CONFIG: file,
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
1219
|
+
const state = { buffer: Buffer.alloc(0), parseError: null };
|
|
1220
|
+
const responses = [];
|
|
1221
|
+
let stderr = '';
|
|
1222
|
+
let settled = false;
|
|
1223
|
+
const finish = (err, result) => {
|
|
1224
|
+
if (settled) return;
|
|
1225
|
+
settled = true;
|
|
1226
|
+
clearTimeout(timer);
|
|
1227
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
1228
|
+
if (err) reject(err);
|
|
1229
|
+
else resolve(result);
|
|
1230
|
+
};
|
|
1231
|
+
const timer = setTimeout(() => {
|
|
1232
|
+
finish(new Error(`MCP smoke timed out after ${timeoutMs}ms. stderr: ${stderr.slice(0, 1000)}`));
|
|
1233
|
+
}, timeoutMs);
|
|
1234
|
+
child.on('error', (err) => finish(err));
|
|
1235
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
1236
|
+
child.stdout.on('data', (chunk) => {
|
|
1237
|
+
state.buffer = Buffer.concat([state.buffer, chunk]);
|
|
1238
|
+
for (const msg of parseMcpMessages(state)) responses.push(msg);
|
|
1239
|
+
if (state.parseError) {
|
|
1240
|
+
finish(new Error(state.parseError));
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (responses.some((msg) => msg.id === 1) && responses.some((msg) => msg.id === 2)) {
|
|
1244
|
+
const init = responses.find((msg) => msg.id === 1);
|
|
1245
|
+
const tools = responses.find((msg) => msg.id === 2);
|
|
1246
|
+
finish(null, {
|
|
1247
|
+
ok: Boolean(init?.result?.serverInfo && Array.isArray(tools?.result?.tools)),
|
|
1248
|
+
command: process.execPath,
|
|
1249
|
+
args,
|
|
1250
|
+
config: file,
|
|
1251
|
+
server_info: init?.result?.serverInfo || null,
|
|
1252
|
+
tools: (tools?.result?.tools || []).map((tool) => tool.name),
|
|
1253
|
+
stderr: stderr.trim() || null,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
child.stdin.write(encodeMcpMessage({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }));
|
|
1258
|
+
child.stdin.write(encodeMcpMessage({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }));
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1172
1262
|
function printCodexConfig(opts) {
|
|
1173
1263
|
const file = configPath(opts);
|
|
1174
|
-
console.log(mcpConfigText(String(opts.client || 'codex'), file));
|
|
1264
|
+
console.log(mcpConfigText(String(opts.client || 'codex'), file, opts));
|
|
1175
1265
|
}
|
|
1176
1266
|
|
|
1177
1267
|
async function embeddingStatus() {
|
|
@@ -1670,14 +1760,35 @@ async function runSetupSmoke(config, file, roomId, prompts) {
|
|
|
1670
1760
|
};
|
|
1671
1761
|
}
|
|
1672
1762
|
|
|
1673
|
-
function
|
|
1763
|
+
function defaultPathEnv() {
|
|
1764
|
+
const parts = [
|
|
1765
|
+
path.dirname(process.execPath),
|
|
1766
|
+
'/opt/homebrew/bin',
|
|
1767
|
+
'/usr/local/bin',
|
|
1768
|
+
'/usr/bin',
|
|
1769
|
+
'/bin',
|
|
1770
|
+
'/usr/sbin',
|
|
1771
|
+
'/sbin',
|
|
1772
|
+
process.env.PATH || '',
|
|
1773
|
+
].flatMap((part) => String(part || '').split(path.delimiter)).filter(Boolean);
|
|
1774
|
+
return Array.from(new Set(parts)).join(path.delimiter);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function mcpConfigText(client, file, opts = {}) {
|
|
1778
|
+
const absoluteFile = path.resolve(file);
|
|
1674
1779
|
const escaped = file.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
1675
1780
|
if (client === 'claude') {
|
|
1781
|
+
const cwd = path.resolve(String(opts.cwd || process.env.SUPERCOLLAB_WORKDIR || process.cwd()));
|
|
1676
1782
|
return JSON.stringify({
|
|
1677
1783
|
mcpServers: {
|
|
1678
1784
|
supercollab: {
|
|
1679
|
-
|
|
1680
|
-
|
|
1785
|
+
type: 'stdio',
|
|
1786
|
+
command: process.execPath,
|
|
1787
|
+
args: [CLI_ENTRY, 'mcp', 'stdio', '--config', absoluteFile],
|
|
1788
|
+
env: {
|
|
1789
|
+
PATH: defaultPathEnv(),
|
|
1790
|
+
SUPERCOLLAB_WORKDIR: cwd,
|
|
1791
|
+
},
|
|
1681
1792
|
},
|
|
1682
1793
|
},
|
|
1683
1794
|
}, null, 2);
|
|
@@ -2321,6 +2432,7 @@ async function main() {
|
|
|
2321
2432
|
if (sub === 'warmup') return console.log(JSON.stringify(await embeddingWarmup(), null, 2));
|
|
2322
2433
|
}
|
|
2323
2434
|
if (cmd === 'mcp' && sub === 'stdio') return runMcp(opts);
|
|
2435
|
+
if (cmd === 'mcp' && sub === 'smoke') return console.log(JSON.stringify(await runMcpSmoke(opts), null, 2));
|
|
2324
2436
|
if (cmd === 'mcp' && sub === 'print-config') return printCodexConfig(opts);
|
|
2325
2437
|
throw new Error(`unknown command: ${positionals.join(' ')}`);
|
|
2326
2438
|
}
|