@dex-ai/tools-extension
v0.1.9
Published
Core tool Extensions for @dex-ai/sdk — file read/write/edit, search, bash. Each tool is its own Extension.
Downloads
353
Readme
@dex-ai/tools
Core tool Extensions for @dex-ai/sdk. Each tool is its own Extension — compose the ones you want, skip the ones you don't.
| Extension | Tool | What it does | Sandboxed | Destructive |
|------------------------|-------------|-----------------------------------------------------------|-----------|-------------|
| readFileExtension | read | Read text, normalized images, or base64 binary files. | yes (cwd) | no |
| readImageExtension | read_image| Compatibility image-only reader; prefer read. | yes (cwd) | no |
| writeFileExtension | write_file| Create or overwrite a file. | yes (cwd) | yes |
| editFileExtension | edit_file | Atomic multi-edit by exact string match. | yes (cwd) | yes |
| searchExtension | search | grep regex over contents, or glob over paths. | yes (cwd) | no |
| bashExtension | bash | Run sh -c <command> with timeout + captured output. | no | yes |
Install + use
import { DexAgent } from '@dex-ai/runtime';
import { openai } from '@dex-ai/openai';
import {
readFileExtension, readImageExtension,
writeFileExtension, editFileExtension,
searchExtension, bashExtension,
allFsExtensions,
} from '@dex-ai/tools';
const agent = new DexAgent({
provider: openai({ modelId: 'gpt-4.1' }),
extensions: [
// pick individually...
readFileExtension({ cwd: process.cwd() }),
searchExtension({ cwd: process.cwd() }),
// compatibility only; `read` handles images too
readImageExtension({ cwd: process.cwd() }),
// ...or bulk register the full fs set:
...allFsExtensions({ cwd: process.cwd() }),
bashExtension({ cwd: process.cwd() }),
],
});Subpath imports are also available for bundle-size control:
import { bashExtension } from '@dex-ai/tools/bash';Tool contracts
read
params: { path: string, lineStart?: number, lineEnd?: number }
text output: { type: 'text', value: string }
image output: {
type: 'content',
value: [
{ type: 'text', text: string },
{ type: 'image', image: Uint8Array, mediaType: string }
]
}
binary output: {
type: 'content',
value: [
{ type: 'text', text: string },
{ type: 'file', data: string /* base64 */, mediaType: string, name: string }
]
}
image behavior: supported raster images are normalized to fit within 1080p at medium JPEG quality before being returned.
line ranges: lineStart/lineEnd are supported for text files only.
errors: path-outside-cwd, file-not-found, not-a-regular-file, too-large (>10MB text/binary, >20MB image input)read_image compatibility tool
params: { path: string }
output: same image content shape as `read`
supported: image/png, image/jpeg, image/webp, image/gif
behavior: normalizes images to fit within 1080p at medium JPEG quality before returning.
errors: path-outside-cwd, file-not-found, not-a-regular-file, too-large (>20MB), unsupported-image-typewrite_file
params: { path: string, content: string }
output: { type: 'json', value: { bytesWritten: number, created: boolean } }
behavior: creates parent dirs if missing; overwrites existing filesedit_file
params: {
path: string,
edits: Array<{ oldString: string, newString: string, replaceAll?: boolean }>,
}
output: { type: 'json', value: { appliedEdits: number } }
semantics: all-or-nothing. Each oldString must appear exactly once unless
replaceAll is true. Overlapping ranges fail. File must exist.
Use write_file to create new files.search
params: {
mode: 'grep' | 'find',
pattern: string, // grep: regex source; find: glob (*, **, ?)
path?: string, // defaults to cwd
maxResults?: number, // default 100
caseInsensitive?: boolean // grep-only
}
output: { type: 'json', value: { matches: [...], truncated: boolean } }
behavior: skips node_modules, .git, dist by default unless the caller scopes
`path` into them.bash
params: { command: string, timeoutMs?: number } // default 120_000ms
output: { type: 'json', value: { stdout, stderr, exitCode, timedOut } }
behavior: runs via /bin/sh -c in cwd. Timeout kills the process and returns
timedOut: true rather than throwing.Approval — it's not here
This package ships zero approval logic. The SDK treats approval as cross-cutting via the onToolCall hook; apps install their own approver extension. The typical shape:
import type { Extension, ToolCall, ToolResult } from '@dex-ai/sdk';
function approvalExtension(opts: {
gates: (call: ToolCall) => boolean; // returns true if this call needs approval
ask: (call: ToolCall) => Promise<boolean>; // async prompt; returns true to allow
}): Extension {
return {
name: 'approval',
async onToolCall(call): Promise<ToolResult | void> {
if (!opts.gates(call)) return; // no gate — run the tool
const ok = await opts.ask(call);
if (ok) return; // approved — pass through
return { // rejected — model sees this as a tool-result
toolCallId: call.toolCallId,
toolName: call.toolName,
output: { type: 'error-text', value: 'User rejected this tool call.' },
};
},
};
}
// wire it up:
const agent = new DexAgent({
provider,
extensions: [
bashExtension({ cwd }),
writeFileExtension({ cwd }),
approvalExtension({
gates: (call) => ['bash', 'write_file', 'edit_file'].includes(call.toolName),
ask: (call) => ui.confirm(`Run ${call.toolName}: ${JSON.stringify(call.input)}?`),
}),
],
});Rejected tool calls come back to the model as a normal tool-result message containing error-text. The model decides what to do next (try a different tool, stop, ask the user).
Sandbox details
The fs tools resolve every path against cwd and reject anything outside. Specifically:
../traversal is rejected (paths are normalized).- Absolute paths outside
cwdare rejected. - Symlinks are followed via
fs.realpath— a symlink insidecwdthat points outside is rejected.
bash is not sandboxed at the filesystem level. It runs with cwd as its working directory but any approved shell command can read/write anywhere the process has permission to. Approval is the gate — use onToolCall.
Testing
bun test50 tests across 6 files as of v0.1 — every tool exercised end-to-end through a DexAgent + scripted fake Provider.
