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/store.mjs
CHANGED
|
@@ -12,7 +12,11 @@ export function ensureDirs() {
|
|
|
12
12
|
fs.mkdirSync(path.join(home, 'history'), { recursive: true, mode: 0o700 });
|
|
13
13
|
fs.mkdirSync(path.join(home, 'jobs'), { recursive: true, mode: 0o700 });
|
|
14
14
|
fs.mkdirSync(path.join(home, 'homes'), { recursive: true, mode: 0o700 });
|
|
15
|
-
try {
|
|
15
|
+
try {
|
|
16
|
+
fs.chmodSync(home, 0o700);
|
|
17
|
+
} catch {
|
|
18
|
+
/* best effort */
|
|
19
|
+
}
|
|
16
20
|
return home;
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -45,7 +49,11 @@ export function writeJobArtifact(jobId, name, content) {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
export function readIfExists(p) {
|
|
48
|
-
try {
|
|
52
|
+
try {
|
|
53
|
+
return fs.readFileSync(p, 'utf8');
|
|
54
|
+
} catch {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
// A consultation from an earlier server session, found through the index and
|
|
@@ -58,24 +66,34 @@ function indexRow(jobId) {
|
|
|
58
66
|
try {
|
|
59
67
|
const parsed = JSON.parse(line);
|
|
60
68
|
if (parsed.job_id === jobId) return parsed;
|
|
61
|
-
} catch {
|
|
69
|
+
} catch {
|
|
70
|
+
/* a damaged line is skipped, not fatal */
|
|
71
|
+
}
|
|
62
72
|
}
|
|
63
73
|
return null;
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
/** Every row of the history index, in the order written; a damaged line is skipped. */
|
|
67
77
|
export function indexRows() {
|
|
68
|
-
return readIfExists(path.join(POLICY.home, 'history', 'index.jsonl'))
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
return readIfExists(path.join(POLICY.home, 'history', 'index.jsonl'))
|
|
79
|
+
.split('\n')
|
|
80
|
+
.flatMap((line) => {
|
|
81
|
+
try {
|
|
82
|
+
const row = line.trim() ? JSON.parse(line) : null;
|
|
83
|
+
return row && typeof row === 'object' && JOB_ID.test(row.job_id) ? [row] : [];
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
});
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
/** The round file an index row points at, or null. */
|
|
77
91
|
export function loadRoundAt(row) {
|
|
78
|
-
try {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(fs.readFileSync(roundPath(row.chain_id, row.round), 'utf8'));
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
export const roundPath = (chainId, round) =>
|
|
@@ -92,7 +110,11 @@ export function writeDiagnostics(chainId, round, text) {
|
|
|
92
110
|
export function loadRound(jobId) {
|
|
93
111
|
const row = indexRow(jobId);
|
|
94
112
|
if (!row) return null;
|
|
95
|
-
try {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(fs.readFileSync(roundPath(row.chain_id, row.round), 'utf8'));
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
96
118
|
}
|
|
97
119
|
|
|
98
120
|
// Every round file of one chain, oldest first: a follow-up to a consultation
|
|
@@ -100,9 +122,20 @@ export function loadRound(jobId) {
|
|
|
100
122
|
export function loadChain(chainId) {
|
|
101
123
|
const dir = path.join(POLICY.home, 'history', chainId);
|
|
102
124
|
let names = [];
|
|
103
|
-
try {
|
|
125
|
+
try {
|
|
126
|
+
names = fs
|
|
127
|
+
.readdirSync(dir)
|
|
128
|
+
.filter((n) => /^round-\d+\.json$/.test(n))
|
|
129
|
+
.sort();
|
|
130
|
+
} catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
104
133
|
return names.flatMap((n) => {
|
|
105
|
-
try {
|
|
134
|
+
try {
|
|
135
|
+
return [JSON.parse(fs.readFileSync(path.join(dir, n), 'utf8'))];
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
106
139
|
});
|
|
107
140
|
}
|
|
108
141
|
|
|
@@ -169,18 +202,31 @@ export function writeRunning(record) {
|
|
|
169
202
|
|
|
170
203
|
export function loadRunning(jobId) {
|
|
171
204
|
if (!JOB_ID.test(jobId)) return null;
|
|
172
|
-
try {
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(fs.readFileSync(runningFile(jobId), 'utf8'));
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
173
210
|
}
|
|
174
211
|
|
|
175
212
|
export function listRunning() {
|
|
176
213
|
try {
|
|
177
|
-
return fs
|
|
178
|
-
|
|
214
|
+
return fs
|
|
215
|
+
.readdirSync(runningDir())
|
|
216
|
+
.map((n) => n.replace(/\.json$/, ''))
|
|
217
|
+
.filter((id) => JOB_ID.test(id));
|
|
218
|
+
} catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
179
221
|
}
|
|
180
222
|
|
|
181
223
|
export function removeRunning(jobId) {
|
|
182
224
|
if (!JOB_ID.test(jobId)) return;
|
|
183
|
-
try {
|
|
225
|
+
try {
|
|
226
|
+
fs.rmSync(runningFile(jobId), { force: true });
|
|
227
|
+
} catch {
|
|
228
|
+
/* best effort */
|
|
229
|
+
}
|
|
184
230
|
}
|
|
185
231
|
|
|
186
232
|
// Seats: the concurrency cap is shared by every severally process on one
|
|
@@ -198,16 +244,31 @@ function withSeatLock(fn) {
|
|
|
198
244
|
fs.mkdirSync(seatsDir(), { recursive: true, mode: 0o700 });
|
|
199
245
|
const until = Date.now() + LOCK_WAIT_MS;
|
|
200
246
|
for (;;) {
|
|
201
|
-
try {
|
|
247
|
+
try {
|
|
248
|
+
fs.mkdirSync(lock);
|
|
249
|
+
break;
|
|
250
|
+
} catch (err) {
|
|
202
251
|
if (err.code !== 'EEXIST') throw err;
|
|
203
252
|
// Held for a few file operations at most: one this old was left by a
|
|
204
253
|
// process that died holding it.
|
|
205
|
-
try {
|
|
254
|
+
try {
|
|
255
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) fs.rmdirSync(lock);
|
|
256
|
+
} catch {
|
|
257
|
+
/* gone already */
|
|
258
|
+
}
|
|
206
259
|
if (Date.now() > until) throw new Error('the seat lock stayed taken; try again');
|
|
207
260
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
208
261
|
}
|
|
209
262
|
}
|
|
210
|
-
try {
|
|
263
|
+
try {
|
|
264
|
+
return fn();
|
|
265
|
+
} finally {
|
|
266
|
+
try {
|
|
267
|
+
fs.rmdirSync(lock);
|
|
268
|
+
} catch {
|
|
269
|
+
/* best effort */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
211
272
|
}
|
|
212
273
|
|
|
213
274
|
/** Takes one seat per entry of `seats` ({ job_id, owner }), all or none. */
|
|
@@ -217,14 +278,28 @@ export function takeSeats(seats, cap, held) {
|
|
|
217
278
|
const names = fs.readdirSync(seatsDir());
|
|
218
279
|
// A cancel asked for after its consultation ended has no seat left to go with.
|
|
219
280
|
for (const name of names.filter((n) => n.endsWith('.cancel'))) {
|
|
220
|
-
if (!names.includes(name.replace(/\.cancel$/, '.json')))
|
|
281
|
+
if (!names.includes(name.replace(/\.cancel$/, '.json')))
|
|
282
|
+
try {
|
|
283
|
+
fs.rmSync(path.join(seatsDir(), name), { force: true });
|
|
284
|
+
} catch {
|
|
285
|
+
/* best effort */
|
|
286
|
+
}
|
|
221
287
|
}
|
|
222
288
|
for (const name of names) {
|
|
223
289
|
if (!name.endsWith('.json')) continue;
|
|
224
290
|
let seat = null;
|
|
225
|
-
try {
|
|
291
|
+
try {
|
|
292
|
+
seat = JSON.parse(fs.readFileSync(path.join(seatsDir(), name), 'utf8'));
|
|
293
|
+
} catch {
|
|
294
|
+
/* half-written or damaged: free */
|
|
295
|
+
}
|
|
226
296
|
if (seat && held(seat)) live.push(seat);
|
|
227
|
-
else
|
|
297
|
+
else
|
|
298
|
+
try {
|
|
299
|
+
fs.rmSync(path.join(seatsDir(), name), { force: true });
|
|
300
|
+
} catch {
|
|
301
|
+
/* best effort */
|
|
302
|
+
}
|
|
228
303
|
}
|
|
229
304
|
// A chain runs one round at a time, across processes: a live seat on the
|
|
230
305
|
// same chain, or a round file already at the number chosen, means another
|
|
@@ -232,7 +307,11 @@ export function takeSeats(seats, cap, held) {
|
|
|
232
307
|
for (const mine of seats.filter((x) => x.chain_id)) {
|
|
233
308
|
let other = live.find((x) => x.chain_id === mine.chain_id);
|
|
234
309
|
if (!other && fs.existsSync(roundPath(mine.chain_id, mine.round))) {
|
|
235
|
-
try {
|
|
310
|
+
try {
|
|
311
|
+
other = { job_id: JSON.parse(fs.readFileSync(roundPath(mine.chain_id, mine.round), 'utf8')).job_id, round: mine.round };
|
|
312
|
+
} catch {
|
|
313
|
+
other = { job_id: '(unreadable)', round: mine.round };
|
|
314
|
+
}
|
|
236
315
|
}
|
|
237
316
|
if (other) return { ok: false, chainBusy: { live: other, mine } };
|
|
238
317
|
}
|
|
@@ -249,7 +328,11 @@ export function takeSeats(seats, cap, held) {
|
|
|
249
328
|
|
|
250
329
|
export function releaseSeat(jobId) {
|
|
251
330
|
if (!JOB_ID.test(jobId)) return;
|
|
252
|
-
try {
|
|
331
|
+
try {
|
|
332
|
+
fs.rmSync(seatFile(jobId), { force: true });
|
|
333
|
+
} catch {
|
|
334
|
+
/* best effort */
|
|
335
|
+
}
|
|
253
336
|
clearCancel(jobId);
|
|
254
337
|
}
|
|
255
338
|
|
|
@@ -258,16 +341,31 @@ export function releaseSeat(jobId) {
|
|
|
258
341
|
const cancelFile = (jobId) => path.join(seatsDir(), `${jobId}.cancel`);
|
|
259
342
|
export function requestCancel(jobId) {
|
|
260
343
|
if (!JOB_ID.test(jobId)) return false;
|
|
261
|
-
try {
|
|
344
|
+
try {
|
|
345
|
+
fs.mkdirSync(seatsDir(), { recursive: true, mode: 0o700 });
|
|
346
|
+
fs.writeFileSync(cancelFile(jobId), '', { mode: 0o600 });
|
|
347
|
+
return true;
|
|
348
|
+
} catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
262
351
|
}
|
|
263
352
|
export const cancelRequested = (jobId) => JOB_ID.test(jobId) && fs.existsSync(cancelFile(jobId));
|
|
264
353
|
export function clearCancel(jobId) {
|
|
265
|
-
if (JOB_ID.test(jobId))
|
|
354
|
+
if (JOB_ID.test(jobId))
|
|
355
|
+
try {
|
|
356
|
+
fs.rmSync(cancelFile(jobId), { force: true });
|
|
357
|
+
} catch {
|
|
358
|
+
/* best effort */
|
|
359
|
+
}
|
|
266
360
|
}
|
|
267
361
|
|
|
268
362
|
export function loadGroup(groupId) {
|
|
269
363
|
if (!GROUP_ID.test(groupId)) return null;
|
|
270
|
-
try {
|
|
364
|
+
try {
|
|
365
|
+
return JSON.parse(fs.readFileSync(path.join(POLICY.home, 'history', 'groups', `${groupId}.json`), 'utf8'));
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
271
369
|
}
|
|
272
370
|
|
|
273
371
|
// A fan-out is recorded as its own record so a later reader can tell which
|
|
@@ -281,6 +379,10 @@ export function persistGroup(group) {
|
|
|
281
379
|
|
|
282
380
|
export function cleanupJobDir(jobId) {
|
|
283
381
|
for (const dir of [jobDir(jobId), sandboxHome(jobId)]) {
|
|
284
|
-
try {
|
|
382
|
+
try {
|
|
383
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
384
|
+
} catch {
|
|
385
|
+
/* best effort */
|
|
386
|
+
}
|
|
285
387
|
}
|
|
286
388
|
}
|