@drakon-systems/multi-clawd 2.5.1 → 2.6.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 CHANGED
@@ -31,6 +31,7 @@ multi-clawd setup # guided wizard: accounts, isolated second login, pool, wa
31
31
  multi-clawd login claw2 # launch the right Claude sign-in for an account (or re-auth it)
32
32
  multi-clawd explain # your whole setup in plain English — accounts, chain, live health
33
33
  multi-clawd explain --history --since 7d # per-account launches, retries, rotations
34
+ multi-clawd explain --json # live health plus a rough weekly pace estimate, machine-readable
34
35
  multi-clawd account disable claw2 --dry-run # preview benching an account
35
36
  multi-clawd chain # audit your model routing — what actually serves each turn
36
37
  multi-clawd direct # the direct anthropic/* route: status, or `direct sync` (v1.9, optional)
@@ -415,7 +416,7 @@ openclaw plugins install (Get-Location).Path
415
416
  **Or let your agent install it.** Running an OpenClaw assistant or Claude
416
417
  Code on the target machine already? Paste it this and go make coffee:
417
418
 
418
- > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v2.5.1/SETUP-AGENT.md
419
+ > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v2.6.0/SETUP-AGENT.md
419
420
  > and follow it to set up multi-clawd on this machine. I own a second
420
421
  > Claude account — ask me when you need me to log in.
421
422
 
@@ -30,6 +30,49 @@ export function renderUsageLine(usage, nowMs) {
30
30
  })
31
31
  .join(" · ");
32
32
  }
