@pushary/agent-hooks 0.83.2 → 0.84.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/bin/pushary-bell-hook.js +5 -5
  3. package/dist/bin/pushary-bell.js +15 -14
  4. package/dist/bin/pushary-claude.js +72 -131
  5. package/dist/bin/pushary-clean.js +13 -12
  6. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  7. package/dist/bin/pushary-codex-bridge.js +348 -0
  8. package/dist/bin/pushary-codex-hook.js +15 -14
  9. package/dist/bin/pushary-codex.js +7 -7
  10. package/dist/bin/pushary-connect.js +13 -12
  11. package/dist/bin/pushary-daemon.js +313 -41
  12. package/dist/bin/pushary-doctor.js +48 -25
  13. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  14. package/dist/bin/pushary-gemini-bridge.js +298 -0
  15. package/dist/bin/pushary-gemini-hook.js +24 -20
  16. package/dist/bin/pushary-hook.js +14 -13
  17. package/dist/bin/pushary-login.js +15 -14
  18. package/dist/bin/pushary-logout.js +12 -11
  19. package/dist/bin/pushary-mode.js +10 -9
  20. package/dist/bin/pushary-notification-hook.js +5 -5
  21. package/dist/bin/pushary-permission-denied-hook.js +9 -9
  22. package/dist/bin/pushary-permission-hook.js +9 -9
  23. package/dist/bin/pushary-post-hook.js +5 -5
  24. package/dist/bin/pushary-prompt-hook.js +5 -5
  25. package/dist/bin/pushary-session-end-hook.js +5 -5
  26. package/dist/bin/pushary-session-start-hook.js +5 -5
  27. package/dist/bin/pushary-setup.js +42 -36
  28. package/dist/bin/pushary-stats.js +6 -5
  29. package/dist/bin/pushary-status.js +17 -16
  30. package/dist/bin/pushary-stop-hook.js +5 -5
  31. package/dist/bin/pushary-stopfailure-hook.js +5 -5
  32. package/dist/bin/pushary-suggestions.js +7 -6
  33. package/dist/bin/pushary-upgrade.js +18 -17
  34. package/dist/bin/pushary-wait.js +10 -9
  35. package/dist/bin/pushary.js +8 -6
  36. package/dist/{chunk-4QQOKEOS.js → chunk-37AD5K7W.js} +6 -6
  37. package/dist/{chunk-SAA3VBDK.js → chunk-437T77TY.js} +3 -3
  38. package/dist/{chunk-U27BZWFT.js → chunk-6LC4WSCL.js} +1 -1
  39. package/dist/{chunk-7WWZPII5.js → chunk-7PZPJSWI.js} +1 -1
  40. package/dist/chunk-CCH775Y6.js +343 -0
  41. package/dist/chunk-DINDL2CF.js +45 -0
  42. package/dist/{chunk-TXF4QZSQ.js → chunk-DOXSWT4Q.js} +1 -1
  43. package/dist/chunk-EVJ6ULHN.js +153 -0
  44. package/dist/{chunk-QR6MVVQ7.js → chunk-F52SIIDG.js} +4 -4
  45. package/dist/{chunk-26D2YMDU.js → chunk-FBGBJXVC.js} +1 -1
  46. package/dist/{chunk-QF7WTF2C.js → chunk-HZNOAJGB.js} +2 -2
  47. package/dist/{chunk-RRZZ7TFJ.js → chunk-J4KFVOCQ.js} +4 -2
  48. package/dist/{chunk-WDRRULBH.js → chunk-K7CSRGKZ.js} +4 -4
  49. package/dist/{chunk-ZUKH2NPJ.js → chunk-MGJBC6DD.js} +1 -44
  50. package/dist/{chunk-GQLB2GBJ.js → chunk-MHBC2XUP.js} +1 -1
  51. package/dist/{chunk-YWRJGYUH.js → chunk-MQMPG5X4.js} +28 -2
  52. package/dist/{chunk-3AGM3TDM.js → chunk-NJ7JT26G.js} +1 -1
  53. package/dist/chunk-NRGRUOAR.js +25 -0
  54. package/dist/chunk-NWYGW7HY.js +117 -0
  55. package/dist/{chunk-7PO2VEA4.js → chunk-OPMSMF5R.js} +38 -33
  56. package/dist/{chunk-DHAZ6SOT.js → chunk-QNBA5OXQ.js} +81 -25
  57. package/dist/{chunk-EHLTWCDZ.js → chunk-SEAHGHCE.js} +1 -1
  58. package/dist/{chunk-NDC3TBHI.js → chunk-UKSQWTBH.js} +1 -1
  59. package/dist/{chunk-UVAC2KHH.js → chunk-UXWEE3QI.js} +1 -1
  60. package/dist/{chunk-MUEW424A.js → chunk-VT4IVERX.js} +50 -3
  61. package/dist/chunk-YJCMI5GF.js +133 -0
  62. package/dist/src/index.d.ts +6 -3
  63. package/dist/src/index.js +9 -9
  64. package/package.json +3 -1
