@pushary/agent-hooks 0.21.0 → 0.21.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.
@@ -7,6 +7,7 @@ import {
7
7
  codexAllow,
8
8
  codexDeny,
9
9
  codexPass,
10
+ describeApplyPatch,
10
11
  handlePostToolUse,
11
12
  handleStop,
12
13
  handleUserPrompt,
@@ -15,7 +16,7 @@ import {
15
16
  reportEvent,
16
17
  toCodexWire,
17
18
  toPolicyLookup
18
- } from "../chunk-QY4L6XHN.js";
19
+ } from "../chunk-BYNYYZWW.js";
19
20
  import {
20
21
  DEFAULT_SESSION,
21
22
  askUser,
@@ -28,7 +29,7 @@ import {
28
29
  savePendingQuestion,
29
30
  sendNotification,
30
31
  waitForAnswer
31
- } from "../chunk-ACE77TKQ.js";
32
+ } from "../chunk-6QWFMVCD.js";
32
33
  import "../chunk-USUCPCUC.js";
33
34
  import "../chunk-DWED7BS3.js";
34
35
  import {
@@ -41,6 +42,7 @@ import { tmpdir, userInfo } from "os";
41
42
  import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "fs";
42
43
  var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
43
44
  var MAX_WAIT_SECONDS = 170;
45
+ var START_MS = Date.now();
44
46
  var APPROVAL_TTL_MS = 10 * 60 * 1e3;
45
47
  var sanitizeId = (value) => value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128);
46
48
  var userScope = () => {
@@ -104,11 +106,10 @@ var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3
104
106
  return { answered: false };
105
107
  };
106
108
  var agentNameFor = (input) => `Codex - ${basename(input.cwd ?? process.cwd())}`;
