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 +50 -0
- package/backend-dist/agent/agentServer.js +5 -1
- package/backend-dist/bucket-adapter/index.js +59 -0
- package/backend-dist/config.js +11 -0
- package/backend-dist/index.js +15 -2
- package/backend-dist/web-search/browser-playwright.js +191 -80
- package/backend-dist/web-search/web-search-provider.js +15 -7
- package/dist/grantBrowserAccess.js +789 -0
- package/dist/index.js +15 -0
- package/package.json +8 -7
- package/src/grantBrowserAccess.ts +936 -0
- package/src/index.ts +19 -0
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 {
|
|
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
|
+
}
|
package/backend-dist/config.js
CHANGED
|
@@ -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
|
};
|
package/backend-dist/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
363
|
-
*
|
|
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 (
|
|
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
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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 (
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|