@skyramp/mcp 0.2.1 → 0.2.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.
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var skyRampImport_exports = {};
30
+ __export(skyRampImport_exports, {
31
+ actionsFromFile: () => actionsFromFile,
32
+ actionsFromPath: () => actionsFromPath,
33
+ actionsFromZip: () => actionsFromZip,
34
+ parseJsonl: () => parseJsonl
35
+ });
36
+ module.exports = __toCommonJS(skyRampImport_exports);
37
+ var import_fs = __toESM(require("fs"));
38
+ var import_utils = require("playwright-core/lib/utils");
39
+ const SKYRAMP_ACTIVITIES_FILE = "skyramp_playwright.txt";
40
+ const ACTION_NAMES = /* @__PURE__ */ new Set([
41
+ "marker",
42
+ "comment",
43
+ "beginBlock",
44
+ "endBlock",
45
+ "check",
46
+ "click",
47
+ "hover",
48
+ "closePage",
49
+ "fill",
50
+ "pressSequentially",
51
+ "navigate",
52
+ "openPage",
53
+ "press",
54
+ "select",
55
+ "uncheck",
56
+ "setInputFiles",
57
+ "assertText",
58
+ "assertValue",
59
+ "assertChecked",
60
+ "assertVisible",
61
+ "assertSnapshot",
62
+ "visualSnapshot",
63
+ "assertTableCell",
64
+ "dragTo",
65
+ "dragAndDrop",
66
+ "diagramNodeAdd",
67
+ "diagramLinkAdd",
68
+ "selectArea",
69
+ "tableSnapshot",
70
+ "domSnapshot",
71
+ "fileChooser",
72
+ "penTool",
73
+ "mouse.wheel",
74
+ "mouse.move",
75
+ "mouse.down",
76
+ "mouse.up",
77
+ "modalOpen",
78
+ "modalClose",
79
+ "iframeLoad",
80
+ "assertApiRequest",
81
+ "waitForSelector",
82
+ "waitForTimeout"
83
+ ]);
84
+ function isActionName(name) {
85
+ return ACTION_NAMES.has(name);
86
+ }
87
+ async function actionsFromZip(zipPath) {
88
+ const zip = new import_utils.ZipFile(zipPath);
89
+ try {
90
+ const entries = await zip.entries();
91
+ const entry = entries.find((e) => e === SKYRAMP_ACTIVITIES_FILE) ?? entries.find((e) => e.endsWith(".jsonl")) ?? entries.find((e) => e.endsWith(".txt"));
92
+ if (!entry)
93
+ throw new Error(`No activities file found in ${zipPath}. Expected '${SKYRAMP_ACTIVITIES_FILE}' or a .jsonl/.txt file.`);
94
+ const buffer = await zip.read(entry);
95
+ return parseJsonl(buffer.toString("utf-8"));
96
+ } finally {
97
+ zip.close();
98
+ }
99
+ }
100
+ function actionsFromFile(jsonlPath) {
101
+ return parseJsonl(import_fs.default.readFileSync(jsonlPath, "utf-8"));
102
+ }
103
+ async function actionsFromPath(tracePath) {
104
+ return tracePath.endsWith(".zip") ? await actionsFromZip(tracePath) : actionsFromFile(tracePath);
105
+ }
106
+ function parseJsonl(content) {
107
+ const result = [];
108
+ for (const line of content.split("\n")) {
109
+ const trimmed = line.trim();
110
+ if (!trimmed)
111
+ continue;
112
+ const action = parseLine(trimmed);
113
+ if (action)
114
+ result.push(action);
115
+ }
116
+ return result;
117
+ }
118
+ function parseLine(line) {
119
+ let entry;
120
+ try {
121
+ entry = JSON.parse(line);
122
+ } catch {
123
+ return null;
124
+ }
125
+ if (!entry.name || !isActionName(entry.name))
126
+ return null;
127
+ const { pageGuid, pageAlias, framePath, locator: _locator, ...actionFields } = entry;
128
+ const frame = {
129
+ pageGuid: pageGuid ?? "",
130
+ pageAlias: pageAlias ?? "page",
131
+ framePath: framePath ?? []
132
+ };
133
+ const ts = typeof entry.timestamp === "number" ? entry.timestamp : typeof entry.timestamp === "string" && /^\d+$/.test(entry.timestamp.trim()) ? Number(entry.timestamp.trim()) : Date.now();
134
+ return {
135
+ frame,
136
+ action: actionFields,
137
+ startTime: ts
138
+ };
139
+ }
140
+ // Annotate the CommonJS export names for ESM import in node:
141
+ 0 && (module.exports = {
142
+ actionsFromFile,
143
+ actionsFromPath,
144
+ actionsFromZip,
145
+ parseJsonl
146
+ });
@@ -44,6 +44,9 @@ var import_skyRampExport = require("../test/skyRampExport");
44
44
  var import_exportTool = require("./exportTool");
