micro-models-agent 0.28.17 → 0.29.0
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/dist/cli/commands.js +3 -116
- package/dist/cli/main.js +8 -35
- package/dist/cli/repl.js +611 -110
- package/dist/cli/setup.js +12 -32
- package/dist/config/config.js +30 -46
- package/dist/config/defaults.js +1 -10
- package/dist/config/security.js +8 -15
- package/dist/core/agent-moe.js +12 -24
- package/dist/core/agent.js +47 -281
- package/dist/core/bootstrap.js +36 -52
- package/dist/core/session-logger.js +2 -35
- package/dist/i18n/en.json +15 -79
- package/dist/i18n/index.js +9 -12
- package/dist/i18n/ru.json +15 -79
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +10 -39
- package/dist/logger/app-logger.js +16 -83
- package/dist/main.js +624 -243
- package/dist/modules/browser/session.js +60 -108
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/manager.js +10 -119
- package/dist/modules/execution/auditor.js +39 -33
- package/dist/modules/execution/index.js +6 -8
- package/dist/modules/execution/module.js +32 -474
- package/dist/modules/execution/moe-executor.js +40 -97
- package/dist/modules/execution/planner.js +13 -63
- package/dist/modules/execution/stuck-detector.js +39 -252
- package/dist/modules/execution/tracker.js +7 -21
- package/dist/modules/execution/verifier.js +17 -46
- package/dist/modules/hallucination/confidence.js +2 -7
- package/dist/modules/hallucination/consistency.js +42 -8
- package/dist/modules/hallucination/detector.js +21 -26
- package/dist/modules/hallucination/factual.js +150 -170
- package/dist/modules/hallucination/index.js +4 -5
- package/dist/modules/index.js +5 -5
- package/dist/modules/mcp/client.js +2 -8
- package/dist/modules/memory/store.js +0 -4
- package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +2 -1
- package/dist/modules/processes/registry.js +35 -125
- package/dist/modules/processes/runner.js +110 -9
- package/dist/modules/security/audit-log.js +10 -30
- package/dist/modules/security/command-validator.js +16 -42
- package/dist/modules/security/content-scanner.js +8 -9
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +10 -64
- package/dist/modules/security/security-policies.js +67 -221
- package/dist/modules/security/session-encryption.js +25 -42
- package/dist/modules/session/manager.js +10 -15
- package/dist/modules/session/store.js +8 -62
- package/dist/modules/skills/index.js +3 -2
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +23 -10
- package/dist/tools/bash.js +90 -287
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/edit-file.js +8 -10
- package/dist/tools/executor.js +7 -57
- package/dist/tools/grep-tool.js +29 -51
- package/dist/tools/index.js +40 -55
- package/dist/tools/load-skill.js +18 -14
- package/dist/tools/move-file.js +2 -3
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +5 -15
- package/dist/tools/search-history.js +22 -42
- package/dist/tools/subagent.js +12 -21
- package/dist/tools/web-browse.js +25 -54
- package/dist/tools/web-fetch.js +34 -60
- package/dist/tools/web-search.js +20 -39
- package/dist/tools/write-file.js +10 -13
- package/dist/ui/diff.js +16 -9
- package/dist/ui/renderer.js +6 -69
- package/package.json +1 -1
- package/dist/cli/repl-commands.js +0 -633
- package/dist/core/workspace.js +0 -76
- package/dist/logger/file-log.js +0 -151
- package/dist/modules/certification/cli.js +0 -176
- package/dist/modules/certification/fact-checker.js +0 -84
- package/dist/modules/certification/loader.js +0 -111
- package/dist/modules/certification/manifest.js +0 -50
- package/dist/modules/certification/runner.js +0 -162
- package/dist/modules/certification/scenarios.js +0 -124
- package/dist/modules/certification/types.js +0 -1
- package/dist/modules/execution/plan-coverage.js +0 -68
- package/dist/modules/execution/plan-persister.js +0 -46
- package/dist/modules/execution/plan-store.js +0 -159
- package/dist/modules/hallucination/js-identifiers.js +0 -72
- package/dist/modules/hallucination/llm-judge.js +0 -103
- package/dist/modules/lsp/client.js +0 -235
- package/dist/modules/lsp/config.js +0 -81
- package/dist/modules/lsp/index.js +0 -3
- package/dist/modules/lsp/module.js +0 -68
- package/dist/modules/lsp/types.js +0 -1
|
@@ -5,17 +5,12 @@ import { SessionFileEncryptor } from "../security/session-encryption";
|
|
|
5
5
|
export class SessionStore {
|
|
6
6
|
baseDir;
|
|
7
7
|
encryptor = null;
|
|
8
|
-
// In-memory meta cache to avoid O(N²) disk reads on appendMessage.
|
|
9
|
-
_metaCache = new Map();
|
|
10
8
|
constructor(baseDir, encryptionConfig) {
|
|
11
9
|
this.baseDir = baseDir;
|
|
12
10
|
if (encryptionConfig?.enabled) {
|
|
13
11
|
this.encryptor = new SessionFileEncryptor(encryptionConfig);
|
|
14
12
|
}
|
|
15
13
|
}
|
|
16
|
-
getSessionDir(id) {
|
|
17
|
-
return join(this.baseDir, id);
|
|
18
|
-
}
|
|
19
14
|
/**
|
|
20
15
|
* Update encryption configuration
|
|
21
16
|
*/
|
|
@@ -52,7 +47,6 @@ export class SessionStore {
|
|
|
52
47
|
return existsSync(this.metaPath(id));
|
|
53
48
|
}
|
|
54
49
|
saveMeta(id, meta) {
|
|
55
|
-
this._metaCache.set(id, meta);
|
|
56
50
|
const dir = this.sessionDir(id);
|
|
57
51
|
mkdirSync(dir, { recursive: true });
|
|
58
52
|
const content = JSON.stringify(meta, null, 2);
|
|
@@ -64,20 +58,13 @@ export class SessionStore {
|
|
|
64
58
|
}
|
|
65
59
|
}
|
|
66
60
|
loadMeta(id) {
|
|
67
|
-
const cached = this._metaCache.get(id);
|
|
68
|
-
if (cached)
|
|
69
|
-
return cached;
|
|
70
61
|
const path = this.metaPath(id);
|
|
71
62
|
if (!existsSync(path))
|
|
72
63
|
return null;
|
|
73
64
|
try {
|
|
74
65
|
const raw = readFileSync(path, "utf-8");
|
|
75
|
-
const content = this.encryptor
|
|
76
|
-
|
|
77
|
-
: raw;
|
|
78
|
-
const meta = JSON.parse(content);
|
|
79
|
-
this._metaCache.set(id, meta);
|
|
80
|
-
return meta;
|
|
66
|
+
const content = this.encryptor ? this.encryptor.decryptFileContent(raw) : raw;
|
|
67
|
+
return JSON.parse(content);
|
|
81
68
|
}
|
|
82
69
|
catch {
|
|
83
70
|
return null;
|
|
@@ -107,31 +94,11 @@ export class SessionStore {
|
|
|
107
94
|
try {
|
|
108
95
|
const raw = readFileSync(path, "utf-8");
|
|
109
96
|
const lines = raw.split("\n").filter(Boolean);
|
|
110
|
-
const parseLine = (line) => {
|
|
111
|
-
try {
|
|
112
|
-
return JSON.parse(line);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
97
|
if (this.encryptor?.isEnabled()) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
return this.encryptor.decryptFileContent(line);
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
.filter((l) => l !== null)
|
|
129
|
-
.map(parseLine)
|
|
130
|
-
.filter((m) => m !== null);
|
|
98
|
+
const decryptedLines = lines.map(line => this.encryptor.decryptFileContent(line));
|
|
99
|
+
return decryptedLines.map((line) => JSON.parse(line));
|
|
131
100
|
}
|
|
132
|
-
return lines
|
|
133
|
-
.map(parseLine)
|
|
134
|
-
.filter((m) => m !== null);
|
|
101
|
+
return lines.map((line) => JSON.parse(line));
|
|
135
102
|
}
|
|
136
103
|
catch {
|
|
137
104
|
return [];
|
|
@@ -155,31 +122,11 @@ export class SessionStore {
|
|
|
155
122
|
try {
|
|
156
123
|
const raw = readFileSync(path, "utf-8");
|
|
157
124
|
const lines = raw.split("\n").filter(Boolean);
|
|
158
|
-
const parseLine = (line) => {
|
|
159
|
-
try {
|
|
160
|
-
return JSON.parse(line);
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
125
|
if (this.encryptor?.isEnabled()) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
return this.encryptor.decryptFileContent(line);
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
})
|
|
176
|
-
.filter((l) => l !== null)
|
|
177
|
-
.map(parseLine)
|
|
178
|
-
.filter((e) => e !== null);
|
|
126
|
+
const decryptedLines = lines.map(line => this.encryptor.decryptFileContent(line));
|
|
127
|
+
return decryptedLines.map((line) => JSON.parse(line));
|
|
179
128
|
}
|
|
180
|
-
return lines
|
|
181
|
-
.map(parseLine)
|
|
182
|
-
.filter((e) => e !== null);
|
|
129
|
+
return lines.map((line) => JSON.parse(line));
|
|
183
130
|
}
|
|
184
131
|
catch {
|
|
185
132
|
return [];
|
|
@@ -201,7 +148,6 @@ export class SessionStore {
|
|
|
201
148
|
return sessions;
|
|
202
149
|
}
|
|
203
150
|
deleteSession(id) {
|
|
204
|
-
this._metaCache.delete(id);
|
|
205
151
|
const dir = this.sessionDir(id);
|
|
206
152
|
if (existsSync(dir)) {
|
|
207
153
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { SkillsLoader } from
|
|
2
|
-
export {
|
|
1
|
+
export { SkillsLoader } from './loader';
|
|
2
|
+
export { SkillsMatcher } from './matcher';
|
|
3
|
+
export { SkillsModule } from './module';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const MIN_WORD_LENGTH = 4;
|
|
2
|
+
export class SkillsMatcher {
|
|
3
|
+
match(taskDescription, skills, maxResults = 3) {
|
|
4
|
+
const taskWords = taskDescription.toLowerCase().split(/\W+/).filter(w => w.length >= MIN_WORD_LENGTH);
|
|
5
|
+
const scored = skills.map(skill => {
|
|
6
|
+
const allKeywords = [
|
|
7
|
+
skill.name.toLowerCase(),
|
|
8
|
+
skill.description.toLowerCase(),
|
|
9
|
+
...skill.keywords.map(k => k.toLowerCase()),
|
|
10
|
+
];
|
|
11
|
+
let score = 0;
|
|
12
|
+
for (const word of taskWords) {
|
|
13
|
+
for (const kw of allKeywords) {
|
|
14
|
+
if (kw === word || (kw.length >= MIN_WORD_LENGTH && kw.includes(word))) {
|
|
15
|
+
score++;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { skill, score };
|
|
20
|
+
});
|
|
21
|
+
return scored
|
|
22
|
+
.filter(s => s.score > 0)
|
|
23
|
+
.sort((a, b) => b.score - a.score)
|
|
24
|
+
.slice(0, maxResults)
|
|
25
|
+
.map(s => s.skill);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -3,10 +3,12 @@ export class SkillsModule {
|
|
|
3
3
|
name = "skills";
|
|
4
4
|
availableSkills;
|
|
5
5
|
loadedSkills = new Map();
|
|
6
|
+
matcher;
|
|
6
7
|
budget;
|
|
7
8
|
currentTokens = 0;
|
|
8
|
-
constructor(availableSkills, budget) {
|
|
9
|
+
constructor(availableSkills, matcher, budget) {
|
|
9
10
|
this.availableSkills = availableSkills;
|
|
11
|
+
this.matcher = matcher;
|
|
10
12
|
this.budget = budget;
|
|
11
13
|
}
|
|
12
14
|
loadByName(name) {
|
|
@@ -15,10 +17,24 @@ export class SkillsModule {
|
|
|
15
17
|
}
|
|
16
18
|
const skill = this.availableSkills.find((s) => s.name === name);
|
|
17
19
|
if (!skill) {
|
|
20
|
+
const fuzzyMatches = this.matcher.match(name, this.availableSkills, 1);
|
|
21
|
+
if (fuzzyMatches.length > 0) {
|
|
22
|
+
return this.tryLoad(fuzzyMatches[0]);
|
|
23
|
+
}
|
|
18
24
|
return { success: false, message: t("skill.not_found", { name }) };
|
|
19
25
|
}
|
|
20
26
|
return this.tryLoad(skill);
|
|
21
27
|
}
|
|
28
|
+
loadByMatch(taskDescription) {
|
|
29
|
+
const matches = this.matcher.match(taskDescription, this.availableSkills, 1);
|
|
30
|
+
if (matches.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
message: t("skill.no_match", { task: taskDescription }),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return this.tryLoad(matches[0]);
|
|
37
|
+
}
|
|
22
38
|
unload(name) {
|
|
23
39
|
const skill = this.loadedSkills.get(name);
|
|
24
40
|
if (!skill)
|
|
@@ -37,9 +53,7 @@ export class SkillsModule {
|
|
|
37
53
|
return this.availableSkills.find((s) => s.name === name);
|
|
38
54
|
}
|
|
39
55
|
search(query) {
|
|
40
|
-
|
|
41
|
-
return this.availableSkills.filter((s) => s.name.toLowerCase().includes(q) ||
|
|
42
|
-
s.description.toLowerCase().includes(q));
|
|
56
|
+
return this.matcher.match(query, this.availableSkills, 10);
|
|
43
57
|
}
|
|
44
58
|
getBudget() {
|
|
45
59
|
return {
|
|
@@ -58,17 +72,16 @@ export class SkillsModule {
|
|
|
58
72
|
lines.push("[Available Skills]");
|
|
59
73
|
lines.push(t("skill.prompt_hint"));
|
|
60
74
|
for (const skill of this.availableSkills) {
|
|
61
|
-
const desc = skill.description.slice(0,
|
|
75
|
+
const desc = skill.description.slice(0, 60);
|
|
62
76
|
lines.push(`- ${skill.name}: ${desc}`);
|
|
63
77
|
}
|
|
78
|
+
lines.push(t("skill.prompt_fallback"));
|
|
64
79
|
}
|
|
65
80
|
if (this.loadedSkills.size > 0) {
|
|
66
|
-
lines.push("");
|
|
67
81
|
lines.push("[Loaded Skills]");
|
|
68
82
|
for (const skill of this.loadedSkills.values()) {
|
|
69
|
-
|
|
70
|
-
lines.push(skill.
|
|
71
|
-
lines.push("");
|
|
83
|
+
const desc = skill.description.slice(0, 80);
|
|
84
|
+
lines.push(`- ${skill.name}: ${desc}`);
|
|
72
85
|
}
|
|
73
86
|
}
|
|
74
87
|
if (lines.length === 0)
|
|
@@ -77,7 +90,7 @@ export class SkillsModule {
|
|
|
77
90
|
const tokens = this.estimateTokens(content);
|
|
78
91
|
return {
|
|
79
92
|
content,
|
|
80
|
-
priority: "
|
|
93
|
+
priority: "normal",
|
|
81
94
|
essential: false,
|
|
82
95
|
estimatedTokens: tokens,
|
|
83
96
|
};
|
package/dist/tools/bash.js
CHANGED
|
@@ -1,252 +1,61 @@
|
|
|
1
|
-
import { isCommandAllowed, sanitizeCommandForLog
|
|
2
|
-
import { logBashCommand, logSecurityBlock
|
|
3
|
-
import { getSessionSecurityConfig } from
|
|
4
|
-
import { DEFAULT_SECURITY_CONFIG } from
|
|
5
|
-
import {
|
|
6
|
-
import { t } from
|
|
7
|
-
import { platform } from
|
|
8
|
-
import { MAX_PREVIEW_LINES } from
|
|
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
|
-
let bashGraceMs = BASH_GRACE_MS;
|
|
20
|
-
/** Test hook: override the auto-background grace window. */
|
|
21
|
-
export function setBashGraceMs(ms) {
|
|
22
|
-
bashGraceMs = ms;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Detect a file write via `echo/printf ... > file` — common model habit that
|
|
26
|
-
* breaks in cmd.exe: single quotes are not grouping quotes, `>` only applies
|
|
27
|
-
* to the LAST line of a multi-line command, and double quotes inside the text
|
|
28
|
-
* split the command. Returns the target filename or null.
|
|
29
|
-
*/
|
|
30
|
-
export function extractEchoFileWrite(command) {
|
|
31
|
-
if (!/^\s*(?:echo|printf)\b/i.test(command))
|
|
32
|
-
return null;
|
|
33
|
-
const m = command.match(/[>»]{1,2}\s*"?([^"'\s&|]+)"?/i);
|
|
34
|
-
if (!m)
|
|
35
|
-
return null;
|
|
36
|
-
return m[1].replace(/["'']$/g, "");
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* cmd.exe uses `&` as the command separator, not `;` (bash). The model
|
|
40
|
-
* regularly chains commands with `;` — without translation cmd passes the
|
|
41
|
-
* `;` to the first command as an argument (e.g. `node x.ts;` → ENOENT for
|
|
42
|
-
* "x.ts;"). Replace `;` with `&` only OUTSIDE double-quoted strings so
|
|
43
|
-
* `echo "a;b"` stays intact. (Single quotes are not special in cmd.)
|
|
44
|
-
*/
|
|
45
|
-
export function translateSemicolonsForCmd(command) {
|
|
46
|
-
let out = "";
|
|
47
|
-
let inQuotes = false;
|
|
48
|
-
for (let i = 0; i < command.length; i++) {
|
|
49
|
-
const ch = command[i];
|
|
50
|
-
if (ch === '"') {
|
|
51
|
-
inQuotes = !inQuotes;
|
|
52
|
-
out += ch;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
out += ch === ";" && !inQuotes ? "&" : ch;
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
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 { runCommand, processRegistry, isLongRunningCommand } from '../modules/processes';
|
|
6
|
+
import { t } from '../i18n/index';
|
|
7
|
+
import { platform } from 'os';
|
|
8
|
+
import { MAX_PREVIEW_LINES } from './preview';
|
|
9
|
+
const BASH_TIMEOUT_MS = 120_000;
|
|
59
10
|
function adaptCommandForWindows(command) {
|
|
60
|
-
if (platform() !==
|
|
11
|
+
if (platform() !== 'win32')
|
|
61
12
|
return command;
|
|
62
|
-
// The model sometimes appends `|| true` (bash error-suppression idiom)
|
|
63
|
-
// which PowerShell doesn't understand. Replace with `; exit 0` which
|
|
64
|
-
// forces a successful exit code regardless of the previous command's result.
|
|
65
|
-
if (/\|\|\s*true\b/.test(command)) {
|
|
66
|
-
command = command.replace(/\s*\|\|\s*true\b/g, "; exit 0");
|
|
67
|
-
}
|
|
68
13
|
// Windows mkdir does not support -p flag, but creates intermediate dirs by default
|
|
69
14
|
const trimmed = command.trim();
|
|
70
|
-
if (trimmed.startsWith(
|
|
71
|
-
return trimmed.replace(/^mkdir -p /,
|
|
72
|
-
}
|
|
73
|
-
if (trimmed === "mkdir -p" || trimmed.startsWith("mkdir -p ")) {
|
|
74
|
-
return trimmed.replace(/mkdir -p/g, "mkdir");
|
|
15
|
+
if (trimmed.startsWith('mkdir -p ')) {
|
|
16
|
+
return trimmed.replace(/^mkdir -p /, 'mkdir ');
|
|
75
17
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// which cmd tolerates). Pipe-using forms are left alone — they would break.
|
|
79
|
-
const firstWord = trimmed.split(/\s+/)[0]?.split(/[\\/]/).pop();
|
|
80
|
-
const translated = firstWord ? UNIX_TO_WIN_TRANSLATE[firstWord] : undefined;
|
|
81
|
-
if (translated &&
|
|
82
|
-
!trimmed.includes("|") &&
|
|
83
|
-
!trimmed.includes(">") &&
|
|
84
|
-
!trimmed.includes("&&") &&
|
|
85
|
-
!trimmed.includes(";")) {
|
|
86
|
-
return trimmed.replace(firstWord, translated);
|
|
18
|
+
if (trimmed === 'mkdir -p' || trimmed.startsWith('mkdir -p ')) {
|
|
19
|
+
return trimmed.replace(/mkdir -p/g, 'mkdir');
|
|
87
20
|
}
|
|
88
|
-
// No encoding adaptation needed —
|
|
89
|
-
return
|
|
21
|
+
// No encoding adaptation needed — runner.ts handles UTF-8 decoding
|
|
22
|
+
return command;
|
|
90
23
|
}
|
|
91
24
|
/** Common Unix → Windows command mapping for error hints. */
|
|
92
25
|
const UNIX_TO_WIN_HINTS = {
|
|
93
|
-
ls:
|
|
94
|
-
pwd:
|
|
95
|
-
cat:
|
|
96
|
-
cp:
|
|
97
|
-
mv:
|
|
98
|
-
rm:
|
|
99
|
-
grep:
|
|
100
|
-
chmod:
|
|
101
|
-
touch:
|
|
102
|
-
find:
|
|
103
|
-
head:
|
|
104
|
-
tail:
|
|
105
|
-
wc:
|
|
106
|
-
diff:
|
|
107
|
-
which: 'Use "where" instead.',
|
|
108
|
-
echo:
|
|
109
|
-
"Get-Content": "Use the read_file tool instead.",
|
|
110
|
-
"Select-Object": "Use the read_file tool with offset/limit instead.",
|
|
111
|
-
};
|
|
112
|
-
/** Unix commands that can be transparently translated to cmd.exe equivalents. */
|
|
113
|
-
const UNIX_TO_WIN_TRANSLATE = {
|
|
114
|
-
ls: "dir",
|
|
115
|
-
pwd: "cd",
|
|
116
|
-
cat: "type",
|
|
26
|
+
'ls': 'Use "dir" or the list_dir tool instead.',
|
|
27
|
+
'pwd': 'Use "echo %cd%" or the file_info tool instead.',
|
|
28
|
+
'cat': 'Use "type" or the read_file tool instead.',
|
|
29
|
+
'cp': 'Use "copy" or the move_file tool instead.',
|
|
30
|
+
'mv': 'Use "move" or the move_file tool instead.',
|
|
31
|
+
'rm': 'Use "del" or the delete_file tool instead.',
|
|
32
|
+
'grep': 'Use "findstr" or the grep tool instead.',
|
|
33
|
+
'chmod': 'Use icacls or the chmod tool instead.',
|
|
34
|
+
'touch': 'Use type nul > file or the write_file tool instead.',
|
|
35
|
+
'find': 'Use "dir /s" or the glob tool instead.',
|
|
36
|
+
'head': 'Use the read_file tool with offset/limit instead.',
|
|
37
|
+
'tail': 'Use the read_file tool instead.',
|
|
38
|
+
'wc': 'Use the read_file tool instead.',
|
|
39
|
+
'diff': 'Use the diff tool instead.',
|
|
40
|
+
'which': 'Use "where" instead.',
|
|
41
|
+
'echo': 'echo works on Windows, but avoid pipes (|).',
|
|
117
42
|
};
|
|
118
|
-
/**
|
|
119
|
-
* Detect when the model mistakes a tool call for a shell command — e.g.
|
|
120
|
-
* `bash` with command "create_dir path=C:\...\src" or "read_file file=x".
|
|
121
|
-
* These are tool invocations, not commands; running them through the shell
|
|
122
|
-
* fails. The matched tool name and raw args are returned so the caller can
|
|
123
|
-
* redirect into the real tool.
|
|
124
|
-
*/
|
|
125
|
-
/**
|
|
126
|
-
* Shell commands that must NEVER be treated as mistaken tool calls. The
|
|
127
|
-
* model often writes files via `echo '<code with = signs>'` or reads them
|
|
128
|
-
* via `cat` — those are shell commands, not tool invocations. Without this
|
|
129
|
-
* guard the redirect heuristic fires on any `<word> <text containing =>`
|
|
130
|
-
* and fails with "Unknown tool: echo".
|
|
131
|
-
*/
|
|
132
|
-
const NEVER_TOOL_CALLS = new Set([
|
|
133
|
-
"echo",
|
|
134
|
-
"cat",
|
|
135
|
-
"type",
|
|
136
|
-
"printf",
|
|
137
|
-
"touch",
|
|
138
|
-
"mkdir",
|
|
139
|
-
"cp",
|
|
140
|
-
"mv",
|
|
141
|
-
"rm",
|
|
142
|
-
"ls",
|
|
143
|
-
"dir",
|
|
144
|
-
"cd",
|
|
145
|
-
"pwd",
|
|
146
|
-
"grep",
|
|
147
|
-
"find",
|
|
148
|
-
"head",
|
|
149
|
-
"tail",
|
|
150
|
-
"wc",
|
|
151
|
-
"chmod",
|
|
152
|
-
"sed",
|
|
153
|
-
"awk",
|
|
154
|
-
]);
|
|
155
|
-
export function detectToolCallInBash(command) {
|
|
156
|
-
const match = command.trim().match(/^([\w-]+)\s+(.+)$/s);
|
|
157
|
-
if (!match)
|
|
158
|
-
return null;
|
|
159
|
-
const tool = match[1];
|
|
160
|
-
// Real shell commands (echo, cat, ...) are never mistaken tool calls.
|
|
161
|
-
if (NEVER_TOOL_CALLS.has(tool))
|
|
162
|
-
return null;
|
|
163
|
-
const rest = match[2].trim();
|
|
164
|
-
// Only treat as a tool call when the first word looks like a snake_case
|
|
165
|
-
// tool name and the rest has at least one '=' or a JSON object shape.
|
|
166
|
-
if (!/^[a-z][a-z0-9_]+$/.test(tool))
|
|
167
|
-
return null;
|
|
168
|
-
if (!rest.includes("=") && !rest.startsWith("{"))
|
|
169
|
-
return null;
|
|
170
|
-
return { tool, args: rest };
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Parse the raw argument string of a mistaken tool call captured in a bash
|
|
174
|
-
* command into a Record. Supports JSON objects ("{"path": "..."}") and
|
|
175
|
-
* key=value pairs ("path=C:\...\src"). Values keep their literal text.
|
|
176
|
-
*/
|
|
177
|
-
export function parseToolArgs(raw) {
|
|
178
|
-
const trimmed = raw.trim();
|
|
179
|
-
if (trimmed.startsWith("{")) {
|
|
180
|
-
try {
|
|
181
|
-
return JSON.parse(trimmed);
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
/* fall through to key=value parsing */
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const args = {};
|
|
188
|
-
// Tokenize respecting double/single-quoted values.
|
|
189
|
-
const tokens = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
190
|
-
for (const token of tokens) {
|
|
191
|
-
const eq = token.indexOf("=");
|
|
192
|
-
if (eq > 0) {
|
|
193
|
-
const key = token.slice(0, eq);
|
|
194
|
-
const value = token.slice(eq + 1);
|
|
195
|
-
// Strip surrounding quotes from values.
|
|
196
|
-
args[key] = value.replace(/^["']|["']$/g, "");
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return args;
|
|
200
|
-
}
|
|
201
43
|
export const bashTool = {
|
|
202
|
-
name:
|
|
203
|
-
description:
|
|
204
|
-
tags: [
|
|
44
|
+
name: 'bash',
|
|
45
|
+
description: 'Execute a shell command and return its output. Use for running tests, build, git, and shell operations. Long-running commands (dev servers, watchers) start in the background and return a process id immediately — manage them with process_list, process_log, process_kill. Set background=true to force background execution.',
|
|
46
|
+
tags: ['shell', 'code'],
|
|
205
47
|
parameters: {
|
|
206
|
-
type:
|
|
48
|
+
type: 'object',
|
|
207
49
|
properties: {
|
|
208
|
-
command: { type:
|
|
209
|
-
workdir: {
|
|
210
|
-
|
|
211
|
-
description: "Working directory (default: baseDir)",
|
|
212
|
-
},
|
|
213
|
-
background: {
|
|
214
|
-
type: "boolean",
|
|
215
|
-
description: "Return a process id immediately without waiting (default: commands still running after a few seconds are auto-promoted to the background)",
|
|
216
|
-
},
|
|
50
|
+
command: { type: 'string', description: 'Shell command to execute' },
|
|
51
|
+
workdir: { type: 'string', description: 'Working directory (default: baseDir)' },
|
|
52
|
+
background: { type: 'boolean', description: 'Start the command in the background and return immediately with a process id (default: auto-detect long-running commands)' },
|
|
217
53
|
},
|
|
218
|
-
required: [
|
|
54
|
+
required: ['command'],
|
|
219
55
|
},
|
|
220
56
|
handler: async (ctx, args) => {
|
|
221
57
|
const originalCommand = String(args.command);
|
|
222
|
-
// The model sometimes sends a tool invocation (e.g. "create_dir path=...")
|
|
223
|
-
// as a bash command instead of calling the tool directly. Redirect into
|
|
224
|
-
// the real tool so the intent succeeds instead of failing in the shell.
|
|
225
|
-
const toolCall = detectToolCallInBash(originalCommand);
|
|
226
|
-
if (toolCall &&
|
|
227
|
-
toolCall.tool !== "bash" &&
|
|
228
|
-
ctx.toolExecutor &&
|
|
229
|
-
ctx.toolExecutor.hasTool(toolCall.tool)) {
|
|
230
|
-
const redirected = await ctx.toolExecutor.executeByName(toolCall.tool, parseToolArgs(toolCall.args), ctx);
|
|
231
|
-
return {
|
|
232
|
-
success: redirected.success,
|
|
233
|
-
output: `[redirected to tool "${toolCall.tool}"]\n${redirected.output}`,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
58
|
const command = adaptCommandForWindows(originalCommand);
|
|
237
|
-
// echo/printf redirection to a file is unreliable in cmd.exe (single
|
|
238
|
-
// quotes don't group, multi-line commands split, embedded double quotes
|
|
239
|
-
// break the command). Steer the model to write_file instead — it
|
|
240
|
-
// produces correct files every time.
|
|
241
|
-
if (platform() === "win32") {
|
|
242
|
-
const echoWrite = extractEchoFileWrite(originalCommand);
|
|
243
|
-
if (echoWrite) {
|
|
244
|
-
return {
|
|
245
|
-
success: false,
|
|
246
|
-
output: t("bash.echo_write_blocked", { path: echoWrite }),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
59
|
const workdir = args.workdir ? String(args.workdir) : ctx.baseDir;
|
|
251
60
|
// Get session-specific security config with defaults
|
|
252
61
|
const appConfig = ctx.config || {};
|
|
@@ -268,70 +77,64 @@ export const bashTool = {
|
|
|
268
77
|
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, // Will be updated after execution
|
|
269
78
|
`Working directory: ${workdir}`);
|
|
270
79
|
}
|
|
271
|
-
const
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
80
|
+
const background = args.background === true || isLongRunningCommand(command);
|
|
81
|
+
if (background) {
|
|
82
|
+
const entry = processRegistry.start(command, workdir, ctx.sessionId);
|
|
83
|
+
if (securityConfig?.logCommands) {
|
|
84
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), true, `Started in background: ${entry.id} (PID ${entry.pid})`);
|
|
85
|
+
}
|
|
86
|
+
const explicit = args.background === true;
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
output: `${t("proc.started", {
|
|
90
|
+
id: entry.id,
|
|
91
|
+
pid: entry.pid,
|
|
92
|
+
command,
|
|
93
|
+
})}${explicit ? "" : `\n${t("proc.detected_hint")}`}\n${t("proc.manage_hint", {
|
|
94
|
+
id: entry.id,
|
|
95
|
+
})}`,
|
|
96
|
+
};
|
|
275
97
|
}
|
|
276
98
|
try {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
?.split(/[\\/]/)
|
|
299
|
-
.pop();
|
|
300
|
-
const hint = firstWord ? UNIX_TO_WIN_HINTS[firstWord] : undefined;
|
|
301
|
-
if (hint) {
|
|
302
|
-
output = `${output}\n\nHint: "${firstWord}" may not work on Windows. ${hint}`;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
// Update audit log with result
|
|
306
|
-
if (securityConfig?.logCommands) {
|
|
307
|
-
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), code === 0, `Working directory: ${workdir}, Output length: ${output.length}`);
|
|
308
|
-
}
|
|
309
|
-
const lines = output.split("\n");
|
|
310
|
-
if (lines.length > MAX_PREVIEW_LINES) {
|
|
311
|
-
output =
|
|
312
|
-
lines.slice(0, MAX_PREVIEW_LINES).join("\n") +
|
|
313
|
-
`\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
99
|
+
const res = await runCommand(command, {
|
|
100
|
+
cwd: workdir,
|
|
101
|
+
callId: ctx.activeCallId,
|
|
102
|
+
timeoutMs: BASH_TIMEOUT_MS,
|
|
103
|
+
});
|
|
104
|
+
const parts = [res.stdout.trimEnd(), res.stderr.trimEnd()].filter(Boolean);
|
|
105
|
+
let output = parts.join("\n");
|
|
106
|
+
if (res.timedOut) {
|
|
107
|
+
output = `${output ? output + "\n" : ""}${t("proc.timed_out", {
|
|
108
|
+
ms: BASH_TIMEOUT_MS,
|
|
109
|
+
})}`;
|
|
110
|
+
}
|
|
111
|
+
else if (!output && res.code !== 0) {
|
|
112
|
+
output = `(exit code ${res.code})`;
|
|
113
|
+
}
|
|
114
|
+
// On Windows, hint about Unix commands that don't work
|
|
115
|
+
if (platform() === 'win32' && res.code !== 0) {
|
|
116
|
+
const firstWord = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop();
|
|
117
|
+
const hint = firstWord ? UNIX_TO_WIN_HINTS[firstWord] : undefined;
|
|
118
|
+
if (hint) {
|
|
119
|
+
output = `${output}\n\nHint: "${firstWord}" may not work on Windows. ${hint}`;
|
|
314
120
|
}
|
|
315
|
-
return { success: code === 0, output };
|
|
316
121
|
}
|
|
317
|
-
//
|
|
318
|
-
const explicit = args.background === true;
|
|
319
|
-
const output = `${t("proc.started", {
|
|
320
|
-
id: entry.id,
|
|
321
|
-
pid: entry.pid,
|
|
322
|
-
command,
|
|
323
|
-
})}${explicit ? "" : `\n${t("proc.promoted_hint", { ms: settleMs })}`}\n${t("proc.manage_hint", {
|
|
324
|
-
id: entry.id,
|
|
325
|
-
})}`;
|
|
122
|
+
// Update audit log with result
|
|
326
123
|
if (securityConfig?.logCommands) {
|
|
327
|
-
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command),
|
|
124
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), res.code === 0, `Working directory: ${workdir}, Output length: ${res.stdout.length}`);
|
|
328
125
|
}
|
|
329
|
-
|
|
126
|
+
const lines = output.split('\n');
|
|
127
|
+
if (lines.length > MAX_PREVIEW_LINES) {
|
|
128
|
+
output = lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
129
|
+
}
|
|
130
|
+
return { success: res.code === 0, output };
|
|
330
131
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
132
|
+
catch (e) {
|
|
133
|
+
// Update audit log with failure
|
|
134
|
+
if (securityConfig?.logCommands) {
|
|
135
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, `Working directory: ${workdir}, Error: ${e.message?.slice(0, 100) || ""}`);
|
|
334
136
|
}
|
|
137
|
+
return { success: false, output: e.message || String(e) };
|
|
335
138
|
}
|
|
336
139
|
},
|
|
337
140
|
};
|