# Ursly TypeScript SDK

Public documentation: [developer.ursly.io](https://developer.ursly.io/).

An ESM client for the stateless v1 intent API, with bundled TypeScript declarations and no runtime dependencies. It supports Node.js 22+ and browser environments with native fetch/AbortController. Install the versioned `.tgz` downloaded from the website or build this directory; registry publication is separate.

```sh
npm install ./ursly-sdk-<version>.tgz
```

`<version>` is `release.version` in `/downloads/manifest.json`. The public API host is `https://ursly.io`; pass it explicitly as `baseUrl`, because the client has no default destination.

```ts
import { UrslyClient } from "@ursly/sdk";

const client = new UrslyClient({ baseUrl: "https://YOUR-API-HOST" });
client.registerHandler("navigation.open", ({ target }) => {
  // Your router decides what navigation means. No built-in navigation occurs.
  console.log("Requested route:", target);
});
const request = {
  input: { sense: "voice" as const, transcript: "open home" },
  rules: [
    {
      id: "home",
      match: "open home",
      action: { type: "navigation.open" as const, target: "/" },
    },
  ],
};
const resolution = await client.resolve(request); // Returns intents, executes nothing.
const executed = await client.resolveAndDispatch(request); // Explicitly invokes registered handlers.
```

`capabilities()`, `actions()`, `templates()`, `validate(request)`, and `resolve(request)` call the matching `/api/v1/` endpoint. `templates()` returns requests you can edit. `registerHandler(type, fn)` receives a typed action and its intent; it returns an unregister callback. `dispatch(resolution)` is also explicit. All required handlers are checked before dispatch begins. Handler failure stops later handlers and is never retried; completed application effects are not rolled back.

## Configuration and cancellation

```ts
const controller = new AbortController();
const client = new UrslyClient({
  baseUrl: "https://YOUR-API-HOST",
  timeoutMs: 5000,
  fetch: globalThis.fetch, // Optional injectable transport, also useful for tests.
});
const pending = client.resolve(request, { signal: controller.signal });
controller.abort();
```

The base URL is required and can contain an application proxy prefix. Credentials, query strings and fragments are rejected. Default timeout is 10000 ms. Requests and response-body reading obey the deadline; abort cancels client waiting and native fetch. At most 1 MiB of a response body is read; a larger body, or one that is not valid UTF-8 JSON, is an `InvalidResponseError`. Non-streaming transports are supported too: a whatwg-fetch polyfill (React Native, Jest/jsdom) whose response has no `body`, node-fetch v2, or an injected double offering only `text()` or `json()`. Their body is read whole through `text()` (or `json()`, re-serialized), so on that path the 1 MiB cap is applied after reading, and the transport's own text decoding means invalid UTF-8 is not detected. Non-finite numbers such as `NaN` are refused with code `invalid_request` before sending, since JSON would turn them into `null`. In Node and other runtimes without a DOM, requests send `User-Agent: ursly-sdk-typescript/<version>`, where `<version>` is the exported `VERSION`. Browsers keep their own User-Agent, because a custom one would trigger a CORS preflight. Cancellation cannot undo a request the server already received. Nothing is retried and redirects are never followed. In Node a redirect is an `ApiError` carrying its 3xx status, as in the CLI and the other SDKs. Browsers hide a redirect's status, so there it is an `ApiError` with status 0; a cross-origin redirect without CORS headers stays a `TransportError`.

Typed errors distinguish API rejection (`status`, server `code`), request timeout, cancellation (`RequestAbortedError`, code `cancelled` as in the other Ursly SDKs), transport/invalid response, missing handler and handler failure. No request-body or secret logging occurs.

## Response validation

Catalog entries and template requests are validated before being returned. Invalid action payloads are rejected before registered handlers execute, including external navigation (a path whose `//`, `.` or `..` segments, `%2e` included, could resolve to another host), invalid identifiers, unknown action fields and oversized UTF-8 payloads. Optional `confidence` and `minConfidence` accept both omission and JSON `null`, matching the generated API and templates. Applications still own authorization and the safety of handler side effects.

## Actions and product boundaries

Configure `enabled` and `minConfidence` on rules, and explicit `confidence` on inputs. Confidence comes from your adapter, never server inference. Motion input is a normalized gesture such as `swipe_left`; voice input is an explicit transcript. The SDK does not acquire microphone/camera data or perform recognition.

The six declarative types are `navigation.open`, `text.insert`, `media.play`, `media.pause`, `scene.focus`, and `workflow.trigger`. Workflow parameters are bounded string maps; destinations/resources remain application-owned. SDK handlers do not imply a shell or server HTTP relay. Voice production/motion in development are founder-reported product stages; they do not prove hardware, model accuracy or an integration. Other senses currently produce HTTP 422 `sense_unavailable`.

## Changes since 0.1.0

Breaking:

- Caller cancellation reports code `cancelled` with the message "Request cancelled" instead of `aborted`; the class is still `RequestAbortedError`.
- A redirect (3xx) is an `ApiError` carrying its status, or status 0 for a browser's opaque redirect, instead of a `TransportError`.
- Response bodies are capped at 1 MiB and must be strict UTF-8 JSON; otherwise the call fails with `InvalidResponseError`.
- A base URL containing `?` or `#`, even with nothing after it, is a `ConfigurationError`.
- Responses are validated against the v1 contract before any handler runs: malformed actions, catalog entries and templates are an `InvalidResponseError`. `navigation.open` targets must be origin-relative paths with no `//`, no `.` or `..` segment (also spelled `%2e`), backslash, line break or whitespace.
- `NaN` and `±Infinity` in a request are refused with code `invalid_request` before sending, instead of being sent as `null`.

Also new: outside browsers, requests send `User-Agent: ursly-sdk-typescript/<version>`, and `confidence`/`minConfidence` accept `null`, as the API does.

## Development

```sh
cd sdk/typescript
npm ci
npm run typecheck
npm test
npm run build
npm pack
```

The version is `version` in `package.json`, exported as `VERSION`; the package exports compiled ESM and generated `.d.ts` declarations from `dist/`. Typed request/action contracts, explicit handler dispatch and transport adapters remain separate. Offline injected-transport tests and optional real endpoint contract tests establish different evidence. [SOURCES.md](SOURCES.md) records official guidance and checked versions.

## License

License terms have not yet been published. Public downloads do not grant an open-source license; the source repository remains private.
