# SDK reference

The full @kortix/sdk API surface — client methods, modules, turns, and distribution.

Canonical page: https://kortix.com/docs/sdk/reference

This page is the full `@kortix/sdk` API surface: every client method, the
framework-free modules, the turns helpers, and how the package ships. Use
[SDK](/docs/sdk) to get started and [Sessions](/docs/sdk/sessions) for the
session lifecycle in depth.

## The client

`createKortix(config)` returns one client. Every method is a typed call to
the platform API. The `project(id)` and `session(pid, sid)` handles bind ids
so you never repeat them.

```ts
const kortix = createKortix({ backendUrl, getToken });

kortix.accounts; // account / team operations
kortix.accountInvites; // invite accept/decline by token alone
kortix.projects; // top-level project operations
kortix.project(id); // id-bound project handle
kortix.session(pid, sid); // id-bound session handle → see Sessions
kortix.github; // GitHub App install + repo linking
kortix.billing; // credits, subscription, tier, transactions
kortix.sandboxShares; // public share links for a sandbox port
kortix.transcribe; // speech-to-text
kortix.connectStatus; // easy-connect (Pipedream) status
kortix.marketplace; // public marketplace catalog
kortix.validateToken; // pasted-API-key check
kortix.config; // platform config in effect
kortix.runtime(); // OpenCode client for the active runtime — throws on a @kortix/sdk/server scoped client
```

### Accounts — `kortix.accounts`

| method | what |
| --- | --- |
| `list()` · `get(accountId)` | accounts you belong to · one account |
| `create({ name })` · `updateName(accountId, name)` | create · rename an account |
| `members(accountId)` · `invite(accountId, input)` | list members · invite one |
| `updateMemberRole(accountId, userId, role)` · `removeMember(accountId, userId)` | change a role · remove a member |
| `invites(accountId)` | pending invites |
| `cancelInvite(accountId, inviteId)` · `resendInvite(accountId, inviteId)` | cancel · resend a pending invite |
| `leave(accountId)` | leave the account |

`accounts.tokens` mints account-scoped API keys (`kortix_pat_...`). See
[SDK auth](/docs/sdk/auth) for the full token model.

| method | what |
| --- | --- |
| `tokens.list(accountId?)` | list your API keys |
| `tokens.create(input)` | mint one — `{ accountId?, name, expiresAt?, projectId? }` |
| `tokens.revoke(tokenId, accountId?)` | revoke one |

`accounts.audit` is the enterprise audit log.

| method | what |
| --- | --- |
| `audit.log(...)` · `audit.export(...)` · `audit.webhooks.list/create/update/remove(...)` | audit events · CSV/JSONL export · manage SIEM webhooks |

### Account invites — `kortix.accountInvites`

Reached by invite token alone — the invitee may not be a member yet.

| method | what |
| --- | --- |
| `describe(inviteId)` · `accept(inviteId)` · `decline(inviteId)` | preview · accept · decline an invite |

### Projects — `kortix.projects`

| method | what |
| --- | --- |
| `list()` · `listForAccount(accountId)` | your projects · projects in an account |
| `get(id)` · `detail(id)` | summary · full detail |
| `create(input)` · `createRepo(input)` | from an existing `repo_url` · new empty GitHub repo |
| `provision(input)` | new project on a new Kortix-managed repo, seeded with a starter template — `{ name, account_id?, seed_starter?, starter_template?, marketplace_items?, source_item_id? }` |
| `update(id, input)` · `archive(id)` | update settings · archive |
| `llmCatalog(id)` · `modelPicker(id)` | full · compact model catalog for a selector |
| `sandboxTemplates(id)` · `sandboxHealth(id)` | sandbox build templates · build health |
| `sessions(id)` · `createSession(id, input?)` | list visible sessions · create a session |

`provision` creates a new project; it does not start an existing project's
sandbox. Start a session instead — see [Sessions](/docs/sdk/sessions).

`project(id).sessions.list({ scope: 'project' })` is a manager-only inventory:
private, unavailable, and soft-deleted sessions, with ownership and
runtime-state metadata.

