npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@strait/ts

v0.1.2

Published

TypeScript SDK for the Strait platform API with full feature parity across all five Strait SDKs.

Downloads

44

Readme

@strait/ts

TypeScript SDK for the Strait platform API with full feature parity across all five Strait SDKs.

Install

npm install @strait/ts

Or with Bun:

bun add @strait/ts

Quick Start

import { createClient } from "@strait/ts";

const client = createClient({
  baseUrl: "https://api.strait.dev",
  auth: { type: "bearer", token: "sk_live_..." },
});

// List jobs
const jobs = await client.jobs.list({});

// Trigger a job
const run = await client.triggerJob({
  pathParams: { jobID: "job_abc" },
  body: { payload: { sku: "ABC-123" } },
});

console.log("Run ID:", run.id);

Configuration

From strait.json (recommended)

Create a strait.json at your project root:

{
  "$schema": "https://strait.dev/schema.json",
  "project": {
    "id": "proj_abc123",
    "name": "My Project"
  },
  "sdk": {
    "base_url": "https://api.strait.dev",
    "auth_type": "apiKey",
    "timeout_ms": 30000
  }
}

Then load the client from it:

import { createClientFromConfigFile } from "@strait/ts/node";

// Reads strait.json from working directory + STRAIT_API_KEY from env
const client = await createClientFromConfigFile();

// Or specify a custom directory
const client = await createClientFromConfigFile({ cwd: "/path/to/project" });

// Or an explicit file path
const client = await createClientFromConfigFile({ configPath: "/path/to/custom-config.json" });

The SDK reads the sdk section from the file. Auth tokens are never read from the file — they always come from the STRAIT_API_KEY environment variable.

From environment variables

import { createClientFromEnv } from "@strait/ts/node";

// Reads STRAIT_BASE_URL, STRAIT_API_KEY, STRAIT_AUTH_TYPE, STRAIT_TIMEOUT_MS
const client = createClientFromEnv();

Inline

import { createClient } from "@strait/ts";

const client = createClient({
  baseUrl: "https://api.strait.dev",
  auth: { type: "bearer", token: "sk_live_..." },
  timeoutMs: 5000,
  defaultHeaders: { "X-Custom": "value" },
});

Environment variable override precedence

Environment variables always take precedence over strait.json values:

| strait.json field | Env var | Wins | |---|---|---| | sdk.base_url | STRAIT_BASE_URL | env var | | sdk.auth_type | STRAIT_AUTH_TYPE | env var | | sdk.timeout_ms | STRAIT_TIMEOUT_MS | env var | | (not in file) | STRAIT_API_KEY | env var (only source) |

Authoring DSL

import { defineJob, zodSchema } from "@strait/ts";
import { z } from "zod";

const job = defineJob({
  name: "Sync Inventory",
  slug: "sync-inventory",
  endpointUrl: "https://worker.dev/jobs/sync",
  projectId: "proj_1",
  schema: z.object({ sku: z.string() }),
  maxConcurrency: 5,
  maxAttempts: 3,
  retryStrategy: "exponential",
  timeoutSecs: 300,
  run: async (payload, ctx) => {
    ctx.logger.info("Starting sync", { sku: payload.sku });
    const result = await syncInventory(payload.sku);
    await ctx.reportProgress(100, "Done");
    return result;
  },
  onSuccess: async ({ payload, output, ctx }) => {
    ctx.logger.info("Sync complete", { sku: payload.sku });
  },
  onFailure: async ({ payload, error, ctx }) => {
    ctx.logger.error("Sync failed", { sku: payload.sku });
  },
});

// Register and trigger
await job.register(client, { projectId: "proj_1" });
await job.trigger(client, { payload: { sku: "ABC-123" } });

// Trigger and wait for completion
const run = await job.triggerAndWait(client, { payload: { sku: "ABC-123" } });

// Batch trigger
await job.batchTrigger(client, {
  items: [
    { payload: { sku: "ABC-123" } },
    { payload: { sku: "DEF-456" }, priority: 10 },
  ],
});

Schema adapters

The SDK supports multiple schema libraries. You can pass a Standard Schema v1 compliant object directly (Zod 3.24+, Valibot 1.0+, ArkType 2.0+), or use an explicit adapter:

import { zodSchema, effectSchema, standardSchema, customSchema } from "@strait/ts";

// Zod (auto-detected or explicit)
defineJob({ schema: z.object({ sku: z.string() }), ... });
defineJob({ schema: zodSchema(myZodSchema), ... });

// Effect Schema
defineJob({ schema: effectSchema(Schema.Struct({ sku: Schema.String })), ... });

// Any Standard Schema v1
defineJob({ schema: standardSchema(myValibotSchema), ... });

