severally-mcp 2.1.0 → 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/CODE_OF_CONDUCT.md +83 -0
- package/CONFIG.md +10 -6
- package/README.ja.md +146 -69
- package/README.md +150 -73
- package/bin/severally-mcp.mjs +17 -2
- package/config.example.json +26 -0
- package/docs/how-it-works.ja.md +4 -2
- package/docs/how-it-works.md +5 -3
- package/docs/install.ja.md +59 -24
- package/docs/install.md +60 -24
- package/package.json +16 -4
- 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 +29220 -27764
- package/plugins/severally/skills/_template/SKILL.md.tmpl +16 -22
- package/plugins/severally/skills/_template/reference.md.tmpl +11 -5
- package/plugins/severally/skills/antigravity/severally/SKILL.md +16 -22
- package/plugins/severally/skills/antigravity/severally/reference.md +11 -5
- package/plugins/severally/skills/claude/severally/SKILL.md +16 -22
- package/plugins/severally/skills/claude/severally/reference.md +11 -5
- package/plugins/severally/skills/codex/severally/SKILL.md +16 -22
- package/plugins/severally/skills/codex/severally/reference.md +11 -5
- package/plugins/severally/skills/opencode/severally/SKILL.md +16 -22
- package/plugins/severally/skills/opencode/severally/reference.md +11 -5
- package/scripts/build.mjs +31 -26
- package/scripts/doctor.mjs +52 -17
- package/scripts/init-config.mjs +8 -6
- package/scripts/install.mjs +342 -84
- 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 +99 -30
- package/src/export.mjs +49 -12
- package/src/expose-paths.mjs +162 -44
- package/src/failures.mjs +23 -13
- package/src/jobs.mjs +420 -140
- package/src/parse-result.mjs +85 -30
- package/src/platform.mjs +5 -7
- package/src/policy.mjs +139 -32
- package/src/redact.mjs +38 -14
- package/src/run.mjs +104 -22
- package/src/schema.mjs +76 -22
- package/src/server.mjs +176 -166
- package/src/status.mjs +132 -0
- package/src/store.mjs +164 -19
package/src/status.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
// 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';
|
|
14
|
+
import { SEVERALLY_VERSION, staleInstall } from './server.mjs';
|
|
15
|
+
import { findCommand, execCommandSync } from './platform.mjs';
|
|
16
|
+
|
|
17
|
+
// How each client lists severally, plugin installs first. A command that exits 0
|
|
18
|
+
// and names severally counts as registered.
|
|
19
|
+
const CLIENTS = [
|
|
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
|
+
},
|
|
36
|
+
{ label: 'Antigravity', cmd: 'agy', checks: [['mcp', 'list']] },
|
|
37
|
+
{ label: 'OpenCode', cmd: 'opencode', checks: [['mcp', 'list']] },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// "registered": the client's own listing names severally (it may still be
|
|
41
|
+
// turned off there). "not registered": the client answered, without it.
|
|
42
|
+
// No answer at all (a timeout, a failure to start): unknown.
|
|
43
|
+
function registration({ cmd, checks }) {
|
|
44
|
+
if (!findCommand(cmd)) return 'not installed';
|
|
45
|
+
let ran = false;
|
|
46
|
+
for (const args of checks) {
|
|
47
|
+
try {
|
|
48
|
+
const out = execCommandSync(cmd, args, { encoding: 'utf8', timeout: 20_000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
49
|
+
ran = true;
|
|
50
|
+
if (/(^|[\s@:])severally([\s@:]|$)/m.test(out)) return 'registered (listed by the client)';
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// It ran and said no (`mcp get` of a missing server exits non-zero); a
|
|
53
|
+
// timeout or a failure to start says nothing either way.
|
|
54
|
+
if (typeof err.status === 'number') ran = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return ran ? 'not registered' : `could not tell (${cmd} ${checks.map((a) => a.join(' ')).join(' / ')} failed)`;
|
|
58
|
+
}
|
|
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
|
+
|
|
100
|
+
export function status() {
|
|
101
|
+
const lines = [`severally ${SEVERALLY_VERSION}`, ''];
|
|
102
|
+
const problem = configProblem();
|
|
103
|
+
lines.push(`config: ${problem ? `needs fixing, every consultation is refused until it is -- ${problem}` : 'ok'}`);
|
|
104
|
+
for (const w of configWarnings()) lines.push(` mistake (only that consultant or CLI is out of use): ${w}`);
|
|
105
|
+
lines.push(`network: ${POLICY.network === 'off' ? 'off for every consultation (server.network)' : 'per request'}`, '');
|
|
106
|
+
|
|
107
|
+
lines.push('consultants:');
|
|
108
|
+
for (const c of POLICY.consultants) {
|
|
109
|
+
lines.push(
|
|
110
|
+
` ${c.name}: ${c.cli}, ${c.model} ${consultantAvailable(c) ? 'available' : `not available: ${unavailableReason(c)}`}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
for (const name of brokenConsultantNames()) lines.push(` ${name}: not available: ${brokenConsultant(name)}`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
|
|
116
|
+
const stale = staleInstall();
|
|
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
|
+
);
|
|
125
|
+
|
|
126
|
+
lines.push('clients:');
|
|
127
|
+
for (const client of CLIENTS) lines.push(` ${client.label.padEnd(12)} ${registration(client)}`);
|
|
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)}`);
|
|
130
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
131
|
+
return problem ? 1 : 0;
|
|
132
|
+
}
|
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,11 +66,36 @@ 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
|
|
|
76
|
+
/** Every row of the history index, in the order written; a damaged line is skipped. */
|
|
77
|
+
export function indexRows() {
|
|
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
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The round file an index row points at, or null. */
|
|
91
|
+
export function loadRoundAt(row) {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(fs.readFileSync(roundPath(row.chain_id, row.round), 'utf8'));
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
export const roundPath = (chainId, round) =>
|
|
67
100
|
path.join(POLICY.home, 'history', chainId, `round-${String(round).padStart(2, '0')}.json`);
|
|
68
101
|
|
|
@@ -77,7 +110,11 @@ export function writeDiagnostics(chainId, round, text) {
|
|
|
77
110
|
export function loadRound(jobId) {
|
|
78
111
|
const row = indexRow(jobId);
|
|
79
112
|
if (!row) return null;
|
|
80
|
-
try {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(fs.readFileSync(roundPath(row.chain_id, row.round), 'utf8'));
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
// Every round file of one chain, oldest first: a follow-up to a consultation
|
|
@@ -85,9 +122,20 @@ export function loadRound(jobId) {
|
|
|
85
122
|
export function loadChain(chainId) {
|
|
86
123
|
const dir = path.join(POLICY.home, 'history', chainId);
|
|
87
124
|
let names = [];
|
|
88
|
-
try {
|
|
125
|
+
try {
|
|
126
|
+
names = fs
|
|
127
|
+
.readdirSync(dir)
|
|
128
|
+
.filter((n) => /^round-\d+\.json$/.test(n))
|
|
129
|
+
.sort();
|
|
130
|
+
} catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
89
133
|
return names.flatMap((n) => {
|
|
90
|
-
try {
|
|
134
|
+
try {
|
|
135
|
+
return [JSON.parse(fs.readFileSync(path.join(dir, n), 'utf8'))];
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
91
139
|
});
|
|
92
140
|
}
|
|
93
141
|
|
|
@@ -154,18 +202,31 @@ export function writeRunning(record) {
|
|
|
154
202
|
|
|
155
203
|
export function loadRunning(jobId) {
|
|
156
204
|
if (!JOB_ID.test(jobId)) return null;
|
|
157
|
-
try {
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(fs.readFileSync(runningFile(jobId), 'utf8'));
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
158
210
|
}
|
|
159
211
|
|
|
160
212
|
export function listRunning() {
|
|
161
213
|
try {
|
|
162
|
-
return fs
|
|
163
|
-
|
|
214
|
+
return fs
|
|
215
|
+
.readdirSync(runningDir())
|
|
216
|
+
.map((n) => n.replace(/\.json$/, ''))
|
|
217
|
+
.filter((id) => JOB_ID.test(id));
|
|
218
|
+
} catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
164
221
|
}
|
|
165
222
|
|
|
166
223
|
export function removeRunning(jobId) {
|
|
167
224
|
if (!JOB_ID.test(jobId)) return;
|
|
168
|
-
try {
|
|
225
|
+
try {
|
|
226
|
+
fs.rmSync(runningFile(jobId), { force: true });
|
|
227
|
+
} catch {
|
|
228
|
+
/* best effort */
|
|
229
|
+
}
|
|
169
230
|
}
|
|
170
231
|
|
|
171
232
|
// Seats: the concurrency cap is shared by every severally process on one
|
|
@@ -183,28 +244,76 @@ function withSeatLock(fn) {
|
|
|
183
244
|
fs.mkdirSync(seatsDir(), { recursive: true, mode: 0o700 });
|
|
184
245
|
const until = Date.now() + LOCK_WAIT_MS;
|
|
185
246
|
for (;;) {
|
|
186
|
-
try {
|
|
247
|
+
try {
|
|
248
|
+
fs.mkdirSync(lock);
|
|
249
|
+
break;
|
|
250
|
+
} catch (err) {
|
|
187
251
|
if (err.code !== 'EEXIST') throw err;
|
|
188
252
|
// Held for a few file operations at most: one this old was left by a
|
|
189
253
|
// process that died holding it.
|
|
190
|
-
try {
|
|
254
|
+
try {
|
|
255
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) fs.rmdirSync(lock);
|
|
256
|
+
} catch {
|
|
257
|
+
/* gone already */
|
|
258
|
+
}
|
|
191
259
|
if (Date.now() > until) throw new Error('the seat lock stayed taken; try again');
|
|
192
260
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
193
261
|
}
|
|
194
262
|
}
|
|
195
|
-
try {
|
|
263
|
+
try {
|
|
264
|
+
return fn();
|
|
265
|
+
} finally {
|
|
266
|
+
try {
|
|
267
|
+
fs.rmdirSync(lock);
|
|
268
|
+
} catch {
|
|
269
|
+
/* best effort */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
196
272
|
}
|
|
197
273
|
|
|
198
274
|
/** Takes one seat per entry of `seats` ({ job_id, owner }), all or none. */
|
|
199
275
|
export function takeSeats(seats, cap, held) {
|
|
200
276
|
return withSeatLock(() => {
|
|
201
277
|
const live = [];
|
|
202
|
-
|
|
278
|
+
const names = fs.readdirSync(seatsDir());
|
|
279
|
+
// A cancel asked for after its consultation ended has no seat left to go with.
|
|
280
|
+
for (const name of names.filter((n) => n.endsWith('.cancel'))) {
|
|
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
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (const name of names) {
|
|
203
289
|
if (!name.endsWith('.json')) continue;
|
|
204
290
|
let seat = null;
|
|
205
|
-
try {
|
|
291
|
+
try {
|
|
292
|
+
seat = JSON.parse(fs.readFileSync(path.join(seatsDir(), name), 'utf8'));
|
|
293
|
+
} catch {
|
|
294
|
+
/* half-written or damaged: free */
|
|
295
|
+
}
|
|
206
296
|
if (seat && held(seat)) live.push(seat);
|
|
207
|
-
else
|
|
297
|
+
else
|
|
298
|
+
try {
|
|
299
|
+
fs.rmSync(path.join(seatsDir(), name), { force: true });
|
|
300
|
+
} catch {
|
|
301
|
+
/* best effort */
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// A chain runs one round at a time, across processes: a live seat on the
|
|
305
|
+
// same chain, or a round file already at the number chosen, means another
|
|
306
|
+
// server continued it first.
|
|
307
|
+
for (const mine of seats.filter((x) => x.chain_id)) {
|
|
308
|
+
let other = live.find((x) => x.chain_id === mine.chain_id);
|
|
309
|
+
if (!other && fs.existsSync(roundPath(mine.chain_id, mine.round))) {
|
|
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
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (other) return { ok: false, chainBusy: { live: other, mine } };
|
|
208
317
|
}
|
|
209
318
|
if (live.length + seats.length > cap) {
|
|
210
319
|
return { ok: false, live: live.length, elsewhere: live.filter((x) => x.owner?.pid !== process.pid).length };
|
|
@@ -219,12 +328,44 @@ export function takeSeats(seats, cap, held) {
|
|
|
219
328
|
|
|
220
329
|
export function releaseSeat(jobId) {
|
|
221
330
|
if (!JOB_ID.test(jobId)) return;
|
|
222
|
-
try {
|
|
331
|
+
try {
|
|
332
|
+
fs.rmSync(seatFile(jobId), { force: true });
|
|
333
|
+
} catch {
|
|
334
|
+
/* best effort */
|
|
335
|
+
}
|
|
336
|
+
clearCancel(jobId);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// A cancel asked for by a server that does not run the consultation: left
|
|
340
|
+
// beside its seat, for the server that does to find and act on.
|
|
341
|
+
const cancelFile = (jobId) => path.join(seatsDir(), `${jobId}.cancel`);
|
|
342
|
+
export function requestCancel(jobId) {
|
|
343
|
+
if (!JOB_ID.test(jobId)) return false;
|
|
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
|
+
}
|
|
351
|
+
}
|
|
352
|
+
export const cancelRequested = (jobId) => JOB_ID.test(jobId) && fs.existsSync(cancelFile(jobId));
|
|
353
|
+
export function clearCancel(jobId) {
|
|
354
|
+
if (JOB_ID.test(jobId))
|
|
355
|
+
try {
|
|
356
|
+
fs.rmSync(cancelFile(jobId), { force: true });
|
|
357
|
+
} catch {
|
|
358
|
+
/* best effort */
|
|
359
|
+
}
|
|
223
360
|
}
|
|
224
361
|
|
|
225
362
|
export function loadGroup(groupId) {
|
|
226
363
|
if (!GROUP_ID.test(groupId)) return null;
|
|
227
|
-
try {
|
|
364
|
+
try {
|
|
365
|
+
return JSON.parse(fs.readFileSync(path.join(POLICY.home, 'history', 'groups', `${groupId}.json`), 'utf8'));
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
228
369
|
}
|
|
229
370
|
|
|
230
371
|
// A fan-out is recorded as its own record so a later reader can tell which
|
|
@@ -238,6 +379,10 @@ export function persistGroup(group) {
|
|
|
238
379
|
|
|
239
380
|
export function cleanupJobDir(jobId) {
|
|
240
381
|
for (const dir of [jobDir(jobId), sandboxHome(jobId)]) {
|
|
241
|
-
try {
|
|
382
|
+
try {
|
|
383
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
384
|
+
} catch {
|
|
385
|
+
/* best effort */
|
|
386
|
+
}
|
|
242
387
|
}
|
|
243
388
|
}
|