openclaw-voice 1.0.2 → 1.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 CHANGED
@@ -12,7 +12,7 @@ Speak -> edit -> send -> stream response.
12
12
 
13
13
  <p align="center">
14
14
  <video
15
- src="https://github.com/user-attachments/assets/65f799f3-c87b-4c13-8b5f-23491efd5ec5"
15
+ src="https://github.com/user-attachments/assets/17678911-a359-490c-a529-67e9b38ff4bb"
16
16
  poster="https://raw.githubusercontent.com/kyaukyuai/openclaw-voice/main/docs/screenshots/response-light.png"
17
17
  controls
18
18
  playsinline
@@ -24,14 +24,14 @@ Speak -> edit -> send -> stream response.
24
24
  </p>
25
25
 
26
26
  <p align="center">
27
- <a href="https://github.com/user-attachments/assets/65f799f3-c87b-4c13-8b5f-23491efd5ec5"><strong>Watch demo video (MP4)</strong></a>
27
+ <a href="https://github.com/user-attachments/assets/17678911-a359-490c-a529-67e9b38ff4bb"><strong>Watch demo video (MP4)</strong></a>
28
28
  </p>
29
29
 
30
30
  ## Why OpenClaw Voice
31
31
 
32
32
  - Fast voice-to-chat workflow optimized for iOS
33
33
  - Reusable `GatewayClient` SDK on npm (`openclaw-voice`)
34
- - Streaming response handling with reconnect and pairing flow support
34
+ - Streaming + recovery handling for unstable mobile networks
35
35
  - Secure device identity signing via Ed25519
36
36
 
37
37
  ## Run The App (5 Minutes)
@@ -111,35 +111,38 @@ Notes:
111
111
  <td align="center" width="25%">
112
112
  <strong>Idle</strong><br>
113
113
  <sub>Connected, waiting for input</sub><br><br>
114
- <img src="https://raw.githubusercontent.com/kyaukyuai/openclaw-voice/main/docs/screenshots/idle.png" width="200" alt="Idle state" />
114
+ <img src="https://raw.githubusercontent.com/kyaukyuai/openclaw-voice/main/docs/screenshots/idle.png" width="230" alt="Idle state" />
115
115
  </td>
116
116
  <td align="center" width="25%">
117
117
  <strong>Ready to Send</strong><br>
118
118
  <sub>Transcript ready, tap to send</sub><br><br>
119
- <img src="https://raw.githubusercontent.com/kyaukyuai/openclaw-voice/main/docs/screenshots/ready-to-send.png" width="200" alt="Ready to send state" />
119
+ <img src="https://raw.githubusercontent.com/kyaukyuai/openclaw-voice/main/docs/screenshots/ready-to-send.png" width="230" alt="Ready to send state" />
120
120
  </td>
121
121
  <td align="center" width="25%">
122
122
  <strong>Sending</strong><br>
123
123
  <sub>Waiting for Gateway response</sub><br><br>
124
- <img src="https://raw.githubusercontent.com/kyaukyuai/openclaw-voice/main/docs/screenshots/sending.png" width="200" alt="Sending state" />
124
+ <img src="https://raw.githubusercontent.com/kyaukyuai/openclaw-voice/main/docs/screenshots/sending.png" width="230" alt="Sending state" />
125
125
  </td>
126
126
  <td align="center" width="25%">
127
- <strong>Response (Light)</strong><br>
127
+ <strong>Response</strong><br>
128
128
  <sub>Streamed response displayed</sub><br><br>
129
- <img src="https://raw.githubusercontent.com/kyaukyuai/openclaw-voice/main/docs/screenshots/response-light.png" width="200" alt="Response in light theme" />
129
+ <img src="https://raw.githubusercontent.com/kyaukyuai/openclaw-voice/main/docs/screenshots/response-light.png" width="230" alt="Response in light theme" />
130
130
  </td>
131
131
  </tr>
132
132
  </table>
133
133
 
134
134
  ## Features
135
135
 
136
- - Speech-to-text input using `expo-speech-recognition`
137
- - Editable transcript before sending
138
- - OpenClaw Gateway connection with URL + token/password
136
+ - Voice input with hold-to-record (`expo-speech-recognition`)
137
+ - Editable transcript and quick text insert buttons
138
+ - Speech language switch (`ja-JP` / `en-US`)
139
+ - Dedicated **Settings** screen and **Sessions** screen
140
+ - Session management: list, switch, rename, pin/unpin, create
141
+ - Gateway connect/reconnect flow with startup auto-connect retry
142
+ - History sync and manual refresh with status notice
139
143
  - Streaming response rendering with per-turn states (`WAIT`, `OK`, `ERR`)
