@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 +40 -0
- package/README.md +8 -6
- package/dist/client.d.ts +50 -1
- package/dist/client.js +90 -11
- package/dist/index.d.ts +1 -1
- package/dist/protocol.d.ts +7 -1
- package/dist/protocol.js +10 -4
- package/dist/scopes.js +1 -1
- package/package.json +1 -1
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(
|
|
100
|
-
| `ingestKnowledge(text,
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
487
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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';
|
package/dist/protocol.d.ts
CHANGED
|
@@ -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.
|
|
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",
|