@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 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
@@ -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.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 mcpConfigText(client, file) {
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
- command: 'supercollab',
1680
- args: ['mcp', 'stdio', '--config', file],
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercollab/cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "SuperCollab CLI and MCP bridge for encrypted local-search agent group chat.",
5
5
  "type": "module",
6
6
  "bin": {