# @qoder-ai/qoder-agent-sdk

TypeScript SDK for building applications on top of Qoder Agent.

The SDK starts `qodercli` for you, streams agent messages back to Node.js, and
lets your application configure tools, permissions, working directories, MCP
servers, hooks, and interactive sessions.

## Installation

```bash
npm install @qoder-ai/qoder-agent-sdk zod
```

Prerequisites:

- Node.js 18+
- A Qoder account or another authentication method supported by your host
  application

`zod` is a peer dependency used when you define MCP tool schemas. Keep it
installed if your application uses SDK tools or MCP integration.

## Runtime Behavior

The npm package uses `WorkerTransport.default` for `query()` by default. During
`npm install`, its postinstall script downloads the Worker runtime selected by
the package's pinned CLI version into `dist/_worker`. It does not download the
process CLI unless `QODER_INSTALL_BUNDLED_CLI=1` is set.

You can still select a transport explicitly on the same `query()` entry:

```ts
import {
  qodercliAuth,
  query,
  WorkerTransport,
} from "@qoder-ai/qoder-agent-sdk";

const q = query({
  prompt: "Summarize this repository.",
  options: {
    auth: qodercliAuth(),
    transport: WorkerTransport.default,
  },
});
```

The worker transport uses the runtime installed at
`dist/_worker/qoder-worker-runtime.obf.mjs`. To use another Worker runtime,
pass its path explicitly with
`new WorkerTransport({ pathToQoderWorkerRuntime })`. Environment variables do
not override the Worker runtime entry.

The package includes a `dist/runtime-manifest.json` that records the default
runtime and the URL template for qodercli Worker runtime artifacts. At
installation time, the SDK resolves exactly one artifact for its own brand,
the current operating system, and the current architecture. It does not fall
back to the universal artifact. The SDK package itself does not need to host a
copy of the archive. The presence of `dist/_worker` is used as an integrity
check after installation, not as the mode selector.

The installed Worker runtime is self-contained. Platform packages include the
matching native keychain, PTY, image-processing, and ripgrep assets. Linux
packages retain both glibc and musl image-processing bindings. OpenHarmony does
not have an install-delivered platform artifact and must use an explicitly
supplied runtime source.

If your environment blocks install scripts or network downloads, skip postinstall
runtime downloads:

```bash
QODER_SKIP_DOWNLOAD=1 npm install @qoder-ai/qoder-agent-sdk zod
```

`QODER_SKIP_DOWNLOAD` only affects install-time downloads. It does not change the
package's default runtime. Then point the process transport at an existing
`qodercli`:

```bash
export QODERCLI_PATH=/absolute/path/to/qodercli
```

To use an existing Worker runtime, pass its absolute path through
`new WorkerTransport({ pathToQoderWorkerRuntime })`.

`query()` is the only query entry point and defaults to Worker. Pass
`transport: ProcessTransport.default`,
`transport: WorkerTransport.default`, or a custom transport provider when a
single call needs to override the package default.

`pathToQoderCLIExecutable` accepts either local runtime form. Native binaries
and ordinary CLI `.js`/`.mjs` entries use the process transport; recognized
`qoder-worker-runtime*.mjs` entries use the Worker transport. An explicit
`transport` provider always takes precedence over automatic path selection.

## Authentication

Every SDK query needs an explicit authentication option.

| Authentication method | Identity | Use case |
| --- | --- | --- |
| Personal Access Token (PAT) | A Qoder user | Automation that needs the user's permissions and data |
| Service Account | An organization workload | Services and jobs that should not depend on a personal account |
| Local `qodercli` session | The signed-in user | Interactive development on a workstation |

