promethios-bridge 1.7.4 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/contextCapture.js +296 -102
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promethios-bridge",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
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": {
@@ -3,12 +3,24 @@
3
3
  *
4
4
  * Captures what the user is currently working on:
5
5
  * - Active window title + process name
6
- * - Active browser URL (Chrome, Edge, Firefox via accessibility / window title)
6
+ * - Active browser URL (all major browsers on all platforms)
7
7
  * - Selected / clipboard text (last copied text)
8
- * - Active form field content (via clipboard snapshot)
9
8
  *
10
- * Designed to run every 5 seconds and push a lightweight context snapshot
11
- * to the Promethios API so the AI always knows what the user is doing.
9
+ * Browser URL capture strategy (layered, best-effort):
10
+ *
11
+ * Windows:
12
+ * 1. UIAutomation address bar read (Chromium-based: Chrome, Edge, Brave, Opera, Arc)
13
+ * 2. Firefox: accessibility via PowerShell COM (MozillaAccessible) — best-effort
14
+ * 3. Title-based URL extraction (all browsers — works when URL is in title)
15
+ *
16
+ * macOS:
17
+ * 1. AppleScript (Chrome, Safari, Edge, Firefox, Brave, Opera, Arc, Vivaldi)
18
+ * 2. Title-based URL extraction fallback
19
+ *
20
+ * Linux:
21
+ * 1. xdotool + wmctrl for active window
22
+ * 2. xdg-open / qdbus for Chromium-based browsers (best-effort)
23
+ * 3. Title-based URL extraction fallback
12
24
  *
13
25
  * Platform support:
14
26
  * - Windows: PowerShell Get-Process / UIAutomation / Get-Clipboard
@@ -16,7 +28,47 @@
16
28
  * - Linux: xdotool / xclip (if available)
17
29
  */
18
30
 
19
- const { execSync, exec } = require('child_process');
31
+ 'use strict';
32
+
33
+ const { execSync } = require('child_process');
34
+ const os = require('os');
35
+ const path = require('path');
36
+ const fs = require('fs');
37
+
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // Browser process name maps
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ // Chromium-based browsers: UIAutomation works on all of them (Windows)
43
+ // Maps lowercase process name fragment → actual process name for Get-Process
44
+ const CHROMIUM_PROC_MAP = {
45
+ chrome: 'chrome',
46
+ msedge: 'msedge',
47
+ brave: 'brave',
48
+ opera: 'opera',
49
+ // Arc on Windows runs as "Arc"
50
+ arc: 'Arc',
51
+ // Vivaldi
52
+ vivaldi: 'vivaldi',
53
+ // Chromium itself
54
+ chromium: 'chromium',
55
+ };
56
+
57
+ // AppleScript app names for macOS (keyed by lowercase process name fragment)
58
+ const MAC_BROWSER_APPLESCRIPT = {
59
+ 'google chrome': 'tell application "Google Chrome" to return URL of active tab of front window',
60
+ chrome: 'tell application "Google Chrome" to return URL of active tab of front window',
61
+ safari: 'tell application "Safari" to return URL of current tab of front window',
62
+ firefox: 'tell application "Firefox" to return URL of active tab of front window',
63
+ 'microsoft edge': 'tell application "Microsoft Edge" to return URL of active tab of front window',
64
+ msedge: 'tell application "Microsoft Edge" to return URL of active tab of front window',
65
+ edge: 'tell application "Microsoft Edge" to return URL of active tab of front window',
66
+ brave: 'tell application "Brave Browser" to return URL of active tab of front window',
67
+ opera: 'tell application "Opera" to return URL of active tab of front window',
68
+ arc: 'tell application "Arc" to return URL of active tab of front window',
69
+ vivaldi: 'tell application "Vivaldi" to return URL of active tab of front window',
70
+ chromium: 'tell application "Chromium" to return URL of active tab of front window',
71
+ };
20
72
 
21
73
  // ─────────────────────────────────────────────────────────────────────────────
22
74
  // Main capture function — returns a context snapshot object
