npm package: @protolabsai/sdk
Description: TypeScript SDK for programmatic access to protoCLI
Author: protoLabs
Latest version: 0.2.0

# @protolabsai/sdk

A TypeScript SDK for programmatic access to protoCLI.

Feel free to submit a feature request/issue/PR.

## Installation

```bash
npm install @protolabsai/sdk
```

## Requirements

- Node.js >= 20.0.0

> From v0.1.1, the CLI is bundled with the SDK. So no standalone CLI installation is needed.

## Quick Start

```typescript
import { query } from '@protolabsai/sdk';

// Single-turn query
const result = query({
  prompt: 'What files are in the current directory?',
  options: {
    cwd: '/path/to/project',
  },
});

// Iterate over messages
for await (const message of result) {
  if (message.type === 'assistant') {
    console.log('Assistant:', message.message.content);
  } else if (message.type === 'result') {
    console.log('Result:', message.result);
  }
}
```

## API Reference

### `query(config)`

Creates a new query session with protoCLI.

#### Parameters

- `prompt`: `string | AsyncIterable<SDKUserMessage>` - The prompt to send. Use a string for single-turn queries or an async iterable for multi-turn conversations.
- `options`: `QueryOptions` - Configuration options for the query session.

#### QueryOptions

| Option                   | Type                                           | Default          | Description                                                                                                                                                                                                                                                                                                                                   |
| ------------------------ | ---------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cwd`                    | `string`                                       | `process.cwd()`  | The working directory for the query session. Determines the context in which file operations and commands are executed.                                                                                                                                                                                                                       |
| `model`                  | `string`                                       | -                | The AI model to use (e.g., `'claude-opus-4-6'`, `'claude-sonnet-4-6'`, `'claude-haiku-4-5-20251001'`). Takes precedence over the `OPENAI_MODEL` environment variable.                                                                                                                                                                         |
| `pathToQwenExecutable`   | `string`                                       | Auto-detected    | Path to the proto CLI executable. Supports multiple formats: `'proto'` (native binary from PATH), `'/path/to/proto'` (explicit path), `'/path/to/cli.js'` (Node.js bundle), `'node:/path/to/cli.js'` (force Node.js runtime), `'bun:/path/to/cli.js'` (force Bun runtime). If not provided, auto-detects the bundled CLI.                     |
| `permissionMode`         | `'default' \| 'plan' \| 'auto-edit' \| 'yolo'` | `'default'`      | Permission mode controlling tool execution approval. See [Permission Modes](#permission-modes) for details.                                                                                                                                                                                                                                   |
| `canUseTool`             | `CanUseTool`                                   | -                | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See [Custom Permission Handler](#custom-permission-handler).                                                                                                             |
| `baseURL`                | `string`                                       | -                | Base URL for the AI provider API. Routes requests through a custom gateway or proxy instead of calling the provider directly. Sets `OPENAI_BASE_URL` in the CLI process environment. Example: `'http://gateway:4000/v1'`.                                                                                                                     |
| `env`                    | `Record<string, string>`                       | -                | Environment variables to pass to the proto CLI process. Merged with the current process environment.                                                                                                                                                                                                                                          |
| `systemPrompt`           | `string \| QuerySystemPromptPreset`            | -                | System prompt configuration for the main session. Use a string to fully override the built-in protoCLI system prompt, or a preset object to keep the built-in prompt and append extra instructions.                                                                                                                                           |
| `mcpServers`             | `Record<string, McpServerConfig>`              | -                | MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like `command`, `args`, `url`, `httpUrl`, etc. SDK servers use `{ type: 'sdk', name: string, instance: Server }`.                                                |
| `abortController`        | `AbortController`                              | -                | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources.                                                                                                                                                                                                                        |
| `debug`                  | `boolean`                                      | `false`          | Enable debug mode for verbose logging from the CLI process.                                                                                                                                                                                                                                                                                   |
| `maxSessionTurns`        | `number`                                       | `-1` (unlimited) | Maximum number of conversation turns before the session automatically terminates. A turn consists of a user message and an assistant response.                                                                                                                                                                                                |
| `coreTools`              | `string[]`                                     | -                | Equivalent to `permissions.allow` in settings.json as an allowlist. If specified, only these tools will be available to the AI (all other tools are disabled at registry level). Supports tool name aliases and pattern matching. Example: `['Read', 'Edit', 'Bash(git *)']`.                                                                 |
| `excludeTools`           | `string[]`                                     | -                | Equivalent to `permissions.deny` in settings.json. Excluded tools return a permission error immediately. Takes highest priority over all other permission settings. Supports tool name aliases and pattern matching: tool name (`'write_file'`), shell command prefix (`'Bash(rm *)'`), or path patterns (`'Read(.env)'`, `'Edit(/src/**)'`). |
| `allowedTools`           | `string[]`                                     | -                | Equivalent to `permissions.allow` in settings.json. Matching tools bypass `canUseTool` callback and execute automatically. Only applies when tool requires confirmation. Supports same pattern matching as `excludeTools`. Example: `['ShellTool(git status)', 'ShellTool(npm test)']`.                                                       |
| `authType`               | `'openai'`                                     | `'openai'`       | Authentication type for the AI service. Authenticates via OpenAI-compatible API using `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`.                                                                                                                                                                                                                |
| `agents`                 | `SubagentConfig[]`                             | -                | Configuration for subagents that can be invoked during the session. Subagents are specialized AI agents for specific tasks or domains.                                                                                                                                                                                                        |
| `includePartialMessages` | `boolean`                                      | `false`          | When `true`, the SDK emits incomplete messages as they are being generated, allowing real-time streaming of the AI's response.                                                                                                                                                                                                                |
| `resume`                 | `string`                                       | -                | Resume a previous session by providing its session ID. Equivalent to CLI's `--resume` flag.                                                                                                                                                                                                                                                   |
| `sessionId`              | `string`                                       | -                | Specify a session ID for the new session. Ensures SDK and CLI use the same ID without resuming history. Equivalent to CLI's `--session-id` flag.                                                                                                                                                                                              |

> [!tip]
> When configuring `coreTools`, `excludeTools`, or `allowedTools`, use tool name aliases and pattern matching syntax (e.g., `Bash(git *)`, `Read(.env)`, `Edit(/src/**)`) to target specific tools or shell commands.

### Timeouts

The SDK enforces the following default timeouts:

| Timeout          | Default  | Description                                                                                                                  |
| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `canUseTool`     | 1 minute | Maximum time for `canUseTool` callback to respond. If exceeded, the tool request is auto-denied.                             |
| `mcpRequest`     | 1 minute | Maximum time for SDK MCP tool calls to complete.                                                                             |
| `controlRequest` | 1 minute | Maximum time for control operations like `initialize()`, `setModel()`, `setPermissionMode()`, and `interrupt()` to complete. |
| `streamClose`    | 1 minute | Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers.        |

You can customize these timeouts via the `timeout` option:

```typescript
const q = query({
  prompt: 'Your prompt',
  options: {
    timeout: {
      canUseTool: 60000, // 60 seconds for permission callback
      mcpRequest: 600000, // 10 minutes for MCP tool calls
      controlRequest: 60000, // 60 seconds for control requests
      streamClose: 15000, // 15 seconds for stream close wait
    },
  },
});
```

### Message Types

The SDK provides type guards to identify different message types:

```typescript
import {
  isSDKUserMessage,
  isSDKAssistantMessage,
  isSDKSystemMessage,
  isSDKResultMessage,
  isSDKPartialAssistantMessage,
} from '@protolabsai/sdk';

for await (const message of result) {
  if (isSDKAssistantMessage(message)) {
    // Handle assistant message
  } else if (isSDKResultMessage(message)) {
    // Handle result message
  }
}
```

### Query Instance Methods

The `Query` instance returned by `query()` provides several methods:

```typescript
const q = query({ prompt: 'Hello', options: {} });

// Get session ID
const sessionId = q.getSessionId();

// Check if closed
const closed = q.isClosed();

// Interrupt the current operation
await q.interrupt();

// Change permission mode mid-session
await q.setPermissionMode('yolo');

// Change model mid-session
await q.setModel('claude-sonnet-4-6');

// Close the session
await q.close();
```

## Permission Modes

The SDK supports different permission modes for controlling tool execution:

- **`default`**: Write tools are denied unless approved via `canUseTool` callback or in `allowedTools`. Read-only tools execute without confirmation.
- **`plan`**: Blocks all write tools, instructing AI to present a plan first.
- **`auto-edit`**: Auto-approve edit tools (edit, write_file) while other tools require confirmation.
- **`yolo`**: All tools execute automatically without confirmation.

### Permission Priority Chain

Decision priority (highest first): `deny` > `ask` > `allow` > _(default/interactive mode)_

The first matching rule wins.

1. `excludeTools` / `permissions.deny` - Blocks tools completely (returns permission error)
2. `permissions.ask` - Always requires user confirmation
3. `permissionMode: 'plan'` - Blocks all non-read-only tools
4. `permissionMode: 'yolo'` - Auto-approves all tools
5. `allowedTools` / `permissions.allow` - Auto-approves matching tools
6. `canUseTool` callback - Custom approval logic (if provided, not called for allowed tools)
7. Default behavior - Auto-deny in SDK mode (write tools require explicit approval)

## Examples

### Multi-turn Conversation

```typescript
import { query, type SDKUserMessage } from '@protolabsai/sdk';

async function* generateMessages(): AsyncIterable<SDKUserMessage> {
  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Create a hello.txt file' },
    parent_tool_use_id: null,
  };

  // Wait for some condition or user input
  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Now read the file back' },
    parent_tool_use_id: null,
  };
}

const result = query({
  prompt: generateMessages(),
  options: {
    permissionMode: 'auto-edit',
  },
});

for await (const message of result) {
  console.log(message);
}
```

### Custom Permission Handler

```typescript
import { query, type CanUseTool } from '@protolabsai/sdk';

const canUseTool: CanUseTool = async (toolName, input, { signal }) => {
  // Allow all read operations
  if (toolName.startsWith('read_')) {
    return { behavior: 'allow', updatedInput: input };
  }

  // Prompt user for write operations (in a real app)
  const userApproved = await promptUser(`Allow ${toolName}?`);

  if (userApproved) {
    return { behavior: 'allow', updatedInput: input };
  }

  return { behavior: 'deny', message: 'User denied the operation' };
};

const result = query({
  prompt: 'Create a new file',
  options: {
    canUseTool,
  },
});
```

### With External MCP Servers

```typescript
import { query } from '@protolabsai/sdk';

const result = query({
  prompt: 'Use the custom tool from my MCP server',
  options: {
    mcpServers: {
      'my-server': {
        command: 'node',
        args: ['path/to/mcp-server.js'],
        env: { PORT: '3000' },
      },
    },
  },
});
```

### Route Through a Custom Gateway

```typescript
import { query } from '@protolabsai/sdk';

// Route through a gateway or proxy instead of calling the API directly
const result = query({
  prompt: 'What is the status of this project?',
  options: {
    model: 'claude-opus-4-6',
    baseURL: 'http://gateway:4000/v1',
    cwd: '/path/to/project',
  },
});

for await (const message of result) {
  if (message.type === 'result') {
    console.log(message.result);
  }
}
```

### Override the System Prompt

```typescript
import { query } from '@protolabsai/sdk';

const result = query({
  prompt: 'Say hello in one sentence.',
  options: {
    systemPrompt: 'You are a terse assistant. Answer in exactly one sentence.',
  },
});
```

### Append to the Built-in System Prompt

```typescript
import { query } from '@protolabsai/sdk';

const result = query({
  prompt: 'Review the current directory.',
  options: {
    systemPrompt: {
      append: 'Be terse and focus on concrete findings.',
    },
  },
});
```

### With SDK-Embedded MCP Servers

The SDK provides `tool` and `createSdkMcpServer` to create MCP servers that run in the same process as your SDK application. This is useful when you want to expose custom tools to the AI without running a separate server process.

#### `tool(name, description, inputSchema, handler)`

Creates a tool definition with Zod schema type inference.

| Parameter     | Type                               | Description                                                              |
| ------------- | ---------------------------------- | ------------------------------------------------------------------------ |
| `name`        | `string`                           | Tool name (1-64 chars, starts with letter, alphanumeric and underscores) |
| `description` | `string`                           | Human-readable description of what the tool does                         |
| `inputSchema` | `ZodRawShape`                      | Zod schema object defining the tool's input parameters                   |
| `handler`     | `(args, extra) => Promise<Result>` | Async function that executes the tool and returns MCP content blocks     |

The handler must return a `CallToolResult` object with the following structure:

```typescript
{
  content: Array<
    | { type: 'text'; text: string }
    | { type: 'image'; data: string; mimeType: string }
    | { type: 'resource'; uri: string; mimeType?: string; text?: string }
  >;
  isError?: boolean;
}
```

#### `createSdkMcpServer(options)`

Creates an SDK-embedded MCP server instance.

| Option    | Type                     | Default   | Description                          |
| --------- | ------------------------ | --------- | ------------------------------------ |
| `name`    | `string`                 | Required  | Unique name for the MCP server       |
| `version` | `string`                 | `'1.0.0'` | Server version                       |
| `tools`   | `SdkMcpToolDefinition[]` | -         | Array of tools created with `tool()` |

Returns a `McpSdkServerConfigWithInstance` object that can be passed directly to the `mcpServers` option.

#### Example

```typescript
import { z } from 'zod';
import { query, tool, createSdkMcpServer } from '@protolabsai/sdk';

// Define a tool with Zod schema
const calculatorTool = tool(
  'calculate_sum',
  'Add two numbers',
  { a: z.number(), b: z.number() },
  async (args) => ({
    content: [{ type: 'text', text: String(args.a + args.b) }],
  }),
);

// Create the MCP server
const server = createSdkMcpServer({
  name: 'calculator',
  tools: [calculatorTool],
});

// Use the server in a query
const result = query({
  prompt: 'What is 42 + 17?',
  options: {
    permissionMode: 'yolo',
    mcpServers: {
      calculator: server,
    },
  },
});

for await (const message of result) {
  console.log(message);
}
```

### Abort a Query

```typescript
import { query, isAbortError } from '@protolabsai/sdk';

const abortController = new AbortController();

const result = query({
  prompt: 'Long running task...',
  options: {
    abortController,
  },
});

// Abort after 5 seconds
setTimeout(() => abortController.abort(), 5000);

try {
  for await (const message of result) {
    console.log(message);
  }
} catch (error) {
  if (isAbortError(error)) {
    console.log('Query was aborted');
  } else {
    throw error;
  }
}
```

## Error Handling

The SDK provides an `AbortError` class for handling aborted queries:

```typescript
import { AbortError, isAbortError } from '@protolabsai/sdk';

try {
  // ... query operations
} catch (error) {
  if (isAbortError(error)) {
    // Handle abort
  } else {
    // Handle other errors
  }
}
```

## License

Apache-2.0 - see [LICENSE](./LICENSE) for details.