Acme-Claude-Shell

 view release on metacpan or  search on metacpan

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

package Acme::Claude::Shell::Query;

use 5.020;
use strict;
use warnings;

use Object::Proto ();
use Claude::Agent qw(query create_sdk_mcp_server);
use Claude::Agent::Options;
use Claude::Agent::Content;
use Claude::Agent::CLI qw(
    start_spinner stop_spinner
    header divider status
);
use Acme::Claude::Shell::Tools qw(shell_tools);
use Acme::Claude::Shell::Hooks qw(safety_hooks);
use Future::AsyncAwait;

BEGIN {
    Object::Proto::define('Acme::Claude::Shell::Query',
        'loop:Object:required',
        'dry_run:Bool:default(0):predicate',
        'safe_mode:Bool:default(1):predicate',
        'working_dir:Str:default(.):predicate',
        'colorful:Bool:default(1):predicate',
        'verbose:Bool:default(0):predicate',
        'audit_log:Bool:default(0):predicate',
        'model:Str:predicate',
        'effort:Str:predicate',
        '_history:ArrayRef:default([])',
        '_spinner',
        '_session_start',
        '_tool_count:Int:default(0)',
        '_tool_errors:Int:default(0)',
        '_audit_log:ArrayRef:default([])',
    );
}

my @SPINNERS = (
    { spinner => 'moon',          spinner_color => 'yellow' },
    { spinner => 'earth',         spinner_color => 'cyan' },
    { spinner => 'clock',         spinner_color => 'blue' },
    { spinner => 'dots',          spinner_color => 'magenta' },
    { spinner => 'material',      spinner_color => 'green' },
    { spinner => 'circle_half',   spinner_color => 'cyan' },
    { spinner => 'color_circles', spinner_color => 'white' },
);

sub _random_spinner {
    return %{ $SPINNERS[rand @SPINNERS] };
}

sub options {
    my ($self) = @_;

    my $mcp = create_sdk_mcp_server(
        name  => 'shell-tools',
        tools => shell_tools($self),
    );

    return Claude::Agent::Options->new(
        permission_mode => 'bypassPermissions',
        tools           => [],
        mcp_servers     => { 'shell-tools' => $mcp },
        hooks           => safety_hooks($self),
        dry_run         => $self->dry_run,
        system_prompt   => $self->_system_prompt,
        ($self->has_model  ? (model  => $self->model)  : ()),
        ($self->has_effort ? (effort => $self->effort) : ()),
    );
}

async sub run {
    my ($self, $prompt) = @_;

    if ($self->colorful) {
        header("Acme::Claude::Shell - Query Mode");
    }

    $self->_spinner(start_spinner("Thinking...", $self->loop, _random_spinner())) if $self->colorful;

    my $iter = query(
        prompt  => $prompt,
        options => $self->options,
        loop    => $self->loop,
    );

    my $response_text = '';
    my $result;
    my %tool_names;

    while (my $msg = await $iter->next_async) {
        if ($msg->isa('Claude::Agent::Message::Assistant')) {
            $response_text .= $msg->text // '';
            $tool_names{ $_->id } = $_->name for @{ $msg->tool_uses };

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

        my $text = $result->text // '';
        if ($result->is_error) {
            next if $text =~ /^(Dry-run:|User cancelled)/;
            status('error', $text) if $self->colorful;
        }
        elsif (length $text) {
            print $text;
            print "\n" unless $text =~ /\n\z/;
        }
    }
    return;
}

sub _system_prompt {
    my ($self) = @_;
    return <<'PROMPT';
You are an AI shell assistant. The user describes a task in natural language,
and you translate it into shell commands.

When the user asks you to do something:
1. Explain what command(s) you'll run and why
2. Use the execute_command tool to run them
3. Summarize the results

PERL FALLBACK: When a task cannot be done with standard shell commands,
or when a shell command isn't available on the system, use Perl one-liners instead.
Perl is always available. Examples:
- Instead of: jq '.key' file.json
  Use: perl -MJSON -0777 -ne 'print decode_json($_)->{key}' file.json
- Instead of: sed -i 's/old/new/g' file
  Use: perl -pi -e 's/old/new/g' file
- For complex text processing, JSON/YAML parsing, or when shell tools are missing,
  prefer Perl one-liners as they are portable and powerful.

Be helpful but safe - warn about destructive operations.
Always explain what you're about to do before using tools.
PROMPT
}

1;

__END__

=head1 NAME

Acme::Claude::Shell::Query - Single-shot query mode for Acme::Claude::Shell

=head1 SYNOPSIS

    use Acme::Claude::Shell::Query;
    use IO::Async::Loop;

    my $loop = IO::Async::Loop->new;

    my $query = Acme::Claude::Shell::Query->new(
        loop      => $loop,
        dry_run   => 0,
        safe_mode => 1,
    );

    my $result = $query->run("find all large log files")->get;

=head1 DESCRIPTION

Executes a single natural language command through Claude::Agent's
C<query()>. Each C<run> is independent: no context is kept between calls.
This suits scripting, or a one-shot command without starting an
interactive session.

=head1 ATTRIBUTES

=head2 loop

The L<IO::Async::Loop> to run on. Required.

=head2 dry_run

Preview mode. Default 0.

=head2 safe_mode

Ask for extra confirmation before dangerous commands. Default 1.

=head2 working_dir

Starting directory for commands. Default C<.>.

=head2 colorful

Use colored output and spinners. Default 1.

=head2 verbose

Log every tool call and notification through the hooks. Default 0.

=head2 audit_log

Record every tool call in C<_audit_log>. Default 0.

=head2 model

Claude model to use. Optional.

=head2 effort

Effort level: C<low>, C<medium>, C<high>, C<xhigh> or C<max>. Optional.

=head2 _history, _spinner, _session_start, _tool_count, _tool_errors, _audit_log

State the tools and hooks read and write.

=head1 METHODS

=head2 new

Takes a flat list of name/value pairs. Unknown names are ignored.

=head2 options

The L<Claude::Agent::Options> for the query. The CLI's built-in tools are
turned off, so every command goes through C<execute_command> and its
approval menu.

=head2 run

    my $result = $query->run($prompt)->get;

Execute one prompt. Returns a L<Future> of the
L<Claude::Agent::Message::Result>.

=head1 AUTHOR

LNATION, C<< <email at lnation.org> >>

=head1 LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=cut



( run in 1.308 second using v1.01-cache-2.11-cpan-eca53f54451 )