@drakon-systems/multi-clawd 1.3.0 → 1.4.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 +45 -9
- package/dist/login-plan.js +43 -0
- package/package.json +1 -1
- package/scripts/cli.mjs +93 -0
- package/scripts/setup.mjs +7 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ same model, next account, full harness on every hop.
|
|
|
11
11
|
|
|
12
12
|
[](https://docs.openclaw.ai/plugins)
|
|
13
13
|
[](https://www.npmjs.com/package/@drakon-systems/multi-clawd)
|
|
14
|
-
[](CHANGELOG.md)
|
|
15
15
|
[](LICENSE)
|
|
16
16
|
[](tsconfig.json)
|
|
17
17
|
|
|
@@ -21,6 +21,38 @@ same model, next account, full harness on every hop.
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm i -g @drakon-systems/multi-clawd # the CLI (once)
|
|
28
|
+
|
|
29
|
+
multi-clawd update # install (or update) the OpenClaw plugin — right flags, restart, doctor
|
|
30
|
+
multi-clawd setup # guided wizard: accounts, isolated second login, pool, watchdog
|
|
31
|
+
multi-clawd login claw2 # launch the right Claude sign-in for an account (or re-auth it)
|
|
32
|
+
multi-clawd explain # your whole setup in plain English — accounts, chain, live health
|
|
33
|
+
multi-clawd doctor # health check (add --probe for a live end-to-end turn)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That's the entire lifecycle. `update` installs the plugin when it's missing and
|
|
37
|
+
upgrades it when it's not — nobody types registry flags. `setup` walks you
|
|
38
|
+
through the whole shape below and merges config **non-destructively** (backup
|
|
39
|
+
first, existing accounts never overwritten, re-runs are no-ops). Prefer not to
|
|
40
|
+
install anything? Every command also runs as
|
|
41
|
+
`npx @drakon-systems/multi-clawd <command>`.
|
|
42
|
+
|
|
43
|
+
**The 3-step picture** `setup` walks you through:
|
|
44
|
+
|
|
45
|
+
1. **Main account** — your existing `claude` login, used as-is (nothing to do).
|
|
46
|
+
2. **Second account** — lives in its **own isolated config dir** (a separate
|
|
47
|
+
Claude "app", e.g. `~/.claw2`), so the two logins can never clobber each
|
|
48
|
+
other; token via a secret-manager reference (recommended), token file, or
|
|
49
|
+
the dir's own login.
|
|
50
|
+
3. **The pool** — one backend id (`clawd/…`) fronting both: every launch runs
|
|
51
|
+
on the first account that isn't nearly maxed out, and your fallback chain
|
|
52
|
+
routes through it.
|
|
53
|
+
|
|
54
|
+
Then `multi-clawd explain` shows you exactly what you built.
|
|
55
|
+
|
|
24
56
|
## Why
|
|
25
57
|
|
|
26
58
|
OpenClaw's bundled `claude-cli` backend runs Claude Code on a **single**
|
|
@@ -113,20 +145,24 @@ Code backend runs.
|
|
|
113
145
|
|
|
114
146
|
## Install
|
|
115
147
|
|
|
116
|
-
**
|
|
148
|
+
**The CLI does it all (recommended)** — see [Quick start](#quick-start):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm i -g @drakon-systems/multi-clawd && multi-clawd update
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`update` runs the registry install with the right flags, offers the gateway
|
|
155
|
+
restart, and finishes with a doctor health check. Prefer the raw form?
|
|
117
156
|
|
|
118
157
|
```bash
|
|
119
158
|
openclaw plugins install @drakon-systems/multi-clawd --pin
|
|
120
159
|
openclaw gateway restart
|
|
121
160
|
```
|
|
122
161
|
|
|
123
|
-
|
|
124
|
-
keep in sync. `--pin` records the exact resolved version, so an
|
|
125
|
-
deliberate
|
|
126
|
-
dependency (the host provides it), so the install stays lean.
|
|
127
|
-
`openclaw plugins list` (expect `multi-clawd` enabled) — it also shows the
|
|
128
|
-
install path; run the doctor from there:
|
|
129
|
-
`node <install-path>/scripts/doctor.mjs`.
|
|
162
|
+
Either way the gateway pulls the prebuilt package — no clone, no build step,
|
|
163
|
+
nothing to keep in sync. `--pin` records the exact resolved version, so an
|
|
164
|
+
upgrade is a deliberate act, never a surprise. `openclaw` itself is a *peer*
|
|
165
|
+
dependency (the host provides it), so the install stays lean.
|
|
130
166
|
|
|
131
167
|
**From ClawHub (alternative registry):**
|
|
132
168
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function loginPlanForAccount(acc) {
|
|
2
|
+
if (acc.native) {
|
|
3
|
+
return {
|
|
4
|
+
command: ["claude", "auth", "login"],
|
|
5
|
+
clearConfigDir: true,
|
|
6
|
+
verify: "auth-status",
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
if (acc.oauthTokenRef) {
|
|
10
|
+
return {
|
|
11
|
+
command: ["claude", "setup-token"],
|
|
12
|
+
configDir: acc.configDir,
|
|
13
|
+
ensureDir: acc.configDir !== undefined,
|
|
14
|
+
clearConfigDir: false,
|
|
15
|
+
scratchDir: acc.configDir === undefined,
|
|
16
|
+
verify: "manual",
|
|
17
|
+
afterNote: `store the printed setup-token in your ${acc.oauthTokenRef.provider ?? "secret"} ` +
|
|
18
|
+
`provider at the item your secret reference points to — the gateway resolves it from there; ` +
|
|
19
|
+
`this tool never touches the token itself`,
|
|
20
|
+
warn: acc.configDir === undefined
|
|
21
|
+
? "this account has no config dir of its own — running in a temporary scratch dir so your default login is untouched"
|
|
22
|
+
: undefined,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (acc.oauthTokenFile) {
|
|
26
|
+
return {
|
|
27
|
+
command: ["claude", "setup-token"],
|
|
28
|
+
configDir: acc.configDir,
|
|
29
|
+
ensureDir: acc.configDir !== undefined,
|
|
30
|
+
clearConfigDir: false,
|
|
31
|
+
scratchDir: acc.configDir === undefined,
|
|
32
|
+
verify: "token-file",
|
|
33
|
+
afterNote: `save the printed setup-token to ${acc.oauthTokenFile} and chmod 600 it`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
command: ["claude", "auth", "login"],
|
|
38
|
+
configDir: acc.configDir,
|
|
39
|
+
ensureDir: true,
|
|
40
|
+
clearConfigDir: false,
|
|
41
|
+
verify: "auth-status",
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakon-systems/multi-clawd",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/scripts/cli.mjs
CHANGED
|
@@ -33,6 +33,7 @@ function usage() {
|
|
|
33
33
|
${BOLD}🦞 multi-clawd${RESET} — multi-account Claude failover for OpenClaw
|
|
34
34
|
|
|
35
35
|
${BOLD}setup${RESET} guided setup wizard (accounts, pool, watchdog)
|
|
36
|
+
${BOLD}login${RESET} log a configured account in (or re-auth it) — right dir, right env
|
|
36
37
|
${BOLD}explain${RESET} your setup in plain English — accounts, pool, fallback chain
|
|
37
38
|
${BOLD}update${RESET} update the plugin to the latest version
|
|
38
39
|
${BOLD}doctor${RESET} health check (add --probe for a live turn)
|
|
@@ -270,10 +271,102 @@ async function explain() {
|
|
|
270
271
|
console.log(`\n${DIM}(health checks: multi-clawd doctor · change things: multi-clawd setup)${RESET}`);
|
|
271
272
|
}
|
|
272
273
|
|
|
274
|
+
/**
|
|
275
|
+
* `login <account>` — launch the RIGHT Claude login flow for a configured
|
|
276
|
+
* account: correct config-dir environment, dir created if missing, verified
|
|
277
|
+
* afterwards (shows which email is signed in). The human does the OAuth; this
|
|
278
|
+
* never captures, stores, or prints a token value.
|
|
279
|
+
*/
|
|
280
|
+
async function login() {
|
|
281
|
+
const { readFileSync: rf, existsSync, mkdirSync, chmodSync, statSync, mkdtempSync, rmSync } =
|
|
282
|
+
await import("node:fs");
|
|
283
|
+
const { homedir, tmpdir } = await import("node:os");
|
|
284
|
+
let lp, ec;
|
|
285
|
+
try {
|
|
286
|
+
lp = await import(resolve(__dirname, "..", "dist", "login-plan.js"));
|
|
287
|
+
ec = await import(resolve(__dirname, "..", "dist", "explain-core.js"));
|
|
288
|
+
} catch {
|
|
289
|
+
console.error("login: built dist/ is missing — reinstall the package.");
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
let config = {};
|
|
293
|
+
try {
|
|
294
|
+
config = JSON.parse(rf(join(homedir(), ".openclaw", "openclaw.json"), "utf8"));
|
|
295
|
+
} catch {
|
|
296
|
+
console.error("login: could not read ~/.openclaw/openclaw.json — run `multi-clawd setup` first.");
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
const accounts = config?.plugins?.entries?.["multi-clawd"]?.config?.accounts ?? [];
|
|
300
|
+
if (accounts.length === 0) {
|
|
301
|
+
console.error("login: no multi-clawd accounts configured — run `multi-clawd setup` first.");
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
const target = rest[0];
|
|
305
|
+
const acc = accounts.find((a) => a.id === target);
|
|
306
|
+
if (!acc) {
|
|
307
|
+
console.log(`\n${BOLD}Which account?${RESET} multi-clawd login <id>\n`);
|
|
308
|
+
for (const a of accounts) {
|
|
309
|
+
console.log(` ${BOLD}${a.id}${RESET}${a.label ? ` "${a.label}"` : ""}`);
|
|
310
|
+
console.log(` → ${ec.describeAccount(a)}`);
|
|
311
|
+
}
|
|
312
|
+
process.exit(target ? 1 : 0);
|
|
313
|
+
}
|
|
314
|
+
const plan = lp.loginPlanForAccount(acc);
|
|
315
|
+
const expand = (p) => (p.startsWith("~/") ? join(homedir(), p.slice(2)) : p);
|
|
316
|
+
const env = { ...process.env };
|
|
317
|
+
delete env.CLAUDE_CONFIG_DIR;
|
|
318
|
+
let scratch;
|
|
319
|
+
if (plan.scratchDir) {
|
|
320
|
+
scratch = mkdtempSync(join(tmpdir(), "multi-clawd-login-"));
|
|
321
|
+
env.CLAUDE_CONFIG_DIR = scratch;
|
|
322
|
+
} else if (plan.configDir) {
|
|
323
|
+
const dir = expand(plan.configDir);
|
|
324
|
+
if (plan.ensureDir && !existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
325
|
+
env.CLAUDE_CONFIG_DIR = dir;
|
|
326
|
+
}
|
|
327
|
+
if (plan.warn) console.log(` ⚠ ${plan.warn}`);
|
|
328
|
+
console.log(`\n Launching ${DIM}${plan.command.join(" ")}${RESET} for ${BOLD}${acc.id}${RESET}${acc.label ? ` ("${acc.label}")` : ""}.`);
|
|
329
|
+
console.log(` ${BOLD}Sign in as the account this slot is for${RESET} — not your other one!\n`);
|
|
330
|
+
const r = spawnSync(plan.command[0], plan.command.slice(1), { stdio: "inherit", env });
|
|
331
|
+
if (scratch) rmSync(scratch, { recursive: true, force: true });
|
|
332
|
+
if (r.status !== 0) {
|
|
333
|
+
console.error(`\n ❌ ${plan.command.join(" ")} exited with ${r.status ?? "an error"}.`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
if (plan.verify === "auth-status") {
|
|
337
|
+
try {
|
|
338
|
+
const out = spawnSync("claude", ["auth", "status"], { encoding: "utf8", env }).stdout ?? "";
|
|
339
|
+
const email = out.match(/"email"\s*:\s*"([^"]+)"/)?.[1];
|
|
340
|
+
const loggedIn = /"loggedIn"\s*:\s*true/.test(out);
|
|
341
|
+
if (loggedIn) console.log(`\n ✅ ${acc.id} is signed in${email ? ` as ${BOLD}${email}${RESET}` : ""} — double-check that's the right account for this slot.`);
|
|
342
|
+
else console.log("\n ⚠ auth status does not show a login — try again or check `claude auth status` yourself.");
|
|
343
|
+
} catch {
|
|
344
|
+
console.log("\n (could not verify — run `claude auth status` to confirm)");
|
|
345
|
+
}
|
|
346
|
+
} else if (plan.verify === "token-file" && acc.oauthTokenFile) {
|
|
347
|
+
const f = expand(acc.oauthTokenFile);
|
|
348
|
+
console.log(`\n Now: ${plan.afterNote}`);
|
|
349
|
+
if (await askYes(" Done — token saved?")) {
|
|
350
|
+
if (existsSync(f) && statSync(f).size > 0) {
|
|
351
|
+
chmodSync(f, 0o600);
|
|
352
|
+
console.log(` ✅ ${f} present (permissions set to 600). Restart the gateway to pick it up.`);
|
|
353
|
+
} else {
|
|
354
|
+
console.log(` ❌ ${f} is missing or empty — the account won't authenticate until it's there.`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} else if (plan.afterNote) {
|
|
358
|
+
console.log(`\n Now: ${plan.afterNote}`);
|
|
359
|
+
console.log(" Then restart the gateway; its login probe will confirm within ~15 min (or run `multi-clawd doctor`).");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
273
363
|
switch (cmd) {
|
|
274
364
|
case "setup":
|
|
275
365
|
runSibling("setup.mjs", rest);
|
|
276
366
|
break;
|
|
367
|
+
case "login":
|
|
368
|
+
await login();
|
|
369
|
+
break;
|
|
277
370
|
case "explain":
|
|
278
371
|
await explain();
|
|
279
372
|
break;
|
package/scripts/setup.mjs
CHANGED
|
@@ -156,8 +156,12 @@ async function secondAccountFlow(id, prior) {
|
|
|
156
156
|
console.log(` ✗ ${err}`);
|
|
157
157
|
}
|
|
158
158
|
console.log(`
|
|
159
|
-
Now log the SECOND account in (you
|
|
160
|
-
|
|
159
|
+
Now log the SECOND account in (you — it needs your browser and your
|
|
160
|
+
credentials). Easiest AFTER this wizard finishes:
|
|
161
|
+
|
|
162
|
+
multi-clawd login ${id} # launches the right flow, right dir, verified
|
|
163
|
+
|
|
164
|
+
or manually, right now, in your own terminal:
|
|
161
165
|
|
|
162
166
|
CLAUDE_CONFIG_DIR=${configDir} claude setup-token
|
|
163
167
|
|
|
@@ -395,6 +399,7 @@ if (DRY_RUN || !wroteConfig) {
|
|
|
395
399
|
console.log(`
|
|
396
400
|
Done. Finish with:
|
|
397
401
|
|
|
402
|
+
multi-clawd login <account-id> # if any account still needs its Claude sign-in
|
|
398
403
|
openclaw gateway restart
|
|
399
404
|
node ${join(__dirname, "doctor.mjs")} # expect READY
|
|
400
405
|
|