@botbuddy/cli 1.8.1 → 1.8.3

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/bin/pw.mjs ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ // BOT-1491: `pw` is the first-class short binary for the lock-gated Playwright
3
+ // lane driver. `bb-pw` remains a working alias (bin/bb-pw.mjs) and
4
+ // `botbuddy pw …` the subcommand — all three call the same runPw entrypoint.
5
+ import { runPw } from "../src/pw/run.mjs";
6
+
7
+ runPw(process.argv.slice(2)).then((code) => process.exit(code)).catch((error) => {
8
+ console.error(`pw: ${error?.message ?? error}`);
9
+ process.exit(1);
10
+ });
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "botbuddy": "./bin/botbuddy.mjs",
8
+ "pw": "./bin/pw.mjs",
8
9
  "bb-pw": "./bin/bb-pw.mjs"
9
10
  },
10
11
  "files": [
package/src/commands.mjs CHANGED
@@ -105,7 +105,8 @@ ${bold("AGENT WAITS")}
105
105
 
106
106
  ${bold("BROWSER LANES")}
107
107
  pw <lane> <verb> [args…] Drive a lock-gated Playwright lane
108
- pw --help Show bb-pw-compatible lane usage
108
+ (also the short ${cyan("pw")} binary, or ${cyan("bb-pw")} alias)
109
+ pw --help Show lane usage
109
110
 
110
111
  ${bold("OTHER")}
111
112
  locks -m [--host name] Reserve typed local resources, including Playwright MCP lanes
@@ -8,5 +8,5 @@ export function createProfileCoordinator({ profile, identity, fetchImpl = fetch
8
8
  const json = await response.json(); if (json.error) throw new Error(json.error.message || "BotBuddy lock verification failed");
9
9
  const text = json.result?.content?.find((item) => item.type === "text")?.text; try { return text ? JSON.parse(text) : {}; } catch { throw new Error("BotBuddy lock verification returned invalid JSON"); }
10
10
  }
11
- return { kind: "profile", async status({ host, slot }) { const result = await call("list_resources", { host, subtype: "playwright_lane" }); const list = Array.isArray(result) ? result : result.resources ?? []; const resource = list.find((item) => item.name === `playwright_lane:${host}:${slot}` || String(item.slot) === String(slot)); return resource ? { held: resource.status !== "free", heldBy: resource.owner_agent_id ?? null, heldByName: resource.agents?.name ?? null, host: resource.host ?? null, name: resource.name ?? null, slot: resource.slot ?? null } : { held: false, heldBy: null, heldByName: null, host: null, name: null, slot: null }; }, async emit(event) { await call("record_lane_event", event); }, agentId: identity.agentId };
11
+ return { kind: "profile", async status({ host, slot }) { const result = await call("list_resources", { host, subtype: "playwright_lane" }); const list = Array.isArray(result) ? result : result.resources ?? []; const resource = list.find((item) => item.name === `playwright_lane:${host}:${slot}` || String(item.slot) === String(slot)); /* BOT-1490: canonical_host is a TOP-LEVEL field the server echoes even on an empty page (not-held lane); owner_is_caller is per-row. Older servers send neither → null. */ const canonicalHost = (result && !Array.isArray(result) ? result.canonical_host : null) ?? null; return resource ? { held: resource.status !== "free", heldBy: resource.owner_agent_id ?? null, heldByName: resource.agents?.name ?? null, host: resource.host ?? null, name: resource.name ?? null, slot: resource.slot ?? null, canonicalHost, ownerIsCaller: resource.owner_is_caller ?? null } : { held: false, heldBy: null, heldByName: null, host: null, name: null, slot: null, canonicalHost, ownerIsCaller: null }; }, async emit(event) { await call("record_lane_event", event); }, agentId: identity.agentId };
12
12
  }
package/src/pw/run.mjs CHANGED
@@ -11,7 +11,7 @@ import { loadConfig, getConfig } from "../config.mjs";
11
11
  // server-side, so the lane name bb-pw builds/matches/prints is the one the lock
12
12
  // kernel actually stored ("jonos-mbp:8", not "Jonos-MBP.localdomain:8").
13
13
  const hostFor = (env) => canonicalizeHostString(env.PLAYWRIGHT_MCP_HOST || env.HOSTNAME || os.hostname());
14
- function help(out) { out.write("Usage: botbuddy pw [--profile <name>] [--session-id <id>] <lane> <verb> [args…]\n\nAlias: bb-pw <lane> <verb> [args…]\n\n--session-id <id> accept a lane held by this arming-session agent id (from\n register_agent); defaults to $BOTBUDDY_SESSION_ID, then the\n id saved by `botbuddy register`.\n"); }
14
+ function help(out) { out.write("Usage: pw [--profile <name>] [--session-id <id>] <lane> <verb> [args…]\n\nAliases: bb-pw <lane> <verb> [args…] · botbuddy pw <lane> <verb> [args…]\n (all three drive the same lock-gated Playwright lane)\n\n--session-id <id> accept a lane held by this arming-session agent id (from\n register_agent); defaults to $BOTBUDDY_SESSION_ID, then the\n id saved by `botbuddy register`.\n"); }
15
15
  function redact(value, secretValues = []) { return secretValues.reduce((text, secret) => secret ? text.split(secret).join("[redacted]") : text, String(value ?? "")); }
16
16
  // BOT-1488: the register_agent identity for this machine, persisted by
17
17
  // `botbuddy register` into ~/.botbuddy/config.json. This is the SESSION agent
@@ -43,16 +43,24 @@ async function gate({ env, host, lane, deps }) {
43
43
  try {
44
44
  const status = await coordinator.status({ host, slot: lane });
45
45
  // The server's alias map can collapse the locally-normalized host further
46
- // (jonos-macbook-pro -> jonos-mbp). When the lock service returned the resource,
47
- // prefer its canonical host + name so messages/telemetry match what it stored,
48
- // rather than the string bb-pw built — the ticket's "match by the canonical host
49
- // field the server returns" path, without replicating the alias map here.
50
- const canonicalHost = status.host ?? host;
51
- const foundLaneName = status.name ?? laneName;
52
- if (status.held && selfAgentIds.has(status.heldBy)) return { allowed: true, coordinator, canonicalHost };
46
+ // (jonos-macbook-pro -> jonos-mbp). BOT-1490: the lock service now echoes that
47
+ // canonical host as a top-level field EVEN on a not-held lane (empty page), so
48
+ // prefer it for every lane name/message; fall back to the resource host
49
+ // (BOT-1488, only present when a resource was returned) then the local string —
50
+ // without replicating the alias map here.
51
+ const canonicalHost = status.canonicalHost ?? status.host ?? host;
52
+ const foundLaneName = status.name ?? `playwright_lane:${canonicalHost}:${lane}`;
53
+ // Allow when a local id matches the holder (BOT-1488: profile / --session-id /
54
+ // $BOTBUDDY_SESSION_ID / ~/.botbuddy/config.json) OR the lock service attributes
55
+ // the holder to the caller's owner (BOT-1490 owner_is_caller — server-authoritative,
56
+ // closing the shared-hostname gap without trusting a local file). The local paths
57
+ // stay as fallbacks for older servers that do not send owner_is_caller.
58
+ if (status.held && (selfAgentIds.has(status.heldBy) || status.ownerIsCaller === true)) return { allowed: true, coordinator, canonicalHost };
53
59
  // Distinguish an expired/free lane from one another operator holds so the
54
- // operator knows whether to re-acquire vs. wait for a handover (BOT-660).
55
- if (!status.held) return { allowed: false, message: `bb-pw: lane lock ${laneName} is not held (caller ${identity.agentId}). Acquire it via BotBuddy first, or set BB_PW_NO_LOCK=1 for local-only work.` };
60
+ // operator knows whether to re-acquire vs. wait for a handover (BOT-660). Name it
61
+ // by the server's canonical host so a not-held message matches the dashboard even
62
+ // when local normalization cannot reach the alias-collapsed key (BOT-1490 AC-5).
63
+ if (!status.held) return { allowed: false, message: `bb-pw: lane lock ${foundLaneName} is not held (caller ${identity.agentId}). Acquire it via BotBuddy first, or set BB_PW_NO_LOCK=1 for local-only work.` };
56
64
  const holder = status.heldBy ?? "unknown";
57
65
  const holderName = status.heldByName ? ` (${status.heldByName})` : "";
58
66
  return { allowed: false, message: `bb-pw: lane lock ${foundLaneName} is held by ${holder}${holderName}, not you (caller ${identity.agentId}). Acquire it first or set BB_PW_NO_LOCK=1 for local-only work.` };