pi-shit 0.1.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 (42) hide show
  1. package/README.md +44 -0
  2. package/extensions/README.md +23 -0
  3. package/extensions/deep-review/README.md +56 -0
  4. package/extensions/deep-review/index.test.ts +97 -0
  5. package/extensions/deep-review/index.ts +1541 -0
  6. package/extensions/pi-notify/LICENSE +21 -0
  7. package/extensions/pi-notify/README.md +84 -0
  8. package/extensions/pi-notify/index.ts +75 -0
  9. package/extensions/pi-notify/package.json +28 -0
  10. package/extensions/plan-mode/README.md +69 -0
  11. package/extensions/plan-mode/index.ts +345 -0
  12. package/extensions/plan-mode/utils.test.ts +261 -0
  13. package/extensions/plan-mode/utils.ts +168 -0
  14. package/package.json +35 -0
  15. package/skills/README.md +70 -0
  16. package/skills/brave-search/SKILL.md +83 -0
  17. package/skills/brave-search/content.js +86 -0
  18. package/skills/brave-search/package-lock.json +623 -0
  19. package/skills/brave-search/package.json +14 -0
  20. package/skills/brave-search/search.js +199 -0
  21. package/skills/code-review/SKILL.md +97 -0
  22. package/skills/code-simplifier/SKILL.md +55 -0
  23. package/skills/context-packer/SKILL.md +77 -0
  24. package/skills/context-packer/prepare-context.sh +490 -0
  25. package/skills/image-compress/SKILL.md +53 -0
  26. package/skills/image-compress/compress.sh +172 -0
  27. package/skills/markdown-converter/SKILL.md +71 -0
  28. package/skills/multi-review/SKILL.md +143 -0
  29. package/skills/package.json +26 -0
  30. package/skills/pr-context-packer/SKILL.md +76 -0
  31. package/skills/pr-context-packer/prepare-pr-context.sh +941 -0
  32. package/skills/session-analyzer/IDEAS.md +42 -0
  33. package/skills/session-analyzer/SKILL.md +81 -0
  34. package/skills/session-analyzer/analyze.js +460 -0
  35. package/skills/session-analyzer/package-lock.json +3943 -0
  36. package/skills/session-analyzer/package.json +7 -0
  37. package/skills/video-compress/SKILL.md +43 -0
  38. package/skills/video-compress/compress.sh +107 -0
  39. package/skills/youtube-transcript/SKILL.md +59 -0
  40. package/skills/youtube-transcript/transcript.sh +46 -0
  41. package/themes/rose-pine-dawn.json +102 -0
  42. package/themes/rose-pine.json +102 -0
