promethios-bridge 1.7.7 → 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 +1 -1
- package/src/bridge.js +268 -72
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": {
|
package/src/bridge.js
CHANGED
|
@@ -73,36 +73,84 @@ async function checkForUpdates(currentVersion, log) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Open
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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.
|
|
83
84
|
*/
|
|
84
85
|
async function openInBrowser(url, log) {
|
|
85
|
-
const { exec } = require('child_process');
|
|
86
|
+
const { exec, execFile } = require('child_process');
|
|
87
|
+
const { existsSync } = require('fs');
|
|
86
88
|
const platform = process.platform;
|
|
87
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) ──────────────────
|
|
88
143
|
return new Promise((resolve) => {
|
|
89
144
|
let cmd;
|
|
90
145
|
if (platform === 'win32') {
|
|
91
|
-
// On Windows, `start` requires an empty title arg before the URL
|
|
92
|
-
// to prevent issues with URLs containing special characters.
|
|
93
146
|
cmd = `cmd /c start "" "${url}"`;
|
|
94
147
|
} else if (platform === 'darwin') {
|
|
95
148
|
cmd = `open "${url}"`;
|
|
96
149
|
} else {
|
|
97
150
|
cmd = `xdg-open "${url}"`;
|
|
98
151
|
}
|
|
99
|
-
|
|
100
152
|
exec(cmd, { timeout: 5000 }, (err) => {
|
|
101
|
-
if (!err) {
|
|
102
|
-
resolve(true);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
// Native command failed — try the `open` npm package as fallback
|
|
153
|
+
if (!err) { resolve(true); return; }
|
|
106
154
|
log('Native browser open failed, trying open package:', err.message);
|
|
107
155
|
try {
|
|
108
156
|
const openModule = require('open');
|
|
@@ -236,13 +284,36 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
236
284
|
});
|
|
237
285
|
|
|
238
286
|
// ── Overlay route: serves the floating chat UI in the default browser ────
|
|
239
|
-
//
|
|
240
|
-
//
|
|
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).
|
|
241
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
|
+
|
|
242
313
|
app.get('/overlay', (req, res) => {
|
|
243
|
-
const token = overlayAuthToken || '';
|
|
244
|
-
const apiBase_ = apiBase;
|
|
245
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.
|
|
246
317
|
res.send(`<!DOCTYPE html>
|
|
247
318
|
<html lang="en">
|
|
248
319
|
<head>
|
|
@@ -251,61 +322,179 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
251
322
|
<title>Promethios</title>
|
|
252
323
|
<style>
|
|
253
324
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
254
|
-
body {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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; }
|
|
287
466
|
</style>
|
|
288
467
|
</head>
|
|
289
468
|
<body>
|
|
290
469
|
<div id="header">
|
|
291
|
-
<div class="
|
|
292
|
-
<span class="
|
|
293
|
-
<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>
|
|
294
475
|
</div>
|
|
295
476
|
<div id="messages">
|
|
296
|
-
<div class="msg system">
|
|
477
|
+
<div class="msg system">Your computer is connected. Ask me anything.</div>
|
|
297
478
|
</div>
|
|
298
479
|
<div id="input-row">
|
|
299
480
|
<textarea id="input" placeholder="Ask Promethios..." rows="1"></textarea>
|
|
300
|
-
<button id="send"
|
|
481
|
+
<button id="send" title="Send (Enter)">▲</button>
|
|
301
482
|
</div>
|
|
302
483
|
<script>
|
|
303
|
-
|
|
304
|
-
|
|
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';
|
|
305
487
|
const messagesEl = document.getElementById('messages');
|
|
306
|
-
const inputEl
|
|
307
|
-
const sendBtn
|
|
308
|
-
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
|
+
}
|
|
309
498
|
|
|
310
499
|
function addMsg(role, text) {
|
|
311
500
|
const d = document.createElement('div');
|
|
@@ -313,33 +502,40 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
313
502
|
d.textContent = text;
|
|
314
503
|
messagesEl.appendChild(d);
|
|
315
504
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
505
|
+
return d;
|
|
316
506
|
}
|
|
317
507
|
|
|
318
508
|
async function sendMessage() {
|
|
319
509
|
const text = inputEl.value.trim();
|
|
320
|
-
if (!text) return;
|
|
510
|
+
if (!text || sendBtn.disabled) return;
|
|
321
511
|
inputEl.value = '';
|
|
512
|
+
inputEl.style.height = 'auto';
|
|
322
513
|
addMsg('user', text);
|
|
323
514
|
sendBtn.disabled = true;
|
|
324
|
-
|
|
515
|
+
setStatus('Thinking…', true);
|
|
516
|
+
const thinking = addMsg('thinking', 'Promethios is thinking…');
|
|
325
517
|
try {
|
|
326
|
-
const res = await fetch(
|
|
518
|
+
const res = await fetch(PROXY, {
|
|
327
519
|
method: 'POST',
|
|
328
|
-
headers: { 'Content-Type': 'application/json',
|
|
329
|
-
'Authorization': 'Bearer ' + AUTH_TOKEN },
|
|
520
|
+
headers: { 'Content-Type': 'application/json' },
|
|
330
521
|
body: JSON.stringify({ message: text, source: 'overlay' }),
|
|
331
522
|
});
|
|
523
|
+
thinking.remove();
|
|
332
524
|
if (res.ok) {
|
|
333
525
|
const data = await res.json();
|
|
334
526
|
addMsg('ai', data.reply || data.message || JSON.stringify(data));
|
|
335
527
|
} else {
|
|
336
|
-
|
|
528
|
+
const err = await res.json().catch(() => ({}));
|
|
529
|
+
addMsg('system', 'Error ' + res.status + (err.error ? ': ' + err.error : ''));
|
|
337
530
|
}
|
|
338
531
|
} catch (e) {
|
|
339
|
-
|
|
532
|
+
thinking.remove();
|
|
533
|
+
addMsg('system', 'Connection error: ' + e.message);
|
|
534
|
+
setStatus('Reconnecting…', false);
|
|
535
|
+
setTimeout(() => setStatus('Connected', true), 3000);
|
|
340
536
|
}
|
|
341
537
|
sendBtn.disabled = false;
|
|
342
|
-
|
|
538
|
+
setStatus('Connected', true);
|
|
343
539
|
}
|
|
344
540
|
|
|
345
541
|
sendBtn.addEventListener('click', sendMessage);
|
|
@@ -348,7 +544,7 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
348
544
|
});
|
|
349
545
|
inputEl.addEventListener('input', () => {
|
|
350
546
|
inputEl.style.height = 'auto';
|
|
351
|
-
inputEl.style.height = Math.min(inputEl.scrollHeight,
|
|
547
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
|
|
352
548
|
});
|
|
353
549
|
<\/script>
|
|
354
550
|
</body>
|