@amg555/frameforge-mcp 2.7.6 → 2.7.7

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 (1129) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +8 -0
  3. package/package.json +1 -1
  4. package/plugins/video-pipeline/.claude-plugin/plugin.json +1 -1
  5. package/plugins/video-pipeline/skills/.upstream-skills.json +1 -1
  6. package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +1 -1
  7. package/scripts/deep-validate.js +2 -2
  8. package/src/mcp-server/server.js +5 -5
  9. package/src/skills/sync-upstream.js +3 -3
  10. package/.agent/skills/ai-video-editor/README.md +0 -32
  11. package/.agent/skills/ai-video-editor/SKILL.md +0 -152
  12. package/.agent/skills/ai-video-editor/assets/fonts/fonts.css +0 -2409
  13. package/.agent/skills/ai-video-editor/assets/fonts/ttf/anton-400.ttf +0 -0
  14. package/.agent/skills/ai-video-editor/assets/fonts/ttf/archivo-800.ttf +0 -0
  15. package/.agent/skills/ai-video-editor/assets/fonts/ttf/cabin-sketch-700.ttf +0 -0
  16. package/.agent/skills/ai-video-editor/assets/fonts/ttf/dm-sans-700.ttf +0 -0
  17. package/.agent/skills/ai-video-editor/assets/fonts/ttf/fraunces-600.ttf +0 -0
  18. package/.agent/skills/ai-video-editor/assets/fonts/ttf/fraunces-700.ttf +0 -0
  19. package/.agent/skills/ai-video-editor/assets/fonts/ttf/fredoka-700.ttf +0 -0
  20. package/.agent/skills/ai-video-editor/assets/fonts/ttf/inter-700.ttf +0 -0
  21. package/.agent/skills/ai-video-editor/assets/fonts/ttf/inter-800.ttf +0 -0
  22. package/.agent/skills/ai-video-editor/assets/fonts/ttf/inter-tight-800.ttf +0 -0
  23. package/.agent/skills/ai-video-editor/assets/fonts/ttf/kalam-700.ttf +0 -0
  24. package/.agent/skills/ai-video-editor/assets/fonts/ttf/noto-sans-devanagari-700.ttf +0 -0
  25. package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-400-normal.woff2 +0 -0
  26. package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-500-normal.woff2 +0 -0
  27. package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-600-normal.woff2 +0 -0
  28. package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-700-normal.woff2 +0 -0
  29. package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-800-normal.woff2 +0 -0
  30. package/.agent/skills/ai-video-editor/engine/hidpi.js +0 -117
  31. package/.agent/skills/ai-video-editor/engine/page.html +0 -43
  32. package/.agent/skills/ai-video-editor/engine/player.js +0 -145
  33. package/.agent/skills/ai-video-editor/references/example-story-warm-canvas.js +0 -95
  34. package/.agent/skills/ai-video-editor/references/story-guide.md +0 -112
  35. package/.agent/skills/ai-video-editor/requirements.txt +0 -4
  36. package/.agent/skills/ai-video-editor/styles/chalk-talk/base.js +0 -490
  37. package/.agent/skills/ai-video-editor/styles/chalk-talk/style.json +0 -14
  38. package/.agent/skills/ai-video-editor/styles/chalk-talk/style.md +0 -41
  39. package/.agent/skills/ai-video-editor/styles/clean-tech/base.js +0 -447
  40. package/.agent/skills/ai-video-editor/styles/clean-tech/style.json +0 -15
  41. package/.agent/skills/ai-video-editor/styles/clean-tech/style.md +0 -38
  42. package/.agent/skills/ai-video-editor/styles/creator-motion-graphics/base.js +0 -698
  43. package/.agent/skills/ai-video-editor/styles/creator-motion-graphics/style.json +0 -16
  44. package/.agent/skills/ai-video-editor/styles/creator-motion-graphics/style.md +0 -39
  45. package/.agent/skills/ai-video-editor/styles/data-story/base.js +0 -432
  46. package/.agent/skills/ai-video-editor/styles/data-story/style.json +0 -14
  47. package/.agent/skills/ai-video-editor/styles/data-story/style.md +0 -38
  48. package/.agent/skills/ai-video-editor/styles/index.json +0 -103
  49. package/.agent/skills/ai-video-editor/styles/pop-info/base.js +0 -570
  50. package/.agent/skills/ai-video-editor/styles/pop-info/style.json +0 -13
  51. package/.agent/skills/ai-video-editor/styles/pop-info/style.md +0 -37
  52. package/.agent/skills/ai-video-editor/styles/software-walkthrough/base.js +0 -544
  53. package/.agent/skills/ai-video-editor/styles/software-walkthrough/style.json +0 -15
  54. package/.agent/skills/ai-video-editor/styles/software-walkthrough/style.md +0 -38
  55. package/.agent/skills/ai-video-editor/styles/warm-canvas/base.js +0 -610
  56. package/.agent/skills/ai-video-editor/styles/warm-canvas/style.json +0 -14
  57. package/.agent/skills/ai-video-editor/styles/warm-canvas/style.md +0 -37
  58. package/.agent/skills/faceless-shorts/README.md +0 -34
  59. package/.agent/skills/faceless-shorts/SKILL.md +0 -124
  60. package/.agent/skills/faceless-shorts/styles/deep-dive/STYLE.md +0 -58
  61. package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/cues.json +0 -140
  62. package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/lines.json +0 -67
  63. package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/words.json +0 -614
  64. package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/words.txt +0 -102
  65. package/.agent/skills/faceless-shorts/styles/deep-dive/example/fonts/Inter-Black.otf +0 -0
  66. package/.agent/skills/faceless-shorts/styles/deep-dive/example/fonts/Inter-ExtraBold.otf +0 -0
  67. package/.agent/skills/faceless-shorts/styles/deep-dive/example/index.html +0 -16
  68. package/.agent/skills/faceless-shorts/styles/deep-dive/example/script.json +0 -27
  69. package/.agent/skills/faceless-shorts/styles/deep-dive/example/sheet.html +0 -10
  70. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/board.js +0 -62
  71. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/captions.js +0 -68
  72. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/engine.js +0 -65
  73. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/human.js +0 -128
  74. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/lib.js +0 -44
  75. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/look2d.js +0 -33
  76. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/sdf.js +0 -79
  77. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/sets.js +0 -107
  78. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/timing.js +0 -23
  79. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/main.js +0 -56
  80. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/sheet.js +0 -79
  81. package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/shots.js +0 -136
  82. package/.agent/skills/faceless-shorts/styles/deep-dive/example/tools/capture.mjs +0 -122
  83. package/.agent/skills/faceless-shorts/styles/deep-dive/example/tools/mix.py +0 -172
  84. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/LICENSE-three.txt +0 -21
  85. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/environments/RoomEnvironment.js +0 -144
  86. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/BokehPass.js +0 -141
  87. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/EffectComposer.js +0 -231
  88. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/MaskPass.js +0 -104
  89. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/OutputPass.js +0 -97
  90. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/Pass.js +0 -95
  91. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/RenderPass.js +0 -99
  92. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/ShaderPass.js +0 -77
  93. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/UnrealBloomPass.js +0 -415
  94. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/BokehShader.js +0 -145
  95. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/CopyShader.js +0 -45
  96. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/LuminosityHighPassShader.js +0 -64
  97. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/OutputShader.js +0 -85
  98. package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/three.module.js +0 -6
  99. package/.agent/skills/faceless-shorts/styles/deep-dive/style-sheet.png +0 -0
  100. package/.agent/skills/faceless-shorts/styles/inside-out/STYLE.md +0 -97
  101. package/.agent/skills/faceless-shorts/styles/inside-out/example/data/cues.json +0 -178
  102. package/.agent/skills/faceless-shorts/styles/inside-out/example/data/lines.json +0 -73
  103. package/.agent/skills/faceless-shorts/styles/inside-out/example/data/words.json +0 -662
  104. package/.agent/skills/faceless-shorts/styles/inside-out/example/fonts/Inter-Black.otf +0 -0
  105. package/.agent/skills/faceless-shorts/styles/inside-out/example/index.html +0 -16
  106. package/.agent/skills/faceless-shorts/styles/inside-out/example/script.json +0 -26
  107. package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/captions.js +0 -68
  108. package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/kit.js +0 -345
  109. package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/lib.js +0 -44
  110. package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/timing.js +0 -23
  111. package/.agent/skills/faceless-shorts/styles/inside-out/example/src/main.js +0 -62
  112. package/.agent/skills/faceless-shorts/styles/inside-out/example/src/shots.js +0 -195
  113. package/.agent/skills/faceless-shorts/styles/inside-out/example/tools/capture.mjs +0 -122
  114. package/.agent/skills/faceless-shorts/styles/inside-out/example/tools/mix.py +0 -172
  115. package/.agent/skills/faceless-shorts/styles/inside-out/style-sheet.png +0 -0
  116. package/.agent/skills/faceless-shorts/styles/night-file/STYLE.md +0 -36
  117. package/.agent/skills/faceless-shorts/styles/night-file/example/data/cues.json +0 -146
  118. package/.agent/skills/faceless-shorts/styles/night-file/example/data/lines.json +0 -73
  119. package/.agent/skills/faceless-shorts/styles/night-file/example/data/words.json +0 -572
  120. package/.agent/skills/faceless-shorts/styles/night-file/example/data/words.txt +0 -95
  121. package/.agent/skills/faceless-shorts/styles/night-file/example/fonts/FreeMonoBold.ttf +0 -0
  122. package/.agent/skills/faceless-shorts/styles/night-file/example/fonts/Inter-Black.otf +0 -0
  123. package/.agent/skills/faceless-shorts/styles/night-file/example/index.html +0 -16
  124. package/.agent/skills/faceless-shorts/styles/night-file/example/script.json +0 -26
  125. package/.agent/skills/faceless-shorts/styles/night-file/example/sheet.html +0 -11
  126. package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/board.js +0 -62
  127. package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/captions.js +0 -49
  128. package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/engine.js +0 -65
  129. package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/human.js +0 -128
  130. package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/kit.js +0 -151
  131. package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/lib.js +0 -44
  132. package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/sdf.js +0 -79
  133. package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/sets3d.js +0 -106
  134. package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/timing.js +0 -23
  135. package/.agent/skills/faceless-shorts/styles/night-file/example/src/main.js +0 -56
  136. package/.agent/skills/faceless-shorts/styles/night-file/example/src/sheet.js +0 -86
  137. package/.agent/skills/faceless-shorts/styles/night-file/example/src/shots.js +0 -130
  138. package/.agent/skills/faceless-shorts/styles/night-file/example/tools/capture.mjs +0 -122
  139. package/.agent/skills/faceless-shorts/styles/night-file/example/tools/mix.py +0 -172
  140. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/LICENSE-three.txt +0 -21
  141. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/environments/RoomEnvironment.js +0 -144
  142. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/BokehPass.js +0 -141
  143. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/EffectComposer.js +0 -231
  144. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/MaskPass.js +0 -104
  145. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/OutputPass.js +0 -97
  146. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/Pass.js +0 -95
  147. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/RenderPass.js +0 -99
  148. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/ShaderPass.js +0 -77
  149. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/UnrealBloomPass.js +0 -415
  150. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/BokehShader.js +0 -145
  151. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/CopyShader.js +0 -45
  152. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/LuminosityHighPassShader.js +0 -64
  153. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/OutputShader.js +0 -85
  154. package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/three.module.js +0 -6
  155. package/.agent/skills/faceless-shorts/styles/night-file/style-sheet.png +0 -0
  156. package/.agent/skills/faceless-shorts/styles/paper-atlas/STYLE.md +0 -30
  157. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/cues.json +0 -167
  158. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/lines.json +0 -67
  159. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/words.json +0 -614
  160. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/words.txt +0 -102
  161. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/fonts/Caladea-Bold.ttf +0 -0
  162. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/fonts/Inter-Black.otf +0 -0
  163. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/index.html +0 -13
  164. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/script.json +0 -26
  165. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/sheet.html +0 -10
  166. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/board.js +0 -62
  167. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/captions.js +0 -47
  168. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/kit.js +0 -152
  169. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/lib.js +0 -44
  170. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/timing.js +0 -23
  171. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/main.js +0 -62
  172. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/sheet.js +0 -61
  173. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/shots.js +0 -139
  174. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/tools/capture.mjs +0 -122
  175. package/.agent/skills/faceless-shorts/styles/paper-atlas/example/tools/mix.py +0 -172
  176. package/.agent/skills/faceless-shorts/styles/paper-atlas/style-sheet.png +0 -0
  177. package/.agent/skills/faceless-shorts/styles/stick-story/STYLE.md +0 -32
  178. package/.agent/skills/faceless-shorts/styles/stick-story/example/data/cues.json +0 -182
  179. package/.agent/skills/faceless-shorts/styles/stick-story/example/data/lines.json +0 -79
  180. package/.agent/skills/faceless-shorts/styles/stick-story/example/data/words.json +0 -662
  181. package/.agent/skills/faceless-shorts/styles/stick-story/example/data/words.txt +0 -110
  182. package/.agent/skills/faceless-shorts/styles/stick-story/example/fonts/Inter-Black.otf +0 -0
  183. package/.agent/skills/faceless-shorts/styles/stick-story/example/fonts/Inter-ExtraBold.otf +0 -0
  184. package/.agent/skills/faceless-shorts/styles/stick-story/example/index.html +0 -12
  185. package/.agent/skills/faceless-shorts/styles/stick-story/example/script.json +0 -27
  186. package/.agent/skills/faceless-shorts/styles/stick-story/example/sheet.html +0 -9
  187. package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/board.js +0 -62
  188. package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/captions.js +0 -47
  189. package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/kit.js +0 -166
  190. package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/lib.js +0 -44
  191. package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/timing.js +0 -23
  192. package/.agent/skills/faceless-shorts/styles/stick-story/example/src/main.js +0 -62
  193. package/.agent/skills/faceless-shorts/styles/stick-story/example/src/sheet.js +0 -64
  194. package/.agent/skills/faceless-shorts/styles/stick-story/example/src/shots.js +0 -148
  195. package/.agent/skills/faceless-shorts/styles/stick-story/example/tools/capture.mjs +0 -122
  196. package/.agent/skills/faceless-shorts/styles/stick-story/example/tools/mix.py +0 -172
  197. package/.agent/skills/faceless-shorts/styles/stick-story/style-sheet.png +0 -0
  198. package/.agent/skills/faceless-shorts/styles/tall-tale/STYLE.md +0 -51
  199. package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/cues.json +0 -271
  200. package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/lines.json +0 -127
  201. package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/words.json +0 -908
  202. package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/words.txt +0 -151
  203. package/.agent/skills/faceless-shorts/styles/tall-tale/example/fonts/Caladea-Bold.ttf +0 -0
  204. package/.agent/skills/faceless-shorts/styles/tall-tale/example/fonts/Inter-Black.otf +0 -0
  205. package/.agent/skills/faceless-shorts/styles/tall-tale/example/fonts/Inter-ExtraBold.otf +0 -0
  206. package/.agent/skills/faceless-shorts/styles/tall-tale/example/index.html +0 -17
  207. package/.agent/skills/faceless-shorts/styles/tall-tale/example/script.json +0 -37
  208. package/.agent/skills/faceless-shorts/styles/tall-tale/example/sheet.html +0 -11
  209. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/board.js +0 -62
  210. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/captions.js +0 -56
  211. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/engine.js +0 -65
  212. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/lib.js +0 -44
  213. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/props.js +0 -166
  214. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/timing.js +0 -23
  215. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/toon.js +0 -352
  216. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/world.js +0 -340
  217. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/main.js +0 -56
  218. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/sheet.js +0 -99
  219. package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/shots.js +0 -377
  220. package/.agent/skills/faceless-shorts/styles/tall-tale/example/tools/capture.mjs +0 -122
  221. package/.agent/skills/faceless-shorts/styles/tall-tale/example/tools/mix.py +0 -204
  222. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/LICENSE-three.txt +0 -21
  223. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/environments/RoomEnvironment.js +0 -144
  224. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/BokehPass.js +0 -141
  225. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/EffectComposer.js +0 -231
  226. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/MaskPass.js +0 -104
  227. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/OutputPass.js +0 -97
  228. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/Pass.js +0 -95
  229. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/RenderPass.js +0 -99
  230. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/ShaderPass.js +0 -77
  231. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/UnrealBloomPass.js +0 -415
  232. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/BokehShader.js +0 -145
  233. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/CopyShader.js +0 -45
  234. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/LuminosityHighPassShader.js +0 -64
  235. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/OutputShader.js +0 -85
  236. package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/three.module.js +0 -6
  237. package/.agent/skills/faceless-shorts/styles/tall-tale/style-sheet.png +0 -0
  238. package/.agent/skills/gstack-autoplan/SKILL.md +0 -972
  239. package/.agent/skills/gstack-autoplan/SKILL.md.tmpl +0 -468
  240. package/.agent/skills/gstack-autoplan/bin/guard-journal.ts +0 -115
  241. package/.agent/skills/gstack-autoplan/bin/guard-log.ts +0 -40
  242. package/.agent/skills/gstack-autoplan/bin/guard-reasons.ts +0 -92
  243. package/.agent/skills/gstack-autoplan/bin/owned-read.ts +0 -72
  244. package/.agent/skills/gstack-autoplan/bin/phase-publication-hook +0 -15
  245. package/.agent/skills/gstack-autoplan/bin/phase-publication-hook.ts +0 -769
  246. package/.agent/skills/gstack-autoplan/sections/ceo-phase.md +0 -195
  247. package/.agent/skills/gstack-autoplan/sections/ceo-phase.md.tmpl +0 -147
  248. package/.agent/skills/gstack-autoplan/sections/design-phase.md +0 -128
  249. package/.agent/skills/gstack-autoplan/sections/design-phase.md.tmpl +0 -82
  250. package/.agent/skills/gstack-autoplan/sections/dx-phase.md +0 -155
  251. package/.agent/skills/gstack-autoplan/sections/dx-phase.md.tmpl +0 -109
  252. package/.agent/skills/gstack-autoplan/sections/eng-phase.md +0 -164
  253. package/.agent/skills/gstack-autoplan/sections/eng-phase.md.tmpl +0 -116
  254. package/.agent/skills/gstack-autoplan/sections/manifest.json +0 -50
  255. package/.agent/skills/gstack-autoplan/sections/phase-close.md +0 -70
  256. package/.agent/skills/gstack-autoplan/sections/phase-close.md.tmpl +0 -68
  257. package/.agent/skills/gstack-autoplan/sections/tasks-aggregator.md +0 -71
  258. package/.agent/skills/gstack-autoplan/sections/tasks-aggregator.md.tmpl +0 -1
  259. package/.agent/skills/gstack-autoplan/sections/unattended.md +0 -11
  260. package/.agent/skills/gstack-autoplan/sections/unattended.md.tmpl +0 -9
  261. package/.agent/skills/gstack-benchmark/SKILL.md +0 -456
  262. package/.agent/skills/gstack-benchmark/SKILL.md.tmpl +0 -249
  263. package/.agent/skills/gstack-benchmark-models/SKILL.md +0 -285
  264. package/.agent/skills/gstack-benchmark-models/SKILL.md.tmpl +0 -155
  265. package/.agent/skills/gstack-browse/PLAN-snapshot-dropdown-interactive.md +0 -102
  266. package/.agent/skills/gstack-browse/SKILL.md +0 -470
  267. package/.agent/skills/gstack-browse/SKILL.md.tmpl +0 -129
  268. package/.agent/skills/gstack-browse/bin/find-browse +0 -21
  269. package/.agent/skills/gstack-browse/bin/remote-slug +0 -18
  270. package/.agent/skills/gstack-browse/sections/command-list.md +0 -176
  271. package/.agent/skills/gstack-browse/sections/command-list.md.tmpl +0 -7
  272. package/.agent/skills/gstack-browse/sections/manifest.json +0 -14
  273. package/.agent/skills/gstack-browse/src/activity.ts +0 -209
  274. package/.agent/skills/gstack-browse/src/audit.ts +0 -69
  275. package/.agent/skills/gstack-browse/src/browse-client.ts +0 -264
  276. package/.agent/skills/gstack-browse/src/browser-manager.ts +0 -2334
  277. package/.agent/skills/gstack-browse/src/browser-skill-commands.ts +0 -466
  278. package/.agent/skills/gstack-browse/src/browser-skill-write.ts +0 -216
  279. package/.agent/skills/gstack-browse/src/browser-skills.ts +0 -420
  280. package/.agent/skills/gstack-browse/src/buffers.ts +0 -137
  281. package/.agent/skills/gstack-browse/src/bun-child-config.ts +0 -35
  282. package/.agent/skills/gstack-browse/src/bun-polyfill.cjs +0 -276
  283. package/.agent/skills/gstack-browse/src/cdp-allowlist.ts +0 -235
  284. package/.agent/skills/gstack-browse/src/cdp-bridge.ts +0 -180
  285. package/.agent/skills/gstack-browse/src/cdp-commands.ts +0 -64
  286. package/.agent/skills/gstack-browse/src/cdp-inspector.ts +0 -824
  287. package/.agent/skills/gstack-browse/src/chromium-profiles.ts +0 -212
  288. package/.agent/skills/gstack-browse/src/claude-bin.ts +0 -2
  289. package/.agent/skills/gstack-browse/src/cli.ts +0 -2254
  290. package/.agent/skills/gstack-browse/src/commands.ts +0 -296
  291. package/.agent/skills/gstack-browse/src/config.ts +0 -307
  292. package/.agent/skills/gstack-browse/src/content-security.ts +0 -373
  293. package/.agent/skills/gstack-browse/src/cookie-auth-verification.ts +0 -225
  294. package/.agent/skills/gstack-browse/src/cookie-database.ts +0 -73
  295. package/.agent/skills/gstack-browse/src/cookie-import-browser.ts +0 -929
  296. package/.agent/skills/gstack-browse/src/cookie-import-native-integrity.ts +0 -95
  297. package/.agent/skills/gstack-browse/src/cookie-import-native-job.ts +0 -161
  298. package/.agent/skills/gstack-browse/src/cookie-import-native-qualification.json +0 -1
  299. package/.agent/skills/gstack-browse/src/cookie-import-native-worker.ts +0 -268
  300. package/.agent/skills/gstack-browse/src/cookie-import-native.ts +0 -166
  301. package/.agent/skills/gstack-browse/src/cookie-import-operation.ts +0 -232
  302. package/.agent/skills/gstack-browse/src/cookie-picker-routes.ts +0 -343
  303. package/.agent/skills/gstack-browse/src/cookie-picker-ui.ts +0 -717
  304. package/.agent/skills/gstack-browse/src/domain-skill-commands.ts +0 -301
  305. package/.agent/skills/gstack-browse/src/domain-skills.ts +0 -438
  306. package/.agent/skills/gstack-browse/src/error-handling.ts +0 -2
  307. package/.agent/skills/gstack-browse/src/extension-id.ts +0 -25
  308. package/.agent/skills/gstack-browse/src/file-permissions.ts +0 -370
  309. package/.agent/skills/gstack-browse/src/find-browse.ts +0 -118
  310. package/.agent/skills/gstack-browse/src/find-security-sidecar.ts +0 -78
  311. package/.agent/skills/gstack-browse/src/media-extract.ts +0 -177
  312. package/.agent/skills/gstack-browse/src/memory-command.ts +0 -115
  313. package/.agent/skills/gstack-browse/src/memory-snapshot.ts +0 -73
  314. package/.agent/skills/gstack-browse/src/meta-commands.ts +0 -1150
  315. package/.agent/skills/gstack-browse/src/network-capture.ts +0 -179
  316. package/.agent/skills/gstack-browse/src/path-security.ts +0 -132
  317. package/.agent/skills/gstack-browse/src/platform.ts +0 -38
  318. package/.agent/skills/gstack-browse/src/port-allocator.ts +0 -140
  319. package/.agent/skills/gstack-browse/src/project-slug.ts +0 -36
  320. package/.agent/skills/gstack-browse/src/proxy-config.ts +0 -283
  321. package/.agent/skills/gstack-browse/src/proxy-redact.ts +0 -46
  322. package/.agent/skills/gstack-browse/src/pty-session-cookie.ts +0 -64
  323. package/.agent/skills/gstack-browse/src/pty-session-lease.ts +0 -137
  324. package/.agent/skills/gstack-browse/src/read-commands.ts +0 -756
  325. package/.agent/skills/gstack-browse/src/routes/activity.ts +0 -63
  326. package/.agent/skills/gstack-browse/src/routes/commands.ts +0 -154
  327. package/.agent/skills/gstack-browse/src/routes/core.ts +0 -123
  328. package/.agent/skills/gstack-browse/src/routes/files.ts +0 -47
  329. package/.agent/skills/gstack-browse/src/routes/index.ts +0 -27
  330. package/.agent/skills/gstack-browse/src/routes/inspector.ts +0 -132
  331. package/.agent/skills/gstack-browse/src/routes/pairing.ts +0 -158
  332. package/.agent/skills/gstack-browse/src/routes/pty.ts +0 -254
  333. package/.agent/skills/gstack-browse/src/routes/table.ts +0 -182
  334. package/.agent/skills/gstack-browse/src/routes/tokens.ts +0 -86
  335. package/.agent/skills/gstack-browse/src/routes/tunnel.ts +0 -62
  336. package/.agent/skills/gstack-browse/src/sanitize.ts +0 -49
  337. package/.agent/skills/gstack-browse/src/screenshot-size-guard.ts +0 -106
  338. package/.agent/skills/gstack-browse/src/security-classifier.ts +0 -258
  339. package/.agent/skills/gstack-browse/src/security-sidecar-client.ts +0 -234
  340. package/.agent/skills/gstack-browse/src/security-sidecar-entry.ts +0 -120
  341. package/.agent/skills/gstack-browse/src/security.ts +0 -328
  342. package/.agent/skills/gstack-browse/src/server.ts +0 -2220
  343. package/.agent/skills/gstack-browse/src/session-cookie-store.ts +0 -124
  344. package/.agent/skills/gstack-browse/src/session-persist.ts +0 -178
  345. package/.agent/skills/gstack-browse/src/skill-token.ts +0 -91
  346. package/.agent/skills/gstack-browse/src/snapshot-flags.ts +0 -30
  347. package/.agent/skills/gstack-browse/src/snapshot.ts +0 -656
  348. package/.agent/skills/gstack-browse/src/socks-bridge.ts +0 -314
  349. package/.agent/skills/gstack-browse/src/sse-helpers.ts +0 -147
  350. package/.agent/skills/gstack-browse/src/sse-session-cookie.ts +0 -65
  351. package/.agent/skills/gstack-browse/src/stealth.ts +0 -574
  352. package/.agent/skills/gstack-browse/src/tab-session.ts +0 -210
  353. package/.agent/skills/gstack-browse/src/telemetry.ts +0 -115
  354. package/.agent/skills/gstack-browse/src/terminal-agent-control.ts +0 -379
  355. package/.agent/skills/gstack-browse/src/terminal-agent.ts +0 -1124
  356. package/.agent/skills/gstack-browse/src/terminal-pty-lifecycle.ts +0 -144
  357. package/.agent/skills/gstack-browse/src/token-registry.ts +0 -721
  358. package/.agent/skills/gstack-browse/src/tunnel-denial-log.ts +0 -97
  359. package/.agent/skills/gstack-browse/src/url-validation.ts +0 -299
  360. package/.agent/skills/gstack-browse/src/welcome.html +0 -237
  361. package/.agent/skills/gstack-browse/src/write-commands.ts +0 -1438
  362. package/.agent/skills/gstack-browse/src/xprotect-heal.ts +0 -439
  363. package/.agent/skills/gstack-browse/src/xvfb.ts +0 -313
  364. package/.agent/skills/gstack-browse/test/activity.test.ts +0 -120
  365. package/.agent/skills/gstack-browse/test/adversarial-security.test.ts +0 -32
  366. package/.agent/skills/gstack-browse/test/batch.test.ts +0 -267
  367. package/.agent/skills/gstack-browse/test/bridge-chromium-e2e.test.ts +0 -205
  368. package/.agent/skills/gstack-browse/test/browse-client.test.ts +0 -300
  369. package/.agent/skills/gstack-browse/test/browse-eval-wrapping.test.ts +0 -86
  370. package/.agent/skills/gstack-browse/test/browser-manager-custom-chromium.test.ts +0 -67
  371. package/.agent/skills/gstack-browse/test/browser-manager-unit.test.ts +0 -335
  372. package/.agent/skills/gstack-browse/test/browser-skill-commands.test.ts +0 -433
  373. package/.agent/skills/gstack-browse/test/browser-skill-write.test.ts +0 -350
  374. package/.agent/skills/gstack-browse/test/browser-skills-e2e.test.ts +0 -101
  375. package/.agent/skills/gstack-browse/test/browser-skills-storage.test.ts +0 -283
  376. package/.agent/skills/gstack-browse/test/build-command-response.test.ts +0 -68
  377. package/.agent/skills/gstack-browse/test/build.test.ts +0 -52
  378. package/.agent/skills/gstack-browse/test/bun-child-config.test.ts +0 -83
  379. package/.agent/skills/gstack-browse/test/bun-polyfill.test.ts +0 -402
  380. package/.agent/skills/gstack-browse/test/busy-daemon-iron-rule.test.ts +0 -270
  381. package/.agent/skills/gstack-browse/test/busy-daemon-recovery.test.ts +0 -134
  382. package/.agent/skills/gstack-browse/test/cdp-allowlist.test.ts +0 -109
  383. package/.agent/skills/gstack-browse/test/cdp-e2e.test.ts +0 -117
  384. package/.agent/skills/gstack-browse/test/cdp-inspector-history-cap.test.ts +0 -95
  385. package/.agent/skills/gstack-browse/test/cdp-mutex.test.ts +0 -113
  386. package/.agent/skills/gstack-browse/test/cdp-session-cleanup.test.ts +0 -171
  387. package/.agent/skills/gstack-browse/test/chromium-profile-isolation.test.ts +0 -418
  388. package/.agent/skills/gstack-browse/test/claude-bin.test.ts +0 -95
  389. package/.agent/skills/gstack-browse/test/cli-chain-stdin.test.ts +0 -122
  390. package/.agent/skills/gstack-browse/test/cli-config-mismatch-message.test.ts +0 -72
  391. package/.agent/skills/gstack-browse/test/cli-lock.test.ts +0 -120
  392. package/.agent/skills/gstack-browse/test/cli-loopback-no-proxy.test.ts +0 -77
  393. package/.agent/skills/gstack-browse/test/cli-setsid-daemonize.test.ts +0 -75
  394. package/.agent/skills/gstack-browse/test/cli-start-final-healthcheck.test.ts +0 -77
  395. package/.agent/skills/gstack-browse/test/cli-supervisor.test.ts +0 -194
  396. package/.agent/skills/gstack-browse/test/commands.test.ts +0 -2779
  397. package/.agent/skills/gstack-browse/test/compare-board.test.ts +0 -382
  398. package/.agent/skills/gstack-browse/test/config.test.ts +0 -519
  399. package/.agent/skills/gstack-browse/test/content-security.test.ts +0 -681
  400. package/.agent/skills/gstack-browse/test/cookie-auth-verification.test.ts +0 -760
  401. package/.agent/skills/gstack-browse/test/cookie-credential-deadline.test.ts +0 -225
  402. package/.agent/skills/gstack-browse/test/cookie-database.test.ts +0 -277
  403. package/.agent/skills/gstack-browse/test/cookie-fixture-delete-lease.test.ts +0 -256
  404. package/.agent/skills/gstack-browse/test/cookie-import-browser.test.ts +0 -523
  405. package/.agent/skills/gstack-browse/test/cookie-import-native-job.test.ts +0 -1645
  406. package/.agent/skills/gstack-browse/test/cookie-import-native-qualification.ts +0 -128
  407. package/.agent/skills/gstack-browse/test/cookie-import-native.test.ts +0 -436
  408. package/.agent/skills/gstack-browse/test/cookie-import-node.test.ts +0 -166
  409. package/.agent/skills/gstack-browse/test/cookie-import-operation.test.ts +0 -452
  410. package/.agent/skills/gstack-browse/test/cookie-import-reliability.test.ts +0 -659
  411. package/.agent/skills/gstack-browse/test/cookie-import-transport.test.ts +0 -38
  412. package/.agent/skills/gstack-browse/test/cookie-picker-binding-browser.test.ts +0 -96
  413. package/.agent/skills/gstack-browse/test/cookie-picker-binding.test.ts +0 -155
  414. package/.agent/skills/gstack-browse/test/cookie-picker-csrf-browser.test.ts +0 -50
  415. package/.agent/skills/gstack-browse/test/cookie-picker-routes.test.ts +0 -460
  416. package/.agent/skills/gstack-browse/test/cookie-picker-ui.test.ts +0 -535
  417. package/.agent/skills/gstack-browse/test/daemon-log-hygiene.test.ts +0 -135
  418. package/.agent/skills/gstack-browse/test/daemon-mismatch-refuse.test.ts +0 -178
  419. package/.agent/skills/gstack-browse/test/data-platform.test.ts +0 -176
  420. package/.agent/skills/gstack-browse/test/dia-gui-readiness.test.ts +0 -174
  421. package/.agent/skills/gstack-browse/test/dia-launch-comparison.test.ts +0 -180
  422. package/.agent/skills/gstack-browse/test/dia-macos-qualification.test.ts +0 -1695
  423. package/.agent/skills/gstack-browse/test/domain-skills-e2e.test.ts +0 -142
  424. package/.agent/skills/gstack-browse/test/domain-skills-storage.test.ts +0 -263
  425. package/.agent/skills/gstack-browse/test/dotenv-isolation.test.ts +0 -121
  426. package/.agent/skills/gstack-browse/test/dual-listener.test.ts +0 -411
  427. package/.agent/skills/gstack-browse/test/dx-polish.test.ts +0 -101
  428. package/.agent/skills/gstack-browse/test/error-handling.test.ts +0 -47
  429. package/.agent/skills/gstack-browse/test/extension-sender-auth.test.ts +0 -285
  430. package/.agent/skills/gstack-browse/test/extension-token.test.ts +0 -232
  431. package/.agent/skills/gstack-browse/test/file-drop.test.ts +0 -271
  432. package/.agent/skills/gstack-browse/test/file-permissions.test.ts +0 -346
  433. package/.agent/skills/gstack-browse/test/fill-change-event.test.ts +0 -78
  434. package/.agent/skills/gstack-browse/test/find-browse.test.ts +0 -61
  435. package/.agent/skills/gstack-browse/test/findport.test.ts +0 -232
  436. package/.agent/skills/gstack-browse/test/fixtures/basic.html +0 -33
  437. package/.agent/skills/gstack-browse/test/fixtures/change-only-validator.html +0 -31
  438. package/.agent/skills/gstack-browse/test/fixtures/cursor-interactive.html +0 -22
  439. package/.agent/skills/gstack-browse/test/fixtures/dialog.html +0 -15
  440. package/.agent/skills/gstack-browse/test/fixtures/dropdown.html +0 -61
  441. package/.agent/skills/gstack-browse/test/fixtures/empty.html +0 -2
  442. package/.agent/skills/gstack-browse/test/fixtures/forms.html +0 -55
  443. package/.agent/skills/gstack-browse/test/fixtures/iframe.html +0 -30
  444. package/.agent/skills/gstack-browse/test/fixtures/injection-combined.html +0 -58
  445. package/.agent/skills/gstack-browse/test/fixtures/injection-hidden.html +0 -62
  446. package/.agent/skills/gstack-browse/test/fixtures/injection-social.html +0 -25
  447. package/.agent/skills/gstack-browse/test/fixtures/injection-visible.html +0 -26
  448. package/.agent/skills/gstack-browse/test/fixtures/media-page.html +0 -67
  449. package/.agent/skills/gstack-browse/test/fixtures/mock-claude/claude +0 -185
  450. package/.agent/skills/gstack-browse/test/fixtures/native-cookie-delete-lease.ts +0 -125
  451. package/.agent/skills/gstack-browse/test/fixtures/native-cookie-file-owners.ts +0 -132
  452. package/.agent/skills/gstack-browse/test/fixtures/native-cookie-launch.cjs +0 -139
  453. package/.agent/skills/gstack-browse/test/fixtures/native-cookie-process-observer.ts +0 -150
  454. package/.agent/skills/gstack-browse/test/fixtures/native-cookie-process.cjs +0 -13
  455. package/.agent/skills/gstack-browse/test/fixtures/native-cookie-remove-fixture.cjs +0 -42
  456. package/.agent/skills/gstack-browse/test/fixtures/network-idle.html +0 -30
  457. package/.agent/skills/gstack-browse/test/fixtures/qa-eval-checkout.html +0 -108
  458. package/.agent/skills/gstack-browse/test/fixtures/qa-eval-spa.html +0 -98
  459. package/.agent/skills/gstack-browse/test/fixtures/qa-eval.html +0 -51
  460. package/.agent/skills/gstack-browse/test/fixtures/qa-only.html +0 -13
  461. package/.agent/skills/gstack-browse/test/fixtures/responsive.html +0 -49
  462. package/.agent/skills/gstack-browse/test/fixtures/route-dispatch-allowlist.json +0 -22
  463. package/.agent/skills/gstack-browse/test/fixtures/snapshot-ambiguous.html +0 -24
  464. package/.agent/skills/gstack-browse/test/fixtures/snapshot.html +0 -55
  465. package/.agent/skills/gstack-browse/test/fixtures/spa.html +0 -24
  466. package/.agent/skills/gstack-browse/test/fixtures/states.html +0 -17
  467. package/.agent/skills/gstack-browse/test/fixtures/upload-path-validation.ts +0 -91
  468. package/.agent/skills/gstack-browse/test/fixtures/upload.html +0 -25
  469. package/.agent/skills/gstack-browse/test/from-file-path-validation.test.ts +0 -68
  470. package/.agent/skills/gstack-browse/test/gstack-config.test.ts +0 -252
  471. package/.agent/skills/gstack-browse/test/gstack-update-check.test.ts +0 -614
  472. package/.agent/skills/gstack-browse/test/handoff.test.ts +0 -508
  473. package/.agent/skills/gstack-browse/test/headless-gpu-and-reap.test.ts +0 -286
  474. package/.agent/skills/gstack-browse/test/headless-launch-options.test.ts +0 -93
  475. package/.agent/skills/gstack-browse/test/launch-signal-flags.test.ts +0 -75
  476. package/.agent/skills/gstack-browse/test/learnings-injection.test.ts +0 -33
  477. package/.agent/skills/gstack-browse/test/media-extract-unit.test.ts +0 -294
  478. package/.agent/skills/gstack-browse/test/memory-command.test.ts +0 -219
  479. package/.agent/skills/gstack-browse/test/memory-leak-reproducer.test.ts +0 -133
  480. package/.agent/skills/gstack-browse/test/navigation-guard.test.ts +0 -106
  481. package/.agent/skills/gstack-browse/test/output-path-symlinks.test.ts +0 -76
  482. package/.agent/skills/gstack-browse/test/pair-agent-e2e.test.ts +0 -793
  483. package/.agent/skills/gstack-browse/test/pair-agent-optin-gate.test.ts +0 -171
  484. package/.agent/skills/gstack-browse/test/pair-agent-tunnel-eval.test.ts +0 -501
  485. package/.agent/skills/gstack-browse/test/path-security-realpath.test.ts +0 -89
  486. package/.agent/skills/gstack-browse/test/path-validation.test.ts +0 -268
  487. package/.agent/skills/gstack-browse/test/pdf-flags.test.ts +0 -86
  488. package/.agent/skills/gstack-browse/test/platform.test.ts +0 -37
  489. package/.agent/skills/gstack-browse/test/playwright-core-patch.test.ts +0 -62
  490. package/.agent/skills/gstack-browse/test/poisoned-bundle-probe.test.ts +0 -148
  491. package/.agent/skills/gstack-browse/test/process-liveness-windows.test.ts +0 -176
  492. package/.agent/skills/gstack-browse/test/proxy-config.test.ts +0 -189
  493. package/.agent/skills/gstack-browse/test/proxy-env-daemon.test.ts +0 -137
  494. package/.agent/skills/gstack-browse/test/proxy-env.test.ts +0 -189
  495. package/.agent/skills/gstack-browse/test/proxy-redact.test.ts +0 -64
  496. package/.agent/skills/gstack-browse/test/pty-inject-scan.test.ts +0 -153
  497. package/.agent/skills/gstack-browse/test/pty-session-lease.test.ts +0 -98
  498. package/.agent/skills/gstack-browse/test/rebrand-signed-bundle.test.ts +0 -53
  499. package/.agent/skills/gstack-browse/test/regression-pr1169-pdf-from-file-invalid-json.test.ts +0 -84
  500. package/.agent/skills/gstack-browse/test/restart-env.test.ts +0 -39
  501. package/.agent/skills/gstack-browse/test/route-test-harness.ts +0 -121
  502. package/.agent/skills/gstack-browse/test/runtime-root-server-bundle.test.ts +0 -100
  503. package/.agent/skills/gstack-browse/test/sanitize.test.ts +0 -112
  504. package/.agent/skills/gstack-browse/test/screenshot-size-guard.test.ts +0 -118
  505. package/.agent/skills/gstack-browse/test/security-adversarial-fixes.test.ts +0 -101
  506. package/.agent/skills/gstack-browse/test/security-adversarial.test.ts +0 -342
  507. package/.agent/skills/gstack-browse/test/security-audit-r2.test.ts +0 -432
  508. package/.agent/skills/gstack-browse/test/security-bench.test.ts +0 -159
  509. package/.agent/skills/gstack-browse/test/security-classifier-download-cleanup.test.ts +0 -138
  510. package/.agent/skills/gstack-browse/test/security-classifier.test.ts +0 -29
  511. package/.agent/skills/gstack-browse/test/security-integration.test.ts +0 -184
  512. package/.agent/skills/gstack-browse/test/security-live-playwright.test.ts +0 -195
  513. package/.agent/skills/gstack-browse/test/security-sidecar-client.test.ts +0 -66
  514. package/.agent/skills/gstack-browse/test/security.test.ts +0 -268
  515. package/.agent/skills/gstack-browse/test/server-auth.test.ts +0 -600
  516. package/.agent/skills/gstack-browse/test/server-embedder-terminal-port.test.ts +0 -301
  517. package/.agent/skills/gstack-browse/test/server-factory.test.ts +0 -640
  518. package/.agent/skills/gstack-browse/test/server-flush-trackers.test.ts +0 -73
  519. package/.agent/skills/gstack-browse/test/server-launch-resolution.test.ts +0 -49
  520. package/.agent/skills/gstack-browse/test/server-lock-errors.test.ts +0 -124
  521. package/.agent/skills/gstack-browse/test/server-no-import-side-effects.test.ts +0 -110
  522. package/.agent/skills/gstack-browse/test/server-proxy-fail-fast.test.ts +0 -98
  523. package/.agent/skills/gstack-browse/test/server-pty-lease-routes.test.ts +0 -156
  524. package/.agent/skills/gstack-browse/test/server-route-auth-blackbox.test.ts +0 -270
  525. package/.agent/skills/gstack-browse/test/server-route-dispatch-ratchet.test.ts +0 -129
  526. package/.agent/skills/gstack-browse/test/server-route-table.test.ts +0 -238
  527. package/.agent/skills/gstack-browse/test/server-sanitize-surrogates.test.ts +0 -182
  528. package/.agent/skills/gstack-browse/test/server-tmp-state-path.test.ts +0 -110
  529. package/.agent/skills/gstack-browse/test/session-cookie-store.test.ts +0 -159
  530. package/.agent/skills/gstack-browse/test/session-persist.test.ts +0 -287
  531. package/.agent/skills/gstack-browse/test/sidebar-tabs.test.ts +0 -268
  532. package/.agent/skills/gstack-browse/test/sidebar-ux.test.ts +0 -1006
  533. package/.agent/skills/gstack-browse/test/sidepanel-font-size.test.ts +0 -147
  534. package/.agent/skills/gstack-browse/test/sidepanel-patient-autoconnect.test.ts +0 -70
  535. package/.agent/skills/gstack-browse/test/sidepanel-reattach.test.ts +0 -93
  536. package/.agent/skills/gstack-browse/test/sidepanel-restart-dispose.test.ts +0 -106
  537. package/.agent/skills/gstack-browse/test/skill-token.test.ts +0 -167
  538. package/.agent/skills/gstack-browse/test/snapshot.test.ts +0 -593
  539. package/.agent/skills/gstack-browse/test/socks-bridge.test.ts +0 -461
  540. package/.agent/skills/gstack-browse/test/sse-helpers.test.ts +0 -194
  541. package/.agent/skills/gstack-browse/test/sse-session-cookie.test.ts +0 -155
  542. package/.agent/skills/gstack-browse/test/state-ttl.test.ts +0 -35
  543. package/.agent/skills/gstack-browse/test/stealth-extended.test.ts +0 -127
  544. package/.agent/skills/gstack-browse/test/stealth-layer-c.test.ts +0 -284
  545. package/.agent/skills/gstack-browse/test/stealth-webdriver.test.ts +0 -394
  546. package/.agent/skills/gstack-browse/test/stop-ack-before-shutdown.test.ts +0 -23
  547. package/.agent/skills/gstack-browse/test/stop-dead-daemon.test.ts +0 -153
  548. package/.agent/skills/gstack-browse/test/tab-each.test.ts +0 -196
  549. package/.agent/skills/gstack-browse/test/tab-guardrail.test.ts +0 -118
  550. package/.agent/skills/gstack-browse/test/tab-isolation.test.ts +0 -309
  551. package/.agent/skills/gstack-browse/test/tab-session-frame-detach.test.ts +0 -154
  552. package/.agent/skills/gstack-browse/test/telemetry-optout.test.ts +0 -171
  553. package/.agent/skills/gstack-browse/test/telemetry.test.ts +0 -77
  554. package/.agent/skills/gstack-browse/test/temp-dirs.test.ts +0 -129
  555. package/.agent/skills/gstack-browse/test/terminal-agent-detach-reattach.test.ts +0 -84
  556. package/.agent/skills/gstack-browse/test/terminal-agent-import.test.ts +0 -42
  557. package/.agent/skills/gstack-browse/test/terminal-agent-integration.test.ts +0 -565
  558. package/.agent/skills/gstack-browse/test/terminal-agent-keepalive.test.ts +0 -88
  559. package/.agent/skills/gstack-browse/test/terminal-agent-lifecycle.test.ts +0 -371
  560. package/.agent/skills/gstack-browse/test/terminal-agent-native-observation.test.ts +0 -66
  561. package/.agent/skills/gstack-browse/test/terminal-agent-owner-watchdog.test.ts +0 -81
  562. package/.agent/skills/gstack-browse/test/terminal-agent-pid-identity.test.ts +0 -161
  563. package/.agent/skills/gstack-browse/test/terminal-agent-port-range.test.ts +0 -80
  564. package/.agent/skills/gstack-browse/test/terminal-agent-publication-lock.test.ts +0 -289
  565. package/.agent/skills/gstack-browse/test/terminal-agent-ring-buffer-runtime.test.ts +0 -157
  566. package/.agent/skills/gstack-browse/test/terminal-agent-session-routing.test.ts +0 -96
  567. package/.agent/skills/gstack-browse/test/terminal-agent-watchdog.test.ts +0 -101
  568. package/.agent/skills/gstack-browse/test/terminal-agent.test.ts +0 -259
  569. package/.agent/skills/gstack-browse/test/terminal-pty-lifecycle.test.ts +0 -212
  570. package/.agent/skills/gstack-browse/test/test-server.ts +0 -57
  571. package/.agent/skills/gstack-browse/test/token-registry.test.ts +0 -615
  572. package/.agent/skills/gstack-browse/test/tunnel-gate-unit.test.ts +0 -129
  573. package/.agent/skills/gstack-browse/test/tunnel-revoke-cli.test.ts +0 -455
  574. package/.agent/skills/gstack-browse/test/url-validation.test.ts +0 -384
  575. package/.agent/skills/gstack-browse/test/watch.test.ts +0 -129
  576. package/.agent/skills/gstack-browse/test/watchdog.test.ts +0 -352
  577. package/.agent/skills/gstack-browse/test/welcome-page.test.ts +0 -143
  578. package/.agent/skills/gstack-browse/test/windows-spawn-hide.test.ts +0 -167
  579. package/.agent/skills/gstack-browse/test/xprotect-heal.test.ts +0 -457
  580. package/.agent/skills/gstack-browse/test/xvfb.test.ts +0 -603
  581. package/.agent/skills/gstack-canary/SKILL.md +0 -699
  582. package/.agent/skills/gstack-canary/SKILL.md.tmpl +0 -254
  583. package/.agent/skills/gstack-careful/SKILL.md +0 -96
  584. package/.agent/skills/gstack-careful/SKILL.md.tmpl +0 -107
  585. package/.agent/skills/gstack-careful/bin/check-careful.sh +0 -407
  586. package/.agent/skills/gstack-careful/bin/hook-extract.sh +0 -198
  587. package/.agent/skills/gstack-codex/SKILL.md +0 -889
  588. package/.agent/skills/gstack-codex/SKILL.md.tmpl +0 -358
  589. package/.agent/skills/gstack-codex/sections/challenge-mode.md +0 -150
  590. package/.agent/skills/gstack-codex/sections/challenge-mode.md.tmpl +0 -136
  591. package/.agent/skills/gstack-codex/sections/consult-mode.md +0 -221
  592. package/.agent/skills/gstack-codex/sections/consult-mode.md.tmpl +0 -207
  593. package/.agent/skills/gstack-codex/sections/manifest.json +0 -26
  594. package/.agent/skills/gstack-codex/sections/review-mode.md +0 -265
  595. package/.agent/skills/gstack-codex/sections/review-mode.md.tmpl +0 -246
  596. package/.agent/skills/gstack-context-restore/SKILL.md +0 -612
  597. package/.agent/skills/gstack-context-restore/SKILL.md.tmpl +0 -289
  598. package/.agent/skills/gstack-context-save/SKILL.md +0 -631
  599. package/.agent/skills/gstack-context-save/SKILL.md.tmpl +0 -307
  600. package/.agent/skills/gstack-cso/ACKNOWLEDGEMENTS.md +0 -17
  601. package/.agent/skills/gstack-cso/SKILL.md +0 -166
  602. package/.agent/skills/gstack-cso/SKILL.md.tmpl +0 -162
  603. package/.agent/skills/gstack-cso/sections/audit-phases.md +0 -116
  604. package/.agent/skills/gstack-cso/sections/audit-phases.md.tmpl +0 -114
  605. package/.agent/skills/gstack-cso/sections/manifest.json +0 -14
  606. package/.agent/skills/gstack-design-consultation/SKILL.md +0 -795
  607. package/.agent/skills/gstack-design-consultation/SKILL.md.tmpl +0 -234
  608. package/.agent/skills/gstack-design-consultation/sections/manifest.json +0 -14
  609. package/.agent/skills/gstack-design-consultation/sections/proposal-and-preview.md +0 -595
  610. package/.agent/skills/gstack-design-consultation/sections/proposal-and-preview.md.tmpl +0 -350
  611. package/.agent/skills/gstack-design-html/SKILL.md +0 -866
  612. package/.agent/skills/gstack-design-html/SKILL.md.tmpl +0 -476
  613. package/.agent/skills/gstack-design-html/sections/detector-install-offer.md +0 -40
  614. package/.agent/skills/gstack-design-html/sections/detector-install-offer.md.tmpl +0 -6
  615. package/.agent/skills/gstack-design-html/sections/doctrine.md +0 -86
  616. package/.agent/skills/gstack-design-html/sections/doctrine.md.tmpl +0 -1
  617. package/.agent/skills/gstack-design-html/sections/manifest.json +0 -26
  618. package/.agent/skills/gstack-design-html/sections/pretext-patterns.md +0 -145
  619. package/.agent/skills/gstack-design-html/sections/pretext-patterns.md.tmpl +0 -143
  620. package/.agent/skills/gstack-design-html/vendor/pretext.js +0 -5
  621. package/.agent/skills/gstack-design-review/SKILL.md +0 -1957
  622. package/.agent/skills/gstack-design-review/SKILL.md.tmpl +0 -344
  623. package/.agent/skills/gstack-design-shotgun/SKILL.md +0 -885
  624. package/.agent/skills/gstack-design-shotgun/SKILL.md.tmpl +0 -310
  625. package/.agent/skills/gstack-design-shotgun/sections/doctrine.md +0 -86
  626. package/.agent/skills/gstack-design-shotgun/sections/doctrine.md.tmpl +0 -1
  627. package/.agent/skills/gstack-design-shotgun/sections/manifest.json +0 -14
  628. package/.agent/skills/gstack-deslop-shared-libs/SKILL.md +0 -209
  629. package/.agent/skills/gstack-deslop-shared-libs/SKILL.md.tmpl +0 -181
  630. package/.agent/skills/gstack-devex-review/SKILL.md +0 -1107
  631. package/.agent/skills/gstack-devex-review/SKILL.md.tmpl +0 -241
  632. package/.agent/skills/gstack-diagram/SKILL.md +0 -295
  633. package/.agent/skills/gstack-diagram/SKILL.md.tmpl +0 -175
  634. package/.agent/skills/gstack-document-generate/SKILL.md +0 -814
  635. package/.agent/skills/gstack-document-generate/SKILL.md.tmpl +0 -460
  636. package/.agent/skills/gstack-document-release/SKILL.md +0 -557
  637. package/.agent/skills/gstack-document-release/SKILL.md.tmpl +0 -188
  638. package/.agent/skills/gstack-document-release/sections/audit-scope.md +0 -51
  639. package/.agent/skills/gstack-document-release/sections/audit-scope.md.tmpl +0 -49
  640. package/.agent/skills/gstack-document-release/sections/manifest.json +0 -20
  641. package/.agent/skills/gstack-document-release/sections/release-body.md +0 -640
  642. package/.agent/skills/gstack-document-release/sections/release-body.md.tmpl +0 -428
  643. package/.agent/skills/gstack-eval-plan/SKILL.md +0 -472
  644. package/.agent/skills/gstack-eval-plan/SKILL.md.tmpl +0 -157
  645. package/.agent/skills/gstack-freeze/SKILL.md +0 -79
  646. package/.agent/skills/gstack-freeze/SKILL.md.tmpl +0 -96
  647. package/.agent/skills/gstack-freeze/bin/check-freeze.sh +0 -210
  648. package/.agent/skills/gstack-freeze/bin/freeze-state.sh +0 -68
  649. package/.agent/skills/gstack-gstack-upgrade/SKILL.md +0 -410
  650. package/.agent/skills/gstack-gstack-upgrade/SKILL.md.tmpl +0 -413
  651. package/.agent/skills/gstack-gstack-upgrade/migrations/v0.15.2.0.sh +0 -20
  652. package/.agent/skills/gstack-gstack-upgrade/migrations/v0.16.2.0.sh +0 -56
  653. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.0.0.0.sh +0 -40
  654. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.1.3.0.sh +0 -137
  655. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.17.0.0.sh +0 -59
  656. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.27.0.0.sh +0 -432
  657. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.37.0.0.sh +0 -104
  658. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.38.1.0.sh +0 -104
  659. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.40.0.0.sh +0 -144
  660. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.58.0.0.sh +0 -65
  661. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.65.0.0.sh +0 -152
  662. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.67.0.0.sh +0 -73
  663. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.78.0.0.sh +0 -30
  664. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.86.0.0.sh +0 -12
  665. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.91.19.0.sh +0 -53
  666. package/.agent/skills/gstack-gstack-upgrade/migrations/v1.91.65.0.sh +0 -31
  667. package/.agent/skills/gstack-guard/SKILL.md +0 -60
  668. package/.agent/skills/gstack-guard/SKILL.md.tmpl +0 -88
  669. package/.agent/skills/gstack-health/SKILL.md +0 -710
  670. package/.agent/skills/gstack-health/SKILL.md.tmpl +0 -386
  671. package/.agent/skills/gstack-investigate/SKILL.md +0 -706
  672. package/.agent/skills/gstack-investigate/SKILL.md.tmpl +0 -305
  673. package/.agent/skills/gstack-investigate/sections/backlog.md +0 -31
  674. package/.agent/skills/gstack-investigate/sections/backlog.md.tmpl +0 -29
  675. package/.agent/skills/gstack-investigate/sections/eval-bisect.md +0 -25
  676. package/.agent/skills/gstack-investigate/sections/eval-bisect.md.tmpl +0 -23
  677. package/.agent/skills/gstack-investigate/sections/flake.md +0 -50
  678. package/.agent/skills/gstack-investigate/sections/flake.md.tmpl +0 -43
  679. package/.agent/skills/gstack-investigate/sections/manifest.json +0 -38
  680. package/.agent/skills/gstack-investigate/sections/polluter.md +0 -33
  681. package/.agent/skills/gstack-investigate/sections/polluter.md.tmpl +0 -31
  682. package/.agent/skills/gstack-investigate/sections/race.md +0 -27
  683. package/.agent/skills/gstack-investigate/sections/race.md.tmpl +0 -25
  684. package/.agent/skills/gstack-ios-clean/SKILL.md +0 -427
  685. package/.agent/skills/gstack-ios-clean/SKILL.md.tmpl +0 -107
  686. package/.agent/skills/gstack-ios-design-review/SKILL.md +0 -434
  687. package/.agent/skills/gstack-ios-design-review/SKILL.md.tmpl +0 -111
  688. package/.agent/skills/gstack-ios-fix/SKILL.md +0 -423
  689. package/.agent/skills/gstack-ios-fix/SKILL.md.tmpl +0 -103
  690. package/.agent/skills/gstack-ios-qa/SKILL.md +0 -643
  691. package/.agent/skills/gstack-ios-qa/SKILL.md.tmpl +0 -297
  692. package/.agent/skills/gstack-ios-qa/daemon/src/allowlist.ts +0 -114
  693. package/.agent/skills/gstack-ios-qa/daemon/src/audit.ts +0 -97
  694. package/.agent/skills/gstack-ios-qa/daemon/src/auth-mint.ts +0 -89
  695. package/.agent/skills/gstack-ios-qa/daemon/src/cli-mint.ts +0 -149
  696. package/.agent/skills/gstack-ios-qa/daemon/src/devicectl.ts +0 -335
  697. package/.agent/skills/gstack-ios-qa/daemon/src/index.ts +0 -697
  698. package/.agent/skills/gstack-ios-qa/daemon/src/proxy.ts +0 -160
  699. package/.agent/skills/gstack-ios-qa/daemon/src/session-tokens.ts +0 -126
  700. package/.agent/skills/gstack-ios-qa/daemon/src/single-instance.ts +0 -171
  701. package/.agent/skills/gstack-ios-qa/daemon/src/tailscale-localapi.ts +0 -120
  702. package/.agent/skills/gstack-ios-qa/daemon/src/tunnel-bootstrap.ts +0 -459
  703. package/.agent/skills/gstack-ios-qa/daemon/src/types.ts +0 -91
  704. package/.agent/skills/gstack-ios-qa/daemon/test/allowlist.test.ts +0 -146
  705. package/.agent/skills/gstack-ios-qa/daemon/test/audit.test.ts +0 -111
  706. package/.agent/skills/gstack-ios-qa/daemon/test/auth-mint.test.ts +0 -106
  707. package/.agent/skills/gstack-ios-qa/daemon/test/cli-mint.test.ts +0 -120
  708. package/.agent/skills/gstack-ios-qa/daemon/test/daemon-integration.test.ts +0 -855
  709. package/.agent/skills/gstack-ios-qa/daemon/test/fake-device.ts +0 -238
  710. package/.agent/skills/gstack-ios-qa/daemon/test/proxy-classify.test.ts +0 -47
  711. package/.agent/skills/gstack-ios-qa/daemon/test/proxy-transport.test.ts +0 -499
  712. package/.agent/skills/gstack-ios-qa/daemon/test/session-tokens.test.ts +0 -156
  713. package/.agent/skills/gstack-ios-qa/daemon/test/single-instance.test.ts +0 -96
  714. package/.agent/skills/gstack-ios-qa/daemon/test/tailscale-localapi.test.ts +0 -55
  715. package/.agent/skills/gstack-ios-qa/daemon/test/tunnel-bootstrap.test.ts +0 -1030
  716. package/.agent/skills/gstack-ios-qa/templates/Bridges.swift.template +0 -474
  717. package/.agent/skills/gstack-ios-qa/templates/DebugBridgeManager.swift.template +0 -38
  718. package/.agent/skills/gstack-ios-qa/templates/DebugBridgeTouch.h.template +0 -38
  719. package/.agent/skills/gstack-ios-qa/templates/DebugBridgeTouch.m.template +0 -365
  720. package/.agent/skills/gstack-ios-qa/templates/DebugBridgeWiring.swift.template +0 -59
  721. package/.agent/skills/gstack-ios-qa/templates/DebugOverlay.swift.template +0 -137
  722. package/.agent/skills/gstack-ios-qa/templates/Package.swift.template +0 -82
  723. package/.agent/skills/gstack-ios-qa/templates/StateAccessor.swift.template +0 -41
  724. package/.agent/skills/gstack-ios-qa/templates/StateServer.swift.template +0 -625
  725. package/.agent/skills/gstack-ios-sync/SKILL.md +0 -418
  726. package/.agent/skills/gstack-ios-sync/SKILL.md.tmpl +0 -99
  727. package/.agent/skills/gstack-land-and-deploy/SKILL.md +0 -1028
  728. package/.agent/skills/gstack-land-and-deploy/SKILL.md.tmpl +0 -509
  729. package/.agent/skills/gstack-land-and-deploy/sections/first-run-validation.md +0 -188
  730. package/.agent/skills/gstack-land-and-deploy/sections/first-run-validation.md.tmpl +0 -150
  731. package/.agent/skills/gstack-land-and-deploy/sections/manifest.json +0 -26
  732. package/.agent/skills/gstack-land-and-deploy/sections/merge-and-deploy.md +0 -250
  733. package/.agent/skills/gstack-land-and-deploy/sections/merge-and-deploy.md.tmpl +0 -248
  734. package/.agent/skills/gstack-land-and-deploy/sections/readiness-gate.md +0 -308
  735. package/.agent/skills/gstack-land-and-deploy/sections/readiness-gate.md.tmpl +0 -306
  736. package/.agent/skills/gstack-landing-report/SKILL.md +0 -488
  737. package/.agent/skills/gstack-landing-report/SKILL.md.tmpl +0 -164
  738. package/.agent/skills/gstack-learn/SKILL.md +0 -519
  739. package/.agent/skills/gstack-learn/SKILL.md.tmpl +0 -197
  740. package/.agent/skills/gstack-make-pdf/SKILL.md +0 -409
  741. package/.agent/skills/gstack-make-pdf/SKILL.md.tmpl +0 -253
  742. package/.agent/skills/gstack-make-pdf/src/asideClient.ts +0 -114
  743. package/.agent/skills/gstack-make-pdf/src/cli.ts +0 -294
  744. package/.agent/skills/gstack-make-pdf/src/commands.ts +0 -62
  745. package/.agent/skills/gstack-make-pdf/src/diagram-prepass.ts +0 -834
  746. package/.agent/skills/gstack-make-pdf/src/html-to-docx.d.ts +0 -7
  747. package/.agent/skills/gstack-make-pdf/src/image-policy.ts +0 -236
  748. package/.agent/skills/gstack-make-pdf/src/image-size.ts +0 -117
  749. package/.agent/skills/gstack-make-pdf/src/orchestrator.ts +0 -339
  750. package/.agent/skills/gstack-make-pdf/src/pdftotext.ts +0 -318
  751. package/.agent/skills/gstack-make-pdf/src/print-css.ts +0 -452
  752. package/.agent/skills/gstack-make-pdf/src/render.ts +0 -636
  753. package/.agent/skills/gstack-make-pdf/src/setup.ts +0 -113
  754. package/.agent/skills/gstack-make-pdf/src/smartypants.ts +0 -105
  755. package/.agent/skills/gstack-make-pdf/src/toc-pages.ts +0 -197
  756. package/.agent/skills/gstack-make-pdf/src/types.ts +0 -109
  757. package/.agent/skills/gstack-make-pdf/test/asideClient.test.ts +0 -153
  758. package/.agent/skills/gstack-make-pdf/test/cli-args.test.ts +0 -94
  759. package/.agent/skills/gstack-make-pdf/test/cli-exit-codes.test.ts +0 -329
  760. package/.agent/skills/gstack-make-pdf/test/diagram-prepass.test.ts +0 -743
  761. package/.agent/skills/gstack-make-pdf/test/e2e/ci-prereqs.test.ts +0 -48
  762. package/.agent/skills/gstack-make-pdf/test/e2e/combined-gate.test.ts +0 -74
  763. package/.agent/skills/gstack-make-pdf/test/e2e/diagram-gate.test.ts +0 -173
  764. package/.agent/skills/gstack-make-pdf/test/e2e/emoji-gate.test.ts +0 -191
  765. package/.agent/skills/gstack-make-pdf/test/e2e/format-gate.test.ts +0 -130
  766. package/.agent/skills/gstack-make-pdf/test/e2e/landscape-gate.test.ts +0 -150
  767. package/.agent/skills/gstack-make-pdf/test/e2e/toc-gate.test.ts +0 -143
  768. package/.agent/skills/gstack-make-pdf/test/fixtures/combined-gate.expected.txt +0 -20
  769. package/.agent/skills/gstack-make-pdf/test/fixtures/combined-gate.md +0 -30
  770. package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/huge-noise.png +0 -0
  771. package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/red-box.png +0 -0
  772. package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/wide-arch.png +0 -0
  773. package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/wide-screenshot.png +0 -0
  774. package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-gate.md +0 -48
  775. package/.agent/skills/gstack-make-pdf/test/fixtures/emoji-gate.md +0 -12
  776. package/.agent/skills/gstack-make-pdf/test/fixtures/landscape-gate.md +0 -52
  777. package/.agent/skills/gstack-make-pdf/test/image-policy.test.ts +0 -215
  778. package/.agent/skills/gstack-make-pdf/test/pdftotext.test.ts +0 -327
  779. package/.agent/skills/gstack-make-pdf/test/render-offline-sanitize.test.ts +0 -268
  780. package/.agent/skills/gstack-make-pdf/test/render.test.ts +0 -603
  781. package/.agent/skills/gstack-make-pdf/test/setup-smoke.test.ts +0 -259
  782. package/.agent/skills/gstack-make-pdf/test/toc-anchors.test.ts +0 -95
  783. package/.agent/skills/gstack-make-pdf/test/toc-pages.test.ts +0 -148
  784. package/.agent/skills/gstack-office-hours/SKILL.md +0 -1324
  785. package/.agent/skills/gstack-office-hours/SKILL.md.tmpl +0 -372
  786. package/.agent/skills/gstack-office-hours/sections/design-and-handoff.md +0 -706
  787. package/.agent/skills/gstack-office-hours/sections/design-and-handoff.md.tmpl +0 -547
  788. package/.agent/skills/gstack-office-hours/sections/manifest.json +0 -26
  789. package/.agent/skills/gstack-office-hours/sections/phase-2a-startup-diagnostic.md +0 -163
  790. package/.agent/skills/gstack-office-hours/sections/phase-2a-startup-diagnostic.md.tmpl +0 -161
  791. package/.agent/skills/gstack-office-hours/sections/phase-2b-builder-brainstorm.md +0 -43
  792. package/.agent/skills/gstack-office-hours/sections/phase-2b-builder-brainstorm.md.tmpl +0 -41
  793. package/.agent/skills/gstack-open-gstack-browser/SKILL.md +0 -414
  794. package/.agent/skills/gstack-open-gstack-browser/SKILL.md.tmpl +0 -248
  795. package/.agent/skills/gstack-pair-agent/SKILL.md +0 -730
  796. package/.agent/skills/gstack-pair-agent/SKILL.md.tmpl +0 -366
  797. package/.agent/skills/gstack-plan-ceo-review/SKILL.md +0 -1245
  798. package/.agent/skills/gstack-plan-ceo-review/SKILL.md.tmpl +0 -596
  799. package/.agent/skills/gstack-plan-ceo-review/bin/mode-handoff-hook +0 -5
  800. package/.agent/skills/gstack-plan-ceo-review/bin/mode-handoff-hook.ts +0 -34
  801. package/.agent/skills/gstack-plan-ceo-review/sections/checklist.md +0 -37
  802. package/.agent/skills/gstack-plan-ceo-review/sections/checklist.md.tmpl +0 -35
  803. package/.agent/skills/gstack-plan-ceo-review/sections/manifest.json +0 -23
  804. package/.agent/skills/gstack-plan-ceo-review/sections/review-sections.md +0 -1207
  805. package/.agent/skills/gstack-plan-ceo-review/sections/review-sections.md.tmpl +0 -605
  806. package/.agent/skills/gstack-plan-design-review/SKILL.md +0 -1230
  807. package/.agent/skills/gstack-plan-design-review/SKILL.md.tmpl +0 -344
  808. package/.agent/skills/gstack-plan-design-review/sections/checklist.md +0 -27
  809. package/.agent/skills/gstack-plan-design-review/sections/checklist.md.tmpl +0 -25
  810. package/.agent/skills/gstack-plan-design-review/sections/manifest.json +0 -23
  811. package/.agent/skills/gstack-plan-design-review/sections/review-sections.md +0 -727
  812. package/.agent/skills/gstack-plan-design-review/sections/review-sections.md.tmpl +0 -281
  813. package/.agent/skills/gstack-plan-devex-review/SKILL.md +0 -1072
  814. package/.agent/skills/gstack-plan-devex-review/SKILL.md.tmpl +0 -459
  815. package/.agent/skills/gstack-plan-devex-review/dx-hall-of-fame.md +0 -127
  816. package/.agent/skills/gstack-plan-devex-review/sections/checklist.md +0 -33
  817. package/.agent/skills/gstack-plan-devex-review/sections/checklist.md.tmpl +0 -31
  818. package/.agent/skills/gstack-plan-devex-review/sections/manifest.json +0 -23
  819. package/.agent/skills/gstack-plan-devex-review/sections/review-sections.md +0 -1037
  820. package/.agent/skills/gstack-plan-devex-review/sections/review-sections.md.tmpl +0 -446
  821. package/.agent/skills/gstack-plan-eng-review/SKILL.md +0 -702
  822. package/.agent/skills/gstack-plan-eng-review/SKILL.md.tmpl +0 -191
  823. package/.agent/skills/gstack-plan-eng-review/sections/checklist.md +0 -34
  824. package/.agent/skills/gstack-plan-eng-review/sections/checklist.md.tmpl +0 -32
  825. package/.agent/skills/gstack-plan-eng-review/sections/manifest.json +0 -23
  826. package/.agent/skills/gstack-plan-eng-review/sections/review-sections.md +0 -1545
  827. package/.agent/skills/gstack-plan-eng-review/sections/review-sections.md.tmpl +0 -614
  828. package/.agent/skills/gstack-plan-tune/SKILL.md +0 -986
  829. package/.agent/skills/gstack-plan-tune/SKILL.md.tmpl +0 -669
  830. package/.agent/skills/gstack-qa/SKILL.md +0 -781
  831. package/.agent/skills/gstack-qa/SKILL.md.tmpl +0 -305
  832. package/.agent/skills/gstack-qa/references/issue-taxonomy.md +0 -85
  833. package/.agent/skills/gstack-qa/sections/browser-setup.md +0 -114
  834. package/.agent/skills/gstack-qa/sections/browser-setup.md.tmpl +0 -28
  835. package/.agent/skills/gstack-qa/sections/browser-verify.md +0 -19
  836. package/.agent/skills/gstack-qa/sections/browser-verify.md.tmpl +0 -17
  837. package/.agent/skills/gstack-qa/sections/exploratory.md +0 -92
  838. package/.agent/skills/gstack-qa/sections/exploratory.md.tmpl +0 -1
  839. package/.agent/skills/gstack-qa/sections/manifest.json +0 -50
  840. package/.agent/skills/gstack-qa/sections/qa-patterns.md +0 -226
  841. package/.agent/skills/gstack-qa/sections/qa-patterns.md.tmpl +0 -1
  842. package/.agent/skills/gstack-qa/sections/scope.md +0 -23
  843. package/.agent/skills/gstack-qa/sections/scope.md.tmpl +0 -1
  844. package/.agent/skills/gstack-qa/sections/system-functional.md +0 -61
  845. package/.agent/skills/gstack-qa/sections/system-functional.md.tmpl +0 -1
  846. package/.agent/skills/gstack-qa/sections/test-bootstrap.md +0 -86
  847. package/.agent/skills/gstack-qa/sections/test-bootstrap.md.tmpl +0 -74
  848. package/.agent/skills/gstack-qa/templates/functional-report-template.md +0 -55
  849. package/.agent/skills/gstack-qa/templates/qa-report-template.md +0 -124
  850. package/.agent/skills/gstack-qa-only/SKILL.md +0 -595
  851. package/.agent/skills/gstack-qa-only/SKILL.md.tmpl +0 -214
  852. package/.agent/skills/gstack-qa-only/sections/exploratory.md +0 -107
  853. package/.agent/skills/gstack-qa-only/sections/exploratory.md.tmpl +0 -1
  854. package/.agent/skills/gstack-qa-only/sections/manifest.json +0 -19
  855. package/.agent/skills/gstack-qa-only/sections/reporting.md +0 -78
  856. package/.agent/skills/gstack-qa-only/sections/reporting.md.tmpl +0 -76
  857. package/.agent/skills/gstack-retro/SKILL.md +0 -1178
  858. package/.agent/skills/gstack-retro/SKILL.md.tmpl +0 -769
  859. package/.agent/skills/gstack-retro/sections/manifest.json +0 -14
  860. package/.agent/skills/gstack-retro/sections/report-format.md +0 -127
  861. package/.agent/skills/gstack-retro/sections/report-format.md.tmpl +0 -125
  862. package/.agent/skills/gstack-review/SKILL.md +0 -1199
  863. package/.agent/skills/gstack-review/SKILL.md.tmpl +0 -482
  864. package/.agent/skills/gstack-review/TODOS-format.md +0 -61
  865. package/.agent/skills/gstack-review/checklist.md +0 -222
  866. package/.agent/skills/gstack-review/design-checklist.md +0 -193
  867. package/.agent/skills/gstack-review/gate-integrity.md +0 -171
  868. package/.agent/skills/gstack-review/greptile-triage.md +0 -261
  869. package/.agent/skills/gstack-review/sections/adversarial.md +0 -296
  870. package/.agent/skills/gstack-review/sections/adversarial.md.tmpl +0 -1
  871. package/.agent/skills/gstack-review/sections/manifest.json +0 -32
  872. package/.agent/skills/gstack-review/sections/plan-completion.md +0 -232
  873. package/.agent/skills/gstack-review/sections/plan-completion.md.tmpl +0 -3
  874. package/.agent/skills/gstack-review/sections/review-army.md +0 -263
  875. package/.agent/skills/gstack-review/sections/review-army.md.tmpl +0 -1
  876. package/.agent/skills/gstack-review/sections/shared-code-reuse.md +0 -34
  877. package/.agent/skills/gstack-review/sections/shared-code-reuse.md.tmpl +0 -1
  878. package/.agent/skills/gstack-review/specialists/api-contract.md +0 -49
  879. package/.agent/skills/gstack-review/specialists/data-migration.md +0 -48
  880. package/.agent/skills/gstack-review/specialists/maintainability.md +0 -46
  881. package/.agent/skills/gstack-review/specialists/performance.md +0 -52
  882. package/.agent/skills/gstack-review/specialists/red-team.md +0 -45
  883. package/.agent/skills/gstack-review/specialists/security.md +0 -65
  884. package/.agent/skills/gstack-review/specialists/simplification.md +0 -49
  885. package/.agent/skills/gstack-review/specialists/testing.md +0 -160
  886. package/.agent/skills/gstack-scrape/SKILL.md +0 -431
  887. package/.agent/skills/gstack-scrape/SKILL.md.tmpl +0 -187
  888. package/.agent/skills/gstack-setup-browser-cookies/SKILL.md +0 -231
  889. package/.agent/skills/gstack-setup-browser-cookies/SKILL.md.tmpl +0 -64
  890. package/.agent/skills/gstack-setup-deploy/SKILL.md +0 -605
  891. package/.agent/skills/gstack-setup-deploy/SKILL.md.tmpl +0 -250
  892. package/.agent/skills/gstack-setup-gbrain/SKILL.md +0 -1074
  893. package/.agent/skills/gstack-setup-gbrain/SKILL.md.tmpl +0 -742
  894. package/.agent/skills/gstack-setup-gbrain/memory.md +0 -426
  895. package/.agent/skills/gstack-setup-gbrain/sections/brain-init.md +0 -269
  896. package/.agent/skills/gstack-setup-gbrain/sections/brain-init.md.tmpl +0 -267
  897. package/.agent/skills/gstack-setup-gbrain/sections/claude-md-persist.md +0 -40
  898. package/.agent/skills/gstack-setup-gbrain/sections/claude-md-persist.md.tmpl +0 -38
  899. package/.agent/skills/gstack-setup-gbrain/sections/engine-remediation.md +0 -71
  900. package/.agent/skills/gstack-setup-gbrain/sections/engine-remediation.md.tmpl +0 -69
  901. package/.agent/skills/gstack-setup-gbrain/sections/manifest.json +0 -32
  902. package/.agent/skills/gstack-setup-gbrain/sections/transcript-gate.md +0 -105
  903. package/.agent/skills/gstack-setup-gbrain/sections/transcript-gate.md.tmpl +0 -103
  904. package/.agent/skills/gstack-ship/SKILL.md +0 -1220
  905. package/.agent/skills/gstack-ship/SKILL.md.tmpl +0 -694
  906. package/.agent/skills/gstack-ship/sections/adversarial.md +0 -362
  907. package/.agent/skills/gstack-ship/sections/adversarial.md.tmpl +0 -22
  908. package/.agent/skills/gstack-ship/sections/apple-release.md +0 -98
  909. package/.agent/skills/gstack-ship/sections/apple-release.md.tmpl +0 -96
  910. package/.agent/skills/gstack-ship/sections/changelog.md +0 -47
  911. package/.agent/skills/gstack-ship/sections/changelog.md.tmpl +0 -5
  912. package/.agent/skills/gstack-ship/sections/documentation.md +0 -118
  913. package/.agent/skills/gstack-ship/sections/documentation.md.tmpl +0 -116
  914. package/.agent/skills/gstack-ship/sections/greptile-early.md +0 -57
  915. package/.agent/skills/gstack-ship/sections/greptile-early.md.tmpl +0 -48
  916. package/.agent/skills/gstack-ship/sections/greptile.md +0 -95
  917. package/.agent/skills/gstack-ship/sections/greptile.md.tmpl +0 -93
  918. package/.agent/skills/gstack-ship/sections/handoff.md +0 -22
  919. package/.agent/skills/gstack-ship/sections/handoff.md.tmpl +0 -20
  920. package/.agent/skills/gstack-ship/sections/manifest.json +0 -92
  921. package/.agent/skills/gstack-ship/sections/measure.md +0 -66
  922. package/.agent/skills/gstack-ship/sections/measure.md.tmpl +0 -64
  923. package/.agent/skills/gstack-ship/sections/plan-completion.md +0 -387
  924. package/.agent/skills/gstack-ship/sections/plan-completion.md.tmpl +0 -67
  925. package/.agent/skills/gstack-ship/sections/pr-body.md +0 -258
  926. package/.agent/skills/gstack-ship/sections/pr-body.md.tmpl +0 -241
  927. package/.agent/skills/gstack-ship/sections/review-army.md +0 -728
  928. package/.agent/skills/gstack-ship/sections/review-army.md.tmpl +0 -171
  929. package/.agent/skills/gstack-ship/sections/shared-code-reuse.md +0 -34
  930. package/.agent/skills/gstack-ship/sections/shared-code-reuse.md.tmpl +0 -1
  931. package/.agent/skills/gstack-ship/sections/test-coverage.md +0 -478
  932. package/.agent/skills/gstack-ship/sections/test-coverage.md.tmpl +0 -118
  933. package/.agent/skills/gstack-ship/sections/tests.md +0 -453
  934. package/.agent/skills/gstack-ship/sections/tests.md.tmpl +0 -144
  935. package/.agent/skills/gstack-skillify/SKILL.md +0 -790
  936. package/.agent/skills/gstack-skillify/SKILL.md.tmpl +0 -437
  937. package/.agent/skills/gstack-spec/SKILL.md +0 -893
  938. package/.agent/skills/gstack-spec/SKILL.md.tmpl +0 -498
  939. package/.agent/skills/gstack-spec/sections/gate-and-file.md +0 -470
  940. package/.agent/skills/gstack-spec/sections/gate-and-file.md.tmpl +0 -319
  941. package/.agent/skills/gstack-spec/sections/manifest.json +0 -14
  942. package/.agent/skills/gstack-sync-gbrain/SKILL.md +0 -935
  943. package/.agent/skills/gstack-sync-gbrain/SKILL.md.tmpl +0 -615
  944. package/.agent/skills/gstack-test-audit/SKILL.md +0 -500
  945. package/.agent/skills/gstack-test-audit/SKILL.md.tmpl +0 -149
  946. package/.agent/skills/gstack-unfreeze/SKILL.md +0 -39
  947. package/.agent/skills/gstack-unfreeze/SKILL.md.tmpl +0 -39
  948. package/.agent/skills/gstack-upgrade/SKILL.md +0 -410
  949. package/.agent/skills/gstack-upgrade/SKILL.md.tmpl +0 -413
  950. package/.agent/skills/gstack-upgrade/migrations/v0.15.2.0.sh +0 -20
  951. package/.agent/skills/gstack-upgrade/migrations/v0.16.2.0.sh +0 -56
  952. package/.agent/skills/gstack-upgrade/migrations/v1.0.0.0.sh +0 -40
  953. package/.agent/skills/gstack-upgrade/migrations/v1.1.3.0.sh +0 -137
  954. package/.agent/skills/gstack-upgrade/migrations/v1.17.0.0.sh +0 -59
  955. package/.agent/skills/gstack-upgrade/migrations/v1.27.0.0.sh +0 -432
  956. package/.agent/skills/gstack-upgrade/migrations/v1.37.0.0.sh +0 -104
  957. package/.agent/skills/gstack-upgrade/migrations/v1.38.1.0.sh +0 -104
  958. package/.agent/skills/gstack-upgrade/migrations/v1.40.0.0.sh +0 -144
  959. package/.agent/skills/gstack-upgrade/migrations/v1.58.0.0.sh +0 -65
  960. package/.agent/skills/gstack-upgrade/migrations/v1.65.0.0.sh +0 -152
  961. package/.agent/skills/gstack-upgrade/migrations/v1.67.0.0.sh +0 -73
  962. package/.agent/skills/gstack-upgrade/migrations/v1.78.0.0.sh +0 -30
  963. package/.agent/skills/gstack-upgrade/migrations/v1.86.0.0.sh +0 -12
  964. package/.agent/skills/gstack-upgrade/migrations/v1.91.19.0.sh +0 -53
  965. package/.agent/skills/gstack-upgrade/migrations/v1.91.65.0.sh +0 -31
  966. package/.agent/skills/html-renderer/SKILL.md +0 -336
  967. package/.agent/skills/html-renderer/references/animation-patterns.md +0 -609
  968. package/.agent/skills/html-renderer/references/character-animation.md +0 -388
  969. package/.agent/skills/html-renderer/references/collage-broll.md +0 -41
  970. package/.agent/skills/html-renderer/references/design.md +0 -217
  971. package/.agent/skills/html-renderer/references/directing.md +0 -165
  972. package/.agent/skills/html-renderer/references/earshot-audio.md +0 -201
  973. package/.agent/skills/html-renderer/references/engine-api.md +0 -480
  974. package/.agent/skills/html-renderer/references/film-styles.md +0 -95
  975. package/.agent/skills/html-renderer/references/hyperframes-motion.md +0 -208
  976. package/.agent/skills/html-renderer/references/motion-craft.md +0 -92
  977. package/.agent/skills/html-renderer/references/onetake-camera.md +0 -59
  978. package/.agent/skills/html-renderer/references/procedural-sfx.md +0 -147
  979. package/.agent/skills/html-renderer/references/remotion-patterns.md +0 -159
  980. package/.agent/skills/html-renderer/references/render-contract.md +0 -66
  981. package/.agent/skills/motion-styles/LIBRARY.md +0 -896
  982. package/.agent/skills/motion-styles/README.md +0 -29
  983. package/.agent/skills/motion-styles/SKILL.md +0 -70
  984. package/.agent/skills/motion-styles/catalog.json +0 -2253
  985. package/.agent/skills/motion-styles/prompts/bio-glow.md +0 -149
  986. package/.agent/skills/motion-styles/prompts/blueprint.md +0 -145
  987. package/.agent/skills/motion-styles/prompts/breaking-desk.md +0 -147
  988. package/.agent/skills/motion-styles/prompts/callout-kit.md +0 -147
  989. package/.agent/skills/motion-styles/prompts/chalk-talk.md +0 -153
  990. package/.agent/skills/motion-styles/prompts/chat-story.md +0 -143
  991. package/.agent/skills/motion-styles/prompts/clay-diorama.md +0 -149
  992. package/.agent/skills/motion-styles/prompts/clean-tech.md +0 -147
  993. package/.agent/skills/motion-styles/prompts/comic-panels.md +0 -145
  994. package/.agent/skills/motion-styles/prompts/cosmic-scale.md +0 -147
  995. package/.agent/skills/motion-styles/prompts/creator-motion.md +0 -149
  996. package/.agent/skills/motion-styles/prompts/data-city.md +0 -149
  997. package/.agent/skills/motion-styles/prompts/data-story.md +0 -147
  998. package/.agent/skills/motion-styles/prompts/deep-layers.md +0 -147
  999. package/.agent/skills/motion-styles/prompts/doc-cinema.md +0 -145
  1000. package/.agent/skills/motion-styles/prompts/evidence-board.md +0 -145
  1001. package/.agent/skills/motion-styles/prompts/exploded-view.md +0 -149
  1002. package/.agent/skills/motion-styles/prompts/glass-icons.md +0 -147
  1003. package/.agent/skills/motion-styles/prompts/glitch-pack.md +0 -145
  1004. package/.agent/skills/motion-styles/prompts/globe-arcs.md +0 -147
  1005. package/.agent/skills/motion-styles/prompts/headline-backdrop.md +0 -145
  1006. package/.agent/skills/motion-styles/prompts/iso-cutaway.md +0 -147
  1007. package/.agent/skills/motion-styles/prompts/isotype.md +0 -145
  1008. package/.agent/skills/motion-styles/prompts/karaoke-subs.md +0 -145
  1009. package/.agent/skills/motion-styles/prompts/low-poly-atlas.md +0 -149
  1010. package/.agent/skills/motion-styles/prompts/lower-thirds.md +0 -147
  1011. package/.agent/skills/motion-styles/prompts/marker-board.md +0 -145
  1012. package/.agent/skills/motion-styles/prompts/market-tape.md +0 -145
  1013. package/.agent/skills/motion-styles/prompts/math-canvas.md +0 -145
  1014. package/.agent/skills/motion-styles/prompts/myth-or-fact.md +0 -145
  1015. package/.agent/skills/motion-styles/prompts/neon-line.md +0 -145
  1016. package/.agent/skills/motion-styles/prompts/paper-craft.md +0 -145
  1017. package/.agent/skills/motion-styles/prompts/parchment-map.md +0 -147
  1018. package/.agent/skills/motion-styles/prompts/physics-toybox.md +0 -147
  1019. package/.agent/skills/motion-styles/prompts/pixel-quest.md +0 -145
  1020. package/.agent/skills/motion-styles/prompts/podcast-clip.md +0 -147
  1021. package/.agent/skills/motion-styles/prompts/pop-info.md +0 -145
  1022. package/.agent/skills/motion-styles/prompts/product-turntable.md +0 -149
  1023. package/.agent/skills/motion-styles/prompts/punch-captions.md +0 -145
  1024. package/.agent/skills/motion-styles/prompts/quiz-countdown.md +0 -145
  1025. package/.agent/skills/motion-styles/prompts/quote-cinema.md +0 -145
  1026. package/.agent/skills/motion-styles/prompts/recipe-steps.md +0 -145
  1027. package/.agent/skills/motion-styles/prompts/rubber-hose-reel.md +0 -145
  1028. package/.agent/skills/motion-styles/prompts/shadow-puppets.md +0 -145
  1029. package/.agent/skills/motion-styles/prompts/size-lineup.md +0 -149
  1030. package/.agent/skills/motion-styles/prompts/software-walkthrough.md +0 -147
  1031. package/.agent/skills/motion-styles/prompts/stack-split.md +0 -145
  1032. package/.agent/skills/motion-styles/prompts/sticker-pop.md +0 -145
  1033. package/.agent/skills/motion-styles/prompts/storybook.md +0 -145
  1034. package/.agent/skills/motion-styles/prompts/swiss-grid.md +0 -145
  1035. package/.agent/skills/motion-styles/prompts/synth-grid.md +0 -147
  1036. package/.agent/skills/motion-styles/prompts/tactics-board.md +0 -145
  1037. package/.agent/skills/motion-styles/prompts/terminal.md +0 -145
  1038. package/.agent/skills/motion-styles/prompts/tier-list.md +0 -143
  1039. package/.agent/skills/motion-styles/prompts/timeline-rail.md +0 -145
  1040. package/.agent/skills/motion-styles/prompts/top-5-countdown.md +0 -145
  1041. package/.agent/skills/motion-styles/prompts/versus-split.md +0 -145
  1042. package/.agent/skills/motion-styles/prompts/vhs-camcorder.md +0 -145
  1043. package/.agent/skills/motion-styles/prompts/visual-novel.md +0 -145
  1044. package/.agent/skills/motion-styles/prompts/warm-canvas.md +0 -145
  1045. package/.agent/skills/motion-styles/prompts/woodcut-tale.md +0 -145
  1046. package/.agent/skills/motion-styles/prompts/zine-collage.md +0 -145
  1047. package/.agent/skills/motion-styles/sources/bio-glow.html +0 -639
  1048. package/.agent/skills/motion-styles/sources/blueprint.html +0 -617
  1049. package/.agent/skills/motion-styles/sources/breaking-desk.html +0 -585
  1050. package/.agent/skills/motion-styles/sources/callout-kit.html +0 -586
  1051. package/.agent/skills/motion-styles/sources/chalk-talk.html +0 -597
  1052. package/.agent/skills/motion-styles/sources/chat-story.html +0 -597
  1053. package/.agent/skills/motion-styles/sources/clay-diorama.html +0 -681
  1054. package/.agent/skills/motion-styles/sources/clean-tech.html +0 -554
  1055. package/.agent/skills/motion-styles/sources/comic-panels.html +0 -608
  1056. package/.agent/skills/motion-styles/sources/cosmic-scale.html +0 -628
  1057. package/.agent/skills/motion-styles/sources/creator-motion.html +0 -805
  1058. package/.agent/skills/motion-styles/sources/data-city.html +0 -613
  1059. package/.agent/skills/motion-styles/sources/data-story.html +0 -539
  1060. package/.agent/skills/motion-styles/sources/deep-layers.html +0 -624
  1061. package/.agent/skills/motion-styles/sources/doc-cinema.html +0 -590
  1062. package/.agent/skills/motion-styles/sources/evidence-board.html +0 -610
  1063. package/.agent/skills/motion-styles/sources/exploded-view.html +0 -618
  1064. package/.agent/skills/motion-styles/sources/glass-icons.html +0 -612
  1065. package/.agent/skills/motion-styles/sources/glitch-pack.html +0 -583
  1066. package/.agent/skills/motion-styles/sources/globe-arcs.html +0 -621
  1067. package/.agent/skills/motion-styles/sources/headline-backdrop.html +0 -584
  1068. package/.agent/skills/motion-styles/sources/iso-cutaway.html +0 -621
  1069. package/.agent/skills/motion-styles/sources/isotype.html +0 -588
  1070. package/.agent/skills/motion-styles/sources/karaoke-subs.html +0 -577
  1071. package/.agent/skills/motion-styles/sources/low-poly-atlas.html +0 -621
  1072. package/.agent/skills/motion-styles/sources/lower-thirds.html +0 -582
  1073. package/.agent/skills/motion-styles/sources/marker-board.html +0 -598
  1074. package/.agent/skills/motion-styles/sources/market-tape.html +0 -598
  1075. package/.agent/skills/motion-styles/sources/math-canvas.html +0 -592
  1076. package/.agent/skills/motion-styles/sources/myth-or-fact.html +0 -587
  1077. package/.agent/skills/motion-styles/sources/neon-line.html +0 -597
  1078. package/.agent/skills/motion-styles/sources/paper-craft.html +0 -612
  1079. package/.agent/skills/motion-styles/sources/parchment-map.html +0 -624
  1080. package/.agent/skills/motion-styles/sources/physics-toybox.html +0 -617
  1081. package/.agent/skills/motion-styles/sources/pixel-quest.html +0 -609
  1082. package/.agent/skills/motion-styles/sources/podcast-clip.html +0 -585
  1083. package/.agent/skills/motion-styles/sources/pop-info.html +0 -677
  1084. package/.agent/skills/motion-styles/sources/product-turntable.html +0 -611
  1085. package/.agent/skills/motion-styles/sources/punch-captions.html +0 -583
  1086. package/.agent/skills/motion-styles/sources/quiz-countdown.html +0 -593
  1087. package/.agent/skills/motion-styles/sources/quote-cinema.html +0 -581
  1088. package/.agent/skills/motion-styles/sources/recipe-steps.html +0 -599
  1089. package/.agent/skills/motion-styles/sources/rubber-hose-reel.html +0 -627
  1090. package/.agent/skills/motion-styles/sources/shadow-puppets.html +0 -609
  1091. package/.agent/skills/motion-styles/sources/size-lineup.html +0 -620
  1092. package/.agent/skills/motion-styles/sources/software-walkthrough.html +0 -651
  1093. package/.agent/skills/motion-styles/sources/stack-split.html +0 -588
  1094. package/.agent/skills/motion-styles/sources/sticker-pop.html +0 -587
  1095. package/.agent/skills/motion-styles/sources/storybook.html +0 -600
  1096. package/.agent/skills/motion-styles/sources/swiss-grid.html +0 -590
  1097. package/.agent/skills/motion-styles/sources/synth-grid.html +0 -614
  1098. package/.agent/skills/motion-styles/sources/tactics-board.html +0 -608
  1099. package/.agent/skills/motion-styles/sources/terminal.html +0 -593
  1100. package/.agent/skills/motion-styles/sources/tier-list.html +0 -600
  1101. package/.agent/skills/motion-styles/sources/timeline-rail.html +0 -598
  1102. package/.agent/skills/motion-styles/sources/top-5-countdown.html +0 -589
  1103. package/.agent/skills/motion-styles/sources/versus-split.html +0 -592
  1104. package/.agent/skills/motion-styles/sources/vhs-camcorder.html +0 -587
  1105. package/.agent/skills/motion-styles/sources/visual-novel.html +0 -623
  1106. package/.agent/skills/motion-styles/sources/warm-canvas.html +0 -742
  1107. package/.agent/skills/motion-styles/sources/woodcut-tale.html +0 -598
  1108. package/.agent/skills/motion-styles/sources/zine-collage.html +0 -594
  1109. package/.agent/skills/reverse-engineer-anything/SKILL.md +0 -356
  1110. package/.agent/skills/reverse-engineer-anything/references/android-applications.md +0 -36
  1111. package/.agent/skills/reverse-engineer-anything/references/connection-and-recovery.md +0 -127
  1112. package/.agent/skills/reverse-engineer-anything/references/evidence-workflows.md +0 -71
  1113. package/.agent/skills/reverse-engineer-anything/references/javascript-applications.md +0 -218
  1114. package/.agent/skills/reverse-engineer-anything/references/native-and-artifacts.md +0 -297
  1115. package/.agent/skills/reverse-engineer-anything/references/runtime-observation.md +0 -78
  1116. package/.agent/skills/video-pipeline/SKILL.md +0 -384
  1117. package/.agent/skills/video-pipeline/assets/project/ANIMATION_GUIDE.md +0 -258
  1118. package/.agent/skills/video-pipeline/assets/project/STORYBOARD.md +0 -77
  1119. package/.agent/skills/video-pipeline/assets/project/index.html +0 -80
  1120. package/.agent/skills/video-pipeline/assets/project/pipeline/briefings/diagram-author.md +0 -43
  1121. package/.agent/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +0 -75
  1122. package/.agent/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +0 -63
  1123. package/.agent/skills/video-pipeline/assets/project/reel/engine.css +0 -140
  1124. package/.agent/skills/video-pipeline/assets/project/reel/engine.js +0 -2143
  1125. package/.agent/skills/video-pipeline/assets/project/reel/virtual-clock.js +0 -168
  1126. package/.agent/skills/video-pipeline/references/mcp-tools.md +0 -441
  1127. package/.agent/skills/video-pipeline/references/motion-craft.md +0 -91
  1128. package/.agent/skills/video-pipeline/references/providers.md +0 -174
  1129. package/.agent/skills/video-pipeline/references/render-contract.md +0 -65
