promethios-bridge 1.9.0 → 2.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promethios-bridge",
3
- "version": "1.9.0",
3
+ "version": "2.0.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": {
@@ -53,7 +53,9 @@
53
53
  },
54
54
  "optionalDependencies": {
55
55
  "playwright": "^1.42.0",
56
- "electron": "^29.0.0"
56
+ "electron": "^29.0.0",
57
+ "screenshot-desktop": "^1.12.7",
58
+ "sharp": "^0.33.0"
57
59
  },
58
60
  "engines": {
59
61
  "node": ">=18.0.0"
package/src/bridge.js CHANGED
@@ -22,7 +22,18 @@ const fetch = require('node-fetch');
22
22
  const { executeLocalTool } = require('./executor');
23
23
  const { captureContext } = require('./contextCapture');
24
24
  const { startMcpServer } = require('./mcp-server');
25
- const { setPinnedRegion, setPinnedApps } = require('./tools/desktop');
25
+ const { setPinnedRegion, setPinnedApps, registerBrowserPageAccessor } = require('./tools/desktop');
26
+
27
+ // Wire the browser-dom tools to the shared Playwright context.
28
+ // The context is created lazily in executor.js when browser_control is first used.
29
+ // We expose a getter so browser-dom tools can access the live page.
30
+ registerBrowserPageAccessor(async () => {
31
+ if (!global.__playwrightContext) {
32
+ throw new Error('Browser not open. Use the browser_control tool to navigate to a page first, then retry.');
33
+ }
34
+ const pages = global.__playwrightContext.pages();
35
+ return pages.length > 0 ? pages[pages.length - 1] : await global.__playwrightContext.newPage();
36
+ });
26
37
  const { initAndroidTools } = require('./tools/android');
27
38
 
28
39
  // Optional: Electron overlay window (bundled in src/overlay — gracefully skipped if Electron not available)
@@ -573,84 +573,110 @@ function openManusBrowser({ mcpPrompt } = {}) {
573
573
  if (DEV) manusBrowserWin.webContents.openDevTools({ mode: 'detach' });
574
574
  }
575
575
 
576
- // ── OAuth popup ───────────────────────────────────────────────────────────────
576
+ // ── OAuth via real system browser + localhost callback server ─────────────────
577
577
  /**
578
- * Open a small BrowserWindow pointing at the Promethios OAuth consent page.
579
- * The consent page at RELAY_URL/api/mcp/oauth/authorize will:
580
- * 1. Show a "Grant access to <provider>" page
581
- * 2. On Allow: redirect to promethios://oauth/callback?token=xxx&provider=yyy
582
- * 3. We intercept that redirect here and forward the result to the renderer
578
+ * Open OAuth in the user's REAL system browser (Chrome/Edge/Safari/Firefox).
579
+ *
580
+ * Why: Electron's Chromium webview is blocked by Google, Meta, and other OAuth
581
+ * providers with "This browser or app may not be secure". The fix:
582
+ * 1. Start a temporary localhost HTTP server on port 7826
583
+ * 2. Build the OAuth URL with redirect_uri=http://localhost:7826/callback
584
+ * 3. Open that URL in the real system browser via shell.openExternal()
585
+ * 4. The relay redirects back to localhost:7826/callback?token=xxx
586
+ * 5. We intercept it, close the server, and deliver the token to the renderer
587
+ * 6. The localhost page shows "You can close this tab" to the user
583
588
  */
589
+ let oauthCallbackServer = null;
590
+ let oauthCallbackPort = 7826;
591
+
584
592
  function openOAuthPopup(provider) {
585
- if (oauthPopup && !oauthPopup.isDestroyed()) {
586
- oauthPopup.focus();
587
- return;
593
+ // Close any existing callback server
594
+ if (oauthCallbackServer) {
595
+ try { oauthCallbackServer.close(); } catch {}
596
+ oauthCallbackServer = null;
588
597
  }
589
598
 
590
- // Include the bridge token so the relay can pre-authenticate the user
591
- // and skip the "Login Required" page — going straight to the consent screen
592
- const tokenParam = AUTH_TOKEN ? `&bridge_token=${encodeURIComponent(AUTH_TOKEN)}` : '';
599
+ const callbackPort = oauthCallbackPort;
600
+ const redirectUri = `http://localhost:${callbackPort}/callback`;
601
+ const tokenParam = AUTH_TOKEN ? `&bridge_token=${encodeURIComponent(AUTH_TOKEN)}` : '';
593
602
  const authUrl = `${RELAY_URL}/api/mcp/oauth/authorize?` +
594
603
  `client_id=${encodeURIComponent(provider)}&` +
595
604
  `response_type=code&` +
596
- `redirect_uri=${encodeURIComponent('promethios://oauth/callback')}${tokenParam}`;
597
-
598
- oauthPopup = new BrowserWindow({
599
- width: OAUTH_W,
600
- height: OAUTH_H,
601
- title: `Connect ${provider} to Promethios`,
602
- frame: true,
603
- modal: false,
604
- parent: overlayWindow,
605
- webPreferences: {
606
- nodeIntegration: false,
607
- contextIsolation: true,
608
- },
609
- });
610
-
611
- oauthPopup.setMenuBarVisibility(false);
612
- oauthPopup.loadURL(authUrl);
605
+ `redirect_uri=${encodeURIComponent(redirectUri)}${tokenParam}`;
606
+
607
+ const successHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Promethios Connected</title>
608
+ <style>*{box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;background:#0f0f14;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;flex-direction:column;gap:16px;}
609
+ .check{width:56px;height:56px;background:rgba(139,92,246,0.15);border:2px solid rgba(139,92,246,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;}
610
+ h1{font-size:20px;font-weight:700;margin:0;}p{font-size:13px;color:rgba(255,255,255,0.45);margin:0;text-align:center;}</style></head>
611
+ <body><div class="check">&#10003;</div><h1>Connected to Promethios!</h1><p>Authorization complete.<br>You can close this tab and return to the overlay.</p></body></html>`;
612
+
613
+ const errorHtml = (msg) => `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Connection Failed</title>
614
+ <style>*{box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;background:#0f0f14;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;flex-direction:column;gap:16px;}
615
+ .x{width:56px;height:56px;background:rgba(239,68,68,0.1);border:2px solid rgba(239,68,68,0.3);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;color:#ef4444;}
616
+ h1{font-size:20px;font-weight:700;margin:0;}p{font-size:13px;color:rgba(255,255,255,0.45);margin:0;text-align:center;}</style></head>
617
+ <body><div class="x">&#10007;</div><h1>Connection Failed</h1><p>${msg}<br>Please close this tab and try again.</p></body></html>`;
618
+
619
+ // Start the localhost callback server
620
+ oauthCallbackServer = http.createServer((req, res) => {
621
+ const reqUrl = new URL(req.url, `http://localhost:${callbackPort}`);
622
+ if (reqUrl.pathname !== '/callback') { res.writeHead(404); res.end('Not found'); return; }
623
+
624
+ const token = reqUrl.searchParams.get('token') || reqUrl.searchParams.get('access_token');
625
+ const code = reqUrl.searchParams.get('code');
626
+ const clientId = reqUrl.searchParams.get('client_id') || provider;
627
+ const error = reqUrl.searchParams.get('error');
613
628
 
614
- // Intercept the custom URI scheme redirect
615
- oauthPopup.webContents.on('will-redirect', (event, url) => {
616
- handleOAuthCallback(url, provider);
617
- });
629
+ if (error) {
630
+ res.writeHead(200, { 'Content-Type': 'text/html' });
631
+ res.end(errorHtml(error));
632
+ if (DEV) console.error(`[OAuth] Error for ${provider}: ${error}`);
633
+ try { oauthCallbackServer.close(); } catch {}
634
+ oauthCallbackServer = null;
635
+ return;
636
+ }
618
637
 
619
- // Also handle navigation (some servers do a full navigate instead of redirect)
620
- oauthPopup.webContents.on('will-navigate', (event, url) => {
621
- if (url.startsWith('promethios://')) {
622
- event.preventDefault();
623
- handleOAuthCallback(url, provider);
638
+ if (token) {
639
+ res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(successHtml);
640
+ deliverOAuthResult({ provider, token });
641
+ try { oauthCallbackServer.close(); } catch {}
642
+ oauthCallbackServer = null;
643
+ } else if (code) {
644
+ res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(successHtml);
645
+ exchangeCodeForToken(code, clientId, provider, redirectUri);
646
+ try { oauthCallbackServer.close(); } catch {}
647
+ oauthCallbackServer = null;
648
+ } else {
649
+ res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(errorHtml('No token received'));
624
650
  }
625
651
  });
626
652
 
627
- // Read auth code from page title after Allow click
628
- // The /approve endpoint sets title to: promethios_oauth_success|code=<code>|client=<id>
629
- oauthPopup.webContents.on('page-title-updated', (_, title) => {
630
- if (title.startsWith('promethios_oauth_success')) {
631
- const codeMatch = title.match(/code=([^|]+)/);
632
- const clientMatch = title.match(/client=([^|]+)/);
633
- if (codeMatch) {
634
- const code = codeMatch[1];
635
- const clientId = clientMatch ? clientMatch[1] : provider;
636
- exchangeCodeForToken(code, clientId, provider);
637
- }
653
+ oauthCallbackServer.on('error', (err) => {
654
+ if (err.code === 'EADDRINUSE') {
655
+ // Port in use — try next port
656
+ oauthCallbackPort++;
657
+ oauthCallbackServer = null;
658
+ openOAuthPopup(provider);
659
+ } else {
660
+ console.error('[OAuth] Callback server error:', err.message);
638
661
  }
639
662
  });
640
663
 
641
- oauthPopup.on('closed', () => {
642
- oauthPopup = null;
664
+ oauthCallbackServer.listen(callbackPort, '127.0.0.1', () => {
665
+ shell.openExternal(authUrl);
666
+ if (DEV) console.log(`[OAuth] Opened system browser for ${provider}: ${authUrl}`);
667
+ // Auto-close after 5 minutes if no callback received
668
+ setTimeout(() => {
669
+ if (oauthCallbackServer) { try { oauthCallbackServer.close(); } catch {} oauthCallbackServer = null; }
670
+ }, 5 * 60 * 1000);
643
671
  });
644
-
645
- if (DEV) console.log(`[OAuth] Opened popup for ${provider}: ${authUrl}`);
646
672
  }
647
673
 
648
674
  /**
649
675
  * Exchange an OAuth auth code for a bridge session token.
650
676
  * Called after the consent page sets its title to promethios_oauth_success|code=...
651
677
  */
652
- async function exchangeCodeForToken(code, clientId, provider) {
653
- const redirectUri = 'promethios://oauth/callback';
678
+ async function exchangeCodeForToken(code, clientId, provider, redirectUri) {
679
+ if (!redirectUri) redirectUri = `http://localhost:${oauthCallbackPort}/callback`;
654
680
  const tokenUrl = `${RELAY_URL}/api/mcp/oauth/token`;
655
681
 
656
682
  try {
@@ -695,12 +721,9 @@ async function exchangeCodeForToken(code, clientId, provider) {
695
721
  deliverOAuthResult({ provider, token: resp.body.access_token });
696
722
  } else {
697
723
  console.error('[OAuth] Token exchange failed:', resp.body);
698
- // Still close the popup — user can try again
699
- if (oauthPopup && !oauthPopup.isDestroyed()) oauthPopup.close();
700
724
  }
701
725
  } catch (e) {
702
726
  console.error('[OAuth] Token exchange error:', e.message);
703
- if (oauthPopup && !oauthPopup.isDestroyed()) oauthPopup.close();
704
727
  }
705
728
  }
706
729
 
@@ -730,12 +753,6 @@ function deliverOAuthResult({ provider, token }) {
730
753
  connectedProviders[provider] = { token, clientId: provider, ts: Date.now() };
731
754
  saveState();
732
755
 
733
- // Close the popup
734
- if (oauthPopup && !oauthPopup.isDestroyed()) {
735
- oauthPopup.close();
736
- oauthPopup = null;
737
- }
738
-
739
756
  // Send result to renderer so it can show the copy-prompt modal
740
757
  if (overlayWindow && !overlayWindow.isDestroyed()) {
741
758
  overlayWindow.webContents.send('oauth-complete', { provider, token, clientId: provider });
@@ -824,9 +824,13 @@
824
824
 
825
825
  <!-- ══ Providers tab ═══════════════════════════════════════════════════ -->
826
826
  <div class="tab-content" id="tab-providers">
827
-
828
827
  <div id="providers-list" style="display:flex;flex-direction:column;gap:10px;padding:14px;overflow-y:auto;flex:1;">
829
-
828
+ <!-- Section header -->
829
+ <div style="font-size:10px;font-weight:700;letter-spacing:0.08em;color:rgba(139,92,246,0.8);text-transform:uppercase;margin-bottom:2px;">AI Providers</div>
830
+ <div style="font-size:10px;color:rgba(255,255,255,0.35);line-height:1.5;margin-bottom:6px;">
831
+ <strong style="color:rgba(255,255,255,0.55)">Open</strong> launches the provider in your real browser.
832
+ <strong style="color:rgba(255,255,255,0.55)">Connect</strong> authorizes MCP — sign in first, then connect.
833
+ </div>
830
834
  <!-- Manus -->
831
835
  <div class="provider-card" data-provider="manus">
832
836
  <div class="provider-icon manus">M</div>
@@ -835,11 +839,10 @@
835
839
  <div class="provider-desc">manus.im — autonomous AI agent</div>
836
840
  </div>
837
841
  <div class="provider-actions">
838
- <button class="btn-open" data-url="https://manus.im">Open ↗</button>
842
+ <button class="btn-open" data-url="https://manus.im" data-provider="manus">Open ↗</button>
839
843
  <button class="btn-connect" data-provider="manus" id="connect-manus">Connect</button>
840
844
  </div>
841
845
  </div>
842
-
843
846
  <!-- Claude -->
844
847
  <div class="provider-card" data-provider="claude">
845
848
  <div class="provider-icon claude">C</div>
@@ -848,11 +851,10 @@
848
851
  <div class="provider-desc">claude.ai — Anthropic's AI assistant</div>
849
852
  </div>
850
853
  <div class="provider-actions">
851
- <button class="btn-open" data-url="https://claude.ai">Open ↗</button>
854
+ <button class="btn-open" data-url="https://claude.ai" data-provider="claude">Open ↗</button>
852
855
  <button class="btn-connect" data-provider="claude" id="connect-claude">Connect</button>
853
856
  </div>
854
857
  </div>
855
-
856
858
  <!-- ChatGPT -->
857
859
  <div class="provider-card" data-provider="chatgpt">
858
860
  <div class="provider-icon chatgpt">G</div>
@@ -861,11 +863,10 @@
861
863
  <div class="provider-desc">chatgpt.com — OpenAI's assistant</div>
862
864
  </div>
863
865
  <div class="provider-actions">
864
- <button class="btn-open" data-url="https://chatgpt.com">Open ↗</button>
866
+ <button class="btn-open" data-url="https://chatgpt.com" data-provider="chatgpt">Open ↗</button>
865
867
  <button class="btn-connect" data-provider="chatgpt" id="connect-chatgpt">Connect</button>
866
868
  </div>
867
869
  </div>
868
-
869
870
  <!-- Perplexity -->
870
871
  <div class="provider-card" data-provider="perplexity">
871
872
  <div class="provider-icon perplexity">P</div>
@@ -874,24 +875,24 @@
874
875
  <div class="provider-desc">perplexity.ai — search-powered AI</div>
875
876
  </div>
876
877
  <div class="provider-actions">
877
- <button class="btn-open" data-url="https://perplexity.ai">Open ↗</button>
878
+ <button class="btn-open" data-url="https://perplexity.ai" data-provider="perplexity">Open ↗</button>
878
879
  <button class="btn-connect" data-provider="perplexity" id="connect-perplexity">Connect</button>
879
880
  </div>
880
881
  </div>
881
-
882
882
  <!-- Gemini -->
883
883
  <div class="provider-card" data-provider="gemini">
884
- <div class="provider-icon gemini">G</div>
884
+ <div class="provider-icon gemini">✦</div>
885
885
  <div class="provider-info">
886
886
  <div class="provider-name">Gemini</div>
887
887
  <div class="provider-desc">gemini.google.com — Google's AI</div>
888
888
  </div>
889
889
  <div class="provider-actions">
890
- <button class="btn-open" data-url="https://gemini.google.com">Open ↗</button>
890
+ <button class="btn-open" data-url="https://gemini.google.com" data-provider="gemini">Open ↗</button>
891
891
  <button class="btn-connect" data-provider="gemini" id="connect-gemini">Connect</button>
892
892
  </div>
893
893
  </div>
894
-
894
+ <!-- Inline error/hint row (shown when login check fails) -->
895
+ <div id="provider-login-hint" style="display:none;margin-top:4px;padding:10px 12px;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);border-radius:8px;font-size:10px;color:rgba(239,68,68,0.9);line-height:1.5;"></div>
895
896
  </div>
896
897
  </div><!-- /tab-providers -->
897
898
 
@@ -1207,14 +1208,12 @@
1207
1208
  document.querySelectorAll('.btn-open').forEach(btn => {
1208
1209
  btn.addEventListener('click', () => {
1209
1210
  const url = btn.dataset.url;
1210
- const provider = btn.closest('[data-provider]')?.dataset.provider;
1211
+ const provider = btn.dataset.provider || btn.closest('[data-provider]')?.dataset.provider;
1211
1212
  if (!url) return;
1212
- // Manus gets the floating branded browser window; others open in system browser
1213
- if (provider === 'manus') {
1214
- window.promethios.openManusBrowser({});
1215
- } else {
1216
- window.promethios.openExternal(url);
1217
- }
1213
+ // Always open in the real system browser OAuth requires it (Google/Meta block Electron webviews)
1214
+ // Manus also gets the system browser so Google login works
1215
+ window.promethios.openExternal(url);
1216
+ hideProviderLoginHint();
1218
1217
  });
1219
1218
  });
1220
1219
 
@@ -1229,10 +1228,33 @@
1229
1228
  });
1230
1229
  });
1231
1230
 
1231
+ // Provider login URLs — used to open the sign-in page if not logged in
1232
+ const PROVIDER_LOGIN_URLS = {
1233
+ manus: 'https://manus.im',
1234
+ claude: 'https://claude.ai/login',
1235
+ chatgpt: 'https://chatgpt.com/auth/login',
1236
+ perplexity: 'https://www.perplexity.ai/',
1237
+ gemini: 'https://gemini.google.com/',
1238
+ };
1239
+
1240
+ function showProviderLoginHint(provider, message) {
1241
+ const hint = document.getElementById('provider-login-hint');
1242
+ if (!hint) return;
1243
+ hint.innerHTML = message;
1244
+ hint.style.display = 'block';
1245
+ setTimeout(() => { hint.style.display = 'none'; }, 12000);
1246
+ }
1247
+
1248
+ function hideProviderLoginHint() {
1249
+ const hint = document.getElementById('provider-login-hint');
1250
+ if (hint) hint.style.display = 'none';
1251
+ }
1252
+
1232
1253
  async function startOAuthFlow(provider) {
1254
+ hideProviderLoginHint();
1255
+
1233
1256
  if (!authToken) {
1234
- showToast('Paste your bridge token in the Bridge tab first.');
1235
- // Switch to Bridge tab
1257
+ showToast('Bridge not connected start the bridge first.');
1236
1258
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
1237
1259
  document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
1238
1260
  document.querySelector('.tab[data-tab="bridge"]').classList.add('active');
@@ -1240,7 +1262,7 @@
1240
1262
  return;
1241
1263
  }
1242
1264
 
1243
- // Find the clicked button and show loading state
1265
+ const info = PROVIDER_LABELS[provider] || { name: provider, color: 'rgba(255,255,255,0.1)', text: '#e2e8f0' };
1244
1266
  const btn = document.querySelector(`.btn-connect[data-provider="${provider}"]`);
1245
1267
  if (btn) { btn.textContent = 'Connecting…'; btn.disabled = true; }
1246
1268
 
@@ -1253,17 +1275,29 @@
1253
1275
  const data = await res.json();
1254
1276
 
1255
1277
  if (!data.success) {
1256
- showToast(`Connect failed: ${data.error || 'Unknown error'}`);
1278
+ const errMsg = (data.error || '').toLowerCase();
1279
+ const isLoginRequired = errMsg.includes('login') || errMsg.includes('not logged') ||
1280
+ errMsg.includes('auth') || errMsg.includes('session') ||
1281
+ errMsg.includes('sign in') || res.status === 401;
1282
+
1283
+ if (isLoginRequired) {
1284
+ const loginUrl = PROVIDER_LOGIN_URLS[provider] || `https://${provider}.com`;
1285
+ showProviderLoginHint(provider,
1286
+ `<strong>Sign in to ${info.name} first.</strong><br>` +
1287
+ `Click <strong>Open ↗</strong> next to ${info.name} to sign in, then click <strong>Connect</strong> again.`
1288
+ );
1289
+ window.promethios.openExternal(loginUrl);
1290
+ } else {
1291
+ showToast(`Connect failed: ${data.error || 'Unknown error'}`);
1292
+ }
1257
1293
  if (btn) { btn.textContent = 'Connect'; btn.disabled = false; }
1258
1294
  return;
1259
1295
  }
1260
1296
 
1261
- // Store the connection
1262
1297
  connectedProviders[provider] = { token: data.token, clientId: provider, ts: Date.now() };
1263
1298
  updateProviderButtons();
1299
+ hideProviderLoginHint();
1264
1300
 
1265
- // Show copy-prompt modal with the relay-generated prompt
1266
- const info = PROVIDER_LABELS[provider] || { name: provider, color: 'rgba(255,255,255,0.1)', text: '#e2e8f0' };
1267
1301
  modalProviderName.textContent = info.name;
1268
1302
  modalProviderBadge.textContent = info.name;
1269
1303
  modalProviderBadge.style.background = info.color;
@@ -1272,8 +1306,8 @@
1272
1306
  modalCopyBtn.textContent = '⌘ Copy Prompt';
1273
1307
  modalCopyBtn.classList.remove('copied');
1274
1308
  promptModal.classList.add('open');
1275
-
1276
1309
  showToast(`✓ ${info.name} connected!`);
1310
+
1277
1311
  } catch (err) {
1278
1312
  showToast('Network error — check your connection and try again.');
1279
1313
  if (btn) { btn.textContent = 'Connect'; btn.disabled = false; }