@pushary/agent-hooks 0.68.0 → 0.69.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.
@@ -0,0 +1,64 @@
1
+ import {
2
+ isModeStateDegraded,
3
+ resolveAutoResolveOrigin,
4
+ resolvePolicyAcross
5
+ } from "./chunk-H46LSQRT.js";
6
+ import {
7
+ evaluateScope,
8
+ scopeChangeReason,
9
+ scopeRequiresHuman
10
+ } from "./chunk-DQAN3JQP.js";
11
+
12
+ // src/answer.ts
13
+ var denyReasonFrom = (value, note) => {
14
+ const trimmed = note?.trim();
15
+ if (trimmed) return `Denied from your phone: ${trimmed}`;
16
+ return value && value !== "no" && value !== "yes" ? `Denied from your phone: ${value}` : "Denied via push notification";
17
+ };
18
+ var isDeferAnswer = (value) => value === "defer";
19
+
20
+ // src/gate.ts
21
+ var KILL_REASON = "Stopped by user, this agent was halted from Pushary";
22
+ var strictestScopeVerdict = (scope, paths) => {
23
+ if (paths.length === 0) return evaluateScope(scope, void 0);
24
+ let worst = { outcome: "in_scope" };
25
+ for (const path of paths) {
26
+ const verdict = evaluateScope(scope, path);
27
+ if (verdict.outcome === "off_limits") return verdict;
28
+ if (verdict.outcome === "out_of_scope") worst = verdict;
29
+ }
30
+ return worst;
31
+ };
32
+ var survivesDegraded = (config, toolName, toolInputs, cwd, repoKey) => toolInputs.length === 1 && resolveAutoResolveOrigin(config, toolName, toolInputs[0], cwd, repoKey) === "safe_readonly";
33
+ var resolveGate = (input) => {
34
+ const { modeState, config, toolName, toolInputs, cwd, repoKey } = input;
35
+ if (modeState.kill) return { kind: "kill", reason: KILL_REASON };
36
+ const policy = resolvePolicyAcross(config, toolName, modeState.mode, toolInputs, cwd, repoKey);
37
+ const autoApproves = policy.timeoutSeconds === 0 && policy.timeoutAction === "approve";
38
+ const autoDenies = policy.timeoutSeconds === 0 && policy.timeoutAction === "deny";
39
+ if (autoDenies) return { kind: "deny", policy, reason: `Denied by policy for ${policy.tool}` };
40
+ if (isModeStateDegraded(modeState)) {
41
+ if (autoApproves) {
42
+ return survivesDegraded(config, toolName, toolInputs, cwd, repoKey) ? { kind: "allow", policy } : { kind: "defer", policy };
43
+ }
44
+ return { kind: "gate", policy };
45
+ }
46
+ const scopeVerdict = modeState.scope ? strictestScopeVerdict(modeState.scope, input.scopePaths ?? []) : { outcome: "no_contract" };
47
+ if (autoApproves) {
48
+ if (!scopeRequiresHuman(scopeVerdict)) return { kind: "allow", policy };
49
+ }
50
+ const scopeReason = scopeChangeReason(scopeVerdict);
51
+ return {
52
+ kind: "gate",
53
+ policy,
54
+ scopeReason,
55
+ scopePath: scopeReason && "path" in scopeVerdict ? scopeVerdict.path : void 0
56
+ };
57
+ };
58
+
59
+ export {
60
+ denyReasonFrom,
61
+ isDeferAnswer,
62
+ KILL_REASON,
63
+ resolveGate
64
+ };
@@ -3,8 +3,9 @@ import {
3
3
  } from "./chunk-CAJZAFVS.js";
