promethios-bridge 1.8.0 → 2.0.0

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.8.0",
3
+ "version": "2.0.0",
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": {
@@ -53,7 +53,9 @@
53
53
  },
54
54
  "optionalDependencies": {
55
55
  "playwright": "^1.42.0",
56
- "electron": "^29.0.0"
56
+ "electron": "^29.0.0",
57
+ "screenshot-desktop": "^1.12.7",
58
+ "sharp": "^0.33.0"
57
59
  },
58
60
  "engines": {
59
61
  "node": ">=18.0.0"
package/src/bridge.js CHANGED
@@ -21,6 +21,20 @@ const ora = require('ora');
21
21
  const fetch = require('node-fetch');
22
22
  const { executeLocalTool } = require('./executor');
23
23
  const { captureContext } = require('./contextCapture');
24
+ const { startMcpServer } = require('./mcp-server');
25
+ const { setPinnedRegion, setPinnedApps, registerBrowserPageAccessor } = require('./tools/desktop');
26
+
27
+ // Wire the browser-dom tools to the shared Playwright context.
28
+ // The context is created lazily in executor.js when browser_control is first used.
29
+ // We expose a getter so browser-dom tools can access the live page.
30
+ registerBrowserPageAccessor(async () => {
31
+ if (!global.__playwrightContext) {
32
+ throw new Error('Browser not open. Use the browser_control tool to navigate to a page first, then retry.');
33
+ }
34
+ const pages = global.__playwrightContext.pages();
35
+ return pages.length > 0 ? pages[pages.length - 1] : await global.__playwrightContext.newPage();
36
+ });
37
+ const { initAndroidTools } = require('./tools/android');
24
38
 
25
39
  // Optional: Electron overlay window (bundled in src/overlay — gracefully skipped if Electron not available)
26
40
  let launchOverlay = null;
@@ -196,6 +210,22 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
196
210
  // Health check
197
211
  app.get('/health', (req, res) => res.json({ ok: true, version: require('../package.json').version }));
198
212
 
213
+ // ── /status: used by the Electron overlay to auto-connect without manual token entry ──
214
+ // Only accessible from localhost (127.0.0.1 or ::1) for security.
215
+ let bridgeUsername = null; // set after registerBridge resolves
216
+ app.get('/status', (req, res) => {
217
+ const ip = req.socket.remoteAddress || '';
218
+ const isLocal = ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
219
+ if (!isLocal) return res.status(403).json({ error: 'localhost only' });
220
+ res.json({
221
+ ok: true,
222
+ token: overlayAuthToken || null,
223
+ username: bridgeUsername || require('os').userInfo().username,
224
+ version: require('../package.json').version,
225
+ port,
226
+ });
227
+ });
228
+
199
229
  // Legacy direct tool-call endpoint (kept for backward compatibility with
200
230
  // older backend versions that call callbackUrl/tool-call directly)
201
231
  app.post('/tool-call', async (req, res) => {
@@ -303,8 +333,11 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
303
333
  'Authorization': `Bearer ${token}`,
304
334
  },
305
335
  body: JSON.stringify({
306
- message: req.body.message,
336
+ message: req.body.message,
307
337
  conversationHistory: req.body.conversationHistory || [],
338
+ model: req.body.model || undefined,
339
+ provider: req.body.provider || undefined,
340
+ context: req.body.context || undefined,
308
341
  }),
309
342
  });
310
343
  const data = await upstream.json();
@@ -761,6 +794,20 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
761
794
  const callbackUrl = `http://127.0.0.1:${port}`;
762
795
  console.log(chalk.green(` ✓ Local server listening on port ${port}`));
763
796
 
