mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
CLI to list skills with token counts (#9326)
This commit is contained in:
@@ -29,6 +29,7 @@ use crate::commands::schedule::{
|
||||
handle_schedule_sessions,
|
||||
};
|
||||
use crate::commands::session::{handle_session_list, handle_session_remove};
|
||||
use crate::commands::skills::handle_skills_list;
|
||||
use crate::recipes::extract_from_cli::extract_recipe_info_from_cli;
|
||||
use crate::recipes::recipe::{explain_recipe, render_recipe_as_yaml};
|
||||
use crate::session::{build_session, SessionBuilderConfig};
|
||||
@@ -699,6 +700,13 @@ enum PluginCommand {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum SkillsCommand {
|
||||
/// List all skills available to the goose agent
|
||||
#[command(about = "List all skills available to the goose agent")]
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum RecipeCommand {
|
||||
/// Validate a recipe file
|
||||
@@ -910,6 +918,13 @@ enum Command {
|
||||
command: RecipeCommand,
|
||||
},
|
||||
|
||||
/// Skill utilities
|
||||
#[command(about = "Skill utilities")]
|
||||
Skills {
|
||||
#[command(subcommand)]
|
||||
command: SkillsCommand,
|
||||
},
|
||||
|
||||
/// Manage plugins
|
||||
#[command(about = "Manage plugins")]
|
||||
Plugin {
|
||||
@@ -1273,6 +1288,7 @@ fn get_command_name(command: &Option<Command>) -> &'static str {
|
||||
#[cfg(feature = "update")]
|
||||
Some(Command::Update { .. }) => "update",
|
||||
Some(Command::Recipe { .. }) => "recipe",
|
||||
Some(Command::Skills { .. }) => "skills",
|
||||
Some(Command::Plugin { .. }) => "plugin",
|
||||
Some(Command::Term { .. }) => "term",
|
||||
Some(Command::Tui { .. }) => "tui",
|
||||
@@ -1799,6 +1815,12 @@ fn handle_recipe_subcommand(command: RecipeCommand) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_skills_subcommand(command: SkillsCommand) -> Result<()> {
|
||||
match command {
|
||||
SkillsCommand::List => handle_skills_list().await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_term_subcommand(command: TermCommand) -> Result<()> {
|
||||
match command {
|
||||
TermCommand::Init {
|
||||
@@ -2129,6 +2151,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Some(Command::Recipe { command }) => handle_recipe_subcommand(command),
|
||||
Some(Command::Skills { command }) => handle_skills_subcommand(command).await,
|
||||
Some(Command::Plugin { command }) => handle_plugin_subcommand(command),
|
||||
Some(Command::Term { command }) => handle_term_subcommand(command).await,
|
||||
Some(Command::Tui { args }) => crate::commands::tui::handle_tui(args),
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod recipe;
|
||||
pub mod review;
|
||||
pub mod schedule;
|
||||
pub mod session;
|
||||
pub mod skills;
|
||||
pub mod term;
|
||||
pub mod tui;
|
||||
#[cfg(feature = "update")]
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
use anyhow::Result;
|
||||
use console::{measure_text_width, Term};
|
||||
use goose::skills::list_installed_skills;
|
||||
use goose::token_counter::create_token_counter;
|
||||
|
||||
const DESCRIPTION_PREVIEW_CHARS: usize = 50;
|
||||
const SEPARATOR: &str = " | ";
|
||||
const MIN_DESCRIPTION_WIDTH: usize = 4;
|
||||
const MIN_LOCATION_WIDTH: usize = 4;
|
||||
const NAME_HEADER: &str = "Name";
|
||||
const DESCRIPTION_HEADER: &str = "Description";
|
||||
const DESCRIPTION_TOKENS_HEADER: &str = "Description tokens";
|
||||
const CONTENT_TOKENS_HEADER: &str = "Content tokens";
|
||||
const LOCATION_HEADER: &str = "Location";
|
||||
|
||||
struct SkillRow {
|
||||
name: String,
|
||||
description: String,
|
||||
description_tokens: String,
|
||||
content_tokens: String,
|
||||
location: String,
|
||||
}
|
||||
|
||||
struct ColumnWidths {
|
||||
name: usize,
|
||||
description: usize,
|
||||
description_tokens: usize,
|
||||
content_tokens: usize,
|
||||
location: usize,
|
||||
}
|
||||
|
||||
pub async fn handle_skills_list() -> Result<()> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let terminal_width = terminal_width();
|
||||
let token_counter = create_token_counter().await.map_err(anyhow::Error::msg)?;
|
||||
let mut skills = list_installed_skills(Some(&cwd));
|
||||
skills.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
let rows = skills
|
||||
.iter()
|
||||
.map(|skill| SkillRow {
|
||||
name: skill.name.clone(),
|
||||
description: description_preview(&skill.description),
|
||||
description_tokens: token_counter.count_tokens(&skill.description).to_string(),
|
||||
content_tokens: token_counter.count_tokens(&skill.content).to_string(),
|
||||
location: skill.path.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let widths = column_widths(&rows, terminal_width);
|
||||
|
||||
println!("{}", header_line(&widths, terminal_width));
|
||||
for row in rows {
|
||||
println!("{}", skill_line(&row, &widths, terminal_width));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn terminal_width() -> Option<usize> {
|
||||
Term::stdout()
|
||||
.size_checked()
|
||||
.map(|(_height, width)| width as usize)
|
||||
}
|
||||
|
||||
fn column_widths(rows: &[SkillRow], max_display_width: Option<usize>) -> ColumnWidths {
|
||||
let description_tokens = max_width(
|
||||
DESCRIPTION_TOKENS_HEADER,
|
||||
rows.iter().map(|row| row.description_tokens.as_str()),
|
||||
);
|
||||
let content_tokens = max_width(
|
||||
CONTENT_TOKENS_HEADER,
|
||||
rows.iter().map(|row| row.content_tokens.as_str()),
|
||||
);
|
||||
let longest_name = max_width(NAME_HEADER, rows.iter().map(|row| row.name.as_str()));
|
||||
let description = max_width(
|
||||
DESCRIPTION_HEADER,
|
||||
rows.iter().map(|row| row.description.as_str()),
|
||||
);
|
||||
let location = max_width(
|
||||
LOCATION_HEADER,
|
||||
rows.iter().map(|row| row.location.as_str()),
|
||||
);
|
||||
|
||||
let Some(width) = max_display_width else {
|
||||
return ColumnWidths {
|
||||
name: longest_name,
|
||||
description,
|
||||
description_tokens,
|
||||
content_tokens,
|
||||
location,
|
||||
};
|
||||
};
|
||||
|
||||
let separator_width = measure_text_width(SEPARATOR) * 4;
|
||||
let available_width = width.saturating_sub(separator_width);
|
||||
let dynamic_width = available_width.saturating_sub(description_tokens + content_tokens);
|
||||
|
||||
let name =
|
||||
longest_name.min(dynamic_width.saturating_sub(MIN_DESCRIPTION_WIDTH + MIN_LOCATION_WIDTH));
|
||||
let remaining_after_name = dynamic_width.saturating_sub(name);
|
||||
let description = description.min(remaining_after_name.saturating_sub(MIN_LOCATION_WIDTH));
|
||||
let remaining_after_description = remaining_after_name.saturating_sub(description);
|
||||
let location = location.min(remaining_after_description);
|
||||
|
||||
ColumnWidths {
|
||||
name,
|
||||
description,
|
||||
description_tokens,
|
||||
content_tokens,
|
||||
location,
|
||||
}
|
||||
}
|
||||
|
||||
fn max_width<'a>(header: &str, values: impl Iterator<Item = &'a str>) -> usize {
|
||||
values
|
||||
.map(measure_text_width)
|
||||
.chain(std::iter::once(measure_text_width(header)))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn header_line(widths: &ColumnWidths, max_display_width: Option<usize>) -> String {
|
||||
let line = format_line(
|
||||
NAME_HEADER,
|
||||
DESCRIPTION_HEADER,
|
||||
DESCRIPTION_TOKENS_HEADER,
|
||||
CONTENT_TOKENS_HEADER,
|
||||
LOCATION_HEADER,
|
||||
widths,
|
||||
);
|
||||
|
||||
match max_display_width {
|
||||
Some(width) => truncate_to_display_width(&line, width),
|
||||
None => line,
|
||||
}
|
||||
}
|
||||
|
||||
fn skill_line(row: &SkillRow, widths: &ColumnWidths, max_display_width: Option<usize>) -> String {
|
||||
let line = format_line(
|
||||
&row.name,
|
||||
&row.description,
|
||||
&row.description_tokens,
|
||||
&row.content_tokens,
|
||||
&row.location,
|
||||
widths,
|
||||
);
|
||||
|
||||
match max_display_width {
|
||||
Some(width) => truncate_to_display_width(&line, width),
|
||||
None => line,
|
||||
}
|
||||
}
|
||||
|
||||
fn format_line(
|
||||
name: &str,
|
||||
description: &str,
|
||||
description_tokens: &str,
|
||||
content_tokens: &str,
|
||||
location: &str,
|
||||
widths: &ColumnWidths,
|
||||
) -> String {
|
||||
format!(
|
||||
"{}{}{}{}{}{}{}{}{}",
|
||||
pad_to_display_width(&truncate_to_display_width(name, widths.name), widths.name),
|
||||
SEPARATOR,
|
||||
pad_to_display_width(
|
||||
&truncate_to_display_width(description, widths.description),
|
||||
widths.description
|
||||
),
|
||||
SEPARATOR,
|
||||
pad_to_display_width(description_tokens, widths.description_tokens),
|
||||
SEPARATOR,
|
||||
pad_to_display_width(content_tokens, widths.content_tokens),
|
||||
SEPARATOR,
|
||||
pad_to_display_width(
|
||||
&truncate_to_display_width(location, widths.location),
|
||||
widths.location
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn description_preview(description: &str) -> String {
|
||||
let normalized = description.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
truncate_to_chars(&normalized, DESCRIPTION_PREVIEW_CHARS)
|
||||
}
|
||||
|
||||
fn truncate_to_chars(text: &str, max_chars: usize) -> String {
|
||||
if text.chars().count() <= max_chars {
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
if max_chars <= 3 {
|
||||
return ".".repeat(max_chars);
|
||||
}
|
||||
|
||||
let mut output = text.chars().take(max_chars - 3).collect::<String>();
|
||||
output.push_str("...");
|
||||
output
|
||||
}
|
||||
|
||||
fn truncate_to_display_width(text: &str, max_width: usize) -> String {
|
||||
if measure_text_width(text) <= max_width {
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
if max_width <= 3 {
|
||||
return ".".repeat(max_width);
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
let suffix_width = measure_text_width("...");
|
||||
|
||||
for ch in text.chars() {
|
||||
output.push(ch);
|
||||
if measure_text_width(&output) + suffix_width > max_width {
|
||||
output.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("...");
|
||||
output
|
||||
}
|
||||
|
||||
fn pad_to_display_width(text: &str, width: usize) -> String {
|
||||
let padding = width.saturating_sub(measure_text_width(text));
|
||||
format!("{}{}", text, " ".repeat(padding))
|
||||
}
|
||||
Reference in New Issue
Block a user