severally-mcp 2.3.1 → 2.4.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.ja.md +17 -7
- package/README.md +17 -7
- package/bin/severally-mcp.mjs +14 -2
- package/docs/install.ja.md +48 -20
- package/docs/install.md +48 -21
- package/package.json +13 -3
- package/plugins/severally/.claude-plugin/plugin.json +1 -1
- package/plugins/severally/.codex-plugin/plugin.json +1 -1
- package/plugins/severally/dist/severally-mcp.mjs +10669 -10404
- package/scripts/build.mjs +31 -26
- package/scripts/doctor.mjs +52 -17
- package/scripts/init-config.mjs +8 -6
- package/scripts/install.mjs +259 -86
- package/scripts/live-check.mjs +52 -19
- package/scripts/live-mcp-check.mjs +42 -12
- package/scripts/test.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +39 -23
- package/src/adapters/antigravity.mjs +34 -16
- package/src/adapters/claude-code.mjs +73 -21
- package/src/adapters/codex.mjs +69 -27
- package/src/adapters/opencode-sandbox.mjs +40 -21
- package/src/adapters/opencode.mjs +31 -13
- package/src/brief.mjs +65 -41
- package/src/consultants.mjs +72 -24
- package/src/export.mjs +49 -12
- package/src/expose-paths.mjs +162 -44
- package/src/failures.mjs +23 -13
- package/src/jobs.mjs +320 -152
- package/src/parse-result.mjs +85 -30
- package/src/platform.mjs +5 -7
- package/src/policy.mjs +97 -27
- package/src/redact.mjs +38 -14
- package/src/run.mjs +67 -18
- package/src/schema.mjs +50 -18
- package/src/server.mjs +134 -72
- package/src/status.mjs +81 -12
- package/src/store.mjs +131 -29
package/scripts/install.mjs
CHANGED
|
@@ -27,24 +27,38 @@
|
|
|
27
27
|
// client's own CLI rather than hand-edited, and every file this touches is
|
|
28
28
|
// backed up first under ~/.severally/backups/<timestamp>/.
|
|
29
29
|
//
|
|
30
|
-
//
|
|
30
|
+
// When the global npm install cannot be made (npm needs sudo, or npm's global
|
|
31
|
+
// folder is not on PATH), plugin mode registers Codex directly instead, so the
|
|
32
|
+
// install still finishes.
|
|
33
|
+
//
|
|
34
|
+
// node scripts/install.mjs [--dry-run] [--manual] [--skip-global] [--force] [--allow-local-tools]
|
|
31
35
|
|
|
32
36
|
import { copyTree, execCommandSync, findCommand } from '../src/platform.mjs';
|
|
33
37
|
import fs from 'node:fs';
|
|
34
38
|
import os from 'node:os';
|
|
35
39
|
import path from 'node:path';
|
|
40
|
+
import readline from 'node:readline/promises';
|
|
36
41
|
import { fileURLToPath } from 'node:url';
|
|
37
42
|
|
|
43
|
+
if (Number(process.versions.node.split('.')[0]) < 22) {
|
|
44
|
+
console.error(
|
|
45
|
+
`severally-install: Node.js 22 or newer is needed (this is ${process.versions.node}); update Node.js and run it again`,
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
51
|
const PLUGIN = path.join(root, 'plugins', 'severally');
|
|
40
|
-
const FLAGS = ['--dry-run', '--manual', '--skip-global', '--force'];
|
|
52
|
+
const FLAGS = ['--dry-run', '--manual', '--skip-global', '--force', '--allow-local-tools'];
|
|
41
53
|
const argv = new Set(process.argv.slice(2));
|
|
42
54
|
// A word the installer does not know is refused before anything changes: a
|
|
43
55
|
// shell that passes a pasted "# add --dry-run ..." comment on as arguments
|
|
44
56
|
// would otherwise turn an install into a silent dry run.
|
|
45
57
|
const unknownArgs = [...argv].filter((a) => !FLAGS.includes(a));
|
|
46
58
|
if (unknownArgs.length) {
|
|
47
|
-
console.error(
|
|
59
|
+
console.error(
|
|
60
|
+
`severally-install: unknown argument(s) ${unknownArgs.map((a) => JSON.stringify(a)).join(' ')}; it takes ${FLAGS.join(' ')}`,
|
|
61
|
+
);
|
|
48
62
|
process.exit(2);
|
|
49
63
|
}
|
|
50
64
|
const dryRun = argv.has('--dry-run');
|
|
@@ -98,7 +112,10 @@ function backupName(target) {
|
|
|
98
112
|
|
|
99
113
|
function backup(file) {
|
|
100
114
|
if (!fs.existsSync(file)) return null;
|
|
101
|
-
if (dryRun) {
|
|
115
|
+
if (dryRun) {
|
|
116
|
+
log(` [dry-run] backup ${file}`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
102
119
|
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
|
|
103
120
|
const dest = path.join(backupDir, backupName(file));
|
|
104
121
|
// Several client operations can touch the same config in one install.
|
|
@@ -110,7 +127,10 @@ function backup(file) {
|
|
|
110
127
|
|
|
111
128
|
function backupTree(dir) {
|
|
112
129
|
if (!fs.existsSync(dir)) return null;
|
|
113
|
-
if (dryRun) {
|
|
130
|
+
if (dryRun) {
|
|
131
|
+
log(` [dry-run] backup ${dir}`);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
114
134
|
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
|
|
115
135
|
const dest = path.join(backupDir, backupName(dir));
|
|
116
136
|
copyTree(dir, dest);
|
|
@@ -128,7 +148,8 @@ function which(bin) {
|
|
|
128
148
|
// severally-mcp package, so they are not where the command is looked for.
|
|
129
149
|
function runtimeCommand() {
|
|
130
150
|
const key = Object.keys(process.env).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH';
|
|
131
|
-
const dirs = (process.env[key] ?? '')
|
|
151
|
+
const dirs = (process.env[key] ?? '')
|
|
152
|
+
.split(path.delimiter)
|
|
132
153
|
.filter((dir) => !/(^|[\\/])node_modules[\\/]\.bin[\\/]?$/.test(dir.replace(/^"(.*)"$/, '$1')));
|
|
133
154
|
return findCommand('severally-mcp', { env: { ...process.env, [key]: dirs.join(path.delimiter) } });
|
|
134
155
|
}
|
|
@@ -153,7 +174,10 @@ function installRuntime() {
|
|
|
153
174
|
run(process.execPath, ['--check', staged], { real: true });
|
|
154
175
|
// The bundle carries other packages' code, so their licences travel with
|
|
155
176
|
// it -- copied first, so a missing one fails before the runtime changes.
|
|
156
|
-
fs.copyFileSync(
|
|
177
|
+
fs.copyFileSync(
|
|
178
|
+
path.join(path.dirname(source), 'THIRD_PARTY_NOTICES'),
|
|
179
|
+
path.join(path.dirname(installed), 'THIRD_PARTY_NOTICES'),
|
|
180
|
+
);
|
|
157
181
|
fs.copyFileSync(path.join(PLUGIN, 'LICENSE'), path.join(path.dirname(installed), 'LICENSE'));
|
|
158
182
|
backup(installed);
|
|
159
183
|
fs.renameSync(staged, installed);
|
|
@@ -164,15 +188,28 @@ function installRuntime() {
|
|
|
164
188
|
return installed;
|
|
165
189
|
}
|
|
166
190
|
|
|
191
|
+
const RUNTIME_PACKAGE = 'severally-runtime';
|
|
192
|
+
// Why plugin mode registers Codex directly (no global command), or null.
|
|
193
|
+
let codexDirect = null;
|
|
194
|
+
let globalRoot = null;
|
|
167
195
|
// Run from a global install of the npm package, the global install below
|
|
168
196
|
// would replace the very package this script and its plugin files are read
|
|
169
197
|
// from. Refused before anything changes.
|
|
170
198
|
if (mode === 'plugin' && !skipGlobal) {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
199
|
+
const npmRoot = tryRun('npm', ['root', '-g'], { real: true });
|
|
200
|
+
globalRoot = npmRoot.ok ? npmRoot.out.trim() || null : null;
|
|
201
|
+
const inside = (dir) => {
|
|
202
|
+
try {
|
|
203
|
+
return fs.realpathSync(root).startsWith(fs.realpathSync(dir) + path.sep);
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
if (globalRoot && inside(globalRoot)) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
'severally-install was run from a global npm install of severally-mcp, which installing would replace. ' +
|
|
211
|
+
'Remove it (npm uninstall -g severally-mcp), then run npx -y severally-mcp@latest install, or install from a clone.',
|
|
212
|
+
);
|
|
176
213
|
}
|
|
177
214
|
}
|
|
178
215
|
|
|
@@ -184,29 +221,53 @@ const marketplaceRoot = path.join(home, '.severally', 'marketplace');
|
|
|
184
221
|
if (mode === 'plugin') {
|
|
185
222
|
// npm may symlink a directory install. Its source must therefore be the
|
|
186
223
|
// persistent runtime, never the user's disposable checkout.
|
|
224
|
+
// The runtime goes global under a name of its own: under the npm package's
|
|
225
|
+
// name and version, npx would run this installer-less copy for
|
|
226
|
+
// `npx severally-mcp@<that version> install` instead of fetching the package.
|
|
187
227
|
const packageFile = path.join(runtimeDir, 'package.json');
|
|
188
228
|
const metadata = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
189
229
|
if (!dryRun) {
|
|
190
230
|
backup(packageFile);
|
|
191
|
-
fs.writeFileSync(
|
|
192
|
-
|
|
231
|
+
fs.writeFileSync(
|
|
232
|
+
packageFile,
|
|
233
|
+
`${JSON.stringify(
|
|
234
|
+
{ name: RUNTIME_PACKAGE, version: metadata.version, type: 'module', bin: { 'severally-mcp': 'severally-mcp.mjs' } },
|
|
235
|
+
null,
|
|
236
|
+
2,
|
|
237
|
+
)}\n`,
|
|
238
|
+
);
|
|
193
239
|
fs.chmodSync(serverCommand[1], 0o755);
|
|
194
240
|
}
|
|
195
241
|
if (skipGlobal) {
|
|
196
242
|
const existing = runtimeCommand();
|
|
197
243
|
if (!dryRun && (!existing || fs.realpathSync(existing) !== fs.realpathSync(serverCommand[1]))) {
|
|
198
|
-
throw new Error(
|
|
244
|
+
throw new Error(
|
|
245
|
+
'--skip-global requires severally-mcp on PATH to point to the installed runtime. Omit --skip-global or use --manual.',
|
|
246
|
+
);
|
|
199
247
|
}
|
|
200
248
|
log(' --skip-global: using the existing runtime command');
|
|
201
249
|
} else {
|
|
202
|
-
|
|
250
|
+
// Installs before the rename put the runtime there under the package's
|
|
251
|
+
// own name; its severally-mcp command would collide with the new one.
|
|
252
|
+
if (globalRoot && fs.existsSync(path.join(globalRoot, metadata.name))) {
|
|
253
|
+
const r = tryRun('npm', ['uninstall', '-g', metadata.name]);
|
|
254
|
+
log(
|
|
255
|
+
r.ok
|
|
256
|
+
? ` removed the global ${metadata.name} package (the runtime is now ${RUNTIME_PACKAGE})`
|
|
257
|
+
: ` could not remove the global ${metadata.name} package: ${r.out}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const r = tryRun('npm', ['install', '-g', runtimeDir, '--install-links=false'], { cwd: dryRun ? root : runtimeDir });
|
|
203
261
|
if (which('nodenv')) tryRun('nodenv', ['rehash']);
|
|
204
|
-
if (!
|
|
262
|
+
if (!r.ok) {
|
|
263
|
+
codexDirect = `npm could not install the severally-mcp command globally (${r.out.split('\n').find((l) => /ERR|EACCES|EPERM/.test(l)) ?? r.out.split('\n')[0]})`;
|
|
264
|
+
} else if (!dryRun) {
|
|
205
265
|
const installedCommand = runtimeCommand();
|
|
206
266
|
if (!installedCommand || fs.realpathSync(installedCommand) !== fs.realpathSync(serverCommand[1])) {
|
|
207
|
-
|
|
267
|
+
codexDirect = "npm's global severally-mcp command is not first on PATH";
|
|
208
268
|
}
|
|
209
269
|
}
|
|
270
|
+
if (codexDirect) log(` ${codexDirect}; Codex will be registered directly instead, which needs no global command`);
|
|
210
271
|
}
|
|
211
272
|
if (dryRun) {
|
|
212
273
|
backupTree(marketplaceRoot);
|
|
@@ -221,8 +282,10 @@ if (mode === 'plugin') {
|
|
|
221
282
|
try {
|
|
222
283
|
fs.mkdirSync(path.join(next, '.agents', 'plugins'), { recursive: true });
|
|
223
284
|
copyTree(PLUGIN, path.join(next, 'plugins', 'severally'));
|
|
224
|
-
fs.copyFileSync(
|
|
225
|
-
path.join(
|
|
285
|
+
fs.copyFileSync(
|
|
286
|
+
path.join(root, '.agents', 'plugins', 'marketplace.json'),
|
|
287
|
+
path.join(next, '.agents', 'plugins', 'marketplace.json'),
|
|
288
|
+
);
|
|
226
289
|
backupTree(marketplaceRoot);
|
|
227
290
|
if (fs.existsSync(marketplaceRoot)) fs.renameSync(marketplaceRoot, previous);
|
|
228
291
|
try {
|
|
@@ -266,14 +329,23 @@ let priorCodexTimeout;
|
|
|
266
329
|
// that is not one of its own sub-tables (codex may put env in one).
|
|
267
330
|
function codexTable() {
|
|
268
331
|
let lines;
|
|
269
|
-
try {
|
|
332
|
+
try {
|
|
333
|
+
lines = fs.readFileSync(codexConfig, 'utf8').split('\n');
|
|
334
|
+
} catch (err) {
|
|
335
|
+
return { reason: `cannot read ${codexConfig}: ${err.message}` };
|
|
336
|
+
}
|
|
270
337
|
const start = lines.findIndex((l) => /^\s*\[\s*mcp_servers\.severally\s*\]/.test(l));
|
|
271
338
|
if (start < 0) return { reason: `${codexConfig} has no [mcp_servers.severally] table` };
|
|
272
339
|
let end = lines.findIndex((l, i) => i > start && /^\s*\[/.test(l) && !/^\s*\[\s*mcp_servers\.severally\./.test(l));
|
|
273
340
|
if (end < 0) end = lines.length;
|
|
274
341
|
const block = lines.slice(start + 1, end).join('\n');
|
|
275
342
|
const timeout = block.match(/^\s*tool_timeout_sec\s*=\s*(\d+(?:\.\d+)?)/m)?.[1];
|
|
276
|
-
return {
|
|
343
|
+
return {
|
|
344
|
+
lines,
|
|
345
|
+
start,
|
|
346
|
+
timeout: timeout === undefined ? undefined : Number(timeout),
|
|
347
|
+
wait: /SEVERALLY_MAX_WAIT_MS\s*=\s*"?\d+/.test(block),
|
|
348
|
+
};
|
|
277
349
|
}
|
|
278
350
|
|
|
279
351
|
// The wait follows the timeout Codex will enforce: 15 s under the user's own
|
|
@@ -290,7 +362,11 @@ function writeCodexTimeout(sec) {
|
|
|
290
362
|
if (t.reason) return t.reason;
|
|
291
363
|
if (t.timeout !== undefined) return null; // never overwrite the user's value
|
|
292
364
|
t.lines.splice(t.start + 1, 0, `tool_timeout_sec = ${sec}`);
|
|
293
|
-
try {
|
|
365
|
+
try {
|
|
366
|
+
fs.writeFileSync(codexConfig, t.lines.join('\n'));
|
|
367
|
+
} catch (err) {
|
|
368
|
+
return `cannot write ${codexConfig}: ${err.message}`;
|
|
369
|
+
}
|
|
294
370
|
return null;
|
|
295
371
|
}
|
|
296
372
|
|
|
@@ -348,7 +424,9 @@ const CLIENTS = [
|
|
|
348
424
|
keptNote: () => {
|
|
349
425
|
const t = codexTable();
|
|
350
426
|
if (t.timeout === undefined || !t.wait) {
|
|
351
|
-
log(
|
|
427
|
+
log(
|
|
428
|
+
` this registration lacks the long consult_get wait (45 s); --force adds it (SEVERALLY_MAX_WAIT_MS and tool_timeout_sec = ${CODEX_TIMEOUT_SEC})`,
|
|
429
|
+
);
|
|
352
430
|
}
|
|
353
431
|
},
|
|
354
432
|
skillFrom: path.join(PLUGIN, 'skills', 'codex', 'severally'),
|
|
@@ -381,10 +459,7 @@ const CLIENTS = [
|
|
|
381
459
|
{
|
|
382
460
|
bin: 'opencode',
|
|
383
461
|
label: 'OpenCode',
|
|
384
|
-
backup: [
|
|
385
|
-
path.join(xdgConfig, 'opencode', 'opencode.json'),
|
|
386
|
-
path.join(xdgConfig, 'opencode', 'opencode.jsonc'),
|
|
387
|
-
],
|
|
462
|
+
backup: [path.join(xdgConfig, 'opencode', 'opencode.json'), path.join(xdgConfig, 'opencode', 'opencode.jsonc')],
|
|
388
463
|
// opencode has no `mcp get`; list and look for the exact server name.
|
|
389
464
|
// `mcp list` prints through the prompt library, but the per-server lines
|
|
390
465
|
// still reach stdout when stdout is not a TTY ("✓ severally connected").
|
|
@@ -405,24 +480,36 @@ const CLIENTS = [
|
|
|
405
480
|
remove: () => ({ ok: true, out: 'opencode has no mcp remove; re-adding replaces the entry' }),
|
|
406
481
|
// OpenCode sets no deadline of its own on a tool call (a 610 s call came
|
|
407
482
|
// back), so it gets the long consult_get wait with no timeout to pair.
|
|
408
|
-
|
|
483
|
+
// opencode 1.19 (its npm release) has no --global and always writes the
|
|
484
|
+
// global config; it is asked again without the flag, from the home
|
|
485
|
+
// folder, and the check after the add confirms where it landed.
|
|
486
|
+
add: (bin) => {
|
|
487
|
+
const r = tryRun('opencode', ['mcp', 'add', 'severally', '--global', '--env', LONG_WAIT, '--', ...bin]);
|
|
488
|
+
return r.ok ? r : tryRun('opencode', ['mcp', 'add', 'severally', '--env', LONG_WAIT, '--', ...bin], { cwd: home });
|
|
489
|
+
},
|
|
409
490
|
keptNote: () => {
|
|
410
491
|
const text = [path.join(xdgConfig, 'opencode', 'opencode.json'), path.join(xdgConfig, 'opencode', 'opencode.jsonc')]
|
|
411
|
-
.map((f) => {
|
|
412
|
-
|
|
492
|
+
.map((f) => {
|
|
493
|
+
try {
|
|
494
|
+
return fs.readFileSync(f, 'utf8');
|
|
495
|
+
} catch {
|
|
496
|
+
return '';
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
.join('\n');
|
|
500
|
+
if (!text.includes('SEVERALLY_MAX_WAIT_MS'))
|
|
501
|
+
log(' this registration lacks the long consult_get wait (45 s); --force adds it (SEVERALLY_MAX_WAIT_MS)');
|
|
413
502
|
},
|
|
414
503
|
// A zero exit from `mcp add` can be a usage dump in disguise (see above),
|
|
415
504
|
// so this client's registration is verified by listing after adding --
|
|
416
505
|
// and the recorded command is compared against this install's.
|
|
417
506
|
verifyAdd: true,
|
|
418
|
-
registrationConfig: [
|
|
419
|
-
path.join(xdgConfig, 'opencode', 'opencode.json'),
|
|
420
|
-
path.join(xdgConfig, 'opencode', 'opencode.jsonc'),
|
|
421
|
-
],
|
|
507
|
+
registrationConfig: [path.join(xdgConfig, 'opencode', 'opencode.json'), path.join(xdgConfig, 'opencode', 'opencode.jsonc')],
|
|
422
508
|
skillFrom: path.join(PLUGIN, 'skills', 'opencode', 'severally'),
|
|
423
509
|
skillTo: path.join(xdgConfig, 'opencode', 'skills', 'severally'),
|
|
424
510
|
},
|
|
425
511
|
];
|
|
512
|
+
const codexClient = CLIENTS.find((c) => c.bin === 'codex');
|
|
426
513
|
const antigravityClient = CLIENTS.find((c) => c.bin === 'agy');
|
|
427
514
|
const opencodeClient = CLIENTS.find((c) => c.bin === 'opencode');
|
|
428
515
|
|
|
@@ -435,7 +522,9 @@ function registrationCommandMatches(client, bin) {
|
|
|
435
522
|
let cfg;
|
|
436
523
|
try {
|
|
437
524
|
cfg = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\s*\/\/.*$/gm, ''));
|
|
438
|
-
} catch {
|
|
525
|
+
} catch {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
439
528
|
const entry = cfg.mcp?.servers?.severally ?? cfg.mcp?.severally;
|
|
440
529
|
const command = entry?.command ?? entry;
|
|
441
530
|
if (Array.isArray(command)) return JSON.stringify(command) === JSON.stringify(bin);
|
|
@@ -456,7 +545,8 @@ function listedAfterAdd(client) {
|
|
|
456
545
|
// Registers one client's MCP server directly (through its own `mcp add`) and
|
|
457
546
|
// copies its skill, backing up everything it touches first. Shared by plugin
|
|
458
547
|
// mode (Antigravity only) and --manual mode (all three clients).
|
|
459
|
-
function installClientDirectly(client, bin) {
|
|
548
|
+
function installClientDirectly(client, bin) {
|
|
549
|
+
step(`Registering with ${client.label}`);
|
|
460
550
|
if (!which(client.bin)) {
|
|
461
551
|
log(` ${client.bin} CLI not found on PATH — skipped`);
|
|
462
552
|
return;
|
|
@@ -501,7 +591,10 @@ function installClientDirectly(client, bin) { step(`Registering with ${client.l
|
|
|
501
591
|
if (!fs.existsSync(parent) && !dryRun) fs.mkdirSync(parent, { recursive: true });
|
|
502
592
|
const saved = backupTree(client.skillTo);
|
|
503
593
|
if (saved) log(` backed up the existing ${client.label} skill to ${saved}`);
|
|
504
|
-
if (dryRun) {
|
|
594
|
+
if (dryRun) {
|
|
595
|
+
log(` [dry-run] copy ${client.skillFrom} -> ${client.skillTo}`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
505
598
|
fs.rmSync(client.skillTo, { recursive: true, force: true });
|
|
506
599
|
copyTree(client.skillFrom, client.skillTo);
|
|
507
600
|
log(` skill: ${client.skillTo}`);
|
|
@@ -511,13 +604,17 @@ function installClientDirectly(client, bin) { step(`Registering with ${client.l
|
|
|
511
604
|
// severally@severally) loads that copy, and a skills-dir copy or user-scope
|
|
512
605
|
// registration from this installer would load beside it as a second server
|
|
513
606
|
// and skill (verified with claude 2.1.302). Claude Code is left to it.
|
|
607
|
+
// Returns the plugin's id (severally@<marketplace>), or false.
|
|
514
608
|
function claudeHasMarketplacePlugin() {
|
|
515
609
|
if (!which('claude')) return false;
|
|
516
610
|
const r = tryRun('claude', ['plugin', 'list', '--json'], { real: true });
|
|
517
611
|
try {
|
|
518
|
-
return r.ok && JSON.parse(r.out).
|
|
519
|
-
} catch {
|
|
612
|
+
return (r.ok && JSON.parse(r.out).find((p) => /^severally@/.test(p.id) && p.id !== 'severally@skills-dir')?.id) || false;
|
|
613
|
+
} catch {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
520
616
|
}
|
|
617
|
+
const claudeMarketplace = claudeHasMarketplacePlugin();
|
|
521
618
|
|
|
522
619
|
function leaveClaudeToMarketplace() {
|
|
523
620
|
step('Claude Code has severally from the marketplace; leaving Claude Code to it');
|
|
@@ -532,9 +629,28 @@ function leaveClaudeToMarketplace() {
|
|
|
532
629
|
const r = tryRun('claude', ['mcp', 'remove', '--scope', 'user', 'severally']);
|
|
533
630
|
log(r.ok ? ' removed the user-scope MCP registration' : ` could not remove the user-scope registration: ${r.out}`);
|
|
534
631
|
}
|
|
535
|
-
|
|
632
|
+
// Brought to this release too, so one install run updates every copy.
|
|
633
|
+
const updated =
|
|
634
|
+
tryRun('claude', ['plugin', 'marketplace', 'update', 'severally'], { timeout: 120_000 }).ok &&
|
|
635
|
+
tryRun('claude', ['plugin', 'update', claudeMarketplace], { timeout: 120_000 }).ok;
|
|
636
|
+
log(
|
|
637
|
+
updated
|
|
638
|
+
? ` updated ${claudeMarketplace} from its marketplace`
|
|
639
|
+
: ' could not update it here; in Claude Code run /plugin marketplace update severally',
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// A previous plugin installation would otherwise retain its old checkout
|
|
644
|
+
// marketplace alongside a new direct server registration.
|
|
645
|
+
function removeCodexLocalMarketplace() {
|
|
646
|
+
if (!which('codex')) return;
|
|
647
|
+
const known = tryRun('codex', ['plugin', 'marketplace', 'list'], { real: true });
|
|
648
|
+
if (known.ok && known.out.includes('severally-local')) {
|
|
649
|
+
backup(path.join(codexHome, 'config.toml'));
|
|
650
|
+
tryRun('codex', ['plugin', 'remove', 'severally', '--marketplace', 'severally-local']);
|
|
651
|
+
run('codex', ['plugin', 'marketplace', 'remove', 'severally-local']);
|
|
652
|
+
}
|
|
536
653
|
}
|
|
537
|
-
const claudeMarketplace = claudeHasMarketplacePlugin();
|
|
538
654
|
|
|
539
655
|
if (mode === 'plugin') {
|
|
540
656
|
// ------------------------------------------------------------- plugin mode
|
|
@@ -558,41 +674,54 @@ if (mode === 'plugin') {
|
|
|
558
674
|
if (dup.ok) {
|
|
559
675
|
backup(path.join(home, '.claude.json'));
|
|
560
676
|
const r = tryRun('claude', ['mcp', 'remove', '--scope', 'user', 'severally']);
|
|
561
|
-
log(
|
|
677
|
+
log(
|
|
678
|
+
r.ok
|
|
679
|
+
? ' removed the duplicate user-scope MCP registration'
|
|
680
|
+
: ` could not remove the duplicate registration: ${r.out}`,
|
|
681
|
+
);
|
|
562
682
|
}
|
|
563
683
|
}
|
|
564
684
|
}
|
|
565
685
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
686
|
+
if (codexDirect && which('codex')) {
|
|
687
|
+
removeCodexLocalMarketplace();
|
|
688
|
+
installClientDirectly(codexClient, serverCommand);
|
|
569
689
|
} else {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
690
|
+
step('Installing the plugin into Codex (managed local marketplace)');
|
|
691
|
+
if (!which('codex')) {
|
|
692
|
+
log(' codex CLI not found on PATH — skipped');
|
|
693
|
+
} else {
|
|
694
|
+
backup(path.join(codexHome, 'config.toml'));
|
|
695
|
+
const marketplace = 'severally-local';
|
|
696
|
+
const known = tryRun('codex', ['plugin', 'marketplace', 'list'], { real: true });
|
|
697
|
+
if (!known.ok) throw new Error(`Could not inspect Codex marketplaces: ${known.out}`);
|
|
698
|
+
// Refresh our own registration even when already present: older installs
|
|
699
|
+
// registered the checkout here. Reusing it would retain that dependency.
|
|
700
|
+
if (known.out.includes(marketplace)) {
|
|
701
|
+
tryRun('codex', ['plugin', 'remove', 'severally', '--marketplace', marketplace]);
|
|
702
|
+
run('codex', ['plugin', 'marketplace', 'remove', marketplace]);
|
|
703
|
+
}
|
|
704
|
+
run('codex', ['plugin', 'marketplace', 'add', marketplaceRoot]);
|
|
705
|
+
log(` marketplace added: ${marketplace} -> ${marketplaceRoot}`);
|
|
706
|
+
const r = tryRun('codex', ['plugin', 'add', `severally@${marketplace}`]);
|
|
707
|
+
log(r.ok ? ' plugin installed' : ` plugin install failed: ${r.out}`);
|
|
708
|
+
if (!r.ok) process.exitCode = 1;
|
|
709
|
+
// Same duplication concern on the Codex side.
|
|
710
|
+
const dup = tryRun('codex', ['mcp', 'get', 'severally'], { real: true });
|
|
711
|
+
if (dup.ok) {
|
|
712
|
+
const rm = tryRun('codex', ['mcp', 'remove', 'severally']);
|
|
713
|
+
log(
|
|
714
|
+
rm.ok
|
|
715
|
+
? ' removed the duplicate global MCP registration'
|
|
716
|
+
: ` could not remove the duplicate registration: ${rm.out}`,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
const strayCodexSkill = path.join(codexHome, 'skills', 'severally');
|
|
720
|
+
if (fs.existsSync(strayCodexSkill)) {
|
|
721
|
+
const s2 = backupTree(strayCodexSkill);
|
|
722
|
+
if (!dryRun) fs.rmSync(strayCodexSkill, { recursive: true, force: true });
|
|
723
|
+
log(` removed the hand-copied skill (the plugin supplies it); backup: ${s2}`);
|
|
724
|
+
}
|
|
596
725
|
}
|
|
597
726
|
}
|
|
598
727
|
|
|
@@ -605,22 +734,62 @@ if (mode === 'plugin') {
|
|
|
605
734
|
installClientDirectly(opencodeClient, serverCommand);
|
|
606
735
|
} else {
|
|
607
736
|
// ------------------------------------------------------------- manual mode
|
|
608
|
-
|
|
609
|
-
// marketplace alongside the new direct server registration.
|
|
610
|
-
if (which('codex')) {
|
|
611
|
-
const known = tryRun('codex', ['plugin', 'marketplace', 'list'], { real: true });
|
|
612
|
-
if (known.ok && known.out.includes('severally-local')) {
|
|
613
|
-
backup(path.join(codexHome, 'config.toml'));
|
|
614
|
-
tryRun('codex', ['plugin', 'remove', 'severally', '--marketplace', 'severally-local']);
|
|
615
|
-
run('codex', ['plugin', 'marketplace', 'remove', 'severally-local']);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
737
|
+
removeCodexLocalMarketplace();
|
|
618
738
|
for (const client of CLIENTS) {
|
|
619
739
|
if (client.bin === 'claude' && claudeMarketplace) leaveClaudeToMarketplace();
|
|
620
740
|
else installClientDirectly(client, serverCommand);
|
|
621
741
|
}
|
|
622
742
|
}
|
|
623
743
|
|
|
744
|
+
// ---------------------------------------------------------------- local tools
|
|
745
|
+
// Five of the six tools stay on this machine; Claude Code asks before each
|
|
746
|
+
// call unless they are allowed, so a first consultation can stop on prompt
|
|
747
|
+
// after prompt. Offered here, never assumed: consult_start, which sends the
|
|
748
|
+
// brief out, keeps asking.
|
|
749
|
+
const LOCAL_TOOLS = ['consult_get', 'consult_list', 'consult_cancel', 'consult_record', 'consult_export'];
|
|
750
|
+
async function allowLocalTools() {
|
|
751
|
+
if (!which('claude')) return;
|
|
752
|
+
const prefix = mode === 'plugin' || claudeMarketplace ? 'mcp__plugin_severally_severally__' : 'mcp__severally__';
|
|
753
|
+
const settingsFile = path.join(home, '.claude', 'settings.json');
|
|
754
|
+
let settings = {};
|
|
755
|
+
try {
|
|
756
|
+
settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
757
|
+
} catch (err) {
|
|
758
|
+
if (err.code !== 'ENOENT') {
|
|
759
|
+
log(` could not read ${settingsFile} (${err.message}); left as it is`);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const allow = settings.permissions?.allow ?? [];
|
|
764
|
+
const missing = LOCAL_TOOLS.map((t) => prefix + t).filter((t) => !allow.includes(t));
|
|
765
|
+
if (!missing.length) return;
|
|
766
|
+
step('Claude Code tool permissions');
|
|
767
|
+
let yes = argv.has('--allow-local-tools');
|
|
768
|
+
if (!yes && process.stdin.isTTY && process.stdout.isTTY && !dryRun) {
|
|
769
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
770
|
+
const answer = await rl.question(
|
|
771
|
+
` Let Claude Code run ${LOCAL_TOOLS.join(', ')} without asking each time?\n` +
|
|
772
|
+
' They send nothing off this machine; consult_start, which does, still asks. [Y/n] ',
|
|
773
|
+
);
|
|
774
|
+
rl.close();
|
|
775
|
+
yes = !/^\s*n/i.test(answer);
|
|
776
|
+
}
|
|
777
|
+
if (!yes) {
|
|
778
|
+
log(` not changed; rerun with --allow-local-tools to allow them in ${settingsFile}`);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (dryRun) {
|
|
782
|
+
log(` [dry-run] add ${missing.join(', ')} to permissions.allow in ${settingsFile}`);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
backup(settingsFile);
|
|
786
|
+
settings.permissions = { ...settings.permissions, allow: [...allow, ...missing] };
|
|
787
|
+
fs.mkdirSync(path.dirname(settingsFile), { recursive: true });
|
|
788
|
+
fs.writeFileSync(settingsFile, `${JSON.stringify(settings, null, 2)}\n`);
|
|
789
|
+
log(` allowed in ${settingsFile}: ${missing.join(', ')}`);
|
|
790
|
+
}
|
|
791
|
+
await allowLocalTools();
|
|
792
|
+
|
|
624
793
|
// ---------------------------------------------------------------- summary
|
|
625
794
|
step('Done');
|
|
626
795
|
if (fs.existsSync(backupDir)) log(` backups: ${backupDir}`);
|
|
@@ -629,10 +798,12 @@ if (keptRegistrations) {
|
|
|
629
798
|
} else if (!dryRun && !process.exitCode) {
|
|
630
799
|
log(' The registered server and skills no longer depend on the source checkout.');
|
|
631
800
|
}
|
|
632
|
-
log(
|
|
801
|
+
log(
|
|
802
|
+
mode === 'plugin'
|
|
803
|
+
? `
|
|
633
804
|
Verify with:
|
|
634
805
|
claude plugin details severally
|
|
635
|
-
codex plugin list
|
|
806
|
+
${codexDirect ? 'codex mcp get severally' : 'codex plugin list'}
|
|
636
807
|
agy mcp list
|
|
637
808
|
opencode mcp list
|
|
638
809
|
|
|
@@ -640,7 +811,8 @@ Restart any running client session to pick the plugin up. In Claude Code the too
|
|
|
640
811
|
mcp__plugin_severally_severally__consult_start / _get / _cancel / _list.
|
|
641
812
|
|
|
642
813
|
Re-run this after "npm run build" to push an updated plugin to ${claudeMarketplace ? 'Codex' : 'both clients'} (Antigravity and
|
|
643
|
-
OpenCode are registered directly each run, so a re-run always refreshes them too).`
|
|
814
|
+
OpenCode are registered directly each run, so a re-run always refreshes them too).`
|
|
815
|
+
: `
|
|
644
816
|
Verify with:
|
|
645
817
|
claude mcp get severally
|
|
646
818
|
codex mcp get severally
|
|
@@ -648,4 +820,5 @@ Verify with:
|
|
|
648
820
|
opencode mcp list
|
|
649
821
|
|
|
650
822
|
In a new Claude Code session the tools appear as mcp__severally__consult_start / _get / _cancel / _list.
|
|
651
|
-
Restart any running client session to pick the server up
|
|
823
|
+
Restart any running client session to pick the server up.`,
|
|
824
|
+
);
|