npm package: @perstack/api-client
Description: Perstack API Client
Author: Wintermute Technologies, Inc.
Latest version: 0.0.59

# @perstack/api-client

Official TypeScript API client for the Perstack platform.

## Installation

```bash
npm install @perstack/api-client
```

## Quick Start

```typescript
import { createApiClient } from "@perstack/api-client"

const client = createApiClient({
  apiKey: "your-api-key",
})

// List applications
const result = await client.applications.list()
if (result.ok) {
  console.log(result.data.data.applications)
} else {
  console.error(result.error.message)
}
```

## Configuration

The `createApiClient` function accepts a configuration object:

```typescript
interface ApiClientConfig {
  apiKey: string       // Required: Your Perstack API key
  baseUrl?: string     // Optional: API base URL (default: "https://api.perstack.ai")
  timeout?: number     // Optional: Request timeout in milliseconds (default: 30000)
}
```

## API Reference

The client provides access to four main API modules:

- `client.applications` - Application management
- `client.env` - Environment configuration (secrets and variables)
- `client.jobs` - Job execution and monitoring
- `client.experts` - Expert definitions and versioning

### Result Type

All API methods return a discriminated union type for type-safe error handling:

```typescript
type ApiResult<T> =
  | { ok: true; status: number; headers: Headers; data: T }
  | { ok: false; status: number; headers: Headers; error: ApiError }

interface ApiError {
  type: ApiErrorType  // "http" | "network" | "timeout" | "validation" | "abort"
  message: string     // Error message
  reason?: unknown    // Additional error details
  cause?: Error       // Original error (for network errors)
}
```

### Request Options

All API methods accept an optional `RequestOptions` object:

```typescript
interface RequestOptions {
  signal?: AbortSignal  // AbortController signal for cancellation
}
```

For streaming endpoints, an extended `StreamRequestOptions` is available:

```typescript
interface StreamRequestOptions extends RequestOptions {
  streamIdleTimeout?: number  // Idle timeout in ms between chunks (default: client timeout)
}
```

---

### Applications API

Manage applications within your organization.

#### `client.applications.list(params?, options?)`

List all applications with optional filtering and pagination.

```typescript
const result = await client.applications.list({
  name: "my-app",           // Filter by name
  sort: "createdAt",        // Sort by: "name" | "createdAt" | "updatedAt"
  order: "desc",            // Order: "asc" | "desc"
  take: 10,                 // Number of results
  skip: 0,                  // Offset for pagination
})
```

#### `client.applications.get(id, options?)`

Get a single application by ID.

```typescript
const result = await client.applications.get("app-id")
if (result.ok) {
  console.log(result.data.data.application)
}
```

#### `client.applications.create(input, options?)`

Create a new application.

```typescript
const result = await client.applications.create({
  name: "My Application",
  applicationGroupId: "group-id",  // Optional
})
```

#### `client.applications.update(id, input, options?)`

Update an existing application.

```typescript
const result = await client.applications.update("app-id", {
  name: "Updated Name",
  status: "active",  // "active" | "inactive"
})
```

#### `client.applications.delete(id, options?)`

Delete an application.

```typescript
const result = await client.applications.delete("app-id")
```

---

### Environment API

Manage secrets and environment variables.

#### Secrets

##### `client.env.secrets.list(options?)`

List all secrets.

```typescript
const result = await client.env.secrets.list()
if (result.ok) {
  for (const secret of result.data.data.secrets) {
    console.log(secret.name)  // Secret values are not returned
  }
}
```

##### `client.env.secrets.get(name, options?)`

Get a secret by name.

```typescript
const result = await client.env.secrets.get("API_KEY")
```

##### `client.env.secrets.create(input, options?)`

Create a new secret.

```typescript
const result = await client.env.secrets.create({
  name: "API_KEY",
  value: "secret-value",
})
```

##### `client.env.secrets.update(name, input, options?)`

Update an existing secret.

```typescript
const result = await client.env.secrets.update("API_KEY", {
  value: "new-secret-value",
})
```

##### `client.env.secrets.delete(name, options?)`

Delete a secret.

```typescript
const result = await client.env.secrets.delete("API_KEY")
```

#### Variables

##### `client.env.variables.list(options?)`

List all environment variables.

```typescript
const result = await client.env.variables.list()
```

##### `client.env.variables.get(name, options?)`

Get a variable by name.

```typescript
const result = await client.env.variables.get("DATABASE_URL")
```

##### `client.env.variables.create(input, options?)`

Create a new variable.

```typescript
const result = await client.env.variables.create({
  name: "DATABASE_URL",
  value: "postgres://...",
})
```

##### `client.env.variables.update(name, input, options?)`

Update an existing variable.

```typescript
const result = await client.env.variables.update("DATABASE_URL", {
  value: "postgres://new-url...",
})
```

##### `client.env.variables.delete(name, options?)`

Delete a variable.

```typescript
const result = await client.env.variables.delete("DATABASE_URL")
```

---

### Jobs API

Execute and monitor expert jobs.

#### `client.jobs.list(params?, options?)`

