promethios-bridge 1.7.3 → 1.7.5
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 +146 -9
- package/src/contextCapture.js +42 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promethios-bridge",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.5",
|
|
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,24 +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
353
|
const overlayChild = launchOverlay({ authToken, apiBase, dev });
|
|
231
354
|
if (overlayChild) {
|
|
232
|
-
|
|
233
|
-
// (e.g. ENOENT because electron is not installed), the error handler
|
|
234
|
-
// in launcher.js will silently absorb it. We print the success message
|
|
235
|
-
// optimistically but it's non-critical if the overlay doesn't appear.
|
|
355
|
+
overlayLaunched = true;
|
|
236
356
|
console.log(chalk.cyan(' ⬡ Promethios overlay launched — floating chat is ready'));
|
|
237
357
|
console.log(chalk.gray(' Hotkey: Ctrl+Shift+P (Win/Linux) or Cmd+Shift+P (Mac)'));
|
|
238
358
|
console.log('');
|
|
239
359
|
}
|
|
240
360
|
} catch (err) {
|
|
241
|
-
log('
|
|
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}`));
|
|
374
|
+
console.log('');
|
|
375
|
+
} catch (err) {
|
|
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('');
|
|
242
379
|
}
|
|
243
380
|
}
|
|
244
381
|
|
package/src/contextCapture.js
CHANGED
|
@@ -223,33 +223,57 @@ async function captureLinux(snapshot, log) {
|
|
|
223
223
|
* approach via PowerShell — but that's expensive, so we only do it when
|
|
224
224
|
* the active process is a known browser.
|
|
225
225
|
*/
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
// NOTE: parameter renamed from 'process' to 'processName' to avoid shadowing
|
|
227
|
+
// the global Node.js process object (which caused process.pid to be undefined).
|
|
228
|
+
function extractUrlFromBrowserTitle(title, processName) {
|
|
229
|
+
const proc = (processName || '').toLowerCase();
|
|
228
230
|
const isBrowser = proc.includes('chrome') || proc.includes('msedge') ||
|
|
229
231
|
proc.includes('firefox') || proc.includes('opera') || proc.includes('brave');
|
|
230
232
|
if (!isBrowser) return null;
|
|
231
233
|
|
|
232
234
|
// Try to get URL from Chrome/Edge via PowerShell UIAutomation (Windows only)
|
|
233
235
|
// This reads the address bar text directly — works even for SPAs
|
|
236
|
+
// NOTE: Must use -File (temp script) not -Command to preserve multi-line PS syntax.
|
|
237
|
+
// NOTE: Guard $browserProc.MainWindowHandle against IntPtr.Zero (0) — this happens
|
|
238
|
+
// when Chrome is minimized or has no foreground window. FromHandle(0) throws.
|
|
234
239
|
if (proc.includes('chrome') || proc.includes('msedge')) {
|
|
235
240
|
try {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
241
|
+
const os = require('os');
|
|
242
|
+
const path = require('path');
|
|
243
|
+
const fs = require('fs');
|
|
244
|
+
const browserName = proc.includes('msedge') ? 'msedge' : 'chrome';
|
|
245
|
+
// Use global process.pid (Node built-in) for a unique temp file name.
|
|
246
|
+
const tmpFile = path.join(os.tmpdir(), `promethios_url_${process.pid}.ps1`);
|
|
247
|
+
const psLines = [
|
|
248
|
+
'Add-Type -AssemblyName UIAutomationClient',
|
|
249
|
+
'Add-Type -AssemblyName UIAutomationTypes',
|
|
250
|
+
// Get all processes with a non-zero MainWindowHandle (i.e. a visible window)
|
|
251
|
+
`$procs = Get-Process -Name "${browserName}" -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 }`,
|
|
252
|
+
'if ($procs) {',
|
|
253
|
+
' $browserProc = $procs | Select-Object -First 1',
|
|
254
|
+
' try {',
|
|
255
|
+
' $root = [System.Windows.Automation.AutomationElement]::FromHandle($browserProc.MainWindowHandle)',
|
|
256
|
+
' if ($root) {',
|
|
257
|
+
' $cond = New-Object System.Windows.Automation.PropertyCondition(',
|
|
258
|
+
' [System.Windows.Automation.AutomationElement]::NameProperty,',
|
|
259
|
+
' "Address and search bar")',
|
|
260
|
+
' $bar = $root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $cond)',
|
|
261
|
+
' if ($bar) {',
|
|
262
|
+
' $vp = $bar.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)',
|
|
263
|
+
' Write-Output $vp.Current.Value',
|
|
264
|
+
' }',
|
|
265
|
+
' }',
|
|
266
|
+
' } catch {',
|
|
267
|
+
' # Silently ignore UIAutomation errors (window may have closed, etc.)',
|
|
268
|
+
' }',
|
|
269
|
+
'}',
|
|
270
|
+
];
|
|
271
|
+
fs.writeFileSync(tmpFile, psLines.join('\r\n'), 'utf8');
|
|
249
272
|
const url = execSync(
|
|
250
|
-
`powershell -NoProfile -NonInteractive -
|
|
273
|
+
`powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${tmpFile}"`,
|
|
251
274
|
{ encoding: 'utf8', timeout: 5000, windowsHide: true }
|
|
252
275
|
).trim();
|
|
276
|
+
try { fs.unlinkSync(tmpFile); } catch { /* cleanup best-effort */ }
|
|
253
277
|
if (url && (url.startsWith('http') || url.startsWith('chrome'))) {
|
|
254
278
|
return url;
|
|
255
279
|
}
|
|
@@ -261,8 +285,8 @@ if ($proc) {
|
|
|
261
285
|
return null; // URL not extractable from title alone
|
|
262
286
|
}
|
|
263
287
|
|
|
264
|
-
function extractTabTitle(windowTitle,
|
|
265
|
-
const proc = (
|
|
288
|
+
function extractTabTitle(windowTitle, processName) {
|
|
289
|
+
const proc = (processName || '').toLowerCase();
|
|
266
290
|
// Strip browser suffix from title
|
|
267
291
|
return windowTitle
|
|
268
292
|
.replace(/ - Google Chrome$/, '')
|