@pushary/agent-hooks 0.84.1 → 0.85.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 (71) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +24 -0
  3. package/data/cursor-plugin/CHANGELOG.md +21 -0
  4. package/data/cursor-plugin/scripts/pushary-gate.mjs +54 -222
  5. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +11 -106
  6. package/data/vscode-plugin/CHANGELOG.md +26 -0
  7. package/data/vscode-plugin/scripts/pushary-gate.mjs +66 -74
  8. package/dist/bin/pushary-bell-hook.js +9 -7
  9. package/dist/bin/pushary-bell.js +12 -11
  10. package/dist/bin/pushary-claude.js +12 -11
  11. package/dist/bin/pushary-clean.js +8 -7
  12. package/dist/bin/pushary-codex-bridge.js +5 -5
  13. package/dist/bin/pushary-codex-hook.js +14 -20
  14. package/dist/bin/pushary-codex.js +3 -3
  15. package/dist/bin/pushary-connect.js +10 -10
  16. package/dist/bin/pushary-daemon.js +6 -6
  17. package/dist/bin/pushary-disconnect.d.ts +1 -0
  18. package/dist/bin/pushary-disconnect.js +125 -0
  19. package/dist/bin/pushary-doctor.js +16 -16
  20. package/dist/bin/pushary-gemini-bridge.js +5 -5
  21. package/dist/bin/pushary-gemini-hook.js +14 -20
  22. package/dist/bin/pushary-hook.js +16 -23
  23. package/dist/bin/pushary-login.js +6 -6
  24. package/dist/bin/pushary-logout.js +6 -6
  25. package/dist/bin/pushary-mode.js +5 -5
  26. package/dist/bin/pushary-notification-hook.js +8 -11
  27. package/dist/bin/pushary-permission-denied-hook.js +14 -12
  28. package/dist/bin/pushary-permission-hook.js +14 -12
  29. package/dist/bin/pushary-post-hook.js +8 -11
  30. package/dist/bin/pushary-prompt-hook.js +8 -11
  31. package/dist/bin/pushary-session-end-hook.js +8 -8
  32. package/dist/bin/pushary-session-start-hook.js +8 -8
  33. package/dist/bin/pushary-setup.js +29 -30
  34. package/dist/bin/pushary-stats.js +4 -4
  35. package/dist/bin/pushary-status.js +7 -7
  36. package/dist/bin/pushary-stop-hook.js +8 -8
  37. package/dist/bin/pushary-stopfailure-hook.js +8 -8
  38. package/dist/bin/pushary-suggestions.js +2 -2
  39. package/dist/bin/pushary-upgrade.js +15 -10
  40. package/dist/bin/pushary-wait.js +5 -5
  41. package/dist/bin/pushary.js +2 -1
  42. package/dist/{chunk-ILB6UOAF.js → chunk-55656H4Q.js} +1 -1
  43. package/dist/chunk-6CUUA7HO.js +59 -0
  44. package/dist/{chunk-6LC4WSCL.js → chunk-BZKTCOZQ.js} +1 -1
  45. package/dist/{chunk-K7CSRGKZ.js → chunk-CIVG746V.js} +1 -1
  46. package/dist/{chunk-FBGBJXVC.js → chunk-DNSIVHIA.js} +1 -1
  47. package/dist/{chunk-437T77TY.js → chunk-ES3FAX5X.js} +1 -1
  48. package/dist/chunk-EZPRY2ZY.js +70 -0
  49. package/dist/{chunk-MQMPG5X4.js → chunk-G3LXA2ZP.js} +207 -10
  50. package/dist/chunk-GM54KC62.js +187 -0
  51. package/dist/{chunk-MGJBC6DD.js → chunk-GOEXZ5QJ.js} +1 -0
  52. package/dist/chunk-HSGEEBBL.js +37 -0
  53. package/dist/{chunk-UXWEE3QI.js → chunk-IGV4KL7B.js} +1 -1
  54. package/dist/{chunk-GCKAWHFP.js → chunk-JDQY47OW.js} +1 -1
  55. package/dist/{chunk-7PZPJSWI.js → chunk-KTKIZEFT.js} +1 -1
  56. package/dist/{chunk-YR5TSPXB.js → chunk-LB6RSCLL.js} +12 -10
  57. package/dist/{chunk-DOXSWT4Q.js → chunk-OZKRF7MS.js} +6 -6
  58. package/dist/{chunk-SEAHGHCE.js → chunk-PHXEADSL.js} +40 -7
  59. package/dist/{chunk-F52SIIDG.js → chunk-QDKJTPAT.js} +1 -1
  60. package/dist/{chunk-J4KFVOCQ.js → chunk-SQOFGPNX.js} +1 -1
  61. package/dist/{chunk-NJ7JT26G.js → chunk-VGADRFG6.js} +1 -1
  62. package/dist/{chunk-HZNOAJGB.js → chunk-VKJUVPTJ.js} +2 -2
  63. package/dist/{chunk-ERYKGH4J.js → chunk-XIR6N66V.js} +1 -1
  64. package/dist/{chunk-CCH775Y6.js → chunk-XLM6COJA.js} +1 -1
  65. package/dist/{chunk-CCCJ6BPW.js → chunk-Y7BALRVB.js} +73 -134
  66. package/dist/chunk-YHG74UFF.js +12 -0
  67. package/dist/src/index.d.ts +7 -22
  68. package/dist/src/index.js +11 -8
  69. package/package.json +6 -3
  70. package/dist/chunk-BLDDXHHA.js +0 -66
  71. package/dist/chunk-UKSQWTBH.js +0 -208
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-MQMPG5X4.js";
3
+ } from "./chunk-G3LXA2ZP.js";
4
4
 
