promethios-bridge 1.7.0 → 1.7.2

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.7.0",
3
+ "version": "1.7.2",
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": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "start": "node src/cli.js",
11
11
  "dev": "node src/cli.js --dev",
12
- "postinstall": "node node_modules/playwright/cli.js install chromium || npx playwright install chromium || true"
12
+ "postinstall": "node src/postinstall.js"
13
13
  },
14
14
  "keywords": [
15
15
  "promethios",
package/src/bridge.js CHANGED
@@ -22,14 +22,56 @@ const fetch = require('node-fetch');
22
22
  const { executeLocalTool } = require('./executor');
23
23
  const { captureContext } = require('./contextCapture');
24
24
 
25
- // Optional: Electron overlay window (gracefully skipped if not installed)
25
+ // Optional: Electron overlay window (bundled in src/overlay — gracefully skipped if Electron not available)
26
26
  let launchOverlay = null;
27
- try { launchOverlay = require('promethios-overlay/src/launcher').launchOverlay; } catch { /* overlay not installed */ }
27
+ try { launchOverlay = require('./overlay/launcher').launchOverlay; } catch { /* overlay launcher not found */ }
28
28
 
29
29
  const HEARTBEAT_INTERVAL = 30_000; // 30s
30
30
  const POLL_INTERVAL = 1_000; // 1s — poll for pending tool calls
31
31
  const CONTEXT_PUSH_INTERVAL = 5_000; // 5s — push ambient context snapshot
32
32
 
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+ // Check npm registry for a newer version (non-blocking, best-effort)
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+ async function checkForUpdates(currentVersion, log) {
37
+ try {
38
+ const res = await fetch('https://registry.npmjs.org/promethios-bridge/latest', {
39
+ headers: { Accept: 'application/json' },
40
+ timeout: 5000,
41
+ });
42
+ if (!res.ok) return;
43
+ const data = await res.json();
44
+ const latestVersion = data.version;
45
+ if (!latestVersion) return;
46
+ // Simple semver comparison: split into [major, minor, patch] and compare
47
+ const toNum = v => v.split('.').map(Number);
48
+ const [cMaj, cMin, cPat] = toNum(currentVersion);
49
+ const [lMaj, lMin, lPat] = toNum(latestVersion);
50
+ const isOutdated =
51
+ lMaj > cMaj ||
52
+ (lMaj === cMaj && lMin > cMin) ||
53
+ (lMaj === cMaj && lMin === cMin && lPat > cPat);
54
+ if (isOutdated) {
55
+ console.log('');
56
+ console.log(chalk.yellow(' ┌─────────────────────────────────────────────────────────┐'));
57
+ console.log(chalk.yellow(' │ 🔄 Update available: v' + currentVersion + ' → v' + latestVersion + ' '.repeat(Math.max(0, 28 - currentVersion.length - latestVersion.length)) + '│'));
58
+ console.log(chalk.yellow(' │ │'));
59
+ console.log(chalk.yellow(' │ To update, close this window and re-run the │'));
60
+ console.log(chalk.yellow(' │ installer script from Promethios, or run: │'));
61
+ console.log(chalk.yellow(' │ │'));
62
+ console.log(chalk.yellow(' │ ' + chalk.white('npx promethios-bridge@latest --token <your-token>') + ' │'));
63
+ console.log(chalk.yellow(' │ │'));
64
+ console.log(chalk.yellow(' │ Your current version will continue to work. │'));
65
+ console.log(chalk.yellow(' └─────────────────────────────────────────────────────────┘'));
66
+ console.log('');
67
+ } else {
68
+ log('Version check: up to date (v' + currentVersion + ')');
69
+ }
70
+ } catch (err) {
71
+ log('Version check failed (non-critical):', err.message);
72
+ }
73
+ }
74
+
33
75
  async function startBridge({ setupToken, apiBase, port, dev }) {
34
76
  const log = dev
35
77
  ? (...args) => console.log(chalk.gray(' [debug]'), ...args)
@@ -165,6 +207,10 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
165
207
  process.exit(1);
166
208
  }
167
209
 
210
+ // ── Step 3b: Check for updates (non-blocking) ───────────────────────────
211
+ const currentVersion = require('../package.json').version;
212
+ checkForUpdates(currentVersion, log).catch(() => {}); // fire-and-forget
213
+
168
214
  // ── Step 4: Show status and keep alive ───────────────────────────────────
169
215
  console.log('');
170
216
  console.log(chalk.bold.green(' ✓ Promethios Local Bridge is running'));
@@ -0,0 +1,74 @@
1
+ /**
2
+ * launcher.js
3
+ *
4
+ * Called by the bridge CLI after successful authentication.
5
+ * Spawns the Electron overlay window as a detached child process.
6
+ * Bundled inside promethios-bridge — no separate install required.
7
+ *
8
+ * Usage (from bridge CLI):
9
+ * const { launchOverlay } = require('./overlay/launcher');
10
+ * launchOverlay({ authToken, apiBase, threadId, dev });
11
+ */
12
+
13
+ const { spawn } = require('child_process');
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ /**
18
+ * Launch the Promethios overlay Electron window.
19
+ * Returns the child process (or null if Electron is not available).
20
+ */
21
+ function launchOverlay({ authToken, apiBase = 'https://api.promethios.ai', threadId = '', dev = false } = {}) {
22
+ // Find electron binary — try local node_modules first, then global
23
+ let electronBin = null;
24
+
25
+ const candidates = [
26
+ // Installed inside promethios-bridge's own node_modules (postinstall puts it here)
27
+ path.join(__dirname, '..', '..', 'node_modules', '.bin', 'electron'),
28
+ path.join(__dirname, '..', '..', 'node_modules', '.bin', 'electron.cmd'),
29
+ // Global electron fallback
30
+ 'electron',
31
+ ];
32
+
33
+ for (const candidate of candidates) {
34
+ try {
35
+ if (candidate === 'electron') {
36
+ // Try to resolve globally
37
+ require.resolve('electron');
38
+ electronBin = candidate;
39
+ break;
40
+ }
41
+ if (fs.existsSync(candidate)) {
42
+ electronBin = candidate;
43
+ break;
44
+ }
45
+ } catch { /* not found */ }
46
+ }
47
+
48
+ if (!electronBin) {
49
+ if (dev) console.log('[overlay] Electron not found — overlay will not launch. Install with: npm install -g electron');
50
+ return null;
51
+ }
52
+
53
+ const mainScript = path.join(__dirname, 'main.js');
54
+
55
+ const args = [mainScript];
56
+ if (authToken) { args.push('--token', authToken); }
57
+ if (apiBase) { args.push('--api', apiBase); }
58
+ if (threadId) { args.push('--thread', threadId); }
59
+ if (dev) { args.push('--dev'); }
60
+
61
+ const child = spawn(electronBin, args, {
62
+ detached: true,
63
+ stdio: 'ignore',
64
+ env: { ...process.env, ELECTRON_NO_ATTACH_CONSOLE: '1' },
65
+ });
66
+
67
+ child.unref(); // Don't keep bridge CLI alive waiting for overlay
68
+
69
+ if (dev) console.log(`[overlay] Launched (pid ${child.pid})`);
70
+
71
+ return child;
72
+ }
73
+
74
+ module.exports = { launchOverlay };
@@ -0,0 +1,192 @@
1
+ /**
2
+ * promethios-overlay — Electron main process
3
+ *
4
+ * Creates an always-on-top floating chat window that works across all apps
5
+ * on Windows, Mac, and Linux. Launched by the bridge CLI after authentication.
6
+ *
7
+ * Launch args (passed from bridge CLI via child_process.spawn):
8
+ * --token <authToken> Bearer token for Promethios API
9
+ * --api <apiBase> API base URL (default: https://api.promethios.ai)
10
+ * --thread <threadId> Optional: open a specific thread on launch
11
+ * --dev Enable verbose logging
12
+ */
13
+
14
+ const { app, BrowserWindow, globalShortcut, ipcMain, screen, Tray, Menu, nativeImage } = require('electron');
15
+ const path = require('path');
16
+
17
+ // ── Parse launch args ────────────────────────────────────────────────────────
18
+ const args = process.argv.slice(2);
19
+ const getArg = (name) => {
20
+ const idx = args.indexOf(`--${name}`);
21
+ return idx !== -1 ? args[idx + 1] : null;
22
+ };
23
+ const AUTH_TOKEN = getArg('token') || '';
24
+ const API_BASE = getArg('api') || 'https://api.promethios.ai';
25
+ const THREAD_ID = getArg('thread') || '';
26
+ const DEV = args.includes('--dev');
27
+
28
+ // ── Window state ─────────────────────────────────────────────────────────────
29
+ let overlayWindow = null;
30
+ let tray = null;
31
+ let isExpanded = false;
32
+
33
+ // Pill dimensions (collapsed)
34
+ const PILL_W = 220;
35
+ const PILL_H = 52;
36
+
37
+ // Chat dimensions (expanded)
38
+ const CHAT_W = 380;
39
+ const CHAT_H = 560;
40
+
41
+ // ── App ready ────────────────────────────────────────────────────────────────
42
+ app.whenReady().then(() => {
43
+ createOverlayWindow();
44
+ registerHotkey();
45
+ createTray();
46
+ });
47
+
48
+ app.on('window-all-closed', () => {
49
+ // Keep app alive even if window is closed — user can restore via tray
50
+ if (process.platform !== 'darwin') {
51
+ // On Windows/Linux, keep running in tray
52
+ }
53
+ });
54
+
55
+ app.on('will-quit', () => {
56
+ globalShortcut.unregisterAll();
57
+ });
58
+
59
+ // ── Create the overlay window ────────────────────────────────────────────────
60
+ function createOverlayWindow() {
61
+ const { width: screenW, height: screenH } = screen.getPrimaryDisplay().workAreaSize;
62
+
63
+ overlayWindow = new BrowserWindow({
64
+ width: PILL_W,
65
+ height: PILL_H,
66
+ x: screenW - PILL_W - 24,
67
+ y: screenH - PILL_H - 24,
68
+ frame: false,
69
+ transparent: true,
70
+ alwaysOnTop: true,
71
+ skipTaskbar: true,
72
+ resizable: false,
73
+ movable: true,
74
+ hasShadow: true,
75
+ webPreferences: {
76
+ nodeIntegration: false,
77
+ contextIsolation: true,
78
+ preload: path.join(__dirname, 'preload.js'),
79
+ },
80
+ });
81
+
82
+ // Always on top — level 'floating' keeps it above most apps including full-screen
83
+ overlayWindow.setAlwaysOnTop(true, 'floating');
84
+ overlayWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
85
+
86
+ overlayWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
87
+
88
+ // Pass config to renderer once DOM is ready
89
+ overlayWindow.webContents.on('did-finish-load', () => {
90
+ overlayWindow.webContents.send('config', {
91
+ authToken: AUTH_TOKEN,
92
+ apiBase: API_BASE,
93
+ threadId: THREAD_ID,
94
+ dev: DEV,
95
+ });
96
+ });
97
+
98
+ if (DEV) {
99
+ overlayWindow.webContents.openDevTools({ mode: 'detach' });
100
+ }
101
+ }
102
+
103
+ // ── Toggle between pill and expanded chat ────────────────────────────────────
104
+ function toggleExpanded(expand) {
105
+ if (expand === undefined) expand = !isExpanded;
106
+ isExpanded = expand;
107
+
108
+ const { width: screenW, height: screenH } = screen.getPrimaryDisplay().workAreaSize;
109
+
110
+ if (isExpanded) {
111
+ const x = screenW - CHAT_W - 24;
112
+ const y = screenH - CHAT_H - 24;
113
+ overlayWindow.setBounds({ x, y, width: CHAT_W, height: CHAT_H }, true);
114
+ overlayWindow.webContents.send('expand', true);
115
+ } else {
116
+ const x = screenW - PILL_W - 24;
117
+ const y = screenH - PILL_H - 24;
118
+ overlayWindow.setBounds({ x, y, width: PILL_W, height: PILL_H }, true);
119
+ overlayWindow.webContents.send('expand', false);
120
+ }
121
+ }
122
+
123
+ // ── IPC from renderer ────────────────────────────────────────────────────────
124
+ ipcMain.on('toggle-expand', (_, expand) => toggleExpanded(expand));
125
+
126
+ ipcMain.on('set-watching', (_, watching) => {
127
+ // Relay to renderer (already handled there) — also log for debug
128
+ if (DEV) console.log('[overlay] watching:', watching);
129
+ });
130
+
131
+ ipcMain.on('close-overlay', () => {
132
+ overlayWindow.hide();
133
+ });
134
+
135
+ ipcMain.on('show-overlay', () => {
136
+ overlayWindow.show();
137
+ });
138
+
139
+ // ── Global hotkey: Ctrl+Shift+P (Win/Linux) or Cmd+Shift+P (Mac) ─────────────
140
+ function registerHotkey() {
141
+ const hotkey = process.platform === 'darwin' ? 'Command+Shift+P' : 'Control+Shift+P';
142
+ const registered = globalShortcut.register(hotkey, () => {
143
+ if (overlayWindow.isVisible()) {
144
+ toggleExpanded();
145
+ } else {
146
+ overlayWindow.show();
147
+ toggleExpanded(true);
148
+ }
149
+ });
150
+ if (DEV) console.log(`[overlay] Hotkey ${hotkey} registered:`, registered);
151
+ }
152
+
153
+ // ── System tray icon ─────────────────────────────────────────────────────────
154
+ function createTray() {
155
+ // Use a simple 16x16 template image (will be replaced with real icon in production)
156
+ const iconSize = process.platform === 'darwin' ? 16 : 32;
157
+ const trayIcon = nativeImage.createEmpty();
158
+ tray = new Tray(trayIcon);
159
+
160
+ const contextMenu = Menu.buildFromTemplate([
161
+ {
162
+ label: 'Show Promethios',
163
+ click: () => {
164
+ overlayWindow.show();
165
+ toggleExpanded(true);
166
+ },
167
+ },
168
+ {
169
+ label: 'Hide',
170
+ click: () => overlayWindow.hide(),
171
+ },
172
+ { type: 'separator' },
173
+ {
174
+ label: 'Quit',
175
+ click: () => {
176
+ app.quit();
177
+ },
178
+ },
179
+ ]);
180
+
181
+ tray.setToolTip('Promethios AI Overlay');
182
+ tray.setContextMenu(contextMenu);
183
+
184
+ tray.on('click', () => {
185
+ if (overlayWindow.isVisible()) {
186
+ toggleExpanded();
187
+ } else {
188
+ overlayWindow.show();
189
+ toggleExpanded(true);
190
+ }
191
+ });
192
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * preload.js — Electron contextBridge
3
+ *
4
+ * Exposes a safe, limited API to the renderer process.
5
+ * No direct Node.js access in the renderer — all IPC goes through here.
6
+ */
7
+
8
+ const { contextBridge, ipcRenderer } = require('electron');
9
+
10
+ contextBridge.exposeInMainWorld('promethios', {
11
+ // Receive config from main process (authToken, apiBase, threadId)
12
+ onConfig: (cb) => ipcRenderer.on('config', (_, data) => cb(data)),
13
+
14
+ // Receive expand/collapse state changes from main process
15
+ onExpand: (cb) => ipcRenderer.on('expand', (_, expanded) => cb(expanded)),
16
+
17
+ // Tell main process to toggle expand/collapse
18
+ toggleExpand: (expand) => ipcRenderer.send('toggle-expand', expand),
19
+
20
+ // Tell main process about watching state change
21
+ setWatching: (watching) => ipcRenderer.send('set-watching', watching),
22
+
23
+ // Close the overlay window
24
+ close: () => ipcRenderer.send('close-overlay'),
25
+ });
@@ -0,0 +1,296 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Promethios</title>
7
+ <style>
8
+ /* ── Reset ─────────────────────────────────────────────────────────────── */
9
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
+ html, body { width: 100%; height: 100%; overflow: hidden; background: transparent; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
11
+
12
+ /* ── Pill (collapsed state) ─────────────────────────────────────────────── */
13
+ #pill {
14
+ position: fixed; bottom: 0; right: 0;
15
+ width: 220px; height: 52px;
16
+ background: rgba(15, 15, 20, 0.92);
17
+ backdrop-filter: blur(16px);
18
+ border: 1px solid rgba(139, 92, 246, 0.4);
19
+ border-radius: 26px;
20
+ display: flex; align-items: center; gap: 10px;
21
+ padding: 0 14px;
22
+ cursor: pointer;
23
+ -webkit-app-region: drag;
24
+ box-shadow: 0 4px 24px rgba(0,0,0,0.5), 0 0 0 1px rgba(139,92,246,0.15);
25
+ transition: border-color 0.2s, box-shadow 0.2s;
26
+ user-select: none;
27
+ }
28
+ #pill:hover {
29
+ border-color: rgba(139, 92, 246, 0.7);
30
+ box-shadow: 0 4px 32px rgba(139,92,246,0.25), 0 0 0 1px rgba(139,92,246,0.3);
31
+ }
32
+
33
+ /* Logo mark */
34
+ .pill-logo {
35
+ width: 28px; height: 28px; border-radius: 8px;
36
+ background: linear-gradient(135deg, #7c3aed, #a855f7);
37
+ display: flex; align-items: center; justify-content: center;
38
+ font-size: 14px; flex-shrink: 0;
39
+ -webkit-app-region: no-drag;
40
+ }
41
+
42
+ .pill-label {
43
+ flex: 1;
44
+ font-size: 13px; font-weight: 600;
45
+ color: #e2e8f0;
46
+ letter-spacing: 0.01em;
47
+ -webkit-app-region: no-drag;
48
+ }
49
+
50
+ /* Status dot — green=watching, yellow=paused */
51
+ #status-dot {
52
+ width: 8px; height: 8px; border-radius: 50%;
53
+ background: #22c55e;
54
+ flex-shrink: 0;
55
+ cursor: pointer;
56
+ -webkit-app-region: no-drag;
57
+ transition: background 0.3s;
58
+ position: relative;
59
+ }
60
+ #status-dot.paused { background: #eab308; }
61
+ #status-dot::after {
62
+ content: attr(data-tooltip);
63
+ position: absolute; bottom: 14px; right: 0;
64
+ background: rgba(15,15,20,0.95); color: #e2e8f0;
65
+ font-size: 11px; padding: 3px 8px; border-radius: 4px;
66
+ white-space: nowrap; pointer-events: none;
67
+ opacity: 0; transition: opacity 0.15s;
68
+ }
69
+ #status-dot:hover::after { opacity: 1; }
70
+
71
+ /* ── Chat window (expanded state) ───────────────────────────────────────── */
72
+ #chat {
73
+ display: none;
74
+ position: fixed; bottom: 0; right: 0;
75
+ width: 380px; height: 560px;
76
+ background: rgba(10, 10, 16, 0.96);
77
+ backdrop-filter: blur(20px);
78
+ border: 1px solid rgba(139, 92, 246, 0.35);
79
+ border-radius: 16px;
80
+ flex-direction: column;
81
+ overflow: hidden;
82
+ box-shadow: 0 8px 48px rgba(0,0,0,0.7), 0 0 0 1px rgba(139,92,246,0.1);
83
+ }
84
+ #chat.visible { display: flex; }
85
+
86
+ /* Header */
87
+ .chat-header {
88
+ display: flex; align-items: center; gap: 10px;
89
+ padding: 12px 14px;
90
+ background: rgba(139, 92, 246, 0.08);
91
+ border-bottom: 1px solid rgba(139, 92, 246, 0.2);
92
+ -webkit-app-region: drag;
93
+ flex-shrink: 0;
94
+ }
95
+ .chat-header .logo {
96
+ width: 26px; height: 26px; border-radius: 7px;
97
+ background: linear-gradient(135deg, #7c3aed, #a855f7);
98
+ display: flex; align-items: center; justify-content: center;
99
+ font-size: 13px; flex-shrink: 0;
100
+ -webkit-app-region: no-drag;
101
+ }
102
+ .chat-header .title {
103
+ flex: 1; font-size: 13px; font-weight: 600; color: #e2e8f0;
104
+ -webkit-app-region: no-drag;
105
+ }
106
+ .header-actions {
107
+ display: flex; align-items: center; gap: 8px;
108
+ -webkit-app-region: no-drag;
109
+ }
110
+ .header-btn {
111
+ width: 26px; height: 26px; border-radius: 6px;
112
+ background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
113
+ color: #94a3b8; font-size: 12px;
114
+ display: flex; align-items: center; justify-content: center;
115
+ cursor: pointer; transition: background 0.15s, color 0.15s;
116
+ }
117
+ .header-btn:hover { background: rgba(255,255,255,0.12); color: #e2e8f0; }
118
+
119
+ /* Watching toggle in header */
120
+ #watching-toggle {
121
+ display: flex; align-items: center; gap: 5px;
122
+ font-size: 11px; color: #94a3b8; cursor: pointer;
123
+ padding: 3px 8px; border-radius: 12px;
124
+ background: rgba(255,255,255,0.05);
125
+ border: 1px solid rgba(255,255,255,0.08);
126
+ transition: background 0.15s;
127
+ -webkit-app-region: no-drag;
128
+ }
129
+ #watching-toggle:hover { background: rgba(255,255,255,0.1); }
130
+ #watching-toggle .dot {
131
+ width: 6px; height: 6px; border-radius: 50%;
132
+ background: #22c55e; transition: background 0.3s;
133
+ }
134
+ #watching-toggle.paused .dot { background: #eab308; }
135
+ #watching-toggle.paused .watch-label::after { content: 'Paused'; }
136
+ #watching-toggle:not(.paused) .watch-label::after { content: 'Watching'; }
137
+
138
+ /* Ambient context bar */
139
+ #context-bar {
140
+ padding: 7px 14px;
141
+ background: rgba(139, 92, 246, 0.05);
142
+ border-bottom: 1px solid rgba(139, 92, 246, 0.12);
143
+ font-size: 11px; color: #7c6fa0;
144
+ display: flex; align-items: center; gap: 6px;
145
+ flex-shrink: 0; min-height: 30px;
146
+ }
147
+ #context-bar .ctx-icon { font-size: 12px; }
148
+ #context-bar .ctx-text {
149
+ flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
150
+ }
151
+ #context-bar.hidden { display: none; }
152
+
153
+ /* Messages area */
154
+ #messages {
155
+ flex: 1; overflow-y: auto; padding: 12px 14px;
156
+ display: flex; flex-direction: column; gap: 10px;
157
+ scroll-behavior: smooth;
158
+ }
159
+ #messages::-webkit-scrollbar { width: 4px; }
160
+ #messages::-webkit-scrollbar-track { background: transparent; }
161
+ #messages::-webkit-scrollbar-thumb { background: rgba(139,92,246,0.3); border-radius: 2px; }
162
+
163
+ .msg { display: flex; flex-direction: column; gap: 2px; max-width: 88%; }
164
+ .msg.user { align-self: flex-end; align-items: flex-end; }
165
+ .msg.agent { align-self: flex-start; align-items: flex-start; }
166
+
167
+ .msg-bubble {
168
+ padding: 8px 12px; border-radius: 12px;
169
+ font-size: 13px; line-height: 1.5; color: #e2e8f0;
170
+ word-break: break-word;
171
+ }
172
+ .msg.user .msg-bubble {
173
+ background: linear-gradient(135deg, #7c3aed, #6d28d9);
174
+ border-bottom-right-radius: 4px;
175
+ }
176
+ .msg.agent .msg-bubble {
177
+ background: rgba(255,255,255,0.07);
178
+ border: 1px solid rgba(255,255,255,0.08);
179
+ border-bottom-left-radius: 4px;
180
+ }
181
+ .msg-time { font-size: 10px; color: #4b5563; }
182
+
183
+ /* Typing indicator */
184
+ #typing {
185
+ display: none; align-self: flex-start;
186
+ padding: 8px 14px; background: rgba(255,255,255,0.07);
187
+ border: 1px solid rgba(255,255,255,0.08);
188
+ border-radius: 12px; border-bottom-left-radius: 4px;
189
+ }
190
+ #typing.visible { display: flex; gap: 4px; align-items: center; }
191
+ .dot-pulse { width: 6px; height: 6px; border-radius: 50%; background: #7c3aed; animation: pulse 1.2s infinite; }
192
+ .dot-pulse:nth-child(2) { animation-delay: 0.2s; }
193
+ .dot-pulse:nth-child(3) { animation-delay: 0.4s; }
194
+ @keyframes pulse { 0%,80%,100% { opacity: 0.3; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1); } }
195
+
196
+ /* Input area */
197
+ .chat-input-area {
198
+ padding: 10px 12px;
199
+ border-top: 1px solid rgba(255,255,255,0.07);
200
+ display: flex; align-items: flex-end; gap: 8px;
201
+ flex-shrink: 0;
202
+ -webkit-app-region: no-drag;
203
+ }
204
+ #msg-input {
205
+ flex: 1; background: rgba(255,255,255,0.06);
206
+ border: 1px solid rgba(255,255,255,0.1); border-radius: 10px;
207
+ color: #e2e8f0; font-size: 13px; padding: 8px 12px;
208
+ resize: none; outline: none; max-height: 100px;
209
+ font-family: inherit; line-height: 1.4;
210
+ transition: border-color 0.15s;
211
+ }
212
+ #msg-input::placeholder { color: #4b5563; }
213
+ #msg-input:focus { border-color: rgba(139,92,246,0.5); }
214
+
215
+ #send-btn {
216
+ width: 34px; height: 34px; border-radius: 9px;
217
+ background: linear-gradient(135deg, #7c3aed, #6d28d9);
218
+ border: none; cursor: pointer; color: white; font-size: 14px;
219
+ display: flex; align-items: center; justify-content: center;
220
+ transition: opacity 0.15s, transform 0.1s; flex-shrink: 0;
221
+ }
222
+ #send-btn:hover { opacity: 0.85; }
223
+ #send-btn:active { transform: scale(0.95); }
224
+ #send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
225
+
226
+ /* Empty state */
227
+ #empty-state {
228
+ flex: 1; display: flex; flex-direction: column;
229
+ align-items: center; justify-content: center; gap: 8px;
230
+ color: #4b5563; text-align: center; padding: 20px;
231
+ }
232
+ #empty-state .empty-icon { font-size: 32px; }
233
+ #empty-state .empty-title { font-size: 14px; font-weight: 600; color: #6b7280; }
234
+ #empty-state .empty-sub { font-size: 12px; line-height: 1.5; }
235
+ </style>
236
+ </head>
237
+ <body>
238
+
239
+ <!-- ── Pill (collapsed) ──────────────────────────────────────────────────── -->
240
+ <div id="pill">
241
+ <div class="pill-logo">⬡</div>
242
+ <span class="pill-label">Promethios</span>
243
+ <div id="status-dot" data-tooltip="Watching — click to pause"></div>
244
+ </div>
245
+
246
+ <!-- ── Chat window (expanded) ────────────────────────────────────────────── -->
247
+ <div id="chat">
248
+
249
+ <!-- Header -->
250
+ <div class="chat-header">
251
+ <div class="logo">⬡</div>
252
+ <span class="title">Promethios</span>
253
+ <div class="header-actions">
254
+ <!-- Watching toggle -->
255
+ <div id="watching-toggle" title="Toggle context watching">
256
+ <div class="dot"></div>
257
+ <span class="watch-label"></span>
258
+ </div>
259
+ <!-- Collapse to pill -->
260
+ <div class="header-btn" id="collapse-btn" title="Collapse">╌</div>
261
+ </div>
262
+ </div>
263
+
264
+ <!-- Ambient context bar -->
265
+ <div id="context-bar" class="hidden">
266
+ <span class="ctx-icon">⚡</span>
267
+ <span class="ctx-text" id="ctx-text">Watching your screen...</span>
268
+ </div>
269
+
270
+ <!-- Messages -->
271
+ <div id="messages">
272
+ <div id="empty-state">
273
+ <div class="empty-icon">🤖</div>
274
+ <div class="empty-title">Promethios is ready</div>
275
+ <div class="empty-sub">Ask anything — I can see what you're working on and help without you switching tabs.</div>
276
+ </div>
277
+ </div>
278
+
279
+ <!-- Typing indicator -->
280
+ <div id="typing">
281
+ <div class="dot-pulse"></div>
282
+ <div class="dot-pulse"></div>
283
+ <div class="dot-pulse"></div>
284
+ </div>
285
+
286
+ <!-- Input -->
287
+ <div class="chat-input-area">
288
+ <textarea id="msg-input" rows="1" placeholder="Ask anything..."></textarea>
289
+ <button id="send-btn">↑</button>
290
+ </div>
291
+
292
+ </div>
293
+
294
+ <script src="overlay.js"></script>
295
+ </body>
296
+ </html>
@@ -0,0 +1,311 @@
1
+ /**
2
+ * overlay.js — Renderer process logic
3
+ *
4
+ * Handles:
5
+ * - Config injection from main process (authToken, apiBase, threadId)
6
+ * - Pill ↔ chat expand/collapse
7
+ * - Watching toggle (green/yellow dot)
8
+ * - Ambient context display in the context bar
9
+ * - Chat message sending via Promethios API
10
+ * - Streaming response rendering
11
+ */
12
+
13
+ // ── State ────────────────────────────────────────────────────────────────────
14
+ let config = { authToken: '', apiBase: 'https://api.promethios.ai', threadId: '', dev: false };
15
+ let isExpanded = false;
16
+ let isWatching = true;
17
+ let messages = [];
18
+ let isStreaming = false;
19
+ let contextPollTimer = null;
20
+
21
+ // ── DOM refs ─────────────────────────────────────────────────────────────────
22
+ const pill = document.getElementById('pill');
23
+ const chat = document.getElementById('chat');
24
+ const statusDot = document.getElementById('status-dot');
25
+ const watchingToggle = document.getElementById('watching-toggle');
26
+ const collapseBtn = document.getElementById('collapse-btn');
27
+ const contextBar = document.getElementById('context-bar');
28
+ const ctxText = document.getElementById('ctx-text');
29
+ const messagesEl = document.getElementById('messages');
30
+ const emptyState = document.getElementById('empty-state');
31
+ const typingEl = document.getElementById('typing');
32
+ const msgInput = document.getElementById('msg-input');
33
+ const sendBtn = document.getElementById('send-btn');
34
+
35
+ // ── Receive config from main process ─────────────────────────────────────────
36
+ window.promethios.onConfig((data) => {
37
+ config = { ...config, ...data };
38
+ if (config.dev) console.log('[overlay] Config received:', config);
39
+ startContextPoll();
40
+ });
41
+
42
+ // ── Receive expand/collapse from main process ─────────────────────────────────
43
+ window.promethios.onExpand((expanded) => {
44
+ setExpanded(expanded);
45
+ });
46
+
47
+ // ── Pill click → expand ───────────────────────────────────────────────────────
48
+ pill.addEventListener('click', (e) => {
49
+ if (e.target === statusDot) return; // handled separately
50
+ window.promethios.toggleExpand(true);
51
+ });
52
+
53
+ // ── Status dot click → toggle watching ───────────────────────────────────────
54
+ statusDot.addEventListener('click', (e) => {
55
+ e.stopPropagation();
56
+ toggleWatching();
57
+ });
58
+
59
+ watchingToggle.addEventListener('click', () => {
60
+ toggleWatching();
61
+ });
62
+
63
+ // ── Collapse button ───────────────────────────────────────────────────────────
64
+ collapseBtn.addEventListener('click', () => {
65
+ window.promethios.toggleExpand(false);
66
+ });
67
+
68
+ // ── Send message ──────────────────────────────────────────────────────────────
69
+ sendBtn.addEventListener('click', sendMessage);
70
+ msgInput.addEventListener('keydown', (e) => {
71
+ if (e.key === 'Enter' && !e.shiftKey) {
72
+ e.preventDefault();
73
+ sendMessage();
74
+ }
75
+ });
76
+
77
+ // Auto-resize textarea
78
+ msgInput.addEventListener('input', () => {
79
+ msgInput.style.height = 'auto';
80
+ msgInput.style.height = Math.min(msgInput.scrollHeight, 100) + 'px';
81
+ });
82
+
83
+ // ── Expand / collapse ─────────────────────────────────────────────────────────
84
+ function setExpanded(expanded) {
85
+ isExpanded = expanded;
86
+ if (expanded) {
87
+ pill.style.display = 'none';
88
+ chat.classList.add('visible');
89
+ msgInput.focus();
90
+ } else {
91
+ pill.style.display = 'flex';
92
+ chat.classList.remove('visible');
93
+ }
94
+ }
95
+
96
+ // ── Watching toggle ───────────────────────────────────────────────────────────
97
+ function toggleWatching() {
98
+ isWatching = !isWatching;
99
+
100
+ // Update pill dot
101
+ statusDot.classList.toggle('paused', !isWatching);
102
+ statusDot.setAttribute('data-tooltip', isWatching
103
+ ? 'Watching — click to pause'
104
+ : 'Paused — click to resume watching');
105
+
106
+ // Update header toggle
107
+ watchingToggle.classList.toggle('paused', !isWatching);
108
+
109
+ // Hide/show context bar
110
+ if (!isWatching) {
111
+ contextBar.classList.add('hidden');
112
+ ctxText.textContent = '';
113
+ }
114
+
115
+ // Notify main process
116
+ window.promethios.setWatching(isWatching);
117
+
118
+ // Tell the API to pause/resume context capture
119
+ if (config.authToken) {
120
+ fetch(`${config.apiBase}/api/local-bridge/context/watching`, {
121
+ method: 'POST',
122
+ headers: { Authorization: `Bearer ${config.authToken}`, 'Content-Type': 'application/json' },
123
+ body: JSON.stringify({ watching: isWatching }),
124
+ }).catch(() => {});
125
+ }
126
+ }
127
+
128
+ // ── Context polling ───────────────────────────────────────────────────────────
129
+ function startContextPoll() {
130
+ if (contextPollTimer) clearInterval(contextPollTimer);
131
+ contextPollTimer = setInterval(fetchContext, 5000);
132
+ fetchContext(); // immediate first fetch
133
+ }
134
+
135
+ async function fetchContext() {
136
+ if (!isWatching || !config.authToken) return;
137
+ try {
138
+ const res = await fetch(`${config.apiBase}/api/local-bridge/context`, {
139
+ headers: { Authorization: `Bearer ${config.authToken}` },
140
+ });
141
+ if (!res.ok) return;
142
+ const ctx = await res.json();
143
+ updateContextBar(ctx);
144
+ } catch { /* ignore */ }
145
+ }
146
+
147
+ function updateContextBar(ctx) {
148
+ if (!ctx || (!ctx.active_app && !ctx.active_url)) {
149
+ contextBar.classList.add('hidden');
150
+ return;
151
+ }
152
+ contextBar.classList.remove('hidden');
153
+
154
+ let label = '';
155
+ if (ctx.active_url) {
156
+ try {
157
+ const url = new URL(ctx.active_url);
158
+ label = url.hostname.replace('www.', '');
159
+ if (ctx.active_window?.title) {
160
+ const title = ctx.active_window.title.replace(/ [-–|].*$/, '').trim();
161
+ if (title && title.length < 50) label = `${title} · ${label}`;
162
+ }
163
+ } catch {
164
+ label = ctx.active_url.slice(0, 60);
165
+ }
166
+ } else if (ctx.active_app) {
167
+ label = ctx.active_app;
168
+ if (ctx.active_window?.title) {
169
+ const title = ctx.active_window.title.replace(/ [-–|].*$/, '').trim();
170
+ if (title && title.length < 50 && title !== ctx.active_app) label = `${title} · ${ctx.active_app}`;
171
+ }
172
+ }
173
+
174
+ ctxText.textContent = label;
175
+ }
176
+
177
+ // ── Send message ──────────────────────────────────────────────────────────────
178
+ async function sendMessage() {
179
+ const text = msgInput.value.trim();
180
+ if (!text || isStreaming) return;
181
+ if (!config.authToken) {
182
+ appendMessage('agent', 'Not connected. Please restart the bridge with a valid token.');
183
+ return;
184
+ }
185
+
186
+ // Clear input
187
+ msgInput.value = '';
188
+ msgInput.style.height = 'auto';
189
+
190
+ // Add user message to UI
191
+ appendMessage('user', text);
192
+ hideEmptyState();
193
+
194
+ // Show typing indicator
195
+ setStreaming(true);
196
+
197
+ try {
198
+ // Use the Promethios chat/stream API
199
+ // If we have a threadId, send to that thread; otherwise create a new one
200
+ const endpoint = config.threadId
201
+ ? `${config.apiBase}/api/chat/stream`
202
+ : `${config.apiBase}/api/chat/stream`;
203
+
204
+ const body = {
205
+ message: text,
206
+ threadId: config.threadId || null,
207
+ source: 'overlay',
208
+ overlayMode: true,
209
+ };
210
+
211
+ const res = await fetch(endpoint, {
212
+ method: 'POST',
213
+ headers: {
214
+ Authorization: `Bearer ${config.authToken}`,
215
+ 'Content-Type': 'application/json',
216
+ Accept: 'text/event-stream',
217
+ },
218
+ body: JSON.stringify(body),
219
+ });
220
+
221
+ if (!res.ok) {
222
+ const err = await res.text();
223
+ appendMessage('agent', `Error: ${res.status} — ${err.slice(0, 200)}`);
224
+ setStreaming(false);
225
+ return;
226
+ }
227
+
228
+ // Stream the response
229
+ const reader = res.body.getReader();
230
+ const decoder = new TextDecoder();
231
+ let agentMsgEl = null;
232
+ let buffer = '';
233
+
234
+ while (true) {
235
+ const { done, value } = await reader.read();
236
+ if (done) break;
237
+
238
+ buffer += decoder.decode(value, { stream: true });
239
+ const lines = buffer.split('\n');
240
+ buffer = lines.pop(); // keep incomplete line
241
+
242
+ for (const line of lines) {
243
+ if (!line.startsWith('data: ')) continue;
244
+ const data = line.slice(6).trim();
245
+ if (data === '[DONE]') continue;
246
+ try {
247
+ const parsed = JSON.parse(data);
248
+ const delta = parsed.choices?.[0]?.delta?.content || parsed.delta?.text || parsed.text || '';
249
+ if (delta) {
250
+ if (!agentMsgEl) {
251
+ agentMsgEl = appendMessage('agent', '', true);
252
+ }
253
+ agentMsgEl.querySelector('.msg-bubble').textContent += delta;
254
+ scrollToBottom();
255
+ }
256
+ // Capture threadId if returned
257
+ if (parsed.threadId && !config.threadId) {
258
+ config.threadId = parsed.threadId;
259
+ }
260
+ } catch { /* non-JSON line, skip */ }
261
+ }
262
+ }
263
+
264
+ if (!agentMsgEl) {
265
+ appendMessage('agent', '(No response)');
266
+ }
267
+
268
+ } catch (err) {
269
+ appendMessage('agent', `Connection error: ${err.message}`);
270
+ }
271
+
272
+ setStreaming(false);
273
+ }
274
+
275
+ // ── UI helpers ────────────────────────────────────────────────────────────────
276
+ function appendMessage(role, text, returnEl = false) {
277
+ const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
278
+ const div = document.createElement('div');
279
+ div.className = `msg ${role}`;
280
+ div.innerHTML = `
281
+ <div class="msg-bubble">${escapeHtml(text)}</div>
282
+ <span class="msg-time">${now}</span>
283
+ `;
284
+ messagesEl.appendChild(div);
285
+ scrollToBottom();
286
+ if (returnEl) return div;
287
+ }
288
+
289
+ function hideEmptyState() {
290
+ if (emptyState) emptyState.style.display = 'none';
291
+ }
292
+
293
+ function setStreaming(streaming) {
294
+ isStreaming = streaming;
295
+ typingEl.classList.toggle('visible', streaming);
296
+ sendBtn.disabled = streaming;
297
+ scrollToBottom();
298
+ }
299
+
300
+ function scrollToBottom() {
301
+ messagesEl.scrollTop = messagesEl.scrollHeight;
302
+ }
303
+
304
+ function escapeHtml(str) {
305
+ return str
306
+ .replace(/&/g, '&amp;')
307
+ .replace(/</g, '&lt;')
308
+ .replace(/>/g, '&gt;')
309
+ .replace(/"/g, '&quot;')
310
+ .replace(/\n/g, '<br>');
311
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Promethios Bridge — Post-Install Script
4
+ * Silently installs Electron and Playwright Chromium after the main package installs.
5
+ * Both are optional — the bridge works without them, but the overlay and browser tools
6
+ * require them. We install in the background so the user doesn't have to do anything.
7
+ */
8
+
9
+ const { execSync, spawn } = require('child_process');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ const PKG_DIR = path.resolve(__dirname, '..');
14
+
15
+ function log(msg) {
16
+ process.stdout.write(`\x1b[36m[Promethios]\x1b[0m ${msg}\n`);
17
+ }
18
+
19
+ function installElectron() {
20
+ try {
21
+ // Check if electron is already available
22
+ require.resolve('electron');
23
+ log('Electron already installed — overlay ready.');
24
+ return;
25
+ } catch (_) {
26
+ // Not installed yet — install it
27
+ }
28
+
29
+ log('Installing Electron for the floating overlay (this may take a moment)...');
30
+ try {
31
+ execSync('npm install --no-save electron@^29.0.0', {
32
+ cwd: PKG_DIR,
33
+ stdio: 'pipe',
34
+ timeout: 120000
35
+ });
36
+ log('Electron installed — floating overlay is ready.');
37
+ } catch (err) {
38
+ // Non-fatal — bridge works without overlay
39
+ log('Note: Electron install skipped (overlay will be unavailable). Run `npm install electron` in the bridge directory to enable it.');
40
+ }
41
+ }
42
+
43
+ function installPlaywright() {
44
+ try {
45
+ const playwrightCli = path.join(PKG_DIR, 'node_modules', 'playwright', 'cli.js');
46
+ if (fs.existsSync(playwrightCli)) {
47
+ log('Installing Playwright Chromium for browser tools...');
48
+ execSync(`node "${playwrightCli}" install chromium`, {
49
+ cwd: PKG_DIR,
50
+ stdio: 'pipe',
51
+ timeout: 180000
52
+ });
53
+ log('Playwright Chromium installed — browser tools ready.');
54
+ }
55
+ } catch (_) {
56
+ // Non-fatal
57
+ }
58
+ }
59
+
60
+ // Run installs
61
+ installElectron();
62
+ installPlaywright();