skills support
This commit is contained in:
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user