@av-pi-studio/client 0.0.3 → 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.
Files changed (2) hide show
  1. package/README.md +171 -0
  2. 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",
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.3"
21
+ "@av-pi-studio/protocol": "^0.0.4"
22
22
  }
23
23
  }