@pushary/agent-hooks 0.64.0 → 0.64.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.64.2
4
+
5
+ ### A run that gave up reported nothing, and said it stopped at the start
6
+
7
+ `agent_setup_completed` was wired to one of setup's fifteen exits: the last line
8
+ of a run that finished. Every other way out returned before it. A key the server
9
+ refused, a cancelled pairing, no terminal to sign in from, an installer that
10
+ threw, a Ctrl-C at the agent checkbox: all silent. The beacon was added so failed
11
+ and abandoned runs would stop being invisible, and the runs it could see were
12
+ still only the ones that already worked.
13
+
14
+ Every early exit reports now, and each says why it stopped: `key-refused`,
15
+ `pairing-cancelled`, `no-key`, `login-rejected`, `no-agents-detected`,
16
+ `invalid-key`, `interrupted`, `crashed`.
17
+
18
+ Argv-time exits stay silent, along with `--help` and `--dry-run`. A flag typo is
19
+ not an abandoned run: nothing was attempted and nothing was written. It is also
20
+ the fastest-failing path there is, and waiting on a beacon took `setup --bogus`
21
+ from 60ms to two seconds against an unreachable server. A dry run stays silent
22
+ for the other reason: counting it as a completed setup would be its own lie.
23
+
24
+ `abandonedAt` said where. It was always `start`, because `markStep` had no call
25
+ site outside its own test, so the field that was supposed to locate a failure was
26
+ a constant. It now moves through the run, per agent inside the configure loop, so
27
+ an installer that hangs is named rather than grouped.
28
+
29
+ Ctrl-C at the pairing QR reports before the process leaves. The cancel and the
30
+ beacon run concurrently and the exit waits for both, so this adds nothing to the
31
+ worst case a cancel already had.
32
+
33
+ The beacon authenticates with the key the run resolved rather than the one
34
+ already installed. Most of these exits happen before the key is written to disk,
35
+ so without that they posted as nobody, which meant the window holding the
36
+ interesting failures was exactly the window that could not report.
37
+
38
+ ## 0.64.1
39
+
40
+ ### Ctrl-C at the pairing QR printed nothing
41
+
42
+ The QR screen said "No app? Press Ctrl-C for the ways to finish without one", and
43
+ pressing it printed nothing at all. `connectViaAppPairing` handles SIGINT by
44
+ exiting the process itself, so control never returned to setup and the lines it
45
+ meant to show were unreachable on the one path that advertised them.
46
+
47
+ Setup now hands the escape to the pairing wait, which prints it inside the
48
+ handler before exiting, and the line on screen names the command directly instead
49
+ of telling you to interrupt the program to find out what it is.
50
+
51
+ The same two commands are printed when a pairing simply never completes, so both
52
+ ways out of the QR screen say the same thing.
53
+
54
+
3
55
  ## 0.64.0
4
56
 
5
57
  ### A typo on `--dry-run` ran a real clean
@@ -17,7 +17,7 @@ import {
17
17
  reportEvent,
18
18
  resolvePolicy,
19
19
  waitForAnswer
20
- } from "../chunk-5ZDYDU2Q.js";
20
+ } from "../chunk-XOUYM27W.js";
21
21
  import "../chunk-BSZYIAZL.js";
22
22
  import "../chunk-SAF6HGAA.js";
23
23
  import "../chunk-7QLSKOSU.js";
@@ -31,7 +31,7 @@ import {
31
31
  toCodexWire,
32
32
  toPolicyLookup,
33
33
  waitForAnswer
34
- } from "../chunk-5ZDYDU2Q.js";
34
+ } from "../chunk-XOUYM27W.js";
35
35
  import {
36
36
  isGatingMoment,
37
37
  recordKeylessMoment
@@ -3,7 +3,7 @@ import {
3
3
  askUser,
4
4
  reportEvent,
5
5
  waitForAnswer
6
- } from "../chunk-5ZDYDU2Q.js";
6
+ } from "../chunk-XOUYM27W.js";
7
7
  import "../chunk-BSZYIAZL.js";
8
8
  import "../chunk-SAF6HGAA.js";
9
9
  import "../chunk-7QLSKOSU.js";
