@pouchy_ai/companion-sdk 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,79 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.30.0] - 2026-07-12
16
+
17
+ No breaking changes despite the minor bump — everything here is additive
18
+ surface (which this repo's house policy ships as `0.x.0`).
19
+
20
+ ### Added
21
+
22
+ - **Typed `control.error` code vocabulary.** New runtime export
23
+ `CONTROL_ERROR_CODES` — the server-emitted stream-plane codes
24
+ (`agent_error`, `call_mint_failed`) — plus the types `ControlErrorCode`,
25
+ `ControlErrorPayload`, and the open union `ControlErrorCodeValue` (server
26
+ codes + the SDK-synthesized `stream_unauthorized` + a forward-compat string
27
+ arm, same doctrine as `CompanionErrorCodeValue`). `onError`'s handler now
28
+ sees `code: ControlErrorCodeValue` instead of a bare `string`, so a `switch`
29
+ on it autocompletes. Drift-tested two ways: the SDK list must equal the
30
+ server list, AND every `control.error` emit site's code literal (server and
31
+ SDK) must be declared — a new site inventing a code fails CI.
32
+ - **`on()` narrows the envelope payload per event name.** New
33
+ `OutboundPayloadMap` + a generic overload:
34
+ `on('companion.typing', (env) => env.payload.active)` type-checks without a
35
+ cast; `on('*')` keeps the untyped envelope. Type-only — zero runtime change.
36
+ New wire-payload types exported along the way: `HelloAckPayload`,
37
+ `CallReadyPayload`, `ControlErrorPayload`.
38
+ - **Stream-state observability.** `client.streamState` +
39
+ `onStreamStateChange((state, prev) => …)` with
40
+ `'idle' | 'connecting' | 'connected' | 'reconnecting' | 'degraded_sse' |
41
+ 'stopped'` (`CompanionStreamState`) — including the previously invisible
42
+ WebSocket→SSE degrade (`degraded_sse`) and the permanent-401/403 stop. Hosts
43
+ no longer hand-roll a connection enum around `onMessage` traffic.
44
+
45
+ ### Fixed
46
+
47
+ - **Reconnect backoff now carries jitter (additive, 0–25%)** in both the SSE
48
+ and WebSocket receive loops, so a fleet of embeds doesn't reconnect in
49
+ lockstep after a server deploy/restart (thundering herd).
50
+ - **`Retry-After` HTTP-date form is parsed.** RFC 9110 allows a date form
51
+ (proxies/CDNs emit it); it used to read as `undefined`. Delta-seconds still
52
+ win; a date converts to a non-negative seconds delta.
53
+ - **The streaming `sendText` leg keeps the 429 body `hint`.** The non-SSE
54
+ error fallback dropped the actionable hint (e.g. "provision a project …")
55
+ that the buffered path has surfaced since 0.27.0.
56
+
57
+ ## [0.29.1] - 2026-07-12
58
+
59
+ ### Fixed
60
+
61
+ - **`CompanionError.retryAfter` no longer reports a spurious `0`.** A missing
62
+ `Retry-After` header was parsed as `Number(null) === 0`, so EVERY error
63
+ thrown from a POST without the header (a `400 invalid_request`, a
64
+ `404 session_not_found`, …) carried `retryAfter: 0` — telling backoff code
65
+ "retry immediately" on non-retryable errors. It is now `undefined` unless
66
+ the body's `retryAfterSec` or a real header names a value, as documented.
67
+ - **`sendText({ awaitReply: true, stream: false })` now cancels its POST.**
68
+ The buffered leg never passed the operation's `AbortSignal` to the network
69
+ call, so the promise rejected on abort/deadline but the billed turn kept
70
+ running server-side. It now aborts with the same linked controller the
71
+ streaming leg uses.
72
+ - **Abort-listener leak on a reused `AbortSignal` in `awaitReply`.** The
73
+ deadline's reject listener was never removed, so a host reusing one
74
+ long-lived signal across many `sendText({ awaitReply, signal })` calls
75
+ accumulated one orphan listener per call (memory growth + Node's
76
+ MaxListenersExceeded warning). Removed in the same `finally` as the rest.
77
+
78
+ ### Added
79
+
80
+ - **`signal` on `sendToolResult` and `startCall`** — the two request-performing
81
+ methods 0.29.0's "AbortSignal on every network method" pass missed
82
+ (backwards-compatible addition, per this package's patch policy).
83
+ - **`require()` of the package now works on Node ≥ 20.17** (require-ESM): the
84
+ `exports` map gained a `default` condition — previously `require()` failed
85
+ with `ERR_PACKAGE_PATH_NOT_EXPORTED` before the feature could apply, despite
86
+ the README documenting it.
87
+
15
88
  ## [0.29.0] - 2026-07-12
16
89
 
17
90
  ### Added
@@ -616,7 +689,9 @@ self-hosted/CDN build; this entry captures the surface that ships publicly.
616
689
  - **Versioned wire protocol.** `PROTOCOL_VERSION = 1`, kept in lockstep with the
617
690
  server by `protocol.drift.test.ts` (fails CI on divergence).
618
691
 
619
- [Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.0...HEAD
692
+ [Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.30.0...HEAD
693
+ [0.30.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.1...companion-sdk-v0.30.0
694
+ [0.29.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.0...companion-sdk-v0.29.1
620
695
  [0.29.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.1...companion-sdk-v0.29.0
621
696
  [0.28.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.0...companion-sdk-v0.28.1
622
697
  [0.28.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.27.0...companion-sdk-v0.28.0
package/README.md CHANGED
@@ -63,9 +63,25 @@ Token as `token` instead — same API. See the docs' authentication section.)
63
63
  - **Capabilities map:** [`docs/companion-capabilities.md`](../../docs/companion-capabilities.md)
64
64
  - **Instant UI renderer contract:** [`docs/companion-instant-ui.md`](../../docs/companion-instant-ui.md)
65
65
 
66
+ ### Constructor options (beyond `baseUrl` + `token`)
67
+
68
+ `createCompanion({ … })` also takes: `surface` (one resumable session per
69
+ surface), `modalities` / `handles` / `contextKinds` / `tools` /
70
+ `appContext` (the capability handshake), `visitor` (representative mode,
71
+ below), `onAuthError` (401 → return a fresh token and the client retries
72
+ transparently), `stream: 'sse' | 'websocket'` (receive transport —
73
+ `'websocket'` opts into the lower-latency WS plane when the deployment serves
74
+ one and **falls back to SSE automatically**, observable as
75
+ `streamState === 'degraded_sse'`), and two injection points for Node/tests:
76
+ `fetch` (custom fetch implementation) and `webSocketImpl` (WebSocket
77
+ constructor when `globalThis.WebSocket` is absent).
78
+
66
79
  ## Events
67
80
 
68
- Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers:
81
+ Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers.
82
+ Since 0.30.0 `on()` narrows the envelope payload per event name
83
+ (`OutboundPayloadMap`) — e.g. `on('companion.typing', (env) => env.payload.active)`
84
+ type-checks without a cast; `'*'` keeps the untyped envelope:
69
85
 
70
86
  | Helper | Event | Use |
71
87
  | --- | --- | --- |
@@ -82,7 +98,7 @@ Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers:
82
98
  | `onTyping(fn)` | `companion.typing` | activity indicator — `fn({active})` fires `true` when a turn starts working and `false` when it finishes / pauses, spanning the tool-loop / thinking phase before the first text delta. Drive a "typing…" state |
83
99
  | `on('control.call_ready', fn)` | `control.call_ready` | a voice call is ready — the stream echo of `startCall`'s accept. Deliberately **secret-free** (`{ provider, agentId/model, voice, … }` — the actual WebRTC credentials only ride the `startCall` HTTP response); useful for UI state on surfaces that didn't initiate the call |
84
100
  | `onUsage(fn)` | `control.usage` | per-token metering echo. _(reserved — not emitted yet)_ |
85
- | `onError(fn)` | `control.error` | agent / stream errors — `agent_error` (server-side turn failed after accept; safe to re-send), `call_mint_failed` (voice-credential mint failed — retry later or fall back to text) or the SDK-synthesized `stream_unauthorized` (stream 401 exhausted reconnects — refresh the token, `start()` again) |
101
+ | `onError(fn)` | `control.error` | agent / stream errors — `agent_error` (server-side turn failed after accept; safe to re-send), `call_mint_failed` (voice-credential mint failed — retry later or fall back to text) or the SDK-synthesized `stream_unauthorized` (stream 401 exhausted reconnects — refresh the token, `start()` again). Since 0.30.0 the vocabulary is EXPORTED TYPED: `CONTROL_ERROR_CODES` (runtime list, drift-tested against the server) + the `ControlErrorCodeValue` union `fn`'s `err.code` now carries — `switch` on it with autocomplete |
86
102
 
87
103
  ### Instant UI
88
104
 
@@ -112,7 +128,8 @@ local/device skills).
112
128
  | `endSession()` | Distill the session into durable memory now; returns `{ skipped?: 'no_session' \| 'no_content' \| 'throttled' }`. |
113
129
  | `close()` | One-call teardown: `stop()` + `endSession()` — the text-session mirror of the call handle's `close()`. |
114
130
  | `startCall(opts)` / `connectCall(opts)` | Voice plane: raw credentials / fully-wired call handle (`call.close()` ends + folds the transcript into memory). |
115
- | `getAvatar()` / `brandIconUrl(size?)` | The user's avatar (VRM/portrait) and the Pouchy brand icon for your UI. |
131
+ | `streamState` / `onStreamStateChange(fn)` | Receive-stream lifecycle (0.30.0): `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'degraded_sse' \| 'stopped'` (`CompanionStreamState`). `fn(state, prev)` fires on change only — drive a connection indicator; `degraded_sse` = the WS transport errored and delivery continues on the SSE fallback; `stopped` = `stop()`/`close()` or a permanent `stream_unauthorized` failure. |
132
+ | `getAvatar()` / `brandIconUrl(size?)` | The user's avatar (VRM/portrait) and the Pouchy brand icon for your UI. Standalone (no client/token): `pouchyBrandIconUrl(baseUrl, size?)` derives the same URL before/without connecting. |
116
133
  | `getWallet()` | Read the instance's own wallet — `{ balances: [{ currency, amount }], totalUsd, currency }`. Read-only + receive-only; needs the `wallet.read` scope. |
117
134
 
118
135
  Tool calling beyond your own declared tools: `HOST_CONTROL_TOOLS` (universal
@@ -122,12 +139,14 @@ are offered automatically once you declare `tools`/`handles`, and
122
139
  embeds receive expression cues — declare them or they no-op silently. Full
123
140
  contract: `docs/companion-host-control.md`.
124
141
 
125
- **Cancellation (0.29.0):** every request-performing method accepts an optional
126
- `AbortSignal`. Methods with an options object take a `signal?` field
127
- (`sendText(text, { signal })`, `recall({ signal })`, …); the rest take a
128
- trailing `opts` (`getAvatar({ signal })`, `ping({ signal })`, …). Aborting
129
- rejects with `CompanionError` `code: 'aborted'`, and for `sendText({ awaitReply })`
130
- it also tears down the in-flight event stream.
142
+ **Cancellation (0.29.0, completed in 0.29.1):** every request-performing method
143
+ accepts an optional `AbortSignal` including `sendToolResult(callId, result,
144
+ { signal })` and `startCall({ signal })` since 0.29.1. Methods with an options
145
+ object take a `signal?` field (`sendText(text, { signal })`,
146
+ `recall({ signal })`, …); the rest take a trailing `opts`
147
+ (`getAvatar({ signal })`, `ping({ signal })`, …). Aborting rejects with
148
+ `CompanionError` `code: 'aborted'`, and for `sendText({ awaitReply })` it also
149
+ tears down the in-flight event stream.
131
150
 
132
151
  ```ts
133
152
  const ac = new AbortController();
package/dist/client.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, ToolCallPayload, TypingPayload, UsagePayload, VoiceInjectPayload, WorldStateEvent } from './protocol.js';
1
+ import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundPayloadMap, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, ToolCallPayload, TypingPayload, UsagePayload, VoiceInjectPayload, WorldStateEvent } from './protocol.js';
2
2
  /** Ergonomic world-state input: a WorldStateEvent with the CloudEvents plumbing
3
3
  * (specversion / id / source) optional — sendWorldState fills them. */
4
4
  export type WorldStateInput<D = unknown> = Omit<WorldStateEvent<D>, 'specversion' | 'id' | 'source'> & Partial<Pick<WorldStateEvent<D>, 'specversion' | 'id' | 'source'>>;
5
+ import { type ControlErrorCodeValue } from './errors.js';
5
6
  import { type CompanionCall, type CompanionCallOptions } from './call.js';
6
7
  export interface CompanionClientOptions {
7
8
  /** Origin of the Pouchy deployment, e.g. "https://pouchy.ai". */
@@ -198,6 +199,18 @@ type Handler = (envelope: CompanionEnvelope) => void;
198
199
  type DeltaHandler = (chunk: string, meta: {
199
200
  reset?: boolean;
200
201
  }) => void;
202
+ /** Lifecycle of the receive event stream (0.30.0) — observable via
203
+ * `client.streamState` + `onStreamStateChange`, so a host can render a
204
+ * connection indicator without hand-rolling its own enum.
205
+ * - `idle` — before start()
206
+ * - `connecting` — start() called; first connection being established
207
+ * - `connected` — the event stream is live (WS or SSE as configured)
208
+ * - `reconnecting`— connection lost / closed; backing off before the next try
209
+ * - `degraded_sse`— the WebSocket transport errored and delivery continues on
210
+ * the SSE fallback (still live — replies keep arriving)
211
+ * - `stopped` — stop()/close() was called, or a permanent stream failure
212
+ * (the `stream_unauthorized` control.error) ended the loop */
213
+ export type CompanionStreamState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'degraded_sse' | 'stopped';
201
214
  export declare class CompanionClient {
202
215
  private readonly opts;
203
216
  private readonly doFetch;
@@ -210,9 +223,22 @@ export declare class CompanionClient {
210
223
  private readonly seen;
211
224
  private readonly deltaHandlers;
212
225
  private readonly pendingVoiceTools;
226
+ private _streamState;
227
+ private readonly streamStateHandlers;
213
228
  constructor(opts: CompanionClientOptions);
214
229
  /** The active session id, or null before connect(). */
215
230
  get sessionId(): string | null;
231
+ /** Current lifecycle of the receive event stream (0.30.0) — `idle` before
232
+ * start(), then `connecting` / `connected` / `reconnecting` /
233
+ * `degraded_sse` (WS errored, SSE fallback carrying delivery) / `stopped`.
234
+ * Subscribe to transitions with `onStreamStateChange`. */
235
+ get streamState(): CompanionStreamState;
236
+ /** Subscribe to receive-stream lifecycle transitions (0.30.0) — drive a
237
+ * connection indicator: `client.onStreamStateChange((s) => setStatus(s))`.
238
+ * Fires only on change, with the previous state as the second argument.
239
+ * Returns an unsubscribe function. */
240
+ onStreamStateChange(handler: (state: CompanionStreamState, prev: CompanionStreamState) => void): () => void;
241
+ private setStreamState;
216
242
  /** Swap the bearer token used by every subsequent request and stream
217
243
  * reconnect. Session tokens expire (default 1h) — call this when your
218
244
  * backend re-mints one, or wire `onAuthError` to do it on demand. */
@@ -346,6 +372,7 @@ export declare class CompanionClient {
346
372
  startCall(opts?: {
347
373
  voice?: string;
348
374
  locale?: string;
375
+ signal?: AbortSignal;
349
376
  }): Promise<CallCredentials>;
350
377
  /** Open a live voice call end-to-end: mints credentials (startCall) and opens a
351
378
  * WebRTC session directly to the provider — no first-party app code needed.
@@ -388,6 +415,8 @@ export declare class CompanionClient {
388
415
  sendToolResult(callId: string, result: {
389
416
  ok?: boolean;
390
417
  result?: unknown;
418
+ }, opts?: {
419
+ signal?: AbortSignal;
391
420
  }): Promise<{
392
421
  allDone: boolean;
393
422
  }>;
@@ -404,9 +433,12 @@ export declare class CompanionClient {
404
433
  onToolCall(handler: (call: ToolCallEvent, envelope: CompanionEnvelope) => void): () => void;
405
434
  /** Convenience: subscribe to errors the companion surfaces — server-side agent
406
435
  * errors and stream failures (e.g. a permanent `stream_unauthorized`) both
407
- * arrive as `control.error`. Returns an unsubscribe fn. */
436
+ * arrive as `control.error`. Since 0.30.0 `code` is typed as
437
+ * `ControlErrorCodeValue` (the CONTROL_ERROR_CODES vocabulary + the
438
+ * SDK-synthesized stream codes, open for newer servers) so a `switch` on it
439
+ * autocompletes. Returns an unsubscribe fn. */
408
440
  onError(handler: (err: {
409
- code: string;
441
+ code: ControlErrorCodeValue;
410
442
  message: string;
411
443
  }, envelope: CompanionEnvelope) => void): () => void;
412
444
  /** Recall the memory this token is authorized to see. Without `query` the
@@ -530,8 +562,12 @@ export declare class CompanionClient {
530
562
  * the companion's app tile, etc. Sizes: 256 | 512 | 1024 (default 512). */
531
563
  brandIconUrl(size?: BrandIconSize): string;
532
564
  /** Subscribe to outbound events of a given type, or "*" for all. Returns an
533
- * unsubscribe function. */
534
- on(type: OutboundType | '*', handler: Handler): () => void;
565
+ * unsubscribe function. Since 0.30.0 the envelope payload is NARROWED per
566
+ * event name (OutboundPayloadMap) e.g. `on('companion.typing', (env) =>
567
+ * env.payload.active)` type-checks without a cast; `'*'` keeps the untyped
568
+ * envelope. Type-only: runtime behavior is unchanged. */
569
+ on<T extends OutboundType>(type: T, handler: (envelope: CompanionEnvelope<OutboundPayloadMap[T]>) => void): () => void;
570
+ on(type: '*', handler: Handler): () => void;
535
571
  /** Convenience: subscribe to assistant text replies. */
536
572
  onMessage(handler: (text: string, envelope: CompanionEnvelope) => void): () => void;
537
573
  /** Convenience: subscribe to Instant UI render surfaces (companion.ui_action).
@@ -624,7 +660,14 @@ export declare class CompanionClient {
624
660
  stop(): void;
625
661
  private emit;
626
662
  private handleFrame;
663
+ /** SSE receive loop. `degraded` marks that this loop is the WS transport's
664
+ * fallback, so its healthy state reads `degraded_sse` (still live) instead
665
+ * of `connected` — the host can tell "fine" from "fine, but on plan B". */
627
666
  private loop;
628
667
  private consume;
629
668
  private sleep;
669
+ /** Backoff sleep with additive jitter (0–25%, 0.30.0) so a fleet of embeds
670
+ * doesn't reconnect in lockstep after a server deploy/restart (thundering
671
+ * herd) — every reconnect path sleeps through here, never bare sleep(). */
672
+ private backoffSleep;
630
673
  }
package/dist/client.js CHANGED
@@ -62,6 +62,9 @@ export class CompanionClient {
62
62
  deltaHandlers = new Set();
63
63
  // Voice tool-calls awaiting the app's sendToolResult, keyed by a synthetic id.
64
64
  pendingVoiceTools = new Map();
65
+ // Receive-stream lifecycle (0.30.0) — see CompanionStreamState.
66
+ _streamState = 'idle';
67
+ streamStateHandlers = new Set();
65
68
  constructor(opts) {
66
69
  if (!opts.baseUrl)
67
70
  throw new CompanionError('CompanionClient: baseUrl is required', 0, 'missing_option');
@@ -74,6 +77,37 @@ export class CompanionClient {
74
77
  get sessionId() {
75
78
  return this._session;
76
79
  }
80
+ /** Current lifecycle of the receive event stream (0.30.0) — `idle` before
81
+ * start(), then `connecting` / `connected` / `reconnecting` /
82
+ * `degraded_sse` (WS errored, SSE fallback carrying delivery) / `stopped`.
83
+ * Subscribe to transitions with `onStreamStateChange`. */
84
+ get streamState() {
85
+ return this._streamState;
86
+ }
87
+ /** Subscribe to receive-stream lifecycle transitions (0.30.0) — drive a
88
+ * connection indicator: `client.onStreamStateChange((s) => setStatus(s))`.
89
+ * Fires only on change, with the previous state as the second argument.
90
+ * Returns an unsubscribe function. */
91
+ onStreamStateChange(handler) {
92
+ this.streamStateHandlers.add(handler);
93
+ return () => this.streamStateHandlers.delete(handler);
94
+ }
95
+ setStreamState(next) {
96
+ if (next === this._streamState)
97
+ return;
98
+ const prev = this._streamState;
99
+ this._streamState = next;
100
+ // Same isolation doctrine as emit(): a throwing consumer handler must not
101
+ // skip the remaining handlers or bubble into the stream loop.
102
+ for (const h of this.streamStateHandlers) {
103
+ try {
104
+ h(next, prev);
105
+ }
106
+ catch (e) {
107
+ console.error('[companion-sdk] stream-state handler threw', e);
108
+ }
109
+ }
110
+ }
77
111
  /** Swap the bearer token used by every subsequent request and stream
78
112
  * reconnect. Session tokens expire (default 1h) — call this when your
79
113
  * backend re-mints one, or wire `onAuthError` to do it on demand. */
@@ -191,8 +225,21 @@ export class CompanionClient {
191
225
  const fromBody = json?.retryAfterSec;
192
226
  if (typeof fromBody === 'number' && Number.isFinite(fromBody) && fromBody >= 0)
193
227
  return fromBody;
194
- const header = Number(res.headers.get('Retry-After'));
195
- return Number.isFinite(header) && header >= 0 ? header : undefined;
228
+ // Missing/blank header must stay undefined — Number(null) is 0, which
229
+ // used to stamp retryAfter: 0 on EVERY error without the header (a 400,
230
+ // a 404 …), telling backoff code "retry immediately".
231
+ const raw = res.headers.get('Retry-After');
232
+ if (raw === null || raw.trim() === '')
233
+ return undefined;
234
+ const header = Number(raw);
235
+ if (Number.isFinite(header) && header >= 0)
236
+ return header;
237
+ // RFC 9110 also allows an HTTP-date (proxies/CDNs emit it) — convert to a
238
+ // non-negative seconds delta (0.30.0). Unparseable stays undefined.
239
+ const at = Date.parse(raw);
240
+ if (!Number.isNaN(at))
241
+ return Math.max(0, Math.ceil((at - Date.now()) / 1000));
242
+ return undefined;
196
243
  }
197
244
  async postJson(path, body, signal) {
198
245
  const res = await this.fetchAuthed(this.url(path), {
@@ -305,6 +352,9 @@ export class CompanionClient {
305
352
  opts.signal.addEventListener('abort', onCallerAbort, { once: true });
306
353
  }
307
354
  let timer;
355
+ // Held so the finally can REMOVE it — a host reusing one long-lived signal
356
+ // across many awaitReply calls accumulated one orphan listener per call.
357
+ let onCallerAbortReject;
308
358
  const deadline = new Promise((_, reject) => {
309
359
  timer = setTimeout(() => {
310
360
  ac.abort();
@@ -313,7 +363,8 @@ export class CompanionClient {
313
363
  // The caller's signal also rejects the operation (the stream teardown
314
364
  // alone would otherwise leave the fallback wait hanging to the timer).
315
365
  if (opts?.signal) {
316
- opts.signal.addEventListener('abort', () => reject(new CompanionError('request aborted', 0, 'aborted')), { once: true });
366
+ onCallerAbortReject = () => reject(new CompanionError('request aborted', 0, 'aborted'));
367
+ opts.signal.addEventListener('abort', onCallerAbortReject, { once: true });
317
368
  }
318
369
  });
319
370
  try {
@@ -322,7 +373,10 @@ export class CompanionClient {
322
373
  let pausedOnTools = false;
323
374
  if (opts?.stream === false) {
324
375
  const json = await Promise.race([
325
- this.postJson(`/api/companion/session/${sessionId}/input`, body),
376
+ // ac.signal, like the streaming leg: the deadline and the caller's
377
+ // signal both abort it — without this the promise rejected but the
378
+ // billed POST kept running beyond cancellation.
379
+ this.postJson(`/api/companion/session/${sessionId}/input`, body, ac.signal),
326
380
  deadline
327
381
  ]);
328
382
  seq = json.seq ?? null;
@@ -354,6 +408,8 @@ export class CompanionClient {
354
408
  clearTimeout(timer);
355
409
  if (onCallerAbort)
356
410
  opts?.signal?.removeEventListener('abort', onCallerAbort);
411
+ if (onCallerAbortReject)
412
+ opts?.signal?.removeEventListener('abort', onCallerAbortReject);
357
413
  }
358
414
  }
359
415
  /** The streaming leg of sendText: parse the POST response's SSE frames —
@@ -373,7 +429,11 @@ export class CompanionClient {
373
429
  // Old server / error before the stream opened — behave like postJson.
374
430
  const json = (await res.json().catch(() => null));
375
431
  if (!res.ok || !json || json.ok === false) {
376
- throw new CompanionError(json?.error || `request failed (${res.status})`, res.status, json?.code, this.retryAfterFrom(res, json));
432
+ // Same as postJson: the 429 body's actionable `hint` (e.g. "provision
433
+ // a project …") must survive the streaming leg too (0.30.0).
434
+ const hint = json?.hint;
435
+ const detail = typeof hint === 'string' && hint ? ` — ${hint}` : '';
436
+ throw new CompanionError((json?.error || `request failed (${res.status})`) + detail, res.status, json?.code, this.retryAfterFrom(res, json));
377
437
  }
378
438
  return { seq: json.seq ?? null };
379
439
  }
@@ -512,10 +572,7 @@ export class CompanionClient {
512
572
  */
513
573
  async startCall(opts) {
514
574
  const id = this.requireSession();
515
- return this.postJson(`/api/companion/session/${id}/call`, {
516
- voice: opts?.voice,
517
- locale: opts?.locale
518
- });
575
+ return this.postJson(`/api/companion/session/${id}/call`, { voice: opts?.voice, locale: opts?.locale }, opts?.signal);
519
576
  }
520
577
  /** Open a live voice call end-to-end: mints credentials (startCall) and opens a
521
578
  * WebRTC session directly to the provider — no first-party app code needed.
@@ -651,7 +708,7 @@ export class CompanionClient {
651
708
  /** Report the result of a companion.tool_call this surface performed. When all
652
709
  * of the turn's calls are reported, the companion resumes and the continuation
653
710
  * (a companion.message or more tool calls) arrives on the event stream. */
654
- async sendToolResult(callId, result) {
711
+ async sendToolResult(callId, result, opts) {
655
712
  // Voice tool-call: resolve it locally so the result flows back to the voice
656
713
  // provider, not to a server text turn. (Same call shape as the text path.)
657
714
  const pending = this.pendingVoiceTools.get(callId);
@@ -660,7 +717,7 @@ export class CompanionClient {
660
717
  return { allDone: true };
661
718
  }
662
719
  const id = this.requireSession();
663
- const json = await this.postJson(`/api/companion/session/${id}/tool-result`, { callId, ok: result.ok !== false, result: result.result });
720
+ const json = await this.postJson(`/api/companion/session/${id}/tool-result`, { callId, ok: result.ok !== false, result: result.result }, opts?.signal);
664
721
  return { allDone: json.allDone ?? false };
665
722
  }
666
723
  /** A tool the live voice call asked the app to run. Fires the SAME
@@ -758,7 +815,10 @@ export class CompanionClient {
758
815
  }
759
816
  /** Convenience: subscribe to errors the companion surfaces — server-side agent
760
817
  * errors and stream failures (e.g. a permanent `stream_unauthorized`) both
761
- * arrive as `control.error`. Returns an unsubscribe fn. */
818
+ * arrive as `control.error`. Since 0.30.0 `code` is typed as
819
+ * `ControlErrorCodeValue` (the CONTROL_ERROR_CODES vocabulary + the
820
+ * SDK-synthesized stream codes, open for newer servers) so a `switch` on it
821
+ * autocompletes. Returns an unsubscribe fn. */
762
822
  onError(handler) {
763
823
  return this.on('control.error', (env) => {
764
824
  const p = env.payload;
@@ -928,8 +988,6 @@ export class CompanionClient {
928
988
  brandIconUrl(size = 512) {
929
989
  return pouchyBrandIconUrl(this.opts.baseUrl, size);
930
990
  }
931
- /** Subscribe to outbound events of a given type, or "*" for all. Returns an
932
- * unsubscribe function. */
933
991
  on(type, handler) {
934
992
  let set = this.handlers.get(type);
935
993
  if (!set) {
@@ -1084,6 +1142,7 @@ export class CompanionClient {
1084
1142
  return;
1085
1143
  this.requireSession();
1086
1144
  this.streaming = true;
1145
+ this.setStreamState('connecting');
1087
1146
  const gen = ++this.streamGen;
1088
1147
  const wsImpl = this.opts.webSocketImpl ?? globalThis.WebSocket;
1089
1148
  if (this.opts.stream === 'websocket' && wsImpl) {
@@ -1114,6 +1173,10 @@ export class CompanionClient {
1114
1173
  const ws = new WS(deriveWsStreamUrl(this.opts.baseUrl, id, this.opts.token, this.cursor));
1115
1174
  let poll;
1116
1175
  let settled = false;
1176
+ ws.onopen = () => {
1177
+ if (this.streaming && gen === this.streamGen)
1178
+ this.setStreamState('connected');
1179
+ };
1117
1180
  const settle = (err) => {
1118
1181
  if (poll) {
1119
1182
  clearInterval(poll);
@@ -1157,8 +1220,12 @@ export class CompanionClient {
1157
1220
  }
1158
1221
  if (errored) {
1159
1222
  // Socket unavailable / rejected — degrade to the robust SSE loop (once).
1160
- if (this.streaming && gen === this.streamGen)
1161
- await this.loop(gen);
1223
+ // The SSE loop's healthy state then reads `degraded_sse`, so the host
1224
+ // can observe the (previously invisible) WS→SSE degrade.
1225
+ if (this.streaming && gen === this.streamGen) {
1226
+ this.setStreamState('reconnecting');
1227
+ await this.loop(gen, true);
1228
+ }
1162
1229
  return;
1163
1230
  }
1164
1231
  // Clean close: the while-guard reconnects if still streaming, else exits.
@@ -1166,8 +1233,10 @@ export class CompanionClient {
1166
1233
  // closes — auth-as-close, LB idle policy, misconfigured proxy) must
1167
1234
  // back off like the SSE loop, or this reconnects in a zero-delay hot
1168
1235
  // loop. A connection that lived a while resets the backoff.
1236
+ if (this.streaming && gen === this.streamGen)
1237
+ this.setStreamState('reconnecting');
1169
1238
  if (Date.now() - connectedAt < 2000) {
1170
- await this.sleep(backoff);
1239
+ await this.backoffSleep(backoff);
1171
1240
  backoff = Math.min(backoff * 2, 8000);
1172
1241
  }
1173
1242
  else {
@@ -1181,6 +1250,7 @@ export class CompanionClient {
1181
1250
  this.streamGen++; // invalidate any loop still unwinding
1182
1251
  this.abort?.abort();
1183
1252
  this.abort = null;
1253
+ this.setStreamState('stopped');
1184
1254
  }
1185
1255
  emit(envelope) {
1186
1256
  // Dedup by envelope id — an ungraceful drop reconnects from the last KNOWN
@@ -1265,7 +1335,10 @@ export class CompanionClient {
1265
1335
  if (env && typeof env.type === 'string')
1266
1336
  this.emit(env);
1267
1337
  }
1268
- async loop(gen) {
1338
+ /** SSE receive loop. `degraded` marks that this loop is the WS transport's
1339
+ * fallback, so its healthy state reads `degraded_sse` (still live) instead
1340
+ * of `connected` — the host can tell "fine" from "fine, but on plan B". */
1341
+ async loop(gen, degraded = false) {
1269
1342
  const id = this.requireSession();
1270
1343
  let backoff = 500;
1271
1344
  let authRefreshes = 0; // consecutive 401→refresh cycles without a healthy connect
@@ -1299,23 +1372,27 @@ export class CompanionClient {
1299
1372
  message: `event stream rejected (${res.status}); the token likely lacks the "events.subscribe" scope`
1300
1373
  }
1301
1374
  });
1375
+ this.setStreamState('stopped');
1302
1376
  break;
1303
1377
  }
1304
1378
  if (!res.ok || !res.body) {
1305
1379
  if (!this.streaming || gen !== this.streamGen)
1306
1380
  break;
1307
- await this.sleep(backoff);
1381
+ this.setStreamState('reconnecting');
1382
+ await this.backoffSleep(backoff);
1308
1383
  backoff = Math.min(backoff * 2, 8000);
1309
1384
  continue;
1310
1385
  }
1311
1386
  backoff = 500; // healthy connection resets backoff
1312
1387
  authRefreshes = 0;
1388
+ this.setStreamState(degraded ? 'degraded_sse' : 'connected');
1313
1389
  await this.consume(res.body, gen);
1314
1390
  }
1315
1391
  catch {
1316
1392
  if (!this.streaming || gen !== this.streamGen)
1317
1393
  break;
1318
- await this.sleep(backoff);
1394
+ this.setStreamState('reconnecting');
1395
+ await this.backoffSleep(backoff);
1319
1396
  backoff = Math.min(backoff * 2, 8000);
1320
1397
  }
1321
1398
  }
@@ -1338,4 +1415,10 @@ export class CompanionClient {
1338
1415
  sleep(ms) {
1339
1416
  return new Promise((res) => setTimeout(res, ms));
1340
1417
  }
1418
+ /** Backoff sleep with additive jitter (0–25%, 0.30.0) so a fleet of embeds
1419
+ * doesn't reconnect in lockstep after a server deploy/restart (thundering
1420
+ * herd) — every reconnect path sleeps through here, never bare sleep(). */
1421
+ backoffSleep(ms) {
1422
+ return this.sleep(ms + Math.floor(Math.random() * ms * 0.25));
1423
+ }
1341
1424
  }
package/dist/errors.d.ts CHANGED
@@ -1,9 +1,22 @@
1
- import type { CompanionErrorCode } from './protocol.js';
1
+ import type { CompanionErrorCode, ControlErrorCode } from './protocol.js';
2
2
  /** Codes the SDK itself synthesizes (status 0 — never sent by the server).
3
3
  * Runtime array so the drift test can assert every `new CompanionError(...)`
4
4
  * call site uses a declared code; APPEND-ONLY like the server vocabulary. */
5
5
  export declare const SDK_SYNTHESIZED_ERROR_CODES: readonly ["reply_timeout", "needs_event_stream", "not_connected", "missing_option", "not_representative", "call_unsupported", "call_connect_failed", "call_dependency_missing", "aborted"];
6
6
  export type SdkSynthesizedErrorCode = (typeof SDK_SYNTHESIZED_ERROR_CODES)[number];
7
+ /** `control.error` STREAM codes the SDK itself synthesizes (never sent by the
8
+ * server) — the stream-plane sibling of SDK_SYNTHESIZED_ERROR_CODES above.
9
+ * Runtime array so the drift test can assert every locally-emitted
10
+ * `control.error` envelope uses a declared code; APPEND-ONLY. */
11
+ export declare const SDK_STREAM_ERROR_CODES: readonly ["stream_unauthorized"];
12
+ export type SdkStreamErrorCode = (typeof SDK_STREAM_ERROR_CODES)[number];
13
+ /** `control.error` `code` values a consumer can switch on (0.30.0): the server
14
+ * vocabulary (CONTROL_ERROR_CODES, drift-tested against the server list) plus
15
+ * the SDK-synthesized stream codes — DERIVED from the runtime lists rather
16
+ * than hand-retyped, same doctrine as CompanionErrorCodeValue below. The
17
+ * `(string & {})` arm keeps the type open for codes newer than this SDK build
18
+ * while preserving autocomplete. */
19
+ export type ControlErrorCodeValue = ControlErrorCode | SdkStreamErrorCode | (string & {});
7
20
  /** Thrown by every helper on an HTTP or client-side failure. `status` is the
8
21
  * HTTP status (0 for client-synthesized failures — timeouts, unsupported
9
22
  * environments, missing optional deps); `code` is machine-switchable when
package/dist/errors.js CHANGED
@@ -17,6 +17,13 @@ export const SDK_SYNTHESIZED_ERROR_CODES = [
17
17
  'call_dependency_missing', // optional voice dependency not installed
18
18
  'aborted' // a caller-supplied AbortSignal cancelled the request (0.29.0)
19
19
  ];
20
+ /** `control.error` STREAM codes the SDK itself synthesizes (never sent by the
21
+ * server) — the stream-plane sibling of SDK_SYNTHESIZED_ERROR_CODES above.
22
+ * Runtime array so the drift test can assert every locally-emitted
23
+ * `control.error` envelope uses a declared code; APPEND-ONLY. */
24
+ export const SDK_STREAM_ERROR_CODES = [
25
+ 'stream_unauthorized' // the event stream got a permanent 401/403 and reconnect gave up
26
+ ];
20
27
  export class CompanionError extends Error {
21
28
  status;
22
29
  code;
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { CompanionClient, type CompanionClientOptions } from './client.js';
2
2
  export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js';
3
- export type { CompanionErrorCodeValue } from './errors.js';
4
- export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, ToolCallEvent, WorldStateInput, CompanionAvatar, CompanionWallet, WalletBalance, BrandIconSize, EndSessionResult, SendTextReply } from './client.js';
3
+ export type { CompanionErrorCodeValue, ControlErrorCodeValue } from './errors.js';
4
+ export type { CompanionClientOptions, CompanionStreamState, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, ToolCallEvent, WorldStateInput, CompanionAvatar, CompanionWallet, WalletBalance, BrandIconSize, EndSessionResult, SendTextReply } from './client.js';
5
5
  export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_VISUAL_TOOLS, AVATAR_VISUAL_TOOL_NAMES } from './call.js';
6
6
  export type { CompanionCall, CompanionCallOptions, VoiceToolBridge } from './call.js';
7
7
  export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
8
8
  export type { CompanionScope, CompanionModality } from './scopes.js';
9
- export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, ToolCallPayload, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload, MessagePayload } from './protocol.js';
10
- export { PROTOCOL_VERSION, INBOUND_TYPES, OUTBOUND_TYPES, COMPANION_ERROR_CODES } from './protocol.js';
11
- export type { CompanionErrorCode } from './protocol.js';
9
+ export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, ToolCallPayload, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload, MessagePayload, ControlErrorPayload, HelloAckPayload, CallReadyPayload, OutboundPayloadMap } from './protocol.js';
10
+ export { PROTOCOL_VERSION, INBOUND_TYPES, OUTBOUND_TYPES, COMPANION_ERROR_CODES, CONTROL_ERROR_CODES } from './protocol.js';
11
+ export type { CompanionErrorCode, ControlErrorCode } from './protocol.js';
12
12
  /** Create a companion client. */
13
13
  export declare function createCompanion(opts: CompanionClientOptions): CompanionClient;
package/dist/index.js CHANGED
@@ -15,7 +15,9 @@ export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_
15
15
  export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
16
16
  // Runtime protocol constants — so a host can assert PROTOCOL_VERSION or
17
17
  // enumerate/validate the event vocabulary without hard-coding the strings.
18
- export { PROTOCOL_VERSION, INBOUND_TYPES, OUTBOUND_TYPES, COMPANION_ERROR_CODES } from './protocol.js';
18
+ // CONTROL_ERROR_CODES (0.30.0) is the stream-plane control.error vocabulary
19
+ // a separate, smaller list than the HTTP COMPANION_ERROR_CODES.
20
+ export { PROTOCOL_VERSION, INBOUND_TYPES, OUTBOUND_TYPES, COMPANION_ERROR_CODES, CONTROL_ERROR_CODES } from './protocol.js';
19
21
  /** Create a companion client. */
20
22
  export function createCompanion(opts) {
21
23
  return new CompanionClient(opts);
@@ -12,6 +12,15 @@ export type InboundType = (typeof INBOUND_TYPES)[number];
12
12
  export declare const OUTBOUND_TYPES: readonly ["hello.ack", "companion.message", "companion.audio", "companion.tool_call", "companion.ui_action", "companion.ui_update", "companion.expression", "companion.voice_inject", "companion.confirm_request", "companion.social_message", "companion.typing", "control.call_ready", "control.error", "control.usage"];
13
13
  export type OutboundType = (typeof OUTBOUND_TYPES)[number];
14
14
  export type MessageType = InboundType | OutboundType;
15
+ /** `control.error` STREAM-plane code vocabulary (0.30.0) — a separate, smaller
16
+ * vocabulary from COMPANION_ERROR_CODES below (those tag HTTP `{ok:false}`
17
+ * response bodies; these tag `control.error` envelopes on the event stream,
18
+ * surfaced by `onError`). Self-contained mirror of the server list
19
+ * (drift-tested); APPEND-ONLY. The SDK itself synthesizes one more,
20
+ * `stream_unauthorized` (errors.ts) — the public union `ControlErrorCodeValue`
21
+ * covers both plus a forward-compat string arm. */
22
+ export declare const CONTROL_ERROR_CODES: readonly ["agent_error", "call_mint_failed"];
23
+ export type ControlErrorCode = (typeof CONTROL_ERROR_CODES)[number];
15
24
  /** HTTP error `code` vocabulary — what `CompanionError.code` can hold. A
16
25
  * self-contained mirror of the server's api-error.ts list (drift-tested);
17
26
  * APPEND-ONLY: renaming or removing a code is a breaking SDK change.
@@ -187,3 +196,51 @@ export interface WorldStateEvent<D = unknown> {
187
196
  salience?: number;
188
197
  voiceRelevant?: boolean;
189
198
  }
199
+ /** Payload of a `control.error` event — a typed error on the event stream
200
+ * (surfaced by `onError`). `code` is one of CONTROL_ERROR_CODES, the
201
+ * SDK-synthesized `stream_unauthorized`, or a code newer than this SDK build
202
+ * (the open arm keeps forward compat without losing autocomplete). */
203
+ export interface ControlErrorPayload {
204
+ code: ControlErrorCode | (string & {});
205
+ message: string;
206
+ }
207
+ /** Payload of a `hello.ack` — the handshake reply. On the REST plane this is
208
+ * the `/session` response body (what `connect()` normalizes into `HelloAck`);
209
+ * the event stream's greeting `open` frame carries a partial echo (just
210
+ * `{ resumeCursor }`), so every field is optional at the wire level. */
211
+ export interface HelloAckPayload {
212
+ session?: string;
213
+ grantedScopes?: string[];
214
+ modalities?: string[];
215
+ resumeCursor?: number;
216
+ representative?: boolean;
217
+ visitorPaired?: boolean;
218
+ }
219
+ /** Payload of a `control.call_ready` event — the stream echo of `start_call`'s
220
+ * accept. Deliberately SECRET-FREE (`provider` plus non-secret metadata such
221
+ * as `agentId` / `model` / `voice`); the actual WebRTC credentials only ride
222
+ * the `startCall` HTTP response. */
223
+ export interface CallReadyPayload {
224
+ provider: string;
225
+ [k: string]: unknown;
226
+ }
227
+ /** Per-event payload types, keyed by outbound event name — what `client.on()`
228
+ * narrows envelopes with (0.30.0). Type-only; extending the base Record keeps
229
+ * indexing valid for every OutboundType, so an event added to the vocabulary
230
+ * before it gets a dedicated payload type simply reads as `unknown`. */
231
+ export interface OutboundPayloadMap extends Record<OutboundType, unknown> {
232
+ 'hello.ack': HelloAckPayload;
233
+ 'companion.message': MessagePayload;
234
+ 'companion.audio': AudioClipPayload;
235
+ 'companion.tool_call': ToolCallPayload;
236
+ 'companion.ui_action': RenderInterfacePayload;
237
+ 'companion.ui_update': InterfaceUpdatePayload;
238
+ 'companion.expression': ExpressionPayload;
239
+ 'companion.voice_inject': VoiceInjectPayload;
240
+ 'companion.confirm_request': ConfirmRequestPayload;
241
+ 'companion.social_message': SocialMessagePayload;
242
+ 'companion.typing': TypingPayload;
243
+ 'control.call_ready': CallReadyPayload;
244
+ 'control.error': ControlErrorPayload;
245
+ 'control.usage': UsagePayload;
246
+ }
package/dist/protocol.js CHANGED
@@ -40,6 +40,17 @@ export const OUTBOUND_TYPES = [
40
40
  'control.error',
41
41
  'control.usage' // reserved — not emitted yet
42
42
  ];
43
+ /** `control.error` STREAM-plane code vocabulary (0.30.0) — a separate, smaller
44
+ * vocabulary from COMPANION_ERROR_CODES below (those tag HTTP `{ok:false}`
45
+ * response bodies; these tag `control.error` envelopes on the event stream,
46
+ * surfaced by `onError`). Self-contained mirror of the server list
47
+ * (drift-tested); APPEND-ONLY. The SDK itself synthesizes one more,
48
+ * `stream_unauthorized` (errors.ts) — the public union `ControlErrorCodeValue`
49
+ * covers both plus a forward-compat string arm. */
50
+ export const CONTROL_ERROR_CODES = [
51
+ 'agent_error', // a server-side turn failed after the input was accepted — no reply was produced; safe to re-send
52
+ 'call_mint_failed' // start_call couldn't mint the voice-provider credential — retry later or fall back to text
53
+ ];
43
54
  /** HTTP error `code` vocabulary — what `CompanionError.code` can hold. A
44
55
  * self-contained mirror of the server's api-error.ts list (drift-tested);
45
56
  * APPEND-ONLY: renaming or removing a code is a breaking SDK change.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.29.0",
4
- "description": "Embed the Pouchy companion \u2014 chat, voice, tools, memory, live world-state, instant UI, and agent-to-agent messaging \u2014 in any app, game, or site.",
3
+ "version": "0.30.0",
4
+ "description": "Embed the Pouchy companion chat, voice, tools, memory, live world-state, instant UI, and agent-to-agent messaging in any app, game, or site.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "homepage": "https://pouchy.ai",
@@ -16,7 +16,8 @@
16
16
  "exports": {
17
17
  ".": {
18
18
  "types": "./dist/index.d.ts",
19
- "import": "./dist/index.js"
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
20
21
  }
21
22
  },
22
23
  "main": "./dist/index.js",