@pushary/agent-hooks 0.68.0 → 0.70.0

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +3 -2
  3. package/data/cursor-plugin/scripts/pushary-gate.mjs +70 -15
  4. package/data/vscode-plugin/scripts/pushary-gate.mjs +59 -11
  5. package/dist/bin/pushary-claude.js +21 -11
  6. package/dist/bin/pushary-clean.js +60 -22
  7. package/dist/bin/pushary-codex-hook.js +38 -23
  8. package/dist/bin/pushary-codex.js +1 -1
  9. package/dist/bin/pushary-connect.js +6 -4
  10. package/dist/bin/pushary-daemon.js +3 -3
  11. package/dist/bin/pushary-doctor.js +120 -28
  12. package/dist/bin/pushary-gemini-hook.js +32 -16
  13. package/dist/bin/pushary-hook.js +5 -5
  14. package/dist/bin/pushary-login.js +2 -2
  15. package/dist/bin/pushary-logout.js +2 -2
  16. package/dist/bin/pushary-mode.js +2 -2
  17. package/dist/bin/pushary-notification-hook.js +1 -1
  18. package/dist/bin/pushary-permission-denied-hook.js +3 -3
  19. package/dist/bin/pushary-permission-hook.js +3 -3
  20. package/dist/bin/pushary-post-hook.js +1 -1
  21. package/dist/bin/pushary-prompt-hook.js +1 -1
  22. package/dist/bin/pushary-session-end-hook.js +1 -1
  23. package/dist/bin/pushary-session-start-hook.js +1 -1
  24. package/dist/bin/pushary-setup.js +90 -13
  25. package/dist/bin/pushary-stats.js +2 -2
  26. package/dist/bin/pushary-status.js +4 -4
  27. package/dist/bin/pushary-stop-hook.js +1 -1
  28. package/dist/bin/pushary-stopfailure-hook.js +1 -1
  29. package/dist/bin/pushary-suggestions.js +2 -2
  30. package/dist/bin/pushary-upgrade.js +2 -2
  31. package/dist/bin/pushary-wait.js +2 -2
  32. package/dist/bin/pushary.js +1 -1
  33. package/dist/{chunk-JGH37QRB.js → chunk-7OYGFJYZ.js} +1 -1
  34. package/dist/chunk-B7R4YSAB.js +38 -0
  35. package/dist/{chunk-ATP6UQPH.js → chunk-BBK3TTU2.js} +1 -1
  36. package/dist/{chunk-XOUYM27W.js → chunk-FZIQSDTQ.js} +207 -113
  37. package/dist/chunk-GD275GEX.js +66 -0
  38. package/dist/{chunk-SONXGCZY.js → chunk-HU43EZ5Q.js} +45 -27
  39. package/dist/{chunk-KB4ODLGB.js → chunk-I4BYZIX4.js} +2 -0
  40. package/dist/{chunk-YMFZUXL5.js → chunk-MGFZXUUR.js} +57 -4
  41. package/dist/{chunk-YZT2CBC3.js → chunk-NQTRA7H5.js} +1 -1
  42. package/dist/src/index.d.ts +1 -0
  43. package/dist/src/index.js +3 -3
  44. package/package.json +1 -1
  45. package/dist/chunk-YHG74UFF.js +0 -12
@@ -3,8 +3,9 @@ import {
3
3
  } from "./chunk-CAJZAFVS.js";
