@skyramp/mcp 0.0.65 → 0.1.0-rc.2

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 (50) hide show
  1. package/build/playwright/traceRecordingPrompt.js +30 -36
  2. package/build/prompts/architectPersona.js +19 -0
  3. package/build/prompts/test-maintenance/drift-analysis-prompt.js +11 -6
  4. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +49 -0
  5. package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -2
  6. package/build/prompts/test-recommendation/analysisOutputPrompt.js +42 -50
  7. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +125 -0
  8. package/build/prompts/test-recommendation/recommendationSections.js +121 -4
  9. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +151 -9
  10. package/build/prompts/test-recommendation/test-recommendation-prompt.js +416 -61
  11. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +455 -63
  12. package/build/prompts/testbot/testbot-prompts.js +111 -100
  13. package/build/prompts/testbot/testbot-prompts.test.js +142 -0
  14. package/build/resources/analysisResources.js +13 -5
  15. package/build/services/ScenarioGenerationService.js +2 -2
  16. package/build/services/ScenarioGenerationService.test.js +35 -0
  17. package/build/services/TestExecutionService.js +1 -1
  18. package/build/tools/code-refactor/modularizationTool.js +2 -2
  19. package/build/tools/executeSkyrampTestTool.js +4 -3
  20. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +51 -21
  21. package/build/tools/generate-tests/generateContractRestTool.js +26 -4
  22. package/build/tools/generate-tests/generateIntegrationRestTool.js +44 -13
  23. package/build/tools/generate-tests/generateScenarioRestTool.js +17 -39
  24. package/build/tools/generate-tests/generateUIRestTool.js +69 -4
  25. package/build/tools/submitReportTool.js +27 -13
  26. package/build/tools/test-management/analyzeChangesTool.js +32 -10
  27. package/build/tools/test-management/analyzeChangesTool.test.js +85 -0
  28. package/build/types/RepositoryAnalysis.js +25 -3
  29. package/build/types/TestRecommendation.js +5 -4
  30. package/build/types/TestTypes.js +44 -9
  31. package/build/utils/AnalysisStateManager.js +43 -9
  32. package/build/utils/AnalysisStateManager.test.js +35 -0
  33. package/build/utils/routeParsers.js +35 -0
  34. package/build/utils/routeParsers.test.js +66 -1
  35. package/build/utils/scenarioDrafting.js +207 -360
  36. package/build/utils/scenarioDrafting.test.js +191 -256
  37. package/build/utils/trace-parser.js +24 -6
  38. package/build/utils/trace-parser.test.js +140 -0
  39. package/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +3 -0
  40. package/node_modules/playwright/lib/mcp/browser/tab.js +8 -1
  41. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -2
  42. package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
  43. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +4 -4
  44. package/node_modules/playwright/lib/mcp/browser/tools/tabs.js +5 -4
  45. package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
  46. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -9
  47. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +304 -7
  48. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +128 -20
  49. package/package.json +2 -2
  50. package/node_modules/playwright/lib/mcp/terminal/help.json +0 -32
@@ -53,10 +53,10 @@ function parseSkyrampTrace(data) {
53
53
  path: entry.path || entry.Path || entry.url || entry.request?.url || "/",
54
54
  statusCode: entry.statusCode || entry.StatusCode || entry.status || entry.response?.status || 0,
55
55
  requestHeaders: entry.request?.headers ? sanitizeHeaders(entry.request.headers) : undefined,
56
- requestBody: entry.request?.body || entry.requestBody || entry.RequestBody,
56
+ requestBody: summarizeBody(entry.request?.body ?? entry.requestBody ?? entry.RequestBody),
57
57
  queryParams,
58
58
  responseHeaders: entry.response?.headers ? sanitizeHeaders(entry.response.headers) : undefined,
59
- responseBody: summarizeBody(entry.response?.body || entry.responseBody || entry.ResponseBody),
59
+ responseBody: summarizeBody(entry.response?.body ?? entry.responseBody ?? entry.ResponseBody),
60
60
  timestamp: entry.timestamp || entry.Timestamp || entry.request?.timestamp || "",
61
61
  };
62
62
  });