// Custom validation function
defineJob({ schema: customSchema((input) => validate(input)), ... });

Workflow DAG

import { defineWorkflow, step } from "@strait/ts";

const pipeline = defineWorkflow({
  name: "Order Pipeline",
  slug: "order-pipeline",
  projectId: "proj_1",
  schema: z.object({ orderId: z.string() }),
  maxConcurrentRuns: 10,
  maxParallelSteps: 3,
  steps: [
    step.job("validate", "job_validate"),
    step.job("charge", "job_charge", {
      dependsOn: ["validate"],
      onFailure: "fail_workflow",
      retryMaxAttempts: 3,
    }),
    step.approval("review", {
      dependsOn: ["charge"],
      approvalTimeoutSecs: 3600,
    }),
    step.waitForEvent("confirm", "shipping.confirmed", {
      dependsOn: ["review"],
      eventTimeoutSecs: 86400,
    }),
    step.sleep("cooldown", 60, { dependsOn: ["confirm"] }),
    step.subWorkflow("notify", "wf_notifications", { dependsOn: ["cooldown"] }),
  ],
});

// Register and trigger
await pipeline.register(client);
await pipeline.trigger(client, { payload: { orderId: "ord_123" } });

AI step builder

Use step.ai() to add an AI-powered step to a workflow DAG. It applies sensible defaults for long-running AI work: 600s timeout, 5 retries with exponential backoff, and the large resource class.

const steps = [
  step.job("extract", "job_extract"),
  step.ai("summarize", "job_summarize", { dependsOn: ["extract"] }),
];

DAG definitions

defineDag() is an alias for defineWorkflow() that brands the definition as dag:

import { defineDag, step } from "@strait/ts";

const dag = defineDag({
  name: "ETL Pipeline",
  slug: "etl-pipeline",
  projectId: "proj_1",
  schema: z.object({ source: z.string() }),
  steps: [
    step.job("extract", "job_extract"),
    step.job("transform", "job_transform", { dependsOn: ["extract"] }),
    step.job("load", "job_load", { dependsOn: ["transform"] }),
  ],
});

Durable AI Agents

defineAgent() creates a durable, cost-aware agent that can run across multiple iterations and survive restarts.

import { defineAgent } from "@strait/ts";

const agent = defineAgent({
  name: "Research Agent",
  slug: "research-agent",
  endpointUrl: "https://worker.dev/agents/research",
  projectId: "proj_1",
  schema: z.object({ topic: z.string() }),
  maxCostMicrousd: 5_000_000,
  run: async (payload, ctx) => {
    while (!ctx.isBudgetExceeded()) {
      const result = await doResearch(payload.topic);
      await ctx.checkpoint({ lastResult: result });
      await ctx.reportUsage?.({
        provider: "openai",
        model: "gpt-4o",
        costMicrousd: result.cost,
      });
    }
    return { summary: "done" };
  },
});

AgentRunContext exposes:

| Property / Method | Description | |---|---| | ctx.iteration | Current iteration index (auto-incremented on checkpoint) | | ctx.accumulatedCostMicrousd() | Total cost so far in micro-USD | | ctx.isBudgetExceeded() | Whether the cost budget has been exceeded |

Defaults applied by defineAgent(): strait.kind=agent tag, 600s timeout, 5 max attempts, exponential retry strategy.

AI SDK Integration

The SDK integrates with Vercel AI SDK v6 for automatic usage reporting, tool call logging, and streaming.

import { createStraitAgent, createStraitProvider } from "@strait/ts/ai";
import { openai } from "@ai-sdk/openai";

// Middleware approach — wrap any model
const middleware = createStraitProvider(ctx, {
  providerName: "openai",
  reportUsage: true,
  logToolCalls: true,
  streamToStrait: true,
});

// Agent approach — full ToolLoopAgent with Strait tools
const agent = createStraitAgent(ctx, {
  model: openai("gpt-4o"),
  instructions: "You are a helpful assistant.",
  tools: myTools,
  straitTools: { checkpoint: true, spawn: true, saveOutput: true },
});

Built-in Strait tools (automatically available to the agent):

| Tool | Description | |---|---| | strait_checkpoint | Save agent state for durable execution | | strait_spawn | Spawn a child job run | | strait_save_output | Save a structured output | | strait_wait_for_event | Pause and wait for an external event | | strait_state_get | Read from durable KV state | | strait_state_set | Write to durable KV state | | strait_complete | Mark the run as completed |

Event Definitions

defineEvent() declares a typed event with schema validation:

import { defineEvent } from "@strait/ts";

const approvalEvent = defineEvent(
  "approval.granted",
  z.object({ approvedBy: z.string(), orderId: z.string() })
);

