@pushary/agent-hooks 0.89.13 → 0.90.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/data/SKILL.md +1 -1
  3. package/data/cursor-plugin/skills/pushary/SKILL.md +1 -1
  4. package/data/vscode-plugin/skills/pushary/SKILL.md +1 -1
  5. package/dist/bin/pushary-bell-hook.js +4 -4
  6. package/dist/bin/pushary-bell.js +8 -8
  7. package/dist/bin/pushary-claude.js +72 -21
  8. package/dist/bin/pushary-clean.js +16 -42
  9. package/dist/bin/pushary-codex-bridge.js +37 -12
  10. package/dist/bin/pushary-codex-hook.js +45 -12
  11. package/dist/bin/pushary-codex.js +7 -7
  12. package/dist/bin/pushary-connect.js +11 -10
  13. package/dist/bin/pushary-daemon.js +187 -63
  14. package/dist/bin/pushary-disconnect.js +8 -8
  15. package/dist/bin/pushary-doctor.js +108 -25
  16. package/dist/bin/pushary-elicitation-hook.js +4 -4
  17. package/dist/bin/pushary-gemini-bridge.js +33 -11
  18. package/dist/bin/pushary-gemini-hook.js +21 -10
  19. package/dist/bin/pushary-hook.js +13 -13
  20. package/dist/bin/pushary-login.js +18 -7
  21. package/dist/bin/pushary-logout.js +6 -6
  22. package/dist/bin/pushary-mode.js +28 -21
  23. package/dist/bin/pushary-notification-hook.js +7 -7
  24. package/dist/bin/pushary-opencode-hook.js +8 -8
  25. package/dist/bin/pushary-permission-denied-hook.js +11 -11
  26. package/dist/bin/pushary-permission-hook.js +11 -11
  27. package/dist/bin/pushary-post-hook.js +14 -8
  28. package/dist/bin/pushary-prompt-hook.js +7 -7
  29. package/dist/bin/pushary-session-end-hook.js +7 -7
  30. package/dist/bin/pushary-session-start-hook.js +7 -7
  31. package/dist/bin/pushary-setup.js +168 -45
  32. package/dist/bin/pushary-stats.js +4 -4
  33. package/dist/bin/pushary-status.js +11 -10
  34. package/dist/bin/pushary-stop-hook.js +14 -8
  35. package/dist/bin/pushary-stopfailure-hook.js +7 -7
  36. package/dist/bin/pushary-suggestions.js +2 -2
  37. package/dist/bin/pushary-transcripts.js +6 -6
  38. package/dist/bin/pushary-upgrade.js +15 -15
  39. package/dist/bin/pushary-wait.js +5 -5
  40. package/dist/bin/pushary.js +1 -1
  41. package/dist/{chunk-B2HND22S.js → chunk-2LAHXHBR.js} +1 -1
  42. package/dist/{chunk-GMJDQDXU.js → chunk-43GMYSS4.js} +1 -0
  43. package/dist/{chunk-OX35BSPV.js → chunk-4LOPSGLP.js} +70 -11
  44. package/dist/{chunk-JTBDBKHU.js → chunk-54ELC7D7.js} +1 -1
  45. package/dist/{chunk-C56VHSXU.js → chunk-5NAZ7SFS.js} +1 -1
  46. package/dist/{chunk-4IPSMEGT.js → chunk-7FE2KABY.js} +12 -2
  47. package/dist/{chunk-RAEPAUNJ.js → chunk-7TPQEWUZ.js} +1 -1
  48. package/dist/{chunk-ZO43JTT3.js → chunk-AJYV3VGU.js} +1 -1
  49. package/dist/{chunk-QFJZHVVL.js → chunk-AZPIQG4G.js} +35 -1
  50. package/dist/{chunk-5MNTWAAJ.js → chunk-B6H2KAQC.js} +29 -22
  51. package/dist/{chunk-MNTZKFZI.js → chunk-BQBEH6TV.js} +5 -5
  52. package/dist/{chunk-WHDSJ6PD.js → chunk-BVBEURWJ.js} +1 -1
  53. package/dist/{chunk-ZWEKONU6.js → chunk-CELX75MW.js} +1 -1
  54. package/dist/{chunk-ZP3SNUP7.js → chunk-CNPAT3HL.js} +3 -2
  55. package/dist/{chunk-YXT55PYF.js → chunk-DYP6SMOR.js} +1 -1
  56. package/dist/chunk-DZDI62LY.js +145 -0
  57. package/dist/{chunk-VF3RTVWY.js → chunk-EZIEXHYM.js} +15 -7
  58. package/dist/{chunk-7JZCADBR.js → chunk-F2ICEWZL.js} +101 -19
  59. package/dist/{chunk-JHJHCMAF.js → chunk-F7OURW3C.js} +24 -6
  60. package/dist/{chunk-VASZ6TWW.js → chunk-FQL5E6NG.js} +2 -2
  61. package/dist/{chunk-JVLB5CAJ.js → chunk-GL3FFM7A.js} +7 -4
  62. package/dist/{chunk-5YEF55AX.js → chunk-H2QZRQR2.js} +1 -1
  63. package/dist/{chunk-S4BLNILH.js → chunk-I463AEG3.js} +1 -1
  64. package/dist/{chunk-ANMHIZZY.js → chunk-JW4KO5AF.js} +1 -1
  65. package/dist/{chunk-3YGGEDJK.js → chunk-KUZWXPUS.js} +3 -2
  66. package/dist/{chunk-N4QJ3CLX.js → chunk-L4E2BIJ2.js} +80 -7
  67. package/dist/{chunk-NBDU3R3G.js → chunk-OSALSIUF.js} +1 -1
  68. package/dist/{chunk-KNLKHJZR.js → chunk-PYA4FLKI.js} +32 -21
  69. package/dist/chunk-QCGNGEGE.js +15 -0
  70. package/dist/chunk-S6LYP7QA.js +127 -0
  71. package/dist/{chunk-M3UABYRT.js → chunk-UZNKVRLE.js} +1 -1
  72. package/dist/{chunk-FOGW6O2Y.js → chunk-VHWCVD42.js} +1 -1
  73. package/dist/{chunk-QINYXLA5.js → chunk-VNMKIJZB.js} +15 -2
  74. package/dist/{chunk-CBGGUEKE.js → chunk-Y6BWPERV.js} +90 -7
  75. package/dist/src/index.d.ts +3 -0
  76. package/dist/src/index.js +11 -11
  77. package/package.json +1 -1
  78. package/dist/chunk-MSTHT2M7.js +0 -72
