@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.
@@ -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);
@@ -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": ["claw1", "claw2", "clawd"],
6
- "providers": ["claw1", "claw2", "clawd"],
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": { "type": "string" },
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": { "type": "string" },
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": { "type": "string", "description": "Pin when the launching agent's dir contains this substring." },
65
- "workspaceDirIncludes": { "type": "string", "description": "Pin when the workspace dir contains this substring." }
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": { "type": "string" },
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": ["id"],
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": { "type": "string", "description": "Secret source kind: env | file | exec." },
116
- "provider": { "type": "string", "description": "Configured secret provider name, e.g. \"onepassword\"." },
117
- "id": { "type": "string", "description": "Provider-scoped secret id, e.g. an op:// reference." }
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": { "type": "string" },
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.4.2",
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
- return { id: a.id, verdict: h.verdict, detail };
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
  }