### GitHub — `kortix.github`

Account-scoped GitHub App install and repo linking, not project-scoped.

| method | what |
| --- | --- |
| `getInstallation(accountId)` · `listInstallations(accountId)` | this account's install · installs the user can reach |
| `saveInstallation(input)` · `deleteInstallation(accountId, installationId?)` | record · unlink an install |
| `listRepositories(accountId, installationId?)` · `listRepositoryBranches(...)` | repos the install can see · branches and the GitHub default |
| `linkRepository(input)` | import a repo as a project |

### Billing — `kortix.billing`

Reads for credits, subscription, tier, and transaction history. Checkout,
the customer portal, and credit purchases are Stripe flows, app-owned.

| method | what |
| --- | --- |
| `accountState(accountId?)` · `accountStateMinimal(accountId?)` | full · minimal billing state |
| `transactions(params?)` · `transactionsSummary(params?)` | history · summarized totals |
| `creditBreakdown(accountId?)` · `usageHistory(params?)` | credit balance by source · usage over time |
| `tierConfigurations()` | available plan tiers |
| `checkout.createSession(input)` · `checkout.confirmSession(sessionId, accountId?)` | start · confirm a Stripe Checkout session |
| `subscription.createPortalSession(...)` · `subscription.cancel(...)` · `subscription.reactivate(...)` | open the customer portal · cancel · reactivate |
| `subscription.scheduleDowngrade(...)` · `cancelScheduledChange(...)` · `prorationPreview(...)` | schedule · cancel · preview a plan change |
| `credits.purchase(input)` · `credits.autoTopupSettings(...)` · `credits.configureAutoTopup(...)` | one-off purchase · read · configure auto-topup |

### Sandbox shares — `kortix.sandboxShares`

Public share links for one exposed sandbox port. Sandbox-scoped, not
project-scoped.

| method | what |
| --- | --- |
| `list(sandboxId)` | active share links |
| `create(input)` | create one — `{ sandboxId, port, ttl?, label? }` |
| `revoke(sandboxId, token)` | revoke one |

### Marketplace catalog — `kortix.marketplace`

Public catalog browsing, read-only — distinct from `project(id).marketplace`,
which installs an item onto a project.

| method | what |
| --- | --- |
| `items(options?)` · `item(id)` · `itemFile(id, path)` | browse · one item · a file inside an item |
| `marketplaces()` · `featured()` | all · featured marketplaces |
| `sources.list()` · `sources.add(input)` · `sources.remove(id)` | list · add · remove a source |

### The project handle — `kortix.project(id)`

Binds the project id; every sub-resource hangs off it.

```ts
const p = kortix.project(projectId);
await p.detail();
await p.update({ name });
await p.llmCatalog();
```

| method | what |
| --- | --- |
| `get` · `detail` · `update` · `archive` | read · full detail · update · archive |
| `llmCatalog` · `modelPicker` · `sandboxHealth` | model and sandbox-build reads |
| `onboardingComplete` | mark project onboarding done |
| `validateManifest(raw)` | validate a `kortix.yaml` (or legacy `kortix.toml`) manifest server-side |
| `gitToken()` | mint a fresh scoped git push token (`409` for a bring-your-own repo) |
| `setAgentScope(agentName, scope)` | bind an agent's allowed secrets and connectors |

#### `p.tokens` — project-scoped API keys

Auto-minted at session create as `KORTIX_TOKEN`; can also be minted by hand.

| method | what |
| --- | --- |
| `list()` | project API keys |
| `create(input?)` | mint a new one |
| `revoke(tokenId)` | revoke one |

#### `p.setupLinks` — agent-minted setup links

A link a person opens to enter a secret or connect an app, without full
project access.

| method | what |
| --- | --- |
| `requestSecret(input)` · `requestConnector(input)` | link to collect a secret · connect an app |

#### `p.secrets` — project secrets

