@pushary/agent-hooks 0.54.1 → 0.56.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.
@@ -13,17 +13,18 @@ import {
13
13
  describeToolCall,
14
14
  fetchModeState,
15
15
  getPolicy,
16
+ repoKeyFor,
16
17
  reportEvent,
17
18
  resolvePolicy,
18
19
  waitForAnswer
19
- } from "../chunk-SWNBIHDX.js";
20
+ } from "../chunk-3EOK3M44.js";
20
21
  import {
21
22
  getMachineId
22
23
  } from "../chunk-RN3NOEJF.js";
23
24
  import "../chunk-DWED7BS3.js";
24
25
  import {
25
26
  redactSecrets
26
- } from "../chunk-SY3NBF67.js";
27
+ } from "../chunk-SYAAGYLZ.js";
27
28
  import {
28
29
  getApiKey,
29
30
  getBaseUrl
@@ -528,8 +529,8 @@ var createRemoteApprover = (deps) => {
528
529
  const sessionId = deps.getSessionId?.();
529
530
  const modeState = await fetchModeState2(deps.apiKey, sessionId);
530
531
  if (modeState.kill) return deny("Stopped by user \u2014 this agent was halted from Pushary");
531
- const policyConfig = await getPolicy2(deps.apiKey, modeState.policyVersion, "claude_code");
532
- const policy = resolvePolicy2(policyConfig, toolName, modeState.mode, input);
532
+ const policyConfig = await getPolicy2(deps.apiKey, modeState.policyVersion, "claude_code", true);
533
+ const policy = resolvePolicy2(policyConfig, toolName, modeState.mode, input, deps.cwd, repoKeyFor(deps.cwd));
533
534
  if (policy.timeoutSeconds === 0 && policy.timeoutAction === "approve") return allow(input);
534
535
  if (policy.timeoutSeconds === 0 && policy.timeoutAction === "deny") {
535
536
  return deny(`Denied by policy for ${policy.tool}`);
@@ -1341,6 +1342,7 @@ var runDualMode = async (binary, args2, startingMode) => {
1341
1342
  apiKey,
1342
1343
  machineId,
1343
1344
  projectName,
1345
+ cwd: process.cwd(),
1344
1346
  getSessionId: () => sessionId
1345
1347
  });
1346
1348
  const initialModeState = await fetchModeState(apiKey).catch(() => null);
@@ -6,15 +6,15 @@ import {
6
6
  removeCodexHooks,
7
7
  removeGeminiSettings,
8
8
  removeInstructionBlock
9
- } from "../chunk-PZWZ77QH.js";
9
+ } from "../chunk-GSERIC6Q.js";
10
10
  import {
11
11
  execNpm
12
12
  } from "../chunk-J7JWI3KU.js";
13
13
  import {
14
14
  removeClaudeMcpServers,
15
15
  removePusharySettings
16
- } from "../chunk-T2EEBR2D.js";
17
- import "../chunk-SY3NBF67.js";
16
+ } from "../chunk-QYQO6RHJ.js";
17
+ import "../chunk-SYAAGYLZ.js";
18
18
 
19
19
  // bin/pushary-clean.ts
20
20
  import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync } from "fs";
@@ -31,21 +31,21 @@ import {
31
31
  toCodexWire,
32
32
  toPolicyLookup,
33
33
  waitForAnswer
34
- } from "../chunk-SWNBIHDX.js";
34
+ } from "../chunk-3EOK3M44.js";
35
35
  import {
36
36
  getMachineId
37
37
  } from "../chunk-RN3NOEJF.js";
38
38
  import {
39
39
  isGatingMoment,
40
40
  recordKeylessMoment
41
- } from "../chunk-WYQSLB7K.js";
41
+ } from "../chunk-5TECIV6T.js";
42
42
  import "../chunk-DWED7BS3.js";
43
43
  import {
44
44
  DECISION_LINE_MAX,
45
45
  effectiveWaitSeconds,
46
46
  hookWaitClamped,
47
47
  hookWaitDeadline
48
- } from "../chunk-SY3NBF67.js";
48
+ } from "../chunk-SYAAGYLZ.js";
49
49
  import {
50
50
  getApiKey
51
51
  } from "../chunk-NKXSILEW.js";
@@ -3,12 +3,12 @@ import {
3
3
  askUser,
4
4
  reportEvent,
5
5
  waitForAnswer
6
- } from "../chunk-SWNBIHDX.js";
6
+ } from "../chunk-3EOK3M44.js";
7
7
  import {
8
8
  getMachineId
9
9
  } from "../chunk-RN3NOEJF.js";
10
10
  import "../chunk-DWED7BS3.js";
11
- import "../chunk-SY3NBF67.js";
11
+ import "../chunk-SYAAGYLZ.js";
12
12
  import {
13
13
  getApiKey
14
14
  } from "../chunk-NKXSILEW.js";
@@ -11,18 +11,18 @@ import {
11
11
  missingGeminiHookEvents,
12
12
  readCodexMcpAuth,
13
13
  untrustedCodexHookEvents
14
- } from "../chunk-PZWZ77QH.js";
14
+ } from "../chunk-GSERIC6Q.js";
15
15
  import {
16
16
  execNpm
17
17
  } from "../chunk-J7JWI3KU.js";
18
18
  import {
19
19
  readLedgerSummary
20
- } from "../chunk-WYQSLB7K.js";
20
+ } from "../chunk-5TECIV6T.js";
21
21
  import {
22
22
  callMcpTool,
23
23
  sendMcpRequest
24
24
  } from "../chunk-DWED7BS3.js";
25
- import "../chunk-SY3NBF67.js";
25
+ import "../chunk-SYAAGYLZ.js";
26
26
  import {
27
27
  getBaseUrl
28
28
  } from "../chunk-NKXSILEW.js";
@@ -23,21 +23,21 @@ import {
23
23
  savePendingQuestion,
24
24
  sendNotification,
25
25
  waitForAnswer
26
- } from "../chunk-SWNBIHDX.js";
26
+ } from "../chunk-3EOK3M44.js";
27
27
  import {
28
28
  getMachineId
29
29
  } from "../chunk-RN3NOEJF.js";
30
30
  import {
31
31
  isGatingMoment,
32
32
  recordKeylessMoment
33
- } from "../chunk-WYQSLB7K.js";
33
+ } from "../chunk-5TECIV6T.js";
34
34
  import "../chunk-DWED7BS3.js";