@@ -1,6 +1,30 @@
1
1
  // ../contracts/src/index.ts
2
2
  var APPROVAL_MODES = ["push_only", "terminal_only", "push_first", "notify_only"];
3
3
  var isApprovalMode = (value) => typeof value === "string" && APPROVAL_MODES.includes(value);
4
+ var parseAgentQuestionAnswers = (questions, value) => {
5
+ let parsed;
6
+ try {
7
+ parsed = JSON.parse(value);
8
+ } catch {
9
+ return null;
10
+ }
11
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
12
+ const answers = parsed;
13
+ if (Object.keys(answers).length !== questions.length) return null;
14
+ for (const question of questions) {
15
+ const answer = answers[question.question];
16
+ if (typeof answer !== "string" || answer.trim().length === 0) return null;
17
+ if (!question.multiSelect) continue;
18
+ try {
19
+ const selected = JSON.parse(answer);
20
+ if (!Array.isArray(selected)) continue;
21
+ const labels = new Set(question.options.map((option) => option.label));
22
+ if (selected.length === 0 || new Set(selected).size !== selected.length || !selected.every((label) => typeof label === "string" && labels.has(label))) return null;
23
+ } catch {
24
+ }
25
+ }
26
+ return answers;
27
+ };
4
28
  var INSTALL_SOURCES = [
5
29
  /** `npx @pushary/agent-hooks@latest ...`, the documented one-liner. */
6
30
  "npx",
@@ -454,17 +478,18 @@ var verdictForPolicy = (policy) => {
454
478
  if (isAutoApprove(policy)) return "allow";
455
479
  return "requires_human";
456
480
  };
457
- var selectGoverningRule = (policies, toolName, args, repoKey) => {
481
+ var selectGoverningRule = (policies, toolName, args, repoKey, sessionId) => {
458
482
  let best;
459
483
  let bestWeight = 0;
460
484
  let bestScoped = -1;
461
485
  let bestLength = -1;
462
486
  for (const candidate of policies) {
463
487
  if (!isRepoKeyMatch(candidate.repoKey, repoKey)) continue;
488
+ if (candidate.sessionId && candidate.sessionId !== sessionId) continue;
464
489
  const rank = matchToolPatternAny(candidate.tool, toolName, args);
465
490
  if (rank === "none") continue;
466
491
  const weight = matchRankWeight(rank);
467
- const scoped = candidate.repoKey ? 1 : 0;
492
+ const scoped = candidate.sessionId ? 2 : candidate.repoKey ? 1 : 0;
468
493
  const length = rank === "prefix" ? candidate.tool.length : -1;
469
494
  const better = weight > bestWeight || weight === bestWeight && scoped > bestScoped || weight === bestWeight && scoped === bestScoped && length > bestLength;
470
495
  if (better) {
@@ -811,6 +836,7 @@ var isScopeContract = (value) => {
811
836
 
812
837
  export {
813
838
  isApprovalMode,
839
+ parseAgentQuestionAnswers,
814
840
  normalizeInstallSource,
815
841
  HOOK_BUDGETS,
816
842
  hookWaitDeadline,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isSafeReadOnlyCommand
3
- } from "./chunk-YWRJGYUH.js";
3
+ } from "./chunk-MQMPG5X4.js";
4
4
 
5
5
  // src/ledger.ts
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from "fs";
@@ -0,0 +1,25 @@
1
+ // src/live-capabilities.ts
2
+ var CODEX_BRIDGE_CAPABILITY = "codex-bridge";
3
+ var CODEX_RESUME_CAPABILITY = "codex-resume";
4
+ var GEMINI_BRIDGE_CAPABILITY = "gemini-bridge";
5
+ var GEMINI_RESUME_CAPABILITY = "gemini-resume";
6
+ var GEMINI_BRIDGE_MIN_VERSION = [0, 40, 0];
7
+ var version = (raw) => {
8
+ const match = raw?.match(/^(\d+)\.(\d+)\.(\d+)/);
9
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
10
+ };
11
+ var geminiLiveCapabilities = (rawVersion) => {
12
+ const installed = version(rawVersion);
13
+ if (!installed) return [];
14
+ for (let index = 0; index < GEMINI_BRIDGE_MIN_VERSION.length; index++) {
15
+ if (installed[index] > GEMINI_BRIDGE_MIN_VERSION[index]) break;
16
+ if (installed[index] < GEMINI_BRIDGE_MIN_VERSION[index]) return [];
17
+ }
18
+ return [GEMINI_BRIDGE_CAPABILITY, GEMINI_RESUME_CAPABILITY];
19
+ };
20
+
21
+ export {
22
+ CODEX_BRIDGE_CAPABILITY,
23
+ CODEX_RESUME_CAPABILITY,
24
+ geminiLiveCapabilities
25
+ };
@@ -0,0 +1,117 @@
1
+ import {
2
+ getBaseUrl
3
+ } from "./chunk-2UMNXADU.js";
4
+ import {
5
+ redactSecrets
6
+ } from "./chunk-MQMPG5X4.js";
7
+
8
+ // src/crypto.ts
9
+ import nacl from "tweetnacl";
10
+ var EPH_PUB_LEN = nacl.box.publicKeyLength;
11
+ var BOX_NONCE_LEN = nacl.box.nonceLength;
12
+ var SECRETBOX_NONCE_LEN = nacl.secretbox.nonceLength;
13
+ var SESSION_KEY_LEN = nacl.secretbox.keyLength;
14
+ var toBase64 = (bytes) => Buffer.from(bytes).toString("base64");
15
+ var fromBase64 = (value) => new Uint8Array(Buffer.from(value, "base64"));
16
+ var utf8Encode = (text) => new Uint8Array(Buffer.from(text, "utf-8"));
17
+ var decodePublicKey = (value) => fromBase64(value);
18
+ var generateSessionKey = () => nacl.randomBytes(SESSION_KEY_LEN);
19
+ var wrapSessionKey = (sessionKey, accountPublicKey) => {
20
+ const ephemeral = nacl.box.keyPair();
21
+ const nonce = nacl.randomBytes(BOX_NONCE_LEN);
22
+ const box = nacl.box(sessionKey, nonce, accountPublicKey, ephemeral.secretKey);
23
+ const bundle = new Uint8Array(EPH_PUB_LEN + BOX_NONCE_LEN + box.length);
24
+ bundle.set(ephemeral.publicKey, 0);
25
+ bundle.set(nonce, EPH_PUB_LEN);
26
+ bundle.set(box, EPH_PUB_LEN + BOX_NONCE_LEN);
27
+ return toBase64(bundle);
28
+ };
29
+ var encryptRecord = (plaintext, sessionKey) => {
30
+ const nonce = nacl.randomBytes(SECRETBOX_NONCE_LEN);
31
+ const box = nacl.secretbox(utf8Encode(plaintext), nonce, sessionKey);
32
+ const bundle = new Uint8Array(SECRETBOX_NONCE_LEN + box.length);
33
+ bundle.set(nonce, 0);
34
+ bundle.set(box, SECRETBOX_NONCE_LEN);
35
+ return toBase64(bundle);
36
+ };
37
+
38
+ // src/transcript-capture.ts
39
+ import { createHash } from "crypto";
40
+ var INTERNAL_TYPES = /* @__PURE__ */ new Set([
41
+ "file-history-snapshot",
42
+ "change",
43
+ "queue-operation",
44
+ "summary"
45
+ ]);
46
+ var redactDeep = (value) => {
47
+ if (typeof value === "string") return redactSecrets(value);
48
+ if (Array.isArray(value)) return value.map(redactDeep);
49
+ if (value && typeof value === "object") {
50
+ const out = {};
51
+ for (const [key, inner] of Object.entries(value)) {
52
+ out[key] = redactDeep(inner);
53
+ }
54
+ return out;
55
+ }
56
+ return value;
57
+ };
58
+ var localIdFor = (parsed, rawLine) => typeof parsed.uuid === "string" && parsed.uuid.length > 0 ? parsed.uuid : createHash("sha256").update(rawLine).digest("hex").slice(0, 32);
59
+ var buildTranscriptRecords = (lines, sessionKey, seen = /* @__PURE__ */ new Set()) => {
60
+ const records = [];
61
+ const nextSeen = new Set(seen);
62
+ for (const line of lines) {
63
+ const trimmed = line.trim();
64
+ if (!trimmed) continue;
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(trimmed);
68
+ } catch {
69
+ continue;
70
+ }
71
+ const type = typeof parsed.type === "string" ? parsed.type : "";
72
+ if (INTERNAL_TYPES.has(type)) continue;
73
+ const localId = localIdFor(parsed, trimmed);
74
+ if (nextSeen.has(localId)) continue;
75
+ nextSeen.add(localId);
76
+ const content = encryptRecord(JSON.stringify(redactDeep(parsed)), sessionKey);
77
+ records.push({ localId, content });
78
+ }
79
+ return { records, seen: nextSeen };
80
+ };
81
+
82
+ // src/transcript-api.ts
83
+ var fetchAccountPublicKey = async (apiKey, baseUrl = getBaseUrl()) => {
84
+ try {
85
+ const res = await fetch(`${baseUrl}/api/agent/encryption-account`, {
86
+ headers: { Authorization: `Bearer ${apiKey}` },
87
+ signal: AbortSignal.timeout(5e3)
88
+ });
89
+ if (!res.ok) return null;
90
+ const data = await res.json();
91
+ return typeof data.publicKey === "string" && data.publicKey.length > 0 ? data.publicKey : null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ };
96
+ var uploadTranscriptRecords = async (apiKey, params, baseUrl = getBaseUrl()) => {
97
+ try {
98
+ const res = await fetch(`${baseUrl}/api/agent/transcript`, {
99
+ method: "POST",
100
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
101
+ body: JSON.stringify(params),
102
+ signal: AbortSignal.timeout(15e3)
103
+ });
104
+ return res.ok;
105
+ } catch {
106
+ return false;
107
+ }
108
+ };
109
+
110
+ export {
111
+ decodePublicKey,
112
+ generateSessionKey,
113
+ wrapSessionKey,
114
+ buildTranscriptRecords,
115
+ fetchAccountPublicKey,
116
+ uploadTranscriptRecords
117
+ };
@@ -1,3 +1,6 @@
1
+ import {
2
+ getMachineId
3
+ } from "./chunk-RN3NOEJF.js";
1
4
  import {
2
5
  callMcpTool,
3
6
  withRetry
@@ -7,7 +10,11 @@ import {
7
10
  } from "./chunk-SAF6HGAA.js";
8
11
  import {
9
12
  reportedInstallSource
10
- } from "./chunk-SAA3VBDK.js";
13
+ } from "./chunk-437T77TY.js";
14
+ import {
15
+ getApiKey,
16
+ getBaseUrl
17
+ } from "./chunk-2UMNXADU.js";
11
18
  import {
12
19
  ACTION_BODY_MAX,
13
20
  DECISION_LINE_MAX,
@@ -23,14 +30,7 @@ import {
23
30
  redactSecrets,
24
31
  redactSecretsDeep,
25
32
  selectGoverningRule
26
- } from "./chunk-YWRJGYUH.js";
27
- import {
28
- getMachineId
29
- } from "./chunk-RN3NOEJF.js";
30
- import {
31
- getApiKey,
32
- getBaseUrl
33
- } from "./chunk-2UMNXADU.js";
33
+ } from "./chunk-MQMPG5X4.js";
34
34
 
35
35
  // src/policy.ts
36
36
  import { createHash } from "crypto";
@@ -67,12 +67,14 @@ var policyCacheFile = (apiKey, agent, repoAware = false) => {
67
67
  const hash = createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 12);
68
68
  return join(tmpdir(), `pushary-policy-${hash}.json`);
69
69
  };
70
- var fetchPolicy = async (apiKey, agent, repoAware = false) => {
70
+ var fetchPolicy = async (apiKey, agent, repoAware = false, sessionId) => {
71
71
  return withRetry(async () => {
72
72
  const baseUrl = getBaseUrl();
73
73
  const params = new URLSearchParams();
74
74
  if (agent) params.set("agent", agent);
75
75
  if (repoAware) params.set("repoAware", "1");
76
+ params.set("scopeAware", "1");
77
+ if (sessionId) params.set("sessionId", sessionId);
76
78
  const query = params.toString();
77
79
  const url = query ? `${baseUrl}/api/mcp/policy?${query}` : `${baseUrl}/api/mcp/policy`;
78
80
  const response = await fetch(url, {
@@ -87,7 +89,7 @@ var fetchPolicy = async (apiKey, agent, repoAware = false) => {
87
89
  return raw;
88
90
  }, { maxAttempts: 2 });
89
91
  };
90
- var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false) => {
92
+ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false, sessionId) => {
91
93
  const path = policyCacheFile(apiKey, agent, repoAware);
92
94
  let staleCache = null;
93
95
  if (existsSync(path)) {
@@ -96,19 +98,20 @@ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false) => {
96
98
  const cached = JSON.parse(stat);
97
99
  if (!isPolicyConfig(cached)) throw new Error("Corrupted cache");
98
100
  const versionStale = expectedVersion != null && (cached._policyVersion ?? null) !== expectedVersion;
101
+ const sessionStale = (cached._sessionId ?? null) !== (sessionId ?? null);
99
102
  const age = cached._cachedAt ? Date.now() - cached._cachedAt : 0;
100
103
  const ttlFresh = !cached._cachedAt || age < CACHE_TTL_MS;
101
- if (ttlFresh && !versionStale) {
104
+ if (ttlFresh && !versionStale && !sessionStale) {
102
105
  return cached;
103
106
  }
104
- if (age < STALE_CACHE_MAX_MS) staleCache = cached;
107
+ if (!sessionStale && age < STALE_CACHE_MAX_MS) staleCache = cached;
105
108
  } catch {
106
109
  }
107
110
  }
108
111
  try {
109
- const policy = await fetchPolicy(apiKey, agent, repoAware);
112
+ const policy = await fetchPolicy(apiKey, agent, repoAware, sessionId);
110
113
  try {
111
- writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), { encoding: "utf-8", mode: 384 });
114
+ writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null, _sessionId: sessionId ?? null }), { encoding: "utf-8", mode: 384 });
112
115
  } catch {
113
116
  }
114
117
  return policy;
@@ -124,20 +127,20 @@ var autoApprove = (tool) => ({
124
127
  mode: "terminal_only",
125
128
  pushFirstSeconds: 0
126
129
  });
127
- var resolveAutoResolveOrigin = (config, toolName, toolInput, cwd, repoKey) => {
130
+ var resolveAutoResolveOrigin = (config, toolName, toolInput, cwd, repoKey, sessionId) => {
128
131
  const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
129
132
  const arg = args[0];
130
- const match = selectGoverningRule(config.policies, toolName, args, repoKey);
133
+ const match = selectGoverningRule(config.policies, toolName, args, repoKey, sessionId);
131
134
  const governedBySpecificRule = match?.rank === "exact" || match?.rank === "prefix";
132
135
  if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
133
136
  return "safe_readonly";
134
137
  }
135
138
  return "policy_timeout";
136
139
  };
137
- var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey) => {
140
+ var resolvePolicy = (config, toolName, modeOverride, toolInput, cwd, repoKey, sessionId) => {
138
141
  const args = toolInput ? policyArgForms(toolName, toolInput, cwd) : [];
139
142
  const arg = args[0];
140
- const match = selectGoverningRule(config.policies, toolName, args, repoKey);
143
+ const match = selectGoverningRule(config.policies, toolName, args, repoKey, sessionId);
141
144
  let base = match?.policy ?? config.policies.find((p) => p.tool === "*") ?? {
142
145
  tool: toolName,
143
146
  timeoutSeconds: config.defaultTimeoutSeconds,
@@ -172,9 +175,9 @@ var restraintRank = (policy) => {
172
175
  return policy.mode === "terminal_only" || policy.mode === "notify_only" ? 1 : 2;
173
176
  };
174
177
  var strictestPolicy = (a, b) => restraintRank(b) > restraintRank(a) ? b : a;
175
- var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey) => {
176
- if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey);
177
- return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey)).reduce(strictestPolicy);
178
+ var resolvePolicyAcross = (config, toolName, modeOverride, toolInputs, cwd, repoKey, sessionId) => {
179
+ if (toolInputs.length === 0) return resolvePolicy(config, toolName, modeOverride, void 0, cwd, repoKey, sessionId);
180
+ return toolInputs.map((input) => resolvePolicy(config, toolName, modeOverride, input, cwd, repoKey, sessionId)).reduce(strictestPolicy);
178
181
  };
179
182
  var isModeStateDegraded = (state) => state?.degraded === true;
180
183
  var MODE_GRACE_MS = 60 * 1e3;
@@ -930,12 +933,13 @@ var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
930
933
  };
931
934
  var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
932
935
  var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
933
- var readFreshCachedPolicy = (apiKey, agentType) => {
936
+ var readFreshCachedPolicy = (apiKey, agentType, sessionId) => {
934
937
  const request = policyRequestFor(agentType);
935
938
  const path = policyCacheFile(apiKey, request.agent, request.repoAware);
936
939
  if (!existsSync5(path)) return null;
937
940
  const cached = JSON.parse(readFileSync4(path, "utf-8"));
938
941
  if (!isPolicyConfig(cached)) return null;
942
+ if ((cached._sessionId ?? null) !== (sessionId ?? null)) return null;
939
943
  if (!cached._cachedAt || Date.now() - cached._cachedAt >= POLICY_CACHE_TTL_MS) return null;
940
944
  return cached;
941
945
  };
@@ -943,13 +947,13 @@ var autoDecisionEnabled = () => {
943
947
  const flag = process.env.PUSHARY_AUTO_DECISION_BEACON;
944
948
  return flag === "1" || flag === "true";
945
949
  };
946
- var deriveDecisionSource = (toolName, toolInput, liveMode, agentType, cwd) => {
950
+ var deriveDecisionSource = (toolName, toolInput, liveMode, agentType, cwd, sessionId) => {
947
951
  try {
948
952
  if (liveMode.kill) return "terminal";
949
953
  if (!autoDecisionEnabled()) return void 0;
950
- const policy = readFreshCachedPolicy(getApiKey(), agentType);
954
+ const policy = readFreshCachedPolicy(getApiKey(), agentType, sessionId);
951
955
  if (!policy) return void 0;
952
- const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd));
956
+ const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd), sessionId);
953
957
  if (isAutoApprove(resolved)) return "policy_auto";
954
958
  if (resolved.mode === "push_only" || resolved.mode === "push_first") return "human";
955
959
  return "terminal";
@@ -957,16 +961,16 @@ var deriveDecisionSource = (toolName, toolInput, liveMode, agentType, cwd) => {
957
961
  return void 0;
958
962
  }
959
963
  };
960
- var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode, agentType, cwd) => {
964
+ var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode, agentType, cwd, sessionId) => {
961
965
  try {
962
966
  if (liveMode.kill) return void 0;
963
967
  if (!autoDecisionEnabled()) return void 0;
964
- const policy = readFreshCachedPolicy(getApiKey(), agentType);
968
+ const policy = readFreshCachedPolicy(getApiKey(), agentType, sessionId);
965
969
  if (!policy) return void 0;
966
- const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd));
970
+ const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd), sessionId);
967
971
  if (resolved.timeoutSeconds !== 0 || resolved.timeoutAction !== "approve") return void 0;
