@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,23 +1,32 @@
1
+ import {
2
+ DEFAULT_SESSION,
3
+ cancelQuestion,
4
+ isDefaultSession,
5
+ isPolicyConfig,
6
+ listPendingQuestions,
7
+ removePendingQuestion,
8
+ removePendingSession,
9
+ sendNotification,
10
+ waitForAnswer
11
+ } from "./chunk-J3YDT4HM.js";
1
12
  import {
2
13
  getMachineId
3
14
  } from "./chunk-RN3NOEJF.js";
4
15
  import {
5
- callMcpTool,
6
16
  withRetry
7
17
  } from "./chunk-BSZYIAZL.js";
8
- import {
9
- installProxyDispatcher
10
- } from "./chunk-SAF6HGAA.js";
11
18
  import {
12
19
  reportedInstallSource
13
- } from "./chunk-CWV4CMB6.js";
20
+ } from "./chunk-Y3UTTFTZ.js";
14
21
  import {
15
22
  getApiKey,
16
23
  getBaseUrl
17
24
  } from "./chunk-2UMNXADU.js";
18
25
  import {
19
26
  ACTION_BODY_MAX,
27
+ AGENT_LABELS_BY_TYPE,
20
28
  DECISION_LINE_MAX,
29
+ FILE_TARGET_TOOLS,
21
30
  gateOutputFor,
22
31
  isAutoApprove,
23
32
  localRepoKey,
@@ -27,32 +36,13 @@ import {
27
36
  redactSecretsDeep,
28
37
  resolveAutoResolveOrigin,
29
38
  resolvePolicy
30
- } from "./chunk-K337LIBD.js";
39
+ } from "./chunk-7YJS2VTG.js";
31
40
 
32
41
  // src/policy.ts
33
42
  import { createHash } from "crypto";
34
43
  import { existsSync, readFileSync, writeFileSync } from "fs";
35
44
  import { join } from "path";
36
45
  import { tmpdir } from "os";
37
-
38
- // src/validate.ts
39
- var isPolicyConfig = (data) => {
40
- if (!data || typeof data !== "object") return false;
41
- const d = data;
42
- return Array.isArray(d.policies) && typeof d.defaultTimeoutSeconds === "number" && typeof d.defaultTimeoutAction === "string";
43
- };
44
- var isAskUserResponse = (data) => {
45
- if (!data || typeof data !== "object") return false;
46
- const d = data;
47
- return typeof d.correlationId === "string" && typeof d.status === "string";
48
- };
49
- var isWaitForAnswerResponse = (data) => {
50
- if (!data || typeof data !== "object") return false;
51
- const d = data;
52
- return typeof d.answered === "boolean";
53
- };
54
-
55
- // src/policy.ts
56
46
  var CACHE_TTL_MS = 60 * 1e3;
57
47
  var STALE_CACHE_MAX_MS = 12 * 60 * 60 * 1e3;
