micro-models-agent 0.40.1 → 0.41.2

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 (171) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +9 -19
  3. package/dist/cli/completer.js +36 -37
  4. package/dist/cli/index.js +2 -2
  5. package/dist/cli/main.js +48 -23
  6. package/dist/cli/plugin-commands.js +36 -0
  7. package/dist/cli/repl-commands.js +40 -12
  8. package/dist/cli/repl.js +217 -87
  9. package/dist/cli/run-result.js +22 -0
  10. package/dist/cli/security-commands.js +5 -7
  11. package/dist/cli/setup.js +8 -26
  12. package/dist/config/config.js +52 -5
  13. package/dist/config/defaults.js +29 -5
  14. package/dist/config/experts.js +1 -1
  15. package/dist/config/index.js +3 -3
  16. package/dist/config/security.js +3 -10
  17. package/dist/core/agent-moe.js +2 -10
  18. package/dist/core/agent.js +273 -82
  19. package/dist/core/bootstrap.js +80 -13
  20. package/dist/core/index.js +2 -2
  21. package/dist/core/prompt-builder.js +23 -2
  22. package/dist/core/session-logger.js +46 -4
  23. package/dist/core/version.js +24 -0
  24. package/dist/i18n/en.json +75 -2
  25. package/dist/i18n/ru.json +74 -1
  26. package/dist/index.js +1 -1
  27. package/dist/llm/image-utils.js +4 -5
  28. package/dist/llm/index.js +4 -4
  29. package/dist/llm/model-loader.js +6 -6
  30. package/dist/llm/openai-compat.js +40 -34
  31. package/dist/llm/orchestrator.js +33 -29
  32. package/dist/llm/response.js +9 -9
  33. package/dist/logger/app-logger.js +1 -1
  34. package/dist/logger/index.js +1 -1
  35. package/dist/main.js +2489 -2186
  36. package/dist/migration/backup.js +13 -13
  37. package/dist/migration/detect.js +11 -11
  38. package/dist/migration/index.js +2 -2
  39. package/dist/modules/artifacts/store.js +61 -0
  40. package/dist/modules/browser/actions.js +34 -4
  41. package/dist/modules/browser/bridge-client.js +199 -0
  42. package/dist/modules/browser/bridge-path.js +10 -0
  43. package/dist/modules/browser/bridge-server.mjs +202 -202
  44. package/dist/modules/browser/cookie-store.js +6 -6
  45. package/dist/modules/browser/driver.js +136 -0
  46. package/dist/modules/browser/index.js +7 -5
  47. package/dist/modules/browser/module.js +8 -7
  48. package/dist/modules/browser/session.js +87 -84
  49. package/dist/modules/browser/snapshot.js +92 -58
  50. package/dist/modules/browser/types.js +4 -1
  51. package/dist/modules/certification/cli.js +2 -4
  52. package/dist/modules/certification/fact-checker.js +1 -3
  53. package/dist/modules/certification/loader.js +3 -9
  54. package/dist/modules/certification/runner.js +1 -4
  55. package/dist/modules/context/chunk-query.js +100 -0
  56. package/dist/modules/context/fact-extractor.js +162 -0
  57. package/dist/modules/context/history.js +15 -0
  58. package/dist/modules/context/index.js +1 -1
  59. package/dist/modules/context/manager.js +160 -86
  60. package/dist/modules/execution/audit-runners.js +152 -0
  61. package/dist/modules/execution/auditor.js +177 -25
  62. package/dist/modules/execution/execution-plugin.js +272 -0
  63. package/dist/modules/execution/module.js +201 -544
  64. package/dist/modules/execution/moe-executor.js +25 -0
  65. package/dist/modules/execution/plan-store.js +1 -3
  66. package/dist/modules/execution/plan-tool.js +508 -0
  67. package/dist/modules/execution/plan-validator.js +10 -10
  68. package/dist/modules/execution/planner.js +6 -1
  69. package/dist/modules/execution/stuck-detector.js +173 -10
  70. package/dist/modules/execution/verifier.js +86 -42
  71. package/dist/modules/execution/windows-commands.js +41 -0
  72. package/dist/modules/hallucination/confidence.js +8 -1
  73. package/dist/modules/hallucination/detector.js +2 -5
  74. package/dist/modules/hallucination/factual.js +3 -64
  75. package/dist/modules/hallucination/index.js +1 -1
  76. package/dist/modules/hallucination/js-identifiers.js +190 -0
  77. package/dist/modules/hallucination/llm-judge.js +1 -3
  78. package/dist/modules/indexer/cache.js +9 -7
  79. package/dist/modules/indexer/index.js +3 -3
  80. package/dist/modules/indexer/module.js +95 -42
  81. package/dist/modules/indexer/project-profile.js +183 -0
  82. package/dist/modules/indexer/walker.js +17 -17
  83. package/dist/modules/lsp/check-tool.js +58 -0
  84. package/dist/modules/lsp/client.js +74 -31
  85. package/dist/modules/lsp/command.js +60 -0
  86. package/dist/modules/lsp/config.js +87 -33
  87. package/dist/modules/lsp/index.js +3 -3
  88. package/dist/modules/lsp/module.js +185 -21
  89. package/dist/modules/lsp/probe.js +76 -0
  90. package/dist/modules/lsp/project-root.js +32 -0
  91. package/dist/modules/lsp/startup-check.js +141 -0
  92. package/dist/modules/mcp/module.js +2 -6
  93. package/dist/modules/memory/index.js +1 -1
  94. package/dist/modules/memory/module.js +71 -23
  95. package/dist/modules/memory/search.js +11 -9
  96. package/dist/modules/memory/store.js +13 -13
  97. package/dist/modules/pipelines/engine.js +10 -10
  98. package/dist/modules/pipelines/index.js +3 -3
  99. package/dist/modules/pipelines/parser.js +17 -14
  100. package/dist/modules/pipelines/template.js +1 -1
  101. package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
  102. package/dist/modules/plugins/builtin/notify.js +3 -2
  103. package/dist/modules/plugins/index.js +1 -1
  104. package/dist/modules/plugins/loader.js +59 -17
  105. package/dist/modules/plugins/manager.js +73 -17
  106. package/dist/modules/processes/detect.js +34 -0
  107. package/dist/modules/processes/index.js +1 -1
  108. package/dist/modules/processes/registry.js +135 -46
  109. package/dist/modules/registry.js +4 -2
  110. package/dist/modules/security/audit-notifier.js +39 -39
  111. package/dist/modules/security/command-validator.js +2 -8
  112. package/dist/modules/security/data-sanitizer.js +1 -9
  113. package/dist/modules/security/encryption.js +58 -56
  114. package/dist/modules/security/network-validator.js +1 -9
  115. package/dist/modules/security/path-validator.js +1 -3
  116. package/dist/modules/security/security-policies.js +3 -19
  117. package/dist/modules/security/session-encryption.js +1 -1
  118. package/dist/modules/security/session-isolation.js +8 -8
  119. package/dist/modules/session/index.js +3 -3
  120. package/dist/modules/session/module.js +5 -5
  121. package/dist/modules/session/store.js +3 -9
  122. package/dist/modules/skills/matcher.js +27 -0
  123. package/dist/modules/skills/module.js +1 -2
  124. package/dist/modules/updater/checker.js +70 -6
  125. package/dist/modules/updater/index.js +2 -1
  126. package/dist/modules/updater/module.js +116 -0
  127. package/dist/modules/user-profile/compressor.js +2 -2
  128. package/dist/modules/user-profile/index.js +1 -1
  129. package/dist/modules/user-profile/profile.js +9 -9
  130. package/dist/tools/attach-image.js +1 -1
  131. package/dist/tools/bash.js +178 -19
  132. package/dist/tools/browser.js +46 -29
  133. package/dist/tools/chunk-query.js +99 -0
  134. package/dist/tools/download-file.js +116 -0
  135. package/dist/tools/enable-tools.js +58 -0
  136. package/dist/tools/executor.js +4 -5
  137. package/dist/tools/file-info.js +13 -12
  138. package/dist/tools/filter-tools.js +9 -2
  139. package/dist/tools/glob-tool.js +11 -11
  140. package/dist/tools/grep-tool.js +1 -3
  141. package/dist/tools/hidden-tools-block.js +37 -0
  142. package/dist/tools/index.js +13 -2
  143. package/dist/tools/list-dir.js +18 -17
  144. package/dist/tools/load-skill.js +1 -3
  145. package/dist/tools/path-utils.js +4 -4
  146. package/dist/tools/pipeline-run.js +25 -25
  147. package/dist/tools/process-kill.js +11 -11
  148. package/dist/tools/process-list.js +20 -22
  149. package/dist/tools/process-log.js +22 -18
  150. package/dist/tools/question.js +1 -3
  151. package/dist/tools/read-file.js +10 -2
  152. package/dist/tools/recall.js +44 -37
  153. package/dist/tools/registry.js +15 -4
  154. package/dist/tools/remember.js +29 -29
  155. package/dist/tools/scope-check.js +9 -9
  156. package/dist/tools/subagent.js +54 -9
  157. package/dist/tools/user-input.js +1 -1
  158. package/dist/tools/web-browse.js +3 -3
  159. package/dist/tools/web-fetch.js +3 -3
  160. package/dist/tools/web-search.js +3 -3
  161. package/dist/tools/write-file.js +1 -3
  162. package/dist/ui/box.js +1 -5
  163. package/dist/ui/index.js +6 -6
  164. package/dist/ui/line-editor.js +703 -0
  165. package/dist/ui/line-math.js +69 -0
  166. package/dist/ui/md-formatter.js +33 -33
  167. package/dist/ui/output.js +5 -5
  168. package/dist/ui/plan-view.js +103 -0
  169. package/dist/ui/renderer.js +15 -10
  170. package/dist/ui/table.js +1 -1
  171. package/package.json +48 -48
