@xiaou66/vite-plugin-vue-mcp-next 1.2.0 → 1.3.1

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.
@@ -1,6 +1,116 @@
1
1
  // src/runtime/client.ts
2
2
  import { createHotContext } from "vite-hot-client";
3
3
 
4
+ // src/shared/limits.ts
5
+ var DEFAULT_DOM_MAX_DEPTH = 8;
6
+ var DEFAULT_DOM_MAX_NODES = 2e3;
7
+ var DEFAULT_DOM_MAX_TEXT_LENGTH = 300;
8
+ var DEFAULT_CONSOLE_MAX_RECORDS = 1e3;
9
+ var DEFAULT_NETWORK_MAX_RECORDS = 500;
10
+ var DEFAULT_NETWORK_MAX_BODY_SIZE = 1e5;
11
+ var DEFAULT_MASK_HEADERS = [
12
+ "authorization",
13
+ "cookie",
14
+ "set-cookie"
15
+ ];
16
+
17
+ // src/constants.ts
18
+ var DEFAULT_MCP_PATH = "/__mcp";
19
+ var DEFAULT_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
20
+ var DEFAULT_SCREENSHOT_SAVE_DIR = ".vite-mcp/screenshot";
21
+ var DEFAULT_PERFORMANCE_SAVE_DIR = ".vite-mcp/performance";
22
+ var DEFAULT_PERFORMANCE_MAX_DURATION_MS = 3e4;
23
+ var DEFAULT_PERFORMANCE_SAMPLE_INTERVAL_MS = 250;
24
+ var DEFAULT_PERFORMANCE_LONG_TASK_THRESHOLD_MS = 50;
25
+ var VIRTUAL_RUNTIME_ID = "virtual:vite-plugin-vue-mcp-next/runtime";
26
+ var RESOLVED_VIRTUAL_RUNTIME_ID = `\0${VIRTUAL_RUNTIME_ID}`;
27
+ var VIRTUAL_SCREENSHOT_CONFIG_ID = "virtual:vite-plugin-vue-mcp-next/screenshot-config";
28
+ var RESOLVED_VIRTUAL_SCREENSHOT_CONFIG_ID = `\0${VIRTUAL_SCREENSHOT_CONFIG_ID}`;
29
+ var VIRTUAL_SNAPDOM_LOADER_ID = "virtual:vite-plugin-vue-mcp-next/snapdom-loader";
30
+ var RESOLVED_VIRTUAL_SNAPDOM_LOADER_ID = `\0${VIRTUAL_SNAPDOM_LOADER_ID}`;
31
+ var DEFAULT_MCP_CLIENT_SERVER_NAME = "vite-mcp-next";
32
+ var RUNTIME_PAGE_CONNECTED_EVENT = "vite-plugin-vue-mcp-next:page-connected";
33
+ var RUNTIME_PAGE_DISCONNECTED_EVENT = "vite-plugin-vue-mcp-next:page-disconnected";
34
+ var RUNTIME_PAGE_HEARTBEAT_EVENT = "vite-plugin-vue-mcp-next:heartbeat";
35
+ var DEFAULT_RUNTIME_PAGE_HEARTBEAT_INTERVAL_MS = 15e3;
36
+ var DEFAULT_ELEMENT_PICKER_TOAST_DURATION_MS = 2200;
37
+ var DEFAULT_OPTIONS = {
38
+ mcpPath: DEFAULT_MCP_PATH,
39
+ host: "localhost",
40
+ printUrl: true,
41
+ updateCursorMcpJson: {
42
+ enabled: true,
43
+ serverName: DEFAULT_MCP_CLIENT_SERVER_NAME
44
+ },
45
+ mcpClients: {
46
+ cursor: true,
47
+ codex: true,
48
+ claudeCode: true,
49
+ trae: true,
50
+ serverName: DEFAULT_MCP_CLIENT_SERVER_NAME
51
+ },
52
+ skill: {
53
+ autoConfig: true
54
+ },
55
+ elementPicker: {
56
+ enabled: true,
57
+ shortcut: {
58
+ altKey: true,
59
+ shiftKey: true,
60
+ metaKey: false,
61
+ ctrlKey: false
62
+ },
63
+ toastDurationMs: DEFAULT_ELEMENT_PICKER_TOAST_DURATION_MS
64
+ },
65
+ runtime: {
66
+ mode: "auto",
67
+ evaluate: {
68
+ enabled: false,
69
+ timeoutMs: 3e3
70
+ }
71
+ },
72
+ cdp: {},
73
+ network: {
74
+ mode: "auto",
75
+ maxRecords: DEFAULT_NETWORK_MAX_RECORDS,
76
+ captureRequestBody: true,
77
+ captureResponseBody: true,
78
+ maxBodySize: DEFAULT_NETWORK_MAX_BODY_SIZE,
79
+ maskHeaders: [...DEFAULT_MASK_HEADERS]
80
+ },
81
+ dom: {
82
+ maxDepth: DEFAULT_DOM_MAX_DEPTH,
83
+ maxNodes: DEFAULT_DOM_MAX_NODES,
84
+ maxTextLength: DEFAULT_DOM_MAX_TEXT_LENGTH
85
+ },
86
+ console: {
87
+ maxRecords: DEFAULT_CONSOLE_MAX_RECORDS
88
+ },
89
+ screenshot: {
90
+ type: "path",
91
+ saveDir: DEFAULT_SCREENSHOT_SAVE_DIR,
92
+ prefer: "auto",
93
+ maxBytes: DEFAULT_SCREENSHOT_MAX_BYTES,
94
+ snapdom: {
95
+ options: {},
96
+ plugins: []
97
+ }
98
+ },
99
+ performance: {
100
+ mode: "auto",
101
+ maxDurationMs: DEFAULT_PERFORMANCE_MAX_DURATION_MS,
102
+ sampleIntervalMs: DEFAULT_PERFORMANCE_SAMPLE_INTERVAL_MS,
103
+ longTaskThresholdMs: DEFAULT_PERFORMANCE_LONG_TASK_THRESHOLD_MS,
104
+ saveDir: DEFAULT_PERFORMANCE_SAVE_DIR,
105
+ memory: {
106
+ enabled: true
107
+ },
108
+ stacks: {
109
+ enabled: true
110
+ }
111
+ }
112
+ };
113
+
4
114
  // src/runtime/consoleHook.ts
