CLI
Documentation

Version Latest

SDK, server and embedding

Drive the CLI's agent from your own code in process, over the local HTTP and SSE server, or as an agent embedded in a governing host with the NI Host Protocol.

There are three ways to drive the agent from code. All of them use one event schema, the same one --print --output-format stream-json writes.

In processLocal serverGoverning host
Entry pointcreateAgent()neuralinverse serve and connect()neuralinverse host and spawnAgent()
Package export@neuralinverse/cli/sdk@neuralinverse/cli/sdk@neuralinverse/cli/embed
RunsInside your Node.js processIn a separate server processIn a child process the host controls
Good forScripts, CI tools, Node.js appsEditor plugins, web interfaces, other languages, several clientsApps that supply models, credentials and policy, and decide every approval

Permission rules, modes, managed policy and organisation governance apply in all three, as in the terminal.

In process

import { createAgent } from '@neuralinverse/cli/sdk';

const agent = await createAgent({
  cwd: '/path/to/repo',
  model: 'anthropic/claude-sonnet-5-5',
  mode: 'ask',
  // Called whenever a tool call needs approval. Without it such calls are denied.
  onPermission: async (req) => {
    console.log(`${req.toolName}: ${req.target.summary}`);
    return req.target.effect === 'write' ? { kind: 'allow_once' } : { kind: 'deny', feedback: 'read-only please' };
  },
});

try {
  const result = await agent.prompt('Fix the failing test in src/parser.ts', {
    onEvent: (e) => {
      if (e.type === 'text_delta') process.stdout.write(e.text);
    },
  });
  console.log(`\n${result.reason}, ${result.usage.outputTokens} output tokens, $${result.costUsd ?? '?'}`);
} finally {
  await agent.close();
}

Stream with an async iterator; leaving the loop early interrupts the prompt:

for await (const e of agent.stream('Summarize the README')) {
  if (e.type === 'text_delta') process.stdout.write(e.text);
}
OptionMeaning
cwdWorkspace folder (default process.cwd())
modelprovider/model
modeask, auto-edit, read-only, full-auto
settingsExtra settings layered on the settings files
configDir, managedSettingsPathOverride ~/.neuralinverse and the managed settings file (null disables it)
onPermission(req, signal)Approve or deny tool calls: allow_once, allow_always with rule and persist, or deny
askUser, reviewPlanAnswer the agent's questions and plan proposals
providersReady-made provider instances by id
enableMcpStart configured MCP servers (default true)
persistSessions, resumeSave the conversation (default false) or continue a saved one
appendSystemPrompt, addDirs, env, onLogAs the CLI flags of the same names

The returned agent has prompt(), stream(), interrupt(), setMode(), setModel(), messages(), close() and runtime.

The local server

neuralinverse serve                      # 127.0.0.1:7420, root = current folder
neuralinverse serve --port 0 --root ~/src --root ~/work --mode auto-edit

On start the server prints its URL and a newly generated bearer token, and writes both to ~/.neuralinverse/server.json (mode 0600; removed on shutdown). Global flags (--model, --mode, --allow, --deny, --config, --no-mcp, --append-system-prompt and others) apply to every session.

FlagDefaultMeaning
--host <addr>127.0.0.1Interface to bind
--port <n>74200 picks a free port
--token <t>NEURALINVERSE_SERVER_TOKEN, else randomAt least 16 characters
--allow-remoteoffRequired for non-loopback addresses, together with an explicit token
--cors <origin>offAllow browser requests from exactly this origin
--root <dir>current folderFolders sessions may work in (repeatable)
--permission-timeout <s>300Unanswered approvals are denied after this long
--max-sessions <n>32Concurrent sessions
--weboffAlso serve the browser interface
--json-readyoffOnce listening, print one JSON line {"type":"ready","url","host","port","pid","version","api_version","roots"} (plus token when generated) and do not write server.json
--exit-on-stdin-closeoffShut down when standard input closes

A tool that starts the server typically runs neuralinverse serve --port 0 --json-ready --exit-on-stdin-close with the token in NEURALINVERSE_SERVER_TOKEN (keeping it out of process listings) and reads the first line of output. The VS Code extension works this way. SIGINT and SIGTERM stop the server gracefully.

