@pushary/agent-hooks 0.85.2 → 0.87.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 (60) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/bin/pushary-bell-hook.js +4 -4
  3. package/dist/bin/pushary-bell.js +6 -6
  4. package/dist/bin/pushary-claude.js +12 -10
  5. package/dist/bin/pushary-clean.js +10 -9
  6. package/dist/bin/pushary-codex-bridge.js +5 -5
  7. package/dist/bin/pushary-codex-hook.js +13 -29
  8. package/dist/bin/pushary-codex.js +6 -4
  9. package/dist/bin/pushary-connect.js +6 -6
  10. package/dist/bin/pushary-daemon.js +4 -4
  11. package/dist/bin/pushary-disconnect.js +25 -10
  12. package/dist/bin/pushary-doctor.js +21 -15
  13. package/dist/bin/pushary-elicitation-hook.d.ts +1 -0
  14. package/dist/bin/pushary-elicitation-hook.js +210 -0
  15. package/dist/bin/pushary-gemini-bridge.js +5 -5
  16. package/dist/bin/pushary-gemini-hook.js +10 -8
  17. package/dist/bin/pushary-hook.js +7 -8
  18. package/dist/bin/pushary-login.js +4 -4
  19. package/dist/bin/pushary-logout.js +4 -4
  20. package/dist/bin/pushary-mode.js +3 -3
  21. package/dist/bin/pushary-notification-hook.js +4 -3
  22. package/dist/bin/pushary-permission-denied-hook.js +7 -8
  23. package/dist/bin/pushary-permission-hook.js +7 -8
  24. package/dist/bin/pushary-post-hook.js +4 -3
  25. package/dist/bin/pushary-prompt-hook.js +4 -3
  26. package/dist/bin/pushary-session-end-hook.js +4 -3
  27. package/dist/bin/pushary-session-start-hook.js +4 -3
  28. package/dist/bin/pushary-setup.js +26 -21
  29. package/dist/bin/pushary-stats.js +2 -2
  30. package/dist/bin/pushary-status.js +5 -5
  31. package/dist/bin/pushary-stop-hook.js +4 -3
  32. package/dist/bin/pushary-stopfailure-hook.js +4 -3
  33. package/dist/bin/pushary-upgrade.js +13 -11
  34. package/dist/bin/pushary-wait.js +3 -3
  35. package/dist/{chunk-GM74DUMS.js → chunk-2T3GX7WG.js} +1 -1
  36. package/dist/{chunk-XJDL3MUE.js → chunk-5646VKR4.js} +18 -20
  37. package/dist/{chunk-PKUNB3SN.js → chunk-7MBDFNUC.js} +1 -1
  38. package/dist/{chunk-K337LIBD.js → chunk-7YJS2VTG.js} +36 -4
  39. package/dist/{chunk-LCA2EQTK.js → chunk-A3PFIWWN.js} +1 -1
  40. package/dist/{chunk-LSB6PFLN.js → chunk-A4I5JGCW.js} +1 -1
  41. package/dist/chunk-ATBKJNWS.js +19 -0
  42. package/dist/{chunk-KJW6NKF7.js → chunk-B3V2H6SA.js} +39 -15
  43. package/dist/{chunk-T54OT7IY.js → chunk-ECEUMWBH.js} +1 -1
  44. package/dist/{chunk-Q4URN3E7.js → chunk-HAUTLCMA.js} +1 -1
  45. package/dist/{chunk-Y7E2MC3G.js → chunk-HJ44HIUW.js} +60 -31
  46. package/dist/{chunk-4U2YRHNF.js → chunk-IWL5LWIE.js} +2 -2
  47. package/dist/chunk-J3YDT4HM.js +128 -0
  48. package/dist/{chunk-NTIUQB3E.js → chunk-JAICP7BA.js} +1 -1
  49. package/dist/{chunk-HSGEEBBL.js → chunk-M7RYIZX6.js} +3 -3
  50. package/dist/{chunk-KZIGJLOX.js → chunk-OT4YD54Q.js} +1 -1
  51. package/dist/{chunk-LXWISFF3.js → chunk-QBWYNVKY.js} +1 -1
  52. package/dist/{chunk-4YMWLMAP.js → chunk-SJVLFIYP.js} +1 -1
  53. package/dist/{chunk-DPDUB4XF.js → chunk-V2XPF77V.js} +104 -163
  54. package/dist/{chunk-JBLRZGTT.js → chunk-WLLBFDVY.js} +1 -1
  55. package/dist/{chunk-JT5NQQCE.js → chunk-XW5CHAQX.js} +2 -2
  56. package/dist/{chunk-CWV4CMB6.js → chunk-Y3UTTFTZ.js} +1 -1
  57. package/dist/{chunk-GY3I353G.js → chunk-ZZE6FN7T.js} +7 -3
  58. package/dist/src/index.d.ts +2 -2
  59. package/dist/src/index.js +11 -11
  60. package/package.json +3 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseVersion
