Acme-Claude-Shell

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

        - 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' },



( run in 0.854 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )