Architecture
Source-oriented map of the nikcli monorepo, CLI lifecycle, server stack, session runtime, storage model, integrations, mobile host, and SDK boundary.
Overview
nikcli is a fork of 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.
Single runtime core
CLI, TUI, server routes, mobile host, remote control, providers, tools, permissions, and storage live in the main package.
Generated client
The web, TUI, mobile, and external clients use the SDK boundary instead of importing server internals.
Multiple shells
Different user surfaces route into the same Effect HttpApi server or the same session services.
Effect services
Config, project instance, sessions, storage, and provider services are assembled through Effect layers and per-instance context.
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.
Main CLI
Command registry, TUI, server app, session engine, provider registry, tools, permissions, config, storage, connectors, MCP, LSP, mobile host, and remote control.
Terminal UI
The whole OpenTUI/Solid renderer — app shell, routes, dialogs, contexts, feature plugins. It talks to the runtime only through the SDK.
Shared utilities
Global paths, Flag, Log, filesystem helpers, and the primitives both the runtime and the TUI need.
SDK
Generated JavaScript client and schemas for the server API. Regenerate it from the typed HttpApi with generate:httpapi-clients.
Plugin system
Reusable plugin API surface and package-level plugin tooling.
LLM client
Route-based multi-provider client — protocols, providers, tool runtime — used for native provider traffic.
Automation engines
Sibling daemons with a shared CLI grammar and evidence-bundle format, driven by the computer and browser tools.
bun install
bun run typecheck
bun test
bun run --cwd packages/nikcli dev
bun run --cwd packages/nikcli generate:httpapi-clients 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.
- Global initialization creates data, config, cache, state, log, bin, and repo paths.
- Logging setup respects
--print-logs,--log-level, and local development defaults. - Global flags are parsed before any command runs:
--print-logs,--log-level,--island, and--auto(aliased as--yoloand--dangerously-skip-permissions, which setNIKCLI_AUTO_APPROVEso the TUI's worker thread sees it too). - 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.
--attachis also a flag onrun. - Failure formatting uses
FormatErrorfor known errors and points unknown errors at the log file. - Process exit runs in
finallyto avoid hanging child processes, especially external MCP servers.
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.
| 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. |
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 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.
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.
- Runtime module changes state, such as session update, message part append, permission request, or TUI command publish.
- Bus event is published with a typed schema and payload.
- Server event route streams it to SDK subscribers.
- Client sync provider handles it and updates route, transcript, provider, permission, toast, or workspace state.
- 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. |
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) |
| 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. |