@equinor/fusion-framework-cli-plugin-copilot 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/esm/commands/app/{session-logger.js → attach-session-logger.js} +16 -2
  3. package/dist/esm/commands/app/attach-session-logger.js.map +1 -0
  4. package/dist/esm/commands/app/{command.js → copilot-command.js} +22 -11
  5. package/dist/esm/commands/app/copilot-command.js.map +1 -0
  6. package/dist/esm/commands/app/{eval.js → create-session.js} +7 -5
  7. package/dist/esm/commands/app/create-session.js.map +1 -0
  8. package/dist/esm/commands/app/format-plan.js +24 -0
  9. package/dist/esm/commands/app/format-plan.js.map +1 -0
  10. package/dist/esm/commands/app/format-step-result.js +14 -0
  11. package/dist/esm/commands/app/format-step-result.js.map +1 -0
  12. package/dist/esm/commands/app/format-verdict.js +38 -0
  13. package/dist/esm/commands/app/format-verdict.js.map +1 -0
  14. package/dist/esm/commands/app/prompts/{system.dev-server.prompt.js → create-dev-server-context.js} +1 -1
  15. package/dist/esm/commands/app/prompts/create-dev-server-context.js.map +1 -0
  16. package/dist/esm/commands/app/prompts/{execute-step.prompt.js → create-execute-step-prompt.js} +1 -1
  17. package/dist/esm/commands/app/prompts/create-execute-step-prompt.js.map +1 -0
  18. package/dist/esm/commands/app/prompts/{judge.prompt.js → create-judge-prompt.js} +5 -1
  19. package/dist/esm/commands/app/prompts/create-judge-prompt.js.map +1 -0
  20. package/dist/esm/commands/app/prompts/{plan.prompt.js → create-plan-prompt.js} +1 -1
  21. package/dist/esm/commands/app/prompts/create-plan-prompt.js.map +1 -0
  22. package/dist/esm/commands/app/prompts/{step.prompt.js → create-step-prompt.js} +3 -2
  23. package/dist/esm/commands/app/prompts/create-step-prompt.js.map +1 -0
  24. package/dist/esm/commands/app/prompts/{system.prompt.js → create-system-prompt.js} +1 -1
  25. package/dist/esm/commands/app/prompts/create-system-prompt.js.map +1 -0
  26. package/dist/esm/commands/app/{login.js → run-login.js} +5 -1
  27. package/dist/esm/commands/app/run-login.js.map +1 -0
  28. package/dist/esm/commands/app/{server.js → start-app-server.js} +7 -1
  29. package/dist/esm/commands/app/start-app-server.js.map +1 -0
  30. package/dist/esm/commands/app/status-icon.js +18 -0
  31. package/dist/esm/commands/app/status-icon.js.map +1 -0
  32. package/dist/esm/commands/app/tools/{registry.js → AgentBrowserToolRegistry.js} +1 -1
  33. package/dist/esm/commands/app/tools/AgentBrowserToolRegistry.js.map +1 -0
  34. package/dist/esm/commands/app/tools/{write-file.js → create-append-file-tool.js} +5 -38
  35. package/dist/esm/commands/app/tools/create-append-file-tool.js.map +1 -0
  36. package/dist/esm/commands/app/tools/{click.js → create-click-tool.js} +1 -1
  37. package/dist/esm/commands/app/tools/create-click-tool.js.map +1 -0
  38. package/dist/esm/commands/app/tools/{errors.js → create-errors-tool.js} +1 -1
  39. package/dist/esm/commands/app/tools/create-errors-tool.js.map +1 -0
  40. package/dist/esm/commands/app/tools/{eval-js.js → create-eval-js-tool.js} +1 -1
  41. package/dist/esm/commands/app/tools/create-eval-js-tool.js.map +1 -0
  42. package/dist/esm/commands/app/tools/{fill.js → create-fill-tool.js} +1 -1
  43. package/dist/esm/commands/app/tools/create-fill-tool.js.map +1 -0
  44. package/dist/esm/commands/app/tools/{find.js → create-find-tool.js} +4 -1
  45. package/dist/esm/commands/app/tools/create-find-tool.js.map +1 -0
  46. package/dist/esm/commands/app/tools/{get-styles.js → create-get-styles-tool.js} +1 -1
  47. package/dist/esm/commands/app/tools/create-get-styles-tool.js.map +1 -0
  48. package/dist/esm/commands/app/tools/{get-url.js → create-get-url-tool.js} +1 -1
  49. package/dist/esm/commands/app/tools/create-get-url-tool.js.map +1 -0
  50. package/dist/esm/commands/app/tools/{go-back.js → create-go-back-tool.js} +1 -1
  51. package/dist/esm/commands/app/tools/create-go-back-tool.js.map +1 -0
  52. package/dist/esm/commands/app/tools/{hover.js → create-hover-tool.js} +1 -1
  53. package/dist/esm/commands/app/tools/create-hover-tool.js.map +1 -0
  54. package/dist/esm/commands/app/tools/{navigate.js → create-navigate-tool.js} +1 -1
  55. package/dist/esm/commands/app/tools/create-navigate-tool.js.map +1 -0
  56. package/dist/esm/commands/app/tools/{press-key.js → create-press-key-tool.js} +1 -1
  57. package/dist/esm/commands/app/tools/create-press-key-tool.js.map +1 -0
  58. package/dist/esm/commands/app/tools/{read-file.js → create-read-file-tool.js} +4 -2
  59. package/dist/esm/commands/app/tools/create-read-file-tool.js.map +1 -0
  60. package/dist/esm/commands/app/tools/{reload.js → create-reload-tool.js} +1 -1
  61. package/dist/esm/commands/app/tools/create-reload-tool.js.map +1 -0
  62. package/dist/esm/commands/app/tools/{screenshot.js → create-screenshot-tool.js} +7 -1
  63. package/dist/esm/commands/app/tools/create-screenshot-tool.js.map +1 -0
  64. package/dist/esm/commands/app/tools/{scroll.js → create-scroll-tool.js} +3 -1
  65. package/dist/esm/commands/app/tools/create-scroll-tool.js.map +1 -0
  66. package/dist/esm/commands/app/tools/{select.js → create-select-tool.js} +1 -1
  67. package/dist/esm/commands/app/tools/create-select-tool.js.map +1 -0
  68. package/dist/esm/commands/app/tools/{snapshot.js → create-snapshot-tool.js} +1 -1
  69. package/dist/esm/commands/app/tools/create-snapshot-tool.js.map +1 -0
  70. package/dist/esm/commands/app/tools/{type-text.js → create-type-text-tool.js} +1 -1
  71. package/dist/esm/commands/app/tools/create-type-text-tool.js.map +1 -0
  72. package/dist/esm/commands/app/tools/{wait.js → create-wait-tool.js} +5 -1
  73. package/dist/esm/commands/app/tools/create-wait-tool.js.map +1 -0
  74. package/dist/esm/commands/app/tools/create-write-file-tool.js +65 -0
  75. package/dist/esm/commands/app/tools/create-write-file-tool.js.map +1 -0
  76. package/dist/esm/commands/app/tools/index.js +19 -19
  77. package/dist/esm/commands/app/tools/index.js.map +1 -1
  78. package/dist/esm/commands/app/truncate.js +11 -0
  79. package/dist/esm/commands/app/truncate.js.map +1 -0
  80. package/dist/esm/commands/app/try-format-message.js +39 -0
  81. package/dist/esm/commands/app/try-format-message.js.map +1 -0
  82. package/dist/esm/exit-with-resolution-error.js +16 -0
  83. package/dist/esm/exit-with-resolution-error.js.map +1 -0
  84. package/dist/esm/index.js +1 -1
  85. package/dist/esm/index.js.map +1 -1
  86. package/dist/esm/resolve-eval-command-input.js +37 -0
  87. package/dist/esm/resolve-eval-command-input.js.map +1 -0
  88. package/dist/esm/resolve-eval-files.js +49 -0
  89. package/dist/esm/resolve-eval-files.js.map +1 -0
  90. package/dist/esm/utils/ab-error-message.js +34 -0
  91. package/dist/esm/utils/ab-error-message.js.map +1 -0
  92. package/dist/esm/utils/{agent-browser.js → ab.js} +3 -32
  93. package/dist/esm/utils/ab.js.map +1 -0
  94. package/dist/esm/utils/create-run-dir.js +24 -0
  95. package/dist/esm/utils/create-run-dir.js.map +1 -0
  96. package/dist/esm/utils/{process.js → helpers.js} +28 -11
  97. package/dist/esm/utils/helpers.js.map +1 -0
  98. package/dist/esm/utils/index.js +8 -5
  99. package/dist/esm/utils/index.js.map +1 -1
  100. package/dist/esm/utils/{browser-poll.js → poll-console.js} +5 -3
  101. package/dist/esm/utils/poll-console.js.map +1 -0
  102. package/dist/esm/utils/{daemon.js → reset-daemon.js} +5 -14
  103. package/dist/esm/utils/reset-daemon.js.map +1 -0
  104. package/dist/esm/utils/resolve-app-key.js +20 -0
  105. package/dist/esm/utils/resolve-app-key.js.map +1 -0
  106. package/dist/esm/utils/wait-for-server.js +26 -0
  107. package/dist/esm/utils/wait-for-server.js.map +1 -0
  108. package/dist/esm/version.js +1 -1
  109. package/dist/tsconfig.tsbuildinfo +1 -1
  110. package/dist/types/commands/app/format-plan.d.ts +8 -0
  111. package/dist/types/commands/app/format-step-result.d.ts +8 -0
  112. package/dist/types/commands/app/format-verdict.d.ts +8 -0
  113. package/dist/types/commands/app/prompts/{step.prompt.d.ts → create-step-prompt.d.ts} +1 -1
  114. package/dist/types/commands/app/status-icon.d.ts +8 -0
  115. package/dist/types/commands/app/tools/create-append-file-tool.d.ts +10 -0
  116. package/dist/types/commands/app/tools/{write-file.d.ts → create-write-file-tool.d.ts} +0 -9
  117. package/dist/types/commands/app/truncate.d.ts +8 -0
  118. package/dist/types/commands/app/try-format-message.d.ts +10 -0
  119. package/dist/types/exit-with-resolution-error.d.ts +8 -0
  120. package/dist/types/{eval-resolve.d.ts → resolve-eval-command-input.d.ts} +0 -8
  121. package/dist/types/resolve-eval-files.d.ts +8 -0
  122. package/dist/types/utils/ab-error-message.d.ts +11 -0
  123. package/dist/types/utils/{agent-browser.d.ts → ab.d.ts} +0 -13
  124. package/dist/types/utils/create-run-dir.d.ts +8 -0
  125. package/dist/types/utils/{process.d.ts → helpers.d.ts} +13 -6
  126. package/dist/types/utils/index.d.ts +8 -5
  127. package/dist/types/utils/{daemon.d.ts → reset-daemon.d.ts} +0 -5
  128. package/dist/types/utils/resolve-app-key.d.ts +9 -0
  129. package/dist/types/utils/wait-for-server.d.ts +9 -0
  130. package/dist/types/version.d.ts +1 -1
  131. package/package.json +4 -4
  132. package/src/commands/app/{session-logger.ts → attach-session-logger.ts} +15 -1
  133. package/src/commands/app/{command.ts → copilot-command.ts} +21 -12
  134. package/src/commands/app/{eval.ts → create-session.ts} +6 -4
  135. package/src/commands/app/format-plan.ts +26 -0
  136. package/src/commands/app/format-step-result.ts +16 -0
  137. package/src/commands/app/format-verdict.ts +43 -0
  138. package/src/commands/app/prompts/{judge.prompt.ts → create-judge-prompt.ts} +4 -0
  139. package/src/commands/app/prompts/{step.prompt.ts → create-step-prompt.ts} +2 -1
  140. package/src/commands/app/{login.ts → run-login.ts} +4 -0
  141. package/src/commands/app/{server.ts → start-app-server.ts} +6 -0
  142. package/src/commands/app/status-icon.ts +16 -0
  143. package/src/commands/app/tools/{write-file.ts → create-append-file-tool.ts} +4 -39
  144. package/src/commands/app/tools/{find.ts → create-find-tool.ts} +3 -0
  145. package/src/commands/app/tools/{read-file.ts → create-read-file-tool.ts} +3 -1
  146. package/src/commands/app/tools/{screenshot.ts → create-screenshot-tool.ts} +6 -0
  147. package/src/commands/app/tools/{scroll.ts → create-scroll-tool.ts} +2 -0
  148. package/src/commands/app/tools/{wait.ts → create-wait-tool.ts} +4 -0
  149. package/src/commands/app/tools/create-write-file-tool.ts +69 -0
  150. package/src/commands/app/tools/index.ts +19 -19
  151. package/src/commands/app/truncate.ts +10 -0
  152. package/src/commands/app/try-format-message.ts +42 -0
  153. package/src/exit-with-resolution-error.ts +15 -0
  154. package/src/index.ts +1 -1
  155. package/src/resolve-eval-command-input.ts +59 -0
  156. package/src/resolve-eval-files.ts +57 -0
  157. package/src/utils/ab-error-message.ts +31 -0
  158. package/src/utils/{agent-browser.ts → ab.ts} +2 -30
  159. package/src/utils/create-run-dir.ts +24 -0
  160. package/src/utils/{process.ts → helpers.ts} +28 -10
  161. package/src/utils/index.ts +8 -5
  162. package/src/utils/{browser-poll.ts → poll-console.ts} +4 -2
  163. package/src/utils/{daemon.ts → reset-daemon.ts} +4 -13
  164. package/src/utils/resolve-app-key.ts +20 -0
  165. package/src/utils/wait-for-server.ts +24 -0
  166. package/src/version.ts +1 -1
  167. package/dist/esm/commands/app/command.js.map +0 -1
  168. package/dist/esm/commands/app/eval.js.map +0 -1
  169. package/dist/esm/commands/app/format.js +0 -93
  170. package/dist/esm/commands/app/format.js.map +0 -1
  171. package/dist/esm/commands/app/login.js.map +0 -1
  172. package/dist/esm/commands/app/prompts/execute-step.prompt.js.map +0 -1
  173. package/dist/esm/commands/app/prompts/judge.prompt.js.map +0 -1
  174. package/dist/esm/commands/app/prompts/plan.prompt.js.map +0 -1
  175. package/dist/esm/commands/app/prompts/step.prompt.js.map +0 -1
  176. package/dist/esm/commands/app/prompts/system.dev-server.prompt.js.map +0 -1
  177. package/dist/esm/commands/app/prompts/system.prompt.js.map +0 -1
  178. package/dist/esm/commands/app/server.js.map +0 -1
  179. package/dist/esm/commands/app/session-logger.js.map +0 -1
  180. package/dist/esm/commands/app/tools/click.js.map +0 -1
  181. package/dist/esm/commands/app/tools/errors.js.map +0 -1
  182. package/dist/esm/commands/app/tools/eval-js.js.map +0 -1
  183. package/dist/esm/commands/app/tools/fill.js.map +0 -1
  184. package/dist/esm/commands/app/tools/find.js.map +0 -1
  185. package/dist/esm/commands/app/tools/get-styles.js.map +0 -1
  186. package/dist/esm/commands/app/tools/get-url.js.map +0 -1
  187. package/dist/esm/commands/app/tools/go-back.js.map +0 -1
  188. package/dist/esm/commands/app/tools/hover.js.map +0 -1
  189. package/dist/esm/commands/app/tools/navigate.js.map +0 -1
  190. package/dist/esm/commands/app/tools/press-key.js.map +0 -1
  191. package/dist/esm/commands/app/tools/read-file.js.map +0 -1
  192. package/dist/esm/commands/app/tools/registry.js.map +0 -1
  193. package/dist/esm/commands/app/tools/reload.js.map +0 -1
  194. package/dist/esm/commands/app/tools/screenshot.js.map +0 -1
  195. package/dist/esm/commands/app/tools/scroll.js.map +0 -1
  196. package/dist/esm/commands/app/tools/select.js.map +0 -1
  197. package/dist/esm/commands/app/tools/snapshot.js.map +0 -1
  198. package/dist/esm/commands/app/tools/type-text.js.map +0 -1
  199. package/dist/esm/commands/app/tools/wait.js.map +0 -1
  200. package/dist/esm/commands/app/tools/write-file.js.map +0 -1
  201. package/dist/esm/eval-resolve.js +0 -83
  202. package/dist/esm/eval-resolve.js.map +0 -1
  203. package/dist/esm/utils/agent-browser.js.map +0 -1
  204. package/dist/esm/utils/browser-poll.js.map +0 -1
  205. package/dist/esm/utils/daemon.js.map +0 -1
  206. package/dist/esm/utils/process.js.map +0 -1
  207. package/dist/esm/utils/server.js +0 -65
  208. package/dist/esm/utils/server.js.map +0 -1
  209. package/dist/types/commands/app/format.d.ts +0 -14
  210. package/dist/types/utils/server.d.ts +0 -26
  211. package/src/commands/app/format.ts +0 -110
  212. package/src/eval-resolve.ts +0 -113
  213. package/src/utils/server.ts +0 -66
  214. /package/dist/types/commands/app/{session-logger.d.ts → attach-session-logger.d.ts} +0 -0
  215. /package/dist/types/commands/app/{command.d.ts → copilot-command.d.ts} +0 -0
  216. /package/dist/types/commands/app/{eval.d.ts → create-session.d.ts} +0 -0
  217. /package/dist/types/commands/app/prompts/{system.dev-server.prompt.d.ts → create-dev-server-context.d.ts} +0 -0
  218. /package/dist/types/commands/app/prompts/{execute-step.prompt.d.ts → create-execute-step-prompt.d.ts} +0 -0
  219. /package/dist/types/commands/app/prompts/{judge.prompt.d.ts → create-judge-prompt.d.ts} +0 -0
  220. /package/dist/types/commands/app/prompts/{plan.prompt.d.ts → create-plan-prompt.d.ts} +0 -0
  221. /package/dist/types/commands/app/prompts/{system.prompt.d.ts → create-system-prompt.d.ts} +0 -0
  222. /package/dist/types/commands/app/{login.d.ts → run-login.d.ts} +0 -0
  223. /package/dist/types/commands/app/{server.d.ts → start-app-server.d.ts} +0 -0
  224. /package/dist/types/commands/app/tools/{registry.d.ts → AgentBrowserToolRegistry.d.ts} +0 -0
  225. /package/dist/types/commands/app/tools/{click.d.ts → create-click-tool.d.ts} +0 -0
  226. /package/dist/types/commands/app/tools/{errors.d.ts → create-errors-tool.d.ts} +0 -0
  227. /package/dist/types/commands/app/tools/{eval-js.d.ts → create-eval-js-tool.d.ts} +0 -0
  228. /package/dist/types/commands/app/tools/{fill.d.ts → create-fill-tool.d.ts} +0 -0
  229. /package/dist/types/commands/app/tools/{find.d.ts → create-find-tool.d.ts} +0 -0
  230. /package/dist/types/commands/app/tools/{get-styles.d.ts → create-get-styles-tool.d.ts} +0 -0
  231. /package/dist/types/commands/app/tools/{get-url.d.ts → create-get-url-tool.d.ts} +0 -0
  232. /package/dist/types/commands/app/tools/{go-back.d.ts → create-go-back-tool.d.ts} +0 -0
  233. /package/dist/types/commands/app/tools/{hover.d.ts → create-hover-tool.d.ts} +0 -0
  234. /package/dist/types/commands/app/tools/{navigate.d.ts → create-navigate-tool.d.ts} +0 -0
  235. /package/dist/types/commands/app/tools/{press-key.d.ts → create-press-key-tool.d.ts} +0 -0
  236. /package/dist/types/commands/app/tools/{read-file.d.ts → create-read-file-tool.d.ts} +0 -0
  237. /package/dist/types/commands/app/tools/{reload.d.ts → create-reload-tool.d.ts} +0 -0
  238. /package/dist/types/commands/app/tools/{screenshot.d.ts → create-screenshot-tool.d.ts} +0 -0
  239. /package/dist/types/commands/app/tools/{scroll.d.ts → create-scroll-tool.d.ts} +0 -0
  240. /package/dist/types/commands/app/tools/{select.d.ts → create-select-tool.d.ts} +0 -0
  241. /package/dist/types/commands/app/tools/{snapshot.d.ts → create-snapshot-tool.d.ts} +0 -0
  242. /package/dist/types/commands/app/tools/{type-text.d.ts → create-type-text-tool.d.ts} +0 -0
  243. /package/dist/types/commands/app/tools/{wait.d.ts → create-wait-tool.d.ts} +0 -0
  244. /package/dist/types/utils/{browser-poll.d.ts → poll-console.d.ts} +0 -0
  245. /package/src/commands/app/prompts/{system.dev-server.prompt.ts → create-dev-server-context.ts} +0 -0
  246. /package/src/commands/app/prompts/{execute-step.prompt.ts → create-execute-step-prompt.ts} +0 -0
  247. /package/src/commands/app/prompts/{plan.prompt.ts → create-plan-prompt.ts} +0 -0
  248. /package/src/commands/app/prompts/{system.prompt.ts → create-system-prompt.ts} +0 -0
  249. /package/src/commands/app/tools/{registry.ts → AgentBrowserToolRegistry.ts} +0 -0
  250. /package/src/commands/app/tools/{click.ts → create-click-tool.ts} +0 -0
  251. /package/src/commands/app/tools/{errors.ts → create-errors-tool.ts} +0 -0
  252. /package/src/commands/app/tools/{eval-js.ts → create-eval-js-tool.ts} +0 -0
  253. /package/src/commands/app/tools/{fill.ts → create-fill-tool.ts} +0 -0
  254. /package/src/commands/app/tools/{get-styles.ts → create-get-styles-tool.ts} +0 -0
  255. /package/src/commands/app/tools/{get-url.ts → create-get-url-tool.ts} +0 -0
  256. /package/src/commands/app/tools/{go-back.ts → create-go-back-tool.ts} +0 -0
  257. /package/src/commands/app/tools/{hover.ts → create-hover-tool.ts} +0 -0
  258. /package/src/commands/app/tools/{navigate.ts → create-navigate-tool.ts} +0 -0
  259. /package/src/commands/app/tools/{press-key.ts → create-press-key-tool.ts} +0 -0
  260. /package/src/commands/app/tools/{reload.ts → create-reload-tool.ts} +0 -0
  261. /package/src/commands/app/tools/{select.ts → create-select-tool.ts} +0 -0
  262. /package/src/commands/app/tools/{snapshot.ts → create-snapshot-tool.ts} +0 -0
  263. /package/src/commands/app/tools/{type-text.ts → create-type-text-tool.ts} +0 -0
