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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.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 a URL in the user's default browser.
77
- * Uses platform-native commands for maximum reliability:
78
- * Windows: cmd /c start "" "<url>"
79
- * macOS: open <url>
80
- * Linux: xdg-open <url>
81
- * Falls back to the `open` npm package if the native command fails.
82
- * Returns true if the browser was successfully opened, false otherwise.
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
- // This is the fallback when Electron is not available (e.g. running via npx).
240
- // The overlay HTML is stored inline here so no file path resolution is needed.
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 { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
255
- background: #0f0f11; color: #e4e4e7; height: 100vh; display: flex;
256
- flex-direction: column; overflow: hidden; }
257
- #header { background: #18181b; border-bottom: 1px solid #27272a;
258
- padding: 10px 14px; display: flex; align-items: center; gap: 8px;
259
- flex-shrink: 0; }
260
- #header .dot { width: 8px; height: 8px; border-radius: 50%;
261
- background: #22c55e; flex-shrink: 0; }
262
- #header .title { font-size: 13px; font-weight: 600; color: #a1a1aa; }
263
- #header .status { font-size: 11px; color: #22c55e; margin-left: auto; }
264
- #messages { flex: 1; overflow-y: auto; padding: 12px; display: flex;
265
- flex-direction: column; gap: 8px; }
266
- .msg { max-width: 85%; padding: 8px 12px; border-radius: 12px;
267
- font-size: 13px; line-height: 1.5; word-break: break-word; }
268
- .msg.user { background: #3f3f46; align-self: flex-end; color: #e4e4e7; }
269
- .msg.ai { background: #1e1e2e; border: 1px solid #27272a;
270
- align-self: flex-start; color: #c4b5fd; }
271
- .msg.system { background: transparent; border: none;
272
- color: #52525b; font-size: 11px; align-self: center; font-style: italic; }
273
- #input-row { padding: 10px 12px; background: #18181b;
274
- border-top: 1px solid #27272a; display: flex; gap: 8px; flex-shrink: 0; }
275
- #input { flex: 1; background: #27272a; border: 1px solid #3f3f46;
276
- border-radius: 8px; padding: 8px 12px; color: #e4e4e7; font-size: 13px;
277
- outline: none; resize: none; height: 36px; font-family: inherit; }
278
- #input:focus { border-color: #6d28d9; }
279
- #send { background: #6d28d9; color: white; border: none; border-radius: 8px;
280
- padding: 0 14px; font-size: 13px; cursor: pointer; height: 36px;
281
- flex-shrink: 0; }
282
- #send:hover { background: #7c3aed; }
283
- #send:disabled { background: #3f3f46; cursor: not-allowed; }
284
- ::-webkit-scrollbar { width: 4px; }
285
- ::-webkit-scrollbar-track { background: transparent; }
286
- ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 2px; }
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="dot"></div>
292
- <span class="title">Promethios</span>
293
- <span class="status" id="status">Connected</span>
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">Bridge connected ready to help</div>
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">Send</button>
481
+ <button id="send" title="Send (Enter)">&#9650;</button>
301
482
  </div>
302
483
  <script>
303
- const AUTH_TOKEN = '${token}';
304
- const API_BASE = '${apiBase_}';
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 = document.getElementById('input');
307
- const sendBtn = document.getElementById('send');
308
- const statusEl = document.getElementById('status');
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
- statusEl.textContent = 'Thinking...';
515
+ setStatus('Thinking', true);
516
+ const thinking = addMsg('thinking', 'Promethios is thinking…');
325
517
  try {
326
- const res = await fetch(API_BASE + '/api/chat/quick', {
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
- addMsg('system', 'Error: ' + res.status);
528
+ const err = await res.json().catch(() => ({}));
529
+ addMsg('system', 'Error ' + res.status + (err.error ? ': ' + err.error : ''));
337
530
  }
338
531
  } catch (e) {
339
- addMsg('system', 'Could not reach Promethios: ' + e.message);
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
- statusEl.textContent = 'Connected';
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, 100) + 'px';
547
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
352
548
  });
353
549
  <\/script>
354
550
  </body>