@drakon-systems/multi-clawd 1.8.8 → 1.8.10

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
@@ -369,7 +369,7 @@ openclaw plugins install (Get-Location).Path
369
369
  **Or let your agent install it.** Running an OpenClaw assistant or Claude
370
370
  Code on the target machine already? Paste it this and go make coffee:
371
371
 
372
- > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.8.8/SETUP-AGENT.md
372
+ > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.8.10/SETUP-AGENT.md
373
373
  > and follow it to set up multi-clawd on this machine. I own a second
374
374
  > Claude account — ask me when you need me to log in.
375
375
 
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { healthStateFile, clearAccountCredentialFailure } from "./credential-sta
18
18
  export { healthStateFile, clearAccountCredentialFailure };
19
19
  import { buildAccountChildEnv, tokenFileModeWarning, validateAccountTokenSources, } from "./account-env.js";
20
20
  import { diffCatalogModels, formatNewModelNotice, } from "./model-currency.js";
21
- import { checkAccountCredential, createRefProbeTracker, } from "./login-health.js";
21
+ import { checkAccountCredential, keychainServiceForConfigDir, createRefProbeTracker, } from "./login-health.js";
22
22
  import { execFileSync } from "node:child_process";
23
23
  const CLI_FRESH_WATCHDOG_DEFAULTS = {
24
24
  noOutputTimeoutRatio: 0.8,
@@ -151,19 +151,19 @@ const LOGIN_PROBE_INTERVAL_MS = 15 * 60 * 1000;
151
151
  const LOGIN_PROBE_INITIAL_DELAY_MS = 45 * 1000;
152
152
  const realCredentialIo = {
153
153
  readFile: (p) => readFileSync(expandHome(p), "utf8"),
154
- keychainHasClaudeCredentials: () => {
155
- try {
156
- execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
157
- stdio: "ignore",
158
- });
159
- return true;
160
- }
161
- catch {
162
- return false;
163
- }
164
- },
154
+ keychainHasClaudeCredentials: () => keychainHasService("Claude Code-credentials"),
155
+ keychainHasClaudeCredentialsForDir: (dir) => keychainHasService(keychainServiceForConfigDir(expandHome(dir))),
165
156
  platform: process.platform,
166
157
  };
158
+ function keychainHasService(service) {
159
+ try {
160
+ execFileSync("security", ["find-generic-password", "-s", service], { stdio: "ignore" });
161
+ return true;
162
+ }
163
+ catch {
164
+ return false;
165
+ }
166
+ }
167
167
  const refProbeTrackers = new Map();
168
168
  const lastProbeStatus = new Map();
169
169
  function recordAccountCredentialFailure(accountId, reason, nowMs) {
@@ -437,6 +437,10 @@ async function buildAccountEnv(account, resolver) {
437
437
  const token = await resolveTokenAsync(account, resolver);
438
438
  return buildAccountChildEnv(account, token, healthStateFile(account.id));
439
439
  }
440
+ export function isRuntimelessRegistration(api) {
441
+ const mode = api?.registrationMode;
442
+ return mode === "cli-metadata" || mode === "setup-only";
443
+ }
440
444
  function buildCatalogProvider(account) {
441
445
  return {
442
446
  id: account.id,
@@ -466,6 +470,8 @@ export default definePluginEntry({
466
470
  name: "multi-clawd",
467
471
  description: "Register additional Claude Code logins as first-class OpenClaw CLI backends for cross-account failover.",
468
472
  register(api) {
473
+ if (isRuntimelessRegistration(api))
474
+ return;
469
475
  const runtimeConfigLoader = api.runtime?.config?.current
470
476
  ? () => api.runtime.config.current()
471
477
  : undefined;
@@ -1,3 +1,8 @@
1
+ import { createHash } from "node:crypto";
2
+ export function keychainServiceForConfigDir(absDir) {
3
+ const hash = createHash("sha256").update(absDir).digest("hex").slice(0, 8);
4
+ return `Claude Code-credentials-${hash}`;
5
+ }
1
6
  export function createRefProbeTracker(options = {}) {
2
7
  const deadAfterConsecutive = options.deadAfterConsecutive ?? 3;
3
8
  const deadAfterMs = options.deadAfterMs ?? 10 * 60 * 1000;
@@ -87,7 +92,16 @@ export function checkAccountCredential(account, io) {
87
92
  return checkCredentialsJson(io, "~/.claude");
88
93
  }
89
94
  if (account.configDir) {
90
- return checkCredentialsJson(io, account.configDir);
95
+ if (io.platform === "darwin" && io.keychainHasClaudeCredentialsForDir(account.configDir)) {
96
+ return { status: "ok" };
97
+ }
98
+ const file = checkCredentialsJson(io, account.configDir);
99
+ if (file.status === "ok" || io.platform !== "darwin")
100
+ return file;
101
+ return {
102
+ status: "broken",
103
+ reason: `keychain has no Claude Code credentials for ${account.configDir} and ${file.reason}`,
104
+ };
91
105
  }
92
106
  return { status: "unknown" };
93
107
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "multi-clawd",
3
3
  "name": "multi-clawd",
4
- "version": "1.8.8",
4
+ "version": "1.8.10",
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": "1.8.8",
3
+ "version": "1.8.10",
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",
@@ -280,7 +280,10 @@ try {
280
280
 
281
281
  // ── 4. account credentials (values never printed) ──────────────────────────
282
282
  console.log("account credentials");
283
- const { checkAccountCredential } = await importDist("login-health.js", ["checkAccountCredential"]);
283
+ const { checkAccountCredential, keychainServiceForConfigDir } = await importDist("login-health.js", [
284
+ "checkAccountCredential",
285
+ "keychainServiceForConfigDir",
286
+ ]);
284
287
  const { summarizeWindowUsage, classifyAccountHealth } = await importDist("health.js", [
285
288
  "summarizeWindowUsage",
286
289
  "classifyAccountHealth",
@@ -295,18 +298,20 @@ const { resolveAccountIdentity, describeIdentity, findDuplicateLogins, maskEmail
295
298
  const { decideStickySelection } = await importDist("sticky.js", ["decideStickySelection"]);
296
299
  const io = {
297
300
  readFile: (p) => readFileSync(expandHome(p), "utf8"),
298
- keychainHasClaudeCredentials: () => {
299
- try {
300
- execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
301
- stdio: "ignore",
302
- });
303
- return true;
304
- } catch {
305
- return false;
306
- }
307
- },
301
+ keychainHasClaudeCredentials: () => keychainHasService("Claude Code-credentials"),
302
+ keychainHasClaudeCredentialsForDir: (dir) =>
303
+ keychainHasService(keychainServiceForConfigDir(expandHome(dir))),
308
304
  platform: process.platform,
309
305
  };
306
+ // Metadata-only probe (no `-w`): the secret is never read.
307
+ function keychainHasService(service) {
308
+ try {
309
+ execFileSync("security", ["find-generic-password", "-s", service], { stdio: "ignore" });
310
+ return true;
311
+ } catch {
312
+ return false;
313
+ }
314
+ }
310
315
  // A native account's child sets no CLAUDE_CONFIG_DIR, so it authenticates
311
316
  // against whatever the default dir is in the ENV THE GATEWAY RUNS IN. Doctor
312
317
  // reads the same variable and prints the path it used, so a box that exports