45
45
  var import_assertTool = require("./assertTool");
46
46
  var import_assertApiRequestTool = require("./assertApiRequestTool");
47
+ var import_loadTraceTool = require("./loadTraceTool");
48
+ var import_skyRampImport = require("./skyRampImport");
49
+ var import_utils = require("playwright-core/lib/utils");
47
50
  var import_types = require("./types");
48
51
  const traceDebug = (0, import_utilsBundle.debug)("pw:mcp:trace");
49
52
  class TraceRecordingBackend {
@@ -116,7 +119,7 @@ class TraceRecordingBackend {
116
119
  }
117
120
  async listTools() {
118
121
  const browserTools = await this._browserBackend.listTools();
119
- return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)()];
122
+ return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)(), (0, import_loadTraceTool.loadTraceMcpTool)()];
120
123
  }
121
124
  async callTool(name, args, progress) {
122
125
  if (!this._initialized)
@@ -139,6 +142,10 @@ class TraceRecordingBackend {
139
142
  }
140
143
  return exportResult;
141
144
  }
145
+ if (name === import_loadTraceTool.loadTraceSchema.name) {
146
+ const parsed = import_loadTraceTool.loadTraceSchema.inputSchema.parse(args || {});
147
+ return this._handleLoadTrace(parsed);
148
+ }
142
149
  if (name === import_assertTool.assertToolSchema.name) {
143
150
  const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
144
151
  return this._handleAssert(parsed);
@@ -313,6 +320,216 @@ Reloaded current page: ${currentUrl}
313
320
  serverClosed() {
314
321
  void this._autoExportAndClose().catch(import_log.logUnhandledError);
315
322
  }
323
+ /**
324
+ * Load a prior Skyramp trace and replay it against the live browser, honoring
325
+ * an optional stop point, then seed _trackedActions with the replayed actions
326
+ * so the model can continue recording and export a combined trace.
327
+ */
328
+ async _handleLoadTrace(params) {
329
+ const stopParams = ["stopAtStep", "stopAtUrl", "stopBefore"].filter((k) => {
330
+ const v = params[k];
331
+ return v !== void 0 && v !== "";
332
+ });
333
+ if (stopParams.length > 1)
334
+ return { content: [{ type: "text", text: `### Error
335
+ Pass at most ONE of stopAtStep / stopAtUrl / stopBefore (got ${stopParams.join(", ")}). Omit all three to replay the whole trace.` }], isError: true };
336
+ let loadedActions;
337
+ try {
338
+ loadedActions = await (0, import_skyRampImport.actionsFromPath)(params.path);
339
+ } catch (e) {
340
+ return { content: [{ type: "text", text: `### Error
341
+ Failed to load trace from ${params.path}: ${e.message}` }], isError: true };
342
+ }
343
+ if (!loadedActions.length)
344
+ return { content: [{ type: "text", text: `### Error
345
+ No actions found in ${params.path}.` }], isError: true };
346
+ if (params.dryRun) {
347
+ const stepList = loadedActions.map((a, i) => (0, import_loadTraceTool.describeStep)(a, i)).join("\n");
348
+ return {
349
+ content: [{
350
+ type: "text",
351
+ text: `### Trace loaded (dry run)
352
+ ${loadedActions.length} steps in ${params.path}. Nothing was replayed.
353
+
354
+ Steps:
355
+ ${stepList}
356
+
357
+ Call skyramp_load_trace again with stopAtStep / stopAtUrl / stopBefore (or none of them to replay all) to replay.`
358
+ }]
359
+ };
360
+ }
361
+ try {
362
+ await this._browserBackend.context.ensureTab();
363
+ } catch {
364
+ }
365
+ let result;
366
+ try {
367
+ result = await (0, import_loadTraceTool.replayActions)(loadedActions, { pageForAlias: (alias) => this._pageForAlias(alias) }, params);
368
+ } catch (e) {
369
+ return { content: [{ type: "text", text: `### Error
370
+ Replay failed: ${e.message}` }], isError: true };
371
+ }
372
+ for (let i = 0; i < result.stopIndex; i++)
373
+ this._seedTrackedAction(loadedActions[i]);
374
+ if (result.stopIndex > 0)
375
+ this._currentPageAlias = loadedActions[result.stopIndex - 1].frame.pageAlias || "page";
376
+ const remaining = (0, import_loadTraceTool.listStepsFrom)(loadedActions, result.stopIndex);
377
+ const stoppedNote = (0, import_loadTraceTool.describeStopReason)(result);
378
+ const errorNote = result.error ? `
379
+
380
+ \u26A0\uFE0F Replay stopped at step ${result.error.stepIndex} (${result.error.actionName}): ${result.error.message}
381
+ You can continue recording from this point or fix the issue.` : "";
382
+ return {
383
+ content: [{
384
+ type: "text",
385
+ text: `### Trace replayed
386
+ ${result.completedCount} of ${loadedActions.length} steps replayed from ${params.path}. ${stoppedNote}${errorNote}
387
+
388
+ Remaining steps (NOT replayed):
389
+ ${remaining}
390
+
391
+ Continue recording with browser_* tools, then call skyramp_export_zip to write the combined trace.`
392
+ }],
393
+ isError: result.stopReason === "error"
394
+ };
395
+ }
396
+ /**
397
+ * Resolve a JSONL page alias to a live page. Numeric aliases ('page',
398
+ * 'pageN') map by tab index; semantic aliases ('popupPage') match by the
399
+ * popup URL pattern, falling back to the most recently opened tab.
400
+ */
401
+ _pageForAlias(alias) {
402
+ const tabs = this._browserBackend.context?.tabs() ?? [];
403
+ if (!tabs.length)
404
+ return void 0;
405
+ const numeric = alias === "page" ? 0 : /^page(\d+)$/.exec(alias)?.[1];
406
+ if (numeric !== void 0 && numeric !== null) {
407
+ const idx = Number(numeric);
408
+ return (tabs[idx] ?? tabs[tabs.length - 1]).page;
409
+ }
410
+ const popupTab = tabs.find((t) => /chrome-extension:\/\/[a-p]{32}\/popup\//.test(t.page.url()));
411
+ return (popupTab ?? tabs[tabs.length - 1]).page;
412
+ }
413
+ /**
414
+ * Convert a replayed ActionInContext back into a TrackedAction so it merges
415
+ * with freshly-recorded actions. The export pipeline (buildJsonlContent)
416
+ * re-derives the JSONL selector from the `code` field, so we regenerate clean
417
+ * Playwright code from the stored internal: selector via asLocator.
418
+ */
419
+ _seedTrackedAction(action) {
420
+ const a = action.action;
421
+ const pageAlias = action.frame.pageAlias || "page";
422
+ if (a.name === "navigate") {
423
+ this._trackedActions.push({
424
+ toolName: "browser_navigate",
425
+ args: { url: a.url },
426
+ code: "",
427
+ timestamp: action.startTime,
428
+ pageAlias
429
+ });
430
+ return;
431
+ }
432
+ const locatorExpr = a.selector ? (0, import_utils.asLocator)("javascript", a.selector) : "";
433
+ const seeded = this._seedTrackedActionFields(a, locatorExpr);
434
+ if (!seeded)
435
+ return;
436
+ this._trackedActions.push({
437
+ ...seeded,
438
+ timestamp: action.startTime,
439
+ pageAlias,
440
+ framePath: action.frame.framePath?.length ? action.frame.framePath : void 0
441
+ });
442
+ }
443
+ /**
444
+ * Build the { toolName, code, args } triple a seeded (replayed) action must
445
+ * carry so it round-trips through skyRampExport.buildJsonlContent exactly as
446
+ * a freshly-recorded action would. toolName, code and args are emitted
447
+ * together (not via separate mappers) because export keys on all three:
448
+ * trackedActionToJsonl dispatches on toolName, derives the locator from code,
449
+ * and reads specific args (checked, values, type). A mismatch silently
450
+ * mis-exports (e.g. hover as click) or drops the action.
451
+ *
452
+ * Code uses single quotes to match the live recorder's format
453
+ * (extractFillText / extractSelectOptions only match single-quoted calls).
454
+ * Returns null for unsupported actions so they are skipped, never defaulted.
455
+ */
456
+ _seedTrackedActionFields(a, locatorExpr) {
457
+ const sq = (s) => `'${String(s).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}'`;
458
+ const SELECTOR_REQUIRED = /* @__PURE__ */ new Set(["click", "hover", "fill", "pressSequentially", "check", "uncheck", "select"]);
459
+ if (SELECTOR_REQUIRED.has(a.name) && !locatorExpr)
460
+ return null;
461
+ const root = a.frameUrlMatch ? `page.frames().find(f => f.url().includes(${sq(a.frameUrlMatch)}))!` : "page";
462
+ switch (a.name) {
463
+ case "click": {
464
+ const isDouble = (a.clickCount ?? 1) > 1;
465
+ const verb = isDouble ? "dblclick" : "click";
466
+ const args = {};
467
+ if (a.button && a.button !== "left")
468
+ args.button = a.button;
469
+ const mods = (0, import_loadTraceTool.decodeModifiers)(a.modifiers);
470
+ if (mods.length)
471
+ args.modifiers = mods;
472
+ return { toolName: "browser_click", code: `await ${root}.${locatorExpr}.${verb}();`, args };
473
+ }
474
+ case "hover":
475
+ return { toolName: "browser_hover", code: `await ${root}.${locatorExpr}.hover();`, args: {} };
476
+ case "fill":
477
+ return { toolName: "browser_type", code: `await ${root}.${locatorExpr}.fill(${sq(a.text ?? "")});`, args: { text: a.text ?? "" } };
478
+ case "pressSequentially":
479
+ return { toolName: "browser_type", code: `await ${root}.${locatorExpr}.pressSequentially(${sq(a.text ?? "")});`, args: { text: a.text ?? "", slowly: true } };
480
+ case "press": {
481
+ const args = { key: a.key };
482
+ const mods = (0, import_loadTraceTool.decodeModifiers)(a.modifiers);
483
+ if (mods.length)
484
+ args.modifiers = mods;
485
+ return { toolName: "browser_press_key", code: "", args };
486
+ }
487
+ case "assertApiRequest":
488
+ return { toolName: "browser_assert_api_request", code: "", args: {} };
489
+ case "waitForTimeout":
490
+ return { toolName: "browser_wait_for", code: "", args: { time: (a.duration ?? 0) / 1e3 } };
491
+ case "check":
492
+ return { toolName: "browser_set_checked", code: `await ${root}.${locatorExpr}.check();`, args: { checked: true } };
493
+ case "uncheck":
494
+ return { toolName: "browser_set_checked", code: `await ${root}.${locatorExpr}.uncheck();`, args: { checked: false } };
495
+ case "select":
496
+ return { toolName: "browser_select_option", code: `await ${root}.${locatorExpr}.selectOption(${sq((a.options ?? [])[0] ?? "")});`, args: { values: a.options ?? [] } };
497
+ case "waitForSelector": {
498
+ const sel = a.selector ?? "";
499
+ const m = /^text=(.*)$/.exec(sel);
500
+ if (!m)
501
+ return null;
502
+ return a.state === "hidden" ? { toolName: "browser_wait_for", code: "", args: { textGone: m[1] } } : { toolName: "browser_wait_for", code: "", args: { text: m[1] } };
503
+ }
504
+ case "dragAndDrop":
505
+ case "dragTo": {
506
+ const srcSel = a.source ?? a.selector;
507
+ const tgtSel = a.target ?? a.targetSelector;
508
+ if (!srcSel || !tgtSel)
509
+ return null;
510
+ const srcExpr = (0, import_utils.asLocator)("javascript", srcSel);
511
+ const tgtExpr = (0, import_utils.asLocator)("javascript", tgtSel);
512
+ return { toolName: "browser_drag", code: `await page.${srcExpr}.dragTo(page.${tgtExpr});`, args: {} };
513
+ }
514
+ // Assertions are encoded the same way _handleAssert encodes them, so
515
+ // assertActionToJsonl in skyRampExport parses them identically. That
516
+ // parser dispatches on toolName === 'browser_assert' and keys on args.type.
517
+ // Assertions pass selector/expected as structured args (unambiguous even
518
+ // when the value contains ':'). assertActionToJsonl reads these directly;
519
+ // the legacy colon-encoded code string is kept only as a human-readable
520
+ // breadcrumb / backward-compat fallback.
521
+ case "assertText":
522
+ return { toolName: "browser_assert", code: `assertText:${a.selector}:${a.text}:${a.substring ?? true}`, args: { type: "text", selector: a.selector, expected: a.text, substring: a.substring ?? true } };
523
+ case "assertValue":
524
+ return { toolName: "browser_assert", code: `assertValue:${a.selector}:${a.value}`, args: { type: "value", selector: a.selector, expected: a.value } };
525
+ case "assertChecked":
526
+ return { toolName: "browser_assert", code: `assertChecked:${a.selector}:${!!a.checked}`, args: { type: "checked", selector: a.selector, checked: !!a.checked } };
527
+ case "assertVisible":
528
+ return { toolName: "browser_assert", code: `assertVisible:${a.selector}`, args: { type: "visible", selector: a.selector } };
529
+ default:
530
+ return null;
531
+ }
532
+ }
316
533
  /**
317
534
  * Handle browser_select_option with automatic fallback for custom dropdowns.
318
535
  * 1. Try native selectOption first.
@@ -473,7 +690,10 @@ Could not parse locator: ${locatorExpr}` }], isError: true };
473
690
  const assertName = params.type === "value" ? "assertValue" : "assertText";
474
691
  this._trackedActions.push({
475
692
  toolName: "browser_assert",
476
- args: { type: params.type, ref: params.ref, expected: params.expected },
693
+ // Pass selector/expected as structured args so assertActionToJsonl can
694
+ // decode unambiguously (the colon-encoded code below corrupts values
695
+ // that contain ':', e.g. URLs). The code string is a fallback only.
696
+ args: { type: params.type, ref: params.ref, selector: parsed.selector, expected: params.expected, ...params.type === "text" ? { substring: params.substring } : {} },
477
697
  code: `${assertName}:${parsed.selector}:${params.expected}${params.type === "text" ? ":" + params.substring : ""}`,
478
698
  timestamp
479
699
  });
@@ -226,7 +226,7 @@ function trackedActionToJsonl(action, pageGuid, timestamp) {
226
226
  return null;
227
227
  }
228
228
  if (toolName === "browser_press_key")
229
- return JSON.stringify({ name: "press", key: args.key, selector: "", ...base });
229
+ return JSON.stringify({ name: "press", key: args.key, modifiers: modifiersArrayToMask(args.modifiers), selector: "", ...base });
230
230
  if (!code)
231
231
  return null;
232
232
  const extracted = extractLocatorFromCode(code);
@@ -304,23 +304,34 @@ function assertActionToJsonl(action, pageGuid, timestamp) {
304
304
  signals: [],
305
305
  timestamp: String(timestamp),
306
306
  pageGuid,
307
- pageAlias: DEFAULT_PAGE_ALIAS,
308
- framePath: DEFAULT_FRAME_PATH
307
+ pageAlias: action.pageAlias ?? DEFAULT_PAGE_ALIAS,
308
+ framePath: action.framePath ?? DEFAULT_FRAME_PATH
309
309
  };
310
- const parts = code.split(":");
311
- const selectorPart = parts[1] || "";
312
310
  let selector = "";
313
311
  let expected = "";
314
312
  let substring = true;
315
- if (assertType === "visible") {
316
- selector = parts.slice(1).join(":");
317
- } else if (assertType === "text") {
318
- substring = parts[parts.length - 1] === "true";
319
- expected = parts[parts.length - 2] || "";
320
- selector = parts.slice(1, parts.length - 2).join(":");
321
- } else if (assertType === "value") {
322
- expected = parts[parts.length - 1] || "";
323
- selector = parts.slice(1, parts.length - 1).join(":");
313
+ if (args.selector !== void 0) {
314
+ selector = args.selector;
315
+ if (assertType === "text") {
316
+ expected = args.expected ?? "";
317
+ substring = args.substring !== false;
318
+ } else if (assertType === "value") {
319
+ expected = args.expected ?? "";
320
+ } else if (assertType === "checked") {
321
+ expected = String(args.checked === true || args.expected === "true" || args.expected === true);
322
+ }
323
+ } else {
324
+ const parts = code.split(":");
325
+ if (assertType === "visible") {
326
+ selector = parts.slice(1).join(":");
327
+ } else if (assertType === "text") {
328
+ substring = parts[parts.length - 1] === "true";
329
+ expected = parts[parts.length - 2] || "";
330
+ selector = parts.slice(1, parts.length - 2).join(":");
331
+ } else if (assertType === "value" || assertType === "checked") {
332
+ expected = parts[parts.length - 1] || "";
333
+ selector = parts.slice(1, parts.length - 1).join(":");
334
+ }
324
335
  }
325
336
  const locator = selectorToLocator(selector);
326
337
  switch (assertType) {
@@ -330,6 +341,8 @@ function assertActionToJsonl(action, pageGuid, timestamp) {
330
341
  return JSON.stringify({ name: "assertVisible", selector, locator, ...base });
331
342
  case "value":
332
343
  return JSON.stringify({ name: "assertValue", selector, value: expected, locator, ...base });
344
+ case "checked":
345
+ return JSON.stringify({ name: "assertChecked", selector, checked: expected === "true", locator, ...base });
333
346
  default:
334
347
  return null;
335
348
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "main": "build/index.js",
5
5
  "exports": {
6
6
  ".": "./build/index.js",