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.
Files changed (151) hide show
  1. package/dist/cli/commands.js +173 -0
  2. package/dist/cli/completer.js +168 -0
  3. package/dist/cli/index.js +2 -0
  4. package/dist/cli/main.js +95 -0
  5. package/dist/cli/repl.js +762 -0
  6. package/dist/cli/security-commands.js +166 -0
  7. package/dist/cli/setup.js +214 -0
  8. package/dist/config/config.js +186 -0
  9. package/dist/config/defaults.js +91 -0
  10. package/dist/config/experts.js +15 -0
  11. package/dist/config/index.js +3 -0
  12. package/dist/config/security.js +187 -0
  13. package/dist/config/types.js +1 -0
  14. package/dist/core/agent.js +626 -0
  15. package/dist/core/bootstrap.js +317 -0
  16. package/dist/core/index.js +2 -0
  17. package/dist/core/prompt-builder.js +55 -0
  18. package/dist/core/types.js +1 -0
  19. package/dist/i18n/en.json +405 -0
  20. package/dist/i18n/index.js +43 -0
  21. package/dist/i18n/ru.json +405 -0
  22. package/dist/index.js +22 -0
  23. package/dist/llm/index.js +4 -0
  24. package/dist/llm/model-loader.js +78 -0
  25. package/dist/llm/openai-compat.js +277 -0
  26. package/dist/llm/orchestrator.js +194 -0
  27. package/dist/llm/provider.js +2 -0
  28. package/dist/llm/response.js +39 -0
  29. package/dist/llm/token-counter.js +37 -0
  30. package/dist/llm/types.js +1 -0
  31. package/dist/logger/app-logger.js +76 -0
  32. package/dist/logger/index.js +1 -0
  33. package/dist/main.js +15904 -0
  34. package/dist/migration/backup.js +45 -0
  35. package/dist/migration/detect.js +50 -0
  36. package/dist/migration/index.js +2 -0
  37. package/dist/modules/browser/actions.js +46 -0
  38. package/dist/modules/browser/cookie-store.js +24 -0
  39. package/dist/modules/browser/index.js +5 -0
  40. package/dist/modules/browser/module.js +28 -0
  41. package/dist/modules/browser/session.js +287 -0
  42. package/dist/modules/browser/snapshot.js +114 -0
  43. package/dist/modules/browser/types.js +9 -0
  44. package/dist/modules/context/history.js +15 -0
  45. package/dist/modules/context/index.js +1 -0
  46. package/dist/modules/context/manager.js +179 -0
  47. package/dist/modules/execution/auditor.js +72 -0
  48. package/dist/modules/execution/index.js +6 -0
  49. package/dist/modules/execution/module.js +334 -0
  50. package/dist/modules/execution/moe-executor.js +196 -0
  51. package/dist/modules/execution/plan-validator.js +153 -0
  52. package/dist/modules/execution/planner.js +35 -0
  53. package/dist/modules/execution/stuck-detector.js +113 -0
  54. package/dist/modules/execution/tracker.js +53 -0
  55. package/dist/modules/execution/types.js +1 -0
  56. package/dist/modules/execution/verifier.js +149 -0
  57. package/dist/modules/hallucination/confidence.js +47 -0
  58. package/dist/modules/hallucination/consistency.js +32 -0
  59. package/dist/modules/hallucination/detector.js +41 -0
  60. package/dist/modules/hallucination/factual.js +128 -0
  61. package/dist/modules/hallucination/index.js +4 -0
  62. package/dist/modules/index.js +5 -0
  63. package/dist/modules/indexer/cache.js +38 -0
  64. package/dist/modules/indexer/index.js +3 -0
  65. package/dist/modules/indexer/module.js +192 -0
  66. package/dist/modules/indexer/walker.js +101 -0
  67. package/dist/modules/mcp/client.js +393 -0
  68. package/dist/modules/mcp/index.js +3 -0
  69. package/dist/modules/mcp/module.js +146 -0
  70. package/dist/modules/mcp/registry.js +15 -0
  71. package/dist/modules/memory/index.js +1 -0
  72. package/dist/modules/memory/search.js +26 -0
  73. package/dist/modules/memory/store.js +38 -0
  74. package/dist/modules/pipelines/engine.js +60 -0
  75. package/dist/modules/pipelines/index.js +3 -0
  76. package/dist/modules/pipelines/parser.js +53 -0
  77. package/dist/modules/pipelines/template.js +14 -0
  78. package/dist/modules/plugins/builtin/lint-on-write.js +121 -0
  79. package/dist/modules/plugins/builtin/notify.js +8 -0
  80. package/dist/modules/plugins/index.js +1 -0
  81. package/dist/modules/plugins/loader.js +28 -0
  82. package/dist/modules/plugins/manager.js +161 -0
  83. package/dist/modules/plugins/types.js +1 -0
  84. package/dist/modules/registry.js +45 -0
  85. package/dist/modules/security/audit-log.js +116 -0
  86. package/dist/modules/security/audit-notifier.js +292 -0
  87. package/dist/modules/security/command-validator.js +104 -0
  88. package/dist/modules/security/content-scanner.js +52 -0
  89. package/dist/modules/security/data-sanitizer.js +97 -0
  90. package/dist/modules/security/encryption.js +238 -0
  91. package/dist/modules/security/index.js +14 -0
  92. package/dist/modules/security/network-validator.js +79 -0
  93. package/dist/modules/security/path-validator.js +155 -0
  94. package/dist/modules/security/rate-limiter.js +119 -0
  95. package/dist/modules/security/security-policies.js +393 -0
  96. package/dist/modules/security/session-encryption.js +193 -0
  97. package/dist/modules/security/session-isolation.js +95 -0
  98. package/dist/modules/session/index.js +3 -0
  99. package/dist/modules/session/manager.js +167 -0
  100. package/dist/modules/session/module.js +28 -0
  101. package/dist/modules/session/store.js +174 -0
  102. package/dist/modules/session/types.js +1 -0
  103. package/dist/modules/skills/index.js +3 -0
  104. package/dist/modules/skills/loader.js +72 -0
  105. package/dist/modules/skills/matcher.js +27 -0
  106. package/dist/modules/skills/module.js +180 -0
  107. package/dist/modules/types.js +1 -0
  108. package/dist/modules/updater/checker.js +32 -0
  109. package/dist/modules/updater/index.js +1 -0
  110. package/dist/modules/user-profile/compressor.js +16 -0
  111. package/dist/modules/user-profile/index.js +1 -0
  112. package/dist/modules/user-profile/profile.js +68 -0
  113. package/dist/tools/approve.js +32 -0
  114. package/dist/tools/bash.js +80 -0
  115. package/dist/tools/browser.js +97 -0
  116. package/dist/tools/create-dir.js +57 -0
  117. package/dist/tools/delete-file.js +64 -0
  118. package/dist/tools/edit-file.js +78 -0
  119. package/dist/tools/executor.js +83 -0
  120. package/dist/tools/file-info.js +46 -0
  121. package/dist/tools/filter-tools.js +10 -0
  122. package/dist/tools/glob-tool.js +19 -0
  123. package/dist/tools/grep-tool.js +57 -0
  124. package/dist/tools/index.js +44 -0
  125. package/dist/tools/list-dir.js +40 -0
  126. package/dist/tools/load-skill.js +48 -0
  127. package/dist/tools/mcp-call.js +68 -0
  128. package/dist/tools/move-file.js +84 -0
  129. package/dist/tools/pipeline-run.js +144 -0
  130. package/dist/tools/question.js +142 -0
  131. package/dist/tools/read-file.js +70 -0
  132. package/dist/tools/registry.js +36 -0
  133. package/dist/tools/scope-check.js +30 -0
  134. package/dist/tools/search-history.js +64 -0
  135. package/dist/tools/subagent.js +142 -0
  136. package/dist/tools/types.js +1 -0
  137. package/dist/tools/user-input.js +123 -0
  138. package/dist/tools/web-browse.js +51 -0
  139. package/dist/tools/web-fetch.js +62 -0
  140. package/dist/tools/web-search.js +59 -0
  141. package/dist/tools/write-file.js +80 -0
  142. package/dist/ui/box.js +81 -0
  143. package/dist/ui/colors.js +4 -0
  144. package/dist/ui/diff.js +185 -0
  145. package/dist/ui/index.js +6 -0
  146. package/dist/ui/md-formatter.js +212 -0
  147. package/dist/ui/output.js +13 -0
  148. package/dist/ui/renderer.js +141 -0
  149. package/dist/ui/spinner.js +70 -0
  150. package/dist/ui/table.js +144 -0
  151. package/package.json +1 -1
