1
0
Files
Toak/Commands/SkillCommand.cs

125 lines
4.4 KiB
C#

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<bool> 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<string>("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<SkillDefinition>(
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<string>("Skill [green]Name[/]:");
var description = AnsiConsole.Ask<string>("Skill [green]Description[/]:");
var hotwordsStr = AnsiConsole.Ask<string>("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<string>()
.Title("What is the [green]Action[/] type?")
.AddChoices(new[] { "type", "script" })
);
string? scriptPath = null;
if (action == "script")
{
scriptPath = AnsiConsole.Ask<string>("Enter the absolute [green]Script Path[/] (~/ is allowed):");
}
var systemPrompt = AnsiConsole.Ask<string>("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}");
}
}
}