@av-pi-studio/client 0.0.1 → 0.0.4
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/README.md +171 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# `@av-pi-studio/client`
|
|
2
|
+
|
|
3
|
+
The client-side library for talking to a Pi-Studio daemon: a low-level WebSocket driver
|
|
4
|
+
(`DaemonClient`) plus a high-level, typed SDK facade (`PiStudioClient`). This package is what
|
|
5
|
+
`@av-pi-studio/cli` is built on, and what any future web/mobile/desktop client should build on
|
|
6
|
+
too.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @av-pi-studio/client
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Two layers
|
|
17
|
+
|
|
18
|
+
1. **`DaemonClient`** — the low-level WebSocket driver: transport lifecycle, the `hello`
|
|
19
|
+
handshake, JSON + binary frame parsing, RPC request/response correlation, ping/pong liveness,
|
|
20
|
+
and a pluggable `Transport` abstraction so tests (or a future non-WebSocket transport) can
|
|
21
|
+
inject their own implementation.
|
|
22
|
+
2. **`PiStudioClient`** — a high-level facade over `DaemonClient` with typed, named methods
|
|
23
|
+
(`createAgent`, `agent(id).send(...)`, `providers.listModels(...)`, …) instead of raw
|
|
24
|
+
`request(type, payload)` calls.
|
|
25
|
+
|
|
26
|
+
Also included: a **`ReconnectionManager`** (exponential-backoff auto-reconnect) and a
|
|
27
|
+
**`TerminalStreamRouter`** (demuxes binary terminal frames by slot).
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { DaemonClient, PiStudioClient, createWebSocketTransport } from "@av-pi-studio/client";
|
|
33
|
+
|
|
34
|
+
const daemon = new DaemonClient({
|
|
35
|
+
url: "ws://127.0.0.1:6767",
|
|
36
|
+
clientId: "my-stable-client-id",
|
|
37
|
+
clientType: "cli", // "mobile" | "browser" | "cli" | "mcp"
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await daemon.connect(); // resolves once the hello → status handshake completes
|
|
41
|
+
|
|
42
|
+
const client = new PiStudioClient(daemon);
|
|
43
|
+
|
|
44
|
+
const { agentId } = await client.createAgent({
|
|
45
|
+
provider: "pi",
|
|
46
|
+
model: "claude-3-5-sonnet",
|
|
47
|
+
cwd: "~/my-project",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const agent = client.agent(agentId);
|
|
51
|
+
agent.timeline.subscribe((event) => console.log(event.kind, event));
|
|
52
|
+
await agent.send("Add a health-check endpoint");
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## `DaemonClient`
|
|
56
|
+
|
|
57
|
+
### Connection lifecycle
|
|
58
|
+
|
|
59
|
+
`idle → connecting → open → closing → closed`, observable via `onStateChange(handler)`.
|
|
60
|
+
|
|
61
|
+
### Key methods
|
|
62
|
+
|
|
63
|
+
| Method | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `connect()` | Opens the transport, waits for the `hello` → `status` handshake to complete |
|
|
66
|
+
| `disconnect()` | Graceful close |
|
|
67
|
+
| `request<T>(type, payload, opts?)` | Sends a correlated RPC, resolves with the response or rejects with `RpcError`/`RpcTimeoutError` |
|
|
68
|
+
| `onSessionMessage(handler)` | Subscribe to every inbound `session` message; returns an unsubscribe fn |
|
|
69
|
+
| `onTerminalFrame(handler)` | Subscribe to every inbound binary terminal frame |
|
|
70
|
+
| `onStateChange(handler)` | Subscribe to connection state transitions |
|
|
71
|
+
| `state` / `serverId` / `features` / `serverCapabilities` | Current connection state and the identity/capabilities from the last handshake |
|
|
72
|
+
|
|
73
|
+
### Errors
|
|
74
|
+
|
|
75
|
+
| Class | When it's thrown |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `RpcError` | The daemon replied with `rpc_error` for this request |
|
|
78
|
+
| `RpcTimeoutError` | No response arrived within `rpcTimeoutMs` — **an operation-level failure only**; the socket is left open |
|
|
79
|
+
|
|
80
|
+
### Constructor options
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
interface DaemonClientOptions {
|
|
84
|
+
url: string;
|
|
85
|
+
clientId: string;
|
|
86
|
+
clientType: "mobile" | "browser" | "cli" | "mcp";
|
|
87
|
+
protocolVersion?: number; // defaults to the current protocol version
|
|
88
|
+
appVersion?: string;
|
|
89
|
+
capabilities?: Record<string, boolean>; // CLIENT_CAPS flags to advertise in `hello`
|
|
90
|
+
transport?: Transport; // inject a stub for tests; defaults to native WebSocket
|
|
91
|
+
rpcTimeoutMs?: number; // per-request timeout; never tears down the socket
|
|
92
|
+
now?: () => number; // inject a clock for deterministic tests
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## `PiStudioClient`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const client = new PiStudioClient(daemonClient);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Member | Description |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `createAgent(req)` | `create_agent_request` RPC |
|
|
105
|
+
| `agent(agentId)` | Scoped actions for an existing agent (`send`, `interrupt`, `update`, `resume`, `archive`, `onUpdate`, `timeline.fetch`/`.subscribe`) |
|
|
106
|
+
| `workspace(workspaceId)` | Scoped actions for a workspace |
|
|
107
|
+
| `providers` | `listProviders()`, `listModels(provider)`, `listModes(provider)`, `refreshSnapshot()` |
|
|
108
|
+
| `onAgentUpdate(handler)` / `onWorkspaceUpdate(handler)` | Subscribe to broadcasts across all agents/workspaces |
|
|
109
|
+
| `connection` | Escape hatch back to the underlying `DaemonClient` |
|
|
110
|
+
| `importAgentSession(daemon, args)` | Named export — resume a provider-native session by handle |
|
|
111
|
+
|
|
112
|
+
## Auto-reconnect
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { ReconnectionManager } from "@av-pi-studio/client";
|
|
116
|
+
|
|
117
|
+
const mgr = new ReconnectionManager(daemon, { initialDelayMs: 500, maxDelayMs: 30_000 });
|
|
118
|
+
mgr.onReconnected(({ attempt, serverId }) => console.log(`reconnected after ${attempt} tries`));
|
|
119
|
+
mgr.start(); // arms; automatically retries with exponential backoff on socket drop
|
|
120
|
+
mgr.stop(); // disarm
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Every reconnect attempt calls `daemon.connect()`, which re-sends the full `hello` handshake, so
|
|
124
|
+
capabilities and identity are always rehydrated transparently.
|
|
125
|
+
|
|
126
|
+
## Terminal frame routing
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { TerminalStreamRouter } from "@av-pi-studio/client";
|
|
130
|
+
|
|
131
|
+
const router = new TerminalStreamRouter();
|
|
132
|
+
const unsubscribe = router.subscribe(slot, (frame) => { /* handle this terminal's frames */ });
|
|
133
|
+
daemon.onTerminalFrame((frame) => router.handleFrame(frame));
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Custom transports
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
interface Transport {
|
|
140
|
+
connect(): Promise<void>;
|
|
141
|
+
send(data: string | Uint8Array): void;
|
|
142
|
+
close(code?: number, reason?: string): void;
|
|
143
|
+
onMessage(handler: (data: string | Uint8Array) => void): () => void;
|
|
144
|
+
onClose(handler: (code: number, reason: string) => void): () => void;
|
|
145
|
+
onError(handler: (err: Error) => void): () => void;
|
|
146
|
+
readonly readyState: "connecting" | "open" | "closing" | "closed";
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`createWebSocketTransport(url, password?)` is the default Node/browser WebSocket-backed
|
|
151
|
+
implementation. Inject your own `Transport` for tests, or to run over a different underlying
|
|
152
|
+
channel.
|
|
153
|
+
|
|
154
|
+
## Design rules
|
|
155
|
+
|
|
156
|
+
- **`RpcTimeoutError` never closes the socket.** A slow RPC is an operation-level failure, not a
|
|
157
|
+
connection failure.
|
|
158
|
+
- **`clientId` stays stable across reconnects** — it identifies the logical client session, not
|
|
159
|
+
the physical connection.
|
|
160
|
+
- **No DOM/Node-specific globals** in `daemon-client.ts` or the base `Transport` type — only
|
|
161
|
+
`createWebSocketTransport` itself touches the platform WebSocket, and the base driver never
|
|
162
|
+
imports it directly.
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm run build # tsc -b
|
|
168
|
+
npm test -- --project packages/client
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Tests inject stub `Transport` implementations and mock clocks — no real sockets are opened.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@av-pi-studio/client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -18,6 +18,6 @@
|
|
|
18
18
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@av-pi-studio/protocol": "^0.0.
|
|
21
|
+
"@av-pi-studio/protocol": "^0.0.4"
|
|
22
22
|
}
|
|
23
23
|
}
|