From a21c256efa68eb7dbf2bbd737c31b43e2a5cd75b Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 29 Jul 2026 23:19:03 +0200 Subject: [PATCH] tools: one file per builtin tool Each tool is a single file, single function. builtins.go is the registry. --- tools/builtins.go | 18 ++++ tools/shell.go | 38 +++++++++ tools/skill_active.go | 32 +++++++ tools/skill_list.go | 32 +++++++ tools/skill_load.go | 37 ++++++++ tools/tool_active.go | 32 +++++++ tools/tool_list.go | 32 +++++++ tools/tool_load.go | 31 +++++++ tools/tools.go | 193 ------------------------------------------ 9 files changed, 252 insertions(+), 193 deletions(-) create mode 100644 tools/builtins.go create mode 100644 tools/shell.go create mode 100644 tools/skill_active.go create mode 100644 tools/skill_list.go create mode 100644 tools/skill_load.go create mode 100644 tools/tool_active.go create mode 100644 tools/tool_list.go create mode 100644 tools/tool_load.go delete mode 100644 tools/tools.go diff --git a/tools/builtins.go b/tools/builtins.go new file mode 100644 index 0000000..d1db7e1 --- /dev/null +++ b/tools/builtins.go @@ -0,0 +1,18 @@ +// Package tools provides the built-in tool handlers for the toolsrv server. +// Each file in this package is one tool. Builtins() is the single registry. +package tools + +import "ollie/toolsrv" + +// Builtins returns the default set of built-in tool handlers. +func Builtins() map[string]toolsrv.Handler { + return map[string]toolsrv.Handler{ + "shell": Shell, + "tool_list": ToolList, + "tool_load": ToolLoad, + "tool_active": ToolActive, + "skill_list": SkillList, + "skill_load": SkillLoad, + "skill_active": SkillActive, + } +} diff --git a/tools/shell.go b/tools/shell.go new file mode 100644 index 0000000..d1bab41 --- /dev/null +++ b/tools/shell.go @@ -0,0 +1,38 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + + "ollie/toolsrv" +) + +// Shell executes a single bash command in a sandboxed environment. +func Shell(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { + var a struct { + Cmd string `json:"cmd"` + Timeout int `json:"timeout"` + Sandbox string `json:"sandbox"` + Elevated bool `json:"elevated"` + Detach bool `json:"detach"` + } + if err := json.Unmarshal(args, &a); err != nil { + return "", fmt.Errorf("shell: bad args: %w", err) + } + if a.Cmd == "" { + return "", fmt.Errorf("shell: cmd is required") + } + timeout := a.Timeout + if timeout <= 0 { + timeout = 30 + } + if a.Elevated { + return srv.ExecuteElevated(ctx, a.Cmd, srv.CWD(), timeout, a.Detach) + } + sandboxName := a.Sandbox + if sandboxName == "" { + sandboxName = "default" + } + return srv.ExecuteInSandbox(ctx, a.Cmd, "bash", timeout, sandboxName, false, "", a.Detach) +} diff --git a/tools/skill_active.go b/tools/skill_active.go new file mode 100644 index 0000000..2b4e39b --- /dev/null +++ b/tools/skill_active.go @@ -0,0 +1,32 @@ +package tools + +import ( + "context" + "encoding/json" + "strings" + + "ollie/toolsrv" +) + +// SkillActive lists skills currently loaded in this session. +func SkillActive(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { + reg := srv.SkillsRegistry() + sid := srv.SessionID() + if reg == nil || sid == "" { + return "(no skills loaded)", nil + } + loaded := reg.Loaded(sid) + if len(loaded) == 0 { + return "(no skills loaded)", nil + } + var out strings.Builder + for _, s := range loaded { + out.WriteString(s.Name) + if s.Description != "" { + out.WriteString(" — ") + out.WriteString(s.Description) + } + out.WriteString("\n") + } + return strings.TrimRight(out.String(), "\n"), nil +} diff --git a/tools/skill_list.go b/tools/skill_list.go new file mode 100644 index 0000000..c5706af --- /dev/null +++ b/tools/skill_list.go @@ -0,0 +1,32 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "ollie/toolsrv" +) + +// SkillList lists all available skills from the global registry. +func SkillList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { + reg := srv.SkillsRegistry() + if reg == nil { + return "", fmt.Errorf("skill_list: no skills registry available") + } + summaries := reg.Summaries() + if len(summaries) == 0 { + return "(no skills found)", nil + } + var out strings.Builder + for _, s := range summaries { + out.WriteString(s.Name) + if s.Description != "" { + out.WriteString(" — ") + out.WriteString(s.Description) + } + out.WriteString("\n") + } + return strings.TrimRight(out.String(), "\n"), nil +} diff --git a/tools/skill_load.go b/tools/skill_load.go new file mode 100644 index 0000000..a7837d2 --- /dev/null +++ b/tools/skill_load.go @@ -0,0 +1,37 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + + "ollie/toolsrv" +) + +// SkillLoad loads a skill into the current session. +func SkillLoad(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { + var a struct { + Name string `json:"name"` + } + if err := json.Unmarshal(args, &a); err != nil { + return "", fmt.Errorf("skill_load: bad args: %w", err) + } + if a.Name == "" { + return "", fmt.Errorf("skill_load: name is required") + } + reg := srv.SkillsRegistry() + sid := srv.SessionID() + if reg == nil || sid == "" { + return "", fmt.Errorf("skill_load: no session registry") + } + if err := reg.Load(sid, a.Name); err != nil { + return "", fmt.Errorf("skill_load: %w", err) + } + content, err := reg.SkillContent(a.Name) + if err != nil { + return "", fmt.Errorf("skill_load: %w", err) + } + injectMsg := fmt.Sprintf("[system: loaded skill %s]\n\n%s", a.Name, content) + srv.InjectContent(injectMsg) + return fmt.Sprintf("loaded: %s (%d bytes)", a.Name, len(content)), nil +} diff --git a/tools/tool_active.go b/tools/tool_active.go new file mode 100644 index 0000000..782a126 --- /dev/null +++ b/tools/tool_active.go @@ -0,0 +1,32 @@ +package tools + +import ( + "context" + "encoding/json" + "strings" + + "ollie/toolsrv" +) + +// ToolActive lists tools currently loaded (promoted) in this session. +func ToolActive(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { + reg := srv.ToolRegistry() + sid := srv.SessionID() + if reg == nil || sid == "" { + return "(no tools loaded)", nil + } + loaded := reg.Loaded(sid) + if len(loaded) == 0 { + return "(no tools loaded)", nil + } + var out strings.Builder + for _, t := range loaded { + out.WriteString(t.Name) + if t.Description != "" { + out.WriteString(" — ") + out.WriteString(t.Description) + } + out.WriteString("\n") + } + return strings.TrimRight(out.String(), "\n"), nil +} diff --git a/tools/tool_list.go b/tools/tool_list.go new file mode 100644 index 0000000..e0f577f --- /dev/null +++ b/tools/tool_list.go @@ -0,0 +1,32 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "ollie/toolsrv" +) + +// ToolList lists all available tools from the global registry. +func ToolList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { + reg := srv.ToolRegistry() + if reg == nil { + return "", fmt.Errorf("tool_list: no registry available") + } + summaries := reg.Summaries() + if len(summaries) == 0 { + return "(no tools found)", nil + } + var out strings.Builder + for _, s := range summaries { + out.WriteString(s.Name) + if s.Description != "" { + out.WriteString(" — ") + out.WriteString(s.Description) + } + out.WriteString("\n") + } + return strings.TrimRight(out.String(), "\n"), nil +} diff --git a/tools/tool_load.go b/tools/tool_load.go new file mode 100644 index 0000000..6aa736e --- /dev/null +++ b/tools/tool_load.go @@ -0,0 +1,31 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + + "ollie/toolsrv" +) + +// ToolLoad loads a tool into the current session. +func ToolLoad(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { + var a struct { + Name string `json:"name"` + } + if err := json.Unmarshal(args, &a); err != nil { + return "", fmt.Errorf("tool_load: bad args: %w", err) + } + if a.Name == "" { + return "", fmt.Errorf("tool_load: name is required") + } + reg := srv.ToolRegistry() + sid := srv.SessionID() + if reg == nil || sid == "" { + return "", fmt.Errorf("tool_load: no session registry") + } + if err := reg.Load(sid, a.Name); err != nil { + return "", fmt.Errorf("tool_load: %w", err) + } + return fmt.Sprintf("loaded: %s", a.Name), nil +} diff --git a/tools/tools.go b/tools/tools.go deleted file mode 100644 index 835dce8..0000000 --- a/tools/tools.go +++ /dev/null @@ -1,193 +0,0 @@ -// Package tools provides the built-in tool handlers for the toolsrv server. -package tools - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "ollie/toolsrv" -) - -// Builtins returns the default set of built-in tool handlers. -func Builtins() map[string]toolsrv.Handler { - return map[string]toolsrv.Handler{ - "shell": Shell, - "tool_list": ToolList, - "tool_load": ToolLoad, - "tool_active": ToolActive, - "skill_list": SkillList, - "skill_load": SkillLoad, - "skill_active": SkillActive, - } -} - -// Shell executes a single bash command in a sandboxed environment. -func Shell(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { - var a struct { - Cmd string `json:"cmd"` - Timeout int `json:"timeout"` - Sandbox string `json:"sandbox"` - Elevated bool `json:"elevated"` - Detach bool `json:"detach"` - } - if err := json.Unmarshal(args, &a); err != nil { - return "", fmt.Errorf("shell: bad args: %w", err) - } - if a.Cmd == "" { - return "", fmt.Errorf("shell: cmd is required") - } - timeout := a.Timeout - if timeout <= 0 { - timeout = 30 - } - if a.Elevated { - return srv.ExecuteElevated(ctx, a.Cmd, srv.CWD(), timeout, a.Detach) - } - sandboxName := a.Sandbox - if sandboxName == "" { - sandboxName = "default" - } - return srv.ExecuteInSandbox(ctx, a.Cmd, "bash", timeout, sandboxName, false, "", a.Detach) -} - -// ToolList lists all available tools from the global registry. -func ToolList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { - reg := srv.ToolRegistry() - if reg == nil { - return "", fmt.Errorf("tool_list: no registry available") - } - summaries := reg.Summaries() - if len(summaries) == 0 { - return "(no tools found)", nil - } - var out strings.Builder - for _, s := range summaries { - out.WriteString(s.Name) - if s.Description != "" { - out.WriteString(" — ") - out.WriteString(s.Description) - } - out.WriteString("\n") - } - return strings.TrimRight(out.String(), "\n"), nil -} - -// ToolLoad loads a tool into the current session. -func ToolLoad(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { - var a struct { - Name string `json:"name"` - } - if err := json.Unmarshal(args, &a); err != nil { - return "", fmt.Errorf("tool_load: bad args: %w", err) - } - if a.Name == "" { - return "", fmt.Errorf("tool_load: name is required") - } - reg := srv.ToolRegistry() - sid := srv.SessionID() - if reg == nil || sid == "" { - return "", fmt.Errorf("tool_load: no session registry") - } - if err := reg.Load(sid, a.Name); err != nil { - return "", fmt.Errorf("tool_load: %w", err) - } - return fmt.Sprintf("loaded: %s", a.Name), nil -} - -// ToolActive lists tools currently loaded (promoted) in this session. -func ToolActive(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { - reg := srv.ToolRegistry() - sid := srv.SessionID() - if reg == nil || sid == "" { - return "(no tools loaded)", nil - } - loaded := reg.Loaded(sid) - if len(loaded) == 0 { - return "(no tools loaded)", nil - } - var out strings.Builder - for _, t := range loaded { - out.WriteString(t.Name) - if t.Description != "" { - out.WriteString(" — ") - out.WriteString(t.Description) - } - out.WriteString("\n") - } - return strings.TrimRight(out.String(), "\n"), nil -} - -// SkillList lists all available skills from the global registry. -func SkillList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { - reg := srv.SkillsRegistry() - if reg == nil { - return "", fmt.Errorf("skill_list: no skills registry available") - } - summaries := reg.Summaries() - if len(summaries) == 0 { - return "(no skills found)", nil - } - var out strings.Builder - for _, s := range summaries { - out.WriteString(s.Name) - if s.Description != "" { - out.WriteString(" — ") - out.WriteString(s.Description) - } - out.WriteString("\n") - } - return strings.TrimRight(out.String(), "\n"), nil -} - -// SkillLoad loads a skill into the current session. -func SkillLoad(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { - var a struct { - Name string `json:"name"` - } - if err := json.Unmarshal(args, &a); err != nil { - return "", fmt.Errorf("skill_load: bad args: %w", err) - } - if a.Name == "" { - return "", fmt.Errorf("skill_load: name is required") - } - reg := srv.SkillsRegistry() - sid := srv.SessionID() - if reg == nil || sid == "" { - return "", fmt.Errorf("skill_load: no session registry") - } - if err := reg.Load(sid, a.Name); err != nil { - return "", fmt.Errorf("skill_load: %w", err) - } - content, err := reg.SkillContent(a.Name) - if err != nil { - return "", fmt.Errorf("skill_load: %w", err) - } - injectMsg := fmt.Sprintf("[system: loaded skill %s]\n\n%s", a.Name, content) - srv.InjectContent(injectMsg) - return fmt.Sprintf("loaded: %s (%d bytes)", a.Name, len(content)), nil -} - -// SkillActive lists skills currently loaded in this session. -func SkillActive(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (string, error) { - reg := srv.SkillsRegistry() - sid := srv.SessionID() - if reg == nil || sid == "" { - return "(no skills loaded)", nil - } - loaded := reg.Loaded(sid) - if len(loaded) == 0 { - return "(no skills loaded)", nil - } - var out strings.Builder - for _, s := range loaded { - out.WriteString(s.Name) - if s.Description != "" { - out.WriteString(" — ") - out.WriteString(s.Description) - } - out.WriteString("\n") - } - return strings.TrimRight(out.String(), "\n"), nil -}