@@ -5,7 +5,7 @@ import {
5
5
  FILE_TARGET_TOOLS,
6
6
  redactSecrets,
7
7
  redactSecretsDeep
8
- } from "./chunk-QINYXLA5.js";
8
+ } from "./chunk-VNMKIJZB.js";
9
9
 
10
10
  // ../hook-mapping/src/describe.ts
11
11
  import { isAbsolute, relative, resolve } from "path";
@@ -44,6 +44,48 @@ var eventPrefixes = {
44
44
  Read: (input) => `read: ${input.file_path ?? "unknown"}`,
45
45
  TodoWrite: (input) => describeTodoProgress(input.todos)
46
46
  };
47
+ var MCP_TOOL_PREFIX = "mcp__";
48
+ var parseMcpToolName = (toolName) => {
49
+ if (!toolName.startsWith(MCP_TOOL_PREFIX)) return void 0;
50
+ const rest = toolName.slice(MCP_TOOL_PREFIX.length);
51
+ const split = rest.indexOf("__");
52
+ if (split <= 0 || split + 2 >= rest.length) return void 0;
53
+ return { server: rest.slice(0, split), tool: rest.slice(split + 2) };
54
+ };
55
+ var NETWORK_TARGET_KEYS = ["url", "urls", "href", "host", "hosts", "hostname", "domain", "domains"];
56
+ var ABSOLUTE_HTTP_URL = /^https?:\/\//i;
57
+ var HOSTNAME = /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+(?::\d{1,5})?$/i;
58
+ var hostOfUrl = (value) => {
59
+ try {
60
+ return new URL(value).host || void 0;
61
+ } catch {
62
+ return void 0;
63
+ }
64
+ };
65
+ var hostOf = (value, bareHostnames) => {
66
+ if (typeof value !== "string") return void 0;
67
+ const trimmed = value.trim();
68
+ if (ABSOLUTE_HTTP_URL.test(trimmed)) return hostOfUrl(trimmed);
69
+ return bareHostnames && HOSTNAME.test(trimmed) ? trimmed : void 0;
70
+ };
71
+ var firstHost = (value, bareHostnames) => Array.isArray(value) ? value.map((entry) => hostOf(entry, bareHostnames)).find((host) => host !== void 0) : hostOf(value, bareHostnames);
72
+ var deriveNetworkTarget = (toolInput) => {
73
+ for (const key of NETWORK_TARGET_KEYS) {
74
+ const host = firstHost(toolInput[key], true);
75
+ if (host) return host;
76
+ }
77
+ return Object.values(toolInput).map((value) => firstHost(value, false)).find((host) => host !== void 0);
78
+ };
79
+ var describeUnbuiltCall = (toolName, toolInput, format) => {
80
+ const mcp = parseMcpToolName(toolName);
81
+ const host = deriveNetworkTarget(toolInput);
82
+ if (mcp) {
83
+ if (format === "event") return `called ${mcp.tool} on ${mcp.server}`;
84
+ return host ? `call ${mcp.tool} on ${mcp.server} to reach ${host}` : `call ${mcp.tool} on ${mcp.server}`;
85
+ }
86
+ if (host) return format === "event" ? `accessed ${host}` : `network access to ${host}`;
87
+ return void 0;
88
+ };
47
89
  var EVENT_ACTION_MAX = 120;
48
90
  var HOOK_FALLBACK_MAX = 200;
49
91
  var UNNAMED_ACTION = "this request";
@@ -51,11 +93,12 @@ var describeToolCall = (toolName, toolInput, format = "hook") => {
51
93
  if (!toolName) return "";
52
94
  const prefixes = format === "hook" ? hookPrefixes : eventPrefixes;
53
95
  const builder = prefixes[toolName];
96
+ const unbuilt = builder ? void 0 : describeUnbuiltCall(toolName, toolInput ?? {}, format);
54
97
  const detail = JSON.stringify(toolInput ?? {});
55
- const raw = builder ? builder(toolInput) : format === "hook" ? detail === "{}" || detail === void 0 ? toolName : `${toolName}: ${detail}` : `${toolName}: done`;
98
+ const raw = builder ? builder(toolInput) : unbuilt ?? (format === "hook" ? detail === "{}" || detail === void 0 ? toolName : `${toolName}: ${detail}` : `${toolName}: done`);
56
99
  const redacted = redactSecrets(raw);
57
100
  if (format === "event") return redacted.slice(0, EVENT_ACTION_MAX);
58
- return builder ? redacted : redacted.slice(0, HOOK_FALLBACK_MAX);
101
+ return builder || unbuilt ? redacted : redacted.slice(0, HOOK_FALLBACK_MAX);
59
102
  };
