@a9n-shoji/rvw 0.2.1 → 0.2.3

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 (103) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +24 -4
  3. package/dist/cli.mjs +188 -19
  4. package/dist/cli.mjs.map +4 -4
  5. package/dist/web/assets/DocumentViewer-B4LdSL1l.js +1560 -0
  6. package/dist/web/assets/{MarkdownTable-DbDc9VoP.js → MarkdownTable-CdbKPft2.js} +1 -1
  7. package/dist/web/assets/WalkthroughViewer-D1LlSQw2.js +1 -0
  8. package/dist/web/assets/{abnfDiagram-N423BO3Z-fQDP4qYL.js → abnfDiagram-N423BO3Z-CE1nZSC9.js} +1 -1
  9. package/dist/web/assets/architecture-TIHT7OUA-edLV2Zq-.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-T3A2C74G-D3Aon3Nd.js → architectureDiagram-T3A2C74G-B88qYFUo.js} +1 -1
  11. package/dist/web/assets/{blockDiagram-VBNYF7ZC-DS1DcAqy.js → blockDiagram-VBNYF7ZC-BaX0rhbb.js} +1 -1
  12. package/dist/web/assets/{c4Diagram-5PPSVZJV-BHQWz7l2.js → c4Diagram-5PPSVZJV-C1ppBaEC.js} +1 -1
  13. package/dist/web/assets/channel-J8FHSJDG.js +1 -0
  14. package/dist/web/assets/{chunk-2GRJ4B5K-CaahxDVT.js → chunk-2GRJ4B5K-D2TTocfh.js} +1 -1
  15. package/dist/web/assets/{chunk-4I5QYGJK-Bat5hdXD.js → chunk-4I5QYGJK-ikT25rPN.js} +1 -1
  16. package/dist/web/assets/{chunk-5RXB4S5H-jyonN41f.js → chunk-5RXB4S5H-CGlPaulj.js} +1 -1
  17. package/dist/web/assets/{chunk-6Q2QTUOP-BBo9TYzL.js → chunk-6Q2QTUOP-BuuMeCo2.js} +1 -1
  18. package/dist/web/assets/{chunk-7Z6QIM7H-CGSwsKhC.js → chunk-7Z6QIM7H-DT3BAZ4L.js} +1 -1
  19. package/dist/web/assets/{chunk-GF5L2VYU-Cuca3bAY.js → chunk-GF5L2VYU-mIdlxzfr.js} +1 -1
  20. package/dist/web/assets/{chunk-I66GZJ75-DRdqLSyH.js → chunk-I66GZJ75-CGl1hx4J.js} +1 -1
  21. package/dist/web/assets/{chunk-JQJVKLGR-8xhLkhs2.js → chunk-JQJVKLGR-BLG6gAfx.js} +1 -1
  22. package/dist/web/assets/{chunk-KBJHAD2P-oOV-3e5-.js → chunk-KBJHAD2P-D_MPzBJx.js} +1 -1
  23. package/dist/web/assets/{chunk-NSK5VX7P-CXit1l3q.js → chunk-NSK5VX7P-CiYiU8wP.js} +1 -1
  24. package/dist/web/assets/{chunk-QR6OTTB3-DcdCU-01.js → chunk-QR6OTTB3-BK8Tnvcc.js} +1 -1
  25. package/dist/web/assets/{chunk-UBXNYLIW-i72Hgljr.js → chunk-UBXNYLIW-BuzttTFk.js} +1 -1
  26. package/dist/web/assets/{chunk-W5SLKNZC-DetAQo7M.js → chunk-W5SLKNZC-UidmFHe2.js} +1 -1
  27. package/dist/web/assets/{chunk-WRU74C26-DDUvJFNs.js → chunk-WRU74C26-nox7WFnh.js} +1 -1
  28. package/dist/web/assets/classDiagram-JCYQIIEL-CmTWSnH0.js +1 -0
  29. package/dist/web/assets/classDiagram-v2-OCEON4UE-CmTWSnH0.js +1 -0
  30. package/dist/web/assets/{cynefin-VYW2F7L2-fd-v6ani.js → cynefin-VYW2F7L2-wDMibS5s.js} +1 -1
  31. package/dist/web/assets/{cynefinDiagram-MW4NZA55-DRsVpAC3.js → cynefinDiagram-MW4NZA55-CBTrdvOE.js} +1 -1
  32. package/dist/web/assets/{dagre-VZM6K2ZE-BnRSs_fe.js → dagre-VZM6K2ZE-Bbgn4Rr7.js} +1 -1
  33. package/dist/web/assets/{diagram-7IWD3JNH-D1g0PR4y.js → diagram-7IWD3JNH-Bm9AuXb9.js} +1 -1
  34. package/dist/web/assets/{diagram-B4RE2ZJO-DuDKsQLp.js → diagram-B4RE2ZJO-D3lZ-r-H.js} +1 -1
  35. package/dist/web/assets/{diagram-LBJQPF4R-COpzgGe7.js → diagram-LBJQPF4R-D-GaynX7.js} +1 -1
  36. package/dist/web/assets/{diagram-Q27KOJAE-DR2vRbPE.js → diagram-Q27KOJAE-BeZRiu1d.js} +1 -1
  37. package/dist/web/assets/{diagram-UB23O5K3-hs4VGPP7.js → diagram-UB23O5K3-C3GP7iMe.js} +1 -1
  38. package/dist/web/assets/{ebnfDiagram-BXEA7PRR-B_4NlK9q.js → ebnfDiagram-BXEA7PRR-BJWg0NVU.js} +1 -1
  39. package/dist/web/assets/{erDiagram-JOGREHBK-JyoZ3U-o.js → erDiagram-JOGREHBK-XPmhUeB_.js} +1 -1
  40. package/dist/web/assets/eventmodeling-45OFAUF4-C3c4qddw.js +1 -0
  41. package/dist/web/assets/flowDiagram-UKHOOZJN-DMA-a1zf.js +1 -0
  42. package/dist/web/assets/{ganttDiagram-PKOTCBZU-f37I0Twm.js → ganttDiagram-PKOTCBZU-CL-xEo-M.js} +1 -1
  43. package/dist/web/assets/{gitGraph-TEB2WS4Q-B_QWjr7j.js → gitGraph-TEB2WS4Q-BVRyuIx3.js} +1 -1
  44. package/dist/web/assets/{gitGraphDiagram-DS77QQ5N-AZsaC0hu.js → gitGraphDiagram-DS77QQ5N-BVNIEupW.js} +1 -1
  45. package/dist/web/assets/index-D8GJBw23.js +384 -0
  46. package/dist/web/assets/{index-kK3hngnz.css → index-DMD2vvQW.css} +1 -1
  47. package/dist/web/assets/{info-DKCQHKI2-BRfisbEI.js → info-DKCQHKI2-BKBUVpfc.js} +1 -1
  48. package/dist/web/assets/{infoDiagram-6WML65LV-CaIb64N5.js → infoDiagram-6WML65LV-_dTFnzSE.js} +1 -1
  49. package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-BoJrcTja.js → ishikawaDiagram-WSZJBQD7-DIRgD4hO.js} +1 -1
  50. package/dist/web/assets/{journeyDiagram-NVQOT4AX-CRR1dZy8.js → journeyDiagram-NVQOT4AX-bp7nZrfR.js} +1 -1
  51. package/dist/web/assets/{kanban-definition-27J2QSJJ-D5skDuP4.js → kanban-definition-27J2QSJJ-C6mZIo8S.js} +1 -1
  52. package/dist/web/assets/{line-BKKx0vSh.js → line-DWTym7eJ.js} +1 -1
  53. package/dist/web/assets/{mermaid-parser.core-DvZnIth0.js → mermaid-parser.core-CbecsNA1.js} +3 -3
  54. package/dist/web/assets/{mermaid.core-hykiOF_o.js → mermaid.core-CdRuLyYI.js} +4 -4
  55. package/dist/web/assets/{mindmap-definition-FAOFIHXS-BN2LQ4Yn.js → mindmap-definition-FAOFIHXS-CU2P1z-9.js} +1 -1
  56. package/dist/web/assets/{packet-7NZHBO7P-CS5I0NlV.js → packet-7NZHBO7P-CZsup6Od.js} +1 -1
  57. package/dist/web/assets/{pegDiagram-VL7TDLO6-WvOdhbxo.js → pegDiagram-VL7TDLO6-DTTyttpe.js} +1 -1
  58. package/dist/web/assets/{pie-RZYD4A2V-BwNAZrQ1.js → pie-RZYD4A2V-DgrIiWZA.js} +1 -1
  59. package/dist/web/assets/{pieDiagram-7S7Q4E2Y-CbyrZjm7.js → pieDiagram-7S7Q4E2Y-BBNFyPT1.js} +1 -1
  60. package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-CW9YBxAO.js → quadrantDiagram-CIZ2JOQS-ejp-Aa2l.js} +1 -1
  61. package/dist/web/assets/{radar-I7S5WNFK-DBoCLDNH.js → radar-I7S5WNFK-Cp_hfjrI.js} +1 -1
  62. package/dist/web/assets/{railroad-3IZDKUUU-BOjpYyDz.js → railroad-3IZDKUUU-Dw-oMal2.js} +1 -1
  63. package/dist/web/assets/railroad-abnf-AHOZXSZD-BN-ZBhE0.js +1 -0
  64. package/dist/web/assets/railroad-ebnf-EBAXGLYW-CG97zyI2.js +1 -0
  65. package/dist/web/assets/railroad-peg-LSFZ7HO6-DHLQ1evQ.js +1 -0
  66. package/dist/web/assets/{railroadDiagram-AXF67PYL-BdOOOq8o.js → railroadDiagram-AXF67PYL-DkBUaj6Z.js} +1 -1
  67. package/dist/web/assets/{requirementDiagram-LRYGKXZP-DIYlSrTb.js → requirementDiagram-LRYGKXZP-Dff1ex_h.js} +1 -1
  68. package/dist/web/assets/{sankeyDiagram-W5VNT64P-Cc1IGxp_.js → sankeyDiagram-W5VNT64P-DptSL9Bc.js} +1 -1
  69. package/dist/web/assets/{sequenceDiagram-SI44F4Z6-CJsPO9Z9.js → sequenceDiagram-SI44F4Z6-DEnPcs3U.js} +1 -1
  70. package/dist/web/assets/{stateDiagram-OKZ733FA-B033gpHZ.js → stateDiagram-OKZ733FA-BwJO1MKA.js} +1 -1
  71. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-LaSNqvWy.js +1 -0
  72. package/dist/web/assets/{swimlanes-SLNWSIFB-C0Tstg7e.js → swimlanes-SLNWSIFB-BosKiwZA.js} +1 -1
  73. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-C5OBtBlv.js +8 -0
  74. package/dist/web/assets/{timeline-definition-Z64GVDOM-BA3y-16W.js → timeline-definition-Z64GVDOM-C5isAn4X.js} +1 -1
  75. package/dist/web/assets/{treeView-QDETBFTQ-BUNq-JXr.js → treeView-QDETBFTQ-zEyUXRc4.js} +1 -1
  76. package/dist/web/assets/{treemap-6X3UGDF4-FIR_NbUo.js → treemap-6X3UGDF4-B-X0EQSE.js} +1 -1
  77. package/dist/web/assets/{vennDiagram-T6HMQDX7-IeFFjnlq.js → vennDiagram-T6HMQDX7-QMdGieCa.js} +1 -1
  78. package/dist/web/assets/{wardley-OPB4EBWU-DQAX4LKo.js → wardley-OPB4EBWU-BaeAF8d8.js} +1 -1
  79. package/dist/web/assets/{wardleyDiagram-T6FBY63Y-guGPPMAG.js → wardleyDiagram-T6FBY63Y-DTQ_uLUq.js} +1 -1
  80. package/dist/web/assets/{xychartDiagram-ELKLHX3M-Cj0v359_.js → xychartDiagram-ELKLHX3M-BMdBWzib.js} +1 -1
  81. package/dist/web/index.html +2 -2
  82. package/package.json +17 -14
  83. package/skills/rvw/SKILL.md +56 -15
  84. package/skills/rvw-watch-comments/SKILL.md +117 -47
  85. package/skills/rvw-watch-comments/agents/openai.yaml +2 -2
  86. package/skills/rvw-watch-comments/scripts/auto-ack.mjs +0 -0
  87. package/skills/rvw-watch-comments/scripts/preflight.mjs +0 -0
  88. package/skills/rvw-watch-comments/scripts/watch-driver.mjs +223 -48
  89. package/skills/rvw-watch-comments/scripts/watch-state.mjs +125 -64
  90. package/dist/web/assets/DocumentViewer-DvrDlIpR.js +0 -1560
  91. package/dist/web/assets/WalkthroughViewer-aDUeKC3B.js +0 -1
  92. package/dist/web/assets/architecture-TIHT7OUA-CuFS9GkN.js +0 -1
  93. package/dist/web/assets/channel-CTWz7tRI.js +0 -1
  94. package/dist/web/assets/classDiagram-JCYQIIEL-BJQv9QuO.js +0 -1
  95. package/dist/web/assets/classDiagram-v2-OCEON4UE-BJQv9QuO.js +0 -1
  96. package/dist/web/assets/eventmodeling-45OFAUF4-CfUhQK5g.js +0 -1
  97. package/dist/web/assets/flowDiagram-UKHOOZJN-FFZ82SPa.js +0 -1
  98. package/dist/web/assets/index-CY_JoUL3.js +0 -384
  99. package/dist/web/assets/railroad-abnf-AHOZXSZD-DQhDS2pf.js +0 -1
  100. package/dist/web/assets/railroad-ebnf-EBAXGLYW-Dtt2kOaN.js +0 -1
  101. package/dist/web/assets/railroad-peg-LSFZ7HO6-nfH8zno6.js +0 -1
  102. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-wS01qj3t.js +0 -1
  103. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-DoAqfWAd.js +0 -8
