# Architecture

Source-oriented map of the nikcli monorepo, CLI lifecycle, server stack, session runtime, storage model, integrations, mobile host, and SDK boundary.

## Overview

Forked from OpenCode

nikcli is a fork of [OpenCode](https://github.com/anomalyco/opencode), the open source coding agent. The runtime boundaries described on this page — CLI lifecycle, instance model, server stack, session runtime, tool and permission layers — descend from OpenCode's design, and credit for them belongs to the upstream project and its contributors.

nikcli is a Bun monorepo centered on `packages/nikcli`. The CLI process initializes global paths and logging, resolves project instances, loads config through Effect layers, and then runs one of several front doors: the OpenTUI terminal app, one-shot commands, an HTTP server, workspace hosting, mobile hosting, companion UI, remote control, or automation flows. Most of those surfaces converge on the same session, provider, tool, permission, storage, and event services.

packages/nikcli

### Single runtime core

CLI, TUI, server routes, mobile host, remote control, providers, tools, permissions, and storage live in the main package.

packages/sdk

### Generated client

The web, TUI, mobile, and external clients use the SDK boundary instead of importing server internals.

TUI, HTTP, mobile, companion

### Multiple shells

Different user surfaces route into the same Effect HttpApi server or the same session services.

Runtime state

### Effect services

Config, project instance, sessions, storage, and provider services are assembled through Effect layers and per-instance context.

Read architecture from runtime edges

The fastest way to orient is to start at `packages/nikcli/src/cli-main.ts`, then follow the command into `server/server.ts`, `cli/cmd/tui/thread.ts`, `session/index.ts`, or the command-specific module. Those files reveal which shared services are actually on the hot path.

## Package map

The repository is a Bun workspace. The most important boundary is that `packages/nikcli` owns the executable runtime, while sibling packages provide clients, UI shells, plugins, remote services, and companion surfaces.

packages/nikcli

### Main CLI

Command registry, TUI, server app, session engine, provider registry, tools, permissions, config, storage, connectors, MCP, LSP, mobile host, and remote control.

packages/tui

### Terminal UI

The whole OpenTUI/Solid renderer — app shell, routes, dialogs, contexts, feature plugins. It talks to the runtime only through the SDK.

packages/util

### Shared utilities

`Global` paths, `Flag`, `Log`, filesystem helpers, and the primitives both the runtime and the TUI need.

packages/sdk

### SDK

Generated JavaScript client and schemas for the server API. Regenerate it from the typed HttpApi with `generate:httpapi-clients`.

packages/plugin

### Plugin system

Reusable plugin API surface and package-level plugin tooling.

packages/llm

### LLM client

Route-based multi-provider client — protocols, providers, tool runtime — used for native provider traffic.

computer-use, browser-control, terminal-control

### Automation engines

Sibling daemons with a shared CLI grammar and evidence-bundle format, driven by the `computer` and `browser` tools.

Terminal

bash 

```
bun install
bun run typecheck
bun test
bun run --cwd packages/nikcli dev
bun run --cwd packages/nikcli generate:httpapi-clients
```

$ shell · paste into your terminal

## CLI lifecycle

`packages/nikcli/src/index.ts` is the process entrypoint, and it is deliberately tiny: it installs a document shim and hands off to `src/cli-main.ts`. Keeping the two apart is what lets the compiled binary re-enter itself as a `--browser-control-daemon` / `--computer-use-daemon` host without paying for the whole CLI module graph first.

`cli-main.ts` builds the yargs CLI, installs global error handlers, initializes global paths and logging, sets `AGENT=1` and `NIKCLI=1`, registers commands, and forces process exit to avoid hanging subprocesses.

1.  **Global initialization** creates data, config, cache, state, log, bin, and repo paths.
2.  **Logging setup** respects `--print-logs`, `--log-level`, and local development defaults.
3.  **Global flags** are parsed before any command runs: `--print-logs`, `--log-level`, `--island`, and `--auto` (aliased as `--yolo` and `--dangerously-skip-permissions`, which set `NIKCLI_AUTO_APPROVE` so the TUI's worker thread sees it too).
4.  **Command registration** wires ACP, MCP, ads, TUI thread/attach, run, goal, analytics, generate, debug, auth, account, artifact, agent, upgrade, quickstart, doctor, uninstall, serve, workspace-serve, web, heap, models, locale, stats, export/import, GitHub, PR, session, image/speak/brain model pickers, remote, teleport, companion, mobile, routine, mission, usage, plugin, sync, connectors, and chatbot commands. `--attach` is also a flag on `run`.
5.  **Failure formatting** uses `FormatError` for known errors and points unknown errors at the log file.
6.  **Process exit** runs in `finally` to avoid hanging child processes, especially external MCP servers.

The default command is the TUI

Because `TuiThreadCommand` is registered as `$0 [project]`, a bare `nikcli` command starts the TUI. One-shot agent execution goes through `nikcli run`.

## Instance model

Runtime services are scoped to project instances rather than process globals wherever possible. Commands and server routes run inside an instance context that carries the active directory, worktree, config paths, and service layers. This is why the TUI worker can host a server, respond to SDK calls, and still dispose project state cleanly through `Instance.disposeAll()`.

| Layer | Responsibility | Why it matters |
| --- | --- | --- |
| `global` | Cross-project filesystem roots, cache versioning, data directories, log directories, and managed binary paths. | Every process needs the same durable root model before config or storage can load. |
| `project/instance` | Current directory, worktree, lifecycle, and per-project state. | Lets TUI, server, mobile, and workspace requests isolate project behavior. |
| `Config.defaultLayer` | Remote, global, project, env, content, directory, mode, provider, command, agent, and plugin config merging. | Prevents commands from manually reimplementing config precedence. |
| `Database.defaultLayer` | The shared `nikcli.db` SQLite client, migrations, and transactions. | Unifies persistence for CLI, TUI, server, mobile, sessions, goals, and stats. |
| `Session.defaultLayer` | Session lookup, creation, updates, messages, tool parts, goals, todos, summaries, and processing. | Session behavior stays shared across local and HTTP surfaces. |

## Server stack

`packages/nikcli/src/server/server.ts` owns the `Bun.serve` listener and `server/server-router.ts` owns the request pipeline. Every declared route is a typed `effect/unstable/httpapi` group under `server/httpapi/`, composed into one API in `httpapi/public.ts` and served through `HttpApiBridge`. There is no Hono layer and no `server/routes/` directory. See [Server & API](/docs/server-api).

| Stage | Behavior |
| --- | --- |
| Body limit + CORS | Oversized bodies are rejected before parsing; `OPTIONS` short-circuits with computed CORS headers. |
| Public shares | `/s/:id`, `/share/:id`, and `/api/share/:id` answer before any auth check. |
| Auth | `Auth.authenticate()` resolves one principal from mobile tokens, account tokens, Tailscale identity, or Basic Auth. |
| Global vs instance | `isInstanceLessPath()` sends `/global`, `/user`, `/account` down a branch that never binds a directory. |
| Context | Everything else resolves directory + workspace, proxies remote workspaces, and enters the instance context. |
| Raw handlers | SSE feeds, prompt streaming, chatbot webhooks, and websocket upgrades claim their paths before the bridge, because a schema cannot hold an open stream. |
| HttpApi bridge | The typed handler answers whenever `HttpApiBridge.supports(path, method)` matches. |
| Fallback | Unmatched non-`/mobile` paths proxy to the hosted web app so it can be served from the same origin. |

The bridge validates responses

Unlike the removed Hono handlers, the typed groups check response bodies at runtime. A response schema stricter than the real payload turns a working route into a 500, so new or tightened schemas need verifying against real data.

## Session runtime

Sessions are the main execution unit. The session layer persists metadata, messages, message parts, diffs, summaries, todos, goals, and share state; coordinates agent execution; publishes events; and exposes both SDK routes and internal service calls. The one-shot command, TUI, mobile host, remote control, workspace hosting, and server API all converge here.

| Concern | Primary modules | Notes |
| --- | --- | --- |
| Session metadata | `session/index.ts` | Create, update, delete, list, title, plan path, share lookup, and project-scoped session keys. |
| Messages and parts | `session/message-v2.ts`, processors | Message records and part records are normalized in JSON storage. |
| LLM request loop | `session/llm.ts`, `session/llm/native-runtime.ts` | Normalizes prompts, selects provider/runtime, streams responses, handles tool calls, and repairs prompt-shape issues. |
| Goals and todos | `session/goal`, todo services | Persist planning state and task state alongside the transcript. |
| Sharing and remote | share, remote, server routes | Expose sessions to external clients while preserving session storage as the source of truth. |

## LLM and providers

Provider configuration is split from the session hot path. `provider/provider.ts` builds provider and model metadata from bundled SDK adapters, models.dev, config providers, saved auth, dynamic providers such as Ollama and nikcli inference, and plugin-supplied auth or model loaders. `session/llm.ts` is where requests are actually shaped and sent.

| Layer | Responsibility |
| --- | --- |
| Provider registry | Discovers providers, applies enabled/disabled filters, merges config, loads auth, filters deprecated or alpha models, and caches results. |
| Model selection | Resolves provider/model ids from config, CLI args, dialogs, or session state. |
| Request shaping | Converts stored messages into model-compatible message arrays and handles provider-specific prompt constraints. |
| Native runtime | Uses `@nikcli-ai/llm` for supported providers when the native runtime path is available. |
| Refresh | `Provider.refresh()` clears and rebuilds caches after auth or config changes. |

## Tools and permissions

Tools are available to agents through the tool registry and are guarded by the permission system where needed. Permissions are evaluated from rules, path patterns, and action defaults, with pending prompts published as bus events and persisted always-approvals stored under the project storage namespace. See [Permission modes](/docs/permissions#modes) for the TUI presets (`require_approval` / `approve_for_me` / `full_access` / `custom`) that write the per-agent permission map.

| Area | Behavior |
| --- | --- |
| Tool registry | Registers filesystem, shell, edit, search, web, MCP, LSP, and domain-specific tools according to config and feature flags. |
| Permission evaluation | Finds the last matching allow, deny, or ask rule, expands home paths, and resolves action-specific policy. |
| Interactive asks | Publishes pending permission requests and waits for a one-time, always, or reject reply. |
| Persistence | Always approvals are stored as project-scoped records in the JSON storage layer under `permission/:projectID`. |
| LSP tool gate | The agent-facing `lsp` tool is available only when the experimental LSP tool flag is enabled. |
| Mode presets | The TUI exposes three presets that write the agent's `config.agent.<name>.permission` map. The default is `approve_for_me` (allow trusted local work, ask for shell/web/external/sensitive reads). |

## Storage and state

All durable state lives in a single SQLite database, `nikcli.db` under `Global.Path.data`, reached through Drizzle. Each domain declares its tables in a `*.sql.ts` file, re-exported from `src/database/schema.ts`, and queries them through a repository namespace. See [Storage](/docs/storage).

One database, not a JSON store

The JSON object store under `data/storage` and the separate `users.db` / `accounts.db` / `mobile_auth.db` / `workspaces.db` files were both folded into `nikcli.db`. Migrations import the old data on first run.

| Area | Tables | Declared in |
| --- | --- | --- |
| Sessions | `session_info`, `message_info`, `message_part`, `session_entry`, `session_diff`, `session_pending`, `todo_info`, `session_goal` | `src/session/*.sql.ts` |
| Projects & workspaces | `project`, `workspace` | `src/project`, `src/workspace` |
| Automation | `loop`, `loop_run`, `mission`, `mission_exec`, `routine`, `background_run`, `monitor` | `src/loop`, `src/mission`, `src/mobile`, `src/background`, `src/monitor` |
| Identity | `users`, `user_sessions`, `account`, `mobile_tokens` | `src/user`, `src/account`, `src/mobile` |
| Permissions & sharing | `permission_ruleset`, `session_share`, `local_share`, `artifact` | `src/permission`, `src/share`, `src/artifact` |
| Sync | `sync_event`, `sync_sequence`, `sync_snapshot`, `sync_outbox` | `src/sync` |

Things that stay files do so because a file _is_ the interface: config (`nikcli.json`), provider credentials (`auth.json`), markdown plans, snapshots, skills, commands, agents, and themes.

## Events and sync

The server publishes typed events through bus definitions and exposes event streams to SDK clients. The TUI consumes them through `SDKProvider`, batches deliveries, and updates Solid stores. Mobile and companion surfaces consume the same event model over server routes.

1.  **Runtime module changes state**, such as session update, message part append, permission request, or TUI command publish.
2.  **Bus event is published** with a typed schema and payload.
3.  **Server event route streams it** to SDK subscribers.
4.  **Client sync provider handles it** and updates route, transcript, provider, permission, toast, or workspace state.
5.  **UI renders from synchronized state** instead of manually polling each route.

## Integrations

nikcli has three integration families: MCP servers, first-party connector registry entries, and plugins. MCP extends tool/resource/prompt surfaces. Connectors integrate named services and bot transports. Plugins can contribute auth, providers, commands, server hooks, and TUI routes.

| Integration | Source modules | Runtime role |
| --- | --- | --- |
| MCP | `mcp/index.ts`, `mcp/auth.ts`, HTTP API routes | Local stdio and remote Streamable HTTP/SSE MCP servers, OAuth, tools, prompts, and resources. |
| Connectors | `connectors/registry.ts`, `connectors/auth.ts`, `connectors/credentials.ts` | Figma, Slack, GitHub, Lovable, and bot connector metadata, operations, auth, and health checks. |
| Chatbots | `server/httpapi/chatbot.ts`, chatbot command modules | Discord, Slack, Teams, Google Chat, Linear, and GitHub webhook entrypoints. |
| Plugins | `plugin/index.ts`, `plugin/install.ts`, `cli/cmd/plug.ts` | Internal plugins, config plugins, installable package plugins, server exports, and TUI exports. |
| LSP | `lsp/index.ts`, `lsp/server.ts`, `tool/lsp.ts` | Optional language-server clients and an experimental agent-facing code-intelligence tool. |

## Mobile and remote

Mobile hosting is a server route family, not the same feature as terminal remote control. The mobile host serves `/mobile/*` routes, validates bearer tokens from `mobile_auth.db`, streams sessions over SSE, handles permission replies, exposes routine and repository workflows, and remains compatible with hosted environments such as Railway when using the host path.

| Surface | Entry | Architecture note |
| --- | --- | --- |
| TUI remote control | `remote` command and TUI slash commands | Controls a running terminal session through server/TUI events and shared URLs. |
| Run against a server | `nikcli run --attach <url>` | One-shot agent execution attached to an existing nikcli server (no separate `attach` command). |
| Mobile host | `mobile` command and `server/httpapi/mobile.ts` | Hosts mobile app APIs, auth tokens, sessions, routines, GitHub-backed repos, notifications, and Live Activity flows. |
| Companion | `companion` command | Desktop browser companion surface using the server and SDK event model. |
| Workspace server | `workspace-serve` and workspace routes | Hosts workspace contexts, status, events, and session proxying. |

Good mobile smoke test

`POST /mobile/auth/token` is a better first smoke than a deep bootstrap call because it proves route mounting and auth/token plumbing before requiring a full mobile session bootstrap.

## SDK boundary

The generated JavaScript SDK is the contract between server routes and clients. The TUI imports `@nikcli-ai/sdk/httpapi` even when running in-process; it swaps only the fetch/event transport. Web, mobile, companion, and external clients use the same conceptual surface.

| Server change | Follow-up |
| --- | --- |
| Change an endpoint in a `server/httpapi/*.ts` group | Regenerate the clients with `bun run --cwd packages/nikcli generate:httpapi-clients`. |
| Add or change typed HTTP API routes | Verify route schemas, generated client methods, and any TUI/mobile callers. |
| Change event payloads | Update server event schema and every sync consumer that narrows on that event type. |
| Change auth behavior | Verify direct worker fetch, HTTP SDK clients, attach, mobile, companion, and remote control separately. |

## Operational map

Use this map to decide where to start for common architecture questions.

| Question | Start here | Then inspect |
| --- | --- | --- |
| Why does bare `nikcli` open the terminal app? | `src/cli-main.ts` | `cli/cmd/tui/thread.ts`, then `packages/tui/src/app.tsx` |
| Which route handles this API call? | `server/server-router.ts` | The group under `server/httpapi/`, or a raw handler in `server/public.ts` / `server/extra.ts` |
| Why is config different in a worktree? | `config/config.ts` | `config/paths.ts` and environment flags |
| Why did a provider/model disappear? | `provider/provider.ts` | Saved auth, config provider entries, enabled/disabled filters, plugin auth/model hooks |
| Where is session content stored? | `database/schema.ts` | `session/repo.ts`, `session/message-repo.ts`, `session/v2/` |
| Why is a tool blocked? | `permission/next.ts` | `permission/schema.ts`, `permission/evaluate.ts`, the active permission mode (see [Permission modes](/docs/permissions#modes)) |
| Why does mobile auth fail? | `server/httpapi/auth.ts` | `mobile/auth.ts` and the `mobile_tokens` table |
| Why does the TUI not receive updates? | `packages/tui/src/context/sdk.tsx` | `cli/cmd/tui/worker.ts`, `GET /event`, server auth |

## Source anchors

| File or directory | What to inspect |
| --- | --- |
| `packages/nikcli/src/index.ts`, `src/cli-main.ts` | Process entrypoint and daemon re-entry split; then yargs setup, global flags, middleware, command registration, failure formatting, and forced exit. |
| `@nikcli-ai/util/global` | Global path model, data/cache/config/state/log/bin/repos roots, environment overrides, and initialization. |
| `packages/nikcli/src/project/instance.ts` | Per-directory instance lifecycle and state scoping. |
| `packages/nikcli/src/effect/` | Effect helpers, current instance context, and service layer execution helpers. |
| `packages/nikcli/src/config/config.ts` | Main config merge order, env overrides, mode migration, provider/plugin/command/agent loading, and flag overrides. |
| `packages/nikcli/src/server/server.ts`, `server/server-router.ts` | Bun listener, port fallback, OpenAPI; then CORS, auth, the global/instance split, dispatch, and error mapping. |
| `packages/nikcli/src/session/index.ts` | Session service, metadata persistence, updates, deletion, share lookup, and plan path selection. |
| `packages/nikcli/src/session/llm.ts` | Runtime LLM request path, prompt normalization, provider selection, streaming, and tool call loop. |
| `packages/nikcli/src/provider/provider.ts` | Provider registry, models.dev integration, config providers, saved auth, plugins, dynamic providers, and refresh. |
| `packages/nikcli/src/tool` | Agent-facing tools, tool registration, and domain-specific tools. |
| `packages/nikcli/src/permission` | Permission schemas, evaluation, persisted approvals, and interactive request flow. |
| `packages/nikcli/src/database` | SQLite client, PRAGMAs, transactions, post-commit effects, schema re-exports, and the migration list. |
| `packages/nikcli/src/server/httpapi/mobile.ts` | Mobile host API surface, auth token smoke path, sessions, routines, repos, permissions, and event streaming. |
| `packages/nikcli/src/cli/cmd/tui` | Terminal app command, worker transport, and the local plugin host — the runtime-side half of the TUI. |
| `packages/tui/src` | Renderer, app shell, routes, dialogs, contexts, feature plugins, and the SDK provider. |
| `packages/nikcli/src/mcp` | MCP client lifecycle, OAuth, local/remote transports, tool/resource/prompt conversion, and auth storage. |
| `packages/nikcli/src/connectors` | Connector registry, credentials, auth storage, CLI commands, and bot integrations. |
| `packages/nikcli/src/plugin` | Internal plugins, installable plugins, package exports, server/TUI targets, and default plugin behavior. |
| `packages/nikcli/script/generate-httpapi-clients.ts` | SDK and Effect client generation from the typed HttpApi. `packages/sdk/js/script/build.ts` only bundles the package for publish. |

---

Source: https://nikcli-ai.dev/docs/architecture