4
4
  import {
5
5
  denyReasonFrom,
6
- isDeferAnswer
7
- } from "./chunk-YHG74UFF.js";
6
+ isDeferAnswer,
7
+ resolveGate
8
+ } from "./chunk-GD275GEX.js";
8
9
  import {
9
10
  DEFAULT_SESSION,
10
11
  askUser,
@@ -19,13 +20,12 @@ import {
19
20
  readLastPrompt,
20
21
  readLastUserPrompt,
21
22
  repoKeyFor,
22
- resolvePolicy,
23
23
  savePendingQuestion,
24
24
  scopePathFor,
25
25
  sendNotification,
26
26
  throttlePass,
27
27
  waitForAnswer
28
- } from "./chunk-XOUYM27W.js";
28
+ } from "./chunk-FZIQSDTQ.js";
29
29
  import {
30
30
  isGatingMoment,
31
31
  recordKeylessMoment
@@ -33,11 +33,8 @@ import {
33
33
  import {
34
34
  buildDecisionEpisodeFeatures,
35
35
  effectiveWaitSeconds,
36
- evaluateScope,
37
36
  hookWaitClamped,
38
- hookWaitDeadline,
39
- scopeChangeReason,
40
- scopeRequiresHuman
37
+ hookWaitDeadline
41
38
  } from "./chunk-DQAN3JQP.js";
42
39
  import {
43
40
  getMachineId
@@ -383,6 +380,18 @@ var handleKeyless = (input) => {
383
380
  }
384
381
  return void 0;
385
382
  };
383
+ var claudeGateInput = (modeState, config, input) => {
384
+ const scopePath = modeState.scope ? scopePathFor(input.tool_name, input.tool_input, input.cwd) : void 0;
385
+ return {
386
+ modeState,
387
+ config,
388
+ toolName: input.tool_name,
389
+ toolInputs: [input.tool_input ?? {}],
390
+ scopePaths: scopePath ? [scopePath] : [],
391
+ cwd: input.cwd,
392
+ repoKey: repoKeyFor(input.cwd)
393
+ };
394
+ };
386
395
  var handlePreToolUse = async (input) => {
387
396
  if (input.tool_name.startsWith("mcp__pushary__")) return void 0;
388
397
  let apiKey;
@@ -394,26 +403,26 @@ var handlePreToolUse = async (input) => {
394
403
  try {
395
404
  const modeState = await fetchModeState(apiKey, input.session_id);
396
405
  const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true);
397
- if (modeState.kill) {
398
- return deny("Stopped by user, this agent was halted from Pushary");
399
- }
406
+ const verdict = resolveGate(claudeGateInput(modeState, policy, input));
407
+ if (verdict.kind === "kill") return deny(verdict.reason);
400
408
  if (input.tool_name === "AskUserQuestion") {
401
409
  return handleAskUserQuestion(apiKey, input);
402
410
  }
403
411
  if (shouldDeferToNativeMode(input.permission_mode, input.tool_name)) {
404
412
  return void 0;
405
413
  }
406
- const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input, input.cwd, repoKeyFor(input.cwd));
407
- const scopePath = modeState.scope ? scopePathFor(input.tool_name, input.tool_input, input.cwd) : void 0;
408
- const scopeVerdict = evaluateScope(modeState.scope, scopePath);
409
- const scopeReason = scopeChangeReason(scopeVerdict);
410
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") {
411
- if (!scopeRequiresHuman(scopeVerdict)) return allow();
412
- }
413
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
414
- return deny(`Denied by policy for ${toolPolicy.tool}`);
414
+ switch (verdict.kind) {
415
+ case "allow":
416
+ return allow();
417
+ case "deny":
418
+ return deny(verdict.reason);
419
+ // State we could not establish, with an auto-approve on the table. No
420
+ // opinion, so Claude Code's own flow runs as if the hook were absent.
421
+ case "defer":
422
+ return void 0;
423
+ default:
424
+ return dispatchModeHandler(apiKey, input, verdict.policy, getMachineId(), modeState.trainingConsent, verdict.scopeReason, verdict.scopePath);
415
425
  }
416
- return dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent, scopeReason, scopePath);
417
426
  } catch {
418
427
  return void 0;
419
428
  }
@@ -452,13 +461,22 @@ var handlePermissionRequest = async (input) => {
452
461
  try {
453
462
  const modeState = await fetchModeState(apiKey, input.session_id);
454
463
  const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true);
455
- if (modeState.kill) return permReqDeny("Stopped by user \u2014 this agent was halted from Pushary");
456
- const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input, input.cwd, repoKeyFor(input.cwd));
457
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return permReqAllow();
458
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
459
- return permReqDeny(`Denied by policy for ${toolPolicy.tool}`);
464
+ const verdict = resolveGate(claudeGateInput(modeState, policy, input));
465
+ switch (verdict.kind) {
466
+ case "kill":
467
+ return permReqDeny(verdict.reason);
468
+ case "allow":
469
+ return permReqAllow();
470
+ case "deny":
471
+ return permReqDeny(verdict.reason);
472
+ // No opinion, so Claude Code's own dialog shows.
473
+ case "defer":
474
+ return void 0;
475
+ default:
476
+ return toPermissionRequestOutput(
477
+ await dispatchModeHandler(apiKey, input, verdict.policy, getMachineId(), modeState.trainingConsent, verdict.scopeReason, verdict.scopePath)
478
+ );
460
479
  }