package/CHANGELOG.md CHANGED
@@ -5,6 +5,35 @@
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.2.3] - 2026-08-21
9
+
10
+ ### Added
11
+
12
+ - PR本文へ貼り付けたmodern GitHub user attachment画像を、public/private共通のlocalhost proxyで表示
13
+ - repositoryのPNG、JPEG、GIF、WebP、AVIF、SVGをexact commit固定の全文表示とold/new Split表示で閲覧
14
+ - 画像ファイルのadded、deleted、renamed、画像・非画像間の変更表示とfile-level comment
15
+
16
+ ### Fixed
17
+
18
+ - 同じtask stateで`rvw-watch-comments`のwatch driverを複数起動できないようにし、二重監視と重複処理を防止
19
+
20
+ ### Security
21
+
22
+ - GitHub attachment proxyを厳密な`https://github.com/user-attachments/assets/<uuid>`だけへ限定し、
23
+ 同一origin request、GitHub CLIの既存認証、10 MiB上限、magic-byte判定、`nosniff`、SVG sandbox CSPを適用
24
+ - arbitrary external image、legacy GitHub image host、unknown binary、HTML/JSON error responseは引き続き自動取得しない
25
+
26
+ ## [0.2.2] - 2026-08-21
27
+
28
+ ### Changed
29
+
30
+ - `rvw`と`rvw-watch-comments` Skillが具体的なcode上の結論、実装、testを投稿するとき、navigation価値のあるtyped code referenceを既定で付けるよう改善
31
+
32
+ ### Fixed
33
+
34
+ - `rvw-watch-comments`が同じthreadの後続replyを処理するとき、以前の回答を確認中へ戻さず、新しい確認中replyを最終回答へ更新するよう修正
35
+ - Markdown Previewで別commentへの返信が同期されても、入力中の未送信replyとfocusを保持するよう修正
36
+
8
37
  ## [0.2.1] - 2026-08-20
