openvoiceui 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.
Files changed (185) hide show
  1. package/.env.example +104 -0
  2. package/Dockerfile +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +638 -0
  5. package/SETUP.md +360 -0
  6. package/app.py +232 -0
  7. package/auto-approve-devices.js +111 -0
  8. package/cli/index.js +372 -0
  9. package/config/__init__.py +4 -0
  10. package/config/default.yaml +43 -0
  11. package/config/flags.yaml +67 -0
  12. package/config/loader.py +203 -0
  13. package/config/providers.yaml +71 -0
  14. package/config/speech_normalization.yaml +182 -0
  15. package/config/theme.json +4 -0
  16. package/data/greetings.json +25 -0
  17. package/default-pages/ai-image-creator.html +915 -0
  18. package/default-pages/bulk-image-uploader.html +492 -0
  19. package/default-pages/desktop.html +2865 -0
  20. package/default-pages/file-explorer.html +854 -0
  21. package/default-pages/interactive-map.html +655 -0
  22. package/default-pages/style-guide.html +1005 -0
  23. package/default-pages/website-setup.html +1623 -0
  24. package/deploy/openclaw/Dockerfile +46 -0
  25. package/deploy/openvoiceui.service +30 -0
  26. package/deploy/setup-nginx.sh +50 -0
  27. package/deploy/setup-sudo.sh +306 -0
  28. package/deploy/skill-runner/Dockerfile +19 -0
  29. package/deploy/skill-runner/requirements.txt +14 -0
  30. package/deploy/skill-runner/server.py +269 -0
  31. package/deploy/supertonic/Dockerfile +22 -0
  32. package/deploy/supertonic/server.py +79 -0
  33. package/docker-compose.pinokio.yml +11 -0
  34. package/docker-compose.yml +59 -0
  35. package/greetings.json +25 -0
  36. package/index.html +65 -0
  37. package/inject-device-identity.js +142 -0
  38. package/package.json +82 -0
  39. package/profiles/default.json +114 -0
  40. package/profiles/manager.py +354 -0
  41. package/profiles/schema.json +337 -0
  42. package/prompts/voice-system-prompt.md +149 -0
  43. package/providers/__init__.py +39 -0
  44. package/providers/base.py +63 -0
  45. package/providers/llm/__init__.py +12 -0
  46. package/providers/llm/base.py +71 -0
  47. package/providers/llm/clawdbot_provider.py +112 -0
  48. package/providers/llm/zai_provider.py +115 -0
  49. package/providers/registry.py +320 -0
  50. package/providers/stt/__init__.py +12 -0
  51. package/providers/stt/base.py +58 -0
  52. package/providers/stt/webspeech_provider.py +49 -0
  53. package/providers/stt/whisper_provider.py +100 -0
  54. package/providers/tts/__init__.py +20 -0
  55. package/providers/tts/base.py +91 -0
  56. package/providers/tts/groq_provider.py +74 -0
  57. package/providers/tts/supertonic_provider.py +72 -0
  58. package/requirements.txt +38 -0
  59. package/routes/__init__.py +10 -0
  60. package/routes/admin.py +515 -0
  61. package/routes/canvas.py +1315 -0
  62. package/routes/chat.py +51 -0
  63. package/routes/conversation.py +2158 -0
  64. package/routes/elevenlabs_hybrid.py +306 -0
  65. package/routes/greetings.py +98 -0
  66. package/routes/icons.py +279 -0
  67. package/routes/image_gen.py +364 -0
  68. package/routes/instructions.py +190 -0
  69. package/routes/music.py +838 -0
  70. package/routes/onboarding.py +43 -0
  71. package/routes/pi.py +62 -0
  72. package/routes/profiles.py +215 -0
  73. package/routes/report_issue.py +68 -0
  74. package/routes/static_files.py +533 -0
  75. package/routes/suno.py +664 -0
  76. package/routes/theme.py +81 -0
  77. package/routes/transcripts.py +199 -0
  78. package/routes/vision.py +348 -0
  79. package/routes/workspace.py +288 -0
  80. package/server.py +1510 -0
  81. package/services/__init__.py +1 -0
  82. package/services/auth.py +143 -0
  83. package/services/canvas_versioning.py +239 -0
  84. package/services/db_pool.py +107 -0
  85. package/services/gateway.py +16 -0
  86. package/services/gateway_manager.py +333 -0
  87. package/services/gateways/__init__.py +12 -0
  88. package/services/gateways/base.py +110 -0
  89. package/services/gateways/compat.py +264 -0
  90. package/services/gateways/openclaw.py +1134 -0
  91. package/services/health.py +100 -0
  92. package/services/memory_client.py +455 -0
  93. package/services/paths.py +26 -0
  94. package/services/speech_normalizer.py +285 -0
  95. package/services/tts.py +270 -0
  96. package/setup-config.js +262 -0
  97. package/sounds/air_horn.mp3 +0 -0
  98. package/sounds/bruh.mp3 +0 -0
  99. package/sounds/crowd_cheer.mp3 +0 -0
  100. package/sounds/gunshot.mp3 +0 -0
  101. package/sounds/impact.mp3 +0 -0
  102. package/sounds/lets_go.mp3 +0 -0
  103. package/sounds/record_stop.mp3 +0 -0
  104. package/sounds/rewind.mp3 +0 -0
  105. package/sounds/sad_trombone.mp3 +0 -0
  106. package/sounds/scratch_long.mp3 +0 -0
  107. package/sounds/yeah.mp3 +0 -0
  108. package/src/adapters/ClawdBotAdapter.js +264 -0
  109. package/src/adapters/_template.js +133 -0
  110. package/src/adapters/elevenlabs-classic.js +841 -0
  111. package/src/adapters/elevenlabs-hybrid.js +812 -0
  112. package/src/adapters/hume-evi.js +676 -0
  113. package/src/admin.html +1339 -0
  114. package/src/app.js +8802 -0
  115. package/src/core/Config.js +173 -0
  116. package/src/core/EmotionEngine.js +307 -0
  117. package/src/core/EventBridge.js +180 -0
  118. package/src/core/EventBus.js +117 -0
  119. package/src/core/VoiceSession.js +607 -0
  120. package/src/face/BaseFace.js +259 -0
  121. package/src/face/EyeFace.js +208 -0
  122. package/src/face/HaloSmokeFace.js +509 -0
  123. package/src/face/manifest.json +27 -0
  124. package/src/face/previews/eyes.svg +16 -0
  125. package/src/face/previews/orb.svg +29 -0
  126. package/src/features/MusicPlayer.js +620 -0
  127. package/src/features/Soundboard.js +128 -0
  128. package/src/providers/DeepgramSTT.js +472 -0
  129. package/src/providers/DeepgramStreamingSTT.js +766 -0
  130. package/src/providers/GroqSTT.js +559 -0
  131. package/src/providers/TTSPlayer.js +323 -0
  132. package/src/providers/WebSpeechSTT.js +479 -0
  133. package/src/providers/tts/BaseTTSProvider.js +81 -0
  134. package/src/providers/tts/HumeProvider.js +77 -0
  135. package/src/providers/tts/SupertonicProvider.js +174 -0
  136. package/src/providers/tts/index.js +140 -0
  137. package/src/shell/adapter-registry.js +154 -0
  138. package/src/shell/caller-bridge.js +35 -0
  139. package/src/shell/camera-bridge.js +28 -0
  140. package/src/shell/canvas-bridge.js +32 -0
  141. package/src/shell/commercial-bridge.js +44 -0
  142. package/src/shell/face-bridge.js +44 -0
  143. package/src/shell/music-bridge.js +60 -0
  144. package/src/shell/orchestrator.js +233 -0
  145. package/src/shell/profile-discovery.js +303 -0
  146. package/src/shell/sounds-bridge.js +28 -0
  147. package/src/shell/transcript-bridge.js +61 -0
  148. package/src/shell/waveform-bridge.js +33 -0
  149. package/src/styles/base.css +2862 -0
  150. package/src/styles/face.css +417 -0
  151. package/src/styles/pi-overrides.css +89 -0
  152. package/src/styles/theme-dark.css +67 -0
  153. package/src/test-tts.html +175 -0
  154. package/src/ui/AppShell.js +544 -0
  155. package/src/ui/ProfileSwitcher.js +228 -0
  156. package/src/ui/SessionControl.js +240 -0
  157. package/src/ui/face/FacePicker.js +195 -0
  158. package/src/ui/face/FaceRenderer.js +309 -0
  159. package/src/ui/settings/PlaylistEditor.js +366 -0
  160. package/src/ui/settings/SettingsPanel.css +684 -0
  161. package/src/ui/settings/SettingsPanel.js +419 -0
  162. package/src/ui/settings/TTSVoicePreview.js +210 -0
  163. package/src/ui/themes/ThemeManager.js +213 -0
  164. package/src/ui/visualizers/BaseVisualizer.js +29 -0
  165. package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
  166. package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
  167. package/static/emulators/jsdos/js-dos.css +1 -0
  168. package/static/emulators/jsdos/js-dos.js +22 -0
  169. package/static/favicon.svg +55 -0
  170. package/static/icons/apple-touch-icon.png +0 -0
  171. package/static/icons/favicon-32.png +0 -0
  172. package/static/icons/icon-192.png +0 -0
  173. package/static/icons/icon-512.png +0 -0
  174. package/static/install.html +449 -0
  175. package/static/manifest.json +26 -0
  176. package/static/sw.js +21 -0
  177. package/tts_providers/__init__.py +136 -0
  178. package/tts_providers/base_provider.py +319 -0
  179. package/tts_providers/groq_provider.py +155 -0
  180. package/tts_providers/hume_provider.py +226 -0
  181. package/tts_providers/providers_config.json +119 -0
  182. package/tts_providers/qwen3_provider.py +371 -0
  183. package/tts_providers/resemble_provider.py +315 -0
  184. package/tts_providers/supertonic_provider.py +557 -0
  185. package/tts_providers/supertonic_tts.py +399 -0
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ // setup-config.js — Generates all config files for OpenVoiceUI Pinokio install.
3
+ // Reads API keys from PINOKIO_* environment variables (set by install.js env: block).
4
+ // This avoids the broken json.set / pinokio-input.json flow entirely.
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const crypto = require("crypto");
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function getKey(envName) {
15
+ // install.js passes keys as PINOKIO_<KEY_NAME> via env
16
+ const v = process.env["PINOKIO_" + envName] || "";
17
+ if (!v || v === "undefined" || v === "null" || v.startsWith("{{")) return "";
18
+ return v.trim();
19
+ }
20
+
21
+ const PORT = getKey("PORT") || "5001";
22
+ const token = crypto.randomBytes(24).toString("hex");
23
+ const secret = crypto.randomBytes(32).toString("hex");
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // 1. Write openclaw.json (nested gateway/agents format for v2026.3.2+)
27
+ // ---------------------------------------------------------------------------
28
+
29
+ // Pick default model based on which keys the user provided
30
+ let defaultModel = "groq/llama-3.3-70b-versatile"; // fallback — Groq is required
31
+ if (getKey("ZAI_API_KEY")) defaultModel = "z-ai/glm-4.7";
32
+ if (getKey("ANTHROPIC_API_KEY")) defaultModel = "anthropic/claude-sonnet-4-5";
33
+ if (getKey("OPENAI_API_KEY")) defaultModel = "openai/gpt-4o";
34
+ // Z.AI wins if multiple are set (best value)
35
+ if (getKey("ZAI_API_KEY")) defaultModel = "z-ai/glm-4.7";
36
+
37
+ const openclawConfig = {
38
+ gateway: {
39
+ mode: "local",
40
+ port: 18791,
41
+ bind: "lan",
42
+ auth: { mode: "token", token: token },
43
+ trustedProxies: ["127.0.0.1", "172.16.0.0/12", "10.0.0.0/8"],
44
+ controlUi: {
45
+ allowInsecureAuth: true,
46
+ dangerouslyDisableDeviceAuth: true,
47
+ },
48
+ },
49
+ agents: {
50
+ defaults: {
51
+ model: defaultModel,
52
+ thinkingDefault: "off",
53
+ blockStreamingDefault: "on",
54
+ blockStreamingBreak: "text_end",
55
+ timeoutSeconds: 300,
56
+ },
57
+ list: [{ id: "main", default: true, workspace: "/root/.openclaw/workspace" }],
58
+ },
59
+ };
60
+
61
+ fs.mkdirSync("openclaw-data/workspace", { recursive: true });
62
+ fs.writeFileSync(
63
+ "openclaw-data/openclaw.json",
64
+ JSON.stringify(openclawConfig, null, 2) + "\n"
65
+ );
66
+ console.log(" Wrote openclaw-data/openclaw.json");
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // 2. Write .env
70
+ // ---------------------------------------------------------------------------
71
+
72
+ const envLines = [
73
+ "# OpenVoiceUI — generated by Pinokio installer",
74
+ `PORT=${PORT}`,
75
+ "DOMAIN=localhost",
76
+ `SECRET_KEY=${secret}`,
77
+ "",
78
+ "# OpenClaw Gateway",
79
+ "CLAWDBOT_GATEWAY_URL=ws://127.0.0.1:18791",
80
+ `CLAWDBOT_AUTH_TOKEN=${token}`,
81
+ "GATEWAY_SESSION_KEY=voice-main-1",
82
+ "",
83
+ "# AI Provider Keys",
84
+ ];
85
+
86
+ const envKeyList = [
87
+ "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY",
88
+ "OPENROUTER_API_KEY", "MISTRAL_API_KEY", "XAI_API_KEY", "ZAI_API_KEY",
89
+ "CEREBRAS_API_KEY", "TOGETHER_API_KEY", "HF_TOKEN",
90
+ "MOONSHOT_API_KEY", "KIMI_API_KEY", "MINIMAX_API_KEY", "QIANFAN_API_KEY",
91
+ "MODELSTUDIO_API_KEY", "XIAOMI_API_KEY", "VOLCANO_ENGINE_API_KEY",
92
+ "BYTEPLUS_API_KEY", "SYNTHETIC_API_KEY", "VENICE_API_KEY",
93
+ "OPENCODE_ZEN_API_KEY", "KILOCODE_API_KEY", "AI_GATEWAY_API_KEY",
94
+ "CLOUDFLARE_AI_GATEWAY_API_KEY", "LITELLM_API_KEY", "SUNO_API_KEY",
95
+ "RESEMBLE_API_KEY", "RESEMBLE_VOICE_UUID",
96
+ ];
97
+ for (const k of envKeyList) envLines.push(`${k}=${getKey(k)}`);
98
+
99
+ envLines.push(
100
+ "",
101
+ "# TTS",
102
+ `GROQ_API_KEY=${getKey("GROQ_API_KEY")}`,
103
+ "USE_GROQ=true",
104
+ "USE_GROQ_TTS=true",
105
+ "",
106
+ "# STT",
107
+ `DEEPGRAM_API_KEY=${getKey("DEEPGRAM_API_KEY")}`,
108
+ "",
109
+ "# Supertonic TTS",
110
+ "SUPERTONIC_API_URL=http://supertonic:8765"
111
+ );
112
+
113
+ fs.writeFileSync(".env", envLines.join("\n") + "\n");
114
+ console.log(" Wrote .env");
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // 3. Write auth-profiles.json (so OpenClaw has working providers on first start)
118
+ // ---------------------------------------------------------------------------
119
+
120
+ const providerMap = {
121
+ ANTHROPIC_API_KEY: "anthropic",
122
+ OPENAI_API_KEY: "openai",
123
+ GEMINI_API_KEY: "google",
124
+ OPENROUTER_API_KEY: "openrouter",
125
+ MISTRAL_API_KEY: "mistral",
126
+ XAI_API_KEY: "xai",
127
+ ZAI_API_KEY: "zai",
128
+ CEREBRAS_API_KEY: "cerebras",
129
+ TOGETHER_API_KEY: "together",
130
+ HF_TOKEN: "huggingface",
131
+ GROQ_API_KEY: "groq",
132
+ DEEPGRAM_API_KEY: "deepgram",
133
+ MOONSHOT_API_KEY: "moonshot",
134
+ KIMI_API_KEY: "kimi-coding",
135
+ MINIMAX_API_KEY: "minimax",
136
+ QIANFAN_API_KEY: "qianfan",
137
+ MODELSTUDIO_API_KEY: "modelstudio",
138
+ XIAOMI_API_KEY: "xiaomi",
139
+ VOLCANO_ENGINE_API_KEY: "volcano-engine",
140
+ BYTEPLUS_API_KEY: "byteplus",
141
+ SYNTHETIC_API_KEY: "synthetic",
142
+ VENICE_API_KEY: "venice",
143
+ OPENCODE_ZEN_API_KEY: "opencode",
144
+ KILOCODE_API_KEY: "kilocode",
145
+ AI_GATEWAY_API_KEY: "ai-gateway",
146
+ CLOUDFLARE_AI_GATEWAY_API_KEY: "cloudflare-ai-gateway",
147
+ LITELLM_API_KEY: "litellm",
148
+ SUNO_API_KEY: "suno",
149
+ };
150
+
151
+ const authProfiles = {};
152
+ let keyCount = 0;
153
+ for (const [envVar, providerId] of Object.entries(providerMap)) {
154
+ const key = getKey(envVar);
155
+ if (key) {
156
+ authProfiles[providerId] = [
157
+ {
158
+ id: "default",
159
+ type: "api_key",
160
+ key,
161
+ lastUsed: null,
162
+ disabled: false,
163
+ cooldown: null,
164
+ },
165
+ ];
166
+ keyCount++;
167
+ }
168
+ }
169
+
170
+ const agentDir = "openclaw-data/agents/main/agent";
171
+ fs.mkdirSync(agentDir, { recursive: true });
172
+ fs.writeFileSync(
173
+ path.join(agentDir, "auth-profiles.json"),
174
+ JSON.stringify(authProfiles, null, 2) + "\n"
175
+ );
176
+ console.log(` Wrote auth-profiles.json (${keyCount} provider(s))`);
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // 4. Pre-generate device identity and pre-pair it
180
+ // ---------------------------------------------------------------------------
181
+ // OpenClaw requires device pairing for WebSocket connections. Even with
182
+ // dangerouslyDisableDeviceAuth:true, the gateway still requires pairing for
183
+ // the WS protocol (that flag only affects the control UI).
184
+ //
185
+ // Fix: generate an Ed25519 keypair at install time, register the public key
186
+ // in OpenClaw's devices/paired.json, and save the full identity so start.js
187
+ // can inject it into the OpenVoiceUI container via docker exec.
188
+
189
+ // Generate Ed25519 keypair
190
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
191
+
192
+ // Raw 32-byte public key — extract via JWK.x (most reliable cross-platform)
193
+ const devJwk = publicKey.export({ format: "jwk" });
194
+ const rawPub = Buffer.from(devJwk.x, "base64url");
195
+
196
+ // deviceId = SHA256(raw public key) — matches Python's hashlib.sha256(raw_pub).hexdigest()
197
+ const deviceId = crypto.createHash("sha256").update(rawPub).digest("hex");
198
+
199
+ // PEM formats — OpenVoiceUI Python client uses PEM for signing
200
+ const pubPem = publicKey.export({ type: "spki", format: "pem" });
201
+ const privPem = privateKey.export({ type: "pkcs8", format: "pem" });
202
+
203
+ // base64url of raw Ed25519 bytes — this is what the gateway compares during WS handshake
204
+ const pubB64url = rawPub.toString("base64url");
205
+
206
+ // Save full identity for injection into OpenVoiceUI container at start time
207
+ // (Python client reads PEM format for Ed25519 signing)
208
+ const deviceIdentity = {
209
+ deviceId: deviceId,
210
+ publicKeyPem: pubPem,
211
+ privateKeyPem: privPem,
212
+ };
213
+ fs.writeFileSync(
214
+ "openclaw-data/pre-paired-device.json",
215
+ JSON.stringify(deviceIdentity, null, 2) + "\n"
216
+ );
217
+ console.log(` Generated device identity: ${deviceId.slice(0, 16)}...`);
218
+
219
+ // Pre-register in OpenClaw's devices/paired.json so it accepts this device.
220
+ // Format must match what approveDevicePairing() writes:
221
+ // - publicKey: base64url of raw Ed25519 bytes (NOT PEM — gateway compares literally)
222
+ // - role/roles/scopes/approvedScopes: authorization metadata
223
+ // - tokens: per-role auth tokens for verifyDeviceToken() calls
224
+ fs.mkdirSync("openclaw-data/devices", { recursive: true });
225
+ const nowMs = Date.now();
226
+ const pairingToken = crypto.randomBytes(32).toString("hex");
227
+ const pairedDevices = {};
228
+ pairedDevices[deviceId] = {
229
+ deviceId: deviceId,
230
+ publicKey: pubB64url,
231
+ displayName: "pinokio-openvoiceui",
232
+ platform: "linux",
233
+ clientId: "cli",
234
+ clientMode: "cli",
235
+ role: "operator",
236
+ roles: ["operator"],
237
+ scopes: ["operator.read", "operator.write"],
238
+ approvedScopes: ["operator.read", "operator.write"],
239
+ tokens: {
240
+ operator: {
241
+ token: pairingToken,
242
+ role: "operator",
243
+ scopes: ["operator.read", "operator.write"],
244
+ createdAtMs: nowMs,
245
+ },
246
+ },
247
+ createdAtMs: nowMs,
248
+ approvedAtMs: nowMs,
249
+ };
250
+ fs.writeFileSync(
251
+ "openclaw-data/devices/paired.json",
252
+ JSON.stringify(pairedDevices, null, 2) + "\n"
253
+ );
254
+ // Clear pending.json — stale entries with silent:false permanently block auto-approval
255
+ fs.writeFileSync("openclaw-data/devices/pending.json", "{}\n");
256
+ console.log(" Wrote devices/paired.json (pre-paired)");
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Done
260
+ // ---------------------------------------------------------------------------
261
+
262
+ console.log("\n Configuration complete!\n");
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,264 @@
1
+ /**
2
+ * ClawdBotAdapter — Multi-Agent Framework adapter for ClawdBot / OpenClaw (P6-T2)
3
+ *
4
+ * Wraps the existing VoiceSession (P3-T7) and exposes it through the EventBridge
5
+ * contract so the app shell can treat ClawdBot as a swappable agent adapter.
6
+ *
7
+ * Internally:
8
+ * - VoiceSession handles STT (Web Speech API), HTTP streaming to /api/conversation,
9
+ * TTS playback (TTSPlayer), and canvas/music command parsing.
10
+ * - This adapter translates between EventBus events (VoiceSession's internal bus)
11
+ * and EventBridge AgentEvents (the shell's canonical event vocabulary).
12
+ *
13
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md — "ClawdBot Adapter" section
14
+ *
15
+ * Adapter contract:
16
+ * init(bridge, config) — called when this mode is selected
17
+ * start() — called when user clicks the call button
18
+ * stop() — called when user clicks stop
19
+ * destroy() — called when switching to a different adapter
20
+ *
21
+ * Config shape:
22
+ * {
23
+ * serverUrl: string, // e.g. 'http://localhost:5001'
24
+ * sessionKey: string, // e.g. 'voice-main-3' (passed to VoiceSession)
25
+ * musicPlayer: object, // optional — MusicPlayer instance from shell
26
+ * }
27
+ */
28
+
29
+ import { AgentEvents, AgentActions } from '../core/EventBridge.js';
30
+ import { eventBus } from '../core/EventBus.js';
31
+ import { VoiceSession } from '../core/VoiceSession.js';
32
+
33
+ const ClawdBotAdapter = {
34
+ // ── Identity & capabilities ───────────────────────────────────────────────
35
+
36
+ name: 'ClawdBot (OpenClaw)',
37
+
38
+ /**
39
+ * Feature flags: app shell shows/hides UI elements based on this array.
40
+ * Ref: doc 17 — "capability-driven UI shell"
41
+ */
42
+ capabilities: [
43
+ 'canvas', // [CANVAS:...] commands parsed and emitted
44
+ 'vps_control', // agent can run server-side tools
45
+ 'file_ops', // agent can read/write files on VPS
46
+ 'code_execution', // agent can execute code on VPS
47
+ 'dj_soundboard', // future: soundboard integration
48
+ 'music_sync', // [MUSIC_PLAY/STOP/NEXT] commands parsed and emitted
49
+ 'camera', // webcam + Gemini vision + face recognition
50
+ ],
51
+
52
+ // ── Private state ─────────────────────────────────────────────────────────
53
+
54
+ _bridge: null, // EventBridge singleton
55
+ _session: null, // VoiceSession instance
56
+ _config: null, // adapter config passed to init()
57
+ _eventUnsubs: [], // eventBus.on() cleanup functions
58
+ _bridgeUnsubs: [], // bridge.on() cleanup functions (belt-and-suspenders)
59
+
60
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Initialize the adapter.
64
+ * Called by AgentOrchestrator when this mode is selected.
65
+ *
66
+ * @param {import('../core/EventBridge.js').EventBridge} bridge
67
+ * @param {object} config
68
+ */
69
+ async init(bridge, config) {
70
+ this._bridge = bridge;
71
+ this._config = config;
72
+
73
+ // Create VoiceSession — the existing ClawdBot conversation engine
74
+ this._session = new VoiceSession({
75
+ serverUrl: config.serverUrl || '',
76
+ musicPlayer: config.musicPlayer || null,
77
+ });
78
+
79
+ // Translate VoiceSession's internal EventBus events → EventBridge AgentEvents
80
+ this._wireSessionEvents();
81
+
82
+ // Listen for UI→Agent actions emitted on the bridge
83
+ this._bridgeUnsubs.push(
84
+ bridge.on(AgentActions.SEND_MESSAGE, (d) => this._session.sendMessage(d.text))
85
+ );
86
+ this._bridgeUnsubs.push(
87
+ bridge.on(AgentActions.FORCE_MESSAGE, (d) => this._session.sendMessage(d.text))
88
+ );
89
+ this._bridgeUnsubs.push(
90
+ bridge.on(AgentActions.END_SESSION, () => this.stop())
91
+ );
92
+ // CONTEXT_UPDATE: inject background context as a silent system message
93
+ this._bridgeUnsubs.push(
94
+ bridge.on(AgentActions.CONTEXT_UPDATE, (d) => this._session.sendMessage(`[CONTEXT: ${d.text}]`))
95
+ );
96
+ },
97
+
98
+ /**
99
+ * Start the conversation.
100
+ * Called when the user clicks the call button.
101
+ */
102
+ async start() {
103
+ if (!this._session) return;
104
+ // VoiceSession.start() emits session:start on success, which we forward as CONNECTED
105
+ await this._session.start();
106
+ },
107
+
108
+ /**
109
+ * Stop the conversation.
110
+ * Called when the user clicks stop.
111
+ */
112
+ async stop() {
113
+ if (!this._session) return;
114
+ this._session.stop();
115
+ // session:stop handler emits DISCONNECTED via the wired event
116
+ },
117
+
118
+ /**
119
+ * Full teardown — called when switching to a different adapter.
120
+ * MUST release all resources.
121
+ */
122
+ async destroy() {
123
+ // Stop conversation first
124
+ this._session?.stop();
125
+
126
+ // Destroy VoiceSession's AudioContext
127
+ this._session?.destroy();
128
+ this._session = null;
129
+
130
+ // Clean up EventBus subscriptions (prevents stale handlers from a dead session)
131
+ this._eventUnsubs.forEach(fn => fn());
132
+ this._eventUnsubs = [];
133
+
134
+ // Bridge unsubs: AgentOrchestrator calls bridge.clearAll() anyway,
135
+ // but we clean up explicitly for correctness.
136
+ this._bridgeUnsubs.forEach(fn => fn());
137
+ this._bridgeUnsubs = [];
138
+
139
+ this._bridge = null;
140
+ this._config = null;
141
+ },
142
+
143
+ // ── Private: VoiceSession → EventBridge wiring ───────────────────────────
144
+
145
+ /**
146
+ * Subscribe to all EventBus events emitted by VoiceSession and translate
147
+ * them into EventBridge AgentEvents for the app shell.
148
+ */
149
+ _wireSessionEvents() {
150
+ const b = this._bridge;
151
+ const push = (unsub) => this._eventUnsubs.push(unsub);
152
+
153
+ // ── Connection lifecycle ──────────────────────────────────────────────
154
+
155
+ push(eventBus.on('session:start', () => {
156
+ b.emit(AgentEvents.CONNECTED);
157
+ b.emit(AgentEvents.MOOD, { mood: 'happy' });
158
+ }));
159
+
160
+ push(eventBus.on('session:stop', () => {
161
+ b.emit(AgentEvents.DISCONNECTED);
162
+ b.emit(AgentEvents.MOOD, { mood: 'neutral' });
163
+ }));
164
+
165
+ push(eventBus.on('session:reset', (d) => {
166
+ // Server-side session was reset (e.g. context overflow)
167
+ console.info('[ClawdBotAdapter] Session reset:', d.old, '→', d.new);
168
+ }));
169
+
170
+ // ── Conversation state ────────────────────────────────────────────────
171
+
172
+ push(eventBus.on('session:thinking', () => {
173
+ b.emit(AgentEvents.STATE_CHANGED, { state: 'thinking' });
174
+ b.emit(AgentEvents.MOOD, { mood: 'thinking' });
175
+ }));
176
+
177
+ push(eventBus.on('session:listening', () => {
178
+ b.emit(AgentEvents.STATE_CHANGED, { state: 'listening' });
179
+ }));
180
+
181
+ // ── Content ───────────────────────────────────────────────────────────
182
+
183
+ // Final messages (user speech → text, assistant response)
184
+ push(eventBus.on('session:message', (d) => {
185
+ b.emit(AgentEvents.MESSAGE, {
186
+ role: d.role,
187
+ text: d.text,
188
+ final: true,
189
+ });
190
+ }));
191
+
192
+ // Streaming text deltas (assistant thinking → partial transcript display)
193
+ push(eventBus.on('session:streaming', (d) => {
194
+ b.emit(AgentEvents.TRANSCRIPT, { text: d.text, partial: true });
195
+ }));
196
+
197
+ // ── Audio / TTS ───────────────────────────────────────────────────────
198
+
199
+ push(eventBus.on('tts:start', () => {
200
+ b.emit(AgentEvents.TTS_PLAYING);
201
+ b.emit(AgentEvents.STATE_CHANGED, { state: 'speaking' });
202
+ }));
203
+
204
+ push(eventBus.on('tts:stop', () => {
205
+ b.emit(AgentEvents.TTS_STOPPED);
206
+ b.emit(AgentEvents.STATE_CHANGED, { state: 'listening' });
207
+ }));
208
+
209
+ // ── Errors ────────────────────────────────────────────────────────────
210
+
211
+ push(eventBus.on('session:error', (d) => {
212
+ b.emit(AgentEvents.ERROR, { message: d.message });
213
+ b.emit(AgentEvents.MOOD, { mood: 'sad' });
214
+ }));
215
+
216
+ // ── Tool calls ────────────────────────────────────────────────────────
217
+
218
+ push(eventBus.on('session:tool', (d) => {
219
+ b.emit(AgentEvents.TOOL_CALLED, { name: d.name, params: {}, result: null });
220
+ }));
221
+
222
+ // ── Emotion / mood ────────────────────────────────────────────────────
223
+
224
+ // Server-provided emotion state (ADR-004: mood + intensity + directives)
225
+ push(eventBus.on('session:emotion', (d) => {
226
+ if (d.mood) b.emit(AgentEvents.MOOD, { mood: d.mood });
227
+ }));
228
+
229
+ // ── Canvas commands ───────────────────────────────────────────────────
230
+
231
+ // [CANVAS_MENU] → open canvas page picker
232
+ push(eventBus.on('cmd:canvas_menu', () => {
233
+ b.emit(AgentEvents.CANVAS_CMD, { action: 'menu' });
234
+ }));
235
+
236
+ // [CANVAS:pagename] → present a specific canvas page
237
+ push(eventBus.on('cmd:canvas_page', (d) => {
238
+ b.emit(AgentEvents.CANVAS_CMD, { action: 'present', url: d.page });
239
+ }));
240
+
241
+ // ── Music commands ────────────────────────────────────────────────────
242
+
243
+ // [MUSIC_PLAY] or [MUSIC_PLAY:trackname]
244
+ push(eventBus.on('cmd:music_play', (d) => {
245
+ b.emit(AgentEvents.MUSIC_PLAY, {
246
+ action: 'play',
247
+ track: d.track || null,
248
+ });
249
+ }));
250
+
251
+ // [MUSIC_STOP]
252
+ push(eventBus.on('cmd:music_stop', () => {
253
+ b.emit(AgentEvents.MUSIC_PLAY, { action: 'stop' });
254
+ }));
255
+
256
+ // [MUSIC_NEXT]
257
+ push(eventBus.on('cmd:music_next', () => {
258
+ b.emit(AgentEvents.MUSIC_PLAY, { action: 'skip' });
259
+ }));
260
+ },
261
+ };
262
+
263
+ export default ClawdBotAdapter;
264
+ export { ClawdBotAdapter };