App-Raider
view release on metacpan or search on metacpan
}
# --- Output helpers -----------------------------------------------------
my $LOGO = <<'LOGO';
__ __
.----.---.-.|__|.--| |.-----.----.
| _| _ || || _ || -__| _|
|__| |___._||__||_____||_____|__|
LOGO
sub banner {
my ($app, $rl_impl, $active_profiles, $saved_now) = @_;
$active_profiles //= [];
$saved_now //= {};
my $env = $app->api_key_env;
my $env_display = defined $env
? ($ENV{$env} ? "$env (set)" : "$env (missing)")
: '(no API key required)';
my $model = $app->has_model ? $app->model : '(engine default)';
my $custom = path($app->root)->child('.raider.md');
my $persona = -f $custom ? "custom (.raider.md loaded)" : "Langertha (default)";
print c(brand => $LOGO);
print c(accent => " perl agent - powered by Langertha"), "\n\n";
my @skills = $app->loaded_skill_names;
my @ignored = $app->ignored_agent_files;
print c(meta => "persona: "), c(title => $persona), "\n";
if (@$active_profiles) {
my @parts;
for my $p (@$active_profiles) {
my $tag = $saved_now->{$p} ? ' (saved)' : '';
push @parts, $p . $tag;
}
print c(meta => "profiles: "), c(title => join(', ', @parts)), "\n";
}
my $skills_line = @skills
? sprintf("%d loaded (%s)", scalar @skills, join(', ', @skills))
: 'none';
print c(meta => "skills: "), c(title => $skills_line), "\n";
for my $ign (@ignored) {
print c(meta => " "), c(warn => "seeing $ign->{path}, ignoring (use --$ign->{profile} to load)"), "\n";
}
print c(meta => "engine: "), c(title => $app->engine_name), "\n";
print c(meta => "model: "), c(title => $model), "\n";
print c(meta => "api key: "), c(title => $env_display), "\n";
print c(meta => "root: "), c(title => $app->root), "\n";
print c(meta => "readline: "), c(title => $rl_impl), "\n";
print c(meta => "type "), c(accent => "/help"), c(meta => " for commands, "),
c(accent => "/quit"), c(meta => " to leave"), "\n\n";
}
sub render_inline_code {
my ($text) = @_;
return $text unless -t STDOUT && !$ENV{ANSI_COLORS_DISABLED};
# Wrap `...` spans with a subtly-darker background. Triple backticks stay
# untouched so fenced code blocks keep their own flow.
my $code_on = color('on_grey3');
my $agent_on = color($C{agent});
my $reset = color('reset');
$text =~ s{(?<!`)`([^`\n]+?)`(?!`)}{$code_on$1$reset$agent_on}g;
return $text;
}
sub persist_profiles {
my ($root, $profiles) = @_;
my $file = Path::Tiny::path($root)->child('.raider.yml');
my $yml = {};
if (-f $file) {
my $loaded = eval { YAML::PP->new->load_string($file->slurp_utf8) };
$yml = $loaded if ref $loaded eq 'HASH';
}
my @existing;
if (defined $yml->{skills}) {
@existing = ref $yml->{skills} eq 'ARRAY' ? @{$yml->{skills}} : ($yml->{skills});
}
my %have = map { (!ref $_ ? $_ : '') => 1 } @existing;
my %saved_now;
my $changed = 0;
for my $p (@$profiles) {
next if $have{$p};
push @existing, $p;
$have{$p} = 1;
$saved_now{$p} = 1;
$changed = 1;
}
if ($changed) {
$yml->{skills} = \@existing;
my $dumped = YAML::PP->new->dump_string($yml);
$file->spew_utf8($dumped);
}
return %saved_now;
}
sub collapse_model_list {
my ($list, $current) = @_;
my %all = map { $_ => 1 } @$list;
my %groups; # base => [snapshot variants]
my %is_snap; # id => 1
for my $id (@$list) {
if (my ($base) = ($id =~ /^(.+?)-\d{4}-\d{2}-\d{2}(?:-.+)?$/)) {
push @{$groups{$base}}, $id;
$is_snap{$id} = 1;
}
elsif (my ($base2) = ($id =~ /^(.+)-\d{4}$/)) {
push @{$groups{$base2}}, $id;
$is_snap{$id} = 1;
}
}
my @rows;
( run in 0.714 second using v1.01-cache-2.11-cpan-f56aa216473 )