@primitive.ai/prim 0.1.0-alpha.9 → 0.1.0-alpha.90

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 (45) hide show
  1. package/README.md +295 -37
  2. package/SKILL.md +269 -0
  3. package/dist/chunk-2JRX7ZR4.js +247 -0
  4. package/dist/chunk-3EEFL6NJ.js +529 -0
  5. package/dist/chunk-4GLVJLEQ.js +55 -0
  6. package/dist/chunk-6IWNH776.js +77 -0
  7. package/dist/chunk-C7T7B45R.js +1400 -0
  8. package/dist/chunk-CUE2LKLR.js +155 -0
  9. package/dist/chunk-EZXEWFDG.js +210 -0
  10. package/dist/chunk-FI3LIRCB.js +262 -0
  11. package/dist/chunk-LEMX3IOG.js +79 -0
  12. package/dist/chunk-LMRYZL3D.js +209 -0
  13. package/dist/chunk-MAZWKJGL.js +18548 -0
  14. package/dist/chunk-PMRDE2DA.js +310 -0
  15. package/dist/chunk-PPQYQEIR.js +815 -0
  16. package/dist/chunk-R26A7TWY.js +36 -0
  17. package/dist/chunk-RO3F6JYV.js +126 -0
  18. package/dist/chunk-UZPBB7V4.js +505 -0
  19. package/dist/chunk-WB7AUZ4C.js +52 -0
  20. package/dist/chunk-YWO7O6QG.js +327 -0
  21. package/dist/chunk-Z7YPFWAZ.js +917 -0
  22. package/dist/daemon/server.js +21633 -0
  23. package/dist/hook-runtime/daemon/server.js +21633 -0
  24. package/dist/hook-runtime/hooks/post-commit.js +19258 -0
  25. package/dist/hook-runtime/hooks/post-rewrite.js +19273 -0
  26. package/dist/hook-runtime/hooks/post-tool-use.js +25846 -0
  27. package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
  28. package/dist/hook-runtime/hooks/pre-tool-use.js +25284 -0
  29. package/dist/hook-runtime/hooks/prim-hook.js +25870 -0
  30. package/dist/hook-runtime/hooks/session-end.js +495 -0
  31. package/dist/hook-runtime/hooks/session-start.js +28180 -0
  32. package/dist/hook-runtime/index.js +50516 -0
  33. package/dist/hook-runtime/statusline-main.js +765 -0
  34. package/dist/hooks/post-commit.js +188 -0
  35. package/dist/hooks/post-rewrite.js +201 -0
  36. package/dist/hooks/post-tool-use.js +263 -0
  37. package/dist/hooks/pre-commit.js +16 -101
  38. package/dist/hooks/pre-tool-use.js +367 -0
  39. package/dist/hooks/prim-hook.js +207 -0
  40. package/dist/hooks/session-end.js +70 -0
  41. package/dist/hooks/session-start.js +289 -0
  42. package/dist/index.js +9749 -425
  43. package/dist/statusline-main.js +765 -0
  44. package/package.json +35 -13
  45. package/dist/chunk-3APLWTLB.js +0 -165
