@pushary/agent-hooks 0.43.0 → 0.44.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.
@@ -10,9 +10,10 @@ import {
10
10
  fetchModeState,
11
11
  getMachineId,
12
12
  getPolicy,
13
+ reportEvent,
13
14
  resolvePolicy,
14
15
  waitForAnswer
15
- } from "../chunk-ETDXSKR5.js";
16
+ } from "../chunk-VZNP4R5I.js";
16
17
  import "../chunk-DWED7BS3.js";
17
18
  import "../chunk-Z5PL3K7C.js";
18
19
  import {
@@ -932,6 +933,21 @@ var runRemoteMode = async (binary, args2) => {
932
933
  let sessionId;
933
934
  let killed = false;
934
935
  let totalCostUsd = 0;
936
+ const agentName = `Claude Code - ${projectName}`;
937
+ const announcePresence = (event, action) => {
938
+ if (!sessionId) return Promise.resolve();
939
+ return reportEvent(
940
+ { event, agentType: "claude_code", agentName, action, sessionId, machineId },
941
+ { maxAttempts: 2, timeoutMs: 4e3 }
942
+ ).then(() => {
943
+ }).catch(() => {
944
+ });
945
+ };
946
+ const presenceTimer = setInterval(
947
+ () => void announcePresence("stop", "Standing by for phone commands"),
948
+ 5 * 6e4
949
+ );
950
+ presenceTimer.unref?.();
935
951
  const input = new BoundedInputQueue({
936
952
  cap: INPUT_QUEUE_CAP,
937
953
  onDrop: (_dropped, total) => process.stderr.write(`[pushary] input queue full; dropped ${total} stale instruction(s)
@@ -1009,6 +1025,7 @@ var runRemoteMode = async (binary, args2) => {
1009
1025
  onSessionId: (id) => {
1010
1026
  sessionId = id;
1011
1027
  relay?.reannounce();
1028
+ void announcePresence("session_start", "Remote session ready \u2014 reachable from your phone");
1012
1029
  },
1013
1030
  onCost: (usd) => {
1014
1031
  totalCostUsd = usd;
@@ -1025,10 +1042,12 @@ var runRemoteMode = async (binary, args2) => {
1025
1042
  return { implemented: true, exitCode: result.exitCode };
1026
1043
  } finally {
1027
1044
  for (const signal of signals) process.off(signal, onSignal);
1045
+ clearInterval(presenceTimer);
1028
1046
  relay?.stop();
1029
1047
  poller.stop();
1030
1048
  approver.teardown();
1031
1049
  input.close();
1050
+ await announcePresence("session_closed", "Remote session ended");
1032
1051
  if (totalCostUsd > 0) {
1033
1052
  process.stderr.write(`[pushary] remote session cost: $${totalCostUsd.toFixed(4)}
1034
1053
  `);
@@ -6,10 +6,19 @@ import {
6
6
  import {
7
7
  CODEX_AGENT,
8
8
  DEFAULT_SESSION,
9
+ askUser,
10
+ cancelQuestion,
9
11
  codexAllow,
10
12
  codexDeny,
11
13
  codexPass,
14
+ deriveActionBody,
15
+ deriveBlocker,
16
+ deriveToolTarget,
12
17
  describeApplyPatch,
18
+ describeToolCall,
19
+ fetchModeState,
20
+ getMachineId,
21
+ getPolicy,
13
22
  handlePostToolUse,
14
23
  handleStop,
15
24
  handleUserPrompt,
@@ -17,24 +26,13 @@ import {
17
26
  preToolUseTimeoutDecision,
18
27
  readLastPrompt,
19
28
  reportEvent,
20
- savePendingQuestion,
21
- toCodexWire,
22
- toPolicyLookup
23
- } from "../chunk-NRCQ5UUR.js";
24
- import {
25
- askUser,
26
- cancelQuestion,
27
- deriveActionBody,
28
- deriveBlocker,
29
- deriveToolTarget,
30
- describeToolCall,
31
- fetchModeState,
32
- getMachineId,
33
- getPolicy,
34
29
  resolvePolicy,
30
+ savePendingQuestion,
35
31
  sendNotification,
32
+ toCodexWire,
33
+ toPolicyLookup,
36
34
  waitForAnswer
37
- } from "../chunk-ETDXSKR5.js";
35
+ } from "../chunk-VZNP4R5I.js";
38
36
  import {
39
37
  isGatingMoment,
40
38
  recordKeylessMoment
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- reportEvent
4
- } from "../chunk-NRCQ5UUR.js";
5
2
  import {
6
3
  askUser,
7
4
  getMachineId,
5
+ reportEvent,
8
6
  waitForAnswer
9
- } from "../chunk-ETDXSKR5.js";
7
+ } from "../chunk-VZNP4R5I.js";
10
8
  import "../chunk-DWED7BS3.js";
11
9
  import "../chunk-Z5PL3K7C.js";
12
10
  import {
@@ -5,15 +5,6 @@ import {
5
5
  } from "../chunk-KQYIHZ5E.js";
6
6
  import {
7
7
  DEFAULT_SESSION,
8
- handlePostToolUse,
9
- handleStop,
10
- handleUserPrompt,
11
- readLastPrompt,
12
- readLastUserPrompt,
13
- reportEvent,
14
- savePendingQuestion
15
- } from "../chunk-NRCQ5UUR.js";
16
- import {
17
8
  askUser,
18
9
  cancelQuestion,
19
10
  deriveActionBody,
@@ -23,10 +14,17 @@ import {
23
14
  fetchModeState,
24
15
  getMachineId,
25
16
  getPolicy,
17
+ handlePostToolUse,
18
+ handleStop,
19
+ handleUserPrompt,
20
+ readLastPrompt,
21
+ readLastUserPrompt,
22
+ reportEvent,
26
23
  resolvePolicy,
24
+ savePendingQuestion,
27
25
  sendNotification,
28
26
  waitForAnswer
29
- } from "../chunk-ETDXSKR5.js";
27
+ } from "../chunk-VZNP4R5I.js";
30
28
  import {
31
29
  isGatingMoment,
32
30
  recordKeylessMoment
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-V2TSLWTV.js";
4
+ } from "../chunk-E3Q3KYZF.js";
5
5
  import "../chunk-7EW3USQF.js";
6
6
  import "../chunk-KQYIHZ5E.js";
7
- import "../chunk-NRCQ5UUR.js";
8
- import "../chunk-ETDXSKR5.js";
7
+ import "../chunk-VZNP4R5I.js";
9
8
  import "../chunk-R5AJNXZS.js";
10
9
  import "../chunk-DWED7BS3.js";
11
10
  import "../chunk-Z5PL3K7C.js";
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleNotification
4
- } from "../chunk-NRCQ5UUR.js";
5
- import "../chunk-ETDXSKR5.js";
4
+ } from "../chunk-VZNP4R5I.js";
6
5
  import "../chunk-DWED7BS3.js";
7
6
  import "../chunk-Z5PL3K7C.js";
8
7
  import "../chunk-NKXSILEW.js";
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionDenied
4
- } from "../chunk-V2TSLWTV.js";
4
+ } from "../chunk-E3Q3KYZF.js";
5
5
  import "../chunk-7EW3USQF.js";
6
6
  import "../chunk-KQYIHZ5E.js";
7
- import "../chunk-NRCQ5UUR.js";
8
- import "../chunk-ETDXSKR5.js";
7
+ import "../chunk-VZNP4R5I.js";
9
8
  import "../chunk-R5AJNXZS.js";
10
9
  import "../chunk-DWED7BS3.js";
11
10
  import "../chunk-Z5PL3K7C.js";
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionRequest
4
- } from "../chunk-V2TSLWTV.js";
4
+ } from "../chunk-E3Q3KYZF.js";
5
5
  import "../chunk-7EW3USQF.js";
6
6
  import "../chunk-KQYIHZ5E.js";
7
- import "../chunk-NRCQ5UUR.js";
8
- import "../chunk-ETDXSKR5.js";
7
+ import "../chunk-VZNP4R5I.js";
9
8
  import "../chunk-R5AJNXZS.js";
10
9
  import "../chunk-DWED7BS3.js";
11
10
  import "../chunk-Z5PL3K7C.js";
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-NRCQ5UUR.js";
5
- import "../chunk-ETDXSKR5.js";
4
+ } from "../chunk-VZNP4R5I.js";
6
5
  import "../chunk-DWED7BS3.js";
7
6
  import "../chunk-Z5PL3K7C.js";
8
7
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleUserPrompt
4
- } from "../chunk-NRCQ5UUR.js";
5
- import "../chunk-ETDXSKR5.js";
4
+ } from "../chunk-VZNP4R5I.js";
6
5
  import "../chunk-DWED7BS3.js";
7
6
  import "../chunk-Z5PL3K7C.js";
8
7
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionEnd
4
- } from "../chunk-NRCQ5UUR.js";
5
- import "../chunk-ETDXSKR5.js";
4
+ } from "../chunk-VZNP4R5I.js";
6
5
  import "../chunk-DWED7BS3.js";
7
6
  import "../chunk-Z5PL3K7C.js";
8
7
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionStart
4
- } from "../chunk-NRCQ5UUR.js";
5
- import "../chunk-ETDXSKR5.js";
4
+ } from "../chunk-VZNP4R5I.js";
6
5
  import "../chunk-DWED7BS3.js";
7
6
  import "../chunk-Z5PL3K7C.js";
8
7
  import "../chunk-NKXSILEW.js";
@@ -24,8 +24,7 @@ import {
24
24
  } from "../chunk-7EW3USQF.js";
25
25
  import {
26
26
  reportEvent
27
- } from "../chunk-NRCQ5UUR.js";
28
- import "../chunk-ETDXSKR5.js";
27
+ } from "../chunk-VZNP4R5I.js";
29
28
  import "../chunk-DWED7BS3.js";
30
29
  import {
31
30
  isValidApiKey
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-NRCQ5UUR.js";
5
- import "../chunk-ETDXSKR5.js";
4
+ } from "../chunk-VZNP4R5I.js";
6
5
  import "../chunk-DWED7BS3.js";
7
6
  import "../chunk-Z5PL3K7C.js";
8
7
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStopFailure
4
- } from "../chunk-NRCQ5UUR.js";
5
- import "../chunk-ETDXSKR5.js";
4
+ } from "../chunk-VZNP4R5I.js";
6
5
  import "../chunk-DWED7BS3.js";
7
6
  import "../chunk-Z5PL3K7C.js";
8
7
  import "../chunk-NKXSILEW.js";
@@ -7,12 +7,6 @@ import {
7
7
  } from "./chunk-KQYIHZ5E.js";
8
8
  import {
9
9
  DEFAULT_SESSION,
10
- readLastPrompt,
11
- readLastUserPrompt,
12
- savePendingQuestion,
13
- throttlePass
14
- } from "./chunk-NRCQ5UUR.js";
15
- import {
16
10
  askUser,
17
11
  cancelQuestion,
18
12
  deriveAction,
@@ -23,10 +17,14 @@ import {
23
17
  fetchModeState,
24
18
  getMachineId,
25
19
  getPolicy,
20
+ readLastPrompt,
21
+ readLastUserPrompt,
26
22
  resolvePolicy,
23
+ savePendingQuestion,
27
24
  sendNotification,
25
+ throttlePass,
28
26
  waitForAnswer
29
- } from "./chunk-ETDXSKR5.js";
27
+ } from "./chunk-VZNP4R5I.js";
30
28
  import {
31
29
  isGatingMoment,
32
30
  recordKeylessMoment
@@ -1,39 +1,353 @@
1
1
  import {
2
- cancelQuestion,
3
- deriveReceiptMeta,
4
- deriveToolTarget,
5
- describeToolCall,
6
- fetchModeState,
7
- getMachineId,
8
- isPolicyConfig,
9
- isToolResultError,
10
- resolveAutoResolveOrigin,
11
- resolvePolicy,
12
- sendNotification,
13
- waitForAnswer
14
- } from "./chunk-ETDXSKR5.js";
15
- import {
2
+ callMcpTool,
16
3
  withRetry
17
4
  } from "./chunk-DWED7BS3.js";
5
+ import {
6
+ ACTION_BODY_MAX,
7
+ DECISION_LINE_MAX,
8
+ extractPolicyArg,
9
+ isApprovalMode,
10
+ isSafeReadOnlyCommand,
11
+ matchRankWeight,
12
+ matchToolPattern,
13
+ redactSecrets,
14
+ redactSecretsDeep
15
+ } from "./chunk-Z5PL3K7C.js";
18
16
  import {
19
17
  getApiKey,
20
18
  getBaseUrl
21
19
  } from "./chunk-NKXSILEW.js";
22
20
 
23
- // src/pending.ts
21
+ // src/identity.ts
22
+ import { createHash } from "crypto";
23
+ import { hostname } from "os";
24
+ var deriveMachineId = (host) => createHash("sha256").update(host).digest("hex").slice(0, 8);
25
+ var getMachineId = () => deriveMachineId(hostname());
26
+
27
+ // src/policy.ts
28
+ import { createHash as createHash2 } from "crypto";
29
+ import { existsSync, readFileSync, writeFileSync } from "fs";
24
30
  import { join } from "path";
25
31
  import { tmpdir } from "os";
26
- import { existsSync, mkdirSync, writeFileSync, readdirSync, unlinkSync, rmSync, statSync } from "fs";
27
- var PENDING_DIR = join(tmpdir(), "pushary-pending");
32
+
33
+ // src/validate.ts
34
+ var isPolicyConfig = (data) => {
35
+ if (!data || typeof data !== "object") return false;
36
+ const d = data;
37
+ return Array.isArray(d.policies) && typeof d.defaultTimeoutSeconds === "number" && typeof d.defaultTimeoutAction === "string";
38
+ };
39
+ var isAskUserResponse = (data) => {
40
+ if (!data || typeof data !== "object") return false;
41
+ const d = data;
42
+ return typeof d.correlationId === "string" && typeof d.status === "string";
43
+ };
44
+ var isWaitForAnswerResponse = (data) => {
45
+ if (!data || typeof data !== "object") return false;
46
+ const d = data;
47
+ return typeof d.answered === "boolean";
48
+ };
49
+
50
+ // src/policy.ts
51
+ var CACHE_TTL_MS = 60 * 1e3;
52
+ var cacheFile = (apiKey) => {
53
+ const hash = createHash2("sha256").update(apiKey).digest("hex").slice(0, 12);
54
+ return join(tmpdir(), `pushary-policy-${hash}.json`);
55
+ };
56
+ var fetchPolicy = async (apiKey) => {
57
+ return withRetry(async () => {
58
+ const baseUrl = getBaseUrl();
59
+ const response = await fetch(`${baseUrl}/api/mcp/policy`, {
60
+ headers: { "Authorization": `Bearer ${apiKey}` },
61
+ signal: AbortSignal.timeout(1e4)
62
+ });
63
+ if (!response.ok) {
64
+ throw new Error(`Failed to fetch policy: ${response.status}`);
65
+ }
66
+ const raw = await response.json();
67
+ if (!isPolicyConfig(raw)) throw new Error("Invalid policy response");
68
+ return raw;
69
+ }, { maxAttempts: 2 });
70
+ };
71
+ var getPolicy = async (apiKey, expectedVersion) => {
72
+ const path = cacheFile(apiKey);
73
+ let staleCache = null;
74
+ if (existsSync(path)) {
75
+ try {
76
+ const stat = readFileSync(path, "utf-8");
77
+ const cached = JSON.parse(stat);
78
+ if (!isPolicyConfig(cached)) throw new Error("Corrupted cache");
79
+ const versionStale = expectedVersion != null && (cached._policyVersion ?? null) !== expectedVersion;
80
+ const ttlFresh = !cached._cachedAt || Date.now() - cached._cachedAt < CACHE_TTL_MS;
81
+ if (ttlFresh && !versionStale) {
82
+ return cached;
83
+ }
84
+ staleCache = cached;
85
+ } catch {
86
+ }
87
+ }
88
+ try {
89
+ const policy = await fetchPolicy(apiKey);
90
+ try {
91
+ writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), "utf-8");
92
+ } catch {
93
+ }
94
+ return policy;
95
+ } catch {
96
+ if (staleCache) return staleCache;
97
+ throw new Error("Failed to fetch policy and no cached policy available");
98
+ }
99
+ };
100
+ var findBestMatch = (policies, toolName, arg) => {
101
+ let best;
102
+ let bestWeight = 0;
103
+ let bestLength = -1;
104
+ for (const candidate of policies) {
105
+ const rank = matchToolPattern(candidate.tool, toolName, arg);
106
+ if (rank === "none") continue;
107
+ const weight = matchRankWeight(rank);
108
+ const length = rank === "prefix" ? candidate.tool.length : -1;
109
+ if (weight > bestWeight || weight === bestWeight && length > bestLength) {
110
+ best = { policy: candidate, rank };
111
+ bestWeight = weight;
112
+ bestLength = length;
113
+ }
114
+ }
115
+ return best;
116
+ };
117
+ var autoApprove = (tool) => ({
118
+ tool,
119
+ timeoutSeconds: 0,
120
+ timeoutAction: "approve",
121
+ mode: "terminal_only",
122
+ pushFirstSeconds: 0
123
+ });
124
+ var resolveAutoResolveOrigin = (config, toolName, toolInput) => {
125
+ const arg = toolInput ? extractPolicyArg(toolName, toolInput) : void 0;
126
+ const match = findBestMatch(config.policies, toolName, arg);
127
+ const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
128
+ if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
129
+ return "safe_readonly";
130
+ }
131
+ return "policy_timeout";
132
+ };
133
+ var resolvePolicy = (config, toolName, modeOverride, toolInput) => {
134
+ const arg = toolInput ? extractPolicyArg(toolName, toolInput) : void 0;
135
+ const match = findBestMatch(config.policies, toolName, arg);
136
+ let base = match?.policy ?? config.policies.find((p) => p.tool === "*") ?? {
137
+ tool: toolName,
138
+ timeoutSeconds: config.defaultTimeoutSeconds,
139
+ timeoutAction: config.defaultTimeoutAction,
140
+ mode: config.defaultMode ?? "push_first",
141
+ pushFirstSeconds: config.defaultPushFirstSeconds ?? 20
142
+ };
143
+ const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
144
+ if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
145
+ base = autoApprove(base.tool);
146
+ }
147
+ const effectiveOverride = modeOverride ?? config.modeOverride;
148
+ if (effectiveOverride) {
149
+ return { ...base, mode: effectiveOverride };
150
+ }
151
+ return base;
152
+ };
153
+ var toPolicyVersion = (value) => typeof value === "string" || typeof value === "number" ? String(value) : null;
154
+ var fetchModeState = async (apiKey, sessionId) => {
155
+ try {
156
+ const baseUrl = getBaseUrl();
157
+ const url = sessionId ? `${baseUrl}/api/mcp/mode?session=${encodeURIComponent(sessionId)}` : `${baseUrl}/api/mcp/mode`;
158
+ const response = await fetch(url, {
159
+ headers: { "Authorization": `Bearer ${apiKey}` },
160
+ signal: AbortSignal.timeout(3e3)
161
+ });
162
+ if (!response.ok) return { mode: null, kill: false, policyVersion: null, relayUrl: null };
163
+ const data = await response.json();
164
+ const mode = data.override?.mode;
165
+ return {
166
+ mode: isApprovalMode(mode) ? mode : null,
167
+ kill: data.kill === true,
168
+ policyVersion: toPolicyVersion(data.policyVersion),
169
+ relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null
170
+ };
171
+ } catch {
172
+ return { mode: null, kill: false, policyVersion: null, relayUrl: null };
173
+ }
174
+ };
175
+ var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
176
+
177
+ // src/api.ts
178
+ var askUser = async (apiKey, params, timeoutMs = 15e3) => {
179
+ const result = await callMcpTool(apiKey, "ask_user", { ...params, wait: false }, { maxRetries: 3, timeoutMs });
180
+ if (!isAskUserResponse(result)) throw new Error("Invalid ask_user response");
181
+ return result;
182
+ };
183
+ var waitForAnswer = async (apiKey, correlationId, timeoutMs = 3e4) => {
184
+ const result = await callMcpTool(apiKey, "wait_for_answer", {
185
+ correlationId,
186
+ timeoutMs
187
+ }, { timeoutMs: timeoutMs + 1e4 });
188
+ if (!isWaitForAnswerResponse(result)) throw new Error("Invalid wait_for_answer response");
189
+ return result;
190
+ };
191
+ var cancelQuestion = async (apiKey, correlationId) => {
192
+ await callMcpTool(apiKey, "cancel_question", { correlationId });
193
+ };
194
+ var sendNotification = async (apiKey, params, timeoutMs = 15e3) => {
195
+ await callMcpTool(apiKey, "send_notification", { ...params }, { maxRetries: 3, timeoutMs });
196
+ };
197
+
198
+ // src/describe.ts
199
+ import { isAbsolute, relative } from "path";
200
+ var hookPrefixes = {
201
+ Bash: (input) => `bash: ${input.command ?? "(no command)"}`,
202
+ PowerShell: (input) => `powershell: ${input.command ?? "(no command)"}`,
203
+ Monitor: (input) => `monitor: ${input.command ?? "(no command)"}`,
204
+ Write: (input) => `write file: ${input.file_path ?? "(unknown path)"}`,
205
+ Edit: (input) => `edit file: ${input.file_path ?? "(unknown path)"}`,
206
+ MultiEdit: (input) => `edit file: ${input.file_path ?? "(unknown path)"}`,
207
+ Read: (input) => `read file: ${input.file_path ?? "(unknown path)"}`
208
+ };
209
+ var describeTodoProgress = (todos) => {
210
+ if (!Array.isArray(todos)) return "updated the plan";
211
+ const items = todos.filter((t) => !!t && typeof t === "object");
212
+ const total = items.length;
213
+ if (total === 0) return "updated the plan";
214
+ const done = items.filter((t) => t.status === "completed").length;
215
+ const current = items.find((t) => t.status === "in_progress");
216
+ const label = current ? [current.activeForm, current.content].find((v) => typeof v === "string" && v.trim().length > 0) : void 0;
217
+ if (done >= total) return `finished all ${total} tasks`;
218
+ if (label) return `${label.trim()} (${done}/${total} done)`;
219
+ return `${done}/${total} tasks done`;
220
+ };
221
+ var eventPrefixes = {
222
+ Bash: (input) => `ran: ${String(input.command ?? "")}`,
223
+ PowerShell: (input) => `ran: ${String(input.command ?? "")}`,
224
+ Monitor: (input) => `ran: ${String(input.command ?? "")}`,
225
+ Write: (input) => `wrote: ${input.file_path ?? "unknown"}`,
226
+ Edit: (input) => `edited: ${input.file_path ?? "unknown"}`,
227
+ MultiEdit: (input) => `edited: ${input.file_path ?? "unknown"}`,
228
+ Read: (input) => `read: ${input.file_path ?? "unknown"}`,
229
+ TodoWrite: (input) => describeTodoProgress(input.todos)
230
+ };
231
+ var EVENT_ACTION_MAX = 120;
232
+ var HOOK_FALLBACK_MAX = 200;
233
+ var describeToolCall = (toolName, toolInput, format = "hook") => {
234
+ const prefixes = format === "hook" ? hookPrefixes : eventPrefixes;
235
+ const builder = prefixes[toolName];
236
+ const raw = builder ? builder(toolInput) : format === "hook" ? `${toolName}: ${JSON.stringify(toolInput)}` : `${toolName}: done`;
237
+ const redacted = redactSecrets(raw);
238
+ if (format === "event") return redacted.slice(0, EVENT_ACTION_MAX);
239
+ return builder ? redacted : redacted.slice(0, HOOK_FALLBACK_MAX);
240
+ };
241
+ var TOOL_TARGET_MAX_LENGTH = 80;
242
+ var deriveCommandHead = (command) => {
243
+ if (typeof command !== "string") return void 0;
244
+ const head = command.trim().split(/\s+/).slice(0, 2).join(" ");
245
+ return head ? head.slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
246
+ };
247
+ var deriveToolTarget = (toolName, toolInput) => {
248
+ if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
249
+ return deriveCommandHead(toolInput.command);
250
+ }
251
+ if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit") {
252
+ const filePath = toolInput.file_path;
253
+ if (typeof filePath !== "string") return void 0;
254
+ const separator = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
255
+ const base = filePath.slice(separator + 1);
256
+ const dot = base.lastIndexOf(".");
257
+ if (dot <= 0) return void 0;
258
+ return base.slice(dot).slice(0, TOOL_TARGET_MAX_LENGTH);
259
+ }
260
+ return void 0;
261
+ };
262
+ var RECEIPT_TARGET_MAX_LENGTH = 256;
263
+ var isToolResultError = (toolResult) => {
264
+ try {
265
+ return Boolean(toolResult && ("error" in toolResult || "is_error" in toolResult));
266
+ } catch {
267
+ return false;
268
+ }
269
+ };
270
+ var relativizeReceiptPath = (filePath, cwd) => {
271
+ if (!cwd || !isAbsolute(filePath)) return filePath;
272
+ return relative(cwd, filePath);
273
+ };
274
+ var deriveReceiptMeta = (toolName, toolInput, toolResult, cwd) => {
275
+ try {
276
+ const ok = !isToolResultError(toolResult);
277
+ if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit") {
278
+ const filePath = toolInput.file_path;
279
+ if (typeof filePath !== "string" || !filePath) return void 0;
280
+ return {
281
+ kind: toolName === "Write" ? "write" : "edit",
282
+ target: relativizeReceiptPath(filePath, cwd).slice(0, RECEIPT_TARGET_MAX_LENGTH),
283
+ ok
284
+ };
285
+ }
286
+ if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
287
+ const head = deriveCommandHead(toolInput.command);
288
+ if (!head) return void 0;
289
+ return {
290
+ kind: head === "git commit" ? "commit" : "bash",
291
+ target: head,
292
+ ok
293
+ };
294
+ }
295
+ return void 0;
296
+ } catch {
297
+ return void 0;
298
+ }
299
+ };
300
+ var firstString = (...values) => values.find((v) => typeof v === "string" && v.length > 0);
301
+ var ACTION_BODY_TRUNCATION_MARKER = "\n\u2026 [truncated]";
302
+ var capActionBody = (text) => text.length <= ACTION_BODY_MAX ? text : `${text.slice(0, ACTION_BODY_MAX - ACTION_BODY_TRUNCATION_MARKER.length)}${ACTION_BODY_TRUNCATION_MARKER}`;
303
+ var rawActionBody = (toolName, toolInput) => {
304
+ if (toolName === "Edit" || toolName === "replace") {
305
+ const before = firstString(toolInput.old_string);
306
+ const after = firstString(toolInput.new_string);
307
+ if (before === void 0 && after === void 0) return void 0;
308
+ return `- ${before ?? ""}
309
+ + ${after ?? ""}`;
310
+ }
311
+ if (toolName === "Write" || toolName === "write_file") return firstString(toolInput.content);
312
+ if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor" || toolName === "run_shell_command" || toolName === "shell" || toolName === "apply_patch") {
313
+ return firstString(toolInput.command);
314
+ }
315
+ return void 0;
316
+ };
317
+ var deriveActionBody = (toolName, toolInput) => {
318
+ try {
319
+ const raw = rawActionBody(toolName, toolInput);
320
+ if (!raw) return void 0;
321
+ return capActionBody(redactSecretsDeep(raw));
322
+ } catch {
323
+ return void 0;
324
+ }
325
+ };
326
+ var deriveAction = (toolName, toolInput) => describeToolCall(toolName, toolInput, "hook").slice(0, DECISION_LINE_MAX);
327
+ var BLOCKER_BY_MODE = {
328
+ push_only: "Approval required before this runs",
329
+ push_first: "Paused for your approval before continuing",
330
+ escalate: "Escalated to you after the approval timeout"
331
+ };
332
+ var deriveBlocker = (mode, reason) => {
333
+ const text = reason?.replace(/\s+/g, " ").trim() || BLOCKER_BY_MODE[mode] || "Waiting for your approval";
334
+ return text.slice(0, DECISION_LINE_MAX);
335
+ };
336
+
337
+ // src/pending.ts
338
+ import { join as join2 } from "path";
339
+ import { tmpdir as tmpdir2 } from "os";
340
+ import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2, readdirSync, unlinkSync, rmSync, statSync } from "fs";
341
+ var PENDING_DIR = join2(tmpdir2(), "pushary-pending");
28
342
  var DEFAULT_SESSION = "_no_session";
29
343
  var GRACE_MS = 10 * 60 * 1e3;
30
344
  var sanitize = (sessionId) => sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || DEFAULT_SESSION;
31
- var dirFor = (sessionId) => join(PENDING_DIR, sanitize(sessionId));
345
+ var dirFor = (sessionId) => join2(PENDING_DIR, sanitize(sessionId));
32
346
  var isDefaultSession = (sessionId) => sanitize(sessionId) === DEFAULT_SESSION;
33
347
  var savePendingQuestion = (sessionId, correlationId) => {
34
348
  const dir = dirFor(sessionId);
35
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
- writeFileSync(join(dir, correlationId), "", "utf-8");
349
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
350
+ writeFileSync2(join2(dir, correlationId), "", "utf-8");
37
351
  };
38
352
  var listPendingQuestions = (sessionId) => {
39
353
  const dir = dirFor(sessionId);
@@ -47,7 +361,7 @@ var listPendingQuestions = (sessionId) => {
47
361
  const cutoff = Date.now() - GRACE_MS;
48
362
  return files.filter((name) => {
49
363
  try {
50
- return statSync(join(dir, name)).mtimeMs < cutoff;
364
+ return statSync(join2(dir, name)).mtimeMs < cutoff;
51
365
  } catch {
52
366
  return false;
53
367
  }
@@ -55,7 +369,7 @@ var listPendingQuestions = (sessionId) => {
55
369
  };
56
370
  var removePendingQuestion = (sessionId, correlationId) => {
57
371
  try {
58
- unlinkSync(join(dirFor(sessionId), correlationId));
372
+ unlinkSync(join2(dirFor(sessionId), correlationId));
59
373
  } catch {
60
374
  }
61
375
  };
@@ -67,9 +381,9 @@ var removePendingSession = (sessionId) => {
67
381
  };
68
382
 
69
383
  // src/usage.ts
70
- import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync, readSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
71
- import { join as join2 } from "path";
72
- import { tmpdir as tmpdir2 } from "os";
384
+ import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, readSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
385
+ import { join as join3 } from "path";
386
+ import { tmpdir as tmpdir3 } from "os";
73
387
  var DEFAULT_PRICES = [
74
388
  { match: "opus-4-1", in: 15, out: 75 },
75
389
  { match: "opus-4-0", in: 15, out: 75 },
@@ -104,12 +418,12 @@ var estimateCostUsd = (usage, model) => {
104
418
  const perTokenOut = price.out / 1e6;
105
419
  return usage.inputTokens * perTokenIn + usage.outputTokens * perTokenOut + usage.cacheCreationTokens * perTokenIn * CACHE_WRITE_MULTIPLIER + usage.cacheReadTokens * perTokenIn * CACHE_READ_MULTIPLIER;
106
420
  };
107
- var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join2(tmpdir2(), "pushary-usage");
108
- var stateFile = (sessionId) => join2(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
421
+ var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join3(tmpdir3(), "pushary-usage");
422
+ var stateFile = (sessionId) => join3(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
109
423
  var emptyState = () => ({ offset: 0, tokensIn: 0, tokensOut: 0, costUsd: 0, recentIds: [] });
110
424
  var readState = (path) => {
111
425
  try {
112
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
426
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
113
427
  if (typeof parsed.offset === "number" && parsed.offset >= 0 && typeof parsed.tokensIn === "number" && typeof parsed.tokensOut === "number" && typeof parsed.costUsd === "number" && Array.isArray(parsed.recentIds)) {
114
428
  return {
115
429
  offset: parsed.offset,
@@ -224,7 +538,7 @@ var readNewUsage = (transcriptPath, sessionId) => {
224
538
  }
225
539
  }
226
540
  mkdirSync2(stateDir(), { recursive: true });
227
- writeFileSync2(path, JSON.stringify(state), "utf-8");
541
+ writeFileSync3(path, JSON.stringify(state), "utf-8");
228
542
  if (state.tokensIn === 0 && state.tokensOut === 0) return null;
229
543
  return {
230
544
  tokensIn: state.tokensIn,
@@ -316,18 +630,18 @@ var permissionTimeoutDecision = (timeoutAction) => {
316
630
  var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
317
631
 
318
632
  // src/throttle.ts
319
- import { join as join3 } from "path";
320
- import { tmpdir as tmpdir3 } from "os";
321
- import { createHash } from "crypto";
322
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
323
- var THROTTLE_DIR = join3(tmpdir3(), "pushary-throttle");
324
- var markerPath = (key) => join3(THROTTLE_DIR, createHash("sha256").update(key).digest("hex").slice(0, 16));
633
+ import { join as join4 } from "path";
634
+ import { tmpdir as tmpdir4 } from "os";
635
+ import { createHash as createHash3 } from "crypto";
636
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
637
+ var THROTTLE_DIR = join4(tmpdir4(), "pushary-throttle");
638
+ var markerPath = (key) => join4(THROTTLE_DIR, createHash3("sha256").update(key).digest("hex").slice(0, 16));
325
639
  var throttlePass = (key, windowMs) => {
326
640
  try {
327
641
  const path = markerPath(key);
328
- if (existsSync2(path) && Date.now() - statSync3(path).mtimeMs < windowMs) return false;
642
+ if (existsSync3(path) && Date.now() - statSync3(path).mtimeMs < windowMs) return false;
329
643
  mkdirSync3(THROTTLE_DIR, { recursive: true });
330
- writeFileSync3(path, "", "utf-8");
644
+ writeFileSync4(path, "", "utf-8");
331
645
  return true;
332
646
  } catch {
333
647
  return false;
@@ -335,21 +649,21 @@ var throttlePass = (key, windowMs) => {
335
649
  };
336
650
 
337
651
  // src/events.ts
338
- import { basename, join as join4 } from "path";
339
- import { createHash as createHash2 } from "crypto";
340
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
341
- import { tmpdir as tmpdir4 } from "os";
342
- var INTENT_DIR = join4(tmpdir4(), "pushary-intent");
652
+ import { basename, join as join5 } from "path";
653
+ import { createHash as createHash4 } from "crypto";
654
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
655
+ import { tmpdir as tmpdir5 } from "os";
656
+ var INTENT_DIR = join5(tmpdir5(), "pushary-intent");
343
657
  var INTENT_MAX_BYTES = 2048;
344
658
  var INTENT_GRACE_MS = 10 * 60 * 1e3;
345
659
  var sanitizeSession = (sessionId) => sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || DEFAULT_SESSION;
346
- var intentFile = (sessionId) => join4(INTENT_DIR, sanitizeSession(sessionId));
660
+ var intentFile = (sessionId) => join5(INTENT_DIR, sanitizeSession(sessionId));
347
661
  var saveLastPrompt = (sessionId, prompt) => {
348
662
  try {
349
663
  const trimmed = prompt.replace(/\s+/g, " ").trim().slice(0, INTENT_MAX_BYTES);
350
664
  if (!trimmed) return;
351
665
  mkdirSync4(INTENT_DIR, { recursive: true });
352
- writeFileSync4(intentFile(sessionId), trimmed, "utf-8");
666
+ writeFileSync5(intentFile(sessionId), trimmed, "utf-8");
353
667
  } catch {
354
668
  }
355
669
  };
@@ -357,7 +671,7 @@ var readLastPrompt = (sessionId) => {
357
671
  try {
358
672
  const path = intentFile(sessionId);
359
673
  if (isDefaultSession(sessionId) && statSync4(path).mtimeMs < Date.now() - INTENT_GRACE_MS) return void 0;
360
- const value = readFileSync2(path, "utf-8").trim();
674
+ const value = readFileSync3(path, "utf-8").trim();
361
675
  return value.length > 0 ? value : void 0;
362
676
  } catch {
363
677
  return void 0;
@@ -413,10 +727,10 @@ var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
413
727
  var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
414
728
  var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
415
729
  var readFreshCachedPolicy = (apiKey) => {
416
- const hash = createHash2("sha256").update(apiKey).digest("hex").slice(0, 12);
417
- const path = join4(tmpdir4(), `pushary-policy-${hash}.json`);
418
- if (!existsSync3(path)) return null;
419
- const cached = JSON.parse(readFileSync2(path, "utf-8"));
730
+ const hash = createHash4("sha256").update(apiKey).digest("hex").slice(0, 12);
731
+ const path = join5(tmpdir5(), `pushary-policy-${hash}.json`);
732
+ if (!existsSync4(path)) return null;
733
+ const cached = JSON.parse(readFileSync3(path, "utf-8"));
420
734
  if (!isPolicyConfig(cached)) return null;
421
735
  if (!cached._cachedAt || Date.now() - cached._cachedAt >= POLICY_CACHE_TTL_MS) return null;
422
736
  return cached;
@@ -773,6 +1087,20 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
773
1087
  };
774
1088
 
775
1089
  export {
1090
+ getMachineId,
1091
+ getPolicy,
1092
+ resolvePolicy,
1093
+ fetchModeState,
1094
+ fetchModeOverride,
1095
+ askUser,
1096
+ waitForAnswer,
1097
+ cancelQuestion,
1098
+ sendNotification,
1099
+ describeToolCall,
1100
+ deriveToolTarget,
1101
+ deriveActionBody,
1102
+ deriveAction,
1103
+ deriveBlocker,
776
1104
  DEFAULT_SESSION,
777
1105
  savePendingQuestion,
778
1106
  throttlePass,
package/dist/src/index.js CHANGED
@@ -1,23 +1,21 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-V2TSLWTV.js";
3
+ } from "../chunk-E3Q3KYZF.js";
4
4
  import "../chunk-7EW3USQF.js";
5
5
  import "../chunk-KQYIHZ5E.js";
6
- import {
7
- handleNotification,
8
- handlePostToolUse,
9
- handleStop,
10
- reportEvent
11
- } from "../chunk-NRCQ5UUR.js";
12
6
  import {
13
7
  askUser,
14
8
  cancelQuestion,
15
9
  fetchModeOverride,
16
10
  fetchModeState,
17
11
  getPolicy,
12
+ handleNotification,
13
+ handlePostToolUse,
14
+ handleStop,
15
+ reportEvent,
18
16
  resolvePolicy,
19
17
  waitForAnswer
20
- } from "../chunk-ETDXSKR5.js";
18
+ } from "../chunk-VZNP4R5I.js";
21
19
  import "../chunk-R5AJNXZS.js";
22
20
  import "../chunk-DWED7BS3.js";
23
21
  import "../chunk-Z5PL3K7C.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -1,355 +0,0 @@
1
- import {
2
- callMcpTool,
3
- withRetry
4
- } from "./chunk-DWED7BS3.js";
5
- import {
6
- ACTION_BODY_MAX,
7
- DECISION_LINE_MAX,
8
- extractPolicyArg,
9
- isApprovalMode,
10
- isSafeReadOnlyCommand,
11
- matchRankWeight,
12
- matchToolPattern,
13
- redactSecrets,
14
- redactSecretsDeep
15
- } from "./chunk-Z5PL3K7C.js";
16
- import {
17
- getBaseUrl
18
- } from "./chunk-NKXSILEW.js";
19
-
20
- // src/identity.ts
21
- import { createHash } from "crypto";
22
- import { hostname } from "os";
23
- var deriveMachineId = (host) => createHash("sha256").update(host).digest("hex").slice(0, 8);
24
- var getMachineId = () => deriveMachineId(hostname());
25
-
26
- // src/policy.ts
27
- import { createHash as createHash2 } from "crypto";
28
- import { existsSync, readFileSync, writeFileSync } from "fs";
29
- import { join } from "path";
30
- import { tmpdir } from "os";
31
-
32
- // src/validate.ts
33
- var isPolicyConfig = (data) => {
34
- if (!data || typeof data !== "object") return false;
35
- const d = data;
36
- return Array.isArray(d.policies) && typeof d.defaultTimeoutSeconds === "number" && typeof d.defaultTimeoutAction === "string";
37
- };
38
- var isAskUserResponse = (data) => {
39
- if (!data || typeof data !== "object") return false;
40
- const d = data;
41
- return typeof d.correlationId === "string" && typeof d.status === "string";
42
- };
43
- var isWaitForAnswerResponse = (data) => {
44
- if (!data || typeof data !== "object") return false;
45
- const d = data;
46
- return typeof d.answered === "boolean";
47
- };
48
-
49
- // src/policy.ts
50
- var CACHE_TTL_MS = 60 * 1e3;
51
- var cacheFile = (apiKey) => {
52
- const hash = createHash2("sha256").update(apiKey).digest("hex").slice(0, 12);
53
- return join(tmpdir(), `pushary-policy-${hash}.json`);
54
- };
55
- var fetchPolicy = async (apiKey) => {
56
- return withRetry(async () => {
57
- const baseUrl = getBaseUrl();
58
- const response = await fetch(`${baseUrl}/api/mcp/policy`, {
59
- headers: { "Authorization": `Bearer ${apiKey}` },
60
- signal: AbortSignal.timeout(1e4)
61
- });
62
- if (!response.ok) {
63
- throw new Error(`Failed to fetch policy: ${response.status}`);
64
- }
65
- const raw = await response.json();
66
- if (!isPolicyConfig(raw)) throw new Error("Invalid policy response");
67
- return raw;
68
- }, { maxAttempts: 2 });
69
- };
70
- var getPolicy = async (apiKey, expectedVersion) => {
71
- const path = cacheFile(apiKey);
72
- let staleCache = null;
73
- if (existsSync(path)) {
74
- try {
75
- const stat = readFileSync(path, "utf-8");
76
- const cached = JSON.parse(stat);
77
- if (!isPolicyConfig(cached)) throw new Error("Corrupted cache");
78
- const versionStale = expectedVersion != null && (cached._policyVersion ?? null) !== expectedVersion;
79
- const ttlFresh = !cached._cachedAt || Date.now() - cached._cachedAt < CACHE_TTL_MS;
80
- if (ttlFresh && !versionStale) {
81
- return cached;
82
- }
83
- staleCache = cached;
84
- } catch {
85
- }
86
- }
87
- try {
88
- const policy = await fetchPolicy(apiKey);
89
- try {
90
- writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), "utf-8");
91
- } catch {
92
- }
93
- return policy;
94
- } catch {
95
- if (staleCache) return staleCache;
96
- throw new Error("Failed to fetch policy and no cached policy available");
97
- }
98
- };
99
- var findBestMatch = (policies, toolName, arg) => {
100
- let best;
101
- let bestWeight = 0;
102
- let bestLength = -1;
103
- for (const candidate of policies) {
104
- const rank = matchToolPattern(candidate.tool, toolName, arg);
105
- if (rank === "none") continue;
106
- const weight = matchRankWeight(rank);
107
- const length = rank === "prefix" ? candidate.tool.length : -1;
108
- if (weight > bestWeight || weight === bestWeight && length > bestLength) {
109
- best = { policy: candidate, rank };
110
- bestWeight = weight;
111
- bestLength = length;
112
- }
113
- }
114
- return best;
115
- };
116
- var autoApprove = (tool) => ({
117
- tool,
118
- timeoutSeconds: 0,
119
- timeoutAction: "approve",
120
- mode: "terminal_only",
121
- pushFirstSeconds: 0
122
- });
123
- var resolveAutoResolveOrigin = (config, toolName, toolInput) => {
124
- const arg = toolInput ? extractPolicyArg(toolName, toolInput) : void 0;
125
- const match = findBestMatch(config.policies, toolName, arg);
126
- const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
127
- if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
128
- return "safe_readonly";
129
- }
130
- return "policy_timeout";
131
- };
132
- var resolvePolicy = (config, toolName, modeOverride, toolInput) => {
133
- const arg = toolInput ? extractPolicyArg(toolName, toolInput) : void 0;
134
- const match = findBestMatch(config.policies, toolName, arg);
135
- let base = match?.policy ?? config.policies.find((p) => p.tool === "*") ?? {
136
- tool: toolName,
137
- timeoutSeconds: config.defaultTimeoutSeconds,
138
- timeoutAction: config.defaultTimeoutAction,
139
- mode: config.defaultMode ?? "push_first",
140
- pushFirstSeconds: config.defaultPushFirstSeconds ?? 20
141
- };
142
- const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
143
- if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
144
- base = autoApprove(base.tool);
145
- }
146
- const effectiveOverride = modeOverride ?? config.modeOverride;
147
- if (effectiveOverride) {
148
- return { ...base, mode: effectiveOverride };
149
- }
150
- return base;
151
- };
152
- var toPolicyVersion = (value) => typeof value === "string" || typeof value === "number" ? String(value) : null;
153
- var fetchModeState = async (apiKey, sessionId) => {
154
- try {
155
- const baseUrl = getBaseUrl();
156
- const url = sessionId ? `${baseUrl}/api/mcp/mode?session=${encodeURIComponent(sessionId)}` : `${baseUrl}/api/mcp/mode`;
157
- const response = await fetch(url, {
158
- headers: { "Authorization": `Bearer ${apiKey}` },
159
- signal: AbortSignal.timeout(3e3)
160
- });
161
- if (!response.ok) return { mode: null, kill: false, policyVersion: null, relayUrl: null };
162
- const data = await response.json();
163
- const mode = data.override?.mode;
164
- return {
165
- mode: isApprovalMode(mode) ? mode : null,
166
- kill: data.kill === true,
167
- policyVersion: toPolicyVersion(data.policyVersion),
168
- relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null
169
- };
170
- } catch {
171
- return { mode: null, kill: false, policyVersion: null, relayUrl: null };
172
- }
173
- };
174
- var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
175
-
176
- // src/api.ts
177
- var askUser = async (apiKey, params, timeoutMs = 15e3) => {
178
- const result = await callMcpTool(apiKey, "ask_user", { ...params, wait: false }, { maxRetries: 3, timeoutMs });
179
- if (!isAskUserResponse(result)) throw new Error("Invalid ask_user response");
180
- return result;
181
- };
182
- var waitForAnswer = async (apiKey, correlationId, timeoutMs = 3e4) => {
183
- const result = await callMcpTool(apiKey, "wait_for_answer", {
184
- correlationId,
185
- timeoutMs
186
- }, { timeoutMs: timeoutMs + 1e4 });
187
- if (!isWaitForAnswerResponse(result)) throw new Error("Invalid wait_for_answer response");
188
- return result;
189
- };
190
- var cancelQuestion = async (apiKey, correlationId) => {
191
- await callMcpTool(apiKey, "cancel_question", { correlationId });
192
- };
193
- var sendNotification = async (apiKey, params, timeoutMs = 15e3) => {
194
- await callMcpTool(apiKey, "send_notification", { ...params }, { maxRetries: 3, timeoutMs });
195
- };
196
-
197
- // src/describe.ts
198
- import { isAbsolute, relative } from "path";
199
- var hookPrefixes = {
200
- Bash: (input) => `bash: ${input.command ?? "(no command)"}`,
201
- PowerShell: (input) => `powershell: ${input.command ?? "(no command)"}`,
202
- Monitor: (input) => `monitor: ${input.command ?? "(no command)"}`,
203
- Write: (input) => `write file: ${input.file_path ?? "(unknown path)"}`,
204
- Edit: (input) => `edit file: ${input.file_path ?? "(unknown path)"}`,
205
- MultiEdit: (input) => `edit file: ${input.file_path ?? "(unknown path)"}`,
206
- Read: (input) => `read file: ${input.file_path ?? "(unknown path)"}`
207
- };
208
- var describeTodoProgress = (todos) => {
209
- if (!Array.isArray(todos)) return "updated the plan";
210
- const items = todos.filter((t) => !!t && typeof t === "object");
211
- const total = items.length;
212
- if (total === 0) return "updated the plan";
213
- const done = items.filter((t) => t.status === "completed").length;
214
- const current = items.find((t) => t.status === "in_progress");
215
- const label = current ? [current.activeForm, current.content].find((v) => typeof v === "string" && v.trim().length > 0) : void 0;
216
- if (done >= total) return `finished all ${total} tasks`;
217
- if (label) return `${label.trim()} (${done}/${total} done)`;
218
- return `${done}/${total} tasks done`;
219
- };
220
- var eventPrefixes = {
221
- Bash: (input) => `ran: ${String(input.command ?? "")}`,
222
- PowerShell: (input) => `ran: ${String(input.command ?? "")}`,
223
- Monitor: (input) => `ran: ${String(input.command ?? "")}`,
224
- Write: (input) => `wrote: ${input.file_path ?? "unknown"}`,
225
- Edit: (input) => `edited: ${input.file_path ?? "unknown"}`,
226
- MultiEdit: (input) => `edited: ${input.file_path ?? "unknown"}`,
227
- Read: (input) => `read: ${input.file_path ?? "unknown"}`,
228
- TodoWrite: (input) => describeTodoProgress(input.todos)
229
- };
230
- var EVENT_ACTION_MAX = 120;
231
- var HOOK_FALLBACK_MAX = 200;
232
- var describeToolCall = (toolName, toolInput, format = "hook") => {
233
- const prefixes = format === "hook" ? hookPrefixes : eventPrefixes;
234
- const builder = prefixes[toolName];
235
- const raw = builder ? builder(toolInput) : format === "hook" ? `${toolName}: ${JSON.stringify(toolInput)}` : `${toolName}: done`;
236
- const redacted = redactSecrets(raw);
237
- if (format === "event") return redacted.slice(0, EVENT_ACTION_MAX);
238
- return builder ? redacted : redacted.slice(0, HOOK_FALLBACK_MAX);
239
- };
240
- var TOOL_TARGET_MAX_LENGTH = 80;
241
- var deriveCommandHead = (command) => {
242
- if (typeof command !== "string") return void 0;
243
- const head = command.trim().split(/\s+/).slice(0, 2).join(" ");
244
- return head ? head.slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
245
- };
246
- var deriveToolTarget = (toolName, toolInput) => {
247
- if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
248
- return deriveCommandHead(toolInput.command);
249
- }
250
- if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit") {
251
- const filePath = toolInput.file_path;
252
- if (typeof filePath !== "string") return void 0;
253
- const separator = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
254
- const base = filePath.slice(separator + 1);
255
- const dot = base.lastIndexOf(".");
256
- if (dot <= 0) return void 0;
257
- return base.slice(dot).slice(0, TOOL_TARGET_MAX_LENGTH);
258
- }
259
- return void 0;
260
- };
261
- var RECEIPT_TARGET_MAX_LENGTH = 256;
262
- var isToolResultError = (toolResult) => {
263
- try {
264
- return Boolean(toolResult && ("error" in toolResult || "is_error" in toolResult));
265
- } catch {
266
- return false;
267
- }
268
- };
269
- var relativizeReceiptPath = (filePath, cwd) => {
270
- if (!cwd || !isAbsolute(filePath)) return filePath;
271
- return relative(cwd, filePath);
272
- };
273
- var deriveReceiptMeta = (toolName, toolInput, toolResult, cwd) => {
274
- try {
275
- const ok = !isToolResultError(toolResult);
276
- if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit") {
277
- const filePath = toolInput.file_path;
278
- if (typeof filePath !== "string" || !filePath) return void 0;
279
- return {
280
- kind: toolName === "Write" ? "write" : "edit",
281
- target: relativizeReceiptPath(filePath, cwd).slice(0, RECEIPT_TARGET_MAX_LENGTH),
282
- ok
283
- };
284
- }
285
- if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
286
- const head = deriveCommandHead(toolInput.command);
287
- if (!head) return void 0;
288
- return {
289
- kind: head === "git commit" ? "commit" : "bash",
290
- target: head,
291
- ok
292
- };
293
- }
294
- return void 0;
295
- } catch {
296
- return void 0;
297
- }
298
- };
299
- var firstString = (...values) => values.find((v) => typeof v === "string" && v.length > 0);
300
- var ACTION_BODY_TRUNCATION_MARKER = "\n\u2026 [truncated]";
301
- var capActionBody = (text) => text.length <= ACTION_BODY_MAX ? text : `${text.slice(0, ACTION_BODY_MAX - ACTION_BODY_TRUNCATION_MARKER.length)}${ACTION_BODY_TRUNCATION_MARKER}`;
302
- var rawActionBody = (toolName, toolInput) => {
303
- if (toolName === "Edit" || toolName === "replace") {
304
- const before = firstString(toolInput.old_string);
305
- const after = firstString(toolInput.new_string);
306
- if (before === void 0 && after === void 0) return void 0;
307
- return `- ${before ?? ""}
308
- + ${after ?? ""}`;
309
- }
310
- if (toolName === "Write" || toolName === "write_file") return firstString(toolInput.content);
311
- if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor" || toolName === "run_shell_command" || toolName === "shell" || toolName === "apply_patch") {
312
- return firstString(toolInput.command);
313
- }
314
- return void 0;
315
- };
316
- var deriveActionBody = (toolName, toolInput) => {
317
- try {
318
- const raw = rawActionBody(toolName, toolInput);
319
- if (!raw) return void 0;
320
- return capActionBody(redactSecretsDeep(raw));
321
- } catch {
322
- return void 0;
323
- }
324
- };
325
- var deriveAction = (toolName, toolInput) => describeToolCall(toolName, toolInput, "hook").slice(0, DECISION_LINE_MAX);
326
- var BLOCKER_BY_MODE = {
327
- push_only: "Approval required before this runs",
328
- push_first: "Paused for your approval before continuing",
329
- escalate: "Escalated to you after the approval timeout"
330
- };
331
- var deriveBlocker = (mode, reason) => {
332
- const text = reason?.replace(/\s+/g, " ").trim() || BLOCKER_BY_MODE[mode] || "Waiting for your approval";
333
- return text.slice(0, DECISION_LINE_MAX);
334
- };
335
-
336
- export {
337
- getMachineId,
338
- isPolicyConfig,
339
- getPolicy,
340
- resolveAutoResolveOrigin,
341
- resolvePolicy,
342
- fetchModeState,
343
- fetchModeOverride,
344
- askUser,
345
- waitForAnswer,
346
- cancelQuestion,
347
- sendNotification,
348
- describeToolCall,
349
- deriveToolTarget,
350
- isToolResultError,
351
- deriveReceiptMeta,
352
- deriveActionBody,
353
- deriveAction,
354
- deriveBlocker
355
- };