@@ -105,7 +105,7 @@ function parseHarTrace(harData) {
105
105
  path: url.pathname,
106
106
  statusCode: res.status || 0,
107
107
  requestHeaders: sanitizeHeaders(reqHeaders),
108
- requestBody: reqBody,
108
+ requestBody: summarizeBody(reqBody),
109
109
  queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
110
110
  responseHeaders: sanitizeHeaders(resHeaders),
111
111
  responseBody: summarizeBody(resBody),
@@ -156,6 +156,8 @@ export async function parseTraceFile(filePath) {
156
156
  return { entries, userFlows, format };
157
157
  }
158
158
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", "coverage", "__pycache__", ".venv", "venv"]);
159
+ /** Known test-artifact directories where testbot-generated traces are written. */
160
+ const TRACE_SCAN_DIRS = [".skyramp", "tests", "test", "e2e", "playwright"];
159
161
  /**
160
162
  * Recursively scan a directory for files matching a predicate, up to maxDepth levels.
161
163
  */
@@ -180,6 +182,22 @@ function scanDir(dir, predicate, maxDepth, results) {
180
182
  }
181
183
  }
182
184
  }
185
+ /**
186
+ * Scan only known test-artifact directories for trace files.
187
+ * Root-level files are checked at depth 0; named test-artifact subdirs are scanned
188
+ * at full depth. This prevents picking up committed demo assets (e.g. frontend/public/traces/).
189
+ */
190
+ function scanTraceArtifactDirs(repositoryPath, predicate, results) {
191
+ // Root-level files only (depth 0)
192
+ scanDir(repositoryPath, predicate, 0, results);
193
+ // Named test-artifact subdirectories (full depth)
194
+ for (const dir of TRACE_SCAN_DIRS) {
195
+ const full = path.join(repositoryPath, dir);
196
+ if (fs.existsSync(full)) {
197
+ scanDir(full, predicate, 5, results);
198
+ }
199
+ }
200
+ }
183
201
  /**
184
202
  * Discover trace JSON files in a repository path.
185
203
  */
@@ -191,12 +209,12 @@ export function discoverTraceFiles(repositoryPath) {
191
209
  if (fs.existsSync(full))
192
210
  found.push(full);
193
211
  }
194
- // Recursive scan: any *trace*.json|har, but exclude scenario files and test output files
212
+ // Recursive scan scoped to test-artifact dirs: any *trace*.json|har, excluding scenario/test output files
195
213
  const isTraceJson = (name) => /\.(json|har)$/i.test(name) &&
196
214
  /trace/i.test(name) &&
197
215
  !/^scenario_/i.test(name) &&
198
216
  !/_test\.(json|har)$/i.test(name);
199
- scanDir(repositoryPath, isTraceJson, 5, found);
217
+ scanTraceArtifactDirs(repositoryPath, isTraceJson, found);
200
218
  // Deduplicate and sort for deterministic ordering
201
219
  return [...new Set(found)].sort();
202
220
  }
@@ -209,6 +227,6 @@ export function discoverPlaywrightZips(repositoryPath) {
209
227
  const isPlaywrightZip = (name) => /\.zip$/i.test(name) && (/playwright/i.test(name) ||
210
228
  /_trace\.zip$/i.test(name) ||
211
229
  name.toLowerCase() === "trace.zip");
212
- scanDir(repositoryPath, isPlaywrightZip, 5, found);
230
+ scanTraceArtifactDirs(repositoryPath, isPlaywrightZip, found);
213
231
  return [...new Set(found)].sort();
214
232
  }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Unit tests for trace-parser.ts — specifically the scanTraceArtifactDirs scoping
