@equinor/fusion-framework-cli-plugin-copilot 2.0.1 → 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 (265) hide show
  1. package/CHANGELOG.md +17 -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} +8 -6
  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/{command.d.ts → copilot-command.d.ts} +1 -1
  111. package/dist/types/commands/app/format-plan.d.ts +8 -0
  112. package/dist/types/commands/app/format-step-result.d.ts +8 -0
  113. package/dist/types/commands/app/format-verdict.d.ts +8 -0
  114. package/dist/types/commands/app/prompts/{step.prompt.d.ts → create-step-prompt.d.ts} +1 -1
  115. package/dist/types/commands/app/status-icon.d.ts +8 -0
  116. package/dist/types/commands/app/tools/create-append-file-tool.d.ts +10 -0
  117. package/dist/types/commands/app/tools/{click.d.ts → create-click-tool.d.ts} +1 -1
  118. package/dist/types/commands/app/tools/{errors.d.ts → create-errors-tool.d.ts} +1 -1
  119. package/dist/types/commands/app/tools/{eval-js.d.ts → create-eval-js-tool.d.ts} +1 -1
  120. package/dist/types/commands/app/tools/{fill.d.ts → create-fill-tool.d.ts} +1 -1
  121. package/dist/types/commands/app/tools/{find.d.ts → create-find-tool.d.ts} +1 -1
  122. package/dist/types/commands/app/tools/{get-styles.d.ts → create-get-styles-tool.d.ts} +1 -1
  123. package/dist/types/commands/app/tools/{get-url.d.ts → create-get-url-tool.d.ts} +1 -1
  124. package/dist/types/commands/app/tools/{go-back.d.ts → create-go-back-tool.d.ts} +1 -1
  125. package/dist/types/commands/app/tools/{hover.d.ts → create-hover-tool.d.ts} +1 -1
  126. package/dist/types/commands/app/tools/{navigate.d.ts → create-navigate-tool.d.ts} +1 -1
  127. package/dist/types/commands/app/tools/{press-key.d.ts → create-press-key-tool.d.ts} +1 -1
  128. package/dist/types/commands/app/tools/{read-file.d.ts → create-read-file-tool.d.ts} +1 -1
  129. package/dist/types/commands/app/tools/{reload.d.ts → create-reload-tool.d.ts} +1 -1
  130. package/dist/types/commands/app/tools/{screenshot.d.ts → create-screenshot-tool.d.ts} +1 -1
  131. package/dist/types/commands/app/tools/{scroll.d.ts → create-scroll-tool.d.ts} +1 -1
  132. package/dist/types/commands/app/tools/{select.d.ts → create-select-tool.d.ts} +1 -1
  133. package/dist/types/commands/app/tools/{snapshot.d.ts → create-snapshot-tool.d.ts} +1 -1
  134. package/dist/types/commands/app/tools/{type-text.d.ts → create-type-text-tool.d.ts} +1 -1
  135. package/dist/types/commands/app/tools/{wait.d.ts → create-wait-tool.d.ts} +1 -1
  136. package/dist/types/commands/app/tools/create-write-file-tool.d.ts +10 -0
  137. package/dist/types/commands/app/truncate.d.ts +8 -0
  138. package/dist/types/commands/app/try-format-message.d.ts +10 -0
  139. package/dist/types/exit-with-resolution-error.d.ts +8 -0
  140. package/dist/types/{eval-resolve.d.ts → resolve-eval-command-input.d.ts} +0 -8
  141. package/dist/types/resolve-eval-files.d.ts +8 -0
  142. package/dist/types/utils/ab-error-message.d.ts +11 -0
  143. package/dist/types/utils/{agent-browser.d.ts → ab.d.ts} +0 -13
  144. package/dist/types/utils/create-run-dir.d.ts +8 -0
  145. package/dist/types/utils/{process.d.ts → helpers.d.ts} +13 -6
  146. package/dist/types/utils/index.d.ts +8 -5
  147. package/dist/types/utils/{daemon.d.ts → reset-daemon.d.ts} +0 -5
  148. package/dist/types/utils/resolve-app-key.d.ts +9 -0
  149. package/dist/types/utils/wait-for-server.d.ts +9 -0
  150. package/dist/types/version.d.ts +1 -1
  151. package/package.json +6 -6
  152. package/src/commands/app/{session-logger.ts → attach-session-logger.ts} +15 -1
  153. package/src/commands/app/{command.ts → copilot-command.ts} +21 -12
  154. package/src/commands/app/{eval.ts → create-session.ts} +7 -5
  155. package/src/commands/app/format-plan.ts +26 -0
  156. package/src/commands/app/format-step-result.ts +16 -0
  157. package/src/commands/app/format-verdict.ts +43 -0
  158. package/src/commands/app/prompts/{judge.prompt.ts → create-judge-prompt.ts} +4 -0
  159. package/src/commands/app/prompts/{step.prompt.ts → create-step-prompt.ts} +2 -1
  160. package/src/commands/app/{login.ts → run-login.ts} +4 -0
  161. package/src/commands/app/{server.ts → start-app-server.ts} +6 -0
  162. package/src/commands/app/status-icon.ts +16 -0
  163. package/src/commands/app/tools/{write-file.ts → create-append-file-tool.ts} +4 -39
  164. package/src/commands/app/tools/{find.ts → create-find-tool.ts} +3 -0
  165. package/src/commands/app/tools/{read-file.ts → create-read-file-tool.ts} +3 -1
  166. package/src/commands/app/tools/{screenshot.ts → create-screenshot-tool.ts} +6 -0
  167. package/src/commands/app/tools/{scroll.ts → create-scroll-tool.ts} +2 -0
  168. package/src/commands/app/tools/{wait.ts → create-wait-tool.ts} +4 -0
  169. package/src/commands/app/tools/create-write-file-tool.ts +69 -0
  170. package/src/commands/app/tools/index.ts +19 -19
  171. package/src/commands/app/truncate.ts +10 -0
  172. package/src/commands/app/try-format-message.ts +42 -0
  173. package/src/exit-with-resolution-error.ts +15 -0
  174. package/src/index.ts +1 -1
  175. package/src/resolve-eval-command-input.ts +59 -0
  176. package/src/resolve-eval-files.ts +57 -0
  177. package/src/utils/ab-error-message.ts +31 -0
  178. package/src/utils/{agent-browser.ts → ab.ts} +2 -30
  179. package/src/utils/create-run-dir.ts +24 -0
  180. package/src/utils/{process.ts → helpers.ts} +28 -10
  181. package/src/utils/index.ts +8 -5
  182. package/src/utils/{browser-poll.ts → poll-console.ts} +4 -2
  183. package/src/utils/{daemon.ts → reset-daemon.ts} +4 -13
  184. package/src/utils/resolve-app-key.ts +20 -0
  185. package/src/utils/wait-for-server.ts +24 -0
  186. package/src/version.ts +1 -1
  187. package/tsconfig.json +3 -1
  188. package/dist/esm/commands/app/command.js.map +0 -1
  189. package/dist/esm/commands/app/eval.js.map +0 -1
  190. package/dist/esm/commands/app/format.js +0 -93
  191. package/dist/esm/commands/app/format.js.map +0 -1
  192. package/dist/esm/commands/app/login.js.map +0 -1
  193. package/dist/esm/commands/app/prompts/execute-step.prompt.js.map +0 -1
  194. package/dist/esm/commands/app/prompts/judge.prompt.js.map +0 -1
  195. package/dist/esm/commands/app/prompts/plan.prompt.js.map +0 -1
  196. package/dist/esm/commands/app/prompts/step.prompt.js.map +0 -1
  197. package/dist/esm/commands/app/prompts/system.dev-server.prompt.js.map +0 -1
  198. package/dist/esm/commands/app/prompts/system.prompt.js.map +0 -1
  199. package/dist/esm/commands/app/server.js.map +0 -1
  200. package/dist/esm/commands/app/session-logger.js.map +0 -1
  201. package/dist/esm/commands/app/tools/click.js.map +0 -1
  202. package/dist/esm/commands/app/tools/errors.js.map +0 -1
  203. package/dist/esm/commands/app/tools/eval-js.js.map +0 -1
  204. package/dist/esm/commands/app/tools/fill.js.map +0 -1
  205. package/dist/esm/commands/app/tools/find.js.map +0 -1
  206. package/dist/esm/commands/app/tools/get-styles.js.map +0 -1
  207. package/dist/esm/commands/app/tools/get-url.js.map +0 -1
  208. package/dist/esm/commands/app/tools/go-back.js.map +0 -1
  209. package/dist/esm/commands/app/tools/hover.js.map +0 -1
  210. package/dist/esm/commands/app/tools/navigate.js.map +0 -1
  211. package/dist/esm/commands/app/tools/press-key.js.map +0 -1
  212. package/dist/esm/commands/app/tools/read-file.js.map +0 -1
  213. package/dist/esm/commands/app/tools/registry.js.map +0 -1
  214. package/dist/esm/commands/app/tools/reload.js.map +0 -1
  215. package/dist/esm/commands/app/tools/screenshot.js.map +0 -1
  216. package/dist/esm/commands/app/tools/scroll.js.map +0 -1
  217. package/dist/esm/commands/app/tools/select.js.map +0 -1
  218. package/dist/esm/commands/app/tools/snapshot.js.map +0 -1
  219. package/dist/esm/commands/app/tools/type-text.js.map +0 -1
  220. package/dist/esm/commands/app/tools/wait.js.map +0 -1
  221. package/dist/esm/commands/app/tools/write-file.js.map +0 -1
  222. package/dist/esm/eval-resolve.js +0 -83
  223. package/dist/esm/eval-resolve.js.map +0 -1
  224. package/dist/esm/utils/agent-browser.js.map +0 -1
  225. package/dist/esm/utils/browser-poll.js.map +0 -1
  226. package/dist/esm/utils/daemon.js.map +0 -1
  227. package/dist/esm/utils/process.js.map +0 -1
  228. package/dist/esm/utils/server.js +0 -65
  229. package/dist/esm/utils/server.js.map +0 -1
  230. package/dist/types/commands/app/format.d.ts +0 -14
  231. package/dist/types/commands/app/tools/write-file.d.ts +0 -19
  232. package/dist/types/utils/server.d.ts +0 -26
  233. package/src/commands/app/format.ts +0 -110
  234. package/src/eval-resolve.ts +0 -113
  235. package/src/utils/server.ts +0 -66
  236. /package/dist/types/commands/app/{session-logger.d.ts → attach-session-logger.d.ts} +0 -0
  237. /package/dist/types/commands/app/{eval.d.ts → create-session.d.ts} +0 -0
  238. /package/dist/types/commands/app/prompts/{system.dev-server.prompt.d.ts → create-dev-server-context.d.ts} +0 -0
  239. /package/dist/types/commands/app/prompts/{execute-step.prompt.d.ts → create-execute-step-prompt.d.ts} +0 -0
  240. /package/dist/types/commands/app/prompts/{judge.prompt.d.ts → create-judge-prompt.d.ts} +0 -0
  241. /package/dist/types/commands/app/prompts/{plan.prompt.d.ts → create-plan-prompt.d.ts} +0 -0
  242. /package/dist/types/commands/app/prompts/{system.prompt.d.ts → create-system-prompt.d.ts} +0 -0
  243. /package/dist/types/commands/app/{login.d.ts → run-login.d.ts} +0 -0
  244. /package/dist/types/commands/app/{server.d.ts → start-app-server.d.ts} +0 -0
  245. /package/dist/types/commands/app/tools/{registry.d.ts → AgentBrowserToolRegistry.d.ts} +0 -0
  246. /package/dist/types/utils/{browser-poll.d.ts → poll-console.d.ts} +0 -0
  247. /package/src/commands/app/prompts/{system.dev-server.prompt.ts → create-dev-server-context.ts} +0 -0
  248. /package/src/commands/app/prompts/{execute-step.prompt.ts → create-execute-step-prompt.ts} +0 -0
  249. /package/src/commands/app/prompts/{plan.prompt.ts → create-plan-prompt.ts} +0 -0
  250. /package/src/commands/app/prompts/{system.prompt.ts → create-system-prompt.ts} +0 -0
  251. /package/src/commands/app/tools/{registry.ts → AgentBrowserToolRegistry.ts} +0 -0
  252. /package/src/commands/app/tools/{click.ts → create-click-tool.ts} +0 -0
  253. /package/src/commands/app/tools/{errors.ts → create-errors-tool.ts} +0 -0
  254. /package/src/commands/app/tools/{eval-js.ts → create-eval-js-tool.ts} +0 -0
  255. /package/src/commands/app/tools/{fill.ts → create-fill-tool.ts} +0 -0
  256. /package/src/commands/app/tools/{get-styles.ts → create-get-styles-tool.ts} +0 -0
  257. /package/src/commands/app/tools/{get-url.ts → create-get-url-tool.ts} +0 -0
  258. /package/src/commands/app/tools/{go-back.ts → create-go-back-tool.ts} +0 -0
  259. /package/src/commands/app/tools/{hover.ts → create-hover-tool.ts} +0 -0
  260. /package/src/commands/app/tools/{navigate.ts → create-navigate-tool.ts} +0 -0
  261. /package/src/commands/app/tools/{press-key.ts → create-press-key-tool.ts} +0 -0
  262. /package/src/commands/app/tools/{reload.ts → create-reload-tool.ts} +0 -0
  263. /package/src/commands/app/tools/{select.ts → create-select-tool.ts} +0 -0
  264. /package/src/commands/app/tools/{snapshot.ts → create-snapshot-tool.ts} +0 -0
  265. /package/src/commands/app/tools/{type-text.ts → create-type-text-tool.ts} +0 -0
