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 )