severally-mcp 2.3.1 → 2.4.2
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 +52 -17
- package/README.md +52 -16
- package/bin/severally-mcp.mjs +14 -2
- package/docs/install.ja.md +48 -20
- package/docs/install.md +48 -21
- package/package.json +15 -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 +6573 -5978
- package/plugins/severally/skills/_template/reference.md.tmpl +2 -2
- package/plugins/severally/skills/antigravity/severally/reference.md +2 -2
- package/plugins/severally/skills/claude/severally/reference.md +2 -2
- package/plugins/severally/skills/codex/severally/reference.md +2 -2
- package/plugins/severally/skills/opencode/severally/reference.md +2 -2
- package/scripts/build.mjs +76 -68
- package/scripts/doctor.mjs +66 -20
- package/scripts/init-config.mjs +8 -6
- package/scripts/install.mjs +327 -90
- package/scripts/live-check.mjs +62 -23
- package/scripts/live-mcp-check.mjs +52 -16
- package/scripts/test.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +62 -28
- package/src/adapters/antigravity.mjs +111 -30
- package/src/adapters/claude-code.mjs +121 -28
- package/src/adapters/codex.mjs +125 -33
- package/src/adapters/opencode-sandbox.mjs +59 -22
- package/src/adapters/opencode.mjs +144 -30
- package/src/brief.mjs +148 -47
- package/src/consultants.mjs +255 -48
- package/src/cross-spawn-escape.d.ts +7 -0
- package/src/export.mjs +99 -18
- package/src/expose-paths.mjs +310 -50
- package/src/failures.mjs +31 -14
- package/src/jobs.mjs +863 -249
- package/src/jsonc.mjs +11 -3
- package/src/parse-result.mjs +172 -40
- package/src/platform.mjs +56 -8
- package/src/policy.mjs +245 -52
- package/src/redact.mjs +59 -16
- package/src/result-schema.mjs +13 -0
- package/src/run.mjs +225 -30
- package/src/schema.mjs +132 -27
- package/src/server.mjs +178 -81
- package/src/status.mjs +94 -13
- package/src/store.mjs +418 -35
package/src/schema.mjs
CHANGED
|
@@ -5,12 +5,27 @@
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import {
|
|
8
|
-
POLICY,
|
|
9
|
-
|
|
8
|
+
POLICY,
|
|
9
|
+
CALLER_INPUTS,
|
|
10
|
+
MODES,
|
|
11
|
+
STANCES,
|
|
12
|
+
artifactKinds,
|
|
13
|
+
resolveCaller,
|
|
14
|
+
availableConsultants,
|
|
15
|
+
consultantAvailable,
|
|
16
|
+
unavailableReason,
|
|
17
|
+
brokenConsultant,
|
|
10
18
|
} from './policy.mjs';
|
|
11
19
|
import { normalizeName, resolveConsultant } from './consultants.mjs';
|
|
12
20
|
|
|
21
|
+
/** @typedef {import('./consultants.mjs').Consultant} Consultant */
|
|
22
|
+
/** @typedef {import('./consultants.mjs').CliId} CliId */
|
|
23
|
+
|
|
13
24
|
const L = POLICY.input;
|
|
25
|
+
/**
|
|
26
|
+
* @param {number} max
|
|
27
|
+
* @param {string} label
|
|
28
|
+
*/
|
|
14
29
|
const trimmed = (max, label) =>
|
|
15
30
|
z.string().trim().min(1, `${label} must not be empty`).max(max, `${label} exceeds ${max} characters`);
|
|
16
31
|
|
|
@@ -36,7 +51,11 @@ const exposedPathSchema = z
|
|
|
36
51
|
.refine((p) => path.isAbsolute(p), { message: 'context.expose_paths[] must be an absolute path' });
|
|
37
52
|
|
|
38
53
|
// A revision for `git diff`: a ref, a commit, or HEAD~n -- never an option.
|
|
39
|
-
const gitRevision = z
|
|
54
|
+
const gitRevision = z
|
|
55
|
+
.string()
|
|
56
|
+
.trim()
|
|
57
|
+
.min(1)
|
|
58
|
+
.max(200)
|
|
40
59
|
.regex(/^[A-Za-z0-9._/~^@{}-]+$/, 'context.expose_diff.base must be a git revision (a branch, tag, commit or HEAD~n)')
|
|
41
60
|
.refine((r) => !r.startsWith('-'), { message: 'context.expose_diff.base must not start with "-"' });
|
|
42
61
|
|
|
@@ -65,7 +84,11 @@ export const contextSchema = z
|
|
|
65
84
|
// for it, its CLI, its vendor, or its model ("glm", "claude opus"). Which
|
|
66
85
|
// consultant that is depends on the operator's config, so it is resolved in
|
|
67
86
|
// parseRequest, not by an enum here.
|
|
68
|
-
const targetSpec = z
|
|
87
|
+
const targetSpec = z
|
|
88
|
+
.string()
|
|
89
|
+
.trim()
|
|
90
|
+
.min(1, 'target must not be empty')
|
|
91
|
+
.max(120, 'target exceeds 120 characters')
|
|
69
92
|
.regex(/^[^\p{Cc}\p{Cf}]+$/u, 'target must be a single line of printable characters');
|
|
70
93
|
|
|
71
94
|
// Who asked for a consultation, as the lead reports it.
|
|
@@ -128,10 +151,59 @@ export const requestSchema = z
|
|
|
128
151
|
})
|
|
129
152
|
.strict();
|
|
130
153
|
|
|
154
|
+
/**
|
|
155
|
+
* What charCount reads of a request, once parseRequest has filled in its defaults.
|
|
156
|
+
* @typedef {{
|
|
157
|
+
* question: string,
|
|
158
|
+
* objective?: string | null | undefined,
|
|
159
|
+
* constraints?: string[] | undefined,
|
|
160
|
+
* success_criteria?: string[] | undefined,
|
|
161
|
+
* context: { facts: string[], counterpoints: string[], proposal?: string | null | undefined, artifacts: { name: string, excerpt: string }[] },
|
|
162
|
+
* }} CountedRequest
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
/** @typedef {'explore' | 'review' | 'debate'} Mode */
|
|
166
|
+
/** @typedef {z.output<typeof requestSchema>} ValidRequest */
|
|
167
|
+
/** @typedef {z.output<typeof contextSchema>} ValidContext */
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A request as parseRequest returns it: validated, defaults filled in, the
|
|
171
|
+
* consultant(s) resolved. `target` is the first of `targets`.
|
|
172
|
+
* @typedef {Omit<
|
|
173
|
+
* ValidRequest,
|
|
174
|
+
* 'target' | 'targets' | 'caller' | 'caller_model' | 'initiator' | 'followup_to' | 'mode' | 'network' | 'success_criteria' | 'constraints' | 'context'
|
|
175
|
+
* > & {
|
|
176
|
+
* target: string,
|
|
177
|
+
* targets: string[],
|
|
178
|
+
* fanout: boolean,
|
|
179
|
+
* caller: CliId | null,
|
|
180
|
+
* caller_model: string | null,
|
|
181
|
+
* initiator: string | null,
|
|
182
|
+
* followup_to: string | null,
|
|
183
|
+
* mode: Mode,
|
|
184
|
+
* network: 'on' | 'off',
|
|
185
|
+
* success_criteria: string[],
|
|
186
|
+
* constraints: string[],
|
|
187
|
+
* context: Omit<ValidContext, 'constraints' | 'success_criteria' | 'facts' | 'counterpoints' | 'artifacts' | 'proposal'> & {
|
|
188
|
+
* facts: string[],
|
|
189
|
+
* counterpoints: string[],
|
|
190
|
+
* artifacts: ValidContext['artifacts'] & {},
|
|
191
|
+
* proposal: string | null,
|
|
192
|
+
* },
|
|
193
|
+
* }} ParsedRequest
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* parseRequest's working copy: the request as it is until the defaults are filled in.
|
|
198
|
+
* `constraints` and `success_criteria` may still sit inside the context at first.
|
|
199
|
+
* @typedef {ParsedRequest & { context: { constraints?: string[] | undefined, success_criteria?: string[] | undefined } }} DraftRequest
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
/** @param {CountedRequest} req */
|
|
131
203
|
export function charCount(req) {
|
|
132
204
|
let n = req.question.length + (req.objective?.length ?? 0);
|
|
133
|
-
for (const c of req.constraints) n += c.length;
|
|
134
|
-
for (const c of req.success_criteria) n += c.length;
|
|
205
|
+
for (const c of req.constraints ?? []) n += c.length;
|
|
206
|
+
for (const c of req.success_criteria ?? []) n += c.length;
|
|
135
207
|
for (const f of req.context.facts) n += f.length;
|
|
136
208
|
for (const c of req.context.counterpoints) n += c.length;
|
|
137
209
|
if (req.context.proposal) n += req.context.proposal.length;
|
|
@@ -140,6 +212,11 @@ export function charCount(req) {
|
|
|
140
212
|
}
|
|
141
213
|
|
|
142
214
|
export class RequestError extends Error {
|
|
215
|
+
/**
|
|
216
|
+
* @param {string} message
|
|
217
|
+
* @param {string} [code]
|
|
218
|
+
* @param {unknown[]} [details]
|
|
219
|
+
*/
|
|
143
220
|
constructor(message, code = 'invalid_request', details = undefined) {
|
|
144
221
|
super(message);
|
|
145
222
|
this.name = 'RequestError';
|
|
@@ -152,6 +229,9 @@ export class RequestError extends Error {
|
|
|
152
229
|
* Validate + normalise a consult request.
|
|
153
230
|
* `isFollowup` is decided by the caller (the job store resolves followup_to first);
|
|
154
231
|
* `groupFollowup` when followup_to named a fan-out, whose members are the targets.
|
|
232
|
+
* @param {unknown} raw
|
|
233
|
+
* @param {{ isFollowup?: boolean, parentMode?: Mode | null, groupFollowup?: boolean }} [options]
|
|
234
|
+
* @returns {ParsedRequest}
|
|
155
235
|
*/
|
|
156
236
|
export function parseRequest(raw, { isFollowup = false, parentMode = null, groupFollowup = false } = {}) {
|
|
157
237
|
const parsed = requestSchema.safeParse(raw);
|
|
@@ -159,46 +239,62 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
159
239
|
const issues = parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`);
|
|
160
240
|
throw new RequestError(`request failed validation: ${issues.join('; ')}`, 'invalid_request', issues);
|
|
161
241
|
}
|
|
162
|
-
|
|
242
|
+
// Filled in below with what the request resolves to (targets, caller, ...), so
|
|
243
|
+
// until it returns it is only partly a ParsedRequest: the validated data is
|
|
244
|
+
// typed as the finished shape and every field the shape adds is set before the return.
|
|
245
|
+
const req = /** @type {DraftRequest} */ (parsed.data);
|
|
163
246
|
// Left without a default in the schema, so that a follow-up can tell a list
|
|
164
247
|
// it left out (inherited from the round before, jobs.mjs) from one it
|
|
165
248
|
// emptied on purpose: the tool layer applies the schema's defaults first.
|
|
166
|
-
|
|
167
|
-
|
|
249
|
+
/** @param {'constraints' | 'success_criteria'} k */
|
|
250
|
+
const hoist = (k) => {
|
|
251
|
+
const inner = req.context[k];
|
|
252
|
+
if (inner === undefined) return;
|
|
168
253
|
if (req[k] !== undefined) {
|
|
169
254
|
throw new RequestError(`pass ${k} once: at the top of the request or inside context, not both`, 'invalid_request');
|
|
170
255
|
}
|
|
171
|
-
req[k] =
|
|
256
|
+
req[k] = inner;
|
|
172
257
|
delete req.context[k];
|
|
173
|
-
}
|
|
258
|
+
};
|
|
259
|
+
hoist('constraints');
|
|
260
|
+
hoist('success_criteria');
|
|
174
261
|
req.network ??= 'on';
|
|
175
262
|
req.success_criteria ??= [];
|
|
176
263
|
req.constraints ??= [];
|
|
177
|
-
|
|
264
|
+
req.context.facts ??= [];
|
|
265
|
+
req.context.counterpoints ??= [];
|
|
266
|
+
req.context.artifacts ??= [];
|
|
178
267
|
const hasSingle = req.target !== undefined && req.target !== null;
|
|
179
268
|
const hasMany = Array.isArray(req.targets) && req.targets.length > 0;
|
|
180
269
|
if (hasSingle && hasMany) {
|
|
181
270
|
throw new RequestError('pass either target (one consultant) or targets (several), not both', 'invalid_request');
|
|
182
271
|
}
|
|
183
|
-
const listed = () =>
|
|
184
|
-
|
|
272
|
+
const listed = () =>
|
|
273
|
+
availableConsultants()
|
|
274
|
+
.map((c) => `${c.name} (${c.cli}, ${c.model})`)
|
|
275
|
+
.join(', ') || 'none -- no consultant CLI is available here';
|
|
185
276
|
if (!hasSingle && !hasMany) {
|
|
186
277
|
throw new RequestError(
|
|
187
278
|
`name the consultant: target: a consultant, its CLI or its model (available: ${listed()}), or targets: [...] to ask several the same question, targets: ["all"] to ask everyone`,
|
|
188
279
|
'target_required',
|
|
189
280
|
);
|
|
190
281
|
}
|
|
282
|
+
/** @type {string[]} */
|
|
191
283
|
const named = hasSingle ? [req.target] : req.targets;
|
|
192
284
|
const isAll = named.map((n) => normalizeName(n) === 'all');
|
|
193
285
|
if (isAll.some(Boolean) && named.length > 1) {
|
|
194
286
|
throw new RequestError('"all" already means every available consultant; pass it alone', 'all_mixed');
|
|
195
287
|
}
|
|
196
288
|
|
|
289
|
+
/** @type {Consultant[]} */
|
|
197
290
|
let chosen;
|
|
198
291
|
if (isAll[0]) {
|
|
199
292
|
chosen = availableConsultants();
|
|
200
293
|
if (!chosen.length) {
|
|
201
|
-
throw new RequestError(
|
|
294
|
+
throw new RequestError(
|
|
295
|
+
'no consultant is available on this machine -- install one of the CLIs, or check the config file',
|
|
296
|
+
'target_unavailable',
|
|
297
|
+
);
|
|
202
298
|
}
|
|
203
299
|
} else {
|
|
204
300
|
// Resolved among the reachable consultants first, so one that is off does
|
|
@@ -211,15 +307,18 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
211
307
|
let r = resolveConsultant(name, reachable, POLICY.ranking);
|
|
212
308
|
if (r.none) r = resolveConsultant(name, POLICY.consultants, POLICY.ranking);
|
|
213
309
|
if (r.none && brokenConsultant(name)) {
|
|
214
|
-
throw new RequestError(
|
|
310
|
+
throw new RequestError(
|
|
311
|
+
`consultant ${JSON.stringify(name)} is not available: ${brokenConsultant(name)}; available: ${listed()}`,
|
|
312
|
+
'target_unavailable',
|
|
313
|
+
);
|
|
215
314
|
}
|
|
216
315
|
if (r.none) {
|
|
217
316
|
throw new RequestError(`no consultant answers to ${JSON.stringify(name)}; available: ${listed()}`, 'unknown_target');
|
|
218
317
|
}
|
|
219
318
|
if (r.ambiguous) {
|
|
220
319
|
throw new RequestError(
|
|
221
|
-
`${JSON.stringify(name)} fits more than one consultant and no ranking rule decides between `
|
|
222
|
-
|
|
320
|
+
`${JSON.stringify(name)} fits more than one consultant and no ranking rule decides between ` +
|
|
321
|
+
`${r.ambiguous.map((c) => `${c.name} (${c.cli}, ${c.model})`).join(', ')}; name one of them`,
|
|
223
322
|
'target_ambiguous',
|
|
224
323
|
);
|
|
225
324
|
}
|
|
@@ -228,8 +327,8 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
228
327
|
const off = [...new Set(chosen.filter((c) => !consultantAvailable(c)))];
|
|
229
328
|
if (off.length) {
|
|
230
329
|
throw new RequestError(
|
|
231
|
-
`consultant ${off.map((c) => `${JSON.stringify(c.name)} (${unavailableReason(c)})`).join(', ')} `
|
|
232
|
-
|
|
330
|
+
`consultant ${off.map((c) => `${JSON.stringify(c.name)} (${unavailableReason(c)})`).join(', ')} ` +
|
|
331
|
+
`is not available on this machine; available: ${listed()}`,
|
|
233
332
|
'target_unavailable',
|
|
234
333
|
);
|
|
235
334
|
}
|
|
@@ -249,7 +348,7 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
249
348
|
);
|
|
250
349
|
}
|
|
251
350
|
req.targets = unique;
|
|
252
|
-
req.target = unique[0]; // single-target consumers (brief, history, adapters) keep working
|
|
351
|
+
req.target = /** @type {string} */ (unique[0]); // non-empty: every name in `named` resolved to a consultant; single-target consumers (brief, history, adapters) keep working
|
|
253
352
|
req.fanout = unique.length > 1;
|
|
254
353
|
req.caller = resolveCaller(req.caller ?? '') ?? null;
|
|
255
354
|
req.caller_model = req.caller_model ?? null;
|
|
@@ -261,8 +360,7 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
261
360
|
// one its context fits -- no proposal is explore, a proposal is review, a
|
|
262
361
|
// proposal with the other side's claims is debate. Checked below like a
|
|
263
362
|
// mode that was given.
|
|
264
|
-
req.mode ??= parentMode
|
|
265
|
-
?? (!req.context.proposal ? 'explore' : req.context.counterpoints.length ? 'debate' : 'review');
|
|
363
|
+
req.mode ??= parentMode ?? (!req.context.proposal ? 'explore' : req.context.counterpoints.length ? 'debate' : 'review');
|
|
266
364
|
|
|
267
365
|
const total = charCount(req);
|
|
268
366
|
if (total > L.totalCharsMax) {
|
|
@@ -296,11 +394,18 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
296
394
|
'expose_from_parent_needs_followup',
|
|
297
395
|
);
|
|
298
396
|
}
|
|
299
|
-
if (
|
|
300
|
-
|
|
397
|
+
if (
|
|
398
|
+
req.context.facts.length === 0 &&
|
|
399
|
+
req.context.artifacts.length === 0 &&
|
|
400
|
+
req.context.expose_paths.length === 0 &&
|
|
401
|
+
!req.context.expose_diff &&
|
|
402
|
+
!req.context.expose_from_parent
|
|
403
|
+
) {
|
|
301
404
|
throw new RequestError(
|
|
302
|
-
'provide at least one entry in context.facts, context.artifacts or context.expose_paths: the consultant starts in an empty working directory and is not told where your repository is, so every fact it needs must be in the request'
|
|
303
|
-
|
|
405
|
+
'provide at least one entry in context.facts, context.artifacts or context.expose_paths: the consultant starts in an empty working directory and is not told where your repository is, so every fact it needs must be in the request' +
|
|
406
|
+
(isFollowup
|
|
407
|
+
? '; on a follow-up, context.expose_from_parent: true copies again the files the earlier round was shown'
|
|
408
|
+
: ''),
|
|
304
409
|
'context_required',
|
|
305
410
|
);
|
|
306
411
|
}
|