@@ -2,17 +2,18 @@ import { createCommand } from 'commander';
2
2
  import { basename } from 'node:path';
3
3
 
4
4
  import { resetDaemon, cleanup, createRunDir } from '../../utils/index.js';
5
- import { runLogin } from './login.js';
6
- import { resolveEvalCommandInput, resolveEvalFiles } from '../../eval-resolve.js';
7
- import { createSession } from './eval.js';
8
- import { startAppServer } from './server.js';
9
- import { attachSessionLogger } from './session-logger.js';
5
+ import { runLogin } from './run-login.js';
6
+ import { resolveEvalCommandInput } from '../../resolve-eval-command-input.js';
7
+ import { resolveEvalFiles } from '../../resolve-eval-files.js';
8
+ import { createSession } from './create-session.js';
9
+ import { startAppServer } from './start-app-server.js';
10
+ import { attachSessionLogger } from './attach-session-logger.js';
10
11
  import type { CopilotEvalOptions, Plan, Verdict } from './types.js';
11
- import { formatVerdict } from './format.js';
12
+ import { formatVerdict } from './format-verdict.js';
12
13
  import { existsSync, readFileSync } from 'node:fs';
13
- import { createPlanPrompt } from './prompts/plan.prompt.js';
14
- import { createStepPrompt } from './prompts/step.prompt.js';
15
- import { createJudgePrompt } from './prompts/judge.prompt.js';
14
+ import { createPlanPrompt } from './prompts/create-plan-prompt.js';
15
+ import { createStepPrompt } from './prompts/create-step-prompt.js';
16
+ import { createJudgePrompt } from './prompts/create-judge-prompt.js';
16
17
  import chalk from 'chalk';
