micro-models-agent 0.45.0 → 0.46.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/README.md +312 -312
- package/dist/cli/commands.js +323 -0
- package/dist/cli/completer.js +167 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/main.js +165 -0
- package/dist/cli/plugin-commands.js +36 -0
- package/dist/cli/repl-commands.js +661 -0
- package/dist/cli/repl.js +616 -0
- package/dist/cli/run-result.js +22 -0
- package/dist/cli/security-commands.js +164 -0
- package/dist/cli/setup.js +231 -0
- package/dist/config/config.js +249 -0
- package/dist/config/defaults.js +124 -0
- package/dist/config/experts.js +15 -0
- package/dist/config/index.js +3 -0
- package/dist/config/security.js +193 -0
- package/dist/config/types.js +1 -0
- package/dist/core/agent-moe.js +102 -0
- package/dist/core/agent.js +886 -0
- package/dist/core/bootstrap.js +404 -0
- package/dist/core/index.js +2 -0
- package/dist/core/prompt-builder.js +76 -0
- package/dist/core/session-logger.js +197 -0
- package/dist/core/types.js +1 -0
- package/dist/core/version.js +24 -0
- package/dist/core/workspace.js +76 -0
- package/dist/i18n/en.json +598 -0
- package/dist/i18n/index.js +46 -0
- package/dist/i18n/ru.json +598 -0
- package/dist/index.js +22 -0
- package/dist/llm/image-utils.js +143 -0
- package/dist/llm/index.js +4 -0
- package/dist/llm/model-loader.js +78 -0
- package/dist/llm/openai-compat.js +359 -0
- package/dist/llm/orchestrator.js +198 -0
- package/dist/llm/provider.js +10 -0
- package/dist/llm/response.js +39 -0
- package/dist/llm/token-counter.js +39 -0
- package/dist/llm/types.js +1 -0
- package/dist/logger/app-logger.js +143 -0
- package/dist/logger/file-log.js +151 -0
- package/dist/logger/index.js +1 -0
- package/dist/main.js +690 -168
- package/dist/migration/backup.js +45 -0
- package/dist/migration/detect.js +50 -0
- package/dist/migration/index.js +2 -0
- package/dist/modules/artifacts/store.js +61 -0
- package/dist/modules/browser/actions.js +76 -0
- 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 +24 -0
- package/dist/modules/browser/driver.js +136 -0
- package/dist/modules/browser/index.js +7 -0
- package/dist/modules/browser/module.js +29 -0
- package/dist/modules/browser/session.js +338 -0
- package/dist/modules/browser/snapshot.js +148 -0
- package/dist/modules/browser/types.js +12 -0
- package/dist/modules/certification/cli.js +174 -0
- package/dist/modules/certification/fact-checker.js +82 -0
- package/dist/modules/certification/loader.js +105 -0
- package/dist/modules/certification/manifest.js +50 -0
- package/dist/modules/certification/runner.js +159 -0
- package/dist/modules/certification/scenarios.js +124 -0
- package/dist/modules/certification/types.js +1 -0
- 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 -0
- package/dist/modules/context/manager.js +423 -0
- package/dist/modules/execution/audit-runners.js +152 -0
- package/dist/modules/execution/auditor.js +218 -0
- package/dist/modules/execution/execution-plugin.js +272 -0
- package/dist/modules/execution/index.js +8 -0
- package/dist/modules/execution/module.js +436 -0
- package/dist/modules/execution/moe-executor.js +291 -0
- package/dist/modules/execution/plan-coverage.js +68 -0
- package/dist/modules/execution/plan-persister.js +46 -0
- package/dist/modules/execution/plan-store.js +157 -0
- package/dist/modules/execution/plan-tool.js +508 -0
- package/dist/modules/execution/plan-validator.js +153 -0
- package/dist/modules/execution/planner.js +90 -0
- package/dist/modules/execution/stuck-detector.js +510 -0
- package/dist/modules/execution/tracker.js +67 -0
- package/dist/modules/execution/types.js +1 -0
- package/dist/modules/execution/verifier.js +222 -0
- package/dist/modules/execution/windows-commands.js +41 -0
- package/dist/modules/hallucination/confidence.js +66 -0
- package/dist/modules/hallucination/consistency.js +26 -0
- package/dist/modules/hallucination/detector.js +43 -0
- package/dist/modules/hallucination/factual.js +129 -0
- package/dist/modules/hallucination/index.js +5 -0
- package/dist/modules/hallucination/js-identifiers.js +262 -0
- package/dist/modules/hallucination/llm-judge.js +101 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/indexer/cache.js +40 -0
- package/dist/modules/indexer/index.js +3 -0
- package/dist/modules/indexer/module.js +245 -0
- package/dist/modules/indexer/project-profile.js +183 -0
- package/dist/modules/indexer/walker.js +101 -0
- package/dist/modules/lsp/check-tool.js +58 -0
- package/dist/modules/lsp/client.js +278 -0
- package/dist/modules/lsp/command.js +60 -0
- package/dist/modules/lsp/config.js +135 -0
- package/dist/modules/lsp/index.js +3 -0
- package/dist/modules/lsp/module.js +232 -0
- 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/lsp/types.js +1 -0
- package/dist/modules/mcp/client.js +399 -0
- package/dist/modules/mcp/index.js +3 -0
- package/dist/modules/mcp/module.js +142 -0
- package/dist/modules/mcp/registry.js +15 -0
- package/dist/modules/memory/index.js +1 -0
- package/dist/modules/memory/module.js +96 -0
- package/dist/modules/memory/search.js +42 -0
- package/dist/modules/memory/store.js +69 -0
- package/dist/modules/pipelines/engine.js +60 -0
- package/dist/modules/pipelines/index.js +3 -0
- package/dist/modules/pipelines/parser.js +56 -0
- package/dist/modules/pipelines/template.js +14 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +231 -0
- package/dist/modules/plugins/builtin/notify.js +9 -0
- package/dist/modules/plugins/index.js +1 -0
- package/dist/modules/plugins/loader.js +70 -0
- package/dist/modules/plugins/manager.js +217 -0
- package/dist/modules/plugins/types.js +1 -0
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +2 -0
- package/dist/modules/processes/registry.js +327 -0
- package/dist/modules/processes/runner.js +23 -0
- package/dist/modules/registry.js +47 -0
- package/dist/modules/security/audit-log.js +136 -0
- package/dist/modules/security/audit-notifier.js +292 -0
- package/dist/modules/security/command-validator.js +205 -0
- package/dist/modules/security/content-scanner.js +53 -0
- package/dist/modules/security/data-sanitizer.js +89 -0
- package/dist/modules/security/encryption.js +242 -0
- package/dist/modules/security/index.js +14 -0
- package/dist/modules/security/network-validator.js +71 -0
- package/dist/modules/security/path-validator.js +207 -0
- package/dist/modules/security/rate-limiter.js +119 -0
- package/dist/modules/security/security-policies.js +531 -0
- package/dist/modules/security/session-encryption.js +210 -0
- package/dist/modules/security/session-isolation.js +95 -0
- package/dist/modules/session/index.js +3 -0
- package/dist/modules/session/manager.js +172 -0
- package/dist/modules/session/module.js +24 -0
- package/dist/modules/session/store.js +222 -0
- package/dist/modules/session/types.js +1 -0
- package/dist/modules/skills/index.js +2 -0
- package/dist/modules/skills/loader.js +72 -0
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +129 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/updater/checker.js +96 -0
- package/dist/modules/updater/index.js +2 -0
- package/dist/modules/updater/module.js +116 -0
- package/dist/modules/user-profile/compressor.js +16 -0
- package/dist/modules/user-profile/index.js +1 -0
- package/dist/modules/user-profile/profile.js +68 -0
- package/dist/skills/builtin/git.md +36 -36
- package/dist/skills/builtin/typescript.md +35 -35
- package/dist/tools/approve.js +32 -0
- package/dist/tools/attach-image.js +89 -0
- package/dist/tools/bash.js +496 -0
- package/dist/tools/browser.js +114 -0
- package/dist/tools/chunk-query.js +99 -0
- package/dist/tools/create-dir.js +55 -0
- package/dist/tools/delete-file.js +62 -0
- package/dist/tools/download-file.js +116 -0
- package/dist/tools/edit-file.js +79 -0
- package/dist/tools/enable-tools.js +58 -0
- package/dist/tools/executor.js +144 -0
- package/dist/tools/file-info.js +46 -0
- package/dist/tools/filter-tools.js +17 -0
- package/dist/tools/glob-tool.js +26 -0
- package/dist/tools/grep-tool.js +84 -0
- package/dist/tools/hidden-tools-block.js +37 -0
- package/dist/tools/index.js +78 -0
- package/dist/tools/list-dir.js +48 -0
- package/dist/tools/load-skill.js +42 -0
- package/dist/tools/mcp-call.js +68 -0
- package/dist/tools/move-file.js +85 -0
- package/dist/tools/path-utils.js +51 -0
- package/dist/tools/pipeline-run.js +144 -0
- package/dist/tools/preview.js +2 -0
- package/dist/tools/process-kill.js +29 -0
- package/dist/tools/process-list.js +36 -0
- package/dist/tools/process-log.js +45 -0
- package/dist/tools/question.js +140 -0
- package/dist/tools/read-file.js +91 -0
- package/dist/tools/recall.js +117 -0
- package/dist/tools/registry.js +47 -0
- package/dist/tools/remember.js +67 -0
- package/dist/tools/scope-check.js +30 -0
- package/dist/tools/search-history.js +84 -0
- package/dist/tools/subagent.js +196 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/user-input.js +123 -0
- package/dist/tools/web-browse.js +86 -0
- package/dist/tools/web-fetch.js +98 -0
- package/dist/tools/web-search.js +78 -0
- package/dist/tools/write-file.js +81 -0
- package/dist/ui/box.js +77 -0
- package/dist/ui/colors.js +4 -0
- package/dist/ui/diff.js +178 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/line-editor.js +703 -0
- package/dist/ui/line-math.js +69 -0
- package/dist/ui/md-formatter.js +212 -0
- package/dist/ui/output.js +13 -0
- package/dist/ui/plan-view.js +103 -0
- package/dist/ui/renderer.js +209 -0
- package/dist/ui/spinner.js +70 -0
- package/dist/ui/table.js +144 -0
- package/package.json +48 -48
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { isCommandAllowed, sanitizeCommandForLog } from "../modules/security/command-validator";
|
|
2
|
+
import { logBashCommand, logSecurityBlock } from "../modules/security/audit-log";
|
|
3
|
+
import { getSessionSecurityConfig } from "../modules/security/session-isolation";
|
|
4
|
+
import { DEFAULT_SECURITY_CONFIG } from "../config/security";
|
|
5
|
+
import { processRegistry, registerKillable, unregisterKillable } from "../modules/processes";
|
|
6
|
+
import { t } from "../i18n/index";
|
|
7
|
+
import { platform } from "os";
|
|
8
|
+
import { MAX_PREVIEW_LINES } from "./preview";
|
|
9
|
+
/**
|
|
10
|
+
* A command still running after this window is promoted to the background.
|
|
11
|
+
* The decision is based on process *behavior* (still alive), not on matching
|
|
12
|
+
* words in the command text — a command that happens to mention "vite",
|
|
13
|
+
* "server", etc. runs normally, and any genuinely long-running command is
|
|
14
|
+
* caught regardless of how it is written.
|
|
15
|
+
*/
|
|
16
|
+
export const BASH_GRACE_MS = 5000;
|
|
17
|
+
/** Short window for explicit `background: true` — surfaces immediate spawn failures (bad cwd, missing shell). */
|
|
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;
|
|
21
|
+
let bashGraceMs = BASH_GRACE_MS;
|
|
22
|
+
/** Test hook: override the auto-background grace window. */
|
|
23
|
+
export function setBashGraceMs(ms) {
|
|
24
|
+
bashGraceMs = ms;
|
|
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
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Detect a file write via `echo/printf ... > file` — common model habit that
|
|
41
|
+
* breaks in cmd.exe: single quotes are not grouping quotes, `>` only applies
|
|
42
|
+
* to the LAST line of a multi-line command, and double quotes inside the text
|
|
43
|
+
* split the command. Returns the target filename or null.
|
|
44
|
+
*/
|
|
45
|
+
export function extractEchoFileWrite(command) {
|
|
46
|
+
if (!/^\s*(?:echo|printf)\b/i.test(command))
|
|
47
|
+
return null;
|
|
48
|
+
const m = command.match(/[>»]{1,2}\s*"?([^"'\s&|]+)"?/i);
|
|
49
|
+
if (!m)
|
|
50
|
+
return null;
|
|
51
|
+
return m[1].replace(/["'']$/g, "");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* cmd.exe uses `&` as the command separator, not `;` (bash). The model
|
|
55
|
+
* regularly chains commands with `;` — without translation cmd passes the
|
|
56
|
+
* `;` to the first command as an argument (e.g. `node x.ts;` → ENOENT for
|
|
57
|
+
* "x.ts;"). Replace `;` with `&` only OUTSIDE double-quoted strings so
|
|
58
|
+
* `echo "a;b"` stays intact. (Single quotes are not special in cmd.)
|
|
59
|
+
*/
|
|
60
|
+
export function translateSemicolonsForCmd(command) {
|
|
61
|
+
let out = "";
|
|
62
|
+
let inQuotes = false;
|
|
63
|
+
for (let i = 0; i < command.length; i++) {
|
|
64
|
+
const ch = command[i];
|
|
65
|
+
if (ch === '"') {
|
|
66
|
+
inQuotes = !inQuotes;
|
|
67
|
+
out += ch;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
out += ch === ";" && !inQuotes ? "&" : ch;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function adaptCommandForWindows(command) {
|
|
75
|
+
if (platform() !== "win32")
|
|
76
|
+
return command;
|
|
77
|
+
// The model sometimes appends `|| true` (bash error-suppression idiom)
|
|
78
|
+
// which PowerShell doesn't understand. Replace with `; exit 0` which
|
|
79
|
+
// forces a successful exit code regardless of the previous command's result.
|
|
80
|
+
if (/\|\|\s*true\b/.test(command)) {
|
|
81
|
+
command = command.replace(/\s*\|\|\s*true\b/g, "; exit 0");
|
|
82
|
+
}
|
|
83
|
+
// Windows mkdir does not support -p flag, but creates intermediate dirs by default
|
|
84
|
+
const trimmed = command.trim();
|
|
85
|
+
if (trimmed.startsWith("mkdir -p ")) {
|
|
86
|
+
return trimmed.replace(/^mkdir -p /, "mkdir ");
|
|
87
|
+
}
|
|
88
|
+
if (trimmed === "mkdir -p" || trimmed.startsWith("mkdir -p ")) {
|
|
89
|
+
return trimmed.replace(/mkdir -p/g, "mkdir");
|
|
90
|
+
}
|
|
91
|
+
// Translate simple Unix commands to their cmd.exe equivalents. Only the
|
|
92
|
+
// leading word is rewritten; flags are passed through (ls -la → dir -la,
|
|
93
|
+
// which cmd tolerates). Pipe-using forms are left alone — they would break.
|
|
94
|
+
const firstWord = trimmed.split(/\s+/)[0]?.split(/[\\/]/).pop();
|
|
95
|
+
const translated = firstWord ? UNIX_TO_WIN_TRANSLATE[firstWord] : undefined;
|
|
96
|
+
if (translated &&
|
|
97
|
+
!trimmed.includes("|") &&
|
|
98
|
+
!trimmed.includes(">") &&
|
|
99
|
+
!trimmed.includes("&&") &&
|
|
100
|
+
!trimmed.includes(";")) {
|
|
101
|
+
return trimmed.replace(firstWord, translated);
|
|
102
|
+
}
|
|
103
|
+
// No encoding adaptation needed — registry.ts handles UTF-8/OEM decoding
|
|
104
|
+
return translateSemicolonsForCmd(command);
|
|
105
|
+
}
|
|
106
|
+
/** Common Unix → Windows command mapping for error hints. */
|
|
107
|
+
const UNIX_TO_WIN_HINTS = {
|
|
108
|
+
ls: "Use the list_dir tool instead.",
|
|
109
|
+
pwd: "Use the file_info tool instead.",
|
|
110
|
+
cat: "Use the read_file tool instead.",
|
|
111
|
+
cp: "Use the move_file tool instead.",
|
|
112
|
+
mv: "Use the move_file tool instead.",
|
|
113
|
+
rm: "Use the delete_file tool instead.",
|
|
114
|
+
grep: "Use the grep tool instead.",
|
|
115
|
+
chmod: "Use the chmod tool instead.",
|
|
116
|
+
touch: "Use the write_file tool instead.",
|
|
117
|
+
find: "Use the glob tool instead.",
|
|
118
|
+
head: "Use the read_file tool with offset/limit instead.",
|
|
119
|
+
tail: "Use the read_file tool instead.",
|
|
120
|
+
wc: "Use the read_file tool instead.",
|
|
121
|
+
diff: "Use the diff tool instead.",
|
|
122
|
+
which: 'Use "where" instead.',
|
|
123
|
+
echo: "echo works on Windows, but avoid pipes (|).",
|
|
124
|
+
"Get-Content": "Use the read_file tool instead.",
|
|
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.",
|
|
132
|
+
};
|
|
133
|
+
/** Unix commands that can be transparently translated to cmd.exe equivalents. */
|
|
134
|
+
const UNIX_TO_WIN_TRANSLATE = {
|
|
135
|
+
ls: "dir",
|
|
136
|
+
pwd: "cd",
|
|
137
|
+
cat: "type",
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Detect when the model mistakes a tool call for a shell command — e.g.
|
|
141
|
+
* `bash` with command "create_dir path=C:\...\src" or "read_file file=x".
|
|
142
|
+
* These are tool invocations, not commands; running them through the shell
|
|
143
|
+
* fails. The matched tool name and raw args are returned so the caller can
|
|
144
|
+
* redirect into the real tool.
|
|
145
|
+
*/
|
|
146
|
+
/**
|
|
147
|
+
* Shell commands that must NEVER be treated as mistaken tool calls. The
|
|
148
|
+
* model often writes files via `echo '<code with = signs>'` or reads them
|
|
149
|
+
* via `cat` — those are shell commands, not tool invocations. Without this
|
|
150
|
+
* guard the redirect heuristic fires on any `<word> <text containing =>`
|
|
151
|
+
* and fails with "Unknown tool: echo".
|
|
152
|
+
*/
|
|
153
|
+
const NEVER_TOOL_CALLS = new Set([
|
|
154
|
+
"echo",
|
|
155
|
+
"cat",
|
|
156
|
+
"type",
|
|
157
|
+
"printf",
|
|
158
|
+
"touch",
|
|
159
|
+
"mkdir",
|
|
160
|
+
"cp",
|
|
161
|
+
"mv",
|
|
162
|
+
"rm",
|
|
163
|
+
"ls",
|
|
164
|
+
"dir",
|
|
165
|
+
"cd",
|
|
166
|
+
"pwd",
|
|
167
|
+
"grep",
|
|
168
|
+
"find",
|
|
169
|
+
"head",
|
|
170
|
+
"tail",
|
|
171
|
+
"wc",
|
|
172
|
+
"chmod",
|
|
173
|
+
"sed",
|
|
174
|
+
"awk",
|
|
175
|
+
]);
|
|
176
|
+
export function detectToolCallInBash(command) {
|
|
177
|
+
const match = command.trim().match(/^([\w-]+)\s+(.+)$/s);
|
|
178
|
+
if (!match)
|
|
179
|
+
return null;
|
|
180
|
+
const tool = match[1];
|
|
181
|
+
// Real shell commands (echo, cat, ...) are never mistaken tool calls.
|
|
182
|
+
if (NEVER_TOOL_CALLS.has(tool))
|
|
183
|
+
return null;
|
|
184
|
+
const rest = match[2].trim();
|
|
185
|
+
// Only treat as a tool call when the first word looks like a snake_case
|
|
186
|
+
// tool name and the rest has at least one '=' or a JSON object shape.
|
|
187
|
+
if (!/^[a-z][a-z0-9_]+$/.test(tool))
|
|
188
|
+
return null;
|
|
189
|
+
if (!rest.includes("=") && !rest.startsWith("{"))
|
|
190
|
+
return null;
|
|
191
|
+
return { tool, args: rest };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Parse the raw argument string of a mistaken tool call captured in a bash
|
|
195
|
+
* command into a Record. Supports JSON objects ("{"path": "..."}") and
|
|
196
|
+
* key=value pairs ("path=C:\...\src"). Values keep their literal text.
|
|
197
|
+
*/
|
|
198
|
+
export function parseToolArgs(raw) {
|
|
199
|
+
const trimmed = raw.trim();
|
|
200
|
+
if (trimmed.startsWith("{")) {
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(trimmed);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* fall through to key=value parsing */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const args = {};
|
|
209
|
+
// Tokenize respecting double/single-quoted values.
|
|
210
|
+
const tokens = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
211
|
+
for (const token of tokens) {
|
|
212
|
+
const eq = token.indexOf("=");
|
|
213
|
+
if (eq > 0) {
|
|
214
|
+
const key = token.slice(0, eq);
|
|
215
|
+
const value = token.slice(eq + 1);
|
|
216
|
+
// Strip surrounding quotes from values.
|
|
217
|
+
args[key] = value.replace(/^["']|["']$/g, "");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return args;
|
|
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
|
+
}
|
|
307
|
+
export const bashTool = {
|
|
308
|
+
name: "bash",
|
|
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 &).",
|
|
310
|
+
tags: ["shell", "code"],
|
|
311
|
+
parameters: {
|
|
312
|
+
type: "object",
|
|
313
|
+
properties: {
|
|
314
|
+
command: { type: "string", description: "Shell command to execute" },
|
|
315
|
+
workdir: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "Working directory (default: baseDir)",
|
|
318
|
+
},
|
|
319
|
+
background: {
|
|
320
|
+
type: "boolean",
|
|
321
|
+
description: "Return a process id immediately without waiting (default: commands still running after a few seconds are auto-promoted to the background)",
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
required: ["command"],
|
|
325
|
+
},
|
|
326
|
+
handler: async (ctx, args) => {
|
|
327
|
+
const originalCommand = String(args.command);
|
|
328
|
+
// The model sometimes sends a tool invocation (e.g. "create_dir path=...")
|
|
329
|
+
// as a bash command instead of calling the tool directly. Redirect into
|
|
330
|
+
// the real tool so the intent succeeds instead of failing in the shell.
|
|
331
|
+
const toolCall = detectToolCallInBash(originalCommand);
|
|
332
|
+
if (toolCall &&
|
|
333
|
+
toolCall.tool !== "bash" &&
|
|
334
|
+
ctx.toolExecutor &&
|
|
335
|
+
ctx.toolExecutor.hasTool(toolCall.tool)) {
|
|
336
|
+
const redirected = await ctx.toolExecutor.executeByName(toolCall.tool, parseToolArgs(toolCall.args), ctx);
|
|
337
|
+
return {
|
|
338
|
+
success: redirected.success,
|
|
339
|
+
output: `[redirected to tool "${toolCall.tool}"]\n${redirected.output}`,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const command = adaptCommandForWindows(originalCommand);
|
|
343
|
+
// echo/printf redirection to a file is unreliable in cmd.exe (single
|
|
344
|
+
// quotes don't group, multi-line commands split, embedded double quotes
|
|
345
|
+
// break the command). Steer the model to write_file instead — it
|
|
346
|
+
// produces correct files every time.
|
|
347
|
+
if (platform() === "win32") {
|
|
348
|
+
const echoWrite = extractEchoFileWrite(originalCommand);
|
|
349
|
+
if (echoWrite) {
|
|
350
|
+
return {
|
|
351
|
+
success: false,
|
|
352
|
+
output: t("bash.echo_write_blocked", { path: echoWrite }),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const workdir = args.workdir ? String(args.workdir) : ctx.baseDir;
|
|
357
|
+
// Get session-specific security config with defaults
|
|
358
|
+
const appConfig = ctx.config || {};
|
|
359
|
+
const fullSecurityConfig = ctx.sessionContext
|
|
360
|
+
? getSessionSecurityConfig(appConfig, ctx.sessionContext)
|
|
361
|
+
: appConfig.security || DEFAULT_SECURITY_CONFIG;
|
|
362
|
+
const securityConfig = fullSecurityConfig.bash || DEFAULT_SECURITY_CONFIG.bash;
|
|
363
|
+
const validation = isCommandAllowed(command, securityConfig);
|
|
364
|
+
if (!validation.allowed) {
|
|
365
|
+
// Log security block
|
|
366
|
+
logSecurityBlock(ctx.sessionId, "bash_command", validation.reason || "Command blocked by security policy", sanitizeCommandForLog(originalCommand));
|
|
367
|
+
return {
|
|
368
|
+
success: false,
|
|
369
|
+
output: `[SECURITY BLOCKED] Command is not allowed: ${validation.reason}\nHint: Use the "workdir" parameter to run commands in a specific directory instead of "cd dir && cmd". Run one command per tool call.`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
// Log command execution if enabled
|
|
373
|
+
if (securityConfig?.logCommands) {
|
|
374
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, // Will be updated after execution
|
|
375
|
+
`Working directory: ${workdir}`);
|
|
376
|
+
}
|
|
377
|
+
const entry = processRegistry.start(command, workdir, ctx.sessionId);
|
|
378
|
+
if (ctx.activeCallId) {
|
|
379
|
+
// Let the executor abort/kill the child during the grace window.
|
|
380
|
+
registerKillable(ctx.activeCallId, () => processRegistry.kill(entry.id));
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
const settleMs = args.background === true ? SPAWN_SETTLE_MS : bashGraceMs;
|
|
384
|
+
const exited = await processRegistry.waitForExit(entry.id, settleMs);
|
|
385
|
+
if (exited) {
|
|
386
|
+
// Command finished (or failed to spawn) within the window.
|
|
387
|
+
if (entry.spawnError) {
|
|
388
|
+
return {
|
|
389
|
+
success: false,
|
|
390
|
+
output: `[process error] ${entry.spawnError}\nHint: check the "workdir" path exists and the command is valid for this OS.`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const code = entry.exitCode;
|
|
394
|
+
let output = entry.log.join("\n");
|
|
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
|
+
}
|
|
414
|
+
if (!output && code !== 0) {
|
|
415
|
+
output = `(exit code ${code})`;
|
|
416
|
+
}
|
|
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
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Update audit log with result
|
|
448
|
+
if (securityConfig?.logCommands) {
|
|
449
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), code === 0, `Working directory: ${workdir}, Output length: ${output.length}`);
|
|
450
|
+
}
|
|
451
|
+
const lines = output.split("\n");
|
|
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) {
|
|
464
|
+
output =
|
|
465
|
+
lines.slice(0, MAX_PREVIEW_LINES).join("\n") +
|
|
466
|
+
`\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
467
|
+
}
|
|
468
|
+
return { success: code === 0, output };
|
|
469
|
+
}
|
|
470
|
+
// Still running — promote to a background process.
|
|
471
|
+
const explicit = args.background === true;
|
|
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", {
|
|
477
|
+
id: entry.id,
|
|
478
|
+
pid: entry.pid,
|
|
479
|
+
command,
|
|
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 })}`;
|
|
485
|
+
if (securityConfig?.logCommands) {
|
|
486
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), true, `Started in background: ${entry.id} (PID ${entry.pid})`);
|
|
487
|
+
}
|
|
488
|
+
return { success: true, output };
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
if (ctx.activeCallId) {
|
|
492
|
+
unregisterKillable(ctx.activeCallId);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
let session = null;
|
|
6
|
+
function getSession(ctx) {
|
|
7
|
+
if (!session) {
|
|
8
|
+
const cookieDir = join(ctx.baseDir, ".mma", "browser");
|
|
9
|
+
session = new BrowserSession({
|
|
10
|
+
...DEFAULT_BROWSER_CONFIG,
|
|
11
|
+
headless: ctx.config.browser?.headless ?? true,
|
|
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,
|
|
16
|
+
navigationTimeout: ctx.config.browser?.navigationTimeout ?? 15000,
|
|
17
|
+
viewportWidth: ctx.config.browser?.viewportWidth ?? 1280,
|
|
18
|
+
viewportHeight: ctx.config.browser?.viewportHeight ?? 720,
|
|
19
|
+
cookieDir,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return session;
|
|
23
|
+
}
|
|
24
|
+
export function formatScreenshotForTextModel(snapshot) {
|
|
25
|
+
return `${t("tool.screenshot_unavailable")}\n\n${snapshot}`;
|
|
26
|
+
}
|
|
27
|
+
export function formatScreenshotResult(snapshot, screenshot, supportsVision) {
|
|
28
|
+
if (!supportsVision) {
|
|
29
|
+
return formatScreenshotForTextModel(snapshot);
|
|
30
|
+
}
|
|
31
|
+
const base64 = screenshot.toString("base64");
|
|
32
|
+
return `${snapshot}\n\n[Screenshot: data:image/png;base64,${base64}]`;
|
|
33
|
+
}
|
|
34
|
+
export function createBrowserTool() {
|
|
35
|
+
return {
|
|
36
|
+
name: "browser",
|
|
37
|
+
tags: ["browser", "vision"],
|
|
38
|
+
description: [
|
|
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(" "),
|
|
46
|
+
parameters: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
action: {
|
|
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",
|
|
64
|
+
},
|
|
65
|
+
url: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: 'URL to open (for action "open")',
|
|
68
|
+
},
|
|
69
|
+
target: {
|
|
70
|
+
type: "number",
|
|
71
|
+
description: 'Element number from snapshot (for action "click" or "type")',
|
|
72
|
+
},
|
|
73
|
+
text: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: 'Text to type (for action "type")',
|
|
76
|
+
},
|
|
77
|
+
direction: {
|
|
78
|
+
type: "string",
|
|
79
|
+
enum: ["up", "down", "top", "bottom"],
|
|
80
|
+
description: 'Scroll direction (for action "scroll", default "down")',
|
|
81
|
+
},
|
|
82
|
+
ms: {
|
|
83
|
+
type: "number",
|
|
84
|
+
description: 'Milliseconds to wait (for action "wait", default 1000)',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
required: ["action"],
|
|
88
|
+
},
|
|
89
|
+
handler: async (ctx, args) => {
|
|
90
|
+
const action = String(args.action || "");
|
|
91
|
+
if (!action) {
|
|
92
|
+
return { success: false, output: t("tool.action_required") };
|
|
93
|
+
}
|
|
94
|
+
const s = getSession(ctx);
|
|
95
|
+
const result = await s.execute(action, args);
|
|
96
|
+
const supportsVision = ctx.config.model.includes("vision") ||
|
|
97
|
+
ctx.config.model.includes("gpt-4o") ||
|
|
98
|
+
ctx.config.model.includes("claude");
|
|
99
|
+
if (result.screenshot) {
|
|
100
|
+
return {
|
|
101
|
+
success: result.success,
|
|
102
|
+
output: formatScreenshotResult(result.output, result.screenshot, supportsVision),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { success: result.success, output: result.output };
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function closeBrowserSession() {
|
|
110
|
+
if (session) {
|
|
111
|
+
session.close().catch(() => { });
|
|
112
|
+
session = null;
|
|
113
|
+
}
|
|
114
|
+
}
|