promethios-bridge 1.7.7 → 1.7.9
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 +285 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promethios-bridge",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.9",
|
|
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,40 @@ 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
|
+
// Endpoint: POST /api/local-bridge/chat (authenticated with bridge session token)
|
|
295
|
+
app.post('/chat-proxy', express.json(), async (req, res) => {
|
|
296
|
+
const token = overlayAuthToken;
|
|
297
|
+
if (!token) { return res.status(401).json({ error: 'Not authenticated' }); }
|
|
298
|
+
try {
|
|
299
|
+
const upstream = await fetch(`${apiBase}/api/local-bridge/chat`, {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
headers: {
|
|
302
|
+
'Content-Type': 'application/json',
|
|
303
|
+
'Authorization': `Bearer ${token}`,
|
|
304
|
+
},
|
|
305
|
+
body: JSON.stringify({
|
|
306
|
+
message: req.body.message,
|
|
307
|
+
conversationHistory: req.body.conversationHistory || [],
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
const data = await upstream.json();
|
|
311
|
+
res.status(upstream.status).json(data);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
res.status(502).json({ error: 'Bridge proxy error: ' + err.message });
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
242
317
|
app.get('/overlay', (req, res) => {
|
|
243
|
-
const token = overlayAuthToken || '';
|
|
244
|
-
const apiBase_ = apiBase;
|
|
245
318
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
319
|
+
// NOTE: The overlay HTML talks to /chat-proxy (same origin, no CORS).
|
|
320
|
+
// It never calls the remote API directly.
|
|
246
321
|
res.send(`<!DOCTYPE html>
|
|
247
322
|
<html lang="en">
|
|
248
323
|
<head>
|
|
@@ -251,61 +326,182 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
251
326
|
<title>Promethios</title>
|
|
252
327
|
<style>
|
|
253
328
|
* { 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
|
-
|
|
329
|
+
html, body { height: 100%; }
|
|
330
|
+
body {
|
|
331
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
|
|
332
|
+
background: #09090b;
|
|
333
|
+
color: #e4e4e7;
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-direction: column;
|
|
336
|
+
height: 100vh;
|
|
337
|
+
overflow: hidden;
|
|
338
|
+
}
|
|
339
|
+
/* ── Header ── */
|
|
340
|
+
#header {
|
|
341
|
+
background: #111113;
|
|
342
|
+
border-bottom: 1px solid #1f1f23;
|
|
343
|
+
padding: 0 16px;
|
|
344
|
+
height: 48px;
|
|
345
|
+
display: flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
gap: 10px;
|
|
348
|
+
flex-shrink: 0;
|
|
349
|
+
user-select: none;
|
|
350
|
+
}
|
|
351
|
+
#header .logo {
|
|
352
|
+
width: 22px; height: 22px;
|
|
353
|
+
background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
|
|
354
|
+
border-radius: 6px;
|
|
355
|
+
display: flex; align-items: center; justify-content: center;
|
|
356
|
+
font-size: 11px; font-weight: 700; color: white; letter-spacing: -0.5px;
|
|
357
|
+
flex-shrink: 0;
|
|
358
|
+
}
|
|
359
|
+
#header .brand { font-size: 14px; font-weight: 600; color: #e4e4e7; }
|
|
360
|
+
#header .badge {
|
|
361
|
+
font-size: 10px; font-weight: 500; color: #a855f7;
|
|
362
|
+
background: #1e1030; border: 1px solid #3b1f6b;
|
|
363
|
+
border-radius: 4px; padding: 1px 6px; margin-left: 2px;
|
|
364
|
+
}
|
|
365
|
+
#status-dot {
|
|
366
|
+
width: 7px; height: 7px; border-radius: 50%;
|
|
367
|
+
background: #22c55e; margin-left: auto; flex-shrink: 0;
|
|
368
|
+
box-shadow: 0 0 6px #22c55e88;
|
|
369
|
+
}
|
|
370
|
+
#status-text { font-size: 11px; color: #71717a; }
|
|
371
|
+
/* ── Messages ── */
|
|
372
|
+
#messages {
|
|
373
|
+
flex: 1;
|
|
374
|
+
overflow-y: auto;
|
|
375
|
+
padding: 16px 14px;
|
|
376
|
+
display: flex;
|
|
377
|
+
flex-direction: column;
|
|
378
|
+
gap: 10px;
|
|
379
|
+
scroll-behavior: smooth;
|
|
380
|
+
}
|
|
381
|
+
#messages::-webkit-scrollbar { width: 4px; }
|
|
382
|
+
#messages::-webkit-scrollbar-track { background: transparent; }
|
|
383
|
+
#messages::-webkit-scrollbar-thumb { background: #27272a; border-radius: 2px; }
|
|
384
|
+
.msg {
|
|
385
|
+
max-width: 88%;
|
|
386
|
+
padding: 9px 13px;
|
|
387
|
+
border-radius: 14px;
|
|
388
|
+
font-size: 13px;
|
|
389
|
+
line-height: 1.55;
|
|
390
|
+
word-break: break-word;
|
|
391
|
+
white-space: pre-wrap;
|
|
392
|
+
}
|
|
393
|
+
.msg.user {
|
|
394
|
+
background: #3b1f6b;
|
|
395
|
+
border: 1px solid #5b21b6;
|
|
396
|
+
align-self: flex-end;
|
|
397
|
+
color: #ede9fe;
|
|
398
|
+
border-bottom-right-radius: 4px;
|
|
399
|
+
}
|
|
400
|
+
.msg.ai {
|
|
401
|
+
background: #111113;
|
|
402
|
+
border: 1px solid #1f1f23;
|
|
403
|
+
align-self: flex-start;
|
|
404
|
+
color: #d4d4d8;
|
|
405
|
+
border-bottom-left-radius: 4px;
|
|
406
|
+
}
|
|
407
|
+
.msg.system {
|
|
408
|
+
background: transparent;
|
|
409
|
+
border: none;
|
|
410
|
+
color: #3f3f46;
|
|
411
|
+
font-size: 11px;
|
|
412
|
+
align-self: center;
|
|
413
|
+
font-style: italic;
|
|
414
|
+
max-width: 100%;
|
|
415
|
+
text-align: center;
|
|
416
|
+
}
|
|
417
|
+
.msg.thinking {
|
|
418
|
+
background: #111113;
|
|
419
|
+
border: 1px solid #1f1f23;
|
|
420
|
+
align-self: flex-start;
|
|
421
|
+
color: #52525b;
|
|
422
|
+
font-size: 12px;
|
|
423
|
+
font-style: italic;
|
|
424
|
+
border-bottom-left-radius: 4px;
|
|
425
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
426
|
+
}
|
|
427
|
+
@keyframes pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }
|
|
428
|
+
/* ── Input row ── */
|
|
429
|
+
#input-row {
|
|
430
|
+
padding: 10px 12px 12px;
|
|
431
|
+
background: #111113;
|
|
432
|
+
border-top: 1px solid #1f1f23;
|
|
433
|
+
display: flex;
|
|
434
|
+
gap: 8px;
|
|
435
|
+
flex-shrink: 0;
|
|
436
|
+
align-items: flex-end;
|
|
437
|
+
}
|
|
438
|
+
#input {
|
|
439
|
+
flex: 1;
|
|
440
|
+
background: #18181b;
|
|
441
|
+
border: 1px solid #27272a;
|
|
442
|
+
border-radius: 10px;
|
|
443
|
+
padding: 9px 13px;
|
|
444
|
+
color: #e4e4e7;
|
|
445
|
+
font-size: 13px;
|
|
446
|
+
font-family: inherit;
|
|
447
|
+
outline: none;
|
|
448
|
+
resize: none;
|
|
449
|
+
min-height: 38px;
|
|
450
|
+
max-height: 120px;
|
|
451
|
+
line-height: 1.4;
|
|
452
|
+
transition: border-color 0.15s;
|
|
453
|
+
}
|
|
454
|
+
#input::placeholder { color: #3f3f46; }
|
|
455
|
+
#input:focus { border-color: #7c3aed; }
|
|
456
|
+
#send {
|
|
457
|
+
background: #7c3aed;
|
|
458
|
+
color: white;
|
|
459
|
+
border: none;
|
|
460
|
+
border-radius: 10px;
|
|
461
|
+
width: 38px; height: 38px;
|
|
462
|
+
font-size: 16px;
|
|
463
|
+
cursor: pointer;
|
|
464
|
+
flex-shrink: 0;
|
|
465
|
+
display: flex; align-items: center; justify-content: center;
|
|
466
|
+
transition: background 0.15s;
|
|
467
|
+
}
|
|
468
|
+
#send:hover { background: #6d28d9; }
|
|
469
|
+
#send:disabled { background: #27272a; cursor: not-allowed; }
|
|
287
470
|
</style>
|
|
288
471
|
</head>
|
|
289
472
|
<body>
|
|
290
473
|
<div id="header">
|
|
291
|
-
<div class="
|
|
292
|
-
<span class="
|
|
293
|
-
<span class="
|
|
474
|
+
<div class="logo">P</div>
|
|
475
|
+
<span class="brand">Promethios</span>
|
|
476
|
+
<span class="badge">Local Bridge</span>
|
|
477
|
+
<span id="status-text">Connected</span>
|
|
478
|
+
<div id="status-dot"></div>
|
|
294
479
|
</div>
|
|
295
480
|
<div id="messages">
|
|
296
|
-
<div class="msg system">
|
|
481
|
+
<div class="msg system">Your computer is connected. Ask me anything.</div>
|
|
297
482
|
</div>
|
|
298
483
|
<div id="input-row">
|
|
299
484
|
<textarea id="input" placeholder="Ask Promethios..." rows="1"></textarea>
|
|
300
|
-
<button id="send"
|
|
485
|
+
<button id="send" title="Send (Enter)">▲</button>
|
|
301
486
|
</div>
|
|
302
487
|
<script>
|
|
303
|
-
|
|
304
|
-
|
|
488
|
+
// Chat is proxied through the bridge's own server (/chat-proxy)
|
|
489
|
+
// to avoid CORS — the browser never calls the remote API directly.
|
|
490
|
+
const PROXY = 'http://127.0.0.1:${port}/chat-proxy';
|
|
305
491
|
const messagesEl = document.getElementById('messages');
|
|
306
|
-
const inputEl
|
|
307
|
-
const sendBtn
|
|
308
|
-
const
|
|
492
|
+
const inputEl = document.getElementById('input');
|
|
493
|
+
const sendBtn = document.getElementById('send');
|
|
494
|
+
const statusTxt = document.getElementById('status-text');
|
|
495
|
+
const statusDot = document.getElementById('status-dot');
|
|
496
|
+
|
|
497
|
+
// Maintain conversation history for multi-turn context
|
|
498
|
+
const conversationHistory = [];
|
|
499
|
+
|
|
500
|
+
function setStatus(text, ok) {
|
|
501
|
+
statusTxt.textContent = text;
|
|
502
|
+
statusDot.style.background = ok ? '#22c55e' : '#f59e0b';
|
|
503
|
+
statusDot.style.boxShadow = ok ? '0 0 6px #22c55e88' : '0 0 6px #f59e0b88';
|
|
504
|
+
}
|
|
309
505
|
|
|
310
506
|
function addMsg(role, text) {
|
|
311
507
|
const d = document.createElement('div');
|
|
@@ -313,33 +509,48 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
313
509
|
d.textContent = text;
|
|
314
510
|
messagesEl.appendChild(d);
|
|
315
511
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
512
|
+
return d;
|
|
316
513
|
}
|
|
317
514
|
|
|
318
515
|
async function sendMessage() {
|
|
319
516
|
const text = inputEl.value.trim();
|
|
320
|
-
if (!text) return;
|
|
517
|
+
if (!text || sendBtn.disabled) return;
|
|
321
518
|
inputEl.value = '';
|
|
519
|
+
inputEl.style.height = 'auto';
|
|
322
520
|
addMsg('user', text);
|
|
323
521
|
sendBtn.disabled = true;
|
|
324
|
-
|
|
522
|
+
setStatus('Thinking\u2026', true);
|
|
523
|
+
const thinking = addMsg('thinking', 'Promethios is thinking\u2026');
|
|
325
524
|
try {
|
|
326
|
-
const res = await fetch(
|
|
525
|
+
const res = await fetch(PROXY, {
|
|
327
526
|
method: 'POST',
|
|
328
|
-
headers: { 'Content-Type': 'application/json',
|
|
329
|
-
|
|
330
|
-
|
|
527
|
+
headers: { 'Content-Type': 'application/json' },
|
|
528
|
+
body: JSON.stringify({
|
|
529
|
+
message: text,
|
|
530
|
+
conversationHistory: conversationHistory.slice(-20), // last 20 turns max
|
|
531
|
+
source: 'overlay',
|
|
532
|
+
}),
|
|
331
533
|
});
|
|
534
|
+
thinking.remove();
|
|
332
535
|
if (res.ok) {
|
|
333
536
|
const data = await res.json();
|
|
334
|
-
|
|
537
|
+
const reply = data.reply || data.message || JSON.stringify(data);
|
|
538
|
+
addMsg('ai', reply);
|
|
539
|
+
// Update conversation history for next turn
|
|
540
|
+
conversationHistory.push({ role: 'user', content: text });
|
|
541
|
+
conversationHistory.push({ role: 'assistant', content: reply });
|
|
335
542
|
} else {
|
|
336
|
-
|
|
543
|
+
const err = await res.json().catch(() => ({}));
|
|
544
|
+
addMsg('system', 'Error ' + res.status + (err.error ? ': ' + err.error : ''));
|
|
337
545
|
}
|
|
338
546
|
} catch (e) {
|
|
339
|
-
|
|
547
|
+
thinking.remove();
|
|
548
|
+
addMsg('system', 'Connection error: ' + e.message);
|
|
549
|
+
setStatus('Reconnecting\u2026', false);
|
|
550
|
+
setTimeout(() => setStatus('Connected', true), 3000);
|
|
340
551
|
}
|
|
341
552
|
sendBtn.disabled = false;
|
|
342
|
-
|
|
553
|
+
setStatus('Connected', true);
|
|
343
554
|
}
|
|
344
555
|
|
|
345
556
|
sendBtn.addEventListener('click', sendMessage);
|
|
@@ -348,7 +559,7 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
348
559
|
});
|
|
349
560
|
inputEl.addEventListener('input', () => {
|
|
350
561
|
inputEl.style.height = 'auto';
|
|
351
|
-
inputEl.style.height = Math.min(inputEl.scrollHeight,
|
|
562
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
|
|
352
563
|
});
|
|
353
564
|
<\/script>
|
|
354
565
|
</body>
|