35
35
  import {
36
36
  DECISION_LINE_MAX,
37
37
  effectiveWaitSeconds,
38
38
  hookWaitClamped,
39
39
  hookWaitDeadline
40
- } from "../chunk-SY3NBF67.js";
40
+ } from "../chunk-SYAAGYLZ.js";
41
41
  import {
42
42
  getApiKey
43
43
  } from "../chunk-NKXSILEW.js";
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-SVRDT5PP.js";
5
- import "../chunk-T2EEBR2D.js";
4
+ } from "../chunk-GQPIOJA3.js";
5
+ import "../chunk-QYQO6RHJ.js";
6
6
  import "../chunk-YHG74UFF.js";
7
- import "../chunk-SWNBIHDX.js";
7
+ import "../chunk-3EOK3M44.js";
8
8
  import "../chunk-RN3NOEJF.js";
9
- import "../chunk-WYQSLB7K.js";
9
+ import "../chunk-5TECIV6T.js";
10
10
  import "../chunk-DWED7BS3.js";
11
- import "../chunk-SY3NBF67.js";
11
+ import "../chunk-SYAAGYLZ.js";
12
12
  import "../chunk-NKXSILEW.js";
13
13
 
14
14
  // bin/pushary-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleNotification
4
- } from "../chunk-SWNBIHDX.js";
4
+ } from "../chunk-3EOK3M44.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-SY3NBF67.js";
7
+ import "../chunk-SYAAGYLZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-notification-hook.ts
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionDenied
4
- } from "../chunk-SVRDT5PP.js";
5
- import "../chunk-T2EEBR2D.js";
4
+ } from "../chunk-GQPIOJA3.js";
5
+ import "../chunk-QYQO6RHJ.js";
6
6
  import "../chunk-YHG74UFF.js";
7
- import "../chunk-SWNBIHDX.js";
7
+ import "../chunk-3EOK3M44.js";
8
8
  import "../chunk-RN3NOEJF.js";
9
- import "../chunk-WYQSLB7K.js";
9
+ import "../chunk-5TECIV6T.js";
10
10
  import "../chunk-DWED7BS3.js";
11
- import "../chunk-SY3NBF67.js";
11
+ import "../chunk-SYAAGYLZ.js";
12
12
  import "../chunk-NKXSILEW.js";
13
13
 
14
14
  // bin/pushary-permission-denied-hook.ts
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionRequest
4
- } from "../chunk-SVRDT5PP.js";
5
- import "../chunk-T2EEBR2D.js";
4
+ } from "../chunk-GQPIOJA3.js";
5
+ import "../chunk-QYQO6RHJ.js";
6
6
  import "../chunk-YHG74UFF.js";
7
- import "../chunk-SWNBIHDX.js";
7
+ import "../chunk-3EOK3M44.js";
8
8
  import "../chunk-RN3NOEJF.js";
9
- import "../chunk-WYQSLB7K.js";
9
+ import "../chunk-5TECIV6T.js";
10
10
  import "../chunk-DWED7BS3.js";
11
- import "../chunk-SY3NBF67.js";
11
+ import "../chunk-SYAAGYLZ.js";
12
12
  import "../chunk-NKXSILEW.js";
13
13
 
14
14
  // bin/pushary-permission-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-SWNBIHDX.js";
4
+ } from "../chunk-3EOK3M44.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-SY3NBF67.js";
7
+ import "../chunk-SYAAGYLZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-post-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleUserPrompt
4
- } from "../chunk-SWNBIHDX.js";
4
+ } from "../chunk-3EOK3M44.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-SY3NBF67.js";
7
+ import "../chunk-SYAAGYLZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-prompt-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionEnd
4
- } from "../chunk-SWNBIHDX.js";
4
+ } from "../chunk-3EOK3M44.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-SY3NBF67.js";
7
+ import "../chunk-SYAAGYLZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-session-end-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionStart
4
- } from "../chunk-SWNBIHDX.js";
4
+ } from "../chunk-3EOK3M44.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-SY3NBF67.js";
7
+ import "../chunk-SYAAGYLZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-session-start-hook.ts
@@ -13,7 +13,7 @@ import {
13
13
  renderAgentInstructions,
14
14
  renderProjectAgentInstructions,
15
15
  writeInstructionBlock
16
- } from "../chunk-PZWZ77QH.js";
16
+ } from "../chunk-GSERIC6Q.js";
17
17
  import {
18
18
  execNpm,
19
19
  npmErrorMessage,
@@ -25,15 +25,15 @@ import {
25
25
  addClaudeMcpServer,
26
26
  addPusharyHooks,
27
27
  addPusharyToolPermissions
28
- } from "../chunk-T2EEBR2D.js";
28
+ } from "../chunk-QYQO6RHJ.js";
29
29
  import {
30
30
  reportEvent
31
- } from "../chunk-SWNBIHDX.js";
31
+ } from "../chunk-3EOK3M44.js";
32
32
  import "../chunk-RN3NOEJF.js";
33
33
  import "../chunk-DWED7BS3.js";
34
34
  import {
35
35
  isValidApiKey
36
- } from "../chunk-SY3NBF67.js";
36
+ } from "../chunk-SYAAGYLZ.js";
37
37
  import "../chunk-NKXSILEW.js";
38
38
 
39
39
  // bin/pushary-setup.ts
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readLedgerSummary
4
- } from "../chunk-WYQSLB7K.js";
5
- import "../chunk-SY3NBF67.js";
4
+ } from "../chunk-5TECIV6T.js";
5
+ import "../chunk-SYAAGYLZ.js";
6
6
  import {
7
7
  getApiKey
8
8
  } from "../chunk-NKXSILEW.js";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-SWNBIHDX.js";
4
+ } from "../chunk-3EOK3M44.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-SY3NBF67.js";
7
+ import "../chunk-SYAAGYLZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-stop-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStopFailure
4
- } from "../chunk-SWNBIHDX.js";
4
+ } from "../chunk-3EOK3M44.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-SY3NBF67.js";
7
+ import "../chunk-SYAAGYLZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-stopfailure-hook.ts
@@ -8,8 +8,8 @@ import {
8
8
  addClaudeMcpServer,
9
9
  addPusharyHooks,
10
10
  addPusharyToolPermissions
11
- } from "../chunk-T2EEBR2D.js";
12
- import "../chunk-SY3NBF67.js";
11
+ } from "../chunk-QYQO6RHJ.js";
12
+ import "../chunk-SYAAGYLZ.js";
13
13
  import {
14
14
  getApiKey
15
15
  } from "../chunk-NKXSILEW.js";