968
972
  return {
969
- decisionOrigin: resolveAutoResolveOrigin(policy, toolName, toolInput, cwd, repoKeyFor(cwd)),
973
+ decisionOrigin: resolveAutoResolveOrigin(policy, toolName, toolInput, cwd, repoKeyFor(cwd), sessionId),
970
974
  toolName,
971
975
  toolTarget: deriveToolTarget(toolName, toolInput)
972
976
  };
@@ -1009,7 +1013,7 @@ var reportEvent = async (event, options = {}) => {
1009
1013
  machineId: event.machineId ?? getMachineId(),
1010
1014
  // Lets the phone's Stop end this process instead of only denying its next
1011
1015
  // tool call. Ours alone: never another process, never a parent.
1012
- pid: event.pid ?? process.pid,
1016
+ pid: event.pid,
1013
1017
  repoKey: event.repoKey ?? repoKeyFor(),
1014
1018
  installMode: detectInstallMode(),
1015
1019
  // How the binary was INVOKED this time and which front door produced the
@@ -1031,6 +1035,7 @@ var reportEvent = async (event, options = {}) => {
1031
1035
  }),
1032
1036
  signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
1033
1037
  });
1038
+ if (options.requireOk && !res.ok) throw new Error(`/api/agent/event rejected (${res.status})`);
1034
1039
  try {
1035
1040
  return await res.json();
1036
1041
  } catch {
@@ -1056,8 +1061,8 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
1056
1061
  for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
1057
1062
  }
