# Client

Drive a running nikcli server from your own program with the generated, typed HTTP client.

## Overview

`@nikcli-ai/sdk` wraps the server's HTTP API in a typed, namespaced client. It is generated from the same OpenAPI document as the [API reference](/docs/api) by `@nikcli-ai/httpapi-codegen`, so every endpoint is a method and a contract change breaks generation rather than becoming a runtime 404.

This is the path the web app, the desktop app and the mobile app already take. Nothing about them is privileged — they call the endpoints you can call.

Terminal

bash 

```
npm install @nikcli-ai/sdk
```

$ shell · paste into your terminal

You also need a server to talk to:

Terminal

bash 

```
nikcli serve --hostname 127.0.0.1 --port 4096
```

$ shell · paste into your terminal

## Create a client

ts 

1

2

3

4

5

6

7

```
import { createNikcliClient } from "@nikcli-ai/sdk/httpapi"

const client = createNikcliClient({
  baseUrl: "http://127.0.0.1:4096",
  // binds every call to one project instance
  directory: "/Users/me/project",
})
```

| Option | Type | Effect |
| --- | --- | --- |
| `baseUrl` | `string` | Required. Where the server is listening. |
| `directory` | `string` | Sent as `x-nikcli-directory` on every call. Non-ASCII paths are percent-encoded for you. |
| `workspace` | `string` | Sent as `x-nikcli-workspace` on every call. |
| `headers` | `HeadersInit` | Merged into every request — this is where an `Authorization` header goes. |
| `signal` | `AbortSignal` | Applied to every request, combined with any per-call signal. |
| `fetch` | `typeof fetch` | Swap the transport — a proxy, a mock, or an in-memory handler. |
| `throwOnError` | `boolean` | Reject on failure instead of resolving with `error`. See below. |

## The result envelope

By default calls **resolve** with an envelope rather than rejecting. Transport failures and declared HTTP errors both arrive as `error`, so a failed call in a UI event handler cannot become an unhandled rejection.

ts 

1

2

3

4

5

6

```
const { data, error } = await client.session.list()
// guard on `data`, not on `error`: `error` is `unknown`, so testing it
// does not narrow `data` away from `undefined`
if (!data) throw error

data.forEach((session) => console.log(session.id, session.title))
```

In a script, where an unhandled rejection is exactly what you want, opt back into throwing — per call or once for the whole client:

ts 

1

2

3

4

5

6

7

```
const client = createNikcliClient({ baseUrl, throwOnError: true })

// rejects on failure, so `data` is always present at runtime
const { data } = await client.session.list()

// or per call, on a client that does not throw by default
const one = await client.session.get({ sessionID }, { throwOnError: true })
```

throwOnError does not change the type

It changes runtime behaviour only. The call still returns `{ data, error }` and `data` is still typed as possibly `undefined`, so you keep destructuring it — with `data!`, or a guard, or a one-line `unwrap` helper of your own.

## Choosing an instance

One server hosts many projects. The client's `directory` is the default for every call, and any call can override it — useful when one process drives several checkouts.

ts 

1

2

3

4

5

6

7

8

```
// the client default
await client.session.list()

// inline, for one call
await client.session.list({ directory: "/Users/me/other-project" })

// a workspace within the instance
await client.session.list({ workspace: workspaceID })
```

