@mindrian_os/cli 2.0.0-beta.33 → 2.0.0-beta.37
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +88 -0
- package/bin/mindrian-brain-mcp-client.cjs +14 -0
- package/commands/doctor.md +4 -3
- package/data/brain-census.generated.json +2827 -1523
- package/data/hooks-markers.json +25 -0
- package/hooks/hooks.json +0 -23
- package/lib/core/brain-client.cjs +83 -3
- package/lib/core/brain-prewarm.cjs +164 -0
- package/lib/core/doctor/class-m-brain-smoke.cjs +182 -26
- package/lib/core/doctor/class-m-brain-smoke.test.cjs +441 -82
- package/lib/core/install-id.cjs +232 -0
- package/lib/core/integration-registry.cjs +96 -20
- package/lib/mcp/brain-composition-census.cjs +22 -6
- package/lib/mcp/brain-route-bound.cjs +66 -0
- package/lib/mcp/brain-router.cjs +25 -10
- package/lib/mcp/hook-adapter-audit.cjs +13 -9
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/build-brain-census.cjs +321 -4
- package/scripts/doctor.cjs +57 -7
- package/scripts/session-start +22 -0
- package/skills/doctor/SKILL.md +4 -3
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_mcpFirst198Migrated": {
|
|
3
|
+
"_note": "Phase 198-08 (SPEC-5, D-05/D-06) migration markers, extended by Phase 198-09 (D-05 final wave). Each entry names a hook script that now dispatches to lib/mcp/adapter-client.cjs behind MINDRIAN_MCP_FIRST ('cli' or 'all') instead of running its business logic locally; flag OFF (unset/empty, the default) keeps the pre-migration legacy behavior byte-identical (SPEC-7). This is metadata for tests/test-198-adapter-budget.test.cjs's import-audit + line-count budget (D-06) to enumerate -- it is NOT consumed by Claude Code's hook loader, which only reads the 'hooks' key in hooks/hooks.json. Migration order is D-05: statusline + SessionStart FIRST (198-08); Stop-gate enforcement LAST (198-09, this entry), only after server-side gate dedup + relevance existed (lib/mcp/gate-dedup.cjs, Task 1 of 198-09). scripts/check-card-fire.cjs is NOT listed here -- it stays a separate Stop hook entry that no-ops under the flag (deferring to on-stop's daemon-backed decision), never itself migrated to a thin adapter shape.",
|
|
4
|
+
"surfaces": [
|
|
5
|
+
{
|
|
6
|
+
"script": "scripts/statusline-mos-dispatch",
|
|
7
|
+
"tool": "status_read",
|
|
8
|
+
"note": "Statusline segment (spend/cap from day one). Flag ON wakes the daemon and queries status_read; flag OFF execs the legacy scripts/statusline-mos exactly as shipped."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"script": "scripts/sessionstart-coordinator.cjs",
|
|
12
|
+
"tool": "room_state_bound,status_read",
|
|
13
|
+
"note": "SessionStart binding + status segments. Flag ON wakes the daemon and queries room_state_bound + status_read (runThinAdapter); flag OFF runs the full 11-contributor runAll() exactly as shipped."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"script": "scripts/on-stop",
|
|
17
|
+
"tool": "stop_gate_check",
|
|
18
|
+
"note": "Stop-gate enforcement + business close-out (D-05 final wave). Flag ON wakes the daemon and queries stop_gate_check (lib/mcp/tools/stop-gate.cjs -> lib/mcp/stop-gate-handler.cjs -- gate-dedup + relevance gate the fire decision, the shipped check-card-fire.cjs predicate is the final authority, the daemon owns STATE.md/memory-lifecycle/minto-debouncer/folder-memory close-out); flag OFF runs the full legacy body exactly as shipped."
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"_firstInstallRouterOrdering": {
|
|
23
|
+
"_note": "Phase 267.2-06 (HOOK-08). scripts/first-install-router.cjs is registered in the UserPromptSubmit chain immediately AFTER scripts/mva-detect.cjs and before scripts/brain-derivation-drain.cjs. This is metadata for a human reader (and any future drift test) -- it is NOT consumed by Claude Code's hook loader, which only reads the 'hooks' key in hooks/hooks.json. ORDERING IS LOAD BEARING: the router consumes the pending MVA state that mva-detect.cjs writes on the same turn, so running before it would read a stale or absent state. Do not reorder these two entries without re-checking this dependency."
|
|
24
|
+
}
|
|
25
|
+
}
|
package/hooks/hooks.json
CHANGED
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_mcpFirst198Migrated": {
|
|
3
|
-
"_note": "Phase 198-08 (SPEC-5, D-05/D-06) migration markers, extended by Phase 198-09 (D-05 final wave). Each entry names a hook script that now dispatches to lib/mcp/adapter-client.cjs behind MINDRIAN_MCP_FIRST ('cli' or 'all') instead of running its business logic locally; flag OFF (unset/empty, the default) keeps the pre-migration legacy behavior byte-identical (SPEC-7). This is metadata for tests/test-198-adapter-budget.test.cjs's import-audit + line-count budget (D-06) to enumerate -- it is NOT consumed by Claude Code's hook loader, which only reads the 'hooks' key below. Migration order is D-05: statusline + SessionStart FIRST (198-08); Stop-gate enforcement LAST (198-09, this entry), only after server-side gate dedup + relevance existed (lib/mcp/gate-dedup.cjs, Task 1 of 198-09). scripts/check-card-fire.cjs is NOT listed here -- it stays a separate Stop hook entry that no-ops under the flag (deferring to on-stop's daemon-backed decision), never itself migrated to a thin adapter shape.",
|
|
4
|
-
"surfaces": [
|
|
5
|
-
{
|
|
6
|
-
"script": "scripts/statusline-mos-dispatch",
|
|
7
|
-
"tool": "status_read",
|
|
8
|
-
"note": "Statusline segment (spend/cap from day one). Flag ON wakes the daemon and queries status_read; flag OFF execs the legacy scripts/statusline-mos exactly as shipped."
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"script": "scripts/sessionstart-coordinator.cjs",
|
|
12
|
-
"tool": "room_state_bound,status_read",
|
|
13
|
-
"note": "SessionStart binding + status segments. Flag ON wakes the daemon and queries room_state_bound + status_read (runThinAdapter); flag OFF runs the full 11-contributor runAll() exactly as shipped."
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"script": "scripts/on-stop",
|
|
17
|
-
"tool": "stop_gate_check",
|
|
18
|
-
"note": "Stop-gate enforcement + business close-out (D-05 final wave). Flag ON wakes the daemon and queries stop_gate_check (lib/mcp/tools/stop-gate.cjs -> lib/mcp/stop-gate-handler.cjs -- gate-dedup + relevance gate the fire decision, the shipped check-card-fire.cjs predicate is the final authority, the daemon owns STATE.md/memory-lifecycle/minto-debouncer/folder-memory close-out); flag OFF runs the full legacy body exactly as shipped."
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
|
-
"_firstInstallRouterOrdering": {
|
|
23
|
-
"_note": "Phase 267.2-06 (HOOK-08). scripts/first-install-router.cjs is registered in the UserPromptSubmit chain immediately AFTER scripts/mva-detect.cjs and before scripts/brain-derivation-drain.cjs. This is metadata for a human reader (and any future drift test) -- it is NOT consumed by Claude Code's hook loader, which only reads the 'hooks' key below. ORDERING IS LOAD BEARING: the router consumes the pending MVA state that mva-detect.cjs writes on the same turn, so running before it would read a stale or absent state. Do not reorder these two entries without re-checking this dependency."
|
|
24
|
-
},
|
|
25
2
|
"hooks": {
|
|
26
3
|
"SessionStart": [
|
|
27
4
|
{
|
|
@@ -305,6 +305,34 @@ function isAvailable() {
|
|
|
305
305
|
return !!getApiKey();
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Quick 260911-iko (D-04): the opaque per-install header, spread into both
|
|
309
|
+
// of callTool's wire requests. A per-process memo is correct here (mirrors
|
|
310
|
+
// the getApiKey() memo idiom above) and must be commented as such: the id
|
|
311
|
+
// only changes on reinstall or on a `doctor --reset-install-id` run, and
|
|
312
|
+
// both of those happen in a DIFFERENT process, so re-reading it on every
|
|
313
|
+
// call would buy nothing. Wrapped in its own try/catch so a missing or
|
|
314
|
+
// broken install-id module can never take a Brain call down -- the header
|
|
315
|
+
// is a bucket key, never a requirement (D-04: omitted, never an error).
|
|
316
|
+
//
|
|
317
|
+
// This helper never touches the Part 8 egress belt (classify() at
|
|
318
|
+
// callTool's egress-guard call below): that belt classifies `args`, and
|
|
319
|
+
// headers are not payload. The install id never enters `args` and must
|
|
320
|
+
// never be added to them.
|
|
321
|
+
let _memoizedInstallIdHeaders = null;
|
|
322
|
+
function _installIdHeaders() {
|
|
323
|
+
if (_memoizedInstallIdHeaders) return _memoizedInstallIdHeaders;
|
|
324
|
+
try {
|
|
325
|
+
const installIdMod = require('./install-id.cjs');
|
|
326
|
+
const id = installIdMod.getInstallId();
|
|
327
|
+
_memoizedInstallIdHeaders = (typeof id === 'string')
|
|
328
|
+
? { [installIdMod.installIdHeaderName]: id }
|
|
329
|
+
: {};
|
|
330
|
+
} catch (_e) {
|
|
331
|
+
_memoizedInstallIdHeaders = {};
|
|
332
|
+
}
|
|
333
|
+
return _memoizedInstallIdHeaders;
|
|
334
|
+
}
|
|
335
|
+
|
|
308
336
|
// ---------------------------------------------------------------------------
|
|
309
337
|
// Phase 250-04 (HONEST-03, SEED-011 Option A) -- per-install silent
|
|
310
338
|
// registration. Design doc: docs/BRAIN-IDENTITY-DESIGN.md.
|
|
@@ -350,6 +378,12 @@ async function _tryAutoRegister() {
|
|
|
350
378
|
|
|
351
379
|
try {
|
|
352
380
|
const installId = crypto.randomUUID();
|
|
381
|
+
// Quick 260911-iko: the x-theo-install-id header does NOT ride here.
|
|
382
|
+
// D-04 names callTool's two header blocks only; registration is a
|
|
383
|
+
// different endpoint with its own contract, and it runs before there is
|
|
384
|
+
// an established Brain session to bucket. Adding it here would be a
|
|
385
|
+
// change to Theo's registration surface, which D-06 says must be
|
|
386
|
+
// announced to Theo first -- a deliberate exclusion, not a miss.
|
|
353
387
|
const res = await fetch(`${BRAIN_URL}/register`, {
|
|
354
388
|
method: 'POST',
|
|
355
389
|
signal: AbortSignal.timeout(BRAIN_REQUEST_TIMEOUT_MS),
|
|
@@ -456,6 +490,7 @@ async function _ensureSession(apiKey) {
|
|
|
456
490
|
'Content-Type': 'application/json',
|
|
457
491
|
'Accept': 'application/json, text/event-stream',
|
|
458
492
|
'Authorization': `Bearer ${apiKey}`,
|
|
493
|
+
..._installIdHeaders(),
|
|
459
494
|
},
|
|
460
495
|
body: JSON.stringify({
|
|
461
496
|
jsonrpc: '2.0',
|
|
@@ -675,6 +710,7 @@ async function callTool(toolName, args) {
|
|
|
675
710
|
'Content-Type': 'application/json',
|
|
676
711
|
'Accept': 'application/json, text/event-stream',
|
|
677
712
|
'Authorization': `Bearer ${key}`,
|
|
713
|
+
..._installIdHeaders(),
|
|
678
714
|
},
|
|
679
715
|
body: JSON.stringify({
|
|
680
716
|
jsonrpc: '2.0',
|
|
@@ -1250,29 +1286,68 @@ async function _composeTheoAsk(payload, question, deps) {
|
|
|
1250
1286
|
}
|
|
1251
1287
|
|
|
1252
1288
|
// CONFIDENCE: top step reads exactly 0.9; a missing/non-finite degree -> 0.5.
|
|
1289
|
+
// theo_rank and the command-bearing-first sort below never touch this
|
|
1290
|
+
// computation -- the sort reorders `options`, it never recomputes a
|
|
1291
|
+
// confidence value (Quick 260911-ddd, DDD-01).
|
|
1253
1292
|
const top = steps.reduce((max, s) => {
|
|
1254
1293
|
const d = s && Number.isFinite(s.degree) ? s.degree : 0;
|
|
1255
1294
|
return d > max ? d : max;
|
|
1256
1295
|
}, 0);
|
|
1257
|
-
const options = steps.map((s) => {
|
|
1296
|
+
const options = steps.map((s, idx) => {
|
|
1258
1297
|
const degree = s && s.degree;
|
|
1259
1298
|
let confidence = 0.5;
|
|
1260
1299
|
if (top > 0 && Number.isFinite(degree)) {
|
|
1261
1300
|
confidence = Math.max(0.5, Math.min(0.9, Math.round((0.5 + 0.4 * (degree / top)) * 100) / 100));
|
|
1262
1301
|
}
|
|
1302
|
+
// Quick 260911-ddd (DDD-01): theo_rank carries Theo's own rank so it
|
|
1303
|
+
// is never lost by the reorder below. Theo's `step` field when it is
|
|
1304
|
+
// a finite number, else the 1-based index of this step WITHIN Theo's
|
|
1305
|
+
// own chain array, computed here before any sort so a later reorder
|
|
1306
|
+
// can never leak into this number.
|
|
1307
|
+
const theoRank = (s && Number.isFinite(s.step)) ? s.step : idx + 1;
|
|
1263
1308
|
return {
|
|
1264
1309
|
framework: s && s.framework,
|
|
1265
1310
|
confidence: confidence,
|
|
1266
1311
|
commands: commandsByFramework.get(s && s.framework) || [],
|
|
1312
|
+
theo_rank: theoRank,
|
|
1267
1313
|
};
|
|
1268
1314
|
});
|
|
1269
1315
|
|
|
1316
|
+
// Quick 260911-ddd (DDD-01): stable partition, command-bearing options
|
|
1317
|
+
// first, Theo's relative order preserved inside each group. Theo's
|
|
1318
|
+
// ranking is NEVER altered at the source -- this reorders the plugin's
|
|
1319
|
+
// OWN options array only. A single forward pass into two queues, then
|
|
1320
|
+
// concatenated, is stable BY CONSTRUCTION; it does not rest on
|
|
1321
|
+
// Array.prototype.sort's engine-stability semantics. When every option
|
|
1322
|
+
// or no option carries a command, one queue is empty and the
|
|
1323
|
+
// concatenation is the identity, so Theo's order survives unchanged
|
|
1324
|
+
// for free.
|
|
1325
|
+
//
|
|
1326
|
+
// DOWNSTREAM CONSEQUENCE, stated here so it is not mistaken for a
|
|
1327
|
+
// regression later: lib/mcp/brain-router.cjs:398 derives topConf from
|
|
1328
|
+
// options[0].confidence. On a chain whose top-ranked framework has no
|
|
1329
|
+
// command, the routed confidence now reads the first command-bearing
|
|
1330
|
+
// option's confidence (0.83 on the live IllDefined shape, previously
|
|
1331
|
+
// 0.9) -- that is the intended meaning of the change (confidence
|
|
1332
|
+
// describes the option actually surfaced to the user), not a bug.
|
|
1333
|
+
// brain-router.cjs is deliberately NOT touched to compensate for this.
|
|
1334
|
+
const commandBearing = [];
|
|
1335
|
+
const commandLess = [];
|
|
1336
|
+
for (const opt of options) {
|
|
1337
|
+
if (Array.isArray(opt.commands) && opt.commands.length > 0) {
|
|
1338
|
+
commandBearing.push(opt);
|
|
1339
|
+
} else {
|
|
1340
|
+
commandLess.push(opt);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const sortedOptions = commandBearing.concat(commandLess);
|
|
1344
|
+
|
|
1270
1345
|
const out = Object.assign({}, payload);
|
|
1271
1346
|
delete out.query_terms;
|
|
1272
1347
|
out.directive = {
|
|
1273
|
-
guided: { questions: [], framework: (
|
|
1348
|
+
guided: { questions: [], framework: (sortedOptions[0] && sortedOptions[0].framework) || null, stage: rung },
|
|
1274
1349
|
};
|
|
1275
|
-
out.next_gate = { sub_shape: 'F.1', options:
|
|
1350
|
+
out.next_gate = { sub_shape: 'F.1', options: sortedOptions };
|
|
1276
1351
|
out.grounding = {
|
|
1277
1352
|
source: 'theo',
|
|
1278
1353
|
answer_mode: payload.answer_mode,
|
|
@@ -1282,6 +1357,10 @@ async function _composeTheoAsk(payload, question, deps) {
|
|
|
1282
1357
|
chain_coverage: (chainRes && chainRes.coverage && typeof chainRes.coverage === 'object') ? chainRes.coverage : null,
|
|
1283
1358
|
chain_status: chainStatus,
|
|
1284
1359
|
confidence_source: 'theo_degree_normalized',
|
|
1360
|
+
// Quick 260911-ddd (DDD-01): names the applied ordering so a
|
|
1361
|
+
// consumer never has to branch on the key's presence -- present here
|
|
1362
|
+
// AND on the trailing catch path below.
|
|
1363
|
+
option_order: 'command_bearing_first',
|
|
1285
1364
|
};
|
|
1286
1365
|
return out;
|
|
1287
1366
|
} catch (_e) {
|
|
@@ -1299,6 +1378,7 @@ async function _composeTheoAsk(payload, question, deps) {
|
|
|
1299
1378
|
chain_coverage: null,
|
|
1300
1379
|
chain_status: 'unreachable',
|
|
1301
1380
|
confidence_source: 'theo_degree_normalized',
|
|
1381
|
+
option_order: 'command_bearing_first',
|
|
1302
1382
|
};
|
|
1303
1383
|
return out;
|
|
1304
1384
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Quick 260911-ddd (DDD-02) -- a content-free Brain pre-warm fired at MCP
|
|
8
|
+
* shim startup so the Brain is already warm by the time the first question
|
|
9
|
+
* arrives, instead of leaving every cold Render wake to silently degrade
|
|
10
|
+
* /mos:act's Tier 3 race to the local heuristic (see
|
|
11
|
+
* lib/mcp/brain-route-bound.cjs for the measured cold-wake numbers this
|
|
12
|
+
* exists to cover).
|
|
13
|
+
*
|
|
14
|
+
* WHY THE TIMEOUT DOES NOT BOUND THE WAKE ITSELF: the request reaching
|
|
15
|
+
* Render is what wakes the instance; aborting our own wait does not cancel
|
|
16
|
+
* that in-flight wake on Render's side. The timeout here only bounds how
|
|
17
|
+
* long THIS PROCESS holds the marker write open waiting for a verdict --
|
|
18
|
+
* which is why 15000 ms is generous without costing anyone anything: even
|
|
19
|
+
* if we give up waiting, the wake we triggered keeps running on Render and
|
|
20
|
+
* the NEXT real call benefits from it.
|
|
21
|
+
*
|
|
22
|
+
* CANON PART 8 (D-02): the probe sends theo_health with NO ARGUMENTS
|
|
23
|
+
* (`callTool('theo_health', {})`, the exact shape class-m-brain-smoke.cjs:225
|
|
24
|
+
* already ships) and the marker persists ONLY {at, ok, origin_host} --
|
|
25
|
+
* never a response body, never a header value, never a question. Every fs
|
|
26
|
+
* and network failure is swallowed; this function NEVER throws and NEVER
|
|
27
|
+
* rejects.
|
|
28
|
+
*
|
|
29
|
+
* MCP STDIO SAFETY: this file writes NOTHING to stdout, ever, because it
|
|
30
|
+
* can run inside an MCP stdio process where a stray stdout byte corrupts
|
|
31
|
+
* the JSON-RPC transport. A single stderr line only when MINDRIAN_DEBUG is
|
|
32
|
+
* set.
|
|
33
|
+
*
|
|
34
|
+
* The deps seam ({ probe, originHost, now, homeDir, timeoutMs }, all
|
|
35
|
+
* optional) mirrors the same injectable-probe discipline
|
|
36
|
+
* lib/core/doctor/class-m-brain-smoke.cjs already uses for its own
|
|
37
|
+
* theoHealthFn seam, so a test drives every arm through deps and never
|
|
38
|
+
* touches a real Brain.
|
|
39
|
+
*
|
|
40
|
+
* No em-dashes. CJS only.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const fs = require('fs');
|
|
44
|
+
const path = require('path');
|
|
45
|
+
const os = require('os');
|
|
46
|
+
|
|
47
|
+
const DEFAULT_PREWARM_TIMEOUT_MS = 15000;
|
|
48
|
+
|
|
49
|
+
const debugLog = (msg) => {
|
|
50
|
+
if (!process.env.MINDRIAN_DEBUG) return;
|
|
51
|
+
try {
|
|
52
|
+
process.stderr.write('[brain-prewarm] ' + msg + '\n');
|
|
53
|
+
} catch (_e) {
|
|
54
|
+
// swallow -- this function must never throw
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive the timeout (ms) for the probe race. Reads
|
|
60
|
+
* MINDRIAN_BRAIN_PREWARM_TIMEOUT_MS when it parses to a finite positive
|
|
61
|
+
* integer; otherwise DEFAULT_PREWARM_TIMEOUT_MS.
|
|
62
|
+
* @returns {number}
|
|
63
|
+
*/
|
|
64
|
+
function _resolveTimeoutMs() {
|
|
65
|
+
const raw = process.env.MINDRIAN_BRAIN_PREWARM_TIMEOUT_MS;
|
|
66
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) return DEFAULT_PREWARM_TIMEOUT_MS;
|
|
67
|
+
const parsed = Number(raw);
|
|
68
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) return DEFAULT_PREWARM_TIMEOUT_MS;
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Reduce a Brain origin URL to its host only (never the full URL, never a
|
|
74
|
+
* path, never a query string -- the marker holds origin_host, not the URL).
|
|
75
|
+
* @param {string} url
|
|
76
|
+
* @returns {string|null}
|
|
77
|
+
*/
|
|
78
|
+
function _hostOnly(url) {
|
|
79
|
+
if (typeof url !== 'string' || url.length === 0) return null;
|
|
80
|
+
try {
|
|
81
|
+
return new URL(url).host || null;
|
|
82
|
+
} catch (_e) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* markerPath(homeDir) -- the on-disk location of the pre-warm marker.
|
|
89
|
+
* @param {string} [homeDir] defaults to MINDRIAN_HOME or ~/.mindrian
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function markerPath(homeDir) {
|
|
93
|
+
const home = homeDir || process.env.MINDRIAN_HOME || path.join(os.homedir(), '.mindrian');
|
|
94
|
+
return path.join(home, 'brain-prewarm.json');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* prewarm(deps) -- fires one content-free theo_health probe and persists a
|
|
99
|
+
* minimal marker. NEVER throws, NEVER rejects, NEVER writes to stdout.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} [deps]
|
|
102
|
+
* @param {() => Promise<any>} [deps.probe] defaults to
|
|
103
|
+
* `() => require('./brain-client.cjs').callTool('theo_health', {})`,
|
|
104
|
+
* required LAZILY so importing this module costs nothing.
|
|
105
|
+
* @param {string} [deps.originHost] defaults to the host of
|
|
106
|
+
* `require('./brain-client.cjs').getBrainUrl()`.
|
|
107
|
+
* @param {() => Date} [deps.now] defaults to `() => new Date()`.
|
|
108
|
+
* @param {string} [deps.homeDir] defaults to MINDRIAN_HOME or ~/.mindrian.
|
|
109
|
+
* @param {number} [deps.timeoutMs] defaults to
|
|
110
|
+
* MINDRIAN_BRAIN_PREWARM_TIMEOUT_MS or 15000.
|
|
111
|
+
* @returns {Promise<{ at: string, ok: boolean, origin_host: string|null }>}
|
|
112
|
+
*/
|
|
113
|
+
async function prewarm(deps) {
|
|
114
|
+
deps = deps || {};
|
|
115
|
+
const probe = deps.probe || (() => require('./brain-client.cjs').callTool('theo_health', {}));
|
|
116
|
+
const originHost = (typeof deps.originHost === 'string')
|
|
117
|
+
? deps.originHost
|
|
118
|
+
: _hostOnly((() => {
|
|
119
|
+
try {
|
|
120
|
+
return require('./brain-client.cjs').getBrainUrl();
|
|
121
|
+
} catch (_e) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
})());
|
|
125
|
+
const now = deps.now || (() => new Date());
|
|
126
|
+
const homeDir = deps.homeDir;
|
|
127
|
+
const timeoutMs = Number.isFinite(deps.timeoutMs) && deps.timeoutMs > 0 ? deps.timeoutMs : _resolveTimeoutMs();
|
|
128
|
+
|
|
129
|
+
let ok = false;
|
|
130
|
+
try {
|
|
131
|
+
const TIMEOUT_SENTINEL = Symbol('brain-prewarm-timeout');
|
|
132
|
+
const result = await Promise.race([
|
|
133
|
+
Promise.resolve()
|
|
134
|
+
.then(() => probe())
|
|
135
|
+
.catch(() => null),
|
|
136
|
+
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs)),
|
|
137
|
+
]);
|
|
138
|
+
ok = !!(result && result !== TIMEOUT_SENTINEL && typeof result === 'object');
|
|
139
|
+
} catch (_e) {
|
|
140
|
+
ok = false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Canon Part 8 (D-02): EXACTLY three keys, nothing else. Never any part
|
|
144
|
+
// of the probe response body, never a key, never a question.
|
|
145
|
+
const marker = { at: now().toISOString(), ok: ok, origin_host: originHost || null };
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const filePath = markerPath(homeDir);
|
|
149
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
150
|
+
fs.writeFileSync(filePath, JSON.stringify(marker), 'utf8');
|
|
151
|
+
debugLog('marker written: ' + JSON.stringify(marker));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
debugLog('marker write failed: ' + (e && e.message ? e.message : String(e)));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return marker;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Directly spawnable: `node lib/core/brain-prewarm.cjs`.
|
|
160
|
+
if (require.main === module) {
|
|
161
|
+
prewarm().catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { prewarm, markerPath };
|