using System; using System.CommandLine; using System.IO; using System.Text.Json; using System.Threading.Tasks; using Spectre.Console; using Toak.Core.Skills; using Toak.Serialization; namespace Toak.Commands; public static class SkillCommand { public static Command CreateCommand(Option verboseOption) { var skillCmd = new Command("skill", "Manage dynamic skills (list, add, remove)"); var listCmd = new Command("list", "List all available skills"); listCmd.SetHandler(ExecuteListAsync); skillCmd.AddCommand(listCmd); // Add var addCmd = new Command("add", "Add a new skill interactively"); addCmd.SetHandler(ExecuteAddAsync); skillCmd.AddCommand(addCmd); // Remove var removeCmd = new Command("remove", "Remove a skill"); var nameArg = new Argument("name", "The name of the skill to remove"); removeCmd.AddArgument(nameArg); removeCmd.SetHandler((name) => ExecuteRemoveAsync(name), nameArg); skillCmd.AddCommand(removeCmd); return skillCmd; } private static async Task ExecuteListAsync() { SkillRegistry.Initialize(); var table = new Table().Border(TableBorder.Rounded); table.AddColumn("Name"); table.AddColumn("Action"); table.AddColumn("Hotwords"); table.AddColumn("Description"); table.AddColumn("ScriptPath"); foreach (var skill in SkillRegistry.AllSkills) { var def = JsonSerializer.Deserialize( File.ReadAllText(Path.Combine(SkillRegistry.SkillsDirectory, $"{skill.Name.ToLowerInvariant()}.json")), AppJsonSerializerContext.Default.SkillDefinition); if (def == null) continue; table.AddRow( $"[green]{def.Name}[/]", !string.IsNullOrEmpty(def.Action) ? $"[yellow]{def.Action}[/]" : "type", $"[blue]{string.Join(", ", def.Hotwords)}[/]", def.Description, def.Action == "script" ? $"[dim]{def.ScriptPath ?? "None"}[/]" : "-" ); } AnsiConsole.Write(table); } private static async Task ExecuteAddAsync() { AnsiConsole.MarkupLine("[bold blue]Add a new Dynamic Skill[/]"); var name = AnsiConsole.Ask("Skill [green]Name[/]:"); var description = AnsiConsole.Ask("Skill [green]Description[/]:"); var hotwordsStr = AnsiConsole.Ask("Comma-separated [green]Hotwords[/] (e.g. 'System my skill, System do skill'):"); var hotwords = hotwordsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var action = AnsiConsole.Prompt( new SelectionPrompt() .Title("What is the [green]Action[/] type?") .AddChoices(new[] { "type", "script" }) ); string? scriptPath = null; if (action == "script") { scriptPath = AnsiConsole.Ask("Enter the absolute [green]Script Path[/] (~/ is allowed):"); } var systemPrompt = AnsiConsole.Ask("Enter the [green]System Prompt[/] (use {transcript} to inject the user's speech):"); var def = new SkillDefinition { Name = name, Description = description, Hotwords = hotwords, Action = action, ScriptPath = scriptPath, SystemPrompt = systemPrompt }; SkillRegistry.Initialize(); // ensure dir exists string filename = Path.Combine(SkillRegistry.SkillsDirectory, $"{name.ToLowerInvariant()}.json"); string json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition); File.WriteAllText(filename, json); AnsiConsole.MarkupLine($"[bold green]Success![/] Skill '{name}' saved to {filename}"); } private static async Task ExecuteRemoveAsync(string name) { SkillRegistry.Initialize(); var filename = Path.Combine(SkillRegistry.SkillsDirectory, $"{name.ToLowerInvariant()}.json"); if (File.Exists(filename)) { File.Delete(filename); AnsiConsole.MarkupLine($"[bold green]Success![/] Deleted skill '{name}' ({filename})"); } else { AnsiConsole.MarkupLine($"[bold red]Error:[/] Skill file not found: {filename}"); } } }