@drakon-systems/multi-clawd 1.4.2 → 1.5.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/dist/explain-core.js +26 -0
- package/dist/health.js +22 -0
- package/dist/models.js +2 -0
- package/dist/shim-core.js +10 -1
- package/openclaw.plugin.json +46 -12
- package/package.json +4 -2
- package/scripts/cli.mjs +6 -2
package/dist/explain-core.js
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
const WINDOW_LABELS = {
|
|
2
|
+
five_hour: "5-hour",
|
|
3
|
+
seven_day: "weekly",
|
|
4
|
+
seven_day_overage_included: "weekly incl. overage",
|
|
5
|
+
};
|
|
6
|
+
export function relativeUntil(ms, nowMs) {
|
|
7
|
+
const mins = Math.max(0, Math.round((ms - nowMs) / 60000));
|
|
8
|
+
if (mins < 90)
|
|
9
|
+
return `~${mins}m`;
|
|
10
|
+
const hours = Math.round(mins / 60);
|
|
11
|
+
if (hours < 36)
|
|
12
|
+
return `~${hours}h`;
|
|
13
|
+
return `~${Math.round(hours / 24)}d`;
|
|
14
|
+
}
|
|
15
|
+
export function renderUsageLine(usage, nowMs) {
|
|
16
|
+
return usage
|
|
17
|
+
.map((u) => {
|
|
18
|
+
const label = WINDOW_LABELS[u.window] ?? u.window;
|
|
19
|
+
const pct = `${Math.round(u.utilization * 100)}%`;
|
|
20
|
+
const reset = u.resetsAt !== undefined ? ` (resets ${relativeUntil(u.resetsAt, nowMs)})` : "";
|
|
21
|
+
return `${label} ${pct}${reset}`;
|
|
22
|
+
})
|
|
23
|
+
.join(" · ");
|
|
24
|
+
}
|
|
1
25
|
export function describeAccount(acc) {
|
|
2
26
|
if (acc.native) {
|
|
3
27
|
return "the machine's main `claude` login (default config dir; OS keychain on macOS)";
|
|
@@ -82,10 +106,12 @@ export function renderExplanation(model) {
|
|
|
82
106
|
}
|
|
83
107
|
lines.push("");
|
|
84
108
|
if (model.health.length > 0) {
|
|
109
|
+
const nowMs = model.nowMs ?? Date.now();
|
|
85
110
|
lines.push("RIGHT NOW");
|
|
86
111
|
for (const h of model.health) {
|
|
87
112
|
const word = VERDICT_WORDS[h.verdict] ?? h.verdict;
|
|
88
113
|
lines.push(` ${h.id}: ${word}${h.detail ? ` — ${h.detail}` : ""}`);
|
|
114
|
+
lines.push(` usage: ${h.usage?.length ? renderUsageLine(h.usage, nowMs) : "no live telemetry"}`);
|
|
89
115
|
}
|
|
90
116
|
if (model.pool) {
|
|
91
117
|
lines.push(model.stickyAccount
|
package/dist/health.js
CHANGED
|
@@ -72,6 +72,28 @@ export function classifyAccountHealth(state, options, nowMs, requestedModel) {
|
|
|
72
72
|
return { verdict: "no_data" };
|
|
73
73
|
return worst;
|
|
74
74
|
}
|
|
75
|
+
export function summarizeWindowUsage(state, options, nowMs) {
|
|
76
|
+
if (!state)
|
|
77
|
+
return [];
|
|
78
|
+
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
79
|
+
const usage = [];
|
|
80
|
+
for (const [window, w] of Object.entries(state.windows)) {
|
|
81
|
+
if (window.startsWith(MODEL_WINDOW_PREFIX))
|
|
82
|
+
continue;
|
|
83
|
+
if (typeof w.utilization !== "number")
|
|
84
|
+
continue;
|
|
85
|
+
const resetMs = typeof w.resetsAt === "number" ? w.resetsAt * 1000 : undefined;
|
|
86
|
+
const resetBearing = resetMs !== undefined && resetMs > nowMs;
|
|
87
|
+
if (resetBearing && nowMs - w.seenAt > MAX_RESET_HORIZON_MS)
|
|
88
|
+
continue;
|
|
89
|
+
if (resetMs !== undefined && !resetBearing)
|
|
90
|
+
continue;
|
|
91
|
+
if (!resetBearing && nowMs - w.seenAt > staleAfterMs)
|
|
92
|
+
continue;
|
|
93
|
+
usage.push({ window, utilization: w.utilization, resetsAt: resetMs });
|
|
94
|
+
}
|
|
95
|
+
return usage.sort((a, b) => (b.resetsAt ?? 0) - (a.resetsAt ?? 0));
|
|
96
|
+
}
|
|
75
97
|
export function choosePoolAccount(pool) {
|
|
76
98
|
const usable = pool.find((a) => a.verdict === "ok" || a.verdict === "no_data");
|
|
77
99
|
if (usable)
|
package/dist/models.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const MODEL_ALIASES = {
|
|
2
2
|
opus: "opus",
|
|
3
|
+
"opus-5": "claude-opus-5",
|
|
3
4
|
"opus-4.8": "claude-opus-4-8",
|
|
4
5
|
"opus-4.7": "claude-opus-4-7",
|
|
5
6
|
"opus-4.6": "claude-opus-4-6",
|
|
@@ -8,6 +9,7 @@ export const MODEL_ALIASES = {
|
|
|
8
9
|
haiku: "haiku",
|
|
9
10
|
};
|
|
10
11
|
const KNOWN_SPECS = {
|
|
12
|
+
"claude-opus-5": { name: "Claude Opus 5", contextWindow: 1000000, maxTokens: 128000 },
|
|
11
13
|
"claude-opus-4-8": { name: "Claude Opus 4.8", contextWindow: 1048576, maxTokens: 128000 },
|
|
12
14
|
"claude-opus-4-7": { name: "Claude Opus 4.7", contextWindow: 1048576, maxTokens: 64000 },
|
|
13
15
|
"claude-opus-4-6": { name: "Claude Opus 4.6", contextWindow: 1048576, maxTokens: 64000 },
|
package/dist/shim-core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const PRUNE_AFTER_MS = 14 * 24 * 60 * 60 * 1000;
|
|
2
|
+
export const EXPIRED_REJECTED_GRACE_MS = 5 * 60 * 1000;
|
|
2
3
|
const RAW_INFO_MAX_CHARS = 512;
|
|
3
4
|
export function createLineScanner(onLine) {
|
|
4
5
|
let buffer = "";
|
|
@@ -106,8 +107,16 @@ export function mergeHealthStates(disk, live, now, pruneAfterMs = PRUNE_AFTER_MS
|
|
|
106
107
|
const windows = canonicalizeModelWindowKeys(merged);
|
|
107
108
|
if (now !== undefined) {
|
|
108
109
|
for (const [key, w] of Object.entries(windows)) {
|
|
109
|
-
if (now - w.seenAt > pruneAfterMs)
|
|
110
|
+
if (now - w.seenAt > pruneAfterMs) {
|
|
110
111
|
delete windows[key];
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const resetMs = typeof w.resetsAt === "number" ? w.resetsAt * 1000 : undefined;
|
|
115
|
+
if (w.status === "rejected" &&
|
|
116
|
+
resetMs !== undefined &&
|
|
117
|
+
now - resetMs > EXPIRED_REJECTED_GRACE_MS) {
|
|
118
|
+
delete windows[key];
|
|
119
|
+
}
|
|
111
120
|
}
|
|
112
121
|
}
|
|
113
122
|
const updatedAt = Math.max(disk.updatedAt ?? 0, live.updatedAt ?? 0);
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "multi-clawd",
|
|
3
3
|
"name": "multi-clawd",
|
|
4
|
+
"version": "1.5.1",
|
|
4
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.",
|
|
5
|
-
"cliBackends": [
|
|
6
|
-
|
|
6
|
+
"cliBackends": [
|
|
7
|
+
"claw1",
|
|
8
|
+
"claw2",
|
|
9
|
+
"clawd"
|
|
10
|
+
],
|
|
11
|
+
"providers": [
|
|
12
|
+
"claw1",
|
|
13
|
+
"claw2",
|
|
14
|
+
"clawd"
|
|
15
|
+
],
|
|
7
16
|
"modelCatalog": {
|
|
8
17
|
"runtimeAugment": true
|
|
9
18
|
},
|
|
@@ -29,7 +38,9 @@
|
|
|
29
38
|
},
|
|
30
39
|
"accounts": {
|
|
31
40
|
"type": "array",
|
|
32
|
-
"items": {
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
},
|
|
33
44
|
"description": "Account ids (from accounts[]) in preference order; the first is the home account and reclaims the pool when its usage window resets."
|
|
34
45
|
},
|
|
35
46
|
"utilizationThreshold": {
|
|
@@ -51,7 +62,9 @@
|
|
|
51
62
|
"properties": {
|
|
52
63
|
"ladder": {
|
|
53
64
|
"type": "array",
|
|
54
|
-
"items": {
|
|
65
|
+
"items": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
},
|
|
55
68
|
"description": "Same-provider models to step down to, best first (e.g. [\"claude-opus-4-8\"]). Requests already at/below the ladder never degrade further."
|
|
56
69
|
},
|
|
57
70
|
"pins": {
|
|
@@ -61,8 +74,14 @@
|
|
|
61
74
|
"type": "object",
|
|
62
75
|
"additionalProperties": false,
|
|
63
76
|
"properties": {
|
|
64
|
-
"agentDirIncludes": {
|
|
65
|
-
|
|
77
|
+
"agentDirIncludes": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "Pin when the launching agent's dir contains this substring."
|
|
80
|
+
},
|
|
81
|
+
"workspaceDirIncludes": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "Pin when the workspace dir contains this substring."
|
|
84
|
+
}
|
|
66
85
|
}
|
|
67
86
|
}
|
|
68
87
|
}
|
|
@@ -70,7 +89,9 @@
|
|
|
70
89
|
},
|
|
71
90
|
"models": {
|
|
72
91
|
"type": "array",
|
|
73
|
-
"items": {
|
|
92
|
+
"items": {
|
|
93
|
+
"type": "string"
|
|
94
|
+
},
|
|
74
95
|
"description": "Extra model ids to expose on the pool backend."
|
|
75
96
|
},
|
|
76
97
|
"defaultModel": {
|
|
@@ -85,7 +106,9 @@
|
|
|
85
106
|
"items": {
|
|
86
107
|
"type": "object",
|
|
87
108
|
"additionalProperties": false,
|
|
88
|
-
"required": [
|
|
109
|
+
"required": [
|
|
110
|
+
"id"
|
|
111
|
+
],
|
|
89
112
|
"properties": {
|
|
90
113
|
"id": {
|
|
91
114
|
"type": "string",
|
|
@@ -112,14 +135,25 @@
|
|
|
112
135
|
"description": "Secret reference resolving to this account's Claude Code setup-token via the gateway's configured secret providers — same shape as every other secret in openclaw.json, e.g. {\"source\":\"exec\",\"provider\":\"onepassword\",\"id\":\"op://Vault/Item/field\"}. Preferred over oauthTokenFile (no plaintext on disk). Resolution failures degrade the account (auth fails, chain steps) rather than crashing the launch.",
|
|
113
136
|
"additionalProperties": true,
|
|
114
137
|
"properties": {
|
|
115
|
-
"source": {
|
|
116
|
-
|
|
117
|
-
|
|
138
|
+
"source": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"description": "Secret source kind: env | file | exec."
|
|
141
|
+
},
|
|
142
|
+
"provider": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"description": "Configured secret provider name, e.g. \"onepassword\"."
|
|
145
|
+
},
|
|
146
|
+
"id": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"description": "Provider-scoped secret id, e.g. an op:// reference."
|
|
149
|
+
}
|
|
118
150
|
}
|
|
119
151
|
},
|
|
120
152
|
"models": {
|
|
121
153
|
"type": "array",
|
|
122
|
-
"items": {
|
|
154
|
+
"items": {
|
|
155
|
+
"type": "string"
|
|
156
|
+
},
|
|
123
157
|
"description": "Extra model ids to expose for this account beyond the mirrored claude-cli catalog (e.g. a brand-new model OpenClaw doesn't list yet). Unknown modern claude-* ids also resolve on demand without being listed here."
|
|
124
158
|
},
|
|
125
159
|
"defaultModel": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakon-systems/multi-clawd",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
"test:watch": "vitest",
|
|
56
56
|
"prepare": "npm run build",
|
|
57
57
|
"doctor": "node scripts/doctor.mjs",
|
|
58
|
-
"setup": "node scripts/setup.mjs"
|
|
58
|
+
"setup": "node scripts/setup.mjs",
|
|
59
|
+
"sync-manifest": "node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('openclaw.plugin.json','utf8'));m.version=process.env.npm_package_version||JSON.parse(fs.readFileSync('package.json','utf8')).version;fs.writeFileSync('openclaw.plugin.json',JSON.stringify(m,null,2)+'\\n')\"",
|
|
60
|
+
"version": "npm run sync-manifest && git add openclaw.plugin.json"
|
|
59
61
|
},
|
|
60
62
|
"peerDependencies": {
|
|
61
63
|
"openclaw": ">=2026.6"
|
package/scripts/cli.mjs
CHANGED
|
@@ -253,7 +253,11 @@ async function explain() {
|
|
|
253
253
|
if (h.verdict === "exhausted" && h.resumeAt) {
|
|
254
254
|
detail = `${h.reason ?? "limit hit"} — back in ${rel(h.resumeAt)}`;
|
|
255
255
|
}
|
|
256
|
-
|
|
256
|
+
const usage = health.summarizeWindowUsage(state, {
|
|
257
|
+
utilizationThreshold: pool?.utilizationThreshold,
|
|
258
|
+
staleAfterMs: pool?.staleAfterMs,
|
|
259
|
+
}, now);
|
|
260
|
+
return { id: a.id, verdict: h.verdict, detail, usage };
|
|
257
261
|
});
|
|
258
262
|
let stickyAccount;
|
|
259
263
|
if (pool) {
|
|
@@ -266,7 +270,7 @@ async function explain() {
|
|
|
266
270
|
}
|
|
267
271
|
console.log(`\n${BOLD}🦞 multi-clawd — your setup, in plain English${RESET}\n`);
|
|
268
272
|
console.log(
|
|
269
|
-
ec.renderExplanation({ accounts, pool, chain, health: healthRows, stickyAccount }),
|
|
273
|
+
ec.renderExplanation({ accounts, pool, chain, health: healthRows, stickyAccount, nowMs: now }),
|
|
270
274
|
);
|
|
271
275
|
console.log(`\n${DIM}(health checks: multi-clawd doctor · change things: multi-clawd setup)${RESET}`);
|
|
272
276
|
}
|