micro-models-agent 0.41.0 → 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.
- package/bin/mma.mjs +41 -41
- package/dist/cli/commands.js +9 -19
- package/dist/cli/completer.js +36 -37
- package/dist/cli/index.js +2 -2
- package/dist/cli/main.js +48 -23
- package/dist/cli/plugin-commands.js +36 -0
- package/dist/cli/repl-commands.js +40 -12
- package/dist/cli/repl.js +217 -87
- package/dist/cli/run-result.js +22 -0
- package/dist/cli/security-commands.js +5 -7
- package/dist/cli/setup.js +8 -26
- package/dist/config/config.js +52 -5
- package/dist/config/defaults.js +29 -5
- package/dist/config/experts.js +1 -1
- package/dist/config/index.js +3 -3
- package/dist/config/security.js +3 -10
- package/dist/core/agent-moe.js +2 -10
- package/dist/core/agent.js +273 -82
- package/dist/core/bootstrap.js +80 -13
- package/dist/core/index.js +2 -2
- package/dist/core/prompt-builder.js +23 -2
- package/dist/core/session-logger.js +46 -4
- package/dist/core/version.js +24 -0
- package/dist/i18n/en.json +75 -2
- package/dist/i18n/ru.json +74 -1
- package/dist/index.js +1 -1
- package/dist/llm/image-utils.js +4 -5
- package/dist/llm/index.js +4 -4
- package/dist/llm/model-loader.js +6 -6
- package/dist/llm/openai-compat.js +40 -34
- package/dist/llm/orchestrator.js +33 -29
- package/dist/llm/response.js +9 -9
- package/dist/logger/app-logger.js +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/main.js +1111 -954
- package/dist/migration/backup.js +13 -13
- package/dist/migration/detect.js +11 -11
- package/dist/migration/index.js +2 -2
- package/dist/modules/artifacts/store.js +61 -0
- package/dist/modules/browser/actions.js +34 -4
- package/dist/modules/browser/bridge-client.js +199 -0
- package/dist/modules/browser/bridge-path.js +10 -0
- package/dist/modules/browser/bridge-server.mjs +202 -202
- package/dist/modules/browser/cookie-store.js +6 -6
- package/dist/modules/browser/driver.js +136 -0
- package/dist/modules/browser/index.js +7 -5
- package/dist/modules/browser/module.js +8 -7
- package/dist/modules/browser/session.js +87 -84
- package/dist/modules/browser/snapshot.js +92 -58
- package/dist/modules/browser/types.js +4 -1
- package/dist/modules/certification/cli.js +2 -4
- package/dist/modules/certification/fact-checker.js +1 -3
- package/dist/modules/certification/loader.js +3 -9
- package/dist/modules/certification/runner.js +1 -4
- package/dist/modules/context/chunk-query.js +100 -0
- package/dist/modules/context/fact-extractor.js +162 -0
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/index.js +1 -1
- package/dist/modules/context/manager.js +160 -86
- package/dist/modules/execution/audit-runners.js +152 -0
- package/dist/modules/execution/auditor.js +177 -25
- package/dist/modules/execution/execution-plugin.js +272 -0
- package/dist/modules/execution/module.js +201 -544
- package/dist/modules/execution/moe-executor.js +25 -0
- package/dist/modules/execution/plan-store.js +1 -3
- package/dist/modules/execution/plan-tool.js +508 -0
- package/dist/modules/execution/plan-validator.js +10 -10
- package/dist/modules/execution/planner.js +6 -1
- package/dist/modules/execution/stuck-detector.js +173 -10
- package/dist/modules/execution/verifier.js +86 -42
- package/dist/modules/execution/windows-commands.js +41 -0
- package/dist/modules/hallucination/confidence.js +8 -1
- package/dist/modules/hallucination/detector.js +2 -5
- package/dist/modules/hallucination/factual.js +3 -64
- package/dist/modules/hallucination/index.js +1 -1
- package/dist/modules/hallucination/js-identifiers.js +190 -0
- package/dist/modules/hallucination/llm-judge.js +1 -3
- package/dist/modules/indexer/cache.js +9 -7
- package/dist/modules/indexer/index.js +3 -3
- package/dist/modules/indexer/module.js +95 -42
- package/dist/modules/indexer/project-profile.js +183 -0
- package/dist/modules/indexer/walker.js +17 -17
- package/dist/modules/lsp/check-tool.js +58 -0
- package/dist/modules/lsp/client.js +74 -31
- package/dist/modules/lsp/command.js +60 -0
- package/dist/modules/lsp/config.js +87 -33
- package/dist/modules/lsp/index.js +3 -3
- package/dist/modules/lsp/module.js +185 -21
- package/dist/modules/lsp/probe.js +76 -0
- package/dist/modules/lsp/project-root.js +32 -0
- package/dist/modules/lsp/startup-check.js +141 -0
- package/dist/modules/mcp/module.js +2 -6
- package/dist/modules/memory/index.js +1 -1
- package/dist/modules/memory/module.js +71 -23
- package/dist/modules/memory/search.js +11 -9
- package/dist/modules/memory/store.js +13 -13
- package/dist/modules/pipelines/engine.js +10 -10
- package/dist/modules/pipelines/index.js +3 -3
- package/dist/modules/pipelines/parser.js +17 -14
- package/dist/modules/pipelines/template.js +1 -1
- package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
- package/dist/modules/plugins/builtin/notify.js +3 -2
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/loader.js +59 -17
- package/dist/modules/plugins/manager.js +73 -17
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +1 -1
- package/dist/modules/processes/registry.js +135 -46
- package/dist/modules/registry.js +4 -2
- package/dist/modules/security/audit-notifier.js +39 -39
- package/dist/modules/security/command-validator.js +2 -8
- package/dist/modules/security/data-sanitizer.js +1 -9
- package/dist/modules/security/encryption.js +58 -56
- package/dist/modules/security/network-validator.js +1 -9
- package/dist/modules/security/path-validator.js +1 -3
- package/dist/modules/security/security-policies.js +3 -19
- package/dist/modules/security/session-encryption.js +1 -1
- package/dist/modules/security/session-isolation.js +8 -8
- package/dist/modules/session/index.js +3 -3
- package/dist/modules/session/module.js +5 -5
- package/dist/modules/session/store.js +3 -9
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +1 -2
- package/dist/modules/updater/checker.js +70 -6
- package/dist/modules/updater/index.js +2 -1
- package/dist/modules/updater/module.js +116 -0
- package/dist/modules/user-profile/compressor.js +2 -2
- package/dist/modules/user-profile/index.js +1 -1
- package/dist/modules/user-profile/profile.js +9 -9
- package/dist/tools/attach-image.js +1 -1
- package/dist/tools/bash.js +178 -19
- package/dist/tools/browser.js +46 -29
- package/dist/tools/chunk-query.js +99 -0
- package/dist/tools/download-file.js +116 -0
- package/dist/tools/enable-tools.js +58 -0
- package/dist/tools/executor.js +4 -5
- package/dist/tools/file-info.js +13 -12
- package/dist/tools/filter-tools.js +9 -2
- package/dist/tools/glob-tool.js +11 -11
- package/dist/tools/grep-tool.js +1 -3
- package/dist/tools/hidden-tools-block.js +37 -0
- package/dist/tools/index.js +13 -2
- package/dist/tools/list-dir.js +18 -17
- package/dist/tools/load-skill.js +1 -3
- package/dist/tools/path-utils.js +4 -4
- package/dist/tools/pipeline-run.js +25 -25
- package/dist/tools/process-kill.js +11 -11
- package/dist/tools/process-list.js +20 -22
- package/dist/tools/process-log.js +22 -18
- package/dist/tools/question.js +1 -3
- package/dist/tools/read-file.js +10 -2
- package/dist/tools/recall.js +44 -37
- package/dist/tools/registry.js +15 -4
- package/dist/tools/remember.js +29 -29
- package/dist/tools/scope-check.js +9 -9
- package/dist/tools/subagent.js +54 -9
- package/dist/tools/user-input.js +1 -1
- package/dist/tools/web-browse.js +3 -3
- package/dist/tools/web-fetch.js +3 -3
- package/dist/tools/web-search.js +3 -3
- package/dist/tools/write-file.js +1 -3
- package/dist/ui/box.js +1 -5
- package/dist/ui/index.js +6 -6
- package/dist/ui/line-editor.js +703 -0
- package/dist/ui/line-math.js +69 -0
- package/dist/ui/md-formatter.js +33 -33
- package/dist/ui/output.js +5 -5
- package/dist/ui/plan-view.js +103 -0
- package/dist/ui/renderer.js +15 -10
- package/dist/ui/table.js +1 -1
- package/package.json +48 -48
|
@@ -1,8 +1,35 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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.
|
|
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
|
|
108
|
-
if (!this.
|
|
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.
|
|
139
|
+
if (this.driver)
|
|
115
140
|
return;
|
|
116
|
-
this.
|
|
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.
|
|
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.
|
|
148
|
+
if (!this.driver)
|
|
135
149
|
return;
|
|
136
|
-
const cookies = await this.
|
|
150
|
+
const cookies = await this.driver.cookies();
|
|
137
151
|
await this.cookieStore.save(cookies);
|
|
138
152
|
}
|
|
139
153
|
async takeSnapshot() {
|
|
140
|
-
if (!this.
|
|
154
|
+
if (!this.driver)
|
|
141
155
|
return "No page open.";
|
|
142
|
-
await this.
|
|
143
|
-
const html = await this.
|
|
144
|
-
const url = this.
|
|
145
|
-
const title = await this.
|
|
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({
|
|
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.
|
|
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.
|
|
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.
|
|
212
|
+
const err = await this.ensureDriver();
|
|
188
213
|
if (err)
|
|
189
214
|
return err;
|
|
190
|
-
if (!this.
|
|
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
|
-
|
|
197
|
-
await this.
|
|
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.
|
|
236
|
+
const err = await this.ensureDriver();
|
|
215
237
|
if (err)
|
|
216
238
|
return err;
|
|
217
|
-
if (!this.
|
|
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
|
-
|
|
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.
|
|
260
|
+
const err = await this.ensureDriver();
|
|
240
261
|
if (err)
|
|
241
262
|
return err;
|
|
242
|
-
if (!this.
|
|
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.
|
|
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.
|
|
283
|
+
const err = await this.ensureDriver();
|
|
263
284
|
if (err)
|
|
264
285
|
return err;
|
|
265
|
-
if (!this.
|
|
286
|
+
if (!this.driver)
|
|
266
287
|
return { success: false, output: "No page" };
|
|
267
|
-
await this.
|
|
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.
|
|
294
|
+
const err = await this.ensureDriver();
|
|
279
295
|
if (err)
|
|
280
296
|
return err;
|
|
281
|
-
if (!this.
|
|
297
|
+
if (!this.driver)
|
|
282
298
|
return { success: false, output: "No page" };
|
|
283
|
-
await this.
|
|
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.
|
|
305
|
+
const err = await this.ensureDriver();
|
|
295
306
|
if (err)
|
|
296
307
|
return err;
|
|
297
|
-
if (!this.
|
|
308
|
+
if (!this.driver)
|
|
298
309
|
return { success: false, output: "No page" };
|
|
299
|
-
const buffer = await this.
|
|
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.
|
|
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.
|
|
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.
|
|
321
|
-
await this.
|
|
322
|
-
this.
|
|
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
|
|
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 ===
|
|
5
|
-
return
|
|
6
|
-
if (t ===
|
|
7
|
-
return
|
|
8
|
-
if (t ===
|
|
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 ===
|
|
11
|
-
return
|
|
12
|
-
if (inputType ===
|
|
13
|
-
return
|
|
14
|
-
if (inputType ===
|
|
15
|
-
return
|
|
16
|
-
return
|
|
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 ===
|
|
19
|
-
return
|
|
20
|
-
if (t ===
|
|
21
|
-
return
|
|
22
|
-
return
|
|
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 =
|
|
26
|
-
attrs[
|
|
27
|
-
attrs[
|
|
28
|
-
attrs[
|
|
29
|
-
attrs[
|
|
30
|
-
attrs[
|
|
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
|
|
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[
|
|
65
|
+
if (attrs["style"]?.includes("display:none") || attrs["style"]?.includes("display: none"))
|
|
62
66
|
continue;
|
|
63
|
-
if (attrs[
|
|
67
|
+
if (attrs["hidden"] !== undefined)
|
|
64
68
|
continue;
|
|
65
|
-
if (attrs[
|
|
69
|
+
if (attrs["aria-hidden"] === "true")
|
|
66
70
|
continue;
|
|
67
|
-
if (tag ===
|
|
71
|
+
if (tag === "input" && attrs["type"] === "hidden")
|
|
68
72
|
continue;
|
|
69
|
-
const role = attrs[
|
|
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 ===
|
|
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[
|
|
84
|
-
href: attrs[
|
|
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
|
|
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(
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
lines.push(
|
|
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.
|
|
110
|
-
lines.push(
|
|
111
|
-
lines.push(t(
|
|
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(
|
|
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
|
|
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;
|