# @coda/core-api

Shared API contracts and client for the ows-coda chatbot. This package is the single source of truth for every type, constant, and error class that crosses the client/server boundary.

## Installation

This is a private workspace package — consumed via the pnpm workspace link:

```jsonc
// package.json
{ "dependencies": { "@coda/core-api": "workspace:*" } }
```

## Usage

```ts
import {
  ApiClient,
  Environment,
  ROUTES,
  API_VERSION,
  NetworkError,
  type SourceLink,
  type UsageStats,
  type Conversation,
} from "@coda/core-api";
```

### ApiClient

Framework-agnostic HTTP client for every chat endpoint. Accepts a config object with either an explicit `baseUrl` or a known `env` — no React, Vite, or Node-specific dependencies.

```ts
// Using a known environment (resolves URL automatically)
const client = new ApiClient({
  env: Environment.Production,
  auth: async () => ({
    token: await getToken(),
    identityHeaders: { "orchard-identity-id": "42" },
  }),
});

// Or using an explicit base URL
const client = new ApiClient({
  baseUrl: "https://coda-custom.example.com",
  auth: async () => ({ token: null, identityHeaders: {} }),
});

// List conversations
const conversations = await client.listConversations();

// Create a conversation
const conv = await client.createConversation({ title: "Q1 Revenue" });

// Stream a chat response
await client.streamQuery({
  conversationId: conv.id,
  query: "Summarize my account",
  onChunk: (text) => process.stdout.write(text),
  onSources: (sources) => console.log("Sources:", sources),
  onUsage: (usage) => console.log(`${usage.totalTokens} tokens`),
});

// Update a conversation (rename, star)
await client.updateConversation(conv.id, { starred: true });

// Delete a conversation
await client.deleteConversation(conv.id);
```

### Environments

The `Environment` enum maps to default base URLs via `DEFAULT_BASE_URLS`:

| Environment              | Default Base URL                    |
| ------------------------ | ----------------------------------- |
| `Environment.QA`         | `https://qa-ows-coda.theorchard.io` |
| `Environment.Production` | `https://coda.theorchard.io`        |

When `baseUrl` is provided in the config, it takes precedence over `env`.

### Route Constants

```ts
import { API_VERSION, ROUTES } from "@coda/core-api";

ROUTES.CHATS; // "/api/v1/chats"
ROUTES.CHAT("abc"); // "/api/v1/chats/abc"
ROUTES.CHAT_STREAM("abc"); // "/api/v1/chats/abc/stream"
ROUTES.HEALTH; // "/health"
```

### Error Handling

All errors extend `ChatbotError`. Use `isChatbotError()` as a type guard, or catch specific subclasses:

```ts
import { NetworkError, StreamingError, isChatbotError } from "@coda/core-api";

try {
  await client.streamQuery(opts);
} catch (e) {
  if (e instanceof NetworkError && e.statusCode === 429) {
    // rate limited
  } else if (e instanceof StreamingError) {
    // SSE protocol error
  } else if (isChatbotError(e)) {
    // any other chatbot error
  }
}
```

### Validation Schemas

Zod schemas are exported from a separate entry point to keep the main bundle zod-free:

```ts
import {
  streamQuerySchema,
  createChatSchema,
  updateChatSchema,
} from "@coda/core-api/schemas";
```

## Exports

### Types

| Type                        | Description                                            |
| --------------------------- | ------------------------------------------------------ |
| `ConversationStep`          | Tool-call progress step (`thinking` SSE event)         |
| `SourceLink`                | Deep-link to a fetched entity (`sources` SSE event)    |
| `UsageStats`                | Accumulated token usage (`usage` SSE event)            |
| `ExtractedAttachment`       | Generated file as a data URL (`attachments` SSE event) |
| `SelectionItem`             | Single item in an entity disambiguation list           |
| `SelectionRequired`         | Payload of the `selection_required` SSE event          |
| `ThinkingData`              | Union type for the `onThinking` callback parameter     |
| `Attachment`                | User-uploaded file attachment (data URL)               |
| `StreamQueryRequest`        | Body of `POST /api/v1/chats/:id/stream`                |
| `StreamQueryOptions`        | Options for `ApiClient.streamQuery()`                  |
| `ApiClientConfig`           | Config object for `ApiClient` constructor              |
| `AuthResolver`              | Async function providing auth credentials per-request  |
| `Conversation`              | Conversation metadata (id, title, starred, timestamps) |
| `CreateConversationRequest` | Body of `POST /api/v1/chats`                           |
| `UpdateConversationRequest` | Body of `PATCH /api/v1/chats/:id`                      |

### Classes

| Class            | Description                              |
| ---------------- | ---------------------------------------- |
| `ApiClient`      | HTTP client for all chat endpoints       |
| `ChatbotError`   | Base error class (has `.code`)           |
| `NetworkError`   | HTTP failure (has `.statusCode`, `.url`) |
| `StreamingError` | SSE protocol error (has `.cause`)        |

### Constants

| Export              | Value                                                        |
| ------------------- | ------------------------------------------------------------ |
| `API_VERSION`       | `"v1"`                                                       |
| `ROUTES`            | Object with `CHATS`, `CHAT(id)`, `CHAT_STREAM(id)`, `HEALTH` |
| `Environment`       | Object with `QA`, `Production` values                        |
| `DEFAULT_BASE_URLS` | `Record<Environment, string>` mapping envs to base URLs      |

### Functions

| Export              | Description                                           |
| ------------------- | ----------------------------------------------------- |
| `isChatbotError(e)` | Type guard narrowing `unknown` to `ChatbotError`      |
| `unwrapArray(d, k)` | Normalize an SSE payload to an array (raw or wrapped) |

## Building

```sh
pnpm build        # one-shot build (ESM + CJS + declarations)
pnpm dev          # watch mode
```

Output lands in `dist/` as dual-format bundles:

| File                        | Format                      |
| --------------------------- | --------------------------- |
| `index.mjs` / `index.d.mts` | ESM (Vite, modern bundlers) |
| `index.cjs` / `index.d.cts` | CJS (Node.js, server build) |

## Project Structure

```
api/
├── src/
│   ├── index.ts      # barrel export
│   ├── types.ts      # shared contract types
│   ├── schemas.ts    # Zod validation schemas (separate entry point)
│   ├── errors.ts     # error classes
│   ├── constants.ts  # Environment, DEFAULT_BASE_URLS, API_VERSION, ROUTES
│   ├── utils.ts      # unwrapArray and other helpers
│   └── client.ts     # ApiClient class
├── dist/             # build output (gitignored)
├── tsconfig.json
├── tsdown.config.ts
└── package.json
```