140
- - Auto reconnect support
141
- - Persistent settings for gateway URL, token/password, and theme
142
- - Local device identity generation/signing for gateway auth
144
+ - Markdown response rendering with URL linkification
145
+ - Persistent local settings and secure local device identity reuse
143
146
 
144
147
  ## Environment Variables
145
148
 
@@ -151,9 +154,10 @@ cp .env.example .env
151
154
 
152
155
  - `EXPO_PUBLIC_DEFAULT_GATEWAY_URL`
153
156
  - `EXPO_PUBLIC_DEFAULT_THEME` (`light` or `dark`)
157
+ - `EXPO_PUBLIC_DEFAULT_SESSION_KEY` (default: `main`)
154
158
  - `EXPO_PUBLIC_GATEWAY_CLIENT_ID` (default: `openclaw-ios`)
155
159
  - `EXPO_PUBLIC_GATEWAY_DISPLAY_NAME` (default: `OpenClawVoice`)
156
- - `EXPO_PUBLIC_DEBUG_MODE` (`true` to show warnings in dev, default: `false`)
160
+ - `EXPO_PUBLIC_DEBUG_MODE` (`true` to show dev warnings and runtime debug panel, default: `false`)
157
161
 
158
162
  ## Connection Defaults
159
163
 
@@ -172,8 +176,28 @@ Device identity is generated locally and reused when persistent storage is avail
172
176
  - `npm run android` - Build and run Android app
173
177
  - `npm run web` - Run web target
174
178
  - `npm run typecheck` - Run TypeScript checks
179
+ - `npm run lint` - Run repository lint checks
180
+ - `npm test` - Run regression tests (runtime logic + manifest switch)
181
+ - `npm run smoke:pack-install` - Pack tarball and verify install/import from a clean temp app
175
182
  - `npm run build:package` - Build npm package files to `dist/`
176
183
 
184
+ ## Local Quality Checks
185
+
186
+ Run before opening a PR:
187
+
188
+ ```bash
189
+ npm run typecheck
190
+ npm run lint
191
+ npm test
192
+ npm run smoke:pack-install
193
+ ```
194
+
195
+ If your environment cannot access npm network during smoke test:
196
+
197
+ ```bash
198
+ OPENCLAW_SMOKE_SKIP_INSTALL=1 npm run smoke:pack-install
199
+ ```
200
+
177
201
  ## Security Notes
178
202
 
179
203
  - Do not commit private gateway tokens.
@@ -182,6 +206,12 @@ Device identity is generated locally and reused when persistent storage is avail
182
206
  - Do not expose raw Gateway ports publicly.
183
207
  - Rotate credentials and keep TLS/server packages up to date.
184
208
 
209
+ ## Funding
210
+
211
+ If this project helps your workflow, you can support maintenance on GitHub Sponsors:
212
+
213
+ - [@kyaukyuai](https://github.com/sponsors/kyaukyuai)
214
+
185
215
  ## Troubleshooting
186
216
 
187
217
  See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).
@@ -195,16 +225,36 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
195
225
  GitHub Actions runs on push/PR:
196
226
 
197
227
  - Type check (`npm run typecheck`)
198
- - Lint (`npm run lint --if-present`)
199
- - Tests (`npm test --if-present`)
228
+ - Package dry-run (`npm pack --dry-run`)
229
+ - Manifest restore check after pack (`package.json.main` stays `index.ts`)
230
+ - Lint (`npm run lint`)
231
+ - Tests (`npm test`)
232
+ - Tarball install smoke test (`npm run smoke:pack-install`)
200
233
 
201
234
  Issue/PR templates are in `.github/`.
202
235
 
203
236
  ## Publish to npm
204
237
 
238
+ This repo uses two entry contexts:
239
+
240
+ - App runtime: `package.json.main = index.ts`
241
+ - npm package tarball: `main = ./dist/package.js` (switched automatically during pack/publish)
242
+
243
+ Release steps:
244
+
205
245
  ```bash
206
- npm run build:package
246
+ npm version patch --no-git-tag-version
247
+ git add package.json package-lock.json
248
+ git commit -m "chore(release): bump version to x.y.z"
249
+
250
+ # Runs prepack/postpack hooks automatically:
251
+ # - prepack: build + switch manifest for package publish
252
+ # - postpack: restore app manifest
207
253
  npm publish --access public
254
+
255
+ git tag vX.Y.Z
256
+ git push -u origin main
257
+ git push origin vX.Y.Z
208
258
  ```
209
259
 
210
260
  ## Acknowledgements
@@ -53,6 +53,17 @@ export interface ModelsListResponse {
53
53
  count: number;
54
54
  models: ModelEntry[];
55
55
  }