For a PAT, generate a token at
[qoder.com/account/integrations](https://qoder.com/account/integrations), store
it in a secret manager, and expose it through the default environment variable:

```bash
export QODER_PERSONAL_ACCESS_TOKEN=your-token
```

```ts
import { accessTokenFromEnv, query } from "@qoder-ai/qoder-agent-sdk";

const q = query({
  prompt: "Summarize this repository.",
  options: {
    auth: accessTokenFromEnv(),
    cwd: process.cwd(),
  },
});
```

For a Service Account, read the key from your secret manager and pass it
directly to the SDK:

```ts
import { query, serviceAccount } from "@qoder-ai/qoder-agent-sdk";

// Get the Service Account key from the host's secret manager adapter.
const serviceAccountKey = await readSecret("qoder-service-account-key");

const q = query({
  prompt: "Summarize this repository.",
  options: {
    auth: serviceAccount({ serviceAccountKey }),
    cwd: process.cwd(),
  },
});
```

The SDK and CLI obtain and refresh short-lived Service Account tokens for this
authentication method. A host can retain the Service Account key and use
`serviceAccount({ fetchServiceAccountToken })` to obtain and refresh
short-lived SATs for qodercli. See the
[host callback example](examples/service-account-token-demo/demo.mjs) for a
complete Token exchange and query. To reuse a signed-in developer
workstation, use `qodercliAuth()`. See the
[SDK authentication guide](docs/public/global/cli/sdk/authentication.mdx) for
complete setup instructions and security guidance.

## Quick Start

Create `demo.mjs`:

```js
import { qodercliAuth, query } from "@qoder-ai/qoder-agent-sdk";

const q = query({
  prompt: process.argv.slice(2).join(" ") || "Explain what this project does.",
  options: {
    auth: qodercliAuth(),
    cwd: process.cwd(),
  },
});

try {
  for await (const message of q) {
    console.dir(message, { depth: null });
  }
} finally {
  await q.close();
}
```

Run it:

```bash
node demo.mjs "List the important files in this repository."
```

## Basic Usage

`query()` runs a single SDK query and returns an async iterator of response
messages.

```js
import { qodercliAuth, query } from "@qoder-ai/qoder-agent-sdk";

const q = query({
  prompt: "Explain this repository.",
  options: {
    auth: qodercliAuth(),
    cwd: process.cwd(),
    systemPrompt: "You are a helpful assistant.",
    maxTurns: 1,
  },
});

for await (const message of q) {
  if (message.type !== "assistant") {
    continue;
  }

  for (const block of message.message.content) {
    if (block.type === "text") {
      console.log(block.text);
    }
  }
}
```

## Code Security

Built-in code security is disabled by default in SDK sessions. Enable only the
capabilities the integration needs:

```ts
const q = query({
  prompt: "Review this repository for security issues.",
  options: {
    auth: qodercliAuth(),
    cwd: process.cwd(),
    securityScan: {
      l1StaticCheck: true,
      l2LightweightScan: true,
      l3DeepScan: false,
    },
  },
});
```

Omitted switches remain disabled. The top-level option is authoritative for
the SDK session. Enabling any switch also makes the native security skill
available; project and file scans do not have a separate switch.

## Tools and Permissions

Qoder Agent can use tools such as file reads, file edits, shell commands, and
MCP tools. `allowedTools` is an approval allowlist: listed tools are
auto-approved, while unlisted tools continue through `permissionMode` and
`canUseTool` for a decision. It does not remove tools from the agent's
available toolset. To block tools, use `disallowedTools`.

```ts
import { qodercliAuth, query } from "@qoder-ai/qoder-agent-sdk";

const q = query({
  prompt: "Update the README introduction.",
  options: {
    auth: qodercliAuth(),
    cwd: process.cwd(),
    allowedTools: ["Read", "Edit"],
    disallowedTools: ["Bash"],
    permissionMode: "acceptEdits",
  },
});
```

For application-specific approval flows, provide `canUseTool`:

```ts
import { qodercliAuth, query } from "@qoder-ai/qoder-agent-sdk";

const q = query({
  prompt: "Inspect the source code, but do not run shell commands.",
  options: {
    auth: qodercliAuth(),
    cwd: process.cwd(),
    canUseTool: async (toolName) => {
      if (toolName === "Bash") {
        return {
          behavior: "deny",
          message: "Shell commands are disabled here.",
        };
      }

      return { behavior: "allow" };
    },
  },
});
```

## Working Directory

Use `cwd` to run the agent in a specific project directory:

```ts
import { qodercliAuth, query } from "@qoder-ai/qoder-agent-sdk";

const q = query({
  prompt: "Analyze this project.",
  options: {
    auth: qodercliAuth(),
    cwd: "/path/to/project",
  },
});
```

## Interactive Sessions

Use an async iterable prompt when you need a long-lived, bidirectional session
instead of a single string prompt.

```ts
import { qodercliAuth, query } from "@qoder-ai/qoder-agent-sdk";

function userMessage(text: string) {
  return {
    type: "user",
    message: { role: "user", content: [{ type: "text", text }] },
    parent_tool_use_id: null,
  };
}

async function* conversation() {
  yield userMessage("Inspect this project and summarize the main modules.");
  yield userMessage("Now list the files that are most likely to need review.");
}

const q = query({
  prompt: conversation(),
  options: {
    auth: qodercliAuth(),
    cwd: process.cwd(),
  },
});

for await (const message of q) {
  console.dir(message, { depth: null });
}
```

The returned `Query` object is useful for chat interfaces, follow-up prompts,
interrupts, runtime permission changes, MCP server management, and other
workflows that need state across multiple turns. Runtime helpers include
`q.streamInput(...)`, `q.interrupt()`, `q.setPermissionMode(...)`,
`q.mcpAuthenticate(...)`, and `q.mcpSubmitOAuthCallbackUrl(...)`.

Streaming user messages support three delivery priorities:

- `priority: "now"` stops the current response and handles the message immediately.
- `priority: "next"` is the default and handles the message at the next suitable point.
- `priority: "later"` waits until the current response finishes.

`shouldQuery: false` adds the message to the conversation without starting a
response by itself. Its processing time still follows `priority`.

Assign `uuid` to messages that need delivery tracking or cancellation.
Do not reuse UUIDs within a session. `await q.interrupt()` stops the current
response without closing the session. `await q.cancelAsyncMessage(uuid)` returns
`true` when the queued message is cancelled and `false` when it can no longer
be cancelled.

## External Session Storage

Queries persist their local session by default. Set `persistSession: false` for
an ephemeral query that must not create resumable session state. Ephemeral
queries cannot use `sessionStore` because mirroring starts from locally
committed transcript entries.

Use `sessionStore` when a host needs durable transcripts outside the local
machine. The SDK mirrors entries after qodercli commits them locally. A later
process can restore the same session before qodercli starts:

```text
qodercli commit -> SDK append(key, entries) -> external store
external store -> SDK load(key) -> temporary QODER_CONFIG_DIR -> qodercli resume
```

```ts
import {
  InMemorySessionStore,
  qodercliAuth,
  query,
} from "@qoder-ai/qoder-agent-sdk";

const sessionStore = new InMemorySessionStore();

const first = query({
  prompt: "Inspect this project.",
  options: {
    auth: qodercliAuth(),
    cwd: "/path/to/project",
    sessionStore,
  },
});

for await (const message of first) {
  console.dir(message, { depth: null });
}

const resumed = query({
  prompt: "Continue the analysis.",
  options: {
    auth: qodercliAuth(),
    cwd: "/path/to/project",
    resume: "11111111-1111-4111-8111-111111111111",
    sessionStore,
  },
});
```

Every store implements `append(key, entries)` and `load(key)`. Implement
`listSessions(projectKey)` for `continue: true` and session listing,
`listSubkeys(key)` to restore and inspect child-agent transcripts, and
`delete(key)` for deletion. Entries are opaque JSON objects and must remain in
append order. A child transcript uses an opaque `subpath` such as
`subagents/agent-<id>`; the key does not include the on-disk `.jsonl`
extension.

When `load()` returns `null` or an empty array for an explicit `resume`, the SDK
falls back to the same local session ID. Store-backed `continue` starts a new
session when `listSessions()` has no restorable session. Unsafe subpaths are
ignored, and missing or empty child transcripts do not prevent restoration of
the main session.

`sessionStoreFlush: "batched"` is the default. `"eager"` starts each append
without waiting for the result boundary. Final append failures are emitted as
non-fatal `system/mirror_error` messages. `loadTimeoutMs` defaults to 60,000 ms.
Session storage cannot be combined with `persistSession: false`, file
checkpointing, custom transports, or the Cloud Agent runtime. Session storage
supports the built-in Process and Worker transports. Use
`importSessionToStore(sessionId, store, options)` to copy an existing local main
transcript, child-agent transcripts, and metadata into a store.

### Production stores

The SDK exports the `SessionStore` interface but does not ship a
production-ready external storage implementation. Implement the interface
against shared storage operated by your application, then validate its
append/load ordering, project isolation, subkey handling, and deletion behavior
with the shared conformance suite under `examples/session-stores/`.

## Custom Tools

You can expose JavaScript or TypeScript functions to Qoder Agent as in-process
SDK MCP servers. This avoids managing a separate MCP subprocess for simple
application-local tools.

```ts
import {
  createSdkMcpServer,
  qodercliAuth,
  query,
  tool,
} from "@qoder-ai/qoder-agent-sdk";
import { z } from "zod";

const server = createSdkMcpServer({
  name: "my-tools",
  version: "1.0.0",
  tools: [
    tool("greet", "Greet a user.", { name: z.string() }, async ({ name }) => ({
      content: [{ type: "text", text: `Hello, ${name}!` }],
    })),
  ],
});

const q = query({
  prompt: "Greet Alice.",
  options: {
    auth: qodercliAuth(),
    cwd: process.cwd(),
    mcpServers: {
      tools: server,
    },
    allowedTools: ["mcp__tools__greet"],
  },
});
```

For stdio, SSE, and Streamable HTTP MCP servers, set `timeout` to the hard
wall-clock limit for each tool call. The value is in milliseconds, progress
notifications do not extend it, and values below 1,000 ms are ignored.

An effective server `timeout` overrides `MCP_TOOL_TIMEOUT`. If neither is set,
the CLI uses 100,000,000 ms (about 27.8 hours). Connection and discovery
requests are independent: they use `MCP_TIMEOUT`, which defaults to 30,000 ms.

```ts
const q = query({
  prompt: "Run the long report.",
  options: {
    auth: qodercliAuth(),
    mcpServers: {
      reports: {
        type: "stdio",
        command: "report-mcp-server",
        timeout: 1_800_000,
      },
    },
    allowedTools: ["mcp__reports__run_report"],
  },
});
```

The CLI also protects silent tool calls with an idle timeout: 30 minutes for
stdio and 5 minutes for remote transports. Progress resets the idle timer but
not the hard timeout. Set `QODER_MCP_TOOL_IDLE_TIMEOUT` to adjust the global
idle baseline, or to `0` to disable it; a valid server `timeout` can extend the
idle limit up to the hard timeout. In-process servers returned by
`createSdkMcpServer()` do not expose a server-level `timeout` field and do not
use the idle watchdog.

## Hooks

Hooks are deterministic callbacks invoked at specific points in the agent loop.
They are useful for validation, policy checks, logging, and
application-specific feedback.

```ts
import { qodercliAuth, query } from "@qoder-ai/qoder-agent-sdk";
import type { HookCallback } from "@qoder-ai/qoder-agent-sdk";

const blockDeployScript: HookCallback = async (input) => {
  if (input.hook_event_name !== "PreToolUse") {
    return {};
  }

  if (input.tool_name !== "Bash") {
    return {};
  }

  const toolInput = input.tool_input as { command?: unknown } | undefined;
  const command = String(toolInput?.command ?? "");
  if (command.includes("./deploy.sh")) {
    return {
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Deployment scripts require review.",
      },
    };
  }

  return {};
};

const q = query({
  prompt: "Review this project without deploying it.",
  options: {
    auth: qodercliAuth(),
    cwd: process.cwd(),
    hooks: {
      PreToolUse: [{ matcher: "Bash", hooks: [blockDeployScript] }],
    },
  },
});
```

## Error Handling

```ts
import {
  AbortError,
  ModelPolicyTimeoutError,
  ProtocolVersionMismatchError,
  qodercliAuth,
  query,
} from "@qoder-ai/qoder-agent-sdk";

function errorCode(error: unknown): string | undefined {
  if (typeof error === "object" && error !== null && "code" in error) {
    const code = (error as { code?: unknown }).code;
    return typeof code === "string" ? code : undefined;
  }

  return undefined;
}

try {
  const q = query({
    prompt: "Hello Qoder",
    options: {
      auth: qodercliAuth(),
      cwd: process.cwd(),
    },
  });

  for await (const message of q) {
    console.log(message);
  }
} catch (error) {
  if (errorCode(error) === "auth_not_configured") {
    console.error("Configure options.auth before calling query().");
  } else if (error instanceof ProtocolVersionMismatchError) {
    console.error("Upgrade qodercli or @qoder-ai/qoder-agent-sdk.");
  } else if (error instanceof ModelPolicyTimeoutError) {
    console.error("Model policy resolution timed out.");
  } else if (error instanceof AbortError) {
    console.error("The query was aborted.");
  } else {
    console.error(error);
  }
}
```

## License and Terms

Copyright (c) 2026 Qoder

Use of this software is governed by the Qoder Product Service Terms:

https://qoder.com/product-service

By installing or using this package, you agree to those terms.
