@pouchy_ai/companion-sdk 0.11.0 → 0.12.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,27 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.12.0] - 2026-07-05
16
+
17
+ ### Added
18
+
19
+ - **`confirmAction(confirmId, approve)`** — resolve a pending sensitive-op
20
+ confirmation from the embed. Platform **session tokens** only (end-user
21
+ instances minted via `/v1/sessions`): the embedding app's end user is that
22
+ account's only human, so the session may approve. First-party user tokens
23
+ remain observe-only (401) — approval of a real Pouchy user's ops stays on
24
+ their first-party surface with its biometric step-up. On approve the
25
+ recorded action runs server-side and its outcome arrives as a normal
26
+ `companion.message`. This is the run path for the platform's new
27
+ **confirm-gated custom skills** (POST / credentialed tools, keys served
28
+ from the project credential vault).
29
+ - **`pendingConfirms()`** — the session's still-pending confirmations
30
+ (display-safe: `confirmId`, `scope`, `summary`, `createdAt`), for rebuilding
31
+ a confirm card after a reload. Same session-token-only auth.
32
+ - **`PendingConfirm`** type export; `ConfirmRequestPayload` /
33
+ `onConfirmRequest` docs updated for the two-token model. No wire-protocol
34
+ change (`PROTOCOL_VERSION` stays `1`); additive surface.
35
+
15
36
  ## [0.11.0] - 2026-07-02
16
37
 
17
38
  ### Added
@@ -116,7 +137,8 @@ self-hosted/CDN build; this entry captures the surface that ships publicly.
116
137
  - **Versioned wire protocol.** `PROTOCOL_VERSION = 1`, kept in lockstep with the
117
138
  server by `protocol.drift.test.ts` (fails CI on divergence).
118
139
 