@@ -0,0 +1,815 @@
1
+ // src/hooks/pre-tool-use-scoring.ts
2
+ import { isAbsolute, relative, sep } from "path";
3
+ function anyUnverified(results) {
4
+ return results.some((r) => r.verdict === "unavailable" || r.truncated);
5
+ }
6
+ function unverifiedNote(results) {
7
+ const causes = [];
8
+ const unavailable = results.find((r) => r.verdict === "unavailable");
9
+ if (unavailable) {
10
+ causes.push(
11
+ unavailable.unavailable ? `decision check skipped \u2014 ${unavailable.unavailable}` : "decision check skipped \u2014 not verified"
12
+ );
13
+ }
14
+ if (results.some((r) => r.truncated)) {
15
+ causes.push("decision check partial \u2014 conflict set truncated (per-file cap hit)");
16
+ }
17
+ return causes.map((c) => `[primitive] ${c}`).join("\n");
18
+ }
19
+ function blockingReason(results) {
20
+ const parts = results.filter((result) => result.verdict === "ask" || result.verdict === "deny").flatMap((result) => [result.reason, result.additionalContext]).filter((value) => value.length > 0);
21
+ return [...new Set(parts)].join("\n\n") || "[primitive] conflict detected (no detail available)";
22
+ }
23
+ function advisoryNote(aggregate, results) {
24
+ const details = results.flatMap((result) => [
25
+ ...aggregate === "warn" ? [result.reason] : [],
26
+ result.additionalContext
27
+ ]);
28
+ if (anyUnverified(results)) details.push(unverifiedNote(results));
29
+ return [...new Set(details.filter((value) => value.length > 0))].join("\n\n");
30
+ }
31
+ function buildHookOutput(aggregate, results) {
32
+ if (aggregate === "deny" || aggregate === "ask") {
33
+ return {
34
+ hookSpecificOutput: {
35
+ hookEventName: "PreToolUse",
36
+ permissionDecision: aggregate,
37
+ permissionDecisionReason: blockingReason(results)
38
+ }
39
+ };
40
+ }
41
+ const notes = advisoryNote(aggregate, results);
42
+ if (notes.length > 0) {
43
+ return {
44
+ systemMessage: notes,
45
+ hookSpecificOutput: {
46
+ hookEventName: "PreToolUse",
47
+ permissionDecision: "allow",
48
+ additionalContext: notes
49
+ }
50
+ };
51
+ }
52
+ return {
53
+ hookSpecificOutput: {
54
+ hookEventName: "PreToolUse",
55
+ permissionDecision: "allow"
56
+ }
57
+ };
58
+ }
59
+ function buildCodexOutput(aggregate, results) {
60
+ if (aggregate === "deny" || aggregate === "ask") {
61
+ return {
62
+ hookSpecificOutput: {
63
+ hookEventName: "PreToolUse",
64
+ permissionDecision: "deny",
65
+ permissionDecisionReason: blockingReason(results)
66
+ }
67
+ };
68
+ }
69
+ const notes = advisoryNote(aggregate, results);
70
+ return notes ? {
71
+ systemMessage: notes,
72
+ hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: notes }
73
+ } : {};
74
+ }
75
+ function failOpenOutput() {
76
+ return {
77
+ hookSpecificOutput: {
78
+ hookEventName: "PreToolUse",
79
+ permissionDecision: "allow"
80
+ }
81
+ };
82
+ }
83
+ function failOpenCodex() {
84
+ return {};
85
+ }
86
+ function buildHermesOutput(aggregate, results) {
87
+ if (aggregate !== "deny" && aggregate !== "ask") {
88
+ return {};
89
+ }
90
+ const reason = results.filter((r) => r.verdict === "deny" || r.verdict === "ask").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
91
+ const directive = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
92
+ const message = [reason, directive].filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
93
+ return { action: "block", message };
94
+ }
95
+ function failOpenHermes() {
96
+ return {};
97
+ }
98
+ var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
99
+ var APPLY_PATCH_FILE_RE = /^\*\*\* (?:Update|Add|Delete) File: (?<path>.+)$/;
100
+ var MOVE_FILE_RE = /^\*\*\* Move File:\s*(?<src>.+?)\s*->\s*(?<dst>.+?)\s*$/;
101
+ var MOVE_TO_RE = /^\*\*\* Move to: (?<dst>.+)$/;
102
+ var LINE_SPLIT_RE = /\r?\n/;
103
+ function parseApplyPatchTargets(command) {
104
+ const paths = /* @__PURE__ */ new Set();
105
+ let activeUpdate = false;
106
+ let complete = true;
107
+ for (const line of command.split(LINE_SPLIT_RE)) {
108
+ const path = APPLY_PATCH_FILE_RE.exec(line)?.groups?.path;
109
+ if (path) {
110
+ paths.add(path);
111
+ activeUpdate = line.startsWith("*** Update File:");
112
+ continue;
113
+ }
114
+ const moveTo = MOVE_TO_RE.exec(line)?.groups?.dst;
115
+ if (moveTo) {
116
+ if (activeUpdate) paths.add(moveTo);
117
+ else complete = false;
118
+ activeUpdate = false;
119
+ continue;
120
+ }
121
+ const move = MOVE_FILE_RE.exec(line)?.groups;
122
+ if (move?.src) {
123
+ paths.add(move.src);
124
+ if (move.dst) {
125
+ paths.add(move.dst);
126
+ }
127
+ activeUpdate = false;
128
+ continue;
129
+ }
130
+ if (line === "*** Begin Patch" || line === "*** End Patch") activeUpdate = false;
131
+ else if (line.startsWith("*** ")) complete = false;
132
+ }
133
+ return { paths: [...paths], complete: complete && paths.size > 0 };
134
+ }
135
+ function extractCodexFileTargets(toolName, toolInput) {
136
+ if (toolName !== "apply_patch") {
137
+ return null;
138
+ }
139
+ const text = codexPatchText(toolInput);
140
+ return text !== null ? parseApplyPatchTargets(text) : { paths: [], complete: false };
141
+ }
142
+ function codexPatchText(toolInput) {
143
+ if (typeof toolInput === "string") return toolInput;
144
+ if (!toolInput || typeof toolInput !== "object") return null;
145
+ const input = toolInput;
146
+ for (const key of ["command", "input", "patch"]) {
147
+ const value = input[key];
148
+ if (typeof value === "string") return value;
149
+ }
150
+ return null;
151
+ }
152
+ var HERMES_EDITING_TOOLS = /* @__PURE__ */ new Set(["write_file", "patch"]);
153
+ function extractHermesFileTargets(toolName, toolInput) {
154
+ if (!HERMES_EDITING_TOOLS.has(toolName)) {
155
+ return null;
156
+ }
157
+ if (!toolInput || typeof toolInput !== "object") {
158
+ return { paths: [], complete: false };
159
+ }
160
+ const input = toolInput;
161
+ if (toolName === "patch" && input.mode === "patch") {
162
+ return typeof input.patch === "string" ? parseApplyPatchTargets(input.patch) : { paths: [], complete: false };
163
+ }
164
+ const paths = typeof input.path === "string" && input.path.length > 0 ? [input.path] : [];
165
+ const mode = input.mode;
166
+ return { paths, complete: paths.length > 0 && (mode === void 0 || mode === "replace") };
167
+ }
168
+ function extractFileTargets(toolName, toolInput, agent = "claude_code") {
169
+ if (agent === "codex") {
170
+ return extractCodexFileTargets(toolName, toolInput);
171
+ }
172
+ if (agent === "hermes") {
173
+ return extractHermesFileTargets(toolName, toolInput);
174
+ }
175
+ if (!SUPPORTED_TOOLS.has(toolName)) {
176
+ return null;
177
+ }
178
+ if (!toolInput || typeof toolInput !== "object") {
179
+ return { paths: [], complete: false };
180
+ }
181
+ const input = toolInput;
182
+ if (toolName === "NotebookEdit") {
183
+ const paths2 = typeof input.notebook_path === "string" && input.notebook_path.length > 0 ? [input.notebook_path] : [];
184
+ return { paths: paths2, complete: paths2.length > 0 };
185
+ }
186
+ const paths = typeof input.file_path === "string" && input.file_path.length > 0 ? [input.file_path] : [];
187
+ return { paths, complete: paths.length > 0 };
188
+ }
189
+ function readHookMode(env) {
190
+ if (env.PRIM_BYPASS === "1" || env.PRIM_BYPASS === "true") {
191
+ return "off";
192
+ }
193
+ const mode = env.PRIM_HOOK_MODE;
194
+ if (mode === "off" || mode === "warn") {
195
+ return mode;
196
+ }
197
+ return "block";
198
+ }
199
+ function demoteForMode(verdict, mode) {
200
+ if (mode === "off") {
201
+ return "allow";
202
+ }
203
+ if (mode === "warn" && (verdict === "ask" || verdict === "deny")) {
204
+ return "warn";
205
+ }
206
+ return verdict;
207
+ }
208
+
209
+ // src/hooks/shell-targets.ts
210
+ import { parse } from "unbash";
211
+ var OUTPUT_REDIRECTS = /* @__PURE__ */ new Set([">", ">>", ">|", "&>", "&>>"]);
212
+ var READ_ONLY_COMMANDS = new Set(": cat echo false printf pwd test true [".split(" "));
213
+ var SINKS = /* @__PURE__ */ new Set(["/dev/null", "/dev/stdout", "/dev/stderr"]);
214
+ var STATIC_PARTS = /* @__PURE__ */ new Set(["Literal", "SingleQuoted", "AnsiCQuoted"]);
215
+ var set = (values) => new Set(values.split(" ").filter(Boolean));
216
+ var INFO_OPTIONS = set("--help --version");
217
+ var COMMAND_OPTIONS = {
218
+ rg: {
219
+ shortFlags: "0FHILNPSUabchilnopqsvwx",
220
+ shortValues: "ABCEgjMmrtTef",
221
+ longFlags: set(
222
+ "--binary --case-sensitive --column --count --count-matches --crlf --debug --files --files-with-matches --files-without-match --fixed-strings --follow --heading --hidden --ignore-case --invert-match --json --line-number --line-regexp --multiline --multiline-dotall --no-filename --no-heading --no-hidden --no-ignore --no-ignore-parent --no-ignore-vcs --no-line-number --null --null-data --one-file-system --only-matching --passthru --pcre2 --pretty --quiet --smart-case --stats --text --trim --unrestricted --with-filename --word-regexp"
223
+ ),
224
+ longValues: set(
225
+ "--after-context --before-context --context --dfa-size-limit --encoding --engine --file --glob --iglob --max-columns --max-count --max-depth --path-separator --regexp --regex-size-limit --replace --sort --sortr --threads --type --type-add --type-clear --type-not"
226
+ ),
227
+ longOptionalValues: set("--color")
228
+ },
229
+ grep: {
230
+ shortFlags: "EFGHIPRTUVabcdhLlnqrsuvwxyZz",
231
+ shortValues: "ABCDefm",
232
+ longFlags: set(
233
+ "--basic-regexp --binary-files-without-match --byte-offset --count --extended-regexp --files-with-matches --files-without-match --fixed-regexp --fixed-strings --help --ignore-case --initial-tab --invert-match --line-buffered --line-number --line-regexp --null --null-data --only-matching --perl-regexp --quiet --recursive --silent --unix-byte-offsets --version --with-filename --word-regexp"
234
+ ),
235
+ longValues: set(
236
+ "--after-context --before-context --binary-files --context --devices --directories --exclude --exclude-dir --exclude-from --file --include --label --max-count --regexp"
237
+ ),
238
+ longOptionalValues: set("--color")
239
+ },
240
+ head: {
241
+ shortFlags: "qvz",
242
+ shortValues: "cn",
243
+ longFlags: set("--help --quiet --verbose --version --zero-terminated"),
244
+ longValues: set("--bytes --lines")
245
+ },
246
+ tail: {
247
+ shortFlags: "Fqsvz",
248
+ shortValues: "cn",
249
+ longFlags: set("--help --quiet --retry --silent --verbose --version --zero-terminated"),
250
+ longValues: set("--bytes --lines --max-unchanged-stats --pid --sleep-interval"),
251
+ longOptionalValues: set("--follow")
252
+ },
253
+ wc: {
254
+ shortFlags: "cLlmw",
255
+ longFlags: set("--bytes --chars --help --lines --max-line-length --version --words"),
256
+ longValues: set("--files0-from")
257
+ },
258
+ ls: {
259
+ shortFlags: "ABCDFGHILNQRSTUXZabcdfghiklmnopqrstuvwxy",
260
+ shortValues: "ITw",
261
+ longFlags: set(
262
+ "--all --almost-all --author --classify --dereference --directory --dired --file-type --group-directories-first --help --hide-control-chars --human-readable --ignore-backups --inode --kibibytes --literal --no-group --numeric-uid-gid --quote-name --recursive --reverse --show-control-chars --size --version"
263
+ ),
264
+ longValues: set(
265
+ "--block-size --format --hide --ignore --indicator-style --quoting-style --sort --tabsize --time --time-style --width"
266
+ ),
267
+ longOptionalValues: set("--color --hyperlink")
268
+ }
269
+ };
270
+ var GIT_INSPECTION_OPTIONS = {
271
+ shortFlags: "abcdfhiklmnprstuvwz",
272
+ shortValues: "ABCGLMSU",
273
+ longFlags: set(
274
+ "--abbrev-commit --all --binary --branches --cached --check --children --date-order --deduplicate --deleted --error-unmatch --exclude-standard --exit-code --files-with-matches --full-history --full-index --full-name --graph --heading --ignore-all-space --ignore-blank-lines --ignore-space-at-eol --ignore-space-change --ignored --is-bare-repository --is-inside-git-dir --is-inside-work-tree --killed --left-only --line-number --merge-base --merges --modified --name-only --name-status --no-abbrev-commit --no-color --no-decorate --no-ext-diff --no-renames --no-textconv --numstat --objects --oneline --others --patch --quiet --remotes --right-only --root --short --shortstat --show-current --show-toplevel --stage --staged --stat --summary --symbolic-full-name --tags --topo-order --unmerged --verify --zero-commit"
275
+ ),
276
+ longValues: set(
277
+ "--after --author --before --committer --date --decorate-refs-exclude --diff-filter --exclude --format --git-path --grep --max-count --max-depth --max-parents --min-parents --object-format --path-format --pretty --since --skip --sort --until"
278
+ ),
279
+ longOptionalValues: set(
280
+ "--abbrev --batch --batch-check --color --decorate --relative --word-diff"
281
+ )
282
+ };
283
+ var GIT_METADATA_OPTIONS = {
284
+ add: {
285
+ shortFlags: "Afnpuv",
286
+ longFlags: set(
287
+ "--all --dry-run --force --ignore-errors --ignore-missing --intent-to-add --no-all --no-ignore-removal --pathspec-file-nul --refresh --renormalize --sparse --update --verbose"
288
+ ),
289
+ longValues: set("--chmod --pathspec-from-file")
290
+ },
291
+ commit: {
292
+ shortFlags: "anqsu",
293
+ shortValues: "CFm",
294
+ longFlags: set(
295
+ "--all --allow-empty --allow-empty-message --amend --branch --dry-run --include --long --no-edit --no-post-rewrite --no-status --no-verify --null --only --pathspec-file-nul --porcelain --reset-author --short --signoff --status"
296
+ ),
297
+ longValues: set(
298
+ "--author --cleanup --date --file --fixup --message --pathspec-from-file --reuse-message --squash --trailer --untracked-files"
299
+ )
300
+ },
301
+ push: {
302
+ shortFlags: "fquv",
303
+ longFlags: set(
304
+ "--all --atomic --delete --dry-run --follow-tags --force --force-if-includes --mirror --no-verify --porcelain --prune --quiet --set-upstream --tags --verbose"
305
+ ),
306
+ longValues: set("--push-option --recurse-submodules"),
307
+ longOptionalValues: set("--force-with-lease")
308
+ },
309
+ branch: {
310
+ shortFlags: "adfMmrqv",
311
+ longFlags: set(
312
+ "--all --copy --delete --force --ignore-case --list --merged --move --no-color --no-contains --no-merged --remotes --show-current --verbose"
313
+ ),
314
+ longValues: set("--contains --format --points-at --sort"),
315
+ longOptionalValues: set("--color --column --track")
316
+ },
317
+ "update-index": {
318
+ shortFlags: "q",
319
+ longFlags: set(
320
+ "--add --again --assume-unchanged --force-remove --fsmonitor-valid --ignore-missing --index-info --info-only --no-assume-unchanged --no-fsmonitor-valid --refresh --remove --replace --skip-worktree --unresolve"
321
+ ),
322
+ longValues: set("--cacheinfo --chmod --index-version")
323
+ }
324
+ };
325
+ var GH_PR_OPTIONS = {
326
+ view: {
327
+ longFlags: set("--comments"),
328
+ longValues: set("--jq --json --template")
329
+ },
330
+ list: {
331
+ longFlags: set("--draft"),
332
+ longValues: set(
333
+ "--app --assignee --author --base --head --jq --json --label --limit --search --state --template"
334
+ )
335
+ },
336
+ status: {},
337
+ checks: {
338
+ longFlags: set("--fail-fast --required --watch"),
339
+ longValues: set("--interval --jq --json --template")
340
+ },
341
+ diff: {
342
+ longFlags: set("--name-only --patch"),
343
+ longValues: set("--color")
344
+ },
345
+ create: {
346
+ longFlags: set("--draft --fill --fill-first --fill-verbose --no-maintainer-edit"),
347
+ longValues: set(
348
+ "--assignee --base --body --body-file --head --label --milestone --project --recover --reviewer --template --title"
349
+ )
350
+ },
351
+ edit: {
352
+ longValues: set(
353
+ "--add-assignee --add-label --add-project --add-reviewer --base --body --body-file --milestone --remove-assignee --remove-label --remove-milestone --remove-project --remove-reviewer --title"
354
+ )
355
+ },
356
+ close: {
357
+ longValues: set("--comment")
358
+ },
359
+ reopen: {
360
+ longValues: set("--comment")
361
+ },
362
+ ready: {
363
+ longFlags: set("--undo")
364
+ },
365
+ review: {
366
+ longFlags: set("--approve --comment --request-changes"),
367
+ longValues: set("--body --body-file")
368
+ },
369
+ merge: {
370
+ shortFlags: "mdrs",
371
+ longFlags: set("--admin --auto --disable-auto --merge --rebase --squash"),
372
+ longValues: set("--author-email --body --body-file --match-head-commit --subject")
373
+ },
374
+ comment: {
375
+ longFlags: set("--delete-last --edit-last"),
376
+ longValues: set("--body --body-file")
377
+ },
378
+ lock: {
379
+ longValues: set("--reason")
380
+ },
381
+ unlock: {}
382
+ };
383
+ function staticPart(part) {
384
+ return STATIC_PARTS.has(part.type) || part.type === "DoubleQuoted" && part.parts.every(staticPart);
385
+ }
386
+ function staticWord(word) {
387
+ if (!word || !word.value || word.value.includes("\0") || word.value.includes("\n")) return;
388
+ if (word.parts ? !word.parts.every(staticPart) : word.value.startsWith("~") || /[*?\[$`]/.test(word.text))
389
+ return;
390
+ return word.value;
391
+ }
392
+ function parseOptions(args, spec) {
393
+ const result = {
394
+ operands: [],
395
+ seen: /* @__PURE__ */ new Set(),
396
+ values: /* @__PURE__ */ new Map()
397
+ };
398
+ let optionsEnded = false;
399
+ const recordValue = (option, value) => {
400
+ result.seen.add(option);
401
+ result.values.set(option, [...result.values.get(option) ?? [], value]);
402
+ };
403
+ for (let index = 0; index < args.length; index += 1) {
404
+ const arg = args[index];
405
+ if (optionsEnded || arg === "-" || !arg.startsWith("-")) {
406
+ result.operands.push(arg);
407
+ continue;
408
+ }
409
+ if (arg === "--") {
410
+ optionsEnded = true;
411
+ continue;
412
+ }
413
+ if (arg.startsWith("--")) {
414
+ const separator = arg.indexOf("=");
415
+ const option = separator === -1 ? arg : arg.slice(0, separator);
416
+ const attached = separator === -1 ? void 0 : arg.slice(separator + 1);
417
+ if (spec.longValues?.has(option)) {
418
+ const value = attached ?? args[++index];
419
+ if (!value) return;
420
+ recordValue(option, value);
421
+ } else if (spec.longOptionalValues?.has(option)) {
422
+ result.seen.add(option);
423
+ if (attached !== void 0) recordValue(option, attached);
424
+ } else if (spec.longFlags?.has(option) && attached === void 0) {
425
+ result.seen.add(option);
426
+ } else {
427
+ return;
428
+ }
429
+ continue;
430
+ }
431
+ for (let offset = 1; offset < arg.length; offset += 1) {
432
+ const option = `-${arg[offset]}`;
433
+ if (spec.shortValues?.includes(arg[offset])) {
434
+ const attached = arg.slice(offset + 1);
435
+ const value = attached || args[++index];
436
+ if (!value) return;
437
+ recordValue(option, value);
438
+ break;
439
+ }
440
+ if (!spec.shortFlags?.includes(arg[offset])) return;
441
+ result.seen.add(option);
442
+ }
443
+ }
444
+ return result;
445
+ }
446
+ function staticArgs(words) {
447
+ const args = [];
448
+ for (const word of words) {
449
+ const value = staticWord(word);
450
+ if (value === void 0) return;
451
+ args.push(value);
452
+ }
453
+ return args;
454
+ }
455
+ function safeSedProgram(program) {
456
+ if (/^(?:(?:\d+|\$)(?:,(?:\d+|\$))?)?[dDpPqQnN=]$/.test(program) || /^\/(?:\\.|[^/])*\/[dDpPqQnN=]$/.test(program))
457
+ return true;
458
+ const match = /^(?:(?:\d+|\$)(?:,(?:\d+|\$))?)?s(.)(.*)$/s.exec(program);
459
+ if (!match || /[\\\nA-Za-z0-9]/.test(match[1])) return false;
460
+ const delimiter = match[1];
461
+ const tail = match[2];
462
+ let offset = 0;
463
+ for (let section = 0; section < 2; section += 1) {
464
+ let closed = false;
465
+ while (offset < tail.length) {
466
+ if (tail[offset] === "\\") {
467
+ offset += 2;
468
+ } else if (tail[offset] === delimiter) {
469
+ offset += 1;
470
+ closed = true;
471
+ break;
472
+ } else {
473
+ offset += 1;
474
+ }
475
+ }
476
+ if (!closed) return false;
477
+ }
478
+ return /^[0-9gIpMm]*$/.test(tail.slice(offset));
479
+ }
480
+ function safeSed(args) {
481
+ if (args.length === 1 && INFO_OPTIONS.has(args[0])) return true;
482
+ const parsed = parseOptions(args, {
483
+ shortFlags: "Enrsu",
484
+ shortValues: "e",
485
+ longFlags: set("--quiet --regexp-extended --separate --silent --unbuffered"),
486
+ longValues: set("--expression")
487
+ });
488
+ if (!parsed) return false;
489
+ const optionPrograms = [
490
+ ...parsed.values.get("-e") ?? [],
491
+ ...parsed.values.get("--expression") ?? []
492
+ ];
493
+ const programs = optionPrograms.length > 0 ? optionPrograms : parsed.operands.length > 0 ? [parsed.operands.shift()] : [];
494
+ return programs.length > 0 && programs.every(safeSedProgram);
495
+ }
496
+ function safeFind(args) {
497
+ if (args.length === 1 && INFO_OPTIONS.has(args[0])) return true;
498
+ const noValue = set(
499
+ "! ( ) -a -and -daystart -empty -executable -false -follow -ignore_readdir_race -ls -mount -noignore_readdir_race -not -o -or -print -print0 -prune -readable -true -writable -xdev"
500
+ );
501
+ const oneValue = set(
502
+ "-amin -anewer -atime -cmin -cnewer -ctime -fstype -gid -group -ilname -iname -inum -ipath -iregex -iwholename -links -lname -maxdepth -mindepth -mmin -mtime -name -newer -path -perm -printf -regex -regextype -samefile -size -type -uid -used -user -wholename -xtype"
503
+ );
504
+ let expressionStarted = false;
505
+ for (let index = 0; index < args.length; index += 1) {
506
+ const arg = args[index];
507
+ if (!expressionStarted && (arg === "-H" || arg === "-L" || arg === "-P" || /^-O\d+$/.test(arg)))
508
+ continue;
509
+ if (!expressionStarted && !arg.startsWith("-") && arg !== "!" && arg !== "(") continue;
510
+ expressionStarted = true;
511
+ if (noValue.has(arg)) continue;
512
+ if (/^-newer(?:a|B|c|m)t$/.test(arg) || /^-newer[acm][acm]$/.test(arg) || oneValue.has(arg)) {
513
+ if (!args[++index]) return false;
514
+ continue;
515
+ }
516
+ return false;
517
+ }
518
+ return true;
519
+ }
520
+ function withLongOptions(spec, flags, values) {
521
+ return {
522
+ ...spec,
523
+ longFlags: /* @__PURE__ */ new Set([...spec.longFlags ?? [], ...flags.split(" ").filter(Boolean)]),
524
+ longValues: /* @__PURE__ */ new Set([...spec.longValues ?? [], ...values.split(" ").filter(Boolean)])
525
+ };
526
+ }
527
+ function safeGit(args) {
528
+ let index = 0;
529
+ while (index < args.length) {
530
+ const arg = args[index];
531
+ if (arg === "--no-pager" || arg === "--paginate") {
532
+ index += 1;
533
+ } else if (arg === "-C" || arg === "--git-dir" || arg === "--work-tree") {
534
+ if (!args[index + 1]) return false;
535
+ index += 2;
536
+ } else if (arg.startsWith("--git-dir=") || arg.startsWith("--work-tree=")) {
537
+ if (!arg.slice(arg.indexOf("=") + 1)) return false;
538
+ index += 1;
539
+ } else {
540
+ break;
541
+ }
542
+ }
543
+ const subcommand = args[index++];
544
+ if (!subcommand) return args.length === 1 && INFO_OPTIONS.has(args[0]);
545
+ const rest = args.slice(index);
546
+ if ((/* @__PURE__ */ new Set([
547
+ "blame",
548
+ "cat-file",
549
+ "describe",
550
+ "diff",
551
+ "for-each-ref",
552
+ "grep",
553
+ "log",
554
+ "ls-files",
555
+ "ls-tree",
556
+ "merge-base",
557
+ "name-rev",
558
+ "rev-list",
559
+ "rev-parse",
560
+ "shortlog",
561
+ "show",
562
+ "status"
563
+ ])).has(subcommand))
564
+ return parseOptions(rest, GIT_INSPECTION_OPTIONS) !== void 0;
565
+ const metadata = GIT_METADATA_OPTIONS[subcommand];
566
+ if (metadata) {
567
+ const parsed = parseOptions(rest, metadata);
568
+ if (!parsed) return false;
569
+ if (subcommand !== "commit") return true;
570
+ return [
571
+ "-C",
572
+ "-F",
573
+ "-m",
574
+ "--dry-run",
575
+ "--file",
576
+ "--message",
577
+ "--no-edit",
578
+ "--reuse-message"
579
+ ].some((option) => parsed.seen.has(option));
580
+ }
581
+ if (subcommand === "reset") {
582
+ const parsed = parseOptions(rest, {
583
+ shortFlags: "Npq",
584
+ longFlags: set("--intent-to-add --mixed --no-refresh --pathspec-file-nul --quiet --soft"),
585
+ longValues: set("--pathspec-from-file")
586
+ });
587
+ return parsed !== void 0;
588
+ }
589
+ if (subcommand === "restore") {
590
+ const parsed = parseOptions(rest, {
591
+ shortFlags: "pq",
592
+ longFlags: set(
593
+ "--ignore-skip-worktree-bits --no-overlay --overlay --pathspec-file-nul --staged"
594
+ ),
595
+ longValues: set("--pathspec-from-file --source")
596
+ });
597
+ return parsed?.seen.has("--staged") === true;
598
+ }
599
+ if (subcommand === "rm") {
600
+ const parsed = parseOptions(rest, {
601
+ shortFlags: "fnqr",
602
+ longFlags: set(
603
+ "--cached --dry-run --force --ignore-unmatch --pathspec-file-nul --quiet --recursive"
604
+ ),
605
+ longValues: set("--pathspec-from-file")
606
+ });
607
+ return parsed?.seen.has("--cached") === true;
608
+ }
609
+ if (subcommand === "update-ref")
610
+ return parseOptions(rest, {
611
+ shortFlags: "d",
612
+ longFlags: set("--create-reflog --delete --no-deref --stdin"),
613
+ longValues: set("--reason")
614
+ }) !== void 0;
615
+ return false;
616
+ }
617
+ function safeGh(args) {
618
+ let index = 0;
619
+ while (index < args.length) {
620
+ const arg = args[index];
621
+ if (arg === "-R" || arg === "--repo" || arg === "--hostname") {
622
+ if (!args[index + 1]) return false;
623
+ index += 2;
624
+ } else if (arg.startsWith("--repo=") || arg.startsWith("--hostname=")) {
625
+ if (!arg.slice(arg.indexOf("=") + 1)) return false;
626
+ index += 1;
627
+ } else {
628
+ break;
629
+ }
630
+ }
631
+ if (args[index++] !== "pr") return false;
632
+ const subcommand = args[index++];
633
+ if (!subcommand) return args.length === index && args.at(-1) === "--help";
634
+ const spec = GH_PR_OPTIONS[subcommand];
635
+ if (!spec) return false;
636
+ const common = withLongOptions(spec, "--help", "--hostname --repo");
637
+ const parsed = parseOptions(args.slice(index), {
638
+ ...common,
639
+ shortValues: `${common.shortValues ?? ""}R`
640
+ });
641
+ if (!parsed) return false;
642
+ if (subcommand === "create") {
643
+ const filled = ["--fill", "--fill-first", "--fill-verbose", "--recover"].some(
644
+ (option) => parsed.seen.has(option)
645
+ );
646
+ return filled || parsed.seen.has("--title") && (parsed.seen.has("--body") || parsed.seen.has("--body-file"));
647
+ }
648
+ if (subcommand === "comment" && parsed.seen.has("--edit-last"))
649
+ return parsed.seen.has("--body") || parsed.seen.has("--body-file");
650
+ if (subcommand === "merge")
651
+ return ["--disable-auto", "--merge", "--rebase", "--squash", "-m", "-r", "-s"].some(
652
+ (option) => parsed.seen.has(option)
653
+ );
654
+ return true;
655
+ }
656
+ function knownNoWorktreeMutation(name, args) {
657
+ const options = COMMAND_OPTIONS[name];
658
+ if (options)
659
+ return args.length === 1 && INFO_OPTIONS.has(args[0]) || parseOptions(args, options) !== void 0;
660
+ if (name === "sed") return safeSed(args);
661
+ if (name === "find") return safeFind(args);
662
+ if (name === "git") return safeGit(args);
663
+ if (name === "gh") return safeGh(args);
664
+ return false;
665
+ }
666
+ function unknown(state) {
667
+ state.complete = false;
668
+ state.mutation = true;
669
+ }
670
+ function recordTarget(word, state) {
671
+ const path = staticWord(word);
672
+ if (path === "-" || path && (SINKS.has(path) || path.startsWith("/dev/fd/"))) return;
673
+ state.mutation = true;
674
+ if (path) state.paths.add(path);
675
+ else state.complete = false;
676
+ }
677
+ function visitRedirect(redirect, state) {
678
+ const target = staticWord(redirect.target);
679
+ if (redirect.operator === ">&" && (target === "-" || /^\d+$/.test(target ?? ""))) return;
680
+ if (OUTPUT_REDIRECTS.has(redirect.operator) || redirect.operator === ">&")
681
+ recordTarget(redirect.target, state);
682
+ else unknown(state);
683
+ }
684
+ function visitApplyPatch(command, state) {
685
+ state.mutation = true;
686
+ state.definiteEdit = true;
687
+ let hasBody = false;
688
+ if (command.suffix.length > 0) state.complete = false;
689
+ for (const redirect of command.redirects) {
690
+ if (redirect.operator !== "<<") {
691
+ visitRedirect(redirect, state);
692
+ continue;
693
+ }
694
+ if (typeof redirect.content !== "string") {
695
+ state.complete = false;
696
+ continue;
697
+ }
698
+ hasBody = true;
699
+ if (redirect.heredocQuoted !== true && redirect.body !== void 0) {
700
+ state.complete = false;
701
+ continue;
702
+ }
703
+ const targets = parseApplyPatchTargets(redirect.content);
704
+ for (const path of targets.paths) state.paths.add(path);
705
+ if (!targets.complete) state.complete = false;
706
+ }
707
+ if (!hasBody) state.complete = false;
708
+ }
709
+ function operands(words, short = "", long = /* @__PURE__ */ new Set()) {
710
+ const result = [];
711
+ let optionsEnded = false;
712
+ for (const word of words) {
713
+ const value = staticWord(word);
714
+ if (value === void 0) return;
715
+ if (optionsEnded || value === "-" || !value.startsWith("-")) result.push(value);
716
+ else if (value === "--") optionsEnded = true;
717
+ else if (value.startsWith("--") ? !long.has(value) : [...value.slice(1)].some((c) => !short.includes(c)))
718
+ return;
719
+ }
720
+ return result;
721
+ }
722
+ function visitCommand(command, state) {
723
+ const name = staticWord(command.name);
724
+ const literalApplyPatch = name === "apply_patch" && !command.redirects.some((redirect) => redirect.operator === "<<-");
725
+ if (literalApplyPatch) {
726
+ visitApplyPatch(command, state);
727
+ } else {
728
+ for (const redirect of command.redirects) visitRedirect(redirect, state);
729
+ }
730
+ if (!name || name.includes("/") || !literalApplyPatch && command.suffix.some((word) => !staticWord(word))) {
731
+ unknown(state);
732
+ } else if (literalApplyPatch) {
733
+ } else if (!READ_ONLY_COMMANDS.has(name) && !knownNoWorktreeMutation(name, staticArgs(command.suffix))) {
734
+ state.mutation = true;
735
+ let targets;
736
+ if (name === "tee") {
737
+ targets = operands(command.suffix, "ai", /* @__PURE__ */ new Set(["--append", "--ignore-interrupts"]));
738
+ } else if (name === "touch") {
739
+ targets = operands(command.suffix);
740
+ } else if (name === "rm") {
741
+ targets = operands(
742
+ command.suffix,
743
+ "fivI",
744
+ /* @__PURE__ */ new Set(["--force", "--interactive", "--interactive=once", "--verbose"])
745
+ );
746
+ } else if (name === "cp" || name === "mv") {
747
+ const pair = operands(command.suffix);
748
+ if (pair?.length === 2) targets = name === "cp" ? [pair[1]] : pair;
749
+ }
750
+ if (!targets?.length) state.complete = false;
751
+ else for (const target of targets) state.paths.add(target);
752
+ }
753
+ visitNested([command.name, command.prefix, command.suffix, command.redirects], state);
754
+ }
755
+ var CONTAINERS = /* @__PURE__ */ new Set(["Script", "Pipeline", "AndOr", "CompoundList"]);
756
+ function visitNested(value, state) {
757
+ if (Array.isArray(value)) {
758
+ for (const child of value) visitNested(child, state);
759
+ } else if (value && typeof value === "object") {
760
+ const child = value;
761
+ if (child.type === "Command") {
762
+ visitCommand(child, state);
763
+ } else if (child.type === "Statement") {
764
+ visitStatement(child, state);
765
+ } else if ("parts" in child) {
766
+ for (const part of value.parts ?? []) visitNested(part, state);
767
+ } else {
768
+ const node = typeof child.type === "string" && "pos" in child && "end" in child;
769
+ if (node && !CONTAINERS.has(child.type)) unknown(state);
770
+ if (child.type === "Function") return;
771
+ for (const nested of Object.values(child)) visitNested(nested, state);
772
+ }
773
+ }
774
+ }
775
+ function visitStatement(statement, state) {
776
+ if (statement.background) unknown(state);
777
+ for (const redirect of statement.redirects) visitRedirect(redirect, state);
778
+ visitNested([statement.redirects, statement.command], state);
779
+ }
780
+ function analyzeShellTargets(source) {
781
+ if (!source.trim())
782
+ return { paths: [], coverage: "complete", mutation: "none" };
783
+ try {
784
+ const script = parse(source);
785
+ const state = {
786
+ paths: /* @__PURE__ */ new Set(),
787
+ complete: !script.errors?.length,
788
+ mutation: false,
789
+ definiteEdit: false
790
+ };
791
+ for (const statement of script.commands) visitStatement(statement, state);
792
+ if (script.errors?.length) state.mutation = true;
793
+ return {
794
+ paths: [...state.paths],
795
+ coverage: state.complete ? "complete" : "unverified",
796
+ mutation: state.mutation ? "present" : "none",
797
+ ...state.definiteEdit ? { definiteEdit: true } : {}
798
+ };
799
+ } catch {
800
+ return { paths: [], coverage: "unverified", mutation: "present" };
801
+ }
802
+ }
803
+
804
+ export {
805
+ buildHookOutput,
806
+ buildCodexOutput,
807
+ failOpenOutput,
808
+ failOpenCodex,
809
+ buildHermesOutput,
810
+ failOpenHermes,
811
+ extractFileTargets,
812
+ readHookMode,
813
+ demoteForMode,
814
+ analyzeShellTargets
815
+ };