5
5
  // src/codex-config.ts
6
6
  import { createHash } from "crypto";
@@ -14,12 +14,12 @@ var codexHooksJson = () => join(codexHome(), "hooks.json");
14
14
  var codexAgentsMd = () => join(codexHome(), "AGENTS.md");
15
15
  var codexSkillDir = () => join(codexHome(), "skills", "pushary");
16
16
  var CODEX_HOOK_EVENTS = [
17
- { event: "PermissionRequest", matcher: "Bash|apply_patch", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Waiting for your phone" },
18
- { event: "PreToolUse", matcher: "Bash|apply_patch", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Checking Pushary policy" },
19
- { event: "PostToolUse", matcher: "Bash|apply_patch", timeout: 10 },
20
- { event: "UserPromptSubmit", timeout: 10 },
21
- { event: "Stop", timeout: 10 },
22
- { event: "SessionStart", matcher: "startup|resume", timeout: 10 }
17
+ { event: "PermissionRequest", matcher: "Bash|apply_patch", delivery: "approval", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Waiting for your phone" },
18
+ { event: "PreToolUse", matcher: "Bash|apply_patch", delivery: "approval", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Checking Pushary policy" },
19
+ { event: "PostToolUse", matcher: "Bash|apply_patch", delivery: "telemetry", timeout: 10 },
20
+ { event: "UserPromptSubmit", delivery: "telemetry", timeout: 10 },
21
+ { event: "Stop", delivery: "telemetry", timeout: 10 },
22
+ { event: "SessionStart", matcher: "startup|resume", delivery: "telemetry", timeout: 10 }
23
23
  ];
24
24
  var CODEX_EVENT_KEY = {
25
25
  PermissionRequest: "permission_request",
@@ -139,6 +139,38 @@ var untrustedCodexHookEvents = (config, hooksJsonPath, command) => {
139
139
  return stored !== codexHookTrustHash(definition, command);
140
140
  }).map((definition) => definition.event);
141
141
  };
142
+ var removeCodexMcpServer = (config) => {
143
+ const servers = asRecord(config.mcp_servers);
144
+ if (!servers?.pushary) return false;
145
+ delete servers.pushary;
146
+ if (Object.keys(servers).length === 0) delete config.mcp_servers;
147
+ return true;
148
+ };
149
+ var removeCodexHookTrust = (config, hooksJsonPath) => {
150
+ const state = asRecord(asRecord(config.hooks)?.state);
151
+ if (!state) return false;
152
+ let changed = false;
153
+ for (const definition of CODEX_HOOK_EVENTS) {
154
+ const key = codexHookStateKey(hooksJsonPath, definition.event);
155
+ if (key in state) {
156
+ delete state[key];
157
+ changed = true;
158
+ }
159
+ }
160
+ if (changed && Object.keys(state).length === 0) {
161
+ const hooks = asRecord(config.hooks);
162
+ if (hooks) {
163
+ delete hooks.state;
164
+ if (Object.keys(hooks).length === 0) delete config.hooks;
165
+ }
166
+ }
167
+ return changed;
168
+ };
169
+ var removeCodexSettings = (config, hooksJsonPath) => {
170
+ const mcp = removeCodexMcpServer(config);
171
+ const trust = removeCodexHookTrust(config, hooksJsonPath);
172
+ return mcp || trust;
173
+ };
142
174
  var addCodexHookTrust = (config, hooksJsonPath, command) => {
143
175
  const hooks = ensureRecord(config, "hooks");
144
176
  const state = ensureRecord(hooks, "state");
@@ -183,6 +215,7 @@ export {
183
215
  hasCodexHooks,
184
216
  missingCodexHookEvents,
185
217
  untrustedCodexHookEvents,
218
+ removeCodexSettings,
186
219
  addCodexHookTrust,
187
220
  claudeSettings,
188
221
  claudeJson,
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-2UMNXADU.js";
13
13
  import {
14
14
  isValidApiKey
15
- } from "./chunk-MQMPG5X4.js";
15
+ } from "./chunk-G3LXA2ZP.js";
16
16
 
17
17
  // src/session.ts
18
18
  var IDENTITY_PATH = "/api/v1/server/identity";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  renderCommandHelp
3
- } from "./chunk-MGJBC6DD.js";
3
+ } from "./chunk-GOEXZ5QJ.js";
4
4
  import {
5
5
  getPackageVersion
6
6
  } from "./chunk-DINDL2CF.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isSafeReadOnlyCommand
3
- } from "./chunk-MQMPG5X4.js";
3
+ } from "./chunk-G3LXA2ZP.js";
4
4
 
