@pouchy_ai/companion-sdk 0.18.0 → 0.19.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,34 @@ a protocol bump is always called out explicitly here.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.19.0] - 2026-07-10
16
+
17
+ ### Added
18
+
19
+ - **Typed scope vocabulary exported from the package.** `COMPANION_SCOPES` /
20
+ `CompanionScope`, `COMPANION_MODALITIES` / `CompanionModality`,
21
+ `SENSITIVE_SCOPES`, `REPRESENT_SCOPES`, `DEFAULT_SCOPES`,
22
+ `DEFAULT_MODALITIES`, and the helpers `isSensitiveScope()`,
23
+ `isRepresentScope()`, `hasScope(granted, required)`. Configure keys, gate
24
+ features on `hello.ack.grantedScopes`, and explain 403s with compile-checked
25
+ scope strings instead of prose-copied literals. Mirrors the server vocabulary
26
+ and is guarded by the same drift test as the wire protocol. Additive — no
27
+ wire-protocol change.
28
+
29
+ ## [0.18.1] - 2026-07-08
30
+
31
+ ### Changed
32
+
33
+ - `confirmAction()` return type widened: `status` may now be `'exec_failed'` and
34
+ the response carries an optional `retryable` flag. When an approved **idempotent**
35
+ action (a wallet payment) fails on a transient error, the server no longer burns
36
+ the confirmation — it returns `{ status: 'exec_failed', retryable: true }`, and
37
+ you MAY re-call `confirmAction(sameConfirmId, true)` to re-run it (the server
38
+ dedupes a partial first attempt on the confirmId). Non-idempotent actions (a
39
+ friend message, a skill POST) stay strictly single-use and are never retryable.
40
+ Additive + backwards-compatible: existing `'approved' | 'denied'` handling is
41
+ unaffected. No wire-protocol change.
42
+
15
43
  ## [0.18.0] - 2026-07-08
16
44
 
17
45
  ### Added
