skills support

This commit is contained in:
Luxferre
2026-09-10 10:38:36 +03:00
parent 5b5853a2dd
commit b3f2864f6a
3 changed files with 220 additions and 2 deletions
+94 -1
View File
@@ -56,7 +56,8 @@ type Cfg struct {
func internalKey(k string) bool {
switch k {
case "endpoint", "model", "temperature", "stream", "api_key", "timeout",
"shell_timeout", "max_al_iterations", "color", "context_window":
"shell_timeout", "max_al_iterations", "color", "context_window",
"bantam_tools_dir", "bantam_skills_dir":
return true
}
return false
@@ -306,6 +307,18 @@ func toolsDir(cfg *Cfg) string {
return ""
}
// skillsDir returns the directory Bantam should scan for skills, preferring the
// BANTAM_SKILLS_DIR environment variable, then the bantam_skills_dir config key.
func skillsDir(cfg *Cfg) string {
if v := strings.TrimSpace(os.Getenv("BANTAM_SKILLS_DIR")); v != "" {
return v
}
if v := strings.TrimSpace(cfg.Raw["bantam_skills_dir"]); v != "" {
return v
}
return ""
}
func c(t string, cs ...int) string {
if !COL || len(cs) == 0 { return t }
s := make([]string, len(cs))
@@ -1801,6 +1814,71 @@ func readLine(prompt string) (string, bool) {
}
}
// skillPrompt resolves and composes the prompt for the /skill command: it reads
// <dir>/<name>/SKILL.md (or an absolute path when no skills directory is set) and
// prefixes its contents to the optional user prompt.
func skillPrompt(u string, cfg *Cfg) (string, error) {
rest := strings.TrimSpace(strings.TrimPrefix(u, "/skill"))
if rest == "" {
return "", fmt.Errorf("usage: /skill <skill_name|absolute_path> [prompt]")
}
fields := strings.SplitN(rest, " ", 2)
name := fields[0]
prompt := ""
if len(fields) == 2 {
prompt = strings.TrimSpace(fields[1])
}
sd := skillsDir(cfg)
var path string
if sd != "" {
path = filepath.Join(sd, name, "SKILL.md")
} else if strings.HasSuffix(name, "SKILL.md") {
path = name // absolute path to the SKILL.md file itself
} else {
path = filepath.Join(name, "SKILL.md") // absolute path to the skill directory
}
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("cannot read skill %q: %v", name, err)
}
content := strings.TrimRight(string(data), "\r\n")
if prompt != "" {
return content + "\n\n" + prompt, nil
}
return content, nil
}
// listSkills prints every skill found under the configured skills directory, or a
// clear notice when none is configured / none are present.
func listSkills(cfg *Cfg) {
sd := skillsDir(cfg)
if sd == "" {
fmt.Println(c("No skills directory configured (set BANTAM_SKILLS_DIR or bantam_skills_dir).", 33))
return
}
entries, err := os.ReadDir(sd)
if err != nil {
fmt.Println(c("[skill error: cannot read skills dir: "+err.Error()+"]", 31))
return
}
var names []string
for _, e := range entries {
if e.IsDir() {
if _, err := os.Stat(filepath.Join(sd, e.Name(), "SKILL.md")); err == nil {
names = append(names, e.Name())
}
}
}
if len(names) == 0 {
fmt.Println(c("No skills found in "+sd, 33))
return
}
fmt.Println(c("Available skills in "+sd+":", 1, 36))
for _, n := range names {
fmt.Println(c(" "+n, 32))
}
}
func runDirectShell(cmd string, timeout int) {
cmd = filterText(strings.TrimSpace(cmd))
if cmd == "" { return }
@@ -1844,6 +1922,9 @@ func main() {
if td := toolsDir(&cfg); td != "" {
sp += "\n\nExtra shell tools can be found at " + td
}
if sd := skillsDir(&cfg); sd != "" {
sp += "\n\nSkills may be discovered and invoked from " + sd
}
COL = col(cfg)
stdin = bufio.NewReader(os.Stdin)
msgs := []Message{{Role: "system", Content: strp(sp)}}
@@ -1987,6 +2068,17 @@ func main() {
fmt.Println(c("Usage: /cfg <param> [val]", 31))
}
continue
case strings.HasPrefix(u, "/skill"):
if strings.TrimSpace(strings.TrimPrefix(u, "/skill")) == "" {
listSkills(&cfg) // bare /skill lists available skills
continue
}
composed, err := skillPrompt(u, &cfg)
if err != nil {
fmt.Println(c("[skill error: "+err.Error()+"]", 31))
continue
}
u = composed // fall through to a normal LLM turn with the composed prompt
case u == "/models":
ml, err := listModels(&cfg)
if err != nil {
@@ -2014,6 +2106,7 @@ func main() {
{"/endpoint [e]", "alias for /cfg endpoint"},
{"!<cmd>", "run shell command directly"},
{"/models", "list models at endpoint"},
{"/skill <n> [p]", "load SKILL.md and run as prompt"},
{"/help", "show help"},
} {
fmt.Println(c(fmt.Sprintf(" %-18s", kv[0]), 1, 32) + kv[1])