@pouchy_ai/companion-sdk 0.19.1 → 0.21.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,46 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.21.0] - 2026-07-11
16
+
17
+ ### Added
18
+
19
+ - **Token refresh:** `client.setToken(token)` swaps the bearer used by every
20
+ subsequent request and stream reconnect, and the new `onAuthError` client
21
+ option refreshes on demand — a 401 (REST **or** event stream) calls it; return
22
+ a fresh token (e.g. re-minted via `POST /v1/sessions`) and the client retries
23
+ transparently, return `null` to surface the failure exactly as before.
24
+ Concurrent 401s share one refresh; the stream loop caps consecutive
25
+ refresh-and-reject cycles at 3 so a bad hook can't hot-loop. Previously a
26
+ 1-hour session token expiring mid-embed silently stopped reply delivery
27
+ (`stream_unauthorized`) with no recovery short of rebuilding the client.
28
+ - **Semantic recall:** `recall({ query })` — the server has always supported
29
+ `?q=` semantic search over the token-visible facts; the client now exposes it.
30
+ Without `query` the ranking is importance/recency, as before.
31
+ - **Machine-readable error codes:** companion API error responses now carry a
32
+ stable `code` (`missing_token` / `invalid_token` / `missing_scope` /
33
+ `invalid_request` / `session_not_found` / `turn_pending` / `no_pending_tools` /
34
+ `unknown_call` / `payload_too_large` / `forbidden` / `unavailable`), so
35
+ `CompanionError.code` is finally switchable for HTTP failures — the type
36
+ always promised it; the server now delivers it. The vocabulary is append-only.
37
+
38
+ ### Fixed
39
+
40
+ - README method table showed pre-0.18 signatures for `recall` / `remember` /
41
+ `ingestKnowledge` / `history` (positional args instead of the actual object
42
+ args). Corrected to match the shipped API.
43
+
44
+ ## [0.20.0] - 2026-07-11
45
+
46
+ ### Added
47
+
48
+ - `client.getWallet(): Promise<CompanionWallet>` and the `GET /api/companion/wallet`
49
+ endpoint — read the companion instance's OWN wallet (nonzero stablecoin
50
+ balances + total USD). Read-only and receive-only; requires the `wallet.read`
51
+ scope (or `wallet.spend`, which subsumes it). Shares its read path with the
52
+ conversational `get_wallet_balance` tool so the endpoint and the in-chat answer
53
+ can never diverge. New exported types `CompanionWallet` / `WalletBalance`.
54
+
15
55
  ## [0.19.1] - 2026-07-10
16
56
 
17
57
  ### Documentation
package/README.md CHANGED
@@ -72,11 +72,11 @@ Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers:
72
72
  | `onInterfaceUpdate(fn)` | `companion.ui_update` | live `{key,value}` update to an already-rendered panel (no rebuild) |
73
73
  | `onSocialMessage(fn)` | `companion.social_message` | inbound A2A friend message, delivered cross-app. Needs `social.message` |
74
74
  | `onConfirmRequest(fn)` | `companion.confirm_request` | a sensitive-op approval request. **Platform session tokens** (your end users): show your own confirm card and resolve it with `confirmAction` — this is how confirm-gated custom skills (POST / credentialed) run. First-party user tokens: observe-only — approval stays first-party (where the `stepUp:true` biometric gate lives) |
75
- | `onAudio(fn)` | `companion.audio` | TTS clip (non-call modality) |
76
- | `onExpression(fn)` | `companion.expression` | avatar viseme / expression / gesture |
75
+ | `onAudio(fn)` | `companion.audio` | TTS clip (non-call modality). _(reserved — not emitted yet)_ |
76
+ | `onExpression(fn)` | `companion.expression` | avatar viseme / expression / gesture. _(reserved — not emitted yet)_ |
77
77
  | `onVoiceInject(fn)` | `companion.voice_inject` | `fn({text, speak})` — a `voiceRelevant` world-state line to say aloud during a live call; route `text` to your voice session when `speak` |
