@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
JavaScript
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