tabwright 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (599) hide show
  1. package/LICENSE +21 -0
  2. package/bin.js +4 -0
  3. package/dist/a11y-client.js +204 -0
  4. package/dist/agent-skills/tabwright/SKILL.md +119 -0
  5. package/dist/aria-snapshot.d.ts +262 -0
  6. package/dist/aria-snapshot.d.ts.map +1 -0
  7. package/dist/aria-snapshot.js +1256 -0
  8. package/dist/aria-snapshot.js.map +1 -0
  9. package/dist/aria-snapshot.test.d.ts +2 -0
  10. package/dist/aria-snapshot.test.d.ts.map +1 -0
  11. package/dist/aria-snapshot.test.js +270 -0
  12. package/dist/aria-snapshot.test.js.map +1 -0
  13. package/dist/aria-snapshot.unit.test.d.ts +2 -0
  14. package/dist/aria-snapshot.unit.test.d.ts.map +1 -0
  15. package/dist/aria-snapshot.unit.test.js +542 -0
  16. package/dist/aria-snapshot.unit.test.js.map +1 -0
  17. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
  18. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
  19. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
  20. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
  21. package/dist/bippy.js +894 -0
  22. package/dist/browser-config.d.ts +13 -0
  23. package/dist/browser-config.d.ts.map +1 -0
  24. package/dist/browser-config.js +126 -0
  25. package/dist/browser-config.js.map +1 -0
  26. package/dist/browser-install.d.ts +16 -0
  27. package/dist/browser-install.d.ts.map +1 -0
  28. package/dist/browser-install.js +238 -0
  29. package/dist/browser-install.js.map +1 -0
  30. package/dist/browser-launch.d.ts +17 -0
  31. package/dist/browser-launch.d.ts.map +1 -0
  32. package/dist/browser-launch.js +44 -0
  33. package/dist/browser-launch.js.map +1 -0
  34. package/dist/builtin-capabilities.d.ts +28 -0
  35. package/dist/builtin-capabilities.d.ts.map +1 -0
  36. package/dist/builtin-capabilities.js +2673 -0
  37. package/dist/builtin-capabilities.js.map +1 -0
  38. package/dist/capability-agent-skill.d.ts +60 -0
  39. package/dist/capability-agent-skill.d.ts.map +1 -0
  40. package/dist/capability-agent-skill.js +225 -0
  41. package/dist/capability-agent-skill.js.map +1 -0
  42. package/dist/capability-auth-state.d.ts +34 -0
  43. package/dist/capability-auth-state.d.ts.map +1 -0
  44. package/dist/capability-auth-state.js +157 -0
  45. package/dist/capability-auth-state.js.map +1 -0
  46. package/dist/capability-auth.d.ts +36 -0
  47. package/dist/capability-auth.d.ts.map +1 -0
  48. package/dist/capability-auth.js +198 -0
  49. package/dist/capability-auth.js.map +1 -0
  50. package/dist/capability-cli-confirmation.test.d.ts +2 -0
  51. package/dist/capability-cli-confirmation.test.d.ts.map +1 -0
  52. package/dist/capability-cli-confirmation.test.js +79 -0
  53. package/dist/capability-cli-confirmation.test.js.map +1 -0
  54. package/dist/capability-options.d.ts +20 -0
  55. package/dist/capability-options.d.ts.map +1 -0
  56. package/dist/capability-options.js +31 -0
  57. package/dist/capability-options.js.map +1 -0
  58. package/dist/capability-options.test.d.ts +2 -0
  59. package/dist/capability-options.test.d.ts.map +1 -0
  60. package/dist/capability-options.test.js +81 -0
  61. package/dist/capability-options.test.js.map +1 -0
  62. package/dist/capability-package.d.ts +27 -0
  63. package/dist/capability-package.d.ts.map +1 -0
  64. package/dist/capability-package.js +444 -0
  65. package/dist/capability-package.js.map +1 -0
  66. package/dist/capability-package.test.d.ts +2 -0
  67. package/dist/capability-package.test.d.ts.map +1 -0
  68. package/dist/capability-package.test.js +179 -0
  69. package/dist/capability-package.test.js.map +1 -0
  70. package/dist/capability-registry.d.ts +262 -0
  71. package/dist/capability-registry.d.ts.map +1 -0
  72. package/dist/capability-registry.js +931 -0
  73. package/dist/capability-registry.js.map +1 -0
  74. package/dist/capability-registry.test.d.ts +2 -0
  75. package/dist/capability-registry.test.d.ts.map +1 -0
  76. package/dist/capability-registry.test.js +1683 -0
  77. package/dist/capability-registry.test.js.map +1 -0
  78. package/dist/capability-runner.d.ts +99 -0
  79. package/dist/capability-runner.d.ts.map +1 -0
  80. package/dist/capability-runner.js +608 -0
  81. package/dist/capability-runner.js.map +1 -0
  82. package/dist/capability-search-relevance.test.d.ts +2 -0
  83. package/dist/capability-search-relevance.test.d.ts.map +1 -0
  84. package/dist/capability-search-relevance.test.js +67 -0
  85. package/dist/capability-search-relevance.test.js.map +1 -0
  86. package/dist/capability-studio.d.ts +11 -0
  87. package/dist/capability-studio.d.ts.map +1 -0
  88. package/dist/capability-studio.js +593 -0
  89. package/dist/capability-studio.js.map +1 -0
  90. package/dist/capability-studio.test.d.ts +2 -0
  91. package/dist/capability-studio.test.d.ts.map +1 -0
  92. package/dist/capability-studio.test.js +41 -0
  93. package/dist/capability-studio.test.js.map +1 -0
  94. package/dist/cdp-log.d.ts +19 -0
  95. package/dist/cdp-log.d.ts.map +1 -0
  96. package/dist/cdp-log.js +83 -0
  97. package/dist/cdp-log.js.map +1 -0
  98. package/dist/cdp-log.test.d.ts +2 -0
  99. package/dist/cdp-log.test.d.ts.map +1 -0
  100. package/dist/cdp-log.test.js +109 -0
  101. package/dist/cdp-log.test.js.map +1 -0
  102. package/dist/cdp-relay.d.ts +19 -0
  103. package/dist/cdp-relay.d.ts.map +1 -0
  104. package/dist/cdp-relay.js +2375 -0
  105. package/dist/cdp-relay.js.map +1 -0
  106. package/dist/cdp-session.d.ts +37 -0
  107. package/dist/cdp-session.d.ts.map +1 -0
  108. package/dist/cdp-session.js +36 -0
  109. package/dist/cdp-session.js.map +1 -0
  110. package/dist/cdp-types.d.ts +65 -0
  111. package/dist/cdp-types.d.ts.map +1 -0
  112. package/dist/cdp-types.js +91 -0
  113. package/dist/cdp-types.js.map +1 -0
  114. package/dist/channel-owner-inspect.test.d.ts +2 -0
  115. package/dist/channel-owner-inspect.test.d.ts.map +1 -0
  116. package/dist/channel-owner-inspect.test.js +75 -0
  117. package/dist/channel-owner-inspect.test.js.map +1 -0
  118. package/dist/chrome-discovery.d.ts +65 -0
  119. package/dist/chrome-discovery.d.ts.map +1 -0
  120. package/dist/chrome-discovery.js +173 -0
  121. package/dist/chrome-discovery.js.map +1 -0
  122. package/dist/chrome-discovery.test.d.ts +2 -0
  123. package/dist/chrome-discovery.test.d.ts.map +1 -0
  124. package/dist/chrome-discovery.test.js +41 -0
  125. package/dist/chrome-discovery.test.js.map +1 -0
  126. package/dist/clean-html.d.ts +11 -0
  127. package/dist/clean-html.d.ts.map +1 -0
  128. package/dist/clean-html.js +106 -0
  129. package/dist/clean-html.js.map +1 -0
  130. package/dist/cli-help.test.d.ts +2 -0
  131. package/dist/cli-help.test.d.ts.map +1 -0
  132. package/dist/cli-help.test.js +98 -0
  133. package/dist/cli-help.test.js.map +1 -0
  134. package/dist/cli.d.ts +3 -0
  135. package/dist/cli.d.ts.map +1 -0
  136. package/dist/cli.js +2709 -0
  137. package/dist/cli.js.map +1 -0
  138. package/dist/cloud-client.d.ts +56 -0
  139. package/dist/cloud-client.d.ts.map +1 -0
  140. package/dist/cloud-client.js +127 -0
  141. package/dist/cloud-client.js.map +1 -0
  142. package/dist/create-logger.d.ts +9 -0
  143. package/dist/create-logger.d.ts.map +1 -0
  144. package/dist/create-logger.js +27 -0
  145. package/dist/create-logger.js.map +1 -0
  146. package/dist/debugger-api.md +458 -0
  147. package/dist/debugger-examples-types.d.ts +24 -0
  148. package/dist/debugger-examples-types.d.ts.map +1 -0
  149. package/dist/debugger-examples-types.js +2 -0
  150. package/dist/debugger-examples-types.js.map +1 -0
  151. package/dist/debugger-examples.d.ts +6 -0
  152. package/dist/debugger-examples.d.ts.map +1 -0
  153. package/dist/debugger-examples.js +53 -0
  154. package/dist/debugger-examples.js.map +1 -0
  155. package/dist/debugger.d.ts +381 -0
  156. package/dist/debugger.d.ts.map +1 -0
  157. package/dist/debugger.js +630 -0
  158. package/dist/debugger.js.map +1 -0
  159. package/dist/diff-utils.d.ts +20 -0
  160. package/dist/diff-utils.d.ts.map +1 -0
  161. package/dist/diff-utils.js +50 -0
  162. package/dist/diff-utils.js.map +1 -0
  163. package/dist/diff-utils.test.d.ts +2 -0
  164. package/dist/diff-utils.test.d.ts.map +1 -0
  165. package/dist/diff-utils.test.js +137 -0
  166. package/dist/diff-utils.test.js.map +1 -0
  167. package/dist/doctor.d.ts +44 -0
  168. package/dist/doctor.d.ts.map +1 -0
  169. package/dist/doctor.js +246 -0
  170. package/dist/doctor.js.map +1 -0
  171. package/dist/doctor.test.d.ts +2 -0
  172. package/dist/doctor.test.d.ts.map +1 -0
  173. package/dist/doctor.test.js +208 -0
  174. package/dist/doctor.test.js.map +1 -0
  175. package/dist/editor-api.md +374 -0
  176. package/dist/editor-examples.d.ts +11 -0
  177. package/dist/editor-examples.d.ts.map +1 -0
  178. package/dist/editor-examples.js +124 -0
  179. package/dist/editor-examples.js.map +1 -0
  180. package/dist/editor.d.ts +203 -0
  181. package/dist/editor.d.ts.map +1 -0
  182. package/dist/editor.js +363 -0
  183. package/dist/editor.js.map +1 -0
  184. package/dist/env-compat.d.ts +2 -0
  185. package/dist/env-compat.d.ts.map +1 -0
  186. package/dist/env-compat.js +27 -0
  187. package/dist/env-compat.js.map +1 -0
  188. package/dist/executor.d.ts +229 -0
  189. package/dist/executor.d.ts.map +1 -0
  190. package/dist/executor.js +1465 -0
  191. package/dist/executor.js.map +1 -0
  192. package/dist/executor.unit.test.d.ts +2 -0
  193. package/dist/executor.unit.test.d.ts.map +1 -0
  194. package/dist/executor.unit.test.js +143 -0
  195. package/dist/executor.unit.test.js.map +1 -0
  196. package/dist/extension/_locales/en/messages.json +278 -0
  197. package/dist/extension/_locales/zh_CN/messages.json +256 -0
  198. package/dist/extension/assets/options-CYH96Lcx.css +1349 -0
  199. package/dist/extension/background.js +3994 -0
  200. package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
  201. package/dist/extension/icons/icon-black-128.png +0 -0
  202. package/dist/extension/icons/icon-black-16.png +0 -0
  203. package/dist/extension/icons/icon-black-32.png +0 -0
  204. package/dist/extension/icons/icon-black-48.png +0 -0
  205. package/dist/extension/icons/icon-gray-128.png +0 -0
  206. package/dist/extension/icons/icon-gray-16.png +0 -0
  207. package/dist/extension/icons/icon-gray-32.png +0 -0
  208. package/dist/extension/icons/icon-gray-48.png +0 -0
  209. package/dist/extension/icons/icon-green-128.png +0 -0
  210. package/dist/extension/icons/icon-green-16.png +0 -0
  211. package/dist/extension/icons/icon-green-32.png +0 -0
  212. package/dist/extension/icons/icon-green-48.png +0 -0
  213. package/dist/extension/icons/tabwright-icon-black.png +0 -0
  214. package/dist/extension/icons/tabwright-icon-gray-disabled.png +0 -0
  215. package/dist/extension/icons/tabwright-icon-gray-disabled.svg +1 -0
  216. package/dist/extension/manifest.json +54 -0
  217. package/dist/extension/options.js +16145 -0
  218. package/dist/extension/rrweb-recorder.js +12952 -0
  219. package/dist/extension/src/options.html +165 -0
  220. package/dist/extension/src/prism-bash.min.js +1 -0
  221. package/dist/extension/src/prism.min.js +1 -0
  222. package/dist/extension/src/welcome.html +396 -0
  223. package/dist/extension-connection.test.d.ts +2 -0
  224. package/dist/extension-connection.test.d.ts.map +1 -0
  225. package/dist/extension-connection.test.js +708 -0
  226. package/dist/extension-connection.test.js.map +1 -0
  227. package/dist/extension-icon-warning.test.d.ts +2 -0
  228. package/dist/extension-icon-warning.test.d.ts.map +1 -0
  229. package/dist/extension-icon-warning.test.js +33 -0
  230. package/dist/extension-icon-warning.test.js.map +1 -0
  231. package/dist/ghost-browser.d.ts +62 -0
  232. package/dist/ghost-browser.d.ts.map +1 -0
  233. package/dist/ghost-browser.js +107 -0
  234. package/dist/ghost-browser.js.map +1 -0
  235. package/dist/ghost-cursor-client.js +374 -0
  236. package/dist/ghost-cursor-controller.d.ts +46 -0
  237. package/dist/ghost-cursor-controller.d.ts.map +1 -0
  238. package/dist/ghost-cursor-controller.js +98 -0
  239. package/dist/ghost-cursor-controller.js.map +1 -0
  240. package/dist/ghost-cursor.d.ts +27 -0
  241. package/dist/ghost-cursor.d.ts.map +1 -0
  242. package/dist/ghost-cursor.js +79 -0
  243. package/dist/ghost-cursor.js.map +1 -0
  244. package/dist/htmlrewrite.d.ts +8 -0
  245. package/dist/htmlrewrite.d.ts.map +1 -0
  246. package/dist/htmlrewrite.js +339 -0
  247. package/dist/htmlrewrite.js.map +1 -0
  248. package/dist/htmlrewrite.test.d.ts +2 -0
  249. package/dist/htmlrewrite.test.d.ts.map +1 -0
  250. package/dist/htmlrewrite.test.js +7975 -0
  251. package/dist/htmlrewrite.test.js.map +1 -0
  252. package/dist/index.d.ts +20 -0
  253. package/dist/index.d.ts.map +1 -0
  254. package/dist/index.js +11 -0
  255. package/dist/index.js.map +1 -0
  256. package/dist/kill-port.d.ts +29 -0
  257. package/dist/kill-port.d.ts.map +1 -0
  258. package/dist/kill-port.js +190 -0
  259. package/dist/kill-port.js.map +1 -0
  260. package/dist/kitty-graphics.d.ts +22 -0
  261. package/dist/kitty-graphics.d.ts.map +1 -0
  262. package/dist/kitty-graphics.js +71 -0
  263. package/dist/kitty-graphics.js.map +1 -0
  264. package/dist/kitty-graphics.test.d.ts +2 -0
  265. package/dist/kitty-graphics.test.d.ts.map +1 -0
  266. package/dist/kitty-graphics.test.js +125 -0
  267. package/dist/kitty-graphics.test.js.map +1 -0
  268. package/dist/locator-selector.test.d.ts +2 -0
  269. package/dist/locator-selector.test.d.ts.map +1 -0
  270. package/dist/locator-selector.test.js +98 -0
  271. package/dist/locator-selector.test.js.map +1 -0
  272. package/dist/mcp-client.d.ts +20 -0
  273. package/dist/mcp-client.d.ts.map +1 -0
  274. package/dist/mcp-client.js +56 -0
  275. package/dist/mcp-client.js.map +1 -0
  276. package/dist/mcp.d.ts +6 -0
  277. package/dist/mcp.d.ts.map +1 -0
  278. package/dist/mcp.js +511 -0
  279. package/dist/mcp.js.map +1 -0
  280. package/dist/on-mouse-action.test.d.ts +2 -0
  281. package/dist/on-mouse-action.test.d.ts.map +1 -0
  282. package/dist/on-mouse-action.test.js +180 -0
  283. package/dist/on-mouse-action.test.js.map +1 -0
  284. package/dist/options-page.test.d.ts +2 -0
  285. package/dist/options-page.test.d.ts.map +1 -0
  286. package/dist/options-page.test.js +773 -0
  287. package/dist/options-page.test.js.map +1 -0
  288. package/dist/package-paths.d.ts +4 -0
  289. package/dist/package-paths.d.ts.map +1 -0
  290. package/dist/package-paths.js +30 -0
  291. package/dist/package-paths.js.map +1 -0
  292. package/dist/page-markdown.d.ts +40 -0
  293. package/dist/page-markdown.d.ts.map +1 -0
  294. package/dist/page-markdown.js +181 -0
  295. package/dist/page-markdown.js.map +1 -0
  296. package/dist/performance-examples.d.ts +5 -0
  297. package/dist/performance-examples.d.ts.map +1 -0
  298. package/dist/performance-examples.js +112 -0
  299. package/dist/performance-examples.js.map +1 -0
  300. package/dist/performance-profiling.md +417 -0
  301. package/dist/playwright-import.d.ts +19 -0
  302. package/dist/playwright-import.d.ts.map +1 -0
  303. package/dist/playwright-import.js +40 -0
  304. package/dist/playwright-import.js.map +1 -0
  305. package/dist/popup-relocation.test.d.ts +7 -0
  306. package/dist/popup-relocation.test.d.ts.map +1 -0
  307. package/dist/popup-relocation.test.js +139 -0
  308. package/dist/popup-relocation.test.js.map +1 -0
  309. package/dist/product-paths.d.ts +9 -0
  310. package/dist/product-paths.d.ts.map +1 -0
  311. package/dist/product-paths.js +24 -0
  312. package/dist/product-paths.js.map +1 -0
  313. package/dist/product-paths.test.d.ts +2 -0
  314. package/dist/product-paths.test.d.ts.map +1 -0
  315. package/dist/product-paths.test.js +39 -0
  316. package/dist/product-paths.test.js.map +1 -0
  317. package/dist/prompt.md +1004 -0
  318. package/dist/protocol.d.ts +248 -0
  319. package/dist/protocol.d.ts.map +1 -0
  320. package/dist/protocol.js +59 -0
  321. package/dist/protocol.js.map +1 -0
  322. package/dist/protocol.test.d.ts +2 -0
  323. package/dist/protocol.test.d.ts.map +1 -0
  324. package/dist/protocol.test.js +31 -0
  325. package/dist/protocol.test.js.map +1 -0
  326. package/dist/react-source.d.ts +57 -0
  327. package/dist/react-source.d.ts.map +1 -0
  328. package/dist/react-source.js +254 -0
  329. package/dist/react-source.js.map +1 -0
  330. package/dist/readability.js +1674 -0
  331. package/dist/relay-client.d.ts +104 -0
  332. package/dist/relay-client.d.ts.map +1 -0
  333. package/dist/relay-client.js +426 -0
  334. package/dist/relay-client.js.map +1 -0
  335. package/dist/relay-client.test.d.ts +2 -0
  336. package/dist/relay-client.test.d.ts.map +1 -0
  337. package/dist/relay-client.test.js +305 -0
  338. package/dist/relay-client.test.js.map +1 -0
  339. package/dist/relay-core.test.d.ts +2 -0
  340. package/dist/relay-core.test.d.ts.map +1 -0
  341. package/dist/relay-core.test.js +1427 -0
  342. package/dist/relay-core.test.js.map +1 -0
  343. package/dist/relay-navigation.test.d.ts +2 -0
  344. package/dist/relay-navigation.test.d.ts.map +1 -0
  345. package/dist/relay-navigation.test.js +661 -0
  346. package/dist/relay-navigation.test.js.map +1 -0
  347. package/dist/relay-session.test.d.ts +2 -0
  348. package/dist/relay-session.test.d.ts.map +1 -0
  349. package/dist/relay-session.test.js +1121 -0
  350. package/dist/relay-session.test.js.map +1 -0
  351. package/dist/relay-state.d.ts +178 -0
  352. package/dist/relay-state.d.ts.map +1 -0
  353. package/dist/relay-state.js +346 -0
  354. package/dist/relay-state.js.map +1 -0
  355. package/dist/relay-state.test.d.ts +2 -0
  356. package/dist/relay-state.test.d.ts.map +1 -0
  357. package/dist/relay-state.test.js +594 -0
  358. package/dist/relay-state.test.js.map +1 -0
  359. package/dist/replay-ai-index.d.ts +99 -0
  360. package/dist/replay-ai-index.d.ts.map +1 -0
  361. package/dist/replay-ai-index.js +677 -0
  362. package/dist/replay-ai-index.js.map +1 -0
  363. package/dist/replay-ai-index.test.d.ts +2 -0
  364. package/dist/replay-ai-index.test.d.ts.map +1 -0
  365. package/dist/replay-ai-index.test.js +255 -0
  366. package/dist/replay-ai-index.test.js.map +1 -0
  367. package/dist/replay-cli.test.d.ts +2 -0
  368. package/dist/replay-cli.test.d.ts.map +1 -0
  369. package/dist/replay-cli.test.js +275 -0
  370. package/dist/replay-cli.test.js.map +1 -0
  371. package/dist/replay-eval.d.ts +44 -0
  372. package/dist/replay-eval.d.ts.map +1 -0
  373. package/dist/replay-eval.js +902 -0
  374. package/dist/replay-eval.js.map +1 -0
  375. package/dist/replay-handoff.d.ts +29 -0
  376. package/dist/replay-handoff.d.ts.map +1 -0
  377. package/dist/replay-handoff.js +83 -0
  378. package/dist/replay-handoff.js.map +1 -0
  379. package/dist/replay-handoff.test.d.ts +2 -0
  380. package/dist/replay-handoff.test.d.ts.map +1 -0
  381. package/dist/replay-handoff.test.js +123 -0
  382. package/dist/replay-handoff.test.js.map +1 -0
  383. package/dist/replay-workflow-compiler.d.ts +44 -0
  384. package/dist/replay-workflow-compiler.d.ts.map +1 -0
  385. package/dist/replay-workflow-compiler.js +601 -0
  386. package/dist/replay-workflow-compiler.js.map +1 -0
  387. package/dist/replay-workflow-compiler.test.d.ts +2 -0
  388. package/dist/replay-workflow-compiler.test.d.ts.map +1 -0
  389. package/dist/replay-workflow-compiler.test.js +223 -0
  390. package/dist/replay-workflow-compiler.test.js.map +1 -0
  391. package/dist/rrweb-recording-relay.d.ts +56 -0
  392. package/dist/rrweb-recording-relay.d.ts.map +1 -0
  393. package/dist/rrweb-recording-relay.js +299 -0
  394. package/dist/rrweb-recording-relay.js.map +1 -0
  395. package/dist/rrweb-recording-relay.test.d.ts +2 -0
  396. package/dist/rrweb-recording-relay.test.d.ts.map +1 -0
  397. package/dist/rrweb-recording-relay.test.js +85 -0
  398. package/dist/rrweb-recording-relay.test.js.map +1 -0
  399. package/dist/rrweb-recording.d.ts +104 -0
  400. package/dist/rrweb-recording.d.ts.map +1 -0
  401. package/dist/rrweb-recording.js +180 -0
  402. package/dist/rrweb-recording.js.map +1 -0
  403. package/dist/scoped-fs.d.ts +95 -0
  404. package/dist/scoped-fs.d.ts.map +1 -0
  405. package/dist/scoped-fs.js +358 -0
  406. package/dist/scoped-fs.js.map +1 -0
  407. package/dist/scoped-fs.test.d.ts +5 -0
  408. package/dist/scoped-fs.test.d.ts.map +1 -0
  409. package/dist/scoped-fs.test.js +73 -0
  410. package/dist/scoped-fs.test.js.map +1 -0
  411. package/dist/selector-generator.js +7378 -0
  412. package/dist/skill-stub.test.d.ts +2 -0
  413. package/dist/skill-stub.test.d.ts.map +1 -0
  414. package/dist/skill-stub.test.js +22 -0
  415. package/dist/skill-stub.test.js.map +1 -0
  416. package/dist/snapshot-tools.test.d.ts +2 -0
  417. package/dist/snapshot-tools.test.d.ts.map +1 -0
  418. package/dist/snapshot-tools.test.js +856 -0
  419. package/dist/snapshot-tools.test.js.map +1 -0
  420. package/dist/start-relay-server.d.ts +6 -0
  421. package/dist/start-relay-server.d.ts.map +1 -0
  422. package/dist/start-relay-server.js +57 -0
  423. package/dist/start-relay-server.js.map +1 -0
  424. package/dist/styles-api.md +124 -0
  425. package/dist/styles-examples.d.ts +8 -0
  426. package/dist/styles-examples.d.ts.map +1 -0
  427. package/dist/styles-examples.js +64 -0
  428. package/dist/styles-examples.js.map +1 -0
  429. package/dist/styles.d.ts +27 -0
  430. package/dist/styles.d.ts.map +1 -0
  431. package/dist/styles.js +231 -0
  432. package/dist/styles.js.map +1 -0
  433. package/dist/tabwright-agent-skill.d.ts +30 -0
  434. package/dist/tabwright-agent-skill.d.ts.map +1 -0
  435. package/dist/tabwright-agent-skill.js +74 -0
  436. package/dist/tabwright-agent-skill.js.map +1 -0
  437. package/dist/tabwright-agent-skill.test.d.ts +2 -0
  438. package/dist/tabwright-agent-skill.test.d.ts.map +1 -0
  439. package/dist/tabwright-agent-skill.test.js +51 -0
  440. package/dist/tabwright-agent-skill.test.js.map +1 -0
  441. package/dist/test-declarations.d.ts +13 -0
  442. package/dist/test-declarations.d.ts.map +1 -0
  443. package/dist/test-declarations.js +2 -0
  444. package/dist/test-declarations.js.map +1 -0
  445. package/dist/test-utils.d.ts +69 -0
  446. package/dist/test-utils.d.ts.map +1 -0
  447. package/dist/test-utils.js +448 -0
  448. package/dist/test-utils.js.map +1 -0
  449. package/dist/test-utils.test.d.ts +2 -0
  450. package/dist/test-utils.test.d.ts.map +1 -0
  451. package/dist/test-utils.test.js +55 -0
  452. package/dist/test-utils.test.js.map +1 -0
  453. package/dist/utils.d.ts +26 -0
  454. package/dist/utils.d.ts.map +1 -0
  455. package/dist/utils.js +60 -0
  456. package/dist/utils.js.map +1 -0
  457. package/dist/wait-for-page-load.d.ts +16 -0
  458. package/dist/wait-for-page-load.d.ts.map +1 -0
  459. package/dist/wait-for-page-load.js +146 -0
  460. package/dist/wait-for-page-load.js.map +1 -0
  461. package/dist/workflow-capability.d.ts +72 -0
  462. package/dist/workflow-capability.d.ts.map +1 -0
  463. package/dist/workflow-capability.js +316 -0
  464. package/dist/workflow-capability.js.map +1 -0
  465. package/dist/workflow-script-flow.test.d.ts +2 -0
  466. package/dist/workflow-script-flow.test.d.ts.map +1 -0
  467. package/dist/workflow-script-flow.test.js +285 -0
  468. package/dist/workflow-script-flow.test.js.map +1 -0
  469. package/package.json +80 -0
  470. package/src/__snapshots__/x.com.processed.html +1003 -0
  471. package/src/__snapshots__/x.com.processed.withStyles.html +2720 -0
  472. package/src/a11y-client.ts +241 -0
  473. package/src/agent-skills/conan-config/SKILL.md +141 -0
  474. package/src/agent-skills/conan-config/agents/openai.yaml +4 -0
  475. package/src/aria-snapshot.test.ts +311 -0
  476. package/src/aria-snapshot.ts +1681 -0
  477. package/src/aria-snapshot.unit.test.ts +615 -0
  478. package/src/aria-snapshots/github-interactive.txt +193 -0
  479. package/src/aria-snapshots/github-raw.txt +309 -0
  480. package/src/aria-snapshots/hackernews-interactive.txt +477 -0
  481. package/src/aria-snapshots/hackernews-raw.txt +428 -0
  482. package/src/aria-snapshots/prosemirror-interactive.txt +66 -0
  483. package/src/aria-snapshots/prosemirror-raw.txt +145 -0
  484. package/src/assets/aria-labels-hacker-news.png +0 -0
  485. package/src/assets/aria-labels-old-reddit.png +0 -0
  486. package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
  487. package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
  488. package/src/assets/x.com.html +32946 -0
  489. package/src/browser-config.ts +179 -0
  490. package/src/browser-install.ts +284 -0
  491. package/src/browser-launch.ts +75 -0
  492. package/src/builtin-capabilities.ts +2765 -0
  493. package/src/capability-agent-skill.ts +305 -0
  494. package/src/capability-auth-state.ts +197 -0
  495. package/src/capability-auth.ts +249 -0
  496. package/src/capability-cli-confirmation.test.ts +90 -0
  497. package/src/capability-options.test.ts +89 -0
  498. package/src/capability-options.ts +57 -0
  499. package/src/capability-package.test.ts +204 -0
  500. package/src/capability-package.ts +555 -0
  501. package/src/capability-registry.test.ts +1905 -0
  502. package/src/capability-registry.ts +1234 -0
  503. package/src/capability-runner.ts +801 -0
  504. package/src/capability-search-relevance.test.ts +73 -0
  505. package/src/capability-studio.test.ts +45 -0
  506. package/src/capability-studio.ts +641 -0
  507. package/src/cdp-log.test.ts +131 -0
  508. package/src/cdp-log.ts +112 -0
  509. package/src/cdp-relay.ts +2944 -0
  510. package/src/cdp-session.ts +77 -0
  511. package/src/cdp-types.ts +158 -0
  512. package/src/channel-owner-inspect.test.ts +94 -0
  513. package/src/chrome-discovery.test.ts +50 -0
  514. package/src/chrome-discovery.ts +220 -0
  515. package/src/clean-html.ts +141 -0
  516. package/src/cli-help.test.ts +123 -0
  517. package/src/cli.ts +3181 -0
  518. package/src/cloud-client.ts +179 -0
  519. package/src/create-logger.ts +38 -0
  520. package/src/debugger-examples-types.ts +16 -0
  521. package/src/debugger-examples.ts +66 -0
  522. package/src/debugger.ts +708 -0
  523. package/src/diff-utils.test.ts +152 -0
  524. package/src/diff-utils.ts +70 -0
  525. package/src/doctor.test.ts +244 -0
  526. package/src/doctor.ts +321 -0
  527. package/src/editor-examples.ts +158 -0
  528. package/src/editor.ts +426 -0
  529. package/src/env-compat.ts +28 -0
  530. package/src/executor.ts +1824 -0
  531. package/src/executor.unit.test.ts +168 -0
  532. package/src/extension-connection.test.ts +855 -0
  533. package/src/extension-icon-warning.test.ts +45 -0
  534. package/src/ghost-browser.ts +149 -0
  535. package/src/ghost-cursor-client.ts +494 -0
  536. package/src/ghost-cursor-controller.ts +129 -0
  537. package/src/ghost-cursor.ts +123 -0
  538. package/src/htmlrewrite.test.ts +8008 -0
  539. package/src/htmlrewrite.ts +390 -0
  540. package/src/index.ts +53 -0
  541. package/src/kill-port.ts +219 -0
  542. package/src/kitty-graphics.test.ts +139 -0
  543. package/src/kitty-graphics.ts +79 -0
  544. package/src/locator-selector.test.ts +119 -0
  545. package/src/mcp-client.ts +78 -0
  546. package/src/mcp.ts +629 -0
  547. package/src/on-mouse-action.test.ts +226 -0
  548. package/src/options-page.test.ts +839 -0
  549. package/src/package-paths.ts +38 -0
  550. package/src/page-markdown.ts +240 -0
  551. package/src/performance-examples.ts +186 -0
  552. package/src/playwright-import.ts +59 -0
  553. package/src/popup-relocation.test.ts +163 -0
  554. package/src/product-paths.test.ts +47 -0
  555. package/src/product-paths.ts +28 -0
  556. package/src/protocol.test.ts +52 -0
  557. package/src/protocol.ts +351 -0
  558. package/src/react-source.ts +379 -0
  559. package/src/relay-client.test.ts +375 -0
  560. package/src/relay-client.ts +572 -0
  561. package/src/relay-core.test.ts +1603 -0
  562. package/src/relay-navigation.test.ts +790 -0
  563. package/src/relay-session.test.ts +1375 -0
  564. package/src/relay-state.test.ts +729 -0
  565. package/src/relay-state.ts +561 -0
  566. package/src/replay-ai-index.test.ts +278 -0
  567. package/src/replay-ai-index.ts +872 -0
  568. package/src/replay-cli.test.ts +325 -0
  569. package/src/replay-eval.ts +1039 -0
  570. package/src/replay-handoff.test.ts +163 -0
  571. package/src/replay-handoff.ts +109 -0
  572. package/src/replay-workflow-compiler.test.ts +250 -0
  573. package/src/replay-workflow-compiler.ts +700 -0
  574. package/src/resource.md +409 -0
  575. package/src/rrweb-recording-relay.test.ts +99 -0
  576. package/src/rrweb-recording-relay.ts +400 -0
  577. package/src/rrweb-recording.ts +322 -0
  578. package/src/scoped-fs.test.ts +80 -0
  579. package/src/scoped-fs.ts +420 -0
  580. package/src/skill-stub.test.ts +24 -0
  581. package/src/skill.md +1413 -0
  582. package/src/snapshot-tools.test.ts +1017 -0
  583. package/src/snapshots/hacker-news-accessibility-full.md +81 -0
  584. package/src/snapshots/hacker-news-accessibility-interactive.md +43 -0
  585. package/src/snapshots/page-markdown-output.txt +16 -0
  586. package/src/snapshots/shadcn-ui-accessibility-full.md +201 -0
  587. package/src/snapshots/shadcn-ui-accessibility-interactive.md +109 -0
  588. package/src/start-relay-server.ts +69 -0
  589. package/src/styles-examples.ts +84 -0
  590. package/src/styles.ts +343 -0
  591. package/src/tabwright-agent-skill.test.ts +61 -0
  592. package/src/tabwright-agent-skill.ts +105 -0
  593. package/src/test-declarations.ts +13 -0
  594. package/src/test-utils.test.ts +70 -0
  595. package/src/test-utils.ts +566 -0
  596. package/src/utils.ts +78 -0
  597. package/src/wait-for-page-load.ts +198 -0
  598. package/src/workflow-capability.ts +417 -0
  599. package/src/workflow-script-flow.test.ts +388 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2709 @@
