promethios-bridge 1.7.2 → 1.7.4
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 +151 -8
- package/src/contextCapture.js +57 -42
- package/src/overlay/launcher.js +15 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promethios-bridge",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4",
|
|
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
|
@@ -189,6 +189,126 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
189
189
|
res.sendStatus(204);
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
// ── Overlay route: serves the floating chat UI in the default browser ────
|
|
193
|
+
// This is the fallback when Electron is not available (e.g. running via npx).
|
|
194
|
+
// The overlay HTML is stored inline here so no file path resolution is needed.
|
|
195
|
+
let overlayAuthToken = null; // set after authentication
|
|
196
|
+
app.get('/overlay', (req, res) => {
|
|
197
|
+
const token = overlayAuthToken || '';
|
|
198
|
+
const apiBase_ = apiBase;
|
|
199
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
200
|
+
res.send(`<!DOCTYPE html>
|
|
201
|
+
<html lang="en">
|
|
202
|
+
<head>
|
|
203
|
+
<meta charset="UTF-8">
|
|
204
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
205
|
+
<title>Promethios</title>
|
|
206
|
+
<style>
|
|
207
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
208
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
209
|
+
background: #0f0f11; color: #e4e4e7; height: 100vh; display: flex;
|
|
210
|
+
flex-direction: column; overflow: hidden; }
|
|
211
|
+
#header { background: #18181b; border-bottom: 1px solid #27272a;
|
|
212
|
+
padding: 10px 14px; display: flex; align-items: center; gap: 8px;
|
|
213
|
+
flex-shrink: 0; }
|
|
214
|
+
#header .dot { width: 8px; height: 8px; border-radius: 50%;
|
|
215
|
+
background: #22c55e; flex-shrink: 0; }
|
|
216
|
+
#header .title { font-size: 13px; font-weight: 600; color: #a1a1aa; }
|
|
217
|
+
#header .status { font-size: 11px; color: #22c55e; margin-left: auto; }
|
|
218
|
+
#messages { flex: 1; overflow-y: auto; padding: 12px; display: flex;
|
|
219
|
+
flex-direction: column; gap: 8px; }
|
|
220
|
+
.msg { max-width: 85%; padding: 8px 12px; border-radius: 12px;
|
|
221
|
+
font-size: 13px; line-height: 1.5; word-break: break-word; }
|
|
222
|
+
.msg.user { background: #3f3f46; align-self: flex-end; color: #e4e4e7; }
|
|
223
|
+
.msg.ai { background: #1e1e2e; border: 1px solid #27272a;
|
|
224
|
+
align-self: flex-start; color: #c4b5fd; }
|
|
225
|
+
.msg.system { background: transparent; border: none;
|
|
226
|
+
color: #52525b; font-size: 11px; align-self: center; font-style: italic; }
|
|
227
|
+
#input-row { padding: 10px 12px; background: #18181b;
|
|
228
|
+
border-top: 1px solid #27272a; display: flex; gap: 8px; flex-shrink: 0; }
|
|
229
|
+
#input { flex: 1; background: #27272a; border: 1px solid #3f3f46;
|
|
230
|
+
border-radius: 8px; padding: 8px 12px; color: #e4e4e7; font-size: 13px;
|
|
231
|
+
outline: none; resize: none; height: 36px; font-family: inherit; }
|
|
232
|
+
#input:focus { border-color: #6d28d9; }
|
|
233
|
+
#send { background: #6d28d9; color: white; border: none; border-radius: 8px;
|
|
234
|
+
padding: 0 14px; font-size: 13px; cursor: pointer; height: 36px;
|
|
235
|
+
flex-shrink: 0; }
|
|
236
|
+
#send:hover { background: #7c3aed; }
|
|
237
|
+
#send:disabled { background: #3f3f46; cursor: not-allowed; }
|
|
238
|
+
::-webkit-scrollbar { width: 4px; }
|
|
239
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
240
|
+
::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 2px; }
|
|
241
|
+
</style>
|
|
242
|
+
</head>
|
|
243
|
+
<body>
|
|
244
|
+
<div id="header">
|
|
245
|
+
<div class="dot"></div>
|
|
246
|
+
<span class="title">Promethios</span>
|
|
247
|
+
<span class="status" id="status">Connected</span>
|
|
248
|
+
</div>
|
|
249
|
+
<div id="messages">
|
|
250
|
+
<div class="msg system">Bridge connected — ready to help</div>
|
|
251
|
+
</div>
|
|
252
|
+
<div id="input-row">
|
|
253
|
+
<textarea id="input" placeholder="Ask Promethios..." rows="1"></textarea>
|
|
254
|
+
<button id="send">Send</button>
|
|
255
|
+
</div>
|
|
256
|
+
<script>
|
|
257
|
+
const AUTH_TOKEN = '${token}';
|
|
258
|
+
const API_BASE = '${apiBase_}';
|
|
259
|
+
const messagesEl = document.getElementById('messages');
|
|
260
|
+
const inputEl = document.getElementById('input');
|
|
261
|
+
const sendBtn = document.getElementById('send');
|
|
262
|
+
const statusEl = document.getElementById('status');
|
|
263
|
+
|
|
264
|
+
function addMsg(role, text) {
|
|
265
|
+
const d = document.createElement('div');
|
|
266
|
+
d.className = 'msg ' + role;
|
|
267
|
+
d.textContent = text;
|
|
268
|
+
messagesEl.appendChild(d);
|
|
269
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function sendMessage() {
|
|
273
|
+
const text = inputEl.value.trim();
|
|
274
|
+
if (!text) return;
|
|
275
|
+
inputEl.value = '';
|
|
276
|
+
addMsg('user', text);
|
|
277
|
+
sendBtn.disabled = true;
|
|
278
|
+
statusEl.textContent = 'Thinking...';
|
|
279
|
+
try {
|
|
280
|
+
const res = await fetch(API_BASE + '/api/chat/quick', {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'Content-Type': 'application/json',
|
|
283
|
+
'Authorization': 'Bearer ' + AUTH_TOKEN },
|
|
284
|
+
body: JSON.stringify({ message: text, source: 'overlay' }),
|
|
285
|
+
});
|
|
286
|
+
if (res.ok) {
|
|
287
|
+
const data = await res.json();
|
|
288
|
+
addMsg('ai', data.reply || data.message || JSON.stringify(data));
|
|
289
|
+
} else {
|
|
290
|
+
addMsg('system', 'Error: ' + res.status);
|
|
291
|
+
}
|
|
292
|
+
} catch (e) {
|
|
293
|
+
addMsg('system', 'Could not reach Promethios: ' + e.message);
|
|
294
|
+
}
|
|
295
|
+
sendBtn.disabled = false;
|
|
296
|
+
statusEl.textContent = 'Connected';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
300
|
+
inputEl.addEventListener('keydown', e => {
|
|
301
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
302
|
+
});
|
|
303
|
+
inputEl.addEventListener('input', () => {
|
|
304
|
+
inputEl.style.height = 'auto';
|
|
305
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 100) + 'px';
|
|
306
|
+
});
|
|
307
|
+
<\/script>
|
|
308
|
+
</body>
|
|
309
|
+
</html>`);
|
|
310
|
+
});
|
|
311
|
+
|
|
192
312
|
await new Promise((resolve, reject) => {
|
|
193
313
|
const server = app.listen(port, '127.0.0.1', () => resolve(server));
|
|
194
314
|
server.on('error', reject);
|
|
@@ -221,18 +341,41 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
221
341
|
console.log(chalk.gray(' Press Ctrl+C to disconnect.'));
|
|
222
342
|
console.log('');
|
|
223
343
|
|
|
224
|
-
// ── Step 4c: Launch
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
344
|
+
// ── Step 4c: Launch overlay ──────────────────────────────────────────────
|
|
345
|
+
// Set the auth token so the /overlay route can embed it in the HTML.
|
|
346
|
+
overlayAuthToken = authToken;
|
|
347
|
+
|
|
348
|
+
// Try Electron first (available when installed globally or via postinstall).
|
|
349
|
+
// Fall back to opening the browser-based overlay at http://localhost:<port>/overlay.
|
|
350
|
+
let overlayLaunched = false;
|
|
228
351
|
if (launchOverlay) {
|
|
229
352
|
try {
|
|
230
|
-
launchOverlay({ authToken, apiBase, dev });
|
|
231
|
-
|
|
232
|
-
|
|
353
|
+
const overlayChild = launchOverlay({ authToken, apiBase, dev });
|
|
354
|
+
if (overlayChild) {
|
|
355
|
+
overlayLaunched = true;
|
|
356
|
+
console.log(chalk.cyan(' ⬡ Promethios overlay launched — floating chat is ready'));
|
|
357
|
+
console.log(chalk.gray(' Hotkey: Ctrl+Shift+P (Win/Linux) or Cmd+Shift+P (Mac)'));
|
|
358
|
+
console.log('');
|
|
359
|
+
}
|
|
360
|
+
} catch (err) {
|
|
361
|
+
log('Electron overlay launch failed (non-critical):', err.message);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!overlayLaunched) {
|
|
366
|
+
// Electron not available — open the lightweight browser overlay instead.
|
|
367
|
+
// This is a small chat UI served by the bridge's own Express server.
|
|
368
|
+
const overlayUrl = `http://127.0.0.1:${port}/overlay`;
|
|
369
|
+
try {
|
|
370
|
+
const openModule = require('open');
|
|
371
|
+
await openModule(overlayUrl);
|
|
372
|
+
console.log(chalk.cyan(' ⬡ Promethios overlay opened in your browser'));
|
|
373
|
+
console.log(chalk.gray(` URL: ${overlayUrl}`));
|
|
233
374
|
console.log('');
|
|
234
375
|
} catch (err) {
|
|
235
|
-
log('
|
|
376
|
+
log('Browser overlay launch failed (non-critical):', err.message);
|
|
377
|
+
console.log(chalk.gray(` ℹ Open ${overlayUrl} in your browser for the chat overlay`));
|
|
378
|
+
console.log('');
|
|
236
379
|
}
|
|
237
380
|
}
|
|
238
381
|
|
package/src/contextCapture.js
CHANGED
|
@@ -53,39 +53,43 @@ async function captureContext(platform, dev) {
|
|
|
53
53
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
54
|
async function captureWindows(snapshot, log) {
|
|
55
55
|
// Active window title + process name
|
|
56
|
+
// NOTE: PowerShell here-strings (@"..."@) cannot be passed via -Command because
|
|
57
|
+
// newlines get collapsed. We write a temp .ps1 file and run it with -File instead.
|
|
56
58
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
using System
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
[DllImport("user32.dll")] public static extern
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
[Win32]::
|
|
71
|
-
$
|
|
72
|
-
[Win32]::
|
|
73
|
-
$
|
|
74
|
-
$
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
const os = require('os');
|
|
60
|
+
const tmpFile = require('path').join(os.tmpdir(), `promethios_ctx_${process.pid}.ps1`);
|
|
61
|
+
const psScript = [
|
|
62
|
+
'Add-Type @"',
|
|
63
|
+
'using System;',
|
|
64
|
+
'using System.Runtime.InteropServices;',
|
|
65
|
+
'using System.Text;',
|
|
66
|
+
'public class Win32 {',
|
|
67
|
+
' [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();',
|
|
68
|
+
' [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);',
|
|
69
|
+
' [DllImport("user32.dll")] public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);',
|
|
70
|
+
'}',
|
|
71
|
+
'"@',
|
|
72
|
+
'$hwnd = [Win32]::GetForegroundWindow()',
|
|
73
|
+
'$sb = New-Object System.Text.StringBuilder 512',
|
|
74
|
+
'[Win32]::GetWindowText($hwnd, $sb, 512) | Out-Null',
|
|
75
|
+
'$pid2 = 0',
|
|
76
|
+
'[Win32]::GetWindowThreadProcessId($hwnd, [ref]$pid2) | Out-Null',
|
|
77
|
+
'$proc = Get-Process -Id $pid2 -ErrorAction SilentlyContinue',
|
|
78
|
+
'$result = @{',
|
|
79
|
+
' title = $sb.ToString()',
|
|
80
|
+
' procName = if ($proc) { $proc.Name } else { "unknown" }',
|
|
81
|
+
'} | ConvertTo-Json -Compress',
|
|
82
|
+
'Write-Output $result',
|
|
83
|
+
].join('\r\n');
|
|
84
|
+
require('fs').writeFileSync(tmpFile, psScript, 'utf8');
|
|
81
85
|
const output = execSync(
|
|
82
|
-
`powershell -NoProfile -NonInteractive -
|
|
86
|
+
`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${tmpFile}"`,
|
|
83
87
|
{ encoding: 'utf8', timeout: 4000, windowsHide: true }
|
|
84
88
|
).trim();
|
|
85
|
-
|
|
89
|
+
try { require('fs').unlinkSync(tmpFile); } catch { /* cleanup best-effort */ }
|
|
86
90
|
const parsed = JSON.parse(output);
|
|
87
|
-
snapshot.active_window = { title: parsed.title, process: parsed.
|
|
88
|
-
snapshot.active_app = friendlyAppName(parsed.
|
|
91
|
+
snapshot.active_window = { title: parsed.title, process: parsed.procName };
|
|
92
|
+
snapshot.active_app = friendlyAppName(parsed.procName, parsed.title);
|
|
89
93
|
log('Active window:', snapshot.active_window);
|
|
90
94
|
} catch (err) {
|
|
91
95
|
log('Windows active window capture failed:', err.message);
|
|
@@ -227,25 +231,36 @@ function extractUrlFromBrowserTitle(title, process) {
|
|
|
227
231
|
|
|
228
232
|
// Try to get URL from Chrome/Edge via PowerShell UIAutomation (Windows only)
|
|
229
233
|
// This reads the address bar text directly — works even for SPAs
|
|
234
|
+
// NOTE: Must use -File (temp script) not -Command to preserve multi-line PS syntax.
|
|
230
235
|
if (proc.includes('chrome') || proc.includes('msedge')) {
|
|
231
236
|
try {
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
237
|
+
const os = require('os');
|
|
238
|
+
const path = require('path');
|
|
239
|
+
const fs = require('fs');
|
|
240
|
+
const browserName = proc.includes('msedge') ? 'msedge' : 'chrome';
|
|
241
|
+
const tmpFile = path.join(os.tmpdir(), `promethios_url_${process.pid}.ps1`);
|
|
242
|
+
const psLines = [
|
|
243
|
+
'Add-Type -AssemblyName UIAutomationClient',
|
|
244
|
+
'Add-Type -AssemblyName UIAutomationTypes',
|
|
245
|
+
`$browserProc = Get-Process -Name "${browserName}" -ErrorAction SilentlyContinue | Select-Object -First 1`,
|
|
246
|
+
'if ($browserProc) {',
|
|
247
|
+
' $root = [System.Windows.Automation.AutomationElement]::FromHandle($browserProc.MainWindowHandle)',
|
|
248
|
+
' $cond = New-Object System.Windows.Automation.PropertyCondition(',
|
|
249
|
+
' [System.Windows.Automation.AutomationElement]::NameProperty,',
|
|
250
|
+
' "Address and search bar")',
|
|
251
|
+
' $bar = $root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $cond)',
|
|
252
|
+
' if ($bar) {',
|
|
253
|
+
' $vp = $bar.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)',
|
|
254
|
+
' Write-Output $vp.Current.Value',
|
|
255
|
+
' }',
|
|
256
|
+
'}',
|
|
257
|
+
];
|
|
258
|
+
fs.writeFileSync(tmpFile, psLines.join('\r\n'), 'utf8');
|
|
245
259
|
const url = execSync(
|
|
246
|
-
`powershell -NoProfile -NonInteractive -
|
|
260
|
+
`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${tmpFile}"`,
|
|
247
261
|
{ encoding: 'utf8', timeout: 5000, windowsHide: true }
|
|
248
262
|
).trim();
|
|
263
|
+
try { fs.unlinkSync(tmpFile); } catch { /* cleanup best-effort */ }
|
|
249
264
|
if (url && (url.startsWith('http') || url.startsWith('chrome'))) {
|
|
250
265
|
return url;
|
|
251
266
|
}
|
package/src/overlay/launcher.js
CHANGED
|
@@ -17,6 +17,11 @@ const fs = require('fs');
|
|
|
17
17
|
/**
|
|
18
18
|
* Launch the Promethios overlay Electron window.
|
|
19
19
|
* Returns the child process (or null if Electron is not available).
|
|
20
|
+
*
|
|
21
|
+
* IMPORTANT: When running via `npx`, optional dependencies (including electron)
|
|
22
|
+
* are NOT installed in the npx cache. The spawn() call may therefore fail with
|
|
23
|
+
* ENOENT. We attach an 'error' handler before unref() to silently absorb this
|
|
24
|
+
* instead of crashing the bridge process with an unhandled error event.
|
|
20
25
|
*/
|
|
21
26
|
function launchOverlay({ authToken, apiBase = 'https://api.promethios.ai', threadId = '', dev = false } = {}) {
|
|
22
27
|
// Find electron binary — try local node_modules first, then global
|
|
@@ -33,7 +38,7 @@ function launchOverlay({ authToken, apiBase = 'https://api.promethios.ai', threa
|
|
|
33
38
|
for (const candidate of candidates) {
|
|
34
39
|
try {
|
|
35
40
|
if (candidate === 'electron') {
|
|
36
|
-
// Try to resolve globally
|
|
41
|
+
// Try to resolve globally — throws if not installed
|
|
37
42
|
require.resolve('electron');
|
|
38
43
|
electronBin = candidate;
|
|
39
44
|
break;
|
|
@@ -46,7 +51,7 @@ function launchOverlay({ authToken, apiBase = 'https://api.promethios.ai', threa
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
if (!electronBin) {
|
|
49
|
-
if (dev) console.log('[overlay] Electron not found — overlay will not launch.
|
|
54
|
+
if (dev) console.log('[overlay] Electron not found — overlay will not launch.');
|
|
50
55
|
return null;
|
|
51
56
|
}
|
|
52
57
|
|
|
@@ -64,6 +69,14 @@ function launchOverlay({ authToken, apiBase = 'https://api.promethios.ai', threa
|
|
|
64
69
|
env: { ...process.env, ELECTRON_NO_ATTACH_CONSOLE: '1' },
|
|
65
70
|
});
|
|
66
71
|
|
|
72
|
+
// Attach error handler BEFORE unref() to prevent unhandled 'error' event crashes.
|
|
73
|
+
// When running via npx, optional deps like electron are not installed, so spawn
|
|
74
|
+
// may emit ENOENT. We catch it silently — the bridge works fine without the overlay.
|
|
75
|
+
child.on('error', (err) => {
|
|
76
|
+
if (dev) console.log(`[overlay] Spawn error (non-critical): ${err.message}`);
|
|
77
|
+
// No-op: overlay is optional, bridge continues running normally
|
|
78
|
+
});
|
|
79
|
+
|
|
67
80
|
child.unref(); // Don't keep bridge CLI alive waiting for overlay
|
|
68
81
|
|
|
69
82
|
if (dev) console.log(`[overlay] Launched (pid ${child.pid})`);
|