starbridge 0.1.0 → 0.1.1

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.
@@ -4056,7 +4056,7 @@ var require_png = __commonJS(function(exports) {
4056
4056
  });
4057
4057
 
4058
4058
  // src/cli.ts
4059
- import { readFileSync as readFileSync13 } from "node:fs";
4059
+ import { readFileSync as readFileSync15 } from "node:fs";
4060
4060
  import { parseArgs } from "node:util";
4061
4061
 
4062
4062
  // ../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/util.js
@@ -9651,7 +9651,7 @@ function duration2(params) {
9651
9651
  }
9652
9652
  // ../packages/protocol/src/client.ts
9653
9653
  var CLIENT_HEADER = "starbridge-client";
9654
- var CLIENT_NAMES = ["cli", "android", "web", "mod"];
9654
+ var CLIENT_NAMES = ["cli", "android", "web", "mod", "desktop"];
9655
9655
  function clientHeader(name, version) {
9656
9656
  return `${name}/${version}`;
9657
9657
  }
@@ -13901,30 +13901,65 @@ function activeMembers(dir, role) {
13901
13901
  return [...dir.members.values()].filter((e) => e.active && e.member.role === role).map((e) => e.member);
13902
13902
  }
13903
13903
  // ../packages/protocol/src/heads.ts
13904
+ function revocationOf(entries, id) {
13905
+ for (const raw of entries) {
13906
+ const env = SignedEnvelope.safeParse(raw);
13907
+ if (!env.success)
13908
+ continue;
13909
+ const body = JSON.parse(env.data.body);
13910
+ if (body.op === "revoke" && body.id === id)
13911
+ return { id, by: env.data.signer, at: body.at ?? "" };
13912
+ }
13913
+ return;
13914
+ }
13915
+ function removed(dir, entries, id) {
13916
+ if (dir.members.get(id)?.active !== false)
13917
+ return false;
13918
+ let revokedAt = -1;
13919
+ let recoveredAt = -1;
13920
+ entries.forEach((raw, i) => {
13921
+ const env = SignedEnvelope.safeParse(raw);
13922
+ if (!env.success)
13923
+ return;
13924
+ const body = JSON.parse(env.data.body);
13925
+ if (body.op === "revoke" && body.id === id)
13926
+ revokedAt = i;
13927
+ if (body.op === "recover")
13928
+ recoveredAt = i;
13929
+ });
13930
+ return recoveredAt > revokedAt;
13931
+ }
13904
13932
  function noteHead(heads, signer, head, entries, dir) {
13905
13933
  if (!head)
13906
13934
  return false;
13907
13935
  const by = head.by && head.by !== signer ? head.by : undefined;
13936
+ if (by && dir?.members.get(by)?.active === false)
13937
+ return false;
13908
13938
  const key = !by ? signer : dir && !dir.members.has(by) ? `${signer}/?` : `${signer}/${by}`;
13909
13939
  const known = heads[key];
13910
- const dropped = !!known?.by && dir?.members.get(known.by)?.active === false;
13940
+ const dropped = !!known?.by && !!dir && removed(dir, entries, known.by);
13911
13941
  const replace = !known || dropped || head.length > known.length || holdsHead(entries, known) && !holdsHead(entries, head);
13912
13942
  if (replace)
13913
13943
  heads[key] = head;
13914
13944
  return replace;
13915
13945
  }
13916
13946
  function withheldBy(heads, dir, entries) {
13947
+ let fork;
13917
13948
  for (const [key, head] of Object.entries(heads)) {
13918
13949
  const id = key.split("/")[0];
13919
13950
  const by = head.by && head.by !== id ? head.by : undefined;
13920
- if (!dir.members.get(id)?.active)
13951
+ if (!dir.members.has(id) || removed(dir, entries, id))
13921
13952
  continue;
13922
- if (by && dir.members.get(by)?.active === false)
13953
+ if (by && removed(dir, entries, by))
13923
13954
  continue;
13924
- if (!holdsHead(entries, head))
13955
+ if (holdsHead(entries, head))
13956
+ continue;
13957
+ const revoked = (dir.members.get(id)?.active ? undefined : revocationOf(entries, id)) ?? (by && dir.members.get(by)?.active === false ? revocationOf(entries, by) : undefined);
13958
+ if (!revoked)
13925
13959
  return { id, ...by ? { by } : {}, head };
13960
+ fork ??= { id, ...by ? { by } : {}, head, revoked };
13926
13961
  }
13927
- return;
13962
+ return fork;
13928
13963
  }
13929
13964
  // ../packages/protocol/src/images.ts
13930
13965
  var label = concat(utf8("starbridge/v1/image"), new Uint8Array([0]));
@@ -13966,7 +14001,19 @@ function formatPairingCode(code) {
13966
14001
  return (code.rendezvous + code.secret).match(/.{4}/g)?.join("-") ?? "";
13967
14002
  }
13968
14003
  function pairingLink(server, code) {
13969
- return `${server.replace(/\/+$/, "")}/pair#${formatPairingCode(code)}`;
14004
+ let end = server.length;
14005
+ while (end > 0 && server[end - 1] === "/")
14006
+ end--;
14007
+ return `${server.slice(0, end)}/pair#${formatPairingCode(code)}`;
14008
+ }
14009
+ function appPairingLink(server, code, checkKey) {
14010
+ let end = server.length;
14011
+ while (end > 0 && server[end - 1] === "/")
14012
+ end--;
14013
+ return `starbridge://pair?server=${encodeURIComponent(server.slice(0, end))}&k=${checkKey}#${formatPairingCode(code)}`;
14014
+ }
14015
+ function newCheckKey() {
14016
+ return encodeCrockford(libsodium_wrappers_default.randombytes_buf(10));
13970
14017
  }
