@pushary/agent-hooks 0.27.0 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/dist/bin/pushary-clean.js +3 -3
  4. package/dist/bin/pushary-codex-hook.js +2 -2
  5. package/dist/bin/pushary-codex.js +2 -2
  6. package/dist/bin/pushary-doctor.js +2 -2
  7. package/dist/bin/pushary-gemini-hook.js +2 -2
  8. package/dist/bin/pushary-hook.js +3 -3
  9. package/dist/bin/pushary-post-hook.js +2 -2
  10. package/dist/bin/pushary-prompt-hook.js +2 -2
  11. package/dist/bin/pushary-setup.js +4 -4
  12. package/dist/bin/pushary-stop-hook.js +2 -2
  13. package/dist/chunk-22CV7V7A.js +38 -0
  14. package/dist/chunk-2BE2IPMO.js +315 -0
  15. package/dist/chunk-2CRLBMOX.js +244 -0
  16. package/dist/chunk-2HMNOZPY.js +109 -0
  17. package/dist/chunk-2I6DLXJN.js +219 -0
  18. package/dist/chunk-2Q6XPZ4Q.js +395 -0
  19. package/dist/chunk-3LC4YUMA.js +194 -0
  20. package/dist/chunk-3MIR7ODJ.js +112 -0
  21. package/dist/chunk-3QZESA66.js +222 -0
  22. package/dist/chunk-4Z4MB37G.js +96 -0
  23. package/dist/chunk-5GFUI5N6.js +132 -0
  24. package/dist/chunk-5JEDLXEC.js +99 -0
  25. package/dist/chunk-5LTQNC7R.js +824 -0
  26. package/dist/chunk-5MA3CPZB.js +141 -0
  27. package/dist/chunk-5QHQVXWN.js +174 -0
  28. package/dist/{chunk-WXWHRXDK.js → chunk-5V3XJ2ES.js} +1 -1
  29. package/dist/chunk-6A3WCJO2.js +416 -0
  30. package/dist/chunk-6QWFMVCD.js +328 -0
  31. package/dist/chunk-6SRXMZAN.js +99 -0
  32. package/dist/chunk-7OXL2QQ4.js +357 -0
  33. package/dist/chunk-7PTU7TGE.js +96 -0
  34. package/dist/chunk-AB4KX4XT.js +109 -0
  35. package/dist/chunk-AC4UYAGX.js +136 -0
  36. package/dist/chunk-ACE77TKQ.js +328 -0
  37. package/dist/chunk-BHAMEPOP.js +323 -0
  38. package/dist/chunk-BYNYYZWW.js +412 -0
  39. package/dist/{chunk-FDR5WYT7.js → chunk-C4BMEVK7.js} +1 -1
  40. package/dist/chunk-C5TFTNHG.js +244 -0
  41. package/dist/chunk-CH53PBQN.js +265 -0
  42. package/dist/chunk-DF3BM6BF.js +195 -0
  43. package/dist/chunk-DKZVKEOW.js +175 -0
  44. package/dist/chunk-EMPL27ZV.js +96 -0
  45. package/dist/chunk-EQJXMEIR.js +175 -0
  46. package/dist/{chunk-MVNOP5IZ.js → chunk-FB3N633T.js} +1 -1
  47. package/dist/chunk-FGZZRCOG.js +194 -0
  48. package/dist/chunk-FXYTRZHC.js +177 -0
  49. package/dist/chunk-GTJGZBTQ.js +328 -0
  50. package/dist/chunk-GYPPVD3P.js +1047 -0
  51. package/dist/chunk-HJMFYDWY.js +222 -0
  52. package/dist/chunk-HRQEECB6.js +315 -0
  53. package/dist/chunk-HWEMAAOY.js +370 -0
  54. package/dist/{chunk-VHD3GMZ3.js → chunk-I4GMPUYN.js} +2 -2
  55. package/dist/{chunk-TUILTFLS.js → chunk-IAOXM7X5.js} +1 -1
  56. package/dist/chunk-IBWCHA5M.js +10 -0
  57. package/dist/chunk-JPRRYC5X.js +328 -0
  58. package/dist/chunk-KTM4ZQ3W.js +96 -0
  59. package/dist/chunk-KTP2EPVB.js +27 -0
  60. package/dist/chunk-KYARP7KP.js +97 -0
  61. package/dist/chunk-LXUHXG2X.js +937 -0
  62. package/dist/chunk-M2N5DYWN.js +247 -0
  63. package/dist/chunk-M4GDBGXF.js +824 -0
  64. package/dist/chunk-M5F2L4KF.js +177 -0
  65. package/dist/chunk-M6S3M6E3.js +244 -0
  66. package/dist/chunk-N7VXDBQU.js +6 -0
  67. package/dist/chunk-O2QL6U6G.js +194 -0
  68. package/dist/chunk-O6A5RHWY.js +122 -0
  69. package/dist/chunk-ODUXELPM.js +219 -0
  70. package/dist/chunk-OF5WIOYS.js +129 -0
  71. package/dist/chunk-OP4QAN23.js +99 -0
  72. package/dist/chunk-P4JH2Q7Z.js +102 -0
  73. package/dist/chunk-P6QBZS27.js +231 -0
  74. package/dist/chunk-PMD5JSV3.js +242 -0
  75. package/dist/chunk-PSSHNBEL.js +177 -0
  76. package/dist/chunk-PZO7RZ5W.js +145 -0
  77. package/dist/chunk-QEPRH7JB.js +315 -0
  78. package/dist/chunk-QNEYHDKR.js +145 -0
  79. package/dist/chunk-QRXWPZKN.js +308 -0
  80. package/dist/chunk-QY4L6XHN.js +384 -0
  81. package/dist/chunk-RNWPCELY.js +176 -0
  82. package/dist/chunk-RSHN2AQ7.js +29 -0
  83. package/dist/chunk-RZLVE57X.js +38 -0
  84. package/dist/chunk-SDCIKREA.js +241 -0
  85. package/dist/chunk-SDREQWNI.js +175 -0
  86. package/dist/chunk-SH26ZOHU.js +159 -0
  87. package/dist/chunk-SJ5MC36D.js +882 -0
  88. package/dist/chunk-TRLBBLSS.js +176 -0
  89. package/dist/chunk-U6OXZPYC.js +384 -0
  90. package/dist/chunk-UDQZNO5H.js +231 -0
  91. package/dist/chunk-UIBTFZUD.js +314 -0
  92. package/dist/chunk-USUCPCUC.js +156 -0
  93. package/dist/chunk-UTK3TJC4.js +882 -0
  94. package/dist/chunk-V4GLWPD7.js +174 -0
  95. package/dist/chunk-VC6U3QGF.js +142 -0
  96. package/dist/chunk-VIST7ACL.js +88 -0
  97. package/dist/chunk-VUNL35KE.js +16 -0
  98. package/dist/chunk-VWBNI4SC.js +174 -0
  99. package/dist/chunk-W5KRWUNE.js +262 -0
  100. package/dist/chunk-WCGKLHCL.js +154 -0
  101. package/dist/chunk-WNXGIEX7.js +219 -0
  102. package/dist/chunk-WPVL6VIT.js +160 -0
  103. package/dist/chunk-X3YCHQQK.js +357 -0
  104. package/dist/chunk-X6LV22NZ.js +229 -0
  105. package/dist/chunk-XHTZ2DWC.js +206 -0
  106. package/dist/chunk-XPVSZIA3.js +315 -0
  107. package/dist/chunk-Y633ZIJF.js +384 -0
  108. package/dist/chunk-YCL4GUFR.js +222 -0
  109. package/dist/chunk-YTMKB44I.js +220 -0
  110. package/dist/chunk-Z5PL3K7C.js +322 -0
  111. package/dist/chunk-ZWBS3T7Q.js +244 -0
  112. package/dist/pushary-mode-T7XOPI6Z.js +83 -0
  113. package/dist/pushary-wait-4BUC6ZM6.js +93 -0
  114. package/dist/src/index.js +3 -3
  115. package/package.json +25 -1