5
5
  // src/ledger.ts
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from "fs";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  hasGeminiHooks
3
- } from "./chunk-DOXSWT4Q.js";
3
+ } from "./chunk-OZKRF7MS.js";
4
4
  import {
5
5
  hasCodexHooks
6
- } from "./chunk-SEAHGHCE.js";
6
+ } from "./chunk-PHXEADSL.js";
7
7
 
8
8
  // src/diagnostics/wiring.ts
9
9
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -5,7 +5,7 @@ import {
5
5
  generateSessionKey,
6
6
  uploadTranscriptRecords,
7
7
  wrapSessionKey
8
- } from "./chunk-GCKAWHFP.js";
8
+ } from "./chunk-JDQY47OW.js";
9
9
 
10
10
  // src/live-session.ts
11
11
  var MAX_TRANSCRIPT_QUEUE = 100;
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VT4IVERX.js";
8
8
  import {
9
9
  pusharyDir
10
- } from "./chunk-SEAHGHCE.js";
10
+ } from "./chunk-PHXEADSL.js";
11
11
  import {
12
12
  getMachineId
13
13
  } from "./chunk-RN3NOEJF.js";
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-SAF6HGAA.js";
11
11
  import {
12
12
  reportedInstallSource
13
- } from "./chunk-437T77TY.js";
13
+ } from "./chunk-ES3FAX5X.js";
14
14
  import {
15
15
  getApiKey,
16
16
  getBaseUrl
@@ -18,19 +18,15 @@ import {
18
18
  import {
19
19
  ACTION_BODY_MAX,
20
20
  DECISION_LINE_MAX,
21
- classifyDecisionRisk,
22
- isApprovalMode,
23
21
  isAutoApprove,
24
- isAutoDeny,
25
- isSafeReadOnlyCommand,
26
- isScopeContract,
27
22
  localRepoKey,
23
+ modeStateFromResponse,
28
24
  normalizeRepoRemote,
29
- policyArgForms,
30
25
  redactSecrets,
31
26
  redactSecretsDeep,
32
- selectGoverningRule
33
- } from "./chunk-MQMPG5X4.js";
27
+ resolveAutoResolveOrigin,
28
+ resolvePolicy
29
+ } from "./chunk-G3LXA2ZP.js";
34
30
 
35
31
  // src/policy.ts
36
32
  import { createHash } from "crypto";
@@ -120,66 +116,6 @@ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false, sessio
120
116
  throw new Error("Failed to fetch policy and no cached policy available");
121
117
  }
122
118
  };
123
- var autoApprove = (tool) => ({
124
- tool,
125
- timeoutSeconds: 0,
126
- timeoutAction: "approve",
127
- mode: "terminal_only",
128
- pushFirstSeconds: 0
129
- });
130
- var resolveAutoResolveOrigin = (config, toolName, toolInput, cwd, repoKey, sessionId) => {
131
- const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
132
- const arg = args[0];
133
- const match = selectGoverningRule(config.policies, toolName, args, repoKey, sessionId);
134
- const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
135
- if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
136
- return "safe_readonly";
137
- }
138
- return "policy_timeout";
139
- };
140
- var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey, sessionId) => {
141
- const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
142
- const arg = args[0];
143
- const match = selectGoverningRule(config.policies, toolName, args, repoKey, sessionId);
144
- let base = match?.policy ?? config.policies.find((p) => p.tool === "*") ?? {
145
- tool: toolName,
146
- timeoutSeconds: config.defaultTimeoutSeconds,
147
- timeoutAction: config.defaultTimeoutAction,
148
- mode: config.defaultMode ?? "push_first",
149
- pushFirstSeconds: config.defaultPushFirstSeconds ?? 20
150
- };
151
- const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
152
- if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
153
- base = autoApprove(base.tool);
154
- }
155
- const wouldAutoApprove = isAutoApprove(base);
156
- if (!governedBySpecificRule && wouldAutoApprove && toolName === "Bash" && typeof arg === "string" && classifyDecisionRisk(toolName, arg) === "destructive") {
157
- base = {
158
- tool: base.tool,
159
- timeoutSeconds: config.defaultTimeoutSeconds,
160
- timeoutAction: "wait",
161
- mode: "push_first",
162
- pushFirstSeconds: config.defaultPushFirstSeconds ?? 20
163
- };
164
- }
165
- const effectiveOverride = modeOverride ?? config.modeOverride;
166
- if (effectiveOverride) {
167
- return { ...base, mode: effectiveOverride };
168
- }
169
- return base;
170
- };
171
- var restraintRank = (policy) => {
172
- if (isAutoDeny(policy)) return 4;
173
- if (isAutoApprove(policy)) return 0;
174
- if (policy.mode === "push_only") return 3;
175
- return policy.mode === "terminal_only" || policy.mode === "notify_only" ? 1 : 2;
176
- };
177
- var strictestPolicy = (a, b) => restraintRank(b) > restraintRank(a) ? b : a;
178
- var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey, sessionId) => {
179
- if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey, sessionId);
180
- return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey, sessionId)).reduce(strictestPolicy);
181
- };
182
- var isModeStateDegraded = (state) => state?.degraded === true;
183
119
  var MODE_GRACE_MS = 60 * 1e3;
