@pouchy_ai/companion-sdk 0.29.1 → 0.32.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +117 -1
  2. package/README.md +65 -4
  3. package/dist/adapter-core.d.ts +58 -0
  4. package/dist/adapter-core.d.ts.map +1 -0
  5. package/dist/adapter-core.js +126 -0
  6. package/dist/adapter-core.js.map +1 -0
  7. package/dist/call.d.ts +1 -0
  8. package/dist/call.d.ts.map +1 -0
  9. package/dist/call.js +1 -0
  10. package/dist/call.js.map +1 -0
  11. package/dist/client.d.ts +91 -5
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +138 -14
  14. package/dist/client.js.map +1 -0
  15. package/dist/errors.d.ts +15 -1
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +8 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/index.d.ts +9 -5
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +8 -1
  22. package/dist/index.js.map +1 -0
  23. package/dist/protocol.d.ts +58 -0
  24. package/dist/protocol.d.ts.map +1 -0
  25. package/dist/protocol.js +12 -0
  26. package/dist/protocol.js.map +1 -0
  27. package/dist/scopes.d.ts +1 -0
  28. package/dist/scopes.d.ts.map +1 -0
  29. package/dist/scopes.js +1 -0
  30. package/dist/scopes.js.map +1 -0
  31. package/dist/sse.d.ts +1 -0
  32. package/dist/sse.d.ts.map +1 -0
  33. package/dist/sse.js +1 -0
  34. package/dist/sse.js.map +1 -0
  35. package/dist/svelte.d.ts +16 -0
  36. package/dist/svelte.d.ts.map +1 -0
  37. package/dist/svelte.js +25 -0
  38. package/dist/svelte.js.map +1 -0
  39. package/dist/vue.d.ts +15 -0
  40. package/dist/vue.d.ts.map +1 -0
  41. package/dist/vue.js +27 -0
  42. package/dist/vue.js.map +1 -0
  43. package/dist/ws-transport.d.ts +1 -0
  44. package/dist/ws-transport.d.ts.map +1 -0
  45. package/dist/ws-transport.js +1 -0
  46. package/dist/ws-transport.js.map +1 -0
  47. package/package.json +24 -6
  48. package/src/adapter-core.ts +193 -0
  49. package/src/call.ts +699 -0
  50. package/src/client.ts +1959 -0
  51. package/src/errors.ts +77 -0
  52. package/src/index.ts +104 -0
  53. package/src/protocol.ts +316 -0
  54. package/src/scopes.ts +94 -0
  55. package/src/sse.ts +41 -0
  56. package/src/svelte.ts +47 -0
  57. package/src/vue.ts +46 -0
  58. package/src/ws-transport.ts +70 -0
