micro-models-agent 2.11.0 → 2.12.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 (347) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/js/tabs.js +22 -3
  3. package/dist/cli/cache-line.js +1 -0
  4. package/dist/cli/command-suggest.js +5 -4
  5. package/dist/cli/commands.js +231 -122
  6. package/dist/cli/completer.js +132 -31
  7. package/dist/cli/index.js +1 -0
  8. package/dist/cli/json-payload.js +4 -4
  9. package/dist/cli/main.js +166 -61
  10. package/dist/cli/onboarding-commands.js +126 -0
  11. package/dist/cli/plugin-commands.js +2 -1
  12. package/dist/cli/relaunch.js +16 -13
  13. package/dist/cli/repl-commands-lsp.js +75 -0
  14. package/dist/cli/repl-commands-skills.js +105 -0
  15. package/dist/cli/repl-commands.js +589 -266
  16. package/dist/cli/repl.js +436 -177
  17. package/dist/cli/run-result.js +5 -6
  18. package/dist/cli/security-commands.js +37 -28
  19. package/dist/cli/setup-order.js +4 -55
  20. package/dist/cli/setup-prompt.js +3 -2
  21. package/dist/cli/setup.js +27 -107
  22. package/dist/cli/web-command.js +98 -0
  23. package/dist/config/budget.js +5 -5
  24. package/dist/config/config.js +103 -96
  25. package/dist/config/defaults.js +38 -8
  26. package/dist/config/domains.js +24 -27
  27. package/dist/config/env.js +46 -0
  28. package/dist/config/experts.js +2 -0
  29. package/dist/config/index.js +1 -0
  30. package/dist/config/paths.js +56 -0
  31. package/dist/config/redact.js +44 -0
  32. package/dist/config/registry-extra.js +334 -0
  33. package/dist/config/registry.js +584 -0
  34. package/dist/config/security.js +35 -36
  35. package/dist/config/timeouts.js +74 -0
  36. package/dist/config/utils.js +16 -15
  37. package/dist/core/agent/artifact-progress.js +59 -0
  38. package/dist/core/agent/artifact-resolve.js +79 -0
  39. package/dist/core/agent/audit-gate.js +7 -3
  40. package/dist/core/agent/compaction.js +2 -2
  41. package/dist/core/agent/constants.js +33 -4
  42. package/dist/core/agent/context-log.js +454 -0
  43. package/dist/core/agent/context-renderer.js +5 -0
  44. package/dist/core/agent/critic-gate.js +132 -0
  45. package/dist/core/agent/hallucination-gate.js +148 -38
  46. package/dist/core/agent/loop-state.js +15 -0
  47. package/dist/core/agent/prefix-monitor.js +1 -1
  48. package/dist/core/agent/reasoning-resolver.js +10 -1
  49. package/dist/core/agent/token-tracker.js +7 -4
  50. package/dist/core/agent/tool-batch.js +93 -18
  51. package/dist/core/agent/tool-images.js +18 -0
  52. package/dist/core/agent/tool-output.js +65 -11
  53. package/dist/core/agent/tool-pairing.js +60 -0
  54. package/dist/core/agent-moe.js +25 -24
  55. package/dist/core/agent.js +429 -108
  56. package/dist/core/bootstrap/agent-phase.js +66 -0
  57. package/dist/core/bootstrap/config-phase.js +86 -0
  58. package/dist/core/bootstrap/modules-phase.js +269 -0
  59. package/dist/core/bootstrap/provider-phase.js +183 -0
  60. package/dist/core/bootstrap/session-phase.js +204 -0
  61. package/dist/core/bootstrap/state.js +94 -0
  62. package/dist/core/bootstrap/workspace-phase.js +115 -0
  63. package/dist/core/bootstrap.js +92 -660
  64. package/dist/core/crash-handler.js +17 -7
  65. package/dist/core/environment.js +37 -12
  66. package/dist/core/host-plan.js +43 -0
  67. package/dist/core/host.js +23 -0
  68. package/dist/core/index.js +1 -0
  69. package/dist/core/prompt-builder.js +17 -1
  70. package/dist/core/prompt-overflow.js +42 -43
  71. package/dist/core/session-logger.js +122 -30
  72. package/dist/core/system-prompt.js +117 -0
  73. package/dist/core/version.js +6 -4
  74. package/dist/core/web-host.js +34 -0
  75. package/dist/core/workspace.js +19 -15
  76. package/dist/i18n/index.js +37 -3
  77. package/dist/index.js +7 -3
  78. package/dist/llm/image-utils.js +55 -64
  79. package/dist/llm/llm-errors.js +9 -0
  80. package/dist/llm/model-loader.js +10 -32
  81. package/dist/llm/models-dev.js +121 -0
  82. package/dist/llm/openai-compat.js +146 -73
  83. package/dist/llm/orchestrator.js +33 -11
  84. package/dist/llm/provider-budget.js +2 -2
  85. package/dist/llm/provider-context.js +233 -0
  86. package/dist/llm/provider.js +1 -1
  87. package/dist/llm/response.js +5 -0
  88. package/dist/llm/token-counter.js +27 -11
  89. package/dist/logger/app-logger.js +48 -22
  90. package/dist/logger/file-log.js +28 -19
  91. package/dist/logger/index.js +1 -0
  92. package/dist/main.js +273 -47
  93. package/dist/migration/backup.js +14 -3
  94. package/dist/migration/detect.js +12 -3
  95. package/dist/migration/index.js +1 -0
  96. package/dist/modules/artifacts/store.js +9 -8
  97. package/dist/modules/browser/actions.js +6 -0
  98. package/dist/modules/browser/bridge-client.js +14 -12
  99. package/dist/modules/browser/bridge-path.js +4 -4
  100. package/dist/modules/browser/console-buffer.js +37 -0
  101. package/dist/modules/browser/cookie-store.js +5 -0
  102. package/dist/modules/browser/driver-types.js +1 -0
  103. package/dist/modules/browser/driver.js +19 -11
  104. package/dist/modules/browser/index.js +3 -1
  105. package/dist/modules/browser/module.js +2 -1
  106. package/dist/modules/browser/session.js +10 -30
  107. package/dist/modules/browser/snapshot.js +19 -13
  108. package/dist/modules/browser/types.js +1 -0
  109. package/dist/modules/certification/cli.js +28 -20
  110. package/dist/modules/certification/fact-checker.js +9 -1
  111. package/dist/modules/certification/loader.js +9 -2
  112. package/dist/modules/certification/manifest.js +83 -21
  113. package/dist/modules/certification/runner.js +23 -13
  114. package/dist/modules/certification/scenarios.js +20 -20
  115. package/dist/modules/certification/syntax-scenarios.js +1 -2
  116. package/dist/modules/context/chunk-query.js +11 -9
  117. package/dist/modules/context/fact-extractor.js +37 -39
  118. package/dist/modules/context/index.js +1 -0
  119. package/dist/modules/context/manager.js +106 -80
  120. package/dist/modules/critic/client.js +176 -0
  121. package/dist/modules/critic/evidence.js +81 -0
  122. package/dist/modules/critic/provider.js +45 -0
  123. package/dist/modules/critic/tools.js +78 -0
  124. package/dist/modules/critic/types.js +1 -0
  125. package/dist/modules/execution/audit-runners.js +28 -34
  126. package/dist/modules/execution/auditor.js +53 -49
  127. package/dist/modules/execution/execution-plugin.js +142 -102
  128. package/dist/modules/execution/index.js +1 -1
  129. package/dist/modules/execution/input-from.js +37 -13
  130. package/dist/modules/execution/module.js +368 -191
  131. package/dist/modules/execution/moe-executor.js +53 -26
  132. package/dist/modules/execution/plan-actions.js +320 -202
  133. package/dist/modules/execution/plan-checks.js +35 -0
  134. package/dist/modules/execution/plan-coverage.js +36 -33
  135. package/dist/modules/execution/plan-steps.js +11 -3
  136. package/dist/modules/execution/plan-store.js +83 -17
  137. package/dist/modules/execution/plan-tool.js +89 -35
  138. package/dist/modules/execution/plan-validator.js +30 -10
  139. package/dist/modules/execution/planner.js +22 -13
  140. package/dist/modules/execution/stuck-detector.js +196 -93
  141. package/dist/modules/execution/tracker.js +22 -8
  142. package/dist/modules/execution/verifier.js +69 -48
  143. package/dist/modules/execution/windows-commands.js +12 -12
  144. package/dist/modules/hallucination/artifact-claim.js +93 -0
  145. package/dist/modules/hallucination/confidence.js +13 -11
  146. package/dist/modules/hallucination/consistency.js +10 -2
  147. package/dist/modules/hallucination/detector.js +10 -5
  148. package/dist/modules/hallucination/factual.js +43 -26
  149. package/dist/modules/hallucination/index.js +1 -0
  150. package/dist/modules/hallucination/js-identifiers.js +44 -45
  151. package/dist/modules/hallucination/llm-judge.js +17 -12
  152. package/dist/modules/hallucination/success-claim.js +59 -0
  153. package/dist/modules/indexer/cache.js +52 -4
  154. package/dist/modules/indexer/index.js +1 -0
  155. package/dist/modules/indexer/map-command.js +2 -4
  156. package/dist/modules/indexer/map-select.js +16 -16
  157. package/dist/modules/indexer/module.js +47 -12
  158. package/dist/modules/indexer/project-profile.js +8 -7
  159. package/dist/modules/indexer/symbols.js +15 -15
  160. package/dist/modules/indexer/walker.js +159 -29
  161. package/dist/modules/lsp/check-tool.js +7 -5
  162. package/dist/modules/lsp/client.js +39 -39
  163. package/dist/modules/lsp/command.js +17 -24
  164. package/dist/modules/lsp/config.js +12 -11
  165. package/dist/modules/lsp/index.js +1 -0
  166. package/dist/modules/lsp/module.js +48 -46
  167. package/dist/modules/lsp/probe.js +13 -14
  168. package/dist/modules/lsp/project-root.js +14 -17
  169. package/dist/modules/lsp/startup-check.js +18 -16
  170. package/dist/modules/mcp/client.js +59 -62
  171. package/dist/modules/mcp/index.js +1 -0
  172. package/dist/modules/mcp/module.js +66 -37
  173. package/dist/modules/mcp/registry.js +5 -0
  174. package/dist/modules/memory/index.js +1 -0
  175. package/dist/modules/memory/module.js +12 -8
  176. package/dist/modules/memory/search.js +4 -1
  177. package/dist/modules/memory/store.js +40 -2
  178. package/dist/modules/onboarding/init.js +47 -0
  179. package/dist/modules/onboarding/meet.js +30 -0
  180. package/dist/modules/onboarding/mode.js +30 -0
  181. package/dist/modules/onboarding/prompts.js +43 -0
  182. package/dist/modules/pipelines/engine.js +10 -0
  183. package/dist/modules/pipelines/index.js +1 -0
  184. package/dist/modules/pipelines/parser.js +4 -1
  185. package/dist/modules/pipelines/template.js +4 -0
  186. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  187. package/dist/modules/plugins/builtin/notify.js +2 -0
  188. package/dist/modules/plugins/index.js +1 -0
  189. package/dist/modules/plugins/loader.js +18 -3
  190. package/dist/modules/plugins/manager.js +37 -17
  191. package/dist/modules/pricing/index.js +10 -11
  192. package/dist/modules/pricing/prices.js +12 -12
  193. package/dist/modules/processes/index.js +1 -0
  194. package/dist/modules/processes/kill-tree.js +27 -31
  195. package/dist/modules/processes/registry.js +44 -25
  196. package/dist/modules/processes/runner.js +4 -4
  197. package/dist/modules/providers/capabilities.js +28 -0
  198. package/dist/modules/providers/create.js +3 -4
  199. package/dist/modules/providers/factory.js +7 -7
  200. package/dist/modules/providers/fallback.js +12 -12
  201. package/dist/modules/providers/health.js +4 -4
  202. package/dist/modules/providers/manager.js +50 -19
  203. package/dist/modules/providers/naming.js +15 -0
  204. package/dist/modules/providers/preflight.js +71 -0
  205. package/dist/modules/providers/presets.js +39 -8
  206. package/dist/modules/providers/registry.js +7 -2
  207. package/dist/modules/reasoning/policy.js +8 -6
  208. package/dist/modules/reasoning/probe.js +30 -25
  209. package/dist/modules/security/audit-log.js +19 -36
  210. package/dist/modules/security/audit-notifier.js +35 -75
  211. package/dist/modules/security/command-validator.js +99 -51
  212. package/dist/modules/security/content-scanner.js +8 -8
  213. package/dist/modules/security/data-sanitizer.js +9 -16
  214. package/dist/modules/security/encryption.js +32 -52
  215. package/dist/modules/security/index.js +2 -1
  216. package/dist/modules/security/network-validator.js +120 -43
  217. package/dist/modules/security/path-validator.js +61 -46
  218. package/dist/modules/security/protected-paths.js +73 -0
  219. package/dist/modules/security/rate-limiter.js +17 -49
  220. package/dist/modules/security/security-policies.js +25 -57
  221. package/dist/modules/security/session-encryption.js +32 -71
  222. package/dist/modules/security/session-isolation.js +15 -28
  223. package/dist/modules/session/auto-name.js +63 -0
  224. package/dist/modules/session/export.js +98 -0
  225. package/dist/modules/session/index.js +1 -0
  226. package/dist/modules/session/manager.js +79 -28
  227. package/dist/modules/session/module.js +4 -3
  228. package/dist/modules/session/store.js +57 -20
  229. package/dist/modules/setup/answers.js +35 -0
  230. package/dist/modules/setup/apply.js +79 -0
  231. package/dist/modules/setup/discovery.js +72 -0
  232. package/dist/modules/skills/index.js +1 -0
  233. package/dist/modules/skills/loader.js +9 -1
  234. package/dist/modules/skills/module.js +11 -0
  235. package/dist/modules/updater/changelog-reader.js +15 -16
  236. package/dist/modules/updater/checker.js +9 -4
  237. package/dist/modules/updater/dev-detect.js +7 -6
  238. package/dist/modules/updater/index.js +1 -0
  239. package/dist/modules/updater/module.js +17 -15
  240. package/dist/modules/user-profile/compressor.js +2 -0
  241. package/dist/modules/user-profile/index.js +1 -0
  242. package/dist/modules/user-profile/profile.js +10 -1
  243. package/dist/modules/webui/answer-api.js +29 -0
  244. package/dist/modules/webui/assets.js +50 -0
  245. package/dist/modules/webui/body.js +21 -0
  246. package/dist/modules/webui/commands-settings.js +209 -0
  247. package/dist/modules/webui/commands-shared.js +60 -0
  248. package/dist/modules/webui/commands.js +174 -0
  249. package/dist/modules/webui/config-api.js +134 -0
  250. package/dist/modules/webui/file-api.js +202 -0
  251. package/dist/modules/webui/forward.js +159 -0
  252. package/dist/modules/webui/history-api.js +112 -0
  253. package/dist/modules/webui/host-state.js +60 -0
  254. package/dist/modules/webui/i18n-api.js +9 -0
  255. package/dist/modules/webui/limits.js +6 -0
  256. package/dist/modules/webui/log-forward.js +20 -0
  257. package/dist/modules/webui/memory-commands.js +64 -0
  258. package/dist/modules/webui/model-cache.js +18 -0
  259. package/dist/modules/webui/module.js +148 -0
  260. package/dist/modules/webui/persist.js +24 -0
  261. package/dist/modules/webui/prompt-io.js +97 -0
  262. package/dist/modules/webui/providers-api.js +337 -0
  263. package/dist/modules/webui/providers-view.js +56 -0
  264. package/dist/modules/webui/reasoning-view.js +15 -0
  265. package/dist/modules/webui/run-agent.js +81 -0
  266. package/dist/modules/webui/server.js +284 -0
  267. package/dist/modules/webui/sessions-api.js +178 -0
  268. package/dist/modules/webui/settings-api.js +561 -0
  269. package/dist/modules/webui/setup-api.js +201 -0
  270. package/dist/modules/webui/state.js +74 -0
  271. package/dist/modules/webui/util.js +261 -0
  272. package/dist/output/bus.js +7 -2
  273. package/dist/output/channel.js +80 -28
  274. package/dist/output/format.js +2 -1
  275. package/dist/output/index.js +1 -0
  276. package/dist/output/json-sink.js +5 -3
  277. package/dist/output/machine.js +4 -3
  278. package/dist/output/session-sink.js +4 -4
  279. package/dist/tools/approve.js +4 -0
  280. package/dist/tools/attach-image.js +5 -4
  281. package/dist/tools/auto-fixer.js +26 -44
  282. package/dist/tools/auto-tags.js +83 -0
  283. package/dist/tools/bash.js +193 -116
  284. package/dist/tools/browser.js +76 -31
  285. package/dist/tools/chunk-query.js +14 -6
  286. package/dist/tools/create-dir.js +9 -5
  287. package/dist/tools/delete-file.js +9 -5
  288. package/dist/tools/download-file.js +32 -7
  289. package/dist/tools/edit-file.js +52 -14
  290. package/dist/tools/enable-tools.js +6 -6
  291. package/dist/tools/executor.js +132 -27
  292. package/dist/tools/file-info.js +3 -1
  293. package/dist/tools/filter-tools.js +3 -3
  294. package/dist/tools/glob-tool.js +4 -1
  295. package/dist/tools/grep-tool.js +13 -10
  296. package/dist/tools/hidden-tools-block.js +6 -6
  297. package/dist/tools/index.js +16 -8
  298. package/dist/tools/list-dir.js +4 -2
  299. package/dist/tools/load-skill.js +7 -0
  300. package/dist/tools/mcp-call.js +15 -4
  301. package/dist/tools/meet-done.js +14 -0
  302. package/dist/tools/move-file.js +14 -7
  303. package/dist/tools/path-utils.js +29 -29
  304. package/dist/tools/pipeline-run.js +29 -7
  305. package/dist/tools/preview.js +1 -1
  306. package/dist/tools/process-ids.js +31 -0
  307. package/dist/tools/process-kill.js +9 -16
  308. package/dist/tools/process-list.js +1 -0
  309. package/dist/tools/process-log.js +11 -16
  310. package/dist/tools/process-wait.js +63 -0
  311. package/dist/tools/prompt-io.js +4 -3
  312. package/dist/tools/question.js +172 -25
  313. package/dist/tools/read-file.js +55 -34
  314. package/dist/tools/recall.js +14 -8
  315. package/dist/tools/registry.js +6 -5
  316. package/dist/tools/remember.js +8 -4
  317. package/dist/tools/scope-request.js +11 -6
  318. package/dist/tools/search-history.js +5 -2
  319. package/dist/tools/session-info.js +2 -0
  320. package/dist/tools/set-thinking.js +8 -4
  321. package/dist/tools/subagent.js +59 -39
  322. package/dist/tools/syntax-validator.js +292 -90
  323. package/dist/tools/user-input.js +22 -18
  324. package/dist/tools/web-browse.js +9 -3
  325. package/dist/tools/web-fetch.js +12 -23
  326. package/dist/tools/web-search.js +7 -5
  327. package/dist/tools/write-file.js +45 -15
  328. package/dist/ui/action-log.js +13 -0
  329. package/dist/ui/box.js +4 -3
  330. package/dist/ui/colors.js +2 -0
  331. package/dist/ui/diff.js +6 -5
  332. package/dist/ui/index.js +1 -0
  333. package/dist/ui/line-editor.js +230 -122
  334. package/dist/ui/line-math.js +10 -2
  335. package/dist/ui/md-formatter.js +57 -37
  336. package/dist/ui/output.js +8 -3
  337. package/dist/ui/plan-view.js +21 -10
  338. package/dist/ui/renderer.js +393 -67
  339. package/dist/ui/spinner.js +16 -5
  340. package/dist/ui/table.js +15 -17
  341. package/dist/ui/theme.js +143 -0
  342. package/dist/utils/child-run.js +20 -0
  343. package/dist/utils/error.js +1 -1
  344. package/dist/utils/index.js +2 -0
  345. package/dist/utils/retry.js +2 -2
  346. package/dist/utils/truncate.js +1 -1
  347. package/package.json +1 -1
