@nlindstedt/zag-agent
v0.12.0
Published
TypeScript SDK for zag — a unified CLI for AI coding agents
Maintainers
Readme
Zag TypeScript Binding
TypeScript binding for zag — a unified CLI for AI coding agents.
Prerequisites
- Node.js 18+
- The
zagCLI binary installed and on yourPATH(or set viaZAG_BINenv var)
Installation
npm install @nlindstedt/zag-agentDevelopment setup
To work with the binding from source:
cd bindings/typescript
npm install
npm run buildQuick start
import { ZagBuilder } from "@nlindstedt/zag-agent";
// Non-interactive execution
const output = await new ZagBuilder()
.provider("claude")
.model("sonnet")
.autoApprove()
.exec("write a hello world program");
console.log(output.result);Streaming
import { ZagBuilder } from "@nlindstedt/zag-agent";
// Stream events as they arrive (NDJSON)
for await (const event of new ZagBuilder().provider("claude").stream("analyze code")) {
console.log(event.type, event);
}Bidirectional streaming sessions
execStreaming() returns a StreamingSession with piped stdin for sending
user messages mid-flight (Claude only). Call .close({ timeout }) when you
are done to shut the session down gracefully — it closes stdin, waits for
the child to exit, escalates to SIGTERM, and finally SIGKILL if the child
refuses to stop:
const session = await new ZagBuilder()
.provider("claude")
.execStreaming("initial prompt");
session.sendUserMessage("follow-up question");
for await (const event of session.events()) {
console.log(event.type);
}
// Graceful shutdown — replaces the closeInput/wait/terminate/kill dance.
await session.close({ timeout: "5s" });Automatic orphan cleanup
Long-running Node servers (Next.js, Express) can leak zag/agent
subprocesses if the parent process dies unexpectedly. Opt in to
.autoCleanup() on the builder to install process-wide shutdown handlers
(exit, SIGINT, SIGTERM, SIGHUP, uncaughtException) that SIGTERM
every tracked live session:
const session = await new ZagBuilder()
.provider("claude")
.autoCleanup()
.execStreaming("hello");Off by default so the SDK imposes no global side effects on consumers that don't need them.
Builder methods
| Method | Description |
|--------|-------------|
| .provider(name) | Set provider: "claude", "codex", "gemini", "copilot", "ollama" |
| .model(name) | Set model name or size alias ("small", "medium", "large") |
| .systemPrompt(text) | Set a system prompt |
| .root(path) | Set the working directory |
| .autoApprove() | Skip permission prompts |
| .addDir(path) | Add an additional directory (chainable) |
| .file(path) | Attach a file to the prompt (chainable) |
| .env(key, value) | Add an environment variable for the agent subprocess (chainable) |
| .json() | Request JSON output |
| .jsonSchema(schema) | Validate output against a JSON schema (implies .json()) |
| .worktree(name?) | Run in an isolated git worktree |
| .sandbox(name?) | Run in a Docker sandbox |
| .sessionId(uuid) | Use a specific session ID |
| .outputFormat(fmt) | Set output format ("text", "json", "json-pretty", "stream-json") |
| .inputFormat(fmt) | Set input format ("text", "stream-json" — Claude only) |
| .replayUserMessages() | Re-emit user messages on stdout (Claude only) |
| .includePartialMessages() | Include partial message chunks (Claude only) |
| .maxTurns(n) | Set the maximum number of agentic turns |
| .timeout(duration) | Set a timeout duration (e.g., "30s", "5m", "1h"). Kills the agent if exceeded. |
| .mcpConfig(config) | MCP server config: JSON string or file path (Claude only) |
| .showUsage() | Show token usage statistics (JSON output mode) |
| .size(size) | Set Ollama model parameter size (e.g., "2b", "9b", "35b") |
| .autoCleanup(enabled?) | Install process-wide shutdown handlers that SIGTERM tracked StreamingSession children on parent exit (opt-in) |
| .verbose() | Enable verbose output |
| .quiet() | Suppress non-essential output |
| .debug() | Enable debug logging |
| .bin(path) | Override the zag binary path |
Provider support for streaming / MCP flags
Four builder methods that toggle streaming I/O details and per-invocation MCP configuration are only honored by the Claude provider. Passing them to any other provider is a no-op.
| Method | Claude | Codex | Gemini | Copilot | Ollama |
|--------|--------|-------|--------|---------|--------|
| .inputFormat() | Yes | No | No | No | No |
| .replayUserMessages() | Yes | No | No | No | No |
| .includePartialMessages() | Yes | No | No | No | No |
| .mcpConfig() | Yes | No | No | No | No |
.execStreaming() is Claude-only and always sets -i stream-json, -o stream-json, and --replay-user-messages. By default it emits one assistant_message event per complete assistant turn — you get one event when the model finishes speaking, not a stream of token chunks. Call .includePartialMessages(true) to receive token-level partial assistant_message chunks instead. The default stays false so existing callers that render whole-turn bubbles are not broken.
At the end of every agent turn the session emits a turn_complete event carrying the provider's stop_reason (end_turn, tool_use, max_tokens, stop_sequence, or null), a zero-based monotonic turn_index, and the turn's usage. A per-turn result event fires immediately after. New code should key turn-boundary UI off turn_complete — it is the authoritative signal and carries richer metadata than result. result continues to fire per-turn for backward compatibility.
Terminal methods
| Method | Returns | Description |
|--------|---------|-------------|
| .exec(prompt) | Promise<AgentOutput> | Run non-interactively, return structured output |
| .stream(prompt) | AsyncGenerator<Event> | Stream NDJSON events |
| .execStreaming(prompt) | StreamingSession | Bidirectional streaming (Claude only). Emits one assistant_message event per complete turn; pair with .includePartialMessages(true) for token-level chunks. |
| .run(prompt?) | Promise<void> | Start an interactive session (inherits stdio) |
| .resume(sessionId) | Promise<void> | Resume a previous session by ID |
| .continueLast() | Promise<void> | Resume the most recent session |
| .execResume(sessionId, prompt) | Promise<AgentOutput> | Resume a session non-interactively with a follow-up prompt |
| .execContinue(prompt) | Promise<AgentOutput> | Resume the most recent session non-interactively |
| .streamResume(sessionId, prompt) | AsyncGenerator<Event> | Resume a session in streaming mode |
| .streamContinue(prompt) | AsyncGenerator<Event> | Resume the most recent session in streaming mode |
Version checking
The SDK automatically checks the installed zag CLI version before running commands. If you use a builder method that requires a newer CLI version than what's installed, a clear error is thrown:
env() requires zag CLI >= 0.6.0, but the installed version is 0.5.0.
Please update the zag binary.The version is detected once (by running zag --version) and cached for the lifetime of the process.
| Method | Minimum CLI version |
|--------|-------------------|
| .env() | 0.6.0 |
| .mcpConfig() | 0.6.0 |
All other methods are available since the initial release (0.2.3).
Discovery
Standalone functions for discovering available providers, models, and capabilities:
import { listProviders, getCapability, getAllCapabilities, resolveModel } from "zag-agent";
const providers = await listProviders();
const cap = await getCapability("claude");
const all = await getAllCapabilities();
const resolved = await resolveModel("claude", "small"); // { input: "small", resolved: "haiku", is_alias: true }| Function | Description |
|----------|-------------|
| listProviders(bin?) | List available provider names |
| getCapability(provider, bin?) | Get capabilities for a provider |
| getAllCapabilities(bin?) | Get capabilities for all providers |
| resolveModel(provider, model, bin?) | Resolve a model alias |
How it works
The SDK spawns the zag CLI as a subprocess (zag exec -o json or -o stream-json) and parses the JSON/NDJSON output into typed models. Zero external runtime dependencies — only Node.js built-ins.
Testing
npm run build && npm test