From 7d44e9ac1fa98857223ccbc96ce70c4f0bdf2afc Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 12 Aug 2026 19:14:32 +0200 Subject: [PATCH] doc: add tool overlays documentation - Add doc/tool-overlays.md explaining meta-only tools and overlay pattern - Examples: ripgrep, kubectl, MCP server wrapping - Update README.md with tool overlays row in capabilities table --- README.md | 1 + doc/tool-overlays.md | 198 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 doc/tool-overlays.md diff --git a/README.md b/README.md index 93ccd0b..f6bc67f 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ Everything lives under `$XDG_CONFIG_HOME/ollie/` (default: `~/.config/ollie/`) | **Parallel execution** | Non-conflicting tool calls within a turn run in parallel (scope-based conflict scheduling) | | **Agents** | Write a JSON config in `~/.config/ollie/agents/` with agent prompt in `~/.config/ollie/prompts/` | | **Tools** | Drop an executable + `.meta` in `~/.config/ollie/tools/` — load at runtime via `tool_load` | +| **Tool overlays** | Integrate external CLIs (or MCP servers via CLI bridges) with `.meta` files only — no wrapper code ([doc](doc/tool-overlays.md)) | | **Domain skills** | Load markdown skill modules at runtime | ## Repository layout diff --git a/doc/tool-overlays.md b/doc/tool-overlays.md new file mode 100644 index 0000000..817d283 --- /dev/null +++ b/doc/tool-overlays.md @@ -0,0 +1,198 @@ +# Tool Overlays + +A tool overlay is a set of `.meta` files that expose external CLIs to ollie +without wrapper scripts. The `.meta` file declares the schema and specifies +the shell command to run — no executable needed. + +## Resolution order + +When the registry resolves a tool, it checks: + +1. `~/.config/ollie/tools/` — local script or binary +2. `.meta` `cmd` field — shell command string, executed directly +3. `exec.LookPath()` — PATH lookup + +If a `.meta` file has a `cmd` field, the registry executes it via shell. The +tool's JSON arguments are passed on stdin. + +## Meta-only tool structure + +```json +{ + "description": "One-liner for listings.", + "prompt": "## my_tool\n\nUsage docs.\n\n```\nmy_tool(arg=\"value\")\n```", + "cmd": "jq -r '.arg' | xargs my-cli", + "args": { + "type": "object", + "required": ["arg"], + "properties": { + "arg": {"type": "string", "description": "Some argument"} + } + }, + "scope": "global" +} +``` + +No script, no binary — just the `.meta` file. + +## Parsing arguments + +The model sends JSON on stdin. Parse with jq: + +```json +{ + "cmd": "input=$(cat); arg=$(echo \"$input\" | jq -r '.arg'); my-cli \"$arg\"" +} +``` + +For subcommand-style CLIs with variable arguments: + +```json +{ + "cmd": "input=$(cat); cmd=$(echo \"$input\" | jq -r '.command'); args=$(echo \"$input\" | jq -r 'del(.command) | to_entries | map(\"--\\(.key)=\\(.value|tostring)\") | join(\" \")'); eval \"my-cli $cmd $args\"", + "args": { + "type": "object", + "required": ["command"], + "properties": { + "command": {"type": "string", "description": "Subcommand"} + }, + "additionalProperties": true + } +} +``` + +This extracts a `command` field, builds `--key=value` flags from remaining +fields, and calls `my-cli --key=value ...`. + +## Example: wrapping ripgrep + +Expose `rg` as a tool with curated arguments: + +```json +{ + "description": "Search file contents with ripgrep.", + "prompt": "## rg_search\n\nSearch files.\n\n**Args**: `pattern` (required), `path`, `glob`, `case_insensitive`\n\n```\nrg_search(pattern=\"TODO\", path=\"src/\", glob=\"*.go\")\n```", + "cmd": "input=$(cat); p=$(echo \"$input\" | jq -r '.pattern'); dir=$(echo \"$input\" | jq -r '.path // \".\"'); glob=$(echo \"$input\" | jq -r '.glob // empty'); ci=$(echo \"$input\" | jq -r '.case_insensitive // empty'); rg ${ci:+-i} ${glob:+-g \"$glob\"} \"$p\" \"$dir\"", + "args": { + "type": "object", + "required": ["pattern"], + "properties": { + "pattern": {"type": "string", "description": "Regex pattern"}, + "path": {"type": "string", "description": "Search directory"}, + "glob": {"type": "string", "description": "File glob filter"}, + "case_insensitive": {"type": "boolean", "description": "Case-insensitive search"} + } + }, + "scope": "read" +} +``` + +The model calls `rg_search(pattern="error", glob="*.log")`. The cmd pipeline +extracts fields, builds flags conditionally, and runs `rg -g "*.log" "error" "."`. + +## Example: wrapping kubectl + +Expose common kubectl operations: + +```json +{ + "description": "Kubernetes cluster operations.", + "prompt": "## k8s\n\nKubernetes operations.\n\n**Args**: `command` (required), plus command-specific args.\n\n| Command | Args |\n|---------|------|\n| `get` | `resource`, `name?`, `namespace?`, `output?` |\n| `describe` | `resource`, `name`, `namespace?` |\n| `logs` | `pod`, `namespace?`, `tail?`, `follow?` |\n| `apply` | `file` |\n| `delete` | `resource`, `name`, `namespace?` |\n\n```\nk8s(command=\"get\", resource=\"pods\", namespace=\"default\")\nk8s(command=\"logs\", pod=\"api-7d4b8c6f9-x2k4m\", tail=100)\nk8s(command=\"apply\", file=\"deploy.yaml\")\n```", + "cmd": "input=$(cat); cmd=$(echo \"$input\" | jq -r '.command'); case $cmd in get) res=$(echo \"$input\" | jq -r '.resource'); name=$(echo \"$input\" | jq -r '.name // empty'); ns=$(echo \"$input\" | jq -r '.namespace // empty'); out=$(echo \"$input\" | jq -r '.output // empty'); kubectl get $res $name ${ns:+-n $ns} ${out:+-o $out} ;; describe) res=$(echo \"$input\" | jq -r '.resource'); name=$(echo \"$input\" | jq -r '.name'); ns=$(echo \"$input\" | jq -r '.namespace // empty'); kubectl describe $res $name ${ns:+-n $ns} ;; logs) pod=$(echo \"$input\" | jq -r '.pod'); ns=$(echo \"$input\" | jq -r '.namespace // empty'); tail=$(echo \"$input\" | jq -r '.tail // empty'); follow=$(echo \"$input\" | jq -r '.follow // empty'); kubectl logs $pod ${ns:+-n $ns} ${tail:+--tail=$tail} ${follow:+-f} ;; apply) file=$(echo \"$input\" | jq -r '.file'); kubectl apply -f \"$file\" ;; delete) res=$(echo \"$input\" | jq -r '.resource'); name=$(echo \"$input\" | jq -r '.name'); ns=$(echo \"$input\" | jq -r '.namespace // empty'); kubectl delete $res $name ${ns:+-n $ns} ;; *) echo \"unknown command: $cmd\" >&2; exit 1 ;; esac", + "args": { + "type": "object", + "required": ["command"], + "properties": { + "command": {"type": "string", "description": "Operation (get, describe, logs, apply, delete)"} + }, + "additionalProperties": true + }, + "scope": "global" +} +``` + +Complex routing logic lives entirely in the `cmd` field. No wrapper script. + +## Example: wrapping an MCP server + +MCP servers expose tools over JSON-RPC. CLI bridges like `mcp-client-cli` or +`npx @anthropic/mcp-client` let you call them from the command line. Wrap the +bridge in a `.meta`: + +```json +{ + "description": "GitHub operations via MCP.", + "prompt": "## github\n\nGitHub MCP tools.\n\n**Args**: `tool` (required), plus tool-specific args.\n\n| Tool | Args |\n|------|------|\n| `search_repositories` | `query` |\n| `get_file_contents` | `owner`, `repo`, `path`, `branch?` |\n| `create_issue` | `owner`, `repo`, `title`, `body?` |\n| `list_issues` | `owner`, `repo`, `state?` |\n\n```\ngithub(tool=\"search_repositories\", query=\"language:go stars:>1000\")\ngithub(tool=\"get_file_contents\", owner=\"golang\", repo=\"go\", path=\"README.md\")\n```", + "cmd": "input=$(cat); tool=$(echo \"$input\" | jq -r '.tool'); args=$(echo \"$input\" | jq -c 'del(.tool)'); echo \"$args\" | mcp-client-cli --server=github call \"$tool\"", + "args": { + "type": "object", + "required": ["tool"], + "properties": { + "tool": {"type": "string", "description": "MCP tool name"} + }, + "additionalProperties": true + }, + "scope": "global" +} +``` + +The `cmd` extracts the tool name, passes remaining args as JSON to the MCP +bridge. No MCP protocol implementation needed — the bridge handles it. + +This pattern works for any MCP server with a CLI bridge: filesystem, database, +Slack, linear, etc. + +## Overlay structure + +An overlay is a directory (usually a git repo) with `.meta` files, agent +configs, and prompts: + +``` +my-overlay/ +├── tools/ +│ ├── tool_a.meta +│ └── tool_b.meta +├── agents/ +│ └── my-agent.json +├── prompts/ +│ └── agent-my-agent.md +└── justfile +``` + +The justfile installs to `~/.config/ollie/`: + +```just +home := env_var('HOME') +cfg := home / ".config/ollie" + +default: install + +install: tools agents prompts + +tools: + install -m644 tools/tool_a.meta {{cfg}}/tools/tool_a.meta + install -m644 tools/tool_b.meta {{cfg}}/tools/tool_b.meta + +agents: + mkdir -p {{cfg}}/agents + install -m644 agents/my-agent.json {{cfg}}/agents/my-agent.json + +prompts: + mkdir -p {{cfg}}/prompts + install -m644 prompts/agent-my-agent.md {{cfg}}/prompts/agent-my-agent.md + +uninstall: + rm -f {{cfg}}/tools/tool_a.meta {{cfg}}/tools/tool_b.meta + rm -f {{cfg}}/agents/my-agent.json + rm -f {{cfg}}/prompts/agent-my-agent.md +``` + +Run `just install`. Tools are live immediately — no restart needed. + +## Notes + +- `additionalProperties: true` allows pass-through arguments the schema doesn't enumerate. +- Prefix tool names to avoid collisions (`acme_deploy`, not `deploy`). +- Test cmd pipelines: `echo '{"pattern":"TODO"}' | bash -c 'YOUR_CMD'` +- Agent configs can auto-load overlay tools via `autoLoad`. +- Overlays compose: multiple overlays install to the same tools directory.