@drakon-systems/multi-clawd 1.5.3 → 1.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 +1 -1
- package/dist/update-core.js +42 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/cli.mjs +71 -1
- package/scripts/doctor.mjs +52 -2
package/README.md
CHANGED
|
@@ -188,7 +188,7 @@ openclaw plugins install (Get-Location).Path
|
|
|
188
188
|
**Or let your agent install it.** Running an OpenClaw assistant or Claude
|
|
189
189
|
Code on the target machine already? Paste it this and go make coffee:
|
|
190
190
|
|
|
191
|
-
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.
|
|
191
|
+
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.6.0/SETUP-AGENT.md
|
|
192
192
|
> and follow it to set up multi-clawd on this machine. I own a second
|
|
193
193
|
> Claude account — ask me when you need me to log in.
|
|
194
194
|
|
package/dist/update-core.js
CHANGED
|
@@ -15,6 +15,48 @@ export function decideUpdateAction(opts) {
|
|
|
15
15
|
return "unknown";
|
|
16
16
|
return compareVersions(opts.installed, opts.latest) < 0 ? "update" : "up-to-date";
|
|
17
17
|
}
|
|
18
|
+
export function classifyCliSkew(opts) {
|
|
19
|
+
if (opts.pluginVersion === undefined)
|
|
20
|
+
return "plugin-missing";
|
|
21
|
+
const d = compareVersions(opts.cliVersion, opts.pluginVersion);
|
|
22
|
+
if (d === 0)
|
|
23
|
+
return "aligned";
|
|
24
|
+
return d < 0 ? "cli-behind" : "cli-ahead";
|
|
25
|
+
}
|
|
26
|
+
export function detectCliInstallKind(cliDir) {
|
|
27
|
+
if (/[/\\]_npx[/\\]/.test(cliDir))
|
|
28
|
+
return "npx";
|
|
29
|
+
if (/[/\\]node_modules[/\\]/.test(cliDir))
|
|
30
|
+
return "global";
|
|
31
|
+
return "source";
|
|
32
|
+
}
|
|
33
|
+
export function cliUpdateCommand(kind, pkg) {
|
|
34
|
+
switch (kind) {
|
|
35
|
+
case "global":
|
|
36
|
+
return `npm i -g ${pkg}@latest`;
|
|
37
|
+
case "npx":
|
|
38
|
+
return `npx ${pkg}@latest <command>`;
|
|
39
|
+
case "source":
|
|
40
|
+
return "git pull && npm run build";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function formatCliSkew(opts) {
|
|
44
|
+
const skew = classifyCliSkew(opts);
|
|
45
|
+
const fix = cliUpdateCommand(opts.installKind, opts.pkg);
|
|
46
|
+
switch (skew) {
|
|
47
|
+
case "aligned":
|
|
48
|
+
return undefined;
|
|
49
|
+
case "plugin-missing":
|
|
50
|
+
return undefined;
|
|
51
|
+
case "cli-behind":
|
|
52
|
+
return (`CLI v${opts.cliVersion} is older than the installed plugin v${opts.pluginVersion} — ` +
|
|
53
|
+
`\`doctor\`/\`setup\` run from the CLI, so this one reports on the plugin using ` +
|
|
54
|
+
`older logic. Fix: ${fix}`);
|
|
55
|
+
case "cli-ahead":
|
|
56
|
+
return (`CLI v${opts.cliVersion} is newer than the installed plugin v${opts.pluginVersion} — ` +
|
|
57
|
+
`the plugin serving your turns is behind. Fix: multi-clawd update`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
18
60
|
export function formatUpdateBanner(opts) {
|
|
19
61
|
const action = decideUpdateAction(opts);
|
|
20
62
|
switch (action) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "multi-clawd",
|
|
3
3
|
"name": "multi-clawd",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.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": "1.
|
|
3
|
+
"version": "1.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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/scripts/cli.mjs
CHANGED
|
@@ -88,6 +88,30 @@ async function askYes(question, dflt = true) {
|
|
|
88
88
|
return a.startsWith("y");
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/** This package's own version (the CLI half). */
|
|
92
|
+
function cliVersion() {
|
|
93
|
+
return JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf8")).version;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Skew advice for the current install, or undefined when the two halves agree.
|
|
98
|
+
* Loads the pure classifier from dist; stays silent if dist is unavailable so
|
|
99
|
+
* a missing build can never turn an informational note into a hard failure.
|
|
100
|
+
*/
|
|
101
|
+
async function cliSkewNote(cli = cliVersion(), plugin = installedVersion()) {
|
|
102
|
+
try {
|
|
103
|
+
const uc = await import(resolve(__dirname, "..", "dist", "update-core.js"));
|
|
104
|
+
return uc.formatCliSkew({
|
|
105
|
+
cliVersion: cli,
|
|
106
|
+
pluginVersion: plugin,
|
|
107
|
+
installKind: uc.detectCliInstallKind(resolve(__dirname, "..")),
|
|
108
|
+
pkg: PKG,
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
91
115
|
async function update() {
|
|
92
116
|
let uc;
|
|
93
117
|
try {
|
|
@@ -130,6 +154,7 @@ async function update() {
|
|
|
130
154
|
console.log(" ⏳ remember: the new version loads on the next gateway restart.");
|
|
131
155
|
}
|
|
132
156
|
await healWatchdogUnit();
|
|
157
|
+
await offerCliSelfUpdate(uc);
|
|
133
158
|
console.log(`\n${BOLD} health check${RESET}`);
|
|
134
159
|
const doc = spawnSync(process.execPath, [join(__dirname, "doctor.mjs")], { stdio: "inherit" });
|
|
135
160
|
if (doc.status !== 0) {
|
|
@@ -139,6 +164,46 @@ async function update() {
|
|
|
139
164
|
console.log(`\n ✅ done — now on v${installedVersion() ?? "?"}`);
|
|
140
165
|
}
|
|
141
166
|
|
|
167
|
+
/**
|
|
168
|
+
* `update` upgrades the PLUGIN; this finishes the job by offering to upgrade
|
|
169
|
+
* the CLI too, so "update" means what a user reasonably assumes it means.
|
|
170
|
+
*
|
|
171
|
+
* Runs LAST in the update flow on purpose: `npm i -g` replaces this package's
|
|
172
|
+
* own directory, so nothing may dynamically import from it afterwards. Skipped
|
|
173
|
+
* silently when the halves already agree, and never forced — a global install
|
|
174
|
+
* can need permissions we shouldn't assume, and npx users have nothing to
|
|
175
|
+
* update at all.
|
|
176
|
+
*/
|
|
177
|
+
async function offerCliSelfUpdate(uc) {
|
|
178
|
+
const cli = cliVersion();
|
|
179
|
+
const plugin = installedVersion();
|
|
180
|
+
if (uc.classifyCliSkew({ cliVersion: cli, pluginVersion: plugin }) !== "cli-behind") return;
|
|
181
|
+
|
|
182
|
+
const kind = uc.detectCliInstallKind(resolve(__dirname, ".."));
|
|
183
|
+
const fix = uc.cliUpdateCommand(kind, PKG);
|
|
184
|
+
console.log(
|
|
185
|
+
`\n ⚠️ Your ${BOLD}multi-clawd${RESET} command is v${cli} but the plugin is now v${plugin}.`,
|
|
186
|
+
);
|
|
187
|
+
console.log(
|
|
188
|
+
` ${DIM}doctor and setup run from the command, so they'd report on the new plugin using old logic.${RESET}`,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (kind !== "global") {
|
|
192
|
+
console.log(` Bring it up to date with: ${BOLD}${fix}${RESET}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!(await askYes(` Update the command now? (${fix})`))) {
|
|
196
|
+
console.log(` ${DIM}Skipped — run \`${fix}\` when you're ready.${RESET}`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const r = spawnSync("npm", ["i", "-g", `${PKG}@latest`], { stdio: "inherit" });
|
|
200
|
+
if (r.status === 0) {
|
|
201
|
+
console.log(` ✅ command updated — the new version applies from your next run.`);
|
|
202
|
+
} else {
|
|
203
|
+
console.log(` ⚠ that failed (permissions?) — run it yourself: ${BOLD}${fix}${RESET}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
142
207
|
/**
|
|
143
208
|
* Self-heal the scheduled watchdog after an update: the npm install dir is
|
|
144
209
|
* regenerated on every update, so a unit pointing into it just orphaned.
|
|
@@ -386,8 +451,13 @@ switch (cmd) {
|
|
|
386
451
|
const cliVersion = JSON.parse(
|
|
387
452
|
readFileSync(resolve(__dirname, "..", "package.json"), "utf8"),
|
|
388
453
|
).version;
|
|
454
|
+
const pluginVersion = installedVersion();
|
|
389
455
|
console.log(`cli: v${cliVersion}`);
|
|
390
|
-
console.log(`installed plugin: ${
|
|
456
|
+
console.log(`installed plugin: ${pluginVersion ? `v${pluginVersion}` : "(not installed)"}`);
|
|
457
|
+
// Two versions printed side by side invite exactly one question — "is that
|
|
458
|
+
// a problem?" — so answer it here rather than leaving the reader to guess.
|
|
459
|
+
const skewNote = await cliSkewNote(cliVersion, pluginVersion);
|
|
460
|
+
if (skewNote) console.log(`\n⚠️ ${skewNote}`);
|
|
391
461
|
break;
|
|
392
462
|
}
|
|
393
463
|
default:
|
package/scripts/doctor.mjs
CHANGED
|
@@ -114,6 +114,31 @@ const entry = config?.plugins?.entries?.["multi-clawd"];
|
|
|
114
114
|
const pluginConfig = entry?.config ?? {};
|
|
115
115
|
if (!manifest) bad(`no installed manifest at ${EXT_DIR}`);
|
|
116
116
|
else ok(`installed at ${EXT_DIR}`);
|
|
117
|
+
|
|
118
|
+
// Doctor itself ships in the CLI half, so a stale CLI means these very
|
|
119
|
+
// findings were produced by older logic than the plugin they describe. That
|
|
120
|
+
// has to be the first thing reported, or every line below is suspect.
|
|
121
|
+
{
|
|
122
|
+
// REPO_DIR first, unlike the health imports below: this is CLI-side logic and
|
|
123
|
+
// doctor IS the CLI half, so it must use its own copy. Reaching for the
|
|
124
|
+
// plugin's copy would ask a possibly-older artifact whether it is older.
|
|
125
|
+
const uc = await import(join(REPO_DIR, "dist", "update-core.js")).catch(() =>
|
|
126
|
+
import(join(EXT_DIR, "dist", "update-core.js")).catch(() => undefined),
|
|
127
|
+
);
|
|
128
|
+
const cliVer = readJson(join(REPO_DIR, "package.json"))?.version;
|
|
129
|
+
const pluginVer = manifest?.version;
|
|
130
|
+
const note =
|
|
131
|
+
uc && cliVer && typeof uc.formatCliSkew === "function"
|
|
132
|
+
? uc.formatCliSkew({
|
|
133
|
+
cliVersion: cliVer,
|
|
134
|
+
pluginVersion: pluginVer,
|
|
135
|
+
installKind: uc.detectCliInstallKind(REPO_DIR),
|
|
136
|
+
pkg: "@drakon-systems/multi-clawd",
|
|
137
|
+
})
|
|
138
|
+
: undefined;
|
|
139
|
+
if (note) warn(note);
|
|
140
|
+
else if (cliVer && pluginVer) ok(`CLI and plugin both v${cliVer}`);
|
|
141
|
+
}
|
|
117
142
|
if (!entry) bad("no plugins.entries[\"multi-clawd\"] in openclaw.json");
|
|
118
143
|
else if (entry.enabled !== true) bad("plugin entry present but not enabled");
|
|
119
144
|
else ok("plugin entry enabled");
|
|
@@ -182,6 +207,9 @@ console.log("account credentials");
|
|
|
182
207
|
const { checkAccountCredential } = await import(join(EXT_DIR, "dist", "login-health.js")).catch(
|
|
183
208
|
() => import(join(REPO_DIR, "dist", "login-health.js")),
|
|
184
209
|
);
|
|
210
|
+
const { summarizeWindowUsage } = await import(join(EXT_DIR, "dist", "health.js")).catch(() =>
|
|
211
|
+
import(join(REPO_DIR, "dist", "health.js")),
|
|
212
|
+
);
|
|
185
213
|
const io = {
|
|
186
214
|
readFile: (p) => readFileSync(expandHome(p), "utf8"),
|
|
187
215
|
keychainHasClaudeCredentials: () => {
|
|
@@ -218,10 +246,32 @@ for (const account of accounts) {
|
|
|
218
246
|
continue;
|
|
219
247
|
}
|
|
220
248
|
const ageMin = Math.round((Date.now() - (state.updatedAt ?? 0)) / 60000);
|
|
249
|
+
// A window whose own reset has PASSED describes the previous cycle, and the
|
|
250
|
+
// rotation logic already voids it ("a passed reset voids the observation",
|
|
251
|
+
// health.ts). Doctor used to print it raw beside a fresh `(0m old)` stamp,
|
|
252
|
+
// so a five-day-dead `seven_day@96%` read as "96% used right now" when
|
|
253
|
+
// nothing was acting on it. The age stamp is the age of the OBSERVATION,
|
|
254
|
+
// never the window's validity.
|
|
255
|
+
//
|
|
256
|
+
// `summarizeWindowUsage` is the same function `explain` uses, so all three
|
|
257
|
+
// surfaces — rotation, explain, doctor — agree on what counts as live
|
|
258
|
+
// rather than each carrying its own copy of the rule.
|
|
259
|
+
const live = new Set(
|
|
260
|
+
summarizeWindowUsage(state, { staleAfterMs: pluginConfig.pool?.staleAfterMs }, Date.now()).map(
|
|
261
|
+
(u) => u.window,
|
|
262
|
+
),
|
|
263
|
+
);
|
|
221
264
|
const windows = Object.entries(state.windows ?? {})
|
|
222
|
-
.map(([w, d]) =>
|
|
265
|
+
.map(([w, d]) => {
|
|
266
|
+
const hasUtil = typeof d.utilization === "number";
|
|
267
|
+
const util = hasUtil ? `@${Math.round(d.utilization * 100)}%` : "";
|
|
268
|
+
// Only utilization-bearing account windows are summarised; a window
|
|
269
|
+
// carrying a number that did NOT survive is one the pool ignores.
|
|
270
|
+
const stale = hasUtil && !w.startsWith("model:") && !live.has(w);
|
|
271
|
+
return `${w}:${d.status}${util}${stale ? " (expired — ignored)" : ""}`;
|
|
272
|
+
})
|
|
223
273
|
.join(" ");
|
|
224
|
-
ok(`${account.id}: ${windows || "no windows"} (${ageMin}m
|
|
274
|
+
ok(`${account.id}: ${windows || "no windows"} (observed ${ageMin}m ago)`);
|
|
225
275
|
}
|
|
226
276
|
|
|
227
277
|
// ── 6. pool ─────────────────────────────────────────────────────────────────
|