| name | clawpi-tools |
| description | Develop and extend the clawpi-tools OpenClaw plugin — add new agent tools for Pi hardware control, Eww overlays, display, and canvas. Use when the user wants to add, modify, or debug tools in pkgs/clawpi-tools/. |
| user-invocable | true |
ClawPi Tools Plugin Development
Guide for developing the clawpi-tools OpenClaw plugin that gives the agent typed tools for controlling the Pi's hardware and display.
Project structure
pkgs/clawpi-tools/
├── openclaw.plugin.json # Plugin manifest (id, version, config schema)
├── index.ts # Entry point — imports and registers all category modules
├── helpers.ts # Shared utilities (run, runWayland, text, image)
├── audio.ts # Audio tools (audio_*)
├── screenshot.ts # Screenshot tools (screenshot_*)
└── package.nix # Nix derivation
File-per-category convention
Each tool category lives in its own file. The file is named after the category prefix (e.g. audio.ts for audio_* tools, screenshot.ts for screenshot_* tools). Each category file exports a default function that takes api and registers all tools for that category.
When adding a new category:
- Create
<category>.ts in pkgs/clawpi-tools/
- Export a default function:
export default function (api: any) { ... }
- Import and call it from
index.ts: import registerFooTools from "./foo"; registerFooTools(api);
- Import shared helpers from
./helpers (never duplicate run, text, image, etc.)
Wiring files:
overlays/clawpi.nix — adds clawpi-tools to the Nix package set
home/openclaw.nix — configures plugins.load.paths and plugins.entries
How tools work
The gateway loads index.ts at startup via jiti (TypeScript runtime — no build step needed). The exported function receives an api object and calls api.registerTool() for each tool. Tools are then available to the agent during conversations.
Naming convention
Tool names use a category prefix followed by an underscore and the action: <category>_<action>. This groups related tools together and makes them discoverable.
Examples:
audio_status, audio_get_volume, audio_set_volume — Audio category
display_power, display_brightness — Display category
browser_mode — Browser category
When adding a new tool, pick the category from the existing ones or introduce a new one if the tool doesn't fit.
Tool anatomy
import { Type } from "@sinclair/typebox";
api.registerTool({
name: "category_action",
description: "What this tool does.",
parameters: Type.Object({
param1: Type.String({ description: "..." }),
param2: Type.Optional(Type.Number({ description: "...", minimum: 0 })),
}),
async execute(_id: string, params: { param1: string; param2?: number }) {
const result = await doSomething(params.param1);
return { content: [{ type: "text", text: result }] };
},
});
Execute signature
async execute(_id: string, params: T) => { content: ContentBlock[] }
_id: tool call ID (string) — usually ignored
params: parsed parameters matching the JSON Schema
- Returns:
{ content: [{ type: "text", text: "..." }] } — array of content blocks
This matches the built-in OpenClaw plugin convention (see voice-call plugin for reference at /nix/store/.../openclaw/extensions/voice-call/index.ts on the Pi).
Parameter types (typebox)
Type.String({ description: "..." })
Type.Number({ description: "...", minimum: 0, maximum: 100 })
Type.Boolean({ description: "..." })
Type.Enum(MyEnum, { description: "..." })
Type.Optional(Type.String({ description: "..." }))
Type.Union([Type.Literal("on"), Type.Literal("off")])
Type.Array(Type.String())
Type.Object({})
Return values
return { content: [{ type: "text", text: "Done." }] };
return { content: [
{ type: "text", text: "Result:" },
{ type: "text", text: JSON.stringify(data, null, 2) },
] };
Running system commands
The plugin includes a run() helper that executes commands with the correct NixOS PATH and XDG_RUNTIME_DIR:
const { stdout, stderr } = await run("wpctl", ["status"]);
Important details:
- The gateway runs as the
kiosk user — commands execute as kiosk
XDG_RUNTIME_DIR is set to /run/user/<uid> for PipeWire/Wayland access
- NixOS system PATH (
/run/current-system/sw/bin) is prepended since the gateway's Node.js process may not inherit it
- Commands that need the Wayland display must also set
WAYLAND_DISPLAY=wayland-0
Adding new system commands
If a tool needs a binary that isn't in the system PATH:
- Add the package to
home/clawpi.nix (home.packages) or modules/base.nix (environment.systemPackages)
- Use the full Nix store path if it's only needed by the plugin:
"/run/current-system/sw/bin/mybinary"
Wayland-aware commands
For tools that interact with the Wayland compositor (e.g. display control, screenshots):
async function runWayland(cmd: string, args: string[]) {
return exec(cmd, args, {
env: {
...process.env,
PATH: SYSTEM_PATH,
XDG_RUNTIME_DIR: `/run/user/${process.getuid?.() ?? 1000}`,
WAYLAND_DISPLAY: "wayland-0",
},
});
}
Documentation
Every time you add, remove, or change a tool, update docs/tools.md to match. Keep the summary table, per-tool parameter tables, and "when to use which" guidance in sync with the actual implementation.
Version bumping
Every time you change any .ts file, bump the version in both:
openclaw.plugin.json — the "version" field
package.nix — the version attribute
This is required because Nix derives the store path from the version. Without bumping, the Pi will keep the old code.
Deploy and test cycle
- Edit the category file (e.g.
audio.ts, screenshot.ts) or add a new one
- Bump version in
openclaw.plugin.json and package.nix
git add pkgs/clawpi-tools/ (Nix needs files tracked to see them)
- Deploy:
FLAKE_ATTR=rpi5-telegram-debug ./scripts/deploy.sh 192.168.36.124 --specialisation kiosk
- Restart gateway:
ssh -i id_ed25519_rpi5 nixos@<host> "sudo -u kiosk XDG_RUNTIME_DIR=/run/user/\$(id -u kiosk) systemctl --user restart openclaw-gateway.service"
- Verify plugin loaded:
ssh -i id_ed25519_rpi5 nixos@<host> "sudo -u kiosk XDG_RUNTIME_DIR=/run/user/\$(id -u kiosk) openclaw plugins list"
- Test parameterless tools via HTTP API, or test all tools by talking to the agent
Quick HTTP test (parameterless tools only)
TOKEN=$(ssh -i id_ed25519_rpi5 nixos@<host> "sudo cat /var/lib/kiosk/.openclaw/gateway-token.env" | grep -oP 'OPENCLAW_GATEWAY_TOKEN=\K.*')
curl -s -X POST http://localhost:18789/tools/invoke \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tool":"<tool_name>","input":{}}'
Limitation: The HTTP /tools/invoke API does not forward parameters — params are always {}. Tools with parameters can only be tested by the agent in a conversation.
Checking logs for errors
ssh -i id_ed25519_rpi5 nixos@<host> "grep -i 'error\|clawpi-tools' /tmp/openclaw/openclaw-gateway.log | tail -20"
Tool ideas (from docs/ideas.md)
Planned tools to add to this plugin:
| Tool | Category | Description |
|---|
display_power | Display | Turn HDMI display on/off via wlr-randr or DDC/CI |
display_brightness | Display | Adjust display brightness (if DDC/CI supported) |
show_choices | Eww overlay | Present numbered choice list, return user selection |
show_message | Eww overlay | Show speech bubble overlay with agent message |
show_osd | Eww overlay | Show volume/brightness OSD bar |
browser_mode | Browser | Switch between kiosk (--app) and browse (--start-fullscreen) mode |
Existing tools (current)
| Tool | File | Parameters | Description |
|---|
audio_status | audio.ts | none | List PipeWire sinks/sources via wpctl status |
audio_get_volume | audio.ts | none | Get default sink volume |
audio_set_volume | audio.ts | level: number (0.0–1.0) | Set default sink volume |
audio_test_tone | audio.ts | frequency?: number, duration?: number | Play test sine wave |
audio_set_default_sink | audio.ts | sink_id: number | Switch default output by ID |
screenshot_display | screenshot.ts | none | Full compositor screenshot via grim |
screenshot_browser | screenshot.ts | format?, quality? | Browser viewport screenshot via CDP |
Gateway plugin config
The plugin is wired in home/openclaw.nix:
plugins = {
enabled = true;
allow = [ "clawpi-tools" ];
load.paths = [
"${pkgs.clawpi-tools}/lib/clawpi-tools"
];
entries.clawpi-tools = {
enabled = true;
config = {};
};
};
If new tools should be optional (not auto-available to the agent), register with { optional: true } and add the tool name to agents.list[].tools.allow in the gateway config.
Reference: built-in plugin source
The gateway's stock plugins are at this path on the Pi (useful for studying patterns):
/nix/store/<hash>-openclaw-gateway-unstable-<rev>/lib/openclaw/extensions/
Key references:
voice-call/index.ts — best example of registerTool with typed params
memory-core/index.ts — factory pattern, multiple tools from one registration
lobster/index.ts — optional tool with sandbox check