184
120
  var modeStateCacheFile = (apiKey) => {
185
121
  const hash = createHash("sha256").update(`mode:${apiKey}`).digest("hex").slice(0, 12);
@@ -196,9 +132,10 @@ var readLastGoodMode = (apiKey) => {
196
132
  return null;
197
133
  }
198
134
  };
199
- var writeLastGoodMode = (apiKey, state) => {
135
+ var writeLastGoodMode = (apiKey, state, sessionId) => {
200
136
  try {
201
- writeFileSync(modeStateCacheFile(apiKey), JSON.stringify({ state, at: Date.now() }), { encoding: "utf-8", mode: 384 });
137
+ const cached = { state, at: Date.now(), ...sessionId ? { sessionId } : {} };
138
+ writeFileSync(modeStateCacheFile(apiKey), JSON.stringify(cached), { encoding: "utf-8", mode: 384 });
202
139
  } catch {
203
140
  }
204
141
  };
@@ -219,8 +156,18 @@ var degradedFallback = (apiKey, authoritative = false) => {
219
156
  }
220
157
  return unknownMode();
221
158
  };
222
- var toPolicyVersion = (value) => typeof value === "string" || typeof value === "number" ? String(value) : null;
223
- var fetchModeState = async (apiKey, sessionId) => {
159
+ var MODE_REUSE_MS = CACHE_TTL_MS;
160
+ var readReusableMode = (apiKey, sessionId, maxAgeMs) => {
161
+ const last = readLastGoodMode(apiKey);
162
+ if (!last || (last.sessionId ?? null) !== (sessionId ?? null)) return null;
163
+ if (Date.now() - last.at >= maxAgeMs) return null;
164
+ return { ...last.state, degraded: false };
165
+ };
166
+ var fetchModeState = async (apiKey, sessionId, reuseWithinMs = 0) => {
167
+ if (reuseWithinMs > 0) {
168
+ const reusable = readReusableMode(apiKey, sessionId, reuseWithinMs);
169
+ if (reusable) return reusable;
170
+ }
224
171
  try {
225
172
  const baseUrl = getBaseUrl();
226
173
  const url = sessionId ? `${baseUrl}/api/mcp/mode?session=${encodeURIComponent(sessionId)}` : `${baseUrl}/api/mcp/mode`;
@@ -231,20 +178,8 @@ var fetchModeState = async (apiKey, sessionId) => {
231
178
  if (!response.ok) {
232
179
  return degradedFallback(apiKey, response.status === 401 || response.status === 403);
233
180
  }
234
- const data = await response.json();
235
- const mode = data.override?.mode;
236
- const state = {
237
- mode: isApprovalMode(mode) ? mode : null,
238
- kill: data.kill === true,
239
- policyVersion: toPolicyVersion(data.policyVersion),
240
- relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null,
241
- trainingConsent: data.trainingConsent === true,
242
- // Validated rather than cast: a malformed contract must read as no contract,
243
- // never as one that silently matches nothing.
244
- scope: isScopeContract(data.scope) ? data.scope : null,
245
- degraded: false
246
- };
247
- writeLastGoodMode(apiKey, state);
181
+ const state = modeStateFromResponse(await response.json());
182
+ writeLastGoodMode(apiKey, state, sessionId);
248
183
  return state;
249
184
  } catch {
250
185
  return degradedFallback(apiKey);
@@ -253,9 +188,11 @@ var fetchModeState = async (apiKey, sessionId) => {
253
188
  var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
254
189
 
255
190
  // src/api.ts
191
+ import { randomUUID } from "crypto";
256
192
  installProxyDispatcher();
257
193
  var askUser = async (apiKey, params, timeoutMs = 15e3) => {
258
- const result = await callMcpTool(apiKey, "ask_user", { ...params, wait: false }, { maxRetries: 3, timeoutMs });
194
+ const identified = params.toolUseId || params.requestId ? params : { ...params, requestId: randomUUID() };
195
+ const result = await callMcpTool(apiKey, "ask_user", { ...identified, wait: false }, { maxRetries: 3, timeoutMs });
259
196
  if (!isAskUserResponse(result)) throw new Error("Invalid ask_user response");
260
197
  return result;
261
198
  };
@@ -274,7 +211,7 @@ var sendNotification = async (apiKey, params, timeoutMs = 15e3) => {
274
211
  await callMcpTool(apiKey, "send_notification", { ...params }, { maxRetries: 3, timeoutMs });
275
212
  };
276
213
 
277
- // src/describe.ts
214
+ // ../hook-mapping/src/describe.ts
278
215
  import { isAbsolute, relative } from "path";
279
216
  var hookPrefixes = {
280
217
  Bash: (input) => `bash: ${input.command ?? "(no command)"}`,
@@ -443,6 +380,36 @@ var deriveBlocker = (mode, reason) => {
443
380
  return text.slice(0, DECISION_LINE_MAX);
444
381
  };
445
382
 
383
+ // ../hook-mapping/src/map.ts
384
+ import { basename } from "path";
385
+ var TASK_TITLE_MAX_LENGTH = 120;
386
+ var STOP_SUMMARY_MAX_LENGTH = 200;
387
+ var SESSION_END_ACTIONS = {
388
+ clear: "Session cleared",
389
+ logout: "Logged out",
390
+ resume: "Session suspended",
391
+ prompt_input_exit: "Session ended",
392
+ bypass_permissions_disabled: "Session ended",
393
+ other: "Session ended"
394
+ };
395
+ var countArray = (value) => Array.isArray(value) ? value.length : 0;
396
+ var summarizeFinalMessage = (message) => {
397
+ if (typeof message !== "string") return "Session ended";
398
+ const collapsed = message.replace(/\s+/g, " ").trim();
399
+ if (!collapsed) return "Session ended";
400
+ return collapsed.slice(0, STOP_SUMMARY_MAX_LENGTH);
401
+ };
402
+ var summarizeStop = (input) => {
403
+ const running = countArray(input.background_tasks);
404
+ if (running > 0) return `Paused with ${running} background task${running === 1 ? "" : "s"} still running`;
405
+ const crons = countArray(input.session_crons);
406
+ if (crons > 0) return `Idle until a scheduled task wakes it (${crons} pending)`;
407
+ return summarizeFinalMessage(input.last_assistant_message);
408
+ };
409
+ var sessionStartAction = (source) => source === "resume" ? "Session resumed" : "Session started";
410
+ var sessionEndAction = (reason) => typeof reason === "string" && SESSION_END_ACTIONS[reason] || "Session ended";
411
+ var taskTitleFrom = (prompt) => typeof prompt === "string" ? prompt.replace(/\s+/g, " ").trim().slice(0, TASK_TITLE_MAX_LENGTH) || void 0 : void 0;
412
+
446
413
  // src/pending.ts
447
414
  import { join as join2 } from "path";
448
415
  import { tmpdir as tmpdir2 } from "os";
@@ -491,7 +458,7 @@ var removePendingSession = (sessionId) => {
491
458
 
492
459
  // src/repo.ts
493
460
  import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
494
- import { basename, dirname, isAbsolute as isAbsolute2, join as join3, resolve } from "path";
461
+ import { basename as basename2, dirname, isAbsolute as isAbsolute2, join as join3, resolve } from "path";
495
462
  var MAX_PARENT_WALK = 64;
496
463
  var GIT_FILE_PREFIX = "gitdir:";
497
464
  var findGitDir = (startDir) => {
@@ -550,9 +517,9 @@ var deriveRepoKey = (cwd) => {
550
517
  const normalized = remote ? normalizeRepoRemote(remote) : void 0;
551
518
  if (normalized) return normalized;
552
519
  const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
553
- return localRepoKey(basename(root));
520
+ return localRepoKey(basename2(root));
554
521
  }
555
- return localRepoKey(basename(resolve(dir)));
522
+ return localRepoKey(basename2(resolve(dir)));
556
523
  } catch {
557
524
  return void 0;
558
525
  }
@@ -857,7 +824,7 @@ var throttlePass = (key, windowMs) => {
857
824
  };
858
825
 
859
826
  // src/events.ts
860
- import { basename as basename2, join as join6 } from "path";
827
+ import { basename as basename3, join as join6 } from "path";
861
828
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync5 } from "fs";
862
829
  import { tmpdir as tmpdir5 } from "os";
863
830
  var INTENT_DIR = join6(tmpdir5(), "pushary-intent");
@@ -1046,14 +1013,14 @@ var reportEvent = async (event, options = {}) => {
1046
1013
  var NON_GATED_MIRROR_TOOLS = /* @__PURE__ */ new Set(["TodoWrite"]);
1047
1014
  var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
1048
1015
  try {
1049
- const projectName = basename2(input.cwd ?? process.cwd());
1016
+ const projectName = basename3(input.cwd ?? process.cwd());
1050
1017
  const action = describeToolCall(input.tool_name, input.tool_input, "event");
1051
1018
  const lookup = toPolicyLookup(input.tool_name, input.tool_input);
1052
1019
  const toolResult = input.tool_response ?? input.tool_result;
1053
1020
  const isError = isToolResultError(toolResult);
1054
1021
  const receiptsEnabled = process.env.PUSHARY_RECEIPTS !== "off";
1055
1022
  const sessionKey = input.session_id || DEFAULT_SESSION;
1056
- const liveMode = await fetchModeState(getApiKey(), input.session_id);
1023
+ const liveMode = await fetchModeState(getApiKey(), input.session_id, MODE_REUSE_MS);
1057
1024
  const late = await reconcilePendingQuestions(sessionKey);
1058
1025
  let additionalContext;
1059
1026
  if (late.length > 0 && agent.type === CLAUDE_CODE_AGENT.type) {
@@ -1098,13 +1065,12 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
1098
1065
  return void 0;
1099
1066
  }
1100
1067
  };
1101
- var TASK_TITLE_MAX_LENGTH = 120;
1102
1068
  var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
1103
1069
  try {
1104
- const projectName = basename2(input.cwd ?? process.cwd());
1070
+ const projectName = basename3(input.cwd ?? process.cwd());
1105
1071
  const sessionKey = input.session_id || DEFAULT_SESSION;
1106
1072
  const titlesEnabled = process.env.PUSHARY_TASK_TITLES !== "off";
1107
- const taskTitle = titlesEnabled ? input.prompt?.replace(/\s+/g, " ").trim().slice(0, TASK_TITLE_MAX_LENGTH) || void 0 : void 0;
1073
+ const taskTitle = titlesEnabled ? taskTitleFrom(input.prompt) : void 0;
1108
1074
  if (input.prompt) saveLastPrompt(sessionKey, input.prompt);
1109
1075
  const late = await reconcilePendingQuestions(sessionKey);
1110
1076
  let additionalContext;
@@ -1129,24 +1095,9 @@ var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
1129
1095
  return void 0;
1130
1096
  }
1131
1097
  };
1132
- var STOP_SUMMARY_MAX_LENGTH = 200;
1133
- var summarizeFinalMessage = (message) => {
1134
- if (typeof message !== "string") return "Session ended";
1135
- const collapsed = message.replace(/\s+/g, " ").trim();
1136
- if (!collapsed) return "Session ended";
1137
- return collapsed.slice(0, STOP_SUMMARY_MAX_LENGTH);
1138
- };
1139
- var countArray = (value) => Array.isArray(value) ? value.length : 0;
1140
- var summarizeStop = (input) => {
1141
- const running = countArray(input.background_tasks);
1142
- if (running > 0) return `Paused with ${running} background task${running === 1 ? "" : "s"} still running`;
1143
- const crons = countArray(input.session_crons);
1144
- if (crons > 0) return `Idle until a scheduled task wakes it (${crons} pending)`;
1145
- return summarizeFinalMessage(input.last_assistant_message);
1146
- };
1147
1098
  var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
1148
1099
  try {
1149
- const projectName = basename2(input.cwd ?? process.cwd());
1100
+ const projectName = basename3(input.cwd ?? process.cwd());
1150
1101
  const sessionKey = input.session_id || DEFAULT_SESSION;
1151
1102
  const late = await reconcilePendingQuestions(sessionKey);
1152
1103
  const tasks = [
@@ -1182,8 +1133,8 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
1182
1133
  };
1183
1134
  var handleSessionStart = async (input, agent = CLAUDE_CODE_AGENT) => {
1184
1135
  try {
1185
- const projectName = basename2(input.cwd ?? process.cwd());
1186
- const action = input.source === "resume" ? "Session resumed" : "Session started";
1136
+ const projectName = basename3(input.cwd ?? process.cwd());
1137
+ const action = sessionStartAction(input.source);
1187
1138
  await reportEvent({
1188
1139
  event: "session_start",
1189
1140
  agentType: agent.type,
@@ -1194,18 +1145,10 @@ var handleSessionStart = async (input, agent = CLAUDE_CODE_AGENT) => {
1194
1145
  } catch {
1195
1146
  }
1196
1147
  };
1197
- var SESSION_END_ACTIONS = {
1198
- clear: "Session cleared",
1199
- logout: "Logged out",
1200
- resume: "Session suspended",
1201
- prompt_input_exit: "Session ended",
1202
- bypass_permissions_disabled: "Session ended",
1203
- other: "Session ended"
1204
- };
1205
1148
  var handleSessionEnd = async (input, agent = CLAUDE_CODE_AGENT) => {
1206
1149
  try {
1207
- const projectName = basename2(input.cwd ?? process.cwd());
1208
- const action = typeof input.reason === "string" && SESSION_END_ACTIONS[input.reason] || "Session ended";
1150
+ const projectName = basename3(input.cwd ?? process.cwd());
1151
+ const action = sessionEndAction(input.reason);
1209
1152
  const sessionKey = input.session_id || DEFAULT_SESSION;
1210
1153
  const late = await reconcilePendingQuestions(sessionKey);
1211
1154
  if (late.length > 0) {
@@ -1228,7 +1171,7 @@ var NOTIFICATION_PUSH_TYPES = /* @__PURE__ */ new Set(["idle_prompt", "agent_nee
1228
1171
  var NOTIFICATION_THROTTLE_MS = 60 * 1e3;
1229
1172
  var handleNotification = async (input, agent = CLAUDE_CODE_AGENT) => {
1230
1173
  try {
1231
- const projectName = basename2(input.cwd ?? process.cwd());
1174
+ const projectName = basename3(input.cwd ?? process.cwd());
1232
1175
  const notifType = input.notification_type ?? input.type ?? "";
1233
1176
  const sessionKey = input.session_id || DEFAULT_SESSION;
1234
1177
  if (NOTIFICATION_PUSH_TYPES.has(notifType)) {
@@ -1239,7 +1182,7 @@ var handleNotification = async (input, agent = CLAUDE_CODE_AGENT) => {
1239
1182
  } catch {
1240
1183
  return;
1241
1184
  }
1242
- const mode = await fetchModeState(apiKey, input.session_id).catch(() => null);
1185
+ const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS).catch(() => null);
1243
1186
  if (mode?.kill || mode?.mode === "terminal_only") return;
1244
1187
  await sendNotification(apiKey, {
1245
1188
  title: `${agent.label} is waiting`,
@@ -1301,8 +1244,8 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
1301
1244
  } catch {
1302
1245
  return;
1303
1246
  }
1304
- const projectName = basename2(input.cwd ?? process.cwd());
1305
- const mode = await fetchModeState(apiKey, input.session_id).catch(() => null);
1247
+ const projectName = basename3(input.cwd ?? process.cwd());
1248
+ const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS).catch(() => null);
1306
1249
  if (mode?.kill || mode?.mode === "terminal_only") return;
1307
1250
  const reason = describeStopFailure(input.error);
1308
1251
  await sendNotification(apiKey, {
@@ -1320,10 +1263,6 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
1320
1263
  export {
1321
1264
  policyRequestFor,
1322
1265
  getPolicy,
1323
- resolveAutoResolveOrigin,
1324
- resolvePolicy,
1325
- resolvePolicyAcross,
1326
- isModeStateDegraded,
1327
1266
  fetchModeState,
1328
1267
  fetchModeOverride,
1329
1268
  askUser,
@@ -0,0 +1,12 @@
1
+ // src/answer.ts
2
+ var denyReasonFrom = (value, note) => {
3
+ const trimmed = note?.trim();
4
+ if (trimmed) return `Denied from your phone: ${trimmed}`;
5
+ return value && value !== "no" && value !== "yes" ? `Denied from your phone: ${value}` : "Denied via push notification";
6
+ };
7
+ var isDeferAnswer = (value) => value === "defer";
8
+
9
+ export {
10
+ denyReasonFrom,
11
+ isDeferAnswer
12
+ };
@@ -1,5 +1,6 @@
1
- import { PolicyConfig, ApprovalMode, ToolPolicy, ScopeContract, AgentQuestion } from '@pushary/contracts';
2
- export { ApprovalMode, PolicyConfig, ToolPolicy } from '@pushary/contracts';
1
+ import { ReceiptMeta } from '@pushary/hook-mapping';
2
+ import { PolicyConfig, ApprovalMode, ModeState, AutoResolveOrigin, AgentQuestion } from '@pushary/contracts';
3
+ export { ApprovalMode, ModeState, PolicyConfig, ToolPolicy, resolvePolicy } from '@pushary/contracts';
3
4
 
4
5
  interface ToolInput {
5
6
  tool_name: string;
@@ -20,26 +21,9 @@ interface HookOutput {
20
21
  }
21
22
  declare const handlePreToolUse: (input: ToolInput) => Promise<HookOutput | undefined>;
22
23
 
23
- type ReceiptKind = 'edit' | 'write' | 'bash' | 'commit';
24
- interface ReceiptMeta {
25
- kind: ReceiptKind;
26
- target: string;
27
- ok: boolean;
28
- }
29
-
30
24
  declare const getPolicy: (apiKey: string, expectedVersion?: string | null, agent?: string, repoAware?: boolean, sessionId?: string) => Promise<PolicyConfig>;
31
- type AutoResolveOrigin = 'safe_readonly' | 'policy_timeout';
32
- declare const resolvePolicy: (config: PolicyConfig, toolName: string, modeOverride?: ApprovalMode | null, toolInput?: Record<string, unknown>, cwd?: string, repoKey?: string, sessionId?: string) => ToolPolicy;
33
- interface ModeState {
34
- readonly mode: ApprovalMode | null;
35
- readonly kill: boolean;
36
- readonly policyVersion: string | null;
37
- readonly relayUrl: string | null;
38
- readonly trainingConsent?: boolean;
39
- readonly scope?: ScopeContract | null;
40
- readonly degraded?: boolean;
41
- }
42
- declare const fetchModeState: (apiKey: string, sessionId?: string) => Promise<ModeState>;
25
+
26
+ declare const fetchModeState: (apiKey: string, sessionId?: string, reuseWithinMs?: number) => Promise<ModeState>;
43
27
  declare const fetchModeOverride: (apiKey: string) => Promise<ApprovalMode | null>;
44
28
 
45
29
  interface UsageReport {
@@ -142,6 +126,7 @@ interface AskUserParams {
142
126
  actionBody?: string;
143
127
  scopePath?: string;
144
128
  toolUseId?: string;
129
+ requestId?: string;
145
130
  }
146
131
  interface AskUserResponse {
147
132
  correlationId: string;
@@ -166,4 +151,4 @@ declare const cancelQuestion: (apiKey: string, correlationId: string) => Promise
166
151
  declare const getApiKey: () => string;
167
152
  declare const getBaseUrl: () => string;
168
153
 
169
- export { type ModeState, askUser, cancelQuestion, fetchModeOverride, fetchModeState, getApiKey, getBaseUrl, getPolicy, handleNotification, handlePostToolUse, handlePreToolUse, handleStop, reportEvent, resolvePolicy, waitForAnswer };
154
+ export { askUser, cancelQuestion, fetchModeOverride, fetchModeState, getApiKey, getBaseUrl, getPolicy, handleNotification, handlePostToolUse, handlePreToolUse, handleStop, reportEvent, waitForAnswer };
package/dist/src/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-YR5TSPXB.js";
4
- import "../chunk-UKSQWTBH.js";
5
- import "../chunk-NJ7JT26G.js";
6
- import "../chunk-BLDDXHHA.js";
3
+ } from "../chunk-LB6RSCLL.js";
4
+ import "../chunk-GM54KC62.js";
5
+ import "../chunk-HSGEEBBL.js";
6
+ import "../chunk-VT4IVERX.js";
7
+ import "../chunk-VGADRFG6.js";
8
+ import "../chunk-YHG74UFF.js";
7
9
  import {
8
10
  askUser,
9
11
  cancelQuestion,
@@ -14,20 +16,21 @@ import {
14
16
  handlePostToolUse,
15
17
  handleStop,
16
18
  reportEvent,
17
- resolvePolicy,
18
19
  waitForAnswer
19
- } from "../chunk-CCCJ6BPW.js";
20
+ } from "../chunk-Y7BALRVB.js";
20
21
  import "../chunk-RN3NOEJF.js";
21
22
  import "../chunk-BSZYIAZL.js";
22
23
  import "../chunk-SAF6HGAA.js";
23
- import "../chunk-437T77TY.js";
24
+ import "../chunk-ES3FAX5X.js";
24
25
  import "../chunk-7QLSKOSU.js";
25
26
  import "../chunk-KZERVKTD.js";
26
27
  import {
27
28
  getApiKey,
28
29
  getBaseUrl
29
30
  } from "../chunk-2UMNXADU.js";
30
- import "../chunk-MQMPG5X4.js";
31
+ import {
32
+ resolvePolicy
33
+ } from "../chunk-G3LXA2ZP.js";
31
34
  export {
32
35
  askUser,
33
36
  cancelQuestion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.84.1",
3
+ "version": "0.85.1",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -68,7 +68,8 @@
68
68
  "pushary-suggestions": "./dist/bin/pushary-suggestions.js",
69
69
  "pushary-upgrade": "./dist/bin/pushary-upgrade.js",
70
70
  "pushary-bell": "./dist/bin/pushary-bell.js",
71
- "pushary-bell-hook": "./dist/bin/pushary-bell-hook.js"
71
+ "pushary-bell-hook": "./dist/bin/pushary-bell-hook.js",
72
+ "pushary-disconnect": "./dist/bin/pushary-disconnect.js"
72
73
  },
73
74
  "files": [
74
75
  "dist",
@@ -79,7 +80,8 @@
79
80
  "build": "node scripts/bundle-plugin.mjs && tsup",
80
81
  "dev": "tsup --watch",
81
82
  "test": "node ../../scripts/ci/run-tests.mjs src bin",
82
- "typecheck": "tsc --noEmit"
83
+ "typecheck": "tsc --noEmit",
84
+ "gen:hooks-spec": "bun scripts/generate-hooks-spec.ts"
83
85
  },
84
86
  "dependencies": {
85
87
  "@inquirer/prompts": "^8.4.2",
@@ -90,6 +92,7 @@
90
92
  },
91
93
  "devDependencies": {
92
94
  "@pushary/contracts": "workspace:*",
95
+ "@pushary/hook-mapping": "workspace:*",
93
96
  "tsup": "^8.0.0",
94
97
  "typescript": "^5.0.0"
95
98
  }