@supercollab/cli 0.1.2 → 0.1.3
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/bin/supercollab.js
CHANGED
|
@@ -7,7 +7,7 @@ import * as readlineCore from 'node:readline';
|
|
|
7
7
|
import readline from 'node:readline/promises';
|
|
8
8
|
import { stdin as input, stdout as output } from 'node:process';
|
|
9
9
|
|
|
10
|
-
const VERSION = '0.1.
|
|
10
|
+
const VERSION = '0.1.3';
|
|
11
11
|
const DEFAULT_SERVER = process.env.SUPERCOLLAB_URL || 'https://hyper.polynode.dev';
|
|
12
12
|
const DEFAULT_CONFIG = process.env.SUPERCOLLAB_CONFIG || path.join(os.homedir(), '.supercollab', 'config.json');
|
|
13
13
|
const SESSION_TTL_SKEW = 60;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supercollab/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "SuperCollab CLI and MCP bridge for secure agent collaboration workspaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"supercollab": "./bin/supercollab.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"bin",
|
|
10
|
+
"bin/supercollab.js",
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"engines": {
|
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import crypto from 'node:crypto';
|
|
6
|
-
import readline from 'node:readline/promises';
|
|
7
|
-
import { stdin as input, stdout as output } from 'node:process';
|
|
8
|
-
|
|
9
|
-
const VERSION = '0.1.1';
|
|
10
|
-
const DEFAULT_SERVER = process.env.SUPERCOLLAB_URL || 'https://hyper.polynode.dev';
|
|
11
|
-
const DEFAULT_CONFIG = process.env.SUPERCOLLAB_CONFIG || path.join(os.homedir(), '.supercollab', 'config.json');
|
|
12
|
-
const SESSION_TTL_SKEW = 60;
|
|
13
|
-
|
|
14
|
-
function printHelp() {
|
|
15
|
-
console.log(`SuperCollab CLI ${VERSION}
|
|
16
|
-
|
|
17
|
-
Usage:
|
|
18
|
-
supercollab register --username NAME [--password PASS] [--server URL] [--label LABEL]
|
|
19
|
-
supercollab login --username NAME [--password PASS] [--server URL]
|
|
20
|
-
supercollab whoami
|
|
21
|
-
supercollab agent register [--label LABEL]
|
|
22
|
-
supercollab workspace list
|
|
23
|
-
supercollab workspace create --title TITLE --goal GOAL [--slug SLUG]
|
|
24
|
-
supercollab workspace invite --workspace ID [--role member]
|
|
25
|
-
supercollab workspace join --invite TOKEN
|
|
26
|
-
supercollab workspace message --workspace ID --text TEXT
|
|
27
|
-
supercollab workspace messages --workspace ID [--limit 20]
|
|
28
|
-
supercollab workspace search --workspace ID --query TEXT
|
|
29
|
-
supercollab workspace write --workspace ID --path PATH --content TEXT
|
|
30
|
-
supercollab workspace read --workspace ID --path PATH
|
|
31
|
-
supercollab workspace git --workspace ID
|
|
32
|
-
supercollab heartbeat set --workspace ID --prompt TEXT [--interval 900]
|
|
33
|
-
supercollab heartbeat tick --workspace ID
|
|
34
|
-
supercollab mcp stdio
|
|
35
|
-
supercollab mcp print-config --client codex
|
|
36
|
-
supercollab config path
|
|
37
|
-
|
|
38
|
-
Options:
|
|
39
|
-
--config PATH Config path, default ~/.supercollab/config.json
|
|
40
|
-
--server URL SuperCollab API URL, default ${DEFAULT_SERVER}
|
|
41
|
-
|
|
42
|
-
Environment:
|
|
43
|
-
SUPERCOLLAB_PASSWORD can provide password non-interactively.
|
|
44
|
-
SUPERCOLLAB_CONFIG can override config path.
|
|
45
|
-
`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function parse(argv) {
|
|
49
|
-
const positionals = [];
|
|
50
|
-
const opts = {};
|
|
51
|
-
for (let i = 0; i < argv.length; i++) {
|
|
52
|
-
const arg = argv[i];
|
|
53
|
-
if (arg.startsWith('--')) {
|
|
54
|
-
const key = arg.slice(2);
|
|
55
|
-
const next = argv[i + 1];
|
|
56
|
-
if (!next || next.startsWith('--')) {
|
|
57
|
-
opts[key] = true;
|
|
58
|
-
} else {
|
|
59
|
-
opts[key] = next;
|
|
60
|
-
i++;
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
positionals.push(arg);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return { positionals, opts };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function configPath(opts = {}) {
|
|
70
|
-
return opts.config || DEFAULT_CONFIG;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function ensureConfigDir(file) {
|
|
74
|
-
const dir = path.dirname(file);
|
|
75
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
76
|
-
try { fs.chmodSync(dir, 0o700); } catch {}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function loadConfig(file = DEFAULT_CONFIG) {
|
|
80
|
-
if (!fs.existsSync(file)) return { serverUrl: DEFAULT_SERVER };
|
|
81
|
-
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
82
|
-
return { serverUrl: DEFAULT_SERVER, ...data };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function saveConfig(config, file = DEFAULT_CONFIG) {
|
|
86
|
-
ensureConfigDir(file);
|
|
87
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
88
|
-
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
89
|
-
fs.renameSync(tmp, file);
|
|
90
|
-
try { fs.chmodSync(file, 0o600); } catch {}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function requireValue(opts, key) {
|
|
94
|
-
if (!opts[key] || opts[key] === true) throw new Error(`missing --${key}`);
|
|
95
|
-
return String(opts[key]);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function readPassword(prompt = 'Password: ') {
|
|
99
|
-
if (process.env.SUPERCOLLAB_PASSWORD) return process.env.SUPERCOLLAB_PASSWORD;
|
|
100
|
-
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
|
101
|
-
throw new Error('password required; pass --password or set SUPERCOLLAB_PASSWORD');
|
|
102
|
-
}
|
|
103
|
-
return await new Promise((resolve, reject) => {
|
|
104
|
-
let password = '';
|
|
105
|
-
const stdin = process.stdin;
|
|
106
|
-
const wasRaw = stdin.isRaw;
|
|
107
|
-
const cleanup = () => {
|
|
108
|
-
stdin.off('data', onData);
|
|
109
|
-
try { stdin.setRawMode(Boolean(wasRaw)); } catch {}
|
|
110
|
-
stdin.pause();
|
|
111
|
-
};
|
|
112
|
-
const onData = (chunk) => {
|
|
113
|
-
for (const char of chunk.toString('utf8')) {
|
|
114
|
-
if (char === '\u0003') {
|
|
115
|
-
cleanup();
|
|
116
|
-
output.write('\n');
|
|
117
|
-
reject(new Error('cancelled'));
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (char === '\r' || char === '\n' || char === '\u0004') {
|
|
121
|
-
cleanup();
|
|
122
|
-
output.write('\n');
|
|
123
|
-
resolve(password);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (char === '\u007f' || char === '\b') {
|
|
127
|
-
if (password.length > 0) {
|
|
128
|
-
password = password.slice(0, -1);
|
|
129
|
-
output.write('\b \b');
|
|
130
|
-
}
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
password += char;
|
|
134
|
-
output.write('*');
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
output.write(prompt);
|
|
138
|
-
stdin.setEncoding('utf8');
|
|
139
|
-
stdin.setRawMode(true);
|
|
140
|
-
stdin.resume();
|
|
141
|
-
stdin.on('data', onData);
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function api(config, method, endpoint, body, token) {
|
|
146
|
-
const server = (config.serverUrl || DEFAULT_SERVER).replace(/\/$/, '');
|
|
147
|
-
const headers = { 'content-type': 'application/json', 'user-agent': `supercollab-cli/${VERSION}` };
|
|
148
|
-
const auth = token || config.userToken || config.agentSessionToken;
|
|
149
|
-
if (auth) headers.authorization = `Bearer ${auth}`;
|
|
150
|
-
const res = await fetch(server + endpoint, {
|
|
151
|
-
method,
|
|
152
|
-
headers,
|
|
153
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
154
|
-
});
|
|
155
|
-
const text = await res.text();
|
|
156
|
-
let data = {};
|
|
157
|
-
if (text) {
|
|
158
|
-
try { data = JSON.parse(text); } catch { data = { text }; }
|
|
159
|
-
}
|
|
160
|
-
if (!res.ok) {
|
|
161
|
-
const detail = data.detail ? JSON.stringify(data.detail) : text;
|
|
162
|
-
throw new Error(`HTTP ${res.status} ${endpoint}: ${detail}`);
|
|
163
|
-
}
|
|
164
|
-
return data;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function generateAgentKeypair() {
|
|
168
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
169
|
-
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
|
|
170
|
-
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
171
|
-
const raw = publicKey.export({ type: 'spki', format: 'der' });
|
|
172
|
-
const digest = crypto.createHash('sha256').update(raw).digest().subarray(0, 10);
|
|
173
|
-
const fingerprint = 'ed25519:' + digest.toString('base64url').toLowerCase();
|
|
174
|
-
return { publicKeyPem, privateKeyPem, fingerprint };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function signRequest(privateKeyPem, method, endpoint, bodyString, timestamp, nonce) {
|
|
178
|
-
const bodyHash = crypto.createHash('sha256').update(bodyString).digest('hex');
|
|
179
|
-
const signing = Buffer.from(`${method.toUpperCase()}\n${endpoint}\n${bodyHash}\n${timestamp}\n${nonce}`);
|
|
180
|
-
const sig = crypto.sign(null, signing, crypto.createPrivateKey(privateKeyPem)).toString('base64url');
|
|
181
|
-
return sig + '='.repeat((4 - (sig.length % 4)) % 4);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function ensureAgentSession(config) {
|
|
185
|
-
const now = Math.floor(Date.now() / 1000);
|
|
186
|
-
if (config.agentSessionToken && config.agentSessionExpiresAt && config.agentSessionExpiresAt - SESSION_TTL_SKEW > now) {
|
|
187
|
-
return config.agentSessionToken;
|
|
188
|
-
}
|
|
189
|
-
if (!config.agentId || !config.agentPrivateKeyPem) throw new Error('no local agent registered; run supercollab agent register');
|
|
190
|
-
const endpoint = '/v1/agent-sessions';
|
|
191
|
-
const bodyString = JSON.stringify({ agent_id: config.agentId });
|
|
192
|
-
const timestamp = String(now);
|
|
193
|
-
const nonce = crypto.randomBytes(18).toString('base64url');
|
|
194
|
-
const signature = signRequest(config.agentPrivateKeyPem, 'POST', endpoint, bodyString, timestamp, nonce);
|
|
195
|
-
const server = (config.serverUrl || DEFAULT_SERVER).replace(/\/$/, '');
|
|
196
|
-
const res = await fetch(server + endpoint, {
|
|
197
|
-
method: 'POST',
|
|
198
|
-
headers: {
|
|
199
|
-
'content-type': 'application/json',
|
|
200
|
-
'user-agent': `supercollab-cli/${VERSION}`,
|
|
201
|
-
'x-supercollab-timestamp': timestamp,
|
|
202
|
-
'x-supercollab-nonce': nonce,
|
|
203
|
-
'x-supercollab-signature': signature,
|
|
204
|
-
},
|
|
205
|
-
body: bodyString,
|
|
206
|
-
});
|
|
207
|
-
const data = await res.json();
|
|
208
|
-
if (!res.ok) throw new Error(`agent session failed: ${JSON.stringify(data)}`);
|
|
209
|
-
config.agentSessionToken = data.token;
|
|
210
|
-
config.agentSessionExpiresAt = data.expires_at;
|
|
211
|
-
return data.token;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function apiAsAgent(config, method, endpoint, body) {
|
|
215
|
-
const token = await ensureAgentSession(config);
|
|
216
|
-
return api(config, method, endpoint, body, token);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function registerAgent(config, label) {
|
|
220
|
-
if (!config.userToken) throw new Error('login/register first');
|
|
221
|
-
const keys = generateAgentKeypair();
|
|
222
|
-
const data = await api(config, 'POST', '/v1/agents/register', { label, public_key_pem: keys.publicKeyPem }, config.userToken);
|
|
223
|
-
config.agentId = data.agent_id;
|
|
224
|
-
config.agentLabel = label;
|
|
225
|
-
config.agentFingerprint = data.fingerprint;
|
|
226
|
-
config.agentPrivateKeyPem = keys.privateKeyPem;
|
|
227
|
-
delete config.agentSessionToken;
|
|
228
|
-
delete config.agentSessionExpiresAt;
|
|
229
|
-
return data;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function doRegister(opts) {
|
|
233
|
-
const file = configPath(opts);
|
|
234
|
-
const config = loadConfig(file);
|
|
235
|
-
config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
|
|
236
|
-
const username = requireValue(opts, 'username');
|
|
237
|
-
const password = opts.password ? String(opts.password) : await readPassword();
|
|
238
|
-
const data = await api(config, 'POST', '/v1/auth/register', { username, password }, null);
|
|
239
|
-
config.userId = data.user_id;
|
|
240
|
-
config.username = data.username;
|
|
241
|
-
config.userToken = data.token;
|
|
242
|
-
const agent = await registerAgent(config, String(opts.label || `${os.hostname()}-agent`));
|
|
243
|
-
saveConfig(config, file);
|
|
244
|
-
console.log(JSON.stringify({ ok: true, username: config.username, user_id: config.userId, agent_id: agent.agent_id, fingerprint: agent.fingerprint, config: file }, null, 2));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function doLogin(opts) {
|
|
248
|
-
const file = configPath(opts);
|
|
249
|
-
const config = loadConfig(file);
|
|
250
|
-
config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
|
|
251
|
-
const username = requireValue(opts, 'username');
|
|
252
|
-
const password = opts.password ? String(opts.password) : await readPassword();
|
|
253
|
-
const data = await api(config, 'POST', '/v1/auth/login', { username, password }, null);
|
|
254
|
-
config.userId = data.user_id;
|
|
255
|
-
config.username = data.username;
|
|
256
|
-
config.userToken = data.token;
|
|
257
|
-
delete config.agentSessionToken;
|
|
258
|
-
delete config.agentSessionExpiresAt;
|
|
259
|
-
saveConfig(config, file);
|
|
260
|
-
console.log(JSON.stringify({ ok: true, username: config.username, user_id: config.userId, config: file }, null, 2));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function toolSchema(name, description, properties = {}, required = []) {
|
|
264
|
-
return { name, description, inputSchema: { type: 'object', properties, required } };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function mcpTools() {
|
|
268
|
-
const s = { type: 'string' };
|
|
269
|
-
return [
|
|
270
|
-
toolSchema('supercollab_status', 'Check SuperCollab server and authenticated actor status.'),
|
|
271
|
-
toolSchema('workspace_list', 'List workspaces visible to this agent.'),
|
|
272
|
-
toolSchema('workspace_create', 'Create a goal-scoped workspace.', { title: s, goal: s, slug: s }, ['title', 'goal']),
|
|
273
|
-
toolSchema('workspace_invite', 'Create a short-lived invite token.', { workspace_id: s, role: s, ttl_seconds: { type: 'integer' } }, ['workspace_id']),
|
|
274
|
-
toolSchema('workspace_join', 'Accept an invite token.', { invite_token: s, fingerprint: s }, ['invite_token']),
|
|
275
|
-
toolSchema('workspace_message_send', 'Send a workspace message.', { workspace_id: s, text: s }, ['workspace_id', 'text']),
|
|
276
|
-
toolSchema('workspace_messages_read', 'Read workspace events/messages.', { workspace_id: s, limit: { type: 'integer' } }, ['workspace_id']),
|
|
277
|
-
toolSchema('workspace_file_list', 'List workspace files.', { workspace_id: s, path: s }, ['workspace_id']),
|
|
278
|
-
toolSchema('workspace_file_read', 'Read a workspace file.', { workspace_id: s, path: s }, ['workspace_id', 'path']),
|
|
279
|
-
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']),
|
|
280
|
-
toolSchema('workspace_git_status', 'Show workspace Git status and recent commits.', { workspace_id: s }, ['workspace_id']),
|
|
281
|
-
toolSchema('workspace_task_create', 'Create a task.', { workspace_id: s, title: s, body: s, owner: s }, ['workspace_id', 'title']),
|
|
282
|
-
toolSchema('workspace_decision_record', 'Record a decision.', { workspace_id: s, title: s, decision: s, rationale: s }, ['workspace_id', 'title', 'decision']),
|
|
283
|
-
toolSchema('workspace_memory_reindex', 'Rebuild workspace search index.', { workspace_id: s }, ['workspace_id']),
|
|
284
|
-
toolSchema('workspace_memory_search', 'Search workspace memory.', { workspace_id: s, query: s, limit: { type: 'integer' } }, ['workspace_id', 'query']),
|
|
285
|
-
toolSchema('workspace_heartbeat_get', 'Read heartbeat config.', { workspace_id: s }, ['workspace_id']),
|
|
286
|
-
toolSchema('workspace_heartbeat_set', 'Set heartbeat config.', { workspace_id: s, prompt: s, interval_seconds: { type: 'integer' }, enabled: { type: 'boolean' } }, ['workspace_id', 'prompt']),
|
|
287
|
-
toolSchema('workspace_heartbeat_tick', 'Emit heartbeat now.', { workspace_id: s }, ['workspace_id']),
|
|
288
|
-
];
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async function callTool(config, name, args) {
|
|
292
|
-
if (name === 'supercollab_status') return { health: await api(config, 'GET', '/health'), me: await apiAsAgent(config, 'GET', '/v1/me') };
|
|
293
|
-
if (name === 'workspace_list') return apiAsAgent(config, 'GET', '/v1/workspaces');
|
|
294
|
-
if (name === 'workspace_create') return apiAsAgent(config, 'POST', '/v1/workspaces', { title: args.title, goal: args.goal, slug: args.slug });
|
|
295
|
-
if (name === 'workspace_invite') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/invites`, { role: args.role || 'member', ttl_seconds: args.ttl_seconds || 86400 });
|
|
296
|
-
if (name === 'workspace_join') return apiAsAgent(config, 'POST', '/v1/invites/accept', { token: args.invite_token, fingerprint: args.fingerprint || config.agentFingerprint });
|
|
297
|
-
if (name === 'workspace_message_send') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/messages`, { text: args.text });
|
|
298
|
-
if (name === 'workspace_messages_read') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/messages?limit=${encodeURIComponent(args.limit || 50)}`);
|
|
299
|
-
if (name === 'workspace_file_list') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/files?path=${encodeURIComponent(args.path || '')}`);
|
|
300
|
-
if (name === 'workspace_file_read') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/files/read?path=${encodeURIComponent(args.path)}`);
|
|
301
|
-
if (name === 'workspace_file_write') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/files/write`, { path: args.path, content: args.content, commit_message: args.commit_message });
|
|
302
|
-
if (name === 'workspace_git_status') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/git/status`);
|
|
303
|
-
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' });
|
|
304
|
-
if (name === 'workspace_decision_record') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/decisions`, { title: args.title, decision: args.decision, rationale: args.rationale || '' });
|
|
305
|
-
if (name === 'workspace_memory_reindex') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/memory/reindex`, {});
|
|
306
|
-
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)}`);
|
|
307
|
-
if (name === 'workspace_heartbeat_get') return apiAsAgent(config, 'GET', `/v1/workspaces/${args.workspace_id}/heartbeat`);
|
|
308
|
-
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 });
|
|
309
|
-
if (name === 'workspace_heartbeat_tick') return apiAsAgent(config, 'POST', `/v1/workspaces/${args.workspace_id}/heartbeat/tick`, {});
|
|
310
|
-
throw new Error(`unknown tool: ${name}`);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function writeRpc(payload) {
|
|
314
|
-
const raw = Buffer.from(JSON.stringify(payload));
|
|
315
|
-
process.stdout.write(`Content-Length: ${raw.length}\r\n\r\n`);
|
|
316
|
-
process.stdout.write(raw);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async function runMcp(opts) {
|
|
320
|
-
const config = loadConfig(configPath(opts));
|
|
321
|
-
let buffer = Buffer.alloc(0);
|
|
322
|
-
for await (const chunk of process.stdin) {
|
|
323
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
324
|
-
while (true) {
|
|
325
|
-
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
326
|
-
if (headerEnd < 0) break;
|
|
327
|
-
const header = buffer.subarray(0, headerEnd).toString();
|
|
328
|
-
const match = header.match(/content-length:\s*(\d+)/i);
|
|
329
|
-
if (!match) throw new Error('missing content-length');
|
|
330
|
-
const length = Number(match[1]);
|
|
331
|
-
const total = headerEnd + 4 + length;
|
|
332
|
-
if (buffer.length < total) break;
|
|
333
|
-
const body = buffer.subarray(headerEnd + 4, total).toString();
|
|
334
|
-
buffer = buffer.subarray(total);
|
|
335
|
-
const msg = JSON.parse(body);
|
|
336
|
-
const id = msg.id;
|
|
337
|
-
try {
|
|
338
|
-
let result;
|
|
339
|
-
if (msg.method === 'initialize') {
|
|
340
|
-
result = { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'supercollab', version: VERSION } };
|
|
341
|
-
} else if (msg.method === 'tools/list') {
|
|
342
|
-
result = { tools: mcpTools() };
|
|
343
|
-
} else if (msg.method === 'tools/call') {
|
|
344
|
-
const data = await callTool(config, msg.params.name, msg.params.arguments || {});
|
|
345
|
-
result = { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
346
|
-
} else if (msg.method && msg.method.startsWith('notifications/')) {
|
|
347
|
-
continue;
|
|
348
|
-
} else {
|
|
349
|
-
throw new Error(`unsupported method: ${msg.method}`);
|
|
350
|
-
}
|
|
351
|
-
if (id !== undefined) writeRpc({ jsonrpc: '2.0', id, result });
|
|
352
|
-
} catch (err) {
|
|
353
|
-
if (id !== undefined) writeRpc({ jsonrpc: '2.0', id, error: { code: -32000, message: err.message } });
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function printCodexConfig(opts) {
|
|
360
|
-
const file = configPath(opts);
|
|
361
|
-
console.log(`[mcp_servers.supercollab]\ncommand = "supercollab"\nargs = ["mcp", "stdio", "--config", "${file.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"]`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async function main() {
|
|
365
|
-
const { positionals, opts } = parse(process.argv.slice(2));
|
|
366
|
-
if (opts.help || positionals.length === 0) { printHelp(); return; }
|
|
367
|
-
const [cmd, sub] = positionals;
|
|
368
|
-
const file = configPath(opts);
|
|
369
|
-
const config = loadConfig(file);
|
|
370
|
-
config.serverUrl = opts.server || config.serverUrl || DEFAULT_SERVER;
|
|
371
|
-
|
|
372
|
-
if (cmd === 'register') return doRegister(opts);
|
|
373
|
-
if (cmd === 'login') return doLogin(opts);
|
|
374
|
-
if (cmd === 'whoami') return console.log(JSON.stringify(await api(config, 'GET', '/v1/me'), null, 2));
|
|
375
|
-
if (cmd === 'config' && sub === 'path') return console.log(file);
|
|
376
|
-
if (cmd === 'agent' && sub === 'register') {
|
|
377
|
-
const data = await registerAgent(config, String(opts.label || `${os.hostname()}-agent`));
|
|
378
|
-
saveConfig(config, file);
|
|
379
|
-
return console.log(JSON.stringify({ ok: true, agent_id: data.agent_id, fingerprint: data.fingerprint, config: file }, null, 2));
|
|
380
|
-
}
|
|
381
|
-
if (cmd === 'mcp' && sub === 'stdio') return runMcp(opts);
|
|
382
|
-
if (cmd === 'mcp' && sub === 'print-config') return printCodexConfig(opts);
|
|
383
|
-
if (cmd === 'workspace') {
|
|
384
|
-
if (sub === 'list') return console.log(JSON.stringify(await callTool(config, 'workspace_list', {}), null, 2));
|
|
385
|
-
if (sub === 'create') return console.log(JSON.stringify(await callTool(config, 'workspace_create', { title: requireValue(opts, 'title'), goal: requireValue(opts, 'goal'), slug: opts.slug }), null, 2));
|
|
386
|
-
if (sub === 'invite') return console.log(JSON.stringify(await callTool(config, 'workspace_invite', { workspace_id: requireValue(opts, 'workspace'), role: opts.role || 'member', ttl_seconds: opts.ttl || 86400 }), null, 2));
|
|
387
|
-
if (sub === 'join') return console.log(JSON.stringify(await callTool(config, 'workspace_join', { invite_token: requireValue(opts, 'invite') }), null, 2));
|
|
388
|
-
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));
|
|
389
|
-
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));
|
|
390
|
-
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));
|
|
391
|
-
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));
|
|
392
|
-
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));
|
|
393
|
-
if (sub === 'git') return console.log(JSON.stringify(await callTool(config, 'workspace_git_status', { workspace_id: requireValue(opts, 'workspace') }), null, 2));
|
|
394
|
-
}
|
|
395
|
-
if (cmd === 'heartbeat') {
|
|
396
|
-
if (sub === 'set') return console.log(JSON.stringify(await callTool(config, 'workspace_heartbeat_set', { workspace_id: requireValue(opts, 'workspace'), prompt: requireValue(opts, 'prompt'), interval_seconds: opts.interval || 900, enabled: opts.enabled !== 'false' }), null, 2));
|
|
397
|
-
if (sub === 'tick') return console.log(JSON.stringify(await callTool(config, 'workspace_heartbeat_tick', { workspace_id: requireValue(opts, 'workspace') }), null, 2));
|
|
398
|
-
}
|
|
399
|
-
throw new Error(`unknown command: ${positionals.join(' ')}`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
main().catch((err) => {
|
|
403
|
-
console.error(`supercollab: ${err.message}`);
|
|
404
|
-
process.exit(1);
|
|
405
|
-
});
|