@mseep/anything-analyzer 3.6.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,211 @@
1
+ import { contextBridge, ipcRenderer } from "electron";
2
+
3
+ // Forward JS hook messages from the target page context to the main process.
4
+ // The hook script (injected via executeJavaScript) uses window.postMessage
5
+ // to send captured data; we relay it over IPC.
6
+ window.addEventListener("message", (event) => {
7
+ if (event.data?.type === "ar-hook") {
8
+ ipcRenderer.send("capture:hook-data", event.data);
9
+ }
10
+ if (event.data?.type === "ar-interaction") {
11
+ ipcRenderer.send("capture:hook-data", event.data);
12
+ }
13
+ });
14
+
15
+ // Expose IPC APIs to renderer
16
+ contextBridge.exposeInMainWorld("electronAPI", {
17
+ // Window control (frameless window)
18
+ minimizeWindow: () => ipcRenderer.invoke("window:minimize"),
19
+ maximizeWindow: () => ipcRenderer.invoke("window:maximize"),
20
+ closeWindow: () => ipcRenderer.invoke("window:close"),
21
+ isWindowMaximized: () => ipcRenderer.invoke("window:isMaximized"),
22
+
23
+ // Session management
24
+ createSession: (name: string, targetUrl: string) =>
25
+ ipcRenderer.invoke("session:create", name, targetUrl),
26
+ listSessions: () => ipcRenderer.invoke("session:list"),
27
+ startCapture: (sessionId: string) =>
28
+ ipcRenderer.invoke("session:start", sessionId),
29
+ pauseCapture: (sessionId: string) =>
30
+ ipcRenderer.invoke("session:pause", sessionId),
31
+ resumeCapture: (sessionId: string) =>
32
+ ipcRenderer.invoke("session:resume", sessionId),
33
+ stopCapture: (sessionId: string) =>
34
+ ipcRenderer.invoke("session:stop", sessionId),
35
+ deleteSession: (sessionId: string) =>
36
+ ipcRenderer.invoke("session:delete", sessionId),
37
+
38
+ // Browser control
39
+ navigate: (url: string) => ipcRenderer.invoke("browser:navigate", url),
40
+ goBack: () => ipcRenderer.invoke("browser:back"),
41
+ goForward: () => ipcRenderer.invoke("browser:forward"),
42
+ reload: () => ipcRenderer.invoke("browser:reload"),
43
+ setBrowserRatio: (ratio: number) =>
44
+ ipcRenderer.invoke("browser:setRatio", ratio),
45
+ setTargetViewVisible: (visible: boolean) =>
46
+ ipcRenderer.invoke("browser:setVisible", visible),
47
+ toggleDevTools: () => ipcRenderer.invoke("browser:toggleDevTools"),
48
+ exportFile: (defaultName: string, content: string) =>
49
+ ipcRenderer.invoke("dialog:exportFile", defaultName, content),
50
+
51
+ // Shell
52
+ openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url),
53
+
54
+ // Tab management
55
+ createTab: (url?: string) => ipcRenderer.invoke("tabs:create", url),
56
+ closeTab: (tabId: string) => ipcRenderer.invoke("tabs:close", tabId),
57
+ activateTab: (tabId: string) => ipcRenderer.invoke("tabs:activate", tabId),
58
+ listTabs: () => ipcRenderer.invoke("tabs:list"),
59
+
60
+ // Data queries
61
+ getRequests: (sessionId: string) =>
62
+ ipcRenderer.invoke("data:requests", sessionId),
63
+ getHooks: (sessionId: string) => ipcRenderer.invoke("data:hooks", sessionId),
64
+ getStorage: (sessionId: string) =>
65
+ ipcRenderer.invoke("data:storage", sessionId),
66
+ getReports: (sessionId: string) =>
67
+ ipcRenderer.invoke("data:reports", sessionId),
68
+ clearCaptureData: (sessionId: string) =>
69
+ ipcRenderer.invoke("data:clear", sessionId),
70
+
71
+ // AI analysis
72
+ startAnalysis: (sessionId: string, purpose?: string, selectedSeqs?: number[]) =>
73
+ ipcRenderer.invoke("ai:analyze", sessionId, purpose, selectedSeqs),
74
+ cancelAnalysis: (sessionId: string) =>
75
+ ipcRenderer.invoke("ai:cancel", sessionId),
76
+ sendFollowUp: (sessionId: string, reportId: string, history: unknown[], userMessage: string) =>
77
+ ipcRenderer.invoke("ai:chat", sessionId, reportId, history, userMessage),
78
+ getChatMessages: (reportId: string) =>
79
+ ipcRenderer.invoke("data:chatMessages", reportId),
80
+ saveChatMessages: (reportId: string, messages: unknown[]) =>
81
+ ipcRenderer.invoke("data:saveChatMessages", reportId, messages),
82
+
83
+ // Browser bounds sync (renderer → main, fire-and-forget)
84
+ syncBrowserBounds: (bounds: {
85
+ x: number;
86
+ y: number;
87
+ width: number;
88
+ height: number;
89
+ }) => ipcRenderer.send("browser:syncBounds", bounds),
90
+
91
+ // Settings
92
+ getLLMConfig: () => ipcRenderer.invoke("settings:getLLM"),
93
+ saveLLMConfig: (config: unknown) =>
94
+ ipcRenderer.invoke("settings:saveLLM", config),
95
+
96
+ // Auto update
97
+ getAppVersion: () => ipcRenderer.invoke("app:version"),
98
+ checkForUpdate: () => ipcRenderer.invoke("update:check"),
99
+ installUpdate: () => ipcRenderer.send("update:install"),
100
+ onUpdateStatus: (callback: (status: unknown) => void) => {
101
+ ipcRenderer.on("update:status", (_event, status) => callback(status));
102
+ },
103
+
104
+ // Prompt Templates
105
+ getPromptTemplates: () => ipcRenderer.invoke("templates:list"),
106
+ savePromptTemplate: (template: unknown) =>
107
+ ipcRenderer.invoke("templates:save", template),
108
+ deletePromptTemplate: (id: string) =>
109
+ ipcRenderer.invoke("templates:delete", id),
110
+ resetPromptTemplate: (id: string) =>
111
+ ipcRenderer.invoke("templates:reset", id),
112
+
113
+ // MCP Servers
114
+ getMCPServers: () => ipcRenderer.invoke("mcp:list"),
115
+ saveMCPServer: (server: unknown) => ipcRenderer.invoke("mcp:save", server),
116
+ deleteMCPServer: (id: string) => ipcRenderer.invoke("mcp:delete", id),
117
+
118
+ // Export requests
119
+ exportRequests: (sessionId: string) =>
120
+ ipcRenderer.invoke("data:exportRequests", sessionId),
121
+
122
+ // AI Request Logs
123
+ getAiRequestLogs: (sessionId: string) => ipcRenderer.invoke("data:aiRequestLogs", sessionId),
124
+ getAiRequestLogsAll: (limit: number, offset: number) => ipcRenderer.invoke("data:aiRequestLogsAll", limit, offset),
125
+ getAiRequestLogDetail: (id: number) => ipcRenderer.invoke("data:aiRequestLogDetail", id),
126
+
127
+ // Proxy
128
+ getProxyConfig: () => ipcRenderer.invoke("proxy:get"),
129
+ saveProxyConfig: (config: unknown) =>
130
+ ipcRenderer.invoke("proxy:save", config),
131
+
132
+ // Browser environment
133
+ clearBrowserEnv: () => ipcRenderer.invoke("browser:clearEnv"),
134
+
135
+ // MCP Server
136
+ getMCPServerConfig: () => ipcRenderer.invoke("mcp-server:getConfig"),
137
+ saveMCPServerConfig: (config: unknown) =>
138
+ ipcRenderer.invoke("mcp-server:saveConfig", config),
139
+ getMCPServerStatus: () => ipcRenderer.invoke("mcp-server:status"),
140
+
141
+ // MITM Proxy
142
+ getMitmProxyConfig: () => ipcRenderer.invoke("mitm-proxy:getConfig"),
143
+ saveMitmProxyConfig: (config: unknown) => ipcRenderer.invoke("mitm-proxy:saveConfig", config),
144
+ getMitmProxyStatus: () => ipcRenderer.invoke("mitm-proxy:status"),
145
+ installMitmCA: () => ipcRenderer.invoke("mitm-proxy:installCA"),
146
+ uninstallMitmCA: () => ipcRenderer.invoke("mitm-proxy:uninstallCA"),
147
+ exportMitmCA: () => ipcRenderer.invoke("mitm-proxy:exportCA"),
148
+ regenerateMitmCA: () => ipcRenderer.invoke("mitm-proxy:regenerateCA"),
149
+ enableMitmSystemProxy: () => ipcRenderer.invoke("mitm-proxy:enableSystemProxy"),
150
+ disableMitmSystemProxy: () => ipcRenderer.invoke("mitm-proxy:disableSystemProxy"),
151
+
152
+ // Interaction Recording
153
+ getInteractions: (sessionId: string, limit?: number) =>
154
+ ipcRenderer.invoke("interaction:getEvents", sessionId, limit),
155
+ getInteractionCount: (sessionId: string) =>
156
+ ipcRenderer.invoke("interaction:getCount", sessionId),
157
+ clearInteractions: (sessionId: string) =>
158
+ ipcRenderer.invoke("interaction:clear", sessionId),
159
+ onInteractionRecorded: (callback: (data: unknown) => void) => {
160
+ ipcRenderer.on("interaction:recorded", (_event, data) => callback(data));
161
+ },
162
+
163
+ // Fingerprint
164
+ getFingerprintProfile: (sessionId: string) =>
165
+ ipcRenderer.invoke("fingerprint:get", sessionId),
166
+ updateFingerprintProfile: (profile: unknown) =>
167
+ ipcRenderer.invoke("fingerprint:update", JSON.stringify(profile)),
168
+ regenerateFingerprintProfile: (sessionId: string) =>
169
+ ipcRenderer.invoke("fingerprint:regenerate", sessionId),
170
+ enableFingerprint: (sessionId: string) =>
171
+ ipcRenderer.invoke("fingerprint:enable", sessionId),
172
+ disableFingerprint: () =>
173
+ ipcRenderer.invoke("fingerprint:disable"),
174
+
175
+ // Tab events
176
+ onTabCreated: (callback: (tab: unknown) => void) => {
177
+ ipcRenderer.on("tabs:created", (_event, data) => callback(data));
178
+ },
179
+ onTabClosed: (callback: (data: unknown) => void) => {
180
+ ipcRenderer.on("tabs:closed", (_event, data) => callback(data));
181
+ },
182
+ onTabActivated: (callback: (data: unknown) => void) => {
183
+ ipcRenderer.on("tabs:activated", (_event, data) => callback(data));
184
+ },
185
+ onTabUpdated: (callback: (data: unknown) => void) => {
186
+ ipcRenderer.on("tabs:updated", (_event, data) => callback(data));
187
+ },
188
+
189
+ // Events from main process
190
+ onRequestCaptured: (callback: (data: unknown) => void) => {
191
+ ipcRenderer.on("capture:request", (_event, data) => callback(data));
192
+ },
193
+ onHookCaptured: (callback: (data: unknown) => void) => {
194
+ ipcRenderer.on("capture:hook", (_event, data) => callback(data));
195
+ },
196
+ onStorageCaptured: (callback: (data: unknown) => void) => {
197
+ ipcRenderer.on("capture:storage", (_event, data) => callback(data));
198
+ },
199
+ onAnalysisProgress: (callback: (chunk: string) => void) => {
200
+ ipcRenderer.on("ai:progress", (_event, chunk) => callback(chunk));
201
+ },
202
+
203
+ // Log files
204
+ getLogPath: () => ipcRenderer.invoke("log:getPath"),
205
+ openLogFolder: () => ipcRenderer.invoke("log:openFolder"),
206
+ exportLogs: () => ipcRenderer.invoke("log:export"),
207
+
208
+ removeAllListeners: (channel: string) => {
209
+ ipcRenderer.removeAllListeners(channel);
210
+ },
211
+ });
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Interaction recording hook script injected into the target browser page context.
3
+ * Records clicks, inputs, scrolls, and mouse movement for AI automation replay.
4
+ */
5
+ ;(function () {
6
+ const MSG_TYPE = 'ar-interaction'
7
+ let isRecording = false
8
+
9
+ // Mouse move sampling config
10
+ const MOVE_SAMPLE_INTERVAL = 50 // ms between samples
11
+ const MOVE_FLUSH_INTERVAL = 2000 // ms, flush buffer every 2s
12
+ const IDLE_THRESHOLD = 500 // ms, end trace segment on idle
13
+ const MIN_MOVE_DISTANCE = 5 // px, ignore micro-movements
14
+
15
+ let moveBuffer: Array<{ x: number; y: number; t: number }> = []
16
+ let lastMoveTime = 0
17
+ let lastMoveX = 0
18
+ let lastMoveY = 0
19
+ let moveFlushTimer: ReturnType<typeof setTimeout> | null = null
20
+
21
+ // Input debounce
22
+ const inputTimers = new WeakMap<Element, ReturnType<typeof setTimeout>>()
23
+
24
+ // Control: main process toggles recording via executeJavaScript
25
+ window.addEventListener('message', (e) => {
26
+ if (e.data?.type === 'ar-interaction-control') {
27
+ isRecording = e.data.recording
28
+ if (!isRecording) {
29
+ flushMoveBuffer()
30
+ }
31
+ }
32
+ })
33
+
34
+ function send(data: Record<string, unknown>): void {
35
+ try {
36
+ window.postMessage({ type: MSG_TYPE, ...data }, '*')
37
+ } catch { /* ignore serialization errors */ }
38
+ }
39
+
40
+ // ---- Selector Generation ----
41
+
42
+ function isDynamicId(id: string): boolean {
43
+ return /[0-9a-f]{8,}|_\d+$|^:r\d+:|^ember\d+|^react-|^mui-/.test(id)
44
+ }
45
+
46
+ function getSelector(el: Element): string {
47
+ // Priority 1: stable id
48
+ if (el.id && !isDynamicId(el.id)) {
49
+ return `#${CSS.escape(el.id)}`
50
+ }
51
+ // Priority 2: test attributes
52
+ for (const attr of ['data-testid', 'data-cy', 'data-test', 'aria-label', 'data-id']) {
53
+ const val = el.getAttribute(attr)
54
+ if (val) {
55
+ return `[${attr}="${CSS.escape(val)}"]`
56
+ }
57
+ }
58
+ // Priority 3: unique tag + class combination
59
+ const tag = el.tagName.toLowerCase()
60
+ if (el.className && typeof el.className === 'string') {
61
+ const classes = el.className.trim().split(/\s+/).filter(c => !isDynamicId(c)).slice(0, 3)
62
+ if (classes.length > 0) {
63
+ const sel = `${tag}.${classes.map(c => CSS.escape(c)).join('.')}`
64
+ if (document.querySelectorAll(sel).length === 1) {
65
+ return sel
66
+ }
67
+ }
68
+ }
69
+ // Priority 4: nth-child path
70
+ return buildNthChildPath(el)
71
+ }
72
+
73
+ function buildNthChildPath(el: Element): string {
74
+ const parts: string[] = []
75
+ let current: Element | null = el
76
+ while (current && current !== document.documentElement) {
77
+ const tag = current.tagName.toLowerCase()
78
+ const parent = current.parentElement
79
+ if (!parent) {
80
+ parts.unshift(tag)
81
+ break
82
+ }
83
+ const sameTagSiblings = Array.from(parent.children).filter(c => c.tagName === current!.tagName)
84
+ if (sameTagSiblings.length === 1) {
85
+ parts.unshift(tag)
86
+ } else {
87
+ const index = sameTagSiblings.indexOf(current) + 1
88
+ parts.unshift(`${tag}:nth-of-type(${index})`)
89
+ }
90
+ current = parent
91
+ if (parts.length >= 5) break // limit depth
92
+ }
93
+ return parts.join(' > ')
94
+ }
95
+
96
+ function getXPath(el: Element): string {
97
+ const parts: string[] = []
98
+ let current: Element | null = el
99
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
100
+ let index = 1
101
+ let sibling: Element | null = current.previousElementSibling
102
+ while (sibling) {
103
+ if (sibling.tagName === current.tagName) index++
104
+ sibling = sibling.previousElementSibling
105
+ }
106
+ const tag = current.tagName.toLowerCase()
107
+ parts.unshift(`${tag}[${index}]`)
108
+ current = current.parentElement
109
+ if (parts.length >= 7) break
110
+ }
111
+ return '//' + parts.join('/')
112
+ }
113
+
114
+ function getAttributes(el: Element): Record<string, string> {
115
+ const attrs: Record<string, string> = {}
116
+ const keys = ['id', 'class', 'name', 'type', 'href', 'src', 'placeholder', 'role', 'aria-label',
117
+ 'data-testid', 'data-id', 'data-action', 'value', 'title', 'alt']
118
+ for (const key of keys) {
119
+ const val = el.getAttribute(key)
120
+ if (val) attrs[key] = val.slice(0, 200) // truncate long values
121
+ }
122
+ return attrs
123
+ }
124
+
125
+ // ---- Event Handlers ----
126
+
127
+ // Click
128
+ document.addEventListener('click', (e: MouseEvent) => {
129
+ if (!isRecording) return
130
+ const el = e.target as Element
131
+ if (!el) return
132
+ send({
133
+ interactionType: 'click',
134
+ timestamp: Date.now(),
135
+ x: e.pageX,
136
+ y: e.pageY,
137
+ viewportX: e.clientX,
138
+ viewportY: e.clientY,
139
+ selector: getSelector(el),
140
+ xpath: getXPath(el),
141
+ tagName: el.tagName.toLowerCase(),
142
+ elementText: (el.textContent || '').trim().slice(0, 100),
143
+ attributes: getAttributes(el),
144
+ boundingRect: el.getBoundingClientRect().toJSON(),
145
+ url: location.href,
146
+ pageTitle: document.title,
147
+ })
148
+ }, true)
149
+
150
+ // Double click
151
+ document.addEventListener('dblclick', (e: MouseEvent) => {
152
+ if (!isRecording) return
153
+ const el = e.target as Element
154
+ if (!el) return
155
+ send({
156
+ interactionType: 'dblclick',
157
+ timestamp: Date.now(),
158
+ x: e.pageX,
159
+ y: e.pageY,
160
+ viewportX: e.clientX,
161
+ viewportY: e.clientY,
162
+ selector: getSelector(el),
163
+ xpath: getXPath(el),
164
+ tagName: el.tagName.toLowerCase(),
165
+ elementText: (el.textContent || '').trim().slice(0, 100),
166
+ attributes: getAttributes(el),
167
+ boundingRect: el.getBoundingClientRect().toJSON(),
168
+ url: location.href,
169
+ pageTitle: document.title,
170
+ })
171
+ }, true)
172
+
173
+ // Input (debounced — record final value after 500ms idle)
174
+ document.addEventListener('input', (e: Event) => {
175
+ if (!isRecording) return
176
+ const el = e.target as HTMLInputElement | HTMLTextAreaElement
177
+ if (!el || !('value' in el)) return
178
+
179
+ // Clear previous timer for this element
180
+ const prev = inputTimers.get(el)
181
+ if (prev) clearTimeout(prev)
182
+
183
+ const timer = setTimeout(() => {
184
+ const isSensitive = el.type === 'password' || el.getAttribute('autocomplete')?.includes('password')
185
+ send({
186
+ interactionType: 'input',
187
+ timestamp: Date.now(),
188
+ selector: getSelector(el),
189
+ xpath: getXPath(el),
190
+ tagName: el.tagName.toLowerCase(),
191
+ elementText: null,
192
+ attributes: getAttributes(el),
193
+ boundingRect: el.getBoundingClientRect().toJSON(),
194
+ inputValue: isSensitive ? '[MASKED]' : el.value,
195
+ url: location.href,
196
+ pageTitle: document.title,
197
+ })
198
+ inputTimers.delete(el)
199
+ }, 500)
200
+ inputTimers.set(el, timer)
201
+ }, true)
202
+
203
+ // Scroll (throttled to max once per 200ms)
204
+ let lastScrollTime = 0
205
+ let scrollTimer: ReturnType<typeof setTimeout> | null = null
206
+ document.addEventListener('scroll', () => {
207
+ if (!isRecording) return
208
+ const now = Date.now()
209
+ if (now - lastScrollTime < 200) {
210
+ // Queue final position
211
+ if (scrollTimer) clearTimeout(scrollTimer)
212
+ scrollTimer = setTimeout(() => {
213
+ emitScroll()
214
+ scrollTimer = null
215
+ }, 250)
216
+ return
217
+ }
218
+ lastScrollTime = now
219
+ emitScroll()
220
+ }, true)
221
+
222
+ function emitScroll(): void {
223
+ send({
224
+ interactionType: 'scroll',
225
+ timestamp: Date.now(),
226
+ scrollX: window.scrollX,
227
+ scrollY: window.scrollY,
228
+ viewportX: window.innerWidth / 2,
229
+ viewportY: window.innerHeight / 2,
230
+ url: location.href,
231
+ pageTitle: document.title,
232
+ })
233
+ }
234
+
235
+ // Mouse move (sampled)
236
+ document.addEventListener('mousemove', (e: MouseEvent) => {
237
+ if (!isRecording) return
238
+ const now = Date.now()
239
+
240
+ // Check idle threshold — flush if mouse was idle
241
+ if (now - lastMoveTime > IDLE_THRESHOLD && moveBuffer.length > 0) {
242
+ flushMoveBuffer()
243
+ }
244
+
245
+ // Time-based sampling
246
+ if (now - lastMoveTime < MOVE_SAMPLE_INTERVAL) return
247
+
248
+ // Distance filter
249
+ const dx = e.clientX - lastMoveX
250
+ const dy = e.clientY - lastMoveY
251
+ if (Math.sqrt(dx * dx + dy * dy) < MIN_MOVE_DISTANCE) return
252
+
253
+ lastMoveTime = now
254
+ lastMoveX = e.clientX
255
+ lastMoveY = e.clientY
256
+
257
+ moveBuffer.push({ x: e.clientX, y: e.clientY, t: now })
258
+
259
+ // Start flush timer if not already running
260
+ if (!moveFlushTimer) {
261
+ moveFlushTimer = setTimeout(() => {
262
+ flushMoveBuffer()
263
+ moveFlushTimer = null
264
+ }, MOVE_FLUSH_INTERVAL)
265
+ }
266
+ }, true)
267
+
268
+ function flushMoveBuffer(): void {
269
+ if (moveBuffer.length < 3) {
270
+ moveBuffer = []
271
+ return // ignore very short movements
272
+ }
273
+ send({
274
+ interactionType: 'hover',
275
+ timestamp: moveBuffer[0].t,
276
+ path: moveBuffer,
277
+ url: location.href,
278
+ pageTitle: document.title,
279
+ })
280
+ moveBuffer = []
281
+ if (moveFlushTimer) {
282
+ clearTimeout(moveFlushTimer)
283
+ moveFlushTimer = null
284
+ }
285
+ }
286
+ })()