13971
14018
  function newClaimSecret() {
13972
14019
  return toB64(libsodium_wrappers_default.randombytes_buf(32));
@@ -13993,7 +14040,8 @@ var PairingApprovalBody = object({
13993
14040
  account: Id,
13994
14041
  length: number2().int().positive(),
13995
14042
  head: B64,
13996
- approver: Id
14043
+ approver: Id,
14044
+ check: B64.optional()
13997
14045
  });
13998
14046
  var PairingMessage = object({ body: string2(), mac: B64 });
13999
14047
  function macMessage(kind, body) {
@@ -14035,6 +14083,33 @@ function checkJoined(dir, me) {
14035
14083
  if (!entry?.active || entry.member.role !== me.role || entry.member.boxPk !== me.boxPk || entry.member.signPk !== me.signPk)
14036
14084
  throw new ProtocolError("unknown-member", "the directory does not hold my keys");
14037
14085
  }
14086
+ function checkInput(entries, id) {
14087
+ const nul = new Uint8Array([0]);
14088
+ for (const e of entries) {
14089
+ const { body, sig } = e ?? {};
14090
+ if (typeof body !== "string" || typeof sig !== "string")
14091
+ continue;
14092
+ const parsed = JSON.parse(body);
14093
+ if (parsed.op !== "add" || parsed.member?.id !== id)
14094
+ continue;
14095
+ return concat(utf8("starbridge/v1/check"), nul, utf8(body), nul, utf8(sig));
14096
+ }
14097
+ throw new ProtocolError("unknown-member", `no add entry for ${id}`);
14098
+ }
14099
+ function checkCode(entries, id) {
14100
+ const hash = libsodium_wrappers_default.crypto_generichash(32, checkInput(entries, id), null);
14101
+ return encodeCrockford(hash.subarray(0, 10)).match(/.{4}/g).join("-");
14102
+ }
14103
+ function checkKeyBytes(key) {
14104
+ return libsodium_wrappers_default.crypto_generichash(32, concat(utf8("starbridge/v1/check-key"), new Uint8Array([0]), utf8(key)), null);
14105
+ }
14106
+ function verifyCheckProof(entries, id, key, proof) {
14107
+ try {
14108
+ return libsodium_wrappers_default.crypto_auth_verify(fromB64(proof), checkInput(entries, id), checkKeyBytes(key));
14109
+ } catch {
14110
+ return false;
14111
+ }
14112
+ }
14038
14113
 
14039
14114
  // ../packages/protocol/src/join.ts
14040
14115
  var JoinRequestBody = object({
@@ -14171,6 +14246,17 @@ function checkPermissionAnswer(asked, answer, device, now = Date.now()) {
14171
14246
  if (now > Date.parse(asked.expiresAt))
14172
14247
  throw new ProtocolError("expired");
14173
14248
  }
14249
+ // ../packages/protocol/src/presence.ts
14250
+ var PRESENCE_INPUT_MS = 60000;
14251
+ var PRESENCE_BEAT_MS = 30000;
14252
+ var Presence = object({ present: boolean2() });
14253
+ var PUSH_HOLD_MAX = 300;
14254
+ var AccountSettings = object({
14255
+ pushHold: number2().int().min(0).max(PUSH_HOLD_MAX)
14256
+ });
14257
+ function isPresent(screen) {
14258
+ return !screen.locked && screen.idleMs >= 0 && screen.idleMs < PRESENCE_INPUT_MS;
14259
+ }
14174
14260
  // src/agent/client.ts
14175
14261
  import { randomBytes as randomBytes2 } from "node:crypto";
14176
14262
  import { existsSync as existsSync2 } from "node:fs";
@@ -14273,7 +14359,7 @@ function processAlive(pid) {
14273
14359
  // package.json
14274
14360
  var package_default = {
14275
14361
  name: "starbridge",
14276
- version: "0.1.0",
14362
+ version: "0.1.1",
14277
14363
  description: "Post decisions to your Starbridge devices and upload quota windows from CodexBar",
14278
14364
  license: "MIT",
14279
14365
  repository: {
@@ -14344,8 +14430,9 @@ function configDir(env) {
14344
14430
  }
14345
14431
  var PROMPTS_OPEN = "permissions-open";
14346
14432
  var CLI_PATH = "cli-path";
14433
+ var PICKER_MAX_MS = 86400000;
14347
14434
  function promptsMark(s, now = Date.now()) {
14348
- const open = Object.values(s.permissions ?? {}).some((p) => !p.settled && Date.parse(p.permission.expiresAt) > now);
14435
+ const open = Object.values(s.permissions ?? {}).some((p) => !p.settled && Date.parse(p.permission.expiresAt) > now) || Object.entries(s.asked).some(([id, a]) => a.picker && !a.settled && !s.answers[id] && Date.parse(a.askedAt) > now - PICKER_MAX_MS);
14349
14436
  return open ? "open" : "";
14350
14437
  }
14351
14438
  var STATE_VERSION = 1;
@@ -14800,6 +14887,9 @@ class Api {
14800
14887
  const r = await this.call("GET", "/quota/senders", { signal: AbortSignal.timeout(15000) });
14801
14888
  return r.json.senders;
14802
14889
  }
14890
+ async presence(present, signal) {
14891
+ await this.call("PUT", "/presence", { body: { present }, ...signal ? { signal } : {} });
14892
+ }
14803
14893
  async postItem(item, signal) {
14804
14894
  await this.call("POST", "/items", { body: item, ...signal ? { signal } : {} });
14805
14895
  }
@@ -14865,20 +14955,12 @@ function signedHead(dir) {
14865
14955
  return { length: dir.length, head: dir.head };
14866
14956
  }
14867
14957
  function withheld(st, dir, entries) {
14868
- const heads = st.heads ?? {};
14869
- const held = withheldBy(heads, dir, entries);
14870
- if (held)
14871
- return `the server is holding back directory entries ${held.id} has seen (${held.head.length}, this machine has ${dir.length})`;
14872
- for (const [id, head] of Object.entries(heads))
14873
- if (!holdsHead(entries, head) && revokes(entries, id))
14874
- return `${id} signed a chain this machine does not hold (${head.length}), and this machine's chain revokes it: the server may be serving a fork. If you revoked ${id} because it was compromised, run \`starbridge pair --force\``;
14875
- return;
14876
- }
14877
- function revokes(entries, id) {
14878
- return entries.some((e) => {
14879
- const body = JSON.parse(e.body);
14880
- return body.op === "revoke" && body.id === id;
14881
- });
14958
+ const held = withheldBy(st.heads ?? {}, dir, entries);
14959
+ if (!held)
14960
+ return;
14961
+ if (held.revoked)
14962
+ return `${held.id} signed a chain this machine does not hold (${held.head.length}), and this machine's chain revokes it: the server may be serving a fork. If you revoked ${held.id} because it was compromised, run \`starbridge pair --force\``;
14963
+ return `the server is holding back directory entries ${held.id} has seen (${held.head.length}, this machine has ${dir.length})`;
14882
14964
  }
14883
14965
  function devices(ctx, dir) {
14884
14966
  const behind = withheld(ctx.store.state(), dir, ctx.store.directory().slice(0, dir.length));
@@ -15621,6 +15703,7 @@ function prune(st, now) {
15621
15703
  if (Date.parse(p.permission.expiresAt) + KEEP_MS < now)
15622
15704
  delete st.permissions?.[id];
15623
15705
  }
15706
+ var LATE_REPORT_MS = 5000;
15624
15707
  async function postPermission(ctx, s, hook, opts, signal) {
15625
15708
  const dir = await refreshDirectory(ctx, s, signal);
15626
15709
  const to = devices(ctx, dir);
@@ -15648,7 +15731,9 @@ async function postPermission(ctx, s, hook, opts, signal) {
15648
15731
  await s.api.postItem(item, signal);
15649
15732
  } catch (e) {
15650
15733
  const keyboard = signal?.aborted && signal.reason?.name !== "TimeoutError";
15651
- markSettled(ctx, permission.id, keyboard ? "keyboard" : "timeout");
15734
+ const how = markSettled(ctx, permission.id, keyboard ? "keyboard" : "timeout");
15735
+ if (how)
15736
+ await postSettled(ctx, s, permission.id, how, AbortSignal.timeout(LATE_REPORT_MS)).catch(() => {});
15652
15737
  throw e;
15653
15738
  }
15654
15739
  return permission.id;
@@ -15871,7 +15956,8 @@ async function postDecision(ctx, s, input) {
15871
15956
  ...decision.done ? { done: true } : {},
15872
15957
  ...input.codex && decision.source.session ? { codex: input.codex } : {},
15873
15958
  ...input.extensionAnswers && decision.source.session ? { extensionAnswers: true } : {},
15874
- ...input.held ? { held: true } : {}
15959
+ ...input.held ? { held: true } : {},
15960
+ ...input.picker ? { picker: input.picker } : {}
15875
15961
  };
15876
15962
  });
15877
15963
  if (input.waiting)
@@ -16993,6 +17079,11 @@ starbridge run --title 'Mac e2e' --reason 'takes over your screen and keyboard'
16993
17079
 
16994
17080
  Wrap the whole command, chained or not. \`--reason\` is required: what it
16995
17081
  blocks, or the rule it matches. Output and exit code are the command's own.
17082
+
17083
+ In Codex, ask to run \`starbridge run\` outside the sandbox on the first call
17084
+ (escalated permissions, justified by the network the report needs). In the
17085
+ sandbox the command runs but no device hears of it, and it runs again once
17086
+ the escalation is approved.
16996
17087
  `;
16997
17088
 
16998
17089
  // src/setup/marker.ts
@@ -17027,7 +17118,7 @@ import { configDir } from "./poller.ts";
17027
17118
 
17028
17119
  /** The agent API revision this mod speaks, sent in the \`starbridge-api\` header. */
17029
17120
  export const API = 1;
17030
- export const VERSION = "0.1.0";
17121
+ export const VERSION = "0.1.1";
17031
17122
  const CLIENT = \`starbridge-mod/\${VERSION}\`;
17032
17123
 
17033
17124
  /** Event types this mod submits; others are skipped and left unconfirmed, as the API asks. */
@@ -17817,6 +17908,19 @@ var switch_default = `/**
17817
17908
  import type { AgentLoop } from "./agent.ts";
17818
17909
  import type { Poller } from "./poller.ts";
17819
17910
 
17911
+ /** Entry points of a run that ends with its last turn: \`claude -p\` and Agent SDK scripts. */
17912
+ const ONE_SHOT = new Set(["sdk-cli", "sdk-ts", "sdk-py"]);
17913
+
17914
+ /**
17915
+ * Whether a session gets an answer loop: one under the REPL, or one a host such as Claude
17916
+ * desktop's Code tab or an IDE keeps open for a person (#863). The engine calls those not
17917
+ * interactive, as it does a \`claude -p\` run, so the host's \`CLAUDE_CODE_ENTRYPOINT\` tells them
17918
+ * apart; a run with none set is \`claude -p\`'s own.
17919
+ */
17920
+ export function wantsLoop(isInteractive: boolean, entrypoint: string | undefined): boolean {
17921
+ return isInteractive || (!!entrypoint && !ONE_SHOT.has(entrypoint));
17922
+ }
17923
+
17820
17924
  export interface Paths {
17821
17925
  /** True when an agent answers \`GET /v1/status\` with a 2xx. */
17822
17926
  agentUp(): Promise<boolean>;
@@ -18524,6 +18628,7 @@ function run(sys, cmd, args, opts = {}) {
18524
18628
  const child = spawn3(start.file, start.args, {
18525
18629
  windowsVerbatimArguments: start.windowsVerbatimArguments,
18526
18630
  env: { ...sys.ctx.env, ...opts.env },
18631
+ ...opts.cwd ? { cwd: opts.cwd } : {},
18527
18632
  stdio: [
18528
18633
  opts.input === undefined ? "ignore" : "pipe",
18529
18634
  opts.inherit ? "inherit" : "pipe",
@@ -18649,6 +18754,7 @@ var PI_PACKAGE = piSource(VERSION);
18649
18754
  function hasPi(sys) {
18650
18755
  return which(sys.ctx.env, "pi") !== undefined;
18651
18756
  }
18757
+ var STARBRIDGE_PI = /github\.com[/:]T0mSIlver\/starbridge(?:\.git)?\/?(?:@.*)?$/i;
18652
18758
  function piPackage(sys) {
18653
18759
  const dir = sys.ctx.env.PI_CODING_AGENT_DIR || join7(sys.home, ".pi", "agent");
18654
18760
  let packages;
@@ -18661,7 +18767,7 @@ function piPackage(sys) {
18661
18767
  return;
18662
18768
  for (const p of packages) {
18663
18769
  const source = typeof p === "string" ? p : p?.source;
18664
- if (typeof source === "string" && /github\.com[/:]T0mSIlver\/starbridge\b/i.test(source))
18770
+ if (typeof source === "string" && STARBRIDGE_PI.test(source))
18665
18771
  return source;
18666
18772
  }
18667
18773
  return;
@@ -19561,16 +19667,234 @@ class Permissions {
19561
19667
  }
19562
19668
  }
19563
19669
 
19670
+ // src/screen.ts
19671
+ import { execFile, spawn as spawn4 } from "node:child_process";
19672
+ import { createInterface as createInterface2 } from "node:readline";
19673
+ var TIMEOUT_MS = 5000;
19674
+ var exec = (cmd, args) => new Promise((resolve) => {
19675
+ execFile(cmd, args, { timeout: TIMEOUT_MS, windowsHide: true }, (err, stdout) => resolve(err ? undefined : String(stdout)));
19676
+ });
19677
+ function macIdleMs(ioreg) {
19678
+ const m = /"HIDIdleTime"\s*=\s*(\d+)/.exec(ioreg);
19679
+ return m ? Number(m[1]) / 1e6 : undefined;
19680
+ }
19681
+ function macLocked(ioreg) {
19682
+ return /"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(ioreg);
19683
+ }
19684
+ async function readMac(run) {
19685
+ const [hid, root] = await Promise.all([
19686
+ run("ioreg", ["-c", "IOHIDSystem", "-d", "4", "-r", "-k", "HIDIdleTime"]),
19687
+ run("ioreg", ["-n", "Root", "-d1"])
19688
+ ]);
19689
+ const idleMs = hid === undefined ? undefined : macIdleMs(hid);
19690
+ if (idleMs === undefined || root === undefined)
19691
+ return;
19692
+ return { locked: macLocked(root), idleMs };
19693
+ }
19694
+ function properties(text) {
19695
+ const out = {};
19696
+ for (const line of text.split(`
19697
+ `)) {
19698
+ const i = line.indexOf("=");
19699
+ if (i > 0)
19700
+ out[line.slice(0, i)] = line.slice(i + 1).trim();
19701
+ }
19702
+ return out;
19703
+ }
19704
+ function graphical(sessions) {
19705
+ return sessions.map(properties).filter((p) => p.Active === "yes" && (p.Type === "x11" || p.Type === "wayland") && p.Class === "user").map((p) => ({ locked: p.LockedHint === "yes" }));
19706
+ }
19707
+ function linuxIdleMs(text) {
19708
+ const m = /^\(?(?:uint64\s+)?(\d+),?\)?\s*$/.exec(text.trim());
19709
+ return m ? Number(m[1]) : undefined;
19710
+ }
19711
+ async function readLinux(run) {
19712
+ const list = await run("loginctl", ["list-sessions", "--no-legend", "--no-pager"]);
19713
+ const ids = (list ?? "").split(`
19714
+ `).map((l) => l.trim().split(/\s+/)[0]).filter((id) => !!id);
19715
+ const shown = await Promise.all(ids.map((id) => run("loginctl", [
19716
+ "show-session",
19717
+ id,
19718
+ "-p",
19719
+ "Type",
19720
+ "-p",
19721
+ "Active",
19722
+ "-p",
19723
+ "Class",
19724
+ "-p",
19725
+ "LockedHint"
19726
+ ])));
19727
+ const screens = graphical(shown.filter((s) => s !== undefined));
19728
+ if (screens.length === 0)
19729
+ return;
19730
+ const idle = await run("gdbus", [
19731
+ "call",
19732
+ "--session",
19733
+ "--dest",
19734
+ "org.gnome.Mutter.IdleMonitor",
19735
+ "--object-path",
19736
+ "/org/gnome/Mutter/IdleMonitor/Core",
19737
+ "--method",
19738
+ "org.gnome.Mutter.IdleMonitor.GetIdletime"
19739
+ ]) ?? await run("xprintidle", []);
19740
+ const idleMs = idle === undefined ? undefined : linuxIdleMs(idle);
19741
+ if (idleMs === undefined)
19742
+ return;
19743
+ return { locked: screens.every((s) => s.locked), idleMs };
19744
+ }
19745
+ var WINDOWS_SCRIPT = `
19746
+ Add-Type @'
19747
+ using System; using System.Runtime.InteropServices;
19748
+ public static class Idle {
19749
+ [StructLayout(LayoutKind.Sequential)] struct LASTINPUTINFO { public uint cbSize; public uint dwTime; }
19750
+ [DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
19751
+ public static long Ms() {
19752
+ var i = new LASTINPUTINFO(); i.cbSize = (uint)Marshal.SizeOf(i);
19753
+ if (!GetLastInputInfo(ref i)) return -1;
19754
+ return (long)unchecked((uint)Environment.TickCount - i.dwTime);
19755
+ }
19756
+ }
19757
+ '@
19758
+ $session = (Get-Process -Id $PID).SessionId
19759
+ while ($null -ne [Console]::In.ReadLine()) {
19760
+ $locked = @(Get-Process LogonUI -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $session }).Count -gt 0
19761
+ [Console]::Out.WriteLine("$([Idle]::Ms()) $locked")
19762
+ }
19763
+ `;
19764
+ function windowsScreen(line) {
19765
+ const m = /^(-?\d+) (True|False)$/.exec(line.trim());
19766
+ if (!m || Number(m[1]) < 0)
19767
+ return;
19768
+ return { idleMs: Number(m[1]), locked: m[2] === "True" };
19769
+ }
19770
+
19771
+ class WindowsReader {
19772
+ child;
19773
+ pending = [];
19774
+ read() {
19775
+ const child = this.start();
19776
+ return new Promise((resolve) => {
19777
+ const timer = setTimeout(() => done(undefined), TIMEOUT_MS);
19778
+ const done = (line) => {
19779
+ clearTimeout(timer);
19780
+ resolve(line === undefined ? undefined : windowsScreen(line));
19781
+ };
19782
+ this.pending.push(done);
19783
+ child.stdin?.write(`
19784
+ `);
19785
+ });
19786
+ }
19787
+ start() {
19788
+ if (this.child && this.child.exitCode === null)
19789
+ return this.child;
19790
+ const child = spawn4("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "-"], { stdio: ["pipe", "pipe", "ignore"], windowsHide: true });
19791
+ child.stdin?.write(`${WINDOWS_SCRIPT}
19792
+ `);
19793
+ createInterface2({ input: child.stdout }).on("line", (line) => this.pending.shift()?.(line));
19794
+ const gone = () => {
19795
+ if (this.child === child)
19796
+ this.child = undefined;
19797
+ for (const p of this.pending.splice(0))
19798
+ p(undefined);
19799
+ };
19800
+ child.on("exit", gone);
19801
+ child.on("error", gone);
19802
+ this.child = child;
19803
+ return child;
19804
+ }
19805
+ stop() {
19806
+ this.child?.kill();
19807
+ this.child = undefined;
19808
+ }
19809
+ }
19810
+ function screenReader(platform = process.platform, run = exec) {
19811
+ if (platform === "darwin")
19812
+ return { read: () => readMac(run), stop: () => {} };
19813
+ if (platform === "linux")
19814
+ return { read: () => readLinux(run), stop: () => {} };
19815
+ if (platform === "win32") {
19816
+ const reader = new WindowsReader;
19817
+ return { read: () => reader.read(), stop: () => reader.stop() };
19818
+ }
19819
+ return { read: async () => {
19820
+ return;
19821
+ }, stop: () => {} };
19822
+ }
19823
+
19824
+ // src/agent/presence.ts
19825
+ var READ_MS = 1e4;
19826
+ function presenceEnabled(hub) {
19827
+ return hub.ctx.store.agentConfig().presence?.enabled === true;
19828
+ }
19829
+
19830
+ class Presence2 {
19831
+ hub;
19832
+ reader;
19833
+ sent;
19834
+ lastError;
19835
+ constructor(hub, reader = screenReader()) {
19836
+ this.hub = hub;
19837
+ this.reader = reader;
19838
+ }
19839
+ status(into) {
19840
+ into.presence = {
19841
+ enabled: presenceEnabled(this.hub),
19842
+ present: this.sent?.present === true,
19843
+ ...this.lastError ? { lastError: this.lastError } : {}
19844
+ };
19845
+ }
19846
+ async send(present, signal) {
19847
+ const ctx = this.hub.ctx;
19848
+ if (!ctx.store.machine())
19849
+ return;
19850
+ try {
19851
+ await session(ctx).api.presence(present, signal ?? AbortSignal.timeout(1e4));
19852
+ this.sent = { present, at: Date.now() };
19853
+ this.lastError = undefined;
19854
+ } catch (e) {
19855
+ this.lastError = e.message;
19856
+ }
19857
+ }
19858
+ async tick(now = Date.now()) {
19859
+ const screen = presenceEnabled(this.hub) ? await this.reader.read() : undefined;
19860
+ const present = screen !== undefined && isPresent(screen);
19861
+ if (present) {
19862
+ if (!this.sent?.present || now - this.sent.at >= PRESENCE_BEAT_MS)
19863
+ await this.send(true);
19864
+ } else if (this.sent?.present)
19865
+ await this.send(false);
19866
+ }
19867
+ async run(signal) {
19868
+ while (!signal.aborted) {
19869
+ await this.tick();
19870
+ await pause(READ_MS, signal);
19871
+ }
19872
+ if (this.sent?.present)
19873
+ await this.send(false, AbortSignal.timeout(3000));
19874
+ this.reader.stop();
19875
+ }
19876
+ }
19877
+
19564
19878
  // src/quota.ts
19565
19879
  import { randomBytes as randomBytes6 } from "node:crypto";
19566
19880
 
19567
19881
  // src/codexbar.ts
19568
- import { spawn as spawn4 } from "node:child_process";
19882
+ import { spawn as spawn5 } from "node:child_process";
19569
19883
  var RUN_TIMEOUT_MS = 120000;
19570
19884
  function runCodexbar(bin, provider, timeoutMs = RUN_TIMEOUT_MS) {
19571
19885
  const args = ["usage", "--format", "json", ...provider ? ["--provider", provider] : []];
19572
19886
  return new Promise((resolve) => {
19573
- const child = spawn4(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
19887
+ let start;
19888
+ try {
19889
+ start = spawnable(bin, args);
19890
+ } catch (e) {
19891
+ return resolve({ code: null, stdout: "", stderr: e.message });
19892
+ }
19893
+ const child = spawn5(start.file, start.args, {
19894
+ windowsVerbatimArguments: start.windowsVerbatimArguments,
19895
+ windowsHide: true,
19896
+ stdio: ["ignore", "pipe", "pipe"]
19897
+ });
19574
19898
  let stdout = "";
19575
19899
  let stderr = "";
19576
19900
  child.stdout.on("data", (d) => {
@@ -19579,7 +19903,7 @@ function runCodexbar(bin, provider, timeoutMs = RUN_TIMEOUT_MS) {
19579
19903
  child.stderr.on("data", (d) => {
19580
19904
  stderr += d;
19581
19905
  });
19582
- const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
19906
+ const timer = setTimeout(() => killTree(child), timeoutMs);
19583
19907
  child.on("error", (e) => {
19584
19908
  clearTimeout(timer);
19585
19909
  resolve({ code: null, stdout, stderr: e.message });
@@ -19935,7 +20259,7 @@ class Quota {
19935
20259
  }
19936
20260
 
19937
20261
  // src/run.ts
19938
- import { spawn as spawn5 } from "node:child_process";
20262
+ import { spawn as spawn6 } from "node:child_process";
19939
20263
  import { randomBytes as randomBytes7 } from "node:crypto";
19940
20264
  import { constants as constants2 } from "node:os";
19941
20265
  var PROGRESS_MS = 1e4;
@@ -20039,6 +20363,8 @@ class Reporter {
20039
20363
  ctx;
20040
20364
  timing;
20041
20365
  progress;
20366
+ progressSent = false;
20367
+ dirty = false;
20042
20368
  exit;
20043
20369
  lastPost = Number.NEGATIVE_INFINITY;
20044
20370
  inflight;
@@ -20060,7 +20386,8 @@ class Reporter {
20060
20386
  if (JSON.stringify(next) === JSON.stringify(this.progress))
20061
20387
  return;
20062
20388
  this.progress = next;
20063
- this.schedule(this.timing.progressMs);
20389
+ this.dirty = true;
20390
+ this.schedule(this.progressGap());
20064
20391
  }
20065
20392
  async finish(code) {
20066
20393
  this.exit = { code, at: iso2(this.ctx.now()) };
@@ -20074,6 +20401,9 @@ class Reporter {
20074
20401
  await Promise.race([settled, new Promise((r) => t = setTimeout(r, FINAL_MS))]);
20075
20402
  clearTimeout(t);
20076
20403
  }
20404
+ progressGap() {
20405
+ return this.progressSent ? this.timing.progressMs : 0;
20406
+ }
20077
20407
  schedule(gap) {
20078
20408
  if (this.off || this.exit)
20079
20409
  return;
@@ -20091,11 +20421,14 @@ class Reporter {
20091
20421
  }
20092
20422
  const input = {
20093
20423
  ...this.base,
20094
- at: iso2(this.ctx.now()),
20424
+ at: this.ctx.now().toISOString(),
20095
20425
  ...this.progress ? { progress: this.progress } : {},
20096
20426
  ...this.exit ? { exit: this.exit } : {}
20097
20427
  };
20098
20428
  this.lastPost = this.ctx.now().getTime();
20429
+ if (this.progress)
20430
+ this.progressSent = true;
20431
+ this.dirty = false;
20099
20432
  this.inflight = this.post(input).catch((e) => {
20100
20433
  if (e instanceof UsageError && !(e instanceof WithheldError))
20101
20434
  this.off = true;
@@ -20108,7 +20441,7 @@ class Reporter {
20108
20441
  this.queued = false;
20109
20442
  this.send();
20110
20443
  } else
20111
- this.schedule(this.timing.heartbeatMs);
20444
+ this.schedule(this.dirty ? this.progressGap() : this.timing.heartbeatMs);
20112
20445
  });
20113
20446
  }
20114
20447
  }
@@ -20156,7 +20489,7 @@ async function runCommand(ctx, opts) {
20156
20489
  }, opts.post ?? makePoster(ctx), ctx);
20157
20490
  const stdout = opts.stdout ?? process.stdout;
20158
20491
  const stderr = opts.stderr ?? process.stderr;
20159
- const child = spawn5(start.file, start.args, {
20492
+ const child = spawn6(start.file, start.args, {
20160
20493
  stdio: ["inherit", "pipe", "pipe"],
20161
20494
  windowsVerbatimArguments: start.windowsVerbatimArguments
20162
20495
  });
@@ -20218,7 +20551,13 @@ function makeAgent(ctx, opts = {}) {
20218
20551
  const quota = { providers, intervalMs, ...codexbar ? { codexbar } : {} };
20219
20552
  const agent = new Agent2(ctx, socket, (hub) => {
20220
20553
  const q = new Quota(hub, quota);
20221
- return [new Decisions(hub, (why) => q.now(why)), q, new Permissions(hub), new Runs(hub)];
20554
+ return [
20555
+ new Decisions(hub, (why) => q.now(why)),
20556
+ q,
20557
+ new Permissions(hub),
20558
+ new Runs(hub),
20559
+ new Presence2(hub)
20560
+ ];
20222
20561
  });
20223
20562
  return agent;
20224
20563
  }
@@ -20260,6 +20599,109 @@ async function runAgent(ctx, opts) {
20260
20599
 
20261
20600
  // src/hook.ts
20262
20601
  import { basename as basename3 } from "node:path";
20602
+
20603
+ // src/ran.ts
20604
+ import { execFile as execFile2 } from "node:child_process";
20605
+ import { readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
20606
+ var RUN_CHECK_MS = 500;
20607
+ var SHELLS = new Set(["sh", "dash", "bash", "zsh", "ash", "ksh", "fish", "busybox"]);
20608
+ function evalForm(command) {
20609
+ return `eval '${command.replaceAll("'", `'"'"'`)}'`;
20610
+ }
20611
+ function program(p) {
20612
+ return (p.argv[0] ?? "").split(/\s/)[0]?.split("/").pop() ?? "";
20613
+ }
20614
+ function runs(p, command) {
20615
+ const quoted = evalForm(command);
20616
+ return SHELLS.has(program(p)) && p.argv.some((a) => a.includes(quoted));
20617
+ }
20618
+ function descends(pid, root, byPid) {
20619
+ const seen = new Set;
20620
+ for (let p = byPid.get(pid);p && !seen.has(p.pid); p = byPid.get(p.ppid)) {
20621
+ if (p.ppid === root)
20622
+ return true;
20623
+ seen.add(p.pid);
20624
+ }
20625
+ return false;
20626
+ }
20627
+ function procLinux() {
20628
+ const out = [];
20629
+ for (const name of readdirSync3("/proc")) {
20630
+ const pid = Number(name);
20631
+ if (!Number.isInteger(pid))
20632
+ continue;
20633
+ try {
20634
+ const stat = readFileSync9(`/proc/${pid}/stat`, "utf8");
20635
+ const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
20636
+ const argv = readFileSync9(`/proc/${pid}/cmdline`, "utf8").split("\x00").filter(Boolean);
20637
+ out.push({ pid, ppid, argv });
20638
+ } catch {}
20639
+ }
20640
+ return out;
20641
+ }
20642
+ function procPs() {
20643
+ return new Promise((resolve) => execFile2("ps", ["-A", "-ww", "-o", "pid=,ppid=,args="], { maxBuffer: 16 * 1024 * 1024, timeout: 5000 }, (e, stdout) => {
20644
+ if (e)
20645
+ return resolve([]);
20646
+ resolve(stdout.split(`
20647
+ `).flatMap((line) => {
20648
+ const m = /^\s*(\d+)\s+(\d+)\s(.*)$/.exec(line);
20649
+ return m ? [{ pid: Number(m[1]), ppid: Number(m[2]), argv: [m[3]] }] : [];
20650
+ }));
20651
+ }));
20652
+ }
20653
+ async function processes(platform = process.platform) {
20654
+ if (platform === "linux")
20655
+ return procLinux();
20656
+ if (platform === "win32")
20657
+ return [];
20658
+ return procPs();
20659
+ }
20660
+ function agentPid(procs, from = process.ppid) {
20661
+ const byPid = new Map(procs.map((p) => [p.pid, p]));
20662
+ for (let pid = from, hops = 0;hops < 4; hops++) {
20663
+ const p = byPid.get(pid);
20664
+ if (!p)
20665
+ return;
20666
+ if (!SHELLS.has(program(p)))
20667
+ return pid;
20668
+ pid = p.ppid;
20669
+ }
20670
+ return;
20671
+ }
20672
+ function untilRan(signal, command, list = processes, from = process.ppid) {
20673
+ const ran = new AbortController;
20674
+ let timer;
20675
+ let stopped = false;
20676
+ const watch = async () => {
20677
+ const first = await list().catch(() => []);
20678
+ const root = agentPid(first, from);
20679
+ if (root === undefined)
20680
+ return;
20681
+ const before = new Set(first.map((p) => p.pid));
20682
+ const tick = async () => {
20683
+ if (stopped)
20684
+ return;
20685
+ const now = await list().catch(() => []);
20686
+ const byPid = new Map(now.map((p) => [p.pid, p]));
20687
+ if (now.some((p) => !before.has(p.pid) && runs(p, command) && descends(p.pid, root, byPid)))
20688
+ return ran.abort();
20689
+ timer = setTimeout(tick, RUN_CHECK_MS);
20690
+ timer.unref?.();
20691
+ };
20692
+ await tick();
20693
+ };
20694
+ watch();
20695
+ return {
20696
+ signal: signal ? AbortSignal.any([signal, ran.signal]) : ran.signal,
20697
+ stop: () => {
20698
+ stopped = true;
20699
+ clearTimeout(timer);
20700
+ }
20701
+ };
20702
+ }
20703
+
20704
+ // src/hook.ts
20263
20705
  var SLACK_MS2 = 15000;
20264
20706
  var DIRECT_POLL_SECONDS = 5;
20265
20707
  var REPORT_MS = 5000;
@@ -20288,14 +20730,22 @@ function untilOrphaned(signal, ppid = () => process.ppid) {
20288
20730
  stop: () => clearInterval(timer)
20289
20731
  };
20290
20732
  }
20291
- async function hookPermission(outer, stdin, opts) {
20733
+ async function hookPermission(outer, stdin, opts, ran = untilRan) {
20292
20734
  const watch = untilOrphaned(outer.signal);
20293
- const ctx = { ...outer, signal: watch.signal };
20735
+ let ctx = { ...outer, signal: watch.signal };
20736
+ let started;
20294
20737
  try {
20738
+ const hook = parseHook(stdin);
20739
+ if (hook.tool_name === "AskUserQuestion" && opts.agent === "claude-code")
20740
+ return await racePicker(ctx, outer, hook, opts.wait);
20295
20741
  if (!permissionsEnabled(ctx))
20296
20742
  return 0;
20297
20743
  const agent = agentName(opts.agent);
20298
- const hook = parseHook(stdin);
20744
+ const command = hook.tool_input?.command;
20745
+ if (agent === "claude-code" && hook.tool_name === "Bash" && typeof command === "string") {
20746
+ started = ran(ctx.signal, command);
20747
+ ctx = { ...ctx, signal: started.signal };
20748
+ }
20299
20749
  const waitMs = opts.wait ? parseDuration(opts.wait) : DEFAULT_WAIT_MS;
20300
20750
  const deadline = ctx.now().getTime() + waitMs;
20301
20751
  const source = permissionSource(hook, ctx.env);
@@ -20305,6 +20755,7 @@ async function hookPermission(outer, stdin, opts) {
20305
20755
  } catch (e) {
20306
20756
  ctx.err(`starbridge: permission prompt not sent: ${e.message}`);
20307
20757
  } finally {
20758
+ started?.stop();
20308
20759
  watch.stop();
20309
20760
  }
20310
20761
  return 0;
@@ -20406,7 +20857,11 @@ async function hookSettle(ctx, stdin, opts) {
20406
20857
  const st = ctx.store.state();
20407
20858
  if (ctx.store.promptsMarkStale(st))
20408
20859
  ctx.store.updateState(() => {});
20409
- if (!sessionId || waitingFor(st, sessionId).length === 0)
20860
+ if (!sessionId)
20861
+ return 0;
20862
+ if (hook.tool_name === "AskUserQuestion" || hook.tool_input === undefined)
20863
+ await settlePicker(ctx, sessionId, "elsewhere");
20864
+ if (waitingFor(ctx.store.state(), sessionId).length === 0)
20410
20865
  return 0;
20411
20866
  const inputHash = hook.tool_input !== undefined ? inputHashOf(session(ctx).keys, hook.tool_input) : undefined;
20412
20867
  if (waitingFor(ctx.store.state(), sessionId, inputHash).length === 0)
@@ -20425,41 +20880,77 @@ async function hookSettle(ctx, stdin, opts) {
20425
20880
  }
20426
20881
  return 0;
20427
20882
  }
20428
- var ASK_USER_REASON = "Not answered here: the user is away from this terminal, so ask through Starbridge instead. Run `starbridge ask` with the question, the context they need to answer it cold, the options and what each one changes, your recommendation first, as the `starbridge` skill says. Then keep working on what does not depend on the answer.";
20429
- var REACH_MS = 3000;
20430
- async function hookAskUser(ctx, stdin) {
20431
- try {
20432
- const machine = ctx.store.machine();
20433
- if (!machine)
20434
- return 0;
20435
- const res = await fetch(`${machine.server.replace(/\/+$/, "")}/healthz`, {
20436
- signal: AbortSignal.timeout(REACH_MS)
20437
- });
20438
- if (!res.ok)
20439
- return 0;
20440
- } catch {
20883
+ function pickerQuestions(input) {
20884
+ const questions = input?.questions;
20885
+ if (!Array.isArray(questions) || questions.length === 0 || !questions.every((q) => typeof q?.question === "string"))
20886
+ return;
20887
+ return questions;
20888
+ }
20889
+ function pickerOpen(st, session) {
20890
+ return Object.entries(st.asked).filter(([id, a]) => a.picker === session && !a.settled && !st.answers[id]).map(([id]) => id);
20891
+ }
20892
+ async function settlePicker(ctx, session, outcome) {
20893
+ await Promise.all(pickerOpen(ctx.store.state(), session).map((id) => settle2({ ...ctx, signal: undefined }, { id, outcome }).catch((e) => ctx.err(`starbridge: could not settle ${id}: ${e.message}`))));
20894
+ }
20895
+ async function racePicker(ctx, outer, hook, waitText) {
20896
+ const questions = pickerQuestions(hook.tool_input);
20897
+ const sessionId = typeof hook.session_id === "string" ? hook.session_id : "";
20898
+ if (!questions || !sessionId)
20899
+ return 0;
20900
+ const s = session(ctx);
20901
+ const waitMs = waitText ? parseDuration(waitText) : DEFAULT_WAIT_MS;
20902
+ const cwd = typeof hook.cwd === "string" && hook.cwd ? hook.cwd : process.cwd();
20903
+ const deadline = AbortSignal.timeout(waitMs);
20904
+ const failed = new AbortController;
20905
+ const signal = AbortSignal.any([
20906
+ ctx.signal ?? new AbortController().signal,
20907
+ deadline,
20908
+ failed.signal
20909
+ ]);
20910
+ const results = await Promise.allSettled(questions.map(async (q) => {
20911
+ const sub = { ...ctx, signal, err: () => {} };
20912
+ const input = {
20913
+ ...questionInput({ ...q, multiple: q.multiSelect }),
20914
+ agent: "claude-code",
20915
+ session: sessionId,
20916
+ project: basename3(cwd),
20917
+ held: true,
20918
+ picker: sessionId,
20919
+ waiting: true
20920
+ };
20921
+ let id;
20922
+ try {
20923
+ ({ id } = await postDecision(sub, s, resolveSource(input, ctx.env, cwd)));
20924
+ } catch (e) {
20925
+ failed.abort();
20926
+ throw e;
20927
+ }
20928
+ while (true) {
20929
+ const lines = [];
20930
+ const code = await wait({ ...sub, out: (l) => lines.push(l) }, { id, json: true, "no-mark": true }, s);
20931
+ if (code === 0 && lines[0])
20932
+ return opencodeAnswer(JSON.parse(lines[0]), { ...q, multiple: q.multiSelect });
20933
+ if (code !== EXIT_SNOOZED)
20934
+ return;
20935
+ }
20936
+ }));
20937
+ const picked = results.map((r) => r.status === "fulfilled" ? r.value : undefined);
20938
+ const error = results.find((r) => r.status === "rejected");
20939
+ if (error && !signal.aborted)
20940
+ ctx.err(`starbridge: question not sent: ${error.reason.message}`);
20941
+ if (picked.every((p) => p !== undefined) && !signal.aborted) {
20942
+ const answers = Object.fromEntries(questions.map((q, i) => [q.question, picked[i].join(", ")]));
20943
+ const decision = {
20944
+ behavior: "allow",
20945
+ updatedInput: { ...hook.tool_input, answers }
20946
+ };
20947
+ ctx.out(JSON.stringify({ hookSpecificOutput: { hookEventName: "PermissionRequest", decision } }));
20441
20948
  return 0;
20442
20949
  }
20443
- let input;
20444
- try {
20445
- const parsed = JSON.parse(stdin);
20446
- const questions = parsed.tool_input?.questions;
20447
- if (Array.isArray(questions) && questions.length > 0 && questions.every((q) => typeof q?.question === "string"))
20448
- input = parsed.tool_input;
20449
- } catch {}
20450
- const hookSpecificOutput = input ? {
20451
- hookEventName: "PreToolUse",
20452
- permissionDecision: "allow",
20453
- updatedInput: {
20454
- ...input,
20455
- answers: Object.fromEntries(input.questions.map((q) => [q.question, ASK_USER_REASON]))
20456
- }
20457
- } : {
20458
- hookEventName: "PreToolUse",
20459
- permissionDecision: "deny",
20460
- permissionDecisionReason: ASK_USER_REASON
20461
- };
20462
- ctx.out(JSON.stringify({ hookSpecificOutput }));
20950
+ await settlePicker(ctx, sessionId, outer.signal?.aborted ? "elsewhere" : "withdrawn");
20951
+ return 0;
20952
+ }
20953
+ function hookAskUser() {
20463
20954
  return 0;
20464
20955
  }
20465
20956
  var QUESTION_CHARS = 300;
@@ -20545,7 +21036,10 @@ async function hookQuestion(outer, stdin, opts) {
20545
21036
 
20546
21037
  // src/pair.ts
20547
21038
  import { randomBytes as randomBytes8 } from "node:crypto";
21039
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
20548
21040
  import { hostname } from "node:os";
21041
+ import { join as join8 } from "node:path";
21042
+ import { createInterface as createInterface3 } from "node:readline";
20549
21043
 
20550
21044
  // ../node_modules/.pnpm/uqr@0.1.3/node_modules/uqr/dist/index.mjs
20551
21045
  var QrCodeDataType = /* @__PURE__ */ ((QrCodeDataType2) => {
@@ -21148,7 +21642,7 @@ function terminalQr(text) {
21148
21642
 
21149
21643
  // src/settings.ts
21150
21644
  import { spawnSync as spawnSync2 } from "node:child_process";
21151
- import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
21645
+ import { existsSync as existsSync4, readdirSync as readdirSync4 } from "node:fs";
21152
21646
  function detectMachineKind(env, platform = process.platform) {
21153
21647
  if (env.CLAUDE_CODE_REMOTE === "true" || env.CODESPACES === "true" || env.GITPOD_WORKSPACE_ID)
21154
21648
  return "cloud";
@@ -21162,7 +21656,7 @@ function hasBattery(platform) {
21162
21656
  try {
21163
21657
  if (platform === "linux") {
21164
21658
  const dir = "/sys/class/power_supply";
21165
- return existsSync4(dir) && readdirSync3(dir).some((n) => n.startsWith("BAT"));
21659
+ return existsSync4(dir) && readdirSync4(dir).some((n) => n.startsWith("BAT"));
21166
21660
  }
21167
21661
  if (platform === "darwin")
21168
21662
  return spawnSync2("pmset", ["-g", "batt"], { encoding: "utf8" }).stdout.includes("InternalBattery");
@@ -21230,7 +21724,7 @@ async function offerPiAllow(ctx, prompt, quiet = false) {
21230
21724
  } else
21231
21725
  ctx.out(`Pi: pi-permission-system will ask before each \`starbridge ask\`; to allow them later, ${how}.`);
21232
21726
  }
21233
- var USAGE = "usage: starbridge config [permissions on|off] [machine-kind server|desktop|laptop|cloud]";
21727
+ var USAGE = "usage: starbridge config [permissions on|off] [presence on|off] [machine-kind server|desktop|laptop|cloud]";
21234
21728
  async function configCommand(ctx, args, prompt) {
21235
21729
  const [key, value, ...extra] = args;
21236
21730
  if (extra.length > 0 || key !== undefined && value === undefined)
@@ -21243,6 +21737,11 @@ async function configCommand(ctx, args, prompt) {
21243
21737
  await offerPiChain(ctx, prompt);
21244
21738
  await offerPiAllow(ctx, prompt, true);
21245
21739
  }
21740
+ } else if (key === "presence") {
21741
+ if (value !== "on" && value !== "off")
21742
+ throw new UsageError(USAGE);
21743
+ const config = ctx.store.agentConfig();
21744
+ ctx.store.saveAgentConfig({ ...config, presence: { enabled: value === "on" } });
21246
21745
  } else if (key === "machine-kind") {
21247
21746
  const kind = MachineKind.safeParse(value);
21248
21747
  if (!kind.success)
@@ -21252,6 +21751,7 @@ async function configCommand(ctx, args, prompt) {
21252
21751
  throw new UsageError(USAGE);
21253
21752
  }
21254
21753
  ctx.out(`permissions ${permissionsEnabled(ctx) ? "on" : "off"}`);
21754
+ ctx.out(`presence ${ctx.store.agentConfig().presence?.enabled ? "on" : "off"}`);
21255
21755
  ctx.out(`machine-kind ${ctx.store.agentConfig().machineKind ?? "not set"}`);
21256
21756
  return 0;
21257
21757
  }
@@ -21270,6 +21770,7 @@ async function pair(ctx, opts) {
21270
21770
  const id = `m_${randomBytes8(9).toString("base64url")}`;
21271
21771
  const name = opts.name ?? previous?.name ?? hostname();
21272
21772
  const claim = newClaimSecret();
21773
+ const checkKey = newCheckKey();
21273
21774
  const deadline = ctx.now().getTime() + CODE_LIFETIME_MS;
21274
21775
  const expired = () => new UsageError(`the pairing code expired; run \`${opts.again ?? "starbridge pair"}\` again`);
21275
21776
  let code = newPairingCode();
@@ -21294,10 +21795,10 @@ async function pair(ctx, opts) {
21294
21795
  }
21295
21796
  const link = pairingLink(server, code);
21296
21797
  ctx.out(`Pairing code: ${formatPairingCode(code)}`);
21297
- ctx.out(" Scan this with your phone's camera or the Starbridge app:");
21298
- for (const line of terminalQr(link))
21798
+ ctx.out(" Scan this with the Starbridge Android app or your phone's camera:");
21799
+ for (const line of terminalQr(appPairingLink(server, code, checkKey)))
21299
21800
  ctx.out(line);
21300
- ctx.out(` Or open ${link}`);
21801
+ ctx.out(` No app? Open ${link}`);
21301
21802
  ctx.out(" Or type the code under Devices in the app or web page. It expires in 10 minutes.");
21302
21803
  let result;
21303
21804
  while (!result) {
@@ -21325,6 +21826,20 @@ async function pair(ctx, opts) {
21325
21826
  const pin = { length: approval.length, head: approval.head };
21326
21827
  const dir = verifyDirectory(entries, { account: approval.account, pin });
21327
21828
  checkJoined(dir, { id, role: "machine", ...publicKeys(keys) });
21829
+ const again = opts.again ?? "starbridge pair";
21830
+ const check = checkCode(entries, id);
21831
+ if (approval.check !== undefined) {
21832
+ if (!verifyCheckProof(entries, id, checkKey, approval.check))
21833
+ throw new UsageError(`the Android app that scanned the QR code saw another check code, so a server may have paired this machine into an account it controls. Nothing is saved: revoke "${name}" under Devices, then run \`${again}\` again`);
21834
+ } else {
21835
+ const answer = await confirmCheck(ctx, check, name);
21836
+ if (ctx.signal?.aborted)
21837
+ return 130;
21838
+ if (answer === "no")
21839
+ throw new UsageError(`the codes differ, so this machine is not paired. A server may have read the pairing code. Revoke "${name}" under Devices, then run \`${again}\` again`);
21840
+ if (answer === undefined)
21841
+ throw new UsageError(`the check code was not confirmed within 10 minutes, so this machine is not paired. Revoke "${name}" under Devices, then run \`${again}\` again`);
21842
+ }
21328
21843
  const token = result.token;
21329
21844
  ctx.store.locked(() => {
21330
21845
  ctx.store.saveDirectory(entries);
@@ -21357,14 +21872,78 @@ async function pair(ctx, opts) {
21357
21872
  function addedAt(entries, id) {
21358
21873
  for (const e of entries) {
21359
21874
  const body = JSON.parse(e.body);
21360
- if (body.op === "add" && body.member?.id === id)
21361
- return new Date(body.at).toLocaleString(undefined, {
21362
- month: "short",
21363
- day: "numeric",
21875
+ if (body.op === "add" && body.member?.id === id) {
21876
+ const at = new Date(body.at);
21877
+ const day = at.toLocaleDateString(undefined, { month: "short", day: "numeric" });
21878
+ const time = at.toLocaleTimeString(undefined, {
21364
21879
  hour: "2-digit",
21365
21880
  minute: "2-digit",
21366
21881
  timeZoneName: "short"
21367
21882
  });
21883
+ return `${day}, ${time}`;
21884
+ }
21885
+ }
21886
+ }
21887
+ var CONFIRM_MS = 10 * 60000;
21888
+ var waitingFile = (ctx) => join8(ctx.store.dir, "pair-waiting");
21889
+ var answerFile = (ctx, code) => join8(ctx.store.dir, `pair-answer.${code}`);
21890
+ function writeWhole(path, text) {
21891
+ const tmp = `${path}.${process.pid}`;
21892
+ rmSync3(tmp, { force: true });
21893
+ writeFileSync5(tmp, text, { mode: 384, flag: "wx" });
21894
+ renameSync3(tmp, path);
21895
+ }
21896
+ function sendConfirm(ctx, same) {
21897
+ let code;
21898
+ try {
21899
+ code = readFileSync10(waitingFile(ctx), "utf8").trim();
21900
+ } catch {
21901
+ throw new UsageError("no `starbridge pair` is waiting for its check code to be confirmed");
21902
+ }
21903
+ if (!/^[0-9A-Z]{4}(-[0-9A-Z]{4}){3}$/.test(code))
21904
+ throw new UsageError("no `starbridge pair` is waiting for its check code to be confirmed");
21905
+ writeWhole(answerFile(ctx, code), same ? "yes" : "no");
21906
+ ctx.out(`${same ? "Confirmed" : "Rejected"} check code ${code} for the waiting \`starbridge pair\`.`);
21907
+ return 0;
21908
+ }
21909
+ async function confirmCheck(ctx, code, name) {
21910
+ const file = answerFile(ctx, code);
21911
+ rmSync3(file, { force: true });
21912
+ mkdirSync6(ctx.store.dir, { recursive: true, mode: 448 });
21913
+ writeWhole(waitingFile(ctx), code);
21914
+ const tty = process.stdin.isTTY;
21915
+ ctx.out(`Check code: ${code}`);
21916
+ ctx.out(` The Starbridge Android app shows "${name}" under Devices with its check code. A browser shows what the server sends, so compare with the app if you have it. An app that shows no code needs updating.`);
21917
+ ctx.out(tty ? "Same code? [Y/n]" : " Same code? Run `starbridge pair --confirm` if so, `starbridge pair --reject` if not.");
21918
+ const typed = [];
21919
+ const shown = Date.now();
21920
+ const rl = tty ? createInterface3({ input: process.stdin }).on("line", (l) => {
21921
+ if (Date.now() - shown > 500)
21922
+ typed.push(l);
21923
+ }) : undefined;
21924
+ try {
21925
+ const until = ctx.now().getTime() + CONFIRM_MS;
21926
+ for (;; ) {
21927
+ if (ctx.signal?.aborted || ctx.now().getTime() > until)
21928
+ return;
21929
+ const line = typed.shift();
21930
+ if (line !== undefined)
21931
+ return /^\s*(y(es)?)?\s*$/i.test(line) ? "yes" : "no";
21932
+ try {
21933
+ const answer = readFileSync10(file, "utf8");
21934
+ rmSync3(file, { force: true });
21935
+ return answer === "yes" ? "yes" : "no";
21936
+ } catch {
21937
+ await ctx.sleep(500);
21938
+ }
21939
+ }
21940
+ } finally {
21941
+ rl?.close();
21942
+ rmSync3(file, { force: true });
21943
+ try {
21944
+ if (readFileSync10(waitingFile(ctx), "utf8") === code)
21945
+ rmSync3(waitingFile(ctx), { force: true });
21946
+ } catch {}
21368
21947
  }
21369
21948
  }
21370
21949
 
@@ -21484,8 +22063,8 @@ function installKind() {
21484
22063
  }
21485
22064
 
21486
22065
  // src/setup/plugins.ts
21487
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
21488
- import { join as join8 } from "node:path";
22066
+ import { existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "node:fs";
22067
+ import { join as join9 } from "node:path";
21489
22068
  var MARKETPLACE = "starbridge";
21490
22069
  var MARKETPLACE_SOURCE = "T0mSIlver/starbridge";
21491
22070
  var PLUGINS = [`starbridge@${MARKETPLACE}`, `starbridge-mod@${MARKETPLACE}`];
@@ -21493,9 +22072,9 @@ function hasClaude(sys) {
21493
22072
  return which(sys.ctx.env, "claude") !== undefined;
21494
22073
  }
21495
22074
  function claudeDir(sys) {
21496
- return sys.ctx.env.CLAUDE_CONFIG_DIR || join8(sys.home, ".claude");
22075
+ return sys.ctx.env.CLAUDE_CONFIG_DIR || join9(sys.home, ".claude");
21497
22076
  }
21498
- var claude = (sys, ...args) => run(sys, "claude", args, { timeoutMs: 180000 });
22077
+ var claude = (sys, ...args) => run(sys, "claude", args, { timeoutMs: 180000, cwd: sys.home });
21499
22078
  async function listJson(sys, ...args) {
21500
22079
  const r = await claude(sys, ...args, "--json");
21501
22080
  const why = `\`claude ${args.join(" ")} --json\` failed`;
@@ -21565,7 +22144,7 @@ async function installPlugins(sys, state) {
21565
22144
  throw new Error(foreign);
21566
22145
  const done = [];
21567
22146
  if (!state.marketplace) {
21568
- const r = await claude(sys, "plugin", "marketplace", "add", MARKETPLACE_SOURCE);
22147
+ const r = await claude(sys, "plugin", "marketplace", "add", MARKETPLACE_SOURCE, "--scope", "user");
21569
22148
  if (r?.code !== 0)
21570
22149
  throw new Error(`claude plugin marketplace add ${MARKETPLACE_SOURCE}: ${failure2(r)}`);
21571
22150
  done.push(`Added the ${MARKETPLACE} marketplace.`);
@@ -21581,6 +22160,8 @@ async function installPlugins(sys, state) {
21581
22160
  return done;
21582
22161
  }
21583
22162
  async function removePlugins(sys, state) {
22163
+ if (state.foreign)
22164
+ return [`Left the ${MARKETPLACE} marketplace from ${state.foreign} and its plugins alone.`];
21584
22165
  const done = [];
21585
22166
  for (const id of PLUGINS) {
21586
22167
  if (!state.plugins[id])
@@ -21595,17 +22176,17 @@ async function removePlugins(sys, state) {
21595
22176
  return done;
21596
22177
  }
21597
22178
  function settingsPath(sys) {
21598
- return join8(claudeDir(sys), "settings.json");
22179
+ return join9(claudeDir(sys), "settings.json");
21599
22180
  }
21600
22181
  function readSettings(sys) {
21601
22182
  try {
21602
- return JSON.parse(readFileSync9(settingsPath(sys), "utf8"));
22183
+ return JSON.parse(readFileSync11(settingsPath(sys), "utf8"));
21603
22184
  } catch {
21604
22185
  return;
21605
22186
  }
21606
22187
  }
21607
22188
  function writeSettings(sys, s) {
21608
- writeFileSync5(settingsPath(sys), `${JSON.stringify(s, null, 2)}
22189
+ writeFileSync6(settingsPath(sys), `${JSON.stringify(s, null, 2)}
21609
22190
  `);
21610
22191
  }
21611
22192
  function autoUpdate(sys) {
@@ -21646,7 +22227,7 @@ function addAllowRules(sys) {
21646
22227
  ...s.permissions,
21647
22228
  allow: [...allow, ...ALLOW_RULES.filter((r) => !allow.includes(r))]
21648
22229
  };
21649
- mkdirSync6(claudeDir(sys), { recursive: true });
22230
+ mkdirSync7(claudeDir(sys), { recursive: true });
21650
22231
  writeSettings(sys, s);
21651
22232
  return true;
21652
22233
  }
@@ -21858,24 +22439,39 @@ import {
21858
22439
  closeSync as closeSync2,
21859
22440
  existsSync as existsSync6,
21860
22441
  lstatSync as lstatSync2,
21861
- mkdirSync as mkdirSync7,
22442
+ mkdirSync as mkdirSync8,
21862
22443
  openSync as openSync2,
21863
- readdirSync as readdirSync4,
21864
- readFileSync as readFileSync10,
22444
+ readdirSync as readdirSync5,
22445
+ readFileSync as readFileSync12,
21865
22446
  realpathSync as realpathSync4,
21866
- renameSync as renameSync3,
21867
- rmSync as rmSync3,
22447
+ renameSync as renameSync4,
22448
+ rmSync as rmSync4,
21868
22449
  symlinkSync,
21869
22450
  unlinkSync as unlinkSync4,
21870
22451
  writeSync
21871
22452
  } from "node:fs";
21872
- import { dirname as dirname5, join as join9 } from "node:path";
22453
+ import { dirname as dirname5, join as join10 } from "node:path";
22454
+ // src/setup/codexbar-pin.json
22455
+ var codexbar_pin_default = {
22456
+ version: "0.73.0",
22457
+ sha256: {
22458
+ "macos-arm64": "cd8f853779f8faec003f53d5fc2a1614aef577ec9f603cbd58a9559b54e159b9",
22459
+ "macos-x86_64": "19e7059de688e62be98790bc10198257f5d326e4a3f4191183c23106eb91780a",
22460
+ "linux-aarch64": "ae6f9be046b09dc9d17210b661c25e2ee8a0150edb047b917d9eb17c295c7094",
22461
+ "linux-x86_64": "a84f556c7ecebc8e606e0a34ee0d121626f72d7afc1e3adf5746410bc10f9611",
22462
+ "linux-musl-aarch64": "a4007dfa6577a436eaeb6b46601982c69548c6e20730b3eb9133d4266d5ea92b",
22463
+ "linux-musl-x86_64": "75fd9cfc00784eef8badd6e76f4f74cbdf7c13db0b7f9ca6e105ddb793e6e8f8"
22464
+ }
22465
+ };
22466
+
22467
+ // src/setup/codexbar.ts
22468
+ var PIN = codexbar_pin_default;
21873
22469
  var RELEASES = "https://github.com/steipete/CodexBar/releases";
21874
22470
  var PROBE_TIMEOUT_MS = 20000;
21875
22471
  var PROGRESS_MS2 = 5000;
21876
22472
  var APP_HELPERS = (home) => [
21877
22473
  "/Applications/CodexBar.app/Contents/Helpers/CodexBarCLI",
21878
- join9(home, "Applications/CodexBar.app/Contents/Helpers/CodexBarCLI")
22474
+ join10(home, "Applications/CodexBar.app/Contents/Helpers/CodexBarCLI")
21879
22475
  ];
21880
22476
  function findCodexbar(sys, configured) {
21881
22477
  for (const p of [configured, sys.ctx.env.STARBRIDGE_CODEXBAR])
@@ -21884,7 +22480,7 @@ function findCodexbar(sys, configured) {
21884
22480
  const onPath = which(sys.ctx.env, "codexbar");
21885
22481
  if (onPath)
21886
22482
  return { path: onPath };
21887
- const opt = join9(sys.home, ".local/opt/codexbar/codexbar");
22483
+ const opt = join10(sys.home, ".local/opt/codexbar/codexbar");
21888
22484
  if (existsSync6(opt))
21889
22485
  return { path: opt };
21890
22486
  if (sys.platform === "darwin") {
@@ -21907,7 +22503,7 @@ function tarballKey(sys, musl = isMusl()) {
21907
22503
  function isMusl() {
21908
22504
  const has = (dir, prefix) => {
21909
22505
  try {
21910
- return readdirSync4(dir).some((f) => f.startsWith(prefix));
22506
+ return readdirSync5(dir).some((f) => f.startsWith(prefix));
21911
22507
  } catch {
21912
22508
  return false;
21913
22509
  }
@@ -21924,9 +22520,9 @@ function isMusl() {
21924
22520
  return !libs.some((d) => has(d, "libcurl.so.4"));
21925
22521
  }
21926
22522
  function linkIntoLocalBin(sys, target) {
21927
- const bin = join9(sys.home, ".local/bin");
21928
- const link = join9(bin, "codexbar");
21929
- mkdirSync7(bin, { recursive: true });
22523
+ const bin = join10(sys.home, ".local/bin");
22524
+ const link = join10(bin, "codexbar");
22525
+ mkdirSync8(bin, { recursive: true });
21930
22526
  try {
21931
22527
  if (!lstatSync2(link).isSymbolicLink())
21932
22528
  return;
@@ -21935,7 +22531,7 @@ function linkIntoLocalBin(sys, target) {
21935
22531
  symlinkSync(target, link);
21936
22532
  return link;
21937
22533
  }
21938
- var optDir = (sys) => join9(sys.home, ".local/opt/codexbar");
22534
+ var optDir = (sys) => join10(sys.home, ".local/opt/codexbar");
21939
22535
  function releaseVersion(v) {
21940
22536
  const m = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)$/.exec(v.trim());
21941
22537
  if (!m)
@@ -21953,19 +22549,28 @@ async function fetchRelease(url, init, what) {
21953
22549
  throw new Error(`${what}: GitHub is limiting requests from this address (${res.status}); try again in an hour`);
21954
22550
  return res;
21955
22551
  }
21956
- async function latestCodexbar(sys) {
21957
- const releases = sys.ctx.env.STARBRIDGE_CODEXBAR_RELEASES ?? RELEASES;
21958
- const what = "finding CodexBar's latest release";
21959
- const res = await fetchRelease(`${releases}/latest`, { redirect: "manual", signal: AbortSignal.timeout(60000) }, what);
21960
- const tag = /\/tag\/([^/?#]+)$/.exec(res.headers.get("location") ?? "")?.[1];
21961
- if (!tag)
21962
- throw new Error(`${what}: ${res.status}, no release tag`);
21963
- return releaseVersion(decodeURIComponent(tag));
22552
+ function compareCodexbar(a, b) {
22553
+ const parse = (v) => {
22554
+ const [main = "", pre] = releaseVersion(v).split(/-(.*)/);
22555
+ return { nums: main.split(".").map(Number), pre };
22556
+ };
22557
+ const x = parse(a);
22558
+ const y = parse(b);
22559
+ for (let i = 0;i < 3; i++)
22560
+ if (x.nums[i] !== y.nums[i])
22561
+ return x.nums[i] < y.nums[i] ? -1 : 1;
22562
+ if (x.pre === y.pre)
22563
+ return 0;
22564
+ if (x.pre === undefined)
22565
+ return 1;
22566
+ if (y.pre === undefined)
22567
+ return -1;
22568
+ return Math.sign(x.pre.localeCompare(y.pre, "en", { numeric: true }));
21964
22569
  }
21965
22570
  var size = (bytes) => bytes >= 1e6 ? `${Math.round(bytes / 1e6)} MB` : `${Math.max(1, Math.round(bytes / 1000))} kB`;
21966
22571
  function installedVersion(sys) {
21967
22572
  try {
21968
- return readFileSync10(join9(optDir(sys), "VERSION"), "utf8").trim() || undefined;
22573
+ return readFileSync12(join10(optDir(sys), "VERSION"), "utf8").trim() || undefined;
21969
22574
  } catch {
21970
22575
  return;
21971
22576
  }
@@ -21973,40 +22578,43 @@ function installedVersion(sys) {
21973
22578
 
21974
22579
  class NoChecksum extends Error {
21975
22580
  }
21976
- async function installTarball(sys, key, version) {
22581
+ async function installTarball(sys, key, version, sha256) {
21977
22582
  const base = `${sys.ctx.env.STARBRIDGE_CODEXBAR_RELEASES ?? RELEASES}/download`;
21978
22583
  const name = `CodexBarCLI-v${version}-${key}.tar.gz`;
21979
22584
  const url = `${base}/v${version}/${name}`;
21980
22585
  const signal = AbortSignal.timeout(10 * 60000);
21981
- const sums = await fetchRelease(`${url}.sha256`, { signal }, `downloading ${name}.sha256`);
21982
- if (!sums.ok)
21983
- throw new NoChecksum(`no checksum for ${name} (${sums.status}): not installed`);
21984
- const want = /^[0-9a-f]{64}\b/i.exec((await sums.text()).trim())?.[0].toLowerCase();
21985
- if (!want)
21986
- throw new Error(`${name}.sha256 holds no SHA-256: not installed`);
22586
+ let want = sha256;
22587
+ if (!want) {
22588
+ const sums = await fetchRelease(`${url}.sha256`, { signal }, `downloading ${name}.sha256`);
22589
+ if (!sums.ok)
22590
+ throw new NoChecksum(`no checksum for ${name} (${sums.status}): not installed`);
22591
+ want = /^[0-9a-f]{64}\b/i.exec((await sums.text()).trim())?.[0].toLowerCase();
22592
+ if (!want)
22593
+ throw new Error(`${name}.sha256 holds no SHA-256: not installed`);
22594
+ }
21987
22595
  const res = await fetchRelease(url, { signal }, `downloading ${name}`);
21988
22596
  if (!res.ok || !res.body)
21989
22597
  throw new Error(`downloading ${name}: ${res.status}`);
21990
22598
  const dest = optDir(sys);
21991
22599
  const opt = dirname5(dest);
21992
- const fresh = join9(opt, `.codexbar.${process.pid}`);
22600
+ const fresh = join10(opt, `.codexbar.${process.pid}`);
21993
22601
  const file = `${fresh}.tar.gz`;
21994
22602
  const old = `${fresh}.old`;
21995
- mkdirSync7(fresh, { recursive: true });
22603
+ mkdirSync8(fresh, { recursive: true });
21996
22604
  try {
21997
22605
  await download(sys, res, file, want, name, `CodexBar ${version} (${key})`);
21998
22606
  const r = await run(sys, "tar", ["-xzf", file, "-C", fresh]);
21999
22607
  if (r?.code !== 0)
22000
22608
  throw new Error(`could not unpack ${name} into ${opt}; is the disk full? (${failure2(r)})`);
22001
22609
  if (existsSync6(dest))
22002
- renameSync3(dest, old);
22003
- renameSync3(fresh, dest);
22610
+ renameSync4(dest, old);
22611
+ renameSync4(fresh, dest);
22004
22612
  } finally {
22005
- rmSync3(file, { force: true });
22006
- rmSync3(fresh, { recursive: true, force: true });
22007
- rmSync3(old, { recursive: true, force: true });
22613
+ rmSync4(file, { force: true });
22614
+ rmSync4(fresh, { recursive: true, force: true });
22615
+ rmSync4(old, { recursive: true, force: true });
22008
22616
  }
22009
- return join9(dest, "codexbar");
22617
+ return join10(dest, "codexbar");
22010
22618
  }
22011
22619
  async function download(sys, res, file, want, name, label) {
22012
22620
  const total = Number(res.headers.get("content-length")) || undefined;
@@ -22035,12 +22643,15 @@ async function download(sys, res, file, want, name, label) {
22035
22643
  if (sha !== want)
22036
22644
  throw new Error(`${name} has SHA-256 ${sha}, expected ${want}: not installed`);
22037
22645
  }
22038
- async function installRelease(sys, version) {
22646
+ async function installRelease(sys, version, pin = PIN) {
22039
22647
  const key = tarballKey(sys);
22040
22648
  if (!key)
22041
22649
  throw new Error(`no CodexBar build for ${sys.platform} ${sys.arch}`);
22042
- const v = version ? releaseVersion(version) : await latestCodexbar(sys);
22043
- const path = await installTarball(sys, key, v);
22650
+ const v = version ? releaseVersion(version) : pin.version;
22651
+ const sha = v === pin.version ? pin.sha256[key] : undefined;
22652
+ if (v === pin.version && !sha)
22653
+ throw new Error(`no checksum pinned for CodexBar ${v} (${key})`);
22654
+ const path = await installTarball(sys, key, v, sha);
22044
22655
  const link = linkIntoLocalBin(sys, path);
22045
22656
  sys.ctx.out(`Installed CodexBar ${v} to ${optDir(sys)}${link ? `, linked as ${link}` : ""}.`);
22046
22657
  return path;
@@ -22087,10 +22698,10 @@ async function listProviders(sys, bin) {
22087
22698
  }
22088
22699
  function probeSet(sys, list, named) {
22089
22700
  const set = new Set([...list.filter((p) => p.enabled).map((p) => p.provider), ...named]);
22090
- const claude = existsSync6(join9(sys.home, ".claude/.credentials.json")) || which(sys.ctx.env, "claude");
22701
+ const claude = existsSync6(join10(sys.home, ".claude/.credentials.json")) || which(sys.ctx.env, "claude");
22091
22702
  if (claude)
22092
22703
  set.add("claude");
22093
- if (existsSync6(join9(sys.home, ".codex/auth.json")))
22704
+ if (existsSync6(join10(sys.home, ".codex/auth.json")))
22094
22705
  set.add("codex");
22095
22706
  return [...set];
22096
22707
  }
@@ -22121,7 +22732,7 @@ async function probe(bin, providers, list) {
22121
22732
  }
22122
22733
  }));
22123
22734
  }
22124
- async function updateCodexbar(sys, configured, version) {
22735
+ async function updateCodexbar(sys, configured, version, pin = PIN) {
22125
22736
  const out = sys.ctx.out;
22126
22737
  const found = findCodexbar(sys, configured);
22127
22738
  if (!found) {
@@ -22141,20 +22752,19 @@ async function updateCodexbar(sys, configured, version) {
22141
22752
  return version ? 1 : 0;
22142
22753
  }
22143
22754
  const have = installedVersion(sys);
22144
- let want;
22145
22755
  try {
22146
- want = version ? releaseVersion(version) : await latestCodexbar(sys);
22756
+ const want = version ? releaseVersion(version) : pin.version;
22147
22757
  if (have === want) {
22148
22758
  out(`CodexBar ${have} is ${version ? "installed" : "up to date"}.`);
22149
22759
  return 0;
22150
22760
  }
22151
- await installRelease(sys, want);
22152
- return 0;
22153
- } catch (e) {
22154
- if (!version && e instanceof NoChecksum) {
22155
- out(`CodexBar ${want}'s build for this machine is not published yet: kept ${have ?? "the installed one"}.`);
22761
+ if (!version && have && compareCodexbar(have, want) > 0) {
22762
+ out(`CodexBar ${have} is newer than the ${want} this release installs: kept.`);
22156
22763
  return 0;
22157
22764
  }
22765
+ await installRelease(sys, want, pin);
22766
+ return 0;
22767
+ } catch (e) {
22158
22768
  out(`Could not update CodexBar: ${e.message}`);
22159
22769
  return 1;
22160
22770
  }
@@ -22165,23 +22775,23 @@ import {
22165
22775
  appendFileSync as appendFileSync2,
22166
22776
  closeSync as closeSync3,
22167
22777
  existsSync as existsSync7,
22168
- mkdirSync as mkdirSync8,
22778
+ mkdirSync as mkdirSync9,
22169
22779
  openSync as openSync3,
22170
- readFileSync as readFileSync11,
22780
+ readFileSync as readFileSync13,
22171
22781
  readSync as readSync2,
22172
- rmSync as rmSync4,
22173
- writeFileSync as writeFileSync6
22782
+ rmSync as rmSync5,
22783
+ writeFileSync as writeFileSync7
22174
22784
  } from "node:fs";
22175
- import { basename as basename4, delimiter, dirname as dirname6, join as join10 } from "node:path";
22785
+ import { delimiter, dirname as dirname6, join as join11 } from "node:path";
22176
22786
  function recordSelf(sys) {
22177
- const file = join10(sys.ctx.store.dir, CLI_PATH);
22787
+ const file = join11(sys.ctx.store.dir, CLI_PATH);
22178
22788
  if (sys.self.length !== 1 || script(sys.self[0])) {
22179
- rmSync4(file, { force: true });
22789
+ rmSync5(file, { force: true });
22180
22790
  return;
22181
22791
  }
22182
- mkdirSync8(dirname6(file), { recursive: true, mode: 448 });
22792
+ mkdirSync9(dirname6(file), { recursive: true, mode: 448 });
22183
22793
  const self2 = sys.self[0];
22184
- writeFileSync6(file, `${sys.platform === "win32" ? self2.replace(/\\/g, "/") : self2}
22794
+ writeFileSync7(file, `${sys.platform === "win32" ? self2.replace(/\\/g, "/") : self2}
22185
22795
  `);
22186
22796
  }
22187
22797
  function script(path) {
@@ -22198,7 +22808,8 @@ function script(path) {
22198
22808
  function missingDir(sys) {
22199
22809
  if (sys.self.length !== 1)
22200
22810
  return;
22201
- const dir = dirname6(sys.self[0]);
22811
+ const { dirname } = pathFor(sys.platform);
22812
+ const dir = dirname(sys.self[0]);
22202
22813
  const norm = (p) => {
22203
22814
  const bare = p.replace(/[/\\]+$/, "");
22204
22815
  return sys.platform === "win32" ? bare.toLowerCase() : bare;
@@ -22207,20 +22818,21 @@ function missingDir(sys) {
22207
22818
  return path.includes(norm(dir)) ? undefined : dir;
22208
22819
  }
22209
22820
  function shellProfile(sys, dir) {
22210
- const shell = basename4(sys.ctx.env.SHELL ?? "");
22821
+ const { basename, join } = pathFor(sys.platform);
22822
+ const shell = basename(sys.ctx.env.SHELL ?? "");
22211
22823
  const shown = dir.startsWith(`${sys.home}/`) ? `$HOME${dir.slice(sys.home.length)}` : dir;
22212
22824
  const exported = `export PATH="${shown}:$PATH"`;
22213
22825
  if (shell === "fish")
22214
- return { file: join10(sys.home, ".config", "fish", "config.fish"), line: `fish_add_path ${dir}` };
22826
+ return { file: join(sys.home, ".config", "fish", "config.fish"), line: `fish_add_path ${dir}` };
22215
22827
  if (shell === "zsh")
22216
- return { file: join10(sys.ctx.env.ZDOTDIR || sys.home, ".zshrc"), line: exported };
22828
+ return { file: join(sys.ctx.env.ZDOTDIR || sys.home, ".zshrc"), line: exported };
22217
22829
  if (shell === "bash") {
22218
22830
  if (sys.platform !== "darwin")
22219
- return { file: join10(sys.home, ".bashrc"), line: exported };
22220
- const login = [".bash_profile", ".bash_login"].map((f) => join10(sys.home, f)).find(existsSync7);
22221
- return { file: login ?? join10(sys.home, ".profile"), line: exported };
22831
+ return { file: join(sys.home, ".bashrc"), line: exported };
22832
+ const login = [".bash_profile", ".bash_login"].map((f) => join(sys.home, f)).find(existsSync7);
22833
+ return { file: login ?? join(sys.home, ".profile"), line: exported };
22222
22834
  }
22223
- return { file: join10(sys.home, ".profile"), line: exported };
22835
+ return { file: join(sys.home, ".profile"), line: exported };
22224
22836
  }
22225
22837
  async function pathStep(sys) {
22226
22838
  const dir = missingDir(sys);
@@ -22236,12 +22848,13 @@ async function pathStep(sys) {
22236
22848
  ];
22237
22849
  }
22238
22850
  const { file, line } = shellProfile(sys, dir);
22851
+ const { dirname } = pathFor(sys.platform);
22239
22852
  const shown = file.startsWith(`${sys.home}/`) ? `~${file.slice(sys.home.length)}` : file;
22240
- const text = existsSync7(file) ? readFileSync11(file, "utf8") : "";
22853
+ const text = existsSync7(file) ? readFileSync13(file, "utf8") : "";
22241
22854
  const rest = dir.startsWith(`${sys.home}/`) ? dir.slice(sys.home.length) : undefined;
22242
22855
  const has = [dir, line, ...rest ? [`$HOME${rest}`, `\${HOME}${rest}`, `~${rest}`] : []].some((spelled) => text.includes(spelled));
22243
22856
  if (!has && await prompt.confirm(`Add ${dir} to your PATH in ${shown}?`, true)) {
22244
- mkdirSync8(dirname6(file), { recursive: true });
22857
+ mkdirSync9(dirname(file), { recursive: true });
22245
22858
  appendFileSync2(file, `${text === "" || text.endsWith(`
22246
22859
  `) ? "" : `
22247
22860
  `}
@@ -22262,9 +22875,9 @@ ${line}
22262
22875
  }
22263
22876
 
22264
22877
  // src/setup/service.ts
22265
- import { existsSync as existsSync8, mkdirSync as mkdirSync9, readFileSync as readFileSync12, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
22878
+ import { existsSync as existsSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
22266
22879
  import { userInfo as userInfo2 } from "node:os";
22267
- import { delimiter as delimiter2, dirname as dirname7, join as join11 } from "node:path";
22880
+ import { delimiter as delimiter2, dirname as dirname7, join as join12 } from "node:path";
22268
22881
  var UNIT = "starbridge-agent.service";
22269
22882
  var LABEL = "run.starbridge.agent";
22270
22883
  var TASK = "starbridge-agent";
@@ -22272,25 +22885,25 @@ function kind(sys) {
22272
22885
  return { linux: "systemd", darwin: "launchd", win32: "task" }[sys.platform];
22273
22886
  }
22274
22887
  function localDir(sys) {
22275
- return join11(sys.ctx.env.LOCALAPPDATA || join11(sys.home, "AppData", "Local"), "starbridge");
22888
+ return join12(sys.ctx.env.LOCALAPPDATA || join12(sys.home, "AppData", "Local"), "starbridge");
22276
22889
  }
22277
- var agentLog = (sys) => kind(sys) === "task" ? join11(localDir(sys), "agent.log") : join11(sys.home, "Library/Logs/starbridge-agent.log");
22890
+ var agentLog = (sys) => kind(sys) === "task" ? join12(localDir(sys), "agent.log") : join12(sys.home, "Library/Logs/starbridge-agent.log");
22278
22891
  function unitDir(sys) {
22279
- return join11(sys.home, ".config/systemd/user");
22892
+ return join12(sys.home, ".config/systemd/user");
22280
22893
  }
22281
22894
  function servicePath(sys) {
22282
22895
  const k = kind(sys);
22283
22896
  if (k === "systemd")
22284
- return join11(unitDir(sys), UNIT);
22897
+ return join12(unitDir(sys), UNIT);
22285
22898
  if (k === "launchd")
22286
- return join11(sys.home, "Library/LaunchAgents", `${LABEL}.plist`);
22899
+ return join12(sys.home, "Library/LaunchAgents", `${LABEL}.plist`);
22287
22900
  if (k === "task")
22288
- return join11(localDir(sys), `${TASK}.xml`);
22901
+ return join12(localDir(sys), `${TASK}.xml`);
22289
22902
  return;
22290
22903
  }
22291
22904
  function servicePathVar(sys) {
22292
22905
  const extra = [
22293
- join11(sys.home, ".local/bin"),
22906
+ join12(sys.home, ".local/bin"),
22294
22907
  "/opt/homebrew/bin",
22295
22908
  "/usr/local/bin",
22296
22909
  "/usr/bin",
@@ -22386,12 +22999,12 @@ ${env}
22386
22999
  </plist>
22387
23000
  `;
22388
23001
  }
22389
- var winArg = (s) => /[\s"]/.test(s) ? `"${s.replace(/"/g, "\\\"")}"` : s;
23002
+ var winArg = (s) => /[\s"]/.test(s) ? `"${s.replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\+)$/, "$1$1")}"` : s;
22390
23003
  function taskXml(sys) {
22391
23004
  const env = sys.ctx.env;
22392
23005
  const name = env.USERNAME || userInfo2().username;
22393
23006
  const user = env.USERDOMAIN ? `${env.USERDOMAIN}\\${name}` : name;
22394
- const conhost = join11(env.SystemRoot || env.SYSTEMROOT || "C:\\Windows", "System32", "conhost.exe");
23007
+ const conhost = join12(env.SystemRoot || env.SYSTEMROOT || "C:\\Windows", "System32", "conhost.exe");
22395
23008
  const args = ["--headless", ...sys.self, "agent", "--log", agentLog(sys)].map(winArg).join(" ");
22396
23009
  return `<?xml version="1.0" encoding="UTF-16"?>
22397
23010
  ${marker("<!--", "-->")}
@@ -22443,7 +23056,7 @@ ${marker("<!--", "-->")}
22443
23056
  </Task>
22444
23057
  `;
22445
23058
  }
22446
- var powershell = (sys, script, env = {}) => run(sys, join11(sys.ctx.env.SystemRoot || sys.ctx.env.SYSTEMROOT || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { timeoutMs: 60000, env });
23059
+ var powershell = (sys, script, env = {}) => run(sys, join12(sys.ctx.env.SystemRoot || sys.ctx.env.SYSTEMROOT || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { timeoutMs: 60000, env });
22447
23060
  async function stopTask(sys) {
22448
23061
  let pid;
22449
23062
  const socket = socketPath(sys.ctx.env, sys.ctx.store.dir, sys.platform);
@@ -22489,7 +23102,7 @@ async function unavailable(sys) {
22489
23102
  }
22490
23103
  function readText2(path) {
22491
23104
  try {
22492
- const bytes = readFileSync12(path);
23105
+ const bytes = readFileSync14(path);
22493
23106
  if (bytes[0] === 255 && bytes[1] === 254)
22494
23107
  return bytes.toString("utf16le").slice(1);
22495
23108
  return bytes.toString("utf8");
@@ -22507,12 +23120,12 @@ async function installService(sys, restart) {
22507
23120
  const before = readText2(path);
22508
23121
  if (before !== undefined && !ours(before))
22509
23122
  throw new Error(`${path} was not written by setup: remove it to install the agent`);
22510
- mkdirSync9(dirname7(path), { recursive: true });
23123
+ mkdirSync10(dirname7(path), { recursive: true });
22511
23124
  const k = kind(sys);
22512
23125
  const text = k === "systemd" ? unitText(sys) : k === "launchd" ? plistText(sys) : taskXml(sys);
22513
23126
  const changed = readText2(path) !== text;
22514
23127
  if (changed)
22515
- writeFileSync7(path, k === "task" ? `\uFEFF${text}` : text, k === "task" ? "utf16le" : "utf8");
23128
+ writeFileSync8(path, k === "task" ? `\uFEFF${text}` : text, k === "task" ? "utf16le" : "utf8");
22516
23129
  const running = /^(active|running)$/i.test((await serviceState(sys)).state);
22517
23130
  const go = changed || restart || !running;
22518
23131
  if (k === "task") {
@@ -22555,16 +23168,16 @@ async function removeService(sys) {
22555
23168
  const r = await powershell(sys, `Unregister-ScheduledTask -TaskName ${TASK} -Confirm:$false -ErrorAction SilentlyContinue; if (Get-ScheduledTask -TaskName ${TASK} -ErrorAction SilentlyContinue) { exit 1 }`);
22556
23169
  if (r?.code !== 0)
22557
23170
  throw new Error(`Unregister-ScheduledTask: ${failure2(r)}`);
22558
- rmSync5(path, { force: true });
23171
+ rmSync6(path, { force: true });
22559
23172
  } else if (kind(sys) === "systemd") {
22560
23173
  await stopUnit(sys, UNIT);
22561
- rmSync5(path, { force: true });
23174
+ rmSync6(path, { force: true });
22562
23175
  await systemctl(sys, "daemon-reload");
22563
23176
  } else {
22564
23177
  const r = await launchctl(sys, "bootout", `gui/${sys.uid}/${LABEL}`);
22565
23178
  if (r?.code !== 0 && r?.code !== 3 && r?.code !== 113)
22566
23179
  throw new Error(`launchctl bootout: ${failure2(r)}`);
22567
- rmSync5(path, { force: true });
23180
+ rmSync6(path, { force: true });
22568
23181
  }
22569
23182
  return true;
22570
23183
  }
@@ -22771,6 +23384,7 @@ async function setup(sys, opts) {
22771
23384
  for (const line of commandTable([
22772
23385
  ["Check it", "starbridge status"],
22773
23386
  ...permissionsEnabled(ctx) ? [] : [["Send permission prompts", "starbridge config permissions on"]],
23387
+ ...ctx.store.agentConfig().presence?.enabled || !["desktop", "laptop"].includes(ctx.store.agentConfig().machineKind ?? "") ? [] : [["Quiet the phone while here", "starbridge config presence on"]],
22774
23388
  ["Remove from one agent", `starbridge uninstall --agent ${one}`],
22775
23389
  ["Remove everything", "starbridge uninstall"]
22776
23390
  ]))
@@ -23137,6 +23751,9 @@ async function status(sys) {
23137
23751
  out(`Sessions: ${agent.sessions.length}`);
23138
23752
  for (const x of agent.sessions)
23139
23753
  out(` ${x.id}${x.title ? ` "${x.title}"` : ""}${x.client ? ` ${x.client}` : ""}, last seen ${x.lastSeenAt}`);
23754
+ const own = ctx.env.CLAUDECODE === "1" ? ctx.env.CLAUDE_CODE_SESSION_ID : undefined;
23755
+ if (own && ctx.env.CLAUDE_CODE_SESSION_ATTENDED !== "0" && !agent.sessions.some((x) => x.id === own))
23756
+ out(` Not this session (${own}): no Starbridge mod runs in it, so answers come back only through \`starbridge wait\``);
23140
23757
  }
23141
23758
  const svc = await serviceState(sys);
23142
23759
  out(svc.installed ? `Service: ${svc.state}${svc.enabled === undefined ? "" : svc.enabled ? ", enabled" : ", not enabled"}` : "Service: not installed");
@@ -23192,14 +23809,14 @@ async function status(sys) {
23192
23809
  }
23193
23810
 
23194
23811
  // src/setup/uninstall.ts
23195
- import { existsSync as existsSync10, realpathSync as realpathSync5, rmSync as rmSync7 } from "node:fs";
23196
- import { join as join13 } from "node:path";
23812
+ import { existsSync as existsSync10, realpathSync as realpathSync5, rmSync as rmSync8 } from "node:fs";
23813
+ import { join as join14 } from "node:path";
23197
23814
 
23198
23815
  // src/update.ts
23199
- import { spawn as spawn6, spawnSync as spawnSync3 } from "node:child_process";
23200
- import { chmodSync as chmodSync3, existsSync as existsSync9, readdirSync as readdirSync5, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
23816
+ import { spawn as spawn7, spawnSync as spawnSync3 } from "node:child_process";
23817
+ import { chmodSync as chmodSync3, existsSync as existsSync9, readdirSync as readdirSync6, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
23201
23818
  import { homedir as homedir5 } from "node:os";
23202
- import { basename as basename5, dirname as dirname8, join as join12 } from "node:path";
23819
+ import { basename as basename4, dirname as dirname8, join as join13 } from "node:path";
23203
23820
  var MANAGED = {
23204
23821
  brew: {
23205
23822
  update: "brew upgrade starbridge, then starbridge setup --refresh",
@@ -23235,8 +23852,8 @@ async function restartAgent(ctx) {
23235
23852
  return;
23236
23853
  }
23237
23854
  const home = ctx.env.HOME ?? homedir5();
23238
- const linux = join12(home, ".config/systemd/user/starbridge-agent.service");
23239
- const mac = join12(home, "Library/LaunchAgents/run.starbridge.agent.plist");
23855
+ const linux = join13(home, ".config/systemd/user/starbridge-agent.service");
23856
+ const mac = join13(home, "Library/LaunchAgents/run.starbridge.agent.plist");
23240
23857
  const r = existsSync9(linux) ? sh(ctx, "systemctl", ["--user", "try-restart", "starbridge-agent.service"]) : existsSync9(mac) ? sh(ctx, "launchctl", ["kickstart", "-k", `gui/${process.getuid?.()}/run.starbridge.agent`]) : undefined;
23241
23858
  if (r === undefined)
23242
23859
  return;
@@ -23272,7 +23889,14 @@ async function update(ctx, install, pubkey = RELEASE_KEY, codexbar) {
23272
23889
  return updateCodexbar(sys, configured, codexbar);
23273
23890
  let self2 = 0;
23274
23891
  try {
23275
- await updateSelf(ctx, install, pubkey);
23892
+ if (await updateSelf(ctx, install, pubkey) && install.kind === "binary") {
23893
+ const r = spawnSync3(install.path, ["update"], {
23894
+ env: ctx.env,
23895
+ stdio: ["ignore", "inherit", "inherit"],
23896
+ timeout: 15 * 60000
23897
+ });
23898
+ return r.status ?? 1;
23899
+ }
23276
23900
  } catch (e) {
23277
23901
  if (e instanceof ReleaseError && !(e instanceof DownloadError))
23278
23902
  throw e;
@@ -23286,17 +23910,17 @@ async function update(ctx, install, pubkey = RELEASE_KEY, codexbar) {
23286
23910
  async function updateSelf(ctx, install, pubkey) {
23287
23911
  if (install.kind !== "binary") {
23288
23912
  ctx.out(`starbridge was installed with ${install.kind}: run ${MANAGED[install.kind].update}`);
23289
- return;
23913
+ return false;
23290
23914
  }
23291
23915
  const releases = ctx.env.STARBRIDGE_RELEASES_URL ?? RELEASES_URL;
23292
23916
  const latest = await latestVersion(releases);
23293
23917
  if (compareVersions(latest, VERSION) <= 0) {
23294
23918
  ctx.out(`starbridge ${VERSION} is up to date.`);
23295
- return;
23919
+ return false;
23296
23920
  }
23297
23921
  const bytes = await downloadVerified(latest, platformAsset(), { releases, pubkey });
23298
- const next = join12(dirname8(install.path), ".starbridge.new");
23299
- writeFileSync8(next, bytes, { mode: 493 });
23922
+ const next = join13(dirname8(install.path), ".starbridge.new");
23923
+ writeFileSync9(next, bytes, { mode: 493 });
23300
23924
  chmodSync3(next, 493);
23301
23925
  replaceBinary(next, install.path);
23302
23926
  ctx.out(`Updated starbridge ${VERSION} to ${latest} in ${install.path}.`);
@@ -23315,28 +23939,29 @@ async function updateSelf(ctx, install, pubkey) {
23315
23939
  }
23316
23940
  updatePlugins(ctx);
23317
23941
  movePiPackage(ctx, latest);
23942
+ return true;
23318
23943
  }
23319
23944
  function replaceBinary(next, target, platform = process.platform) {
23320
23945
  if (platform !== "win32")
23321
- return renameSync4(next, target);
23946
+ return renameSync5(next, target);
23322
23947
  let aside = `${target}.old`;
23323
23948
  try {
23324
- rmSync6(aside, { force: true });
23949
+ rmSync7(aside, { force: true });
23325
23950
  } catch {
23326
23951
  aside = `${target}.${process.pid}.old`;
23327
23952
  }
23328
- renameSync4(target, aside);
23953
+ renameSync5(target, aside);
23329
23954
  try {
23330
- renameSync4(next, target);
23955
+ renameSync5(next, target);
23331
23956
  } catch (e) {
23332
- renameSync4(aside, target);
23957
+ renameSync5(aside, target);
23333
23958
  throw e;
23334
23959
  }
23335
23960
  const dir = dirname8(target);
23336
- const leftover = (f) => f.startsWith(`${basename5(target)}.`) && f.endsWith(".old");
23337
- for (const f of readdirSync5(dir).filter(leftover))
23961
+ const leftover = (f) => f.startsWith(`${basename4(target)}.`) && f.endsWith(".old");
23962
+ for (const f of readdirSync6(dir).filter(leftover))
23338
23963
  try {
23339
- rmSync6(join12(dir, f), { force: true });
23964
+ rmSync7(join13(dir, f), { force: true });
23340
23965
  } catch {}
23341
23966
  }
23342
23967
  function removeBinary(ctx, install) {
@@ -23346,7 +23971,7 @@ function removeBinary(ctx, install) {
23346
23971
  }
23347
23972
  if (process.platform === "win32")
23348
23973
  return removeRunningExe(ctx, install.path);
23349
- rmSync6(install.path, { force: true });
23974
+ rmSync7(install.path, { force: true });
23350
23975
  ctx.out(`Removed ${install.path}.`);
23351
23976
  return 0;
23352
23977
  }
@@ -23356,7 +23981,7 @@ function removeRunningExe(ctx, path) {
23356
23981
  return 0;
23357
23982
  }
23358
23983
  const del = `ping -n 3 127.0.0.1 >nul & del /f /q "${path}" "${path}.old" "${path}.*.old"`;
23359
- spawn6(ctx.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${del}"`], {
23984
+ spawn7(ctx.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${del}"`], {
23360
23985
  detached: true,
23361
23986
  stdio: "ignore",
23362
23987
  windowsHide: true,
@@ -23403,7 +24028,7 @@ async function uninstall(sys, opts) {
23403
24028
  ctx.out(`Kept ${dir}: a service still uses it. Stop it, then rerun \`starbridge uninstall\`.`);
23404
24029
  } else if (existsSync10(dir)) {
23405
24030
  if (opts.purge || await prompt.confirm(`Delete ${dir} (this machine's keys and state)?`, false)) {
23406
- rmSync7(dir, { recursive: true, force: true });
24031
+ rmSync8(dir, { recursive: true, force: true });
23407
24032
  ctx.out(`Deleted ${dir}.`);
23408
24033
  } else
23409
24034
  ctx.out(`Kept ${dir}: \`starbridge setup\` reuses it.`);
@@ -23412,8 +24037,8 @@ async function uninstall(sys, opts) {
23412
24037
  ctx.out(`Revoke "${machine.name}" under Devices in the Starbridge app or web page.`);
23413
24038
  const cb = findCodexbar(sys);
23414
24039
  if (cb) {
23415
- const opt = join13(sys.home, ".local/opt/codexbar");
23416
- const how = cb.inApp ? "brew uninstall --cask codexbar (or drag CodexBar.app to the Bin)" : realpathSync5(cb.path).startsWith(opt) ? `rm -rf ${opt} ${join13(sys.home, ".local/bin/codexbar")}` : "your package manager";
24040
+ const opt = join14(sys.home, ".local/opt/codexbar");
24041
+ const how = cb.inApp ? "brew uninstall --cask codexbar (or drag CodexBar.app to the Bin)" : realpathSync5(cb.path).startsWith(opt) ? `rm -rf ${opt} ${join14(sys.home, ".local/bin/codexbar")}` : "your package manager";
23417
24042
  ctx.out(`CodexBar stays installed (${cb.path}); remove it with ${how}.`);
23418
24043
  }
23419
24044
  if (opts.install && stopped)
@@ -23468,7 +24093,10 @@ var HELP = `starbridge: post decisions to your devices, report runs, upload quot
23468
24093
  setup leaves it out until \`setup --agent\` brings it back.
23469
24094
 
23470
24095
  starbridge pair [--server <url>] [--name <name>] [--force]
23471
- Make this machine's keys and print a pairing code to type on a device.
24096
+ Make this machine's keys and print a pairing code to type on a device. Unless the
24097
+ Starbridge Android app scanned the QR code, confirm that its check code matches.
24098
+ starbridge pair --confirm | --reject
24099
+ Answer that for a waiting \`pair\` or \`setup\` that has no terminal.
23472
24100
  --server <url> a self-hosted server (default: $STARBRIDGE_SERVER, else
23473
24101
  https://starbridge.run)
23474
24102
 
@@ -23559,24 +24187,29 @@ var HELP = `starbridge: post decisions to your devices, report runs, upload quot
23559
24187
  The commands above go through it when it runs, and to the server directly when not
23560
24188
  (or with STARBRIDGE_NO_AGENT=1).
23561
24189
 
23562
- starbridge config [permissions on|off] [machine-kind server|desktop|laptop|cloud]
24190
+ starbridge config [permissions on|off] [presence on|off] [machine-kind server|desktop|laptop|cloud]
23563
24191
  Print this machine's settings, or change one. permissions: send its Claude Code
23564
24192
  permission prompts to your devices, where they can be allowed or denied; the prompt
23565
24193
  stays open at the keyboard and the first answer wins. Off by default; while off, the
23566
- starbridge plugin's permission hook exits at once. machine-kind: the icon devices
23567
- show, detected by setup.
24194
+ starbridge plugin's permission hook exits at once. presence: while this machine's
24195
+ screen is unlocked and had keyboard or mouse input in the last minute, notifications
24196
+ on your other devices wait a few seconds, so a question you answer here doesn't buzz
24197
+ your phone; the server hears only yes or no. Off by default. machine-kind: the icon
24198
+ devices show, detected by setup.
23568
24199
 
23569
24200
  starbridge hook permission --agent claude-code|pi|opencode [--wait 570s]
23570
24201
  starbridge hook settle --agent claude-code
23571
- For Claude Code's PermissionRequest hook, and for its PostToolUse, PermissionDenied,
23572
- Stop and SessionEnd hooks: hook JSON on stdin; prints the hook's decision, or nothing
24202
+ For Claude Code's PermissionRequest hook, and for its PostToolUse, PostToolUseFailure,
24203
+ PermissionDenied, Stop and SessionEnd hooks: hook JSON on stdin; prints the hook's decision, or nothing
23573
24204
  to leave the prompt to the keyboard. The Starbridge Pi extension runs it with --agent pi
23574
- for pi-permission-system's prompts, the opencode plugin with --agent opencode.
24205
+ for pi-permission-system's prompts, the opencode plugin with --agent opencode. On
24206
+ Claude Code's AskUserQuestion it posts each question to your devices, whatever the
24207
+ permissions setting, and prints the first device answers as the picker's; an answer
24208
+ in the picker settles them.
23575
24209
 
23576
24210
  starbridge hook ask-user
23577
- For Claude Code's PreToolUse hook on AskUserQuestion: hook JSON on stdin; answers each
23578
- question by telling the agent to use \`starbridge ask\`; prints nothing, which lets it
23579
- through, when this machine is not paired or the server does not answer.
24211
+ For Claude Code's PreToolUse hook on AskUserQuestion, from older plugins: prints
24212
+ nothing, so the picker opens and hook permission races it.
23580
24213
 
23581
24214
  starbridge hook question --agent opencode
23582
24215
  For the Starbridge opencode plugin, on each call of opencode's question tool: posts each
@@ -23586,9 +24219,9 @@ var HELP = `starbridge: post decisions to your devices, report runs, upload quot
23586
24219
 
23587
24220
  starbridge update [--codexbar <version>]
23588
24221
  Install the latest release once its signature checks out, then \`setup --refresh\` (brew
23589
- and npm installs: use their manager, then \`starbridge setup --refresh\`); then CodexBar's
23590
- latest release if setup installed it. --codexbar installs that CodexBar release instead,
23591
- and only that.
24222
+ and npm installs: use their manager, then \`starbridge setup --refresh\`); then the
24223
+ CodexBar release this Starbridge release pins, if setup installed CodexBar. --codexbar
24224
+ installs that CodexBar release instead, and only that; update keeps it if it is newer.
23592
24225
 
23593
24226
  starbridge --version
23594
24227
 
@@ -23610,7 +24243,7 @@ function parseSessionLink(text) {
23610
24243
  }
23611
24244
  function readText3(path) {
23612
24245
  try {
23613
- return readFileSync13(path === "-" ? 0 : path, "utf8");
24246
+ return readFileSync15(path === "-" ? 0 : path, "utf8");
23614
24247
  } catch (e) {
23615
24248
  throw new UsageError(`cannot read ${path === "-" ? "stdin" : path}: ${e.message}`);
23616
24249
  }
@@ -23644,9 +24277,13 @@ async function run2(argv, ctx) {
23644
24277
  options: {
23645
24278
  server: { type: "string" },
23646
24279
  name: { type: "string" },
23647
- force: { type: "boolean" }
24280
+ force: { type: "boolean" },
24281
+ confirm: { type: "boolean" },
24282
+ reject: { type: "boolean" }
23648
24283
  }
23649
24284
  });
24285
+ if (values.confirm || values.reject)
24286
+ return sendConfirm(ctx, !values.reject);
23650
24287
  return await pair(ctx, values);
23651
24288
  }
23652
24289
  case "ask": {
@@ -23892,7 +24529,7 @@ async function run2(argv, ctx) {
23892
24529
  if (sub === "settle")
23893
24530
  return await hookSettle(ctx, readText3("-"), values);
23894
24531
  if (sub === "ask-user")
23895
- return await hookAskUser(ctx, readText3("-"));
24532
+ return hookAskUser();
23896
24533
  if (sub === "question")
23897
24534
  return await hookQuestion(ctx, readText3("-"), values);
23898
24535
  throw new UsageError("usage: starbridge hook permission|settle --agent claude-code, starbridge hook ask-user, or starbridge hook question --agent opencode");