9
38
 
10
39
  ### Added
package/README.md CHANGED
@@ -118,8 +118,8 @@ rvw walkthrough publish --stdin --json
118
118
  構成されます。参照は意味のある複数行range、単行、またはfile全体を指せます。rvwは登録時にcommit、
119
119
  path、指定された行範囲を検証します。publishしてもbrowserは開かれず、
120
120
  active tabやscroll位置も変わりません。人間がviewerのWalkthroughを開き、必要なreferenceを選んだ
121
- 時だけ、exact sourceがdocument tabへ開き、行指定があれば範囲全体が強調されます。`Cmd` / `Ctrl`を押しながら選べば
122
- 操作元と反対のペインへ開きます。説明tabは残るため、
121
+ 時だけ、exact sourceがdocument tabへ開き、行指定があれば範囲全体が強調されます。通常clickは左ペイン、
122
+ `Cmd` / `Ctrl`を押しながら選べば右ペインへ開きます。説明tabは残るため、
123
123
  複数のclaimと実装を任意の順序・任意のタイミングで往復できます。
124
124
 
125
125
  Walkthrough本文のinline referenceとMermaid node linkは維持しますが、同じ参照を横や下へ列挙する
@@ -198,7 +198,13 @@ reference付きartifactとして検証してpublishします。Walkthrough全体
198
198
  PRは常にcode/GitHub read-onlyで調査します。同梱driverがcursor resumeとRFC 7464 ingestを行い、
