@equinor/fusion-framework-cli-plugin-copilot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -0
  4. package/dist/esm/commands/app/command.js +176 -0
  5. package/dist/esm/commands/app/command.js.map +1 -0
  6. package/dist/esm/commands/app/eval.js +92 -0
  7. package/dist/esm/commands/app/eval.js.map +1 -0
  8. package/dist/esm/commands/app/format.js +93 -0
  9. package/dist/esm/commands/app/format.js.map +1 -0
  10. package/dist/esm/commands/app/login.js +71 -0
  11. package/dist/esm/commands/app/login.js.map +1 -0
  12. package/dist/esm/commands/app/prompts/execute-step.prompt.js +44 -0
  13. package/dist/esm/commands/app/prompts/execute-step.prompt.js.map +1 -0
  14. package/dist/esm/commands/app/prompts/judge.prompt.js +116 -0
  15. package/dist/esm/commands/app/prompts/judge.prompt.js.map +1 -0
  16. package/dist/esm/commands/app/prompts/plan.prompt.js +47 -0
  17. package/dist/esm/commands/app/prompts/plan.prompt.js.map +1 -0
  18. package/dist/esm/commands/app/prompts/step.prompt.js +42 -0
  19. package/dist/esm/commands/app/prompts/step.prompt.js.map +1 -0
  20. package/dist/esm/commands/app/prompts/system.dev-server.prompt.js +21 -0
  21. package/dist/esm/commands/app/prompts/system.dev-server.prompt.js.map +1 -0
  22. package/dist/esm/commands/app/prompts/system.prompt.js +77 -0
  23. package/dist/esm/commands/app/prompts/system.prompt.js.map +1 -0
  24. package/dist/esm/commands/app/server.js +83 -0
  25. package/dist/esm/commands/app/server.js.map +1 -0
  26. package/dist/esm/commands/app/session-logger.js +171 -0
  27. package/dist/esm/commands/app/session-logger.js.map +1 -0
  28. package/dist/esm/commands/app/tools/click.js +27 -0
  29. package/dist/esm/commands/app/tools/click.js.map +1 -0
  30. package/dist/esm/commands/app/tools/errors.js +24 -0
  31. package/dist/esm/commands/app/tools/errors.js.map +1 -0
  32. package/dist/esm/commands/app/tools/eval-js.js +36 -0
  33. package/dist/esm/commands/app/tools/eval-js.js.map +1 -0
  34. package/dist/esm/commands/app/tools/fill.js +28 -0
  35. package/dist/esm/commands/app/tools/fill.js.map +1 -0
  36. package/dist/esm/commands/app/tools/find.js +44 -0
  37. package/dist/esm/commands/app/tools/find.js.map +1 -0
  38. package/dist/esm/commands/app/tools/get-styles.js +36 -0
  39. package/dist/esm/commands/app/tools/get-styles.js.map +1 -0
  40. package/dist/esm/commands/app/tools/get-url.js +24 -0
  41. package/dist/esm/commands/app/tools/get-url.js.map +1 -0
  42. package/dist/esm/commands/app/tools/go-back.js +15 -0
  43. package/dist/esm/commands/app/tools/go-back.js.map +1 -0
  44. package/dist/esm/commands/app/tools/hover.js +24 -0
  45. package/dist/esm/commands/app/tools/hover.js.map +1 -0
  46. package/dist/esm/commands/app/tools/index.js +32 -0
  47. package/dist/esm/commands/app/tools/index.js.map +1 -0
  48. package/dist/esm/commands/app/tools/navigate.js +31 -0
  49. package/dist/esm/commands/app/tools/navigate.js.map +1 -0
  50. package/dist/esm/commands/app/tools/press-key.js +24 -0
  51. package/dist/esm/commands/app/tools/press-key.js.map +1 -0
  52. package/dist/esm/commands/app/tools/read-file.js +98 -0
  53. package/dist/esm/commands/app/tools/read-file.js.map +1 -0
  54. package/dist/esm/commands/app/tools/registry.js +34 -0
  55. package/dist/esm/commands/app/tools/registry.js.map +1 -0
  56. package/dist/esm/commands/app/tools/reload.js +15 -0
  57. package/dist/esm/commands/app/tools/reload.js.map +1 -0
  58. package/dist/esm/commands/app/tools/screenshot.js +133 -0
  59. package/dist/esm/commands/app/tools/screenshot.js.map +1 -0
  60. package/dist/esm/commands/app/tools/scroll.js +41 -0
  61. package/dist/esm/commands/app/tools/scroll.js.map +1 -0
  62. package/dist/esm/commands/app/tools/select.js +25 -0
  63. package/dist/esm/commands/app/tools/select.js.map +1 -0
  64. package/dist/esm/commands/app/tools/snapshot.js +24 -0
  65. package/dist/esm/commands/app/tools/snapshot.js.map +1 -0
  66. package/dist/esm/commands/app/tools/type-text.js +29 -0
  67. package/dist/esm/commands/app/tools/type-text.js.map +1 -0
  68. package/dist/esm/commands/app/tools/types.js +2 -0
  69. package/dist/esm/commands/app/tools/types.js.map +1 -0
  70. package/dist/esm/commands/app/tools/wait.js +39 -0
  71. package/dist/esm/commands/app/tools/wait.js.map +1 -0
  72. package/dist/esm/commands/app/tools/write-file.js +98 -0
  73. package/dist/esm/commands/app/tools/write-file.js.map +1 -0
  74. package/dist/esm/commands/app/types.js +2 -0
  75. package/dist/esm/commands/app/types.js.map +1 -0
  76. package/dist/esm/eval-resolve.js +83 -0
  77. package/dist/esm/eval-resolve.js.map +1 -0
  78. package/dist/esm/index.js +18 -0
  79. package/dist/esm/index.js.map +1 -0
  80. package/dist/esm/utils/agent-browser.js +67 -0
  81. package/dist/esm/utils/agent-browser.js.map +1 -0
  82. package/dist/esm/utils/browser-poll.js +26 -0
  83. package/dist/esm/utils/browser-poll.js.map +1 -0
  84. package/dist/esm/utils/daemon.js +83 -0
  85. package/dist/esm/utils/daemon.js.map +1 -0
  86. package/dist/esm/utils/index.js +6 -0
  87. package/dist/esm/utils/index.js.map +1 -0
  88. package/dist/esm/utils/process.js +30 -0
  89. package/dist/esm/utils/process.js.map +1 -0
  90. package/dist/esm/utils/server.js +65 -0
  91. package/dist/esm/utils/server.js.map +1 -0
  92. package/dist/esm/version.js +3 -0
  93. package/dist/esm/version.js.map +1 -0
  94. package/dist/tsconfig.tsbuildinfo +1 -0
  95. package/dist/types/commands/app/command.d.ts +7 -0
  96. package/dist/types/commands/app/eval.d.ts +9 -0
  97. package/dist/types/commands/app/format.d.ts +14 -0
  98. package/dist/types/commands/app/login.d.ts +14 -0
  99. package/dist/types/commands/app/prompts/execute-step.prompt.d.ts +13 -0
  100. package/dist/types/commands/app/prompts/judge.prompt.d.ts +15 -0
  101. package/dist/types/commands/app/prompts/plan.prompt.d.ts +13 -0
  102. package/dist/types/commands/app/prompts/step.prompt.d.ts +12 -0
  103. package/dist/types/commands/app/prompts/system.dev-server.prompt.d.ts +12 -0
  104. package/dist/types/commands/app/prompts/system.prompt.d.ts +12 -0
  105. package/dist/types/commands/app/server.d.ts +26 -0
  106. package/dist/types/commands/app/session-logger.d.ts +14 -0
  107. package/dist/types/commands/app/tools/click.d.ts +9 -0
  108. package/dist/types/commands/app/tools/errors.d.ts +9 -0
  109. package/dist/types/commands/app/tools/eval-js.d.ts +13 -0
  110. package/dist/types/commands/app/tools/fill.d.ts +9 -0
  111. package/dist/types/commands/app/tools/find.d.ts +9 -0
  112. package/dist/types/commands/app/tools/get-styles.d.ts +13 -0
  113. package/dist/types/commands/app/tools/get-url.d.ts +9 -0
  114. package/dist/types/commands/app/tools/go-back.d.ts +9 -0
  115. package/dist/types/commands/app/tools/hover.d.ts +9 -0
  116. package/dist/types/commands/app/tools/index.d.ts +9 -0
  117. package/dist/types/commands/app/tools/navigate.d.ts +9 -0
  118. package/dist/types/commands/app/tools/press-key.d.ts +9 -0
  119. package/dist/types/commands/app/tools/read-file.d.ts +13 -0
  120. package/dist/types/commands/app/tools/registry.d.ts +28 -0
  121. package/dist/types/commands/app/tools/reload.d.ts +9 -0
  122. package/dist/types/commands/app/tools/screenshot.d.ts +9 -0
  123. package/dist/types/commands/app/tools/scroll.d.ts +9 -0
  124. package/dist/types/commands/app/tools/select.d.ts +9 -0
  125. package/dist/types/commands/app/tools/snapshot.d.ts +9 -0
  126. package/dist/types/commands/app/tools/type-text.d.ts +9 -0
  127. package/dist/types/commands/app/tools/types.d.ts +12 -0
  128. package/dist/types/commands/app/tools/wait.d.ts +9 -0
  129. package/dist/types/commands/app/tools/write-file.d.ts +19 -0
  130. package/dist/types/commands/app/types.d.ts +68 -0
  131. package/dist/types/eval-resolve.d.ts +31 -0
  132. package/dist/types/index.d.ts +15 -0
  133. package/dist/types/utils/agent-browser.d.ts +26 -0
  134. package/dist/types/utils/browser-poll.d.ts +9 -0
  135. package/dist/types/utils/daemon.d.ts +16 -0
  136. package/dist/types/utils/index.d.ts +5 -0
  137. package/dist/types/utils/process.d.ts +21 -0
  138. package/dist/types/utils/server.d.ts +26 -0
  139. package/dist/types/version.d.ts +1 -0
  140. package/package.json +57 -0
  141. package/src/commands/app/command.ts +204 -0
  142. package/src/commands/app/eval.ts +114 -0
  143. package/src/commands/app/format.ts +110 -0
  144. package/src/commands/app/login.ts +92 -0
  145. package/src/commands/app/prompts/execute-step.prompt.ts +48 -0
  146. package/src/commands/app/prompts/judge.prompt.ts +121 -0
  147. package/src/commands/app/prompts/plan.prompt.ts +48 -0
  148. package/src/commands/app/prompts/step.prompt.ts +45 -0
  149. package/src/commands/app/prompts/system.dev-server.prompt.ts +22 -0
  150. package/src/commands/app/prompts/system.prompt.ts +78 -0
  151. package/src/commands/app/server.ts +105 -0
  152. package/src/commands/app/session-logger.ts +187 -0
  153. package/src/commands/app/tools/click.ts +28 -0
  154. package/src/commands/app/tools/errors.ts +26 -0
  155. package/src/commands/app/tools/eval-js.ts +40 -0
  156. package/src/commands/app/tools/fill.ts +29 -0
  157. package/src/commands/app/tools/find.ts +49 -0
  158. package/src/commands/app/tools/get-styles.ts +40 -0
  159. package/src/commands/app/tools/get-url.ts +26 -0
  160. package/src/commands/app/tools/go-back.ts +16 -0
  161. package/src/commands/app/tools/hover.ts +25 -0
  162. package/src/commands/app/tools/index.ts +57 -0
  163. package/src/commands/app/tools/navigate.ts +32 -0
  164. package/src/commands/app/tools/press-key.ts +25 -0
  165. package/src/commands/app/tools/read-file.ts +113 -0
  166. package/src/commands/app/tools/registry.ts +38 -0
  167. package/src/commands/app/tools/reload.ts +16 -0
  168. package/src/commands/app/tools/screenshot.ts +168 -0
  169. package/src/commands/app/tools/scroll.ts +45 -0
  170. package/src/commands/app/tools/select.ts +26 -0
  171. package/src/commands/app/tools/snapshot.ts +27 -0
  172. package/src/commands/app/tools/type-text.ts +30 -0
  173. package/src/commands/app/tools/types.ts +15 -0
  174. package/src/commands/app/tools/wait.ts +43 -0
  175. package/src/commands/app/tools/write-file.ts +104 -0
  176. package/src/commands/app/types.ts +76 -0
  177. package/src/eval-resolve.ts +113 -0
  178. package/src/index.ts +20 -0
  179. package/src/utils/agent-browser.ts +66 -0
  180. package/src/utils/browser-poll.ts +27 -0
  181. package/src/utils/daemon.ts +84 -0
  182. package/src/utils/index.ts +5 -0
  183. package/src/utils/process.ts +33 -0
  184. package/src/utils/server.ts +66 -0
  185. package/src/version.ts +2 -0
  186. package/tsconfig.json +13 -0