@@ -6,7 +6,7 @@ import {
6
6
  confirmAppConnection,
7
7
  connectDevice,
8
8
  printConnectInstructions
9
- } from "../chunk-NGZMVCXN.js";
9
+ } from "../chunk-GFHQ26ST.js";
10
10
  import "../chunk-3EGEA4KH.js";
11
11
  import {
12
12
  readKeySource
@@ -34,7 +34,7 @@ import {
34
34
  describeReach,
35
35
  fetchChannels,
36
36
  reachVerdict
37
- } from "../chunk-NGZMVCXN.js";
37
+ } from "../chunk-GFHQ26ST.js";
38
38
  import "../chunk-3EGEA4KH.js";
39
39
  import {
40
40
  keyPrefix,
@@ -102,12 +102,13 @@ var globalVersion = () => {
102
102
  };
103
103
  var collectBundle = async (options2 = {}) => {
104
104
  const now = options2.now ?? (() => /* @__PURE__ */ new Date());
105
+ const readGlobalVersion = options2.readGlobalVersion ?? globalVersion;
105
106
  const source = readKeySource();
106
107
  const identity = options2.identity ?? null;
107
108
  return {
108
109
  generatedAt: now().toISOString(),
109
110
  cliVersion: getPackageVersion(),
110
- globalVersion: globalVersion(),
111
+ globalVersion: readGlobalVersion(),
111
112
  node: process.version,
112
113
  platform: process.platform,
113
114
  arch: process.arch,
@@ -23,7 +23,7 @@ import {
23
23
  savePendingQuestion,
24
24
  sendNotification,
25
25
  waitForAnswer
26
- } from "../chunk-5ZDYDU2Q.js";
26
+ } from "../chunk-XOUYM27W.js";
27
27
  import {
28
28
  isGatingMoment,
29
29
  recordKeylessMoment
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-SDXF5I46.js";
4
+ } from "../chunk-SONXGCZY.js";
5
5
  import "../chunk-CAJZAFVS.js";
6
6
  import {
7
7
  exitOnHelpFlag
8
8
  } from "../chunk-ATP6UQPH.js";
9
9
  import "../chunk-JGH37QRB.js";
10
10
  import "../chunk-YHG74UFF.js";
11
- import "../chunk-5ZDYDU2Q.js";
11
+ import "../chunk-XOUYM27W.js";
12
12
  import "../chunk-WLGEY4UX.js";
13
13
  import "../chunk-BSZYIAZL.js";
14
14
  import "../chunk-SAF6HGAA.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleNotification
4
- } from "../chunk-5ZDYDU2Q.js";
4
+ } from "../chunk-XOUYM27W.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionDenied
4
- } from "../chunk-SDXF5I46.js";
4
+ } from "../chunk-SONXGCZY.js";
5
5
  import "../chunk-CAJZAFVS.js";
6
6
  import "../chunk-YHG74UFF.js";
7
- import "../chunk-5ZDYDU2Q.js";
7
+ import "../chunk-XOUYM27W.js";
8
8
  import "../chunk-WLGEY4UX.js";
9
9
  import "../chunk-BSZYIAZL.js";
10
10
  import "../chunk-SAF6HGAA.js";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionRequest
4
- } from "../chunk-SDXF5I46.js";
4
+ } from "../chunk-SONXGCZY.js";
5
5
  import "../chunk-CAJZAFVS.js";
6
6
  import "../chunk-YHG74UFF.js";
7
- import "../chunk-5ZDYDU2Q.js";
7
+ import "../chunk-XOUYM27W.js";
8
8
  import "../chunk-WLGEY4UX.js";
9
9
  import "../chunk-BSZYIAZL.js";
10
10
  import "../chunk-SAF6HGAA.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-5ZDYDU2Q.js";
4
+ } from "../chunk-XOUYM27W.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleUserPrompt
4
- } from "../chunk-5ZDYDU2Q.js";
4
+ } from "../chunk-XOUYM27W.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionEnd
4
- } from "../chunk-5ZDYDU2Q.js";
4
+ } from "../chunk-XOUYM27W.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionStart
4
- } from "../chunk-5ZDYDU2Q.js";
4
+ } from "../chunk-XOUYM27W.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -45,7 +45,7 @@ import {
45
45
  connectViaAppPairing,