List jobs with optional filtering and pagination.

```typescript
const result = await client.jobs.list({
  take: 20,
  skip: 0,
  sort: "createdAt",
  order: "desc",
  filter: "status:running",
})
```

#### `client.jobs.get(id, options?)`

Get a job by ID.

```typescript
const result = await client.jobs.get("job-id")
if (result.ok) {
  console.log(result.data.data.job.status)
}
```

#### `client.jobs.start(input, options?)`

Start a new job.

```typescript
const result = await client.jobs.start({
  applicationId: "app-id",
  expertKey: "@org/expert@1.0.0",
  query: "Help me with this task",
  files: ["file1.txt", "file2.txt"],  // Optional
  provider: "anthropic",
  model: "claude-sonnet-4-20250514",          // Optional
  reasoningBudget: "medium",          // Optional: "low" | "medium" | "high"
  maxRetries: 3,                      // Optional
})
```

#### `client.jobs.update(id, input, options?)`

Update a job's status.

```typescript
const result = await client.jobs.update("job-id", {
  status: "paused",
})
```

#### `client.jobs.continue(id, input, options?)`

Continue a paused job with additional input.

```typescript
const result = await client.jobs.continue("job-id", {
  query: "Continue with this additional context",
  files: ["additional-file.txt"],
  interactiveToolCallResult: true,  // Optional: for tool call responses
  provider: "anthropic",            // Optional: override provider
  model: "claude-sonnet-4-20250514",          // Optional: override model
})
```

#### `client.jobs.cancel(id, options?)`

Cancel a running job.

```typescript
const result = await client.jobs.cancel("job-id")
```

#### Checkpoints

Track job progress through checkpoints.

##### `client.jobs.checkpoints.list(jobId, params?, options?)`

List checkpoints for a job.

```typescript
const result = await client.jobs.checkpoints.list("job-id", {
  take: 50,
  skip: 0,
  sort: "createdAt",
  order: "asc",
})
```

##### `client.jobs.checkpoints.get(jobId, checkpointId, options?)`

Get a specific checkpoint.

```typescript
const result = await client.jobs.checkpoints.get("job-id", "checkpoint-id")
```

##### `client.jobs.stream(jobId, options?)`

Stream job events in real-time using Server-Sent Events (SSE).

```typescript
const result = await client.jobs.stream("job-id")
if (result.ok) {
  for await (const event of result.data.events) {
    console.log("Event:", event)
  }
}
```

---

### Experts API

Manage expert definitions and versions.

#### `client.experts.list(params?, options?)`

List available experts.

```typescript
const result = await client.experts.list({
  filter: "search term",
  category: "coding",       // "general" | "coding" | "research" | "writing" | "data" | "automation"
  includeDrafts: false,
  limit: 20,
  offset: 0,
})
```

#### `client.experts.get(key, options?)`

Get an expert definition by key.

```typescript
// Get latest version
const result = await client.experts.get("@org/expert")

// Get specific version
const result = await client.experts.get("@org/expert@1.0.0")

// Get by tag
const result = await client.experts.get("@org/expert@latest")
```

#### `client.experts.getMeta(key, options?)`

Get expert metadata without the full definition.

```typescript
const result = await client.experts.getMeta("@org/expert@1.0.0")
if (result.ok) {
  console.log("Scope:", result.data.data.scope)
  console.log("Version:", result.data.data.version)
}
```

#### `client.experts.publish(scopeName, options?)`

Publish an expert scope (make it publicly visible).

```typescript
const result = await client.experts.publish("@org/expert")
```

#### `client.experts.unpublish(scopeName, options?)`

Unpublish an expert scope (make it private).

```typescript
const result = await client.experts.unpublish("@org/expert")
```

#### `client.experts.yank(key, options?)`

Yank (deprecate) a specific version.

```typescript
const result = await client.experts.yank("@org/expert@1.0.0")
if (result.ok) {
  console.log("Yanked:", result.data.data.yanked)
  console.log("Latest tag updated:", result.data.data.latestTagUpdated)
}
```

#### Drafts

Manage expert draft versions before publishing.

##### `client.experts.drafts.list(scopeName, params?, options?)`

List drafts for a scope.

```typescript
const result = await client.experts.drafts.list("@org/expert", {
  limit: 10,
  offset: 0,
})
```

##### `client.experts.drafts.get(scopeName, draftRef, options?)`

Get a specific draft.

```typescript
const result = await client.experts.drafts.get("@org/expert", "draft-ref")
```

##### `client.experts.drafts.create(scopeName, input, options?)`

Create a new draft.

```typescript
const result = await client.experts.drafts.create("@org/expert", {
  applicationId: "app-id",
  experts: [
    {
      key: "main",
      name: "Main Expert",
      description: "A helpful assistant",
      instruction: "You are a helpful assistant...",
      skills: {},
      delegates: [],
    },
  ],
})
```

##### `client.experts.drafts.update(scopeName, draftRef, input, options?)`

Update an existing draft.