107
- var pushQuestion = async (apiKey, input, waitSeconds) => {
108
- const toolName = input.tool_name ?? "";
109
- const toolInput = input.tool_input ?? {};
109
+ var describeFor = (input, lookup) => input.tool_name === "apply_patch" ? describeApplyPatch(input.tool_input?.command) ?? describeToolCall(lookup.tool, lookup.input, "hook") : describeToolCall(lookup.tool, lookup.input, "hook");
110
+ var pushQuestion = async (apiKey, input, lookup, waitSeconds) => {
110
111
  const projectName = basename(input.cwd ?? process.cwd());
111
- const description = describeToolCall(toolName, toolInput, "hook");
112
+ const description = describeFor(input, lookup);
112
113
  const result = await askUser(apiKey, {
113
114
  question: `Allow ${description}?`,
114
115
  type: "confirm",
@@ -116,18 +117,21 @@ var pushQuestion = async (apiKey, input, waitSeconds) => {
116
117
  agentName: agentNameFor(input),
117
118
  sessionId: input.session_id,
118
119
  machineId: getMachineId(),
119
- toolName,
120
- toolTarget: deriveToolTarget(toolName, toolInput)
120
+ toolName: lookup.tool,
121
+ toolTarget: deriveToolTarget(lookup.tool, lookup.input)
121
122
  });
122
- const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
123
+ const deadline = Math.min(
124
+ Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3,
125
+ START_MS + MAX_WAIT_SECONDS * 1e3
126
+ );
123
127
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
124
128
  return { answer, correlationId: result.correlationId };
125
129
  };
126
- var notifyApprovalNeeded = async (apiKey, input) => {
130
+ var notifyApprovalNeeded = async (apiKey, input, lookup) => {
127
131
  try {
128
132
  await sendNotification(apiKey, {
129
133
  title: "Agent needs approval",
130
- body: describeToolCall(input.tool_name ?? "", input.tool_input ?? {}, "hook"),
134
+ body: describeFor(input, lookup),
131
135
  agentName: agentNameFor(input),
132
136
  sessionId: input.session_id,
133
137
  machineId: getMachineId()
@@ -140,7 +144,7 @@ var decidePermissionRequest = async (input) => {
140
144
  const apiKey = getApiKey();
141
145
  const modeState = await fetchModeState(apiKey, input.session_id);
142
146
  if (modeState.kill) return codexDeny(KILL_REASON);
143
- const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
147
+ const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
144
148
  const policy = await getPolicy(apiKey, modeState.policyVersion);
145
149
  const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
146
150
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexAllow();
@@ -149,11 +153,11 @@ var decidePermissionRequest = async (input) => {
149
153
  }
150
154
  if (toolPolicy.mode === "terminal_only") return codexPass();
151
155
  if (toolPolicy.mode === "notify_only") {
152
- if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input);
156
+ if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input, lookup);
153
157
  return codexPass();
154
158
  }
155
159
  const waitSeconds = toolPolicy.mode === "push_first" ? toolPolicy.pushFirstSeconds : toolPolicy.timeoutSeconds;
156
- const { answer, correlationId } = await pushQuestion(apiKey, input, waitSeconds);
160
+ const { answer, correlationId } = await pushQuestion(apiKey, input, lookup, waitSeconds);
157
161
  if (answer.answered) {
158
162
  if (answer.value === "yes") {
159
163
  if (toolPolicy.mode === "push_only") markApproved(input.tool_use_id);
@@ -175,7 +179,7 @@ var decidePreToolUse = async (input) => {
175
179
  const apiKey = getApiKey();
176
180
  const modeState = await fetchModeState(apiKey, input.session_id);
177
181
  if (modeState.kill) return codexDeny(KILL_REASON);
178
- const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
182
+ const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
179
183
  const policy = await getPolicy(apiKey, modeState.policyVersion);
180
184
  const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
181
185
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexPass();
@@ -186,7 +190,7 @@ var decidePreToolUse = async (input) => {
186
190
  if (consumeApproval(input.tool_use_id)) return codexPass();
187
191
  let pushed;
188
192
  try {
189
- pushed = await pushQuestion(apiKey, input, toolPolicy.timeoutSeconds);
193
+ pushed = await pushQuestion(apiKey, input, lookup, toolPolicy.timeoutSeconds);
190
194
  } catch {
191
195
  return preToolUseTimeoutDecision(toolPolicy.timeoutAction, "Push notification failed, denying per policy");
192
196
  }
@@ -198,7 +202,7 @@ var decidePreToolUse = async (input) => {
198
202
  return preToolUseTimeoutDecision(toolPolicy.timeoutAction);
199
203
  }
200
204
  if (toolPolicy.mode === "notify_only") {
201
- if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input);
205
+ if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input, lookup);
202
206
  return codexPass();
203
207
  }
204
208
  return codexPass();
@@ -244,15 +248,17 @@ var main = async () => {
244
248
  case "PreToolUse":
245
249
  emit(toCodexWire("PreToolUse", await decidePreToolUse(input)));
246
250
  break;
247
- case "PostToolUse":
251
+ case "PostToolUse": {
252
+ const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
248
253
  await handlePostToolUse({
249
- tool_name: input.tool_name ?? "",
250
- tool_input: input.tool_input ?? {},
254
+ tool_name: lookup.tool,
255
+ tool_input: lookup.input,
251
256
  tool_result: asToolResult(input.tool_response),
252
257
  cwd: input.cwd,
253
258
  session_id: input.session_id
254
259
  }, CODEX_AGENT);
255
260
  break;
261
+ }
256
262
  case "UserPromptSubmit":
257
263
  await handleUserPrompt({
258
264
  prompt: input.prompt,
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  reportEvent
4
- } from "../chunk-QY4L6XHN.js";
4
+ } from "../chunk-BYNYYZWW.js";
5
5
  import {
6
6
  askUser,
7
7
  getMachineId,
8
8
  waitForAnswer
9
- } from "../chunk-ACE77TKQ.js";
9
+ } from "../chunk-6QWFMVCD.js";
10
10
  import "../chunk-USUCPCUC.js";
11
11
  import "../chunk-DWED7BS3.js";
12
12
  import {
@@ -7,7 +7,7 @@ import {
7
7
  handleStop,
8
8
  handleUserPrompt,
9
9
  reportEvent
10
- } from "../chunk-QY4L6XHN.js";
10
+ } from "../chunk-BYNYYZWW.js";
11
11
  import {
12
12
  DEFAULT_SESSION,
13
13
  askUser,
@@ -20,7 +20,7 @@ import {
20
20
  savePendingQuestion,
21
21
  sendNotification,
22
22
  waitForAnswer
23
- } from "../chunk-ACE77TKQ.js";
23
+ } from "../chunk-6QWFMVCD.js";
24
24
  import "../chunk-USUCPCUC.js";
25
25
  import "../chunk-DWED7BS3.js";
26
26
  import {
@@ -66,6 +66,7 @@ var geminiTimeoutDecision = (timeoutAction, denyReason = "No response within tim
66
66
  // bin/pushary-gemini-hook.ts
67
67
  var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
68
68
  var MAX_WAIT_SECONDS = 170;
69
+ var START_MS = Date.now();
69
70
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
70
71
  var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
71
72
  while (Date.now() < deadlineMs) {
@@ -98,7 +99,10 @@ var pushQuestion = async (apiKey, input, lookup, waitSeconds, pollInterval = 2e3
98
99
  toolName: lookup.tool,
99
100
  toolTarget: deriveToolTarget(lookup.tool, lookup.input)
100
101
  });
101
- const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
102
+ const deadline = Math.min(
103
+ Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3,
104
+ START_MS + MAX_WAIT_SECONDS * 1e3
105
+ );
102
106
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline, pollInterval);
103
107
  return { answer, correlationId: result.correlationId };
104
108
  };
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-FXYTRZHC.js";
4
+ } from "../chunk-M5F2L4KF.js";
5
5
  import "../chunk-N7VXDBQU.js";
6
- import "../chunk-ACE77TKQ.js";
6
+ import "../chunk-6QWFMVCD.js";
7
7
  import "../chunk-USUCPCUC.js";
8
8
  import "../chunk-DWED7BS3.js";
9
9
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-QY4L6XHN.js";
5
- import "../chunk-ACE77TKQ.js";
4
+ } from "../chunk-BYNYYZWW.js";
5
+ import "../chunk-6QWFMVCD.js";
6
6
  import "../chunk-USUCPCUC.js";
7
7
  import "../chunk-DWED7BS3.js";
8
8
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleUserPrompt
4
- } from "../chunk-QY4L6XHN.js";
5
- import "../chunk-ACE77TKQ.js";
4
+ } from "../chunk-BYNYYZWW.js";
5
+ import "../chunk-6QWFMVCD.js";
6
6
  import "../chunk-USUCPCUC.js";
7
7
  import "../chunk-DWED7BS3.js";
8
8
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-QY4L6XHN.js";
5
- import "../chunk-ACE77TKQ.js";
4
+ } from "../chunk-BYNYYZWW.js";
5
+ import "../chunk-6QWFMVCD.js";
6
6
  import "../chunk-USUCPCUC.js";
7
7
  import "../chunk-DWED7BS3.js";
8
8
  import "../chunk-NKXSILEW.js";
@@ -0,0 +1,395 @@
1
+ import {
2
+ DEFAULT_SESSION,
3
+ cancelQuestion,
4
+ deriveReceiptMeta,
5
+ describeToolCall,
6
+ fetchModeState,
7
+ getMachineId,
8
+ isDefaultSession,
9
+ isPolicyConfig,
10
+ isToolResultError,
11
+ listPendingQuestions,
12
+ removePendingQuestion,
13
+ removePendingSession,
14
+ resolvePolicy
15
+ } from "./chunk-GTJGZBTQ.js";
16
+ import {
17
+ withRetry
18
+ } from "./chunk-DWED7BS3.js";
19
+ import {
20
+ getApiKey,
21
+ getBaseUrl
22
+ } from "./chunk-NKXSILEW.js";
23
+
24
+ // src/codex-adapter.ts
25
+ var CODEX_AGENT = { type: "codex", label: "Codex" };
26
+ var codexAllow = () => ({ kind: "allow" });
27
+ var codexDeny = (reason) => ({ kind: "deny", reason });
28
+ var codexPass = () => ({ kind: "pass" });
29
+ var toCodexWire = (event, decision) => {
30
+ if (event === "PermissionRequest") {
31
+ if (decision.kind === "allow") {
32
+ return {
33
+ hookSpecificOutput: {
34
+ hookEventName: "PermissionRequest",
35
+ decision: { behavior: "allow" }
36
+ }
37
+ };
38
+ }
39
+ if (decision.kind === "deny") {
40
+ return {
41
+ hookSpecificOutput: {
42
+ hookEventName: "PermissionRequest",
43
+ decision: { behavior: "deny", message: decision.reason }
44
+ }
45
+ };
46
+ }
47
+ return null;
48
+ }
49
+ if (event === "PreToolUse" && decision.kind === "deny") {
50
+ return {
51
+ hookSpecificOutput: {
52
+ hookEventName: "PreToolUse",
53
+ permissionDecision: "deny",
54
+ permissionDecisionReason: decision.reason
55
+ }
56
+ };
57
+ }
58
+ return null;
59
+ };
60
+ var parseApplyPatchFiles = (command) => {
61
+ if (typeof command !== "string") return [];
62
+ const files = [];
63
+ for (const line of command.split("\n")) {
64
+ const match = line.match(/^\*\*\*\s+(?:Add|Update|Delete) File:\s+(.+?)\s*$/);
65
+ if (match) files.push(match[1]);
66
+ }
67
+ return files;
68
+ };
69
+ var toPolicyLookup = (toolName, toolInput) => {
70
+ if (toolName !== "apply_patch") return { tool: toolName, input: toolInput };
71
+ const command = toolInput.command;
72
+ const [fromPatch] = parseApplyPatchFiles(command);
73
+ const filePath = fromPatch ?? (typeof command === "string" && command.trim() && !command.includes("\n") && !command.includes("***") ? command.trim() : void 0);
74
+ return {
75
+ tool: "Edit",
76
+ input: filePath ? { file_path: filePath } : {}
77
+ };
78
+ };
79
+ var permissionTimeoutDecision = (timeoutAction) => {
80
+ if (timeoutAction === "approve") return codexAllow();
81
+ if (timeoutAction === "deny") return codexDeny("No response within timeout");
82
+ return codexPass();
83
+ };
84
+ var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
85
+
86
+ // src/usage.ts
87
+ import { closeSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeFileSync } from "fs";
88
+ import { join } from "path";
89
+ import { tmpdir } from "os";
90
+ var DEFAULT_PRICES = [
91
+ { match: "opus-4-1", in: 15, out: 75 },
92
+ { match: "opus-4-0", in: 15, out: 75 },
93
+ { match: "3-opus", in: 15, out: 75 },
94
+ { match: "opus", in: 5, out: 25 },
95
+ { match: "haiku", in: 1, out: 5 },
96
+ { match: "sonnet", in: 3, out: 15 }
97
+ ];
98
+ var FALLBACK_PRICE = { match: "", in: 3, out: 15 };
99
+ var CACHE_WRITE_MULTIPLIER = 1.25;
100
+ var CACHE_READ_MULTIPLIER = 0.1;
101
+ var RECENT_ID_LIMIT = 200;
102
+ var READ_CHUNK_BYTES = 1024 * 1024;
103
+ var isModelPrice = (value) => {
104
+ if (!value || typeof value !== "object") return false;
105
+ const candidate = value;
106
+ return typeof candidate.match === "string" && typeof candidate.in === "number" && typeof candidate.out === "number";
107
+ };
108
+ var priceTable = () => {
109
+ const raw = process.env.PUSHARY_MODEL_PRICING?.trim();
110
+ if (!raw) return DEFAULT_PRICES;
111
+ try {
112
+ const parsed = JSON.parse(raw);
113
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed.every(isModelPrice)) return parsed;
114
+ } catch {
115
+ }
116
+ return DEFAULT_PRICES;
117
+ };
118
+ var estimateCostUsd = (usage, model) => {
119
+ const price = priceTable().find((p) => model.includes(p.match)) ?? FALLBACK_PRICE;
120
+ const perTokenIn = price.in / 1e6;
121
+ const perTokenOut = price.out / 1e6;
122
+ return usage.inputTokens * perTokenIn + usage.outputTokens * perTokenOut + usage.cacheCreationTokens * perTokenIn * CACHE_WRITE_MULTIPLIER + usage.cacheReadTokens * perTokenIn * CACHE_READ_MULTIPLIER;
123
+ };
124
+ var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join(tmpdir(), "pushary-usage");
125
+ var stateFile = (sessionId) => join(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
126
+ var emptyState = () => ({ offset: 0, tokensIn: 0, tokensOut: 0, costUsd: 0, recentIds: [] });
127
+ var readState = (path) => {
128
+ try {
129
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
130
+ if (typeof parsed.offset === "number" && parsed.offset >= 0 && typeof parsed.tokensIn === "number" && typeof parsed.tokensOut === "number" && typeof parsed.costUsd === "number" && Array.isArray(parsed.recentIds)) {
131
+ return {
132
+ offset: parsed.offset,
133
+ tokensIn: parsed.tokensIn,
134
+ tokensOut: parsed.tokensOut,
135
+ costUsd: parsed.costUsd,
136
+ recentIds: parsed.recentIds.filter((id) => typeof id === "string")
137
+ };
138
+ }
139
+ } catch {
140
+ }
141
+ return emptyState();
142
+ };
143
+ var readRange = (path, start, end) => {
144
+ const fd = openSync(path, "r");
145
+ try {
146
+ const chunks = [];
147
+ let position = start;
148
+ while (position < end) {
149
+ const length = Math.min(READ_CHUNK_BYTES, end - position);
150
+ const buffer = Buffer.alloc(length);
151
+ const bytesRead = readSync(fd, buffer, 0, length, position);
152
+ if (bytesRead <= 0) break;
153
+ chunks.push(buffer.subarray(0, bytesRead));
154
+ position += bytesRead;
155
+ }
156
+ return Buffer.concat(chunks);
157
+ } finally {
158
+ closeSync(fd);
159
+ }
160
+ };
161
+ var toCount = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
162
+ var applyLine = (state, line) => {
163
+ let parsed;
164
+ try {
165
+ parsed = JSON.parse(line);
166
+ } catch {
167
+ return 0;
168
+ }
169
+ if (parsed.type !== "assistant") return 0;
170
+ const usage = parsed.message?.usage;
171
+ if (!usage || typeof usage !== "object") return 0;
172
+ const model = typeof parsed.message?.model === "string" ? parsed.message.model : "";
173
+ if (model.includes("<synthetic>")) return 0;
174
+ const id = typeof parsed.message?.id === "string" ? parsed.message.id : null;
175
+ if (id) {
176
+ if (state.recentIds.includes(id)) return 0;
177
+ state.recentIds.push(id);
178
+ if (state.recentIds.length > RECENT_ID_LIMIT) state.recentIds.splice(0, state.recentIds.length - RECENT_ID_LIMIT);
179
+ }
180
+ const messageUsage = {
181
+ inputTokens: toCount(usage.input_tokens),
182
+ outputTokens: toCount(usage.output_tokens),
183
+ cacheCreationTokens: toCount(usage.cache_creation_input_tokens),
184
+ cacheReadTokens: toCount(usage.cache_read_input_tokens)
185
+ };
186
+ const cost = estimateCostUsd(messageUsage, model);
187
+ state.tokensIn += messageUsage.inputTokens + messageUsage.cacheCreationTokens + messageUsage.cacheReadTokens;
188
+ state.tokensOut += messageUsage.outputTokens;
189
+ state.costUsd += cost;
190
+ return cost;
191
+ };
192
+ var readNewUsage = (transcriptPath, sessionId) => {
193
+ try {
194
+ const size = statSync(transcriptPath).size;
195
+ const path = stateFile(sessionId);
196
+ let state = readState(path);
197
+ if (size < state.offset) state = { ...emptyState(), recentIds: state.recentIds };
198
+ let deltaUsd = 0;
199
+ if (size > state.offset) {
200
+ const buffer = readRange(transcriptPath, state.offset, size);
201
+ const lastNewline = buffer.lastIndexOf(10);
202
+ if (lastNewline >= 0) {
203
+ const complete = buffer.subarray(0, lastNewline + 1);
204
+ for (const line of complete.toString("utf-8").split("\n")) {
205
+ if (line.trim()) deltaUsd += applyLine(state, line);
206
+ }
207
+ state.offset += lastNewline + 1;
208
+ }
209
+ }
210
+ mkdirSync(stateDir(), { recursive: true });
211
+ writeFileSync(path, JSON.stringify(state), "utf-8");
212
+ if (state.tokensIn === 0 && state.tokensOut === 0) return null;
213
+ return {
214
+ tokensIn: state.tokensIn,
215
+ tokensOut: state.tokensOut,
216
+ costUsd: state.costUsd,
217
+ deltaUsd
218
+ };
219
+ } catch {
220
+ return null;
221
+ }
222
+ };
223
+
224
+ // src/events.ts
225
+ import { basename, join as join2 } from "path";
226
+ import { createHash } from "crypto";
227
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
228
+ import { tmpdir as tmpdir2 } from "os";
229
+ var cleanupPendingQuestions = async (sessionId) => {
230
+ try {
231
+ const files = listPendingQuestions(sessionId);
232
+ const apiKey = getApiKey();
233
+ for (const correlationId of files) {
234
+ try {
235
+ await cancelQuestion(apiKey, correlationId);
236
+ } catch {
237
+ }
238
+ removePendingQuestion(sessionId, correlationId);
239
+ }
240
+ if (!isDefaultSession(sessionId)) removePendingSession(sessionId);
241
+ } catch {
242
+ }
243
+ };
244
+ var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
245
+ var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
246
+ var readFreshCachedPolicy = (apiKey) => {
247
+ const hash = createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
248
+ const path = join2(tmpdir2(), `pushary-policy-${hash}.json`);
249
+ if (!existsSync(path)) return null;
250
+ const cached = JSON.parse(readFileSync2(path, "utf-8"));
251
+ if (!isPolicyConfig(cached)) return null;
252
+ if (!cached._cachedAt || Date.now() - cached._cachedAt >= POLICY_CACHE_TTL_MS) return null;
253
+ return cached;
254
+ };
255
+ var deriveDecisionSource = (toolName, toolInput, liveMode) => {
256
+ try {
257
+ if (liveMode.kill) return "terminal";
258
+ const policy = readFreshCachedPolicy(getApiKey());
259
+ if (!policy) return void 0;
260
+ const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput);
261
+ if (resolved.timeoutSeconds === 0 && resolved.timeoutAction === "approve") return "policy_auto";
262
+ if (resolved.mode === "push_only" || resolved.mode === "push_first") return "human";
263
+ return "terminal";
264
+ } catch {
265
+ return void 0;
266
+ }
267
+ };
268
+ var deriveUsage = (transcriptPath, sessionId) => {
269
+ if (!transcriptPath || process.env.PUSHARY_COST_TRACKING === "off") return void 0;
270
+ try {
271
+ return readNewUsage(transcriptPath, sessionId || DEFAULT_SESSION) ?? void 0;
272
+ } catch {
273
+ return void 0;
274
+ }
275
+ };
276
+ var reportEvent = async (event, options = {}) => {
277
+ const apiKey = getApiKey();
278
+ const baseUrl = getBaseUrl();
279
+ return withRetry(async () => {
280
+ const res = await fetch(`${baseUrl}/api/agent/event`, {
281
+ method: "POST",
282
+ headers: {
283
+ "Content-Type": "application/json",
284
+ "Authorization": `Bearer ${apiKey}`
285
+ },
286
+ body: JSON.stringify({
287
+ ...event,
288
+ machineId: event.machineId ?? getMachineId()
289
+ }),
290
+ signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
291
+ });
292
+ try {
293
+ return await res.json();
294
+ } catch {
295
+ return null;
296
+ }
297
+ }, { maxAttempts: options.maxAttempts ?? 2, baseDelayMs: 300 });
298
+ };
299
+ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
300
+ try {
301
+ const projectName = basename(input.cwd ?? process.cwd());
302
+ const action = describeToolCall(input.tool_name, input.tool_input, "event");
303
+ const lookup = toPolicyLookup(input.tool_name, input.tool_input);
304
+ const isError = isToolResultError(input.tool_result);
305
+ const receiptsEnabled = process.env.PUSHARY_RECEIPTS !== "off";
306
+ const liveMode = await fetchModeState(getApiKey(), input.session_id);
307
+ await Promise.allSettled([
308
+ cleanupPendingQuestions(input.session_id || DEFAULT_SESSION),
309
+ reportEvent({
310
+ event: isError ? "tool_error" : "tool_complete",
311
+ agentType: agent.type,
312
+ agentName: `${agent.label} - ${projectName}`,
313
+ action,
314
+ sessionId: input.session_id,
315
+ error: isError ? String(input.tool_result?.error ?? input.tool_result?.stderr ?? "").slice(0, 500) : void 0,
316
+ decisionSource: deriveDecisionSource(lookup.tool, lookup.input, liveMode),
317
+ meta: receiptsEnabled ? deriveReceiptMeta(lookup.tool, lookup.input, input.tool_result, input.cwd ?? process.cwd()) : void 0,
318
+ usage: deriveUsage(input.transcript_path, input.session_id)
319
+ })
320
+ ]);
321
+ } catch {
322
+ }
323
+ };
324
+ var TASK_TITLE_MAX_LENGTH = 120;
325
+ var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
326
+ try {
327
+ const projectName = basename(input.cwd ?? process.cwd());
328
+ const titlesEnabled = process.env.PUSHARY_TASK_TITLES !== "off";
329
+ const taskTitle = titlesEnabled ? input.prompt?.replace(/\s+/g, " ").trim().slice(0, TASK_TITLE_MAX_LENGTH) || void 0 : void 0;
330
+ await reportEvent({
331
+ event: "user_prompt",
332
+ agentType: agent.type,
333
+ agentName: `${agent.label} - ${projectName}`,
334
+ sessionId: input.session_id,
335
+ taskTitle
336
+ }, { maxAttempts: 1, timeoutMs: 800 });
337
+ } catch {
338
+ }
339
+ };
340
+ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
341
+ try {
342
+ const projectName = basename(input.cwd ?? process.cwd());
343
+ const [, reported] = await Promise.allSettled([
344
+ cleanupPendingQuestions(input.session_id || DEFAULT_SESSION),
345
+ reportEvent({
346
+ event: "session_end",
347
+ agentType: agent.type,
348
+ agentName: `${agent.label} - ${projectName}`,
349
+ action: "Session ended",
350
+ sessionId: input.session_id,
351
+ usage: deriveUsage(input.transcript_path, input.session_id)
352
+ })
353
+ ]);
354
+ const pendingCommand = reported.status === "fulfilled" ? reported.value?.pendingCommand : void 0;
355
+ if (typeof pendingCommand === "string" && pendingCommand.trim().length > 0) {
356
+ return {
357
+ decision: "block",
358
+ reason: `The user sent a new instruction from their phone via Pushary: ${pendingCommand.trim()}`
359
+ };
360
+ }
361
+ return void 0;
362
+ } catch {
363
+ return void 0;
364
+ }
365
+ };
366
+ var handleNotification = async (input) => {
367
+ try {
368
+ const projectName = basename(input.cwd ?? process.cwd());
369
+ await reportEvent({
370
+ event: input.type === "error" ? "error" : "notification",
371
+ agentType: "claude_code",
372
+ agentName: `Claude Code - ${projectName}`,
373
+ action: input.title ?? input.message ?? "Notification",
374
+ sessionId: input.session_id,
375
+ error: input.type === "error" ? input.message : void 0
376
+ });
377
+ } catch {
378
+ }
379
+ };
380
+
381
+ export {
382
+ CODEX_AGENT,
383
+ codexAllow,
384
+ codexDeny,
385
+ codexPass,
386
+ toCodexWire,
387
+ toPolicyLookup,
388
+ permissionTimeoutDecision,
389
+ preToolUseTimeoutDecision,
390
+ reportEvent,
391
+ handlePostToolUse,
392
+ handleUserPrompt,
393
+ handleStop,
394
+ handleNotification
395
+ };