@pouchy_ai/companion-sdk 0.29.1 → 0.32.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 +117 -1
- package/README.md +65 -4
- package/dist/adapter-core.d.ts +58 -0
- package/dist/adapter-core.d.ts.map +1 -0
- package/dist/adapter-core.js +126 -0
- package/dist/adapter-core.js.map +1 -0
- package/dist/call.d.ts +1 -0
- package/dist/call.d.ts.map +1 -0
- package/dist/call.js +1 -0
- package/dist/call.js.map +1 -0
- package/dist/client.d.ts +91 -5
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +138 -14
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +15 -1
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +58 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +12 -0
- package/dist/protocol.js.map +1 -0
- package/dist/scopes.d.ts +1 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +1 -0
- package/dist/scopes.js.map +1 -0
- package/dist/sse.d.ts +1 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +1 -0
- package/dist/sse.js.map +1 -0
- package/dist/svelte.d.ts +16 -0
- package/dist/svelte.d.ts.map +1 -0
- package/dist/svelte.js +25 -0
- package/dist/svelte.js.map +1 -0
- package/dist/vue.d.ts +15 -0
- package/dist/vue.d.ts.map +1 -0
- package/dist/vue.js +27 -0
- package/dist/vue.js.map +1 -0
- package/dist/ws-transport.d.ts +1 -0
- package/dist/ws-transport.d.ts.map +1 -0
- package/dist/ws-transport.js +1 -0
- package/dist/ws-transport.js.map +1 -0
- package/package.json +24 -6
- package/src/adapter-core.ts +193 -0
- package/src/call.ts +699 -0
- package/src/client.ts +1959 -0
- package/src/errors.ts +77 -0
- package/src/index.ts +104 -0
- package/src/protocol.ts +316 -0
- package/src/scopes.ts +94 -0
- package/src/sse.ts +41 -0
- package/src/svelte.ts +47 -0
- package/src/vue.ts +46 -0
- package/src/ws-transport.ts +70 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// CompanionError — the one error type every SDK helper rejects with (README
|
|
2
|
+
// contract: consumers `switch` on `.code` instead of string-matching prose).
|
|
3
|
+
// Lives in its own module so call.ts can use it without a runtime cycle
|
|
4
|
+
// through client.ts (client.ts re-exports it, keeping the public import path
|
|
5
|
+
// `@pouchy_ai/companion-sdk` → `CompanionError` unchanged).
|
|
6
|
+
|
|
7
|
+
// Type-only import: protocol.ts imports nothing at runtime, so this cannot
|
|
8
|
+
// re-create the client.ts cycle this module exists to avoid.
|
|
9
|
+
import type { CompanionErrorCode, ControlErrorCode } from './protocol';
|
|
10
|
+
|
|
11
|
+
/** Codes the SDK itself synthesizes (status 0 — never sent by the server).
|
|
12
|
+
* Runtime array so the drift test can assert every `new CompanionError(...)`
|
|
13
|
+
* call site uses a declared code; APPEND-ONLY like the server vocabulary. */
|
|
14
|
+
export const SDK_SYNTHESIZED_ERROR_CODES = [
|
|
15
|
+
'reply_timeout', // awaitReply/replyTimeoutMs elapsed before the reply
|
|
16
|
+
'needs_event_stream', // turn paused on tools but start() was never called
|
|
17
|
+
'not_connected', // a call/stream helper was used before connect
|
|
18
|
+
'missing_option', // a required option was omitted (e.g. sessionId)
|
|
19
|
+
'not_representative', // pairVisitor on a non-representative token
|
|
20
|
+
'call_unsupported', // startCall on a server/provider without voice
|
|
21
|
+
'call_connect_failed', // WebRTC/provider handshake failed
|
|
22
|
+
'call_dependency_missing', // optional voice dependency not installed
|
|
23
|
+
'aborted' // a caller-supplied AbortSignal cancelled the request (0.29.0)
|
|
24
|
+
] as const;
|
|
25
|
+
export type SdkSynthesizedErrorCode = (typeof SDK_SYNTHESIZED_ERROR_CODES)[number];
|
|
26
|
+
|
|
27
|
+
/** `control.error` STREAM codes the SDK itself synthesizes (never sent by the
|
|
28
|
+
* server) — the stream-plane sibling of SDK_SYNTHESIZED_ERROR_CODES above.
|
|
29
|
+
* Runtime array so the drift test can assert every locally-emitted
|
|
30
|
+
* `control.error` envelope uses a declared code; APPEND-ONLY. */
|
|
31
|
+
export const SDK_STREAM_ERROR_CODES = [
|
|
32
|
+
'stream_unauthorized' // the event stream got a permanent 401/403 and reconnect gave up
|
|
33
|
+
] as const;
|
|
34
|
+
export type SdkStreamErrorCode = (typeof SDK_STREAM_ERROR_CODES)[number];
|
|
35
|
+
|
|
36
|
+
/** `control.error` `code` values a consumer can switch on (0.30.0): the server
|
|
37
|
+
* vocabulary (CONTROL_ERROR_CODES, drift-tested against the server list) plus
|
|
38
|
+
* the SDK-synthesized stream codes — DERIVED from the runtime lists rather
|
|
39
|
+
* than hand-retyped, same doctrine as CompanionErrorCodeValue below. The
|
|
40
|
+
* `(string & {})` arm keeps the type open for codes newer than this SDK build
|
|
41
|
+
* while preserving autocomplete. */
|
|
42
|
+
export type ControlErrorCodeValue =
|
|
43
|
+
| ControlErrorCode
|
|
44
|
+
| SdkStreamErrorCode
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
46
|
+
| (string & {});
|
|
47
|
+
|
|
48
|
+
/** Thrown by every helper on an HTTP or client-side failure. `status` is the
|
|
49
|
+
* HTTP status (0 for client-synthesized failures — timeouts, unsupported
|
|
50
|
+
* environments, missing optional deps); `code` is machine-switchable when
|
|
51
|
+
* the server (or the SDK itself) names a cause. On a `rate_limited` (429)
|
|
52
|
+
* failure, `retryAfter` carries the server's Retry-After in SECONDS so a
|
|
53
|
+
* consumer can back off precisely instead of guessing. */
|
|
54
|
+
/** `CompanionError.code` values: the server vocabulary (COMPANION_ERROR_CODES,
|
|
55
|
+
* drift-tested against api-error.ts) plus the SDK-synthesized ones — DERIVED
|
|
56
|
+
* from those runtime lists rather than hand-retyped, so a new code can never
|
|
57
|
+
* silently miss this union again (the old copy had drifted 5 codes behind).
|
|
58
|
+
* The `(string & {})` arm keeps the type open for codes newer than this SDK
|
|
59
|
+
* build while preserving autocomplete. */
|
|
60
|
+
export type CompanionErrorCodeValue =
|
|
61
|
+
| CompanionErrorCode
|
|
62
|
+
| SdkSynthesizedErrorCode
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
64
|
+
| (string & {});
|
|
65
|
+
|
|
66
|
+
export class CompanionError extends Error {
|
|
67
|
+
status: number;
|
|
68
|
+
code?: CompanionErrorCodeValue;
|
|
69
|
+
retryAfter?: number;
|
|
70
|
+
constructor(message: string, status: number, code?: string, retryAfter?: number) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = 'CompanionError';
|
|
73
|
+
this.status = status;
|
|
74
|
+
if (code) this.code = code;
|
|
75
|
+
if (typeof retryAfter === 'number' && Number.isFinite(retryAfter)) this.retryAfter = retryAfter;
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Companion SDK — public entry. Embed the Pouchy companion in a third-party
|
|
2
|
+
// surface (web / app / game / hardware) over the REST/SSE plane (with an
|
|
3
|
+
// optional WebSocket transport that degrades to SSE).
|
|
4
|
+
//
|
|
5
|
+
// import { createCompanion } from '$lib/companion-sdk';
|
|
6
|
+
// const c = createCompanion({ baseUrl: 'https://pouchy.ai', token: PAT });
|
|
7
|
+
// await c.connect();
|
|
8
|
+
// c.onMessage((text) => console.log(text));
|
|
9
|
+
// c.start();
|
|
10
|
+
// c.sendWorldState({ type: 'game.scene', data: 'tavern', retained: true });
|
|
11
|
+
// await c.sendText('what should I do?');
|
|
12
|
+
|
|
13
|
+
import { CompanionClient, type CompanionClientOptions } from './client';
|
|
14
|
+
|
|
15
|
+
export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client';
|
|
16
|
+
export type { CompanionDebugEvent } from './client';
|
|
17
|
+
export type { CompanionErrorCodeValue, ControlErrorCodeValue } from './errors';
|
|
18
|
+
export type {
|
|
19
|
+
CompanionClientOptions,
|
|
20
|
+
CompanionStreamState,
|
|
21
|
+
HelloAck,
|
|
22
|
+
RecalledMemory,
|
|
23
|
+
CompanionTurn,
|
|
24
|
+
CallCredentials,
|
|
25
|
+
CompanionToolDecl,
|
|
26
|
+
ToolCallEvent,
|
|
27
|
+
WorldStateInput,
|
|
28
|
+
CompanionAvatar,
|
|
29
|
+
CompanionWallet,
|
|
30
|
+
WalletBalance,
|
|
31
|
+
BrandIconSize,
|
|
32
|
+
EndSessionResult,
|
|
33
|
+
SendTextReply
|
|
34
|
+
} from './client';
|
|
35
|
+
export {
|
|
36
|
+
openCompanionCall,
|
|
37
|
+
HOST_CONTROL_TOOLS,
|
|
38
|
+
HOST_CONTROL_TOOL_NAMES,
|
|
39
|
+
AVATAR_VISUAL_TOOLS,
|
|
40
|
+
AVATAR_VISUAL_TOOL_NAMES
|
|
41
|
+
} from './call';
|
|
42
|
+
export type { CompanionCall, CompanionCallOptions, VoiceToolBridge } from './call';
|
|
43
|
+
export {
|
|
44
|
+
COMPANION_SCOPES,
|
|
45
|
+
COMPANION_MODALITIES,
|
|
46
|
+
SENSITIVE_SCOPES,
|
|
47
|
+
REPRESENT_SCOPES,
|
|
48
|
+
DEFAULT_SCOPES,
|
|
49
|
+
DEFAULT_MODALITIES,
|
|
50
|
+
isSensitiveScope,
|
|
51
|
+
isRepresentScope,
|
|
52
|
+
hasScope
|
|
53
|
+
} from './scopes';
|
|
54
|
+
export type { CompanionScope, CompanionModality } from './scopes';
|
|
55
|
+
export type {
|
|
56
|
+
CompanionEnvelope,
|
|
57
|
+
OutboundType,
|
|
58
|
+
InboundType,
|
|
59
|
+
WorldStateEvent,
|
|
60
|
+
RenderInterfacePayload,
|
|
61
|
+
InterfaceUpdatePayload,
|
|
62
|
+
SocialMessagePayload,
|
|
63
|
+
ConfirmRequestPayload,
|
|
64
|
+
PendingConfirm,
|
|
65
|
+
ToolCallPayload,
|
|
66
|
+
AudioClipPayload,
|
|
67
|
+
ExpressionPayload,
|
|
68
|
+
TypingPayload,
|
|
69
|
+
VoiceInjectPayload,
|
|
70
|
+
UsagePayload,
|
|
71
|
+
MessagePayload,
|
|
72
|
+
ControlErrorPayload,
|
|
73
|
+
HelloAckPayload,
|
|
74
|
+
CallReadyPayload,
|
|
75
|
+
OutboundPayloadMap
|
|
76
|
+
} from './protocol';
|
|
77
|
+
// Runtime protocol constants — so a host can assert PROTOCOL_VERSION or
|
|
78
|
+
// enumerate/validate the event vocabulary without hard-coding the strings.
|
|
79
|
+
// CONTROL_ERROR_CODES (0.30.0) is the stream-plane control.error vocabulary —
|
|
80
|
+
// a separate, smaller list than the HTTP COMPANION_ERROR_CODES.
|
|
81
|
+
export {
|
|
82
|
+
PROTOCOL_VERSION,
|
|
83
|
+
INBOUND_TYPES,
|
|
84
|
+
OUTBOUND_TYPES,
|
|
85
|
+
COMPANION_ERROR_CODES,
|
|
86
|
+
CONTROL_ERROR_CODES
|
|
87
|
+
} from './protocol';
|
|
88
|
+
export type { CompanionErrorCode, ControlErrorCode } from './protocol';
|
|
89
|
+
|
|
90
|
+
// The framework-agnostic view controller behind the /react, /svelte and /vue
|
|
91
|
+
// subpath adapters — exported from the root too, so a host on any OTHER
|
|
92
|
+
// framework (or none) can reuse the same tested snapshot bookkeeping.
|
|
93
|
+
export { createCompanionView } from './adapter-core';
|
|
94
|
+
export type {
|
|
95
|
+
CompanionView,
|
|
96
|
+
CompanionViewOptions,
|
|
97
|
+
CompanionViewSnapshot,
|
|
98
|
+
CompanionTranscriptEntry
|
|
99
|
+
} from './adapter-core';
|
|
100
|
+
|
|
101
|
+
/** Create a companion client. */
|
|
102
|
+
export function createCompanion(opts: CompanionClientOptions): CompanionClient {
|
|
103
|
+
return new CompanionClient(opts);
|
|
104
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// The wire contract, as the SDK sees it — a self-contained copy so the package
|
|
2
|
+
// has ZERO repo-internal ($lib) imports and publishes / builds with plain tsc.
|
|
3
|
+
//
|
|
4
|
+
// This mirrors the server's source of truth (src/lib/types/companion-protocol.ts);
|
|
5
|
+
// a drift test (protocol.drift.test.ts) keeps PROTOCOL_VERSION + the type unions
|
|
6
|
+
// in sync. Adding an outbound type is non-breaking (receivers ignore unknowns).
|
|
7
|
+
|
|
8
|
+
export const PROTOCOL_VERSION = 1 as const;
|
|
9
|
+
|
|
10
|
+
/** App → Pouchy (control/data plane). */
|
|
11
|
+
export const INBOUND_TYPES = [
|
|
12
|
+
'hello',
|
|
13
|
+
'input.text',
|
|
14
|
+
'context.event',
|
|
15
|
+
'context.snapshot',
|
|
16
|
+
'tool.result',
|
|
17
|
+
'control.start_call',
|
|
18
|
+
'control.end_call',
|
|
19
|
+
'control.set_modalities',
|
|
20
|
+
'control.ping'
|
|
21
|
+
] as const;
|
|
22
|
+
export type InboundType = (typeof INBOUND_TYPES)[number];
|
|
23
|
+
|
|
24
|
+
/** Pouchy → app (control/data plane).
|
|
25
|
+
* NOTE: three types are RESERVED — part of the vocabulary + typed handlers, but
|
|
26
|
+
* the server does NOT emit them yet, so `onAudio` / `onExpression` / `onUsage`
|
|
27
|
+
* are safe to register but won't fire until a producer ships. Don't build a UX
|
|
28
|
+
* that depends on them today. (companion.audio → non-call reply TTS;
|
|
29
|
+
* companion.expression → avatar viseme/expression cues; control.usage →
|
|
30
|
+
* per-turn usage-metering echo.) */
|
|
31
|
+
export const OUTBOUND_TYPES = [
|
|
32
|
+
'hello.ack',
|
|
33
|
+
'companion.message',
|
|
34
|
+
'companion.audio', // reserved — not emitted yet
|
|
35
|
+
'companion.tool_call',
|
|
36
|
+
'companion.ui_action',
|
|
37
|
+
'companion.ui_update',
|
|
38
|
+
'companion.expression', // reserved — not emitted yet
|
|
39
|
+
'companion.voice_inject',
|
|
40
|
+
'companion.confirm_request',
|
|
41
|
+
'companion.social_message',
|
|
42
|
+
'companion.typing',
|
|
43
|
+
'control.call_ready',
|
|
44
|
+
'control.error',
|
|
45
|
+
'control.usage' // reserved — not emitted yet
|
|
46
|
+
] as const;
|
|
47
|
+
export type OutboundType = (typeof OUTBOUND_TYPES)[number];
|
|
48
|
+
|
|
49
|
+
export type MessageType = InboundType | OutboundType;
|
|
50
|
+
|
|
51
|
+
/** `control.error` STREAM-plane code vocabulary (0.30.0) — a separate, smaller
|
|
52
|
+
* vocabulary from COMPANION_ERROR_CODES below (those tag HTTP `{ok:false}`
|
|
53
|
+
* response bodies; these tag `control.error` envelopes on the event stream,
|
|
54
|
+
* surfaced by `onError`). Self-contained mirror of the server list
|
|
55
|
+
* (drift-tested); APPEND-ONLY. The SDK itself synthesizes one more,
|
|
56
|
+
* `stream_unauthorized` (errors.ts) — the public union `ControlErrorCodeValue`
|
|
57
|
+
* covers both plus a forward-compat string arm. */
|
|
58
|
+
export const CONTROL_ERROR_CODES = [
|
|
59
|
+
'agent_error', // a server-side turn failed after the input was accepted — no reply was produced; safe to re-send
|
|
60
|
+
'call_mint_failed' // start_call couldn't mint the voice-provider credential — retry later or fall back to text
|
|
61
|
+
] as const;
|
|
62
|
+
export type ControlErrorCode = (typeof CONTROL_ERROR_CODES)[number];
|
|
63
|
+
|
|
64
|
+
/** HTTP error `code` vocabulary — what `CompanionError.code` can hold. A
|
|
65
|
+
* self-contained mirror of the server's api-error.ts list (drift-tested);
|
|
66
|
+
* APPEND-ONLY: renaming or removing a code is a breaking SDK change.
|
|
67
|
+
* Consumers can switch on these instead of string-matching error prose. */
|
|
68
|
+
export const COMPANION_ERROR_CODES = [
|
|
69
|
+
'missing_token', // 401 — no Authorization: Bearer header
|
|
70
|
+
'invalid_token', // 401 — token unknown, revoked, or expired
|
|
71
|
+
'missing_scope', // 403 — token lacks a required grant
|
|
72
|
+
'invalid_request', // 400 — malformed JSON / missing or invalid fields
|
|
73
|
+
'session_not_found', // 404 — sessionId expired or never started
|
|
74
|
+
'turn_pending', // 409 — a turn is paused awaiting tool results
|
|
75
|
+
'no_pending_tools', // 409 — tool result posted but nothing is pending
|
|
76
|
+
'unknown_call', // 404 — tool result for a callId the turn didn't issue
|
|
77
|
+
'payload_too_large', // 413 — body/image over the documented cap
|
|
78
|
+
'rate_limited', // 429 — turn burst / demo daily budget spent; Retry-After + retryAfterSec say when
|
|
79
|
+
'forbidden', // 403 — authorization denial other than a missing scope
|
|
80
|
+
'unavailable', // 503 — backing store/provider not configured or down
|
|
81
|
+
'confirm_not_found', // 404 — confirmId unknown or not on this session
|
|
82
|
+
'confirm_resolved', // 409 — confirm already approved/denied/expired (single-use)
|
|
83
|
+
'step_up_required', // 401 — approval needs a passkey assertion (fetch confirm/stepup)
|
|
84
|
+
'step_up_failed' // 401 — the passkey assertion didn't verify; retry the step-up
|
|
85
|
+
] as const;
|
|
86
|
+
export type CompanionErrorCode = (typeof COMPANION_ERROR_CODES)[number];
|
|
87
|
+
|
|
88
|
+
/** The single envelope every control/data-plane message shares. */
|
|
89
|
+
export interface CompanionEnvelope<T = unknown> {
|
|
90
|
+
v: typeof PROTOCOL_VERSION;
|
|
91
|
+
id: string;
|
|
92
|
+
session?: string;
|
|
93
|
+
ts: number;
|
|
94
|
+
type: MessageType;
|
|
95
|
+
payload: T;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Payload of a `companion.ui_action` event — an Instant UI surface for the host
|
|
99
|
+
* to render. `interface` is the platform-neutral genui schema: a tree of UI
|
|
100
|
+
* `nodes` (Text/Slider/Button/Chart/…), an optional `state` bag input atoms
|
|
101
|
+
* read+write, and optional `computed` derived values. The SAME shape is drawn by
|
|
102
|
+
* a web embed, a native iOS/Android renderer, or a CLI/TUI — only the renderer
|
|
103
|
+
* differs per platform, never the contract. Kept structurally loose here (the
|
|
104
|
+
* SDK package carries no $lib import); the authoritative schema lives in
|
|
105
|
+
* src/lib/genui/schema.ts and the renderer-contract doc. When the panel sets
|
|
106
|
+
* `reportChanges`, the user's edits stream back as a normal text turn. */
|
|
107
|
+
export interface RenderInterfacePayload {
|
|
108
|
+
interface: {
|
|
109
|
+
title?: string;
|
|
110
|
+
nodes: Array<{ type: string; [k: string]: unknown }>;
|
|
111
|
+
state?: Record<string, string | number | boolean>;
|
|
112
|
+
computed?: Record<string, unknown>;
|
|
113
|
+
reportChanges?: boolean;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Payload of a `companion.confirm_request` event — the companion is asking the
|
|
118
|
+
* user to approve a sensitive op (a payment, a skill run, a friend message).
|
|
119
|
+
* Who may approve depends on the token: a PLATFORM SESSION TOKEN (an end-user
|
|
120
|
+
* instance minted via /v1/sessions) resolves it directly with
|
|
121
|
+
* `client.confirmAction` — the embedding app's end user is that account's
|
|
122
|
+
* human. A first-party user's token can only OBSERVE: approval of a real
|
|
123
|
+
* Pouchy user's ops is authed as that user (their app / a hosted confirm
|
|
124
|
+
* page), so a third-party DOM can never confirm a spend, and the first-party
|
|
125
|
+
* host can gate approval behind a biometric (Face ID / passkey) — see
|
|
126
|
+
* docs/companion-wallet-noncustodial.md. */
|
|
127
|
+
export interface ConfirmRequestPayload {
|
|
128
|
+
/** Opaque id the first-party surface passes to the confirm endpoint. */
|
|
129
|
+
confirmId: string;
|
|
130
|
+
/** The scope of the op needing approval (e.g. `wallet.spend`). */
|
|
131
|
+
scope: string;
|
|
132
|
+
/** A human, user-facing summary of what approving will do. */
|
|
133
|
+
summary: string;
|
|
134
|
+
/** Advisory: when true, the first-party approval surface should require a
|
|
135
|
+
* stronger user gesture — a biometric / passkey (Face ID, Touch ID) — before
|
|
136
|
+
* approving, not just a tap. Set for irreversible money ops (wallet.spend).
|
|
137
|
+
* Cryptographic enforcement is server-side follow-up; this is the host's cue. */
|
|
138
|
+
stepUp?: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** One still-pending confirmation, as returned by `client.pendingConfirms()` —
|
|
142
|
+
* display-safe (never the recorded tool call or its raw args). */
|
|
143
|
+
export interface PendingConfirm {
|
|
144
|
+
confirmId: string;
|
|
145
|
+
/** The scope of the op needing approval (e.g. `skills.execute`). */
|
|
146
|
+
scope: string;
|
|
147
|
+
/** A human, user-facing summary of what approving will do. */
|
|
148
|
+
summary: string;
|
|
149
|
+
/** Epoch ms when the companion asked. */
|
|
150
|
+
createdAt: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Payload of a `companion.tool_call` event — the companion is asking the host
|
|
154
|
+
* to run one of the app-declared tools it was given at `hello`. `args` is the
|
|
155
|
+
* raw JSON string the model produced (parse it yourself); reply by posting the
|
|
156
|
+
* result back with `client.sendToolResult(id, …)`. */
|
|
157
|
+
export interface ToolCallPayload {
|
|
158
|
+
/** Correlation id — echo it back on the tool result. */
|
|
159
|
+
id: string;
|
|
160
|
+
/** The declared tool name the model chose. */
|
|
161
|
+
name: string;
|
|
162
|
+
/** The model's arguments as a raw JSON string. */
|
|
163
|
+
args: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Payload of a `companion.ui_update` event — a live change to an
|
|
167
|
+
* already-rendered Instant UI panel: write each `{ key, value }` into the
|
|
168
|
+
* panel's state bag (re-evaluating templates / progress / `when`), without
|
|
169
|
+
* rebuilding. `panelId` targets one of several on-screen panels when present. */
|
|
170
|
+
export interface InterfaceUpdatePayload {
|
|
171
|
+
update: {
|
|
172
|
+
panelId?: string;
|
|
173
|
+
updates: Array<{ key: string; value: string | number | boolean }>;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Payload of a `companion.audio` event — a TTS clip for the assistant's reply on
|
|
178
|
+
* a non-call modality. Either a playable `url` or an opaque `ref` the host
|
|
179
|
+
* resolves. */
|
|
180
|
+
export interface AudioClipPayload {
|
|
181
|
+
url?: string;
|
|
182
|
+
ref?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Payload of a `companion.expression` event — avatar cues for an embed rendering
|
|
186
|
+
* the companion's body (e.g. a VRM): a lip-sync `viseme`, an `expression`, or a
|
|
187
|
+
* `gesture`. A pure-text/audio host can ignore it. */
|
|
188
|
+
export interface ExpressionPayload {
|
|
189
|
+
viseme?: string;
|
|
190
|
+
expression?: string;
|
|
191
|
+
gesture?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Payload of a `control.usage` event — a per-token metering echo the host can
|
|
195
|
+
* surface in a usage/billing view. */
|
|
196
|
+
export interface UsagePayload {
|
|
197
|
+
chatTurns?: number;
|
|
198
|
+
voiceSeconds?: number;
|
|
199
|
+
memoryOps?: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Payload of a `companion.social_message` event — an inbound A2A message from
|
|
203
|
+
* one of the user's paired friends, delivered cross-app to any live embed whose
|
|
204
|
+
* token holds `social.message`. Lets a native/web host surface friend messages
|
|
205
|
+
* in real time without the first-party Pouchy app. Only plain human messages are
|
|
206
|
+
* delivered (Pouchy-internal structured envelopes are not). */
|
|
207
|
+
export interface SocialMessagePayload {
|
|
208
|
+
/** The sender's Pouchy uid. */
|
|
209
|
+
fromUid: string;
|
|
210
|
+
/** The sender's display name. */
|
|
211
|
+
fromName: string;
|
|
212
|
+
/** The message text. */
|
|
213
|
+
content: string;
|
|
214
|
+
/** ISO timestamp of delivery. */
|
|
215
|
+
createdAt: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Payload of a `companion.voice_inject` event — a `voiceRelevant` world-state
|
|
219
|
+
* line the companion should say aloud DURING a live call (the app pushed the
|
|
220
|
+
* moment via `sendWorldState`; the server decided it's worth voicing). `speak`
|
|
221
|
+
* is the host's cue to route `text` to the active voice session. A text-only
|
|
222
|
+
* host can ignore it — the moment still shaped the next turn's context. */
|
|
223
|
+
export interface VoiceInjectPayload {
|
|
224
|
+
text: string;
|
|
225
|
+
speak: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Payload of a `companion.typing` event — an activity indicator bracketing the
|
|
229
|
+
* model work of a turn: `active:true` when the companion starts working,
|
|
230
|
+
* `active:false` when it emits its reply or pauses for an app tool result. It
|
|
231
|
+
* spans the whole turn — the tool-loop / thinking phase BEFORE the first text
|
|
232
|
+
* delta, which `onDelta` can't observe — so a host can show a "typing…" state
|
|
233
|
+
* uniformly across streaming and non-streaming replies. Advisory: a lost event
|
|
234
|
+
* never affects the turn; the terminal `companion.message` is authoritative. */
|
|
235
|
+
export interface TypingPayload {
|
|
236
|
+
active: boolean;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Payload of a `companion.message` event — the companion's text reply. When
|
|
240
|
+
* the turn was started by this client with sendText, `replyTo` echoes the
|
|
241
|
+
* request's turnId (0.28.0) so a reply can be correlated to its turn;
|
|
242
|
+
* proactive messages (world-state reactions, confirm outcomes) carry none. */
|
|
243
|
+
export interface MessagePayload {
|
|
244
|
+
text: string;
|
|
245
|
+
replyTo?: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** A live world-state event the app streams in. `retained:true` = latest-value
|
|
249
|
+
* state (coalesced); `retained:false` = a transient moment carrying `salience`
|
|
250
|
+
* and optional `voiceRelevant`. */
|
|
251
|
+
export interface WorldStateEvent<D = unknown> {
|
|
252
|
+
specversion: '1.0';
|
|
253
|
+
id: string;
|
|
254
|
+
source: string;
|
|
255
|
+
/** Namespaced dotted kind, e.g. `game.player.hp`, `game.event.boss_spawned`. */
|
|
256
|
+
type: string;
|
|
257
|
+
time?: string;
|
|
258
|
+
data: D;
|
|
259
|
+
retained?: boolean;
|
|
260
|
+
ttl?: number;
|
|
261
|
+
salience?: number;
|
|
262
|
+
voiceRelevant?: boolean;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Payload of a `control.error` event — a typed error on the event stream
|
|
266
|
+
* (surfaced by `onError`). `code` is one of CONTROL_ERROR_CODES, the
|
|
267
|
+
* SDK-synthesized `stream_unauthorized`, or a code newer than this SDK build
|
|
268
|
+
* (the open arm keeps forward compat without losing autocomplete). */
|
|
269
|
+
export interface ControlErrorPayload {
|
|
270
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
271
|
+
code: ControlErrorCode | (string & {});
|
|
272
|
+
message: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Payload of a `hello.ack` — the handshake reply. On the REST plane this is
|
|
276
|
+
* the `/session` response body (what `connect()` normalizes into `HelloAck`);
|
|
277
|
+
* the event stream's greeting `open` frame carries a partial echo (just
|
|
278
|
+
* `{ resumeCursor }`), so every field is optional at the wire level. */
|
|
279
|
+
export interface HelloAckPayload {
|
|
280
|
+
session?: string;
|
|
281
|
+
grantedScopes?: string[];
|
|
282
|
+
modalities?: string[];
|
|
283
|
+
resumeCursor?: number;
|
|
284
|
+
representative?: boolean;
|
|
285
|
+
visitorPaired?: boolean;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Payload of a `control.call_ready` event — the stream echo of `start_call`'s
|
|
289
|
+
* accept. Deliberately SECRET-FREE (`provider` plus non-secret metadata such
|
|
290
|
+
* as `agentId` / `model` / `voice`); the actual WebRTC credentials only ride
|
|
291
|
+
* the `startCall` HTTP response. */
|
|
292
|
+
export interface CallReadyPayload {
|
|
293
|
+
provider: string;
|
|
294
|
+
[k: string]: unknown;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Per-event payload types, keyed by outbound event name — what `client.on()`
|
|
298
|
+
* narrows envelopes with (0.30.0). Type-only; extending the base Record keeps
|
|
299
|
+
* indexing valid for every OutboundType, so an event added to the vocabulary
|
|
300
|
+
* before it gets a dedicated payload type simply reads as `unknown`. */
|
|
301
|
+
export interface OutboundPayloadMap extends Record<OutboundType, unknown> {
|
|
302
|
+
'hello.ack': HelloAckPayload;
|
|
303
|
+
'companion.message': MessagePayload;
|
|
304
|
+
'companion.audio': AudioClipPayload;
|
|
305
|
+
'companion.tool_call': ToolCallPayload;
|
|
306
|
+
'companion.ui_action': RenderInterfacePayload;
|
|
307
|
+
'companion.ui_update': InterfaceUpdatePayload;
|
|
308
|
+
'companion.expression': ExpressionPayload;
|
|
309
|
+
'companion.voice_inject': VoiceInjectPayload;
|
|
310
|
+
'companion.confirm_request': ConfirmRequestPayload;
|
|
311
|
+
'companion.social_message': SocialMessagePayload;
|
|
312
|
+
'companion.typing': TypingPayload;
|
|
313
|
+
'control.call_ready': CallReadyPayload;
|
|
314
|
+
'control.error': ControlErrorPayload;
|
|
315
|
+
'control.usage': UsagePayload;
|
|
316
|
+
}
|
package/src/scopes.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
|
|
11
|
+
/** Every capability scope a Personal Access Token / OAuth grant can carry. */
|
|
12
|
+
export const COMPANION_SCOPES = [
|
|
13
|
+
'chat', // text turns
|
|
14
|
+
'voice', // TTS / STT
|
|
15
|
+
'call', // realtime WebRTC voice session
|
|
16
|
+
'files', // multimodal file in/out (vision / image)
|
|
17
|
+
'events.subscribe', // receive the outbound companion event stream
|
|
18
|
+
'worldstate.write', // push inbound live world-state / context events
|
|
19
|
+
'ui.render', // let the companion render Instant UI surfaces on the host (display + forms; not sensitive)
|
|
20
|
+
'memory.read:app', // read this app's own memory namespace
|
|
21
|
+
'memory.write:app', // write into this app's own memory namespace
|
|
22
|
+
'memory.read:core', // read the shared global brain (SENSITIVE)
|
|
23
|
+
'memory.write:core', // write into the shared global brain (SENSITIVE)
|
|
24
|
+
'skills.execute', // run the user's installed skills (SENSITIVE)
|
|
25
|
+
'wallet.read', // READ-ONLY wallet: balance + own deposit address (receive-only; not sensitive). GET /api/companion/wallet (SDK: getWallet(), 0.20.0+) or ask the companion's wallet tools.
|
|
26
|
+
'wallet.spend', // spend from the Care Wallet (SENSITIVE)
|
|
27
|
+
'social.message', // message friends via A2A (SENSITIVE)
|
|
28
|
+
// ── Representative ("on-behalf-of") plane ─────────────────────────────
|
|
29
|
+
'represent', // open visitor-facing representative sessions (SENSITIVE)
|
|
30
|
+
'expose:knowledge', // let the representative draw on the owner's knowledge base (SENSITIVE)
|
|
31
|
+
'expose:facts', // widen exposed facts past the identity/preference floor (SENSITIVE)
|
|
32
|
+
'represent:pair', // let a representative pair a visitor (also a Pouchy user) as a friend (SENSITIVE)
|
|
33
|
+
'represent:remember' // let a representative keep durable per-visitor notes across visits (SENSITIVE)
|
|
34
|
+
] as const;
|
|
35
|
+
export type CompanionScope = (typeof COMPANION_SCOPES)[number];
|
|
36
|
+
|
|
37
|
+
/** I/O modalities a key can carry alongside its scopes. */
|
|
38
|
+
export const COMPANION_MODALITIES = ['text', 'voice', 'call', 'files'] as const;
|
|
39
|
+
export type CompanionModality = (typeof COMPANION_MODALITIES)[number];
|
|
40
|
+
|
|
41
|
+
/** Sensitive scopes — never granted by default; the user opts in at key
|
|
42
|
+
* creation, and execution still passes a confirm on a Pouchy-controlled
|
|
43
|
+
* surface (see `companion.confirm_request`). */
|
|
44
|
+
export const SENSITIVE_SCOPES: ReadonlySet<CompanionScope> = new Set<CompanionScope>([
|
|
45
|
+
'memory.read:core',
|
|
46
|
+
'memory.write:core',
|
|
47
|
+
'skills.execute',
|
|
48
|
+
'wallet.spend',
|
|
49
|
+
'social.message',
|
|
50
|
+
'represent',
|
|
51
|
+
'expose:knowledge',
|
|
52
|
+
'expose:facts',
|
|
53
|
+
'represent:pair',
|
|
54
|
+
'represent:remember'
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** The representative ("on-behalf-of") plane — a distinct axis from the
|
|
58
|
+
* execution scopes: one lets the companion ACT for the owner, the other lets
|
|
59
|
+
* it REPRESENT the owner to an outside visitor. `represent` is the umbrella;
|
|
60
|
+
* the `expose:*` / `represent:*` scopes only mean anything alongside it. */
|
|
61
|
+
export const REPRESENT_SCOPES: ReadonlySet<CompanionScope> = new Set<CompanionScope>([
|
|
62
|
+
'represent',
|
|
63
|
+
'expose:knowledge',
|
|
64
|
+
'expose:facts',
|
|
65
|
+
'represent:pair',
|
|
66
|
+
'represent:remember'
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
/** What a freshly-generated key carries unless the user opts into more: a
|
|
70
|
+
* chat/play companion confined to its own app memory namespace. */
|
|
71
|
+
export const DEFAULT_SCOPES: readonly CompanionScope[] = [
|
|
72
|
+
'chat',
|
|
73
|
+
'voice',
|
|
74
|
+
'call',
|
|
75
|
+
'files',
|
|
76
|
+
'events.subscribe',
|
|
77
|
+
'worldstate.write',
|
|
78
|
+
'memory.read:app',
|
|
79
|
+
'memory.write:app'
|
|
80
|
+
];
|
|
81
|
+
export const DEFAULT_MODALITIES: readonly CompanionModality[] = ['text', 'voice'];
|
|
82
|
+
|
|
83
|
+
export function isSensitiveScope(s: CompanionScope): boolean {
|
|
84
|
+
return SENSITIVE_SCOPES.has(s);
|
|
85
|
+
}
|
|
86
|
+
export function isRepresentScope(s: CompanionScope): boolean {
|
|
87
|
+
return REPRESENT_SCOPES.has(s);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Does a granted scope set (e.g. `hello.ack.grantedScopes`) satisfy a
|
|
91
|
+
* required scope? Exact-match for v1 (no hierarchical wildcards). */
|
|
92
|
+
export function hasScope(granted: readonly string[], required: CompanionScope): boolean {
|
|
93
|
+
return granted.includes(required);
|
|
94
|
+
}
|
package/src/sse.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Minimal, dependency-free SSE frame parser for the companion SDK. The browser's
|
|
2
|
+
// EventSource can't set an Authorization header, so the SDK consumes the stream
|
|
3
|
+
// over fetch() instead and parses frames itself — this is that parser, kept pure
|
|
4
|
+
// so it unit-tests in isolation.
|
|
5
|
+
//
|
|
6
|
+
// An SSE frame is a block of `field: value` lines terminated by a blank line.
|
|
7
|
+
// Comment lines (starting ":") are keep-alives and ignored. A chunk off the wire
|
|
8
|
+
// may end mid-frame, so the parser returns the leftover for the next call.
|
|
9
|
+
|
|
10
|
+
export interface SseEvent {
|
|
11
|
+
/** The `event:` field; defaults to "message" per the SSE spec. */
|
|
12
|
+
event: string;
|
|
13
|
+
/** The joined `data:` field(s). */
|
|
14
|
+
data: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Parse all COMPLETE frames out of an accumulated buffer. Returns the parsed
|
|
18
|
+
* events plus the unparsed remainder (an incomplete trailing frame). */
|
|
19
|
+
export function parseSse(buffer: string): { events: SseEvent[]; rest: string } {
|
|
20
|
+
const events: SseEvent[] = [];
|
|
21
|
+
let working = buffer.replace(/\r\n/g, '\n');
|
|
22
|
+
let sep = working.indexOf('\n\n');
|
|
23
|
+
while (sep !== -1) {
|
|
24
|
+
const block = working.slice(0, sep);
|
|
25
|
+
working = working.slice(sep + 2);
|
|
26
|
+
let event = 'message';
|
|
27
|
+
const dataLines: string[] = [];
|
|
28
|
+
for (const line of block.split('\n')) {
|
|
29
|
+
if (!line || line.startsWith(':')) continue; // blank or comment (keep-alive)
|
|
30
|
+
const ci = line.indexOf(':');
|
|
31
|
+
const field = ci === -1 ? line : line.slice(0, ci);
|
|
32
|
+
let value = ci === -1 ? '' : line.slice(ci + 1);
|
|
33
|
+
if (value.startsWith(' ')) value = value.slice(1); // single leading space stripped
|
|
34
|
+
if (field === 'event') event = value;
|
|
35
|
+
else if (field === 'data') dataLines.push(value);
|
|
36
|
+
}
|
|
37
|
+
if (dataLines.length) events.push({ event, data: dataLines.join('\n') });
|
|
38
|
+
sep = working.indexOf('\n\n');
|
|
39
|
+
}
|
|
40
|
+
return { events, rest: working };
|
|
41
|
+
}
|
package/src/svelte.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Svelte adapter (0.31.0) — `@pouchy_ai/companion-sdk/svelte`.
|
|
2
|
+
// Implements the Svelte STORE CONTRACT (subscribe(run) → unsubscribe) by hand,
|
|
3
|
+
// so it needs no `svelte` import and works from Svelte 3 through 5 (`$store`
|
|
4
|
+
// auto-subscription, or `fromStore()` in runes mode). Behavior lives in
|
|
5
|
+
// adapter-core.ts.
|
|
6
|
+
//
|
|
7
|
+
// const companion = companionStore(client);
|
|
8
|
+
// $companion.transcript / $companion.draft / $companion.streamState
|
|
9
|
+
// companion.sendText('hi');
|
|
10
|
+
|
|
11
|
+
import type { CompanionClient } from './client';
|
|
12
|
+
import {
|
|
13
|
+
createCompanionView,
|
|
14
|
+
type CompanionView,
|
|
15
|
+
type CompanionViewOptions,
|
|
16
|
+
type CompanionViewSnapshot
|
|
17
|
+
} from './adapter-core';
|
|
18
|
+
|
|
19
|
+
/** Svelte-store-contract readable of the companion view snapshot. */
|
|
20
|
+
export interface CompanionStore {
|
|
21
|
+
/** Store contract: calls `run` with the current snapshot immediately and on
|
|
22
|
+
* every change. Returns unsubscribe. */
|
|
23
|
+
subscribe(run: (snapshot: CompanionViewSnapshot) => void): () => void;
|
|
24
|
+
sendText: CompanionView['sendText'];
|
|
25
|
+
confirmAction: CompanionView['confirmAction'];
|
|
26
|
+
/** Detach from the client (does not close it). */
|
|
27
|
+
dispose(): void;
|
|
28
|
+
client: CompanionClient;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Wrap a host-owned client as a Svelte-compatible readable store. */
|
|
32
|
+
export function companionStore(
|
|
33
|
+
client: CompanionClient,
|
|
34
|
+
opts?: CompanionViewOptions
|
|
35
|
+
): CompanionStore {
|
|
36
|
+
const view = createCompanionView(client, opts);
|
|
37
|
+
return {
|
|
38
|
+
subscribe(run) {
|
|
39
|
+
run(view.getSnapshot());
|
|
40
|
+
return view.subscribe(() => run(view.getSnapshot()));
|
|
41
|
+
},
|
|
42
|
+
sendText: view.sendText,
|
|
43
|
+
confirmAction: view.confirmAction,
|
|
44
|
+
dispose: view.dispose,
|
|
45
|
+
client
|
|
46
|
+
};
|
|
47
|
+
}
|