Acme-Claude-Shell

 view release on metacpan or  search on metacpan

lib/Acme/Claude/Shell/Session.pm  view on Meta::CPAN


    return if @lines <= $MAX_HISTORY;

    # Keep only last MAX_HISTORY lines
    @lines = @lines[-$MAX_HISTORY..-1];

    open $fh, '>:encoding(UTF-8)', $HISTORY_FILE or return;
    print $fh @lines;
    close $fh;
}

async sub run {
    my ($self) = @_;

    # Show colorful header
    $self->_show_banner;

    my $term = Term::ReadLine->new('acme_claude_shell');

    # Load persistent history
    _load_history($term);

    # Create SDK MCP server with shell tools
    my $mcp = create_sdk_mcp_server(
        name  => 'shell-tools',
        tools => shell_tools($self),
    );

    # Create options with hooks
    my $options = Claude::Agent::Options->new(
        permission_mode => 'bypassPermissions',
        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) : ()),
    );

    # Create persistent session client
    $self->_client(session(
        options => $options,
        loop    => $self->loop,
    ));

    # REPL loop
    while (1) {
        my $prompt_str = $self->colorful
            ? colored(['bold', 'green'], 'acme_claude_shell> ')
            : 'acme_claude_shell> ';

        my $input = $term->readline($prompt_str);
        last unless defined $input;

        $input =~ s/^\s+|\s+$//g;
        next unless length $input;

        # Built-in commands
        last if $input =~ /^(exit|quit)$/i;

        if ($input =~ /^history$/i) {
            my $selected = $self->_show_history;
            if (defined $selected && length $selected) {
                # User selected a command to re-run - process it
                $term->addhistory($selected);
                _append_to_history($selected);
                await $self->_process_input($selected);
            }
            next;
        }
        if ($input =~ /^clear$/i) {
            system('clear');
            next;
        }
        if ($input =~ /^help$/i) {
            $self->_show_help;
            next;
        }

        # Add to readline history and persist to file
        $term->addhistory($input);
        _append_to_history($input);

        # Process with Claude
        await $self->_process_input($input);
    }

    status('info', "Goodbye!") if $self->colorful;
    return 1;
}

async sub _process_input {
    my ($self, $input) = @_;

    # Query cursor position via /dev/tty before starting spinner
    # This avoids Term::ProgressSpinner's STDIN query which fails after Term::ReadLine
    my $cursor_row = _get_cursor_row();

    # Store spinner in session so hooks can stop it before reading STDIN
    # Pick a random fun spinner each time
    $self->_spinner(start_spinner("Thinking...", $self->loop,
        _random_spinner(),
        defined $cursor_row ? (terminal_line => $cursor_row) : ()));

    # First turn or follow-up
    if ($self->_client->session_id) {
        $self->_client->send($input);
    } else {
        $self->_client->connect($input);
    }

    my $printed_response = 0;

    while (my $msg = await $self->_client->receive_async) {
        if ($msg->isa('Claude::Agent::Message::Assistant')) {
            # Print reasoning immediately so it appears BEFORE tool approval
            my $text = $msg->text // '';
            if ($text) {
                # Stop spinner before printing text
                if ($self->_spinner) {
                    stop_spinner($self->_spinner);
                    $self->_spinner(undef);
                }
                print "\n" unless $printed_response;
                my $output = $self->colorful ? colored(['white'], $text) : $text;
                print $output;
                $printed_response = 1;

lib/Acme/Claude/Shell/Session.pm  view on Meta::CPAN

        print "AI-powered shell - describe what you want in plain English\n";
        print "Type 'help' for commands, 'exit' to quit\n";
        print "-" x 60, "\n\n";
    }
}

sub _show_help {
    my ($self) = @_;

    if ($self->colorful) {
        header("Help");
    } else {
        print "\n--- Help ---\n";
    }

    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 {
    my ($self) = @_;

    # Load prompt history from file
    my @history;
    if (-f $HISTORY_FILE) {
        open my $fh, '<:encoding(UTF-8)', $HISTORY_FILE or do {
            print "Could not read history file.\n\n";
            return;
        };
        @history = <$fh>;
        close $fh;
        chomp @history;
    }

    # Add current session prompts not yet in file
    push @history, @_session_prompts if @_session_prompts;

    unless (@history) {
        if ($self->colorful) {
            status('info', "No history yet.");
        } else {
            print "No history yet.\n";
        }
        return;
    }

    # Get last 20 unique entries for selection
    my @recent = @history > 20 ? @history[-20..-1] : @history;

    # Use choose_from for interactive selection
    my $selected = choose_from(
        \@recent,
        prompt        => "Select a command to re-run (or press 'q' to cancel):",
        inline_prompt => @history > 20 ? "(Last 20 of " . scalar(@history) . ")" : "",
        layout        => 2,  # Single column for readability
    );

    return $selected;
}

sub _system_prompt {
    my ($self) = @_;
    return <<'PROMPT';
You are an AI shell assistant. The user describes tasks in natural language,
and you translate them 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

IMPORTANT: Remember context from previous commands!
If the user says "now do X to those files", use the results from the
previous command to know which files they mean.

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 (rm, dd, etc.)
- Prefer safe alternatives when possible
- Explain what each command does

Always explain what you're about to do before using tools.
PROMPT
}

=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

1;



( run in 0.667 second using v1.01-cache-2.11-cpan-39bf76dae61 )