starbridge 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.2",
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)
@@ -16860,12 +16946,12 @@ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSyn
16860
16946
  import { dirname as dirname2, join as join7 } from "node:path";
16861
16947
 
16862
16948
  // ../plugin/hooks/rule.md
16863
- var rule_default = "I am often away from this terminal and may not read your final message for hours. Reach me through the `starbridge` skill, not here or with AskUserQuestion, when you need a decision that is mine, and before you end a turn on work that waits on me, such as a PR to review or merge, or a failure only I can fix. Decide everything else yourself and keep working. Ask here only when `starbridge` fails.\n\nRun a command that blocks me (takes over the screen, keyboard or session, or holds a device I use), or that my instructions ask you to report, whole through `starbridge run`.\n";
16949
+ var rule_default = "I am often away from this terminal and may not read your final message for hours. Reach me through the `starbridge` skill, not here, when you need a decision that is mine, and before you end a turn on work that waits on me, such as a PR to review or merge, or a failure only I can fix. In Claude Code, a quick question may use AskUserQuestion instead, since it reaches my devices too. Decide everything else yourself and keep working. Ask here only when `starbridge` fails.\n\nRun a command that blocks me (takes over the screen, keyboard or session, or holds a device I use), or that my instructions ask you to report, whole through `starbridge run`.\n";
16864
16950
 
16865
16951
  // ../plugin/skills/starbridge/SKILL.md
16866
16952
  var SKILL_default = `---
16867
16953
  name: starbridge
16868
- description: "Reach your user through Starbridge: a card on their phone that they answer with one tap, sent back into this session. Use it, instead of asking in chat or with AskUserQuestion, whenever you need a decision that is theirs, and before ending a turn on work that waits on them (a PR to review or merge, a failure only they can fix). Wrap in \`starbridge run\`, unasked, any command that blocks them or that their instructions ask you to report."
16954
+ description: "Reach your user through Starbridge: a card on their phone that they answer with one tap, sent back into this session. Use it, instead of asking in chat, whenever you need a decision that is theirs, and before ending a turn on work that waits on them (a PR to review or merge, a failure only they can fix). In Claude Code, a quick question may use AskUserQuestion, which reaches their devices too. Wrap in \`starbridge run\`, unasked, any command that blocks them or that their instructions ask you to report."
16869
16955
  compatibility: The \`starbridge\` CLI on PATH, paired with \`starbridge pair\`.
16870
16956
  ---
16871
16957
 
@@ -16888,8 +16974,10 @@ their phone; they read it cold, between other things, and tap an option.
16888
16974
  report: wrap it in \`starbridge run\` (below).
16889
16975
 
16890
16976
  Decide everything else yourself and say what you did in your final message.
16891
- Ask one question in one place: never also in the terminal or with
16892
- \`AskUserQuestion\`. Ask in the terminal only when \`starbridge\` fails (not
16977
+ Ask one question in one place, never also in the terminal. In Claude Code,
16978
+ \`AskUserQuestion\` reaches their devices too, so a quick question may use it;
16979
+ post a card for a decision they will read cold, or that needs links or
16980
+ images. Ask in the terminal only when \`starbridge\` fails (not
16893
16981
  installed, not paired, an error), and say that it failed. A card cannot
16894
16982
  approve an action your guidelines say needs the user's yes in this chat
16895
16983
  (account settings, external messages, purchases, deleting data), since its
@@ -16993,6 +17081,11 @@ starbridge run --title 'Mac e2e' --reason 'takes over your screen and keyboard'
16993
17081
 
16994
17082
  Wrap the whole command, chained or not. \`--reason\` is required: what it
16995
17083
  blocks, or the rule it matches. Output and exit code are the command's own.
17084
+
17085
+ In Codex, ask to run \`starbridge run\` outside the sandbox on the first call
17086
+ (escalated permissions, justified by the network the report needs). In the
17087
+ sandbox the command runs but no device hears of it, and it runs again once
17088
+ the escalation is approved.
16996
17089
  `;
16997
17090
 
16998
17091
  // src/setup/marker.ts
@@ -17027,7 +17120,7 @@ import { configDir } from "./poller.ts";
17027
17120
 
17028
17121
  /** The agent API revision this mod speaks, sent in the \`starbridge-api\` header. */
17029
17122
  export const API = 1;
