@drakon-systems/multi-clawd 1.3.1 → 1.4.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/README.md CHANGED
@@ -28,6 +28,7 @@ npm i -g @drakon-systems/multi-clawd # the CLI (once)
28
28
 
29
29
  multi-clawd update # install (or update) the OpenClaw plugin — right flags, restart, doctor
30
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)
31
32
  multi-clawd explain # your whole setup in plain English — accounts, chain, live health
32
33
  multi-clawd doctor # health check (add --probe for a live end-to-end turn)
33
34
  ```
@@ -223,17 +224,28 @@ install → re-add plan.
223
224
 
224
225
  ## Set up a second account
225
226
 
226
- **Easiest: the setup wizard.** It walks you through the whole shape below
227
- main account, isolated second account, token storage, pool — and merges the
228
- result into `openclaw.json` non-destructively (backup first, merge by id,
229
- re-runs are no-ops, never sees a token value):
227
+ **Easiest: the wizard, then the login command.** The wizard walks you through
228
+ the whole shape below — main account, isolated second account, token storage,
229
+ pool — and merges the result into `openclaw.json` non-destructively (backup
230
+ first, merge by id, existing accounts never overwritten, re-runs are no-ops,
231
+ never sees a token value). Then `login` performs the actual Claude sign-in
232
+ for any account — right flow, right config dir, verified afterwards with the
233
+ signed-in email shown:
230
234
 
231
235
  ```bash
232
- npm run setup # source checkout
233
- node ~/.openclaw/extensions/multi-clawd/scripts/setup.mjs # installed copy
234
- # add --dry-run to preview the changes without writing anything
236
+ multi-clawd setup # wire the accounts + pool (add --dry-run to preview)
237
+ multi-clawd login claw2 # launch the second account's Claude sign-in
238
+ multi-clawd login claw1 # (works for re-authing ANY account, any time)
235
239
  ```
236
240
 
241
+ `login` knows what each account is: a native account signs in against the
242
+ default dir/keychain, an isolated-dir account inside its own dir (created
243
+ `0700` if missing), and token accounts get `claude setup-token` — in a
244
+ throwaway scratch dir when the account has no dir of its own, so your main
245
+ login is never disturbed. You do the OAuth in the launched flow; the CLI
246
+ never captures, stores, or prints a token value.
247
+ (From a source checkout, `npm run setup` still works for the wizard.)
248
+
237
249
  The key idea either way: your **main** account keeps the default `~/.claude`
238
250
  login untouched, and each **extra** account gets its *own isolated config
239
251
  dir* — a separate Claude "app" — so the logins can never clobber each other.
@@ -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.1",
3
+ "version": "1.4.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",
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, in your own terminal the wizard
160
- cannot and must not do this for you):
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