@@ -5,6 +5,7 @@ mod channels;
55mod config;
66mod format;
77mod session;
8+ mod skills;
89mod tools;
910
1011use crate :: agent:: agent_loop:: { HybridLlmRouter , ToolCallingLoop } ;
@@ -14,8 +15,10 @@ use crate::channel::{Channel, ChannelManager, build_channels_from_config};
1415use crate :: channels:: cli:: CliChannel ;
1516use crate :: config:: Config ;
1617use crate :: session:: SessionManager ;
18+ use crate :: skills:: { Skill , SkillExecutor } ;
1719use crate :: tools:: create_default_registry;
1820use clap:: { Parser , Subcommand } ;
21+ use std:: collections:: HashMap ;
1922use std:: path:: PathBuf ;
2023use std:: sync:: Arc ;
2124
@@ -47,6 +50,37 @@ enum Commands {
4750 } ,
4851 /// Run as gateway server with all enabled channels.
4952 Gateway ,
53+ /// Manage skills (workflows).
54+ Skill {
55+ #[ command( subcommand) ]
56+ command : SkillCommands ,
57+ } ,
58+ }
59+
60+ #[ derive( Subcommand , Debug ) ]
61+ enum SkillCommands {
62+ /// Save a skill from a JSON file.
63+ Save {
64+ /// Path to the skill JSON file
65+ path : PathBuf ,
66+ } ,
67+ /// Load and display a skill.
68+ Load {
69+ /// Name of the skill to load
70+ name : String ,
71+ } ,
72+ /// List all saved skills.
73+ List ,
74+ /// Run a skill with optional inputs.
75+ Run {
76+ /// Name of the skill to run
77+ name : String ,
78+ /// Input values as key=value pairs (e.g., name=Alice message=hello)
79+ #[ arg( value_name = "INPUTS" ) ]
80+ inputs : Vec < String > ,
81+ } ,
82+ /// Cancel the currently running skill.
83+ Cancel ,
5084}
5185
5286#[ tokio:: main]
@@ -92,6 +126,10 @@ async fn main() -> anyhow::Result<()> {
92126 log:: info!( "Starting in gateway mode" ) ;
93127 run_gateway_mode ( config) . await ?;
94128 }
129+ Commands :: Skill { command } => {
130+ log:: info!( "Executing skill command" ) ;
131+ run_skill_command ( command) . await ?;
132+ }
95133 }
96134
97135 log:: info!( "terraphim-tinyclaw shutting down" ) ;
@@ -226,3 +264,144 @@ async fn run_gateway_mode(config: Config) -> anyhow::Result<()> {
226264
227265 Ok ( ( ) )
228266}
267+
268+ async fn run_skill_command ( command : SkillCommands ) -> anyhow:: Result < ( ) > {
269+ let executor = SkillExecutor :: with_default_storage ( )
270+ . map_err ( |e| anyhow:: anyhow!( "Failed to initialize skill executor: {}" , e) ) ?;
271+
272+ match command {
273+ SkillCommands :: Save { path } => {
274+ let json = tokio:: fs:: read_to_string ( & path)
275+ . await
276+ . map_err ( |e| anyhow:: anyhow!( "Failed to read skill file: {}" , e) ) ?;
277+
278+ let skill: Skill = serde_json:: from_str ( & json)
279+ . map_err ( |e| anyhow:: anyhow!( "Invalid skill JSON: {}" , e) ) ?;
280+
281+ executor
282+ . save_skill ( & skill)
283+ . map_err ( |e| anyhow:: anyhow!( "Failed to save skill: {}" , e) ) ?;
284+
285+ println ! (
286+ "✓ Skill '{}' saved successfully (v{})" ,
287+ skill. name, skill. version
288+ ) ;
289+ }
290+
291+ SkillCommands :: Load { name } => {
292+ let skill = executor
293+ . load_skill ( & name)
294+ . map_err ( |e| anyhow:: anyhow!( "Failed to load skill: {}" , e) ) ?;
295+
296+ println ! ( "Skill: {}" , skill. name) ;
297+ println ! ( "Version: {}" , skill. version) ;
298+ println ! ( "Description: {}" , skill. description) ;
299+ if let Some ( author) = skill. author {
300+ println ! ( "Author: {}" , author) ;
301+ }
302+
303+ if !skill. inputs . is_empty ( ) {
304+ println ! ( "\n Inputs:" ) ;
305+ for input in & skill. inputs {
306+ let req = if input. required {
307+ "required"
308+ } else {
309+ "optional"
310+ } ;
311+ let default = input
312+ . default
313+ . as_ref ( )
314+ . map ( |d| format ! ( " (default: {})" , d) )
315+ . unwrap_or_default ( ) ;
316+ println ! (
317+ " - {}: {} [{}]{}" ,
318+ input. name, input. description, req, default
319+ ) ;
320+ }
321+ }
322+
323+ println ! ( "\n Steps ({} total):" , skill. steps. len( ) ) ;
324+ for ( i, step) in skill. steps . iter ( ) . enumerate ( ) {
325+ let step_type = match step {
326+ crate :: skills:: SkillStep :: Tool { tool, .. } => format ! ( "tool: {}" , tool) ,
327+ crate :: skills:: SkillStep :: Llm { .. } => "llm" . to_string ( ) ,
328+ crate :: skills:: SkillStep :: Shell { .. } => "shell" . to_string ( ) ,
329+ } ;
330+ println ! ( " {}. {}" , i + 1 , step_type) ;
331+ }
332+ }
333+
334+ SkillCommands :: List => {
335+ let skills = executor
336+ . list_skills ( )
337+ . map_err ( |e| anyhow:: anyhow!( "Failed to list skills: {}" , e) ) ?;
338+
339+ if skills. is_empty ( ) {
340+ println ! ( "No skills saved. Use 'skill save <file>' to add one." ) ;
341+ } else {
342+ println ! ( "Saved skills ({} total):" , skills. len( ) ) ;
343+ for skill in skills {
344+ println ! (
345+ " • {} (v{}) - {}" ,
346+ skill. name, skill. version, skill. description
347+ ) ;
348+ }
349+ }
350+ }
351+
352+ SkillCommands :: Run { name, inputs } => {
353+ let skill = executor
354+ . load_skill ( & name)
355+ . map_err ( |e| anyhow:: anyhow!( "Failed to load skill: {}" , e) ) ?;
356+
357+ // Parse inputs
358+ let mut input_map = HashMap :: new ( ) ;
359+ for input in inputs {
360+ if let Some ( ( key, value) ) = input. split_once ( '=' ) {
361+ input_map. insert ( key. to_string ( ) , value. to_string ( ) ) ;
362+ } else {
363+ eprintln ! (
364+ "Warning: Invalid input format '{}', expected key=value" ,
365+ input
366+ ) ;
367+ }
368+ }
369+
370+ println ! ( "Running skill '{}'..." , skill. name) ;
371+
372+ let result = executor
373+ . execute_skill ( & skill, input_map, None )
374+ . await
375+ . map_err ( |e| anyhow:: anyhow!( "Skill execution failed: {}" , e) ) ?;
376+
377+ println ! ( "\n Status: {:?}" , result. status) ;
378+ println ! ( "Duration: {}ms" , result. duration_ms) ;
379+
380+ if !result. output . is_empty ( ) {
381+ println ! ( "\n Output:\n {}" , result. output) ;
382+ }
383+
384+ if !result. execution_log . is_empty ( ) {
385+ println ! ( "\n Execution Log:" ) ;
386+ for log in & result. execution_log {
387+ let status = if log. success { "✓" } else { "✗" } ;
388+ println ! (
389+ " {} Step {} ({}): {}ms - {}" ,
390+ status,
391+ log. step_number + 1 ,
392+ log. step_type,
393+ log. duration_ms,
394+ log. output. chars( ) . take( 50 ) . collect:: <String >( )
395+ ) ;
396+ }
397+ }
398+ }
399+
400+ SkillCommands :: Cancel => {
401+ executor. cancel ( ) ;
402+ println ! ( "Cancellation signal sent." ) ;
403+ }
404+ }
405+
406+ Ok ( ( ) )
407+ }
0 commit comments