| method | what |
| --- | --- |
| `list()` · `upsert({ name, value })` | all project secrets · create or update one |
| `remove(name)` | delete a secret |
| `setPersonal(name, value)` · `removePersonal(name)` | set · remove a per-user override |
| `setGitCredential(input)` | set a git auth credential |

#### `p.access` — members, invites, requests

| method | what |
| --- | --- |
| `list()` · `invite(email, role)` | members with access · invite a user |
| `update(userId, role)` · `revoke(userId)` | change a role · remove access |
| `pendingInvites()` · `requests()` | outstanding invites · pending access requests |
| `resendInvite(inviteId)` · `revokeInvite(inviteId)` · `approveRequest(id)` · `rejectRequest(id)` | resend/revoke an invite · approve/reject a request |
| `groupGrants()` · `attachGroupGrant(...)` · `updateGroupGrant(...)` · `detachGroupGrant(...)` | list, grant, change, or remove an IAM group grant |

`p.access.resourceGrants` grants a member or group access to one resource (an
agent, a skill, a secret) instead of the whole project.

| method | what |
| --- | --- |
| `resourceGrants.list()` | per-resource grants |
| `resourceGrants.create(input)` | grant a member or group access to a resource |
| `resourceGrants.remove(grantId)` | revoke a resource grant |

#### `p.connectors` — tool connectors

| method | what |
| --- | --- |
| `list()` · `config(connectorId)` | configured connectors · one connector's config |
| `create(input)` | add a connector |
| `auth.discover(input)` | preview auth from an OpenAPI spec, Postman collection, or endpoint |
| `remove(connectorId)` · `sync()` | delete a connector · re-sync connectors |
| `setName(connectorId, name)` · `setSensitive(connectorId, sensitive)` | rename · mark it sensitive (extra approval gating) |
| `setCredentialMode(connectorId, mode)` · `setCredential(connectorId, input)` | switch source · set the credential value |
| `policies.get(connectorId)` · `policies.set(connectorId, policies)` | read · replace its tool policies |
| `profiles.list()` · `profiles.reconcile(input)` | server-side identities (manager-only) · create/update one |
| `profiles.updateCredential(profileId, input)` | rotate a profile's credential |
| `profiles.revoke(profileId)` · `profiles.activate(profileId)` | deny · restore the identity live |

`p.connectors.discover` browses the direct-integration catalog. It is **experimental** and off by default — enable it per project under Customize → Settings → Experimental → "Connectors API Discover". Easy Connect (Pipedream) remains the default connector marketplace.

| method | what |
| --- | --- |
| `discover.list(query?, cursor?)` · `discover.detail(id)` | search OpenAPI/MCP/GraphQL/CLI entries · one entry's detail |

`p.connectors.pipedream` is the optional managed-OAuth path; `listApps`
returns OAuth apps only. Connect API-key apps directly instead.

| method | what |
| --- | --- |
| `pipedream.listApps(params?)` · `pipedream.connect(input)` · `pipedream.finalize(input)` | browse the app catalog · start a connect flow · finalize it |

A connector is how a session reaches an external tool; `profiles` select the
identity it uses (`connector_bindings: { alias: { profile_id } }` at session
create). Credentials stay encrypted and resolve per request.

#### `p.policies` — project policies

| method | what |
| --- | --- |
| `list()` · `set(policies)` | the project's policies · replace the set |

#### `p.triggers` — cron and webhook automations

A trigger starts an agent action on a schedule or an inbound webhook. See
[Triggers](/docs/connect/triggers) for session strategy and payload
templating.

| method | what |
| --- | --- |
| `list()` | all triggers |
| `create(input)` | create one |
| `update(triggerId, input)` | edit a trigger |
| `remove(triggerId)` | delete a trigger |
| `fire(triggerId)` | run it now |
| `setActivation(paused)` | pause or resume every trigger on the project |

`create(input)` takes `{ name, type: 'cron' | 'webhook', prompt_template,
slug?, agent?, model?, enabled?, session_mode?, session_id?, cron?, run_at?,
timezone?, secret_env? }`. `name` and `prompt_template` are required.
`cron`/`run_at` are mutually exclusive (`type: 'cron'`); `secret_env` (the
webhook HMAC secret) applies to `type: 'webhook'`.