46
46
  printConnectInstructions,
47
47
  setHumanStream
48
- } from "../chunk-NGZMVCXN.js";
48
+ } from "../chunk-GFHQ26ST.js";
49
49
  import "../chunk-3EGEA4KH.js";
50
50
  import {
51
51
  KEY_FILE_MODE,
@@ -86,7 +86,7 @@ import {
86
86
  } from "../chunk-JGH37QRB.js";
87
87
  import {
88
88
  reportEvent
89
- } from "../chunk-5ZDYDU2Q.js";
89
+ } from "../chunk-XOUYM27W.js";
90
90
  import "../chunk-BSZYIAZL.js";
91
91
  import "../chunk-SAF6HGAA.js";
92
92
  import {
@@ -206,32 +206,60 @@ var installSkillViaSkillsCli = (agent) => {
206
206
 
207
207
  // src/setup/telemetry.ts
208
208
  var currentStep = "start";
209
+ var markStep = (step) => {
210
+ currentStep = step;
211
+ };
212
+ var recordedOptions = null;
213
+ var recordSetupOptions = (options) => {
214
+ recordedOptions = options;
215
+ };
216
+ var recordedKey = null;
217
+ var recordSetupKey = (apiKey) => {
218
+ recordedKey = apiKey;
219
+ };
209
220
  var emitted = false;
210
- var reportSetupOutcome = async (outcome) => {
221
+ var defaultDeps = { report: reportEvent };
222
+ var emit = async (deps, props) => {
211
223
  if (emitted) return;
212
224
  emitted = true;
213
225
  try {
214
- await reportEvent(
215
- {
216
- event: "agent_setup_completed",
217
- agentType: "setup",
218
- agents: outcome.completed,
219
- connectMode: outcome.options.connectMode,
220
- connectOutcome: outcome.connectOutcome,
221
- failedAgents: outcome.failed.length,
222
- skippedAgents: outcome.skipped,
223
- nonInteractive: outcome.options.nonInteractive,
224
- keyCheck: outcome.keyCheck,
225
- exitCode: outcome.exitCode,
226
- // Where the run stopped. On a completed run this is the closing step, so
227
- // the interesting values are the ones that are not.
228
- abandonedAt: outcome.exitCode === 0 ? null : currentStep
229
- },
230
- { maxAttempts: 1, timeoutMs: 2e3 }
226
+ await deps.report(
227
+ { event: "agent_setup_completed", agentType: "setup", ...props },
228
+ { maxAttempts: 1, timeoutMs: 2e3, ...recordedKey ? { apiKey: recordedKey } : {} }
231
229
  );
232
230
  } catch {
233
231
  }
234
232
  };
233
+ var reportSetupOutcomeWith = async (deps, outcome) => emit(deps, {
234
+ agents: outcome.completed,
235
+ connectMode: outcome.options.connectMode,
236
+ connectOutcome: outcome.connectOutcome,
237
+ failedAgents: outcome.failed.length,
238
+ skippedAgents: outcome.skipped,
239
+ nonInteractive: outcome.options.nonInteractive,
240
+ keyCheck: outcome.keyCheck,
241
+ exitCode: outcome.exitCode,
242
+ // Where the run stopped. On a completed run this is the closing step, so the
243
+ // interesting values are the ones that are not.
244
+ abandonedAt: outcome.exitCode === 0 ? null : currentStep,
245
+ // Always present, so a query can separate "ended" from "gave up" on the
246
+ // property rather than on the absence of one.
247
+ abortReason: null
248
+ });
249
+ var reportSetupOutcome = (outcome) => reportSetupOutcomeWith(defaultDeps, outcome);
250
+ var reportSetupAbortWith = async (deps, reason, exitCode, keyCheck = "unknown") => emit(deps, {
251
+ agents: [],
252
+ connectMode: recordedOptions?.connectMode ?? null,
253
+ connectOutcome: "skipped",
254
+ failedAgents: 0,
255
+ skippedAgents: [],
256
+ nonInteractive: recordedOptions?.nonInteractive ?? null,
257
+ keyCheck,
258
+ exitCode,
259
+ abandonedAt: currentStep,
260
+ abortReason: reason
261
+ });
262
+ var reportSetupAbort = (reason, exitCode, keyCheck) => reportSetupAbortWith(defaultDeps, reason, exitCode, keyCheck);
235
263
 
236
264
  // src/setup/options.ts
237
265
  var AGENT_CHOICES = ["claude_code", "codex", "gemini_cli", "hermes", "cursor", "custom"];
@@ -333,6 +361,10 @@ var green = (s) => `\x1B[32m${s}\x1B[0m`;
333
361
  var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
334
362
  var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
335
363
  var check = green("\u2713");
364
+ var abortWith = async (reason, code, keyCheck) => {
365
+ await reportSetupAbort(reason, code, keyCheck);
366
+ return code;
367
+ };
336
368
  var readKeyFromStdin = async (options) => {
337
369
  if (!options.keyStdin) return void 0;
338
370
  const chunks = [];
@@ -341,14 +373,14 @@ var readKeyFromStdin = async (options) => {
341
373
  if (!first) {
342
374
  console.log(`
343
375
  ${yellow("!")} --key-stdin was passed but stdin was empty.`);
344
- process.exit(2);
376
+ process.exit(EXIT.USAGE);
345
377
  }
346
378
  if (!isValidApiKey(first)) {
347
379
  console.log(`
348
380
  ${yellow("!")} The key on stdin is not a valid Pushary key. Expected: ${dim("pk_xxx.xxx")}`);
349
381
  console.log(` ${dim("Copy your key from")} ${cyan("https://pushary.com/dashboard/agent/settings")}
350
382
  `);
351
- process.exit(2);
383
+ process.exit(EXIT.USAGE);
352
384
  }
353
385
  return first;
354
386
  };
@@ -971,7 +1003,7 @@ var verifyKeyOrExit = async (apiKey, options, precomputed) => {
971
1003
  console.log(` ${yellow("!")} ${report.message}`);
972
1004
  for (const line of report.next) console.log(` ${dim(line)}`);
973
1005
  console.log();
974
- process.exit(report.exitCode);
1006
+ process.exit(await abortWith("key-refused", report.exitCode, result.kind));
975
1007
  }
976
1008
  if (result.kind === "ok") {
977
1009
  const { siteSlug, siteName, plan } = result.identity;
@@ -982,6 +1014,19 @@ var verifyKeyOrExit = async (apiKey, options, precomputed) => {
982
1014
  }
983
1015
  return result;
984
1016
  };
1017
+ var printPairingEscape = () => {
1018
+ console.log();
1019
+ console.log(` ${yellow("!")} No app? Two ways to finish without one:`);
1020
+ console.log();
1021
+ console.log(` ${cyan("npx @pushary/agent-hooks setup --connect browser")}`);
1022
+ console.log(` ${dim("Approve in a browser tab. No phone needed.")}`);
1023
+ console.log();
1024
+ console.log(` ${cyan("npx @pushary/agent-hooks setup --key pk_...")}`);
1025
+ console.log(` ${dim("Paste a key from")} ${cyan("pushary.com/dashboard/agent/settings")}`);
1026
+ console.log();
1027
+ console.log(` ${dim("Or get the app at")} ${cyan("https://pushary.com/download")} ${dim("and re-run setup.")}`);
1028
+ console.log();
1029
+ };
985
1030
  var usableStoredKey = async (apiKey) => {
986
1031
  let result;
987
1032
  try {
@@ -1041,7 +1086,7 @@ var resolveAgents = async (options) => {
1041
1086
  console.log(` ${yellow("!")} No agents detected, so nothing was configured. Your key is saved.`);
1042
1087
  console.log(` ${dim("Install an agent and re-run, name one with --agents <id>, or pass --agents none to accept this.")}`);
1043
1088
  console.log();
1044
- process.exit(EXIT.PROBLEMS_FOUND);
1089
+ process.exit(await abortWith("no-agents-detected", EXIT.PROBLEMS_FOUND));
1045
1090
  }
1046
1091
  console.log(` ${check} Selected automatically: ${picked.map((a) => AGENT_NAMES[a]).join(", ")} ${dim("(--agents to choose)")}`);
1047
1092
  return picked;
@@ -1112,6 +1157,7 @@ var main = async () => {
1112
1157
  `);
1113
1158
  process.exit(EXIT.OK);
1114
1159
  }
1160
+ recordSetupOptions(options);
1115
1161
  if (options.json) {
1116
1162
  humanStream = process.stderr;
1117
1163
  setHumanStream(process.stderr);
@@ -1126,6 +1172,7 @@ var main = async () => {
1126
1172
  console.log(` ${dim("Push notifications for AI agents")}`);
1127
1173
  console.log();
1128
1174
  await checkForUpdates(version);
1175
+ markStep("resolve-key");
1129
1176
  const connectMode = options.connectMode;
1130
1177
  const flagKey = await readKeyFromStdin(options) ?? options.key ?? void 0;
1131
1178
  const envKey = process.env.PUSHARY_API_KEY?.trim();
@@ -1160,18 +1207,18 @@ var main = async () => {
1160
1207
  }
1161
1208
  } else if (connectMode === "app" && canRunBrowserLogin() && !options.skipPhone) {
1162
1209
  console.log(` ${dim("Pairing with your Pushary app.")}`);
1163
- console.log(` ${dim("No app? Press Ctrl-C for the ways to finish without one.")}`);
1210
+ console.log(` ${dim("No app? Ctrl-C, then:")} ${cyan("npx @pushary/agent-hooks setup --connect browser")}`);
1211
+ markStep("pair");
1164
1212
  const previous = readKeySource();
1165
- const paired = await connectViaAppPairing();
1213
+ const paired = await connectViaAppPairing({
1214
+ onCancelled: async () => {
1215
+ printPairingEscape();
1216
+ await reportSetupAbort("pairing-cancelled", 130);
1217
+ }
1218
+ });
1166
1219
  if (!paired) {
1167
- console.log();
1168
- console.log(` ${yellow("!")} Pairing didn't complete. Two ways to finish without the app:`);
1169
- console.log();
1170
- console.log(` ${cyan("setup --connect browser")} ${dim("Approve in a browser tab")}`);
1171
- console.log(` ${cyan("setup --key pk_...")} ${dim("Paste a key you already have")}`);
1172
- console.log();
1173
- console.log(` ${dim("Or get the app at")} ${cyan("https://pushary.com/download")} ${dim("and re-run setup.")}`);
1174
- process.exit(EXIT.NOT_CONFIGURED);
1220
+ printPairingEscape();
1221
+ process.exit(await abortWith("pairing-cancelled", EXIT.NOT_CONFIGURED));
1175
1222
  }
1176
1223
  trimmedKey = paired.apiKey;
1177
1224
  keyFromPairing = true;
@@ -1188,8 +1235,9 @@ var main = async () => {
1188
1235
  console.log(` ${dim("Pass --key <pk_xxx.xxx>, or --key-stdin, or export PUSHARY_API_KEY.")}`);
1189
1236
  console.log(` ${dim("On a machine with a terminal, run")} ${cyan("pushary login")} ${dim("once and re-run this.")}`);
1190
1237
  console.log();
1191
- process.exit(EXIT.NOT_CONFIGURED);
1238
+ process.exit(await abortWith("no-key", EXIT.NOT_CONFIGURED));
1192
1239
  } else {
1240
+ markStep("browser-login");
1193
1241
  const outcome = await runBrowserLogin(
1194
1242
  { say: (line) => console.log(line), bold, dim, cyan, yellow },
1195
1243
  { cliVersion: version }
@@ -1203,7 +1251,7 @@ var main = async () => {
1203
1251
  console.log();
1204
1252
  console.log(` ${yellow("!")} That request was rejected in the browser. Nothing was written.`);
1205
1253
  console.log();
1206
- process.exit(EXIT.NOT_CONFIGURED);
1254
+ process.exit(await abortWith("login-rejected", EXIT.NOT_CONFIGURED));
1207
1255
  } else {
1208
1256
  console.log();
1209
1257
  if (outcome.kind === "unavailable") {
@@ -1224,15 +1272,19 @@ var main = async () => {
1224
1272
  console.log(` ${dim("Copy your key from")} ${cyan("https://pushary.com/dashboard/agent/settings")}`);
1225
1273
  console.log(` ${dim("Or sign up at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
1226
1274
  `);
1227
- process.exit(EXIT.USAGE);
1275
+ process.exit(await abortWith("invalid-key", EXIT.USAGE));
1228
1276
  }
1277
+ recordSetupKey(trimmedKey);
1278
+ markStep("verify-key");
1229
1279
  const keyCheck = keyFromPairing ? null : await verifyKeyOrExit(trimmedKey, options, storedKeyCheck ?? void 0);
1280
+ markStep("resolve-agents");
1230
1281
  const agents = await resolveAgents(options);
1231
1282
  if (options.dryRun) {
1232
1283
  reportDryRun(trimmedKey, agents, keyCheck);
1233
1284
  emitJson(options, { ok: true, dryRun: true, agents, keyCheck: keyCheck?.kind ?? "skipped" });
1234
1285
  process.exit(EXIT.OK);
1235
1286
  }
1287
+ markStep("save-key");
1236
1288
  const rc = await saveApiKey(trimmedKey);
1237
1289
  const context = { rc };
1238
1290
  const failed = [];
@@ -1244,6 +1296,7 @@ var main = async () => {
1244
1296
  console.log(` ${dim("Run setup again with --agents <id> when you want one wired up.")}`);
1245
1297
  } else {
1246
1298
  for (const agent of agents) {
1299
+ markStep(`configure:${agent}`);
1247
1300
  try {
1248
1301
  const outcome = await AGENT_SETUP[agent](trimmedKey, context);
1249
1302
  if (outcome === "skipped") skipped.push(agent);
@@ -1262,11 +1315,13 @@ var main = async () => {
1262
1315
  console.log();
1263
1316
  console.log(` ${yellow("!")} Failed: ${failed.join(", ")} ${dim("(others completed successfully)")}`);
1264
1317
  }
1318
+ markStep("project-instructions");
1265
1319
  await offerProjectInstructions(agents, options);
1266
1320
  if (completed.includes("claude_code")) {
1267
1321
  await offerClaudeAlias(options);
1268
1322
  }
1269
1323
  }
1324
+ markStep("connect-phone");
1270
1325
  const skipPhone = options.skipPhone;
1271
1326
  let phone = null;
1272
1327
  if (keyCheck?.kind === "locked") {
@@ -1304,6 +1359,15 @@ var main = async () => {
1304
1359
  rc: rc.kind,
1305
1360
  exitCode: report.exitCode
1306
1361
  });
1362
+ await reportSetupOutcome({
1363
+ options,
1364
+ completed,
1365
+ skipped,
1366
+ failed,
1367
+ connectOutcome: "skipped",
1368
+ keyCheck: "locked",
1369
+ exitCode: report.exitCode
1370
+ });
1307
1371
  process.exit(report.exitCode);
1308
1372
  }
1309
1373
  printManualSteps();
@@ -1311,6 +1375,7 @@ var main = async () => {
1311
1375
  const agentsOk = failed.length === 0 && !configuredNothing;
1312
1376
  const reachChecked = phone !== null;
1313
1377
  const reachesYou = phone?.reachesYou ?? true;
1378
+ markStep("closing");
1314
1379
  const closing = describeClosing({
1315
1380
  agentsOk,
1316
1381
  configuredNothing,
@@ -1365,7 +1430,7 @@ var main = async () => {
1365
1430
  process.exit(exitCode);
1366
1431
  }
1367
1432
  };
1368
- main().catch((err) => {
1433
+ main().catch(async (err) => {
1369
1434
  if (err instanceof UsageError) {
1370
1435
  console.log();
1371
1436
  console.log(` ${yellow("!")} ${err.message}`);
@@ -1377,11 +1442,11 @@ main().catch((err) => {
1377
1442
  console.log();
1378
1443
  console.log(` ${dim("Cancelled. Nothing further was changed.")}`);
1379
1444
  console.log();
1380
- process.exit(130);
1445
+ process.exit(await abortWith("interrupted", 130));
1381
1446
  }
1382
1447
  console.log();
1383
1448
  console.log(` ${yellow("!")} Setup failed: ${formatError(err)}`);
1384
1449
  console.log(` ${dim("Run")} ${cyan("npx @pushary/agent-hooks doctor")} ${dim("after fixing the issue, then rerun setup.")}`);
1385
1450
  console.log();
1386
- process.exit(1);
1451
+ process.exit(await abortWith("crashed", EXIT.FAILED));
1387
1452
  });
@@ -7,7 +7,7 @@ import {
7
7
  describeReach,
8
8
  fetchChannels,
9
9
  reachVerdict
10
- } from "../chunk-NGZMVCXN.js";
10
+ } from "../chunk-GFHQ26ST.js";
11
11
  import "../chunk-3EGEA4KH.js";
12
12
  import {
13
13
  readKeySource
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-5ZDYDU2Q.js";
4
+ } from "../chunk-XOUYM27W.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStopFailure
4
- } from "../chunk-5ZDYDU2Q.js";
4
+ } from "../chunk-XOUYM27W.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -494,7 +494,7 @@ var buildPairLinks = (pairId, publicKeyB64, baseUrl = apiBase()) => {
494
494
  deepLink: `pushary://pair?pk=${pk}&id=${id}`
495
495
  };
496
496
  };
497
- var connectViaAppPairing = async () => {
497
+ var connectViaAppPairing = async (options = {}) => {
498
498
  const keypair = generateKeypair();
499
499
  const pairId = await startPairing(keypair.publicKeyB64);
500
500
  if (!pairId) {
@@ -520,7 +520,8 @@ var connectViaAppPairing = async () => {
520
520
  const onInterrupt = () => {
521
521
  cancelled = true;
522
522
  stop(yellow("!"), "Cancelled");
523
- void cancelPairing(pairId).finally(() => process.exit(EXIT_ABORTED));
523
+ const flushed = options.onCancelled?.();
524
+ void Promise.allSettled([Promise.resolve(flushed), cancelPairing(pairId)]).finally(() => process.exit(EXIT_ABORTED));
524
525
  };
525
526
  process.once("SIGINT", onInterrupt);
526
527
  try {
@@ -25,7 +25,7 @@ import {
25
25
  sendNotification,
26
26
  throttlePass,
27
27
  waitForAnswer
28
- } from "./chunk-5ZDYDU2Q.js";
28
+ } from "./chunk-XOUYM27W.js";
29
29
  import {
30
30
  isGatingMoment,
31
31
  recordKeylessMoment
@@ -928,7 +928,7 @@ ${injected}` : injected;
928
928
  };
929
929
  var detectInstallMode = () => (process.argv[1] ?? "").includes("_npx") ? "npx" : "cli";
930
930
  var reportEvent = async (event, options = {}) => {
931
- const apiKey = getApiKey();
931
+ const apiKey = options.apiKey ?? getApiKey();
932
932
  const baseUrl = getBaseUrl();
933
933
  return withRetry(async () => {
934
934
  const res = await fetch(`${baseUrl}/api/agent/event`, {
@@ -73,6 +73,16 @@ interface AgentEvent {
73
73
  interface ReportEventOptions {
74
74
  maxAttempts?: number;
75
75
  timeoutMs?: number;
76
+ /**
77
+ * Authenticate with this key instead of the ambient one.
78
+ *
79
+ * getApiKey() reads the environment and ~/.pushary/config.json, so it can only
80
+ * ever speak for a key that is already installed. setup resolves a key minutes
81
+ * before it writes one, and its abort beacons run inside that window: without
82
+ * an override they post as nobody, so the runs that fail before saveApiKey are
83
+ * exactly the runs that cannot report failing.
84
+ */
85
+ apiKey?: string;
76
86
  }
77
87
  interface EventResponse {
78
88
  readonly ok?: boolean;
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-SDXF5I46.js";
3
+ } from "../chunk-SONXGCZY.js";
4
4
  import "../chunk-CAJZAFVS.js";
5
5
  import "../chunk-YHG74UFF.js";
6
6
  import {
@@ -15,7 +15,7 @@ import {
15
15
  reportEvent,
16
16
  resolvePolicy,
17
17
  waitForAnswer
18
- } from "../chunk-5ZDYDU2Q.js";
18
+ } from "../chunk-XOUYM27W.js";
19
19
  import "../chunk-WLGEY4UX.js";
20
20
  import "../chunk-BSZYIAZL.js";
21
21
  import "../chunk-SAF6HGAA.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.64.0",
3
+ "version": "0.64.2",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",