promethios-bridge 1.7.6 → 1.7.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promethios-bridge",
3
- "version": "1.7.6",
3
+ "version": "1.7.8",
4
4
  "description": "Run Promethios agent frameworks locally on your computer with full file, terminal, browser access, ambient context capture, and the always-on-top floating chat overlay. Native Framework Mode supports OpenClaw and other frameworks via the bridge.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -49,8 +49,7 @@
49
49
  "express": "^4.18.2",
50
50
  "open": "^8.4.2",
51
51
  "ora": "^5.4.1",
52
- "node-fetch": "^2.7.0",
53
- "playwright": "^1.42.0"
52
+ "node-fetch": "^2.7.0"
54
53
  },
55
54
  "optionalDependencies": {
56
55
  "playwright": "^1.42.0",
package/src/bridge.js CHANGED
@@ -72,6 +72,100 @@ async function checkForUpdates(currentVersion, log) {
72
72
  }
73
73
  }
74
74
 
75
+ /**
76
+ * Open the overlay URL as a standalone app window (no tabs, no address bar).
77
+ *
78
+ * Strategy (in order):
79
+ * 1. Chrome/Edge --app flag: opens a frameless app window with no tabs
80
+ * 2. Firefox --new-window: opens in a new window (has address bar)
81
+ * 3. platform-native fallback: cmd start / open / xdg-open (opens as tab)
82
+ *
83
+ * Returns true if any method succeeded, false otherwise.
84
+ */
85
+ async function openInBrowser(url, log) {
86
+ const { exec, execFile } = require('child_process');
87
+ const { existsSync } = require('fs');
88
+ const platform = process.platform;
89
+
90
+ // ── Try to open as a standalone app window (Chrome/Edge --app flag) ──────
91
+ // This gives the closest experience to the Electron overlay:
92
+ // no address bar, no tabs, no bookmarks bar.
93
+ if (platform === 'win32') {
94
+ const chromeCandidates = [
95
+ process.env['PROGRAMFILES'] + '\\Google\\Chrome\\Application\\chrome.exe',
96
+ process.env['PROGRAMFILES(X86)'] + '\\Google\\Chrome\\Application\\chrome.exe',
97
+ process.env['LOCALAPPDATA'] + '\\Google\\Chrome\\Application\\chrome.exe',
98
+ process.env['PROGRAMFILES'] + '\\Microsoft\\Edge\\Application\\msedge.exe',
99
+ process.env['PROGRAMFILES(X86)'] + '\\Microsoft\\Edge\\Application\\msedge.exe',
100
+ ].filter(Boolean);
101
+
102
+ for (const chromePath of chromeCandidates) {
103
+ if (existsSync(chromePath)) {
104
+ const launched = await new Promise((resolve) => {
105
+ const child = require('child_process').spawn(
106
+ chromePath,
107
+ [`--app=${url}`, '--window-size=440,720', '--window-position=50,50'],
108
+ { detached: true, stdio: 'ignore' }
109
+ );
110
+ child.on('error', () => resolve(false));
111
+ child.unref();
112
+ // If spawn didn't immediately error, assume success
113
+ setTimeout(() => resolve(true), 500);
114
+ });
115
+ if (launched) {
116
+ log('Opened overlay as Chrome/Edge app window:', chromePath);
117
+ return true;
118
+ }
119
+ }
120
+ }
121
+ } else if (platform === 'darwin') {
122
+ // macOS: try Chrome --app flag
123
+ const chromeMac = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
124
+ const edgeMac = '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge';
125
+ for (const chromePath of [chromeMac, edgeMac]) {
126
+ if (existsSync(chromePath)) {
127
+ const launched = await new Promise((resolve) => {
128
+ const child = require('child_process').spawn(
129
+ chromePath,
130
+ [`--app=${url}`, '--window-size=440,720'],
131
+ { detached: true, stdio: 'ignore' }
132
+ );
133
+ child.on('error', () => resolve(false));
134
+ child.unref();
135
+ setTimeout(() => resolve(true), 500);
136
+ });
137
+ if (launched) return true;
138
+ }
139
+ }
140
+ }
141
+
142
+ // ── Fallback: open in default browser (may open as tab) ──────────────────
143
+ return new Promise((resolve) => {
144
+ let cmd;
145
+ if (platform === 'win32') {
146
+ cmd = `cmd /c start "" "${url}"`;
147
+ } else if (platform === 'darwin') {
148
+ cmd = `open "${url}"`;
149
+ } else {
150
+ cmd = `xdg-open "${url}"`;
151
+ }
152
+ exec(cmd, { timeout: 5000 }, (err) => {
153
+ if (!err) { resolve(true); return; }
154
+ log('Native browser open failed, trying open package:', err.message);
155
+ try {
156
+ const openModule = require('open');
157
+ openModule(url).then(() => resolve(true)).catch((e) => {
158
+ log('open package also failed:', e.message);
159
+ resolve(false);
160
+ });
161
+ } catch (e) {
162
+ log('open package not available:', e.message);
163
+ resolve(false);
164
+ }
165
+ });
166
+ });
167
+ }
168
+
75
169
  async function startBridge({ setupToken, apiBase, port, dev }) {
76
170
  const log = dev
77
171
  ? (...args) => console.log(chalk.gray(' [debug]'), ...args)
@@ -190,13 +284,36 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
190
284
  });