#### `p.marketplace` / `p.registry` — installed items

Installs a catalog item's files onto the project's default branch.
`registry.*` is an identical alias of `marketplace.*`.

| method | what |
| --- | --- |
| `marketplace.list()` · `marketplace.install(id)` | installed items · install a catalog item |
| `marketplace.updates()` · `marketplace.update(name)` · `marketplace.updateAll()` | available updates · update one · update all |
| `marketplace.remove(name)` | uninstall an item |

#### `p.files` — repo files (read)

Read-only access to the project's git tree. To read and write files inside a
running session, use the session's file operations — see
[Files](#files) under Modules below.

| method | what |
| --- | --- |
| `list(options?)` · `read(path, ref?)` | the repo tree · a file's contents at a git ref |
| `search(query)` | search the repo |
| `archive(options?)` · `history(path)` | download a tarball · a file's git history |

#### `p.git` — history

| method | what |
| --- | --- |
| `commits()` | the commit log |
| `commit(sha)` · `commitDiff(sha)` | one commit · its diff |
| `branches()` · `versionDiff(from, to)` | branches · diff between two refs |

#### `p.changeRequests` — lifecycle and merge

A change request (CR) is how a session's work merges into the default branch.

| method | what |
| --- | --- |
| `list()` · `get(crId)` | open CRs · one CR |
| `diff(crId)` · `mergePreview(crId)` | its diff · preview the merge result |
| `open(input)` · `merge(crId, input?)` | open · merge a CR |
| `close(crId, input?)` · `reopen(crId, input?)` | close without merging · reopen a closed one |
| `requestChanges(crId, input)` | record feedback, optionally delivered back to the originating session |

#### `p.sessions` — and the session handle

| method | what |
| --- | --- |
| `list()` | the project's sessions |
| `create(input?)` | create a session |
| `session(sid)` | the session handle (same as `kortix.session(id, sid)`) |

The session handle is the heart of the runtime — see
[Sessions](/docs/sdk/sessions).

#### `p.approvals` — the executor approval inbox

Pending executor-gated actions awaiting a decision — backs the
permission-approval UX (`APPROVE` / `ASK` / `BLOCK`).

| method | what |
| --- | --- |
| `list(options?)` · `sessionsNeedingInput(options?)` | pending approvals · sessions blocked on a decision |
| `resolve(executionId, decision, scope?)` | approve or deny one — `decision: 'approve' \| 'deny'`, `scope: 'once' \| 'session' \| 'session_all'` |

#### `p.gateway` — LLM observability

Request logs, cost/latency rollups, budgets, and gateway API keys for this
project's model traffic.

| method | what |
| --- | --- |
| `logs(opts?)` · `log(logId)` | request log entries · one log entry |
| `overview(days?)` · `series(days?)` · `breakdown(days?)` · `sessions(days?)` · `errors(days?)` | rollups, per-session cost, and errors over a window |
| `budgets()` · `setBudget(input)` · `deleteBudget(budgetId)` | read · create/edit · remove a budget |
| `keys()` · `createKey(name)` · `revokeKey(keyId)` | list · mint · revoke a gateway API key |
| `playground(prompt, models)` | run one prompt against up to 6 models |
| `routing.get()` · `routing.set(policy)` · `routing.reset()` | read · replace · inherit the routing policy |
| `routing.preview(input)` | resolve a route without invoking a model |

A routing policy holds a default model, a vision model, and an ordered
fallback chain, each model attempted at most once; `fallbackOn` is
`transient` or `any-error`.

#### `p.channels` — Slack / email / Meet

Integration surfaces that let an agent act as a Slack app, an email address,
or a meeting bot.

| method | what |
| --- | --- |
| `slack.installation()` · `slack.mode()` · `slack.manifest()` | current install · mode · app manifest |
| `slack.connect(input)` · `slack.disconnect()` | connect · disconnect |
| `slack.getFile(url)` · `slack.uploadFile(input)` | download · upload a file via the server proxy |
| `email.installation(connectorSlug?)` · `email.mode()` | current install · mode |
| `email.connect(input)` · `email.disconnect(...)` · `email.updatePolicy(input)` | connect · disconnect · update the send/reply policy |
| `meet.voices()` · `meet.setVoice(voice)` · `meet.previewVoice(voiceId)` | list · set · preview a bot voice |
| `meet.setBotName(name)` · `meet.speak(botId, text, voice?)` | rename the bot · make it speak in a live meeting |

#### `p.modelDefaults` — default model preferences

Account, agent, and project-scoped model defaults, resolved by the gateway.

| method | what |
| --- | --- |
| `get()` · `set(input)` · `clear(params)` | read · set a default · clear an override |

#### `p.setDefaultAgent` — project default agent

`p.setDefaultAgent(agentName)` checks that the agent is declared and enabled,
then sets it as `default_agent` in the project's `kortix.yaml`. New sessions
prefer this agent unless a user picks another one.

#### `p.updateExperimentalFeature` — feature flags

`p.updateExperimentalFeature(feature, enabled)` toggles an experimental
feature for the project. Pass `enabled: null` to clear the override.

#### `p.sandbox` — templates and snapshot builds

Sandbox build config beyond `sandboxHealth`/`sandboxTemplates` on the project
handle: Dockerfile/image/warm-pool templates and their snapshot builds.

| method | what |
| --- | --- |
| `list()` · `snapshots()` | sandboxes for this project · built snapshots |
| `rebuildSnapshot(slug?)` · `fixWithAgent()` | rebuild a snapshot · ask an agent to fix a broken build |
| `createTemplate(input)` · `updateTemplate(...)` · `removeTemplate(...)` · `buildTemplate(...)` | add · edit · delete · build a template |
| `setProvider(provider)` | request a provider switch. `null` (or the platform default / the already-active provider) applies immediately; switching to a different enabled provider starts a durable prepare→verify→activate transition — the current provider keeps serving while the target warm image is built and verified, then activated. The return is a tagged union: `kind:'project'` (immediate) or `kind:'preparation'` (poll `getProjectSandboxProviderTransition()` until `activated`/`failed`) |

### Escape hatch

`kortix.runtime()` returns the typed OpenCode client for the active runtime.
On a client created by `createScopedKortix` (`@kortix/sdk/server`), it **throws**
— the process-global "active" runtime is another request's sandbox in a
multi-tenant server, a cross-tenant leak. Use the session-scoped
`kortix.session(pid, sid).runtime` (call `ensureReady()` first) instead, which
resolves that session's own sandbox.

## Modules

The framework-free modules behind [the client](#the-client) facade and the
React hooks. Reach for them when you need one operation without the facade,
a pure helper, or a Node-only isolation layer. Each module carries a
stability tier so you know what to build on.

| Tier | Meaning |
| --- | --- |
| Canonical | Import from the root `@kortix/sdk`. Use this for all new code. |
| Supported | A dedicated subpath (`@kortix/sdk/react`, `@kortix/sdk/server`). First-class, not deprecated. |
| Deprecated alias | An old subpath that still works. It re-exports code the root already exports. Import from root instead. |
| Internal | Outside semver. Do not import this in host code. |

The root entry is canonical. Every framework-free name below is importable
straight from `@kortix/sdk`:

```ts
import { files, getSessionHealth, getClient, authenticatedFetch, backendApi } from '@kortix/sdk';
```

### Canonical modules

| Module | What it does |
| --- | --- |
| Files | Workspace file operations: list, read, search, write |
| Session runtime | Health probe and preview/proxy URL builders |
| OpenCode client | The typed OpenCode v2 client and its full type surface |
| Auth | `authenticatedFetch` and token accessors |
| Projects REST | The raw REST functions the facade wraps |
| API client | `backendApi`, the low-level typed HTTP client |
| Turns | Message-to-turn grouping, cost, and status math — see [Turns](#turns) |
| Transcripts | `formatTranscript`, a client-side Markdown export |

#### Files

```ts
import { files } from '@kortix/sdk';

const tree = await files.list('/workspace/src');
const { content } = await files.read('/workspace/README.md');
const hits = await files.findText('TODO');
await files.upload(file, '/workspace/uploads');
```

`files` targets the globally active sandbox. If your host runs more than one
session at a time, call `s.files` on the session handle instead. It always
targets that session's own sandbox. See [Sessions](/docs/sdk/sessions).

#### Session runtime helpers

```ts
import { getSessionHealth, isRuntimeReady } from '@kortix/sdk';

const result = await getSessionHealth();
if (result.ok && isRuntimeReady(result.health)) {
  // the runtime is up and OpenCode is ready
}
```

`getSessionHealth` never throws on a non-2xx status. It returns
`{ status, ok, health, body }` and lets you decide what a status means. The
same module exports the URL helpers that rewrite an agent's `localhost` output
into a reachable proxy URL: `detectLocalhostUrls`, `rewriteLocalhostUrl`,
`proxyLocalhostUrl`, `parseLocalhostUrl`, and `buildWebProxyUrl`.

#### OpenCode client

```ts
import { getClient } from '@kortix/sdk';

const client = getClient();
const { data } = await client.session.list({ limit: 100 });
```

`getClient()` returns the typed OpenCode v2 client for the active runtime,
with auth already injected. Prefer `kortix.session(pid, sid).runtime`, the
same client scoped to one session, over the global `getClient()` when your
host runs more than one session.

#### Auth helpers

```ts
import { authenticatedFetch, getAuthToken } from '@kortix/sdk';

const res = await authenticatedFetch(`${runtimeUrl}/kortix/health`);
const token = await getAuthToken();
```

The token comes from the `getToken` function you passed to `createKortix`.
Most app code does not need this module — the file, session, and facade
layers already authenticate for you.

#### API client

```ts
import { backendApi } from '@kortix/sdk';

const data = await backendApi.get('/some/endpoint');
await backendApi.post('/some/endpoint', { name: 'x' });
```

`backendApi` is the typed HTTP client every REST function builds on. Use it
only for an endpoint that has no typed wrapper yet.

### Supported subpaths

| Subpath | What it does |
| --- | --- |
| `@kortix/sdk/react` | React hooks — see [React hooks](/docs/sdk/react) |
| `@kortix/sdk/server` | Request-scoped config for multi-tenant backends |

#### Server-side isolation

`createKortix` stores its config, including the token function, in one
process-wide variable. That is fine for a browser tab, a CLI, or a
single-tenant server. It is unsafe for a Node server that handles concurrent
requests for different users, because the last `createKortix` call wins for
every in-flight request. `@kortix/sdk/server` fixes this with per-request
isolation:

```ts
import { createScopedKortix } from '@kortix/sdk/server';

export async function handler(req: Request) {
  const kortix = createScopedKortix({ backendUrl, getToken: () => tokenFor(req) });
  return kortix.projects.list();
}
```

`createScopedKortix` and `runWithKortix` isolate config per request with
Node's `AsyncLocalStorage`. Never import `@kortix/sdk/server` from a browser
bundle — it statically pulls in `node:async_hooks`.

A scoped client's top-level `runtime()` throws (it would resolve another
tenant's sandbox). Reach a specific session's runtime via
`kortix.session(pid, sid).runtime` after `await s.ensureReady()`.

### Deprecated aliases

About twenty old subpaths still work: `/files`, `/turns`, `/session`, `/auth`,
`/projects-client`, `/api-client`, `/config`, `/event-stream`,
`/opencode-client`, `/platform-client`, and more. Each one re-exports code the
root `@kortix/sdk` entry already exports. They stay working so no existing
```ts
import { classifyPart, classifyTurn, toolInfo, toolViewModel } from '@kortix/sdk';
```
```ts
import { classifyPart, type ClassifiedPart } from '@kortix/sdk';

for (const part of message.parts) {
  const classified: ClassifiedPart = classifyPart(part);
  switch (classified.kind) {
    case 'text':
      render(classified.text);
      break;
    case 'tool':
      render(classified.tool.title, classified.tool.status);
      break;
  }
}
```
```ts
interface ToolView {
  name: string;
  title: string;
  status: 'pending' | 'running' | 'done' | 'error';
  input?: Record<string, unknown>;
  output?: string;
  error?: string;
  outputParsed?: unknown; // JSON.parse(output) when it parses, capped at 256KB
  outputText?: string;    // the raw output text, always present
}
```
```ts
toolInfo('bash'); // { label: 'Shell', category: 'shell' }
getToolInfo('write', { filePath: '/workspace/main.go' });
// { icon: 'file-pen', title: 'Write', subtitle: 'main.go /workspace' }
```
```ts
const vm = toolViewModel(classifiedTool);
if (vm.kind === 'shell') {
  render(vm.command, vm.stdout, vm.exitCode);
}
```
```ts
import { groupMessagesIntoTurns, collectTurnParts, type TurnLike } from '@kortix/sdk';

const turns: TurnLike[] = groupMessagesIntoTurns(messages);
for (const turn of turns) {
  const parts = collectTurnParts(turn);
}
```
```ts
import { isTextPart, isToolPart, getPartText } from '@kortix/sdk';

if (isTextPart(part)) {
  // part.type narrowed to 'text'
}
const text = getPartText(part); // works for 'text' and 'reasoning' parts
```
```ts
import { getWorkingState, getTurnStatus, formatDuration } from '@kortix/sdk';

const status = getTurnStatus(parts, childMessages); // "Running commands..."
formatDuration(4300); // "4s" — durations under 1s return ''
```
```ts
import { getTurnError, getChildSessionError, unwrapError } from '@kortix/sdk';

getTurnError(turn);                    // the first assistant error, unwrapped
getChildSessionError(childMessages);   // newest error in a sub-agent's messages
unwrapError(rawError);                 // normalizes double-JSON and mixed error shapes
```
```ts
import { getTurnCost, getSessionCost, formatCost, formatTokens, COST_MARKUP } from '@kortix/sdk';

const info = getTurnCost(partsWithMessage, modelPricingLookup);
const sessionCost = getSessionCost(messages, modelPricingLookup);

formatCost(0.0032);  // "$0.003"
formatTokens(12345); // "12k"
```
```ts
import { getChildSessionId, getChildSessionToolParts } from '@kortix/sdk';

const childId = getChildSessionId(taskToolPart);
const steps = getChildSessionToolParts(childMessages);
```
```ts
import { getPermissionForTool, getHiddenToolParts, isToolPartHidden } from '@kortix/sdk';

const permission = getPermissionForTool(permissions, callID);
const hidden = getHiddenToolParts(activePermission, activeQuestion);
```
```ts
import { getFilename, getDirectory, relativizePath, stripAnsi } from '@kortix/sdk';

getFilename('/workspace/src/main.go');               // "main.go"
getDirectory('/workspace/src/main.go');               // "/workspace/src"
relativizePath('/workspace/src/main.go', '/workspace'); // "src/main.go"
```
```ts
import { sortSessions, childMapByParent, allDescendantIds } from '@kortix/sdk';

sessions.sort(sortSessions(Date.now())); // pins sessions updated in the last 60s
const childMap = childMapByParent(sessions);
const descendants = allDescendantIds(childMap, sessionId);
```
```sh
npm install @kortix/sdk
```
```ts
import { createKortix } from '@kortix/sdk'; // framework-free core
import { useSession } from '@kortix/sdk/react'; // optional React layer
import { createScopedKortix } from '@kortix/sdk/server'; // Node and Bun servers
```
```html
<script src="https://unpkg.com/@kortix/sdk/dist/kortix.global.js"></script>
<script>
  const kortix = Kortix.createKortix({
    backendUrl: 'https://api.kortix.com/v1',
    getToken: async () => KEY,
  });
</script>
```
