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/src/schema.mjs
CHANGED
|
@@ -5,8 +5,16 @@
|
|
|
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
|
|
|
@@ -36,7 +44,11 @@ const exposedPathSchema = z
|
|
|
36
44
|
.refine((p) => path.isAbsolute(p), { message: 'context.expose_paths[] must be an absolute path' });
|
|
37
45
|
|
|
38
46
|
// A revision for `git diff`: a ref, a commit, or HEAD~n -- never an option.
|
|
39
|
-
const gitRevision = z
|
|
47
|
+
const gitRevision = z
|
|
48
|
+
.string()
|
|
49
|
+
.trim()
|
|
50
|
+
.min(1)
|
|
51
|
+
.max(200)
|
|
40
52
|
.regex(/^[A-Za-z0-9._/~^@{}-]+$/, 'context.expose_diff.base must be a git revision (a branch, tag, commit or HEAD~n)')
|
|
41
53
|
.refine((r) => !r.startsWith('-'), { message: 'context.expose_diff.base must not start with "-"' });
|
|
42
54
|
|
|
@@ -65,7 +77,11 @@ export const contextSchema = z
|
|
|
65
77
|
// for it, its CLI, its vendor, or its model ("glm", "claude opus"). Which
|
|
66
78
|
// consultant that is depends on the operator's config, so it is resolved in
|
|
67
79
|
// parseRequest, not by an enum here.
|
|
68
|
-
const targetSpec = z
|
|
80
|
+
const targetSpec = z
|
|
81
|
+
.string()
|
|
82
|
+
.trim()
|
|
83
|
+
.min(1, 'target must not be empty')
|
|
84
|
+
.max(120, 'target exceeds 120 characters')
|
|
69
85
|
.regex(/^[^\p{Cc}\p{Cf}]+$/u, 'target must be a single line of printable characters');
|
|
70
86
|
|
|
71
87
|
// Who asked for a consultation, as the lead reports it.
|
|
@@ -159,6 +175,8 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
159
175
|
const issues = parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`);
|
|
160
176
|
throw new RequestError(`request failed validation: ${issues.join('; ')}`, 'invalid_request', issues);
|
|
161
177
|
}
|
|
178
|
+
// Filled in below with what the request resolves to (targets, caller, ...).
|
|
179
|
+
/** @type {Record<string, any>} */
|
|
162
180
|
const req = parsed.data;
|
|
163
181
|
// Left without a default in the schema, so that a follow-up can tell a list
|
|
164
182
|
// it left out (inherited from the round before, jobs.mjs) from one it
|
|
@@ -180,8 +198,10 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
180
198
|
if (hasSingle && hasMany) {
|
|
181
199
|
throw new RequestError('pass either target (one consultant) or targets (several), not both', 'invalid_request');
|
|
182
200
|
}
|
|
183
|
-
const listed = () =>
|
|
184
|
-
|
|
201
|
+
const listed = () =>
|
|
202
|
+
availableConsultants()
|
|
203
|
+
.map((c) => `${c.name} (${c.cli}, ${c.model})`)
|
|
204
|
+
.join(', ') || 'none -- no consultant CLI is available here';
|
|
185
205
|
if (!hasSingle && !hasMany) {
|
|
186
206
|
throw new RequestError(
|
|
187
207
|
`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`,
|
|
@@ -198,7 +218,10 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
198
218
|
if (isAll[0]) {
|
|
199
219
|
chosen = availableConsultants();
|
|
200
220
|
if (!chosen.length) {
|
|
201
|
-
throw new RequestError(
|
|
221
|
+
throw new RequestError(
|
|
222
|
+
'no consultant is available on this machine -- install one of the CLIs, or check the config file',
|
|
223
|
+
'target_unavailable',
|
|
224
|
+
);
|
|
202
225
|
}
|
|
203
226
|
} else {
|
|
204
227
|
// Resolved among the reachable consultants first, so one that is off does
|
|
@@ -211,15 +234,18 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
211
234
|
let r = resolveConsultant(name, reachable, POLICY.ranking);
|
|
212
235
|
if (r.none) r = resolveConsultant(name, POLICY.consultants, POLICY.ranking);
|
|
213
236
|
if (r.none && brokenConsultant(name)) {
|
|
214
|
-
throw new RequestError(
|
|
237
|
+
throw new RequestError(
|
|
238
|
+
`consultant ${JSON.stringify(name)} is not available: ${brokenConsultant(name)}; available: ${listed()}`,
|
|
239
|
+
'target_unavailable',
|
|
240
|
+
);
|
|
215
241
|
}
|
|
216
242
|
if (r.none) {
|
|
217
243
|
throw new RequestError(`no consultant answers to ${JSON.stringify(name)}; available: ${listed()}`, 'unknown_target');
|
|
218
244
|
}
|
|
219
245
|
if (r.ambiguous) {
|
|
220
246
|
throw new RequestError(
|
|
221
|
-
`${JSON.stringify(name)} fits more than one consultant and no ranking rule decides between `
|
|
222
|
-
|
|
247
|
+
`${JSON.stringify(name)} fits more than one consultant and no ranking rule decides between ` +
|
|
248
|
+
`${r.ambiguous.map((c) => `${c.name} (${c.cli}, ${c.model})`).join(', ')}; name one of them`,
|
|
223
249
|
'target_ambiguous',
|
|
224
250
|
);
|
|
225
251
|
}
|
|
@@ -228,8 +254,8 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
228
254
|
const off = [...new Set(chosen.filter((c) => !consultantAvailable(c)))];
|
|
229
255
|
if (off.length) {
|
|
230
256
|
throw new RequestError(
|
|
231
|
-
`consultant ${off.map((c) => `${JSON.stringify(c.name)} (${unavailableReason(c)})`).join(', ')} `
|
|
232
|
-
|
|
257
|
+
`consultant ${off.map((c) => `${JSON.stringify(c.name)} (${unavailableReason(c)})`).join(', ')} ` +
|
|
258
|
+
`is not available on this machine; available: ${listed()}`,
|
|
233
259
|
'target_unavailable',
|
|
234
260
|
);
|
|
235
261
|
}
|
|
@@ -261,8 +287,7 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
261
287
|
// one its context fits -- no proposal is explore, a proposal is review, a
|
|
262
288
|
// proposal with the other side's claims is debate. Checked below like a
|
|
263
289
|
// mode that was given.
|
|
264
|
-
req.mode ??= parentMode
|
|
265
|
-
?? (!req.context.proposal ? 'explore' : req.context.counterpoints.length ? 'debate' : 'review');
|
|
290
|
+
req.mode ??= parentMode ?? (!req.context.proposal ? 'explore' : req.context.counterpoints.length ? 'debate' : 'review');
|
|
266
291
|
|
|
267
292
|
const total = charCount(req);
|
|
268
293
|
if (total > L.totalCharsMax) {
|
|
@@ -296,11 +321,18 @@ export function parseRequest(raw, { isFollowup = false, parentMode = null, group
|
|
|
296
321
|
'expose_from_parent_needs_followup',
|
|
297
322
|
);
|
|
298
323
|
}
|
|
299
|
-
if (
|
|
300
|
-
|
|
324
|
+
if (
|
|
325
|
+
req.context.facts.length === 0 &&
|
|
326
|
+
req.context.artifacts.length === 0 &&
|
|
327
|
+
req.context.expose_paths.length === 0 &&
|
|
328
|
+
!req.context.expose_diff &&
|
|
329
|
+
!req.context.expose_from_parent
|
|
330
|
+
) {
|
|
301
331
|
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
|
-
|
|
332
|
+
'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' +
|
|
333
|
+
(isFollowup
|
|
334
|
+
? '; on a follow-up, context.expose_from_parent: true copies again the files the earlier round was shown'
|
|
335
|
+
: ''),
|
|
304
336
|
'context_required',
|
|
305
337
|
);
|
|
306
338
|
}
|
package/src/server.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { requestSchema, RequestError } from './schema.mjs';
|
|
|
9
9
|
import { JobManager, pointsOf } from './jobs.mjs';
|
|
10
10
|
import { exportChain, ExportError } from './export.mjs';
|
|
11
11
|
|
|
12
|
-
export const SEVERALLY_VERSION = '2.
|
|
12
|
+
export const SEVERALLY_VERSION = '2.4.0';
|
|
13
13
|
|
|
14
14
|
// consult_get waits this long when no wait_ms is given: a call that forgot it
|
|
15
15
|
// would otherwise return at once and be repeated. Up to 45 s, under the 60 s
|
|
@@ -22,7 +22,11 @@ const DEFAULT_WAIT_MS = process.env.SEVERALLY_MAX_WAIT_MS ? POLICY.maxWaitMs : M
|
|
|
22
22
|
const PROGRESS_EVERY_MS = 15_000;
|
|
23
23
|
|
|
24
24
|
const semver = (v) => String(v).split('.').map(Number);
|
|
25
|
-
const older = (a, b) => {
|
|
25
|
+
const older = (a, b) => {
|
|
26
|
+
const [x, y] = [semver(a), semver(b)];
|
|
27
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i];
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
26
30
|
|
|
27
31
|
/**
|
|
28
32
|
* The other installed copies of severally, when one runs a different version
|
|
@@ -31,7 +35,13 @@ const older = (a, b) => { const [x, y] = [semver(a), semver(b)]; for (let i = 0;
|
|
|
31
35
|
* version strings are compared. A copy that is missing is not reported.
|
|
32
36
|
*/
|
|
33
37
|
export function staleInstall(home = os.homedir(), severallyHome = POLICY.home) {
|
|
34
|
-
const read = (file) => {
|
|
38
|
+
const read = (file) => {
|
|
39
|
+
try {
|
|
40
|
+
return fs.readFileSync(file, 'utf8');
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
35
45
|
// The runtime's version is read from its bundle, which every install mode
|
|
36
46
|
// replaces; its package.json is written by plugin installs only.
|
|
37
47
|
const bundle = read(path.join(severallyHome, 'runtime', 'severally-mcp.mjs'));
|
|
@@ -39,7 +49,13 @@ export function staleInstall(home = os.homedir(), severallyHome = POLICY.home) {
|
|
|
39
49
|
const found = {
|
|
40
50
|
// This release names it SEVERALLY_VERSION; older ones only had it in the server info.
|
|
41
51
|
runtime: bundle?.match(/(?:SEVERALLY_VERSION = |name: ["']severally["'], version: )["'](\d+\.\d+\.\d+)["']/)?.[1],
|
|
42
|
-
claude_skills_dir: (() => {
|
|
52
|
+
claude_skills_dir: (() => {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(plugin).version;
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
})(),
|
|
43
59
|
};
|
|
44
60
|
const copies = { this_server: SEVERALLY_VERSION };
|
|
45
61
|
// Only plain x.y.z is compared; anything else is left out rather than guessed at.
|
|
@@ -50,9 +66,10 @@ export function staleInstall(home = os.homedir(), severallyHome = POLICY.home) {
|
|
|
50
66
|
return {
|
|
51
67
|
copies,
|
|
52
68
|
behind: Object.keys(copies).filter((k) => older(copies[k], newest)),
|
|
53
|
-
fix:
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
fix:
|
|
70
|
+
`bring every copy to ${newest}: run \`npx -y severally-mcp@${newest} install\` (or \`node scripts/install.mjs\` from a checkout of that version) ` +
|
|
71
|
+
'(it updates the runtime and the ~/.claude/skills copy), run `/plugin marketplace update severally` for a ' +
|
|
72
|
+
'marketplace install, then restart the clients',
|
|
56
73
|
};
|
|
57
74
|
}
|
|
58
75
|
|
|
@@ -73,7 +90,11 @@ check the grounds behind a point before you adopt it, and record what you adopte
|
|
|
73
90
|
const startDescription = `Start a consultation with another agent (or several, via targets). Returns a job_id (or a group_id for several) at once; the work runs in the background.
|
|
74
91
|
|
|
75
92
|
target: which consultant to ask. This machine can reach (name: CLI, model):
|
|
76
|
-
${
|
|
93
|
+
${
|
|
94
|
+
availableConsultants()
|
|
95
|
+
.map((c) => ` ${c.name}: ${c.cli}, ${c.model}`)
|
|
96
|
+
.join('\n') || ' (none -- no consultant CLI is installed)'
|
|
97
|
+
}
|
|
77
98
|
Pass the word the user used: a name, a CLI ("codex", "agy"), a vendor or a model ("claude opus"). One
|
|
78
99
|
that fits several, unresolved by the operator's ranking, tier and version, is refused with the
|
|
79
100
|
candidates (target_ambiguous); one not listed is refused (target_unavailable). Your own CLI on a fresh
|
|
@@ -107,29 +128,38 @@ Model, permissions, rounds, timeout and size caps are fixed by this server.`;
|
|
|
107
128
|
// element of its `records`.
|
|
108
129
|
const recordShape = {
|
|
109
130
|
job_id: z.string().min(1).describe('the job_id consult_get returned'),
|
|
110
|
-
entries: z
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
entries: z
|
|
132
|
+
.array(
|
|
133
|
+
z.object({
|
|
134
|
+
id: z.string().min(1).describe('f1, u1, c1 ... from the result'),
|
|
135
|
+
verdict: z.enum(VERDICTS),
|
|
136
|
+
effect: z.string().max(4000).optional().describe('what it changed, or why it is unverified'),
|
|
137
|
+
note: z.string().max(4000).optional().describe('what you checked and found'),
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
.min(1)
|
|
141
|
+
.max(100)
|
|
142
|
+
.optional(),
|
|
143
|
+
reflection: z
|
|
144
|
+
.object({
|
|
145
|
+
delta: z.string().min(1).max(4000).describe('what the answer added over what you expected ("nothing new" counts)'),
|
|
146
|
+
related_item_ids: z.array(z.string().min(1)).max(50).optional().describe('f1, u1, c1 ...'),
|
|
147
|
+
})
|
|
148
|
+
.optional()
|
|
149
|
+
.describe('written after reading the answer'),
|
|
120
150
|
};
|
|
121
151
|
|
|
122
152
|
export function createServer(manager = new JobManager()) {
|
|
123
153
|
// Read once: the copies on disk change only when someone reinstalls, and then
|
|
124
154
|
// this server is the one to restart.
|
|
125
155
|
const stale = staleInstall();
|
|
126
|
-
const server = new McpServer(
|
|
127
|
-
{ name: 'severally', version: SEVERALLY_VERSION },
|
|
128
|
-
{ instructions: SERVER_INSTRUCTIONS },
|
|
129
|
-
);
|
|
156
|
+
const server = new McpServer({ name: 'severally', version: SEVERALLY_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
130
157
|
|
|
131
|
-
const ok = (payload) => ({ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] });
|
|
132
|
-
const fail = (payload) => ({
|
|
158
|
+
const ok = (payload) => ({ content: [{ type: /** @type {const} */ ('text'), text: JSON.stringify(payload, null, 2) }] });
|
|
159
|
+
const fail = (payload) => ({
|
|
160
|
+
content: [{ type: /** @type {const} */ ('text'), text: JSON.stringify(payload, null, 2) }],
|
|
161
|
+
isError: true,
|
|
162
|
+
});
|
|
133
163
|
|
|
134
164
|
server.registerTool(
|
|
135
165
|
'consult_start',
|
|
@@ -164,7 +194,7 @@ export function createServer(manager = new JobManager()) {
|
|
|
164
194
|
'again while status is running. completed carries the structured answer; failed carries ' +
|
|
165
195
|
'failure.kind (timeout, auth, usage_limit, rate_limited, model_unavailable, invalid_output, cli_error, ' +
|
|
166
196
|
'spawn_error, interrupted), meaning no advice was obtained -- not the same as advice with thin evidence ' +
|
|
167
|
-
|
|
197
|
+
"(quality.evidence_basis, quality.caveat). group_id fetches a fan-out: each member's status, failure and " +
|
|
168
198
|
'job_id, and `comparison.by_target` with every stance, summary and finding id side by side, cut where ' +
|
|
169
199
|
'`truncated` says so. ids: ["f2", "u1"] with a job_id returns just those points, whole, with their grounds ' +
|
|
170
200
|
'and your verdict (`recorded`). A finished consultation names its brief rather than repeating it; detail: ' +
|
|
@@ -174,8 +204,14 @@ export function createServer(manager = new JobManager()) {
|
|
|
174
204
|
group_id: z.string().min(1).optional(),
|
|
175
205
|
wait_ms: z.number().int().min(0).max(POLICY.maxWaitMs).optional(),
|
|
176
206
|
detail: z.enum(['full']).optional(),
|
|
177
|
-
ids: z
|
|
178
|
-
.
|
|
207
|
+
ids: z
|
|
208
|
+
.array(z.string().min(1))
|
|
209
|
+
.min(1)
|
|
210
|
+
.max(100)
|
|
211
|
+
.optional()
|
|
212
|
+
.describe(
|
|
213
|
+
'with job_id: return only these points of the finished answer (f1, u1, c1 ...), each whole with its grounds and, under `recorded`, the verdict you wrote',
|
|
214
|
+
),
|
|
179
215
|
}),
|
|
180
216
|
},
|
|
181
217
|
async ({ job_id, group_id, wait_ms, detail, ids }, ctx) => {
|
|
@@ -183,16 +219,26 @@ export function createServer(manager = new JobManager()) {
|
|
|
183
219
|
return fail({ error: 'invalid_request', message: 'pass exactly one of job_id or group_id' });
|
|
184
220
|
}
|
|
185
221
|
if (ids && (group_id || detail)) {
|
|
186
|
-
return fail({
|
|
222
|
+
return fail({
|
|
223
|
+
error: 'invalid_request',
|
|
224
|
+
message: "ids goes with job_id alone: a point id belongs to one consultant's answer",
|
|
225
|
+
});
|
|
187
226
|
}
|
|
188
227
|
const req = ctx?.mcpReq;
|
|
189
228
|
const token = req?._meta?.progressToken;
|
|
190
229
|
let beats = 0;
|
|
191
|
-
const timer =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
230
|
+
const timer =
|
|
231
|
+
token === undefined
|
|
232
|
+
? null
|
|
233
|
+
: setInterval(() => {
|
|
234
|
+
beats += 1;
|
|
235
|
+
req
|
|
236
|
+
.notify({
|
|
237
|
+
method: 'notifications/progress',
|
|
238
|
+
params: { progressToken: token, progress: beats, message: 'the consultation is still running' },
|
|
239
|
+
})
|
|
240
|
+
.catch(() => {});
|
|
241
|
+
}, PROGRESS_EVERY_MS);
|
|
196
242
|
let view;
|
|
197
243
|
try {
|
|
198
244
|
// A request the client cancels stops the wait with it.
|
|
@@ -203,7 +249,10 @@ export function createServer(manager = new JobManager()) {
|
|
|
203
249
|
clearInterval(timer);
|
|
204
250
|
}
|
|
205
251
|
if (!view) {
|
|
206
|
-
return fail({
|
|
252
|
+
return fail({
|
|
253
|
+
error: 'unknown_job',
|
|
254
|
+
message: `no consultation with ${group_id ? 'group_id' : 'job_id'} "${group_id ?? job_id}"`,
|
|
255
|
+
});
|
|
207
256
|
}
|
|
208
257
|
// Still running: the poll answer as it is; ids applies once there is an answer.
|
|
209
258
|
if (!ids || ['running', 'queued', 'cancelling'].includes(view.status)) return ok(view);
|
|
@@ -223,8 +272,8 @@ export function createServer(manager = new JobManager()) {
|
|
|
223
272
|
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false },
|
|
224
273
|
description:
|
|
225
274
|
'Stop a running consultation and kill the consultant process and everything it spawned. ' +
|
|
226
|
-
'Pass group_id instead of job_id to stop every consultant in a fan-out. One another severally server runs '
|
|
227
|
-
|
|
275
|
+
'Pass group_id instead of job_id to stop every consultant in a fan-out. One another severally server runs ' +
|
|
276
|
+
'is asked to stop, and that server stops it within a second.',
|
|
228
277
|
inputSchema: z.object({
|
|
229
278
|
job_id: z.string().min(1).optional(),
|
|
230
279
|
group_id: z.string().min(1).optional(),
|
|
@@ -236,7 +285,10 @@ export function createServer(manager = new JobManager()) {
|
|
|
236
285
|
}
|
|
237
286
|
const view = group_id ? manager.cancelGroup(group_id) : manager.cancel(job_id);
|
|
238
287
|
if (!view) {
|
|
239
|
-
return fail({
|
|
288
|
+
return fail({
|
|
289
|
+
error: 'unknown_job',
|
|
290
|
+
message: `no consultation with ${group_id ? 'group_id' : 'job_id'} "${group_id ?? job_id}"`,
|
|
291
|
+
});
|
|
240
292
|
}
|
|
241
293
|
return ok(view);
|
|
242
294
|
},
|
|
@@ -248,35 +300,44 @@ export function createServer(manager = new JobManager()) {
|
|
|
248
300
|
title: 'Record what checking a point showed',
|
|
249
301
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
250
302
|
description:
|
|
251
|
-
'Write your verdict on points of an answer after checking them: ids f1 ... (findings), u1 ... (unknowns), '
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
303
|
+
'Write your verdict on points of an answer after checking them: ids f1 ... (findings), u1 ... (unknowns), ' +
|
|
304
|
+
'c1 ... (next_checks). verdict is what checking showed -- confirmed (holds here), not_applicable (true in ' +
|
|
305
|
+
'general, not here), unverifiable, unverified (not checked yet; say why in effect) -- not whether you adopted ' +
|
|
306
|
+
'it. effect is what it changed about your decision, note the evidence. A follow-up shows the consultant each ' +
|
|
307
|
+
'verdict and effect (credential-masked), never the note. The same id again replaces the entry. Saved beside ' +
|
|
308
|
+
'the answer in ~/.severally/history, so a job from an earlier session can be recorded against; the server ' +
|
|
309
|
+
'never infers a verdict. Answers with counts; consult_get reads the entries back. reflection records what the ' +
|
|
310
|
+
'answer added over your consult_start prediction. records: [{ job_id, entries, reflection? }, ...] records ' +
|
|
311
|
+
'every member of a fan-out in one call; all are checked first and nothing is written if any job or id is ' +
|
|
312
|
+
'wrong. A job whose write fails is listed under results with its error.',
|
|
261
313
|
inputSchema: z.object({
|
|
262
314
|
...recordShape,
|
|
263
315
|
job_id: recordShape.job_id.optional(),
|
|
264
|
-
records: z
|
|
316
|
+
records: z
|
|
317
|
+
.array(z.object(recordShape))
|
|
318
|
+
.min(1)
|
|
319
|
+
.max(POLICY.maxConcurrent)
|
|
320
|
+
.optional()
|
|
265
321
|
.describe('one per fan-out member, instead of job_id/entries/reflection'),
|
|
266
322
|
}),
|
|
267
323
|
},
|
|
268
324
|
async ({ job_id, entries, reflection, records }) => {
|
|
269
325
|
try {
|
|
270
326
|
if (records !== undefined) {
|
|
271
|
-
const out = manager.record(
|
|
327
|
+
const out = manager.record(
|
|
328
|
+
Object.fromEntries(Object.entries({ job_id, entries, reflection, records }).filter(([, v]) => v !== undefined)),
|
|
329
|
+
);
|
|
272
330
|
return out.not_saved ? fail(out) : ok(out);
|
|
273
331
|
}
|
|
274
|
-
if (!job_id)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
332
|
+
if (!job_id)
|
|
333
|
+
return fail({ error: 'invalid_request', message: 'pass job_id (with entries and/or reflection), or records' });
|
|
334
|
+
return ok(
|
|
335
|
+
manager.record({
|
|
336
|
+
job_id,
|
|
337
|
+
...(entries === undefined ? {} : { entries }),
|
|
338
|
+
...(reflection === undefined ? {} : { reflection }),
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
280
341
|
} catch (err) {
|
|
281
342
|
if (err instanceof RequestError) {
|
|
282
343
|
return fail({ error: err.code, message: err.message, details: err.details ?? null });
|
|
@@ -292,10 +353,10 @@ export function createServer(manager = new JobManager()) {
|
|
|
292
353
|
title: 'Export a consultation as a record to commit',
|
|
293
354
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
294
355
|
description:
|
|
295
|
-
'Render one consultation (chain_id) or fan-out (group_id) as Markdown: the brief as sent, each answer as '
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
356
|
+
'Render one consultation (chain_id) or fan-out (group_id) as Markdown: the brief as sent, each answer as ' +
|
|
357
|
+
'it came back, and the verdicts on each point, unchecked ones marked. Nothing is summarised or scored. ' +
|
|
358
|
+
'The text is returned, not written: put it beside the code the decision is about. Reads the history, so ' +
|
|
359
|
+
"an earlier session's consultation can be exported.",
|
|
299
360
|
inputSchema: z.object({
|
|
300
361
|
chain_id: z.string().min(1).optional(),
|
|
301
362
|
group_id: z.string().min(1).optional(),
|
|
@@ -317,16 +378,17 @@ export function createServer(manager = new JobManager()) {
|
|
|
317
378
|
title: 'List recent consultations',
|
|
318
379
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
319
380
|
description:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
381
|
+
"Recent consultations, earlier sessions' too (this_session is false), newest first, with their ids, " +
|
|
382
|
+
'status, summary and verdicts; and usage_totals, the tokens (and cost) each consultant reported this session, ' +
|
|
383
|
+
'retries included. A figure a CLI does not report is named under not_reported, not counted as zero.',
|
|
323
384
|
inputSchema: z.object({ limit: z.number().int().min(1).max(100).optional() }),
|
|
324
385
|
},
|
|
325
|
-
async ({ limit }) =>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
386
|
+
async ({ limit }) =>
|
|
387
|
+
ok({
|
|
388
|
+
jobs: manager.list({ limit: limit ?? 20 }),
|
|
389
|
+
usage_totals: manager.usageTotals(),
|
|
390
|
+
limits: limitsSummary(),
|
|
391
|
+
}),
|
|
330
392
|
);
|
|
331
393
|
|
|
332
394
|
return { server, manager };
|
|
@@ -336,17 +398,17 @@ export async function main() {
|
|
|
336
398
|
// A config mistake is said here, at start, as well as on every refused
|
|
337
399
|
// consult_start: the operator may be the only one who reads stderr.
|
|
338
400
|
const problem = configProblem();
|
|
339
|
-
if (problem)
|
|
340
|
-
|
|
401
|
+
if (problem)
|
|
402
|
+
process.stderr.write(`severally: config needs fixing; every consultation will be refused until it is -- ${problem}\n`);
|
|
403
|
+
for (const w of configWarnings())
|
|
404
|
+
process.stderr.write(`severally: config mistake; the consultant or CLI it is in stays unused until it is fixed -- ${w}\n`);
|
|
341
405
|
if (availableConsultants().length === 0) {
|
|
342
406
|
process.stderr.write(
|
|
343
407
|
'severally: no consultant CLI found on PATH (codex / claude / agy / opencode); every consultation will be refused\n',
|
|
344
408
|
);
|
|
345
409
|
}
|
|
346
410
|
if (process.env.SEVERALLY_ACTIVE === '1') {
|
|
347
|
-
process.stderr.write(
|
|
348
|
-
'severally: refusing to start inside a severally consultant session (recursion barrier)\n',
|
|
349
|
-
);
|
|
411
|
+
process.stderr.write('severally: refusing to start inside a severally consultant session (recursion barrier)\n');
|
|
350
412
|
process.exit(2);
|
|
351
413
|
}
|
|
352
414
|
// One manager for the process; serveStdio builds the server once the client's
|
package/src/status.mjs
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
// `severally-mcp status`: what this machine has set up, read without starting a
|
|
2
|
-
// consultation, so it spends no quota. It says what it can see and no more:
|
|
3
|
-
//
|
|
4
|
-
// reported as not checked rather than as working.
|
|
5
|
-
import {
|
|
2
|
+
// consultation, so it spends no quota. It says what it can see and no more:
|
|
3
|
+
// login is read from each CLI's own status command where it has one, and where
|
|
4
|
+
// it has none it is reported as not checked rather than as working.
|
|
5
|
+
import {
|
|
6
|
+
POLICY,
|
|
7
|
+
configProblem,
|
|
8
|
+
configWarnings,
|
|
9
|
+
unavailableReason,
|
|
10
|
+
consultantAvailable,
|
|
11
|
+
brokenConsultantNames,
|
|
12
|
+
brokenConsultant,
|
|
13
|
+
} from './policy.mjs';
|
|
6
14
|
import { SEVERALLY_VERSION, staleInstall } from './server.mjs';
|
|
7
15
|
import { findCommand, execCommandSync } from './platform.mjs';
|
|
8
16
|
|
|
9
17
|
// How each client lists severally, plugin installs first. A command that exits 0
|
|
10
18
|
// and names severally counts as registered.
|
|
11
19
|
const CLIENTS = [
|
|
12
|
-
{
|
|
13
|
-
|
|
20
|
+
{
|
|
21
|
+
label: 'Claude Code',
|
|
22
|
+
cmd: 'claude',
|
|
23
|
+
checks: [
|
|
24
|
+
['plugin', 'list'],
|
|
25
|
+
['mcp', 'get', 'severally'],
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: 'Codex',
|
|
30
|
+
cmd: 'codex',
|
|
31
|
+
checks: [
|
|
32
|
+
['plugin', 'list'],
|
|
33
|
+
['mcp', 'get', 'severally'],
|
|
34
|
+
],
|
|
35
|
+
},
|
|
14
36
|
{ label: 'Antigravity', cmd: 'agy', checks: [['mcp', 'list']] },
|
|
15
37
|
{ label: 'OpenCode', cmd: 'opencode', checks: [['mcp', 'list']] },
|
|
16
38
|
];
|
|
@@ -35,6 +57,46 @@ function registration({ cmd, checks }) {
|
|
|
35
57
|
return ran ? 'not registered' : `could not tell (${cmd} ${checks.map((a) => a.join(' ')).join(' / ')} failed)`;
|
|
36
58
|
}
|
|
37
59
|
|
|
60
|
+
// Each CLI's own word on its login, read without sending a prompt. Only the
|
|
61
|
+
// verdict is kept: `claude auth status` also prints the account's email.
|
|
62
|
+
// Antigravity has no such command, so it is not checked.
|
|
63
|
+
const LOGIN = {
|
|
64
|
+
'claude-code': { cmd: 'claude', args: ['auth', 'status', '--json'], read: (out) => JSON.parse(out).loggedIn === true },
|
|
65
|
+
// Codex prints its answer on stderr; the exit status carries the verdict.
|
|
66
|
+
codex: { cmd: 'codex', args: ['login', 'status'], read: (out, ok) => ok },
|
|
67
|
+
// OpenCode can also take keys from the environment, so no saved credential
|
|
68
|
+
// is a warning, not a verdict.
|
|
69
|
+
opencode: {
|
|
70
|
+
cmd: 'opencode',
|
|
71
|
+
args: ['auth', 'list'],
|
|
72
|
+
read: (out) => {
|
|
73
|
+
const n = out.split('\n').filter((l) => /\bstored\b/.test(l)).length;
|
|
74
|
+
return n ? `${n} saved credential${n > 1 ? 's' : ''}` : 'no saved credentials (keys in the environment still work)';
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function login(cli) {
|
|
80
|
+
const check = LOGIN[cli];
|
|
81
|
+
if (!check) return 'not checked (no login status command)';
|
|
82
|
+
if (!findCommand(check.cmd)) return 'not installed';
|
|
83
|
+
let out;
|
|
84
|
+
let ok = true;
|
|
85
|
+
try {
|
|
86
|
+
out = execCommandSync(check.cmd, check.args, { encoding: 'utf8', timeout: 20_000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (typeof err.status !== 'number') return `could not tell (${check.cmd} ${check.args.join(' ')} failed)`;
|
|
89
|
+
out = `${err.stdout ?? ''}`;
|
|
90
|
+
ok = false;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const v = check.read(out, ok);
|
|
94
|
+
return v === true ? 'logged in' : v || 'not logged in';
|
|
95
|
+
} catch {
|
|
96
|
+
return `could not tell (${check.cmd} ${check.args.join(' ')} answered in an unexpected form)`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
38
100
|
export function status() {
|
|
39
101
|
const lines = [`severally ${SEVERALLY_VERSION}`, ''];
|
|
40
102
|
const problem = configProblem();
|
|
@@ -44,20 +106,27 @@ export function status() {
|
|
|
44
106
|
|
|
45
107
|
lines.push('consultants:');
|
|
46
108
|
for (const c of POLICY.consultants) {
|
|
47
|
-
lines.push(
|
|
109
|
+
lines.push(
|
|
110
|
+
` ${c.name}: ${c.cli}, ${c.model} ${consultantAvailable(c) ? 'available' : `not available: ${unavailableReason(c)}`}`,
|
|
111
|
+
);
|
|
48
112
|
}
|
|
49
113
|
for (const name of brokenConsultantNames()) lines.push(` ${name}: not available: ${brokenConsultant(name)}`);
|
|
50
114
|
lines.push('');
|
|
51
115
|
|
|
52
116
|
const stale = staleInstall();
|
|
53
|
-
lines.push(
|
|
54
|
-
|
|
55
|
-
|
|
117
|
+
lines.push(
|
|
118
|
+
stale
|
|
119
|
+
? `copies: ${Object.entries(stale.copies)
|
|
120
|
+
.map(([k, v]) => `${k} ${v}${stale.behind.includes(k) ? ' (behind)' : ''}`)
|
|
121
|
+
.join(', ')}\n ${stale.fix}`
|
|
122
|
+
: 'copies: every installed copy is this version',
|
|
123
|
+
'',
|
|
124
|
+
);
|
|
56
125
|
|
|
57
126
|
lines.push('clients:');
|
|
58
127
|
for (const client of CLIENTS) lines.push(` ${client.label.padEnd(12)} ${registration(client)}`);
|
|
59
|
-
lines.push('', 'login
|
|
60
|
-
|
|
128
|
+
lines.push('', 'login (out of quota is not checked; the first consultation shows it):');
|
|
129
|
+
for (const cli of [...new Set(POLICY.consultants.map((c) => c.cli))]) lines.push(` ${cli.padEnd(12)} ${login(cli)}`);
|
|
61
130
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
62
131
|
return problem ? 1 : 0;
|
|
63
132
|
}
|