797
+ // ── MCP server (port 7824) — universal tool layer for all frameworks ────────
798
+ const mcpPort = port + 1; // 7824 when main bridge is on 7823
799
+
800
+ // Init Android tools with auth context
801
+ initAndroidTools({ authToken, apiBase });
802
+
803
+ try {
804
+ await startMcpServer({ port: mcpPort, authToken, log });
805
+ console.log(chalk.cyan(` ✓ MCP server ready on port ${mcpPort} (Claude, OpenClaw, ChatGPT, Cursor)`));
806
+ console.log(chalk.cyan(` ✓ Android bridge tools registered (SMS, contacts, calls, notifications)`));
807
+ } catch (err) {
808
+ log('MCP server failed to start (non-critical):', err.message);
809
+ }
810
+
764
811
  // ── Step 3: Register with Promethios API ─────────────────────────────────
765
812
  const regSpinner = ora(' Registering bridge with Promethios...').start();
766
813
  try {
@@ -788,6 +835,7 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
788
835
  // ── Step 4c: Launch overlay ──────────────────────────────────────────────
789
836
  // Set the auth token so the /overlay route can embed it in the HTML.
790
837
  overlayAuthToken = authToken;
838
+ bridgeUsername = require('os').userInfo().username;
791
839
 
792
840
  // Try Electron first (available when installed globally or via postinstall).
793
841
  // Fall back to opening the browser-based overlay at http://localhost:<port>/overlay.
@@ -0,0 +1,348 @@
1
+ /**
2
+ * mcp-server.js — Promethios MCP Server
3
+ *
4
+ * Implements the Model Context Protocol (MCP) so that any MCP-compatible
5
+ * AI framework can discover and call Promethios desktop tools.
6
+ *
7
+ * Supported frameworks:
8
+ * - Claude Desktop (Anthropic's MCP client)
9
+ * - Cursor / Windsurf
10
+ * - Any framework implementing MCP spec
11
+ *
12
+ * Also exposes a REST API for non-MCP frameworks:
13
+ * GET /tools → tool manifest (OpenAPI-compatible)
14
+ * POST /tools/call → execute a tool
15
+ * GET /tools/context → get current context block (for context injection)
16
+ *
17
+ * Authentication: Bearer token (same Promethios token as the main bridge)
18
+ *
19
+ * Runs on port 7824 alongside the main bridge on 7823.
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const express = require('express');
25
+ const { TOOL_MANIFEST, executeDesktopTool, buildContextBlock } = require('./tools/desktop');
26
+ const { ANDROID_TOOL_SCHEMAS, executeAndroidTool, isAndroidTool } = require('./tools/android');
27
+
28
+ // Also include the core executor tools in the manifest for external frameworks
29
+ const { executeLocalTool } = require('./executor');
30
+
31
+ // ── Extended manifest — adds executor tools for external frameworks ───────────
32
+ const EXECUTOR_TOOL_MANIFEST = [
33
+ {
34
+ name: 'read_file',
35
+ description: 'Read the contents of a file on the user\'s desktop computer.',
36
+ inputSchema: {
37
+ type: 'object',
38
+ required: ['path'],
39
+ properties: {
40
+ path: { type: 'string', description: 'Absolute or ~ relative path to the file' },
41
+ encoding: { type: 'string', description: 'File encoding (default: utf8)', default: 'utf8' },
42
+ },
43
+ },
44
+ handler: async (args) => executeLocalTool({ toolName: 'read_file', args }),
45
+ },
46
+ {
47
+ name: 'write_file',
48
+ description: 'Write or create a file on the user\'s desktop computer.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ required: ['path', 'content'],
52
+ properties: {
53
+ path: { type: 'string', description: 'Absolute or ~ relative path to the file' },
54
+ content: { type: 'string', description: 'Content to write' },
55
+ },
56
+ },
57
+ handler: async (args) => executeLocalTool({ toolName: 'write_file', args }),
58
+ },
59
+ {
60
+ name: 'list_directory',
61
+ description: 'List files and folders in a directory on the user\'s desktop computer.',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ path: { type: 'string', description: 'Directory path (default: home directory)', default: '~' },
66
+ },
67
+ },
68
+ handler: async (args) => executeLocalTool({ toolName: 'list_directory', args }),
69
+ },
70
+ {
71
+ name: 'run_command',
72
+ description: 'Execute a shell command on the user\'s desktop computer and return the output.',
73
+ inputSchema: {
74
+ type: 'object',
75
+ required: ['command'],
76
+ properties: {
77
+ command: { type: 'string', description: 'Shell command to execute' },
78
+ cwd: { type: 'string', description: 'Working directory (optional)' },
79
+ timeout: { type: 'number', description: 'Timeout in milliseconds (default: 30000)' },
80
+ },
81
+ },
82
+ handler: async (args) => executeLocalTool({ toolName: 'run_command', args }),
83
+ },
84
+ {
85
+ name: 'open_browser',
86
+ description: 'Open a URL in the user\'s default browser.',
87
+ inputSchema: {
88
+ type: 'object',
89
+ required: ['url'],
90
+ properties: {
91
+ url: { type: 'string', description: 'URL to open' },
92
+ },
93
+ },
94
+ handler: async (args) => executeLocalTool({ toolName: 'open_browser', args }),
95
+ },
96
+ ];
97
+
98
+ // Android tools — add handlers to the schemas
99
+ const ANDROID_TOOL_MANIFEST = ANDROID_TOOL_SCHEMAS.map(schema => ({
100
+ ...schema,
101
+ handler: async (args) => executeAndroidTool(schema.name, args),
102
+ }));
103
+
104
+ // Full combined manifest — desktop tools + executor tools + android tools
105
+ const FULL_MANIFEST = [...TOOL_MANIFEST, ...EXECUTOR_TOOL_MANIFEST, ...ANDROID_TOOL_MANIFEST];
106
+
107
+ // ── MCP Server ────────────────────────────────────────────────────────────────
108
+
109
+ function createMcpServer(authToken) {
110
+ const app = express();
111
+ app.use(express.json({ limit: '10mb' }));
112
+
113
+ // CORS — allow all origins so web-based AI tools can connect
114
+ app.use((req, res, next) => {
115
+ res.setHeader('Access-Control-Allow-Origin', '*');
116
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
117
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
118
+ if (req.method === 'OPTIONS') return res.sendStatus(200);
119
+ next();
120
+ });
121
+
122
+ // ── Auth middleware ───────────────────────────────────────────────────────
123
+ function requireAuth(req, res, next) {
124
+ if (!authToken) return next(); // No token configured — open access (dev mode)
125
+ const header = req.headers['authorization'] || '';
126
+ const token = header.replace(/^Bearer\s+/i, '').trim();
127
+ if (token !== authToken) {
128
+ return res.status(401).json({ error: 'Invalid or missing Promethios token' });
129
+ }
130
+ next();
131
+ }
132
+
133
+ // ── Health check ─────────────────────────────────────────────────────────
134
+ app.get('/health', (req, res) => {
135
+ res.json({
136
+ status: 'ok',
137
+ service: 'promethios-mcp',
138
+ version: require('../package.json').version,
139
+ tools: FULL_MANIFEST.length,
140
+ platform: process.platform,
141
+ });
142
+ });
143
+
144
+ // ── Tool discovery ────────────────────────────────────────────────────────
145
+ // GET /tools — returns the full tool manifest
146
+ // Compatible with MCP tool discovery and OpenAPI Custom Actions
147
+ app.get('/tools', requireAuth, (req, res) => {
148
+ res.json({
149
+ tools: FULL_MANIFEST.map(t => ({
150
+ name: t.name,
151
+ description: t.description,
152
+ inputSchema: t.inputSchema,
153
+ })),
154
+ });
155
+ });
156
+
157
+ // ── MCP protocol endpoint ─────────────────────────────────────────────────
158
+ // POST /mcp — handles MCP JSON-RPC messages
159
+ // Claude Desktop and other MCP clients send requests here
160
+ app.post('/mcp', requireAuth, async (req, res) => {
161
+ const { method, params, id } = req.body;
162
+
163
+ try {
164
+ if (method === 'tools/list') {
165
+ return res.json({
166
+ jsonrpc: '2.0',
167
+ id,
168
+ result: {
169
+ tools: FULL_MANIFEST.map(t => ({
170
+ name: t.name,
171
+ description: t.description,
172
+ inputSchema: t.inputSchema,
173
+ })),
174
+ },
175
+ });
176
+ }
177
+
178
+ if (method === 'tools/call') {
179
+ const { name, arguments: args } = params;
180
+ const tool = FULL_MANIFEST.find(t => t.name === name);
181
+
182
+ if (!tool) {
183
+ return res.json({
184
+ jsonrpc: '2.0',
185
+ id,
186
+ error: { code: -32601, message: `Tool not found: ${name}` },
187
+ });
188
+ }
189
+
190
+ const result = await tool.handler(args || {});
191
+ return res.json({
192
+ jsonrpc: '2.0',
193
+ id,
194
+ result: {
195
+ content: [
196
+ {
197
+ type: result.screenshot || result.fresh_screenshot ? 'image' : 'text',
198
+ text: result.screenshot || result.fresh_screenshot
199
+ ? undefined
200
+ : JSON.stringify(result, null, 2),
201
+ data: result.screenshot || result.fresh_screenshot || undefined,
202
+ mimeType: result.screenshot || result.fresh_screenshot ? 'image/png' : undefined,
203
+ },
204
+ ],
205
+ },
206
+ });
207
+ }
208
+
209
+ // Unknown method
210
+ return res.json({
211
+ jsonrpc: '2.0',
212
+ id,
213
+ error: { code: -32601, message: `Unknown method: ${method}` },
214
+ });
215
+
216
+ } catch (err) {
217
+ return res.json({
218
+ jsonrpc: '2.0',
219
+ id,
220
+ error: { code: -32603, message: err.message },
221
+ });
222
+ }
223
+ });
224
+
225
+ // ── REST tool execution ───────────────────────────────────────────────────
226
+ // POST /tools/call — execute any tool by name (REST alternative to MCP)
227
+ // Used by ChatGPT Custom Actions, web apps, and non-MCP frameworks
228
+ app.post('/tools/call', requireAuth, async (req, res) => {
229
+ const { tool, args } = req.body;
230
+ if (!tool) return res.status(400).json({ error: 'tool name is required' });
231
+
232
+ const toolDef = FULL_MANIFEST.find(t => t.name === tool);
233
+ if (!toolDef) {
234
+ return res.status(404).json({
235
+ error: `Tool not found: ${tool}`,
236
+ available: FULL_MANIFEST.map(t => t.name),
237
+ });
238
+ }
239
+
240
+ try {
241
+ const result = await toolDef.handler(args || {});
242
+ res.json({ ok: true, tool, result });
243
+ } catch (err) {
244
+ res.status(500).json({ ok: false, tool, error: err.message });
245
+ }
246
+ });
247
+
248
+ // ── Context endpoint ──────────────────────────────────────────────────────
249
+ // GET /tools/context — returns the current context block
250
+ // Used by the Manus floater for context injection
251
+ app.get('/tools/context', requireAuth, async (req, res) => {
252
+ try {
253
+ const includeScreenshot = req.query.screenshot === 'true';
254
+ const block = await buildContextBlock({ include_screenshot: includeScreenshot });
255
+ res.json({ ok: true, ...block });
256
+ } catch (err) {
257
+ res.status(500).json({ ok: false, error: err.message });
258
+ }
259
+ });
260
+
261
+ // ── OpenAPI spec ──────────────────────────────────────────────────────────
262
+ // GET /openapi.json — OpenAPI 3.0 spec for ChatGPT Custom Actions
263
+ app.get('/openapi.json', (req, res) => {
264
+ const host = req.headers.host || 'bridge.promethios.ai';
265
+ res.json({
266
+ openapi: '3.0.0',
267
+ info: {
268
+ title: 'Promethios Desktop Bridge',
269
+ description: 'Give any AI access to the user\'s desktop context, files, and applications.',
270
+ version: require('../package.json').version,
271
+ },
272
+ servers: [{ url: `https://${host}` }],
273
+ paths: {
274
+ '/tools': {
275
+ get: {
276
+ operationId: 'listTools',
277
+ summary: 'List all available desktop tools',
278
+ responses: { '200': { description: 'Tool manifest' } },
279
+ },
280
+ },
281
+ '/tools/call': {
282
+ post: {
283
+ operationId: 'callTool',
284
+ summary: 'Execute a desktop tool',
285
+ requestBody: {
286
+ required: true,
287
+ content: {
288
+ 'application/json': {
289
+ schema: {
290
+ type: 'object',
291
+ required: ['tool'],
292
+ properties: {
293
+ tool: { type: 'string', description: 'Tool name' },
294
+ args: { type: 'object', description: 'Tool arguments' },
295
+ },
296
+ },
297
+ },
298
+ },
299
+ },
300
+ responses: { '200': { description: 'Tool result' } },
301
+ },
302
+ },
303
+ '/tools/context': {
304
+ get: {
305
+ operationId: 'getContext',
306
+ summary: 'Get current desktop context (active app, window, clipboard)',
307
+ parameters: [
308
+ {
309
+ name: 'screenshot', in: 'query', schema: { type: 'boolean' },
310
+ description: 'Include screenshot of pinned region',
311
+ },
312
+ ],
313
+ responses: { '200': { description: 'Context block' } },
314
+ },
315
+ },
316
+ },
317
+ components: {
318
+ securitySchemes: {
319
+ BearerAuth: { type: 'http', scheme: 'bearer' },
320
+ },
321
+ },
322
+ security: [{ BearerAuth: [] }],
323
+ });
324
+ });
325
+
326
+ return app;
327
+ }
328
+
329
+ /**
330
+ * Start the MCP server on the given port.
331
+ * Called from bridge.js during startup.
332
+ */
333
+ function startMcpServer({ port = 7824, authToken, log = console.log } = {}) {
334
+ const app = createMcpServer(authToken);
335
+
336
+ return new Promise((resolve, reject) => {
337
+ const server = app.listen(port, '127.0.0.1', () => {
338
+ log(`[mcp] Promethios MCP server listening on port ${port}`);
339
+ log(`[mcp] Tool discovery: http://127.0.0.1:${port}/tools`);
340
+ log(`[mcp] MCP endpoint: http://127.0.0.1:${port}/mcp`);
341
+ log(`[mcp] OpenAPI spec: http://127.0.0.1:${port}/openapi.json`);
342
+ resolve(server);
343
+ });
344
+ server.on('error', reject);
345
+ });
346
+ }
347
+
348
+ module.exports = { startMcpServer, createMcpServer, FULL_MANIFEST, ANDROID_TOOL_MANIFEST };
@@ -1,101 +1,133 @@
1
1
  /**
2
- * launcher.js
2
+ * launcher.js — Promethios Bridge
3
3
  *
4
- * Called by the bridge CLI after successful authentication.
5
- * Attempts to launch the Electron overlay window. Returns null if Electron
6
- * is not available (which is the case when running via `npx`, since optional
7
- * dependencies are not installed in the npx cache).
4
+ * Called by bridge.js after successful authentication.
5
+ * Finds the pre-built Promethios Desktop binary (downloaded by postinstall.js
6
+ * into ~/.promethios/desktop/) and spawns it with auth credentials via env vars.
8
7
  *
9
- * IMPORTANT: We check for the actual Electron binary on disk, NOT just whether
10
- * the `electron` npm package is present. When running via `npx`, the electron
11
- * package IS downloaded (it's a listed optionalDependency), but its postinstall
12
- * script downloads the real binary separately that binary may not exist yet.
13
- * Using require.resolve('electron') would return true even without the binary,
14
- * causing spawn() to fail silently and the browser fallback to never be reached.
8
+ * Binary search order:
9
+ * 1. PROMETHIOS_DESKTOP_BIN environment variable (dev override)
10
+ * 2. ~/.promethios/desktop/ — where postinstall.js places the binary
11
+ * 3. Electron source-mode fallback (dev only, requires electron in node_modules)
12
+ *
13
+ * The pre-built binary reads its config from env vars:
14
+ * PROMETHIOS_TOKEN, PROMETHIOS_API_BASE, PROMETHIOS_THREAD_ID, PROMETHIOS_DEV
15
15
  */
16
16
 
17
17
  'use strict';
18
18
 
19
19
  const { spawn } = require('child_process');
20
- const path = require('path');
21
- const fs = require('fs');
20
+ const path = require('path');
21
+ const fs = require('fs');
22
+ const os = require('os');
22
23
 
23
- /**
24
- * Find the actual Electron binary on disk.
25
- * Returns the path string if found and executable, or null if not available.
26
- */
24
+ const INSTALL_DIR = path.join(os.homedir(), '.promethios', 'desktop');
25
+
26
+ // ── Find the pre-built binary ─────────────────────────────────────────────────
27
+ function findDesktopBinary() {
28
+ // 1. Explicit env override (useful for local dev / CI)
29
+ if (process.env.PROMETHIOS_DESKTOP_BIN && fs.existsSync(process.env.PROMETHIOS_DESKTOP_BIN)) {
30
+ return process.env.PROMETHIOS_DESKTOP_BIN;
31
+ }
32
+
33
+ // 2. Scan ~/.promethios/desktop/ for a matching binary
34
+ if (fs.existsSync(INSTALL_DIR)) {
35
+ const files = fs.readdirSync(INSTALL_DIR);
36
+ const exts = process.platform === 'win32' ? ['.exe']
37
+ : process.platform === 'darwin' ? ['.dmg', '.app']
38
+ : ['.AppImage'];
39
+
40
+ for (const ext of exts) {
41
+ const match = files.find(f => f.endsWith(ext));
42
+ if (match) {
43
+ const full = path.join(INSTALL_DIR, match);
44
+ // Ensure executable bit is set on Unix
45
+ try {
46
+ if (process.platform !== 'win32') fs.chmodSync(full, 0o755);
47
+ } catch {}
48
+ return full;
49
+ }
50
+ }
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ // ── Find Electron binary for source-mode fallback (dev / CI) ─────────────────
27
57
  function findElectronBinary() {
28
58
  const isWin = process.platform === 'win32';
29
-
30
- // Candidate paths for the real Electron binary (not just the npm package stub)
59
+ const isMac = process.platform === 'darwin';
31
60
  const candidates = [
32
- // 1. Installed inside promethios-bridge's own node_modules (full npm install, not npx)
33
61
  path.join(__dirname, '..', '..', 'node_modules', 'electron', 'dist',
34
- isWin ? 'electron.exe' : process.platform === 'darwin' ? 'Electron.app/Contents/MacOS/Electron' : 'electron'),
35
- // 2. .bin shim (only works if the real binary was downloaded by postinstall)
62
+ isWin ? 'electron.exe' : isMac ? 'Electron.app/Contents/MacOS/Electron' : 'electron'),
36
63
  path.join(__dirname, '..', '..', 'node_modules', '.bin', isWin ? 'electron.cmd' : 'electron'),
37
- // 3. Global electron install
38
- path.join(require('os').homedir(), '.npm', '_npx', '**', 'node_modules', 'electron', 'dist', isWin ? 'electron.exe' : 'electron'),
39
64
  ];
40
-
41
- for (const candidate of candidates) {
42
- // Skip glob-style paths (we can't expand them here)
43
- if (candidate.includes('**')) continue;
44
- try {
45
- if (fs.existsSync(candidate)) {
46
- // Extra check: the .bin shim always exists even without the real binary.
47
- // For the .bin shim, also verify the actual dist binary exists.
48
- if (candidate.includes('.bin')) {
49
- const distBin = path.join(
50
- path.dirname(candidate), '..', 'electron', 'dist',
51
- isWin ? 'electron.exe' : process.platform === 'darwin'
52
- ? 'Electron.app/Contents/MacOS/Electron' : 'electron'
53
- );
54
- if (!fs.existsSync(distBin)) continue;
55
- }
56
- return candidate;
65
+ for (const c of candidates) {
66
+ if (!c.includes('**') && fs.existsSync(c)) {
67
+ if (c.includes('.bin')) {
68
+ const dist = path.join(path.dirname(c), '..', 'electron', 'dist',
69
+ isWin ? 'electron.exe' : isMac ? 'Electron.app/Contents/MacOS/Electron' : 'electron');
70
+ if (!fs.existsSync(dist)) continue;
57
71
  }
58
- } catch { /* ignore */ }
72
+ return c;
73
+ }
59
74
  }
60
-
61
75
  return null;
62
76
  }
63
77
 
64
- /**
65
- * Launch the Promethios overlay Electron window.
66
- * Returns the child process if Electron was found and launched, or null otherwise.
67
- */
78
+ // ── Launch ────────────────────────────────────────────────────────────────────
68
79
  function launchOverlay({ authToken, apiBase = 'https://api.promethios.ai', threadId = '', dev = false } = {}) {
69
- const electronBin = findElectronBinary();
80
+ const desktopBin = findDesktopBinary();
81
+
82
+ if (desktopBin) {
83
+ // ── Mode A: pre-built binary ──────────────────────────────────────────────
84
+ // Pass credentials via env vars — AppImage/exe wrappers don't always
85
+ // forward CLI args to the inner Electron process reliably.
86
+ const env = {
87
+ ...process.env,
88
+ PROMETHIOS_TOKEN: authToken || '',
89
+ PROMETHIOS_API_BASE: apiBase || '',
90
+ PROMETHIOS_THREAD_ID: threadId || '',
91
+ PROMETHIOS_DEV: dev ? '1' : '',
92
+ ELECTRON_NO_ATTACH_CONSOLE: '1',
93
+ };
94
+
95
+ if (dev) console.log(`[overlay] Launching pre-built binary: ${desktopBin}`);
96
+
97
+ const child = spawn(desktopBin, [], { detached: true, stdio: 'ignore', env });
98
+ child.on('error', (err) => {
99
+ if (dev) console.log(`[overlay] Binary spawn error (non-critical): ${err.message}`);
100
+ });
101
+ child.unref();
102
+ return child;
103
+ }
70
104
 
105
+ // ── Mode B: source mode via Electron (dev / CI fallback) ─────────────────
106
+ const electronBin = findElectronBinary();
71
107
  if (!electronBin) {
72
- if (dev) console.log('[overlay] Electron binary not found on disk — skipping Electron overlay.');
108
+ if (dev) console.log('[overlay] No pre-built binary and no Electron found — skipping overlay.');
73
109
  return null;
74
110
  }
75
111
 
76
112
  const mainScript = path.join(__dirname, 'main.js');
77
-
78
113
  const args = [mainScript];
79
- if (authToken) { args.push('--token', authToken); }
80
- if (apiBase) { args.push('--api', apiBase); }
81
- if (threadId) { args.push('--thread', threadId); }
82
- if (dev) { args.push('--dev'); }
114
+ if (authToken) args.push('--token', authToken);
115
+ if (apiBase) args.push('--api', apiBase);
116
+ if (threadId) args.push('--thread', threadId);
117
+ if (dev) args.push('--dev');
118
+
119
+ if (dev) console.log(`[overlay] Launching Electron source mode: ${electronBin}`);
83
120
 
84
121
  const child = spawn(electronBin, args, {
85
122
  detached: true,
86
123
  stdio: 'ignore',
87
124
  env: { ...process.env, ELECTRON_NO_ATTACH_CONSOLE: '1' },
88
125
  });
89
-
90
- // Attach error handler BEFORE unref() to prevent unhandled 'error' event crashes.
91
126
  child.on('error', (err) => {
92
127
  if (dev) console.log(`[overlay] Spawn error (non-critical): ${err.message}`);
93
128
  });
94
-
95
129
  child.unref();
96
-
97
- if (dev) console.log(`[overlay] Launched Electron (pid ${child.pid})`);
98
-
130
+ if (dev) console.log(`[overlay] Launched Electron source mode (pid ${child.pid})`);
99
131
  return child;
100
132
  }
101
133