severally-mcp 2.4.0 → 2.5.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/CONFIG.md +1 -0
- package/README.ja.md +44 -19
- package/README.md +44 -18
- package/config.example.json +1 -1
- package/docs/how-it-works.ja.md +2 -0
- package/docs/how-it-works.md +2 -0
- package/package.json +3 -1
- 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 +630 -271
- package/plugins/severally/skills/_template/SKILL.md.tmpl +2 -1
- package/plugins/severally/skills/_template/reference.md.tmpl +2 -2
- package/plugins/severally/skills/antigravity/severally/SKILL.md +2 -1
- package/plugins/severally/skills/antigravity/severally/reference.md +2 -2
- package/plugins/severally/skills/claude/severally/SKILL.md +2 -1
- package/plugins/severally/skills/claude/severally/reference.md +2 -2
- package/plugins/severally/skills/codex/severally/SKILL.md +2 -1
- package/plugins/severally/skills/codex/severally/reference.md +2 -2
- package/plugins/severally/skills/opencode/severally/SKILL.md +2 -1
- package/plugins/severally/skills/opencode/severally/reference.md +2 -2
- package/scripts/build.mjs +57 -54
- package/scripts/doctor.mjs +15 -4
- package/scripts/init-config.mjs +2 -1
- package/scripts/install.mjs +73 -9
- package/scripts/live-check.mjs +11 -5
- package/scripts/live-mcp-check.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +24 -6
- package/src/adapters/antigravity.mjs +80 -17
- package/src/adapters/claude-code.mjs +49 -8
- package/src/adapters/codex.mjs +56 -6
- package/src/adapters/opencode-sandbox.mjs +20 -2
- package/src/adapters/opencode.mjs +115 -19
- package/src/brief.mjs +84 -7
- package/src/consultants.mjs +193 -27
- package/src/cross-spawn-escape.d.ts +7 -0
- package/src/export.mjs +50 -6
- package/src/expose-paths.mjs +149 -7
- package/src/failures.mjs +8 -1
- package/src/jobs.mjs +632 -152
- package/src/jsonc.mjs +11 -3
- package/src/parse-result.mjs +91 -14
- package/src/platform.mjs +52 -2
- package/src/policy.mjs +151 -28
- package/src/redact.mjs +22 -3
- package/src/result-schema.mjs +13 -0
- package/src/run.mjs +158 -12
- package/src/schema.mjs +84 -11
- package/src/server.mjs +49 -14
- package/src/status.mjs +16 -4
- package/src/store.mjs +287 -6
package/src/jsonc.mjs
CHANGED
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
// byte offsets -- and therefore JSON.parse's error positions -- still point at
|
|
12
12
|
// the right place in the original text.
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Remove // and /* *\/ comments and trailing commas, preserving offsets.
|
|
16
|
+
* @param {string} text
|
|
17
|
+
*/
|
|
15
18
|
export function stripJsonc(text) {
|
|
19
|
+
/** @type {string[]} */
|
|
16
20
|
const out = [];
|
|
17
21
|
let inString = false;
|
|
18
22
|
let i = 0;
|
|
@@ -28,7 +32,7 @@ export function stripJsonc(text) {
|
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
while (i < text.length) {
|
|
31
|
-
const c = text
|
|
35
|
+
const c = text.charAt(i);
|
|
32
36
|
const next = text[i + 1];
|
|
33
37
|
|
|
34
38
|
if (inString) {
|
|
@@ -78,7 +82,11 @@ export function stripJsonc(text) {
|
|
|
78
82
|
return out.join('');
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* JSON.parse, tolerating comments and trailing commas.
|
|
87
|
+
* @param {string} text
|
|
88
|
+
* @returns {unknown}
|
|
89
|
+
*/
|
|
82
90
|
export function parseJsonc(text) {
|
|
83
91
|
return JSON.parse(stripJsonc(text));
|
|
84
92
|
}
|
package/src/parse-result.mjs
CHANGED
|
@@ -8,6 +8,10 @@ import { RESULT_KEYS } from './result-schema.mjs';
|
|
|
8
8
|
import { redact } from './redact.mjs';
|
|
9
9
|
|
|
10
10
|
export class OutputError extends Error {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} message
|
|
13
|
+
* @param {string} [detail]
|
|
14
|
+
*/
|
|
11
15
|
constructor(message, detail) {
|
|
12
16
|
super(message);
|
|
13
17
|
this.name = 'OutputError';
|
|
@@ -17,6 +21,38 @@ export class OutputError extends Error {
|
|
|
17
21
|
|
|
18
22
|
const O = POLICY.output;
|
|
19
23
|
|
|
24
|
+
/** @typedef {'high' | 'medium' | 'low'} Level */
|
|
25
|
+
/** @typedef {'sufficient' | 'thin' | 'insufficient'} EvidenceBasis */
|
|
26
|
+
/** @typedef {(typeof STANCES)[number]} Stance */
|
|
27
|
+
/** @typedef {{ id: string, point: string, grounds: string | null, impact: string | null, severity: Level | null, confidence: Level | null }} Finding */
|
|
28
|
+
/** @typedef {{ option: string, tradeoffs: string | null, when_preferred: string | null }} Alternative */
|
|
29
|
+
/** @typedef {{ id: string, item: string, why_it_matters: string | null, how_to_obtain: string | null }} Unknown */
|
|
30
|
+
/** @typedef {{ condition: string, changes_to: string | null }} DecisionChanger */
|
|
31
|
+
/** @typedef {{ id: string, check: string, method: string | null, expected_signal: string | null }} NextCheck */
|
|
32
|
+
/** @typedef {{ topic: string, your_position: string | null, why_unresolved: string | null }} RemainingDisagreement */
|
|
33
|
+
/** @typedef {{ title: string | null, url: string | null, relevance: string | null }} Reference */
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The validated consult result, in the shape of CONSULT_RESULT_SCHEMA.
|
|
37
|
+
* @typedef {object} ConsultResult
|
|
38
|
+
* @property {string} summary
|
|
39
|
+
* @property {Level | null} confidence
|
|
40
|
+
* @property {EvidenceBasis | null} evidence_basis
|
|
41
|
+
* @property {Stance | null} stance
|
|
42
|
+
* @property {Finding[]} findings
|
|
43
|
+
* @property {Alternative[]} alternatives
|
|
44
|
+
* @property {Unknown[]} unknowns
|
|
45
|
+
* @property {DecisionChanger[]} decision_changers
|
|
46
|
+
* @property {NextCheck[]} next_checks
|
|
47
|
+
* @property {RemainingDisagreement[]} remaining_disagreements
|
|
48
|
+
* @property {Reference[]} references
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {unknown} v
|
|
53
|
+
* @param {number} max
|
|
54
|
+
* @returns {string | null}
|
|
55
|
+
*/
|
|
20
56
|
function clampText(v, max) {
|
|
21
57
|
if (typeof v !== 'string') return null;
|
|
22
58
|
const t = v.trim();
|
|
@@ -24,34 +60,61 @@ function clampText(v, max) {
|
|
|
24
60
|
return t.length > max ? `${t.slice(0, max)}\n…[truncated by severally]` : t;
|
|
25
61
|
}
|
|
26
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Narrows a string to one of the listed values.
|
|
65
|
+
* @template {string} T
|
|
66
|
+
* @param {readonly T[]} list
|
|
67
|
+
* @param {string} t
|
|
68
|
+
* @returns {t is T}
|
|
69
|
+
*/
|
|
70
|
+
const oneOf = (list, t) => list.some((x) => x === t);
|
|
71
|
+
|
|
72
|
+
/** @type {Level[]} */
|
|
27
73
|
const LEVELS = ['high', 'medium', 'low'];
|
|
74
|
+
/** @type {EvidenceBasis[]} */
|
|
28
75
|
const BASIS = ['sufficient', 'thin', 'insufficient'];
|
|
29
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @param {unknown} v
|
|
79
|
+
* @param {Level | null} [fallback]
|
|
80
|
+
* @returns {Level | null}
|
|
81
|
+
*/
|
|
30
82
|
function level(v, fallback = null) {
|
|
31
83
|
const t = typeof v === 'string' ? v.trim().toLowerCase() : '';
|
|
32
|
-
return LEVELS
|
|
84
|
+
return oneOf(LEVELS, t) ? t : fallback;
|
|
33
85
|
}
|
|
34
86
|
|
|
87
|
+
/**
|
|
88
|
+
* @param {unknown} v
|
|
89
|
+
* @returns {EvidenceBasis | null}
|
|
90
|
+
*/
|
|
35
91
|
function basis(v) {
|
|
36
92
|
const t = typeof v === 'string' ? v.trim().toLowerCase() : '';
|
|
37
|
-
return BASIS
|
|
93
|
+
return oneOf(BASIS, t) ? t : null;
|
|
38
94
|
}
|
|
39
95
|
|
|
96
|
+
/**
|
|
97
|
+
* @param {unknown} v
|
|
98
|
+
* @returns {Stance | null}
|
|
99
|
+
*/
|
|
40
100
|
function stance(v) {
|
|
41
101
|
const t = typeof v === 'string' ? v.trim().toLowerCase() : '';
|
|
42
|
-
return STANCES
|
|
102
|
+
return oneOf(STANCES, t) ? t : null;
|
|
43
103
|
}
|
|
44
104
|
|
|
45
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Extract a JSON object from raw model text (fenced block, or first balanced object).
|
|
107
|
+
* @param {unknown} text
|
|
108
|
+
* @returns {Record<string, unknown> | null}
|
|
109
|
+
*/
|
|
46
110
|
export function extractJson(text) {
|
|
47
111
|
if (typeof text !== 'string') return null;
|
|
48
112
|
const trimmed = text.trim();
|
|
49
113
|
if (!trimmed) return null;
|
|
114
|
+
/** @type {string[]} */
|
|
50
115
|
const candidates = [];
|
|
51
116
|
if (trimmed.startsWith('{')) candidates.push(trimmed);
|
|
52
|
-
const
|
|
53
|
-
let m;
|
|
54
|
-
while ((m = fence.exec(trimmed)) !== null) candidates.push(m[1].trim());
|
|
117
|
+
for (const [, body = ''] of trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) candidates.push(body.trim());
|
|
55
118
|
// Longest balanced {...} run as a last resort.
|
|
56
119
|
const start = trimmed.indexOf('{');
|
|
57
120
|
if (start !== -1) {
|
|
@@ -98,24 +161,34 @@ export function extractJson(text) {
|
|
|
98
161
|
// verdict against one point later. The id is ours, assigned at parse time and
|
|
99
162
|
// never read from the answer: a consultant that numbered its own findings
|
|
100
163
|
// cannot make one of them impersonate another.
|
|
164
|
+
/**
|
|
165
|
+
* @template T
|
|
166
|
+
* @param {unknown} raw
|
|
167
|
+
* @param {Record<string, 'text' | 'level'>} fields
|
|
168
|
+
* @param {{ requiredField: string, idPrefix?: string | null }} options
|
|
169
|
+
* @returns {T[]}
|
|
170
|
+
*/
|
|
101
171
|
function mapList(raw, fields, { requiredField, idPrefix = null }) {
|
|
102
172
|
if (!Array.isArray(raw)) return [];
|
|
173
|
+
/** @type {T[]} */
|
|
103
174
|
const out = [];
|
|
104
175
|
for (const entry of raw.slice(0, O.listMax)) {
|
|
105
176
|
if (!entry || typeof entry !== 'object') continue;
|
|
177
|
+
/** @type {Record<string, unknown>} */
|
|
106
178
|
const item = idPrefix ? { id: `${idPrefix}${out.length + 1}` } : {};
|
|
107
179
|
for (const [key, kind] of Object.entries(fields)) {
|
|
108
180
|
item[key] = kind === 'level' ? level(entry[key]) : clampText(entry[key], O.itemTextMax);
|
|
109
181
|
}
|
|
110
182
|
if (!item[requiredField]) continue;
|
|
111
|
-
|
|
183
|
+
// Every key of `fields` is a key of T, and the required one is set.
|
|
184
|
+
out.push(/** @type {T} */ (item));
|
|
112
185
|
}
|
|
113
186
|
return out;
|
|
114
187
|
}
|
|
115
188
|
|
|
116
189
|
/**
|
|
117
|
-
* @param {
|
|
118
|
-
* @returns {{result:
|
|
190
|
+
* @param {Record<string, unknown> | string} raw parsed JSON object, or raw text to mine one out of
|
|
191
|
+
* @returns {{result: ConsultResult, quality: {missing_sections: string[], findings_without_grounds: number, unsourced: boolean, list_truncated?: Record<string, number>}}}
|
|
119
192
|
*/
|
|
120
193
|
export function normalizeResult(raw) {
|
|
121
194
|
const objIn = typeof raw === 'string' ? extractJson(raw) : raw;
|
|
@@ -129,6 +202,7 @@ export function normalizeResult(raw) {
|
|
|
129
202
|
throw new OutputError('consultant response has no usable "summary" field', JSON.stringify(src).slice(0, 2000));
|
|
130
203
|
}
|
|
131
204
|
|
|
205
|
+
/** @type {ConsultResult} */
|
|
132
206
|
const result = {
|
|
133
207
|
summary,
|
|
134
208
|
confidence: level(src.confidence),
|
|
@@ -202,13 +276,16 @@ export function normalizeResult(raw) {
|
|
|
202
276
|
// The lists are cut to O.listMax before anything else looks at them; say how
|
|
203
277
|
// long the ones that were cut had been, so the cut is not silent.
|
|
204
278
|
const listTruncated = Object.fromEntries(
|
|
205
|
-
['findings', 'alternatives', 'unknowns', 'decision_changers', 'next_checks', 'remaining_disagreements']
|
|
206
|
-
|
|
207
|
-
.
|
|
279
|
+
['findings', 'alternatives', 'unknowns', 'decision_changers', 'next_checks', 'remaining_disagreements'].flatMap((k) => {
|
|
280
|
+
const v = src[k];
|
|
281
|
+
return Array.isArray(v) && v.length > O.listMax ? [[k, v.length]] : [];
|
|
282
|
+
}),
|
|
208
283
|
);
|
|
209
284
|
|
|
285
|
+
/** @type {Record<string, unknown>} */
|
|
286
|
+
const fields = result;
|
|
210
287
|
const missing = RESULT_KEYS.filter((k) => {
|
|
211
|
-
const v =
|
|
288
|
+
const v = fields[k];
|
|
212
289
|
return v === null || v === undefined || (Array.isArray(v) && v.length === 0);
|
|
213
290
|
});
|
|
214
291
|
|
package/src/platform.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference path="./cross-spawn-escape.d.ts" />
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import which from 'which';
|
|
@@ -5,12 +6,24 @@ import spawn from 'cross-spawn';
|
|
|
5
6
|
import escape from 'cross-spawn/lib/util/escape.js';
|
|
6
7
|
import { spawn as nativeSpawn, spawnSync as nativeSpawnSync } from 'node:child_process';
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} source
|
|
11
|
+
* @param {string} destination
|
|
12
|
+
*/
|
|
8
13
|
export function copyTree(source, destination) {
|
|
9
14
|
// Node 22.23.2's native cpSync fast path corrupts non-ASCII Windows paths.
|
|
10
15
|
// A filter selects its JS traversal, preserving Japanese directory names.
|
|
11
16
|
fs.cpSync(source, destination, { recursive: true, filter: () => true });
|
|
12
17
|
}
|
|
13
18
|
|
|
19
|
+
/** @typedef {import('node:child_process').SpawnOptions & { cwd?: string | undefined, env?: NodeJS.ProcessEnv | undefined }} SpawnOpts */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @template {SpawnOpts} O
|
|
23
|
+
* @param {string} command
|
|
24
|
+
* @param {readonly string[]} args
|
|
25
|
+
* @param {O} options
|
|
26
|
+
*/
|
|
14
27
|
function batchInvocation(command, args, options) {
|
|
15
28
|
if (process.platform !== 'win32') return null;
|
|
16
29
|
const resolved = findCommand(command, options);
|
|
@@ -26,17 +39,44 @@ function batchInvocation(command, args, options) {
|
|
|
26
39
|
};
|
|
27
40
|
}
|
|
28
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @overload
|
|
44
|
+
* @param {string} command
|
|
45
|
+
* @param {readonly string[]} args
|
|
46
|
+
* @param {SpawnOpts & { stdio: ['pipe', 'pipe', 'pipe'] }} options
|
|
47
|
+
* @returns {import('node:child_process').ChildProcessWithoutNullStreams}
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* @overload
|
|
51
|
+
* @param {string} command
|
|
52
|
+
* @param {readonly string[]} args
|
|
53
|
+
* @param {SpawnOpts} [options]
|
|
54
|
+
* @returns {import('node:child_process').ChildProcess}
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} command
|
|
58
|
+
* @param {readonly string[]} args
|
|
59
|
+
* @param {SpawnOpts} [options]
|
|
60
|
+
*/
|
|
29
61
|
export function spawnCommand(command, args, options = {}) {
|
|
30
62
|
const batch = batchInvocation(command, args, options);
|
|
31
63
|
return batch ? nativeSpawn(batch.command, batch.args, batch.options) : spawn(command, args, options);
|
|
32
64
|
}
|
|
33
65
|
|
|
34
66
|
// Explicit script paths are supported by cross-spawn's shebang handling too.
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} command
|
|
69
|
+
* @param {{ cwd?: string | undefined, env?: NodeJS.ProcessEnv | undefined }} [options]
|
|
70
|
+
* @returns {string | null}
|
|
71
|
+
*/
|
|
35
72
|
export function findCommand(command, { cwd = process.cwd(), env = process.env } = {}) {
|
|
36
73
|
if (typeof command !== 'string' || !command) return null;
|
|
37
74
|
cwd = path.resolve(cwd);
|
|
38
75
|
const windows = process.platform === 'win32';
|
|
39
|
-
const value = (
|
|
76
|
+
const value = (/** @type {string} */ name) => {
|
|
77
|
+
const key = windows ? Object.keys(env).find((k) => k.toUpperCase() === name) : name;
|
|
78
|
+
return key === undefined ? undefined : env[key];
|
|
79
|
+
};
|
|
40
80
|
const explicit = command.includes('/') || (windows && command.includes('\\'));
|
|
41
81
|
const dirs = explicit ? [cwd] : [...(windows ? [cwd] : []), ...(value('PATH') ?? '').split(path.delimiter).filter(Boolean)];
|
|
42
82
|
for (const dir of dirs) {
|
|
@@ -59,8 +99,18 @@ export function findCommand(command, { cwd = process.cwd(), env = process.env }
|
|
|
59
99
|
return null;
|
|
60
100
|
}
|
|
61
101
|
|
|
102
|
+
/** @typedef {Omit<import('node:child_process').SpawnSyncOptionsWithStringEncoding, 'cwd' | 'encoding'> & { cwd?: string, encoding?: BufferEncoding }} ExecOptions */
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} command
|
|
106
|
+
* @param {readonly string[]} args
|
|
107
|
+
* @param {ExecOptions} [options]
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
62
110
|
export function execCommandSync(command, args, options = {}) {
|
|
63
|
-
|
|
111
|
+
/** @type {BufferEncoding} */
|
|
112
|
+
const encoding = 'utf8';
|
|
113
|
+
const opts = { windowsHide: true, encoding, ...options };
|
|
64
114
|
const batch = batchInvocation(command, args, opts);
|
|
65
115
|
const result = batch ? nativeSpawnSync(batch.command, batch.args, batch.options) : spawn.sync(command, args, opts);
|
|
66
116
|
if (result.error) throw result.error;
|