@pushary/agent-hooks 0.87.1 → 0.87.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.
@@ -36,6 +36,8 @@ import { existsSync } from "fs";
36
36
  import { spawn } from "child_process";
37
37
  import { basename } from "path";
38
38
  var MAX_RPC_LINE = 1024 * 1024;
39
+ var STDERR_TAIL_LIMIT = 2e3;
40
+ var FAILURE_MESSAGE_MAX = 240;
39
41
  var MAX_PENDING_RPC = 8;
40
42
  var MAX_TRANSCRIPT_TEXT = 32 * 1024;
41
43
  var MAX_MESSAGE_BUFFERS = 16;
@@ -46,20 +48,34 @@ var JsonRpcClient = class {
46
48
  nextId = 1;
47
49
  buffer = "";
48
50
  closed = false;
51
+ stderrTail = "";
52
+ failure = null;
49
53
  constructor(command, cwd, timeoutMs) {
50
54
  const options = { cwd, env: process.env, stdio: ["pipe", "pipe", "pipe"] };
51
55
  this.child = process.platform === "win32" ? spawnClaude(command.command, command.args ?? [], options) : spawn(command.command, command.args ?? [], options);
52
56
  this.child.stdout?.setEncoding("utf8");
53
57
  this.child.stdout?.on("data", (chunk) => this.read(String(chunk)));
54
- this.child.stderr?.resume();
58
+ this.child.stderr?.setEncoding("utf8");
59
+ this.child.stderr?.on("data", (chunk) => this.keepStderrTail(String(chunk)));
55
60
  this.child.once("error", (error) => this.fail(error));
56
- this.child.once("exit", () => this.fail(new Error("Codex app-server exited")));
61
+ this.child.once("exit", (code, signal) => this.fail(new Error(this.exitMessage(code, signal))));
57
62
  this.timeoutMs = timeoutMs;
58
63
  }
64
+ keepStderrTail(chunk) {
65
+ this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_LIMIT);
66
+ }
67
+ exitMessage(code, signal) {
68
+ const how = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
69
+ const tail = this.stderrTail.trim();
70
+ return tail ? `Codex app-server exited (${how}): ${tail}` : `Codex app-server exited (${how})`;
71
+ }
59
72
  timeoutMs;
60
73
  get isClosed() {
61
74
  return this.closed;
62
75
  }
