@pouchy_ai/companion-sdk 0.28.1 → 0.29.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,23 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.29.0] - 2026-07-12
16
+
17
+ ### Added
18
+
19
+ - **`AbortSignal` on every network method.** All request-performing public
20
+ methods now accept an optional `signal` — cancel an in-flight `sendText`
21
+ (including its `awaitReply` deadline + event-stream teardown), `recall`,
22
+ `history`, `sendWorldState`, `ingestKnowledge`, `ingestFile`, `remember`,
23
+ `getAvatar`, `getWallet`, `confirmAction`, `pendingConfirms`,
24
+ `setModalities`, `ping`, `pairVisitor`, or `connect`. Methods with an
25
+ existing options object gained a `signal?` field; the rest gained a trailing
26
+ `opts?: { signal?: AbortSignal }`. Fully additive — no existing call
27
+ changes. A fired signal rejects with `CompanionError` `code: 'aborted'`
28
+ (new synthesized code), converted at the single fetch choke point so the
29
+ "every helper rejects with CompanionError" contract holds; the signal also
30
+ rides the 401-refresh retry automatically.
31
+
15
32
  ## [0.28.1] - 2026-07-12
16
33
 
17
34
  ### Fixed
@@ -599,7 +616,8 @@ self-hosted/CDN build; this entry captures the surface that ships publicly.
599
616
  - **Versioned wire protocol.** `PROTOCOL_VERSION = 1`, kept in lockstep with the
600
617
  server by `protocol.drift.test.ts` (fails CI on divergence).
601
618
 
