@drakon-systems/multi-clawd 1.9.3 → 1.9.4

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
@@ -375,7 +375,7 @@ openclaw plugins install (Get-Location).Path
375
375
  **Or let your agent install it.** Running an OpenClaw assistant or Claude
376
376
  Code on the target machine already? Paste it this and go make coffee:
377
377
 
378
- > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.9.3/SETUP-AGENT.md
378
+ > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.9.4/SETUP-AGENT.md
379
379
  > and follow it to set up multi-clawd on this machine. I own a second
380
380
  > Claude account — ask me when you need me to log in.
381
381
 
@@ -1,10 +1,11 @@
1
- export function identityFilePath(account, io) {
2
- const dir = !account.native && account.configDir ? io.expandHome(account.configDir) : io.defaultConfigDir;
3
- if (!dir)
4
- return undefined;
1
+ export function identityFilePaths(account, io) {
5
2
  if (account.oauthTokenFile || account.oauthTokenRef)
6
- return undefined;
7
- return `${dir.replace(/\/+$/, "")}/.claude.json`;
3
+ return [];
4
+ const file = (dir) => `${dir.replace(/\/+$/, "")}/.claude.json`;
5
+ if (!account.native && account.configDir)
6
+ return [file(io.expandHome(account.configDir))];
7
+ const candidates = [io.homeDir ? file(io.homeDir) : undefined, io.defaultConfigDir ? file(io.defaultConfigDir) : undefined];
8
+ return [...new Set(candidates.filter((c) => Boolean(c)))];
8
9
  }
9
10
  export function formatPlan(oauthAccount) {
10
11
  const tier = typeof oauthAccount.organizationRateLimitTier === "string"
@@ -47,14 +48,15 @@ export function parseIdentityFile(text) {
47
48
  email: str("emailAddress"),
48
49
  organizationName: str("organizationName"),
49
50
  plan: formatPlan(account),
51
+ profileFetchedAt: typeof account.profileFetchedAt === "number" ? account.profileFetchedAt : undefined,
50
52
  };
51
53
  if (!identity.accountUuid && !identity.email)
52
54
  return undefined;
53
55
  return identity;
54
56
  }
55
57
  export function resolveAccountIdentity(account, io) {
56
- const path = identityFilePath(account, io);
57
- if (!path) {
58
+ const paths = identityFilePaths(account, io);
59
+ if (paths.length === 0) {
58
60
  return {
59
61
  accountId: account.id,
60
62
  status: "unknown",
@@ -63,28 +65,41 @@ export function resolveAccountIdentity(account, io) {
63
65
  : "no config dir to read",
64
66
  };
65
67
  }
66
- let text;
67
- try {
68
- text = io.readFile(path);
69
- }
70
- catch {
71
- return {
72
- accountId: account.id,
73
- status: "unknown",
74
- reason: `no readable ${path} — this account has never completed a login here`,
75
- source: path,
76
- };
77
- }
78
- const identity = parseIdentityFile(text);
79
- if (!identity) {
80
- return {
68
+ let best;
69
+ const problems = [];
70
+ for (const path of paths) {
71
+ let text;
72
+ try {
73
+ text = io.readFile(path);
74
+ }
75
+ catch {
76
+ problems.push(`no readable ${path}`);
77
+ continue;
78
+ }
79
+ const identity = parseIdentityFile(text);
80
+ if (!identity) {
81
+ problems.push(`${path} carries no oauthAccount record`);
82
+ continue;
83
+ }
84
+ const candidate = {
81
85
  accountId: account.id,
82
- status: "unknown",
83
- reason: `${path} carries no oauthAccount record`,
86
+ status: "resolved",
84
87
  source: path,
88
+ ...identity,
85
89
  };
90
+ if (!best || (candidate.profileFetchedAt ?? 0) > (best.profileFetchedAt ?? 0))
91
+ best = candidate;
86
92
  }
87
- return { accountId: account.id, status: "resolved", source: path, ...identity };
93
+ if (best)
94
+ return best;
95
+ return {
96
+ accountId: account.id,
97
+ status: "unknown",
98
+ reason: problems.length === 1
99
+ ? `${problems[0]} — this account has never completed a login here`
100
+ : `${problems.join("; ")} — this account has never completed a login here`,
101
+ source: paths[0],
102
+ };
88
103
  }
89
104
  export function maskEmail(email) {
90
105
  const at = email.lastIndexOf("@");
@@ -271,6 +271,8 @@ export function auditSessionOverrides(sessions, poolConfigured, opts = {}) {
271
271
  const source = entry.modelOverrideSource;
272
272
  if (typeof source !== "string" || source === "auto")
273
273
  continue;
274
+ if (source === "default" && !entry.modelOverride)
275
+ continue;
274
276
  const provider = entry.providerOverride ?? entry.modelProvider;
275
277
  const model = entry.modelOverride;
276
278
  if (!provider || !model) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "multi-clawd",
3
3
  "name": "multi-clawd",
4
- "version": "1.9.3",
4
+ "version": "1.9.4",
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.9.3",
3
+ "version": "1.9.4",
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",
@@ -288,13 +288,17 @@ const { summarizeWindowUsage, classifyAccountHealth } = await importDist("health
288
288
  "summarizeWindowUsage",
289
289
  "classifyAccountHealth",
290
290
  ]);
291
+ // "cli", like the chain audits: resolving WHICH login backs an account is
292
+ // doctor's own diagnostic, not a description of the running plugin's
293
+ // behaviour. Asking the installed copy meant a fix to the resolver could not
294
+ // take effect until the plugin itself was reinstalled — so the CLI reported a
295
+ // bug it had already been taught not to make.
291
296
  const { resolveAccountIdentity, describeIdentity, findDuplicateLogins, maskEmail } =
292
- await importDist("account-identity.js", [
293
- "resolveAccountIdentity",
294
- "describeIdentity",
295
- "findDuplicateLogins",
296
- "maskEmail",
297
- ]);
297
+ await importDist(
298
+ "account-identity.js",
299
+ ["resolveAccountIdentity", "describeIdentity", "findDuplicateLogins", "maskEmail"],
300
+ "cli",
301
+ );
298
302
  const { decideStickySelection } = await importDist("sticky.js", ["decideStickySelection"]);
299
303
  const io = {
300
304
  readFile: (p) => readFileSync(expandHome(p), "utf8"),
@@ -322,6 +326,7 @@ const identityIo = {
322
326
  defaultConfigDir: process.env.CLAUDE_CONFIG_DIR
323
327
  ? expandHome(process.env.CLAUDE_CONFIG_DIR)
324
328
  : join(HOME, ".claude"),
329
+ homeDir: HOME,
325
330
  };
326
331
  const accounts = pluginConfig.accounts ?? [];
327
332
  const identities = [];
@@ -334,7 +339,7 @@ for (const account of accounts) {
334
339
  const identity = resolveAccountIdentity(account, identityIo);
335
340
  identities.push(identity);
336
341
  const source = account.native
337
- ? `native login, ${identityIo.defaultConfigDir}`
342
+ ? `native login, ${identity.source ?? identityIo.defaultConfigDir}`
338
343
  : account.configDir
339
344
  ? `config dir ${account.configDir}`
340
345
  : account.oauthTokenFile