461
- return toPermissionRequestOutput(await dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent));
462
480
  } catch {
463
481
  return void 0;
464
482
  }
@@ -320,6 +320,8 @@ export {
320
320
  isPluginRegistered,
321
321
  pluginLocationSnippet,
322
322
  vscodeSettingsTargets,
323
+ detectAgent,
324
+ agentProbes,
323
325
  detectAllAgents,
324
326
  isDetected,
325
327
  shortenHome,
@@ -45,6 +45,27 @@ var describeReach = (channels) => {
45
45
  if (verdict.kind === "unattributable") return `${summary}, and no way to tell whose`;
46
46
  return summary;
47
47
  };
48
+ var describeAppRepair = (verdict) => {
49
+ switch (verdict.kind) {
50
+ case "yours":
51
+ return null;
52
+ case "nobody":
53
+ return [
54
+ "Open the Pushary app, sign in, and allow notifications.",
55
+ "If this terminal was never paired with your phone, run: npx @pushary/agent-hooks@latest setup --connect app"
56
+ ];
57
+ case "others":
58
+ return [
59
+ `Reachable devices belong to other members of this workspace (${verdict.otherMembers}).`,
60
+ "Sign in on the Pushary app with your own account so approvals can reach you."
61
+ ];
62
+ case "unattributable":
63
+ return [
64
+ "Something is reachable here, but not identifiably yours.",
65
+ "Sign in on the Pushary app so your approvals are routed to your device."
66
+ ];
67
+ }
68
+ };
48
69
 
49
70
  // src/cli/human.ts
50
71
  var stream = process.stdout;
@@ -324,7 +345,12 @@ var printQr = (url) => new Promise((resolve, reject) => {
324
345
  reject(err);
325
346
  }
326
347
  });
327
- var phoneFailure = (detail) => ({ reachesYou: false, outcome: null, detail });
348
+ var phoneFailure = (detail, repair) => ({
349
+ reachesYou: false,
350
+ outcome: null,
351
+ detail,
352
+ ...repair ? { repair } : {}
353
+ });
328
354
  var waitForDevice = async (apiKey, baseline) => {
329
355
  const deadline = Date.now() + CONNECT_TIMEOUT_MS;
330
356
  let consecutiveFailures = 0;
@@ -469,9 +495,11 @@ var confirmAppConnection = async (apiKey) => {
469
495
  const channels = probe.status;
470
496
  const ownedApp = channels.yours?.appDevices ?? channels.appDevices;
471
497
  if (channels.precise && ownedApp === 0) {
472
- console.log(` ${yellow("!")} Paired, but the app hasn't registered for notifications yet.`);
473
- console.log(` ${dim("Open the Pushary app and allow notifications, then run")} ${cyan("pushary doctor")}${dim(".")}`);
474
- return phoneFailure("app_not_registered");
498
+ const repair = describeAppRepair(reachVerdict(channels)) ?? [];
499
+ console.log(` ${yellow("!")} No device of yours is registered for notifications yet.`);
500
+ for (const line of repair) console.log(` ${dim(line)}`);
501
+ console.log(` ${dim("Then run")} ${cyan("pushary doctor")}${dim(".")}`);
502
+ return phoneFailure("app_not_registered", repair);
475
503
  }
476
504
  return sendAndConfirm(apiKey, channels);
477
505
  };
@@ -533,6 +561,7 @@ var cancelPairing = async (pairId) => {
533
561
  } catch {
534
562
  }
535
563
  };