119
- [Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.11.0...HEAD
140
+ [Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.12.0...HEAD
141
+ [0.12.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.11.0...companion-sdk-v0.12.0
120
142
  [0.11.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.10.1...companion-sdk-v0.11.0
121
143
  [0.10.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.10.0...companion-sdk-v0.10.1
122
144
  [0.10.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.9.0...companion-sdk-v0.10.0
package/README.md CHANGED
@@ -47,7 +47,7 @@ Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers:
47
47
  | `onRender(fn)` | `companion.ui_action` | **Instant UI** — draw `payload.interface` (platform-neutral genui schema) with your own renderer (web / iOS / Android / CLI). Needs `ui.render` |
48
48
  | `onInterfaceUpdate(fn)` | `companion.ui_update` | live `{key,value}` update to an already-rendered panel (no rebuild) |
49
49
  | `onSocialMessage(fn)` | `companion.social_message` | inbound A2A friend message, delivered cross-app. Needs `social.message` |
50
- | `onConfirmRequest(fn)` | `companion.confirm_request` | observe a sensitive-op approval (`stepUp:true` the first-party surface should gate it with a biometric). Observe-only — approval is first-party |
50
+ | `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) |
51
51
  | `onAudio(fn)` | `companion.audio` | TTS clip (non-call modality) |
52
52
  | `onExpression(fn)` | `companion.expression` | avatar viseme / expression / gesture |
53
53
  | `onUsage(fn)` | `control.usage` | per-token metering echo |
package/dist/call.js CHANGED
@@ -153,8 +153,35 @@ export async function openCompanionCall(creds, opts = {}, bridge) {
153
153
  ? openOpenAICall(creds, opts, bridge)
154
154
  : openConvaiCall(creds, opts, bridge);
155
155
  }
156
+ // Connect-step timeouts (mirror the first-party realtime path). Without them a
157
+ // stuck mic (Android WebView holding the device) or a hung SDP exchange hangs
158
+ // the connect promise forever — and if gUM already resolved, with a hot mic.
159
+ const GUM_TIMEOUT_MS = 10_000;
160
+ const SDP_TIMEOUT_MS = 8_000;
161
+ /** getUserMedia with a timeout. If the timeout wins, a late-resolving mic is
162
+ * stopped so a slow permission grant can't leave the device recording after
163
+ * we've already given up on the call. */
164
+ async function getMicWithTimeout() {
165
+ let timedOut = false;
166
+ const micPromise = navigator.mediaDevices.getUserMedia({ audio: true });
167
+ micPromise
168
+ .then((m) => {
169
+ if (timedOut)
170
+ m.getTracks().forEach((t) => t.stop());
171
+ })
172
+ .catch(() => { });
173
+ const timeout = new Promise((_, reject) => setTimeout(() => {
174
+ timedOut = true;
175
+ reject(new Error('microphone request timed out'));
176
+ }, GUM_TIMEOUT_MS));
177
+ return Promise.race([micPromise, timeout]);
178
+ }
156
179
  // ── OpenAI Realtime — self-contained WebRTC ─────────────────────────────────
157
180
  async function openOpenAICall(creds, opts, bridge) {
181
+ // Acquire the mic FIRST (with a timeout): if gUM fails/times out there is no
182
+ // RTCPeerConnection yet to leak. Creating pc before gUM (the old order) leaked
183
+ // a peer connection on every permission denial.
184
+ const mic = await getMicWithTimeout();
158
185
  const pc = new RTCPeerConnection();
159
186
  const audioEl = opts.audioElement ?? new Audio();
160
187
  audioEl.autoplay = true;
@@ -162,7 +189,6 @@ async function openOpenAICall(creds, opts, bridge) {
162
189
  audioEl.srcObject = e.streams[0];
163
190
  void audioEl.play().catch(() => { });
164
191
  };
165
- const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
166
192
  for (const track of mic.getAudioTracks())
167
193
  pc.addTrack(track, mic);
168
194
  // Half-duplex mic (mirrors the first-party realtime path, #1524): on a phone
@@ -290,14 +316,33 @@ async function openOpenAICall(creds, opts, bridge) {
290
316
  };
291
317
  const offer = await pc.createOffer();
292
318
  await pc.setLocalDescription(offer);
293
- const res = await fetch(OPENAI_REALTIME_CALLS, {
294
- method: 'POST',
295
- headers: { Authorization: `Bearer ${creds.clientSecret}`, 'Content-Type': 'application/sdp' },
296
- body: offer.sdp
297
- });
298
- if (!res.ok) {
319
+ // Bound the SDP exchange so a hung network can't leave the pc + hot mic open
320
+ // forever. On any failure, close both before throwing.
321
+ const sdpAbort = new AbortController();
322
+ const sdpTimer = setTimeout(() => sdpAbort.abort(), SDP_TIMEOUT_MS);
323
+ const cleanupPartial = () => {
299
324
  pc.close();
300
325
  mic.getTracks().forEach((t) => t.stop());
326
+ };
327
+ let res;
328
+ try {
329
+ res = await fetch(OPENAI_REALTIME_CALLS, {
330
+ method: 'POST',
331
+ headers: { Authorization: `Bearer ${creds.clientSecret}`, 'Content-Type': 'application/sdp' },
332
+ body: offer.sdp,
333
+ signal: sdpAbort.signal
334
+ });
335
+ }
336
+ catch {
337
+ clearTimeout(sdpTimer);
338
+ cleanupPartial();
339
+ throw new Error(sdpAbort.signal.aborted
340
+ ? 'OpenAI Realtime SDP exchange timed out'
341
+ : 'OpenAI Realtime SDP exchange failed');
342
+ }
343
+ clearTimeout(sdpTimer);
344
+ if (!res.ok) {
345
+ cleanupPartial();
301
346
  throw new Error(`OpenAI Realtime SDP exchange failed (${res.status})`);
302
347
  }
303
348
  await pc.setRemoteDescription({ type: 'answer', sdp: await res.text() });
@@ -367,6 +412,23 @@ async function openOpenAICall(creds, opts, bridge) {
367
412
  }, DISCONNECT_GRACE_MS);
368
413
  }
369
414
  };
415
+ // Data-channel liveness (mirrors the first-party dataChannel.onclose/onerror).
416
+ // A dc-only death — the channel closes/errors while pc.connectionState still
417
+ // reads 'connected' — otherwise goes undetected and the mic stays hot.
418
+ // teardown flips `closed` before it calls dc.close(), so a normal teardown's
419
+ // own channel close is guarded out here (no double-fire / spurious error).
420
+ dc.onclose = () => {
421
+ if (closed)
422
+ return;
423
+ teardown();
424
+ opts.onError?.(new Error('call data channel closed'));
425
+ };
426
+ dc.onerror = () => {
427
+ if (closed)
428
+ return;
429
+ teardown();
430
+ opts.onError?.(new Error('call data channel error'));
431
+ };
370
432
  return {
371
433
  provider: 'openai-realtime',
372
434
  close: teardown,
@@ -485,6 +547,21 @@ async function openConvaiCall(creds, opts, bridge) {
485
547
  /* session not yet established */
486
548
  }
487
549
  opts.onError?.(new Error('call disconnected'));
550
+ },
551
+ // Liveness backstop (first-party F3): a transport wedge can flip status to
552
+ // 'disconnected' WITHOUT firing onDisconnect, leaving "connected" with a hot
553
+ // mic forever. Treat a disconnected status the same as onDisconnect.
554
+ onStatusChange: ({ status }) => {
555
+ if (closed || status !== 'disconnected')
556
+ return;
557
+ closed = true;
558
+ try {
559
+ void conv.endSession().catch(() => { });
560
+ }
561
+ catch {
562
+ /* session not yet established */
563
+ }
564
+ opts.onError?.(new Error('call disconnected'));
488
565
  }
489
566
  });
490
567
  return {
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundType, RenderInterfacePayload, SocialMessagePayload, UsagePayload, WorldStateEvent } from './protocol.js';
1
+ import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, UsagePayload, 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'>>;
@@ -331,14 +331,36 @@ export declare class CompanionClient {
331
331
  /** Convenience: subscribe to the per-token metering echo (control.usage) for a
332
332
  * usage / billing view. */
333
333
  onUsage(handler: (payload: UsagePayload, envelope: CompanionEnvelope) => void): () => void;
334
- /** Convenience: OBSERVE sensitive-op confirmation requests
334
+ /** Convenience: subscribe to sensitive-op confirmation requests
335
335
  * (companion.confirm_request) — a payment, skill run, or friend message the
336
- * companion wants the user to approve. Use it to surface the pending approval
337
- * in your UI (e.g. "open Pouchy to approve"). NOTE: an embed cannot approve —
338
- * approval is authed as the first-party Pouchy user (their app / a hosted
339
- * confirm page), which is also where a biometric (Face ID / passkey) gate
340
- * lives. This handler is observe-only. */
336
+ * companion wants the user to approve. Use it to surface the pending
337
+ * approval in your UI.
338
+ *
339
+ * Whether YOU can resolve it depends on the token:
340
+ * - **Platform session tokens** (minted via /v1/sessions for your end
341
+ * users): show your own confirm card and call `confirmAction` — your end
342
+ * user is the account's human, so the session may approve. This is how
343
+ * confirm-gated custom skills (POST / credentialed) run.
344
+ * - **First-party user tokens** (Login-with-Pouchy PATs): observe-only —
345
+ * approval is authed as the Pouchy user (their app / a hosted confirm
346
+ * page), which is also where the biometric (Face ID / passkey) gate
347
+ * lives. `confirmAction` returns 401 on these tokens. */
341
348
  onConfirmRequest(handler: (payload: ConfirmRequestPayload, envelope: CompanionEnvelope) => void): () => void;
349
+ /** Resolve a pending confirmation (platform session tokens only — see
350
+ * `onConfirmRequest`). Show the user what they're approving (the event's
351
+ * `summary`), collect an explicit tap, then pass their decision here. On
352
+ * approval the recorded action runs server-side and its outcome arrives as
353
+ * a normal companion.message; a denial emits a brief decline. One-time:
354
+ * re-resolving a settled confirmId fails with 409. */
355
+ confirmAction(confirmId: string, approve: boolean): Promise<{
356
+ ok: boolean;
357
+ status: 'approved' | 'denied';
358
+ }>;
359
+ /** The session's still-pending confirmations (display-safe: id, summary,
360
+ * scope, timestamps — never raw args). Use it to rebuild your confirm card
361
+ * after a reload, since confirm_request events are not replayed. Platform
362
+ * session tokens only, like `confirmAction`. */
363
+ pendingConfirms(): Promise<PendingConfirm[]>;
342
364
  /** Convenience: subscribe to live updates of an already-rendered Instant UI
343
365
  * panel (companion.ui_update). Apply each `{ key, value }` to the panel's state
344
366
  * bag and re-evaluate bound displays — no rebuild. */
package/dist/client.js CHANGED
@@ -487,13 +487,20 @@ export class CompanionClient {
487
487
  onUsage(handler) {
488
488
  return this.on('control.usage', (env) => handler(env.payload, env));
489
489
  }
490
- /** Convenience: OBSERVE sensitive-op confirmation requests
490
+ /** Convenience: subscribe to sensitive-op confirmation requests
491
491
  * (companion.confirm_request) — a payment, skill run, or friend message the
492
- * companion wants the user to approve. Use it to surface the pending approval
493
- * in your UI (e.g. "open Pouchy to approve"). NOTE: an embed cannot approve —
494
- * approval is authed as the first-party Pouchy user (their app / a hosted
495
- * confirm page), which is also where a biometric (Face ID / passkey) gate
496
- * lives. This handler is observe-only. */
492
+ * companion wants the user to approve. Use it to surface the pending
493
+ * approval in your UI.
494
+ *
495
+ * Whether YOU can resolve it depends on the token:
496
+ * - **Platform session tokens** (minted via /v1/sessions for your end
497
+ * users): show your own confirm card and call `confirmAction` — your end
498
+ * user is the account's human, so the session may approve. This is how
499
+ * confirm-gated custom skills (POST / credentialed) run.
500
+ * - **First-party user tokens** (Login-with-Pouchy PATs): observe-only —
501
+ * approval is authed as the Pouchy user (their app / a hosted confirm
502
+ * page), which is also where the biometric (Face ID / passkey) gate
503
+ * lives. `confirmAction` returns 401 on these tokens. */
497
504
  onConfirmRequest(handler) {
498
505
  return this.on('companion.confirm_request', (env) => {
499
506
  const p = env.payload;
@@ -501,6 +508,29 @@ export class CompanionClient {
501
508
  handler(p, env);
502
509
  });
503
510
  }
511
+ /** Resolve a pending confirmation (platform session tokens only — see
512
+ * `onConfirmRequest`). Show the user what they're approving (the event's
513
+ * `summary`), collect an explicit tap, then pass their decision here. On
514
+ * approval the recorded action runs server-side and its outcome arrives as
515
+ * a normal companion.message; a denial emits a brief decline. One-time:
516
+ * re-resolving a settled confirmId fails with 409. */
517
+ async confirmAction(confirmId, approve) {
518
+ const sessionId = this.requireSession();
519
+ return this.postJson(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`, { confirmId, approve });
520
+ }
521
+ /** The session's still-pending confirmations (display-safe: id, summary,
522
+ * scope, timestamps — never raw args). Use it to rebuild your confirm card
523
+ * after a reload, since confirm_request events are not replayed. Platform
524
+ * session tokens only, like `confirmAction`. */
525
+ async pendingConfirms() {
526
+ const sessionId = this.requireSession();
527
+ const res = await this.doFetch(this.url(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
528
+ const json = (await res.json().catch(() => null));
529
+ if (!res.ok || !json || json.ok === false) {
530
+ throw new CompanionError(json?.error || `pending confirms failed (${res.status})`, res.status);
531
+ }
532
+ return json.pending ?? [];
533
+ }
504
534
  /** Convenience: subscribe to live updates of an already-rendered Instant UI
505
535
  * panel (companion.ui_update). Apply each `{ key, value }` to the panel's state
506
536
  * bag and re-evaluate bound displays — no rebuild. */
@@ -543,40 +573,66 @@ export class CompanionClient {
543
573
  async wsLoop(WS) {
544
574
  const id = this.requireSession();
545
575
  const { deriveWsStreamUrl, decodeWsMessage } = await import('./ws-transport.js');
546
- try {
547
- await new Promise((resolve, reject) => {
548
- const ws = new WS(deriveWsStreamUrl(this.opts.baseUrl, id, this.opts.token, this.cursor));
549
- const done = (err) => (err ? reject(err) : resolve());
550
- ws.onmessage = (ev) => {
551
- if (!this.streaming) {
552
- ws.close();
553
- return;
554
- }
555
- const raw = typeof ev.data === 'string' ? ev.data : '';
556
- const frame = raw ? decodeWsMessage(raw) : null;
557
- if (frame)
558
- this.handleFrame(frame.event, frame.data);
559
- };
560
- ws.onerror = () => done(new Error('companion ws error'));
561
- ws.onclose = () => done();
562
- // Stopping the stream closes the socket.
563
- const poll = setInterval(() => {
564
- if (!this.streaming) {
565
- clearInterval(poll);
576
+ // Reconnect across CLEAN closes (a WS gateway with a bounded lifetime, like
577
+ // the SSE endpoint's window) just as the SSE loop does — a graceful close
578
+ // must not silently end reply delivery. Only a connection ERROR degrades to
579
+ // SSE, and only once. Each connection's poll timer + socket are released in
580
+ // the settle() finally so neither leaks after fallback/reconnect.
581
+ while (this.streaming) {
582
+ let errored = false;
583
+ try {
584
+ await new Promise((resolve, reject) => {
585
+ const ws = new WS(deriveWsStreamUrl(this.opts.baseUrl, id, this.opts.token, this.cursor));
586
+ let poll;
587
+ let settled = false;
588
+ const settle = (err) => {
589
+ if (poll) {
590
+ clearInterval(poll);
591
+ poll = undefined;
592
+ }
566
593
  try {
567
594
  ws.close();
568
595
  }
569
596
  catch {
570
597
  /* already closing */
571
598
  }
572
- }
573
- }, 250);
574
- });
575
- }
576
- catch {
577
- // Socket unavailable / rejected — degrade to the robust SSE loop.
578
- if (this.streaming)
579
- await this.loop();
599
+ if (settled)
600
+ return;
601
+ settled = true;
602
+ if (err)
603
+ reject(err);
604
+ else
605
+ resolve();
606
+ };
607
+ ws.onmessage = (ev) => {
608
+ if (!this.streaming) {
609
+ settle();
610
+ return;
611
+ }
612
+ const raw = typeof ev.data === 'string' ? ev.data : '';
613
+ const frame = raw ? decodeWsMessage(raw) : null;
614
+ if (frame)
615
+ this.handleFrame(frame.event, frame.data);
616
+ };
617
+ ws.onerror = () => settle(new Error('companion ws error'));
618
+ ws.onclose = () => settle();
619
+ // Stopping the stream closes the socket + clears this timer.
620
+ poll = setInterval(() => {
621
+ if (!this.streaming)
622
+ settle();
623
+ }, 250);
624
+ });
625
+ }
626
+ catch {
627
+ errored = true;
628
+ }
629
+ if (errored) {
630
+ // Socket unavailable / rejected — degrade to the robust SSE loop (once).
631
+ if (this.streaming)
632
+ await this.loop();
633
+ return;
634
+ }
635
+ // Clean close: the while-guard reconnects if still streaming, else exits.
580
636
  }
581
637
  }
582
638
  /** Stop the event stream. */
package/dist/index.d.ts CHANGED
@@ -3,6 +3,6 @@ export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js
3
3
  export type { CompanionClientOptions, HelloAck, RecalledMemory, CallCredentials, CompanionToolDecl, WorldStateInput, CompanionAvatar, BrandIconSize } 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
- export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, AudioClipPayload, ExpressionPayload, UsagePayload } from './protocol.js';
6
+ export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, AudioClipPayload, ExpressionPayload, UsagePayload } from './protocol.js';
7
7
  /** Create a companion client. */
8
8
  export declare function createCompanion(opts: CompanionClientOptions): CompanionClient;
@@ -38,11 +38,14 @@ export interface RenderInterfacePayload {
38
38
  }
39
39
  /** Payload of a `companion.confirm_request` event — the companion is asking the
40
40
  * user to approve a sensitive op (a payment, a skill run, a friend message).
41
- * An embed may OBSERVE this to surface the pending approval in its own UI, but
42
- * it CANNOT approve: approval is authed as the first-party Pouchy user (their
43
- * app / a hosted confirm page), so a third-party DOM can never confirm a spend.
44
- * A first-party native host can gate that approval behind a biometric (Face ID
45
- * / passkey) see docs/companion-wallet-noncustodial.md. */
41
+ * Who may approve depends on the token: a PLATFORM SESSION TOKEN (an end-user
42
+ * instance minted via /v1/sessions) resolves it directly with
43
+ * `client.confirmAction` the embedding app's end user is that account's
44
+ * human. A first-party user's token can only OBSERVE: approval of a real
45
+ * Pouchy user's ops is authed as that user (their app / a hosted confirm
46
+ * page), so a third-party DOM can never confirm a spend, and the first-party
47
+ * host can gate approval behind a biometric (Face ID / passkey) — see
48
+ * docs/companion-wallet-noncustodial.md. */
46
49
  export interface ConfirmRequestPayload {
47
50
  /** Opaque id the first-party surface passes to the confirm endpoint. */
48
51
  confirmId: string;
@@ -56,6 +59,17 @@ export interface ConfirmRequestPayload {
56
59
  * Cryptographic enforcement is server-side follow-up; this is the host's cue. */
57
60
  stepUp?: boolean;
58
61
  }
62
+ /** One still-pending confirmation, as returned by `client.pendingConfirms()` —
63
+ * display-safe (never the recorded tool call or its raw args). */
64
+ export interface PendingConfirm {
65
+ confirmId: string;
66
+ /** The scope of the op needing approval (e.g. `skills.execute`). */
67
+ scope: string;
68
+ /** A human, user-facing summary of what approving will do. */
69
+ summary: string;
70
+ /** Epoch ms when the companion asked. */
71
+ createdAt: number;
72
+ }
59
73
  /** Payload of a `companion.ui_update` event — a live change to an
60
74
  * already-rendered Instant UI panel: write each `{ key, value }` into the
61
75
  * panel's state bag (re-evaluating templates / progress / `when`), without
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.11.0",
3
+ "version": "0.12.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",