# 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) |