@@ -0,0 +1,941 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PROJECT_DIR="$(pwd)"
5
+ BASE_REF=""
6
+ BUDGET=272000
7
+ OUTPUT_NAME="pr-context.txt"
8
+ TMP_OUTPUT=true
9
+ WITH_SCRIBE=true
10
+ SCRIBE_INCLUDE_DEPENDENTS=true
11
+ SCRIBE_MAX_DEPTH=2
12
+ SCRIBE_MAX_FILES=25
13
+ SCRIBE_TARGET_LIMIT=0
14
+ MAX_RELATED=80
15
+ WITH_DOCS=false
16
+ WITH_TESTS=true
17
+ INCLUDE_LOCKFILES=false
18
+ INCLUDE_ENV=false
19
+ INCLUDE_SECRETS=false
20
+ NO_CLIPBOARD=false
21
+ FAIL_OVER_BUDGET=false
22
+ INSTALL_TOOLS=false
23
+ DIFF_CONTEXT=3
24
+ INCLUDE_PR_DESCRIPTION=true
25
+ PR_REF=""
26
+
27
+ show_help() {
28
+ cat <<'EOF'
29
+ Build a PR-focused LLM context pack: PR description + diff + full changed files + related files.
30
+
31
+ Usage:
32
+ prepare-pr-context.sh [project_dir] [options]
33
+
34
+ Options:
35
+ --base <ref> Base ref for PR diff (default: auto-detect origin/main, origin/master, main, master)
36
+ --output <name> Output filename (default: pr-context.txt)
37
+ --tmp-output Write to /tmp/context-packer/... (default)
38
+ --in-project-output Write to <repo>/prompt/
39
+ --budget <tokens> Token budget threshold (default: 272000)
40
+ --no-scribe Disable Scribe relevance expansion
41
+ --no-dependents Disable dependent-file expansion in Scribe
42
+ --scribe-max-depth <n> Scribe covering-set max depth (default: 2)
43
+ --scribe-max-files <n> Scribe covering-set max files per target (default: 25)
44
+ --scribe-target-limit <n>Limit changed files used as Scribe targets (default: 0 = all)
45
+ --max-related <n> Max related files included after Scribe expansion (default: 80)
46
+ --with-docs Include docs/ files in related expansion
47
+ --no-tests Exclude tests from related expansion
48
+ --include-lockfiles Include lockfiles
49
+ --include-env Include env files (.env, .env.*, .envrc)
50
+ --include-secrets Include potentially sensitive files (.npmrc, keys/certs, etc.)
51
+ --diff-context <n> Git diff context lines (default: 3)
52
+ --pr <ref> Explicit PR number/url/branch for gh lookup (optional)
53
+ --no-pr-description Skip auto-including GitHub PR title/body section
54
+ --no-clipboard Do not copy final output to clipboard
55
+ --fail-over-budget Exit with code 2 when tokens exceed budget
56
+ --install-tools Install missing tokencount (cargo install tokencount)
57
+ -h, --help Show this help
58
+
59
+ Examples:
60
+ prepare-pr-context.sh ~/dev/mobile-1 --base origin/main
61
+ prepare-pr-context.sh ~/dev/mobile-1 --base origin/main --tmp-output --budget 272000
62
+ prepare-pr-context.sh ~/dev/mobile-1 --base origin/main --no-scribe --max-related 0
63
+ EOF
64
+ }
65
+
66
+ command_exists() {
67
+ command -v "$1" >/dev/null 2>&1
68
+ }
69
+
70
+ count_lines() {
71
+ local path="$1"
72
+ if [[ -s "$path" ]]; then
73
+ wc -l < "$path" | tr -d '[:space:]'
74
+ else
75
+ echo 0
76
+ fi
77
+ }
78
+
79
+ is_allowed_extension() {
80
+ local rel="$1"
81
+ case "$rel" in
82
+ *.rs|*.zig|*.c|*.h|*.cpp|*.hpp|*.cc|*.hh|*.m|*.mm|*.swift|*.kt|*.kts|*.java|*.py|*.go|*.rb|*.php|*.cs|*.fs|*.lua|*.r|\
83
+ *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.svelte|*.vue|\
84
+ *.css|*.scss|*.sass|*.less|*.html|*.htm|*.svg|*.xml|*.xsd|*.xsl|\
85
+ *.json|*.jsonc|*.toml|*.yaml|*.yml|*.ini|*.cfg|*.conf|*.properties|\
86
+ *.md|*.mdx|*.rst|*.txt|\
87
+ *.sh|*.bash|*.zsh|*.fish|*.ps1|\
88
+ *.sql|*.graphql|*.gql|*.proto|*.tf|*.tfvars|*.cmake|*.gradle)
89
+ return 0
90
+ ;;
91
+ *)
92
+ return 1
93
+ ;;
94
+ esac
95
+ }
96
+
97
+ is_explicit_include() {
98
+ local rel="$1"
99
+ local base
100
+ base="$(basename "$rel")"
101
+
102
+ case "$base" in
103
+ Dockerfile|Containerfile|Makefile|GNUmakefile|justfile|Justfile|Procfile|Procfile.*|\
104
+ Brewfile|Gemfile|Gemfile.*|Rakefile|Rakefile.*|Vagrantfile|\
105
+ CMakeLists.txt|meson.build|meson_options.txt|BUILD|BUILD.bazel|WORKSPACE|WORKSPACE.bazel|MODULE.bazel|\
106
+ Jenkinsfile|Tiltfile|Podfile|Cartfile|Fastfile|flake.nix|default.nix|shell.nix|Taskfile)
107
+ return 0
108
+ ;;
109
+ .editorconfig|.gitignore|.gitattributes|.dockerignore|\
110
+ .npmrc|.nvmrc|.prettierignore|.prettierrc|.eslintignore|\
111
+ .tool-versions|.python-version|.ruby-version|.node-version|.terraform.lock.hcl)
112
+ return 0
113
+ ;;
114
+ *)
115
+ return 1
116
+ ;;
117
+ esac
118
+ }
119
+
120
+ is_lockfile() {
121
+ local rel="$1"
122
+ case "$rel" in
123
+ pnpm-lock.yaml|*/pnpm-lock.yaml|\
124
+ package-lock.json|*/package-lock.json|\
125
+ yarn.lock|*/yarn.lock|\
126
+ bun.lock|*/bun.lock|bun.lockb|*/bun.lockb|\
127
+ npm-shrinkwrap.json|*/npm-shrinkwrap.json|\
128
+ Cargo.lock|*/Cargo.lock|\
129
+ composer.lock|*/composer.lock|\
130
+ Gemfile.lock|*/Gemfile.lock|\
131
+ poetry.lock|*/poetry.lock|\
132
+ Pipfile.lock|*/Pipfile.lock)
133
+ return 0
134
+ ;;
135
+ *)
136
+ return 1
137
+ ;;
138
+ esac
139
+ }
140
+
141
+ is_env_path() {
142
+ local rel="$1"
143
+ case "$rel" in
144
+ .env|.env.*|*/.env|*/.env.*|.envrc|*/.envrc)
145
+ return 0
146
+ ;;
147
+ *)
148
+ return 1
149
+ ;;
150
+ esac
151
+ }
152
+
153
+ is_secret_path() {
154
+ local rel="$1"
155
+ local base
156
+ base="$(basename "$rel")"
157
+
158
+ case "$rel" in
159
+ .npmrc|*/.npmrc|.pypirc|*/.pypirc|.netrc|*/.netrc|\
160
+ .aws/credentials|*/.aws/credentials|.aws/config|*/.aws/config|\
161
+ .gem/credentials|*/.gem/credentials)
162
+ return 0
163
+ ;;
164
+ esac
165
+
166
+ case "$base" in
167
+ id_rsa|id_dsa|id_ecdsa|id_ed25519|\
168
+ google-services.json|GoogleService-Info.plist|\
169
+ firebase-adminsdk*.json|*service-account*.json|*serviceaccount*.json)
170
+ return 0
171
+ ;;
172
+ esac
173
+
174
+ case "$rel" in
175
+ *.pem|*.key|*.p12|*.pfx|*.jks|*.keystore|*.kdbx|*.pkcs12|*.der|*.crt|*.cer|*.csr|\
176
+ *.mobileprovision|*.provisionprofile)
177
+ return 0
178
+ ;;
179
+ esac
180
+
181
+ return 1
182
+ }
183
+
184
+ is_docs_path() {
185
+ local rel="$1"
186
+ case "$rel" in
187
+ docs/*|*/docs/*|doc/*|*/doc/*|documentation/*|*/documentation/*)
188
+ return 0
189
+ ;;
190
+ *)
191
+ return 1
192
+ ;;
193
+ esac
194
+ }
195
+
196
+ is_test_path() {
197
+ local rel="$1"
198
+ case "$rel" in
199
+ __tests__/*|*/__tests__/*|\
200
+ test/*|*/test/*|tests/*|*/tests/*|\
201
+ *.test.*|*.spec.*|*_test.*|test_*.py)
202
+ return 0
203
+ ;;
204
+ *)
205
+ return 1
206
+ ;;
207
+ esac
208
+ }
209
+
210
+ is_hard_excluded_path() {
211
+ local rel="$1"
212
+ case "$rel" in
213
+ .git/*|*/.git/*|.hg/*|*/.hg/*|.svn/*|*/.svn/*|\
214
+ node_modules/*|*/node_modules/*|\
215
+ prompt/*|*/prompt/*|\
216
+ dist/*|*/dist/*|build/*|*/build/*|target/*|*/target/*|out/*|*/out/*|coverage/*|*/coverage/*|\
217
+ .next/*|*/.next/*|.nuxt/*|*/.nuxt/*|.svelte-kit/*|*/.svelte-kit/*|.turbo/*|*/.turbo/*|\
218
+ .cache/*|*/.cache/*|.parcel-cache/*|*/.parcel-cache/*|\
219
+ .venv/*|*/.venv/*|venv/*|*/venv/*|\
220
+ __pycache__/*|*/__pycache__/*|.pytest_cache/*|*/.pytest_cache/*|.mypy_cache/*|*/.mypy_cache/*|\
221
+ .terraform/*|*/.terraform/*|.direnv/*|*/.direnv/*|\
222
+ .gradle/*|*/.gradle/*|.idea/*|*/.idea/*|\
223
+ *.egg-info/*|*/*.egg-info/*)
224
+ return 0
225
+ ;;
226
+ esac
227
+
228
+ case "$rel" in
229
+ .DS_Store|*/.DS_Store|*CHATGPT_CODE_DUMP*|*code-dump*.txt)
230
+ return 0
231
+ ;;
232
+ esac
233
+
234
+ return 1
235
+ }
236
+
237
+ is_scribe_target_candidate() {
238
+ local rel="$1"
239
+ case "$rel" in
240
+ *.rs|*.py|*.js|*.jsx|*.ts|*.tsx|*.mjs|*.cjs|*.go)
241
+ return 0
242
+ ;;
243
+ *)
244
+ return 1
245
+ ;;
246
+ esac
247
+ }
248
+
249
+ is_probably_text_file() {
250
+ local rel="$1"
251
+ local abs="$REPO_ROOT/$rel"
252
+
253
+ if [[ ! -f "$abs" ]]; then
254
+ return 1
255
+ fi
256
+
257
+ if [[ ! -s "$abs" ]]; then
258
+ return 0
259
+ fi
260
+
261
+ LC_ALL=C grep -Iq . "$abs"
262
+ }
263
+
264
+ should_include_changed_file() {
265
+ local rel="$1"
266
+
267
+ if is_hard_excluded_path "$rel"; then
268
+ return 1
269
+ fi
270
+
271
+ if [[ "$INCLUDE_ENV" != true ]] && is_env_path "$rel"; then
272
+ return 1
273
+ fi
274
+
275
+ if [[ "$INCLUDE_SECRETS" != true ]] && is_secret_path "$rel"; then
276
+ return 1
277
+ fi
278
+
279
+ if [[ "$INCLUDE_LOCKFILES" != true ]] && is_lockfile "$rel"; then
280
+ return 1
281
+ fi
282
+
283
+ if is_explicit_include "$rel" || is_allowed_extension "$rel" || is_lockfile "$rel" || is_probably_text_file "$rel"; then
284
+ return 0
285
+ fi
286
+
287
+ return 1
288
+ }
289
+
290
+ should_include_related_file() {
291
+ local rel="$1"
292
+
293
+ if is_hard_excluded_path "$rel"; then
294
+ return 1
295
+ fi
296
+
297
+ if [[ "$INCLUDE_ENV" != true ]] && is_env_path "$rel"; then
298
+ return 1
299
+ fi
300
+
301
+ if [[ "$INCLUDE_SECRETS" != true ]] && is_secret_path "$rel"; then
302
+ return 1
303
+ fi
304
+
305
+ if [[ "$INCLUDE_LOCKFILES" != true ]] && is_lockfile "$rel"; then
306
+ return 1
307
+ fi
308
+
309
+ if [[ "$WITH_DOCS" != true ]] && is_docs_path "$rel"; then
310
+ return 1
311
+ fi
312
+
313
+ if [[ "$WITH_TESTS" != true ]] && is_test_path "$rel"; then
314
+ return 1
315
+ fi
316
+
317
+ if is_explicit_include "$rel" || is_allowed_extension "$rel" || is_lockfile "$rel"; then
318
+ return 0
319
+ fi
320
+
321
+ return 1
322
+ }
323
+
324
+ reason_for_omitted_changed_file() {
325
+ local rel="$1"
326
+
327
+ if [[ "$INCLUDE_LOCKFILES" != true ]] && is_lockfile "$rel"; then
328
+ echo "lockfile"
329
+ return 0
330
+ fi
331
+
332
+ if [[ "$INCLUDE_ENV" != true ]] && is_env_path "$rel"; then
333
+ echo "env"
334
+ return 0
335
+ fi
336
+
337
+ if [[ "$INCLUDE_SECRETS" != true ]] && is_secret_path "$rel"; then
338
+ echo "secret"
339
+ return 0
340
+ fi
341
+
342
+ if is_hard_excluded_path "$rel"; then
343
+ echo "generated/cache"
344
+ return 0
345
+ fi
346
+
347
+ if ! is_probably_text_file "$rel"; then
348
+ echo "binary/non-text"
349
+ return 0
350
+ fi
351
+
352
+ echo "filtered"
353
+ }
354
+
355
+ ensure_tokencount() {
356
+ if command_exists tokencount; then
357
+ return 0
358
+ fi
359
+
360
+ if [[ "$INSTALL_TOOLS" == true ]]; then
361
+ if ! command_exists cargo; then
362
+ echo "❌ cargo not found; cannot install tokencount" >&2
363
+ return 1
364
+ fi
365
+ echo "ℹ️ Installing tokencount via cargo..." >&2
366
+ cargo install tokencount
367
+ fi
368
+
369
+ if ! command_exists tokencount; then
370
+ echo "❌ tokencount not found. Install with: cargo install tokencount" >&2
371
+ return 1
372
+ fi
373
+
374
+ return 0
375
+ }
376
+
377
+ run_scribe() {
378
+ if command_exists scribe; then
379
+ scribe "$@"
380
+ return $?
381
+ fi
382
+
383
+ if command_exists npx; then
384
+ npx -y @sibyllinesoft/scribe "$@"
385
+ return $?
386
+ fi
387
+
388
+ return 127
389
+ }
390
+
391
+ ensure_scribe() {
392
+ if [[ "$WITH_SCRIBE" != true ]]; then
393
+ return 0
394
+ fi
395
+
396
+ if command_exists scribe || command_exists npx; then
397
+ return 0
398
+ fi
399
+
400
+ echo "⚠️ Scribe not found (scribe or npx unavailable); continuing without related expansion" >&2
401
+ WITH_SCRIBE=false
402
+ return 0
403
+ }
404
+
405
+ copy_output_to_clipboard() {
406
+ local output_path="$1"
407
+
408
+ if [[ "$NO_CLIPBOARD" == true ]]; then
409
+ return 0
410
+ fi
411
+
412
+ if command_exists pbcopy; then
413
+ pbcopy < "$output_path"
414
+ return 0
415
+ fi
416
+
417
+ if command_exists wl-copy; then
418
+ wl-copy < "$output_path"
419
+ return 0
420
+ fi
421
+
422
+ echo "ℹ️ Clipboard tool not found (pbcopy/wl-copy). Output file was still written." >&2
423
+ return 0
424
+ }
425
+
426
+ render_file_blocks() {
427
+ local list_path="$1"
428
+ local output_path="$2"
429
+
430
+ while IFS= read -r rel; do
431
+ [[ -z "$rel" ]] && continue
432
+
433
+ local src="$REPO_ROOT/$rel"
434
+ if [[ ! -f "$src" ]]; then
435
+ continue
436
+ fi
437
+
438
+ printf '### %s\n\n' "$rel" >> "$output_path"
439
+ printf '```\n' >> "$output_path"
440
+ cat "$src" >> "$output_path"
441
+
442
+ if [[ -s "$src" ]]; then
443
+ local last_char
444
+ last_char="$(tail -c 1 "$src" || true)"
445
+ if [[ "$last_char" != $'\n' ]]; then
446
+ printf '\n' >> "$output_path"
447
+ fi
448
+ fi
449
+
450
+ printf '```\n\n' >> "$output_path"
451
+ done < "$list_path"
452
+ }
453
+
454
+ write_pr_description_section() {
455
+ local output_path="$1"
456
+
457
+ if [[ "$INCLUDE_PR_DESCRIPTION" != true ]]; then
458
+ return 1
459
+ fi
460
+
461
+ if ! command_exists gh; then
462
+ return 1
463
+ fi
464
+
465
+ local -a gh_args
466
+ gh_args=(pr view --json number,title,body,url,baseRefName,headRefName,state,author)
467
+ if [[ -n "$PR_REF" ]]; then
468
+ gh_args=(pr view "$PR_REF" --json number,title,body,url,baseRefName,headRefName,state,author)
469
+ fi
470
+
471
+ local pr_json
472
+ if ! pr_json="$(cd "$REPO_ROOT" && gh "${gh_args[@]}" 2>/dev/null)"; then
473
+ return 1
474
+ fi
475
+
476
+ PR_JSON="$pr_json" python3 - <<'PY' > "$output_path"
477
+ import json
478
+ import os
479
+
480
+ pr = json.loads(os.environ["PR_JSON"])
481
+ author = pr.get("author") or {}
482
+ body = (pr.get("body") or "(no description)").rstrip()
483
+
484
+ print("# PR Description")
485
+ print()
486
+ print(f"- PR: #{pr.get('number', '')}")
487
+ print(f"- Title: {pr.get('title', '')}")
488
+ print(f"- URL: {pr.get('url', '')}")
489
+ print(f"- State: {pr.get('state', '')}")
490
+ print(f"- Base: {pr.get('baseRefName', '')}")
491
+ print(f"- Head: {pr.get('headRefName', '')}")
492
+ print(f"- Author: {author.get('login', '')}")
493
+ print()
494
+ print("## Body")
495
+ print()
496
+ print(body)
497
+ PY
498
+ }
499
+
500
+ resolve_base_ref() {
501
+ if [[ -n "$BASE_REF" ]]; then
502
+ if git -C "$REPO_ROOT" rev-parse --verify "$BASE_REF^{commit}" >/dev/null 2>&1; then
503
+ return 0
504
+ fi
505
+
506
+ echo "❌ Base ref not found: $BASE_REF" >&2
507
+ return 1
508
+ fi
509
+
510
+ for candidate in origin/main origin/master main master; do
511
+ if git -C "$REPO_ROOT" rev-parse --verify "$candidate^{commit}" >/dev/null 2>&1; then
512
+ BASE_REF="$candidate"
513
+ return 0
514
+ fi
515
+ done
516
+
517
+ BASE_REF="HEAD~1"
518
+ if git -C "$REPO_ROOT" rev-parse --verify "$BASE_REF^{commit}" >/dev/null 2>&1; then
519
+ return 0
520
+ fi
521
+
522
+ echo "❌ Could not auto-detect base ref (tried origin/main, origin/master, main, master, HEAD~1)" >&2
523
+ return 1
524
+ }
525
+
526
+ while [[ $# -gt 0 ]]; do
527
+ case "$1" in
528
+ --base)
529
+ BASE_REF="$2"
530
+ shift 2
531
+ ;;
532
+ --output)
533
+ OUTPUT_NAME="$2"
534
+ shift 2
535
+ ;;
536
+ --tmp-output)
537
+ TMP_OUTPUT=true
538
+ shift
539
+ ;;
540
+ --in-project-output)
541
+ TMP_OUTPUT=false
542
+ shift
543
+ ;;
544
+ --budget)
545
+ BUDGET="$2"
546
+ shift 2
547
+ ;;
548
+ --no-scribe)
549
+ WITH_SCRIBE=false
550
+ shift
551
+ ;;
552
+ --no-dependents)
553
+ SCRIBE_INCLUDE_DEPENDENTS=false
554
+ shift
555
+ ;;
556
+ --scribe-max-depth)
557
+ SCRIBE_MAX_DEPTH="$2"
558
+ shift 2
559
+ ;;
560
+ --scribe-max-files)
561
+ SCRIBE_MAX_FILES="$2"
562
+ shift 2
563
+ ;;
564
+ --scribe-target-limit)
565
+ SCRIBE_TARGET_LIMIT="$2"
566
+ shift 2
567
+ ;;
568
+ --max-related)
569
+ MAX_RELATED="$2"
570
+ shift 2
571
+ ;;
572
+ --with-docs)
573
+ WITH_DOCS=true
574
+ shift
575
+ ;;
576
+ --no-tests)
577
+ WITH_TESTS=false
578
+ shift
579
+ ;;
580
+ --include-lockfiles)
581
+ INCLUDE_LOCKFILES=true
582
+ shift
583
+ ;;
584
+ --include-env)
585
+ INCLUDE_ENV=true
586
+ shift
587
+ ;;
588
+ --include-secrets)
589
+ INCLUDE_SECRETS=true
590
+ shift
591
+ ;;
592
+ --diff-context)
593
+ DIFF_CONTEXT="$2"
594
+ shift 2
595
+ ;;
596
+ --pr)
597
+ PR_REF="$2"
598
+ shift 2
599
+ ;;
600
+ --no-pr-description)
601
+ INCLUDE_PR_DESCRIPTION=false
602
+ shift
603
+ ;;
604
+ --no-clipboard)
605
+ NO_CLIPBOARD=true
606
+ shift
607
+ ;;
608
+ --fail-over-budget)
609
+ FAIL_OVER_BUDGET=true
610
+ shift
611
+ ;;
612
+ --install-tools)
613
+ INSTALL_TOOLS=true
614
+ shift
615
+ ;;
616
+ -h|--help)
617
+ show_help
618
+ exit 0
619
+ ;;
620
+ -* )
621
+ echo "❌ Unknown option: $1" >&2
622
+ show_help
623
+ exit 1
624
+ ;;
625
+ *)
626
+ PROJECT_DIR="$1"
627
+ shift
628
+ ;;
629
+ esac
630
+ done
631
+
632
+ if [[ ! -d "$PROJECT_DIR" ]]; then
633
+ echo "❌ Project directory not found: $PROJECT_DIR" >&2
634
+ exit 1
635
+ fi
636
+
637
+ PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
638
+
639
+ if ! git -C "$PROJECT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
640
+ echo "❌ Not a git repository: $PROJECT_DIR" >&2
641
+ exit 1
642
+ fi
643
+
644
+ REPO_ROOT="$(git -C "$PROJECT_DIR" rev-parse --show-toplevel)"
645
+
646
+ if [[ ! "$BUDGET" =~ ^[0-9]+$ ]]; then
647
+ echo "❌ Budget must be an integer: $BUDGET" >&2
648
+ exit 1
649
+ fi
650
+
651
+ for number_arg in "$SCRIBE_MAX_DEPTH" "$SCRIBE_MAX_FILES" "$SCRIBE_TARGET_LIMIT" "$MAX_RELATED" "$DIFF_CONTEXT"; do
652
+ if [[ ! "$number_arg" =~ ^[0-9]+$ ]]; then
653
+ echo "❌ Numeric option must be an integer: $number_arg" >&2
654
+ exit 1
655
+ fi
656
+ done
657
+
658
+ if [[ "$INCLUDE_ENV" == true || "$INCLUDE_SECRETS" == true ]]; then
659
+ echo "⚠️ Including potentially sensitive files (env/secrets)" >&2
660
+ fi
661
+
662
+ ensure_tokencount
663
+ ensure_scribe
664
+ resolve_base_ref
665
+
666
+ BASE_COMMIT="$(git -C "$REPO_ROOT" merge-base HEAD "$BASE_REF")"
667
+ HEAD_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)"
668
+
669
+ WORK_DIR="$(mktemp -d "/tmp/pr-context-packer.XXXXXX")"
670
+ trap 'rm -rf "$WORK_DIR"' EXIT
671
+
672
+ CHANGED_RAW="$WORK_DIR/changed.raw.txt"
673
+ CHANGED_LIST="$WORK_DIR/changed.files.txt"
674
+ RELATED_CANDIDATES="$WORK_DIR/related.candidates.txt"
675
+ RELATED_LIST="$WORK_DIR/related.files.txt"
676
+ OMITTED_LIST="$WORK_DIR/omitted.files.txt"
677
+ STATUS_PATH="$WORK_DIR/name-status.txt"
678
+ DIFF_PATH="$WORK_DIR/pr.diff"
679
+
680
+ : > "$CHANGED_LIST"
681
+ : > "$RELATED_CANDIDATES"
682
+ : > "$RELATED_LIST"
683
+ : > "$OMITTED_LIST"
684
+
685
+ git -C "$REPO_ROOT" diff --name-only --diff-filter=ACMR "$BASE_COMMIT...HEAD" > "$CHANGED_RAW"
686
+ git -C "$REPO_ROOT" diff --name-status "$BASE_COMMIT...HEAD" > "$STATUS_PATH"
687
+ git -C "$REPO_ROOT" diff --no-color --unified="$DIFF_CONTEXT" "$BASE_COMMIT...HEAD" > "$DIFF_PATH"
688
+
689
+ if [[ ! -s "$CHANGED_RAW" ]]; then
690
+ echo "❌ No changed files found between $BASE_REF and HEAD" >&2
691
+ exit 1
692
+ fi
693
+
694
+ while IFS= read -r rel; do
695
+ [[ -z "$rel" ]] && continue
696
+
697
+ src="$REPO_ROOT/$rel"
698
+ if [[ ! -f "$src" ]]; then
699
+ printf '%s\t%s\n' "$rel" "missing" >> "$OMITTED_LIST"
700
+ continue
701
+ fi
702
+
703
+ if should_include_changed_file "$rel"; then
704
+ printf '%s\n' "$rel" >> "$CHANGED_LIST"
705
+ else
706
+ reason="$(reason_for_omitted_changed_file "$rel")"
707
+ printf '%s\t%s\n' "$rel" "$reason" >> "$OMITTED_LIST"
708
+ fi
709
+ done < "$CHANGED_RAW"
710
+
711
+ if [[ ! -s "$CHANGED_LIST" ]]; then
712
+ echo "❌ No eligible changed files after filtering" >&2
713
+ exit 1
714
+ fi
715
+
716
+ LC_ALL=C sort -u "$CHANGED_LIST" -o "$CHANGED_LIST"
717
+
718
+ if [[ "$WITH_SCRIBE" == true && "$MAX_RELATED" -gt 0 ]]; then
719
+ scribe_targets_used=0
720
+
721
+ while IFS= read -r target; do
722
+ [[ -z "$target" ]] && continue
723
+
724
+ if [[ "$SCRIBE_TARGET_LIMIT" -gt 0 && "$scribe_targets_used" -ge "$SCRIBE_TARGET_LIMIT" ]]; then
725
+ break
726
+ fi
727
+
728
+ if ! is_scribe_target_candidate "$target"; then
729
+ continue
730
+ fi
731
+
732
+ scribe_targets_used=$((scribe_targets_used + 1))
733
+ scribe_out="$WORK_DIR/scribe-${scribe_targets_used}.xml"
734
+
735
+ scribe_args=(
736
+ "$REPO_ROOT"
737
+ --covering-set "$target"
738
+ --granularity file
739
+ --max-depth "$SCRIBE_MAX_DEPTH"
740
+ --max-files "$SCRIBE_MAX_FILES"
741
+ --stdout
742
+ )
743
+
744
+ if [[ "$SCRIBE_INCLUDE_DEPENDENTS" == true ]]; then
745
+ scribe_args+=(--include-dependents)
746
+ fi
747
+
748
+ if ! run_scribe "${scribe_args[@]}" > "$scribe_out" 2>/dev/null; then
749
+ continue
750
+ fi
751
+
752
+ while IFS= read -r abs_path; do
753
+ [[ -z "$abs_path" ]] && continue
754
+
755
+ if [[ "$abs_path" != "$REPO_ROOT"/* ]]; then
756
+ continue
757
+ fi
758
+
759
+ rel_path="${abs_path#$REPO_ROOT/}"
760
+ [[ "$rel_path" == "$target" ]] && continue
761
+ [[ ! -f "$REPO_ROOT/$rel_path" ]] && continue
762
+
763
+ if should_include_related_file "$rel_path"; then
764
+ printf '%s\n' "$rel_path" >> "$RELATED_CANDIDATES"
765
+ fi
766
+ done < <(rg -o '<path>[^<]+</path>' "$scribe_out" | sed -E 's#<path>(.*)</path>#\1#')
767
+ done < "$CHANGED_LIST"
768
+ fi
769
+
770
+ if [[ -s "$RELATED_CANDIDATES" ]]; then
771
+ LC_ALL=C sort -u "$RELATED_CANDIDATES" > "$WORK_DIR/related.unique.txt"
772
+ grep -Fvx -f "$CHANGED_LIST" "$WORK_DIR/related.unique.txt" > "$RELATED_LIST" || true
773
+ fi
774
+
775
+ if [[ "$MAX_RELATED" -ge 0 ]]; then
776
+ related_total_before_limit="$(count_lines "$RELATED_LIST")"
777
+ if [[ "$related_total_before_limit" -gt "$MAX_RELATED" ]]; then
778
+ head -n "$MAX_RELATED" "$RELATED_LIST" > "$WORK_DIR/related.limited.txt"
779
+ mv "$WORK_DIR/related.limited.txt" "$RELATED_LIST"
780
+ related_truncated=true
781
+ else
782
+ related_truncated=false
783
+ fi
784
+ else
785
+ related_total_before_limit="$(count_lines "$RELATED_LIST")"
786
+ related_truncated=false
787
+ fi
788
+
789
+ changed_count="$(count_lines "$CHANGED_LIST")"
790
+ related_count="$(count_lines "$RELATED_LIST")"
791
+ omitted_count="$(count_lines "$OMITTED_LIST")"
792
+
793
+ repo_slug="$(basename "$REPO_ROOT" | tr ' ' '-' | tr -cd '[:alnum:]._-')"
794
+ timestamp="$(date +%Y%m%d-%H%M%S)"
795
+
796
+ if [[ "$TMP_OUTPUT" == true ]]; then
797
+ OUTPUT_DIR="/tmp/context-packer/pr-${repo_slug}-${timestamp}"
798
+ else
799
+ OUTPUT_DIR="$REPO_ROOT/prompt"
800
+ fi
801
+
802
+ mkdir -p "$OUTPUT_DIR"
803
+
804
+ OUTPUT_PATH="$OUTPUT_DIR/$OUTPUT_NAME"
805
+ BASE_NAME="${OUTPUT_NAME%.txt}"
806
+ CHANGED_MANIFEST="$OUTPUT_DIR/${BASE_NAME}.changed.files.txt"
807
+ RELATED_MANIFEST="$OUTPUT_DIR/${BASE_NAME}.related.files.txt"
808
+ OMITTED_MANIFEST="$OUTPUT_DIR/${BASE_NAME}.omitted.files.txt"
809
+
810
+ cp "$CHANGED_LIST" "$CHANGED_MANIFEST"
811
+ cp "$RELATED_LIST" "$RELATED_MANIFEST"
812
+ cp "$OMITTED_LIST" "$OMITTED_MANIFEST"
813
+
814
+ PR_DESC_PATH="$WORK_DIR/pr-description.md"
815
+ pr_description_included=false
816
+ if write_pr_description_section "$PR_DESC_PATH"; then
817
+ pr_description_included=true
818
+ else
819
+ if [[ "$INCLUDE_PR_DESCRIPTION" == true ]]; then
820
+ echo "ℹ️ PR description unavailable via gh (no matching PR, gh missing, or not authenticated)" >&2
821
+ fi
822
+ fi
823
+
824
+ {
825
+ if [[ "$pr_description_included" == true ]]; then
826
+ cat "$PR_DESC_PATH"
827
+ echo ""
828
+ echo "---"
829
+ echo ""
830
+ fi
831
+
832
+ echo "# PR Context Pack"
833
+ echo ""
834
+ echo "- Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
835
+ echo "- Repo root: $REPO_ROOT"
836
+ echo "- Working dir: $PROJECT_DIR"
837
+ echo "- Base ref: $BASE_REF"
838
+ echo "- Base commit: $BASE_COMMIT"
839
+ echo "- Head commit: $HEAD_COMMIT"
840
+ echo "- Scribe expansion: $WITH_SCRIBE"
841
+ echo "- Token budget: $BUDGET"
842
+ echo ""
843
+
844
+ echo "## Changed files (git name-status)"
845
+ echo ""
846
+ echo '```text'
847
+ cat "$STATUS_PATH"
848
+ echo '```'
849
+ echo ""
850
+
851
+ echo "## Git diff ($BASE_COMMIT...$HEAD_COMMIT)"
852
+ echo ""
853
+ echo '```diff'
854
+ cat "$DIFF_PATH"
855
+ echo '```'
856
+ echo ""
857
+
858
+ echo "## Full current code: changed files ($changed_count)"
859
+ echo ""
860
+ } > "$OUTPUT_PATH"
861
+
862
+ render_file_blocks "$CHANGED_LIST" "$OUTPUT_PATH"
863
+
864
+ {
865
+ echo "## Full current code: related files ($related_count)"
866
+ echo ""
867
+
868
+ if [[ "$WITH_SCRIBE" == true ]]; then
869
+ echo "_Source: Scribe covering-set expansion (max-depth=$SCRIBE_MAX_DEPTH, max-files=$SCRIBE_MAX_FILES, target-limit=$SCRIBE_TARGET_LIMIT, include-dependents=$SCRIBE_INCLUDE_DEPENDENTS)_"
870
+ else
871
+ echo "_Scribe expansion disabled_"
872
+ fi
873
+
874
+ if [[ "$related_truncated" == true ]]; then
875
+ echo ""
876
+ echo "_Related file list truncated to max-related=$MAX_RELATED (from $related_total_before_limit)_"
877
+ fi
878
+
879
+ echo ""
880
+ } >> "$OUTPUT_PATH"
881
+
882
+ render_file_blocks "$RELATED_LIST" "$OUTPUT_PATH"
883
+
884
+ {
885
+ echo "## Omitted changed files ($omitted_count)"
886
+ echo ""
887
+
888
+ if [[ "$omitted_count" -eq 0 ]]; then
889
+ echo "None"
890
+ else
891
+ while IFS=$'\t' read -r rel reason; do
892
+ [[ -z "$rel" ]] && continue
893
+ if [[ -n "$reason" ]]; then
894
+ echo "- $rel — $reason"
895
+ else
896
+ echo "- $rel"
897
+ fi
898
+ done < "$OMITTED_LIST"
899
+ fi
900
+
901
+ echo ""
902
+ } >> "$OUTPUT_PATH"
903
+
904
+ copy_output_to_clipboard "$OUTPUT_PATH"
905
+
906
+ output_ext="${OUTPUT_NAME##*.}"
907
+ if [[ "$output_ext" == "$OUTPUT_NAME" ]]; then
908
+ output_ext="txt"
909
+ fi
910
+
911
+ TOKENS_RAW="$(tokencount --encoding o200k-base --include-ext "$output_ext" "$OUTPUT_PATH")"
912
+ TOKENS="$(printf '%s\n' "$TOKENS_RAW" | awk 'NR==1 {print $1}')"
913
+
914
+ if [[ ! "$TOKENS" =~ ^[0-9]+$ ]]; then
915
+ echo "❌ Failed to parse tokencount output" >&2
916
+ echo "$TOKENS_RAW" >&2
917
+ exit 1
918
+ fi
919
+
920
+ echo ""
921
+ echo "✅ PR context pack ready"
922
+ echo "📁 Project: $PROJECT_DIR"
923
+ echo "📂 Out dir: $OUTPUT_DIR"
924
+ echo "📄 Output: $OUTPUT_PATH"
925
+ echo "📝 PR description: $pr_description_included"
926
+ echo "🧾 Changed manifest: $CHANGED_MANIFEST"
927
+ echo "🧾 Related manifest: $RELATED_MANIFEST"
928
+ echo "🧾 Omitted manifest: $OMITTED_MANIFEST"
929
+ echo "📦 Changed files: $changed_count"
930
+ echo "📦 Related files: $related_count"
931
+ echo "🔢 Tokens: $TOKENS (o200k-base)"
932
+ echo "🎯 Budget: $BUDGET"
933
+
934
+ if (( TOKENS > BUDGET )); then
935
+ echo "⚠️ Over budget by $((TOKENS - BUDGET)) tokens"
936
+ if [[ "$FAIL_OVER_BUDGET" == true ]]; then
937
+ exit 2
938
+ fi
939
+ else
940
+ echo "✅ Within budget by $((BUDGET - TOKENS)) tokens"
941
+ fi