Pilot

Pilot JavaScript SDK

PilotClient for TypeScript and Node.js — run playbooks, manage runs, and stream events programmatically.

Pilot JavaScript SDK

@cuitty/pilot-sdk is the TypeScript/Node.js client for the Pilot API. It provides typed methods for managing playbooks, starting runs, streaming real-time events, and listing providers.

Install

sfw bun add @cuitty/pilot-sdk
# or
sfw npm install @cuitty/pilot-sdk

Quick start

import { PilotClient } from "@cuitty/pilot-sdk";

const pilot = new PilotClient({
  baseUrl: "http://localhost:4320",
  auth: { apiKey: process.env.PILOT_API_KEY },
});

// List all playbooks
const playbooks = await pilot.playbooks.list();

// Start a run
const { id } = await pilot.runs.start({
  playbookId: "cloudflare.dns.add-a-record",
  mode: "autonomous",
  inputs: { zone: "example.com", record_name: "app", ip_address: "1.2.3.4" },
});

// Stream events until completion
for await (const event of pilot.runs.stream(id)) {
  console.log(event.type, event.stepId, event.status);
}

// Clean up
pilot.close();

PilotClient

The main entry point. Creates service instances and manages shared configuration.

interface PilotClientConfig {
  baseUrl: string;            // Pilot server URL
  auth?: {
    token?: string;           // Bearer token
    cookie?: string;          // Raw cookie string
    apiKey?: string;          // X-Pilot-API-Key header
  };
  timeout?: number;           // Per-request timeout in ms (default 30000)
}

const pilot = new PilotClient(config);
PropertyTypeDescription
pilot.playbooksPlaybookServicePlaybook CRUD
pilot.runsRunServiceRun lifecycle + streaming
pilot.providersProviderServiceProvider profile listing
MethodReturnsDescription
pilot.health()Promise<HealthStatus>Check server health
pilot.close()voidClose all active event subscriptions

PlaybookService

// List all playbooks, optionally filtered by target
const all = await pilot.playbooks.list();
const cfOnly = await pilot.playbooks.list({ target: "cloudflare" });

// Get a single playbook (latest version)
const pb = await pilot.playbooks.get("cloudflare.dns.add-a-record");

// Create a new playbook
const { id, version } = await pilot.playbooks.create({
  target: "cloudflare",
  title: "Add CNAME record",
  yaml: playbookYaml,
  authoredBy: "human",
});

// Update (bumps version)
await pilot.playbooks.update("cloudflare.dns.add-cname", {
  yaml: updatedYaml,
});

// Delete all versions
await pilot.playbooks.delete("cloudflare.dns.add-cname");
MethodParamsReturns
list(options?){ target?: string }PlaybookSummary[]
get(id)playbook idPlaybook
create(req)CreatePlaybookRequest{ id, version, created }
update(id, req)id + UpdatePlaybookRequest{ id, version }
delete(id)playbook id{ deleted }

RunService

// Start a run
const { id, status } = await pilot.runs.start({
  playbookId: "cloudflare.dns.add-a-record",
  mode: "interactive",         // "autonomous" | "interactive" | "review"
  inputs: { zone: "example.com", record_name: "api", ip_address: "1.2.3.4" },
  providerProfileId: "cf-prod",
});

// Get run details
const run = await pilot.runs.get(id);

// List runs with filters
const recent = await pilot.runs.list({ status: "running", limit: 10 });

// Abort a running run
await pilot.runs.abort(id);

// Approve a step awaiting review
await pilot.runs.approve(id, {
  stepId: "save",
  decision: "approve",         // "approve" | "skip" | "abort"
});
MethodReturns
start(req){ id, status }
get(id)Run (includes steps array)
list(options?)Run[]
abort(id){ aborted, runId }
approve(id, req){ approved, runId, stepId, decision }

Event streaming

Two patterns for consuming real-time run events via SSE:

Async iterator

for await (const event of pilot.runs.stream(runId)) {
  switch (event.type) {
    case "step.start":
      console.log(`Step ${event.stepId} started`);
      break;
    case "step.complete":
      console.log(`Step ${event.stepId} done in ${event.durationMs}ms`);
      break;
    case "step.failed":
      console.error(`Step ${event.stepId} failed: ${event.error}`);
      break;
    case "run.completed":
      console.log("Run finished", event.outputs);
      break;
  }
}

The iterator yields events until the run reaches a terminal state (run.completed, run.failed, or run.aborted).

Callback subscription

const unsubscribe = pilot.runs.subscribe(runId, {
  onStep: (event) => console.log("step", event.stepId, event.status),
  onComplete: (event) => console.log("done", event.outputs),
  onError: (event) => console.error("error", event.error),
  onEvent: (event) => {}, // every event
});

// Later: close the SSE connection
unsubscribe();

Event types

TypeFieldsWhen
step.startstepId, actionStep begins executing
step.completestepId, action, durationMs, aiUsedStep succeeded
step.failedstepId, action, errorStep errored
step.awaiting_approvalstepId, actionStep paused at breakpoint
run.completedoutputsAll steps finished
run.failederrorRun failed
run.abortedRun was aborted

ProviderService

const providers = await pilot.providers.list();
// Returns: Provider[]
// { id, projectId, provider, label, baseUrl, lastAuthenticated, expiresAt }

Error handling

The SDK throws typed errors:

import { PilotApiError, PilotConnectionError } from "@cuitty/pilot-sdk";

try {
  await pilot.runs.start({ playbookId: "nonexistent" });
} catch (err) {
  if (err instanceof PilotApiError) {
    console.error(`API ${err.status}: ${err.statusText}`, err.body);
  } else if (err instanceof PilotConnectionError) {
    console.error("Cannot reach Pilot:", err.message);
  }
}
Error classPropertiesWhen
PilotApiErrorstatus, statusText, bodyNon-2xx HTTP response
PilotConnectionErrormessage, causeNetwork / timeout failure

See also