Security

  • Loopback only, unless --allow-remote and an explicit token are given. There is no TLS: put a TLS-terminating proxy in front of a remote server.
  • Every request except GET /v1/health needs Authorization: Bearer <token>, compared in constant time.
  • On loopback binds the Host header must name a loopback address, which defeats DNS rebinding from web pages.
  • A session's working folder must be inside one of the --root folders (symbolic links are resolved first).
  • Request bodies are limited (1 MiB; prompts 25 MiB) and validated; at most 16 prompts can wait in a session's queue.
  • Anyone with the token can run tools with your user's rights. Treat the token like a password.

Endpoints

Bodies are JSON. Errors are {"error": {"code": "...", "message": "..."}} with a matching status, for example 400 validation_error, 401 unauthorized, 403 cwd_outside_roots, 404 session_not_found, 409 session_busy, 413 payload_too_large, 415 unsupported_media_type, 429 too_many_sessions.

Method and pathBodyResponse
GET /v1/health (no auth){status: "ok"}
GET /v1/info[?live=1]Version, api_version, roots, limits, providers, default_model, models
POST /v1/sessions{cwd?, model?, mode?, resume?}201 session info
GET /v1/sessions{sessions: [...]}
GET /v1/stored-sessions[?cwd=]Resumable sessions of a workspace
GET /v1/sessions/:idSession state, model, mode, title, usage, queue and pending requests, last_event_id
DELETE /v1/sessions/:idCloses the session
POST /v1/sessions/:id/prompt{text, images?, queue?}202 {turn_id, queued, position?}; 409 session_busy while a prompt runs unless queue: true
POST /v1/sessions/:id/interrupt{clear_queue?}{interrupted, cleared_queue}
GET /v1/sessions/:id/eventsSSE stream
GET /v1/sessions/:id/messages{messages: [...]}
POST /v1/sessions/:id/permissions/:requestIdAn answer (below)Answers a permission, question or plan request (/questions/…, /plans/… are aliases)
POST /v1/sessions/:id/mode{mode}{mode}
POST /v1/sessions/:id/model{model}{model} (idle sessions only)
POST /v1/sessions/:id/compact{instructions?}{before_tokens, after_tokens}
GET /v1/sessions/:id/checkpoints{checkpoints: [...]}
POST /v1/sessions/:id/rewind{checkpoint_id, files?, conversation?}{restored_files, message_count}
GET /v1/sessions/:id/files[?q=&limit=]Fuzzy file search in the session's workspace (limit at most 500, default 50)
GET /v1/fs/dirs[?path=]Sub-folders of a folder inside the roots

The event stream

GET /v1/sessions/:id/events is a server-sent events stream. Every event has an id: line (a counter per session), an event: line with its type, and a JSON data: line. A heartbeat comment is sent every 15 seconds. The last 1000 events of each session are kept: reconnect with Last-Event-ID (or ?last_event_id=) to receive everything after that id; if some was dropped, a replay_gap event comes first.

Agent events: turn_start, text_delta, thinking_delta, assistant_message, tool_call, tool_progress, tool_result, tool_denied, retry, compacted, usage, notice, task_update, done and others. Server events:

EventFields
prompt_queuedposition
prompt_started
prompt_finishedreason (end_turn, max_turns, max_tokens, refusal, aborted, error, budget), error?
permission_requestrequest_id, call_id, tool, summary, preview?, suggested_rule, effect, input, outside_workspace?, expires_at
question_requestrequest_id, question, options, multi_select, allow_other, expires_at
plan_requestrequest_id, plan, expires_at
request_resolvedrequest_id, kind, outcome (answered, timeout, cancelled)
mode_changed, model_changedmode, model
session_closedThe stream ends after it

Answers to POST …/permissions/:requestId:

{ "answer": "allow_once" }                                         // permission
{ "answer": "allow_always", "rule": "run_shell(npm test *)", "persist": "session" }
{ "answer": "deny", "feedback": "use the staging database instead" }
{ "answer": { "answers": ["Postgres"], "other": "with pgvector" } }  // question
{ "answer": { "dismissed": true } }
{ "answer": { "approved": true, "mode": "auto-edit" } }            // plan
{ "answer": { "approved": false, "feedback": "split it in two PRs" } }