191
285
 
192
286
  // ── Overlay route: serves the floating chat UI in the default browser ────
193
- // This is the fallback when Electron is not available (e.g. running via npx).
194
- // The overlay HTML is stored inline here so no file path resolution is needed.
287
+ // Chat messages are proxied through the bridge's own Express server to avoid
288
+ // CORS issues (the browser cannot call the remote API directly from localhost).
195
289
  let overlayAuthToken = null; // set after authentication
290
+
291
+ // ── /chat-proxy: forwards messages to the Promethios API on behalf of the overlay ──
292
+ // This avoids CORS entirely — the browser talks to 127.0.0.1, the bridge
293
+ // talks to the remote API using the stored auth token.
294
+ app.post('/chat-proxy', express.json(), async (req, res) => {
295
+ const token = overlayAuthToken;
296
+ if (!token) { return res.status(401).json({ error: 'Not authenticated' }); }
297
+ try {
298
+ const upstream = await fetch(`${apiBase}/api/chat/quick`, {
299
+ method: 'POST',
300
+ headers: {
301
+ 'Content-Type': 'application/json',
302
+ 'Authorization': `Bearer ${token}`,
303
+ },
304
+ body: JSON.stringify(req.body),
305
+ });
306
+ const data = await upstream.json();
307
+ res.status(upstream.status).json(data);
308
+ } catch (err) {
309
+ res.status(502).json({ error: 'Bridge proxy error: ' + err.message });
310
+ }
311
+ });
312
+
196
313
  app.get('/overlay', (req, res) => {
197
- const token = overlayAuthToken || '';
198
- const apiBase_ = apiBase;
199
314
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
315
+ // NOTE: The overlay HTML talks to /chat-proxy (same origin, no CORS).
316
+ // It never calls the remote API directly.
200
317
  res.send(`<!DOCTYPE html>
201
318
  <html lang="en">
202
319
  <head>
@@ -205,61 +322,179 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
205
322
  <title>Promethios</title>
206
323
  <style>
207
324
  * { box-sizing: border-box; margin: 0; padding: 0; }
208
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
209
- background: #0f0f11; color: #e4e4e7; height: 100vh; display: flex;
210
- flex-direction: column; overflow: hidden; }
211
- #header { background: #18181b; border-bottom: 1px solid #27272a;
212
- padding: 10px 14px; display: flex; align-items: center; gap: 8px;
213
- flex-shrink: 0; }
214
- #header .dot { width: 8px; height: 8px; border-radius: 50%;
215
- background: #22c55e; flex-shrink: 0; }
216
- #header .title { font-size: 13px; font-weight: 600; color: #a1a1aa; }
217
- #header .status { font-size: 11px; color: #22c55e; margin-left: auto; }
218
- #messages { flex: 1; overflow-y: auto; padding: 12px; display: flex;
219
- flex-direction: column; gap: 8px; }
220
- .msg { max-width: 85%; padding: 8px 12px; border-radius: 12px;
221
- font-size: 13px; line-height: 1.5; word-break: break-word; }
222
- .msg.user { background: #3f3f46; align-self: flex-end; color: #e4e4e7; }
223
- .msg.ai { background: #1e1e2e; border: 1px solid #27272a;
224
- align-self: flex-start; color: #c4b5fd; }
225
- .msg.system { background: transparent; border: none;
226
- color: #52525b; font-size: 11px; align-self: center; font-style: italic; }
227
- #input-row { padding: 10px 12px; background: #18181b;
228
- border-top: 1px solid #27272a; display: flex; gap: 8px; flex-shrink: 0; }
229
- #input { flex: 1; background: #27272a; border: 1px solid #3f3f46;
230
- border-radius: 8px; padding: 8px 12px; color: #e4e4e7; font-size: 13px;
231
- outline: none; resize: none; height: 36px; font-family: inherit; }
232
- #input:focus { border-color: #6d28d9; }
233
- #send { background: #6d28d9; color: white; border: none; border-radius: 8px;
234
- padding: 0 14px; font-size: 13px; cursor: pointer; height: 36px;
235
- flex-shrink: 0; }
236
- #send:hover { background: #7c3aed; }
237
- #send:disabled { background: #3f3f46; cursor: not-allowed; }
238
- ::-webkit-scrollbar { width: 4px; }
239
- ::-webkit-scrollbar-track { background: transparent; }
240
- ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 2px; }
325
+ html, body { height: 100%; }
326
+ body {
327
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
328
+ background: #09090b;
329
+ color: #e4e4e7;
330
+ display: flex;
331
+ flex-direction: column;
332
+ height: 100vh;
333
+ overflow: hidden;
334
+ }
335
+ /* ── Header ── */
336
+ #header {
337
+ background: #111113;
338
+ border-bottom: 1px solid #1f1f23;
339
+ padding: 0 16px;
340
+ height: 48px;
341
+ display: flex;
342
+ align-items: center;
343
+ gap: 10px;
344
+ flex-shrink: 0;
345
+ user-select: none;
346
+ }
347
+ #header .logo {
348
+ width: 22px; height: 22px;
349
+ background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
350
+ border-radius: 6px;
351
+ display: flex; align-items: center; justify-content: center;
352
+ font-size: 11px; font-weight: 700; color: white; letter-spacing: -0.5px;
353
+ flex-shrink: 0;
354
+ }
355
+ #header .brand { font-size: 14px; font-weight: 600; color: #e4e4e7; }
356
+ #header .badge {
357
+ font-size: 10px; font-weight: 500; color: #a855f7;
358
+ background: #1e1030; border: 1px solid #3b1f6b;
359
+ border-radius: 4px; padding: 1px 6px; margin-left: 2px;
360
+ }
361
+ #status-dot {
362
+ width: 7px; height: 7px; border-radius: 50%;
363
+ background: #22c55e; margin-left: auto; flex-shrink: 0;
364
+ box-shadow: 0 0 6px #22c55e88;
365
+ }
366
+ #status-text { font-size: 11px; color: #71717a; }
367
+ /* ── Messages ── */
368
+ #messages {
369
+ flex: 1;
370
+ overflow-y: auto;
371
+ padding: 16px 14px;
372
+ display: flex;
373
+ flex-direction: column;
374
+ gap: 10px;
375
+ scroll-behavior: smooth;
376
+ }
377
+ #messages::-webkit-scrollbar { width: 4px; }
378
+ #messages::-webkit-scrollbar-track { background: transparent; }
379
+ #messages::-webkit-scrollbar-thumb { background: #27272a; border-radius: 2px; }
380
+ .msg {
381
+ max-width: 88%;
382
+ padding: 9px 13px;
383
+ border-radius: 14px;
384
+ font-size: 13px;
385
+ line-height: 1.55;
386
+ word-break: break-word;
387
+ white-space: pre-wrap;
388
+ }
389
+ .msg.user {
390
+ background: #3b1f6b;
391
+ border: 1px solid #5b21b6;
392
+ align-self: flex-end;
393
+ color: #ede9fe;
394
+ border-bottom-right-radius: 4px;
395
+ }
396
+ .msg.ai {
397
+ background: #111113;
398
+ border: 1px solid #1f1f23;
399
+ align-self: flex-start;
400
+ color: #d4d4d8;
401
+ border-bottom-left-radius: 4px;
402
+ }
403
+ .msg.system {
404
+ background: transparent;
405
+ border: none;
406
+ color: #3f3f46;
407
+ font-size: 11px;
408
+ align-self: center;
409
+ font-style: italic;
410
+ max-width: 100%;
411
+ text-align: center;
412
+ }
413
+ .msg.thinking {
414
+ background: #111113;
415
+ border: 1px solid #1f1f23;
416
+ align-self: flex-start;
417
+ color: #52525b;
418
+ font-size: 12px;
419
+ font-style: italic;
420
+ border-bottom-left-radius: 4px;
421
+ animation: pulse 1.5s ease-in-out infinite;
422
+ }
423
+ @keyframes pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }
424
+ /* ── Input row ── */
425
+ #input-row {
426
+ padding: 10px 12px 12px;
427
+ background: #111113;
428
+ border-top: 1px solid #1f1f23;
429
+ display: flex;
430
+ gap: 8px;
431
+ flex-shrink: 0;
432
+ align-items: flex-end;
433
+ }
434
+ #input {
435
+ flex: 1;
436
+ background: #18181b;
437
+ border: 1px solid #27272a;
438
+ border-radius: 10px;
439
+ padding: 9px 13px;
440
+ color: #e4e4e7;
441
+ font-size: 13px;
442
+ font-family: inherit;
443
+ outline: none;
444
+ resize: none;
445
+ min-height: 38px;
446
+ max-height: 120px;
447
+ line-height: 1.4;
448
+ transition: border-color 0.15s;
449
+ }
450
+ #input::placeholder { color: #3f3f46; }
451
+ #input:focus { border-color: #7c3aed; }
452
+ #send {
453
+ background: #7c3aed;
454
+ color: white;
455
+ border: none;
456
+ border-radius: 10px;
457
+ width: 38px; height: 38px;
458
+ font-size: 16px;
459
+ cursor: pointer;
460
+ flex-shrink: 0;
461
+ display: flex; align-items: center; justify-content: center;
462
+ transition: background 0.15s;
463
+ }
464
+ #send:hover { background: #6d28d9; }
465
+ #send:disabled { background: #27272a; cursor: not-allowed; }
241
466
  </style>
242
467
  </head>
243
468
  <body>
244
469
  <div id="header">
245
- <div class="dot"></div>
246
- <span class="title">Promethios</span>
247
- <span class="status" id="status">Connected</span>
470
+ <div class="logo">P</div>
471
+ <span class="brand">Promethios</span>
472
+ <span class="badge">Local Bridge</span>
473
+ <span id="status-text">Connected</span>
474
+ <div id="status-dot"></div>
248
475
  </div>
249
476
  <div id="messages">
250
- <div class="msg system">Bridge connected ready to help</div>
477
+ <div class="msg system">Your computer is connected. Ask me anything.</div>
251
478
  </div>
252
479
  <div id="input-row">
253
480
  <textarea id="input" placeholder="Ask Promethios..." rows="1"></textarea>
254
- <button id="send">Send</button>
481
+ <button id="send" title="Send (Enter)">&#9650;</button>
255
482
  </div>
256
483
  <script>
257
- const AUTH_TOKEN = '${token}';
258
- const API_BASE = '${apiBase_}';
484
+ // Chat is proxied through the bridge's own server (/chat-proxy)
485
+ // to avoid CORS — the browser never calls the remote API directly.
486
+ const PROXY = 'http://127.0.0.1:${port}/chat-proxy';
259
487
  const messagesEl = document.getElementById('messages');
260
- const inputEl = document.getElementById('input');
261
- const sendBtn = document.getElementById('send');
262
- const statusEl = document.getElementById('status');
488
+ const inputEl = document.getElementById('input');
489
+ const sendBtn = document.getElementById('send');
490
+ const statusTxt = document.getElementById('status-text');
491
+ const statusDot = document.getElementById('status-dot');
492
+
493
+ function setStatus(text, ok) {
494
+ statusTxt.textContent = text;
495
+ statusDot.style.background = ok ? '#22c55e' : '#f59e0b';
496
+ statusDot.style.boxShadow = ok ? '0 0 6px #22c55e88' : '0 0 6px #f59e0b88';
497
+ }
263
498
 
264
499
  function addMsg(role, text) {
265
500
  const d = document.createElement('div');
@@ -267,33 +502,40 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
267
502
  d.textContent = text;
268
503
  messagesEl.appendChild(d);
269
504
  messagesEl.scrollTop = messagesEl.scrollHeight;
505
+ return d;
270
506
  }
271
507
 
272
508
  async function sendMessage() {
273
509
  const text = inputEl.value.trim();
274
- if (!text) return;
510
+ if (!text || sendBtn.disabled) return;
275
511
  inputEl.value = '';
512
+ inputEl.style.height = 'auto';
276
513
  addMsg('user', text);
277
514
  sendBtn.disabled = true;
278
- statusEl.textContent = 'Thinking...';
515
+ setStatus('Thinking', true);
516
+ const thinking = addMsg('thinking', 'Promethios is thinking…');
279
517
  try {
280
- const res = await fetch(API_BASE + '/api/chat/quick', {
518
+ const res = await fetch(PROXY, {
281
519
  method: 'POST',
282
- headers: { 'Content-Type': 'application/json',
283
- 'Authorization': 'Bearer ' + AUTH_TOKEN },
520
+ headers: { 'Content-Type': 'application/json' },
284
521
  body: JSON.stringify({ message: text, source: 'overlay' }),
285
522
  });
523
+ thinking.remove();
286
524
  if (res.ok) {
287
525
  const data = await res.json();
288
526
  addMsg('ai', data.reply || data.message || JSON.stringify(data));
289
527
  } else {
290
- addMsg('system', 'Error: ' + res.status);
528
+ const err = await res.json().catch(() => ({}));
529
+ addMsg('system', 'Error ' + res.status + (err.error ? ': ' + err.error : ''));
291
530
  }
292
531
  } catch (e) {
293
- addMsg('system', 'Could not reach Promethios: ' + e.message);
532
+ thinking.remove();
533
+ addMsg('system', 'Connection error: ' + e.message);
534
+ setStatus('Reconnecting…', false);
535
+ setTimeout(() => setStatus('Connected', true), 3000);
294
536
  }
295
537
  sendBtn.disabled = false;
296
- statusEl.textContent = 'Connected';
538
+ setStatus('Connected', true);
297
539
  }
298
540
 
299
541
  sendBtn.addEventListener('click', sendMessage);
@@ -302,7 +544,7 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
302
544
  });
303
545
  inputEl.addEventListener('input', () => {
304
546
  inputEl.style.height = 'auto';
305
- inputEl.style.height = Math.min(inputEl.scrollHeight, 100) + 'px';
547
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
306
548
  });
307
549
  <\/script>
308
550
  </body>
@@ -366,17 +608,15 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
366
608
  // Electron not available — open the lightweight browser overlay instead.
367
609
  // This is a small chat UI served by the bridge's own Express server.
368
610
  const overlayUrl = `http://127.0.0.1:${port}/overlay`;
369
- try {
370
- const openModule = require('open');
371
- await openModule(overlayUrl);
611
+ const openedInBrowser = await openInBrowser(overlayUrl, log);
612
+ if (openedInBrowser) {
372
613
  console.log(chalk.cyan(' ⬡ Promethios overlay opened in your browser'));
373
614
  console.log(chalk.gray(` URL: ${overlayUrl}`));
374
- console.log('');
375
- } catch (err) {
376
- log('Browser overlay launch failed (non-critical):', err.message);
377
- console.log(chalk.gray(` ℹ Open ${overlayUrl} in your browser for the chat overlay`));
378
- console.log('');
615
+ } else {
616
+ console.log(chalk.gray(` ℹ Open this URL in your browser for the chat overlay:`));
617
+ console.log(chalk.cyan(` ${overlayUrl}`));
379
618
  }
619
+ console.log('');
380
620
  }
381
621
 
382
622
  // Heartbeat loop
@@ -2,56 +2,74 @@
2
2
  * launcher.js
3
3
  *
4
4
  * Called by the bridge CLI after successful authentication.
5
- * Spawns the Electron overlay window as a detached child process.
6
- * Bundled inside promethios-bridge no separate install required.
5
+ * Attempts to launch the Electron overlay window. Returns null if Electron
6
+ * is not available (which is the case when running via `npx`, since optional
7
+ * dependencies are not installed in the npx cache).
7
8
  *
8
- * Usage (from bridge CLI):
9
- * const { launchOverlay } = require('./overlay/launcher');
10
- * launchOverlay({ authToken, apiBase, threadId, dev });
9
+ * IMPORTANT: We check for the actual Electron binary on disk, NOT just whether
10
+ * the `electron` npm package is present. When running via `npx`, the electron
11
+ * package IS downloaded (it's a listed optionalDependency), but its postinstall
12
+ * script downloads the real binary separately — that binary may not exist yet.
13
+ * Using require.resolve('electron') would return true even without the binary,
14
+ * causing spawn() to fail silently and the browser fallback to never be reached.
11
15
  */
12
16
 
17
+ 'use strict';
18
+
13
19
  const { spawn } = require('child_process');
14
20
  const path = require('path');
15
21
  const fs = require('fs');
16
22
 
17
23
  /**
18
- * Launch the Promethios overlay Electron window.
19
- * Returns the child process (or null if Electron is not available).
20
- *
21
- * IMPORTANT: When running via `npx`, optional dependencies (including electron)
22
- * are NOT installed in the npx cache. The spawn() call may therefore fail with
23
- * ENOENT. We attach an 'error' handler before unref() to silently absorb this
24
- * instead of crashing the bridge process with an unhandled error event.
24
+ * Find the actual Electron binary on disk.
25
+ * Returns the path string if found and executable, or null if not available.
25
26
  */
26
- function launchOverlay({ authToken, apiBase = 'https://api.promethios.ai', threadId = '', dev = false } = {}) {
27
- // Find electron binary try local node_modules first, then global
28
- let electronBin = null;
27
+ function findElectronBinary() {
28
+ const isWin = process.platform === 'win32';
29
29
 
30
+ // Candidate paths for the real Electron binary (not just the npm package stub)
30
31
  const candidates = [
31
- // Installed inside promethios-bridge's own node_modules (postinstall puts it here)
32
- path.join(__dirname, '..', '..', 'node_modules', '.bin', 'electron'),
33
- path.join(__dirname, '..', '..', 'node_modules', '.bin', 'electron.cmd'),
34
- // Global electron fallback
35
- 'electron',
32
+ // 1. Installed inside promethios-bridge's own node_modules (full npm install, not npx)
33
+ path.join(__dirname, '..', '..', 'node_modules', 'electron', 'dist',
34
+ isWin ? 'electron.exe' : process.platform === 'darwin' ? 'Electron.app/Contents/MacOS/Electron' : 'electron'),
35
+ // 2. .bin shim (only works if the real binary was downloaded by postinstall)
36
+ path.join(__dirname, '..', '..', 'node_modules', '.bin', isWin ? 'electron.cmd' : 'electron'),
37
+ // 3. Global electron install
38
+ path.join(require('os').homedir(), '.npm', '_npx', '**', 'node_modules', 'electron', 'dist', isWin ? 'electron.exe' : 'electron'),
36
39
  ];
37
40
 
38
41
  for (const candidate of candidates) {
42
+ // Skip glob-style paths (we can't expand them here)
43
+ if (candidate.includes('**')) continue;
39
44
  try {
40
- if (candidate === 'electron') {
41
- // Try to resolve globally — throws if not installed
42
- require.resolve('electron');
43
- electronBin = candidate;
44
- break;
45
- }
46
45
  if (fs.existsSync(candidate)) {
47
- electronBin = candidate;
48
- break;
46
+ // Extra check: the .bin shim always exists even without the real binary.
47
+ // For the .bin shim, also verify the actual dist binary exists.
48
+ if (candidate.includes('.bin')) {
49
+ const distBin = path.join(
50
+ path.dirname(candidate), '..', 'electron', 'dist',
51
+ isWin ? 'electron.exe' : process.platform === 'darwin'
52
+ ? 'Electron.app/Contents/MacOS/Electron' : 'electron'
53
+ );
54
+ if (!fs.existsSync(distBin)) continue;
55
+ }
56
+ return candidate;
49
57
  }
50
- } catch { /* not found */ }
58
+ } catch { /* ignore */ }
51
59
  }
52
60
 
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Launch the Promethios overlay Electron window.
66
+ * Returns the child process if Electron was found and launched, or null otherwise.
67
+ */
68
+ function launchOverlay({ authToken, apiBase = 'https://api.promethios.ai', threadId = '', dev = false } = {}) {
69
+ const electronBin = findElectronBinary();
70
+
53
71
  if (!electronBin) {
54
- if (dev) console.log('[overlay] Electron not found — overlay will not launch.');
72
+ if (dev) console.log('[overlay] Electron binary not found on disk skipping Electron overlay.');
55
73
  return null;
56
74
  }
57
75
 
@@ -70,16 +88,13 @@ function launchOverlay({ authToken, apiBase = 'https://api.promethios.ai', threa
70
88
  });
71
89
 
72
90
  // Attach error handler BEFORE unref() to prevent unhandled 'error' event crashes.
73
- // When running via npx, optional deps like electron are not installed, so spawn
74
- // may emit ENOENT. We catch it silently — the bridge works fine without the overlay.
75
91
  child.on('error', (err) => {
76
92
  if (dev) console.log(`[overlay] Spawn error (non-critical): ${err.message}`);
77
- // No-op: overlay is optional, bridge continues running normally
78
93
  });
79
94
 
80
- child.unref(); // Don't keep bridge CLI alive waiting for overlay
95
+ child.unref();
81
96
 
82
- if (dev) console.log(`[overlay] Launched (pid ${child.pid})`);
97
+ if (dev) console.log(`[overlay] Launched Electron (pid ${child.pid})`);
83
98
 
84
99
  return child;
85
100
  }