@drakon-systems/multi-clawd 1.7.4 → 1.8.1
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.md +1 -1
- package/dist/alerts.js +3 -0
- package/dist/credential-state.js +30 -0
- package/dist/health.js +51 -10
- package/dist/index.js +35 -37
- package/dist/shim-core.js +3 -1
- package/dist/shim.js +1 -1
- package/openclaw.plugin.json +5 -1
- package/package.json +1 -1
- package/scripts/cli.mjs +41 -10
package/README.md
CHANGED
|
@@ -364,7 +364,7 @@ openclaw plugins install (Get-Location).Path
|
|
|
364
364
|
**Or let your agent install it.** Running an OpenClaw assistant or Claude
|
|
365
365
|
Code on the target machine already? Paste it this and go make coffee:
|
|
366
366
|
|
|
367
|
-
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.
|
|
367
|
+
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.8.1/SETUP-AGENT.md
|
|
368
368
|
> and follow it to set up multi-clawd on this machine. I own a second
|
|
369
369
|
> Claude account — ask me when you need me to log in.
|
|
370
370
|
|
package/dist/alerts.js
CHANGED
|
@@ -13,6 +13,9 @@ export function addAlert(state, alert, nowMs) {
|
|
|
13
13
|
export function clearAlert(state, key) {
|
|
14
14
|
return { alerts: state.alerts.filter((a) => a.key !== key) };
|
|
15
15
|
}
|
|
16
|
+
export function alertKeysWithPrefix(state, prefix) {
|
|
17
|
+
return state.alerts.filter((a) => a.key.startsWith(prefix)).map((a) => a.key);
|
|
18
|
+
}
|
|
16
19
|
function isLive(alert, nowMs) {
|
|
17
20
|
const ttl = alert.ttlMs ?? DEFAULT_TTL_MS[alert.severity];
|
|
18
21
|
return nowMs - alert.at <= ttl;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { parseStoredState, mergeHealthStates, clearCredentialFailure, } from "./shim-core.js";
|
|
5
|
+
export function healthStateFile(accountId) {
|
|
6
|
+
return join(homedir(), ".openclaw", "state", "multi-clawd", `${accountId}.json`);
|
|
7
|
+
}
|
|
8
|
+
export function clearAccountCredentialFailure(accountId) {
|
|
9
|
+
const file = healthStateFile(accountId);
|
|
10
|
+
let state;
|
|
11
|
+
try {
|
|
12
|
+
state = parseStoredState(readFileSync(file, "utf8")) ?? { accountId, windows: {} };
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (state.credential?.status !== "failed")
|
|
18
|
+
return false;
|
|
19
|
+
const cleared = mergeHealthStates(state, clearCredentialFailure(state, Date.now()), Date.now());
|
|
20
|
+
try {
|
|
21
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
22
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
23
|
+
writeFileSync(tmp, JSON.stringify(cleared, null, 2), { mode: 0o600 });
|
|
24
|
+
renameSync(tmp, file);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/health.js
CHANGED
|
@@ -16,10 +16,24 @@ const PERIOD_WINDOW_PATTERN = /(^|_)(minutes?|hours?|days?|weeks?|months?)(_|$)/
|
|
|
16
16
|
export function isPeriodWindow(window) {
|
|
17
17
|
return PERIOD_WINDOW_PATTERN.test(window);
|
|
18
18
|
}
|
|
19
|
+
export function isOverageWindow(window) {
|
|
20
|
+
return /overage/i.test(window);
|
|
21
|
+
}
|
|
19
22
|
export function isWarningStatus(status) {
|
|
20
23
|
return /warning/i.test(status);
|
|
21
24
|
}
|
|
22
25
|
export const MAX_RESET_HORIZON_MS = 8 * 24 * 60 * 60 * 1000;
|
|
26
|
+
export const PERIOD_RESET_LESS_BLOCK_MS = 6 * 60 * 60 * 1000;
|
|
27
|
+
export function resetLessBlockMs(window) {
|
|
28
|
+
return isShortWindow(window) ? MODEL_REJECTED_TTL_MS : PERIOD_RESET_LESS_BLOCK_MS;
|
|
29
|
+
}
|
|
30
|
+
function windowAppliesToModel(w, requestedWindowKey) {
|
|
31
|
+
if (w.model === undefined)
|
|
32
|
+
return true;
|
|
33
|
+
if (requestedWindowKey === undefined)
|
|
34
|
+
return false;
|
|
35
|
+
return modelWindowKey(w.model) === requestedWindowKey;
|
|
36
|
+
}
|
|
23
37
|
export function credentialFailureFor(state, nowMs) {
|
|
24
38
|
const credential = state?.credential;
|
|
25
39
|
if (!credential || credential.status !== "failed")
|
|
@@ -43,6 +57,8 @@ export function classifyAccountHealth(state, options, nowMs, requestedModel) {
|
|
|
43
57
|
const requestedWindowKey = requestedModel !== undefined ? modelWindowKey(requestedModel) : undefined;
|
|
44
58
|
let worst = { verdict: "ok" };
|
|
45
59
|
let hasLiveEvidence = false;
|
|
60
|
+
let overageUtilization;
|
|
61
|
+
let quotaUtilization;
|
|
46
62
|
for (const [window, w] of Object.entries(state.windows)) {
|
|
47
63
|
const resetMs = typeof w.resetsAt === "number" ? w.resetsAt * 1000 : undefined;
|
|
48
64
|
const resetBearing = resetMs !== undefined && resetMs > nowMs;
|
|
@@ -78,13 +94,17 @@ export function classifyAccountHealth(state, options, nowMs, requestedModel) {
|
|
|
78
94
|
reason: `${requestedModel} limit hit ${Math.round((nowMs - w.seenAt) / 60000)}m ago (no reset time; TTL block)`,
|
|
79
95
|
};
|
|
80
96
|
}
|
|
81
|
-
const
|
|
97
|
+
const ownBlockMs = w.status === "rejected" && resetMs === undefined && isPeriodWindow(window)
|
|
98
|
+
? resetLessBlockMs(window)
|
|
99
|
+
: 0;
|
|
100
|
+
const fresh = resetBearing || nowMs - w.seenAt <= Math.max(staleAfterMs, ownBlockMs);
|
|
82
101
|
if (!fresh)
|
|
83
102
|
continue;
|
|
84
103
|
hasLiveEvidence = true;
|
|
85
104
|
if (w.status === "rejected" &&
|
|
86
105
|
resetBearing &&
|
|
87
106
|
isPeriodWindow(window) &&
|
|
107
|
+
windowAppliesToModel(w, requestedWindowKey) &&
|
|
88
108
|
rejectionStillAssertable(w.seenAt, nowMs)) {
|
|
89
109
|
return {
|
|
90
110
|
verdict: "exhausted",
|
|
@@ -92,21 +112,32 @@ export function classifyAccountHealth(state, options, nowMs, requestedModel) {
|
|
|
92
112
|
reason: `${window} rejected until ${new Date(resetMs).toISOString()}`,
|
|
93
113
|
};
|
|
94
114
|
}
|
|
95
|
-
if (w.status === "rejected" &&
|
|
115
|
+
if (w.status === "rejected" &&
|
|
116
|
+
resetMs === undefined &&
|
|
117
|
+
isPeriodWindow(window) &&
|
|
118
|
+
windowAppliesToModel(w, requestedWindowKey) &&
|
|
119
|
+
nowMs - w.seenAt <= resetLessBlockMs(window)) {
|
|
96
120
|
return {
|
|
97
121
|
verdict: "exhausted",
|
|
98
|
-
resumeAt: w.seenAt +
|
|
99
|
-
reason: `${window} rejected ${Math.round((nowMs - w.seenAt) / 60000)}m ago (no reset time;
|
|
122
|
+
resumeAt: w.seenAt + resetLessBlockMs(window),
|
|
123
|
+
reason: `${window} rejected ${Math.round((nowMs - w.seenAt) / 60000)}m ago (no reset time; ${Math.round(resetLessBlockMs(window) / 3600000)}h block)`,
|
|
100
124
|
};
|
|
101
125
|
}
|
|
102
|
-
if (
|
|
103
|
-
typeof w.utilization === "number" &&
|
|
126
|
+
if (typeof w.utilization === "number" &&
|
|
104
127
|
w.utilization >= threshold &&
|
|
105
128
|
(resetBearing || resetMs === undefined)) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
if (isOverageWindow(window) && !options.rotateOnOverage) {
|
|
130
|
+
overageUtilization = Math.max(overageUtilization ?? 0, w.utilization);
|
|
131
|
+
}
|
|
132
|
+
else if (worst.verdict === "ok") {
|
|
133
|
+
worst = {
|
|
134
|
+
verdict: "near_limit",
|
|
135
|
+
reason: `${window} utilization ${w.utilization} >= ${threshold}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!isOverageWindow(window) && typeof w.utilization === "number") {
|
|
140
|
+
quotaUtilization = Math.max(quotaUtilization ?? 0, w.utilization);
|
|
110
141
|
}
|
|
111
142
|
if (worst.verdict === "ok" &&
|
|
112
143
|
typeof w.utilization !== "number" &&
|
|
@@ -121,6 +152,16 @@ export function classifyAccountHealth(state, options, nowMs, requestedModel) {
|
|
|
121
152
|
}
|
|
122
153
|
if (worst.verdict === "ok" && !hasLiveEvidence)
|
|
123
154
|
return { verdict: "no_data" };
|
|
155
|
+
if (worst.verdict === "ok" && overageUtilization !== undefined) {
|
|
156
|
+
const pct = (n) => `${Math.round(n * 100)}%`;
|
|
157
|
+
worst = {
|
|
158
|
+
...worst,
|
|
159
|
+
overageAdvisory: `paid overage is ${pct(overageUtilization)} spent while real quota is at ` +
|
|
160
|
+
`${quotaUtilization !== undefined ? pct(quotaUtilization) : "an unreported level"} — ` +
|
|
161
|
+
`not rotating on spill-over alone. Set pool.rotateOnOverage: true to rotate on it, ` +
|
|
162
|
+
`or leave it to keep using the overage you have paid for.`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
124
165
|
return worst;
|
|
125
166
|
}
|
|
126
167
|
export function summarizeWindowUsage(state, options, nowMs) {
|
package/dist/index.js
CHANGED
|
@@ -11,10 +11,12 @@ import { resolveExecMode, permissionModeArgs } from "./exec-policy.js";
|
|
|
11
11
|
import { resolveBaseModelIds } from "./catalog-source.js";
|
|
12
12
|
import { allCredentialFailed, classifyAccountHealth } from "./health.js";
|
|
13
13
|
import { decideStickySelection } from "./sticky.js";
|
|
14
|
-
import {
|
|
14
|
+
import { mergeHealthStates, parseStoredState, recordCredentialFailure, } from "./shim-core.js";
|
|
15
15
|
import { createTokenRefResolver, isSecretRefShape, } from "./token-resolution.js";
|
|
16
16
|
import { resolveSecretRefValues } from "openclaw/plugin-sdk/secret-ref-runtime";
|
|
17
|
-
import { addAlert, clearAlert, pendingAlertText } from "./alerts.js";
|
|
17
|
+
import { addAlert, alertKeysWithPrefix, clearAlert, pendingAlertText, } from "./alerts.js";
|
|
18
|
+
import { healthStateFile, clearAccountCredentialFailure } from "./credential-state.js";
|
|
19
|
+
export { healthStateFile, clearAccountCredentialFailure };
|
|
18
20
|
import { buildAccountChildEnv, tokenFileModeWarning, validateAccountTokenSources, } from "./account-env.js";
|
|
19
21
|
import { diffCatalogModels, formatNewModelNotice, } from "./model-currency.js";
|
|
20
22
|
import { checkAccountCredential, createRefProbeTracker, } from "./login-health.js";
|
|
@@ -69,6 +71,10 @@ let loginProbeTimer;
|
|
|
69
71
|
function raiseAlert(alert) {
|
|
70
72
|
alertState = addAlert(alertState, alert, Date.now());
|
|
71
73
|
}
|
|
74
|
+
export function pendingOperatorAlerts(nowMs) {
|
|
75
|
+
ingestAlertSpool();
|
|
76
|
+
return pendingAlertText(alertState, nowMs);
|
|
77
|
+
}
|
|
72
78
|
function ingestAlertSpool() {
|
|
73
79
|
const spool = join(homedir(), ".openclaw", "state", "multi-clawd", "alerts-spool.jsonl");
|
|
74
80
|
let raw;
|
|
@@ -281,9 +287,6 @@ function buildRuntimeModel(account, modelId) {
|
|
|
281
287
|
};
|
|
282
288
|
}
|
|
283
289
|
const SHIM_PATH = fileURLToPath(new URL("./shim.js", import.meta.url));
|
|
284
|
-
export function healthStateFile(accountId) {
|
|
285
|
-
return join(homedir(), ".openclaw", "state", "multi-clawd", `${accountId}.json`);
|
|
286
|
-
}
|
|
287
290
|
function knownModelsFile() {
|
|
288
291
|
return join(homedir(), ".openclaw", "state", "multi-clawd", "known-models.json");
|
|
289
292
|
}
|
|
@@ -476,8 +479,7 @@ export default definePluginEntry({
|
|
|
476
479
|
const logger = api.logger;
|
|
477
480
|
try {
|
|
478
481
|
api.on("heartbeat_prompt_contribution", () => {
|
|
479
|
-
|
|
480
|
-
const text = pendingAlertText(alertState, Date.now());
|
|
482
|
+
const text = pendingOperatorAlerts(Date.now());
|
|
481
483
|
return text ? { appendContext: text } : undefined;
|
|
482
484
|
});
|
|
483
485
|
}
|
|
@@ -497,32 +499,6 @@ function readHealthState(accountId) {
|
|
|
497
499
|
return undefined;
|
|
498
500
|
}
|
|
499
501
|
}
|
|
500
|
-
export function clearAccountCredentialFailure(accountId) {
|
|
501
|
-
const file = healthStateFile(accountId);
|
|
502
|
-
let state;
|
|
503
|
-
try {
|
|
504
|
-
state = parseStoredState(readFileSync(file, "utf8")) ?? {
|
|
505
|
-
accountId,
|
|
506
|
-
windows: {},
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
catch {
|
|
510
|
-
return false;
|
|
511
|
-
}
|
|
512
|
-
if (state.credential?.status !== "failed")
|
|
513
|
-
return false;
|
|
514
|
-
const cleared = mergeHealthStates(state, clearCredentialFailure(state, Date.now()), Date.now());
|
|
515
|
-
try {
|
|
516
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
517
|
-
const tmp = `${file}.tmp-${process.pid}`;
|
|
518
|
-
writeFileSync(tmp, JSON.stringify(cleared, null, 2), { mode: 0o600 });
|
|
519
|
-
renameSync(tmp, file);
|
|
520
|
-
return true;
|
|
521
|
-
}
|
|
522
|
-
catch {
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
502
|
function readStickyEntry(file) {
|
|
527
503
|
try {
|
|
528
504
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
@@ -576,6 +552,7 @@ export function registerPoolBackend(api, pool, accounts, registeredIds, execMode
|
|
|
576
552
|
const options = {
|
|
577
553
|
utilizationThreshold: pool.utilizationThreshold,
|
|
578
554
|
staleAfterMs: pool.staleAfterMs,
|
|
555
|
+
rotateOnOverage: pool.rotateOnOverage,
|
|
579
556
|
};
|
|
580
557
|
const poolAccount = {
|
|
581
558
|
id: poolId,
|
|
@@ -589,9 +566,10 @@ export function registerPoolBackend(api, pool, accounts, registeredIds, execMode
|
|
|
589
566
|
backend.prepareExecution = async (ctx) => {
|
|
590
567
|
const now = Date.now();
|
|
591
568
|
const requestedModel = canonicalModelId(ctx.modelId) ?? ctx.modelId;
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
569
|
+
const states = members.map((a) => ({ id: a.id, state: readHealthState(a.id) }));
|
|
570
|
+
const verdicts = states.map(({ id, state }) => ({
|
|
571
|
+
id,
|
|
572
|
+
health: classifyAccountHealth(state, options, now, requestedModel),
|
|
595
573
|
}));
|
|
596
574
|
if (allCredentialFailed(verdicts.map((v) => ({ id: v.id, verdict: v.health.verdict })))) {
|
|
597
575
|
const detail = verdicts
|
|
@@ -621,13 +599,33 @@ export function registerPoolBackend(api, pool, accounts, registeredIds, execMode
|
|
|
621
599
|
logger.info(`[multi-clawd] ${line}`);
|
|
622
600
|
raiseAlert({ key: `rotation:${poolId}`, severity: "info", text: line });
|
|
623
601
|
}
|
|
602
|
+
const exhaustionPrefix = `pool-exhausted:${poolId}:`;
|
|
603
|
+
for (const key of alertKeysWithPrefix(alertState, exhaustionPrefix)) {
|
|
604
|
+
const alertedModel = key.slice(exhaustionPrefix.length);
|
|
605
|
+
const stillExhausted = states.every(({ state }) => classifyAccountHealth(state, options, now, alertedModel).verdict === "exhausted");
|
|
606
|
+
if (!stillExhausted)
|
|
607
|
+
alertState = clearAlert(alertState, key);
|
|
608
|
+
}
|
|
624
609
|
if (verdicts.every((v) => v.health.verdict === "exhausted")) {
|
|
625
610
|
raiseAlert({
|
|
626
|
-
key:
|
|
611
|
+
key: `${exhaustionPrefix}${requestedModel}`,
|
|
627
612
|
severity: "error",
|
|
628
613
|
text: `pool ${poolId}: every account is exhausted for ${requestedModel} — turns are degrading or falling through the chain`,
|
|
629
614
|
});
|
|
630
615
|
}
|
|
616
|
+
for (const v of verdicts) {
|
|
617
|
+
const key = `overage:${poolId}:${v.id}`;
|
|
618
|
+
if (v.health.overageAdvisory) {
|
|
619
|
+
raiseAlert({
|
|
620
|
+
key,
|
|
621
|
+
severity: "info",
|
|
622
|
+
text: `pool ${poolId}: account "${v.id}" — ${v.health.overageAdvisory}`,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
alertState = clearAlert(alertState, key);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
631
629
|
for (const v of verdicts) {
|
|
632
630
|
const key = `credential:${poolId}:${v.id}`;
|
|
633
631
|
if (v.health.verdict === "credential_failed") {
|
package/dist/shim-core.js
CHANGED
|
@@ -89,6 +89,7 @@ export function parseStoredState(raw) {
|
|
|
89
89
|
isUsingOverage: typeof w.isUsingOverage === "boolean" ? w.isUsingOverage : undefined,
|
|
90
90
|
seenAt: w.seenAt,
|
|
91
91
|
rawInfo: typeof w.rawInfo === "string" ? w.rawInfo : undefined,
|
|
92
|
+
model: typeof w.model === "string" ? w.model : undefined,
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
let credential;
|
|
@@ -291,7 +292,7 @@ export function clearCredentialFailure(state, now) {
|
|
|
291
292
|
credential: { status: "ok", seenAt: now },
|
|
292
293
|
};
|
|
293
294
|
}
|
|
294
|
-
export function updateHealthState(state, event, now) {
|
|
295
|
+
export function updateHealthState(state, event, now, model) {
|
|
295
296
|
const key = event.rateLimitType ?? "unknown";
|
|
296
297
|
return {
|
|
297
298
|
...state,
|
|
@@ -305,6 +306,7 @@ export function updateHealthState(state, event, now) {
|
|
|
305
306
|
isUsingOverage: event.isUsingOverage,
|
|
306
307
|
seenAt: now,
|
|
307
308
|
rawInfo: event.rawInfo,
|
|
309
|
+
model: model === undefined ? undefined : canonicalizeModelIdForWindow(model),
|
|
308
310
|
},
|
|
309
311
|
},
|
|
310
312
|
};
|
package/dist/shim.js
CHANGED
|
@@ -113,7 +113,7 @@ const scanner = createLineScanner((line) => {
|
|
|
113
113
|
try {
|
|
114
114
|
const event = parseRateLimitEvent(line);
|
|
115
115
|
if (event) {
|
|
116
|
-
state = updateHealthState(state, event, Date.now());
|
|
116
|
+
state = updateHealthState(state, event, Date.now(), effectiveModelId());
|
|
117
117
|
persistState();
|
|
118
118
|
}
|
|
119
119
|
const limitHit = parseModelLimitError(line);
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "multi-clawd",
|
|
3
3
|
"name": "multi-clawd",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.1",
|
|
5
5
|
"description": "Register additional Claude Code logins (Max/Pro accounts) as first-class OpenClaw CLI backends for cross-account failover, keeping the full skills/MCP harness on every account.",
|
|
6
6
|
"cliBackends": [
|
|
7
7
|
"claw1",
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
"type": "number",
|
|
52
52
|
"description": "Ignore health data older than this. Default 21600000 (6h)."
|
|
53
53
|
},
|
|
54
|
+
"rotateOnOverage": {
|
|
55
|
+
"type": "boolean",
|
|
56
|
+
"description": "Rotate when the seven_day_overage_included window (quota PLUS purchased spill-over) crosses utilizationThreshold. Default false: high utilization there means little paid overage left, not little quota left, so the pool keeps using the overage you have paid for and raises an operator alert instead of deciding for you."
|
|
57
|
+
},
|
|
54
58
|
"minDwellMs": {
|
|
55
59
|
"type": "number",
|
|
56
60
|
"description": "After rotating away from home, stay on the rotated-to account at least this long before returning home (anti-flap hysteresis; health always overrides). Default 600000 (10min)."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakon-systems/multi-clawd",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Multi-account Claude Code failover for OpenClaw — register additional Claude (Max/Pro) logins as first-class CLI backends and keep the full skills/MCP harness across every account. Also imports those accounts' setup tokens into Hermes Agent's Anthropic credential pool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/scripts/cli.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* has to remember `--pin --force`.
|
|
17
17
|
*/
|
|
18
18
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
19
|
-
import { readFileSync } from "node:fs";
|
|
19
|
+
import { readFileSync, existsSync as existsSyncEarly } from "node:fs";
|
|
20
20
|
import { dirname, join, resolve } from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import readline from "node:readline/promises";
|
|
@@ -92,6 +92,34 @@ async function askYes(question, dflt = true) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/** This package's own version (the CLI half). */
|
|
95
|
+
/**
|
|
96
|
+
* Why a `dist/` import failed, in the user's terms.
|
|
97
|
+
*
|
|
98
|
+
* The bare "reinstall the package" message was wrong in the one case that
|
|
99
|
+
* actually happens: this CLI installs globally, `openclaw` is a peerDependency,
|
|
100
|
+
* and on a box where the peer is not resolvable from this directory a perfectly
|
|
101
|
+
* complete build still throws ERR_MODULE_NOT_FOUND. Reinstalling cannot fix
|
|
102
|
+
* that, so "reinstall the package" sends people in circles — it did, on a Mac,
|
|
103
|
+
* 18 Aug 2026. Check the file exists first, then report the real cause.
|
|
104
|
+
*/
|
|
105
|
+
function distFailure(cmd, mod, err) {
|
|
106
|
+
const path = resolve(__dirname, "..", "dist", mod);
|
|
107
|
+
if (!existsSyncEarly(path)) {
|
|
108
|
+
return `${cmd}: built dist/${mod} is missing — reinstall the package.`;
|
|
109
|
+
}
|
|
110
|
+
const missingPeer = /Cannot find package '([^']+)'/.exec(String(err?.message ?? ""));
|
|
111
|
+
if (missingPeer) {
|
|
112
|
+
const peer = missingPeer[1];
|
|
113
|
+
return [
|
|
114
|
+
`${cmd}: dist/${mod} is present but cannot load — the "${peer}" package is not`,
|
|
115
|
+
`resolvable from this install (${resolve(__dirname, "..")}).`,
|
|
116
|
+
`Install "${peer}" globally alongside this CLI, or run the CLI with npx from a`,
|
|
117
|
+
`directory where "${peer}" resolves.`,
|
|
118
|
+
].join("\n ");
|
|
119
|
+
}
|
|
120
|
+
return `${cmd}: dist/${mod} failed to load — ${err?.message ?? err}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
95
123
|
function cliVersion() {
|
|
96
124
|
return JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf8")).version;
|
|
97
125
|
}
|
|
@@ -134,8 +162,8 @@ async function chain(args = []) {
|
|
|
134
162
|
let ca;
|
|
135
163
|
try {
|
|
136
164
|
ca = await import(resolve(__dirname, "..", "dist", "chain-audit.js"));
|
|
137
|
-
} catch {
|
|
138
|
-
console.error("chain
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error(distFailure("chain", "chain-audit.js", err));
|
|
139
167
|
process.exit(1);
|
|
140
168
|
}
|
|
141
169
|
|
|
@@ -227,8 +255,8 @@ async function update() {
|
|
|
227
255
|
let uc;
|
|
228
256
|
try {
|
|
229
257
|
uc = await import(resolve(__dirname, "..", "dist", "update-core.js"));
|
|
230
|
-
} catch {
|
|
231
|
-
console.error("update
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error(distFailure("update", "update-core.js", err));
|
|
232
260
|
process.exit(1);
|
|
233
261
|
}
|
|
234
262
|
console.log(`\n${BOLD}🦞 multi-clawd update${RESET}\n`);
|
|
@@ -391,8 +419,8 @@ async function explain() {
|
|
|
391
419
|
ec = await import(resolve(__dirname, "..", "dist", "explain-core.js"));
|
|
392
420
|
health = await import(resolve(__dirname, "..", "dist", "health.js"));
|
|
393
421
|
shim = await import(resolve(__dirname, "..", "dist", "shim-core.js"));
|
|
394
|
-
} catch {
|
|
395
|
-
console.error("explain
|
|
422
|
+
} catch (err) {
|
|
423
|
+
console.error(distFailure("explain", "explain-core.js", err));
|
|
396
424
|
process.exit(1);
|
|
397
425
|
}
|
|
398
426
|
let config = {};
|
|
@@ -465,9 +493,12 @@ async function login() {
|
|
|
465
493
|
try {
|
|
466
494
|
lp = await import(resolve(__dirname, "..", "dist", "login-plan.js"));
|
|
467
495
|
ec = await import(resolve(__dirname, "..", "dist", "explain-core.js"));
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
496
|
+
// credential-state.js, NOT index.js: this is the one call login needs from
|
|
497
|
+
// the plugin side, and index.js drags in the `openclaw` peer, which is not
|
|
498
|
+
// resolvable from a global CLI install on every machine.
|
|
499
|
+
idx = await import(resolve(__dirname, "..", "dist", "credential-state.js"));
|
|
500
|
+
} catch (err) {
|
|
501
|
+
console.error(distFailure("login", "login-plan.js", err));
|
|
471
502
|
process.exit(1);
|
|
472
503
|
}
|
|
473
504
|
let config = {};
|