CLI to list skills with token counts (#9326)
This commit is contained in:
@@ -29,6 +29,7 @@ use crate::commands::schedule::{
|
|||||||
handle_schedule_sessions,
|
handle_schedule_sessions,
|
||||||
};
|
};
|
||||||
use crate::commands::session::{handle_session_list, handle_session_remove};
|
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::extract_from_cli::extract_recipe_info_from_cli;
|
||||||
use crate::recipes::recipe::{explain_recipe, render_recipe_as_yaml};
|
use crate::recipes::recipe::{explain_recipe, render_recipe_as_yaml};
|
||||||
use crate::session::{build_session, SessionBuilderConfig};
|
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)]
|
#[derive(Subcommand)]
|
||||||
enum RecipeCommand {
|
enum RecipeCommand {
|
||||||
/// Validate a recipe file
|
/// Validate a recipe file
|
||||||
@@ -910,6 +918,13 @@ enum Command {
|
|||||||
command: RecipeCommand,
|
command: RecipeCommand,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Skill utilities
|
||||||
|
#[command(about = "Skill utilities")]
|
||||||
|
Skills {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: SkillsCommand,
|
||||||
|
},
|
||||||
|
|
||||||
/// Manage plugins
|
/// Manage plugins
|
||||||
#[command(about = "Manage plugins")]
|
#[command(about = "Manage plugins")]
|
||||||
Plugin {
|
Plugin {
|
||||||
@@ -1273,6 +1288,7 @@ fn get_command_name(command: &Option<Command>) -> &'static str {
|
|||||||
#[cfg(feature = "update")]
|
#[cfg(feature = "update")]
|
||||||
Some(Command::Update { .. }) => "update",
|
Some(Command::Update { .. }) => "update",
|
||||||
Some(Command::Recipe { .. }) => "recipe",
|
Some(Command::Recipe { .. }) => "recipe",
|
||||||
|
Some(Command::Skills { .. }) => "skills",
|
||||||
Some(Command::Plugin { .. }) => "plugin",
|
Some(Command::Plugin { .. }) => "plugin",
|
||||||
Some(Command::Term { .. }) => "term",
|
Some(Command::Term { .. }) => "term",
|
||||||
Some(Command::Tui { .. }) => "tui",
|
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<()> {
|
async fn handle_term_subcommand(command: TermCommand) -> Result<()> {
|
||||||
match command {
|
match command {
|
||||||
TermCommand::Init {
|
TermCommand::Init {
|
||||||
@@ -2129,6 +2151,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(Command::Recipe { command }) => handle_recipe_subcommand(command),
|
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::Plugin { command }) => handle_plugin_subcommand(command),
|
||||||
Some(Command::Term { command }) => handle_term_subcommand(command).await,
|
Some(Command::Term { command }) => handle_term_subcommand(command).await,
|
||||||
Some(Command::Tui { args }) => crate::commands::tui::handle_tui(args),
|
Some(Command::Tui { args }) => crate::commands::tui::handle_tui(args),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod recipe;
|
|||||||
pub mod review;
|
pub mod review;
|
||||||
pub mod schedule;
|
pub mod schedule;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
pub mod skills;
|
||||||
pub mod term;
|
pub mod term;
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
#[cfg(feature = "update")]
|
#[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