```typescript
const result = await client.experts.drafts.update("@org/expert", "draft-ref", {
  experts: [
    {
      key: "main",
      name: "Updated Expert",
      instruction: "Updated instructions...",
    },
  ],
})
```

##### `client.experts.drafts.delete(scopeName, draftRef, options?)`

Delete a draft.

```typescript
const result = await client.experts.drafts.delete("@org/expert", "draft-ref")
```

##### `client.experts.drafts.assignVersion(scopeName, draftRef, input, options?)`

Promote a draft to a versioned release.

```typescript
const result = await client.experts.drafts.assignVersion("@org/expert", "draft-ref", {
  version: "1.0.0",
  tag: "latest",  // Optional: assign a tag
})
```

#### Versions

List expert versions.

##### `client.experts.versions.list(scopeName, options?)`

List all versions for a scope.

```typescript
const result = await client.experts.versions.list("@org/expert")
if (result.ok) {
  for (const version of result.data.data.versions) {
    console.log(`${version.version}: ${version.tag || "(no tag)"}`)
  }
}
```

---

## Error Handling

The client uses a result type pattern for predictable error handling:

```typescript
const result = await client.applications.get("app-id")

if (!result.ok) {
  // result.status contains the HTTP status code (or 0 for network/timeout errors)
  switch (result.status) {
    case 401:
      console.error("Authentication failed")
      break
    case 404:
      console.error("Application not found")
      break
    case 0:
      // Network error, timeout, or validation error
      console.error("Network error:", result.error.message)
      break
    default:
      console.error(`Error ${result.status}: ${result.error.message}`)
  }
  return
}

// TypeScript knows result.data exists here
console.log(result.data.data.application)
```

### Request Cancellation

Use `AbortController` to cancel requests:

```typescript
const controller = new AbortController()

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000)

const result = await client.jobs.start(
  { applicationId: "app-id", expertKey: "@org/expert", provider: "anthropic" },
  { signal: controller.signal }
)

if (!result.ok && result.error.type === "abort") {
  console.log("Request was cancelled")
}
```

---

## SSE Streaming

For real-time checkpoint streaming, the client provides an async generator:

```typescript
import { createApiClient } from "@perstack/api-client"

const client = createApiClient({ apiKey: "your-api-key" })

// Start a job
const jobResult = await client.jobs.start({
  applicationId: "app-id",
  expertKey: "@org/expert",
  provider: "anthropic",
})

if (!jobResult.ok) {
  throw new Error(jobResult.error.message)
}

const jobId = jobResult.data.data.job.id

// Stream job events
const streamResult = await client.jobs.stream(jobId)
if (!streamResult.ok) {
  console.error("Stream request failed:", streamResult.error.message)
} else {
  for await (const event of streamResult.data.events) {
    console.log("Event:", event)
  }
}
```

### Stream Timeout Configuration

The streaming endpoints support an idle timeout that aborts the stream if no data is received within the specified period:

```typescript
const streamResult = await client.jobs.stream(jobId, {
  streamIdleTimeout: 60000, // 60 second idle timeout
})
if (!streamResult.ok) {
  if (streamResult.error.type === "abort" || streamResult.error.type === "timeout") {
    console.error("Stream timed out or was cancelled")
  }
} else {
  for await (const event of streamResult.data.events) {
    console.log("Event:", event)
  }
}
```

By default, the stream idle timeout uses the client's configured timeout value (30 seconds if not specified).

### Advanced SSE Parsing

For custom SSE parsing, the package exports utility functions:

```typescript
import { parseSSE, parseCheckpointSSE, parseSSEWithSchema } from "@perstack/api-client"
import { z } from "zod"

// Generic SSE parser
const reader = response.body.getReader()
for await (const event of parseSSE<MyEventType>(reader)) {
  console.log(event)
}

// Checkpoint-specific parser with validation
for await (const event of parseCheckpointSSE(reader)) {
  if (event.type === "checkpoint") {
    console.log("Checkpoint:", event.data)
  } else if (event.type === "done") {
    console.log("Stream complete:", event.data.status)
  } else if (event.type === "error") {
    console.error("Parse error:", event.data.message)
  }
}

// Custom schema validation
const mySchema = z.object({ id: z.string(), value: z.number() })
for await (const event of parseSSEWithSchema(reader, mySchema)) {
  console.log(event.id, event.value)
}
```

---

## TypeScript Support

The package is written in TypeScript and exports all types:

```typescript
import type {
  // Client types
  ApiClient,
  ApiClientConfig,
  ApiResult,
  ApiError,
  RequestOptions,
  StreamRequestOptions,

  // Application types
  Application,
  ApplicationStatus,
  CreateApplicationInput,
  UpdateApplicationInput,
  ListApplicationsParams,

  // Job types
  Job,
  JobStatus,
  StartJobInput,
  UpdateJobInput,
  ContinueJobInput,
  Checkpoint,
  CheckpointStatus,

  // Expert types
  Expert,
  ExpertDefinition,
  ExpertVersion,
  ExpertScope,
} from "@perstack/api-client"
```

---

## License

MIT