UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

138 lines 5.53 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { Box, Text } from 'ink'; import BashProgress from '../components/bash-progress.js'; import { isNanocoderToolAlwaysAllowed } from '../config/nanocoder-tools-config.js'; import { TRUNCATION_OUTPUT_LIMIT } from '../constants.js'; import { getCurrentMode } from '../context/mode-context.js'; import { useTheme } from '../hooks/useTheme.js'; import { bashExecutor } from '../services/bash-executor.js'; import { jsonSchema, tool } from '../types/core.js'; /** * Execute a bash command using the bash executor service. * This is the internal implementation used by both the tool and direct !command mode. * * @param command - The bash command to execute * @returns Object containing executionId and promise for the result */ export function executeBashCommand(command) { return bashExecutor.execute(command); } /** * Format bash execution result for LLM context */ export function formatBashResultForLLM(result) { let fullOutput = ''; const exitCodeInfo = result.exitCode !== null ? `EXIT_CODE: ${result.exitCode}\n` : ''; if (result.stderr) { fullOutput = `${exitCodeInfo}STDERR:\n${result.stderr}\nSTDOUT:\n${result.fullOutput}`; } else { fullOutput = `${exitCodeInfo}${result.fullOutput}`; } // Handle errors if (result.error) { fullOutput = `Error: ${result.error}\n${fullOutput}`; } // Limit the context for LLM to prevent overwhelming the model const llmContext = fullOutput.length > TRUNCATION_OUTPUT_LIMIT ? fullOutput.substring(0, TRUNCATION_OUTPUT_LIMIT) + '\n... [Output truncated. Use more specific commands to see full output]' : fullOutput; return llmContext; } /** * Tool execute function - called by the tool system * Note: For streaming tools, the tool handler will use executeBashCommand directly * and this function serves as a fallback/compatibility layer */ const executeExecuteBash = async (args) => { const { promise } = bashExecutor.execute(args.command); const result = await promise; return formatBashResultForLLM(result); }; const executeBashCoreTool = tool({ description: 'Execute a bash command and return the output (use for running commands)', inputSchema: jsonSchema({ type: 'object', properties: { command: { type: 'string', description: 'The bash command to execute.', }, }, required: ['command'], }), // High risk: bash commands require approval unless explicitly configured in nanocoderTools.alwaysAllow needsApproval: () => { // Check if this tool is configured to always be allowed if (isNanocoderToolAlwaysAllowed('execute_bash')) { return false; } // Scheduler mode auto-executes all tools including bash const mode = getCurrentMode(); if (mode === 'scheduler') return false; // Even in auto-accept mode, bash commands should require approval for security return true; }, execute: async (args, _options) => { return await executeExecuteBash(args); }, }); /** * Formatter component - used for tool confirmation preview */ function ExecuteBashFormatterComponent({ command, }) { const { colors } = useTheme(); return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.tool, children: "\u2692 execute_bash" }), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Command: " }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: colors.primary, children: command }) })] })] })); } /** * Regular formatter - called for tool confirmation preview * Shows the command that will be executed */ const executeBashFormatter = (args) => { return _jsx(ExecuteBashFormatterComponent, { command: args.command }); }; /** * Streaming formatter - called BEFORE execution to set up progress component * The component subscribes to bash executor events and updates itself */ const executeBashStreamingFormatter = (args, executionId) => { return _jsx(BashProgress, { executionId: executionId, command: args.command }); }; const executeBashValidator = (args) => { const command = args.command?.trim(); // Check if command is empty if (!command) { return Promise.resolve({ valid: false, error: '⚒ Command cannot be empty', }); } // Check for extremely dangerous commands const dangerousPatterns = [ /rm\s+-rf\s+\/(?!\w)/i, // rm -rf / (but allow /path) /mkfs/i, // Format filesystem /dd\s+if=/i, // Direct disk write /:(){:|:&};:/i, // Fork bomb />\s*\/dev\/sd[a-z]/i, // Writing to raw disk devices /chmod\s+-R\s+000/i, // Remove all permissions recursively ]; for (const pattern of dangerousPatterns) { if (pattern.test(command)) { return Promise.resolve({ valid: false, error: `⚒ Command contains potentially destructive operation: "${command}". This command is blocked for safety.`, }); } } return Promise.resolve({ valid: true }); }; export const executeBashTool = { name: 'execute_bash', tool: executeBashCoreTool, formatter: executeBashFormatter, streamingFormatter: executeBashStreamingFormatter, validator: executeBashValidator, }; //# sourceMappingURL=execute-bash.js.map