78
78
  | `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 |
79
- | `onUsage(fn)` | `control.usage` | per-token metering echo |
79
+ | `onUsage(fn)` | `control.usage` | per-token metering echo. _(reserved — not emitted yet)_ |
80
80
  | `onError(fn)` | `control.error` | agent / stream errors |
81
81
 
82
82
  ### Instant UI
@@ -96,16 +96,18 @@ local/device skills).
96
96
 
97
97
  | Method | What it does |
98
98
  |---|---|
99
- | `recall(query)` / `remember(text)` | Read / write the token-scoped memory namespace. |
100
- | `ingestKnowledge(text, opts?)` / `ingestFile(file)` | Push documents into the user's knowledge base ("My materials"); needs `memory.write:core`. |
101
- | `history(limit?)` | Fetch the session transcript — restore chat on reload. |
99
+ | `recall({ query?, limit? })` / `remember({ content, … })` | Read / write the token-scoped memory namespace. `query` runs SEMANTIC search over the facts server-side (embedding-based, ranked fallback); omit it for the plain importance/recency ranking. |
100
+ | `ingestKnowledge({ text, name, … })` / `ingestFile(file)` | Push documents into the user's knowledge base ("My materials"); needs `memory.write:core`. |
101
+ | `history({ limit? })` | Fetch the session transcript — restore chat on reload. |
102
102
  | `setModalities([...])` | Switch active I/O mid-session (within the token's grant). |
103
103
  | `ping()` | Keepalive for long-idle embeds. |
104
+ | `setToken(token)` | Swap the bearer for every subsequent request + stream reconnect — session tokens expire (1h default), so call this when your backend re-mints one. Prefer the `onAuthError` constructor option to refresh on demand instead. |
104
105
  | `pendingConfirms()` / `confirmAction(id, approve)` | List / resolve pending sensitive-action confirms (platform session tokens). On `{ status: 'exec_failed', retryable: true }` you MAY re-call the same confirmId — idempotent actions only. |
105
106
  | `endSession()` | Distill the session into durable memory now; returns `{ skipped?: 'no_session' \| 'no_content' \| 'throttled' }`. |
106
107
  | `close()` | One-call teardown: `stop()` + `endSession()` — the text-session mirror of the call handle's `close()`. |
107
108
  | `startCall(opts)` / `connectCall(opts)` | Voice plane: raw credentials / fully-wired call handle (`call.close()` ends + folds the transcript into memory). |
108
109
  | `getAvatar()` / `brandIconUrl(size?)` | The user's avatar (VRM/portrait) and the Pouchy brand icon for your UI. |
110
+ | `getWallet()` | Read the instance's own wallet — `{ balances: [{ currency, amount }], totalUsd, currency }`. Read-only + receive-only; needs the `wallet.read` scope. |
109
111
 
110
112
  Tool calling beyond your own declared tools: `HOST_CONTROL_TOOLS` (universal
111
113
  verbs — `invoke_action`, `set_feature`, `set_value`, `navigate`, `highlight`)
package/dist/client.d.ts CHANGED
@@ -47,6 +47,14 @@ export interface CompanionClientOptions {
47
47
  /** Injectable WebSocket constructor (Node without a global WebSocket, or
48
48
  * tests). Defaults to globalThis.WebSocket when present. */
49
49
  webSocketImpl?: typeof WebSocket;
50
+ /** Called when a request or the event stream is rejected with 401 (expired /
51
+ * revoked token — session tokens live 1h by default). Return a fresh token
52
+ * (e.g. re-minted by your backend via POST /v1/sessions) and the client
53
+ * retries transparently; return null to give up, surfacing the failure
54
+ * exactly as without this hook. Scope denials (403 `missing_scope`) are
55
+ * configuration errors and never trigger it. Alternative: call `setToken()`
56
+ * proactively on your own refresh schedule. */
57
+ onAuthError?: () => string | null | Promise<string | null>;
50
58
  }
51
59
  export interface HelloAck {
52
60
  session: string;
@@ -123,6 +131,23 @@ export interface CompanionAvatar {
123
131
  * built-ins today (reserved for custom previews / server-rendered thumbs). */
124
132
  imageUrl: string | null;
125
133
  }
134
+ /** One nonzero stablecoin balance line. */
135
+ export interface WalletBalance {
136
+ /** Stablecoin code (USDT / USDC / DAI / USD1). */
137
+ currency: string;
138
+ /** Decimal amount as a string (not atomic units), e.g. "5.83". */
139
+ amount: string;
140
+ }
141
+ /** Read-only snapshot of the companion instance's OWN wallet (receive-only —
142
+ * there is no way to move funds via the SDK). Requires the `wallet.read` scope. */
143
+ export interface CompanionWallet {
144
+ /** Nonzero per-currency balances (empty when the wallet is empty). */
145
+ balances: WalletBalance[];
146
+ /** Total across the balances; the allowed stablecoins are USD-pegged 1:1. */
147
+ totalUsd: number;
148
+ /** Denomination of `totalUsd` (always "USD" today). */
149
+ currency: string;
150
+ }
126
151
  /** Available sizes for the Pouchy brand icon (px). */
127
152
  export type BrandIconSize = 256 | 512 | 1024;
128
153
  /** Canonical URL of the official Pouchy brand icon at `size` px, derived from a
@@ -173,6 +198,19 @@ export declare class CompanionClient {
173
198
  constructor(opts: CompanionClientOptions);
174
199
  /** The active session id, or null before connect(). */
175
200
  get sessionId(): string | null;
201
+ /** Swap the bearer token used by every subsequent request and stream
202
+ * reconnect. Session tokens expire (default 1h) — call this when your
203
+ * backend re-mints one, or wire `onAuthError` to do it on demand. */
204
+ setToken(token: string): void;
205
+ private refreshing;
206
+ /** Run opts.onAuthError (deduped), apply the fresh token, and report whether
207
+ * the caller should retry. Never throws — a failed refresh means "surface
208
+ * the original 401". */
209
+ private tryRefreshToken;
210
+ /** doFetch with one transparent retry after a 401-triggered token refresh.
211
+ * Everything except the stream loop (which has its own auth handling) goes
212
+ * through here. */
213
+ private fetchAuthed;
176
214
  private url;
177
215
  private jsonHeaders;
178
216
  private requireSession;
@@ -307,9 +345,14 @@ export declare class CompanionClient {
307
345
  code: string;
308
346
  message: string;
309
347
  }, envelope: CompanionEnvelope) => void): () => void;
310
- /** Recall the memory this token is authorized to see (ranked). */
348
+ /** Recall the memory this token is authorized to see. Without `query` the
349
+ * facts come back ranked by importance/recency; with `query` the server
350
+ * runs semantic search over them (embedding-based, falls back to ranked
351
+ * when embeddings are unavailable) — e.g.
352
+ * `recall({ query: 'food preferences', limit: 10 })`. */
311
353
  recall(opts?: {
312
354
  limit?: number;
355
+ query?: string;
313
356
  }): Promise<RecalledMemory[]>;
314
357
  /** Fetch this session's recent conversation turns (oldest→newest) so a
315
358
  * reconnecting embed can restore its transcript. Distinct from `recall`
@@ -392,6 +435,12 @@ export declare class CompanionClient {
392
435
  * and the /models asset is CORS-enabled, so a cross-origin renderer can fetch
393
436
  * the VRM directly. Reflects the user's live choice (built-in or custom). */
394
437
  getAvatar(): Promise<CompanionAvatar>;
438
+ /** Read the companion instance's OWN wallet — nonzero stablecoin balances +
439
+ * total USD. Read-only and receive-only (spends go through the confirm-gated
440
+ * pay tools, never here). Requires the `wallet.read` scope (or `wallet.spend`,
441
+ * which subsumes it). Same data the companion gives when asked "what's my
442
+ * balance?". */
443
+ getWallet(): Promise<CompanionWallet>;
395
444
  /** Canonical URL of the official Pouchy brand icon (square PNG, transparent
396
445
  * background) at the given size. Static + public — needs no token — so it's
397
446
  * safe to drop straight into an `<img src>` for "powered by Pouchy" badges,
package/dist/client.js CHANGED
@@ -63,6 +63,45 @@ export class CompanionClient {
63
63
  get sessionId() {
64
64
  return this._session;
65
65
  }
66
+ /** Swap the bearer token used by every subsequent request and stream
67
+ * reconnect. Session tokens expire (default 1h) — call this when your
68
+ * backend re-mints one, or wire `onAuthError` to do it on demand. */
69
+ setToken(token) {
70
+ if (!token)
71
+ throw new CompanionError('setToken: token is required', 0, 'missing_option');
72
+ this.opts.token = token;
73
+ }
74
+ // In-flight onAuthError call, shared so concurrent 401s trigger ONE refresh.
75
+ refreshing = null;
76
+ /** Run opts.onAuthError (deduped), apply the fresh token, and report whether
77
+ * the caller should retry. Never throws — a failed refresh means "surface
78
+ * the original 401". */
79
+ async tryRefreshToken() {
80
+ const refresh = this.opts.onAuthError;
81
+ if (!refresh)
82
+ return false;
83
+ this.refreshing ??= Promise.resolve()
84
+ .then(refresh)
85
+ .catch(() => null)
86
+ .finally(() => {
87
+ this.refreshing = null;
88
+ });
89
+ const token = await this.refreshing;
90
+ if (!token)
91
+ return false;
92
+ this.opts.token = token;
93
+ return true;
94
+ }
95
+ /** doFetch with one transparent retry after a 401-triggered token refresh.
96
+ * Everything except the stream loop (which has its own auth handling) goes
97
+ * through here. */
98
+ async fetchAuthed(url, init) {
99
+ const res = await this.doFetch(url, init);
100
+ if (res.status !== 401 || !(await this.tryRefreshToken()))
101
+ return res;
102
+ init.headers.Authorization = `Bearer ${this.opts.token}`;
103
+ return this.doFetch(url, init);
104
+ }
66
105
  url(path) {
67
106
  return this.opts.baseUrl.replace(/\/+$/, '') + path;
68
107
  }
@@ -75,7 +114,7 @@ export class CompanionClient {
75
114
  return this._session;
76
115
  }
77
116
  async postJson(path, body) {
78
- const res = await this.doFetch(this.url(path), {
117
+ const res = await this.fetchAuthed(this.url(path), {
79
118
  method: 'POST',
80
119
  headers: this.jsonHeaders(),
81
120
  body: JSON.stringify(body)
@@ -151,7 +190,7 @@ export class CompanionClient {
151
190
  * dedups by id). Falls back to buffered semantics if the server ever
152
191
  * responds with plain JSON (e.g. an old deployment). */
153
192
  async sendTextStreaming(sessionId, body) {
154
- const res = await this.doFetch(this.url(`/api/companion/session/${sessionId}/input`), {
193
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${sessionId}/input`), {
155
194
  method: 'POST',
156
195
  headers: this.jsonHeaders(),
157
196
  body: JSON.stringify({ ...body, stream: true })
@@ -347,7 +386,7 @@ export class CompanionClient {
347
386
  .slice(-60)
348
387
  .map((t) => ({ role: t.role, text: t.text.slice(0, 500) }));
349
388
  try {
350
- const res = await this.doFetch(this.url(`/api/companion/session/${id}/end`), {
389
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${id}/end`), {
351
390
  method: 'POST',
352
391
  headers: this.jsonHeaders(),
353
392
  body: JSON.stringify({ transcript }),
@@ -481,10 +520,20 @@ export class CompanionClient {
481
520
  }, env);
482
521
  });
483
522
  }
484
- /** Recall the memory this token is authorized to see (ranked). */
523
+ /** Recall the memory this token is authorized to see. Without `query` the
524
+ * facts come back ranked by importance/recency; with `query` the server
525
+ * runs semantic search over them (embedding-based, falls back to ranked
526
+ * when embeddings are unavailable) — e.g.
527
+ * `recall({ query: 'food preferences', limit: 10 })`. */
485
528
  async recall(opts) {
486
- const qs = opts?.limit ? `?limit=${encodeURIComponent(opts.limit)}` : '';
487
- const res = await this.doFetch(this.url(`/api/companion/memory${qs}`), {
529
+ const params = new URLSearchParams();
530
+ if (opts?.limit)
531
+ params.set('limit', String(opts.limit));
532
+ if (opts?.query?.trim())
533
+ params.set('q', opts.query.trim());
534
+ const query = params.toString();
535
+ const qs = query ? `?${query}` : '';
536
+ const res = await this.fetchAuthed(this.url(`/api/companion/memory${qs}`), {
488
537
  headers: { Authorization: `Bearer ${this.opts.token}` }
489
538
  });
490
539
  const json = (await res.json().catch(() => null));
@@ -501,7 +550,7 @@ export class CompanionClient {
501
550
  async history(opts) {
502
551
  const id = this.requireSession();
503
552
  const qs = opts?.limit ? `?limit=${encodeURIComponent(opts.limit)}` : '';
504
- const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(id)}/history${qs}`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
553
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/history${qs}`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
505
554
  const json = (await res.json().catch(() => null));
506
555
  if (!res.ok || !json || json.ok === false) {
507
556
  throw new CompanionError(json?.error || `history failed (${res.status})`, res.status);
@@ -514,7 +563,7 @@ export class CompanionClient {
514
563
  * returned. Requires a live session. */
515
564
  async setModalities(modalities) {
516
565
  const id = this.requireSession();
517
- const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(id)}/modalities`), {
566
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/modalities`), {
518
567
  method: 'POST',
519
568
  headers: {
520
569
  Authorization: `Bearer ${this.opts.token}`,
@@ -533,7 +582,7 @@ export class CompanionClient {
533
582
  * it). Cheap; call it on a timer for a background tab. Requires a live session. */
534
583
  async ping() {
535
584
  const id = this.requireSession();
536
- const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(id)}/ping`), { method: 'POST', headers: { Authorization: `Bearer ${this.opts.token}` } });
585
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/ping`), { method: 'POST', headers: { Authorization: `Bearer ${this.opts.token}` } });
537
586
  if (!res.ok) {
538
587
  const json = (await res.json().catch(() => null));
539
588
  throw new CompanionError(json?.error || `ping failed (${res.status})`, res.status);
@@ -578,7 +627,7 @@ export class CompanionClient {
578
627
  * and the /models asset is CORS-enabled, so a cross-origin renderer can fetch
579
628
  * the VRM directly. Reflects the user's live choice (built-in or custom). */
580
629
  async getAvatar() {
581
- const res = await this.doFetch(this.url('/api/companion/avatar'), {
630
+ const res = await this.fetchAuthed(this.url('/api/companion/avatar'), {
582
631
  headers: { Authorization: `Bearer ${this.opts.token}` }
583
632
  });
584
633
  const json = (await res.json().catch(() => null));
@@ -593,6 +642,25 @@ export class CompanionClient {
593
642
  imageUrl: json.imageUrl ?? null
594
643
  };
595
644
  }
645
+ /** Read the companion instance's OWN wallet — nonzero stablecoin balances +
646
+ * total USD. Read-only and receive-only (spends go through the confirm-gated
647
+ * pay tools, never here). Requires the `wallet.read` scope (or `wallet.spend`,
648
+ * which subsumes it). Same data the companion gives when asked "what's my
649
+ * balance?". */
650
+ async getWallet() {
651
+ const res = await this.fetchAuthed(this.url('/api/companion/wallet'), {
652
+ headers: { Authorization: `Bearer ${this.opts.token}` }
653
+ });
654
+ const json = (await res.json().catch(() => null));
655
+ if (!res.ok || !json || json.ok === false) {
656
+ throw new CompanionError(json?.error || `wallet fetch failed (${res.status})`, res.status);
657
+ }
658
+ return {
659
+ balances: json.balances ?? [],
660
+ totalUsd: json.totalUsd ?? 0,
661
+ currency: json.currency ?? 'USD'
662
+ };
663
+ }
596
664
  /** Canonical URL of the official Pouchy brand icon (square PNG, transparent
597
665
  * background) at the given size. Static + public — needs no token — so it's
598
666
  * safe to drop straight into an `<img src>` for "powered by Pouchy" badges,
@@ -696,7 +764,7 @@ export class CompanionClient {
696
764
  * session tokens only, like `confirmAction`. */
697
765
  async pendingConfirms() {
698
766
  const sessionId = this.requireSession();
699
- const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
767
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
700
768
  const json = (await res.json().catch(() => null));
701
769
  if (!res.ok || !json || json.ok === false) {
702
770
  throw new CompanionError(json?.error || `pending confirms failed (${res.status})`, res.status);
@@ -905,11 +973,21 @@ export class CompanionClient {
905
973
  async loop() {
906
974
  const id = this.requireSession();
907
975
  let backoff = 500;
976
+ let authRefreshes = 0; // consecutive 401→refresh cycles without a healthy connect
908
977
  while (this.streaming) {
909
978
  this.abort = new AbortController();
910
979
  try {
911
980
  const res = await this.doFetch(this.url(`/api/companion/session/${id}/stream?cursor=${this.cursor}`), { headers: { Authorization: `Bearer ${this.opts.token}` }, signal: this.abort.signal });
912
981
  if (res.status === 401 || res.status === 403) {
982
+ // 401 = the token itself is bad — usually an EXPIRED session token
983
+ // (they live ~1h). With an onAuthError hook this is recoverable:
984
+ // refresh and reconnect instead of going permanently silent. The
985
+ // counter stops a hot loop when the hook keeps returning a token
986
+ // the server keeps rejecting.
987
+ if (res.status === 401 && authRefreshes < 3 && (await this.tryRefreshToken())) {
988
+ authRefreshes += 1;
989
+ continue;
990
+ }
913
991
  // Permanent: the token can't subscribe (most often it lacks the
914
992
  // `events.subscribe` scope; also revoked / wrong user). Retrying is
915
993
  // futile and would silently swallow every reply — surface it as a
@@ -936,6 +1014,7 @@ export class CompanionClient {
936
1014
  continue;
937
1015
  }
938
1016
  backoff = 500; // healthy connection resets backoff
1017
+ authRefreshes = 0;
939
1018
  await this.consume(res.body);
940
1019
  }
941
1020
  catch {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CompanionClient, type CompanionClientOptions } from './client.js';
2
2
  export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js';
3
- export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, WorldStateInput, CompanionAvatar, BrandIconSize, EndSessionResult } from './client.js';
3
+ export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, WorldStateInput, CompanionAvatar, CompanionWallet, WalletBalance, BrandIconSize, EndSessionResult } from './client.js';
4
4
  export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_VISUAL_TOOLS, AVATAR_VISUAL_TOOL_NAMES } from './call.js';
5
5
  export type { CompanionCall, CompanionCallOptions, VoiceToolBridge } from './call.js';
6
6
  export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
@@ -2,7 +2,13 @@ export declare const PROTOCOL_VERSION: 1;
2
2
  /** App → Pouchy (control/data plane). */
3
3
  export declare const INBOUND_TYPES: readonly ["hello", "input.text", "context.event", "context.snapshot", "tool.result", "control.start_call", "control.end_call", "control.set_modalities", "control.ping"];
4
4
  export type InboundType = (typeof INBOUND_TYPES)[number];
5
- /** Pouchy → app (control/data plane). */
5
+ /** Pouchy → app (control/data plane).
6
+ * NOTE: three types are RESERVED — part of the vocabulary + typed handlers, but
7
+ * the server does NOT emit them yet, so `onAudio` / `onExpression` / `onUsage`
8
+ * are safe to register but won't fire until a producer ships. Don't build a UX
9
+ * that depends on them today. (companion.audio → non-call reply TTS;
10
+ * companion.expression → avatar viseme/expression cues; control.usage →
11
+ * per-turn usage-metering echo.) */
6
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"];
7
13
  export type OutboundType = (typeof OUTBOUND_TYPES)[number];
8
14
  export type MessageType = InboundType | OutboundType;
package/dist/protocol.js CHANGED
@@ -17,20 +17,26 @@ export const INBOUND_TYPES = [
17
17
  'control.set_modalities',
18
18
  'control.ping'
19
19
  ];
20
- /** Pouchy → app (control/data plane). */
20
+ /** Pouchy → app (control/data plane).
21
+ * NOTE: three types are RESERVED — part of the vocabulary + typed handlers, but
22
+ * the server does NOT emit them yet, so `onAudio` / `onExpression` / `onUsage`
23
+ * are safe to register but won't fire until a producer ships. Don't build a UX
24
+ * that depends on them today. (companion.audio → non-call reply TTS;
25
+ * companion.expression → avatar viseme/expression cues; control.usage →
26
+ * per-turn usage-metering echo.) */
21
27
  export const OUTBOUND_TYPES = [
22
28
  'hello.ack',
23
29
  'companion.message',
24
- 'companion.audio',
30
+ 'companion.audio', // reserved — not emitted yet
25
31
  'companion.tool_call',
26
32
  'companion.ui_action',
27
33
  'companion.ui_update',
28
- 'companion.expression',
34
+ 'companion.expression', // reserved — not emitted yet
29
35
  'companion.voice_inject',
30
36
  'companion.confirm_request',
31
37
  'companion.social_message',
32
38
  'companion.typing',
33
39
  'control.call_ready',
34
40
  'control.error',
35
- 'control.usage'
41
+ 'control.usage' // reserved — not emitted yet
36
42
  ];
package/dist/scopes.js CHANGED
@@ -21,7 +21,7 @@ export const COMPANION_SCOPES = [
21
21
  'memory.read:core', // read the shared global brain (SENSITIVE)
22
22
  'memory.write:core', // write into the shared global brain (SENSITIVE)
23
23
  'skills.execute', // run the user's installed skills (SENSITIVE)
24
- 'wallet.read', // READ-ONLY wallet: balance + own deposit address (receive-only; not sensitive)
24
+ 'wallet.read', // READ-ONLY wallet: balance + own deposit address (receive-only; not sensitive). Surfaced via the companion's wallet tools (ask it) — no standalone REST endpoint.
25
25
  'wallet.spend', // spend from the Care Wallet (SENSITIVE)
26
26
  'social.message', // message friends via A2A (SENSITIVE)
27
27
  // ── Representative ("on-behalf-of") plane ─────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.19.1",
3
+ "version": "0.21.0",
4
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",