5
115
  import { nanoid } from "nanoid";
6
116
 
@@ -36,13 +146,14 @@ function installConsoleHook(options) {
36
146
  debug: console.debug
37
147
  };
38
148
  const emit = (level, args) => {
149
+ const serializedArgs = serializeConsoleArgs(args);
39
150
  options.send({
40
151
  id: nanoid(),
41
152
  pageId: options.pageId,
42
153
  source: "hook",
43
154
  level,
44
- message: args.map((arg) => safeStringify(arg)).join(" "),
45
- args,
155
+ message: serializedArgs.join(" "),
156
+ args: serializedArgs,
46
157
  timestamp: Date.now()
47
158
  });
48
159
  };
@@ -69,9 +180,231 @@ function installConsoleHook(options) {
69
180
  window.removeEventListener("error", onError);
70
181
  };
71
182
  }
183
+ function serializeConsoleArgs(args) {
184
+ return args.map((arg) => safeStringify(arg));
185
+ }
72
186
 
73
- // src/runtime/networkHook.ts
187
+ // src/runtime/elementRegistry.ts
74
188
  import { nanoid as nanoid2 } from "nanoid";
189
+ var RUNTIME_ELEMENT_ID_PREFIX = "runtime:vmcp_";
190
+ var RUNTIME_ELEMENT_ID_SIZE = 8;
191
+ var runtimeElementRegistry = createRuntimeElementRegistry();
192
+ function createRuntimeElementId() {
193
+ return `${RUNTIME_ELEMENT_ID_PREFIX}${nanoid2(RUNTIME_ELEMENT_ID_SIZE)}`;
194
+ }
195
+ function createRuntimeElementRegistry() {
196
+ const records = /* @__PURE__ */ new Map();
197
+ return {
198
+ register(element) {
199
+ const elementId = createRuntimeElementId();
200
+ records.set(elementId, {
201
+ elementId,
202
+ element,
203
+ createdAt: Date.now()
204
+ });
205
+ return elementId;
206
+ },
207
+ get(elementId) {
208
+ return records.get(elementId);
209
+ },
210
+ clear() {
211
+ records.clear();
212
+ }
213
+ };
214
+ }
215
+
216
+ // src/runtime/elementPicker.ts
217
+ var MCP_ID_ATTR = "data-v-mcp-id";
218
+ var INTERNAL_ATTR = "data-v-mcp-internal";
219
+ var SUCCESS_MESSAGE = "\u5143\u7D20\u4F4D\u7F6E\u5DF2\u590D\u5236\uFF0C\u8BF7\u53D1\u9001\u7ED9 AI";
220
+ var COPY_FAILED_PREFIX = "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u590D\u5236\u5143\u7D20 ID";
221
+ var OVERLAY_Z_INDEX = "2147483647";
222
+ var TOAST_Z_INDEX = "2147483647";
223
+ function installElementPicker(options) {
224
+ if (!options.enabled) {
225
+ return;
226
+ }
227
+ const registry = runtimeElementRegistry;
228
+ const overlay = createOverlay();
229
+ let active = false;
230
+ let currentElement;
231
+ window.addEventListener("keydown", (event) => {
232
+ active = matchesShortcut(event, options.shortcut);
233
+ });
234
+ window.addEventListener("keyup", () => {
235
+ active = false;
236
+ currentElement = void 0;
237
+ updateOverlay(overlay);
238
+ });
239
+ window.addEventListener("mousemove", (event) => {
240
+ if (!active) {
241
+ return;
242
+ }
243
+ currentElement = document.elementFromPoint(event.clientX, event.clientY) ?? void 0;
244
+ updateOverlay(overlay, currentElement);
245
+ });
246
+ window.addEventListener(
247
+ "click",
248
+ (event) => {
249
+ if (!active || !isElementLike(event.target)) {
250
+ return;
251
+ }
252
+ event.preventDefault();
253
+ event.stopPropagation();
254
+ void copyAndNotify(event.target, registry, options.toastDurationMs);
255
+ },
256
+ true
257
+ );
258
+ }
259
+ function matchesShortcut(event, shortcut) {
260
+ return event.altKey === shortcut.altKey && event.shiftKey === shortcut.shiftKey && event.metaKey === shortcut.metaKey && event.ctrlKey === shortcut.ctrlKey;
261
+ }
262
+ function resolveElementId(element, registry) {
263
+ return element.getAttribute(MCP_ID_ATTR) ?? registry.register(element);
264
+ }
265
+ async function copyAndNotify(element, registry, toastDurationMs) {
266
+ const elementId = resolveElementId(element, registry);
267
+ const copied = await copyElementId(elementId);
268
+ showToast(
269
+ copied ? SUCCESS_MESSAGE : `${COPY_FAILED_PREFIX}: ${elementId}`,
270
+ toastDurationMs
271
+ );
272
+ }
273
+ async function copyElementId(elementId) {
274
+ try {
275
+ await navigator.clipboard.writeText(elementId);
276
+ return true;
277
+ } catch {
278
+ return false;
279
+ }
280
+ }
281
+ function createOverlay() {
282
+ const overlay = document.createElement("div");
283
+ markInternalElement(overlay);
284
+ Object.assign(overlay.style, {
285
+ position: "fixed",
286
+ pointerEvents: "none",
287
+ border: "2px solid #1d4ed8",
288
+ background: "rgba(29, 78, 216, 0.08)",
289
+ zIndex: OVERLAY_Z_INDEX,
290
+ display: "none"
291
+ });
292
+ document.body.appendChild(overlay);
293
+ return overlay;
294
+ }
295
+ function updateOverlay(overlay, element) {
296
+ if (!element || getElementAttr(element, INTERNAL_ATTR) === "true") {
297
+ overlay.style.display = "none";
298
+ return;
299
+ }
300
+ const rect = element.getBoundingClientRect();
301
+ Object.assign(overlay.style, {
302
+ display: "block",
303
+ left: `${String(rect.x)}px`,
304
+ top: `${String(rect.y)}px`,
305
+ width: `${String(rect.width)}px`,
306
+ height: `${String(rect.height)}px`
307
+ });
308
+ }
309
+ function showToast(message, durationMs) {
310
+ const toast = document.createElement("div");
311
+ markInternalElement(toast);
312
+ toast.textContent = message;
313
+ Object.assign(toast.style, {
314
+ position: "fixed",
315
+ left: "50%",
316
+ bottom: "32px",
317
+ transform: "translateX(-50%)",
318
+ zIndex: TOAST_Z_INDEX,
319
+ padding: "8px 12px",
320
+ borderRadius: "6px",
321
+ background: "rgba(17, 24, 39, 0.92)",
322
+ color: "#fff",
323
+ fontSize: "13px",
324
+ pointerEvents: "none"
325
+ });
326
+ document.body.appendChild(toast);
327
+ globalThis.setTimeout(() => {
328
+ toast.remove();
329
+ }, durationMs);
330
+ }
331
+ function isElementLike(value) {
332
+ return Boolean(
333
+ value && typeof value === "object" && "getAttribute" in value && "getBoundingClientRect" in value
334
+ );
335
+ }
336
+ function markInternalElement(element) {
337
+ if (typeof element.setAttribute === "function") {
338
+ element.setAttribute(INTERNAL_ATTR, "true");
339
+ }
340
+ element.dataset.vMcpInternal = "true";
341
+ }
342
+ function getElementAttr(element, name) {
343
+ if (typeof element.getAttribute === "function") {
344
+ return element.getAttribute(name);
345
+ }
346
+ return null;
347
+ }
348
+
349
+ // src/shared/elementId.ts
350
+ var PROJECT_SOURCE_ID_PATTERN = /^(.+\.(?:vue|tsx|jsx|ts|js)):(\d+):(\d+)$/;
351
+ var RUNTIME_ID_PATTERN = /^runtime:([A-Za-z0-9_-]+)$/;
352
+ var PACKAGE_ID_PREFIX = "pkg:";
353
+ function parseElementId(elementId) {
354
+ const sourceMatch = PROJECT_SOURCE_ID_PATTERN.exec(elementId);
355
+ if (sourceMatch) {
356
+ return {
357
+ kind: "project-source",
358
+ elementId,
359
+ file: sourceMatch[1],
360
+ line: Number(sourceMatch[2]),
361
+ column: Number(sourceMatch[3])
362
+ };
363
+ }
364
+ if (elementId.startsWith(PACKAGE_ID_PREFIX)) {
365
+ return parsePackageElementId(elementId);
366
+ }
367
+ const runtimeMatch = RUNTIME_ID_PATTERN.exec(elementId);
368
+ if (runtimeMatch) {
369
+ return {
370
+ kind: "runtime",
371
+ elementId,
372
+ runtimeId: runtimeMatch[1]
373
+ };
374
+ }
375
+ return {
376
+ kind: "invalid",
377
+ elementId,
378
+ reason: "unsupported elementId format"
379
+ };
380
+ }
381
+ function parsePackageElementId(elementId) {
382
+ const value = elementId.slice(PACKAGE_ID_PREFIX.length);
383
+ const parts = value.split("/").filter(Boolean);
384
+ if (parts.length < 2) {
385
+ return {
386
+ kind: "invalid",
387
+ elementId,
388
+ reason: "package elementId must include packageName and entryFile"
389
+ };
390
+ }
391
+ const scoped = parts[0]?.startsWith("@");
392
+ const packageName = scoped ? parts.slice(0, 2).join("/") : parts[0];
393
+ const entryFile = parts.slice(scoped ? 2 : 1).join("/");
394
+ if (!entryFile) {
395
+ return {
396
+ kind: "invalid",
397
+ elementId,
398
+ reason: "package elementId must include entryFile"
399
+ };
400
+ }
401
+ return {
402
+ kind: "package",
403
+ elementId,
404
+ packageName,
405
+ entryFile
406
+ };
407
+ }
75
408
 
