@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,130 @@
1
+ import { EventEmitter } from 'events'
2
+ import type { WebContents } from 'electron'
3
+ import type { CapturedRequest, JsHookRecord, StorageSnapshot } from '@shared/types'
4
+ import type { RequestsRepo, JsHooksRepo, StorageSnapshotsRepo } from '../db/repositories'
5
+
6
+ /**
7
+ * CaptureEngine — Aggregates data from CDP, JS hooks, and storage collectors,
8
+ * writes structured data to SQLite, and emits IPC events to the renderer.
9
+ */
10
+ export class CaptureEngine extends EventEmitter {
11
+ private sessionId: string | null = null
12
+ private rendererWebContents: WebContents | null = null
13
+
14
+ constructor(
15
+ private requestsRepo: RequestsRepo,
16
+ private jsHooksRepo: JsHooksRepo,
17
+ private storageSnapshotsRepo: StorageSnapshotsRepo
18
+ ) {
19
+ super()
20
+ }
21
+
22
+ start(sessionId: string, rendererWebContents: WebContents): void {
23
+ this.sessionId = sessionId
24
+ this.rendererWebContents = rendererWebContents
25
+ }
26
+
27
+ stop(): void {
28
+ this.sessionId = null
29
+ this.rendererWebContents = null
30
+ }
31
+
32
+ handleResponseCaptured(data: {
33
+ requestId: string; method: string; url: string;
34
+ requestHeaders: string; requestBody: string | null;
35
+ statusCode: number; responseHeaders: string;
36
+ responseBody: string | null; contentType: string | null;
37
+ initiator: string | null; durationMs: number | null;
38
+ isOptions: boolean; isStatic: boolean; isStreaming: boolean; isWebSocket: boolean; truncated: boolean; timestamp: number;
39
+ source?: 'cdp' | 'proxy'
40
+ }): void {
41
+ if (!this.sessionId) return
42
+
43
+ let sequence: number
44
+ // Guard quit race: DB may already be closing during app shutdown.
45
+ try {
46
+ sequence = this.requestsRepo.getNextSequence(this.sessionId)
47
+ } catch (err) {
48
+ console.warn('[CaptureEngine] getNextSequence failed:', (err as Error).message)
49
+ return
50
+ }
51
+
52
+ // Generate a unique ID per record to avoid UNIQUE constraint conflicts.
53
+ // The original requestId from CDP/proxy may repeat across sessions or retries.
54
+ const uniqueId = `${this.sessionId}-${sequence}`
55
+
56
+ try {
57
+ this.requestsRepo.insert({
58
+ id: uniqueId, session_id: this.sessionId, sequence,
59
+ timestamp: data.timestamp, method: data.method, url: data.url,
60
+ request_headers: data.requestHeaders, request_body: data.requestBody,
61
+ content_type: data.contentType, initiator: data.initiator,
62
+ source: data.source || 'cdp'
63
+ })
64
+ } catch (err) {
65
+ console.warn('[CaptureEngine] Insert failed:', (err as Error).message)
66
+ }
67
+
68
+ try {
69
+ this.requestsRepo.updateResponse({
70
+ id: uniqueId, status_code: data.statusCode,
71
+ response_headers: data.responseHeaders,
72
+ response_body: data.responseBody,
73
+ content_type: data.contentType, duration_ms: data.durationMs || 0,
74
+ is_streaming: data.isStreaming ? 1 : 0,
75
+ is_websocket: data.isWebSocket ? 1 : 0
76
+ })
77
+ } catch { /* ignore */ }
78
+
79
+ const captured: CapturedRequest = {
80
+ id: uniqueId, session_id: this.sessionId, sequence,
81
+ timestamp: data.timestamp, method: data.method, url: data.url,
82
+ request_headers: data.requestHeaders, request_body: data.requestBody,
83
+ status_code: data.statusCode, response_headers: data.responseHeaders,
84
+ response_body: data.responseBody, content_type: data.contentType,
85
+ initiator: data.initiator, duration_ms: data.durationMs,
86
+ is_streaming: data.isStreaming, is_websocket: data.isWebSocket,
87
+ source: data.source || 'cdp'
88
+ }
89
+ this.sendToRenderer('capture:request', captured)
90
+ }
91
+
92
+ handleHookCaptured(data: {
93
+ hookType: string; functionName: string; arguments: string;
94
+ result: string | null; callStack: string | null; timestamp: number
95
+ }): void {
96
+ if (!this.sessionId) return
97
+
98
+ const record: Omit<JsHookRecord, 'id'> = {
99
+ session_id: this.sessionId, timestamp: data.timestamp,
100
+ hook_type: data.hookType as JsHookRecord['hook_type'],
101
+ function_name: data.functionName, arguments: data.arguments,
102
+ result: data.result, call_stack: data.callStack, related_request_id: null
103
+ }
104
+
105
+ try { this.jsHooksRepo.insert(record) } catch { /* ignore */ }
106
+ this.sendToRenderer('capture:hook', record)
107
+ }
108
+
109
+ handleStorageCollected(data: {
110
+ domain: string; storageType: string; data: string; timestamp: number
111
+ }): void {
112
+ if (!this.sessionId) return
113
+
114
+ const snapshot: Omit<StorageSnapshot, 'id'> = {
115
+ session_id: this.sessionId, timestamp: data.timestamp,
116
+ domain: data.domain,
117
+ storage_type: data.storageType as StorageSnapshot['storage_type'],
118
+ data: data.data
119
+ }
120
+
121
+ try { this.storageSnapshotsRepo.insert(snapshot) } catch { /* ignore */ }
122
+ this.sendToRenderer('capture:storage', snapshot)
123
+ }
124
+
125
+ private sendToRenderer(channel: string, data: unknown): void {
126
+ if (this.rendererWebContents && !this.rendererWebContents.isDestroyed()) {
127
+ this.rendererWebContents.send(channel, data)
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,171 @@
1
+ import { EventEmitter } from 'events'
2
+ import type { WebContents } from 'electron'
3
+ import type { InteractionEventsRepo } from '../db/repositories'
4
+ import type { InteractionEvent, RawInteractionData } from '@shared/types'
5
+ import { readFileSync } from 'fs'
6
+ import { join } from 'path'
7
+
8
+ /**
9
+ * InteractionRecorder — Records user interactions (clicks, inputs, scrolls, mouse movements)
10
+ * from the target browser and persists them to SQLite.
11
+ */
12
+ export class InteractionRecorder extends EventEmitter {
13
+ private sessionId: string | null = null
14
+ private rendererWebContents: WebContents | null = null
15
+ private recording = false
16
+ private scriptContent: string | null = null
17
+ private injectedWebContents: Set<WebContents> = new Set()
18
+
19
+ constructor(private repo: InteractionEventsRepo) {
20
+ super()
21
+ }
22
+
23
+ /** Start recording interactions (injection is handled separately via injectIntoWebContents) */
24
+ start(sessionId: string, rendererWebContents: WebContents): void {
25
+ this.sessionId = sessionId
26
+ this.rendererWebContents = rendererWebContents
27
+ this.recording = true
28
+ this.loadScript()
29
+ }
30
+
31
+ /**
32
+ * Inject interaction-hook script into a WebContents (for multi-tab support).
33
+ * Called by SessionManager when tabs are attached to the capture pipeline.
34
+ */
35
+ injectIntoWebContents(webContents: WebContents): void {
36
+ this.loadScript()
37
+ if (!this.scriptContent) return
38
+ if (webContents.isDestroyed()) return
39
+ webContents.executeJavaScript(this.scriptContent, true).catch(() => { /* page not ready or destroyed */ })
40
+
41
+ this.injectedWebContents.add(webContents)
42
+ webContents.once('destroyed', () => {
43
+ this.injectedWebContents.delete(webContents)
44
+ })
45
+
46
+ // Set recording state
47
+ if (this.recording) {
48
+ webContents.executeJavaScript(
49
+ `window.postMessage({type:'ar-interaction-control',recording:true},'*')`,
50
+ true
51
+ ).catch(() => {})
52
+ }
53
+
54
+ // Re-inject on navigation for this tab
55
+ const handler = () => {
56
+ if (!this.recording || webContents.isDestroyed()) return
57
+ webContents.executeJavaScript(this.scriptContent!, true).catch(() => {})
58
+ webContents.executeJavaScript(
59
+ `window.postMessage({type:'ar-interaction-control',recording:true},'*')`,
60
+ true
61
+ ).catch(() => {})
62
+ }
63
+ webContents.on('did-navigate', handler)
64
+ webContents.on('did-navigate-in-page', handler)
65
+ }
66
+
67
+ /** Pause recording (keeps session active) */
68
+ pause(): void {
69
+ this.recording = false
70
+ this.setRecordingState(false)
71
+ }
72
+
73
+ /** Resume recording */
74
+ resume(): void {
75
+ this.recording = true
76
+ this.setRecordingState(true)
77
+ }
78
+
79
+ /** Stop recording and clean up */
80
+ stop(): void {
81
+ this.recording = false
82
+ this.sessionId = null
83
+ this.rendererWebContents = null
84
+ this.injectedWebContents.clear()
85
+ }
86
+
87
+ /** Process interaction data from page injection script */
88
+ handleInteraction(data: RawInteractionData): void {
89
+ if (!this.recording || !this.sessionId) return
90
+
91
+ let sequence: number
92
+ try {
93
+ sequence = this.repo.getNextSequence(this.sessionId)
94
+ } catch (err) {
95
+ console.warn('[InteractionRecorder] getNextSequence failed:', (err as Error).message)
96
+ return
97
+ }
98
+
99
+ const event: Omit<InteractionEvent, 'id'> = {
100
+ session_id: this.sessionId,
101
+ sequence,
102
+ type: data.type,
103
+ timestamp: data.timestamp,
104
+ x: data.x ?? null,
105
+ y: data.y ?? null,
106
+ viewport_x: data.viewportX ?? null,
107
+ viewport_y: data.viewportY ?? null,
108
+ selector: data.selector ?? null,
109
+ xpath: data.xpath ?? null,
110
+ tag_name: data.tagName ?? null,
111
+ element_text: data.elementText ?? null,
112
+ attributes: data.attributes ? JSON.stringify(data.attributes) : null,
113
+ bounding_rect: data.boundingRect ? JSON.stringify(data.boundingRect) : null,
114
+ input_value: data.inputValue ?? null,
115
+ key: data.key ?? null,
116
+ scroll_x: data.scrollX ?? null,
117
+ scroll_y: data.scrollY ?? null,
118
+ scroll_dx: data.scrollDX ?? null,
119
+ scroll_dy: data.scrollDY ?? null,
120
+ url: data.url,
121
+ page_title: data.pageTitle ?? null,
122
+ path: data.path ? JSON.stringify(data.path) : null,
123
+ created_at: Date.now(),
124
+ }
125
+
126
+ try {
127
+ this.repo.insert(event)
128
+ } catch (err) {
129
+ console.warn('[InteractionRecorder] Insert failed:', (err as Error).message)
130
+ return
131
+ }
132
+
133
+ // Notify renderer (lightweight event, no heavy data)
134
+ if (this.rendererWebContents && !this.rendererWebContents.isDestroyed()) {
135
+ this.rendererWebContents.send('interaction:recorded', {
136
+ type: data.type,
137
+ sequence,
138
+ timestamp: data.timestamp,
139
+ })
140
+ }
141
+ this.emit('interaction', event)
142
+ }
143
+
144
+ isRecording(): boolean {
145
+ return this.recording
146
+ }
147
+
148
+ getSessionId(): string | null {
149
+ return this.sessionId
150
+ }
151
+
152
+ private loadScript(): void {
153
+ if (this.scriptContent) return
154
+ try {
155
+ const scriptPath = join(__dirname, '../preload/interaction-hook.js')
156
+ this.scriptContent = readFileSync(scriptPath, 'utf-8')
157
+ } catch {
158
+ this.scriptContent = `console.log('[AnythingAnalyzer] Interaction hook script not found')`
159
+ }
160
+ }
161
+
162
+ private setRecordingState(recording: boolean): void {
163
+ for (const wc of this.injectedWebContents) {
164
+ if (wc.isDestroyed()) continue
165
+ wc.executeJavaScript(
166
+ `window.postMessage({type:'ar-interaction-control',recording:${recording}},'*')`,
167
+ true
168
+ ).catch(() => {})
169
+ }
170
+ }
171
+ }
@@ -0,0 +1,57 @@
1
+ import { EventEmitter } from "events";
2
+ import type { WebContents } from "electron";
3
+ import { readFileSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ /**
7
+ * JsInjector — Manages hook script injection into a target browser WebContents.
8
+ * Only handles script injection and re-injection on navigation.
9
+ * IPC hook data listening is handled externally (SessionManager).
10
+ */
11
+ export class JsInjector extends EventEmitter {
12
+ private webContents: WebContents | null = null;
13
+ private hookScriptContent: string | null = null;
14
+ private navigationHandler: (() => void) | null = null;
15
+
16
+ start(webContents: WebContents): void {
17
+ this.webContents = webContents;
18
+ this.loadHookScript();
19
+ this.injectHooks();
20
+
21
+ this.navigationHandler = () => {
22
+ this.injectHooks();
23
+ };
24
+ webContents.on("did-navigate", this.navigationHandler);
25
+ webContents.on("did-navigate-in-page", this.navigationHandler);
26
+ }
27
+
28
+ stop(): void {
29
+ if (this.webContents && this.navigationHandler) {
30
+ this.webContents.removeListener("did-navigate", this.navigationHandler);
31
+ this.webContents.removeListener(
32
+ "did-navigate-in-page",
33
+ this.navigationHandler,
34
+ );
35
+ }
36
+ this.webContents = null;
37
+ this.navigationHandler = null;
38
+ }
39
+
40
+ private loadHookScript(): void {
41
+ if (this.hookScriptContent) return;
42
+ try {
43
+ const scriptPath = join(__dirname, "../preload/hook-script.js");
44
+ this.hookScriptContent = readFileSync(scriptPath, "utf-8");
45
+ } catch {
46
+ this.hookScriptContent = `console.log('[AnythingAnalyzer] Hook script not found')`;
47
+ }
48
+ }
49
+
50
+ private injectHooks(): void {
51
+ if (!this.webContents || !this.hookScriptContent) return;
52
+ if (this.webContents.isDestroyed()) return;
53
+ this.webContents.executeJavaScript(this.hookScriptContent, true).catch(() => {
54
+ /* not ready or destroyed */
55
+ });
56
+ }
57
+ }
@@ -0,0 +1,256 @@
1
+ import type { WebContents } from 'electron'
2
+ import type { InteractionEvent } from '@shared/types'
3
+
4
+ interface ReplayOptions {
5
+ speed: number // Playback speed multiplier (1.0 = original)
6
+ skipMoves: boolean // Whether to skip hover/move events
7
+ }
8
+
9
+ /**
10
+ * ReplayEngine — Replays recorded interaction events via CDP Input domain.
11
+ */
12
+ export class ReplayEngine {
13
+ private aborted = false
14
+
15
+ /** Send a CDP command, checking for destroyed WebContents first. */
16
+ private async cdp(
17
+ wc: WebContents,
18
+ method: string,
19
+ params: Record<string, unknown> = {}
20
+ ): Promise<Record<string, unknown>> {
21
+ if (wc.isDestroyed()) throw new Error('WebContents destroyed during replay')
22
+ return wc.debugger.sendCommand(method, params) as Promise<Record<string, unknown>>
23
+ }
24
+
25
+ /** Safely load a URL, checking for destroyed WebContents first. */
26
+ private async safeLoadURL(wc: WebContents, url: string): Promise<void> {
27
+ if (wc.isDestroyed()) throw new Error('WebContents destroyed during replay')
28
+ await wc.loadURL(url)
29
+ }
30
+
31
+ async replay(
32
+ webContents: WebContents,
33
+ events: InteractionEvent[],
34
+ options: ReplayOptions = { speed: 1, skipMoves: false }
35
+ ): Promise<{ success: boolean; stepsCompleted: number; error?: string }> {
36
+ this.aborted = false
37
+ let completed = 0
38
+
39
+ if (webContents.isDestroyed()) {
40
+ return { success: false, stepsCompleted: 0, error: 'WebContents destroyed' }
41
+ }
42
+
43
+ if (!webContents.debugger.isAttached()) {
44
+ try {
45
+ webContents.debugger.attach('1.3')
46
+ } catch (err) {
47
+ return { success: false, stepsCompleted: 0, error: `Failed to attach debugger: ${(err as Error).message}` }
48
+ }
49
+ }
50
+
51
+ try {
52
+ for (let i = 0; i < events.length; i++) {
53
+ if (this.aborted || webContents.isDestroyed()) break
54
+ const event = events[i]
55
+
56
+ if (options.skipMoves && event.type === 'hover') {
57
+ continue
58
+ }
59
+
60
+ await this.executeStep(webContents, event)
61
+ completed++
62
+
63
+ // Wait between events based on original timing
64
+ const nextEvent = events[i + 1]
65
+ if (nextEvent) {
66
+ const delay = (nextEvent.timestamp - event.timestamp) / options.speed
67
+ await this.wait(Math.min(Math.max(delay, 10), 3000))
68
+ }
69
+ }
70
+ } catch (err) {
71
+ return { success: false, stepsCompleted: completed, error: (err as Error).message }
72
+ }
73
+
74
+ return { success: !this.aborted, stepsCompleted: completed }
75
+ }
76
+
77
+ abort(): void {
78
+ this.aborted = true
79
+ }
80
+
81
+ /** Execute a single browser action (for MCP execute_browser_action tool) */
82
+ async executeAction(
83
+ webContents: WebContents,
84
+ action: { type: string; selector?: string; text?: string; url?: string; x?: number; y?: number; scrollDelta?: number }
85
+ ): Promise<{ success: boolean; error?: string }> {
86
+ if (webContents.isDestroyed()) {
87
+ return { success: false, error: 'WebContents destroyed' }
88
+ }
89
+
90
+ if (!webContents.debugger.isAttached()) {
91
+ try {
92
+ webContents.debugger.attach('1.3')
93
+ } catch (err) {
94
+ return { success: false, error: `Failed to attach debugger: ${(err as Error).message}` }
95
+ }
96
+ }
97
+
98
+ try {
99
+ switch (action.type) {
100
+ case 'click': {
101
+ if (action.selector) {
102
+ const coords = await this.resolveElementCenter(webContents, action.selector)
103
+ if (!coords) return { success: false, error: `Element not found: ${action.selector}` }
104
+ await this.clickAt(webContents, coords.x, coords.y)
105
+ } else if (action.x != null && action.y != null) {
106
+ await this.clickAt(webContents, action.x, action.y)
107
+ } else {
108
+ return { success: false, error: 'click requires selector or x/y coordinates' }
109
+ }
110
+ break
111
+ }
112
+ case 'type': {
113
+ if (!action.text) return { success: false, error: 'type requires text' }
114
+ if (action.selector) {
115
+ await this.cdp(webContents, 'Runtime.evaluate', {
116
+ expression: `document.querySelector(${JSON.stringify(action.selector)})?.focus()`
117
+ })
118
+ await this.wait(50)
119
+ }
120
+ for (const char of action.text) {
121
+ await this.cdp(webContents, 'Input.dispatchKeyEvent', {
122
+ type: 'char', text: char
123
+ })
124
+ await this.wait(20)
125
+ }
126
+ break
127
+ }
128
+ case 'scroll': {
129
+ const x = action.x ?? 400
130
+ const y = action.y ?? 300
131
+ const delta = action.scrollDelta ?? 200
132
+ await this.cdp(webContents, 'Input.dispatchMouseEvent', {
133
+ type: 'mouseWheel', x, y, deltaX: 0, deltaY: delta
134
+ })
135
+ break
136
+ }
137
+ case 'navigate': {
138
+ if (!action.url) return { success: false, error: 'navigate requires url' }
139
+ await this.safeLoadURL(webContents, action.url)
140
+ break
141
+ }
142
+ default:
143
+ return { success: false, error: `Unknown action type: ${action.type}` }
144
+ }
145
+ return { success: true }
146
+ } catch (err) {
147
+ return { success: false, error: (err as Error).message }
148
+ }
149
+ }
150
+
151
+ private async executeStep(webContents: WebContents, event: InteractionEvent): Promise<void> {
152
+ switch (event.type) {
153
+ case 'click':
154
+ case 'dblclick': {
155
+ const x = event.viewport_x ?? event.x ?? 0
156
+ const y = event.viewport_y ?? event.y ?? 0
157
+ const clickCount = event.type === 'dblclick' ? 2 : 1
158
+ await this.cdp(webContents, 'Input.dispatchMouseEvent', {
159
+ type: 'mouseMoved', x, y
160
+ })
161
+ await this.wait(30)
162
+ await this.cdp(webContents, 'Input.dispatchMouseEvent', {
163
+ type: 'mousePressed', x, y, button: 'left', clickCount
164
+ })
165
+ await this.cdp(webContents, 'Input.dispatchMouseEvent', {
166
+ type: 'mouseReleased', x, y, button: 'left', clickCount
167
+ })
168
+ break
169
+ }
170
+ case 'input': {
171
+ if (event.selector && event.input_value != null) {
172
+ await this.cdp(webContents, 'Runtime.evaluate', {
173
+ expression: `document.querySelector(${JSON.stringify(event.selector)})?.focus()`
174
+ })
175
+ await this.wait(50)
176
+ await this.cdp(webContents, 'Runtime.evaluate', {
177
+ expression: `{
178
+ const el = document.querySelector(${JSON.stringify(event.selector)});
179
+ if (el) { el.value = ''; el.dispatchEvent(new Event('input', {bubbles:true})); }
180
+ }`
181
+ })
182
+ for (const char of event.input_value) {
183
+ await this.cdp(webContents, 'Input.dispatchKeyEvent', {
184
+ type: 'char', text: char
185
+ })
186
+ await this.wait(15)
187
+ }
188
+ }
189
+ break
190
+ }
191
+ case 'scroll': {
192
+ await this.cdp(webContents, 'Input.dispatchMouseEvent', {
193
+ type: 'mouseWheel',
194
+ x: event.viewport_x ?? 400,
195
+ y: event.viewport_y ?? 300,
196
+ deltaX: event.scroll_dx ?? 0,
197
+ deltaY: event.scroll_dy ?? 0
198
+ })
199
+ break
200
+ }
201
+ case 'navigate': {
202
+ if (event.url) {
203
+ await this.safeLoadURL(webContents, event.url)
204
+ await this.wait(500)
205
+ }
206
+ break
207
+ }
208
+ case 'hover': {
209
+ if (event.path) {
210
+ const points = JSON.parse(event.path) as Array<{ x: number; y: number; t: number }>
211
+ for (const point of points) {
212
+ if (this.aborted) break
213
+ await this.cdp(webContents, 'Input.dispatchMouseEvent', {
214
+ type: 'mouseMoved', x: point.x, y: point.y
215
+ })
216
+ await this.wait(20)
217
+ }
218
+ }
219
+ break
220
+ }
221
+ }
222
+ }
223
+
224
+ private async clickAt(webContents: WebContents, x: number, y: number): Promise<void> {
225
+ await this.cdp(webContents, 'Input.dispatchMouseEvent', {
226
+ type: 'mouseMoved', x, y
227
+ })
228
+ await this.wait(30)
229
+ await this.cdp(webContents, 'Input.dispatchMouseEvent', {
230
+ type: 'mousePressed', x, y, button: 'left', clickCount: 1
231
+ })
232
+ await this.cdp(webContents, 'Input.dispatchMouseEvent', {
233
+ type: 'mouseReleased', x, y, button: 'left', clickCount: 1
234
+ })
235
+ }
236
+
237
+ private async resolveElementCenter(
238
+ webContents: WebContents,
239
+ selector: string
240
+ ): Promise<{ x: number; y: number } | null> {
241
+ const result = await this.cdp(webContents, 'Runtime.evaluate', {
242
+ expression: `(function() {
243
+ const el = document.querySelector(${JSON.stringify(selector)});
244
+ if (!el) return null;
245
+ const r = el.getBoundingClientRect();
246
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
247
+ })()`,
248
+ returnByValue: true
249
+ }) as { result?: { value?: { x: number; y: number } | null } }
250
+ return result?.result?.value ?? null
251
+ }
252
+
253
+ private wait(ms: number): Promise<void> {
254
+ return new Promise(resolve => setTimeout(resolve, Math.max(ms, 5)))
255
+ }
256
+ }
@@ -0,0 +1,76 @@
1
+ import { EventEmitter } from 'events'
2
+ import type { WebContents } from 'electron'
3
+
4
+ const COLLECTION_INTERVAL = 5000 // 5 seconds
5
+
6
+ /**
7
+ * StorageCollector — Periodically collects Cookie, localStorage, and sessionStorage
8
+ * snapshots from the target browser via CDP commands.
9
+ */
10
+ export class StorageCollector extends EventEmitter {
11
+ private webContents: WebContents | null = null
12
+ private sessionId: string | null = null
13
+ private timer: ReturnType<typeof setInterval> | null = null
14
+ private collecting = false
15
+
16
+ start(sessionId: string, webContents: WebContents): void {
17
+ this.sessionId = sessionId
18
+ this.webContents = webContents
19
+ this.collectAll()
20
+ this.timer = setInterval(() => { this.collectAll() }, COLLECTION_INTERVAL)
21
+ }
22
+
23
+ stop(): void {
24
+ if (this.timer) { clearInterval(this.timer); this.timer = null }
25
+ if (this.webContents && !this.webContents.isDestroyed()) this.collectAll()
26
+ this.webContents = null
27
+ this.sessionId = null
28
+ }
29
+
30
+ triggerCollection(): void { this.collectAll() }
31
+
32
+ private async collectAll(): Promise<void> {
33
+ if (this.collecting || !this.webContents || !this.sessionId) return
34
+ if (this.webContents.isDestroyed()) return
35
+ this.collecting = true
36
+ const domain = this.getCurrentDomain()
37
+ const timestamp = Date.now()
38
+ try {
39
+ await Promise.allSettled([
40
+ this.collectCookies(domain, timestamp),
41
+ this.collectLocalStorage(domain, timestamp),
42
+ this.collectSessionStorage(domain, timestamp)
43
+ ])
44
+ } finally { this.collecting = false }
45
+ }
46
+
47
+ private async collectCookies(domain: string, timestamp: number): Promise<void> {
48
+ if (!this.webContents || this.webContents.isDestroyed()) return
49
+ try {
50
+ const currentUrl = this.webContents.getURL()
51
+ const result = await this.webContents.debugger.sendCommand('Network.getCookies', { urls: [currentUrl] }) as { cookies: Array<Record<string, unknown>> }
52
+ this.emit('storage-collected', { domain, storageType: 'cookie', data: JSON.stringify(result.cookies || []), timestamp })
53
+ } catch (err) { console.warn('[StorageCollector] collectCookies failed:', (err as Error).message) }
54
+ }
55
+
56
+ private async collectLocalStorage(domain: string, timestamp: number): Promise<void> {
57
+ if (!this.webContents || this.webContents.isDestroyed()) return
58
+ try {
59
+ const result = await this.webContents.debugger.sendCommand('Runtime.evaluate', { expression: 'JSON.stringify(localStorage)', returnByValue: true }) as { result: { value?: string } }
60
+ this.emit('storage-collected', { domain, storageType: 'localStorage', data: result.result?.value || '{}', timestamp })
61
+ } catch (err) { console.warn('[StorageCollector] collectLocalStorage failed:', (err as Error).message) }
62
+ }
63
+
64
+ private async collectSessionStorage(domain: string, timestamp: number): Promise<void> {
65
+ if (!this.webContents || this.webContents.isDestroyed()) return
66
+ try {
67
+ const result = await this.webContents.debugger.sendCommand('Runtime.evaluate', { expression: 'JSON.stringify(sessionStorage)', returnByValue: true }) as { result: { value?: string } }
68
+ this.emit('storage-collected', { domain, storageType: 'sessionStorage', data: result.result?.value || '{}', timestamp })
69
+ } catch (err) { console.warn('[StorageCollector] collectSessionStorage failed:', (err as Error).message) }
70
+ }
71
+
72
+ private getCurrentDomain(): string {
73
+ if (!this.webContents || this.webContents.isDestroyed()) return 'unknown'
74
+ try { return new URL(this.webContents.getURL()).hostname } catch { return 'unknown' }
75
+ }
76
+ }