3
- } from "./chunk-XJDL3MUE.js";
3
+ } from "./chunk-5646VKR4.js";
4
4
 
5
5
  // src/setup/agent-probe.ts
6
6
  import { execSync } from "child_process";
@@ -1,18 +1,16 @@
1
1
  import {
2
- PRETOOLUSE_HANDLED_TOOLS
3
- } from "./chunk-XJDL3MUE.js";
2
+ PRETOOLUSE_HANDLED_TOOLS,
3
+ PRETOOLUSE_PHONE_ANSWERED_TOOLS
4
+ } from "./chunk-ATBKJNWS.js";
4
5
  import {
5
6
  isGatingMoment,
6
7
  recordKeylessMoment
7
- } from "./chunk-PKUNB3SN.js";
8
+ } from "./chunk-7MBDFNUC.js";
8
9
  import {
9
10
  denyReasonFrom,
10
11
  isDeferAnswer
11
12
  } from "./chunk-YHG74UFF.js";
12
13
  import {
13
- DEFAULT_SESSION,
14
- askUser,
15
- cancelQuestion,
16
14
  deriveAction,
17
15
  deriveActionBody,
18
16
  deriveBlocker,
@@ -23,12 +21,17 @@ import {
23
21
  readLastPrompt,
24
22
  readLastUserPrompt,
25
23
  repoKeyFor,
26
- savePendingQuestion,
27
24
  scopePathFor,
28
- sendNotification,
29
- throttlePass,
30
- waitForAnswer
31
- } from "./chunk-DPDUB4XF.js";
25
+ throttlePass
26
+ } from "./chunk-V2XPF77V.js";
27
+ import {
28
+ DEFAULT_SESSION,
29
+ askUser,
30
+ cancelQuestion,
31
+ pollForAnswer,
32
+ savePendingQuestion,
33
+ sendNotification
34
+ } from "./chunk-J3YDT4HM.js";
32
35
  import {
33
36
  getMachineId
34
37
  } from "./chunk-RN3NOEJF.js";
@@ -45,7 +48,7 @@ import {
45
48
  hookWaitDeadline,
46
49
  parseAgentQuestionAnswers,
47
50
  resolveGate
48
- } from "./chunk-K337LIBD.js";
51
+ } from "./chunk-7YJS2VTG.js";
49
52
 
50
53
  // src/decision-episode.ts
51
54
  var EPISODE_PATH = "/api/agent/decision-episode";