@@ -0,0 +1,16 @@
1
+ export class ProfileCompressor {
2
+ compress(info) {
3
+ const parts = [
4
+ `OS: ${info.os}`,
5
+ `Shell: ${info.shell}`,
6
+ `Platform: ${info.platform}`,
7
+ ];
8
+ if (Object.keys(info.preferences).length > 0) {
9
+ const prefs = Object.entries(info.preferences)
10
+ .map(([k, v]) => `${k}=${v}`)
11
+ .join(',');
12
+ parts.push(`Prefs: ${prefs}`);
13
+ }
14
+ return parts.join(', ');
15
+ }
16
+ }
@@ -0,0 +1 @@
1
+ export { UserProfile } from './profile';
@@ -0,0 +1,68 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir, hostname, platform, type } from 'os';
4
+ import { env } from 'process';
5
+ import { ProfileCompressor } from './compressor';
6
+ export class UserProfile {
7
+ profileDir;
8
+ info = null;
9
+ preferences = {};
10
+ constructor(profileDir) {
11
+ this.profileDir = profileDir;
12
+ }
13
+ collect() {
14
+ this.info = {
15
+ platform: platform(),
16
+ os: `${type()} ${hostname()}`,
17
+ hostname: hostname(),
18
+ shell: env.SHELL || env.ComSpec || 'unknown',
19
+ home: homedir(),
20
+ nodeVersion: process.version,
21
+ preferences: { ...this.preferences },
22
+ };
23
+ return this.info;
24
+ }
25
+ save() {
26
+ if (!existsSync(this.profileDir)) {
27
+ mkdirSync(this.profileDir, { recursive: true });
28
+ }
29
+ writeFileSync(join(this.profileDir, 'profile.json'), JSON.stringify({ ...this.info, preferences: this.preferences }, null, 2), 'utf-8');
30
+ }
31
+ load() {
32
+ const path = join(this.profileDir, 'profile.json');
33
+ if (!existsSync(path))
34
+ return null;
35
+ try {
36
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
37
+ this.info = {
38
+ platform: data.platform,
39
+ os: data.os,
40
+ hostname: data.hostname,
41
+ shell: data.shell,
42
+ home: data.home,
43
+ nodeVersion: data.nodeVersion,
44
+ preferences: data.preferences || {},
45
+ };
46
+ this.preferences = data.preferences || {};
47
+ return this.info;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ getInfo() {
54
+ return this.info;
55
+ }
56
+ setPreference(key, value) {
57
+ this.preferences[key] = value;
58
+ }
59
+ getPreference(key) {
60
+ return this.preferences[key];
61
+ }
62
+ compress() {
63
+ if (!this.info)
64
+ this.collect();
65
+ const compressor = new ProfileCompressor();
66
+ return compressor.compress(this.info);
67
+ }
68
+ }
@@ -0,0 +1,32 @@
1
+ import { t } from "../i18n/index";
2
+ import { askChoice } from "./user-input";
3
+ export const approveTool = {
4
+ name: "approve",
5
+ description: "Request user approval for an action. The user picks Yes or No from a menu.",
6
+ tags: ["core"],
7
+ interactive: true,
8
+ parameters: {
9
+ type: "object",
10
+ properties: {
11
+ action: {
12
+ type: "string",
13
+ description: "Description of the action requiring approval",
14
+ },
15
+ },
16
+ required: ["action"],
17
+ },
18
+ handler: async (ctx, args) => {
19
+ if (ctx.exitOnComplete) {
20
+ return { success: false, output: t("tool.interactive_disabled") };
21
+ }
22
+ const action = String(args.action || "");
23
+ const indexes = await askChoice(t("tool.approve_prompt", { action }), [
24
+ { label: t("tool.approve_yes"), description: t("tool.approve_yes_desc") },
25
+ { label: t("tool.approve_no"), description: t("tool.approve_no_desc") },
26
+ ]);
27
+ if (indexes[0] === 0) {
28
+ return { success: true, output: t("tool.approved") };
29
+ }
30
+ return { success: false, output: t("tool.rejected") };
31
+ },
32
+ };
@@ -0,0 +1,80 @@
1
+ import { execSync } from 'child_process';
2
+ import { platform } from 'os';
3
+ import { isCommandAllowed, sanitizeCommandForLog } from '../modules/security/command-validator';
4
+ import { logBashCommand, logSecurityBlock } from '../modules/security/audit-log';
5
+ import { getSessionSecurityConfig } from '../modules/security/session-isolation';
6
+ import { DEFAULT_SECURITY_CONFIG } from '../config/security';
7
+ function adaptCommandForWindows(command) {
8
+ if (platform() !== 'win32')
9
+ return command;
10
+ // Windows mkdir does not support -p flag, but creates intermediate dirs by default
11
+ const trimmed = command.trim();
12
+ if (trimmed.startsWith('mkdir -p ')) {
13
+ return trimmed.replace(/^mkdir -p /, 'mkdir ');
14
+ }
15
+ if (trimmed === 'mkdir -p' || trimmed.startsWith('mkdir -p ')) {
16
+ return trimmed.replace(/mkdir -p/g, 'mkdir');
17
+ }
18
+ return command;
19
+ }
20
+ export const bashTool = {
21
+ name: 'bash',
22
+ description: 'Execute a shell command and return its output. Use for running tests, build, git, and shell operations.',
23
+ tags: ['shell', 'code'],
24
+ parameters: {
25
+ type: 'object',
26
+ properties: {
27
+ command: { type: 'string', description: 'Shell command to execute' },
28
+ workdir: { type: 'string', description: 'Working directory (default: baseDir)' },
29
+ },
30
+ required: ['command'],
31
+ },
32
+ handler: async (ctx, args) => {
33
+ const originalCommand = String(args.command);
34
+ const command = adaptCommandForWindows(originalCommand);
35
+ const workdir = args.workdir ? String(args.workdir) : ctx.baseDir;
36
+ // Get session-specific security config with defaults
37
+ const appConfig = ctx.config || {};
38
+ const fullSecurityConfig = ctx.sessionContext
39
+ ? getSessionSecurityConfig(appConfig, ctx.sessionContext)
40
+ : appConfig.security || DEFAULT_SECURITY_CONFIG;
41
+ const securityConfig = fullSecurityConfig.bash || DEFAULT_SECURITY_CONFIG.bash;
42
+ const validation = isCommandAllowed(command, securityConfig);
43
+ if (!validation.allowed) {
44
+ // Log security block
45
+ logSecurityBlock(ctx.sessionId, "bash_command", validation.reason || "Command blocked by security policy", sanitizeCommandForLog(originalCommand));
46
+ return {
47
+ success: false,
48
+ output: `[SECURITY BLOCKED] Command is not allowed: ${validation.reason}`,
49
+ };
50
+ }
51
+ // Log command execution if enabled
52
+ if (securityConfig?.logCommands) {
53
+ logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, // Will be updated after execution
54
+ `Working directory: ${workdir}`);
55
+ }
56
+ try {
57
+ const output = execSync(command, {
58
+ cwd: workdir,
59
+ encoding: 'utf-8',
60
+ maxBuffer: 10 * 1024 * 1024,
61
+ timeout: 120_000,
62
+ });
63
+ // Update audit log with success
64
+ if (securityConfig?.logCommands) {
65
+ logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), true, `Working directory: ${workdir}, Output length: ${output.length}`);
66
+ }
67
+ return { success: true, output: output.trimEnd() };
68
+ }
69
+ catch (e) {
70
+ const stderr = e.stderr?.toString() || '';
71
+ const stdout = e.stdout?.toString() || '';
72
+ const errorOutput = stderr || stdout || e.message;
73
+ // Update audit log with failure
74
+ if (securityConfig?.logCommands) {
75
+ logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, `Working directory: ${workdir}, Error: ${errorOutput.slice(0, 100)}`);
76
+ }
77
+ return { success: false, output: errorOutput };
78
+ }
79
+ },
80
+ };
@@ -0,0 +1,97 @@
1
+ import { BrowserSession } from '../modules/browser/session';
2
+ import { DEFAULT_BROWSER_CONFIG } from '../modules/browser/types';
3
+ import { t } from '../i18n/index';
4
+ import { join } from 'path';
5
+ let session = null;
6
+ function getSession(ctx) {
7
+ if (!session) {
8
+ const cookieDir = join(ctx.baseDir, '.mma', 'browser');
9
+ session = new BrowserSession({
10
+ ...DEFAULT_BROWSER_CONFIG,
11
+ headless: ctx.config.browser?.headless ?? true,
12
+ maxElements: ctx.config.browser?.maxElements ?? 30,
13
+ navigationTimeout: ctx.config.browser?.navigationTimeout ?? 15000,
14
+ viewportWidth: ctx.config.browser?.viewportWidth ?? 1280,
15
+ viewportHeight: ctx.config.browser?.viewportHeight ?? 720,
16
+ cookieDir,
17
+ });
18
+ }
19
+ return session;
20
+ }
21
+ export function formatScreenshotForTextModel(snapshot) {
22
+ return `${t('tool.screenshot_unavailable')}\n\n${snapshot}`;
23
+ }
24
+ export function formatScreenshotResult(snapshot, screenshot, supportsVision) {
25
+ if (!supportsVision) {
26
+ return formatScreenshotForTextModel(snapshot);
27
+ }
28
+ const base64 = screenshot.toString('base64');
29
+ return `${snapshot}\n\n[Screenshot: data:image/png;base64,${base64}]`;
30
+ }
31
+ export function createBrowserTool() {
32
+ return {
33
+ name: 'browser',
34
+ tags: ['browser', 'vision'],
35
+ description: [
36
+ 'Control a headless browser. Navigate pages, click elements, type text, scroll, take screenshots.',
37
+ 'Returns a numbered list of interactive elements — use element numbers as targets for click/type.',
38
+ 'Actions: open (url), click (target), type (target, text), scroll (direction), back, forward, screenshot, snapshot, close, wait (ms).',
39
+ ].join(' '),
40
+ parameters: {
41
+ type: 'object',
42
+ properties: {
43
+ action: {
44
+ type: 'string',
45
+ enum: ['open', 'click', 'type', 'scroll', 'back', 'forward', 'screenshot', 'snapshot', 'close', 'wait'],
46
+ description: 'Browser action to perform',
47
+ },
48
+ url: {
49
+ type: 'string',
50
+ description: 'URL to open (for action "open")',
51
+ },
52
+ target: {
53
+ type: 'number',
54
+ description: 'Element number from snapshot (for action "click" or "type")',
55
+ },
56
+ text: {
57
+ type: 'string',
58
+ description: 'Text to type (for action "type")',
59
+ },
60
+ direction: {
61
+ type: 'string',
62
+ enum: ['up', 'down', 'top', 'bottom'],
63
+ description: 'Scroll direction (for action "scroll", default "down")',
64
+ },
65
+ ms: {
66
+ type: 'number',
67
+ description: 'Milliseconds to wait (for action "wait", default 1000)',
68
+ },
69
+ },
70
+ required: ['action'],
71
+ },
72
+ handler: async (ctx, args) => {
73
+ const action = String(args.action || '');
74
+ if (!action) {
75
+ return { success: false, output: t('tool.action_required') };
76
+ }
77
+ const s = getSession(ctx);
78
+ const result = await s.execute(action, args);
79
+ const supportsVision = ctx.config.model.includes('vision') ||
80
+ ctx.config.model.includes('gpt-4o') ||
81
+ ctx.config.model.includes('claude');
82
+ if (result.screenshot) {
83
+ return {
84
+ success: result.success,
85
+ output: formatScreenshotResult(result.output, result.screenshot, supportsVision),
86
+ };
87
+ }
88
+ return { success: result.success, output: result.output };
89
+ },
90
+ };
91
+ }
92
+ export function closeBrowserSession() {
93
+ if (session) {
94
+ session.close().catch(() => { });
95
+ session = null;
96
+ }
97
+ }
@@ -0,0 +1,57 @@
1
+ import { mkdirSync, existsSync } from "fs";
2
+ import { resolve, normalize } 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
+ export const createDirTool = {
8
+ name: "create_dir",
9
+ description: "Create a directory (and any intermediate directories).",
10
+ tags: ["file"],
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ path: { type: "string", description: "Directory path" },
15
+ },
16
+ required: ["path"],
17
+ },
18
+ handler: async (ctx, args) => {
19
+ const path = String(args.path);
20
+ const baseDir = resolve(ctx.baseDir);
21
+ const resolved = resolve(baseDir, normalize(path));
22
+ // Get session-specific security config
23
+ const securityConfig = ctx.sessionContext
24
+ ? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
25
+ : ctx.config.security;
26
+ // Check file operations limit
27
+ const maxFileOps = securityConfig?.maxFileOperations ?? 100;
28
+ const currentCount = ctx.fileOperationsCount ?? 0;
29
+ if (currentCount >= maxFileOps) {
30
+ logSecurityBlock(ctx.sessionId, "file_write", `Maximum file operations (${maxFileOps}) exceeded`, path);
31
+ return {
32
+ success: false,
33
+ output: `[SECURITY BLOCKED] Maximum file operations (${maxFileOps}) exceeded`,
34
+ };
35
+ }
36
+ // Check path permissions
37
+ const scopeCheck = isPathWritable(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
38
+ if (!scopeCheck.allowed) {
39
+ logSecurityBlock(ctx.sessionId, "file_write", scopeCheck.reason || "Path not allowed", path);
40
+ return {
41
+ success: false,
42
+ output: t("file.path_not_allowed", {
43
+ path: `${path} — ${scopeCheck.reason}`,
44
+ }),
45
+ };
46
+ }
47
+ if (!existsSync(resolved)) {
48
+ mkdirSync(resolved, { recursive: true });
49
+ }
50
+ // Increment file operations counter
51
+ ctx.fileOperationsCount = currentCount + 1;
52
+ // Log directory creation
53
+ logFileWrite(ctx.sessionId, path, true, "Directory created");
54
+ ctx.trackCreatedPath?.(path);
55
+ return { success: true, output: t("file.created", { path }) };
56
+ },
57
+ };
@@ -0,0 +1,64 @@
1
+ import { unlinkSync, existsSync, statSync, readFileSync } from "fs";
2
+ import { resolve, normalize } from "path";
3
+ import { t } from "../i18n/index";
4
+ import { isPathWritable } from "../modules/security/path-validator";
5
+ import { logFileDelete, logSecurityBlock } from "../modules/security/audit-log";
6
+ import { getSessionSecurityConfig } from "../modules/security/session-isolation";
7
+ import { generateDeleteDiff } from "../ui/diff";
8
+ export const deleteFileTool = {
9
+ name: "delete_file",
10
+ description: "Delete a file from the filesystem.",
11
+ tags: ["file"],
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ path: { type: "string", description: "File path" },
16
+ },
17
+ required: ["path"],
18
+ },
19
+ handler: async (ctx, args) => {
20
+ const path = String(args.path);
21
+ const baseDir = resolve(ctx.baseDir);
22
+ const resolved = resolve(baseDir, normalize(path));
23
+ // Get session-specific security config
24
+ const securityConfig = ctx.sessionContext
25
+ ? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
26
+ : ctx.config.security;
27
+ // Check file operations limit
28
+ const maxFileOps = securityConfig?.maxFileOperations ?? 100;
29
+ const currentCount = ctx.fileOperationsCount ?? 0;
30
+ if (currentCount >= maxFileOps) {
31
+ logSecurityBlock(ctx.sessionId, "file_delete", `Maximum file operations (${maxFileOps}) exceeded`, path);
32
+ return {
33
+ success: false,
34
+ output: `[SECURITY BLOCKED] Maximum file operations (${maxFileOps}) exceeded`,
35
+ };
36
+ }
37
+ // Check path permissions
38
+ const scopeCheck = isPathWritable(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
39
+ if (!scopeCheck.allowed) {
40
+ logSecurityBlock(ctx.sessionId, "file_delete", scopeCheck.reason || "Path not allowed", path);
41
+ return {
42
+ success: false,
43
+ output: t("file.path_not_allowed", {
44
+ path: `${path} — ${scopeCheck.reason}`,
45
+ }),
46
+ };
47
+ }
48
+ if (!existsSync(resolved)) {
49
+ return { success: false, output: t("file.notfound", { path }) };
50
+ }
51
+ if (statSync(resolved).isDirectory()) {
52
+ return { success: false, output: t("file.is_directory", { path }) };
53
+ }
54
+ const content = readFileSync(resolved, "utf-8");
55
+ unlinkSync(resolved);
56
+ const diff = generateDeleteDiff(content);
57
+ // Increment file operations counter
58
+ ctx.fileOperationsCount = currentCount + 1;
59
+ // Log successful file deletion
60
+ logFileDelete(ctx.sessionId, path, true);
61
+ ctx.trackDeletedPath?.(path);
62
+ return { success: true, output: t("file.deleted", { path }), diff };
63
+ },
64
+ };
@@ -0,0 +1,78 @@
1
+ import { readFileSync, writeFileSync } from "fs";
2
+ import { resolve, normalize } from "path";
3
+ import { t } from "../i18n/index";
4
+ import { isPathWritable } from "../modules/security/path-validator";
5
+ import { scanContent } from "../modules/security/content-scanner";
6
+ import { logFileWrite, logSecurityBlock } from "../modules/security/audit-log";
7
+ import { getSessionSecurityConfig } from "../modules/security/session-isolation";
8
+ import { generateDiff } from "../ui/diff";
9
+ export const editFileTool = {
10
+ name: "edit_file",
11
+ description: "Find and replace text in an existing file. Uses exact string match (not regex).",
12
+ tags: ["file", "code"],
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ path: { type: "string", description: "File path" },
17
+ old: { type: "string", description: "Text to replace" },
18
+ new: { type: "string", description: "Replacement text" },
19
+ },
20
+ required: ["path", "old", "new"],
21
+ },
22
+ handler: async (ctx, args) => {
23
+ const path = String(args.path);
24
+ const baseDir = resolve(ctx.baseDir);
25
+ const resolved = resolve(baseDir, normalize(path));
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`, path);
35
+ return {
36
+ success: false,
37
+ output: `[SECURITY BLOCKED] Maximum file operations (${maxFileOps}) exceeded`,
38
+ };
39
+ }
40
+ // Check path permissions
41
+ const scopeCheck = isPathWritable(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
42
+ if (!scopeCheck.allowed) {
43
+ logSecurityBlock(ctx.sessionId, "file_write", scopeCheck.reason || "Path not allowed", path);
44
+ return {
45
+ success: false,
46
+ output: t("file.path_not_allowed", {
47
+ path: `${path} — ${scopeCheck.reason}`,
48
+ }),
49
+ };
50
+ }
51
+ const content = readFileSync(resolved, "utf-8");
52
+ const oldStr = String(args.old);
53
+ const newStr = String(args.new);
54
+ if (!content.includes(oldStr)) {
55
+ return {
56
+ success: false,
57
+ output: t("file.string_not_found", { str: oldStr }),
58
+ };
59
+ }
60
+ const updated = content.replace(oldStr, newStr);
61
+ // Check new content for dangerous patterns
62
+ const scanResult = scanContent(updated, path, ctx.config.security?.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
+ }
70
+ writeFileSync(resolved, updated, "utf-8");
71
+ const diff = generateDiff(content, updated);
72
+ // Increment file operations counter
73
+ ctx.fileOperationsCount = currentCount + 1;
74
+ // Log successful file edit
75
+ logFileWrite(ctx.sessionId, path, true, "File edited");
76
+ return { success: true, output: t("file.replaced_in", { path }), diff };
77
+ },
78
+ };
@@ -0,0 +1,83 @@
1
+ import { t } from "../i18n/index";
2
+ const TOOL_EXECUTION_TIMEOUT_MS = 60000;
3
+ export class ToolExecutor {
4
+ registry;
5
+ ctx;
6
+ pluginManager;
7
+ constructor(registry, ctx, pluginManager) {
8
+ this.registry = registry;
9
+ this.ctx = ctx;
10
+ this.pluginManager = pluginManager;
11
+ }
12
+ async execute(call) {
13
+ const tool = this.registry.get(call.name);
14
+ if (!tool) {
15
+ return {
16
+ success: false,
17
+ output: t("tool.unknown", { name: call.name }),
18
+ toolCallId: call.id,
19
+ };
20
+ }
21
+ for (const plugin of this.pluginManager.getAllPlugins()) {
22
+ if (plugin.onBeforeTool) {
23
+ try {
24
+ const proceed = await plugin.onBeforeTool(this.ctx, call);
25
+ if (!proceed) {
26
+ return {
27
+ success: false,
28
+ output: t("tool.blocked", {
29
+ plugin: plugin.constructor?.name || "unknown",
30
+ }),
31
+ toolCallId: call.id,
32
+ };
33
+ }
34
+ }
35
+ catch (e) {
36
+ this.ctx.logger.warn(`Plugin onBeforeTool error: ${e.message}`);
37
+ }
38
+ }
39
+ }
40
+ let result;
41
+ try {
42
+ if (tool.interactive) {
43
+ // Interactive tools wait for user input — no timeout.
44
+ result = await tool.handler(this.ctx, call.arguments);
45
+ }
46
+ else {
47
+ const timeoutPromise = new Promise((_, reject) => {
48
+ setTimeout(() => reject(new Error(t("tool.timeout", {
49
+ name: call.name,
50
+ seconds: TOOL_EXECUTION_TIMEOUT_MS / 1000,
51
+ }))), TOOL_EXECUTION_TIMEOUT_MS);
52
+ });
53
+ result = await Promise.race([
54
+ tool.handler(this.ctx, call.arguments),
55
+ timeoutPromise,
56
+ ]);
57
+ }
58
+ result.toolCallId = call.id;
59
+ }
60
+ catch (e) {
61
+ result = {
62
+ success: false,
63
+ output: `${t("error.prefix")}${e.message}`,
64
+ toolCallId: call.id,
65
+ };
66
+ }
67
+ for (const plugin of this.pluginManager.getAllPlugins()) {
68
+ if (plugin.onAfterTool) {
69
+ try {
70
+ await plugin.onAfterTool(this.ctx, call, result);
71
+ }
72
+ catch (e) {
73
+ this.ctx.logger.warn(`Plugin onAfterTool error: ${e.message}`);
74
+ }
75
+ }
76
+ }
77
+ this.ctx.logger.debug(`Tool ${call.name}: ${result.success ? "OK" : "FAIL"}`);
78
+ return result;
79
+ }
80
+ getToolDefinitions(tags) {
81
+ return this.registry.getAllForLLM(tags);
82
+ }
83
+ }
@@ -0,0 +1,46 @@
1
+ import { 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 fileInfoTool = {
6
+ name: 'file_info',
7
+ description: 'Get metadata about a file or directory (size, creation date, modification date).',
8
+ tags: ['file'],
9
+ parameters: {
10
+ type: 'object',
11
+ properties: {
12
+ path: { type: 'string', description: 'File 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.not_found_short', { path }) };
32
+ }
33
+ const stat = statSync(resolved);
34
+ return {
35
+ success: true,
36
+ output: JSON.stringify({
37
+ path,
38
+ size: stat.size,
39
+ isDirectory: stat.isDirectory(),
40
+ isFile: stat.isFile(),
41
+ created: stat.birthtime,
42
+ modified: stat.mtime,
43
+ }, null, 2),
44
+ };
45
+ },
46
+ };
@@ -0,0 +1,10 @@
1
+ export function filterToolsByTags(tools, toolTags) {
2
+ if (!toolTags || toolTags.length === 0)
3
+ return tools;
4
+ const tagSet = new Set(toolTags);
5
+ return tools.filter(t => {
6
+ if (!t.tags || t.tags.length === 0)
7
+ return false;
8
+ return t.tags.some(tag => tagSet.has(tag));
9
+ });
10
+ }
@@ -0,0 +1,19 @@
1
+ import { globSync } from 'fs';
2
+ import { t } from '../i18n/index';
3
+ export const globTool = {
4
+ name: 'glob',
5
+ description: 'Search for files matching a glob pattern. Uses standard glob syntax (e.g., **/*.ts, src/**/*.test.ts).',
6
+ tags: ['file', 'code', 'research'],
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ pattern: { type: 'string', description: 'Glob pattern' },
11
+ },
12
+ required: ['pattern'],
13
+ },
14
+ handler: async (ctx, args) => {
15
+ const pattern = String(args.pattern);
16
+ const results = globSync(pattern, { cwd: ctx.baseDir });
17
+ return { success: true, output: results.length > 0 ? results.join('\n') : t('file.no_matches') };
18
+ },
19
+ };