@pushary/agent-hooks 0.94.0 → 0.95.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 (83) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/data/SKILL.md +31 -24
  3. package/data/cursor-plugin/.cursor-plugin/plugin.json +1 -1
  4. package/data/cursor-plugin/CONTRIBUTING.md +31 -19
  5. package/data/cursor-plugin/skills/pushary/SKILL.md +30 -23
  6. package/data/vscode-plugin/.claude-plugin/plugin.json +1 -1
  7. package/data/vscode-plugin/CONTRIBUTING.md +31 -19
  8. package/data/vscode-plugin/skills/pushary/SKILL.md +30 -23
  9. package/dist/bin/pushary-bell-hook.js +26 -5
  10. package/dist/bin/pushary-bell.js +8 -8
  11. package/dist/bin/pushary-claude.js +343 -365
  12. package/dist/bin/pushary-clean.js +28 -23
  13. package/dist/bin/pushary-codex-bridge.js +12 -12
  14. package/dist/bin/pushary-codex-hook.js +28 -10
  15. package/dist/bin/pushary-codex.js +7 -7
  16. package/dist/bin/pushary-connect.js +9 -9
  17. package/dist/bin/pushary-daemon.js +41 -26
  18. package/dist/bin/pushary-disconnect.js +8 -6
  19. package/dist/bin/pushary-doctor.js +42 -39
  20. package/dist/bin/pushary-elicitation-hook.js +24 -4
  21. package/dist/bin/pushary-gemini-bridge.js +12 -12
  22. package/dist/bin/pushary-gemini-hook.js +27 -9
  23. package/dist/bin/pushary-hook.js +28 -11
  24. package/dist/bin/pushary-login.js +10 -10
  25. package/dist/bin/pushary-logout.js +7 -7
  26. package/dist/bin/pushary-mode.js +6 -6
  27. package/dist/bin/pushary-notification-hook.js +26 -8
  28. package/dist/bin/pushary-opencode-hook.js +27 -9
  29. package/dist/bin/pushary-permission-denied-hook.js +29 -11
  30. package/dist/bin/pushary-permission-hook.js +29 -11
  31. package/dist/bin/pushary-post-hook.js +26 -8
  32. package/dist/bin/pushary-prompt-hook.js +26 -8
  33. package/dist/bin/pushary-session-end-hook.js +30 -9
  34. package/dist/bin/pushary-session-start-hook.js +29 -14
  35. package/dist/bin/pushary-setup.js +59 -53
  36. package/dist/bin/pushary-stats.js +2 -2
  37. package/dist/bin/pushary-status.js +11 -11
  38. package/dist/bin/pushary-stop-hook.js +26 -8
  39. package/dist/bin/pushary-stopfailure-hook.js +26 -8
  40. package/dist/bin/pushary-suggestions.js +3 -3
  41. package/dist/bin/pushary-transcript-register.d.ts +1 -0
  42. package/dist/bin/pushary-transcript-register.js +42 -0
  43. package/dist/bin/pushary-transcripts.js +4 -4
  44. package/dist/bin/pushary-upgrade.js +19 -18
  45. package/dist/bin/pushary-wait.js +6 -6
  46. package/dist/{chunk-Y6XXFXVB.js → chunk-3F4VV5TS.js} +1 -1
  47. package/dist/chunk-77U3ZVV6.js +86 -0
  48. package/dist/{chunk-RH3TM7TO.js → chunk-7MGF5Q7G.js} +1 -1
  49. package/dist/{chunk-WY4ALDYN.js → chunk-AQ5OAYOH.js} +1 -1
  50. package/dist/chunk-BWNU4MX4.js +185 -0
  51. package/dist/{chunk-OYNPLEXP.js → chunk-C2MV4G4A.js} +1 -1
  52. package/dist/chunk-CC625NRG.js +503 -0
  53. package/dist/{chunk-ZTJF6ANY.js → chunk-CU5KI425.js} +2 -2
  54. package/dist/{chunk-HSAVPZ46.js → chunk-IFCS6LXB.js} +3 -3
  55. package/dist/{chunk-36LR2LOT.js → chunk-IP63LMZM.js} +10 -8
  56. package/dist/{chunk-BSEAOCCQ.js → chunk-IZRI4M3C.js} +1 -1
  57. package/dist/{chunk-5HGLZGI4.js → chunk-J55K6YS4.js} +6 -5
  58. package/dist/{chunk-6BIMJIRG.js → chunk-JSASMV3H.js} +1 -1
  59. package/dist/{chunk-6CUUA7HO.js → chunk-K7ZZGI2Y.js} +7 -1
  60. package/dist/{chunk-TDUTNYAQ.js → chunk-M64I4T7J.js} +1 -1
  61. package/dist/{chunk-ZO2XG3CX.js → chunk-MF64S7WO.js} +1 -1
  62. package/dist/chunk-N6FAXQDD.js +276 -0
  63. package/dist/{chunk-UZFVNEEU.js → chunk-PDFVIRPB.js} +1 -1
  64. package/dist/chunk-PQ4K74WL.js +30 -0
  65. package/dist/{chunk-ROHFVGXM.js → chunk-R7NPGCSD.js} +1 -1
  66. package/dist/{chunk-QGA2ZB4I.js → chunk-RNH2AE52.js} +1 -1
  67. package/dist/{chunk-NVPYRDCT.js → chunk-SSQ5X6SA.js} +1 -1
  68. package/dist/{chunk-OB4RF2TF.js → chunk-T23EGPRI.js} +1 -1
  69. package/dist/{chunk-ODL52O5V.js → chunk-T5B3LRYF.js} +1 -1
  70. package/dist/{chunk-Y3UGCQQC.js → chunk-TTO76NZJ.js} +1 -1
  71. package/dist/{chunk-46P3VES5.js → chunk-UFGDCMUD.js} +2 -1
  72. package/dist/{chunk-2ORRI66J.js → chunk-UQSKCQBO.js} +1 -1
  73. package/dist/{chunk-4PE2SQVM.js → chunk-VLKEAYJ4.js} +1 -1
  74. package/dist/chunk-WJZUTMMU.js +194 -0
  75. package/dist/{chunk-QKNLW7H6.js → chunk-Y24D5ZWS.js} +7 -7
  76. package/dist/{chunk-YANV5TGK.js → chunk-YPYPI6BG.js} +8 -90
  77. package/dist/{chunk-6WYL3XIP.js → chunk-ZOR5OJZJ.js} +4 -3
  78. package/dist/chunk-ZW3KAWTM.js +266 -0
  79. package/dist/src/index.js +11 -11
  80. package/package.json +3 -2
  81. package/dist/chunk-FYOIW5RV.js +0 -539
  82. package/dist/chunk-RX75MRGC.js +0 -368
  83. /package/dist/{chunk-Z6CP5ZZD.js → chunk-VOWZ5VOO.js} +0 -0
