crouter
SDK

Getting started

Connect to a local or remote daemon and run your first agent with the SDK.

Getting started

Phase 1.

An agent run needs a crouter daemon (crtrd) to run it. The SDK is a client for that daemon — it never runs an agent itself. There are two ways to reach one: the unix socket on the machine you are running on, or a TCP listener with a bearer token.

1. Install the runtime

npm i -g @north-light/crouter

That installs the crtr CLI and the crtrd daemon. You do not have to start the daemon: new Crouter() starts it for you on a cold socket (see autostart in Client construction).

2. Install the SDK in your application

npm i @north-light/crouter-sdk

One dependency. It re-exports every data type you need, so you do not also install @north-light/crouter-api.

3. Node, on the owner's own daemon

Pass nothing. The client resolves the same socket path the crtr CLI does, and starts the daemon if it is not already up.

import Crouter from '@north-light/crouter-sdk';

const client = new Crouter();

const node = await client.nodes.create({
  prompt: 'List the top-level directories here and say what each one is for.',
  cwd: process.cwd(),
  root: true,
  root_lifecycle: 'terminal',
});

const outcome = await client.nodes.waitForOutcome(node.node_id);
if (outcome.kind === 'result') console.log(outcome.final_report_path);

root: true says this run has no parent node and reports to nobody — which is what an application's run is. root_lifecycle: 'terminal' says it finishes and reaps; use 'resident' for a run a person will open and keep talking to.

4. A browser or a remote application

A process that is not on the daemon's machine — or a page in a browser, which has no unix sockets at all — reaches the daemon over TCP with a bearer token. There are two kinds. The owner token is the owner credential: whoever holds it can drive the whole surface. A scoped token (crtr sys connect --scopes) carries a list of scopes that caps what its holder and every run it creates may do.

Turn the listener on, once

On the machine running the daemon:

crtr sys connect

This stores a listener address and generates a token if either is missing, then prints the base URL and token before handing the daemon over to a successor that boots with the listener on. It then logs in to the selected model provider when that provider has no usable credential. Run it again and, when the listener is up and that provider is ready, it prints the credentials and changes nothing.

The command returns base_url and token. Use JSON output when a program or a setup UI needs those exact fields:

$ crtr --json sys connect
{"base_url":"http://127.0.0.1:8787","token":"<64-character bearer token>"}

A token that holds less than the owner

crtr sys connect --scopes ask,memory:read

This mints a new token whose scope list is a ceiling, appends it to the same secrets store, prints it in place of the owner token, and always hands the daemon over (it reads tokens once, at boot). The ceiling is checked on every request that carries the token:

  • A scope-gated operation the ceiling lacks answers 403 scope_denied. Creating, reviving, forking, messaging, or yielding a run needs act; creating a review or human request needs ask; arming or running a cron needs schedule; memory reads and writes need memory:read and memory:write. bash and files also need act: they run code and touch the host directly, which is more than any run does, and files:<dir> is recorded, not enforced, so it cannot narrow them.
  • nodes.create with scopes outside the ceiling answers 403 scope_denied; details.scopes lists the offending scopes. Omit scopes and the run gets the ceiling.
  • Owner-only operations — daemon restart, the attach viewer, broker internals, canvas prune, profile pause/resume/delete, model credential install — answer 403 owner_only to any scoped token.

What a scope does not do on a developer's machine: there is no OS sandbox. Scopes gate the daemon and the CLI; a run's own bash tool is ungated, and the daemon identifies a calling node by a field the caller declares, so a scope check bounds a well-behaved agent, not a hostile one. llm, files:<dir>, net, and provider scopes are recorded on the run but not enforced. There is no list or revoke verb yet; to retire a scoped token, remove it from the 0600 user secrets store and restart the daemon.

Check setup before generating

Construct the client from the application's saved connection. On the first visit that value is absent, and client.auth.status() returns 'connect' without a request. Once the user pastes the base URL and token printed by crtr sys connect, construct it again and call status() to check the selected provider.

