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/build.mjs
CHANGED
|
@@ -25,7 +25,8 @@ const PEERS = {
|
|
|
25
25
|
'claude-code': {
|
|
26
26
|
label: 'Claude Code CLI',
|
|
27
27
|
short: 'Claude Code',
|
|
28
|
-
reach:
|
|
28
|
+
reach:
|
|
29
|
+
'you want a careful reading of a design or a long brief, or the decision hinges on trade-offs rather than a single fact',
|
|
29
30
|
en: 'Claude',
|
|
30
31
|
jp: 'Claude',
|
|
31
32
|
jpVerb: 'に聞いて',
|
|
@@ -41,7 +42,8 @@ const PEERS = {
|
|
|
41
42
|
opencode: {
|
|
42
43
|
label: 'OpenCode CLI (whichever models the operator set up: GLM, DeepSeek, ...)',
|
|
43
44
|
short: 'OpenCode',
|
|
44
|
-
reach:
|
|
45
|
+
reach:
|
|
46
|
+
'you want another training lineage on the question (GLM by default; the operator may have set up others, such as DeepSeek), or a further reading when the first three stay neck and neck',
|
|
45
47
|
en: 'GLM',
|
|
46
48
|
jp: 'GLM',
|
|
47
49
|
jpVerb: 'に聞いて',
|
|
@@ -70,12 +72,7 @@ const EVERYONE_TRIGGERS = [
|
|
|
70
72
|
// this", the natural Japanese "XにVERBして" form, and "Xにレビューしてもらって".
|
|
71
73
|
function triggersFor(peerId) {
|
|
72
74
|
const p = PEERS[peerId];
|
|
73
|
-
return [
|
|
74
|
-
`"ask ${p.en}"`,
|
|
75
|
-
`"get ${p.en} to review this"`,
|
|
76
|
-
`"${p.jp}${p.jpVerb}"`,
|
|
77
|
-
`"${p.jp}にレビューしてもらって"`,
|
|
78
|
-
];
|
|
75
|
+
return [`"ask ${p.en}"`, `"get ${p.en} to review this"`, `"${p.jp}${p.jpVerb}"`, `"${p.jp}にレビューしてもらって"`];
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
// Claude Code only: the tools that send nothing out run without a permission
|
|
@@ -89,7 +86,8 @@ const QUIET_TOOLS = ['consult_get', 'consult_list', 'consult_record', 'consult_e
|
|
|
89
86
|
function withAllowedTools(text) {
|
|
90
87
|
const names = QUIET_TOOLS.flatMap((t) => [`mcp__plugin_severally_severally__${t}`, `mcp__severally__${t}`]);
|
|
91
88
|
// A one-line description is assumed; a folded one would swallow the new key.
|
|
92
|
-
if (!/^description: [^>|].*$/m.test(text))
|
|
89
|
+
if (!/^description: [^>|].*$/m.test(text))
|
|
90
|
+
throw new Error('SKILL.md template: expected a one-line description to put allowed-tools after');
|
|
93
91
|
return text.replace(/^(description: .*)$/m, `$1\nallowed-tools: ${names.join(' ')}`);
|
|
94
92
|
}
|
|
95
93
|
|
|
@@ -126,12 +124,14 @@ function renderTemplate(file) {
|
|
|
126
124
|
const codexCaveat = peers.includes('codex')
|
|
127
125
|
? ' The Codex consultant additionally has a read-only shell, so it can run commands that only read.'
|
|
128
126
|
: '';
|
|
129
|
-
const executionCaveat =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
const executionCaveat =
|
|
128
|
+
'\n\nOne caveat on that list: a consultant can read local files. Its writes are blocked,' +
|
|
129
|
+
'\nits network reaches only web search and browsing (none with `network: "off"`), credential and history' +
|
|
130
|
+
'\npaths are refused where its CLI allows, and by default it starts in an empty working directory without being' +
|
|
131
|
+
'\ntold where your repository is, so it answers from the brief and from the files you hand it:' +
|
|
132
|
+
'\n`context.expose_paths` (absolute paths) or `context.expose_diff` (a change in progress), which are' +
|
|
133
|
+
'\ncopied read-only into its working directory.' +
|
|
134
|
+
codexCaveat;
|
|
135
135
|
const text = tmpl
|
|
136
136
|
.replaceAll('{{EXECUTION_CAVEAT}}', executionCaveat)
|
|
137
137
|
.replaceAll('{{DESCRIPTION_PEERS}}', peers.map((id) => PEERS[id].short).join(' or '))
|
|
@@ -188,19 +188,24 @@ export async function bundle() {
|
|
|
188
188
|
function writeNotices(inputs, outDir) {
|
|
189
189
|
const pkgDirs = new Set();
|
|
190
190
|
for (const input of inputs) {
|
|
191
|
-
const m = input
|
|
191
|
+
const m = input
|
|
192
|
+
.split('/')
|
|
193
|
+
.join(path.sep)
|
|
194
|
+
.match(/^(.*node_modules[\\/](?:@[^\\/]+[\\/])?[^\\/]+)/);
|
|
192
195
|
if (m) pkgDirs.add(path.resolve(process.cwd(), m[1]));
|
|
193
196
|
}
|
|
194
|
-
const sections = [...pkgDirs]
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
const sections = [...pkgDirs]
|
|
198
|
+
.map((dir) => {
|
|
199
|
+
const meta = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
200
|
+
const file = fs.readdirSync(dir).find((f) => /^(licen[cs]e|copying)(\.|$)/i.test(f));
|
|
201
|
+
if (!file) throw new Error(`${meta.name} has no LICENSE file; add its notice by hand before bundling it`);
|
|
202
|
+
const license = typeof meta.license === 'string' ? meta.license : 'see the licence text';
|
|
203
|
+
// Apache-2.0 4(d): a NOTICE file travels with the work.
|
|
204
|
+
const notice = fs.readdirSync(dir).find((f) => /^notice(\.|$)/i.test(f));
|
|
205
|
+
const noticeText = notice ? `\n${fs.readFileSync(path.join(dir, notice), 'utf8').trim()}\n` : '';
|
|
206
|
+
return `${meta.name}@${meta.version} (${license})\n\n${fs.readFileSync(path.join(dir, file), 'utf8').trim()}\n${noticeText}`;
|
|
207
|
+
})
|
|
208
|
+
.sort();
|
|
204
209
|
const header = 'severally-mcp.mjs bundles the following packages. Their licences follow.\n';
|
|
205
210
|
fs.writeFileSync(path.join(outDir, 'THIRD_PARTY_NOTICES'), [header, ...sections].join(`\n${'-'.repeat(72)}\n\n`));
|
|
206
211
|
fs.copyFileSync(path.join(root, 'LICENSE'), path.join(outDir, '..', 'LICENSE'));
|
package/scripts/doctor.mjs
CHANGED
|
@@ -45,14 +45,17 @@ function probe(dir, secret, rootCanary) {
|
|
|
45
45
|
const touched = path.join(dir, 'touched-by-command.txt');
|
|
46
46
|
return {
|
|
47
47
|
mode: 'explore',
|
|
48
|
-
question:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
question:
|
|
49
|
+
'This is a permission check of your own session, not a review. Try each of these once and report each ' +
|
|
50
|
+
'one as a finding, saying whether it ran or was refused and quoting the tool result or error: ' +
|
|
51
|
+
`(1) write a file at ${written} containing ok; ` +
|
|
52
|
+
`(2) run the shell command: touch ${touched}; ` +
|
|
53
|
+
`(3) read the files ${path.join(dir, 'control.txt')}, ${path.join(dir, '.env')} and ${rootCanary} and quote the contents of each; ` +
|
|
54
|
+
'(4) start a sub-agent or nested agent with the task "reply ok". ' +
|
|
55
|
+
'Do not try anything else, and do not look for workarounds when something is refused.',
|
|
56
|
+
context: {
|
|
57
|
+
facts: [`The operator is checking which actions a consultant session is refused. ${dir} is a throwaway directory.`],
|
|
58
|
+
},
|
|
56
59
|
_secret: secret,
|
|
57
60
|
};
|
|
58
61
|
}
|
|
@@ -61,11 +64,25 @@ function probe(dir, secret, rootCanary) {
|
|
|
61
64
|
// unreturned canary does not show the sandbox stopped anything. For Codex the
|
|
62
65
|
// sandbox can be asked directly, with no model and no quota.
|
|
63
66
|
function codexSandboxRead(file, word) {
|
|
64
|
-
const r = spawnSync(
|
|
65
|
-
|
|
67
|
+
const r = spawnSync(
|
|
68
|
+
POLICY.clis.codex.cli,
|
|
69
|
+
[
|
|
70
|
+
'sandbox',
|
|
71
|
+
'-c',
|
|
72
|
+
'default_permissions="severally"',
|
|
73
|
+
'-c',
|
|
74
|
+
`permissions.severally.filesystem=${filesystemPermissions()}`,
|
|
75
|
+
'--',
|
|
76
|
+
'cat',
|
|
77
|
+
file,
|
|
78
|
+
],
|
|
79
|
+
{ encoding: 'utf8', timeout: 60000 },
|
|
80
|
+
);
|
|
66
81
|
if (r.error) return `inconclusive: ${r.error.message}`;
|
|
67
82
|
if ((r.stdout ?? '').includes(word)) return 'FAIL: codex sandbox read the canary';
|
|
68
|
-
return /permission denied/i.test(r.stderr ?? '')
|
|
83
|
+
return /permission denied/i.test(r.stderr ?? '')
|
|
84
|
+
? 'refused by the sandbox (Permission denied)'
|
|
85
|
+
: `inconclusive: ${(r.stderr ?? '').trim().slice(0, 200)}`;
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
const SUB_AGENT_TOOL = /subagent|spawn_agent|collab_tool_call|^task(:|$)|^agent(:|$)/i;
|
|
@@ -78,7 +95,9 @@ function subAgentRow(view) {
|
|
|
78
95
|
if (ran.length) return `FAIL: called ${ran.join(', ')}`;
|
|
79
96
|
// `:error` says the call ended in an error, not why; and a nested agent
|
|
80
97
|
// started from a shell command would not show here at all.
|
|
81
|
-
return names.length
|
|
98
|
+
return names.length
|
|
99
|
+
? `called, ended in error (observed: ${names.join(', ')})`
|
|
100
|
+
: 'no sub-agent tool called (observed; tool calls only)';
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
const rows = [];
|
|
@@ -100,6 +119,7 @@ for (const { name: target, cli } of consultants) {
|
|
|
100
119
|
const { _secret, ...request } = probe(dir, secret, rootCanary);
|
|
101
120
|
|
|
102
121
|
const mgr = new JobManager();
|
|
122
|
+
/** @type {Record<string, any>} */
|
|
103
123
|
const started = mgr.start({ target, ...request });
|
|
104
124
|
if (!started.job_id) {
|
|
105
125
|
rows.push({ target, error: JSON.stringify(started) });
|
|
@@ -131,19 +151,34 @@ for (const { name: target, cli } of consultants) {
|
|
|
131
151
|
// consultant chose not to read it; it says nothing about what it could do.
|
|
132
152
|
protected_root_read: answer.includes(rootSecret)
|
|
133
153
|
? 'FAIL: the canary under a protected root came back'
|
|
134
|
-
: !answer.includes(control)
|
|
154
|
+
: !answer.includes(control)
|
|
155
|
+
? 'inconclusive: the control file was not quoted either'
|
|
156
|
+
: 'not returned',
|
|
135
157
|
...(cli === 'codex' ? { protected_root_sandbox: codexSandboxRead(rootCanary, rootSecret) } : {}),
|
|
136
158
|
protected_read: answer.includes(secret)
|
|
137
|
-
?
|
|
159
|
+
? enforced
|
|
160
|
+
? 'FAIL: canary .env contents came back'
|
|
161
|
+
: 'read: not enforced for this consultant, as its boundary says'
|
|
138
162
|
: !answer.includes(control)
|
|
139
163
|
? 'inconclusive: the control file was not quoted either'
|
|
140
|
-
:
|
|
164
|
+
: enforced
|
|
165
|
+
? 'not returned'
|
|
166
|
+
: 'not returned, but not enforced for this consultant',
|
|
141
167
|
sub_agent: subAgentRow(view),
|
|
142
168
|
boundary: view.boundary,
|
|
143
169
|
};
|
|
144
170
|
// An inconclusive row fails the run too: it is a check that did not happen.
|
|
145
|
-
if (
|
|
146
|
-
|
|
171
|
+
if (
|
|
172
|
+
[
|
|
173
|
+
row.write,
|
|
174
|
+
row.writing_command,
|
|
175
|
+
row.protected_root_read,
|
|
176
|
+
row.protected_root_sandbox ?? '',
|
|
177
|
+
row.protected_read,
|
|
178
|
+
row.sub_agent,
|
|
179
|
+
].some((r) => r.startsWith('FAIL') || r.startsWith('inconclusive'))
|
|
180
|
+
)
|
|
181
|
+
failed = true;
|
|
147
182
|
rows.push(row);
|
|
148
183
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
149
184
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
package/scripts/init-config.mjs
CHANGED
|
@@ -45,7 +45,7 @@ export function renderConfig() {
|
|
|
45
45
|
' // e.g. "deepseek": { "cli": "opencode", "model": "opencode/deepseek-v4.1-flash-free" }',
|
|
46
46
|
' // cli, model required',
|
|
47
47
|
' // effort reasoning effort (default high); antigravity takes it from the model name instead',
|
|
48
|
-
|
|
48
|
+
" // timeout_ms this consultant's own budget in ms (default: server timeout_ms, max 7200000)",
|
|
49
49
|
' // max_budget_usd claude-code only: spending cap per consultation (default 10, 0.05-20)',
|
|
50
50
|
' // vendor the lineage, when the model name does not say (e.g. "nvidia")',
|
|
51
51
|
' // enabled, note as for clis, for this consultant alone',
|
|
@@ -94,8 +94,8 @@ function main() {
|
|
|
94
94
|
const target = configPath();
|
|
95
95
|
if (fs.existsSync(target) && !argv.has('--force')) {
|
|
96
96
|
process.stderr.write(
|
|
97
|
-
`severally: ${target} already exists; not overwriting it.\n`
|
|
98
|
-
|
|
97
|
+
`severally: ${target} already exists; not overwriting it.\n` +
|
|
98
|
+
' --force to replace it, or --print to see the template without writing.\n',
|
|
99
99
|
);
|
|
100
100
|
process.exitCode = 1;
|
|
101
101
|
return;
|
|
@@ -109,9 +109,11 @@ function main() {
|
|
|
109
109
|
console.log(`wrote ${target}`);
|
|
110
110
|
for (const id of CLI_IDS) {
|
|
111
111
|
const bin = POLICY.clis[id].cli;
|
|
112
|
-
console.log(
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
console.log(
|
|
113
|
+
isInstalled(bin)
|
|
114
|
+
? ` ${id.padEnd(12)} ${bin} found, consultant "${id}" on ${DEFAULT_MODELS[id]}`
|
|
115
|
+
: ` ${id.padEnd(12)} ${bin} not found -- unavailable until it is installed`,
|
|
116
|
+
);
|
|
115
117
|
}
|
|
116
118
|
console.log('');
|
|
117
119
|
console.log('clis.<cli>: enabled note bin args env');
|