@@ -1,12 +1,9 @@
1
1
  import {
2
2
  openCodeConfigDirFrom
3
- } from "./chunk-BSEAOCCQ.js";
3
+ } from "./chunk-IZRI4M3C.js";
4
4
  import {
5
5
  codexHomeFrom
6
- } from "./chunk-Y6XXFXVB.js";
7
- import {
8
- HOOK_LOCATOR_BINARY
9
- } from "./chunk-Z6CP5ZZD.js";
6
+ } from "./chunk-3F4VV5TS.js";
10
7
 
11
8
  // src/vscode-config.ts
12
9
  import { homedir } from "os";
@@ -265,12 +262,13 @@ var instructionBody = (label) => `## Pushary notifications and approvals
265
262
 
266
263
  You have Pushary MCP tools that reach the user on their phone. Use them proactively. Do not wait for the user to ask.
267
264
 
268
- - When you need the user to make a decision or answer a clarifying question, call \`ask_user\` instead of guessing or stalling. Use type "confirm" for yes/no, "select" for a fixed set of options, and "input" for free text. \`ask_user\` blocks until the user replies or it times out, so you do not need a separate wait step.
265
+ - When you need the user to make a decision or answer a clarifying question, call \`ask_user\` instead of guessing or stalling. Use type "confirm" for yes/no, "select" for a fixed set of options, and "input" for free text. Read \`answered\`, \`status\` and the returned handoff instead of assuming the tool blocked. Silence is not consent; choice/text answers do not authorize a separate action.
269
266
  - Ask once, on one channel. If you also have a built-in way to prompt the user, use that or \`ask_user\` for a given question, never both. \`ask_user\` is the one that reaches the user when they are away from this machine, so prefer it. Raising both strands the user in whichever prompt you are not waiting on.
270
- - Before any risky or irreversible step (deleting files, force pushing, spending money, sending external messages), call \`ask_user\` with type "confirm" and wait for approval.
271
- - Before starting a multi-step run that will change several files, call \`propose_scope\` once with the paths you expect to change, anything you promise not to touch, and what done means. The user agrees to the boundary in one tap. A ratified boundary NARROWS what may proceed without asking, it does not pre-approve work inside it, so an action that would normally need approval still needs it. Skip it for a single quick edit, and do not propose a new scope mid-run just to widen one: if you need to go outside the agreed scope, do the work and let the approval that follows widen it.
267
+ - Honor authorization already granted in this session. Ask before a risky or irreversible action outside that authorization, or when an enforced host policy requires it. Do not ask again for an already authorized step, and never bypass an enforced gate.
268
+ - Call \`propose_scope\` only for an unresolved or requested file boundary. A ratified scope narrows what may proceed; it does not bypass other approval rules. Do not add a plan approval merely because work spans several files, and do not propose a new scope mid-run just to widen it. Let the installed gate request any scope expansion before the edit executes.
272
269
  - When you are blocked, stuck, or hit an error you cannot resolve on your own, call \`send_notification\` so the user knows, and call \`ask_user\` if you need a decision to continue.
273
- - When a task that took several steps finishes, call \`send_notification\` so the user knows it is done. Always pass \`context\` with a \`type\` of "task_complete", "error" or "info": it is what tells Pushary this is a task update, so the user's own setting for where updates land can route it.
270
+ - When meaningful work finishes while the user is away, or they requested an alert, call \`send_notification\` so the user knows it is done. Skip trivial completions and duplicate reports to someone already reading. Always pass \`context\` with a \`type\` of "task_complete", "error" or "info": it is what tells Pushary this is a task update, so the user's own setting for where updates land can route it.
271
+ - Confirm notifications can offer lock-screen actions; arbitrary choices and text open the app. The Mac notch handles personal account questions, not Partner customer enrollment.
274
272
  - Use \`cancel_question\` to retract a pending question once it is no longer needed.
275
273
  - Respect the user's Pushary delivery mode: it decides where an approval is surfaced (phone, terminal, or awareness only), not whether it is required. In Terminal mode do not expect a phone answer; the approval waits in the terminal. If the Pushary hook already gated an action, a single approval is enough, do not ask again.
276
274
 
@@ -332,82 +330,6 @@ var hasInstructionBlock = (filePath) => {
332
330
  // src/hooks-spec.ts
333
331
  var CURSOR_GATE_SCRIPT = "pushary-gate.mjs";
334
332
 
335
- // src/diagnostics/bridge.ts
336
- import { execFileSync } from "child_process";
337
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
338
- import { homedir as homedir3 } from "os";
339
- import { dirname as dirname3, join as join3 } from "path";
340
- var BRIDGE_BUNDLE_IDENTIFIER = "com.pushary.app";
341
- var BRIDGE_HELPER_SUFFIX = "/Contents/Helpers/pushary-bridge";
342
- var BRIDGE_BUNDLE_ROOTS = ["/Applications/Pushary.app", "$HOME/Applications/Pushary.app"];
343
- var expandHome = (path, home) => path.replaceAll("$HOME", home);
344
- var parseBridgeStub = (contents, stubPath) => {
345
- const helperSuffix = contents.match(/^H=(\S+)$/m)?.[1] ?? BRIDGE_HELPER_SUFFIX;
346
- const cachePath = contents.match(/^C="([^"\n]+)"$/m)?.[1] ?? join3(dirname3(stubPath), ".bridge-cache");
347
- const rootsLine = contents.match(/^for P in ((?:"[^"\n]*"\s*)+); do$/m)?.[1];
348
- const bundleRoots = rootsLine ? [...rootsLine.matchAll(/"([^"]*)"/g)].map((match) => match[1]) : BRIDGE_BUNDLE_ROOTS;
349
- const bundleIdentifier = contents.match(/kMDItemCFBundleIdentifier == "([^"]+)"/)?.[1] ?? BRIDGE_BUNDLE_IDENTIFIER;
350
- return { helperSuffix, cachePath, bundleRoots, bundleIdentifier };
351
- };
352
- var resolveBridgeStub = (stubPath, probe) => {
353
- const contents = probe.readFile(stubPath);
354
- if (contents === null) return { kind: "no-stub", stub: stubPath };
355
- const shape = parseBridgeStub(contents, stubPath);
356
- const helperIn = (bundle) => `${bundle}${shape.helperSuffix}`;
357
- const looked = [];
358
- for (const root of shape.bundleRoots.map((root2) => expandHome(root2, probe.home))) {
359
- looked.push(root);
360
- if (probe.exists(helperIn(root))) return { kind: "bundle", helper: helperIn(root), via: "root" };
361
- }
362
- const cached = shape.cachePath ? probe.readFile(shape.cachePath)?.split("\n")[0]?.trim() : void 0;
363
- if (cached) {
364
- looked.push(cached);
365
- if (probe.exists(helperIn(cached))) return { kind: "bundle", helper: helperIn(cached), via: "cache" };
366
- }
367
- for (const found of probe.mdfind(shape.bundleIdentifier)) {
368
- looked.push(found);
369
- if (probe.exists(helperIn(found))) return { kind: "bundle", helper: helperIn(found), via: "spotlight" };
370
- }
371
- return { kind: "missing", stub: stubPath, looked };
372
- };
373
- var BRIDGE_GONE_MESSAGE = "the Mac app is gone but its hooks remain; run pushary setup or reinstall the app";
374
- var describeBridgeResolution = (resolution) => {
375
- switch (resolution.kind) {
376
- case "bundle":
377
- return `${resolution.helper} (${resolution.via === "root" ? "installed" : resolution.via === "cache" ? "cached path" : "found by Spotlight"})`;
378
- case "missing":
379
- return BRIDGE_GONE_MESSAGE;
380
- case "no-stub":
381
- return `${resolution.stub} is not there; ${BRIDGE_GONE_MESSAGE}`;
382
- }
383
- };
384
- var isBridgeLocator = (executable) => executable !== null && executable.split(/[/\\]/).pop() === HOOK_LOCATOR_BINARY;
385
- var spotlight = (bundleIdentifier) => {
386
- if (process.platform !== "darwin") return [];
387
- try {
388
- return execFileSync("/usr/bin/mdfind", [`kMDItemCFBundleIdentifier == "${bundleIdentifier}"`], {
389
- encoding: "utf-8",
390
- timeout: 5e3,
391
- stdio: ["ignore", "pipe", "ignore"]
392
- }).split("\n").map((line) => line.trim()).filter((line) => line !== "");
393
- } catch {
394
- return [];
395
- }
396
- };
397
- var readOrNull = (path) => {
398
- try {
399
- return readFileSync2(path, "utf-8");
400
- } catch {
401
- return null;
402
- }
403
- };
404
- var liveBridgeProbe = () => ({
405
- exists: existsSync3,
406
- readFile: readOrNull,
407
- mdfind: spotlight,
408
- home: homedir3()
409
- });
410
-
411
333
  export {
412
334
  registerPluginLocation,
413
335
  unregisterPluginLocation,
@@ -425,9 +347,5 @@ export {
425
347
  writeInstructionBlock,
426
348
  removeInstructionBlock,
427
349
  hasInstructionBlock,
428
- CURSOR_GATE_SCRIPT,
429
- resolveBridgeStub,
430
- describeBridgeResolution,
431
- isBridgeLocator,
432
- liveBridgeProbe
350
+ CURSOR_GATE_SCRIPT
433
351
  };
@@ -1,20 +1,21 @@
1
1
  import {
2
2
  readTranscriptPreference
3
- } from "./chunk-6BIMJIRG.js";
3
+ } from "./chunk-JSASMV3H.js";
4
4
 
5
5
  // src/transcript-setting.ts
6
- var fromEnv = () => {
6
+ var transcriptEnvironmentPreference = () => {
7
7
  const value = process.env.PUSHARY_TRANSCRIPTS?.trim().toLowerCase();
8
8
  if (!value) return null;
9
9
  if (["0", "false", "off"].includes(value)) return false;
10
10
  if (["1", "true", "on"].includes(value)) return true;
11
11
  return null;
12
12
  };
13
- var configuredPreference = () => fromEnv() ?? readTranscriptPreference();
13
+ var configuredPreference = () => transcriptEnvironmentPreference() ?? readTranscriptPreference();
14
14
  var transcriptsEnabled = () => configuredPreference() ?? false;
15
15
  var transcriptPreferenceForSetup = (explicit = null) => explicit ?? configuredPreference() ?? true;
16
16
 
17
17
  export {
18
+ transcriptEnvironmentPreference,
18
19
  transcriptsEnabled,
19
20
  transcriptPreferenceForSetup
20
21
  };
@@ -0,0 +1,266 @@
1
+ import {
2
+ isBridgeLocator
3
+ } from "./chunk-77U3ZVV6.js";
4
+ import {
5
+ pusharyHookExecutable
6
+ } from "./chunk-IFCS6LXB.js";
7
+ import {
8
+ BELL_HOOK_BINARY
9
+ } from "./chunk-DSTLA2TC.js";
10
+ import {
11
+ parseFlags
12
+ } from "./chunk-GMXKITVA.js";
13
+ import {
14
+ UsageError
15
+ } from "./chunk-7QLSKOSU.js";
16
+ import {
17
+ isValidApiKey
18
+ } from "./chunk-VOWZ5VOO.js";
19
+
20
+ // src/setup/options.ts
21
+ var AGENT_CHOICES = ["claude_code", "codex", "gemini_cli", "hermes", "cursor", "vscode", "opencode", "custom"];
22
+ var CONNECT_MODES = ["app", "browser", "web", "none"];
23
+ var CONNECT_ALIASES = { auto: "app", pwa: "web" };
24
+ var parseConnectMode = (raw) => {
25
+ const value = raw.trim().toLowerCase();
26
+ if (Object.hasOwn(CONNECT_ALIASES, value)) return CONNECT_ALIASES[value];
27
+ if (CONNECT_MODES.includes(value)) return value;
28
+ throw new UsageError(`--connect must be one of: ${CONNECT_MODES.join(", ")} (got ${raw})`);
29
+ };
30
+ var VERIFY_MODES = ["roundtrip", "push", "none"];
31
+ var parseVerifyMode = (raw) => {
32
+ const value = raw.trim().toLowerCase();
33
+ if (VERIFY_MODES.includes(value)) return value;
34
+ throw new UsageError(`--verify must be one of: ${VERIFY_MODES.join(", ")} (got ${raw})`);
35
+ };
36
+ var CONTROL_MODES = ["auto", "on", "off"];
37
+ var parseControlMode = (raw) => {
38
+ const value = raw.trim().toLowerCase();
39
+ if (CONTROL_MODES.includes(value)) return value;
40
+ throw new UsageError(`--control must be one of: ${CONTROL_MODES.join(", ")} (got ${raw})`);
41
+ };
42
+ var effectiveVerify = (options, context) => {
43
+ if (options.verifyExplicit) return options.verify;
44
+ if (options.verify === "roundtrip") return "roundtrip";
45
+ return context.paired ? "roundtrip" : options.verify;
46
+ };
47
+ var SETUP_FLAGS = {
48
+ bools: ["key-stdin", "skip-phone", "yes", "y", "json", "dry-run", "take-over-hooks", "help", "h"],
49
+ values: ["key", "connect", "agents", "verify", "control", "transcripts"]
50
+ };
51
+ var effectiveControlMode = (options, stored, configPresent) => options.controlExplicit ? options.control : stored ?? (configPresent ? null : options.control);
52
+ var isAgentChoice = (value) => AGENT_CHOICES.includes(value);
53
+ var parseAgentSelection = (raw, yes) => {
54
+ if (raw === void 0) {
55
+ return yes ? { kind: "auto" } : { kind: "ask" };
56
+ }
57
+ const value = raw.trim().toLowerCase();
58
+ if (value === "auto") return { kind: "auto" };
59
+ if (value === "none") return { kind: "none" };
60
+ const parts = value.split(",").map((part) => part.trim()).filter((part) => part !== "");
61
+ if (parts.length === 0) throw new UsageError("--agents needs a value");
62
+ const unknown = parts.filter((part) => !isAgentChoice(part));
63
+ if (unknown.length > 0) {
64
+ throw new UsageError(
65
+ `--agents does not know: ${unknown.join(", ")}. Use auto, none, or any of: ${AGENT_CHOICES.join(", ")}`
66
+ );
67
+ }
68
+ return { kind: "explicit", agents: [...new Set(parts)] };
69
+ };
70
+ var parseSetupOptions = (input) => {
71
+ const flags = parseFlags(input.argv, SETUP_FLAGS);
72
+ const stray = [
73
+ ...flags.command !== void 0 && flags.command !== "setup" ? [flags.command] : [],
74
+ ...flags.rest
75
+ ];
76
+ if (stray.length > 0) {
77
+ throw new UsageError(
78
+ `setup takes no positional arguments (got ${stray.join(", ")}). Did you mean --agents ${stray[0]}?`
79
+ );
80
+ }
81
+ const yes = flags.bools.has("yes") || flags.bools.has("y");
82
+ const help = flags.bools.has("help") || flags.bools.has("h");
83
+ const agentsFlag = flags.values.get("agents");
84
+ const agents = parseAgentSelection(agentsFlag, yes);
85
+ const connectRaw = flags.values.get("connect");
86
+ const connectMode = connectRaw === void 0 ? "app" : parseConnectMode(connectRaw);
87
+ const verifyRaw = flags.values.get("verify");
88
+ const controlRaw = flags.values.get("control");
89
+ const transcriptsRaw = flags.values.get("transcripts")?.trim().toLowerCase();
90
+ if (transcriptsRaw !== void 0 && transcriptsRaw !== "on" && transcriptsRaw !== "off") {
91
+ throw new UsageError(`--transcripts must be one of: on, off (got ${transcriptsRaw})`);
92
+ }
93
+ const nonInteractive = yes || agentsFlag !== void 0 || flags.bools.has("json") || !input.stdinIsTty;
94
+ const verify = verifyRaw !== void 0 ? parseVerifyMode(verifyRaw) : nonInteractive ? "push" : "roundtrip";
95
+ const key = flags.values.get("key")?.trim() || null;
96
+ const keyStdin = flags.bools.has("key-stdin");
97
+ if (key && keyStdin) {
98
+ throw new UsageError("Pass either --key or --key-stdin, not both");
99
+ }
100
+ if (key && !isValidApiKey(key)) {
101
+ throw new UsageError("--key is not a valid API key. Expected the pk_xxx.xxx shape from pushary.com/dashboard/agent/settings");
102
+ }
103
+ return {
104
+ connectMode,
105
+ connectExplicit: connectRaw !== void 0,
106
+ // `--connect none` and `--skip-phone` are the same instruction, so they
107
+ // resolve to the same field rather than being checked separately at the two
108
+ // call sites that would inevitably drift apart.
109
+ skipPhone: flags.bools.has("skip-phone") || connectMode === "none",
110
+ // Anything that means "there is nobody to answer a prompt". A non-TTY stdin
111
+ // is included because that is the case that used to hang: it is better to
112
+ // take the defaults and say so than to wait forever for a keypress.
113
+ //
114
+ // --json counts. Under it stdout carries exactly one object and every human
115
+ // line is redirected to stderr, so a prompt has nowhere to render: the
116
+ // question goes to stderr while the run blocks on a keypress the caller
117
+ // cannot see it is waiting for, and whatever is reading stdout waits forever
118
+ // for an object that never arrives. Asking for machine-readable output is
119
+ // itself a statement that no human is watching.
120
+ nonInteractive,
121
+ json: flags.bools.has("json"),
122
+ dryRun: flags.bools.has("dry-run"),
123
+ help,
124
+ agents,
125
+ key,
126
+ keyStdin,
127
+ verify,
128
+ verifyExplicit: verifyRaw !== void 0,
129
+ control: controlRaw === void 0 ? "auto" : parseControlMode(controlRaw),
130
+ controlExplicit: controlRaw !== void 0,
131
+ transcripts: transcriptsRaw === void 0 ? null : transcriptsRaw === "on",
132
+ takeOverHooks: flags.bools.has("take-over-hooks")
133
+ };
134
+ };
135
+
136
+ // src/setup/hook-ownership.ts
137
+ var commandsIn = (value) => {
138
+ if (Array.isArray(value)) return value.flatMap(commandsIn);
139
+ if (!value || typeof value !== "object") return [];
140
+ return Object.entries(value).flatMap(([key, item]) => key === "command" && typeof item === "string" ? [item] : commandsIn(item));
141
+ };
142
+ var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
143
+ var hasPaidPusharyCommand = (value) => commandsIn(value).some((command) => {
144
+ const executable = pusharyHookExecutable(command);
145
+ const name = executable?.split(/[/\\]/).pop()?.replace(/\.(cmd|exe|mjs)$/i, "");
146
+ return executable !== null && !isBridgeLocator(executable) && name !== BELL_HOOK_BINARY;
147
+ });
148
+ var hasAppCommand = (value) => commandsIn(value).some((command) => {
149
+ const executable = pusharyHookExecutable(command);
150
+ return executable !== null && isBridgeLocator(executable);
151
+ });
152
+ var mcpBearerKey = (document, containerKey, headersKey) => {
153
+ const servers = isRecord(document?.[containerKey]) ? document[containerKey] : null;
154
+ const pushary = isRecord(servers?.pushary) ? servers.pushary : null;
155
+ const headers = isRecord(pushary?.[headersKey]) ? pushary[headersKey] : null;
156
+ const authorization = headers?.Authorization;
157
+ const match = typeof authorization === "string" ? authorization.trim().match(/^Bearer\s+(\S+)$/) : null;
158
+ return match?.[1] ?? null;
159
+ };
160
+ var captureMcpServer = (document, containerKey) => {
161
+ const container = document[containerKey];
162
+ const servers = isRecord(container) ? container : null;
163
+ if (!servers && Object.hasOwn(document, containerKey)) {
164
+ return { present: false, containerValue: structuredClone(container) };
165
+ }
166
+ return servers && Object.hasOwn(servers, "pushary") ? { present: true, value: structuredClone(servers.pushary) } : { present: false };
167
+ };
168
+ var restoreMcpServer = (document, containerKey, snapshot) => {
169
+ const before = JSON.stringify(document);
170
+ const existing = isRecord(document[containerKey]) ? document[containerKey] : null;
171
+ if (snapshot.present) {
172
+ const servers = existing ?? {};
173
+ servers.pushary = structuredClone(snapshot.value);
174
+ document[containerKey] = servers;
175
+ } else if (Object.hasOwn(snapshot, "containerValue")) {
176
+ document[containerKey] = structuredClone(snapshot.containerValue);
177
+ } else if (existing && Object.hasOwn(existing, "pushary")) {
178
+ delete existing.pushary;
179
+ if (Object.keys(existing).length === 0) delete document[containerKey];
180
+ }
181
+ return JSON.stringify(document) !== before;
182
+ };
183
+ var captureHookTakeover = (agent, document) => {
184
+ const hooks = isRecord(document?.hooks) ? document.hooks : null;
185
+ if (!hooks) return null;
186
+ const captured = {};
187
+ for (const [event, entries] of Object.entries(hooks)) {
188
+ if (!Array.isArray(entries)) continue;
189
+ const appEntries = entries.flatMap((value, index) => isRecord(value) && hasAppCommand(value) ? [{ index, value: structuredClone(value) }] : []);
190
+ if (appEntries.length > 0) captured[event] = appEntries;
191
+ }
192
+ return Object.keys(captured).length > 0 ? { version: 1, agent, hooks: captured } : null;
193
+ };
194
+ var isHookTakeoverReceipt = (value) => {
195
+ if (!isRecord(value) || value.version !== 1 || typeof value.agent !== "string" || !AGENT_CHOICES.includes(value.agent) || !isRecord(value.hooks)) return false;
196
+ const expectsMcp = ["claude_code", "codex", "gemini_cli", "vscode"].includes(value.agent);
197
+ if (value.mcpServer !== void 0 !== expectsMcp) return false;
198
+ if (value.codexTrust !== void 0 !== (value.agent === "codex")) return false;
199
+ if (value.codexTrust !== void 0 && !isRecord(value.codexTrust)) return false;
200
+ if (value.mcpServer !== void 0 && (!isRecord(value.mcpServer) || typeof value.mcpServer.present !== "boolean" || value.mcpServer.present && !Object.hasOwn(value.mcpServer, "value"))) return false;
201
+ return Object.values(value.hooks).every((entries) => Array.isArray(entries) && entries.length > 0 && entries.every((item) => isRecord(item) && Number.isInteger(item.index) && item.index >= 0 && isRecord(item.value)));
202
+ };
203
+ var restoreHookTakeover = (document, receipt) => {
204
+ const before = JSON.stringify(document);
205
+ const appOwnsNow = hookOwnerOf(document) === "app";
206
+ const hooks = isRecord(document.hooks) ? document.hooks : {};
207
+ for (const [event, value] of Object.entries(hooks)) {
208
+ if (!Array.isArray(value)) continue;
209
+ const kept = value.filter((entry) => !hasPaidPusharyCommand(entry));
210
+ if (kept.length === 0) delete hooks[event];
211
+ else hooks[event] = kept;
212
+ }
213
+ if (!appOwnsNow) {
214
+ for (const [event, snapshots] of Object.entries(receipt.hooks)) {
215
+ const entries = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
216
+ for (const snapshot of snapshots) {
217
+ entries.splice(Math.min(snapshot.index, entries.length), 0, structuredClone(snapshot.value));
218
+ }
219
+ hooks[event] = entries;
220
+ }
221
+ }
222
+ document.hooks = hooks;
223
+ return JSON.stringify(document) !== before;
224
+ };
225
+ var hookOwnerOf = (document, appAvailable = true) => {
226
+ const hooks = document && typeof document === "object" ? document.hooks : void 0;
227
+ const executables = commandsIn(hooks).map(pusharyHookExecutable).filter((executable) => executable !== null);
228
+ if (appAvailable && executables.some(isBridgeLocator)) return "app";
229
+ if (executables.some((executable) => !isBridgeLocator(executable))) return "cli";
230
+ return "none";
231
+ };
232
+ var planHooks = (context) => {
233
+ if (context.owner !== "app") return "install";
234
+ if (context.takeOverHooks) return "install";
235
+ return "keep";
236
+ };
237
+ var describeAppOwnership = (agentName) => `The Pushary Mac app owns ${agentName}'s hooks.`;
238
+ var KEEP_HINT = "Kept with the app's account. Pass --take-over-hooks to have the CLI own them instead.";
239
+ var OWNER_LABELS = {
240
+ app: "the Pushary Mac app",
241
+ cli: "the CLI"
242
+ };
243
+ var describeHookOwnership = (entries, names) => {
244
+ const owned = entries.filter((entry) => entry.owner !== "none");
245
+ if (owned.length === 0) return null;
246
+ const parts = owned.map((entry) => `${names[entry.agent]} by ${OWNER_LABELS[entry.owner]}${entry.kept ? " (kept)" : ""}`);
247
+ return `Hooks: ${parts.join(", ")}.`;
248
+ };
249
+
250
+ export {
251
+ AGENT_CHOICES,
252
+ effectiveVerify,
253
+ effectiveControlMode,
254
+ parseSetupOptions,
255
+ mcpBearerKey,
256
+ captureMcpServer,
257
+ restoreMcpServer,
258
+ captureHookTakeover,
259
+ isHookTakeoverReceipt,
260
+ restoreHookTakeover,
261
+ hookOwnerOf,
262
+ planHooks,
263
+ describeAppOwnership,
264
+ KEEP_HINT,
265
+ describeHookOwnership
266
+ };
package/dist/src/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-QKNLW7H6.js";
3
+ } from "../chunk-Y24D5ZWS.js";
4
+ import "../chunk-C2MV4G4A.js";
5
+ import "../chunk-UQSKCQBO.js";
6
+ import "../chunk-YHG74UFF.js";
4
7
  import {
5
8
  fetchAccountPublicKey
6
9
  } from "../chunk-EQRS3SGM.js";
