omnikey-cli 1.0.27 → 1.0.28

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/README.md CHANGED
@@ -18,6 +18,7 @@ OmnikeyAI is a productivity tool that helps you quickly rewrite selected text us
18
18
  - Optional **web search tool** integration for enhanced responses.
19
19
  - Accepts CLI flags for non-interactive setup.
20
20
  - Configure and run the backend daemon — persisted across reboots on both macOS and Windows.
21
+ - `omnikey grant-browser-access`: One-time setup to give Omnikey access to authenticated browser tabs for web fetch.
21
22
 
22
23
  ## Usage
23
24
 
@@ -57,6 +58,12 @@ omnikey logs --lines 100
57
58
 
58
59
  # Check daemon error logs only
59
60
  omnikey logs --errors
61
+
62
+ # Grant Omnikey access to authenticated browser tabs
63
+ omnikey grant-browser-access
64
+
65
+ # Reopen the browser with its saved Omnikey debug profile at any time
66
+ omnikey browser open
60
67
  ```
61
68
 
62
69
  ### Command reference
@@ -72,6 +79,49 @@ omnikey logs --errors
72
79
  | `omnikey remove-config [--db]` | Remove config files; add `--db` to also delete the database |
73
80
  | `omnikey status` | Show what process is using the daemon port |
74
81
  | `omnikey logs [--lines N] [--errors]` | Tail daemon logs |
82
+ | `omnikey grant-browser-access` | Set up authenticated browser tab access for web fetch |
83
+ | `omnikey browser open` | Reopen the browser with the saved Omnikey debug profile |
84
+
85
+ ## Browser access (`grant-browser-access` / `browser open`)
86
+
87
+ Omnikey can read content from your authenticated browser tabs when fetching web pages that require a login. `omnikey grant-browser-access` performs a guided, one-time setup to enable this.
88
+
89
+ After setup, run `omnikey browser open` at any time to relaunch the browser with its saved Omnikey debug profile (kills any running instance first, cleans up stale lock files, then re-launches and confirms the debug port is active).
90
+
91
+ ### Windows
92
+
93
+ On Windows the only supported method is **Remote Debugging Port (CDP)**:
94
+
95
+ 1. Detects installed browsers (Chrome, Edge, Brave).
96
+ 2. Prompts you to select a browser and profile.
97
+ 3. Finds an available port starting at 9222.
98
+ 4. Saves `BROWSER_DEBUG_PORT` to `~/.omnikey/config.json`.
99
+ 5. Registers a **Windows Registry Run key** (`HKCU\Software\Microsoft\Windows\CurrentVersion\Run\OmnikeyBrowserDebug`) so the browser launches automatically with `--remote-debugging-port=<port>` on every login.
100
+ 6. Force-kills any running browser processes, waits until they are fully gone, then launches the browser immediately.
101
+ 7. Verifies the debug port is reachable at `http://127.0.0.1:<port>/json` and reports success or a diagnostic error.
102
+
103
+ Re-running the command when a startup entry already exists lets you **Update** (change browser/profile/port) or **Remove** (disable the startup entry).
104
+
105
+ ### macOS
106
+
107
+ On macOS you choose between two methods:
108
+
109
+ #### Remote Debugging Port (CDP) — recommended
110
+
111
+ Same flow as Windows, but the startup entry is written as a **launchd agent** (`~/Library/LaunchAgents/com.omnikey.browser-debug.plist`) loaded immediately with `launchctl`.
112
+
113
+ Supported browsers: Chrome, Brave, Edge, Arc, Vivaldi, Opera, Chromium.
114
+
115
+ #### AppleScript
116
+
117
+ No port or browser restart needed. Omnikey reads the live tab content directly via Apple Events.
118
+
119
+ The CLI automatically enables **"Allow JavaScript from Apple Events"** for every selected browser:
120
+
121
+ - **Chrome / Brave / Edge / Arc / Vivaldi / Opera** — patches the `devtools.allow_javascript_apple_events` key in each profile's `Preferences` JSON file. The browser must be closed before patching (the CLI will warn you if it is still running).
122
+ - **Safari** — runs `defaults write com.apple.Safari AllowJavaScriptFromAppleEvents -bool YES`.
123
+
124
+ Both changes are permanent and survive reboots. Restart each browser once after setup for the change to take effect.
75
125
 