564
+ var PAIR_MAX_CONSECUTIVE_FAILURES = 8;
536
565
  var claimPairing = async (pairId) => {
537
566
  try {
538
567
  const res = await fetch(`${apiBase()}/api/mobile/pair/claim?id=${encodeURIComponent(pairId)}`, {
@@ -544,6 +573,17 @@ var claimPairing = async (pairId) => {
544
573
  return null;
545
574
  }
546
575
  };
576
+ var ackPairing = async (pairId) => {
577
+ try {
578
+ await fetch(`${apiBase()}/api/mobile/pair/ack`, {
579
+ method: "POST",
580
+ headers: { "Content-Type": "application/json" },
581
+ body: JSON.stringify({ pairId }),
582
+ signal: AbortSignal.timeout(5e3)
583
+ });
584
+ } catch {
585
+ }
586
+ };
547
587
  var buildPairLinks = (pairId, publicKeyB64, baseUrl = apiBase()) => {
548
588
  const id = encodeURIComponent(pairId);
549
589
  const pk = encodeURIComponent(publicKeyB64);
@@ -593,10 +633,22 @@ var connectViaAppPairing = async (options = {}) => {
593
633
  void Promise.allSettled([Promise.resolve(flushed), cancelPairing(pairId)]).finally(() => process.exit(EXIT_ABORTED));
594
634
  };
595
635
  process.once("SIGINT", onInterrupt);
636
+ let consecutiveFailures = 0;
596
637
  try {
597
638
  while (Date.now() < deadline) {
598
639
  if (cancelled) return null;
599
640
  const claim = await claimPairing(pairId);
641
+ if (claim === null) {
642
+ consecutiveFailures += 1;
643
+ if (consecutiveFailures >= PAIR_MAX_CONSECUTIVE_FAILURES) {
644
+ stop(yellow("!"), "Can't reach pushary.com to finish pairing. Check your connection, then re-run setup.");
645
+ await cancelPairing(pairId);
646
+ return null;
647
+ }
648
+ await sleep(PAIR_POLL_INTERVAL_MS);
649
+ continue;
650
+ }
651
+ consecutiveFailures = 0;
600
652
  if (claim?.status === "authorized") {
601
653
  let apiKey;
602
654
  try {
@@ -606,6 +658,7 @@ var connectViaAppPairing = async (options = {}) => {
606
658
  await cancelPairing(pairId);
607
659
  return null;
608
660
  }
661
+ await ackPairing(pairId);
609
662
  stop(check, "App connected");
610
663
  return { apiKey, handle: claim.siteSlug };
611
664
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getPackageVersion
3
- } from "./chunk-JGH37QRB.js";
3
+ } from "./chunk-7OYGFJYZ.js";
4
4
  import {
5
5
  getMachineId
6
6
  } from "./chunk-RN3NOEJF.js";
@@ -36,6 +36,7 @@ interface ModeState {
36
36
  readonly relayUrl: string | null;
37
37
  readonly trainingConsent?: boolean;
38
38
  readonly scope?: ScopeContract | null;
39
+ readonly degraded?: boolean;
39
40
  }
40
41
  declare const fetchModeState: (apiKey: string, sessionId?: string) => Promise<ModeState>;
41
42
  declare const fetchModeOverride: (apiKey: string) => Promise<ApprovalMode | null>;
package/dist/src/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-SONXGCZY.js";
3
+ } from "../chunk-HU43EZ5Q.js";
4
4
  import "../chunk-CAJZAFVS.js";
5
- import "../chunk-YHG74UFF.js";
5
+ import "../chunk-GD275GEX.js";
6
6
  import {
7
7
  askUser,
8
8
  cancelQuestion,
@@ -15,7 +15,7 @@ import {
15
15
  reportEvent,
16
16
  resolvePolicy,
17
17
  waitForAnswer
18
- } from "../chunk-XOUYM27W.js";
18
+ } from "../chunk-FZIQSDTQ.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.68.0",
3
+ "version": "0.70.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -1,12 +0,0 @@
1
- // src/answer.ts
2
- var denyReasonFrom = (value, note) => {
3
- const trimmed = note?.trim();
4
- if (trimmed) return `Denied from your phone: ${trimmed}`;
5
- return value && value !== "no" && value !== "yes" ? `Denied from your phone: ${value}` : "Denied via push notification";
6
- };
7
- var isDeferAnswer = (value) => value === "defer";
8
-
9
- export {
10
- denyReasonFrom,
11
- isDeferAnswer
12
- };