@xiaou66/vite-plugin-vue-mcp-next 1.0.4 → 1.1.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.
package/dist/index.cjs CHANGED
@@ -55,6 +55,11 @@ var DEFAULT_MASK_HEADERS = [
55
55
  var DEFAULT_MCP_PATH = "/__mcp";
56
56
  var DEFAULT_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
57
57
  var DEFAULT_SCREENSHOT_SAVE_DIR = ".vite-mcp/screenshot";
58
+ var DEFAULT_PERFORMANCE_SAVE_DIR = ".vite-mcp/performance";
59
+ var DEFAULT_PERFORMANCE_MAX_REPORTS = 100;
60
+ var DEFAULT_PERFORMANCE_MAX_DURATION_MS = 3e4;
61
+ var DEFAULT_PERFORMANCE_SAMPLE_INTERVAL_MS = 250;
62
+ var DEFAULT_PERFORMANCE_LONG_TASK_THRESHOLD_MS = 50;
58
63
  var MCP_TOOL_NAMES = {
59
64
  listPages: "list_pages",
60
65
  reloadPage: "reload_page",
@@ -67,6 +72,11 @@ var MCP_TOOL_NAMES = {
67
72
  getNetworkRequests: "get_network_requests",
68
73
  getNetworkRequestDetail: "get_network_request_detail",
69
74
  clearNetworkRequests: "clear_network_requests",
75
+ recordPerformance: "record_performance",
76
+ startPerformanceRecording: "start_performance_recording",
77
+ stopPerformanceRecording: "stop_performance_recording",
78
+ getPerformanceReport: "get_performance_report",
79
+ takeHeapSnapshot: "take_heap_snapshot",
70
80
  getComponentTree: "get_component_tree",
71
81
  getComponentState: "get_component_state",
72
82
  editComponentState: "edit_component_state",
@@ -84,6 +94,11 @@ var RESOLVED_VIRTUAL_SNAPDOM_LOADER_ID = `\0${VIRTUAL_SNAPDOM_LOADER_ID}`;
84
94
  var DEFAULT_MCP_CLIENT_SERVER_NAME = "vite-mcp-next";
85
95
  var LEGACY_MCP_CLIENT_SERVER_NAMES = ["vue-mcp-next"];
86
96
  var RUNTIME_PAGE_RECONNECTED_EVENT = "vite-plugin-vue-mcp-next:page-reconnected";
97
+ var RUNTIME_PAGE_CONNECTED_EVENT = "vite-plugin-vue-mcp-next:page-connected";
98
+ var RUNTIME_PAGE_DISCONNECTED_EVENT = "vite-plugin-vue-mcp-next:page-disconnected";
99
+ var RUNTIME_PAGE_HEARTBEAT_EVENT = "vite-plugin-vue-mcp-next:heartbeat";
100
+ var DEFAULT_RUNTIME_PAGE_HEARTBEAT_TIMEOUT_MS = 45e3;
101
+ var DEFAULT_RUNTIME_PAGE_HEARTBEAT_SCAN_INTERVAL_MS = 45e3;
87
102
  var DEFAULT_OPTIONS = {
88
103
  mcpPath: DEFAULT_MCP_PATH,
89
104
  host: "localhost",
@@ -135,6 +150,19 @@ var DEFAULT_OPTIONS = {
135
150
  options: {},
136
151
  plugins: []
137
152
  }
153
+ },
154
+ performance: {
155
+ mode: "auto",
156
+ maxDurationMs: DEFAULT_PERFORMANCE_MAX_DURATION_MS,
157
+ sampleIntervalMs: DEFAULT_PERFORMANCE_SAMPLE_INTERVAL_MS,
158
+ longTaskThresholdMs: DEFAULT_PERFORMANCE_LONG_TASK_THRESHOLD_MS,
159
+ saveDir: DEFAULT_PERFORMANCE_SAVE_DIR,
160
+ memory: {
161
+ enabled: true
162
+ },
163
+ stacks: {
164
+ enabled: true
165
+ }
138
166
  }
139
167
  };
140
168
  function mergeMcpClientOptions(cursorConfig, mcpClients) {
@@ -204,6 +232,18 @@ function mergeOptions(options = {}) {
204
232
  ...DEFAULT_OPTIONS.screenshot.snapdom.plugins
205
233
  ]
206
234
  }
235
+ },
236
+ performance: {
237
+ ...DEFAULT_OPTIONS.performance,
238
+ ...options.performance,
239
+ memory: {
240
+ ...DEFAULT_OPTIONS.performance.memory,
241
+ ...options.performance?.memory
242
+ },
243
+ stacks: {
244
+ ...DEFAULT_OPTIONS.performance.stacks,
245
+ ...options.performance?.stacks
246
+ }
207
247
  }
208
248
  };
209
249
  }
@@ -299,7 +339,11 @@ function createVueMcpNextContext(options) {
299
339
  rpcServer: void 0,
300
340
  pages: createPageTargetRegistry(),
301
341
  consoleRecords: createRingBuffer(options.console.maxRecords),
302
- networkRecords: createRingBuffer(options.network.maxRecords)
342
+ networkRecords: createRingBuffer(options.network.maxRecords),
343
+ performanceReports: createRingBuffer(
344
+ DEFAULT_PERFORMANCE_MAX_REPORTS
345
+ ),
346
+ performanceSessions: /* @__PURE__ */ new Map()
303
347
  };
304
348
  }
305
349
 
@@ -417,13 +461,13 @@ async function requestRuntimeData(ctx, trigger) {
417
461
  return { ok: false, error: "runtime bridge is not connected" };
418
462
  }
419
463
  const event = (0, import_nanoid.nanoid)();
