omnikey-cli 1.0.27 → 1.0.29

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.
@@ -40,44 +40,12 @@ exports.isAnyBrowserRunning = isAnyBrowserRunning;
40
40
  exports.isBrowserOpenWithUrl = isBrowserOpenWithUrl;
41
41
  exports.fetchWithPlaywright = fetchWithPlaywright;
42
42
  const axios_1 = __importDefault(require("axios"));
43
- // Utility: Promise with timeout
44
- async function withTimeout(promise, ms, label, log) {
45
- let timeoutId;
46
- return Promise.race([
47
- promise,
48
- new Promise((resolve) => {
49
- timeoutId = setTimeout(() => {
50
- log.warn('browser-playwright: fetch timed out', { label, ms });
51
- resolve(null);
52
- }, ms);
53
- }),
54
- ]).then((result) => {
55
- clearTimeout(timeoutId);
56
- return result;
57
- });
58
- }
59
- /**
60
- * Playwright-based web fetching using the user's installed browser profile.
61
- *
62
- * Key design decisions:
63
- * 1. Detects which Chromium browsers are currently RUNNING and tries those
64
- * first — the active browser is where the authenticated session lives.
65
- * 2. Discovers the actual profile directory dynamically (Default, Profile 1,
66
- * Profile 2 …) rather than hardcoding "Default".
67
- * 3. Checks multiple executable locations (system /Applications and
68
- * user ~/Applications).
69
- * 4. Firefox is intentionally excluded from Playwright — headless Firefox
70
- * on macOS has a known RenderCompositorSWGL rendering bug that causes
71
- * 30-second timeouts. Cookies from Firefox are still extracted separately
72
- * by browser-cookies.ts for the plain-HTTP fallback.
73
- *
74
- * macOS only. Returns null on other platforms.
75
- */
76
43
  const child_process_1 = require("child_process");
77
44
  const fs = __importStar(require("fs"));
78
45
  const os = __importStar(require("os"));
79
46
  const path = __importStar(require("path"));
80
47
  const playwright_core_1 = __importDefault(require("playwright-core"));
48
+ const config_1 = require("../config");
81
49
  const home = os.homedir();
