@drakon-systems/multi-clawd 1.1.0 → 1.2.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 +8 -3
- package/dist/update-core.js +30 -0
- package/package.json +4 -1
- package/scripts/cli.mjs +187 -0
package/README.md
CHANGED
|
@@ -165,10 +165,15 @@ second Claude subscription you own.
|
|
|
165
165
|
**Upgrading:**
|
|
166
166
|
|
|
167
167
|
```bash
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
openclaw gateway restart
|
|
168
|
+
npx @drakon-systems/multi-clawd update
|
|
169
|
+
```
|
|
171
170
|
|
|
171
|
+
One command: checks the registry, installs the new version with the right
|
|
172
|
+
flags, offers the gateway restart, and finishes with a doctor health check.
|
|
173
|
+
(`npm i -g @drakon-systems/multi-clawd` once, and it's just `multi-clawd
|
|
174
|
+
update` — with `multi-clawd setup` and `multi-clawd doctor` alongside.)
|
|
175
|
+
|
|
176
|
+
```bash
|
|
172
177
|
# From source:
|
|
173
178
|
cd multi-clawd && git pull && npm install && npm run build && npm run doctor
|
|
174
179
|
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function compareVersions(a, b) {
|
|
2
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
3
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
4
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
5
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
6
|
+
if (d !== 0)
|
|
7
|
+
return d;
|
|
8
|
+
}
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
export function decideUpdateAction(opts) {
|
|
12
|
+
if (opts.installed === undefined)
|
|
13
|
+
return "install";
|
|
14
|
+
if (opts.latest === undefined)
|
|
15
|
+
return "unknown";
|
|
16
|
+
return compareVersions(opts.installed, opts.latest) < 0 ? "update" : "up-to-date";
|
|
17
|
+
}
|
|
18
|
+
export function formatUpdateBanner(opts) {
|
|
19
|
+
const action = decideUpdateAction(opts);
|
|
20
|
+
switch (action) {
|
|
21
|
+
case "install":
|
|
22
|
+
return `not installed — latest is v${opts.latest}`;
|
|
23
|
+
case "update":
|
|
24
|
+
return `update available: v${opts.installed} → v${opts.latest}`;
|
|
25
|
+
case "up-to-date":
|
|
26
|
+
return `up to date (v${opts.installed})`;
|
|
27
|
+
case "unknown":
|
|
28
|
+
return `installed v${opts.installed} — could not reach the registry to check for updates`;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakon-systems/multi-clawd",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"main": "./dist/index.js",
|
|
38
|
+
"bin": {
|
|
39
|
+
"multi-clawd": "./scripts/cli.mjs"
|
|
40
|
+
},
|
|
38
41
|
"publishConfig": {
|
|
39
42
|
"access": "public"
|
|
40
43
|
},
|
package/scripts/cli.mjs
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* multi-clawd CLI — the friendly front door.
|
|
4
|
+
*
|
|
5
|
+
* npx @drakon-systems/multi-clawd setup guided setup wizard
|
|
6
|
+
* npx @drakon-systems/multi-clawd update update to the latest version
|
|
7
|
+
* npx @drakon-systems/multi-clawd doctor health check
|
|
8
|
+
* npx @drakon-systems/multi-clawd version versions (CLI + installed plugin)
|
|
9
|
+
*
|
|
10
|
+
* (Installed globally via `npm i -g @drakon-systems/multi-clawd`, the same
|
|
11
|
+
* commands are just `multi-clawd setup` / `multi-clawd update` / …)
|
|
12
|
+
*
|
|
13
|
+
* `update` wraps the whole upgrade dance — registry version check, the
|
|
14
|
+
* openclaw install with the right flags, gateway restart, doctor — so nobody
|
|
15
|
+
* has to remember `--pin --force`.
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
18
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import readline from "node:readline/promises";
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const PKG = "@drakon-systems/multi-clawd";
|
|
26
|
+
const HOME = homedir();
|
|
27
|
+
const BOLD = process.stdout.isTTY ? "\x1b[1m" : "";
|
|
28
|
+
const DIM = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
29
|
+
const RESET = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
30
|
+
|
|
31
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
32
|
+
|
|
33
|
+
function usage() {
|
|
34
|
+
console.log(`
|
|
35
|
+
${BOLD}🦞 multi-clawd${RESET} — multi-account Claude failover for OpenClaw
|
|
36
|
+
|
|
37
|
+
${BOLD}setup${RESET} guided setup wizard (accounts, pool, watchdog)
|
|
38
|
+
${BOLD}update${RESET} update the plugin to the latest version
|
|
39
|
+
${BOLD}doctor${RESET} health check (add --probe for a live turn)
|
|
40
|
+
${BOLD}version${RESET} show CLI + installed plugin versions
|
|
41
|
+
|
|
42
|
+
Run via npx (${DIM}npx ${PKG} <command>${RESET}) or install globally
|
|
43
|
+
(${DIM}npm i -g ${PKG}${RESET}) for a bare ${DIM}multi-clawd <command>${RESET}.
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Same resolution as the doctor: path install wins, else newest npm-project install. */
|
|
48
|
+
function resolveInstallDir() {
|
|
49
|
+
const extDir = join(HOME, ".openclaw", "extensions", "multi-clawd");
|
|
50
|
+
if (existsSync(join(extDir, "openclaw.plugin.json"))) return extDir;
|
|
51
|
+
const projects = join(HOME, ".openclaw", "npm", "projects");
|
|
52
|
+
let best;
|
|
53
|
+
let bestM = -1;
|
|
54
|
+
try {
|
|
55
|
+
for (const p of readdirSync(projects)) {
|
|
56
|
+
if (!p.startsWith("drakon-systems-multi-clawd-")) continue;
|
|
57
|
+
const dir = join(projects, p, "node_modules", "@drakon-systems", "multi-clawd");
|
|
58
|
+
const manifest = join(dir, "openclaw.plugin.json");
|
|
59
|
+
if (!existsSync(manifest)) continue;
|
|
60
|
+
const m = statSync(manifest).mtimeMs;
|
|
61
|
+
if (m > bestM) {
|
|
62
|
+
bestM = m;
|
|
63
|
+
best = dir;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
/* no npm projects dir */
|
|
68
|
+
}
|
|
69
|
+
return best;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function installedVersion() {
|
|
73
|
+
const dir = resolveInstallDir();
|
|
74
|
+
if (!dir) return undefined;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version;
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function latestVersion() {
|
|
83
|
+
try {
|
|
84
|
+
return execFileSync("npm", ["view", PKG, "version"], { encoding: "utf8", timeout: 15000 })
|
|
85
|
+
.trim();
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function haveOpenclaw() {
|
|
92
|
+
try {
|
|
93
|
+
execFileSync("openclaw", ["--version"], { stdio: "pipe", timeout: 15000 });
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function runSibling(script, args) {
|
|
101
|
+
const r = spawnSync(process.execPath, [join(__dirname, script), ...args], { stdio: "inherit" });
|
|
102
|
+
process.exit(r.status ?? 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function askYes(question, dflt = true) {
|
|
106
|
+
if (!process.stdin.isTTY) return dflt;
|
|
107
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
108
|
+
const a = (await rl.question(`${question} ${dflt ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase();
|
|
109
|
+
rl.close();
|
|
110
|
+
if (!a) return dflt;
|
|
111
|
+
return a.startsWith("y");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function update() {
|
|
115
|
+
let uc;
|
|
116
|
+
try {
|
|
117
|
+
uc = await import(resolve(__dirname, "..", "dist", "update-core.js"));
|
|
118
|
+
} catch {
|
|
119
|
+
console.error("update: built dist/ is missing — reinstall the package.");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
console.log(`\n${BOLD}🦞 multi-clawd update${RESET}\n`);
|
|
123
|
+
if (!haveOpenclaw()) {
|
|
124
|
+
console.error(" ❌ the `openclaw` CLI is not on PATH — install OpenClaw first.");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const installed = installedVersion();
|
|
128
|
+
process.stdout.write(` checking registry… `);
|
|
129
|
+
const latest = latestVersion();
|
|
130
|
+
console.log(latest ? `latest is v${latest}` : "unreachable");
|
|
131
|
+
const banner = uc.formatUpdateBanner({ installed, latest });
|
|
132
|
+
const action = uc.decideUpdateAction({ installed, latest });
|
|
133
|
+
console.log(` ${action === "up-to-date" ? "✅" : action === "unknown" ? "⚠️ " : "⬆️ "} ${banner}\n`);
|
|
134
|
+
if (action === "up-to-date") return;
|
|
135
|
+
if (action === "unknown") {
|
|
136
|
+
console.log(" Check your network and try again.");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
if (!(await askYes(` ${action === "install" ? "Install" : "Update"} now?`))) return;
|
|
140
|
+
|
|
141
|
+
console.log(`\n ${DIM}openclaw plugins install ${PKG} --pin --force${RESET}`);
|
|
142
|
+
const inst = spawnSync("openclaw", ["plugins", "install", PKG, "--pin", "--force"], {
|
|
143
|
+
stdio: "inherit",
|
|
144
|
+
});
|
|
145
|
+
if (inst.status !== 0) {
|
|
146
|
+
console.error("\n ❌ install failed — see output above.");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
if (await askYes("\n Restart the gateway to load it? (briefly interrupts running turns)")) {
|
|
150
|
+
const r = spawnSync("openclaw", ["gateway", "restart"], { stdio: "inherit" });
|
|
151
|
+
if (r.status !== 0) console.log(" ⚠ restart failed — run `openclaw gateway restart` yourself.");
|
|
152
|
+
} else {
|
|
153
|
+
console.log(" ⏳ remember: the new version loads on the next gateway restart.");
|
|
154
|
+
}
|
|
155
|
+
console.log(`\n${BOLD} health check${RESET}`);
|
|
156
|
+
const doc = spawnSync(process.execPath, [join(__dirname, "doctor.mjs")], { stdio: "inherit" });
|
|
157
|
+
if (doc.status !== 0) {
|
|
158
|
+
console.log(`\n ⚠ doctor found problems — if it flagged the watchdog, run ${BOLD}multi-clawd setup${RESET} to repair it.`);
|
|
159
|
+
process.exit(doc.status ?? 1);
|
|
160
|
+
}
|
|
161
|
+
console.log(`\n ✅ done — now on v${installedVersion() ?? "?"}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
switch (cmd) {
|
|
165
|
+
case "setup":
|
|
166
|
+
runSibling("setup.mjs", rest);
|
|
167
|
+
break;
|
|
168
|
+
case "doctor":
|
|
169
|
+
runSibling("doctor.mjs", rest);
|
|
170
|
+
break;
|
|
171
|
+
case "update":
|
|
172
|
+
await update();
|
|
173
|
+
break;
|
|
174
|
+
case "version":
|
|
175
|
+
case "--version":
|
|
176
|
+
case "-v": {
|
|
177
|
+
const cliVersion = JSON.parse(
|
|
178
|
+
readFileSync(resolve(__dirname, "..", "package.json"), "utf8"),
|
|
179
|
+
).version;
|
|
180
|
+
console.log(`cli: v${cliVersion}`);
|
|
181
|
+
console.log(`installed plugin: ${installedVersion() ? `v${installedVersion()}` : "(not installed)"}`);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
usage();
|
|
186
|
+
process.exit(cmd === undefined || cmd === "help" || cmd === "--help" ? 0 : 1);
|
|
187
|
+
}
|