@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,233 @@
1
+ import { EventEmitter } from 'events'
2
+ import type { WebContents } from 'electron'
3
+
4
+ const MAX_BODY_SIZE = 1024 * 1024 // 1MB
5
+
6
+ // Binary content types that should not have their body stored
7
+ const BINARY_CONTENT_TYPES = [
8
+ 'image/', 'font/', 'audio/', 'video/',
9
+ 'application/octet-stream', 'application/pdf', 'application/zip'
10
+ ]
11
+
12
+ interface RequestInfo {
13
+ method: string
14
+ url: string
15
+ headers: Record<string, string>
16
+ postData: string | null
17
+ timestamp: number
18
+ initiator: unknown
19
+ isOptions: boolean
20
+ }
21
+
22
+ /**
23
+ * CdpManager — Chrome DevTools Protocol manager for network interception.
24
+ * Attaches to a WebContents debugger and intercepts all Fetch/XHR requests.
25
+ */
26
+ export class CdpManager extends EventEmitter {
27
+ private webContents: WebContents | null = null
28
+ private pendingRequests = new Map<string, RequestInfo>()
29
+ private running = false
30
+ private messageHandler: ((event: Electron.Event, method: string, params: Record<string, unknown>) => void) | null = null
31
+ private detachedHandler: (() => void) | null = null
32
+
33
+ async start(webContents: WebContents): Promise<void> {
34
+ this.webContents = webContents
35
+
36
+ if (webContents.isDestroyed()) {
37
+ throw new Error('Cannot start CDP on destroyed WebContents')
38
+ }
39
+
40
+ // Detach if already attached (e.g. leftover from a previous session)
41
+ if (webContents.debugger.isAttached()) {
42
+ try { webContents.debugger.detach() } catch { /* ignore */ }
43
+ }
44
+
45
+ try {
46
+ webContents.debugger.attach('1.3')
47
+ } catch (err) {
48
+ throw new Error(`Failed to attach CDP debugger: ${(err as Error).message}`)
49
+ }
50
+
51
+ this.messageHandler = (_event, method, params) => {
52
+ this.handleCdpMessage(method, params)
53
+ }
54
+ this.detachedHandler = () => {
55
+ this.running = false
56
+ this.emit('detached')
57
+ }
58
+
59
+ webContents.debugger.on('message', this.messageHandler)
60
+ webContents.debugger.on('detach', this.detachedHandler)
61
+
62
+ await Promise.all([
63
+ this.send('Fetch.enable', {
64
+ patterns: [
65
+ { urlPattern: '*', requestStage: 'Request' },
66
+ { urlPattern: '*', requestStage: 'Response' }
67
+ ]
68
+ }),
69
+ this.send('Network.enable', {}),
70
+ this.send('Page.enable', {})
71
+ ])
72
+
73
+ this.running = true
74
+ }
75
+
76
+ async stop(): Promise<void> {
77
+ if (!this.running || !this.webContents) return
78
+ this.running = false
79
+ if (this.webContents.isDestroyed()) return
80
+ try {
81
+ await this.send('Fetch.disable', {})
82
+ } catch { /* ignore */ }
83
+ }
84
+
85
+ detach(): void {
86
+ if (!this.webContents) return
87
+ if (!this.webContents.isDestroyed()) {
88
+ if (this.messageHandler) {
89
+ this.webContents.debugger.removeListener('message', this.messageHandler)
90
+ }
91
+ if (this.detachedHandler) {
92
+ this.webContents.debugger.removeListener('detach', this.detachedHandler)
93
+ }
94
+ try { this.webContents.debugger.detach() } catch { /* already detached */ }
95
+ }
96
+ this.pendingRequests.clear()
97
+ this.webContents = null
98
+ }
99
+
100
+ /**
101
+ * Send a raw CDP command. Exposed for advanced use cases (e.g. stealth injection).
102
+ */
103
+ async sendCommand(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
104
+ if (!this.webContents || this.webContents.isDestroyed()) throw new Error('No WebContents attached')
105
+ return this.webContents.debugger.sendCommand(method, params) as Promise<Record<string, unknown>>
106
+ }
107
+
108
+ private async send(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
109
+ return this.sendCommand(method, params)
110
+ }
111
+
112
+ private handleCdpMessage(method: string, params: Record<string, unknown>): void {
113
+ switch (method) {
114
+ case 'Fetch.requestPaused':
115
+ this.handleRequestPaused(params)
116
+ break
117
+ case 'Network.webSocketFrameSent':
118
+ this.emit('websocket-frame', { direction: 'sent', ...params })
119
+ break
120
+ case 'Network.webSocketFrameReceived':
121
+ this.emit('websocket-frame', { direction: 'received', ...params })
122
+ break
123
+ case 'Network.webSocketCreated':
124
+ this.emit('websocket-created', params)
125
+ break
126
+ case 'Network.webSocketClosed':
127
+ this.emit('websocket-closed', params)
128
+ break
129
+ case 'Page.frameNavigated':
130
+ this.emit('frame-navigated', params)
131
+ break
132
+ }
133
+ }
134
+
135
+ private async handleRequestPaused(params: Record<string, unknown>): Promise<void> {
136
+ const requestId = params.requestId as string
137
+ const responseStatusCode = params.responseStatusCode as number | undefined
138
+
139
+ if (responseStatusCode === undefined) {
140
+ await this.handleRequestStage(requestId, params)
141
+ } else {
142
+ await this.handleResponseStage(requestId, params)
143
+ }
144
+ }
145
+
146
+ private async handleRequestStage(requestId: string, params: Record<string, unknown>): Promise<void> {
147
+ const request = params.request as Record<string, unknown>
148
+ const method = (request.method as string) || 'GET'
149
+ const url = (request.url as string) || ''
150
+ const headers = (request.headers as Record<string, string>) || {}
151
+ const postData = (request.postData as string) || null
152
+ const isOptions = method.toUpperCase() === 'OPTIONS'
153
+
154
+ const info: RequestInfo = {
155
+ method, url, headers, postData,
156
+ timestamp: Date.now(),
157
+ initiator: params.initiator || null,
158
+ isOptions
159
+ }
160
+ this.pendingRequests.set(requestId, info)
161
+
162
+ this.emit('request-captured', {
163
+ requestId, method, url,
164
+ headers: JSON.stringify(headers),
165
+ body: postData,
166
+ timestamp: info.timestamp,
167
+ initiator: params.initiator ? JSON.stringify(params.initiator) : null,
168
+ isOptions
169
+ })
170
+
171
+ try { await this.send('Fetch.continueRequest', { requestId }) } catch { /* cancelled */ }
172
+ }
173
+
174
+ private async handleResponseStage(requestId: string, params: Record<string, unknown>): Promise<void> {
175
+ const requestInfo = this.pendingRequests.get(requestId)
176
+ const statusCode = params.responseStatusCode as number
177
+ const responseHeaders = (params.responseHeaders as Array<{ name: string; value: string }>) || []
178
+
179
+ const headersObj: Record<string, string> = {}
180
+ for (const h of responseHeaders) {
181
+ headersObj[h.name.toLowerCase()] = h.value
182
+ }
183
+
184
+ const contentType = headersObj['content-type'] || null
185
+ const isBinary = contentType ? BINARY_CONTENT_TYPES.some(t => contentType.includes(t)) : false
186
+
187
+ let responseBody: string | null = null
188
+ let truncated = false
189
+
190
+ if (!isBinary) {
191
+ try {
192
+ const bodyResult = await this.send('Fetch.getResponseBody', { requestId })
193
+ const body = bodyResult.body as string
194
+ const base64Encoded = bodyResult.base64Encoded as boolean
195
+ responseBody = base64Encoded ? Buffer.from(body, 'base64').toString('utf-8') : body
196
+
197
+ if (responseBody && responseBody.length > MAX_BODY_SIZE) {
198
+ responseBody = responseBody.substring(0, MAX_BODY_SIZE) + '\n[TRUNCATED]'
199
+ truncated = true
200
+ }
201
+ } catch { responseBody = null }
202
+ }
203
+
204
+ const durationMs = requestInfo ? Date.now() - requestInfo.timestamp : null
205
+ const STATIC_EXTENSIONS = /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|ico|map)(\?|$)/i
206
+
207
+ const isStreaming = contentType ? contentType.includes('text/event-stream') : false
208
+ const isWebSocket = requestInfo
209
+ ? Object.entries(requestInfo.headers).some(([key, value]) =>
210
+ key.toLowerCase() === 'upgrade' && value.toLowerCase() === 'websocket')
211
+ : false
212
+
213
+ this.emit('response-captured', {
214
+ requestId,
215
+ method: requestInfo?.method || 'UNKNOWN',
216
+ url: requestInfo?.url || '',
217
+ requestHeaders: requestInfo ? JSON.stringify(requestInfo.headers) : '{}',
218
+ requestBody: requestInfo?.postData || null,
219
+ statusCode, responseHeaders: JSON.stringify(headersObj),
220
+ responseBody, contentType,
221
+ initiator: requestInfo?.initiator ? JSON.stringify(requestInfo.initiator) : null,
222
+ durationMs,
223
+ isOptions: requestInfo?.isOptions || false,
224
+ isStatic: requestInfo ? STATIC_EXTENSIONS.test(requestInfo.url) : false,
225
+ isStreaming,
226
+ isWebSocket,
227
+ truncated, timestamp: requestInfo?.timestamp || Date.now()
228
+ })
229
+
230
+ this.pendingRequests.delete(requestId)
231
+ try { await this.send('Fetch.continueResponse', { requestId }) } catch { /* cancelled */ }
232
+ }
233
+ }
@@ -0,0 +1,41 @@
1
+ import Database from 'better-sqlite3'
2
+ import { app } from 'electron'
3
+ import { join } from 'path'
4
+ import { existsSync, mkdirSync } from 'fs'
5
+
6
+ let db: Database.Database | null = null
7
+
8
+ /**
9
+ * Get or initialize the SQLite database connection.
10
+ * Database file is stored in the app's user data directory.
11
+ */
12
+ export function getDatabase(): Database.Database {
13
+ if (db) return db
14
+
15
+ const userDataPath = app.getPath('userData')
16
+ const dbDir = join(userDataPath, 'data')
17
+
18
+ if (!existsSync(dbDir)) {
19
+ mkdirSync(dbDir, { recursive: true })
20
+ }
21
+
22
+ const dbPath = join(dbDir, 'anything-register.db')
23
+
24
+ db = new Database(dbPath)
25
+
26
+ // Enable WAL mode for better concurrent read performance
27
+ db.pragma('journal_mode = WAL')
28
+ db.pragma('foreign_keys = ON')
29
+
30
+ return db
31
+ }
32
+
33
+ /**
34
+ * Close the database connection (called on app quit).
35
+ */
36
+ export function closeDatabase(): void {
37
+ if (db) {
38
+ db.close()
39
+ db = null
40
+ }
41
+ }
@@ -0,0 +1,235 @@
1
+ import type Database from 'better-sqlite3'
2
+
3
+ /**
4
+ * Migration 005: Add streaming and WebSocket flags to requests table
5
+ * Safe to call multiple times (handles duplicate column errors)
6
+ */
7
+ export function migrateAddStreamingAndWebSocketFlags(db: Database.Database): void {
8
+ const migrations = [
9
+ `ALTER TABLE requests ADD COLUMN is_streaming INTEGER DEFAULT 0`,
10
+ `ALTER TABLE requests ADD COLUMN is_websocket INTEGER DEFAULT 0`,
11
+ ]
12
+
13
+ for (const migration of migrations) {
14
+ try {
15
+ db.exec(migration)
16
+ } catch (err) {
17
+ // Ignore error if column already exists
18
+ if (!String(err).includes('duplicate column name')) {
19
+ throw err
20
+ }
21
+ }
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Run all database migrations — create tables and indexes.
27
+ * Safe to call multiple times (uses IF NOT EXISTS).
28
+ */
29
+ export function runMigrations(db: Database.Database): void {
30
+ db.exec(`
31
+ -- Sessions table
32
+ CREATE TABLE IF NOT EXISTS sessions (
33
+ id TEXT PRIMARY KEY,
34
+ name TEXT,
35
+ target_url TEXT,
36
+ status TEXT NOT NULL DEFAULT 'stopped',
37
+ created_at INTEGER NOT NULL,
38
+ stopped_at INTEGER
39
+ );
40
+
41
+ -- HTTP request records
42
+ CREATE TABLE IF NOT EXISTS requests (
43
+ id TEXT PRIMARY KEY,
44
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
45
+ sequence INTEGER NOT NULL,
46
+ timestamp INTEGER NOT NULL,
47
+ method TEXT NOT NULL,
48
+ url TEXT NOT NULL,
49
+ request_headers TEXT,
50
+ request_body TEXT,
51
+ status_code INTEGER,
52
+ response_headers TEXT,
53
+ response_body TEXT,
54
+ content_type TEXT,
55
+ initiator TEXT,
56
+ duration_ms INTEGER
57
+ );
58
+
59
+ -- JS Hook capture records
60
+ CREATE TABLE IF NOT EXISTS js_hooks (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
63
+ timestamp INTEGER NOT NULL,
64
+ hook_type TEXT NOT NULL,
65
+ function_name TEXT NOT NULL,
66
+ arguments TEXT,
67
+ result TEXT,
68
+ call_stack TEXT,
69
+ related_request_id TEXT
70
+ );
71
+
72
+ -- Storage snapshots
73
+ CREATE TABLE IF NOT EXISTS storage_snapshots (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
76
+ timestamp INTEGER NOT NULL,
77
+ domain TEXT NOT NULL,
78
+ storage_type TEXT NOT NULL,
79
+ data TEXT
80
+ );
81
+
82
+ -- AI analysis reports
83
+ CREATE TABLE IF NOT EXISTS analysis_reports (
84
+ id TEXT PRIMARY KEY,
85
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
86
+ created_at INTEGER NOT NULL,
87
+ llm_provider TEXT NOT NULL,
88
+ llm_model TEXT NOT NULL,
89
+ prompt_tokens INTEGER,
90
+ completion_tokens INTEGER,
91
+ report_content TEXT
92
+ );
93
+
94
+ -- Fingerprint profiles (one per session)
95
+ CREATE TABLE IF NOT EXISTS fingerprint_profiles (
96
+ session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
97
+ profile_json TEXT NOT NULL
98
+ );
99
+
100
+ -- Indexes
101
+ CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id, sequence);
102
+ CREATE INDEX IF NOT EXISTS idx_requests_url ON requests(url);
103
+ CREATE INDEX IF NOT EXISTS idx_js_hooks_session ON js_hooks(session_id, timestamp);
104
+ CREATE INDEX IF NOT EXISTS idx_storage_session ON storage_snapshots(session_id, timestamp);
105
+ CREATE INDEX IF NOT EXISTS idx_reports_session ON analysis_reports(session_id);
106
+ `)
107
+
108
+ // Run additional migrations
109
+ migrateAddStreamingAndWebSocketFlags(db)
110
+ migrateAddFilterTokenColumns(db)
111
+ migrateAddSourceColumn(db)
112
+ migrateAddChatMessagesTable(db)
113
+ migrateAddAiRequestLogsTable(db)
114
+ migrateAddInteractionEventsTable(db)
115
+ }
116
+
117
+ /**
118
+ * Migration 008: Add chat_messages table for persisting follow-up Q&A per report
119
+ * Safe to call multiple times (uses IF NOT EXISTS).
120
+ */
121
+ export function migrateAddChatMessagesTable(db: Database.Database): void {
122
+ db.exec(`
123
+ CREATE TABLE IF NOT EXISTS chat_messages (
124
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
125
+ report_id TEXT NOT NULL REFERENCES analysis_reports(id) ON DELETE CASCADE,
126
+ role TEXT NOT NULL,
127
+ content TEXT NOT NULL,
128
+ created_at INTEGER NOT NULL
129
+ );
130
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_report ON chat_messages(report_id, id);
131
+ `)
132
+ }
133
+
134
+ /**
135
+ * Migration 007: Add source column to requests table for MITM proxy support
136
+ * Safe to call multiple times (handles duplicate column errors)
137
+ */
138
+ export function migrateAddSourceColumn(db: Database.Database): void {
139
+ try {
140
+ db.exec(`ALTER TABLE requests ADD COLUMN source TEXT DEFAULT 'cdp'`);
141
+ } catch (err) {
142
+ if (!String(err).includes("duplicate column name")) throw err;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Migration 006: Add Phase 1 filter token columns to analysis_reports
148
+ * Safe to call multiple times (handles duplicate column errors)
149
+ */
150
+ export function migrateAddFilterTokenColumns(db: Database.Database): void {
151
+ const migrations = [
152
+ `ALTER TABLE analysis_reports ADD COLUMN filter_prompt_tokens INTEGER`,
153
+ `ALTER TABLE analysis_reports ADD COLUMN filter_completion_tokens INTEGER`,
154
+ ]
155
+
156
+ for (const migration of migrations) {
157
+ try {
158
+ db.exec(migration)
159
+ } catch (err) {
160
+ if (!String(err).includes('duplicate column name')) {
161
+ throw err
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Migration 009: Add ai_request_logs table for recording LLM HTTP requests
169
+ * Safe to call multiple times (uses IF NOT EXISTS).
170
+ */
171
+ export function migrateAddAiRequestLogsTable(db: Database.Database): void {
172
+ db.exec(`
173
+ CREATE TABLE IF NOT EXISTS ai_request_logs (
174
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
175
+ session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
176
+ report_id TEXT REFERENCES analysis_reports(id) ON DELETE SET NULL,
177
+ type TEXT NOT NULL,
178
+ provider TEXT NOT NULL,
179
+ model TEXT NOT NULL,
180
+ request_url TEXT NOT NULL,
181
+ request_method TEXT NOT NULL DEFAULT 'POST',
182
+ request_headers TEXT NOT NULL,
183
+ request_body TEXT NOT NULL,
184
+ status_code INTEGER,
185
+ response_headers TEXT,
186
+ response_body TEXT,
187
+ prompt_tokens INTEGER DEFAULT 0,
188
+ completion_tokens INTEGER DEFAULT 0,
189
+ duration_ms INTEGER,
190
+ error TEXT,
191
+ created_at INTEGER NOT NULL
192
+ );
193
+ CREATE INDEX IF NOT EXISTS idx_ai_logs_session ON ai_request_logs(session_id);
194
+ CREATE INDEX IF NOT EXISTS idx_ai_logs_created ON ai_request_logs(created_at);
195
+ `)
196
+ }
197
+
198
+ /**
199
+ * Migration 010: Add interaction_events table for recording user interactions
200
+ * Safe to call multiple times (uses IF NOT EXISTS).
201
+ */
202
+ export function migrateAddInteractionEventsTable(db: Database.Database): void {
203
+ db.exec(`
204
+ CREATE TABLE IF NOT EXISTS interaction_events (
205
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
206
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
207
+ sequence INTEGER NOT NULL,
208
+ type TEXT NOT NULL,
209
+ timestamp INTEGER NOT NULL,
210
+ x REAL,
211
+ y REAL,
212
+ viewport_x REAL,
213
+ viewport_y REAL,
214
+ selector TEXT,
215
+ xpath TEXT,
216
+ tag_name TEXT,
217
+ element_text TEXT,
218
+ attributes TEXT,
219
+ bounding_rect TEXT,
220
+ input_value TEXT,
221
+ key TEXT,
222
+ scroll_x REAL,
223
+ scroll_y REAL,
224
+ scroll_dx REAL,
225
+ scroll_dy REAL,
226
+ url TEXT NOT NULL,
227
+ page_title TEXT,
228
+ path TEXT,
229
+ created_at INTEGER NOT NULL
230
+ );
231
+ CREATE INDEX IF NOT EXISTS idx_interactions_session ON interaction_events(session_id);
232
+ CREATE INDEX IF NOT EXISTS idx_interactions_session_seq ON interaction_events(session_id, sequence);
233
+ CREATE INDEX IF NOT EXISTS idx_interactions_type ON interaction_events(session_id, type);
234
+ `)
235
+ }