micro-models-agent 0.47.0 → 0.48.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 (218) hide show
  1. package/README.md +358 -312
  2. package/dist/cli/commands.js +323 -0
  3. package/dist/cli/completer.js +167 -0
  4. package/dist/cli/index.js +2 -0
  5. package/dist/cli/main.js +165 -0
  6. package/dist/cli/plugin-commands.js +36 -0
  7. package/dist/cli/repl-commands.js +661 -0
  8. package/dist/cli/repl.js +616 -0
  9. package/dist/cli/run-result.js +22 -0
  10. package/dist/cli/security-commands.js +164 -0
  11. package/dist/cli/setup.js +231 -0
  12. package/dist/config/config.js +249 -0
  13. package/dist/config/defaults.js +124 -0
  14. package/dist/config/experts.js +15 -0
  15. package/dist/config/index.js +3 -0
  16. package/dist/config/security.js +193 -0
  17. package/dist/config/types.js +1 -0
  18. package/dist/core/agent-moe.js +102 -0
  19. package/dist/core/agent.js +886 -0
  20. package/dist/core/bootstrap.js +404 -0
  21. package/dist/core/index.js +2 -0
  22. package/dist/core/prompt-builder.js +76 -0
  23. package/dist/core/session-logger.js +197 -0
  24. package/dist/core/types.js +1 -0
  25. package/dist/core/version.js +24 -0
  26. package/dist/core/workspace.js +76 -0
  27. package/dist/i18n/en.json +598 -0
  28. package/dist/i18n/index.js +46 -0
  29. package/dist/i18n/ru.json +598 -0
  30. package/dist/index.js +22 -0
  31. package/dist/llm/image-utils.js +143 -0
  32. package/dist/llm/index.js +4 -0
  33. package/dist/llm/model-loader.js +78 -0
  34. package/dist/llm/openai-compat.js +359 -0
  35. package/dist/llm/orchestrator.js +198 -0
  36. package/dist/llm/provider.js +10 -0
  37. package/dist/llm/response.js +39 -0
  38. package/dist/llm/token-counter.js +39 -0
  39. package/dist/llm/types.js +1 -0
  40. package/dist/logger/app-logger.js +143 -0
  41. package/dist/logger/file-log.js +151 -0
  42. package/dist/logger/index.js +1 -0
  43. package/dist/main.js +672 -357
  44. package/dist/migration/backup.js +45 -0
  45. package/dist/migration/detect.js +50 -0
  46. package/dist/migration/index.js +2 -0
  47. package/dist/modules/artifacts/store.js +61 -0
  48. package/dist/modules/browser/actions.js +76 -0
  49. package/dist/modules/browser/bridge-client.js +199 -0
  50. package/dist/modules/browser/bridge-path.js +10 -0
  51. package/dist/modules/browser/bridge-server.mjs +202 -202
  52. package/dist/modules/browser/cookie-store.js +24 -0
  53. package/dist/modules/browser/driver.js +136 -0
  54. package/dist/modules/browser/index.js +7 -0
  55. package/dist/modules/browser/module.js +29 -0
  56. package/dist/modules/browser/session.js +338 -0
  57. package/dist/modules/browser/snapshot.js +148 -0
  58. package/dist/modules/browser/types.js +12 -0
  59. package/dist/modules/certification/cli.js +174 -0
  60. package/dist/modules/certification/fact-checker.js +82 -0
  61. package/dist/modules/certification/loader.js +105 -0
  62. package/dist/modules/certification/manifest.js +50 -0
  63. package/dist/modules/certification/runner.js +159 -0
  64. package/dist/modules/certification/scenarios.js +124 -0
  65. package/dist/modules/certification/types.js +1 -0
  66. package/dist/modules/context/chunk-query.js +100 -0
  67. package/dist/modules/context/fact-extractor.js +162 -0
  68. package/dist/modules/context/history.js +15 -0
  69. package/dist/modules/context/index.js +1 -0
  70. package/dist/modules/context/manager.js +423 -0
  71. package/dist/modules/execution/audit-runners.js +152 -0
  72. package/dist/modules/execution/auditor.js +218 -0
  73. package/dist/modules/execution/execution-plugin.js +272 -0
  74. package/dist/modules/execution/index.js +8 -0
  75. package/dist/modules/execution/module.js +436 -0
  76. package/dist/modules/execution/moe-executor.js +291 -0
  77. package/dist/modules/execution/plan-coverage.js +68 -0
  78. package/dist/modules/execution/plan-persister.js +46 -0
  79. package/dist/modules/execution/plan-store.js +157 -0
  80. package/dist/modules/execution/plan-tool.js +508 -0
  81. package/dist/modules/execution/plan-validator.js +153 -0
  82. package/dist/modules/execution/planner.js +90 -0
  83. package/dist/modules/execution/stuck-detector.js +510 -0
  84. package/dist/modules/execution/tracker.js +67 -0
  85. package/dist/modules/execution/types.js +1 -0
  86. package/dist/modules/execution/verifier.js +222 -0
  87. package/dist/modules/execution/windows-commands.js +41 -0
  88. package/dist/modules/hallucination/confidence.js +66 -0
  89. package/dist/modules/hallucination/consistency.js +26 -0
  90. package/dist/modules/hallucination/detector.js +43 -0
  91. package/dist/modules/hallucination/factual.js +129 -0
  92. package/dist/modules/hallucination/index.js +5 -0
  93. package/dist/modules/hallucination/js-identifiers.js +262 -0
  94. package/dist/modules/hallucination/llm-judge.js +101 -0
  95. package/dist/modules/index.js +5 -0
  96. package/dist/modules/indexer/cache.js +40 -0
  97. package/dist/modules/indexer/index.js +3 -0
  98. package/dist/modules/indexer/module.js +245 -0
  99. package/dist/modules/indexer/project-profile.js +183 -0
  100. package/dist/modules/indexer/walker.js +101 -0
  101. package/dist/modules/lsp/check-tool.js +58 -0
  102. package/dist/modules/lsp/client.js +278 -0
  103. package/dist/modules/lsp/command.js +60 -0
  104. package/dist/modules/lsp/config.js +135 -0
  105. package/dist/modules/lsp/index.js +3 -0
  106. package/dist/modules/lsp/module.js +232 -0
  107. package/dist/modules/lsp/probe.js +76 -0
  108. package/dist/modules/lsp/project-root.js +32 -0
  109. package/dist/modules/lsp/startup-check.js +141 -0
  110. package/dist/modules/lsp/types.js +1 -0
  111. package/dist/modules/mcp/client.js +399 -0
  112. package/dist/modules/mcp/index.js +3 -0
  113. package/dist/modules/mcp/module.js +142 -0
  114. package/dist/modules/mcp/registry.js +15 -0
  115. package/dist/modules/memory/index.js +1 -0
  116. package/dist/modules/memory/module.js +96 -0
  117. package/dist/modules/memory/search.js +42 -0
  118. package/dist/modules/memory/store.js +69 -0
  119. package/dist/modules/pipelines/engine.js +60 -0
  120. package/dist/modules/pipelines/index.js +3 -0
  121. package/dist/modules/pipelines/parser.js +56 -0
  122. package/dist/modules/pipelines/template.js +14 -0
  123. package/dist/modules/plugins/builtin/lint-on-write.js +231 -0
  124. package/dist/modules/plugins/builtin/notify.js +9 -0
  125. package/dist/modules/plugins/index.js +1 -0
  126. package/dist/modules/plugins/loader.js +70 -0
  127. package/dist/modules/plugins/manager.js +217 -0
  128. package/dist/modules/plugins/types.js +1 -0
  129. package/dist/modules/processes/detect.js +34 -0
  130. package/dist/modules/processes/index.js +2 -0
  131. package/dist/modules/processes/registry.js +327 -0
  132. package/dist/modules/processes/runner.js +23 -0
  133. package/dist/modules/registry.js +47 -0
  134. package/dist/modules/security/audit-log.js +136 -0
  135. package/dist/modules/security/audit-notifier.js +292 -0
  136. package/dist/modules/security/command-validator.js +205 -0
  137. package/dist/modules/security/content-scanner.js +53 -0
  138. package/dist/modules/security/data-sanitizer.js +89 -0
  139. package/dist/modules/security/encryption.js +242 -0
  140. package/dist/modules/security/index.js +14 -0
  141. package/dist/modules/security/network-validator.js +71 -0
  142. package/dist/modules/security/path-validator.js +207 -0
  143. package/dist/modules/security/rate-limiter.js +119 -0
  144. package/dist/modules/security/security-policies.js +531 -0
  145. package/dist/modules/security/session-encryption.js +210 -0
  146. package/dist/modules/security/session-isolation.js +95 -0
  147. package/dist/modules/session/index.js +3 -0
  148. package/dist/modules/session/manager.js +172 -0
  149. package/dist/modules/session/module.js +24 -0
  150. package/dist/modules/session/store.js +222 -0
  151. package/dist/modules/session/types.js +1 -0
  152. package/dist/modules/skills/index.js +2 -0
  153. package/dist/modules/skills/loader.js +72 -0
  154. package/dist/modules/skills/matcher.js +27 -0
  155. package/dist/modules/skills/module.js +129 -0
  156. package/dist/modules/types.js +1 -0
  157. package/dist/modules/updater/checker.js +96 -0
  158. package/dist/modules/updater/index.js +2 -0
  159. package/dist/modules/updater/module.js +116 -0
  160. package/dist/modules/user-profile/compressor.js +16 -0
  161. package/dist/modules/user-profile/index.js +1 -0
  162. package/dist/modules/user-profile/profile.js +68 -0
  163. package/dist/skills/builtin/git.md +36 -36
  164. package/dist/skills/builtin/typescript.md +35 -35
  165. package/dist/tools/approve.js +32 -0
  166. package/dist/tools/attach-image.js +89 -0
  167. package/dist/tools/bash.js +496 -0
  168. package/dist/tools/browser.js +114 -0
  169. package/dist/tools/chunk-query.js +99 -0
  170. package/dist/tools/create-dir.js +55 -0
  171. package/dist/tools/delete-file.js +62 -0
  172. package/dist/tools/download-file.js +116 -0
  173. package/dist/tools/edit-file.js +79 -0
  174. package/dist/tools/enable-tools.js +58 -0
  175. package/dist/tools/executor.js +144 -0
  176. package/dist/tools/file-info.js +46 -0
  177. package/dist/tools/filter-tools.js +17 -0
  178. package/dist/tools/glob-tool.js +26 -0
  179. package/dist/tools/grep-tool.js +84 -0
  180. package/dist/tools/hidden-tools-block.js +37 -0
  181. package/dist/tools/index.js +78 -0
  182. package/dist/tools/list-dir.js +48 -0
  183. package/dist/tools/load-skill.js +42 -0
  184. package/dist/tools/mcp-call.js +68 -0
  185. package/dist/tools/move-file.js +85 -0
  186. package/dist/tools/path-utils.js +51 -0
  187. package/dist/tools/pipeline-run.js +144 -0
  188. package/dist/tools/preview.js +2 -0
  189. package/dist/tools/process-kill.js +29 -0
  190. package/dist/tools/process-list.js +36 -0
  191. package/dist/tools/process-log.js +45 -0
  192. package/dist/tools/question.js +140 -0
  193. package/dist/tools/read-file.js +91 -0
  194. package/dist/tools/recall.js +117 -0
  195. package/dist/tools/registry.js +47 -0
  196. package/dist/tools/remember.js +67 -0
  197. package/dist/tools/scope-check.js +30 -0
  198. package/dist/tools/search-history.js +84 -0
  199. package/dist/tools/subagent.js +196 -0
  200. package/dist/tools/types.js +1 -0
  201. package/dist/tools/user-input.js +123 -0
  202. package/dist/tools/web-browse.js +86 -0
  203. package/dist/tools/web-fetch.js +98 -0
  204. package/dist/tools/web-search.js +78 -0
  205. package/dist/tools/write-file.js +81 -0
  206. package/dist/ui/box.js +77 -0
  207. package/dist/ui/colors.js +4 -0
  208. package/dist/ui/diff.js +178 -0
  209. package/dist/ui/index.js +6 -0
  210. package/dist/ui/line-editor.js +703 -0
  211. package/dist/ui/line-math.js +69 -0
  212. package/dist/ui/md-formatter.js +212 -0
  213. package/dist/ui/output.js +13 -0
  214. package/dist/ui/plan-view.js +103 -0
  215. package/dist/ui/renderer.js +209 -0
  216. package/dist/ui/spinner.js +70 -0
  217. package/dist/ui/table.js +144 -0
  218. package/package.json +48 -48