76
126
  ## Platform notes
77
127
 
@@ -790,7 +790,11 @@ function createAgentRouter() {
790
790
  const truncated = cleaned.length > MAX_CHARS
791
791
  ? cleaned.slice(0, MAX_CHARS) + '… [message truncated]'
792
792
  : cleaned;
793
- return { id: `${index}-${m.role}`, role: m.role, text: truncated };
793
+ return {
794
+ id: `${index}-${m.role}`,
795
+ role: m.role,
796
+ text: truncated,
797
+ };
794
798
  })
795
799
  .filter((m) => m.text.length > 0);
796
800
  res.json({ messages });
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDownloadCounts = getDownloadCounts;
4
+ exports.incrementDownloadCount = incrementDownloadCount;
5
+ const storage_1 = require("@google-cloud/storage");
6
+ const logger_1 = require("../logger");
7
+ const config_1 = require("../config");
8
+ const DEFAULT_COUNTS = { macos: 0, windows: 0 };
9
+ // Initialised once at module load — uses Application Default Credentials when
10
+ // running on Cloud Run (or any GCP environment), and falls back to ADC from
11
+ // the local environment during development.
12
+ const storage = new storage_1.Storage();
13
+ function getGcsConfig() {
14
+ const bucketName = config_1.config.gcsBucketName;
15
+ const objectPath = config_1.config.gcsDownloadCountObject;
16
+ if (!bucketName || !objectPath)
17
+ return null;
18
+ return { bucketName, objectPath };
19
+ }
20
+ async function getDownloadCounts() {
21
+ const gcs = getGcsConfig();
22
+ if (!gcs)
23
+ return { ...DEFAULT_COUNTS };
24
+ return readCounts(gcs.bucketName, gcs.objectPath);
25
+ }
26
+ async function readCounts(bucketName, objectPath) {
27
+ const file = storage.bucket(bucketName).file(objectPath);
28
+ const [exists] = await file.exists();
29
+ if (!exists) {
30
+ return { ...DEFAULT_COUNTS };
31
+ }
32
+ const [contents] = await file.download();
33
+ const parsed = JSON.parse(contents.toString('utf8'));
34
+ return {
35
+ macos: typeof parsed.macos === 'number' ? parsed.macos : 0,
36
+ windows: typeof parsed.windows === 'number' ? parsed.windows : 0,
37
+ };
38
+ }
39
+ async function writeCounts(bucketName, objectPath, counts) {
40
+ const file = storage.bucket(bucketName).file(objectPath);
41
+ await file.save(JSON.stringify(counts), {
42
+ contentType: 'application/json',
43
+ resumable: false,
44
+ });
45
+ }
46
+ async function incrementDownloadCount(platform) {
47
+ const gcs = getGcsConfig();
48
+ if (!gcs)
49
+ return;
50
+ try {
51
+ const counts = await readCounts(gcs.bucketName, gcs.objectPath);
52
+ counts[platform] += 1;
53
+ await writeCounts(gcs.bucketName, gcs.objectPath, counts);
54
+ logger_1.logger.info(`Download count incremented for ${platform}.`, { counts });
55
+ }
56
+ catch (err) {
57
+ logger_1.logger.error(`Failed to increment download count for ${platform}.`, { error: err });
58
+ }
59
+ }
@@ -93,4 +93,15 @@ exports.config = {
93
93
  searxngUrl: getEnv('SEARXNG_URL', false),
94
94
  terminalPlatform: getEnv('TERMINAL_PLATFORM', false),
95
95
  blockSaas: getBooleanEnv('BLOCK_SAAS', false),
96
+ // User-configured CDP debug port (set by `omnikey grant-browser-access`)
97
+ browserDebugPort: (() => {
98
+ const raw = getEnv('BROWSER_DEBUG_PORT', false);
99
+ if (!raw)
100
+ return undefined;
101
+ const n = parseInt(raw, 10);
102
+ return Number.isNaN(n) ? undefined : n;
103
+ })(),
104
+ // GCS download-count tracking (both must be set to enable counting)
105
+ gcsBucketName: getEnv('GCS_BUCKET_NAME', false),
106
+ gcsDownloadCountObject: getEnv('GCS_DOWNLOAD_COUNT_OBJECT', false),
96
107
  };