@@ -9,14 +9,17 @@ import {
9
9
  ACTION_BODY_MAX,
10
10
  DECISION_LINE_MAX,
11
11
  classifyDecisionRisk,
12
- extractPolicyArg,
13
12
  isApprovalMode,
13
+ isRepoKeyMatch,
14
14
  isSafeReadOnlyCommand,
15
+ localRepoKey,
15
16
  matchRankWeight,
16
- matchToolPattern,
17
+ matchToolPatternAny,
18
+ normalizeRepoRemote,
19
+ policyArgForms,
17
20
  redactSecrets,
18
21
  redactSecretsDeep
19
- } from "./chunk-SY3NBF67.js";
22
+ } from "./chunk-SYAAGYLZ.js";
20
23
  import {
21
24
  getApiKey,
22
25
  getBaseUrl
@@ -47,14 +50,19 @@ var isWaitForAnswerResponse = (data) => {
47
50
 
48
51
  // src/policy.ts
49
52
  var CACHE_TTL_MS = 60 * 1e3;
50
- var cacheFile = (apiKey, agent) => {
51
- const hash = createHash("sha256").update(agent ? `${apiKey}:${agent}` : apiKey).digest("hex").slice(0, 12);
53
+ var policyCacheFile = (apiKey, agent, repoAware = false) => {
54
+ const parts = [apiKey, agent, repoAware ? "repoAware" : void 0].filter(Boolean);
55
+ const hash = createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 12);
52
56
  return join(tmpdir(), `pushary-policy-${hash}.json`);
53
57
  };
54
- var fetchPolicy = async (apiKey, agent) => {
58
+ var fetchPolicy = async (apiKey, agent, repoAware = false) => {
55
59
  return withRetry(async () => {
56
60
  const baseUrl = getBaseUrl();
57
- const url = agent ? `${baseUrl}/api/mcp/policy?agent=${encodeURIComponent(agent)}` : `${baseUrl}/api/mcp/policy`;
61
+ const params = new URLSearchParams();
62
+ if (agent) params.set("agent", agent);
63
+ if (repoAware) params.set("repoAware", "1");
64
+ const query = params.toString();
65
+ const url = query ? `${baseUrl}/api/mcp/policy?${query}` : `${baseUrl}/api/mcp/policy`;
58
66
  const response = await fetch(url, {
59
67
  headers: { "Authorization": `Bearer ${apiKey}` },
60
68
  signal: AbortSignal.timeout(1e4)
@@ -67,8 +75,8 @@ var fetchPolicy = async (apiKey, agent) => {
67
75
  return raw;
68
76
  }, { maxAttempts: 2 });
69
77
  };
70
- var getPolicy = async (apiKey, expectedVersion, agent) => {
71
- const path = cacheFile(apiKey, agent);
78
+ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false) => {
79
+ const path = policyCacheFile(apiKey, agent, repoAware);
72
80
  let staleCache = null;
73
81
  if (existsSync(path)) {
74
82
  try {
@@ -85,7 +93,7 @@ var getPolicy = async (apiKey, expectedVersion, agent) => {
85
93
  }
86
94
  }
87
95
  try {
88
- const policy = await fetchPolicy(apiKey, agent);
96
+ const policy = await fetchPolicy(apiKey, agent, repoAware);
89
97
  try {
90
98
  writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), "utf-8");
91
99
  } catch {
@@ -96,18 +104,23 @@ var getPolicy = async (apiKey, expectedVersion, agent) => {
96
104
  throw new Error("Failed to fetch policy and no cached policy available");
97
105
  }
98
106
  };
99
- var findBestMatch = (policies, toolName, arg) => {
107
+ var findBestMatch = (policies, toolName, args, repoKey) => {
100
108
  let best;
101
109
  let bestWeight = 0;
110
+ let bestScoped = -1;
102
111
  let bestLength = -1;
103
112
  for (const candidate of policies) {
104
- const rank = matchToolPattern(candidate.tool, toolName, arg);
113
+ if (!isRepoKeyMatch(candidate.repoKey, repoKey)) continue;
114
+ const rank = matchToolPatternAny(candidate.tool, toolName, args);
105
115
  if (rank === "none") continue;
106
116
  const weight = matchRankWeight(rank);
117
+ const scoped = candidate.repoKey ? 1 : 0;
107
118
  const length = rank === "prefix" ? candidate.tool.length : -1;
108
- if (weight > bestWeight || weight === bestWeight && length > bestLength) {
119
+ const better = weight > bestWeight || weight === bestWeight && scoped > bestScoped || weight === bestWeight && scoped === bestScoped && length > bestLength;
120
+ if (better) {
109
121
  best = { policy: candidate, rank };
110
122
  bestWeight = weight;
123
+ bestScoped = scoped;
111
124
  bestLength = length;
112
125
  }
113
126
  }
@@ -120,18 +133,20 @@ var autoApprove = (tool) => ({
120
133
  mode: "terminal_only",
121
134
  pushFirstSeconds: 0
122
135
  });
123
- var resolveAutoResolveOrigin = (config, toolName, toolInput) => {
124
- const arg = toolInput ? extractPolicyArg(toolName, toolInput) : void 0;
125
- const match = findBestMatch(config.policies, toolName, arg);
136
+ var resolveAutoResolveOrigin = (config, toolName, toolInput, cwd, repoKey) => {
137
+ const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
138
+ const arg = args[0];
139
+ const match = findBestMatch(config.policies, toolName, args, repoKey);
126
140
  const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
127
141
  if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
128
142
  return "safe_readonly";
129
143
  }
130
144
  return "policy_timeout";
131
145
  };
132
- var resolvePolicy = (config, toolName, modeOverride, toolInput) => {
133
- const arg = toolInput ? extractPolicyArg(toolName, toolInput) : void 0;
134
- const match = findBestMatch(config.policies, toolName, arg);
146
+ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey) => {
147
+ const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
148
+ const arg = args[0];
149
+ const match = findBestMatch(config.policies, toolName, args, repoKey);
135
150
  let base = match?.policy ?? config.policies.find((p) => p.tool === "*") ?? {
136
151
  tool: toolName,
137
152
  timeoutSeconds: config.defaultTimeoutSeconds,
@@ -639,17 +654,101 @@ var permissionTimeoutDecision = (timeoutAction) => {
639
654
  };
640
655
  var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
641
656
 
657
+ // src/repo.ts
658
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
659
+ import { basename, dirname, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "path";
660
+ var MAX_PARENT_WALK = 64;
661
+ var GIT_FILE_PREFIX = "gitdir:";
662
+ var findGitDir = (startDir) => {
663
+ let current = resolve2(startDir);
664
+ for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
665
+ const candidate = join4(current, ".git");
666
+ try {
667
+ if (existsSync3(candidate)) {
668
+ if (statSync3(candidate).isDirectory()) return candidate;
669
+ const pointer = readFileSync3(candidate, "utf-8").trim();
670
+ if (pointer.startsWith(GIT_FILE_PREFIX)) {
671
+ const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
672
+ return isAbsolute2(target) ? target : resolve2(current, target);
673
+ }
674
+ }
675
+ } catch {
676
+ }
677
+ const parent = dirname(current);
678
+ if (parent === current) return void 0;
679
+ current = parent;
680
+ }
681
+ return void 0;
682
+ };
683
+ var configPathFor = (gitDir) => {
684
+ const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
685
+ return index === -1 ? join4(gitDir, "config") : join4(gitDir.slice(0, index), "config");
686
+ };
687
+ var ORIGIN_SECTION = /^\[remote "origin"\]/;
688
+ var SECTION_HEADER = /^\[/;
689
+ var URL_LINE = /^url\s*=\s*(.+)$/;
690
+ var readOriginRemote = (gitDir) => {
691
+ try {
692
+ const configPath = configPathFor(gitDir);
693
+ if (!existsSync3(configPath)) return void 0;
694
+ let inOrigin = false;
695
+ for (const rawLine of readFileSync3(configPath, "utf-8").split("\n")) {
696
+ const line = rawLine.trim();
697
+ if (SECTION_HEADER.test(line)) {
698
+ inOrigin = ORIGIN_SECTION.test(line);
699
+ continue;
700
+ }
701
+ if (!inOrigin) continue;
702
+ const url = URL_LINE.exec(line);
703
+ if (url) return url[1]?.trim();
704
+ }
705
+ } catch {
706
+ }
707
+ return void 0;
708
+ };
709
+ var deriveRepoKey = (cwd) => {
710
+ const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
711
+ try {
712
+ const gitDir = findGitDir(dir);
713
+ if (gitDir) {
714
+ const remote = readOriginRemote(gitDir);
715
+ const normalized = remote ? normalizeRepoRemote(remote) : void 0;
716
+ if (normalized) return normalized;
717
+ const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
718
+ return localRepoKey(basename(root));
719
+ }
720
+ return localRepoKey(basename(resolve2(dir)));
721
+ } catch {
722
+ return void 0;
723
+ }
724
+ };
725
+ var repoKeyCache = /* @__PURE__ */ new Map();
726
+ var REPO_KEY_ENV = "PUSHARY_REPO_KEY";
727
+ var repoKeyFor = (cwd) => {
728
+ const override = process.env[REPO_KEY_ENV];
729
+ if (typeof override === "string") {
730
+ const trimmed = override.trim();
731
+ if (trimmed.toLowerCase() === "off") return void 0;
732
+ if (trimmed.length > 0) return trimmed.toLowerCase();
733
+ }
734
+ const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
735
+ if (repoKeyCache.has(dir)) return repoKeyCache.get(dir);
736
+ const derived = deriveRepoKey(dir);
737
+ repoKeyCache.set(dir, derived);
738
+ return derived;
739
+ };
740
+
642
741
  // src/throttle.ts
643
- import { join as join4 } from "path";
742
+ import { join as join5 } from "path";
644
743
  import { tmpdir as tmpdir4 } from "os";
645
744
  import { createHash as createHash2 } from "crypto";
646
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
647
- var THROTTLE_DIR = join4(tmpdir4(), "pushary-throttle");
648
- var markerPath = (key) => join4(THROTTLE_DIR, createHash2("sha256").update(key).digest("hex").slice(0, 16));
745
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
746
+ var THROTTLE_DIR = join5(tmpdir4(), "pushary-throttle");
747
+ var markerPath = (key) => join5(THROTTLE_DIR, createHash2("sha256").update(key).digest("hex").slice(0, 16));
649
748
  var throttlePass = (key, windowMs) => {
650
749
  try {
651
750
  const path = markerPath(key);
652
- if (existsSync3(path) && Date.now() - statSync3(path).mtimeMs < windowMs) return false;
751
+ if (existsSync4(path) && Date.now() - statSync4(path).mtimeMs < windowMs) return false;
653
752
  mkdirSync3(THROTTLE_DIR, { recursive: true });
654
753
  writeFileSync4(path, "", "utf-8");
655
754
  return true;
@@ -659,15 +758,14 @@ var throttlePass = (key, windowMs) => {
659
758
  };
660
759
 
661
760
  // src/events.ts
662
- import { basename, join as join5 } from "path";
663
- import { createHash as createHash3 } from "crypto";
664
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
761
+ import { basename as basename2, join as join6 } from "path";
762
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync5 } from "fs";
665
763
  import { tmpdir as tmpdir5 } from "os";
666
- var INTENT_DIR = join5(tmpdir5(), "pushary-intent");
764
+ var INTENT_DIR = join6(tmpdir5(), "pushary-intent");
667
765
  var INTENT_MAX_BYTES = 2048;
668
766
  var INTENT_GRACE_MS = 10 * 60 * 1e3;
669
767
  var sanitizeSession = (sessionId) => sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || DEFAULT_SESSION;
670
- var intentFile = (sessionId) => join5(INTENT_DIR, sanitizeSession(sessionId));
768
+ var intentFile = (sessionId) => join6(INTENT_DIR, sanitizeSession(sessionId));
671
769
  var saveLastPrompt = (sessionId, prompt) => {
672
770
  try {
673
771
  const trimmed = prompt.replace(/\s+/g, " ").trim().slice(0, INTENT_MAX_BYTES);
@@ -680,8 +778,8 @@ var saveLastPrompt = (sessionId, prompt) => {
680
778
  var readLastPrompt = (sessionId) => {
681
779
  try {
682
780
  const path = intentFile(sessionId);
683
- if (isDefaultSession(sessionId) && statSync4(path).mtimeMs < Date.now() - INTENT_GRACE_MS) return void 0;
684
- const value = readFileSync3(path, "utf-8").trim();
781
+ if (isDefaultSession(sessionId) && statSync5(path).mtimeMs < Date.now() - INTENT_GRACE_MS) return void 0;
782
+ const value = readFileSync4(path, "utf-8").trim();
685
783
  return value.length > 0 ? value : void 0;
686
784
  } catch {
687
785
  return void 0;
@@ -737,20 +835,19 @@ var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
737
835
  var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
738
836
  var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
739
837
  var readFreshCachedPolicy = (apiKey) => {
740
- const hash = createHash3("sha256").update(apiKey).digest("hex").slice(0, 12);
741
- const path = join5(tmpdir5(), `pushary-policy-${hash}.json`);
742
- if (!existsSync4(path)) return null;
743
- const cached = JSON.parse(readFileSync3(path, "utf-8"));
838
+ const path = policyCacheFile(apiKey);
839
+ if (!existsSync5(path)) return null;
840
+ const cached = JSON.parse(readFileSync4(path, "utf-8"));
744
841
  if (!isPolicyConfig(cached)) return null;
745
842
  if (!cached._cachedAt || Date.now() - cached._cachedAt >= POLICY_CACHE_TTL_MS) return null;
746
843
  return cached;
747
844
  };
748
- var deriveDecisionSource = (toolName, toolInput, liveMode) => {
845
+ var deriveDecisionSource = (toolName, toolInput, liveMode, cwd) => {
749
846
  try {
750
847
  if (liveMode.kill) return "terminal";
751
848
  const policy = readFreshCachedPolicy(getApiKey());
752
849
  if (!policy) return void 0;
753
- const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput);
850
+ const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd));
754
851
  if (resolved.timeoutSeconds === 0 && resolved.timeoutAction === "approve") return "policy_auto";
755
852
  if (resolved.mode === "push_only" || resolved.mode === "push_first") return "human";
756
853
  return "terminal";
@@ -758,15 +855,15 @@ var deriveDecisionSource = (toolName, toolInput, liveMode) => {
758
855
  return void 0;
759
856
  }
760
857
  };
761
- var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode) => {
858
+ var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode, cwd) => {
762
859
  try {
763
860
  if (liveMode.kill) return void 0;
764
861
  const policy = readFreshCachedPolicy(getApiKey());
765
862
  if (!policy) return void 0;
766
- const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput);
863
+ const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd));
767
864
  if (resolved.timeoutSeconds !== 0 || resolved.timeoutAction !== "approve") return void 0;
768
865
  return {
769
- decisionOrigin: resolveAutoResolveOrigin(policy, toolName, toolInput),
866
+ decisionOrigin: resolveAutoResolveOrigin(policy, toolName, toolInput, cwd, repoKeyFor(cwd)),
770
867
  toolName,
771
868
  toolTarget: deriveToolTarget(toolName, toolInput)
772
869
  };
@@ -807,6 +904,7 @@ var reportEvent = async (event, options = {}) => {
807
904
  body: JSON.stringify({
808
905
  ...event,
809
906
  machineId: event.machineId ?? getMachineId(),
907
+ repoKey: event.repoKey ?? repoKeyFor(),
810
908
  installMode: detectInstallMode(),
811
909
  // Link-back for a phone-spawned session: forward the spawn id the daemon
812
910
  // set in the environment so the server can map spawnId -> sessionId.
@@ -831,7 +929,7 @@ var reportEvent = async (event, options = {}) => {
831
929
  var NON_GATED_MIRROR_TOOLS = /* @__PURE__ */ new Set(["TodoWrite"]);
832
930
  var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
833
931
  try {
834
- const projectName = basename(input.cwd ?? process.cwd());
932
+ const projectName = basename2(input.cwd ?? process.cwd());
835
933
  const action = describeToolCall(input.tool_name, input.tool_input, "event");
836
934
  const lookup = toPolicyLookup(input.tool_name, input.tool_input);
837
935
  const toolResult = input.tool_response ?? input.tool_result;
@@ -846,8 +944,8 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
846
944
  for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
847
945
  }
848
946
  const isMirrorOnly = NON_GATED_MIRROR_TOOLS.has(input.tool_name);
849
- const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode);
850
- const beacon = !isMirrorOnly && decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode) : void 0;
947
+ const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode, input.cwd);
948
+ const beacon = !isMirrorOnly && decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode, input.cwd) : void 0;
851
949
  const toolReport = reportEvent({
852
950
  event: isError ? "tool_error" : "tool_complete",
853
951
  agentType: agent.type,
@@ -886,7 +984,7 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
886
984
  var TASK_TITLE_MAX_LENGTH = 120;
887
985
  var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
888
986
  try {
889
- const projectName = basename(input.cwd ?? process.cwd());
987
+ const projectName = basename2(input.cwd ?? process.cwd());
890
988
  const sessionKey = input.session_id || DEFAULT_SESSION;
891
989
  const titlesEnabled = process.env.PUSHARY_TASK_TITLES !== "off";
892
990
  const taskTitle = titlesEnabled ? input.prompt?.replace(/\s+/g, " ").trim().slice(0, TASK_TITLE_MAX_LENGTH) || void 0 : void 0;
@@ -931,7 +1029,7 @@ var summarizeStop = (input) => {
931
1029
  };
932
1030
  var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
933
1031
  try {
934
- const projectName = basename(input.cwd ?? process.cwd());
1032
+ const projectName = basename2(input.cwd ?? process.cwd());
935
1033
  const sessionKey = input.session_id || DEFAULT_SESSION;
936
1034
  const late = await reconcilePendingQuestions(sessionKey);
937
1035
  const tasks = [
@@ -967,7 +1065,7 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
967
1065
  };
968
1066
  var handleSessionStart = async (input, agent = CLAUDE_CODE_AGENT) => {
969
1067
  try {
970
- const projectName = basename(input.cwd ?? process.cwd());
1068
+ const projectName = basename2(input.cwd ?? process.cwd());
971
1069
  const action = input.source === "resume" ? "Session resumed" : "Session started";
972
1070
  await reportEvent({
973
1071
  event: "session_start",
@@ -989,7 +1087,7 @@ var SESSION_END_ACTIONS = {
989
1087
  };
990
1088
  var handleSessionEnd = async (input, agent = CLAUDE_CODE_AGENT) => {
991
1089
  try {
992
- const projectName = basename(input.cwd ?? process.cwd());
1090
+ const projectName = basename2(input.cwd ?? process.cwd());
993
1091
  const action = typeof input.reason === "string" && SESSION_END_ACTIONS[input.reason] || "Session ended";
994
1092
  const sessionKey = input.session_id || DEFAULT_SESSION;
995
1093
  const late = await reconcilePendingQuestions(sessionKey);
@@ -1013,7 +1111,7 @@ var NOTIFICATION_PUSH_TYPES = /* @__PURE__ */ new Set(["idle_prompt", "agent_nee
1013
1111
  var NOTIFICATION_THROTTLE_MS = 60 * 1e3;
1014
1112
  var handleNotification = async (input, agent = CLAUDE_CODE_AGENT) => {
1015
1113
  try {
1016
- const projectName = basename(input.cwd ?? process.cwd());
1114
+ const projectName = basename2(input.cwd ?? process.cwd());
1017
1115
  const notifType = input.notification_type ?? input.type ?? "";
1018
1116
  const sessionKey = input.session_id || DEFAULT_SESSION;
1019
1117
  if (NOTIFICATION_PUSH_TYPES.has(notifType)) {
@@ -1083,7 +1181,7 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
1083
1181
  } catch {
1084
1182
  return;
1085
1183
  }
1086
- const projectName = basename(input.cwd ?? process.cwd());
1184
+ const projectName = basename2(input.cwd ?? process.cwd());
1087
1185
  const mode = await fetchModeState(apiKey, input.session_id).catch(() => null);
1088
1186
  if (mode?.kill || mode?.mode === "terminal_only") return;
1089
1187
  const reason = describeStopFailure(input.error);
@@ -1115,6 +1213,7 @@ export {
1115
1213
  deriveBlocker,
1116
1214
  DEFAULT_SESSION,
1117
1215
  savePendingQuestion,
1216
+ repoKeyFor,
1118
1217
  throttlePass,
1119
1218
  readLastUserPrompt,
1120
1219
  CODEX_AGENT,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isSafeReadOnlyCommand
3
- } from "./chunk-SY3NBF67.js";
3
+ } from "./chunk-SYAAGYLZ.js";
4
4
 
5
5
  // src/ledger.ts
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from "fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PRETOOLUSE_HANDLED_TOOLS
3
- } from "./chunk-T2EEBR2D.js";
3
+ } from "./chunk-QYQO6RHJ.js";
4
4
  import {
5
5
  denyReasonFrom,
6
6
  isDeferAnswer
@@ -18,25 +18,26 @@ import {
18
18
  getPolicy,
19
19
  readLastPrompt,
20
20
  readLastUserPrompt,
21
+ repoKeyFor,
21
22
  resolvePolicy,
22
23
  savePendingQuestion,
23
24
  sendNotification,
24
25
  throttlePass,
25
26
  waitForAnswer
26
- } from "./chunk-SWNBIHDX.js";
27
+ } from "./chunk-3EOK3M44.js";
27
28
  import {
28
29
  getMachineId
29
30
  } from "./chunk-RN3NOEJF.js";
30
31
  import {
31
32
  isGatingMoment,
32
33
  recordKeylessMoment
33
- } from "./chunk-WYQSLB7K.js";
34
+ } from "./chunk-5TECIV6T.js";
34
35
  import {
35
36
  buildDecisionEpisodeFeatures,
36
37
  effectiveWaitSeconds,
37
38
  hookWaitClamped,
38
39
  hookWaitDeadline
39
- } from "./chunk-SY3NBF67.js";
40
+ } from "./chunk-SYAAGYLZ.js";
40
41
  import {
41
42
  getApiKey,
42
43
  getBaseUrl
@@ -385,7 +386,7 @@ var handlePreToolUse = async (input) => {
385
386
  }
386
387
  try {
387
388
  const modeState = await fetchModeState(apiKey, input.session_id);
388
- const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code");
389
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true);
389
390
  if (modeState.kill) {
390
391
  return deny("Stopped by user, this agent was halted from Pushary");
391
392
  }
@@ -395,7 +396,7 @@ var handlePreToolUse = async (input) => {
395
396
  if (shouldDeferToNativeMode(input.permission_mode, input.tool_name)) {
396
397
  return void 0;
397
398
  }
398
- const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input);
399
+ const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input, input.cwd, repoKeyFor(input.cwd));
399
400
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") {
400
401
  return allow();
401
402
  }
@@ -440,9 +441,9 @@ var handlePermissionRequest = async (input) => {
440
441
  }
441
442
  try {
442
443
  const modeState = await fetchModeState(apiKey, input.session_id);
443
- const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code");
444
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true);
444
445
  if (modeState.kill) return permReqDeny("Stopped by user \u2014 this agent was halted from Pushary");
445
- const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input);
446
+ const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input, input.cwd, repoKeyFor(input.cwd));
446
447
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return permReqAllow();
447
448
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
448
449
  return permReqDeny(`Denied by policy for ${toolPolicy.tool}`);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-SY3NBF67.js";
3
+ } from "./chunk-SYAAGYLZ.js";
4
4
 
5
5
  // src/codex-config.ts
6
6
  import { createHash } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-SY3NBF67.js";
3
+ } from "./chunk-SYAAGYLZ.js";
4
4
 
5
5
  // src/claude-config.ts
6
6
  import { join } from "path";
@@ -19,8 +19,112 @@ var hookWaitDeadline = (startMs, policyWaitSeconds, agent, nowMs) => {
19
19
  };
20
20
  var hookWaitClamped = (startMs, policyWaitSeconds, agent, nowMs) => startMs + hookMaxWaitSeconds(agent) * 1e3 < nowMs + Math.max(policyWaitSeconds, 0) * 1e3;
21
21
  var effectiveWaitSeconds = (timeoutAction, policySeconds, agent) => timeoutAction === "wait" ? hookMaxWaitSeconds(agent) : policySeconds;
22
+ var REPO_KEY_MAX_LENGTH = 200;
23
+ var isRepoKeyMatch = (ruleRepoKey, currentRepoKey) => {
24
+ if (!ruleRepoKey) return true;
25
+ return ruleRepoKey === currentRepoKey;
26
+ };
27
+ var SCP_LIKE_REMOTE = /^[A-Za-z0-9._-]+@([A-Za-z0-9._-]+):(.+)$/;
28
+ var URL_LIKE_REMOTE = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/(.+)$/;
29
+ var NUMERIC_PORT = /:\d+$/;
30
+ var stripCredentials = (host) => {
31
+ const at = host.lastIndexOf("@");
32
+ return at === -1 ? host : host.slice(at + 1);
33
+ };
34
+ var normalizeRepoRemote = (remoteUrl) => {
35
+ const raw = remoteUrl.trim();
36
+ if (raw.length === 0) return void 0;
37
+ let hostAndPath;
38
+ const scp = SCP_LIKE_REMOTE.exec(raw);
39
+ if (scp) {
40
+ hostAndPath = `${scp[1]}/${scp[2]}`;
41
+ } else {
42
+ const url = URL_LIKE_REMOTE.exec(raw);
43
+ if (!url) return void 0;
44
+ if (/^file:/i.test(raw)) return void 0;
45
+ hostAndPath = url[1];
46
+ }
47
+ const slash = hostAndPath.indexOf("/");
48
+ if (slash <= 0) return void 0;
49
+ const host = stripCredentials(hostAndPath.slice(0, slash)).replace(NUMERIC_PORT, "");
50
+ const path = hostAndPath.slice(slash + 1).split("/").filter((segment) => segment.length > 0).join("/");
51
+ if (host.length === 0 || path.length === 0) return void 0;
52
+ const key = `${host}/${path}`.replace(/\.git$/i, "").toLowerCase();
53
+ return key.length > REPO_KEY_MAX_LENGTH ? key.slice(0, REPO_KEY_MAX_LENGTH) : key;
54
+ };
55
+ var localRepoKey = (directoryName) => {
56
+ const name = directoryName.trim().replace(/[/\\]+$/, "").toLowerCase();
57
+ if (name.length === 0 || name === "." || name === "..") return void 0;
58
+ const key = `local/${name}`;
59
+ return key.length > REPO_KEY_MAX_LENGTH ? key.slice(0, REPO_KEY_MAX_LENGTH) : key;
60
+ };
22
61
  var MATCH_RANKS = ["none", "tool", "prefix", "exact"];
23
62
  var matchRankWeight = (rank) => MATCH_RANKS.indexOf(rank);
63
+ var GLOB_MAX_WILDCARDS = 8;
64
+ var GLOB_MAX_ARG_LENGTH = 4096;
65
+ var GLOB_CACHE_MAX = 256;
66
+ var tokenizeGlob = (glob) => {
67
+ let stars = 0;
68
+ for (const char of glob) if (char === "*") stars += 1;
69
+ if (stars > GLOB_MAX_WILDCARDS) return void 0;
70
+ const tokens = [];
71
+ let literal = "";
72
+ const flush = () => {
73
+ if (literal.length > 0) {
74
+ tokens.push({ kind: "literal", value: literal });
75
+ literal = "";
76
+ }
77
+ };
78
+ for (let i = 0; i < glob.length; i += 1) {
79
+ if (glob[i] !== "*") {
80
+ literal += glob[i];
81
+ continue;
82
+ }
83
+ flush();
84
+ if (glob[i + 1] === "*") {
85
+ tokens.push({ kind: "globstar" });
86
+ i += 1;
87
+ continue;
88
+ }
89
+ tokens.push({ kind: "star" });
90
+ }
91
+ flush();
92
+ return tokens;
93
+ };
94
+ var SLASH = "/".charCodeAt(0);
95
+ var matchGlobTokens = (tokens, text) => {
96
+ const n = text.length;
97
+ let reachable = new Uint8Array(n + 1);
98
+ reachable[0] = 1;
99
+ for (const token of tokens) {
100
+ const next = new Uint8Array(n + 1);
101
+ if (token.kind === "literal") {
102
+ const { value } = token;
103
+ for (let p = 0; p <= n - value.length; p += 1) {
104
+ if (reachable[p] && text.startsWith(value, p)) next[p + value.length] = 1;
105
+ }
106
+ } else {
107
+ const spansSeparators = token.kind === "globstar";
108
+ let open = 0;
109
+ for (let p = 0; p <= n; p += 1) {
110
+ if (!spansSeparators && p > 0 && text.charCodeAt(p - 1) === SLASH) open = 0;
111
+ if (reachable[p]) open = 1;
112
+ if (open) next[p] = 1;
113
+ }
114
+ }
115
+ reachable = next;
116
+ }
117
+ return reachable[n] === 1;
118
+ };
119
+ var globCache = /* @__PURE__ */ new Map();
120
+ var compiledGlob = (glob) => {
121
+ const cached = globCache.get(glob);
122
+ if (cached !== void 0 || globCache.has(glob)) return cached;
123
+ const compiled = tokenizeGlob(glob);
124
+ if (globCache.size >= GLOB_CACHE_MAX) globCache.clear();
125
+ globCache.set(glob, compiled);
126
+ return compiled;
127
+ };
24
128
  var matchToolPattern = (pattern, toolName, arg) => {
25
129
  const open = pattern.indexOf("(");
26
130
  if (open === -1 || !pattern.endsWith(")")) {
@@ -31,19 +135,65 @@ var matchToolPattern = (pattern, toolName, arg) => {
31
135
  if (inner.endsWith(":*")) {
32
136
  return arg.startsWith(inner.slice(0, -2)) ? "prefix" : "none";
33
137
  }
138
+ if (inner.includes("*")) {
139
+ if (arg.length > GLOB_MAX_ARG_LENGTH) return "none";
140
+ const compiled = compiledGlob(inner);
141
+ return compiled && matchGlobTokens(compiled, arg) ? "prefix" : "none";
142
+ }
34
143
  return arg === inner ? "exact" : "none";
35
144
  };
36
- var POLICY_ARG_KEYS = {
37
- Bash: "command",
145
+ var POLICY_PATH_KEYS = {
146
+ Read: "file_path",
38
147
  Edit: "file_path",
39
- Write: "file_path"
148
+ Write: "file_path",
149
+ MultiEdit: "file_path",
150
+ NotebookEdit: "notebook_path",
151
+ LS: "path"
40
152
  };
153
+ var POLICY_COMMAND_KEYS = {
154
+ Bash: "command",
155
+ PowerShell: "command",
156
+ Monitor: "command"
157
+ };
158
+ var POLICY_ARG_KEYS = { ...POLICY_COMMAND_KEYS, ...POLICY_PATH_KEYS };
41
159
  var extractPolicyArg = (toolName, toolInput) => {
42
160
  const key = POLICY_ARG_KEYS[toolName];
43
161
  if (!key) return void 0;
44
162
  const value = toolInput[key];
45
163
  return typeof value === "string" ? value : void 0;
46
164
  };
165
+ var normalizeSeparators = (value) => value.replace(/\\/g, "/");
166
+ var isAbsolutePath = (value) => value.startsWith("/") || /^[A-Za-z]:\//.test(value);
167
+ var relativeToCwd = (path, cwd) => {
168
+ const base = normalizeSeparators(cwd).replace(/\/+$/, "");
169
+ if (!base) return void 0;
170
+ const prefix = `${base}/`;
171
+ if (!path.startsWith(prefix)) return void 0;
172
+ const rest = path.slice(prefix.length);
173
+ return rest.length > 0 ? rest : void 0;
174
+ };
175
+ var policyArgForms = (toolName, toolInput, cwd) => {
176
+ const raw = extractPolicyArg(toolName, toolInput);
177
+ if (raw === void 0) return [];
178
+ if (!(toolName in POLICY_PATH_KEYS)) return [raw];
179
+ const forms = [raw];
180
+ const normalized = normalizeSeparators(raw);
181
+ if (normalized !== raw) forms.push(normalized);
182
+ if (cwd && isAbsolutePath(normalized)) {
183
+ const relative = relativeToCwd(normalized, cwd);
184
+ if (relative !== void 0) forms.push(relative);
185
+ }
186
+ return forms;
187
+ };
188
+ var matchToolPatternAny = (pattern, toolName, args) => {
189
+ if (args.length === 0) return matchToolPattern(pattern, toolName);
190
+ let best = "none";
191
+ for (const arg of args) {
192
+ const rank = matchToolPattern(pattern, toolName, arg);
193
+ if (matchRankWeight(rank) > matchRankWeight(best)) best = rank;
194
+ }
195
+ return best;
196
+ };
47
197
  var SAFE_SHELL_COMMANDS = /* @__PURE__ */ new Set([
48
198
  "ls",
49
199
  "pwd",
@@ -581,9 +731,12 @@ export {
581
731
  hookWaitDeadline,
582
732
  hookWaitClamped,
583
733
  effectiveWaitSeconds,
734
+ isRepoKeyMatch,
735
+ normalizeRepoRemote,
736
+ localRepoKey,
584
737
  matchRankWeight,
585
- matchToolPattern,
586
- extractPolicyArg,
738
+ policyArgForms,
739
+ matchToolPatternAny,
587
740
  isSafeReadOnlyCommand,
588
741
  isValidApiKey,
589
742
  ACTION_BODY_MAX,
@@ -26,9 +26,9 @@ interface ReceiptMeta {
26
26
  ok: boolean;
27
27
  }
28
28
 
29
- declare const getPolicy: (apiKey: string, expectedVersion?: string | null, agent?: string) => Promise<PolicyConfig>;
29
+ declare const getPolicy: (apiKey: string, expectedVersion?: string | null, agent?: string, repoAware?: boolean) => Promise<PolicyConfig>;
30
30
  type AutoResolveOrigin = 'safe_readonly' | 'policy_timeout';
31
- declare const resolvePolicy: (config: PolicyConfig, toolName: string, modeOverride?: ApprovalMode | null, toolInput?: Record<string, unknown>) => ToolPolicy;
31
+ declare const resolvePolicy: (config: PolicyConfig, toolName: string, modeOverride?: ApprovalMode | null, toolInput?: Record<string, unknown>, cwd?: string, repoKey?: string) => ToolPolicy;
32
32
  interface ModeState {
33
33
  readonly mode: ApprovalMode | null;
34
34
  readonly kill: boolean;
@@ -57,6 +57,7 @@ interface AgentEvent {
57
57
  agentName?: string;
58
58
  action?: string;
59
59
  machineId?: string;
60
+ repoKey?: string;
60
61
  sessionId?: string;
61
62
  error?: string;
62
63
  taskTitle?: string;
package/dist/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-SVRDT5PP.js";
4
- import "../chunk-T2EEBR2D.js";
3
+ } from "../chunk-GQPIOJA3.js";
4
+ import "../chunk-QYQO6RHJ.js";
5
5
  import "../chunk-YHG74UFF.js";
6
6
  import {
7
7
  askUser,
@@ -15,11 +15,11 @@ import {
15
15
  reportEvent,
16
16
  resolvePolicy,
17
17
  waitForAnswer
18
- } from "../chunk-SWNBIHDX.js";
18
+ } from "../chunk-3EOK3M44.js";
19
19
  import "../chunk-RN3NOEJF.js";
20
- import "../chunk-WYQSLB7K.js";
20
+ import "../chunk-5TECIV6T.js";
21
21
  import "../chunk-DWED7BS3.js";
22
- import "../chunk-SY3NBF67.js";
22
+ import "../chunk-SYAAGYLZ.js";
23
23
  import {
24
24
  getApiKey,
25
25
  getBaseUrl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.54.1",
3
+ "version": "0.56.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -74,7 +74,7 @@
74
74
  "scripts": {
75
75
  "build": "node scripts/bundle-plugin.mjs && tsup",
76
76
  "dev": "tsup --watch",
77
- "test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/reapply.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts && bun test src/wait-ladder.test.ts && bun test src/ledger.test.ts && bun test src/wrapper/wrapper.test.ts && bun test src/wrapper/messageQueue.test.ts && bun test src/wrapper/approver.test.ts && bun test src/wrapper/remoteLoop.test.ts && bun test src/wrapper/spawnClaude.test.ts && bun test src/wrapper/wsProtocol.test.ts && bun test src/wrapper/relayClient.test.ts && bun test src/wrapper/modeLoop.test.ts && bun test src/wrapper/stdinHandoff.test.ts && bun test src/wrapper/localLeg.test.ts && bun test src/crypto.test.ts && bun test src/crypto-vector.test.ts && bun test src/transcript-capture.test.ts && bun test src/transcript-sync.test.ts && bun test src/spawn-daemon.test.ts && bun test src/decision-episode.test.ts"
77
+ "test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/reapply.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts && bun test src/wait-ladder.test.ts && bun test src/ledger.test.ts && bun test src/wrapper/wrapper.test.ts && bun test src/wrapper/messageQueue.test.ts && bun test src/wrapper/approver.test.ts && bun test src/wrapper/remoteLoop.test.ts && bun test src/wrapper/spawnClaude.test.ts && bun test src/wrapper/wsProtocol.test.ts && bun test src/wrapper/relayClient.test.ts && bun test src/wrapper/modeLoop.test.ts && bun test src/wrapper/stdinHandoff.test.ts && bun test src/wrapper/localLeg.test.ts && bun test src/crypto.test.ts && bun test src/crypto-vector.test.ts && bun test src/transcript-capture.test.ts && bun test src/transcript-sync.test.ts && bun test src/spawn-daemon.test.ts && bun test src/decision-episode.test.ts && bun test src/repo.test.ts"
78
78
  },
79
79
  "dependencies": {
80
80
  "@inquirer/prompts": "^8.4.2",