import Crouter from '@north-light/crouter-sdk';

const saved = loadConnection(); // { baseURL: string; token: string } | null
const client = new Crouter(saved ?? {});
const status = await client.auth.status({ profile: 'my-app', cwd: '/path/to/repo' });

if (status.next_step !== null) {
  showSetupPanel({ step: status.next_step, instructions: status.instructions! });
  return;
}

const run = await client.nodes.createAndWait({
  prompt: 'What changed in this repo today?',
  cwd: '/path/to/repo',
  profile: 'my-app',
  root: true,
});

status() returns next_step: 'connect' when the application has no daemon transport, when the saved bearer token is rejected, or when the saved base URL cannot be reached. It returns next_step: 'login' when the daemon resolves the selected run to a provider without a ready credential. In both cases, display instructions: it names the exact crtr sys connect action and, for a login, the provider. When status() receives profile, cwd, kind, or model, the login instruction passes those selectors to crtr sys connect; use the text unchanged. A daemon that is still starting is connected with next_step: null; inspect status.daemon.startup_phase and wait for it to become ready before starting a run.

An isolated auth-status probe returned:

{
  "no_request": {
    "next_step": "connect",
    "requests": 0
  },
  "missing": {
    "provider": "anthropic",
    "credential": "missing",
    "next_step": "login",
    "instructions": "Run `crtr sys connect --cwd /private/tmp/crouter-sdk-auth-proof.P3fSWg/empty --model anthropic/claude-opus-5` to log in to anthropic."
  },
  "ready": {
    "provider": "anthropic",
    "credential": "ready",
    "next_step": null
  }
}

The root entry of the SDK is browser-safe: it imports nothing from node:*. The unix-socket code is reached only through a dynamic import taken when socketPath is set, so a browser bundle never resolves it.

Cross-origin calls

The daemon answers the browser's OPTIONS preflight with Access-Control-Allow-Origin: * and allows the authorization and content-type headers, and every authorized response carries the same origin header. This happens only when a token is set — an unauthenticated listener stays same-origin, because otherwise any page the user visits could drive their daemon.

The credential is a header your application already holds, never a cookie, so the daemon sends no Access-Control-Allow-Credentials.

Chrome's local-network permission

A page served over HTTPS that calls http://localhost:<port> is governed by Chrome's Local Network Access permission. The browser prompts the user once per site; the daemon needs no special response header for it — ordinary CORS plus a secure context is the whole requirement. See developer.chrome.com/blog/local-network-access.

Tell your users what the prompt is for. A prompt that appears with no explanation gets dismissed, and the dismissal is sticky.

5. A typed result

parse() creates the run, waits for it to settle, and types the structured result against the schema you gave it.

import { z } from 'zod';

const run = await client.nodes.parse({
  prompt: 'Read package.json here and report its name and version.',
  cwd: '/path/to/repo',
  output_schema: z.object({ name: z.string(), version: z.string() }),
});

if (run.kind === 'result') {
  console.log(run.output_parsed.name, run.output_parsed.version);
} else if (run.reason === 'declined') {
  console.warn('the agent refused the schema:', run.declined?.reason);
} else {
  console.error('the run failed:', run.reason, run.detail);
}

output_parsed is non-null on a result. A Zod schema supplies its inferred output type; a Standard Schema supplies ~standard.types.output. A JSON-Schema literal or any other { toJSONSchema() } object is accepted but makes output_parsed unknown.

An outcome is returned, never thrown — including a decline and a failure. Only transport faults, daemon errors, and your own abort throw. See Errors.

Where to go next

  • A complete local page that demonstrates streaming, structured output, and a plugin: localhost SDK demo
  • Every constructor option and environment-variable fallback: Client construction
  • Checking the daemon connection and selected provider before a run: client.auth.status() above
  • The full create-parameter table and the outcome union: Nodes
  • Watching a run as it works: Streaming
  • Running the daemon in a container instead: Docker environment