Unanswered requests are denied, dismissed or rejected after --permission-timeout, and when the prompt is interrupted.

TOKEN=$(jq -r .token ~/.neuralinverse/server.json)
URL=http://127.0.0.1:7420
H="authorization: Bearer $TOKEN"

ID=$(curl -s -H "$H" -H 'content-type: application/json' -d '{"mode":"ask"}' $URL/v1/sessions | jq -r .id)
curl -sN -H "$H" $URL/v1/sessions/$ID/events &
curl -s -H "$H" -H 'content-type: application/json' -d '{"text":"List the TODOs"}' $URL/v1/sessions/$ID/prompt

Remote client

import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { connect } from '@neuralinverse/cli/sdk';

const { url, token } = JSON.parse(readFileSync(`${homedir()}/.neuralinverse/server.json`, 'utf8'));
const client = connect({ url, token });

const session = await client.createSession({ cwd: '/home/me/src/app', mode: 'ask' });
const result = await session.run('Add input validation to the signup form', {
  onEvent: (e) => {
    if (e.type === 'text_delta') process.stdout.write(e.text);
  },
  onPermission: (req) => (req.effect === 'write' ? { answer: 'allow_once' } : { answer: 'deny' }),
  onQuestion: (q) => ({ answer: { answers: [q.options[0]!.label] } }),
});
console.log(`\n${result.reason}`);
await session.close();

run() subscribes to the event stream, sends the prompt, answers requests with your callbacks (without them, approvals are denied, questions dismissed and plans rejected) and resolves when the prompt finishes. events() reconnects after network errors, resuming with Last-Event-ID. Failures surface as ApiError with status and code. startServer(options) and startAcpAgent(options) are exported too, for hosts that serve their own runtimes.

Embedding in a governing host

neuralinverse host turns the CLI into an agent that a host application drives: the Editor's shared agent host first, and any editor or app after it. The host sees every action, decides approvals, supplies models and credentials, and can push the organisation's policy.

Maturity. The CLI side is ready and tested with a conformance kit. The adapter that registers the CLI as an agent in the Editor's shared agent host is drafted but not yet part of the Editor; file-system, terminal, question and attachment wiring on the Editor side is not done yet.

neuralinverse host [--stdio]                                  # default
neuralinverse host --socket <path> --token-file <file>        # or --token-fd <n>
  --grace <ms>          running turns may finish this long after the host goes away (default 10000)
  --max-sessions <n>    default 64
  --event-buffer <n>    events kept per session for replay (default 2000)
  • stdio: standard output carries the protocol (JSON-RPC 2.0, one message per line); diagnostics go to standard error. The agent exits when standard input ends (after the grace period) or after shutdown.
  • socket: a Unix socket created with mode 0600 in a folder you own (a named pipe on Windows). The first message of every connection must be initialize with auth.token; a wrong token, or no handshake within 10 seconds, closes the connection. The token comes from a 0600 file you own or an inherited file descriptor, never from the command line.

Capabilities a host can declare

CapabilityThe CLI thenWithout it
credentials.keystoneUses a Govern session the host supplies through auth/tokenThe CLI's own Govern sign-in
modelsRegisters the provider host (models host/<id>) on the host's endpoint (anthropic, openai-responses or openai); exclusive removes every other providerUser-configured providers
approvalsSends approval/request for every ask; with review: "mutations", also for every write, command and network call that rules or the mode would allow, including in full-auto; all adds readsRequests wait, then time out (denied)
interactionsSends questions and plans to the hostQuestions answered "nobody available"; plans rejected
policyAccepts policy/push
activitySends activity events to the hostThe CLI's own queue
fsFile tools read and write through the host (open editor buffers)Disk
terminalForeground shell commands run in host terminalsChild processes

