Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Meza-dev/Ghostly/llms.txt

Use this file to discover all available pages before exploring further.

@ghostly-io/client is the official TypeScript and JavaScript SDK for the Ghostly REST API. It wraps every API endpoint in a typed method, handles authentication headers automatically, and provides both a polling-based and an SSE-based path for consuming run results. Use it in Node.js scripts, CI pipelines, or any browser environment that can reach your Ghostly instance.

Installation

npm install @ghostly-io/client
The package is published as a pure ES module. It has no runtime dependencies and ships its own TypeScript declaration files.

Creating a client

import { GhostlyClient } from '@ghostly-io/client';

const client = new GhostlyClient({
  baseUrl: 'http://localhost:4000',
  apiKey: 'your-api-key-here',
});
baseUrl
string
required
The base URL of your Ghostly instance, without a trailing slash. For local development this is typically http://localhost:4000.
apiKey
string
required
The API key generated by ghostly keygen. The client sends this automatically as the X-Api-Key request header on every call.

Method reference

startRun(options: RunFlowOptions): Promise<RunStartResponse>Starts a run asynchronously. The API accepts the request and executes the flow in the background. Returns the run id immediately so you can poll or stream results.
const { id } = await client.startRun({
  project: 'my-project-id',
  baseUrl: 'http://localhost:3000',
  steps: [
    { action: 'goto', url: 'http://localhost:3000/login' },
    { action: 'fill', selector: "[data-testid='email']", value: 'user@example.com' },
    { action: 'click', selector: "[data-testid='submit']" },
    { action: 'waitForSelector', selector: "[data-testid='dashboard']" },
  ],
  headless: true,
  captureScreenshotAfterEachStep: true,
  recordVideoOnFailure: true,
});

waitForRun(id: string, opts?: { pollIntervalMs?: number; timeoutMs?: number }): Promise<RunRecord>Polls GET /v1/runs/:id until the run exits the "running" state. The default poll interval is 1 000 ms and the default timeout is 10 minutes.
const result = await client.waitForRun(id, {
  pollIntervalMs: 2000,
  timeoutMs: 120_000,
});

console.log(result.status); // "pass" | "fail"

runFlow(options: RunFlowOptions, opts?: { pollIntervalMs?: number; timeoutMs?: number }): Promise<RunRecord> — deprecatedCombines startRun + waitForRun in a single call. Preserved for backwards compatibility.
runFlow is deprecated. Prefer startRun + waitForRun for polling scenarios, or use the SSE stream for real-time progress.

Type reference

The SDK exports all types needed to build fully-typed integrations.
type GhostlyClientOptions = {
  /** Base URL of the Ghostly server (e.g. http://localhost:4000) */
  baseUrl: string;
  /** API key generated from the Settings dashboard */
  apiKey: string;
};
type RunFlowOptions = {
  baseUrl: string;
  steps: Step[];
  project: string;
  assisted?: AssistedMeta;
  assist?: AssistRunOptions;
  headless?: boolean;
  captureScreenshotAfterEachStep?: boolean;
  recordVideoOnFailure?: boolean;
  artifactsDir?: string;
  defaultTimeoutMs?: number;
};
type AssistRunOptions = {
  v2: true;
  goal: string;
  maxHealingAttemptsPerStep?: number;
  observerMaxNodes?: number;
  victory?: {
    textIncludes?: string[];
    selectorVisible?: string[];
    urlIncludes?: string[];
    mustAll?: boolean;
  };
  maxHorizons?: number;
  stepsPerHorizon?: number;
  maxLoopMs?: number;
  memoryMode?: "off" | "runtime" | "adaptive";
};
type Step =
  | { action: "goto"; url: string }
  | { action: "click"; selector: string }
  | { action: "fill"; selector: string; value: string }
  | { action: "press"; key: string }
  | { action: "waitForSelector"; selector: string; timeoutMs?: number }
  | { action: "snapshot" };
type RunStartResponse = {
  ok: true;
  id: string;
  status: "running";
};