@@ -0,0 +1,322 @@
1
+ // ../contracts/src/index.ts
2
+ var APPROVAL_MODES = ["push_only", "terminal_only", "push_first", "notify_only"];
3
+ var isApprovalMode = (value) => typeof value === "string" && APPROVAL_MODES.includes(value);
4
+ var HOOK_BUDGETS = {
5
+ claude: { budgetSeconds: 120, guardSeconds: 10 },
6
+ codex: { budgetSeconds: 180, guardSeconds: 10 },
7
+ gemini: { budgetSeconds: 180, guardSeconds: 10 },
8
+ // The Cursor gate is a dependency-free .mjs that cannot import this module; it
9
+ // mirrors these as MAX_BLOCK_MS (45s) inside its 60s failClosed budget.
10
+ cursor: { budgetSeconds: 60, guardSeconds: 15 }
11
+ };
12
+ var hookMaxWaitSeconds = (agent) => Math.max(HOOK_BUDGETS[agent].budgetSeconds - HOOK_BUDGETS[agent].guardSeconds, 1);
13
+ var hookWaitDeadline = (startMs, policyWaitSeconds, agent, nowMs) => {
14
+ const maxWait = hookMaxWaitSeconds(agent);
15
+ return Math.min(
16
+ nowMs + Math.min(Math.max(policyWaitSeconds, 0), maxWait) * 1e3,
17
+ startMs + maxWait * 1e3
18
+ );
19
+ };
20
+ var hookWaitClamped = (startMs, policyWaitSeconds, agent, nowMs) => startMs + hookMaxWaitSeconds(agent) * 1e3 < nowMs + Math.max(policyWaitSeconds, 0) * 1e3;
21
+ var effectiveWaitSeconds = (timeoutAction, policySeconds, agent) => timeoutAction === "wait" ? hookMaxWaitSeconds(agent) : policySeconds;
22
+ var MATCH_RANKS = ["none", "tool", "prefix", "exact"];
23
+ var matchRankWeight = (rank) => MATCH_RANKS.indexOf(rank);
24
+ var matchToolPattern = (pattern, toolName, arg) => {
25
+ const open = pattern.indexOf("(");
26
+ if (open === -1 || !pattern.endsWith(")")) {
27
+ return pattern === toolName ? "tool" : "none";
28
+ }
29
+ if (pattern.slice(0, open) !== toolName || arg === void 0) return "none";
30
+ const inner = pattern.slice(open + 1, -1);
31
+ if (inner.endsWith(":*")) {
32
+ return arg.startsWith(inner.slice(0, -2)) ? "prefix" : "none";
33
+ }
34
+ return arg === inner ? "exact" : "none";
35
+ };
36
+ var POLICY_ARG_KEYS = {
37
+ Bash: "command",
38
+ Edit: "file_path",
39
+ Write: "file_path"
40
+ };
41
+ var extractPolicyArg = (toolName, toolInput) => {
42
+ const key = POLICY_ARG_KEYS[toolName];
43
+ if (!key) return void 0;
44
+ const value = toolInput[key];
45
+ return typeof value === "string" ? value : void 0;
46
+ };
47
+ var SAFE_SHELL_COMMANDS = /* @__PURE__ */ new Set([
48
+ "ls",
49
+ "pwd",
50
+ "cd",
51
+ "cat",
52
+ "head",
53
+ "tail",
54
+ "wc",
55
+ "echo",
56
+ "printf",
57
+ "which",
58
+ "type",
59
+ "whoami",
60
+ "id",
61
+ "uname",
62
+ "arch",
63
+ "printenv",
64
+ "locale",
65
+ "tty",
66
+ "dirname",
67
+ "basename",
68
+ "realpath",
69
+ "readlink",
70
+ "stat",
71
+ "cut",
72
+ "nl",
73
+ "tr",
74
+ "comm",
75
+ "diff",
76
+ "cmp",
77
+ "grep",
78
+ "egrep",
79
+ "fgrep",
80
+ "jq",
81
+ "cksum",
82
+ "md5sum",
83
+ "sha1sum",
84
+ "sha256sum",
85
+ "du",
86
+ "df",
87
+ "ps",
88
+ "true"
89
+ ]);
90
+ var SAFE_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
91
+ "status",
92
+ "log",
93
+ "diff",
94
+ "show",
95
+ "rev-parse",
96
+ "describe",
97
+ "blame",
98
+ "shortlog",
99
+ "ls-files",
100
+ "ls-tree",
101
+ "cat-file",
102
+ "whatchanged",
103
+ "rev-list",
104
+ "name-rev",
105
+ "for-each-ref",
106
+ "var",
107
+ "count-objects"
108
+ ]);
109
+ var GIT_WRITE_FLAGS = (token) => token === "--output" || token.startsWith("--output=");
110
+ var DANGEROUS_FIND_ACTIONS = /* @__PURE__ */ new Set([
111
+ "-exec",
112
+ "-execdir",
113
+ "-ok",
114
+ "-okdir",
115
+ "-delete",
116
+ "-fprintf",
117
+ "-fprint",
118
+ "-fprint0",
119
+ "-fls"
120
+ ]);
121
+ var SAFE_FIND_TOKENS = /* @__PURE__ */ new Set([
122
+ "-name",
123
+ "-iname",
124
+ "-path",
125
+ "-ipath",
126
+ "-wholename",
127
+ "-iwholename",
128
+ "-lname",
129
+ "-ilname",
130
+ "-regex",
131
+ "-iregex",
132
+ "-type",
133
+ "-xtype",
134
+ "-maxdepth",
135
+ "-mindepth",
136
+ "-depth",
137
+ "-mount",
138
+ "-xdev",
139
+ "-size",
140
+ "-empty",
141
+ "-perm",
142
+ "-readable",
143
+ "-writable",
144
+ "-executable",
145
+ "-mtime",
146
+ "-mmin",
147
+ "-atime",
148
+ "-amin",
149
+ "-ctime",
150
+ "-cmin",
151
+ "-newer",
152
+ "-newermt",
153
+ "-anewer",
154
+ "-cnewer",
155
+ "-user",
156
+ "-uid",
157
+ "-group",
158
+ "-gid",
159
+ "-nouser",
160
+ "-nogroup",
161
+ "-samefile",
162
+ "-inum",
163
+ "-links",
164
+ "-print",
165
+ "-print0",
166
+ "-printf",
167
+ "-ls",
168
+ "-quit",
169
+ "-prune",
170
+ "-follow",
171
+ "-noleaf",
172
+ "-ignore_readdir_race",
173
+ "-noignore_readdir_race",
174
+ "-true",
175
+ "-false",
176
+ "-not",
177
+ "-and",
178
+ "-or",
179
+ "-o",
180
+ "-a",
181
+ "-P",
182
+ "-L",
183
+ "-H",
184
+ "-D",
185
+ "-O"
186
+ ]);
187
+ var SAFE_REDIRECTION = /\s*(?:[0-9]*>&[0-9-]+|(?:[0-9]*|&)>>?\s*\/dev\/null)/g;
188
+ var basenameOf = (token) => {
189
+ const slash = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
190
+ return slash === -1 ? token : token.slice(slash + 1);
191
+ };
192
+ var TRUSTED_EXE_PREFIXES = [
193
+ "/bin/",
194
+ "/sbin/",
195
+ "/usr/bin/",
196
+ "/usr/sbin/",
197
+ "/usr/local/bin/",
198
+ "/opt/homebrew/bin/"
199
+ ];
200
+ var isTrustedExecutable = (first) => {
201
+ if (!first.includes("/")) return true;
202
+ if (first.includes("..")) return false;
203
+ return TRUSTED_EXE_PREFIXES.some((prefix) => first.startsWith(prefix));
204
+ };
205
+ var tokenizeShellCommand = (command) => {
206
+ const tokens = [];
207
+ let current = "";
208
+ let quote = null;
209
+ let started = false;
210
+ for (const ch of command) {
211
+ if (quote) {
212
+ if (ch === quote) quote = null;
213
+ else current += ch;
214
+ started = true;
215
+ continue;
216
+ }
217
+ if (ch === '"' || ch === "'") {
218
+ quote = ch;
219
+ started = true;
220
+ continue;
221
+ }
222
+ if (ch === " " || ch === " ") {
223
+ if (started) {
224
+ tokens.push(current);
225
+ current = "";
226
+ started = false;
227
+ }
228
+ continue;
229
+ }
230
+ current += ch;
231
+ started = true;
232
+ }
233
+ if (quote) return null;
234
+ if (started) tokens.push(current);
235
+ return tokens;
236
+ };
237
+ var isSafeSimpleCommand = (segment) => {
238
+ const tokens = tokenizeShellCommand(segment.trim());
239
+ if (!tokens || tokens.length === 0) return false;
240
+ const first = tokens[0];
241
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(first) || first.includes("$")) return false;
242
+ if (!isTrustedExecutable(first)) return false;
243
+ const exe = basenameOf(first);
244
+ if (exe === "git") {
245
+ const sub = tokens[1];
246
+ if (!sub || sub.startsWith("-")) return false;
247
+ if (!SAFE_GIT_SUBCOMMANDS.has(sub)) return false;
248
+ return !tokens.some(GIT_WRITE_FLAGS);
249
+ }
250
+ if (exe === "find") {
251
+ for (let i = 1; i < tokens.length; i += 1) {
252
+ const t = tokens[i];
253
+ if (t === "(" || t === ")" || t === "!" || t === "\\(" || t === "\\)" || t === "\\!") continue;
254
+ if (t.startsWith("-") && (DANGEROUS_FIND_ACTIONS.has(t) || !SAFE_FIND_TOKENS.has(t))) return false;
255
+ }
256
+ return true;
257
+ }
258
+ return SAFE_SHELL_COMMANDS.has(exe);
259
+ };
260
+ var isSafeReadOnlyCommand = (command) => {
261
+ const trimmed = command.trim();
262
+ if (!trimmed || trimmed.length > 2e3) return false;
263
+ if (/[`\n\r{}]/.test(trimmed)) return false;
264
+ if (/;|\|\|/.test(trimmed)) return false;
265
+ if (/\\(?![()!])/.test(trimmed)) return false;
266
+ if (/(?<!\\)[()]/.test(trimmed)) return false;
267
+ const noRedir = trimmed.replace(SAFE_REDIRECTION, " ");
268
+ if (/[<>]/.test(noRedir)) return false;
269
+ if (noRedir.replace(/&&/g, " ").includes("&")) return false;
270
+ for (const andPart of noRedir.split("&&")) {
271
+ if (!andPart.trim()) return false;
272
+ for (const segment of andPart.split("|")) {
273
+ if (!isSafeSimpleCommand(segment)) return false;
274
+ }
275
+ }
276
+ return true;
277
+ };
278
+ var API_KEY_PATTERN = /^pk_[a-f0-9]+\.[a-f0-9]+$/;
279
+ var isValidApiKey = (value) => API_KEY_PATTERN.test(value);
280
+ var ACTION_BODY_MAX = 4e3;
281
+ var DECISION_LINE_MAX = 500;
282
+ var SECRET_REDACTION_RULES = [
283
+ { pattern: /-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, replacement: "[redacted key]" },
284
+ { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: "[redacted]" },
285
+ { pattern: /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: "[redacted]" },
286
+ { pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
287
+ { pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: "[redacted]" },
288
+ { pattern: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, replacement: "[redacted]" },
289
+ { pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, replacement: "[redacted]" },
290
+ { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[redacted]" },
291
+ { pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, replacement: "[redacted]" },
292
+ { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[redacted]" },
293
+ { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: "[redacted]" },
294
+ { pattern: /\bxai-[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
295
+ { pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: "[redacted]" },
296
+ { pattern: /\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, replacement: "bearer [redacted]" },
297
+ { pattern: /\bauthorization:\s*\S+/gi, replacement: "authorization: [redacted]" },
298
+ {
299
+ pattern: /((?:secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/gi,
300
+ replacement: "$1[redacted]"
301
+ }
302
+ ];
303
+ var HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: "[redacted]" };
304
+ var redactSecrets = (text) => SECRET_REDACTION_RULES.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
305
+ var redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
306
+
307
+ export {
308
+ isApprovalMode,
309
+ HOOK_BUDGETS,
310
+ hookWaitDeadline,
311
+ hookWaitClamped,
312
+ effectiveWaitSeconds,
313
+ matchRankWeight,
314
+ matchToolPattern,
315
+ extractPolicyArg,
316
+ isSafeReadOnlyCommand,
317
+ isValidApiKey,
318
+ ACTION_BODY_MAX,
319
+ DECISION_LINE_MAX,
320
+ redactSecrets,
321
+ redactSecretsDeep
322
+ };
@@ -0,0 +1,244 @@
1
+ // src/codex-config.ts
2
+ import { createHash } from "crypto";
3
+ var CODEX_HOOK_BINARY = "pushary-codex-hook";
4
+ var CODEX_HOOK_EVENTS = [
5
+ { event: "PermissionRequest", matcher: "Bash|apply_patch", timeout: 180, statusMessage: "Waiting for your phone" },
6
+ { event: "PreToolUse", matcher: "Bash|apply_patch", timeout: 180, statusMessage: "Checking Pushary policy" },
7
+ { event: "PostToolUse", matcher: "Bash|apply_patch", timeout: 10 },
8
+ { event: "UserPromptSubmit", timeout: 10 },
9
+ { event: "Stop", timeout: 10 },
10
+ { event: "SessionStart", matcher: "startup|resume", timeout: 10 }
11
+ ];
12
+ var CODEX_EVENT_KEY = {
13
+ PermissionRequest: "permission_request",
14
+ PreToolUse: "pre_tool_use",
15
+ PostToolUse: "post_tool_use",
16
+ UserPromptSubmit: "user_prompt_submit",
17
+ Stop: "stop",
18
+ SessionStart: "session_start"
19
+ };
20
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
21
+ var ensureRecord = (target, key) => {
22
+ const existing = asRecord(target[key]);
23
+ if (existing) return existing;
24
+ const created = {};
25
+ target[key] = created;
26
+ return created;
27
+ };
28
+ var isPusharyCodexHook = (entry) => {
29
+ const hooks = asRecord(entry)?.hooks;
30
+ if (!Array.isArray(hooks)) return false;
31
+ return hooks.some((hook) => String(asRecord(hook)?.command ?? "").includes(CODEX_HOOK_BINARY));
32
+ };
33
+ var addCodexHooks = (config, command) => {
34
+ const hooks = ensureRecord(config, "hooks");
35
+ for (const definition of CODEX_HOOK_EVENTS) {
36
+ const existing = Array.isArray(hooks[definition.event]) ? hooks[definition.event] : [];
37
+ const entries = existing.filter((entry) => !isPusharyCodexHook(entry));
38
+ entries.push({
39
+ ...definition.matcher ? { matcher: definition.matcher } : {},
40
+ hooks: [{
41
+ type: "command",
42
+ command,
43
+ timeout: definition.timeout,
44
+ ...definition.statusMessage ? { statusMessage: definition.statusMessage } : {}
45
+ }]
46
+ });
47
+ hooks[definition.event] = entries;
48
+ }
49
+ };
50
+ var removeCodexHooks = (config) => {
51
+ const hooks = asRecord(config.hooks);
52
+ if (!hooks) return false;
53
+ let changed = false;
54
+ for (const definition of CODEX_HOOK_EVENTS) {
55
+ const entries = hooks[definition.event];
56
+ if (!Array.isArray(entries)) continue;
57
+ const filtered = entries.filter((entry) => !isPusharyCodexHook(entry));
58
+ if (filtered.length !== entries.length) {
59
+ if (filtered.length === 0) {
60
+ delete hooks[definition.event];
61
+ } else {
62
+ hooks[definition.event] = filtered;
63
+ }
64
+ changed = true;
65
+ }
66
+ }
67
+ if (Object.keys(hooks).length === 0) delete config.hooks;
68
+ return changed;
69
+ };
70
+ var hasCodexHooks = (config) => {
71
+ const hooks = asRecord(config.hooks);
72
+ if (!hooks) return false;
73
+ return CODEX_HOOK_EVENTS.some((definition) => {
74
+ const entries = hooks[definition.event];
75
+ return Array.isArray(entries) && entries.some(isPusharyCodexHook);
76
+ });
77
+ };
78
+ var missingCodexHookEvents = (config) => {
79
+ const hooks = asRecord(config.hooks);
80
+ return CODEX_HOOK_EVENTS.filter((definition) => {
81
+ const entries = hooks?.[definition.event];
82
+ return !Array.isArray(entries) || !entries.some(isPusharyCodexHook);
83
+ }).map((definition) => definition.event);
84
+ };
85
+ var canonicalJson = (value) => {
86
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
87
+ if (Array.isArray(value)) return "[" + value.map(canonicalJson).join(",") + "]";
88
+ const obj = value;
89
+ return "{" + Object.keys(obj).sort().map((key) => JSON.stringify(key) + ":" + canonicalJson(obj[key])).join(",") + "}";
90
+ };
91
+ var codexHookTrustHash = (definition, command) => {
92
+ const hook = { async: false, command, timeout: definition.timeout, type: "command" };
93
+ if (definition.statusMessage) hook.statusMessage = definition.statusMessage;
94
+ const identity = { event_name: CODEX_EVENT_KEY[definition.event], hooks: [hook] };
95
+ if (definition.matcher) identity.matcher = definition.matcher;
96
+ return "sha256:" + createHash("sha256").update(canonicalJson(identity), "utf8").digest("hex");
97
+ };
98
+ var codexHookStateKey = (hooksJsonPath, event) => `${hooksJsonPath}:${CODEX_EVENT_KEY[event]}:0:0`;
99
+ var addCodexHookTrust = (config, hooksJsonPath, command) => {
100
+ const hooks = ensureRecord(config, "hooks");
101
+ const state = ensureRecord(hooks, "state");
102
+ for (const definition of CODEX_HOOK_EVENTS) {
103
+ const key = codexHookStateKey(hooksJsonPath, definition.event);
104
+ const existing = asRecord(state[key]) ?? {};
105
+ state[key] = { ...existing, trusted_hash: codexHookTrustHash(definition, command) };
106
+ }
107
+ };
108
+
109
+ // src/gemini-config.ts
110
+ var GEMINI_HOOK_BINARY = "pushary-gemini-hook";
111
+ var GEMINI_MCP_URL = "https://pushary.com/api/mcp/mcp";
112
+ var GEMINI_HOOK_EVENTS = [
113
+ { event: "BeforeTool", matcher: "run_shell_command|write_file|replace", timeoutMs: 18e4 },
114
+ { event: "AfterTool", matcher: "run_shell_command|write_file|replace", timeoutMs: 1e4 },
115
+ { event: "BeforeAgent", timeoutMs: 1e4 },
116
+ { event: "SessionStart", timeoutMs: 1e4 },
117
+ { event: "SessionEnd", timeoutMs: 1e4 }
118
+ ];
119
+ var asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
120
+ var ensureRecord2 = (target, key) => {
121
+ const existing = asRecord2(target[key]);
122
+ if (existing) return existing;
123
+ const created = {};
124
+ target[key] = created;
125
+ return created;
126
+ };
127
+ var isPusharyGeminiHook = (entry) => {
128
+ const hooks = asRecord2(entry)?.hooks;
129
+ if (!Array.isArray(hooks)) return false;
130
+ return hooks.some((hook) => String(asRecord2(hook)?.command ?? "").includes(GEMINI_HOOK_BINARY));
131
+ };
132
+ var addGeminiMcpServer = (settings, apiKey) => {
133
+ const mcpServers = ensureRecord2(settings, "mcpServers");
134
+ mcpServers.pushary = {
135
+ httpUrl: GEMINI_MCP_URL,
136
+ headers: { Authorization: `Bearer ${apiKey}` },
137
+ trust: true
138
+ };
139
+ };
140
+ var addGeminiHooks = (settings, command) => {
141
+ const hooks = ensureRecord2(settings, "hooks");
142
+ for (const definition of GEMINI_HOOK_EVENTS) {
143
+ const existing = Array.isArray(hooks[definition.event]) ? hooks[definition.event] : [];
144
+ const entries = existing.filter((entry) => !isPusharyGeminiHook(entry));
145
+ entries.push({
146
+ ...definition.matcher ? { matcher: definition.matcher } : {},
147
+ hooks: [{
148
+ name: "pushary",
149
+ type: "command",
150
+ command,
151
+ timeout: definition.timeoutMs
152
+ }]
153
+ });
154
+ hooks[definition.event] = entries;
155
+ }
156
+ };
157
+ var removeGeminiHooks = (settings) => {
158
+ const hooks = asRecord2(settings.hooks);
159
+ if (!hooks) return false;
160
+ let changed = false;
161
+ for (const definition of GEMINI_HOOK_EVENTS) {
162
+ const entries = hooks[definition.event];
163
+ if (!Array.isArray(entries)) continue;
164
+ const filtered = entries.filter((entry) => !isPusharyGeminiHook(entry));
165
+ if (filtered.length !== entries.length) {
166
+ if (filtered.length === 0) {
167
+ delete hooks[definition.event];
168
+ } else {
169
+ hooks[definition.event] = filtered;
170
+ }
171
+ changed = true;
172
+ }
173
+ }
174
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
175
+ return changed;
176
+ };
177
+ var removeGeminiMcpServer = (settings) => {
178
+ const mcpServers = asRecord2(settings.mcpServers);
179
+ if (!mcpServers?.pushary) return false;
180
+ delete mcpServers.pushary;
181
+ if (Object.keys(mcpServers).length === 0) delete settings.mcpServers;
182
+ return true;
183
+ };
184
+ var removeGeminiSettings = (settings) => {
185
+ const mcpRemoved = removeGeminiMcpServer(settings);
186
+ const hooksRemoved = removeGeminiHooks(settings);
187
+ return mcpRemoved || hooksRemoved;
188
+ };
189
+ var hasGeminiHooks = (settings) => {
190
+ const hooks = asRecord2(settings.hooks);
191
+ if (!hooks) return false;
192
+ return GEMINI_HOOK_EVENTS.some((definition) => {
193
+ const entries = hooks[definition.event];
194
+ return Array.isArray(entries) && entries.some(isPusharyGeminiHook);
195
+ });
196
+ };
197
+ var missingGeminiHookEvents = (settings) => {
198
+ const hooks = asRecord2(settings.hooks);
199
+ return GEMINI_HOOK_EVENTS.filter((definition) => {
200
+ const entries = hooks?.[definition.event];
201
+ return !Array.isArray(entries) || !entries.some(isPusharyGeminiHook);
202
+ }).map((definition) => definition.event);
203
+ };
204
+
205
+ // src/npm.ts
206
+ import { execSync } from "child_process";
207
+ var cleanNpmEnv = () => {
208
+ const env = {};
209
+ for (const [key, value] of Object.entries(process.env)) {
210
+ if (key.toLowerCase().startsWith("npm_config_workspace")) continue;
211
+ env[key] = value;
212
+ }
213
+ return env;
214
+ };
215
+ var npmErrorMessage = (err) => {
216
+ const e = err;
217
+ const text = [e?.stderr, e?.stdout].map((part) => part ? part.toString() : "").join("\n");
218
+ const line = text.split("\n").map((l) => l.replace(/^npm error\s*/i, "").trim()).find((l) => l && !l.startsWith("A complete log") && !/^code\s/i.test(l));
219
+ return line || e?.message || String(err);
220
+ };
221
+ var execNpm = (args, options = {}) => {
222
+ return execSync(`npm ${args}`, {
223
+ timeout: 12e4,
224
+ stdio: "pipe",
225
+ ...options,
226
+ env: { ...cleanNpmEnv(), ...options.env ?? {} }
227
+ });
228
+ };
229
+
230
+ export {
231
+ addCodexHooks,
232
+ removeCodexHooks,
233
+ hasCodexHooks,
234
+ missingCodexHookEvents,
235
+ addCodexHookTrust,
236
+ GEMINI_HOOK_BINARY,
237
+ addGeminiMcpServer,
238
+ addGeminiHooks,
239
+ removeGeminiSettings,
240
+ hasGeminiHooks,
241
+ missingGeminiHookEvents,
242
+ npmErrorMessage,
243
+ execNpm
244
+ };
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getApiKey,
4
+ getBaseUrl
5
+ } from "./chunk-VUNL35KE.js";
6
+
7
+ // bin/pushary-mode.ts
8
+ var VALID_MODES = ["push_only", "push_first", "terminal_only", "notify_only"];
9
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
10
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
11
+ var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
12
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
13
+ var parseDuration = (value) => {
14
+ const match = value.match(/^(\d+)(m|h)$/);
15
+ if (!match) return null;
16
+ const num = Number(match[1]);
17
+ return match[2] === "h" ? num * 3600 : num * 60;
18
+ };
19
+ var main = async () => {
20
+ const apiKey = getApiKey();
21
+ const baseUrl = getBaseUrl();
22
+ const mode = process.argv[2];
23
+ const forFlag = process.argv[3];
24
+ const forValue = process.argv[4];
25
+ const headers = {
26
+ "Authorization": `Bearer ${apiKey}`,
27
+ "Content-Type": "application/json"
28
+ };
29
+ if (!mode || mode === "status") {
30
+ const res2 = await fetch(`${baseUrl}/api/mcp/mode`, { headers });
31
+ const data2 = await res2.json();
32
+ if (!data2.override) {
33
+ console.log(` Mode: ${cyan("default")} ${dim("(using per-tool policies)")}`);
34
+ } else {
35
+ console.log(` Mode: ${green(data2.override.mode)}`);
36
+ if (data2.override.expiresAt) {
37
+ console.log(` Expires: ${new Date(data2.override.expiresAt).toLocaleString()}`);
38
+ } else {
39
+ console.log(` ${dim("Sticky (no expiry)")}`);
40
+ }
41
+ }
42
+ return;
43
+ }
44
+ if (mode === "clear" || mode === "reset") {
45
+ await fetch(`${baseUrl}/api/mcp/mode`, { method: "DELETE", headers });
46
+ console.log(` ${green("\u2713")} Mode override cleared \u2014 using per-tool policies`);
47
+ return;
48
+ }
49
+ if (!VALID_MODES.includes(mode)) {
50
+ console.log(` ${yellow("!")} Invalid mode: ${mode}`);
51
+ console.log(` Valid modes: ${VALID_MODES.join(", ")}`);
52
+ console.log(` ${dim("Usage: pushary mode <mode> [--for <duration>]")}`);
53
+ console.log(` ${dim("Example: pushary mode push_only --for 30m")}`);
54
+ return;
55
+ }
56
+ let ttlSeconds;
57
+ if (forFlag === "--for" && forValue) {
58
+ ttlSeconds = parseDuration(forValue);
59
+ if (!ttlSeconds) {
60
+ console.log(` ${yellow("!")} Invalid duration: ${forValue} ${dim("(use e.g. 30m, 1h)")}`);
61
+ return;
62
+ }
63
+ }
64
+ const res = await fetch(`${baseUrl}/api/mcp/mode`, {
65
+ method: "PUT",
66
+ headers,
67
+ body: JSON.stringify({ mode, ttlSeconds })
68
+ });
69
+ if (!res.ok) {
70
+ const err = await res.json();
71
+ console.log(` ${yellow("!")} Failed: ${err.error ?? res.statusText}`);
72
+ return;
73
+ }
74
+ const data = await res.json();
75
+ console.log(` ${green("\u2713")} Mode set to ${cyan(data.override.mode)}`);
76
+ if (data.override.expiresAt) {
77
+ console.log(` Expires: ${new Date(data.override.expiresAt).toLocaleString()}`);
78
+ }
79
+ };
80
+ main().catch((err) => {
81
+ console.error(` ${yellow("!")} ${err instanceof Error ? err.message : err}`);
82
+ process.exit(1);
83
+ });