@bongos/core 1.24.76 → 1.24.78
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/.bongos-core.json +129 -49
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +12 -1
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +45 -0
- package/docs/api/openapi.json +209 -4
- package/docs/api-reference.md +7 -3
- package/docs/copy-inventory.md +109 -92
- package/docs/copy-registry.json +291 -108
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +2 -0
- package/docs/page-readings.json +111 -97
- package/docs/recipes/autobongos-runner-setup.md +148 -0
- package/docs/recipes/autobongos-windows-host.md +10 -7
- package/modules/autonomy/cadence.js +3 -0
- package/modules/autonomy/db.js +86 -7
- package/modules/autonomy/fence.js +20 -5
- package/modules/autonomy/gauge.js +25 -1
- package/modules/autonomy/migrations/autonomy_006_runner_device.sql +45 -0
- package/modules/autonomy/module.json +2 -1
- package/modules/autonomy/routes/autonomy.js +165 -3
- package/modules/autonomy/runner-health.js +27 -0
- package/modules/autonomy/skills/autobongos-setup/SKILL.md +86 -0
- package/modules/hall-ui/public/commands-lib.js +1 -1
- package/modules/hall-ui/public/gate.js +12 -5
- package/modules/hall-ui/public/runner-button.css +55 -0
- package/modules/hall-ui/public/runner-button.js +187 -0
- package/modules/hall-ui/public/settings-autobongos.js +349 -63
- package/modules/hall-ui/public/settings.html +23 -10
- package/modules/hall-ui/records/runner-button.md +16 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/bongos/autobongos-bootstrap.ps1 +169 -0
- package/scripts/bongos/autobongos-bootstrap.sh +151 -0
- package/scripts/bongos/autobongos-host.js +883 -0
- package/scripts/bongos/autobongos-run.js +25 -4
- package/scripts/bongos/autonomy-gauge.js +11 -3
- package/scripts/bongos/cli-lib.js +5 -1
- package/scripts/bongos/stranded-rows.js +39 -2
- package/scripts/gds/autobongos-bootstrap.ps1 +8 -0
- package/scripts/gds/autobongos-bootstrap.sh +8 -0
- package/scripts/gds/autobongos-host.js +7 -0
- package/scripts/hall-preview/server.js +7 -0
- package/src/instance-config.js +22 -3
- package/src/module-api.js +1 -1
- package/tests/autobongos_host.mjs +647 -0
- package/tests/autobongos_loop.mjs +11 -0
- package/tests/autonomy_config_idle.mjs +9 -4
- package/tests/autonomy_gauge.mjs +43 -0
- package/tests/autonomy_runner_device.mjs +444 -0
- package/tests/autonomy_runner_identity.mjs +3 -1
- package/tests/cli_sessions.mjs +16 -0
- package/tests/hall_runner_button.mjs +467 -0
- package/tests/hall_runner_panel_connect.mjs +73 -0
- package/tests/stranded_rows.mjs +50 -0
|
@@ -265,6 +265,10 @@ const WORKER_ENV_ALLOW = [
|
|
|
265
265
|
// missing here would be set on the host and silently absent in the worker.
|
|
266
266
|
...claudeBinEnvNames(),
|
|
267
267
|
'XDG_CONFIG_HOME',
|
|
268
|
+
// The runner's own config root (task 1005095): its login, heartbeat and run log
|
|
269
|
+
// sit apart from the builder's own CLI session on the same device, and a worker
|
|
270
|
+
// that did not inherit this would claim and ship as the wrong session.
|
|
271
|
+
'BONGOS_CONFIG_ROOT',
|
|
268
272
|
// THE WORKER'S OWN CREDENTIAL, and it has to be here (task 1003907, found by
|
|
269
273
|
// running it). On a desktop, the claude CLI authenticates from the credential
|
|
270
274
|
// store under USERPROFILE/HOME, which are already allowed - so this list looked
|
|
@@ -451,12 +455,18 @@ function worktreeName(taskId) {
|
|
|
451
455
|
// A failure of EITHER read returns the shape that refuses. An unreachable
|
|
452
456
|
// instance and a thrown kill switch are different facts, but they license the
|
|
453
457
|
// same action: do nothing.
|
|
458
|
+
// THIS DEVICE is named on the read (task 1005094): the server folds the device's
|
|
459
|
+
// own Run switch into the answer, so a builder's Stop on this machine arrives as
|
|
460
|
+
// `enabled: false` with nothing for this file to decide. The name is the same
|
|
461
|
+
// os.hostname() the heartbeat reports, which is how the two rows line up.
|
|
462
|
+
const fenceArgs = () => ({ query: { host: os.hostname() } });
|
|
463
|
+
|
|
454
464
|
async function readFence(deps = {}) {
|
|
455
465
|
const api = deps.api || await (require('./cli-lib').cliClient)();
|
|
456
466
|
let raw = null;
|
|
457
467
|
let graderBypassed = null;
|
|
458
468
|
try {
|
|
459
|
-
const r = await api.autonomy.getAutonomyFence();
|
|
469
|
+
const r = await api.autonomy.getAutonomyFence(fenceArgs());
|
|
460
470
|
if (r && r.ok) raw = r.data;
|
|
461
471
|
} catch (_) { /* unreadable → refuses below */ }
|
|
462
472
|
try {
|
|
@@ -545,7 +555,9 @@ function queueGatedRow(queueGate, now) {
|
|
|
545
555
|
async function iteration(opts, deps = {}) {
|
|
546
556
|
const runCmd = deps.run || run;
|
|
547
557
|
const spawnWork = deps.spawnWorker || spawnWorker;
|
|
548
|
-
|
|
558
|
+
// absentMeansGo: a device with no gauge file at all works until the usage limit
|
|
559
|
+
// stops it (task 1005095) — see decideCapacity in modules/autonomy/gauge.js.
|
|
560
|
+
const readGauge = deps.gauge || (() => gauge.evaluate(undefined, { absentMeansGo: true }));
|
|
549
561
|
const pick = deps.pickTask || pickTask;
|
|
550
562
|
const log = deps.record || record;
|
|
551
563
|
|
|
@@ -1005,7 +1017,9 @@ async function pollSignals(deps = {}) {
|
|
|
1005
1017
|
if (r) head = r;
|
|
1006
1018
|
} catch (_) { /* null */ }
|
|
1007
1019
|
try {
|
|
1008
|
-
|
|
1020
|
+
// Named the same way, so a press of Run on this device moves the stamp the
|
|
1021
|
+
// wait is watching and the runner wakes inside a poll instead of an idle.
|
|
1022
|
+
const f = await api.autonomy.getAutonomyFence(fenceArgs());
|
|
1009
1023
|
if (f && f.ok) fenceUpdatedAt = f.data.updated_at || null;
|
|
1010
1024
|
} catch (_) { /* null */ }
|
|
1011
1025
|
try {
|
|
@@ -1227,9 +1241,15 @@ async function forever(opts, deps = {}) {
|
|
|
1227
1241
|
// `config_idle:<code>` instead of "waiting", so the hall shows the fix rather
|
|
1228
1242
|
// than "alive" for ten hours. Any row that is not that refusal ends the stretch.
|
|
1229
1243
|
let idle = null;
|
|
1244
|
+
// When a resting runner resumes, in epoch seconds (task 1005094): the reset the
|
|
1245
|
+
// last hold named. Sent on every beat that is not about a task, so the device's
|
|
1246
|
+
// Run button can show "Resting until 3:40" for the whole wait, not just for the
|
|
1247
|
+
// one beat that followed the hold. Any row that is not such a hold clears it.
|
|
1248
|
+
let restUntil = null;
|
|
1230
1249
|
const clock = () => (deps.now ? deps.now() : Date.now());
|
|
1231
1250
|
const beat = async (rawFields) => {
|
|
1232
|
-
const
|
|
1251
|
+
const resting = restUntil && !(rawFields && rawFields.working_task_id) ? { ...rawFields, hold_until: restUntil } : rawFields;
|
|
1252
|
+
const fields = configIdle.markBeat(resting, idle, clock());
|
|
1233
1253
|
// The runner state is passed, not defaulted: the file names `disk_head` from
|
|
1234
1254
|
// it (task 1004406), and an injected state must reach the beat that reports it.
|
|
1235
1255
|
try { beatFile(Date.now(), deps.state || RUNNER_STATE); } catch (_) { /* a runner that cannot write its file still runs */ }
|
|
@@ -1275,6 +1295,7 @@ async function forever(opts, deps = {}) {
|
|
|
1275
1295
|
row = await iterate(opts, deps);
|
|
1276
1296
|
emit(row);
|
|
1277
1297
|
idle = configIdle.trackIdle(idle, row, clock());
|
|
1298
|
+
restUntil = row.event === 'hold' && Number(row.sleep_until) > 0 ? Number(row.sleep_until) : null;
|
|
1278
1299
|
// Carry the task id so the hall can tell "quiet because it is building
|
|
1279
1300
|
// task 1234" from "quiet because it is dead". A worker may hold the loop for
|
|
1280
1301
|
// ninety minutes, and without this every honest hour of work looks like a
|
|
@@ -26,9 +26,14 @@ function gaugePath() {
|
|
|
26
26
|
catch (_) { return null; }
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// null means there is NO file. A file that exists but cannot be read or parsed
|
|
30
|
+
// comes back as the string 'unreadable', which the decision treats as a bad
|
|
31
|
+
// reading (unknown) and never as "this device has no gauge" (task 1005095).
|
|
29
32
|
function loadGauge(p) {
|
|
30
33
|
if (!p) return null;
|
|
31
|
-
|
|
34
|
+
let text;
|
|
35
|
+
try { text = fs.readFileSync(p, 'utf8'); } catch (e) { return e && e.code === 'ENOENT' ? null : 'unreadable'; }
|
|
36
|
+
try { return JSON.parse(text); } catch (_) { return 'unreadable'; }
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
function readConfig() {
|
|
@@ -47,9 +52,12 @@ function readConfig() {
|
|
|
47
52
|
return c;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
// `overrides` is merged over the env config. The supervisor passes
|
|
56
|
+
// { absentMeansGo: true } (task 1005095); this CLI passes nothing, so a missing
|
|
57
|
+
// file is still UNKNOWN here.
|
|
58
|
+
function evaluate(nowEpochS = Math.floor(Date.now() / 1000), overrides = {}) {
|
|
51
59
|
const p = gaugePath();
|
|
52
|
-
return { path: p, decision: decideCapacity({ gauge: loadGauge(p), now: nowEpochS, config: readConfig() }) };
|
|
60
|
+
return { path: p, decision: decideCapacity({ gauge: loadGauge(p), now: nowEpochS, config: { ...readConfig(), ...overrides } }) };
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
async function main(argv) {
|
|
@@ -333,7 +333,11 @@ function sessionStoreDir() {
|
|
|
333
333
|
// Directory name deliberately NOT 'sessions': that is a live module key, and ADR 0083
|
|
334
334
|
// §Decision #4 forbids naming one in shared machinery. 'instances' is the truer name anyway —
|
|
335
335
|
// one file per instance is exactly what this holds.
|
|
336
|
-
|
|
336
|
+
// Under the config ROOT, which is ~/.config unless BONGOS_CONFIG_ROOT moves it
|
|
337
|
+
// (task 1005095) — so an Autobongos runner's login is never filed into, or read
|
|
338
|
+
// out of, the builder's own store. The root is not the brand: the anchor is
|
|
339
|
+
// still the same fixed dir name for every checkout that shares a root.
|
|
340
|
+
return path.join(ic.configRoot(), ic.FALLBACK_DIR, 'instances');
|
|
337
341
|
}
|
|
338
342
|
|
|
339
343
|
// The store's filename for an instance, derived from its api_base host. Returns null when the base
|
|
@@ -30,6 +30,19 @@
|
|
|
30
30
|
// MERGE commit's body, where GitHub echoes the branch's subject. A subject-only scan
|
|
31
31
|
// misses the one case this file exists for.
|
|
32
32
|
//
|
|
33
|
+
// A SECOND FORM, because the convention moved (task 1005187). Builders, and ship.js's
|
|
34
|
+
// own regeneration commits, now mostly write the task at the START of the subject:
|
|
35
|
+
// `task N - words`, `task N: words`, with an optional conventional prefix in front
|
|
36
|
+
// (`fix: task N - words`, `wip(scope): task N - words`). Measured over the last 400
|
|
37
|
+
// commits on main on 2026-11-10: 140 subjects in that form against 53 lines ending
|
|
38
|
+
// `(task N)`, covering 18 distinct tasks — under the floor of 20 the live-repo test
|
|
39
|
+
// holds, which is how main went red. So both forms count. The new one is read off the
|
|
40
|
+
// SUBJECT only, and only at its start, followed by a dash or a colon: the same
|
|
41
|
+
// "this commit DID task N" statement, and still not "mentions N" (a subject such as
|
|
42
|
+
// "see task N for why", or any body line naming a task, is not counted). A merge
|
|
43
|
+
// commit's subject is GitHub's, so for one of those the first line of its body — the
|
|
44
|
+
// pull request's own title, which GitHub echoes there — is read the same way.
|
|
45
|
+
//
|
|
33
46
|
// MEASURED on this repo before shipping: 877 ids are named as an authoring subject
|
|
34
47
|
// across 4,784 commits, and ZERO of them are open `ready`/`backlog` rows (434 of
|
|
35
48
|
// those). So today's board is clean and the signal is not merely quiet — the same
|
|
@@ -66,13 +79,37 @@ const SEP = `${SOH}COMMIT${SOH}`;
|
|
|
66
79
|
// mentioned N", and the second is the noise that made the supersession half undoable.
|
|
67
80
|
const AUTHORED_RE = /\(task (\d{6,7})\)\s*$/;
|
|
68
81
|
|
|
82
|
+
// The second convention: the task leads the subject, after an optional conventional
|
|
83
|
+
// prefix, and is followed by a dash or a colon. Read off a TITLE line only (see
|
|
84
|
+
// titleLines) — never off every line, which is where "mentions N" lives.
|
|
85
|
+
const LEADING_RE = /^(?:[a-z]+(?:\([^)]*\))?!?:\s*)?task (\d{6,7})\s*[-:\u2013\u2014]/i;
|
|
86
|
+
const MERGE_SUBJECT_RE = /^Merge pull request #\d+ /;
|
|
87
|
+
|
|
88
|
+
// The lines of a message that are a TITLE: its subject, and for a GitHub merge commit
|
|
89
|
+
// the first line of the body too, where GitHub echoes the pull request's own title.
|
|
90
|
+
function titleLines(lines) {
|
|
91
|
+
const at = lines.findIndex((l) => l);
|
|
92
|
+
if (at === -1) return [];
|
|
93
|
+
const out = [lines[at]];
|
|
94
|
+
if (MERGE_SUBJECT_RE.test(lines[at])) {
|
|
95
|
+
const echoed = lines.slice(at + 1).find((l) => l);
|
|
96
|
+
if (echoed) out.push(echoed);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
69
101
|
// PURE — every task id a commit message claims to have DONE. Exported so the rule is
|
|
70
102
|
// testable without a repo: a scanner that quietly stops matching reports a clean board,
|
|
71
103
|
// which is indistinguishable from a healthy one.
|
|
72
104
|
function authoredIds(message) {
|
|
73
105
|
const out = new Set();
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
const lines = String(message || '').split('\n').map((l) => l.trim());
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
const m = AUTHORED_RE.exec(line);
|
|
109
|
+
if (m) out.add(m[1]);
|
|
110
|
+
}
|
|
111
|
+
for (const line of titleLines(lines)) {
|
|
112
|
+
const m = LEADING_RE.exec(line);
|
|
76
113
|
if (m) out.add(m[1]);
|
|
77
114
|
}
|
|
78
115
|
return out;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# scripts/gds/autobongos-bootstrap.ps1 -- @bongos-forwarder (task 1004848). Generated by
|
|
2
|
+
# scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
3
|
+
# The real script is scripts/bongos/autobongos-bootstrap.ps1. This forwarder keeps callers that still name
|
|
4
|
+
# the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
5
|
+
if (-not $env:BONGOS_FORWARDER_WARNED) { Write-Warning '[bongos] scripts/gds/autobongos-bootstrap.ps1 has moved to scripts/bongos/autobongos-bootstrap.ps1. This old path still works for now, but update whatever called it. The forwarders in scripts/gds/ are removed in a later release.' }
|
|
6
|
+
$env:BONGOS_FORWARDER_WARNED = '1'
|
|
7
|
+
& (Join-Path $PSScriptRoot '..\bongos\autobongos-bootstrap.ps1') @args
|
|
8
|
+
exit $LASTEXITCODE
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/gds/autobongos-bootstrap.sh — @bongos-forwarder (task 1004848). Generated by
|
|
3
|
+
# scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
+
# The real script is scripts/bongos/autobongos-bootstrap.sh. This forwarder keeps callers that still name
|
|
5
|
+
# the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
6
|
+
if [ -z "${BONGOS_FORWARDER_WARNED:-}" ]; then echo "[bongos] scripts/gds/autobongos-bootstrap.sh has moved to scripts/bongos/autobongos-bootstrap.sh. This old path still works for now, but update whatever called it. The forwarders in scripts/gds/ are removed in a later release." >&2; fi
|
|
7
|
+
export BONGOS_FORWARDER_WARNED=1
|
|
8
|
+
exec bash "$(dirname "$0")/../bongos/autobongos-bootstrap.sh" "$@"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/autobongos-host.js — @bongos-forwarder (task 1004848). Generated by
|
|
3
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
+
// The real script is scripts/bongos/autobongos-host.js. This forwarder keeps callers that still name
|
|
5
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
6
|
+
'use strict';
|
|
7
|
+
require('../bongos/forwarder.js').forward(module, 'autobongos-host.js');
|
|
@@ -214,6 +214,13 @@ const CANNED_WRITES = [
|
|
|
214
214
|
['POST', /^ai-studio\/uploads$/, 'ai-studio__uploads.post'],
|
|
215
215
|
['POST', /^ai-studio\/requests$/, 'ai-studio__requests.post'],
|
|
216
216
|
['POST', /^ai-studio\/topup$/, 'ai-studio__topup.post'],
|
|
217
|
+
// Settings > Your Autobongos runner (task 1005094): a press of Run or Stop, a
|
|
218
|
+
// setup code for Add a device, and Remove (the login, then the card), so the
|
|
219
|
+
// Starting face, the setup command and the removal toast render here.
|
|
220
|
+
['POST', /^autonomy\/runners\/[^/]+\/run$/, 'autonomy__runners__run.post'],
|
|
221
|
+
['POST', /^auth\/runner-code\/issue$/, 'auth__runner-code__issue.post'],
|
|
222
|
+
['POST', /^me\/sessions\/revoke$/, 'me__sessions__revoke.post'],
|
|
223
|
+
['DELETE', /^autonomy\/runners\/[^/]+$/, 'autonomy__runners.delete'],
|
|
217
224
|
];
|
|
218
225
|
|
|
219
226
|
// A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
|
package/src/instance-config.js
CHANGED
|
@@ -112,17 +112,35 @@ function resolveApiBase({ session, env = process.env, b = safeBrand() } = {}) {
|
|
|
112
112
|
return fromSession || defaultApiBase(b);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// The folder every config dir lives under: ~/.config, unless BONGOS_CONFIG_ROOT
|
|
116
|
+
// names another one (task 1005095).
|
|
117
|
+
//
|
|
118
|
+
// WHY AN OVERRIDE. An Autobongos runner on a builder's own laptop holds a login
|
|
119
|
+
// of its own (src/bongos/runner-login.js), and it must not share a session file
|
|
120
|
+
// with the builder's CLI: one slot means adding the laptop as a runner signs the
|
|
121
|
+
// builder's own tools out, and removing the runner signs them out again. The
|
|
122
|
+
// runner's device program (scripts/bongos/autobongos-host.js) sets this for the
|
|
123
|
+
// supervisor and every worker it starts, so their session, heartbeat and run log
|
|
124
|
+
// sit apart — and with a root of its own the LEGACY dirs below are not found
|
|
125
|
+
// either, so the runner can never fall back to the builder's own session.
|
|
126
|
+
// One fixed name, read directly: the branded-prefix lookup needs the brand, and
|
|
127
|
+
// this decides where the brand's own dir goes.
|
|
128
|
+
function configRoot(env = process.env) {
|
|
129
|
+
const over = typeof env.BONGOS_CONFIG_ROOT === 'string' ? env.BONGOS_CONFIG_ROOT.trim() : '';
|
|
130
|
+
return over ? path.resolve(over) : path.join(os.homedir(), '.config');
|
|
131
|
+
}
|
|
132
|
+
|
|
115
133
|
// Absolute path to the instance config dir — the WRITE target and first read.
|
|
116
134
|
function configHome(b = safeBrand()) {
|
|
117
|
-
return path.join(
|
|
135
|
+
return path.join(configRoot(), configDirName(b));
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
// All config dirs to try on READ, configured first then legacy, de-duped.
|
|
121
139
|
function configReadDirs(b = safeBrand()) {
|
|
122
|
-
const
|
|
140
|
+
const root = configRoot();
|
|
123
141
|
const dirs = [configHome(b)];
|
|
124
142
|
for (const d of LEGACY_DIRS) {
|
|
125
|
-
const p = path.join(
|
|
143
|
+
const p = path.join(root, d);
|
|
126
144
|
if (!dirs.includes(p)) dirs.push(p);
|
|
127
145
|
}
|
|
128
146
|
return dirs;
|
|
@@ -367,6 +385,7 @@ module.exports = {
|
|
|
367
385
|
envPrefix,
|
|
368
386
|
defaultApiBase,
|
|
369
387
|
resolveApiBase,
|
|
388
|
+
configRoot,
|
|
370
389
|
configHome,
|
|
371
390
|
configReadDirs,
|
|
372
391
|
configPath,
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.78'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|