82
50
  const BROWSER_CATALOGUE = [
83
51
  {
@@ -137,6 +105,44 @@ const BROWSER_CATALOGUE = [
137
105
  userDataDir: path.join(home, 'Library/Application Support/Chromium'),
138
106
  },
139
107
  ];
108
+ const WINDOWS_BROWSER_CATALOGUE = [
109
+ {
110
+ name: 'Chrome',
111
+ executablePaths: [
112
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
113
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
114
+ path.join(home, 'AppData', 'Local', 'Google', 'Chrome', 'Application', 'chrome.exe'),
115
+ ],
116
+ userDataDir: path.join(home, 'AppData', 'Local', 'Google', 'Chrome', 'User Data'),
117
+ },
118
+ {
119
+ name: 'Edge',
120
+ executablePaths: [
121
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
122
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
123
+ path.join(home, 'AppData', 'Local', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
124
+ ],
125
+ userDataDir: path.join(home, 'AppData', 'Local', 'Microsoft', 'Edge', 'User Data'),
126
+ },
127
+ {
128
+ name: 'Brave',
129
+ executablePaths: [
130
+ 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
131
+ path.join(home, 'AppData', 'Local', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
132
+ ],
133
+ userDataDir: path.join(home, 'AppData', 'Local', 'BraveSoftware', 'Brave-Browser', 'User Data'),
134
+ },
135
+ ];
136
+ function resolveExistingExecutablePath(paths) {
137
+ for (const p of paths) {
138
+ try {
139
+ if (fs.existsSync(p))
140
+ return p;
141
+ }
142
+ catch { }
143
+ }
144
+ return null;
145
+ }
140
146
  // ─── Running browser detection ────────────────────────────────────────────────
141
147
  /**
142
148
  * Returns the names of browsers that are currently running.
@@ -145,6 +151,32 @@ const BROWSER_CATALOGUE = [
145
151
  */
146
152
  function getRunningBrowserNames() {
147
153
  const running = new Set();
154
+ if (process.platform === 'win32') {
155
+ // tasklist /FO CSV /NH outputs one "ImageName","PID",... line per process.
156
+ const exeMap = {
157
+ 'chrome.exe': 'Chrome',
158
+ 'msedge.exe': 'Edge',
159
+ 'brave.exe': 'Brave',
160
+ 'opera.exe': 'Opera',
161
+ 'vivaldi.exe': 'Vivaldi',
162
+ };
163
+ try {
164
+ const out = (0, child_process_1.execSync)('tasklist /FO CSV /NH 2>nul', {
165
+ encoding: 'utf8',
166
+ stdio: ['pipe', 'pipe', 'pipe'],
167
+ });
168
+ for (const line of out.split('\n')) {
169
+ const exe = line.split(',')[0]?.replace(/"/g, '').trim().toLowerCase();
170
+ const name = exeMap[exe];
171
+ if (name)
172
+ running.add(name);
173
+ }
174
+ }
175
+ catch {
176
+ // tasklist failed — proceed without running-browser info
177
+ }
178
+ return running;
179
+ }
148
180
  try {
149
181
  // ps -axco command lists only the process name (no path, no args)
150
182
  const output = (0, child_process_1.execSync)('ps -axco command', {
@@ -164,13 +196,11 @@ function getRunningBrowserNames() {
164
196
  };
165
197
  for (const [processName, browserName] of Object.entries(processMap)) {
166
198
  if (processName === 'safari') {
167
- // Only match the main Safari process exactly (case-insensitive, trimmed)
168
199
  if (lines.some((l) => l.trim() === 'safari')) {
169
200
  running.add(browserName);
170
201
  }
171
202
  }
172
203
  else {
173
- // For other browsers, allow exact match or substring match
174
204
  if (lines.some((l) => l.trim() === processName || l.includes(processName))) {
175
205
  running.add(browserName);
176
206
  }
@@ -194,23 +224,60 @@ async function fetchWithCDP(url, browsersWithUrl, log) {
194
224
  // Collect candidate ports:
195
225
  // 1. DevToolsActivePort file (written when Chrome was started with --remote-debugging-port)
196
226
  // 2. Well-known default ports developers commonly use
227
+ // 3. On Windows: all ports browser processes are currently listening on
197
228
  const candidatePorts = [];
198
- for (const candidate of BROWSER_CATALOGUE) {
199
- if (!browsersWithUrl.has(candidate.name))
200
- continue;
201
- if (candidate.name === 'Safari')
202
- continue; // CDP is Chromium-only
203
- const portFile = path.join(candidate.userDataDir, 'DevToolsActivePort');
204
- if (fs.existsSync(portFile)) {
205
- try {
206
- const raw = fs.readFileSync(portFile, 'utf8');
207
- const port = parseInt(raw.split('\n')[0].trim(), 10);
208
- if (!isNaN(port) && port > 0 && !candidatePorts.includes(port)) {
229
+ if (process.platform !== 'win32') {
230
+ // macOS: read DevToolsActivePort from confirmed-open browsers
231
+ for (const candidate of BROWSER_CATALOGUE) {
232
+ if (!browsersWithUrl.has(candidate.name))
233
+ continue;
234
+ if (candidate.name === 'Safari')
235
+ continue;
236
+ const portFile = path.join(candidate.userDataDir, 'DevToolsActivePort');
237
+ if (fs.existsSync(portFile)) {
238
+ try {
239
+ const raw = fs.readFileSync(portFile, 'utf8');
240
+ const port = parseInt(raw.split('\n')[0].trim(), 10);
241
+ if (!isNaN(port) && port > 0 && !candidatePorts.includes(port)) {
242
+ candidatePorts.push(port);
243
+ }
244
+ }
245
+ catch { }
246
+ }
247
+ }
248
+ }
249
+ // Windows: AppleScript is unavailable so browsersWithUrl is always empty.
250
+ // Read DevToolsActivePort from Windows browser paths directly, and also ask
251
+ // PowerShell for every TCP port the browser processes are listening on —
252
+ // this catches any --remote-debugging-port value, not just well-known ones.
253
+ if (process.platform === 'win32') {
254
+ for (const candidate of WINDOWS_BROWSER_CATALOGUE) {
255
+ const portFile = path.join(candidate.userDataDir, 'DevToolsActivePort');
256
+ if (fs.existsSync(portFile)) {
257
+ try {
258
+ const raw = fs.readFileSync(portFile, 'utf8');
259
+ const port = parseInt(raw.split('\n')[0].trim(), 10);
260
+ if (!isNaN(port) && port > 0 && !candidatePorts.includes(port)) {
261
+ candidatePorts.push(port);
262
+ }
263
+ }
264
+ catch { }
265
+ }
266
+ }
267
+ // Enumerate all listening ports owned by browser processes via PowerShell.
268
+ try {
269
+ const psOut = (0, child_process_1.execSync)('powershell -NoProfile -NonInteractive -Command ' +
270
+ '"$p=Get-Process -Name chrome,msedge,brave,opera,vivaldi -EA SilentlyContinue;' +
271
+ 'if($p){$p|%{$id=$_.Id;Get-NetTCPConnection -OwningProcess $id -State Listen -EA SilentlyContinue}}' +
272
+ '|Select-Object -ExpandProperty LocalPort|Sort-Object -Unique"', { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
273
+ for (const line of psOut.split('\n')) {
274
+ const port = parseInt(line.trim(), 10);
275
+ if (!isNaN(port) && port > 1024 && !candidatePorts.includes(port)) {
209
276
  candidatePorts.push(port);
210
277
  }
211
278
  }
212
- catch { }
213
279
  }
280
+ catch { }
214
281
  }
215
282
  // Always probe the most common debug ports — many developers run Chrome with
216
283
  // --remote-debugging-port=9222 and these checks are cheap (instant refusal if closed).
@@ -218,11 +285,22 @@ async function fetchWithCDP(url, browsersWithUrl, log) {
218
285
  if (!candidatePorts.includes(p))
219
286
  candidatePorts.push(p);
220
287
  }
288
+ // User-configured port (set via `omnikey grant-browser-access`) gets tried first.
289
+ if (config_1.config.browserDebugPort && !candidatePorts.includes(config_1.config.browserDebugPort)) {
290
+ candidatePorts.unshift(config_1.config.browserDebugPort);
291
+ }
292
+ else if (config_1.config.browserDebugPort) {
293
+ // Already in the list — move it to the front so it is tried before auto-detected ports.
294
+ candidatePorts.splice(candidatePorts.indexOf(config_1.config.browserDebugPort), 1);
295
+ candidatePorts.unshift(config_1.config.browserDebugPort);
296
+ }
221
297
  for (const port of candidatePorts) {
222
298
  // Quick HTTP probe: /json/version returns immediately if the debug endpoint is up.
223
299
  let endpointUp = false;
224
300
  try {
225
- const probe = await axios_1.default.get(`http://localhost:${port}/json/version`, { timeout: 800 });
301
+ // Use 127.0.0.1 explicitly — on Windows, `localhost` may resolve to ::1
302
+ // while Chrome binds its debug endpoint to 127.0.0.1 only.
303
+ const probe = await axios_1.default.get(`http://127.0.0.1:${port}/json/version`, { timeout: 800 });
226
304
  endpointUp = probe.status === 200;
227
305
  }
228
306
  catch {
@@ -345,12 +423,12 @@ function getBrowsersWithUrlOpen(url, log) {
345
423
  return false;
346
424
  }
347
425
  });
348
- log.debug('browser-playwright: tab check', { browser: browserName, targetHostname, found });
426
+ log.info('browser-playwright: tab check', { browser: browserName, targetHostname, found });
349
427
  if (found)
350
428
  confirmed.add(browserName);
351
429
  }
352
430
  catch {
353
- log.debug('browser-playwright: AppleScript tab check failed — skipping browser', {
431
+ log.warn('browser-playwright: AppleScript tab check failed — skipping browser', {
354
432
  browser: browserName,
355
433
  });
356
434
  }
@@ -359,12 +437,62 @@ function getBrowsersWithUrlOpen(url, log) {
359
437
  }
360
438
  /**
361
439
  * Returns true if the given URL's hostname is confirmed open in any running
362
- * browser tab via AppleScript. Returns false if the check cannot be performed
363
- * or if no browser has the URL open.
440
+ * browser tab. On macOS this uses AppleScript; on Windows it queries the CDP
441
+ * debug endpoint's /json tab list (requires --remote-debugging-port).
364
442
  */
365
- function isBrowserOpenWithUrl(url, log) {
443
+ async function isBrowserOpenWithUrl(url, log) {
444
+ if (process.platform === 'win32') {
445
+ return isBrowserOpenWithUrlWindows(url, log);
446
+ }
366
447
  return getBrowsersWithUrlOpen(url, log).size > 0;
367
448
  }
449
+ async function isBrowserOpenWithUrlWindows(url, log) {
450
+ let targetHostname;
451
+ try {
452
+ targetHostname = new URL(url).hostname;
453
+ }
454
+ catch {
455
+ return false;
456
+ }
457
+ // If no browser processes are running at all, skip the port probes.
458
+ if (getRunningBrowserNames().size === 0)
459
+ return false;
460
+ const candidatePorts = [];
461
+ if (config_1.config.browserDebugPort)
462
+ candidatePorts.push(config_1.config.browserDebugPort);
463
+ for (const p of [9222, 9229, 9333]) {
464
+ if (!candidatePorts.includes(p))
465
+ candidatePorts.push(p);
466
+ }
467
+ for (const port of candidatePorts) {
468
+ try {
469
+ const resp = await axios_1.default.get(`http://127.0.0.1:${port}/json`, {
470
+ timeout: 800,
471
+ });
472
+ if (!Array.isArray(resp.data))
473
+ continue;
474
+ const found = resp.data.some((tab) => {
475
+ try {
476
+ return new URL(tab.url ?? '').hostname === targetHostname;
477
+ }
478
+ catch {
479
+ return false;
480
+ }
481
+ });
482
+ if (found) {
483
+ log.info('browser-playwright: Windows CDP tab check confirmed URL open', {
484
+ port,
485
+ hostname: targetHostname,
486
+ });
487
+ return true;
488
+ }
489
+ }
490
+ catch {
491
+ // Port not listening — skip
492
+ }
493
+ }
494
+ return false;
495
+ }
368
496
  // ─── Strategy 0: Live-tab AppleScript extraction ──────────────────────────────
369
497
  //
370
498
  // When the user already has the URL open in a browser we can pull the rendered
@@ -441,7 +569,7 @@ function findTabLocation(appName, url) {
441
569
  * URL open. Returns null if the URL is not open or extraction fails.
442
570
  */
443
571
  async function fetchFromRunningBrowserTab(url, browsersWithUrl, log) {
444
- if (process.platform !== 'darwin' || browsersWithUrl.size === 0)
572
+ if (config_1.config.terminalPlatform !== 'macos' || browsersWithUrl.size === 0)
445
573
  return null;
446
574
  for (const browserName of browsersWithUrl) {
447
575
  const info = BROWSER_APPLESCRIPT[browserName];
@@ -574,40 +702,23 @@ async function fetchFromRunningBrowserTab(url, browsersWithUrl, log) {
574
702
  /**
575
703
  * Fetches a URL using the user's browser session.
576
704
  *
577
- * Only browsers that are confirmed (via AppleScript) to have the URL open are
578
- * tried this avoids wasting time on browsers or profiles that don't hold the
579
- * active session.
580
- *
581
- * Strategies in order:
582
- * 0. Live-tab extraction — reads content directly from the open tab via
583
- * AppleScript JS execution. No cookie decryption required.
584
- * 1. Cookie injection — decrypts cookies and injects into a fresh headless
585
- * Chromium context (handles cookie-based auth when live tab unavailable).
586
- * 2. Profile copy — copies Local Storage + IndexedDB to a temp dir (handles
587
- * localStorage/sessionStorage token auth flows).
588
- * 3. Safari Playwright — WebKit with injected Safari cookies (Safari only).
705
+ * Strategies:
706
+ * -1. CDP via --remote-debugging-port macOS + Windows; requires Chrome to be
707
+ * started with --remote-debugging-port=9222.
708
+ * 0. Live-tab AppleScript extraction — macOS only.
589
709
  */
590
710
  async function fetchWithPlaywright(url, log) {
591
- // Determine which browsers have the URL open right now.
592
711
  const browsersWithUrl = getBrowsersWithUrlOpen(url, log);
593
712
  log.info('browser-playwright: browsers with URL open', {
594
713
  url,
595
714
  browsers: [...browsersWithUrl],
596
715
  });
597
- // ── Strategy -1: CDP via DevToolsActivePort ──────────────────────────────
598
- // Fastest path — connects directly to the live browser's JS-rendered tab.
599
- // Only works when Chrome was launched with --remote-debugging-port.
600
- if (browsersWithUrl.size > 0) {
601
- const cdpResult = await fetchWithCDP(url, browsersWithUrl, log);
602
- if (cdpResult) {
603
- return cdpResult.content;
604
- }
605
- }
606
- // ── Strategy 0: extract from the live tab directly ────────────────────────
716
+ const cdpResult = await fetchWithCDP(url, browsersWithUrl, log);
717
+ if (cdpResult)
718
+ return cdpResult.content;
607
719
  const liveContent = await fetchFromRunningBrowserTab(url, browsersWithUrl, log);
608
- if (liveContent) {
720
+ if (liveContent)
609
721
  return liveContent;
610
- }
611
- log.warn('browser-playwright: all strategies exhausted', { url });
722
+ log.warn('browser-playwright: all strategies exhausted — on Windows, launch Chrome with --remote-debugging-port=9222', { url });
612
723
  return null;
613
724
  }
@@ -170,7 +170,7 @@ async function fetchPlainHttp(url, log) {
170
170
  // sites use redirects, 302s, custom error pages, or soft-blocks
171
171
  // rather than a clean 401/403, so checking status codes alone is
172
172
  // unreliable. Fall through to the browser-session path instead.
173
- if (isSelfHostedMacOS && (0, browser_playwright_1.isBrowserOpenWithUrl)(url, log)) {
173
+ if (isSelfHostedWithBrowserSession && (await (0, browser_playwright_1.isBrowserOpenWithUrl)(url, log))) {
174
174
  return { html: null, authBlocked: true, finalUrl: url };
175
175
  }
176
176
  if (status === 401 || status === 403) {
@@ -192,19 +192,19 @@ async function fetchFromActiveTab(url, log) {
192
192
  log.info('web_fetch: falling back to active-tab extraction', { url });
193
193
  return (0, browser_playwright_1.fetchWithPlaywright)(url, log);
194
194
  }
195
- const isSelfHostedMacOS = config_1.config.isSelfHosted && config_1.config.terminalPlatform === 'macos';
195
+ const isSelfHostedWithBrowserSession = config_1.config.isSelfHosted;
196
196
  async function executeWebFetch(url, log) {
197
197
  log.info('Executing web_fetch tool', { url });
198
198
  // ── Step 1: plain HTTP request ────────────────────────────────────────────
199
199
  const { html, authBlocked, finalUrl } = await fetchPlainHttp(url, log);
200
200
  const plainText = html ? stripHtml(html) : '';
201
- if (!isSelfHostedMacOS) {
201
+ if (!isSelfHostedWithBrowserSession) {
202
202
  if (authBlocked) {
203
- log.warn('Error: page requires authentication. Run OmniKey in self-hosted mode on macOS to enable browser-session access.');
203
+ log.warn('Error: page requires authentication. Run OmniKey in self-hosted mode on macOS or Windows to enable browser-session access.');
204
204
  }
205
205
  return plainText.slice(0, exports.MAX_TOOL_CONTENT_CHARS) || 'No content retrieved';
206
206
  }
207
- // ── Step 2 (self-hosted macOS only): LLM auth check on plain response ─────
207
+ // ── Step 2 (self-hosted desktop): LLM auth check on plain response ────────
208
208
  let looksUnauthenticated = false;
209
209
  if (!authBlocked && plainText) {
210
210
  log.info('web_fetch: performing LLM auth check on plain HTTP response', { url });
@@ -214,7 +214,7 @@ async function executeWebFetch(url, log) {
214
214
  }
215
215
  looksUnauthenticated = true;
216
216
  }
217
- // ── Step 3 (self-hosted macOS only): active-tab extraction ───────────────
217
+ // ── Step 3 (self-hosted desktop): active-tab extraction ──────────────────
218
218
  // Only attempted when there is evidence authentication is required.
219
219
  const needsAuth = authBlocked || looksUnauthenticated;
220
220
  if (needsAuth) {
@@ -226,7 +226,15 @@ async function executeWebFetch(url, log) {
226
226
  }
227
227
  // All strategies exhausted.
228
228
  if (authBlocked) {
229
- log.warn('Error: page requires authentication. Open the page in Chrome and ensure "Allow JavaScript from Apple Events" is enabled (View → Developer → Allow JavaScript from Apple Events).');
229
+ if (config_1.config.terminalPlatform === 'macos') {
230
+ log.warn('Error: page requires authentication. Open the page in Chrome and ensure "Allow JavaScript from Apple Events" is enabled (View → Developer → Allow JavaScript from Apple Events).');
231
+ }
232
+ else if (config_1.config.terminalPlatform === 'windows') {
233
+ log.warn('Error: page requires authentication. To enable live browser-session access on Windows, ' +
234
+ 'launch Chrome with --remote-debugging-port=9222: right-click your Chrome shortcut → Properties, ' +
235
+ 'and append "--remote-debugging-port=9222" to the Target field, then restart Chrome. ' +
236
+ 'OmniKey will then read the authenticated tab directly.');
237
+ }
230
238
  }
231
239
  return plainText.slice(0, exports.MAX_TOOL_CONTENT_CHARS) || 'No content retrieved';
232
240
  }