Skip to content
nikcli
Docs
Documentation

Using the API

Start a server and drive it — with curl and the typed client side by side, one task at a time.

Start a server

There is no hosted API. nikcli runs on your machine and the API is how everything else talks to it — the web app, the desktop app and the mobile app are all clients of these endpoints.

Terminal
bash
nikcli serve --hostname 127.0.0.1 --port 4096
Read the port it prints

If 4096 is taken the server falls back to an ephemeral port rather than failing. Take the URL from the output instead of hardcoding it.

Your first request

Health is instance-less, so it answers before anything else is configured.

Terminal
bash
curl -s http://127.0.0.1:4096/global/health

Then install the client, which is generated from the same contract this guide describes.

Terminal
bash
npm install @nikcli-ai/sdk
ts
import { createNikcliClient } from "@nikcli-ai/sdk/httpapi"

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

const { data } = await client.global.health()
console.log(data)
Calls resolve with { data, error }

They do not reject by default. Guard on data — not on error, which is typed unknown and so does not narrow. Full explanation in the result envelope.

Authenticate

On loopback with no password set, nothing is required. Once the server is reachable from elsewhere, it is.

Terminal
bash
NIKCLI_SERVER_PASSWORD=secret nikcli serve --hostname 0.0.0.0 --port 4096

curl -s -u nikcli:secret http://127.0.0.1:4096/session
curl -s -H "Authorization: Bearer $NIKCLI_TOKEN" http://127.0.0.1:4096/session

# for clients that cannot set headers, such as EventSource
curl -s "http://127.0.0.1:4096/session?token=$NIKCLI_TOKEN"
ts
const client = createNikcliClient({
  baseUrl: "http://127.0.0.1:4096",
  headers: { Authorization: `Bearer ${process.env.NIKCLI_TOKEN}` },
})

Pick the project

One server hosts many projects, so most endpoints need to know which. Pass directory as a query parameter or as the x-nikcli-directory header — the client sets the header for you from its own directory option.

Terminal
bash
curl -s "http://127.0.0.1:4096/project/current?directory=$PWD"
curl -s http://127.0.0.1:4096/project/current -H "x-nikcli-directory: $PWD"
ts
// the client default
await client.project.current()

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

Create a session and prompt it

A session holds the conversation and the changes made in it. Create one, then send prompts to its id.

curl
bash
SESSION=$(curl -s -X POST "http://127.0.0.1:4096/session?directory=$PWD" \
  -H 'content-type: application/json' \
  -d '{"title":"triage"}' | jq -r .id)

curl -s -X POST "http://127.0.0.1:4096/session/$SESSION/message?directory=$PWD" \
  -H 'content-type: application/json' \
  -d '{"parts":[{"type":"text","text":"Which tests are failing, and why?"}]}'
SDK
ts
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 failing, and why?" }],
})

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

session.prompt waits for the whole turn. To get output as it is produced, use promptAsync and the event feed — see Streaming a response.

parts, not a string

A prompt is an array of parts, so one message can mix text with file attachments, an agent reference or a subtask. { type: "text", text } is the common case.

Read what changed

Sessions edit files. Ask the session what it changed rather than diffing the working tree yourself — this scopes the answer to that session's work.

Terminal
bash
curl -s "http://127.0.0.1:4096/session/$SESSION/diff?directory=$PWD"
curl -s "http://127.0.0.1:4096/session/$SESSION/message?directory=$PWD"
ts
const { data: diff } = await client.session.diff({ sessionID: session!.id })
const { data: messages } = await client.session.messages({ sessionID: session!.id, limit: 20 })

Read and write files

File access goes through the instance, so paths are resolved inside the selected project rather than against the server's own working directory.

Terminal
bash
curl -s "http://127.0.0.1:4096/file/content?path=src/index.ts&directory=$PWD"

curl -s -X PUT "http://127.0.0.1:4096/file/content?directory=$PWD" \
  -H 'content-type: application/json' \
  -d '{"path":"NOTES.md","content":"# Notes\n"}'

curl -s "http://127.0.0.1:4096/file/status?directory=$PWD"
ts
const { data: file } = await client.file.read({ path: "src/index.ts" })
await client.file.write({ path: "NOTES.md", content: "# Notes\n" })
const { data: status } = await client.file.status()

Run a shell command

Running a command inside a session records it in the transcript, so the agent sees the result on the next turn. That is usually what you want over shelling out yourself.

ts
const { data: result } = await client.session.shell({
  sessionID: session!.id,
  agent: "build",
  command: "bun test",
})
agent is required here

session.shell takes an agent, because the command runs under that agent's permissions rather than unconditionally.

Answer a permission request

If the agent asks for approval and nothing answers, the turn waits. An automated client has to decide — which is exactly why the endpoints exist.

ts
const { data: pending } = await client.permission.list()

for (const request of pending ?? []) {
  await client.permission.reply({
    requestID: request.id,
    reply: "once", // "once" | "always" | "reject"
  })
}

Watching permission.asked on the event feed is the reactive version of the same thing, and is how the TUI and the mobile app do it.

A complete script

Putting it together: open a session, ask it to do something, approve whatever it asks for, and print the resulting diff.

review.ts
ts
import { createNikcliClient } from "@nikcli-ai/sdk/httpapi"

const client = createNikcliClient({
  baseUrl: process.env.NIKCLI_URL ?? "http://127.0.0.1:4096",
  directory: process.cwd(),
  headers: process.env.NIKCLI_TOKEN
    ? { Authorization: `Bearer ${process.env.NIKCLI_TOKEN}` }
    : undefined,
})

const { data: session, error } = await client.session.create({ title: "automated review" })
if (!session) throw error

// approve anything this session asks for while it works
const approve = setInterval(async () => {
  const { data: pending } = await client.permission.list()
  for (const request of pending ?? []) {
    await client.permission.reply({ requestID: request.id, reply: "once" })
  }
}, 1000)

try {
  const { data: reply } = await client.session.prompt({
    sessionID: session.id,
    parts: [{ type: "text", text: "Review the staged changes and fix anything obviously broken." }],
  })

  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)
} finally {
  clearInterval(approve)
}
Approving everything is a choice, not a default

The loop above says yes to every request. That is reasonable in a sandbox or a throwaway CI container, and not reasonable on a machine that holds credentials. Filter on request.permission if the script runs anywhere real.

Where to go next

You wantGo to
To see what these actually returnA guided tour with real responses
Output as it is producedStreaming a response
The exact shape of a request or responseAPI Reference
Client options, errors and method namingClient
To run nikcli in your own processEmbedded SDK
A client in another languageThe OpenAPI document