micro-models-agent 0.7.10 → 0.8.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 +123 -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 +307 -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/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 +108 -0
- package/dist/modules/security/audit-notifier.js +292 -0
- package/dist/modules/security/command-validator.js +91 -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 +218 -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 +77 -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 +51 -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 +39 -0
- package/dist/tools/question.js +142 -0
- package/dist/tools/read-file.js +65 -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 +130 -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,51 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { t } from '../i18n/index';
|
|
4
|
+
import { isCommandAllowed, sanitizeCommandForLog } from '../modules/security/command-validator';
|
|
5
|
+
import { logSecurityBlock } from '../modules/security/audit-log';
|
|
6
|
+
export const grepTool = {
|
|
7
|
+
name: 'grep',
|
|
8
|
+
description: 'Search file contents using a regular expression. Uses ripgrep (rg) if available, otherwise falls back to grep -r.',
|
|
9
|
+
tags: ['file', 'code', 'research'],
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
pattern: { type: 'string', description: 'Regex pattern to search' },
|
|
14
|
+
include: { type: 'string', description: 'File pattern to filter (e.g. *.ts)' },
|
|
15
|
+
path: { type: 'string', description: 'Directory to search (defaults to baseDir)' },
|
|
16
|
+
},
|
|
17
|
+
required: ['pattern'],
|
|
18
|
+
},
|
|
19
|
+
handler: async (ctx, args) => {
|
|
20
|
+
const pattern = String(args.pattern);
|
|
21
|
+
const searchPath = args.path ? resolve(ctx.baseDir, String(args.path)) : ctx.baseDir;
|
|
22
|
+
// Build grep command
|
|
23
|
+
let cmd = `rg -n "${pattern.replace(/"/g, '\\"')}" "${searchPath}"`;
|
|
24
|
+
if (args.include) {
|
|
25
|
+
cmd += ` -g "${String(args.include)}"`;
|
|
26
|
+
}
|
|
27
|
+
// Security check: validate command (grep/rg should be allowed)
|
|
28
|
+
const securityConfig = ctx.config.security?.bash;
|
|
29
|
+
const validation = isCommandAllowed(cmd, securityConfig);
|
|
30
|
+
if (!validation.allowed) {
|
|
31
|
+
logSecurityBlock(ctx.sessionId, "bash_command", validation.reason || "Command blocked by security policy", sanitizeCommandForLog(cmd));
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
output: `[SECURITY BLOCKED] Command is not allowed: ${validation.reason}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const output = execSync(cmd, {
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
maxBuffer: 1024 * 1024,
|
|
41
|
+
cwd: ctx.baseDir,
|
|
42
|
+
});
|
|
43
|
+
return { success: true, output: output || t('file.no_matches') };
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (e.status === 1)
|
|
47
|
+
return { success: true, output: t('file.no_matches') };
|
|
48
|
+
return { success: false, output: t('error.grep_failed', { message: e.message }) };
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -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,39 @@
|
|
|
1
|
+
import { t } from '../i18n/index';
|
|
2
|
+
import { PipelineEngine } from '../modules/pipelines/engine';
|
|
3
|
+
import { PipelineParser } from '../modules/pipelines/parser';
|
|
4
|
+
const engine = new PipelineEngine();
|
|
5
|
+
export const pipelineRunTool = {
|
|
6
|
+
name: 'pipeline_run',
|
|
7
|
+
description: 'Run a named pipeline with YAML definition. Creates a DAG of sub-agents that execute in dependency order.',
|
|
8
|
+
tags: ['shell', 'code'],
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
name: { type: 'string', description: 'Pipeline name' },
|
|
13
|
+
yaml: { type: 'string', description: 'Pipeline YAML definition with steps' },
|
|
14
|
+
},
|
|
15
|
+
required: ['name', 'yaml'],
|
|
16
|
+
},
|
|
17
|
+
handler: async (_ctx, args) => {
|
|
18
|
+
const name = String(args.name || '');
|
|
19
|
+
const yaml = String(args.yaml || '');
|
|
20
|
+
if (!yaml) {
|
|
21
|
+
return { success: false, output: t('pipeline.invalid') };
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const pipeline = PipelineParser.parse(yaml);
|
|
25
|
+
const order = engine.resolveDependencies(pipeline.steps);
|
|
26
|
+
const readySteps = engine.getReadySteps(pipeline.steps, new Set());
|
|
27
|
+
const summary = [
|
|
28
|
+
`Pipeline "${pipeline.name}" parsed successfully.`,
|
|
29
|
+
`Steps: ${pipeline.steps.length}`,
|
|
30
|
+
`Execution order: ${order.join(' → ')}`,
|
|
31
|
+
`Ready to run: ${readySteps.map(s => s.id).join(', ') || '(none)'}`,
|
|
32
|
+
].join('\n');
|
|
33
|
+
return { success: true, output: summary };
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
return { success: false, output: `Pipeline error: ${e.message}` };
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { resolve, normalize, extname } from "path";
|
|
3
|
+
import { t } from "../i18n/index";
|
|
4
|
+
import { isPathInScope } from "../modules/security/path-validator";
|
|
5
|
+
/** Default number of lines returned when the caller omits `limit`. */
|
|
6
|
+
const DEFAULT_LIMIT = 300;
|
|
7
|
+
export const readFileTool = {
|
|
8
|
+
name: "read_file",
|
|
9
|
+
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.`,
|
|
10
|
+
tags: ["file", "code"],
|
|
11
|
+
parameters: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
path: { type: "string", description: "File path to read" },
|
|
15
|
+
offset: {
|
|
16
|
+
type: "number",
|
|
17
|
+
description: `Starting line (1-indexed). Use offset=<next> from a truncated result to continue reading.`,
|
|
18
|
+
},
|
|
19
|
+
limit: {
|
|
20
|
+
type: "number",
|
|
21
|
+
description: `Number of lines to read (default ${DEFAULT_LIMIT})`,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ["path"],
|
|
25
|
+
},
|
|
26
|
+
handler: async (ctx, args) => {
|
|
27
|
+
const path = String(args.path);
|
|
28
|
+
const scopeCheck = isPathInScope(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
|
|
29
|
+
if (!scopeCheck.allowed) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
output: t("file.path_not_allowed", {
|
|
33
|
+
path: `${path} — ${scopeCheck.reason}`,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const resolved = resolve(ctx.baseDir, normalize(path));
|
|
38
|
+
if (!existsSync(resolved)) {
|
|
39
|
+
return { success: false, output: t("file.notfound", { path }) };
|
|
40
|
+
}
|
|
41
|
+
ctx.trackReadPath?.(path);
|
|
42
|
+
const content = readFileSync(resolved, "utf-8");
|
|
43
|
+
const lines = content.split("\n");
|
|
44
|
+
const total = lines.length;
|
|
45
|
+
const offset = args.offset || 1;
|
|
46
|
+
const limit = args.limit || DEFAULT_LIMIT;
|
|
47
|
+
const end = Math.min(offset - 1 + limit, total);
|
|
48
|
+
const selected = lines.slice(offset - 1, end).join("\n");
|
|
49
|
+
const ext = extname(resolved) || "(no extension)";
|
|
50
|
+
const header = t("file.read_header", {
|
|
51
|
+
path,
|
|
52
|
+
ext,
|
|
53
|
+
total: String(total),
|
|
54
|
+
from: String(offset),
|
|
55
|
+
to: String(end),
|
|
56
|
+
});
|
|
57
|
+
const truncated = end < total
|
|
58
|
+
? t("file.read_truncated", {
|
|
59
|
+
remaining: String(total - end),
|
|
60
|
+
next: String(end + 1),
|
|
61
|
+
})
|
|
62
|
+
: "";
|
|
63
|
+
return { success: true, output: `${header}\n${selected}${truncated}` };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class ToolRegistry {
|
|
2
|
+
tools = new Map();
|
|
3
|
+
register(tool) {
|
|
4
|
+
if (this.tools.has(tool.name)) {
|
|
5
|
+
throw new Error(`Tool already registered: ${tool.name}`);
|
|
6
|
+
}
|
|
7
|
+
this.tools.set(tool.name, tool);
|
|
8
|
+
}
|
|
9
|
+
get(name) {
|
|
10
|
+
return this.tools.get(name);
|
|
11
|
+
}
|
|
12
|
+
has(name) {
|
|
13
|
+
return this.tools.has(name);
|
|
14
|
+
}
|
|
15
|
+
list() {
|
|
16
|
+
return Array.from(this.tools.keys()).sort();
|
|
17
|
+
}
|
|
18
|
+
getAll() {
|
|
19
|
+
return Array.from(this.tools.values());
|
|
20
|
+
}
|
|
21
|
+
getByTags(tags) {
|
|
22
|
+
return this.getAll().filter(t => {
|
|
23
|
+
if (!t.tags || t.tags.length === 0)
|
|
24
|
+
return false;
|
|
25
|
+
return tags.some(tag => t.tags.includes(tag));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
getAllForLLM(tags) {
|
|
29
|
+
const tools = tags ? this.getByTags(tags) : this.getAll();
|
|
30
|
+
return tools.map(t => ({
|
|
31
|
+
name: t.name,
|
|
32
|
+
description: t.description,
|
|
33
|
+
parameters: t.parameters,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { resolve, normalize } from 'path';
|
|
2
|
+
export function isPathInScope(baseDir, targetPath, scope) {
|
|
3
|
+
const baseResolved = resolve(baseDir);
|
|
4
|
+
const targetResolved = resolve(baseDir, normalize(targetPath));
|
|
5
|
+
if (!targetResolved.startsWith(baseResolved)) {
|
|
6
|
+
return { allowed: false, reason: 'Path is outside the base working directory' };
|
|
7
|
+
}
|
|
8
|
+
if (!scope)
|
|
9
|
+
return { allowed: true };
|
|
10
|
+
const isRead = scope.read_only_files.some(f => targetResolved.startsWith(resolve(baseDir, f)));
|
|
11
|
+
const isWrite = scope.allowed_files.some(f => targetResolved.startsWith(resolve(baseDir, f)));
|
|
12
|
+
if (isWrite)
|
|
13
|
+
return { allowed: true };
|
|
14
|
+
if (isRead)
|
|
15
|
+
return { allowed: true, reason: 'Read-only file' };
|
|
16
|
+
return { allowed: false, reason: 'Path is not within the allowed scope for this sub-agent' };
|
|
17
|
+
}
|
|
18
|
+
export function isPathWritable(baseDir, targetPath, scope) {
|
|
19
|
+
const baseResolved = resolve(baseDir);
|
|
20
|
+
const targetResolved = resolve(baseDir, normalize(targetPath));
|
|
21
|
+
if (!targetResolved.startsWith(baseResolved)) {
|
|
22
|
+
return { allowed: false, reason: 'Path is outside the base working directory' };
|
|
23
|
+
}
|
|
24
|
+
if (!scope)
|
|
25
|
+
return { allowed: true };
|
|
26
|
+
const isWrite = scope.allowed_files.some(f => targetResolved.startsWith(resolve(baseDir, f)));
|
|
27
|
+
if (isWrite)
|
|
28
|
+
return { allowed: true };
|
|
29
|
+
return { allowed: false, reason: 'Path is not within the writable scope for this sub-agent' };
|
|
30
|
+
}
|