33
+ const WEEK_MS = 7 * 86_400_000;
34
+ const MIN_PACE_ELAPSED_MS = WEEK_MS / 10;
35
+ const PACE_RESET_SLACK_MS = 3_600_000;
36
+ export function computeWeeklyPace(usage, nowMs) {
37
+ const u = ["usage:seven_day", "seven_day"]
38
+ .map((w) => usage?.find((x) => x.window === w))
39
+ .find((x) => x !== undefined);
40
+ if (!u || u.resetsAt === undefined)
41
+ return undefined;
42
+ if (!Number.isFinite(u.utilization) || u.utilization < 0 || u.utilization > 1)
43
+ return undefined;
44
+ if (!Number.isFinite(u.resetsAt) || !Number.isFinite(nowMs))
45
+ return undefined;
46
+ const remaining = u.resetsAt - nowMs;
47
+ if (remaining <= 0 || remaining > WEEK_MS + PACE_RESET_SLACK_MS)
48
+ return undefined;
49
+ const elapsed = Math.max(0, WEEK_MS - remaining);
50
+ if (elapsed < MIN_PACE_ELAPSED_MS)
51
+ return undefined;
52
+ const expectedPct = elapsed / WEEK_MS;
53
+ const projectedExhaustionAt = u.utilization >= 1
54
+ ? nowMs
55
+ : u.utilization > 0 ? nowMs + ((1 - u.utilization) * elapsed) / u.utilization : undefined;
56
+ return {
57
+ window: u.window,
58
+ expectedPct,
59
+ aheadOfPace: u.utilization - expectedPct,
60
+ projectedExhaustionAt,
61
+ willLastToReset: projectedExhaustionAt === undefined || projectedExhaustionAt >= u.resetsAt,
62
+ resetsAt: u.resetsAt,
63
+ estimate: true,
64
+ };
65
+ }
66
+ export function renderPaceLine(pace, nowMs) {
67
+ if (!pace)
68
+ return undefined;
69
+ const pts = Math.round(pace.aheadOfPace * 100);
70
+ const where = pts === 0 ? "on pace" : pts > 0 ? `${pts} points ahead of pace` : `${-pts} points behind pace`;
71
+ const verdict = pace.willLastToReset
72
+ ? "should last to the reset"
73
+ : `could run out in ${relativeUntil(pace.projectedExhaustionAt ?? nowMs, nowMs)}, before the reset in ${relativeUntil(pace.resetsAt, nowMs)}`;
74
+ return `weekly pace (rough estimate): ~${Math.round(pace.expectedPct * 100)}% of the week gone, ${where}; ${verdict}`;
75
+ }
33
76
  export function describeAccount(acc) {
34
77
  if (acc.native) {
35
78
  return "the machine's main `claude` login (default config dir; OS keychain on macOS)";
@@ -145,6 +188,9 @@ export function renderExplanation(model) {
145
188
  const word = VERDICT_WORDS[h.verdict] ?? h.verdict;
146
189
  lines.push(` ${h.id}: ${model.accounts.find((a) => a.id === h.id)?.disabled ? "disabled by operator" : word}${h.detail ? ` — ${h.detail}` : ""}`);
147
190
  lines.push(` usage: ${h.usage?.length ? renderUsageLine(h.usage, nowMs) : "no live telemetry"}`);
191
+ const paceLine = renderPaceLine(computeWeeklyPace(h.usage, nowMs), nowMs);
192
+ if (paceLine)
193
+ lines.push(` ${paceLine}`);
148
194
  }
149
195
  if (model.pool) {
150
196
  const strategy = poolStrategy(model.pool.strategy);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "multi-clawd",
3
3
  "name": "multi-clawd",
4
- "version": "2.5.1",
4
+ "version": "2.6.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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/multi-clawd",
3
- "version": "2.5.1",
3
+ "version": "2.6.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",
package/scripts/cli.mjs CHANGED
@@ -50,6 +50,27 @@ Run via npx (${DIM}npx ${PKG} <command>${RESET}) or install globally
50
50
  `);
51
51
  }
52
52
 
53
+ // Per-command usage, printed for `<command> --help` / `-h` before anything is
54
+ // dispatched, so asking for help never reads config, runs checks, or starts the
55
+ // setup wizard. `hermes` is absent on purpose: it handles its own help.
56
+ const COMMAND_HELP = {
57
+ setup: "multi-clawd setup [--dry-run]\n\n Guided setup wizard (accounts, pool, watchdog).\n --dry-run preview the changes without writing anything",
58
+ login: "multi-clawd login [<id>]\n\n Log a configured account in (or re-auth it). Without <id>, lists the\n configured accounts to choose from.",
59
+ account: "multi-clawd account <enable|disable> <id> [--dry-run]\n\n Disable or enable a pool account.\n --dry-run preview the change without writing it",
60
+ explain: "multi-clawd explain [--json | --history [--since 7d|24h]]\n\n Your setup in plain English: accounts, pool, fallback chain.\n --json machine-readable output\n --history recent pool events; --since limits the window",
61
+ chain: "multi-clawd chain [--raw]\n\n Audit your model routing: what actually serves each turn.\n --raw include raw routing detail",
62
+ direct: "multi-clawd direct [status | sync [--dry-run] [--yes] [--resync]]\n\n The direct anthropic/* route. Default is status.\n sync store profiles for the direct route\n --dry-run preview without writing\n --yes skip the confirmation prompt\n --resync refresh profiles that are already stored",
63
+ usage: "multi-clawd usage [--json]\n\n Live usage per account from the provider: what is left, and what the\n pool will do.\n --json machine-readable output",
64
+ update: "multi-clawd update\n\n Update the plugin to the latest version.",
65
+ doctor: "multi-clawd doctor [--probe] [--probe-pool] [--verbose] [--raw]\n\n Health check.\n --probe run a live turn per account plus the pool\n --probe-pool with --probe: probe the pool ref only (one turn)\n --verbose show more detail\n --raw show raw output",
66
+ version: "multi-clawd version\n\n Show CLI and installed plugin versions.",
67
+ };
68
+
69
+ if (Object.hasOwn(COMMAND_HELP, cmd) && rest.some((a) => a === "--help" || a === "-h")) {
70
+ console.log(`\n${COMMAND_HELP[cmd]}\n`);
71
+ process.exit(0);
72
+ }
73
+
53
74
  const { resolveInstallDir } = await import(join(__dirname, "_shared.mjs"));
54
75
 
55
76
  function installedVersion(installPath) {
@@ -677,11 +698,12 @@ async function healWatchdogUnit() {
677
698
  /** `explain` — gather config + live health, render the plain-English view. */
678
699
  async function explain(args = []) {
679
700
  let historySince;
680
- if (args.length) {
701
+ const asJson = args.length === 1 && args[0] === "--json";
702
+ if (args.length && !asJson) {
681
703
  const allowed = args[0] === "--history" && (args.length === 1 ||
682
704
  (args.length === 3 && args[1] === "--since"));
683
705
  if (!allowed) {
684
- console.error("explain: usage: multi-clawd explain [--history [--since 7d|24h]]");
706
+ console.error("explain: usage: multi-clawd explain [--json | --history [--since 7d|24h]]");
685
707
  process.exitCode = 1;
686
708
  return;
687
709
  }
@@ -775,6 +797,14 @@ async function explain(args = []) {
775
797
  /* no journal yet: every account counts as never served */
776
798
  }
777
799
  }
800
+ if (asJson) {
801
+ // #94: machine-readable health with the display-only weekly pace estimate.
802
+ console.log(JSON.stringify({
803
+ at: new Date(now).toISOString(),
804
+ accounts: healthRows.map((r) => ({ ...r, weeklyPace: ec.computeWeeklyPace(r.usage, now) ?? null })),
805
+ }, null, 2));
806
+ return;
807
+ }
778
808
  // v1.9: the direct anthropic/* route, when any account opted in.
779
809
  let direct;
780
810
  const dctx = await loadDirect(config);