const parsed = await approvalEvent.parse(rawData);

Extended RunContext

RunContext is the execution context passed to every job and agent handler. It exposes async methods for interacting with the Strait runtime during a run.

import { createRunContextFromClient } from "@strait/ts";

const ctx = createRunContextFromClient(client, "run_123", { attempt: 1 });
await ctx.state?.set("key", value);

| Method | Description | |---|---| | ctx.checkpoint(state) | Persist intermediate state for resume | | ctx.reportProgress(percent, message) | Report progress (0-100) | | ctx.heartbeat() | Keep the run alive | | ctx.reportUsage(metrics) | Report LLM token/cost usage | | ctx.logToolCall(name, input, output) | Log an external tool invocation | | ctx.saveOutput(key, value, schema) | Save a named output artifact | | ctx.streamChunk(chunk, options) | Stream incremental output | | ctx.waitForEvent(eventKey, options) | Pause until an external event fires | | ctx.spawn(options) | Spawn a child run | | ctx.continue(payload) | Continue to the next iteration | | ctx.annotate(annotations) | Attach key-value annotations | | ctx.complete(result) | Mark the run as completed | | ctx.fail(error) | Mark the run as failed | | ctx.state.get(key) | Read from the run's KV store | | ctx.state.set(key, value) | Write to the run's KV store | | ctx.state.delete(key) | Delete from the run's KV store | | ctx.state.list() | List all keys in the run's KV store | | ctx.logger | Structured logger (info, warn, error) forwarded to Strait |

Test Harness

createTestContext() returns an in-memory RunContext and a TestRunRecord that captures every side-effect, so you can unit-test handlers without a running platform.

import { createTestContext } from "@strait/ts";

const { ctx, record } = createTestContext("test-run");
await myJobHandler(payload, ctx);

expect(record.checkpoints).toHaveLength(1);
expect(record.completed).toBe(true);
expect(record.stateStore.get("key")).toBe("expected");

TestRunRecord captures everything the handler did:

| Field | Type | Description | |---|---|---| | record.checkpoints | Record[] | States passed to checkpoint() | | record.logs | { level, message, data }[] | Log entries | | record.usageReports | { provider, model, ... }[] | Usage reports | | record.toolCalls | { toolName, input, output, ... }[] | Tool call logs | | record.outputs | { key, value }[] | Saved outputs | | record.progressUpdates | { percent, message }[] | Progress updates | | record.stateStore | Map<string, unknown> | Final KV state | | record.streamChunks | { chunk, streamId, done }[] | Streamed chunks | | record.heartbeats | number | Heartbeat count | | record.spawns | { jobSlug, projectId, payload }[] | Spawned child runs | | record.events | { eventKey, timeoutSecs, ... }[] | Events waited on | | record.annotations | Record<string, string>[] | Annotations | | record.continuations | { payload }[] | Continuations | | record.completed | boolean | Whether complete() was called | | record.failed | boolean | Whether fail() was called | | record.failError | string \| undefined | Error message from fail() | | record.result | Record \| undefined | Result passed to complete() |

Composition Helpers

import {
  withRetry,
  waitForRun,
  paginate,
  collectAll,
  triggerAndWait,
  withIdempotency,
} from "@strait/ts";

// Retry with exponential backoff
const result = await withRetry(() => client.triggerJob({ ... }), {
  attempts: 5,
  delayMs: 250,
  factor: 2,
  jitter: "full",
});

// Wait for a run to complete
const run = await waitForRun(
  (input) => client.getRun(input),
  "run_123",
  { timeoutMs: 60_000 },
);

// Paginate through results
for await (const job of paginate((q) => client.listJobs({ query: q }))) {
  console.log(job.id);
}

// Or collect all into an array
const allJobs = await collectAll(paginate((q) => client.listJobs({ query: q })));

// Trigger and wait combined
const finalRun = await triggerAndWait(
  (input) => client.triggerJob({ pathParams: { jobID: "job_1" }, body: input }),
  (input) => client.getRun(input),
  { payload: { sku: "ABC-123" } },
);

// Idempotency header
await client.triggerJob(withIdempotency(
  { pathParams: { jobID: "job_1" }, body: { payload: { sku: "ABC-123" } } },
  "unique-key-123",
));

Cost budget

createCostTracker() tracks accumulated cost across retries and iterations. Use withCostBudget() to automatically abort when the budget is exceeded.

import { createCostTracker, withCostBudget } from "@strait/ts";

const tracker = createCostTracker({
  maxCostMicrousd: 5_000_000,
  warningThreshold: 0.8,
  onWarning: (current, max) => console.warn(`Cost warning: ${current}/${max}`),
});