4
4
  import {
5
5
  denyReasonFrom,
6
- isDeferAnswer
7
- } from "./chunk-YHG74UFF.js";
6
+ isDeferAnswer,
7
+ resolveGate
8
+ } from "./chunk-3USMXNVB.js";
8
9
  import {
9
10
  DEFAULT_SESSION,
10
11
  askUser,
@@ -19,13 +20,12 @@ import {
19
20
  readLastPrompt,
20
21
  readLastUserPrompt,
21
22
  repoKeyFor,
22
- resolvePolicy,
23
23
  savePendingQuestion,
24
24
  scopePathFor,
25
25
  sendNotification,
26
26
  throttlePass,
27
27
  waitForAnswer
28
- } from "./chunk-XOUYM27W.js";
28
+ } from "./chunk-H46LSQRT.js";
29
29
  import {
30
30
  isGatingMoment,
31
31
  recordKeylessMoment
@@ -33,11 +33,8 @@ import {
33
33
  import {
34
34
  buildDecisionEpisodeFeatures,
35
35
  effectiveWaitSeconds,
36
- evaluateScope,
37
36
  hookWaitClamped,
38
- hookWaitDeadline,
39
- scopeChangeReason,
40
- scopeRequiresHuman
37
+ hookWaitDeadline
41
38
  } from "./chunk-DQAN3JQP.js";
42
39
  import {
43
40
  getMachineId
@@ -383,6 +380,18 @@ var handleKeyless = (input) => {
383
380
  }
384
381
  return void 0;
385
382
  };
383
+ var claudeGateInput = (modeState, config, input) => {
384
+ const scopePath = modeState.scope ? scopePathFor(input.tool_name, input.tool_input, input.cwd) : void 0;
385
+ return {
386
+ modeState,
387
+ config,
388
+ toolName: input.tool_name,
389
+ toolInputs: [input.tool_input ?? {}],
390
+ scopePaths: scopePath ? [scopePath] : [],
391
+ cwd: input.cwd,
392
+ repoKey: repoKeyFor(input.cwd)
393
+ };
394
+ };
386
395
  var handlePreToolUse = async (input) => {
387
396
  if (input.tool_name.startsWith("mcp__pushary__")) return void 0;
388
397
  let apiKey;
@@ -394,26 +403,26 @@ var handlePreToolUse = async (input) => {
394
403
  try {
395
404
  const modeState = await fetchModeState(apiKey, input.session_id);
396
405
  const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true);
397
- if (modeState.kill) {
398
- return deny("Stopped by user, this agent was halted from Pushary");
399
- }
406
+ const verdict = resolveGate(claudeGateInput(modeState, policy, input));
407
+ if (verdict.kind === "kill") return deny(verdict.reason);
400
408
  if (input.tool_name === "AskUserQuestion") {
401
409
  return handleAskUserQuestion(apiKey, input);
402
410
  }
403
411
  if (shouldDeferToNativeMode(input.permission_mode, input.tool_name)) {
404
412
  return void 0;
405
413
  }
406
- const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input, input.cwd, repoKeyFor(input.cwd));
407
- const scopePath = modeState.scope ? scopePathFor(input.tool_name, input.tool_input, input.cwd) : void 0;
408
- const scopeVerdict = evaluateScope(modeState.scope, scopePath);
409
- const scopeReason = scopeChangeReason(scopeVerdict);
410
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") {
411
- if (!scopeRequiresHuman(scopeVerdict)) return allow();
412
- }
413
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
414
- return deny(`Denied by policy for ${toolPolicy.tool}`);
414
+ switch (verdict.kind) {
415
+ case "allow":
416
+ return allow();
417
+ case "deny":
418
+ return deny(verdict.reason);
419
+ // State we could not establish, with an auto-approve on the table. No
420
+ // opinion, so Claude Code's own flow runs as if the hook were absent.
421
+ case "defer":
422
+ return void 0;
423
+ default:
424
+ return dispatchModeHandler(apiKey, input, verdict.policy, getMachineId(), modeState.trainingConsent, verdict.scopeReason, verdict.scopePath);
415
425
  }
416
- return dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent, scopeReason, scopePath);
417
426
  } catch {
418
427
  return void 0;
419
428
  }