1058
1063
  const isMirrorOnly = NON_GATED_MIRROR_TOOLS.has(input.tool_name);
1059
- const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode, agent.type, input.cwd);
1060
- const beacon = !isMirrorOnly && decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode, agent.type, input.cwd) : void 0;
1064
+ const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode, agent.type, input.cwd, input.session_id);
1065
+ const beacon = !isMirrorOnly && decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode, agent.type, input.cwd, input.session_id) : void 0;
1061
1066
  const toolReport = reportEvent({
1062
1067
  event: isError ? "tool_error" : "tool_complete",
1063
1068
  agentType: agent.type,
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  PRETOOLUSE_HANDLED_TOOLS
3
- } from "./chunk-NDC3TBHI.js";
3
+ } from "./chunk-UKSQWTBH.js";
4
4
  import {
5
5
  isGatingMoment,
6
6
  recordKeylessMoment
7
- } from "./chunk-3AGM3TDM.js";
7
+ } from "./chunk-NJ7JT26G.js";
8
8
  import {
9
9
  denyReasonFrom,
10
10
  isDeferAnswer,
11
11
  resolveGate
12
- } from "./chunk-4QQOKEOS.js";
12
+ } from "./chunk-37AD5K7W.js";
13
13
  import {
14
14
  DEFAULT_SESSION,
15
15
  askUser,
@@ -29,13 +29,7 @@ import {
29
29
  sendNotification,
30
30
  throttlePass,
31
31
  waitForAnswer
32
- } from "./chunk-7PO2VEA4.js";
33
- import {
34
- buildDecisionEpisodeFeatures,
35
- effectiveWaitSeconds,
36
- hookWaitClamped,
37
- hookWaitDeadline
38
- } from "./chunk-YWRJGYUH.js";
32
+ } from "./chunk-OPMSMF5R.js";
39
33
  import {
40
34
  getMachineId
41
35
  } from "./chunk-RN3NOEJF.js";