7
- import "../chunk-OYNPLEXP.js";
8
- import "../chunk-2ORRI66J.js";
9
- import "../chunk-YHG74UFF.js";
10
10
  import {
11
11
  fetchModeOverride,
12
12
  fetchModeState,
@@ -15,21 +15,21 @@ import {
15
15
  handlePostToolUse,
16
16
  handleStop,
17
17
  reportEvent
18
- } from "../chunk-5HGLZGI4.js";
18
+ } from "../chunk-J55K6YS4.js";
19
19
  import {
20
20
  askUser,
21
21
  cancelQuestion,
22
22
  waitForAnswer
23
23
  } from "../chunk-RER3FRTD.js";
24
- import "../chunk-4PE2SQVM.js";
25
- import "../chunk-Y3UGCQQC.js";
26
- import "../chunk-NVPYRDCT.js";
27
- import "../chunk-Y6XXFXVB.js";
24
+ import "../chunk-VLKEAYJ4.js";
25
+ import "../chunk-TTO76NZJ.js";
26
+ import "../chunk-SSQ5X6SA.js";
27
+ import "../chunk-3F4VV5TS.js";
28
28
  import "../chunk-EYRGBYZG.js";
29
29
  import "../chunk-SAF6HGAA.js";
30
30
  import "../chunk-7QLSKOSU.js";