199
199
  batchをclaimすると各threadへ`🔎 確認中です…`をAgent往復なしに即時返信し、
200
200
  完了時は同じreplyを最終結果へ編集します。Skill同梱のtask-state toolがrepository外のSQLiteへcursor、
201
- queue、retry、thread単位のstatus post、自己返信抑制をtransactionalに保存します。
201
+ queue、retry、batch内のthread単位status post、自己返信抑制をtransactionalに保存します。同じthreadへ
202
+ 後から返信が追加された場合は新しいstatus postを返信するため、以前の回答は書き換えません。親taskは
203
+ 起動前にsubagent枠を予約し、driverはその枠数までだけbatchをclaimします。acknowledge済みbatchは大小や
204
+ 変更有無にかかわらず同じscheduling turnでfresh subagentへ委譲し、親taskは直接処理しません。lease解放後の
205
+ 同一PR follow-upと期限到達したretryはdriverがtask stateから自動的に再開します。
206
+ 調査結果、実装内容、test結果が具体的なcodeに基づく場合、Skillは最終replyからexact commitの有用な
207
+ line rangeへ`rvw-ref:` linkを付け、reviewerが根拠へ直接移動できるようにします。
202
208
 
203
209
  三つのSkillはSQLiteを直接読まず、`rvw protocol --json`、`rvw comment ... --json`、