@@ -28,11 +28,13 @@ import {
28
28
  */
29
29
  export async function runLogin(absAppPath: string, port: number, host: string): Promise<void> {
30
30
  const pkgJsonPath = join(absAppPath, 'package.json');
31
+ // Login needs a valid Fusion app directory; bail out clearly if this isn't one
31
32
  if (!existsSync(pkgJsonPath)) {
32
33
  console.error(`❌ No package.json found at ${absAppPath}`);
33
34
  process.exit(1);
34
35
  }
35
36
  const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { name?: string };
37
+ // The app name is required to resolve the appKey later
36
38
  if (!pkgJson.name) {
37
39
  console.error(`❌ package.json at ${absAppPath} has no "name" field`);
38
40
  process.exit(1);
@@ -51,6 +53,7 @@ export async function runLogin(absAppPath: string, port: number, host: string):
51
53
 
52
54
  const serverUrl = `http://localhost:${port}`;
53
55
  const ready = await waitForServer(serverUrl, 60);
56
+ // Abort login when the dev server never comes up within the timeout
54
57
  if (!ready) {
55
58
  console.error('❌ Server failed to start within 60s');
56
59
  cleanup(serverProcess);
@@ -79,6 +82,7 @@ export async function runLogin(absAppPath: string, port: number, host: string):
79
82
  300_000,
80
83
  );
81
84
 
85
+ // Report success or a soft timeout without failing the login flow
82
86
  if (initialized) {
83
87
  await sleep(2000);
84
88
  console.log('✅ Login complete — session saved.');
@@ -22,10 +22,12 @@ export interface AppServerResult {
22
22
  */
23
23
  function freePort(port: number): void {
24
24
  try {
25
+ // Drop the empty string lsof emits when no matching process was found
25
26
  const pids = execSync(`lsof -ti tcp:${port}`, { encoding: 'utf-8' })
26
27
  .trim()
27
28
  .split('\n')
28
29
  .filter(Boolean);
30
+ // Terminate each stale process bound to the port
29
31
  for (const pid of pids) {
30
32
  try {
31
33
  process.kill(parseInt(pid, 10), 'SIGTERM');
@@ -33,6 +35,7 @@ function freePort(port: number): void {
33
35
  // already dead
34
36
  }
35
37
  }
38
+ // Only log when we actually killed something
36
39
  if (pids.length) {
37
40
  console.log(`🔄 Killed stale process(es) on port ${port}: ${pids.join(', ')}`);
38
41
  }
@@ -63,11 +66,13 @@ export async function startAppServer(
63
66
  }
64
67
 
65
68
  const pkgJsonPath = join(absAppPath, 'package.json');
69
+ // Startup needs a valid Fusion app directory; bail out clearly if this isn't one
66
70
  if (!existsSync(pkgJsonPath)) {
67
71
  console.error(`❌ No package.json found at ${absAppPath}`);
68
72
  process.exit(1);
69
73
  }
70
74
  const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { name?: string };
75
+ // The app name is required to resolve the appKey later
71
76
  if (!pkgJson.name) {
72
77
  console.error(`❌ package.json at ${absAppPath} has no "name" field`);
73
78
  process.exit(1);
@@ -92,6 +97,7 @@ export async function startAppServer(
92
97
 
93
98
  const serverUrl = `http://localhost:${options.port}`;
94
99
  const ready = await waitForServer(serverUrl, 60);
100
+ // Abort startup when the dev server never comes up within the timeout
95
101
  if (!ready) {
96
102
  console.error('❌ Server failed to start within 60s');
97
103
  cleanup(serverProcess);
@@ -0,0 +1,16 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Renders a status glyph for a step or verdict outcome.
5
+ *
6
+ * @param ok - Outcome value: `true` (pass), `false` (fail), or a string label
7
+ * such as `'blocked'` or `'flaky'` for anything in between.
8
+ * @returns A single coloured glyph representing the outcome.
9
+ */
10
+ export function statusIcon(ok: boolean | string): string {
11
+ // A literal boolean outcome maps directly to pass/fail glyphs
12
+ if (ok === true) return chalk.green('✔');
13
+ // An explicit failure gets its own glyph, distinct from unknown/other states
14
+ if (ok === false) return chalk.red('✖');
15
+ return chalk.yellow('⚠'); // "blocked", "flaky", etc.
16
+ }
@@ -1,4 +1,4 @@
1
- import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
1
+ import { appendFileSync, mkdirSync } from 'node:fs';
2
2
  import { dirname, isAbsolute, join } from 'node:path';
3
3
 
4
4
  import type { DefineTool } from './types.js';
@@ -18,55 +18,20 @@ import type { DefineTool } from './types.js';
18
18
  function resolveSafe(baseDir: string, input: string): string {
19
19
  // If the model echoed the full absolute path from the prompt, strip the base prefix.
20
20
  const relative = isAbsolute(input)
21
- ? input.startsWith(baseDir + '/') || input.startsWith(baseDir + '\\')
21
+ ? input.startsWith(`${baseDir}/`) || input.startsWith(`${baseDir}\\`)
22
22
  ? input.slice(baseDir.length + 1)
23
23
  : (() => {
24
24
  throw new Error(`Absolute path outside output directory: ${input}`);
25
25
  })()
26
26
  : input;
27
27
  const resolved = join(baseDir, relative);
28
+ // Reject any resolved path that escapes the allowed base directory
28
29
  if (!resolved.startsWith(baseDir)) {
29
30
  throw new Error(`Path escapes the output directory: ${input}`);
30
31
  }
31
32
  return resolved;
32
33
  }
33
34
 
34
- /**
35
- * Creates the write-file tool so the agent can save artifacts
36
- * (plan.json, verdict.json, etc.) to the run output directory.
37
- *
38
- * @param baseDir - The run output directory that scopes all writes
39
- * @param defineTool - Copilot SDK helper used to declare tools
40
- * @returns Copilot tool definition for writing a file
41
- */
42
- export function createWriteFileTool(baseDir: string, defineTool: DefineTool) {
43
- return defineTool('write_file', {
44
- description:
45
- 'Write content to a file in the output directory. Creates parent directories as needed.',
46
- parameters: {
47
- type: 'object' as const,
48
- properties: {
49
- path: {
50
- type: 'string',
51
- description: 'File path relative to the output directory (e.g. "plan.json")',
52
- },
53
- content: {
54
- type: 'string',
55
- description: 'The full file content to write',
56
- },
57
- },
58
- required: ['path', 'content'],
59
- },
60
- handler: async (args) => {
61
- const { path, content } = args as { path: string; content: string };
62
- const resolved = resolveSafe(baseDir, path);
63
- mkdirSync(dirname(resolved), { recursive: true });
64
- writeFileSync(resolved, content);
65
- return `Wrote ${content.length} bytes to ${path}`;
66
- },
67
- });
68
- }
69
-
70
35
  /**
71
36
  * Creates the append-file tool so the agent can append lines
72
37
  * to artifacts like executions.jsonl.
@@ -97,7 +62,7 @@ export function createAppendFileTool(baseDir: string, defineTool: DefineTool) {
97
62
  const { path, content } = args as { path: string; content: string };
98
63
  const resolved = resolveSafe(baseDir, path);
99
64
  mkdirSync(dirname(resolved), { recursive: true });
100
- appendFileSync(resolved, content.endsWith('\n') ? content : content + '\n');
65
+ appendFileSync(resolved, content.endsWith('\n') ? content : `${content}\n`);
101
66
  return `Appended to ${path}`;
102
67
  },
103
68
  });
@@ -40,8 +40,11 @@ export function createFindTool(context: AgentBrowserToolContext, defineTool: Def
40
40
  fillValue?: string;
41
41
  };
42
42
  const command = ['find', by, value];
43
+ // Append optional refinements only when the caller actually provided them
43
44
  if (action) command.push(action);
45
+ // Attach the accessible-name filter when the caller supplied one
44
46
  if (name) command.push('--name', name);
47
+ // Attach the fill value when the action is 'fill'
45
48
  if (fillValue) command.push(fillValue);
46
49
  return context.invoke(command);
47
50
  },
@@ -27,13 +27,14 @@ const IMAGE_MIME: Record<string, string> = {
27
27
  */
28
28
  function resolveSafe(baseDir: string, input: string): string {
29
29
  const relative = isAbsolute(input)
30
- ? input.startsWith(baseDir + '/') || input.startsWith(baseDir + '\\')
30
+ ? input.startsWith(`${baseDir}/`) || input.startsWith(`${baseDir}\\`)
31
31
  ? input.slice(baseDir.length + 1)
32
32
  : (() => {
33
33
  throw new Error(`Absolute path outside output directory: ${input}`);
34
34
  })()
35
35
  : input;
36
36
  const resolved = join(baseDir, relative);
37
+ // Reject any resolved path that escapes the allowed base directory
37
38
  if (!resolved.startsWith(baseDir)) {
38
39
  throw new Error(`Path escapes the output directory: ${input}`);
39
40
  }
@@ -72,6 +73,7 @@ export function createReadFileTool(baseDir: string, defineTool: DefineTool) {
72
73
  const { path } = args as { path: string };
73
74
  const resolved = resolveSafe(baseDir, path);
74
75
 
76
+ // Report a friendly message instead of throwing when the path doesn't exist
75
77
  if (!existsSync(resolved)) {
76
78
  return `File not found: ${path}`;
77
79
  }
@@ -26,15 +26,18 @@ function resolveScreenshotArtifactName(
26
26
  requestedPath: string | undefined,
27
27
  format: 'jpeg' | 'png',
28
28
  ): string | undefined {
29
+ // No requested path means the caller doesn't need a specific artifact name
29
30
  if (!requestedPath) return undefined;
30
31
  const fileName = basename(requestedPath);
31
32
  const currentExtension = extname(fileName).toLowerCase();
32
33
  const expectedExtension = format === 'jpeg' ? '.jpg' : '.png';
33
34
 
35
+ // The extension already matches the selected format; reuse it verbatim
34
36
  if (currentExtension === expectedExtension) {
35
37
  return fileName;
36
38
  }
37
39
 
40
+ // Swap a mismatched extension for the one that matches the selected format
38
41
  if (currentExtension.length > 0) {
39
42
  return `${fileName.slice(0, -currentExtension.length)}${expectedExtension}`;
40
43
  }
@@ -53,6 +56,7 @@ function resolveScreenshotDirectory(
53
56
  evidenceDir: string,
54
57
  requestedDirectory: string | undefined,
55
58
  ): string {
59
+ // No requested directory means the default evidence directory is used
56
60
  if (!requestedDirectory) {
57
61
  return evidenceDir;
58
62
  }
@@ -150,10 +154,12 @@ export function createScreenshotTool(context: AgentBrowserToolContext, defineToo
150
154
  screenshotDirectory,
151
155
  ];
152
156
 
157
+ // Overlay numbered element labels only when the model explicitly asked for it
153
158
  if (annotate) {
154
159
  command.push('--annotate');
155
160
  }
156
161
 
162
+ // JPEG quality only applies when the selected format is actually JPEG
157
163
  if (screenshotFormat === 'jpeg') {
158
164
  const screenshotQuality = Math.min(100, Math.max(0, quality ?? 60));
159
165
  command.push('--screenshot-quality', String(screenshotQuality));
@@ -33,11 +33,13 @@ export function createScrollTool(context: AgentBrowserToolContext, defineTool: D
33
33
  amount?: number;
34
34
  target?: string;
35
35
  };
36
+ // A target ref overrides plain direction/amount scrolling
36
37
  if (target) {
37
38
  return context.invoke(['scrollintoview', target]);
38
39
  }
39
40
  const scrollDirection = direction ?? 'down';
40
41
  const command = ['scroll', scrollDirection];
42
+ // Only pass an explicit pixel amount when the caller provided one
41
43
  if (amount) command.push(String(amount));
42
44
  return context.invoke(command);
43
45
  },
@@ -30,9 +30,13 @@ export function createWaitTool(context: AgentBrowserToolContext, defineTool: Def
30
30
  load?: string;
31
31
  timeout?: number;
32
32
  };
33
+ // Prefer whichever single wait condition the caller specified, in priority order
33
34
  if (text) return context.invoke(['wait', '--text', text], 60_000);
35
+ // Fall back to waiting on a page load state
34
36
  if (load) return context.invoke(['wait', '--load', load], 60_000);
37
+ // Fall back to waiting on a specific element/selector
35
38
  if (selector) return context.invoke(['wait', selector], 60_000);
39
+ // A plain timeout falls back to a bare delay instead of an invoke command
36
40
  if (timeout) {
37
41
  await new Promise((resolve) => setTimeout(resolve, timeout));
38
42
  return `Waited ${timeout}ms`;
@@ -0,0 +1,69 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, isAbsolute, join } from 'node:path';
3
+
4
+ import type { DefineTool } from './types.js';
5
+
6
+ /**
7
+ * Resolves a path within a base directory, rejecting escapes.
8
+ *
9
+ * Accepts both relative paths and absolute paths that fall within
10
+ * the base directory. Strips the base prefix from absolute paths
11
+ * before resolving to handle models that echo full paths from prompts.
12
+ *
13
+ * @param baseDir - The allowed root directory
14
+ * @param input - The user-provided path (relative or absolute within baseDir)
15
+ * @returns The resolved absolute path
16
+ * @throws {Error} When the path escapes the base directory
17
+ */
18
+ function resolveSafe(baseDir: string, input: string): string {
19
+ // If the model echoed the full absolute path from the prompt, strip the base prefix.
20
+ const relative = isAbsolute(input)
21
+ ? input.startsWith(`${baseDir}/`) || input.startsWith(`${baseDir}\\`)
22
+ ? input.slice(baseDir.length + 1)
23
+ : (() => {
24
+ throw new Error(`Absolute path outside output directory: ${input}`);
25
+ })()
26
+ : input;
27
+ const resolved = join(baseDir, relative);
28
+ // Reject any resolved path that escapes the allowed base directory
29
+ if (!resolved.startsWith(baseDir)) {
30
+ throw new Error(`Path escapes the output directory: ${input}`);
31
+ }
32
+ return resolved;
33
+ }
34
+
35
+ /**
36
+ * Creates the write-file tool so the agent can save artifacts
37
+ * (plan.json, verdict.json, etc.) to the run output directory.
38
+ *
39
+ * @param baseDir - The run output directory that scopes all writes
40
+ * @param defineTool - Copilot SDK helper used to declare tools
41
+ * @returns Copilot tool definition for writing a file
42
+ */
43
+ export function createWriteFileTool(baseDir: string, defineTool: DefineTool) {
44
+ return defineTool('write_file', {
45
+ description:
46
+ 'Write content to a file in the output directory. Creates parent directories as needed.',
47
+ parameters: {
48
+ type: 'object' as const,
49
+ properties: {
50
+ path: {
51
+ type: 'string',
52
+ description: 'File path relative to the output directory (e.g. "plan.json")',
53
+ },
54
+ content: {
55
+ type: 'string',
56
+ description: 'The full file content to write',
57
+ },
58
+ },
59
+ required: ['path', 'content'],
60
+ },
61
+ handler: async (args) => {
62
+ const { path, content } = args as { path: string; content: string };
63
+ const resolved = resolveSafe(baseDir, path);
64
+ mkdirSync(dirname(resolved), { recursive: true });
65
+ writeFileSync(resolved, content);
66
+ return `Wrote ${content.length} bytes to ${path}`;
67
+ },
68
+ });
69
+ }
@@ -1,23 +1,23 @@
1
- import { createClickTool } from './click.js';
2
- import { createErrorsTool } from './errors.js';
3
- import { createFillTool } from './fill.js';
4
- import { createFindTool } from './find.js';
5
- import { createGetUrlTool } from './get-url.js';
6
- import { createGoBackTool } from './go-back.js';
7
- import { createHoverTool } from './hover.js';
8
- import { createNavigateTool } from './navigate.js';
9
- import { createPressKeyTool } from './press-key.js';
10
- import { createReloadTool } from './reload.js';
11
- import { createEvalJsTool } from './eval-js.js';
12
- import { createGetStylesTool } from './get-styles.js';
13
- import { AgentBrowserToolRegistry } from './registry.js';
14
- import { createScreenshotTool } from './screenshot.js';
15
- import { createScrollTool } from './scroll.js';
16
- import { createSelectTool } from './select.js';
17
- import { createSnapshotTool } from './snapshot.js';
18
- import { createTypeTextTool } from './type-text.js';
1
+ import { createClickTool } from './create-click-tool.js';
2
+ import { createErrorsTool } from './create-errors-tool.js';
3
+ import { createFillTool } from './create-fill-tool.js';
4
+ import { createFindTool } from './create-find-tool.js';
5
+ import { createGetUrlTool } from './create-get-url-tool.js';
6
+ import { createGoBackTool } from './create-go-back-tool.js';
7
+ import { createHoverTool } from './create-hover-tool.js';
8
+ import { createNavigateTool } from './create-navigate-tool.js';
9
+ import { createPressKeyTool } from './create-press-key-tool.js';
10
+ import { createReloadTool } from './create-reload-tool.js';
11
+ import { createEvalJsTool } from './create-eval-js-tool.js';
12
+ import { createGetStylesTool } from './create-get-styles-tool.js';
13
+ import { AgentBrowserToolRegistry } from './AgentBrowserToolRegistry.js';
14
+ import { createScreenshotTool } from './create-screenshot-tool.js';
15
+ import { createScrollTool } from './create-scroll-tool.js';
16
+ import { createSelectTool } from './create-select-tool.js';
17
+ import { createSnapshotTool } from './create-snapshot-tool.js';
18
+ import { createTypeTextTool } from './create-type-text-tool.js';
19
19
  import type { AgentBrowserToolContext, AgentBrowserToolList, DefineTool } from './types.js';
20
- import { createWaitTool } from './wait.js';
20
+ import { createWaitTool } from './create-wait-tool.js';
21
21
 
22
22
  /**
23
23
  * Creates the full set of Copilot browser tools for an eval session.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Truncates a string to a maximum length, appending an ellipsis when cut.
3
+ *
4
+ * @param str - The string to truncate.
5
+ * @param max - Maximum length of the returned string, including the ellipsis.
6
+ * @returns The original string, or a shortened version ending in `…`.
7
+ */
8
+ export function truncate(str: string, max: number): string {
9
+ return str.length > max ? `${str.slice(0, max - 1)}…` : str;
10
+ }
@@ -0,0 +1,42 @@
1
+ import type { Plan, StepResult, Verdict } from './types.js';
2
+ import { formatPlan } from './format-plan.js';
3
+ import { formatStepResult } from './format-step-result.js';
4
+ import { formatVerdict } from './format-verdict.js';
5
+
6
+ /**
7
+ * Attempts to detect the JSON shape of an assistant message and format it.
8
+ *
9
+ * Returns `null` when the content is not a recognised structure, so the
10
+ * caller can fall back to plain-text rendering.
11
+ *
12
+ * @param content - Raw assistant message content, expected to be a JSON string.
13
+ * @returns A formatted string for a recognised plan, step result, or verdict, or `null` otherwise.
14
+ */
15
+ export function tryFormatMessage(content: string): string | null {
16
+ let parsed: unknown;
17
+ try {
18
+ parsed = JSON.parse(content.trim());
19
+ } catch {
20
+ return null;
21
+ }
22
+ // Only object shapes can be matched against the recognised message formats
23
+ if (!parsed || typeof parsed !== 'object') return null;
24
+
25
+ // Verdict: { pass, reasoning, steps[] }
26
+ if ('pass' in parsed && 'steps' in parsed && Array.isArray((parsed as Verdict).steps)) {
27
+ return formatVerdict(parsed as Verdict);
28
+ }
29
+
30
+ // Plan: { summary, steps[] }
31
+ if ('summary' in parsed && 'steps' in parsed && Array.isArray((parsed as Plan).steps)) {
32
+ return formatPlan(parsed as Plan);
33
+ }
34
+
35
+ // Step result: { criterion, ok, note }
36
+ if ('criterion' in parsed && 'ok' in parsed && 'note' in parsed) {
37
+ return formatStepResult(parsed as StepResult);
38
+ }
39
+
40
+ // No recognised shape matched; caller should fall back to plain-text rendering
41
+ return null;
42
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Prints a CLI resolution error and exits the current process.
3
+ *
4
+ * @param message - Primary error message.
5
+ * @param detail - Optional follow-up detail shown on the next line.
6
+ * @returns This function never returns because the process exits.
7
+ */
8
+ export function exitWithResolutionError(message: string, detail?: string): never {
9
+ console.error(`❌ ${message}`);
10
+ // Only print the detail line when one was provided
11
+ if (detail) {
12
+ console.error(` ${detail}`);
13
+ }
14
+ process.exit(1);
15
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Command } from 'commander';
2
- import { copilotCommand } from './commands/app/command.js';
2
+ import { copilotCommand } from './commands/app/copilot-command.js';
3
3
 
4
4
  /**
5
5
  * Registers the `copilot` CLI plugin with the Fusion Framework CLI.
@@ -0,0 +1,59 @@
1
+ import { basename, dirname, extname, resolve } from 'node:path';
2
+ import { existsSync, statSync } from 'node:fs';
3
+
4
+ import { exitWithResolutionError } from './exit-with-resolution-error.js';
5
+
6
+ /**
7
+ * Normalized positional input for the `copilot app eval` command.
8
+ *
9
+ * When the user passes an application directory, `evalFile` is undefined.
10
+ * When the user passes a direct `eval/*.md` file path, `appPath` is derived
11
+ * from the file location and `evalFile` points to the selected markdown file.
12
+ */
13
+ export interface EvalCommandInput {
14
+ /** Absolute path to the Fusion application directory. */
15
+ appPath: string;
16
+ /** Optional absolute path to the directly selected eval markdown file. */
17
+ evalFile?: string;
18
+ }
19
+
20
+ /**
21
+ * Resolves the positional `copilot app eval` argument.
22
+ *
23
+ * Supports both application directory paths and direct paths to markdown files
24
+ * inside an application's `eval/` directory.
25
+ *
26
+ * @param inputPath - Positional CLI argument passed to the eval command.
27
+ * @returns Normalized application path and optional direct eval file path.
28
+ */
29
+ export function resolveEvalCommandInput(inputPath: string): EvalCommandInput {
30
+ const absInput = resolve(inputPath);
31
+
32
+ // A non-markdown path is treated as the application directory itself
33
+ if (extname(absInput) !== '.md') {
34
+ return { appPath: absInput };
35
+ }
36
+
37
+ // The direct eval file path must actually exist on disk
38
+ if (!existsSync(absInput)) {
39
+ exitWithResolutionError(`Eval file not found: ${absInput}`);
40
+ }
41
+
42
+ // Reject directories passed off as a direct eval file path
43
+ if (!statSync(absInput).isFile()) {
44
+ exitWithResolutionError(`Eval path is not a file: ${absInput}`);
45
+ }
46
+
47
+ const evalDir = dirname(absInput);
48
+ // Direct eval files must live inside an `eval/` directory so appPath can be derived
49
+ if (basename(evalDir) !== 'eval') {
50
+ exitWithResolutionError(
51
+ `Direct eval file path must point to a markdown file inside an eval directory: ${absInput}`,
52
+ );
53
+ }
54
+
55
+ return {
56
+ appPath: dirname(evalDir),
57
+ evalFile: absInput,
58
+ };
59
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { extname, isAbsolute, join, resolve } from 'node:path';
3
+
4
+ import { exitWithResolutionError } from './exit-with-resolution-error.js';
5
+
6
+ /**
7
+ * Resolves eval markdown files for a Fusion application.
8
+ *
9
+ * @param appPath - Path to the Fusion application directory
10
+ * @param evalOpt - Value of the `--eval` CLI option (name, path, or `undefined`)
11
+ * @returns Array of absolute paths to eval markdown files
12
+ */
13
+ export function resolveEvalFiles(appPath: string, evalOpt?: string): string[] {
14
+ const absApp = resolve(appPath);
15
+
16
+ // No --eval option: run every eval file found in the app's eval/ directory
17
+ if (!evalOpt) {
18
+ const evalDir = join(absApp, 'eval');
19
+ // Fail fast when the application has no eval directory to scan
20
+ if (!existsSync(evalDir)) {
21
+ exitWithResolutionError(
22
+ `No eval directory found at ${evalDir}`,
23
+ 'Create eval/*.md files or use --eval <file>',
24
+ );
25
+ }
26
+ const files = readdirSync(evalDir)
27
+ // Keep only markdown files, sorted for stable ordering, resolved to absolute paths
28
+ .filter((f) => f.endsWith('.md'))
29
+ .sort()
30
+ // Resolve each filename to its absolute path within the eval directory
31
+ .map((f) => join(evalDir, f));
32
+
33
+ // Surface a clear error instead of silently running zero eval files
34
+ if (files.length === 0) {
35
+ exitWithResolutionError(`No .md files found in ${evalDir}`);
36
+ }
37
+ return files;
38
+ }
39
+
40
+ // A path-like --eval value (absolute or containing separators) is a direct file reference
41
+ if (isAbsolute(evalOpt) || evalOpt.includes('/') || evalOpt.includes('\\')) {
42
+ const absPath = isAbsolute(evalOpt) ? evalOpt : resolve(evalOpt);
43
+ // Validate the direct file path exists before returning it
44
+ if (!existsSync(absPath)) {
45
+ exitWithResolutionError(`Eval file not found: ${absPath}`);
46
+ }
47
+ return [absPath];
48
+ }
49
+
50
+ const evalFileName = extname(evalOpt) === '.md' ? evalOpt : `${evalOpt}.md`;
51
+ const evalFile = join(absApp, 'eval', evalFileName);
52
+ // Otherwise treat --eval as a bare name inside the app's eval/ directory
53
+ if (!existsSync(evalFile)) {
54
+ exitWithResolutionError(`Eval file not found: ${evalFile}`);
55
+ }
56
+ return [evalFile];
57
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Extracts the user-facing error message from an agent-browser failure.
3
+ *
4
+ * Strips ANSI escape codes from stderr and returns only the diagnostic
5
+ * lines prefixed with `✗` or `⚠`. Falls back to the first 200 characters
6
+ * of the error message when no diagnostic lines are found.
7
+ *
8
+ * @param err - The caught error (typically from {@link ab})
9
+ * @returns A single-line summary suitable for console output
10
+ */
11
+ export function abErrorMessage(err: unknown): string {
12
+ // Non-Error throws have no structured message to extract
13
+ if (!(err instanceof Error)) return String(err);
14
+ const e = err as Error & { stderr?: string };
15
+ // Prefer the raw stderr stream when it was captured on the error
16
+ if (e.stderr) {
17
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escape sequences
18
+ const ansiPattern = /\x1B\[[0-9;]*m/g;
19
+ const lines = e.stderr
20
+ .replace(ansiPattern, '')
21
+ .split('\n')
22
+ // Keep only the diagnostic lines agent-browser prefixes with a status glyph
23
+ .map((l) => l.trim())
24
+ // Filter down to lines that look like agent-browser's own status output
25
+ .filter((l) => l.startsWith('✗') || l.startsWith('⚠'));
26
+ // Only report an assembled diagnostic summary when matching lines were found
27
+ if (lines.length) return lines.join(' | ');
28
+ }
29
+ const match = e.message.match(/[✗⚠].+/);
30
+ return match ? match[0] : e.message.slice(0, 200);
31
+ }
@@ -1,10 +1,8 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { existsSync, unlinkSync } from 'node:fs';
3
- import { homedir } from 'node:os';
4
3
  import { join } from 'node:path';
5
4
 
6
- /** Persistent Chrome profile directory used across agent-browser sessions. */
7
- export const PROFILE_DIR = `${homedir()}/.fusion-smoke-profile`;
5
+ import { PROFILE_DIR } from './helpers.js';
8
6
 
9
7
  /**
10
8
  * Runs an agent-browser command with the persistent Chrome profile.
@@ -22,6 +20,7 @@ export function ab(args: string[], timeoutMs = 30_000): string {
22
20
  // Remove stale SingletonLock left by a previous Chrome crash to prevent
23
21
  // "Failed to create SingletonLock: File exists" errors.
24
22
  const lockPath = join(PROFILE_DIR, 'SingletonLock');
23
+ // Only attempt removal when a stale lock file is actually present
25
24
  if (existsSync(lockPath)) {
26
25
  try {
27
26
  unlinkSync(lockPath);
@@ -37,30 +36,3 @@ export function ab(args: string[], timeoutMs = 30_000): string {
37
36
  cwd: process.cwd(),
38
37
  }).trim();
39
38
  }
40
-
41
- /**
42
- * Extracts the user-facing error message from an agent-browser failure.
43
- *
44
- * Strips ANSI escape codes from stderr and returns only the diagnostic
45
- * lines prefixed with `✗` or `⚠`. Falls back to the first 200 characters
46
- * of the error message when no diagnostic lines are found.
47
- *
48
- * @param err - The caught error (typically from {@link ab})
49
- * @returns A single-line summary suitable for console output
50
- */
51
- export function abErrorMessage(err: unknown): string {
52
- if (!(err instanceof Error)) return String(err);
53
- const e = err as Error & { stderr?: string };
54
- if (e.stderr) {
55
- // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escape sequences
56
- const ansiPattern = /\x1B\[[0-9;]*m/g;
57
- const lines = e.stderr
58
- .replace(ansiPattern, '')
59
- .split('\n')
60
- .map((l) => l.trim())
61
- .filter((l) => l.startsWith('✗') || l.startsWith('⚠'));
62
- if (lines.length) return lines.join(' | ');
63
- }
64
- const match = e.message.match(/[✗⚠].+/);
65
- return match ? match[0] : e.message.slice(0, 200);
66
- }