31
31
  import "../chunk-KZERVKTD.js";
32
- import "../chunk-ODL52O5V.js";
32
+ import "../chunk-T5B3LRYF.js";
33
33
  import "../chunk-4LL6LG5X.js";
34
34
  import {
35
35
  getApiKey,
@@ -37,7 +37,7 @@ import {
37
37
  } from "../chunk-2UMNXADU.js";
38
38
  import {
39
39
  resolvePolicy
40
- } from "../chunk-Z6CP5ZZD.js";
40
+ } from "../chunk-VOWZ5VOO.js";
41
41
  export {
42
42
  askUser,
43
43
  cancelQuestion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.94.0",
3
+ "version": "0.95.1",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -73,7 +73,8 @@
73
73
  "pushary-bell": "./dist/bin/pushary-bell.js",
74
74
  "pushary-bell-hook": "./dist/bin/pushary-bell-hook.js",
75
75
  "pushary-disconnect": "./dist/bin/pushary-disconnect.js",
76
- "pushary-transcripts": "./dist/bin/pushary-transcripts.js"
76
+ "pushary-transcripts": "./dist/bin/pushary-transcripts.js",
77
+ "pushary-transcript-register": "./dist/bin/pushary-transcript-register.js"
77
78
  },
78
79
  "files": [
79
80
  "dist",