promethios-bridge 2.1.8 → 2.2.1
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 +555 -58
- package/src/overlay/launcher.js +75 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promethios-bridge",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
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
|
@@ -39,7 +39,12 @@ const { initAndroidTools } = require('./tools/android');
|
|
|
39
39
|
|
|
40
40
|
// Optional: Electron overlay window (bundled in src/overlay — gracefully skipped if Electron not available)
|
|
41
41
|
let launchOverlay = null;
|
|
42
|
-
|
|
42
|
+
let isDesktopInstalled = null;
|
|
43
|
+
try {
|
|
44
|
+
const overlayLauncher = require('./overlay/launcher');
|
|
45
|
+
launchOverlay = overlayLauncher.launchOverlay;
|
|
46
|
+
isDesktopInstalled = overlayLauncher.isDesktopInstalled;
|
|
47
|
+
} catch { /* overlay launcher not found */ }
|
|
43
48
|
|
|
44
49
|
const HEARTBEAT_INTERVAL = 30_000; // 30s
|
|
45
50
|
const POLL_INTERVAL = 1_000; // 1s — poll for pending tool calls
|
|
@@ -485,9 +490,188 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
485
490
|
}
|
|
486
491
|
});
|
|
487
492
|
|
|
493
|
+
|
|
494
|
+
// ── MCP OAuth routes (for the browser-based overlay Providers tab) ──────────
|
|
495
|
+
// These routes implement the same OAuth flow as the Electron mcp-oauth.js,
|
|
496
|
+
// but running entirely inside the Express server so the npx bridge can use it.
|
|
497
|
+
//
|
|
498
|
+
// Token storage: in-memory map (resets on bridge restart).
|
|
499
|
+
// For persistence across restarts, tokens are also saved to ~/.promethios/mcp-tokens.json
|
|
500
|
+
const path = require('path');
|
|
501
|
+
const fs = require('fs');
|
|
502
|
+
const http = require('http');
|
|
503
|
+
const { URL: NodeURL } = require('url');
|
|
504
|
+
const crypto = require('crypto');
|
|
505
|
+
|
|
506
|
+
const MCP_TOKENS_PATH = path.join(
|
|
507
|
+
process.env.HOME || process.env.USERPROFILE || require('os').homedir(),
|
|
508
|
+
'.promethios', 'mcp-tokens.json'
|
|
509
|
+
);
|
|
510
|
+
function loadMcpTokens() {
|
|
511
|
+
try {
|
|
512
|
+
fs.mkdirSync(path.dirname(MCP_TOKENS_PATH), { recursive: true });
|
|
513
|
+
return JSON.parse(fs.readFileSync(MCP_TOKENS_PATH, 'utf8'));
|
|
514
|
+
} catch { return {}; }
|
|
515
|
+
}
|
|
516
|
+
function saveMcpTokens(tokens) {
|
|
517
|
+
try {
|
|
518
|
+
fs.mkdirSync(path.dirname(MCP_TOKENS_PATH), { recursive: true });
|
|
519
|
+
fs.writeFileSync(MCP_TOKENS_PATH, JSON.stringify(tokens, null, 2), 'utf8');
|
|
520
|
+
} catch (err) { log('[mcp-tokens] save failed:', err.message); }
|
|
521
|
+
}
|
|
522
|
+
const mcpTokens = loadMcpTokens(); // { manus: 'tok_...', claude: 'tok_...', ... }
|
|
523
|
+
|
|
524
|
+
const PROVIDER_NAMES = {
|
|
525
|
+
manus: 'Manus', claude: 'Claude', chatgpt: 'ChatGPT',
|
|
526
|
+
gemini: 'Gemini', perplexity: 'Perplexity',
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// Pending OAuth servers: clientId → { server, resolve, reject }
|
|
530
|
+
const _pendingOAuth = new Map();
|
|
531
|
+
let _oauthCallbackPort = 7826;
|
|
532
|
+
|
|
533
|
+
// GET /mcp-oauth-status — return connected providers
|
|
534
|
+
app.get('/mcp-oauth-status', (req, res) => {
|
|
535
|
+
const tokens = {};
|
|
536
|
+
for (const [id, tok] of Object.entries(mcpTokens)) {
|
|
537
|
+
tokens[id] = { connected: true, tokenPrefix: tok.slice(0, 16) + '...' };
|
|
538
|
+
}
|
|
539
|
+
res.json({ ok: true, tokens });
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// POST /mcp-oauth-start — start OAuth flow for a provider
|
|
543
|
+
// Body: { clientId: 'manus' }
|
|
544
|
+
// Opens the consent page in the system browser and waits for the callback.
|
|
545
|
+
app.post('/mcp-oauth-start', async (req, res) => {
|
|
546
|
+
const { clientId } = req.body || {};
|
|
547
|
+
if (!clientId) return res.status(400).json({ ok: false, error: 'Missing clientId' });
|
|
548
|
+
// Close any existing pending server for this client
|
|
549
|
+
if (_pendingOAuth.has(clientId)) {
|
|
550
|
+
try { _pendingOAuth.get(clientId).server.close(); } catch {}
|
|
551
|
+
_pendingOAuth.delete(clientId);
|
|
552
|
+
}
|
|
553
|
+
const cbPort = _oauthCallbackPort++;
|
|
554
|
+
const redirectUri = `http://localhost:${cbPort}/callback`;
|
|
555
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
556
|
+
const authorizeUrl = new NodeURL(`${apiBase}/api/mcp/oauth/authorize`);
|
|
557
|
+
authorizeUrl.searchParams.set('client_id', clientId);
|
|
558
|
+
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
|
559
|
+
authorizeUrl.searchParams.set('state', state);
|
|
560
|
+
authorizeUrl.searchParams.set('response_type', 'code');
|
|
561
|
+
if (authToken) authorizeUrl.searchParams.set('bridge_token', authToken);
|
|
562
|
+
const successHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Connected</title>
|
|
563
|
+
<style>body{font-family:system-ui,sans-serif;background:#0f0f14;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;flex-direction:column;gap:16px;}
|
|
564
|
+
.check{width:56px;height:56px;background:rgba(139,92,246,.15);border:2px solid rgba(139,92,246,.4);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;}
|
|
565
|
+
h1{font-size:20px;font-weight:700;margin:0;}p{font-size:13px;color:rgba(255,255,255,.45);margin:0;text-align:center;}</style></head>
|
|
566
|
+
<body><div class="check">✓</div><h1>Connected to Promethios!</h1><p>Authorization complete.<br>You can close this tab and return to the overlay.</p></body></html>`;
|
|
567
|
+
const errorHtml = (msg) => `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Failed</title>
|
|
568
|
+
<style>body{font-family:system-ui,sans-serif;background:#0f0f14;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;flex-direction:column;gap:16px;}
|
|
569
|
+
.x{width:56px;height:56px;background:rgba(239,68,68,.1);border:2px solid rgba(239,68,68,.3);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;color:#ef4444;}
|
|
570
|
+
h1{font-size:20px;font-weight:700;margin:0;}p{font-size:13px;color:rgba(255,255,255,.45);margin:0;text-align:center;}</style></head>
|
|
571
|
+
<body><div class="x">✗</div><h1>Connection Failed</h1><p>${msg}<br>Please close this tab and try again.</p></body></html>`;
|
|
572
|
+
const server = http.createServer(async (cbReq, cbRes) => {
|
|
573
|
+
const reqUrl = new NodeURL(cbReq.url, `http://localhost:${cbPort}`);
|
|
574
|
+
if (reqUrl.pathname !== '/callback') { cbRes.writeHead(404); cbRes.end('Not found'); return; }
|
|
575
|
+
const code = reqUrl.searchParams.get('code');
|
|
576
|
+
const retState = reqUrl.searchParams.get('state');
|
|
577
|
+
const token = reqUrl.searchParams.get('token') || reqUrl.searchParams.get('access_token');
|
|
578
|
+
const error = reqUrl.searchParams.get('error');
|
|
579
|
+
try { server.close(); } catch {}
|
|
580
|
+
_pendingOAuth.delete(clientId);
|
|
581
|
+
if (error) {
|
|
582
|
+
cbRes.writeHead(200, { 'Content-Type': 'text/html' }); cbRes.end(errorHtml(error));
|
|
583
|
+
return res.json({ ok: false, error: `Authorization denied: ${error}` });
|
|
584
|
+
}
|
|
585
|
+
if (token) {
|
|
586
|
+
cbRes.writeHead(200, { 'Content-Type': 'text/html' }); cbRes.end(successHtml);
|
|
587
|
+
mcpTokens[clientId] = token;
|
|
588
|
+
saveMcpTokens(mcpTokens);
|
|
589
|
+
return res.json({ ok: true, providerName: PROVIDER_NAMES[clientId] || clientId });
|
|
590
|
+
}
|
|
591
|
+
if (!code) {
|
|
592
|
+
cbRes.writeHead(400, { 'Content-Type': 'text/html' }); cbRes.end(errorHtml('No authorization code received'));
|
|
593
|
+
return res.json({ ok: false, error: 'No authorization code received' });
|
|
594
|
+
}
|
|
595
|
+
if (retState !== state) {
|
|
596
|
+
cbRes.writeHead(400, { 'Content-Type': 'text/html' }); cbRes.end(errorHtml('State mismatch'));
|
|
597
|
+
return res.json({ ok: false, error: 'State mismatch — possible CSRF' });
|
|
598
|
+
}
|
|
599
|
+
cbRes.writeHead(200, { 'Content-Type': 'text/html' }); cbRes.end(successHtml);
|
|
600
|
+
try {
|
|
601
|
+
const tokenRes = await fetch(`${apiBase}/api/mcp/oauth/token`, {
|
|
602
|
+
method: 'POST',
|
|
603
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
604
|
+
body: new URLSearchParams({
|
|
605
|
+
grant_type: 'authorization_code',
|
|
606
|
+
code,
|
|
607
|
+
client_id: clientId,
|
|
608
|
+
redirect_uri: redirectUri,
|
|
609
|
+
}).toString(),
|
|
610
|
+
});
|
|
611
|
+
const tokenData = await tokenRes.json();
|
|
612
|
+
if (!tokenRes.ok || !tokenData.access_token) {
|
|
613
|
+
const msg = tokenData.error_description || tokenData.error || 'Token exchange failed';
|
|
614
|
+
return res.json({ ok: false, error: msg });
|
|
615
|
+
}
|
|
616
|
+
const tok = tokenData.access_token;
|
|
617
|
+
mcpTokens[clientId] = tok;
|
|
618
|
+
saveMcpTokens(mcpTokens);
|
|
619
|
+
res.json({ ok: true, providerName: PROVIDER_NAMES[clientId] || clientId });
|
|
620
|
+
} catch (err) {
|
|
621
|
+
res.json({ ok: false, error: err.message });
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
server.on('error', (err) => {
|
|
625
|
+
_pendingOAuth.delete(clientId);
|
|
626
|
+
res.json({ ok: false, error: 'OAuth callback server error: ' + err.message });
|
|
627
|
+
});
|
|
628
|
+
_pendingOAuth.set(clientId, { server });
|
|
629
|
+
server.listen(cbPort, '127.0.0.1', () => {
|
|
630
|
+
// Open system browser
|
|
631
|
+
const { exec } = require('child_process');
|
|
632
|
+
const platform = process.platform;
|
|
633
|
+
const url = authorizeUrl.toString();
|
|
634
|
+
let cmd;
|
|
635
|
+
if (platform === 'win32') cmd = `start "" "${url}"`;
|
|
636
|
+
else if (platform === 'darwin') cmd = `open "${url}"`;
|
|
637
|
+
else cmd = `xdg-open "${url}"`;
|
|
638
|
+
exec(cmd, (err) => { if (err) log('[mcp-oauth] open browser failed:', err.message); });
|
|
639
|
+
log('[mcp-oauth] Opened browser for', clientId, '→', url);
|
|
640
|
+
// Auto-timeout after 5 minutes
|
|
641
|
+
setTimeout(() => {
|
|
642
|
+
if (_pendingOAuth.has(clientId)) {
|
|
643
|
+
try { server.close(); } catch {}
|
|
644
|
+
_pendingOAuth.delete(clientId);
|
|
645
|
+
if (!res.headersSent) res.json({ ok: false, error: 'Authorization timed out — please try again' });
|
|
646
|
+
}
|
|
647
|
+
}, 5 * 60 * 1000);
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// POST /mcp-oauth-disconnect — remove stored token for a provider
|
|
652
|
+
app.post('/mcp-oauth-disconnect', async (req, res) => {
|
|
653
|
+
const { clientId } = req.body || {};
|
|
654
|
+
if (!clientId) return res.status(400).json({ ok: false, error: 'Missing clientId' });
|
|
655
|
+
const tok = mcpTokens[clientId];
|
|
656
|
+
if (tok) {
|
|
657
|
+
// Best-effort server-side revocation
|
|
658
|
+
try {
|
|
659
|
+
await fetch(`${apiBase}/api/mcp/oauth/revoke`, {
|
|
660
|
+
method: 'POST',
|
|
661
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
662
|
+
body: new URLSearchParams({ token: tok, client_id: clientId }).toString(),
|
|
663
|
+
});
|
|
664
|
+
} catch {}
|
|
665
|
+
delete mcpTokens[clientId];
|
|
666
|
+
saveMcpTokens(mcpTokens);
|
|
667
|
+
}
|
|
668
|
+
res.json({ ok: true });
|
|
669
|
+
});
|
|
670
|
+
|
|
488
671
|
app.get('/overlay', (req, res) => {
|
|
489
672
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
490
|
-
// NOTE: The overlay HTML talks to /chat-proxy, /models-proxy, /set-model-proxy
|
|
673
|
+
// NOTE: The overlay HTML talks to /chat-proxy, /models-proxy, /set-model-proxy,
|
|
674
|
+
// /mcp-oauth-start, /mcp-oauth-status, /mcp-oauth-disconnect
|
|
491
675
|
// (all same origin, no CORS). It never calls the remote API directly.
|
|
492
676
|
res.send(`<!DOCTYPE html>
|
|
493
677
|
<html lang="en">
|
|
@@ -533,7 +717,29 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
533
717
|
background: #1e1030; border: 1px solid #3b1f6b;
|
|
534
718
|
border-radius: 4px; padding: 1px 6px;
|
|
535
719
|
}
|
|
536
|
-
/* ──
|
|
720
|
+
/* ── Tab bar ── */
|
|
721
|
+
#tab-bar {
|
|
722
|
+
display: flex;
|
|
723
|
+
background: #111113;
|
|
724
|
+
border-bottom: 1px solid #1f1f23;
|
|
725
|
+
flex-shrink: 0;
|
|
726
|
+
}
|
|
727
|
+
.tab-btn {
|
|
728
|
+
flex: 1;
|
|
729
|
+
padding: 9px 0;
|
|
730
|
+
font-size: 12px;
|
|
731
|
+
font-weight: 500;
|
|
732
|
+
color: #52525b;
|
|
733
|
+
background: none;
|
|
734
|
+
border: none;
|
|
735
|
+
border-bottom: 2px solid transparent;
|
|
736
|
+
cursor: pointer;
|
|
737
|
+
transition: color 0.15s, border-color 0.15s;
|
|
738
|
+
letter-spacing: 0.01em;
|
|
739
|
+
}
|
|
740
|
+
.tab-btn:hover { color: #a1a1aa; }
|
|
741
|
+
.tab-btn.active { color: #a855f7; border-bottom-color: #7c3aed; }
|
|
742
|
+
/* ── Model selector (in header) ── */
|
|
537
743
|
#model-wrap {
|
|
538
744
|
margin-left: auto;
|
|
539
745
|
position: relative;
|
|
@@ -602,7 +808,10 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
602
808
|
background: #22c55e; flex-shrink: 0;
|
|
603
809
|
box-shadow: 0 0 6px #22c55e88;
|
|
604
810
|
}
|
|
605
|
-
/* ──
|
|
811
|
+
/* ── Tab panels ── */
|
|
812
|
+
.tab-panel { display: none; flex: 1; flex-direction: column; overflow: hidden; }
|
|
813
|
+
.tab-panel.active { display: flex; }
|
|
814
|
+
/* ── Chat panel ── */
|
|
606
815
|
#messages {
|
|
607
816
|
flex: 1;
|
|
608
817
|
overflow-y: auto;
|
|
@@ -701,6 +910,143 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
701
910
|
}
|
|
702
911
|
#send:hover { background: #6d28d9; }
|
|
703
912
|
#send:disabled { background: #27272a; cursor: not-allowed; }
|
|
913
|
+
/* ── Providers tab ── */
|
|
914
|
+
#providers-panel {
|
|
915
|
+
flex: 1;
|
|
916
|
+
overflow-y: auto;
|
|
917
|
+
padding: 14px 12px;
|
|
918
|
+
display: flex;
|
|
919
|
+
flex-direction: column;
|
|
920
|
+
gap: 8px;
|
|
921
|
+
}
|
|
922
|
+
#providers-panel::-webkit-scrollbar { width: 4px; }
|
|
923
|
+
#providers-panel::-webkit-scrollbar-track { background: transparent; }
|
|
924
|
+
#providers-panel::-webkit-scrollbar-thumb { background: #27272a; border-radius: 2px; }
|
|
925
|
+
.providers-heading {
|
|
926
|
+
font-size: 11px;
|
|
927
|
+
font-weight: 600;
|
|
928
|
+
color: #52525b;
|
|
929
|
+
text-transform: uppercase;
|
|
930
|
+
letter-spacing: 0.07em;
|
|
931
|
+
padding: 4px 2px 8px;
|
|
932
|
+
}
|
|
933
|
+
.provider-row {
|
|
934
|
+
background: #111113;
|
|
935
|
+
border: 1px solid #1f1f23;
|
|
936
|
+
border-radius: 12px;
|
|
937
|
+
padding: 12px 14px;
|
|
938
|
+
display: flex;
|
|
939
|
+
align-items: center;
|
|
940
|
+
gap: 12px;
|
|
941
|
+
transition: border-color 0.15s;
|
|
942
|
+
}
|
|
943
|
+
.provider-row:hover { border-color: #27272a; }
|
|
944
|
+
.provider-row.connected { border-color: #1a3a2a; background: #0d1f17; }
|
|
945
|
+
.provider-icon {
|
|
946
|
+
font-size: 22px;
|
|
947
|
+
width: 36px; height: 36px;
|
|
948
|
+
display: flex; align-items: center; justify-content: center;
|
|
949
|
+
flex-shrink: 0;
|
|
950
|
+
background: #18181b;
|
|
951
|
+
border-radius: 8px;
|
|
952
|
+
border: 1px solid #27272a;
|
|
953
|
+
}
|
|
954
|
+
.provider-info { flex: 1; min-width: 0; }
|
|
955
|
+
.provider-name {
|
|
956
|
+
font-size: 13px;
|
|
957
|
+
font-weight: 600;
|
|
958
|
+
color: #e4e4e7;
|
|
959
|
+
margin-bottom: 2px;
|
|
960
|
+
}
|
|
961
|
+
.provider-desc {
|
|
962
|
+
font-size: 11px;
|
|
963
|
+
color: #52525b;
|
|
964
|
+
white-space: nowrap;
|
|
965
|
+
overflow: hidden;
|
|
966
|
+
text-overflow: ellipsis;
|
|
967
|
+
}
|
|
968
|
+
.provider-desc.connected-text { color: #22c55e; }
|
|
969
|
+
.provider-actions {
|
|
970
|
+
display: flex;
|
|
971
|
+
gap: 6px;
|
|
972
|
+
flex-shrink: 0;
|
|
973
|
+
}
|
|
974
|
+
.btn-open {
|
|
975
|
+
background: #18181b;
|
|
976
|
+
border: 1px solid #27272a;
|
|
977
|
+
border-radius: 7px;
|
|
978
|
+
color: #a1a1aa;
|
|
979
|
+
font-size: 11px;
|
|
980
|
+
font-weight: 500;
|
|
981
|
+
padding: 5px 10px;
|
|
982
|
+
cursor: pointer;
|
|
983
|
+
white-space: nowrap;
|
|
984
|
+
transition: border-color 0.15s, color 0.15s;
|
|
985
|
+
text-decoration: none;
|
|
986
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
987
|
+
}
|
|
988
|
+
.btn-open:hover { border-color: #7c3aed; color: #e4e4e7; }
|
|
989
|
+
.btn-connect {
|
|
990
|
+
background: #1e1030;
|
|
991
|
+
border: 1px solid #3b1f6b;
|
|
992
|
+
border-radius: 7px;
|
|
993
|
+
color: #a855f7;
|
|
994
|
+
font-size: 11px;
|
|
995
|
+
font-weight: 500;
|
|
996
|
+
padding: 5px 10px;
|
|
997
|
+
cursor: pointer;
|
|
998
|
+
white-space: nowrap;
|
|
999
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s, opacity 0.15s;
|
|
1000
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1001
|
+
}
|
|
1002
|
+
.btn-connect:hover:not(:disabled) { background: #2d1a4a; border-color: #7c3aed; }
|
|
1003
|
+
.btn-connect:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
1004
|
+
.btn-connect.connected-state {
|
|
1005
|
+
background: #0d1f17;
|
|
1006
|
+
border-color: #1a3a2a;
|
|
1007
|
+
color: #22c55e;
|
|
1008
|
+
}
|
|
1009
|
+
.btn-connect.connecting { opacity: 0.7; cursor: wait; }
|
|
1010
|
+
.connect-dot {
|
|
1011
|
+
width: 6px; height: 6px; border-radius: 50%;
|
|
1012
|
+
background: currentColor;
|
|
1013
|
+
display: inline-block;
|
|
1014
|
+
}
|
|
1015
|
+
.providers-footer {
|
|
1016
|
+
margin-top: 8px;
|
|
1017
|
+
padding: 10px 12px;
|
|
1018
|
+
background: #0d0d10;
|
|
1019
|
+
border: 1px solid #1f1f23;
|
|
1020
|
+
border-radius: 10px;
|
|
1021
|
+
font-size: 11px;
|
|
1022
|
+
color: #3f3f46;
|
|
1023
|
+
line-height: 1.6;
|
|
1024
|
+
}
|
|
1025
|
+
.providers-footer a { color: #7c3aed; text-decoration: none; }
|
|
1026
|
+
.providers-footer a:hover { text-decoration: underline; }
|
|
1027
|
+
/* ── Toast notification ── */
|
|
1028
|
+
#toast {
|
|
1029
|
+
position: fixed;
|
|
1030
|
+
bottom: 16px;
|
|
1031
|
+
left: 50%;
|
|
1032
|
+
transform: translateX(-50%) translateY(20px);
|
|
1033
|
+
background: #1a1a24;
|
|
1034
|
+
border: 1px solid #2a2a3a;
|
|
1035
|
+
border-radius: 10px;
|
|
1036
|
+
padding: 9px 16px;
|
|
1037
|
+
font-size: 12px;
|
|
1038
|
+
color: #e4e4e7;
|
|
1039
|
+
z-index: 9999;
|
|
1040
|
+
opacity: 0;
|
|
1041
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
1042
|
+
pointer-events: none;
|
|
1043
|
+
white-space: nowrap;
|
|
1044
|
+
max-width: 90vw;
|
|
1045
|
+
text-align: center;
|
|
1046
|
+
}
|
|
1047
|
+
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
1048
|
+
#toast.error { border-color: #7f1d1d; background: #1a0d0d; color: #fca5a5; }
|
|
1049
|
+
#toast.success { border-color: #14532d; background: #0d1f17; color: #86efac; }
|
|
704
1050
|
</style>
|
|
705
1051
|
</head>
|
|
706
1052
|
<body>
|
|
@@ -714,19 +1060,38 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
714
1060
|
</div>
|
|
715
1061
|
<div id="status-dot"></div>
|
|
716
1062
|
</div>
|
|
717
|
-
<div id="
|
|
718
|
-
<
|
|
1063
|
+
<div id="tab-bar">
|
|
1064
|
+
<button class="tab-btn active" data-tab="chat">Chat</button>
|
|
1065
|
+
<button class="tab-btn" data-tab="providers">Providers</button>
|
|
719
1066
|
</div>
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
<
|
|
1067
|
+
<!-- Chat tab -->
|
|
1068
|
+
<div id="tab-chat" class="tab-panel active">
|
|
1069
|
+
<div id="messages">
|
|
1070
|
+
<div class="msg system">Your computer is connected. Ask me anything.</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
<div id="input-row">
|
|
1073
|
+
<textarea id="input" placeholder="Ask Promethios..." rows="1"></textarea>
|
|
1074
|
+
<button id="send" title="Send (Enter)">▲</button>
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
<!-- Providers tab -->
|
|
1078
|
+
<div id="tab-providers" class="tab-panel">
|
|
1079
|
+
<div id="providers-panel">
|
|
1080
|
+
<div class="providers-heading">AI Platforms</div>
|
|
1081
|
+
<!-- Rows injected by JS -->
|
|
1082
|
+
<div id="provider-list"></div>
|
|
1083
|
+
<div class="providers-footer">
|
|
1084
|
+
Connect an AI platform to give it access to your local tools via MCP.<br>
|
|
1085
|
+
Click <strong>Open ↗</strong> to open the platform, then <strong>Connect MCP</strong> to authorize.
|
|
1086
|
+
</div>
|
|
1087
|
+
</div>
|
|
723
1088
|
</div>
|
|
1089
|
+
<div id="toast"></div>
|
|
724
1090
|
<script>
|
|
725
1091
|
const BASE = 'http://127.0.0.1:${port}';
|
|
726
1092
|
const PROXY = BASE + '/chat-proxy';
|
|
727
1093
|
const MODELS_URL = BASE + '/models-proxy';
|
|
728
1094
|
const SET_MODEL = BASE + '/set-model-proxy';
|
|
729
|
-
|
|
730
1095
|
const messagesEl = document.getElementById('messages');
|
|
731
1096
|
const inputEl = document.getElementById('input');
|
|
732
1097
|
const sendBtn = document.getElementById('send');
|
|
@@ -734,19 +1099,15 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
734
1099
|
const modelBtn = document.getElementById('model-btn');
|
|
735
1100
|
const modelLabel = document.getElementById('model-label');
|
|
736
1101
|
const modelDropdown = document.getElementById('model-dropdown');
|
|
737
|
-
|
|
738
1102
|
// ── Conversation history (multi-turn context) ──────────────────────────────
|
|
739
1103
|
const conversationHistory = [];
|
|
740
|
-
|
|
741
1104
|
// ── Current model state ────────────────────────────────────────────────────
|
|
742
1105
|
let currentModel = { provider: null, modelId: null, modelName: null };
|
|
743
|
-
|
|
744
1106
|
// ── Status helpers ─────────────────────────────────────────────────────────
|
|
745
1107
|
function setStatus(ok) {
|
|
746
1108
|
statusDot.style.background = ok ? '#22c55e' : '#f59e0b';
|
|
747
1109
|
statusDot.style.boxShadow = ok ? '0 0 6px #22c55e88' : '0 0 6px #f59e0b88';
|
|
748
1110
|
}
|
|
749
|
-
|
|
750
1111
|
function addMsg(role, text) {
|
|
751
1112
|
const d = document.createElement('div');
|
|
752
1113
|
d.className = 'msg ' + role;
|
|
@@ -755,20 +1116,34 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
755
1116
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
756
1117
|
return d;
|
|
757
1118
|
}
|
|
758
|
-
|
|
1119
|
+
// ── Toast ──────────────────────────────────────────────────────────────────
|
|
1120
|
+
let _toastTimer = null;
|
|
1121
|
+
function showToast(msg, type = '') {
|
|
1122
|
+
const el = document.getElementById('toast');
|
|
1123
|
+
el.textContent = msg;
|
|
1124
|
+
el.className = 'show' + (type ? ' ' + type : '');
|
|
1125
|
+
clearTimeout(_toastTimer);
|
|
1126
|
+
_toastTimer = setTimeout(() => { el.className = ''; }, 3200);
|
|
1127
|
+
}
|
|
1128
|
+
// ── Tab switching ──────────────────────────────────────────────────────────
|
|
1129
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
1130
|
+
btn.addEventListener('click', () => {
|
|
1131
|
+
const tab = btn.dataset.tab;
|
|
1132
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b === btn));
|
|
1133
|
+
document.querySelectorAll('.tab-panel').forEach(p => p.classList.toggle('active', p.id === 'tab-' + tab));
|
|
1134
|
+
if (tab === 'providers') loadProviders();
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
759
1137
|
// ── Model switcher ─────────────────────────────────────────────────────────
|
|
760
1138
|
async function loadModels() {
|
|
761
1139
|
try {
|
|
762
1140
|
const res = await fetch(MODELS_URL);
|
|
763
1141
|
if (!res.ok) { modelLabel.textContent = 'No model'; return; }
|
|
764
1142
|
const data = await res.json();
|
|
765
|
-
// data = { current: { provider, modelId, modelName } | null, groups: [{ provider, label, models }] }
|
|
766
|
-
|
|
767
1143
|
if (data.current) {
|
|
768
1144
|
currentModel = { provider: data.current.provider, modelId: data.current.modelId, modelName: data.current.modelName };
|
|
769
1145
|
modelLabel.textContent = data.current.modelName || data.current.modelId;
|
|
770
1146
|
} else if (data.groups?.length) {
|
|
771
|
-
// Pick first available model as default display
|
|
772
1147
|
const first = data.groups[0];
|
|
773
1148
|
const firstModel = first.models?.[0];
|
|
774
1149
|
if (firstModel) {
|
|
@@ -778,8 +1153,6 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
778
1153
|
} else {
|
|
779
1154
|
modelLabel.textContent = 'No API key';
|
|
780
1155
|
}
|
|
781
|
-
|
|
782
|
-
// Build dropdown
|
|
783
1156
|
modelDropdown.innerHTML = '';
|
|
784
1157
|
if (!data.groups?.length) {
|
|
785
1158
|
modelDropdown.innerHTML = '<div class="model-option" style="color:#52525b;cursor:default">No API keys configured</div>';
|
|
@@ -799,67 +1172,183 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
799
1172
|
modelDropdown.appendChild(opt);
|
|
800
1173
|
}
|
|
801
1174
|
}
|
|
802
|
-
} catch
|
|
803
|
-
modelLabel.textContent = 'Error';
|
|
804
|
-
}
|
|
1175
|
+
} catch { modelLabel.textContent = 'Error'; }
|
|
805
1176
|
}
|
|
806
|
-
|
|
807
1177
|
async function selectModel(provider, modelId, modelName) {
|
|
808
|
-
currentModel = { provider, modelId, modelName };
|
|
809
|
-
modelLabel.textContent = modelName;
|
|
810
|
-
modelDropdown.classList.remove('open');
|
|
811
|
-
// Persist to Promethios (syncs with main UI)
|
|
812
1178
|
try {
|
|
813
|
-
await fetch(SET_MODEL, {
|
|
1179
|
+
const res = await fetch(SET_MODEL, {
|
|
814
1180
|
method: 'POST',
|
|
815
1181
|
headers: { 'Content-Type': 'application/json' },
|
|
816
|
-
body: JSON.stringify({ provider, modelId
|
|
1182
|
+
body: JSON.stringify({ provider, modelId }),
|
|
817
1183
|
});
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1184
|
+
if (res.ok) {
|
|
1185
|
+
currentModel = { provider, modelId, modelName };
|
|
1186
|
+
modelLabel.textContent = modelName;
|
|
1187
|
+
modelDropdown.classList.remove('open');
|
|
1188
|
+
await loadModels();
|
|
1189
|
+
}
|
|
1190
|
+
} catch {}
|
|
821
1191
|
}
|
|
822
|
-
|
|
823
1192
|
modelBtn.addEventListener('click', (e) => {
|
|
824
1193
|
e.stopPropagation();
|
|
825
1194
|
modelDropdown.classList.toggle('open');
|
|
826
1195
|
});
|
|
827
1196
|
document.addEventListener('click', () => modelDropdown.classList.remove('open'));
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1197
|
+
// ── Providers tab ──────────────────────────────────────────────────────────
|
|
1198
|
+
const PROVIDERS = [
|
|
1199
|
+
{ id: 'manus', name: 'Manus', icon: '🤖', desc: 'AI agent platform', url: 'https://manus.im' },
|
|
1200
|
+
{ id: 'claude', name: 'Claude', icon: '🧠', desc: 'Anthropic AI assistant', url: 'https://claude.ai' },
|
|
1201
|
+
{ id: 'chatgpt', name: 'ChatGPT', icon: '💬', desc: 'OpenAI ChatGPT', url: 'https://chatgpt.com' },
|
|
1202
|
+
{ id: 'gemini', name: 'Gemini', icon: '✨', desc: 'Google Gemini', url: 'https://gemini.google.com' },
|
|
1203
|
+
{ id: 'perplexity', name: 'Perplexity', icon: '🔍', desc: 'Perplexity AI', url: 'https://perplexity.ai' },
|
|
1204
|
+
];
|
|
1205
|
+
// Track which providers the user has opened (enables Connect MCP button)
|
|
1206
|
+
const openedProviders = new Set(JSON.parse(localStorage.getItem('prom_opened') || '[]'));
|
|
1207
|
+
// Track connected providers (from server)
|
|
1208
|
+
let connectedProviders = {};
|
|
1209
|
+
// Track in-progress connections
|
|
1210
|
+
const connectingProviders = new Set();
|
|
1211
|
+
async function loadProviders() {
|
|
1212
|
+
try {
|
|
1213
|
+
const res = await fetch(BASE + '/mcp-oauth-status');
|
|
1214
|
+
if (res.ok) {
|
|
1215
|
+
const data = await res.json();
|
|
1216
|
+
connectedProviders = data.tokens || {};
|
|
1217
|
+
}
|
|
1218
|
+
} catch {}
|
|
1219
|
+
renderProviders();
|
|
1220
|
+
}
|
|
1221
|
+
function renderProviders() {
|
|
1222
|
+
const list = document.getElementById('provider-list');
|
|
1223
|
+
list.innerHTML = '';
|
|
1224
|
+
for (const p of PROVIDERS) {
|
|
1225
|
+
const isConnected = !!connectedProviders[p.id];
|
|
1226
|
+
const isOpened = openedProviders.has(p.id);
|
|
1227
|
+
const isConnecting = connectingProviders.has(p.id);
|
|
1228
|
+
const row = document.createElement('div');
|
|
1229
|
+
row.className = 'provider-row' + (isConnected ? ' connected' : '');
|
|
1230
|
+
row.dataset.id = p.id;
|
|
1231
|
+
const connectLabel = isConnected ? '✓ Connected' : (isConnecting ? 'Connecting...' : 'Connect MCP');
|
|
1232
|
+
const connectClass = 'btn-connect' +
|
|
1233
|
+
(isConnected ? ' connected-state' : '') +
|
|
1234
|
+
(isConnecting ? ' connecting' : '');
|
|
1235
|
+
row.innerHTML =
|
|
1236
|
+
'<div class="provider-icon">' + p.icon + '</div>' +
|
|
1237
|
+
'<div class="provider-info">' +
|
|
1238
|
+
'<div class="provider-name">' + p.name + '</div>' +
|
|
1239
|
+
'<div class="provider-desc' + (isConnected ? ' connected-text' : '') + '">' +
|
|
1240
|
+
(isConnected ? 'MCP connected · tools available' : p.desc) +
|
|
1241
|
+
'</div>' +
|
|
1242
|
+
'</div>' +
|
|
1243
|
+
'<div class="provider-actions">' +
|
|
1244
|
+
'<button class="btn-open" data-url="' + p.url + '" data-id="' + p.id + '">Open ↗</button>' +
|
|
1245
|
+
'<button class="' + connectClass + '"' +
|
|
1246
|
+
(!isOpened && !isConnected ? ' disabled' : '') +
|
|
1247
|
+
(isConnecting ? ' disabled' : '') +
|
|
1248
|
+
' data-id="' + p.id + '">' +
|
|
1249
|
+
connectLabel +
|
|
1250
|
+
'</button>' +
|
|
1251
|
+
'</div>';
|
|
1252
|
+
list.appendChild(row);
|
|
1253
|
+
}
|
|
1254
|
+
// Attach event listeners
|
|
1255
|
+
list.querySelectorAll('.btn-open').forEach(btn => {
|
|
1256
|
+
btn.addEventListener('click', () => {
|
|
1257
|
+
const id = btn.dataset.id;
|
|
1258
|
+
const url = btn.dataset.url;
|
|
1259
|
+
// Mark as opened so Connect MCP becomes enabled
|
|
1260
|
+
openedProviders.add(id);
|
|
1261
|
+
localStorage.setItem('prom_opened', JSON.stringify([...openedProviders]));
|
|
1262
|
+
// Open via bridge's open-external endpoint (uses system default browser)
|
|
1263
|
+
fetch(BASE + '/open-external', {
|
|
1264
|
+
method: 'POST',
|
|
1265
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1266
|
+
body: JSON.stringify({ url }),
|
|
1267
|
+
}).catch(() => {
|
|
1268
|
+
// Fallback: open directly
|
|
1269
|
+
window.open(url, '_blank');
|
|
1270
|
+
});
|
|
1271
|
+
renderProviders();
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
list.querySelectorAll('.btn-connect').forEach(btn => {
|
|
1275
|
+
if (btn.disabled) return;
|
|
1276
|
+
const id = btn.dataset.id;
|
|
1277
|
+
if (btn.classList.contains('connected-state')) {
|
|
1278
|
+
// Disconnect
|
|
1279
|
+
btn.addEventListener('click', () => disconnectProvider(id));
|
|
1280
|
+
} else {
|
|
1281
|
+
btn.addEventListener('click', () => connectProvider(id));
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
async function connectProvider(id) {
|
|
1286
|
+
if (connectingProviders.has(id)) return;
|
|
1287
|
+
connectingProviders.add(id);
|
|
1288
|
+
renderProviders();
|
|
1289
|
+
showToast('Opening browser for authorization...', '');
|
|
1290
|
+
try {
|
|
1291
|
+
const res = await fetch(BASE + '/mcp-oauth-start', {
|
|
1292
|
+
method: 'POST',
|
|
1293
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1294
|
+
body: JSON.stringify({ clientId: id }),
|
|
1295
|
+
});
|
|
1296
|
+
const data = await res.json();
|
|
1297
|
+
if (data.ok) {
|
|
1298
|
+
connectedProviders[id] = { connected: true };
|
|
1299
|
+
showToast(data.providerName + ' connected!', 'success');
|
|
1300
|
+
} else {
|
|
1301
|
+
showToast('Connection failed: ' + (data.error || 'Unknown error'), 'error');
|
|
1302
|
+
}
|
|
1303
|
+
} catch (e) {
|
|
1304
|
+
showToast('Connection error: ' + e.message, 'error');
|
|
1305
|
+
}
|
|
1306
|
+
connectingProviders.delete(id);
|
|
1307
|
+
renderProviders();
|
|
1308
|
+
}
|
|
1309
|
+
async function disconnectProvider(id) {
|
|
1310
|
+
try {
|
|
1311
|
+
const res = await fetch(BASE + '/mcp-oauth-disconnect', {
|
|
1312
|
+
method: 'POST',
|
|
1313
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1314
|
+
body: JSON.stringify({ clientId: id }),
|
|
1315
|
+
});
|
|
1316
|
+
const data = await res.json();
|
|
1317
|
+
if (data.ok) {
|
|
1318
|
+
delete connectedProviders[id];
|
|
1319
|
+
showToast('Disconnected', '');
|
|
1320
|
+
renderProviders();
|
|
1321
|
+
}
|
|
1322
|
+
} catch (e) {
|
|
1323
|
+
showToast('Disconnect error: ' + e.message, 'error');
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
// ── Chat send ──────────────────────────────────────────────────────────────
|
|
831
1327
|
async function sendMessage() {
|
|
832
1328
|
const text = inputEl.value.trim();
|
|
833
1329
|
if (!text || sendBtn.disabled) return;
|
|
834
1330
|
inputEl.value = '';
|
|
835
1331
|
inputEl.style.height = 'auto';
|
|
836
|
-
addMsg('user', text);
|
|
837
1332
|
sendBtn.disabled = true;
|
|
838
|
-
|
|
839
|
-
const thinking = addMsg('thinking', '
|
|
1333
|
+
addMsg('user', text);
|
|
1334
|
+
const thinking = addMsg('thinking', 'Thinking…');
|
|
1335
|
+
setStatus(false);
|
|
840
1336
|
try {
|
|
841
1337
|
const res = await fetch(PROXY, {
|
|
842
1338
|
method: 'POST',
|
|
843
1339
|
headers: { 'Content-Type': 'application/json' },
|
|
844
1340
|
body: JSON.stringify({
|
|
845
1341
|
message: text,
|
|
846
|
-
|
|
847
|
-
|
|
1342
|
+
history: conversationHistory,
|
|
1343
|
+
model: currentModel.modelId ? { provider: currentModel.provider, modelId: currentModel.modelId } : undefined,
|
|
848
1344
|
}),
|
|
849
1345
|
});
|
|
850
1346
|
thinking.remove();
|
|
851
1347
|
if (res.ok) {
|
|
852
1348
|
const data = await res.json();
|
|
853
|
-
const reply = data.reply || data.message || JSON.stringify(data);
|
|
1349
|
+
const reply = data.reply || data.content || data.message || JSON.stringify(data);
|
|
854
1350
|
addMsg('ai', reply);
|
|
855
|
-
|
|
856
|
-
if (data.model) {
|
|
857
|
-
const meta = document.createElement('div');
|
|
858
|
-
meta.className = 'msg system';
|
|
859
|
-
meta.textContent = data.provider + ' / ' + data.model;
|
|
860
|
-
messagesEl.appendChild(meta);
|
|
861
|
-
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
862
|
-
}
|
|
1351
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
863
1352
|
conversationHistory.push({ role: 'user', content: text });
|
|
864
1353
|
conversationHistory.push({ role: 'assistant', content: reply });
|
|
865
1354
|
} else {
|
|
@@ -875,7 +1364,6 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
875
1364
|
sendBtn.disabled = false;
|
|
876
1365
|
setStatus(true);
|
|
877
1366
|
}
|
|
878
|
-
|
|
879
1367
|
sendBtn.addEventListener('click', sendMessage);
|
|
880
1368
|
inputEl.addEventListener('keydown', e => {
|
|
881
1369
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
@@ -884,7 +1372,6 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
884
1372
|
inputEl.style.height = 'auto';
|
|
885
1373
|
inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
|
|
886
1374
|
});
|
|
887
|
-
|
|
888
1375
|
// Load models on startup
|
|
889
1376
|
loadModels();
|
|
890
1377
|
<\/script>
|
|
@@ -948,17 +1435,27 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
948
1435
|
overlayAuthToken = authToken;
|
|
949
1436
|
bridgeUsername = require('os').userInfo().username;
|
|
950
1437
|
|
|
951
|
-
// Try Electron first
|
|
952
|
-
//
|
|
1438
|
+
// ── Option C: Try Electron first; only fall back to browser overlay if not installed ──
|
|
1439
|
+
// isDesktopInstalled() scans standard install paths (postinstall dir + OS program dirs).
|
|
1440
|
+
// If the EXE is found, we launch it and skip the browser overlay entirely.
|
|
1441
|
+
// If not found, we open the browser overlay as a fallback.
|
|
953
1442
|
let overlayLaunched = false;
|
|
1443
|
+
|
|
1444
|
+
const desktopInstalled = isDesktopInstalled && isDesktopInstalled();
|
|
1445
|
+
|
|
954
1446
|
if (launchOverlay) {
|
|
955
1447
|
try {
|
|
956
1448
|
const overlayChild = launchOverlay({ authToken, apiBase, dev });
|
|
957
1449
|
if (overlayChild) {
|
|
958
1450
|
overlayLaunched = true;
|
|
959
|
-
console.log(chalk.cyan(' ⬡ Promethios
|
|
1451
|
+
console.log(chalk.cyan(' ⬡ Promethios Desktop launched — floating overlay is ready'));
|
|
960
1452
|
console.log(chalk.gray(' Hotkey: Ctrl+Shift+P (Win/Linux) or Cmd+Shift+P (Mac)'));
|
|
961
1453
|
console.log('');
|
|
1454
|
+
} else if (desktopInstalled) {
|
|
1455
|
+
// findDesktopBinary found a path but spawn returned null — EXE may already be running
|
|
1456
|
+
overlayLaunched = true;
|
|
1457
|
+
console.log(chalk.cyan(' ⬡ Promethios Desktop is running'));
|
|
1458
|
+
console.log('');
|
|
962
1459
|
}
|
|
963
1460
|
} catch (err) {
|
|
964
1461
|
log('Electron overlay launch failed (non-critical):', err.message);
|
|
@@ -966,8 +1463,8 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
966
1463
|
}
|
|
967
1464
|
|
|
968
1465
|
if (!overlayLaunched) {
|
|
969
|
-
// Electron not
|
|
970
|
-
// This is
|
|
1466
|
+
// Electron Desktop not installed — open the lightweight browser overlay instead.
|
|
1467
|
+
// This is the full-featured chat + Providers UI served by the bridge's Express server.
|
|
971
1468
|
const overlayUrl = `http://127.0.0.1:${port}/overlay`;
|
|
972
1469
|
const openedInBrowser = await openInBrowser(overlayUrl, log);
|
|
973
1470
|
if (openedInBrowser) {
|
package/src/overlay/launcher.js
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
* launcher.js — Promethios Bridge
|
|
3
3
|
*
|
|
4
4
|
* Called by bridge.js after successful authentication.
|
|
5
|
-
* Finds the pre-built Promethios Desktop binary
|
|
6
|
-
*
|
|
5
|
+
* Finds the pre-built Promethios Desktop binary and spawns it with auth
|
|
6
|
+
* credentials via env vars.
|
|
7
7
|
*
|
|
8
8
|
* Binary search order:
|
|
9
9
|
* 1. PROMETHIOS_DESKTOP_BIN environment variable (dev override)
|
|
10
10
|
* 2. ~/.promethios/desktop/ — where postinstall.js places the binary
|
|
11
|
-
* 3.
|
|
11
|
+
* 3. Standard OS install paths (Windows: %LOCALAPPDATA%\Programs\Promethios\,
|
|
12
|
+
* macOS: /Applications/Promethios.app, Linux: /opt/Promethios/)
|
|
13
|
+
* 4. Electron source-mode fallback (dev only, requires electron in node_modules)
|
|
12
14
|
*
|
|
13
15
|
* The pre-built binary reads its config from env vars:
|
|
14
16
|
* PROMETHIOS_TOKEN, PROMETHIOS_API_BASE, PROMETHIOS_THREAD_ID, PROMETHIOS_DEV
|
|
@@ -30,18 +32,17 @@ function findDesktopBinary() {
|
|
|
30
32
|
return process.env.PROMETHIOS_DESKTOP_BIN;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
// 2. Scan ~/.promethios/desktop/ for a matching binary
|
|
35
|
+
// 2. Scan ~/.promethios/desktop/ for a matching binary (postinstall download location)
|
|
34
36
|
if (fs.existsSync(INSTALL_DIR)) {
|
|
35
37
|
const files = fs.readdirSync(INSTALL_DIR);
|
|
36
38
|
const exts = process.platform === 'win32' ? ['.exe']
|
|
37
|
-
: process.platform === 'darwin' ? ['.
|
|
39
|
+
: process.platform === 'darwin' ? ['.app']
|
|
38
40
|
: ['.AppImage'];
|
|
39
41
|
|
|
40
42
|
for (const ext of exts) {
|
|
41
|
-
const match = files.find(f => f.endsWith(ext));
|
|
43
|
+
const match = files.find(f => f.endsWith(ext) && !f.includes('Setup'));
|
|
42
44
|
if (match) {
|
|
43
45
|
const full = path.join(INSTALL_DIR, match);
|
|
44
|
-
// Ensure executable bit is set on Unix
|
|
45
46
|
try {
|
|
46
47
|
if (process.platform !== 'win32') fs.chmodSync(full, 0o755);
|
|
47
48
|
} catch {}
|
|
@@ -50,6 +51,67 @@ function findDesktopBinary() {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// 3. Standard OS install paths (when user installed via the installer EXE/DMG)
|
|
55
|
+
if (process.platform === 'win32') {
|
|
56
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
57
|
+
const programFiles = process.env.PROGRAMFILES || 'C:\\Program Files';
|
|
58
|
+
const programFilesX86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';
|
|
59
|
+
|
|
60
|
+
const candidates = [
|
|
61
|
+
// Electron-builder default: %LOCALAPPDATA%\Programs\Promethios\Promethios.exe
|
|
62
|
+
path.join(localAppData, 'Programs', 'Promethios', 'Promethios.exe'),
|
|
63
|
+
path.join(localAppData, 'Programs', 'promethios', 'Promethios.exe'),
|
|
64
|
+
// Per-machine install
|
|
65
|
+
path.join(programFiles, 'Promethios', 'Promethios.exe'),
|
|
66
|
+
path.join(programFilesX86, 'Promethios', 'Promethios.exe'),
|
|
67
|
+
// Alternate casing
|
|
68
|
+
path.join(localAppData, 'Programs', 'Promethios Desktop', 'Promethios Desktop.exe'),
|
|
69
|
+
path.join(localAppData, 'Programs', 'Promethios', 'Promethios Desktop.exe'),
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
for (const c of candidates) {
|
|
73
|
+
if (fs.existsSync(c)) return c;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Scan %LOCALAPPDATA%\Programs\ for any Promethios*.exe
|
|
77
|
+
const programsDir = path.join(localAppData, 'Programs');
|
|
78
|
+
if (fs.existsSync(programsDir)) {
|
|
79
|
+
try {
|
|
80
|
+
const dirs = fs.readdirSync(programsDir);
|
|
81
|
+
for (const dir of dirs) {
|
|
82
|
+
if (dir.toLowerCase().includes('promethios')) {
|
|
83
|
+
const dirPath = path.join(programsDir, dir);
|
|
84
|
+
const files = fs.readdirSync(dirPath);
|
|
85
|
+
const exe = files.find(f => f.endsWith('.exe') && !f.includes('Uninstall') && !f.includes('Update'));
|
|
86
|
+
if (exe) return path.join(dirPath, exe);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
} else if (process.platform === 'darwin') {
|
|
92
|
+
const candidates = [
|
|
93
|
+
'/Applications/Promethios.app/Contents/MacOS/Promethios',
|
|
94
|
+
'/Applications/Promethios Desktop.app/Contents/MacOS/Promethios Desktop',
|
|
95
|
+
path.join(os.homedir(), 'Applications', 'Promethios.app', 'Contents', 'MacOS', 'Promethios'),
|
|
96
|
+
];
|
|
97
|
+
for (const c of candidates) {
|
|
98
|
+
if (fs.existsSync(c)) return c;
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Linux
|
|
102
|
+
const candidates = [
|
|
103
|
+
'/opt/Promethios/promethios',
|
|
104
|
+
'/usr/local/bin/promethios',
|
|
105
|
+
path.join(os.homedir(), '.local', 'share', 'promethios', 'promethios'),
|
|
106
|
+
];
|
|
107
|
+
for (const c of candidates) {
|
|
108
|
+
if (fs.existsSync(c)) {
|
|
109
|
+
try { fs.chmodSync(c, 0o755); } catch {}
|
|
110
|
+
return c;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
53
115
|
return null;
|
|
54
116
|
}
|
|
55
117
|
|
|
@@ -131,4 +193,9 @@ function launchOverlay({ authToken, apiBase = 'https://api.promethios.ai', threa
|
|
|
131
193
|
return child;
|
|
132
194
|
}
|
|
133
195
|
|
|
134
|
-
|
|
196
|
+
// ── Check if Electron is installed (without launching) ───────────────────────
|
|
197
|
+
function isDesktopInstalled() {
|
|
198
|
+
return !!findDesktopBinary();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { launchOverlay, isDesktopInstalled, findDesktopBinary };
|