@@ -452,13 +461,22 @@ var handlePermissionRequest = async (input) => {
452
461
  try {
453
462
  const modeState = await fetchModeState(apiKey, input.session_id);
454
463
  const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true);
455
- if (modeState.kill) return permReqDeny("Stopped by user \u2014 this agent was halted from Pushary");
456
- const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input, input.cwd, repoKeyFor(input.cwd));
457
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return permReqAllow();
458
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
459
- return permReqDeny(`Denied by policy for ${toolPolicy.tool}`);
464
+ const verdict = resolveGate(claudeGateInput(modeState, policy, input));
465
+ switch (verdict.kind) {
466
+ case "kill":
467
+ return permReqDeny(verdict.reason);
468
+ case "allow":
469
+ return permReqAllow();
470
+ case "deny":
471
+ return permReqDeny(verdict.reason);
472
+ // No opinion, so Claude Code's own dialog shows.
473
+ case "defer":
474
+ return void 0;
475
+ default:
476
+ return toPermissionRequestOutput(
477
+ await dispatchModeHandler(apiKey, input, verdict.policy, getMachineId(), modeState.trainingConsent, verdict.scopeReason, verdict.scopePath)
478
+ );
460
479
  }
461
- return toPermissionRequestOutput(await dispatchModeHandler(apiKey, input, toolPolicy, getMachineId(), modeState.trainingConsent));
462
480
  } catch {
463
481
  return void 0;
464
482
  }
@@ -54,6 +54,7 @@ var isWaitForAnswerResponse = (data) => {
54
54
 
55
55
  // src/policy.ts
56
56
  var CACHE_TTL_MS = 60 * 1e3;
57
+ var STALE_CACHE_MAX_MS = 12 * 60 * 60 * 1e3;
57
58
  var policyCacheFile = (apiKey, agent, repoAware = false) => {
58
59
  const parts = [apiKey, agent, repoAware ? "repoAware" : void 0].filter(Boolean);
59
60
  const hash = createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 12);
@@ -88,18 +89,19 @@ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false) => {
88
89
  const cached = JSON.parse(stat);
89
90
  if (!isPolicyConfig(cached)) throw new Error("Corrupted cache");
90
91
  const versionStale = expectedVersion != null && (cached._policyVersion ?? null) !== expectedVersion;
91
- const ttlFresh = !cached._cachedAt || Date.now() - cached._cachedAt < CACHE_TTL_MS;
92
+ const age = cached._cachedAt ? Date.now() - cached._cachedAt : 0;
93
+ const ttlFresh = !cached._cachedAt || age < CACHE_TTL_MS;
92
94
  if (ttlFresh && !versionStale) {
93
95
  return cached;
94
96
  }
95
- staleCache = cached;
97
+ if (age < STALE_CACHE_MAX_MS) staleCache = cached;
96
98
  } catch {
97
99
  }
98
100
  }
99
101
  try {
100
102
  const policy = await fetchPolicy(apiKey, agent, repoAware);
101
103
  try {
102
- writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), "utf-8");
104
+ writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), { encoding: "utf-8", mode: 384 });
103
105
  } catch {
104
106
  }
105
107
  return policy;
@@ -178,6 +180,57 @@ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey) =>
178
180
  }
179
181
  return base;
180
182
  };