3
+ * introduced to prevent demo/fixture files (e.g. frontend/public/traces/) from being
4
+ * misidentified as testbot-generated traces.
5
+ */
6
+ import * as fs from "fs";
7
+ import * as os from "os";
8
+ import * as path from "path";
9
+ import { discoverTraceFiles, discoverPlaywrightZips } from "./trace-parser.js";
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers
12
+ // ---------------------------------------------------------------------------
13
+ function mkdirp(dir) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+ function touch(file) {
17
+ mkdirp(path.dirname(file));
18
+ fs.writeFileSync(file, "");
19
+ }
20
+ function withTempRepo(fn) {
21
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "trace-parser-test-"));
22
+ try {
23
+ fn(dir);
24
+ }
25
+ finally {
26
+ fs.rmSync(dir, { recursive: true, force: true });
27
+ }
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // discoverPlaywrightZips — scoping tests
31
+ // ---------------------------------------------------------------------------
32
+ describe("discoverPlaywrightZips — scanTraceArtifactDirs scoping", () => {
33
+ it("does NOT discover playwright zip in frontend/public/traces/ (demo fixture dir)", () => {
34
+ withTempRepo(repo => {
35
+ touch(path.join(repo, "frontend", "public", "traces", "ui_test_playwright.zip"));
36
+ expect(discoverPlaywrightZips(repo)).toEqual([]);
37
+ });
38
+ });
39
+ it("discovers playwright zip in tests/ (test-artifact dir)", () => {
40
+ withTempRepo(repo => {
41
+ const zip = path.join(repo, "tests", "ui_test_playwright.zip");
42
+ touch(zip);
43
+ expect(discoverPlaywrightZips(repo)).toContain(zip);
44
+ });
45
+ });
46
+ it("discovers playwright zip in .skyramp/ (test-artifact dir)", () => {
47
+ withTempRepo(repo => {
48
+ const zip = path.join(repo, ".skyramp", "recording_playwright.zip");
49
+ touch(zip);
50
+ expect(discoverPlaywrightZips(repo)).toContain(zip);
51
+ });
52
+ });
53
+ it("discovers playwright zip in e2e/ (test-artifact dir)", () => {
54
+ withTempRepo(repo => {
55
+ const zip = path.join(repo, "e2e", "flow_playwright.zip");
56
+ touch(zip);
57
+ expect(discoverPlaywrightZips(repo)).toContain(zip);
58
+ });
59
+ });
60
+ it("discovers playwright zip in playwright/ (test-artifact dir)", () => {
61
+ withTempRepo(repo => {
62
+ const zip = path.join(repo, "playwright", "trace.zip");
63
+ touch(zip);
64
+ expect(discoverPlaywrightZips(repo)).toContain(zip);
65
+ });
66
+ });
67
+ it("does NOT discover zip in src/ (not a test-artifact dir)", () => {
68
+ withTempRepo(repo => {
69
+ touch(path.join(repo, "src", "recordings", "ui_playwright.zip"));
70
+ expect(discoverPlaywrightZips(repo)).toEqual([]);
71
+ });
72
+ });
73
+ it("does NOT discover zip in deeply nested non-test dir", () => {
74
+ withTempRepo(repo => {
75
+ touch(path.join(repo, "frontend", "src", "assets", "demo_playwright.zip"));
76
+ expect(discoverPlaywrightZips(repo)).toEqual([]);
77
+ });
78
+ });
79
+ });
80
+ // ---------------------------------------------------------------------------
81
+ // discoverTraceFiles — scoping tests
82
+ // ---------------------------------------------------------------------------
83
+ describe("discoverTraceFiles — scanTraceArtifactDirs scoping", () => {
84
+ it("does NOT discover trace.json nested under frontend/public/traces/", () => {
85
+ withTempRepo(repo => {
86
+ touch(path.join(repo, "frontend", "public", "traces", "backend_trace.json"));
87
+ const found = discoverTraceFiles(repo);
88
+ // fixed-name root candidates don't match "backend_trace.json", and scan won't reach frontend/
89
+ expect(found.some(f => f.includes("frontend"))).toBe(false);
90
+ });
91
+ });
92
+ it("discovers trace.json in tests/ dir", () => {
93
+ withTempRepo(repo => {
94
+ const f = path.join(repo, "tests", "backend_trace.json");
95
+ touch(f);
96
+ expect(discoverTraceFiles(repo)).toContain(f);
97
+ });
98
+ });
99
+ it("discovers trace.json in .skyramp/ dir", () => {
100
+ withTempRepo(repo => {
101
+ const f = path.join(repo, ".skyramp", "skyramp_trace.json");
102
+ touch(f);
103
+ expect(discoverTraceFiles(repo)).toContain(f);
104
+ });
105
+ });
106
+ it("discovers root-level trace.json", () => {
107
+ withTempRepo(repo => {
108
+ const f = path.join(repo, "trace.json");
109
+ touch(f);
110
+ expect(discoverTraceFiles(repo)).toContain(f);
111
+ });
112
+ });
113
+ it("discovers root-level skyramp_traces.json via fixed-name check", () => {
114
+ withTempRepo(repo => {
115
+ const f = path.join(repo, "skyramp_traces.json");
116
+ touch(f);
117
+ expect(discoverTraceFiles(repo)).toContain(f);
118
+ });
119
+ });
120
+ it("does NOT discover scenario_ json files (excluded by predicate)", () => {
121
+ withTempRepo(repo => {
122
+ touch(path.join(repo, "tests", "scenario_orders_trace.json"));
123
+ expect(discoverTraceFiles(repo)).toEqual([]);
124
+ });
125
+ });
126
+ it("does NOT discover _test.json files (excluded by predicate)", () => {
127
+ withTempRepo(repo => {
128
+ touch(path.join(repo, "tests", "orders_trace_test.json"));
129
+ expect(discoverTraceFiles(repo)).toEqual([]);
130
+ });
131
+ });
132
+ it("results are deduplicated when fixed-name and scan both find the same root file", () => {
133
+ withTempRepo(repo => {
134
+ const f = path.join(repo, "trace.json");
135
+ touch(f);
136
+ const found = discoverTraceFiles(repo);
137
+ expect(found.filter(x => x === f)).toHaveLength(1);
138
+ });
139
+ });
140
+ });
@@ -74,6 +74,9 @@ ${String(error)}` }],
74
74
  }
75
75
  return responseObject;
76
76
  }
77
+ get context() {
78
+ return this._context;
79
+ }
77
80
  serverClosed() {
78
81
  void this._context?.dispose().catch(import_log.logUnhandledError);
79
82
  }
@@ -253,7 +253,14 @@ class Tab extends import_events.EventEmitter {
253
253
  if (param.element)
254
254
  locator = locator.describe(param.element);
255
255
  const { resolvedSelector } = await locator._resolveSelector();
256
- return { locator, resolved: (0, import_utils.asLocator)("javascript", resolvedSelector) };
256
+ let fixedSelector = resolvedSelector;
257
+ if (!resolvedSelector.includes("internal:control=enter-frame") && /^(css=)?iframe\b[^>]*\s+>>\s+/.test(resolvedSelector)) {
258
+ fixedSelector = resolvedSelector.replace(
259
+ /^(css=)?(iframe\b[^>]*)\s+>>\s+/,
260
+ "css=$2 >> internal:control=enter-frame >> "
261
+ );
262
+ }
263
+ return { locator, resolved: (0, import_utils.asLocator)("javascript", fixedSelector) };
257
264
  } catch (e) {
258
265
  throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`);
