view release on metacpan or search on metacpan
Revision history for Acme-Claude-Shell
0.04 2026-10-17
- Require Claude::Agent 0.14: hooks now run through the CLI and command output is printed from the tool results.
- Add verbose, audit_log and effort options and the --verbose and --effort flags.
- Turn off the CLI built-in tools so every command goes through execute_command.
- Read the approval from STDIN when output is not colorful; end of input cancels.
- Declare utf8 in Hooks.pm so the statistics rule prints.
0.01 2026-01-11
First version - AI-powered interactive shell showcasing Claude::Agent SDK
[Features]
- shell() for interactive multi-turn sessions using session()
- run() for single-shot commands using query()
- Safety hooks with dangerous command detection
- Colorful terminal UI with spinners and menus
- Dry-run mode for previewing commands
- Command history tracking
[SDK Features Demonstrated]
- query() - Single-shot commands via run()
- session() - Multi-turn context via shell()
- SDK MCP Tools - Custom shell execution tools
- Hooks (PreToolUse) - Safety confirmation menus
- Hooks (PostToolUse) - Command logging
- Dry-run mode - Preview without executing
- IO::Async - Non-blocking spinners
- CLI utilities - Colored output, menus, status messages
bin/acme_claude_shell view on Meta::CPAN
my $c = $opts{'color'};
if ($c) {
print colored(['bold', 'cyan'], "\n Acme::Claude::Shell"), " - AI-powered interactive shell\n\n";
} else {
print "\n Acme::Claude::Shell - AI-powered interactive shell\n\n";
}
print_section("Usage", $c);
print " acme_claude_shell [options]\n";
print " acme_claude_shell -c \"find all large log files\"\n\n";
print_section("Options", $c);
print_opt("-n, --dry-run", "Preview mode - show commands without executing", $c);
print_opt("-u, --unsafe", "Disable safety confirmations for dangerous commands", $c);
print_opt("--no-color", "Disable colored output", $c);
print_opt("-d, --directory DIR", "Set working directory (default: current)", $c);
print_opt("-c, --command CMD", "Run single command and exit (use '-' for stdin)", $c);
print_opt("-m, --model MODEL", "Claude model to use (e.g., opus, sonnet)", $c);
print_opt("-e, --effort LEVEL", "Effort level: low, medium, high, xhigh or max", $c);
print_opt("--verbose", "Log every tool call and notification", $c);
print_opt("-h, --help", "Show this help", $c);
print_opt("-v, --version", "Show version", $c);
print "\n";
print_section("Examples", $c);
print " acme_claude_shell # Start interactive shell\n";
print " acme_claude_shell --dry-run # Preview mode\n";
print " acme_claude_shell -d /var/log # Start in specific directory\n";
print " acme_claude_shell -c \"list perl files\" # Single command\n";
print " acme_claude_shell -m claude-opus-4-5 # Use a specific model\n";
print " echo \"list files\" | acme_claude_shell -c - # Piped input\n\n";
print_section("Interactive Commands", $c);
print " help Show help inside the shell\n";
print " history Show executed command history\n";
print " clear Clear the screen\n";
print " exit Exit the shell (or 'quit')\n\n";
bin/acme_claude_shell view on Meta::CPAN
__END__
=head1 NAME
acme_claude_shell - AI-powered interactive shell
=head1 SYNOPSIS
acme_claude_shell [options]
acme_claude_shell -c "find all large log files"
=head1 OPTIONS
=over 4
=item B<-n, --dry-run>
Preview mode - show commands without executing them.
=item B<-u, --unsafe>
lib/Acme/Claude/Shell.pm view on Meta::CPAN
my $session = Acme::Claude::Shell::Session->new(
loop => $loop,
dry_run => $args{dry_run} // 0,
safe_mode => $args{safe_mode} // 1,
working_dir => $args{working_dir} // '.',
colorful => $args{colorful} // _detect_color(),
($args{model} ? (model => $args{model}) : ()),
($args{effort} ? (effort => $args{effort}) : ()),
verbose => $args{verbose} // 0,
audit_log => $args{audit_log} // 0,
);
return $session->run->get;
}
sub run {
my ($prompt, %args) = @_;
my $loop = $args{loop} // IO::Async::Loop->new;
my $query = Acme::Claude::Shell::Query->new(
loop => $loop,
dry_run => $args{dry_run} // 0,
safe_mode => $args{safe_mode} // 1,
working_dir => $args{working_dir} // '.',
colorful => $args{colorful} // _detect_color(),
($args{model} ? (model => $args{model}) : ()),
($args{effort} ? (effort => $args{effort}) : ()),
verbose => $args{verbose} // 0,
audit_log => $args{audit_log} // 0,
);
return $query->run($prompt)->get;
}
sub _detect_color {
return -t STDOUT ? 1 : 0;
}
lib/Acme/Claude/Shell.pm view on Meta::CPAN
Version 0.04
=head1 SYNOPSIS
use Acme::Claude::Shell qw(shell run);
# Interactive session mode (uses session() for multi-turn context)
shell();
# Single-shot query mode (uses query())
my $result = run("find all large log files");
# With options
shell(
dry_run => 1, # Preview mode - show commands without executing
safe_mode => 0, # Disable dangerous command warnings
colorful => 1, # Force colors (default: auto-detect)
);
=head1 DESCRIPTION
lib/Acme/Claude/Shell.pm view on Meta::CPAN
=cut
=head1 EXAMPLE SESSION
============================================================
Acme::Claude::Shell
============================================================
i AI-powered shell - describe what you want in plain English
i Type 'exit' or 'quit' to leave, 'history' for command log
------------------------------------------------------------
acme_claude_shell> find all perl files larger than 100k
Thinking...
I'll find all .pl files over 100KB and display their sizes:
i Command: find . -name "*.pl" -size +100k -exec ls -lh {} \;
Action:
lib/Acme/Claude/Shell.pm view on Meta::CPAN
This module demonstrates every major feature of the Claude::Agent SDK:
=over 4
=item B<query()> - Single-shot mode via C<run()>
=item B<session()> - Multi-turn context via C<shell()>
=item B<SDK MCP Tools> - 6 tools: execute_command, read_file, list_directory, search_files, get_system_info, get_working_directory
=item B<Hooks (PreToolUse)> - Audit logging of tool calls
=item B<Hooks (PostToolUse)> - Stop spinner, track statistics
=item B<Hooks (PostToolUseFailure)> - Graceful error handling
=item B<Hooks (Stop)> - Session statistics on exit
=item B<Hooks (Notification)> - Event logging (verbose mode)
=item B<Dry-run mode> - Preview without executing
=item B<IO::Async> - Non-blocking command execution and spinners
=item B<CLI utilities> - Spinners, menus, colored output
=back
B<Note:> Command approval is handled directly in the execute_command tool
lib/Acme/Claude/Shell/Hooks.pm view on Meta::CPAN
if ($session->verbose) {
if ($session->colorful) {
status('info', "Tool: $short_name");
}
else {
print "[Tool] $short_name\n";
}
}
if ($session->audit_log) {
push @{$session->_audit_log}, {
time => time(),
tool => $short_name,
input => $tool_input,
tool_use_id => $tool_use_id,
};
}
return Claude::Agent::Hook::Result->proceed();
}],
),
lib/Acme/Claude/Shell/Hooks.pm view on Meta::CPAN
=head1 SYNOPSIS
use Acme::Claude::Shell::Hooks qw(safety_hooks);
my $hooks = safety_hooks($session);
=head1 DESCRIPTION
Provides hooks for Acme::Claude::Shell. These hooks integrate with the
Claude::Agent SDK hook system to provide logging, statistics, and error
handling.
B<Note:> Command approval is handled directly in the tool handler (Tools.pm)
to ensure it happens synchronously before execution.
=head2 Hooks
=over 4
=item * B<PreToolUse> - Audit logging of tool calls
Triggered before any shell-tools MCP tool executes. Logs tool usage in
verbose mode and tracks calls in an audit log if C<< $session->audit_log >>
is enabled.
=item * B<PostToolUse> - Stop spinner after command execution
Triggered after C<execute_command> completes successfully. Stops the
execution spinner and increments the tool usage counter.
=item * B<PostToolUseFailure> - Handle tool failures gracefully
Triggered when any shell-tools MCP tool fails. Displays a user-friendly
lib/Acme/Claude/Shell/Hooks.pm view on Meta::CPAN
Triggered for SDK notifications. Logs notification types in verbose mode.
=back
=head2 Session Options
The following session attributes affect hook behavior:
=over 4
=item * C<verbose> - Enable verbose logging of tool calls and notifications
=item * C<audit_log> - Enable detailed audit logging to C<_audit_log>
=item * C<colorful> - Use colored output (default: auto-detect TTY)
=back
=cut
=head1 FUNCTIONS
=head2 safety_hooks
lib/Acme/Claude/Shell/Query.pm view on Meta::CPAN
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' },
lib/Acme/Claude/Shell/Query.pm view on Meta::CPAN
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
lib/Acme/Claude/Shell/Query.pm view on Meta::CPAN
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
lib/Acme/Claude/Shell/Session.pm view on Meta::CPAN
use File::Spec;
BEGIN {
Object::Proto::define('Acme::Claude::Shell::Session',
'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',
'_client',
'_history:ArrayRef:default([])',
'_spinner',
'_session_start',
'_tool_count:Int:default(0)',
'_tool_errors:Int:default(0)',
'_audit_log:ArrayRef:default([])',
);
}
my $HISTORY_FILE = File::Spec->catfile($ENV{HOME} // '.', '.acme_claude_shell_history');
my $MAX_HISTORY = 1000;
my @SPINNERS = (
{ spinner => 'moon', spinner_color => 'yellow' },
{ spinner => 'earth', spinner_color => 'cyan' },
{ spinner => 'clock', spinner_color => 'blue' },
lib/Acme/Claude/Shell/Session.pm view on Meta::CPAN
}
print <<'HELP';
Built-in commands:
help - Show this help
history - Select and re-run previous commands
clear - Clear screen
exit - Exit shell (or 'quit')
Just type what you want in plain English:
"find all large log files"
"show disk usage by directory"
"compress files older than 7 days"
"now delete those files" (uses context from previous command)
Claude will show you the command before running it.
You can approve, edit, dry-run, or cancel.
HELP
}
sub _show_history {
lib/Acme/Claude/Shell/Session.pm view on Meta::CPAN
Starting directory for commands. Default C<.>.
=head2 colorful
Use colored output, spinners and menus. 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 _client, _history, _spinner, _session_start, _tool_count, _tool_errors, _audit_log
State the tools and hooks read and write: the L<Claude::Agent::Client>,
the commands run this session, the active spinner, and the statistics the
Stop hook reports.
=head1 METHODS
=head2 new
Takes a flat list of name/value pairs. Unknown names are ignored.
t/02-hooks.t view on Meta::CPAN
subtest 'Session statistics initialization' => sub {
my $fresh = make_session();
ok(!$fresh->_session_start, 'no start time before the hooks are built');
safety_hooks($fresh);
ok($fresh->_session_start, 'Session start time initialized');
is($fresh->_tool_count, 0, 'Tool count starts at zero');
is($fresh->_tool_errors, 0, 'Tool errors start at zero');
};
subtest 'hooks run through the registry' => sub {
my $audit = make_session(audit_log => 1);
my $registry = Claude::Agent::Hook::Registry->new(hooks => safety_hooks($audit), loop => $loop);
my $config = $registry->to_initialize_config;
is_deeply([sort keys %$config], [qw(Notification PostToolUse PostToolUseFailure PreToolUse Stop)], 'every event is registered');
is($config->{PreToolUse}[0]{matcher}, 'mcp__shell-tools__.*', 'the PreToolUse pattern is sent to the CLI');
my %id_for;
for my $event (keys %$config) {
$id_for{$event} = $config->{$event}[0]{hookCallbackIds}[0];
}
my $out = $registry->run($id_for{PreToolUse}, {
hook_event_name => 'PreToolUse',
tool_name => 'mcp__shell-tools__read_file',
tool_input => { path => '/tmp/test.txt' },
}, 'test-id-123')->get;
is_deeply($out, {}, 'PreToolUse proceeds');
is(scalar @{$audit->_audit_log}, 1, 'One audit entry');
is($audit->_audit_log->[0]{tool}, 'read_file', 'audit entry has the short tool name');
is($audit->_audit_log->[0]{tool_use_id}, 'test-id-123', 'audit entry has the tool use id');
$out = $registry->run($id_for{PostToolUse}, {
hook_event_name => 'PostToolUse', tool_name => 'mcp__shell-tools__execute_command', tool_input => { command => 'ls' },
}, 'test-id-124')->get;
is_deeply($out, {}, 'PostToolUse proceeds');
is($audit->_tool_count, 1, 'tool count incremented');
my $printed = '';
{
local *STDOUT;
t/02-hooks.t view on Meta::CPAN
local *STDOUT;
open STDOUT, '>', \$printed or die $!;
$chatty_registry->run($chatty_config->{Notification}[0]{hookCallbackIds}[0], {
hook_event_name => 'Notification', notification_type => 'idle', message => 'Waiting',
}, undef)->get;
$chatty_registry->run($chatty_config->{PreToolUse}[0]{hookCallbackIds}[0], {
hook_event_name => 'PreToolUse', tool_name => 'mcp__shell-tools__search_files', tool_input => {},
}, 'x')->get;
}
like($printed, qr/\[Notification\] idle - Waiting/, 'verbose notification');
like($printed, qr/\[Tool\] search_files/, 'verbose tool log');
};
done_testing();