204
210
  `rvw walkthrough get/update/publish/delete ... --json`、`rvw pr sync --stdin --json`だけを利用します。
@@ -253,9 +259,11 @@ rvw pr attach <PR_REF> --repository <PATH> --json
253
259
  stdinをcloseし、shellではpipe、quoted heredoc、input redirectionのいずれかを使います。起動済みの
254
260
  対話commandへJSONと改行だけを送るとEOF待ちになります。
255
261
 
256
- `comment create`は登録済みPR、通常のcomment target、本文、任意のAgent名、任意の投稿単位code referenceをstdin JSONで受け取り、
262
+ `comment create`は登録済みPR、通常のcomment target、本文、任意のAgent名、投稿単位code referenceをstdin JSONで受け取り、
257
263
  未解決のroot threadを一件作成します。repository targetはexact commit、path、任意のinclusive line rangeを
258
264
  指定し、viewerと同じ文書・行検証を通ります。作成してもbrowserを開かず、tabやcommit選択を変更しません。
265
+ 同梱Skillは具体的なcode上のclaimにnavigation価値のある根拠がある場合、Walkthroughと同じくtyped
266
+ referenceを既定で使います。
259
267
 
260
268
  `comment list`は未解決を既定として`unresolved` / `resolved` / `all`をページング列挙し、各threadの
261
269
  root post preview、post件数、最新head時点のOutdated判定を返します。`hasMore`なら`nextOffset`から
@@ -295,6 +303,18 @@ maintainer向けのversion、tag、npm staged publishing、障害対応手順は
295
303
 
296
304
  ## 開発
297
305
 