Governance in a host

  • The host sees every action. Permission asks go to the host with the full approval card; with approvals.review: "mutations", every mutating call is routed to the host first, even in full-auto. The host's denial is final; a blocking checks finding refuses the change whatever the answer.
  • Policy layers only tighten. Local managed settings and the remote policy apply first. A host-pushed policy applies like a managed layer for sessions created after the push. It must carry an Ed25519 signature that verifies against remotePolicyPublicKey or a key in embed.hostPolicyPublicKeys in the local managed settings; unsigned bundles are rejected unless that file sets embed.trustHostPolicy: true. remotePolicy* keys are stripped from host bundles.
  • Models and credentials stay with the host. With models.exclusive, the session can use only the host's loopback endpoint; /provider, /login and switching providers are refused.
  • Locked settings stay locked. Mode changes honour permissions.mode locks and disableFullAuto.
{
  "remotePolicyPublicKey": "MCowBQYDK2VwAyEA…",
  "embed": { "hostPolicyPublicKeys": ["MCowBQYDK2VwAyEA…"], "trustHostPolicy": false }
}

Example host

import { spawnAgent } from '@neuralinverse/cli/embed';

const agent = await spawnAgent({
  clientInfo: { name: 'my-host', version: '1.0.0' },
  capabilities: {
    models: { endpoint: { kind: 'anthropic', baseUrl: 'http://127.0.0.1:4100', auth: 'token' }, exclusive: true },
    approvals: { review: 'mutations' },
  },
  handlers: {
    token: async ({ audience }) => ({ token: await myTokens.mint(audience) }),
    approvals: async ({ approval }) =>
      approval.risk === 'high' || approval.checks?.blocking ? { outcome: 'deny', reason: 'needs a human' } : { outcome: 'allow_once' },
    activity: ({ events }) => myAudit.forward(events),
  },
});

const session = await agent.newSession({ cwd: '/work/repo', mode: 'auto-edit' });
const result = await agent.run(session.id, 'Fix the failing test in src/date.ts', {
  onEvent: (e) => { if (e.type === 'text_delta') process.stdout.write(e.text); },
});
await agent.stop({ graceMs: 5000 });

createHostedRuntime(options) runs the agent inside your process with the same protocol over an in-memory stream; connectSocket(path, { token }) connects to --socket mode.

Protocol reference and versioning

The message reference is the machine-readable schema schema/host-protocol.schema.json in the package. Host-to-agent methods include initialize, health, shutdown, policy/push, session/new, session/load, session/list, session/fork, session/close, session/prompt, session/cancel, session/set_model, session/set_mode, session/subscribe, session/events, session/answer, session/checkpoints, session/rewind, session/undo, session/tasks, session/usage and session/commands. Agent-to-host methods are approval/request, interaction/question, interaction/plan, auth/token, fs/*, terminal/*, and the notifications session/event and activity/event.

Each session keeps its last --event-buffer events; subscribe with the last id you processed as afterSeq to replay what you missed. Application errors use code -32000 with data.code (session_not_found, session_busy, locked_by_policy, model_not_allowed, shutting_down and others). Protocol errors: -32002 not initialised, -32003 wrong handshake token, -32004 no common protocol version, -32005 policy bundle rejected.

The protocol version is semver, now 1.0.0. The agent accepts the same major version and answers the lower of the two; minor versions only add. A breaking change bumps the major, and the CLI keeps accepting the previous major for at least two minor releases after a new major ships.

Conformance kit

Run the CLI's conformance scenarios (handshake, approvals, full-auto still honouring host denials, token refresh, policy push, cancel, replay after reconnect, resume after restart, clean exit) against your integration from a checkout of the CLI:

npm run embed:conformance -- --command "neuralinverse host --stdio"
npm run embed:conformance -- --command "/path/to/ide-wrapper --stdio" --only approval --verbose

The command must speak the protocol on standard input and output and honour NEURALINVERSE_CONFIG_DIR and NEURALINVERSE_MANAGED_SETTINGS, which the kit points at throwaway folders.

Licence for embedding

The CLI is proprietary. You may embed it in, or ship it with, another product only as your agreement with Neural Inverse allows. Keep LICENSE and NOTICE in the package, and keep the licence headers of its files.

Last reviewed 17 October 2026View as Markdown

SDK, server and embedding · Docs · Neural Inverse