@@ -0,0 +1,48 @@
1
+ import type { RuntimeExecutionContext } from '../types.js';
2
+
3
+ /**
4
+ * Creates the planning prompt that turns a feature specification into
5
+ * a structured test plan.
6
+ *
7
+ * The agent produces a `plan.json` with one step per atomic acceptance
8
+ * criterion, each containing browser actions, pass evidence, and fail evidence.
9
+ *
10
+ * @param query - The feature specification / eval content to plan against
11
+ * @param ctx - Runtime context with output directory and app URL
12
+ * @returns The fully interpolated planning prompt string
13
+ */
14
+ export const createPlanPrompt = (query: string, ctx: RuntimeExecutionContext): string => {
15
+ return `
16
+ Create a test plan for this feature specification:
17
+
18
+ ${query}
19
+
20
+ Application URL: ${ctx.url}
21
+
22
+ Save the plan using write_file with path "plan.json".
23
+
24
+ JSON structure:
25
+
26
+ {
27
+ "summary": "One-sentence description of what is being tested",
28
+ "steps": [
29
+ {
30
+ "scenario": "A logical user scenario (e.g. 'Open the landing page')",
31
+ "criteria": ["First thing to verify", "Second thing to verify"],
32
+ "pass": ["What pass looks like for first", "What pass looks like for second"],
33
+ "fail": ["What fail looks like for first", "What fail looks like for second"]
34
+ }
35
+ ]
36
+ }
37
+
38
+ Rules:
39
+ - Group related criteria under one step when they share the same scenario
40
+ (e.g. same page, same state). Do NOT create separate steps for things
41
+ that can be verified in a single browser visit.
42
+ - Only create multiple steps when there is a distinct user action between them
43
+ (navigation, click, form submission, etc.).
44
+ - Do NOT include HOW to test — no actions, selectors, or tool calls.
45
+ - passEvidence and failEvidence describe what a human would see.
46
+ - Keep criteria directly traceable to the spec. Do NOT invent criteria.
47
+ `.trim();
48
+ };
@@ -0,0 +1,45 @@
1
+ import type { PlanStep, RuntimeExecutionContext } from '../types.js';
2
+
3
+ /**
4
+ * Creates the execution prompt for a single test step.
5
+ *
6
+ * The agent autonomously verifies all criteria for the scenario,
7
+ * takes evidence, and records results.
8
+ *
9
+ * @param step - The plan step with scenario and criteria
10
+ * @param ctx - Runtime context with output directory and app URL
11
+ * @returns The fully interpolated prompt string
12
+ */
13
+ export const createStepPrompt = (step: PlanStep, ctx: RuntimeExecutionContext): string => {
14
+ const slug = slugify(step.scenario);
15
+ const criteriaBlock = step.criteria
16
+ .map((c, i) => `${i + 1}. ${c}\n Pass: ${step.pass[i] ?? ''}\n Fail: ${step.fail[i] ?? ''}`)
17
+ .join('\n');
18
+
19
+ return `
20
+ Scenario: ${step.scenario}
21
+
22
+ Verify ALL of these criteria:
23
+ ${criteriaBlock}
24
+
25
+ Use browser tools to verify. Take a screenshot when done:
26
+ browser_screenshot path="evidence/step-${slug}.jpg" annotate=true
27
+
28
+ Then for EACH criterion, append ONE JSON line using append_file with path "executions.jsonl":
29
+ {"criterion": "...", "ok": true|false, "note": "what you observed", "evidence": ["evidence/step-${slug}.jpg"]}
30
+ `.trim();
31
+ };
32
+
33
+ /**
34
+ * Converts text to a URL-safe slug for filenames.
35
+ *
36
+ * @param text - The text to slugify
37
+ * @returns Lowercase hyphenated string safe for filenames
38
+ */
39
+ function slugify(text: string): string {
40
+ return text
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9]+/g, '-')
43
+ .replace(/^-|-$/g, '')
44
+ .slice(0, 60);
45
+ }
@@ -0,0 +1,22 @@
1
+ import type { RuntimeExecutionContext } from '../types.js';
2
+
3
+ /**
4
+ * Creates the Fusion dev-server–specific system prompt supplement.
5
+ *
6
+ * Describes the DOM structure of a Fusion Framework application served
7
+ * by the local dev server / dev-portal so the agent knows where to
8
+ * find app content in the page.
9
+ *
10
+ * @param ctx - Runtime context with the application URL
11
+ * @returns The dev-server DOM context block to append to the system prompt
12
+ */
13
+ export const createDevServerContext = (ctx: RuntimeExecutionContext): string => {
14
+ return `
15
+ Fusion dev-server DOM:
16
+ - App content lives inside <section id="app-section" style="display:contents">.
17
+ - While loading, #app-section contains an EquinorLoader spinner — wait for it to disappear.
18
+ - Once loaded, the app renders in an anonymous <div> child of #app-section.
19
+ - Target #app-section for assertions, not <body>.
20
+ - URL pattern: /apps/{appKey} → ${ctx.url}
21
+ `.trim();
22
+ };
@@ -0,0 +1,78 @@
1
+ import type { RuntimeExecutionContext } from '../types.js';
2
+
3
+ /**
4
+ * Creates the system prompt that sets the agent's identity and ground rules
5
+ * for the entire eval session.
6
+ *
7
+ * Injected once as the session-level system message. Individual phase prompts
8
+ * (plan, execute, evaluate, judge) are sent as user messages within this session.
9
+ *
10
+ * @param ctx - Runtime context with the application URL and output directory
11
+ * @returns The fully interpolated system prompt string
12
+ */
13
+ export const createSystemPrompt = (ctx: RuntimeExecutionContext): string => {
14
+ return `
15
+ You are an expert software test agent with perfect knowledge of the feature specification,
16
+ the application under test, and the testing tools at your disposal.
17
+
18
+ Application under test: ${ctx.url}
19
+
20
+ ## File tools use RELATIVE paths
21
+
22
+ The write_file, append_file, and read_file tools are scoped to an output directory.
23
+ Always use **relative paths** like "plan.json", "executions.jsonl", "evidence/screenshot.jpg".
24
+ Never use absolute paths with these tools.
25
+
26
+ You will be guided through phases in order:
27
+ 1. PLAN — produce a structured test plan (plan.json)
28
+ 2. EXECUTE — run each step's browser actions and collect evidence
29
+ 3. JUDGE — review all evidence and deliver a final pass/fail verdict (verdict.json)
30
+
31
+ All phases run in this single session. You have access to both browser tools
32
+ and file tools throughout.
33
+
34
+ ## Tool inventory
35
+
36
+ ### File tools
37
+ - **write_file** — write a file to the artifacts directory (plan.json, verdict.json)
38
+ - **append_file** — append a line to a file (executions.jsonl)
39
+ - **read_file** — read a file or list a directory from the artifacts directory.
40
+ Image files (png, jpg) are returned as inline vision content you can inspect.
41
+ Use this in the JUDGE phase to view screenshots taken during execution.
42
+
43
+ ### Browser tools
44
+ - **browser_navigate** — open a URL
45
+ - **browser_snapshot** — capture an accessibility snapshot (DOM tree). Use \`-i --json\` for interactive-only, JSON format.
46
+ - **browser_screenshot** — capture a screenshot. Returns the image inline so you can see it.
47
+ Use \`--annotate --full\` for labeled full-page screenshots.
48
+ - **browser_get_styles** — get computed styles of an element
49
+ - **browser_eval** — evaluate JavaScript in the page context
50
+ - **browser_click** — click an element (use @eN refs from snapshot, or CSS/role selectors)
51
+ - **browser_fill** — fill an input field
52
+ - **browser_type** — type text character by character
53
+ - **browser_press_key** — press a keyboard key
54
+ - **browser_hover** — hover over an element
55
+ - **browser_select** — select an option from a dropdown
56
+ - **browser_scroll** — scroll the page or an element
57
+ - **browser_wait** — wait for a condition: \`networkidle\`, text, selector, or time (ms)
58
+ - **browser_find** — find elements by role, text, label, placeholder, testid, or CSS
59
+ - **browser_errors** — get console errors and uncaught exceptions
60
+ - **browser_get_url** — get the current URL
61
+ - **browser_go_back** — navigate back
62
+ - **browser_reload** — reload the current page
63
+
64
+ ## Tool use rules
65
+ - Act immediately. Do not deliberate or plan between tool calls — execute then evaluate.
66
+ - Keep tool calls minimal. Prefer fewer, targeted calls over exhaustive probing.
67
+ - After navigation, always \`browser_wait\` for \`networkidle\` before interacting.
68
+ - Use \`@eN\` element refs from \`browser_snapshot\` for clicks, fills, and assertions.
69
+ - Take ONE screenshot per step (after the action). Skip redundant before-screenshots.
70
+ - Capture \`browser_errors\` only when something looks wrong, not on every step.
71
+ - Do NOT use browser tools during the PLAN phase — only describe actions as strings.
72
+ - Do NOT use browser tools during the JUDGE phase — use \`read_file\` to view evidence.
73
+
74
+ ## General rules
75
+ - Save artifacts using write_file / append_file with RELATIVE paths (e.g. "plan.json").
76
+ - Never fabricate evidence. If a tool call fails, report the failure honestly.
77
+ `.trim();
78
+ };
@@ -0,0 +1,105 @@
1
+ import { spawn, execSync, type ChildProcess } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ import { resolveAppKey, cleanup, waitForServer } from '../../utils/index.js';
6
+
7
+ /** Result of starting (or skipping) the app dev server. */
8
+ export interface AppServerResult {
9
+ /** The child process running the dev server, if one was spawned. */
10
+ serverProcess: ChildProcess | undefined;
11
+ /** The fully-qualified app URL (e.g. `http://localhost:3000/apps/my-app`). */
12
+ appUrl: string;
13
+ }
14
+
15
+ /**
16
+ * Kills any process listening on the given port.
17
+ *
18
+ * Uses `lsof` to find PIDs bound to the port and sends SIGTERM.
19
+ * Failures are silently ignored (port may already be free).
20
+ *
21
+ * @param port - The TCP port to free
22
+ */
23
+ function freePort(port: number): void {
24
+ try {
25
+ const pids = execSync(`lsof -ti tcp:${port}`, { encoding: 'utf-8' })
26
+ .trim()
27
+ .split('\n')
28
+ .filter(Boolean);
29
+ for (const pid of pids) {
30
+ try {
31
+ process.kill(parseInt(pid, 10), 'SIGTERM');
32
+ } catch {
33
+ // already dead
34
+ }
35
+ }
36
+ if (pids.length) {
37
+ console.log(`🔄 Killed stale process(es) on port ${port}: ${pids.join(', ')}`);
38
+ }
39
+ } catch {
40
+ // lsof exits non-zero when no matches — port is free
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Starts the Fusion app dev server for the given application path,
46
+ * or returns the caller-provided URL when the server is already running.
47
+ *
48
+ * @param absAppPath - Absolute path to the Fusion application directory
49
+ * @param options - Server configuration options
50
+ * @param options.port - Port number for the dev server
51
+ * @param options.host - Host address for the dev server
52
+ * @param options.url - When provided, skips server startup and uses this URL directly
53
+ * @param options.verbose - When true, inherits stdio from the spawned server process
54
+ * @returns The spawned server process (if any) and the resolved app URL
55
+ */
56
+ export async function startAppServer(
57
+ absAppPath: string,
58
+ options: { port: number; host: string; url?: string; verbose: boolean },
59
+ ): Promise<AppServerResult> {
60
+ // Caller already has a running server — skip startup entirely
61
+ if (options.url) {
62
+ return { serverProcess: undefined, appUrl: options.url };
63
+ }
64
+
65
+ const pkgJsonPath = join(absAppPath, 'package.json');
66
+ if (!existsSync(pkgJsonPath)) {
67
+ console.error(`❌ No package.json found at ${absAppPath}`);
68
+ process.exit(1);
69
+ }
70
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { name?: string };
71
+ if (!pkgJson.name) {
72
+ console.error(`❌ package.json at ${absAppPath} has no "name" field`);
73
+ process.exit(1);
74
+ }
75
+
76
+ console.log('📦 Resolving app manifest...');
77
+ const appKey = resolveAppKey(absAppPath);
78
+ console.log(`🚀 Starting app server (${appKey})...`);
79
+
80
+ // Kill stale dev servers on the target port from previous crashed runs
81
+ freePort(options.port);
82
+
83
+ const serverProcess = spawn(
84
+ 'ffc',
85
+ ['app', 'serve', '--host', options.host, '--port', String(options.port)],
86
+ {
87
+ cwd: absAppPath,
88
+ stdio: options.verbose ? 'inherit' : 'ignore',
89
+ detached: false,
90
+ },
91
+ );
92
+
93
+ const serverUrl = `http://localhost:${options.port}`;
94
+ const ready = await waitForServer(serverUrl, 60);
95
+ if (!ready) {
96
+ console.error('❌ Server failed to start within 60s');
97
+ cleanup(serverProcess);
98
+ process.exit(1);
99
+ }
100
+
101
+ const appUrl = `${serverUrl}/apps/${appKey}`;
102
+ console.log(`✅ Server ready at ${appUrl}`);
103
+
104
+ return { serverProcess, appUrl };
105
+ }
@@ -0,0 +1,187 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+
4
+ import type { CopilotSession } from '@github/copilot-sdk';
5
+
6
+ import { tryFormatMessage } from './format.js';
7
+
8
+ const TOOL_ICONS: Record<string, string> = {
9
+ browser_screenshot: '📷',
10
+ browser_navigate: '🌐',
11
+ browser_click: '👆',
12
+ browser_wait: '⏳',
13
+ browser_type: '⌨️',
14
+ browser_scroll: '📜',
15
+ browser_snapshot: '🔍',
16
+ };
17
+
18
+ /**
19
+ * Attaches a console logger to a Copilot session that renders tool progress
20
+ * with a single shared ora spinner (avoiding concurrent-spinner warnings).
21
+ *
22
+ * @param session - The active Copilot SDK session
23
+ * @param options - Optional config (e.g. requested model name for mismatch detection)
24
+ * @returns A handle with `stop()` to tear down the spinner (e.g. on SIGINT)
25
+ */
26
+ export function attachSessionLogger(
27
+ session: CopilotSession,
28
+ options?: { requestedModel?: string },
29
+ ): { stop: () => void } {
30
+ const pendingTools = new Map<string, string>();
31
+ let spinner: ReturnType<typeof ora> | null = null;
32
+
33
+ function syncSpinner(): void {
34
+ if (pendingTools.size === 0) {
35
+ if (spinner) {
36
+ spinner.stop();
37
+ spinner = null;
38
+ }
39
+ return;
40
+ }
41
+ const text = [...pendingTools.values()].join(chalk.dim(' | '));
42
+ if (spinner) {
43
+ spinner.text = text;
44
+ } else {
45
+ spinner = ora({ text, color: 'cyan' }).start();
46
+ }
47
+ }
48
+
49
+ function pauseSpinner(): void {
50
+ if (spinner) {
51
+ spinner.stop();
52
+ spinner = null;
53
+ }
54
+ }
55
+
56
+ function logCompletion(label: string, ok: boolean): void {
57
+ pauseSpinner();
58
+ const prefix = ok ? chalk.green('✔') : chalk.red('✖');
59
+ console.log(`${prefix} ${label}`);
60
+ syncSpinner();
61
+ }
62
+
63
+ session.on((event) => {
64
+ switch (event.type) {
65
+ case 'tool.execution_start': {
66
+ const { toolCallId, toolName, arguments: args } = event.data;
67
+ const detail = args?.url ?? args?.path ?? args?.load ?? args?.selector;
68
+ const icon = TOOL_ICONS[toolName] ?? '🔧';
69
+ const label =
70
+ typeof detail === 'string' ? `${icon} ${toolName} (${detail})` : `${icon} ${toolName}`;
71
+ pendingTools.set(toolCallId, label);
72
+ syncSpinner();
73
+ break;
74
+ }
75
+ case 'tool.execution_complete': {
76
+ const label = pendingTools.get(event.data.toolCallId);
77
+ if (label) {
78
+ pendingTools.delete(event.data.toolCallId);
79
+ logCompletion(label, event.data.success);
80
+ }
81
+ break;
82
+ }
83
+ case 'external_tool.completed': {
84
+ const label = pendingTools.get(event.data.requestId);
85
+ if (label) {
86
+ pendingTools.delete(event.data.requestId);
87
+ logCompletion(label, true);
88
+ }
89
+ break;
90
+ }
91
+ case 'assistant.reasoning': {
92
+ pauseSpinner();
93
+ console.log(chalk.dim(`🧠 ${event.data.content}`));
94
+ syncSpinner();
95
+ break;
96
+ }
97
+ case 'assistant.message': {
98
+ if (event.data.content) {
99
+ pauseSpinner();
100
+ const formatted = tryFormatMessage(event.data.content);
101
+ if (formatted) {
102
+ console.log(formatted);
103
+ } else {
104
+ console.log(chalk.green(`🤖 ${event.data.content}`));
105
+ }
106
+ syncSpinner();
107
+ }
108
+ break;
109
+ }
110
+ case 'permission.requested': {
111
+ const { requestId, permissionRequest } = event.data;
112
+ const toolName =
113
+ permissionRequest.kind === 'custom-tool'
114
+ ? permissionRequest.toolName
115
+ : permissionRequest.kind;
116
+ pendingTools.set(requestId, `🔐 ${toolName}`);
117
+ syncSpinner();
118
+ break;
119
+ }
120
+ case 'permission.completed': {
121
+ const label = pendingTools.get(event.data.requestId);
122
+ if (label) {
123
+ const approved = event.data.result?.kind === 'approved';
124
+ pendingTools.delete(event.data.requestId);
125
+ logCompletion(label, approved);
126
+ }
127
+ break;
128
+ }
129
+ // @ts-expect-error - missing case types in SDK typings
130
+ case 'session.tools_updated': {
131
+ // @ts-expect-error - model property missing in SDK typings
132
+ const negotiatedModel = event.data.model as string | undefined;
133
+ if (negotiatedModel) {
134
+ console.log(chalk.dim(`💾 ${negotiatedModel}`));
135
+ if (options?.requestedModel && negotiatedModel !== options.requestedModel) {
136
+ console.log(
137
+ chalk.yellow(
138
+ `⚠️ Requested model "${options.requestedModel}" but server negotiated "${negotiatedModel}"`,
139
+ ),
140
+ );
141
+ }
142
+ }
143
+ break;
144
+ }
145
+ case 'external_tool.requested':
146
+ case 'user.message': {
147
+ break;
148
+ }
149
+ case 'assistant.message_delta':
150
+ case 'assistant.reasoning_delta':
151
+ case 'assistant.streaming_delta':
152
+ case 'assistant.turn_start':
153
+ case 'assistant.turn_end':
154
+ case 'assistant.usage':
155
+ case 'pending_messages.modified':
156
+ case 'session.idle':
157
+ case 'session.usage_info': {
158
+ break;
159
+ }
160
+ case 'session.info': {
161
+ const { message } = event.data as { message?: string };
162
+ if (message) {
163
+ pauseSpinner();
164
+ console.log(chalk.dim(`ℹ️ ${message}`));
165
+ syncSpinner();
166
+ }
167
+ break;
168
+ }
169
+ case 'assistant.intent': {
170
+ const { intent } = event.data as { intent?: string };
171
+ if (intent) {
172
+ pauseSpinner();
173
+ console.log(chalk.yellow(`🎯 ${intent}`));
174
+ syncSpinner();
175
+ }
176
+ break;
177
+ }
178
+ default:
179
+ console.log(event);
180
+ break;
181
+ }
182
+ });
183
+
184
+ return {
185
+ stop: () => pauseSpinner(),
186
+ };
187
+ }
@@ -0,0 +1,28 @@
1
+ import type { AgentBrowserToolContext, DefineTool } from './types.js';
2
+
3
+ /**
4
+ * Creates the click interaction tool.
5
+ *
6
+ * @param context - Shared browser tool execution context
7
+ * @param defineTool - Copilot SDK helper used to declare tools
8
+ * @returns Copilot tool definition for clicking a page element
9
+ */
10
+ export function createClickTool(context: AgentBrowserToolContext, defineTool: DefineTool) {
11
+ return defineTool('browser_click', {
12
+ description: 'Click an element identified by a ref (@e1), CSS selector, or text selector.',
13
+ parameters: {
14
+ type: 'object' as const,
15
+ properties: {
16
+ target: {
17
+ type: 'string',
18
+ description: 'Element ref (e.g. @e3), CSS selector, or text selector',
19
+ },
20
+ },
21
+ required: ['target'],
22
+ },
23
+ handler: async (args) => {
24
+ const { target } = args as { target: string };
25
+ return context.invoke(['click', target]);
26
+ },
27
+ });
28
+ }
@@ -0,0 +1,26 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import type { AgentBrowserToolContext, DefineTool } from './types.js';
5
+
6
+ /**
7
+ * Creates the browser error collection tool.
8
+ *
9
+ * @param context - Shared browser tool execution context
10
+ * @param defineTool - Copilot SDK helper used to declare tools
11
+ * @returns Copilot tool definition for retrieving JavaScript errors
12
+ */
13
+ export function createErrorsTool(context: AgentBrowserToolContext, defineTool: DefineTool) {
14
+ return defineTool('browser_errors', {
15
+ description: 'Get all JavaScript console errors from the browser.',
16
+ parameters: {
17
+ type: 'object' as const,
18
+ properties: {},
19
+ },
20
+ handler: async () => {
21
+ const output = context.invoke(['errors']);
22
+ writeFileSync(join(context.outDir, 'errors.txt'), output, 'utf-8');
23
+ return output || 'No errors';
24
+ },
25
+ });
26
+ }
@@ -0,0 +1,40 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import type { AgentBrowserToolContext, DefineTool } from './types.js';
5
+
6
+ /**
7
+ * Creates the JavaScript evaluation tool.
8
+ *
9
+ * Runs a JS expression in the browser's page context and returns the result.
10
+ * Ideal for extracting computed values such as `getComputedStyle(document.body).backgroundColor`
11
+ * that cannot be reliably inferred from screenshots.
12
+ *
13
+ * @param context - Shared browser tool execution context
14
+ * @param defineTool - Copilot SDK helper used to declare tools
15
+ * @returns Copilot tool definition for evaluating arbitrary JavaScript in the browser
16
+ */
17
+ export function createEvalJsTool(context: AgentBrowserToolContext, defineTool: DefineTool) {
18
+ return defineTool('browser_eval', {
19
+ description:
20
+ 'Evaluate a JavaScript expression in the browser page context and return the result. Use for reading DOM values, computed styles, or element properties that need exact values (e.g. getComputedStyle(document.body).backgroundColor).',
21
+ parameters: {
22
+ type: 'object' as const,
23
+ properties: {
24
+ script: {
25
+ type: 'string',
26
+ description:
27
+ 'A single JavaScript expression to evaluate (e.g. "getComputedStyle(document.body).backgroundColor")',
28
+ },
29
+ },
30
+ required: ['script'],
31
+ },
32
+ handler: async (args) => {
33
+ const { script } = args as { script: string };
34
+ const output = context.invoke(['eval', script, '--json']);
35
+ const timestamp = Date.now();
36
+ writeFileSync(join(context.outDir, `eval-${timestamp}.json`), output, 'utf-8');
37
+ return output;
38
+ },
39
+ });
40
+ }
@@ -0,0 +1,29 @@
1
+ import type { AgentBrowserToolContext, DefineTool } from './types.js';
2
+
3
+ /**
4
+ * Creates the form fill tool.
5
+ *
6
+ * @param context - Shared browser tool execution context
7
+ * @param defineTool - Copilot SDK helper used to declare tools
8
+ * @returns Copilot tool definition for filling input fields
9
+ */
10
+ export function createFillTool(context: AgentBrowserToolContext, defineTool: DefineTool) {
11
+ return defineTool('browser_fill', {
12
+ description: 'Fill a form field with text. Clears existing content first.',
13
+ parameters: {
14
+ type: 'object' as const,
15
+ properties: {
16
+ target: {
17
+ type: 'string',
18
+ description: 'Element ref or CSS selector for the input field',
19
+ },
20
+ value: { type: 'string', description: 'Text to fill into the field' },
21
+ },
22
+ required: ['target', 'value'],
23
+ },
24
+ handler: async (args) => {
25
+ const { target, value } = args as { target: string; value: string };
26
+ return context.invoke(['fill', target, value]);
27
+ },
28
+ });
29
+ }
@@ -0,0 +1,49 @@
1
+ import type { AgentBrowserToolContext, DefineTool } from './types.js';
2
+
3
+ /**
4
+ * Creates the semantic locator tool.
5
+ *
6
+ * @param context - Shared browser tool execution context
7
+ * @param defineTool - Copilot SDK helper used to declare tools
8
+ * @returns Copilot tool definition for semantic element lookup and optional action execution
9
+ */
10
+ export function createFindTool(context: AgentBrowserToolContext, defineTool: DefineTool) {
11
+ return defineTool('browser_find', {
12
+ description:
13
+ 'Find an element using semantic locators (role, text, label) and optionally perform an action on it.',
14
+ parameters: {
15
+ type: 'object' as const,
16
+ properties: {
17
+ by: {
18
+ type: 'string',
19
+ description: 'Locator type: "role", "text", or "label"',
20
+ },
21
+ value: {
22
+ type: 'string',
23
+ description: 'Value to search for (role name, text content, or label)',
24
+ },
25
+ action: {
26
+ type: 'string',
27
+ description: 'Action to perform: "click" or "fill"',
28
+ },
29
+ name: { type: 'string', description: 'Accessible name filter (for role locators)' },
30
+ fillValue: { type: 'string', description: 'Text to fill (when action is "fill")' },
31
+ },
32
+ required: ['by', 'value'],
33
+ },
34
+ handler: async (args) => {
35
+ const { by, value, action, name, fillValue } = args as {
36
+ by: string;
37
+ value: string;
38
+ action?: string;
39
+ name?: string;
40
+ fillValue?: string;
41
+ };
42
+ const command = ['find', by, value];
43
+ if (action) command.push(action);
44
+ if (name) command.push('--name', name);
45
+ if (fillValue) command.push(fillValue);
46
+ return context.invoke(command);
47
+ },
48
+ });
49
+ }