1
+ #!/usr/bin/env node
2
+ import './env-compat.js';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import util from 'node:util';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { goke, openInBrowser } from 'goke';
8
+ import { z } from 'zod';
9
+ import pc from 'picocolors';
10
+ // Prevent Buffers from dumping hex bytes in util.inspect output.
11
+ Buffer.prototype[util.inspect.custom] = function () {
12
+ return `<Buffer ${this.length} bytes>`;
13
+ };
14
+ import { killPortProcess } from './kill-port.js';
15
+ import { canEmitKittyGraphics, emitKittyImage } from './kitty-graphics.js';
16
+ import { VERSION, LOG_FILE_PATH, LOG_CDP_FILE_PATH, parseRelayHost } from './utils.js';
17
+ import { ensureRelayServer, RELAY_PORT, waitForConnectedExtensions, getExtensionOutdatedWarning, getExtensionStatus, getExtensionsStatus, getLocalRelayHttpBaseUrl, getRelayServerFeatures, getRelayServerVersion, selectImplicitExtension, } from './relay-client.js';
18
+ import { discoverChromeInstances, resolveDirectInput } from './chrome-discovery.js';
19
+ import { getCloudClient, loadCloudAuth, saveCloudAuth, buildLiveUrl } from './cloud-client.js';
20
+ import { createCapability, getCapabilitySafetySummary, listCapabilities, readCapabilityScript, routeCapabilities, searchCapabilities, toCapabilityContract, toCapabilitySummary, updateCapabilityManifest, updateCapabilityScript, } from './capability-registry.js';
21
+ import { initCapabilityAgentSkill, installCapabilityAgentSkill, showCapabilityAgentSkill } from './capability-agent-skill.js';
22
+ import { getTabwrightAgentSkillStatus, installTabwrightAgentSkill, } from './tabwright-agent-skill.js';
23
+ import { installCapabilityPackage, packCapability } from './capability-package.js';
24
+ import { refreshCapabilityAuthWithExecutor } from './capability-auth.js';
25
+ import { finalizeCapabilityRun, normalizeCapabilityExecutionText, prepareCapabilityRun, readCapabilityExecutionObservation, runNodeCapability, } from './capability-runner.js';
26
+ import { installBuiltinCapabilitySuite, isBuiltinCapabilitySuite } from './builtin-capabilities.js';
27
+ import { createReplayAiIndexFromRecording, saveReplayAiIndex } from './replay-ai-index.js';
28
+ import { listSavedRrwebRecordings } from './rrweb-recording-relay.js';
29
+ import { compileReplayWorkflow, UnsupportedReplayWorkflowError, } from './replay-workflow-compiler.js';
30
+ import { buildReplayCreateCommand, buildReplayIndexCommand, buildReplayMakeCommand, buildReplayRunCommand, replayCapabilityId, toCompactReplayAiIndex, } from './replay-handoff.js';
31
+ import { formatReplayEvalReport, runReplayEval } from './replay-eval.js';
32
+ import { buildDoctorReport, formatDoctorReport } from './doctor.js';
33
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
+ const cli = goke('tabwright');
35
+ cli.on('command:*', () => {
36
+ const firstArg = cli.args[0];
37
+ if (!firstArg) {
38
+ return;
39
+ }
40
+ const hasMatchingCommandPrefix = cli.commands.some((command) => {
41
+ if (command.name === '') {
42
+ return false;
43
+ }
44
+ return command.name.split(' ')[0] === firstArg;
45
+ });
46
+ if (hasMatchingCommandPrefix) {
47
+ return;
48
+ }
49
+ console.error('Run "tabwright --help" for usage information.');
50
+ });
51
+ cli
52
+ .command('browser start [binaryPath]', 'Start Chromium or Chrome for Testing with the bundled Tabwright extension')
53
+ .hidden()
54
+ .option('--user-data-dir <dir>', 'Persistent browser profile directory used for the managed browser')
55
+ .option('--headless', 'Run the browser in headless mode')
56
+ .option('--headed', 'Force headed mode even on Linux without DISPLAY/WAYLAND_DISPLAY')
57
+ .option('--disable-sandbox', 'Disable the browser sandbox, useful on some VPS setups')
58
+ .action(async (binaryPath, options) => {
59
+ if (options.headless && options.headed) {
60
+ console.error('Error: --headless and --headed cannot be used together.');
61
+ process.exit(1);
62
+ }
63
+ try {
64
+ // Avoid loading playwright-core during generic CLI startup/help. This command
65
+ // is the only path that needs browser discovery and bundled extension launch.
66
+ const [{ getBrowserLaunchArgs, getDefaultBrowserUserDataDir, startBrowserProcess }, { resolveBrowserExecutablePath, shouldUseHeadlessByDefault }, { getBundledExtensionPath }] = await Promise.all([
67
+ import('./browser-launch.js'),
68
+ import('./browser-config.js'),
69
+ import('./package-paths.js'),
70
+ ]);
71
+ await ensureRelayServer({ logger: console });
72
+ const browserPath = resolveBrowserExecutablePath({ browserPath: binaryPath });
73
+ const extensionPath = getBundledExtensionPath();
74
+ const userDataDir = path.resolve(options.userDataDir || getDefaultBrowserUserDataDir());
75
+ const headless = options.headed ? false : options.headless ? true : shouldUseHeadlessByDefault();
76
+ const args = getBrowserLaunchArgs({
77
+ extensionPath,
78
+ userDataDir,
79
+ headless,
80
+ noSandbox: options.disableSandbox,
81
+ });
82
+ const { pid } = startBrowserProcess({
83
+ browserPath,
84
+ args,
85
+ userDataDir,
86
+ });
87
+ const connectedExtensions = await waitForConnectedExtensions({
88
+ timeoutMs: 15000,
89
+ pollIntervalMs: 250,
90
+ logger: console,
91
+ });
92
+ console.log(`Browser started (pid ${pid}).`);
93
+ console.log(` Binary: ${browserPath}`);
94
+ console.log(` Extension: ${extensionPath}`);
95
+ console.log(` Profile: ${userDataDir}`);
96
+ console.log(` Mode: ${headless ? 'headless' : 'headed'}`);
97
+ console.log(' Replay recording: rrweb DOM capture enabled');
98
+ if (connectedExtensions.length > 0) {
99
+ console.log('Tabwright extension connected to the relay server.');
100
+ return;
101
+ }
102
+ console.log('Browser started, but the extension has not connected yet.');
103
+ console.log(`Check logs at: ${LOG_FILE_PATH}`);
104
+ }
105
+ catch (error) {
106
+ console.error(`Error: ${error.message}`);
107
+ process.exit(1);
108
+ }
109
+ });
110
+ cli
111
+ .command('browser install', 'Download Chrome for Testing for headless browser automation')
112
+ .action(async () => {
113
+ try {
114
+ const { installChrome } = await import('./browser-install.js');
115
+ await installChrome();
116
+ }
117
+ catch (error) {
118
+ console.error(`Error: ${error.message}`);
119
+ process.exit(1);
120
+ }
121
+ });
122
+ cli
123
+ .command('', 'Start the MCP server or controls the browser with -e')
124
+ .option('--host <host>', 'Remote relay server host to connect to (or use TABWRIGHT_HOST env var)')
125
+ .option('--token <token>', 'Authentication token (or use TABWRIGHT_TOKEN env var)')
126
+ .option('-s, --session <name>', 'Session ID (required for -e, get one with `tabwright session new`)')
127
+ .option('-e, --eval <code>', 'Execute JavaScript code and exit, read https://playwriter.dev/SKILL.md for usage')
128
+ .option('-f, --file <path>', 'Execute JavaScript from a file and exit')
129
+ .option('--patchright', 'Use @playwriter/patchright-core for stealth mode (bypasses bot detection)')
130
+ .option('--timeout [ms]', z.number().default(10000).describe('Execution timeout in milliseconds'))
131
+ .action(async (options) => {
132
+ if (options.patchright) {
133
+ process.env.TABWRIGHT_PATCHRIGHT = '1';
134
+ }
135
+ if (options.eval && options.file) {
136
+ console.error('Error: -e and -f cannot be used together.');
137
+ process.exit(1);
138
+ }
139
+ // If -e or -f flag is provided, execute code via relay server
140
+ const code = (() => {
141
+ if (options.eval) {
142
+ return options.eval;
143
+ }
144
+ if (options.file) {
145
+ const filePath = path.resolve(options.file);
146
+ if (!fs.existsSync(filePath)) {
147
+ console.error(`Error: File not found: ${filePath}`);
148
+ process.exit(1);
149
+ }
150
+ return fs.readFileSync(filePath, 'utf-8');
151
+ }
152
+ return null;
153
+ })();
154
+ if (code) {
155
+ await executeCode({
156
+ code,
157
+ timeout: options.timeout || 10000,
158
+ sessionId: options.session,
159
+ host: options.host,
160
+ token: options.token,
161
+ });
162
+ return;
163
+ }
164
+ // Otherwise start the MCP server
165
+ // For direct CDP in MCP mode, use TABWRIGHT_DIRECT env var
166
+ const { startMcp } = await import('./mcp.js');
167
+ await startMcp({
168
+ host: options.host,
169
+ token: options.token,
170
+ });
171
+ });
172
+ async function getServerUrl(host) {
173
+ if (!host && !process.env.TABWRIGHT_HOST) {
174
+ return await getLocalRelayHttpBaseUrl(RELAY_PORT);
175
+ }
176
+ const serverHost = host || process.env.TABWRIGHT_HOST || '127.0.0.1';
177
+ const { httpBaseUrl } = parseRelayHost(serverHost, RELAY_PORT);
178
+ return httpBaseUrl;
179
+ }
180
+ // Centralized header builder so every CLI subcommand sends the token consistently.
181
+ // Falls back to TABWRIGHT_TOKEN env var when --token is not provided.
182
+ function buildAuthHeaders({ token, json }) {
183
+ const headers = {};
184
+ if (json) {
185
+ headers['Content-Type'] = 'application/json';
186
+ }
187
+ const effectiveToken = token || process.env.TABWRIGHT_TOKEN;
188
+ if (effectiveToken) {
189
+ headers['Authorization'] = `Bearer ${effectiveToken}`;
190
+ }
191
+ return headers;
192
+ }
193
+ async function fetchExtensionsStatus({ host, token } = {}) {
194
+ try {
195
+ const serverUrl = await getServerUrl(host);
196
+ const headers = buildAuthHeaders({ token });
197
+ const response = await fetch(`${serverUrl}/extensions/status`, {
198
+ signal: AbortSignal.timeout(2000),
199
+ headers,
200
+ });
201
+ if (!response.ok) {
202
+ const fallback = await fetch(`${serverUrl}/extension/status`, {
203
+ signal: AbortSignal.timeout(2000),
204
+ headers,
205
+ });
206
+ if (!fallback.ok) {
207
+ return [];
208
+ }
209
+ const fallbackData = (await fallback.json());
210
+ if (!fallbackData?.connected) {
211
+ return [];
212
+ }
213
+ return [
214
+ {
215
+ extensionId: 'default',
216
+ stableKey: undefined,
217
+ browser: fallbackData?.browser,
218
+ profile: fallbackData?.profile,
219
+ activeTargets: fallbackData?.activeTargets,
220
+ playwriterVersion: fallbackData?.playwriterVersion || null,
221
+ protocolVersion: fallbackData?.protocolVersion,
222
+ features: fallbackData?.features,
223
+ connectionHealth: fallbackData?.connectionHealth,
224
+ missingFeatures: fallbackData?.missingFeatures,
225
+ },
226
+ ];
227
+ }
228
+ const data = (await response.json());
229
+ return data?.extensions || [];
230
+ }
231
+ catch {
232
+ return [];
233
+ }
234
+ }
235
+ async function executeCode(options) {
236
+ const { code, timeout, host, token } = options;
237
+ const cwd = process.cwd();
238
+ const sessionId = options.sessionId ? String(options.sessionId) : process.env.TABWRIGHT_SESSION;
239
+ // Session is required
240
+ if (!sessionId) {
241
+ console.error('Error: -s/--session is required.');
242
+ console.error('Always run `tabwright session new` first to get a session ID to use.');
243
+ process.exit(1);
244
+ }
245
+ // Ensure relay server is running (only for local)
246
+ if (!host && !process.env.TABWRIGHT_HOST) {
247
+ const restarted = await ensureRelayServer({ logger: console });
248
+ if (restarted) {
249
+ const connectedExtensions = await waitForConnectedExtensions({
250
+ logger: console,
251
+ timeoutMs: 10000,
252
+ pollIntervalMs: 250,
253
+ });
254
+ if (connectedExtensions.length === 0) {
255
+ console.error('Warning: Extension not connected. Commands may fail.');
256
+ }
257
+ }
258
+ }
259
+ const serverUrl = await getServerUrl(host);
260
+ // Warn once if extension is outdated
261
+ const extensionStatus = await getExtensionStatus();
262
+ const outdatedWarning = getExtensionOutdatedWarning(extensionStatus?.playwriterVersion);
263
+ if (outdatedWarning) {
264
+ console.error(outdatedWarning);
265
+ }
266
+ // Build request URL with token if provided
267
+ const executeUrl = `${serverUrl}/cli/execute`;
268
+ try {
269
+ const response = await fetch(executeUrl, {
270
+ method: 'POST',
271
+ headers: buildAuthHeaders({ token, json: true }),
272
+ body: JSON.stringify({ sessionId, code, timeout, cwd }),
273
+ });
274
+ if (!response.ok) {
275
+ const text = await response.text();
276
+ console.error(`Error: ${response.status} ${text}`);
277
+ process.exit(1);
278
+ }
279
+ const result = (await response.json());
280
+ // Print output
281
+ if (result.text) {
282
+ if (result.isError) {
283
+ console.error(result.text);
284
+ }
285
+ else {
286
+ console.log(result.text);
287
+ }
288
+ }
289
+ // Emit images via Kitty Graphics Protocol when AGENT_GRAPHICS=kitty.
290
+ // Agents with kitty-graphics-agent intercept these escape sequences and pass
291
+ // the PNG images to the LLM as media parts — no extra tool call needed.
292
+ const kittyEnabled = canEmitKittyGraphics();
293
+ // Track emitted base64 to avoid duplicates (screenshots appear in both
294
+ // result.screenshots and result.images from the same screenshotCollector)
295
+ const emittedImages = new Set();
296
+ if (result.screenshots && result.screenshots.length > 0) {
297
+ for (const s of result.screenshots) {
298
+ if (kittyEnabled && s.base64) {
299
+ emitKittyImage({ base64: s.base64 });
300
+ emittedImages.add(s.base64);
301
+ }
302
+ console.log(`\nScreenshot saved to: ${s.path}`);
303
+ console.log(`Labels shown: ${s.labelCount}\n`);
304
+ console.log(`Accessibility snapshot:\n${s.snapshot}`);
305
+ }
306
+ }
307
+ // Emit resized images from resizeImageForAgent() calls that aren't
308
+ // already emitted as part of labeled screenshots
309
+ if (kittyEnabled && result.images && result.images.length > 0) {
310
+ for (const img of result.images) {
311
+ if (img.data && !emittedImages.has(img.data)) {
312
+ emitKittyImage({ base64: img.data });
313
+ emittedImages.add(img.data);
314
+ }
315
+ }
316
+ }
317
+ if (result.isCloud) {
318
+ console.error(pc.dim(`\nCloud session. Run \`tabwright session delete ${sessionId}\` when done.`));
319
+ }
320
+ if (result.isError) {
321
+ process.exit(1);
322
+ }
323
+ }
324
+ catch (error) {
325
+ if (error.cause?.code === 'ECONNREFUSED') {
326
+ console.error('Error: Cannot connect to relay server.');
327
+ console.error('The Tabwright relay server should start automatically. Check logs at:');
328
+ console.error(` ${LOG_FILE_PATH}`);
329
+ }
330
+ else {
331
+ console.error(`Error: ${error.message}`);
332
+ }
333
+ process.exit(1);
334
+ }
335
+ }
336
+ function parseCapabilityInput(options) {
337
+ const rawInput = options.inputJson || options.input || '{}';
338
+ try {
339
+ return JSON.parse(rawInput);
340
+ }
341
+ catch (error) {
342
+ throw new Error(`Invalid JSON input: ${rawInput}`, { cause: error });
343
+ }
344
+ }
345
+ async function requestCliExecute(options) {
346
+ if (!options.host && !process.env.TABWRIGHT_HOST) {
347
+ await ensureRelayServer({ logger: console });
348
+ }
349
+ const serverUrl = await getServerUrl(options.host);
350
+ const response = await fetch(`${serverUrl}/cli/execute`, {
351
+ method: 'POST',
352
+ headers: buildAuthHeaders({ token: options.token, json: true }),
353
+ body: JSON.stringify({
354
+ sessionId: options.sessionId,
355
+ code: options.code,
356
+ timeout: options.timeout,
357
+ cwd: process.cwd(),
358
+ includeStructuredResult: options.includeStructuredResult,
359
+ }),
360
+ });
361
+ if (!response.ok) {
362
+ const text = await response.text();
363
+ throw new Error(`Execute failed: ${response.status} ${text}`);
364
+ }
365
+ return (await response.json());
366
+ }
367
+ async function createCapabilityRunSession(options) {
368
+ const isLocal = !options.host && !process.env.TABWRIGHT_HOST;
369
+ await ensureRelayForSessionCreation(isLocal);
370
+ const serverUrl = await getServerUrl(options.host);
371
+ const body = (() => {
372
+ if (options.browser === 'headless') {
373
+ return { headless: true, cwd: process.cwd() };
374
+ }
375
+ if (options.browser === 'user') {
376
+ return { cwd: process.cwd() };
377
+ }
378
+ return { extensionId: options.browser, cwd: process.cwd() };
379
+ })();
380
+ const response = await fetch(`${serverUrl}/cli/session/new`, {
381
+ method: 'POST',
382
+ headers: buildAuthHeaders({ token: options.token, json: true }),
383
+ body: JSON.stringify(body),
384
+ });
385
+ if (!response.ok) {
386
+ const text = await response.text();
387
+ throw new Error(`Failed to create ${options.browser} session: ${response.status} ${text}`);
388
+ }
389
+ const result = (await response.json());
390
+ return { sessionId: result.id, autoCreated: true };
391
+ }
392
+ async function deleteCapabilityRunSession(options) {
393
+ const serverUrl = await getServerUrl(options.host);
394
+ await fetch(`${serverUrl}/cli/session/delete`, {
395
+ method: 'POST',
396
+ headers: buildAuthHeaders({ token: options.token, json: true }),
397
+ body: JSON.stringify({ sessionId: options.sessionId }),
398
+ }).catch(() => { });
399
+ }
400
+ function printCapabilityList(options) {
401
+ if (options.json) {
402
+ console.log(JSON.stringify(options.capabilities.map(toCapabilitySummary), null, 2));
403
+ return;
404
+ }
405
+ if (options.capabilities.length === 0) {
406
+ console.log('No capabilities found.');
407
+ return;
408
+ }
409
+ const idWidth = Math.max(2, ...options.capabilities.map((capability) => capability.manifest.id.length));
410
+ const statusWidth = Math.max(6, ...options.capabilities.map((capability) => capability.manifest.status.length));
411
+ console.log(`${'ID'.padEnd(idWidth)} ${'Status'.padEnd(statusWidth)} Runtime Location Title`);
412
+ console.log(`${'-'.repeat(idWidth)} ${'-'.repeat(statusWidth)} ------- -------- -----`);
413
+ options.capabilities.forEach((capability) => {
414
+ console.log(`${capability.manifest.id.padEnd(idWidth)} ${capability.manifest.status.padEnd(statusWidth)} ${capability.manifest.runtime.padEnd(7)} ${capability.location.padEnd(8)} ${capability.manifest.title}`);
415
+ });
416
+ }
417
+ function printCapabilitySearch(options) {
418
+ if (options.json) {
419
+ console.log(JSON.stringify(options.results.map((result) => {
420
+ return {
421
+ score: result.score,
422
+ reasons: result.reasons,
423
+ ...toCapabilityContract(result.capability),
424
+ };
425
+ }), null, 2));
426
+ return;
427
+ }
428
+ if (options.results.length === 0) {
429
+ console.log(`No capabilities matched: ${options.query}`);
430
+ return;
431
+ }
432
+ const lines = options.results.map((result) => {
433
+ const safety = getCapabilitySafetySummary(result.capability);
434
+ const autonomy = safety.sideEffect === 'mixed' ? 'mixed' : safety.requiresConfirmation ? 'confirm' : safety.sideEffect;
435
+ return `${result.capability.manifest.id} score=${result.score} ${result.capability.manifest.runtime}/${autonomy} ${result.capability.manifest.title}`;
436
+ });
437
+ console.log(lines.join('\n'));
438
+ }
439
+ function printCapabilityRoutes(options) {
440
+ const routeSummaries = options.routes.map((route) => {
441
+ return {
442
+ shellCommand: route.shellCommand,
443
+ command: route.command,
444
+ capabilityId: route.capability.manifest.id,
445
+ operation: route.operation,
446
+ id: route.capability.manifest.id,
447
+ title: route.capability.manifest.title,
448
+ location: route.capability.location,
449
+ routingHint: route.operation === undefined
450
+ ? route.capability.manifest.routingHint
451
+ : route.capability.manifest.operations[route.operation]?.routingHint,
452
+ input: route.input,
453
+ commandWarning: route.commandWarning,
454
+ executionHint: route.executionHint,
455
+ reasons: route.reasons,
456
+ matchedText: route.matchedText,
457
+ };
458
+ });
459
+ if (options.json) {
460
+ console.log(JSON.stringify(routeSummaries, null, 2));
461
+ return;
462
+ }
463
+ if (routeSummaries.length === 0) {
464
+ console.log(`No exact direct-run capability matched: ${options.task}`);
465
+ console.log(`Next: tabwright capability search ${quoteShell(options.task)}`);
466
+ return;
467
+ }
468
+ console.log(routeSummaries
469
+ .map((route) => {
470
+ return `run: ${route.shellCommand}\ncapabilityId: ${route.capabilityId}\nwarning: ${route.commandWarning}`;
471
+ })
472
+ .join('\n'));
473
+ }
474
+ function quoteShell(value) {
475
+ return `'${value.replace(/'/g, "'\\''")}'`;
476
+ }
477
+ function readCapabilityManifestPatchFromFile(filePath) {
478
+ const parsed = JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf-8'));
479
+ if (!isRecord(parsed)) {
480
+ throw new Error('Capability contract file must contain a JSON object');
481
+ }
482
+ return parsed;
483
+ }
484
+ function isRecord(value) {
485
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
486
+ }
487
+ async function refreshCapabilityAuthFromCli(id, options) {
488
+ const session = options.session || process.env.TABWRIGHT_SESSION;
489
+ const sessionInfo = session
490
+ ? { sessionId: session, autoCreated: false }
491
+ : await createCapabilityRunSession({
492
+ browser: options.browser || 'user',
493
+ host: options.host,
494
+ token: options.token,
495
+ });
496
+ try {
497
+ const result = await refreshCapabilityAuthWithExecutor({
498
+ id,
499
+ cwd: process.cwd(),
500
+ timeout: options.timeout || 10000,
501
+ executor: {
502
+ execute: (code, timeout, executeOptions) => {
503
+ return requestCliExecute({
504
+ code,
505
+ timeout: timeout || 10000,
506
+ sessionId: sessionInfo.sessionId,
507
+ host: options.host,
508
+ token: options.token,
509
+ includeStructuredResult: executeOptions?.includeStructuredResult,
510
+ });
511
+ },
512
+ },
513
+ });
514
+ if (options.json) {
515
+ console.log(JSON.stringify({
516
+ capability: result.capability.manifest.id,
517
+ saved: result.saved,
518
+ secretKey: result.secretKey,
519
+ cookieCount: result.cookieCount,
520
+ cookieNames: result.cookieNames,
521
+ urls: result.urls,
522
+ expiresAt: result.expiresAt,
523
+ path: result.path,
524
+ }, null, 2));
525
+ return;
526
+ }
527
+ console.log(`Refreshed ${result.capability.manifest.id} auth at ${result.path}`);
528
+ console.log(`Saved ${result.cookieCount} cookies into secret "${result.secretKey}".`);
529
+ if (result.expiresAt) {
530
+ console.log(`Expires at ${result.expiresAt}.`);
531
+ }
532
+ }
533
+ finally {
534
+ if (sessionInfo.autoCreated && !options.keepSession) {
535
+ await deleteCapabilityRunSession({
536
+ sessionId: sessionInfo.sessionId,
537
+ host: options.host,
538
+ token: options.token,
539
+ });
540
+ }
541
+ }
542
+ }
543
+ async function runCapabilityFromCli(id, options) {
544
+ const input = parseCapabilityInput({ input: options.input, inputJson: options.inputJson });
545
+ const prepared = prepareCapabilityRun({
546
+ id,
547
+ input,
548
+ cwd: process.cwd(),
549
+ force: options.force,
550
+ confirmation: options.confirm,
551
+ });
552
+ if (prepared.capability.manifest.runtime === 'node') {
553
+ const result = await runNodeCapability({
554
+ id,
555
+ input,
556
+ cwd: process.cwd(),
557
+ force: options.force,
558
+ confirmation: options.confirm,
559
+ timeout: options.timeout || 10000,
560
+ });
561
+ if (options.json) {
562
+ console.log(JSON.stringify({
563
+ capability: result.capability.manifest.id,
564
+ output: result.output,
565
+ text: result.text,
566
+ isError: result.isError,
567
+ }, null, 2));
568
+ return;
569
+ }
570
+ console.log(JSON.stringify(result.output, null, 2));
571
+ return;
572
+ }
573
+ const session = options.session || process.env.TABWRIGHT_SESSION;
574
+ const sessionInfo = session
575
+ ? { sessionId: session, autoCreated: false }
576
+ : await createCapabilityRunSession({
577
+ browser: options.browser || 'headless',
578
+ host: options.host,
579
+ token: options.token,
580
+ });
581
+ const start = Date.now();
582
+ try {
583
+ const result = await requestCliExecute({
584
+ code: prepared.code,
585
+ timeout: options.timeout || 10000,
586
+ sessionId: sessionInfo.sessionId,
587
+ host: options.host,
588
+ token: options.token,
589
+ includeStructuredResult: true,
590
+ }).catch((error) => {
591
+ finalizeCapabilityRun({
592
+ capability: prepared.capability,
593
+ operation: prepared.operation,
594
+ cwd: process.cwd(),
595
+ inputHash: prepared.inputHash,
596
+ startedAt: start,
597
+ execution: {
598
+ status: 'error',
599
+ output: undefined,
600
+ error: error instanceof Error ? error.message : String(error),
601
+ },
602
+ });
603
+ throw error;
604
+ });
605
+ const observation = readCapabilityExecutionObservation(result.structuredResult);
606
+ const isExecutionError = result.isError || Boolean(observation.error);
607
+ const normalizedText = normalizeCapabilityExecutionText({
608
+ text: result.text,
609
+ output: observation.output,
610
+ error: observation.error,
611
+ });
612
+ const finalized = finalizeCapabilityRun({
613
+ capability: prepared.capability,
614
+ operation: prepared.operation,
615
+ cwd: process.cwd(),
616
+ inputHash: prepared.inputHash,
617
+ startedAt: start,
618
+ execution: {
619
+ status: isExecutionError ? 'error' : 'success',
620
+ output: observation.output,
621
+ error: isExecutionError ? observation.error || normalizedText : undefined,
622
+ observedNetworkUrls: observation.observedNetworkUrls,
623
+ url: observation.url,
624
+ },
625
+ });
626
+ if (finalized.contractError) {
627
+ throw finalized.contractError;
628
+ }
629
+ if (options.json) {
630
+ console.log(JSON.stringify({
631
+ capability: prepared.capability.manifest.id,
632
+ output: observation.output,
633
+ text: normalizedText,
634
+ isError: isExecutionError,
635
+ }, null, 2));
636
+ }
637
+ else {
638
+ console.log(JSON.stringify(observation.output, null, 2));
639
+ if (isExecutionError) {
640
+ console.error(normalizedText);
641
+ }
642
+ }
643
+ if (isExecutionError) {
644
+ process.exit(1);
645
+ }
646
+ }
647
+ finally {
648
+ if (sessionInfo.autoCreated && !options.keepSession) {
649
+ await deleteCapabilityRunSession({
650
+ sessionId: sessionInfo.sessionId,
651
+ host: options.host,
652
+ token: options.token,
653
+ });
654
+ }
655
+ }
656
+ }
657
+ function exitWithError(error) {
658
+ const message = error instanceof Error ? error.message : String(error);
659
+ console.error(`Error: ${message}`);
660
+ process.exit(1);
661
+ }
662
+ function buildNestedExampleInput(pathValue) {
663
+ const parts = pathValue.split('.').filter((part) => {
664
+ return part.length > 0;
665
+ });
666
+ if (parts.length === 0) {
667
+ return { value: '...' };
668
+ }
669
+ return parts.reduceRight((current, part) => {
670
+ if (typeof current === 'string') {
671
+ return { [part]: current };
672
+ }
673
+ return { [part]: current };
674
+ }, '...');
675
+ }
676
+ function toReplayCompilerSummary(analysis) {
677
+ return {
678
+ supported: analysis.actionKind !== 'unknown',
679
+ ...analysis,
680
+ };
681
+ }
682
+ function buildReplayNeedsAiHandoff(options) {
683
+ const index = createReplayAiIndexFromRecording(options.replayId);
684
+ return {
685
+ status: 'needs_ai',
686
+ replay: {
687
+ id: options.replayId,
688
+ url: index.url,
689
+ },
690
+ capabilityWritten: false,
691
+ compiler: toReplayCompilerSummary(options.analysis),
692
+ evidence: toCompactReplayAiIndex(index),
693
+ next: {
694
+ action: 'author_capability',
695
+ inspectCommand: buildReplayIndexCommand({ replayId: options.replayId, full: true }),
696
+ createCommand: buildReplayCreateCommand({
697
+ capabilityId: options.capabilityId,
698
+ title: `Workflow from replay ${options.replayId}`,
699
+ description: options.goal,
700
+ }),
701
+ },
702
+ };
703
+ }
704
+ function printReplayNeedsAiHandoff(options) {
705
+ if (options.json) {
706
+ console.log(JSON.stringify(options.handoff, null, 2));
707
+ return;
708
+ }
709
+ console.log(`Replay ${options.handoff.replay.id} needs AI authoring.`);
710
+ console.log('No capability was written.');
711
+ options.handoff.compiler.reasons.forEach((reason) => {
712
+ console.log(`- ${reason}`);
713
+ });
714
+ console.log(`Inspect full evidence: ${options.handoff.next.inspectCommand}`);
715
+ console.log(`Create a browser scaffold: ${options.handoff.next.createCommand}`);
716
+ }
717
+ cli
718
+ .command('replay list', 'List saved rrweb replays and the next commands for each recording')
719
+ .option('--limit <n>', z.number().default(10).describe('Maximum number of recordings'))
720
+ .option('--json', 'Print JSON')
721
+ .action((options) => {
722
+ try {
723
+ const recordings = listSavedRrwebRecordings({ limit: options.limit || 10 }).map((recording) => {
724
+ return {
725
+ id: recording.id,
726
+ url: recording.url,
727
+ savedAt: recording.savedAt,
728
+ durationMs: recording.duration,
729
+ eventCount: recording.eventCount,
730
+ commands: {
731
+ inspect: buildReplayIndexCommand({ replayId: recording.id }),
732
+ make: buildReplayMakeCommand({
733
+ replayId: recording.id,
734
+ capabilityId: replayCapabilityId(recording.id),
735
+ }),
736
+ },
737
+ };
738
+ });
739
+ if (options.json) {
740
+ console.log(JSON.stringify({
741
+ recordings,
742
+ next: recordings.length > 0
743
+ ? recordings[0]?.commands
744
+ : { action: 'record', command: 'Use replay.start() and replay.stop() in a Tabwright session.' },
745
+ }, null, 2));
746
+ return;
747
+ }
748
+ if (recordings.length === 0) {
749
+ console.log('No saved replays. Use replay.start() and replay.stop() in a Tabwright session first.');
750
+ return;
751
+ }
752
+ recordings.forEach((recording) => {
753
+ console.log(`${recording.id} ${recording.url || '-'} ${recording.eventCount} events`);
754
+ console.log(` Inspect: ${recording.commands.inspect}`);
755
+ console.log(` Make: ${recording.commands.make}`);
756
+ });
757
+ }
758
+ catch (error) {
759
+ exitWithError(error);
760
+ }
761
+ });
762
+ cli
763
+ .command('replay index <replayId>', 'Build an AI-readable index from an rrweb replay')
764
+ .option('--write', 'Save the index under ~/.tabwright/replay-ai-indexes')
765
+ .option('--full', 'Include page text and the full interactive-element inventory')
766
+ .option('--json', 'Print JSON')
767
+ .action((replayId, options) => {
768
+ try {
769
+ const index = createReplayAiIndexFromRecording(replayId);
770
+ const saved = options.write ? saveReplayAiIndex(index) : undefined;
771
+ if (options.json) {
772
+ const outputIndex = options.full ? index : toCompactReplayAiIndex(index);
773
+ console.log(JSON.stringify(saved ? { index: outputIndex, saved } : { index: outputIndex }, null, 2));
774
+ return;
775
+ }
776
+ console.log(`Replay: ${index.replayId}`);
777
+ console.log(`URL: ${index.url || '-'}`);
778
+ console.log(`Actions: ${index.actions.length}`);
779
+ console.log(`Fields: ${index.fields.length}`);
780
+ console.log(`Annotations: ${index.annotations.length}`);
781
+ console.log(`Stats: ${JSON.stringify(index.stats)}`);
782
+ if (saved) {
783
+ console.log(`Saved: ${saved.path}`);
784
+ }
785
+ index.annotations.slice(0, 8).forEach((annotation, index) => {
786
+ const target = annotation.target?.label || annotation.target?.selectorHints[0] || annotation.target?.tagName || 'target';
787
+ console.log(`${index + 1}. ${pc.magenta('annotation')} ${target}: ${annotation.text}`);
788
+ });
789
+ index.actions.slice(0, 12).forEach((action, index) => {
790
+ const value = action.value === undefined ? '' : ` = ${JSON.stringify(action.value)}`;
791
+ console.log(`${index + 1}. ${pc.cyan(action.kind)} ${action.label}${value}`);
792
+ });
793
+ }
794
+ catch (error) {
795
+ exitWithError(error);
796
+ }
797
+ });
798
+ cli
799
+ .command('replay compile <replayId> <capabilityId>', 'Compile an rrweb replay into a draft workflow capability')
800
+ .option('--title <title>', 'Capability title')
801
+ .option('--description <description>', 'Capability description')
802
+ .option('--goal <goal>', 'User goal to store as capability description')
803
+ .option('--value-input-path <path>', 'Input path for the appended value (default: value)')
804
+ .option('--force', 'Overwrite an existing capability')
805
+ .option('--json', 'Print JSON')
806
+ .action((replayId, capabilityId, options) => {
807
+ try {
808
+ const valueInputPath = options.valueInputPath || 'value';
809
+ const compiled = compileReplayWorkflow({
810
+ replayId,
811
+ id: capabilityId,
812
+ cwd: process.cwd(),
813
+ title: options.title,
814
+ description: options.description || options.goal,
815
+ valueInputPath,
816
+ overwrite: options.force,
817
+ });
818
+ const runCommand = buildReplayRunCommand({
819
+ capabilityId,
820
+ input: buildNestedExampleInput(valueInputPath),
821
+ });
822
+ if (options.json) {
823
+ console.log(JSON.stringify({
824
+ status: 'compiled',
825
+ replay: {
826
+ id: replayId,
827
+ url: compiled.analysis.url,
828
+ },
829
+ compiler: toReplayCompilerSummary(compiled.analysis),
830
+ capability: compiled.saved.capability,
831
+ next: {
832
+ requiresUserConfirmation: true,
833
+ runCommand,
834
+ },
835
+ }, null, 2));
836
+ return;
837
+ }
838
+ console.log(`Compiled ${replayId} into ${capabilityId}`);
839
+ console.log(`Action: ${compiled.analysis.actionKind}`);
840
+ console.log(`Confidence: ${compiled.analysis.confidence}`);
841
+ console.log(`Observed value: ${compiled.analysis.demonstratedValue || '-'}`);
842
+ console.log('Requires explicit user confirmation before running.');
843
+ console.log(`After approval: ${runCommand}`);
844
+ }
845
+ catch (error) {
846
+ if (error instanceof UnsupportedReplayWorkflowError) {
847
+ printReplayNeedsAiHandoff({
848
+ handoff: buildReplayNeedsAiHandoff({
849
+ replayId,
850
+ capabilityId,
851
+ goal: options.description || options.goal,
852
+ analysis: error.analysis,
853
+ }),
854
+ json: options.json,
855
+ });
856
+ return;
857
+ }
858
+ exitWithError(error);
859
+ }
860
+ });
861
+ cli
862
+ .command('replay make <replayId> <capabilityId>', 'Index and compile a replay into a runnable draft capability')
863
+ .option('--title <title>', 'Capability title')
864
+ .option('--description <description>', 'Capability description')
865
+ .option('--goal <goal>', 'User goal to store as capability description')
866
+ .option('--value-input-path <path>', 'Input path for the appended value (default: value)')
867
+ .option('--write-index', 'Save the generated replay index under ~/.tabwright/replay-ai-indexes')
868
+ .option('--force', 'Overwrite an existing capability')
869
+ .option('--json', 'Print JSON')
870
+ .action((replayId, capabilityId, options) => {
871
+ try {
872
+ const valueInputPath = options.valueInputPath || 'value';
873
+ const index = createReplayAiIndexFromRecording(replayId);
874
+ const savedIndex = options.writeIndex ? saveReplayAiIndex(index) : undefined;
875
+ const compiled = compileReplayWorkflow({
876
+ replayId,
877
+ id: capabilityId,
878
+ cwd: process.cwd(),
879
+ title: options.title,
880
+ description: options.description || options.goal,
881
+ valueInputPath,
882
+ overwrite: options.force,
883
+ });
884
+ const runCommand = buildReplayRunCommand({
885
+ capabilityId,
886
+ input: buildNestedExampleInput(valueInputPath),
887
+ });
888
+ if (options.json) {
889
+ console.log(JSON.stringify({
890
+ status: 'compiled',
891
+ replay: {
892
+ id: replayId,
893
+ url: index.url,
894
+ },
895
+ savedIndex,
896
+ compiler: toReplayCompilerSummary(compiled.analysis),
897
+ evidence: toCompactReplayAiIndex(index),
898
+ capability: compiled.saved.capability,
899
+ next: {
900
+ requiresUserConfirmation: true,
901
+ runCommand,
902
+ },
903
+ }, null, 2));
904
+ return;
905
+ }
906
+ console.log(`Replay: ${index.replayId}`);
907
+ console.log(`URL: ${index.url || '-'}`);
908
+ console.log(`Actions: ${index.actions.length}`);
909
+ console.log(`Fields: ${index.fields.length}`);
910
+ console.log(`Annotations: ${index.annotations.length}`);
911
+ if (savedIndex) {
912
+ console.log(`Saved index: ${savedIndex.path}`);
913
+ }
914
+ console.log(`Compiled capability: ${capabilityId}`);
915
+ console.log(`Action: ${compiled.analysis.actionKind}`);
916
+ console.log(`Confidence: ${compiled.analysis.confidence}`);
917
+ console.log(`Observed value: ${compiled.analysis.demonstratedValue || '-'}`);
918
+ console.log('Requires explicit user confirmation before running.');
919
+ console.log(`After approval: ${runCommand}`);
920
+ }
921
+ catch (error) {
922
+ if (error instanceof UnsupportedReplayWorkflowError) {
923
+ printReplayNeedsAiHandoff({
924
+ handoff: buildReplayNeedsAiHandoff({
925
+ replayId,
926
+ capabilityId,
927
+ goal: options.description || options.goal,
928
+ analysis: error.analysis,
929
+ }),
930
+ json: options.json,
931
+ });
932
+ return;
933
+ }
934
+ exitWithError(error);
935
+ }
936
+ });
937
+ cli
938
+ .command('replay eval', 'Run replay-to-capability evaluation cases against local example pages')
939
+ .option('--case <id>', 'Run one evaluation case')
940
+ .option('--json', 'Print JSON')
941
+ .option('--report <path>', 'Write an HTML report')
942
+ .option('--keep-artifacts', 'Keep generated temporary recordings and capabilities')
943
+ .option('--headed', 'Run the evaluation browser in headed mode')
944
+ .action(async (options) => {
945
+ try {
946
+ const report = await runReplayEval({
947
+ caseId: options.case,
948
+ reportPath: options.report,
949
+ keepArtifacts: options.keepArtifacts,
950
+ headed: options.headed,
951
+ });
952
+ if (options.json) {
953
+ console.log(JSON.stringify(report, null, 2));
954
+ return;
955
+ }
956
+ console.log(formatReplayEvalReport(report));
957
+ if (options.report) {
958
+ console.log(`Report: ${path.resolve(options.report)}`);
959
+ }
960
+ if (report.failed > 0) {
961
+ process.exitCode = 1;
962
+ }
963
+ }
964
+ catch (error) {
965
+ exitWithError(error);
966
+ }
967
+ });
968
+ cli
969
+ .command('capability list', 'List saved Tabwright capabilities')
970
+ .option('--json', 'Print JSON')
971
+ .action((options) => {
972
+ try {
973
+ printCapabilityList({ capabilities: listCapabilities({ cwd: process.cwd() }), json: options.json });
974
+ }
975
+ catch (error) {
976
+ exitWithError(error);
977
+ }
978
+ });
979
+ cli
980
+ .command('capability route <task>', 'Find an exact-match direct-run capability for a concrete task or URL')
981
+ .option('--limit <n>', z.number().default(3).describe('Maximum number of routes'))
982
+ .option('--json', 'Print JSON')
983
+ .action((task, options) => {
984
+ try {
985
+ printCapabilityRoutes({
986
+ task,
987
+ routes: routeCapabilities({ task, cwd: process.cwd(), limit: options.limit || 3 }),
988
+ json: options.json,
989
+ });
990
+ }
991
+ catch (error) {
992
+ exitWithError(error);
993
+ }
994
+ });
995
+ cli
996
+ .command('capability search <query>', 'Search saved Tabwright capabilities by user intent')
997
+ .option('--limit <n>', z.number().default(10).describe('Maximum number of results'))
998
+ .option('--json', 'Print JSON')
999
+ .action((query, options) => {
1000
+ try {
1001
+ printCapabilitySearch({
1002
+ query,
1003
+ results: searchCapabilities({ query, cwd: process.cwd(), limit: options.limit || 10 }),
1004
+ json: options.json,
1005
+ });
1006
+ }
1007
+ catch (error) {
1008
+ exitWithError(error);
1009
+ }
1010
+ });
1011
+ cli
1012
+ .command('capability describe <id>', 'Print the AI-readable contract for a capability')
1013
+ .option('--json', 'Print JSON')
1014
+ .action((id, options) => {
1015
+ try {
1016
+ const capability = listCapabilities({ cwd: process.cwd() }).find((candidate) => {
1017
+ return candidate.manifest.id === id;
1018
+ });
1019
+ if (!capability) {
1020
+ throw new Error(`Capability not found: ${id}`);
1021
+ }
1022
+ const contract = toCapabilityContract(capability);
1023
+ if (options.json) {
1024
+ console.log(JSON.stringify(contract, null, 2));
1025
+ return;
1026
+ }
1027
+ console.log(JSON.stringify(contract, null, 2));
1028
+ }
1029
+ catch (error) {
1030
+ exitWithError(error);
1031
+ }
1032
+ });
1033
+ cli
1034
+ .command('capability show <id>', 'Show a saved Tabwright capability')
1035
+ .option('--json', 'Print JSON')
1036
+ .option('--script', 'Print the script source')
1037
+ .action((id, options) => {
1038
+ try {
1039
+ const capability = listCapabilities({ cwd: process.cwd() }).find((candidate) => {
1040
+ return candidate.manifest.id === id;
1041
+ });
1042
+ if (!capability) {
1043
+ throw new Error(`Capability not found: ${id}`);
1044
+ }
1045
+ if (options.script) {
1046
+ console.log(readCapabilityScript({ id, cwd: process.cwd() }));
1047
+ return;
1048
+ }
1049
+ if (options.json) {
1050
+ console.log(JSON.stringify(toCapabilitySummary(capability), null, 2));
1051
+ return;
1052
+ }
1053
+ console.log(`${capability.manifest.title} (${capability.manifest.id})`);
1054
+ console.log(`Status: ${capability.manifest.status}`);
1055
+ console.log(`Runtime: ${capability.manifest.runtime}`);
1056
+ console.log(`Location: ${capability.location}`);
1057
+ console.log(`Directory: ${capability.dir}`);
1058
+ if (capability.manifest.description) {
1059
+ console.log(`Description: ${capability.manifest.description}`);
1060
+ }
1061
+ console.log(`Match: ${capability.manifest.match.length > 0 ? capability.manifest.match.join(', ') : '-'}`);
1062
+ console.log(`Permissions: ${capability.manifest.permissions.join(', ') || '-'}`);
1063
+ console.log(`Entry: ${capability.manifest.entry}`);
1064
+ }
1065
+ catch (error) {
1066
+ exitWithError(error);
1067
+ }
1068
+ });
1069
+ cli
1070
+ .command('capability create <id>', 'Create a draft Tabwright capability')
1071
+ .option('--title <title>', 'Capability title')
1072
+ .option('--description <description>', 'Capability description')
1073
+ .option('--project', 'Create under .tabwright/capabilities in the current project')
1074
+ .option('--runtime <browser|node>', 'Capability runtime (default: browser)')
1075
+ .option('--force', 'Overwrite an existing capability')
1076
+ .option('--json', 'Print JSON')
1077
+ .action((id, options) => {
1078
+ try {
1079
+ if (options.runtime && options.runtime !== 'browser' && options.runtime !== 'node') {
1080
+ throw new Error(`Invalid runtime: ${options.runtime}`);
1081
+ }
1082
+ const runtime = options.runtime === 'browser' || options.runtime === 'node' ? options.runtime : undefined;
1083
+ const capability = createCapability({
1084
+ id,
1085
+ title: options.title,
1086
+ description: options.description,
1087
+ location: options.project ? 'project' : 'user',
1088
+ cwd: process.cwd(),
1089
+ overwrite: options.force,
1090
+ createdBy: 'ai',
1091
+ runtime,
1092
+ });
1093
+ if (options.json) {
1094
+ console.log(JSON.stringify(toCapabilitySummary(capability), null, 2));
1095
+ return;
1096
+ }
1097
+ console.log(`Created ${capability.manifest.id} at ${capability.dir}`);
1098
+ }
1099
+ catch (error) {
1100
+ exitWithError(error);
1101
+ }
1102
+ });
1103
+ cli
1104
+ .command('capability skill init <id>', 'Create an editable agent skill scaffold for a saved capability')
1105
+ .option('--force', 'Overwrite an existing agent skill draft')
1106
+ .option('--json', 'Print JSON')
1107
+ .action((id, options) => {
1108
+ try {
1109
+ const result = initCapabilityAgentSkill({
1110
+ id,
1111
+ cwd: process.cwd(),
1112
+ overwrite: options.force,
1113
+ });
1114
+ if (options.json) {
1115
+ console.log(JSON.stringify(result, null, 2));
1116
+ return;
1117
+ }
1118
+ console.log(`Created agent skill draft for ${result.capabilityId}: ${result.dir}`);
1119
+ result.files.forEach((file) => {
1120
+ console.log(`- ${file.status}: ${file.path}`);
1121
+ });
1122
+ console.log('');
1123
+ console.log('Next:');
1124
+ result.next.forEach((step) => {
1125
+ console.log(` ${step}`);
1126
+ });
1127
+ }
1128
+ catch (error) {
1129
+ exitWithError(error);
1130
+ }
1131
+ });
1132
+ cli
1133
+ .command('capability skill install <id>', 'Install an edited capability agent skill into Codex')
1134
+ .option('--force', 'Overwrite an existing installed agent skill')
1135
+ .option('--codex-home <dir>', 'Codex home directory (defaults to CODEX_HOME or ~/.codex)')
1136
+ .option('--json', 'Print JSON')
1137
+ .action((id, options) => {
1138
+ try {
1139
+ const result = installCapabilityAgentSkill({
1140
+ id,
1141
+ cwd: process.cwd(),
1142
+ overwrite: options.force,
1143
+ codexHome: options.codexHome,
1144
+ });
1145
+ if (options.json) {
1146
+ console.log(JSON.stringify(result, null, 2));
1147
+ return;
1148
+ }
1149
+ console.log(`Installed agent skill for ${result.capabilityId}: ${result.dir}`);
1150
+ result.files.forEach((file) => {
1151
+ console.log(`- ${file.status}: ${file.path}`);
1152
+ });
1153
+ console.log('');
1154
+ console.log('Next:');
1155
+ result.next.forEach((step) => {
1156
+ console.log(` ${step}`);
1157
+ });
1158
+ }
1159
+ catch (error) {
1160
+ exitWithError(error);
1161
+ }
1162
+ });
1163
+ cli
1164
+ .command('capability skill show <id>', 'Show the editable agent skill draft for a saved capability')
1165
+ .option('--json', 'Print JSON')
1166
+ .action((id, options) => {
1167
+ try {
1168
+ const result = showCapabilityAgentSkill({
1169
+ id,
1170
+ cwd: process.cwd(),
1171
+ });
1172
+ if (options.json) {
1173
+ console.log(JSON.stringify(result, null, 2));
1174
+ return;
1175
+ }
1176
+ const skill = result.files.find((file) => {
1177
+ return file.relativePath === 'SKILL.md';
1178
+ });
1179
+ if (!skill) {
1180
+ throw new Error(`Agent skill draft missing SKILL.md for ${id}`);
1181
+ }
1182
+ console.log(skill.content);
1183
+ }
1184
+ catch (error) {
1185
+ exitWithError(error);
1186
+ }
1187
+ });
1188
+ cli
1189
+ .command('capability pack <id>', 'Pack a capability for safe sharing')
1190
+ .option('-o, --output <path>', 'Output .tgz path (default: <id>.tgz)')
1191
+ .option('--force', 'Overwrite an existing package')
1192
+ .option('--json', 'Print JSON')
1193
+ .action(async (id, options) => {
1194
+ try {
1195
+ const packed = await packCapability({
1196
+ id,
1197
+ cwd: process.cwd(),
1198
+ output: options.output,
1199
+ overwrite: options.force,
1200
+ });
1201
+ if (options.json) {
1202
+ console.log(JSON.stringify(packed, null, 2));
1203
+ return;
1204
+ }
1205
+ console.log(`Packed ${packed.capabilityId}: ${packed.path}`);
1206
+ console.log(`Integrity: ${packed.integrity}`);
1207
+ console.log('Included files:');
1208
+ packed.files.map((file) => {
1209
+ console.log(`- ${file}`);
1210
+ return file;
1211
+ });
1212
+ }
1213
+ catch (error) {
1214
+ exitWithError(error);
1215
+ }
1216
+ });
1217
+ cli
1218
+ .command('capability install <source>', 'Install a built-in suite, capability directory, local .tgz, or .tgz URL')
1219
+ .option('--project', 'Install under .tabwright/capabilities in the current project')
1220
+ .option('--force', 'Overwrite existing installed capabilities')
1221
+ .option('--draft', 'Install a built-in suite as draft instead of trusted')
1222
+ .option('--skip-agent-skills', 'Do not install bundled agent skills such as Codex skills')
1223
+ .option('--with-agent-skill', 'Install a shared package agent skill after reviewing the source')
1224
+ .option('--json', 'Print JSON')
1225
+ .action(async (source, options) => {
1226
+ try {
1227
+ if (isBuiltinCapabilitySuite(source)) {
1228
+ const installed = installBuiltinCapabilitySuite({
1229
+ suite: source,
1230
+ cwd: process.cwd(),
1231
+ location: options.project ? 'project' : 'user',
1232
+ overwrite: options.force,
1233
+ trust: options.draft ? false : true,
1234
+ installAgentSkills: options.skipAgentSkills ? false : true,
1235
+ });
1236
+ const summary = {
1237
+ type: 'builtin',
1238
+ suite: installed.suite,
1239
+ capabilities: installed.capabilities.map((capability) => {
1240
+ return toCapabilitySummary(capability);
1241
+ }),
1242
+ agentSkills: installed.agentSkills,
1243
+ next: installed.capabilities.flatMap((capability) => {
1244
+ if (capability.manifest.auth.refresh !== 'from-browser') {
1245
+ return [];
1246
+ }
1247
+ return [`tabwright capability refresh-auth ${capability.manifest.id} --browser user --json`];
1248
+ }),
1249
+ };
1250
+ if (options.json) {
1251
+ console.log(JSON.stringify(summary, null, 2));
1252
+ return;
1253
+ }
1254
+ console.log(`Installed ${installed.suite}:`);
1255
+ installed.capabilities.map((capability) => {
1256
+ console.log(`- ${capability.manifest.id} (${capability.location}, ${capability.manifest.status})`);
1257
+ return capability.manifest.id;
1258
+ });
1259
+ if (installed.agentSkills.length > 0) {
1260
+ console.log('');
1261
+ console.log('Installed agent skills:');
1262
+ installed.agentSkills.map((agentSkill) => {
1263
+ console.log(`- ${agentSkill.target}:${agentSkill.name} at ${agentSkill.dir}`);
1264
+ return agentSkill.name;
1265
+ });
1266
+ }
1267
+ if (summary.next.length > 0) {
1268
+ console.log('');
1269
+ console.log('Refresh auth with:');
1270
+ summary.next.map((command) => {
1271
+ console.log(` ${command}`);
1272
+ return command;
1273
+ });
1274
+ }
1275
+ return;
1276
+ }
1277
+ const installed = await installCapabilityPackage({
1278
+ source,
1279
+ cwd: process.cwd(),
1280
+ location: options.project ? 'project' : 'user',
1281
+ overwrite: options.force,
1282
+ });
1283
+ const agentSkill = installed.agentSkillAvailable && options.withAgentSkill
1284
+ ? installCapabilityAgentSkill({
1285
+ id: installed.capability.manifest.id,
1286
+ cwd: process.cwd(),
1287
+ overwrite: options.force,
1288
+ capability: installed.capability,
1289
+ })
1290
+ : null;
1291
+ const next = [
1292
+ `tabwright capability describe ${installed.capability.manifest.id} --json`,
1293
+ ...(installed.agentSkillAvailable && !agentSkill
1294
+ ? [`Review the packaged agent skill, then install it with: tabwright capability skill install ${installed.capability.manifest.id}`]
1295
+ : []),
1296
+ ...(installed.capability.manifest.auth.refresh === 'from-browser'
1297
+ ? [`tabwright capability refresh-auth ${installed.capability.manifest.id} --browser user --json`]
1298
+ : []),
1299
+ `Validate with capability run --force before trusting ${installed.capability.manifest.id}.`,
1300
+ ];
1301
+ const summary = {
1302
+ type: 'package',
1303
+ source: installed.source,
1304
+ capability: toCapabilitySummary(installed.capability),
1305
+ files: installed.files,
1306
+ integrity: installed.integrity,
1307
+ agentSkill,
1308
+ next,
1309
+ };
1310
+ if (options.json) {
1311
+ console.log(JSON.stringify(summary, null, 2));
1312
+ return;
1313
+ }
1314
+ console.log(`Installed ${installed.capability.manifest.id} as draft: ${installed.capability.dir}`);
1315
+ console.log(`Integrity: ${installed.integrity}`);
1316
+ if (agentSkill) {
1317
+ console.log(`Installed agent skill: ${agentSkill.dir}`);
1318
+ }
1319
+ console.log('');
1320
+ console.log('Next:');
1321
+ next.map((step) => {
1322
+ console.log(` ${step}`);
1323
+ return step;
1324
+ });
1325
+ }
1326
+ catch (error) {
1327
+ exitWithError(error);
1328
+ }
1329
+ });
1330
+ cli
1331
+ .command('capability update <id>', 'Update a capability script from a file')
1332
+ .option('--from-file <path>', 'Path to the new script.js source')
1333
+ .option('--contract-file <path>', 'Path to a JSON manifest contract patch')
1334
+ .option('--title <title>', 'Update title')
1335
+ .option('--description <description>', 'Update description')
1336
+ .option('--json', 'Print JSON')
1337
+ .action((id, options) => {
1338
+ try {
1339
+ let capability = null;
1340
+ if (options.fromFile) {
1341
+ const sourcePath = path.resolve(options.fromFile);
1342
+ capability = updateCapabilityScript({ id, cwd: process.cwd(), source: fs.readFileSync(sourcePath, 'utf-8') });
1343
+ }
1344
+ if (options.contractFile) {
1345
+ const patch = readCapabilityManifestPatchFromFile(options.contractFile);
1346
+ const currentCapability = capability ||
1347
+ listCapabilities({ cwd: process.cwd() }).find((candidate) => {
1348
+ return candidate.manifest.id === id;
1349
+ }) ||
1350
+ null;
1351
+ if (currentCapability?.manifest.status === 'trusted' && !patch.status) {
1352
+ patch.status = 'draft';
1353
+ }
1354
+ capability = updateCapabilityManifest({ id, cwd: process.cwd(), patch });
1355
+ }
1356
+ if (options.title || options.description) {
1357
+ const patch = {};
1358
+ if (options.title) {
1359
+ patch.title = options.title;
1360
+ }
1361
+ if (options.description) {
1362
+ patch.description = options.description;
1363
+ }
1364
+ capability = updateCapabilityManifest({
1365
+ id,
1366
+ cwd: process.cwd(),
1367
+ patch,
1368
+ });
1369
+ }
1370
+ if (!capability) {
1371
+ throw new Error('Nothing to update. Pass --from-file, --contract-file, --title, or --description.');
1372
+ }
1373
+ if (options.json) {
1374
+ console.log(JSON.stringify(toCapabilitySummary(capability), null, 2));
1375
+ return;
1376
+ }
1377
+ console.log(`Updated ${capability.manifest.id}. Status: ${capability.manifest.status}`);
1378
+ }
1379
+ catch (error) {
1380
+ exitWithError(error);
1381
+ }
1382
+ });
1383
+ cli
1384
+ .command('capability trust <id>', 'Mark a capability trusted')
1385
+ .action((id) => {
1386
+ try {
1387
+ const capability = updateCapabilityManifest({ id, cwd: process.cwd(), patch: { status: 'trusted' } });
1388
+ console.log(`Trusted ${capability.manifest.id}`);
1389
+ }
1390
+ catch (error) {
1391
+ exitWithError(error);
1392
+ }
1393
+ });
1394
+ cli
1395
+ .command('capability draft <id>', 'Mark a capability draft')
1396
+ .action((id) => {
1397
+ try {
1398
+ const capability = updateCapabilityManifest({ id, cwd: process.cwd(), patch: { status: 'draft' } });
1399
+ console.log(`Marked ${capability.manifest.id} as draft`);
1400
+ }
1401
+ catch (error) {
1402
+ exitWithError(error);
1403
+ }
1404
+ });
1405
+ cli
1406
+ .command('capability disable <id>', 'Disable a capability')
1407
+ .action((id) => {
1408
+ try {
1409
+ const capability = updateCapabilityManifest({ id, cwd: process.cwd(), patch: { status: 'disabled' } });
1410
+ console.log(`Disabled ${capability.manifest.id}`);
1411
+ }
1412
+ catch (error) {
1413
+ exitWithError(error);
1414
+ }
1415
+ });
1416
+ cli
1417
+ .command('capability refresh-auth <id>', 'Refresh a capability auth secret from the current browser session')
1418
+ .option('-s, --session <id>', 'Existing Tabwright session id')
1419
+ .option('--host <host>', 'Remote relay server host')
1420
+ .option('--token <token>', 'Authentication token (or use TABWRIGHT_TOKEN env var)')
1421
+ .option('--browser <headless|user|key>', 'Runtime when --session is omitted (default: user)')
1422
+ .option('--keep-session', 'Keep auto-created session alive after refresh')
1423
+ .option('--json', 'Print JSON envelope')
1424
+ .option('--timeout [ms]', z.number().default(10000).describe('Execution timeout in milliseconds'))
1425
+ .action(async (id, options) => {
1426
+ try {
1427
+ await refreshCapabilityAuthFromCli(id, options);
1428
+ }
1429
+ catch (error) {
1430
+ exitWithError(error);
1431
+ }
1432
+ });
1433
+ cli
1434
+ .command('capability run <id>', 'Run a Tabwright capability')
1435
+ .option('--input <json>', 'JSON input object')
1436
+ .option('--input-json <json>', 'JSON input object')
1437
+ .option('-s, --session <id>', 'Existing Tabwright session id')
1438
+ .option('--host <host>', 'Remote relay server host')
1439
+ .option('--token <token>', 'Authentication token (or use TABWRIGHT_TOKEN env var)')
1440
+ .option('--browser <headless|user|key>', 'Runtime when --session is omitted (default: headless)')
1441
+ .option('--force', 'Run draft capabilities or bypass URL match checks')
1442
+ .option('--confirm <capability-id>', 'Repeat the capability id after explicit user approval of its side effect')
1443
+ .option('--keep-session', 'Keep auto-created session alive after run')
1444
+ .option('--json', 'Print JSON envelope')
1445
+ .option('--timeout [ms]', z.number().default(10000).describe('Execution timeout in milliseconds'))
1446
+ .action(async (id, options) => {
1447
+ try {
1448
+ await runCapabilityFromCli(id, options);
1449
+ }
1450
+ catch (error) {
1451
+ exitWithError(error);
1452
+ }
1453
+ });
1454
+ cli
1455
+ .command('studio', 'Start the local Tabwright capability studio')
1456
+ .option('--host <host>', z.string().default('127.0.0.1').describe('Host to bind to'))
1457
+ .option('--port <port>', z.number().default(19989).describe('Port to bind to'))
1458
+ .option('--open', 'Open the studio URL in the default browser')
1459
+ .action(async (options) => {
1460
+ try {
1461
+ const { startCapabilityStudio } = await import('./capability-studio.js');
1462
+ const server = await startCapabilityStudio({
1463
+ host: options.host || '127.0.0.1',
1464
+ port: options.port || 19989,
1465
+ cwd: process.cwd(),
1466
+ });
1467
+ const url = `http://${server.host}:${server.port}`;
1468
+ console.log(`Tabwright capability studio: ${url}`);
1469
+ console.log('Press Ctrl+C to stop.');
1470
+ if (options.open) {
1471
+ await openInBrowser(url);
1472
+ }
1473
+ }
1474
+ catch (error) {
1475
+ exitWithError(error);
1476
+ }
1477
+ });
1478
+ cli
1479
+ .command('session new', 'Create a new session and print the session ID')
1480
+ .option('--host <host>', 'Remote relay server host')
1481
+ .option('--token <token>', 'Authentication token (or use TABWRIGHT_TOKEN env var)')
1482
+ .option('--browser <key>', 'Browser key when multiple browsers are available. Special values: "headless" (launch headless Chrome, no extension), "cloud" (cloud browser with stealth/proxies)')
1483
+ .option('--patchright', 'Use @playwriter/patchright-core for stealth mode (bypasses bot detection)')
1484
+ .option('--direct [endpoint]', 'Use direct CDP connection without the extension. Enable debugging first at chrome://inspect/#remote-debugging or launch Chrome with --remote-debugging-port=9222. Auto-discovers instances or accepts an explicit ws:// endpoint')
1485
+ .option('--proxy <region>', 'Enable residential proxy for cloud browser (e.g. us, de, jp). Disabled by default. Use for anti-detection or geo-targeting.')
1486
+ .option('--custom-proxy <url>', 'Custom proxy for cloud browser (host:port or user:pass@host:port)')
1487
+ .option('--timeout <minutes>', 'Cloud browser timeout in minutes (1-240, default 60)')
1488
+ .option('--disable-proxy-bandwidth-acceleration', 'Allow loading images, video, and fonts when proxy is enabled (they are blocked by default to save proxy bandwidth)')
1489
+ .action(async (options) => {
1490
+ if (options.patchright) {
1491
+ process.env.TABWRIGHT_PATCHRIGHT = '1';
1492
+ }
1493
+ const isLocal = !options.host && !process.env.TABWRIGHT_HOST;
1494
+ // --browser headless: launch headless Chrome via chromium.launch(), no extension
1495
+ if (options.browser === 'headless') {
1496
+ try {
1497
+ await ensureRelayForSessionCreation(isLocal);
1498
+ const serverUrl = await getServerUrl(options.host);
1499
+ const response = await fetch(`${serverUrl}/cli/session/new`, {
1500
+ method: 'POST',
1501
+ headers: buildAuthHeaders({ token: options.token, json: true }),
1502
+ body: JSON.stringify({ headless: true, cwd: process.cwd() }),
1503
+ });
1504
+ if (!response.ok) {
1505
+ const text = await response.text();
1506
+ if (text.includes('Could not find a supported browser binary')) {
1507
+ console.error('No Chrome browser found. Install one first:');
1508
+ console.error('');
1509
+ console.error(' tabwright browser install');
1510
+ console.error('');
1511
+ console.error('This downloads Chrome for Testing from Google.');
1512
+ process.exit(1);
1513
+ }
1514
+ console.error(`Error: ${response.status} ${text}`);
1515
+ process.exit(1);
1516
+ }
1517
+ const result = (await response.json());
1518
+ console.log(`Session ${result.id} created (headless). Use with: tabwright -s ${result.id} -e "..."`);
1519
+ console.log(pc.dim('NOTE: Recording unavailable in headless mode.'));
1520
+ }
1521
+ catch (error) {
1522
+ if (error.message?.includes('Could not find a supported browser binary')) {
1523
+ console.error('No Chrome browser found. Install one first:');
1524
+ console.error('');
1525
+ console.error(' tabwright browser install');
1526
+ console.error('');
1527
+ console.error('This downloads Chrome for Testing from Google.');
1528
+ process.exit(1);
1529
+ }
1530
+ console.error(`Error: ${error.message}`);
1531
+ process.exit(1);
1532
+ }
1533
+ return;
1534
+ }
1535
+ // goke 6.6: optional-value flags are string | undefined
1536
+ // `--direct ws://...` → 'ws://...' (explicit endpoint)
1537
+ // `--direct` → '' (bare flag, auto-discover)
1538
+ // (omitted) → undefined (don't use direct CDP)
1539
+ const directEndpoint = options.direct || null;
1540
+ // If --direct with explicit endpoint, resolve it (handles host:port → ws://) then skip discovery
1541
+ if (directEndpoint) {
1542
+ let cdpEndpoint;
1543
+ try {
1544
+ cdpEndpoint = await resolveDirectInput(directEndpoint);
1545
+ }
1546
+ catch (error) {
1547
+ console.error(`Error: ${error.message}`);
1548
+ process.exit(1);
1549
+ }
1550
+ await ensureRelayForSessionCreation(isLocal);
1551
+ const serverUrl = await getServerUrl(options.host);
1552
+ const result = await createDirectSession({ serverUrl, cdpEndpoint, token: options.token });
1553
+ console.log(`Session ${result.id} created (direct CDP). Use with: tabwright -s ${result.id} -e "..."`);
1554
+ console.log(pc.dim('NOTE: Recording unavailable in direct CDP mode.'));
1555
+ return;
1556
+ }
1557
+ // If --direct with no endpoint, discover Chrome instances
1558
+ if (options.direct === '') {
1559
+ if (!isLocal) {
1560
+ console.error('Error: --direct auto-discovery only works locally.');
1561
+ console.error('For remote relay, pass an explicit endpoint reachable from the relay host:');
1562
+ console.error(' tabwright session new --host <host> --direct ws://relay-host:9222/devtools/browser/...');
1563
+ process.exit(1);
1564
+ }
1565
+ await ensureRelayForSessionCreation(isLocal);
1566
+ console.log(pc.dim('Discovering Chrome instances with debugging enabled...'));
1567
+ const instances = await discoverChromeInstances();
1568
+ if (instances.length === 0) {
1569
+ console.error('No Chrome instances with debugging enabled found.');
1570
+ console.error('');
1571
+ console.error('Enable debugging in one of these ways:');
1572
+ console.error(' 1. Open chrome://inspect/#remote-debugging in Chrome');
1573
+ console.error(' 2. Launch Chrome with: chrome --remote-debugging-port=9222');
1574
+ process.exit(1);
1575
+ }
1576
+ if (instances.length === 1 && !options.browser) {
1577
+ const instance = instances[0];
1578
+ const serverUrl = await getServerUrl(options.host);
1579
+ const result = await createDirectSession({ serverUrl, cdpEndpoint: instance.wsUrl, browser: instance.browser, profiles: instance.profiles, token: options.token });
1580
+ const profileLabel = formatInstanceProfiles(instance);
1581
+ console.log(`Session ${result.id} created (direct CDP, ${instance.browser}${profileLabel}). Use with: tabwright -s ${result.id} -e "..."`);
1582
+ console.log(pc.dim('NOTE: Recording unavailable in direct CDP mode.'));
1583
+ return;
1584
+ }
1585
+ // Multiple instances or --browser specified
1586
+ const directOptions = instances.map((instance) => {
1587
+ return instanceToBrowserOption(instance);
1588
+ });
1589
+ if (options.browser) {
1590
+ const selected = directOptions.find((opt) => {
1591
+ return opt.key === options.browser;
1592
+ });
1593
+ if (!selected) {
1594
+ await handleCloudBrowserNotFound(options.browser, { hasCloudOptions: false });
1595
+ console.error(`Browser not found: ${options.browser}`);
1596
+ console.error('Available: ' + directOptions.map((opt) => opt.key).join(', '));
1597
+ process.exit(1);
1598
+ }
1599
+ const serverUrl = await getServerUrl(options.host);
1600
+ const result = await createDirectSession({ serverUrl, cdpEndpoint: selected.wsUrl, browser: selected.browser, profiles: selected.profiles, token: options.token });
1601
+ console.log(`Session ${result.id} created (direct CDP). Use with: tabwright -s ${result.id} -e "..."`);
1602
+ console.log(pc.dim('NOTE: Recording unavailable in direct CDP mode.'));
1603
+ return;
1604
+ }
1605
+ printBrowserTable(directOptions);
1606
+ console.log('\nRun again with --browser <key>.');
1607
+ process.exit(1);
1608
+ }
1609
+ // Default mode: extension-based (existing behavior)
1610
+ let extensions = [];
1611
+ if (isLocal) {
1612
+ await ensureRelayServer({ logger: console });
1613
+ extensions = await waitForConnectedExtensions({
1614
+ timeoutMs: 12000,
1615
+ pollIntervalMs: 250,
1616
+ settleMs: 750,
1617
+ logger: console,
1618
+ });
1619
+ if (extensions.length === 0) {
1620
+ console.log(pc.dim('Waiting briefly for extension to reconnect...'));
1621
+ extensions = await waitForConnectedExtensions({
1622
+ timeoutMs: 10000,
1623
+ pollIntervalMs: 250,
1624
+ settleMs: 750,
1625
+ logger: console,
1626
+ });
1627
+ }
1628
+ }
1629
+ else {
1630
+ extensions = await fetchExtensionsStatus({ host: options.host, token: options.token });
1631
+ }
1632
+ if (extensions.length === 0) {
1633
+ // Before giving up, check if cloud browsers are available
1634
+ const cloudOptions = await discoverCloudBrowsers();
1635
+ if (cloudOptions.length > 0) {
1636
+ // Cloud-only user: skip extension requirement, show cloud options
1637
+ await ensureRelayForSessionCreation(isLocal);
1638
+ const allOptions = [...cloudOptions];
1639
+ if (options.browser) {
1640
+ const selected = allOptions.find((opt) => { return opt.key === options.browser; });
1641
+ if (!selected) {
1642
+ await handleCloudBrowserNotFound(options.browser, { hasCloudOptions: true });
1643
+ console.error(`Browser not found: ${options.browser}`);
1644
+ console.error('Available: ' + allOptions.map((opt) => opt.key).join(', '));
1645
+ process.exit(1);
1646
+ }
1647
+ const serverUrl = await getServerUrl(options.host);
1648
+ // Reuse existing running VM if selected, otherwise create new
1649
+ const result = selected.activeCloudSessionId
1650
+ ? await attachExistingCloudSession({
1651
+ serverUrl,
1652
+ cloudSessionId: selected.activeCloudSessionId,
1653
+ blockProxyResources: computeBlockProxyResources(options),
1654
+ token: options.token,
1655
+ })
1656
+ : await createCloudSession({
1657
+ serverUrl,
1658
+ proxyRegion: options.proxy,
1659
+ customProxy: options.customProxy,
1660
+ timeout: parseCloudTimeout(options.timeout),
1661
+ blockProxyResources: computeBlockProxyResources(options),
1662
+ token: options.token,
1663
+ });
1664
+ console.log(`Session ${result.id} created (cloud). Use with: tabwright -s ${result.id} -e "..."`);
1665
+ if (result.liveUrl) {
1666
+ console.log(pc.dim(`Live view: ${result.liveUrl}`));
1667
+ }
1668
+ return;
1669
+ }
1670
+ console.log('\nNo local browsers detected, but cloud browsers are available:\n');
1671
+ printBrowserTable(allOptions);
1672
+ console.log('\nRun again with --browser <key>.');
1673
+ process.exit(1);
1674
+ }
1675
+ if (options.browser) {
1676
+ await handleCloudBrowserNotFound(options.browser, { hasCloudOptions: false });
1677
+ }
1678
+ console.error('No connected browsers detected. Click the Tabwright extension icon.');
1679
+ console.error(pc.dim('Tip: Use --direct to connect via Chrome DevTools Protocol instead.'));
1680
+ console.error(pc.dim('Tip: Run `tabwright cloud login` to use cloud browsers.'));
1681
+ process.exit(1);
1682
+ }
1683
+ // Warn if any connected extension was built with an older tabwright version
1684
+ for (const ext of extensions) {
1685
+ const warning = getExtensionOutdatedWarning(ext.playwriterVersion);
1686
+ if (warning) {
1687
+ console.error(warning);
1688
+ break;
1689
+ }
1690
+ }
1691
+ // Auto-select only when the extension choice is unambiguous. With multiple
1692
+ // profiles, a single extension with enabled tabs is the user's active choice.
1693
+ const implicitExtension = options.browser ? null : selectImplicitExtension(extensions);
1694
+ if (implicitExtension) {
1695
+ const selectedExtension = implicitExtension;
1696
+ try {
1697
+ const serverUrl = await getServerUrl(options.host);
1698
+ const extensionId = selectedExtension.extensionId === 'default'
1699
+ ? null
1700
+ : selectedExtension.stableKey || selectedExtension.extensionId;
1701
+ const cwd = process.cwd();
1702
+ const response = await fetch(`${serverUrl}/cli/session/new`, {
1703
+ method: 'POST',
1704
+ headers: buildAuthHeaders({ token: options.token, json: true }),
1705
+ body: JSON.stringify({ extensionId, cwd }),
1706
+ });
1707
+ if (!response.ok) {
1708
+ const text = await response.text();
1709
+ console.error(`Error: ${response.status} ${text}`);
1710
+ process.exit(1);
1711
+ }
1712
+ const result = (await response.json());
1713
+ console.log(`Session ${result.id} created. Use with: tabwright -s ${result.id} -e "..."`);
1714
+ printCloudTip();
1715
+ }
1716
+ catch (error) {
1717
+ console.error(`Error: ${error.message}`);
1718
+ process.exit(1);
1719
+ }
1720
+ return;
1721
+ }
1722
+ // Multiple extensions: also discover direct CDP instances and cloud browsers.
1723
+ // Direct discovery only works locally — remote relay can't reach local Chrome debug ports.
1724
+ const directInstances = isLocal ? await (async () => {
1725
+ console.log(pc.dim('Discovering additional Chrome instances...'));
1726
+ return await discoverChromeInstances();
1727
+ })() : [];
1728
+ // Fetch cloud browser slots if user is logged in
1729
+ const cloudOptions = await discoverCloudBrowsers();
1730
+ const allOptions = [
1731
+ ...extensions.map((ext) => {
1732
+ return {
1733
+ key: ext.stableKey || ext.extensionId,
1734
+ type: 'extension',
1735
+ browser: ext.browser || 'Chrome',
1736
+ profile: ext.profile?.email || '(not signed in)',
1737
+ extensionId: ext.extensionId === 'default' ? null : ext.stableKey || ext.extensionId,
1738
+ };
1739
+ }),
1740
+ ...directInstances.map((instance) => {
1741
+ return instanceToBrowserOption(instance);
1742
+ }),
1743
+ ...cloudOptions,
1744
+ ];
1745
+ if (options.browser) {
1746
+ const selected = allOptions.find((opt) => {
1747
+ return opt.key === options.browser;
1748
+ });
1749
+ if (!selected) {
1750
+ await handleCloudBrowserNotFound(options.browser, { hasCloudOptions: cloudOptions.length > 0 });
1751
+ console.error(`Browser not found: ${options.browser}`);
1752
+ console.error('Available: ' + allOptions.map((opt) => opt.key).join(', '));
1753
+ process.exit(1);
1754
+ }
1755
+ try {
1756
+ const serverUrl = await getServerUrl(options.host);
1757
+ if (selected.type === 'cloud') {
1758
+ // Reuse existing running VM if selected, otherwise create new
1759
+ const result = selected.activeCloudSessionId
1760
+ ? await attachExistingCloudSession({
1761
+ serverUrl,
1762
+ cloudSessionId: selected.activeCloudSessionId,
1763
+ blockProxyResources: computeBlockProxyResources(options),
1764
+ token: options.token,
1765
+ })
1766
+ : await createCloudSession({
1767
+ serverUrl,
1768
+ proxyRegion: options.proxy,
1769
+ customProxy: options.customProxy,
1770
+ timeout: parseCloudTimeout(options.timeout),
1771
+ blockProxyResources: computeBlockProxyResources(options),
1772
+ token: options.token,
1773
+ });
1774
+ console.log(`Session ${result.id} created (cloud). Use with: tabwright -s ${result.id} -e "..."`);
1775
+ if (result.liveUrl) {
1776
+ console.log(pc.dim(`Live view: ${result.liveUrl}`));
1777
+ }
1778
+ }
1779
+ else if (selected.type === 'direct') {
1780
+ const result = await createDirectSession({ serverUrl, cdpEndpoint: selected.wsUrl, browser: selected.browser, profiles: selected.profiles, token: options.token });
1781
+ console.log(`Session ${result.id} created (direct CDP). Use with: tabwright -s ${result.id} -e "..."`);
1782
+ console.log(pc.dim('NOTE: Recording unavailable in direct CDP mode.'));
1783
+ }
1784
+ else {
1785
+ const cwd = process.cwd();
1786
+ const response = await fetch(`${serverUrl}/cli/session/new`, {
1787
+ method: 'POST',
1788
+ headers: buildAuthHeaders({ token: options.token, json: true }),
1789
+ body: JSON.stringify({ extensionId: selected.extensionId, cwd }),
1790
+ });
1791
+ if (!response.ok) {
1792
+ const text = await response.text();
1793
+ console.error(`Error: ${response.status} ${text}`);
1794
+ process.exit(1);
1795
+ }
1796
+ const result = (await response.json());
1797
+ console.log(`Session ${result.id} created. Use with: tabwright -s ${result.id} -e "..."`);
1798
+ printCloudTip();
1799
+ }
1800
+ }
1801
+ catch (error) {
1802
+ console.error(`Error: ${error.message}`);
1803
+ process.exit(1);
1804
+ }
1805
+ return;
1806
+ }
1807
+ // Show unified table
1808
+ console.log('\nMultiple browsers detected:\n');
1809
+ printBrowserTable(allOptions);
1810
+ console.log('\nRun again with --browser <key>.');
1811
+ process.exit(1);
1812
+ });
1813
+ async function ensureRelayForSessionCreation(isLocal) {
1814
+ if (isLocal) {
1815
+ await ensureRelayServer({ logger: console });
1816
+ }
1817
+ }
1818
+ async function createDirectSession({ serverUrl, cdpEndpoint, browser, profiles, token, }) {
1819
+ const cwd = process.cwd();
1820
+ const response = await fetch(`${serverUrl}/cli/session/new`, {
1821
+ method: 'POST',
1822
+ headers: buildAuthHeaders({ token, json: true }),
1823
+ body: JSON.stringify({ cdpEndpoint, cwd, browser, profiles }),
1824
+ });
1825
+ if (!response.ok) {
1826
+ const text = await response.text();
1827
+ throw new Error(`${response.status} ${text}`);
1828
+ }
1829
+ return (await response.json());
1830
+ }
1831
+ function instanceToBrowserOption(instance) {
1832
+ return {
1833
+ key: `direct:${instance.port}`,
1834
+ type: 'direct',
1835
+ browser: instance.browser,
1836
+ profile: formatInstanceProfiles(instance),
1837
+ wsUrl: instance.wsUrl,
1838
+ profiles: instance.profiles,
1839
+ };
1840
+ }
1841
+ function formatInstanceProfiles(instance) {
1842
+ if (instance.profiles.length === 0) {
1843
+ return '(unknown)';
1844
+ }
1845
+ return instance.profiles
1846
+ .map((p) => {
1847
+ return p.email ? `${p.name} (${p.email})` : p.name;
1848
+ })
1849
+ .join(', ');
1850
+ }
1851
+ /** Discover cloud sessions from the website API, if logged in.
1852
+ * Also adds a "cloud-new" option to create a new cloud browser. */
1853
+ async function discoverCloudBrowsers() {
1854
+ const client = getCloudClient();
1855
+ if (!client)
1856
+ return [];
1857
+ try {
1858
+ const { sessions } = await client.getStatus();
1859
+ const options = sessions.map((s) => {
1860
+ return {
1861
+ key: `cloud-${s.index}`,
1862
+ type: 'cloud',
1863
+ browser: 'Chromium',
1864
+ profile: `(running, expires ${new Date(s.timeoutAt).toLocaleTimeString()})`,
1865
+ activeCloudSessionId: s.cloudSessionId,
1866
+ };
1867
+ });
1868
+ // Always offer a "cloud-new" option to spin up a fresh VM
1869
+ options.push({
1870
+ key: 'cloud',
1871
+ type: 'cloud',
1872
+ browser: 'Chromium',
1873
+ profile: '(new cloud browser)',
1874
+ });
1875
+ return options;
1876
+ }
1877
+ catch (error) {
1878
+ const msg = error instanceof Error ? error.message : String(error);
1879
+ console.error(pc.dim(`Cloud browser discovery failed: ${msg}`));
1880
+ return [];
1881
+ }
1882
+ }
1883
+ /** Compute whether to block images/video/fonts for proxy bandwidth savings.
1884
+ * Enabled by default when proxy or custom-proxy is set, disabled via
1885
+ * --disable-proxy-bandwidth-acceleration. */
1886
+ function computeBlockProxyResources(options) {
1887
+ const proxyEnabled = !!(options.proxy || options.customProxy);
1888
+ if (!proxyEnabled)
1889
+ return undefined; // no proxy, no blocking needed
1890
+ if (options.disableProxyBandwidthAcceleration)
1891
+ return false;
1892
+ return true;
1893
+ }
1894
+ /** Check if user requested a cloud browser that isn't available.
1895
+ * Shows helpful login/subscribe instructions instead of a generic "not found" error.
1896
+ * @param hasCloudOptions whether any cloud options were discovered (to distinguish
1897
+ * "not logged in" from "typo in cloud key") */
1898
+ async function handleCloudBrowserNotFound(browserKey, { hasCloudOptions }) {
1899
+ if (!browserKey.startsWith('cloud'))
1900
+ return false;
1901
+ // If cloud options exist, this is a typo (e.g. cloud-99) — let the
1902
+ // generic "Browser not found" message show the available list instead.
1903
+ if (hasCloudOptions)
1904
+ return false;
1905
+ const auth = loadCloudAuth();
1906
+ if (!auth) {
1907
+ console.error('Cloud browsers require authentication.');
1908
+ console.error('');
1909
+ console.error(' Option 1: Run `tabwright cloud login` (interactive browser flow)');
1910
+ console.error(' Option 2: Set TABWRIGHT_API_KEY env var (create one at playwriter.dev/dashboard)');
1911
+ console.error('');
1912
+ console.error(' Then subscribe at playwriter.dev/dashboard and run `tabwright session new --browser cloud`');
1913
+ }
1914
+ else {
1915
+ // Verify token is still valid with a quick API check
1916
+ const client = getCloudClient();
1917
+ const tokenValid = await (async () => {
1918
+ if (!client)
1919
+ return false;
1920
+ try {
1921
+ await client.getStatus();
1922
+ return true;
1923
+ }
1924
+ catch {
1925
+ return false;
1926
+ }
1927
+ })();
1928
+ if (!tokenValid) {
1929
+ console.error('Cloud authentication expired. Please re-authenticate.');
1930
+ console.error('');
1931
+ console.error(' Run `tabwright cloud login` or set TABWRIGHT_API_KEY env var.');
1932
+ }
1933
+ else {
1934
+ console.error('No cloud browser sessions available.');
1935
+ console.error('');
1936
+ console.error(' You are logged in, but you may need an active subscription.');
1937
+ console.error(' Run `tabwright cloud subscribe` to manage your plan.');
1938
+ console.error(' Then run `tabwright session new --browser cloud` to start a cloud browser.');
1939
+ }
1940
+ }
1941
+ process.exit(1);
1942
+ }
1943
+ function printCloudTip() {
1944
+ console.log('');
1945
+ console.log(pc.dim('Tip: Need stealth browsing, VPS control, or auto CAPTCHA solving? Run `tabwright cloud login` or set TABWRIGHT_API_KEY'));
1946
+ console.log(pc.dim(' to control a browser in the cloud instead of local Chrome.'));
1947
+ }
1948
+ /** Parse a custom proxy string (host:port or user:pass@host:port) into an object. */
1949
+ function parseCustomProxy(proxyStr) {
1950
+ // Format: [user:pass@]host:port
1951
+ const atIdx = proxyStr.lastIndexOf('@');
1952
+ let hostPort;
1953
+ let username;
1954
+ let password;
1955
+ if (atIdx !== -1) {
1956
+ const userPass = proxyStr.slice(0, atIdx);
1957
+ hostPort = proxyStr.slice(atIdx + 1);
1958
+ const colonIdx = userPass.indexOf(':');
1959
+ if (colonIdx !== -1) {
1960
+ username = userPass.slice(0, colonIdx);
1961
+ password = userPass.slice(colonIdx + 1);
1962
+ }
1963
+ else {
1964
+ username = userPass;
1965
+ }
1966
+ }
1967
+ else {
1968
+ hostPort = proxyStr;
1969
+ }
1970
+ const lastColon = hostPort.lastIndexOf(':');
1971
+ if (lastColon === -1) {
1972
+ throw new Error(`Invalid proxy format: missing port in "${proxyStr}". Expected host:port or user:pass@host:port`);
1973
+ }
1974
+ const host = hostPort.slice(0, lastColon);
1975
+ const port = parseInt(hostPort.slice(lastColon + 1), 10);
1976
+ if (isNaN(port)) {
1977
+ throw new Error(`Invalid proxy port in "${proxyStr}"`);
1978
+ }
1979
+ return { host, port, username, password };
1980
+ }
1981
+ /** Parse and validate the --timeout CLI option (integer 1-240). */
1982
+ function parseCloudTimeout(value) {
1983
+ if (value === undefined)
1984
+ return undefined;
1985
+ if (!/^\d+$/.test(value)) {
1986
+ throw new Error('--timeout must be an integer from 1 to 240');
1987
+ }
1988
+ const timeout = Number(value);
1989
+ if (timeout < 1 || timeout > 240) {
1990
+ throw new Error('--timeout must be between 1 and 240 minutes');
1991
+ }
1992
+ return timeout;
1993
+ }
1994
+ /** Connect to a cloud browser and create a tabwright session via the relay. */
1995
+ async function createCloudSession({ serverUrl, proxyRegion, customProxy, timeout, blockProxyResources, token, }) {
1996
+ const client = getCloudClient();
1997
+ if (!client) {
1998
+ throw new Error('Not logged in to cloud. Run `tabwright cloud login` first.');
1999
+ }
2000
+ const connectResult = await client.connect({
2001
+ proxyRegion,
2002
+ customProxy: customProxy ? parseCustomProxy(customProxy) : undefined,
2003
+ timeout,
2004
+ });
2005
+ if (!connectResult.cdpUrl) {
2006
+ throw new Error('Cloud browser returned no CDP URL. The VM may have failed to start.');
2007
+ }
2008
+ // Normalize https:// CDP URL to wss:// for the relay
2009
+ const cdpEndpoint = await resolveDirectInput(connectResult.cdpUrl);
2010
+ // Create a tabwright session via the relay using the CDP URL (same as --direct).
2011
+ // Also pass cloud metadata so the relay can track idle timeout and auto-disconnect.
2012
+ const auth = loadCloudAuth();
2013
+ const cwd = process.cwd();
2014
+ let response;
2015
+ try {
2016
+ response = await fetch(`${serverUrl}/cli/session/new`, {
2017
+ method: 'POST',
2018
+ headers: buildAuthHeaders({ token, json: true }),
2019
+ body: JSON.stringify({
2020
+ cdpEndpoint,
2021
+ cwd,
2022
+ browser: 'Chromium (cloud)',
2023
+ cloud: {
2024
+ cloudSessionId: connectResult.cloudSessionId,
2025
+ cloudBaseUrl: auth.baseUrl,
2026
+ cloudToken: auth.token,
2027
+ timeoutAt: connectResult.timeoutAt,
2028
+ blockProxyResources,
2029
+ },
2030
+ }),
2031
+ });
2032
+ }
2033
+ catch (cause) {
2034
+ // Relay session creation failed — stop the cloud VM so we don't leak a paid resource
2035
+ await client.disconnect(connectResult.cloudSessionId).catch(() => { });
2036
+ throw new Error('Failed to create relay session', { cause });
2037
+ }
2038
+ if (!response.ok) {
2039
+ await client.disconnect(connectResult.cloudSessionId).catch(() => { });
2040
+ const text = await response.text();
2041
+ throw new Error(`${response.status} ${text}`);
2042
+ }
2043
+ const result = (await response.json());
2044
+ return { id: result.id, liveUrl: connectResult.cdpUrl ? buildLiveUrl(connectResult.cdpUrl, auth.baseUrl) : null };
2045
+ }
2046
+ /** Reattach to an existing running cloud browser VM instead of creating a new one.
2047
+ * Fetches the session's cdpUrl from the cloud API and creates a relay session. */
2048
+ async function attachExistingCloudSession({ serverUrl, cloudSessionId, blockProxyResources, token, }) {
2049
+ const client = getCloudClient();
2050
+ if (!client) {
2051
+ throw new Error('Not logged in to cloud. Run `tabwright cloud login` first.');
2052
+ }
2053
+ const session = await client.getSessionStatus(cloudSessionId);
2054
+ if (!session || session.status !== 'active') {
2055
+ throw new Error('Cloud session is no longer active. It may have timed out.');
2056
+ }
2057
+ if (!session.cdpUrl) {
2058
+ throw new Error('Cloud session has no CDP URL available.');
2059
+ }
2060
+ const cdpEndpoint = await resolveDirectInput(session.cdpUrl);
2061
+ const auth = loadCloudAuth();
2062
+ const cwd = process.cwd();
2063
+ const response = await fetch(`${serverUrl}/cli/session/new`, {
2064
+ method: 'POST',
2065
+ headers: buildAuthHeaders({ token, json: true }),
2066
+ body: JSON.stringify({
2067
+ cdpEndpoint,
2068
+ cwd,
2069
+ browser: 'Chromium (cloud)',
2070
+ cloud: {
2071
+ cloudSessionId,
2072
+ cloudBaseUrl: auth.baseUrl,
2073
+ cloudToken: auth.token,
2074
+ timeoutAt: session.timeoutAt,
2075
+ blockProxyResources,
2076
+ },
2077
+ }),
2078
+ });
2079
+ if (!response.ok) {
2080
+ const text = await response.text();
2081
+ throw new Error(`${response.status} ${text}`);
2082
+ }
2083
+ const result = (await response.json());
2084
+ return { id: result.id, liveUrl: session.cdpUrl ? buildLiveUrl(session.cdpUrl, auth.baseUrl) : null };
2085
+ }
2086
+ function printBrowserTable(options) {
2087
+ const typeLabels = options.map((opt) => {
2088
+ if (opt.type === 'direct')
2089
+ return '--direct';
2090
+ if (opt.type === 'cloud')
2091
+ return 'cloud';
2092
+ return opt.type;
2093
+ });
2094
+ const keyWidth = Math.max(3, ...options.map((opt) => opt.key.length));
2095
+ const typeWidth = Math.max(4, ...typeLabels.map((t) => t.length));
2096
+ const browserWidth = Math.max(7, ...options.map((opt) => opt.browser.length));
2097
+ console.log('KEY'.padEnd(keyWidth) + ' ' + 'TYPE'.padEnd(typeWidth) + ' ' + 'BROWSER'.padEnd(browserWidth) + ' ' + 'PROFILE');
2098
+ console.log('-'.repeat(keyWidth + typeWidth + browserWidth + 20));
2099
+ for (let i = 0; i < options.length; i++) {
2100
+ const opt = options[i];
2101
+ console.log(opt.key.padEnd(keyWidth) +
2102
+ ' ' +
2103
+ typeLabels[i].padEnd(typeWidth) +
2104
+ ' ' +
2105
+ opt.browser.padEnd(browserWidth) +
2106
+ ' ' +
2107
+ opt.profile);
2108
+ }
2109
+ }
2110
+ cli
2111
+ .command('doctor', 'Check Tabwright readiness and print the single best next step')
2112
+ .option('--host <host>', 'Remote relay server host')
2113
+ .option('--token <token>', 'Authentication token (or use TABWRIGHT_TOKEN env var)')
2114
+ .option('--json', 'Print a machine-readable health report')
2115
+ .action(async (options) => {
2116
+ const isRemote = Boolean(options.host || process.env.TABWRIGHT_HOST);
2117
+ const relayStartup = isRemote
2118
+ ? { started: false, error: null }
2119
+ : await ensureRelayServer({ logger: options.json ? undefined : console })
2120
+ .then((started) => {
2121
+ return { started: started === true, error: null };
2122
+ })
2123
+ .catch((error) => {
2124
+ return {
2125
+ started: false,
2126
+ error: error instanceof Error ? error.message : String(error),
2127
+ };
2128
+ });
2129
+ const serverUrl = await getServerUrl(options.host);
2130
+ const headers = buildAuthHeaders({ token: options.token });
2131
+ const [relayVersion, relayFeatures, initialExtensions, sessions] = await Promise.all([
2132
+ isRemote
2133
+ ? fetch(`${serverUrl}/version`, { headers, signal: AbortSignal.timeout(2000) })
2134
+ .then(async (response) => {
2135
+ if (!response.ok) {
2136
+ return null;
2137
+ }
2138
+ const result = (await response.json());
2139
+ return result.version || null;
2140
+ })
2141
+ .catch(() => {
2142
+ return null;
2143
+ })
2144
+ : getRelayServerVersion(RELAY_PORT),
2145
+ isRemote
2146
+ ? fetch(`${serverUrl}/features`, { headers, signal: AbortSignal.timeout(2000) })
2147
+ .then(async (response) => {
2148
+ if (!response.ok) {
2149
+ return null;
2150
+ }
2151
+ const result = await response.json();
2152
+ if (!result || typeof result !== 'object') {
2153
+ return null;
2154
+ }
2155
+ const features = result.features;
2156
+ return Array.isArray(features) && features.every((feature) => typeof feature === 'string')
2157
+ ? features
2158
+ : null;
2159
+ })
2160
+ .catch(() => {
2161
+ return null;
2162
+ })
2163
+ : getRelayServerFeatures(RELAY_PORT),
2164
+ isRemote ? fetchExtensionsStatus({ host: options.host, token: options.token }) : getExtensionsStatus(RELAY_PORT),
2165
+ fetch(`${serverUrl}/cli/sessions`, { headers, signal: AbortSignal.timeout(2000) })
2166
+ .then(async (response) => {
2167
+ if (!response.ok) {
2168
+ return [];
2169
+ }
2170
+ const result = (await response.json());
2171
+ return result.sessions || [];
2172
+ })
2173
+ .catch(() => {
2174
+ return [];
2175
+ }),
2176
+ ]);
2177
+ const extensions = relayStartup.started && initialExtensions.length === 0
2178
+ ? await waitForConnectedExtensions({
2179
+ timeoutMs: 4000,
2180
+ pollIntervalMs: 200,
2181
+ logger: options.json ? undefined : console,
2182
+ })
2183
+ : initialExtensions;
2184
+ const report = buildDoctorReport({
2185
+ version: VERSION,
2186
+ cwd: process.cwd(),
2187
+ remote: isRemote,
2188
+ relayVersion,
2189
+ relayFeatures,
2190
+ relayError: relayStartup.error,
2191
+ extensions,
2192
+ sessions,
2193
+ capabilityCount: listCapabilities({ cwd: process.cwd() }).length,
2194
+ skillStatus: getTabwrightAgentSkillStatus(),
2195
+ });
2196
+ if (options.json) {
2197
+ console.log(JSON.stringify(report, null, 2));
2198
+ return;
2199
+ }
2200
+ console.log(formatDoctorReport(report));
2201
+ });
2202
+ cli
2203
+ .command('session list', 'List all active sessions')
2204
+ .option('--host <host>', 'Remote relay server host')
2205
+ .option('--token <token>', 'Authentication token (or use TABWRIGHT_TOKEN env var)')
2206
+ .action(async (options) => {
2207
+ if (!options.host && !process.env.TABWRIGHT_HOST) {
2208
+ await ensureRelayServer({ logger: console });
2209
+ }
2210
+ const serverUrl = await getServerUrl(options.host);
2211
+ let sessions = [];
2212
+ try {
2213
+ const response = await fetch(`${serverUrl}/cli/sessions`, {
2214
+ headers: buildAuthHeaders({ token: options.token }),
2215
+ signal: AbortSignal.timeout(2000),
2216
+ });
2217
+ if (!response.ok) {
2218
+ console.error(`Error: ${response.status} ${await response.text()}`);
2219
+ process.exit(1);
2220
+ }
2221
+ const result = (await response.json());
2222
+ sessions = result.sessions;
2223
+ }
2224
+ catch (error) {
2225
+ console.error(`Error: ${error.message}`);
2226
+ process.exit(1);
2227
+ }
2228
+ if (sessions.length === 0) {
2229
+ console.log('No active sessions');
2230
+ return;
2231
+ }
2232
+ const idWidth = Math.max(2, ...sessions.map((session) => String(session.id).length));
2233
+ const browserWidth = Math.max(7, ...sessions.map((session) => (session.browser || 'Chrome').length));
2234
+ const profileWidth = Math.max(7, ...sessions.map((session) => (session.profile?.email || '').length || 1));
2235
+ const extensionWidth = Math.max(2, ...sessions.map((session) => (session.extensionId || '').length || 1));
2236
+ const cwdWidth = Math.max(3, ...sessions.map((session) => (session.cwd || '').length || 1));
2237
+ const stateWidth = Math.max(10, ...sessions.map((session) => session.stateKeys.join(', ').length || 1));
2238
+ console.log('ID'.padEnd(idWidth) +
2239
+ ' ' +
2240
+ 'BROWSER'.padEnd(browserWidth) +
2241
+ ' ' +
2242
+ 'PROFILE'.padEnd(profileWidth) +
2243
+ ' ' +
2244
+ 'EXT'.padEnd(extensionWidth) +
2245
+ ' ' +
2246
+ 'CWD'.padEnd(cwdWidth) +
2247
+ ' ' +
2248
+ 'STATE KEYS');
2249
+ console.log('-'.repeat(idWidth + browserWidth + profileWidth + extensionWidth + cwdWidth + stateWidth + 10));
2250
+ for (const session of sessions) {
2251
+ const stateStr = session.stateKeys.length > 0 ? session.stateKeys.join(', ') : '-';
2252
+ const profileLabel = session.profile?.email || '-';
2253
+ const cwdLabel = session.cwd || '-';
2254
+ console.log(String(session.id).padEnd(idWidth) +
2255
+ ' ' +
2256
+ (session.browser || 'Chrome').padEnd(browserWidth) +
2257
+ ' ' +
2258
+ profileLabel.padEnd(profileWidth) +
2259
+ ' ' +
2260
+ (session.extensionId || '-').padEnd(extensionWidth) +
2261
+ ' ' +
2262
+ cwdLabel.padEnd(cwdWidth) +
2263
+ ' ' +
2264
+ stateStr);
2265
+ }
2266
+ });
2267
+ cli
2268
+ .command('session delete <sessionId>', 'Delete a session and clear its state')
2269
+ .option('--host <host>', 'Remote relay server host')
2270
+ .option('--token <token>', 'Authentication token (or use TABWRIGHT_TOKEN env var)')
2271
+ .action(async (sessionId, options) => {
2272
+ const serverUrl = await getServerUrl(options.host);
2273
+ if (!options.host && !process.env.TABWRIGHT_HOST) {
2274
+ await ensureRelayServer({ logger: console });
2275
+ }
2276
+ try {
2277
+ const response = await fetch(`${serverUrl}/cli/session/delete`, {
2278
+ method: 'POST',
2279
+ headers: buildAuthHeaders({ token: options.token, json: true }),
2280
+ body: JSON.stringify({ sessionId }),
2281
+ });
2282
+ if (!response.ok) {
2283
+ const result = (await response.json());
2284
+ console.error(`Error: ${result.error}`);
2285
+ process.exit(1);
2286
+ }
2287
+ console.log(`Session ${sessionId} deleted.`);
2288
+ }
2289
+ catch (error) {
2290
+ console.error(`Error: ${error.message}`);
2291
+ process.exit(1);
2292
+ }
2293
+ });
2294
+ cli
2295
+ .command('session reset <sessionId>', 'Reset the browser connection for a session')
2296
+ .option('--host <host>', 'Remote relay server host')
2297
+ .option('--token <token>', 'Authentication token (or use TABWRIGHT_TOKEN env var)')
2298
+ .action(async (sessionId, options) => {
2299
+ const cwd = process.cwd();
2300
+ const serverUrl = await getServerUrl(options.host);
2301
+ if (!options.host && !process.env.TABWRIGHT_HOST) {
2302
+ await ensureRelayServer({ logger: console });
2303
+ }
2304
+ try {
2305
+ const response = await fetch(`${serverUrl}/cli/reset`, {
2306
+ method: 'POST',
2307
+ headers: buildAuthHeaders({ token: options.token, json: true }),
2308
+ body: JSON.stringify({ sessionId, cwd }),
2309
+ });
2310
+ if (!response.ok) {
2311
+ const text = await response.text();
2312
+ console.error(`Error: ${response.status} ${text}`);
2313
+ process.exit(1);
2314
+ }
2315
+ const result = (await response.json());
2316
+ console.log(`Connection reset successfully. ${result.pagesCount} page(s) available. Current page URL: ${result.pageUrl}`);
2317
+ }
2318
+ catch (error) {
2319
+ console.error(`Error: ${error.message}`);
2320
+ process.exit(1);
2321
+ }
2322
+ });
2323
+ cli
2324
+ .command('serve', `Start the relay server on this machine (must be the same host where Chrome is running). Remote clients (Docker, other machines) connect via TABWRIGHT_HOST. Use --host localhost for Docker (no token needed) — containers reach it via host.docker.internal. Use --host 0.0.0.0 for LAN/internet access (requires --token).`)
2325
+ .option('--host [host]', z.string().default('0.0.0.0').describe('Host to bind to (use "localhost" for Docker, "0.0.0.0" for remote access)'))
2326
+ .option('--token <token>', 'Authentication token, required when --host is 0.0.0.0 (or use TABWRIGHT_TOKEN env var)')
2327
+ .option('--replace', 'Kill existing server if running')
2328
+ .action(async (options) => {
2329
+ const token = options.token || process.env.TABWRIGHT_TOKEN;
2330
+ const isPublicHost = options.host === '0.0.0.0' || options.host === '::';
2331
+ if (isPublicHost && !token) {
2332
+ console.error('Error: Authentication token is required when binding to a public host.');
2333
+ console.error('Provide --token <token> or set TABWRIGHT_TOKEN environment variable.');
2334
+ process.exit(1);
2335
+ }
2336
+ // Expose the token to in-process callers so
2337
+ // they can attach Authorization: Bearer ... when calling the relay's own
2338
+ // privileged endpoints. Required because we no longer bypass auth for
2339
+ // loopback — see commit history for the tunnel-agent threat model.
2340
+ if (token) {
2341
+ process.env.TABWRIGHT_TOKEN = token;
2342
+ }
2343
+ // Check if server is already running on the port
2344
+ const net = await import('node:net');
2345
+ const isPortInUse = await new Promise((resolve) => {
2346
+ const socket = new net.Socket();
2347
+ socket.setTimeout(500);
2348
+ socket.on('connect', () => {
2349
+ socket.destroy();
2350
+ resolve(true);
2351
+ });
2352
+ socket.on('timeout', () => {
2353
+ socket.destroy();
2354
+ resolve(false);
2355
+ });
2356
+ socket.on('error', () => {
2357
+ resolve(false);
2358
+ });
2359
+ socket.connect(RELAY_PORT, '127.0.0.1');
2360
+ });
2361
+ if (isPortInUse) {
2362
+ if (!options.replace) {
2363
+ console.log(`Tabwright server is already running on port ${RELAY_PORT}`);
2364
+ console.log('Tip: Use --replace to kill the existing server and start a new one.');
2365
+ process.exit(0);
2366
+ }
2367
+ // Kill existing process on the port
2368
+ console.log(`Killing existing server on port ${RELAY_PORT}...`);
2369
+ await killPortProcess({ port: RELAY_PORT });
2370
+ }
2371
+ // Lazy-load heavy dependencies only when serve command is used
2372
+ const { createFileLogger } = await import('./create-logger.js');
2373
+ const { startTabwrightCDPRelayServer } = await import('./cdp-relay.js');
2374
+ const logger = createFileLogger();
2375
+ process.title = 'tabwright-serve';
2376
+ process.on('uncaughtException', async (err) => {
2377
+ await logger.error('Uncaught Exception:', err);
2378
+ process.exit(1);
2379
+ });
2380
+ process.on('unhandledRejection', async (reason) => {
2381
+ await logger.error('Unhandled Rejection:', reason);
2382
+ process.exit(1);
2383
+ });
2384
+ const server = await startTabwrightCDPRelayServer({
2385
+ port: RELAY_PORT,
2386
+ host: options.host,
2387
+ token,
2388
+ logger,
2389
+ });
2390
+ console.log('Tabwright CDP relay server started');
2391
+ console.log(` Host: ${options.host}`);
2392
+ console.log(` Port: ${RELAY_PORT}`);
2393
+ console.log(` Token: ${token ? '(configured)' : '(none)'}`);
2394
+ console.log(` Logs: ${logger.logFilePath}`);
2395
+ console.log(` CDP Logs: ${LOG_CDP_FILE_PATH}`);
2396
+ console.log('');
2397
+ console.log(`CDP endpoint: http://${options.host}:${RELAY_PORT}${token ? '?token=<token>' : ''}`);
2398
+ console.log('');
2399
+ console.log('Press Ctrl+C to stop.');
2400
+ process.on('SIGINT', () => {
2401
+ console.log('\nShutting down...');
2402
+ server.close();
2403
+ process.exit(0);
2404
+ });
2405
+ process.on('SIGTERM', () => {
2406
+ console.log('\nShutting down...');
2407
+ server.close();
2408
+ process.exit(0);
2409
+ });
2410
+ });
2411
+ cli
2412
+ .command('browser list', 'List all available browsers: extension-connected and direct CDP on port 9222')
2413
+ .option('--host <host>', z.string().describe('Remote relay server host'))
2414
+ .option('--token <token>', 'Authentication token (or use TABWRIGHT_TOKEN env var)')
2415
+ .action(async (options) => {
2416
+ const isLocal = !options.host && !process.env.TABWRIGHT_HOST;
2417
+ // Start relay if local so the extension can connect, then fetch in parallel
2418
+ if (isLocal) {
2419
+ await ensureRelayServer({ logger: console });
2420
+ }
2421
+ const [extensions, directInstances] = await Promise.all([
2422
+ isLocal
2423
+ ? waitForConnectedExtensions({ timeoutMs: 2000, pollIntervalMs: 200, logger: console })
2424
+ : fetchExtensionsStatus({ host: options.host, token: options.token }),
2425
+ isLocal ? discoverChromeInstances() : Promise.resolve([]),
2426
+ ]);
2427
+ const cloudOptions = await discoverCloudBrowsers();
2428
+ // Check if a Chrome binary is available for headless mode
2429
+ const headlessOption = await (async () => {
2430
+ try {
2431
+ const { resolveBrowserExecutablePath } = await import('./browser-config.js');
2432
+ resolveBrowserExecutablePath();
2433
+ return [{
2434
+ key: 'headless',
2435
+ type: 'headless',
2436
+ browser: 'Chrome (Headless)',
2437
+ profile: '-',
2438
+ }];
2439
+ }
2440
+ catch {
2441
+ return [];
2442
+ }
2443
+ })();
2444
+ const allOptions = [
2445
+ ...extensions.map((ext) => {
2446
+ return {
2447
+ key: ext.stableKey || ext.extensionId,
2448
+ type: 'extension',
2449
+ browser: ext.browser || 'Chrome',
2450
+ profile: ext.profile?.email || '(not signed in)',
2451
+ extensionId: ext.extensionId === 'default' ? null : ext.stableKey || ext.extensionId,
2452
+ };
2453
+ }),
2454
+ ...directInstances.map(instanceToBrowserOption),
2455
+ ...headlessOption,
2456
+ ...cloudOptions,
2457
+ ];
2458
+ if (allOptions.length === 0) {
2459
+ console.log('No browsers detected.\n');
2460
+ console.log(' Extension: click the Tabwright icon on a tab to connect');
2461
+ console.log(' Direct: open chrome://inspect/#remote-debugging in Chrome');
2462
+ console.log(' Headless: run `tabwright browser install` then `--browser headless`');
2463
+ console.log(' Cloud: run `tabwright cloud login` to connect cloud browsers');
2464
+ return;
2465
+ }
2466
+ printBrowserTable(allOptions);
2467
+ console.log('');
2468
+ const hasDirectInstances = allOptions.some((opt) => {
2469
+ return opt.type === 'direct';
2470
+ });
2471
+ if (hasDirectInstances) {
2472
+ console.log(pc.dim('Connect with: tabwright session new --direct'));
2473
+ console.log(pc.dim('Chrome may ask to approve the debugging connection.'));
2474
+ }
2475
+ else {
2476
+ console.log(pc.dim('Use with: tabwright session new [--browser <key>]'));
2477
+ }
2478
+ const hasCloud = allOptions.some((opt) => {
2479
+ return opt.type === 'cloud';
2480
+ });
2481
+ if (!hasCloud) {
2482
+ printCloudTip();
2483
+ }
2484
+ });
2485
+ // ── Cloud commands ──────────────────────────────────────────────────
2486
+ cli
2487
+ .command('cloud login', 'Authenticate with playwriter.dev to use cloud browsers')
2488
+ .option('--base-url <url>', 'Website base URL (default: https://playwriter.dev)')
2489
+ .action(async (options) => {
2490
+ const baseUrl = options.baseUrl || process.env.TABWRIGHT_CLOUD_URL || 'https://playwriter.dev';
2491
+ // Use the better-auth client SDK so we don't hardcode endpoint URLs.
2492
+ // Hardcoded URLs broke before when better-auth changed paths between versions.
2493
+ const { createAuthClient } = await import('better-auth/client');
2494
+ const { deviceAuthorizationClient } = await import('better-auth/client/plugins');
2495
+ const client = createAuthClient({
2496
+ baseURL: baseUrl,
2497
+ plugins: [deviceAuthorizationClient()],
2498
+ });
2499
+ console.log('Requesting device authorization...');
2500
+ const { data: deviceData, error: requestError } = await client.device.code({
2501
+ client_id: 'tabwright-cli',
2502
+ });
2503
+ if (requestError || !deviceData) {
2504
+ console.error(`Error: failed to request device code — ${requestError?.error_description || requestError?.error || 'unknown error'}`);
2505
+ process.exit(1);
2506
+ }
2507
+ const verificationUrl = deviceData.verification_uri_complete || `${baseUrl}/device?user_code=${deviceData.user_code}`;
2508
+ console.log(`\nOpen this URL in your browser:\n ${verificationUrl}\n`);
2509
+ console.log(`Code: ${deviceData.user_code}\n`);
2510
+ await openInBrowser(verificationUrl);
2511
+ console.log('Waiting for approval...');
2512
+ const pollInterval = (deviceData.interval || 5) * 1000;
2513
+ const deadline = Date.now() + (deviceData.expires_in || 300) * 1000;
2514
+ while (Date.now() < deadline) {
2515
+ await new Promise((r) => { setTimeout(r, pollInterval); });
2516
+ const { data: tokenData, error: pollError } = await client.device.token({
2517
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
2518
+ device_code: deviceData.device_code,
2519
+ client_id: 'tabwright-cli',
2520
+ });
2521
+ if (tokenData?.access_token) {
2522
+ saveCloudAuth({ token: tokenData.access_token, baseUrl });
2523
+ console.log(pc.green('\nLogged in successfully!'));
2524
+ console.log('Cloud browsers will now appear in `tabwright session new`.');
2525
+ return;
2526
+ }
2527
+ if (pollError?.error === 'authorization_pending' || pollError?.error === 'slow_down') {
2528
+ continue;
2529
+ }
2530
+ if (pollError) {
2531
+ console.error(`\nError: Device authorization failed — ${pollError.error_description || pollError.error}`);
2532
+ process.exit(1);
2533
+ }
2534
+ }
2535
+ console.error('\nError: Device authorization timed out.');
2536
+ process.exit(1);
2537
+ });
2538
+ cli
2539
+ .command('cloud subscribe', 'Open the subscription page to purchase cloud browser sessions')
2540
+ .action(async () => {
2541
+ const auth = loadCloudAuth();
2542
+ if (!auth) {
2543
+ console.error('Not logged in. Run `tabwright cloud login` first.');
2544
+ process.exit(1);
2545
+ }
2546
+ const subscribeUrl = new URL('/dashboard', auth.baseUrl).toString();
2547
+ console.log(`Open your browser to manage your subscription:\n ${subscribeUrl}\n`);
2548
+ await openInBrowser(subscribeUrl);
2549
+ });
2550
+ cli
2551
+ .command('cloud status', 'Show active cloud browser sessions')
2552
+ .action(async () => {
2553
+ const client = getCloudClient();
2554
+ if (!client) {
2555
+ console.error('Not logged in. Run `tabwright cloud login` first.');
2556
+ process.exit(1);
2557
+ }
2558
+ try {
2559
+ const { sessions } = await client.getStatus();
2560
+ if (sessions.length === 0) {
2561
+ console.log('No active cloud sessions.');
2562
+ console.log(pc.dim('Start one with: tabwright session new --browser cloud'));
2563
+ return;
2564
+ }
2565
+ const keyWidth = Math.max(3, ...sessions.map((s) => `cloud-${s.index}`.length));
2566
+ console.log('KEY'.padEnd(keyWidth) + ' ' + 'STATUS'.padEnd(10) + ' ' + 'DETAILS');
2567
+ console.log('-'.repeat(keyWidth + 30));
2568
+ for (const s of sessions) {
2569
+ const key = `cloud-${s.index}`;
2570
+ const timeoutAt = new Date(s.timeoutAt).toLocaleTimeString();
2571
+ console.log(key.padEnd(keyWidth) +
2572
+ ' ' +
2573
+ pc.green('running'.padEnd(10)) +
2574
+ ' ' +
2575
+ `expires ${timeoutAt}`);
2576
+ }
2577
+ }
2578
+ catch (error) {
2579
+ const msg = error instanceof Error ? error.message : String(error);
2580
+ console.error(`Error: ${msg}`);
2581
+ process.exit(1);
2582
+ }
2583
+ });
2584
+ cli
2585
+ .command('cloud live [key]', 'Open a live browser view for an active cloud session')
2586
+ .action(async (key) => {
2587
+ const client = getCloudClient();
2588
+ if (!client) {
2589
+ console.error('Not logged in. Run `tabwright cloud login` first.');
2590
+ process.exit(1);
2591
+ }
2592
+ try {
2593
+ const { sessions } = await client.getStatus();
2594
+ if (sessions.length === 0) {
2595
+ console.log('No active cloud sessions.');
2596
+ console.log(pc.dim('Start one with: tabwright session new --browser cloud'));
2597
+ process.exit(1);
2598
+ }
2599
+ let session;
2600
+ if (key) {
2601
+ // Match by cloud-N key or by cloudSessionId
2602
+ session = sessions.find((s) => {
2603
+ return `cloud-${s.index}` === key || s.cloudSessionId === key || s.browserUseSessionId === key;
2604
+ });
2605
+ if (!session) {
2606
+ console.error(`No active session matching "${key}".`);
2607
+ console.error('Active sessions: ' + sessions.map((s) => { return `cloud-${s.index}`; }).join(', '));
2608
+ process.exit(1);
2609
+ }
2610
+ }
2611
+ else if (sessions.length === 1) {
2612
+ session = sessions[0];
2613
+ }
2614
+ else {
2615
+ console.log('Multiple active sessions. Specify one:\n');
2616
+ for (const s of sessions) {
2617
+ console.log(` cloud-${s.index} (expires ${new Date(s.timeoutAt).toLocaleTimeString()})`);
2618
+ }
2619
+ console.log(`\nUsage: tabwright cloud live cloud-1`);
2620
+ process.exit(1);
2621
+ }
2622
+ if (!session.cdpUrl) {
2623
+ console.error('Session has no CDP URL — it may still be starting.');
2624
+ process.exit(1);
2625
+ }
2626
+ const auth = loadCloudAuth();
2627
+ const liveUrl = buildLiveUrl(session.cdpUrl, auth.baseUrl);
2628
+ console.log(liveUrl);
2629
+ await openInBrowser(liveUrl);
2630
+ }
2631
+ catch (error) {
2632
+ const msg = error instanceof Error ? error.message : String(error);
2633
+ console.error(`Error: ${msg}`);
2634
+ process.exit(1);
2635
+ }
2636
+ });
2637
+ cli.command('logfile', 'Print the path to the relay server log file').action(() => {
2638
+ console.log(`relay: ${LOG_FILE_PATH}`);
2639
+ console.log(`cdp: ${LOG_CDP_FILE_PATH}`);
2640
+ });
2641
+ cli
2642
+ .command('skill install', 'Install the Tabwright agent skill bundled with this CLI')
2643
+ .option('--target <target>', 'Agent skill target (default: codex)')
2644
+ .option('--codex-home <dir>', 'Codex home directory (defaults to CODEX_HOME or ~/.codex)')
2645
+ .option('--force', 'Overwrite an installed Tabwright skill from another CLI build')
2646
+ .option('--json', 'Print JSON')
2647
+ .action((options) => {
2648
+ try {
2649
+ const target = parseTabwrightAgentSkillTarget(options.target);
2650
+ const result = installTabwrightAgentSkill({
2651
+ target,
2652
+ codexHome: options.codexHome,
2653
+ overwrite: options.force,
2654
+ });
2655
+ if (options.json) {
2656
+ console.log(JSON.stringify(result, null, 2));
2657
+ return;
2658
+ }
2659
+ console.log(`Tabwright skill ${result.fileStatus}: ${result.installedPath}`);
2660
+ result.next.forEach((step) => {
2661
+ console.log(`Next: ${step}`);
2662
+ });
2663
+ }
2664
+ catch (error) {
2665
+ exitWithError(error);
2666
+ }
2667
+ });
2668
+ cli
2669
+ .command('skill status', 'Check whether the installed Tabwright agent skill matches this CLI')
2670
+ .option('--target <target>', 'Agent skill target (default: codex)')
2671
+ .option('--codex-home <dir>', 'Codex home directory (defaults to CODEX_HOME or ~/.codex)')
2672
+ .option('--json', 'Print JSON')
2673
+ .action((options) => {
2674
+ try {
2675
+ const status = getTabwrightAgentSkillStatus({
2676
+ target: parseTabwrightAgentSkillTarget(options.target),
2677
+ codexHome: options.codexHome,
2678
+ });
2679
+ if (options.json) {
2680
+ console.log(JSON.stringify(status, null, 2));
2681
+ return;
2682
+ }
2683
+ console.log(`Tabwright skill (${status.target}): ${status.state}`);
2684
+ console.log(`Bundled: ${status.bundledPath}`);
2685
+ console.log(`Installed: ${status.installedPath}`);
2686
+ if (status.state !== 'current') {
2687
+ console.log(`Next: ${status.installCommand}`);
2688
+ }
2689
+ }
2690
+ catch (error) {
2691
+ exitWithError(error);
2692
+ }
2693
+ });
2694
+ cli.command('skill', 'Print the full tabwright usage instructions').action(() => {
2695
+ const skillPath = path.join(__dirname, '..', 'src', 'skill.md');
2696
+ const content = fs.readFileSync(skillPath, 'utf-8');
2697
+ console.log(content);
2698
+ });
2699
+ function parseTabwrightAgentSkillTarget(value) {
2700
+ if (!value || value === 'codex') {
2701
+ return 'codex';
2702
+ }
2703
+ throw new Error(`Unsupported Tabwright skill target: ${value}. Expected codex.`);
2704
+ }
2705
+ cli.help();
2706
+ cli.completions();
2707
+ cli.version(VERSION);
2708
+ await cli.parse();
2709
+ //# sourceMappingURL=cli.js.map