259
266
  }
@@ -69,14 +69,15 @@ const pressSequentially = (0, import_tool.defineTabTool)({
69
69
  const typeSchema = import_snapshot.elementSchema.extend({
70
70
  text: import_mcpBundle.z.string().describe("Text to type into the element"),
71
71
  submit: import_mcpBundle.z.boolean().optional().describe("Whether to submit entered text (press Enter after)"),
72
- slowly: import_mcpBundle.z.boolean().optional().describe("Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.")
72
+ slowly: import_mcpBundle.z.boolean().optional().describe("DO NOT USE \u2014 causes silent failures in contenteditable and rich text editors. Use default fast fill instead."),
73
+ clear: import_mcpBundle.z.boolean().optional().describe("Ignored \u2014 browser_type always replaces existing content. Do not pass this parameter.")
73
74
  });
74
75
  const type = (0, import_tool.defineTabTool)({
75
76
  capability: "core",
76
77
  schema: {
77
78
  name: "browser_type",
78
79
  title: "Type text",
79
- description: "Type text into editable element",
80
+ description: "Type text into an editable element. Auto-focuses and replaces existing content. NEVER call browser_click on the field first \u2014 clicking before typing injects extra network requests that corrupt the trace.",
80
81
  inputSchema: typeSchema,
81
82
  type: "input"
82
83
  },
@@ -28,7 +28,7 @@ const navigate = (0, import_tool.defineTool)({
28
28
  schema: {
29
29
  name: "browser_navigate",
30
30
  title: "Navigate to a URL",
31
- description: "Navigate to a URL",
31
+ description: "Navigate to a URL. Prefer direct navigation to known URLs over clicking menus or carousels \u2014 menus may open unwanted popups and carousel items cause strict-mode violations. To reload the current page, navigate to the same URL \u2014 the backend converts this to page.reload(). After navigating to a folder where content was just created/edited, always call browser_wait_for with the file name before interacting with it.",
32
32
  inputSchema: import_mcpBundle.z.object({
33
33
  url: import_mcpBundle.z.string().describe("The URL to navigate to")
34
34
  }),
@@ -30,7 +30,7 @@ const snapshot = (0, import_tool.defineTool)({
30
30
  schema: {
31
31
  name: "browser_snapshot",
32
32
  title: "Page snapshot",
33
- description: "Capture accessibility snapshot of the current page, this is better than screenshot",
33
+ description: "Capture the ARIA accessibility tree of the current page. Returns element refs required by all interaction tools (browser_click, browser_type, browser_hover, etc.). Call before any interaction and after every action that changes the page to get fresh refs. If any interaction tool fails with a stale ref error, call this first to refresh.",
34
34
  inputSchema: import_mcpBundle.z.object({
35
35
  filename: import_mcpBundle.z.string().optional().describe("Save snapshot to markdown file instead of returning it in the response.")
36
36
  }),
@@ -55,7 +55,7 @@ const click = (0, import_tool.defineTabTool)({
55
55
  schema: {
56
56
  name: "browser_click",
57
57
  title: "Click",
58
- description: "Perform click on a web page",
58
+ description: "Click an element on the page. Always click the actual interactive element (button, link, input) \u2014 never a container or wrapper div. NEVER click a text field before typing \u2014 browser_type auto-focuses; a prior click injects extra network requests that corrupt the trace. NEVER click a row or link in a file list to access contextual actions \u2014 use browser_hover on the row instead.",
59
59
  inputSchema: clickSchema,
60
60
  type: "input"
61
61
  },
@@ -111,7 +111,7 @@ const hover = (0, import_tool.defineTabTool)({
111
111
  schema: {
112
112
  name: "browser_hover",
113
113
  title: "Hover mouse",
114
- description: "Hover over element on page",
114
+ description: "Hover over an element. Required pattern for contextual actions (More Options, Delete, Rename, \u22EF) on list/grid rows: (1) call browser_hover on the row element, (2) call browser_snapshot to reveal hover-only controls, (3) click the target button. NEVER click the row itself \u2014 that navigates into the item.",
115
115
  inputSchema: elementSchema,
116
116
  type: "input"
117
117
  },
@@ -132,7 +132,7 @@ const selectOption = (0, import_tool.defineTabTool)({
132
132
  schema: {
133
133
  name: "browser_select_option",
134
134
  title: "Select option",
135
- description: "Select an option in a dropdown",
135
+ description: "Select an option in a native <select> dropdown only. For custom dropdowns (Radix, MUI, etc.) that appear as combobox in the snapshot, do NOT use this tool \u2014 instead: (1) click the combobox to open it, (2) call browser_snapshot to see the listbox options, (3) click the desired option.",
136
136
  inputSchema: selectOptionSchema,
137
137
  type: "input"
138
138
  },
@@ -29,10 +29,10 @@ const browserTabs = (0, import_tool.defineTool)({
29
29
  schema: {
30
30
  name: "browser_tabs",
31
31
  title: "Manage tabs",
32
- description: "List, create, close, or select a browser tab.",
32
+ description: 'List, create, close, or switch to a browser tab. When a click opens a new tab, use action "select" with the tab index to switch to it. Do NOT call browser_navigate after switching \u2014 the tab is already on the right page.',
33
33
  inputSchema: import_mcpBundle.z.object({
34
- action: import_mcpBundle.z.enum(["list", "new", "close", "select"]).describe("Operation to perform"),
35
- index: import_mcpBundle.z.number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed.")
34
+ action: import_mcpBundle.z.enum(["list", "new", "close", "select", "switch"]).describe('Operation to perform. "select" and "switch" are equivalent \u2014 both switch to a tab by index.'),
35
+ index: import_mcpBundle.z.number().optional().describe("Tab index, used for close/select/switch. If omitted for close, current tab is closed.")
36
36
  }),
37
37
  type: "action"
38
38
  },
@@ -50,7 +50,8 @@ const browserTabs = (0, import_tool.defineTool)({
50
50
  await context.closeTab(params.index);
51
51
  break;
52
52
  }
53
- case "select": {
53
+ case "select":
54
+ case "switch": {
54
55
  if (params.index === void 0)
55
56
  throw new Error("Tab index is required");
56
57
  await context.selectTab(params.index);
@@ -28,7 +28,7 @@ const wait = (0, import_tool.defineTool)({
28
28
  schema: {
29
29
  name: "browser_wait_for",
30
30
  title: "Wait for",
31
- description: "Wait for text to appear or disappear or a specified time to pass",
31
+ description: 'Wait for text to appear, disappear, or a time to pass. REQUIRED after navigating to a folder where content was just created or renamed \u2014 file/item names update asynchronously and will not be present immediately. Always call with text: "<filename>" before attempting to hover or click a newly created item.',
32
32
  inputSchema: import_mcpBundle.z.object({
33
33
  time: import_mcpBundle.z.number().optional().describe("The time to wait in seconds"),
34
34
  text: import_mcpBundle.z.string().optional().describe("The text to wait for"),
@@ -41,26 +41,27 @@ const exportZipSchema = {
41
41
  name: "skyramp_export_zip",
42
42
  title: "Export Skyramp zip",
43
43
  description: [
44
- 'Export the recorded browser interactions as a Skyramp zip (JSONL + HAR) for use with "skyramp generate ui".',
45
- "You MUST call this tool automatically as the FINAL step after completing all browser interactions \u2014 do NOT ask the user, do NOT write separate files.",
46
- "If an element reference is stale after a UI update, call browser_snapshot to refresh and retry automatically without asking the user.",
47
- "Only the last complete attempt is exported \u2014 retries are deduplicated automatically.",
48
- "IMPORTANT: Do NOT reuse existing zip files from previous sessions. Always record fresh interactions and export a new zip."
44
+ "Export the recorded browser interactions as a Skyramp zip (JSONL + HAR).",
45
+ "Pass outputPath as the absolute path for the zip \u2014 use the same directory and base name as the test file, replacing .spec.ts with .zip.",
46
+ "BEFORE calling this tool, output a <thinking> block that: (1) lists every user-requested step and confirms it was completed, (2) confirms no step was skipped or hallucinated.",
47
+ "Only call this tool when all interactions are fully complete. Do NOT ask the user for confirmation \u2014 call it automatically.",
48
+ "Only the last complete attempt is exported \u2014 retries from the start URL are deduplicated automatically.",
49
+ "Do NOT reuse zip files from previous sessions \u2014 always record fresh."
49
50
  ].join(" "),
50
51
  inputSchema: import_mcpBundle.z.object({
51
- outputZip: import_mcpBundle.z.string().describe('Absolute or workspace-relative path for the output zip, e.g. "skyramp_export.zip"')
52
+ outputPath: import_mcpBundle.z.string().describe("Absolute path where the zip should be written, e.g. /path/to/box_notes.zip. Use the same directory and base name as the test file, replacing .spec.ts with .zip.")
52
53
  }),
53
54
  type: "readOnly"
54
55
  };
55
56
  function createExportZipHandler(ctx) {
56
- return async (_params) => {
57
+ return async (params) => {
57
58
  if (!ctx.trackedActions.length) {
58
59
  return {
59
- content: [{ type: "text", text: "### Error\nNo browser actions recorded. Use browser_navigate and browser_click/browser_type first." }],
60
+ content: [{ type: "text", text: "### Error\nNo browser interactions recorded. At minimum, call browser_navigate to open the target URL, then use browser_click or browser_type to record at least one user action before exporting." }],
60
61
  isError: true
61
62
  };
62
63
  }
63
- const outputZip = import_path.default.isAbsolute(_params.outputZip) ? _params.outputZip : import_path.default.resolve(ctx.rootPath, _params.outputZip);
64
+ const outputZip = import_path.default.resolve(params.outputPath);
64
65
  const { jsonl: jsonlContent, actionCount, skipped } = (0, import_skyRampExport.buildJsonlContent)(ctx.trackedActions, "chromium", ctx.harPath);
65
66
  await (0, import_skyRampExport.writeSkyrampZip)(outputZip, jsonlContent, ctx.harPath);
66
67
  const skippedNote = skipped.length ? `