17
18
 
18
19
  /**
@@ -58,6 +59,7 @@ const _appEvalCommand = createCommand('eval')
58
59
  const port = parseInt(options.port, 10);
59
60
  const verbose = options.verbose;
60
61
 
62
+ // Reject ambiguous input that selects an eval two different ways
61
63
  if (resolvedInput.evalFile && options.eval) {
62
64
  console.error('❌ Do not combine a positional eval markdown file with --eval');
63
65
  process.exit(1);
@@ -73,9 +75,9 @@ const _appEvalCommand = createCommand('eval')
73
75
  const evalFiles = resolvedInput.evalFile
74
76
  ? [resolvedInput.evalFile]
75
77
  : resolveEvalFiles(absAppPath, options.eval);
76
- console.log(
77
- `📋 Found ${evalFiles.length} eval(s): ${evalFiles.map((f) => basename(f)).join(', ')}`,
78
- );
78
+ // Summarize which eval files were discovered before the run starts
79
+ const evalFileNames = evalFiles.map((f) => basename(f)).join(', ');
80
+ console.log(`📋 Found ${evalFiles.length} eval(s): ${evalFileNames}`);
79
81
 
80
82
  // Start the app dev server (unless --url provides an already-running URL)
81
83
  const { serverProcess, appUrl } = await startAppServer(absAppPath, {
@@ -103,6 +105,7 @@ const _appEvalCommand = createCommand('eval')
103
105
 
104
106
  let failures = 0;
105
107
  try {
108
+ // Run every resolved eval file sequentially so output stays readable
106
109
  for (const evalFilePath of evalFiles) {
107
110
  const evalName = basename(evalFilePath, '.md');
108
111
  const query = readFileSync(evalFilePath, 'utf-8').trim();
@@ -129,6 +132,7 @@ const _appEvalCommand = createCommand('eval')
129
132
  await session.sendAndWait({ prompt: createPlanPrompt(query, ctx) }, 300_000);
130
133
 
131
134
  const planPath = `${runDir}/plan.json`;
135
+ // Bail out of this eval when the planning phase produced no plan file
132
136
  if (!existsSync(planPath)) {
133
137
  console.error(
134
138
  chalk.red(
@@ -138,6 +142,7 @@ const _appEvalCommand = createCommand('eval')
138
142
  session.disconnect();
139
143
  logger.stop();
140
144
  failures++;
145
+ // Skip execution/judging phases; this eval already failed at planning
141
146
  continue;
142
147
  }
143
148
 
@@ -147,6 +152,7 @@ const _appEvalCommand = createCommand('eval')
147
152
  // ── Phase 2: Execute + Evaluate each step ──────────────────
148
153
  for (const step of plan.steps) {
149
154
  console.log(chalk.cyan(`\n── Scenario: ${step.scenario}`));
155
+ // Print each acceptance criterion the agent will be judged against
150
156
  for (const c of step.criteria) {
151
157
  console.log(chalk.dim(` • ${c}`));
152
158
  }
@@ -162,9 +168,11 @@ const _appEvalCommand = createCommand('eval')
162
168
 
163
169
  // Print the verdict
164
170
  const verdictPath = `${runDir}/verdict.json`;
171
+ // Only report a verdict when the judge phase actually produced one
165
172
  if (existsSync(verdictPath)) {
166
173
  const verdict = JSON.parse(readFileSync(verdictPath, 'utf-8')) as Verdict;
167
174
  console.log(formatVerdict(verdict));
175
+ // A failing verdict counts toward the overall exit-code failure total
168
176
  if (!verdict.pass) failures++;
169
177
  } else {
170
178
  console.error(chalk.red(`❌ Judge did not produce ${verdictPath}.`));
@@ -174,6 +182,7 @@ const _appEvalCommand = createCommand('eval')
174
182
  logger?.stop();
175
183
  const msg = err instanceof Error ? err.message : String(err);
176
184
  console.error(chalk.red(`\n❌ Eval "${evalName}" failed: ${msg}`));
185
+ // Print the stack trace too when one is available for debugging
177
186
  if (err instanceof Error && err.stack) {
178
187
  console.error(chalk.dim(err.stack));
179
188
  }
@@ -3,12 +3,13 @@ import { join } from 'node:path';
3
3
 
4
4
  import { ab, abErrorMessage } from '../../utils/index.js';
5
5
  import { createAgentBrowserTools } from './tools/index.js';
6
- import { createWriteFileTool, createAppendFileTool } from './tools/write-file.js';
7
- import { createReadFileTool } from './tools/read-file.js';
6
+ import { createWriteFileTool } from './tools/create-write-file-tool.js';
7
+ import { createAppendFileTool } from './tools/create-append-file-tool.js';
8
+ import { createReadFileTool } from './tools/create-read-file-tool.js';
8
9
  import type { RuntimeExecutionContext } from './types.js';
9
10
  import type { CopilotSession, SessionConfig } from '@github/copilot-sdk';
10
- import { createSystemPrompt } from './prompts/system.prompt.js';
11
- import { createDevServerContext } from './prompts/system.dev-server.prompt.js';
11
+ import { createSystemPrompt } from './prompts/create-system-prompt.js';
12
+ import { createDevServerContext } from './prompts/create-dev-server-context.js';
12
13
 
13
14
  /**
14
15
  * Lazily imports the Copilot SDK to avoid module resolution errors at CLI startup.
@@ -35,7 +36,7 @@ export const createSession = async (options: SessionOptions): Promise<CopilotSes
35
36
  const { ctx, config, defaultToolTimeoutMs } = options;
36
37
 
37
38
  const { CopilotClient, defineTool, approveAll } = await loadCopilotSdk();
38
- const client = new CopilotClient({ autoStart: true });
39
+ const client = new CopilotClient();
39
40
  await client.start();
40
41
 
41
42
  const fileTools = [
@@ -44,6 +45,7 @@ export const createSession = async (options: SessionOptions): Promise<CopilotSes
44
45
  createAppendFileTool(ctx.outDir, defineTool),
45
46
  ];
46
47
 
48
+ // Combine built-in browser and file tools with any caller-supplied tools
47
49
  const tools = [
48
50
  ...createBrowserTools(defineTool, {
49
51
  cwd: ctx.outDir,
@@ -0,0 +1,26 @@
1
+ import chalk from 'chalk';
2
+
3
+ import type { Plan } from './types.js';
4
+ import { truncate } from './truncate.js';
5
+
6
+ /**
7
+ * Renders a structured plan as a human-readable summary.
8
+ *
9
+ * @param plan - The plan to render, containing a summary and ordered steps.
10
+ * @returns A multi-line string suitable for console output.
11
+ */
12
+ export function formatPlan(plan: Plan): string {
13
+ const lines = [
14
+ '',
15
+ chalk.bold(`📋 ${plan.summary}`),
16
+ '',
17
+ ...plan.steps.flatMap((s, si) => [
18
+ ` ${chalk.dim(`${si + 1}.`)} ${s.scenario}`,
19
+ ...s.criteria
20
+ // Truncate each criterion so long descriptions don't wrap the terminal
21
+ .map((c) => chalk.dim(` • ${truncate(c, 70)}`)),
22
+ ]),
23
+ '',
24
+ ];
25
+ return lines.join('\n');
26
+ }
@@ -0,0 +1,16 @@
1
+ import chalk from 'chalk';
2
+
3
+ import type { StepResult } from './types.js';
4
+ import { statusIcon } from './status-icon.js';
5
+
6
+ /**
7
+ * Renders a single step execution result.
8
+ *
9
+ * @param result - The step result to render, including its outcome, note, and evidence.
10
+ * @returns A formatted two-line string describing the step outcome.
11
+ */
12
+ export function formatStepResult(result: StepResult): string {
13
+ const icon = statusIcon(result.ok);
14
+ const files = result.evidence.length ? chalk.dim(` [${result.evidence.join(', ')}]`) : '';
15
+ return ` ${icon} ${result.criterion}\n${chalk.dim(` ${result.note}`)}${files}`;
16
+ }
@@ -0,0 +1,43 @@
1
+ import chalk from 'chalk';
2
+
3
+ import type { Verdict } from './types.js';
4
+ import { statusIcon } from './status-icon.js';
5
+
6
+ /**
7
+ * Renders the final verdict as a full-width coloured report.
8
+ *
9
+ * @param verdict - The verdict to render, including per-step results, reasoning, and UX feedback.
10
+ * @returns A multi-line string suitable for console output.
11
+ */
12
+ export function formatVerdict(verdict: Verdict): string {
13
+ const bar = '═'.repeat(60);
14
+ const banner = verdict.pass
15
+ ? chalk.bgGreen.black.bold(' ✅ PASS ')
16
+ : chalk.bgRed.white.bold(' ❌ FAIL ');
17
+
18
+ const lines = [
19
+ '',
20
+ bar,
21
+ ` ${banner}`,
22
+ bar,
23
+ '',
24
+ ...verdict.steps
25
+ // Render each step's outcome as a status glyph, criterion, and note
26
+ .map((s) => ` ${statusIcon(s.ok)} ${s.criterion}\n${chalk.dim(` ${s.note}`)}`),
27
+ '',
28
+ chalk.dim(` ${verdict.reasoning}`),
29
+ '',
30
+ ];
31
+
32
+ // Append optional UX feedback only when the judge actually provided any
33
+ if (verdict.ux && verdict.ux.length > 0) {
34
+ lines.push(chalk.bold(' 💡 UX Feedback'), '');
35
+ // List every UX hint as its own bullet line
36
+ for (const hint of verdict.ux) {
37
+ lines.push(chalk.yellow(` • ${hint}`));
38
+ }
39
+ lines.push('');
40
+ }
41
+
42
+ return lines.join('\n');
43
+ }
@@ -19,6 +19,7 @@ function collectEvidence(outDir: string): string {
19
19
 
20
20
  // ── executions.jsonl ──
21
21
  const execPath = join(outDir, 'executions.jsonl');
22
+ // Fall back to a placeholder note when the executions log is missing
22
23
  if (existsSync(execPath)) {
23
24
  parts.push('### executions.jsonl', readFileSync(execPath, 'utf-8').trim());
24
25
  } else {
@@ -27,10 +28,13 @@ function collectEvidence(outDir: string): string {
27
28
 
28
29
  // ── evidence/ directory ──
29
30
  const evidenceDir = join(outDir, 'evidence');
31
+ // Only scan for evidence files when the directory actually exists
30
32
  if (existsSync(evidenceDir)) {
31
33
  const files = readdirSync(evidenceDir).sort();
34
+ // Inline readable text evidence; list binary evidence by name only
32
35
  for (const file of files) {
33
36
  const ext = extname(file).toLowerCase();
37
+ // Inline the content of readable text extensions; list other files by name only
34
38
  if (TEXT_EXTENSIONS.has(ext)) {
35
39
  const content = readFileSync(join(evidenceDir, file), 'utf-8').trim();
36
40
  parts.push(`### evidence/${file}`, content);
@@ -10,8 +10,9 @@ import type { PlanStep, RuntimeExecutionContext } from '../types.js';
10
10
  * @param ctx - Runtime context with output directory and app URL
11
11
  * @returns The fully interpolated prompt string
12
12
  */
13
- export const createStepPrompt = (step: PlanStep, ctx: RuntimeExecutionContext): string => {
13
+ export const createStepPrompt = (step: PlanStep, _ctx: RuntimeExecutionContext): string => {
14
14
  const slug = slugify(step.scenario);
15
+ // Render each criterion with its expected pass/fail evidence for the prompt
15
16
  const criteriaBlock = step.criteria
16
17
  .map((c, i) => `${i + 1}. ${c}\n Pass: ${step.pass[i] ?? ''}\n Fail: ${step.fail[i] ?? ''}`)
17
18
  .join('\n');
@@ -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
+ }