76
+ get closedReason() {
77
+ return this.failure?.message ?? "Codex app-server exited";
78
+ }
63
79
  onNotification(listener) {
64
80
  this.notifications.add(listener);
65
81
  return () => this.notifications.delete(listener);
@@ -142,6 +158,7 @@ var JsonRpcClient = class {
142
158
  fail(error) {
143
159
  if (this.closed) return;
144
160
  this.closed = true;
161
+ this.failure = error;
145
162
  this.rejectPending(error);
146
163
  if (this.child.exitCode == null) this.child.kill();
147
164
  }
@@ -281,7 +298,7 @@ var runCodexBridge = async (options) => {
281
298
  await relay?.flush();
282
299
  await liveSessionSleep(options.pollIntervalMs ?? 2e3, options.signal);
283
300
  }
284
- if (rpc.isClosed && !options.signal.aborted) throw new Error("Codex app-server exited");
301
+ if (rpc.isClosed && !options.signal.aborted) throw new Error(rpc.closedReason);
285
302
  if (activeTurnId) {
286
303
  await rpc.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => null);
287
304
  }
@@ -289,8 +306,8 @@ var runCodexBridge = async (options) => {
289
306
  await report("session_closed", "Session stopped").catch(() => null);
290
307
  return "stopped";
291
308
  } catch (error) {
309
+ const message = error instanceof Error ? error.message : String(error);
292
310
  if (!linked && options.claimId) {
293
- const message = error instanceof Error ? error.message : String(error);
294
311
  for (let attempt = 0; attempt < 3; attempt++) {
295
312
  const acknowledged = await postLiveSession(options, "/api/agent/spawn/ack", {
296
313
  spawnId: options.spawnId,
@@ -299,7 +316,7 @@ var runCodexBridge = async (options) => {
299
316
  state: "failed",
300
317
  failure: {
301
318
  code: "bridge_failed",
302
- message: message.slice(0, 240),
319
+ message: message.slice(0, FAILURE_MESSAGE_MAX),
303
320
  retryable: false
304
321
  }
305
322
  }).catch(() => null);
@@ -307,7 +324,7 @@ var runCodexBridge = async (options) => {
307
324
  await liveSessionSleep(200 * (attempt + 1), options.signal);
308
325
  }
309
326
  }
310
- await report("error", "Live bridge failed").catch(() => null);
327
+ await report("error", `Live bridge failed: ${message}`.slice(0, FAILURE_MESSAGE_MAX)).catch(() => null);
311
328
  return "failed";
312
329
  } finally {
313
330
  stopHeartbeat();
@@ -6,7 +6,7 @@ import {
6
6
  confirmAppConnection,
7
7
  connectDevice,
8
8
  printConnectInstructions
9
- } from "../chunk-A3PFIWWN.js";
9
+ } from "../chunk-3LKYBFWA.js";
10
10
  import "../chunk-3EGEA4KH.js";
11
11
  import {
12
12
  checkApiKey
@@ -60,7 +60,7 @@ import {
60
60
  fetchChannels,
61
61
  reachVerdict,
62
62
  verifyRoundTrip
63
- } from "../chunk-A3PFIWWN.js";
63
+ } from "../chunk-3LKYBFWA.js";
64
64
  import "../chunk-3EGEA4KH.js";
65
65
  import {
66
66
  checkApiKey,
@@ -81,7 +81,7 @@ import {
81
81
  printConnectInstructions,
82
82
  setHumanStream,
83
83
  verifyRoundTrip
84
- } from "../chunk-A3PFIWWN.js";
84
+ } from "../chunk-3LKYBFWA.js";
85
85
  import "../chunk-3EGEA4KH.js";
86
86
  import {
87
87
  checkApiKey,
@@ -1865,7 +1865,8 @@ var main = async () => {
1865
1865
  // `false` is allowed to hold back "Setup complete.".
1866
1866
  roundTripAnswered: roundTrip === null ? null : roundTrip.kind === "answered" ? true : roundTrip.kind === "unanswered" ? false : null
1867
1867
  });
1868
- const daemon = keyCheck?.kind === "ok" && completed.length > 0 ? await ensureDaemonRunning() : null;
1868
+ const keyIsTrusted = keyFromPairing || keyCheck?.kind === "ok";
1869
+ const daemon = keyIsTrusted && completed.length > 0 ? await ensureDaemonRunning() : null;
1869
1870
  console.log(` ${closing.ok ? green(bold(closing.headline)) : yellow(bold(closing.headline))}`);
1870
1871
  if (failed.length > 0) {
1871
1872
  console.log();
@@ -8,7 +8,7 @@ import {
8
8
  describeReach,
9
9
  fetchChannels,
10
10
  reachVerdict
11
- } from "../chunk-A3PFIWWN.js";
11
+ } from "../chunk-3LKYBFWA.js";
12
12
  import "../chunk-3EGEA4KH.js";
13
13
  import {
14
14
  checkApiKey,
@@ -72,6 +72,15 @@ var describeAppRepair = (verdict) => {
72
72
  ];
73
73
  }
74
74
  };
75
+ var describeJustPairedRepair = (verdict) => {
76
+ if (verdict.kind === "nobody") {
77
+ return [
78
+ "The app asks for the notification permission after it grants the pairing.",
79
+ "Open the Pushary app and allow notifications. That is the whole remaining step."
80
+ ];
81
+ }
82
+ return describeAppRepair(verdict) ?? [];
83
+ };
75
84
 
76
85
  // src/setup/verify.ts
77
86
  var plural2 = (count, one) => `${count} ${one}${count === 1 ? "" : "s"}`;
@@ -586,14 +595,55 @@ var describeExistingConnection = (channels) => {
586
595
  }
587
596
  return "Phone already connected";
588
597
  };
598
+ var APP_REGISTER_TIMEOUT_MS = 45e3;
599
+ var appRegisterTimeoutMs = () => {
600
+ const raw = Number(process.env.PUSHARY_APP_REGISTER_TIMEOUT_MS);
601
+ return Number.isFinite(raw) && raw >= 0 ? raw : APP_REGISTER_TIMEOUT_MS;
602
+ };
603
+ var ownedApp = (status) => status.yours?.appDevices ?? status.appDevices;
604
+ var waitForAppRegistration = async (apiKey) => {
605
+ const deadline = Date.now() + appRegisterTimeoutMs();
606
+ if (Date.now() >= deadline) return { kind: "timeout" };
607
+ const stop = startSpinner(() => {
608
+ const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
609
+ return `Waiting for your phone to register for notifications ${dim(`(${left}s)`)}`;
610
+ });
611
+ try {
612
+ while (Date.now() < deadline) {
613
+ await sleep(CONNECT_POLL_INTERVAL_MS);
614
+ const probe = await fetchChannels(apiKey);
615
+ if (probe.kind === "unauthorized") {
616
+ stop(yellow("!"), "The server stopped accepting this key");
617
+ return { kind: "unauthorized" };
618
+ }
619
+ if (probe.kind === "locked") {
620
+ stop(yellow("!"), "This workspace has no active plan");
621
+ return { kind: "locked" };
622
+ }
623
+ if (probe.kind !== "ok") continue;
624
+ if (!probe.status.precise || ownedApp(probe.status) > 0) {
625
+ stop(check, "Phone registered for notifications");
626
+ return { kind: "connected", channels: probe.status };
627
+ }
628
+ }
629
+ } catch {
630
+ }
631
+ stop(yellow("!"), "This phone has not registered for notifications yet");
632
+ return { kind: "timeout" };
633
+ };
589
634
  var confirmAppConnection = async (apiKey) => {
590
635
  const probe = await fetchChannels(apiKey);
591
636
  if (probe.kind !== "ok") return describeProbeFailure(probe);
592
- const channels = probe.status;
593
- const ownedApp = channels.yours?.appDevices ?? channels.appDevices;
594
- if (channels.precise && ownedApp === 0) {
595
- const repair = describeAppRepair(reachVerdict(channels)) ?? [];
596
- console.log(` ${yellow("!")} No device of yours is registered for notifications yet.`);
637
+ let channels = probe.status;
638
+ if (channels.precise && ownedApp(channels) === 0) {
639
+ const settled = await waitForAppRegistration(apiKey);
640
+ if (settled.kind === "unauthorized") return describeProbeFailure({ kind: "unauthorized" });
641
+ if (settled.kind === "locked") return describeProbeFailure({ kind: "locked" });
642
+ if (settled.kind === "connected") channels = settled.channels;
643
+ }
644
+ if (channels.precise && ownedApp(channels) === 0) {
645
+ const repair = describeJustPairedRepair(reachVerdict(channels));
646
+ console.log(` ${yellow("!")} Paired, but this phone has not registered for notifications yet.`);
597
647
  for (const line of repair) console.log(` ${dim(line)}`);
598
648
  console.log(` ${dim("Then run")} ${cyan("pushary doctor")}${dim(".")}`);
599
649
  return phoneFailure("app_not_registered", repair);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.87.1",
3
+ "version": "0.87.2",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",