@@ -0,0 +1,338 @@
1
+ import { createBrowserDriver } from "./driver";
2
+ import { extractInteractiveElements, formatSnapshot } from "./snapshot";
3
+ import { buildClickScript, buildTypeScript, buildScrollScript, buildIndexInjectionScript, buildTextExtractionScript, } from "./actions";
4
+ import { CookieStore } from "./cookie-store";
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
+ }
33
+ export class BrowserActionTracker {
34
+ history = [];
35
+ threshold;
36
+ constructor(threshold = 3) {
37
+ this.threshold = threshold;
38
+ }
39
+ record(action, args, url) {
40
+ this.history.push({ action, args, url });
41
+ if (this.history.length < this.threshold)
42
+ return null;
43
+ const recent = this.history.slice(-this.threshold);
44
+ const allSame = recent.every((h) => h.action === recent[0].action &&
45
+ h.url === recent[0].url &&
46
+ JSON.stringify(h.args) === JSON.stringify(recent[0].args));
47
+ if (allSame) {
48
+ return t("browser.repeated_action", {
49
+ action,
50
+ threshold: this.threshold,
51
+ });
52
+ }
53
+ return null;
54
+ }
55
+ reset() {
56
+ this.history = [];
57
+ }
58
+ }
59
+ export class BrowserSession {
60
+ driver = null;
61
+ config;
62
+ cookieStore;
63
+ state = {
64
+ isOpen: false,
65
+ url: null,
66
+ title: null,
67
+ elementCount: 0,
68
+ };
69
+ actionTracker = new BrowserActionTracker(3);
70
+ constructor(config) {
71
+ this.config = config;
72
+ this.cookieStore = new CookieStore(config.cookieDir);
73
+ }
74
+ getState() {
75
+ return { ...this.state };
76
+ }
77
+ async execute(action, args) {
78
+ try {
79
+ let result;
80
+ switch (action) {
81
+ case "open":
82
+ result = await this.open(String(args.url || ""));
83
+ break;
84
+ case "click":
85
+ result = await this.click(Number(args.target));
86
+ break;
87
+ case "type":
88
+ result = await this.type(Number(args.target), String(args.text || ""));
89
+ break;
90
+ case "scroll":
91
+ result = await this.scroll(String(args.direction || "down"));
92
+ break;
93
+ case "back":
94
+ result = await this.back();
95
+ break;
96
+ case "forward":
97
+ result = await this.forward();
98
+ break;
99
+ case "screenshot":
100
+ result = await this.screenshot();
101
+ break;
102
+ case "snapshot":
103
+ result = await this.snapshot();
104
+ break;
105
+ case "close":
106
+ result = await this.close();
107
+ break;
108
+ case "wait":
109
+ result = await this.wait(Number(args.ms || 1000));
110
+ break;
111
+ default:
112
+ return {
113
+ success: false,
114
+ output: t("browser.unknown_action", { action }),
115
+ };
116
+ }
117
+ if (action !== "open") {
118
+ const warning = this.actionTracker.record(action, args, this.driver?.url() || "");
119
+ if (warning && result.success) {
120
+ result.output = result.output + "\n\n⚠️ " + warning;
121
+ }
122
+ }
123
+ return result;
124
+ }
125
+ catch (err) {
126
+ return {
127
+ success: false,
128
+ output: t("browser.error", { message: err.message }),
129
+ };
130
+ }
131
+ }
132
+ async ensureDriver() {
133
+ if (!this.driver) {
134
+ return { success: false, output: t("browser.no_page") };
135
+ }
136
+ return null;
137
+ }
138
+ async launch() {
139
+ if (this.driver)
140
+ return;
141
+ this.driver = await createBrowserDriver(this.config);
142
+ const savedCookies = await this.cookieStore.load();
143
+ if (savedCookies.length > 0) {
144
+ await this.driver.addCookies(savedCookies);
145
+ }
146
+ }
147
+ async saveCookies() {
148
+ if (!this.driver)
149
+ return;
150
+ const cookies = await this.driver.cookies();
151
+ await this.cookieStore.save(cookies);
152
+ }
153
+ async takeSnapshot() {
154
+ if (!this.driver)
155
+ return "No page open.";
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
+ }
167
+ const allElements = extractInteractiveElements(html, this.config.maxElements + 5);
168
+ const truncated = allElements.length > this.config.maxElements;
169
+ const elements = allElements.slice(0, this.config.maxElements);
170
+ this.state = {
171
+ isOpen: true,
172
+ url,
173
+ title,
174
+ elementCount: elements.length,
175
+ };
176
+ return formatSnapshot({
177
+ url,
178
+ title,
179
+ elements,
180
+ content,
181
+ console: this.driver.getConsole(),
182
+ networkErrors: this.driver.getNetworkErrors(),
183
+ truncated,
184
+ });
185
+ }
186
+ async open(url) {
187
+ if (!url)
188
+ return { success: false, output: t("browser.url_required") };
189
+ if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("file://")) {
190
+ const isLocalhost = /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?$/i.test(url);
191
+ url = isLocalhost ? "http://" + url : "https://" + url;
192
+ }
193
+ await this.launch();
194
+ if (!this.driver)
195
+ return { success: false, output: t("browser.create_page_failed") };
196
+ this.actionTracker.reset();
197
+ this.driver.resetEventLog();
198
+ try {
199
+ await this.driver.goto(url, this.config.navigationTimeout);
200
+ }
201
+ catch (err) {
202
+ return {
203
+ success: false,
204
+ output: t("browser.nav_failed", { message: err.message }),
205
+ };
206
+ }
207
+ const snapshot = await this.takeSnapshot();
208
+ await this.saveCookies();
209
+ return { success: true, output: snapshot };
210
+ }
211
+ async click(target) {
212
+ const err = await this.ensureDriver();
213
+ if (err)
214
+ return err;
215
+ if (!this.driver)
216
+ return { success: false, output: "No page" };
217
+ if (!target || target < 1) {
218
+ return { success: false, output: t("browser.invalid_target") };
219
+ }
220
+ try {
221
+ await this.driver.evaluate(buildClickScript(target));
222
+ await this.driver.waitForLoad(5000);
223
+ await new Promise((r) => setTimeout(r, 500));
224
+ const snapshot = await this.takeSnapshot();
225
+ await this.saveCookies();
226
+ return { success: true, output: snapshot };
227
+ }
228
+ catch (err) {
229
+ return {
230
+ success: false,
231
+ output: t("browser.click_failed", { message: err.message }),
232
+ };
233
+ }
234
+ }
235
+ async type(target, text) {
236
+ const err = await this.ensureDriver();
237
+ if (err)
238
+ return err;
239
+ if (!this.driver)
240
+ return { success: false, output: "No page" };
241
+ if (!target || target < 1) {
242
+ return { success: false, output: t("browser.invalid_target_short") };
243
+ }
244
+ if (!text) {
245
+ return { success: false, output: t("browser.text_required") };
246
+ }
247
+ try {
248
+ await this.driver.evaluate(buildTypeScript(target, text));
249
+ const snapshot = await this.takeSnapshot();
250
+ return { success: true, output: snapshot };
251
+ }
252
+ catch (err) {
253
+ return {
254
+ success: false,
255
+ output: t("browser.type_failed", { message: err.message }),
256
+ };
257
+ }
258
+ }
259
+ async scroll(direction) {
260
+ const err = await this.ensureDriver();
261
+ if (err)
262
+ return err;
263
+ if (!this.driver)
264
+ return { success: false, output: "No page" };
265
+ const dir = direction;
266
+ if (!["up", "down", "top", "bottom"].includes(dir)) {
267
+ return { success: false, output: t("browser.direction_invalid") };
268
+ }
269
+ try {
270
+ await this.driver.evaluate(buildScrollScript(dir));
271
+ await new Promise((r) => setTimeout(r, 300));
272
+ const snapshot = await this.takeSnapshot();
273
+ return { success: true, output: snapshot };
274
+ }
275
+ catch (err) {
276
+ return {
277
+ success: false,
278
+ output: t("browser.scroll_failed", { message: err.message }),
279
+ };
280
+ }
281
+ }
282
+ async back() {
283
+ const err = await this.ensureDriver();
284
+ if (err)
285
+ return err;
286
+ if (!this.driver)
287
+ return { success: false, output: "No page" };
288
+ await this.driver.goBack(this.config.navigationTimeout);
289
+ await new Promise((r) => setTimeout(r, 300));
290
+ const snapshot = await this.takeSnapshot();
291
+ return { success: true, output: snapshot };
292
+ }
293
+ async forward() {
294
+ const err = await this.ensureDriver();
295
+ if (err)
296
+ return err;
297
+ if (!this.driver)
298
+ return { success: false, output: "No page" };
299
+ await this.driver.goForward(this.config.navigationTimeout);
300
+ await new Promise((r) => setTimeout(r, 300));
301
+ const snapshot = await this.takeSnapshot();
302
+ return { success: true, output: snapshot };
303
+ }
304
+ async screenshot() {
305
+ const err = await this.ensureDriver();
306
+ if (err)
307
+ return err;
308
+ if (!this.driver)
309
+ return { success: false, output: "No page" };
310
+ const buffer = await this.driver.screenshot();
311
+ const snapshot = await this.takeSnapshot();
312
+ return { success: true, output: snapshot, screenshot: buffer };
313
+ }
314
+ async snapshot() {
315
+ const err = await this.ensureDriver();
316
+ if (err)
317
+ return err;
318
+ const snap = await this.takeSnapshot();
319
+ return { success: true, output: snap };
320
+ }
321
+ async wait(ms) {
322
+ const err = await this.ensureDriver();
323
+ if (err)
324
+ return err;
325
+ await new Promise((r) => setTimeout(r, Math.min(ms, 10000)));
326
+ const snapshot = await this.takeSnapshot();
327
+ return { success: true, output: snapshot };
328
+ }
329
+ async close() {
330
+ await this.saveCookies();
331
+ if (this.driver) {
332
+ await this.driver.close();
333
+ this.driver = null;
334
+ }
335
+ this.state = { isOpen: false, url: null, title: null, elementCount: 0 };
336
+ return { success: true, output: t("browser.closed") };
337
+ }
338
+ }
@@ -0,0 +1,148 @@
1
+ import { t } from "../../i18n/index";
2
+ import { DEFAULT_BROWSER_CONFIG } from "./types";
3
+ function inferRole(tag, type) {
4
+ const t = tag.toLowerCase();
5
+ if (t === "a")
6
+ return "link";
7
+ if (t === "button")
8
+ return "button";
9
+ if (t === "input") {
10
+ const inputType = type?.toLowerCase();
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";
18
+ }
19
+ if (t === "textarea")
20
+ return "textbox";
21
+ if (t === "select")
22
+ return "combobox";
23
+ return "widget";
24
+ }
25
+ function getName(tag, attrs, html, matchIndex) {
26
+ const fromAttr = attrs["aria-label"] ||
27
+ attrs["placeholder"] ||
28
+ attrs["title"] ||
29
+ attrs["alt"] ||
30
+ attrs["value"] ||
31
+ attrs["name"] ||
32
+ "";
33
+ if (fromAttr)
34
+ return fromAttr;
35
+ const afterTag = html.slice(matchIndex);
36
+ const closeIdx = afterTag.indexOf(">");
37
+ if (closeIdx === -1)
38
+ return "";
39
+ const afterOpen = afterTag.slice(closeIdx + 1);
40
+ const endTag = `</${tag}>`;
41
+ const endIdx = afterOpen.indexOf(endTag);
42
+ if (endIdx === -1)
43
+ return "";
44
+ const text = afterOpen
45
+ .slice(0, endIdx)
46
+ .replace(/<[^>]+>/g, "")
47
+ .trim();
48
+ return text.slice(0, 50);
49
+ }
50
+ export function extractInteractiveElements(html, maxElements = 30) {
51
+ const elements = [];
52
+ const tagRegex = /<(a|button|input|textarea|select|div|span)([^>]*)>/gi;
53
+ let match;
54
+ while ((match = tagRegex.exec(html)) !== null) {
55
+ if (elements.length >= maxElements)
56
+ break;
57
+ const tag = match[1].toLowerCase();
58
+ const attrStr = match[2];
59
+ const attrs = {};
60
+ const attrRegex = /(\w[\w-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
61
+ let attrMatch;
62
+ while ((attrMatch = attrRegex.exec(attrStr)) !== null) {
63
+ attrs[attrMatch[1].toLowerCase()] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
64
+ }
65
+ if (attrs["style"]?.includes("display:none") || attrs["style"]?.includes("display: none"))
66
+ continue;
67
+ if (attrs["hidden"] !== undefined)
68
+ continue;
69
+ if (attrs["aria-hidden"] === "true")
70
+ continue;
71
+ if (tag === "input" && attrs["type"] === "hidden")
72
+ continue;
73
+ const role = attrs["role"] || inferRole(tag, attrs["type"]);
74
+ const isInteractive = ["a", "button", "input", "textarea", "select"].includes(tag) || attrs["role"] !== undefined;
75
+ if (!isInteractive)
76
+ continue;
77
+ if (tag === "a" && !attrs["href"])
78
+ continue;
79
+ const name = getName(tag, attrs, html, match.index);
80
+ elements.push({
81
+ index: elements.length + 1,
82
+ tag,
83
+ role,
84
+ name,
85
+ value: attrs["value"],
86
+ href: attrs["href"],
87
+ visible: true,
88
+ });
89
+ }
90
+ return elements;
91
+ }
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) {
103
+ const lines = [];
104
+ lines.push(`Page: "${snapshot.title}"`);
105
+ lines.push(`URL: ${snapshot.url}`);
106
+ lines.push("");
107
+ if (snapshot.elements.length === 0 && !snapshot.truncated) {
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
+ }
125
+ }
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
+ }
139
+ }
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
+ }
146
+ }
147
+ return lines.join("\n");
148
+ }
@@ -0,0 +1,12 @@
1
+ export const DEFAULT_BROWSER_CONFIG = {
2
+ headless: true,
3
+ maxElements: 30,
4
+ maxContentChars: 2500,
5
+ maxConsoleEntries: 40,
6
+ maxConsoleLineChars: 400,
7
+ screenshotMaxWidth: 1280,
8
+ cookieDir: "",
9
+ viewportWidth: 1280,
10
+ viewportHeight: 720,
11
+ navigationTimeout: 15000,
12
+ };
@@ -0,0 +1,174 @@
1
+ import { rmSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { existsSync, readFileSync } from "fs";
6
+ import { t } from "../../i18n/index";
7
+ import { pc } from "../../ui/colors";
8
+ import { loadScenarios, filterByTags } from "./loader";
9
+ import { runScenario, findMmaRoot } from "./runner";
10
+ import { readManifest, upsertCertification, removeCertification } from "./manifest";
11
+ const HERE = dirname(fileURLToPath(import.meta.url));
12
+ const MMA_ROOT = findMmaRoot(HERE);
13
+ const USER_SCENARIO_DIR = join(homedir(), ".mma", "certification", "scenarios");
14
+ function readVersion() {
15
+ const candidates = [join(MMA_ROOT, "package.json")];
16
+ for (const p of candidates) {
17
+ if (existsSync(p)) {
18
+ try {
19
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
20
+ if (raw.version)
21
+ return raw.version;
22
+ }
23
+ catch {
24
+ // Broken package.json — fall back to the default version
25
+ }
26
+ }
27
+ }
28
+ return "0.0.0";
29
+ }
30
+ export function parseTags(s) {
31
+ return s
32
+ .split(",")
33
+ .map((x) => x.trim())
34
+ .filter(Boolean);
35
+ }
36
+ export async function certify(opts) {
37
+ const providerUrl = opts.providerUrl || opts.config.provider.baseUrl;
38
+ if (opts.tags.includes("security") && !opts.config.security?.enabled) {
39
+ console.error(pc.red(t("cli.cert_security_required")));
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ const { scenarios, errors } = loadScenarios(USER_SCENARIO_DIR);
44
+ for (const e of errors)
45
+ console.error(pc.yellow(` ${e}`));
46
+ const selected = filterByTags(scenarios, opts.tags);
47
+ if (selected.length === 0) {
48
+ console.error(pc.red(t("cli.cert_no_scenarios", { tags: opts.tags.join(",") })));
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+ const manifest = readManifest();
53
+ const existing = manifest.certifications.find((e) => e.model === opts.name && e.providerUrl === providerUrl);
54
+ if (existing && !opts.force) {
55
+ console.error(pc.yellow(t("cli.cert_exists", { model: opts.name })));
56
+ console.error(pc.yellow(t("cli.cert_exists_hint")));
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+ console.log(t("cli.cert_started", { model: opts.name, provider: providerUrl }));
61
+ const sandboxBase = join(process.cwd(), ".mma", "certification");
62
+ const results = [];
63
+ const total = selected.length;
64
+ let idx = 0;
65
+ for (const scenario of selected) {
66
+ idx++;
67
+ if (scenario.mode === "skip") {
68
+ console.log(pc.dim(`[${idx}/${total}] ${scenario.id} ... skipped`));
69
+ results.push({
70
+ id: scenario.id,
71
+ title: scenario.title,
72
+ status: "skipped",
73
+ passed: 0,
74
+ of: 0,
75
+ });
76
+ continue;
77
+ }
78
+ const res = await runScenario(scenario, {
79
+ model: opts.name,
80
+ providerUrl,
81
+ providerKey: opts.providerKey,
82
+ contextWindow: opts.contextWindow,
83
+ mmaRoot: MMA_ROOT,
84
+ sandboxBase,
85
+ defaultReps: opts.reps,
86
+ defaultThreshold: 2,
87
+ onRep: (id, rep, reps, passed, failures) => {
88
+ const word = passed ? pc.green(t("cli.cert_rep_pass")) : pc.red(t("cli.cert_rep_fail"));
89
+ console.log(`[${idx}/${total}] ${id} (${rep}/${reps})... ${word}`);
90
+ if (!passed)
91
+ console.log(` ${pc.dim(failures.join("; "))}`);
92
+ },
93
+ });
94
+ results.push(res);
95
+ }
96
+ if (opts.clean) {
97
+ try {
98
+ rmSync(sandboxBase, { recursive: true, force: true });
99
+ }
100
+ catch {
101
+ /* ignore */
102
+ }
103
+ }
104
+ const suite = summarize(results);
105
+ const entry = {
106
+ model: opts.name,
107
+ providerUrl,
108
+ mmaVersion: readVersion(),
109
+ certifiedAt: new Date().toISOString(),
110
+ suite,
111
+ results,
112
+ };
113
+ upsertCertification(entry);
114
+ console.log(t("cli.cert_done", {
115
+ passed: String(suite.passed),
116
+ failed: String(suite.failed),
117
+ skipped: String(suite.skipped),
118
+ total: String(suite.total),
119
+ }));
120
+ printResults(results);
121
+ }
122
+ export async function certStatus(name, config) {
123
+ const m = readManifest();
124
+ const providerUrl = config.provider.baseUrl;
125
+ const entry = m.certifications.find((e) => e.model === name && e.providerUrl === providerUrl);
126
+ if (!entry) {
127
+ console.log(t("cli.cert_not_found", { model: name }));
128
+ return;
129
+ }
130
+ console.log(`${t("cli.cert_provider_col")}: ${entry.providerUrl}`);
131
+ console.log(`${t("cli.cert_version_col")}: ${entry.mmaVersion} ${t("cli.cert_date_col")}: ${entry.certifiedAt.slice(0, 10)}`);
132
+ console.log(`${t("cli.cert_suite_col")}: ${entry.suite.passed} pass / ${entry.suite.failed} fail / ${entry.suite.skipped} skipped`);
133
+ printResults(entry.results);
134
+ }
135
+ export async function certList() {
136
+ const m = readManifest();
137
+ if (m.certifications.length === 0) {
138
+ console.log(t("cli.cert_empty"));
139
+ return;
140
+ }
141
+ for (const e of m.certifications) {
142
+ console.log(` ${pc.green("✔")} ${e.model} ${pc.dim(e.providerUrl)} ${e.mmaVersion} ${e.certifiedAt.slice(0, 10)} ${e.suite.passed}/${e.suite.total} pass`);
143
+ }
144
+ }
145
+ export async function uncertify(name, config) {
146
+ const removed = removeCertification(name, config.provider.baseUrl);
147
+ if (removed)
148
+ console.log(t("cli.cert_uncertified", { model: name }));
149
+ else
150
+ console.log(t("cli.cert_not_found", { model: name }));
151
+ }
152
+ function summarize(results) {
153
+ return {
154
+ passed: results.filter((r) => r.status === "pass").length,
155
+ failed: results.filter((r) => r.status === "fail").length,
156
+ skipped: results.filter((r) => r.status === "skipped").length,
157
+ total: results.length,
158
+ };
159
+ }
160
+ function printResults(results) {
161
+ for (const r of results) {
162
+ const icon = r.status === "pass"
163
+ ? pc.green("✔")
164
+ : r.status === "fail"
165
+ ? pc.red("✘")
166
+ : r.status === "skipped"
167
+ ? pc.dim("–")
168
+ : pc.yellow("!");
169
+ const detail = r.status === "skipped" ? pc.dim(r.title) : `${r.passed}/${r.of}`;
170
+ console.log(` ${icon} ${r.id} ${detail}`);
171
+ if (r.error)
172
+ console.log(` ${pc.dim(r.error)}`);
173
+ }
174
+ }