@@ -73,29 +76,11 @@ var reportDecisionEpisode = async (apiKey, input) => {
73
76
  import { basename, join } from "path";
74
77
  import { tmpdir } from "os";
75
78
  import { existsSync, writeFileSync } from "fs";
76
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
77
79
  var START_MS = Date.now();
78
80
  var allow = () => claudePreToolUseOutput("allow");
79
81
  var deny = (reason) => claudePreToolUseOutput("deny", reason);
80
82
  var allowWithInput = (updatedInput) => claudePreToolUseOutput("allow", void 0, updatedInput);
81
83
  var ask = (reason) => claudePreToolUseOutput("ask", reason);
82
- var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
83
- while (Date.now() < deadlineMs) {
84
- const remaining = Math.min(Math.max(deadlineMs - Date.now(), 1e3), 3e4);
85
- let answer;
86
- try {
87
- answer = await waitForAnswer(apiKey, correlationId, remaining);
88
- } catch {
89
- if (Date.now() + pollInterval >= deadlineMs) break;
90
- await sleep(pollInterval);
91
- continue;
92
- }
93
- if (answer.answered) return answer;
94
- if (Date.now() + pollInterval >= deadlineMs) break;
95
- await sleep(pollInterval);
96
- }
97
- return { answered: false };
98
- };
99
84
  var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, timeoutAction, sessionId, machineId, toolName, toolTarget, decision, signals) => {
100
85
  let result;
101
86
  try {
@@ -448,6 +433,47 @@ var handleAskUserQuestion = async (apiKey, input) => {
448
433
  if (!answers) return void 0;
449
434
  return allowWithInput({ ...input.tool_input ?? {}, answers });
450
435
  };
436
+ var PLAN_APPROVE = "Approve, start coding";
437
+ var PLAN_KEEP_PLANNING = "Keep planning";
438
+ var handleExitPlanMode = async (apiKey, input) => {
439
+ const toolInput = input.tool_input ?? {};
440
+ const projectName = basename(input.cwd ?? process.cwd());
441
+ let result;
442
+ try {
443
+ result = await askUser(apiKey, {
444
+ question: `Approve the plan and start coding in ${projectName}?`,
445
+ type: "select",
446
+ options: [PLAN_APPROVE, PLAN_KEEP_PLANNING],
447
+ context: `Your agent finished planning in ${projectName} and is waiting to start.`,
448
+ agentName: `Claude Code - ${projectName}`,
449
+ sessionId: input.session_id,
450
+ machineId: getMachineId(),
451
+ toolName: "ExitPlanMode",
452
+ action: deriveAction("ExitPlanMode", toolInput),
453
+ // The plan markdown itself, through the same redaction and cap as any diff,
454
+ // so approving from a lock screen is a real decision and not a blind yes.
455
+ actionBody: deriveActionBody("ExitPlanMode", toolInput),
456
+ intent: readLastPrompt(input.session_id ?? DEFAULT_SESSION) ?? (input.transcript_path ? readLastUserPrompt(input.transcript_path) : void 0),
457
+ blocker: "Waiting for you to approve the plan"
458
+ });
459
+ } catch {
460
+ return void 0;
461
+ }
462
+ if (result.suppressed || result.noDevices) {
463
+ await cancelQuestion(apiKey, result.correlationId).catch(() => {
464
+ });
465
+ return void 0;
466
+ }
467
+ const deadline = hookWaitDeadline(START_MS, effectiveWaitSeconds("wait", 0, "claude"), "claude", Date.now());
468
+ const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
469
+ const value = answer.answered ? (answer.value ?? "").trim() : "";
470
+ if (!value || isDeferAnswer(answer.value)) {
471
+ savePendingQuestion(input.session_id || DEFAULT_SESSION, result.correlationId);
472
+ return void 0;
473
+ }
474
+ if (value === PLAN_APPROVE) return allow();
475
+ return deny("Plan declined from the phone. Ask what to change before coding.");
476
+ };
451
477
  var handleKeyless = (input) => {
452
478
  try {
453
479
  if (isGatingMoment(input.tool_name, input.tool_input ?? {})) {
@@ -493,6 +519,9 @@ var handlePreToolUse = async (input) => {
493
519
  return handleAskUserQuestion(apiKey, input);
494
520
  }
495
521
  if (verdict.kind === "deny") return deny(verdict.reason);
522
+ if (input.tool_name === "ExitPlanMode") {
523
+ return handleExitPlanMode(apiKey, input);
524
+ }
496
525
  if (shouldDeferToNativeMode(input.permission_mode, input.tool_name)) {
497
526
  return void 0;
498
527
  }
@@ -524,7 +553,7 @@ var toPermissionRequestOutput = (hookOutput) => {
524
553
  };
525
554
  var handlePermissionRequest = async (input) => {
526
555
  if (PRETOOLUSE_HANDLED_TOOLS.has(input.tool_name)) {
527
- const reclaimDeferredExec = input.tool_name !== "AskUserQuestion" && shouldDeferToNativeMode(input.permission_mode, input.tool_name);
556
+ const reclaimDeferredExec = !PRETOOLUSE_PHONE_ANSWERED_TOOLS.includes(input.tool_name) && shouldDeferToNativeMode(input.permission_mode, input.tool_name);
528
557
  if (!reclaimDeferredExec) return void 0;
529
558
  }
530
559
  if (input.tool_name.startsWith("mcp__pushary__")) return void 0;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  codexHomeFrom
3
- } from "./chunk-KJW6NKF7.js";
3
+ } from "./chunk-B3V2H6SA.js";
4
4
 
5
5
  // src/vscode-config.ts
6
6
  import { homedir } from "os";
@@ -253,7 +253,7 @@ You have Pushary MCP tools that reach the user on their phone. Use them proactiv
253
253
 
254
254
  - 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.
255
255
  - 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.
256
- - 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, and after that only work outside it needs approving. 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.
256
+ - 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.
257
257
  - 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.
258
258
  - 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.
259
259
  - Use \`cancel_question\` to retract a pending question once it is no longer needed.
@@ -0,0 +1,128 @@
1
+ import {
2
+ callMcpTool
3
+ } from "./chunk-BSZYIAZL.js";
4
+ import {
5
+ installProxyDispatcher
6
+ } from "./chunk-SAF6HGAA.js";
7
+
8
+ // src/api.ts
9
+ import { randomUUID } from "crypto";
10
+
11
+ // src/validate.ts
12
+ var isPolicyConfig = (data) => {
13
+ if (!data || typeof data !== "object") return false;
14
+ const d = data;
15
+ return Array.isArray(d.policies) && typeof d.defaultTimeoutSeconds === "number" && typeof d.defaultTimeoutAction === "string";
16
+ };
17
+ var isAskUserResponse = (data) => {
18
+ if (!data || typeof data !== "object") return false;
19
+ const d = data;
20
+ return typeof d.correlationId === "string" && typeof d.status === "string";
21
+ };
22
+ var isWaitForAnswerResponse = (data) => {
23
+ if (!data || typeof data !== "object") return false;
24
+ const d = data;
25
+ return typeof d.answered === "boolean";
26
+ };
27
+
28
+ // src/api.ts
29
+ installProxyDispatcher();
30
+ var askUser = async (apiKey, params, timeoutMs = 15e3) => {
31
+ const identified = params.toolUseId || params.requestId ? params : { ...params, requestId: randomUUID() };
32
+ const result = await callMcpTool(apiKey, "ask_user", { ...identified, wait: false }, { maxRetries: 3, timeoutMs });
33
+ if (!isAskUserResponse(result)) throw new Error("Invalid ask_user response");
34
+ return result;
35
+ };
36
+ var waitForAnswer = async (apiKey, correlationId, timeoutMs = 3e4) => {
37
+ const result = await callMcpTool(apiKey, "wait_for_answer", {
38
+ correlationId,
39
+ timeoutMs
40
+ }, { timeoutMs: timeoutMs + 1e4 });
41
+ if (!isWaitForAnswerResponse(result)) throw new Error("Invalid wait_for_answer response");
42
+ return result;
43
+ };
44
+ var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
45
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
46
+ while (Date.now() < deadlineMs) {
47
+ const remaining = Math.min(Math.max(deadlineMs - Date.now(), 1e3), 3e4);
48
+ let answer;
49
+ try {
50
+ answer = await waitForAnswer(apiKey, correlationId, remaining);
51
+ } catch {
52
+ if (Date.now() + pollInterval >= deadlineMs) break;
53
+ await sleep(pollInterval);
54
+ continue;
55
+ }
56
+ if (answer.answered) return answer;
57
+ if (Date.now() + pollInterval >= deadlineMs) break;
58
+ await sleep(pollInterval);
59
+ }
60
+ return { answered: false };
61
+ };
62
+ var cancelQuestion = async (apiKey, correlationId) => {
63
+ await callMcpTool(apiKey, "cancel_question", { correlationId });
64
+ };
65
+ var sendNotification = async (apiKey, params, timeoutMs = 15e3) => {
66
+ await callMcpTool(apiKey, "send_notification", { ...params }, { maxRetries: 3, timeoutMs });
67
+ };
68
+
69
+ // src/pending.ts
70
+ import { join } from "path";
71
+ import { tmpdir } from "os";
72
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, unlinkSync, rmSync, statSync } from "fs";
73
+ var PENDING_DIR = join(tmpdir(), "pushary-pending");
74
+ var DEFAULT_SESSION = "_no_session";
75
+ var GRACE_MS = 10 * 60 * 1e3;
76
+ var sanitize = (sessionId) => sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || DEFAULT_SESSION;
77
+ var dirFor = (sessionId) => join(PENDING_DIR, sanitize(sessionId));
78
+ var isDefaultSession = (sessionId) => sanitize(sessionId) === DEFAULT_SESSION;
79
+ var savePendingQuestion = (sessionId, correlationId) => {
80
+ const dir = dirFor(sessionId);
81
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
82
+ writeFileSync(join(dir, correlationId), "", "utf-8");
83
+ };
84
+ var listPendingQuestions = (sessionId) => {
85
+ const dir = dirFor(sessionId);
86
+ let files;
87
+ try {
88
+ files = readdirSync(dir);
89
+ } catch {
90
+ return [];
91
+ }
92
+ if (!isDefaultSession(sessionId)) return files;
93
+ const cutoff = Date.now() - GRACE_MS;
94
+ return files.filter((name) => {
95
+ try {
96
+ return statSync(join(dir, name)).mtimeMs < cutoff;
97
+ } catch {
98
+ return false;
99
+ }
100
+ });
101
+ };
102
+ var removePendingQuestion = (sessionId, correlationId) => {
103
+ try {
104
+ unlinkSync(join(dirFor(sessionId), correlationId));
105
+ } catch {
106
+ }
107
+ };
108
+ var removePendingSession = (sessionId) => {
109
+ try {
110
+ rmSync(dirFor(sessionId), { recursive: true, force: true });
111
+ } catch {
112
+ }
113
+ };
114
+
115
+ export {
116
+ isPolicyConfig,
117
+ askUser,
118
+ waitForAnswer,
119
+ pollForAnswer,
120
+ cancelQuestion,
121
+ sendNotification,
122
+ DEFAULT_SESSION,
123
+ isDefaultSession,
124
+ savePendingQuestion,
125
+ listPendingQuestions,
126
+ removePendingQuestion,
127
+ removePendingSession
128
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  describeKeyCheck,
3
3
  keyCheckFromResponse
4
- } from "./chunk-LSB6PFLN.js";
4
+ } from "./chunk-A4I5JGCW.js";
5
5
  import {
6
6
  createIo
7
7
  } from "./chunk-5RUIFDTP.js";
@@ -7,9 +7,9 @@ var HOOK_SHELL = "/bin/sh -c";
7
7
  var LOCATE = 'B=$(command -v "$0") || exit 0; [ -f "$B" ] || exit 0; [ -x "$B" ] || exit 0';
8
8
  var NODE_PRESENT = "command -v node >/dev/null 2>&1 || exit 0";
9
9
  var HOOK_GUARD_SCRIPTS = {
10
- native_binary: `${LOCATE}; exec "$B"`,
11
- node_binary: `${LOCATE}; ${NODE_PRESENT}; exec "$B"`,
12
- node_script: `[ -f "$0" ] || exit 0; ${NODE_PRESENT}; exec node "$0"`
10
+ native_binary: `${LOCATE}; exec "$B" "$@"`,
11
+ node_binary: `${LOCATE}; ${NODE_PRESENT}; exec "$B" "$@"`,
12
+ node_script: `[ -f "$0" ] || exit 0; ${NODE_PRESENT}; exec node "$0" "$@"`
13
13
  };
14
14
  var BINARY_GUARDS = {
15
15
  native: "native_binary",
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-6MTNS63X.js";
10
10
  import {
11
11
  resolveInstallSourceFor
12
- } from "./chunk-CWV4CMB6.js";
12
+ } from "./chunk-Y3UTTFTZ.js";
13
13
  import {
14
14
  configFilePath
15
15
  } from "./chunk-2UMNXADU.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pusharyDir
3
- } from "./chunk-KJW6NKF7.js";
3
+ } from "./chunk-B3V2H6SA.js";
4
4
  import {
5
5
  readJsonSafe
6
6
  } from "./chunk-6MTNS63X.js";
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VT4IVERX.js";
8
8
  import {
9
9
  pusharyDir
10
- } from "./chunk-KJW6NKF7.js";
10
+ } from "./chunk-B3V2H6SA.js";
11
11
  import {
12
12
  getMachineId
13
13
  } from "./chunk-RN3NOEJF.js";