183
+ var restraintRank = (policy) => {
184
+ if (policy.timeoutSeconds === 0 && policy.timeoutAction === "deny") return 4;
185
+ if (policy.timeoutSeconds === 0 && policy.timeoutAction === "approve") return 0;
186
+ if (policy.mode === "push_only") return 3;
187
+ return policy.mode === "terminal_only" || policy.mode === "notify_only" ? 1 : 2;
188
+ };
189
+ var strictestPolicy = (a, b) => restraintRank(b) > restraintRank(a) ? b : a;
190
+ var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey) => {
191
+ if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey);
192
+ return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey)).reduce(strictestPolicy);
193
+ };
194
+ var isModeStateDegraded = (state) => state?.degraded === true;
195
+ var MODE_GRACE_MS = 60 * 1e3;
196
+ var modeStateCacheFile = (apiKey) => {
197
+ const hash = createHash("sha256").update(`mode:${apiKey}`).digest("hex").slice(0, 12);
198
+ return join(tmpdir(), `pushary-mode-${hash}.json`);
199
+ };
200
+ var readLastGoodMode = (apiKey) => {
201
+ try {
202
+ const path = modeStateCacheFile(apiKey);
203
+ if (!existsSync(path)) return null;
204
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
205
+ if (!parsed || typeof parsed.at !== "number" || !parsed.state) return null;
206
+ return parsed;
207
+ } catch {
208
+ return null;
209
+ }
210
+ };
211
+ var writeLastGoodMode = (apiKey, state) => {
212
+ try {
213
+ writeFileSync(modeStateCacheFile(apiKey), JSON.stringify({ state, at: Date.now() }), { encoding: "utf-8", mode: 384 });
214
+ } catch {
215
+ }
216
+ };
217
+ var unknownMode = () => ({
218
+ mode: null,
219
+ kill: false,
220
+ policyVersion: null,
221
+ relayUrl: null,
222
+ trainingConsent: false,
223
+ scope: null,
224
+ degraded: true
225
+ });
226
+ var degradedFallback = (apiKey, authoritative = false) => {
227
+ if (authoritative) return unknownMode();
228
+ const last = readLastGoodMode(apiKey);
229
+ if (last && Date.now() - last.at < MODE_GRACE_MS) {
230
+ return { ...last.state, degraded: false };
231
+ }
232
+ return unknownMode();
233
+ };
181
234
  var toPolicyVersion = (value) => typeof value === "string" || typeof value === "number" ? String(value) : null;
