view release on metacpan or search on metacpan
- 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., claude-opus-4-5)", $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
Version 0.03
=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
return -t STDOUT ? 1 : 0;
}
=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
=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<< $session->{_audit_log} >>
=item * C<colorful> - Use colored output (default: auto-detect TTY)
=back
=cut
sub safety_hooks {
my ($session) = @_;
lib/Acme/Claude/Shell/Hooks.pm view on Meta::CPAN
my $execute_cmd_pattern = 'execute_command$';
# Match any tool from our shell-tools server
my $any_shell_tool = 'shell-tools__';
# Track session start time
$session->{_session_start} //= time();
$session->{_tool_count} //= 0;
$session->{_tool_errors} //= 0;
return {
# PreToolUse: Audit logging - track what tools Claude wants to use
PreToolUse => [
Claude::Agent::Hook::Matcher->new(
matcher => $any_shell_tool,
hooks => [sub {
my ($input, $tool_use_id, $context) = @_;
my $tool_name = $input->{tool_name} // 'unknown';
my $tool_input = $input->{tool_input} // {};
# Extract short tool name (after mcp__shell-tools__)
lib/Acme/Claude/Shell/Hooks.pm view on Meta::CPAN
# Log tool usage in verbose mode
if ($session->{verbose}) {
if ($session->colorful) {
status('info', "Tool: $short_name");
}
else {
print "[Tool] $short_name\n";
}
}
# Track in audit log if enabled
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/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 using Claude's query() function.
Does not maintain session context between calls - each run() is independent.
This is useful for scripting or when you want a one-shot command without
starting an interactive session.
Uses Claude::Agent SDK features:
=over 4
=item * C<query()> - Single-shot prompt execution
=item * SDK MCP tools - execute_command, read_file, list_directory, search_files, get_system_info, get_working_directory
=item * Hooks - PreToolUse (audit), PostToolUse (stats), PostToolUseFailure (errors), Stop (statistics), Notification (logging)
=item * CLI utilities - Spinners and colored output
=back
=head2 Attributes
=over 4
=item * C<loop> (required) - IO::Async::Loop instance
lib/Acme/Claude/Shell/Session.pm view on Meta::CPAN
say things like "now compress those files" after a find command.
Uses Claude::Agent SDK features:
=over 4
=item * C<session()> - Multi-turn conversation with context
=item * SDK MCP tools - execute_command, read_file, list_directory, search_files, get_system_info, get_working_directory
=item * Hooks - PreToolUse (audit), PostToolUse (stats), PostToolUseFailure (errors), Stop (statistics), Notification (logging)
=item * CLI utilities - Spinners, menus, colored output
=back
=head2 Attributes
=over 4
=item * C<loop> (required) - IO::Async::Loop instance
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 {
t/02-hooks.t view on Meta::CPAN
# Create a mock session object for testing
package MockSession;
sub new {
my ($class, %args) = @_;
return bless {
working_dir => $args{working_dir} // '.',
colorful => $args{colorful} // 0,
safe_mode => $args{safe_mode} // 1,
verbose => $args{verbose} // 0,
audit_log => $args{audit_log} // 0,
_history => [],
_spinner => undef,
}, $class;
}
sub working_dir { $_[0]->{working_dir} }
sub colorful { $_[0]->{colorful} }
sub safe_mode { $_[0]->{safe_mode} }
sub _history { $_[0]->{_history} }
sub _spinner {
my $self = shift;
t/02-hooks.t view on Meta::CPAN
# Create fresh session
my $fresh_session = MockSession->new(colorful => 0);
my $fresh_hooks = safety_hooks($fresh_session);
ok(exists $fresh_session->{_session_start}, 'Session start time initialized');
ok(exists $fresh_session->{_tool_count}, 'Tool count initialized');
ok(exists $fresh_session->{_tool_errors}, 'Tool errors initialized');
};
# Test audit log option
subtest 'Audit log option' => sub {
plan tests => 2;
my $audit_session = MockSession->new(
colorful => 0,
audit_log => 1,
);
my $audit_hooks = safety_hooks($audit_session);
# Run a PreToolUse hook
my $matcher = $audit_hooks->{PreToolUse}[0];
my $input = {
tool_name => 'mcp__shell-tools__read_file',
tool_input => { path => '/tmp/test.txt' },
};
# Create mock context
my $context = bless {}, 'Claude::Agent::Hook::Context';
require IO::Async::Loop;
my $loop = IO::Async::Loop->new;
my $future = $matcher->run_hooks($input, 'test-id-123', $context, $loop);
my $results = $future->get;
# Check audit log was populated
ok(exists $audit_session->{_audit_log}, 'Audit log created');
is(scalar(@{$audit_session->{_audit_log} // []}), 1, 'One audit entry');
};
# Test hook returns proper Result
subtest 'Hook returns proper Result' => sub {
plan tests => 2;
my $matcher = $hooks->{PreToolUse}[0];
my $input = {
tool_name => 'mcp__shell-tools__execute_command',
tool_input => { command => 'ls' },