@serjm/deepseek-code 0.3.1

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 (216) hide show
  1. package/CONTRIBUTING.md +73 -0
  2. package/README.md +194 -0
  3. package/README.ru.md +194 -0
  4. package/dist/api/index.d.ts +77 -0
  5. package/dist/api/index.d.ts.map +1 -0
  6. package/dist/api/index.js +263 -0
  7. package/dist/api/index.js.map +1 -0
  8. package/dist/cli/headless.d.ts +22 -0
  9. package/dist/cli/headless.d.ts.map +1 -0
  10. package/dist/cli/headless.js +122 -0
  11. package/dist/cli/headless.js.map +1 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +90 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/cli/interactive.d.ts +18 -0
  17. package/dist/cli/interactive.d.ts.map +1 -0
  18. package/dist/cli/interactive.js +75 -0
  19. package/dist/cli/interactive.js.map +1 -0
  20. package/dist/commands/index.d.ts +30 -0
  21. package/dist/commands/index.d.ts.map +1 -0
  22. package/dist/commands/index.js +964 -0
  23. package/dist/commands/index.js.map +1 -0
  24. package/dist/config/defaults.d.ts +37 -0
  25. package/dist/config/defaults.d.ts.map +1 -0
  26. package/dist/config/defaults.js +39 -0
  27. package/dist/config/defaults.js.map +1 -0
  28. package/dist/config/loader.d.ts +4 -0
  29. package/dist/config/loader.d.ts.map +1 -0
  30. package/dist/config/loader.js +76 -0
  31. package/dist/config/loader.js.map +1 -0
  32. package/dist/core/agent-loop.d.ts +111 -0
  33. package/dist/core/agent-loop.d.ts.map +1 -0
  34. package/dist/core/agent-loop.js +485 -0
  35. package/dist/core/agent-loop.js.map +1 -0
  36. package/dist/core/checkpoint.d.ts +10 -0
  37. package/dist/core/checkpoint.d.ts.map +1 -0
  38. package/dist/core/checkpoint.js +83 -0
  39. package/dist/core/checkpoint.js.map +1 -0
  40. package/dist/core/extensions.d.ts +55 -0
  41. package/dist/core/extensions.d.ts.map +1 -0
  42. package/dist/core/extensions.js +113 -0
  43. package/dist/core/extensions.js.map +1 -0
  44. package/dist/core/git.d.ts +68 -0
  45. package/dist/core/git.d.ts.map +1 -0
  46. package/dist/core/git.js +148 -0
  47. package/dist/core/git.js.map +1 -0
  48. package/dist/core/hooks.d.ts +37 -0
  49. package/dist/core/hooks.d.ts.map +1 -0
  50. package/dist/core/hooks.js +77 -0
  51. package/dist/core/hooks.js.map +1 -0
  52. package/dist/core/i18n.d.ts +90 -0
  53. package/dist/core/i18n.d.ts.map +1 -0
  54. package/dist/core/i18n.js +253 -0
  55. package/dist/core/i18n.js.map +1 -0
  56. package/dist/core/lsp.d.ts +74 -0
  57. package/dist/core/lsp.d.ts.map +1 -0
  58. package/dist/core/lsp.js +239 -0
  59. package/dist/core/lsp.js.map +1 -0
  60. package/dist/core/mcp.d.ts +49 -0
  61. package/dist/core/mcp.d.ts.map +1 -0
  62. package/dist/core/mcp.js +195 -0
  63. package/dist/core/mcp.js.map +1 -0
  64. package/dist/core/memory.d.ts +38 -0
  65. package/dist/core/memory.d.ts.map +1 -0
  66. package/dist/core/memory.js +231 -0
  67. package/dist/core/memory.js.map +1 -0
  68. package/dist/core/metrics.d.ts +36 -0
  69. package/dist/core/metrics.d.ts.map +1 -0
  70. package/dist/core/metrics.js +111 -0
  71. package/dist/core/metrics.js.map +1 -0
  72. package/dist/core/review.d.ts +27 -0
  73. package/dist/core/review.d.ts.map +1 -0
  74. package/dist/core/review.js +201 -0
  75. package/dist/core/review.js.map +1 -0
  76. package/dist/core/sandbox.d.ts +52 -0
  77. package/dist/core/sandbox.d.ts.map +1 -0
  78. package/dist/core/sandbox.js +140 -0
  79. package/dist/core/sandbox.js.map +1 -0
  80. package/dist/core/scheduler.d.ts +56 -0
  81. package/dist/core/scheduler.d.ts.map +1 -0
  82. package/dist/core/scheduler.js +167 -0
  83. package/dist/core/scheduler.js.map +1 -0
  84. package/dist/core/session.d.ts +49 -0
  85. package/dist/core/session.d.ts.map +1 -0
  86. package/dist/core/session.js +127 -0
  87. package/dist/core/session.js.map +1 -0
  88. package/dist/core/skills.d.ts +36 -0
  89. package/dist/core/skills.d.ts.map +1 -0
  90. package/dist/core/skills.js +90 -0
  91. package/dist/core/skills.js.map +1 -0
  92. package/dist/core/subagent.d.ts +45 -0
  93. package/dist/core/subagent.d.ts.map +1 -0
  94. package/dist/core/subagent.js +130 -0
  95. package/dist/core/subagent.js.map +1 -0
  96. package/dist/core/themes.d.ts +35 -0
  97. package/dist/core/themes.d.ts.map +1 -0
  98. package/dist/core/themes.js +188 -0
  99. package/dist/core/themes.js.map +1 -0
  100. package/dist/tools/bash.d.ts +3 -0
  101. package/dist/tools/bash.d.ts.map +1 -0
  102. package/dist/tools/bash.js +92 -0
  103. package/dist/tools/bash.js.map +1 -0
  104. package/dist/tools/chrome-manager.d.ts +35 -0
  105. package/dist/tools/chrome-manager.d.ts.map +1 -0
  106. package/dist/tools/chrome-manager.js +163 -0
  107. package/dist/tools/chrome-manager.js.map +1 -0
  108. package/dist/tools/chrome.d.ts +78 -0
  109. package/dist/tools/chrome.d.ts.map +1 -0
  110. package/dist/tools/chrome.js +1058 -0
  111. package/dist/tools/chrome.js.map +1 -0
  112. package/dist/tools/edit.d.ts +3 -0
  113. package/dist/tools/edit.d.ts.map +1 -0
  114. package/dist/tools/edit.js +81 -0
  115. package/dist/tools/edit.js.map +1 -0
  116. package/dist/tools/glob.d.ts +3 -0
  117. package/dist/tools/glob.d.ts.map +1 -0
  118. package/dist/tools/glob.js +41 -0
  119. package/dist/tools/glob.js.map +1 -0
  120. package/dist/tools/grep.d.ts +3 -0
  121. package/dist/tools/grep.d.ts.map +1 -0
  122. package/dist/tools/grep.js +74 -0
  123. package/dist/tools/grep.js.map +1 -0
  124. package/dist/tools/path-safety.d.ts +3 -0
  125. package/dist/tools/path-safety.d.ts.map +1 -0
  126. package/dist/tools/path-safety.js +19 -0
  127. package/dist/tools/path-safety.js.map +1 -0
  128. package/dist/tools/read.d.ts +3 -0
  129. package/dist/tools/read.d.ts.map +1 -0
  130. package/dist/tools/read.js +58 -0
  131. package/dist/tools/read.js.map +1 -0
  132. package/dist/tools/registry.d.ts +4 -0
  133. package/dist/tools/registry.d.ts.map +1 -0
  134. package/dist/tools/registry.js +43 -0
  135. package/dist/tools/registry.js.map +1 -0
  136. package/dist/tools/types.d.ts +47 -0
  137. package/dist/tools/types.d.ts.map +1 -0
  138. package/dist/tools/types.js +90 -0
  139. package/dist/tools/types.js.map +1 -0
  140. package/dist/tools/write.d.ts +3 -0
  141. package/dist/tools/write.d.ts.map +1 -0
  142. package/dist/tools/write.js +51 -0
  143. package/dist/tools/write.js.map +1 -0
  144. package/dist/ui/activity-cards.d.ts +50 -0
  145. package/dist/ui/activity-cards.d.ts.map +1 -0
  146. package/dist/ui/activity-cards.js +185 -0
  147. package/dist/ui/activity-cards.js.map +1 -0
  148. package/dist/ui/app.d.ts +9 -0
  149. package/dist/ui/app.d.ts.map +1 -0
  150. package/dist/ui/app.js +852 -0
  151. package/dist/ui/app.js.map +1 -0
  152. package/dist/ui/chat-view.d.ts +10 -0
  153. package/dist/ui/chat-view.d.ts.map +1 -0
  154. package/dist/ui/chat-view.js +94 -0
  155. package/dist/ui/chat-view.js.map +1 -0
  156. package/dist/ui/error-boundary.d.ts +13 -0
  157. package/dist/ui/error-boundary.d.ts.map +1 -0
  158. package/dist/ui/error-boundary.js +16 -0
  159. package/dist/ui/error-boundary.js.map +1 -0
  160. package/dist/ui/fade-in.d.ts +8 -0
  161. package/dist/ui/fade-in.d.ts.map +1 -0
  162. package/dist/ui/fade-in.js +14 -0
  163. package/dist/ui/fade-in.js.map +1 -0
  164. package/dist/ui/input-bar.d.ts +16 -0
  165. package/dist/ui/input-bar.d.ts.map +1 -0
  166. package/dist/ui/input-bar.js +269 -0
  167. package/dist/ui/input-bar.js.map +1 -0
  168. package/dist/ui/markdown-view.d.ts +9 -0
  169. package/dist/ui/markdown-view.d.ts.map +1 -0
  170. package/dist/ui/markdown-view.js +240 -0
  171. package/dist/ui/markdown-view.js.map +1 -0
  172. package/dist/ui/matrix-rain.d.ts +2 -0
  173. package/dist/ui/matrix-rain.d.ts.map +1 -0
  174. package/dist/ui/matrix-rain.js +134 -0
  175. package/dist/ui/matrix-rain.js.map +1 -0
  176. package/dist/ui/reasoning-view.d.ts +12 -0
  177. package/dist/ui/reasoning-view.d.ts.map +1 -0
  178. package/dist/ui/reasoning-view.js +34 -0
  179. package/dist/ui/reasoning-view.js.map +1 -0
  180. package/dist/ui/results-panel.d.ts +11 -0
  181. package/dist/ui/results-panel.d.ts.map +1 -0
  182. package/dist/ui/results-panel.js +17 -0
  183. package/dist/ui/results-panel.js.map +1 -0
  184. package/dist/ui/setup-wizard.d.ts +30 -0
  185. package/dist/ui/setup-wizard.d.ts.map +1 -0
  186. package/dist/ui/setup-wizard.js +166 -0
  187. package/dist/ui/setup-wizard.js.map +1 -0
  188. package/dist/ui/status-bar.d.ts +14 -0
  189. package/dist/ui/status-bar.d.ts.map +1 -0
  190. package/dist/ui/status-bar.js +63 -0
  191. package/dist/ui/status-bar.js.map +1 -0
  192. package/dist/ui/tool-activity-card.d.ts +9 -0
  193. package/dist/ui/tool-activity-card.d.ts.map +1 -0
  194. package/dist/ui/tool-activity-card.js +172 -0
  195. package/dist/ui/tool-activity-card.js.map +1 -0
  196. package/dist/ui/tool-call-view.d.ts +9 -0
  197. package/dist/ui/tool-call-view.d.ts.map +1 -0
  198. package/dist/ui/tool-call-view.js +149 -0
  199. package/dist/ui/tool-call-view.js.map +1 -0
  200. package/dist/utils/clipboard.d.ts +6 -0
  201. package/dist/utils/clipboard.d.ts.map +1 -0
  202. package/dist/utils/clipboard.js +56 -0
  203. package/dist/utils/clipboard.js.map +1 -0
  204. package/dist/utils/ignore.d.ts +6 -0
  205. package/dist/utils/ignore.d.ts.map +1 -0
  206. package/dist/utils/ignore.js +40 -0
  207. package/dist/utils/ignore.js.map +1 -0
  208. package/dist/utils/logger.d.ts +4 -0
  209. package/dist/utils/logger.d.ts.map +1 -0
  210. package/dist/utils/logger.js +13 -0
  211. package/dist/utils/logger.js.map +1 -0
  212. package/dist/utils/string-width.d.ts +6 -0
  213. package/dist/utils/string-width.d.ts.map +1 -0
  214. package/dist/utils/string-width.js +37 -0
  215. package/dist/utils/string-width.js.map +1 -0
  216. package/package.json +68 -0