17030
- export const VERSION = "0.1.0";
17123
+ export const VERSION = "0.1.2";
17031
17124
  const CLIENT = \`starbridge-mod/\${VERSION}\`;
17032
17125
 
17033
17126
  /** Event types this mod submits; others are skipped and left unconfirmed, as the API asks. */
@@ -17817,6 +17910,19 @@ var switch_default = `/**
17817
17910
  import type { AgentLoop } from "./agent.ts";
17818
17911
  import type { Poller } from "./poller.ts";
17819
17912
 
17913
+ /** Entry points of a run that ends with its last turn: \`claude -p\` and Agent SDK scripts. */
17914
+ const ONE_SHOT = new Set(["sdk-cli", "sdk-ts", "sdk-py"]);
17915
+
17916
+ /**
17917
+ * Whether a session gets an answer loop: one under the REPL, or one a host such as Claude
17918
+ * desktop's Code tab or an IDE keeps open for a person (#863). The engine calls those not
17919
+ * interactive, as it does a \`claude -p\` run, so the host's \`CLAUDE_CODE_ENTRYPOINT\` tells them
17920
+ * apart; a run with none set is \`claude -p\`'s own.
17921
+ */
17922
+ export function wantsLoop(isInteractive: boolean, entrypoint: string | undefined): boolean {
17923
+ return isInteractive || (!!entrypoint && !ONE_SHOT.has(entrypoint));
17924
+ }
17925
+
17820
17926
  export interface Paths {
17821
17927
  /** True when an agent answers \`GET /v1/status\` with a 2xx. */
17822
17928
  agentUp(): Promise<boolean>;
@@ -18524,6 +18630,7 @@ function run(sys, cmd, args, opts = {}) {
18524
18630
  const child = spawn3(start.file, start.args, {
18525
18631
  windowsVerbatimArguments: start.windowsVerbatimArguments,
18526
18632
  env: { ...sys.ctx.env, ...opts.env },
18633
+ ...opts.cwd ? { cwd: opts.cwd } : {},
18527
18634
  stdio: [
18528
18635
  opts.input === undefined ? "ignore" : "pipe",
18529
18636
  opts.inherit ? "inherit" : "pipe",
@@ -18649,6 +18756,7 @@ var PI_PACKAGE = piSource(VERSION);
18649
18756
  function hasPi(sys) {
18650
18757
  return which(sys.ctx.env, "pi") !== undefined;
18651
18758
  }
18759
+ var STARBRIDGE_PI = /github\.com[/:]T0mSIlver\/starbridge(?:\.git)?\/?(?:@.*)?$/i;
18652
18760
  function piPackage(sys) {
18653
18761
  const dir = sys.ctx.env.PI_CODING_AGENT_DIR || join7(sys.home, ".pi", "agent");
18654
18762
  let packages;
@@ -18661,7 +18769,7 @@ function piPackage(sys) {
18661
18769
  return;
18662
18770
  for (const p of packages) {
18663
18771
  const source = typeof p === "string" ? p : p?.source;
18664
- if (typeof source === "string" && /github\.com[/:]T0mSIlver\/starbridge\b/i.test(source))
18772
+ if (typeof source === "string" && STARBRIDGE_PI.test(source))
18665
18773
  return source;
18666
18774
  }
18667
18775
  return;
@@ -19561,16 +19669,234 @@ class Permissions {
19561
19669
  }
19562
19670
  }
19563
19671
 
19672
+ // src/screen.ts
19673
+ import { execFile, spawn as spawn4 } from "node:child_process";
19674
+ import { createInterface as createInterface2 } from "node:readline";
19675
+ var TIMEOUT_MS = 5000;
19676
+ var exec = (cmd, args) => new Promise((resolve) => {
19677
+ execFile(cmd, args, { timeout: TIMEOUT_MS, windowsHide: true }, (err, stdout) => resolve(err ? undefined : String(stdout)));
19678
+ });
19679
+ function macIdleMs(ioreg) {
19680
+ const m = /"HIDIdleTime"\s*=\s*(\d+)/.exec(ioreg);
19681
+ return m ? Number(m[1]) / 1e6 : undefined;
19682
+ }
19683
+ function macLocked(ioreg) {
19684
+ return /"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(ioreg);
19685
+ }
19686
+ async function readMac(run) {
19687
+ const [hid, root] = await Promise.all([
19688
+ run("ioreg", ["-c", "IOHIDSystem", "-d", "4", "-r", "-k", "HIDIdleTime"]),
19689
+ run("ioreg", ["-n", "Root", "-d1"])
19690
+ ]);
19691
+ const idleMs = hid === undefined ? undefined : macIdleMs(hid);
19692
+ if (idleMs === undefined || root === undefined)
19693
+ return;
19694
+ return { locked: macLocked(root), idleMs };
19695
+ }
19696
+ function properties(text) {
19697
+ const out = {};
19698
+ for (const line of text.split(`
19699
+ `)) {
19700
+ const i = line.indexOf("=");
19701
+ if (i > 0)
19702
+ out[line.slice(0, i)] = line.slice(i + 1).trim();
19703
+ }
19704
+ return out;
19705
+ }
19706
+ function graphical(sessions) {
19707
+ return sessions.map(properties).filter((p) => p.Active === "yes" && (p.Type === "x11" || p.Type === "wayland") && p.Class === "user").map((p) => ({ locked: p.LockedHint === "yes" }));
19708
+ }
19709
+ function linuxIdleMs(text) {
19710
+ const m = /^\(?(?:uint64\s+)?(\d+),?\)?\s*$/.exec(text.trim());
19711
+ return m ? Number(m[1]) : undefined;
19712
+ }
19713
+ async function readLinux(run) {
19714
+ const list = await run("loginctl", ["list-sessions", "--no-legend", "--no-pager"]);
19715
+ const ids = (list ?? "").split(`
19716
+ `).map((l) => l.trim().split(/\s+/)[0]).filter((id) => !!id);
19717
+ const shown = await Promise.all(ids.map((id) => run("loginctl", [
19718
+ "show-session",
19719
+ id,
19720
+ "-p",
19721
+ "Type",
19722
+ "-p",
19723
+ "Active",
19724
+ "-p",
19725
+ "Class",
19726
+ "-p",
19727
+ "LockedHint"
19728
+ ])));
19729
+ const screens = graphical(shown.filter((s) => s !== undefined));
19730
+ if (screens.length === 0)
19731
+ return;
19732
+ const idle = await run("gdbus", [
19733
+ "call",
19734
+ "--session",
19735
+ "--dest",
19736
+ "org.gnome.Mutter.IdleMonitor",
19737
+ "--object-path",
19738
+ "/org/gnome/Mutter/IdleMonitor/Core",
19739
+ "--method",
19740
+ "org.gnome.Mutter.IdleMonitor.GetIdletime"
19741
+ ]) ?? await run("xprintidle", []);
19742
+ const idleMs = idle === undefined ? undefined : linuxIdleMs(idle);
19743
+ if (idleMs === undefined)
19744
+ return;
19745
+ return { locked: screens.every((s) => s.locked), idleMs };
19746
+ }
19747
+ var WINDOWS_SCRIPT = `
19748
+ Add-Type @'
19749
+ using System; using System.Runtime.InteropServices;
19750
+ public static class Idle {
19751
+ [StructLayout(LayoutKind.Sequential)] struct LASTINPUTINFO { public uint cbSize; public uint dwTime; }
19752
+ [DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
19753
+ public static long Ms() {
19754
+ var i = new LASTINPUTINFO(); i.cbSize = (uint)Marshal.SizeOf(i);
19755
+ if (!GetLastInputInfo(ref i)) return -1;
19756
+ return (long)unchecked((uint)Environment.TickCount - i.dwTime);
19757
+ }
19758
+ }
19759
+ '@
19760
+ $session = (Get-Process -Id $PID).SessionId
19761
+ while ($null -ne [Console]::In.ReadLine()) {
19762
+ $locked = @(Get-Process LogonUI -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $session }).Count -gt 0
19763
+ [Console]::Out.WriteLine("$([Idle]::Ms()) $locked")
19764
+ }
19765
+ `;
19766
+ function windowsScreen(line) {
19767
+ const m = /^(-?\d+) (True|False)$/.exec(line.trim());
19768
+ if (!m || Number(m[1]) < 0)
19769
+ return;
19770
+ return { idleMs: Number(m[1]), locked: m[2] === "True" };
19771
+ }
19772
+
19773
+ class WindowsReader {
19774
+ child;
19775
+ pending = [];
19776
+ read() {
19777
+ const child = this.start();
19778
+ return new Promise((resolve) => {
19779
+ const timer = setTimeout(() => done(undefined), TIMEOUT_MS);
19780
+ const done = (line) => {
19781
+ clearTimeout(timer);
19782
+ resolve(line === undefined ? undefined : windowsScreen(line));
19783
+ };
19784
+ this.pending.push(done);
19785
+ child.stdin?.write(`
19786
+ `);
19787
+ });
19788
+ }
19789
+ start() {
19790
+ if (this.child && this.child.exitCode === null)
19791
+ return this.child;
19792
+ const child = spawn4("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "-"], { stdio: ["pipe", "pipe", "ignore"], windowsHide: true });
19793
+ child.stdin?.write(`${WINDOWS_SCRIPT}
19794
+ `);
19795
+ createInterface2({ input: child.stdout }).on("line", (line) => this.pending.shift()?.(line));
19796
+ const gone = () => {
19797
+ if (this.child === child)
19798
+ this.child = undefined;
19799
+ for (const p of this.pending.splice(0))
19800
+ p(undefined);
19801
+ };
19802
+ child.on("exit", gone);
19803
+ child.on("error", gone);
19804
+ this.child = child;
19805
+ return child;
19806
+ }
19807
+ stop() {
19808
+ this.child?.kill();
19809
+ this.child = undefined;
19810
+ }
19811
+ }
19812
+ function screenReader(platform = process.platform, run = exec) {
19813
+ if (platform === "darwin")
19814
+ return { read: () => readMac(run), stop: () => {} };
19815
+ if (platform === "linux")
19816
+ return { read: () => readLinux(run), stop: () => {} };
19817
+ if (platform === "win32") {
19818
+ const reader = new WindowsReader;
19819
+ return { read: () => reader.read(), stop: () => reader.stop() };
19820
+ }
19821
+ return { read: async () => {
19822
+ return;
19823
+ }, stop: () => {} };
19824
+ }
19825
+
19826
+ // src/agent/presence.ts
19827
+ var READ_MS = 1e4;
19828
+ function presenceEnabled(hub) {
19829
+ return hub.ctx.store.agentConfig().presence?.enabled === true;
19830
+ }
19831
+
19832
+ class Presence2 {
19833
+ hub;
19834
+ reader;
19835
+ sent;
19836
+ lastError;
19837
+ constructor(hub, reader = screenReader()) {
19838
+ this.hub = hub;
19839
+ this.reader = reader;
19840
+ }
19841
+ status(into) {
19842
+ into.presence = {
19843
+ enabled: presenceEnabled(this.hub),
19844
+ present: this.sent?.present === true,
19845
+ ...this.lastError ? { lastError: this.lastError } : {}
19846
+ };
19847
+ }
19848
+ async send(present, signal) {
19849
+ const ctx = this.hub.ctx;
19850
+ if (!ctx.store.machine())
19851
+ return;
19852
+ try {
19853
+ await session(ctx).api.presence(present, signal ?? AbortSignal.timeout(1e4));
19854
+ this.sent = { present, at: Date.now() };
19855
+ this.lastError = undefined;
19856
+ } catch (e) {
19857
+ this.lastError = e.message;
19858
+ }
19859
+ }
19860
+ async tick(now = Date.now()) {
19861
+ const screen = presenceEnabled(this.hub) ? await this.reader.read() : undefined;
19862
+ const present = screen !== undefined && isPresent(screen);
19863
+ if (present) {
19864
+ if (!this.sent?.present || now - this.sent.at >= PRESENCE_BEAT_MS)
19865
+ await this.send(true);
19866
+ } else if (this.sent?.present)
19867
+ await this.send(false);
19868
+ }
19869
+ async run(signal) {
19870
+ while (!signal.aborted) {
19871
+ await this.tick();
19872
+ await pause(READ_MS, signal);
19873
+ }
19874
+ if (this.sent?.present)
19875
+ await this.send(false, AbortSignal.timeout(3000));
19876
+ this.reader.stop();
19877
+ }
19878
+ }
19879
+
19564
19880
  // src/quota.ts
19565
19881
  import { randomBytes as randomBytes6 } from "node:crypto";
19566
19882
 
19567
19883
  // src/codexbar.ts
19568
- import { spawn as spawn4 } from "node:child_process";
19884
+ import { spawn as spawn5 } from "node:child_process";
19569
19885
  var RUN_TIMEOUT_MS = 120000;
19570
19886
  function runCodexbar(bin, provider, timeoutMs = RUN_TIMEOUT_MS) {
19571
19887
  const args = ["usage", "--format", "json", ...provider ? ["--provider", provider] : []];
19572
19888
  return new Promise((resolve) => {
19573
- const child = spawn4(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
19889
+ let start;
19890
+ try {
19891
+ start = spawnable(bin, args);
19892
+ } catch (e) {
19893
+ return resolve({ code: null, stdout: "", stderr: e.message });
19894
+ }
19895
+ const child = spawn5(start.file, start.args, {
19896
+ windowsVerbatimArguments: start.windowsVerbatimArguments,
19897
+ windowsHide: true,
19898
+ stdio: ["ignore", "pipe", "pipe"]
19899
+ });
19574
19900
  let stdout = "";
19575
19901
  let stderr = "";
19576
19902
  child.stdout.on("data", (d) => {
@@ -19579,7 +19905,7 @@ function runCodexbar(bin, provider, timeoutMs = RUN_TIMEOUT_MS) {
19579
19905
  child.stderr.on("data", (d) => {
19580
19906
  stderr += d;
19581
19907
  });
19582
- const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
19908
+ const timer = setTimeout(() => killTree(child), timeoutMs);
19583
19909
  child.on("error", (e) => {
19584
19910
  clearTimeout(timer);
19585
19911
  resolve({ code: null, stdout, stderr: e.message });
@@ -19599,6 +19925,11 @@ function isoOrNull(x) {
19599
19925
  const t = Date.parse(x);
19600
19926
  return Number.isNaN(t) ? null : `${new Date(t).toISOString().slice(0, 19)}Z`;
19601
19927
  }
19928
+ function named(label, minutes) {
19929
+ if (label !== "Session" || minutes === null || minutes >= 24 * 60 || minutes % 60 !== 0)
19930
+ return label;
19931
+ return `${minutes / 60}-hour`;
19932
+ }
19602
19933
  function window2(id, label, raw, now) {
19603
19934
  if (!isObj2(raw))
19604
19935
  return;
@@ -19606,11 +19937,12 @@ function window2(id, label, raw, now) {
19606
19937
  if (typeof used !== "number" || !Number.isFinite(used) || used < 0)
19607
19938
  return;
19608
19939
  const minutes = raw.windowMinutes;
19940
+ const windowMinutes = typeof minutes === "number" && Number.isFinite(minutes) && minutes >= 1 ? Math.round(minutes) : null;
19609
19941
  const w = {
19610
19942
  id: clip(id, 100),
19611
- label: clip(label, 100),
19943
+ label: clip(named(label, windowMinutes), 100),
19612
19944
  usedPercent: used,
19613
- windowMinutes: typeof minutes === "number" && Number.isFinite(minutes) && minutes >= 1 ? Math.round(minutes) : null,
19945
+ windowMinutes,
19614
19946
  resetsAt: isoOrNull(raw.resetsAt)
19615
19947
  };
19616
19948
  return { ...w, pace: computePace(w, now) };
@@ -19935,7 +20267,7 @@ class Quota {
19935
20267
  }
19936
20268
 
19937
20269
  // src/run.ts
19938
- import { spawn as spawn5 } from "node:child_process";
20270
+ import { spawn as spawn6 } from "node:child_process";
19939
20271
  import { randomBytes as randomBytes7 } from "node:crypto";
19940
20272
  import { constants as constants2 } from "node:os";
19941
20273
  var PROGRESS_MS = 1e4;
@@ -20039,6 +20371,8 @@ class Reporter {
20039
20371
  ctx;
20040
20372
  timing;
20041
20373
  progress;
20374
+ progressSent = false;
20375
+ dirty = false;
20042
20376
  exit;
20043
20377
  lastPost = Number.NEGATIVE_INFINITY;
20044
20378
  inflight;
@@ -20060,7 +20394,8 @@ class Reporter {
20060
20394
  if (JSON.stringify(next) === JSON.stringify(this.progress))
20061
20395
  return;
20062
20396
  this.progress = next;
20063
- this.schedule(this.timing.progressMs);
20397
+ this.dirty = true;
20398
+ this.schedule(this.progressGap());
20064
20399
  }
20065
20400
  async finish(code) {
20066
20401
  this.exit = { code, at: iso2(this.ctx.now()) };
@@ -20074,6 +20409,9 @@ class Reporter {
20074
20409
  await Promise.race([settled, new Promise((r) => t = setTimeout(r, FINAL_MS))]);
20075
20410
  clearTimeout(t);
20076
20411
  }
20412
+ progressGap() {
20413
+ return this.progressSent ? this.timing.progressMs : 0;
20414
+ }
20077
20415
  schedule(gap) {
20078
20416
  if (this.off || this.exit)
20079
20417
  return;
@@ -20091,11 +20429,14 @@ class Reporter {
20091
20429
  }
20092
20430
  const input = {
20093
20431
  ...this.base,
20094
- at: iso2(this.ctx.now()),
20432
+ at: this.ctx.now().toISOString(),
20095
20433
  ...this.progress ? { progress: this.progress } : {},
20096
20434
  ...this.exit ? { exit: this.exit } : {}
20097
20435
  };
20098
20436
  this.lastPost = this.ctx.now().getTime();
20437
+ if (this.progress)
20438
+ this.progressSent = true;
20439
+ this.dirty = false;
20099
20440
  this.inflight = this.post(input).catch((e) => {
20100
20441
  if (e instanceof UsageError && !(e instanceof WithheldError))
20101
20442
  this.off = true;
@@ -20108,7 +20449,7 @@ class Reporter {
20108
20449
  this.queued = false;
20109
20450
  this.send();
20110
20451
  } else
20111
- this.schedule(this.timing.heartbeatMs);
20452
+ this.schedule(this.dirty ? this.progressGap() : this.timing.heartbeatMs);
20112
20453
  });
20113
20454
  }
20114
20455
  }
@@ -20156,7 +20497,7 @@ async function runCommand(ctx, opts) {
20156
20497
  }, opts.post ?? makePoster(ctx), ctx);
20157
20498
  const stdout = opts.stdout ?? process.stdout;
20158
20499
  const stderr = opts.stderr ?? process.stderr;
20159
- const child = spawn5(start.file, start.args, {
20500
+ const child = spawn6(start.file, start.args, {
20160
20501
  stdio: ["inherit", "pipe", "pipe"],
20161
20502
  windowsVerbatimArguments: start.windowsVerbatimArguments
20162
20503
  });
@@ -20218,7 +20559,13 @@ function makeAgent(ctx, opts = {}) {
20218
20559
  const quota = { providers, intervalMs, ...codexbar ? { codexbar } : {} };
20219
20560
  const agent = new Agent2(ctx, socket, (hub) => {
20220
20561
  const q = new Quota(hub, quota);
20221
- return [new Decisions(hub, (why) => q.now(why)), q, new Permissions(hub), new Runs(hub)];
20562
+ return [
20563
+ new Decisions(hub, (why) => q.now(why)),
20564
+ q,
20565
+ new Permissions(hub),
20566
+ new Runs(hub),
20567
+ new Presence2(hub)
20568
+ ];
20222
20569
  });
20223
20570
  return agent;
20224
20571
  }
@@ -20260,6 +20607,109 @@ async function runAgent(ctx, opts) {
20260
20607
 
20261
20608
  // src/hook.ts
20262
20609
  import { basename as basename3 } from "node:path";
20610
+
20611
+ // src/ran.ts
20612
+ import { execFile as execFile2 } from "node:child_process";
20613
+ import { readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
20614
+ var RUN_CHECK_MS = 500;
20615
+ var SHELLS = new Set(["sh", "dash", "bash", "zsh", "ash", "ksh", "fish", "busybox"]);
20616
+ function evalForm(command) {
20617
+ return `eval '${command.replaceAll("'", `'"'"'`)}'`;
20618
+ }
20619
+ function program(p) {
20620
+ return (p.argv[0] ?? "").split(/\s/)[0]?.split("/").pop() ?? "";
20621
+ }
20622
+ function runs(p, command) {
20623
+ const quoted = evalForm(command);
20624
+ return SHELLS.has(program(p)) && p.argv.some((a) => a.includes(quoted));
20625
+ }
20626
+ function descends(pid, root, byPid) {
20627
+ const seen = new Set;
20628
+ for (let p = byPid.get(pid);p && !seen.has(p.pid); p = byPid.get(p.ppid)) {
20629
+ if (p.ppid === root)
20630
+ return true;
20631
+ seen.add(p.pid);
20632
+ }
20633
+ return false;
20634
+ }
20635
+ function procLinux() {
20636
+ const out = [];
20637
+ for (const name of readdirSync3("/proc")) {
20638
+ const pid = Number(name);
20639
+ if (!Number.isInteger(pid))
20640
+ continue;
20641
+ try {
20642
+ const stat = readFileSync9(`/proc/${pid}/stat`, "utf8");
20643
+ const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
20644
+ const argv = readFileSync9(`/proc/${pid}/cmdline`, "utf8").split("\x00").filter(Boolean);
20645
+ out.push({ pid, ppid, argv });
20646
+ } catch {}
20647
+ }
20648
+ return out;
20649
+ }
20650
+ function procPs() {
20651
+ return new Promise((resolve) => execFile2("ps", ["-A", "-ww", "-o", "pid=,ppid=,args="], { maxBuffer: 16 * 1024 * 1024, timeout: 5000 }, (e, stdout) => {
20652
+ if (e)
20653
+ return resolve([]);
20654
+ resolve(stdout.split(`
20655
+ `).flatMap((line) => {
20656
+ const m = /^\s*(\d+)\s+(\d+)\s(.*)$/.exec(line);
20657
+ return m ? [{ pid: Number(m[1]), ppid: Number(m[2]), argv: [m[3]] }] : [];
20658
+ }));
20659
+ }));
20660
+ }
20661
+ async function processes(platform = process.platform) {
20662
+ if (platform === "linux")
20663
+ return procLinux();
20664
+ if (platform === "win32")
20665
+ return [];
20666
+ return procPs();
20667
+ }
20668
+ function agentPid(procs, from = process.ppid) {
20669
+ const byPid = new Map(procs.map((p) => [p.pid, p]));
20670
+ for (let pid = from, hops = 0;hops < 4; hops++) {
20671
+ const p = byPid.get(pid);
20672
+ if (!p)
20673
+ return;
20674
+ if (!SHELLS.has(program(p)))
20675
+ return pid;
20676
+ pid = p.ppid;
20677
+ }
20678
+ return;
20679
+ }
20680
+ function untilRan(signal, command, list = processes, from = process.ppid) {
20681
+ const ran = new AbortController;
20682
+ let timer;
20683
+ let stopped = false;
20684
+ const watch = async () => {
20685
+ const first = await list().catch(() => []);
20686
+ const root = agentPid(first, from);
20687
+ if (root === undefined)
20688
+ return;
20689
+ const before = new Set(first.map((p) => p.pid));
20690
+ const tick = async () => {
20691
+ if (stopped)
20692
+ return;
20693
+ const now = await list().catch(() => []);
20694
+ const byPid = new Map(now.map((p) => [p.pid, p]));
20695
+ if (now.some((p) => !before.has(p.pid) && runs(p, command) && descends(p.pid, root, byPid)))
20696
+ return ran.abort();
20697
+ timer = setTimeout(tick, RUN_CHECK_MS);
20698
+ timer.unref?.();
20699
+ };
20700
+ await tick();
20701
+ };
20702
+ watch();
20703
+ return {
20704
+ signal: signal ? AbortSignal.any([signal, ran.signal]) : ran.signal,
20705
+ stop: () => {
20706
+ stopped = true;
20707
+ clearTimeout(timer);
20708
+ }
20709
+ };
20710
+ }
20711
+
20712
+ // src/hook.ts
20263
20713
  var SLACK_MS2 = 15000;
20264
20714
  var DIRECT_POLL_SECONDS = 5;
20265
20715
  var REPORT_MS = 5000;
@@ -20288,14 +20738,22 @@ function untilOrphaned(signal, ppid = () => process.ppid) {
20288
20738
  stop: () => clearInterval(timer)
20289
20739
  };
20290
20740
  }
20291
- async function hookPermission(outer, stdin, opts) {
20741
+ async function hookPermission(outer, stdin, opts, ran = untilRan) {
20292
20742
  const watch = untilOrphaned(outer.signal);
20293
- const ctx = { ...outer, signal: watch.signal };
20743
+ let ctx = { ...outer, signal: watch.signal };
20744
+ let started;
20294
20745
  try {
20746
+ const hook = parseHook(stdin);
20747
+ if (hook.tool_name === "AskUserQuestion" && opts.agent === "claude-code")
20748
+ return await racePicker(ctx, outer, hook, opts.wait);
20295
20749
  if (!permissionsEnabled(ctx))
20296
20750
  return 0;
20297
20751
  const agent = agentName(opts.agent);
20298
- const hook = parseHook(stdin);
20752
+ const command = hook.tool_input?.command;
20753
+ if (agent === "claude-code" && hook.tool_name === "Bash" && typeof command === "string") {
20754
+ started = ran(ctx.signal, command);
20755
+ ctx = { ...ctx, signal: started.signal };
20756
+ }
20299
20757
  const waitMs = opts.wait ? parseDuration(opts.wait) : DEFAULT_WAIT_MS;
20300
20758
  const deadline = ctx.now().getTime() + waitMs;
20301
20759
  const source = permissionSource(hook, ctx.env);
@@ -20305,6 +20763,7 @@ async function hookPermission(outer, stdin, opts) {
20305
20763
  } catch (e) {
20306
20764
  ctx.err(`starbridge: permission prompt not sent: ${e.message}`);
20307
20765
  } finally {
20766
+ started?.stop();
20308
20767
  watch.stop();
20309
20768
  }
20310
20769
  return 0;
@@ -20406,7 +20865,11 @@ async function hookSettle(ctx, stdin, opts) {
20406
20865
  const st = ctx.store.state();
20407
20866
  if (ctx.store.promptsMarkStale(st))
20408
20867
  ctx.store.updateState(() => {});
20409
- if (!sessionId || waitingFor(st, sessionId).length === 0)
20868
+ if (!sessionId)
20869
+ return 0;
20870
+ if (hook.tool_name === "AskUserQuestion" || hook.tool_input === undefined)
20871
+ await settlePicker(ctx, sessionId, "elsewhere");
20872
+ if (waitingFor(ctx.store.state(), sessionId).length === 0)
20410
20873
  return 0;
20411
20874
  const inputHash = hook.tool_input !== undefined ? inputHashOf(session(ctx).keys, hook.tool_input) : undefined;
20412
20875
  if (waitingFor(ctx.store.state(), sessionId, inputHash).length === 0)
@@ -20425,41 +20888,77 @@ async function hookSettle(ctx, stdin, opts) {
20425
20888
  }
20426
20889
  return 0;
20427
20890
  }
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 {
20891
+ function pickerQuestions(input) {
20892
+ const questions = input?.questions;
20893
+ if (!Array.isArray(questions) || questions.length === 0 || !questions.every((q) => typeof q?.question === "string"))
20894
+ return;
20895
+ return questions;
20896
+ }
20897
+ function pickerOpen(st, session) {
20898
+ return Object.entries(st.asked).filter(([id, a]) => a.picker === session && !a.settled && !st.answers[id]).map(([id]) => id);
20899
+ }
20900
+ async function settlePicker(ctx, session, outcome) {
20901
+ 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}`))));
20902
+ }
20903
+ async function racePicker(ctx, outer, hook, waitText) {
20904
+ const questions = pickerQuestions(hook.tool_input);
20905
+ const sessionId = typeof hook.session_id === "string" ? hook.session_id : "";
20906
+ if (!questions || !sessionId)
20907
+ return 0;
20908
+ const s = session(ctx);
20909
+ const waitMs = waitText ? parseDuration(waitText) : DEFAULT_WAIT_MS;
20910
+ const cwd = typeof hook.cwd === "string" && hook.cwd ? hook.cwd : process.cwd();
20911
+ const deadline = AbortSignal.timeout(waitMs);
20912
+ const failed = new AbortController;
20913
+ const signal = AbortSignal.any([
20914
+ ctx.signal ?? new AbortController().signal,
20915
+ deadline,
20916
+ failed.signal
20917
+ ]);
20918
+ const results = await Promise.allSettled(questions.map(async (q) => {
20919
+ const sub = { ...ctx, signal, err: () => {} };
20920
+ const input = {
20921
+ ...questionInput({ ...q, multiple: q.multiSelect }),
20922
+ agent: "claude-code",
20923
+ session: sessionId,
20924
+ project: basename3(cwd),
20925
+ held: true,
20926
+ picker: sessionId,
20927
+ waiting: true
20928
+ };
20929
+ let id;
20930
+ try {
20931
+ ({ id } = await postDecision(sub, s, resolveSource(input, ctx.env, cwd)));
20932
+ } catch (e) {
20933
+ failed.abort();
20934
+ throw e;
20935
+ }
20936
+ while (true) {
20937
+ const lines = [];
20938
+ const code = await wait({ ...sub, out: (l) => lines.push(l) }, { id, json: true, "no-mark": true }, s);
20939
+ if (code === 0 && lines[0])
20940
+ return opencodeAnswer(JSON.parse(lines[0]), { ...q, multiple: q.multiSelect });
20941
+ if (code !== EXIT_SNOOZED)
20942
+ return;
20943
+ }
20944
+ }));
20945
+ const picked = results.map((r) => r.status === "fulfilled" ? r.value : undefined);
20946
+ const error = results.find((r) => r.status === "rejected");
20947
+ if (error && !signal.aborted)
20948
+ ctx.err(`starbridge: question not sent: ${error.reason.message}`);
20949
+ if (picked.every((p) => p !== undefined) && !signal.aborted) {
20950
+ const answers = Object.fromEntries(questions.map((q, i) => [q.question, picked[i].join(", ")]));
20951
+ const decision = {
20952
+ behavior: "allow",
20953
+ updatedInput: { ...hook.tool_input, answers }
20954
+ };
20955
+ ctx.out(JSON.stringify({ hookSpecificOutput: { hookEventName: "PermissionRequest", decision } }));
20441
20956
  return 0;
20442
20957
  }
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 }));
20958
+ await settlePicker(ctx, sessionId, outer.signal?.aborted ? "elsewhere" : "withdrawn");
20959
+ return 0;
20960
+ }
20961
+ function hookAskUser() {
20463
20962
  return 0;
20464
20963
  }
20465
20964
  var QUESTION_CHARS = 300;
@@ -20545,7 +21044,10 @@ async function hookQuestion(outer, stdin, opts) {
20545
21044
 
20546
21045
  // src/pair.ts
20547
21046
  import { randomBytes as randomBytes8 } from "node:crypto";
21047
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
20548
21048
  import { hostname } from "node:os";
21049
+ import { join as join8 } from "node:path";
21050
+ import { createInterface as createInterface3 } from "node:readline";
20549
21051
 
20550
21052
  // ../node_modules/.pnpm/uqr@0.1.3/node_modules/uqr/dist/index.mjs
20551
21053
  var QrCodeDataType = /* @__PURE__ */ ((QrCodeDataType2) => {
@@ -21148,7 +21650,7 @@ function terminalQr(text) {
21148
21650
 
21149
21651
  // src/settings.ts
21150
21652
  import { spawnSync as spawnSync2 } from "node:child_process";
21151
- import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
21653
+ import { existsSync as existsSync4, readdirSync as readdirSync4 } from "node:fs";
21152
21654
  function detectMachineKind(env, platform = process.platform) {
21153
21655
  if (env.CLAUDE_CODE_REMOTE === "true" || env.CODESPACES === "true" || env.GITPOD_WORKSPACE_ID)
21154
21656
  return "cloud";
@@ -21162,7 +21664,7 @@ function hasBattery(platform) {
21162
21664
  try {
21163
21665
  if (platform === "linux") {
21164
21666
  const dir = "/sys/class/power_supply";
21165
- return existsSync4(dir) && readdirSync3(dir).some((n) => n.startsWith("BAT"));
21667
+ return existsSync4(dir) && readdirSync4(dir).some((n) => n.startsWith("BAT"));
21166
21668
  }
21167
21669
  if (platform === "darwin")
21168
21670
  return spawnSync2("pmset", ["-g", "batt"], { encoding: "utf8" }).stdout.includes("InternalBattery");
@@ -21230,7 +21732,7 @@ async function offerPiAllow(ctx, prompt, quiet = false) {
21230
21732
  } else
21231
21733
  ctx.out(`Pi: pi-permission-system will ask before each \`starbridge ask\`; to allow them later, ${how}.`);
21232
21734
  }
21233
- var USAGE = "usage: starbridge config [permissions on|off] [machine-kind server|desktop|laptop|cloud]";
21735
+ var USAGE = "usage: starbridge config [permissions on|off] [presence on|off] [machine-kind server|desktop|laptop|cloud]";
21234
21736
  async function configCommand(ctx, args, prompt) {
21235
21737
  const [key, value, ...extra] = args;
21236
21738
  if (extra.length > 0 || key !== undefined && value === undefined)
@@ -21243,6 +21745,11 @@ async function configCommand(ctx, args, prompt) {
21243
21745
  await offerPiChain(ctx, prompt);
21244
21746
  await offerPiAllow(ctx, prompt, true);
21245
21747
  }
21748
+ } else if (key === "presence") {
21749
+ if (value !== "on" && value !== "off")
21750
+ throw new UsageError(USAGE);
21751
+ const config = ctx.store.agentConfig();
21752
+ ctx.store.saveAgentConfig({ ...config, presence: { enabled: value === "on" } });
21246
21753
  } else if (key === "machine-kind") {
21247
21754
  const kind = MachineKind.safeParse(value);
21248
21755
  if (!kind.success)
@@ -21252,6 +21759,7 @@ async function configCommand(ctx, args, prompt) {
21252
21759
  throw new UsageError(USAGE);
21253
21760
  }
21254
21761
  ctx.out(`permissions ${permissionsEnabled(ctx) ? "on" : "off"}`);
21762
+ ctx.out(`presence ${ctx.store.agentConfig().presence?.enabled ? "on" : "off"}`);
21255
21763
  ctx.out(`machine-kind ${ctx.store.agentConfig().machineKind ?? "not set"}`);
21256
21764
  return 0;
21257
21765
  }
@@ -21270,6 +21778,7 @@ async function pair(ctx, opts) {
21270
21778
  const id = `m_${randomBytes8(9).toString("base64url")}`;
21271
21779
  const name = opts.name ?? previous?.name ?? hostname();
21272
21780
  const claim = newClaimSecret();
21781
+ const checkKey = newCheckKey();
21273
21782
  const deadline = ctx.now().getTime() + CODE_LIFETIME_MS;
21274
21783
  const expired = () => new UsageError(`the pairing code expired; run \`${opts.again ?? "starbridge pair"}\` again`);
21275
21784
  let code = newPairingCode();
@@ -21294,10 +21803,10 @@ async function pair(ctx, opts) {
21294
21803
  }
21295
21804
  const link = pairingLink(server, code);
21296
21805
  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))
21806
+ ctx.out(" Scan this with the Starbridge Android app or your phone's camera:");
21807
+ for (const line of terminalQr(appPairingLink(server, code, checkKey)))
21299
21808
  ctx.out(line);
21300
- ctx.out(` Or open ${link}`);
21809
+ ctx.out(` No app? Open ${link}`);
21301
21810
  ctx.out(" Or type the code under Devices in the app or web page. It expires in 10 minutes.");
21302
21811
  let result;
21303
21812
  while (!result) {
@@ -21325,6 +21834,20 @@ async function pair(ctx, opts) {
21325
21834
  const pin = { length: approval.length, head: approval.head };
21326
21835
  const dir = verifyDirectory(entries, { account: approval.account, pin });
21327
21836
  checkJoined(dir, { id, role: "machine", ...publicKeys(keys) });
21837
+ const again = opts.again ?? "starbridge pair";
21838
+ const check = checkCode(entries, id);
21839
+ if (approval.check !== undefined) {
21840
+ if (!verifyCheckProof(entries, id, checkKey, approval.check))
21841
+ 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`);
21842
+ } else {
21843
+ const answer = await confirmCheck(ctx, check, name);
21844
+ if (ctx.signal?.aborted)
21845
+ return 130;
21846
+ if (answer === "no")
21847
+ 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`);
21848
+ if (answer === undefined)
21849
+ 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`);
21850
+ }
21328
21851
  const token = result.token;
21329
21852
  ctx.store.locked(() => {
21330
21853
  ctx.store.saveDirectory(entries);
@@ -21357,14 +21880,78 @@ async function pair(ctx, opts) {
21357
21880
  function addedAt(entries, id) {
21358
21881
  for (const e of entries) {
21359
21882
  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",
21883
+ if (body.op === "add" && body.member?.id === id) {
21884
+ const at = new Date(body.at);
21885
+ const day = at.toLocaleDateString(undefined, { month: "short", day: "numeric" });
21886
+ const time = at.toLocaleTimeString(undefined, {
21364
21887
  hour: "2-digit",
21365
21888
  minute: "2-digit",
21366
21889
  timeZoneName: "short"
21367
21890
  });
21891
+ return `${day}, ${time}`;
21892
+ }
21893
+ }
21894
+ }
21895
+ var CONFIRM_MS = 10 * 60000;
21896
+ var waitingFile = (ctx) => join8(ctx.store.dir, "pair-waiting");
21897
+ var answerFile = (ctx, code) => join8(ctx.store.dir, `pair-answer.${code}`);
21898
+ function writeWhole(path, text) {
21899
+ const tmp = `${path}.${process.pid}`;
21900
+ rmSync3(tmp, { force: true });
21901
+ writeFileSync5(tmp, text, { mode: 384, flag: "wx" });
21902
+ renameSync3(tmp, path);
21903
+ }
21904
+ function sendConfirm(ctx, same) {
21905
+ let code;
21906
+ try {
21907
+ code = readFileSync10(waitingFile(ctx), "utf8").trim();
21908
+ } catch {
21909
+ throw new UsageError("no `starbridge pair` is waiting for its check code to be confirmed");
21910
+ }
21911
+ if (!/^[0-9A-Z]{4}(-[0-9A-Z]{4}){3}$/.test(code))
21912
+ throw new UsageError("no `starbridge pair` is waiting for its check code to be confirmed");
21913
+ writeWhole(answerFile(ctx, code), same ? "yes" : "no");
21914
+ ctx.out(`${same ? "Confirmed" : "Rejected"} check code ${code} for the waiting \`starbridge pair\`.`);
21915
+ return 0;
21916
+ }
21917
+ async function confirmCheck(ctx, code, name) {
21918
+ const file = answerFile(ctx, code);
21919
+ rmSync3(file, { force: true });
21920
+ mkdirSync6(ctx.store.dir, { recursive: true, mode: 448 });
21921
+ writeWhole(waitingFile(ctx), code);
21922
+ const tty = process.stdin.isTTY;
21923
+ ctx.out(`Check code: ${code}`);
21924
+ 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.`);
21925
+ ctx.out(tty ? "Same code? [Y/n]" : " Same code? Run `starbridge pair --confirm` if so, `starbridge pair --reject` if not.");
21926
+ const typed = [];
21927
+ const shown = Date.now();
21928
+ const rl = tty ? createInterface3({ input: process.stdin }).on("line", (l) => {
21929
+ if (Date.now() - shown > 500)
21930
+ typed.push(l);
21931
+ }) : undefined;
21932
+ try {
21933
+ const until = ctx.now().getTime() + CONFIRM_MS;
21934
+ for (;; ) {
21935
+ if (ctx.signal?.aborted || ctx.now().getTime() > until)
21936
+ return;
21937
+ const line = typed.shift();
21938
+ if (line !== undefined)
21939
+ return /^\s*(y(es)?)?\s*$/i.test(line) ? "yes" : "no";
21940
+ try {
21941
+ const answer = readFileSync10(file, "utf8");
21942
+ rmSync3(file, { force: true });
21943
+ return answer === "yes" ? "yes" : "no";
21944
+ } catch {
21945
+ await ctx.sleep(500);
21946
+ }
21947
+ }
21948
+ } finally {
21949
+ rl?.close();
21950
+ rmSync3(file, { force: true });
21951
+ try {
21952
+ if (readFileSync10(waitingFile(ctx), "utf8") === code)
21953
+ rmSync3(waitingFile(ctx), { force: true });
21954
+ } catch {}
21368
21955
  }
21369
21956
  }
21370
21957
 
@@ -21484,8 +22071,8 @@ function installKind() {
21484
22071
  }
21485
22072
 
21486
22073
  // 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";
22074
+ import { existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "node:fs";
22075
+ import { join as join9 } from "node:path";
21489
22076
  var MARKETPLACE = "starbridge";
21490
22077
  var MARKETPLACE_SOURCE = "T0mSIlver/starbridge";
21491
22078
  var PLUGINS = [`starbridge@${MARKETPLACE}`, `starbridge-mod@${MARKETPLACE}`];
@@ -21493,9 +22080,9 @@ function hasClaude(sys) {
21493
22080
  return which(sys.ctx.env, "claude") !== undefined;
21494
22081
  }
21495
22082
  function claudeDir(sys) {
21496
- return sys.ctx.env.CLAUDE_CONFIG_DIR || join8(sys.home, ".claude");
22083
+ return sys.ctx.env.CLAUDE_CONFIG_DIR || join9(sys.home, ".claude");
21497
22084
  }
21498
- var claude = (sys, ...args) => run(sys, "claude", args, { timeoutMs: 180000 });
22085
+ var claude = (sys, ...args) => run(sys, "claude", args, { timeoutMs: 180000, cwd: sys.home });
21499
22086
  async function listJson(sys, ...args) {
21500
22087
  const r = await claude(sys, ...args, "--json");
21501
22088
  const why = `\`claude ${args.join(" ")} --json\` failed`;
@@ -21565,7 +22152,7 @@ async function installPlugins(sys, state) {
21565
22152
  throw new Error(foreign);
21566
22153
  const done = [];
21567
22154
  if (!state.marketplace) {
21568
- const r = await claude(sys, "plugin", "marketplace", "add", MARKETPLACE_SOURCE);
22155
+ const r = await claude(sys, "plugin", "marketplace", "add", MARKETPLACE_SOURCE, "--scope", "user");
21569
22156
  if (r?.code !== 0)
21570
22157
  throw new Error(`claude plugin marketplace add ${MARKETPLACE_SOURCE}: ${failure2(r)}`);
21571
22158
  done.push(`Added the ${MARKETPLACE} marketplace.`);
@@ -21581,6 +22168,8 @@ async function installPlugins(sys, state) {
21581
22168
  return done;
21582
22169
  }
21583
22170
  async function removePlugins(sys, state) {
22171
+ if (state.foreign)
22172
+ return [`Left the ${MARKETPLACE} marketplace from ${state.foreign} and its plugins alone.`];
21584
22173
  const done = [];
21585
22174
  for (const id of PLUGINS) {
21586
22175
  if (!state.plugins[id])
@@ -21595,17 +22184,17 @@ async function removePlugins(sys, state) {
21595
22184
  return done;
21596
22185
  }
21597
22186
  function settingsPath(sys) {
21598
- return join8(claudeDir(sys), "settings.json");
22187
+ return join9(claudeDir(sys), "settings.json");
21599
22188
  }
21600
22189
  function readSettings(sys) {
21601
22190
  try {
21602
- return JSON.parse(readFileSync9(settingsPath(sys), "utf8"));
22191
+ return JSON.parse(readFileSync11(settingsPath(sys), "utf8"));
21603
22192
  } catch {
21604
22193
  return;
21605
22194
  }
21606
22195
  }
21607
22196
  function writeSettings(sys, s) {
21608
- writeFileSync5(settingsPath(sys), `${JSON.stringify(s, null, 2)}
22197
+ writeFileSync6(settingsPath(sys), `${JSON.stringify(s, null, 2)}
21609
22198
  `);
21610
22199
  }
21611
22200
  function autoUpdate(sys) {
@@ -21646,7 +22235,7 @@ function addAllowRules(sys) {
21646
22235
  ...s.permissions,
21647
22236
  allow: [...allow, ...ALLOW_RULES.filter((r) => !allow.includes(r))]
21648
22237
  };
21649
- mkdirSync6(claudeDir(sys), { recursive: true });
22238
+ mkdirSync7(claudeDir(sys), { recursive: true });
21650
22239
  writeSettings(sys, s);
21651
22240
  return true;
21652
22241
  }
@@ -21858,24 +22447,39 @@ import {
21858
22447
  closeSync as closeSync2,
21859
22448
  existsSync as existsSync6,
21860
22449
  lstatSync as lstatSync2,
21861
- mkdirSync as mkdirSync7,
22450
+ mkdirSync as mkdirSync8,
21862
22451
  openSync as openSync2,
21863
- readdirSync as readdirSync4,
21864
- readFileSync as readFileSync10,
22452
+ readdirSync as readdirSync5,
22453
+ readFileSync as readFileSync12,
21865
22454
  realpathSync as realpathSync4,
21866
- renameSync as renameSync3,
21867
- rmSync as rmSync3,
22455
+ renameSync as renameSync4,
22456
+ rmSync as rmSync4,
21868
22457
  symlinkSync,
21869
22458
  unlinkSync as unlinkSync4,
21870
22459
  writeSync
21871
22460
  } from "node:fs";
21872
- import { dirname as dirname5, join as join9 } from "node:path";
22461
+ import { dirname as dirname5, join as join10 } from "node:path";
22462
+ // src/setup/codexbar-pin.json
22463
+ var codexbar_pin_default = {
22464
+ version: "0.73.0",
22465
+ sha256: {
22466
+ "macos-arm64": "cd8f853779f8faec003f53d5fc2a1614aef577ec9f603cbd58a9559b54e159b9",
22467
+ "macos-x86_64": "19e7059de688e62be98790bc10198257f5d326e4a3f4191183c23106eb91780a",
22468
+ "linux-aarch64": "ae6f9be046b09dc9d17210b661c25e2ee8a0150edb047b917d9eb17c295c7094",
22469
+ "linux-x86_64": "a84f556c7ecebc8e606e0a34ee0d121626f72d7afc1e3adf5746410bc10f9611",
22470
+ "linux-musl-aarch64": "a4007dfa6577a436eaeb6b46601982c69548c6e20730b3eb9133d4266d5ea92b",
22471
+ "linux-musl-x86_64": "75fd9cfc00784eef8badd6e76f4f74cbdf7c13db0b7f9ca6e105ddb793e6e8f8"
22472
+ }
22473
+ };
22474
+
22475
+ // src/setup/codexbar.ts
22476
+ var PIN = codexbar_pin_default;
21873
22477
  var RELEASES = "https://github.com/steipete/CodexBar/releases";
21874
22478
  var PROBE_TIMEOUT_MS = 20000;
21875
22479
  var PROGRESS_MS2 = 5000;
21876
22480
  var APP_HELPERS = (home) => [
21877
22481
  "/Applications/CodexBar.app/Contents/Helpers/CodexBarCLI",
21878
- join9(home, "Applications/CodexBar.app/Contents/Helpers/CodexBarCLI")
22482
+ join10(home, "Applications/CodexBar.app/Contents/Helpers/CodexBarCLI")
21879
22483
  ];
21880
22484
  function findCodexbar(sys, configured) {
21881
22485
  for (const p of [configured, sys.ctx.env.STARBRIDGE_CODEXBAR])
@@ -21884,7 +22488,7 @@ function findCodexbar(sys, configured) {
21884
22488
  const onPath = which(sys.ctx.env, "codexbar");
21885
22489
  if (onPath)
21886
22490
  return { path: onPath };
21887
- const opt = join9(sys.home, ".local/opt/codexbar/codexbar");
22491
+ const opt = join10(sys.home, ".local/opt/codexbar/codexbar");
21888
22492
  if (existsSync6(opt))
21889
22493
  return { path: opt };
21890
22494
  if (sys.platform === "darwin") {
@@ -21907,7 +22511,7 @@ function tarballKey(sys, musl = isMusl()) {
21907
22511
  function isMusl() {
21908
22512
  const has = (dir, prefix) => {
21909
22513
  try {
21910
- return readdirSync4(dir).some((f) => f.startsWith(prefix));
22514
+ return readdirSync5(dir).some((f) => f.startsWith(prefix));
21911
22515
  } catch {
21912
22516
  return false;
21913
22517
  }
@@ -21924,9 +22528,9 @@ function isMusl() {
21924
22528
  return !libs.some((d) => has(d, "libcurl.so.4"));
21925
22529
  }
21926
22530
  function linkIntoLocalBin(sys, target) {
21927
- const bin = join9(sys.home, ".local/bin");
21928
- const link = join9(bin, "codexbar");
21929
- mkdirSync7(bin, { recursive: true });
22531
+ const bin = join10(sys.home, ".local/bin");
22532
+ const link = join10(bin, "codexbar");
22533
+ mkdirSync8(bin, { recursive: true });
21930
22534
  try {
21931
22535
  if (!lstatSync2(link).isSymbolicLink())
21932
22536
  return;
@@ -21935,7 +22539,7 @@ function linkIntoLocalBin(sys, target) {
21935
22539
  symlinkSync(target, link);
21936
22540
  return link;
21937
22541
  }
21938
- var optDir = (sys) => join9(sys.home, ".local/opt/codexbar");
22542
+ var optDir = (sys) => join10(sys.home, ".local/opt/codexbar");
21939
22543
  function releaseVersion(v) {
21940
22544
  const m = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)$/.exec(v.trim());
21941
22545
  if (!m)
@@ -21953,19 +22557,28 @@ async function fetchRelease(url, init, what) {
21953
22557
  throw new Error(`${what}: GitHub is limiting requests from this address (${res.status}); try again in an hour`);
21954
22558
  return res;
21955
22559
  }
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));
22560
+ function compareCodexbar(a, b) {
22561
+ const parse = (v) => {
22562
+ const [main = "", pre] = releaseVersion(v).split(/-(.*)/);
22563
+ return { nums: main.split(".").map(Number), pre };
22564
+ };
22565
+ const x = parse(a);
22566
+ const y = parse(b);
22567
+ for (let i = 0;i < 3; i++)
22568
+ if (x.nums[i] !== y.nums[i])
22569
+ return x.nums[i] < y.nums[i] ? -1 : 1;
22570
+ if (x.pre === y.pre)
22571
+ return 0;
22572
+ if (x.pre === undefined)
22573
+ return 1;
22574
+ if (y.pre === undefined)
22575
+ return -1;
22576
+ return Math.sign(x.pre.localeCompare(y.pre, "en", { numeric: true }));
21964
22577
  }
21965
22578
  var size = (bytes) => bytes >= 1e6 ? `${Math.round(bytes / 1e6)} MB` : `${Math.max(1, Math.round(bytes / 1000))} kB`;
21966
22579
  function installedVersion(sys) {
21967
22580
  try {
21968
- return readFileSync10(join9(optDir(sys), "VERSION"), "utf8").trim() || undefined;
22581
+ return readFileSync12(join10(optDir(sys), "VERSION"), "utf8").trim() || undefined;
21969
22582
  } catch {
21970
22583
  return;
21971
22584
  }
@@ -21973,40 +22586,43 @@ function installedVersion(sys) {
21973
22586
 
21974
22587
  class NoChecksum extends Error {
21975
22588
  }
21976
- async function installTarball(sys, key, version) {
22589
+ async function installTarball(sys, key, version, sha256) {
21977
22590
  const base = `${sys.ctx.env.STARBRIDGE_CODEXBAR_RELEASES ?? RELEASES}/download`;
21978
22591
  const name = `CodexBarCLI-v${version}-${key}.tar.gz`;
21979
22592
  const url = `${base}/v${version}/${name}`;
21980
22593
  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`);
22594
+ let want = sha256;
22595
+ if (!want) {
22596
+ const sums = await fetchRelease(`${url}.sha256`, { signal }, `downloading ${name}.sha256`);
22597
+ if (!sums.ok)
22598
+ throw new NoChecksum(`no checksum for ${name} (${sums.status}): not installed`);
22599
+ want = /^[0-9a-f]{64}\b/i.exec((await sums.text()).trim())?.[0].toLowerCase();
22600
+ if (!want)
22601
+ throw new Error(`${name}.sha256 holds no SHA-256: not installed`);
22602
+ }
21987
22603
  const res = await fetchRelease(url, { signal }, `downloading ${name}`);
21988
22604
  if (!res.ok || !res.body)
21989
22605
  throw new Error(`downloading ${name}: ${res.status}`);
21990
22606
  const dest = optDir(sys);
21991
22607
  const opt = dirname5(dest);
21992
- const fresh = join9(opt, `.codexbar.${process.pid}`);
22608
+ const fresh = join10(opt, `.codexbar.${process.pid}`);
21993
22609
  const file = `${fresh}.tar.gz`;
21994
22610
  const old = `${fresh}.old`;
21995
- mkdirSync7(fresh, { recursive: true });
22611
+ mkdirSync8(fresh, { recursive: true });
21996
22612
  try {
21997
22613
  await download(sys, res, file, want, name, `CodexBar ${version} (${key})`);
21998
22614
  const r = await run(sys, "tar", ["-xzf", file, "-C", fresh]);
21999
22615
  if (r?.code !== 0)
22000
22616
  throw new Error(`could not unpack ${name} into ${opt}; is the disk full? (${failure2(r)})`);
22001
22617
  if (existsSync6(dest))
22002
- renameSync3(dest, old);
22003
- renameSync3(fresh, dest);
22618
+ renameSync4(dest, old);
22619
+ renameSync4(fresh, dest);
22004
22620
  } finally {
22005
- rmSync3(file, { force: true });
22006
- rmSync3(fresh, { recursive: true, force: true });
22007
- rmSync3(old, { recursive: true, force: true });
22621
+ rmSync4(file, { force: true });
22622
+ rmSync4(fresh, { recursive: true, force: true });
22623
+ rmSync4(old, { recursive: true, force: true });
22008
22624
  }
22009
- return join9(dest, "codexbar");
22625
+ return join10(dest, "codexbar");
22010
22626
  }
22011
22627
  async function download(sys, res, file, want, name, label) {
22012
22628
  const total = Number(res.headers.get("content-length")) || undefined;
@@ -22035,12 +22651,15 @@ async function download(sys, res, file, want, name, label) {
22035
22651
  if (sha !== want)
22036
22652
  throw new Error(`${name} has SHA-256 ${sha}, expected ${want}: not installed`);
22037
22653
  }
22038
- async function installRelease(sys, version) {
22654
+ async function installRelease(sys, version, pin = PIN) {
22039
22655
  const key = tarballKey(sys);
22040
22656
  if (!key)
22041
22657
  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);
22658
+ const v = version ? releaseVersion(version) : pin.version;
22659
+ const sha = v === pin.version ? pin.sha256[key] : undefined;
22660
+ if (v === pin.version && !sha)
22661
+ throw new Error(`no checksum pinned for CodexBar ${v} (${key})`);
22662
+ const path = await installTarball(sys, key, v, sha);
22044
22663
  const link = linkIntoLocalBin(sys, path);
22045
22664
  sys.ctx.out(`Installed CodexBar ${v} to ${optDir(sys)}${link ? `, linked as ${link}` : ""}.`);
22046
22665
  return path;
@@ -22087,10 +22706,10 @@ async function listProviders(sys, bin) {
22087
22706
  }
22088
22707
  function probeSet(sys, list, named) {
22089
22708
  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");
22709
+ const claude = existsSync6(join10(sys.home, ".claude/.credentials.json")) || which(sys.ctx.env, "claude");
22091
22710
  if (claude)
22092
22711
  set.add("claude");
22093
- if (existsSync6(join9(sys.home, ".codex/auth.json")))
22712
+ if (existsSync6(join10(sys.home, ".codex/auth.json")))
22094
22713
  set.add("codex");
22095
22714
  return [...set];
22096
22715
  }
@@ -22121,7 +22740,7 @@ async function probe(bin, providers, list) {
22121
22740
  }
22122
22741
  }));
22123
22742
  }
22124
- async function updateCodexbar(sys, configured, version) {
22743
+ async function updateCodexbar(sys, configured, version, pin = PIN) {
22125
22744
  const out = sys.ctx.out;
22126
22745
  const found = findCodexbar(sys, configured);
22127
22746
  if (!found) {
@@ -22141,20 +22760,19 @@ async function updateCodexbar(sys, configured, version) {
22141
22760
  return version ? 1 : 0;
22142
22761
  }
22143
22762
  const have = installedVersion(sys);
22144
- let want;
22145
22763
  try {
22146
- want = version ? releaseVersion(version) : await latestCodexbar(sys);
22764
+ const want = version ? releaseVersion(version) : pin.version;
22147
22765
  if (have === want) {
22148
22766
  out(`CodexBar ${have} is ${version ? "installed" : "up to date"}.`);
22149
22767
  return 0;
22150
22768
  }
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"}.`);
22769
+ if (!version && have && compareCodexbar(have, want) > 0) {
22770
+ out(`CodexBar ${have} is newer than the ${want} this release installs: kept.`);
22156
22771
  return 0;
22157
22772
  }
22773
+ await installRelease(sys, want, pin);
22774
+ return 0;
22775
+ } catch (e) {
22158
22776
  out(`Could not update CodexBar: ${e.message}`);
22159
22777
  return 1;
22160
22778
  }
@@ -22165,23 +22783,23 @@ import {
22165
22783
  appendFileSync as appendFileSync2,
22166
22784
  closeSync as closeSync3,
22167
22785
  existsSync as existsSync7,
22168
- mkdirSync as mkdirSync8,
22786
+ mkdirSync as mkdirSync9,
22169
22787
  openSync as openSync3,
22170
- readFileSync as readFileSync11,
22788
+ readFileSync as readFileSync13,
22171
22789
  readSync as readSync2,
22172
- rmSync as rmSync4,
22173
- writeFileSync as writeFileSync6
22790
+ rmSync as rmSync5,
22791
+ writeFileSync as writeFileSync7
22174
22792
  } from "node:fs";
22175
- import { basename as basename4, delimiter, dirname as dirname6, join as join10 } from "node:path";
22793
+ import { delimiter, dirname as dirname6, join as join11 } from "node:path";
22176
22794
  function recordSelf(sys) {
22177
- const file = join10(sys.ctx.store.dir, CLI_PATH);
22795
+ const file = join11(sys.ctx.store.dir, CLI_PATH);
22178
22796
  if (sys.self.length !== 1 || script(sys.self[0])) {
22179
- rmSync4(file, { force: true });
22797
+ rmSync5(file, { force: true });
22180
22798
  return;
22181
22799
  }
22182
- mkdirSync8(dirname6(file), { recursive: true, mode: 448 });
22800
+ mkdirSync9(dirname6(file), { recursive: true, mode: 448 });
22183
22801
  const self2 = sys.self[0];
22184
- writeFileSync6(file, `${sys.platform === "win32" ? self2.replace(/\\/g, "/") : self2}
22802
+ writeFileSync7(file, `${sys.platform === "win32" ? self2.replace(/\\/g, "/") : self2}
22185
22803
  `);
22186
22804
  }
22187
22805
  function script(path) {
@@ -22198,7 +22816,8 @@ function script(path) {
22198
22816
  function missingDir(sys) {
22199
22817
  if (sys.self.length !== 1)
22200
22818
  return;
22201
- const dir = dirname6(sys.self[0]);
22819
+ const { dirname } = pathFor(sys.platform);
22820
+ const dir = dirname(sys.self[0]);
22202
22821
  const norm = (p) => {
22203
22822
  const bare = p.replace(/[/\\]+$/, "");
22204
22823
  return sys.platform === "win32" ? bare.toLowerCase() : bare;
@@ -22207,20 +22826,21 @@ function missingDir(sys) {
22207
22826
  return path.includes(norm(dir)) ? undefined : dir;
22208
22827
  }
22209
22828
  function shellProfile(sys, dir) {
22210
- const shell = basename4(sys.ctx.env.SHELL ?? "");
22829
+ const { basename, join } = pathFor(sys.platform);
22830
+ const shell = basename(sys.ctx.env.SHELL ?? "");
22211
22831
  const shown = dir.startsWith(`${sys.home}/`) ? `$HOME${dir.slice(sys.home.length)}` : dir;
22212
22832
  const exported = `export PATH="${shown}:$PATH"`;
22213
22833
  if (shell === "fish")
22214
- return { file: join10(sys.home, ".config", "fish", "config.fish"), line: `fish_add_path ${dir}` };
22834
+ return { file: join(sys.home, ".config", "fish", "config.fish"), line: `fish_add_path ${dir}` };
22215
22835
  if (shell === "zsh")
22216
- return { file: join10(sys.ctx.env.ZDOTDIR || sys.home, ".zshrc"), line: exported };
22836
+ return { file: join(sys.ctx.env.ZDOTDIR || sys.home, ".zshrc"), line: exported };
22217
22837
  if (shell === "bash") {
22218
22838
  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 };
22839
+ return { file: join(sys.home, ".bashrc"), line: exported };
22840
+ const login = [".bash_profile", ".bash_login"].map((f) => join(sys.home, f)).find(existsSync7);
22841
+ return { file: login ?? join(sys.home, ".profile"), line: exported };
22222
22842
  }
22223
- return { file: join10(sys.home, ".profile"), line: exported };
22843
+ return { file: join(sys.home, ".profile"), line: exported };
22224
22844
  }
22225
22845
  async function pathStep(sys) {
22226
22846
  const dir = missingDir(sys);
@@ -22236,12 +22856,13 @@ async function pathStep(sys) {
22236
22856
  ];
22237
22857
  }
22238
22858
  const { file, line } = shellProfile(sys, dir);
22859
+ const { dirname } = pathFor(sys.platform);
22239
22860
  const shown = file.startsWith(`${sys.home}/`) ? `~${file.slice(sys.home.length)}` : file;
22240
- const text = existsSync7(file) ? readFileSync11(file, "utf8") : "";
22861
+ const text = existsSync7(file) ? readFileSync13(file, "utf8") : "";
22241
22862
  const rest = dir.startsWith(`${sys.home}/`) ? dir.slice(sys.home.length) : undefined;
22242
22863
  const has = [dir, line, ...rest ? [`$HOME${rest}`, `\${HOME}${rest}`, `~${rest}`] : []].some((spelled) => text.includes(spelled));
22243
22864
  if (!has && await prompt.confirm(`Add ${dir} to your PATH in ${shown}?`, true)) {
22244
- mkdirSync8(dirname6(file), { recursive: true });
22865
+ mkdirSync9(dirname(file), { recursive: true });
22245
22866
  appendFileSync2(file, `${text === "" || text.endsWith(`
22246
22867
  `) ? "" : `
22247
22868
  `}
@@ -22262,9 +22883,9 @@ ${line}
22262
22883
  }
22263
22884
 
22264
22885
  // src/setup/service.ts
22265
- import { existsSync as existsSync8, mkdirSync as mkdirSync9, readFileSync as readFileSync12, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
22886
+ import { existsSync as existsSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
22266
22887
  import { userInfo as userInfo2 } from "node:os";
22267
- import { delimiter as delimiter2, dirname as dirname7, join as join11 } from "node:path";
22888
+ import { delimiter as delimiter2, dirname as dirname7, join as join12 } from "node:path";
22268
22889
  var UNIT = "starbridge-agent.service";
22269
22890
  var LABEL = "run.starbridge.agent";
22270
22891
  var TASK = "starbridge-agent";
@@ -22272,25 +22893,25 @@ function kind(sys) {
22272
22893
  return { linux: "systemd", darwin: "launchd", win32: "task" }[sys.platform];
22273
22894
  }
22274
22895
  function localDir(sys) {
22275
- return join11(sys.ctx.env.LOCALAPPDATA || join11(sys.home, "AppData", "Local"), "starbridge");
22896
+ return join12(sys.ctx.env.LOCALAPPDATA || join12(sys.home, "AppData", "Local"), "starbridge");
22276
22897
  }
22277
- var agentLog = (sys) => kind(sys) === "task" ? join11(localDir(sys), "agent.log") : join11(sys.home, "Library/Logs/starbridge-agent.log");
22898
+ var agentLog = (sys) => kind(sys) === "task" ? join12(localDir(sys), "agent.log") : join12(sys.home, "Library/Logs/starbridge-agent.log");
22278
22899
  function unitDir(sys) {
22279
- return join11(sys.home, ".config/systemd/user");
22900
+ return join12(sys.home, ".config/systemd/user");
22280
22901
  }
22281
22902
  function servicePath(sys) {
22282
22903
  const k = kind(sys);
22283
22904
  if (k === "systemd")
22284
- return join11(unitDir(sys), UNIT);
22905
+ return join12(unitDir(sys), UNIT);
22285
22906
  if (k === "launchd")
22286
- return join11(sys.home, "Library/LaunchAgents", `${LABEL}.plist`);
22907
+ return join12(sys.home, "Library/LaunchAgents", `${LABEL}.plist`);
22287
22908
  if (k === "task")
22288
- return join11(localDir(sys), `${TASK}.xml`);
22909
+ return join12(localDir(sys), `${TASK}.xml`);
22289
22910
  return;
22290
22911
  }
22291
22912
  function servicePathVar(sys) {
22292
22913
  const extra = [
22293
- join11(sys.home, ".local/bin"),
22914
+ join12(sys.home, ".local/bin"),
22294
22915
  "/opt/homebrew/bin",
22295
22916
  "/usr/local/bin",
22296
22917
  "/usr/bin",
@@ -22386,12 +23007,12 @@ ${env}
22386
23007
  </plist>
22387
23008
  `;
22388
23009
  }
22389
- var winArg = (s) => /[\s"]/.test(s) ? `"${s.replace(/"/g, "\\\"")}"` : s;
23010
+ var winArg = (s) => /[\s"]/.test(s) ? `"${s.replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\+)$/, "$1$1")}"` : s;
22390
23011
  function taskXml(sys) {
22391
23012
  const env = sys.ctx.env;
22392
23013
  const name = env.USERNAME || userInfo2().username;
22393
23014
  const user = env.USERDOMAIN ? `${env.USERDOMAIN}\\${name}` : name;
22394
- const conhost = join11(env.SystemRoot || env.SYSTEMROOT || "C:\\Windows", "System32", "conhost.exe");
23015
+ const conhost = join12(env.SystemRoot || env.SYSTEMROOT || "C:\\Windows", "System32", "conhost.exe");
22395
23016
  const args = ["--headless", ...sys.self, "agent", "--log", agentLog(sys)].map(winArg).join(" ");
22396
23017
  return `<?xml version="1.0" encoding="UTF-16"?>
22397
23018
  ${marker("<!--", "-->")}
@@ -22443,7 +23064,7 @@ ${marker("<!--", "-->")}
22443
23064
  </Task>
22444
23065
  `;
22445
23066
  }
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 });
23067
+ 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
23068
  async function stopTask(sys) {
22448
23069
  let pid;
22449
23070
  const socket = socketPath(sys.ctx.env, sys.ctx.store.dir, sys.platform);
@@ -22489,7 +23110,7 @@ async function unavailable(sys) {
22489
23110
  }
22490
23111
  function readText2(path) {
22491
23112
  try {
22492
- const bytes = readFileSync12(path);
23113
+ const bytes = readFileSync14(path);
22493
23114
  if (bytes[0] === 255 && bytes[1] === 254)
22494
23115
  return bytes.toString("utf16le").slice(1);
22495
23116
  return bytes.toString("utf8");
@@ -22507,12 +23128,12 @@ async function installService(sys, restart) {
22507
23128
  const before = readText2(path);
22508
23129
  if (before !== undefined && !ours(before))
22509
23130
  throw new Error(`${path} was not written by setup: remove it to install the agent`);
22510
- mkdirSync9(dirname7(path), { recursive: true });
23131
+ mkdirSync10(dirname7(path), { recursive: true });
22511
23132
  const k = kind(sys);
22512
23133
  const text = k === "systemd" ? unitText(sys) : k === "launchd" ? plistText(sys) : taskXml(sys);
22513
23134
  const changed = readText2(path) !== text;
22514
23135
  if (changed)
22515
- writeFileSync7(path, k === "task" ? `\uFEFF${text}` : text, k === "task" ? "utf16le" : "utf8");
23136
+ writeFileSync8(path, k === "task" ? `\uFEFF${text}` : text, k === "task" ? "utf16le" : "utf8");
22516
23137
  const running = /^(active|running)$/i.test((await serviceState(sys)).state);
22517
23138
  const go = changed || restart || !running;
22518
23139
  if (k === "task") {
@@ -22555,16 +23176,16 @@ async function removeService(sys) {
22555
23176
  const r = await powershell(sys, `Unregister-ScheduledTask -TaskName ${TASK} -Confirm:$false -ErrorAction SilentlyContinue; if (Get-ScheduledTask -TaskName ${TASK} -ErrorAction SilentlyContinue) { exit 1 }`);
22556
23177
  if (r?.code !== 0)
22557
23178
  throw new Error(`Unregister-ScheduledTask: ${failure2(r)}`);
22558
- rmSync5(path, { force: true });
23179
+ rmSync6(path, { force: true });
22559
23180
  } else if (kind(sys) === "systemd") {
22560
23181
  await stopUnit(sys, UNIT);
22561
- rmSync5(path, { force: true });
23182
+ rmSync6(path, { force: true });
22562
23183
  await systemctl(sys, "daemon-reload");
22563
23184
  } else {
22564
23185
  const r = await launchctl(sys, "bootout", `gui/${sys.uid}/${LABEL}`);
22565
23186
  if (r?.code !== 0 && r?.code !== 3 && r?.code !== 113)
22566
23187
  throw new Error(`launchctl bootout: ${failure2(r)}`);
22567
- rmSync5(path, { force: true });
23188
+ rmSync6(path, { force: true });
22568
23189
  }
22569
23190
  return true;
22570
23191
  }
@@ -22771,6 +23392,7 @@ async function setup(sys, opts) {
22771
23392
  for (const line of commandTable([
22772
23393
  ["Check it", "starbridge status"],
22773
23394
  ...permissionsEnabled(ctx) ? [] : [["Send permission prompts", "starbridge config permissions on"]],
23395
+ ...ctx.store.agentConfig().presence?.enabled || !["desktop", "laptop"].includes(ctx.store.agentConfig().machineKind ?? "") ? [] : [["Quiet the phone while here", "starbridge config presence on"]],
22774
23396
  ["Remove from one agent", `starbridge uninstall --agent ${one}`],
22775
23397
  ["Remove everything", "starbridge uninstall"]
22776
23398
  ]))
@@ -23137,6 +23759,9 @@ async function status(sys) {
23137
23759
  out(`Sessions: ${agent.sessions.length}`);
23138
23760
  for (const x of agent.sessions)
23139
23761
  out(` ${x.id}${x.title ? ` "${x.title}"` : ""}${x.client ? ` ${x.client}` : ""}, last seen ${x.lastSeenAt}`);
23762
+ const own = ctx.env.CLAUDECODE === "1" ? ctx.env.CLAUDE_CODE_SESSION_ID : undefined;
23763
+ if (own && ctx.env.CLAUDE_CODE_SESSION_ATTENDED !== "0" && !agent.sessions.some((x) => x.id === own))
23764
+ out(` Not this session (${own}): no Starbridge mod runs in it, so answers come back only through \`starbridge wait\``);
23140
23765
  }
23141
23766
  const svc = await serviceState(sys);
23142
23767
  out(svc.installed ? `Service: ${svc.state}${svc.enabled === undefined ? "" : svc.enabled ? ", enabled" : ", not enabled"}` : "Service: not installed");
@@ -23192,14 +23817,14 @@ async function status(sys) {
23192
23817
  }
23193
23818
 
23194
23819
  // 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";
23820
+ import { existsSync as existsSync10, realpathSync as realpathSync5, rmSync as rmSync8 } from "node:fs";
23821
+ import { join as join14 } from "node:path";
23197
23822
 
23198
23823
  // 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";
23824
+ import { spawn as spawn7, spawnSync as spawnSync3 } from "node:child_process";
23825
+ import { chmodSync as chmodSync3, existsSync as existsSync9, readdirSync as readdirSync6, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
23201
23826
  import { homedir as homedir5 } from "node:os";
23202
- import { basename as basename5, dirname as dirname8, join as join12 } from "node:path";
23827
+ import { basename as basename4, dirname as dirname8, join as join13 } from "node:path";
23203
23828
  var MANAGED = {
23204
23829
  brew: {
23205
23830
  update: "brew upgrade starbridge, then starbridge setup --refresh",
@@ -23235,8 +23860,8 @@ async function restartAgent(ctx) {
23235
23860
  return;
23236
23861
  }
23237
23862
  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");
23863
+ const linux = join13(home, ".config/systemd/user/starbridge-agent.service");
23864
+ const mac = join13(home, "Library/LaunchAgents/run.starbridge.agent.plist");
23240
23865
  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
23866
  if (r === undefined)
23242
23867
  return;
@@ -23272,7 +23897,14 @@ async function update(ctx, install, pubkey = RELEASE_KEY, codexbar) {
23272
23897
  return updateCodexbar(sys, configured, codexbar);
23273
23898
  let self2 = 0;
23274
23899
  try {
23275
- await updateSelf(ctx, install, pubkey);
23900
+ if (await updateSelf(ctx, install, pubkey) && install.kind === "binary") {
23901
+ const r = spawnSync3(install.path, ["update"], {
23902
+ env: ctx.env,
23903
+ stdio: ["ignore", "inherit", "inherit"],
23904
+ timeout: 15 * 60000
23905
+ });
23906
+ return r.status ?? 1;
23907
+ }
23276
23908
  } catch (e) {
23277
23909
  if (e instanceof ReleaseError && !(e instanceof DownloadError))
23278
23910
  throw e;
@@ -23286,17 +23918,17 @@ async function update(ctx, install, pubkey = RELEASE_KEY, codexbar) {
23286
23918
  async function updateSelf(ctx, install, pubkey) {
23287
23919
  if (install.kind !== "binary") {
23288
23920
  ctx.out(`starbridge was installed with ${install.kind}: run ${MANAGED[install.kind].update}`);
23289
- return;
23921
+ return false;
23290
23922
  }
23291
23923
  const releases = ctx.env.STARBRIDGE_RELEASES_URL ?? RELEASES_URL;
23292
23924
  const latest = await latestVersion(releases);
23293
23925
  if (compareVersions(latest, VERSION) <= 0) {
23294
23926
  ctx.out(`starbridge ${VERSION} is up to date.`);
23295
- return;
23927
+ return false;
23296
23928
  }
23297
23929
  const bytes = await downloadVerified(latest, platformAsset(), { releases, pubkey });
23298
- const next = join12(dirname8(install.path), ".starbridge.new");
23299
- writeFileSync8(next, bytes, { mode: 493 });
23930
+ const next = join13(dirname8(install.path), ".starbridge.new");
23931
+ writeFileSync9(next, bytes, { mode: 493 });
23300
23932
  chmodSync3(next, 493);
23301
23933
  replaceBinary(next, install.path);
23302
23934
  ctx.out(`Updated starbridge ${VERSION} to ${latest} in ${install.path}.`);
@@ -23315,28 +23947,29 @@ async function updateSelf(ctx, install, pubkey) {
23315
23947
  }
23316
23948
  updatePlugins(ctx);
23317
23949
  movePiPackage(ctx, latest);
23950
+ return true;
23318
23951
  }
23319
23952
  function replaceBinary(next, target, platform = process.platform) {
23320
23953
  if (platform !== "win32")
23321
- return renameSync4(next, target);
23954
+ return renameSync5(next, target);
23322
23955
  let aside = `${target}.old`;
23323
23956
  try {
23324
- rmSync6(aside, { force: true });
23957
+ rmSync7(aside, { force: true });
23325
23958
  } catch {
23326
23959
  aside = `${target}.${process.pid}.old`;
23327
23960
  }
23328
- renameSync4(target, aside);
23961
+ renameSync5(target, aside);
23329
23962
  try {
23330
- renameSync4(next, target);
23963
+ renameSync5(next, target);
23331
23964
  } catch (e) {
23332
- renameSync4(aside, target);
23965
+ renameSync5(aside, target);
23333
23966
  throw e;
23334
23967
  }
23335
23968
  const dir = dirname8(target);
23336
- const leftover = (f) => f.startsWith(`${basename5(target)}.`) && f.endsWith(".old");
23337
- for (const f of readdirSync5(dir).filter(leftover))
23969
+ const leftover = (f) => f.startsWith(`${basename4(target)}.`) && f.endsWith(".old");
23970
+ for (const f of readdirSync6(dir).filter(leftover))
23338
23971
  try {
23339
- rmSync6(join12(dir, f), { force: true });
23972
+ rmSync7(join13(dir, f), { force: true });
23340
23973
  } catch {}
23341
23974
  }
23342
23975
  function removeBinary(ctx, install) {
@@ -23346,7 +23979,7 @@ function removeBinary(ctx, install) {
23346
23979
  }
23347
23980
  if (process.platform === "win32")
23348
23981
  return removeRunningExe(ctx, install.path);
23349
- rmSync6(install.path, { force: true });
23982
+ rmSync7(install.path, { force: true });
23350
23983
  ctx.out(`Removed ${install.path}.`);
23351
23984
  return 0;
23352
23985
  }
@@ -23356,7 +23989,7 @@ function removeRunningExe(ctx, path) {
23356
23989
  return 0;
23357
23990
  }
23358
23991
  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}"`], {
23992
+ spawn7(ctx.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${del}"`], {
23360
23993
  detached: true,
23361
23994
  stdio: "ignore",
23362
23995
  windowsHide: true,
@@ -23403,7 +24036,7 @@ async function uninstall(sys, opts) {
23403
24036
  ctx.out(`Kept ${dir}: a service still uses it. Stop it, then rerun \`starbridge uninstall\`.`);
23404
24037
  } else if (existsSync10(dir)) {
23405
24038
  if (opts.purge || await prompt.confirm(`Delete ${dir} (this machine's keys and state)?`, false)) {
23406
- rmSync7(dir, { recursive: true, force: true });
24039
+ rmSync8(dir, { recursive: true, force: true });
23407
24040
  ctx.out(`Deleted ${dir}.`);
23408
24041
  } else
23409
24042
  ctx.out(`Kept ${dir}: \`starbridge setup\` reuses it.`);
@@ -23412,8 +24045,8 @@ async function uninstall(sys, opts) {
23412
24045
  ctx.out(`Revoke "${machine.name}" under Devices in the Starbridge app or web page.`);
23413
24046
  const cb = findCodexbar(sys);
23414
24047
  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";
24048
+ const opt = join14(sys.home, ".local/opt/codexbar");
24049
+ 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
24050
  ctx.out(`CodexBar stays installed (${cb.path}); remove it with ${how}.`);
23418
24051
  }
23419
24052
  if (opts.install && stopped)
@@ -23468,7 +24101,10 @@ var HELP = `starbridge: post decisions to your devices, report runs, upload quot
23468
24101
  setup leaves it out until \`setup --agent\` brings it back.
23469
24102
 
23470
24103
  starbridge pair [--server <url>] [--name <name>] [--force]
23471
- Make this machine's keys and print a pairing code to type on a device.
24104
+ Make this machine's keys and print a pairing code to type on a device. Unless the
24105
+ Starbridge Android app scanned the QR code, confirm that its check code matches.
24106
+ starbridge pair --confirm | --reject
24107
+ Answer that for a waiting \`pair\` or \`setup\` that has no terminal.
23472
24108
  --server <url> a self-hosted server (default: $STARBRIDGE_SERVER, else
23473
24109
  https://starbridge.run)
23474
24110
 
@@ -23559,24 +24195,29 @@ var HELP = `starbridge: post decisions to your devices, report runs, upload quot
23559
24195
  The commands above go through it when it runs, and to the server directly when not
23560
24196
  (or with STARBRIDGE_NO_AGENT=1).
23561
24197
 
23562
- starbridge config [permissions on|off] [machine-kind server|desktop|laptop|cloud]
24198
+ starbridge config [permissions on|off] [presence on|off] [machine-kind server|desktop|laptop|cloud]
23563
24199
  Print this machine's settings, or change one. permissions: send its Claude Code
23564
24200
  permission prompts to your devices, where they can be allowed or denied; the prompt
23565
24201
  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.
24202
+ starbridge plugin's permission hook exits at once. presence: while this machine's
24203
+ screen is unlocked and had keyboard or mouse input in the last minute, notifications
24204
+ on your other devices wait a few seconds, so a question you answer here doesn't buzz
24205
+ your phone; the server hears only yes or no. Off by default. machine-kind: the icon
24206
+ devices show, detected by setup.
23568
24207
 
23569
24208
  starbridge hook permission --agent claude-code|pi|opencode [--wait 570s]
23570
24209
  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
24210
+ For Claude Code's PermissionRequest hook, and for its PostToolUse, PostToolUseFailure,
24211
+ PermissionDenied, Stop and SessionEnd hooks: hook JSON on stdin; prints the hook's decision, or nothing
23573
24212
  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.
24213
+ for pi-permission-system's prompts, the opencode plugin with --agent opencode. On
24214
+ Claude Code's AskUserQuestion it posts each question to your devices, whatever the
24215
+ permissions setting, and prints the first device answers as the picker's; an answer
24216
+ in the picker settles them.
23575
24217
 
23576
24218
  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.
24219
+ For Claude Code's PreToolUse hook on AskUserQuestion, from older plugins: prints
24220
+ nothing, so the picker opens and hook permission races it.
23580
24221
 
23581
24222
  starbridge hook question --agent opencode
23582
24223
  For the Starbridge opencode plugin, on each call of opencode's question tool: posts each
@@ -23586,9 +24227,9 @@ var HELP = `starbridge: post decisions to your devices, report runs, upload quot
23586
24227
 
23587
24228
  starbridge update [--codexbar <version>]
23588
24229
  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.
24230
+ and npm installs: use their manager, then \`starbridge setup --refresh\`); then the
24231
+ CodexBar release this Starbridge release pins, if setup installed CodexBar. --codexbar
24232
+ installs that CodexBar release instead, and only that; update keeps it if it is newer.
23592
24233
 
23593
24234
  starbridge --version
23594
24235
 
@@ -23610,7 +24251,7 @@ function parseSessionLink(text) {
23610
24251
  }
23611
24252
  function readText3(path) {
23612
24253
  try {
23613
- return readFileSync13(path === "-" ? 0 : path, "utf8");
24254
+ return readFileSync15(path === "-" ? 0 : path, "utf8");
23614
24255
  } catch (e) {
23615
24256
  throw new UsageError(`cannot read ${path === "-" ? "stdin" : path}: ${e.message}`);
23616
24257
  }
@@ -23644,9 +24285,13 @@ async function run2(argv, ctx) {
23644
24285
  options: {
23645
24286
  server: { type: "string" },
23646
24287
  name: { type: "string" },
23647
- force: { type: "boolean" }
24288
+ force: { type: "boolean" },
24289
+ confirm: { type: "boolean" },
24290
+ reject: { type: "boolean" }
23648
24291
  }
23649
24292
  });
24293
+ if (values.confirm || values.reject)
24294
+ return sendConfirm(ctx, !values.reject);
23650
24295
  return await pair(ctx, values);
23651
24296
  }
23652
24297
  case "ask": {
@@ -23892,7 +24537,7 @@ async function run2(argv, ctx) {
23892
24537
  if (sub === "settle")
23893
24538
  return await hookSettle(ctx, readText3("-"), values);
23894
24539
  if (sub === "ask-user")
23895
- return await hookAskUser(ctx, readText3("-"));
24540
+ return hookAskUser();
23896
24541
  if (sub === "question")
23897
24542
  return await hookQuestion(ctx, readText3("-"), values);
23898
24543
  throw new UsageError("usage: starbridge hook permission|settle --agent claude-code, starbridge hook ask-user, or starbridge hook question --agent opencode");