Acme-Claude-Shell

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

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()

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., 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();



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