60
103
  var TOOL_TARGET_MAX_LENGTH = 80;
61
104
  var deriveCommandHead = (command) => {
@@ -76,7 +119,9 @@ var deriveToolTarget = (toolName, toolInput) => {
76
119
  if (dot <= 0) return void 0;
77
120
  return base.slice(dot).slice(0, TOOL_TARGET_MAX_LENGTH);
78
121
  }
79
- return void 0;
122
+ const mcp = parseMcpToolName(toolName);
123
+ if (mcp) return mcp.server.slice(0, TOOL_TARGET_MAX_LENGTH);
124
+ return deriveNetworkTarget(toolInput)?.slice(0, TOOL_TARGET_MAX_LENGTH);
80
125
  };
81
126
  var SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun", "npx", "deno", "rake", "make", "just", "task", "turbo"]);
82
127
  var RUN_SUBCOMMANDS = /* @__PURE__ */ new Set(["run", "run-script", "exec", "task"]);
@@ -210,7 +255,7 @@ var rawActionBody = (toolName, toolInput) => {
210
255
  if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor" || toolName === "run_shell_command" || toolName === "shell" || toolName === "apply_patch") {
211
256
  return firstString(toolInput.command);
212
257
  }
213
- return void 0;
258
+ return firstString(toolInput.url, toolInput.href);
214
259
  };
215
260
  var deriveActionBody = (toolName, toolInput) => {
216
261
  try {
@@ -264,11 +309,36 @@ var summarizeStop = (input) => {
264
309
  if (running > 0) return `Paused with ${running} background task${running === 1 ? "" : "s"} still running`;
265
310
  const crons = countArray(input.session_crons);
266
311
  if (crons > 0) return `Idle until a scheduled task wakes it (${crons} pending)`;
267
- return summarizeFinalMessage(input.last_assistant_message);
312
+ return summarizeFinalMessage(input.last_assistant_message ?? input.prompt_response);
313
+ };
314
+ var subagentStopAction = (agentType) => {
315
+ const name = typeof agentType === "string" ? agentType.replace(/\s+/g, " ").trim() : "";
316
+ return name ? `${name} subagent finished` : "Subagent finished";
317
+ };
318
+ var subagentStopSummary = (input) => {
319
+ const message = input.last_assistant_message ?? input.prompt_response;
320
+ if (typeof message === "string" && message.trim()) return summarizeStop(input);
321
+ return subagentStopAction(input.agent_type);
322
+ };
323
+ var COMPACTION_TRIGGERS = {
324
+ manual: "on request",
325
+ auto: "automatically"
326
+ };
327
+ var compactionAction = (hookEvent, trigger) => {
328
+ const how = typeof trigger === "string" ? COMPACTION_TRIGGERS[trigger] : void 0;
329
+ const base = hookEvent === "PostCompact" ? "Compacted context" : "Compacting context";
330
+ return how ? `${base} ${how}` : base;
268
331
  };
269
332
  var sessionStartAction = (source) => source === "resume" ? "Session resumed" : "Session started";
270
333
  var sessionEndAction = (reason) => typeof reason === "string" && SESSION_END_ACTIONS[reason] || "Session ended";
271
334
  var taskTitleFrom = (prompt) => typeof prompt === "string" ? prompt.replace(/\s+/g, " ").trim().slice(0, TASK_TITLE_MAX_LENGTH) || void 0 : void 0;
335
+ var asString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
336
+ var sessionIdentityOf = (payload) => {
337
+ const sessionId = asString(payload.session_id);
338
+ const agentId = asString(payload.agent_id);
339
+ if (!agentId) return { sessionId };
340
+ return { sessionId: agentId, parentSessionId: sessionId };
341
+ };
272
342
 
273
343
  export {
274
344
  HOOK_FALLBACK_MAX,
@@ -287,7 +357,10 @@ export {
287
357
  deriveBlocker,
288
358
  AGENT_IDENTITIES,
289
359
  summarizeStop,
360
+ subagentStopSummary,
361
+ compactionAction,
290
362
  sessionStartAction,
291
363
  sessionEndAction,
292
- taskTitleFrom
364
+ taskTitleFrom,
365
+ sessionIdentityOf
293
366
  };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-BC3VCZ3E.js";
4
4
  import {
5
5
  resolveInstallSourceFor
6
- } from "./chunk-ANMHIZZY.js";
6
+ } from "./chunk-JW4KO5AF.js";
7
7
  import {
8
8
  backupFile,
9
9
  readJsonSafe,
@@ -3,6 +3,10 @@ import {
3
3
  openSealedKey,
4
4
  publicKeyFingerprint
5
5
  } from "./chunk-3EGEA4KH.js";
6
+ import {
7
+ humanIsTty,
8
+ writeHuman
9
+ } from "./chunk-QCGNGEGE.js";
6
10
  import {
7
11
  useColor
8
12
  } from "./chunk-5RUIFDTP.js";
@@ -11,7 +15,7 @@ import {
11
15
  } from "./chunk-BSZYIAZL.js";
12
16
  import {
13
17
  reportedInstallSource
14
- } from "./chunk-ANMHIZZY.js";
18
+ } from "./chunk-JW4KO5AF.js";
15
19
 
16
20
  // src/reach.ts
17
21
  var reachVerdict = (channels) => {
@@ -263,16 +267,6 @@ var describeRoundTrip = (outcome) => {
263
267
  }
264
268
  };
265
269
 
266
- // src/cli/human.ts
267
- var stream = process.stdout;
268
- var setHumanStream = (target) => {
269
- stream = target;
270
- };
271
- var writeHuman = (text) => {
272
- stream.write(text);
273
- };
274
- var humanIsTty = () => stream === process.stdout && process.stdout.isTTY === true;
275
-
276
270
  // src/onboarding.ts
277
271
  import qrcodeTerminal from "qrcode-terminal";
278
272
  var paint = (open) => (s) => useColor() ? `${open}${s}\x1B[0m` : s;
@@ -650,6 +644,20 @@ var confirmAppConnection = async (apiKey) => {
650
644
  }
651
645
  return sendAndConfirm(apiKey, channels);
652
646
  };
647
+ var planAppConnect = (channels) => channels.precise && ownedApp(channels) === 0 ? { kind: "unpaired", repair: describeAppRepair(reachVerdict(channels)) ?? [] } : { kind: "send" };
648
+ var connectExistingApp = async (apiKey) => {
649
+ const probe = await fetchChannels(apiKey);
650
+ if (probe.kind !== "ok") return describeProbeFailure(probe);
651
+ const plan = planAppConnect(probe.status);
652
+ if (plan.kind === "send") {
653
+ console.log(` ${check} ${describeExistingConnection(probe.status)}`);
654
+ return sendAndConfirm(apiKey, probe.status);
655
+ }
656
+ console.log(` ${yellow("!")} No Pushary app of yours is connected to this site, and nothing was paired just now.`);
657
+ for (const line of plan.repair) console.log(` ${dim(line)}`);
658
+ console.log(` ${dim("No app yet? Get it at")} ${cyan("https://pushary.com/download")}`);
659
+ return phoneFailure("app_not_paired", plan.repair);
660
+ };
653
661
  var printConnectInstructions = async (apiKey) => {
654
662
  const probe = await fetchChannels(apiKey);
655
663
  if (probe.kind !== "ok") return describeProbeFailure(probe);
@@ -737,6 +745,8 @@ var ackPairing = async (pairId) => {
737
745
  } catch {
738
746
  }
739
747
  };
748
+ var pairFailed = (reason) => ({ kind: "failed", reason });
749
+ var pairingEscapeApplies = (outcome) => outcome.reason === "timeout" || outcome.reason === "expired" || outcome.reason === "cancelled";
740
750
  var buildPairLinks = (pairId, publicKeyB64, baseUrl = apiBase(), code = null) => {
741
751
  const id = encodeURIComponent(pairId);
742
752
  const pk = encodeURIComponent(publicKeyB64);
@@ -760,7 +770,7 @@ var connectViaAppPairing = async (options = {}) => {
760
770
  const started = await startPairing(keypair.publicKeyB64);
761
771
  if (!started) {
762
772
  console.log(` ${yellow("!")} Couldn't start pairing. Check your connection and try again.`);
763
- return null;
773
+ return pairFailed("unreachable");
764
774
  }
765
775
  const { pairId, code } = started;
766
776
  const { qrLink, universalLink, deepLink } = buildPairLinks(pairId, keypair.publicKeyB64, apiBase(), code);
@@ -800,14 +810,14 @@ var connectViaAppPairing = async (options = {}) => {
800
810
  let consecutiveFailures = 0;
801
811
  try {
802
812
  while (Date.now() < deadline) {
803
- if (cancelled) return null;
813
+ if (cancelled) return pairFailed("cancelled");
804
814
  const claim = await claimPairing(pairId);
805
815
  if (claim === null) {
806
816
  consecutiveFailures += 1;
807
817
  if (consecutiveFailures >= PAIR_MAX_CONSECUTIVE_FAILURES) {
808
818
  stop(yellow("!"), "Can't reach pushary.com to finish pairing. Check your connection, then re-run setup.");
809
819
  await cancelPairing(pairId);
810
- return null;
820
+ return pairFailed("unreachable");
811
821
  }
812
822
  await sleep(PAIR_POLL_INTERVAL_MS);
813
823
  continue;
@@ -820,29 +830,29 @@ var connectViaAppPairing = async (options = {}) => {
820
830
  } catch {
821
831
  stop(yellow("!"), "Pairing failed to decrypt \u2014 re-run setup to try again");
822
832
  await cancelPairing(pairId);
823
- return null;
833
+ return pairFailed("decrypt");
824
834
  }
825
835
  await ackPairing(pairId);
826
836
  stop(check, "App connected");
827
- return { apiKey, handle: claim.siteSlug };
837
+ return { kind: "paired", apiKey, handle: claim.siteSlug };
828
838
  }
829
839
  if (claim?.status === "expired") {
830
840
  stop(yellow("!"), "Pairing expired \u2014 re-run setup to try again");
831
- return null;
841
+ return pairFailed("expired");
832
842
  }
833
843
  if (claim?.status === "failed") {
834
844
  stop(yellow("!"), describePairFailure(claim.reason));
835
- return null;
845
+ return pairFailed("rejected");
836
846
  }
837
847
  await sleep(PAIR_POLL_INTERVAL_MS);
838
848
  }
839
849
  stop(yellow("!"), "Timed out waiting for the app");
840
850
  await cancelPairing(pairId);
841
- return null;
851
+ return pairFailed("timeout");
842
852
  } catch (err) {
843
853
  stop(yellow("!"), `Pairing error ${dim(`(${err instanceof Error ? err.message : "network"})`)}`);
844
854
  await cancelPairing(pairId);
845
- return null;
855
+ return pairFailed("unreachable");
846
856
  } finally {
847
857
  process.off("SIGINT", onInterrupt);
848
858
  }
@@ -853,10 +863,11 @@ export {
853
863
  describeReach,
854
864
  verifyRoundTrip,
855
865
  describeRoundTrip,
856
- setHumanStream,
857
866
  fetchChannels,
858
867
  connectDevice,
859
868
  confirmAppConnection,
869
+ connectExistingApp,
860
870
  printConnectInstructions,
871
+ pairingEscapeApplies,
861
872
  connectViaAppPairing
862
873
  };
@@ -0,0 +1,15 @@
1
+ // src/cli/human.ts
2
+ var stream = process.stdout;
3
+ var setHumanStream = (target) => {
4
+ stream = target;
5
+ };
6
+ var writeHuman = (text) => {
7
+ stream.write(text);
8
+ };
9
+ var humanIsTty = () => stream === process.stdout && process.stdout.isTTY === true;
10
+
11
+ export {
12
+ setHumanStream,
13
+ writeHuman,
14
+ humanIsTty
15
+ };
@@ -0,0 +1,127 @@
1
+ import {
2
+ HOOK_LOCATOR_BINARY
3
+ } from "./chunk-VNMKIJZB.js";
4
+
5
+ // src/diagnostics/bridge.ts
6
+ import { execFileSync } from "child_process";
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { homedir } from "os";
9
+ import { dirname, join } from "path";
10
+ var BRIDGE_BUNDLE_IDENTIFIER = "com.pushary.app";
11
+ var BRIDGE_HELPER_SUFFIX = "/Contents/Helpers/pushary-bridge";
12
+ var BRIDGE_BUNDLE_ROOTS = ["/Applications/Pushary.app", "$HOME/Applications/Pushary.app"];
13
+ var expandHome = (path, home) => path.replaceAll("$HOME", home);
14
+ var parseBridgeStub = (contents, stubPath) => {
15
+ const helperSuffix = contents.match(/^H=(\S+)$/m)?.[1] ?? BRIDGE_HELPER_SUFFIX;
16
+ const cachePath = contents.match(/^C="([^"\n]+)"$/m)?.[1] ?? join(dirname(stubPath), ".bridge-cache");
17
+ const rootsLine = contents.match(/^for P in ((?:"[^"\n]*"\s*)+); do$/m)?.[1];
18
+ const bundleRoots = rootsLine ? [...rootsLine.matchAll(/"([^"]*)"/g)].map((match) => match[1]) : BRIDGE_BUNDLE_ROOTS;
19
+ const bundleIdentifier = contents.match(/kMDItemCFBundleIdentifier == "([^"]+)"/)?.[1] ?? BRIDGE_BUNDLE_IDENTIFIER;
20
+ return { helperSuffix, cachePath, bundleRoots, bundleIdentifier };
21
+ };
22
+ var resolveBridgeStub = (stubPath, probe) => {
23
+ const contents = probe.readFile(stubPath);
24
+ if (contents === null) return { kind: "no-stub", stub: stubPath };
25
+ const shape = parseBridgeStub(contents, stubPath);
26
+ const helperIn = (bundle) => `${bundle}${shape.helperSuffix}`;
27
+ const looked = [];
28
+ for (const root of shape.bundleRoots.map((root2) => expandHome(root2, probe.home))) {
29
+ looked.push(root);
30
+ if (probe.exists(helperIn(root))) return { kind: "bundle", helper: helperIn(root), via: "root" };
31
+ }
32
+ const cached = shape.cachePath ? probe.readFile(shape.cachePath)?.split("\n")[0]?.trim() : void 0;
33
+ if (cached) {
34
+ looked.push(cached);
35
+ if (probe.exists(helperIn(cached))) return { kind: "bundle", helper: helperIn(cached), via: "cache" };
36
+ }
37
+ for (const found of probe.mdfind(shape.bundleIdentifier)) {
38
+ looked.push(found);
39
+ if (probe.exists(helperIn(found))) return { kind: "bundle", helper: helperIn(found), via: "spotlight" };
40
+ }
41
+ return { kind: "missing", stub: stubPath, looked };
42
+ };
43
+ var BRIDGE_GONE_MESSAGE = "the Mac app is gone but its hooks remain; run pushary setup or reinstall the app";
44
+ var describeBridgeResolution = (resolution) => {
45
+ switch (resolution.kind) {
46
+ case "bundle":
47
+ return `${resolution.helper} (${resolution.via === "root" ? "installed" : resolution.via === "cache" ? "cached path" : "found by Spotlight"})`;
48
+ case "missing":
49
+ return BRIDGE_GONE_MESSAGE;
50
+ case "no-stub":
51
+ return `${resolution.stub} is not there; ${BRIDGE_GONE_MESSAGE}`;
52
+ }
53
+ };
54
+ var isBridgeLocator = (executable) => executable !== null && executable.split(/[/\\]/).pop() === HOOK_LOCATOR_BINARY;
55
+ var spotlight = (bundleIdentifier) => {
56
+ if (process.platform !== "darwin") return [];
57
+ try {
58
+ return execFileSync("/usr/bin/mdfind", [`kMDItemCFBundleIdentifier == "${bundleIdentifier}"`], {
59
+ encoding: "utf-8",
60
+ timeout: 5e3,
61
+ stdio: ["ignore", "pipe", "ignore"]
62
+ }).split("\n").map((line) => line.trim()).filter((line) => line !== "");
63
+ } catch {
64
+ return [];
65
+ }
66
+ };
67
+ var readOrNull = (path) => {
68
+ try {
69
+ return readFileSync(path, "utf-8");
70
+ } catch {
71
+ return null;
72
+ }
73
+ };
74
+ var liveBridgeProbe = () => ({
75
+ exists: existsSync,
76
+ readFile: readOrNull,
77
+ mdfind: spotlight,
78
+ home: homedir()
79
+ });
80
+
81
+ // src/diagnostics/service-runtime.ts
82
+ var unescapeXml = (value) => value.replaceAll("&quot;", '"').replaceAll("&apos;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
83
+ var unquoteSystemd = (value) => value.replaceAll('\\"', '"').replaceAll("\\\\", "\\").replaceAll("%%", "%");
84
+ var readServiceExecPath = (contents, platform) => {
85
+ if (platform === "darwin") {
86
+ const program = contents.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]*)<\/string>/)?.[1];
87
+ return program ? unescapeXml(program) : null;
88
+ }
89
+ if (platform === "linux") {
90
+ const exec = contents.match(/^ExecStart="((?:[^"\\]|\\.)*)"/m)?.[1];
91
+ return exec ? unquoteSystemd(exec) : null;
92
+ }
93
+ if (platform === "win32") {
94
+ const command = contents.match(/<Command>([^<]*)<\/Command>/)?.[1];
95
+ return command ? unescapeXml(command) : null;
96
+ }
97
+ return null;
98
+ };
99
+ var MANAGER_SEGMENTS = [
100
+ [".nvm", "nvm"],
101
+ ["nvm", "nvm"],
102
+ ["fnm", "fnm"],
103
+ [".fnm", "fnm"],
104
+ [".volta", "volta"],
105
+ ["volta", "volta"],
106
+ [".asdf", "asdf"],
107
+ ["asdf", "asdf"]
108
+ ];
109
+ var versionManagerOf = (execPath) => {
110
+ const segments = execPath.split(/[/\\]/);
111
+ for (const segment of segments) {
112
+ const hit = MANAGER_SEGMENTS.find(([name]) => name === segment);
113
+ if (hit) return hit[1];
114
+ }
115
+ return null;
116
+ };
117
+ var describeVersionManagerPin = (execPath, manager) => `The background service is pinned to ${execPath}, which ${manager} manages. Switching Node versions strands it; run pushary daemon install afterwards.`;
118
+
119
+ export {
120
+ resolveBridgeStub,
121
+ describeBridgeResolution,
122
+ isBridgeLocator,
123
+ liveBridgeProbe,
124
+ readServiceExecPath,
125
+ versionManagerOf,
126
+ describeVersionManagerPin
127
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readTranscriptPreference
3
- } from "./chunk-NBDU3R3G.js";
3
+ } from "./chunk-OSALSIUF.js";
4
4
 
