@drakon-systems/multi-clawd 1.2.0 → 1.2.2
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/dist/watchdog-schedule.js +50 -1
- package/package.json +1 -1
- package/scripts/_shared.mjs +52 -0
- package/scripts/cli.mjs +71 -28
- package/scripts/doctor.mjs +6 -1
- package/scripts/setup.mjs +32 -4
|
@@ -56,7 +56,7 @@ WantedBy=timers.target
|
|
|
56
56
|
return [];
|
|
57
57
|
}
|
|
58
58
|
export function extractWatchdogTarget(unitText) {
|
|
59
|
-
const m = unitText.match(/[^<>\s="']*eviction-watchdog\.mjs/);
|
|
59
|
+
const m = unitText.match(/[^<>\s="']*(?:eviction-watchdog|watchdog-launcher)\.mjs/);
|
|
60
60
|
return m ? m[0] : undefined;
|
|
61
61
|
}
|
|
62
62
|
export function classifyWatchdogUnit(targetPath, exists) {
|
|
@@ -64,3 +64,52 @@ export function classifyWatchdogUnit(targetPath, exists) {
|
|
|
64
64
|
return "absent";
|
|
65
65
|
return exists(targetPath) ? "ok" : "orphaned";
|
|
66
66
|
}
|
|
67
|
+
export function isFragileWatchdogTarget(target) {
|
|
68
|
+
return target.includes("/.openclaw/npm/projects/");
|
|
69
|
+
}
|
|
70
|
+
export function renderWatchdogLauncher() {
|
|
71
|
+
return `#!/usr/bin/env node
|
|
72
|
+
// multi-clawd watchdog launcher — the STABLE target for scheduler units.
|
|
73
|
+
// Installs move on every update (the npm project dir is regenerated), so the
|
|
74
|
+
// unit points HERE; this launcher finds the current install at runtime and
|
|
75
|
+
// runs its eviction watchdog. Managed by the setup wizard / update command.
|
|
76
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
77
|
+
import { homedir } from "node:os";
|
|
78
|
+
import { join } from "node:path";
|
|
79
|
+
import { spawnSync } from "node:child_process";
|
|
80
|
+
|
|
81
|
+
const HOME = homedir();
|
|
82
|
+
|
|
83
|
+
function installDir() {
|
|
84
|
+
const ext = join(HOME, ".openclaw", "extensions", "multi-clawd");
|
|
85
|
+
if (existsSync(join(ext, "openclaw.plugin.json"))) return ext;
|
|
86
|
+
const projects = join(HOME, ".openclaw", "npm", "projects");
|
|
87
|
+
let best;
|
|
88
|
+
let bestM = -1;
|
|
89
|
+
try {
|
|
90
|
+
for (const p of readdirSync(projects)) {
|
|
91
|
+
if (!p.startsWith("drakon-systems-multi-clawd-")) continue;
|
|
92
|
+
const dir = join(projects, p, "node_modules", "@drakon-systems", "multi-clawd");
|
|
93
|
+
const manifest = join(dir, "openclaw.plugin.json");
|
|
94
|
+
if (!existsSync(manifest)) continue;
|
|
95
|
+
const t = statSync(manifest).mtimeMs;
|
|
96
|
+
if (t > bestM) { bestM = t; best = dir; }
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
99
|
+
return best;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const dir = installDir();
|
|
103
|
+
if (!dir) {
|
|
104
|
+
console.error("[watchdog-launcher] no multi-clawd install found — nothing to do");
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
const script = join(dir, "scripts", "eviction-watchdog.mjs");
|
|
108
|
+
if (!existsSync(script)) {
|
|
109
|
+
console.error("[watchdog-launcher] " + script + " missing — nothing to do");
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
const r = spawnSync(process.execPath, [script], { stdio: "inherit" });
|
|
113
|
+
process.exit(r.status ?? 0);
|
|
114
|
+
`;
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakon-systems/multi-clawd",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
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",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the CLI scripts (cli.mjs, setup.mjs).
|
|
3
|
+
*
|
|
4
|
+
* Two real footguns shaped this file: run via `npx`, a script's __dirname is
|
|
5
|
+
* the EPHEMERAL npx cache; and the npm install dir itself is regenerated on
|
|
6
|
+
* every update. So nothing durable (scheduler units) may point at either —
|
|
7
|
+
* they point at the stable WATCHDOG_LAUNCHER, which resolves the current
|
|
8
|
+
* install at runtime.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
/** Where the plugin is installed: path install wins, else newest npm-project install. */
|
|
15
|
+
export function resolveInstallDir() {
|
|
16
|
+
const HOME = homedir();
|
|
17
|
+
const extDir = join(HOME, ".openclaw", "extensions", "multi-clawd");
|
|
18
|
+
if (existsSync(join(extDir, "openclaw.plugin.json"))) return extDir;
|
|
19
|
+
const projects = join(HOME, ".openclaw", "npm", "projects");
|
|
20
|
+
let best;
|
|
21
|
+
let bestM = -1;
|
|
22
|
+
try {
|
|
23
|
+
for (const p of readdirSync(projects)) {
|
|
24
|
+
if (!p.startsWith("drakon-systems-multi-clawd-")) continue;
|
|
25
|
+
const dir = join(projects, p, "node_modules", "@drakon-systems", "multi-clawd");
|
|
26
|
+
const manifest = join(dir, "openclaw.plugin.json");
|
|
27
|
+
if (!existsSync(manifest)) continue;
|
|
28
|
+
const m = statSync(manifest).mtimeMs;
|
|
29
|
+
if (m > bestM) {
|
|
30
|
+
bestM = m;
|
|
31
|
+
best = dir;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
/* no npm projects dir */
|
|
36
|
+
}
|
|
37
|
+
return best;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The stable launcher path scheduler units point at. Lives in the state dir
|
|
42
|
+
* (never moved by installs); its content is rendered by
|
|
43
|
+
* watchdog-schedule.ts#renderWatchdogLauncher and resolves the current
|
|
44
|
+
* install at runtime.
|
|
45
|
+
*/
|
|
46
|
+
export const WATCHDOG_LAUNCHER = join(
|
|
47
|
+
homedir(),
|
|
48
|
+
".openclaw",
|
|
49
|
+
"state",
|
|
50
|
+
"multi-clawd",
|
|
51
|
+
"watchdog-launcher.mjs",
|
|
52
|
+
);
|
package/scripts/cli.mjs
CHANGED
|
@@ -15,15 +15,13 @@
|
|
|
15
15
|
* has to remember `--pin --force`.
|
|
16
16
|
*/
|
|
17
17
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
18
|
-
import {
|
|
19
|
-
import { homedir } from "node:os";
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
20
19
|
import { dirname, join, resolve } from "node:path";
|
|
21
20
|
import { fileURLToPath } from "node:url";
|
|
22
21
|
import readline from "node:readline/promises";
|
|
23
22
|
|
|
24
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
24
|
const PKG = "@drakon-systems/multi-clawd";
|
|
26
|
-
const HOME = homedir();
|
|
27
25
|
const BOLD = process.stdout.isTTY ? "\x1b[1m" : "";
|
|
28
26
|
const DIM = process.stdout.isTTY ? "\x1b[2m" : "";
|
|
29
27
|
const RESET = process.stdout.isTTY ? "\x1b[0m" : "";
|
|
@@ -44,30 +42,7 @@ Run via npx (${DIM}npx ${PKG} <command>${RESET}) or install globally
|
|
|
44
42
|
`);
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
|
|
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
|
-
}
|
|
45
|
+
const { resolveInstallDir } = await import(join(__dirname, "_shared.mjs"));
|
|
71
46
|
|
|
72
47
|
function installedVersion() {
|
|
73
48
|
const dir = resolveInstallDir();
|
|
@@ -152,15 +127,83 @@ async function update() {
|
|
|
152
127
|
} else {
|
|
153
128
|
console.log(" ⏳ remember: the new version loads on the next gateway restart.");
|
|
154
129
|
}
|
|
130
|
+
await healWatchdogUnit();
|
|
155
131
|
console.log(`\n${BOLD} health check${RESET}`);
|
|
156
132
|
const doc = spawnSync(process.execPath, [join(__dirname, "doctor.mjs")], { stdio: "inherit" });
|
|
157
133
|
if (doc.status !== 0) {
|
|
158
|
-
console.log(`\n ⚠ doctor found problems — if it flagged the watchdog, run ${BOLD}
|
|
134
|
+
console.log(`\n ⚠ doctor found problems — if it flagged the watchdog, run ${BOLD}npx ${PKG} setup${RESET} to repair it.`);
|
|
159
135
|
process.exit(doc.status ?? 1);
|
|
160
136
|
}
|
|
161
137
|
console.log(`\n ✅ done — now on v${installedVersion() ?? "?"}`);
|
|
162
138
|
}
|
|
163
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Self-heal the scheduled watchdog after an update: the npm install dir is
|
|
142
|
+
* regenerated on every update, so a unit pointing into it just orphaned.
|
|
143
|
+
* Move any broken-or-fragile unit onto the stable launcher; refresh the
|
|
144
|
+
* launcher's content when a unit already uses it. Never fatal.
|
|
145
|
+
*/
|
|
146
|
+
async function healWatchdogUnit() {
|
|
147
|
+
try {
|
|
148
|
+
const wds = await import(resolve(__dirname, "..", "dist", "watchdog-schedule.js"));
|
|
149
|
+
const { WATCHDOG_LAUNCHER } = await import(join(__dirname, "_shared.mjs"));
|
|
150
|
+
const { existsSync, readdirSync, readFileSync: rf, writeFileSync, mkdirSync } = await import("node:fs");
|
|
151
|
+
const { homedir } = await import("node:os");
|
|
152
|
+
const { dirname: dn } = await import("node:path");
|
|
153
|
+
for (const d of [
|
|
154
|
+
join(homedir(), "Library", "LaunchAgents"),
|
|
155
|
+
join(homedir(), ".config", "systemd", "user"),
|
|
156
|
+
]) {
|
|
157
|
+
let files = [];
|
|
158
|
+
try {
|
|
159
|
+
files = readdirSync(d);
|
|
160
|
+
} catch {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
for (const f of files) {
|
|
164
|
+
if (!/\.(plist|service|timer)$/.test(f)) continue;
|
|
165
|
+
const file = join(d, f);
|
|
166
|
+
let text;
|
|
167
|
+
try {
|
|
168
|
+
text = rf(file, "utf8");
|
|
169
|
+
} catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const target = wds.extractWatchdogTarget(text);
|
|
173
|
+
if (!target) continue;
|
|
174
|
+
const refreshLauncher = () => {
|
|
175
|
+
mkdirSync(dn(WATCHDOG_LAUNCHER), { recursive: true });
|
|
176
|
+
writeFileSync(WATCHDOG_LAUNCHER, wds.renderWatchdogLauncher());
|
|
177
|
+
};
|
|
178
|
+
if (target === WATCHDOG_LAUNCHER) {
|
|
179
|
+
refreshLauncher();
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!existsSync(target) || wds.isFragileWatchdogTarget(target)) {
|
|
183
|
+
refreshLauncher();
|
|
184
|
+
writeFileSync(file, text.split(target).join(WATCHDOG_LAUNCHER));
|
|
185
|
+
if (d.endsWith("LaunchAgents")) {
|
|
186
|
+
try {
|
|
187
|
+
execFileSync("/bin/sh", ["-c", `launchctl unload "${file}" 2>/dev/null; launchctl load "${file}" 2>&1 || true`]);
|
|
188
|
+
} catch {
|
|
189
|
+
/* manual load needed */
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
try {
|
|
193
|
+
execFileSync("systemctl", ["--user", "daemon-reload"]);
|
|
194
|
+
} catch {
|
|
195
|
+
/* manual reload needed */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
console.log(` 🔧 watchdog unit ${f} → stable launcher (survives future updates)`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
/* healing is best-effort; doctor still reports the truth */
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
164
207
|
switch (cmd) {
|
|
165
208
|
case "setup":
|
|
166
209
|
runSibling("setup.mjs", rest);
|
package/scripts/doctor.mjs
CHANGED
|
@@ -371,8 +371,13 @@ if (watchdogFound) {
|
|
|
371
371
|
} catch {
|
|
372
372
|
continue;
|
|
373
373
|
}
|
|
374
|
-
const target = text.match(/[^<>\s="']*eviction-watchdog\.mjs/)?.[0];
|
|
374
|
+
const target = text.match(/[^<>\s="']*(?:eviction-watchdog|watchdog-launcher)\.mjs/)?.[0];
|
|
375
375
|
if (target && !existsSync(target)) orphan = { file: join(d, f), target };
|
|
376
|
+
else if (target && target.includes("/.openclaw/npm/projects/")) {
|
|
377
|
+
warn(
|
|
378
|
+
`watchdog unit ${join(d, f)} points INTO the npm install dir — regenerated on every update, so it WILL orphan. Run the setup wizard (or \`update\`) to move it to the stable launcher.`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
376
381
|
}
|
|
377
382
|
}
|
|
378
383
|
if (orphan) {
|
package/scripts/setup.mjs
CHANGED
|
@@ -229,7 +229,16 @@ async function watchdogStep() {
|
|
|
229
229
|
console.log(" (auto-scheduling not supported on this platform — see README for manual setup)");
|
|
230
230
|
return;
|
|
231
231
|
}
|
|
232
|
-
|
|
232
|
+
// Units point at the STABLE LAUNCHER, never at an install path: the npm
|
|
233
|
+
// install dir is regenerated on every update (orphaning any unit that
|
|
234
|
+
// points into it), and under npx our own __dirname is an ephemeral cache.
|
|
235
|
+
// The launcher lives in the state dir and finds the install at runtime.
|
|
236
|
+
const { WATCHDOG_LAUNCHER } = await import(resolve(__dirname, "_shared.mjs"));
|
|
237
|
+
const scriptPath = WATCHDOG_LAUNCHER;
|
|
238
|
+
const refreshLauncher = () => {
|
|
239
|
+
mkdirSync(dirname(WATCHDOG_LAUNCHER), { recursive: true });
|
|
240
|
+
writeFileSync(WATCHDOG_LAUNCHER, wds.renderWatchdogLauncher());
|
|
241
|
+
};
|
|
233
242
|
const scanDir =
|
|
234
243
|
platform === "darwin"
|
|
235
244
|
? join(homedir(), "Library", "LaunchAgents")
|
|
@@ -257,19 +266,37 @@ async function watchdogStep() {
|
|
|
257
266
|
/* scan dir missing — treated as absent */
|
|
258
267
|
}
|
|
259
268
|
const state = wds.classifyWatchdogUnit(found?.target, existsSync);
|
|
260
|
-
if (state === "ok") {
|
|
269
|
+
if (state === "ok" && !wds.isFragileWatchdogTarget(found.target)) {
|
|
270
|
+
if (found.target === WATCHDOG_LAUNCHER && !DRY_RUN) refreshLauncher();
|
|
261
271
|
console.log(` ✅ already scheduled and healthy → ${found.target}`);
|
|
262
272
|
return;
|
|
263
273
|
}
|
|
274
|
+
if (state === "ok") {
|
|
275
|
+
// Exists today, but points INTO the npm install — orphans on next update.
|
|
276
|
+
console.log(
|
|
277
|
+
` ⚠ ${found.file}\n points INTO the npm install dir:\n ${found.target}\n That directory is regenerated on every update — the unit will orphan.`,
|
|
278
|
+
);
|
|
279
|
+
if (DRY_RUN) {
|
|
280
|
+
console.log(` dry-run: would move it to the stable launcher ${scriptPath}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!(await yes(" Move it to the stable launcher (survives every update)?"))) return;
|
|
284
|
+
refreshLauncher();
|
|
285
|
+
writeFileSync(found.file, found.text.split(found.target).join(scriptPath));
|
|
286
|
+
reloadWatchdogUnit(platform, found.file);
|
|
287
|
+
console.log(` ✅ now → ${scriptPath}`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
264
290
|
if (state === "orphaned") {
|
|
265
291
|
console.log(
|
|
266
292
|
` ⚠ ${found.file}\n points at a MISSING script: ${found.target}\n (an old install dir — the watchdog has been failing silently)`,
|
|
267
293
|
);
|
|
268
294
|
if (DRY_RUN) {
|
|
269
|
-
console.log(` dry-run: would repoint it at ${scriptPath}`);
|
|
295
|
+
console.log(` dry-run: would repoint it at the stable launcher ${scriptPath}`);
|
|
270
296
|
return;
|
|
271
297
|
}
|
|
272
|
-
if (!(await yes(" Repoint it at
|
|
298
|
+
if (!(await yes(" Repoint it at the stable launcher (survives every update)?"))) return;
|
|
299
|
+
refreshLauncher();
|
|
273
300
|
writeFileSync(found.file, found.text.split(found.target).join(scriptPath));
|
|
274
301
|
reloadWatchdogUnit(platform, found.file);
|
|
275
302
|
console.log(` ✅ repointed → ${scriptPath}`);
|
|
@@ -280,6 +307,7 @@ async function watchdogStep() {
|
|
|
280
307
|
return;
|
|
281
308
|
}
|
|
282
309
|
if (!(await yes(" Not scheduled. Schedule it now (runs every 5 min)?"))) return;
|
|
310
|
+
refreshLauncher();
|
|
283
311
|
const unitFiles = wds.renderWatchdogUnit({ platform, nodePath: process.execPath, scriptPath });
|
|
284
312
|
for (const uf of unitFiles) {
|
|
285
313
|
const abs = join(homedir(), uf.path);
|