package/README.md CHANGED
@@ -92,6 +92,30 @@ Reference renderers ship in [`examples/`](../../examples): `web-instant-ui` (van
92
92
  `native-instant-ui` (SwiftUI + Jetpack Compose), and `cli-skills` (terminal +
93
93
  local/device skills).
94
94
 
95
+ ## Scopes (typed)
96
+
97
+ The full capability-scope vocabulary ships as typed constants — no more
98
+ copying scope strings from prose:
99
+
100
+ ```ts
101
+ import {
102
+ COMPANION_SCOPES, SENSITIVE_SCOPES, DEFAULT_SCOPES,
103
+ hasScope, isSensitiveScope, type CompanionScope
104
+ } from '@pouchy_ai/companion-sdk';
105
+
106
+ const ack = await c.connect();
107
+ if (!hasScope(ack.grantedScopes, 'skills.execute')) {
108
+ // hide the "run a skill" affordance instead of eating a 403
109
+ }
110
+ ```
111
+
112
+ `COMPANION_SCOPES` lists every scope a key can carry; `SENSITIVE_SCOPES` are
113
+ the ones that are never on by default (money / skills / social / core memory /
114
+ the representative plane); `DEFAULT_SCOPES` is what a fresh key gets. Also
115
+ exported: `COMPANION_MODALITIES`, `REPRESENT_SCOPES`, `DEFAULT_MODALITIES`,
116
+ `isRepresentScope()`. The list mirrors the server vocabulary and is guarded by
117
+ the SDK's drift test.
118
+
95
119
  ## Representative mode (代聊)
96
120
 
97
121
  Pass a `visitor` and the session flips from **owner-facing** to **representative**:
package/dist/client.d.ts CHANGED
@@ -438,12 +438,19 @@ export declare class CompanionClient {
438
438
  * approval the recorded action runs server-side; its result comes back as
439
439
  * `outcome` in this response (render it where the user tapped) AND as a
440
440
  * normal companion.message on the stream (so the conversation shows it).
441
- * A denial returns/emits a brief decline the same way. One-time:
442
- * re-resolving a settled confirmId fails with 409. */
441
+ * A denial returns/emits a brief decline the same way. Single-use:
442
+ * re-resolving a settled confirmId fails with 409.
443
+ *
444
+ * RETRY: if an approved IDEMPOTENT action (a wallet payment) flakes on a
445
+ * transient error, `status` is `'exec_failed'` and `retryable` is true — you
446
+ * MAY call `confirmAction(sameConfirmId, true)` again to re-run it (the server
447
+ * dedupes a partial first attempt). A non-idempotent action (a message / skill)
448
+ * stays terminal and is never retryable. */
443
449
  confirmAction(confirmId: string, approve: boolean): Promise<{
444
450
  ok: boolean;
445
- status: 'approved' | 'denied';
451
+ status: 'approved' | 'denied' | 'exec_failed';
446
452
  outcome?: string;
453
+ retryable?: boolean;
447
454
  }>;
448
455
  /** The session's still-pending confirmations (display-safe: id, summary,
449
456
  * scope, timestamps — never raw args). Use it to rebuild your confirm card
package/dist/client.js CHANGED
@@ -678,8 +678,14 @@ export class CompanionClient {
678
678
  * approval the recorded action runs server-side; its result comes back as
679
679
  * `outcome` in this response (render it where the user tapped) AND as a
680
680
  * normal companion.message on the stream (so the conversation shows it).
681
- * A denial returns/emits a brief decline the same way. One-time:
682
- * re-resolving a settled confirmId fails with 409. */
681
+ * A denial returns/emits a brief decline the same way. Single-use:
682
+ * re-resolving a settled confirmId fails with 409.
683
+ *
684
+ * RETRY: if an approved IDEMPOTENT action (a wallet payment) flakes on a
685
+ * transient error, `status` is `'exec_failed'` and `retryable` is true — you
686
+ * MAY call `confirmAction(sameConfirmId, true)` again to re-run it (the server
687
+ * dedupes a partial first attempt). A non-idempotent action (a message / skill)
688
+ * stays terminal and is never retryable. */
683
689
  async confirmAction(confirmId, approve) {
684
690
  const sessionId = this.requireSession();
685
691
  return this.postJson(`/api/companion/session/${encodeURIComponent(sessionId)}/confirm`, { confirmId, approve });
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js
3
3
  export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, WorldStateInput, CompanionAvatar, 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
+ export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
7
+ export type { CompanionScope, CompanionModality } from './scopes.js';
6
8
  export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, AudioClipPayload, ExpressionPayload, TypingPayload, VoiceInjectPayload, UsagePayload } from './protocol.js';
7
9
  /** Create a companion client. */
8
10
  export declare function createCompanion(opts: CompanionClientOptions): CompanionClient;
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@
11
11
  import { CompanionClient } from './client.js';
12
12
  export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js';
13
13
  export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_VISUAL_TOOLS, AVATAR_VISUAL_TOOL_NAMES } from './call.js';
14
+ export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
14
15
  /** Create a companion client. */
15
16
  export function createCompanion(opts) {
16
17
  return new CompanionClient(opts);
@@ -0,0 +1,24 @@
1
+ /** Every capability scope a Personal Access Token / OAuth grant can carry. */
2
+ export declare const COMPANION_SCOPES: readonly ["chat", "voice", "call", "files", "events.subscribe", "worldstate.write", "ui.render", "memory.read:app", "memory.write:app", "memory.read:core", "memory.write:core", "skills.execute", "wallet.read", "wallet.spend", "social.message", "represent", "expose:knowledge", "expose:facts", "represent:pair", "represent:remember"];
3
+ export type CompanionScope = (typeof COMPANION_SCOPES)[number];
4
+ /** I/O modalities a key can carry alongside its scopes. */
5
+ export declare const COMPANION_MODALITIES: readonly ["text", "voice", "call", "files"];
6
+ export type CompanionModality = (typeof COMPANION_MODALITIES)[number];
7
+ /** Sensitive scopes — never granted by default; the user opts in at key
8
+ * creation, and execution still passes a confirm on a Pouchy-controlled
9
+ * surface (see `companion.confirm_request`). */
10
+ export declare const SENSITIVE_SCOPES: ReadonlySet<CompanionScope>;
11
+ /** The representative ("on-behalf-of") plane — a distinct axis from the
12
+ * execution scopes: one lets the companion ACT for the owner, the other lets
13
+ * it REPRESENT the owner to an outside visitor. `represent` is the umbrella;
14
+ * the `expose:*` / `represent:*` scopes only mean anything alongside it. */
15
+ export declare const REPRESENT_SCOPES: ReadonlySet<CompanionScope>;
16
+ /** What a freshly-generated key carries unless the user opts into more: a
17
+ * chat/play companion confined to its own app memory namespace. */
18
+ export declare const DEFAULT_SCOPES: readonly CompanionScope[];
19
+ export declare const DEFAULT_MODALITIES: readonly CompanionModality[];
20
+ export declare function isSensitiveScope(s: CompanionScope): boolean;
21
+ export declare function isRepresentScope(s: CompanionScope): boolean;
22
+ /** Does a granted scope set (e.g. `hello.ack.grantedScopes`) satisfy a
23
+ * required scope? Exact-match for v1 (no hierarchical wildcards). */
24
+ export declare function hasScope(granted: readonly string[], required: CompanionScope): boolean;
package/dist/scopes.js ADDED
@@ -0,0 +1,85 @@
1
+ // Companion capability scopes — the SDK's self-contained copy (no $lib
2
+ // imports; same doctrine as protocol.ts). The server's source of truth is
3
+ // src/lib/types/companion-scopes.ts; protocol.drift.test.ts fails if the two
4
+ // diverge — extend both together.
5
+ //
6
+ // Why the SDK exports these: a host app configuring a key, gating a feature
7
+ // on `hello.ack.grantedScopes`, or explaining a `403 missing scope` needs the
8
+ // vocabulary as types and constants, not prose. Typos in scope strings become
9
+ // compile errors instead of runtime 403s.
10
+ /** Every capability scope a Personal Access Token / OAuth grant can carry. */
11
+ export const COMPANION_SCOPES = [
12
+ 'chat', // text turns
13
+ 'voice', // TTS / STT
14
+ 'call', // realtime WebRTC voice session
15
+ 'files', // multimodal file in/out (vision / image)
16
+ 'events.subscribe', // receive the outbound companion event stream
17
+ 'worldstate.write', // push inbound live world-state / context events
18
+ 'ui.render', // let the companion render Instant UI surfaces on the host (display + forms; not sensitive)
19
+ 'memory.read:app', // read this app's own memory namespace
20
+ 'memory.write:app', // write into this app's own memory namespace
21
+ 'memory.read:core', // read the shared global brain (SENSITIVE)
22
+ 'memory.write:core', // write into the shared global brain (SENSITIVE)
23
+ 'skills.execute', // run the user's installed skills (SENSITIVE)
24
+ 'wallet.read', // READ-ONLY wallet: balance + own deposit address (receive-only; not sensitive)
25
+ 'wallet.spend', // spend from the Care Wallet (SENSITIVE)
26
+ 'social.message', // message friends via A2A (SENSITIVE)
27
+ // ── Representative ("on-behalf-of") plane ─────────────────────────────
28
+ 'represent', // open visitor-facing representative sessions (SENSITIVE)
29
+ 'expose:knowledge', // let the representative draw on the owner's knowledge base (SENSITIVE)
30
+ 'expose:facts', // widen exposed facts past the identity/preference floor (SENSITIVE)
31
+ 'represent:pair', // let a representative pair a visitor (also a Pouchy user) as a friend (SENSITIVE)
32
+ 'represent:remember' // let a representative keep durable per-visitor notes across visits (SENSITIVE)
33
+ ];
34
+ /** I/O modalities a key can carry alongside its scopes. */
35
+ export const COMPANION_MODALITIES = ['text', 'voice', 'call', 'files'];
36
+ /** Sensitive scopes — never granted by default; the user opts in at key
37
+ * creation, and execution still passes a confirm on a Pouchy-controlled
38
+ * surface (see `companion.confirm_request`). */
39
+ export const SENSITIVE_SCOPES = new Set([
40
+ 'memory.read:core',
41
+ 'memory.write:core',
42
+ 'skills.execute',
43
+ 'wallet.spend',
44
+ 'social.message',
45
+ 'represent',
46
+ 'expose:knowledge',
47
+ 'expose:facts',
48
+ 'represent:pair',
49
+ 'represent:remember'
50
+ ]);
51
+ /** The representative ("on-behalf-of") plane — a distinct axis from the
52
+ * execution scopes: one lets the companion ACT for the owner, the other lets
53
+ * it REPRESENT the owner to an outside visitor. `represent` is the umbrella;
54
+ * the `expose:*` / `represent:*` scopes only mean anything alongside it. */
55
+ export const REPRESENT_SCOPES = new Set([
56
+ 'represent',
57
+ 'expose:knowledge',
58
+ 'expose:facts',
59
+ 'represent:pair',
60
+ 'represent:remember'
61
+ ]);
62
+ /** What a freshly-generated key carries unless the user opts into more: a
63
+ * chat/play companion confined to its own app memory namespace. */
64
+ export const DEFAULT_SCOPES = [
65
+ 'chat',
66
+ 'voice',
67
+ 'call',
68
+ 'files',
69
+ 'events.subscribe',
70
+ 'worldstate.write',
71
+ 'memory.read:app',
72
+ 'memory.write:app'
73
+ ];
74
+ export const DEFAULT_MODALITIES = ['text', 'voice'];
75
+ export function isSensitiveScope(s) {
76
+ return SENSITIVE_SCOPES.has(s);
77
+ }
78
+ export function isRepresentScope(s) {
79
+ return REPRESENT_SCOPES.has(s);
80
+ }
81
+ /** Does a granted scope set (e.g. `hello.ack.grantedScopes`) satisfy a
82
+ * required scope? Exact-match for v1 (no hierarchical wildcards). */
83
+ export function hasScope(granted, required) {
84
+ return granted.includes(required);
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/companion-sdk",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",