5
5
  // src/transcript-setting.ts
6
6
  var fromEnv = () => {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isSafeReadOnlyCommand
3
- } from "./chunk-QINYXLA5.js";
3
+ } from "./chunk-VNMKIJZB.js";
4
4
 
5
5
  // src/ledger.ts
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from "fs";
@@ -91,7 +91,7 @@ var INSTALL_SOURCES = [
91
91
  var isInstallSource = (value) => typeof value === "string" && INSTALL_SOURCES.includes(value);
92
92
  var normalizeInstallSource = (value) => isInstallSource(value) ? value : "unknown";
93
93
  var HOOK_BUDGETS = {
94
- claude: { budgetSeconds: 120, guardSeconds: 10 },
94
+ claude: { budgetSeconds: 600, guardSeconds: 10 },
95
95
  codex: { budgetSeconds: 180, guardSeconds: 10 },
96
96
  gemini: { budgetSeconds: 180, guardSeconds: 10 },
97
97
  // The Cursor gate is a dependency-free .mjs that cannot import this module; it
@@ -1054,6 +1054,8 @@ var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repo
1054
1054
  };
1055
1055
  var isModeStateDegraded = (state) => state?.degraded === true;
1056
1056
  var KILL_REASON = "Stopped by user, this agent was halted from Pushary";
1057
+ var PUSHARY_TOOL_PREFIX = "mcp__pushary__";
1058
+ var isPusharyOwnTool = (toolName) => toolName.startsWith(PUSHARY_TOOL_PREFIX);
1057
1059
  var strictestScopeVerdict = (scope, paths) => {
1058
1060
  if (paths.length === 0) return evaluateScope(scope, void 0);
1059
1061
  let worst = { outcome: "in_scope" };
@@ -1085,6 +1087,13 @@ var ASKED_IN_EVERY_MODE = new Set(ASKED_IN_EVERY_MODE_TOOLS);
1085
1087
  var isAskedInEveryMode = (toolName) => ASKED_IN_EVERY_MODE.has(toolName);
1086
1088
  var resolveGate = (input) => {
1087
1089
  const { modeState, config, toolName, toolInputs, cwd, repoKey, sessionId } = input;
1090
+ if (isPusharyOwnTool(toolName)) {
1091
+ return {
1092
+ kind: "defer",
1093
+ policy: resolvePolicyAcross(config, toolName, modeState.mode, toolInputs, cwd, repoKey, sessionId),
1094
+ reason: "own_tool"
1095
+ };
1096
+ }
1088
1097
  if (modeState.kill) return { kind: "kill", reason: KILL_REASON };
1089
1098
  const policy = resolvePolicyAcross(config, toolName, modeState.mode, toolInputs, cwd, repoKey, sessionId);
1090
1099
  const verdict = verdictForPolicy(policy);
@@ -1128,9 +1137,11 @@ var modeStateFromResponse = (data) => {
1128
1137
  // Validated rather than cast: a malformed contract must read as no contract,
1129
1138
  // never as one that silently matches nothing.
1130
1139
  scope: isScopeContract(data.scope) ? data.scope : null,
1131
- degraded: false
1140
+ degraded: false,
1141
+ askHandoff: data.askHandoff === true
1132
1142
  };
1133
1143
  };
1144
+ var stoppedOutput = (output, stopReason) => ({ ...output, continue: false, stopReason });
1134
1145
  var claudePreToolUseOutput = (decision, reason, updatedInput) => ({
1135
1146
  hookSpecificOutput: {
1136
1147
  hookEventName: "PreToolUse",
@@ -1362,10 +1373,12 @@ export {
1362
1373
  resolveAutoResolveOrigin,
1363
1374
  resolvePolicy,
1364
1375
  KILL_REASON,
1376
+ isPusharyOwnTool,
1365
1377
  shouldDeferToNativeMode,
1366
1378
  ASKED_IN_EVERY_MODE_TOOLS,
1367
1379
  resolveGate,
1368
1380
  modeStateFromResponse,
1381
+ stoppedOutput,
1369
1382
  claudePreToolUseOutput,
1370
1383
  PENDING_COMMAND_PREFIX,
1371
1384
  pendingCommandEvents,
@@ -1,22 +1,23 @@
1
1
  import {
2
2
  pusharyDir
3
- } from "./chunk-3YGGEDJK.js";
3
+ } from "./chunk-KUZWXPUS.js";
4
4
  import {
5
5
  readJsonSafe
6
6
  } from "./chunk-4LL6LG5X.js";
7
7
 
8
8
  // src/process-ownership.ts
9
9
  import { createHash, randomUUID } from "crypto";
10
- import { mkdirSync, readdirSync, unlinkSync, writeFileSync } from "fs";
10
+ import { mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
11
11
  import { createConnection, createServer } from "net";
12
12
  import { join } from "path";
13
13
  var MAX_PROOF_BYTES = 1024;
14
14
  var ownershipDir = (options) => options.dir ?? join(pusharyDir(), "processes");
15
15
  var proofPath = (dir, sessionId) => join(dir, `${createHash("sha256").update(sessionId).digest("hex")}.json`);
16
+ var isOptionalTimestamp = (value) => value === void 0 || typeof value === "string" && Number.isFinite(Date.parse(value));
16
17
  var isProof = (value) => {
17
18
  if (!value || typeof value !== "object") return false;
18
19
  const proof = value;
19
- return typeof proof.sessionId === "string" && Number.isInteger(proof.pid) && proof.pid > 1 && Number.isInteger(proof.port) && proof.port > 0 && proof.port <= 65535 && typeof proof.token === "string" && proof.token.length > 0 && (proof.spawnId === void 0 || typeof proof.spawnId === "string");
20
+ return typeof proof.sessionId === "string" && Number.isInteger(proof.pid) && proof.pid > 1 && Number.isInteger(proof.port) && proof.port > 0 && proof.port <= 65535 && typeof proof.token === "string" && proof.token.length > 0 && (proof.spawnId === void 0 || typeof proof.spawnId === "string") && isOptionalTimestamp(proof.startedAt) && isOptionalTimestamp(proof.heartbeatAt);
20
21
  };
21
22
  var removeProof = (path, token) => {
22
23
  const current = readJsonSafe(path);
@@ -34,16 +35,74 @@ var processIsAlive = (pid) => {
34
35
  return error.code === "EPERM";
35
36
  }
36
37
  };
37
- var pruneProofs = (dir) => {
38
+ var fileTimes = (path) => {
39
+ try {
40
+ const stat = statSync(path);
41
+ return { createdAt: stat.birthtimeMs || stat.mtimeMs, modifiedAt: stat.mtimeMs };
42
+ } catch {
43
+ return { createdAt: 0, modifiedAt: 0 };
44
+ }
45
+ };
46
+ var parseTimestamp = (value) => {
47
+ if (value === void 0) return null;
48
+ const parsed = Date.parse(value);
49
+ return Number.isFinite(parsed) ? parsed : null;
50
+ };
51
+ var proofEntry = (proof, path) => {
52
+ const times = fileTimes(path);
53
+ const recordedStart = parseTimestamp(proof.startedAt);
54
+ const startedAt = recordedStart ?? times.createdAt;
55
+ return {
56
+ sessionId: proof.sessionId,
57
+ pid: proof.pid,
58
+ spawnId: proof.spawnId ?? null,
59
+ startedAt,
60
+ lastSeenAt: parseTimestamp(proof.heartbeatAt) ?? recordedStart ?? times.modifiedAt,
61
+ alive: processIsAlive(proof.pid)
62
+ };
63
+ };
64
+ var safeUnlink = (path) => {
65
+ try {
66
+ unlinkSync(path);
67
+ } catch {
68
+ }
69
+ };
70
+ var listProcessProofs = (options = {}) => {
71
+ const dir = ownershipDir(options);
72
+ const entries = [];
38
73
  try {
39
74
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
40
75
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
41
76
  const path = join(dir, entry.name);
42
77
  const stored = readJsonSafe(path);
43
- if (stored.kind !== "ok" || !isProof(stored.value) || !processIsAlive(stored.value.pid)) unlinkSync(path);
78
+ if (stored.kind !== "ok" || !isProof(stored.value)) {
79
+ if (options.reapDead) safeUnlink(path);
80
+ continue;
81
+ }
82
+ const listed = proofEntry(stored.value, path);
83
+ if (!listed.alive && options.reapDead) safeUnlink(path);
84
+ entries.push(listed);
44
85
  }
45
86
  } catch {
46
87
  }
88
+ return entries;
89
+ };
90
+ var pruneProofs = (dir) => {
91
+ listProcessProofs({ dir, reapDead: true });
92
+ };
93
+ var wasSpawned = (entry) => entry.spawnId !== null;
94
+ var silentSpawnedProofs = (entries, now, silenceMs) => {
95
+ const freshestByPid = /* @__PURE__ */ new Map();
96
+ for (const entry of entries) {
97
+ freshestByPid.set(entry.pid, Math.max(freshestByPid.get(entry.pid) ?? 0, entry.lastSeenAt));
98
+ }
99
+ const reported = /* @__PURE__ */ new Set();
100
+ return entries.filter((entry) => {
101
+ if (!entry.alive || !wasSpawned(entry) || reported.has(entry.pid)) return false;
102
+ if (now - (freshestByPid.get(entry.pid) ?? 0) < silenceMs) return false;
103
+ reported.add(entry.pid);
104
+ return true;
105
+ });
47
106
  };
48
107
  var startProcessOwnership = (identity, options = {}) => {
49
108
  const dir = ownershipDir(options);
@@ -88,7 +147,7 @@ var startProcessOwnership = (identity, options = {}) => {
88
147
  return;
89
148
  }
90
149
  try {
91
- proof = { ...identity, port: address.port, token };
150
+ proof = { ...identity, port: address.port, token, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
92
151
  mkdirSync(dir, { recursive: true, mode: 448 });
93
152
  pruneProofs(dir);
94
153
  writeFileSync(path, JSON.stringify(proof), {
@@ -107,6 +166,14 @@ var startProcessOwnership = (identity, options = {}) => {
107
166
  process.once("exit", cleanup);
108
167
  return {
109
168
  ready,
169
+ touch(at = Date.now()) {
170
+ if (stopped || !proof) return;
171
+ proof = { ...proof, heartbeatAt: new Date(at).toISOString() };
172
+ try {
173
+ writeFileSync(path, JSON.stringify(proof), { encoding: "utf8", mode: 384 });
174
+ } catch {
175
+ }
176
+ },
110
177
  stop() {
111
178
  if (stopped) return;
112
179
  stopped = true;
@@ -184,9 +251,25 @@ var verifyProof = (proof, options) => new Promise((resolve) => {
184
251
  });
185
252
  });
186
253
 
254
+ // src/session-idle.ts
255
+ var SESSION_IDLE_TIMEOUT_MS = 10 * 6e4;
256
+ var SESSION_REAP_GRACE_MS = 5 * 6e4;
257
+ var SESSION_REAP_SILENCE_MS = SESSION_IDLE_TIMEOUT_MS + SESSION_REAP_GRACE_MS;
258
+ var idleDeadlinePassed = (idleSince, now, timeoutMs = SESSION_IDLE_TIMEOUT_MS) => idleSince !== null && now - idleSince >= timeoutMs;
259
+ var describeIdleWindow = (timeoutMs) => {
260
+ const minutes = Math.round(timeoutMs / 6e4);
261
+ return minutes >= 1 ? `${minutes} minute${minutes === 1 ? "" : "s"}` : `${Math.round(timeoutMs / 1e3)}s`;
262
+ };
263
+
187
264
  export {
265
+ listProcessProofs,
266
+ silentSpawnedProofs,
188
267
  startProcessOwnership,
189
268
  withSpawnProcessOwnership,
190
269
  ownsLiveProcess,
191
- findLiveProcessBySpawnId
270
+ findLiveProcessBySpawnId,
271
+ SESSION_IDLE_TIMEOUT_MS,
272
+ SESSION_REAP_SILENCE_MS,
273
+ idleDeadlinePassed,
274
+ describeIdleWindow
192
275
  };