@@ -43,6 +37,13 @@ import {
43
37
  getApiKey,
44
38
  getBaseUrl
45
39
  } from "./chunk-2UMNXADU.js";
40
+ import {
41
+ buildDecisionEpisodeFeatures,
42
+ effectiveWaitSeconds,
43
+ hookWaitClamped,
44
+ hookWaitDeadline,
45
+ parseAgentQuestionAnswers
46
+ } from "./chunk-MQMPG5X4.js";
46
47
 
47
48
  // src/decision-episode.ts
48
49
  var EPISODE_PATH = "/api/agent/decision-episode";
@@ -271,6 +272,7 @@ var dispatchModeHandler = async (apiKey, input, toolPolicy, machineId, trainingC
271
272
  action: deriveAction(input.tool_name, input.tool_input),
272
273
  blocker: scopeReason ?? deriveBlocker(toolPolicy.mode),
273
274
  actionBody: deriveActionBody(input.tool_name, input.tool_input),
275
+ repoKey: repoKeyFor(input.cwd),
274
276
  // Only when the breach is what caused the question, so an ordinary approval
275
277
  // can never silently widen a scope.
276
278
  scopePath: scopeReason ? scopePath : void 0,
@@ -352,10 +354,25 @@ var shouldDeferToNativeMode = (permissionMode, toolName) => {
352
354
  var MAX_PHONE_QUESTIONS = 4;
353
355
  var parseQuestion = (raw) => {
354
356
  const q = raw;
355
- if (!q || typeof q.question !== "string" || q.multiSelect === true) return void 0;
356
- const labels = Array.isArray(q.options) ? q.options.map((o) => o && typeof o === "object" ? o.label : void 0).filter((l) => typeof l === "string" && l.length > 0) : [];
357
+ if (!q || typeof q.question !== "string") return void 0;
358
+ const options = Array.isArray(q.options) ? q.options.flatMap((option) => {
359
+ if (!option || typeof option !== "object") return [];
360
+ const value = option;
361
+ if (typeof value.label !== "string" || value.label.length === 0) return [];
362
+ return [{
363
+ label: value.label,
364
+ ...typeof value.description === "string" ? { description: value.description } : {}
365
+ }];
366
+ }) : [];
367
+ const labels = options.map((option) => option.label);
357
368
  if (labels.length < 2) return void 0;
358
- return { question: q.question, header: typeof q.header === "string" ? q.header : void 0, labels };
369
+ return {
370
+ question: q.question,
371
+ header: typeof q.header === "string" ? q.header : void 0,
372
+ multiSelect: q.multiSelect === true,
373
+ options,
374
+ labels
375
+ };
359
376
  };
360
377
  var parseQuestions = (toolInput) => {
361
378
  const questions = toolInput.questions;
@@ -364,20 +381,49 @@ var parseQuestions = (toolInput) => {
364
381
  const parsed = [];
365
382
  for (const raw of questions) {
366
383
  const one = parseQuestion(raw);
367
- if (!one) return void 0;
384
+ if (!one || parsed.some((question) => question.question === one.question)) return void 0;
368
385
  parsed.push(one);
369
386
  }
370
387
  return parsed;
371
388
  };
372
- var questionContext = (q, index, total, project) => {
373
- const subject = q.header ? `${q.header}: your agent is asking in ${project}` : `Your agent is asking in ${project}`;
389
+ var questionContext = (question, index, total, project) => {
390
+ const subject = question.header ? `${question.header}: your agent is asking in ${project}` : `Your agent is asking in ${project}`;
374
391
  return total > 1 ? `${subject} (${index + 1} of ${total})` : subject;
375
392
  };
376
- var handleAskUserQuestion = async (apiKey, input) => {
377
- const parsed = parseQuestions(input.tool_input ?? {});
378
- if (!parsed) return void 0;
379
- const projectName = basename(input.cwd ?? process.cwd());
380
- const deadline = hookWaitDeadline(START_MS, effectiveWaitSeconds("wait", 0, "claude"), "claude", Date.now());
393
+ var encodeAnswer = (question, value) => question.multiSelect ? JSON.stringify([value]) : value;
394
+ var askAsOneCard = async (apiKey, input, parsed, projectName, deadline) => {
395
+ const only = parsed[0];
396
+ let result;
397
+ try {
398
+ result = await askUser(apiKey, {
399
+ question: only.question,
400
+ type: "select",
401
+ options: [...only.labels],
402
+ questions: parsed.map(({ labels: _, ...question }) => question),
403
+ context: questionContext(only, 0, 1, projectName),
404
+ agentName: `Claude Code - ${projectName}`,
405
+ sessionId: input.session_id,
406
+ machineId: getMachineId(),
407
+ toolName: "AskUserQuestion",
408
+ toolUseId: input.tool_use_id
409
+ });
410
+ } catch {
411
+ return void 0;
412
+ }
413
+ if (result.suppressed || result.noDevices) {
414
+ await cancelQuestion(apiKey, result.correlationId).catch(() => {
415
+ });
416
+ return void 0;
417
+ }
418
+ const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
419
+ const values = answer.answered && answer.value ? parseAgentQuestionAnswers(parsed, answer.value) : null;
420
+ if (!values || isDeferAnswer(answer.value)) {
421
+ savePendingQuestion(input.session_id || DEFAULT_SESSION, result.correlationId);
422
+ return void 0;
423
+ }
424
+ return values;
425
+ };
426
+ var askQuestionByQuestion = async (apiKey, input, parsed, projectName, deadline) => {
381
427
  const answers = {};
382
428
  for (const [index, question] of parsed.entries()) {
383
429
  let result;
@@ -410,8 +456,17 @@ var handleAskUserQuestion = async (apiKey, input) => {
410
456
  savePendingQuestion(input.session_id || DEFAULT_SESSION, result.correlationId);
411
457
  return void 0;
412
458
  }
413
- answers[question.question] = value;
459
+ answers[question.question] = encodeAnswer(question, value);
414
460
  }
461
+ return answers;
462
+ };
463
+ var handleAskUserQuestion = async (apiKey, input) => {
464
+ const parsed = parseQuestions(input.tool_input ?? {});
465
+ if (!parsed) return void 0;
466
+ const projectName = basename(input.cwd ?? process.cwd());
467
+ const deadline = hookWaitDeadline(START_MS, effectiveWaitSeconds("wait", 0, "claude"), "claude", Date.now());
468
+ const answers = parsed.length === 1 ? await askAsOneCard(apiKey, input, parsed, projectName, deadline) : await askQuestionByQuestion(apiKey, input, parsed, projectName, deadline);
469
+ if (!answers) return void 0;
415
470
  return allowWithInput({ ...input.tool_input ?? {}, answers });
416
471
  };
417
472
  var handleKeyless = (input) => {
@@ -438,7 +493,8 @@ var claudeGateInput = (modeState, config, input) => {
438
493
  toolInputs: [input.tool_input ?? {}],
439
494
  scopePaths: scopePath ? [scopePath] : [],
440
495
  cwd: input.cwd,
441
- repoKey: repoKeyFor(input.cwd)
496
+ repoKey: repoKeyFor(input.cwd),
497
+ sessionId: input.session_id
442
498
  };
443
499
  };
444
500
  var handlePreToolUse = async (input) => {
@@ -451,7 +507,7 @@ var handlePreToolUse = async (input) => {
451
507
  }
452
508
  try {
453
509
  const modeState = await fetchModeState(apiKey, input.session_id);
454
- const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true);
510
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true, input.session_id);
455
511
  const verdict = resolveGate(claudeGateInput(modeState, policy, input));
456
512
  if (verdict.kind === "kill") return deny(verdict.reason);
457
513
  if (input.tool_name === "AskUserQuestion") {
@@ -509,7 +565,7 @@ var handlePermissionRequest = async (input) => {
509
565
  }
510
566
  try {
511
567
  const modeState = await fetchModeState(apiKey, input.session_id);
512
- const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true);
568
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code", true, input.session_id);
513
569
  const verdict = resolveGate(claudeGateInput(modeState, policy, input));
514
570
  switch (verdict.kind) {
515
571
  case "kill":
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-YWRJGYUH.js";
3
+ } from "./chunk-MQMPG5X4.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-YWRJGYUH.js";
3
+ } from "./chunk-MQMPG5X4.js";
4
4
 
5
5
  // src/claude-config.ts
6
6
  import { join } from "path";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  codexHomeFrom
3
- } from "./chunk-EHLTWCDZ.js";
3
+ } from "./chunk-SEAHGHCE.js";
4
4
 
5
5
  // src/vscode-config.ts
6
6
  import { homedir } from "os";