@mazees/mark 5.0.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.
Files changed (227) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +209 -0
  3. package/bin/mark.js +27 -0
  4. package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
  5. package/out/renderer/assets/index-G2fADWO-.js +4768 -0
  6. package/out/renderer/assets/index-f3mVse3c.css +1 -0
  7. package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
  8. package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
  9. package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
  10. package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
  11. package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
  12. package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
  13. package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
  14. package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
  15. package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
  16. package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  17. package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  18. package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  19. package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
  20. package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
  21. package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  22. package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
  23. package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  24. package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
  25. package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
  26. package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
  27. package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
  28. package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
  29. package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
  30. package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
  31. package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
  32. package/out/renderer/favicon.ico +0 -0
  33. package/out/renderer/icon-256.png +0 -0
  34. package/out/renderer/icon.svg +1 -0
  35. package/out/renderer/index.html +25 -0
  36. package/out/renderer/manifest.json +25 -0
  37. package/out/renderer/robot-simulator.html +1640 -0
  38. package/package.json +106 -0
  39. package/src/cli/index.js +332 -0
  40. package/src/cli/monitor.js +413 -0
  41. package/src/cli/theme.js +347 -0
  42. package/src/launcher/MarkTray.cs +227 -0
  43. package/src/main/browser-agent.js +1132 -0
  44. package/src/main/git-service.js +90 -0
  45. package/src/main/google/google-calendar.js +69 -0
  46. package/src/main/google/google-drive.js +263 -0
  47. package/src/main/google/google-gmail.js +131 -0
  48. package/src/main/google/google-service.js +241 -0
  49. package/src/main/node-tools.js +23 -0
  50. package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
  51. package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
  52. package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
  53. package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
  54. package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
  55. package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
  56. package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
  57. package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
  58. package/src/main/plugins/plugin-loader.js +306 -0
  59. package/src/main/skills/skill-manager.js +265 -0
  60. package/src/main/syntax-validator.js +455 -0
  61. package/src/main/task-daemon.js +117 -0
  62. package/src/main/telegram/telegram-service.js +835 -0
  63. package/src/main/tools/browser-tools.js +516 -0
  64. package/src/main/tools/file-tools.js +1126 -0
  65. package/src/main/tools/git-tools.js +79 -0
  66. package/src/main/tools/google-tools.js +566 -0
  67. package/src/main/tools/index.js +36 -0
  68. package/src/main/tools/media-tools.js +42 -0
  69. package/src/main/tools/system-tools.js +295 -0
  70. package/src/main/tools/task-tools.js +73 -0
  71. package/src/main/tools/telegram-tools.js +101 -0
  72. package/src/renderer/index.html +24 -0
  73. package/src/renderer/public/favicon.ico +0 -0
  74. package/src/renderer/public/icon-256.png +0 -0
  75. package/src/renderer/public/icon.svg +1 -0
  76. package/src/renderer/public/manifest.json +25 -0
  77. package/src/renderer/public/robot-simulator.html +1640 -0
  78. package/src/renderer/src/App.jsx +304 -0
  79. package/src/renderer/src/api/ai/awareness.js +294 -0
  80. package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
  81. package/src/renderer/src/api/ai/contextCompactor.js +129 -0
  82. package/src/renderer/src/api/ai/contextManager.js +1130 -0
  83. package/src/renderer/src/api/ai/core.js +167 -0
  84. package/src/renderer/src/api/ai/index.js +4 -0
  85. package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
  86. package/src/renderer/src/api/ai/persona.js +115 -0
  87. package/src/renderer/src/api/ai/planning.js +317 -0
  88. package/src/renderer/src/api/ai/relationship.js +177 -0
  89. package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
  90. package/src/renderer/src/api/ai/tools.js +167 -0
  91. package/src/renderer/src/api/ai/utils.js +359 -0
  92. package/src/renderer/src/api/db.js +1013 -0
  93. package/src/renderer/src/api/oramaStore.js +592 -0
  94. package/src/renderer/src/api/ragPipeline.js +87 -0
  95. package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
  96. package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
  97. package/src/renderer/src/api/subagent/subagentStore.js +173 -0
  98. package/src/renderer/src/api/taskExecutor.js +18 -0
  99. package/src/renderer/src/api/taskStore.js +315 -0
  100. package/src/renderer/src/api/tools/core-tools.js +707 -0
  101. package/src/renderer/src/api/tools/index.js +151 -0
  102. package/src/renderer/src/api/turnPairMigrator.js +200 -0
  103. package/src/renderer/src/api/vectorMemory.js +136 -0
  104. package/src/renderer/src/api/wakeWord.js +101 -0
  105. package/src/renderer/src/api/web-bridge.js +885 -0
  106. package/src/renderer/src/api/webSpeech.js +178 -0
  107. package/src/renderer/src/api/workspaceRag.js +76 -0
  108. package/src/renderer/src/assets/electron.svg +10 -0
  109. package/src/renderer/src/assets/icon.svg +1 -0
  110. package/src/renderer/src/assets/main.css +589 -0
  111. package/src/renderer/src/assets/music-cover.png +0 -0
  112. package/src/renderer/src/assets/wavy-lines.svg +25 -0
  113. package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
  114. package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
  115. package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
  116. package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
  117. package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
  118. package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
  119. package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
  120. package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
  121. package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
  122. package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
  123. package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
  124. package/src/renderer/src/components/Chat/index.js +11 -0
  125. package/src/renderer/src/components/ChatList.jsx +434 -0
  126. package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
  127. package/src/renderer/src/components/Versions.jsx +15 -0
  128. package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
  129. package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
  130. package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
  131. package/src/renderer/src/components/config/CameraSection.jsx +100 -0
  132. package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
  133. package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
  134. package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
  135. package/src/renderer/src/components/config/index.js +5 -0
  136. package/src/renderer/src/components/core/Avatar.jsx +2197 -0
  137. package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
  138. package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
  139. package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
  140. package/src/renderer/src/components/core/HoloCard.jsx +64 -0
  141. package/src/renderer/src/components/core/InputBar.jsx +1346 -0
  142. package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
  143. package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
  144. package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
  145. package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
  146. package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
  147. package/src/renderer/src/components/core/native-skills.js +43 -0
  148. package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
  149. package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
  150. package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
  151. package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
  152. package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
  153. package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
  154. package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
  155. package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
  156. package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
  157. package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
  158. package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
  159. package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
  160. package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
  161. package/src/renderer/src/contexts/ChatContext.jsx +61 -0
  162. package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
  163. package/src/renderer/src/data/faqData.js +202 -0
  164. package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
  165. package/src/renderer/src/hooks/agent/index.js +4 -0
  166. package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
  167. package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
  168. package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
  169. package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
  170. package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
  171. package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
  172. package/src/renderer/src/hooks/useAwareness.js +289 -0
  173. package/src/renderer/src/hooks/useChatArchiver.js +52 -0
  174. package/src/renderer/src/hooks/useConfirm.jsx +68 -0
  175. package/src/renderer/src/hooks/useMarkAgent.js +454 -0
  176. package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
  177. package/src/renderer/src/hooks/useVAD.js +511 -0
  178. package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
  179. package/src/renderer/src/main.jsx +12 -0
  180. package/src/renderer/src/pages/ChatStudio.jsx +922 -0
  181. package/src/renderer/src/pages/Configuration.jsx +360 -0
  182. package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
  183. package/src/renderer/src/pages/Guidebook.jsx +917 -0
  184. package/src/renderer/src/pages/Knowledge.jsx +241 -0
  185. package/src/renderer/src/pages/MarkHome.jsx +491 -0
  186. package/src/renderer/src/pages/NeuralCore.jsx +786 -0
  187. package/src/renderer/src/pages/Plugins.jsx +630 -0
  188. package/src/renderer/src/pages/SkillEditor.jsx +380 -0
  189. package/src/renderer/src/pages/Skills.jsx +219 -0
  190. package/src/renderer/src/pages/Subagents.jsx +342 -0
  191. package/src/renderer/src/pages/TelegramBot.jsx +343 -0
  192. package/src/renderer/src/utils/chatContentParser.js +203 -0
  193. package/src/renderer/src/utils/toolClusters.js +201 -0
  194. package/src/server/bin/sha3_wasm_bg.wasm +0 -0
  195. package/src/server/config-manager.js +57 -0
  196. package/src/server/index.js +195 -0
  197. package/src/server/launcher.js +87 -0
  198. package/src/server/memory/db-store.js +758 -0
  199. package/src/server/memory/orama-store.js +177 -0
  200. package/src/server/memory/vector-engine.js +109 -0
  201. package/src/server/routes/ai.routes.js +280 -0
  202. package/src/server/routes/awareness.routes.js +62 -0
  203. package/src/server/routes/chat.routes.js +61 -0
  204. package/src/server/routes/config.routes.js +41 -0
  205. package/src/server/routes/index.js +35 -0
  206. package/src/server/routes/integrations.routes.js +116 -0
  207. package/src/server/routes/memory.routes.js +256 -0
  208. package/src/server/routes/plugins.routes.js +72 -0
  209. package/src/server/routes/skills.routes.js +139 -0
  210. package/src/server/routes/subagents.routes.js +70 -0
  211. package/src/server/routes/tasks.routes.js +175 -0
  212. package/src/server/routes/workspace.routes.js +127 -0
  213. package/src/server/services/ai/ai-utils.js +161 -0
  214. package/src/server/services/ai/openai-provider.js +757 -0
  215. package/src/server/services/ai/web-provider.js +729 -0
  216. package/src/server/services/ai-bridge.js +46 -0
  217. package/src/server/services/deepseek-web.js +956 -0
  218. package/src/server/services/gemini-web.js +170 -0
  219. package/src/server/services/notification-service.js +91 -0
  220. package/src/server/services/updater.js +125 -0
  221. package/src/server/tools/awareness-tracker.js +292 -0
  222. package/src/server/tools/group-tools.js +1164 -0
  223. package/src/server/tools/media-tools.js +244 -0
  224. package/src/server/tools/pc-agent.js +327 -0
  225. package/src/server/tools/screen-service.js +51 -0
  226. package/src/server/ws-hub.js +168 -0
  227. package/src/shared/parsers/mark-tag-parser.js +275 -0
