Files
platform/seed/tools/TOOLS.md
T
pastilhasandClaude Opus 4.6 0068244356 claude-code as channel model, container trust/permissions fixes, terminal cwd fix
- add claude-code as virtual model in channel messaging (telegram/discord/whatsapp)
- new send-claude-code.ts: docker exec claude -p with session resumption
- route claude-code model in sendAndAwait before Pi pipeline
- append claude-code to listPiModels output
- fix container .claude mount (rw for sub-mounts), hooks format (matcher-based)
- pre-seed hasTrustDialogAccepted and bypassPermissions in container settings
- git init in entrypoint to skip workspace trust prompt
- fix ~/~ double-tilde in CommandTerminalWrapper cwd resolution
- remove --continue from claude-code panel command

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 20:00:45 +00:00

604 lines
18 KiB
Markdown

# Tools
A tool is a callable capability that agents can use during task execution. Each tool lives in its own directory and is defined by a `TOOL.md` file and an `index.ts` (or `index.js`) entry point.
Tools are loaded by the `tool-loader` extension at startup. They appear in the agent's system prompt and can be invoked by name.
## Quick Start — Minimal Tool
Create a directory with two files:
```
tools/
hello/
TOOL.md
index.ts
```
**TOOL.md:**
```yaml
---
name: hello
label: Hello
description: Says hello to the user. Use this when the user wants a greeting.
version: 1
language: typescript
inputs:
name:
type: string
description: Who to greet.
---
# Hello Tool
Greets the user by name.
## Usage
Call with a `name` parameter to get a personalized greeting.
```
**index.ts:**
```typescript
type ToolResult = {
content: Array<{ type: string; text: string }>;
isError?: boolean;
};
type Params = {
name: string;
};
export async function execute(
_toolCallId: string,
params: Params,
): Promise<ToolResult> {
return { content: [{ type: 'text', text: `Hello, ${params.name}!` }] };
}
```
That's it. The tool-loader finds it, registers it, and the agent can call it.
## File Structure
```
tools/
<tool-name>/
TOOL.md # Metadata + documentation (required)
index.ts # Implementation (required)
bin/ # Optional helper scripts
```
Both `TOOL.md` and an entry file (`index.ts` or `index.js`) are required. The loader skips directories missing either.
Additional files (scripts, configs, READMEs) are allowed but not loaded — only `TOOL.md` and the entry file matter.
## Tool Locations
Tools live in two directories:
| Location | Purpose | Managed by |
|----------|---------|------------|
| `DATA_PATH/tools/` | Global tools (synced from seed) | `sync-tools.ts` at startup |
| `DATA_PATH/<email>/tools/` | User-created tools | Manual (user creates them) |
Both are mounted into containers and discovered via the `PI_TOOLS_DIRS` environment variable. If a user tool has the same `name` as a global tool, the user tool overrides it (last-writer-wins).
To create a user tool, make a new directory in `DATA_PATH/<email>/tools/<tool-name>/` with `TOOL.md` and `index.ts`. It will be available after the next agent session starts.
## TOOL.md Format
Two parts: **frontmatter** (YAML metadata) and **body** (Markdown documentation).
### Frontmatter
```yaml
---
name: tool_name
label: Tool Name
description: What the tool does and when the agent should use it.
version: 1
language: typescript
inputs:
param_name:
type: string
description: What this parameter is for.
optional_param:
type: number
description: An optional parameter.
optional: true
secret_param:
type: string
description: A sensitive value (e.g., API token).
optional: true
sensitive: true
mode:
type: enum
values: single,batch
description: Choose between modes.
---
```
#### Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | yes | Identifier for the tool (used in tool calls). Use `snake_case`. |
| `label` | string | no | Human-readable display name. Defaults to `name` if omitted. |
| `description` | string | yes | What the tool does. This appears in the agent's system prompt — make it specific enough for the agent to know **when** to use it. |
| `version` | integer | **yes** | Version number. Used by `sync-tools` to detect updates. **Always include and bump when changing the tool.** If omitted, defaults to 0, causing unpredictable sync behavior. |
| `language` | string | no | `typescript`, `bash`, or `python`. Defaults to `typescript`. |
| `inputs` | object | no | Input parameters the tool accepts. Keys are parameter names. |
#### Input Types
| Type | Schema | Notes |
|------|--------|-------|
| `string` | `Type.String()` | Default if type is unrecognized |
| `number` | `Type.Number()` | |
| `boolean` | `Type.Boolean()` | |
| `enum` | `Type.Union(literals)` | Requires `values` field (comma-separated or array) |
**Note:** `object` and `array` types are not supported by the schema builder. If you need complex inputs, accept a JSON string and parse it in the execute function.
#### Input Properties
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | `string`, `number`, `boolean`, or `enum`. |
| `description` | string | yes | What this parameter is for. Shown to the agent. |
| `optional` | boolean | no | Whether the parameter is optional. Defaults to required. |
| `sensitive` | boolean | no | Mark sensitive values (tokens, passwords). Prevents logging in UI. |
| `values` | string | no | Comma-separated allowed values for `enum` type. |
| `default` | any | no | Default value if not provided. |
### Body
The body is Markdown that the agent sees when the tool is loaded. This is your main documentation — the agent reads it to understand how to use the tool.
Include:
- **Title** — `# Tool Name`
- **Usage** — How to call the tool, what parameters to pass, and what to expect back.
- **Examples** — Common usage patterns with example parameter values.
- **Authentication** — How credentials are resolved (env vars, integrations, etc.) if applicable.
- **Error Handling** — What errors can occur and what they mean.
- **Notes** — Limits, external dependencies, related links.
Write the body as instructions for the agent. The agent decides when and how to call the tool based on this documentation.
## index.ts Format
The entry file must export an `execute` function:
```typescript
type ToolResult = {
content: Array<{ type: string; text: string }>;
isError?: boolean;
};
type OnUpdate = (partial: { content: Array<{ type: string; text: string }> }) => void;
export async function execute(
toolCallId: string,
params: Record<string, unknown>,
signal?: AbortSignal,
onUpdate?: OnUpdate,
): Promise<ToolResult> {
// Implementation here
}
```
### Parameters
| Parameter | Description |
|-----------|-------------|
| `toolCallId` | Unique ID for this tool call. Usually unused — prefix with `_`. |
| `params` | Input values from the agent, matching `inputs` in TOOL.md. |
| `signal` | AbortSignal for cancellation. Not widely used yet — accept and ignore. |
| `onUpdate` | Callback for streaming progress to the agent during long operations. |
### Return Value
Return a `ToolResult` object:
```typescript
// Success
return { content: [{ type: 'text', text: 'Done! Created 5 files.' }] };
// Error — agent sees the error and can react
return { content: [{ type: 'text', text: 'API key not found.' }], isError: true };
```
- `content` — Array of content blocks. Usually one `{ type: 'text', text: '...' }`.
- `isError` — Set `true` to indicate failure.
### Recommended Helpers
Define `ok()` and `err()` helpers to keep return statements clean:
```typescript
function ok(text: string): ToolResult {
return { content: [{ type: 'text', text }] };
}
function err(text: string): ToolResult {
return { content: [{ type: 'text', text }], isError: true };
}
```
### Typed Parameters
Define a `Params` type matching your TOOL.md inputs instead of using `Record<string, unknown>`:
```typescript
type Params = {
query: string;
max_results?: number;
format?: string;
};
export async function execute(
_toolCallId: string,
params: Params,
_signal?: AbortSignal,
onUpdate?: OnUpdate,
): Promise<ToolResult> {
const { query, max_results = 10 } = params;
// ...
}
```
### Progress Updates
Use `onUpdate` to stream status during long-running operations. The agent sees each update in real time:
```typescript
onUpdate?.({ content: [{ type: 'text', text: 'Phase 1: Downloading data...' }] });
// ... do work ...
onUpdate?.({ content: [{ type: 'text', text: 'Phase 2: Processing 500 files...' }] });
// ... do work ...
return ok('Done! Processed 500 files.');
```
### Error Handling
Wrap the entire execute body in try/catch. Return errors as `ToolResult` with `isError: true` — never throw from execute:
```typescript
export async function execute(_toolCallId: string, params: Params): Promise<ToolResult> {
try {
// ... implementation ...
return ok('Success');
} catch (e) {
return err(`Failed: ${e instanceof Error ? e.message : String(e)}`);
}
}
```
For missing configuration, return a helpful error that tells the agent what to do:
```typescript
const token = params.api_token ?? process.env.OFFICER_APIFY_TOKEN;
if (!token) {
return err('Apify API token not configured. Ask the user to add it in Settings → Integrations.');
}
```
### Authentication
Tools should resolve credentials internally. Pattern:
1. Check for an explicit parameter override (e.g., `params.api_token`)
2. Fall back to an environment variable (e.g., `process.env.OFFICER_APIFY_TOKEN`)
3. Return a helpful error if neither is available
Environment variables are set by `pi-bridge.ts` from the integration config in the database (Settings → Integrations).
### Large Output
If a tool may return large data, provide an `output_path` parameter. When set, save data to the file and return a summary:
```typescript
if (params.output_path) {
writeFileSync(params.output_path, JSON.stringify(items, null, 2));
return ok(`${items.length} items saved to ${params.output_path}`);
}
```
This prevents flooding the agent's context window.
## Runtime Environment
Tools run inside **sandboxed Docker containers** using **Node.js** (not Bun).
### APIs
Use Node.js standard library only:
| Need | Use | Don't use |
|------|-----|-----------|
| File I/O | `fs.readFileSync`, `fs.writeFileSync` | `Bun.file`, `Bun.write` |
| Delays | `setTimeout`, `setInterval` | `Bun.sleep` |
| HTTP | `fetch` (Node 18+) | Bun-specific fetch options |
| Child processes | `child_process.execFileSync`, `spawn` | `Bun.spawn` |
| Paths | `path.join`, `path.dirname` | |
### Available Environment Variables
These are set by `pi-bridge.ts` and available in all tool containers:
| Variable | Description |
|----------|-------------|
| `HOME` | Container home directory |
| `OFFICER_USER_HOME` | Same as HOME |
| `OFFICER_USER_ROOT` | User data root (`/officer/user`) |
| `PI_TOOLS_DIRS` | Tool discovery paths (colon-separated) |
| `OFFICER_EMAIL_DB` | Path to email SQLite database |
| `OFFICER_RESOURCES` | JSON object with all configured resources/integrations |
| `PI_SEARXNG_URL` | SearXNG search engine URL |
| `OFFICER_APIFY_TOKEN` | Apify API token (if configured) |
| `OFFICER_BROWSER_RELAY_PORT` | Browser relay port (if configured) |
| `OFFICER_BROWSER_RELAY_TOKEN` | Browser relay auth token (if configured) |
**`OFFICER_RESOURCES`** is a JSON string containing all resource configs from Settings → Integrations:
```typescript
const resources = JSON.parse(process.env.OFFICER_RESOURCES ?? '{}');
const ocrConfig = resources['optical-character-recognition'];
```
### Container Filesystem
| Mount | Path in container | Access |
|-------|-------------------|--------|
| Global tools | `/officer/tools/` | Read-only |
| User tools | `/officer/user/tools/` | Read-only |
| User data | `/officer/user/` | Read-write |
| Email database | `/officer/data/emails.db` | Read-write |
### Referencing Local Files
If your tool includes helper scripts (e.g., Python/bash in a `bin/` directory), resolve them relative to the entry file:
```typescript
// Works in both ESM and CJS contexts
const TOOL_DIR = typeof __dirname !== 'undefined'
? __dirname
: dirname(fileURLToPath(import.meta.url));
const BIN_DIR = join(TOOL_DIR, 'bin');
// Then call scripts:
execFileSync('python3', [join(BIN_DIR, 'process.py'), inputPath]);
```
### External Dependencies
If your tool requires system binaries (e.g., `python3`, `tesseract`, `ffmpeg`), check for them early and return a helpful error:
```typescript
import { execSync } from 'node:child_process';
function hasCommand(cmd: string): boolean {
try {
execSync(`which ${cmd}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
// In execute():
if (!hasCommand('python3')) {
return err('python3 is required but not installed in the container.');
}
```
## Sync and Discovery
### How Sync Works
At server startup, `sync-tools.ts` copies tools from `seed/tools/` to `DATA_PATH/tools/` (the global tools directory). The sync is **version-based**:
1. Parse `version` from TOOL.md frontmatter (defaults to `0` if missing)
2. Compare seed version vs target version
3. **Only copy if seed version > target version** (equal versions are skipped)
4. When copying, the entire tool directory is replaced (`rm + cp`)
This means:
- Bumping `version` in seed triggers an update on next restart
- User edits to global tools are preserved until seed version exceeds theirs
- Tools without `version` default to `0` — always include a version number
### How Discovery Works
The `tool-loader` extension reads `PI_TOOLS_DIRS` (colon-separated paths) and scans each directory for tool subdirectories. For each subdirectory:
1. Look for `TOOL.md` — parse frontmatter for metadata
2. Look for `index.ts` or `index.js` — this is the entry file
3. Skip if either is missing
4. Register the tool with the agent (name, description, parameter schema)
5. **Lazy load** the entry file on first call (not at startup)
If multiple tools share the same `name`, the last one registered wins. Since user tools are loaded after global tools, user tools override global tools with the same name.
## Common Mistakes
### No `execute` export
The entry file **must** export `execute` as a named export. Class-based patterns, CLI entry points (`process.argv`), and `module.exports` do not work:
```typescript
// ❌ Wrong — class pattern
export class MyTool { async run() { ... } }
// ❌ Wrong — CLI entry point
if (require.main === module) { main(); }
// ❌ Wrong — console.log instead of return
export async function execute(_id: string, params: Params) {
console.log('result'); // Agent never sees this
}
// ✅ Correct
export async function execute(_id: string, params: Params): Promise<ToolResult> {
return { content: [{ type: 'text', text: 'result' }] };
}
```
### Forgetting to bump version
After editing a seed tool's code or TOOL.md, bump `version` in the frontmatter. Otherwise `sync-tools` won't copy the update to the global directory and the agent will keep using the old version.
### Using Bun APIs
Tools run in Node.js containers. `Bun.file()`, `Bun.write()`, `Bun.sleep()` will throw `ReferenceError`.
### Throwing instead of returning errors
Never throw from `execute`. Always catch and return `{ isError: true }`. Unhandled throws produce generic error messages the agent can't act on.
### Unnecessary files
Tools don't need `package.json`, `tsconfig.json`, `node_modules`, or test directories. The tool-loader only reads `TOOL.md` and `index.ts`. Extra files are harmless but add clutter.
## Full Example — Database Query Tool
A complete tool that queries a SQLite database:
**TOOL.md:**
```yaml
---
name: my_db
label: My Database
description: Query the application database. Use this to look up records, run aggregations, and search data.
version: 1
language: typescript
inputs:
action:
type: enum
values: query,stats
description: "Action to perform: query runs SQL, stats shows database overview."
sql:
type: string
description: SQL SELECT statement to execute (query action only).
optional: true
limit:
type: number
description: Maximum number of results to return.
optional: true
---
# My Database Tool
Query the application database using SQL.
## Usage
Use `action=stats` for a database overview. Use `action=query` with a `sql` parameter for specific queries.
## Examples
Get stats:
- action: stats
Search records:
- action: query, sql: "SELECT * FROM users WHERE name LIKE '%john%' LIMIT 10"
```
**index.ts:**
```typescript
import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
type ToolResult = {
content: Array<{ type: string; text: string }>;
isError?: boolean;
};
type Params = {
action: string;
sql?: string;
limit?: number;
};
const DB_PATH = process.env.MY_DB_PATH ?? '/officer/data/my.db';
function ok(text: string): ToolResult {
return { content: [{ type: 'text', text }] };
}
function err(text: string): ToolResult {
return { content: [{ type: 'text', text }], isError: true };
}
function queryJson(sql: string): Record<string, unknown>[] {
const output = execFileSync('sqlite3', ['-json', DB_PATH], {
input: sql,
encoding: 'utf-8',
timeout: 10000,
});
const trimmed = output.trim();
if (!trimmed) return [];
return JSON.parse(trimmed);
}
export async function execute(_toolCallId: string, params: Params): Promise<ToolResult> {
if (!existsSync(DB_PATH)) {
return err(`Database not found at ${DB_PATH}.`);
}
try {
switch (params.action) {
case 'query': {
if (!params.sql) return err('sql parameter is required for query action.');
if (!params.sql.trim().toLowerCase().startsWith('select')) {
return err('Only SELECT statements are allowed.');
}
const limit = params.limit ? ` LIMIT ${params.limit}` : '';
const rows = queryJson(`${params.sql}${limit}`);
if (rows.length === 0) return ok('No results.');
const text = rows.map((r, i) => {
const fields = Object.entries(r).map(([k, v]) => `${k}: ${v ?? ''}`).join(' | ');
return `${i + 1}. ${fields}`;
}).join('\n');
return ok(`${rows.length} results:\n\n${text}`);
}
case 'stats': {
const tables = queryJson("SELECT name FROM sqlite_master WHERE type='table'");
const lines = tables.map((t) => {
const count = queryJson(`SELECT COUNT(*) as c FROM "${t.name}"`);
return `${t.name}: ${(count[0]?.c as number) ?? 0} rows`;
});
return ok(`Tables:\n${lines.join('\n')}`);
}
default:
return err(`Unknown action: "${params.action}". Available: query, stats.`);
}
} catch (e) {
return err(`Database error: ${e instanceof Error ? e.message : String(e)}`);
}
}
```
## Existing Tools
| Tool | Description |
|------|-------------|
| `gmail` | Read Gmail messages, threads, labels via Google API |
| `web_search` | Search the web via SearXNG |
| `web_fetch` | Fetch and extract content from URLs |
| `browser` | Control a Chrome browser via Browser Relay |
| `apify` | Run any Apify actor (web scraping, social media data) |
| `convert_audio_to_mp3` | Convert audio files to MP3 via ffmpeg |
| `ffmpeg` | Run ffmpeg/ffprobe commands for any audio/video processing |
| `ocr` | Optical character recognition on images |
| `email_db` | Query the synced email database |
| `pdf_categorizer` | Categorize and organize PDF files (user tool) |