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,60 @@
1
+ /**
2
+ * music-bridge.js — Connect MusicModule to EventBridge (P6-T4)
3
+ *
4
+ * Handles music play/pause/stop/skip, volume ducking during speech,
5
+ * and DJ track-ending notifications back to the agent.
6
+ *
7
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
8
+ */
9
+
10
+ import { AgentEvents, AgentActions } from '../core/EventBridge.js';
11
+
12
+ /**
13
+ * Wire MusicModule to the EventBridge.
14
+ * @param {import('../core/EventBridge.js').EventBridge} bridge
15
+ * @returns {Function[]} unsubscribe functions
16
+ */
17
+ export function connectMusic(bridge) {
18
+ const music = () => window.musicPlayer;
19
+
20
+ const unsubs = [
21
+ // Agent-driven music control
22
+ bridge.on(AgentEvents.MUSIC_PLAY, ({ action, track }) => {
23
+ if (!music()) return;
24
+ if (action === 'stop') music().stop?.();
25
+ else if (action === 'pause') music().pause?.();
26
+ else if (action === 'play') music().play?.(track);
27
+ else if (action === 'skip') music().next?.();
28
+ }),
29
+
30
+ // Agent triggers server-side music sync
31
+ bridge.on(AgentEvents.MUSIC_SYNC, () => {
32
+ window.syncMusicWithServer?.();
33
+ }),
34
+
35
+ // Volume ducking when agent speaks
36
+ bridge.on(AgentEvents.STATE_CHANGED, ({ state }) => {
37
+ music()?.duck?.(state === 'speaking');
38
+ }),
39
+ ];
40
+
41
+ // DJ track-ending notifications (tell agent when track is ending)
42
+ if (music()) {
43
+ music().onTrackEnding = (trackName, secondsLeft) => {
44
+ bridge.emit(AgentActions.CONTEXT_UPDATE, {
45
+ text: `[DJ INFO: "${trackName}" has ${secondsLeft}s left]`,
46
+ });
47
+ bridge.emit(AgentActions.FORCE_MESSAGE, {
48
+ text: `[SYSTEM: Song ending! Announce next track and call play_music action=skip!]`,
49
+ });
50
+ };
51
+
52
+ music().onTrackEnded = (trackName) => {
53
+ bridge.emit(AgentActions.FORCE_MESSAGE, {
54
+ text: `[SYSTEM: "${trackName}" ended! Call play_music action=skip NOW!]`,
55
+ });
56
+ };
57
+ }
58
+
59
+ return unsubs;
60
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * orchestrator.js — AgentOrchestrator: capability-driven mode switching (P6-T4)
3
+ *
4
+ * The AgentOrchestrator manages registered agent adapters and handles
5
+ * mode switching with full cleanup + capability-driven UI updates.
6
+ *
7
+ * Key responsibilities:
8
+ * - Register adapters with their configs
9
+ * - Switch modes: destroy old adapter, clear bridge, init new adapter
10
+ * - Connect shell bridge modules based on adapter capabilities
11
+ * - Show/hide UI elements based on what the active adapter supports
12
+ * - Persist selected mode in localStorage
13
+ *
14
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (Mode Switching section)
15
+ *
16
+ * Usage:
17
+ * import { orchestrator } from './shell/orchestrator.js';
18
+ * import { MyAdapter } from './adapters/my-adapter.js';
19
+ *
20
+ * orchestrator.register('my-adapter', MyAdapter, { serverUrl: '...' });
21
+ * await orchestrator.switchMode('my-adapter');
22
+ * await orchestrator.startConversation();
23
+ */
24
+
25
+ import { bridge } from '../core/EventBridge.js';
26
+ import { connectFace } from './face-bridge.js';
27
+ import { connectMusic } from './music-bridge.js';
28
+ import { connectSounds } from './sounds-bridge.js';
29
+ import { connectCallerEffect } from './caller-bridge.js';
30
+ import { connectCanvas } from './canvas-bridge.js';
31
+ import { connectTranscript, connectActionConsole } from './transcript-bridge.js';
32
+ import { connectWaveform } from './waveform-bridge.js';
33
+ import { connectCommercial } from './commercial-bridge.js';
34
+ import { connectCamera } from './camera-bridge.js';
35
+
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+ // Capability → UI element map
38
+ // Maps capability string → array of DOM element IDs to show when present.
39
+ // Elements NOT in any capability's list are hidden by default.
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ const CAPABILITY_UI_MAP = {
43
+ canvas: ['canvas-button'],
44
+ music: ['music-button'],
45
+ wake_word: ['wake-button'],
46
+ camera: ['camera-button'],
47
+ caller_effects: ['caller-effect-toggle'],
48
+ dj_soundboard: [], // no dedicated button, soundboard is always-present
49
+ face_panel: ['face-button'],
50
+ };
51
+
52
+ // All capability-controlled element IDs (used to hide everything first)
53
+ const ALL_CAPABILITY_ELEMENTS = [
54
+ ...new Set(Object.values(CAPABILITY_UI_MAP).flat()),
55
+ ];
56
+
57
+ // ─────────────────────────────────────────────────────────────────────────────
58
+ // AgentOrchestrator
59
+ // ─────────────────────────────────────────────────────────────────────────────
60
+
61
+ class AgentOrchestrator {
62
+ constructor() {
63
+ /** @type {Object.<string, {adapter: object, config: object}>} */
64
+ this._adapters = {};
65
+
66
+ /** @type {object|null} */
67
+ this._activeAdapter = null;
68
+
69
+ /** @type {string|null} */
70
+ this._activeMode = null;
71
+
72
+ /** @type {Function[]} shell bridge unsubscribe functions */
73
+ this._shellConnections = [];
74
+ }
75
+
76
+ /**
77
+ * Register an adapter with its default config.
78
+ * Must be called before switchMode() can select this adapter.
79
+ *
80
+ * @param {string} mode - Unique mode key (e.g. 'clawdbot', 'elevenlabs-classic')
81
+ * @param {object} adapter - Adapter object with init/start/stop/destroy methods
82
+ * @param {object} config - Adapter-specific configuration
83
+ */
84
+ register(mode, adapter, config = {}) {
85
+ this._adapters[mode] = { adapter, config };
86
+ console.log(`[Orchestrator] Registered adapter: ${mode}`);
87
+ }
88
+
89
+ /**
90
+ * Switch the active agent adapter.
91
+ * Performs full teardown of the current adapter, clears all bridge
92
+ * handlers, initialises the new adapter, reconnects shell bridges,
93
+ * and updates UI visibility based on the new adapter's capabilities.
94
+ *
95
+ * @param {string} newMode - Mode key to switch to
96
+ * @param {object} [configOverride] - Optional config overrides for this switch
97
+ */
98
+ async switchMode(newMode, configOverride = {}) {
99
+ const entry = this._adapters[newMode];
100
+ if (!entry) {
101
+ console.error(`[Orchestrator] Unknown mode: ${newMode}`);
102
+ throw new Error(`Unknown agent mode: ${newMode}`);
103
+ }
104
+
105
+ // 1. Tear down current adapter completely
106
+ if (this._activeAdapter) {
107
+ console.log(`[Orchestrator] Destroying ${this._activeMode}`);
108
+ try {
109
+ await this._activeAdapter.destroy();
110
+ } catch (e) {
111
+ console.warn('[Orchestrator] Error during adapter destroy:', e);
112
+ }
113
+ }
114
+
115
+ // 2. Disconnect all shell bridge subscriptions (prevent stale handlers)
116
+ this._shellConnections.forEach(unsub => {
117
+ try { unsub(); } catch (e) { /* ignore */ }
118
+ });
119
+ this._shellConnections = [];
120
+
121
+ // 3. Clear ALL bridge handlers (nuclear clear — guarantees no leaks)
122
+ bridge.clearAll();
123
+
124
+ // 4. Initialise new adapter
125
+ console.log(`[Orchestrator] Initialising ${newMode}`);
126
+ this._activeAdapter = entry.adapter;
127
+ this._activeMode = newMode;
128
+
129
+ const config = { ...entry.config, ...configOverride };
130
+ await entry.adapter.init(bridge, config);
131
+
132
+ // 5. Reconnect shell modules (always-on bridges)
133
+ const caps = entry.adapter.capabilities || [];
134
+
135
+ this._shellConnections.push(
136
+ ...connectFace(bridge),
137
+ ...connectWaveform(bridge),
138
+ ...connectTranscript(bridge),
139
+ ...connectActionConsole(bridge),
140
+ ...connectMusic(bridge),
141
+ ...connectSounds(bridge),
142
+ );
143
+
144
+ // 6. Conditional bridges — only connect when adapter has the capability
145
+ this._shellConnections.push(
146
+ ...connectCallerEffect(bridge, caps),
147
+ ...connectCanvas(bridge, caps),
148
+ ...connectCommercial(bridge, caps),
149
+ ...connectCamera(bridge, caps),
150
+ );
151
+
152
+ // 7. Update UI visibility based on capabilities
153
+ this._updateFeatureUI(caps);
154
+
155
+ // 8. Persist selection
156
+ try { localStorage.setItem('agent_mode', newMode); } catch (e) { /* ignore */ }
157
+
158
+ console.log(`[Orchestrator] Mode switched to: ${newMode} (caps: [${caps.join(', ')}])`);
159
+ }
160
+
161
+ /**
162
+ * Start a conversation with the active adapter.
163
+ */
164
+ async startConversation() {
165
+ if (!this._activeAdapter) {
166
+ console.warn('[Orchestrator] No active adapter — call switchMode() first');
167
+ return;
168
+ }
169
+ await this._activeAdapter.start();
170
+ }
171
+
172
+ /**
173
+ * Stop the active conversation (but keep adapter alive).
174
+ */
175
+ async stopConversation() {
176
+ if (this._activeAdapter) {
177
+ await this._activeAdapter.stop();
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Return the active adapter's capability list.
183
+ * @returns {string[]}
184
+ */
185
+ get capabilities() {
186
+ return this._activeAdapter?.capabilities ?? [];
187
+ }
188
+
189
+ /**
190
+ * Return the active mode key.
191
+ * @returns {string|null}
192
+ */
193
+ get activeMode() {
194
+ return this._activeMode;
195
+ }
196
+
197
+ /**
198
+ * Show/hide UI elements based on what the active adapter supports.
199
+ * Capability-controlled elements are hidden first, then shown for
200
+ * each capability the adapter declares.
201
+ *
202
+ * @param {string[]} capabilities - Capabilities of the active adapter
203
+ * @private
204
+ */
205
+ _updateFeatureUI(capabilities) {
206
+ // Hide all capability-controlled elements first
207
+ ALL_CAPABILITY_ELEMENTS.forEach(id => {
208
+ const el = document.getElementById(id);
209
+ if (el) el.style.display = 'none';
210
+ });
211
+
212
+ // Show elements for each declared capability
213
+ capabilities.forEach(cap => {
214
+ const ids = CAPABILITY_UI_MAP[cap] ?? [];
215
+ ids.forEach(id => {
216
+ const el = document.getElementById(id);
217
+ if (el) el.style.display = '';
218
+ });
219
+ });
220
+
221
+ // Emit capability list for any shell components that inspect it
222
+ console.log(`[Orchestrator] UI updated for capabilities: [${capabilities.join(', ')}]`);
223
+ }
224
+ }
225
+
226
+ // ─────────────────────────────────────────────────────────────────────────────
227
+ // Singleton — one orchestrator for the whole app
228
+ // ─────────────────────────────────────────────────────────────────────────────
229
+
230
+ export const orchestrator = new AgentOrchestrator();
231
+
232
+ // Also export class for testing
233
+ export { AgentOrchestrator };
@@ -0,0 +1,303 @@
1
+ /**
2
+ * profile-discovery.js — Adapter auto-discovery from profiles (P6-T5)
3
+ *
4
+ * ProfileDiscovery fetches agent profiles from the server, resolves which
5
+ * adapter each profile requires, registers every profile as an orchestrator
6
+ * mode, and activates the initial mode (from localStorage or server default).
7
+ *
8
+ * This implements the "plug-and-play" promise from the multi-agent framework:
9
+ * Adding a new agent = 1 adapter file + 1 profile JSON entry.
10
+ * ProfileDiscovery does the rest automatically.
11
+ *
12
+ * Integration:
13
+ * 1. Call `await profileDiscovery.init({ serverUrl })` at app startup.
14
+ * 2. All profiles → orchestrator modes are registered automatically.
15
+ * 3. ProfileSwitcher's 'profile:switched' event triggers adapter hot-swap.
16
+ *
17
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md
18
+ * "Adding a New Agent System (The Plug-and-Play Promise)"
19
+ *
20
+ * Usage:
21
+ * import { profileDiscovery } from './profile-discovery.js';
22
+ * await profileDiscovery.init({ serverUrl: 'http://localhost:5001' });
23
+ * // Orchestrator is now registered with all adapters — call switchMode, start, etc.
24
+ */
25
+
26
+ import { orchestrator } from './orchestrator.js';
27
+ import { adapterRegistry } from './adapter-registry.js';
28
+ import { eventBus } from '../core/EventBus.js';
29
+
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+ // ProfileDiscovery
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+
34
+ class ProfileDiscovery {
35
+ constructor() {
36
+ /** @type {string} */
37
+ this._serverUrl = '';
38
+
39
+ /** @type {object[]} — Raw profile objects from /api/profiles */
40
+ this._profiles = [];
41
+
42
+ /** @type {string|null} — Active profile ID reported by the server */
43
+ this._serverActiveId = null;
44
+
45
+ /** @type {boolean} */
46
+ this._initialized = false;
47
+ }
48
+
49
+ // ── Public API ────────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Fetch profiles, load adapter modules, register each profile as an
53
+ * orchestrator mode, and activate the initial mode.
54
+ *
55
+ * Errors in individual adapter registrations are non-fatal: other adapters
56
+ * are still registered. A total failure falls back to the clawdbot adapter.
57
+ *
58
+ * @param {object} options
59
+ * @param {string} options.serverUrl - Base URL of the backend
60
+ * @param {string} [options.fallbackMode] - Adapter ID used if everything fails
61
+ */
62
+ async init({ serverUrl = '', fallbackMode = 'clawdbot' } = {}) {
63
+ this._serverUrl = serverUrl;
64
+
65
+ try {
66
+ await this._fetchProfiles();
67
+ await this._registerAllAdapters();
68
+ await this._activateInitialMode(fallbackMode);
69
+ } catch (err) {
70
+ console.error('[ProfileDiscovery] Init failed:', err);
71
+ // Best-effort fallback: register and activate clawdbot directly
72
+ try {
73
+ await this._registerFallback(fallbackMode);
74
+ await orchestrator.switchMode(fallbackMode);
75
+ } catch (e) {
76
+ console.error('[ProfileDiscovery] Fallback registration also failed:', e);
77
+ }
78
+ }
79
+
80
+ this._initialized = true;
81
+
82
+ // Listen for profile-switch events emitted by ProfileSwitcher UI
83
+ eventBus.on('profile:switched', (d) => this._onProfileSwitched(d.profile));
84
+
85
+ console.log('[ProfileDiscovery] Initialised. Known modes:', Object.keys(orchestrator._adapters));
86
+ }
87
+
88
+ /**
89
+ * Return the raw profile list fetched from the server.
90
+ * @returns {object[]}
91
+ */
92
+ get profiles() {
93
+ return this._profiles;
94
+ }
95
+
96
+ /**
97
+ * Return the currently active profile ID.
98
+ * Reads from the orchestrator's active mode (source of truth at runtime).
99
+ * @returns {string|null}
100
+ */
101
+ get activeId() {
102
+ return orchestrator.activeMode;
103
+ }
104
+
105
+ /**
106
+ * Return true after init() has completed (successfully or via fallback).
107
+ * @returns {boolean}
108
+ */
109
+ get initialized() {
110
+ return this._initialized;
111
+ }
112
+
113
+ // ── Private: bootstrap ────────────────────────────────────────────────────
114
+
115
+ /**
116
+ * Fetch /api/profiles and store the result.
117
+ * @private
118
+ */
119
+ async _fetchProfiles() {
120
+ const resp = await fetch(`${this._serverUrl}/api/profiles`);
121
+ if (!resp.ok) {
122
+ throw new Error(`/api/profiles returned HTTP ${resp.status}`);
123
+ }
124
+ const data = await resp.json();
125
+ this._profiles = data.profiles || [];
126
+ this._serverActiveId = data.active || null;
127
+
128
+ console.log(
129
+ `[ProfileDiscovery] Fetched ${this._profiles.length} profile(s),`,
130
+ `server active: "${this._serverActiveId}"`
131
+ );
132
+ }
133
+
134
+ /**
135
+ * For each profile, dynamically import its adapter and register the
136
+ * profile as an orchestrator mode.
137
+ *
138
+ * Profile → orchestrator mode mapping:
139
+ * - mode key = profile.id (e.g. 'default', 'hume-evi')
140
+ * - adapter = profile.adapter (e.g. 'clawdbot', 'hume-evi')
141
+ * - config = profile.adapter_config (adapter-specific params from JSON)
142
+ * plus serverUrl and profileId injected automatically.
143
+ *
144
+ * Adapter modules are cached by AdapterRegistry, so multiple profiles
145
+ * sharing the same adapter (e.g. three ClawdBot profiles) only trigger
146
+ * one dynamic import.
147
+ *
148
+ * @private
149
+ */
150
+ async _registerAllAdapters() {
151
+ for (const profile of this._profiles) {
152
+ const adapterId = profile.adapter || adapterRegistry.defaultAdapter;
153
+ const modeKey = profile.id;
154
+ const config = {
155
+ ...(profile.adapter_config || {}),
156
+ // Always inject serverUrl so adapters don't need to hardcode it
157
+ serverUrl: (profile.adapter_config?.serverUrl) || this._serverUrl,
158
+ // profileId lets adapters read the full profile server-side if needed
159
+ profileId: profile.id,
160
+ };
161
+
162
+ try {
163
+ const adapter = await adapterRegistry.load(adapterId);
164
+ orchestrator.register(modeKey, adapter, config);
165
+ console.log(
166
+ `[ProfileDiscovery] Registered mode "${modeKey}"`,
167
+ `→ adapter "${adapterId}"`
168
+ );
169
+ } catch (err) {
170
+ console.warn(
171
+ `[ProfileDiscovery] Could not register mode "${modeKey}"`,
172
+ `(adapter "${adapterId}"):`, err.message
173
+ );
174
+ // Continue with remaining profiles — non-fatal
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Decide which mode to activate at startup.
181
+ *
182
+ * Priority:
183
+ * 1. localStorage 'agent_mode' (user's last manual selection)
184
+ * 2. Server-reported active profile
185
+ * 3. fallbackMode parameter
186
+ *
187
+ * @param {string} fallbackMode
188
+ * @private
189
+ */
190
+ async _activateInitialMode(fallbackMode) {
191
+ const saved = this._readSavedMode();
192
+ const target = saved || this._serverActiveId || fallbackMode;
193
+
194
+ // Make sure target mode is registered (saved mode might be stale)
195
+ const registered = Object.keys(orchestrator._adapters);
196
+ const resolvedTarget = registered.includes(target)
197
+ ? target
198
+ : (this._serverActiveId || fallbackMode);
199
+
200
+ if (resolvedTarget !== target) {
201
+ console.warn(
202
+ `[ProfileDiscovery] Saved mode "${target}" not registered;`,
203
+ `falling back to "${resolvedTarget}"`
204
+ );
205
+ }
206
+
207
+ try {
208
+ await orchestrator.switchMode(resolvedTarget);
209
+ } catch (err) {
210
+ console.error(
211
+ `[ProfileDiscovery] Could not activate mode "${resolvedTarget}":`, err
212
+ );
213
+ // Last resort: try the raw fallback
214
+ if (resolvedTarget !== fallbackMode && registered.includes(fallbackMode)) {
215
+ await orchestrator.switchMode(fallbackMode);
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Register the fallback adapter without needing a profile entry.
222
+ * Used when _fetchProfiles fails entirely.
223
+ * @param {string} adapterId
224
+ * @private
225
+ */
226
+ async _registerFallback(adapterId) {
227
+ const adapter = await adapterRegistry.load(adapterId);
228
+ orchestrator.register(adapterId, adapter, { serverUrl: this._serverUrl });
229
+ console.log(`[ProfileDiscovery] Registered fallback mode "${adapterId}"`);
230
+ }
231
+
232
+ /**
233
+ * Read the saved agent mode from localStorage, returning null on error.
234
+ * @returns {string|null}
235
+ * @private
236
+ */
237
+ _readSavedMode() {
238
+ try {
239
+ return localStorage.getItem('agent_mode') || null;
240
+ } catch (_) {
241
+ return null;
242
+ }
243
+ }
244
+
245
+ // ── Private: runtime profile switching ───────────────────────────────────
246
+
247
+ /**
248
+ * Called when the ProfileSwitcher UI emits 'profile:switched'.
249
+ * Switches the orchestrator to the new profile's adapter mode.
250
+ *
251
+ * If the new profile was not in the initial fetch (e.g. dynamically
252
+ * created), it is registered on-the-fly before switching.
253
+ *
254
+ * @param {object} profile - The newly-activated profile object from the server
255
+ * @private
256
+ */
257
+ async _onProfileSwitched(profile) {
258
+ if (!profile?.id) return;
259
+
260
+ const modeKey = profile.id;
261
+ const adapterId = profile.adapter || adapterRegistry.defaultAdapter;
262
+
263
+ console.log(
264
+ `[ProfileDiscovery] 'profile:switched' → mode "${modeKey}"`,
265
+ `adapter "${adapterId}"`
266
+ );
267
+
268
+ // Register on-the-fly if not already known (handles dynamically-created profiles)
269
+ if (!orchestrator._adapters[modeKey]) {
270
+ try {
271
+ const adapter = await adapterRegistry.load(adapterId);
272
+ orchestrator.register(modeKey, adapter, {
273
+ ...(profile.adapter_config || {}),
274
+ serverUrl: this._serverUrl,
275
+ profileId: profile.id,
276
+ });
277
+ console.log(`[ProfileDiscovery] On-the-fly registered mode "${modeKey}"`);
278
+ } catch (err) {
279
+ console.error(
280
+ `[ProfileDiscovery] Failed to register mode "${modeKey}" on-the-fly:`, err
281
+ );
282
+ return;
283
+ }
284
+ }
285
+
286
+ try {
287
+ await orchestrator.switchMode(modeKey);
288
+ } catch (err) {
289
+ console.error(
290
+ `[ProfileDiscovery] Failed to switch orchestrator to "${modeKey}":`, err
291
+ );
292
+ }
293
+ }
294
+ }
295
+
296
+ // ─────────────────────────────────────────────────────────────────────────────
297
+ // Singleton
298
+ // ─────────────────────────────────────────────────────────────────────────────
299
+
300
+ export const profileDiscovery = new ProfileDiscovery();
301
+
302
+ // Also export class for testing
303
+ export { ProfileDiscovery };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * sounds-bridge.js — Connect DJ/caller sounds to EventBridge (P6-T4)
3
+ *
4
+ * Routes PLAY_SOUND events to the DJ soundboard or caller sound player.
5
+ *
6
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
7
+ */
8
+
9
+ import { AgentEvents } from '../core/EventBridge.js';
10
+
11
+ /**
12
+ * Wire sound effects to the EventBridge.
13
+ * @param {import('../core/EventBridge.js').EventBridge} bridge
14
+ * @returns {Function[]} unsubscribe functions
15
+ */
16
+ export function connectSounds(bridge) {
17
+ const unsubs = [
18
+ bridge.on(AgentEvents.PLAY_SOUND, ({ sound, type }) => {
19
+ if (type === 'dj') {
20
+ window.DJSoundboard?.play(sound);
21
+ } else if (type === 'caller') {
22
+ window.playCallerSound?.(sound);
23
+ }
24
+ }),
25
+ ];
26
+
27
+ return unsubs;
28
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * transcript-bridge.js — Connect TranscriptPanel and ActionConsole to EventBridge (P6-T4)
3
+ *
4
+ * Routes message/transcript events to the transcript panel and
5
+ * tool/lifecycle events to the action console.
6
+ *
7
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
8
+ */
9
+
10
+ import { AgentEvents } from '../core/EventBridge.js';
11
+
12
+ /**
13
+ * Wire TranscriptPanel to the EventBridge.
14
+ * @param {import('../core/EventBridge.js').EventBridge} bridge
15
+ * @returns {Function[]} unsubscribe functions
16
+ */
17
+ export function connectTranscript(bridge) {
18
+ const tp = () => window.TranscriptPanel;
19
+
20
+ const unsubs = [
21
+ bridge.on(AgentEvents.MESSAGE, ({ role, text, final }) => {
22
+ if (!tp()) return;
23
+ if (final) tp().addMessage(role, text);
24
+ else tp().updateStreaming?.(text);
25
+ }),
26
+ bridge.on(AgentEvents.TRANSCRIPT, ({ text }) => {
27
+ tp()?.updateStreaming?.(text);
28
+ }),
29
+ ];
30
+
31
+ return unsubs;
32
+ }
33
+
34
+ /**
35
+ * Wire ActionConsole to the EventBridge.
36
+ * @param {import('../core/EventBridge.js').EventBridge} bridge
37
+ * @returns {Function[]} unsubscribe functions
38
+ */
39
+ export function connectActionConsole(bridge) {
40
+ const ac = () => window.ActionConsole;
41
+
42
+ const unsubs = [
43
+ bridge.on(AgentEvents.TOOL_CALLED, ({ name, result }) => {
44
+ ac()?.addEntry('tool', name, JSON.stringify(result ?? ''));
45
+ }),
46
+ bridge.on(AgentEvents.CONNECTED, () => {
47
+ ac()?.addEntry('lifecycle', 'Agent connected');
48
+ }),
49
+ bridge.on(AgentEvents.DISCONNECTED, () => {
50
+ ac()?.addEntry('lifecycle', 'Agent disconnected');
51
+ }),
52
+ bridge.on(AgentEvents.ERROR, ({ message }) => {
53
+ ac()?.addEntry('error', `Agent error: ${message}`);
54
+ }),
55
+ bridge.on(AgentEvents.STATE_CHANGED, ({ state }) => {
56
+ ac()?.addEntry('system', `State: ${state}`);
57
+ }),
58
+ ];
59
+
60
+ return unsubs;
61
+ }