@@ -0,0 +1,1132 @@
1
+ import puppeteer from 'puppeteer-core'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import os from 'os'
5
+ import { exec } from 'child_process'
6
+ import { wsHub } from '../server/ws-hub.js'
7
+
8
+ export const _getBrowserSign = () =>
9
+ 'RvbmVzaWEuIElmIGFza2VkIGFib3V0IHlvdXIgaWRlbnRpdHksIGNyZWF0b3IsIG9yIHNvdXJjZSBjb2RlLCB5b3UgbXVzdCBzdHJpY3RseSBjcmVkaXQgaGltIGFu'
10
+
11
+ // Map of sessionId -> SessionState { browser, page, url, title, idleTimeout }
12
+ const sessions = new Map()
13
+
14
+ function getEdgePath() {
15
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
16
+ const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files'
17
+ const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)'
18
+
19
+ const possiblePaths = [
20
+ // 1. Microsoft Edge (System-wide & Local)
21
+ path.join(programFilesX86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
22
+ path.join(programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
23
+ path.join(localAppData, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
24
+
25
+ // 2. Google Chrome (System-wide & User Local AppData)
26
+ path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
27
+ path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
28
+ path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'),
29
+
30
+ // 3. Brave Browser
31
+ path.join(programFiles, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
32
+ path.join(localAppData, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
33
+
34
+ // 4. Vivaldi Browser
35
+ path.join(localAppData, 'Vivaldi', 'Application', 'vivaldi.exe'),
36
+ path.join(programFiles, 'Vivaldi', 'Application', 'vivaldi.exe')
37
+ ]
38
+
39
+ for (const p of possiblePaths) {
40
+ if (fs.existsSync(p)) return p
41
+ }
42
+
43
+ // Fallback nama executable di PATH
44
+ return 'msedge.exe'
45
+ }
46
+
47
+ function resetSessionIdleTimeout(session) {
48
+ if (!session) return
49
+ if (session.idleTimeout) clearTimeout(session.idleTimeout)
50
+ session.idleTimeout = setTimeout(
51
+ () => {
52
+ closeBrowser(session.id)
53
+ },
54
+ 5 * 60 * 1000
55
+ )
56
+ }
57
+
58
+ async function getOrCreateSession(sessionId = 'default', headless = false) {
59
+ let session = sessions.get(sessionId)
60
+ if (session && session.page && !session.page.isClosed()) {
61
+ resetSessionIdleTimeout(session)
62
+ return session
63
+ }
64
+
65
+ const edgePath = getEdgePath()
66
+ const userDataDir = path.join(
67
+ os.homedir(),
68
+ '.config',
69
+ 'mark-agent',
70
+ 'browser-sessions',
71
+ sessionId
72
+ )
73
+ if (!fs.existsSync(userDataDir)) {
74
+ fs.mkdirSync(userDataDir, { recursive: true })
75
+ }
76
+
77
+ const browser = await puppeteer.launch({
78
+ executablePath: edgePath,
79
+ headless: headless ? 'new' : false,
80
+ userDataDir,
81
+ ignoreDefaultArgs: ['--enable-automation'],
82
+ defaultViewport: null,
83
+ args: [
84
+ '--disable-blink-features=AutomationControlled',
85
+ '--disable-features=IsolateOrigins,site-per-process',
86
+ '--no-sandbox',
87
+ '--disable-setuid-sandbox',
88
+ '--disable-infobars',
89
+ '--no-first-run',
90
+ '--no-default-browser-check',
91
+ '--disable-background-timer-throttling',
92
+ '--disable-backgrounding-occluded-windows',
93
+ '--disable-renderer-backgrounding',
94
+ '--window-position=80,60',
95
+ '--window-size=1280,800',
96
+ '--lang=id-ID,id,en-US,en'
97
+ ]
98
+ })
99
+
100
+ const pages = await browser.pages()
101
+ const page = pages.length > 0 ? pages[0] : await browser.newPage()
102
+
103
+ // Real Edge Windows 11 User-Agent & HTTP Headers
104
+ const userAgent =
105
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0'
106
+ await page.setUserAgent(userAgent)
107
+
108
+ await page.setExtraHTTPHeaders({
109
+ 'Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7',
110
+ 'Sec-CH-UA': '"Not(A:Brand";v="99", "Microsoft Edge";v="133", "Chromium";v="133"',
111
+ 'Sec-CH-UA-Mobile': '?0',
112
+ 'Sec-CH-UA-Platform': '"Windows"',
113
+ 'Upgrade-Insecure-Requests': '1'
114
+ })
115
+
116
+ // Comprehensive Anti-Bot Detection Stealth Overrides
117
+ await page.evaluateOnNewDocument(() => {
118
+ const makeNative = (fn, name) => {
119
+ Object.defineProperty(fn, 'name', { value: name, configurable: true })
120
+ const toString = () => `function ${name}() { [native code] }`
121
+ Object.defineProperty(fn, 'toString', { value: toString, configurable: true })
122
+ return fn
123
+ }
124
+
125
+ // 1. Mock Screen & Window Coordinates (Anti-Bot Stealth)
126
+ Object.defineProperties(window, {
127
+ screenX: { get: () => 80, configurable: true },
128
+ screenY: { get: () => 60, configurable: true },
129
+ screenLeft: { get: () => 80, configurable: true },
130
+ screenTop: { get: () => 60, configurable: true },
131
+ outerWidth: { get: () => 1280, configurable: true },
132
+ outerHeight: { get: () => 800, configurable: true },
133
+ innerWidth: { get: () => 1280, configurable: true },
134
+ innerHeight: { get: () => 800, configurable: true }
135
+ })
136
+
137
+ Object.defineProperties(window.screen, {
138
+ availLeft: { get: () => 0, configurable: true },
139
+ availTop: { get: () => 0, configurable: true },
140
+ availWidth: { get: () => 1920, configurable: true },
141
+ availHeight: { get: () => 1040, configurable: true },
142
+ width: { get: () => 1920, configurable: true },
143
+ height: { get: () => 1080, configurable: true }
144
+ })
145
+
146
+ // 2. WebDriver stealth: returns false with native getter signature
147
+ try {
148
+ delete Object.getPrototypeOf(navigator).webdriver
149
+ } catch (_) {}
150
+ const webdriverGetter = makeNative(() => false, 'get webdriver')
151
+ Object.defineProperty(Navigator.prototype, 'webdriver', {
152
+ get: webdriverGetter,
153
+ enumerable: true,
154
+ configurable: true
155
+ })
156
+
157
+ // 3. Native Chrome runtime structure matching official Edge
158
+ const sendMessageFn = makeNative(function () {}, 'sendMessage')
159
+ const connectFn = makeNative(function () {}, 'connect')
160
+ window.chrome = {
161
+ app: {
162
+ isInstalled: false,
163
+ InstallState: {
164
+ DISABLED: 'disabled',
165
+ INSTALLED: 'installed',
166
+ NOT_INSTALLED: 'not_installed'
167
+ },
168
+ RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' }
169
+ },
170
+ runtime: {
171
+ OnInstalledReason: {},
172
+ OnRestartRequiredReason: {},
173
+ PlatformArch: {},
174
+ PlatformNaclArch: {},
175
+ PlatformOs: {},
176
+ RequestUpdateCheckStatus: {},
177
+ connect: connectFn,
178
+ sendMessage: sendMessageFn
179
+ },
180
+ loadTimes: makeNative(function () {
181
+ return {
182
+ requestTime: performance.now() / 1000,
183
+ startLoadTime: performance.now() / 1000,
184
+ commitLoadTime: performance.now() / 1000,
185
+ finishDocumentLoadTime: performance.now() / 1000,
186
+ finishLoadTime: performance.now() / 1000,
187
+ firstPaintTime: performance.now() / 1000,
188
+ firstPaintAfterLoadTime: 0,
189
+ navigationType: 'Other',
190
+ wasFetchedViaSpdy: false,
191
+ wasNpnNegotiated: false,
192
+ npnNegotiatedProtocol: '',
193
+ wasAlternateProtocolAvailable: false,
194
+ connectionInfo: 'http/1.1'
195
+ }
196
+ }, 'loadTimes'),
197
+ csi: makeNative(function () {
198
+ return {
199
+ startE: performance.now(),
200
+ onloadT: performance.now(),
201
+ pageT: performance.now(),
202
+ tran: 15
203
+ }
204
+ }, 'csi')
205
+ }
206
+
207
+ // 4. PluginArray and MimeTypeArray Prototype emulation
208
+ const pluginData = [
209
+ {
210
+ name: 'PDF Viewer',
211
+ filename: 'internal-pdf-viewer',
212
+ description: 'Portable Document Format'
213
+ },
214
+ {
215
+ name: 'Chrome PDF Viewer',
216
+ filename: 'internal-pdf-viewer',
217
+ description: 'Portable Document Format'
218
+ },
219
+ {
220
+ name: 'Chromium PDF Viewer',
221
+ filename: 'internal-pdf-viewer',
222
+ description: 'Portable Document Format'
223
+ },
224
+ {
225
+ name: 'Microsoft Edge PDF Viewer',
226
+ filename: 'internal-pdf-viewer',
227
+ description: 'Portable Document Format'
228
+ },
229
+ {
230
+ name: 'WebKit built-in PDF',
231
+ filename: 'internal-pdf-viewer',
232
+ description: 'Portable Document Format'
233
+ }
234
+ ]
235
+
236
+ const createPluginArray = (plugins) => {
237
+ const arr = Object.create(PluginArray.prototype)
238
+ plugins.forEach((p, idx) => {
239
+ const pluginObj = Object.create(Plugin.prototype)
240
+ Object.defineProperties(pluginObj, {
241
+ name: { value: p.name, enumerable: true },
242
+ filename: { value: p.filename, enumerable: true },
243
+ description: { value: p.description, enumerable: true },
244
+ length: { value: 1, enumerable: true }
245
+ })
246
+ arr[idx] = pluginObj
247
+ arr[p.name] = pluginObj
248
+ })
249
+ Object.defineProperty(arr, 'length', { value: plugins.length, enumerable: true })
250
+ arr.item = makeNative((i) => arr[i] || null, 'item')
251
+ arr.namedItem = makeNative((name) => arr[name] || null, 'namedItem')
252
+ arr.refresh = makeNative(() => {}, 'refresh')
253
+ return arr
254
+ }
255
+
256
+ try {
257
+ const fakePlugins = createPluginArray(pluginData)
258
+ Object.defineProperty(navigator, 'plugins', {
259
+ get: () => fakePlugins,
260
+ configurable: true
261
+ })
262
+ } catch (_) {}
263
+
264
+ // 5. Hardware and Locale properties
265
+ Object.defineProperty(navigator, 'languages', {
266
+ get: () => ['id-ID', 'id', 'en-US', 'en'],
267
+ configurable: true
268
+ })
269
+ Object.defineProperty(navigator, 'language', { get: () => 'id-ID', configurable: true })
270
+ Object.defineProperty(navigator, 'platform', { get: () => 'Win32', configurable: true })
271
+ Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8, configurable: true })
272
+ Object.defineProperty(navigator, 'deviceMemory', { get: () => 8, configurable: true })
273
+ Object.defineProperty(navigator, 'maxTouchPoints', { get: () => 0, configurable: true })
274
+
275
+ // 6. User Agent Data (Edge Client Hints)
276
+ Object.defineProperty(navigator, 'userAgentData', {
277
+ get: () => ({
278
+ brands: [
279
+ { brand: 'Not(A:Brand', version: '99' },
280
+ { brand: 'Microsoft Edge', version: '133' },
281
+ { brand: 'Chromium', version: '133' }
282
+ ],
283
+ mobile: false,
284
+ platform: 'Windows',
285
+ getHighEntropyValues: async () => ({
286
+ architecture: 'x86',
287
+ bitness: '64',
288
+ brands: [
289
+ { brand: 'Not(A:Brand', version: '99' },
290
+ { brand: 'Microsoft Edge', version: '133' },
291
+ { brand: 'Chromium', version: '133' }
292
+ ],
293
+ fullVersionList: [
294
+ { brand: 'Not(A:Brand', version: '99.0.0.0' },
295
+ { brand: 'Microsoft Edge', version: '133.0.3065.82' },
296
+ { brand: 'Chromium', version: '133.0.6943.127' }
297
+ ],
298
+ mobile: false,
299
+ model: '',
300
+ platform: 'Windows',
301
+ platformVersion: '15.0.0'
302
+ })
303
+ }),
304
+ configurable: true
305
+ })
306
+
307
+ // 7. Notification Permissions
308
+ const originalQuery = window.navigator.permissions?.query
309
+ if (originalQuery) {
310
+ window.navigator.permissions.query = makeNative(
311
+ (parameters) =>
312
+ parameters && parameters.name === 'notifications'
313
+ ? Promise.resolve({ state: Notification.permission || 'default', onchange: null })
314
+ : originalQuery.call(window.navigator.permissions, parameters),
315
+ 'query'
316
+ )
317
+ }
318
+
319
+ // 8. WebGL Vendor and Renderer
320
+ const patchParam = (proto) => {
321
+ const original = proto.getParameter
322
+ proto.getParameter = makeNative(function (param) {
323
+ if (param === 37445) return 'Google Inc. (Intel)'
324
+ if (param === 37446)
325
+ return 'ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)'
326
+ return original.apply(this, arguments)
327
+ }, 'getParameter')
328
+ }
329
+ patchParam(WebGLRenderingContext.prototype)
330
+ patchParam(WebGL2RenderingContext.prototype)
331
+ })
332
+
333
+ session = {
334
+ id: sessionId,
335
+ browser,
336
+ page,
337
+ url: 'about:blank',
338
+ title: 'New Tab',
339
+ idleTimeout: null,
340
+ isForeground: true
341
+ }
342
+
343
+ // Handle page close / tab close gracefully (Keep alive in background)
344
+ page.on('close', async () => {
345
+ const s = sessions.get(sessionId)
346
+ if (s && s.browser && s.browser.isConnected()) {
347
+ try {
348
+ const remainingPages = await s.browser.pages()
349
+ if (remainingPages.length > 0) {
350
+ s.page = remainingPages[0]
351
+ } else {
352
+ // Re-create background page if user closed the last tab
353
+ s.page = await s.browser.newPage()
354
+ await s.page.setUserAgent(userAgent)
355
+ await hideBrowserWindow(sessionId)
356
+ }
357
+ } catch (_) {}
358
+ }
359
+ })
360
+
361
+ // Pastikan window bounds diatur ke posisi normal di layar (80, 60) saat launch
362
+ try {
363
+ const client = await page.target().createCDPSession()
364
+ const { windowId } = await client.send('Browser.getWindowForTarget')
365
+ await client.send('Browser.setWindowBounds', {
366
+ windowId,
367
+ bounds: { windowState: 'normal', left: 80, top: 60, width: 1280, height: 800 }
368
+ })
369
+ } catch (_) {}
370
+
371
+ sessions.set(sessionId, session)
372
+ resetSessionIdleTimeout(session)
373
+ return session
374
+ }
375
+
376
+ const DOM_PARSER_SCRIPT = `
377
+ (() => {
378
+ // 1. Injeksi Style dan Keyframe Animation jika belum ada
379
+ if (!document.getElementById('mark-blocker-style')) {
380
+ const styleEl = document.createElement('style');
381
+ styleEl.id = 'mark-blocker-style';
382
+ styleEl.textContent = \`
383
+ @keyframes mark-spin {
384
+ 0% { transform: rotate(0deg); }
385
+ 100% { transform: rotate(360deg); }
386
+ }
387
+ @keyframes mark-pulse {
388
+ 0%, 100% { opacity: 1; transform: scale(1); }
389
+ 50% { opacity: 0.6; transform: scale(0.98); }
390
+ }
391
+ .mark-pulse {
392
+ animation: mark-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
393
+ }
394
+ \`;
395
+ document.head ? document.head.appendChild(styleEl) : document.documentElement.appendChild(styleEl);
396
+ }
397
+
398
+ // 2. Injeksi atau Pulihkan Fullscreen Interaction Blocker & Top Loading Pill
399
+ let blocker = document.getElementById('mark-user-blocker');
400
+ if (!blocker) {
401
+ blocker = document.createElement('div');
402
+ blocker.id = 'mark-user-blocker';
403
+ document.documentElement.appendChild(blocker);
404
+
405
+ // Mencegah scroll saat blocker aktif
406
+ const preventScroll = (e) => {
407
+ if (blocker && blocker.dataset.mode !== 'unblock') {
408
+ e.preventDefault();
409
+ }
410
+ };
411
+ window.addEventListener('wheel', preventScroll, { passive: false });
412
+ window.addEventListener('touchmove', preventScroll, { passive: false });
413
+ }
414
+
415
+ // Mode default: Fullscreen blocker dengan Top Loading Pill
416
+ blocker.dataset.mode = 'working';
417
+ blocker.style.cssText = \`
418
+ position: fixed !important;
419
+ top: 0 !important;
420
+ left: 0 !important;
421
+ width: 100vw !important;
422
+ height: 100vh !important;
423
+ z-index: 2147483647 !important;
424
+ pointer-events: auto !important;
425
+ cursor: not-allowed !important;
426
+ display: flex !important;
427
+ justify-content: center !important;
428
+ align-items: flex-start !important;
429
+ padding-top: 24px !important;
430
+ box-sizing: border-box !important;
431
+ background: transparent !important;
432
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
433
+ \`;
434
+
435
+ blocker.innerHTML = \`
436
+ <div style="
437
+ background: rgba(25, 54, 45, 0.92);
438
+ backdrop-filter: blur(8px);
439
+ -webkit-backdrop-filter: blur(8px);
440
+ border: 1px solid rgba(31, 184, 84, 0.4);
441
+ border-radius: 30px;
442
+ padding: 10px 22px;
443
+ display: flex;
444
+ align-items: center;
445
+ gap: 12px;
446
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 0 15px rgba(31, 184, 84, 0.2);
447
+ color: #f1f5f9;
448
+ font-size: 13px;
449
+ font-weight: 500;
450
+ letter-spacing: 0.2px;
451
+ user-select: none;
452
+ pointer-events: auto;
453
+ ">
454
+ <svg style="animation: mark-spin 1s linear infinite; width: 16px; height: 16px; color: #1fb854;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
455
+ <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
456
+ </svg>
457
+ <span class="mark-pulse" style="color: #f8fafc;">Mark is working...</span>
458
+ </div>
459
+ \`;
460
+
461
+ // 3. Parser Elemen Interaktif
462
+ document.querySelectorAll('[data-mark-id]').forEach(el => el.removeAttribute('data-mark-id'));
463
+
464
+ const INTERACTIVE_SELECTORS = [
465
+ 'a[href]', 'button', 'input', 'select', 'textarea',
466
+ '[role="button"]', '[role="link"]', '[role="tab"]',
467
+ '[tabindex]:not([tabindex="-1"])'
468
+ ].join(', ');
469
+
470
+ const allElements = document.querySelectorAll(INTERACTIVE_SELECTORS);
471
+ const results = [];
472
+ let markId = 1;
473
+ const MAX_ELEMENTS = 80;
474
+ const MAX_TEXT_LENGTH = 80;
475
+
476
+ for (const el of allElements) {
477
+ if (el.closest('#mark-user-blocker')) continue;
478
+ if (results.length >= MAX_ELEMENTS) break;
479
+
480
+ const style = window.getComputedStyle(el);
481
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
482
+ if (el.offsetWidth < 5 || el.offsetHeight < 5) continue;
483
+
484
+ const rect = el.getBoundingClientRect();
485
+ if (rect.bottom < 0 || rect.top > window.innerHeight) continue;
486
+ if (rect.right < 0 || rect.left > window.innerWidth) continue;
487
+
488
+ const tag = el.tagName.toLowerCase();
489
+ let type = 'Element';
490
+ if (tag === 'a') type = 'Link';
491
+ else if (tag === 'button' || el.getAttribute('role') === 'button') type = 'Button';
492
+ else if (tag === 'input') type = 'Input (' + (el.type || 'text') + ')';
493
+ else if (tag === 'select') type = 'Dropdown';
494
+ else if (tag === 'textarea') type = 'TextArea';
495
+
496
+ let label = el.innerText?.trim() || el.value || el.placeholder || el.getAttribute('aria-label') || el.title || '';
497
+ label = label.replace(/\\n/g, ' ').substring(0, MAX_TEXT_LENGTH);
498
+
499
+ el.setAttribute('data-mark-id', markId);
500
+ results.push('[' + markId + '] ' + type + ': "' + label + '"');
501
+ markId++;
502
+ }
503
+
504
+ const getVisibleText = () => {
505
+ let text = '';
506
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
507
+ let node;
508
+ let lastParent = null;
509
+ while ((node = walker.nextNode())) {
510
+ if (text.length > 8000) break;
511
+ const parent = node.parentElement;
512
+ if (!parent || parent.closest('#mark-user-blocker')) continue;
513
+
514
+ const tag = parent.tagName.toLowerCase();
515
+ if (tag === 'script' || tag === 'style' || tag === 'noscript') continue;
516
+
517
+ const rect = parent.getBoundingClientRect();
518
+ if (rect.bottom > -200 && rect.top < window.innerHeight + 1500) {
519
+ const val = node.nodeValue.trim();
520
+ if (val.length > 0) {
521
+ if (lastParent !== parent && ['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li'].includes(tag)) {
522
+ text += '\\n';
523
+ }
524
+ text += val + ' ';
525
+ lastParent = parent;
526
+ }
527
+ }
528
+ }
529
+ return text.trim();
530
+ };
531
+
532
+ const bodyText = getVisibleText() || '';
533
+ const pageTitle = document.title || '';
534
+ const currentURL = window.location.href || '';
535
+
536
+ let output = '[URL Aktif]: ' + currentURL + '\\n';
537
+ output += '[Title]: ' + pageTitle + '\\n\\n';
538
+ output += '== ELEMEN INTERAKTIF (' + results.length + ' ditemukan) ==\\n';
539
+ output += results.join('\\n') + '\\n\\n';
540
+ output += '== TEKS UTAMA HALAMAN (Ringkasan) ==\\n';
541
+ output += bodyText.substring(0, 4000);
542
+
543
+ return output;
544
+ })()
545
+ `
546
+
547
+ async function broadcastPreview(session) {
548
+ try {
549
+ const screenshotBuf = await session.page.screenshot({ encoding: 'base64' })
550
+ if (wsHub) {
551
+ const dataUri = `data:image/png;base64,${screenshotBuf}`
552
+ wsHub.broadcast('browser:preview', {
553
+ sessionId: session.id,
554
+ thumbnail: dataUri,
555
+ preview: dataUri,
556
+ title: session.title || 'Browser Session',
557
+ url: session.url || 'about:blank',
558
+ isForeground: Boolean(session.isForeground)
559
+ })
560
+ }
561
+ } catch (_) {}
562
+ }
563
+
564
+ async function detectChallenge(page) {
565
+ if (!page || page.isClosed()) return { isBlocked: false }
566
+ try {
567
+ const url = page.url() || ''
568
+ const title = (await page.title()) || ''
569
+
570
+ // 1. Deteksi Pola URL & Title Khas Block/CAPTCHA (Google "Unusual Traffic", Cloudflare, Cloudflare Turnstile, reCAPTCHA, hCaptcha)
571
+ const isGoogleBlocked =
572
+ url.includes('google.com/sorry') ||
573
+ url.includes('/sorry/index') ||
574
+ title.includes('Sorry...') ||
575
+ title.includes('unusual traffic')
576
+ const isCloudflareBlocked =
577
+ title.includes('Just a moment...') ||
578
+ title.includes('Attention Required!') ||
579
+ title.includes('Security Challenge') ||
580
+ title.includes('Cloudflare')
581
+ const isGenericCaptcha =
582
+ title.toLowerCase().includes('robot check') ||
583
+ title.toLowerCase().includes('captcha') ||
584
+ title.toLowerCase().includes('human verification')
585
+
586
+ if (isGoogleBlocked || isCloudflareBlocked || isGenericCaptcha) {
587
+ return {
588
+ isBlocked: true,
589
+ type: isGoogleBlocked
590
+ ? 'Google Unusual Traffic'
591
+ : isCloudflareBlocked
592
+ ? 'Cloudflare Challenge'
593
+ : 'CAPTCHA Protection',
594
+ url,
595
+ title
596
+ }
597
+ }
598
+
599
+ // 2. Deteksi Selector DOM CAPTCHA / Iframe Challenge
600
+ const hasChallengeElement = await page.evaluate(() => {
601
+ const selectors = [
602
+ '#captcha-form',
603
+ '#challenge-running',
604
+ '#challenge-form',
605
+ '#cf-challenge-running',
606
+ '.cf-turnstile',
607
+ '.g-recaptcha',
608
+ '.h-captcha',
609
+ 'iframe[src*="recaptcha"]',
610
+ 'iframe[src*="turnstile"]',
611
+ 'iframe[src*="hcaptcha"]',
612
+ 'iframe[src*="challenge-platform"]'
613
+ ]
614
+ return selectors.some((s) => Boolean(document.querySelector(s)))
615
+ })
616
+
617
+ if (hasChallengeElement) {
618
+ return {
619
+ isBlocked: true,
620
+ type: 'DOM Challenge Element Detected',
621
+ url,
622
+ title
623
+ }
624
+ }
625
+ } catch (_) {}
626
+
627
+ return { isBlocked: false }
628
+ }
629
+
630
+ async function seedAntiBotCookies(page) {
631
+ try {
632
+ const cookies = [
633
+ {
634
+ name: 'SOCS',
635
+ value:
636
+ 'CAISNQgDEitib3FfaWRlbnRpdHlmcm9udGVuZHVpc2VydmVyXzIwMjMwODI5LjA3X3AwGgJpZCADGgYIgJnPpwY',
637
+ domain: '.google.com',
638
+ path: '/'
639
+ },
640
+ {
641
+ name: 'CONSENT',
642
+ value: 'PENDING+999',
643
+ domain: '.google.com',
644
+ path: '/'
645
+ }
646
+ ]
647
+ await page.setCookie(...cookies)
648
+ } catch (_) {}
649
+ }
650
+
651
+ export async function navigateTo(url, sessionId = 'default') {
652
+ const session = await getOrCreateSession(sessionId)
653
+ let targetUrl = url.trim()
654
+ if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
655
+ targetUrl = 'https://' + targetUrl
656
+ }
657
+
658
+ // Normalisasi URL Google Search dengan query params browser alami
659
+ if (targetUrl.includes('google.com/search') && !targetUrl.includes('sourceid=')) {
660
+ const separator = targetUrl.includes('?') ? '&' : '?'
661
+ targetUrl = `${targetUrl}${separator}sourceid=chrome&ie=UTF-8`
662
+ }
663
+
664
+ if (targetUrl.includes('google.com')) {
665
+ await seedAntiBotCookies(session.page)
666
+ }
667
+
668
+ await session.page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 45000 })
669
+ session.url = session.page.url()
670
+ session.title = await session.page.title()
671
+
672
+ // Auto-dismiss Google Consent / Cookie dialog jika muncul
673
+ try {
674
+ if (session.url.includes('google.com') || session.url.includes('consent.google')) {
675
+ const consentHandled = await session.page.evaluate(() => {
676
+ const consentSelectors = [
677
+ 'button[aria-label*="Setuju"]',
678
+ 'button[aria-label*="Accept"]',
679
+ '#L2AGLb',
680
+ 'button:has-text("Saya setuju")',
681
+ 'button:has-text("Accept all")',
682
+ 'form[action*="consent"] button'
683
+ ]
684
+ for (const selector of consentSelectors) {
685
+ const btn = document.querySelector(selector)
686
+ if (btn && btn.offsetParent !== null) {
687
+ btn.click()
688
+ return true
689
+ }
690
+ }
691
+ return false
692
+ })
693
+ if (consentHandled) {
694
+ await session.page
695
+ .waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 5000 })
696
+ .catch(() => {})
697
+ session.url = session.page.url()
698
+ session.title = await session.page.title()
699
+ }
700
+ }
701
+ } catch (_) {}
702
+
703
+ await broadcastPreview(session)
704
+
705
+ // Otomatis cek apakah halaman memicu CAPTCHA / bot challenge
706
+ const challenge = await detectChallenge(session.page)
707
+ if (challenge.isBlocked) {
708
+ return `[PERINGATAN ANTI-BOT / CAPTCHA TERDETEKSI]: Halaman memicu perlindungan "${challenge.type}" (Judul: "${session.title}", URL: "${session.url}").\nKamu WAJIB segera memanggil tool 'browser-ask-user' dengan pesan agar pengguna menyelesaikan CAPTCHA ini!`
709
+ }
710
+
711
+ return `Berhasil membuka ${session.url} (Judul: "${session.title}"). Gunakan 'browser-read' untuk melihat isi dan elemen interaktif.`
712
+ }
713
+
714
+ export async function readDOM(sessionId = 'default') {
715
+ const session = await getOrCreateSession(sessionId)
716
+ const result = await session.page.evaluate(DOM_PARSER_SCRIPT)
717
+
718
+ session.url = session.page.url()
719
+ session.title = await session.page.title()
720
+
721
+ await broadcastPreview(session)
722
+
723
+ const challenge = await detectChallenge(session.page)
724
+ if (challenge.isBlocked) {
725
+ return `[PERINGATAN ANTI-BOT / CAPTCHA TERDETEKSI]: Terdeteksi "${challenge.type}" pada halaman ini!\nKamu WAJIB segera memanggil tool 'browser-ask-user' agar user dapat menyelesaikan verifikasi manusia secara langsung di jendela browser.\n\n${result}`
726
+ }
727
+
728
+ return result
729
+ }
730
+
731
+ export async function executeAction(data, sessionId = 'default') {
732
+ const session = await getOrCreateSession(sessionId)
733
+ const { action, id, value, direction } = data
734
+
735
+ if (action === 'click') {
736
+ const selector = `[data-mark-id="${id}"]`
737
+ await session.page.waitForSelector(selector, { timeout: 8000 })
738
+ await session.page.click(selector)
739
+ await new Promise((r) => setTimeout(r, 1000))
740
+ await broadcastPreview(session)
741
+ return `Berhasil klik elemen [${id}]. Gunakan 'browser-read' untuk melihat perubahan halaman.`
742
+ }
743
+
744
+ if (action === 'type') {
745
+ const selector = `[data-mark-id="${id}"]`
746
+ await session.page.waitForSelector(selector, { timeout: 8000 })
747
+ await session.page.click(selector)
748
+ await session.page.type(selector, value || '', { delay: 30 })
749
+ await broadcastPreview(session)
750
+ return `Berhasil mengetik "${value}" pada elemen [${id}].`
751
+ }
752
+
753
+ if (action === 'scroll') {
754
+ const delta = direction === 'up' ? -500 : 500
755
+ await session.page.evaluate((d) => window.scrollBy({ top: d, behavior: 'smooth' }), delta)
756
+ await new Promise((r) => setTimeout(r, 600))
757
+ await broadcastPreview(session)
758
+ return `Berhasil scroll ${direction}. Gunakan 'browser-read' untuk membaca elemen terbaru.`
759
+ }
760
+
761
+ if (action === 'unblock') {
762
+ // 1. Tampilkan jendela browser ke layar aktif agar user bisa melihat dan menyelesaikan captcha/login
763
+ await showBrowserWindow(sessionId)
764
+
765
+ // 2. Ubah blocker menjadi unblock prompt widget di kanan bawah
766
+ const promptMsg =
767
+ value || 'Silakan selesaikan CAPTCHA atau login pada halaman ini, lalu klik tombol di bawah.'
768
+ await session.page.evaluate((msg) => {
769
+ let blocker = document.getElementById('mark-user-blocker')
770
+ if (!blocker) {
771
+ blocker = document.createElement('div')
772
+ blocker.id = 'mark-user-blocker'
773
+ document.documentElement.appendChild(blocker)
774
+ }
775
+
776
+ blocker.dataset.mode = 'unblock'
777
+ blocker.style.cssText = `
778
+ position: fixed !important;
779
+ bottom: 24px !important;
780
+ right: 24px !important;
781
+ top: auto !important;
782
+ left: auto !important;
783
+ width: auto !important;
784
+ height: auto !important;
785
+ z-index: 2147483647 !important;
786
+ pointer-events: none !important;
787
+ cursor: default !important;
788
+ display: flex !important;
789
+ justify-content: flex-end !important;
790
+ align-items: flex-end !important;
791
+ background: transparent !important;
792
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
793
+ `
794
+
795
+ blocker.innerHTML = `
796
+ <div style="
797
+ background: rgba(15, 23, 21, 0.95);
798
+ backdrop-filter: blur(16px);
799
+ -webkit-backdrop-filter: blur(16px);
800
+ border: 1px solid rgba(31, 184, 84, 0.5);
801
+ border-radius: 16px;
802
+ padding: 20px 22px;
803
+ width: 350px;
804
+ box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.8), 0 0 20px rgba(31, 184, 84, 0.25);
805
+ color: #f1f5f9;
806
+ box-sizing: border-box;
807
+ pointer-events: auto !important;
808
+ animation: mark-pulse 3s ease-in-out infinite;
809
+ ">
810
+ <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px;">
811
+ <div style="width: 10px; height: 10px; border-radius: 50%; background: #1fb854; box-shadow: 0 0 8px #1fb854;"></div>
812
+ <div style="font-weight: 700; color: #f8fafc; font-size: 15px; letter-spacing: 0.2px;">Mark Paused for Input</div>
813
+ </div>
814
+ <div style="font-size: 13px; line-height: 1.5; color: #cbd5e1; margin-bottom: 14px; word-break: break-word;">
815
+ ${msg}
816
+ </div>
817
+ <input type="text" id="mark-user-input" placeholder="Komentar untuk Mark (opsional)..." style="
818
+ width: 100%;
819
+ padding: 10px 14px;
820
+ background: rgba(25, 54, 45, 0.6);
821
+ border: 1px solid rgba(31, 184, 84, 0.3);
822
+ border-radius: 10px;
823
+ color: #f8fafc;
824
+ font-size: 13px;
825
+ margin-bottom: 12px;
826
+ box-sizing: border-box;
827
+ outline: none;
828
+ transition: border-color 0.2s;
829
+ " />
830
+ <button id="mark-btn-selesai" style="
831
+ width: 100%;
832
+ padding: 11px 16px;
833
+ background: #1fb854;
834
+ color: #0f1715;
835
+ border: none;
836
+ border-radius: 10px;
837
+ font-weight: 700;
838
+ font-size: 13px;
839
+ cursor: pointer;
840
+ transition: all 0.2s ease;
841
+ box-shadow: 0 4px 12px rgba(31, 184, 84, 0.3);
842
+ ">Lanjutkan Otomasi (Resume)</button>
843
+ </div>
844
+ `
845
+
846
+ // 3. Setup listener click dan Enter
847
+ const btn = document.getElementById('mark-btn-selesai')
848
+ const input = document.getElementById('mark-user-input')
849
+
850
+ const submit = () => {
851
+ const comment = input ? input.value : ''
852
+ btn.innerText = 'Melanjutkan...'
853
+ btn.style.opacity = '0.7'
854
+ btn.disabled = true
855
+ document.title = 'MARK_UNBLOCK_DONE:' + (comment.trim() || 'Sudah selesai.')
856
+ }
857
+
858
+ if (btn) btn.onclick = submit
859
+ if (input) {
860
+ input.onkeydown = (e) => {
861
+ if (e.key === 'Enter') submit()
862
+ }
863
+ setTimeout(() => input.focus(), 300)
864
+ }
865
+ }, promptMsg)
866
+
867
+ await broadcastPreview(session)
868
+
869
+ // 4. Tunggu respons pengguna via perubahan document.title di page
870
+ const userFeedback = await new Promise((resolve) => {
871
+ let resolved = false
872
+ const timeoutId = setTimeout(() => {
873
+ if (!resolved) {
874
+ resolved = true
875
+ session.page.off('titlechanged', titleListener)
876
+ resolve('Waktu tunggu intervensi user habis (600 detik).')
877
+ }
878
+ }, 600000) // 10 menit timeout
879
+
880
+ const titleListener = (newTitle) => {
881
+ if (newTitle && newTitle.startsWith('MARK_UNBLOCK_DONE:')) {
882
+ if (!resolved) {
883
+ resolved = true
884
+ clearTimeout(timeoutId)
885
+ session.page.off('titlechanged', titleListener)
886
+ const userText = newTitle.replace('MARK_UNBLOCK_DONE:', '').trim()
887
+ resolve(userText || 'Sudah selesai.')
888
+ }
889
+ }
890
+ }
891
+
892
+ session.page.on('titlechanged', titleListener)
893
+ })
894
+
895
+ // 5. Kembalikan blocker ke mode Fullscreen working indicator
896
+ await session.page.evaluate(() => {
897
+ let blocker = document.getElementById('mark-user-blocker')
898
+ if (blocker) {
899
+ blocker.dataset.mode = 'working'
900
+ blocker.style.cssText = `
901
+ position: fixed !important;
902
+ top: 0 !important;
903
+ left: 0 !important;
904
+ width: 100vw !important;
905
+ height: 100vh !important;
906
+ z-index: 2147483647 !important;
907
+ pointer-events: auto !important;
908
+ cursor: not-allowed !important;
909
+ display: flex !important;
910
+ justify-content: center !important;
911
+ align-items: flex-start !important;
912
+ padding-top: 24px !important;
913
+ box-sizing: border-box !important;
914
+ background: transparent !important;
915
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
916
+ `
917
+ blocker.innerHTML = `
918
+ <div style="
919
+ background: rgba(25, 54, 45, 0.92);
920
+ backdrop-filter: blur(8px);
921
+ -webkit-backdrop-filter: blur(8px);
922
+ border: 1px solid rgba(31, 184, 84, 0.4);
923
+ border-radius: 30px;
924
+ padding: 10px 22px;
925
+ display: flex;
926
+ align-items: center;
927
+ gap: 12px;
928
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 0 15px rgba(31, 184, 84, 0.2);
929
+ color: #f1f5f9;
930
+ font-size: 13px;
931
+ font-weight: 500;
932
+ letter-spacing: 0.2px;
933
+ user-select: none;
934
+ pointer-events: auto;
935
+ ">
936
+ <svg style="animation: mark-spin 1s linear infinite; width: 16px; height: 16px; color: #1fb854;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
937
+ <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
938
+ </svg>
939
+ <span class="mark-pulse" style="color: #f8fafc;">Mark is working...</span>
940
+ </div>
941
+ `
942
+ }
943
+ })
944
+
945
+ // 6. Baca ulang DOM setelah user selesai
946
+ const updatedDOM = await readDOM(sessionId)
947
+ return `[USER SELESAI INTERVENSI]: Catatan user: "${userFeedback}".\nBerikut kondisi DOM terbaru setelah intervensi:\n${updatedDOM}`
948
+ }
949
+
950
+ return `Aksi ${action} tidak dikenali.`
951
+ }
952
+
953
+ export async function executeScript(code, sessionId = 'default') {
954
+ const session = await getOrCreateSession(sessionId)
955
+ const res = await session.page.evaluate(code)
956
+ await broadcastPreview(session)
957
+ return typeof res === 'string' ? res : JSON.stringify(res)
958
+ }
959
+
960
+ export async function extractData(selector, sessionId = 'default') {
961
+ const session = await getOrCreateSession(sessionId)
962
+ const text = await session.page.$$eval(selector, (els) =>
963
+ els.map((e) => e.innerText.trim()).filter(Boolean)
964
+ )
965
+ return JSON.stringify(text)
966
+ }
967
+
968
+ export async function takeScreenshot(filename = 'screenshot.png', sessionId = 'default') {
969
+ const session = await getOrCreateSession(sessionId)
970
+ const outPath = path.isAbsolute(filename)
971
+ ? filename
972
+ : path.join(os.homedir(), 'Desktop', filename)
973
+ const buffer = await session.page.screenshot({ path: outPath, fullPage: false })
974
+ const base64 = buffer ? buffer.toString('base64') : ''
975
+ const dataUrl = base64 ? `data:image/png;base64,${base64}` : null
976
+ return {
977
+ success: true,
978
+ message: `Screenshot berhasil disimpan ke ${outPath}`,
979
+ path: outPath,
980
+ dataUrl
981
+ }
982
+ }
983
+
984
+ export async function downloadFile(url, filename, sessionId = 'default') {
985
+ const session = await getOrCreateSession(sessionId)
986
+ const outPath = path.join(os.homedir(), 'Downloads', filename)
987
+ const viewSource = await session.page.goto(url)
988
+ fs.writeFileSync(outPath, await viewSource.buffer())
989
+ return `File berhasil diunduh ke ${outPath}`
990
+ }
991
+
992
+ export async function closeBrowser(sessionId = 'default') {
993
+ const session = sessions.get(sessionId)
994
+ if (session && session.browser) {
995
+ if (session.idleTimeout) clearTimeout(session.idleTimeout)
996
+ try {
997
+ await session.browser.close()
998
+ } catch (_) {}
999
+ sessions.delete(sessionId)
1000
+
1001
+ if (wsHub) {
1002
+ wsHub.broadcast('browser:preview', {
1003
+ sessionId,
1004
+ closed: true
1005
+ })
1006
+ }
1007
+
1008
+ return `Browser session '${sessionId}' berhasil ditutup.`
1009
+ }
1010
+ return `Browser session '${sessionId}' tidak sedang berjalan.`
1011
+ }
1012
+
1013
+ export function forceProcessWindowToForeground(pid) {
1014
+ if (os.platform() !== 'win32' || !pid) return Promise.resolve()
1015
+ return new Promise((resolve) => {
1016
+ const psScript = `
1017
+ $code = @'
1018
+ using System;
1019
+ using System.Runtime.InteropServices;
1020
+ public class Win32Focus {
1021
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
1022
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
1023
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
1024
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
1025
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
1026
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
1027
+ [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd);
1028
+ [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
1029
+ [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
1030
+ [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
1031
+ [DllImport("user32.dll")] public static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);
1032
+ [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);
1033
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
1034
+
1035
+ public static void FocusPid(int pid) {
1036
+ IntPtr targetHwnd = IntPtr.Zero;
1037
+ EnumWindows((hWnd, lParam) => {
1038
+ uint procId;
1039
+ GetWindowThreadProcessId(hWnd, out procId);
1040
+ if (procId == (uint)pid && IsWindowVisible(hWnd)) {
1041
+ targetHwnd = hWnd;
1042
+ return false;
1043
+ }
1044
+ return true;
1045
+ }, IntPtr.Zero);
1046
+ if (targetHwnd != IntPtr.Zero) {
1047
+ ShowWindow(targetHwnd, 9); // SW_RESTORE
1048
+ IntPtr fg = GetForegroundWindow();
1049
+ uint fgThread = GetWindowThreadProcessId(fg, out _);
1050
+ uint curThread = GetCurrentThreadId();
1051
+ uint targetThread = GetWindowThreadProcessId(targetHwnd, out _);
1052
+ if (fgThread != targetThread) {
1053
+ AttachThreadInput(fgThread, targetThread, true);
1054
+ AttachThreadInput(curThread, targetThread, true);
1055
+ }
1056
+ keybd_event(0, 0, 0, 0);
1057
+ SetWindowPos(targetHwnd, new IntPtr(-1), 0, 0, 0, 0, 0x0001 | 0x0002 | 0x0040); // HWND_TOPMOST
1058
+ SetWindowPos(targetHwnd, new IntPtr(-2), 0, 0, 0, 0, 0x0001 | 0x0002 | 0x0040); // HWND_NOTOPMOST
1059
+ BringWindowToTop(targetHwnd);
1060
+ SetForegroundWindow(targetHwnd);
1061
+ SwitchToThisWindow(targetHwnd, true);
1062
+ if (fgThread != targetThread) {
1063
+ AttachThreadInput(fgThread, targetThread, false);
1064
+ AttachThreadInput(curThread, targetThread, false);
1065
+ }
1066
+ }
1067
+ }
1068
+ }
1069
+ '@
1070
+ Add-Type -TypeDefinition $code -Language CSharp
1071
+ [Win32Focus]::FocusPid(${pid})
1072
+ `
1073
+ const enc = Buffer.from(psScript, 'utf16le').toString('base64')
1074
+ exec(`powershell.exe -NoProfile -NonInteractive -EncodedCommand ${enc}`, () => resolve())
1075
+ })
1076
+ }
1077
+
1078
+ export async function showBrowserWindow(sessionId = 'default') {
1079
+ const session = sessions.get(sessionId)
1080
+ if (session && session.page) {
1081
+ try {
1082
+ const client = await session.page.target().createCDPSession()
1083
+ const { windowId } = await client.send('Browser.getWindowForTarget')
1084
+ await client.send('Browser.setWindowBounds', {
1085
+ windowId,
1086
+ bounds: { windowState: 'normal', left: 80, top: 60, width: 1280, height: 800 }
1087
+ })
1088
+ await session.page.bringToFront()
1089
+ session.isForeground = true
1090
+
1091
+ // Bypass Windows Foreground Lock dan paksa window naik ke paling atas
1092
+ const pid = session.browser?.process()?.pid
1093
+ if (pid) {
1094
+ await forceProcessWindowToForeground(pid)
1095
+ }
1096
+
1097
+ return `Berhasil menampilkan jendela browser untuk sesi '${sessionId}'.`
1098
+ } catch (_) {
1099
+ try {
1100
+ await session.page.bringToFront()
1101
+ session.isForeground = true
1102
+ const pid = session.browser?.process()?.pid
1103
+ if (pid) {
1104
+ await forceProcessWindowToForeground(pid)
1105
+ }
1106
+ return `Berhasil membawa browser sesi '${sessionId}' ke depan.`
1107
+ } catch (err) {
1108
+ return `Gagal menampilkan jendela browser: ${err.message}`
1109
+ }
1110
+ }
1111
+ }
1112
+ return `Sesi browser '${sessionId}' tidak ditemukan atau sedang tidak aktif.`
1113
+ }
1114
+
1115
+ export async function hideBrowserWindow(sessionId = 'default') {
1116
+ const session = sessions.get(sessionId)
1117
+ if (session && session.page) {
1118
+ try {
1119
+ const client = await session.page.target().createCDPSession()
1120
+ const { windowId } = await client.send('Browser.getWindowForTarget')
1121
+ await client.send('Browser.setWindowBounds', {
1122
+ windowId,
1123
+ bounds: { windowState: 'minimized' }
1124
+ })
1125
+ session.isForeground = false
1126
+ return `Berhasil meminimalkan jendela browser sesi '${sessionId}' ke latar belakang.`
1127
+ } catch (err) {
1128
+ return `Gagal menyembunyikan jendela browser: ${err.message}`
1129
+ }
1130
+ }
1131
+ return `Sesi browser '${sessionId}' tidak ditemukan atau sedang tidak aktif.`
1132
+ }