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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.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 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,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
- // 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
+ // 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 { 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; }
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="dot"></div>
292
- <span class="title">Promethios</span>
293
- <span class="status" id="status">Connected</span>
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">Bridge connected ready to help</div>
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">Send</button>
485
+ <button id="send" title="Send (Enter)">&#9650;</button>
301
486
  </div>
302
487
  <script>
303
- const AUTH_TOKEN = '${token}';
304
- const API_BASE = '${apiBase_}';
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 = document.getElementById('input');
307
- const sendBtn = document.getElementById('send');
308
- const statusEl = document.getElementById('status');
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
- statusEl.textContent = 'Thinking...';
522
+ setStatus('Thinking\u2026', true);
523
+ const thinking = addMsg('thinking', 'Promethios is thinking\u2026');
325
524
  try {
326
- const res = await fetch(API_BASE + '/api/chat/quick', {
525
+ const res = await fetch(PROXY, {
327
526
  method: 'POST',
328
- headers: { 'Content-Type': 'application/json',
329
- 'Authorization': 'Bearer ' + AUTH_TOKEN },
330
- body: JSON.stringify({ message: text, source: 'overlay' }),
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
- addMsg('ai', data.reply || data.message || JSON.stringify(data));
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
- addMsg('system', 'Error: ' + res.status);
543
+ const err = await res.json().catch(() => ({}));
544
+ addMsg('system', 'Error ' + res.status + (err.error ? ': ' + err.error : ''));
337
545
  }
338
546
  } catch (e) {
339
- addMsg('system', 'Could not reach Promethios: ' + e.message);
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
- statusEl.textContent = 'Connected';
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, 100) + 'px';
562
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
352
563
  });
353
564
  <\/script>
354
565
  </body>