micro-models-agent 0.16.1 → 0.16.3
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/core/agent.js +21 -0
- package/dist/i18n/en.json +2 -0
- package/dist/i18n/ru.json +2 -0
- package/dist/modules/execution/module.js +4 -1
- package/dist/modules/execution/stuck-detector.js +24 -3
- package/dist/modules/security/command-validator.js +4 -1
- package/dist/tools/bash.js +5 -0
- package/dist/tools/glob-tool.js +9 -2
- package/dist/tools/grep-tool.js +10 -3
- package/dist/tools/list-dir.js +9 -2
- package/dist/tools/preview.js +2 -0
- package/dist/tools/process-log.js +5 -3
- package/dist/tools/read-file.js +2 -2
- package/dist/tools/web-browse.js +9 -3
- package/dist/tools/web-fetch.js +12 -2
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -113,6 +113,8 @@ export class Agent {
|
|
|
113
113
|
let apiPromptTokens = 0;
|
|
114
114
|
let apiCompletionTokens = 0;
|
|
115
115
|
const MAX_HALLUCINATION_RETRIES = 3;
|
|
116
|
+
let consecutiveToolFailures = 0;
|
|
117
|
+
const MAX_CONSECUTIVE_TOOL_FAILURES = 5;
|
|
116
118
|
while (iteration < config.maxToolIterations) {
|
|
117
119
|
iteration++;
|
|
118
120
|
pluginManager.runOnBeforeThink({
|
|
@@ -246,6 +248,7 @@ export class Agent {
|
|
|
246
248
|
})),
|
|
247
249
|
});
|
|
248
250
|
const summaries = [];
|
|
251
|
+
let anyToolFailed = false;
|
|
249
252
|
for (const call of toolCalls) {
|
|
250
253
|
this.setScope();
|
|
251
254
|
const startTime = Date.now();
|
|
@@ -258,6 +261,8 @@ export class Agent {
|
|
|
258
261
|
slog.logToolCall(call, iteration);
|
|
259
262
|
const result = await toolExecutor.execute(call);
|
|
260
263
|
const duration = Date.now() - startTime;
|
|
264
|
+
if (!result.success)
|
|
265
|
+
anyToolFailed = true;
|
|
261
266
|
pluginManager.runOnToolEnd({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
|
|
262
267
|
if (result.display) {
|
|
263
268
|
onMeta?.("\n" + result.display + "\n");
|
|
@@ -292,6 +297,22 @@ export class Agent {
|
|
|
292
297
|
logger.debug("Context compacted after tool result");
|
|
293
298
|
}
|
|
294
299
|
}
|
|
300
|
+
if (anyToolFailed) {
|
|
301
|
+
consecutiveToolFailures++;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
consecutiveToolFailures = 0;
|
|
305
|
+
}
|
|
306
|
+
if (consecutiveToolFailures >= MAX_CONSECUTIVE_TOOL_FAILURES) {
|
|
307
|
+
const recoveryMsg = t("exec.consecutive_failures_recovery", { count: consecutiveToolFailures });
|
|
308
|
+
logger.warn(`Consecutive tool failures: ${consecutiveToolFailures}`);
|
|
309
|
+
const taskSnippet = input.length > 200 ? input.slice(0, 200) + "..." : input;
|
|
310
|
+
const taskReminder = t("exec.task_reminder", { task: taskSnippet });
|
|
311
|
+
contextManager.addMessage({
|
|
312
|
+
role: "user",
|
|
313
|
+
content: `<system-summary>${recoveryMsg}\n${taskReminder}</system-summary>`,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
295
316
|
contextManager.addMessage({
|
|
296
317
|
role: "user",
|
|
297
318
|
content: `<system-summary>${summaries.join("\n")}</system-summary>`,
|
package/dist/i18n/en.json
CHANGED
|
@@ -375,6 +375,8 @@
|
|
|
375
375
|
"exec.tool_errors": "Tool {tool} failed {count} times. Suggest alternative approach.",
|
|
376
376
|
"exec.tool_errors_recovery": "[Tool {tool} failed {count} times. Stop using this tool and try a different approach or explain the issue to the user.]",
|
|
377
377
|
"exec.repetitive_tool": "[You called {tool} {count} times in a row with the same arguments and result. Stop repeating — try a different approach: create files, change arguments, or explain the problem to the user.]",
|
|
378
|
+
"exec.consecutive_failures_recovery": "[{count} consecutive tool calls failed. You are going in circles — stop trying commands that keep failing. Explain the problem to the user, or try a completely different approach: write files directly, use a different tool, or break the task into smaller parts.]",
|
|
379
|
+
"exec.task_reminder": "[Remember the original task: {task}. Do not switch to unrelated tasks. If you cannot complete the original task, explain why to the user and ask for guidance.]",
|
|
378
380
|
"exec.plan_warning": "[\u26a0 Warning: you are working on step {step} - \"{description}\". Your {tool} call touches files not mentioned in the current step. Focus on the current step or update the plan via plan update.]",
|
|
379
381
|
"exec.off_track": "[\u26a0 Outside plan: step {stepId} is \"{description}\", but you're using {tool} on a different path. Explain or return.]",
|
|
380
382
|
"exec.audit_pass": "[\u2713] Task complete: {done}/{total} steps done, {files} files verified",
|
package/dist/i18n/ru.json
CHANGED
|
@@ -375,6 +375,8 @@
|
|
|
375
375
|
"exec.tool_errors": "Инструмент {tool} сбоил {count} раз. Предложите другой подход.",
|
|
376
376
|
"exec.tool_errors_recovery": "[Инструмент {tool} сбоил {count} раз. Прекратите использовать этот инструмент и попробуйте другой подход или объясните проблему пользователю.]",
|
|
377
377
|
"exec.repetitive_tool": "[Вы вызвали {tool} {count} раз подряд с одинаковыми аргументами и результатом. Прекратите повторять — попробуйте другой подход: создайте файлы, измените аргументы или объясните пользователю проблему.]",
|
|
378
|
+
"exec.consecutive_failures_recovery": "[{count} последовательных вызовов тулов завершились ошибкой. Вы ходите по кругу — прекратите пытаться выполнить команды, которые постоянно падают. Объясните проблему пользователю или попробуйте совершенно другой подход: напишите файлы напрямую, используйте другой инструмент или разбейте задачу на части.]",
|
|
379
|
+
"exec.task_reminder": "[Напоминание о задаче: {task}. Не переключайтесь на посторонние задачи. Если не можете выполнить задачу — объясните почему и спросите совета.]",
|
|
378
380
|
"exec.plan_warning": "[⚠ Внимание: вы работаете над шагом {step} — \"{description}\". Ваш вызов {tool} касается файлов, не упомянутых в текущем шаге. Сосредоточьтесь на текущем шаге или обновите план через plan update.]",
|
|
379
381
|
"exec.off_track": "[⚠ Вне плана: шаг {stepId} — \"{description}\", но вы используете {tool} для другого пути. Объясните или вернитесь.]",
|
|
380
382
|
"exec.audit_pass": "[✓] Задача выполнена: {done}/{total} шагов, {files} файлов проверено",
|
|
@@ -271,7 +271,10 @@ export class ExecutionModule {
|
|
|
271
271
|
},
|
|
272
272
|
onAfterTool: (ctx, call, result) => {
|
|
273
273
|
if (!result.success) {
|
|
274
|
-
this.stuckDetector.recordToolError(
|
|
274
|
+
this.stuckDetector.recordToolError(call.name);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
this.stuckDetector.recordToolSuccess();
|
|
275
278
|
}
|
|
276
279
|
if (this.tracker &&
|
|
277
280
|
result.success &&
|
|
@@ -9,6 +9,8 @@ export class StuckDetector {
|
|
|
9
9
|
recentToolCalls = [];
|
|
10
10
|
maxRecentCalls = 10;
|
|
11
11
|
repetitionThreshold = 3;
|
|
12
|
+
consecutiveFailures = 0;
|
|
13
|
+
lastFailedTool = '';
|
|
12
14
|
constructor(threshold = 8, errorThreshold = 3) {
|
|
13
15
|
this.threshold = threshold;
|
|
14
16
|
this.errorThreshold = errorThreshold;
|
|
@@ -31,6 +33,12 @@ export class StuckDetector {
|
|
|
31
33
|
}
|
|
32
34
|
recordToolError(toolName) {
|
|
33
35
|
this.toolErrors.set(toolName, (this.toolErrors.get(toolName) || 0) + 1);
|
|
36
|
+
this.consecutiveFailures++;
|
|
37
|
+
this.lastFailedTool = toolName;
|
|
38
|
+
}
|
|
39
|
+
recordToolSuccess() {
|
|
40
|
+
this.consecutiveFailures = 0;
|
|
41
|
+
this.lastFailedTool = '';
|
|
34
42
|
}
|
|
35
43
|
setCurrentStep(stepId, description) {
|
|
36
44
|
if (stepId !== this.currentStepId) {
|
|
@@ -63,6 +71,12 @@ export class StuckDetector {
|
|
|
63
71
|
}
|
|
64
72
|
return count >= this.repetitionThreshold;
|
|
65
73
|
}
|
|
74
|
+
hasConsecutiveFailures() {
|
|
75
|
+
return this.consecutiveFailures >= this.errorThreshold;
|
|
76
|
+
}
|
|
77
|
+
getConsecutiveFailuresCount() {
|
|
78
|
+
return this.consecutiveFailures;
|
|
79
|
+
}
|
|
66
80
|
getRepetitiveToolMessage() {
|
|
67
81
|
if (!this.hasRepetitiveToolCalls())
|
|
68
82
|
return '';
|
|
@@ -83,13 +97,18 @@ export class StuckDetector {
|
|
|
83
97
|
if (this.isStuck()) {
|
|
84
98
|
return t('exec.stuck_recovery', { iterations: this.iterationsOnCurrentStep });
|
|
85
99
|
}
|
|
86
|
-
|
|
87
|
-
return this.getRepetitiveToolMessage();
|
|
88
|
-
}
|
|
100
|
+
// Per-tool error recovery (more specific — e.g., "bash failed 3 times")
|
|
89
101
|
const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
|
|
90
102
|
if (errorTool) {
|
|
91
103
|
return t('exec.tool_errors_recovery', { tool: errorTool[0], count: errorTool[1] });
|
|
92
104
|
}
|
|
105
|
+
// Consecutive failures from different tools (generic — e.g., "5 consecutive failures")
|
|
106
|
+
if (this.hasConsecutiveFailures()) {
|
|
107
|
+
return t('exec.consecutive_failures_recovery', { count: this.consecutiveFailures });
|
|
108
|
+
}
|
|
109
|
+
if (this.hasRepetitiveToolCalls()) {
|
|
110
|
+
return this.getRepetitiveToolMessage();
|
|
111
|
+
}
|
|
93
112
|
return '';
|
|
94
113
|
}
|
|
95
114
|
checkOffTrack(toolName, call) {
|
|
@@ -109,5 +128,7 @@ export class StuckDetector {
|
|
|
109
128
|
reset() {
|
|
110
129
|
this.iterationsOnCurrentStep = 0;
|
|
111
130
|
this.toolErrors.clear();
|
|
131
|
+
this.consecutiveFailures = 0;
|
|
132
|
+
this.lastFailedTool = '';
|
|
112
133
|
}
|
|
113
134
|
}
|
|
@@ -19,6 +19,8 @@ export const DEFAULT_BASH_CONFIG = DEFAULT_SECURITY_CONFIG?.bash || FALLBACK_BAS
|
|
|
19
19
|
* "NODE_ENV=prod node app.js" → "node"
|
|
20
20
|
* "env PATH=/x rm -rf /" → "rm"
|
|
21
21
|
* "cmd=rm; $cmd -rf /" → "cmd" (variable indirection — not expanded)
|
|
22
|
+
* "powershell Remove-Item x" → "Remove-Item"
|
|
23
|
+
* "cmd /c del /s /q" → "del"
|
|
22
24
|
*/
|
|
23
25
|
function extractBaseCommand(trimmed) {
|
|
24
26
|
const tokens = trimmed.split(/\s+/);
|
|
@@ -30,7 +32,8 @@ function extractBaseCommand(trimmed) {
|
|
|
30
32
|
i++;
|
|
31
33
|
continue;
|
|
32
34
|
}
|
|
33
|
-
if (tok === "sudo" || tok === "env" || tok === "command" || tok === "exec" || tok === "nohup"
|
|
35
|
+
if (tok === "sudo" || tok === "env" || tok === "command" || tok === "exec" || tok === "nohup" ||
|
|
36
|
+
tok === "powershell" || tok === "pwsh" || tok === "cmd") {
|
|
34
37
|
i++;
|
|
35
38
|
continue;
|
|
36
39
|
}
|
package/dist/tools/bash.js
CHANGED
|
@@ -5,6 +5,7 @@ import { DEFAULT_SECURITY_CONFIG } from '../config/security';
|
|
|
5
5
|
import { runCommand, processRegistry, isLongRunningCommand } from '../modules/processes';
|
|
6
6
|
import { t } from '../i18n/index';
|
|
7
7
|
import { platform } from 'os';
|
|
8
|
+
import { MAX_PREVIEW_LINES } from './preview';
|
|
8
9
|
const BASH_TIMEOUT_MS = 120_000;
|
|
9
10
|
function adaptCommandForWindows(command) {
|
|
10
11
|
if (platform() !== 'win32')
|
|
@@ -121,6 +122,10 @@ export const bashTool = {
|
|
|
121
122
|
if (securityConfig?.logCommands) {
|
|
122
123
|
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), res.code === 0, `Working directory: ${workdir}, Output length: ${res.stdout.length}`);
|
|
123
124
|
}
|
|
125
|
+
const lines = output.split('\n');
|
|
126
|
+
if (lines.length > MAX_PREVIEW_LINES) {
|
|
127
|
+
output = lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
128
|
+
}
|
|
124
129
|
return { success: res.code === 0, output };
|
|
125
130
|
}
|
|
126
131
|
catch (e) {
|
package/dist/tools/glob-tool.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { globSync } from 'fs';
|
|
2
2
|
import { t } from '../i18n/index';
|
|
3
|
+
import { MAX_PREVIEW_LINES } from './preview';
|
|
3
4
|
export const globTool = {
|
|
4
5
|
name: 'glob',
|
|
5
|
-
description:
|
|
6
|
+
description: `Search for files matching a glob pattern. Shows up to ${MAX_PREVIEW_LINES} results by default. Uses standard glob syntax (e.g., **/*.ts, src/**/*.test.ts).`,
|
|
6
7
|
tags: ['file', 'code', 'research'],
|
|
7
8
|
parameters: {
|
|
8
9
|
type: 'object',
|
|
@@ -14,6 +15,12 @@ export const globTool = {
|
|
|
14
15
|
handler: async (ctx, args) => {
|
|
15
16
|
const pattern = String(args.pattern);
|
|
16
17
|
const results = globSync(pattern, { cwd: ctx.baseDir });
|
|
17
|
-
|
|
18
|
+
if (results.length === 0) {
|
|
19
|
+
return { success: true, output: t('file.no_matches') };
|
|
20
|
+
}
|
|
21
|
+
const truncated = results.length > MAX_PREVIEW_LINES
|
|
22
|
+
? `\n... (${results.length - MAX_PREVIEW_LINES} more files)`
|
|
23
|
+
: '';
|
|
24
|
+
return { success: true, output: results.slice(0, MAX_PREVIEW_LINES).join('\n') + truncated };
|
|
18
25
|
},
|
|
19
26
|
};
|
package/dist/tools/grep-tool.js
CHANGED
|
@@ -2,9 +2,16 @@ import { execFileSync, execSync } from 'child_process';
|
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { t } from '../i18n/index';
|
|
4
4
|
import { logBashCommand } from '../modules/security/audit-log';
|
|
5
|
+
import { MAX_PREVIEW_LINES } from './preview';
|
|
6
|
+
function truncateLines(output) {
|
|
7
|
+
const lines = output.split('\n');
|
|
8
|
+
if (lines.length <= MAX_PREVIEW_LINES)
|
|
9
|
+
return output;
|
|
10
|
+
return lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
11
|
+
}
|
|
5
12
|
export const grepTool = {
|
|
6
13
|
name: 'grep',
|
|
7
|
-
description:
|
|
14
|
+
description: `Search file contents using a regular expression. Shows up to ${MAX_PREVIEW_LINES} matching lines by default. Uses ripgrep (rg) if available, otherwise falls back to grep -r.`,
|
|
8
15
|
tags: ['file', 'code', 'research'],
|
|
9
16
|
parameters: {
|
|
10
17
|
type: 'object',
|
|
@@ -32,7 +39,7 @@ export const grepTool = {
|
|
|
32
39
|
maxBuffer: 1024 * 1024,
|
|
33
40
|
cwd: ctx.baseDir,
|
|
34
41
|
});
|
|
35
|
-
return { success: true, output: output || t('file.no_matches') };
|
|
42
|
+
return { success: true, output: truncateLines(output || t('file.no_matches')) };
|
|
36
43
|
}
|
|
37
44
|
catch (e) {
|
|
38
45
|
if (e.status === 1)
|
|
@@ -45,7 +52,7 @@ export const grepTool = {
|
|
|
45
52
|
maxBuffer: 1024 * 1024,
|
|
46
53
|
cwd: ctx.baseDir,
|
|
47
54
|
});
|
|
48
|
-
return { success: true, output: output || t('file.no_matches') };
|
|
55
|
+
return { success: true, output: truncateLines(output || t('file.no_matches')) };
|
|
49
56
|
}
|
|
50
57
|
catch (e2) {
|
|
51
58
|
if (e2.status === 1)
|
package/dist/tools/list-dir.js
CHANGED
|
@@ -3,9 +3,10 @@ import { resolve } from 'path';
|
|
|
3
3
|
import { t } from '../i18n/index';
|
|
4
4
|
import { isPathInScope } from '../modules/security/path-validator';
|
|
5
5
|
import { safeResolvePath } from './path-utils';
|
|
6
|
+
import { MAX_PREVIEW_LINES } from './preview';
|
|
6
7
|
export const listDirTool = {
|
|
7
8
|
name: 'list_dir',
|
|
8
|
-
description:
|
|
9
|
+
description: `List files and directories in a given path. Shows up to ${MAX_PREVIEW_LINES} entries by default.`,
|
|
9
10
|
tags: ['file'],
|
|
10
11
|
parameters: {
|
|
11
12
|
type: 'object',
|
|
@@ -35,6 +36,12 @@ export const listDirTool = {
|
|
|
35
36
|
const full = resolve(resolved, e);
|
|
36
37
|
return statSync(full).isDirectory() ? `${e}/` : e;
|
|
37
38
|
});
|
|
38
|
-
|
|
39
|
+
if (lines.length === 0) {
|
|
40
|
+
return { success: true, output: t('file.empty') };
|
|
41
|
+
}
|
|
42
|
+
const truncated = lines.length > MAX_PREVIEW_LINES
|
|
43
|
+
? `\n... (${lines.length - MAX_PREVIEW_LINES} more entries)`
|
|
44
|
+
: '';
|
|
45
|
+
return { success: true, output: lines.slice(0, MAX_PREVIEW_LINES).join('\n') + truncated };
|
|
39
46
|
},
|
|
40
47
|
};
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { processRegistry } from '../modules/processes';
|
|
2
2
|
import { t } from '../i18n/index';
|
|
3
|
+
import { MAX_PREVIEW_LINES } from './preview';
|
|
4
|
+
const DEFAULT_TAIL = MAX_PREVIEW_LINES;
|
|
3
5
|
export const processLogTool = {
|
|
4
6
|
name: 'process_log',
|
|
5
|
-
description:
|
|
7
|
+
description: `Show the buffered output of a background process started via bash. Shows the last ${DEFAULT_TAIL} lines by default. Use after starting a dev server to verify it came up without errors, and while it runs to check its state.`,
|
|
6
8
|
tags: ['shell'],
|
|
7
9
|
parameters: {
|
|
8
10
|
type: 'object',
|
|
9
11
|
properties: {
|
|
10
12
|
id: { type: 'string', description: 'Process id from bash output or process_list' },
|
|
11
|
-
tail: { type: 'number', description:
|
|
13
|
+
tail: { type: 'number', description: `Number of trailing lines to show (default: ${DEFAULT_TAIL}, max 300)` },
|
|
12
14
|
},
|
|
13
15
|
required: ['id'],
|
|
14
16
|
},
|
|
@@ -23,7 +25,7 @@ export const processLogTool = {
|
|
|
23
25
|
: entry.status === 'exited'
|
|
24
26
|
? t('proc.status_exited')
|
|
25
27
|
: t('proc.status_killed');
|
|
26
|
-
const tail = typeof args.tail === 'number' ? args.tail :
|
|
28
|
+
const tail = typeof args.tail === 'number' ? args.tail : DEFAULT_TAIL;
|
|
27
29
|
const log = processRegistry.getLog(id, tail);
|
|
28
30
|
if (!log) {
|
|
29
31
|
return {
|
package/dist/tools/read-file.js
CHANGED
|
@@ -5,8 +5,8 @@ import { isPathInScope } from "../modules/security/path-validator";
|
|
|
5
5
|
import { DEFAULT_SECURITY_CONFIG } from "../config/security";
|
|
6
6
|
import { logSecurityBlock } from "../modules/security/audit-log";
|
|
7
7
|
import { safeResolvePath } from "./path-utils";
|
|
8
|
-
|
|
9
|
-
const DEFAULT_LIMIT =
|
|
8
|
+
import { MAX_PREVIEW_LINES } from "./preview";
|
|
9
|
+
const DEFAULT_LIMIT = MAX_PREVIEW_LINES;
|
|
10
10
|
export const readFileTool = {
|
|
11
11
|
name: "read_file",
|
|
12
12
|
description: `Read a file from the filesystem. Reads up to ${DEFAULT_LIMIT} lines at a time by default; use offset to page through large files.`,
|
package/dist/tools/web-browse.js
CHANGED
|
@@ -2,9 +2,11 @@ import { t } from '../i18n/index';
|
|
|
2
2
|
import { isUrlAllowed, sanitizeUrl } from '../modules/security/network-validator';
|
|
3
3
|
import { logNetworkRequest, logSecurityBlock } from '../modules/security/audit-log';
|
|
4
4
|
import { getSessionSecurityConfig } from '../modules/security/session-isolation';
|
|
5
|
+
import { MAX_PREVIEW_LINES } from './preview';
|
|
6
|
+
const MAX_CHARS = 3000;
|
|
5
7
|
export const webBrowseTool = {
|
|
6
8
|
name: 'web_browse',
|
|
7
|
-
description:
|
|
9
|
+
description: `Fetch and read a web page. Returns the page content as plain text (max ${MAX_PREVIEW_LINES} lines / ${MAX_CHARS} chars).`,
|
|
8
10
|
tags: ['research'],
|
|
9
11
|
parameters: {
|
|
10
12
|
type: 'object',
|
|
@@ -39,8 +41,12 @@ export const webBrowseTool = {
|
|
|
39
41
|
.trim();
|
|
40
42
|
// Log successful network request
|
|
41
43
|
logNetworkRequest(ctx.sessionId, sanitizeUrl(url), true, `Status: ${response.status}`);
|
|
42
|
-
const maxLen = securityConfig?.maxResponseSize ||
|
|
43
|
-
|
|
44
|
+
const maxLen = securityConfig?.maxResponseSize || MAX_CHARS;
|
|
45
|
+
let content = stripped.length > maxLen ? stripped.slice(0, maxLen) + t('file.truncated') : stripped;
|
|
46
|
+
const lines = content.split('\n');
|
|
47
|
+
if (lines.length > MAX_PREVIEW_LINES) {
|
|
48
|
+
content = lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
49
|
+
}
|
|
44
50
|
return { success: true, output: content || t('file.empty_page') };
|
|
45
51
|
}
|
|
46
52
|
catch (err) {
|
package/dist/tools/web-fetch.js
CHANGED
|
@@ -3,7 +3,8 @@ import { isUrlAllowed, sanitizeUrl } from '../modules/security/network-validator
|
|
|
3
3
|
import { logNetworkRequest, logSecurityBlock } from '../modules/security/audit-log';
|
|
4
4
|
import { getSessionSecurityConfig } from '../modules/security/session-isolation';
|
|
5
5
|
import { DEFAULT_SECURITY_CONFIG } from '../config/security';
|
|
6
|
-
|
|
6
|
+
import { MAX_PREVIEW_LINES } from './preview';
|
|
7
|
+
const MAX_CHARS = 5000;
|
|
7
8
|
function stripHtml(html) {
|
|
8
9
|
return html
|
|
9
10
|
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
@@ -50,7 +51,16 @@ export const webFetchTool = {
|
|
|
50
51
|
// Log successful network request
|
|
51
52
|
logNetworkRequest(ctx.sessionId, sanitizeUrl(url), true, `Status: ${response.status}`);
|
|
52
53
|
if (cleaned.length > (securityConfig?.maxResponseSize || MAX_CHARS)) {
|
|
53
|
-
|
|
54
|
+
let content = cleaned.slice(0, securityConfig?.maxResponseSize || MAX_CHARS) + t('file.truncated');
|
|
55
|
+
const lines = content.split('\n');
|
|
56
|
+
if (lines.length > MAX_PREVIEW_LINES) {
|
|
57
|
+
content = lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`;
|
|
58
|
+
}
|
|
59
|
+
return { success: true, output: content };
|
|
60
|
+
}
|
|
61
|
+
const lines = cleaned.split('\n');
|
|
62
|
+
if (lines.length > MAX_PREVIEW_LINES) {
|
|
63
|
+
return { success: true, output: lines.slice(0, MAX_PREVIEW_LINES).join('\n') + `\n... (${lines.length - MAX_PREVIEW_LINES} more lines)` };
|
|
54
64
|
}
|
|
55
65
|
return { success: true, output: cleaned || t('file.empty_page') };
|
|
56
66
|
}
|