package/src/errors.ts ADDED
@@ -0,0 +1,77 @@
1
+ // CompanionError — the one error type every SDK helper rejects with (README
2
+ // contract: consumers `switch` on `.code` instead of string-matching prose).
3
+ // Lives in its own module so call.ts can use it without a runtime cycle
4
+ // through client.ts (client.ts re-exports it, keeping the public import path
5
+ // `@pouchy_ai/companion-sdk` → `CompanionError` unchanged).
6
+
7
+ // Type-only import: protocol.ts imports nothing at runtime, so this cannot
8
+ // re-create the client.ts cycle this module exists to avoid.
9
+ import type { CompanionErrorCode, ControlErrorCode } from './protocol';
10
+
11
+ /** Codes the SDK itself synthesizes (status 0 — never sent by the server).
12
+ * Runtime array so the drift test can assert every `new CompanionError(...)`
13
+ * call site uses a declared code; APPEND-ONLY like the server vocabulary. */
14
+ export const SDK_SYNTHESIZED_ERROR_CODES = [
15
+ 'reply_timeout', // awaitReply/replyTimeoutMs elapsed before the reply
16
+ 'needs_event_stream', // turn paused on tools but start() was never called
17
+ 'not_connected', // a call/stream helper was used before connect
18
+ 'missing_option', // a required option was omitted (e.g. sessionId)
19
+ 'not_representative', // pairVisitor on a non-representative token
20
+ 'call_unsupported', // startCall on a server/provider without voice
21
+ 'call_connect_failed', // WebRTC/provider handshake failed
22
+ 'call_dependency_missing', // optional voice dependency not installed
23
+ 'aborted' // a caller-supplied AbortSignal cancelled the request (0.29.0)
24
+ ] as const;
25
+ export type SdkSynthesizedErrorCode = (typeof SDK_SYNTHESIZED_ERROR_CODES)[number];
26
+
27
+ /** `control.error` STREAM codes the SDK itself synthesizes (never sent by the
28
+ * server) — the stream-plane sibling of SDK_SYNTHESIZED_ERROR_CODES above.
29
+ * Runtime array so the drift test can assert every locally-emitted
30
+ * `control.error` envelope uses a declared code; APPEND-ONLY. */
31
+ export const SDK_STREAM_ERROR_CODES = [
32
+ 'stream_unauthorized' // the event stream got a permanent 401/403 and reconnect gave up
33
+ ] as const;
34
+ export type SdkStreamErrorCode = (typeof SDK_STREAM_ERROR_CODES)[number];
35
+
36
+ /** `control.error` `code` values a consumer can switch on (0.30.0): the server
37
+ * vocabulary (CONTROL_ERROR_CODES, drift-tested against the server list) plus
38
+ * the SDK-synthesized stream codes — DERIVED from the runtime lists rather
39
+ * than hand-retyped, same doctrine as CompanionErrorCodeValue below. The
40
+ * `(string & {})` arm keeps the type open for codes newer than this SDK build
41
+ * while preserving autocomplete. */
42
+ export type ControlErrorCodeValue =
43
+ | ControlErrorCode
44
+ | SdkStreamErrorCode
45
+ // eslint-disable-next-line @typescript-eslint/ban-types
46
+ | (string & {});
47
+
48
+ /** Thrown by every helper on an HTTP or client-side failure. `status` is the
49
+ * HTTP status (0 for client-synthesized failures — timeouts, unsupported
50
+ * environments, missing optional deps); `code` is machine-switchable when
51
+ * the server (or the SDK itself) names a cause. On a `rate_limited` (429)
52
+ * failure, `retryAfter` carries the server's Retry-After in SECONDS so a
53
+ * consumer can back off precisely instead of guessing. */
54
+ /** `CompanionError.code` values: the server vocabulary (COMPANION_ERROR_CODES,
55
+ * drift-tested against api-error.ts) plus the SDK-synthesized ones — DERIVED
56
+ * from those runtime lists rather than hand-retyped, so a new code can never
57
+ * silently miss this union again (the old copy had drifted 5 codes behind).
58
+ * The `(string & {})` arm keeps the type open for codes newer than this SDK
59
+ * build while preserving autocomplete. */
60
+ export type CompanionErrorCodeValue =
61
+ | CompanionErrorCode
62
+ | SdkSynthesizedErrorCode
63
+ // eslint-disable-next-line @typescript-eslint/ban-types
64
+ | (string & {});
65
+
66
+ export class CompanionError extends Error {
67
+ status: number;
68
+ code?: CompanionErrorCodeValue;
69
+ retryAfter?: number;
70
+ constructor(message: string, status: number, code?: string, retryAfter?: number) {
71
+ super(message);
72
+ this.name = 'CompanionError';
73
+ this.status = status;
74
+ if (code) this.code = code;
75
+ if (typeof retryAfter === 'number' && Number.isFinite(retryAfter)) this.retryAfter = retryAfter;
76
+ }
77
+ }
package/src/index.ts ADDED
@@ -0,0 +1,104 @@
1
+ // Companion SDK — public entry. Embed the Pouchy companion in a third-party
2
+ // surface (web / app / game / hardware) over the REST/SSE plane (with an
3
+ // optional WebSocket transport that degrades to SSE).
4
+ //
5
+ // import { createCompanion } from '$lib/companion-sdk';
6
+ // const c = createCompanion({ baseUrl: 'https://pouchy.ai', token: PAT });
7
+ // await c.connect();
8
+ // c.onMessage((text) => console.log(text));
9
+ // c.start();
10
+ // c.sendWorldState({ type: 'game.scene', data: 'tavern', retained: true });
11
+ // await c.sendText('what should I do?');
12
+
13
+ import { CompanionClient, type CompanionClientOptions } from './client';
14
+
15
+ export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client';
16
+ export type { CompanionDebugEvent } from './client';
17
+ export type { CompanionErrorCodeValue, ControlErrorCodeValue } from './errors';
18
+ export type {
19
+ CompanionClientOptions,
20
+ CompanionStreamState,
21
+ HelloAck,
22
+ RecalledMemory,
23
+ CompanionTurn,
24
+ CallCredentials,
25
+ CompanionToolDecl,
26
+ ToolCallEvent,
27
+ WorldStateInput,
28
+ CompanionAvatar,
29
+ CompanionWallet,
30
+ WalletBalance,
31
+ BrandIconSize,
32
+ EndSessionResult,
33
+ SendTextReply
34
+ } from './client';
35
+ export {
36
+ openCompanionCall,
37
+ HOST_CONTROL_TOOLS,
38
+ HOST_CONTROL_TOOL_NAMES,
39
+ AVATAR_VISUAL_TOOLS,
40
+ AVATAR_VISUAL_TOOL_NAMES
41
+ } from './call';
42
+ export type { CompanionCall, CompanionCallOptions, VoiceToolBridge } from './call';
43
+ export {
44
+ COMPANION_SCOPES,
45
+ COMPANION_MODALITIES,
46
+ SENSITIVE_SCOPES,
47
+ REPRESENT_SCOPES,
48
+ DEFAULT_SCOPES,
49
+ DEFAULT_MODALITIES,
50
+ isSensitiveScope,
51
+ isRepresentScope,
52
+ hasScope
53
+ } from './scopes';
54
+ export type { CompanionScope, CompanionModality } from './scopes';
55
+ export type {
56
+ CompanionEnvelope,
57
+ OutboundType,
58
+ InboundType,
59
+ WorldStateEvent,
60
+ RenderInterfacePayload,
61
+ InterfaceUpdatePayload,
62
+ SocialMessagePayload,
63
+ ConfirmRequestPayload,
64
+ PendingConfirm,
65
+ ToolCallPayload,
66
+ AudioClipPayload,
67
+ ExpressionPayload,
68
+ TypingPayload,
69
+ VoiceInjectPayload,
70
+ UsagePayload,
71
+ MessagePayload,
72
+ ControlErrorPayload,
73
+ HelloAckPayload,
74
+ CallReadyPayload,
75
+ OutboundPayloadMap
76
+ } from './protocol';
77
+ // Runtime protocol constants — so a host can assert PROTOCOL_VERSION or
78
+ // enumerate/validate the event vocabulary without hard-coding the strings.
79
+ // CONTROL_ERROR_CODES (0.30.0) is the stream-plane control.error vocabulary —
80
+ // a separate, smaller list than the HTTP COMPANION_ERROR_CODES.
81
+ export {
82
+ PROTOCOL_VERSION,
83
+ INBOUND_TYPES,
84
+ OUTBOUND_TYPES,
85
+ COMPANION_ERROR_CODES,
86
+ CONTROL_ERROR_CODES
87
+ } from './protocol';
88
+ export type { CompanionErrorCode, ControlErrorCode } from './protocol';
89
+
90
+ // The framework-agnostic view controller behind the /react, /svelte and /vue
91
+ // subpath adapters — exported from the root too, so a host on any OTHER
92
+ // framework (or none) can reuse the same tested snapshot bookkeeping.
93
+ export { createCompanionView } from './adapter-core';
94
+ export type {
95
+ CompanionView,
96
+ CompanionViewOptions,
97
+ CompanionViewSnapshot,
98
+ CompanionTranscriptEntry
99
+ } from './adapter-core';
100
+
101
+ /** Create a companion client. */
102
+ export function createCompanion(opts: CompanionClientOptions): CompanionClient {
103
+ return new CompanionClient(opts);
104
+ }
@@ -0,0 +1,316 @@
1
+ // The wire contract, as the SDK sees it — a self-contained copy so the package
2
+ // has ZERO repo-internal ($lib) imports and publishes / builds with plain tsc.
3
+ //
4
+ // This mirrors the server's source of truth (src/lib/types/companion-protocol.ts);
5
+ // a drift test (protocol.drift.test.ts) keeps PROTOCOL_VERSION + the type unions
6
+ // in sync. Adding an outbound type is non-breaking (receivers ignore unknowns).
7
+
8
+ export const PROTOCOL_VERSION = 1 as const;
9
+
10
+ /** App → Pouchy (control/data plane). */
11
+ export const INBOUND_TYPES = [
12
+ 'hello',
13
+ 'input.text',
14
+ 'context.event',
15
+ 'context.snapshot',
16
+ 'tool.result',
17
+ 'control.start_call',
18
+ 'control.end_call',
19
+ 'control.set_modalities',
20
+ 'control.ping'
21
+ ] as const;
22
+ export type InboundType = (typeof INBOUND_TYPES)[number];
23
+
24
+ /** Pouchy → app (control/data plane).
25
+ * NOTE: three types are RESERVED — part of the vocabulary + typed handlers, but
26
+ * the server does NOT emit them yet, so `onAudio` / `onExpression` / `onUsage`
27
+ * are safe to register but won't fire until a producer ships. Don't build a UX
28
+ * that depends on them today. (companion.audio → non-call reply TTS;
29
+ * companion.expression → avatar viseme/expression cues; control.usage →
30
+ * per-turn usage-metering echo.) */
31
+ export const OUTBOUND_TYPES = [
32
+ 'hello.ack',
33
+ 'companion.message',
34
+ 'companion.audio', // reserved — not emitted yet
35
+ 'companion.tool_call',
36
+ 'companion.ui_action',
37
+ 'companion.ui_update',
38
+ 'companion.expression', // reserved — not emitted yet
39
+ 'companion.voice_inject',
40
+ 'companion.confirm_request',
41
+ 'companion.social_message',
42
+ 'companion.typing',
43
+ 'control.call_ready',
44
+ 'control.error',
45
+ 'control.usage' // reserved — not emitted yet
46
+ ] as const;
47
+ export type OutboundType = (typeof OUTBOUND_TYPES)[number];
48
+
49
+ export type MessageType = InboundType | OutboundType;
50
+
51
+ /** `control.error` STREAM-plane code vocabulary (0.30.0) — a separate, smaller
52
+ * vocabulary from COMPANION_ERROR_CODES below (those tag HTTP `{ok:false}`
53
+ * response bodies; these tag `control.error` envelopes on the event stream,
54
+ * surfaced by `onError`). Self-contained mirror of the server list
55
+ * (drift-tested); APPEND-ONLY. The SDK itself synthesizes one more,
56
+ * `stream_unauthorized` (errors.ts) — the public union `ControlErrorCodeValue`
57
+ * covers both plus a forward-compat string arm. */
58
+ export const CONTROL_ERROR_CODES = [
59
+ 'agent_error', // a server-side turn failed after the input was accepted — no reply was produced; safe to re-send
60
+ 'call_mint_failed' // start_call couldn't mint the voice-provider credential — retry later or fall back to text
61
+ ] as const;
62
+ export type ControlErrorCode = (typeof CONTROL_ERROR_CODES)[number];
63
+
64
+ /** HTTP error `code` vocabulary — what `CompanionError.code` can hold. A
65
+ * self-contained mirror of the server's api-error.ts list (drift-tested);
66
+ * APPEND-ONLY: renaming or removing a code is a breaking SDK change.
67
+ * Consumers can switch on these instead of string-matching error prose. */
68
+ export const COMPANION_ERROR_CODES = [
69
+ 'missing_token', // 401 — no Authorization: Bearer header
70
+ 'invalid_token', // 401 — token unknown, revoked, or expired
71
+ 'missing_scope', // 403 — token lacks a required grant
72
+ 'invalid_request', // 400 — malformed JSON / missing or invalid fields
73
+ 'session_not_found', // 404 — sessionId expired or never started
74
+ 'turn_pending', // 409 — a turn is paused awaiting tool results
75
+ 'no_pending_tools', // 409 — tool result posted but nothing is pending
76
+ 'unknown_call', // 404 — tool result for a callId the turn didn't issue
77
+ 'payload_too_large', // 413 — body/image over the documented cap
78
+ 'rate_limited', // 429 — turn burst / demo daily budget spent; Retry-After + retryAfterSec say when
79
+ 'forbidden', // 403 — authorization denial other than a missing scope
80
+ 'unavailable', // 503 — backing store/provider not configured or down
81
+ 'confirm_not_found', // 404 — confirmId unknown or not on this session
82
+ 'confirm_resolved', // 409 — confirm already approved/denied/expired (single-use)
83
+ 'step_up_required', // 401 — approval needs a passkey assertion (fetch confirm/stepup)
84
+ 'step_up_failed' // 401 — the passkey assertion didn't verify; retry the step-up
85
+ ] as const;
86
+ export type CompanionErrorCode = (typeof COMPANION_ERROR_CODES)[number];
87
+
88
+ /** The single envelope every control/data-plane message shares. */
89
+ export interface CompanionEnvelope<T = unknown> {
90
+ v: typeof PROTOCOL_VERSION;
91
+ id: string;
92
+ session?: string;
93
+ ts: number;
94
+ type: MessageType;
95
+ payload: T;
96
+ }
97
+
98
+ /** Payload of a `companion.ui_action` event — an Instant UI surface for the host
99
+ * to render. `interface` is the platform-neutral genui schema: a tree of UI
100
+ * `nodes` (Text/Slider/Button/Chart/…), an optional `state` bag input atoms
101
+ * read+write, and optional `computed` derived values. The SAME shape is drawn by
102
+ * a web embed, a native iOS/Android renderer, or a CLI/TUI — only the renderer
103
+ * differs per platform, never the contract. Kept structurally loose here (the
104
+ * SDK package carries no $lib import); the authoritative schema lives in
105
+ * src/lib/genui/schema.ts and the renderer-contract doc. When the panel sets
106
+ * `reportChanges`, the user's edits stream back as a normal text turn. */
107
+ export interface RenderInterfacePayload {
108
+ interface: {
109
+ title?: string;
110
+ nodes: Array<{ type: string; [k: string]: unknown }>;
111
+ state?: Record<string, string | number | boolean>;
112
+ computed?: Record<string, unknown>;
113
+ reportChanges?: boolean;
114
+ };
115
+ }
116
+
117
+ /** Payload of a `companion.confirm_request` event — the companion is asking the
118
+ * user to approve a sensitive op (a payment, a skill run, a friend message).
119
+ * Who may approve depends on the token: a PLATFORM SESSION TOKEN (an end-user
120
+ * instance minted via /v1/sessions) resolves it directly with
121
+ * `client.confirmAction` — the embedding app's end user is that account's
122
+ * human. A first-party user's token can only OBSERVE: approval of a real
123
+ * Pouchy user's ops is authed as that user (their app / a hosted confirm
124
+ * page), so a third-party DOM can never confirm a spend, and the first-party
125
+ * host can gate approval behind a biometric (Face ID / passkey) — see
126
+ * docs/companion-wallet-noncustodial.md. */
127
+ export interface ConfirmRequestPayload {
128
+ /** Opaque id the first-party surface passes to the confirm endpoint. */
129
+ confirmId: string;
130
+ /** The scope of the op needing approval (e.g. `wallet.spend`). */
131
+ scope: string;
132
+ /** A human, user-facing summary of what approving will do. */
133
+ summary: string;
134
+ /** Advisory: when true, the first-party approval surface should require a
135
+ * stronger user gesture — a biometric / passkey (Face ID, Touch ID) — before
136
+ * approving, not just a tap. Set for irreversible money ops (wallet.spend).
137
+ * Cryptographic enforcement is server-side follow-up; this is the host's cue. */
138
+ stepUp?: boolean;
139
+ }
140
+
141
+ /** One still-pending confirmation, as returned by `client.pendingConfirms()` —
142
+ * display-safe (never the recorded tool call or its raw args). */
143
+ export interface PendingConfirm {
144
+ confirmId: string;
145
+ /** The scope of the op needing approval (e.g. `skills.execute`). */
146
+ scope: string;
147
+ /** A human, user-facing summary of what approving will do. */
148
+ summary: string;
149
+ /** Epoch ms when the companion asked. */
150
+ createdAt: number;
151
+ }
152
+
153
+ /** Payload of a `companion.tool_call` event — the companion is asking the host
154
+ * to run one of the app-declared tools it was given at `hello`. `args` is the
155
+ * raw JSON string the model produced (parse it yourself); reply by posting the
156
+ * result back with `client.sendToolResult(id, …)`. */
157
+ export interface ToolCallPayload {
158
+ /** Correlation id — echo it back on the tool result. */
159
+ id: string;
160
+ /** The declared tool name the model chose. */
161
+ name: string;
162
+ /** The model's arguments as a raw JSON string. */
163
+ args: string;
164
+ }
165
+
166
+ /** Payload of a `companion.ui_update` event — a live change to an
167
+ * already-rendered Instant UI panel: write each `{ key, value }` into the
168
+ * panel's state bag (re-evaluating templates / progress / `when`), without
169
+ * rebuilding. `panelId` targets one of several on-screen panels when present. */
170
+ export interface InterfaceUpdatePayload {
171
+ update: {
172
+ panelId?: string;
173
+ updates: Array<{ key: string; value: string | number | boolean }>;
174
+ };
175
+ }
176
+
177
+ /** Payload of a `companion.audio` event — a TTS clip for the assistant's reply on
178
+ * a non-call modality. Either a playable `url` or an opaque `ref` the host
179
+ * resolves. */
180
+ export interface AudioClipPayload {
181
+ url?: string;
182
+ ref?: string;
183
+ }
184
+
185
+ /** Payload of a `companion.expression` event — avatar cues for an embed rendering
186
+ * the companion's body (e.g. a VRM): a lip-sync `viseme`, an `expression`, or a
187
+ * `gesture`. A pure-text/audio host can ignore it. */
188
+ export interface ExpressionPayload {
189
+ viseme?: string;
190
+ expression?: string;
191
+ gesture?: string;
192
+ }
193
+
194
+ /** Payload of a `control.usage` event — a per-token metering echo the host can
195
+ * surface in a usage/billing view. */
196
+ export interface UsagePayload {
197
+ chatTurns?: number;
198
+ voiceSeconds?: number;
199
+ memoryOps?: number;
200
+ }
201
+
202
+ /** Payload of a `companion.social_message` event — an inbound A2A message from
203
+ * one of the user's paired friends, delivered cross-app to any live embed whose
204
+ * token holds `social.message`. Lets a native/web host surface friend messages
205
+ * in real time without the first-party Pouchy app. Only plain human messages are
206
+ * delivered (Pouchy-internal structured envelopes are not). */
207
+ export interface SocialMessagePayload {
208
+ /** The sender's Pouchy uid. */
209
+ fromUid: string;
210
+ /** The sender's display name. */
211
+ fromName: string;
212
+ /** The message text. */
213
+ content: string;
214
+ /** ISO timestamp of delivery. */
215
+ createdAt: string;
216
+ }
217
+
218
+ /** Payload of a `companion.voice_inject` event — a `voiceRelevant` world-state
219
+ * line the companion should say aloud DURING a live call (the app pushed the
220
+ * moment via `sendWorldState`; the server decided it's worth voicing). `speak`
221
+ * is the host's cue to route `text` to the active voice session. A text-only
222
+ * host can ignore it — the moment still shaped the next turn's context. */
223
+ export interface VoiceInjectPayload {
224
+ text: string;
225
+ speak: boolean;
226
+ }
227
+
228
+ /** Payload of a `companion.typing` event — an activity indicator bracketing the
229
+ * model work of a turn: `active:true` when the companion starts working,
230
+ * `active:false` when it emits its reply or pauses for an app tool result. It
231
+ * spans the whole turn — the tool-loop / thinking phase BEFORE the first text
232
+ * delta, which `onDelta` can't observe — so a host can show a "typing…" state
233
+ * uniformly across streaming and non-streaming replies. Advisory: a lost event
234
+ * never affects the turn; the terminal `companion.message` is authoritative. */
235
+ export interface TypingPayload {
236
+ active: boolean;
237
+ }
238
+
239
+ /** Payload of a `companion.message` event — the companion's text reply. When
240
+ * the turn was started by this client with sendText, `replyTo` echoes the
241
+ * request's turnId (0.28.0) so a reply can be correlated to its turn;
242
+ * proactive messages (world-state reactions, confirm outcomes) carry none. */
243
+ export interface MessagePayload {
244
+ text: string;
245
+ replyTo?: string;
246
+ }
247
+
248
+ /** A live world-state event the app streams in. `retained:true` = latest-value
249
+ * state (coalesced); `retained:false` = a transient moment carrying `salience`
250
+ * and optional `voiceRelevant`. */
251
+ export interface WorldStateEvent<D = unknown> {
252
+ specversion: '1.0';
253
+ id: string;
254
+ source: string;
255
+ /** Namespaced dotted kind, e.g. `game.player.hp`, `game.event.boss_spawned`. */
256
+ type: string;
257
+ time?: string;
258
+ data: D;
259
+ retained?: boolean;
260
+ ttl?: number;
261
+ salience?: number;
262
+ voiceRelevant?: boolean;
263
+ }
264
+
265
+ /** Payload of a `control.error` event — a typed error on the event stream
266
+ * (surfaced by `onError`). `code` is one of CONTROL_ERROR_CODES, the
267
+ * SDK-synthesized `stream_unauthorized`, or a code newer than this SDK build
268
+ * (the open arm keeps forward compat without losing autocomplete). */
269
+ export interface ControlErrorPayload {
270
+ // eslint-disable-next-line @typescript-eslint/ban-types
271
+ code: ControlErrorCode | (string & {});
272
+ message: string;
273
+ }
274
+
275
+ /** Payload of a `hello.ack` — the handshake reply. On the REST plane this is
276
+ * the `/session` response body (what `connect()` normalizes into `HelloAck`);
277
+ * the event stream's greeting `open` frame carries a partial echo (just
278
+ * `{ resumeCursor }`), so every field is optional at the wire level. */
279
+ export interface HelloAckPayload {
280
+ session?: string;
281
+ grantedScopes?: string[];
282
+ modalities?: string[];
283
+ resumeCursor?: number;
284
+ representative?: boolean;
285
+ visitorPaired?: boolean;
286
+ }
287
+
288
+ /** Payload of a `control.call_ready` event — the stream echo of `start_call`'s
289
+ * accept. Deliberately SECRET-FREE (`provider` plus non-secret metadata such
290
+ * as `agentId` / `model` / `voice`); the actual WebRTC credentials only ride
291
+ * the `startCall` HTTP response. */
292
+ export interface CallReadyPayload {
293
+ provider: string;
294
+ [k: string]: unknown;
295
+ }
296
+
297
+ /** Per-event payload types, keyed by outbound event name — what `client.on()`
298
+ * narrows envelopes with (0.30.0). Type-only; extending the base Record keeps
299
+ * indexing valid for every OutboundType, so an event added to the vocabulary
300
+ * before it gets a dedicated payload type simply reads as `unknown`. */
301
+ export interface OutboundPayloadMap extends Record<OutboundType, unknown> {
302
+ 'hello.ack': HelloAckPayload;
303
+ 'companion.message': MessagePayload;
304
+ 'companion.audio': AudioClipPayload;
305
+ 'companion.tool_call': ToolCallPayload;
306
+ 'companion.ui_action': RenderInterfacePayload;
307
+ 'companion.ui_update': InterfaceUpdatePayload;
308
+ 'companion.expression': ExpressionPayload;
309
+ 'companion.voice_inject': VoiceInjectPayload;
310
+ 'companion.confirm_request': ConfirmRequestPayload;
311
+ 'companion.social_message': SocialMessagePayload;
312
+ 'companion.typing': TypingPayload;
313
+ 'control.call_ready': CallReadyPayload;
314
+ 'control.error': ControlErrorPayload;
315
+ 'control.usage': UsagePayload;
316
+ }
package/src/scopes.ts ADDED
@@ -0,0 +1,94 @@
1
+ // Companion capability scopes — the SDK's self-contained copy (no $lib
2
+ // imports; same doctrine as protocol.ts). The server's source of truth is
3
+ // src/lib/types/companion-scopes.ts; protocol.drift.test.ts fails if the two
4
+ // diverge — extend both together.
5
+ //
6
+ // Why the SDK exports these: a host app configuring a key, gating a feature
7
+ // on `hello.ack.grantedScopes`, or explaining a `403 missing scope` needs the
8
+ // vocabulary as types and constants, not prose. Typos in scope strings become
9
+ // compile errors instead of runtime 403s.
10
+
11
+ /** Every capability scope a Personal Access Token / OAuth grant can carry. */
12
+ export const COMPANION_SCOPES = [
13
+ 'chat', // text turns
14
+ 'voice', // TTS / STT
15
+ 'call', // realtime WebRTC voice session
16
+ 'files', // multimodal file in/out (vision / image)
17
+ 'events.subscribe', // receive the outbound companion event stream
18
+ 'worldstate.write', // push inbound live world-state / context events
19
+ 'ui.render', // let the companion render Instant UI surfaces on the host (display + forms; not sensitive)
20
+ 'memory.read:app', // read this app's own memory namespace
21
+ 'memory.write:app', // write into this app's own memory namespace
22
+ 'memory.read:core', // read the shared global brain (SENSITIVE)
23
+ 'memory.write:core', // write into the shared global brain (SENSITIVE)
24
+ 'skills.execute', // run the user's installed skills (SENSITIVE)
25
+ 'wallet.read', // READ-ONLY wallet: balance + own deposit address (receive-only; not sensitive). GET /api/companion/wallet (SDK: getWallet(), 0.20.0+) or ask the companion's wallet tools.
26
+ 'wallet.spend', // spend from the Care Wallet (SENSITIVE)
27
+ 'social.message', // message friends via A2A (SENSITIVE)
28
+ // ── Representative ("on-behalf-of") plane ─────────────────────────────
29
+ 'represent', // open visitor-facing representative sessions (SENSITIVE)
30
+ 'expose:knowledge', // let the representative draw on the owner's knowledge base (SENSITIVE)
31
+ 'expose:facts', // widen exposed facts past the identity/preference floor (SENSITIVE)
32
+ 'represent:pair', // let a representative pair a visitor (also a Pouchy user) as a friend (SENSITIVE)
33
+ 'represent:remember' // let a representative keep durable per-visitor notes across visits (SENSITIVE)
34
+ ] as const;
35
+ export type CompanionScope = (typeof COMPANION_SCOPES)[number];
36
+
37
+ /** I/O modalities a key can carry alongside its scopes. */
38
+ export const COMPANION_MODALITIES = ['text', 'voice', 'call', 'files'] as const;
39
+ export type CompanionModality = (typeof COMPANION_MODALITIES)[number];
40
+
41
+ /** Sensitive scopes — never granted by default; the user opts in at key
42
+ * creation, and execution still passes a confirm on a Pouchy-controlled
43
+ * surface (see `companion.confirm_request`). */
44
+ export const SENSITIVE_SCOPES: ReadonlySet<CompanionScope> = new Set<CompanionScope>([
45
+ 'memory.read:core',
46
+ 'memory.write:core',
47
+ 'skills.execute',
48
+ 'wallet.spend',
49
+ 'social.message',
50
+ 'represent',
51
+ 'expose:knowledge',
52
+ 'expose:facts',
53
+ 'represent:pair',
54
+ 'represent:remember'
55
+ ]);
56
+
57
+ /** The representative ("on-behalf-of") plane — a distinct axis from the
58
+ * execution scopes: one lets the companion ACT for the owner, the other lets
59
+ * it REPRESENT the owner to an outside visitor. `represent` is the umbrella;
60
+ * the `expose:*` / `represent:*` scopes only mean anything alongside it. */
61
+ export const REPRESENT_SCOPES: ReadonlySet<CompanionScope> = new Set<CompanionScope>([
62
+ 'represent',
63
+ 'expose:knowledge',
64
+ 'expose:facts',
65
+ 'represent:pair',
66
+ 'represent:remember'
67
+ ]);
68
+
69
+ /** What a freshly-generated key carries unless the user opts into more: a
70
+ * chat/play companion confined to its own app memory namespace. */
71
+ export const DEFAULT_SCOPES: readonly CompanionScope[] = [
72
+ 'chat',
73
+ 'voice',
74
+ 'call',
75
+ 'files',
76
+ 'events.subscribe',
77
+ 'worldstate.write',
78
+ 'memory.read:app',
79
+ 'memory.write:app'
80
+ ];
81
+ export const DEFAULT_MODALITIES: readonly CompanionModality[] = ['text', 'voice'];
82
+
83
+ export function isSensitiveScope(s: CompanionScope): boolean {
84
+ return SENSITIVE_SCOPES.has(s);
85
+ }
86
+ export function isRepresentScope(s: CompanionScope): boolean {
87
+ return REPRESENT_SCOPES.has(s);
88
+ }
89
+
90
+ /** Does a granted scope set (e.g. `hello.ack.grantedScopes`) satisfy a
91
+ * required scope? Exact-match for v1 (no hierarchical wildcards). */
92
+ export function hasScope(granted: readonly string[], required: CompanionScope): boolean {
93
+ return granted.includes(required);
94
+ }
package/src/sse.ts ADDED
@@ -0,0 +1,41 @@
1
+ // Minimal, dependency-free SSE frame parser for the companion SDK. The browser's
2
+ // EventSource can't set an Authorization header, so the SDK consumes the stream
3
+ // over fetch() instead and parses frames itself — this is that parser, kept pure
4
+ // so it unit-tests in isolation.
5
+ //
6
+ // An SSE frame is a block of `field: value` lines terminated by a blank line.
7
+ // Comment lines (starting ":") are keep-alives and ignored. A chunk off the wire
8
+ // may end mid-frame, so the parser returns the leftover for the next call.
9
+
10
+ export interface SseEvent {
11
+ /** The `event:` field; defaults to "message" per the SSE spec. */
12
+ event: string;
13
+ /** The joined `data:` field(s). */
14
+ data: string;
15
+ }
16
+
17
+ /** Parse all COMPLETE frames out of an accumulated buffer. Returns the parsed
18
+ * events plus the unparsed remainder (an incomplete trailing frame). */
19
+ export function parseSse(buffer: string): { events: SseEvent[]; rest: string } {
20
+ const events: SseEvent[] = [];
21
+ let working = buffer.replace(/\r\n/g, '\n');
22
+ let sep = working.indexOf('\n\n');
23
+ while (sep !== -1) {
24
+ const block = working.slice(0, sep);
25
+ working = working.slice(sep + 2);
26
+ let event = 'message';
27
+ const dataLines: string[] = [];
28
+ for (const line of block.split('\n')) {
29
+ if (!line || line.startsWith(':')) continue; // blank or comment (keep-alive)
30
+ const ci = line.indexOf(':');
31
+ const field = ci === -1 ? line : line.slice(0, ci);
32
+ let value = ci === -1 ? '' : line.slice(ci + 1);
33
+ if (value.startsWith(' ')) value = value.slice(1); // single leading space stripped
34
+ if (field === 'event') event = value;
35
+ else if (field === 'data') dataLines.push(value);
36
+ }
37
+ if (dataLines.length) events.push({ event, data: dataLines.join('\n') });
38
+ sep = working.indexOf('\n\n');
39
+ }
40
+ return { events, rest: working };
41
+ }
package/src/svelte.ts ADDED
@@ -0,0 +1,47 @@
1
+ // Svelte adapter (0.31.0) — `@pouchy_ai/companion-sdk/svelte`.
2
+ // Implements the Svelte STORE CONTRACT (subscribe(run) → unsubscribe) by hand,
3
+ // so it needs no `svelte` import and works from Svelte 3 through 5 (`$store`
4
+ // auto-subscription, or `fromStore()` in runes mode). Behavior lives in
5
+ // adapter-core.ts.
6
+ //
7
+ // const companion = companionStore(client);
8
+ // $companion.transcript / $companion.draft / $companion.streamState
9
+ // companion.sendText('hi');
10
+
11
+ import type { CompanionClient } from './client';
12
+ import {
13
+ createCompanionView,
14
+ type CompanionView,
15
+ type CompanionViewOptions,
16
+ type CompanionViewSnapshot
17
+ } from './adapter-core';
18
+
19
+ /** Svelte-store-contract readable of the companion view snapshot. */
20
+ export interface CompanionStore {
21
+ /** Store contract: calls `run` with the current snapshot immediately and on
22
+ * every change. Returns unsubscribe. */
23
+ subscribe(run: (snapshot: CompanionViewSnapshot) => void): () => void;
24
+ sendText: CompanionView['sendText'];
25
+ confirmAction: CompanionView['confirmAction'];
26
+ /** Detach from the client (does not close it). */
27
+ dispose(): void;
28
+ client: CompanionClient;
29
+ }
30
+
31
+ /** Wrap a host-owned client as a Svelte-compatible readable store. */
32
+ export function companionStore(
33
+ client: CompanionClient,
34
+ opts?: CompanionViewOptions
35
+ ): CompanionStore {
36
+ const view = createCompanionView(client, opts);
37
+ return {
38
+ subscribe(run) {
39
+ run(view.getSnapshot());
40
+ return view.subscribe(() => run(view.getSnapshot()));
41
+ },
42
+ sendText: view.sendText,
43
+ confirmAction: view.confirmAction,
44
+ dispose: view.dispose,
45
+ client
46
+ };
47
+ }