182
235
  var fetchModeState = async (apiKey, sessionId) => {
183
236
  try {
@@ -187,10 +240,12 @@ var fetchModeState = async (apiKey, sessionId) => {
187
240
  headers: { "Authorization": `Bearer ${apiKey}` },
188
241
  signal: AbortSignal.timeout(3e3)
189
242
  });
190
- if (!response.ok) return { mode: null, kill: false, policyVersion: null, relayUrl: null, trainingConsent: false, scope: null };
243
+ if (!response.ok) {
244
+ return degradedFallback(apiKey, response.status === 401 || response.status === 403);
245
+ }
191
246
  const data = await response.json();
192
247
  const mode = data.override?.mode;
193
- return {
248
+ const state = {
194
249
  mode: isApprovalMode(mode) ? mode : null,
195
250
  kill: data.kill === true,
196
251
  policyVersion: toPolicyVersion(data.policyVersion),
@@ -198,10 +253,13 @@ var fetchModeState = async (apiKey, sessionId) => {
198
253
  trainingConsent: data.trainingConsent === true,
199
254
  // Validated rather than cast: a malformed contract must read as no contract,
200
255
  // never as one that silently matches nothing.
201
- scope: isScopeContract(data.scope) ? data.scope : null
256
+ scope: isScopeContract(data.scope) ? data.scope : null,
257
+ degraded: false
202
258
  };
259
+ writeLastGoodMode(apiKey, state);
260
+ return state;
203
261
  } catch {
204
- return { mode: null, kill: false, policyVersion: null, relayUrl: null, trainingConsent: false, scope: null };
262
+ return degradedFallback(apiKey);
205
263
  }
206
264
  };
207
265
  var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
@@ -275,7 +333,7 @@ var TOOL_TARGET_MAX_LENGTH = 80;
275
333
  var deriveCommandHead = (command) => {
276
334
  if (typeof command !== "string") return void 0;
277
335
  const head = command.trim().split(/\s+/).slice(0, 2).join(" ");
278
- return head ? head.slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
336
+ return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
279
337
  };
280
338
  var deriveToolTarget = (toolName, toolInput) => {
281
339
  if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
@@ -300,7 +358,7 @@ var deriveReceiptCommandHead = (command) => {
300
358
  const [first, second] = tokens;
301
359
  const keep = first && second && SCRIPT_RUNNERS.has(first.toLowerCase()) && RUN_SUBCOMMANDS.has(second.toLowerCase()) ? 3 : 2;
302
360
  const head = tokens.slice(0, keep).join(" ");
303
- return head ? head.slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
361
+ return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
304
362
  };
305
363
  var RECEIPT_TARGET_MAX_LENGTH = 256;
306
364
  var EXIT_CODE_FAILURE = /^Error: Exit code \d+/;
@@ -335,7 +393,9 @@ var deriveReceiptMeta = (toolName, toolInput, toolResult, cwd) => {
335
393
  if (typeof filePath !== "string" || !filePath) return void 0;
336
394
  return {
337
395
  kind: toolName === "Write" ? "write" : "edit",
338
- target: relativizeReceiptPath(filePath, cwd).slice(0, RECEIPT_TARGET_MAX_LENGTH),
396
+ // A path is an unlikely place for a credential but not an impossible one
397
+ // (a token in a temp directory name), and this field is persisted.
398
+ target: redactSecrets(relativizeReceiptPath(filePath, cwd)).slice(0, RECEIPT_TARGET_MAX_LENGTH),
339
399
  ok
340
400
  };
341
401
  }
@@ -441,9 +501,93 @@ var removePendingSession = (sessionId) => {
441
501
  }
442
502
  };
443
503
 
504
+ // src/repo.ts
505
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
506
+ import { basename, dirname, isAbsolute as isAbsolute2, join as join3, resolve } from "path";
507
+ var MAX_PARENT_WALK = 64;
508
+ var GIT_FILE_PREFIX = "gitdir:";
509
+ var findGitDir = (startDir) => {
510
+ let current = resolve(startDir);
511
+ for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
512
+ const candidate = join3(current, ".git");
513
+ try {
514
+ if (existsSync3(candidate)) {
515
+ if (statSync2(candidate).isDirectory()) return candidate;
516
+ const pointer = readFileSync2(candidate, "utf-8").trim();
517
+ if (pointer.startsWith(GIT_FILE_PREFIX)) {
518
+ const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
519
+ return isAbsolute2(target) ? target : resolve(current, target);
520
+ }
521
+ }
522
+ } catch {
523
+ }
524
+ const parent = dirname(current);
525
+ if (parent === current) return void 0;
526
+ current = parent;
527
+ }
528
+ return void 0;
529
+ };
530
+ var configPathFor = (gitDir) => {
531
+ const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
532
+ return index === -1 ? join3(gitDir, "config") : join3(gitDir.slice(0, index), "config");
533
+ };
534
+ var ORIGIN_SECTION = /^\[remote "origin"\]/;
535
+ var SECTION_HEADER = /^\[/;
536
+ var URL_LINE = /^url\s*=\s*(.+)$/;
537
+ var readOriginRemote = (gitDir) => {
538
+ try {
539
+ const configPath = configPathFor(gitDir);
540
+ if (!existsSync3(configPath)) return void 0;
541
+ let inOrigin = false;
542
+ for (const rawLine of readFileSync2(configPath, "utf-8").split("\n")) {
543
+ const line = rawLine.trim();
544
+ if (SECTION_HEADER.test(line)) {
545
+ inOrigin = ORIGIN_SECTION.test(line);
546
+ continue;
547
+ }
548
+ if (!inOrigin) continue;
549
+ const url = URL_LINE.exec(line);
550
+ if (url) return url[1]?.trim();
551
+ }
552
+ } catch {
553
+ }
554
+ return void 0;
555
+ };
556
+ var deriveRepoKey = (cwd) => {
557
+ const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
558
+ try {
559
+ const gitDir = findGitDir(dir);
560
+ if (gitDir) {
561
+ const remote = readOriginRemote(gitDir);
562
+ const normalized = remote ? normalizeRepoRemote(remote) : void 0;
563
+ if (normalized) return normalized;
564
+ const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
565
+ return localRepoKey(basename(root));
566
+ }
567
+ return localRepoKey(basename(resolve(dir)));
568
+ } catch {
569
+ return void 0;
570
+ }
571
+ };
572
+ var repoKeyCache = /* @__PURE__ */ new Map();
573
+ var REPO_KEY_ENV = "PUSHARY_REPO_KEY";
574
+ var repoKeyFor = (cwd) => {
575
+ const override = process.env[REPO_KEY_ENV];
576
+ if (typeof override === "string") {
577
+ const trimmed = override.trim();
578
+ if (trimmed.toLowerCase() === "off") return void 0;
579
+ if (trimmed.length > 0) return trimmed.toLowerCase();
580
+ }
581
+ const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
582
+ if (repoKeyCache.has(dir)) return repoKeyCache.get(dir);
583
+ const derived = deriveRepoKey(dir);
584
+ repoKeyCache.set(dir, derived);
585
+ return derived;
586
+ };
587
+
444
588
  // src/usage.ts
445
- import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, readSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
446
- import { join as join3 } from "path";
589
+ import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, readSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
590
+ import { join as join4 } from "path";
447
591
  import { tmpdir as tmpdir3 } from "os";
448
592
  var DEFAULT_PRICES = [
449
593
  { match: "opus-4-1", in: 15, out: 75 },
@@ -479,12 +623,12 @@ var estimateCostUsd = (usage, model) => {
479
623
  const perTokenOut = price.out / 1e6;
480
624
  return usage.inputTokens * perTokenIn + usage.outputTokens * perTokenOut + usage.cacheCreationTokens * perTokenIn * CACHE_WRITE_MULTIPLIER + usage.cacheReadTokens * perTokenIn * CACHE_READ_MULTIPLIER;
481
625
  };
482
- var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join3(tmpdir3(), "pushary-usage");
483
- var stateFile = (sessionId) => join3(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
626
+ var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join4(tmpdir3(), "pushary-usage");
627
+ var stateFile = (sessionId) => join4(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
484
628
  var emptyState = () => ({ offset: 0, tokensIn: 0, tokensOut: 0, costUsd: 0, recentIds: [] });
485
629
  var readState = (path) => {
486
630
  try {
487
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
631
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
488
632
  if (typeof parsed.offset === "number" && parsed.offset >= 0 && typeof parsed.tokensIn === "number" && typeof parsed.tokensOut === "number" && typeof parsed.costUsd === "number" && Array.isArray(parsed.recentIds)) {
489
633
  return {
490
634
  offset: parsed.offset,
@@ -559,7 +703,7 @@ var extractUserText = (content) => {
559
703
  };
560
704
  var readLastUserPrompt = (transcriptPath) => {
561
705
  try {
562
- const size = statSync2(transcriptPath).size;
706
+ const size = statSync3(transcriptPath).size;
563
707
  const start = Math.max(0, size - USER_PROMPT_TAIL_BYTES);
564
708
  const lines = readRange(transcriptPath, start, size).toString("utf-8").split("\n");
565
709
  for (let i = lines.length - 1; i >= 0; i -= 1) {
@@ -582,7 +726,7 @@ var readLastUserPrompt = (transcriptPath) => {
582
726
  };
583
727
  var readNewUsage = (transcriptPath, sessionId) => {
584
728
  try {
585
- const size = statSync2(transcriptPath).size;
729
+ const size = statSync3(transcriptPath).size;
586
730
  const path = stateFile(sessionId);
587
731
  let state = readState(path);
588
732
  if (size < state.offset) state = { ...emptyState(), recentIds: state.recentIds };
@@ -613,7 +757,7 @@ var readNewUsage = (transcriptPath, sessionId) => {
613
757
  };
614
758
 
615
759
  // src/codex-adapter.ts
616
- import { resolve } from "path";
760
+ import { resolve as resolve2 } from "path";
617
761
  var CODEX_AGENT = { type: "codex", label: "Codex" };
618
762
  var codexAllow = () => ({ kind: "allow" });
619
763
  var codexDeny = (reason) => ({ kind: "deny", reason });
@@ -679,10 +823,25 @@ var toPolicyLookup = (toolName, toolInput, cwd) => {
679
823
  const command = toolInput.command;
680
824
  const files = parseApplyPatchFiles(command);
681
825
  if (files.length === 1) {
682
- return { tool: "Edit", input: { file_path: cwd ? resolve(cwd, files[0]) : files[0] } };
826
+ return { tool: "Edit", input: { file_path: cwd ? resolve2(cwd, files[0]) : files[0] } };
683
827
  }
684
828
  return { tool: "Edit", input: typeof command === "string" ? { file_path: command } : {} };
685
829
  };
830
+ var toPolicyLookups = (toolName, toolInput, cwd) => {
831
+ if (toolName !== "apply_patch") {
832
+ const single = toPolicyLookup(toolName, toolInput, cwd);
833
+ return { tool: single.tool, inputs: [single.input] };
834
+ }
835
+ const files = parseApplyPatchFiles(toolInput.command);
836
+ if (files.length === 0) {
837
+ const single = toPolicyLookup(toolName, toolInput, cwd);
838
+ return { tool: single.tool, inputs: [single.input] };
839
+ }
840
+ return {
841
+ tool: "Edit",
842
+ inputs: files.map((file) => ({ file_path: cwd ? resolve2(cwd, file) : file }))
843
+ };
844
+ };
686
845
  var permissionTimeoutDecision = (timeoutAction) => {
687
846
  if (timeoutAction === "approve") return codexAllow();
688
847
  if (timeoutAction === "deny") return codexDeny("No response within timeout");
@@ -690,90 +849,6 @@ var permissionTimeoutDecision = (timeoutAction) => {
690
849
  };
691
850
  var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
692
851
 
693
- // src/repo.ts
694
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
695
- import { basename, dirname, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "path";
696
- var MAX_PARENT_WALK = 64;
697
- var GIT_FILE_PREFIX = "gitdir:";
698
- var findGitDir = (startDir) => {
699
- let current = resolve2(startDir);
700
- for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
701
- const candidate = join4(current, ".git");
702
- try {
703
- if (existsSync3(candidate)) {
704
- if (statSync3(candidate).isDirectory()) return candidate;
705
- const pointer = readFileSync3(candidate, "utf-8").trim();
706
- if (pointer.startsWith(GIT_FILE_PREFIX)) {
707
- const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
708
- return isAbsolute2(target) ? target : resolve2(current, target);
709
- }
710
- }
711
- } catch {
712
- }
713
- const parent = dirname(current);
714
- if (parent === current) return void 0;
715
- current = parent;
716
- }
717
- return void 0;
718
- };
719
- var configPathFor = (gitDir) => {
720
- const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
721
- return index === -1 ? join4(gitDir, "config") : join4(gitDir.slice(0, index), "config");
722
- };
723
- var ORIGIN_SECTION = /^\[remote "origin"\]/;
724
- var SECTION_HEADER = /^\[/;
725
- var URL_LINE = /^url\s*=\s*(.+)$/;
726
- var readOriginRemote = (gitDir) => {
727
- try {
728
- const configPath = configPathFor(gitDir);
729
- if (!existsSync3(configPath)) return void 0;
730
- let inOrigin = false;
731
- for (const rawLine of readFileSync3(configPath, "utf-8").split("\n")) {
732
- const line = rawLine.trim();
733
- if (SECTION_HEADER.test(line)) {
734
- inOrigin = ORIGIN_SECTION.test(line);
735
- continue;
736
- }
737
- if (!inOrigin) continue;
738
- const url = URL_LINE.exec(line);
739
- if (url) return url[1]?.trim();
740
- }
741
- } catch {
742
- }
743
- return void 0;
744
- };
745
- var deriveRepoKey = (cwd) => {
746
- const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
747
- try {
748
- const gitDir = findGitDir(dir);
749
- if (gitDir) {
750
- const remote = readOriginRemote(gitDir);
751
- const normalized = remote ? normalizeRepoRemote(remote) : void 0;
752
- if (normalized) return normalized;
753
- const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
754
- return localRepoKey(basename(root));
755
- }
756
- return localRepoKey(basename(resolve2(dir)));
757
- } catch {
758
- return void 0;
759
- }
760
- };
761
- var repoKeyCache = /* @__PURE__ */ new Map();
762
- var REPO_KEY_ENV = "PUSHARY_REPO_KEY";
763
- var repoKeyFor = (cwd) => {
764
- const override = process.env[REPO_KEY_ENV];
765
- if (typeof override === "string") {
766
- const trimmed = override.trim();
767
- if (trimmed.toLowerCase() === "off") return void 0;
768
- if (trimmed.length > 0) return trimmed.toLowerCase();
769
- }
770
- const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
771
- if (repoKeyCache.has(dir)) return repoKeyCache.get(dir);
772
- const derived = deriveRepoKey(dir);
773
- repoKeyCache.set(dir, derived);
774
- return derived;
775
- };
776
-
777
852
  // src/throttle.ts
778
853
  import { join as join5 } from "path";
779
854
  import { tmpdir as tmpdir4 } from "os";
@@ -1235,7 +1310,10 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
1235
1310
 
1236
1311
  export {
1237
1312
  getPolicy,
1313
+ resolveAutoResolveOrigin,
1238
1314
  resolvePolicy,
1315
+ resolvePolicyAcross,
1316
+ isModeStateDegraded,
1239
1317
  fetchModeState,
1240
1318
  fetchModeOverride,
1241
1319
  askUser,
@@ -1260,6 +1338,7 @@ export {
1260
1338
  toCodexWire,
1261
1339
  describeApplyPatch,
1262
1340
  toPolicyLookup,
1341
+ toPolicyLookups,
1263
1342
  permissionTimeoutDecision,
1264
1343
  preToolUseTimeoutDecision,
1265
1344
  readLastPrompt,
@@ -320,6 +320,8 @@ export {
320
320
  isPluginRegistered,
321
321
  pluginLocationSnippet,
322
322
  vscodeSettingsTargets,
323
+ detectAgent,
324
+ agentProbes,
323
325
  detectAllAgents,
324
326
  isDetected,
325
327
  shortenHome,
@@ -533,6 +533,7 @@ var cancelPairing = async (pairId) => {
533
533
  } catch {
534
534
  }
535
535
  };
536
+ var PAIR_MAX_CONSECUTIVE_FAILURES = 8;
536
537
  var claimPairing = async (pairId) => {
537
538
  try {
538
539
  const res = await fetch(`${apiBase()}/api/mobile/pair/claim?id=${encodeURIComponent(pairId)}`, {
@@ -544,6 +545,17 @@ var claimPairing = async (pairId) => {
544
545
  return null;
545
546
  }
546
547
  };
548
+ var ackPairing = async (pairId) => {
549
+ try {
550
+ await fetch(`${apiBase()}/api/mobile/pair/ack`, {
551
+ method: "POST",
552
+ headers: { "Content-Type": "application/json" },
553
+ body: JSON.stringify({ pairId }),
554
+ signal: AbortSignal.timeout(5e3)
555
+ });
556
+ } catch {
557
+ }
558
+ };
547
559
  var buildPairLinks = (pairId, publicKeyB64, baseUrl = apiBase()) => {
548
560
  const id = encodeURIComponent(pairId);
549
561
  const pk = encodeURIComponent(publicKeyB64);
@@ -593,10 +605,22 @@ var connectViaAppPairing = async (options = {}) => {
593
605
  void Promise.allSettled([Promise.resolve(flushed), cancelPairing(pairId)]).finally(() => process.exit(EXIT_ABORTED));
594
606
  };
595
607
  process.once("SIGINT", onInterrupt);
608
+ let consecutiveFailures = 0;
596
609
  try {
597
610
  while (Date.now() < deadline) {
598
611
  if (cancelled) return null;
599
612
  const claim = await claimPairing(pairId);
613
+ if (claim === null) {
614
+ consecutiveFailures += 1;
615
+ if (consecutiveFailures >= PAIR_MAX_CONSECUTIVE_FAILURES) {
616
+ stop(yellow("!"), "Can't reach pushary.com to finish pairing. Check your connection, then re-run setup.");
617
+ await cancelPairing(pairId);
618
+ return null;
619
+ }
620
+ await sleep(PAIR_POLL_INTERVAL_MS);
621
+ continue;
622
+ }
623
+ consecutiveFailures = 0;
600
624
  if (claim?.status === "authorized") {
601
625
  let apiKey;
602
626
  try {
@@ -606,6 +630,7 @@ var connectViaAppPairing = async (options = {}) => {
606
630
  await cancelPairing(pairId);
607
631
  return null;
608
632
  }
633
+ await ackPairing(pairId);
609
634
  stop(check, "App connected");
610
635
  return { apiKey, handle: claim.siteSlug };
611
636
  }