@@ -1,8 +1,35 @@
1
- import { chromium, } from "playwright";
1
+ import { createBrowserDriver } from "./driver";
2
2
  import { extractInteractiveElements, formatSnapshot } from "./snapshot";
3
- import { buildClickScript, buildTypeScript, buildScrollScript, buildIndexInjectionScript, } from "./actions";
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
33
  export class BrowserActionTracker {
7
34
  history = [];
8
35
  threshold;
@@ -30,9 +57,7 @@ export class BrowserActionTracker {
30
57
  }
31
58
  }
32
59
  export class BrowserSession {
33
- browser = null;
34
- context = null;
35
- page = null;
60
+ driver = null;
36
61
  config;
37
62
  cookieStore;
38
63
  state = {
@@ -90,7 +115,7 @@ export class BrowserSession {
90
115
  };
91
116
  }
92
117
  if (action !== "open") {
93
- const warning = this.actionTracker.record(action, args, this.page?.url() || "");
118
+ const warning = this.actionTracker.record(action, args, this.driver?.url() || "");
94
119
  if (warning && result.success) {
95
120
  result.output = result.output + "\n\n⚠️ " + warning;
96
121
  }
@@ -104,45 +129,41 @@ export class BrowserSession {
104
129
  };
105
130
  }
106
131
  }
