@treelocator/runtime 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -0
- package/dist/_generated_styles.d.ts +1 -1
- package/dist/_generated_styles.js +20 -0
- package/dist/_generated_tree_icon.d.ts +1 -1
- package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
- package/dist/adapters/HtmlElementTreeNode.js +4 -6
- package/dist/adapters/createTreeNode.js +17 -44
- package/dist/adapters/detectFramework.d.ts +8 -0
- package/dist/adapters/detectFramework.js +25 -0
- package/dist/adapters/detectFramework.test.d.ts +1 -0
- package/dist/adapters/detectFramework.test.js +60 -0
- package/dist/adapters/jsx/jsxAdapter.js +54 -89
- package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
- package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
- package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
- package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
- package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
- package/dist/adapters/resolveAdapter.d.ts +1 -1
- package/dist/adapters/resolveAdapter.js +4 -8
- package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
- package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
- package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
- package/dist/adapters/vue/vueAdapter.test.js +222 -0
- package/dist/browserApi.d.ts +148 -0
- package/dist/browserApi.js +146 -5
- package/dist/browserApi.test.d.ts +1 -0
- package/dist/browserApi.test.js +287 -0
- package/dist/components/RecordingPillButton.d.ts +11 -0
- package/dist/components/RecordingPillButton.js +202 -0
- package/dist/components/RecordingResults.d.ts +2 -0
- package/dist/components/RecordingResults.js +213 -78
- package/dist/components/Runtime.js +161 -554
- package/dist/components/SettingsPanel.d.ts +5 -0
- package/dist/components/SettingsPanel.js +312 -0
- package/dist/consoleCapture.d.ts +9 -0
- package/dist/consoleCapture.js +95 -0
- package/dist/functions/cssRuleInspector.d.ts +83 -0
- package/dist/functions/cssRuleInspector.js +608 -0
- package/dist/functions/cssRuleInspector.test.d.ts +1 -0
- package/dist/functions/cssRuleInspector.test.js +439 -0
- package/dist/functions/deduplicateLabels.test.d.ts +1 -0
- package/dist/functions/deduplicateLabels.test.js +178 -0
- package/dist/functions/enrichAncestrySourceMaps.js +0 -1
- package/dist/functions/extractComputedStyles.d.ts +51 -0
- package/dist/functions/extractComputedStyles.js +447 -0
- package/dist/functions/extractComputedStyles.test.d.ts +1 -0
- package/dist/functions/extractComputedStyles.test.js +549 -0
- package/dist/functions/formatAncestryChain.d.ts +8 -0
- package/dist/functions/formatAncestryChain.js +21 -1
- package/dist/functions/formatAncestryChain.test.js +18 -0
- package/dist/functions/getUsableName.test.d.ts +1 -0
- package/dist/functions/getUsableName.test.js +219 -0
- package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
- package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
- package/dist/functions/mergeRects.test.js +210 -1
- package/dist/functions/namedSnapshots.d.ts +52 -0
- package/dist/functions/namedSnapshots.js +161 -0
- package/dist/functions/namedSnapshots.test.d.ts +1 -0
- package/dist/functions/namedSnapshots.test.js +85 -0
- package/dist/functions/normalizeFilePath.test.d.ts +1 -0
- package/dist/functions/normalizeFilePath.test.js +66 -0
- package/dist/functions/parseDataId.test.d.ts +1 -0
- package/dist/functions/parseDataId.test.js +101 -0
- package/dist/hooks/getStorage.d.ts +3 -0
- package/dist/hooks/getStorage.js +17 -0
- package/dist/hooks/useEventListeners.d.ts +15 -0
- package/dist/hooks/useEventListeners.js +56 -0
- package/dist/hooks/useLocatorStorage.d.ts +18 -0
- package/dist/hooks/useLocatorStorage.js +41 -0
- package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
- package/dist/hooks/useLocatorStorage.test.js +124 -0
- package/dist/hooks/useRecordingState.d.ts +43 -0
- package/dist/hooks/useRecordingState.js +387 -0
- package/dist/hooks/useSettings.d.ts +13 -0
- package/dist/hooks/useSettings.js +66 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/initRuntime.d.ts +3 -1
- package/dist/initRuntime.js +4 -1
- package/dist/mcpBridge.d.ts +61 -0
- package/dist/mcpBridge.js +534 -0
- package/dist/mcpBridge.test.d.ts +1 -0
- package/dist/mcpBridge.test.js +248 -0
- package/dist/output.css +20 -0
- package/dist/visualDiff/diff.d.ts +9 -0
- package/dist/visualDiff/diff.js +209 -0
- package/dist/visualDiff/diff.test.d.ts +1 -0
- package/dist/visualDiff/diff.test.js +253 -0
- package/dist/visualDiff/settle.d.ts +3 -0
- package/dist/visualDiff/settle.js +50 -0
- package/dist/visualDiff/settle.test.d.ts +1 -0
- package/dist/visualDiff/settle.test.js +65 -0
- package/dist/visualDiff/snapshot.d.ts +4 -0
- package/dist/visualDiff/snapshot.js +84 -0
- package/dist/visualDiff/snapshot.test.d.ts +1 -0
- package/dist/visualDiff/snapshot.test.js +245 -0
- package/dist/visualDiff/types.d.ts +37 -0
- package/dist/visualDiff/types.js +1 -0
- package/package.json +2 -2
- package/scripts/wrapCSS.js +1 -1
- package/scripts/wrapImage.js +1 -1
- package/src/_generated_styles.ts +21 -1
- package/src/_generated_tree_icon.ts +1 -1
- package/src/adapters/HtmlElementTreeNode.ts +10 -7
- package/src/adapters/createTreeNode.ts +12 -51
- package/src/adapters/detectFramework.test.ts +73 -0
- package/src/adapters/detectFramework.ts +28 -0
- package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
- package/src/adapters/jsx/jsxAdapter.ts +53 -106
- package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
- package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
- package/src/adapters/react/findDebugSource.ts +5 -6
- package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
- package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
- package/src/adapters/react/reactAdapter.ts +1 -2
- package/src/adapters/resolveAdapter.ts +4 -14
- package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
- package/src/adapters/vue/vueAdapter.test.ts +259 -0
- package/src/browserApi.test.ts +329 -0
- package/src/browserApi.ts +351 -4
- package/src/components/RecordingPillButton.tsx +301 -0
- package/src/components/RecordingResults.tsx +114 -13
- package/src/components/Runtime.tsx +176 -621
- package/src/components/SettingsPanel.tsx +339 -0
- package/src/consoleCapture.ts +113 -0
- package/src/functions/cssRuleInspector.test.ts +517 -0
- package/src/functions/cssRuleInspector.ts +708 -0
- package/src/functions/deduplicateLabels.test.ts +115 -0
- package/src/functions/enrichAncestrySourceMaps.ts +6 -3
- package/src/functions/extractComputedStyles.test.ts +681 -0
- package/src/functions/extractComputedStyles.ts +768 -0
- package/src/functions/formatAncestryChain.test.ts +23 -1
- package/src/functions/formatAncestryChain.ts +22 -1
- package/src/functions/getUsableName.test.ts +242 -0
- package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
- package/src/functions/mergeRects.test.ts +111 -1
- package/src/functions/namedSnapshots.test.ts +106 -0
- package/src/functions/namedSnapshots.ts +232 -0
- package/src/functions/normalizeFilePath.test.ts +80 -0
- package/src/functions/parseDataId.test.ts +125 -0
- package/src/hooks/getStorage.ts +26 -0
- package/src/hooks/useEventListeners.ts +97 -0
- package/src/hooks/useLocatorStorage.test.ts +127 -0
- package/src/hooks/useLocatorStorage.ts +60 -0
- package/src/hooks/useRecordingState.ts +516 -0
- package/src/hooks/useSettings.ts +83 -0
- package/src/index.ts +10 -5
- package/src/initRuntime.ts +5 -0
- package/src/mcpBridge.test.ts +260 -0
- package/src/mcpBridge.ts +677 -0
- package/src/visualDiff/diff.test.ts +167 -0
- package/src/visualDiff/diff.ts +242 -0
- package/src/visualDiff/settle.test.ts +77 -0
- package/src/visualDiff/settle.ts +62 -0
- package/src/visualDiff/snapshot.test.ts +200 -0
- package/src/visualDiff/snapshot.ts +119 -0
- package/src/visualDiff/types.ts +40 -0
- package/tsconfig.json +3 -1
- package/vitest.config.ts +18 -0
- package/jest.config.ts +0 -195
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { LocatorJSAPI } from "./browserApi";
|
|
2
|
+
export declare const MCP_BRIDGE_DEFAULT_URL = "wss://127.0.0.1:7463/treelocator";
|
|
3
|
+
export declare const MCP_BRIDGE_FALLBACK_URL = "wss://localhost:7463/treelocator";
|
|
4
|
+
export type BridgeCommandName = "get_path" | "get_ancestry" | "get_path_data" | "get_styles" | "get_css_rules" | "get_css_report" | "take_snapshot" | "get_snapshot_diff" | "clear_snapshot" | "click" | "hover" | "type" | "execute_js" | "get_console";
|
|
5
|
+
export interface HelloMessage {
|
|
6
|
+
type: "hello";
|
|
7
|
+
sessionId: string;
|
|
8
|
+
url: string;
|
|
9
|
+
title: string;
|
|
10
|
+
runtimeVersion: string;
|
|
11
|
+
capabilities: BridgeCommandName[];
|
|
12
|
+
connectedAt: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CommandRequest {
|
|
15
|
+
type: "command";
|
|
16
|
+
id: string;
|
|
17
|
+
command: BridgeCommandName;
|
|
18
|
+
args?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface CommandResponse {
|
|
21
|
+
type: "response";
|
|
22
|
+
id: string;
|
|
23
|
+
ok: boolean;
|
|
24
|
+
result?: unknown;
|
|
25
|
+
error?: {
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
details?: unknown;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface MCPBridgeConfig {
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
bridgeUrl?: string;
|
|
34
|
+
reconnectMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function executeBridgeCommand(api: LocatorJSAPI, command: BridgeCommandName, args?: Record<string, unknown>): Promise<unknown>;
|
|
37
|
+
export declare class TreeLocatorMCPBridgeClient {
|
|
38
|
+
private readonly config;
|
|
39
|
+
private readonly sessionId;
|
|
40
|
+
private readonly runtimeVersion;
|
|
41
|
+
private readonly getApi;
|
|
42
|
+
private socket;
|
|
43
|
+
private heartbeatTimer;
|
|
44
|
+
private reconnectTimer;
|
|
45
|
+
private reconnectAttempts;
|
|
46
|
+
private consecutiveFailures;
|
|
47
|
+
private urlIndex;
|
|
48
|
+
private stopped;
|
|
49
|
+
constructor(getApi: () => LocatorJSAPI | undefined, config?: MCPBridgeConfig, runtimeVersion?: string);
|
|
50
|
+
start(): void;
|
|
51
|
+
stop(): void;
|
|
52
|
+
private connect;
|
|
53
|
+
private sendMessage;
|
|
54
|
+
private sendHello;
|
|
55
|
+
private handleMessage;
|
|
56
|
+
private startHeartbeat;
|
|
57
|
+
private clearHeartbeat;
|
|
58
|
+
private scheduleReconnect;
|
|
59
|
+
private clearTimers;
|
|
60
|
+
}
|
|
61
|
+
export declare function startMCPBridge(config?: MCPBridgeConfig): TreeLocatorMCPBridgeClient | null;
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { getConsoleEntries, installConsoleCapture } from "./consoleCapture";
|
|
2
|
+
export const MCP_BRIDGE_DEFAULT_URL = "wss://127.0.0.1:7463/treelocator";
|
|
3
|
+
export const MCP_BRIDGE_FALLBACK_URL = "wss://localhost:7463/treelocator";
|
|
4
|
+
const HEARTBEAT_MS = 20_000;
|
|
5
|
+
const MAX_RECONNECT_MS = 5 * 60_000;
|
|
6
|
+
const QUIET_RETRY_AFTER_FAILURES = 2;
|
|
7
|
+
const QUIET_RETRY_MS = 5 * 60_000;
|
|
8
|
+
class BridgeRuntimeError extends Error {
|
|
9
|
+
constructor(code, message, details) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "BridgeRuntimeError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const SESSION_ID_STORAGE_KEY = "treelocator:sessionId";
|
|
17
|
+
function generateSessionId() {
|
|
18
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
19
|
+
return crypto.randomUUID();
|
|
20
|
+
}
|
|
21
|
+
return `tl-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
22
|
+
}
|
|
23
|
+
function createSessionId() {
|
|
24
|
+
// Persist per tab via sessionStorage so reloads keep the same id — the
|
|
25
|
+
// broker accepts rehello on an existing id and just swaps the socket, so
|
|
26
|
+
// MCP callers don't need to re-run connect_session after a page refresh.
|
|
27
|
+
let storage = null;
|
|
28
|
+
try {
|
|
29
|
+
storage = typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
30
|
+
} catch {
|
|
31
|
+
storage = null;
|
|
32
|
+
}
|
|
33
|
+
if (storage) {
|
|
34
|
+
try {
|
|
35
|
+
const existing = storage.getItem(SESSION_ID_STORAGE_KEY);
|
|
36
|
+
if (existing) return existing;
|
|
37
|
+
} catch {
|
|
38
|
+
// Access can throw under strict privacy settings — fall through.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const fresh = generateSessionId();
|
|
42
|
+
if (storage) {
|
|
43
|
+
try {
|
|
44
|
+
storage.setItem(SESSION_ID_STORAGE_KEY, fresh);
|
|
45
|
+
} catch {
|
|
46
|
+
// Quota or privacy mode — not fatal, we just won't persist.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return fresh;
|
|
50
|
+
}
|
|
51
|
+
function parseMessage(raw) {
|
|
52
|
+
if (typeof raw !== "string") return null;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
56
|
+
if ("type" in parsed && (parsed.type === "command" || parsed.type === "pong")) {
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function resolveElement(selector, index = 0) {
|
|
65
|
+
let elements;
|
|
66
|
+
try {
|
|
67
|
+
elements = document.querySelectorAll(selector);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new BridgeRuntimeError("invalid_selector", `Invalid selector: ${selector}`);
|
|
70
|
+
}
|
|
71
|
+
const element = elements.item(index);
|
|
72
|
+
if (!(element instanceof HTMLElement)) {
|
|
73
|
+
throw new BridgeRuntimeError("element_not_found", `No HTMLElement found for selector "${selector}" at index ${index}`);
|
|
74
|
+
}
|
|
75
|
+
return element;
|
|
76
|
+
}
|
|
77
|
+
function getTargetArgs(args) {
|
|
78
|
+
const selector = typeof args?.selector === "string" ? args.selector : "";
|
|
79
|
+
if (!selector) {
|
|
80
|
+
throw new BridgeRuntimeError("invalid_args", "selector is required");
|
|
81
|
+
}
|
|
82
|
+
const rawIndex = args?.index;
|
|
83
|
+
const index = typeof rawIndex === "number" && Number.isInteger(rawIndex) && rawIndex >= 0 ? rawIndex : 0;
|
|
84
|
+
return {
|
|
85
|
+
selector,
|
|
86
|
+
index
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function dispatchHover(element) {
|
|
90
|
+
const eventWindow = element.ownerDocument.defaultView;
|
|
91
|
+
const MouseEventCtor = eventWindow?.MouseEvent || MouseEvent;
|
|
92
|
+
const eventInit = {
|
|
93
|
+
bubbles: true,
|
|
94
|
+
cancelable: true,
|
|
95
|
+
composed: true
|
|
96
|
+
};
|
|
97
|
+
element.dispatchEvent(new MouseEventCtor("mouseenter", eventInit));
|
|
98
|
+
element.dispatchEvent(new MouseEventCtor("mouseover", eventInit));
|
|
99
|
+
element.dispatchEvent(new MouseEventCtor("mousemove", eventInit));
|
|
100
|
+
}
|
|
101
|
+
function dispatchType(element, text, submit) {
|
|
102
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
103
|
+
element.focus();
|
|
104
|
+
element.value = text;
|
|
105
|
+
element.dispatchEvent(new Event("input", {
|
|
106
|
+
bubbles: true,
|
|
107
|
+
cancelable: true
|
|
108
|
+
}));
|
|
109
|
+
element.dispatchEvent(new Event("change", {
|
|
110
|
+
bubbles: true,
|
|
111
|
+
cancelable: true
|
|
112
|
+
}));
|
|
113
|
+
if (submit) {
|
|
114
|
+
element.dispatchEvent(new KeyboardEvent("keydown", {
|
|
115
|
+
key: "Enter",
|
|
116
|
+
bubbles: true
|
|
117
|
+
}));
|
|
118
|
+
element.dispatchEvent(new KeyboardEvent("keyup", {
|
|
119
|
+
key: "Enter",
|
|
120
|
+
bubbles: true
|
|
121
|
+
}));
|
|
122
|
+
if (element.form && typeof element.form.requestSubmit === "function") {
|
|
123
|
+
element.form.requestSubmit();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
value: element.value
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (element.isContentEditable) {
|
|
131
|
+
element.focus();
|
|
132
|
+
element.textContent = text;
|
|
133
|
+
element.dispatchEvent(new Event("input", {
|
|
134
|
+
bubbles: true,
|
|
135
|
+
cancelable: true
|
|
136
|
+
}));
|
|
137
|
+
return {
|
|
138
|
+
value: element.textContent || ""
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
throw new BridgeRuntimeError("unsupported_target", "type target must be input, textarea, or contenteditable");
|
|
142
|
+
}
|
|
143
|
+
function safeSerialize(value) {
|
|
144
|
+
const seen = new WeakSet();
|
|
145
|
+
const walk = v => {
|
|
146
|
+
if (v === null || v === undefined) return v ?? null;
|
|
147
|
+
const t = typeof v;
|
|
148
|
+
if (t === "string" || t === "number" || t === "boolean") return v;
|
|
149
|
+
if (t === "bigint") return v.toString();
|
|
150
|
+
if (t === "function") {
|
|
151
|
+
const name = v.name || "anonymous";
|
|
152
|
+
return `[Function: ${name}]`;
|
|
153
|
+
}
|
|
154
|
+
if (t === "symbol") return v.toString();
|
|
155
|
+
if (v instanceof Error) {
|
|
156
|
+
return {
|
|
157
|
+
name: v.name,
|
|
158
|
+
message: v.message,
|
|
159
|
+
stack: v.stack
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (typeof Element !== "undefined" && v instanceof Element) {
|
|
163
|
+
const el = v;
|
|
164
|
+
return `[Element: <${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ""}>]`;
|
|
165
|
+
}
|
|
166
|
+
if (t === "object") {
|
|
167
|
+
if (seen.has(v)) return "[Circular]";
|
|
168
|
+
seen.add(v);
|
|
169
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
170
|
+
const out = {};
|
|
171
|
+
for (const key of Object.keys(v)) {
|
|
172
|
+
try {
|
|
173
|
+
out[key] = walk(v[key]);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
out[key] = `[Unreadable: ${err.message}]`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
return String(v);
|
|
181
|
+
};
|
|
182
|
+
return walk(value);
|
|
183
|
+
}
|
|
184
|
+
const AsyncFunctionCtor = Object.getPrototypeOf(async function () {}).constructor;
|
|
185
|
+
async function runUserCode(code) {
|
|
186
|
+
let fn;
|
|
187
|
+
try {
|
|
188
|
+
fn = new AsyncFunctionCtor(code);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
throw new BridgeRuntimeError("compile_error", err instanceof Error ? err.message : "Failed to compile code");
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
return await fn();
|
|
194
|
+
} catch (err) {
|
|
195
|
+
throw new BridgeRuntimeError("runtime_error", err instanceof Error ? err.message : "Execution failed", err instanceof Error ? {
|
|
196
|
+
stack: err.stack
|
|
197
|
+
} : undefined);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export async function executeBridgeCommand(api, command, args) {
|
|
201
|
+
switch (command) {
|
|
202
|
+
case "get_path":
|
|
203
|
+
{
|
|
204
|
+
const {
|
|
205
|
+
selector
|
|
206
|
+
} = getTargetArgs(args);
|
|
207
|
+
return await api.getPath(selector);
|
|
208
|
+
}
|
|
209
|
+
case "get_ancestry":
|
|
210
|
+
{
|
|
211
|
+
const {
|
|
212
|
+
selector
|
|
213
|
+
} = getTargetArgs(args);
|
|
214
|
+
return await api.getAncestry(selector);
|
|
215
|
+
}
|
|
216
|
+
case "get_path_data":
|
|
217
|
+
{
|
|
218
|
+
const {
|
|
219
|
+
selector
|
|
220
|
+
} = getTargetArgs(args);
|
|
221
|
+
return await api.getPathData(selector);
|
|
222
|
+
}
|
|
223
|
+
case "get_styles":
|
|
224
|
+
{
|
|
225
|
+
const {
|
|
226
|
+
selector
|
|
227
|
+
} = getTargetArgs(args);
|
|
228
|
+
const options = args && typeof args.options === "object" && args.options !== null ? args.options : undefined;
|
|
229
|
+
return api.getStyles(selector, {
|
|
230
|
+
includeDefaults: typeof options?.includeDefaults === "boolean" ? options.includeDefaults : undefined
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
case "get_css_rules":
|
|
234
|
+
{
|
|
235
|
+
const {
|
|
236
|
+
selector
|
|
237
|
+
} = getTargetArgs(args);
|
|
238
|
+
return api.getCSSRules(selector);
|
|
239
|
+
}
|
|
240
|
+
case "get_css_report":
|
|
241
|
+
{
|
|
242
|
+
const {
|
|
243
|
+
selector
|
|
244
|
+
} = getTargetArgs(args);
|
|
245
|
+
const properties = Array.isArray(args?.properties) ? args?.properties.filter(item => typeof item === "string") : undefined;
|
|
246
|
+
return api.getCSSReport(selector, properties ? {
|
|
247
|
+
properties
|
|
248
|
+
} : undefined);
|
|
249
|
+
}
|
|
250
|
+
case "take_snapshot":
|
|
251
|
+
{
|
|
252
|
+
const selector = typeof args?.selector === "string" ? args.selector : "";
|
|
253
|
+
const snapshotId = typeof args?.snapshotId === "string" ? args.snapshotId : "";
|
|
254
|
+
if (!selector) {
|
|
255
|
+
throw new BridgeRuntimeError("invalid_args", "selector is required");
|
|
256
|
+
}
|
|
257
|
+
if (!snapshotId) {
|
|
258
|
+
throw new BridgeRuntimeError("invalid_args", "snapshotId is required");
|
|
259
|
+
}
|
|
260
|
+
const index = typeof args?.index === "number" && Number.isInteger(args.index) && args.index >= 0 ? args.index : 0;
|
|
261
|
+
const label = typeof args?.label === "string" ? args.label : undefined;
|
|
262
|
+
return api.takeSnapshot(selector, snapshotId, {
|
|
263
|
+
index,
|
|
264
|
+
label
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
case "get_snapshot_diff":
|
|
268
|
+
{
|
|
269
|
+
const snapshotId = typeof args?.snapshotId === "string" ? args.snapshotId : "";
|
|
270
|
+
if (!snapshotId) {
|
|
271
|
+
throw new BridgeRuntimeError("invalid_args", "snapshotId is required");
|
|
272
|
+
}
|
|
273
|
+
return api.getSnapshotDiff(snapshotId);
|
|
274
|
+
}
|
|
275
|
+
case "clear_snapshot":
|
|
276
|
+
{
|
|
277
|
+
const snapshotId = typeof args?.snapshotId === "string" ? args.snapshotId : "";
|
|
278
|
+
if (!snapshotId) {
|
|
279
|
+
throw new BridgeRuntimeError("invalid_args", "snapshotId is required");
|
|
280
|
+
}
|
|
281
|
+
api.clearSnapshot(snapshotId);
|
|
282
|
+
return {
|
|
283
|
+
ok: true
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
case "click":
|
|
287
|
+
{
|
|
288
|
+
const {
|
|
289
|
+
selector,
|
|
290
|
+
index
|
|
291
|
+
} = getTargetArgs(args);
|
|
292
|
+
const element = resolveElement(selector, index);
|
|
293
|
+
if (typeof element.click === "function") {
|
|
294
|
+
element.click();
|
|
295
|
+
} else {
|
|
296
|
+
const eventWindow = element.ownerDocument.defaultView;
|
|
297
|
+
const MouseEventCtor = eventWindow?.MouseEvent || MouseEvent;
|
|
298
|
+
element.dispatchEvent(new MouseEventCtor("click", {
|
|
299
|
+
bubbles: true,
|
|
300
|
+
cancelable: true,
|
|
301
|
+
composed: true
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
ok: true
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
case "hover":
|
|
309
|
+
{
|
|
310
|
+
const {
|
|
311
|
+
selector,
|
|
312
|
+
index
|
|
313
|
+
} = getTargetArgs(args);
|
|
314
|
+
const element = resolveElement(selector, index);
|
|
315
|
+
dispatchHover(element);
|
|
316
|
+
return {
|
|
317
|
+
ok: true
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
case "type":
|
|
321
|
+
{
|
|
322
|
+
const {
|
|
323
|
+
selector,
|
|
324
|
+
index
|
|
325
|
+
} = getTargetArgs(args);
|
|
326
|
+
const element = resolveElement(selector, index);
|
|
327
|
+
const text = typeof args?.text === "string" ? args.text : "";
|
|
328
|
+
if (!text) {
|
|
329
|
+
throw new BridgeRuntimeError("invalid_args", "text is required for type");
|
|
330
|
+
}
|
|
331
|
+
const submit = typeof args?.submit === "boolean" ? args.submit : false;
|
|
332
|
+
return dispatchType(element, text, submit);
|
|
333
|
+
}
|
|
334
|
+
case "execute_js":
|
|
335
|
+
{
|
|
336
|
+
const code = typeof args?.code === "string" ? args.code : "";
|
|
337
|
+
if (!code) {
|
|
338
|
+
throw new BridgeRuntimeError("invalid_args", "code is required for execute_js");
|
|
339
|
+
}
|
|
340
|
+
const result = await runUserCode(code);
|
|
341
|
+
return {
|
|
342
|
+
type: result === null ? "null" : typeof result,
|
|
343
|
+
value: safeSerialize(result)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
case "get_console":
|
|
347
|
+
{
|
|
348
|
+
const rawLast = args?.last;
|
|
349
|
+
const last = typeof rawLast === "number" && Number.isFinite(rawLast) && rawLast > 0 ? Math.floor(rawLast) : undefined;
|
|
350
|
+
const captured = getConsoleEntries(last);
|
|
351
|
+
return {
|
|
352
|
+
count: captured.length,
|
|
353
|
+
entries: captured
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
default:
|
|
357
|
+
throw new BridgeRuntimeError("unsupported_command", `Unsupported command: ${command}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function getBridgeUrls(config) {
|
|
361
|
+
const urls = [];
|
|
362
|
+
if (!config?.bridgeUrl) {
|
|
363
|
+
urls.push(MCP_BRIDGE_DEFAULT_URL, MCP_BRIDGE_FALLBACK_URL);
|
|
364
|
+
return urls;
|
|
365
|
+
}
|
|
366
|
+
urls.push(config.bridgeUrl);
|
|
367
|
+
try {
|
|
368
|
+
const parsed = new URL(config.bridgeUrl);
|
|
369
|
+
if (parsed.hostname === "127.0.0.1") {
|
|
370
|
+
parsed.hostname = "localhost";
|
|
371
|
+
urls.push(parsed.toString());
|
|
372
|
+
} else if (parsed.hostname === "localhost") {
|
|
373
|
+
parsed.hostname = "127.0.0.1";
|
|
374
|
+
urls.push(parsed.toString());
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
// Keep the configured URL only when parsing fails.
|
|
378
|
+
}
|
|
379
|
+
return urls;
|
|
380
|
+
}
|
|
381
|
+
export class TreeLocatorMCPBridgeClient {
|
|
382
|
+
socket = null;
|
|
383
|
+
heartbeatTimer = null;
|
|
384
|
+
reconnectTimer = null;
|
|
385
|
+
reconnectAttempts = 0;
|
|
386
|
+
consecutiveFailures = 0;
|
|
387
|
+
urlIndex = 0;
|
|
388
|
+
stopped = false;
|
|
389
|
+
constructor(getApi, config, runtimeVersion = "unknown") {
|
|
390
|
+
this.getApi = getApi;
|
|
391
|
+
this.config = config || {};
|
|
392
|
+
this.sessionId = createSessionId();
|
|
393
|
+
this.runtimeVersion = runtimeVersion;
|
|
394
|
+
}
|
|
395
|
+
start() {
|
|
396
|
+
if (this.config.enabled === false) return;
|
|
397
|
+
if (typeof window === "undefined" || typeof WebSocket === "undefined") return;
|
|
398
|
+
this.connect();
|
|
399
|
+
}
|
|
400
|
+
stop() {
|
|
401
|
+
this.stopped = true;
|
|
402
|
+
this.clearTimers();
|
|
403
|
+
if (this.socket) {
|
|
404
|
+
this.socket.close();
|
|
405
|
+
this.socket = null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
connect() {
|
|
409
|
+
if (this.stopped || this.config.enabled === false || this.socket) return;
|
|
410
|
+
const urls = getBridgeUrls(this.config);
|
|
411
|
+
const url = urls[this.urlIndex % urls.length];
|
|
412
|
+
if (!url) return;
|
|
413
|
+
const socket = new WebSocket(url);
|
|
414
|
+
this.socket = socket;
|
|
415
|
+
socket.addEventListener("open", () => {
|
|
416
|
+
this.reconnectAttempts = 0;
|
|
417
|
+
this.consecutiveFailures = 0;
|
|
418
|
+
this.sendHello();
|
|
419
|
+
this.startHeartbeat();
|
|
420
|
+
});
|
|
421
|
+
socket.addEventListener("message", event => {
|
|
422
|
+
void this.handleMessage(event.data);
|
|
423
|
+
});
|
|
424
|
+
socket.addEventListener("close", () => {
|
|
425
|
+
this.socket = null;
|
|
426
|
+
this.clearHeartbeat();
|
|
427
|
+
this.scheduleReconnect();
|
|
428
|
+
});
|
|
429
|
+
socket.addEventListener("error", () => {
|
|
430
|
+
socket.close();
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
sendMessage(message) {
|
|
434
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return;
|
|
435
|
+
this.socket.send(JSON.stringify(message));
|
|
436
|
+
}
|
|
437
|
+
sendHello() {
|
|
438
|
+
const hello = {
|
|
439
|
+
type: "hello",
|
|
440
|
+
sessionId: this.sessionId,
|
|
441
|
+
url: window.location.href,
|
|
442
|
+
title: document.title || "",
|
|
443
|
+
runtimeVersion: this.runtimeVersion,
|
|
444
|
+
capabilities: ["get_path", "get_ancestry", "get_path_data", "get_styles", "get_css_rules", "get_css_report", "take_snapshot", "get_snapshot_diff", "clear_snapshot", "click", "hover", "type", "execute_js", "get_console"],
|
|
445
|
+
connectedAt: new Date().toISOString()
|
|
446
|
+
};
|
|
447
|
+
this.sendMessage(hello);
|
|
448
|
+
}
|
|
449
|
+
async handleMessage(data) {
|
|
450
|
+
const message = parseMessage(data);
|
|
451
|
+
if (!message) return;
|
|
452
|
+
if (message.type === "pong") {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const api = this.getApi();
|
|
456
|
+
if (!api) {
|
|
457
|
+
this.sendMessage({
|
|
458
|
+
type: "response",
|
|
459
|
+
id: message.id,
|
|
460
|
+
ok: false,
|
|
461
|
+
error: {
|
|
462
|
+
code: "api_unavailable",
|
|
463
|
+
message: "window.__treelocator__ is not available"
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
const result = await executeBridgeCommand(api, message.command, message.args);
|
|
470
|
+
this.sendMessage({
|
|
471
|
+
type: "response",
|
|
472
|
+
id: message.id,
|
|
473
|
+
ok: true,
|
|
474
|
+
result
|
|
475
|
+
});
|
|
476
|
+
} catch (error) {
|
|
477
|
+
const bridgeError = error instanceof BridgeRuntimeError ? error : new BridgeRuntimeError("runtime_error", error instanceof Error ? error.message : "Unknown bridge error");
|
|
478
|
+
this.sendMessage({
|
|
479
|
+
type: "response",
|
|
480
|
+
id: message.id,
|
|
481
|
+
ok: false,
|
|
482
|
+
error: {
|
|
483
|
+
code: bridgeError.code,
|
|
484
|
+
message: bridgeError.message,
|
|
485
|
+
details: bridgeError.details
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
startHeartbeat() {
|
|
491
|
+
this.clearHeartbeat();
|
|
492
|
+
this.heartbeatTimer = window.setInterval(() => {
|
|
493
|
+
this.sendMessage({
|
|
494
|
+
type: "ping",
|
|
495
|
+
timestamp: Date.now()
|
|
496
|
+
});
|
|
497
|
+
}, HEARTBEAT_MS);
|
|
498
|
+
}
|
|
499
|
+
clearHeartbeat() {
|
|
500
|
+
if (this.heartbeatTimer !== null) {
|
|
501
|
+
window.clearInterval(this.heartbeatTimer);
|
|
502
|
+
this.heartbeatTimer = null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
scheduleReconnect() {
|
|
506
|
+
if (this.stopped || this.config.enabled === false || this.reconnectTimer !== null) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const baseReconnect = this.config.reconnectMs && this.config.reconnectMs > 0 ? this.config.reconnectMs : 1_000;
|
|
510
|
+
const delay = this.consecutiveFailures >= QUIET_RETRY_AFTER_FAILURES ? QUIET_RETRY_MS : Math.min(baseReconnect * Math.pow(2, this.reconnectAttempts), MAX_RECONNECT_MS);
|
|
511
|
+
this.reconnectAttempts += 1;
|
|
512
|
+
this.consecutiveFailures += 1;
|
|
513
|
+
this.urlIndex += 1;
|
|
514
|
+
this.reconnectTimer = window.setTimeout(() => {
|
|
515
|
+
this.reconnectTimer = null;
|
|
516
|
+
this.connect();
|
|
517
|
+
}, delay);
|
|
518
|
+
}
|
|
519
|
+
clearTimers() {
|
|
520
|
+
this.clearHeartbeat();
|
|
521
|
+
if (this.reconnectTimer !== null) {
|
|
522
|
+
window.clearTimeout(this.reconnectTimer);
|
|
523
|
+
this.reconnectTimer = null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
export function startMCPBridge(config) {
|
|
528
|
+
if (config?.enabled === false) return null;
|
|
529
|
+
if (typeof window === "undefined") return null;
|
|
530
|
+
installConsoleCapture();
|
|
531
|
+
const client = new TreeLocatorMCPBridgeClient(() => window.__treelocator__, config);
|
|
532
|
+
client.start();
|
|
533
|
+
return client;
|
|
534
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|