306
+ source checkoutでは、GitHub接続や保存済みPRを用意せずにrepository規模のviewerを確認できます。
307
+
308
+ ```bash
309
+ pnpm demo
310
+ ```
311
+
312
+ このデモは小さなE2E fixtureとは分離され、現在のcheckoutにある直近6件のfirst-parent commitを
313
+ 一つのsynthetic PRとして表示します。tree、文書、diff、検索はworktreeではなく実際のGit objectから読み、
314
+ 100件以上の実在file、複数commit、変更外のtest・document・Skill、初期commentとWalkthroughを含みます。
315
+ browserを自動で開かない場合は`pnpm demo -- --no-open`、portを変える場合は`RVW_DEMO_PORT`を指定します。
316
+ デモを停止するには起動したterminalでCtrl+Cを押してください。
317
+
298
318
  ```bash
299
319
  pnpm install --frozen-lockfile
300
320
  pnpm check
package/dist/cli.mjs CHANGED
@@ -19759,12 +19759,15 @@ import path5 from "node:path";
19759
19759
  import { TextDecoder as TextDecoder2 } from "node:util";
19760
19760
 
19761
19761
  // src/shared/constants.ts
19762
- var APP_VERSION = "0.2.1";
19762
+ var APP_VERSION = "0.2.3";
19763
19763
  var PROTOCOL_VERSION = 3;
19764
19764
  var VIEWER_ID_HEADER = "x-rvw-viewer-id";
19765
19765
  var GIT_OBJECT_ID_PATTERN = /^[0-9a-f]{40,64}$/i;
19766
19766
  var MAX_TEXT_DOCUMENT_BYTES = 1024 * 1024;
19767
19767
  var MAX_MARKDOWN_ASSET_BYTES = 5 * 1024 * 1024;
19768
+ var MAX_GITHUB_ATTACHMENT_BYTES = 10 * 1024 * 1024;
19769
+ var MAX_GITHUB_ATTACHMENT_STDERR_BYTES = 64 * 1024;
19770
+ var GITHUB_ATTACHMENT_TIMEOUT_MS = 3e4;
19768
19771
  var MAX_COMMENT_BODY_BYTES = 64 * 1024;
19769
19772
  var MAX_AUTHOR_LABEL_CHARACTERS = 100;
19770
19773
  var MAX_IDEMPOTENCY_KEY_CHARACTERS = 200;
@@ -20414,6 +20417,101 @@ var GitClient = class {
20414
20417
  }
20415
20418
  };
20416
20419
 
20420
+ // src/shared/image-assets.ts
20421
+ var SUPPORTED_IMAGE_EXTENSIONS = [
20422
+ ".png",
20423
+ ".jpg",
20424
+ ".jpeg",
20425
+ ".gif",
20426
+ ".webp",
20427
+ ".avif",
20428
+ ".svg"
20429
+ ];
20430
+ var githubAttachmentPathPattern = /^\/user-attachments\/assets\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20431
+ var utf8Fatal2 = new TextDecoder("utf-8", { fatal: true });
20432
+ function hasBytes(bytes, offset, expected) {
20433
+ return expected.every((value, index2) => bytes[offset + index2] === value);
20434
+ }
20435
+ function ascii(bytes, offset, length) {
20436
+ return String.fromCharCode(...bytes.subarray(offset, offset + length));
20437
+ }
20438
+ function uint16Le(bytes, offset) {
20439
+ return (bytes[offset] ?? 0) | (bytes[offset + 1] ?? 0) << 8;
20440
+ }
20441
+ function uint32Be(bytes, offset) {
20442
+ return (bytes[offset] ?? 0) * 16777216 + ((bytes[offset + 1] ?? 0) << 16) + ((bytes[offset + 2] ?? 0) << 8) + (bytes[offset + 3] ?? 0) >>> 0;
20443
+ }
20444
+ function uint32Le(bytes, offset) {
20445
+ return (bytes[offset] ?? 0) + ((bytes[offset + 1] ?? 0) << 8) + ((bytes[offset + 2] ?? 0) << 16) + (bytes[offset + 3] ?? 0) * 16777216 >>> 0;
20446
+ }
20447
+ function isSvg(bytes) {
20448
+ if (bytes.length === 0) return false;
20449
+ let text3;
20450
+ try {
20451
+ text3 = utf8Fatal2.decode(bytes);
20452
+ } catch {
20453
+ return false;
20454
+ }
20455
+ const withoutBom = text3.charCodeAt(0) === 65279 ? text3.slice(1) : text3;
20456
+ let candidate = withoutBom.trimStart();
20457
+ const declaration = /^<\?xml(?:\s[^?]*)?\?>/iu.exec(candidate);
20458
+ if (declaration) candidate = candidate.slice(declaration[0].length).trimStart();
20459
+ while (candidate.startsWith("<!--")) {
20460
+ const commentEnd = candidate.indexOf("-->");
20461
+ if (commentEnd < 0) return false;
20462
+ candidate = candidate.slice(commentEnd + 3).trimStart();
20463
+ }
20464
+ return /^<svg(?:\s|>)/iu.test(candidate);
20465
+ }
20466
+ function isSupportedImagePath(filePath) {
20467
+ const normalized = filePath.toLowerCase();
20468
+ return SUPPORTED_IMAGE_EXTENSIONS.some((extension2) => normalized.endsWith(extension2));
20469
+ }
20470
+ function canonicalGitHubAttachmentUrl(value) {
20471
+ if (!value?.startsWith("https://github.com/")) return null;
20472
+ let parsed;
20473
+ try {
20474
+ parsed = new URL(value);
20475
+ } catch {
20476
+ return null;
20477
+ }
20478
+ if (parsed.protocol !== "https:" || parsed.hostname !== "github.com" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "" || parsed.search !== "" || parsed.hash !== "" || !githubAttachmentPathPattern.test(parsed.pathname)) {
20479
+ return null;
20480
+ }
20481
+ return parsed.href;
20482
+ }
20483
+ function detectImageContentType(bytes) {
20484
+ if (bytes.length >= 33 && hasBytes(bytes, 0, [137, 80, 78, 71, 13, 10, 26, 10]) && ascii(bytes, 12, 4) === "IHDR" && uint32Be(bytes, 16) > 0 && uint32Be(bytes, 20) > 0) {
20485
+ return "image/png";
20486
+ }
20487
+ if (bytes.length >= 10 && hasBytes(bytes, 0, [255, 216, 255]) && hasBytes(bytes, bytes.length - 2, [255, 217])) {
20488
+ return "image/jpeg";
20489
+ }
20490
+ const gifHeader = bytes.length >= 13 ? ascii(bytes, 0, 6) : "";
20491
+ if ((gifHeader === "GIF87a" || gifHeader === "GIF89a") && uint16Le(bytes, 6) > 0 && uint16Le(bytes, 8) > 0) {
20492
+ return "image/gif";
20493
+ }
20494
+ if (bytes.length >= 20 && ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WEBP" && ["VP8 ", "VP8L", "VP8X"].includes(ascii(bytes, 12, 4)) && uint32Le(bytes, 4) >= 12 && uint32Le(bytes, 4) + 8 <= bytes.length) {
20495
+ return "image/webp";
20496
+ }
20497
+ if (bytes.length >= 16 && ascii(bytes, 4, 4) === "ftyp") {
20498
+ const boxSize = uint32Be(bytes, 0);
20499
+ if (boxSize >= 16 && boxSize <= bytes.length) {
20500
+ const brands = [ascii(bytes, 8, 4)];
20501
+ for (let offset = 16; offset + 4 <= boxSize; offset += 4) {
20502
+ brands.push(ascii(bytes, offset, 4));
20503
+ }
20504
+ if (brands.some((brand) => brand === "avif" || brand === "avis")) {
20505
+ return "image/avif";
20506
+ }
20507
+ }
20508
+ }
20509
+ return isSvg(bytes) ? "image/svg+xml" : null;
20510
+ }
20511
+ function imageContentTypeHeader(contentType2) {
20512
+ return contentType2 === "image/svg+xml" ? `${contentType2}; charset=utf-8` : contentType2;
20513
+ }
20514
+
20417
20515
  // src/infrastructure/github/github-client.ts
20418
20516
  var ghPullRequestSchema = external_exports.object({
20419
20517
  author: external_exports.object({ login: external_exports.string().min(1) }).nullable(),
@@ -20439,6 +20537,10 @@ function parsePullRequestUrl(url2) {
20439
20537
  return { owner: match2[1], repository: match2[2], number: Number(match2[3]) };
20440
20538
  }
20441
20539
  var GitHubClient = class {
20540
+ constructor(processRunner = runProcess) {
20541
+ this.processRunner = processRunner;
20542
+ }
20543
+ processRunner;
20442
20544
  async doctor() {
20443
20545
  const version2 = await runText("gh", ["--version"]);
20444
20546
  const auth = await runProcess("gh", ["auth", "status", "--hostname", "github.com"], {
@@ -20523,6 +20625,31 @@ var GitHubClient = class {
20523
20625
  isDraft: parsed.data.isDraft
20524
20626
  };
20525
20627
  }
20628
+ async getAttachment(absoluteUrl) {
20629
+ const canonicalUrl = canonicalGitHubAttachmentUrl(absoluteUrl);
20630
+ if (!canonicalUrl) {
20631
+ throw new RvwError("INVALID_INPUT", "GitHub user attachment URL\u304C\u4E0D\u6B63\u3067\u3059\u3002");
20632
+ }
20633
+ try {
20634
+ const result = await this.processRunner("gh", ["api", canonicalUrl], {
20635
+ timeoutMs: GITHUB_ATTACHMENT_TIMEOUT_MS,
20636
+ maxStdoutBytes: MAX_GITHUB_ATTACHMENT_BYTES,
20637
+ maxStderrBytes: MAX_GITHUB_ATTACHMENT_STDERR_BYTES
20638
+ });
20639
+ return { content: result.stdout, byteLength: result.stdout.byteLength };
20640
+ } catch (error51) {
20641
+ if (error51 instanceof RvwError && error51.code === "PROCESS_OUTPUT_LIMIT" && error51.details?.stream === "stdout") {
20642
+ throw new RvwError("FILE_TOO_LARGE", "GitHub attachment\u306F10 MiB\u4EE5\u4E0B\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002", {
20643
+ status: 413
20644
+ });
20645
+ }
20646
+ throw new RvwError(
20647
+ "GITHUB_ERROR",
20648
+ "GitHub attachment\u3092\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002gh\u8A8D\u8A3C\u3068\u753B\u50CF\u306E\u95B2\u89A7\u6A29\u9650\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
20649
+ { status: 502 }
20650
+ );
20651
+ }
20652
+ }
20526
20653
  };
20527
20654
 
20528
20655
  // src/application/rvw-service.ts
@@ -28398,6 +28525,21 @@ var RvwService = class {
28398
28525
  await this.assertCommitAvailable(pullRequest, sourceOid);
28399
28526
  return await this.git.readRepositoryAsset(pullRequest.localRepositoryPath, sourceOid, filePath);
28400
28527
  }
28528
+ async getGitHubAttachment(pullRequestId, absoluteUrl) {
28529
+ this.getPullRequest(pullRequestId);
28530
+ const canonicalUrl = canonicalGitHubAttachmentUrl(absoluteUrl);
28531
+ if (!canonicalUrl) {
28532
+ throw new RvwError("INVALID_INPUT", "GitHub user attachment URL\u304C\u4E0D\u6B63\u3067\u3059\u3002");
28533
+ }
28534
+ const attachment = await this.github.getAttachment(canonicalUrl);
28535
+ const contentType2 = detectImageContentType(attachment.content);
28536
+ if (!contentType2) {
28537
+ throw new RvwError("UNSUPPORTED_IMAGE", "GitHub attachment\u306F\u5BFE\u5FDC\u753B\u50CF\u5F62\u5F0F\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002", {
28538
+ status: 415
28539
+ });
28540
+ }
28541
+ return { ...attachment, contentType: contentType2 };
28542
+ }
28401
28543
  async getDiff(ref) {
28402
28544
  const [old, next] = await Promise.all([
28403
28545
  ref.old ? this.getDocument(ref.old) : Promise.resolve(null),
@@ -33257,17 +33399,25 @@ function parseBooleanQuery(value, name) {
33257
33399
  if (value === "true") return true;
33258
33400
  throw new RvwError("INVALID_INPUT", `${name} query\u306Ftrue\u307E\u305F\u306Ffalse\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002`);
33259
33401
  }
33260
- function markdownAssetContentType(filePath) {
33261
- const extension2 = path8.extname(filePath).toLowerCase();
33262
- return {
33263
- ".avif": "image/avif",
33264
- ".gif": "image/gif",
33265
- ".jpeg": "image/jpeg",
33266
- ".jpg": "image/jpeg",
33267
- ".png": "image/png",
33268
- ".svg": "image/svg+xml; charset=utf-8",
33269
- ".webp": "image/webp"
33270
- }[extension2] ?? "application/octet-stream";
33402
+ function repositoryAssetContentType(filePath, content3) {
33403
+ if (!isSupportedImagePath(filePath)) return null;
33404
+ const contentType2 = detectImageContentType(content3);
33405
+ if (!contentType2) {
33406
+ throw new RvwError("UNSUPPORTED_IMAGE", "repository asset\u306F\u5BFE\u5FDC\u753B\u50CF\u5F62\u5F0F\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002", {
33407
+ status: 415
33408
+ });
33409
+ }
33410
+ return contentType2;
33411
+ }
33412
+ function setImageResponseHeaders(setHeader, contentType2) {
33413
+ setHeader("content-type", imageContentTypeHeader(contentType2));
33414
+ setHeader("cache-control", "private, max-age=31536000, immutable");
33415
+ setHeader("x-content-type-options", "nosniff");
33416
+ setHeader("content-disposition", "inline");
33417
+ setHeader("cross-origin-resource-policy", "same-origin");
33418
+ if (contentType2 === "image/svg+xml") {
33419
+ setHeader("content-security-policy", svgAssetContentSecurityPolicy);
33420
+ }
33271
33421
  }
33272
33422
  function documentRefFromQuery(pullRequestId, query) {
33273
33423
  if (query.kind === "pull-request-markdown") {
@@ -33395,17 +33545,36 @@ function createApp(service, options) {
33395
33545
  const ref = documentRefFromQuery(context.req.param("id"), context.req.query());
33396
33546
  return context.json({ ok: true, document: await service.getDocument(ref) });
33397
33547
  });
33398
- app.get("/api/pull-requests/:id/markdown-asset", async (context) => {
33548
+ app.on(["GET", "HEAD"], "/api/pull-requests/:id/markdown-asset", async (context) => {
33399
33549
  const sourceOid = oidQuery(context.req.query("sourceOid"), "sourceOid");
33400
33550
  const filePath = requiredQuery(context.req.query("path"), "path");
33401
33551
  const asset = await service.getRepositoryAsset(context.req.param("id"), sourceOid, filePath);
33402
- context.header("content-type", markdownAssetContentType(filePath));
33403
- context.header("cache-control", "private, max-age=31536000, immutable");
33404
- context.header("x-content-type-options", "nosniff");
33405
- if (path8.extname(filePath).toLowerCase() === ".svg") {
33406
- context.header("content-security-policy", svgAssetContentSecurityPolicy);
33552
+ const contentType2 = repositoryAssetContentType(filePath, asset.content);
33553
+ if (contentType2) {
33554
+ setImageResponseHeaders((name, value) => context.header(name, value), contentType2);
33555
+ } else {
33556
+ context.header("content-type", "application/octet-stream");
33557
+ context.header("cache-control", "private, max-age=31536000, immutable");
33558
+ context.header("x-content-type-options", "nosniff");
33559
+ }
33560
+ return context.req.method === "HEAD" ? context.body(null) : context.body(Uint8Array.from(asset.content));
33561
+ });
33562
+ app.get("/api/pull-requests/:id/github-attachment", async (context) => {
33563
+ const fetchSite = context.req.header("sec-fetch-site");
33564
+ const origin = context.req.header("origin");
33565
+ if (fetchSite !== void 0 && fetchSite !== "same-origin" && fetchSite !== "none" || origin !== void 0 && origin !== options.security.expectedOrigin) {
33566
+ throw new RvwError(
33567
+ "INVALID_ORIGIN",
33568
+ "cross-origin attachment request\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
33569
+ {
33570
+ status: 403
33571
+ }
33572
+ );
33407
33573
  }
33408
- return context.body(Uint8Array.from(asset.content));
33574
+ const absoluteUrl = requiredQuery(context.req.query("url"), "url");
33575
+ const attachment = await service.getGitHubAttachment(context.req.param("id"), absoluteUrl);
33576
+ setImageResponseHeaders((name, value) => context.header(name, value), attachment.contentType);
33577
+ return context.body(Uint8Array.from(attachment.content));
33409
33578
  });
33410
33579
  app.get("/api/pull-requests/:id/diff", async (context) => {
33411
33580
  const pullRequestId = context.req.param("id");