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>
This commit is contained in:
+464
-41
@@ -1,23 +1,96 @@
|
||||
# Tools
|
||||
|
||||
A tool is a callable capability that agents can use during task execution. Each tool lives in its own directory under `tools/` and is defined by a `TOOL.md` file and an `index.ts` (or `index.js`) entry point.
|
||||
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 and registered as callable functions. They appear in the agent's system prompt and can be invoked by name.
|
||||
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 and documentation
|
||||
TOOL.md # Metadata + documentation (required)
|
||||
index.ts # Implementation (required)
|
||||
bin/ # Optional helper scripts
|
||||
```
|
||||
|
||||
Both files are required. The loader skips directories missing either `TOOL.md` or an entry file.
|
||||
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
|
||||
|
||||
A tool file has two parts: **frontmatter** (YAML metadata) and **body** (Markdown documentation).
|
||||
Two parts: **frontmatter** (YAML metadata) and **body** (Markdown documentation).
|
||||
|
||||
### Frontmatter
|
||||
|
||||
@@ -54,34 +127,47 @@ inputs:
|
||||
|-------|------|----------|-------------|
|
||||
| `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 | no | Version number. Used by `sync-tools` to detect updates — bump when changing the tool. |
|
||||
| `language` | string | no | Implementation language: `typescript`, `bash`, or `python`. Defaults to `typescript`. |
|
||||
| `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 Fields
|
||||
#### Input Types
|
||||
|
||||
Each input is a key under `inputs:` with these properties:
|
||||
| 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 | Parameter type: `string`, `number`, `boolean`, or `enum`. |
|
||||
| `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. |
|
||||
| `values` | string | no | Comma-separated allowed values when `type` is `enum`. |
|
||||
| `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 documentation that the agent sees when the tool is loaded. Include:
|
||||
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`
|
||||
- **Authentication** — How credentials are resolved (env vars, integrations, etc.)
|
||||
- **Usage** — How to call the tool and what parameters to pass.
|
||||
- **Examples** — Common usage patterns.
|
||||
- **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, billing, external links.
|
||||
- **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
|
||||
|
||||
@@ -98,7 +184,7 @@ type OnUpdate = (partial: { content: Array<{ type: string; text: string }> }) =>
|
||||
export async function execute(
|
||||
toolCallId: string,
|
||||
params: Record<string, unknown>,
|
||||
signal: AbortSignal | undefined,
|
||||
signal?: AbortSignal,
|
||||
onUpdate?: OnUpdate,
|
||||
): Promise<ToolResult> {
|
||||
// Implementation here
|
||||
@@ -109,61 +195,397 @@ export async function execute(
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `toolCallId` | Unique ID for this tool call. |
|
||||
| `params` | Input values from the agent, matching the `inputs` defined in TOOL.md. |
|
||||
| `signal` | AbortSignal for cancellation. |
|
||||
| `onUpdate` | Callback for streaming progress updates to the agent during long operations. |
|
||||
| `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. The agent sees the error and can react.
|
||||
- `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:
|
||||
Use `onUpdate` to stream status during long-running operations. The agent sees each update in real time:
|
||||
|
||||
```typescript
|
||||
onUpdate?.({ content: [{ type: 'text', text: 'Processing step 2 of 5...' }] });
|
||||
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, not require the agent to pass them. Pattern:
|
||||
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 stored in the database (Settings → Integrations).
|
||||
|
||||
### Runtime Environment
|
||||
|
||||
Tools run inside sandboxed containers using **Node.js** (not Bun). Do not use Bun-specific APIs like `Bun.sleep`, `Bun.file`, etc. Use Node.js equivalents:
|
||||
|
||||
- `setTimeout` / `setInterval` for delays
|
||||
- `fs.readFileSync` / `fs.writeFileSync` for file I/O
|
||||
- `fetch` (available in Node 18+) for HTTP requests
|
||||
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 (e.g., API responses with many items), provide an `output_path` parameter. When set, save the data to the file and return a summary instead:
|
||||
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 { content: [{ type: 'text', text: `${items.length} items saved to ${params.output_path}` }] };
|
||||
return ok(`${items.length} items saved to ${params.output_path}`);
|
||||
}
|
||||
```
|
||||
|
||||
This prevents flooding the agent's context window with raw data.
|
||||
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
|
||||
|
||||
Tools are synced from `seed/tools/` to `DATA_PATH/tools/` at server startup by `sync-tools.ts`. The sync is version-based — it only overwrites when the seed version is higher than the target version. Always bump `version` in the frontmatter when updating a tool.
|
||||
### How Sync Works
|
||||
|
||||
The `tool-loader` extension discovers tools from directories listed in the `PI_TOOLS_DIRS` environment variable (colon-separated). Only `DATA_PATH/tools/` is mounted into containers — `seed/tools/` is not directly accessible at runtime.
|
||||
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
|
||||
|
||||
@@ -178,3 +600,4 @@ The `tool-loader` extension discovers tools from directories listed in the `PI_T
|
||||
| `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) |
|
||||
|
||||
Reference in New Issue
Block a user