@@ -0,0 +1,1058 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { createServer } from 'node:http';
3
+ import { dirname, isAbsolute, join } from 'node:path';
4
+ import { chromeManager } from './chrome-manager.js';
5
+ const CHROME_ACTIONS = [
6
+ 'open',
7
+ 'click',
8
+ 'fill',
9
+ 'eval',
10
+ 'text',
11
+ 'html',
12
+ 'console',
13
+ 'network',
14
+ 'state',
15
+ 'shot',
16
+ 'nav',
17
+ 'wait',
18
+ 'scroll',
19
+ 'locator',
20
+ 'cookies',
21
+ 'storage',
22
+ 'quiz',
23
+ ];
24
+ function isChromeAction(action) {
25
+ return typeof action === 'string' && CHROME_ACTIONS.includes(action);
26
+ }
27
+ async function waitForElement(page, selector, timeout = 10000) {
28
+ await page.waitForSelector(selector, {
29
+ visible: true,
30
+ timeout,
31
+ });
32
+ }
33
+ function formatEvalResult(result) {
34
+ if (result === null || result === undefined)
35
+ return 'undefined';
36
+ if (typeof result === 'string')
37
+ return result;
38
+ if (typeof result === 'object') {
39
+ try {
40
+ return JSON.stringify(result, null, 2);
41
+ }
42
+ catch {
43
+ return String(result);
44
+ }
45
+ }
46
+ return String(result);
47
+ }
48
+ function sleep(ms) {
49
+ return new Promise(resolve => setTimeout(resolve, ms));
50
+ }
51
+ async function resolveArtifactPath(output) {
52
+ const artifactDir = join(process.cwd(), '.deepseek-code', 'artifacts', 'browser');
53
+ const targetPath = output
54
+ ? (isAbsolute(output) ? output : join(process.cwd(), output))
55
+ : join(artifactDir, `chrome-shot-${Date.now()}.png`);
56
+ await mkdir(dirname(targetPath), { recursive: true });
57
+ return targetPath;
58
+ }
59
+ async function navigateIfNeeded(args, page) {
60
+ if (args.url) {
61
+ await chromeManager.navigate(args.url, args.sameTab ?? false);
62
+ return;
63
+ }
64
+ if (page.url() === 'about:blank') {
65
+ throw new Error('URL is required when no page is open yet');
66
+ }
67
+ }
68
+ async function collectConsoleMessages(page, args) {
69
+ const logs = [];
70
+ const errors = [];
71
+ const handleConsole = (msg) => {
72
+ const text = msg.text();
73
+ if (msg.type() === 'error') {
74
+ errors.push(text);
75
+ }
76
+ else {
77
+ logs.push(text);
78
+ }
79
+ };
80
+ page.on('console', handleConsole);
81
+ try {
82
+ if (args.url) {
83
+ await page.goto(args.url, { waitUntil: 'domcontentloaded', timeout: args.timeout ?? 30000 });
84
+ }
85
+ await sleep(2000);
86
+ }
87
+ finally {
88
+ page.off('console', handleConsole);
89
+ }
90
+ if (args.error ?? false) {
91
+ return {
92
+ success: true,
93
+ output: errors.length > 0
94
+ ? `Console errors:\n${errors.join('\n')}`
95
+ : 'No console errors found',
96
+ };
97
+ }
98
+ const parts = [];
99
+ if ((args.all ?? false) || logs.length > 0) {
100
+ parts.push(`Console logs (${logs.length}):\n${logs.join('\n')}`);
101
+ }
102
+ if (errors.length > 0) {
103
+ parts.push(`Console errors (${errors.length}):\n${errors.join('\n')}`);
104
+ }
105
+ return {
106
+ success: true,
107
+ output: parts.length > 0 ? parts.join('\n\n') : 'No console messages',
108
+ };
109
+ }
110
+ async function collectNetworkRequests(page, args) {
111
+ const requests = [];
112
+ const handleRequest = (req) => {
113
+ requests.push({
114
+ url: req.url(),
115
+ method: req.method(),
116
+ type: req.resourceType(),
117
+ });
118
+ };
119
+ page.on('request', handleRequest);
120
+ try {
121
+ if (args.url) {
122
+ await page.goto(args.url, { waitUntil: 'domcontentloaded', timeout: args.timeout ?? 30000 });
123
+ }
124
+ await sleep(2000);
125
+ }
126
+ finally {
127
+ page.off('request', handleRequest);
128
+ }
129
+ const filtered = (args.api ?? false)
130
+ ? requests.filter(r => r.type === 'xhr' || r.type === 'fetch')
131
+ : requests;
132
+ if (filtered.length === 0) {
133
+ return { success: true, output: 'No network requests captured' };
134
+ }
135
+ const lines = filtered.map((r, i) => `${i + 1}. [${r.method}] ${r.type}: ${r.url}`);
136
+ return {
137
+ success: true,
138
+ output: `Network requests (${filtered.length}):\n${lines.join('\n')}`,
139
+ };
140
+ }
141
+ async function executeAction(args) {
142
+ if (!isChromeAction(args.action)) {
143
+ return { success: false, output: '', error: `Unknown action: ${String(args.action)}` };
144
+ }
145
+ if (args.action === 'state') {
146
+ return {
147
+ success: true,
148
+ output: JSON.stringify(chromeManager.getState(), null, 2),
149
+ };
150
+ }
151
+ const timeout = args.timeout ?? 10000;
152
+ const sameTab = args.sameTab ?? true;
153
+ if (args.headless !== undefined) {
154
+ await chromeManager.ensureMode(args.headless);
155
+ }
156
+ const page = await chromeManager.getPage(sameTab);
157
+ try {
158
+ switch (args.action) {
159
+ case 'open': {
160
+ if (!args.url)
161
+ return { success: false, output: '', error: 'URL is required for open action' };
162
+ await page.goto(args.url, { waitUntil: 'domcontentloaded', timeout });
163
+ return { success: true, output: `Opened: ${page.url()}` };
164
+ }
165
+ case 'click': {
166
+ if (!args.selector)
167
+ return { success: false, output: '', error: 'Selector is required for click action' };
168
+ await navigateIfNeeded(args, page);
169
+ await waitForElement(page, args.selector, timeout);
170
+ await page.click(args.selector);
171
+ await page.waitForNetworkIdle({ idleTime: 500, timeout }).catch(() => { });
172
+ return { success: true, output: `Clicked: ${args.selector}` };
173
+ }
174
+ case 'fill': {
175
+ if (!args.selector || args.text === undefined) {
176
+ return { success: false, output: '', error: 'Selector and text are required for fill action' };
177
+ }
178
+ await navigateIfNeeded(args, page);
179
+ await waitForElement(page, args.selector, timeout);
180
+ await page.click(args.selector);
181
+ await page.keyboard.down('Control');
182
+ await page.keyboard.press('a');
183
+ await page.keyboard.up('Control');
184
+ await page.keyboard.press('Backspace');
185
+ await page.type(args.selector, args.text, { delay: 10 });
186
+ return { success: true, output: `Filled "${args.text}" into: ${args.selector}` };
187
+ }
188
+ case 'eval': {
189
+ if (!args.code)
190
+ return { success: false, output: '', error: 'Code is required for eval action' };
191
+ await navigateIfNeeded(args, page);
192
+ const result = await page.evaluate(args.code);
193
+ return { success: true, output: formatEvalResult(result) };
194
+ }
195
+ case 'text': {
196
+ await navigateIfNeeded(args, page);
197
+ if (args.selector) {
198
+ const el = await page.$(args.selector);
199
+ if (!el)
200
+ return { success: false, output: '', error: `Element not found: ${args.selector}` };
201
+ const textContent = await page.evaluate(elm => elm.textContent ?? '', el);
202
+ return { success: true, output: textContent.trim() };
203
+ }
204
+ const textContent = await page.evaluate(() => document.body?.innerText ?? '');
205
+ return { success: true, output: textContent.trim() };
206
+ }
207
+ case 'html': {
208
+ await navigateIfNeeded(args, page);
209
+ return { success: true, output: await page.content() };
210
+ }
211
+ case 'console':
212
+ return collectConsoleMessages(page, args);
213
+ case 'network':
214
+ return collectNetworkRequests(page, args);
215
+ case 'shot': {
216
+ await navigateIfNeeded(args, page);
217
+ const shotPath = await resolveArtifactPath(args.output);
218
+ await page.screenshot({
219
+ path: shotPath,
220
+ fullPage: args.full ?? false,
221
+ });
222
+ return {
223
+ success: true,
224
+ output: `Screenshot saved: ${shotPath}\nPage: ${page.url()}`,
225
+ };
226
+ }
227
+ case 'nav': {
228
+ if (args.back) {
229
+ await page.goBack({ waitUntil: 'load', timeout });
230
+ return { success: true, output: `Navigated back to: ${page.url()}` };
231
+ }
232
+ if (args.forward) {
233
+ await page.goForward({ waitUntil: 'load', timeout });
234
+ return { success: true, output: `Navigated forward to: ${page.url()}` };
235
+ }
236
+ if (args.refresh ?? true) {
237
+ await page.reload({ waitUntil: 'load', timeout });
238
+ return { success: true, output: `Page refreshed: ${page.url()}` };
239
+ }
240
+ return { success: true, output: `Current URL: ${page.url()}` };
241
+ }
242
+ case 'wait': {
243
+ if (!args.selector)
244
+ return { success: false, output: '', error: 'Selector is required for wait action' };
245
+ await navigateIfNeeded(args, page);
246
+ if (args.hidden ?? false) {
247
+ await page.waitForSelector(args.selector, { hidden: true, timeout });
248
+ return { success: true, output: `Element hidden: ${args.selector}` };
249
+ }
250
+ await page.waitForSelector(args.selector, { visible: true, timeout });
251
+ return { success: true, output: `Element visible: ${args.selector}` };
252
+ }
253
+ case 'scroll': {
254
+ await navigateIfNeeded(args, page);
255
+ if (args.top) {
256
+ await page.evaluate(() => window.scrollTo(0, 0));
257
+ return { success: true, output: 'Scrolled to top' };
258
+ }
259
+ if (args.selector) {
260
+ await page.evaluate((selector) => {
261
+ const el = document.querySelector(selector);
262
+ el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
263
+ }, args.selector);
264
+ return { success: true, output: `Scrolled to element: ${args.selector}` };
265
+ }
266
+ if (args.bottom) {
267
+ await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
268
+ await sleep(500);
269
+ return { success: true, output: 'Scrolled to bottom' };
270
+ }
271
+ return { success: true, output: 'No scroll target specified' };
272
+ }
273
+ case 'locator': {
274
+ if (!args.selector)
275
+ return { success: false, output: '', error: 'Selector is required for locator action' };
276
+ await navigateIfNeeded(args, page);
277
+ const elements = await page.$$(args.selector);
278
+ if (args.count ?? false) {
279
+ return { success: true, output: `Found ${elements.length} elements matching: ${args.selector}` };
280
+ }
281
+ const results = [];
282
+ for (let i = 0; i < elements.length; i++) {
283
+ const el = elements[i];
284
+ const tagName = await page.evaluate(node => node.tagName.toLowerCase(), el);
285
+ const elText = await page.evaluate(node => node.innerText?.slice(0, 100) ?? '', el);
286
+ const elAttrs = await page.evaluate(node => {
287
+ const attrs = {};
288
+ for (const attr of node.attributes) {
289
+ attrs[attr.name] = attr.value;
290
+ }
291
+ return attrs;
292
+ }, el);
293
+ if (args.text && !elText.toLowerCase().includes(args.text.toLowerCase()))
294
+ continue;
295
+ if (args.attr && elAttrs[args.attr] === undefined)
296
+ continue;
297
+ const attrStr = Object.entries(elAttrs)
298
+ .map(([key, value]) => `${key}="${value}"`)
299
+ .join(' ');
300
+ results.push(`[${i}] <${tagName}${attrStr ? ` ${attrStr}` : ''}>${elText ? `\n Text: ${elText.trim().slice(0, 200)}` : ''}`);
301
+ }
302
+ if (results.length === 0) {
303
+ return { success: true, output: `No elements found matching: ${args.selector}` };
304
+ }
305
+ return {
306
+ success: true,
307
+ output: `Elements (${results.length}):\n${results.join('\n')}`,
308
+ };
309
+ }
310
+ case 'cookies': {
311
+ await navigateIfNeeded(args, page);
312
+ if (args.clear ?? false) {
313
+ const client = await page.target().createCDPSession();
314
+ await client.send('Network.clearBrowserCookies');
315
+ return { success: true, output: 'Cookies cleared' };
316
+ }
317
+ const cookies = await page.cookies();
318
+ if (args.name) {
319
+ const cookie = cookies.find(cookie => cookie.name === args.name);
320
+ if (!cookie)
321
+ return { success: false, output: '', error: `Cookie not found: ${args.name}` };
322
+ return { success: true, output: JSON.stringify(cookie, null, 2) };
323
+ }
324
+ if (cookies.length === 0) {
325
+ return { success: true, output: 'No cookies found' };
326
+ }
327
+ const lines = cookies.map(cookie => `${cookie.name}=${cookie.value} (domain: ${cookie.domain}, path: ${cookie.path})`);
328
+ return {
329
+ success: true,
330
+ output: `Cookies (${cookies.length}):\n${lines.join('\n')}`,
331
+ };
332
+ }
333
+ case 'storage': {
334
+ await navigateIfNeeded(args, page);
335
+ const result = {};
336
+ const readLocal = args.local ?? !args.session;
337
+ if (readLocal) {
338
+ const localData = await page.evaluate(() => {
339
+ const data = {};
340
+ for (let i = 0; i < localStorage.length; i++) {
341
+ const key = localStorage.key(i);
342
+ if (key)
343
+ data[key] = localStorage.getItem(key) ?? '';
344
+ }
345
+ return data;
346
+ });
347
+ result.localStorage = JSON.stringify(localData, null, 2);
348
+ }
349
+ if (args.session) {
350
+ const sessionData = await page.evaluate(() => {
351
+ const data = {};
352
+ for (let i = 0; i < sessionStorage.length; i++) {
353
+ const key = sessionStorage.key(i);
354
+ if (key)
355
+ data[key] = sessionStorage.getItem(key) ?? '';
356
+ }
357
+ return data;
358
+ });
359
+ result.sessionStorage = JSON.stringify(sessionData, null, 2);
360
+ }
361
+ return {
362
+ success: true,
363
+ output: Object.entries(result)
364
+ .map(([key, value]) => `${key}:\n${value}`)
365
+ .join('\n\n'),
366
+ };
367
+ }
368
+ case 'quiz': {
369
+ if (!args.url)
370
+ return { success: false, output: '', error: 'URL is required for quiz action' };
371
+ await chromeManager.navigate(args.url, sameTab);
372
+ const strategy = args.quizStrategy ?? 'first';
373
+ let answered = 0;
374
+ const questions = await page.$$('[class*="question"], [class*="quiz"], [class*="test"]');
375
+ for (const question of questions) {
376
+ const options = await question.$$('input[type="radio"], input[type="checkbox"], [class*="option"], [class*="answer"], li, button');
377
+ if (options.length === 0)
378
+ continue;
379
+ let targetIndex = 0;
380
+ if (strategy === 'random') {
381
+ targetIndex = Math.floor(Math.random() * options.length);
382
+ }
383
+ try {
384
+ await options[targetIndex].click();
385
+ answered++;
386
+ await sleep(300);
387
+ }
388
+ catch {
389
+ // Best-effort flow for generic quizzes.
390
+ }
391
+ }
392
+ return {
393
+ success: true,
394
+ output: `Quiz completed: ${answered} questions answered (strategy: ${strategy})`,
395
+ };
396
+ }
397
+ default:
398
+ return { success: false, output: '', error: `Unknown action: ${args.action}` };
399
+ }
400
+ }
401
+ catch (err) {
402
+ const message = err instanceof Error ? err.message : String(err);
403
+ return { success: false, output: '', error: `Chrome ${args.action} failed: ${message}` };
404
+ }
405
+ }
406
+ const chromeParameters = [
407
+ {
408
+ name: 'action',
409
+ type: 'string',
410
+ description: 'Browser action: open, click, fill, eval, text, html, console, network, state, shot, nav, wait, scroll, locator, cookies, storage, quiz',
411
+ required: true,
412
+ },
413
+ {
414
+ name: 'url',
415
+ type: 'string',
416
+ description: 'Target page URL for opening or navigation',
417
+ required: false,
418
+ },
419
+ {
420
+ name: 'selector',
421
+ type: 'string',
422
+ description: 'CSS selector for click, fill, text, wait, scroll, or locator',
423
+ required: false,
424
+ },
425
+ {
426
+ name: 'text',
427
+ type: 'string',
428
+ description: 'Text to fill into an input or text filter for locator',
429
+ required: false,
430
+ },
431
+ {
432
+ name: 'code',
433
+ type: 'string',
434
+ description: 'JavaScript expression or code to evaluate on the page',
435
+ required: false,
436
+ },
437
+ {
438
+ name: 'output',
439
+ type: 'string',
440
+ description: 'Screenshot output path for shot action',
441
+ required: false,
442
+ },
443
+ {
444
+ name: 'timeout',
445
+ type: 'number',
446
+ description: 'Timeout in milliseconds for wait-like actions',
447
+ required: false,
448
+ },
449
+ {
450
+ name: 'sameTab',
451
+ type: 'boolean',
452
+ description: 'Reuse the current tab for multi-step browser flows',
453
+ required: false,
454
+ },
455
+ {
456
+ name: 'hidden',
457
+ type: 'boolean',
458
+ description: 'Wait for the selector to become hidden instead of visible',
459
+ required: false,
460
+ },
461
+ {
462
+ name: 'error',
463
+ type: 'boolean',
464
+ description: 'Show only console errors',
465
+ required: false,
466
+ },
467
+ {
468
+ name: 'all',
469
+ type: 'boolean',
470
+ description: 'Show all console messages',
471
+ required: false,
472
+ },
473
+ {
474
+ name: 'api',
475
+ type: 'boolean',
476
+ description: 'Show only XHR/fetch network requests',
477
+ required: false,
478
+ },
479
+ {
480
+ name: 'local',
481
+ type: 'boolean',
482
+ description: 'Read localStorage',
483
+ required: false,
484
+ },
485
+ {
486
+ name: 'session',
487
+ type: 'boolean',
488
+ description: 'Read sessionStorage',
489
+ required: false,
490
+ },
491
+ {
492
+ name: 'clear',
493
+ type: 'boolean',
494
+ description: 'Clear cookies',
495
+ required: false,
496
+ },
497
+ {
498
+ name: 'full',
499
+ type: 'boolean',
500
+ description: 'Take a full-page screenshot',
501
+ required: false,
502
+ },
503
+ {
504
+ name: 'back',
505
+ type: 'boolean',
506
+ description: 'Navigate back in history',
507
+ required: false,
508
+ },
509
+ {
510
+ name: 'forward',
511
+ type: 'boolean',
512
+ description: 'Navigate forward in history',
513
+ required: false,
514
+ },
515
+ {
516
+ name: 'refresh',
517
+ type: 'boolean',
518
+ description: 'Reload the current page',
519
+ required: false,
520
+ },
521
+ {
522
+ name: 'top',
523
+ type: 'boolean',
524
+ description: 'Scroll to top',
525
+ required: false,
526
+ },
527
+ {
528
+ name: 'bottom',
529
+ type: 'boolean',
530
+ description: 'Scroll to bottom',
531
+ required: false,
532
+ },
533
+ {
534
+ name: 'name',
535
+ type: 'string',
536
+ description: 'Cookie name to inspect',
537
+ required: false,
538
+ },
539
+ {
540
+ name: 'attr',
541
+ type: 'string',
542
+ description: 'Attribute filter for locator',
543
+ required: false,
544
+ },
545
+ {
546
+ name: 'count',
547
+ type: 'boolean',
548
+ description: 'Return only the count of matching elements',
549
+ required: false,
550
+ },
551
+ {
552
+ name: 'port',
553
+ type: 'number',
554
+ description: 'Override Chrome remote debugging port',
555
+ required: false,
556
+ },
557
+ {
558
+ name: 'headless',
559
+ type: 'boolean',
560
+ description: 'Run the browser in headless mode for automation or CI',
561
+ required: false,
562
+ },
563
+ {
564
+ name: 'quizStrategy',
565
+ type: 'string',
566
+ description: 'Quiz answer strategy: first or random',
567
+ required: false,
568
+ },
569
+ ];
570
+ let lastBrowserTestResult = null;
571
+ export function getLastBrowserTestResult() {
572
+ return lastBrowserTestResult;
573
+ }
574
+ /**
575
+ * Start a temporary HTTP server serving the browser test page.
576
+ * Returns the server and the URL.
577
+ */
578
+ async function startTestServer() {
579
+ const html = `<!DOCTYPE html>
580
+ <html lang="en">
581
+ <head><meta charset="UTF-8"><title>Browser Test Page</title></head>
582
+ <body>
583
+ <h1 id="test-title">Browser Test</h1>
584
+ <p id="test-paragraph">Hello, world!</p>
585
+ <button id="test-button" onclick="document.getElementById('test-output').textContent='clicked'">Click Me</button>
586
+ <input id="test-input" type="text" placeholder="Type here">
587
+ <label><input type="checkbox" id="test-checkbox" checked> Checkbox</label>
588
+ <label><input type="radio" name="test-radio" id="radio-1" checked> Radio 1</label>
589
+ <label><input type="radio" name="test-radio" id="radio-2"> Radio 2</label>
590
+ <div id="test-output"></div>
591
+ <script>
592
+ console.log('Browser test page loaded');
593
+ console.error('Test error message');
594
+ localStorage.setItem('test-key', 'test-value');
595
+ sessionStorage.setItem('session-key', 'session-value');
596
+ document.cookie = 'test-cookie=chocolate-chip; path=/';
597
+ </script>
598
+ </body>
599
+ </html>`;
600
+ return new Promise((resolve, reject) => {
601
+ const server = createServer((_req, res) => {
602
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
603
+ res.end(html);
604
+ });
605
+ server.listen(0, '127.0.0.1', () => {
606
+ const addr = server.address();
607
+ if (!addr || typeof addr === 'string') {
608
+ server.close();
609
+ reject(new Error('Failed to get server address'));
610
+ return;
611
+ }
612
+ resolve({ server, url: `http://127.0.0.1:${addr.port}` });
613
+ });
614
+ server.unref();
615
+ server.on('error', reject);
616
+ });
617
+ }
618
+ /**
619
+ * Run a controlled browser test on a local HTTP page.
620
+ *
621
+ * @param options - Optional: headless mode, abort signal
622
+ * @returns Markdown report string
623
+ *
624
+ * Features:
625
+ * - All steps use sameTab: true on a single page via local HTTP server
626
+ * - Each step has a timeout (30s max)
627
+ * - A single step failure does NOT abort the entire test
628
+ * - Supports --headless and --headed modes
629
+ * - Supports abort via AbortSignal
630
+ * - Always produces a final structured report
631
+ * - Saves structured result for /last-browser-test
632
+ */
633
+ export async function browserTest(options) {
634
+ const desiredHeadless = options?.headless ?? false; // default: headed (visible)
635
+ const signal = options?.signal;
636
+ const requestedMode = desiredHeadless ? 'headless' : 'headed';
637
+ const steps = [];
638
+ // Ensure browser is in the correct mode — closes and re-launches if needed
639
+ let initFailed = false;
640
+ let actualHeadless = desiredHeadless;
641
+ try {
642
+ await chromeManager.ensureMode(desiredHeadless);
643
+ const state = chromeManager.getState();
644
+ actualHeadless = state.headless;
645
+ }
646
+ catch (err) {
647
+ // If we can't even ensure the mode, add a step for it
648
+ initFailed = true;
649
+ steps.push({
650
+ name: 'browser init',
651
+ status: 'failed',
652
+ durationMs: 0,
653
+ details: `Не удалось запустить Chrome: ${String(err)}. Проверьте, не занят ли порт 9222 другим процессом Chrome.`,
654
+ });
655
+ return buildBrowserTestReport(steps, requestedMode, actualHeadless, signal?.aborted ?? false, undefined, initFailed);
656
+ }
657
+ // Start temporary HTTP server
658
+ let server = null;
659
+ let testUrl = null;
660
+ let serverError = null;
661
+ try {
662
+ const result = await startTestServer();
663
+ server = result.server;
664
+ testUrl = result.url;
665
+ }
666
+ catch (err) {
667
+ serverError = String(err);
668
+ }
669
+ if (!testUrl) {
670
+ // Fall back to data: URL for basic tests, skip storage/network
671
+ const dataUrl = `data:text/html,<!DOCTYPE html>
672
+ <html lang="en">
673
+ <head><meta charset="UTF-8"><title>Browser Test Page</title></head>
674
+ <body>
675
+ <h1 id="test-title">Browser Test</h1>
676
+ <p id="test-paragraph">Hello, world!</p>
677
+ <button id="test-button" onclick="document.getElementById('test-output').textContent='clicked'">Click Me</button>
678
+ <input id="test-input" type="text" placeholder="Type here">
679
+ <label><input type="checkbox" id="test-checkbox" checked> Checkbox</label>
680
+ <label><input type="radio" name="test-radio" id="radio-1" checked> Radio 1</label>
681
+ <label><input type="radio" name="test-radio" id="radio-2"> Radio 2</label>
682
+ <div id="test-output"></div>
683
+ <script>
684
+ console.log('Browser test page loaded');
685
+ console.error('Test error message');
686
+ </script>
687
+ </body>
688
+ </html>`;
689
+ const baseArgs = { url: dataUrl, sameTab: true, timeout: 30000 };
690
+ async function runDataStep(name, action, extraArgs) {
691
+ if (signal?.aborted) {
692
+ steps.push({ name, status: 'skipped', durationMs: 0, details: 'тест отменён' });
693
+ return;
694
+ }
695
+ const start = Date.now();
696
+ try {
697
+ const result = await Promise.race([
698
+ executeAction({ ...baseArgs, ...extraArgs, action }),
699
+ new Promise((_resolve, _reject) => setTimeout(() => _reject(new Error('timeout 30s')), 30000)),
700
+ ]);
701
+ steps.push({ name, status: result.success ? 'passed' : 'failed', durationMs: Date.now() - start, details: result.success ? (result.output || '').slice(0, 200) : (result.error || 'unknown error') });
702
+ }
703
+ catch (err) {
704
+ steps.push({ name, status: 'failed', durationMs: Date.now() - start, details: String(err) });
705
+ }
706
+ }
707
+ await runDataStep('open', 'open', {});
708
+ await runDataStep('html', 'html', {});
709
+ await runDataStep('eval', 'eval', { code: 'document.title' });
710
+ await runDataStep('fill', 'fill', { selector: '#test-input', text: 'test input value' });
711
+ await runDataStep('click (button)', 'click', { selector: '#test-button' });
712
+ await runDataStep('text (after click)', 'text', { selector: '#test-output' });
713
+ await runDataStep('click (checkbox)', 'click', { selector: '#test-checkbox' });
714
+ await runDataStep('click (radio)', 'click', { selector: '#radio-2' });
715
+ await runDataStep('console', 'console', {});
716
+ steps.push({ name: 'storage (local)', status: 'skipped', durationMs: 0, details: `HTTP-сервер не удалось поднять: ${serverError}. Storage недоступен на data: URL` });
717
+ steps.push({ name: 'storage (session)', status: 'skipped', durationMs: 0, details: `HTTP-сервер не удалось поднять: ${serverError}. Storage недоступен на data: URL` });
718
+ steps.push({ name: 'cookies', status: 'skipped', durationMs: 0, details: `HTTP-сервер не удалось поднять: ${serverError}. Cookies недоступны на data: URL` });
719
+ await runDataStep('screenshot', 'shot', {});
720
+ steps.push({ name: 'network', status: 'skipped', durationMs: 0, details: `HTTP-сервер не удалось поднять: ${serverError}. Network/fetch не проверен` });
721
+ return buildBrowserTestReport(steps, requestedMode, actualHeadless, signal?.aborted ?? false, serverError);
722
+ }
723
+ else {
724
+ // Full test with HTTP server
725
+ const baseArgs = { url: testUrl, sameTab: true, timeout: 30000 };
726
+ async function runStep(name, action, extraArgs) {
727
+ if (signal?.aborted) {
728
+ steps.push({ name, status: 'skipped', durationMs: 0, details: 'тест отменён' });
729
+ return;
730
+ }
731
+ const start = Date.now();
732
+ try {
733
+ const result = await Promise.race([
734
+ executeAction({ ...baseArgs, ...extraArgs, action }),
735
+ new Promise((_resolve, _reject) => setTimeout(() => _reject(new Error('timeout 30s')), 30000)),
736
+ ]);
737
+ steps.push({ name, status: result.success ? 'passed' : 'failed', durationMs: Date.now() - start, details: result.success ? (result.output || '').slice(0, 200) : (result.error || 'unknown error') });
738
+ }
739
+ catch (err) {
740
+ steps.push({ name, status: 'failed', durationMs: Date.now() - start, details: String(err) });
741
+ }
742
+ }
743
+ // 1. open
744
+ await runStep('open', 'open', {});
745
+ // 2. html — read full DOM
746
+ await runStep('html', 'html', {});
747
+ // 3. eval
748
+ await runStep('eval', 'eval', { code: 'document.title' });
749
+ // 4. fill
750
+ await runStep('fill', 'fill', { selector: '#test-input', text: 'test input value' });
751
+ // 5. click button
752
+ await runStep('click (button)', 'click', { selector: '#test-button' });
753
+ // 6. text — read result after click
754
+ await runStep('text (after click)', 'text', { selector: '#test-output' });
755
+ // 7. click checkbox
756
+ await runStep('click (checkbox)', 'click', { selector: '#test-checkbox' });
757
+ // 8. click radio
758
+ await runStep('click (radio)', 'click', { selector: '#radio-2' });
759
+ // 9. console
760
+ await runStep('console', 'console', {});
761
+ // 10. storage (localStorage)
762
+ await runStep('storage (local)', 'storage', { local: true });
763
+ // 11. storage (sessionStorage)
764
+ await runStep('storage (session)', 'storage', { session: true });
765
+ // 12. cookies
766
+ await runStep('cookies', 'cookies', {});
767
+ // 13. screenshot
768
+ await runStep('screenshot', 'shot', {});
769
+ // 14. network
770
+ await runStep('network', 'network', { api: true });
771
+ // Close the HTTP server
772
+ server.close();
773
+ }
774
+ return buildBrowserTestReport(steps, requestedMode, actualHeadless, signal?.aborted ?? false, serverError);
775
+ }
776
+ /**
777
+ * Build the markdown report from collected steps.
778
+ * Extracted so it can be called both from the normal flow and from early-exit on browser init failure.
779
+ */
780
+ function buildBrowserTestReport(steps, requestedMode, actualHeadless, stoppedEarly, serverError, initFailed) {
781
+ const actualMode = initFailed ? 'не запущен' : (actualHeadless ? 'headless' : 'headed');
782
+ const modeMatch = !initFailed && requestedMode === actualMode;
783
+ const stoppedReason = stoppedEarly ? 'тест отменён пользователем' : undefined;
784
+ // Build summary
785
+ const passed = steps.filter(s => s.status === 'passed').length;
786
+ const failed = steps.filter(s => s.status === 'failed').length;
787
+ const skipped = steps.filter(s => s.status === 'skipped').length;
788
+ // Save structured result
789
+ lastBrowserTestResult = {
790
+ timestamp: new Date().toISOString(),
791
+ mode: initFailed ? 'headless' : actualMode,
792
+ steps,
793
+ summary: { passed, failed, skipped },
794
+ stoppedEarly,
795
+ stoppedReason,
796
+ };
797
+ // Get PID for the report
798
+ const state = chromeManager.getState();
799
+ const pid = initFailed ? undefined : state.managedProcessPid;
800
+ // Build markdown report
801
+ const lines = [
802
+ '## 🧪 Browser Test Report',
803
+ '',
804
+ '> **Verified** — все результаты подтверждены реальными chrome tool calls.',
805
+ `> **Запрошенный режим:** ${requestedMode}`,
806
+ `> **Фактический режим:** ${actualMode}`,
807
+ `> **PID процесса:** ${pid ?? '—'}`,
808
+ '',
809
+ ];
810
+ if (initFailed) {
811
+ lines.push('> ❌ **Chrome не удалось запустить.** Browser test не выполнялся.');
812
+ lines.push('');
813
+ }
814
+ else if (!modeMatch) {
815
+ lines.push(`> ❌ **Режимы не совпадают.** Запрошен ${requestedMode}, но Chrome работает в ${actualMode}. Browser test считается failed.`);
816
+ lines.push('');
817
+ }
818
+ if (stoppedEarly) {
819
+ lines.push('> ⚠️ **Тест остановлен досрочно.** Причина: отмена пользователем.');
820
+ lines.push(`> Проверено шагов: ${steps.filter(s => s.status !== 'skipped').length} / ${steps.length}`);
821
+ lines.push('');
822
+ }
823
+ if (serverError) {
824
+ lines.push(`> ⚠️ **HTTP-сервер не поднят:** ${serverError}. Storage/cookies/network пропущены.`);
825
+ lines.push('');
826
+ }
827
+ lines.push('| Шаг | Статус | Длительность | Детали |');
828
+ lines.push('|------|--------|-------------|--------|');
829
+ for (const step of steps) {
830
+ const icon = step.status === 'passed' ? '✅' : step.status === 'failed' ? '❌' : '⏭️';
831
+ const dur = step.durationMs > 0 ? `${step.durationMs}ms` : '—';
832
+ // Escape pipes for markdown table — each step is one row
833
+ let detail = step.details.replace(/\|/g, '\\|');
834
+ if (detail.length > 100) {
835
+ detail = detail.slice(0, 100) + '…';
836
+ }
837
+ lines.push(`| ${icon} ${step.name} | ${step.status} | ${dur} | ${detail} |`);
838
+ }
839
+ lines.push('');
840
+ lines.push(`**Итого:** ${passed} passed, ${failed} failed, ${skipped} skipped`);
841
+ // ✅ What works
842
+ const working = steps.filter(s => s.status === 'passed').map(s => s.name);
843
+ if (working.length > 0) {
844
+ lines.push('');
845
+ lines.push('### ✅ Что работает');
846
+ for (const name of working) {
847
+ lines.push(`- **${name}**`);
848
+ }
849
+ }
850
+ // ❌ What fails
851
+ const failing = steps.filter(s => s.status === 'failed');
852
+ if (failing.length > 0) {
853
+ lines.push('');
854
+ lines.push('### ❌ Что не работает');
855
+ for (const step of failing) {
856
+ lines.push(`- **${step.name}**: ${step.details}`);
857
+ }
858
+ }
859
+ // 🔧 What to fix
860
+ if (failing.length > 0) {
861
+ lines.push('');
862
+ lines.push('### 🔧 Что доработать');
863
+ for (const step of failing) {
864
+ lines.push(`- Исправить \`${step.name}\`: ${step.details}`);
865
+ }
866
+ }
867
+ // ⏭️ What is skipped
868
+ const skippedSteps = steps.filter(s => s.status === 'skipped');
869
+ if (skippedSteps.length > 0) {
870
+ lines.push('');
871
+ lines.push('### ⏭️ Что пропущено');
872
+ for (const step of skippedSteps) {
873
+ lines.push(`- **${step.name}**: ${step.details}`);
874
+ }
875
+ }
876
+ return lines.join('\n');
877
+ }
878
+ // ─── Real Site Smoke Test ─────────────────────────────────────────────────────
879
+ const DEFAULT_REAL_SITES = ['example.com', 'wikipedia.org', 'github.com'];
880
+ const MAX_REAL_CALLS = 20;
881
+ export async function browserRealTest(options) {
882
+ const desiredHeadless = options?.headless ?? false;
883
+ const signal = options?.signal;
884
+ const rawSites = options?.sites && options.sites.length > 0
885
+ ? options.sites
886
+ : DEFAULT_REAL_SITES;
887
+ const sites = rawSites.map(s => {
888
+ const t = s.trim().toLowerCase();
889
+ return t.startsWith('http://') || t.startsWith('https://') ? t : `https://${t}`;
890
+ });
891
+ let chromeCalls = 0;
892
+ const results = [];
893
+ try {
894
+ await chromeManager.ensureMode(desiredHeadless);
895
+ }
896
+ catch (err) {
897
+ return `## /browser-real-test\n\n❌ Chrome не удалось запустить: ${String(err)}`;
898
+ }
899
+ for (const url of sites) {
900
+ if (signal?.aborted) {
901
+ results.push({ site: url, steps: [{ action: 'all', status: 'skipped', detail: 'aborted' }], blocked: false, skipReason: 'aborted' });
902
+ continue;
903
+ }
904
+ if (chromeCalls >= MAX_REAL_CALLS) {
905
+ results.push({ site: url, steps: [{ action: 'all', status: 'skipped', detail: `call limit ${MAX_REAL_CALLS} reached` }], blocked: false, skipReason: 'limit' });
906
+ continue;
907
+ }
908
+ const siteResult = { site: url, steps: [], blocked: false };
909
+ results.push(siteResult);
910
+ async function siteAction(args) {
911
+ chromeCalls++;
912
+ try {
913
+ return await Promise.race([
914
+ executeAction(args),
915
+ new Promise((_resolve, reject) => setTimeout(() => reject(new Error('timeout 15s')), 15000)),
916
+ ]);
917
+ }
918
+ catch (err) {
919
+ return { success: false, output: '', error: String(err) };
920
+ }
921
+ }
922
+ // 1: open
923
+ if (chromeCalls >= MAX_REAL_CALLS) {
924
+ siteResult.steps.push({ action: 'open', status: 'skipped', detail: 'call limit' });
925
+ continue;
926
+ }
927
+ const openR = await siteAction({ action: 'open', url, sameTab: true, timeout: 15000 });
928
+ siteResult.steps.push({ action: 'open', status: openR.success ? 'passed' : 'failed', detail: openR.success ? '' : (openR.error ?? 'failed').slice(0, 120) });
929
+ if (!openR.success)
930
+ continue;
931
+ // 2: title via eval (no html action)
932
+ if (chromeCalls >= MAX_REAL_CALLS) {
933
+ siteResult.steps.push({ action: 'title', status: 'skipped', detail: 'call limit' });
934
+ continue;
935
+ }
936
+ const titleR = await siteAction({ action: 'eval', sameTab: true, code: 'document.title', timeout: 10000 });
937
+ siteResult.steps.push({ action: 'title', status: titleR.success ? 'passed' : 'failed', detail: titleR.success ? (titleR.output || '').slice(0, 80) : (titleR.error ?? '').slice(0, 80) });
938
+ // 3: cookie/captcha/consent wall check
939
+ if (chromeCalls >= MAX_REAL_CALLS) {
940
+ siteResult.steps.push({ action: 'cookie-check', status: 'skipped', detail: 'call limit' });
941
+ continue;
942
+ }
943
+ const wallCode = '!!document.querySelector(\'[id*="cookie"],[class*="cookie"],[id*="consent"],[class*="consent"],[id*="captcha"],[class*="captcha"],[id*="gdpr"],[class*="gdpr"]\')';
944
+ const wallR = await siteAction({ action: 'eval', sameTab: true, code: wallCode, timeout: 10000 });
945
+ const hasWall = wallR.success && wallR.output.trim() === 'true';
946
+ siteResult.steps.push({ action: 'cookie-check', status: hasWall ? 'blocked' : 'passed', detail: hasWall ? 'cookie/captcha/consent wall detected — skipped' : 'clear' });
947
+ if (hasWall) {
948
+ siteResult.blocked = true;
949
+ continue;
950
+ }
951
+ // 4: screenshot
952
+ if (chromeCalls >= MAX_REAL_CALLS) {
953
+ siteResult.steps.push({ action: 'screenshot', status: 'skipped', detail: 'call limit' });
954
+ continue;
955
+ }
956
+ const shotR = await siteAction({ action: 'shot', sameTab: true, timeout: 10000 });
957
+ siteResult.steps.push({ action: 'screenshot', status: shotR.success ? 'passed' : 'failed', detail: shotR.success ? 'ok' : (shotR.error ?? '').slice(0, 80) });
958
+ // 5: console errors
959
+ if (chromeCalls >= MAX_REAL_CALLS) {
960
+ siteResult.steps.push({ action: 'console', status: 'skipped', detail: 'call limit' });
961
+ continue;
962
+ }
963
+ const consoleR = await siteAction({ action: 'console', sameTab: true, timeout: 10000 });
964
+ siteResult.steps.push({ action: 'console', status: consoleR.success ? 'passed' : 'failed', detail: consoleR.success ? (consoleR.output || 'no errors').slice(0, 120) : (consoleR.error ?? '').slice(0, 80) });
965
+ }
966
+ return buildRealTestReport(results, chromeCalls, sites.length);
967
+ }
968
+ function buildRealTestReport(results, totalCalls, totalSites) {
969
+ const ACTIONS = ['open', 'title', 'cookie-check', 'screenshot', 'console'];
970
+ const allSteps = results.flatMap(r => r.steps);
971
+ const passed = allSteps.filter(s => s.status === 'passed').length;
972
+ const failed = allSteps.filter(s => s.status === 'failed').length;
973
+ const skipped = allSteps.filter(s => s.status === 'skipped').length;
974
+ const blocked = allSteps.filter(s => s.status === 'blocked').length;
975
+ const partial = results.some(r => r.skipReason === 'limit');
976
+ const lines = [
977
+ '## /browser-real-test Results',
978
+ '',
979
+ `**Sites:** ${results.length}/${totalSites} | **Calls:** ${totalCalls}/${MAX_REAL_CALLS} | **Passed:** ${passed} | **Failed:** ${failed} | **Skipped:** ${skipped} | **Blocked:** ${blocked}`,
980
+ '> Token safety: OK — no full HTML reads, call limit enforced',
981
+ '',
982
+ ];
983
+ if (partial) {
984
+ lines.push(`> ⚠️ Partial Report: reached limit of ${MAX_REAL_CALLS} calls. Some sites were skipped.`);
985
+ lines.push('');
986
+ }
987
+ lines.push(`| Site | ${ACTIONS.join(' | ')} | Notes |`);
988
+ lines.push(`|------|${ACTIONS.map(() => '------').join('|')}|-------|`);
989
+ for (const r of results) {
990
+ const cells = ACTIONS.map(action => {
991
+ const step = r.steps.find(s => s.action === action);
992
+ if (!step)
993
+ return '⏭️';
994
+ if (step.status === 'passed')
995
+ return '✅';
996
+ if (step.status === 'failed')
997
+ return '❌';
998
+ if (step.status === 'blocked')
999
+ return '🚫';
1000
+ return '⏭️';
1001
+ });
1002
+ const domain = r.site.replace(/^https?:\/\//, '');
1003
+ const notes = r.blocked
1004
+ ? 'cookie/consent wall'
1005
+ : r.skipReason === 'limit'
1006
+ ? 'call limit'
1007
+ : r.skipReason === 'aborted'
1008
+ ? 'aborted'
1009
+ : '—';
1010
+ lines.push(`| ${domain} | ${cells.join(' | ')} | ${notes} |`);
1011
+ }
1012
+ const failedSteps = results.flatMap(r => r.steps.filter(s => s.status === 'failed').map(s => ({ site: r.site, ...s })));
1013
+ if (failedSteps.length > 0) {
1014
+ lines.push('');
1015
+ lines.push('### Failed Actions');
1016
+ lines.push('| Site | Action | Reason | Blocker |');
1017
+ lines.push('|------|--------|--------|---------|');
1018
+ for (const s of failedSteps) {
1019
+ const domain = s.site.replace(/^https?:\/\//, '');
1020
+ lines.push(`| ${domain} | ${s.action} | ${s.detail.replace(/\|/g, '\\|')} | ${s.action === 'open' ? 'yes' : 'no'} |`);
1021
+ }
1022
+ }
1023
+ lines.push('');
1024
+ lines.push(`**Что доработать:** ${failedSteps.length === 0 ? 'нет проблем' : failedSteps.map(s => s.action).join(', ')}`);
1025
+ return lines.join('\n');
1026
+ }
1027
+ export const chromeTool = {
1028
+ name: 'chrome',
1029
+ description: `Control a real browser through a native Chrome runtime.
1030
+ Use it for UI validation, rendered DOM inspection, console and network debugging,
1031
+ screenshots, and multi-step browser workflows when terminal tools are not enough.
1032
+ If the task mentions localhost pages, forms, browser bugs, screenshots, rendered state,
1033
+ console output, or network requests, this is the primary tool.
1034
+
1035
+ Examples:
1036
+ - { "action": "open", "url": "https://example.com" }
1037
+ - { "action": "text", "url": "https://example.com", "selector": "h1" }
1038
+ - { "action": "click", "url": "https://example.com", "selector": ".submit-btn" }
1039
+ - { "action": "fill", "url": "https://example.com/login", "selector": "#email", "text": "user@example.com" }
1040
+ - { "action": "eval", "url": "https://example.com", "code": "document.title" }
1041
+ - { "action": "network", "url": "https://example.com", "api": true }
1042
+ - { "action": "state" }
1043
+ - { "action": "shot", "url": "https://example.com", "output": "screenshot.png", "full": true }
1044
+ - Use "sameTab": true for multi-step flows in one tab.
1045
+ - Use "headless": true for automation or CI-safe browser execution.`,
1046
+ parameters: chromeParameters,
1047
+ execute: async (args) => {
1048
+ const typedArgs = args;
1049
+ if (!typedArgs.action) {
1050
+ return { success: false, output: '', error: 'Action is required' };
1051
+ }
1052
+ if (typedArgs.port) {
1053
+ chromeManager.setDebugPort(typedArgs.port);
1054
+ }
1055
+ return executeAction(typedArgs);
1056
+ },
1057
+ };
1058
+ //# sourceMappingURL=chrome.js.map