const result = await withCostBudget(async (t) => {
  t.add(200_000);
  return await callExpensiveModel();
}, { maxCostMicrousd: 5_000_000 });

Checkpoint resume

withCheckpointResume() wraps a function so it can resume from the last checkpoint after a crash or restart.

import { withCheckpointResume } from "@strait/ts";

const result = await withCheckpointResume(
  ctx,
  lastCheckpoint,
  async (state, update) => {
    for (let i = state.nextIndex; i < 100; i++) {
      await doStep(i);
      update({ nextIndex: i + 1 });
    }
    return "done";
  },
  { initialState: { nextIndex: 0 }, checkpointInterval: 5 },
);

Deployment lifecycle

Compose deployment operations into atomic workflows:

import {
  createAndFinalizeDeployment,
  createFinalizePromoteDeployment,
} from "@strait/ts";

// Create + finalize
const { created, finalized } = await createAndFinalizeDeployment(client, {
  create: {
    body: {
      project_id: "proj_1",
      environment: "staging",
      runtime: "node",
      artifact_uri: "s3://bucket/build.tar.gz",
    },
  },
});

// Create + finalize + promote (full deploy)
const { promoted } = await createFinalizePromoteDeployment(client, {
  create: {
    body: {
      project_id: "proj_1",
      environment: "production",
      runtime: "node",
      artifact_uri: "s3://bucket/build.tar.gz",
    },
  },
});

FSM State Machines

import { canTransitionRun, isTerminalRunStatus } from "@strait/ts/fsm";

canTransitionRun("executing", "COMPLETE"); // true
isTerminalRunStatus("completed");           // true

XState machines are available for programmatic use:

import { createActor } from "xstate";
import { runMachine, workflowRunMachine, stepRunMachine } from "@strait/ts/fsm";

const actor = createActor(runMachine);
actor.start();
actor.send({ type: "ENQUEUE" });
actor.send({ type: "DEQUEUE" });
actor.send({ type: "EXECUTE" });
actor.getSnapshot().value; // "executing"

Middleware

const client = createClient(
  {
    baseUrl: "https://api.strait.dev",
    auth: { type: "bearer", token: "sk_live_..." },
  },
  {
    middleware: [
      {
        onRequest: ({ method, url }) => console.log(`-> ${method} ${url}`),
        onResponse: ({ status, durationMs }) => console.log(`<- ${status} (${durationMs}ms)`),
        onError: ({ error }) => console.error("Error:", error),
      },
    ],
  }
);

Custom Fetch

Pass a custom fetch implementation for testing or custom HTTP behavior:

const client = createClient(
  {
    baseUrl: "https://api.strait.dev",
    auth: { type: "bearer", token: "sk_live_..." },
  },
  { fetch: myCustomFetch }
);

Error Handling

All errors are typed using Effect's Data.TaggedError. Use the _tag field to match specific error kinds:

import { createClient } from "@strait/ts";

try {
  await client.getRun({ pathParams: { runID: "nonexistent" } });
} catch (error) {
  switch (error._tag) {
    case "NotFoundError":
      console.log("Not found:", error.message);
      break;
    case "UnauthorizedError":
      console.log("Auth error:", error.message);
      break;
    case "RateLimitedError":
      console.log("Rate limited:", error.message);
      break;
    default:
      console.log("Error:", error);
  }
}

Or use Result variants for non-throwing error handling:

const result = await client.triggerJobResult({
  pathParams: { jobID: "job_1" },
  body: { payload: { sku: "ABC-123" } },
});

if (result.ok) {
  console.log("Run:", result.output.id);
} else {
  console.log("Error:", result.error);
}

| Error type | HTTP status | Description | |---|---|---| | TransportError | -- | Network/transport failure | | DecodeError | -- | JSON decode / schema validation failure | | ValidationError | -- | Config or input validation | | UnauthorizedError | 401, 403 | Authentication failure | | NotFoundError | 404 | Resource not found | | ConflictError | 409 | Conflict (duplicate, etc.) | | RateLimitedError | 429 | Rate limit exceeded | | ApiError | other | Generic HTTP error | | TimeoutError | -- | Polling timeout | | DagValidationError | -- | Workflow DAG is invalid | | CostBudgetExceededError | -- | Cost budget exceeded |

Exports

| Import path | Description | |---|---| | @strait/ts | Client, config, errors, authoring DSL, composition helpers, schema adapters | | @strait/ts/node | Node.js config file discovery, env loading, createClientFromConfigFile, createClientFromEnv | | @strait/ts/fsm | XState run, workflow, step state machines | | @strait/ts/ai | AI SDK v6 provider middleware, agent, tools |

Development

bun install
bun test
bun run typecheck
bun run lint