420
- return new Promise((resolve) => {
464
+ return new Promise((resolve2) => {
421
465
  const timeout = setTimeout(() => {
422
- resolve({ ok: false, error: "runtime bridge response timed out" });
466
+ resolve2({ ok: false, error: "runtime bridge response timed out" });
423
467
  }, 5e3);
424
468
  ctx.hooks.hookOnce(event, (data) => {
425
469
  clearTimeout(timeout);
426
- resolve(data);
470
+ resolve2(data);
427
471
  });
428
472
  trigger(event);
429
473
  });
@@ -702,12 +746,598 @@ function registerNetworkTools(server, ctx) {
702
746
  );
703
747
  }
704
748
 
705
- // src/mcp/tools/pages.ts
749
+ // src/mcp/tools/performance.ts
706
750
  var import_zod5 = require("zod");
707
751
 
752
+ // src/performance/summary.ts
753
+ function buildPerformanceSummary(input) {
754
+ const memory = buildMemorySummary(input.memorySamples);
755
+ const blockedTimeMs = input.longTasks.reduce((total, task) => {
756
+ return total + Math.max(0, task.durationMs - DEFAULT_PERFORMANCE_LONG_TASK_THRESHOLD_MS);
757
+ }, 0);
758
+ const longTaskCount = input.longTasks.length;
759
+ const durations = input.longTasks.map((task) => task.durationMs);
760
+ const maxTaskDurationMs = durations.length ? Math.max(...durations) : 0;
761
+ const averageTaskDurationMs = durations.length ? Math.round(
762
+ durations.reduce((total, duration) => total + duration, 0) / durations.length
763
+ ) : void 0;
764
+ const suspectedJank = blockedTimeMs > 0 || memory.trend === "growing" || longTaskCount > 0;
765
+ const severity = resolveSeverity({
766
+ blockedTimeMs,
767
+ longTaskCount,
768
+ memoryTrend: memory.trend
769
+ });
770
+ return {
771
+ blockedTimeMs,
772
+ longTaskCount,
773
+ maxTaskDurationMs,
774
+ averageTaskDurationMs,
775
+ suspectedJank,
776
+ severity
777
+ };
778
+ }
779
+ function buildMemorySummary(samples) {
780
+ const first = samples[0]?.usedJSHeapSize;
781
+ const last = samples.at(-1)?.usedJSHeapSize;
782
+ const peak = samples.reduce((currentPeak, sample) => {
783
+ if (typeof sample.usedJSHeapSize !== "number") {
784
+ return currentPeak;
785
+ }
786
+ return Math.max(currentPeak, sample.usedJSHeapSize);
787
+ }, 0);
788
+ const trend = resolveMemoryTrend(first, last);
789
+ return {
790
+ samples,
791
+ initialUsedJSHeapSize: first,
792
+ finalUsedJSHeapSize: last,
793
+ peakUsedJSHeapSize: peak || void 0,
794
+ deltaUsedJSHeapSize: typeof first === "number" && typeof last === "number" ? last - first : void 0,
795
+ trend
796
+ };
797
+ }
798
+ function buildStackSummary(frames, options = {}) {
799
+ return {
800
+ topFrames: [...frames].sort(sortByHotness).slice(0, 10),
801
+ rawProfilePath: options.rawProfilePath,
802
+ limitation: options.limitation
803
+ };
804
+ }
805
+ function resolveSeverity(input) {
806
+ if (input.blockedTimeMs >= 1e3 || input.longTaskCount >= 10) {
807
+ return "critical";
808
+ }
809
+ if (input.blockedTimeMs > 0 || input.memoryTrend === "growing") {
810
+ return "warning";
811
+ }
812
+ return "ok";
813
+ }
814
+ function resolveMemoryTrend(first, last) {
815
+ if (typeof first !== "number" || typeof last !== "number") {
816
+ return "unknown";
817
+ }
818
+ if (last > first) {
819
+ return "growing";
820
+ }
821
+ return "stable";
822
+ }
823
+ function sortByHotness(left, right) {
824
+ return compareNumber(right.totalTimeMs, left.totalTimeMs) || compareNumber(right.selfTimeMs, left.selfTimeMs) || compareNumber(right.hitCount, left.hitCount);
825
+ }
826
+ function compareNumber(left, right) {
827
+ return (left ?? -1) - (right ?? -1);
828
+ }
829
+
830
+ // src/performance/output.ts
831
+ var import_promises = require("fs/promises");
832
+ var import_node_path = require("path");
833
+ var import_nanoid2 = require("nanoid");
834
+ async function writePerformanceArtifact(options) {
835
+ const root = (0, import_node_path.resolve)(options.root ?? process.cwd());
836
+ const dir = resolveOutputDirectory(root, options.saveDir);
837
+ const path8 = (0, import_node_path.resolve)(dir, createSafeFileName(options.fileName));
838
+ const data = typeof options.data === "string" ? Buffer.from(options.data) : options.data;
839
+ await (0, import_promises.mkdir)(dir, { recursive: true });
840
+ await (0, import_promises.writeFile)(path8, data);
841
+ return {
842
+ kind: options.kind,
843
+ path: path8,
844
+ relativePath: (0, import_node_path.relative)(root, path8),
845
+ byteLength: data.byteLength,
846
+ source: "cdp"
847
+ };
848
+ }
849
+ function resolveOutputDirectory(root, saveDir) {
850
+ return (0, import_node_path.resolve)(root, saveDir);
851
+ }
852
+ function createSafeFileName(fileName) {
853
+ const trimmed = fileName.trim();
854
+ if (!trimmed) {
855
+ return `${String(Date.now())}-${(0, import_nanoid2.nanoid)()}`;
856
+ }
857
+ const suffix = (0, import_nanoid2.nanoid)(6);
858
+ const dotIndex = trimmed.lastIndexOf(".");
859
+ if (dotIndex === -1) {
860
+ return `${trimmed}-${suffix}`;
861
+ }
862
+ const base = trimmed.slice(0, dotIndex);
863
+ const extension = trimmed.slice(dotIndex);
864
+ return `${base}-${suffix}${extension}`;
865
+ }
866
+
867
+ // src/cdp/cdpPerformance.ts
868
+ async function recordCdpPerformance(options) {
869
+ const session = await startCdpPerformanceRecording(options);
870
+ await waitForDuration(options.durationMs);
871
+ return stopCdpPerformanceRecording({
872
+ client: options.client,
873
+ session
874
+ });
875
+ }
876
+ async function startCdpPerformanceRecording(options) {
877
+ await options.client.Performance.enable();
878
+ await options.client.Profiler.enable();
879
+ await options.client.Profiler.start();
880
+ return {
881
+ recordingId: createRecordingId(options.pageId),
882
+ pageId: options.pageId,
883
+ startedAt: Date.now(),
884
+ includeMemory: options.includeMemory,
885
+ includeStacks: options.includeStacks,
886
+ saveDir: options.saveDir
887
+ };
888
+ }
889
+ async function stopCdpPerformanceRecording(options) {
890
+ const endedAt = Date.now();
891
+ const [metricsResult, profileResult] = await Promise.all([
892
+ options.client.Performance.getMetrics(),
893
+ options.client.Profiler.stop()
894
+ ]);
895
+ const metrics = toMetricMap(metricsResult.metrics);
896
+ const longTasks = toLongTaskRecords(metrics);
897
+ const memorySamples = options.session.includeMemory ? [toMemorySample(metrics)] : [];
898
+ const memory = options.session.includeMemory ? buildMemorySummary(memorySamples) : void 0;
899
+ const stacks = options.session.includeStacks ? buildStackSummary(aggregateCpuProfile(profileResult.profile)) : void 0;
900
+ const artifact = await writeCpuProfileArtifact(
901
+ {
902
+ pageId: options.session.pageId,
903
+ saveDir: options.session.saveDir
904
+ },
905
+ profileResult.profile
906
+ );
907
+ const report = {
908
+ recordingId: options.session.recordingId,
909
+ pageId: options.session.pageId,
910
+ source: "cdp",
911
+ startedAt: options.session.startedAt,
912
+ endedAt,
913
+ durationMs: endedAt - options.session.startedAt,
914
+ summary: buildPerformanceSummary({
915
+ longTasks,
916
+ memorySamples
917
+ }),
918
+ longTasks,
919
+ memory,
920
+ stacks,
921
+ artifacts: [artifact],
922
+ limitations: [
923
+ "CDP path returns sampled CPU profile data, not a full instruction trace"
924
+ ]
925
+ };
926
+ return report;
927
+ }
928
+ async function takeHeapSnapshot(options) {
929
+ const chunks = [];
930
+ await options.client.HeapProfiler.enable();
931
+ options.client.HeapProfiler.addHeapSnapshotChunk((event) => {
932
+ const payload = event;
933
+ if (payload.chunk) {
934
+ chunks.push(payload.chunk);
935
+ }
936
+ });
937
+ await options.client.HeapProfiler.takeHeapSnapshot({
938
+ reportProgress: false
939
+ });
940
+ const artifact = await writePerformanceArtifact({
941
+ root: process.cwd(),
942
+ saveDir: options.saveDir,
943
+ fileName: `${options.pageId}-heap-snapshot.heapsnapshot`,
944
+ kind: "heap-snapshot",
945
+ data: Buffer.from(chunks.join(""))
946
+ });
947
+ return artifact;
948
+ }
949
+ function createRecordingId(pageId) {
950
+ return `cdp-${pageId}-${String(Date.now())}`;
951
+ }
952
+ function waitForDuration(durationMs) {
953
+ return new Promise((resolve2) => {
954
+ setTimeout(() => {
955
+ resolve2();
956
+ }, durationMs);
957
+ });
958
+ }
959
+ function toMetricMap(metrics) {
960
+ return new Map(metrics.map((metric) => [metric.name, metric.value]));
961
+ }
962
+ function toLongTaskRecords(metrics) {
963
+ const taskDuration = metrics.get("TaskDuration") ?? 0;
964
+ if (taskDuration <= 0) {
965
+ return [];
966
+ }
967
+ return [
968
+ {
969
+ startTime: 0,
970
+ durationMs: taskDuration,
971
+ name: "TaskDuration",
972
+ source: "cpu-profile"
973
+ }
974
+ ];
975
+ }
976
+ function toMemorySample(metrics) {
977
+ return {
978
+ timestamp: Date.now(),
979
+ usedJSHeapSize: metrics.get("JSHeapUsedSize"),
980
+ totalJSHeapSize: metrics.get("JSHeapTotalSize"),
981
+ jsHeapSizeLimit: metrics.get("JSHeapSizeLimit")
982
+ };
983
+ }
984
+ function aggregateCpuProfile(profile) {
985
+ const nodes = profile.nodes ?? [];
986
+ const nodeMap = /* @__PURE__ */ new Map();
987
+ const nodeIndex = /* @__PURE__ */ new Map();
988
+ nodes.forEach((node) => nodeIndex.set(node.id, node));
989
+ (profile.samples ?? []).forEach((nodeId, index) => {
990
+ const node = nodeIndex.get(nodeId);
991
+ if (!node) {
992
+ return;
993
+ }
994
+ const delta = profile.timeDeltas?.[index] ?? 0;
995
+ const current = nodeMap.get(nodeId) ?? {
996
+ functionName: node.callFrame.functionName || "<anonymous>",
997
+ url: node.callFrame.url,
998
+ lineNumber: node.callFrame.lineNumber,
999
+ columnNumber: node.callFrame.columnNumber,
1000
+ selfTimeMs: 0,
1001
+ totalTimeMs: 0,
1002
+ hitCount: 0
1003
+ };
1004
+ nodeMap.set(nodeId, {
1005
+ ...current,
1006
+ selfTimeMs: current.selfTimeMs + delta,
1007
+ totalTimeMs: current.totalTimeMs + delta,
1008
+ hitCount: current.hitCount + 1
1009
+ });
1010
+ });
1011
+ return [...nodeMap.values()];
1012
+ }
1013
+ async function writeCpuProfileArtifact(options, profile) {
1014
+ return writePerformanceArtifact({
1015
+ root: process.cwd(),
1016
+ saveDir: options.saveDir ?? ".vite-mcp/performance",
1017
+ fileName: `${options.pageId}-cpu-profile.cpuprofile`,
1018
+ kind: "cpu-profile",
1019
+ data: Buffer.from(JSON.stringify(profile, null, 2))
1020
+ });
1021
+ }
1022
+
1023
+ // src/mcp/tools/performance.ts
1024
+ var PERFORMANCE_NOT_AVAILABLE_ERROR = "Performance diagnostics are disabled by configuration";
1025
+ var activeCdpPerformanceClients = /* @__PURE__ */ new Map();
1026
+ function registerPerformanceTools(server, ctx) {
1027
+ server.registerTool(
1028
+ MCP_TOOL_NAMES.recordPerformance,
1029
+ {
1030
+ description: "Record a performance sample for the selected page.",
1031
+ inputSchema: {
1032
+ pageId: import_zod5.z.string().optional(),
1033
+ durationMs: import_zod5.z.number().optional(),
1034
+ includeMemory: import_zod5.z.boolean().optional(),
1035
+ includeStacks: import_zod5.z.boolean().optional()
1036
+ }
1037
+ },
1038
+ async (input) => handleRecordPerformance(ctx, input)
1039
+ );
1040
+ server.registerTool(
1041
+ MCP_TOOL_NAMES.startPerformanceRecording,
1042
+ {
1043
+ description: "Start a performance recording session.",
1044
+ inputSchema: {
1045
+ pageId: import_zod5.z.string().optional(),
1046
+ includeMemory: import_zod5.z.boolean().optional(),
1047
+ includeStacks: import_zod5.z.boolean().optional()
1048
+ }
1049
+ },
1050
+ async (input) => handleStartPerformanceRecording(ctx, input)
1051
+ );
1052
+ server.registerTool(
1053
+ MCP_TOOL_NAMES.stopPerformanceRecording,
1054
+ {
1055
+ description: "Stop a performance recording session.",
1056
+ inputSchema: {
1057
+ recordingId: import_zod5.z.string()
1058
+ }
1059
+ },
1060
+ async (input) => handleStopPerformanceRecording(ctx, input)
1061
+ );
1062
+ server.registerTool(
1063
+ MCP_TOOL_NAMES.getPerformanceReport,
1064
+ {
1065
+ description: "Get cached performance reports and active sessions.",
1066
+ inputSchema: {
1067
+ pageId: import_zod5.z.string().optional(),
1068
+ recordingId: import_zod5.z.string().optional(),
1069
+ limit: import_zod5.z.number().optional()
1070
+ }
1071
+ },
1072
+ (input) => handleGetPerformanceReport(ctx, input)
1073
+ );
1074
+ server.registerTool(
1075
+ MCP_TOOL_NAMES.takeHeapSnapshot,
1076
+ {
1077
+ description: "Take a heap snapshot with CDP.",
1078
+ inputSchema: {
1079
+ pageId: import_zod5.z.string().optional()
1080
+ }
1081
+ },
1082
+ async (input) => handleTakeHeapSnapshot(ctx, input)
1083
+ );
1084
+ }
1085
+ function appendPerformanceReport(ctx, report) {
1086
+ if (ctx.performanceReports.all().some((item) => item.recordingId === report.recordingId)) {
1087
+ return;
1088
+ }
1089
+ ctx.performanceReports.push(report);
1090
+ }
1091
+ async function handleRecordPerformance(ctx, input) {
1092
+ const durationMs = input.durationMs ?? ctx.options.performance.maxDurationMs;
1093
+ const includeMemory = input.includeMemory ?? ctx.options.performance.memory.enabled;
1094
+ const includeStacks = input.includeStacks ?? ctx.options.performance.stacks.enabled;
1095
+ if (ctx.options.performance.mode === "off") {
1096
+ return createToolError(PERFORMANCE_NOT_AVAILABLE_ERROR);
1097
+ }
1098
+ if (ctx.options.performance.mode !== "hook") {
1099
+ const cdpResult = await connectPerformanceCdp(ctx, input.pageId);
1100
+ const cdp = cdpResult.cdp;
1101
+ if (cdp) {
1102
+ try {
1103
+ const report = await recordCdpPerformance({
1104
+ client: cdp.client,
1105
+ pageId: input.pageId ?? "cdp",
1106
+ durationMs,
1107
+ includeMemory,
1108
+ includeStacks,
1109
+ saveDir: ctx.options.performance.saveDir
1110
+ });
1111
+ appendPerformanceReport(ctx, report);
1112
+ return createToolResponse(toStructuredRecord(report));
1113
+ } finally {
1114
+ await closeCdpClient(cdp.client);
1115
+ }
1116
+ }
1117
+ if (ctx.options.performance.mode === "cdp") {
1118
+ return createToolError(
1119
+ formatCdpUnavailableError(
1120
+ "CDP performance collection is unavailable",
1121
+ cdpResult.error
1122
+ )
1123
+ );
1124
+ }
1125
+ }
1126
+ const result = await requestRuntimeData(ctx, (event) => {
1127
+ void ctx.rpcServer?.recordPerformance({
1128
+ event,
1129
+ durationMs,
1130
+ includeMemory,
1131
+ includeStacks
1132
+ });
1133
+ });
1134
+ if (isPerformanceReport(result)) {
1135
+ appendPerformanceReport(ctx, result);
1136
+ }
1137
+ if (isPlainRecord(result)) {
1138
+ return createToolResponse(toStructuredRecord(result));
1139
+ }
1140
+ return createToolError("runtime bridge returned an invalid response");
1141
+ }
1142
+ async function handleStartPerformanceRecording(ctx, input) {
1143
+ const includeMemory = input.includeMemory ?? ctx.options.performance.memory.enabled;
1144
+ const includeStacks = input.includeStacks ?? ctx.options.performance.stacks.enabled;
1145
+ if (ctx.options.performance.mode === "off") {
1146
+ return createToolError(PERFORMANCE_NOT_AVAILABLE_ERROR);
1147
+ }
1148
+ if (ctx.options.performance.mode !== "hook") {
1149
+ const cdpResult = await connectPerformanceCdp(ctx, input.pageId);
1150
+ const cdp = cdpResult.cdp;
1151
+ if (cdp) {
1152
+ try {
1153
+ const session = await startCdpPerformanceRecording({
1154
+ client: cdp.client,
1155
+ pageId: input.pageId ?? "cdp",
1156
+ includeMemory,
1157
+ includeStacks,
1158
+ saveDir: ctx.options.performance.saveDir
1159
+ });
1160
+ ctx.performanceSessions.set(session.recordingId, {
1161
+ recordingId: session.recordingId,
1162
+ pageId: session.pageId,
1163
+ source: "cdp",
1164
+ startedAt: session.startedAt,
1165
+ includeMemory,
1166
+ includeStacks,
1167
+ mode: ctx.options.performance.mode
1168
+ });
1169
+ activeCdpPerformanceClients.set(session.recordingId, cdp.client);
1170
+ return createToolResponse(
1171
+ toStructuredRecord({
1172
+ ...session,
1173
+ source: "cdp"
1174
+ })
1175
+ );
1176
+ } catch (error) {
1177
+ await closeCdpClient(cdp.client);
1178
+ return createToolError(
1179
+ error instanceof Error ? error.message : String(error)
1180
+ );
1181
+ }
1182
+ }
1183
+ if (ctx.options.performance.mode === "cdp") {
1184
+ return createToolError(
1185
+ formatCdpUnavailableError(
1186
+ "CDP performance collection is unavailable",
1187
+ cdpResult.error
1188
+ )
1189
+ );
1190
+ }
1191
+ }
1192
+ const result = await requestRuntimeData(ctx, (event) => {
1193
+ void ctx.rpcServer?.startPerformanceRecording({
1194
+ event,
1195
+ includeMemory,
1196
+ includeStacks
1197
+ });
1198
+ });
1199
+ if (isPerformanceStartResult(result)) {
1200
+ ctx.performanceSessions.set(result.recordingId, {
1201
+ recordingId: result.recordingId,
1202
+ pageId: input.pageId ?? "runtime",
1203
+ source: "hook",
1204
+ startedAt: result.startedAt,
1205
+ includeMemory,
1206
+ includeStacks,
1207
+ mode: ctx.options.performance.mode
1208
+ });
1209
+ }
1210
+ if (isPlainRecord(result)) {
1211
+ return createToolResponse(toStructuredRecord(result));
1212
+ }
1213
+ return createToolError("runtime bridge returned an invalid response");
1214
+ }
1215
+ async function handleStopPerformanceRecording(ctx, input) {
1216
+ const session = ctx.performanceSessions.get(input.recordingId);
1217
+ if (!session) {
1218
+ return createToolError(`Performance recording not found: ${input.recordingId}`);
1219
+ }
1220
+ try {
1221
+ if (session.source === "cdp") {
1222
+ const client = activeCdpPerformanceClients.get(input.recordingId);
1223
+ if (!client) {
1224
+ return createToolError(
1225
+ `CDP client not found for recording: ${input.recordingId}`
1226
+ );
1227
+ }
1228
+ const report = await stopCdpPerformanceRecording({
1229
+ client,
1230
+ session
1231
+ });
1232
+ appendPerformanceReport(ctx, report);
1233
+ return createToolResponse(toStructuredRecord(report));
1234
+ }
1235
+ const result = await requestRuntimeData(ctx, (event) => {
1236
+ void ctx.rpcServer?.stopPerformanceRecording({
1237
+ event,
1238
+ recordingId: input.recordingId
1239
+ });
1240
+ });
1241
+ if (isPerformanceReport(result)) {
1242
+ appendPerformanceReport(ctx, result);
1243
+ }
1244
+ if (isPlainRecord(result)) {
1245
+ return createToolResponse(toStructuredRecord(result));
1246
+ }
1247
+ return createToolError("runtime bridge returned an invalid response");
1248
+ } finally {
1249
+ ctx.performanceSessions.delete(input.recordingId);
1250
+ const client = activeCdpPerformanceClients.get(input.recordingId);
1251
+ if (client) {
1252
+ activeCdpPerformanceClients.delete(input.recordingId);
1253
+ await closeCdpClient(client);
1254
+ }
1255
+ }
1256
+ }
1257
+ function handleGetPerformanceReport(ctx, input) {
1258
+ const reports = ctx.performanceReports.all().filter((report) => !input.pageId || report.pageId === input.pageId).filter(
1259
+ (report) => !input.recordingId || report.recordingId === input.recordingId
1260
+ );
1261
+ const limit = input.limit ?? reports.length;
1262
+ return createToolResponse(toStructuredRecord({
1263
+ report: reports.at(-1) ?? null,
1264
+ reports: reports.slice(-limit),
1265
+ sessions: [...ctx.performanceSessions.values()].filter(
1266
+ (session) => (!input.pageId || session.pageId === input.pageId) && (!input.recordingId || session.recordingId === input.recordingId)
1267
+ )
1268
+ }));
1269
+ }
1270
+ async function handleTakeHeapSnapshot(ctx, input) {
1271
+ if (ctx.options.performance.mode === "off" || ctx.options.performance.mode === "hook") {
1272
+ return createToolError(PERFORMANCE_NOT_AVAILABLE_ERROR);
1273
+ }
1274
+ const cdpResult = await connectPerformanceCdp(ctx, input.pageId);
1275
+ const cdp = cdpResult.cdp;
1276
+ if (!cdp) {
1277
+ return createToolError(
1278
+ formatCdpUnavailableError(
1279
+ "CDP heap snapshot is unavailable",
1280
+ cdpResult.error
1281
+ )
1282
+ );
1283
+ }
1284
+ try {
1285
+ return createToolResponse(
1286
+ toStructuredRecord(await takeHeapSnapshot({
1287
+ client: cdp.client,
1288
+ pageId: input.pageId ?? "cdp",
1289
+ saveDir: ctx.options.performance.saveDir
1290
+ }))
1291
+ );
1292
+ } finally {
1293
+ await closeCdpClient(cdp.client);
1294
+ }
1295
+ }
1296
+ async function connectPerformanceCdp(ctx, pageId) {
1297
+ try {
1298
+ return { cdp: await connectCdpForPage(ctx, pageId) };
1299
+ } catch (error) {
1300
+ return {
1301
+ error: error instanceof Error ? error.message : String(error)
1302
+ };
1303
+ }
1304
+ }
1305
+ function formatCdpUnavailableError(message, reason) {
1306
+ if (!reason) {
1307
+ return message;
1308
+ }
1309
+ return `${message}: ${reason}`;
1310
+ }
1311
+ function isPerformanceReport(value) {
1312
+ if (!value || typeof value !== "object") {
1313
+ return false;
1314
+ }
1315
+ const report = value;
1316
+ return typeof report.recordingId === "string" && typeof report.pageId === "string" && (report.source === "hook" || report.source === "cdp") && typeof report.startedAt === "number" && typeof report.endedAt === "number" && typeof report.durationMs === "number" && Boolean(report.summary) && Array.isArray(report.longTasks) && Array.isArray(report.limitations);
1317
+ }
1318
+ function isPerformanceStartResult(value) {
1319
+ if (!value || typeof value !== "object") {
1320
+ return false;
1321
+ }
1322
+ const result = value;
1323
+ return typeof result.recordingId === "string" && typeof result.startedAt === "number";
1324
+ }
1325
+ function isPlainRecord(value) {
1326
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1327
+ }
1328
+ function toStructuredRecord(value) {
1329
+ if (isPlainRecord(value)) {
1330
+ return value;
1331
+ }
1332
+ return {};
1333
+ }
1334
+
1335
+ // src/mcp/tools/pages.ts
1336
+ var import_zod6 = require("zod");
1337
+
708
1338
  // src/plugin/entryDiscovery.ts
709
1339
  var import_node_fs = __toESM(require("fs"), 1);
710
- var import_node_path = __toESM(require("path"), 1);
1340
+ var import_node_path2 = __toESM(require("path"), 1);
711
1341
  var import_vite = require("vite");
712
1342
  function discoverHtmlEntries(server) {
713
1343
  const root = server.config.root;
@@ -720,7 +1350,7 @@ function walkHtmlEntries(root, dir, entries) {
720
1350
  if (item.name === "node_modules" || item.name.startsWith(".")) {
721
1351
  continue;
722
1352
  }
723
- const fullPath = import_node_path.default.join(dir, item.name);
1353
+ const fullPath = import_node_path2.default.join(dir, item.name);
724
1354
  if (item.isDirectory()) {
725
1355
  walkHtmlEntries(root, fullPath, entries);
726
1356
  continue;
@@ -728,10 +1358,10 @@ function walkHtmlEntries(root, dir, entries) {
728
1358
  if (!item.isFile() || !item.name.endsWith(".html")) {
729
1359
  continue;
730
1360
  }
731
- const relative = (0, import_vite.normalizePath)(import_node_path.default.relative(root, fullPath));
1361
+ const relative2 = (0, import_vite.normalizePath)(import_node_path2.default.relative(root, fullPath));
732
1362
  entries.push({
733
- file: relative,
734
- pathname: relative === "index.html" ? "/" : `/${relative}`
1363
+ file: relative2,
1364
+ pathname: relative2 === "index.html" ? "/" : `/${relative2}`
735
1365
  });
736
1366
  }
737
1367
  }
@@ -743,7 +1373,7 @@ function registerPageTools(server, ctx, vite) {
743
1373
  {
744
1374
  description: "List Vite page entries and connected runtime/CDP targets.",
745
1375
  inputSchema: {
746
- includeDisconnected: import_zod5.z.boolean().optional()
1376
+ includeDisconnected: import_zod6.z.boolean().optional()
747
1377
  }
748
1378
  },
749
1379
  async (input) => {
@@ -766,8 +1396,8 @@ function registerPageTools(server, ctx, vite) {
766
1396
  {
767
1397
  description: "Reload the selected page. CDP uses ignoreCache; Runtime Hook falls back to normal reload.",
768
1398
  inputSchema: {
769
- pageId: import_zod5.z.string().optional(),
770
- ignoreCache: import_zod5.z.boolean().optional()
1399
+ pageId: import_zod6.z.string().optional(),
1400
+ ignoreCache: import_zod6.z.boolean().optional()
771
1401
  }
772
1402
  },
773
1403
  async (input) => {
@@ -826,16 +1456,16 @@ function resolveRuntimeReloadTarget(ctx, pageId) {
826
1456
  function waitForRuntimePageReconnect(ctx) {
827
1457
  let timeout;
828
1458
  let cleanup;
829
- const promise = new Promise((resolve) => {
1459
+ const promise = new Promise((resolve2) => {
830
1460
  timeout = setTimeout(() => {
831
1461
  cleanup?.();
832
- resolve(null);
1462
+ resolve2(null);
833
1463
  }, 5e3);
834
1464
  cleanup = ctx.hooks.hookOnce(RUNTIME_PAGE_RECONNECTED_EVENT, (payload) => {
835
1465
  if (timeout) {
836
1466
  clearTimeout(timeout);
837
1467
  }
838
- resolve(isPageTarget(payload) ? payload : null);
1468
+ resolve2(isPageTarget(payload) ? payload : null);
839
1469
  });
840
1470
  });
841
1471
  return {
@@ -919,7 +1549,7 @@ function getPathname(url) {
919
1549
  }
920
1550
 
921
1551
  // src/mcp/tools/screenshot.ts
922
- var import_zod6 = require("zod");
1552
+ var import_zod7 = require("zod");
923
1553
 
924
1554
  // src/cdp/cdpScreenshot.ts
925
1555
  async function cdpCaptureScreenshot(options) {
@@ -1025,16 +1655,16 @@ function isElementRect(value) {
1025
1655
 
1026
1656
  // src/mcp/tools/screenshotOutput.ts
1027
1657
  var import_node_crypto = require("crypto");
1028
- var import_promises = require("fs/promises");
1029
- var import_node_path2 = __toESM(require("path"), 1);
1658
+ var import_promises2 = require("fs/promises");
1659
+ var import_node_path3 = __toESM(require("path"), 1);
1030
1660
  async function createScreenshotOutput(ctx, payload) {
1031
1661
  if (ctx.options.screenshot.type === "base64") {
1032
1662
  return payload;
1033
1663
  }
1034
1664
  const saveDir = resolveScreenshotSaveDir(ctx);
1035
- await (0, import_promises.mkdir)(saveDir, { recursive: true });
1036
- const filePath = import_node_path2.default.join(saveDir, createScreenshotFileName(payload));
1037
- await (0, import_promises.writeFile)(filePath, Buffer.from(payload.data, "base64"));
1665
+ await (0, import_promises2.mkdir)(saveDir, { recursive: true });
1666
+ const filePath = import_node_path3.default.join(saveDir, createScreenshotFileName(payload));
1667
+ await (0, import_promises2.writeFile)(filePath, Buffer.from(payload.data, "base64"));
1038
1668
  return {
1039
1669
  source: payload.source,
1040
1670
  target: payload.target,
@@ -1057,10 +1687,10 @@ function resolveScreenshotSaveDir(ctx) {
1057
1687
  if (!root) {
1058
1688
  throw new Error("Vite server root is required for screenshot path output");
1059
1689
  }
1060
- if (import_node_path2.default.isAbsolute(saveDir)) {
1690
+ if (import_node_path3.default.isAbsolute(saveDir)) {
1061
1691
  return saveDir;
1062
1692
  }
1063
- return import_node_path2.default.resolve(root, saveDir);
1693
+ return import_node_path3.default.resolve(root, saveDir);
1064
1694
  }
1065
1695
  function createScreenshotFileName(payload) {
1066
1696
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -1072,21 +1702,21 @@ function createProjectRelativePath(ctx, filePath) {
1072
1702
  if (!root) {
1073
1703
  throw new Error("Vite server root is required for screenshot path output");
1074
1704
  }
1075
- return import_node_path2.default.relative(root, filePath).split(import_node_path2.default.sep).join("/");
1705
+ return import_node_path3.default.relative(root, filePath).split(import_node_path3.default.sep).join("/");
1076
1706
  }
1077
1707
 
1078
1708
  // src/mcp/tools/screenshot.ts
1079
1709
  var DEFAULT_SCREENSHOT_TARGET = "viewport";
1080
1710
  var DEFAULT_SCREENSHOT_FORMAT = "png";
1081
1711
  var screenshotInputSchema = {
1082
- pageId: import_zod6.z.string().optional(),
1083
- target: import_zod6.z.enum(["viewport", "fullPage", "element"]).optional(),
1084
- selector: import_zod6.z.string().optional(),
1085
- format: import_zod6.z.enum(["png", "jpeg", "webp"]).optional(),
1086
- prefer: import_zod6.z.enum(["auto", "cdp", "runtime"]).optional(),
1087
- quality: import_zod6.z.number().optional(),
1088
- scale: import_zod6.z.number().optional(),
1089
- snapdom: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.unknown()).optional()
1712
+ pageId: import_zod7.z.string().optional(),
1713
+ target: import_zod7.z.enum(["viewport", "fullPage", "element"]).optional(),
1714
+ selector: import_zod7.z.string().optional(),
1715
+ format: import_zod7.z.enum(["png", "jpeg", "webp"]).optional(),
1716
+ prefer: import_zod7.z.enum(["auto", "cdp", "runtime"]).optional(),
1717
+ quality: import_zod7.z.number().optional(),
1718
+ scale: import_zod7.z.number().optional(),
1719
+ snapdom: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.unknown()).optional()
1090
1720
  };
1091
1721
  function registerScreenshotTools(server, ctx) {
1092
1722
  server.registerTool(
@@ -1159,7 +1789,7 @@ async function createRuntimeScreenshot(ctx, input, normalized) {
1159
1789
  `screenshot is too large: ${String(result.byteLength)} bytes`
1160
1790
  );
1161
1791
  }
1162
- if (!isPlainRecord(result)) {
1792
+ if (!isPlainRecord2(result)) {
1163
1793
  return createToolError("runtime screenshot returned an invalid response");
1164
1794
  }
1165
1795
  if (result.ok === false) {
@@ -1188,9 +1818,9 @@ async function createScreenshotResponse(ctx, result) {
1188
1818
  }
1189
1819
  }
1190
1820
  function isScreenshotTooLarge(ctx, result) {
1191
- return isPlainRecord(result) && "byteLength" in result && typeof result.byteLength === "number" && result.byteLength > ctx.options.screenshot.maxBytes;
1821
+ return isPlainRecord2(result) && "byteLength" in result && typeof result.byteLength === "number" && result.byteLength > ctx.options.screenshot.maxBytes;
1192
1822
  }
1193
- function isPlainRecord(value) {
1823
+ function isPlainRecord2(value) {
1194
1824
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1195
1825
  }
1196
1826
  function createMimeType(format) {
@@ -1205,8 +1835,8 @@ function isScreenshotImagePayload(result) {
1205
1835
  }
1206
1836
 
1207
1837
  // src/mcp/tools/vue.ts
1208
- var import_nanoid2 = require("nanoid");
1209
- var import_zod7 = require("zod");
1838
+ var import_nanoid3 = require("nanoid");
1839
+ var import_zod8 = require("zod");
1210
1840
  function registerVueTools(server, ctx) {
1211
1841
  server.registerTool(
1212
1842
  MCP_TOOL_NAMES.getComponentTree,
@@ -1219,7 +1849,7 @@ function registerVueTools(server, ctx) {
1219
1849
  MCP_TOOL_NAMES.getComponentState,
1220
1850
  {
1221
1851
  description: "Get Vue component state.",
1222
- inputSchema: { componentName: import_zod7.z.string() }
1852
+ inputSchema: { componentName: import_zod8.z.string() }
1223
1853
  },
1224
1854
  async ({ componentName }) => requestVueData(ctx, (event) => {
1225
1855
  void ctx.rpcServer?.getInspectorState({ event, componentName });
@@ -1230,10 +1860,10 @@ function registerVueTools(server, ctx) {
1230
1860
  {
1231
1861
  description: "Edit Vue component state.",
1232
1862
  inputSchema: {
1233
- componentName: import_zod7.z.string(),
1234
- path: import_zod7.z.array(import_zod7.z.string()),
1235
- value: import_zod7.z.string(),
1236
- valueType: import_zod7.z.enum(["string", "number", "boolean", "object", "array"])
1863
+ componentName: import_zod8.z.string(),
1864
+ path: import_zod8.z.array(import_zod8.z.string()),
1865
+ value: import_zod8.z.string(),
1866
+ valueType: import_zod8.z.enum(["string", "number", "boolean", "object", "array"])
1237
1867
  }
1238
1868
  },
1239
1869
  ({ componentName, path: path8, value, valueType }) => {
@@ -1253,7 +1883,7 @@ function registerVueTools(server, ctx) {
1253
1883
  MCP_TOOL_NAMES.highlightComponent,
1254
1884
  {
1255
1885
  description: "Highlight a Vue component.",
1256
- inputSchema: { componentName: import_zod7.z.string() }
1886
+ inputSchema: { componentName: import_zod8.z.string() }
1257
1887
  },
1258
1888
  ({ componentName }) => {
1259
1889
  if (!ctx.rpcServer) {
@@ -1281,7 +1911,7 @@ function registerVueTools(server, ctx) {
1281
1911
  MCP_TOOL_NAMES.getPiniaState,
1282
1912
  {
1283
1913
  description: "Get Pinia store state.",
1284
- inputSchema: { storeName: import_zod7.z.string() }
1914
+ inputSchema: { storeName: import_zod8.z.string() }
1285
1915
  },
1286
1916
  async ({ storeName }) => requestVueData(ctx, (event) => {
1287
1917
  void ctx.rpcServer?.getPiniaState({ event, storeName });
@@ -1292,7 +1922,7 @@ async function requestVueData(ctx, trigger) {
1292
1922
  if (!ctx.rpcServer) {
1293
1923
  return vueBridgeUnavailable();
1294
1924
  }
1295
- const event = (0, import_nanoid2.nanoid)();
1925
+ const event = (0, import_nanoid3.nanoid)();
1296
1926
  const data = await waitForVueHook(ctx, event, () => {
1297
1927
  trigger(event);
1298
1928
  });
@@ -1301,13 +1931,13 @@ async function requestVueData(ctx, trigger) {
1301
1931
  };
1302
1932
  }
1303
1933
  function waitForVueHook(ctx, event, trigger) {
1304
- return new Promise((resolve) => {
1934
+ return new Promise((resolve2) => {
1305
1935
  const timeout = setTimeout(() => {
1306
- resolve({ ok: false, error: "Vue runtime bridge response timed out" });
1936
+ resolve2({ ok: false, error: "Vue runtime bridge response timed out" });
1307
1937
  }, 5e3);
1308
1938
  ctx.hooks.hookOnce(event, (data) => {
1309
1939
  clearTimeout(timeout);
1310
- resolve(data);
1940
+ resolve2(data);
1311
1941
  });
1312
1942
  trigger();
1313
1943
  });
@@ -1328,6 +1958,7 @@ function createMcpServer(ctx, vite) {
1328
1958
  registerConsoleTools(server, ctx);
1329
1959
  registerEvaluateTools(server, ctx);
1330
1960
  registerNetworkTools(server, ctx);
1961
+ registerPerformanceTools(server, ctx);
1331
1962
  registerVueTools(server, ctx);
1332
1963
  return server;
1333
1964
  }
@@ -1415,6 +2046,18 @@ function createServerVueRuntimeRpc(ctx) {
1415
2046
  onScreenshotTaken: (event, data) => {
1416
2047
  void ctx.hooks.callHook(event, data);
1417
2048
  },
2049
+ recordPerformance: () => void 0,
2050
+ onPerformanceRecorded: (event, data) => {
2051
+ void ctx.hooks.callHook(event, data);
2052
+ },
2053
+ startPerformanceRecording: () => void 0,
2054
+ onPerformanceRecordingStarted: (event, data) => {
2055
+ void ctx.hooks.callHook(event, data);
2056
+ },
2057
+ stopPerformanceRecording: () => void 0,
2058
+ onPerformanceRecordingStopped: (event, data) => {
2059
+ void ctx.hooks.callHook(event, data);
2060
+ },
1418
2061
  getInspectorTree: () => void 0,
1419
2062
  onInspectorTreeUpdated: (event, data) => {
1420
2063
  void ctx.hooks.callHook(event, data);
@@ -1441,7 +2084,7 @@ function createServerVueRuntimeRpc(ctx) {
1441
2084
  }
1442
2085
 
1443
2086
  // src/cdp/cdpConsole.ts
1444
- var import_nanoid3 = require("nanoid");
2087
+ var import_nanoid4 = require("nanoid");
1445
2088
 
1446
2089
  // src/shared/serialization.ts
1447
2090
  function safeStringify(value) {
@@ -1470,7 +2113,7 @@ async function startCdpConsole(options) {
1470
2113
  await options.client.Runtime.enable();
1471
2114
  options.client.Runtime.consoleAPICalled((event) => {
1472
2115
  options.push({
1473
- id: (0, import_nanoid3.nanoid)(),
2116
+ id: (0, import_nanoid4.nanoid)(),
1474
2117
  pageId: options.pageId,
1475
2118
  source: "cdp",
1476
2119
  level: normalizeConsoleLevel(event.type),
@@ -1490,7 +2133,7 @@ function normalizeConsoleLevel(level) {
1490
2133
  }
1491
2134
 
1492
2135
  // src/cdp/cdpNetwork.ts
1493
- var import_nanoid4 = require("nanoid");
2136
+ var import_nanoid5 = require("nanoid");
1494
2137
 
1495
2138
  // src/shared/sanitize.ts
1496
2139
  function maskHeaders(headers = {}, maskNames = []) {
@@ -1530,7 +2173,7 @@ async function startCdpNetwork(options) {
1530
2173
  await options.client.Network.enable();
1531
2174
  options.client.Network.requestWillBeSent((event) => {
1532
2175
  records.set(event.requestId, {
1533
- id: (0, import_nanoid4.nanoid)(),
2176
+ id: (0, import_nanoid5.nanoid)(),
1534
2177
  pageId: options.pageId,
1535
2178
  source: "cdp",
1536
2179
  url: event.request.url,
@@ -1701,7 +2344,7 @@ async function startCdpObservers(ctx, target, client) {
1701
2344
 
1702
2345
  // src/plugin/injectRuntime.ts
1703
2346
  var import_node_module = require("module");
1704
- var import_node_path3 = require("path");
2347
+ var import_node_path4 = require("path");
1705
2348
  function createRuntimeInjectionController(options, getConfig) {
1706
2349
  return {
1707
2350
  resolveId(importee) {
@@ -1785,7 +2428,7 @@ function createSnapdomLoaderModule(root) {
1785
2428
  }
1786
2429
  function canResolveSnapdomFromProject(root) {
1787
2430
  try {
1788
- (0, import_node_module.createRequire)((0, import_node_path3.join)(root ?? process.cwd(), "package.json")).resolve(
2431
+ (0, import_node_module.createRequire)((0, import_node_path4.join)(root ?? process.cwd(), "package.json")).resolve(
1789
2432
  "@zumer/snapdom"
1790
2433
  );
1791
2434
  return true;
@@ -1824,18 +2467,18 @@ function getPluginPath(plugin) {
1824
2467
  }
1825
2468
 
1826
2469
  // src/plugin/mcpClientConfig/index.ts
1827
- var import_promises4 = __toESM(require("fs/promises"), 1);
1828
- var import_node_path6 = __toESM(require("path"), 1);
2470
+ var import_promises5 = __toESM(require("fs/promises"), 1);
2471
+ var import_node_path7 = __toESM(require("path"), 1);
1829
2472
 
1830
2473
  // src/plugin/mcpClientConfig/codexConfig.ts
1831
- var import_promises2 = __toESM(require("fs/promises"), 1);
1832
- var import_node_path4 = __toESM(require("path"), 1);
2474
+ var import_promises3 = __toESM(require("fs/promises"), 1);
2475
+ var import_node_path5 = __toESM(require("path"), 1);
1833
2476
  async function updateCodexMcpClientConfig(options) {
1834
2477
  try {
1835
2478
  const current = await readOptionalTextFile(options.configPath);
1836
2479
  const next = replaceOrAppendOwnedBlock(current, options);
1837
- await import_promises2.default.mkdir(import_node_path4.default.dirname(options.configPath), { recursive: true });
1838
- await import_promises2.default.writeFile(options.configPath, next);
2480
+ await import_promises3.default.mkdir(import_node_path5.default.dirname(options.configPath), { recursive: true });
2481
+ await import_promises3.default.writeFile(options.configPath, next);
1839
2482
  } catch (error) {
1840
2483
  console.warn(
1841
2484
  `[vite-plugin-vue-mcp-next] Failed to update Codex MCP config at ${options.configPath}: ${formatError(error)}`
@@ -1907,7 +2550,7 @@ function renameServerTableHeader(line, fromServerName, toServerName) {
1907
2550
  }
1908
2551
  async function readOptionalTextFile(filePath) {
1909
2552
  try {
1910
- return await import_promises2.default.readFile(filePath, "utf-8");
2553
+ return await import_promises3.default.readFile(filePath, "utf-8");
1911
2554
  } catch (error) {
1912
2555
  if (isNodeError(error) && error.code === "ENOENT") {
1913
2556
  return "";
@@ -1933,16 +2576,16 @@ function isNodeError(error) {
1933
2576
  }
1934
2577
 
1935
2578
  // src/plugin/mcpClientConfig/jsonConfig.ts
1936
- var import_promises3 = __toESM(require("fs/promises"), 1);
1937
- var import_node_path5 = __toESM(require("path"), 1);
2579
+ var import_promises4 = __toESM(require("fs/promises"), 1);
2580
+ var import_node_path6 = __toESM(require("path"), 1);
1938
2581
  async function updateJsonMcpClientConfig(options) {
1939
2582
  try {
1940
2583
  const config = await readJsonConfig(options.configPath);
1941
- if (!isPlainRecord2(config)) {
2584
+ if (!isPlainRecord3(config)) {
1942
2585
  warnConfigFailure(options, "config root must be a JSON object");
1943
2586
  return;
1944
2587
  }
1945
- const mcpServers = isPlainRecord2(config.mcpServers) ? config.mcpServers : {};
2588
+ const mcpServers = isPlainRecord3(config.mcpServers) ? config.mcpServers : {};
1946
2589
  if (Object.hasOwn(mcpServers, options.serverName)) {
1947
2590
  return;
1948
2591
  }
@@ -1974,8 +2617,8 @@ function renameLegacyServer(mcpServers, options) {
1974
2617
  );
1975
2618
  }
1976
2619
  async function writeJsonConfig(configPath, config) {
1977
- await import_promises3.default.mkdir(import_node_path5.default.dirname(configPath), { recursive: true });
1978
- await import_promises3.default.writeFile(configPath, `${JSON.stringify(config, null, 2)}
2620
+ await import_promises4.default.mkdir(import_node_path6.default.dirname(configPath), { recursive: true });
2621
+ await import_promises4.default.writeFile(configPath, `${JSON.stringify(config, null, 2)}
1979
2622
  `);
1980
2623
  }
1981
2624
  async function readJsonConfig(configPath) {
@@ -1987,7 +2630,7 @@ async function readJsonConfig(configPath) {
1987
2630
  }
1988
2631
  async function readOptionalTextFile2(filePath) {
1989
2632
  try {
1990
- return await import_promises3.default.readFile(filePath, "utf-8");
2633
+ return await import_promises4.default.readFile(filePath, "utf-8");
1991
2634
  } catch (error) {
1992
2635
  if (isNodeError2(error) && error.code === "ENOENT") {
1993
2636
  return "{}";
@@ -1995,7 +2638,7 @@ async function readOptionalTextFile2(filePath) {
1995
2638
  throw error;
1996
2639
  }
1997
2640
  }
1998
- function isPlainRecord2(value) {
2641
+ function isPlainRecord3(value) {
1999
2642
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2000
2643
  }
2001
2644
  function warnConfigFailure(options, reason) {
@@ -2019,12 +2662,12 @@ async function updateMcpClientConfigs(root, sseUrl, streamableHttpUrl, options,
2019
2662
  root,
2020
2663
  clientName: "cursor",
2021
2664
  enabled: options.cursor,
2022
- entryPath: import_node_path6.default.join(root, ".cursor"),
2665
+ entryPath: import_node_path7.default.join(root, ".cursor"),
2023
2666
  entryKind: "directory",
2024
2667
  userOptions,
2025
2668
  createJob: () => updateJsonMcpClientConfig({
2026
2669
  clientName: "Cursor",
2027
- configPath: import_node_path6.default.join(root, ".cursor", "mcp.json"),
2670
+ configPath: import_node_path7.default.join(root, ".cursor", "mcp.json"),
2028
2671
  mcpUrl: sseUrl,
2029
2672
  serverName,
2030
2673
  legacyServerNames
@@ -2034,11 +2677,11 @@ async function updateMcpClientConfigs(root, sseUrl, streamableHttpUrl, options,
2034
2677
  root,
2035
2678
  clientName: "codex",
2036
2679
  enabled: options.codex,
2037
- entryPath: import_node_path6.default.join(root, ".codex"),
2680
+ entryPath: import_node_path7.default.join(root, ".codex"),
2038
2681
  entryKind: "directory",
2039
2682
  userOptions,
2040
2683
  createJob: () => updateCodexMcpClientConfig({
2041
- configPath: import_node_path6.default.join(root, ".codex", "config.toml"),
2684
+ configPath: import_node_path7.default.join(root, ".codex", "config.toml"),
2042
2685
  mcpUrl: streamableHttpUrl,
2043
2686
  serverName,
2044
2687
  legacyServerNames
@@ -2048,12 +2691,12 @@ async function updateMcpClientConfigs(root, sseUrl, streamableHttpUrl, options,
2048
2691
  root,
2049
2692
  clientName: "claudeCode",
2050
2693
  enabled: options.claudeCode,
2051
- entryPath: import_node_path6.default.join(root, ".mcp.json"),
2694
+ entryPath: import_node_path7.default.join(root, ".mcp.json"),
2052
2695
  entryKind: "file",
2053
2696
  userOptions,
2054
2697
  createJob: () => updateJsonMcpClientConfig({
2055
2698
  clientName: "Claude Code",
2056
- configPath: import_node_path6.default.join(root, ".mcp.json"),
2699
+ configPath: import_node_path7.default.join(root, ".mcp.json"),
2057
2700
  mcpUrl: sseUrl,
2058
2701
  serverName,
2059
2702
  legacyServerNames
@@ -2063,12 +2706,12 @@ async function updateMcpClientConfigs(root, sseUrl, streamableHttpUrl, options,
2063
2706
  root,
2064
2707
  clientName: "trae",
2065
2708
  enabled: options.trae,
2066
- entryPath: import_node_path6.default.join(root, ".trae"),
2709
+ entryPath: import_node_path7.default.join(root, ".trae"),
2067
2710
  entryKind: "directory",
2068
2711
  userOptions,
2069
2712
  createJob: () => updateJsonMcpClientConfig({
2070
2713
  clientName: "Trae",
2071
- configPath: import_node_path6.default.join(root, ".trae", "mcp.json"),
2714
+ configPath: import_node_path7.default.join(root, ".trae", "mcp.json"),
2072
2715
  mcpUrl: sseUrl,
2073
2716
  serverName,
2074
2717
  legacyServerNames
@@ -2107,7 +2750,7 @@ function isClientExplicitlyConfigured(clientName, userOptions) {
2107
2750
  }
2108
2751
  async function hasExpectedEntry(entryPath, entryKind) {
2109
2752
  try {
2110
- const stat = await import_promises4.default.stat(entryPath);
2753
+ const stat = await import_promises5.default.stat(entryPath);
2111
2754
  return entryKind === "directory" ? stat.isDirectory() : stat.isFile();
2112
2755
  } catch (error) {
2113
2756
  if (isNodeError3(error) && error.code === "ENOENT") {
@@ -2127,12 +2770,12 @@ function isNodeError3(error) {
2127
2770
  }
2128
2771
 
2129
2772
  // src/plugin/skillConfig/index.ts
2130
- var import_promises6 = __toESM(require("fs/promises"), 1);
2131
- var import_node_path8 = __toESM(require("path"), 1);
2773
+ var import_promises7 = __toESM(require("fs/promises"), 1);
2774
+ var import_node_path9 = __toESM(require("path"), 1);
2132
2775
 
2133
2776
  // src/plugin/skillConfig/writers.ts
2134
- var import_promises5 = __toESM(require("fs/promises"), 1);
2135
- var import_node_path7 = __toESM(require("path"), 1);
2777
+ var import_promises6 = __toESM(require("fs/promises"), 1);
2778
+ var import_node_path8 = __toESM(require("path"), 1);
2136
2779
  var GENERATED_SKILL_CONFIG_MARKER = "<!-- Generated by vite-plugin-vue-mcp-next. Safe to edit, but automatic updates only apply while this marker remains. -->";
2137
2780
  async function writeGeneratedTextFile(options) {
2138
2781
  try {
@@ -2143,8 +2786,8 @@ async function writeGeneratedTextFile(options) {
2143
2786
  );
2144
2787
  return;
2145
2788
  }
2146
- await import_promises5.default.mkdir(import_node_path7.default.dirname(options.filePath), { recursive: true });
2147
- await import_promises5.default.writeFile(options.filePath, options.content);
2789
+ await import_promises6.default.mkdir(import_node_path8.default.dirname(options.filePath), { recursive: true });
2790
+ await import_promises6.default.writeFile(options.filePath, options.content);
2148
2791
  } catch (error) {
2149
2792
  console.warn(
2150
2793
  `[vite-plugin-vue-mcp-next] Failed to update ${options.targetName} at ${options.filePath}: ${formatError4(error)}`
@@ -2153,7 +2796,7 @@ async function writeGeneratedTextFile(options) {
2153
2796
  }
2154
2797
  async function readOptionalTextFile3(filePath) {
2155
2798
  try {
2156
- return await import_promises5.default.readFile(filePath, "utf-8");
2799
+ return await import_promises6.default.readFile(filePath, "utf-8");
2157
2800
  } catch (error) {
2158
2801
  if (isNodeError4(error) && error.code === "ENOENT") {
2159
2802
  return "";
@@ -2170,7 +2813,7 @@ function isNodeError4(error) {
2170
2813
 
2171
2814
  // src/plugin/skillConfig/index.ts
2172
2815
  var PACKAGE_NAME = "@xiaou66/vite-plugin-vue-mcp-next";
2173
- var PACKAGED_SKILL_PATH = import_node_path8.default.join("skills", "vite-mcp-next", "SKILL.md");
2816
+ var PACKAGED_SKILL_PATH = import_node_path9.default.join("skills", "vite-mcp-next", "SKILL.md");
2174
2817
  async function updateSkillConfigs(root, options) {
2175
2818
  if (!options.autoConfig) {
2176
2819
  return;
@@ -2186,13 +2829,13 @@ async function updateSkillConfigs(root, options) {
2186
2829
  function createSkillConfigDescriptors(root) {
2187
2830
  return [
2188
2831
  {
2189
- entryPath: import_node_path8.default.join(root, ".codex"),
2190
- filePath: import_node_path8.default.join(root, ".codex", "skills", "vite-mcp-next", "SKILL.md"),
2832
+ entryPath: import_node_path9.default.join(root, ".codex"),
2833
+ filePath: import_node_path9.default.join(root, ".codex", "skills", "vite-mcp-next", "SKILL.md"),
2191
2834
  targetName: "Codex skill"
2192
2835
  },
2193
2836
  {
2194
- entryPath: import_node_path8.default.join(root, ".claude"),
2195
- filePath: import_node_path8.default.join(
2837
+ entryPath: import_node_path9.default.join(root, ".claude"),
2838
+ filePath: import_node_path9.default.join(
2196
2839
  root,
2197
2840
  ".claude",
2198
2841
  "skills",
@@ -2202,8 +2845,8 @@ function createSkillConfigDescriptors(root) {
2202
2845
  targetName: "Claude Code skill"
2203
2846
  },
2204
2847
  {
2205
- entryPath: import_node_path8.default.join(root, ".cursor"),
2206
- filePath: import_node_path8.default.join(root, ".cursor", "rules", "vite-mcp-next.mdc"),
2848
+ entryPath: import_node_path9.default.join(root, ".cursor"),
2849
+ filePath: import_node_path9.default.join(root, ".cursor", "rules", "vite-mcp-next.mdc"),
2207
2850
  targetName: "Cursor rule"
2208
2851
  }
2209
2852
  ];
@@ -2227,7 +2870,7 @@ async function readPackagedSkillContent(root) {
2227
2870
  const candidates = getPackagedSkillCandidates(root);
2228
2871
  for (const candidate of candidates) {
2229
2872
  try {
2230
- return await import_promises6.default.readFile(candidate, "utf-8");
2873
+ return await import_promises7.default.readFile(candidate, "utf-8");
2231
2874
  } catch (error) {
2232
2875
  if (isNodeError5(error) && error.code === "ENOENT") {
2233
2876
  continue;
@@ -2251,14 +2894,14 @@ async function safelyReadPackagedSkillContent(root) {
2251
2894
  }
2252
2895
  function getPackagedSkillCandidates(root) {
2253
2896
  return [
2254
- import_node_path8.default.resolve(root, "node_modules", PACKAGE_NAME, PACKAGED_SKILL_PATH),
2255
- import_node_path8.default.resolve(process.cwd(), "node_modules", PACKAGE_NAME, PACKAGED_SKILL_PATH),
2256
- import_node_path8.default.resolve(process.cwd(), PACKAGED_SKILL_PATH)
2897
+ import_node_path9.default.resolve(root, "node_modules", PACKAGE_NAME, PACKAGED_SKILL_PATH),
2898
+ import_node_path9.default.resolve(process.cwd(), "node_modules", PACKAGE_NAME, PACKAGED_SKILL_PATH),
2899
+ import_node_path9.default.resolve(process.cwd(), PACKAGED_SKILL_PATH)
2257
2900
  ];
2258
2901
  }
2259
2902
  async function hasDirectoryEntry(entryPath) {
2260
2903
  try {
2261
- const stat = await import_promises6.default.stat(entryPath);
2904
+ const stat = await import_promises7.default.stat(entryPath);
2262
2905
  return stat.isDirectory();
2263
2906
  } catch (error) {
2264
2907
  if (isNodeError5(error) && error.code === "ENOENT") {
@@ -2311,29 +2954,58 @@ function vueMcpNext(userOptions = {}) {
2311
2954
  () => createMcpServer(ctx, server),
2312
2955
  server
2313
2956
  );
2314
- server.ws.on(
2315
- "vite-plugin-vue-mcp-next:page-connected",
2316
- (payload) => {
2317
- if (isRuntimePageTarget(payload)) {
2318
- ctx.pages.upsert(payload);
2319
- void ctx.hooks.callHook(RUNTIME_PAGE_RECONNECTED_EVENT, payload);
2320
- void cdpLifecycle.connectPage(payload);
2957
+ const lastSeenAt = /* @__PURE__ */ new Map();
2958
+ const heartbeatTimer = setInterval(() => {
2959
+ const now = Date.now();
2960
+ for (const [pageId, seenAt] of lastSeenAt) {
2961
+ const target = ctx.pages.get(pageId);
2962
+ if (!target || target.source !== "runtime" || !target.connected) {
2963
+ lastSeenAt.delete(pageId);
2964
+ continue;
2965
+ }
2966
+ if (now - seenAt >= DEFAULT_RUNTIME_PAGE_HEARTBEAT_TIMEOUT_MS) {
2967
+ ctx.pages.disconnect(pageId, now);
2968
+ lastSeenAt.delete(pageId);
2321
2969
  }
2322
2970
  }
2323
- );
2324
- server.ws.on(
2325
- "vite-plugin-vue-mcp-next:console-record",
2326
- (payload) => {
2327
- if (isConsoleRecord(payload)) {
2328
- ctx.consoleRecords.push(payload);
2971
+ }, DEFAULT_RUNTIME_PAGE_HEARTBEAT_SCAN_INTERVAL_MS);
2972
+ server.ws.on(RUNTIME_PAGE_CONNECTED_EVENT, (payload) => {
2973
+ if (isRuntimePageTarget(payload)) {
2974
+ ctx.pages.upsert(payload);
2975
+ lastSeenAt.set(payload.pageId, Date.now());
2976
+ void ctx.hooks.callHook(RUNTIME_PAGE_RECONNECTED_EVENT, payload);
2977
+ void cdpLifecycle.connectPage(payload);
2978
+ }
2979
+ });
2980
+ server.ws.on(RUNTIME_PAGE_HEARTBEAT_EVENT, (payload) => {
2981
+ if (isRuntimeHeartbeatTarget(payload)) {
2982
+ const target = ctx.pages.get(payload.pageId);
2983
+ if (target?.source === "runtime" && target.connected) {
2984
+ lastSeenAt.set(payload.pageId, payload.timestamp);
2329
2985
  }
2330
2986
  }
2331
- );
2987
+ });
2988
+ server.ws.on(RUNTIME_PAGE_DISCONNECTED_EVENT, (payload) => {
2989
+ if (isRuntimeDisconnectTarget(payload)) {
2990
+ ctx.pages.disconnect(payload.pageId);
2991
+ lastSeenAt.delete(payload.pageId);
2992
+ }
2993
+ });
2994
+ server.ws.on("vite-plugin-vue-mcp-next:console-record", (payload) => {
2995
+ if (isConsoleRecord(payload)) {
2996
+ ctx.consoleRecords.push(payload);
2997
+ }
2998
+ });
2999
+ server.ws.on("vite-plugin-vue-mcp-next:network-record", (payload) => {
3000
+ if (isNetworkRecord(payload)) {
3001
+ ctx.networkRecords.push(payload);
3002
+ }
3003
+ });
2332
3004
  server.ws.on(
2333
- "vite-plugin-vue-mcp-next:network-record",
3005
+ "vite-plugin-vue-mcp-next:performance-record",
2334
3006
  (payload) => {
2335
- if (isNetworkRecord(payload)) {
2336
- ctx.networkRecords.push(payload);
3007
+ if (isPerformanceReport2(payload)) {
3008
+ appendPerformanceReport(ctx, payload);
2337
3009
  }
2338
3010
  }
2339
3011
  );
@@ -2358,6 +3030,7 @@ function vueMcpNext(userOptions = {}) {
2358
3030
  }, 300);
2359
3031
  }
2360
3032
  server.httpServer?.once("close", () => {
3033
+ clearInterval(heartbeatTimer);
2361
3034
  void cdpLifecycle.closeAll();
2362
3035
  });
2363
3036
  },
@@ -2382,6 +3055,20 @@ function isRuntimePageTarget(payload) {
2382
3055
  const target = payload;
2383
3056
  return target.source === "runtime" && typeof target.pageId === "string" && typeof target.url === "string" && typeof target.pathname === "string" && typeof target.connected === "boolean" && (target.runtimeClientId === void 0 || typeof target.runtimeClientId === "string");
2384
3057
  }
3058
+ function isRuntimeHeartbeatTarget(payload) {
3059
+ if (!payload || typeof payload !== "object") {
3060
+ return false;
3061
+ }
3062
+ const target = payload;
3063
+ return typeof target.pageId === "string" && typeof target.timestamp === "number";
3064
+ }
3065
+ function isRuntimeDisconnectTarget(payload) {
3066
+ if (!payload || typeof payload !== "object") {
3067
+ return false;
3068
+ }
3069
+ const target = payload;
3070
+ return typeof target.pageId === "string";
3071
+ }
2385
3072
  function isConsoleRecord(payload) {
2386
3073
  if (!payload || typeof payload !== "object") {
2387
3074
  return false;
@@ -2399,6 +3086,13 @@ function isNetworkRecord(payload) {
2399
3086
  const record = payload;
2400
3087
  return typeof record.id === "string" && typeof record.pageId === "string" && record.source === "hook" && typeof record.url === "string" && typeof record.method === "string" && typeof record.startedAt === "number";
2401
3088
  }
3089
+ function isPerformanceReport2(payload) {
3090
+ if (!payload || typeof payload !== "object") {
3091
+ return false;
3092
+ }
3093
+ const report = payload;
3094
+ return typeof report.recordingId === "string" && typeof report.pageId === "string" && (report.source === "hook" || report.source === "cdp") && Boolean(report.summary) && Array.isArray(report.longTasks) && Array.isArray(report.limitations);
3095
+ }
2402
3096
  // Annotate the CommonJS export names for ESM import in node:
2403
3097
  0 && (module.exports = {
2404
3098
  vueMcpNext