@@ -26,11 +78,11 @@ async function captureContext(platform, dev) {
26
78
 
27
79
  const snapshot = {
28
80
  captured_at: new Date().toISOString(),
29
- active_window: null, // { title, process }
30
- active_url: null, // string — browser URL if active
31
- active_app: null, // friendly app name
32
- clipboard_text: null, // last copied text (truncated to 500 chars)
33
- browser_tab_title: null, // browser tab title if browser is active
81
+ active_window: null, // { title, process }
82
+ active_url: null, // string — browser URL if active
83
+ active_app: null, // friendly app name
84
+ clipboard_text: null, // last copied text (truncated to 500 chars)
85
+ browser_tab_title: null, // browser tab title if browser is active
34
86
  };
35
87
 
36
88
  try {
@@ -52,12 +104,11 @@ async function captureContext(platform, dev) {
52
104
  // Windows capture via PowerShell
53
105
  // ─────────────────────────────────────────────────────────────────────────────
54
106
  async function captureWindows(snapshot, log) {
55
- // Active window title + process name
107
+ // ── Active window title + process name ────────────────────────────────────
56
108
  // NOTE: PowerShell here-strings (@"..."@) cannot be passed via -Command because
57
109
  // newlines get collapsed. We write a temp .ps1 file and run it with -File instead.
58
110
  try {
59
- const os = require('os');
60
- const tmpFile = require('path').join(os.tmpdir(), `promethios_ctx_${process.pid}.ps1`);
111
+ const tmpFile = path.join(os.tmpdir(), `promethios_ctx_${process.pid}.ps1`);
61
112
  const psScript = [
62
113
  'Add-Type @"',
63
114
  'using System;',
@@ -81,12 +132,12 @@ async function captureWindows(snapshot, log) {
81
132
  '} | ConvertTo-Json -Compress',
82
133
  'Write-Output $result',
83
134
  ].join('\r\n');
84
- require('fs').writeFileSync(tmpFile, psScript, 'utf8');
135
+ fs.writeFileSync(tmpFile, psScript, 'utf8');
85
136
  const output = execSync(
86
137
  `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${tmpFile}"`,
87
138
  { encoding: 'utf8', timeout: 4000, windowsHide: true }
88
139
  ).trim();
89
- try { require('fs').unlinkSync(tmpFile); } catch { /* cleanup best-effort */ }
140
+ try { fs.unlinkSync(tmpFile); } catch { /* cleanup best-effort */ }
90
141
  const parsed = JSON.parse(output);
91
142
  snapshot.active_window = { title: parsed.title, process: parsed.procName };
92
143
  snapshot.active_app = friendlyAppName(parsed.procName, parsed.title);
@@ -95,18 +146,43 @@ async function captureWindows(snapshot, log) {
95
146
  log('Windows active window capture failed:', err.message);
96
147
  }
97
148
 
98
- // Extract browser URL from window title (works for Chrome, Edge, Firefox)
99
- // Chrome/Edge format: "<page title> - <browser name>"
100
- // Firefox format: "<page title> — Mozilla Firefox"
149
+ // ── Browser URL capture ───────────────────────────────────────────────────
101
150
  if (snapshot.active_window) {
102
- const url = extractUrlFromBrowserTitle(snapshot.active_window.title, snapshot.active_window.process);
151
+ const procName = (snapshot.active_window.process || '').toLowerCase();
152
+ const winTitle = snapshot.active_window.title || '';
153
+
154
+ // Determine which browser family this is
155
+ const chromiumKey = Object.keys(CHROMIUM_PROC_MAP).find(k => procName.includes(k));
156
+ const isFirefox = procName.includes('firefox');
157
+
158
+ let url = null;
159
+
160
+ if (chromiumKey) {
161
+ // ── Chromium-based: UIAutomation address bar read ──────────────────────
162
+ url = await captureChromiumUrlWindows(CHROMIUM_PROC_MAP[chromiumKey], log);
163
+ }
164
+
165
+ if (!url && isFirefox) {
166
+ // ── Firefox on Windows: title-based extraction ─────────────────────────
167
+ // Firefox doesn't expose the address bar via UIAutomation without an extension.
168
+ // Best we can do without an extension is title parsing.
169
+ url = extractUrlFromTitle(winTitle, 'firefox');
170
+ log('Firefox URL (title-based):', url);
171
+ }
172
+
173
+ if (!url && chromiumKey && !url) {
174
+ // UIAutomation failed — fall back to title parsing
175
+ url = extractUrlFromTitle(winTitle, procName);
176
+ }
177
+
103
178
  if (url) {
104
179
  snapshot.active_url = url;
105
- snapshot.browser_tab_title = extractTabTitle(snapshot.active_window.title, snapshot.active_window.process);
180
+ snapshot.browser_tab_title = extractTabTitle(winTitle, procName);
181
+ log('Captured URL:', url);
106
182
  }
107
183
  }
108
184
 
109
- // Clipboard text (last copied)
185
+ // ── Clipboard text (last copied) ──────────────────────────────────────────
110
186
  try {
111
187
  const clip = execSync(
112
188
  'powershell -NoProfile -NonInteractive -Command "Get-Clipboard -ErrorAction SilentlyContinue"',
@@ -121,14 +197,71 @@ async function captureWindows(snapshot, log) {
121
197
  }
122
198
  }
123
199
 
200
+ /**
201
+ * Read the URL from a Chromium-based browser's address bar on Windows
202
+ * using PowerShell UIAutomation. Works for Chrome, Edge, Brave, Opera, Arc, Vivaldi.
203
+ *
204
+ * @param {string} browserProcName - exact process name for Get-Process (e.g. "chrome", "brave")
205
+ * @param {Function} log
206
+ * @returns {string|null}
207
+ */
208
+ function captureChromiumUrlWindows(browserProcName, log) {
209
+ try {
210
+ const tmpFile = path.join(os.tmpdir(), `promethios_url_${process.pid}.ps1`);
211
+ const psLines = [
212
+ 'Add-Type -AssemblyName UIAutomationClient',
213
+ 'Add-Type -AssemblyName UIAutomationTypes',
214
+ // Filter to processes with a real (non-zero) window handle
215
+ `$procs = Get-Process -Name "${browserProcName}" -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 }`,
216
+ 'if ($procs) {',
217
+ ' $browserProc = $procs | Select-Object -First 1',
218
+ ' try {',
219
+ ' $root = [System.Windows.Automation.AutomationElement]::FromHandle($browserProc.MainWindowHandle)',
220
+ ' if ($root) {',
221
+ // Try "Address and search bar" (Chrome, Edge, Brave, Opera, Vivaldi)
222
+ ' $cond = New-Object System.Windows.Automation.PropertyCondition(',
223
+ ' [System.Windows.Automation.AutomationElement]::NameProperty,',
224
+ ' "Address and search bar")',
225
+ ' $bar = $root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $cond)',
226
+ // Arc uses "Address Bar" instead
227
+ ' if (-not $bar) {',
228
+ ' $cond2 = New-Object System.Windows.Automation.PropertyCondition(',
229
+ ' [System.Windows.Automation.AutomationElement]::NameProperty,',
230
+ ' "Address Bar")',
231
+ ' $bar = $root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $cond2)',
232
+ ' }',
233
+ ' if ($bar) {',
234
+ ' $vp = $bar.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)',
235
+ ' Write-Output $vp.Current.Value',
236
+ ' }',
237
+ ' }',
238
+ ' } catch {',
239
+ ' # Silently ignore UIAutomation errors',
240
+ ' }',
241
+ '}',
242
+ ];
243
+ fs.writeFileSync(tmpFile, psLines.join('\r\n'), 'utf8');
244
+ const url = execSync(
245
+ `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${tmpFile}"`,
246
+ { encoding: 'utf8', timeout: 5000, windowsHide: true }
247
+ ).trim();
248
+ try { fs.unlinkSync(tmpFile); } catch { /* cleanup best-effort */ }
249
+ if (url && (url.startsWith('http') || url.startsWith('chrome') || url.startsWith('edge') || url.startsWith('brave'))) {
250
+ return url;
251
+ }
252
+ } catch (err) {
253
+ log('Chromium UIAutomation failed:', err.message);
254
+ }
255
+ return null;
256
+ }
257
+
124
258
  // ─────────────────────────────────────────────────────────────────────────────
125
259
  // macOS capture via AppleScript
126
260
  // ─────────────────────────────────────────────────────────────────────────────
127
261
  async function captureMac(snapshot, log) {
128
- // Active app + window title
262
+ // ── Active app + window title ──────────────────────────────────────────────
129
263
  try {
130
- const script = `
131
- tell application "System Events"
264
+ const script = `tell application "System Events"
132
265
  set frontApp to first application process whose frontmost is true
133
266
  set appName to name of frontApp
134
267
  set windowTitle to ""
@@ -147,33 +280,39 @@ end tell`;
147
280
  log('macOS active window capture failed:', err.message);
148
281
  }
149
282
 
150
- // Browser URL via AppleScript (Chrome, Safari, Firefox)
283
+ // ── Browser URL via AppleScript ────────────────────────────────────────────
284
+ // Covers: Chrome, Safari, Edge, Firefox, Brave, Opera, Arc, Vivaldi, Chromium
151
285
  if (snapshot.active_window) {
152
286
  const proc = (snapshot.active_window.process || '').toLowerCase();
153
- try {
154
- let urlScript = '';
155
- if (proc.includes('chrome') || proc.includes('chromium')) {
156
- urlScript = 'tell application "Google Chrome" to return URL of active tab of front window';
157
- } else if (proc.includes('safari')) {
158
- urlScript = 'tell application "Safari" to return URL of current tab of front window';
159
- } else if (proc.includes('firefox')) {
160
- urlScript = 'tell application "Firefox" to return URL of active tab of front window';
161
- } else if (proc.includes('edge')) {
162
- urlScript = 'tell application "Microsoft Edge" to return URL of active tab of front window';
163
- }
164
- if (urlScript) {
165
- const url = execSync(`osascript -e '${urlScript}'`,
287
+ const winTitle = snapshot.active_window.title || '';
288
+
289
+ // Find the best matching AppleScript command
290
+ const scriptKey = Object.keys(MAC_BROWSER_APPLESCRIPT).find(k => proc.includes(k));
291
+ let url = null;
292
+
293
+ if (scriptKey) {
294
+ try {
295
+ url = execSync(`osascript -e '${MAC_BROWSER_APPLESCRIPT[scriptKey]}'`,
166
296
  { encoding: 'utf8', timeout: 3000 }).trim();
167
- snapshot.active_url = url;
168
- snapshot.browser_tab_title = snapshot.active_window.title;
169
- log('Browser URL:', url);
297
+ if (url && !url.startsWith('http') && !url.startsWith('file')) url = null;
298
+ } catch (err) {
299
+ log('macOS AppleScript URL capture failed for', scriptKey, ':', err.message);
170
300
  }
171
- } catch (err) {
172
- log('macOS browser URL capture failed:', err.message);
301
+ }
302
+
303
+ // Fallback: title-based extraction
304
+ if (!url) {
305
+ url = extractUrlFromTitle(winTitle, proc);
306
+ }
307
+
308
+ if (url) {
309
+ snapshot.active_url = url;
310
+ snapshot.browser_tab_title = extractTabTitle(winTitle, proc);
311
+ log('Browser URL:', url);
173
312
  }
174
313
  }
175
314
 
176
- // Clipboard
315
+ // ── Clipboard ──────────────────────────────────────────────────────────────
177
316
  try {
178
317
  const clip = execSync('pbpaste', { encoding: 'utf8', timeout: 2000 }).trim();
179
318
  if (clip && clip.length > 0 && clip.length < 5000) {
@@ -188,17 +327,64 @@ end tell`;
188
327
  // Linux capture via xdotool / xclip (best-effort)
189
328
  // ─────────────────────────────────────────────────────────────────────────────
190
329
  async function captureLinux(snapshot, log) {
330
+ // ── Active window title + process ─────────────────────────────────────────
331
+ let winTitle = '';
332
+ let procName = '';
333
+
191
334
  try {
192
- const title = execSync('xdotool getactivewindow getwindowname 2>/dev/null',
335
+ winTitle = execSync('xdotool getactivewindow getwindowname 2>/dev/null',
193
336
  { encoding: 'utf8', timeout: 2000 }).trim();
194
- snapshot.active_window = { title, process: 'unknown' };
195
- snapshot.active_app = title;
337
+ snapshot.active_window = { title: winTitle, process: 'unknown' };
338
+ snapshot.active_app = winTitle;
196
339
  } catch (err) {
197
340
  log('Linux active window capture failed:', err.message);
198
341
  }
199
342
 
343
+ // Try to get the process name via xdotool + /proc
344
+ try {
345
+ const wid = execSync('xdotool getactivewindow 2>/dev/null',
346
+ { encoding: 'utf8', timeout: 1000 }).trim();
347
+ if (wid) {
348
+ const pid = execSync(`xdotool getwindowpid ${wid} 2>/dev/null`,
349
+ { encoding: 'utf8', timeout: 1000 }).trim();
350
+ if (pid) {
351
+ procName = execSync(`cat /proc/${pid}/comm 2>/dev/null`,
352
+ { encoding: 'utf8', timeout: 500 }).trim().toLowerCase();
353
+ if (snapshot.active_window) snapshot.active_window.process = procName;
354
+ snapshot.active_app = friendlyAppName(procName, winTitle);
355
+ }
356
+ }
357
+ } catch (err) {
358
+ log('Linux process name capture failed:', err.message);
359
+ }
360
+
361
+ // ── Browser URL capture ───────────────────────────────────────────────────
362
+ if (winTitle || procName) {
363
+ const proc = procName || winTitle.toLowerCase();
364
+ let url = null;
365
+
366
+ // Try qdbus for Chromium-based browsers (works on some Linux desktops)
367
+ const chromiumKey = Object.keys(CHROMIUM_PROC_MAP).find(k => proc.includes(k));
368
+ if (chromiumKey) {
369
+ url = captureChromiumUrlLinux(chromiumKey, log);
370
+ }
371
+
372
+ // Fallback: title-based extraction
373
+ if (!url) {
374
+ url = extractUrlFromTitle(winTitle, proc);
375
+ }
376
+
377
+ if (url) {
378
+ snapshot.active_url = url;
379
+ snapshot.browser_tab_title = extractTabTitle(winTitle, proc);
380
+ log('Browser URL:', url);
381
+ }
382
+ }
383
+
384
+ // ── Clipboard ──────────────────────────────────────────────────────────────
200
385
  try {
201
- const clip = execSync('xclip -selection clipboard -o 2>/dev/null || xsel --clipboard --output 2>/dev/null',
386
+ const clip = execSync(
387
+ 'xclip -selection clipboard -o 2>/dev/null || xsel --clipboard --output 2>/dev/null',
202
388
  { encoding: 'utf8', timeout: 2000 }).trim();
203
389
  if (clip && clip.length > 0 && clip.length < 5000) {
204
390
  snapshot.clipboard_text = clip.slice(0, 500);
@@ -208,88 +394,93 @@ async function captureLinux(snapshot, log) {
208
394
  }
209
395
  }
210
396
 
397
+ /**
398
+ * Try to get the active URL from a Chromium browser on Linux via qdbus
399
+ * (works on KDE/GNOME with accessibility enabled).
400
+ */
401
+ function captureChromiumUrlLinux(browserKey, log) {
402
+ try {
403
+ // xdotool + xprop approach: read _NET_WM_NAME of the active window
404
+ // and parse if it looks like a URL (some browsers put URL in title in certain modes)
405
+ // More reliable: use wmctrl to get window title and parse
406
+ const title = execSync('xdotool getactivewindow getwindowname 2>/dev/null',
407
+ { encoding: 'utf8', timeout: 1000 }).trim();
408
+ return extractUrlFromTitle(title, browserKey);
409
+ } catch (err) {
410
+ log('Linux Chromium URL capture failed:', err.message);
411
+ return null;
412
+ }
413
+ }
414
+
211
415
  // ─────────────────────────────────────────────────────────────────────────────
212
416
  // Helpers
213
417
  // ─────────────────────────────────────────────────────────────────────────────
214
418
 
215
419
  /**
216
420
  * Try to extract a URL from a browser window title.
217
- * Chrome/Edge show: "<page title> - Google Chrome"
218
- * Firefox shows: "<page title> — Mozilla Firefox"
219
- * We can't get the actual URL from the title alone, but we can detect
220
- * that a browser is active and parse the tab title.
221
421
  *
222
- * For richer URL capture on Windows, we use the UIAutomation address bar
223
- * approach via PowerShell but that's expensive, so we only do it when
224
- * the active process is a known browser.
422
+ * Some browsers (especially in developer/kiosk mode, or when the URL bar
423
+ * is focused) include the URL in the window title. This is a best-effort
424
+ * fallback when native API access is unavailable.
425
+ *
426
+ * Title patterns:
427
+ * Chrome/Edge/Brave: "<page title> - Google Chrome"
428
+ * Firefox: "<page title> — Mozilla Firefox"
429
+ * Safari: "<page title>"
430
+ * Arc: "<page title> - Arc"
431
+ *
432
+ * We cannot reliably extract the URL from the title alone for most pages,
433
+ * but we can detect that a browser is active and return the tab title.
434
+ * The function returns null if no URL can be determined.
225
435
  */
226
- function extractUrlFromBrowserTitle(title, process) {
227
- const proc = (process || '').toLowerCase();
436
+ function extractUrlFromTitle(windowTitle, processName) {
437
+ const proc = (processName || '').toLowerCase();
228
438
  const isBrowser = proc.includes('chrome') || proc.includes('msedge') ||
229
- proc.includes('firefox') || proc.includes('opera') || proc.includes('brave');
439
+ proc.includes('firefox') || proc.includes('opera') || proc.includes('brave') ||
440
+ proc.includes('arc') || proc.includes('safari') || proc.includes('vivaldi') ||
441
+ proc.includes('chromium') || proc.includes('edge');
230
442
  if (!isBrowser) return null;
231
443
 
232
- // Try to get URL from Chrome/Edge via PowerShell UIAutomation (Windows only)
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.
235
- if (proc.includes('chrome') || proc.includes('msedge')) {
236
- try {
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');
259
- const url = execSync(
260
- `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${tmpFile}"`,
261
- { encoding: 'utf8', timeout: 5000, windowsHide: true }
262
- ).trim();
263
- try { fs.unlinkSync(tmpFile); } catch { /* cleanup best-effort */ }
264
- if (url && (url.startsWith('http') || url.startsWith('chrome'))) {
265
- return url;
266
- }
267
- } catch (err) {
268
- // UIAutomation not available — fall back to title parsing
269
- }
444
+ // Some browsers show the URL directly in the title when the address bar is focused
445
+ // or in certain modes (e.g. "http://localhost:3000 - Google Chrome")
446
+ const urlMatch = windowTitle.match(/https?:\/\/[^\s"'<>]+/);
447
+ if (urlMatch) {
448
+ return urlMatch[0].replace(/\s*[-—|].*$/, '').trim();
270
449
  }
271
450
 
272
- return null; // URL not extractable from title alone
451
+ return null; // URL not determinable from title alone
273
452
  }
274
453
 
275
- function extractTabTitle(windowTitle, process) {
276
- const proc = (process || '').toLowerCase();
277
- // Strip browser suffix from title
278
- return windowTitle
454
+ /**
455
+ * Strip the browser name suffix from a window title to get the tab title.
456
+ */
457
+ function extractTabTitle(windowTitle, processName) {
458
+ return (windowTitle || '')
279
459
  .replace(/ - Google Chrome$/, '')
280
460
  .replace(/ - Microsoft Edge$/, '')
281
461
  .replace(/ — Mozilla Firefox$/, '')
462
+ .replace(/ - Mozilla Firefox$/, '')
282
463
  .replace(/ - Brave$/, '')
464
+ .replace(/ - Brave Browser$/, '')
283
465
  .replace(/ - Opera$/, '')
466
+ .replace(/ - Arc$/, '')
467
+ .replace(/ - Vivaldi$/, '')
468
+ .replace(/ - Safari$/, '')
469
+ .replace(/ \| .+$/, '') // some browsers use " | Site Name" format
284
470
  .trim();
285
471
  }
286
472
 
287
473
  function friendlyAppName(processName, windowTitle) {
288
474
  const proc = (processName || '').toLowerCase();
289
475
  if (proc.includes('chrome')) return 'Google Chrome';
290
- if (proc.includes('msedge')) return 'Microsoft Edge';
476
+ if (proc.includes('msedge') || proc.includes('edge')) return 'Microsoft Edge';
291
477
  if (proc.includes('firefox')) return 'Firefox';
292
478
  if (proc.includes('brave')) return 'Brave Browser';
479
+ if (proc.includes('opera')) return 'Opera';
480
+ if (proc.includes('arc')) return 'Arc';
481
+ if (proc.includes('vivaldi')) return 'Vivaldi';
482
+ if (proc.includes('safari')) return 'Safari';
483
+ if (proc.includes('chromium')) return 'Chromium';
293
484
  if (proc.includes('code')) return 'VS Code';
294
485
  if (proc.includes('excel')) return 'Microsoft Excel';
295
486
  if (proc.includes('word')) return 'Microsoft Word';
@@ -299,7 +490,10 @@ function friendlyAppName(processName, windowTitle) {
299
490
  if (proc.includes('zoom')) return 'Zoom';
300
491
  if (proc.includes('notepad')) return 'Notepad';
301
492
  if (proc.includes('explorer')) return 'File Explorer';
493
+ if (proc.includes('finder')) return 'Finder';
302
494
  if (proc.includes('cmd') || proc.includes('powershell') || proc.includes('windowsterminal')) return 'Terminal';
495
+ if (proc.includes('iterm')) return 'iTerm2';
496
+ if (proc.includes('terminal')) return 'Terminal';
303
497
  return processName || 'Unknown';
304
498
  }
305
499