The server resolves this exactly as described in [Choosing an instance](/docs/api#instance): the header the client sets is the same one `curl` would send.

## Method names

Methods are namespaced by resource and follow the operation ids listed throughout the [API reference](/docs/api): `session.prompt` is `client.session.prompt`, `mobile.git.commit` is `client.mobile.git.commit`. A handful are renamed where the dotted id would collide with a JavaScript keyword or an existing namespace — `session.messageRemove` is `client.session.messageDelete`, for instance — so let the types lead rather than transcribing ids by hand.

ts 

1

2

3

4

5

6

7

```
await client.session.create({ title: "triage" })
await client.session.messages({ sessionID })
await client.file.read({ path: "src/index.ts" })
await client.find.text({ pattern: "TODO" })
await client.project.current()
await client.config.get()
await client.doctor.run()
```

## Running a session

The common shape: create a session, send a prompt, read what came back. Everything else — sharing, forking, reverting, todos, diffs — hangs off the same session id.

triage.ts

ts 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

```
import { createNikcliClient } from "@nikcli-ai/sdk/httpapi"

const client = createNikcliClient({
  baseUrl: "http://127.0.0.1:4096",
  directory: process.cwd(),
  throwOnError: true,
})

const { data: session } = await client.session.create({ title: "triage" })

const { data: reply } = await client.session.prompt({
  sessionID: session!.id,
  parts: [{ type: "text", text: "Which tests are currently failing, and why?" }],
})

for (const part of reply!.parts) {
  if (part.type === "text") console.log(part.text)
}

const { data: diff } = await client.session.diff({ sessionID: session!.id })
console.log(diff)
```

`session.prompt` waits for the assistant to finish. For fire-and-forget submission — queue the work and watch the event feed instead — use `client.session.promptAsync`.

Pick the model at the session, not the call

`session.prompt` accepts an explicit `model` and `agent`, but leaving them unset inherits the launching session's model, which is usually what you want for background work.

## Streams and events

Streaming endpoints resolve to an object holding an async iterable, so you await the connection and then iterate the feed.

ts 

1

2

3

4

5

6

7

8

```
const { stream } = await client.event.subscribe()

for await (const event of stream) {
  if (event.type === "message.part.updated" && event.properties.part.type === "text") {
    // `delta` is the text added since the last event; `part.text` is cumulative
    process.stdout.write(event.properties.delta ?? "")
  }
}
```

`client.event.subscribe()` is scoped to the resolved instance; `client.global.event()` spans every instance the server hosts, and `client.sync.event.stream()` carries sync traffic. All three are the SSE feeds listed under [Streaming](/docs/api#streaming).

Pair promptAsync with the feed

`session.promptAsync` returns as soon as the work is accepted. Subscribe first, then submit, and you see the assistant's output as it is produced instead of waiting for the whole turn. [Streaming a response](/docs/guides/streaming) walks the whole flow.

## Authentication

On loopback with no password set, nothing is needed. Anything else goes through `headers` — the client does not manage credentials for you.

ts 

1

2

3

4

5

6

7

8

9

10

11

12

```
// bearer token (mobile pairing token or an nku_ account token)
const client = createNikcliClient({
  baseUrl,
  headers: { Authorization: `Bearer ${process.env.NIKCLI_TOKEN}` },
})

// basic auth, for a server started with NIKCLI_SERVER_PASSWORD
const basic = btoa(`nikcli:${process.env.NIKCLI_SERVER_PASSWORD}`)
const client2 = createNikcliClient({
  baseUrl,
  headers: { Authorization: `Basic ${basic}` },
})
```

The full scheme table is in [Authentication](/docs/api#auth).

## Errors

`ClientError` is what a transport or protocol failure resolves to. It folds in its cause, so a server that died mid-request reports why rather than a bare `Transport`.

ts 

1

2

3

4

5

6

7

8

9

10

```
import { ClientError, createNikcliClient } from "@nikcli-ai/sdk/httpapi"

const { data, error, response } = await client.session.get({ sessionID })

if (error instanceof ClientError) {
  console.error(error.reason, error.message, error.cause)
} else if (error) {
  // a declared HTTP error — the schema named on the endpoint in the API reference
  console.error(response?.status, error)
}
```

Declared failures are typed on the endpoint, so a `ConfigUpdateError` or a `ProviderAuthError` arrives as that shape rather than collapsing into a generic 500. The endpoint pages list which errors each operation can return.

## Other languages

There is no official client beyond JavaScript, but the contract is public. Point any OpenAPI generator at the document and you get the same surface.

Terminal

bash 

```
curl -s https://nikcli-ai.dev/docs/api/openapi.json -o nikcli-openapi.json
```

$ shell · paste into your terminal

Operation ids are stable and dotted (`session.prompt`, `experimental.workspace.warp`), so most generators produce sensibly grouped method names without extra configuration.

## Related

-   [Build The three ways to build on nikcli, and which to pick.](/docs/build)
-   [API Reference Every endpoint this client wraps.](/docs/api)
-   [Embedded SDK The same surface, in-process, with no listener.](/docs/build/sdk)
-   [Server & API What happens to a request after it leaves the client.](/docs/server-api)

---

Source: https://nikcli-ai.dev/docs/build/client
