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 +2 -3
- package/src/bridge.js +304 -64
- package/src/overlay/launcher.js +50 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promethios-bridge",
|
|
3
|
-
"version": "1.7.
|
|
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
|
-
//
|
|
194
|
-
//
|
|
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 {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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="
|
|
246
|
-
<span class="
|
|
247
|
-
<span class="
|
|
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">
|
|
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"
|
|
481
|
+
<button id="send" title="Send (Enter)">▲</button>
|
|
255
482
|
</div>
|
|
256
483
|
<script>
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
261
|
-
const sendBtn
|
|
262
|
-
const
|
|
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
|
-
|
|
515
|
+
setStatus('Thinking…', true);
|
|
516
|
+
const thinking = addMsg('thinking', 'Promethios is thinking…');
|
|
279
517
|
try {
|
|
280
|
-
const res = await fetch(
|
|
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
|
-
|
|
528
|
+
const err = await res.json().catch(() => ({}));
|
|
529
|
+
addMsg('system', 'Error ' + res.status + (err.error ? ': ' + err.error : ''));
|
|
291
530
|
}
|
|
292
531
|
} catch (e) {
|
|
293
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
370
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
log(
|
|
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
|
package/src/overlay/launcher.js
CHANGED
|
@@ -2,56 +2,74 @@
|
|
|
2
2
|
* launcher.js
|
|
3
3
|
*
|
|
4
4
|
* Called by the bridge CLI after successful authentication.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
19
|
-
* Returns the
|
|
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
|
|
27
|
-
|
|
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 (
|
|
32
|
-
path.join(__dirname, '..', '..', 'node_modules', '
|
|
33
|
-
|
|
34
|
-
//
|
|
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
|
-
|
|
48
|
-
|
|
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 { /*
|
|
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 —
|
|
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();
|
|
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
|
}
|