clawdforge/clients/typescript
Kayos 15de6e765f clients/typescript: initial TypeScript SDK for clawdforge
Drop-in Node 18+ client with strict types, native fetch, AbortSignal
support, and a typed error hierarchy (ForgeAuthError / ForgeAPIError /
ForgeTransportError). Mirrors the existing Python client surface but
stays generic — no Sulkta-specific assumptions, suitable for anyone
running their own clawdforge instance.

- camelCase TS, snake_case wire — converted at the boundary
- node:test suite (17 tests) covering healthz, run success/error paths,
  502 envelopes, abort/timeout, file upload, and admin token CRUD
- tsc --noEmit clean with strict mode + Node16 module resolution
2026-04-28 22:42:46 -07:00
..
examples clients/typescript: initial TypeScript SDK for clawdforge 2026-04-28 22:42:46 -07:00
src clients/typescript: initial TypeScript SDK for clawdforge 2026-04-28 22:42:46 -07:00
tests clients/typescript: initial TypeScript SDK for clawdforge 2026-04-28 22:42:46 -07:00
.gitignore clients/typescript: initial TypeScript SDK for clawdforge 2026-04-28 22:42:46 -07:00
package.json clients/typescript: initial TypeScript SDK for clawdforge 2026-04-28 22:42:46 -07:00
README.md clients/typescript: initial TypeScript SDK for clawdforge 2026-04-28 22:42:46 -07:00
tsconfig.json clients/typescript: initial TypeScript SDK for clawdforge 2026-04-28 22:42:46 -07:00

clawdforge — TypeScript SDK

A small, dependency-free TypeScript / Node.js SDK for clawdforge: a LAN-only HTTP service that wraps claude -p subprocess calls behind a bearer-token-gated REST API.

  • Node 18+ — uses native fetch and AbortController
  • No runtime dependenciestypescript is a dev dep only
  • Strict TypeScript — public types are explicit, result is unknown so you narrow it where you know the shape
  • camelCase TS / snake_case wire — converted at the boundary, you never see timeout_secs or duration_ms in your code
  • Typed errorsForgeAuthError, ForgeAPIError, ForgeTransportError
  • Cancellable — every method takes an optional signal: AbortSignal

Install

For now, the SDK is consumed straight from this repo subtree (no npm publish):

# from your app's repo
npm install file:/path/to/clawdforge/clients/typescript

Or, if you check the clawdforge repo out alongside your app:

npm install file:../clawdforge/clients/typescript

You can also point at a Git URL, since package.json lives at this path:

npm install "git+https://your-git-host/your-org/clawdforge.git#main:clients/typescript"

Quickstart

import { Forge, ForgeAPIError, ForgeError } from "clawdforge";

const forge = new Forge({
  baseUrl: "http://localhost:8800",
  token: process.env.CLAWDFORGE_TOKEN!, // a per-app `cf_...` token
});

const h = await forge.healthz();
console.log(h.claudeVersion); // "1.2.3 (Claude Code)" (or null)

const r = await forge.run({
  prompt: 'Reply with JSON: {"hello":"world"}',
  model: "sonnet",
  system: "Be terse.",
  timeoutSecs: 60,
});
console.log(r.result, r.durationMs, r.stopReason);

// Narrow the result if you trust the prompt:
type Hello = { hello: string };
const data = r.result as Hello;
console.log(data.hello);

API

All methods are async and return Promises.

new Forge(options: ForgeOptions)

field type default notes
baseUrl string required trailing slashes are stripped
token string required per-app cf_... for /run+/files, admin for /admin/*
defaultTimeoutMs number 120000 client-side network timeout; 0 disables
fetch typeof fetch globalThis.fetch inject a custom fetch (testing, proxies, etc.)

forge.healthz(): Promise<HealthzResponse>

{ ok: boolean; claudePresent: boolean; claudeVersion: string | null }

forge.run(req: RunRequest): Promise<RunResult>

interface RunRequest {
  prompt: string;          // required, non-empty
  model?: string;          // e.g. "sonnet"
  system?: string;         // append-system-prompt
  files?: string[];        // file_tokens from uploadFile()
  timeoutSecs?: number;    // 5..600, server-side subprocess timeout
  signal?: AbortSignal;    // cancel the request
}

interface RunResult {
  ok: true;
  result: unknown;         // narrow at call site (object or string)
  durationMs: number;
  stopReason: string | null;  // typically "end_turn"
}

On a server-reported subprocess failure (HTTP 502), the SDK throws a ForgeAPIError whose body is a RunErrorBody:

interface RunErrorBody {
  ok: false;
  error: string | null;
  stderr: string;
  durationMs: number;
  stopReason: string | null;  // "timeout" | "error" | ...
}

forge.uploadFile(source, opts?): Promise<FileToken>

const ft = await forge.uploadFile("./recipe.png", { ttlSecs: 3600 });

await forge.run({
  prompt: "extract recipe data",
  files: [ft.fileToken],
});

source can be a string path, a Uint8Array / Buffer, or a Blob. Pass filename and/or contentType in opts to override defaults.

interface FileToken {
  fileToken: string;   // "ff_..."
  ttlSecs: number;     // 60..86400, default 3600
  size: number;
}

Admin: createToken, listTokens, revokeToken

These require an admin-bootstrap token in the Forge constructor, not a per-app token.

const admin = new Forge({ baseUrl, token: process.env.CLAWDFORGE_ADMIN! });

const minted = await admin.createToken({
  name: "my-app",
  ipCidrs: ["10.0.0.0/8"],
});
console.log(minted.token); // shown ONCE — save it now

for (const t of await admin.listTokens()) {
  console.log(t.name, t.enabled, t.lastUsed);
}

await admin.revokeToken("my-app"); // returns true; false if not found

Errors

All SDK errors extend ForgeError. Catch the leaf you care about, or the base for blanket handling:

import {
  ForgeError,
  ForgeAuthError,
  ForgeAPIError,
  ForgeTransportError,
} from "clawdforge";

try {
  await forge.run({ prompt: "..." });
} catch (e) {
  if (e instanceof ForgeAuthError) {
    // 401 / 403 — bearer wrong/revoked, or IP not allow-listed
  } else if (e instanceof ForgeAPIError) {
    // any other 4xx/5xx; e.status, e.body
    if (e.status === 502) {
      // subprocess failure: e.body is RunErrorBody
    }
  } else if (e instanceof ForgeTransportError) {
    // network failure, DNS, TLS, abort, or timeout
    if (e.aborted) {
      // either caller's AbortSignal fired or the SDK's defaultTimeoutMs did
    }
  } else if (e instanceof ForgeError) {
    // input-validation errors thrown before the network
  }
}

Cancellation

Every method accepts an optional signal: AbortSignal. On abort the SDK throws ForgeTransportError with aborted: true:

const ac = new AbortController();
const p = forge.run({ prompt: "long task", signal: ac.signal });
setTimeout(() => ac.abort(), 5_000);

The SDK also enforces a client-side defaultTimeoutMs (120s by default). For run(), when you pass timeoutSecs, the network timeout is automatically set to timeoutSecs * 1000 + 30_000, so the HTTP socket outlives the server-side subprocess timeout — that way the server gets a chance to send back a clean 502 envelope rather than the client tearing down first.

Develop

cd clients/typescript
npm install
npm run typecheck   # tsc --noEmit
npm test            # node --test --import tsx tests/*.test.ts
npm run build       # emit dist/

The test suite uses node:test with a mock fetch injected via the fetch constructor option — no live server needed.

License

MIT (see repo root).