@@ -19,6 +19,7 @@ import (
1919 "github.com/BackendStack21/kode/internal/llm"
2020 "github.com/BackendStack21/kode/internal/render"
2121 "github.com/BackendStack21/kode/internal/session"
22+ "github.com/BackendStack21/kode/internal/skills"
2223)
2324
2425// version is set at build time via ldflags: -ldflags "-X main.version=v0.2.1"
@@ -106,6 +107,11 @@ func main() {
106107 fmt .Fprintf (os .Stderr , "kode: %v\n " , err )
107108 os .Exit (1 )
108109 }
110+ case "skill" :
111+ if err := skillCmd (os .Args [2 :]); err != nil {
112+ fmt .Fprintf (os .Stderr , "kode: %v\n " , err )
113+ os .Exit (1 )
114+ }
109115 default :
110116 fmt .Fprintf (os .Stderr , "kode: unknown command %q\n " , os .Args [1 ])
111117 printUsage ()
@@ -134,6 +140,7 @@ type runFlags struct {
134140 NoColor * bool // nil = not set
135141 NoAgents * bool // nil = not set
136142 Session * bool // nil = not set; true = save session after run
143+ Learn * bool // nil = not set; true = enable skills learning mode
137144 Task string
138145
139146 // Sandbox-specific CLI flags
@@ -175,6 +182,9 @@ func parseRunFlags(args []string) (runFlags, error) {
175182 case "--sandbox" :
176183 f .Sandbox = boolPtr (true )
177184 i ++
185+ case "--learn" :
186+ f .Learn = boolPtr (true )
187+ i ++
178188 case "--no-color" :
179189 f .NoColor = boolPtr (true )
180190 i ++
@@ -507,6 +517,7 @@ func run(args []string) error {
507517 Sandbox : f .Sandbox ,
508518 NoColor : f .NoColor ,
509519 NoAgents : f .NoAgents ,
520+ Learn : f .Learn ,
510521 System : f .System ,
511522 Task : f .Task ,
512523
@@ -536,9 +547,18 @@ func run(args []string) error {
536547 Volumes : resolved .SandboxVolumes ,
537548 }
538549
550+ // Skills setup
551+ var sm * skills.SkillManager
552+ if resolved .Skills .Learn {
553+ sm = skills .NewSkillManager (
554+ expandHome ("~/.kode/skills" ),
555+ "./.kode/skills" ,
556+ )
557+ }
558+
539559 // Sandbox setup
540560 var sandboxCleanup func () error
541- tools := builtinTools (resolved .Dangerous )
561+ tools := builtinTools (resolved .Dangerous , sm )
542562
543563 if resolved .Sandbox {
544564 cleanup , err := setupSandbox (tools , sbCfg )
@@ -556,6 +576,12 @@ func run(args []string) error {
556576 color := ! resolved .NoColor && render .ColorEnabled ()
557577 rend := render .New (os .Stderr , color ).WithModel (modelLabel )
558578
579+ // Resolve skills config pointer (only when learn mode is enabled)
580+ var skillsCfg * skills.SkillsConfig
581+ if resolved .Skills .Learn {
582+ skillsCfg = & resolved .Skills
583+ }
584+
559585 agent , err := kode .New (kode.Config {
560586 Model : resolved .Model ,
561587 BaseURL : resolved .BaseURL ,
@@ -567,6 +593,8 @@ func run(args []string) error {
567593 Tools : tools ,
568594 SandboxCleanup : sandboxCleanup ,
569595 Renderer : rend ,
596+ Skills : skillsCfg ,
597+ SkillManager : sm ,
570598 })
571599 if err != nil {
572600 return err
@@ -720,12 +748,24 @@ func setupSandbox(tools []kode.Tool, cfg sandboxConfig) (func() error, error) {
720748 return cleanup , nil
721749}
722750
723- func builtinTools (dc danger.DangerousConfig ) []kode.Tool {
724- return []kode.Tool {
751+ func builtinTools (dc danger.DangerousConfig , sm * skills. SkillManager ) []kode.Tool {
752+ tools := []kode.Tool {
725753 & shellTool {
726754 dangerousConfig : dc ,
727755 },
728756 }
757+
758+ if sm != nil {
759+ tools = append (tools ,
760+ & skills.SkillLoadTool {Manager : sm },
761+ & skills.SkillListTool {Manager : sm },
762+ & skills.SkillSaveTool {Manager : sm },
763+ & skills.SkillPatchTool {Manager : sm },
764+ & skills.SkillDeleteTool {Manager : sm },
765+ )
766+ }
767+
768+ return tools
729769}
730770
731771// getVersion returns the version string. Resolution order:
@@ -761,6 +801,170 @@ func getVersion() string {
761801 return "dev"
762802}
763803
804+ // ── Skill Commands ─────────────────────────────────────────────────────
805+
806+ // skillCmd handles `kode skill <list|view|save|delete|import|curate>`.
807+ func skillCmd (args []string ) error {
808+ if len (args ) == 0 {
809+ fmt .Fprintf (os .Stderr , "Usage: kode skill <list|view|save|delete|import|curate> [args]\n " )
810+ return nil
811+ }
812+
813+ userDir := expandHome ("~/.kode/skills" )
814+ os .MkdirAll (userDir , 0755 )
815+
816+ // The first argument is the subcommand
817+ sub := args [0 ]
818+ subArgs := args [1 :]
819+
820+ switch sub {
821+ case "list" :
822+ sm := skills .NewSkillManager (userDir , "./.kode/skills" )
823+ tool := & skills.SkillListTool {}
824+ tool .Manager = sm
825+ result , err := tool .Call ("{}" )
826+ if err != nil {
827+ return err
828+ }
829+ fmt .Println (result )
830+ return nil
831+
832+ case "view" :
833+ if len (subArgs ) == 0 {
834+ return fmt .Errorf ("usage: kode skill view <name>" )
835+ }
836+ sm := skills .NewSkillManager (userDir , "./.kode/skills" )
837+ tool := & skills.SkillLoadTool {}
838+ tool .Manager = sm
839+ result , err := tool .Call (`{"name": "` + subArgs [0 ] + `"}` )
840+ if err != nil {
841+ return err
842+ }
843+ fmt .Println (result )
844+ return nil
845+
846+ case "save" :
847+ if len (subArgs ) < 3 {
848+ return fmt .Errorf ("usage: kode skill save --name <name> --description <desc> --body <body>" )
849+ }
850+ return fmt .Errorf ("interactive save not yet supported — use the agent's skill_save tool" )
851+
852+ case "delete" :
853+ if len (subArgs ) == 0 {
854+ return fmt .Errorf ("usage: kode skill delete <name>" )
855+ }
856+ sm := skills .NewSkillManager (userDir , "./.kode/skills" )
857+ tool := & skills.SkillDeleteTool {}
858+ tool .Manager = sm
859+ result , err := tool .Call (`{"name": "` + subArgs [0 ] + `"}` )
860+ if err != nil {
861+ return err
862+ }
863+ fmt .Println (result )
864+ return nil
865+
866+ case "import" :
867+ if len (subArgs ) == 0 {
868+ return fmt .Errorf ("usage: kode skill import <uri> [--basic] [--yes]" )
869+ }
870+ uri := subArgs [0 ]
871+ basicOnly := false
872+ autoYes := false
873+ for _ , a := range subArgs [1 :] {
874+ switch a {
875+ case "--basic" :
876+ basicOnly = true
877+ case "--yes" :
878+ autoYes = true
879+ }
880+ }
881+
882+ llmCall := func (prompt string ) (string , error ) {
883+ // TODO: use configured LLM for assessment
884+ if basicOnly {
885+ return "" , fmt .Errorf ("basic mode — no LLM call" )
886+ }
887+ // For now, return elevated as a safe default
888+ return `{"risk_class": "elevated", "reasons": ["no LLM configured for assessment"], "what_it_does": "imported skill", "recommended_triggers": [], "red_flags": []}` , nil
889+ }
890+
891+ result , err := skills .ImportSkill (skills.ImportOptions {
892+ URI : uri ,
893+ MaxBytes : 1_048_576 ,
894+ Timeout : 5 ,
895+ BasicOnly : basicOnly ,
896+ AutoYes : autoYes ,
897+ UserDir : userDir ,
898+ }, func (assessment * skills.ImportAssessment ) bool {
899+ if autoYes {
900+ return true
901+ }
902+
903+ fmt .Fprintf (os .Stderr , "\n 📦 Skill Import\n " )
904+ fmt .Fprintf (os .Stderr , "━━━━━━━━━━━━━━━\n " )
905+ if assessment != nil {
906+ riskSymbol := "🟢"
907+ if assessment .RiskClass == "elevated" {
908+ riskSymbol = "🟡"
909+ } else if assessment .RiskClass == "dangerous" {
910+ riskSymbol = "🔴"
911+ }
912+ fmt .Fprintf (os .Stderr , "Risk: %s %s\n " , riskSymbol , assessment .RiskClass )
913+ fmt .Fprintf (os .Stderr , "What: %s\n " , assessment .WhatItDoes )
914+ if len (assessment .Reasons ) > 0 {
915+ fmt .Fprintf (os .Stderr , "Why:\n " )
916+ for _ , r := range assessment .Reasons {
917+ fmt .Fprintf (os .Stderr , " • %s\n " , r )
918+ }
919+ }
920+ if len (assessment .RedFlags ) > 0 {
921+ fmt .Fprintf (os .Stderr , "Red flags:\n " )
922+ for _ , r := range assessment .RedFlags {
923+ fmt .Fprintf (os .Stderr , " • %s\n " , r )
924+ }
925+ }
926+ }
927+ fmt .Fprintf (os .Stderr , "\n Import this skill? [Y/n]: " )
928+
929+ var response string
930+ fmt .Scanf ("%s" , & response )
931+ response = strings .ToLower (strings .TrimSpace (response ))
932+ return response == "" || response == "y" || response == "yes"
933+ }, llmCall )
934+ if err != nil {
935+ return err
936+ }
937+
938+ fmt .Printf ("✓ Imported %q from %s\n " , result .Skill .Name , uri )
939+ fmt .Printf (" Location: %s\n " , result .Path )
940+ return nil
941+
942+ case "curate" :
943+ sm := skills .NewSkillManager (userDir , "./.kode/skills" )
944+ allSkills := append (sm .Result .AutoLoad , sm .Result .Lazy ... )
945+ report := skills .CurateSkills (allSkills , skills.CurateOptions {
946+ StalenessDays : 90 ,
947+ Apply : false ,
948+ })
949+ fmt .Print (skills .FormatCurationReport (report ))
950+ return nil
951+
952+ default :
953+ return fmt .Errorf ("unknown skill command %q (use list, view, save, delete, import, curate)" , sub )
954+ }
955+ }
956+
957+ // expandHome replaces the leading ~/ with the user's home directory.
958+ func expandHome (path string ) string {
959+ if strings .HasPrefix (path , "~/" ) {
960+ home , err := os .UserHomeDir ()
961+ if err == nil {
962+ return strings .Replace (path , "~/" , home + "/" , 1 )
963+ }
964+ }
965+ return path
966+ }
967+
764968// ── Continue (Multi-Turn) ─────────────────────────────────────────────
765969
766970// continueCmd handles `kode continue [--id <id>] <task>`.
@@ -806,7 +1010,14 @@ func continueCmd(args []string) error {
8061010 }
8071011
8081012 // Build tools
809- tools := builtinTools (resolved .Dangerous )
1013+ var sm * skills.SkillManager
1014+ if resolved .Skills .Learn {
1015+ sm = skills .NewSkillManager (
1016+ expandHome ("~/.kode/skills" ),
1017+ "./.kode/skills" ,
1018+ )
1019+ }
1020+ tools := builtinTools (resolved .Dangerous , sm )
8101021 var sandboxCleanup func () error
8111022
8121023 systemMessage := resolved .System
@@ -839,6 +1050,12 @@ func continueCmd(args []string) error {
8391050 color := ! resolved .NoColor && render .ColorEnabled ()
8401051 rend := render .New (os .Stderr , color ).WithModel (modelLabel )
8411052
1053+ // Resolve skills config pointer (only when learn mode is enabled)
1054+ var skillsCfg * skills.SkillsConfig
1055+ if resolved .Skills .Learn {
1056+ skillsCfg = & resolved .Skills
1057+ }
1058+
8421059 agent , err := kode .New (kode.Config {
8431060 Model : resolved .Model ,
8441061 BaseURL : resolved .BaseURL ,
@@ -850,6 +1067,8 @@ func continueCmd(args []string) error {
8501067 Tools : tools ,
8511068 SandboxCleanup : sandboxCleanup ,
8521069 Renderer : rend ,
1070+ Skills : skillsCfg ,
1071+ SkillManager : sm ,
8531072 })
8541073 if err != nil {
8551074 return err
0 commit comments