What this is

The session API is the pairing of brasa (the engine: parser, dispatch, builtins, pipes, session state, output sinks, surface capabilities) and calypso (the daemon that hosts an engine behind a WebSocket, plus the zod-validated wire contract a client speaks); the packages install as @fnndsc/brasa and @fnndsc/calypso. A program on this API exchanges command lines for command envelopes: structured results carrying rendered text, error text and status, exactly what the chell shell itself runs on.

Why this is

Some programs are not scripts but surfaces: a web terminal, a chat integration, an agent that operates ChRIS conversationally. Such a program needs a session (working directory, connection, caches, history), needs interactivity to happen on the correct machine (a password prompt or a pipe through grep must run where the human is, never on a daemon host), and needs results as data rather than scraped text. The engine plus the wire contract provide precisely that, and the capability inversion is the load-bearing design: prompts, pipeline segments, shell escapes and local edits are surface capabilities the engine requests, so an attached client executes them locally and returns the outcome.

How this relates to existing session machinery

chell’s interactive REPL, `chell -c one-shots, the chell --daemon host and chell --remote client are all thin arrangements of these same parts; nothing documented here is a side door. The forthcoming web surface is planned as another such arrangement. For the protocol itself, every message crossing the socket validates against the schemas in the calypso reference.

The example is a program, and the program is a test

The example below is a complete, standalone program, included in full: it needs no CUBE and no credentials, and continuous integration runs it on every pull request. It lives in packages/chell/exemplars/ts/standalone/; copy it whole and it runs.

Host, attach, execute

One program, both roles. It creates the real brasa engine, hosts it behind a real calypso daemon on an ephemeral port, attaches chell’s production `RemoteEngine client over the WebSocket, executes one line, and reads the typed envelope back:

/**
 * @file Standalone example — host an engine behind a daemon and drive it
 * with the production remote client.
 *
 * The complete program, no CUBE and no credentials required: a real brasa
 * engine hosted by a real calypso daemon on an ephemeral port, and chell's
 * own RemoteEngine attached over a real WebSocket. One command line goes
 * across the wire; typed envelopes come back.
 *
 *   node exemplars/ts/dist/standalone/session_wire.js
 *
 * @module
 */
import type { CommandEnvelope } from '@fnndsc/cumin';
import { CalypsoDaemon, daemonSurface_create, DaemonSink } from '@fnndsc/calypso';
import { engine_create, sinkScope_run, surface_set, type BrasaEngine } from '@fnndsc/brasa';
import { RemoteEngine } from '@fnndsc/chell/dist/remote/remoteEngine.js';

/**
 * Hosts, attaches, executes one line, and prints the envelope.
 */
async function main(): Promise<void> {
  // The engine is the same one chell's REPL drives; disconnected it still
  // parses, dispatches and runs the builtins that need no CUBE.
  const engine: BrasaEngine = await engine_create();

  // The daemon hosts the engine behind a WebSocket. Scoping the daemon sink
  // around each executed line streams live output to the executing surface;
  // installing the daemon surface delegates prompts, pipes, shell escapes
  // and edits to whichever attached client runs the command.
  let sink: DaemonSink;
  const daemon: CalypsoDaemon = new CalypsoDaemon({
    engine: {
      line_execute: (line: string): Promise<CommandEnvelope[]> =>
        sinkScope_run(sink, (): Promise<CommandEnvelope[]> => engine.line_execute(line)),
      line_complete: (prefix: string) => engine.line_complete(prefix),
    },
    token: 'example-token',
    port: 0,
  });
  sink = new DaemonSink(daemon);
  surface_set(daemonSurface_create(daemon));

  const port: number = await daemon.start();

  // RemoteEngine is chell's production client: attach handshake, validated
  // messages, and the same line_execute interface as an in-process engine.
  const client: RemoteEngine = await RemoteEngine.connect({
    url: `ws://127.0.0.1:${port}`,
    token: 'example-token',
  });

  const envelopes: CommandEnvelope[] = await client.line_execute('help');
  const first: CommandEnvelope | undefined = envelopes[0];
  console.log(`status: ${first?.status}`);
  console.log(`rendered bytes: ${first?.rendered?.length ?? 0}`);

  await daemon.stop();
  process.exit(first?.status === 'ok' ? 0 : 1);
}

main().catch((error: unknown) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
});

Each envelope carries status, rendered and optionally renderedErr, so a hosting program presents output, detects failure and sets exit codes without parsing text. In a real deployment the two halves live in different processes, usually different machines; exemplar 05_calypsoDaemon.ts shows that operational layer, spawning chell --daemon, discovering it through its berth file, and surviving a restart with the session rehydrated.

Interactivity crosses the wire too

The client above passed no capability callbacks, so it attached as a non-interactive surface. A real surface passes them: onPrompt answers password and confirmation prompts, onPipe runs pipeline segments (help | grep session) through the client machine’s own tools, onShell runs ! escapes locally, and onEdit opens the client’s editor and returns the result. Exemplar 06_wireRoundtrip.ts, run by continuous integration on every pull request, exercises each of those round trips with assertions; this excerpt shows the daemon half it drives:

  const engine: BrasaEngine = await engine_create();
  const pipeCommands: string[] = [];
  const shellCommands: string[] = [];

  // The hosted engine runs each line with the daemon sink scoped in, so live
  // output crosses the wire instead of leaking to this process's stdout. The
  // sink needs the daemon, which needs this wrapper: the wrapper closes over
  // the binding, assigned right after construction.
  let sinkForDaemon: DaemonSink;
  const daemon: CalypsoDaemon = new CalypsoDaemon({
    engine: {
      line_execute: (line: string): Promise<CommandEnvelope[]> =>
        sinkScope_run(sinkForDaemon, (): Promise<CommandEnvelope[]> => engine.line_execute(line)),
      line_complete: (prefix: string) => engine.line_complete(prefix),
    },
    token: TOKEN,
    port: 0,
  });
  sinkForDaemon = new DaemonSink(daemon);
  surface_set(daemonSurface_create(daemon));

Where to go next

brasa reference for the engine, sinks, surfaces and envelope delivery; calypso reference for CalypsoDaemon, the request brokers and the message schemas.