@drakon-systems/multi-clawd 1.5.1 → 1.5.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/README.md CHANGED
@@ -188,13 +188,18 @@ 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/master/SETUP-AGENT.md
191
+ > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.5.2/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
 
195
195
  The guide has the guardrails built in (config backup, merge-don't-overwrite,
196
196
  never print tokens, ask before touching routing).
197
197
 
198
+ That URL is pinned to a release tag on purpose: what your agent reads — and
199
+ therefore executes — is fixed at a version you chose, not whatever `master`
200
+ happens to say today. Swap the tag if you want a different release, but
201
+ prefer a tag over a branch.
202
+
198
203
  **Requirements:** OpenClaw ≥ 2026.6, the `claude` CLI on `PATH`, and a
199
204
  second Claude subscription you own.
200
205
 
@@ -19,6 +19,14 @@ export function buildAccountChildEnv(account, token, stateFile) {
19
19
  }
20
20
  return env;
21
21
  }
22
+ export function tokenFileModeWarning(path, mode) {
23
+ const perms = mode & 0o777;
24
+ if ((perms & 0o077) === 0)
25
+ return undefined;
26
+ return (`token file ${path} is mode ${perms.toString(8).padStart(3, "0")} — readable beyond your ` +
27
+ `user account. Anyone with a login on this machine can take the Claude credential. ` +
28
+ `Fix: chmod 600 ${path}`);
29
+ }
22
30
  export function validateAccountTokenSources(account) {
23
31
  const sources = [];
24
32
  if (account.native)
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
2
2
  import { resolvePluginConfigObject, resolveLivePluginConfigObject, } from "openclaw/plugin-sdk/plugin-config-runtime";
3
3
  import { CLI_FRESH_WATCHDOG_DEFAULTS, CLI_RESUME_WATCHDOG_DEFAULTS, } from "openclaw/plugin-sdk/cli-backend";
4
4
  import { homedir } from "node:os";
5
- import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
+ import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { MODEL_ALIASES, buildCatalogEntries, canonicalModelId, isModernClaudeModelId, resolveModelSpec, } from "./models.js";
@@ -14,7 +14,7 @@ import { decideStickySelection } from "./sticky.js";
14
14
  import { createTokenRefResolver, isSecretRefShape, } from "./token-resolution.js";
15
15
  import { resolveSecretRefValues } from "openclaw/plugin-sdk/secret-ref-runtime";
16
16
  import { addAlert, clearAlert, pendingAlertText } from "./alerts.js";
17
- import { buildAccountChildEnv, validateAccountTokenSources } from "./account-env.js";
17
+ import { buildAccountChildEnv, tokenFileModeWarning, validateAccountTokenSources, } from "./account-env.js";
18
18
  import { checkAccountCredential, createRefProbeTracker, } from "./login-health.js";
19
19
  import { execFileSync } from "node:child_process";
20
20
  const BASE_ARGS = [
@@ -164,11 +164,26 @@ function startLoginHealthProbe(accounts, logger) {
164
164
  loginProbeTimer = setInterval(() => void probe().catch(() => { }), LOGIN_PROBE_INTERVAL_MS);
165
165
  loginProbeTimer.unref?.();
166
166
  }
167
+ const warnedTokenFileModes = new Set();
168
+ function warnIfTokenFileExposed(path) {
169
+ if (warnedTokenFileModes.has(path))
170
+ return;
171
+ warnedTokenFileModes.add(path);
172
+ try {
173
+ const warning = tokenFileModeWarning(path, statSync(path).mode);
174
+ if (warning)
175
+ console.warn(`[multi-clawd] ${warning}`);
176
+ }
177
+ catch {
178
+ }
179
+ }
167
180
  function peekToken(account) {
168
181
  if (account.native)
169
182
  return undefined;
170
183
  if (account.oauthTokenFile) {
171
- return readFileSync(expandHome(account.oauthTokenFile), "utf8").trim();
184
+ const path = expandHome(account.oauthTokenFile);
185
+ warnIfTokenFileExposed(path);
186
+ return readFileSync(path, "utf8").trim();
172
187
  }
173
188
  if (isSecretRefShape(account.oauthTokenRef)) {
174
189
  return activeTokenResolver?.peek(account.oauthTokenRef);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "multi-clawd",
3
3
  "name": "multi-clawd",
4
- "version": "1.5.1",
4
+ "version": "1.5.2",
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.5.1",
3
+ "version": "1.5.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",
@@ -57,7 +57,8 @@
57
57
  "doctor": "node scripts/doctor.mjs",
58
58
  "setup": "node scripts/setup.mjs",
59
59
  "sync-manifest": "node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('openclaw.plugin.json','utf8'));m.version=process.env.npm_package_version||JSON.parse(fs.readFileSync('package.json','utf8')).version;fs.writeFileSync('openclaw.plugin.json',JSON.stringify(m,null,2)+'\\n')\"",
60
- "version": "npm run sync-manifest && git add openclaw.plugin.json"
60
+ "sync-readme-tag": "node scripts/sync-readme-tag.mjs",
61
+ "version": "npm run sync-manifest && npm run sync-readme-tag && git add openclaw.plugin.json README.md"
61
62
  },
62
63
  "peerDependencies": {
63
64
  "openclaw": ">=2026.6"
package/scripts/cli.mjs CHANGED
@@ -185,11 +185,11 @@ async function healWatchdogUnit() {
185
185
  refreshLauncher();
186
186
  writeFileSync(file, text.split(target).join(WATCHDOG_LAUNCHER));
187
187
  if (d.endsWith("LaunchAgents")) {
188
- try {
189
- execFileSync("/bin/sh", ["-c", `launchctl unload "${file}" 2>/dev/null; launchctl load "${file}" 2>&1 || true`]);
190
- } catch {
191
- /* manual load needed */
192
- }
188
+ // Direct spawns, not a `sh -c` string: no quoting to get wrong and
189
+ // no shell for a path to break out of. Both calls are best-effort —
190
+ // unload fails harmlessly when nothing is loaded yet.
191
+ spawnSync("launchctl", ["unload", file], { stdio: "ignore" });
192
+ spawnSync("launchctl", ["load", file], { stdio: "ignore" });
193
193
  } else {
194
194
  try {
195
195
  execFileSync("systemctl", ["--user", "daemon-reload"]);
package/scripts/setup.mjs CHANGED
@@ -26,7 +26,7 @@ import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync, readd
26
26
  import { homedir } from "node:os";
27
27
  import { join, dirname, resolve } from "node:path";
28
28
  import { fileURLToPath } from "node:url";
29
- import { execFileSync } from "node:child_process";
29
+ import { execFileSync, spawnSync } from "node:child_process";
30
30
  import readline from "node:readline/promises";
31
31
 
32
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -369,12 +369,16 @@ function reloadWatchdogUnit(platform, file) {
369
369
  /* was not loaded */
370
370
  }
371
371
  try {
372
- // launchctl can print "Load failed" to stderr and still exit 0 merge
373
- // the streams and check the text, not just the exit code.
374
- const out = execFileSync("/bin/sh", ["-c", `launchctl load "${file}" 2>&1 || true`], {
375
- encoding: "utf8",
376
- });
377
- if (/load failed|bootstrap failed/i.test(out)) throw new Error(out.trim());
372
+ // launchctl can print "Load failed" to stderr and still exit 0, so we
373
+ // inspect BOTH streams rather than trusting the exit code. spawnSync
374
+ // (not a `/bin/sh -c` string) keeps this off the shell entirely: no
375
+ // quoting to get wrong, and nothing for a path to break out of.
376
+ const r = spawnSync("launchctl", ["load", file], { encoding: "utf8" });
377
+ if (r.error) throw r.error;
378
+ const out = `${r.stdout ?? ""}${r.stderr ?? ""}`;
379
+ if (r.status !== 0 || /load failed|bootstrap failed/i.test(out)) {
380
+ throw new Error(out.trim() || `launchctl exited ${r.status}`);
381
+ }
378
382
  console.log(" ✅ launchd agent (re)loaded");
379
383
  } catch {
380
384
  console.log(` ⚠ plist written but load failed — run: launchctl load ${file}`);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Keep the README's pinned SETUP-AGENT.md URL on the current release tag.
3
+ *
4
+ * The agent-install one-liner points people's assistants at a raw GitHub URL,
5
+ * and that URL is what they will actually EXECUTE. Pinning it to a tag makes
6
+ * those instructions immutable per release — but a hand-maintained version
7
+ * number in prose drifts (the openclaw.plugin.json version did exactly that,
8
+ * twice), so the `version` npm hook rewrites it here instead.
9
+ */
10
+ import { readFileSync, writeFileSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
15
+ const readmePath = join(root, "README.md");
16
+
17
+ // npm sets npm_package_version during the version lifecycle; fall back to the
18
+ // file for direct invocations.
19
+ const version =
20
+ process.env.npm_package_version ||
21
+ JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
22
+
23
+ const PATTERN =
24
+ /(raw\.githubusercontent\.com\/Drakon-Systems-Ltd\/multi-clawd\/)(?:master|v[\d.]+)(\/SETUP-AGENT\.md)/g;
25
+
26
+ const readme = readFileSync(readmePath, "utf8");
27
+ const updated = readme.replace(PATTERN, `$1v${version}$2`);
28
+
29
+ if (updated === readme) {
30
+ console.log(`sync-readme-tag: already pinned to v${version}`);
31
+ } else {
32
+ writeFileSync(readmePath, updated);
33
+ console.log(`sync-readme-tag: pinned SETUP-AGENT.md URL to v${version}`);
34
+ }