openclaw-voice 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kyaukyuai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # OpenClaw Voice (Expo React Native)
2
+
3
+ ![OpenClawVoice logo](assets/logo-badge.png)
4
+
5
+ [![Expo SDK 54](https://img.shields.io/badge/Expo-SDK%2054-000020?logo=expo&logoColor=white)](https://expo.dev/)
6
+ [![React Native 0.81](https://img.shields.io/badge/React%20Native-0.81-61DAFB?logo=react&logoColor=1f2937)](https://reactnative.dev/)
7
+ [![TypeScript 5.x](https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
+
9
+ A simple voice-first iOS client for OpenClaw Gateway.
10
+
11
+ Speak -> edit transcript -> send -> stream response.
12
+
13
+ ## npm Package Usage
14
+
15
+ Install:
16
+
17
+ ```bash
18
+ npm install openclaw-voice
19
+ ```
20
+
21
+ Example:
22
+
23
+ ```ts
24
+ import { GatewayClient } from 'openclaw-voice';
25
+
26
+ const client = new GatewayClient('wss://your-openclaw-gateway.example.com', {
27
+ token: 'your-token',
28
+ clientId: 'openclaw-ios',
29
+ displayName: 'OpenClawVoice',
30
+ });
31
+ ```
32
+
33
+ ## Quick Start (5 Minutes)
34
+
35
+ Prerequisites:
36
+
37
+ - Node.js 18+
38
+ - Xcode + iOS runtime
39
+ - CocoaPods
40
+ - A running OpenClaw Gateway endpoint (`wss://...`)
41
+
42
+ Run one command:
43
+
44
+ ```bash
45
+ bash scripts/bootstrap.sh
46
+ ```
47
+
48
+ What this does:
49
+
50
+ - Installs dependencies with `npm install`
51
+ - Generates iOS native project (if missing)
52
+ - Installs CocoaPods
53
+ - Launches the app on a physical device (`npm run ios -- --device`)
54
+
55
+ Or install dependencies manually:
56
+
57
+ ```bash
58
+ npm install
59
+ ```
60
+
61
+ If you prefer simulator:
62
+
63
+ ```bash
64
+ npm run ios
65
+ ```
66
+
67
+ ## Screenshot
68
+
69
+ ![OpenClaw Voice UI states](docs/screenshots/openclaw-voice-v4.html.png)
70
+
71
+ ## Features
72
+
73
+ - Speech-to-text input using `expo-speech-recognition`
74
+ - Editable transcript before sending
75
+ - OpenClaw Gateway connection with URL + token/password
76
+ - Streaming chat response rendering
77
+ - Conversation history with per-turn status (`WAIT`, `OK`, `ERR`)
78
+ - Auto-reconnect capable gateway client
79
+ - Persistent settings for gateway URL, token/password, and theme (`dark` / `light`)
80
+ - Device identity persistence for gateway auth (Ed25519 key pair)
81
+ - Compact UI with header connection status and bottom round action button
82
+
83
+ ## Tech Stack
84
+
85
+ - Expo SDK 54
86
+ - React Native 0.81
87
+ - TypeScript
88
+ - `expo-speech-recognition`
89
+ - `expo-secure-store`
90
+ - `expo-crypto`
91
+ - `@noble/ed25519` + `@noble/hashes`
92
+
93
+ ## Environment Configuration
94
+
95
+ Copy `.env.example` to `.env` and set values for your local environment:
96
+
97
+ ```bash
98
+ cp .env.example .env
99
+ ```
100
+
101
+ Available variables:
102
+
103
+ - `EXPO_PUBLIC_DEFAULT_GATEWAY_URL`
104
+ - `EXPO_PUBLIC_DEFAULT_THEME` (`light` or `dark`)
105
+ - `EXPO_PUBLIC_GATEWAY_CLIENT_ID` (default: `openclaw-ios`)
106
+ - `EXPO_PUBLIC_GATEWAY_DISPLAY_NAME` (default: `OpenClawVoice`)
107
+
108
+ Notes:
109
+
110
+ - `.env` files are ignored by git.
111
+ - Do not commit gateway tokens or secrets.
112
+
113
+ ## Project Structure
114
+
115
+ | Path | Purpose |
116
+ | --- | --- |
117
+ | `App.tsx` | Main UI, voice capture flow, transcript editing, gateway connect/send flow, history rendering |
118
+ | `src/openclaw/client.ts` | OpenClaw Gateway WebSocket protocol client (handshake, events, requests, reconnect, keepalive) |
119
+ | `src/openclaw/protocol.ts` | Protocol v3 types and event/method constants |
120
+ | `src/openclaw/device-identity.ts` | Device identity generation/signing for gateway authentication |
121
+ | `src/openclaw/storage.ts` | Storage abstraction used by identity module |
122
+ | `crypto-polyfill.ts` | `crypto.getRandomValues`, `atob`, `btoa` polyfills for React Native runtime compatibility |
123
+ | `scripts/bootstrap.sh` | One-command setup + run for iOS |
124
+ | `.env.example` | Sample environment configuration |
125
+ | `CONTRIBUTING.md` | Contribution workflow and local checks |
126
+ | `docs/TROUBLESHOOTING.md` | Common issues and fixes |
127
+ | `app.json` | Expo configuration and microphone/speech permission strings |
128
+
129
+ ## How to Use
130
+
131
+ 1. Launch the app.
132
+ 2. If not connected, the gateway panel opens automatically.
133
+ 3. Enter your `Gateway URL` (example: `wss://your-openclaw-gateway.example.com`) and optional `Token / Password`.
134
+ 4. Tap `Connect`.
135
+ 5. Hold the round mic button to speak.
136
+ 6. Release to stop recognition.
137
+ 7. Edit transcript if needed.
138
+ 8. Tap the send button (same round button state) to submit.
139
+ 9. View streamed response in History.
140
+
141
+ ## Scripts
142
+
143
+ - `npm run start` - Start Expo dev server
144
+ - `npm run ios` - Build and run iOS app
145
+ - `npm run android` - Build and run Android app
146
+ - `npm run web` - Run web target
147
+ - `npm run typecheck` - Run TypeScript checks
148
+ - `npm run build:package` - Build npm package files to `dist/`
149
+
150
+ ## Connection and Auth Notes
151
+
152
+ The client currently connects with these defaults:
153
+
154
+ - `clientId: openclaw-ios`
155
+ - `displayName: OpenClawVoice`
156
+ - `role: operator`
157
+ - `scopes: operator.read, operator.write`
158
+ - `caps: talk`
159
+ - Device identity is generated locally and reused via secure persistence when available.
160
+ - If identity is reset, the gateway may request pairing again.
161
+
162
+ ## Speech Recognition Behavior
163
+
164
+ - Language is selectable from the Gateway panel (`日本語 / English`).
165
+ - Interim results are shown while speaking.
166
+ - Final recognition text is stored in editable transcript.
167
+ - Some `aborted/cancelled` recognition errors are intentionally ignored when caused by expected stop flow.
168
+
169
+ ## Configuration Persistence
170
+
171
+ These values are persisted locally:
172
+
173
+ - Gateway URL
174
+ - Token/password
175
+ - Theme mode
176
+ - Device identity record
177
+
178
+ `expo-secure-store` is used when available. If native module loading fails, the app falls back to in-memory storage for runtime continuity.
179
+
180
+ ## Troubleshooting
181
+
182
+ See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for known issues and fixes.
183
+
184
+ ## UI / UX Design Direction
185
+
186
+ The current UI is intentionally minimal:
187
+
188
+ - Small header with live connection chip
189
+ - Gateway settings panel shown only when disconnected
190
+ - Card-based transcript/history surfaces with subtle border + shadow
191
+ - Bottom round icon-only primary action (mic/send state switch)
192
+ - Dark/light theme toggle in header
193
+
194
+ ## Contributing
195
+
196
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for branch/PR workflow and local checks.
197
+
198
+ ## CI
199
+
200
+ GitHub Actions runs on push/PR:
201
+
202
+ - Type check (`npm run typecheck`)
203
+ - Lint (`npm run lint --if-present`)
204
+ - Tests (`npm test --if-present`)
205
+
206
+ Issue and PR templates are available in `.github/`.
207
+
208
+ ## Publish to npm
209
+
210
+ ```bash
211
+ npm run build:package
212
+ npm publish --access public
213
+ ```
214
+
215
+ ## Security Notes
216
+
217
+ - Do not commit private gateway tokens.
218
+ - Use secure `wss://` endpoints.
219
+ - Pair only trusted devices in OpenClaw.
220
+ - Prefer private network exposure first. Recommended order:
221
+ 1. Tailscale/WireGuard private network
222
+ 2. Cloudflare Tunnel with access control
223
+ 3. Hardened VPS reverse proxy
224
+ - Do not expose the Gateway port directly to the public internet.
225
+ - If using Cloudflare Tunnel or VPS, require authentication at both layers:
226
+ - Edge access control (IdP login / service token / mTLS as applicable)
227
+ - Gateway token/password validation
228
+ - Restrict source access (ACL / allowlist), rotate tokens regularly, and revoke leaked credentials immediately.
229
+ - Keep TLS certificates and server packages updated, and enable request/rate logging for incident response.
230
+
231
+ ## Acknowledgements
232
+
233
+ - [`expo-openclaw-chat`](https://github.com/brunobar79/expo-openclaw-chat) for protocol/client reference and implementation ideas.
234
+
235
+ ## License
236
+
237
+ This project is licensed under the MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Gateway WebSocket Client — full protocol v3 implementation
3
+ *
4
+ * Handles connect handshake, request/response, event subscriptions,
5
+ * auto-reconnect with exponential backoff, keepalive, and chat methods.
6
+ */
7
+ import { type HelloOk, type ChatHistoryPayload, type ChatSendResponse, type ChatEventPayload, type AgentEventPayload, type HealthPayload, type SessionsListResponse, type ChatAttachmentPayload, type ErrorShape } from "./protocol";
8
+ export type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting";
9
+ export interface GatewayClientOptions {
10
+ /** Auth token (simple shared secret) */
11
+ token?: string;
12
+ /** Auth password (alternative) */
13
+ password?: string;
14
+ /** Device token from previous pairing */
15
+ deviceToken?: string;
16
+ /** Device identity */
17
+ deviceId?: string;
18
+ /** Auto-reconnect on disconnect (default true) */
19
+ autoReconnect?: boolean;
20
+ /** Default request timeout in ms (default 15000) */
21
+ defaultTimeoutMs?: number;
22
+ /** Client display name */
23
+ displayName?: string;
24
+ /** App version string */
25
+ appVersion?: string;
26
+ /** Platform string (e.g. 'ios', 'android') */
27
+ platform?: string;
28
+ /** Client ID for gateway registration (default: openclaw-ios) */
29
+ clientId?: string;
30
+ /** Role used at connect handshake (default: operator) */
31
+ role?: "operator" | "node";
32
+ /** Scopes used at connect handshake */
33
+ scopes?: string[];
34
+ /** Capabilities used at connect handshake */
35
+ caps?: string[];
36
+ }
37
+ type EventCallback = (payload: unknown) => void;
38
+ type ConnectionStateCallback = (state: ConnectionState) => void;
39
+ type ChatEventCallback = (payload: ChatEventPayload) => void;
40
+ type AgentEventCallback = (payload: AgentEventPayload) => void;
41
+ type HealthEventCallback = (payload: HealthPayload) => void;
42
+ export interface ModelEntry {
43
+ key: string;
44
+ name: string;
45
+ input?: string;
46
+ contextWindow?: number;
47
+ local?: boolean;
48
+ available: boolean;
49
+ tags?: string[];
50
+ missing?: boolean;
51
+ }
52
+ export interface ModelsListResponse {
53
+ count: number;
54
+ models: ModelEntry[];
55
+ }
56
+ export declare class GatewayError extends Error {
57
+ readonly code: string;
58
+ readonly details?: unknown;
59
+ readonly retryable?: boolean;
60
+ readonly retryAfterMs?: number;
61
+ constructor(error: ErrorShape);
62
+ }
63
+ export declare class GatewayClient {
64
+ private url;
65
+ private options;
66
+ private ws;
67
+ private _connectionState;
68
+ private requestIdCounter;
69
+ private pendingRequests;
70
+ private eventListeners;
71
+ private connectionStateListeners;
72
+ private chatEventListeners;
73
+ private agentEventListeners;
74
+ private healthEventListeners;
75
+ private reconnectAttempt;
76
+ private reconnectTimer;
77
+ private intentionalClose;
78
+ private tickTimer;
79
+ private tickIntervalMs;
80
+ private lastTickReceived;
81
+ private missedTickThreshold;
82
+ private lastSeq;
83
+ private connectPromiseResolve;
84
+ private connectPromiseReject;
85
+ private challengeNonce;
86
+ private helloOk;
87
+ private deviceIdentity;
88
+ private awaitingPairing;
89
+ constructor(url: string, options?: GatewayClientOptions);
90
+ get connectionState(): ConnectionState;
91
+ get isConnected(): boolean;
92
+ get serverInfo(): HelloOk | null;
93
+ /**
94
+ * Ensure device identity is loaded (lazy init).
95
+ */
96
+ private ensureIdentity;
97
+ /**
98
+ * Connect to the gateway. Resolves with HelloOk on successful handshake.
99
+ */
100
+ connect(): Promise<HelloOk>;
101
+ /**
102
+ * Cleanly disconnect from the gateway.
103
+ */
104
+ disconnect(): void;
105
+ /**
106
+ * Send a request frame and wait for the matching response.
107
+ */
108
+ request<T = unknown>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
109
+ /**
110
+ * Send a fire-and-forget event frame.
111
+ */
112
+ sendEvent(event: string, payload?: unknown): void;
113
+ /**
114
+ * Subscribe to a specific event name.
115
+ */
116
+ on(eventName: string, callback: EventCallback): void;
117
+ /**
118
+ * Unsubscribe from a specific event name.
119
+ */
120
+ off(eventName: string, callback: EventCallback): void;
121
+ /**
122
+ * Subscribe to connection state changes.
123
+ */
124
+ onConnectionStateChange(callback: ConnectionStateCallback): () => void;
125
+ /**
126
+ * Subscribe to chat streaming events.
127
+ */
128
+ onChatEvent(callback: ChatEventCallback): () => void;
129
+ /**
130
+ * Subscribe to agent run progress events.
131
+ */
132
+ onAgentEvent(callback: AgentEventCallback): () => void;
133
+ /**
134
+ * Subscribe to health events.
135
+ */
136
+ onHealthEvent(callback: HealthEventCallback): () => void;
137
+ /**
138
+ * Fetch chat history for a session.
139
+ */
140
+ chatHistory(sessionKey: string, options?: {
141
+ limit?: number;
142
+ }): Promise<ChatHistoryPayload>;
143
+ /**
144
+ * Send a chat message. Returns runId for tracking streaming events.
145
+ */
146
+ chatSend(sessionKey: string, message: string, options?: {
147
+ thinking?: string;
148
+ attachments?: ChatAttachmentPayload[];
149
+ idempotencyKey?: string;
150
+ timeoutMs?: number;
151
+ }): Promise<ChatSendResponse>;
152
+ /**
153
+ * Abort a running chat agent.
154
+ */
155
+ chatAbort(sessionKey: string, runId: string): Promise<void>;
156
+ /**
157
+ * Subscribe to chat events for a session.
158
+ * Note: Server may auto-subscribe on chatSend, this is a no-op for now.
159
+ */
160
+ chatSubscribe(_sessionKey: string): void;
161
+ /**
162
+ * List available sessions.
163
+ */
164
+ sessionsList(options?: {
165
+ limit?: number;
166
+ includeGlobal?: boolean;
167
+ }): Promise<SessionsListResponse>;
168
+ /**
169
+ * Health check — returns true if gateway responds.
170
+ */
171
+ health(timeoutMs?: number): Promise<boolean>;
172
+ /**
173
+ * List available models from the gateway.
174
+ */
175
+ modelsList(options?: {
176
+ timeoutMs?: number;
177
+ }): Promise<ModelsListResponse>;
178
+ private openWebSocket;
179
+ private handleMessage;
180
+ private handleResponse;
181
+ private handleEvent;
182
+ private handlePairResolved;
183
+ private handleChallenge;
184
+ private handleHelloOk;
185
+ private sendConnectFrame;
186
+ private sendConnectFrameAsync;
187
+ private handleConnectFailure;
188
+ private handleClose;
189
+ private scheduleReconnect;
190
+ private attemptReconnect;
191
+ private clearReconnectTimer;
192
+ private handleTick;
193
+ private startTickMonitor;
194
+ private clearTickTimer;
195
+ private sendFrame;
196
+ private nextRequestId;
197
+ private setConnectionState;
198
+ private emitEvent;
199
+ }
200
+ /**
201
+ * Generate a unique idempotency key for side-effecting requests.
202
+ */
203
+ export declare function generateIdempotencyKey(): string;
204
+ export {};