76
409
  // src/shared/sanitize.ts
77
410
  function truncateText(text, maxLength) {
@@ -96,6 +429,270 @@ function maskHeaders(headers = {}, maskNames = []) {
96
429
  );
97
430
  }
98
431
 
432
+ // src/runtime/domSnapshot.ts
433
+ function createDomSnapshot(root, options) {
434
+ let count = 0;
435
+ function visit(node, depth) {
436
+ if (count >= options.maxNodes || depth > options.maxDepth) {
437
+ return null;
438
+ }
439
+ if (node.nodeType === Node.TEXT_NODE) {
440
+ return createTextSnapshot(node, options, () => {
441
+ count += 1;
442
+ });
443
+ }
444
+ if (!(node instanceof Element)) {
445
+ return null;
446
+ }
447
+ const tag = node.tagName.toLowerCase();
448
+ if (["script", "style", "noscript"].includes(tag) || isInternalMcpElement(node)) {
449
+ return null;
450
+ }
451
+ count += 1;
452
+ return createElementSnapshot(node, tag, (child) => visit(child, depth + 1));
453
+ }
454
+ return visit(root, 0) ?? { tag: root.tagName.toLowerCase() };
455
+ }
456
+ function queryDomElements(selector, limit) {
457
+ return Array.from(document.querySelectorAll(selector)).filter((element) => !isInternalMcpElement(element)).slice(0, limit).map((element) => createDomElementSummary(element));
458
+ }
459
+ function createDomElementSummary(element) {
460
+ return {
461
+ tag: element.tagName.toLowerCase(),
462
+ text: element.textContent.trim(),
463
+ attrs: collectAttrs(element),
464
+ rect: serializeRect(element.getBoundingClientRect())
465
+ };
466
+ }
467
+ function createTextSnapshot(node, options, markVisited) {
468
+ const text = node.textContent?.trim();
469
+ if (!text) {
470
+ return null;
471
+ }
472
+ markVisited();
473
+ return { tag: "#text", text: truncateText(text, options.maxTextLength).text };
474
+ }
475
+ function createElementSnapshot(node, tag, visitChild) {
476
+ const attrs = collectAttrs(node);
477
+ const children = Array.from(node.childNodes).map((child) => visitChild(child)).filter((child) => Boolean(child));
478
+ return {
479
+ tag,
480
+ ...node.textContent.trim() ? { text: node.textContent.trim() } : {},
481
+ ...Object.keys(attrs).length ? { attrs } : {},
482
+ ...children.length ? { children } : {}
483
+ };
484
+ }
485
+ function collectAttrs(element) {
486
+ const attrs = {};
487
+ for (const attr of Array.from(element.attributes)) {
488
+ attrs[attr.name] = attr.value;
489
+ }
490
+ if (typeof HTMLInputElement !== "undefined" && element instanceof HTMLInputElement && element.type === "password") {
491
+ attrs.value = "[masked]";
492
+ }
493
+ return attrs;
494
+ }
495
+ function serializeRect(rect) {
496
+ return {
497
+ x: rect.x,
498
+ y: rect.y,
499
+ width: rect.width,
500
+ height: rect.height,
501
+ top: rect.top,
502
+ right: rect.right,
503
+ bottom: rect.bottom,
504
+ left: rect.left
505
+ };
506
+ }
507
+ function isInternalMcpElement(element) {
508
+ return element.getAttribute("data-v-mcp-internal") === "true";
509
+ }
510
+
511
+ // src/runtime/vueComponentLocator.ts
512
+ function locateVueComponentForElement(element, root) {
513
+ const component = findNearestComponent(element);
514
+ if (!component) {
515
+ return void 0;
516
+ }
517
+ const file = getComponentFile(component);
518
+ const name = getComponentName(component);
519
+ if (!file) {
520
+ return { name };
521
+ }
522
+ const packageLocation = parseNodeModulesFile(file);
523
+ if (packageLocation) {
524
+ return {
525
+ name,
526
+ source: void 0,
527
+ packageLocation
528
+ };
529
+ }
530
+ return {
531
+ name,
532
+ source: {
533
+ file: createProjectRelativePath(root, file)
534
+ },
535
+ packageLocation: void 0
536
+ };
537
+ }
538
+ function findNearestComponent(element) {
539
+ let current = element;
540
+ while (current) {
541
+ const component = current.__vueParentComponent;
542
+ if (isVueRuntimeComponent(component)) {
543
+ return component;
544
+ }
545
+ current = current.parentElement;
546
+ }
547
+ return void 0;
548
+ }
549
+ function getComponentName(component) {
550
+ return component.type?.name ?? component.type?.__name ?? component.type?.displayName;
551
+ }
552
+ function getComponentFile(component) {
553
+ return component.type?.__file;
554
+ }
555
+ function parseNodeModulesFile(file) {
556
+ const normalized = normalizePath(file);
557
+ const marker = "/node_modules/";
558
+ const index = normalized.lastIndexOf(marker);
559
+ if (index < 0) {
560
+ return void 0;
561
+ }
562
+ const parts = normalized.slice(index + marker.length).split("/").filter(Boolean);
563
+ const scoped = parts[0]?.startsWith("@");
564
+ const packageName = scoped ? parts.slice(0, 2).join("/") : parts[0];
565
+ const entryFile = parts.slice(scoped ? 2 : 1).join("/");
566
+ if (!packageName || !entryFile) {
567
+ return void 0;
568
+ }
569
+ return {
570
+ packageName,
571
+ entryFile
572
+ };
573
+ }
574
+ function normalizePath(path) {
575
+ return path.replace(/\\/g, "/");
576
+ }
577
+ function createProjectRelativePath(root, file) {
578
+ const normalizedRoot = normalizePath(root).replace(/\/$/, "");
579
+ const normalizedFile = normalizePath(file);
580
+ const prefix = `${normalizedRoot}/`;
581
+ if (normalizedFile.startsWith(prefix)) {
582
+ return normalizedFile.slice(prefix.length);
583
+ }
584
+ return normalizedFile.replace(/^\//, "");
585
+ }
586
+ function isVueRuntimeComponent(value) {
587
+ return Boolean(value && typeof value === "object" && "type" in value);
588
+ }
589
+
590
+ // src/runtime/elementContext.ts
591
+ var activeResolver;
592
+ function createElementContextResolver(options) {
593
+ return {
594
+ getElementContext(elementId) {
595
+ const parsed = parseElementId(elementId);
596
+ if (parsed.kind === "project-source") {
597
+ const element = options.querySelector(
598
+ `[data-v-mcp-id="${escapeSelector(elementId)}"]`
599
+ );
600
+ return createProjectSourceContext(parsed, element, options.root);
601
+ }
602
+ if (parsed.kind === "package") {
603
+ return createPackageContext(parsed);
604
+ }
605
+ if (parsed.kind === "runtime") {
606
+ const record = options.registry.get(elementId);
607
+ if (!record) {
608
+ return createMissingRuntimeElementError(elementId);
609
+ }
610
+ return createRuntimeContext(elementId, record.element, options.root);
611
+ }
612
+ return {
613
+ ok: false,
614
+ error: parsed.reason,
615
+ elementId,
616
+ limitations: ["please provide a copied elementId from the element picker"]
617
+ };
618
+ }
619
+ };
620
+ }
621
+ function setElementContextResolver(resolver) {
622
+ activeResolver = resolver;
623
+ }
624
+ function getElementContextResolver() {
625
+ activeResolver ??= createElementContextResolver({
626
+ root: "/",
627
+ registry: runtimeElementRegistry,
628
+ querySelector(selector) {
629
+ return document.querySelector(selector);
630
+ }
631
+ });
632
+ return activeResolver;
633
+ }
634
+ function createProjectSourceContext(parsed, element, root) {
635
+ return {
636
+ ok: true,
637
+ elementId: parsed.elementId,
638
+ editable: true,
639
+ codeLocation: {
640
+ file: parsed.file,
641
+ line: parsed.line,
642
+ column: parsed.column
643
+ },
644
+ ...element ? { component: locateVueComponentForElement(element, root) } : {},
645
+ ...element ? { dom: createDomElementSummary(element) } : {},
646
+ limitations: element ? [] : ["runtime DOM element was not found"]
647
+ };
648
+ }
649
+ function createPackageContext(parsed) {
650
+ return {
651
+ ok: true,
652
+ elementId: parsed.elementId,
653
+ editable: false,
654
+ packageLocation: {
655
+ packageName: parsed.packageName,
656
+ entryFile: parsed.entryFile
657
+ },
658
+ limitations: ["third-party package source is not editable from this project"]
659
+ };
660
+ }
661
+ function createRuntimeContext(elementId, element, root) {
662
+ const component = locateVueComponentForElement(element, root);
663
+ const sourceFile = component?.source?.file;
664
+ return {
665
+ ok: true,
666
+ elementId,
667
+ editable: Boolean(sourceFile),
668
+ ...sourceFile ? { codeLocation: { file: sourceFile, line: 1, column: 1 } } : {},
669
+ component,
670
+ dom: createDomElementSummary(element),
671
+ limitations: sourceFile ? ["runtime id maps to nearest component file, exact template node is unavailable"] : ["runtime id is only valid during the current page lifecycle"]
672
+ };
673
+ }
674
+ function createMissingRuntimeElementError(elementId) {
675
+ return {
676
+ ok: false,
677
+ error: "element not found",
678
+ elementId,
679
+ limitations: [
680
+ "element was removed or page refreshed",
681
+ "please ask the user to pick the element again"
682
+ ]
683
+ };
684
+ }
685
+ function escapeSelector(value) {
686
+ const css = globalThis.CSS;
687
+ if (css?.escape) {
688
+ return css.escape(value);
689
+ }
690
+ return value.replace(/["\\]/g, "\\$&");
691
+ }
692
+
693
+ // src/runtime/networkHook.ts
694
+ import { nanoid as nanoid3 } from "nanoid";
695
+
99
696
  // src/shared/url.ts
100
697
  function parseRequestQuery(url) {
101
698
  const parsed = new URL(url, "http://vite-plugin-vue-mcp-next.local");
@@ -125,7 +722,7 @@ function safeUrlPathname(url) {
125
722
  // src/runtime/networkHook.ts
126
723
  function createHookNetworkRecord(input) {
127
724
  return {
128
- id: nanoid2(),
725
+ id: nanoid3(),
129
726
  pageId: input.pageId,
130
727
  source: "hook",
131
728
  url: input.url,
@@ -280,12 +877,12 @@ function safeReadXhrResponseText(xhr) {
280
877
  }
281
878
 
282
879
  // src/runtime/pageIdentity.ts
283
- import { nanoid as nanoid3 } from "nanoid";
880
+ import { nanoid as nanoid4 } from "nanoid";
284
881
  var RUNTIME_CLIENT_ID_STORAGE_KEY = "vite-plugin-vue-mcp-next:runtime-client-id";
285
882
  var RUNTIME_CLIENT_ID_WINDOW_NAME_PREFIX = "vite-plugin-vue-mcp-next:runtime-client-id=";
286
883
  var RUNTIME_CLIENT_ID_WINDOW_NAME_SEPARATOR = "\n";
287
884
  function createRuntimeClientId() {
288
- return `runtime-client-${nanoid3()}`;
885
+ return `runtime-client-${nanoid4()}`;
289
886
  }
290
887
  function readRuntimeClientIdFromTabScope(tabScope) {
291
888
  if (!tabScope) {
@@ -334,7 +931,7 @@ function getRuntimeClientId(storage, tabScope) {
334
931
  }
335
932
  }
336
933
  function createRuntimePageId() {
337
- return `runtime-${nanoid3()}`;
934
+ return `runtime-${nanoid4()}`;
338
935
  }
339
936
  function getRuntimePageIdentity(input) {
340
937
  return {
@@ -354,102 +951,7 @@ function getRuntimePageIdentity(input) {
354
951
  }
355
952
 
356
953
  // src/runtime/performanceHook.ts
357
- import { nanoid as nanoid4 } from "nanoid";
358
-
359
- // src/shared/limits.ts
360
- var DEFAULT_DOM_MAX_DEPTH = 8;
361
- var DEFAULT_DOM_MAX_NODES = 2e3;
362
- var DEFAULT_DOM_MAX_TEXT_LENGTH = 300;
363
- var DEFAULT_CONSOLE_MAX_RECORDS = 1e3;
364
- var DEFAULT_NETWORK_MAX_RECORDS = 500;
365
- var DEFAULT_NETWORK_MAX_BODY_SIZE = 1e5;
366
- var DEFAULT_MASK_HEADERS = [
367
- "authorization",
368
- "cookie",
369
- "set-cookie"
370
- ];
371
-
372
- // src/constants.ts
373
- var DEFAULT_MCP_PATH = "/__mcp";
374
- var DEFAULT_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
375
- var DEFAULT_SCREENSHOT_SAVE_DIR = ".vite-mcp/screenshot";
376
- var DEFAULT_PERFORMANCE_SAVE_DIR = ".vite-mcp/performance";
377
- var DEFAULT_PERFORMANCE_MAX_DURATION_MS = 3e4;
378
- var DEFAULT_PERFORMANCE_SAMPLE_INTERVAL_MS = 250;
379
- var DEFAULT_PERFORMANCE_LONG_TASK_THRESHOLD_MS = 50;
380
- var VIRTUAL_RUNTIME_ID = "virtual:vite-plugin-vue-mcp-next/runtime";
381
- var RESOLVED_VIRTUAL_RUNTIME_ID = `\0${VIRTUAL_RUNTIME_ID}`;
382
- var VIRTUAL_SCREENSHOT_CONFIG_ID = "virtual:vite-plugin-vue-mcp-next/screenshot-config";
383
- var RESOLVED_VIRTUAL_SCREENSHOT_CONFIG_ID = `\0${VIRTUAL_SCREENSHOT_CONFIG_ID}`;
384
- var VIRTUAL_SNAPDOM_LOADER_ID = "virtual:vite-plugin-vue-mcp-next/snapdom-loader";
385
- var RESOLVED_VIRTUAL_SNAPDOM_LOADER_ID = `\0${VIRTUAL_SNAPDOM_LOADER_ID}`;
386
- var DEFAULT_MCP_CLIENT_SERVER_NAME = "vite-mcp-next";
387
- var DEFAULT_OPTIONS = {
388
- mcpPath: DEFAULT_MCP_PATH,
389
- host: "localhost",
390
- printUrl: true,
391
- updateCursorMcpJson: {
392
- enabled: true,
393
- serverName: DEFAULT_MCP_CLIENT_SERVER_NAME
394
- },
395
- mcpClients: {
396
- cursor: true,
397
- codex: true,
398
- claudeCode: true,
399
- trae: true,
400
- serverName: DEFAULT_MCP_CLIENT_SERVER_NAME
401
- },
402
- skill: {
403
- autoConfig: true
404
- },
405
- runtime: {
406
- mode: "auto",
407
- evaluate: {
408
- enabled: false,
409
- timeoutMs: 3e3
410
- }
411
- },
412
- cdp: {},
413
- network: {
414
- mode: "auto",
415
- maxRecords: DEFAULT_NETWORK_MAX_RECORDS,
416
- captureRequestBody: true,
417
- captureResponseBody: true,
418
- maxBodySize: DEFAULT_NETWORK_MAX_BODY_SIZE,
419
- maskHeaders: [...DEFAULT_MASK_HEADERS]
420
- },
421
- dom: {
422
- maxDepth: DEFAULT_DOM_MAX_DEPTH,
423
- maxNodes: DEFAULT_DOM_MAX_NODES,
424
- maxTextLength: DEFAULT_DOM_MAX_TEXT_LENGTH
425
- },
426
- console: {
427
- maxRecords: DEFAULT_CONSOLE_MAX_RECORDS
428
- },
429
- screenshot: {
430
- type: "path",
431
- saveDir: DEFAULT_SCREENSHOT_SAVE_DIR,
432
- prefer: "auto",
433
- maxBytes: DEFAULT_SCREENSHOT_MAX_BYTES,
434
- snapdom: {
435
- options: {},
436
- plugins: []
437
- }
438
- },
439
- performance: {
440
- mode: "auto",
441
- maxDurationMs: DEFAULT_PERFORMANCE_MAX_DURATION_MS,
442
- sampleIntervalMs: DEFAULT_PERFORMANCE_SAMPLE_INTERVAL_MS,
443
- longTaskThresholdMs: DEFAULT_PERFORMANCE_LONG_TASK_THRESHOLD_MS,
444
- saveDir: DEFAULT_PERFORMANCE_SAVE_DIR,
445
- memory: {
446
- enabled: true
447
- },
448
- stacks: {
449
- enabled: true
450
- }
451
- }
452
- };
954
+ import { nanoid as nanoid5 } from "nanoid";
453
955
 
454
956
  // src/performance/summary.ts
455
957
  function buildPerformanceSummary(input) {
@@ -623,7 +1125,7 @@ function createPerformanceCollector(deps) {
623
1125
  };
624
1126
  }
625
1127
  function createRecordingId() {
626
- return `performance-${nanoid4()}`;
1128
+ return `performance-${nanoid5()}`;
627
1129
  }
628
1130
  function startSession(state, deps, options) {
629
1131
  const recordingId = createRecordingId();
@@ -998,78 +1500,6 @@ import {
998
1500
  } from "@vue/devtools-kit";
999
1501
  import { createRPCClient } from "vite-dev-rpc";
1000
1502
 
1001
- // src/runtime/domSnapshot.ts
1002
- function createDomSnapshot(root, options) {
1003
- let count = 0;
1004
- function visit(node, depth) {
1005
- if (count >= options.maxNodes || depth > options.maxDepth) {
1006
- return null;
1007
- }
1008
- if (node.nodeType === Node.TEXT_NODE) {
1009
- return createTextSnapshot(node, options, () => {
1010
- count += 1;
1011
- });
1012
- }
1013
- if (!(node instanceof Element)) {
1014
- return null;
1015
- }
1016
- const tag = node.tagName.toLowerCase();
1017
- if (["script", "style", "noscript"].includes(tag)) {
1018
- return null;
1019
- }
1020
- count += 1;
1021
- return createElementSnapshot(node, tag, (child) => visit(child, depth + 1));
1022
- }
1023
- return visit(root, 0) ?? { tag: root.tagName.toLowerCase() };
1024
- }
1025
- function queryDomElements(selector, limit) {
1026
- return Array.from(document.querySelectorAll(selector)).slice(0, limit).map((element) => ({
1027
- tag: element.tagName.toLowerCase(),
1028
- text: element.textContent.trim(),
1029
- attrs: collectAttrs(element),
1030
- rect: serializeRect(element.getBoundingClientRect())
1031
- }));
1032
- }
1033
- function createTextSnapshot(node, options, markVisited) {
1034
- const text = node.textContent?.trim();
1035
- if (!text) {
1036
- return null;
1037
- }
1038
- markVisited();
1039
- return { tag: "#text", text: truncateText(text, options.maxTextLength).text };
1040
- }
1041
- function createElementSnapshot(node, tag, visitChild) {
1042
- const attrs = collectAttrs(node);
1043
- const children = Array.from(node.childNodes).map((child) => visitChild(child)).filter((child) => Boolean(child));
1044
- return {
1045
- tag,
1046
- ...Object.keys(attrs).length ? { attrs } : {},
1047
- ...children.length ? { children } : {}
1048
- };
1049
- }
1050
- function collectAttrs(element) {
1051
- const attrs = {};
1052
- for (const attr of Array.from(element.attributes)) {
1053
- attrs[attr.name] = attr.value;
1054
- }
1055
- if (element instanceof HTMLInputElement && element.type === "password") {
1056
- attrs.value = "[masked]";
1057
- }
1058
- return attrs;
1059
- }
1060
- function serializeRect(rect) {
1061
- return {
1062
- x: rect.x,
1063
- y: rect.y,
1064
- width: rect.width,
1065
- height: rect.height,
1066
- top: rect.top,
1067
- right: rect.right,
1068
- bottom: rect.bottom,
1069
- left: rect.left
1070
- };
1071
- }
1072
-
1073
1503
  // src/runtime/evaluateExpression.ts
1074
1504
  async function evaluateExpression(request) {
1075
1505
  const value = runExpression(request.expression);
@@ -1497,6 +1927,22 @@ function createRuntimeDevtoolsRpc(getRpc) {
1497
1927
  );
1498
1928
  },
1499
1929
  onDomQueryUpdated: () => void 0,
1930
+ getElementContext(options) {
1931
+ try {
1932
+ getRpc().onElementContextUpdated(
1933
+ options.event,
1934
+ getElementContextResolver().getElementContext(options.elementId)
1935
+ );
1936
+ } catch (error) {
1937
+ getRpc().onElementContextUpdated(options.event, {
1938
+ ok: false,
1939
+ elementId: options.elementId,
1940
+ error: error instanceof Error ? error.message : String(error),
1941
+ limitations: ["runtime element context lookup failed"]
1942
+ });
1943
+ }
1944
+ },
1945
+ onElementContextUpdated: () => void 0,
1500
1946
  reloadPage(options) {
1501
1947
  getRpc().onPageReloaded(options.event, { ok: true, source: "hook" });
1502
1948
  setTimeout(() => {
@@ -1833,13 +2279,16 @@ function createMissingComponentError(componentName) {
1833
2279
  }
1834
2280
 
1835
2281
  // src/runtime/client.ts
1836
- async function startRuntimeClient() {
2282
+ async function startRuntimeClient(runtimeOptions = {
2283
+ elementPicker: DEFAULT_OPTIONS.elementPicker
2284
+ }) {
1837
2285
  initializeVueDevtoolsHook();
1838
2286
  const hot = await createHotContext("vite-plugin-vue-mcp-next", "/");
1839
2287
  if (!hot) {
1840
2288
  return;
1841
2289
  }
1842
2290
  installVueBridge(hot);
2291
+ installElementPicker(runtimeOptions.elementPicker);
1843
2292
  const identity = getRuntimePageIdentity({
1844
2293
  href: window.location.href,
1845
2294
  title: document.title,
@@ -1848,7 +2297,20 @@ async function startRuntimeClient() {
1848
2297
  innerHeight: window.innerHeight,
1849
2298
  readyState: document.readyState
1850
2299
  });
1851
- hot.send("vite-plugin-vue-mcp-next:page-connected", identity);
2300
+ setElementContextResolver(
2301
+ createElementContextResolver({
2302
+ root: runtimeOptions.projectRoot ?? "/",
2303
+ registry: runtimeElementRegistry,
2304
+ querySelector(selector) {
2305
+ return document.querySelector(selector);
2306
+ }
2307
+ })
2308
+ );
2309
+ hot.send(RUNTIME_PAGE_CONNECTED_EVENT, identity);
2310
+ installRuntimePageLifecycle({
2311
+ pageId: identity.pageId,
2312
+ send: hot.send.bind(hot)
2313
+ });
1852
2314
  installPerformanceHook({
1853
2315
  pageId: identity.pageId,
1854
2316
  send(report) {
@@ -1870,6 +2332,27 @@ async function startRuntimeClient() {
1870
2332
  }
1871
2333
  });
1872
2334
  }
2335
+ function installRuntimePageLifecycle(options) {
2336
+ let disconnected = false;
2337
+ const heartbeatTimer = setInterval(() => {
2338
+ options.send(RUNTIME_PAGE_HEARTBEAT_EVENT, {
2339
+ pageId: options.pageId,
2340
+ timestamp: Date.now()
2341
+ });
2342
+ }, DEFAULT_RUNTIME_PAGE_HEARTBEAT_INTERVAL_MS);
2343
+ const disconnect = () => {
2344
+ if (disconnected) {
2345
+ return;
2346
+ }
2347
+ disconnected = true;
2348
+ clearInterval(heartbeatTimer);
2349
+ options.send(RUNTIME_PAGE_DISCONNECTED_EVENT, {
2350
+ pageId: options.pageId
2351
+ });
2352
+ };
2353
+ window.addEventListener("pagehide", disconnect, { once: true });
2354
+ window.addEventListener("beforeunload", disconnect, { once: true });
2355
+ }
1873
2356
  export {
1874
2357
  evaluateExpression,
1875
2358
  setScreenshotModuleRegistry,