Getting started
@clarvis/agent-toolsis a transport-agnostic library that gives an LLM agent nine coding tools over a workspace. You give it a workspace root; it gives you a validated, bounded, workspace-confined surface you can advertise to a model and call. This page takes you from install to a first read and a first edit.
Install
npm install @clarvis/agent-tools- Node ≥ 20, ESM only. The package ships ES modules with type declarations; there is no CommonJS build. Import it from an ESM module (or a
.mts/"type": "module"project). - Optional ripgrep. If ripgrep (
rg) is onPATH,grepuses it; otherwise an equivalent in-process backend runs. Results are kept consistent between the two.
Your first tools instance
The ergonomic entry point is createAgentTools. Give it a workspace root — an existing directory that all relative tool paths resolve against — and it resolves a config, then lets you list and call tools.
import { createAgentTools } from "@clarvis/agent-tools";
const tools = createAgentTools({ workspaceRoot: process.cwd() });
// The surface to advertise to your model: name / description / JSON Schema per tool.
const surface = tools.listTools();
console.log(surface.map((t) => t.name));
// [ 'read_file', 'list_dir', 'glob', 'grep',
// 'write_file', 'edit_file', 'multi_edit', 'apply_patch', 'bash' ]Each entry of listTools() is { name, description, inputSchema }, where inputSchema is a JSON Schema you can hand straight to a model's tool-use / function-calling API.
Call a tool
callTool(name, args) validates args against the tool's schema, runs it, and returns a DispatchResult:
const res = await tools.callTool("read_file", { path: "package.json" });
if (res.isError) {
const err = JSON.parse(res.text) as { error: string; message: string };
console.error(err.error, err.message);
} else {
console.log(res.text); // file contents, line-numbered and byte-bounded
}The result contract
callTool never throws for tool-level problems — it always resolves to { isError, text }:
- On success (
isError: false),textis the tool's output, already bounded tomaxOutputBytes. It is plain text for every tool exceptbash, whose successtextis a JSON object ({ exit_code, stdout, stderr, signal, timed_out }) — a non-zero exit is still a success. - On failure (
isError: true),textis a JSON error envelope:{ "error": "<code>", "message": "…", …fields }. An unknown tool name — or a mutating tool while in read-only mode — comes back as anisErrorresult with codenot_found.
See Error codes for the full list of error values.
Make a change
Mutating tools work the same way. Overwrite or create a file with write_file, then adjust it with edit_file:
await tools.callTool("write_file", {
path: "notes.txt",
content: "hello\nworld\n",
});
const edit = await tools.callTool("edit_file", {
path: "notes.txt",
old_string: "world",
new_string: "agent-tools",
});
console.log(edit.text); // "Replaced 1 occurrence …"edit_file matches old_string literally and exactly first; only when an exact match fails does it fall back to a whitespace-tolerant search, and only if that resolves to exactly one region. See The tools for the matching rules.
Two API levels
- Factory —
createAgentTools(options)resolves a config once and returnslistTools()/callTool(). This is what most consumers want; wire it into your loop with Embed it in an agent loop. - Core —
resolveConfig,dispatch,listTools, and the raw tool registry are exported for building your own transport. See The core API.
See also
- Embed it in an agent loop — the read → model →
callTool→ feed-back cycle - Configuration — every option and its default
- The tools — per-tool inputs, output, and error codes
- Workspace confinement · Deploy securely