micro-models-agent 0.39.0 → 0.40.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/bin/mma.mjs +41 -41
- package/dist/cli/commands.js +116 -3
- package/dist/cli/main.js +35 -8
- package/dist/cli/repl-commands.js +633 -0
- package/dist/cli/repl.js +110 -611
- package/dist/cli/setup.js +32 -12
- package/dist/config/config.js +46 -30
- package/dist/config/defaults.js +10 -1
- package/dist/config/security.js +15 -8
- package/dist/core/agent-moe.js +24 -12
- package/dist/core/agent.js +281 -47
- package/dist/core/bootstrap.js +52 -36
- package/dist/core/session-logger.js +35 -2
- package/dist/core/workspace.js +76 -0
- package/dist/i18n/en.json +79 -15
- package/dist/i18n/index.js +12 -9
- package/dist/i18n/ru.json +79 -15
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +39 -10
- package/dist/logger/app-logger.js +83 -16
- package/dist/logger/file-log.js +151 -0
- package/dist/main.js +537 -284
- package/dist/modules/browser/bridge-server.mjs +113 -105
- package/dist/modules/browser/session.js +108 -60
- package/dist/modules/certification/cli.js +176 -0
- package/dist/modules/certification/fact-checker.js +84 -0
- package/dist/modules/certification/loader.js +111 -0
- package/dist/modules/certification/manifest.js +50 -0
- package/dist/modules/certification/runner.js +162 -0
- package/dist/modules/certification/scenarios.js +124 -0
- package/dist/modules/certification/types.js +1 -0
- package/dist/modules/context/manager.js +119 -10
- package/dist/modules/execution/auditor.js +33 -39
- package/dist/modules/execution/index.js +8 -6
- package/dist/modules/execution/module.js +474 -32
- package/dist/modules/execution/moe-executor.js +97 -40
- 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 +159 -0
- package/dist/modules/execution/planner.js +63 -13
- package/dist/modules/execution/stuck-detector.js +252 -39
- package/dist/modules/execution/tracker.js +21 -7
- package/dist/modules/execution/verifier.js +46 -17
- package/dist/modules/hallucination/confidence.js +7 -2
- package/dist/modules/hallucination/consistency.js +8 -42
- package/dist/modules/hallucination/detector.js +26 -21
- package/dist/modules/hallucination/factual.js +170 -150
- package/dist/modules/hallucination/index.js +5 -4
- package/dist/modules/hallucination/js-identifiers.js +72 -0
- package/dist/modules/hallucination/llm-judge.js +103 -0
- package/dist/modules/index.js +5 -5
- package/dist/modules/lsp/client.js +235 -0
- package/dist/modules/lsp/config.js +81 -0
- package/dist/modules/lsp/index.js +3 -0
- package/dist/modules/lsp/module.js +68 -0
- package/dist/modules/lsp/types.js +1 -0
- package/dist/modules/mcp/client.js +8 -2
- package/dist/modules/memory/store.js +4 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +143 -38
- package/dist/modules/processes/index.js +1 -2
- package/dist/modules/processes/registry.js +125 -35
- package/dist/modules/processes/runner.js +9 -110
- package/dist/modules/security/audit-log.js +30 -10
- package/dist/modules/security/command-validator.js +42 -16
- package/dist/modules/security/content-scanner.js +9 -8
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +64 -10
- package/dist/modules/security/security-policies.js +221 -67
- package/dist/modules/security/session-encryption.js +42 -25
- package/dist/modules/session/manager.js +15 -10
- package/dist/modules/session/store.js +62 -8
- package/dist/modules/skills/index.js +2 -3
- package/dist/modules/skills/module.js +10 -23
- package/dist/tools/bash.js +287 -90
- package/dist/tools/create-dir.js +0 -1
- package/dist/tools/delete-file.js +0 -1
- package/dist/tools/edit-file.js +10 -8
- package/dist/tools/executor.js +57 -7
- package/dist/tools/grep-tool.js +51 -29
- package/dist/tools/index.js +55 -40
- package/dist/tools/load-skill.js +14 -18
- package/dist/tools/move-file.js +3 -2
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +15 -5
- package/dist/tools/search-history.js +42 -22
- package/dist/tools/subagent.js +21 -12
- package/dist/tools/web-browse.js +54 -25
- package/dist/tools/web-fetch.js +60 -34
- package/dist/tools/web-search.js +39 -20
- package/dist/tools/write-file.js +13 -10
- package/dist/ui/diff.js +9 -16
- package/dist/ui/renderer.js +69 -6
- package/package.json +48 -45
- package/dist/modules/context/history.js +0 -15
- package/dist/modules/processes/detect.js +0 -34
- package/dist/modules/skills/matcher.js +0 -27
package/dist/tools/edit-file.js
CHANGED
|
@@ -57,14 +57,16 @@ export const editFileTool = {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
const updated = content.replace(oldStr, newStr);
|
|
60
|
-
// Check new content for dangerous patterns
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
// Check new content for dangerous patterns — only when security is enabled
|
|
61
|
+
if (securityConfig?.enabled && securityConfig?.contentScan?.enabled) {
|
|
62
|
+
const scanResult = scanContent(updated, path, securityConfig.contentScan);
|
|
63
|
+
if (!scanResult.allowed) {
|
|
64
|
+
logSecurityBlock(ctx.sessionId, "file_write", scanResult.reason || "Content contains dangerous patterns", path);
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
output: `[SECURITY BLOCKED] ${scanResult.reason}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
68
70
|
}
|
|
69
71
|
writeFileSync(resolved, updated, "utf-8");
|
|
70
72
|
const diff = generateDiff(content, updated);
|
package/dist/tools/executor.js
CHANGED
|
@@ -10,7 +10,32 @@ export class ToolExecutor {
|
|
|
10
10
|
this.ctx = ctx;
|
|
11
11
|
this.pluginManager = pluginManager;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Execute a tool by name with plain arguments (no call id, no timeout
|
|
15
|
+
* wiring). Used internally — e.g. bash redirects a mistaken tool-call-as-
|
|
16
|
+
* command into the real tool. Runs through the same registry and plugin
|
|
17
|
+
* hooks as a normal call.
|
|
18
|
+
*/
|
|
19
|
+
async executeByName(name, args, ctx) {
|
|
20
|
+
const prevCtx = this.ctx;
|
|
21
|
+
if (ctx)
|
|
22
|
+
this.ctx = ctx;
|
|
23
|
+
try {
|
|
24
|
+
return await this.execute({
|
|
25
|
+
id: `redirect_${Date.now()}`,
|
|
26
|
+
name,
|
|
27
|
+
arguments: args,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
this.ctx = prevCtx;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Whether a tool with this name is registered (used before redirects). */
|
|
35
|
+
hasTool(name) {
|
|
36
|
+
return this.registry.has(name);
|
|
37
|
+
}
|
|
38
|
+
async execute(call, signal) {
|
|
14
39
|
const tool = this.registry.get(call.name);
|
|
15
40
|
if (!tool) {
|
|
16
41
|
return {
|
|
@@ -23,12 +48,16 @@ export class ToolExecutor {
|
|
|
23
48
|
if (plugin.onBeforeTool) {
|
|
24
49
|
try {
|
|
25
50
|
const proceed = await plugin.onBeforeTool(this.ctx, call);
|
|
26
|
-
if (
|
|
51
|
+
if (proceed === false || typeof proceed === "string") {
|
|
52
|
+
const pluginName = plugin.name ||
|
|
53
|
+
plugin.constructor?.name ||
|
|
54
|
+
"unknown";
|
|
55
|
+
const reason = typeof proceed === "string" ? proceed : undefined;
|
|
27
56
|
return {
|
|
28
57
|
success: false,
|
|
29
|
-
output:
|
|
30
|
-
plugin:
|
|
31
|
-
|
|
58
|
+
output: reason
|
|
59
|
+
? t("tool.blocked_reason", { plugin: pluginName, reason })
|
|
60
|
+
: t("tool.blocked", { plugin: pluginName }),
|
|
32
61
|
toolCallId: call.id,
|
|
33
62
|
};
|
|
34
63
|
}
|
|
@@ -56,10 +85,26 @@ export class ToolExecutor {
|
|
|
56
85
|
})));
|
|
57
86
|
}, TOOL_EXECUTION_TIMEOUT_MS);
|
|
58
87
|
});
|
|
59
|
-
|
|
88
|
+
const racePromises = [
|
|
60
89
|
tool.handler(this.ctx, call.arguments),
|
|
61
90
|
timeoutPromise,
|
|
62
|
-
]
|
|
91
|
+
];
|
|
92
|
+
if (signal) {
|
|
93
|
+
const abortPromise = new Promise((_, reject) => {
|
|
94
|
+
const onAbort = () => {
|
|
95
|
+
killByCallId(call.id);
|
|
96
|
+
reject(new Error(t("tool.aborted", { name: call.name })));
|
|
97
|
+
};
|
|
98
|
+
if (signal.aborted) {
|
|
99
|
+
onAbort();
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
racePromises.push(abortPromise);
|
|
106
|
+
}
|
|
107
|
+
result = await Promise.race(racePromises);
|
|
63
108
|
}
|
|
64
109
|
result.toolCallId = call.id;
|
|
65
110
|
}
|
|
@@ -80,6 +125,8 @@ export class ToolExecutor {
|
|
|
80
125
|
}
|
|
81
126
|
}
|
|
82
127
|
}
|
|
128
|
+
this.ctx.logger.logToolCall(call.name, JSON.stringify(call.arguments)?.slice(0, 200) ?? "", result.success ? "OK" : `FAIL: ${result.output?.slice(0, 200)}`);
|
|
129
|
+
this.ctx.logger.logToolOutput(call.name, result.output ?? "", result.success ? 0 : 1);
|
|
83
130
|
this.ctx.logger.debug(`Tool ${call.name}: ${result.success ? "OK" : "FAIL"}`);
|
|
84
131
|
return result;
|
|
85
132
|
}
|
|
@@ -92,4 +139,7 @@ export class ToolExecutor {
|
|
|
92
139
|
setScope(scope) {
|
|
93
140
|
this.ctx.scope = scope;
|
|
94
141
|
}
|
|
142
|
+
updateProvider(provider) {
|
|
143
|
+
this.ctx.llmProvider = provider;
|
|
144
|
+
}
|
|
95
145
|
}
|
package/dist/tools/grep-tool.js
CHANGED
|
@@ -1,63 +1,85 @@
|
|
|
1
|
-
import { execFileSync
|
|
2
|
-
import { resolve } from
|
|
3
|
-
import { t } from
|
|
4
|
-
import { logBashCommand } from
|
|
5
|
-
import { MAX_PREVIEW_LINES } from
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { t } from "../i18n/index";
|
|
4
|
+
import { logBashCommand } from "../modules/security/audit-log";
|
|
5
|
+
import { MAX_PREVIEW_LINES } from "./preview";
|
|
6
6
|
function truncateLines(output) {
|
|
7
|
-
const lines = output.split(
|
|
7
|
+
const lines = output.split("\n");
|
|
8
8
|
if (lines.length <= MAX_PREVIEW_LINES)
|
|
9
9
|
return output;
|
|
10
|
-
return lines.slice(0, MAX_PREVIEW_LINES).join(
|
|
10
|
+
return (lines.slice(0, MAX_PREVIEW_LINES).join("\n") +
|
|
11
|
+
`\n... (${lines.length - MAX_PREVIEW_LINES} more lines)`);
|
|
11
12
|
}
|
|
12
13
|
export const grepTool = {
|
|
13
|
-
name:
|
|
14
|
+
name: "grep",
|
|
14
15
|
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.`,
|
|
15
|
-
tags: [
|
|
16
|
+
tags: ["file", "code", "research"],
|
|
16
17
|
parameters: {
|
|
17
|
-
type:
|
|
18
|
+
type: "object",
|
|
18
19
|
properties: {
|
|
19
|
-
pattern: { type:
|
|
20
|
-
include: {
|
|
21
|
-
|
|
20
|
+
pattern: { type: "string", description: "Regex pattern to search" },
|
|
21
|
+
include: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "File pattern to filter (e.g. *.ts)",
|
|
24
|
+
},
|
|
25
|
+
path: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Directory to search (defaults to baseDir)",
|
|
28
|
+
},
|
|
22
29
|
},
|
|
23
|
-
required: [
|
|
30
|
+
required: ["pattern"],
|
|
24
31
|
},
|
|
25
32
|
handler: async (ctx, args) => {
|
|
26
33
|
const pattern = String(args.pattern);
|
|
27
|
-
const searchPath = args.path
|
|
34
|
+
const searchPath = args.path
|
|
35
|
+
? resolve(ctx.baseDir, String(args.path))
|
|
36
|
+
: ctx.baseDir;
|
|
28
37
|
// Build rg arguments as an array to avoid shell interpretation of regex
|
|
29
38
|
// metacharacters like |, (, ) — these are regex patterns, not shell operators.
|
|
30
|
-
const rgArgs = [
|
|
39
|
+
const rgArgs = ["-n", "--with-filename", pattern, searchPath];
|
|
31
40
|
if (args.include) {
|
|
32
|
-
rgArgs.push(
|
|
41
|
+
rgArgs.push("-g", String(args.include));
|
|
33
42
|
}
|
|
34
43
|
// Log the search (sanitized) for audit purposes
|
|
35
|
-
logBashCommand(ctx.sessionId, `rg ${rgArgs.join(
|
|
44
|
+
logBashCommand(ctx.sessionId, `rg ${rgArgs.join(" ")}`, false, "grep-tool");
|
|
36
45
|
try {
|
|
37
|
-
const output = execFileSync(
|
|
38
|
-
encoding:
|
|
46
|
+
const output = execFileSync("rg", rgArgs, {
|
|
47
|
+
encoding: "utf-8",
|
|
39
48
|
maxBuffer: 1024 * 1024,
|
|
40
49
|
cwd: ctx.baseDir,
|
|
41
50
|
});
|
|
42
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
output: truncateLines(output || t("file.no_matches")),
|
|
54
|
+
};
|
|
43
55
|
}
|
|
44
56
|
catch (e) {
|
|
45
57
|
if (e.status === 1)
|
|
46
|
-
return { success: true, output: t(
|
|
47
|
-
// Fall back to plain grep -r when rg is not available or fails
|
|
58
|
+
return { success: true, output: t("file.no_matches") };
|
|
59
|
+
// Fall back to plain grep -r when rg is not available or fails.
|
|
60
|
+
// Using execFileSync with array args avoids shell injection.
|
|
48
61
|
try {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
const grepArgs = ["-rn", pattern, searchPath];
|
|
63
|
+
if (args.include) {
|
|
64
|
+
grepArgs.push("--include", String(args.include));
|
|
65
|
+
}
|
|
66
|
+
const output = execFileSync("grep", grepArgs, {
|
|
67
|
+
encoding: "utf-8",
|
|
52
68
|
maxBuffer: 1024 * 1024,
|
|
53
69
|
cwd: ctx.baseDir,
|
|
54
70
|
});
|
|
55
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
output: truncateLines(output || t("file.no_matches")),
|
|
74
|
+
};
|
|
56
75
|
}
|
|
57
76
|
catch (e2) {
|
|
58
77
|
if (e2.status === 1)
|
|
59
|
-
return { success: true, output: t(
|
|
60
|
-
return {
|
|
78
|
+
return { success: true, output: t("file.no_matches") };
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
output: t("error.grep_failed", { message: e2.message }),
|
|
82
|
+
};
|
|
61
83
|
}
|
|
62
84
|
}
|
|
63
85
|
},
|
package/dist/tools/index.js
CHANGED
|
@@ -1,46 +1,61 @@
|
|
|
1
|
-
import { ToolRegistry } from
|
|
2
|
-
import { ToolExecutor } from
|
|
3
|
-
import { readFileTool } from
|
|
4
|
-
import { writeFileTool } from
|
|
5
|
-
import { editFileTool } from
|
|
6
|
-
import { globTool } from
|
|
7
|
-
import { grepTool } from
|
|
8
|
-
import { listDirTool } from
|
|
9
|
-
import { createDirTool } from
|
|
10
|
-
import { deleteFileTool } from
|
|
11
|
-
import { moveFileTool } from
|
|
12
|
-
import { fileInfoTool } from
|
|
13
|
-
import { bashTool } from
|
|
14
|
-
import { processListTool } from
|
|
15
|
-
import { processLogTool } from
|
|
16
|
-
import { processKillTool } from
|
|
17
|
-
import { subagentTool } from
|
|
18
|
-
import { webSearchTool } from
|
|
19
|
-
import { webFetchTool } from
|
|
20
|
-
import { webBrowseTool } from
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
1
|
+
import { ToolRegistry } from "./registry";
|
|
2
|
+
import { ToolExecutor } from "./executor";
|
|
3
|
+
import { readFileTool } from "./read-file";
|
|
4
|
+
import { writeFileTool } from "./write-file";
|
|
5
|
+
import { editFileTool } from "./edit-file";
|
|
6
|
+
import { globTool } from "./glob-tool";
|
|
7
|
+
import { grepTool } from "./grep-tool";
|
|
8
|
+
import { listDirTool } from "./list-dir";
|
|
9
|
+
import { createDirTool } from "./create-dir";
|
|
10
|
+
import { deleteFileTool } from "./delete-file";
|
|
11
|
+
import { moveFileTool } from "./move-file";
|
|
12
|
+
import { fileInfoTool } from "./file-info";
|
|
13
|
+
import { bashTool } from "./bash";
|
|
14
|
+
import { processListTool } from "./process-list";
|
|
15
|
+
import { processLogTool } from "./process-log";
|
|
16
|
+
import { processKillTool } from "./process-kill";
|
|
17
|
+
import { subagentTool } from "./subagent";
|
|
18
|
+
import { webSearchTool } from "./web-search";
|
|
19
|
+
import { webFetchTool } from "./web-fetch";
|
|
20
|
+
import { webBrowseTool } from "./web-browse";
|
|
21
|
+
// questionTool and approveTool removed — interactive menus are too disruptive for small models.
|
|
22
|
+
// The model should ask questions as plain text in its response instead.
|
|
23
|
+
// import { questionTool } from './question'
|
|
24
|
+
// import { approveTool } from './approve'
|
|
25
|
+
import { createLoadSkillTool } from "./load-skill";
|
|
26
|
+
import { pipelineRunTool } from "./pipeline-run";
|
|
27
|
+
import { mcpCallTool } from "./mcp-call";
|
|
28
|
+
import { searchHistoryTool } from "./search-history";
|
|
29
|
+
import { rememberTool } from "./remember";
|
|
30
|
+
import { recallTool } from "./recall";
|
|
31
|
+
import { createBrowserTool } from "./browser";
|
|
32
|
+
import { attachImageTool } from "./attach-image";
|
|
31
33
|
export { ToolRegistry, ToolExecutor };
|
|
32
|
-
export { filterToolsByTags } from
|
|
33
|
-
export { readFileTool, writeFileTool, editFileTool, globTool, grepTool, listDirTool, createDirTool, deleteFileTool, moveFileTool, fileInfoTool, bashTool, subagentTool, processListTool, processLogTool, processKillTool, webSearchTool, webFetchTool, webBrowseTool,
|
|
34
|
+
export { filterToolsByTags } from "./filter-tools";
|
|
35
|
+
export { readFileTool, writeFileTool, editFileTool, globTool, grepTool, listDirTool, createDirTool, deleteFileTool, moveFileTool, fileInfoTool, bashTool, subagentTool, processListTool, processLogTool, processKillTool, webSearchTool, webFetchTool, webBrowseTool, createLoadSkillTool, pipelineRunTool, mcpCallTool, searchHistoryTool, rememberTool, recallTool, createBrowserTool, attachImageTool, };
|
|
34
36
|
export function registerAllTools(registry, skillsModule) {
|
|
35
37
|
const tools = [
|
|
36
|
-
readFileTool,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
readFileTool,
|
|
39
|
+
writeFileTool,
|
|
40
|
+
editFileTool,
|
|
41
|
+
globTool,
|
|
42
|
+
grepTool,
|
|
43
|
+
listDirTool,
|
|
44
|
+
createDirTool,
|
|
45
|
+
deleteFileTool,
|
|
46
|
+
moveFileTool,
|
|
47
|
+
fileInfoTool,
|
|
48
|
+
bashTool,
|
|
49
|
+
subagentTool,
|
|
50
|
+
processListTool,
|
|
51
|
+
processLogTool,
|
|
52
|
+
processKillTool,
|
|
53
|
+
webSearchTool,
|
|
54
|
+
webFetchTool,
|
|
55
|
+
webBrowseTool,
|
|
56
|
+
pipelineRunTool,
|
|
57
|
+
mcpCallTool,
|
|
58
|
+
searchHistoryTool,
|
|
44
59
|
attachImageTool,
|
|
45
60
|
];
|
|
46
61
|
if (skillsModule) {
|
package/dist/tools/load-skill.js
CHANGED
|
@@ -3,33 +3,23 @@ export function createLoadSkillTool(skillsModule) {
|
|
|
3
3
|
return {
|
|
4
4
|
name: "load_skill",
|
|
5
5
|
tags: ["core"],
|
|
6
|
-
description: "Load a skill by name
|
|
6
|
+
description: "Load a skill by name. Skills provide specialized instructions and workflows. When loaded, the skill content is injected into the system prompt (not conversation history) so it persists through context compaction. Only use when the task clearly requires domain-specific knowledge.",
|
|
7
7
|
parameters: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
10
|
-
name: {
|
|
11
|
-
task: {
|
|
10
|
+
name: {
|
|
12
11
|
type: "string",
|
|
13
|
-
description: "
|
|
12
|
+
description: "Exact skill name to load (see [Available Skills] for names)",
|
|
14
13
|
},
|
|
15
14
|
},
|
|
15
|
+
required: ["name"],
|
|
16
16
|
},
|
|
17
17
|
handler: async (_ctx, args) => {
|
|
18
18
|
const name = args.name ? String(args.name) : undefined;
|
|
19
|
-
|
|
20
|
-
if (!name && !task) {
|
|
21
|
-
return { success: false, output: t("tool.name_or_task") };
|
|
22
|
-
}
|
|
23
|
-
let result;
|
|
24
|
-
if (name) {
|
|
25
|
-
result = skillsModule.loadByName(name);
|
|
26
|
-
}
|
|
27
|
-
else if (task) {
|
|
28
|
-
result = skillsModule.loadByMatch(task);
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
19
|
+
if (!name) {
|
|
31
20
|
return { success: false, output: t("tool.invalid_params") };
|
|
32
21
|
}
|
|
22
|
+
const result = skillsModule.loadByName(name);
|
|
33
23
|
if (!result.success) {
|
|
34
24
|
const available = skillsModule.getAvailable();
|
|
35
25
|
const hint = available.length > 0
|
|
@@ -37,11 +27,17 @@ export function createLoadSkillTool(skillsModule) {
|
|
|
37
27
|
: "";
|
|
38
28
|
return { success: false, output: result.message + hint };
|
|
39
29
|
}
|
|
40
|
-
const skill = result.skill;
|
|
41
30
|
const budget = skillsModule.getBudget();
|
|
31
|
+
const tokens = result.skill
|
|
32
|
+
? Math.ceil(result.skill.content.length / 4)
|
|
33
|
+
: 0;
|
|
42
34
|
return {
|
|
43
35
|
success: true,
|
|
44
|
-
output:
|
|
36
|
+
output: t("tool.skill_budget", {
|
|
37
|
+
name,
|
|
38
|
+
tokens,
|
|
39
|
+
remaining: budget.remaining,
|
|
40
|
+
}),
|
|
45
41
|
};
|
|
46
42
|
},
|
|
47
43
|
};
|
package/dist/tools/move-file.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isPathWritable } from "../modules/security/path-validator";
|
|
|
5
5
|
import { logFileWrite, logSecurityBlock } from "../modules/security/audit-log";
|
|
6
6
|
import { getSessionSecurityConfig } from "../modules/security/session-isolation";
|
|
7
7
|
import { generateMoveDiff } from "../ui/diff";
|
|
8
|
+
import { safeResolvePath } from "./path-utils";
|
|
8
9
|
export const moveFileTool = {
|
|
9
10
|
name: "move_file",
|
|
10
11
|
description: "Move or rename a file or directory.",
|
|
@@ -21,8 +22,8 @@ export const moveFileTool = {
|
|
|
21
22
|
const fromPath = String(args.from);
|
|
22
23
|
const toPath = String(args.to);
|
|
23
24
|
const baseDir = resolve(ctx.baseDir);
|
|
24
|
-
const fromResolved =
|
|
25
|
-
const toResolved =
|
|
25
|
+
const fromResolved = safeResolvePath(baseDir, normalize(fromPath));
|
|
26
|
+
const toResolved = safeResolvePath(baseDir, normalize(toPath));
|
|
26
27
|
// Get session-specific security config
|
|
27
28
|
const securityConfig = ctx.sessionContext
|
|
28
29
|
? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
|
|
@@ -30,7 +30,7 @@ async function runStep(ctx, step, params, outputs) {
|
|
|
30
30
|
toolExecutor: ctx.toolExecutor,
|
|
31
31
|
pluginManager: subPluginManager,
|
|
32
32
|
contextManager: subContextManager,
|
|
33
|
-
hallucinationDetector: new HallucinationDetector(),
|
|
33
|
+
hallucinationDetector: new HallucinationDetector(ctx.baseDir, ctx.llmProvider),
|
|
34
34
|
logger: ctx.logger,
|
|
35
35
|
baseDir: ctx.baseDir,
|
|
36
36
|
scope: ctx.scope,
|
package/dist/tools/read-file.js
CHANGED
|
@@ -42,12 +42,17 @@ export const readFileTool = {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
if (!existsSync(resolved)) {
|
|
45
|
-
|
|
45
|
+
// For relative paths, tell the model WHERE the path resolved — the
|
|
46
|
+
// "not found" alone is confusing (it cannot tell baseDir from cwd).
|
|
47
|
+
const output = resolved !== path
|
|
48
|
+
? t("file.notfound_resolved", {
|
|
49
|
+
path,
|
|
50
|
+
resolved,
|
|
51
|
+
})
|
|
52
|
+
: t("file.notfound", { path });
|
|
53
|
+
return { success: false, output };
|
|
46
54
|
}
|
|
47
|
-
ctx.trackReadPath?.(path);
|
|
48
55
|
const content = readFileSync(resolved, "utf-8");
|
|
49
|
-
// Track file paths mentioned in the document (e.g. structure.md, README)
|
|
50
|
-
ctx.trackDocumentContent?.(content);
|
|
51
56
|
const lines = content.split("\n");
|
|
52
57
|
const total = lines.length;
|
|
53
58
|
const offset = args.offset || 1;
|
|
@@ -68,6 +73,11 @@ export const readFileTool = {
|
|
|
68
73
|
next: String(end + 1),
|
|
69
74
|
})
|
|
70
75
|
: "";
|
|
71
|
-
|
|
76
|
+
const display = truncated ? `${header}${truncated}` : header;
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
output: `${header}\n${selected}${truncated}`,
|
|
80
|
+
display,
|
|
81
|
+
};
|
|
72
82
|
},
|
|
73
83
|
};
|
|
@@ -1,45 +1,56 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import { join } from
|
|
3
|
-
import { homedir } from
|
|
4
|
-
import { t } from
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { t } from "../i18n/index";
|
|
5
5
|
function searchFile(filePath, query, maxResults, results) {
|
|
6
6
|
if (!fs.existsSync(filePath))
|
|
7
7
|
return;
|
|
8
|
-
const content = fs.readFileSync(filePath,
|
|
9
|
-
for (const line of content.split(
|
|
8
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
9
|
+
for (const line of content.split("\n").filter(Boolean)) {
|
|
10
10
|
if (line.toLowerCase().includes(query)) {
|
|
11
11
|
try {
|
|
12
12
|
const entry = JSON.parse(line);
|
|
13
13
|
results.push(`[${filePath}] ${JSON.stringify(entry).slice(0, 200)}`);
|
|
14
14
|
}
|
|
15
|
-
catch {
|
|
15
|
+
catch {
|
|
16
|
+
/* skip */
|
|
17
|
+
}
|
|
16
18
|
}
|
|
17
19
|
if (results.length >= maxResults)
|
|
18
20
|
break;
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
export const searchHistoryTool = {
|
|
22
|
-
name:
|
|
23
|
-
description:
|
|
24
|
-
tags: [
|
|
24
|
+
name: "search_history",
|
|
25
|
+
description: "Search through session history for past interactions across all sessions.",
|
|
26
|
+
tags: ["research"],
|
|
25
27
|
parameters: {
|
|
26
|
-
type:
|
|
28
|
+
type: "object",
|
|
27
29
|
properties: {
|
|
28
|
-
query: { type:
|
|
29
|
-
maxResults: {
|
|
30
|
-
|
|
30
|
+
query: { type: "string", description: "Search query" },
|
|
31
|
+
maxResults: {
|
|
32
|
+
type: "number",
|
|
33
|
+
description: "Maximum results (default 5)",
|
|
34
|
+
},
|
|
35
|
+
sessionId: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Optional: limit search to a specific session",
|
|
38
|
+
},
|
|
31
39
|
},
|
|
32
|
-
required: [
|
|
40
|
+
required: ["query"],
|
|
33
41
|
},
|
|
34
42
|
handler: async (_ctx, args) => {
|
|
35
|
-
const query = String(args.query ||
|
|
43
|
+
const query = String(args.query || "").toLowerCase();
|
|
36
44
|
const maxResults = Number(args.maxResults) || 5;
|
|
37
45
|
const sessionId = args.sessionId ? String(args.sessionId) : null;
|
|
38
|
-
const sessionDir = join(homedir(),
|
|
46
|
+
const sessionDir = join(homedir(), ".mma", "sessions");
|
|
39
47
|
const results = [];
|
|
40
48
|
try {
|
|
41
49
|
if (!fs.existsSync(sessionDir)) {
|
|
42
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
output: t("tool.no_sessions_dir", { dir: sessionDir }),
|
|
53
|
+
};
|
|
43
54
|
}
|
|
44
55
|
const entries = fs.readdirSync(sessionDir, { withFileTypes: true });
|
|
45
56
|
for (const entry of entries) {
|
|
@@ -47,18 +58,27 @@ export const searchHistoryTool = {
|
|
|
47
58
|
continue;
|
|
48
59
|
if (sessionId && entry.name !== sessionId)
|
|
49
60
|
continue;
|
|
50
|
-
const historyFile = join(sessionDir, entry.name,
|
|
61
|
+
const historyFile = join(sessionDir, entry.name, "history.jsonl");
|
|
51
62
|
searchFile(historyFile, query, maxResults, results);
|
|
52
63
|
if (results.length >= maxResults)
|
|
53
64
|
break;
|
|
54
65
|
}
|
|
55
66
|
if (results.length === 0) {
|
|
56
|
-
return { success: true, output: t(
|
|
67
|
+
return { success: true, output: t("tool.no_history", { query }) };
|
|
57
68
|
}
|
|
58
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
output: t("tool.history_results", {
|
|
72
|
+
query,
|
|
73
|
+
results: results.join("\n"),
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
59
76
|
}
|
|
60
77
|
catch (err) {
|
|
61
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
output: t("tool.history_error", { error: String(err) }),
|
|
81
|
+
};
|
|
62
82
|
}
|
|
63
83
|
},
|
|
64
84
|
};
|
package/dist/tools/subagent.js
CHANGED
|
@@ -98,7 +98,11 @@ export const subagentTool = {
|
|
|
98
98
|
try {
|
|
99
99
|
const subContextManager = new ContextManager(ctx.config.contextWindow, ctx.config.contextBudget);
|
|
100
100
|
const subPluginManager = new PluginManager();
|
|
101
|
-
const hallucinationDetector = new HallucinationDetector();
|
|
101
|
+
const hallucinationDetector = new HallucinationDetector(ctx.baseDir, ctx.llmProvider);
|
|
102
|
+
// Save/restore the shared executor scope so the sub-agent doesn't
|
|
103
|
+
// permanently mutate the parent's scope.
|
|
104
|
+
const parentScope = ctx.scope;
|
|
105
|
+
ctx.toolExecutor.setScope(scope || { allowed_files: [], read_only_files: [] });
|
|
102
106
|
const systemPrompt = {
|
|
103
107
|
content: `You are a sub-agent working on a specific task. ${context ? `Context: ${context}` : ""}`,
|
|
104
108
|
priority: "critical",
|
|
@@ -121,18 +125,23 @@ export const subagentTool = {
|
|
|
121
125
|
};
|
|
122
126
|
const subAgent = new Agent(subDeps);
|
|
123
127
|
const fullTask = context ? `${task}\n\nContext: ${context}` : task;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
let scopeRestored = false;
|
|
129
|
+
try {
|
|
130
|
+
const result = await subAgent.run(fullTask);
|
|
131
|
+
return result.success
|
|
132
|
+
? {
|
|
133
|
+
success: true,
|
|
134
|
+
output: `Sub-agent completed:\n${result.text}\n\nIterations: ${result.iterationCount}`,
|
|
135
|
+
}
|
|
136
|
+
: {
|
|
137
|
+
success: false,
|
|
138
|
+
output: `Sub-agent failed: ${result.error}\nPartial output: ${result.text}`,
|
|
139
|
+
};
|
|
130
140
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
};
|
|
141
|
+
finally {
|
|
142
|
+
if (!scopeRestored) {
|
|
143
|
+
ctx.toolExecutor.setScope(parentScope || { allowed_files: [], read_only_files: [] });
|
|
144
|
+
}
|
|
136
145
|
}
|
|
137
146
|
}
|
|
138
147
|
catch (e) {
|