type RunRecord = {
  id: string;
  status: RunStatus;
  startedAt: string;
  durationMs: number;
  baseUrl: string;
  project?: string;
  assisted?: AssistedMeta;
  steps: StepOutcome[];
  videoPath?: string;
  events?: AssistEvent[];
};
type RunStatus = "pass" | "fail" | "running";

type StepOutcome = {
  index: number;
  action: string;
  ok: boolean;
  error?: string;
  screenshotPath?: string;
};
type AssistedMeta = {
  goal: string;
  model: string;
  generatedAt: string;
  promptVersion: string;
  assistConfig?: {
    victory?: {
      textIncludes?: string[];
      selectorVisible?: string[];
      urlIncludes?: string[];
      mustAll?: boolean;
    };
    maxHorizons?: number;
    stepsPerHorizon?: number;
    maxLoopMs?: number;
    memoryMode?: "off" | "runtime" | "adaptive";
  };
};
type AssistEventType =
  | "recon"
  | "plan_chunk"
  | "loop_state"
  | "horizon_start"
  | "horizon_end"
  | "victory_check"
  | "memory_hit"
  | "memory_miss"
  | "step_start"
  | "step_success"
  | "step_failure"
  | "heal_start"
  | "heal_action"
  | "heal_success"
  | "heal_failure"
  | "run_end";

type AssistEvent = {
  seq: number;
  type: AssistEventType;
  at: string;
  stepIndex?: number;
  payload: Record<string, unknown>;
};
type PlanAssistRequest = {
  project: string;
  baseUrl: string;
  goal: string;
  mode?: "v1" | "v2";
};

type PlanAssistResponse = {
  ok: true;
  draft: RunFlowOptions;
  meta: AssistedMeta;
  observer?: ObserverSnapshot;
  mode?: "v1" | "v2";
};
type ObserverSnapshot = {
  url: string;
  title: string;
  capturedAt: string;
  treeMarkdown: string;
  nodeCount: number;
};
type Project = {
  id: string;
  label: string;
  color: string;
  createdAt: string;
};

Complete working example

The following script starts an assisted v2 run, streams live events to the console, and then waits for the final result using polling.
import { GhostlyClient } from '@ghostly-io/client';

const client = new GhostlyClient({
  baseUrl: 'http://localhost:4000',
  apiKey: process.env.GHOST_API_KEY!,
});

async function main() {
  // 1. Generate an AI-assisted plan with accessibility-tree recon
  const plan = await client.planAssistV2({
    project: 'my-project-id',
    baseUrl: 'http://localhost:3000',
    goal: 'Log in with user@example.com / secret and verify the /dashboard route loads',
  });

  console.log('Plan generated:', plan.draft.steps.length, 'steps');

  // 2. Start the run asynchronously
  const { id } = await client.startRun({
    ...plan.draft,
    captureScreenshotAfterEachStep: true,
    recordVideoOnFailure: true,
  });

  console.log('Run started:', id);

  // 3. Stream live events (browser / environments with EventSource)
  // const url = client.runEventsUrl(id, myJwtToken);
  // const source = new EventSource(url);
  // source.onmessage = (e) => console.log(JSON.parse(e.data));

  // 4. Poll for completion (Node.js / CI environments)
  const result = await client.waitForRun(id, {
    pollIntervalMs: 1500,
    timeoutMs: 300_000,
  });

  console.log('Status:', result.status);        // "pass" or "fail"
  console.log('Duration:', result.durationMs, 'ms');

  if (result.status === 'fail') {
    const failed = result.steps.filter((s) => !s.ok);
    for (const step of failed) {
      console.error(`Step ${step.index} (${step.action}) failed:`, step.error);
    }
    process.exit(1);
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Authentication details

The client sets the X-Api-Key header automatically on every request using the apiKey value you provide to the constructor. You do not need to manage headers manually.
If you need to use a JWT token instead of an API key, you can pass it as apiKey. The MCP server’s authHeader helper detects the token format automatically (JWT tokens receive an Authorization: Bearer header; plain keys receive X-Api-Key). The GhostlyClient always uses X-Api-Key regardless of format.

Build docs developers (and LLMs) love