@@ -1,2220 +0,0 @@
1
- /**
2
- * gstack browse server — persistent Chromium daemon
3
- *
4
- * Architecture:
5
- * Bun.serve HTTP on localhost → routes commands to Playwright
6
- * Console/network/dialog buffers: CircularBuffer in-memory + async disk flush
7
- * Chromium crash → server EXITS with clear error (CLI auto-restarts)
8
- * Auto-shutdown after BROWSE_IDLE_TIMEOUT (default 30 min)
9
- *
10
- * State:
11
- * State file: <project-root>/.gstack/browse.json (set via BROWSE_STATE_FILE env)
12
- * Log files: <project-root>/.gstack/browse-{console,network,dialog}.log
13
- * Port: random 10000-60000 (or BROWSE_PORT env for debug override)
14
- */
15
-
16
- import { BrowserManager, markDaemonProcess } from './browser-manager';
17
- import { handleReadCommand, hasOutArg } from './read-commands';
18
- import { handleWriteCommand } from './write-commands';
19
- import { handleMetaCommand } from './meta-commands';
20
- import { hasActivePicker } from './cookie-picker-routes';
21
- import { COMMAND_DESCRIPTIONS, PAGE_CONTENT_COMMANDS, DOM_CONTENT_COMMANDS, wrapUntrustedContent, canonicalizeCommand, buildUnknownCommandError, ALL_COMMANDS } from './commands';
22
- import {
23
- wrapUntrustedPageContent, datamarkContent,
24
- runContentFilters, type ContentFilterResult,
25
- markHiddenElements, getCleanTextWithStripping, cleanupHiddenMarkers,
26
- } from './content-security';
27
- import { writeSecureFile, mkdirSecure, appendSecureFile } from './file-permissions';
28
- import { handleSnapshot, SNAPSHOT_FLAGS } from './snapshot';
29
- import {
30
- initRegistry, validateToken as validateScopedToken, checkScope, checkDomain,
31
- checkRate, recordCommand, isRootToken, type TokenInfo,
32
- } from './token-registry';
33
- import { resolveConfig, ensureStateDir, readVersionHash, resolveChromiumProfile, cleanSingletonLocks, isPairAgentEnabled } from './config';
34
- import {
35
- isSessionPersistEnabled, persistSessionState, restoreSessionState,
36
- sessionPersistIntervalMs, SESSION_STATE_FILE,
37
- } from './session-persist';
38
- import { emitActivity } from './activity';
39
- import { initAuditLog, writeAuditEntry } from './audit';
40
- import { detachSession } from './cdp-inspector';
41
- // Bun.spawn used instead of child_process.spawn (compiled bun binaries
42
- // fail posix_spawn on all executables including /bin/bash)
43
- import { safeUnlink, safeUnlinkQuiet, safeKill } from './error-handling';
44
- import {
45
- findAvailablePort, formatExplicitPortUnavailableError, formatRandomPortUnavailableError,
46
- } from './port-allocator';
47
- import { acquireAgentStateLock, readAgentRecord, clearAgentRecord, isOurAgent, isAgentRecordLive, isAgentRecordGone, stopAgentByRecord, spawnTerminalAgent } from './terminal-agent-control';
48
- import { isProcessAlive } from './error-handling';
49
- import { allowedExtensionOrigin } from './extension-id';
50
- import { sanitizeBody, stripLoneSurrogates } from './sanitize';
51
- import { startSocksBridge, testUpstream, type BridgeHandle } from './socks-bridge';
52
- import { parseProxyConfig, toUpstreamConfig, ProxyConfigError, serverProxyEnv } from './proxy-config';
53
- import { writeReceipt } from '../../lib/egress-receipt';
54
- import { redactProxyUrl } from './proxy-redact';
55
- import { type XvfbHandle } from './xvfb';
56
- import { logTunnelDenial } from './tunnel-denial-log';
57
- import { validateSseSessionToken, extractSseCookie } from './sse-session-cookie';
58
- import { ROUTES } from './routes';
59
- import { dispatchRoute, type RouteContext, type Surface } from './routes/table';
60
- import { TUNNEL_COMMANDS, canDispatchOverTunnel } from './routes/commands';
61
- import { getInspectorSubscriberCount, clearInspectorSubscribers } from './routes/inspector';
62
- import * as fs from 'fs';
63
- import * as net from 'net';
64
- import * as path from 'path';
65
- import * as crypto from 'crypto';
66
-
67
- const SERVER_INSTANCE_ID = crypto.randomUUID();
68
-
69
- function removeOwnedDaemonStateQuiet(): void {
70
- try {
71
- const release = acquireAgentStateLock(path.dirname(config.stateFile), 0);
72
- try {
73
- const state = JSON.parse(fs.readFileSync(config.stateFile, 'utf8'));
74
- if (state.pid === process.pid && state.instanceId === SERVER_INSTANCE_ID) safeUnlinkQuiet(config.stateFile);
75
- } finally { release(); }
76
- } catch {}
77
- }
78
-
79
- // ─── Unicode Sanitization ───────────────────────────────────────
80
- // Unpaired UTF-16 surrogate halves (\uD800–\uDFFF) in page DOM text, OCR
81
- // output, and other CDP-sourced strings are rejected by JSON consumers
82
- // downstream (Anthropic API in particular: "no low surrogate in string").
83
- // The sanitizers live in sanitize.ts (single source of truth, shared with
84
- // sse-helpers.ts and the read/snapshot pipeline): `stripLoneSurrogates`
85
- // replaces lone halves with U+FFFD (valid pairs like emoji survive), and
86
- // `sanitizeReplacer` runs it on every string value inside JSON.stringify.
87
- //
88
- // INVARIANT: every server egress path that ships page-content strings MUST
89
- // route through the sanitizer. handleCommandInternal wraps the final
90
- // cr.result string (text/plain bodies carry lone surrogates verbatim;
91
- // JSON.stringify already escapes them). The SSE producers stringify with
92
- // `sanitizeReplacer` so payload string fields get cleaned BEFORE escaping.
93
- // Plain post-stringify regex is a no-op there because JSON.stringify
94
- // converts \uD800 → "\\ud800" — the regex can't see the surrogate after
95
- // that point.
96
-
97
- // ─── Config ─────────────────────────────────────────────────────
98
- const config = resolveConfig();
99
- ensureStateDir(config);
100
- initAuditLog(config.auditLog);
101
-
102
- // ─── Auth ───────────────────────────────────────────────────────
103
- // activeShutdown points to the factory-scoped shutdown function once
104
- // buildFetchHandler has been called. Module-level timers (idle check, parent
105
- // watchdog) and signal handlers route through activeShutdown so they close
106
- // the cfg-provided browserManager rather than a stale module-level reference.
107
- // Null before the first buildFetchHandler call, which is correct: nothing to
108
- // shut down yet.
109
- let activeShutdown: ((code?: number) => Promise<void>) | null = null;
110
-
111
- // AUTH_TOKEN is injectable via process.env.AUTH_TOKEN so embedders
112
- // (gbrowser's gbd daemon spawn) can pre-allocate the token and hand it to
113
- // the Bun child via env.
114
- //
115
- // Validation: require >= 16 chars after stripping ALL unicode whitespace
116
- // (not just ASCII — .trim() misses U+200B / U+FEFF / U+00A0 / etc., which
117
- // would otherwise let a misconfigured embedder ship a one-character BOM as
118
- // the bearer secret). Reject tokens that are too short or contain only
119
- // whitespace; fall back to randomUUID so the security boundary is never
120
- // silently weakened by misconfiguration.
121
- function sanitizeAuthToken(raw: string | undefined): string | null {
122
- if (!raw) return null;
123
- const stripped = raw.replace(/[\s ​-‍]/g, '');
124
- if (stripped.length < 16) return null;
125
- return stripped;
126
- }
127
- // AUTH_TOKEN const + module-level initRegistry call deleted in v1.35.0.0.
128
- // buildFetchHandler now owns auth state end-to-end: cfg.authToken is the
129
- // single source of truth, factory body calls initRegistry(cfg.authToken),
130
- // and factory-scoped validateAuth closes over the same value. start() reads
131
- // env once via resolveConfigFromEnv() and threads the result through.
132
- const BROWSE_PORT = parseInt(process.env.BROWSE_PORT || '0', 10);
133
- const IDLE_TIMEOUT_MS = parseInt(process.env.BROWSE_IDLE_TIMEOUT || '1800000', 10); // 30 min
134
-
135
- /**
136
- * Port the local listener bound to. Set once the daemon picks a port.
137
- * Used by `$B skill run` to point spawned skill scripts at the daemon over
138
- * loopback. Module-level so handleCommandInternal can read it without threading
139
- * the port through every dispatch.
140
- */
141
- let LOCAL_LISTEN_PORT: number = 0;
142
- // Sidebar chat is always enabled in headed mode (ungated in v0.12.0)
143
-
144
- // ─── Tunnel State ───────────────────────────────────────────────
145
- //
146
- // Dual-listener architecture: the daemon binds TWO HTTP listeners when a
147
- // tunnel is active. The local listener serves bootstrap + CLI + sidebar
148
- // (never exposed to ngrok). The tunnel listener serves only the pairing
149
- // ceremony and scoped-token command endpoints (the ONLY port ngrok forwards).
150
- //
151
- // Security property comes from physical port separation: a tunnel caller
152
- // cannot reach bootstrap endpoints because they live on a different TCP
153
- // socket, not because of any per-request check.
154
- let tunnelActive = false;
155
- let tunnelUrl: string | null = null;
156
- let tunnelListener: any = null; // ngrok listener handle
157
- let tunnelServer: ReturnType<typeof Bun.serve> | null = null; // tunnel HTTP listener
158
-
159
- export type { Surface };
160
-
161
- /**
162
- * Factory contract for embedders (gbrowser phoenix overlay).
163
- *
164
- * Today the CLI calls `start()` which reads env vars and binds Bun.serve
165
- * itself. Embedders building on this server as a submodule (gbrowser's
166
- * fd-passing gbd architecture) need to inject auth + ports + a
167
- * BrowserManager they pre-launched, and own the listener themselves.
168
- *
169
- * Status: v1 surfaces this type as documentation. AUTH_TOKEN env-injection
170
- * is already live (see ~L70). `start()` is exported and the kickoff /
171
- * signal-handler registration is gated on `import.meta.main`, so phoenix
172
- * can `import { start } from '.../server'` without auto-starting. Full
173
- * `buildFetchHandler` extraction lands in a follow-up; see plan
174
- * `/Users/garrytan/.claude/plans/system-instruction-you-are-working-swirling-fountain.md`
175
- * Part 1.
176
- */
177
- export interface ServerConfig {
178
- /** Bearer token clients must present. Today injected via AUTH_TOKEN env. */
179
- authToken: string;
180
- /** Local listener port. Used in /welcome URL + state-file. */
181
- browsePort: number;
182
- /** Result of resolveConfig() — stateDir, auditLog, stateFile. */
183
- config: ReturnType<typeof resolveConfig>;
184
- /** Pre-launched BrowserManager. Caller owns lifecycle. */
185
- browserManager: BrowserManager;
186
- // NOTE: per-factory idleTimeoutMs and chromiumProfile were deleted — they
187
- // were documented but never read (the idle timer, activity state, and
188
- // shutdown target are module-global, so per-factory wiring would lie for
189
- // any embedder running >1 handler). Real support belongs to the deferred
190
- // server.ts singleton refactor. Until then: BROWSE_IDLE_TIMEOUT
191
- // and CHROMIUM_PROFILE env are the honest knobs.
192
- /** Caller-owned. shutdown() does NOT call xvfb.stop(); caller is responsible. */
193
- xvfb?: XvfbHandle | null;
194
- /** Caller-owned. shutdown() does NOT call proxyBridge.close(); caller is responsible. */
195
- proxyBridge?: BridgeHandle | null;
196
- startTime: number;
197
- /**
198
- * Overlay hook. Runs AFTER gstack resolves auth and BEFORE route dispatch.
199
- * Invalid tokens are auto-rejected at the gstack layer (401 returned
200
- * before hook fires), so the hook only ever sees valid TokenInfo or null
201
- * (no token presented). Returning a Response short-circuits gstack
202
- * dispatch; returning null falls through.
203
- */
204
- beforeRoute?: (req: Request, surface: Surface, auth: TokenInfo | null) => Promise<Response | null>;
205
- /**
206
- * Whether gstack owns the lifecycle of the terminal-agent process and its
207
- * discovery files (`<stateDir>/terminal-port`, `<stateDir>/terminal-internal-token`,
208
- * `<stateDir>/terminal-agent-pid`).
209
- *
210
- * When true (default), shutdown() runs four side effects:
211
- * 1. Identity-based kill via `killAgentByRecord(readAgentRecord(stateDir))`
212
- * (v1.44+). Only signals the PID recorded by THIS daemon's agent.
213
- * Replaced the historical `pkill -f terminal-agent\.ts` regex that
214
- * matched sibling gstack sessions on the same host — see
215
- * terminal-agent-control.ts for rationale.
216
- * 2. `safeUnlinkQuiet(<stateDir>/terminal-port)`
217
- * 3. `safeUnlinkQuiet(<stateDir>/terminal-internal-token)`
218
- * 4. `safeUnlinkQuiet(<stateDir>/terminal-agent-pid)` (the v1.44 record)
219
- *
220
- * This is correct for gstack's CLI path, which spawns `terminal-agent.ts` as
221
- * the producer of those files (see cli.ts:1037-1063).
222
- *
223
- * Embedders (gbrowser phoenix overlay, future hosts) that run their own PTY
224
- * server and write those files themselves should pass `false`. When `false`,
225
- * the embedder owns BOTH the agent process AND all three discovery files.
226
- * Note that terminal-agent.ts's own SIGTERM cleanup removes `terminal-port`
227
- * and `terminal-agent-pid` (the agent writes both at boot), so embedders
228
- * that pre-launch their own agent must ensure their cleanup matches.
229
- *
230
- * Polarity note: this differs from `xvfb?` and `proxyBridge?`, which gate by
231
- * the *presence* of a caller-owned handle (presence ⇒ don't close). This
232
- * field gates by an explicit boolean because there is no handle object —
233
- * the terminal-agent is started elsewhere (cli.ts), and shutdown's only
234
- * reference is the PID record + the file paths.
235
- */
236
- ownsTerminalAgent?: boolean;
237
- }
238
-
239
- /**
240
- * Return shape of buildFetchHandler() — fetch handlers + lifecycle helpers
241
- * embedders need to drive their own Bun.serve binding. See ServerConfig.
242
- */
243
- export interface ServerHandle {
244
- fetchLocal: (req: Request, server: any) => Promise<Response>;
245
- fetchTunnel: (req: Request, server: any) => Promise<Response>;
246
- /**
247
- * Drains buffers, kills terminal-agent, closes browser, clears intervals,
248
- * removes state files. Does NOT stop bound Bun.Server listeners — call
249
- * stopListeners() for that. CLI relies on process.exit() to drop sockets.
250
- */
251
- shutdown: (exitCode?: number) => Promise<void>;
252
- /**
253
- * Graceful listener stop for embedders. Calls server.stop(true) on each
254
- * passed Bun.Server. CLI doesn't need this (process.exit handles it).
255
- */
256
- stopListeners: (local: any, tunnel?: any) => Promise<void>;
257
- }
258
-
259
- /**
260
- * Build a ServerConfig-shaped object from process.env. Used by gstack's
261
- * own CLI when running `bun run dev` or the compiled binary directly.
262
- * Embedders construct their own ServerConfig explicitly.
263
- *
264
- * Reads env, calls resolveConfig(). Does NOT bind a listener or call
265
- * initAuditLog/initRegistry — those happen inside the buildFetchHandler
266
- * lifecycle.
267
- */
268
- export function resolveConfigFromEnv(): Omit<ServerConfig, 'browserManager' | 'startTime'> & {
269
- config: ReturnType<typeof resolveConfig>;
270
- } {
271
- return {
272
- // Same sanitizer as the module-level AUTH_TOKEN: strips ALL unicode
273
- // whitespace and rejects tokens shorter than 16 chars so a misconfigured
274
- // embedder can't ship a BOM/zero-width as the bearer secret.
275
- authToken: sanitizeAuthToken(process.env.AUTH_TOKEN) || crypto.randomUUID(),
276
- browsePort: parseInt(process.env.BROWSE_PORT || '0', 10),
277
- config: resolveConfig(),
278
- };
279
- }
280
-
281
- /**
282
- * Paths reachable over the tunnel surface. Everything else returns 404.
283
- *
284
- * `/connect` is the only unauthenticated tunnel endpoint — POST for setup-key
285
- * exchange, GET for an `{alive: true}` probe used by /pair and /tunnel/start
286
- * to detect dead ngrok tunnels. Other paths in this set require a scoped
287
- * token via Authorization: Bearer.
288
- *
289
- * Updating this set is a deliberate security decision. Every addition widens
290
- * the tunnel attack surface. It must equal the paths of the route-table
291
- * entries that declare the 'tunnel' surface (browse/test/server-route-table.test.ts),
292
- * so widening the tunnel means editing this literal and the entry.
293
- */
294
- const TUNNEL_PATHS = new Set<string>([
295
- '/connect',
296
- '/command',
297
- ]);
298
-
299
- /**
300
- * POST /extension-token releases AUTH_TOKEN only to an Origin of exactly the
301
- * pinned extension, or the one set with `gstack-config set browse_extension_id`
302
- * (browse/src/extension-id.ts, shared with the terminal agent's /ws gate).
303
- */
304
- export { GSTACK_EXTENSION_ID } from './extension-id';
305
-
306
- /**
307
- * The extension-origin auth check (POST /extension-token): Origin is exactly
308
- * the pinned extension and Host is loopback. Defense-in-depth alongside the
309
- * 127.0.0.1 bind: a DNS-rebinding page can't present a localhost Host header.
310
- * Host arrives as '127.0.0.1:34567', so parse out the hostname — never compare
311
- * the raw header (which carries the port) against a literal.
312
- */
313
- function isPinnedExtensionRequest(req: Request): boolean {
314
- let hostname: string | null = null;
315
- try {
316
- hostname = new URL(`http://${req.headers.get('host') ?? ''}`).hostname;
317
- } catch (err) {
318
- if (!(err instanceof TypeError)) throw err; // TypeError = malformed Host
319
- }
320
- const originOk = req.headers.get('origin') === allowedExtensionOrigin();
321
- const hostOk = hostname === '127.0.0.1' || hostname === 'localhost';
322
- return originOk && hostOk;
323
- }
324
-
325
- export { TUNNEL_COMMANDS, canDispatchOverTunnel };
326
-
327
- /**
328
- * Read ngrok authtoken from env var, ~/.gstack/ngrok.env, or ngrok's native
329
- * config files. Returns null if nothing found. Shared between the
330
- * /tunnel/start handler and the BROWSE_TUNNEL=1 auto-start flow.
331
- */
332
- function resolveNgrokAuthtoken(): string | null {
333
- let authtoken = process.env.NGROK_AUTHTOKEN;
334
- if (authtoken) return authtoken;
335
-
336
- const home = process.env.HOME || '';
337
- const ngrokEnvPath = path.join(home, '.gstack', 'ngrok.env');
338
- if (fs.existsSync(ngrokEnvPath)) {
339
- try {
340
- const envContent = fs.readFileSync(ngrokEnvPath, 'utf-8');
341
- const match = envContent.match(/^NGROK_AUTHTOKEN=(.+)$/m);
342
- if (match) return match[1].trim();
343
- } catch {}
344
- }
345
-
346
- const ngrokConfigs = [
347
- path.join(home, 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
348
- path.join(home, '.config', 'ngrok', 'ngrok.yml'),
349
- path.join(home, '.ngrok2', 'ngrok.yml'),
350
- ];
351
- for (const conf of ngrokConfigs) {
352
- try {
353
- const content = fs.readFileSync(conf, 'utf-8');
354
- const match = content.match(/authtoken:\s*(.+)/);
355
- if (match) return match[1].trim();
356
- } catch {}
357
- }
358
- return null;
359
- }
360
-
361
- /**
362
- * Tear down the tunnel: close the ngrok listener and stop the tunnel-surface
363
- * Bun.serve listener. Safe to call with nothing running. Always clears
364
- * tunnel state regardless of individual close failures.
365
- */
366
- async function closeTunnel(): Promise<void> {
367
- try { if (tunnelListener) await tunnelListener.close(); } catch {}
368
- try { if (tunnelServer) tunnelServer.stop(true); } catch {}
369
- tunnelListener = null;
370
- tunnelServer = null;
371
- tunnelUrl = null;
372
- tunnelActive = false;
373
- }
374
-
375
- /**
376
- * Result of startTunnel(). `stage` tells the caller which half failed so it
377
- * can keep its distinct error surface: 'bind' = the tunnel-surface Bun.serve
378
- * listener could not bind (nothing to clean up), 'ngrok' = anything after the
379
- * bind (ngrok forward, egress receipt, state-file write) — startTunnel has
380
- * already torn down both ngrok and the Bun listener by the time it returns.
381
- */
382
- type StartTunnelResult =
383
- | { ok: true; url: string }
384
- | { ok: false; stage: 'bind' | 'ngrok'; error: Error };
385
-
386
- /**
387
- * Start the ngrok tunnel using the dual-listener pattern: bind a dedicated
388
- * tunnel-surface listener on an ephemeral 127.0.0.1 port and point
389
- * ngrok.forward() at THAT port — the local listener (which serves
390
- * /extension-token, /cookie-picker, /inspector/*, welcome, etc.) is never
391
- * exposed to ngrok. Shared by the /tunnel/start route handler (which passes
392
- * its in-closure makeFetchHandler('tunnel')) and the BROWSE_TUNNEL=1
393
- * auto-start flow in start() (which passes handle.fetchTunnel from the
394
- * factory). The BROWSE_TUNNEL_LOCAL_ONLY=1 test path does NOT use this
395
- * helper — it binds the tunnel surface with no ngrok forwarding at all.
396
- *
397
- * Hard fail on listener bind (`stage: 'bind'`) — NEVER fall back to the
398
- * local port, which would silently defeat the whole security property.
399
- *
400
- * On success, sets the module tunnel state (tunnelListener / tunnelUrl /
401
- * tunnelServer / tunnelActive) and records the tunnel in the state file.
402
- */
403
- async function startTunnel(opts: {
404
- fetchHandler: (req: Request, server: any) => Promise<Response>;
405
- authtoken: string;
406
- consent: string;
407
- }): Promise<StartTunnelResult> {
408
- // Bind the tunnel listener on an ephemeral port. HARD FAIL if this
409
- // errors — never fall back to the local port.
410
- let boundTunnel: ReturnType<typeof Bun.serve>;
411
- try {
412
- boundTunnel = Bun.serve({
413
- port: 0,
414
- hostname: '127.0.0.1',
415
- fetch: opts.fetchHandler,
416
- });
417
- } catch (err: any) {
418
- return { ok: false, stage: 'bind', error: err };
419
- }
420
- const tunnelPort = boundTunnel.port;
421
-
422
- // Point ngrok at the TUNNEL port (not the local port). If this fails,
423
- // tear the listener back down so we don't leak sockets.
424
- try {
425
- const ngrok = await import('@ngrok/ngrok');
426
- const domain = process.env.NGROK_DOMAIN;
427
- const forwardOpts: any = { addr: tunnelPort, authtoken: opts.authtoken };
428
- if (domain) forwardOpts.domain = domain;
429
-
430
- // Egress receipt BEFORE the tunnel session opens, fail-closed: a
431
- // writeReceipt failure lands in this catch, which tears the tunnel
432
- // listener back down and refuses the start. One receipt per session
433
- // open; browse command behavior over the tunnel is unchanged.
434
- writeReceipt({
435
- sink: 'browse-tunnel',
436
- host: domain || 'connect.ngrok-agent.com',
437
- payloadClass: 'tunnel-session-open (scoped-token browser-command surface)',
438
- bytes: 0,
439
- sha256: null,
440
- consent: opts.consent,
441
- });
442
-
443
- tunnelListener = await ngrok.forward(forwardOpts);
444
- tunnelUrl = tunnelListener.url();
445
- tunnelServer = boundTunnel;
446
- tunnelActive = true;
447
- console.log(`[browse] Tunnel listener bound on 127.0.0.1:${tunnelPort}, ngrok → ${tunnelUrl}`);
448
-
449
- // Update state file
450
- const releaseStateLock = acquireAgentStateLock(config.stateDir);
451
- try {
452
- const stateContent = JSON.parse(fs.readFileSync(config.stateFile, 'utf-8'));
453
- if (stateContent.pid !== process.pid || stateContent.instanceId !== SERVER_INSTANCE_ID) throw new Error('daemon state was replaced');
454
- stateContent.tunnel = { url: tunnelUrl, domain: domain || null, startedAt: new Date().toISOString() };
455
- const tmpState = tmpStatePath();
456
- fs.writeFileSync(tmpState, JSON.stringify(stateContent, null, 2), { mode: 0o600 });
457
- fs.renameSync(tmpState, config.stateFile);
458
- } finally { releaseStateLock(); }
459
-
460
- return { ok: true, url: tunnelUrl! };
461
- } catch (err: any) {
462
- // Clean up BOTH ngrok and the Bun listener on failure. If
463
- // ngrok.forward() succeeded but tunnelListener.url() or the
464
- // state-file write threw, we'd otherwise leak an active ngrok
465
- // session on the user's account.
466
- try { if (tunnelListener) await tunnelListener.close(); } catch {}
467
- try { boundTunnel.stop(true); } catch {}
468
- tunnelListener = null;
469
- return { ok: false, stage: 'ngrok', error: err };
470
- }
471
- }
472
-
473
- // Module-level validateAuth deleted in v1.35.0.0. Factory-scoped equivalent
474
- // in buildFetchHandler closes over cfg.authToken so every internal auth check
475
- // sees the same token the routes receive.
476
-
477
- /**
478
- * Terminal-agent discovery. The non-compiled bun process at
479
- * `browse/src/terminal-agent.ts` writes its chosen port to
480
- * `<stateDir>/terminal-port` and the loopback handshake token to
481
- * `<stateDir>/terminal-internal-token` once it boots. Read on demand —
482
- * lazy so we don't break tests that don't spawn the agent.
483
- */
484
- function readTerminalPort(): number | null {
485
- try {
486
- const f = path.join(path.dirname(config.stateFile), 'terminal-port');
487
- const v = parseInt(fs.readFileSync(f, 'utf-8').trim(), 10);
488
- return Number.isFinite(v) && v > 0 ? v : null;
489
- } catch { return null; }
490
- }
491
- function readTerminalInternalToken(): string | null {
492
- try {
493
- const f = path.join(path.dirname(config.stateFile), 'terminal-internal-token');
494
- const t = fs.readFileSync(f, 'utf-8').trim();
495
- return t.length > 16 ? t : null;
496
- } catch { return null; }
497
- }
498
-
499
- /**
500
- * Push a freshly-minted PTY cookie token to the terminal-agent so its
501
- * /ws upgrade can validate the cookie. v1.44+: also pushes the bound
502
- * sessionId so the agent can route /internal/restart and (Commit 3)
503
- * re-attach back to the same PtySession. Loopback POST authenticated
504
- * with the internal token written by the agent at startup. If the agent
505
- * isn't up yet, the extension just retries /pty-session.
506
- */
507
- async function grantPtyToken(token: string, sessionId?: string): Promise<boolean> {
508
- const port = readTerminalPort();
509
- const internal = readTerminalInternalToken();
510
- if (!port || !internal) return false;
511
- try {
512
- const resp = await fetch(`http://127.0.0.1:${port}/internal/grant`, {
513
- method: 'POST',
514
- headers: {
515
- 'Content-Type': 'application/json',
516
- 'Authorization': `Bearer ${internal}`,
517
- },
518
- body: JSON.stringify(sessionId ? { token, sessionId } : { token }),
519
- signal: AbortSignal.timeout(2000),
520
- });
521
- return resp.ok;
522
- } catch { return false; }
523
- }
524
-
525
- /**
526
- * Ask the terminal-agent to dispose the PtySession bound to `sessionId`.
527
- * Scoped to one caller's session — sibling tabs/agents untouched. Used by
528
- * /pty-restart and /pty-dispose. Returns true on agent ack.
529
- */
530
- async function restartPtySession(sessionId: string): Promise<boolean> {
531
- const port = readTerminalPort();
532
- const internal = readTerminalInternalToken();
533
- if (!port || !internal) return false;
534
- try {
535
- const resp = await fetch(`http://127.0.0.1:${port}/internal/restart`, {
536
- method: 'POST',
537
- headers: {
538
- 'Content-Type': 'application/json',
539
- 'Authorization': `Bearer ${internal}`,
540
- },
541
- body: JSON.stringify({ sessionId }),
542
- signal: AbortSignal.timeout(5000),
543
- });
544
- return resp.ok;
545
- } catch { return false; }
546
- }
547
-
548
- /** Extract bearer token from request. Returns the token string or null. */
549
- function extractToken(req: Request): string | null {
550
- const header = req.headers.get('authorization');
551
- if (!header?.startsWith('Bearer ')) return null;
552
- return header.slice(7);
553
- }
554
-
555
- /** Validate token and return TokenInfo. Returns null if invalid/expired. */
556
- function getTokenInfo(req: Request): TokenInfo | null {
557
- const token = extractToken(req);
558
- if (!token) return null;
559
- return validateScopedToken(token);
560
- }
561
-
562
- /** Check if request is from root token (local use). */
563
- function isRootRequest(req: Request): boolean {
564
- const token = extractToken(req);
565
- return token !== null && isRootToken(token);
566
- }
567
-
568
- // ─── Help text (auto-generated from COMMAND_DESCRIPTIONS) ────────
569
- function generateHelpText(): string {
570
- // Group commands by category
571
- const groups = new Map<string, string[]>();
572
- for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) {
573
- const display = meta.usage || cmd;
574
- const list = groups.get(meta.category) || [];
575
- list.push(display);
576
- groups.set(meta.category, list);
577
- }
578
-
579
- const categoryOrder = [
580
- 'Navigation', 'Reading', 'Interaction', 'Inspection',
581
- 'Visual', 'Snapshot', 'Meta', 'Tabs', 'Server',
582
- ];
583
-
584
- const lines = ['gstack browse — headless browser for AI agents', '', 'Commands:'];
585
- for (const cat of categoryOrder) {
586
- const cmds = groups.get(cat);
587
- if (!cmds) continue;
588
- lines.push(` ${(cat + ':').padEnd(15)}${cmds.join(', ')}`);
589
- }
590
-
591
- // Snapshot flags from source of truth
592
- lines.push('');
593
- lines.push('Snapshot flags:');
594
- const flagPairs: string[] = [];
595
- for (const flag of SNAPSHOT_FLAGS) {
596
- const label = flag.valueHint ? `${flag.short} ${flag.valueHint}` : flag.short;
597
- flagPairs.push(`${label} ${flag.long}`);
598
- }
599
- // Print two flags per line for compact display
600
- for (let i = 0; i < flagPairs.length; i += 2) {
601
- const left = flagPairs[i].padEnd(28);
602
- const right = flagPairs[i + 1] || '';
603
- lines.push(` ${left}${right}`);
604
- }
605
-
606
- return lines.join('\n');
607
- }
608
-
609
- // ─── Buffer (from buffers.ts) ────────────────────────────────────
610
- import { consoleBuffer, networkBuffer, dialogBuffer, addConsoleEntry, addNetworkEntry, addDialogEntry, type LogEntry, type NetworkEntry, type DialogEntry } from './buffers';
611
-
612
- export { consoleBuffer, networkBuffer, dialogBuffer, addConsoleEntry, addNetworkEntry, addDialogEntry, type LogEntry, type NetworkEntry, type DialogEntry };
613
-
614
- const CONSOLE_LOG_PATH = config.consoleLog;
615
- const NETWORK_LOG_PATH = config.networkLog;
616
- const DIALOG_LOG_PATH = config.dialogLog;
617
-
618
- /**
619
- * Per-process state-file temp path. The state-file write pattern is
620
- * `writeFileSync(tmp, ...) → renameSync(tmp, stateFile)` for atomicity,
621
- * but a shared `${stateFile}.tmp` filename means two concurrent writers
622
- * (cold-start race when N CLIs hit a fresh repo simultaneously, parallel
623
- * /tunnel/start handlers, or a combination) collide on the rename: the
624
- * first writer's renameSync moves the shared temp file out of the way,
625
- * the second writer's writeFileSync re-creates it, the second rename
626
- * then races with the first writer's already-renamed state. Worst case
627
- * the second renameSync throws ENOENT mid-air, killing one of the
628
- * spawning daemons during startup.
629
- *
630
- * Per-process suffix (pid + 4 random bytes) makes each writer's temp
631
- * path unique. The atomic rename still gives last-writer-wins semantics
632
- * for the final state.json content; the only behavior change is that
633
- * concurrent writers no longer kill each other on the rename.
634
- */
635
- function tmpStatePath(stateFile: string = config.stateFile): string {
636
- return `${stateFile}.tmp.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
637
- }
638
-
639
-
640
- // ─── Sidebar agent / chat state ripped ──────────────────────────────
641
- let lastConsoleFlushed = 0;
642
- let lastNetworkFlushed = 0;
643
- let lastDialogFlushed = 0;
644
- let flushInProgress = false;
645
-
646
- async function flushBuffers() {
647
- if (flushInProgress) return; // Guard against concurrent flush
648
- flushInProgress = true;
649
-
650
- try {
651
- // Console buffer
652
- const newConsoleCount = consoleBuffer.totalAdded - lastConsoleFlushed;
653
- if (newConsoleCount > 0) {
654
- const entries = consoleBuffer.last(Math.min(newConsoleCount, consoleBuffer.length));
655
- const lines = entries.map(e =>
656
- `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
657
- ).join('\n') + '\n';
658
- appendSecureFile(CONSOLE_LOG_PATH, lines);
659
- lastConsoleFlushed = consoleBuffer.totalAdded;
660
- }
661
-
662
- // Network buffer
663
- const newNetworkCount = networkBuffer.totalAdded - lastNetworkFlushed;
664
- if (newNetworkCount > 0) {
665
- const entries = networkBuffer.last(Math.min(newNetworkCount, networkBuffer.length));
666
- const lines = entries.map(e =>
667
- `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} → ${e.status || 'pending'} (${e.duration || '?'}ms, ${e.size || '?'}B)`
668
- ).join('\n') + '\n';
669
- appendSecureFile(NETWORK_LOG_PATH, lines);
670
- lastNetworkFlushed = networkBuffer.totalAdded;
671
- }
672
-
673
- // Dialog buffer
674
- const newDialogCount = dialogBuffer.totalAdded - lastDialogFlushed;
675
- if (newDialogCount > 0) {
676
- const entries = dialogBuffer.last(Math.min(newDialogCount, dialogBuffer.length));
677
- const lines = entries.map(e =>
678
- `[${new Date(e.timestamp).toISOString()}] [${e.type}] "${e.message}" → ${e.action}${e.response ? ` "${e.response}"` : ''}`
679
- ).join('\n') + '\n';
680
- appendSecureFile(DIALOG_LOG_PATH, lines);
681
- lastDialogFlushed = dialogBuffer.totalAdded;
682
- }
683
- } catch (err: any) {
684
- console.error('[browse] Buffer flush failed:', err.message);
685
- } finally {
686
- flushInProgress = false;
687
- }
688
- }
689
-
690
- // Flush every 1 second
691
- const flushInterval = setInterval(flushBuffers, 1000);
692
-
693
- // ─── Idle Timer ────────────────────────────────────────────────
694
- let lastActivity = Date.now();
695
-
696
- function resetIdleTimer() {
697
- lastActivity = Date.now();
698
- }
699
-
700
- // Named for behavioral testing via __testInternals__. The factory tests in
701
- // server-factory.test.ts call this directly so the idle-shutdown path can be
702
- // exercised without waiting 60s for the interval to fire.
703
- function idleCheckTick() {
704
- // Headed mode: the user is looking at the browser. Never auto-die.
705
- // Only shut down when the user explicitly disconnects or closes the window.
706
- // Reads via the activeBrowserManager indirection so embedders that pass
707
- // their own BrowserManager into buildFetchHandler hit the right instance.
708
- if (activeBrowserManager.getConnectionMode() === 'headed') return;
709
- // Tunnel mode: remote agents may send commands sporadically. Never auto-die.
710
- if (tunnelActive) return;
711
- if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) {
712
- console.log(`[browse] Idle for ${IDLE_TIMEOUT_MS / 1000}s, shutting down`);
713
- activeShutdown?.();
714
- }
715
- }
716
- const idleCheckInterval = setInterval(idleCheckTick, 60_000);
717
-
718
- // Test-only surface for server-factory.test.ts. Lets the dual-instance
719
- // idle-timer behavior be exercised deterministically without mutating
720
- // Date.now (which would interact with the leaked module-level setInterval).
721
- // Production code must never import this — see `idle timer + onDisconnect
722
- // dual-instance fix` describe block for usage.
723
- export const __testInternals__ = {
724
- serverInstanceId: SERVER_INSTANCE_ID,
725
- tunnelPaths: TUNNEL_PATHS as ReadonlySet<string>,
726
- idleCheckTick,
727
- // Watchdog seams (watchdog.test.ts): drive the 15s poll against an
728
- // arbitrary (dead) PID, trigger the handoff-promotion suppression exactly
729
- // as onHeadedPromotion does, and reset the latches between tests.
730
- parentWatchdogTick,
731
- suppressHeadedParentShutdown,
732
- resetParentWatchdogState: () => { headedParentShutdownSuppressed = false; parentGone = false; },
733
- setTunnelActive: (v: boolean) => { tunnelActive = v; },
734
- setLastActivity: (t: number) => { lastActivity = t; },
735
- formatExplicitPortUnavailableError,
736
- formatRandomPortUnavailableError,
737
- // Reset the module-level shutdown latch so tests that drive shutdown to
738
- // completion (process.exit-stubbed) can be followed by tests that also
739
- // need shutdown to fire. Without this, the second test's shutdown
740
- // returns early at the `if (isShuttingDown) return;` guard.
741
- resetShutdownState: () => { isShuttingDown = false; },
742
- };
743
-
744
- // ─── Parent-Process Watchdog ────────────────────────────────────────
745
- // When the spawning CLI process (e.g. a Claude Code session) exits, this
746
- // server can become an orphan — keeping chrome-headless-shell alive and
747
- // causing console-window flicker on Windows. Poll the parent PID every 15s
748
- // and self-terminate if it is gone.
749
- //
750
- // Headed mode (BROWSE_HEADED=1 or BROWSE_PARENT_PID=0): The user controls
751
- // the browser window lifecycle. The CLI exits immediately after connect,
752
- // so the watchdog would kill the server prematurely. Disabled in both cases
753
- // as defense-in-depth — the CLI sets PID=0 for headed mode, and the server
754
- // also checks BROWSE_HEADED in case a future launcher forgets.
755
- // Cleanup happens via browser disconnect event or $B disconnect.
756
- const BROWSE_PARENT_PID = parseInt(process.env.BROWSE_PARENT_PID || '0', 10);
757
- // Outer gate: if the spawner explicitly marks this as headed (env var set at
758
- // launch time), skip registering the watchdog entirely. Cheaper than entering
759
- // the closure every 15s. The CLI's connect path sets BROWSE_HEADED=1 + PID=0,
760
- // so this branch is the normal path for /open-gstack-browser.
761
- const IS_HEADED_WATCHDOG = process.env.BROWSE_HEADED === '1';
762
- // Runtime promotion to headed (`handoff`) must NOT clear this interval — the
763
- // same tick is the tunnel-orphan reaper, and idle timeout is disabled in
764
- // tunnel mode, so parent death is the ONLY thing that reaps an
765
- // internet-exposed daemon after handoff → resume → /pair-agent. Promotion
766
- // sets this suppress flag instead; the tick re-reads it (and tunnelActive)
767
- // every pass. See suppressHeadedParentShutdown() below.
768
- let headedParentShutdownSuppressed = false;
769
- // Latch for the one-time "parent exited, staying alive" log line.
770
- let parentGone = false;
771
- // Named + parameterized (default: the boot-time env PID) so watchdog.test.ts
772
- // can drive the tick deterministically via __testInternals__, mirroring
773
- // idleCheckTick above. setInterval invokes it with no args in production.
774
- function parentWatchdogTick(parentPid: number = BROWSE_PARENT_PID): void {
775
- try {
776
- process.kill(parentPid, 0); // signal 0 = existence check only, no signal sent
777
- } catch {
778
- // Parent exited. Resolution order:
779
- // 1. Active cookie picker (one-time code or session live)? Stay alive
780
- // regardless of mode — tearing down the server mid-import leaves the
781
- // picker UI with a stale "Failed to fetch" error.
782
- // 2. Headed (unless suppressed by a runtime promotion) / tunnel mode?
783
- // Shutdown. The idle timeout doesn't apply in these modes (see
784
- // idleCheckInterval above — both early-return), so ignoring parent
785
- // death here would leak orphan daemons after /pair-agent or
786
- // /open-gstack-browser sessions.
787
- // 3. Normal (headless) mode, or headed-by-promotion? Stay alive. Claude
788
- // Code's Bash tool kills the parent shell between invocations, and a
789
- // promoted daemon's user owns the window lifecycle. The idle timeout
790
- // (30 min) handles eventual cleanup.
791
- if (hasActivePicker()) return;
792
- const headed = activeBrowserManager.getConnectionMode() === 'headed'
793
- && !headedParentShutdownSuppressed;
794
- if (headed || tunnelActive) {
795
- console.log(`[browse] Parent process ${parentPid} exited in ${headed ? 'headed' : 'tunnel'} mode, shutting down`);
796
- activeShutdown?.();
797
- } else if (!parentGone) {
798
- parentGone = true;
799
- console.log(`[browse] Parent process ${parentPid} exited (server stays alive, idle timeout will clean up)`);
800
- }
801
- }
802
- }
803
- // Poll cadence. Env-overridable as a test seam: watchdog.test.ts shrinks it
804
- // (250ms) so a free-tier test can observe a real tick deciding on a dead
805
- // parent instead of sleeping through the 15s production cadence. Production
806
- // launchers never set this; unparsable or non-positive values fall back to 15s.
807
- const rawWatchdogIntervalMs = parseInt(process.env.BROWSE_PARENT_WATCHDOG_INTERVAL_MS || '', 10);
808
- const PARENT_WATCHDOG_INTERVAL_MS =
809
- Number.isFinite(rawWatchdogIntervalMs) && rawWatchdogIntervalMs > 0
810
- ? rawWatchdogIntervalMs
811
- : 15_000;
812
- if (BROWSE_PARENT_PID > 0 && !IS_HEADED_WATCHDOG) {
813
- setInterval(parentWatchdogTick, PARENT_WATCHDOG_INTERVAL_MS);
814
- } else if (IS_HEADED_WATCHDOG) {
815
- console.log('[browse] Parent-process watchdog disabled (headed mode)');
816
- } else if (BROWSE_PARENT_PID === 0) {
817
- console.log('[browse] Parent-process watchdog disabled (BROWSE_PARENT_PID=0)');
818
- }
819
-
820
- /**
821
- * Suppress the headed-mode parent-death shutdown after a runtime promotion.
822
- *
823
- * The watchdog's contract is "headless daemons outlive their parent, headed ones
824
- * do not" — reasonable at boot, when mode is fixed by env. `handoff` breaks that
825
- * assumption: it swaps in a headed context on a RUNNING daemon
826
- * (browser-manager.ts, connectionMode = 'headed') without a restart, so a daemon
827
- * that legitimately registered a watchdog is suddenly on the fatal side of the
828
- * branch. The parent is typically a short-lived shell — Claude Code's Bash tool
829
- * kills one after every invocation — so the next 15s poll shuts the daemon down,
830
- * discarding whatever the user was handed off to do, such as a login.
831
- *
832
- * Once promoted, the user owns the window lifecycle exactly as if the daemon had
833
- * been started headed, which is the case the env guards already exempt.
834
- *
835
- * A flag, NOT clearInterval: the tick doubles as the tunnel-orphan reaper
836
- * (its `tunnelActive` branch), and idle timeout is disabled in tunnel mode —
837
- * clearing the whole interval here left handoff → resume → /pair-agent with
838
- * an internet-exposed daemon nothing could ever reap. After promotion, parent
839
- * death no longer kills the daemon for BEING HEADED, but still kills it when
840
- * a tunnel is active.
841
- */
842
- function suppressHeadedParentShutdown(stateConfig: ServerConfig['config'] = config, manager: BrowserManager = activeBrowserManager): void {
843
- if (headedParentShutdownSuppressed) return;
844
- headedParentShutdownSuppressed = true;
845
- try {
846
- const release = acquireAgentStateLock(stateConfig.stateDir);
847
- try {
848
- const state = JSON.parse(fs.readFileSync(stateConfig.stateFile, 'utf8'));
849
- if (state.pid === process.pid && state.instanceId === SERVER_INSTANCE_ID) {
850
- const xvfb = manager.getXvfbHandle();
851
- state.mode = 'headed';
852
- delete state.chromiumPid;
853
- delete state.chromiumStartTime;
854
- if (xvfb) Object.assign(state, { xvfbPid: xvfb.pid, xvfbStartTime: xvfb.startTime, xvfbDisplay: xvfb.display });
855
- const tmpFile = tmpStatePath(stateConfig.stateFile);
856
- try {
857
- fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), { mode: 0o600 });
858
- fs.renameSync(tmpFile, stateConfig.stateFile);
859
- } finally { safeUnlinkQuiet(tmpFile); }
860
- }
861
- } finally { release(); }
862
- } catch (err) {
863
- console.warn('[browse] Could not persist headed promotion:', err instanceof Error ? err.message : String(err));
864
- }
865
- console.log('[browse] Parent-death headed shutdown suppressed (promoted to headed at runtime); watchdog stays armed as the tunnel-orphan reaper');
866
- }
867
-
868
- // ─── Command Sets (from commands.ts — single source of truth) ───
869
- import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
870
-
871
- export { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS };
872
-
873
- /**
874
- * Whether an invocation should be treated as a WRITE for capability gating
875
- * (scope, watch-mode block, tab ownership, tunnel). A command is a write if it
876
- * mutates state (`WRITE_COMMANDS`) OR it carries an `--out` flag — `js`/`eval
877
- * --out` writes the evaluate result to local disk, so the capability is
878
- * per-invocation, not per-command-name. This deliberately does NOT change
879
- * dispatch routing: `js`/`eval` still route to `handleReadCommand`; only the
880
- * security gates consult this.
881
- */
882
- function isWriteInvocation(command: string, args: string[]): boolean {
883
- return WRITE_COMMANDS.has(command) || hasOutArg(args);
884
- }
885
-
886
- export { getInspectorSubscriberCount };
887
-
888
- // ─── Server ────────────────────────────────────────────────────
889
- const browserManager = new BrowserManager();
890
- // Declared here rather than beside suppressHeadedParentShutdown: that function
891
- // sits with the watchdog it gates, which is above this line, and binding it up
892
- // there would touch `browserManager` in its temporal dead zone — aborting
893
- // module evaluation and leaving every later const uninitialized.
894
- browserManager.onHeadedPromotion = suppressHeadedParentShutdown;
895
- // Indirection for embedders. Module-level handlers (idleCheckTick, parent
896
- // watchdog, SIGTERM) read activeBrowserManager so that buildFetchHandler can
897
- // retarget them at a caller-supplied BrowserManager. Symmetric with the
898
- // existing `let activeShutdown` pattern at module scope (line ~113).
899
- // Without this, embedders like gbrowser hit the dead module-level instance
900
- // whose connectionMode never leaves 'launched' — and headed mode never
901
- // short-circuits idle-shutdown.
902
- let activeBrowserManager: BrowserManager = browserManager;
903
- // When the user closes the headed browser window, run full cleanup
904
- // (kill terminal agent, save session, remove profile locks, delete state file)
905
- // before exiting. Exit code 0 means user-initiated clean quit (Cmd+Q on
906
- // macOS) so process supervisors like gbrowser's gbd skip the restart loop;
907
- // 2 means a real crash that should respawn. The fallback `?? 2` preserves
908
- // legacy crash semantics for any caller that invokes onDisconnect without
909
- // an explicit code. This is the safety-net default for the CLI flow before
910
- // any buildFetchHandler call rebinds onDisconnect onto the cfg instance.
911
- browserManager.onDisconnect = (code) => activeShutdown?.(code ?? 2);
912
- let isShuttingDown = false;
913
- // Session-persist ticker handle. Registered in start() (module scope so the
914
- // factory's shutdown() can reach it), cleared by shutdown() BEFORE the final
915
- // snapshot — a tick landing during browser teardown would otherwise overwrite
916
- // the good final snapshot with a degraded one (zero tabs).
917
- let sessionPersistInterval: ReturnType<typeof setInterval> | null = null;
918
-
919
- // Port allocation lives in port-allocator.ts (#2314, decision 8) so the
920
- // terminal-agent shares the SAME fixed 10000-60000 scan range instead of
921
- // binding port:0 into the OS ephemeral range. The imports at the top of
922
- // this file re-expose the pieces __testInternals__ pins.
923
-
924
- // Find port: explicit BROWSE_PORT, or random in 10000-60000
925
- function findPort(): Promise<number> {
926
- return findAvailablePort(BROWSE_PORT);
927
- }
928
-
929
- /**
930
- * Translate Playwright errors into actionable messages for AI agents.
931
- */
932
- function wrapError(err: any): string {
933
- const msg = err.message || String(err);
934
- // Timeout errors
935
- if (err.name === 'TimeoutError' || msg.includes('Timeout') || msg.includes('timeout')) {
936
- if (msg.includes('locator.click') || msg.includes('locator.fill') || msg.includes('locator.hover')) {
937
- return `Element not found or not interactable within timeout. Check your selector or run 'snapshot' for fresh refs.`;
938
- }
939
- if (msg.includes('page.goto') || msg.includes('Navigation')) {
940
- return `Page navigation timed out. The URL may be unreachable or the page may be loading slowly.`;
941
- }
942
- return `Operation timed out: ${msg.split('\n')[0]}`;
943
- }
944
- // Multiple elements matched
945
- if (msg.includes('resolved to') && msg.includes('elements')) {
946
- return `Selector matched multiple elements. Be more specific or use @refs from 'snapshot'.`;
947
- }
948
- // Pass through other errors
949
- return msg;
950
- }
951
-
952
- /** Internal command result — used by handleCommand and chain subcommand routing */
953
- interface CommandResult {
954
- status: number;
955
- result: string;
956
- headers?: Record<string, string>;
957
- json?: boolean; // true if result is JSON (errors), false for text/plain
958
- }
959
-
960
- /**
961
- * Core command execution logic. Returns a structured result instead of HTTP Response.
962
- * Used by both the HTTP handler (handleCommand) and chain subcommand routing.
963
- *
964
- * Options:
965
- * skipRateCheck: true when called from chain (chain counts as 1 request)
966
- * skipActivity: true when called from chain (chain emits 1 event for all subcommands)
967
- * chainDepth: recursion guard — reject nested chains (depth > 0 means inside a chain)
968
- */
969
- async function handleCommandInternalImpl(
970
- body: { command: string; args?: string[]; tabId?: number },
971
- tokenInfo?: TokenInfo | null,
972
- opts?: { skipRateCheck?: boolean; skipActivity?: boolean; chainDepth?: number },
973
- ): Promise<CommandResult> {
974
- const { args = [], tabId } = body;
975
- const rawCommand = body.command;
976
-
977
- if (!rawCommand) {
978
- return { status: 400, result: JSON.stringify({ error: 'Missing "command" field' }), json: true };
979
- }
980
-
981
- // ─── Alias canonicalization (before scope, watch, tab-ownership, dispatch) ─
982
- // Agent-friendly names like 'setcontent' route to canonical 'load-html'. Must
983
- // happen BEFORE scope check so a read-scoped token calling 'setcontent' is still
984
- // rejected (load-html lives in SCOPE_WRITE). Audit logging preserves rawCommand
985
- // so the trail records what the agent actually typed.
986
- const command = canonicalizeCommand(rawCommand);
987
- const isAliased = command !== rawCommand;
988
-
989
- // ─── Recursion guard: reject nested chains ──────────────────
990
- if (command === 'chain' && (opts?.chainDepth ?? 0) > 0) {
991
- return { status: 400, result: JSON.stringify({ error: 'Nested chain commands are not allowed' }), json: true };
992
- }
993
-
994
- // ─── Scope check (for scoped tokens) ──────────────────────────
995
- if (tokenInfo && tokenInfo.clientId !== 'root') {
996
- if (!checkScope(tokenInfo, command)) {
997
- return {
998
- status: 403, json: true,
999
- result: JSON.stringify({
1000
- error: `Command "${command}" not allowed by your token scope`,
1001
- hint: `Your scopes: ${tokenInfo.scopes.join(', ')}. Ask the user to re-pair without --restrict for full page access, or with --control for browser control commands.`,
1002
- }),
1003
- };
1004
- }
1005
-
1006
- // `--out` writes the evaluate result to local disk, which is a WRITE
1007
- // capability distinct from the JS-exec (admin) capability js/eval need.
1008
- // Require write scope so an admin-but-not-write token can't write files.
1009
- if (hasOutArg(args) && !tokenInfo.scopes.includes('write')) {
1010
- return {
1011
- status: 403, json: true,
1012
- result: JSON.stringify({
1013
- error: `"--out" writes to disk and requires the "write" scope`,
1014
- hint: `Your scopes: ${tokenInfo.scopes.join(', ')}. Re-pair with write access to use --out.`,
1015
- }),
1016
- };
1017
- }
1018
-
1019
- // Domain check for navigation commands
1020
- if ((command === 'goto' || command === 'newtab') && args[0]) {
1021
- if (!checkDomain(tokenInfo, args[0])) {
1022
- return {
1023
- status: 403, json: true,
1024
- result: JSON.stringify({
1025
- error: `Domain not allowed by your token scope`,
1026
- hint: `Allowed domains: ${tokenInfo.domains?.join(', ') || 'none configured'}`,
1027
- }),
1028
- };
1029
- }
1030
- }
1031
-
1032
- // Rate check (skipped for chain subcommands — chain counts as 1 request)
1033
- if (!opts?.skipRateCheck) {
1034
- const rateResult = checkRate(tokenInfo);
1035
- if (!rateResult.allowed) {
1036
- return {
1037
- status: 429, json: true,
1038
- result: JSON.stringify({
1039
- error: 'Rate limit exceeded',
1040
- hint: `Max ${tokenInfo.rateLimit} requests/second. Retry after ${rateResult.retryAfterMs}ms.`,
1041
- }),
1042
- headers: { 'Retry-After': String(Math.ceil((rateResult.retryAfterMs || 1000) / 1000)) },
1043
- };
1044
- }
1045
- }
1046
-
1047
- // Record command execution for idempotent key exchange tracking
1048
- if (!opts?.skipRateCheck && tokenInfo.token) recordCommand(tokenInfo.token);
1049
- }
1050
-
1051
- // Pin to a specific tab if requested (set by BROWSE_TAB env var, e.g. per-tab agent contexts).
1052
- // This prevents parallel agents from interfering with each other's tab context.
1053
- // Safe because Bun's event loop is single-threaded — no concurrent handleCommand.
1054
- let savedTabId: number | null = null;
1055
- if (tabId !== undefined && tabId !== null) {
1056
- savedTabId = browserManager.getActiveTabId();
1057
- // bringToFront: false — internal tab pinning must NOT steal window focus
1058
- try { browserManager.switchTab(tabId, { bringToFront: false }); } catch (err: any) {
1059
- console.warn('[browse] Failed to pin tab', tabId, ':', err.message);
1060
- }
1061
- }
1062
-
1063
- // ─── Tab ownership check (own-only tokens / pair-agent isolation) ──
1064
- //
1065
- // Only `own-only` tokens (pair-agent over tunnel) are bound to their own
1066
- // tabs. `shared` tokens — the default for skill spawns and local scoped
1067
- // clients — can drive any tab; the capability gate (scope checks above)
1068
- // and rate limits already constrain what they can do.
1069
- //
1070
- // Skip for `newtab` — it creates a tab rather than accessing one.
1071
- if (command !== 'newtab' && tokenInfo && tokenInfo.clientId !== 'root' && tokenInfo.tabPolicy === 'own-only') {
1072
- const targetTab = tabId ?? browserManager.getActiveTabId();
1073
- if (!browserManager.checkTabAccess(targetTab, tokenInfo.clientId, { isWrite: isWriteInvocation(command, args), ownOnly: true })) {
1074
- return {
1075
- status: 403, json: true,
1076
- result: JSON.stringify({
1077
- error: 'Tab not owned by your agent. Use newtab to create your own tab.',
1078
- hint: `Tab ${targetTab} is owned by ${browserManager.getTabOwner(targetTab) || 'root'}. Your agent: ${tokenInfo.clientId}.`,
1079
- }),
1080
- };
1081
- }
1082
- }
1083
-
1084
- // ─── newtab with ownership for scoped tokens ──────────────
1085
- if (command === 'newtab' && tokenInfo && tokenInfo.clientId !== 'root') {
1086
- const newId = await browserManager.newTab(args[0] || undefined, tokenInfo.clientId);
1087
- return {
1088
- status: 200, json: true,
1089
- result: JSON.stringify({
1090
- tabId: newId,
1091
- owner: tokenInfo.clientId,
1092
- hint: 'Include "tabId": ' + newId + ' in subsequent commands to target this tab.',
1093
- }),
1094
- };
1095
- }
1096
-
1097
- // Block mutation commands while watching (read-only observation mode).
1098
- // `--out` invocations count as mutations (they write the result to disk).
1099
- if (browserManager.isWatching() && isWriteInvocation(command, args)) {
1100
- return {
1101
- status: 400, json: true,
1102
- result: JSON.stringify({ error: 'Cannot run mutation commands while watching. Run `$B watch stop` first.' }),
1103
- };
1104
- }
1105
-
1106
- // Activity: emit command_start (skipped for chain subcommands)
1107
- const startTime = Date.now();
1108
- if (!opts?.skipActivity) {
1109
- emitActivity({
1110
- type: 'command_start',
1111
- command,
1112
- args,
1113
- url: browserManager.getCurrentUrl(),
1114
- tabs: browserManager.getTabCount(),
1115
- mode: browserManager.getConnectionMode(),
1116
- clientId: tokenInfo?.clientId,
1117
- });
1118
- }
1119
-
1120
- try {
1121
- let result: string;
1122
-
1123
- const session = browserManager.getActiveSession();
1124
-
1125
- // Per-request warnings collected during hidden-element detection,
1126
- // surfaced into the envelope the LLM sees. Carries across the read
1127
- // phase into the centralized wrap block below.
1128
- let hiddenContentWarnings: string[] = [];
1129
-
1130
- if (READ_COMMANDS.has(command)) {
1131
- const isScoped = tokenInfo && tokenInfo.clientId !== 'root';
1132
- // Hidden-element / ARIA-injection detection for every scoped
1133
- // DOM-reading channel (text, html, links, forms, accessibility,
1134
- // attrs, data, media, ux-audit). Previously only `text` received
1135
- // stripping; other channels let hidden injection payloads reach
1136
- // the LLM despite the envelope wrap. Detections become CONTENT
1137
- // WARNINGS on the outgoing envelope so the model can see what it
1138
- // would have otherwise trusted silently.
1139
- if (isScoped && DOM_CONTENT_COMMANDS.has(command)) {
1140
- const page = session.getPage();
1141
- try {
1142
- const strippedDescs = await markHiddenElements(page);
1143
- if (strippedDescs.length > 0) {
1144
- console.warn(`[browse] Content security: ${strippedDescs.length} hidden elements flagged on ${command} for ${tokenInfo.clientId}`);
1145
- hiddenContentWarnings = strippedDescs.slice(0, 8).map(d =>
1146
- `hidden content: ${d.slice(0, 120)}`,
1147
- );
1148
- if (strippedDescs.length > 8) {
1149
- hiddenContentWarnings.push(`hidden content: +${strippedDescs.length - 8} more flagged elements`);
1150
- }
1151
- }
1152
- if (command === 'text') {
1153
- const target = session.getActiveFrameOrPage();
1154
- result = await getCleanTextWithStripping(target);
1155
- } else {
1156
- result = await handleReadCommand(command, args, session, browserManager);
1157
- }
1158
- } finally {
1159
- await cleanupHiddenMarkers(page);
1160
- }
1161
- } else {
1162
- result = await handleReadCommand(command, args, session, browserManager);
1163
- }
1164
- } else if (WRITE_COMMANDS.has(command)) {
1165
- result = await browserManager.failIfNavigationBlocked(session.getPage(), handleWriteCommand(command, args, session, browserManager));
1166
- } else if (META_COMMANDS.has(command)) {
1167
- // Pass chain depth + executeCommand callback so chain routes subcommands
1168
- // through the full security pipeline (scope, domain, tab, wrapping).
1169
- const chainDepth = (opts?.chainDepth ?? 0);
1170
- // shutdown is factory-scoped (deleted from module scope in v1.35.0.0);
1171
- // route the call through activeShutdown which buildFetchHandler assigns.
1172
- const shutdownFn = () => activeShutdown ? activeShutdown() : Promise.resolve();
1173
- result = await handleMetaCommand(command, args, browserManager, shutdownFn, tokenInfo, {
1174
- chainDepth,
1175
- daemonPort: LOCAL_LISTEN_PORT,
1176
- executeCommand: (body, ti) => handleCommandInternal(body, ti, {
1177
- skipRateCheck: true, // chain counts as 1 request
1178
- skipActivity: true, // chain emits 1 event for all subcommands
1179
- chainDepth: chainDepth + 1, // recursion guard
1180
- }),
1181
- });
1182
- // Start periodic snapshot interval when watch mode begins
1183
- if (command === 'watch' && args[0] !== 'stop' && browserManager.isWatching()) {
1184
- const watchInterval = setInterval(async () => {
1185
- if (!browserManager.isWatching()) {
1186
- clearInterval(watchInterval);
1187
- return;
1188
- }
1189
- try {
1190
- const snapshot = await handleSnapshot(['-i'], browserManager.getActiveSession());
1191
- browserManager.addWatchSnapshot(snapshot);
1192
- } catch {
1193
- // Page may be navigating — skip this snapshot
1194
- }
1195
- }, 5000);
1196
- browserManager.watchInterval = watchInterval;
1197
- }
1198
- } else if (command === 'help') {
1199
- const helpText = generateHelpText();
1200
- return { status: 200, result: helpText };
1201
- } else {
1202
- // Use the rich unknown-command helper: names the input, suggests the closest
1203
- // match via Levenshtein (≤ 2 distance, ≥ 4 chars input), and appends an upgrade
1204
- // hint if the command is listed in NEW_IN_VERSION.
1205
- return {
1206
- status: 400, json: true,
1207
- result: JSON.stringify({
1208
- error: buildUnknownCommandError(rawCommand, ALL_COMMANDS),
1209
- hint: `Available commands: ${[...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS].sort().join(', ')}`,
1210
- }),
1211
- };
1212
- }
1213
-
1214
- // ─── Centralized content wrapping (single location for all commands) ───
1215
- // Scoped tokens: content filter + enhanced envelope + datamarking
1216
- // Root tokens: basic untrusted content wrapper (backward compat)
1217
- // Chain exempt from top-level wrapping (each subcommand wrapped individually)
1218
- if (PAGE_CONTENT_COMMANDS.has(command) && command !== 'chain') {
1219
- const isScoped = tokenInfo && tokenInfo.clientId !== 'root';
1220
- if (isScoped) {
1221
- // Run content filters
1222
- const filterResult: ContentFilterResult = runContentFilters(
1223
- result, browserManager.getCurrentUrl(), command,
1224
- );
1225
- if (filterResult.blocked) {
1226
- return { status: 403, json: true, result: JSON.stringify({ error: filterResult.message }) };
1227
- }
1228
- // Datamark text command output only (not html, forms, or structured data)
1229
- if (command === 'text') {
1230
- result = datamarkContent(result);
1231
- }
1232
- // Enhanced envelope wrapping for scoped tokens.
1233
- // Merge per-request hidden-element warnings with content-filter
1234
- // warnings so both reach the LLM through the same CONTENT
1235
- // WARNINGS header.
1236
- const combinedWarnings = [...filterResult.warnings, ...hiddenContentWarnings];
1237
- result = wrapUntrustedPageContent(
1238
- result, command,
1239
- combinedWarnings.length > 0 ? combinedWarnings : undefined,
1240
- );
1241
- } else {
1242
- // Root token: basic wrapping (backward compat, Decision 2)
1243
- result = wrapUntrustedContent(result, browserManager.getCurrentUrl());
1244
- }
1245
- }
1246
-
1247
- // Activity: emit command_end (skipped for chain subcommands)
1248
- const successDuration = Date.now() - startTime;
1249
- if (!opts?.skipActivity) {
1250
- emitActivity({
1251
- type: 'command_end',
1252
- command,
1253
- args,
1254
- url: browserManager.getCurrentUrl(),
1255
- duration: successDuration,
1256
- status: 'ok',
1257
- result: result,
1258
- tabs: browserManager.getTabCount(),
1259
- mode: browserManager.getConnectionMode(),
1260
- clientId: tokenInfo?.clientId,
1261
- });
1262
- }
1263
-
1264
- writeAuditEntry({
1265
- ts: new Date().toISOString(),
1266
- cmd: command,
1267
- aliasOf: isAliased ? rawCommand : undefined,
1268
- args: args.join(' '),
1269
- origin: browserManager.getCurrentUrl(),
1270
- durationMs: successDuration,
1271
- status: 'ok',
1272
- hasCookies: browserManager.hasCookieImports(),
1273
- mode: browserManager.getConnectionMode(),
1274
- });
1275
-
1276
- browserManager.resetFailures();
1277
- // Restore original active tab if we pinned to a specific one
1278
- if (savedTabId !== null) {
1279
- try { browserManager.switchTab(savedTabId, { bringToFront: false }); } catch (restoreErr: any) {
1280
- console.warn('[browse] Failed to restore tab after command:', restoreErr.message);
1281
- }
1282
- }
1283
- return { status: 200, result };
1284
- } catch (err: any) {
1285
- // Restore original active tab even on error
1286
- if (savedTabId !== null) {
1287
- try { browserManager.switchTab(savedTabId, { bringToFront: false }); } catch (restoreErr: any) {
1288
- console.warn('[browse] Failed to restore tab after error:', restoreErr.message);
1289
- }
1290
- }
1291
-
1292
- // Activity: emit command_end (error) — skipped for chain subcommands
1293
- const errorDuration = Date.now() - startTime;
1294
- if (!opts?.skipActivity) {
1295
- emitActivity({
1296
- type: 'command_end',
1297
- command,
1298
- args,
1299
- url: browserManager.getCurrentUrl(),
1300
- duration: errorDuration,
1301
- status: 'error',
1302
- error: err.message,
1303
- tabs: browserManager.getTabCount(),
1304
- mode: browserManager.getConnectionMode(),
1305
- clientId: tokenInfo?.clientId,
1306
- });
1307
- }
1308
-
1309
- writeAuditEntry({
1310
- ts: new Date().toISOString(),
1311
- cmd: command,
1312
- aliasOf: isAliased ? rawCommand : undefined,
1313
- args: args.join(' '),
1314
- origin: browserManager.getCurrentUrl(),
1315
- durationMs: errorDuration,
1316
- status: 'error',
1317
- error: err.message,
1318
- hasCookies: browserManager.hasCookieImports(),
1319
- mode: browserManager.getConnectionMode(),
1320
- });
1321
-
1322
- browserManager.incrementFailures();
1323
- let errorMsg = wrapError(err);
1324
- const hint = browserManager.getFailureHint();
1325
- if (hint) errorMsg += '\n' + hint;
1326
- return { status: 500, result: JSON.stringify({ error: errorMsg }), json: true };
1327
- }
1328
- }
1329
-
1330
- /**
1331
- * Sanitizing wrapper around handleCommandInternalImpl. ALL callers (single-command
1332
- * HTTP, batch loop, scoped-token dispatch) go through this so the lone-surrogate
1333
- * sanitization happens once at the architectural choke point, not per-leaf.
1334
- * Do not bypass this by calling handleCommandInternalImpl directly.
1335
- */
1336
- async function handleCommandInternal(
1337
- body: { command: string; args?: string[]; tabId?: number },
1338
- tokenInfo?: TokenInfo | null,
1339
- opts?: { skipRateCheck?: boolean; skipActivity?: boolean; chainDepth?: number },
1340
- ): Promise<CommandResult> {
1341
- const cr = await handleCommandInternalImpl(body, tokenInfo, opts);
1342
- return { ...cr, result: stripLoneSurrogates(cr.result) };
1343
- }
1344
-
1345
- /**
1346
- * Build the HTTP response from a CommandResult. Pure function so it can be
1347
- * unit-tested without spinning up the server (#1440). Defense in depth on top
1348
- * of handleCommandInternal's choke-point sanitization: this catches any
1349
- * \uXXXX JSON-escape surrogate forms that the raw-codepoint regex above
1350
- * misses when the body has already been JSON-stringified.
1351
- */
1352
- export function buildCommandResponse(cr: CommandResult): Response {
1353
- const contentType = cr.json ? 'application/json' : 'text/plain';
1354
- const safeBody = typeof cr.result === 'string' ? sanitizeBody(cr.result, !!cr.json) : cr.result;
1355
- return new Response(safeBody, {
1356
- status: cr.status,
1357
- headers: { 'Content-Type': contentType, ...cr.headers },
1358
- });
1359
- }
1360
-
1361
- /** HTTP wrapper — converts CommandResult to Response. Used by the /command
1362
- * route (routes/commands.ts, via RouteContext). The wrapper layer exists so
1363
- * `buildCommandResponse` is independently unit-testable (v1.38.1.0).
1364
- */
1365
- async function handleCommand(body: any, tokenInfo?: TokenInfo | null): Promise<Response> {
1366
- const cr = await handleCommandInternal(body, tokenInfo);
1367
- return buildCommandResponse(cr);
1368
- }
1369
-
1370
- // Module-level shutdown function deleted in v1.39.0.0; it now lives inside
1371
- // the buildFetchHandler closure so it closes the cfg-provided browserManager.
1372
- // Signal handlers below call activeShutdown which buildFetchHandler assigns.
1373
-
1374
- // Handle signals
1375
- //
1376
- // Node passes the signal name (e.g. 'SIGTERM') as the first arg to listeners.
1377
- // Wrap calls so activeShutdown receives no args — otherwise the string gets
1378
- // passed as exitCode and process.exit() coerces it to NaN, exiting with code 1
1379
- // instead of 0. (Caught in v0.18.1.0 #1025.)
1380
- //
1381
- // Gated on `import.meta.main` so embedders (gbrowser phoenix) that import
1382
- // server.ts as a submodule can register their own signal handlers without
1383
- // fighting with gstack's. CLI path is unchanged.
1384
- if (import.meta.main) {
1385
- // Standalone daemon: a Chromium crash must exit THIS process (its
1386
- // supervisor/user notices); embedders and in-process test launches must
1387
- // never be exited by browser-manager's disconnect handler.
1388
- markDaemonProcess();
1389
- // SIGINT (Ctrl+C): user intentionally stopping → shutdown.
1390
- process.on('SIGINT', () => activeShutdown?.());
1391
- // SIGHUP (terminal hangup): with handleSIGHUP:false at the three launch
1392
- // sites (#2220), Playwright no longer closes Chromium when this process
1393
- // gets hung up on — this handler is now the ONLY Chromium cleanup on
1394
- // SIGHUP (ENG-OV4). Route to the same shutdown path as SIGINT:
1395
- // activeShutdown closes the browser, releases ports, and removes the
1396
- // state file. Without it, a hangup would leak a live Chromium.
1397
- process.on('SIGHUP', () => activeShutdown?.());
1398
- // SIGTERM behavior depends on mode:
1399
- // - Normal (headless) mode: Claude Code's Bash sandbox fires SIGTERM when the
1400
- // parent shell exits between tool invocations. Ignoring it keeps the server
1401
- // alive across $B calls. Idle timeout (30 min) handles eventual cleanup.
1402
- // - Headed / tunnel mode: idle timeout doesn't apply in these modes. Respect
1403
- // SIGTERM so external tooling (systemd, supervisord, CI) can shut cleanly
1404
- // without waiting forever. Ctrl+C and /stop still work either way.
1405
- // - Active cookie picker: never tear down mid-import regardless of mode —
1406
- // would strand the picker UI with "Failed to fetch."
1407
- process.on('SIGTERM', () => {
1408
- if (hasActivePicker()) {
1409
- console.log('[browse] Received SIGTERM but cookie picker is active, ignoring to avoid stranding the picker UI');
1410
- return;
1411
- }
1412
- const headed = activeBrowserManager.getConnectionMode() === 'headed';
1413
- if (headed || tunnelActive) {
1414
- console.log(`[browse] Received SIGTERM in ${headed ? 'headed' : 'tunnel'} mode, shutting down`);
1415
- activeShutdown?.();
1416
- } else {
1417
- console.log('[browse] Received SIGTERM (ignoring — use /stop or Ctrl+C for intentional shutdown)');
1418
- }
1419
- });
1420
- // Windows: taskkill /F bypasses SIGTERM, but 'exit' fires for some shutdown paths.
1421
- // Defense-in-depth — primary cleanup is the CLI's stale-state detection via health check.
1422
- if (process.platform === 'win32') {
1423
- process.on('exit', removeOwnedDaemonStateQuiet);
1424
- }
1425
- }
1426
-
1427
- // Emergency cleanup for crashes (OOM, uncaught exceptions, browser disconnect)
1428
- function emergencyCleanup() {
1429
- if (isShuttingDown) return;
1430
- isShuttingDown = true;
1431
- // Xvfb cleanup MUST happen before state-file deletion. spawnXvfb detaches
1432
- // the child, so without this, an uncaught exception leaves the Xvfb
1433
- // running with no PID record — orphan accumulates and eventually
1434
- // exhausts the :99-:120 display range. Read the state file FIRST,
1435
- // call cleanupXvfb (validates cmdline + start-time before kill), THEN
1436
- // delete the state file.
1437
- try {
1438
- if (fs.existsSync(config.stateFile)) {
1439
- const raw = fs.readFileSync(config.stateFile, 'utf-8');
1440
- const state = JSON.parse(raw);
1441
- if (state.pid !== process.pid || state.instanceId !== SERVER_INSTANCE_ID) return;
1442
- if (state.xvfbPid && state.xvfbStartTime) {
1443
- // Lazy import — emergencyCleanup may run on platforms where
1444
- // ./xvfb's Linux-specific helpers fail to load. Best effort.
1445
- try {
1446
- const { cleanupXvfb } = require('./xvfb');
1447
- cleanupXvfb({
1448
- pid: state.xvfbPid,
1449
- startTime: state.xvfbStartTime,
1450
- display: state.xvfbDisplay || ':99',
1451
- });
1452
- } catch { /* best effort */ }
1453
- }
1454
- }
1455
- } catch { /* state file unparseable — fall through to lock + state cleanup */ }
1456
-
1457
- // Clean Chromium profile locks via the shared helper (defensive guard
1458
- // refuses to operate on unrecognized profile dirs).
1459
- if (activeBrowserManager.getConnectionMode() === 'headed' || process.env.BROWSE_HEADED === '1') {
1460
- cleanSingletonLocks(resolveChromiumProfile());
1461
- }
1462
- removeOwnedDaemonStateQuiet();
1463
- }
1464
- // Same import.meta.main gate as SIGINT/SIGTERM — embedders register their
1465
- // own crash handlers.
1466
- if (import.meta.main) {
1467
- process.on('uncaughtException', (err) => {
1468
- console.error('[browse] FATAL uncaught exception:', err.message);
1469
- emergencyCleanup();
1470
- process.exit(1);
1471
- });
1472
- process.on('unhandledRejection', (err: any) => {
1473
- console.error('[browse] FATAL unhandled rejection:', err?.message || err);
1474
- emergencyCleanup();
1475
- process.exit(1);
1476
- });
1477
- }
1478
-
1479
- // ─── Start ─────────────────────────────────────────────────────
1480
- /**
1481
- * Entry point for `bun run dev` and the compiled binary.
1482
- *
1483
- * Exported so embedders (gbrowser phoenix overlay) can call it
1484
- * directly with env vars set, bypassing the module-level `import.meta.main`
1485
- * gate. Phoenix's eventual fd-passing path will use `buildFetchHandler`
1486
- * directly; until that lands, calling `start()` from a non-main entry is
1487
- * supported via env (AUTH_TOKEN, BROWSE_PORT, BROWSE_OWN_SIGNALS).
1488
- */
1489
- /**
1490
- * Build a request handler set for the browse daemon. Embedders (gbrowser
1491
- * phoenix overlay) call this directly with their own cfg to compose overlay
1492
- * routes via cfg.beforeRoute, pass a pre-launched cfg.browserManager, and
1493
- * opt out of terminal-agent teardown via cfg.ownsTerminalAgent (default
1494
- * true, set to false when the embedder runs its own PTY server). The CLI
1495
- * path calls this through start() with env-derived defaults and explicit
1496
- * cfg.ownsTerminalAgent: true — externally-observable behavior is identical.
1497
- *
1498
- * Auth state lives ENTIRELY inside the factory closure: cfg.authToken is the
1499
- * single source of truth for the bearer secret, factory-scoped validateAuth
1500
- * closes over it, and factory-scoped shutdown closes the cfg-provided
1501
- * browserManager. Module-level lifecycle singletons (LOCAL_LISTEN_PORT,
1502
- * tunnelActive, inspector state) intentionally STAY at module scope; see
1503
- * the v1.35.0.0 CHANGELOG entry for the architectural rationale.
1504
- *
1505
- * The returned ServerHandle is callable directly. Bun.serve is the caller's
1506
- * responsibility — embedders may fd-pass; CLI uses Bun.serve normally.
1507
- */
1508
- export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
1509
- if (!cfg.authToken || cfg.authToken.length < 16) {
1510
- throw new Error('buildFetchHandler: cfg.authToken must be a non-empty string >= 16 chars');
1511
- }
1512
- if (!cfg.browserManager) {
1513
- throw new Error('buildFetchHandler: cfg.browserManager is required');
1514
- }
1515
-
1516
- // Re-run init with cfg-provided values. ensureStateDir is idempotent
1517
- // (mkdir -p); initAuditLog is idempotent (sets a module string);
1518
- // initRegistry is idempotent for same-token, throws for different-token.
1519
- // Owning init here (instead of at module load) means cfg.authToken is the
1520
- // single source of truth for the registry root token.
1521
- ensureStateDir(cfg.config);
1522
- initAuditLog(cfg.config.auditLog);
1523
- initRegistry(cfg.authToken);
1524
-
1525
- const { authToken, browserManager: cfgBrowserManager, startTime, beforeRoute, browsePort } = cfg;
1526
- // Strict opt-out: only explicit `false` flips the gate. Any other value
1527
- // (undefined, truthy non-bool from a JS caller bypassing TS, etc.) defaults
1528
- // to gstack-owns. Matches the "default-true preserves CLI bit-for-bit"
1529
- // premise even under malformed cfg.
1530
- const ownsTerminalAgent = cfg.ownsTerminalAgent === false ? false : true;
1531
-
1532
- // ─── Terminal-Agent Watchdog (v1.44+) ─────────────────────────────
1533
- //
1534
- // The terminal-agent process can die independently of the server: SIGKILL
1535
- // from the OS OOM killer, an uncaught exception under load, an external
1536
- // `pkill` from a sibling debugging session. Pre-v1.44 the sidebar would
1537
- // see the broken connection and stay broken until the user reloaded.
1538
- // Now: 60s ticker checks the recorded agent PID, respawns via the shared
1539
- // spawnTerminalAgent helper if dead.
1540
- //
1541
- // Identity-based — uses readAgentRecord + isProcessAlive, NOT a process
1542
- // name probe. Critical: prevents respawning around a slow-but-alive agent
1543
- // (which would create split-brain — two agents writing the port file,
1544
- // tokens diverging between them, mystery PTY upgrade failures).
1545
- //
1546
- // Crash-loop guard: 3 respawn attempts inside 60s → stop trying and emit
1547
- // a one-line error. Manual `forceRestart` from the sidebar clears the
1548
- // history (the user is the explicit signal to retry).
1549
- //
1550
- // Only active when ownsTerminalAgent === true. Embedders that pre-launch
1551
- // their own PTY server (gbrowser phoenix overlay) must not be auto-respawned
1552
- // by us — their lifecycle is their concern.
1553
- let agentWatchdogInterval: ReturnType<typeof setInterval> | null = null;
1554
- const respawnHistory: number[] = [];
1555
- const AGENT_WATCHDOG_TICK_MS = parseInt(
1556
- process.env.GSTACK_AGENT_WATCHDOG_TICK_MS || '60000',
1557
- 10,
1558
- );
1559
- const RESPAWN_GUARD_MAX = 3;
1560
- // The guard window MUST span enough ticks for RESPAWN_GUARD_MAX respawns to
1561
- // land inside it. This was a fixed 60_000 against a 60_000 tick, so at most
1562
- // ONE respawn could ever be in the window and `respawnHistory.length >= 3`
1563
- // was unreachable — the guard could not fire at the default tick rate, and a
1564
- // steady one-per-tick leak ran unbounded instead of stopping after 3. Scale
1565
- // with the tick so the intent ("3 crashes in quick succession → stop") holds
1566
- // at any tick value: 3 respawns within 5 ticks trips it.
1567
- const RESPAWN_GUARD_WINDOW_MS = Math.max(
1568
- 60_000,
1569
- AGENT_WATCHDOG_TICK_MS * (RESPAWN_GUARD_MAX + 2),
1570
- );
1571
- let agentRespawnGuardTripped = false;
1572
- let consecutiveSpawnFailures = 0;
1573
-
1574
- if (ownsTerminalAgent) {
1575
- agentWatchdogInterval = setInterval(() => {
1576
- if (isShuttingDown) return;
1577
- if (agentRespawnGuardTripped) return;
1578
- const stateDir = path.dirname(cfg.config.stateFile);
1579
- const record = readAgentRecord(stateDir);
1580
- // If the record exists and the PID is alive, the agent is healthy
1581
- // (or at least still answering signal 0). Slow-but-alive agents
1582
- // intentionally fall through here — split-brain is worse than
1583
- // unresponsiveness, and slow recovery is handled by the user via
1584
- // restart.
1585
- if (record && !isAgentRecordGone(record)) return;
1586
- // Either no record (never spawned, or cleaned up after crash) or
1587
- // PID is dead. Try to respawn.
1588
- const now = Date.now();
1589
- while (respawnHistory.length && now - respawnHistory[0] > RESPAWN_GUARD_WINDOW_MS) {
1590
- respawnHistory.shift();
1591
- }
1592
- if (respawnHistory.length >= RESPAWN_GUARD_MAX) {
1593
- agentRespawnGuardTripped = true;
1594
- console.error(
1595
- `[browse] terminal-agent respawn guard tripped (${RESPAWN_GUARD_MAX} crashes in ${RESPAWN_GUARD_WINDOW_MS / 1000}s) — manual restart required`,
1596
- );
1597
- return;
1598
- }
1599
- respawnHistory.push(now);
1600
- try {
1601
- const pid = spawnTerminalAgent({
1602
- stateFile: cfg.config.stateFile,
1603
- serverPort: cfg.browsePort,
1604
- ownerPid: process.pid,
1605
- cwd: cfg.config.projectDir,
1606
- });
1607
- if (pid) {
1608
- consecutiveSpawnFailures = 0;
1609
- console.log(`[browse] terminal-agent respawned by watchdog (PID: ${pid})`);
1610
- } else {
1611
- consecutiveSpawnFailures++;
1612
- console.warn('[browse] terminal-agent respawn skipped — script not found on disk');
1613
- }
1614
- } catch (err: any) {
1615
- consecutiveSpawnFailures++;
1616
- console.warn('[browse] terminal-agent respawn failed:', err?.message || err);
1617
- }
1618
- if (consecutiveSpawnFailures >= RESPAWN_GUARD_MAX) {
1619
- agentRespawnGuardTripped = true;
1620
- console.error('[browse] terminal-agent respawn guard tripped after repeated failed starts — manual restart required');
1621
- }
1622
- }, AGENT_WATCHDOG_TICK_MS);
1623
- // Detach the watchdog timer from Node's event-loop ref count so a
1624
- // healthy idle process can still exit cleanly if everything else is
1625
- // also unref'd. Bun's setInterval returns a Timer with unref().
1626
- (agentWatchdogInterval as any)?.unref?.();
1627
- }
1628
-
1629
- // Factory-scoped validateAuth. Closes over cfg.authToken so every internal
1630
- // auth check sees the same token the routes receive. Module-level
1631
- // validateAuth was deleted in v1.35.0.0.
1632
- function validateAuth(req: Request): boolean {
1633
- const header = req.headers.get('authorization');
1634
- if (header === null) return false;
1635
- // Constant-time compare so a byte-by-byte early-exit can't leak the token
1636
- // prefix via response timing. timingSafeEqual requires equal-length inputs,
1637
- // so the length check gates it (the length itself is not secret).
1638
- const got = Buffer.from(header);
1639
- const want = Buffer.from(`Bearer ${authToken}`);
1640
- return got.length === want.length && crypto.timingSafeEqual(got, want);
1641
- }
1642
-
1643
- // Factory-scoped shutdown. Closes the cfg-provided browserManager so
1644
- // embedders that pass their own BrowserManager get correct teardown.
1645
- // Module-level shutdown was deleted in v1.35.0.0.
1646
- async function shutdown(exitCode: number = 0) {
1647
- if (isShuttingDown) return;
1648
- isShuttingDown = true;
1649
- // State and terminal files belong to this instance, including embedders
1650
- // whose cfg differs from the standalone daemon's module-level config.
1651
- const config = cfg.config;
1652
- const stateOwner = (() => {
1653
- try { return JSON.parse(fs.readFileSync(config.stateFile, 'utf8')); } catch { return null; }
1654
- })();
1655
- const foreignState = Number.isSafeInteger(stateOwner?.pid) && stateOwner.pid > 0
1656
- && (stateOwner.pid !== process.pid || (stateOwner.instanceId && stateOwner.instanceId !== SERVER_INSTANCE_ID));
1657
-
1658
- console.log('[browse] Shutting down...');
1659
- if (ownsTerminalAgent && !foreignState) {
1660
- // Identity-based kill (v1.44+). Replaces the v1.43- `pkill -f
1661
- // terminal-agent\.ts` regex teardown which matched sibling gstack
1662
- // sessions on the same host. Only the PID recorded in
1663
- // `<stateDir>/terminal-agent-pid` by THIS daemon's agent is signaled.
1664
- try {
1665
- const stateDir = path.dirname(config.stateFile);
1666
- const releaseAgentLock = acquireAgentStateLock(stateDir);
1667
- try {
1668
- let currentState: { pid?: number; instanceId?: string } | null = null;
1669
- try { currentState = JSON.parse(fs.readFileSync(config.stateFile, 'utf8')); } catch {}
1670
- if (currentState?.pid && (currentState.pid !== process.pid
1671
- || (currentState.instanceId && currentState.instanceId !== SERVER_INSTANCE_ID))) {
1672
- console.warn('[browse] terminal-agent state now belongs to a successor; retaining its files');
1673
- } else {
1674
- const record = readAgentRecord(stateDir);
1675
- const agentStopped = !record || (record.pid !== 0
1676
- && (!isAgentRecordLive(record) || (isOurAgent(record, process.pid) && stopAgentByRecord(record))));
1677
- const current = readAgentRecord(stateDir);
1678
- if (agentStopped && (!record || (current?.pid === record.pid && current.gen === record.gen))) {
1679
- safeUnlinkQuiet(path.join(stateDir, 'terminal-port'));
1680
- safeUnlinkQuiet(path.join(stateDir, 'terminal-internal-token'));
1681
- if (record) clearAgentRecord(stateDir, record);
1682
- } else if (!agentStopped) {
1683
- console.warn('[browse] terminal-agent identity or exit could not be confirmed; retaining its record');
1684
- }
1685
- }
1686
- } finally { releaseAgentLock(); }
1687
- } catch (err: any) {
1688
- console.warn('[browse] Failed to stop terminal-agent; retaining its state:', err.message);
1689
- }
1690
- }
1691
- try { detachSession(); } catch (err: any) {
1692
- console.warn('[browse] Failed to detach CDP session:', err.message);
1693
- }
1694
- clearInspectorSubscribers();
1695
- if (cfgBrowserManager.isWatching()) cfgBrowserManager.stopWatch();
1696
- clearInterval(flushInterval);
1697
- clearInterval(idleCheckInterval);
1698
- if (agentWatchdogInterval) clearInterval(agentWatchdogInterval);
1699
- // Stop the session-persist ticker BEFORE the final snapshot below —
1700
- // paired with the isShuttingDown gate inside the tick, this guarantees
1701
- // no interval snapshot can race the final one during teardown.
1702
- if (sessionPersistInterval) {
1703
- clearInterval(sessionPersistInterval);
1704
- sessionPersistInterval = null;
1705
- }
1706
- await flushBuffers();
1707
-
1708
- // Final session snapshot before the browser goes away (#778). Best
1709
- // effort with a hard 2s deadline: shutdown must never hang on a wedged
1710
- // page.evaluate — after the deadline we proceed to browser close and let
1711
- // the previous interval snapshot stand (atomic writes guarantee it's
1712
- // intact). The .catch is attached to the persist promise itself so a
1713
- // late rejection after losing the race can't become an unhandled
1714
- // rejection.
1715
- if (isSessionPersistEnabled()) {
1716
- const finalSnapshot = persistSessionState(cfgBrowserManager, path.join(config.stateDir, SESSION_STATE_FILE))
1717
- .catch((err: any) => {
1718
- console.warn(`[browse] SESSION_PERSIST_FAILED at shutdown: ${err?.message ?? err}`);
1719
- });
1720
- await Promise.race([
1721
- finalSnapshot,
1722
- new Promise<void>((resolve) => setTimeout(resolve, 2_000)),
1723
- ]);
1724
- }
1725
-
1726
- await cfgBrowserManager.close();
1727
-
1728
- if (cfgBrowserManager.getConnectionMode() === 'headed') {
1729
- cleanSingletonLocks(resolveChromiumProfile());
1730
- }
1731
- if (!foreignState) {
1732
- try {
1733
- const releaseStateLock = acquireAgentStateLock(path.dirname(config.stateFile));
1734
- try {
1735
- const currentState = JSON.parse(fs.readFileSync(config.stateFile, 'utf8'));
1736
- if (currentState.pid === process.pid && currentState.instanceId === SERVER_INSTANCE_ID) safeUnlinkQuiet(config.stateFile);
1737
- } finally { releaseStateLock(); }
1738
- } catch (err: any) {
1739
- if (fs.existsSync(config.stateFile)) console.warn('[browse] Daemon state cleanup could not confirm ownership:', err?.message || err);
1740
- }
1741
- }
1742
- process.exit(exitCode);
1743
- }
1744
-
1745
- // Named lifecycle helper (matches closeTunnel style). Logs failures so
1746
- // future debugging isn't blind to a stuck listener.
1747
- async function stopListeners(local: any, tunnel?: any) {
1748
- try { if (local?.stop) local.stop(true); }
1749
- catch (err: any) { console.warn('[browse] local listener stop failed:', err?.message || err); }
1750
- try { if (tunnel?.stop) tunnel.stop(true); }
1751
- catch (err: any) { console.warn('[browse] tunnel listener stop failed:', err?.message || err); }
1752
- }
1753
-
1754
- // Register this handle's shutdown as the active one. Module-level
1755
- // handlers (idleCheckInterval, parent watchdog, onDisconnect, signal
1756
- // handlers) call activeShutdown so they reach THIS shutdown, not a stale
1757
- // module reference. Critical for embedders whose cfg.browserManager
1758
- // differs from the module-level instance.
1759
- activeShutdown = shutdown;
1760
-
1761
- // Retarget the BrowserManager indirection at the cfg-instance so the
1762
- // module-level idleCheckTick + parent watchdog + SIGTERM handler all read
1763
- // the right connectionMode. Without this, headed embedders auto-shutdown
1764
- // after 30 min of HTTP idle because the dead module-level instance still
1765
- // reports connectionMode === 'launched'.
1766
- activeBrowserManager = cfgBrowserManager;
1767
- // Same reason as above: the watchdog reads activeBrowserManager, so the
1768
- // instance that can promote itself to headed must be the one that can
1769
- // suppress the headed parent-death branch. An embedder-supplied manager
1770
- // otherwise promotes silently and the watchdog keeps shutting down on a
1771
- // promotion it can no longer see.
1772
- cfgBrowserManager.onHeadedPromotion = () => suppressHeadedParentShutdown(cfg.config, cfgBrowserManager);
1773
-
1774
- // Wire the cfg-instance's onDisconnect to run shutdown when the user
1775
- // closes the headed browser window. CHAIN any caller-provided handler
1776
- // instead of overwriting it: gbrowser may have set its own onDisconnect
1777
- // before calling buildFetchHandler (e.g. for snapshot/log work that needs
1778
- // to run before the process exits). Caller errors are logged but never
1779
- // block gstack shutdown — defensive symmetry with the safeUnlinkQuiet /
1780
- // safeKill philosophy in error-handling.ts.
1781
- const callerOnDisconnect = cfgBrowserManager.onDisconnect;
1782
- cfgBrowserManager.onDisconnect = async (code) => {
1783
- if (callerOnDisconnect) {
1784
- try { await callerOnDisconnect(code); }
1785
- catch (err: any) {
1786
- console.warn('[browse] caller onDisconnect threw:', err?.message ?? err);
1787
- }
1788
- }
1789
- await activeShutdown?.(code ?? 2);
1790
- };
1791
-
1792
- // Everything a route handler may use. Handlers get the cfg-provided
1793
- // BrowserManager and auth checks as functions; the raw token reaches only
1794
- // the two bootstrap routes that hand it on (see RouteContext).
1795
- const routeCtx: RouteContext = {
1796
- browserManager: cfgBrowserManager,
1797
- startTime,
1798
- browsePort,
1799
- validateAuth,
1800
- isRootRequest,
1801
- getTokenInfo,
1802
- hasSseCookie: (req) => validateSseSessionToken(extractSseCookie(req)),
1803
- isPinnedExtensionRequest,
1804
- isRootTokenValue: (token) => token !== null && token === authToken,
1805
- bootstrapRootToken: authToken,
1806
- resetIdleTimer,
1807
- terminal: { readPort: readTerminalPort, grantToken: grantPtyToken, restartSession: restartPtySession },
1808
- tunnel: {
1809
- state: () => ({ active: tunnelActive, url: tunnelUrl, hasListener: tunnelServer !== null }),
1810
- close: closeTunnel,
1811
- resolveAuthtoken: resolveNgrokAuthtoken,
1812
- start: (authtoken) => startTunnel({
1813
- fetchHandler: makeFetchHandler('tunnel'),
1814
- authtoken,
1815
- consent: 'pair_agent=on (isPairAgentEnabled gate at /tunnel/start)',
1816
- }),
1817
- },
1818
- commands: {
1819
- handle: handleCommand,
1820
- handleInternal: (body, tokenInfo, opts) => handleCommandInternal(body, tokenInfo, opts),
1821
- },
1822
- };
1823
-
1824
- const makeFetchHandler = (surface: Surface) => async (req: Request): Promise<Response> => {
1825
- const url = new URL(req.url);
1826
-
1827
- // ─── Tunnel surface filter (runs before any route dispatch) ──
1828
- if (surface === 'tunnel') {
1829
- const isGetConnect = req.method === 'GET' && url.pathname === '/connect';
1830
- const allowed = TUNNEL_PATHS.has(url.pathname);
1831
- if (!allowed && !isGetConnect) {
1832
- logTunnelDenial(req, url, 'path_not_on_tunnel');
1833
- return new Response(JSON.stringify({ error: 'Not found' }), {
1834
- status: 404, headers: { 'Content-Type': 'application/json' },
1835
- });
1836
- }
1837
- if (isRootRequest(req)) {
1838
- logTunnelDenial(req, url, 'root_token_on_tunnel');
1839
- return new Response(JSON.stringify({
1840
- error: 'Root token rejected on tunnel surface',
1841
- hint: 'Remote agents must pair via /connect to receive a scoped token.',
1842
- }), { status: 403, headers: { 'Content-Type': 'application/json' } });
1843
- }
1844
- if (url.pathname !== '/connect' && !getTokenInfo(req)) {
1845
- logTunnelDenial(req, url, 'missing_scoped_token');
1846
- return new Response(JSON.stringify({ error: 'Unauthorized' }), {
1847
- status: 401, headers: { 'Content-Type': 'application/json' },
1848
- });
1849
- }
1850
- }
1851
-
1852
- // beforeRoute overlay hook (v1.35.0.0). Runs AFTER the tunnel surface
1853
- // filter and BEFORE per-route dispatch. Pre-resolves bearer auth once
1854
- // so the hook receives TokenInfo | null. Note: getTokenInfo returns null
1855
- // for both missing AND invalid bearer — see the ServerConfig.beforeRoute
1856
- // JSDoc for the security implications.
1857
- if (beforeRoute) {
1858
- const auth = getTokenInfo(req);
1859
- const overlayResp = await beforeRoute(req, surface, auth);
1860
- if (overlayResp) return overlayResp;
1861
- }
1862
-
1863
- return dispatchRoute(ROUTES, req, url, surface, routeCtx);
1864
- };
1865
-
1866
- return {
1867
- fetchLocal: makeFetchHandler('local'),
1868
- fetchTunnel: makeFetchHandler('tunnel'),
1869
- shutdown,
1870
- stopListeners,
1871
- };
1872
- }
1873
-
1874
- export async function start() {
1875
- // Clear old log files
1876
- safeUnlink(CONSOLE_LOG_PATH);
1877
- safeUnlink(NETWORK_LOG_PATH);
1878
- safeUnlink(DIALOG_LOG_PATH);
1879
-
1880
- const port = await findPort();
1881
- LOCAL_LISTEN_PORT = port;
1882
-
1883
- // ─── Proxy config (D8 + codex F5) ──────────────────────────────
1884
- // The CLI resolves the proxy (--proxy or env, #915). SOCKS5 with auth runs a
1885
- // local 127.0.0.1 bridge to the authenticated upstream (Chromium can't do
1886
- // SOCKS5 auth itself); HTTP/HTTPS or unauthenticated SOCKS5 pass through.
1887
- let proxyBridge: BridgeHandle | null = null;
1888
- const { url: proxyUrl, source: proxySource, bypass: proxyBypass } = serverProxyEnv(process.env);
1889
- if (proxyUrl) {
1890
- let parsed;
1891
- try {
1892
- parsed = parseProxyConfig({
1893
- proxyUrl,
1894
- envUser: process.env.BROWSE_PROXY_USER,
1895
- envPass: process.env.BROWSE_PROXY_PASS, source: proxySource,
1896
- });
1897
- } catch (err) {
1898
- if (err instanceof ProxyConfigError) {
1899
- console.error(`[browse] error: ${err.message} (${err.hint})`);
1900
- process.exit(1);
1901
- }
1902
- throw err;
1903
- }
1904
-
1905
- if (parsed.scheme === 'socks5' && parsed.hasAuth) {
1906
- // Pre-flight: verify upstream accepts our creds before launching
1907
- // Chromium. 5s budget, 3 retries with 500ms backoff (D4: handles VPN
1908
- // warm-up race). On failure, exit with redacted error.
1909
- console.log(`[browse] Testing SOCKS5 upstream ${redactProxyUrl(proxyUrl)}...`);
1910
- try {
1911
- const test = await testUpstream({
1912
- upstream: toUpstreamConfig(parsed),
1913
- budgetMs: 5000,
1914
- retries: 3,
1915
- backoffMs: 500,
1916
- });
1917
- console.log(`[browse] [proxy] upstream test ok in ${test.ms}ms (${test.attempts} attempt${test.attempts === 1 ? '' : 's'})`);
1918
- } catch (err) {
1919
- const msg = err instanceof Error ? err.message : String(err);
1920
- console.error(`[browse] [proxy] FAIL upstream ${redactProxyUrl(proxyUrl)}: ${msg}`);
1921
- process.exit(1);
1922
- }
1923
-
1924
- proxyBridge = await startSocksBridge({ upstream: toUpstreamConfig(parsed) });
1925
- console.log(`[browse] [proxy] bridge listening on 127.0.0.1:${proxyBridge.port}`);
1926
- browserManager.setProxyConfig({ server: `socks5://127.0.0.1:${proxyBridge.port}`, bypass: proxyBypass });
1927
- } else {
1928
- // HTTP/HTTPS or unauth SOCKS5 — pass through to Chromium directly.
1929
- browserManager.setProxyConfig({
1930
- server: `${parsed.scheme}://${parsed.host}:${parsed.port}`, bypass: proxyBypass,
1931
- ...(parsed.userId ? { username: parsed.userId } : {}),
1932
- ...(parsed.password ? { password: parsed.password } : {}),
1933
- });
1934
- console.log(`[browse] [proxy] using ${redactProxyUrl(proxyUrl)} from ${proxySource} (pass-through to Chromium)`);
1935
- }
1936
-
1937
- // Tear down bridge on shutdown.
1938
- process.on('exit', () => {
1939
- if (proxyBridge) {
1940
- proxyBridge.close().catch(() => { /* shutting down anyway */ });
1941
- }
1942
- });
1943
- }
1944
-
1945
- process.on('exit', () => { browserManager.closeOwnedDisplay(); });
1946
-
1947
- // Read env once — single source of truth for authToken (and other env).
1948
- // Threaded through launchHeaded, buildFetchHandler, and the state file
1949
- // write so all consumers see the same value. v1.34.x's module-level
1950
- // AUTH_TOKEN const was deleted in v1.35.0.0.
1951
- const envCfg = resolveConfigFromEnv();
1952
-
1953
- // Launch browser (headless or headed with extension)
1954
- // BROWSE_HEADLESS_SKIP=1 skips browser launch entirely (for HTTP-only testing)
1955
- const skipBrowser = process.env.BROWSE_HEADLESS_SKIP === '1';
1956
- if (!skipBrowser) {
1957
- const headed = process.env.BROWSE_HEADED === '1';
1958
- if (headed) {
1959
- await browserManager.launchHeaded(envCfg.authToken);
1960
- console.log(`[browse] Launched headed Chromium with extension`);
1961
- } else {
1962
- await browserManager.launch();
1963
- }
1964
- }
1965
-
1966
- const startTime = Date.now();
1967
-
1968
- // ─── Build the request handlers via buildFetchHandler factory ───
1969
- // CLI path passes env-derived values; no beforeRoute hook. Phoenix uses
1970
- // the same factory with its own cfg + overlay hook.
1971
- const handle = buildFetchHandler({
1972
- ...envCfg,
1973
- browsePort: port, // actual bound port (resolveConfigFromEnv default is 0)
1974
- browserManager, // module-level instance, same as today
1975
- proxyBridge,
1976
- startTime,
1977
- ownsTerminalAgent: true, // CLI spawns terminal-agent.ts itself (see cli.ts:1037-1063)
1978
- });
1979
-
1980
- const server = Bun.serve({
1981
- port,
1982
- hostname: '127.0.0.1',
1983
- fetch: handle.fetchLocal,
1984
- });
1985
-
1986
- browserManager.serverPort = port;
1987
-
1988
- // Navigate to welcome page if in headed mode and still on about:blank
1989
- if (browserManager.getConnectionMode() === 'headed') {
1990
- try {
1991
- const currentUrl = browserManager.getCurrentUrl();
1992
- if (currentUrl === 'about:blank' || currentUrl === '') {
1993
- const page = browserManager.getPage();
1994
- await page.goto(`http://127.0.0.1:${port}/welcome`, { timeout: 3000 }).catch((err: any) => {
1995
- console.warn('[browse] Failed to navigate to welcome page:', err.message);
1996
- });
1997
- }
1998
- } catch (err: any) {
1999
- console.warn('[browse] Welcome page navigation setup failed:', err.message);
2000
- }
2001
- }
2002
-
2003
- if (isShuttingDown) return;
2004
-
2005
- // Write state file (atomic: write .tmp then rename)
2006
- const xvfb = browserManager.getXvfbHandle();
2007
- const state: Record<string, unknown> = {
2008
- pid: process.pid,
2009
- instanceId: SERVER_INSTANCE_ID,
2010
- port,
2011
- token: envCfg.authToken,
2012
- startedAt: new Date().toISOString(),
2013
- serverPath: path.resolve(import.meta.dir, 'server.ts'),
2014
- binaryVersion: readVersionHash() || undefined,
2015
- mode: browserManager.getConnectionMode(),
2016
- // D2 daemon-mismatch detection: CLI computes the same hash from its
2017
- // resolved flags and refuses if it differs from this stored value.
2018
- ...(process.env.BROWSE_CONFIG_HASH ? { configHash: process.env.BROWSE_CONFIG_HASH } : {}),
2019
- ...(proxyUrl ? { proxy: redactProxyUrl(proxyUrl), proxySource, proxyBypass } : {}), // #915 refusal detail
2020
- // Xvfb child PID + start-time + display so disconnect (or a future
2021
- // daemon launch on this state file) can validate-then-cleanup orphans
2022
- // without clobbering a recycled PID.
2023
- ...(xvfb ? { xvfbPid: xvfb.pid, xvfbStartTime: xvfb.startTime, xvfbDisplay: xvfb.display } : {}),
2024
- // #2709: launched-Chromium identity (pid + start time) so `browse stop`
2025
- // can reap a survivor — the headless launch has no SingletonLock for
2026
- // killOrphanChromium to walk, and on macOS 26 the orphaned GPU process
2027
- // kept spinning at ~800% CPU after the daemon exited.
2028
- ...(() => {
2029
- const info = browserManager.getChromiumProcInfo();
2030
- return info ? { chromiumPid: info.pid, chromiumStartTime: info.startTime } : {};
2031
- })(),
2032
- };
2033
- const tmpFile = tmpStatePath();
2034
- fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), { mode: 0o600 });
2035
- try {
2036
- const releaseStateLock = acquireAgentStateLock(config.stateDir);
2037
- try { fs.renameSync(tmpFile, config.stateFile); } finally { releaseStateLock(); }
2038
- } catch (err) {
2039
- safeUnlinkQuiet(tmpFile);
2040
- throw err;
2041
- }
2042
-
2043
- const stateWatchMs = parseInt(process.env.GSTACK_STATE_WATCH_MS || '60000', 10);
2044
- if (stateWatchMs > 0) {
2045
- let missed = 0;
2046
- const stateWatch = setInterval(() => {
2047
- let owner: { pid?: number; instanceId?: string } | null = null;
2048
- try { owner = JSON.parse(fs.readFileSync(config.stateFile, 'utf8')); } catch {}
2049
- if (owner?.pid === process.pid && owner.instanceId === SERVER_INSTANCE_ID) { missed = 0; return; }
2050
- if (++missed < 2) return;
2051
- clearInterval(stateWatch);
2052
- console.warn('[browse] daemon state is no longer reachable; shutting down this instance');
2053
- handle.shutdown();
2054
- }, stateWatchMs);
2055
- (stateWatch as any).unref?.();
2056
- }
2057
-
2058
- // ─── Opt-in session persistence (#778 class) ─────────────────
2059
- // BROWSE_PERSIST_STATE=1: restore cookies/storage/tabs from the last
2060
- // snapshot, then keep snapshotting on an interval. Launched mode only —
2061
- // the headed persistent profile owns its own state. The final snapshot at
2062
- // clean shutdown lives in buildFetchHandler's shutdown().
2063
- //
2064
- // Runs AFTER Bun.serve() + the state-file write, in the BACKGROUND:
2065
- // restore re-creates tabs sequentially with up-to-15s goto timeouts while
2066
- // the CLI's readiness probe gives up at 8s — one slow/unreachable saved
2067
- // URL must never make every `$B` command report "Server failed to start".
2068
- // Fire-and-forget: a restore failure is logged and never affects the
2069
- // daemon.
2070
- if (!skipBrowser && isSessionPersistEnabled() && browserManager.getConnectionMode() === 'launched') {
2071
- const sessionStatePath = path.join(config.stateDir, SESSION_STATE_FILE);
2072
- restoreSessionState(browserManager, sessionStatePath)
2073
- .then((restored) => {
2074
- if (restored) {
2075
- // Counts come from the deserialized snapshot itself — no extra
2076
- // saveState() round-trip against pages that may still be loading.
2077
- console.log(`[browse] Session state restored: ${restored.cookies.length} cookies / ${restored.pages.length} tabs (BROWSE_PERSIST_STATE=1)`);
2078
- } else {
2079
- console.log('[browse] Session persistence on; no prior state — fresh session (BROWSE_PERSIST_STATE=1)');
2080
- }
2081
- })
2082
- .catch((err: any) => {
2083
- console.warn(`[browse] SESSION_RESTORE_FAILED: ${err?.message ?? err}`);
2084
- });
2085
- let persistWarned = false;
2086
- // In-flight guard: never start a new snapshot while the previous one is
2087
- // still pending (a slow page.evaluate would otherwise pile up ticks).
2088
- let persistInFlight = false;
2089
- sessionPersistInterval = setInterval(() => {
2090
- // Shutdown gate (belt; shutdown()'s clearInterval is the suspenders):
2091
- // a tick that fires during browser teardown snapshots a degraded state
2092
- // (zero tabs) over the good final snapshot.
2093
- if (isShuttingDown) return;
2094
- if (persistInFlight) return; // skip the tick
2095
- persistInFlight = true;
2096
- persistSessionState(browserManager, sessionStatePath)
2097
- .catch((err: any) => {
2098
- // Warn once — a full disk must not spam the log every 30s, and a
2099
- // snapshot failure must never kill the daemon (R3).
2100
- if (!persistWarned) {
2101
- persistWarned = true;
2102
- console.warn(`[browse] SESSION_PERSIST_FAILED: ${err?.message ?? err} (further failures suppressed)`);
2103
- }
2104
- })
2105
- .finally(() => { persistInFlight = false; });
2106
- }, sessionPersistIntervalMs());
2107
- (sessionPersistInterval as any)?.unref?.();
2108
- }
2109
-
2110
- // Clean up stale state files (older than 7 days)
2111
- try {
2112
- const stateDir = path.join(config.stateDir, 'browse-states');
2113
- if (fs.existsSync(stateDir)) {
2114
- const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
2115
- for (const file of fs.readdirSync(stateDir)) {
2116
- const filePath = path.join(stateDir, file);
2117
- const stat = fs.statSync(filePath);
2118
- if (Date.now() - stat.mtimeMs > SEVEN_DAYS) {
2119
- fs.unlinkSync(filePath);
2120
- console.log(`[browse] Deleted stale state file: ${file}`);
2121
- }
2122
- }
2123
- }
2124
- } catch (err: any) {
2125
- console.warn('[browse] Failed to clean stale state files:', err.message);
2126
- }
2127
-
2128
- console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
2129
- console.log(`[browse] State file: ${config.stateFile}`);
2130
- console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1000}s`);
2131
-
2132
- // ─── Tunnel startup (optional) ────────────────────────────────
2133
- // Start ngrok tunnel if BROWSE_TUNNEL=1 is set. Uses the dual-listener
2134
- // pattern: bind a dedicated tunnel listener on an ephemeral port and
2135
- // point ngrok.forward() at IT, not the local daemon port.
2136
- if (process.env.BROWSE_TUNNEL === '1' && !isPairAgentEnabled()) {
2137
- console.error('[browse] BROWSE_TUNNEL=1 ignored: pair-agent is off. Enable once with: gstack-config set pair_agent on');
2138
- } else if (process.env.BROWSE_TUNNEL === '1') {
2139
- const authtoken = resolveNgrokAuthtoken();
2140
- if (!authtoken) {
2141
- console.error('[browse] BROWSE_TUNNEL=1 but no NGROK_AUTHTOKEN found. Set it via env var or ~/.gstack/ngrok.env');
2142
- } else {
2143
- // Shared startTunnel helper: binds the tunnel listener, opens ngrok,
2144
- // and on any failure tears down BOTH ngrok and the Bun listener so we
2145
- // don't leak an ngrok session if the error happened after
2146
- // ngrok.forward() resolved.
2147
- const started = await startTunnel({
2148
- fetchHandler: handle.fetchTunnel,
2149
- authtoken,
2150
- consent: 'pair_agent=on (isPairAgentEnabled gate, BROWSE_TUNNEL=1)',
2151
- });
2152
- if (!started.ok) {
2153
- console.error(`[browse] Failed to start tunnel: ${started.error.message}`);
2154
- }
2155
- }
2156
- } else if (process.env.BROWSE_TUNNEL_LOCAL_ONLY === '1') {
2157
- // Test-only: bind the dual-listener tunnel surface on 127.0.0.1 with NO
2158
- // ngrok forwarding. Lets paid evals exercise the surface==='tunnel' gate
2159
- // without an ngrok authtoken or live network. Production tunneling still
2160
- // requires BROWSE_TUNNEL=1 + a valid authtoken above.
2161
- try {
2162
- const boundTunnel = Bun.serve({
2163
- port: 0,
2164
- hostname: '127.0.0.1',
2165
- fetch: handle.fetchTunnel,
2166
- });
2167
- tunnelServer = boundTunnel;
2168
- tunnelActive = true;
2169
- const tunnelPort = boundTunnel.port;
2170
- console.log(`[browse] Tunnel listener bound (local-only test mode) on 127.0.0.1:${tunnelPort}`);
2171
- const releaseStateLock = acquireAgentStateLock(config.stateDir);
2172
- try {
2173
- const stateContent = JSON.parse(fs.readFileSync(config.stateFile, 'utf-8'));
2174
- if (stateContent.pid !== process.pid || stateContent.instanceId !== SERVER_INSTANCE_ID) throw new Error('daemon state was replaced');
2175
- stateContent.tunnelLocalPort = tunnelPort;
2176
- const tmpState = tmpStatePath();
2177
- fs.writeFileSync(tmpState, JSON.stringify(stateContent, null, 2), { mode: 0o600 });
2178
- fs.renameSync(tmpState, config.stateFile);
2179
- } finally { releaseStateLock(); }
2180
- } catch (err: any) {
2181
- console.error(`[browse] BROWSE_TUNNEL_LOCAL_ONLY=1 listener bind failed: ${err.message}`);
2182
- }
2183
- }
2184
- }
2185
-
2186
- /**
2187
- * Test-only. Resets the module-level shutdown latch so a second test case
2188
- * can exercise shutdown() in the same process. Mirrors __resetRegistry in
2189
- * token-registry.ts. shutdown() short-circuits when isShuttingDown is true
2190
- * (see line near the start of shutdown), so without this, tests that call
2191
- * shutdown() more than once silently no-op after the first call.
2192
- *
2193
- * DO NOT call from production code. Defeats the shutdown re-entry guard,
2194
- * which can race process.exit with cfgBrowserManager.close() and the pkill /
2195
- * safeUnlinkQuiet side effects. The `__` prefix is the convention; nothing
2196
- * enforces it. If you find yourself reaching for this outside a test file,
2197
- * the right fix is to make isShuttingDown factory-scoped instead.
2198
- */
2199
- export function __resetShuttingDown(): void {
2200
- isShuttingDown = false;
2201
- }
2202
-
2203
- // Auto-kickoff only when this module is the entry point. Embedders
2204
- // (gbrowser phoenix overlay) import { start, buildFetchHandler, ... }
2205
- // without triggering the listener-binding side effects.
2206
- if (import.meta.main) {
2207
- start().catch((err) => {
2208
- console.error(`[browse] Failed to start: ${err.message}`);
2209
- // Write error to disk for the CLI to read — on Windows, the CLI can't capture
2210
- // stderr because the server is launched with detached: true, stdio: 'ignore'.
2211
- try {
2212
- const errorLogPath = path.join(config.stateDir, 'browse-startup-error.log');
2213
- mkdirSecure(config.stateDir);
2214
- writeSecureFile(errorLogPath, `${new Date().toISOString()} ${err.message}\n${err.stack || ''}\n`);
2215
- } catch {
2216
- // stateDir may not exist — nothing more we can do
2217
- }
2218
- process.exit(1);
2219
- });
2220
- }