58
48
  var policyRequestFor = (agentType) => ({
@@ -188,32 +178,8 @@ var fetchModeState = async (apiKey, sessionId, reuseWithinMs = 0) => {
188
178
  };
189
179
  var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
190
180
 
191
- // src/api.ts
192
- import { randomUUID } from "crypto";
193
- installProxyDispatcher();
194
- var askUser = async (apiKey, params, timeoutMs = 15e3) => {
195
- const identified = params.toolUseId || params.requestId ? params : { ...params, requestId: randomUUID() };
196
- const result = await callMcpTool(apiKey, "ask_user", { ...identified, wait: false }, { maxRetries: 3, timeoutMs });
197
- if (!isAskUserResponse(result)) throw new Error("Invalid ask_user response");
198
- return result;
199
- };
200
- var waitForAnswer = async (apiKey, correlationId, timeoutMs = 3e4) => {
201
- const result = await callMcpTool(apiKey, "wait_for_answer", {
202
- correlationId,
203
- timeoutMs
204
- }, { timeoutMs: timeoutMs + 1e4 });
205
- if (!isWaitForAnswerResponse(result)) throw new Error("Invalid wait_for_answer response");
206
- return result;
207
- };
208
- var cancelQuestion = async (apiKey, correlationId) => {
209
- await callMcpTool(apiKey, "cancel_question", { correlationId });
210
- };
211
- var sendNotification = async (apiKey, params, timeoutMs = 15e3) => {
212
- await callMcpTool(apiKey, "send_notification", { ...params }, { maxRetries: 3, timeoutMs });
213
- };
214
-
215
181
  // ../hook-mapping/src/describe.ts
216
- import { isAbsolute, relative } from "path";
182
+ import { isAbsolute, relative, resolve } from "path";
217
183
  var hookPrefixes = {
218
184
  Bash: (input) => `bash: ${input.command ?? "(no command)"}`,
219
185
  PowerShell: (input) => `powershell: ${input.command ?? "(no command)"}`,
@@ -221,7 +187,11 @@ var hookPrefixes = {
221
187
  Write: (input) => `write file: ${input.file_path ?? "(unknown path)"}`,
222
188
  Edit: (input) => `edit file: ${input.file_path ?? "(unknown path)"}`,
223
189
  MultiEdit: (input) => `edit file: ${input.file_path ?? "(unknown path)"}`,
224
- Read: (input) => `read file: ${input.file_path ?? "(unknown path)"}`
190
+ Read: (input) => `read file: ${input.file_path ?? "(unknown path)"}`,
191
+ // Without an entry here the fallback stringifies the whole tool input, and for
192
+ // ExitPlanMode that input IS the plan: the action line became a truncated dump of
193
+ // the plan markdown. The plan belongs in the body, not the one-line summary.
194
+ ExitPlanMode: () => "finished planning and wants to start coding"
225
195
  };
226
196
  var describeTodoProgress = (todos) => {
227
197
  if (!Array.isArray(todos)) return "updated the plan";
@@ -265,7 +235,7 @@ var deriveToolTarget = (toolName, toolInput) => {
265
235
  if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
266
236
  return deriveCommandHead(toolInput.command);
267
237
  }
268
- if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit") {
238
+ if (FILE_TARGET_TOOLS.has(toolName)) {
269
239
  const filePath = toolInput.file_path;
270
240
  if (typeof filePath !== "string") return void 0;
271
241
  const separator = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
@@ -287,6 +257,21 @@ var deriveReceiptCommandHead = (command) => {
287
257
  return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
288
258
  };
289
259
  var RECEIPT_TARGET_MAX_LENGTH = 256;
260
+ var parseApplyPatchOps = (command) => {
261
+ if (typeof command !== "string") return [];
262
+ const ops = [];
263
+ for (const line of command.split("\n")) {
264
+ const file = line.match(/^\*\*\*\s+(Add|Update|Delete) File:\s+(.+?)\s*$/);
265
+ if (file) {
266
+ ops.push({ op: file[1].toLowerCase(), path: file[2] });
267
+ continue;
268
+ }
269
+ const move = line.match(/^\*\*\*\s+Move to:\s+(.+?)\s*$/);
270
+ if (move) ops.push({ op: "move", path: move[1] });
271
+ }
272
+ return ops;
273
+ };
274
+ var parseApplyPatchFiles = (command) => [...new Set(parseApplyPatchOps(command).map((entry) => entry.path))];
290
275
  var EXIT_CODE_FAILURE = /^Error: Exit code \d+/;
291
276
  var isToolResultError = (toolResult) => {
292
277
  try {
@@ -314,19 +299,34 @@ var relativizeReceiptPath = (filePath, cwd) => {
314
299
  var deriveReceiptMeta = (toolName, toolInput, toolResult, cwd) => {
315
300
  try {
316
301
  const ok = !isToolResultError(toolResult);
317
- if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit") {
318
- const filePath = toolInput.file_path;
302
+ if (toolName === "apply_patch") {
303
+ const files = parseApplyPatchFiles(toolInput.command);
304
+ const target = files.length === 1 ? files[0] : toolInput.command;
305
+ if (typeof target !== "string" || !target) return void 0;
306
+ return { kind: "edit", target: redactSecrets(target).slice(0, RECEIPT_TARGET_MAX_LENGTH), ok };
307
+ }
308
+ const canonicalTool = {
309
+ run_shell_command: "Bash",
310
+ write_file: "Write",
311
+ replace: "Edit"
312
+ }[toolName] ?? toolName;
313
+ const canonicalInput = canonicalTool === toolName ? toolInput : {
314
+ ...toolInput,
315
+ file_path: toolInput.file_path ?? toolInput.absolute_path ?? toolInput.path
316
+ };
317
+ if (FILE_TARGET_TOOLS.has(canonicalTool)) {
318
+ const filePath = canonicalInput.file_path;
319
319
  if (typeof filePath !== "string" || !filePath) return void 0;
320
320
  return {
321
- kind: toolName === "Write" ? "write" : "edit",
321
+ kind: canonicalTool === "Write" ? "write" : "edit",
322
322
  // A path is an unlikely place for a credential but not an impossible one
323
323
  // (a token in a temp directory name), and this field is persisted.
324
324
  target: redactSecrets(relativizeReceiptPath(filePath, cwd)).slice(0, RECEIPT_TARGET_MAX_LENGTH),
325
325
  ok
326
326
  };
327
327
  }
328
- if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
329
- const head = deriveReceiptCommandHead(toolInput.command);
328
+ if (canonicalTool === "Bash" || canonicalTool === "PowerShell" || canonicalTool === "Monitor") {
329
+ const head = deriveReceiptCommandHead(canonicalInput.command);
330
330
  if (!head) return void 0;
331
331
  return {
332
332
  kind: head === "git commit" ? "commit" : "bash",
@@ -356,6 +356,7 @@ var rawActionBody = (toolName, toolInput) => {
356
356
  + ${after ?? ""}`;
357
357
  }
358
358
  if (toolName === "Write" || toolName === "write_file") return firstString(toolInput.content);
359
+ if (toolName === "ExitPlanMode") return firstString(toolInput.plan);
359
360
  if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor" || toolName === "run_shell_command" || toolName === "shell" || toolName === "apply_patch") {
360
361
  return firstString(toolInput.command);
361
362
  }
@@ -383,6 +384,13 @@ var deriveBlocker = (mode, reason) => {
383
384
 
384
385
  // ../hook-mapping/src/map.ts
385
386
  import { basename } from "path";
387
+ var AGENT_IDENTITIES = {
388
+ claude: { type: "claude_code", label: AGENT_LABELS_BY_TYPE.claude_code },
389
+ codex: { type: "codex", label: AGENT_LABELS_BY_TYPE.codex },
390
+ gemini: { type: "gemini_cli", label: AGENT_LABELS_BY_TYPE.gemini_cli },
391
+ cursor: { type: "cursor", label: AGENT_LABELS_BY_TYPE.cursor },
392
+ vscode: { type: "vscode", label: AGENT_LABELS_BY_TYPE.vscode }
393
+ };
386
394
  var TASK_TITLE_MAX_LENGTH = 120;
387
395
  var STOP_SUMMARY_MAX_LENGTH = 200;
388
396
  var SESSION_END_ACTIONS = {
@@ -411,68 +419,22 @@ var sessionStartAction = (source) => source === "resume" ? "Session resumed" : "
411
419
  var sessionEndAction = (reason) => typeof reason === "string" && SESSION_END_ACTIONS[reason] || "Session ended";
412
420
  var taskTitleFrom = (prompt) => typeof prompt === "string" ? prompt.replace(/\s+/g, " ").trim().slice(0, TASK_TITLE_MAX_LENGTH) || void 0 : void 0;
413
421
 
414
- // src/pending.ts
415
- import { join as join2 } from "path";
416
- import { tmpdir as tmpdir2 } from "os";
417
- import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2, readdirSync, unlinkSync, rmSync, statSync } from "fs";
418
- var PENDING_DIR = join2(tmpdir2(), "pushary-pending");
419
- var DEFAULT_SESSION = "_no_session";
420
- var GRACE_MS = 10 * 60 * 1e3;
421
- var sanitize = (sessionId) => sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || DEFAULT_SESSION;
422
- var dirFor = (sessionId) => join2(PENDING_DIR, sanitize(sessionId));
423
- var isDefaultSession = (sessionId) => sanitize(sessionId) === DEFAULT_SESSION;
424
- var savePendingQuestion = (sessionId, correlationId) => {
425
- const dir = dirFor(sessionId);
426
- if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
427
- writeFileSync2(join2(dir, correlationId), "", "utf-8");
428
- };
429
- var listPendingQuestions = (sessionId) => {
430
- const dir = dirFor(sessionId);
431
- let files;
432
- try {
433
- files = readdirSync(dir);
434
- } catch {
435
- return [];
436
- }
437
- if (!isDefaultSession(sessionId)) return files;
438
- const cutoff = Date.now() - GRACE_MS;
439
- return files.filter((name) => {
440
- try {
441
- return statSync(join2(dir, name)).mtimeMs < cutoff;
442
- } catch {
443
- return false;
444
- }
445
- });
446
- };
447
- var removePendingQuestion = (sessionId, correlationId) => {
448
- try {
449
- unlinkSync(join2(dirFor(sessionId), correlationId));
450
- } catch {
451
- }
452
- };
453
- var removePendingSession = (sessionId) => {
454
- try {
455
- rmSync(dirFor(sessionId), { recursive: true, force: true });
456
- } catch {
457
- }
458
- };
459
-
460
422
  // src/repo.ts
461
- import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
462
- import { basename as basename2, dirname, isAbsolute as isAbsolute2, join as join3, resolve } from "path";
423
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
424
+ import { basename as basename2, dirname, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "path";
463
425
  var MAX_PARENT_WALK = 64;
464
426
  var GIT_FILE_PREFIX = "gitdir:";
465
427
  var findGitDir = (startDir) => {
466
- let current = resolve(startDir);
428
+ let current = resolve2(startDir);
467
429
  for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
468
- const candidate = join3(current, ".git");
430
+ const candidate = join2(current, ".git");
469
431
  try {
470
- if (existsSync3(candidate)) {
471
- if (statSync2(candidate).isDirectory()) return candidate;
432
+ if (existsSync2(candidate)) {
433
+ if (statSync(candidate).isDirectory()) return candidate;
472
434
  const pointer = readFileSync2(candidate, "utf-8").trim();
473
435
  if (pointer.startsWith(GIT_FILE_PREFIX)) {
474
436
  const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
475
- return isAbsolute2(target) ? target : resolve(current, target);
437
+ return isAbsolute2(target) ? target : resolve2(current, target);
476
438
  }
477
439
  }
478
440
  } catch {
@@ -485,7 +447,7 @@ var findGitDir = (startDir) => {
485
447
  };
486
448
  var configPathFor = (gitDir) => {
487
449
  const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
488
- return index === -1 ? join3(gitDir, "config") : join3(gitDir.slice(0, index), "config");
450
+ return index === -1 ? join2(gitDir, "config") : join2(gitDir.slice(0, index), "config");
489
451
  };
490
452
  var ORIGIN_SECTION = /^\[remote "origin"\]/;
491
453
  var SECTION_HEADER = /^\[/;
@@ -493,7 +455,7 @@ var URL_LINE = /^url\s*=\s*(.+)$/;
493
455
  var readOriginRemote = (gitDir) => {
494
456
  try {
495
457
  const configPath = configPathFor(gitDir);
496
- if (!existsSync3(configPath)) return void 0;
458
+ if (!existsSync2(configPath)) return void 0;
497
459
  let inOrigin = false;
498
460
  for (const rawLine of readFileSync2(configPath, "utf-8").split("\n")) {
499
461
  const line = rawLine.trim();
@@ -520,7 +482,7 @@ var deriveRepoKey = (cwd) => {
520
482
  const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
521
483
  return localRepoKey(basename2(root));
522
484
  }
523
- return localRepoKey(basename2(resolve(dir)));
485
+ return localRepoKey(basename2(resolve2(dir)));
524
486
  } catch {
525
487
  return void 0;
526
488
  }
@@ -542,9 +504,9 @@ var repoKeyFor = (cwd) => {
542
504
  };
543
505
 
544
506
  // src/usage.ts
545
- import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, readSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
546
- import { join as join4 } from "path";
547
- import { tmpdir as tmpdir3 } from "os";
507
+ import { closeSync, mkdirSync, openSync, readFileSync as readFileSync3, readSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
508
+ import { join as join3 } from "path";
509
+ import { tmpdir as tmpdir2 } from "os";
548
510
  var DEFAULT_PRICES = [
549
511
  { match: "opus-4-1", in: 15, out: 75 },
550
512
  { match: "opus-4-0", in: 15, out: 75 },
@@ -579,8 +541,8 @@ var estimateCostUsd = (usage, model) => {
579
541
  const perTokenOut = price.out / 1e6;
580
542
  return usage.inputTokens * perTokenIn + usage.outputTokens * perTokenOut + usage.cacheCreationTokens * perTokenIn * CACHE_WRITE_MULTIPLIER + usage.cacheReadTokens * perTokenIn * CACHE_READ_MULTIPLIER;
581
543
  };
582
- var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join4(tmpdir3(), "pushary-usage");
583
- var stateFile = (sessionId) => join4(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
544
+ var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join3(tmpdir2(), "pushary-usage");
545
+ var stateFile = (sessionId) => join3(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
584
546
  var emptyState = () => ({ offset: 0, tokensIn: 0, tokensOut: 0, costUsd: 0, recentIds: [] });
585
547
  var readState = (path) => {
586
548
  try {
@@ -659,7 +621,7 @@ var extractUserText = (content) => {
659
621
  };
660
622
  var readLastUserPrompt = (transcriptPath) => {
661
623
  try {
662
- const size = statSync3(transcriptPath).size;
624
+ const size = statSync2(transcriptPath).size;
663
625
  const start = Math.max(0, size - USER_PROMPT_TAIL_BYTES);
664
626
  const lines = readRange(transcriptPath, start, size).toString("utf-8").split("\n");
665
627
  for (let i = lines.length - 1; i >= 0; i -= 1) {
@@ -682,7 +644,7 @@ var readLastUserPrompt = (transcriptPath) => {
682
644
  };
683
645
  var readNewUsage = (transcriptPath, sessionId) => {
684
646
  try {
685
- const size = statSync3(transcriptPath).size;
647
+ const size = statSync2(transcriptPath).size;
686
648
  const path = stateFile(sessionId);
687
649
  let state = readState(path);
688
650
  if (size < state.offset) state = { ...emptyState(), recentIds: state.recentIds };
@@ -698,8 +660,8 @@ var readNewUsage = (transcriptPath, sessionId) => {
698
660
  state.offset += lastNewline + 1;
699
661
  }
700
662
  }
701
- mkdirSync2(stateDir(), { recursive: true });
702
- writeFileSync3(path, JSON.stringify(state), "utf-8");
663
+ mkdirSync(stateDir(), { recursive: true });
664
+ writeFileSync2(path, JSON.stringify(state), "utf-8");
703
665
  if (state.tokensIn === 0 && state.tokensOut === 0) return null;
704
666
  return {
705
667
  tokensIn: state.tokensIn,
@@ -713,7 +675,7 @@ var readNewUsage = (transcriptPath, sessionId) => {
713
675
  };
714
676
 
715
677
  // src/codex-adapter.ts
716
- import { resolve as resolve2 } from "path";
678
+ import { resolve as resolve3 } from "path";
717
679
  var CODEX_AGENT = { type: "codex", label: "Codex" };
718
680
  var codexAllow = () => ({ kind: "allow" });
719
681
  var codexDeny = (reason) => ({ kind: "deny", reason });
@@ -727,21 +689,6 @@ var toCodexWire = (event, decision) => {
727
689
  decision.kind === "deny" ? decision.reason : void 0
728
690
  ) ?? null;
729
691
  };
730
- var parseApplyPatchOps = (command) => {
731
- if (typeof command !== "string") return [];
732
- const ops = [];
733
- for (const line of command.split("\n")) {
734
- const file = line.match(/^\*\*\*\s+(Add|Update|Delete) File:\s+(.+?)\s*$/);
735
- if (file) {
736
- ops.push({ op: file[1].toLowerCase(), path: file[2] });
737
- continue;
738
- }
739
- const move = line.match(/^\*\*\*\s+Move to:\s+(.+?)\s*$/);
740
- if (move) ops.push({ op: "move", path: move[1] });
741
- }
742
- return ops;
743
- };
744
- var parseApplyPatchFiles = (command) => [...new Set(parseApplyPatchOps(command).map((entry) => entry.path))];
745
692
  var describeApplyPatch = (command) => {
746
693
  const ops = parseApplyPatchOps(command);
747
694
  if (ops.length === 0) return null;
@@ -757,7 +704,7 @@ var toPolicyLookup = (toolName, toolInput, cwd) => {
757
704
  const command = toolInput.command;
758
705
  const files = parseApplyPatchFiles(command);
759
706
  if (files.length === 1) {
760
- return { tool: "Edit", input: { file_path: cwd ? resolve2(cwd, files[0]) : files[0] } };
707
+ return { tool: "Edit", input: { file_path: cwd ? resolve3(cwd, files[0]) : files[0] } };
761
708
  }
762
709
  return { tool: "Edit", input: typeof command === "string" ? { file_path: command } : {} };
763
710
  };
@@ -773,7 +720,7 @@ var toPolicyLookups = (toolName, toolInput, cwd) => {
773
720
  }
774
721
  return {
775
722
  tool: "Edit",
776
- inputs: files.map((file) => ({ file_path: cwd ? resolve2(cwd, file) : file }))
723
+ inputs: files.map((file) => ({ file_path: cwd ? resolve3(cwd, file) : file }))
777
724
  };
778
725
  };
779
726
  var permissionTimeoutDecision = (timeoutAction) => {
@@ -784,18 +731,18 @@ var permissionTimeoutDecision = (timeoutAction) => {
784
731
  var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
785
732
 
786
733
  // src/throttle.ts
787
- import { join as join5 } from "path";
788
- import { tmpdir as tmpdir4 } from "os";
734
+ import { join as join4 } from "path";
735
+ import { tmpdir as tmpdir3 } from "os";
789
736
  import { createHash as createHash2 } from "crypto";
790
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
791
- var THROTTLE_DIR = join5(tmpdir4(), "pushary-throttle");
792
- var markerPath = (key) => join5(THROTTLE_DIR, createHash2("sha256").update(key).digest("hex").slice(0, 16));
737
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
738
+ var THROTTLE_DIR = join4(tmpdir3(), "pushary-throttle");
739
+ var markerPath = (key) => join4(THROTTLE_DIR, createHash2("sha256").update(key).digest("hex").slice(0, 16));
793
740
  var throttlePass = (key, windowMs) => {
794
741
  try {
795
742
  const path = markerPath(key);
796
- if (existsSync4(path) && Date.now() - statSync4(path).mtimeMs < windowMs) return false;
797
- mkdirSync3(THROTTLE_DIR, { recursive: true });
798
- writeFileSync4(path, "", "utf-8");
743
+ if (existsSync3(path) && Date.now() - statSync3(path).mtimeMs < windowMs) return false;
744
+ mkdirSync2(THROTTLE_DIR, { recursive: true });
745
+ writeFileSync3(path, "", "utf-8");
799
746
  return true;
800
747
  } catch {
801
748
  return false;
@@ -803,27 +750,27 @@ var throttlePass = (key, windowMs) => {
803
750
  };
804
751
 
805
752
  // src/events.ts
806
- import { basename as basename3, join as join6 } from "path";
807
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync5 } from "fs";
808
- import { tmpdir as tmpdir5 } from "os";
809
- var INTENT_DIR = join6(tmpdir5(), "pushary-intent");
753
+ import { basename as basename3, join as join5 } from "path";
754
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
755
+ import { tmpdir as tmpdir4 } from "os";
756
+ var INTENT_DIR = join5(tmpdir4(), "pushary-intent");
810
757
  var INTENT_MAX_BYTES = 2048;
811
758
  var INTENT_GRACE_MS = 10 * 60 * 1e3;
812
759
  var sanitizeSession = (sessionId) => sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || DEFAULT_SESSION;
813
- var intentFile = (sessionId) => join6(INTENT_DIR, sanitizeSession(sessionId));
760
+ var intentFile = (sessionId) => join5(INTENT_DIR, sanitizeSession(sessionId));
814
761
  var saveLastPrompt = (sessionId, prompt) => {
815
762
  try {
816
763
  const trimmed = prompt.replace(/\s+/g, " ").trim().slice(0, INTENT_MAX_BYTES);
817
764
  if (!trimmed) return;
818
- mkdirSync4(INTENT_DIR, { recursive: true });
819
- writeFileSync5(intentFile(sessionId), trimmed, "utf-8");
765
+ mkdirSync3(INTENT_DIR, { recursive: true });
766
+ writeFileSync4(intentFile(sessionId), trimmed, "utf-8");
820
767
  } catch {
821
768
  }
822
769
  };
823
770
  var readLastPrompt = (sessionId) => {
824
771
  try {
825
772
  const path = intentFile(sessionId);
826
- if (isDefaultSession(sessionId) && statSync5(path).mtimeMs < Date.now() - INTENT_GRACE_MS) return void 0;
773
+ if (isDefaultSession(sessionId) && statSync4(path).mtimeMs < Date.now() - INTENT_GRACE_MS) return void 0;
827
774
  const value = readFileSync4(path, "utf-8").trim();
828
775
  return value.length > 0 ? value : void 0;
829
776
  } catch {
@@ -882,7 +829,7 @@ var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
882
829
  var readFreshCachedPolicy = (apiKey, agentType, sessionId) => {
883
830
  const request = policyRequestFor(agentType);
884
831
  const path = policyCacheFile(apiKey, request.agent, request.repoAware);
885
- if (!existsSync5(path)) return null;
832
+ if (!existsSync4(path)) return null;
886
833
  const cached = JSON.parse(readFileSync4(path, "utf-8"));
887
834
  if (!isPolicyConfig(cached)) return null;
888
835
  if ((cached._sessionId ?? null) !== (sessionId ?? null)) return null;
@@ -1244,18 +1191,12 @@ export {
1244
1191
  getPolicy,
1245
1192
  fetchModeState,
1246
1193
  fetchModeOverride,
1247
- askUser,
1248
- waitForAnswer,
1249
- cancelQuestion,
1250
- sendNotification,
1251
1194
  describeToolCall,
1252
1195
  deriveToolTarget,
1253
1196
  scopePathFor,
1254
1197
  deriveActionBody,
1255
1198
  deriveAction,
1256
1199
  deriveBlocker,
1257
- DEFAULT_SESSION,
1258
- savePendingQuestion,
1259
1200
  repoKeyFor,
1260
1201
  throttlePass,
1261
1202
  readLastUserPrompt,
@@ -5,7 +5,7 @@ import {
5
5
  generateSessionKey,
6
6
  uploadTranscriptRecords,
7
7
  wrapSessionKey
8
- } from "./chunk-T54OT7IY.js";
8
+ } from "./chunk-ECEUMWBH.js";
9
9
 
10
10
  // src/live-session.ts
11
11
  var MAX_TRANSCRIPT_QUEUE = 100;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  hasGeminiHooks
3
- } from "./chunk-GY3I353G.js";
3
+ } from "./chunk-ZZE6FN7T.js";
4
4
  import {
5
5
  hasCodexHooks
6
- } from "./chunk-KJW6NKF7.js";
6
+ } from "./chunk-B3V2H6SA.js";
7
7
 
8
8
  // src/diagnostics/wiring.ts
9
9
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2UMNXADU.js";
4
4
  import {
5
5
  normalizeInstallSource
6
- } from "./chunk-K337LIBD.js";
6
+ } from "./chunk-7YJS2VTG.js";
7
7
 
8
8
  // src/install-source.ts
9
9
  import { readFileSync } from "fs";
@@ -1,6 +1,7 @@
1
1
  import {
2
- HOOK_BUDGETS
3
- } from "./chunk-K337LIBD.js";
2
+ HOOK_BUDGETS,
3
+ HOOK_LOCATOR_BINARY
4
+ } from "./chunk-7YJS2VTG.js";
4
5
 
5
6
  // src/gemini-config.ts
6
7
  var GEMINI_HOOK_BINARY = "pushary-gemini-hook";
@@ -23,7 +24,10 @@ var ensureRecord = (target, key) => {
23
24
  var isPusharyGeminiHook = (entry) => {
24
25
  const hooks = asRecord(entry)?.hooks;
25
26
  if (!Array.isArray(hooks)) return false;
26
- return hooks.some((hook) => String(asRecord(hook)?.command ?? "").includes(GEMINI_HOOK_BINARY));
27
+ return hooks.some((hook) => {
28
+ const command = String(asRecord(hook)?.command ?? "");
29
+ return command.includes(GEMINI_HOOK_BINARY) || command.includes(HOOK_LOCATOR_BINARY);
30
+ });
27
31
  };
28
32
  var addGeminiMcpServer = (settings, apiKey) => {
29
33
  const mcpServers = ensureRecord(settings, "mcpServers");
@@ -132,13 +132,13 @@ interface AskUserResponse {
132
132
  noDevices?: boolean;
133
133
  answerUrl?: string;
134
134
  }
135
- interface WaitForAnswerResponse {
135
+ interface AnswerResult {
136
136
  answered: boolean;
137
137
  value?: string;
138
138
  note?: string;
139
139
  }
140
140
  declare const askUser: (apiKey: string, params: AskUserParams, timeoutMs?: number) => Promise<AskUserResponse>;
141
- declare const waitForAnswer: (apiKey: string, correlationId: string, timeoutMs?: number) => Promise<WaitForAnswerResponse>;
141
+ declare const waitForAnswer: (apiKey: string, correlationId: string, timeoutMs?: number) => Promise<AnswerResult>;
142
142
  declare const cancelQuestion: (apiKey: string, correlationId: string) => Promise<void>;
143
143
 
144
144
  declare const getApiKey: () => string;
package/dist/src/index.js CHANGED
@@ -1,27 +1,27 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-Y7E2MC3G.js";
4
- import "../chunk-XJDL3MUE.js";
5
- import "../chunk-HSGEEBBL.js";
6
- import "../chunk-VT4IVERX.js";
7
- import "../chunk-PKUNB3SN.js";
3
+ } from "../chunk-HJ44HIUW.js";
4
+ import "../chunk-ATBKJNWS.js";
5
+ import "../chunk-7MBDFNUC.js";
8
6
  import "../chunk-YHG74UFF.js";
9
7
  import {
10
- askUser,
11
- cancelQuestion,
12
8
  fetchModeOverride,
13
9
  fetchModeState,
14
10
  getPolicy,
15
11
  handleNotification,
16
12
  handlePostToolUse,
17
13
  handleStop,
18
- reportEvent,
14
+ reportEvent
15
+ } from "../chunk-V2XPF77V.js";
16
+ import {
17
+ askUser,
18
+ cancelQuestion,
19
19
  waitForAnswer
20
- } from "../chunk-DPDUB4XF.js";
20
+ } from "../chunk-J3YDT4HM.js";
21
21
  import "../chunk-RN3NOEJF.js";
22
22
  import "../chunk-BSZYIAZL.js";
23
23
  import "../chunk-SAF6HGAA.js";
24
- import "../chunk-CWV4CMB6.js";
24
+ import "../chunk-Y3UTTFTZ.js";
25
25
  import "../chunk-7QLSKOSU.js";
26
26
  import "../chunk-KZERVKTD.js";
27
27
  import {
@@ -30,7 +30,7 @@ import {
30
30
  } from "../chunk-2UMNXADU.js";
31
31
  import {
32
32
  resolvePolicy
33
- } from "../chunk-K337LIBD.js";
33
+ } from "../chunk-7YJS2VTG.js";
34
34
  export {
35
35
  askUser,
36
36
  cancelQuestion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.85.2",
3
+ "version": "0.87.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -48,6 +48,7 @@
48
48
  "pushary-stopfailure-hook": "./dist/bin/pushary-stopfailure-hook.js",
49
49
  "pushary-permission-hook": "./dist/bin/pushary-permission-hook.js",
50
50
  "pushary-permission-denied-hook": "./dist/bin/pushary-permission-denied-hook.js",
51
+ "pushary-elicitation-hook": "./dist/bin/pushary-elicitation-hook.js",
51
52
  "pushary-claude": "./dist/bin/pushary-claude.js",
52
53
  "pushary-daemon": "./dist/bin/pushary-daemon.js",
53
54
  "pushary-codex": "./dist/bin/pushary-codex.js",
@@ -81,6 +82,7 @@
81
82
  "dev": "tsup --watch",
82
83
  "test": "node ../../scripts/ci/run-tests.mjs src bin",
83
84
  "typecheck": "tsc --noEmit",
85
+ "conformance": "bun scripts/hook-conformance.ts",
84
86
  "gen:hooks-spec": "bun scripts/generate-hooks-spec.ts"
85
87
  },
86
88
  "dependencies": {