107
- async ensurePage() {
108
- if (!this.page) {
132
+ async ensureDriver() {
133
+ if (!this.driver) {
109
134
  return { success: false, output: t("browser.no_page") };
110
135
  }
111
136
  return null;
112
137
  }
113
138
  async launch() {
114
- if (this.browser)
139
+ if (this.driver)
115
140
  return;
116
- this.browser = await chromium.launch({ headless: this.config.headless });
117
- this.context = await this.browser.newContext({
118
- viewport: {
119
- width: this.config.viewportWidth,
120
- height: this.config.viewportHeight,
121
- },
122
- });
141
+ this.driver = await createBrowserDriver(this.config);
123
142
  const savedCookies = await this.cookieStore.load();
124
143
  if (savedCookies.length > 0) {
125
- await this.context.addCookies(savedCookies.map((c) => ({
126
- ...c,
127
- sameSite: c.sameSite,
128
- })));
144
+ await this.driver.addCookies(savedCookies);
129
145
  }
130
- this.page = await this.context.newPage();
131
- this.page.setDefaultTimeout(this.config.navigationTimeout);
132
146
  }
133
147
  async saveCookies() {
134
- if (!this.context)
148
+ if (!this.driver)
135
149
  return;
136
- const cookies = await this.context.cookies();
150
+ const cookies = await this.driver.cookies();
137
151
  await this.cookieStore.save(cookies);
138
152
  }
139
153
  async takeSnapshot() {
140
- if (!this.page)
154
+ if (!this.driver)
141
155
  return "No page open.";
142
- await this.page.evaluate(buildIndexInjectionScript());
143
- const html = await this.page.content();
144
- const url = this.page.url();
145
- const title = await this.page.title();
156
+ await this.driver.evaluate(buildIndexInjectionScript());
157
+ const html = await this.driver.content();
158
+ const url = this.driver.url();
159
+ const title = await this.driver.title();
160
+ let content = "";
161
+ try {
162
+ content = String(await this.driver.evaluate(buildTextExtractionScript()));
163
+ }
164
+ catch {
165
+ content = "";
166
+ }
146
167
  const allElements = extractInteractiveElements(html, this.config.maxElements + 5);
147
168
  const truncated = allElements.length > this.config.maxElements;
148
169
  const elements = allElements.slice(0, this.config.maxElements);
@@ -152,26 +173,30 @@ export class BrowserSession {
152
173
  title,
153
174
  elementCount: elements.length,
154
175
  };
155
- return formatSnapshot({ url, title, elements, truncated });
176
+ return formatSnapshot({
177
+ url,
178
+ title,
179
+ elements,
180
+ content,
181
+ console: this.driver.getConsole(),
182
+ networkErrors: this.driver.getNetworkErrors(),
183
+ truncated,
184
+ });
156
185
  }
157
186
  async open(url) {
158
187
  if (!url)
159
188
  return { success: false, output: t("browser.url_required") };
160
- if (!url.startsWith("http://") &&
161
- !url.startsWith("https://") &&
162
- !url.startsWith("file://")) {
189
+ if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("file://")) {
163
190
  const isLocalhost = /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?$/i.test(url);
164
191
  url = isLocalhost ? "http://" + url : "https://" + url;
165
192
  }
166
193
  await this.launch();
167
- if (!this.page)
194
+ if (!this.driver)
168
195
  return { success: false, output: t("browser.create_page_failed") };
169
196
  this.actionTracker.reset();
197
+ this.driver.resetEventLog();
170
198
  try {
171
- await this.page.goto(url, {
172
- waitUntil: "domcontentloaded",
173
- timeout: this.config.navigationTimeout,
174
- });
199
+ await this.driver.goto(url, this.config.navigationTimeout);
175
200
  }
176
201
  catch (err) {
177
202
  return {
@@ -184,20 +209,17 @@ export class BrowserSession {
184
209
  return { success: true, output: snapshot };
185
210
  }
186
211
  async click(target) {
187
- const err = await this.ensurePage();
212
+ const err = await this.ensureDriver();
188
213
  if (err)
189
214
  return err;
190
- if (!this.page)
215
+ if (!this.driver)
191
216
  return { success: false, output: "No page" };
192
217
  if (!target || target < 1) {
193
218
  return { success: false, output: t("browser.invalid_target") };
194
219
  }
195
220
  try {
196
- const script = buildClickScript(target);
197
- await this.page.evaluate(script);
198
- await this.page
199
- .waitForLoadState("domcontentloaded", { timeout: 5000 })
200
- .catch(() => { });
221
+ await this.driver.evaluate(buildClickScript(target));
222
+ await this.driver.waitForLoad(5000);
201
223
  await new Promise((r) => setTimeout(r, 500));
202
224
  const snapshot = await this.takeSnapshot();
203
225
  await this.saveCookies();
@@ -211,10 +233,10 @@ export class BrowserSession {
211
233
  }
212
234
  }
213
235
  async type(target, text) {
214
- const err = await this.ensurePage();
236
+ const err = await this.ensureDriver();
215
237
  if (err)
216
238
  return err;
217
- if (!this.page)
239
+ if (!this.driver)
218
240
  return { success: false, output: "No page" };
219
241
  if (!target || target < 1) {
220
242
  return { success: false, output: t("browser.invalid_target_short") };
@@ -223,8 +245,7 @@ export class BrowserSession {
223
245
  return { success: false, output: t("browser.text_required") };
224
246
  }
225
247
  try {
226
- const script = buildTypeScript(target, text);
227
- await this.page.evaluate(script);
248
+ await this.driver.evaluate(buildTypeScript(target, text));
228
249
  const snapshot = await this.takeSnapshot();
229
250
  return { success: true, output: snapshot };
230
251
  }
@@ -236,17 +257,17 @@ export class BrowserSession {
236
257
  }
237
258
  }
238
259
  async scroll(direction) {
239
- const err = await this.ensurePage();
260
+ const err = await this.ensureDriver();
240
261
  if (err)
241
262
  return err;
242
- if (!this.page)
263
+ if (!this.driver)
243
264
  return { success: false, output: "No page" };
244
265
  const dir = direction;
245
266
  if (!["up", "down", "top", "bottom"].includes(dir)) {
246
267
  return { success: false, output: t("browser.direction_invalid") };
247
268
  }
248
269
  try {
249
- await this.page.evaluate(buildScrollScript(dir));
270
+ await this.driver.evaluate(buildScrollScript(dir));
250
271
  await new Promise((r) => setTimeout(r, 300));
251
272
  const snapshot = await this.takeSnapshot();
252
273
  return { success: true, output: snapshot };
@@ -259,56 +280,46 @@ export class BrowserSession {
259
280
  }
260
281
  }
261
282
  async back() {
262
- const err = await this.ensurePage();
283
+ const err = await this.ensureDriver();
263
284
  if (err)
264
285
  return err;
265
- if (!this.page)
286
+ if (!this.driver)
266
287
  return { success: false, output: "No page" };
267
- await this.page
268
- .goBack({
269
- waitUntil: "domcontentloaded",
270
- timeout: this.config.navigationTimeout,
271
- })
272
- .catch(() => { });
288
+ await this.driver.goBack(this.config.navigationTimeout);
273
289
  await new Promise((r) => setTimeout(r, 300));
274
290
  const snapshot = await this.takeSnapshot();
275
291
  return { success: true, output: snapshot };
276
292
  }
277
293
  async forward() {
278
- const err = await this.ensurePage();
294
+ const err = await this.ensureDriver();
279
295
  if (err)
280
296
  return err;
281
- if (!this.page)
297
+ if (!this.driver)
282
298
  return { success: false, output: "No page" };
283
- await this.page
284
- .goForward({
285
- waitUntil: "domcontentloaded",
286
- timeout: this.config.navigationTimeout,
287
- })
288
- .catch(() => { });
299
+ await this.driver.goForward(this.config.navigationTimeout);
289
300
  await new Promise((r) => setTimeout(r, 300));
290
301
  const snapshot = await this.takeSnapshot();
291
302
  return { success: true, output: snapshot };
292
303
  }
293
304
  async screenshot() {
294
- const err = await this.ensurePage();
305
+ const err = await this.ensureDriver();
295
306
  if (err)
296
307
  return err;
297
- if (!this.page)
308
+ if (!this.driver)
298
309
  return { success: false, output: "No page" };
299
- const buffer = await this.page.screenshot({ type: "png", fullPage: false });
310
+ const buffer = await this.driver.screenshot();
300
311
  const snapshot = await this.takeSnapshot();
301
312
  return { success: true, output: snapshot, screenshot: buffer };
302
313
  }
303
314
  async snapshot() {
304
- const err = await this.ensurePage();
315
+ const err = await this.ensureDriver();
305
316
  if (err)
306
317
  return err;
307
318
  const snap = await this.takeSnapshot();
308
319
  return { success: true, output: snap };
309
320
  }
310
321
  async wait(ms) {
311
- const err = await this.ensurePage();
322
+ const err = await this.ensureDriver();
312
323
  if (err)
313
324
  return err;
314
325
  await new Promise((r) => setTimeout(r, Math.min(ms, 10000)));
@@ -317,17 +328,9 @@ export class BrowserSession {
317
328
  }
318
329
  async close() {
319
330
  await this.saveCookies();
320
- if (this.page) {
321
- await this.page.close().catch(() => { });
322
- this.page = null;
323
- }
324
- if (this.context) {
325
- await this.context.close().catch(() => { });
326
- this.context = null;
327
- }
328
- if (this.browser) {
329
- await this.browser.close().catch(() => { });
330
- this.browser = null;
331
+ if (this.driver) {
332
+ await this.driver.close();
333
+ this.driver = null;
331
334
  }
332
335
  this.state = { isOpen: false, url: null, title: null, elementCount: 0 };
333
336
  return { success: true, output: t("browser.closed") };
@@ -1,46 +1,50 @@
1
- import { t } from '../../i18n/index';
1
+ import { t } from "../../i18n/index";
2
+ import { DEFAULT_BROWSER_CONFIG } from "./types";
2
3
  function inferRole(tag, type) {
3
4
  const t = tag.toLowerCase();
4
- if (t === 'a')
5
- return 'link';
6
- if (t === 'button')
7
- return 'button';
8
- if (t === 'input') {
5
+ if (t === "a")
6
+ return "link";
7
+ if (t === "button")
8
+ return "button";
9
+ if (t === "input") {
9
10
  const inputType = type?.toLowerCase();
10
- if (inputType === 'submit' || inputType === 'button')
11
- return 'button';
12
- if (inputType === 'checkbox')
13
- return 'checkbox';
14
- if (inputType === 'radio')
15
- return 'radio';
16
- return 'textbox';
11
+ if (inputType === "submit" || inputType === "button")
12
+ return "button";
13
+ if (inputType === "checkbox")
14
+ return "checkbox";
15
+ if (inputType === "radio")
16
+ return "radio";
17
+ return "textbox";
17
18
  }
18
- if (t === 'textarea')
19
- return 'textbox';
20
- if (t === 'select')
21
- return 'combobox';
22
- return 'widget';
19
+ if (t === "textarea")
20
+ return "textbox";
21
+ if (t === "select")
22
+ return "combobox";
23
+ return "widget";
23
24
  }
24
25
  function getName(tag, attrs, html, matchIndex) {
25
- const fromAttr = (attrs['aria-label'] ||
26
- attrs['placeholder'] ||
27
- attrs['title'] ||
28
- attrs['alt'] ||
29
- attrs['value'] ||
30
- attrs['name'] ||
31
- '');
26
+ const fromAttr = attrs["aria-label"] ||
27
+ attrs["placeholder"] ||
28
+ attrs["title"] ||
29
+ attrs["alt"] ||
30
+ attrs["value"] ||
31
+ attrs["name"] ||
32
+ "";
32
33
  if (fromAttr)
33
34
  return fromAttr;
34
35
  const afterTag = html.slice(matchIndex);
35
- const closeIdx = afterTag.indexOf('>');
36
+ const closeIdx = afterTag.indexOf(">");
36
37
  if (closeIdx === -1)
37
- return '';
38
+ return "";
38
39
  const afterOpen = afterTag.slice(closeIdx + 1);
39
40
  const endTag = `</${tag}>`;
40
41
  const endIdx = afterOpen.indexOf(endTag);
41
42
  if (endIdx === -1)
42
- return '';
43
- const text = afterOpen.slice(0, endIdx).replace(/<[^>]+>/g, '').trim();
43
+ return "";
44
+ const text = afterOpen
45
+ .slice(0, endIdx)
46
+ .replace(/<[^>]+>/g, "")
47
+ .trim();
44
48
  return text.slice(0, 50);
45
49
  }
46
50
  export function extractInteractiveElements(html, maxElements = 30) {
@@ -56,23 +60,21 @@ export function extractInteractiveElements(html, maxElements = 30) {
56
60
  const attrRegex = /(\w[\w-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
57
61
  let attrMatch;
58
62
  while ((attrMatch = attrRegex.exec(attrStr)) !== null) {
59
- attrs[attrMatch[1].toLowerCase()] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? '';
63
+ attrs[attrMatch[1].toLowerCase()] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
60
64
  }
61
- if (attrs['style']?.includes('display:none') || attrs['style']?.includes('display: none'))
65
+ if (attrs["style"]?.includes("display:none") || attrs["style"]?.includes("display: none"))
62
66
  continue;
63
- if (attrs['hidden'] !== undefined)
67
+ if (attrs["hidden"] !== undefined)
64
68
  continue;
65
- if (attrs['aria-hidden'] === 'true')
69
+ if (attrs["aria-hidden"] === "true")
66
70
  continue;
67
- if (tag === 'input' && attrs['type'] === 'hidden')
71
+ if (tag === "input" && attrs["type"] === "hidden")
68
72
  continue;
69
- const role = attrs['role'] || inferRole(tag, attrs['type']);
70
- const isInteractive = [
71
- 'a', 'button', 'input', 'textarea', 'select',
72
- ].includes(tag) || attrs['role'] !== undefined;
73
+ const role = attrs["role"] || inferRole(tag, attrs["type"]);
74
+ const isInteractive = ["a", "button", "input", "textarea", "select"].includes(tag) || attrs["role"] !== undefined;
73
75
  if (!isInteractive)
74
76
  continue;
75
- if (tag === 'a' && !attrs['href'])
77
+ if (tag === "a" && !attrs["href"])
76
78
  continue;
77
79
  const name = getName(tag, attrs, html, match.index);
78
80
  elements.push({
@@ -80,35 +82,67 @@ export function extractInteractiveElements(html, maxElements = 30) {
80
82
  tag,
81
83
  role,
82
84
  name,
83
- value: attrs['value'],
84
- href: attrs['href'],
85
+ value: attrs["value"],
86
+ href: attrs["href"],
85
87
  visible: true,
86
88
  });
87
89
  }
88
90
  return elements;
89
91
  }
90
- export function formatSnapshot(snapshot) {
92
+ export function truncateText(text, maxChars) {
93
+ if (maxChars <= 0)
94
+ return "";
95
+ if (text.length <= maxChars)
96
+ return text;
97
+ const cut = text.slice(0, maxChars);
98
+ const lastNewline = cut.lastIndexOf("\n");
99
+ const safe = lastNewline > maxChars * 0.6 ? cut.slice(0, lastNewline) : cut;
100
+ return `${safe}\n${t("browser.truncated")}`;
101
+ }
102
+ export function formatSnapshot(snapshot, maxContentChars = DEFAULT_BROWSER_CONFIG.maxContentChars) {
91
103
  const lines = [];
92
104
  lines.push(`Page: "${snapshot.title}"`);
93
105
  lines.push(`URL: ${snapshot.url}`);
94
- lines.push('');
106
+ lines.push("");
95
107
  if (snapshot.elements.length === 0 && !snapshot.truncated) {
96
- lines.push(t('browser.no_elements'));
97
- return lines.join('\n');
108
+ lines.push(t("browser.no_elements"));
109
+ }
110
+ else {
111
+ for (const el of snapshot.elements) {
112
+ let line = `[${el.index}] ${el.role}`;
113
+ if (el.name)
114
+ line += ` "${el.name}"`;
115
+ if (el.href)
116
+ line += ` → ${el.href}`;
117
+ if (el.value)
118
+ line += ` = "${el.value}"`;
119
+ lines.push(line);
120
+ }
121
+ if (snapshot.truncated) {
122
+ lines.push("");
123
+ lines.push(t("browser.more_elements"));
124
+ }
98
125
  }
99
- for (const el of snapshot.elements) {
100
- let line = `[${el.index}] ${el.role}`;
101
- if (el.name)
102
- line += ` "${el.name}"`;
103
- if (el.href)
104
- line += ` → ${el.href}`;
105
- if (el.value)
106
- line += ` = "${el.value}"`;
107
- lines.push(line);
126
+ if (snapshot.content) {
127
+ lines.push("");
128
+ lines.push(t("browser.content_header"));
129
+ for (const line of truncateText(snapshot.content, maxContentChars).split("\n")) {
130
+ lines.push(`- ${line}`);
131
+ }
132
+ }
133
+ if (snapshot.console.length > 0) {
134
+ lines.push("");
135
+ lines.push(t("browser.console_header"));
136
+ for (const entry of snapshot.console) {
137
+ lines.push(`[${entry.type}] ${entry.text}`);
138
+ }
108
139
  }
109
- if (snapshot.truncated) {
110
- lines.push('');
111
- lines.push(t('browser.more_elements'));
140
+ if (snapshot.networkErrors.length > 0) {
141
+ lines.push("");
142
+ lines.push(t("browser.network_errors_header"));
143
+ for (const err of snapshot.networkErrors) {
144
+ lines.push(`${err.method} ${err.url} → ${err.error}`);
145
+ }
112
146
  }
113
- return lines.join('\n');
147
+ return lines.join("\n");
114
148
  }
@@ -1,8 +1,11 @@
1
1
  export const DEFAULT_BROWSER_CONFIG = {
2
2
  headless: true,
3
3
  maxElements: 30,
4
+ maxContentChars: 2500,
5
+ maxConsoleEntries: 40,
6
+ maxConsoleLineChars: 400,
4
7
  screenshotMaxWidth: 1280,
5
- cookieDir: '',
8
+ cookieDir: "",
6
9
  viewportWidth: 1280,
7
10
  viewportHeight: 720,
8
11
  navigationTimeout: 15000,
@@ -7,7 +7,7 @@ import { t } from "../../i18n/index";
7
7
  import { pc } from "../../ui/colors";
8
8
  import { loadScenarios, filterByTags } from "./loader";
9
9
  import { runScenario, findMmaRoot } from "./runner";
10
- import { readManifest, upsertCertification, removeCertification, } from "./manifest";
10
+ import { readManifest, upsertCertification, removeCertification } from "./manifest";
11
11
  const HERE = dirname(fileURLToPath(import.meta.url));
12
12
  const MMA_ROOT = findMmaRoot(HERE);
13
13
  const USER_SCENARIO_DIR = join(homedir(), ".mma", "certification", "scenarios");
@@ -85,9 +85,7 @@ export async function certify(opts) {
85
85
  defaultReps: opts.reps,
86
86
  defaultThreshold: 2,
87
87
  onRep: (id, rep, reps, passed, failures) => {
88
- const word = passed
89
- ? pc.green(t("cli.cert_rep_pass"))
90
- : pc.red(t("cli.cert_rep_fail"));
88
+ const word = passed ? pc.green(t("cli.cert_rep_pass")) : pc.red(t("cli.cert_rep_fail"));
91
89
  console.log(`[${idx}/${total}] ${id} (${rep}/${reps})... ${word}`);
92
90
  if (!passed)
93
91
  console.log(` ${pc.dim(failures.join("; "))}`);
@@ -71,9 +71,7 @@ function describe(check) {
71
71
  case "dirExists":
72
72
  return `directory missing: ${check.path}`;
73
73
  case "fileContent": {
74
- const what = check.contains !== undefined
75
- ? `contains "${check.contains}"`
76
- : `equals "${check.equals}"`;
74
+ const what = check.contains !== undefined ? `contains "${check.contains}"` : `equals "${check.equals}"`;
77
75
  return `${check.path} does not ${what}`;
78
76
  }
79
77
  case "fileRegex":
@@ -35,9 +35,7 @@ export function validateScenario(s) {
35
35
  if (!CHECK_TYPES.includes(c.type)) {
36
36
  errors.push(`[${s.id}] unknown check type: ${c.type}`);
37
37
  }
38
- if (c.type === "fileContent" &&
39
- c.contains === undefined &&
40
- c.equals === undefined) {
38
+ if (c.type === "fileContent" && c.contains === undefined && c.equals === undefined) {
41
39
  errors.push(`[${s.id}] fileContent needs contains or equals`);
42
40
  }
43
41
  }
@@ -48,9 +46,7 @@ export function validateScenario(s) {
48
46
  (!Number.isInteger(s.passThreshold) || s.passThreshold < 1)) {
49
47
  errors.push(`[${s.id}] passThreshold must be a positive integer`);
50
48
  }
51
- if (s.passThreshold !== undefined &&
52
- s.reps !== undefined &&
53
- s.passThreshold > s.reps) {
49
+ if (s.passThreshold !== undefined && s.reps !== undefined && s.passThreshold > s.reps) {
54
50
  errors.push(`[${s.id}] passThreshold > reps (must be <= reps)`);
55
51
  }
56
52
  return errors;
@@ -96,9 +92,7 @@ function normalizeScenario(data, file) {
96
92
  prompt: String(d.prompt ?? ""),
97
93
  reps: typeof d.reps === "number" ? d.reps : undefined,
98
94
  passThreshold: typeof d.passThreshold === "number" ? d.passThreshold : undefined,
99
- fixtures: Array.isArray(d.fixtures)
100
- ? d.fixtures
101
- : undefined,
95
+ fixtures: Array.isArray(d.fixtures) ? d.fixtures : undefined,
102
96
  checks: Array.isArray(d.checks) ? d.checks : [],
103
97
  skipReason: typeof d.skipReason === "string" ? d.skipReason : undefined,
104
98
  };
@@ -98,10 +98,7 @@ export function resolveMmaEntry(mmaRoot) {
98
98
  return join(mmaRoot, "dist", "main.js");
99
99
  }
100
100
  export function findMmaRoot(fromDir) {
101
- const candidates = [
102
- resolve(fromDir, "..", "..", ".."),
103
- resolve(fromDir, ".."),
104
- ];
101
+ const candidates = [resolve(fromDir, "..", "..", ".."), resolve(fromDir, "..")];
105
102
  for (const c of candidates) {
106
103
  if (existsSync(join(c, "package.json")))
107
104
  return c;