What this is

The intent API is the pairing of cumin (connection lifecycle, the typed wire contract, resource primitives, the Result<T> error discipline) and salsa (file operations, feeds, jobs, pipelines, PACS retrieval); the packages install as @fnndsc/cumin and @fnndsc/salsa. A program built on them states intentions: connect as this user, make this directory, stage this series, run this plugin, and receives typed results rather than raw collection documents.

Why this is

CUBE’s REST surface is correct but request-shaped: creating a feed, attaching an analysis and reading its output takes a choreography of collection lookups that every script would otherwise reimplement, and reimplement slightly wrongly. The intent layer performs that choreography once, in one audited place, and its error discipline is uniform: fallible operations return Result<T>, failure carries its cause on an error stack, and nothing is silently absorbed.

How this relates to existing clients

@fnndsc/chrisapi remains the canonical low-level client, and cumin consumes it behind a single adapter seam rather than competing with it; the type-laundering casts that once spread through consuming code are confined there and held by a ratchet. If your program needs a raw collection the intent layer does not model, the escape hatch is deliberate and typed: see the wire contract in the cumin reference.

The examples are programs, and the programs are tests

Every example below is a complete, standalone program, included on this page in full: imports, entry point, exit codes. They live in packages/chell/exemplars/ts/standalone/ and continuous integration runs them against a live CUBE, so an example that stops working turns the build red rather than the page stale. Copy one whole, point CUBE_URL, CUBE_USER and CUBE_PASSWORD at your instance, and it runs.

Connect and identify

The minimal life of a cumin program. The Node storage provider persists the session under the XDG config directory, which is the same session store chell itself uses; a connected program and a connected shell are interchangeable.

/**
 * @file Standalone example — connect to a CUBE and identify yourself.
 *
 * The complete minimal life of a cumin program: initialize the connection
 * layer, authenticate, and read back who you are. Nothing here depends on
 * any test harness; this file is the whole program.
 *
 * Run with the CUBE settings in the environment:
 *
 *   CUBE_URL=https://cube.chrisproject.org/api/v1/ \
 *   CUBE_USER=alice CUBE_PASSWORD=secret \
 *   node exemplars/ts/dist/standalone/intent_connect.js
 *
 * @module
 */
import {
  chrisConnection_init,
  NodeStorageProvider,
  currentUser_get,
  type ChRISConnection,
  type ChrisUser,
  type Result,
} from '@fnndsc/cumin';

/**
 * Connects, identifies the session user, and prints the identity.
 */
async function main(): Promise<void> {
  const url: string | undefined = process.env.CUBE_URL;
  const user: string | undefined = process.env.CUBE_USER;
  const password: string | undefined = process.env.CUBE_PASSWORD;
  if (!url || !user || !password) {
    console.error('Set CUBE_URL, CUBE_USER and CUBE_PASSWORD.');
    process.exit(2);
  }

  // cumin persists the session (URL, token) through a storage provider; the
  // Node provider keeps it under the XDG config directory, so a later
  // program, or chell itself, can resume this session without a password.
  const connection: ChRISConnection = await chrisConnection_init(new NodeStorageProvider());
  const token: string | null = await connection.connection_connect({ url, user, password, debug: false });
  if (!token) {
    console.error('Authentication failed.');
    process.exit(1);
  }

  // Every fallible call returns Result<T>: check `ok` before using `value`.
  const who: Result<ChrisUser> = await currentUser_get();
  if (!who.ok) {
    console.error('Could not fetch the current user.');
    process.exit(1);
  }

  console.log(`Connected to ${url} as ${who.value.username} (id ${who.value.id}).`);
}

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

A filesystem round trip

