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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Updater } from "./checker";
|
|
2
|
+
import { t } from "../../i18n/index";
|
|
3
|
+
import { pc } from "../../ui/colors";
|
|
4
|
+
/**
|
|
5
|
+
* Self-update module. Fires an ASYNC (non-blocking) check at startup so the
|
|
6
|
+
* agent never waits on the network. When `autoInstall` is enabled, a newer
|
|
7
|
+
* version is installed globally via `npm install -g` in the background and
|
|
8
|
+
* the user is notified; the running process keeps working on the old version
|
|
9
|
+
* until the next launch.
|
|
10
|
+
*/
|
|
11
|
+
export class UpdaterModule {
|
|
12
|
+
name = "updater";
|
|
13
|
+
updater;
|
|
14
|
+
config;
|
|
15
|
+
logger;
|
|
16
|
+
started = false;
|
|
17
|
+
timer = null;
|
|
18
|
+
/** Promise of the current run-loop pass (resolves when it settles). */
|
|
19
|
+
idlePromise = null;
|
|
20
|
+
/** Test hook: disable real npm installs. */
|
|
21
|
+
installEnabled = true;
|
|
22
|
+
constructor(config, currentVersion, packageName, logger, installRunner) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.updater = new Updater(currentVersion, packageName, installRunner);
|
|
25
|
+
this.logger = logger ?? {
|
|
26
|
+
info: () => { },
|
|
27
|
+
warn: (m) => console.warn(pc.yellow(m)),
|
|
28
|
+
error: (m) => console.error(pc.red(m)),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Test hook: prevent the module from actually running npm install. */
|
|
32
|
+
setInstallEnabled(enabled) {
|
|
33
|
+
this.installEnabled = enabled;
|
|
34
|
+
}
|
|
35
|
+
getUpdater() {
|
|
36
|
+
return this.updater;
|
|
37
|
+
}
|
|
38
|
+
isStarted() {
|
|
39
|
+
return this.started;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Start the background check. Returns immediately (fire-and-forget) so the
|
|
43
|
+
* caller never blocks on the registry. Use `intervalMs > 0` for a permanent
|
|
44
|
+
* re-check loop (the "auto-update daemon" behavior).
|
|
45
|
+
*/
|
|
46
|
+
start() {
|
|
47
|
+
if (this.started || !this.config.enabled)
|
|
48
|
+
return;
|
|
49
|
+
this.started = true;
|
|
50
|
+
this.idlePromise = this.runLoop();
|
|
51
|
+
}
|
|
52
|
+
stop() {
|
|
53
|
+
this.started = false;
|
|
54
|
+
if (this.timer) {
|
|
55
|
+
clearTimeout(this.timer);
|
|
56
|
+
this.timer = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Wait for the current check/install cycle to settle, bounded by timeoutMs.
|
|
61
|
+
* Used by one-shot CLI runs so a background `npm install -g` isn't killed by
|
|
62
|
+
* process.exit() before it completes. No-op when the module never started.
|
|
63
|
+
*/
|
|
64
|
+
async waitForIdle(timeoutMs = 130000) {
|
|
65
|
+
const idle = this.idlePromise;
|
|
66
|
+
if (!idle)
|
|
67
|
+
return;
|
|
68
|
+
await Promise.race([idle, new Promise((r) => setTimeout(r, timeoutMs))]);
|
|
69
|
+
}
|
|
70
|
+
async runLoop() {
|
|
71
|
+
while (this.started) {
|
|
72
|
+
await this.checkOnce();
|
|
73
|
+
if (!this.started)
|
|
74
|
+
return;
|
|
75
|
+
if (this.config.intervalMs > 0) {
|
|
76
|
+
await new Promise((r) => (this.timer = setTimeout(r, this.config.intervalMs)));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.started = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Perform a single check; if an update is available, optionally install it. */
|
|
84
|
+
async runOnce() {
|
|
85
|
+
const result = await this.updater.check();
|
|
86
|
+
if (result.error) {
|
|
87
|
+
this.logger.info(t("updater.check_error", { error: result.error }));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!result.updateAvailable || !result.latest)
|
|
91
|
+
return;
|
|
92
|
+
if (this.config.autoInstall && this.installEnabled) {
|
|
93
|
+
this.logger.info(t("updater.installing", {
|
|
94
|
+
current: result.current,
|
|
95
|
+
latest: result.latest,
|
|
96
|
+
}));
|
|
97
|
+
const install = await this.updater.install(result.latest);
|
|
98
|
+
if (install.success) {
|
|
99
|
+
this.logger.info(t("updater.installed", { current: result.current, latest: result.latest }));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.logger.warn(t("updater.install_failed", {
|
|
103
|
+
current: result.current,
|
|
104
|
+
latest: result.latest,
|
|
105
|
+
error: install.error ?? "",
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this.logger.info(t("updater.available", { current: result.current, latest: result.latest }));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async checkOnce() {
|
|
114
|
+
await this.runOnce();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -8,9 +8,9 @@ export class ProfileCompressor {
|
|
|
8
8
|
if (Object.keys(info.preferences).length > 0) {
|
|
9
9
|
const prefs = Object.entries(info.preferences)
|
|
10
10
|
.map(([k, v]) => `${k}=${v}`)
|
|
11
|
-
.join(
|
|
11
|
+
.join(",");
|
|
12
12
|
parts.push(`Prefs: ${prefs}`);
|
|
13
13
|
}
|
|
14
|
-
return parts.join(
|
|
14
|
+
return parts.join(", ");
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { UserProfile } from
|
|
1
|
+
export { UserProfile } from "./profile";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from
|
|
2
|
-
import { join } from
|
|
3
|
-
import { homedir, hostname, platform, type } from
|
|
4
|
-
import { env } from
|
|
5
|
-
import { ProfileCompressor } from
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir, hostname, platform, type } from "os";
|
|
4
|
+
import { env } from "process";
|
|
5
|
+
import { ProfileCompressor } from "./compressor";
|
|
6
6
|
export class UserProfile {
|
|
7
7
|
profileDir;
|
|
8
8
|
info = null;
|
|
@@ -15,7 +15,7 @@ export class UserProfile {
|
|
|
15
15
|
platform: platform(),
|
|
16
16
|
os: `${type()} ${hostname()}`,
|
|
17
17
|
hostname: hostname(),
|
|
18
|
-
shell: env.SHELL || env.ComSpec ||
|
|
18
|
+
shell: env.SHELL || env.ComSpec || "unknown",
|
|
19
19
|
home: homedir(),
|
|
20
20
|
nodeVersion: process.version,
|
|
21
21
|
preferences: { ...this.preferences },
|
|
@@ -26,14 +26,14 @@ export class UserProfile {
|
|
|
26
26
|
if (!existsSync(this.profileDir)) {
|
|
27
27
|
mkdirSync(this.profileDir, { recursive: true });
|
|
28
28
|
}
|
|
29
|
-
writeFileSync(join(this.profileDir,
|
|
29
|
+
writeFileSync(join(this.profileDir, "profile.json"), JSON.stringify({ ...this.info, preferences: this.preferences }, null, 2), "utf-8");
|
|
30
30
|
}
|
|
31
31
|
load() {
|
|
32
|
-
const path = join(this.profileDir,
|
|
32
|
+
const path = join(this.profileDir, "profile.json");
|
|
33
33
|
if (!existsSync(path))
|
|
34
34
|
return null;
|
|
35
35
|
try {
|
|
36
|
-
const data = JSON.parse(readFileSync(path,
|
|
36
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
37
37
|
this.info = {
|
|
38
38
|
platform: data.platform,
|
|
39
39
|
os: data.os,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { t } from "../i18n/index";
|
|
4
|
-
import { loadFileAsDataUrl, loadUrlAsDataUrl, readClipboardImage, bufferToDataUrl } from "../llm/image-utils";
|
|
4
|
+
import { loadFileAsDataUrl, loadUrlAsDataUrl, readClipboardImage, bufferToDataUrl, } from "../llm/image-utils";
|
|
5
5
|
import { logSecurityBlock } from "../modules/security/audit-log";
|
|
6
6
|
export const attachImageTool = {
|
|
7
7
|
name: "attach_image",
|
package/dist/tools/bash.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { isCommandAllowed, sanitizeCommandForLog
|
|
2
|
-
import { logBashCommand, logSecurityBlock
|
|
1
|
+
import { isCommandAllowed, sanitizeCommandForLog } from "../modules/security/command-validator";
|
|
2
|
+
import { logBashCommand, logSecurityBlock } from "../modules/security/audit-log";
|
|
3
3
|
import { getSessionSecurityConfig } from "../modules/security/session-isolation";
|
|
4
4
|
import { DEFAULT_SECURITY_CONFIG } from "../config/security";
|
|
5
|
-
import { processRegistry, registerKillable, unregisterKillable
|
|
5
|
+
import { processRegistry, registerKillable, unregisterKillable } from "../modules/processes";
|
|
6
6
|
import { t } from "../i18n/index";
|
|
7
7
|
import { platform } from "os";
|
|
8
8
|
import { MAX_PREVIEW_LINES } from "./preview";
|
|
@@ -16,11 +16,26 @@ import { MAX_PREVIEW_LINES } from "./preview";
|
|
|
16
16
|
export const BASH_GRACE_MS = 5000;
|
|
17
17
|
/** Short window for explicit `background: true` — surfaces immediate spawn failures (bad cwd, missing shell). */
|
|
18
18
|
const SPAWN_SETTLE_MS = 100;
|
|
19
|
+
/** Number of already-emitted lines shown in the background start message. */
|
|
20
|
+
const BG_OUTPUT_PREVIEW_LINES = 15;
|
|
19
21
|
let bashGraceMs = BASH_GRACE_MS;
|
|
20
22
|
/** Test hook: override the auto-background grace window. */
|
|
21
23
|
export function setBashGraceMs(ms) {
|
|
22
24
|
bashGraceMs = ms;
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Consecutive failures per first command word (resets on success). A model
|
|
28
|
+
* that keeps calling a forbidden Unix command in cmd.exe (head/cat/grep/...)
|
|
29
|
+
* gets a hint the first couple of times; after HARD_BLOCK_THRESHOLD consecutive
|
|
30
|
+
* failures the tool hard-stops with a STOP directive instead of repeating the
|
|
31
|
+
* same hint forever (observed: `head` called 5×, `Select-String` 3× in one run).
|
|
32
|
+
*/
|
|
33
|
+
const FAILING_FIRST_WORDS = new Map();
|
|
34
|
+
const HARD_BLOCK_THRESHOLD = 3;
|
|
35
|
+
/** Test hook: clear the consecutive-failure tracking state. */
|
|
36
|
+
export function resetBashFailureTracking() {
|
|
37
|
+
FAILING_FIRST_WORDS.clear();
|
|
38
|
+
}
|
|
24
39
|
/**
|
|
25
40
|
* Detect a file write via `echo/printf ... > file` — common model habit that
|
|
26
41
|
* breaks in cmd.exe: single quotes are not grouping quotes, `>` only applies
|
|
@@ -108,6 +123,12 @@ const UNIX_TO_WIN_HINTS = {
|
|
|
108
123
|
echo: "echo works on Windows, but avoid pipes (|).",
|
|
109
124
|
"Get-Content": "Use the read_file tool instead.",
|
|
110
125
|
"Select-Object": "Use the read_file tool with offset/limit instead.",
|
|
126
|
+
"Write-Host": "PowerShell cmdlet — this shell is cmd.exe. Print with plain echo instead.",
|
|
127
|
+
"Select-String": "Use the grep tool instead.",
|
|
128
|
+
"Out-File": "Write files with the write_file tool instead.",
|
|
129
|
+
"Set-Content": "Write files with the write_file tool instead.",
|
|
130
|
+
"Get-ChildItem": "Use the list_dir tool instead.",
|
|
131
|
+
"Remove-Item": "Use the delete_file tool instead.",
|
|
111
132
|
};
|
|
112
133
|
/** Unix commands that can be transparently translated to cmd.exe equivalents. */
|
|
113
134
|
const UNIX_TO_WIN_TRANSLATE = {
|
|
@@ -198,9 +219,94 @@ export function parseToolArgs(raw) {
|
|
|
198
219
|
}
|
|
199
220
|
return args;
|
|
200
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Detect a test-runner report embedded in command output (bun test / vitest /
|
|
224
|
+
* jest / mocha / `go test` / pytest). Returns null when the output does not
|
|
225
|
+
* look like a test run. Used to inject a warning when tests fail so the model
|
|
226
|
+
* cannot close a "run the tests" step as done.
|
|
227
|
+
*/
|
|
228
|
+
export function detectTestResults(output) {
|
|
229
|
+
if (!output)
|
|
230
|
+
return null;
|
|
231
|
+
// Framework-specific quantifiers first (unambiguous).
|
|
232
|
+
const jest = output.match(/Tests:\s+(\d+)\s+passed,\s*(\d+)\s+failed/i);
|
|
233
|
+
if (jest) {
|
|
234
|
+
return { framework: "jest", passed: +jest[1], failed: +jest[2] };
|
|
235
|
+
}
|
|
236
|
+
const mochaPass = output.match(/(\d+)\s+passing/i);
|
|
237
|
+
const mochaFail = output.match(/(\d+)\s+failing/i);
|
|
238
|
+
if (mochaPass || mochaFail) {
|
|
239
|
+
return {
|
|
240
|
+
framework: "mocha",
|
|
241
|
+
passed: mochaPass ? +mochaPass[1] : 0,
|
|
242
|
+
failed: mochaFail ? +mochaFail[1] : 0,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const pytest = output.match(/(\d+)\s+passed[^\n]*?(?:,\s*(\d+)\s+failed)?/i);
|
|
246
|
+
if (pytest &&
|
|
247
|
+
/pytest|passed|failed/i.test(output) &&
|
|
248
|
+
/(=====|short test summary|tests\s+ok)/i.test(output) === false &&
|
|
249
|
+
/pytest|collect/i.test(output)) {
|
|
250
|
+
return {
|
|
251
|
+
framework: "pytest",
|
|
252
|
+
passed: +pytest[1],
|
|
253
|
+
failed: pytest[2] ? +pytest[2] : 0,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// bun test / vitest: per-case markers + trailing "N pass / M fail".
|
|
257
|
+
const failMarkers = output.match(/\(fail\)/g)?.length ?? 0;
|
|
258
|
+
const passMarkers = output.match(/\(pass\)/g)?.length ?? 0;
|
|
259
|
+
const xMarkers = output.match(/\s×\s/g)?.length ?? 0;
|
|
260
|
+
const summaryMatch = output.match(/^\s*(\d+)\s+pass[^\n]*$/m);
|
|
261
|
+
const failSummary = output.match(/^\s*(\d+)\s+fail[^\n]*$/m);
|
|
262
|
+
const ran = output.match(/(?:Ran|ran)\s+\d+\s+tests/i);
|
|
263
|
+
if (failMarkers > 0 || passMarkers > 0 || xMarkers > 0 || ran) {
|
|
264
|
+
const failed = Math.max(failMarkers + xMarkers, failSummary ? +failSummary[1] : 0);
|
|
265
|
+
const passed = Math.max(passMarkers, summaryMatch ? +summaryMatch[1] : 0);
|
|
266
|
+
return {
|
|
267
|
+
framework: ran ? "bun/vitest" : "vitest",
|
|
268
|
+
passed,
|
|
269
|
+
failed,
|
|
270
|
+
summary: summaryMatch?.[0] ?? failSummary?.[0],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* R5: a command that runs a script file (bun/node/python/...) exited 0 but
|
|
277
|
+
* printed nothing. The most common cause in agent sessions: the program has
|
|
278
|
+
* no entry point — nobody calls its main() with the CLI args. Surface a
|
|
279
|
+
* concrete hint instead of letting the model stare at an empty output.
|
|
280
|
+
*/
|
|
281
|
+
const CLI_FILE_RUN_RE = /\b(bun|node|deno|python|python3|tsx|ts-node|php|ruby|go\s+run)\S*\s+(run\s+)?["']?[\w./\\-]+\.(ts|js|tsx|jsx|mjs|cjs|py)\b/;
|
|
282
|
+
export function emptyCliRunHint(command, output, code) {
|
|
283
|
+
if (code !== 0 || output.trim())
|
|
284
|
+
return null;
|
|
285
|
+
if (/[>|]/.test(command))
|
|
286
|
+
return null; // redirection/pipes — output went elsewhere
|
|
287
|
+
if (/\b(bun test|vitest|pytest|jest|mocha|--test)\b/i.test(command))
|
|
288
|
+
return null;
|
|
289
|
+
if (!CLI_FILE_RUN_RE.test(command))
|
|
290
|
+
return null;
|
|
291
|
+
return "the command exited 0 but printed NOTHING to stdout. If this should run a CLI program, the file probably has no entry point: read it with read_file and check the code actually calls its main function with command-line arguments (e.g. main(process.argv[2])) and prints results with console.log.";
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Detect npm/npx/bunx failing with "could not determine executable to run"
|
|
295
|
+
* — the package declares no `bin` (or `npm run <script>` named a missing
|
|
296
|
+
* script). Append a concrete hint instead of letting the model re-try blindly
|
|
297
|
+
* (observed in two agent sessions: the model burned many iterations re-running
|
|
298
|
+
* the same failing `npx`/`bunx`/`npm run` command).
|
|
299
|
+
*/
|
|
300
|
+
const NPM_EXEC_RE = /could not determine executable to run/i;
|
|
301
|
+
export function npmExecHint(output) {
|
|
302
|
+
if (NPM_EXEC_RE.test(output)) {
|
|
303
|
+
return `${output}\n\nHint: ${t("exec.npm_exec_hint")}`;
|
|
304
|
+
}
|
|
305
|
+
return output;
|
|
306
|
+
}
|
|
201
307
|
export const bashTool = {
|
|
202
308
|
name: "bash",
|
|
203
|
-
description: "Execute a shell command and return its output. Use for running tests, build, git, and shell operations. Commands that are still running after a few seconds are automatically moved to the background and return a process id — manage them with process_list, process_log, process_kill. Set background=true to return a process id immediately for commands you know are long-running (dev servers, watchers).",
|
|
309
|
+
description: "Execute a shell command and return its output. Use for running tests, build, git, and shell operations. Commands that are still running after a few seconds are automatically moved to the background and return a process id — manage them with process_list, process_log, process_kill. Set background=true to return a process id immediately for commands you know are long-running (dev servers, watchers).\n\nWindows notes: the shell is cmd.exe — PowerShell cmdlets (Write-Host, Get-Content, Select-String, Out-File) and bash heredocs (cat << EOF) do NOT work there. Use the read_file/write_file tools instead of cat/echo redirection. Sequential commands: use && (a leading ; is auto-converted to &).",
|
|
204
310
|
tags: ["shell", "code"],
|
|
205
311
|
parameters: {
|
|
206
312
|
type: "object",
|
|
@@ -287,19 +393,55 @@ export const bashTool = {
|
|
|
287
393
|
const code = entry.exitCode;
|
|
288
394
|
let output = entry.log.join("\n");
|
|
289
395
|
processRegistry.remove(entry.id);
|
|
396
|
+
// R5: script file ran with exit 0 but empty output — likely a
|
|
397
|
+
// missing entry point (main is never called with argv).
|
|
398
|
+
const cliHint = emptyCliRunHint(command, output, code);
|
|
399
|
+
if (cliHint) {
|
|
400
|
+
output = `(exit code 0, no output)\n\nHint: ${cliHint}`;
|
|
401
|
+
}
|
|
402
|
+
// Auto-verify test runs: a failing suite must not be reported as a
|
|
403
|
+
// clean success. Inject a prominent marker the model cannot miss
|
|
404
|
+
// (the tpical failure: tests fail but the CLI exit code is 0).
|
|
405
|
+
const testRun = detectTestResults(output);
|
|
406
|
+
if (testRun && testRun.failed > 0) {
|
|
407
|
+
output =
|
|
408
|
+
`[test-runner] ${testRun.framework}: ${testRun.failed} test(s) FAILING, ${testRun.passed} passing — do NOT mark verification steps as done while tests fail. Investigate the failures, fix the code, then re-run the tests.\n\n` +
|
|
409
|
+
output;
|
|
410
|
+
}
|
|
411
|
+
else if (testRun && testRun.failed === 0 && testRun.passed > 0) {
|
|
412
|
+
output = `[test-runner] ${testRun.framework}: all ${testRun.passed} test(s) passing.\n\n${output}`;
|
|
413
|
+
}
|
|
290
414
|
if (!output && code !== 0) {
|
|
291
415
|
output = `(exit code ${code})`;
|
|
292
416
|
}
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
417
|
+
// npm/npx/bunx: "could not determine executable to run" — the
|
|
418
|
+
// package has no `bin` entry (or the script does not exist).
|
|
419
|
+
// This is cross-platform, so it runs before the win32 hint block.
|
|
420
|
+
output = npmExecHint(output);
|
|
421
|
+
// On Windows, hint about Unix commands that don't work, and
|
|
422
|
+
// hard-stop a command that keeps failing the same way. The
|
|
423
|
+
// hint keys on the ORIGINAL command word (before
|
|
424
|
+
// adaptCommandForWindows translated cat→type): the translation
|
|
425
|
+
// is only for execution, but the model wrote `cat`, and that is
|
|
426
|
+
// what UNIX_TO_WIN_HINTS knows about. Keying on the translated
|
|
427
|
+
// word left `cat` invisible to the hint/block (observed: model
|
|
428
|
+
// ran cat 4+ times in one session with no guidance).
|
|
429
|
+
if (platform() === "win32") {
|
|
430
|
+
const originalFirstWord = originalCommand.trim().split(/\s+/)[0]?.split(/[\\/]/).pop();
|
|
431
|
+
if (originalFirstWord && originalFirstWord in UNIX_TO_WIN_HINTS) {
|
|
432
|
+
if (code === 0) {
|
|
433
|
+
FAILING_FIRST_WORDS.delete(originalFirstWord);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
const failures = (FAILING_FIRST_WORDS.get(originalFirstWord) || 0) + 1;
|
|
437
|
+
FAILING_FIRST_WORDS.set(originalFirstWord, failures);
|
|
438
|
+
if (failures >= HARD_BLOCK_THRESHOLD) {
|
|
439
|
+
output = `STOP using "${originalFirstWord}" — it does not work in this cmd.exe shell and has failed ${failures} times in a row. ${UNIX_TO_WIN_HINTS[originalFirstWord]}`;
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
output = `${output}\n\nHint: "${originalFirstWord}" may not work on Windows. ${UNIX_TO_WIN_HINTS[originalFirstWord]}`;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
303
445
|
}
|
|
304
446
|
}
|
|
305
447
|
// Update audit log with result
|
|
@@ -307,7 +449,18 @@ export const bashTool = {
|
|
|
307
449
|
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), code === 0, `Working directory: ${workdir}, Output length: ${output.length}`);
|
|
308
450
|
}
|
|
309
451
|
const lines = output.split("\n");
|
|
310
|
-
if (
|
|
452
|
+
if (testRun && testRun.failed > 0) {
|
|
453
|
+
// Keep the failure details: test runners print passing markers
|
|
454
|
+
// first and the errors + summary at the very end. A short head
|
|
455
|
+
// preview hides exactly what the model needs to fix the failures.
|
|
456
|
+
const TEST_TAIL_LINES = 400;
|
|
457
|
+
const kept = lines.slice(-TEST_TAIL_LINES);
|
|
458
|
+
const skipped = lines.length - kept.length;
|
|
459
|
+
output =
|
|
460
|
+
(skipped > 0 ? `[... ${skipped} earlier lines omitted — failing tests below]\n` : "") +
|
|
461
|
+
kept.join("\n");
|
|
462
|
+
}
|
|
463
|
+
else if (lines.length > MAX_PREVIEW_LINES) {
|
|
311
464
|
output =
|
|
312
465
|
lines.slice(0, MAX_PREVIEW_LINES).join("\n") +
|
|
313
466
|
`\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
@@ -316,13 +469,19 @@ export const bashTool = {
|
|
|
316
469
|
}
|
|
317
470
|
// Still running — promote to a background process.
|
|
318
471
|
const explicit = args.background === true;
|
|
319
|
-
const
|
|
472
|
+
const preview = entry.log
|
|
473
|
+
.filter((l) => l.trim() !== "")
|
|
474
|
+
.slice(0, BG_OUTPUT_PREVIEW_LINES)
|
|
475
|
+
.join("\n");
|
|
476
|
+
let output = `${t("proc.started", {
|
|
320
477
|
id: entry.id,
|
|
321
478
|
pid: entry.pid,
|
|
322
479
|
command,
|
|
323
|
-
})}${explicit ? "" : `\n${t("proc.promoted_hint", { ms: settleMs })}`}
|
|
324
|
-
|
|
325
|
-
|
|
480
|
+
})}${explicit ? "" : `\n${t("proc.promoted_hint", { ms: settleMs })}`}`;
|
|
481
|
+
if (preview) {
|
|
482
|
+
output += `\n\n${t("proc.output_preview", { lines: preview })}`;
|
|
483
|
+
}
|
|
484
|
+
output += `\n${t("proc.manage_hint", { id: entry.id })}`;
|
|
326
485
|
if (securityConfig?.logCommands) {
|
|
327
486
|
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), true, `Started in background: ${entry.id} (PID ${entry.pid})`);
|
|
328
487
|
}
|
package/dist/tools/browser.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { BrowserSession } from
|
|
2
|
-
import { DEFAULT_BROWSER_CONFIG } from
|
|
3
|
-
import { t } from
|
|
4
|
-
import { join } from
|
|
1
|
+
import { BrowserSession } from "../modules/browser/session";
|
|
2
|
+
import { DEFAULT_BROWSER_CONFIG } from "../modules/browser/types";
|
|
3
|
+
import { t } from "../i18n/index";
|
|
4
|
+
import { join } from "path";
|
|
5
5
|
let session = null;
|
|
6
6
|
function getSession(ctx) {
|
|
7
7
|
if (!session) {
|
|
8
|
-
const cookieDir = join(ctx.baseDir,
|
|
8
|
+
const cookieDir = join(ctx.baseDir, ".mma", "browser");
|
|
9
9
|
session = new BrowserSession({
|
|
10
10
|
...DEFAULT_BROWSER_CONFIG,
|
|
11
11
|
headless: ctx.config.browser?.headless ?? true,
|
|
12
12
|
maxElements: ctx.config.browser?.maxElements ?? 30,
|
|
13
|
+
maxContentChars: ctx.config.browser?.maxContentChars ?? 2500,
|
|
14
|
+
maxConsoleEntries: ctx.config.browser?.maxConsoleEntries ?? 40,
|
|
15
|
+
maxConsoleLineChars: ctx.config.browser?.maxConsoleLineChars ?? 400,
|
|
13
16
|
navigationTimeout: ctx.config.browser?.navigationTimeout ?? 15000,
|
|
14
17
|
viewportWidth: ctx.config.browser?.viewportWidth ?? 1280,
|
|
15
18
|
viewportHeight: ctx.config.browser?.viewportHeight ?? 720,
|
|
@@ -19,66 +22,80 @@ function getSession(ctx) {
|
|
|
19
22
|
return session;
|
|
20
23
|
}
|
|
21
24
|
export function formatScreenshotForTextModel(snapshot) {
|
|
22
|
-
return `${t(
|
|
25
|
+
return `${t("tool.screenshot_unavailable")}\n\n${snapshot}`;
|
|
23
26
|
}
|
|
24
27
|
export function formatScreenshotResult(snapshot, screenshot, supportsVision) {
|
|
25
28
|
if (!supportsVision) {
|
|
26
29
|
return formatScreenshotForTextModel(snapshot);
|
|
27
30
|
}
|
|
28
|
-
const base64 = screenshot.toString(
|
|
31
|
+
const base64 = screenshot.toString("base64");
|
|
29
32
|
return `${snapshot}\n\n[Screenshot: data:image/png;base64,${base64}]`;
|
|
30
33
|
}
|
|
31
34
|
export function createBrowserTool() {
|
|
32
35
|
return {
|
|
33
|
-
name:
|
|
34
|
-
tags: [
|
|
36
|
+
name: "browser",
|
|
37
|
+
tags: ["browser", "vision"],
|
|
35
38
|
description: [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
"Control a headless browser. Navigate pages, click elements, type text, scroll, take screenshots.",
|
|
40
|
+
"Runs a real browser with JavaScript enabled — use it for JS-rendered sites (weather, dashboards, SPAs) where web_fetch/web_browse return empty content.",
|
|
41
|
+
"Do NOT use it for search queries: search engines (Google, Yandex, Bing) block headless browsers with CAPTCHA/429/redirect pages — use the web_search tool for searching.",
|
|
42
|
+
"Each snapshot returns: a numbered list of interactive elements, the visible page text (Content section), browser console messages, and network errors.",
|
|
43
|
+
"Use the Content section to understand what the page says; use element numbers as targets for click/type.",
|
|
44
|
+
"Actions: open (url), click (target), type (target, text), scroll (direction), back, forward, screenshot, snapshot, close, wait (ms).",
|
|
45
|
+
].join(" "),
|
|
40
46
|
parameters: {
|
|
41
|
-
type:
|
|
47
|
+
type: "object",
|
|
42
48
|
properties: {
|
|
43
49
|
action: {
|
|
44
|
-
type:
|
|
45
|
-
enum: [
|
|
46
|
-
|
|
50
|
+
type: "string",
|
|
51
|
+
enum: [
|
|
52
|
+
"open",
|
|
53
|
+
"click",
|
|
54
|
+
"type",
|
|
55
|
+
"scroll",
|
|
56
|
+
"back",
|
|
57
|
+
"forward",
|
|
58
|
+
"screenshot",
|
|
59
|
+
"snapshot",
|
|
60
|
+
"close",
|
|
61
|
+
"wait",
|
|
62
|
+
],
|
|
63
|
+
description: "Browser action to perform",
|
|
47
64
|
},
|
|
48
65
|
url: {
|
|
49
|
-
type:
|
|
66
|
+
type: "string",
|
|
50
67
|
description: 'URL to open (for action "open")',
|
|
51
68
|
},
|
|
52
69
|
target: {
|
|
53
|
-
type:
|
|
70
|
+
type: "number",
|
|
54
71
|
description: 'Element number from snapshot (for action "click" or "type")',
|
|
55
72
|
},
|
|
56
73
|
text: {
|
|
57
|
-
type:
|
|
74
|
+
type: "string",
|
|
58
75
|
description: 'Text to type (for action "type")',
|
|
59
76
|
},
|
|
60
77
|
direction: {
|
|
61
|
-
type:
|
|
62
|
-
enum: [
|
|
78
|
+
type: "string",
|
|
79
|
+
enum: ["up", "down", "top", "bottom"],
|
|
63
80
|
description: 'Scroll direction (for action "scroll", default "down")',
|
|
64
81
|
},
|
|
65
82
|
ms: {
|
|
66
|
-
type:
|
|
83
|
+
type: "number",
|
|
67
84
|
description: 'Milliseconds to wait (for action "wait", default 1000)',
|
|
68
85
|
},
|
|
69
86
|
},
|
|
70
|
-
required: [
|
|
87
|
+
required: ["action"],
|
|
71
88
|
},
|
|
72
89
|
handler: async (ctx, args) => {
|
|
73
|
-
const action = String(args.action ||
|
|
90
|
+
const action = String(args.action || "");
|
|
74
91
|
if (!action) {
|
|
75
|
-
return { success: false, output: t(
|
|
92
|
+
return { success: false, output: t("tool.action_required") };
|
|
76
93
|
}
|
|
77
94
|
const s = getSession(ctx);
|
|
78
95
|
const result = await s.execute(action, args);
|
|
79
|
-
const supportsVision = ctx.config.model.includes(
|
|
80
|
-
ctx.config.model.includes(
|
|
81
|
-
ctx.config.model.includes(
|
|
96
|
+
const supportsVision = ctx.config.model.includes("vision") ||
|
|
97
|
+
ctx.config.model.includes("gpt-4o") ||
|
|
98
|
+
ctx.config.model.includes("claude");
|
|
82
99
|
if (result.screenshot) {
|
|
83
100
|
return {
|
|
84
101
|
success: result.success,
|