@@ -1,11 +1,18 @@
1
+ // Детект необходимости миграции: отличает конфиг v1 от v2 и битого файла.
1
2
  import { existsSync, readFileSync, readdirSync } from "fs";
2
3
  import { join } from "path";
3
4
  const V1_ONLY_FIELDS = ["apiBase", "apiKey", "chatEndpoint", "temperature", "maxTokens"];
5
+ /** Проверяет, лежит ли в configDir конфиг формата v1. Битый файл — не v1. */
4
6
  export class MigrationDetector {
5
7
  configDir;
6
8
  constructor(configDir) {
7
9
  this.configDir = configDir;
8
10
  }
11
+ /**
12
+ * Нужна ли миграция: есть v1-поля или нет всех обязательных v2-полей.
13
+ * Битый/вручную правленный конфиг — не повод: loadConfig предупредит
14
+ * и откатится на дефолты, файл не трогаем.
15
+ */
9
16
  needsMigration() {
10
17
  const configPath = join(this.configDir, "config.json");
11
18
  if (!existsSync(configPath))
@@ -13,7 +20,7 @@ export class MigrationDetector {
13
20
  try {
14
21
  const content = readFileSync(configPath, "utf-8");
15
22
  const config = JSON.parse(content);
16
- if (config.version && config.version.startsWith("2."))
23
+ if (config.version?.startsWith("2."))
17
24
  return false;
18
25
  for (const field of V1_ONLY_FIELDS) {
19
26
  if (field in config)
@@ -26,17 +33,19 @@ export class MigrationDetector {
26
33
  return false;
27
34
  }
28
35
  catch {
29
- // A corrupt or hand-edited config must NOT be treated as v1 and deleted.
30
- // loadConfig will warn and fall back to defaults instead.
36
+ // Битый или вручную правленный конфиг — НЕ v1, удалять нельзя.
37
+ // loadConfig предупредит и откатится на дефолты.
31
38
  return false;
32
39
  }
33
40
  }
41
+ /** Есть ли старые сессии в каталоге sessions. */
34
42
  hasOldSessions() {
35
43
  const sessionsDir = join(this.configDir, "sessions");
36
44
  if (!existsSync(sessionsDir))
37
45
  return false;
38
46
  return readdirSync(sessionsDir).length > 0;
39
47
  }
48
+ /** Профиль старого формата: есть profile.json без полей platform/os. */
40
49
  hasOldProfile() {
41
50
  const profilePath = join(this.configDir, "profile.json");
42
51
  if (!existsSync(profilePath))
@@ -1,2 +1,3 @@
1
+ // Миграция конфига v1 → v2: детект устаревшего формата и бэкапы.
1
2
  export { MigrationDetector } from "./detect";
2
3
  export { BackupManager } from "./backup";
@@ -1,15 +1,16 @@
1
+ // Хранилище артефактов сессии: запись/чтение файлов в <baseDir>/.mma/artifacts/<sessionId>.
1
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { resolve, relative, isAbsolute } from "node:path";
4
+ /** Файловое хранилище артефактов сессии (jail внутри root). */
3
5
  export class ArtifactStore {
4
6
  root;
5
7
  constructor(opts) {
6
8
  const artifactsDir = opts.artifactsDir || ".mma/artifacts";
7
9
  this.root = resolve(opts.baseDir, artifactsDir, opts.sessionId);
8
10
  }
9
- /** Normalize to [a-zA-Z0-9._-]; never empty, never "." or "..". */
11
+ /** Нормализует имя к [a-zA-Z0-9._-]; никогда не пустое, не "." и не "..". */
10
12
  static sanitizeName(name) {
11
- // Strip traversal prefixes so "../../etc/passwd" becomes "etc_passwd",
12
- // never ".._.._etc_passwd".
13
+ // Срезаем префиксы обхода: "../../etc/passwd" станет "etc_passwd", а не ".._.._etc_passwd".
13
14
  const stripped = name.replace(/^(\.\.?[\\/])+/g, "").replace(/^\.{1,2}$/, "");
14
15
  const cleaned = stripped
15
16
  .replace(/[^a-zA-Z0-9._-]/g, "_")
@@ -20,7 +21,7 @@ export class ArtifactStore {
20
21
  }
21
22
  return cleaned;
22
23
  }
23
- /** True when `candidate` is strictly inside (or equal to) `root`. */
24
+ /** True, если `candidate` строго внутри `root` (или равен ему). */
24
25
  static isInside(root, candidate) {
25
26
  const rel = relative(root, candidate);
26
27
  if (rel === "")
@@ -28,8 +29,8 @@ export class ArtifactStore {
28
29
  return !rel.startsWith("..") && !isAbsolute(rel);
29
30
  }
30
31
  /**
31
- * Persist content to <root>/<name>.<ext> and return the absolute path.
32
- * Throws if the resolved path escapes root (defense in depth — sanitizeName already removes separators).
32
+ * Сохраняет контент в <root>/<name>.<ext>, возвращает абсолютный путь.
33
+ * Бросает, если путь выходит за root (defense in depth — sanitizeName уже убрал разделители).
33
34
  */
34
35
  save(name, content, ext = "md") {
35
36
  const safe = ArtifactStore.sanitizeName(name);
@@ -42,7 +43,7 @@ export class ArtifactStore {
42
43
  writeFileSync(abs, content, "utf8");
43
44
  return abs;
44
45
  }
45
- /** Read an artifact by its file name (relative to root); null when outside the root or missing. */
46
+ /** Читает артефакт по имени файла (относительно root); null, если вне root или нет файла. */
46
47
  read(path) {
47
48
  const abs = resolve(this.root, path);
48
49
  if (!ArtifactStore.isInside(this.root, abs))
@@ -51,7 +52,7 @@ export class ArtifactStore {
51
52
  return null;
52
53
  return readFileSync(abs, "utf8");
53
54
  }
54
- /** First maxChars chars (trimmed) + truncation marker. */
55
+ /** Первые maxChars символов (обрезанные справа) + маркер усечения. */
55
56
  summary(content, maxChars) {
56
57
  if (content.length <= maxChars)
57
58
  return content;
@@ -1,3 +1,5 @@
1
+ // JS-сниппеты для действий в странице: клик, ввод, скролл, разметка, извлечение текста.
2
+ /** JS для клика по элементу с индексом из снапшота. */
1
3
  export function buildClickScript(elementIndex) {
2
4
  return `
3
5
  const el = document.querySelector('[data-browser-index="${elementIndex}"]');
@@ -5,6 +7,7 @@ export function buildClickScript(elementIndex) {
5
7
  el.click();
6
8
  `;
7
9
  }
10
+ /** JS для ввода текста в поле: фокус, установка значения, события input/change. */
8
11
  export function buildTypeScript(elementIndex, text) {
9
12
  const escaped = JSON.stringify(text);
10
13
  return `
@@ -17,6 +20,7 @@ export function buildTypeScript(elementIndex, text) {
17
20
  el.dispatchEvent(new Event('change', { bubbles: true }));
18
21
  `;
19
22
  }
23
+ /** JS для прокрутки страницы в заданном направлении. */
20
24
  export function buildScrollScript(direction) {
21
25
  switch (direction) {
22
26
  case "down":
@@ -29,6 +33,7 @@ export function buildScrollScript(direction) {
29
33
  return `window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })`;
30
34
  }
31
35
  }
36
+ /** JS для простановки интерактивным элементам атрибутов data-browser-index. */
32
37
  export function buildIndexInjectionScript() {
33
38
  return `
34
39
  const selectors = 'a[href], button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [contenteditable="true"]';
@@ -41,6 +46,7 @@ export function buildIndexInjectionScript() {
41
46
  });
42
47
  `;
43
48
  }
49
+ /** JS для извлечения видимого текстового содержимого страницы построчно. */
44
50
  export function buildTextExtractionScript() {
45
51
  return `
46
52
  const contentTags = new Set(['h1','h2','h3','h4','h5','h6','p','li','article','main','section','blockquote','pre','td','th','dt','dd','figcaption']);
@@ -1,18 +1,19 @@
1
+ // Драйвер через node-мост (дочерний процесс bridge-server.mjs по JSON-RPC поверх stdio).
1
2
  import { spawn } from "child_process";
2
3
  import { createInterface } from "readline";
3
4
  import { dirname, join } from "path";
4
5
  import { fileURLToPath } from "url";
5
- import { ConsoleBuffer } from "./session";
6
+ import { ConsoleBuffer } from "./console-buffer";
6
7
  import { pickExistingPath } from "./bridge-path";
7
- const BRIDGE_REQUEST_TIMEOUT_MS = 30000;
8
+ import { BROWSER_BRIDGE_REQUEST_TIMEOUT_MS as BRIDGE_REQUEST_TIMEOUT_MS } from "../../config/timeouts";
8
9
  function bridgeScriptPath() {
9
- // Resolve the bridge script for BOTH layouts (rule #12 — read from disk):
10
+ // Резолв скрипта моста для ОБЕИХ раскладок (пути читаются с диска):
10
11
  // src/modules/browser/bridge-server.mjs (bun run src/…)
11
- // dist/modules/browser/bridge-server.mjs (bundle at dist/main.js +
12
- // assets copied by copy-assets.ts)
13
- // The single-candidate version looked next to the bundle (dist/bridge-server.mjs),
14
- // which copy-assets.ts never produces — so the browser tool died with ENOENT
15
- // in the released npm package (observed: v0.37.0).
12
+ // dist/modules/browser/bridge-server.mjs (бандл dist/main.js +
13
+ // ассеты от copy-assets.ts)
14
+ // Версия с одним кандидатом искала рядом с бандлом (dist/bridge-server.mjs),
15
+ // чего copy-assets.ts не создаёт, — и тул браузера падал с ENOENT
16
+ // в опубликованном npm-пакете (наблюдалось: v0.37.0).
16
17
  const dir = dirname(fileURLToPath(import.meta.url));
17
18
  const candidates = [
18
19
  join(dir, "bridge-server.mjs"),
@@ -20,6 +21,7 @@ function bridgeScriptPath() {
20
21
  ];
21
22
  return pickExistingPath(candidates);
22
23
  }
24
+ /** Драйвер поверх node-моста: команды шлются JSON-строками в stdin дочернего процесса. */
23
25
  export class BridgeDriver {
24
26
  proc = null;
25
27
  rl = null;
@@ -50,7 +52,7 @@ export class BridgeDriver {
50
52
  clearTimeout(timer);
51
53
  resolve(resp);
52
54
  });
53
- this.proc.stdin.write(JSON.stringify({ id, cmd, params }) + "\n");
55
+ this.proc.stdin.write(`${JSON.stringify({ id, cmd, params })}\n`);
54
56
  });
55
57
  this.drainEvents(response);
56
58
  if (!response.ok)
@@ -108,14 +110,14 @@ export class BridgeDriver {
108
110
  });
109
111
  proc.on("error", (err) => {
110
112
  if (this.proc !== proc)
111
- return; // stale child — a newer bridge is alive
113
+ return; // протухший потомок — жив уже новый мост
112
114
  this.failPending(`Bridge process failed to start: ${err.message}`);
113
115
  this.proc = null;
114
116
  this.rl = null;
115
117
  });
116
118
  proc.on("exit", (code) => {
117
119
  if (this.proc !== proc)
118
- return; // stale child — a newer bridge is alive
120
+ return; // протухший потомок — жив уже новый мост
119
121
  const stderrTail = this.lastStderr.join("").trim();
120
122
  this.failPending(stderrTail
121
123
  ? `Bridge process exited (code ${code}): ${stderrTail.slice(0, 300)}`
@@ -184,7 +186,7 @@ export class BridgeDriver {
184
186
  await this.send("close");
185
187
  }
186
188
  catch {
187
- // ignore — we're shutting down
189
+ // игнорируем — идёт завершение работы
188
190
  }
189
191
  this.dispose();
190
192
  }
@@ -1,9 +1,9 @@
1
+ // Поиск скрипта node-моста: раскладки src/ и dist/ (тестируемо без запуска моста).
1
2
  import { existsSync } from "fs";
2
3
  /**
3
- * Pick the first existing candidate path, falling back to the first candidate.
4
- * Split out of bridge-client.ts so the source-vs-dist layout resolution is
5
- * unit-testable without spawning a node bridge (rule #12: build-time values
6
- * are resolved from disk at runtime, never inlined).
4
+ * Первый существующий путь из кандидатов, иначе — первый кандидат.
5
+ * Вынесено из bridge-client.ts, чтобы резолв раскладки src-vs-dist
6
+ * тестировался без спавна node-моста (пути читаются с диска в рантайме, не инлайнятся).
7
7
  */
8
8
  export function pickExistingPath(candidates, fallback = candidates[0]) {
9
9
  return candidates.find((p) => existsSync(p)) ?? fallback;
@@ -0,0 +1,37 @@
1
+ const CONSOLE_MAX_LINE = 400;
2
+ /**
3
+ * Ограниченный буфер записей консоли браузера.
4
+ * Вынесен из `session.ts`, чтобы `driver.ts` и `bridge-client.ts` делили его
5
+ * БЕЗ импорта `session.ts` — это ребро давало два цикла импортов
6
+ * (`driver ↔ session`, `driver → bridge-client → session`).
7
+ */
8
+ export class ConsoleBuffer {
9
+ entries = [];
10
+ maxEntries;
11
+ constructor(maxEntries = 20) {
12
+ this.maxEntries = maxEntries;
13
+ }
14
+ /** Добавить запись; пустые отбрасываются, длинные строки режутся. */
15
+ add(type, text) {
16
+ const clean = String(text).replace(/\s+/g, " ").trim();
17
+ if (!clean)
18
+ return;
19
+ const line = clean.length > CONSOLE_MAX_LINE ? `${clean.slice(0, CONSOLE_MAX_LINE)}…` : clean;
20
+ this.entries.push({ type, text: line });
21
+ if (this.entries.length > this.maxEntries) {
22
+ this.entries.splice(0, this.entries.length - this.maxEntries);
23
+ }
24
+ }
25
+ /** Очистить буфер. */
26
+ clear() {
27
+ this.entries = [];
28
+ }
29
+ /** Все записи копией. */
30
+ getAll() {
31
+ return [...this.entries];
32
+ }
33
+ /** Текущее число записей. */
34
+ get size() {
35
+ return this.entries.length;
36
+ }
37
+ }
@@ -1,14 +1,18 @@
1
+ // Персистентное хранилище cookies браузера (cookies.json в cookieDir).
1
2
  import { readFile, writeFile, mkdir } from "fs/promises";
2
3
  import { join } from "path";
4
+ /** Файловое хранилище кук (cookies.json). */
3
5
  export class CookieStore {
4
6
  filePath;
5
7
  constructor(cookieDir) {
6
8
  this.filePath = join(cookieDir, "cookies.json");
7
9
  }
10
+ /** Сохранить куки в файл. */
8
11
  async save(cookies) {
9
12
  await mkdir(join(this.filePath, ".."), { recursive: true });
10
13
  await writeFile(this.filePath, JSON.stringify(cookies, null, 2), "utf-8");
11
14
  }
15
+ /** Загрузить куки; при отсутствии/битом файле — пустой список. */
12
16
  async load() {
13
17
  try {
14
18
  const data = await readFile(this.filePath, "utf-8");
@@ -18,6 +22,7 @@ export class CookieStore {
18
22
  return [];
19
23
  }
20
24
  }
25
+ /** Удалить все куки. */
21
26
  async clear() {
22
27
  await this.save([]);
23
28
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,9 +1,15 @@
1
+ // Playwright-драйвер: запуск Chromium, навигация, снапшоты, выбор транспорта.
1
2
  import { chromium } from "playwright";
2
- import { ConsoleBuffer } from "./session";
3
- export const DIRECT_LAUNCH_TIMEOUT_MS = 8000;
3
+ import { ConsoleBuffer } from "./console-buffer";
4
+ import { MMA_ENV } from "../../config/env";
5
+ import { BROWSER_DIRECT_LAUNCH_TIMEOUT_MS as CANONICAL_DIRECT_LAUNCH_TIMEOUT_MS } from "../../config/timeouts";
6
+ /** Таймаут прямого запуска Playwright до отката на node-мост. */
7
+ /** Публичный экспорт-алиас: то же значение из реестра. */
8
+ export const DIRECT_LAUNCH_TIMEOUT_MS = CANONICAL_DIRECT_LAUNCH_TIMEOUT_MS;
9
+ /** Оверрайды запуска из env (MMA_BROWSER_EXECUTABLE/MMA_BROWSER_CHANNEL). */
4
10
  export function resolveBrowserLaunchOverrides(env = process.env) {
5
- const executablePath = env.MMA_BROWSER_EXECUTABLE?.trim();
6
- const channel = env.MMA_BROWSER_CHANNEL?.trim();
11
+ const executablePath = env[MMA_ENV.BROWSER_EXECUTABLE]?.trim();
12
+ const channel = env[MMA_ENV.BROWSER_CHANNEL]?.trim();
7
13
  const out = {};
8
14
  if (executablePath)
9
15
  out.executablePath = executablePath;
@@ -11,15 +17,15 @@ export function resolveBrowserLaunchOverrides(env = process.env) {
11
17
  out.channel = channel;
12
18
  return out;
13
19
  }
14
- /** System browsers to try when the bundled Chromium revision is missing. */
20
+ /** Системные браузеры, если bundled-ревизия Chromium отсутствует. */
15
21
  const SYSTEM_CHANNEL_FALLBACKS = ["chrome", "msedge"];
16
22
  function isMissingBrowserError(err) {
17
23
  return /Executable doesn't exist|is not found|looks like Playwright was just installed/i.test(String(err?.message ?? err));
18
24
  }
19
25
  /**
20
- * Launch Chromium, falling back to an installed system Chrome/Edge when the
21
- * bundled revision is absent (CDN unreachable). An explicit override always
22
- * wins and suppresses the fallback — the user's choice must fail loudly.
26
+ * Запуск Chromium с фолбэком на системный Chrome/Edge, если bundled-ревизии
27
+ * нет (CDN недоступен). Явный оверрайд всегда побеждает и подавляет фолбэк —
28
+ * выбор пользователя должен падать громко.
23
29
  */
24
30
  export async function launchChromiumWithFallback(launch, cfg, overrides = {}) {
25
31
  const base = { headless: cfg.headless, timeout: cfg.timeoutMs, ...overrides };
@@ -45,6 +51,7 @@ export async function launchChromiumWithFallback(launch, cfg, overrides = {}) {
45
51
  function launchChromium(cfg, overrides) {
46
52
  return launchChromiumWithFallback((opts) => chromium.launch(opts), cfg, overrides);
47
53
  }
54
+ /** Драйвер поверх Playwright: страница, консоль, сетевые ошибки. */
48
55
  export class PlaywrightDriver {
49
56
  browser = null;
50
57
  context = null;
@@ -145,6 +152,7 @@ export class PlaywrightDriver {
145
152
  return this.page;
146
153
  }
147
154
  }
155
+ /** Создать драйвер: прямой Playwright, при неудаче — node-мост. */
148
156
  export function createBrowserDriver(config) {
149
157
  const common = {
150
158
  headless: config.headless,
@@ -153,9 +161,9 @@ export function createBrowserDriver(config) {
153
161
  overrides: resolveBrowserLaunchOverrides(),
154
162
  };
155
163
  return (async () => {
156
- // Playwright's pipe/websocket transport is broken under Bun on some
157
- // platforms (see oven-sh/bun#31105, #9911). Under Bun we always use the
158
- // Node bridge. Under Node we try the direct path first.
164
+ // Транспорт pipe/websocket у Playwright сломан под Bun на части платформ
165
+ // (см. oven-sh/bun#31105, #9911). Под Bun всегда идём через node-мост,
166
+ // под Node сначала пробуем прямой путь.
159
167
  if (process.versions.bun) {
160
168
  return launchBridge(config, common);
161
169
  }
@@ -1,5 +1,7 @@
1
+ // Публичный фасад модуля браузера: сессия, драйверы, снапшоты, типы.
1
2
  export { BrowserModule } from "./module";
2
- export { BrowserSession, BrowserActionTracker, ConsoleBuffer } from "./session";
3
+ export { BrowserSession, BrowserActionTracker } from "./session";
4
+ export { ConsoleBuffer } from "./console-buffer";
3
5
  export { CookieStore } from "./cookie-store";
4
6
  export { createBrowserDriver, PlaywrightDriver } from "./driver";
5
7
  export { BridgeDriver } from "./bridge-client";
@@ -1,4 +1,5 @@
1
1
  import { createBrowserTool, closeBrowserSession } from "../../tools/browser";
2
+ /** Модуль headless-браузера: промпт workflow, тул, плагин закрытия сессии. */
2
3
  export class BrowserModule {
3
4
  name = "browser";
4
5
  getSystemPromptBlock() {
@@ -8,7 +9,7 @@ export class BrowserModule {
8
9
  "Workflow: open(url) → read snapshot → click/type on element numbers → read updated snapshot.",
9
10
  'Each snapshot includes a "Content" section with the visible page text, a "Console" section with browser messages, and network errors. Use these to understand the page, detect JS errors, and verify the result of your actions.',
10
11
  'Always start with "open" action. Use element numbers from snapshot for click/type targets.',
11
- 'Use "screenshot" action only if the model supports vision. Otherwise rely on the text snapshot.',
12
+ 'The "screenshot" action is delivered automatically: a vision model receives the image, a text-only model receives the text snapshot instead. You do not need to decide — just call it and read what you get.',
12
13
  ].join("\n"),
13
14
  priority: "normal",
14
15
  essential: false,
@@ -3,39 +3,14 @@ import { extractInteractiveElements, formatSnapshot } from "./snapshot";
3
3
  import { buildClickScript, buildTypeScript, buildScrollScript, buildIndexInjectionScript, buildTextExtractionScript, } from "./actions";
4
4
  import { CookieStore } from "./cookie-store";
5
5
  import { t } from "../../i18n/index";
6
- const CONSOLE_MAX_LINE = 400;
7
- export class ConsoleBuffer {
8
- entries = [];
9
- maxEntries;
10
- constructor(maxEntries = 20) {
11
- this.maxEntries = maxEntries;
12
- }
13
- add(type, text) {
14
- const clean = String(text).replace(/\s+/g, " ").trim();
15
- if (!clean)
16
- return;
17
- const line = clean.length > CONSOLE_MAX_LINE ? clean.slice(0, CONSOLE_MAX_LINE) + "…" : clean;
18
- this.entries.push({ type, text: line });
19
- if (this.entries.length > this.maxEntries) {
20
- this.entries.splice(0, this.entries.length - this.maxEntries);
21
- }
22
- }
23
- clear() {
24
- this.entries = [];
25
- }
26
- getAll() {
27
- return [...this.entries];
28
- }
29
- get size() {
30
- return this.entries.length;
31
- }
32
- }
6
+ /** Ловит N одинаковых действий подряд на одном URL и возвращает предупреждение. */
33
7
  export class BrowserActionTracker {
34
8
  history = [];
35
9
  threshold;
36
10
  constructor(threshold = 3) {
37
11
  this.threshold = threshold;
38
12
  }
13
+ /** Записать действие; при серии повторов — текст предупреждения, иначе null. */
39
14
  record(action, args, url) {
40
15
  this.history.push({ action, args, url });
41
16
  if (this.history.length < this.threshold)
@@ -52,10 +27,12 @@ export class BrowserActionTracker {
52
27
  }
53
28
  return null;
54
29
  }
30
+ /** Сбросить историю (вызывается при open). */
55
31
  reset() {
56
32
  this.history = [];
57
33
  }
58
34
  }
35
+ /** Сессия: ленивый драйвер, куки, выполнение действий модели, снапшоты. */
59
36
  export class BrowserSession {
60
37
  driver = null;
61
38
  config;
@@ -71,9 +48,11 @@ export class BrowserSession {
71
48
  this.config = config;
72
49
  this.cookieStore = new CookieStore(config.cookieDir);
73
50
  }
51
+ /** Копия текущего состояния (открыта/URL/заголовок/счётчик элементов). */
74
52
  getState() {
75
53
  return { ...this.state };
76
54
  }
55
+ /** Выполнить действие модели (open/click/type/…); ошибки — текстом в ActionResult. */
77
56
  async execute(action, args) {
78
57
  try {
79
58
  let result;
@@ -117,7 +96,7 @@ export class BrowserSession {
117
96
  if (action !== "open") {
118
97
  const warning = this.actionTracker.record(action, args, this.driver?.url() || "");
119
98
  if (warning && result.success) {
120
- result.output = result.output + "\n\n⚠️ " + warning;
99
+ result.output = `${result.output}\n\n⚠️ ${warning}`;
121
100
  }
122
101
  }
123
102
  return result;
@@ -186,13 +165,13 @@ export class BrowserSession {
186
165
  async open(url) {
187
166
  if (!url)
188
167
  return { success: false, output: t("browser.url_required") };
189
- // Security: block dangerous schemes (file:, chrome:, about:, data:).
168
+ // Блок опасных схем (file:, chrome:, about:, data:) — обход запретов исключён.
190
169
  if (/^(file|chrome|about|data|javascript|view-source):/i.test(url)) {
191
170
  return { success: false, output: `[SECURITY BLOCKED] scheme not allowed: ${url}` };
192
171
  }
193
172
  if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("file://")) {
194
173
  const isLocalhost = /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?$/i.test(url);
195
- url = isLocalhost ? "http://" + url : "https://" + url;
174
+ url = isLocalhost ? `http://${url}` : `https://${url}`;
196
175
  }
197
176
  await this.launch();
198
177
  if (!this.driver)
@@ -330,6 +309,7 @@ export class BrowserSession {
330
309
  const snapshot = await this.takeSnapshot();
331
310
  return { success: true, output: snapshot };
332
311
  }
312
+ /** Закрыть сессию: сохранить куки, убить драйвер, сбросить состояние. */
333
313
  async close() {
334
314
  await this.saveCookies();
335
315
  if (this.driver) {
@@ -24,11 +24,11 @@ function inferRole(tag, type) {
24
24
  }
25
25
  function getName(tag, attrs, html, matchIndex) {
26
26
  const fromAttr = attrs["aria-label"] ||
27
- attrs["placeholder"] ||
28
- attrs["title"] ||
29
- attrs["alt"] ||
30
- attrs["value"] ||
31
- attrs["name"] ||
27
+ attrs.placeholder ||
28
+ attrs.title ||
29
+ attrs.alt ||
30
+ attrs.value ||
31
+ attrs.name ||
32
32
  "";
33
33
  if (fromAttr)
34
34
  return fromAttr;
@@ -47,6 +47,10 @@ function getName(tag, attrs, html, matchIndex) {
47
47
  .trim();
48
48
  return text.slice(0, 50);
49
49
  }
50
+ /**
51
+ * Вытащить интерактивные элементы из HTML (ссылки, кнопки, поля).
52
+ * @param html сырой HTML страницы. @param maxElements лимит элементов.
53
+ */
50
54
  export function extractInteractiveElements(html, maxElements = 30) {
51
55
  const elements = [];
52
56
  const tagRegex = /<(a|button|input|textarea|select|div|span)([^>]*)>/gi;
@@ -62,19 +66,19 @@ export function extractInteractiveElements(html, maxElements = 30) {
62
66
  while ((attrMatch = attrRegex.exec(attrStr)) !== null) {
63
67
  attrs[attrMatch[1].toLowerCase()] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
64
68
  }
65
- if (attrs["style"]?.includes("display:none") || attrs["style"]?.includes("display: none"))
69
+ if (attrs.style?.includes("display:none") || attrs.style?.includes("display: none"))
66
70
  continue;
67
- if (attrs["hidden"] !== undefined)
71
+ if (attrs.hidden !== undefined)
68
72
  continue;
69
73
  if (attrs["aria-hidden"] === "true")
70
74
  continue;
71
- if (tag === "input" && attrs["type"] === "hidden")
75
+ if (tag === "input" && attrs.type === "hidden")
72
76
  continue;
73
- const role = attrs["role"] || inferRole(tag, attrs["type"]);
74
- const isInteractive = ["a", "button", "input", "textarea", "select"].includes(tag) || attrs["role"] !== undefined;
77
+ const role = attrs.role || inferRole(tag, attrs.type);
78
+ const isInteractive = ["a", "button", "input", "textarea", "select"].includes(tag) || attrs.role !== undefined;
75
79
  if (!isInteractive)
76
80
  continue;
77
- if (tag === "a" && !attrs["href"])
81
+ if (tag === "a" && !attrs.href)
78
82
  continue;
79
83
  const name = getName(tag, attrs, html, match.index);
80
84
  elements.push({
@@ -82,13 +86,14 @@ export function extractInteractiveElements(html, maxElements = 30) {
82
86
  tag,
83
87
  role,
84
88
  name,
85
- value: attrs["value"],
86
- href: attrs["href"],
89
+ value: attrs.value,
90
+ href: attrs.href,
87
91
  visible: true,
88
92
  });
89
93
  }
90
94
  return elements;
91
95
  }
96
+ /** Обрезать текст по лимиту, предпочтительно по границе строки. */
92
97
  export function truncateText(text, maxChars) {
93
98
  if (maxChars <= 0)
94
99
  return "";
@@ -99,6 +104,7 @@ export function truncateText(text, maxChars) {
99
104
  const safe = lastNewline > maxChars * 0.6 ? cut.slice(0, lastNewline) : cut;
100
105
  return `${safe}\n${t("browser.truncated")}`;
101
106
  }
107
+ /** Собрать текстовый снапшот: элементы, контент, консоль, сетевые ошибки. */
102
108
  export function formatSnapshot(snapshot, maxContentChars = DEFAULT_BROWSER_CONFIG.maxContentChars) {
103
109
  const lines = [];
104
110
  lines.push(`Page: "${snapshot.title}"`);
@@ -1,3 +1,4 @@
1
+ /** Дефолтный конфиг браузера. */
1
2
  export const DEFAULT_BROWSER_CONFIG = {
2
3
  headless: true,
3
4
  maxElements: 30,