56
+ export interface SessionPatchInput {
57
+ label?: string;
58
+ displayName?: string;
59
+ subject?: string;
60
+ room?: string;
61
+ [key: string]: unknown;
62
+ }
63
+ export interface SessionDeleteTarget {
64
+ key: string;
65
+ sessionId?: string;
66
+ }
56
67
  export declare class GatewayError extends Error {
57
68
  readonly code: string;
58
69
  readonly details?: unknown;
@@ -165,6 +176,21 @@ export declare class GatewayClient {
165
176
  limit?: number;
166
177
  includeGlobal?: boolean;
167
178
  }): Promise<SessionsListResponse>;
179
+ /**
180
+ * Patch session metadata (e.g. label/displayName).
181
+ * Uses a few payload shapes for compatibility with gateway variants.
182
+ */
183
+ sessionsPatch(sessionKey: string, patch: SessionPatchInput): Promise<void>;
184
+ /**
185
+ * Delete a session by key.
186
+ * Uses a few payload shapes for compatibility with gateway variants.
187
+ */
188
+ sessionsDelete(target: string | SessionDeleteTarget): Promise<void>;
189
+ /**
190
+ * Reset a session by key.
191
+ * Some gateway builds expose reset semantics where delete is unavailable.
192
+ */
193
+ sessionsReset(target: string | SessionDeleteTarget): Promise<void>;
168
194
  /**
169
195
  * Health check — returns true if gateway responds.
170
196
  */
@@ -268,6 +268,108 @@ class GatewayClient {
268
268
  limit: options === null || options === void 0 ? void 0 : options.limit,
269
269
  }, 15000);
270
270
  }
271
+ /**
272
+ * Patch session metadata (e.g. label/displayName).
273
+ * Uses a few payload shapes for compatibility with gateway variants.
274
+ */
275
+ async sessionsPatch(sessionKey, patch) {
276
+ const payloads = [
277
+ { sessionKey, patch },
278
+ { key: sessionKey, patch },
279
+ { sessionKey, ...patch },
280
+ { key: sessionKey, ...patch },
281
+ ];
282
+ let lastError;
283
+ for (const payload of payloads) {
284
+ try {
285
+ await this.request(protocol_1.GatewayMethods.SESSIONS_PATCH, payload, 10000);
286
+ return;
287
+ }
288
+ catch (error) {
289
+ lastError = error;
290
+ if (!(error instanceof GatewayError) || error.code !== "INVALID_REQUEST") {
291
+ throw error;
292
+ }
293
+ }
294
+ }
295
+ throw lastError instanceof Error
296
+ ? lastError
297
+ : new Error("sessions.patch failed");
298
+ }
299
+ /**
300
+ * Delete a session by key.
301
+ * Uses a few payload shapes for compatibility with gateway variants.
302
+ */
303
+ async sessionsDelete(target) {
304
+ var _a;
305
+ const sessionKey = typeof target === "string" ? target.trim() : target.key.trim();
306
+ const sessionId = typeof target === "string" ? "" : ((_a = target.sessionId) !== null && _a !== void 0 ? _a : "").trim();
307
+ if (!sessionKey) {
308
+ throw new Error("sessions.delete requires a non-empty session key");
309
+ }
310
+ const payloads = [
311
+ { sessionKey },
312
+ { key: sessionKey },
313
+ { sessionKeys: [sessionKey] },
314
+ { keys: [sessionKey] },
315
+ ];
316
+ if (sessionId) {
317
+ payloads.unshift({ sessionKey, sessionId }, { key: sessionKey, sessionId }, { key: sessionKey, id: sessionId }, { sessionId }, { id: sessionId }, { sessionIds: [sessionId] }, { ids: [sessionId] });
318
+ }
319
+ let lastError;
320
+ for (const payload of payloads) {
321
+ try {
322
+ await this.request(protocol_1.GatewayMethods.SESSIONS_DELETE, payload, 10000);
323
+ return;
324
+ }
325
+ catch (error) {
326
+ lastError = error;
327
+ if (!(error instanceof GatewayError) || error.code !== "INVALID_REQUEST") {
328
+ throw error;
329
+ }
330
+ }
331
+ }
332
+ throw lastError instanceof Error
333
+ ? lastError
334
+ : new Error("sessions.delete failed");
335
+ }
336
+ /**
337
+ * Reset a session by key.
338
+ * Some gateway builds expose reset semantics where delete is unavailable.
339
+ */
340
+ async sessionsReset(target) {
341
+ var _a;
342
+ const sessionKey = typeof target === "string" ? target.trim() : target.key.trim();
343
+ const sessionId = typeof target === "string" ? "" : ((_a = target.sessionId) !== null && _a !== void 0 ? _a : "").trim();
344
+ if (!sessionKey) {
345
+ throw new Error("sessions.reset requires a non-empty session key");
346
+ }
347
+ const payloads = [
348
+ { sessionKey },
349
+ { key: sessionKey },
350
+ { sessionKeys: [sessionKey] },
351
+ { keys: [sessionKey] },
352
+ ];
353
+ if (sessionId) {
354
+ payloads.unshift({ sessionKey, sessionId }, { key: sessionKey, sessionId }, { sessionId }, { id: sessionId }, { sessionIds: [sessionId] }, { ids: [sessionId] });
355
+ }
356
+ let lastError;
357
+ for (const payload of payloads) {
358
+ try {
359
+ await this.request(protocol_1.GatewayMethods.SESSIONS_RESET, payload, 10000);
360
+ return;
361
+ }
362
+ catch (error) {
363
+ lastError = error;
364
+ if (!(error instanceof GatewayError) || error.code !== "INVALID_REQUEST") {
365
+ throw error;
366
+ }
367
+ }
368
+ }
369
+ throw lastError instanceof Error
370
+ ? lastError
371
+ : new Error("sessions.reset failed");
372
+ }
271
373
  /**
272
374
  * Health check — returns true if gateway responds.
273
375
  */