Files live in the ChRIS filesystem, addressed by absolute paths under /home/<user>. This program makes a scratch directory with salsa’s `files_mkdir, writes with files_touch, reads back with fileContent_get, verifies the content, and then removes its own evidence with folderByPath_delete, which returns only once the CUBE confirms the folder no longer resolves (server-side deletion is asynchronous, so a fire-and-forget delete would race any rerun).

/**
 * @file Standalone example — a filesystem round trip that cleans up after
 * itself.
 *
 * The complete program: connect with cumin, then use salsa to make a scratch
 * directory, write a file, read it back, verify the content, and delete the
 * scratch directory by path. Every call is the public API; nothing here
 * depends on a test harness.
 *
 * Run with the CUBE settings in the environment:
 *
 *   CUBE_URL=https://cube.chrisproject.org/api/v1/ \
 *   CUBE_USER=alice CUBE_PASSWORD=secret \
 *   node exemplars/ts/dist/standalone/intent_fsRoundtrip.js
 *
 * @module
 */
import { chrisConnection_init, NodeStorageProvider, type ChRISConnection, type Result } from '@fnndsc/cumin';
import { files_mkdir, files_touch, fileContent_get, folderByPath_delete } from '@fnndsc/salsa';

/**
 * Connects, round-trips one file, verifies it, and removes the evidence.
 */
async function main(): Promise<void> {
  const url: string | undefined = process.env.CUBE_URL;
  const user: string | undefined = process.env.CUBE_USER;
  const password: string | undefined = process.env.CUBE_PASSWORD;
  if (!url || !user || !password) {
    console.error('Set CUBE_URL, CUBE_USER and CUBE_PASSWORD.');
    process.exit(2);
  }

  const connection: ChRISConnection = await chrisConnection_init(new NodeStorageProvider());
  if (!(await connection.connection_connect({ url, user, password, debug: false }))) {
    console.error('Authentication failed.');
    process.exit(1);
  }

  // A unique scratch path under the user's home keeps reruns independent.
  const scratchDir: string = `/home/${user}/example-${Date.now().toString(36)}`;
  const filePath: string = `${scratchDir}/hello.txt`;
  const content: string = `hello from ${scratchDir}`;

  try {
    if (!(await files_mkdir(scratchDir))) throw new Error(`mkdir failed: ${scratchDir}`);
    if (!(await files_touch(filePath, content))) throw new Error(`write failed: ${filePath}`);

    const readBack: Result<string> = await fileContent_get(filePath);
    if (!readBack.ok) throw new Error(`read failed: ${filePath}`);
    if (readBack.value !== content) throw new Error('content mismatch after round trip');

    console.log(`Round trip verified through ${filePath}.`);
  } finally {
    // Path-addressed deletion; resolves only once the CUBE confirms the
    // folder is gone (server-side deletion is asynchronous).
    const removed: boolean = await folderByPath_delete(scratchDir);
    console.log(removed ? `Removed ${scratchDir}.` : `Could not remove ${scratchDir}; delete it via chell or the UI.`);
  }
}

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

Feeds and analyses

An analysis is a feed rooted in data plus plugin instances attached to it. The excerpt below is the feed-and-analysis heart of the fuller exemplar 03_feedDcm2niix.ts (a complete program in the same repository directory, one level up from standalone/): it roots a feed on a staged DICOM series with pl-dircopy, attaches pl-dcm2niix, and polls it to completion. The check and section calls are that exemplar’s assertion helpers; the salsa and cumin calls inside feed_root and analysis_runAndVerify are the API.

  section('feed: pl-dircopy on the series folder');
  const parentId: Result<number> = await feed_root(cleanup, staged.value.folderPath);
  if (!parentId.ok) return;

  section('analysis: pl-dcm2niix');
  await analysis_runAndVerify(parentId.value, runId);

Read that exemplar whole for the staging above this excerpt (PACS query and retrieve) and the cleanup discipline below it: every resource it creates registers its own undo, and the CUBE ends the run exactly as it began.

Where to go next

The generated reference documents every exported function with its parameters and result shape: cumin for connection, wire contract and primitives; salsa for files, feeds, jobs, pipelines and PACS. For the PACS retrieve engine specifically (firing bounded pulls, LONK progress watching, storage resolution), read retrieve_fireAndWatch and its neighbours in the salsa reference; exemplar 04_pacsQR.ts exercises the full query, retrieve, verify and restore cycle.