micro-models-agent 0.7.10 → 0.9.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 +173 -0
- package/dist/cli/completer.js +168 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/main.js +95 -0
- package/dist/cli/repl.js +762 -0
- package/dist/cli/security-commands.js +166 -0
- package/dist/cli/setup.js +214 -0
- package/dist/config/config.js +186 -0
- package/dist/config/defaults.js +91 -0
- package/dist/config/experts.js +15 -0
- package/dist/config/index.js +3 -0
- package/dist/config/security.js +187 -0
- package/dist/config/types.js +1 -0
- package/dist/core/agent.js +626 -0
- package/dist/core/bootstrap.js +317 -0
- package/dist/core/index.js +2 -0
- package/dist/core/prompt-builder.js +55 -0
- package/dist/core/types.js +1 -0
- package/dist/i18n/en.json +405 -0
- package/dist/i18n/index.js +43 -0
- package/dist/i18n/ru.json +405 -0
- package/dist/index.js +22 -0
- package/dist/llm/index.js +4 -0
- package/dist/llm/model-loader.js +78 -0
- package/dist/llm/openai-compat.js +277 -0
- package/dist/llm/orchestrator.js +194 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/response.js +39 -0
- package/dist/llm/token-counter.js +37 -0
- package/dist/llm/types.js +1 -0
- package/dist/logger/app-logger.js +76 -0
- package/dist/logger/index.js +1 -0
- package/dist/main.js +15904 -0
- package/dist/migration/backup.js +45 -0
- package/dist/migration/detect.js +50 -0
- package/dist/migration/index.js +2 -0
- package/dist/modules/browser/actions.js +46 -0
- package/dist/modules/browser/cookie-store.js +24 -0
- package/dist/modules/browser/index.js +5 -0
- package/dist/modules/browser/module.js +28 -0
- package/dist/modules/browser/session.js +287 -0
- package/dist/modules/browser/snapshot.js +114 -0
- package/dist/modules/browser/types.js +9 -0
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/index.js +1 -0
- package/dist/modules/context/manager.js +179 -0
- package/dist/modules/execution/auditor.js +72 -0
- package/dist/modules/execution/index.js +6 -0
- package/dist/modules/execution/module.js +334 -0
- package/dist/modules/execution/moe-executor.js +196 -0
- package/dist/modules/execution/plan-validator.js +153 -0
- package/dist/modules/execution/planner.js +35 -0
- package/dist/modules/execution/stuck-detector.js +113 -0
- package/dist/modules/execution/tracker.js +53 -0
- package/dist/modules/execution/types.js +1 -0
- package/dist/modules/execution/verifier.js +149 -0
- package/dist/modules/hallucination/confidence.js +47 -0
- package/dist/modules/hallucination/consistency.js +32 -0
- package/dist/modules/hallucination/detector.js +41 -0
- package/dist/modules/hallucination/factual.js +128 -0
- package/dist/modules/hallucination/index.js +4 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/indexer/cache.js +38 -0
- package/dist/modules/indexer/index.js +3 -0
- package/dist/modules/indexer/module.js +192 -0
- package/dist/modules/indexer/walker.js +101 -0
- package/dist/modules/mcp/client.js +393 -0
- package/dist/modules/mcp/index.js +3 -0
- package/dist/modules/mcp/module.js +146 -0
- package/dist/modules/mcp/registry.js +15 -0
- package/dist/modules/memory/index.js +1 -0
- package/dist/modules/memory/search.js +26 -0
- package/dist/modules/memory/store.js +38 -0
- package/dist/modules/pipelines/engine.js +60 -0
- package/dist/modules/pipelines/index.js +3 -0
- package/dist/modules/pipelines/parser.js +53 -0
- package/dist/modules/pipelines/template.js +14 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +121 -0
- package/dist/modules/plugins/builtin/notify.js +8 -0
- package/dist/modules/plugins/index.js +1 -0
- package/dist/modules/plugins/loader.js +28 -0
- package/dist/modules/plugins/manager.js +161 -0
- package/dist/modules/plugins/types.js +1 -0
- package/dist/modules/registry.js +45 -0
- package/dist/modules/security/audit-log.js +116 -0
- package/dist/modules/security/audit-notifier.js +292 -0
- package/dist/modules/security/command-validator.js +104 -0
- package/dist/modules/security/content-scanner.js +52 -0
- package/dist/modules/security/data-sanitizer.js +97 -0
- package/dist/modules/security/encryption.js +238 -0
- package/dist/modules/security/index.js +14 -0
- package/dist/modules/security/network-validator.js +79 -0
- package/dist/modules/security/path-validator.js +155 -0
- package/dist/modules/security/rate-limiter.js +119 -0
- package/dist/modules/security/security-policies.js +393 -0
- package/dist/modules/security/session-encryption.js +193 -0
- package/dist/modules/security/session-isolation.js +95 -0
- package/dist/modules/session/index.js +3 -0
- package/dist/modules/session/manager.js +167 -0
- package/dist/modules/session/module.js +28 -0
- package/dist/modules/session/store.js +174 -0
- package/dist/modules/session/types.js +1 -0
- package/dist/modules/skills/index.js +3 -0
- package/dist/modules/skills/loader.js +72 -0
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +180 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/updater/checker.js +32 -0
- package/dist/modules/updater/index.js +1 -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/tools/approve.js +32 -0
- package/dist/tools/bash.js +80 -0
- package/dist/tools/browser.js +97 -0
- package/dist/tools/create-dir.js +57 -0
- package/dist/tools/delete-file.js +64 -0
- package/dist/tools/edit-file.js +78 -0
- package/dist/tools/executor.js +83 -0
- package/dist/tools/file-info.js +46 -0
- package/dist/tools/filter-tools.js +10 -0
- package/dist/tools/glob-tool.js +19 -0
- package/dist/tools/grep-tool.js +57 -0
- package/dist/tools/index.js +44 -0
- package/dist/tools/list-dir.js +40 -0
- package/dist/tools/load-skill.js +48 -0
- package/dist/tools/mcp-call.js +68 -0
- package/dist/tools/move-file.js +84 -0
- package/dist/tools/pipeline-run.js +144 -0
- package/dist/tools/question.js +142 -0
- package/dist/tools/read-file.js +70 -0
- package/dist/tools/registry.js +36 -0
- package/dist/tools/scope-check.js +30 -0
- package/dist/tools/search-history.js +64 -0
- package/dist/tools/subagent.js +142 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/user-input.js +123 -0
- package/dist/tools/web-browse.js +51 -0
- package/dist/tools/web-fetch.js +62 -0
- package/dist/tools/web-search.js +59 -0
- package/dist/tools/write-file.js +80 -0
- package/dist/ui/box.js +81 -0
- package/dist/ui/colors.js +4 -0
- package/dist/ui/diff.js +185 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/md-formatter.js +212 -0
- package/dist/ui/output.js +13 -0
- package/dist/ui/renderer.js +141 -0
- package/dist/ui/spinner.js +70 -0
- package/dist/ui/table.js +144 -0
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execFileSync, execSync } from 'child_process';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { t } from '../i18n/index';
|
|
4
|
+
import { logBashCommand } from '../modules/security/audit-log';
|
|
5
|
+
export const grepTool = {
|
|
6
|
+
name: 'grep',
|
|
7
|
+
description: 'Search file contents using a regular expression. Uses ripgrep (rg) if available, otherwise falls back to grep -r.',
|
|
8
|
+
tags: ['file', 'code', 'research'],
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
pattern: { type: 'string', description: 'Regex pattern to search' },
|
|
13
|
+
include: { type: 'string', description: 'File pattern to filter (e.g. *.ts)' },
|
|
14
|
+
path: { type: 'string', description: 'Directory to search (defaults to baseDir)' },
|
|
15
|
+
},
|
|
16
|
+
required: ['pattern'],
|
|
17
|
+
},
|
|
18
|
+
handler: async (ctx, args) => {
|
|
19
|
+
const pattern = String(args.pattern);
|
|
20
|
+
const searchPath = args.path ? resolve(ctx.baseDir, String(args.path)) : ctx.baseDir;
|
|
21
|
+
// Build rg arguments as an array to avoid shell interpretation of regex
|
|
22
|
+
// metacharacters like |, (, ) — these are regex patterns, not shell operators.
|
|
23
|
+
const rgArgs = ['-n', '--with-filename', pattern, searchPath];
|
|
24
|
+
if (args.include) {
|
|
25
|
+
rgArgs.push('-g', String(args.include));
|
|
26
|
+
}
|
|
27
|
+
// Log the search (sanitized) for audit purposes
|
|
28
|
+
logBashCommand(ctx.sessionId, `rg ${rgArgs.join(' ')}`, false, 'grep-tool');
|
|
29
|
+
try {
|
|
30
|
+
const output = execFileSync('rg', rgArgs, {
|
|
31
|
+
encoding: 'utf-8',
|
|
32
|
+
maxBuffer: 1024 * 1024,
|
|
33
|
+
cwd: ctx.baseDir,
|
|
34
|
+
});
|
|
35
|
+
return { success: true, output: output || t('file.no_matches') };
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e.status === 1)
|
|
39
|
+
return { success: true, output: t('file.no_matches') };
|
|
40
|
+
// Fall back to plain grep -r when rg is not available or fails
|
|
41
|
+
try {
|
|
42
|
+
const cmd = `grep -rn "${pattern.replace(/"/g, '\\"')}" "${searchPath}"`;
|
|
43
|
+
const output = execSync(cmd, {
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
maxBuffer: 1024 * 1024,
|
|
46
|
+
cwd: ctx.baseDir,
|
|
47
|
+
});
|
|
48
|
+
return { success: true, output: output || t('file.no_matches') };
|
|
49
|
+
}
|
|
50
|
+
catch (e2) {
|
|
51
|
+
if (e2.status === 1)
|
|
52
|
+
return { success: true, output: t('file.no_matches') };
|
|
53
|
+
return { success: false, output: t('error.grep_failed', { message: e2.message }) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { subagentTool } from './subagent';
|
|
15
|
+
import { webSearchTool } from './web-search';
|
|
16
|
+
import { webFetchTool } from './web-fetch';
|
|
17
|
+
import { webBrowseTool } from './web-browse';
|
|
18
|
+
import { questionTool } from './question';
|
|
19
|
+
import { approveTool } from './approve';
|
|
20
|
+
import { createLoadSkillTool } from './load-skill';
|
|
21
|
+
import { pipelineRunTool } from './pipeline-run';
|
|
22
|
+
import { mcpCallTool } from './mcp-call';
|
|
23
|
+
import { searchHistoryTool } from './search-history';
|
|
24
|
+
import { createBrowserTool } from './browser';
|
|
25
|
+
export { ToolRegistry, ToolExecutor };
|
|
26
|
+
export { filterToolsByTags } from './filter-tools';
|
|
27
|
+
export { readFileTool, writeFileTool, editFileTool, globTool, grepTool, listDirTool, createDirTool, deleteFileTool, moveFileTool, fileInfoTool, bashTool, subagentTool, webSearchTool, webFetchTool, webBrowseTool, questionTool, approveTool, createLoadSkillTool, pipelineRunTool, mcpCallTool, searchHistoryTool, createBrowserTool, };
|
|
28
|
+
export function registerAllTools(registry, skillsModule) {
|
|
29
|
+
const tools = [
|
|
30
|
+
readFileTool, writeFileTool, editFileTool,
|
|
31
|
+
globTool, grepTool, listDirTool, createDirTool,
|
|
32
|
+
deleteFileTool, moveFileTool, fileInfoTool,
|
|
33
|
+
bashTool, subagentTool,
|
|
34
|
+
webSearchTool, webFetchTool, webBrowseTool,
|
|
35
|
+
questionTool, approveTool,
|
|
36
|
+
pipelineRunTool, mcpCallTool, searchHistoryTool,
|
|
37
|
+
];
|
|
38
|
+
if (skillsModule) {
|
|
39
|
+
tools.push(createLoadSkillTool(skillsModule));
|
|
40
|
+
}
|
|
41
|
+
for (const tool of tools) {
|
|
42
|
+
registry.register(tool);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readdirSync, statSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve, normalize } from 'path';
|
|
3
|
+
import { t } from '../i18n/index';
|
|
4
|
+
import { isPathInScope } from '../modules/security/path-validator';
|
|
5
|
+
export const listDirTool = {
|
|
6
|
+
name: 'list_dir',
|
|
7
|
+
description: 'List files and directories in a given path.',
|
|
8
|
+
tags: ['file'],
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
path: { type: 'string', description: 'Directory path' },
|
|
13
|
+
},
|
|
14
|
+
required: ['path'],
|
|
15
|
+
},
|
|
16
|
+
handler: async (ctx, args) => {
|
|
17
|
+
const path = String(args.path);
|
|
18
|
+
const baseDir = resolve(ctx.baseDir);
|
|
19
|
+
const resolved = resolve(baseDir, normalize(path));
|
|
20
|
+
// Check path permissions
|
|
21
|
+
const scopeCheck = isPathInScope(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
|
|
22
|
+
if (!scopeCheck.allowed) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
output: t('file.path_not_allowed', {
|
|
26
|
+
path: `${path} — ${scopeCheck.reason}`,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (!existsSync(resolved)) {
|
|
31
|
+
return { success: false, output: t('file.dir_notfound', { path }) };
|
|
32
|
+
}
|
|
33
|
+
const entries = readdirSync(resolved);
|
|
34
|
+
const lines = entries.map(e => {
|
|
35
|
+
const full = resolve(resolved, e);
|
|
36
|
+
return statSync(full).isDirectory() ? `${e}/` : e;
|
|
37
|
+
});
|
|
38
|
+
return { success: true, output: lines.join('\n') || t('file.empty') };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { t } from "../i18n/index";
|
|
2
|
+
export function createLoadSkillTool(skillsModule) {
|
|
3
|
+
return {
|
|
4
|
+
name: "load_skill",
|
|
5
|
+
tags: ["core"],
|
|
6
|
+
description: "Load a skill by name or task description. Skills provide specialized instructions and workflows. Only use when the task clearly requires domain-specific knowledge. If loading fails because the skill is too large, continue the task without it.",
|
|
7
|
+
parameters: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
name: { type: "string", description: "Skill name to load directly" },
|
|
11
|
+
task: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Task description to find matching skill",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
handler: async (_ctx, args) => {
|
|
18
|
+
const name = args.name ? String(args.name) : undefined;
|
|
19
|
+
const task = args.task ? String(args.task) : undefined;
|
|
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 {
|
|
31
|
+
return { success: false, output: t("tool.invalid_params") };
|
|
32
|
+
}
|
|
33
|
+
if (!result.success) {
|
|
34
|
+
const available = skillsModule.getAvailable();
|
|
35
|
+
const hint = available.length > 0
|
|
36
|
+
? `\n\n${t("tool.skill_available_hint")}: ${available.map((s) => s.name).join(", ")}`
|
|
37
|
+
: "";
|
|
38
|
+
return { success: false, output: result.message + hint };
|
|
39
|
+
}
|
|
40
|
+
const skill = result.skill;
|
|
41
|
+
const budget = skillsModule.getBudget();
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
output: `${result.message}\n\n--- Skill Content ---\n${skill.content}\n\n${t("tool.skill_budget", { remaining: budget.remaining })}`,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { MCPClient } from "../modules/mcp/client";
|
|
2
|
+
import { MCPRegistry } from "../modules/mcp/registry";
|
|
3
|
+
const registry = new MCPRegistry();
|
|
4
|
+
let initialized = false;
|
|
5
|
+
function ensureInitialized(ctx) {
|
|
6
|
+
if (initialized)
|
|
7
|
+
return;
|
|
8
|
+
initialized = true;
|
|
9
|
+
const servers = ctx.config.mcpServers || {};
|
|
10
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
11
|
+
if (cfg.enabled !== false) {
|
|
12
|
+
registry.register({
|
|
13
|
+
name: cfg.name || name,
|
|
14
|
+
command: cfg.command,
|
|
15
|
+
args: cfg.args,
|
|
16
|
+
env: cfg.env,
|
|
17
|
+
transport: cfg.transport,
|
|
18
|
+
url: cfg.url,
|
|
19
|
+
headers: cfg.headers,
|
|
20
|
+
timeout: cfg.timeout,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export const mcpCallTool = {
|
|
26
|
+
name: "mcp_call",
|
|
27
|
+
description: "Call a tool on an MCP (Model Context Protocol) server. MCP servers provide external capabilities like databases, APIs, or specialized tools.",
|
|
28
|
+
tags: ["code", "research"],
|
|
29
|
+
parameters: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
server: { type: "string", description: "MCP server name" },
|
|
33
|
+
tool: { type: "string", description: "Tool name on the server" },
|
|
34
|
+
args: { type: "object", description: "Arguments to pass to the tool" },
|
|
35
|
+
},
|
|
36
|
+
required: ["server", "tool"],
|
|
37
|
+
},
|
|
38
|
+
handler: async (ctx, args) => {
|
|
39
|
+
const serverName = String(args.server || "");
|
|
40
|
+
const toolName = String(args.tool || "");
|
|
41
|
+
const toolArgs = (args.args || {});
|
|
42
|
+
if (!serverName || !toolName) {
|
|
43
|
+
return { success: false, output: "Server and tool names are required" };
|
|
44
|
+
}
|
|
45
|
+
ensureInitialized(ctx);
|
|
46
|
+
const serverConfig = registry.get(serverName);
|
|
47
|
+
if (!serverConfig) {
|
|
48
|
+
const available = registry.list();
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
output: `MCP server "${serverName}" not found. Available: ${available.join(", ") || "(none)"}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const client = new MCPClient(serverConfig);
|
|
56
|
+
await client.connect();
|
|
57
|
+
const result = await client.callTool(toolName, toolArgs);
|
|
58
|
+
await client.disconnect();
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
output: `MCP call result:\n${JSON.stringify(result, null, 2)}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return { success: false, output: `MCP call failed: ${e.message}` };
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { renameSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { resolve, normalize, dirname } from "path";
|
|
3
|
+
import { t } from "../i18n/index";
|
|
4
|
+
import { isPathWritable } from "../modules/security/path-validator";
|
|
5
|
+
import { logFileWrite, logSecurityBlock } from "../modules/security/audit-log";
|
|
6
|
+
import { getSessionSecurityConfig } from "../modules/security/session-isolation";
|
|
7
|
+
import { generateMoveDiff } from "../ui/diff";
|
|
8
|
+
export const moveFileTool = {
|
|
9
|
+
name: "move_file",
|
|
10
|
+
description: "Move or rename a file or directory.",
|
|
11
|
+
tags: ["file"],
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
from: { type: "string", description: "Source path" },
|
|
16
|
+
to: { type: "string", description: "Destination path" },
|
|
17
|
+
},
|
|
18
|
+
required: ["from", "to"],
|
|
19
|
+
},
|
|
20
|
+
handler: async (ctx, args) => {
|
|
21
|
+
const fromPath = String(args.from);
|
|
22
|
+
const toPath = String(args.to);
|
|
23
|
+
const baseDir = resolve(ctx.baseDir);
|
|
24
|
+
const fromResolved = resolve(baseDir, normalize(fromPath));
|
|
25
|
+
const toResolved = resolve(baseDir, normalize(toPath));
|
|
26
|
+
// Get session-specific security config
|
|
27
|
+
const securityConfig = ctx.sessionContext
|
|
28
|
+
? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
|
|
29
|
+
: ctx.config.security;
|
|
30
|
+
// Check file operations limit
|
|
31
|
+
const maxFileOps = securityConfig?.maxFileOperations ?? 100;
|
|
32
|
+
const currentCount = ctx.fileOperationsCount ?? 0;
|
|
33
|
+
if (currentCount >= maxFileOps) {
|
|
34
|
+
logSecurityBlock(ctx.sessionId, "file_write", `Maximum file operations (${maxFileOps}) exceeded`, `${fromPath} -> ${toPath}`);
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
output: `[SECURITY BLOCKED] Maximum file operations (${maxFileOps}) exceeded`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Check source path permissions
|
|
41
|
+
const fromCheck = isPathWritable(ctx.baseDir, fromPath, ctx.scope, ctx.config.security?.paths);
|
|
42
|
+
if (!fromCheck.allowed) {
|
|
43
|
+
logSecurityBlock(ctx.sessionId, "file_write", fromCheck.reason || "Source path not allowed", fromPath);
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
output: t("file.path_not_allowed", {
|
|
47
|
+
path: `${fromPath} — ${fromCheck.reason}`,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Check destination path permissions
|
|
52
|
+
const toCheck = isPathWritable(ctx.baseDir, toPath, ctx.scope, ctx.config.security?.paths);
|
|
53
|
+
if (!toCheck.allowed) {
|
|
54
|
+
logSecurityBlock(ctx.sessionId, "file_write", toCheck.reason || "Destination path not allowed", toPath);
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
output: t("file.path_not_allowed", {
|
|
58
|
+
path: `${toPath} — ${toCheck.reason}`,
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!existsSync(fromResolved)) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
output: t("file.not_found_short", { path: fromPath }),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const toDir = dirname(toResolved);
|
|
69
|
+
if (!existsSync(toDir)) {
|
|
70
|
+
mkdirSync(toDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
renameSync(fromResolved, toResolved);
|
|
73
|
+
const diff = generateMoveDiff(fromPath, toPath);
|
|
74
|
+
// Increment file operations counter
|
|
75
|
+
ctx.fileOperationsCount = currentCount + 1;
|
|
76
|
+
// Log file move
|
|
77
|
+
logFileWrite(ctx.sessionId, `${fromPath} -> ${toPath}`, true, "File moved");
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
output: t("file.moved", { from: fromPath, to: toPath }),
|
|
81
|
+
diff,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { t } from '../i18n/index';
|
|
2
|
+
import { PipelineEngine } from '../modules/pipelines/engine';
|
|
3
|
+
import { PipelineParser } from '../modules/pipelines/parser';
|
|
4
|
+
import { TemplateEngine } from '../modules/pipelines/template';
|
|
5
|
+
import { Agent } from '../core/agent';
|
|
6
|
+
import { ContextManager } from '../modules/context/manager';
|
|
7
|
+
import { PluginManager } from '../modules/plugins/manager';
|
|
8
|
+
import { HallucinationDetector } from '../modules/hallucination/detector';
|
|
9
|
+
import { logSecurityBlock } from '../modules/security/audit-log';
|
|
10
|
+
import { getSessionSecurityConfig } from '../modules/security/session-isolation';
|
|
11
|
+
const engine = new PipelineEngine();
|
|
12
|
+
const MAX_CONCURRENT = 3;
|
|
13
|
+
const MAX_ATTEMPTS = 3;
|
|
14
|
+
async function runStep(ctx, step, params, outputs) {
|
|
15
|
+
const prompt = TemplateEngine.render(step.prompt, params, outputs);
|
|
16
|
+
let lastError = "";
|
|
17
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
18
|
+
try {
|
|
19
|
+
const subContextManager = new ContextManager(ctx.config.contextWindow, ctx.config.contextBudget);
|
|
20
|
+
const subPluginManager = new PluginManager();
|
|
21
|
+
const systemPrompt = {
|
|
22
|
+
content: `You are a pipeline step agent ("${step.agent}"). Complete the given task using the available tools.`,
|
|
23
|
+
priority: "critical",
|
|
24
|
+
essential: true,
|
|
25
|
+
estimatedTokens: 80,
|
|
26
|
+
};
|
|
27
|
+
const subDeps = {
|
|
28
|
+
config: ctx.config,
|
|
29
|
+
llmProvider: ctx.llmProvider,
|
|
30
|
+
toolExecutor: ctx.toolExecutor,
|
|
31
|
+
pluginManager: subPluginManager,
|
|
32
|
+
contextManager: subContextManager,
|
|
33
|
+
hallucinationDetector: new HallucinationDetector(),
|
|
34
|
+
logger: ctx.logger,
|
|
35
|
+
baseDir: ctx.baseDir,
|
|
36
|
+
scope: ctx.scope,
|
|
37
|
+
recursionDepth: (ctx.recursionDepth ?? 0) + 1,
|
|
38
|
+
promptBlocks: [systemPrompt],
|
|
39
|
+
};
|
|
40
|
+
const subAgent = new Agent(subDeps);
|
|
41
|
+
const result = await subAgent.run(prompt);
|
|
42
|
+
if (result.success) {
|
|
43
|
+
return { ok: true, output: result.text };
|
|
44
|
+
}
|
|
45
|
+
lastError = result.error || "no output";
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
lastError = e.message;
|
|
49
|
+
}
|
|
50
|
+
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
51
|
+
}
|
|
52
|
+
return { ok: false, output: "", error: lastError };
|
|
53
|
+
}
|
|
54
|
+
export const pipelineRunTool = {
|
|
55
|
+
name: 'pipeline_run',
|
|
56
|
+
description: 'Run a named pipeline with YAML definition. Creates a DAG of sub-agents that execute in dependency order.',
|
|
57
|
+
tags: ['shell', 'code'],
|
|
58
|
+
parameters: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
name: { type: 'string', description: 'Pipeline name' },
|
|
62
|
+
yaml: { type: 'string', description: 'Pipeline YAML definition with steps' },
|
|
63
|
+
params: { type: 'object', description: 'Template params for {key} placeholders' },
|
|
64
|
+
},
|
|
65
|
+
required: ['name', 'yaml'],
|
|
66
|
+
},
|
|
67
|
+
handler: async (ctx, args) => {
|
|
68
|
+
const name = String(args.name || '');
|
|
69
|
+
const yaml = String(args.yaml || '');
|
|
70
|
+
const params = args.params || {};
|
|
71
|
+
if (!yaml) {
|
|
72
|
+
return { success: false, output: t('pipeline.invalid') };
|
|
73
|
+
}
|
|
74
|
+
if (!ctx.llmProvider || !ctx.toolExecutor) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
output: 'Pipeline cannot run: missing llmProvider or toolExecutor in context',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const securityConfig = ctx.sessionContext
|
|
81
|
+
? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
|
|
82
|
+
: ctx.config.security;
|
|
83
|
+
const maxDepth = securityConfig?.maxRecursionDepth ?? 3;
|
|
84
|
+
const currentDepth = ctx.recursionDepth ?? 0;
|
|
85
|
+
if (currentDepth >= maxDepth) {
|
|
86
|
+
logSecurityBlock(ctx.sessionId, "bash_command", `Maximum recursion depth (${maxDepth}) exceeded`, name);
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
output: `[SECURITY BLOCKED] Maximum pipeline recursion depth (${maxDepth}) exceeded`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const pipeline = PipelineParser.parse(yaml);
|
|
94
|
+
engine.reset();
|
|
95
|
+
const order = engine.resolveDependencies(pipeline.steps);
|
|
96
|
+
const completed = new Set();
|
|
97
|
+
const outputs = {};
|
|
98
|
+
const logs = [];
|
|
99
|
+
const failed = new Set();
|
|
100
|
+
while (completed.size < pipeline.steps.length) {
|
|
101
|
+
const ready = engine
|
|
102
|
+
.getReadySteps(pipeline.steps, completed)
|
|
103
|
+
.filter((s) => !failed.has(s.id));
|
|
104
|
+
if (ready.length === 0) {
|
|
105
|
+
if (failed.size > 0)
|
|
106
|
+
break;
|
|
107
|
+
throw new Error(t('pipeline.circular', { stepId: order.join(', ') }));
|
|
108
|
+
}
|
|
109
|
+
const batch = ready.slice(0, MAX_CONCURRENT);
|
|
110
|
+
const results = await Promise.all(batch.map((step) => runStep(ctx, step, params, outputs)));
|
|
111
|
+
for (let i = 0; i < batch.length; i++) {
|
|
112
|
+
const step = batch[i];
|
|
113
|
+
const result = results[i];
|
|
114
|
+
if (result.ok) {
|
|
115
|
+
completed.add(step.id);
|
|
116
|
+
outputs[step.id] = { output: result.output };
|
|
117
|
+
engine.setStepStatus(step.id, 'done');
|
|
118
|
+
logs.push(` ✓ ${step.id}: ${result.output.split("\n")[0]}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
failed.add(step.id);
|
|
122
|
+
engine.setStepStatus(step.id, 'failed');
|
|
123
|
+
logs.push(` ✗ ${step.id}: ${result.error}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (failed.size > 0)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (failed.size > 0) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
output: `Pipeline "${pipeline.name}" failed. Executed ${completed.size}/${pipeline.steps.length} steps.\n${logs.join("\n")}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
output: `Pipeline "${pipeline.name}" completed successfully (${completed.size} steps).\n${logs.join("\n")}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
return { success: false, output: `Pipeline error: ${e.message}` };
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { t } from "../i18n/index";
|
|
2
|
+
import { askUser } from "./user-input";
|
|
3
|
+
const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
|
|
4
|
+
1. Gather user preferences or requirements
|
|
5
|
+
2. Clarify ambiguous instructions
|
|
6
|
+
3. Get decisions on implementation choices as you work
|
|
7
|
+
4. Offer choices to the user about what direction to take.
|
|
8
|
+
|
|
9
|
+
Usage notes:
|
|
10
|
+
- When "custom" is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options
|
|
11
|
+
- Answers are returned as arrays of labels; set "multiple": true to allow selecting more than one
|
|
12
|
+
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
|
|
13
|
+
- Execution blocks until the user answers every question
|
|
14
|
+
- Omit "options" for a free-text question`;
|
|
15
|
+
function isOption(val) {
|
|
16
|
+
if (!val || typeof val !== "object")
|
|
17
|
+
return false;
|
|
18
|
+
const opt = val;
|
|
19
|
+
return typeof opt.label === "string" && typeof opt.description === "string";
|
|
20
|
+
}
|
|
21
|
+
function normalizeQuestions(args) {
|
|
22
|
+
const raw = args.questions;
|
|
23
|
+
if (Array.isArray(raw)) {
|
|
24
|
+
const specs = [];
|
|
25
|
+
for (const item of raw) {
|
|
26
|
+
if (!item || typeof item !== "object")
|
|
27
|
+
continue;
|
|
28
|
+
const q = item;
|
|
29
|
+
if (typeof q.question !== "string" || !q.question.trim())
|
|
30
|
+
continue;
|
|
31
|
+
const options = Array.isArray(q.options)
|
|
32
|
+
? q.options.filter(isOption)
|
|
33
|
+
: undefined;
|
|
34
|
+
// An explicitly provided options array with no valid entries is a
|
|
35
|
+
// malformed call — skip the question instead of blocking on stdin.
|
|
36
|
+
if (Array.isArray(q.options) && options.length === 0)
|
|
37
|
+
continue;
|
|
38
|
+
specs.push({
|
|
39
|
+
question: q.question,
|
|
40
|
+
header: typeof q.header === "string" ? q.header : undefined,
|
|
41
|
+
options,
|
|
42
|
+
multiple: q.multiple === true,
|
|
43
|
+
custom: q.custom === false ? false : undefined,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return specs;
|
|
47
|
+
}
|
|
48
|
+
// Legacy format: { question: "..." } — plain free-text question.
|
|
49
|
+
if (typeof args.question === "string" && args.question.trim()) {
|
|
50
|
+
return [{ question: args.question }];
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
export const questionTool = {
|
|
55
|
+
name: "question",
|
|
56
|
+
description: DESCRIPTION,
|
|
57
|
+
tags: ["core"],
|
|
58
|
+
interactive: true,
|
|
59
|
+
parameters: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
questions: {
|
|
63
|
+
type: "array",
|
|
64
|
+
description: "Questions to ask",
|
|
65
|
+
items: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
question: { type: "string", description: "Complete question" },
|
|
69
|
+
header: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Very short label (max 30 chars)",
|
|
72
|
+
},
|
|
73
|
+
options: {
|
|
74
|
+
type: "array",
|
|
75
|
+
description: "Available choices; omit for a free-text question",
|
|
76
|
+
items: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
label: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Display text (1-5 words, concise)",
|
|
82
|
+
},
|
|
83
|
+
description: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Explanation of choice",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
required: ["label", "description"],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
multiple: {
|
|
92
|
+
type: "boolean",
|
|
93
|
+
description: "Allow selecting multiple choices",
|
|
94
|
+
},
|
|
95
|
+
custom: {
|
|
96
|
+
type: "boolean",
|
|
97
|
+
description: "Allow typing a custom answer (default: true)",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
required: ["question"],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
question: {
|
|
104
|
+
type: "string",
|
|
105
|
+
description: "Legacy single free-text question",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
handler: async (ctx, args) => {
|
|
110
|
+
if (ctx.exitOnComplete) {
|
|
111
|
+
return { success: false, output: t("tool.interactive_disabled") };
|
|
112
|
+
}
|
|
113
|
+
const questions = normalizeQuestions(args);
|
|
114
|
+
if (questions.length === 0) {
|
|
115
|
+
return { success: false, output: t("tool.question.no_questions") };
|
|
116
|
+
}
|
|
117
|
+
const answers = [];
|
|
118
|
+
for (let i = 0; i < questions.length; i++) {
|
|
119
|
+
const q = questions[i];
|
|
120
|
+
const progress = questions.length > 1
|
|
121
|
+
? t("tool.question.progress", {
|
|
122
|
+
current: i + 1,
|
|
123
|
+
total: questions.length,
|
|
124
|
+
})
|
|
125
|
+
: undefined;
|
|
126
|
+
answers.push(await askUser(q.question, {
|
|
127
|
+
header: q.header,
|
|
128
|
+
options: q.options,
|
|
129
|
+
multiple: q.multiple,
|
|
130
|
+
custom: q.custom,
|
|
131
|
+
progress,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
const formatted = questions
|
|
135
|
+
.map((q, i) => `"${q.question}"="${answers[i].length ? answers[i].join(", ") : t("tool.question.unanswered")}"`)
|
|
136
|
+
.join(", ");
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
output: t("tool.question.answered", { formatted }),
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
};
|