@drakon-systems/multi-clawd 2.1.2 → 2.2.0
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 +31 -15
- package/SECURITY.md +22 -16
- package/dist/explain-core.js +7 -3
- package/dist/health.js +8 -2
- package/dist/index.js +4 -1
- package/openclaw.plugin.json +7 -1
- package/package.json +2 -2
- package/scripts/cli.mjs +12 -3
- package/scripts/doctor.mjs +6 -0
package/README.md
CHANGED
|
@@ -284,16 +284,15 @@ claude-cli/claude-fable-5 # main login
|
|
|
284
284
|
|
|
285
285
|
## Platform support
|
|
286
286
|
|
|
287
|
-
multi-clawd
|
|
288
|
-
|
|
289
|
-
Code backend runs.
|
|
287
|
+
multi-clawd uses Node without native modules. External commands are run with
|
|
288
|
+
argument arrays, never through a shell string. Launch mode varies by platform.
|
|
290
289
|
|
|
291
290
|
| Platform | Status |
|
|
292
291
|
|---|---|
|
|
293
292
|
| Linux | ✅ Verified in production (x64 and arm64) |
|
|
294
|
-
| macOS | ✅ Supported —
|
|
293
|
+
| macOS | ✅ Supported — reads Claude logins from the Keychain through the `security` CLI and schedules the optional watchdog with a launchd plist in `~/Library/LaunchAgents/` |
|
|
295
294
|
| Windows (WSL2) | ✅ Supported — OpenClaw's recommended gateway runtime on Windows; follow the Linux instructions inside WSL |
|
|
296
|
-
| Windows (native) | ⚠️
|
|
295
|
+
| Windows (native) | ⚠️ Not yet verified by us — verified launch is not implemented on Windows; it falls back to the legacy node + shim launch, so the v2.1.0 verified-launch fix for OpenClaw 2026.10.2 setup authority ("Ask OpenClaw") does not apply there. Reports welcome. |
|
|
297
296
|
|
|
298
297
|
## Install
|
|
299
298
|
|
|
@@ -375,6 +374,8 @@ cd multi-clawd && npm install && npm run build
|
|
|
375
374
|
openclaw plugins install "$(pwd)"
|
|
376
375
|
```
|
|
377
376
|
|
|
377
|
+
npm 10 and 11 both work here because the lockfile is committed.
|
|
378
|
+
|
|
378
379
|
```powershell
|
|
379
380
|
# Windows (native, PowerShell)
|
|
380
381
|
git clone https://github.com/Drakon-Systems-Ltd/multi-clawd.git
|
|
@@ -385,7 +386,7 @@ openclaw plugins install (Get-Location).Path
|
|
|
385
386
|
**Or let your agent install it.** Running an OpenClaw assistant or Claude
|
|
386
387
|
Code on the target machine already? Paste it this and go make coffee:
|
|
387
388
|
|
|
388
|
-
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v2.
|
|
389
|
+
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v2.2.0/SETUP-AGENT.md
|
|
389
390
|
> and follow it to set up multi-clawd on this machine. I own a second
|
|
390
391
|
> Claude account — ask me when you need me to log in.
|
|
391
392
|
|
|
@@ -553,7 +554,8 @@ error.
|
|
|
553
554
|
"pool": {
|
|
554
555
|
"id": "clawd",
|
|
555
556
|
"accounts": ["claw1", "claw2"], // preference order; first = home
|
|
556
|
-
"utilizationThreshold": 0.85, // hand
|
|
557
|
+
"utilizationThreshold": 0.85, // default hand-over; minute/hour and overage windows use this
|
|
558
|
+
"weeklyUtilizationThreshold": 0.97, // optional: day/week/month quota windows at 97%
|
|
557
559
|
"minDwellMs": 600000, // v0.3: anti-flap dwell before returning home
|
|
558
560
|
"degrade": { // v0.3.5: last step before provider drop
|
|
559
561
|
"ladder": ["claude-opus-4-8"], // whole pool exhausted → same account, lower tier
|
|
@@ -629,6 +631,7 @@ from the provider** on a timer — the same figures the Claude CLI's own
|
|
|
629
631
|
"id": "clawd",
|
|
630
632
|
"accounts": ["claw1", "claw2"],
|
|
631
633
|
"utilizationThreshold": 0.85, // hand over here (unchanged)
|
|
634
|
+
"weeklyUtilizationThreshold": 0.97, // optional: day/week/month quota windows at 97%; overage stays at 85%
|
|
632
635
|
"usagePoll": { // v1.10 — on by default
|
|
633
636
|
"intervalMs": 120000, // every 2 min (minimum 60000)
|
|
634
637
|
"warnThreshold": 0.95 // operator alert from here
|
|
@@ -745,9 +748,10 @@ What to know:
|
|
|
745
748
|
|
|
746
749
|
- **Why a native or config-dir login needs its own token.** That login is a
|
|
747
750
|
rotating one-time OAuth grant, and a copy of it breaks on the next refresh.
|
|
748
|
-
multi-clawd never
|
|
749
|
-
|
|
750
|
-
|
|
751
|
+
multi-clawd never copies that login into a direct profile. (The usage poll
|
|
752
|
+
and login checks do read Claude's credential files, but never copy or
|
|
753
|
+
rewrite them.) Run `claude setup-token` signed in as that account and store
|
|
754
|
+
the token. `setup` walks you through it.
|
|
751
755
|
- **Already stored a profile yourself?** Adopt it with
|
|
752
756
|
`"direct": { "profileId": "anthropic:<id>" }`. It gets ordered, never
|
|
753
757
|
rewritten.
|
|
@@ -828,6 +832,18 @@ event, and no plugin API can force a rebuild. See `DESIGN.md`.
|
|
|
828
832
|
a usable login now reports as available, a dead one still reports
|
|
829
833
|
"missing-auth", and the direct API transport keeps refusing CLI-login
|
|
830
834
|
accounts as before. Update with `multi-clawd update`.
|
|
835
|
+
If an account's stored login is deleted or becomes unreadable, the account
|
|
836
|
+
can stay available until the 30-second cache window that began at its last
|
|
837
|
+
assessment ends; the window runs from that assessment, not from the loss.
|
|
838
|
+
The first availability check after the window ends starts a background
|
|
839
|
+
re-check, and the account stays available only while that re-check is
|
|
840
|
+
pending; once it finishes it marks the account unavailable. Even if the
|
|
841
|
+
re-check stalls, a login backed by a valid access token is refused at its
|
|
842
|
+
access-token expiry minus 60 seconds, including at that exact moment and
|
|
843
|
+
even inside the first window. A login proven only by a recent successful
|
|
844
|
+
turn (its stored token can already be expired) is refused at two windows
|
|
845
|
+
after the assessment or 24 hours after that turn, whichever comes first. A
|
|
846
|
+
CLI login failure recorded on a real turn removes it at once.
|
|
831
847
|
|
|
832
848
|
- **Shim window persistence is sequential-safe, not concurrent-writer-safe.**
|
|
833
849
|
`persistState()` in `src/shim.ts` does a read-merge-write on every save so a
|
|
@@ -849,11 +865,11 @@ callsite (the ones static scanners flag) and why each exists.
|
|
|
849
865
|
|
|
850
866
|
| Surface | What multi-clawd does |
|
|
851
867
|
|---|---|
|
|
852
|
-
| Network |
|
|
853
|
-
| Credentials | Reads each account's
|
|
854
|
-
| Files read | `~/.openclaw/openclaw.json
|
|
855
|
-
| Files written | `~/.openclaw/state/multi-clawd
|
|
856
|
-
| Processes |
|
|
868
|
+
| Network | The gateway plugin polls `https://api.anthropic.com/api/oauth/usage` for eligible pooled accounts on an interval (default 120000 ms / 2 minutes); `pool.usagePoll.enabled: false` disables that poll. `multi-clawd usage` requests the same endpoint on demand. `doctor` runs `npm view` when its registry cache is stale; install and update commands contact the package registry. Claude Code subprocesses make their own provider requests. No plugin telemetry or analytics endpoint. |
|
|
869
|
+
| Credentials | Reads declared setup-token files or gateway-resolved `oauthTokenRef` values for Claude child launches; reads each eligible account's `.credentials.json` access token for the usage poll; reads Claude login credentials from files or the macOS Keychain via the `security` CLI for login checks and runtime auth; reads account identity for file-backed logins from `.claude.json` (`~/.claude.json` for a native login, or the account config dir). Setup tokens are passed to Claude through the child environment. |
|
|
870
|
+
| Files read | `~/.openclaw/openclaw.json`, declared account config dirs and token files, Claude `.credentials.json` and `.claude.json` files as described above, and local state files. |
|
|
871
|
+
| Files written | Under `~/.openclaw/state/multi-clawd/` by default: `<id>.json`, `<id>.usage.json`, `pool-<id>.sticky.json`, `direct-order.sticky.json`, `known-models.json`, `alerts-spool.jsonl`, `watchdog.json`, `registry-check.json`, `auth-profiles-<agent>.json`, `runtime/<hash>/` (copied runtime files), and `watchdog-launcher.mjs`. The watchdog state file can be redirected with `MULTI_CLAWD_WATCHDOG_STATE`. Setup writes `~/.openclaw/openclaw.json` and a dated backup. On session handover, transcript `.jsonl` files are copied between account config dirs. The optional watchdog installs `~/.config/systemd/user/multi-clawd-eviction-watchdog.service` and `.timer` on Linux, or `~/Library/LaunchAgents/com.multi-clawd.eviction-watchdog.plist` on macOS; the unit points at the state-dir launcher. |
|
|
872
|
+
| Processes | Launches Claude Code and the node + shim transport; CLI, setup and doctor run npm, OpenClaw, Claude and OS scheduler commands; the optional watchdog checks OpenClaw logs and may restart the gateway; Hermes integration runs its Python bridge. See [SECURITY.md](SECURITY.md) for the process-call table. |
|
|
857
873
|
| Consent | The wizard asks before every write, merges non-destructively, and never overwrites an existing account entry. `--dry-run` previews everything. |
|
|
858
874
|
|
|
859
875
|
Housekeeping:
|
package/SECURITY.md
CHANGED
|
@@ -15,9 +15,9 @@ credit you (unless you'd rather we didn't), and note it in the CHANGELOG.
|
|
|
15
15
|
|
|
16
16
|
## How credentials are handled
|
|
17
17
|
|
|
18
|
-
- **Tokens are never logged and never appear in a command line.**
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
- **Tokens are never logged and never appear in a command line.** Claude CLI
|
|
19
|
+
child launches receive setup tokens through their environment; direct-route
|
|
20
|
+
OpenClaw commands receive them on stdin.
|
|
21
21
|
- **Secret references are preferred over token files.** `oauthTokenRef` uses
|
|
22
22
|
the same `{source, provider, id}` shape as the rest of `openclaw.json` and
|
|
23
23
|
resolves through the gateway's own configured secret providers (1Password,
|
|
@@ -34,8 +34,9 @@ credit you (unless you'd rather we didn't), and note it in the CHANGELOG.
|
|
|
34
34
|
environment variables covering authentication, runtime, and telemetry are
|
|
35
35
|
cleared before each launch, so one account's credential cannot bleed into
|
|
36
36
|
another account's session.
|
|
37
|
-
- **
|
|
38
|
-
contain rate-limit
|
|
37
|
+
- **Per-account health and usage files are written `0600`,** using temporary
|
|
38
|
+
files and rename, and contain rate-limit data rather than credentials. Other
|
|
39
|
+
state files include model, alert, registry and watchdog data.
|
|
39
40
|
|
|
40
41
|
## Permissions
|
|
41
42
|
|
|
@@ -48,20 +49,25 @@ nothing and Claude keeps its default prompt-honouring behaviour.
|
|
|
48
49
|
## Why static scanners flag this package
|
|
49
50
|
|
|
50
51
|
Automated scans flag multi-clawd for `dangerous_exec`. That is expected and
|
|
51
|
-
correct: starting Claude Code processes *is* the product. Every
|
|
52
|
-
is listed here so you can check
|
|
52
|
+
correct: starting Claude Code processes *is* the product. Every source file
|
|
53
|
+
that calls a child-process API is listed here so you can check it yourself.
|
|
53
54
|
|
|
54
55
|
| Location | What it runs | Why |
|
|
55
56
|
| --- | --- | --- |
|
|
56
|
-
| `dist/shim.js` |
|
|
57
|
-
| `dist/
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `dist/openclaw-runner.js` | the `openclaw` CLI (or `directRoute.openclawCommand`) | Direct route only
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
| `dist/shim.js` | selected `claude` executable | Launches Claude Code for the selected account and, when an in-turn retry is armed, relaunches it for a sibling account. |
|
|
58
|
+
| `dist/shim-transport.js` | `node <shim>` | Runs the shim for an execute-transport turn. |
|
|
59
|
+
| `dist/credential-store.js` | `security find-generic-password` / `security dump-keychain` | Reads a macOS Keychain login or inventories Keychain item attributes for credential checks. |
|
|
60
|
+
| `dist/watchdog-schedule.js` | generated `watchdog-launcher.mjs` runs `node <bundled eviction-watchdog.mjs>` | Renders the state-dir launcher that a systemd user timer or launchd agent runs. The launcher resolves the current install before spawning its bundled watchdog script. |
|
|
61
|
+
| `dist/openclaw-runner.js` | the `openclaw` CLI (or `directRoute.openclawCommand`) | Direct route only: reads and writes the `anthropic` auth profiles and order through OpenClaw subcommands. A setup token travels on the child's stdin. |
|
|
62
|
+
| `scripts/cli.mjs` | `npm view` / `npm i -g`, `openclaw --version`, `openclaw` plugin install and gateway restart, `claude` login/status commands, `launchctl` / `systemctl`, and `node <bundled script>` | Checks and updates the CLI/plugin, handles account login, dispatches subcommands and repairs watchdog scheduling. |
|
|
63
|
+
| `scripts/setup.mjs` | `claude --version`, `launchctl` / `systemctl` | Checks the Claude CLI and loads or repairs optional watchdog scheduling. |
|
|
64
|
+
| `scripts/doctor.mjs` | `npm view`, `claude --version`, `openclaw models auth list` / `openclaw agent`, `launchctl` / `systemctl` | Checks registry version when its cache is stale, inspects auth and watchdog status, and optionally probes accounts. |
|
|
65
|
+
| `scripts/eviction-watchdog.mjs` | `openclaw logs --plain` / `openclaw gateway restart` | Detects the eviction signature and restarts the gateway when the watchdog's conditions are met. |
|
|
66
|
+
| `scripts/hermes.mjs` | Python interpreter with `-c` / bundled bridge | Checks Python availability and runs the Hermes bridge. |
|
|
67
|
+
|
|
68
|
+
None of these pass a command string to a shell: child-process calls use an
|
|
69
|
+
executable and argument array. The launcher generated by `watchdog-schedule`
|
|
70
|
+
uses the same form.
|
|
65
71
|
|
|
66
72
|
## Things you should decide for yourself
|
|
67
73
|
|
package/dist/explain-core.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_UTILIZATION_THRESHOLD } from "./health.js";
|
|
1
2
|
const WINDOW_LABELS = {
|
|
2
3
|
five_hour: "5-hour",
|
|
3
4
|
seven_day: "weekly",
|
|
@@ -84,11 +85,14 @@ export function renderExplanation(model) {
|
|
|
84
85
|
}
|
|
85
86
|
lines.push("");
|
|
86
87
|
if (model.pool) {
|
|
87
|
-
const
|
|
88
|
+
const generalThreshold = model.pool.utilizationThreshold ?? DEFAULT_UTILIZATION_THRESHOLD;
|
|
89
|
+
const weeklyThreshold = model.pool.weeklyUtilizationThreshold ?? generalThreshold;
|
|
88
90
|
lines.push(`POOL ${model.pool.id} (${model.pool.accounts.join(" → ")})`);
|
|
89
|
-
lines.push(` Every Claude launch runs on the first account
|
|
90
|
-
lines.push(`
|
|
91
|
+
lines.push(` Every Claude launch runs on the first account below its threshold;`);
|
|
92
|
+
lines.push(` the home account reclaims work`);
|
|
91
93
|
lines.push(` automatically once its window resets.`);
|
|
94
|
+
lines.push(` Thresholds: general ${Math.round(generalThreshold * 100)}%, weekly ${Math.round(weeklyThreshold * 100)}%${model.pool.weeklyUtilizationThreshold === undefined ? " (follows general)" : ""}.`);
|
|
95
|
+
lines.push(` The weekly threshold applies to day/week/month quota windows; overage uses the general threshold.`);
|
|
92
96
|
const ladder = model.pool.degrade?.ladder ?? [];
|
|
93
97
|
if (ladder.length > 0) {
|
|
94
98
|
lines.push(` If the WHOLE pool is exhausted: step down to ${ladder.join(" → ")} first.`);
|
package/dist/health.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { modelWindowApplies, modelWindowKey } from "./shim-core.js";
|
|
2
|
-
const DEFAULT_UTILIZATION_THRESHOLD = 0.85;
|
|
2
|
+
export const DEFAULT_UTILIZATION_THRESHOLD = 0.85;
|
|
3
3
|
const DEFAULT_STALE_AFTER_MS = 6 * 60 * 60 * 1000;
|
|
4
4
|
export const MODEL_REJECTED_TTL_MS = 60 * 60 * 1000;
|
|
5
5
|
export const CREDENTIAL_FAILED_TTL_MS = 15 * 60 * 1000;
|
|
@@ -19,6 +19,9 @@ export function isPeriodWindow(window) {
|
|
|
19
19
|
export function isOverageWindow(window) {
|
|
20
20
|
return /overage/i.test(window);
|
|
21
21
|
}
|
|
22
|
+
export function isLongQuotaWindow(window) {
|
|
23
|
+
return /(^|_)(days?|weeks?|months?)(_|$)/.test(window) && !isOverageWindow(window);
|
|
24
|
+
}
|
|
22
25
|
export function isWarningStatus(status) {
|
|
23
26
|
return /warning/i.test(status);
|
|
24
27
|
}
|
|
@@ -75,7 +78,7 @@ export function lastAuthSuccessAt(state) {
|
|
|
75
78
|
}
|
|
76
79
|
export function classifyAccountHealth(state, options, nowMs, requestedModel) {
|
|
77
80
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
78
|
-
const
|
|
81
|
+
const generalThreshold = options.utilizationThreshold ?? DEFAULT_UTILIZATION_THRESHOLD;
|
|
79
82
|
if (!state)
|
|
80
83
|
return { verdict: "no_data" };
|
|
81
84
|
const credentialFailure = credentialFailureFor(state, nowMs);
|
|
@@ -153,6 +156,9 @@ export function classifyAccountHealth(state, options, nowMs, requestedModel) {
|
|
|
153
156
|
reason: `${window} rejected ${Math.round((nowMs - w.seenAt) / 60000)}m ago (no reset time; ${Math.round(resetLessBlockMs(window) / 3600000)}h block)`,
|
|
154
157
|
};
|
|
155
158
|
}
|
|
159
|
+
const threshold = isLongQuotaWindow(window)
|
|
160
|
+
? options.weeklyUtilizationThreshold ?? generalThreshold
|
|
161
|
+
: generalThreshold;
|
|
156
162
|
if (typeof w.utilization === "number" &&
|
|
157
163
|
w.utilization >= threshold &&
|
|
158
164
|
(resetBearing || resetMs === undefined)) {
|
package/dist/index.js
CHANGED
|
@@ -805,6 +805,7 @@ export function registerPoolBackend(api, pool, accounts, registeredIds, execMode
|
|
|
805
805
|
}
|
|
806
806
|
const options = {
|
|
807
807
|
utilizationThreshold: pool.utilizationThreshold,
|
|
808
|
+
weeklyUtilizationThreshold: pool.weeklyUtilizationThreshold,
|
|
808
809
|
staleAfterMs: pool.staleAfterMs,
|
|
809
810
|
rotateOnOverage: pool.rotateOnOverage,
|
|
810
811
|
};
|
|
@@ -1040,6 +1041,7 @@ export function startDirectOrderSync(params) {
|
|
|
1040
1041
|
const intervalMs = Math.max(15_000, params.directRoute?.intervalMs ?? 60_000);
|
|
1041
1042
|
const healthOptions = {
|
|
1042
1043
|
utilizationThreshold: params.pool?.utilizationThreshold,
|
|
1044
|
+
weeklyUtilizationThreshold: params.pool?.weeklyUtilizationThreshold,
|
|
1043
1045
|
staleAfterMs: params.pool?.staleAfterMs,
|
|
1044
1046
|
rotateOnOverage: params.pool?.rotateOnOverage,
|
|
1045
1047
|
};
|
|
@@ -1137,6 +1139,7 @@ export function startUsagePoll(params) {
|
|
|
1137
1139
|
const warnThreshold = effectiveUsageWarnThreshold(pool.usagePoll);
|
|
1138
1140
|
const healthOptions = {
|
|
1139
1141
|
utilizationThreshold: pool.utilizationThreshold,
|
|
1142
|
+
weeklyUtilizationThreshold: pool.weeklyUtilizationThreshold,
|
|
1140
1143
|
staleAfterMs: pool.staleAfterMs,
|
|
1141
1144
|
rotateOnOverage: pool.rotateOnOverage,
|
|
1142
1145
|
};
|
|
@@ -1205,7 +1208,7 @@ export function startUsagePoll(params) {
|
|
|
1205
1208
|
usageInitial.unref?.();
|
|
1206
1209
|
usageTimer = setInterval(tick, intervalMs);
|
|
1207
1210
|
usageTimer.unref?.();
|
|
1208
|
-
logger.info(`[multi-clawd] usage poll: reading live usage for ${members.map((m) => m.id).join(", ")} every ${Math.round(intervalMs / 1000)}s — rotate at ${Math.round((pool.utilizationThreshold ?? 0.85) * 100)}
|
|
1211
|
+
logger.info(`[multi-clawd] usage poll: reading live usage for ${members.map((m) => m.id).join(", ")} every ${Math.round(intervalMs / 1000)}s — rotate at ${Math.round((pool.utilizationThreshold ?? 0.85) * 100)}%${pool.weeklyUtilizationThreshold !== undefined ? ` (weekly ${Math.round(pool.weeklyUtilizationThreshold * 100)}%)` : ""}, warn at ${Math.round(warnThreshold * 100)}%${skipped.length > 0 ? `; not polled: ${skipped.join("; ")}` : ""}`);
|
|
1209
1212
|
return { active: true, members: members.map((m) => m.id) };
|
|
1210
1213
|
}
|
|
1211
1214
|
export async function runUsagePollTickNow() {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "multi-clawd",
|
|
3
3
|
"name": "multi-clawd",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
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",
|
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
"type": "number",
|
|
48
48
|
"description": "Hand over when any rate-limit window's utilization reaches this fraction. Default 0.85."
|
|
49
49
|
},
|
|
50
|
+
"weeklyUtilizationThreshold": {
|
|
51
|
+
"type": "number",
|
|
52
|
+
"minimum": 0,
|
|
53
|
+
"maximum": 1,
|
|
54
|
+
"description": "Hand over when a day/week/month-scale quota window (such as seven_day) reaches this fraction. Defaults to utilizationThreshold. Minute/hour and overage windows keep using utilizationThreshold; overage rotation still requires rotateOnOverage."
|
|
55
|
+
},
|
|
50
56
|
"staleAfterMs": {
|
|
51
57
|
"type": "number",
|
|
52
58
|
"description": "Ignore health data older than this. Default 21600000 (6h)."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakon-systems/multi-clawd",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
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",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"setup": "node scripts/setup.mjs",
|
|
62
62
|
"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')\"",
|
|
63
63
|
"sync-readme-tag": "node scripts/sync-readme-tag.mjs",
|
|
64
|
-
"version": "npm run sync-manifest && npm run sync-readme-tag && git add openclaw.plugin.json README.md"
|
|
64
|
+
"version": "npm run sync-manifest && npm run sync-readme-tag && git add openclaw.plugin.json README.md package-lock.json"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"openclaw": ">=2026.8.1"
|
package/scripts/cli.mjs
CHANGED
|
@@ -200,6 +200,7 @@ async function loadDirect(config) {
|
|
|
200
200
|
const runner = run.createOpenclawRunner(directRoute.openclawCommand ?? "openclaw");
|
|
201
201
|
const healthOptions = {
|
|
202
202
|
utilizationThreshold: pool?.utilizationThreshold,
|
|
203
|
+
weeklyUtilizationThreshold: pool?.weeklyUtilizationThreshold,
|
|
203
204
|
staleAfterMs: pool?.staleAfterMs,
|
|
204
205
|
rotateOnOverage: pool?.rotateOnOverage,
|
|
205
206
|
};
|
|
@@ -676,6 +677,7 @@ async function explain() {
|
|
|
676
677
|
}
|
|
677
678
|
const h = health.classifyAccountHealth(state, {
|
|
678
679
|
utilizationThreshold: pool?.utilizationThreshold,
|
|
680
|
+
weeklyUtilizationThreshold: pool?.weeklyUtilizationThreshold,
|
|
679
681
|
staleAfterMs: pool?.staleAfterMs,
|
|
680
682
|
}, now);
|
|
681
683
|
let detail = h.reason;
|
|
@@ -684,6 +686,7 @@ async function explain() {
|
|
|
684
686
|
}
|
|
685
687
|
const usage = health.summarizeWindowUsage(state, {
|
|
686
688
|
utilizationThreshold: pool?.utilizationThreshold,
|
|
689
|
+
weeklyUtilizationThreshold: pool?.weeklyUtilizationThreshold,
|
|
687
690
|
staleAfterMs: pool?.staleAfterMs,
|
|
688
691
|
}, now);
|
|
689
692
|
return { id: a.id, verdict: h.verdict, detail, usage };
|
|
@@ -742,11 +745,13 @@ async function usageCommand(args) {
|
|
|
742
745
|
const pool = pc.pool ? { ...pc.pool, id: pc.pool.id?.trim() || "clawd", accounts: pc.pool.accounts ?? [] } : undefined;
|
|
743
746
|
const healthOptions = {
|
|
744
747
|
utilizationThreshold: pool?.utilizationThreshold,
|
|
748
|
+
weeklyUtilizationThreshold: pool?.weeklyUtilizationThreshold,
|
|
745
749
|
staleAfterMs: pool?.staleAfterMs,
|
|
746
750
|
rotateOnOverage: pool?.rotateOnOverage,
|
|
747
751
|
};
|
|
748
752
|
const warnThreshold = up.effectiveUsageWarnThreshold(pool?.usagePoll);
|
|
749
753
|
const rotateAt = pool?.utilizationThreshold ?? 0.85;
|
|
754
|
+
const weeklyRotateAt = pool?.weeklyUtilizationThreshold;
|
|
750
755
|
const stateDir = join(homedir(), ".openclaw", "state", "multi-clawd");
|
|
751
756
|
const now = Date.now();
|
|
752
757
|
const rows = [];
|
|
@@ -797,13 +802,17 @@ async function usageCommand(args) {
|
|
|
797
802
|
const mins = Math.max(0, Math.round((resetsAt * 1000 - now) / 60000));
|
|
798
803
|
return mins < 90 ? ` (resets in ~${mins}m)` : mins < 36 * 60 ? ` (resets in ~${Math.round(mins / 60)}h)` : ` (resets in ~${Math.round(mins / 1440)}d)`;
|
|
799
804
|
};
|
|
800
|
-
const mark = (u) =>
|
|
801
|
-
|
|
805
|
+
const mark = (u, window) => {
|
|
806
|
+
const weekly = window && health.isLongQuotaWindow(window);
|
|
807
|
+
const threshold = weekly ? weeklyRotateAt ?? rotateAt : rotateAt;
|
|
808
|
+
return u >= 1 ? "⛔" : u >= warnThreshold ? "🔴" : u >= threshold ? "🟠" : "🟢";
|
|
809
|
+
};
|
|
810
|
+
console.log(`\n${BOLD}🦞 multi-clawd — live usage${RESET} ${DIM}(rotate at ${pct(rotateAt)}${weeklyRotateAt !== undefined ? `, weekly ${pct(weeklyRotateAt)}` : ""}, warn at ${pct(warnThreshold)})${RESET}\n`);
|
|
802
811
|
for (const r of rows) {
|
|
803
812
|
console.log(` ${BOLD}${r.id}${RESET}${r.label ? ` ${DIM}— ${r.label}${RESET}` : ""} pool verdict: ${r.verdict}${r.reason ? ` ${DIM}(${r.reason})${RESET}` : ""}`);
|
|
804
813
|
if (r.snapshot) {
|
|
805
814
|
for (const [w, u] of Object.entries(r.snapshot.windows)) {
|
|
806
|
-
console.log(` ${mark(u.utilization)} ${up.usageWindowLabel(w).padEnd(12)} ${pct(u.utilization).padStart(4)}${until(u.resetsAt)}`);
|
|
815
|
+
console.log(` ${mark(u.utilization, w)} ${up.usageWindowLabel(w).padEnd(12)} ${pct(u.utilization).padStart(4)}${until(u.resetsAt)}`);
|
|
807
816
|
}
|
|
808
817
|
for (const s of r.snapshot.scoped) {
|
|
809
818
|
console.log(` ${mark(s.utilization)} ${`${s.label} model`.padEnd(12)} ${pct(s.utilization).padStart(4)}${until(s.resetsAt)}`);
|
package/scripts/doctor.mjs
CHANGED
|
@@ -301,6 +301,7 @@ const { summarizeWindowUsage, classifyAccountHealth, overdueProbeAccount, lastAu
|
|
|
301
301
|
"overdueProbeAccount",
|
|
302
302
|
"lastAuthSuccessAt",
|
|
303
303
|
]);
|
|
304
|
+
const { DEFAULT_UTILIZATION_THRESHOLD } = await import(join(REPO_DIR, "dist", "health.js"));
|
|
304
305
|
// "cli", like the chain audits: resolving WHICH login backs an account is
|
|
305
306
|
// doctor's own diagnostic, not a description of the running plugin's
|
|
306
307
|
// behaviour. Asking the installed copy meant a fix to the resolver could not
|
|
@@ -495,6 +496,9 @@ else {
|
|
|
495
496
|
const members = (pool.accounts ?? []).filter((id) => accounts.some((a) => a.id === id));
|
|
496
497
|
if (members.length < 2) bad(`pool "${pool.id ?? "clawd"}" has ${members.length} valid member(s); needs ≥ 2`);
|
|
497
498
|
else ok(`pool "${pool.id ?? "clawd"}": ${members.join(" → ")}`);
|
|
499
|
+
const generalThreshold = pool.utilizationThreshold ?? DEFAULT_UTILIZATION_THRESHOLD;
|
|
500
|
+
const weeklyThreshold = pool.weeklyUtilizationThreshold ?? generalThreshold;
|
|
501
|
+
note(`thresholds: general ${Math.round(generalThreshold * 100)}%, weekly ${Math.round(weeklyThreshold * 100)}%${pool.weeklyUtilizationThreshold === undefined ? " (follows general)" : ""}`);
|
|
498
502
|
// Setup-token members: what they do and do not get, said once, plainly.
|
|
499
503
|
const tokenMembers = members
|
|
500
504
|
.map((id) => accounts.find((a) => a.id === id))
|
|
@@ -529,6 +533,7 @@ else {
|
|
|
529
533
|
const healthOptions = {
|
|
530
534
|
staleAfterMs: pool.staleAfterMs,
|
|
531
535
|
utilizationThreshold: pool.utilizationThreshold,
|
|
536
|
+
weeklyUtilizationThreshold: pool.weeklyUtilizationThreshold,
|
|
532
537
|
rotateOnOverage: pool.rotateOnOverage,
|
|
533
538
|
};
|
|
534
539
|
const verdicts = members.map((id) => ({
|
|
@@ -876,6 +881,7 @@ if (accounts.some((a) => a && a.direct !== undefined && a.direct !== false)) {
|
|
|
876
881
|
readHealth,
|
|
877
882
|
healthOptions: {
|
|
878
883
|
utilizationThreshold: pool?.utilizationThreshold,
|
|
884
|
+
weeklyUtilizationThreshold: pool?.weeklyUtilizationThreshold,
|
|
879
885
|
staleAfterMs: pool?.staleAfterMs,
|
|
880
886
|
rotateOnOverage: pool?.rotateOnOverage,
|
|
881
887
|
},
|