@@ -460,7 +562,7 @@ class GatewayClient {
460
562
  this.awaitingPairing = false;
461
563
  if (payload.decision === "approved") {
462
564
  // Retry connect now that we're approved
463
- console.log("[GatewayClient] Device approved, retrying connect...");
565
+ console.info("[GatewayClient] Device approved, retrying connect...");
464
566
  this.sendConnectFrame();
465
567
  }
466
568
  else {
@@ -1,4 +1,4 @@
1
- export { GatewayClient, GatewayError, generateIdempotencyKey, type ConnectionState, type GatewayClientOptions, type ModelEntry, type ModelsListResponse, } from './client';
1
+ export { GatewayClient, GatewayError, generateIdempotencyKey, type ConnectionState, type GatewayClientOptions, type ModelEntry, type ModelsListResponse, type SessionPatchInput, } from './client';
2
2
  export { loadOrCreateIdentity, signPayload, publicKeyBase64Url, buildSignaturePayload, type StoredDeviceIdentity, } from './device-identity';
3
3
  export { type Storage, storage, setStorage } from './storage';
4
4
  export { GATEWAY_PROTOCOL_VERSION, GatewayEvents, GatewayMethods, ErrorCode, type GatewayEventName, type RequestFrame, type ResponseFrame, type EventFrame, type GatewayFrame, type ErrorShape, type ConnectChallenge, type ClientInfo, type DeviceIdentity, type ConnectAuth, type ConnectParams, type HelloOk, type HelloOkAuth, type HelloOkPolicy, type TickPayload, type HealthPayload, type ChatEventPayload, type AgentEventPayload, type SeqGapPayload, type ShutdownPayload, type ChatMessageContentType, type ChatMessageContent, type ChatUsage, type ChatUsageCost, type ChatMessage, type ChatAttachmentPayload, type ChatHistoryPayload, type ChatSendResponse, type SessionEntry, type SessionsListResponse, type SessionsDefaults, type SessionPreviewItem, type SessionPreviewEntry, type SessionsPreviewPayload, type StateVersion, type Snapshot, type PresenceEntry, } from './protocol';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-voice",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "main": "./dist/package.js",
5
5
  "types": "./dist/package.d.ts",
6
6
  "exports": {
@@ -23,7 +23,10 @@
23
23
  "ios": "expo run:ios",
24
24
  "web": "expo start --web",
25
25
  "typecheck": "tsc --noEmit",
26
+ "lint": "node scripts/lint.mjs",
27
+ "test": "node --test tests/*.test.mjs",
26
28
  "build:package": "tsc -p tsconfig.package.json",
29
+ "smoke:pack-install": "node scripts/smoke-pack-install.mjs",
27
30
  "prepare:publish-manifest": "node scripts/switch-package-manifest.mjs prepare",
28
31
  "restore:publish-manifest": "node scripts/switch-package-manifest.mjs restore",
29
32
  "prepack": "npm run build:package && npm run prepare:publish-manifest",
@@ -36,6 +39,7 @@
36
39
  "base-64": "^1.0.0",
37
40
  "expo": "~54.0.33",
38
41
  "expo-crypto": "^15.0.8",
42
+ "expo-haptics": "^15.0.8",
39
43
  "expo-secure-store": "^15.0.8",
40
44
  "expo-speech-recognition": "^3.1.0",
41
45
  "expo-status-bar": "~3.0.9",