602
- [Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.1...HEAD
619
+ [Unreleased]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.29.0...HEAD
620
+ [0.29.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.1...companion-sdk-v0.29.0
603
621
  [0.28.1]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.28.0...companion-sdk-v0.28.1
604
622
  [0.28.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.27.0...companion-sdk-v0.28.0
605
623
  [0.27.0]: https://github.com/oviswang/Pouchy/compare/companion-sdk-v0.26.1...companion-sdk-v0.27.0
package/README.md CHANGED
@@ -122,6 +122,20 @@ are offered automatically once you declare `tools`/`handles`, and
122
122
  embeds receive expression cues — declare them or they no-op silently. Full
123
123
  contract: `docs/companion-host-control.md`.
124
124
 
125
+ **Cancellation (0.29.0):** every request-performing method accepts an optional
126
+ `AbortSignal`. Methods with an options object take a `signal?` field
127
+ (`sendText(text, { signal })`, `recall({ signal })`, …); the rest take a
128
+ trailing `opts` (`getAvatar({ signal })`, `ping({ signal })`, …). Aborting
129
+ rejects with `CompanionError` `code: 'aborted'`, and for `sendText({ awaitReply })`
130
+ it also tears down the in-flight event stream.
131
+
132
+ ```ts
133
+ const ac = new AbortController();
134
+ const p = companion.sendText('summarize this', { awaitReply: true, signal: ac.signal });
135
+ // …user navigated away
136
+ ac.abort(); // p rejects with code: 'aborted'
137
+ ```
138
+
125
139
  ## Scopes (typed)
126
140
 
127
141
  The full capability-scope vocabulary ships as typed constants — no more
package/dist/client.d.ts CHANGED
@@ -230,6 +230,11 @@ export declare class CompanionClient {
230
230
  * the strict awaitReply correlation is only safe to enforce there. */
231
231
  private serverEchoesReplyTo;
232
232
  private fetchAuthed;
233
+ /** Normalize a thrown fetch error to a CompanionError. An AbortError (the
234
+ * caller's signal fired) becomes a switchable `code: 'aborted'`; anything
235
+ * else that is already a CompanionError passes through; the rest wrap as a
236
+ * status-0 network failure. */
237
+ private asCompanionError;
233
238
  /** Peek the error `code` of a failure response without consuming its body. */
234
239
  private errorCodeOf;
235
240
  private url;
@@ -241,7 +246,9 @@ export declare class CompanionClient {
241
246
  private retryAfterFrom;
242
247
  private postJson;
243
248
  /** Handshake: start or resume the session for this surface. */
244
- connect(): Promise<HelloAck>;
249
+ connect(opts?: {
250
+ signal?: AbortSignal;
251
+ }): Promise<HelloAck>;
245
252
  /** Pair this representative session's VISITOR with the owner so their two
246
253
  * companions become friends — unlocking the A2A plane (messaging, gifts,
247
254
  * visiting) between them. Only valid in a representative session (the client
@@ -250,7 +257,9 @@ export declare class CompanionClient {
250
257
  * Two-token consent: this client's PAT (the owner's) must hold `represent:pair`,
251
258
  * and you pass the VISITOR's own Pouchy PAT — which must hold `social.message`
252
259
  * — as proof + consent. The visitor must therefore also be a Pouchy user. */
253
- pairVisitor(visitorToken: string): Promise<{
260
+ pairVisitor(visitorToken: string, opts?: {
261
+ signal?: AbortSignal;
262
+ }): Promise<{
254
263
  pairId: string | null;
255
264
  }>;
256
265
  /** Send a user text turn, optionally with images (data URLs) for a multimodal
@@ -279,12 +288,14 @@ export declare class CompanionClient {
279
288
  images?: string[];
280
289
  stream?: boolean;
281
290
  replyTimeoutMs?: number;
291
+ signal?: AbortSignal;
282
292
  }): Promise<SendTextReply>;
283
293
  sendText(text: string, opts?: {
284
294
  images?: string[];
285
295
  stream?: boolean;
286
296
  awaitReply?: boolean;
287
297
  replyTimeoutMs?: number;
298
+ signal?: AbortSignal;
288
299
  }): Promise<{
289
300
  seq: number | null;
290
301
  }>;
@@ -312,7 +323,9 @@ export declare class CompanionClient {
312
323
  * (id → a fresh handle, source → this surface), so callers only supply the
313
324
  * meaningful fields: `{ type, data, retained?, salience?, voiceRelevant? }`.
314
325
  * A full CloudEvents envelope is still accepted (its fields win). */
315
- sendWorldState(event: WorldStateInput | WorldStateInput[]): Promise<{
326
+ sendWorldState(event: WorldStateInput | WorldStateInput[], opts?: {
327
+ signal?: AbortSignal;
328
+ }): Promise<{
316
329
  accepted: number;
317
330
  dropped: number;
318
331
  injected: number;
@@ -404,6 +417,7 @@ export declare class CompanionClient {
404
417
  recall(opts?: {
405
418
  limit?: number;
406
419
  query?: string;
420
+ signal?: AbortSignal;
407
421
  }): Promise<RecalledMemory[]>;
408
422
  /** Fetch this session's recent conversation turns (oldest→newest) so a
409
423
  * reconnecting embed can restore its transcript. Distinct from `recall`
@@ -412,18 +426,23 @@ export declare class CompanionClient {
412
426
  * first). `limit` defaults to 20, capped at 50. */
413
427
  history(opts?: {
414
428
  limit?: number;
429
+ signal?: AbortSignal;
415
430
  }): Promise<CompanionTurn[]>;
416
431
  /** Change this session's I/O modalities mid-session (e.g. enable/disable
417
432
  * `voice`). The request is intersected with the token's granted modalities
418
433
  * server-side — a session can't widen past its key — and the EFFECTIVE set is
419
434
  * returned. Requires a live session. */
420
- setModalities(modalities: string[]): Promise<{
435
+ setModalities(modalities: string[], opts?: {
436
+ signal?: AbortSignal;
437
+ }): Promise<{
421
438
  modalities: string[];
422
439
  }>;
423
440
  /** Keepalive: bump this session's last-seen time so a long-idle embed stays
424
441
  * "live" within the session TTL (cross-app A2A friend messages keep reaching
425
442
  * it). Cheap; call it on a timer for a background tab. Requires a live session. */
426
- ping(): Promise<void>;
443
+ ping(opts?: {
444
+ signal?: AbortSignal;
445
+ }): Promise<void>;
427
446
  /** Remember a fact (into this app's namespace unless `namespace` is given and
428
447
  * the token has the core scope). Intimate-tier writes are rejected server-side. */
429
448
  remember(fact: {
@@ -436,6 +455,8 @@ export declare class CompanionClient {
436
455
  kind?: string;
437
456
  namespace?: string;
438
457
  sensitivity?: 'public' | 'personal';
458
+ }, opts?: {
459
+ signal?: AbortSignal;
439
460
  }): Promise<{
440
461
  cloudId: string;
441
462
  namespace: string;
@@ -459,6 +480,8 @@ export declare class CompanionClient {
459
480
  name: string;
460
481
  kind?: string;
461
482
  locale?: string;
483
+ }, opts?: {
484
+ signal?: AbortSignal;
462
485
  }): Promise<{
463
486
  ok: boolean;
464
487
  summary: string;
@@ -477,6 +500,8 @@ export declare class CompanionClient {
477
500
  name: string;
478
501
  kind?: string;
479
502
  locale?: string;
503
+ }, opts?: {
504
+ signal?: AbortSignal;
480
505
  }): Promise<{
481
506
  ok: boolean;
482
507
  summary: string;
@@ -488,13 +513,17 @@ export declare class CompanionClient {
488
513
  * portrait when one exists (null today for built-in models). URLs are absolute
489
514
  * and the /models asset is CORS-enabled, so a cross-origin renderer can fetch
490
515
  * the VRM directly. Reflects the user's live choice (built-in or custom). */
491
- getAvatar(): Promise<CompanionAvatar>;
516
+ getAvatar(opts?: {
517
+ signal?: AbortSignal;
518
+ }): Promise<CompanionAvatar>;
492
519
  /** Read the companion instance's OWN wallet — nonzero stablecoin balances +
493
520
  * total USD. Read-only and receive-only (spends go through the confirm-gated
494
521
  * pay tools, never here). Requires the `wallet.read` scope (or `wallet.spend`,
495
522
  * which subsumes it). Same data the companion gives when asked "what's my
496
523
  * balance?". */
497
- getWallet(): Promise<CompanionWallet>;
524
+ getWallet(opts?: {
525
+ signal?: AbortSignal;
526
+ }): Promise<CompanionWallet>;
498
527
  /** Canonical URL of the official Pouchy brand icon (square PNG, transparent
499
528
  * background) at the given size. Static + public — needs no token — so it's
500
529
  * safe to drop straight into an `<img src>` for "powered by Pouchy" badges,
@@ -549,7 +578,9 @@ export declare class CompanionClient {
549
578
  * MAY call `confirmAction(sameConfirmId, true)` again to re-run it (the server
550
579
  * dedupes a partial first attempt). A non-idempotent action (a message / skill)
551
580
  * stays terminal and is never retryable. */
552
- confirmAction(confirmId: string, approve: boolean): Promise<{
581
+ confirmAction(confirmId: string, approve: boolean, opts?: {
582
+ signal?: AbortSignal;
583
+ }): Promise<{
553
584
  ok: boolean;
554
585
  status: 'approved' | 'denied' | 'exec_failed';
555
586
  outcome?: string;
@@ -559,7 +590,9 @@ export declare class CompanionClient {
559
590
  * scope, timestamps — never raw args). Use it to rebuild your confirm card
560
591
  * after a reload, since confirm_request events are not replayed. Platform
561
592
  * session tokens only, like `confirmAction`. */
562
- pendingConfirms(): Promise<PendingConfirm[]>;
593
+ pendingConfirms(opts?: {
594
+ signal?: AbortSignal;
595
+ }): Promise<PendingConfirm[]>;
563
596
  /** Convenience: subscribe to live updates of an already-rendered Instant UI
564
597
  * panel (companion.ui_update). Apply each `{ key, value }` to the panel's state
565
598
  * bag and re-evaluate bound displays — no rebuild. */
package/dist/client.js CHANGED
@@ -120,7 +120,16 @@ export class CompanionClient {
120
120
  return major > 1 || (major === 1 && minor >= 1);
121
121
  }
122
122
  async fetchAuthed(url, init) {
123
- const res = await this.doFetch(url, init);
123
+ let res;
124
+ try {
125
+ res = await this.doFetch(url, init);
126
+ }
127
+ catch (e) {
128
+ // A caller-supplied AbortSignal makes fetch throw a native AbortError —
129
+ // convert it at this single choke point so cancellation honors the
130
+ // "every helper rejects with CompanionError" contract (code 'aborted').
131
+ throw this.asCompanionError(e);
132
+ }
124
133
  const apiVersion = res.headers.get('x-pouchy-api-version');
125
134
  if (apiVersion)
126
135
  this.serverApiVersion = apiVersion;
@@ -135,7 +144,24 @@ export class CompanionClient {
135
144
  if (!(await this.tryRefreshToken()))
136
145
  return res;
137
146
  init.headers.Authorization = `Bearer ${this.opts.token}`;
138
- return this.doFetch(url, init);
147
+ try {
148
+ return await this.doFetch(url, init);
149
+ }
150
+ catch (e) {
151
+ throw this.asCompanionError(e);
152
+ }
153
+ }
154
+ /** Normalize a thrown fetch error to a CompanionError. An AbortError (the
155
+ * caller's signal fired) becomes a switchable `code: 'aborted'`; anything
156
+ * else that is already a CompanionError passes through; the rest wrap as a
157
+ * status-0 network failure. */
158
+ asCompanionError(e) {
159
+ if (e instanceof CompanionError)
160
+ return e;
161
+ const name = e?.name;
162
+ if (name === 'AbortError')
163
+ return new CompanionError('request aborted', 0, 'aborted');
164
+ return new CompanionError(e instanceof Error ? e.message : 'network request failed', 0);
139
165
  }
140
166
  /** Peek the error `code` of a failure response without consuming its body. */
141
167
  async errorCodeOf(res) {
@@ -168,11 +194,14 @@ export class CompanionClient {
168
194
  const header = Number(res.headers.get('Retry-After'));
169
195
  return Number.isFinite(header) && header >= 0 ? header : undefined;
170
196
  }
171
- async postJson(path, body) {
197
+ async postJson(path, body, signal) {
172
198
  const res = await this.fetchAuthed(this.url(path), {
173
199
  method: 'POST',
174
200
  headers: this.jsonHeaders(),
175
- body: JSON.stringify(body)
201
+ body: JSON.stringify(body),
202
+ // The signal rides `init`, so the 401-refresh retry in fetchAuthed
203
+ // reuses it automatically (init is passed to both doFetch calls).
204
+ ...(signal ? { signal } : {})
176
205
  });
177
206
  const json = (await res.json().catch(() => null));
178
207
  if (!res.ok || !json || json.ok === false) {
@@ -185,7 +214,7 @@ export class CompanionClient {
185
214
  return json;
186
215
  }
187
216
  /** Handshake: start or resume the session for this surface. */
188
- async connect() {
217
+ async connect(opts) {
189
218
  const json = await this.postJson('/api/companion/session', {
190
219
  surface: this.opts.surface ?? 'default',
191
220
  modalities: this.opts.modalities,
@@ -194,7 +223,7 @@ export class CompanionClient {
194
223
  tools: this.opts.tools,
195
224
  appContext: this.opts.appContext,
196
225
  ...(this.opts.visitor ? { visitor: this.opts.visitor } : {})
197
- });
226
+ }, opts?.signal);
198
227
  this._session = json.session;
199
228
  this.cursor = json.resumeCursor ?? 0;
200
229
  return {
@@ -211,7 +240,7 @@ export class CompanionClient {
211
240
  * Two-token consent: this client's PAT (the owner's) must hold `represent:pair`,
212
241
  * and you pass the VISITOR's own Pouchy PAT — which must hold `social.message`
213
242
  * — as proof + consent. The visitor must therefore also be a Pouchy user. */
214
- async pairVisitor(visitorToken) {
243
+ async pairVisitor(visitorToken, opts) {
215
244
  if (!this.opts.visitor) {
216
245
  throw new CompanionError('pairVisitor() requires a representative session (createCompanion({ visitor }))', 0, 'not_representative');
217
246
  }
@@ -220,7 +249,7 @@ export class CompanionClient {
220
249
  const json = await this.postJson('/api/companion/pair', {
221
250
  visitorToken,
222
251
  visitorId: this.opts.visitor.id
223
- });
252
+ }, opts?.signal);
224
253
  return { pairId: json.pairId ?? null };
225
254
  }
226
255
  async sendText(text, opts) {
@@ -235,10 +264,10 @@ export class CompanionClient {
235
264
  return this.sendTextAwaitingReply(id, body, opts, turnId);
236
265
  const wantStream = opts?.stream ?? this.deltaHandlers.size > 0;
237
266
  if (!wantStream) {
238
- const json = await this.postJson(`/api/companion/session/${id}/input`, body);
267
+ const json = await this.postJson(`/api/companion/session/${id}/input`, body, opts?.signal);
239
268
  return { seq: json.seq ?? null };
240
269
  }
241
- return this.sendTextStreaming(id, body);
270
+ return this.sendTextStreaming(id, body, opts?.signal);
242
271
  }
243
272
  /** The awaitReply leg of sendText. Prefers the in-request answer (the
244
273
  * streaming `done` frame's envelope); falls back to the next
@@ -265,12 +294,27 @@ export class CompanionClient {
265
294
  // promise past replyTimeoutMs (previously only the fallback wait was
266
295
  // bounded). The abort tears the in-flight stream down with it.
267
296
  const ac = new AbortController();
297
+ // Link the caller's signal: aborting it tears down the in-flight stream
298
+ // (via ac) AND rejects the whole operation with `aborted`, exactly like
299
+ // the deadline does. If it is already aborted, fail fast.
300
+ let onCallerAbort;
301
+ if (opts?.signal) {
302
+ if (opts.signal.aborted)
303
+ throw new CompanionError('request aborted', 0, 'aborted');
304
+ onCallerAbort = () => ac.abort();
305
+ opts.signal.addEventListener('abort', onCallerAbort, { once: true });
306
+ }
268
307
  let timer;
269
308
  const deadline = new Promise((_, reject) => {
270
309
  timer = setTimeout(() => {
271
310
  ac.abort();
272
311
  reject(new CompanionError(`timed out after ${timeoutMs}ms waiting for the reply — buffered mode needs start() polling the event stream`, 0, 'reply_timeout'));
273
312
  }, timeoutMs);
313
+ // The caller's signal also rejects the operation (the stream teardown
314
+ // alone would otherwise leave the fallback wait hanging to the timer).
315
+ if (opts?.signal) {
316
+ opts.signal.addEventListener('abort', () => reject(new CompanionError('request aborted', 0, 'aborted')), { once: true });
317
+ }
274
318
  });
275
319
  try {
276
320
  let seq = null;
@@ -308,6 +352,8 @@ export class CompanionClient {
308
352
  off();
309
353
  if (timer !== undefined)
310
354
  clearTimeout(timer);
355
+ if (onCallerAbort)
356
+ opts?.signal?.removeEventListener('abort', onCallerAbort);
311
357
  }
312
358
  }
313
359
  /** The streaming leg of sendText: parse the POST response's SSE frames —
@@ -430,7 +476,7 @@ export class CompanionClient {
430
476
  * (id → a fresh handle, source → this surface), so callers only supply the
431
477
  * meaningful fields: `{ type, data, retained?, salience?, voiceRelevant? }`.
432
478
  * A full CloudEvents envelope is still accepted (its fields win). */
433
- async sendWorldState(event) {
479
+ async sendWorldState(event, opts) {
434
480
  const id = this.requireSession();
435
481
  const fill = (e) => ({
436
482
  ...e,
@@ -439,7 +485,7 @@ export class CompanionClient {
439
485
  source: e.source ?? this.opts.surface ?? 'companion-sdk'
440
486
  });
441
487
  const body = Array.isArray(event) ? { events: event.map(fill) } : fill(event);
442
- const json = await this.postJson(`/api/companion/session/${id}/context`, body);
488
+ const json = await this.postJson(`/api/companion/session/${id}/context`, body, opts?.signal);
443
489
  // injected/reacted tell the host a live-call inject or a proactive text
444
490
  // reaction was triggered by this batch (0.28.0 — previously discarded).
445
491
  return {
@@ -736,7 +782,8 @@ export class CompanionClient {
736
782
  const query = params.toString();
737
783
  const qs = query ? `?${query}` : '';
738
784
  const res = await this.fetchAuthed(this.url(`/api/companion/memory${qs}`), {
739
- headers: { Authorization: `Bearer ${this.opts.token}` }
785
+ headers: { Authorization: `Bearer ${this.opts.token}` },
786
+ ...(opts?.signal ? { signal: opts.signal } : {})
740
787
  });
741
788
  const json = (await res.json().catch(() => null));
742
789
  if (!res.ok || !json || json.ok === false) {
@@ -752,7 +799,10 @@ export class CompanionClient {
752
799
  async history(opts) {
753
800
  const id = this.requireSession();
754
801
  const qs = opts?.limit ? `?limit=${encodeURIComponent(opts.limit)}` : '';
755
- const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/history${qs}`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
802
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/history${qs}`), {
803
+ headers: { Authorization: `Bearer ${this.opts.token}` },
804
+ ...(opts?.signal ? { signal: opts.signal } : {})
805
+ });
756
806
  const json = (await res.json().catch(() => null));
757
807
  if (!res.ok || !json || json.ok === false) {
758
808
  throw new CompanionError(json?.error || `history failed (${res.status})`, res.status, json?.code);
@@ -763,7 +813,7 @@ export class CompanionClient {
763
813
  * `voice`). The request is intersected with the token's granted modalities
764
814
  * server-side — a session can't widen past its key — and the EFFECTIVE set is
765
815
  * returned. Requires a live session. */
766
- async setModalities(modalities) {
816
+ async setModalities(modalities, opts) {
767
817
  const id = this.requireSession();
768
818
  const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/modalities`), {
769
819
  method: 'POST',
@@ -771,7 +821,8 @@ export class CompanionClient {
771
821
  Authorization: `Bearer ${this.opts.token}`,
772
822
  'Content-Type': 'application/json'
773
823
  },
774
- body: JSON.stringify({ modalities })
824
+ body: JSON.stringify({ modalities }),
825
+ ...(opts?.signal ? { signal: opts.signal } : {})
775
826
  });
776
827
  const json = (await res.json().catch(() => null));
777
828
  if (!res.ok || !json || json.ok === false) {
@@ -782,9 +833,13 @@ export class CompanionClient {
782
833
  /** Keepalive: bump this session's last-seen time so a long-idle embed stays
783
834
  * "live" within the session TTL (cross-app A2A friend messages keep reaching
784
835
  * it). Cheap; call it on a timer for a background tab. Requires a live session. */
785
- async ping() {
836
+ async ping(opts) {
786
837
  const id = this.requireSession();
787
- const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/ping`), { method: 'POST', headers: { Authorization: `Bearer ${this.opts.token}` } });
838
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(id)}/ping`), {
839
+ method: 'POST',
840
+ headers: { Authorization: `Bearer ${this.opts.token}` },
841
+ ...(opts?.signal ? { signal: opts.signal } : {})
842
+ });
788
843
  if (!res.ok) {
789
844
  const json = (await res.json().catch(() => null));
790
845
  throw new CompanionError(json?.error || `ping failed (${res.status})`, res.status, json?.code);
@@ -792,8 +847,8 @@ export class CompanionClient {
792
847
  }
793
848
  /** Remember a fact (into this app's namespace unless `namespace` is given and
794
849
  * the token has the core scope). Intimate-tier writes are rejected server-side. */
795
- async remember(fact) {
796
- return this.postJson('/api/companion/memory', fact);
850
+ async remember(fact, opts) {
851
+ return this.postJson('/api/companion/memory', fact, opts?.signal);
797
852
  }
798
853
  /** Ingest a DOCUMENT into the user's knowledge base. Unlike `remember` (one
799
854
  * short fact), this distils already-extracted text — a PDF body, a meeting
@@ -809,8 +864,8 @@ export class CompanionClient {
809
864
  * are computed on the user's next first-party open (eventually-consistent
810
865
  * semantic recall). `kind` is a free label for the source type (pdf / audio /
811
866
  * note / …) used only for display + the materials icon. */
812
- async ingestKnowledge(doc) {
813
- return this.postJson('/api/companion/knowledge', doc);
867
+ async ingestKnowledge(doc, opts) {
868
+ return this.postJson('/api/companion/knowledge', doc, opts?.signal);
814
869
  }
815
870
  /** Convenience over `ingestKnowledge`: hand over a RAW file and Pouchy
816
871
  * understands it server-side before ingesting — no parser/transcriber/vision on
@@ -820,8 +875,8 @@ export class CompanionClient {
820
875
  * material). For other types, extract the text yourself and call
821
876
  * `ingestKnowledge`. Same `memory.write:core` requirement and "My materials"
822
877
  * integration as `ingestKnowledge`. */
823
- async ingestFile(file) {
824
- return this.postJson('/api/companion/knowledge/file', file);
878
+ async ingestFile(file, opts) {
879
+ return this.postJson('/api/companion/knowledge/file', file, opts?.signal);
825
880
  }
826
881
  /** Fetch the user's CURRENT companion avatar so the embedding can render the
827
882
  * same virtual human Pouchy shows. The avatar is a VRM 3D model (`vrmUrl`,
@@ -829,9 +884,10 @@ export class CompanionClient {
829
884
  * portrait when one exists (null today for built-in models). URLs are absolute
830
885
  * and the /models asset is CORS-enabled, so a cross-origin renderer can fetch
831
886
  * the VRM directly. Reflects the user's live choice (built-in or custom). */
832
- async getAvatar() {
887
+ async getAvatar(opts) {
833
888
  const res = await this.fetchAuthed(this.url('/api/companion/avatar'), {
834
- headers: { Authorization: `Bearer ${this.opts.token}` }
889
+ headers: { Authorization: `Bearer ${this.opts.token}` },
890
+ ...(opts?.signal ? { signal: opts.signal } : {})
835
891
  });
836
892
  const json = (await res.json().catch(() => null));
837
893
  if (!res.ok || !json || json.ok === false) {
@@ -850,9 +906,10 @@ export class CompanionClient {
850
906
  * pay tools, never here). Requires the `wallet.read` scope (or `wallet.spend`,
851
907
  * which subsumes it). Same data the companion gives when asked "what's my
852
908
  * balance?". */
853
- async getWallet() {
909
+ async getWallet(opts) {
854
910
  const res = await this.fetchAuthed(this.url('/api/companion/wallet'), {
855
- headers: { Authorization: `Bearer ${this.opts.token}` }
911
+ headers: { Authorization: `Bearer ${this.opts.token}` },
912
+ ...(opts?.signal ? { signal: opts.signal } : {})
856
913
  });
857
914
  const json = (await res.json().catch(() => null));
858
915
  if (!res.ok || !json || json.ok === false) {
@@ -957,17 +1014,20 @@ export class CompanionClient {
957
1014
  * MAY call `confirmAction(sameConfirmId, true)` again to re-run it (the server
958
1015
  * dedupes a partial first attempt). A non-idempotent action (a message / skill)
959
1016
  * stays terminal and is never retryable. */
960
- async confirmAction(confirmId, approve) {
1017
+ async confirmAction(confirmId, approve, opts) {
961
1018
  const sessionId = this.requireSession();
962
- return this.postJson(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`, { confirmId, approve });
1019
+ return this.postJson(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`, { confirmId, approve }, opts?.signal);
963
1020
  }
964
1021
  /** The session's still-pending confirmations (display-safe: id, summary,
965
1022
  * scope, timestamps — never raw args). Use it to rebuild your confirm card
966
1023
  * after a reload, since confirm_request events are not replayed. Platform
967
1024
  * session tokens only, like `confirmAction`. */
968
- async pendingConfirms() {
1025
+ async pendingConfirms(opts) {
969
1026
  const sessionId = this.requireSession();
970
- const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`), { headers: { Authorization: `Bearer ${this.opts.token}` } });
1027
+ const res = await this.fetchAuthed(this.url(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`), {
1028
+ headers: { Authorization: `Bearer ${this.opts.token}` },
1029
+ ...(opts?.signal ? { signal: opts.signal } : {})
1030
+ });
971
1031
  const json = (await res.json().catch(() => null));
972
1032
  if (!res.ok || !json || json.ok === false) {
973
1033
  throw new CompanionError(json?.error || `pending confirms failed (${res.status})`, res.status, json?.code);
package/dist/errors.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { CompanionErrorCode } from './protocol.js';
2
2
  /** Codes the SDK itself synthesizes (status 0 — never sent by the server).
3
3
  * Runtime array so the drift test can assert every `new CompanionError(...)`
4
4
  * call site uses a declared code; APPEND-ONLY like the server vocabulary. */
5
- export declare const SDK_SYNTHESIZED_ERROR_CODES: readonly ["reply_timeout", "needs_event_stream", "not_connected", "missing_option", "not_representative", "call_unsupported", "call_connect_failed", "call_dependency_missing"];
5
+ export declare const SDK_SYNTHESIZED_ERROR_CODES: readonly ["reply_timeout", "needs_event_stream", "not_connected", "missing_option", "not_representative", "call_unsupported", "call_connect_failed", "call_dependency_missing", "aborted"];
6
6
  export type SdkSynthesizedErrorCode = (typeof SDK_SYNTHESIZED_ERROR_CODES)[number];
7
7
  /** Thrown by every helper on an HTTP or client-side failure. `status` is the
8
8
  * HTTP status (0 for client-synthesized failures — timeouts, unsupported
package/dist/errors.js CHANGED
@@ -14,7 +14,8 @@ export const SDK_SYNTHESIZED_ERROR_CODES = [
14
14
  'not_representative', // pairVisitor on a non-representative token
15
15
  'call_unsupported', // startCall on a server/provider without voice
16
16
  'call_connect_failed', // WebRTC/provider handshake failed
17
- 'call_dependency_missing' // optional voice dependency not installed
17
+ 'call_dependency_missing', // optional voice dependency not installed
18
+ 'aborted' // a caller-supplied AbortSignal cancelled the request (0.29.0)
18
19
  ];
19
20
  export class CompanionError extends Error {
20
21
  status;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "description": "Embed the Pouchy companion \u2014 chat, voice, tools, memory, live world-state, instant UI, and agent-to-agent messaging \u2014 in any app, game, or site.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",