@@ -17,6 +17,7 @@ const config_1 = require("./config");
17
17
  const agentServer_1 = require("./agent/agentServer");
18
18
  // Importing AgentSession ensures the model is registered with Sequelize before initDatabase().
19
19
  require("./models/agentSession");
20
+ const bucket_adapter_1 = require("./bucket-adapter");
20
21
  const app = (0, express_1.default)();
21
22
  const PORT = Number(config_1.config.port);
22
23
  app.set('trust proxy', 1);
@@ -39,6 +40,7 @@ app.get('/macos/download', (_req, res) => {
39
40
  'Content-Disposition': 'attachment; filename="OmniKeyAI.dmg"',
40
41
  'Content-Encoding': 'gzip',
41
42
  });
43
+ (0, bucket_adapter_1.incrementDownloadCount)('macos').catch(() => { });
42
44
  const fileStream = fs_1.default.createReadStream(dmgPath);
43
45
  const gzip = zlib_1.default.createGzip();
44
46
  fileStream.on('error', (err) => {
@@ -97,7 +99,7 @@ app.get('/macos/appcast', (req, res) => {
97
99
  // ── Windows distribution endpoints ───────────────────────────────────────────
98
100
  // These should match the values in windows/OmniKey.Windows.csproj
99
101
  // <Version> and windows/build_release_zip.ps1 $APP_VERSION.
100
- const WIN_VERSION = '1.7';
102
+ const WIN_VERSION = '1.8';
101
103
  const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-win-x64.zip';
102
104
  const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
103
105
  // Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
@@ -112,6 +114,7 @@ app.get('/windows/download', (_req, res) => {
112
114
  'Content-Disposition': `attachment; filename="${WIN_ZIP_FILENAME}"`,
113
115
  'Content-Encoding': 'gzip',
114
116
  });
117
+ (0, bucket_adapter_1.incrementDownloadCount)('windows').catch(() => { });
115
118
  const fileStream = fs_1.default.createReadStream(WIN_ZIP_PATH);
116
119
  const gzip = zlib_1.default.createGzip();
117
120
  fileStream.on('error', (err) => {
@@ -138,9 +141,19 @@ app.get('/windows/update', (req, res) => {
138
141
  version: WIN_VERSION,
139
142
  downloadUrl: `${baseUrl}/windows/download`,
140
143
  fileSize,
141
- releaseNotes: '',
144
+ releaseNotes: `What's new in ${WIN_VERSION}\n\n• OmniAgent session management — choose to start a new session or resume an existing one each time you run @omniAgent. Save a default to skip the picker automatically on future runs.\n• History button in the OmniAgent window — change your default session at any time without re-running the agent.\n• OmniAgent Session tray menu item — open session settings directly from the system tray.\n• Left-clicking the tray icon now opens the menu (previously right-click only).`,
142
145
  });
143
146
  });
147
+ app.get('/downloads/stats', async (_req, res) => {
148
+ try {
149
+ const counts = await (0, bucket_adapter_1.getDownloadCounts)();
150
+ res.json(counts);
151
+ }
152
+ catch (err) {
153
+ logger_1.logger.error('Failed to retrieve download stats.', { error: err });
154
+ res.status(500).json({ error: 'Unable to retrieve download stats.' });
155
+ }
156
+ });
144
157
  app.get('/health', (_req, res) => {
145
158
  res.json({ status: 'ok' });
146
159
  });
@@ -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
  }