skills support
This commit is contained in:
@@ -70,6 +70,7 @@ All implementations read `model.cfg` (or `.bantam.cfg`, which takes priority if
|
|||||||
- `/cfg <param> [val]` — inspect or update a configuration parameter live (writes to `.bantam.cfg`)
|
- `/cfg <param> [val]` — inspect or update a configuration parameter live (writes to `.bantam.cfg`)
|
||||||
- `/model [val]` — alias for `/cfg model` (inspect or set the model)
|
- `/model [val]` — alias for `/cfg model` (inspect or set the model)
|
||||||
- `/endpoint [val]` — alias for `/cfg endpoint` (inspect or set the API endpoint)
|
- `/endpoint [val]` — alias for `/cfg endpoint` (inspect or set the API endpoint)
|
||||||
|
- `/skill` — with no argument, list every skill under the configured skills directory (or report that none are configured); with `<name> [prompt]`, load `<skills_dir>/<name>/SKILL.md` (or an absolute path when no skills dir is set) and send its contents prefixed to the optional `prompt` as the next user turn
|
||||||
- `/models` — query the `/models` path on the current inference endpoint and print a plain list of supported model IDs, marking the currently configured model with a leading `* ` (Go port)
|
- `/models` — query the `/models` path on the current inference endpoint and print a plain list of supported model IDs, marking the currently configured model with a leading `* ` (Go port)
|
||||||
- `!<cmd>` — execute a shell command directly through `shell_exec` without adding the result to the conversation context (Go port)
|
- `!<cmd>` — execute a shell command directly through `shell_exec` without adding the result to the conversation context (Go port)
|
||||||
- `/help` — show all supported commands
|
- `/help` — show all supported commands
|
||||||
@@ -117,7 +118,7 @@ Using these rules, everyone can build their own copy of Bantam from scratch in l
|
|||||||
2. Read model parameters from the config file (`.bantam.cfg` if present, else `model.cfg`) in `key=value` format and discover context window size.
|
2. Read model parameters from the config file (`.bantam.cfg` if present, else `model.cfg`) in `key=value` format and discover context window size.
|
||||||
3. Prepare a new message list with the system prompt (`role: "system"`).
|
3. Prepare a new message list with the system prompt (`role: "system"`).
|
||||||
4. Read the first command-line parameter. If non-empty, read user prompt from the specified file. If prefixed with `!`, execute the shell command directly via `shell_exec` and exit. Otherwise, append to `messages` (`role: "user"`), run `AL(cfg, messages)`, display token usage, and exit.
|
4. Read the first command-line parameter. If non-empty, read user prompt from the specified file. If prefixed with `!`, execute the shell command directly via `shell_exec` and exit. Otherwise, append to `messages` (`role: "user"`), run `AL(cfg, messages)`, display token usage, and exit.
|
||||||
5. Read user prompt from standard input (with `readline` line editing and history in `~/.bantam_history`; **Ctrl+J** inserts a real newline into the line being edited). If equal to `/quit` or EOF, exit. If equal to `/clear`, reset `messages` to step 3 and return to step 5. If starting with `/save`, write the whole `messages` array to `~/.bantam/sessions/<name>.json` (or `<project-md5>.json` if no name is given) and return to step 5. If equal to `/continue` or `/cont`, load the session corresponding to the current project's MD5 hash and return to step 5. If equal to `/list`, print saved sessions and their summaries (marking current project session) and return to step 5. If starting with `/load`, replace `messages` with the saved session's messages (by exact id or unique prefix) and return to step 5. If equal to `/compact`, ask the LLM to summarize the conversation by appending the compaction prompt to derive the summary, replace `messages` with `[system, summary-user-message]`, and return to step 5. If starting with `/cfg`, display the current value (`/cfg <param>`) or update the config live by writing to `.bantam.cfg` (`/cfg <param> <val>`) and return to step 5. `/model` and `/endpoint` are aliases for `/cfg model` and `/cfg endpoint` respectively and behave the same way. If equal to `/models`, query the `/models` path on the current inference endpoint and print a plain list of supported model IDs (the currently configured model marked with a leading `* `), then return to step 5. If starting with `!`, execute the command directly via `shell_exec` without adding the result to `messages` and return to step 5. If equal to `/help`, print the command list and return to step 5. After every user turn and on exit, auto-save `messages` to `~/.bantam/sessions/<project-md5>.json`.
|
5. Read user prompt from standard input (with `readline` line editing and history in `~/.bantam_history`; **Ctrl+J** inserts a real newline into the line being edited). If equal to `/quit` or EOF, exit. If equal to `/clear`, reset `messages` to step 3 and return to step 5. If starting with `/save`, write the whole `messages` array to `~/.bantam/sessions/<name>.json` (or `<project-md5>.json` if no name is given) and return to step 5. If equal to `/continue` or `/cont`, load the session corresponding to the current project's MD5 hash and return to step 5. If equal to `/list`, print saved sessions and their summaries (marking current project session) and return to step 5. If starting with `/load`, replace `messages` with the saved session's messages (by exact id or unique prefix) and return to step 5. If equal to `/compact`, ask the LLM to summarize the conversation by appending the compaction prompt to derive the summary, replace `messages` with `[system, summary-user-message]`, and return to step 5. If starting with `/cfg`, display the current value (`/cfg <param>`) or update the config live by writing to `.bantam.cfg` (`/cfg <param> <val>`) and return to step 5. `/model` and `/endpoint` are aliases for `/cfg model` and `/cfg endpoint` respectively and behave the same way. If equal to `/skill` (no argument), list every skill under the configured skills directory (or report that none is configured) and return to step 5. If starting with `/skill`, resolve the skill (a `<skills_dir>/<name>/SKILL.md` file, or an absolute path to a skill directory or `SKILL.md` file when no skills directory is configured), prefix its contents to the optional remaining text, and send the result as the next user turn (returning to step 5 after the response). If equal to `/models`, query the `/models` path on the current inference endpoint and print a plain list of supported model IDs (the currently configured model marked with a leading `* `), then return to step 5. If starting with `!`, execute the command directly via `shell_exec` without adding the result to `messages` and return to step 5. If equal to `/help`, print the command list and return to step 5. After every user turn and on exit, auto-save `messages` to `~/.bantam/sessions/<project-md5>.json`.
|
||||||
6. Append user prompt to `messages` (`role: "user"`), run `AL(cfg, messages)`, display token usage, check 60% context threshold for auto-compaction, and go to step 5.
|
6. Append user prompt to `messages` (`role: "user"`), run `AL(cfg, messages)`, display token usage, check 60% context threshold for auto-compaction, and go to step 5.
|
||||||
|
|
||||||
### Agentic loop (`AL(cfg, messages)`) function
|
### Agentic loop (`AL(cfg, messages)`) function
|
||||||
@@ -156,6 +157,7 @@ If the API rejects the request with an `Invalid assistant message: content or to
|
|||||||
- `reasoning_effort` (reasoning effort level, forwarded to chat completions API, default `high`)
|
- `reasoning_effort` (reasoning effort level, forwarded to chat completions API, default `high`)
|
||||||
|
|
||||||
- `bantam_tools_dir` (optional path to a directory of extra shell tools; the Go port appends `"Extra shell tools can be found at <dir>"` to the system prompt at startup when set. The `BANTAM_TOOLS_DIR` environment variable overrides this and is checked first; if neither is set, nothing is appended. Note: this is an agent-internal hint, not forwarded to the API.)
|
- `bantam_tools_dir` (optional path to a directory of extra shell tools; the Go port appends `"Extra shell tools can be found at <dir>"` to the system prompt at startup when set. The `BANTAM_TOOLS_DIR` environment variable overrides this and is checked first; if neither is set, nothing is appended. Note: this is an agent-internal hint, not forwarded to the API.)
|
||||||
|
- `bantam_skills_dir` (optional path to a directory of reusable *skills*; each skill lives in its own subdirectory as `<skill_name>/SKILL.md`. When set, the Go port appends `"Skills may be discovered and invoked from <dir>"` to the system prompt at startup, and the `/skill <skill_name> [prompt]` command loads a skill's `SKILL.md` and runs it as a prompt. The `BANTAM_SKILLS_DIR` environment variable overrides this and is checked first; if neither is set, `/skill` accepts an absolute path to a skill directory or `SKILL.md` file instead. Note: this is an agent-internal hint, not forwarded to the API.)
|
||||||
|
|
||||||
The Go port also supports SOCKS5 proxying via the `SOCKS_PROXY` (or `socks_proxy`) environment variable (e.g. `SOCKS_PROXY=socks5://127.0.0.1:1080` or `SOCKS_PROXY=127.0.0.1:1080`), falling back to standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables.
|
The Go port also supports SOCKS5 proxying via the `SOCKS_PROXY` (or `socks_proxy`) environment variable (e.g. `SOCKS_PROXY=socks5://127.0.0.1:1080` or `SOCKS_PROXY=127.0.0.1:1080`), falling back to standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables.
|
||||||
|
|
||||||
@@ -223,6 +225,17 @@ The `extras/` directory contains small, dependency-light shell scripts that exte
|
|||||||
|
|
||||||
If you keep your own collection of helper scripts, point Bantam at them with the `BANTAM_TOOLS_DIR` environment variable or the `bantam_tools_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`). When either is defined (environment variable taking precedence over the config key), the Go port appends the line `Extra shell tools can be found at <dir>` to the system prompt at startup, so the agent is aware of where to look for them. The `extras/` scripts shipped here are just examples of what such a directory can contain.
|
If you keep your own collection of helper scripts, point Bantam at them with the `BANTAM_TOOLS_DIR` environment variable or the `bantam_tools_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`). When either is defined (environment variable taking precedence over the config key), the Go port appends the line `Extra shell tools can be found at <dir>` to the system prompt at startup, so the agent is aware of where to look for them. The `extras/` scripts shipped here are just examples of what such a directory can contain.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
A *skill* is a reusable, self-contained instruction bundle the agent can load on demand. Each skill is a directory containing a `SKILL.md` file with the skill's prompt/instructions. Point Bantam at a skills directory with the `BANTAM_SKILLS_DIR` environment variable or the `bantam_skills_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`); the environment variable takes precedence. When either is set, the Go port appends `Skills may be discovered and invoked from <dir>` to the system prompt at startup, and the agent can run a skill with:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/skill # list every skill under the configured skills directory
|
||||||
|
/skill <name> [prompt] # loads <skills_dir>/<name>/SKILL.md and runs it as a prompt
|
||||||
|
```
|
||||||
|
|
||||||
|
A bare `/skill` (no name) lists all skills found under the configured skills directory — a subdirectory is treated as a skill only if it contains a `SKILL.md` file — or reports that no skills directory is configured. The contents of `SKILL.md` are prefixed to the optional `prompt` and sent as the next user turn. If neither `BANTAM_SKILLS_DIR` nor `bantam_skills_dir` is set, `/skill <name>` accepts an absolute path instead: either a skill directory (e.g. `/abs/path/to/skill`, which resolves to `/abs/path/to/skill/SKILL.md`) or a direct path to a `SKILL.md` file.
|
||||||
|
|
||||||
### `extras/websearch`
|
### `extras/websearch`
|
||||||
|
|
||||||
|
|
||||||
@@ -309,6 +322,10 @@ The default system prompt instructs the agent to respect `AGENTS.md` contents in
|
|||||||
|
|
||||||
Set the `BANTAM_TOOLS_DIR` environment variable (or the `bantam_tools_dir` key in the config file — `.bantam.cfg` if present, else `model.cfg`) to a directory containing your helper scripts. When defined, the Go port appends `Extra shell tools can be found at <dir>` to the system prompt at startup, making the agent aware of them. The environment variable takes precedence over the config key; if neither is set, nothing is appended.
|
Set the `BANTAM_TOOLS_DIR` environment variable (or the `bantam_tools_dir` key in the config file — `.bantam.cfg` if present, else `model.cfg`) to a directory containing your helper scripts. When defined, the Go port appends `Extra shell tools can be found at <dir>` to the system prompt at startup, making the agent aware of them. The environment variable takes precedence over the config key; if neither is set, nothing is appended.
|
||||||
|
|
||||||
|
### How do I give Bantam reusable skills?
|
||||||
|
|
||||||
|
Set the `BANTAM_SKILLS_DIR` environment variable (or the `bantam_skills_dir` key in the config file — `.bantam.cfg` if present, else `model.cfg`) to a directory where each subdirectory is a skill containing a `SKILL.md` file. When defined, the Go port appends `Skills may be discovered and invoked from <dir>` to the system prompt at startup, and you (or the agent) can run a skill with `/skill <name> [prompt]`. The environment variable takes precedence over the config key; if neither is set, `/skill` accepts an absolute path to a skill directory or `SKILL.md` file instead.
|
||||||
|
|
||||||
### Is there any common config place for Bantam?
|
### Is there any common config place for Bantam?
|
||||||
|
|
||||||
No, loading the config file (`.bantam.cfg` if present, else `model.cfg`) is deliberately only supported from the current working directory. This allows natural separation of configs per project. In case there's no config file inside the project, Bantam will use the `openrouter/free` model from Kilo Code with the temperature 0.7.
|
No, loading the config file (`.bantam.cfg` if present, else `model.cfg`) is deliberately only supported from the current working directory. This allows natural separation of configs per project. In case there's no config file inside the project, Bantam will use the `openrouter/free` model from Kilo Code with the temperature 0.7.
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ type Cfg struct {
|
|||||||
func internalKey(k string) bool {
|
func internalKey(k string) bool {
|
||||||
switch k {
|
switch k {
|
||||||
case "endpoint", "model", "temperature", "stream", "api_key", "timeout",
|
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 true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -306,6 +307,18 @@ func toolsDir(cfg *Cfg) string {
|
|||||||
return ""
|
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 {
|
func c(t string, cs ...int) string {
|
||||||
if !COL || len(cs) == 0 { return t }
|
if !COL || len(cs) == 0 { return t }
|
||||||
s := make([]string, len(cs))
|
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) {
|
func runDirectShell(cmd string, timeout int) {
|
||||||
cmd = filterText(strings.TrimSpace(cmd))
|
cmd = filterText(strings.TrimSpace(cmd))
|
||||||
if cmd == "" { return }
|
if cmd == "" { return }
|
||||||
@@ -1844,6 +1922,9 @@ func main() {
|
|||||||
if td := toolsDir(&cfg); td != "" {
|
if td := toolsDir(&cfg); td != "" {
|
||||||
sp += "\n\nExtra shell tools can be found at " + 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)
|
COL = col(cfg)
|
||||||
stdin = bufio.NewReader(os.Stdin)
|
stdin = bufio.NewReader(os.Stdin)
|
||||||
msgs := []Message{{Role: "system", Content: strp(sp)}}
|
msgs := []Message{{Role: "system", Content: strp(sp)}}
|
||||||
@@ -1987,6 +2068,17 @@ func main() {
|
|||||||
fmt.Println(c("Usage: /cfg <param> [val]", 31))
|
fmt.Println(c("Usage: /cfg <param> [val]", 31))
|
||||||
}
|
}
|
||||||
continue
|
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":
|
case u == "/models":
|
||||||
ml, err := listModels(&cfg)
|
ml, err := listModels(&cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2014,6 +2106,7 @@ func main() {
|
|||||||
{"/endpoint [e]", "alias for /cfg endpoint"},
|
{"/endpoint [e]", "alias for /cfg endpoint"},
|
||||||
{"!<cmd>", "run shell command directly"},
|
{"!<cmd>", "run shell command directly"},
|
||||||
{"/models", "list models at endpoint"},
|
{"/models", "list models at endpoint"},
|
||||||
|
{"/skill <n> [p]", "load SKILL.md and run as prompt"},
|
||||||
{"/help", "show help"},
|
{"/help", "show help"},
|
||||||
} {
|
} {
|
||||||
fmt.Println(c(fmt.Sprintf(" %-18s", kv[0]), 1, 32) + kv[1])
|
fmt.Println(c(fmt.Sprintf(" %-18s", kv[0]), 1, 32) + kv[1])
|
||||||
|
|||||||
+108
@@ -2525,3 +2525,111 @@ func TestWriteFileOmittedOverwritesWholeFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// #18: skillsDir mirrors toolsDir, and skillPrompt composes SKILL.md + prompt.
|
||||||
|
func TestSkills(t *testing.T) {
|
||||||
|
base := t.TempDir()
|
||||||
|
skillDir := filepath.Join(base, "skills")
|
||||||
|
if err := os.MkdirAll(filepath.Join(skillDir, "greet"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
skillMd := "You are a friendly greeter.\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(skillDir, "greet", "SKILL.md"), []byte(skillMd), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// skillsDir prefers the env var, then the config key.
|
||||||
|
t.Setenv("BANTAM_SKILLS_DIR", skillDir)
|
||||||
|
if got := skillsDir(&Cfg{}); got != skillDir {
|
||||||
|
t.Errorf("skillsDir() with env = %q, want %q", got, skillDir)
|
||||||
|
}
|
||||||
|
t.Setenv("BANTAM_SKILLS_DIR", "")
|
||||||
|
if got := skillsDir(&Cfg{Raw: map[string]string{"bantam_skills_dir": skillDir}}); got != skillDir {
|
||||||
|
t.Errorf("skillsDir() with config = %q, want %q", got, skillDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With a skills dir set, the name is resolved relative to it.
|
||||||
|
got, err := skillPrompt("/skill greet hello there", &Cfg{Raw: map[string]string{"bantam_skills_dir": skillDir}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("skillPrompt(relative): %v", err)
|
||||||
|
}
|
||||||
|
want := skillMd + "\nhello there"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("skillPrompt(relative) = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With no skills dir, the name is an absolute path to the skill directory.
|
||||||
|
got, err = skillPrompt("/skill "+filepath.Join(skillDir, "greet")+" just hi", &Cfg{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("skillPrompt(absolute): %v", err)
|
||||||
|
}
|
||||||
|
if got != skillMd+"\njust hi" {
|
||||||
|
t.Errorf("skillPrompt(absolute) = %q, want %q", got, skillMd+"\njust hi")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute path directly to a SKILL.md file also works.
|
||||||
|
got, err = skillPrompt("/skill "+filepath.Join(skillDir, "greet", "SKILL.md"), &Cfg{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("skillPrompt(file): %v", err)
|
||||||
|
}
|
||||||
|
if got != strings.TrimRight(skillMd, "\n") {
|
||||||
|
t.Errorf("skillPrompt(file) = %q, want %q", got, strings.TrimRight(skillMd, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing skill reports an error; bare /skill reports usage.
|
||||||
|
if _, err := skillPrompt("/skill nope", &Cfg{Raw: map[string]string{"bantam_skills_dir": skillDir}}); err == nil {
|
||||||
|
t.Error("skillPrompt(missing) expected error, got nil")
|
||||||
|
}
|
||||||
|
if _, err := skillPrompt("/skill", &Cfg{}); err == nil {
|
||||||
|
t.Error("skillPrompt(bare) expected error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #19: bare /skill lists skills under the configured dir, or reports none.
|
||||||
|
func TestListSkills(t *testing.T) {
|
||||||
|
base := t.TempDir()
|
||||||
|
skillDir := filepath.Join(base, "skills")
|
||||||
|
if err := os.MkdirAll(filepath.Join(skillDir, "alpha"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Join(skillDir, "beta"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(skillDir, "alpha", "SKILL.md"), []byte("a"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(skillDir, "beta", "SKILL.md"), []byte("b"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// a directory without SKILL.md must NOT be reported as a skill
|
||||||
|
if err := os.MkdirAll(filepath.Join(skillDir, "notaskill"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
capture := func() string {
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
old := os.Stdout
|
||||||
|
os.Stdout = w
|
||||||
|
listSkills(&Cfg{Raw: map[string]string{"bantam_skills_dir": skillDir}})
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
data, _ := io.ReadAll(r)
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := capture()
|
||||||
|
if !strings.Contains(out, "alpha") || !strings.Contains(out, "beta") {
|
||||||
|
t.Errorf("listSkills missing skills, got:\n%s", out)
|
||||||
|
}
|
||||||
|
if strings.Contains(out, "notaskill") {
|
||||||
|
t.Errorf("listSkills reported a non-skill directory, got:\n%s", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No skills dir configured -> clear notice, no panic.
|
||||||
|
os.Stdout, _ = os.Open(os.DevNull)
|
||||||
|
listSkills(&Cfg{})
|
||||||
|
os.Stdout.Close()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user