Acme-Claude-Shell

 view release on metacpan or  search on metacpan

lib/Acme/Claude/Shell/Tools.pm  view on Meta::CPAN

    my $stderr = '';

    my $process = IO::Async::Process->new(
        command => [ '/bin/sh', '-c', $command ],
        ($dir && -d $dir ? (setup => [ chdir => $dir ]) : ()),
        stdout => {
            into => \$stdout,
        },
        stderr => {
            into => \$stderr,
        },
        on_finish => sub {
            my ($self, $exitcode) = @_;
            my $exit_status = $exitcode >> 8;

            if ($exit_status != 0) {
                $session->_history->[-1]{status} = "exit $exit_status";
                my $output = $stderr || $stdout || "Command failed with exit code $exit_status";
                $future->done(_mcp_result($output));
            } else {
                $session->_history->[-1]{status} = 'success';
                $future->done(_mcp_result($stdout // ''));
            }
        },
        on_exception => sub {
            my ($self, $exception, $errno, $exitcode) = @_;
            $session->_history->[-1]{status} = 'error';
            $future->done(_mcp_result("Error: $exception", 1));
        },
    );

    $loop->add($process);

    return $future;
}

# Helper to format tool results in MCP format
sub _mcp_result {
    my ($text, $is_error) = @_;
    return {
        content  => [{ type => 'text', text => $text }],
        is_error => $is_error ? 1 : 0,
    };
}

# Dangerous command patterns
my @DANGEROUS_PATTERNS = (
    { pattern => qr/\brm\s+(-[rf]+|--recursive|--force)/i,
      reason  => 'Recursive or forced file deletion' },
    { pattern => qr/\bsudo\b/,
      reason  => 'Superuser command' },
    { pattern => qr/\bmkfs\b/,
      reason  => 'Filesystem formatting' },
    { pattern => qr/\bdd\b.*\bof=/,
      reason  => 'Direct disk write' },
    { pattern => qr/>\s*\/dev\//,
      reason  => 'Writing to device file' },
    { pattern => qr/\bchmod\s+(-R\s+)?777\b/,
      reason  => 'World-writable permissions' },
    { pattern => qr/\bchown\s+-R\b.*\//,
      reason  => 'Recursive ownership change' },
    { pattern => qr/\bkill\s+-9\b/,
      reason  => 'Forceful process termination' },
    { pattern => qr/\b(reboot|shutdown|halt|poweroff)\b/,
      reason  => 'System shutdown/reboot' },
    { pattern => qr/\bformat\b/,
      reason  => 'Disk formatting' },
    { pattern => qr/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/,
      reason  => 'Fork bomb detected' },
    { pattern => qr/\bwget\b.*\|\s*(ba)?sh/i,
      reason  => 'Piping remote script to shell' },
    { pattern => qr/\bcurl\b.*\|\s*(ba)?sh/i,
      reason  => 'Piping remote script to shell' },
);

sub _check_dangerous {
    my ($command) = @_;
    for my $check (@DANGEROUS_PATTERNS) {
        if ($command =~ $check->{pattern}) {
            return $check;
        }
    }
    return undef;
}

# Confirm command with user before executing
# Returns ($approved, $new_command) - $new_command is set if user edited it
sub _confirm_command {
    my ($session, $command) = @_;

    my $colorful = $session->colorful;

    # Check for dangerous patterns
    my $danger = _check_dangerous($command);

    print "\n";

    if ($danger && $session->safe_mode) {
        if ($colorful) {
            status('warning', "Potentially dangerous command detected!");
            print colored(['yellow'], "  Reason: $danger->{reason}\n");
        } else {
            print "WARNING: Potentially dangerous command!\n";
            print "  Reason: $danger->{reason}\n";
        }
        print "\n";
    }

    # Show the command
    if ($colorful) {
        status('info', "Command: $command");
    } else {
        print "Command: $command\n";
    }

    # Show action menu
    my $choice = menu("Action", [
        { key => 'a', label => 'Approve and run' },
        { key => 'd', label => 'Dry-run (preview only)' },
        { key => 'e', label => 'Edit command' },
        { key => 'x', label => 'Cancel' },



( run in 0.992 second using v1.01-cache-2.11-cpan-39bf76dae61 )