@mehmoodqureshi/chrome-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/shared/download.d.ts +15 -0
  4. package/dist/shared/download.js +0 -0
  5. package/dist/shared/protocol.d.ts +114 -0
  6. package/dist/shared/protocol.js +55 -0
  7. package/dist/src/bridge/auth.d.ts +32 -0
  8. package/dist/src/bridge/auth.js +76 -0
  9. package/dist/src/bridge/connection.d.ts +48 -0
  10. package/dist/src/bridge/connection.js +192 -0
  11. package/dist/src/bridge/datadir.d.ts +8 -0
  12. package/dist/src/bridge/datadir.js +22 -0
  13. package/dist/src/bridge/server.d.ts +58 -0
  14. package/dist/src/bridge/server.js +178 -0
  15. package/dist/src/cli.d.ts +11 -0
  16. package/dist/src/cli.js +93 -0
  17. package/dist/src/config.d.ts +42 -0
  18. package/dist/src/config.js +188 -0
  19. package/dist/src/executor/cdp-executor.d.ts +131 -0
  20. package/dist/src/executor/cdp-executor.js +422 -0
  21. package/dist/src/executor/extension-executor.d.ts +102 -0
  22. package/dist/src/executor/extension-executor.js +124 -0
  23. package/dist/src/executor/manager.d.ts +43 -0
  24. package/dist/src/executor/manager.js +94 -0
  25. package/dist/src/executor/select.d.ts +23 -0
  26. package/dist/src/executor/select.js +53 -0
  27. package/dist/src/executor/stub-executor.d.ts +60 -0
  28. package/dist/src/executor/stub-executor.js +118 -0
  29. package/dist/src/executor/types.d.ts +192 -0
  30. package/dist/src/executor/types.js +24 -0
  31. package/dist/src/mcp/envelopes.d.ts +13 -0
  32. package/dist/src/mcp/envelopes.js +30 -0
  33. package/dist/src/mcp/helpers.d.ts +37 -0
  34. package/dist/src/mcp/helpers.js +71 -0
  35. package/dist/src/mcp/markdown-extract.d.ts +9 -0
  36. package/dist/src/mcp/markdown-extract.js +61 -0
  37. package/dist/src/mcp/server.d.ts +18 -0
  38. package/dist/src/mcp/server.js +82 -0
  39. package/dist/src/mcp/tools.d.ts +32 -0
  40. package/dist/src/mcp/tools.js +267 -0
  41. package/dist/src/mcp/validators.d.ts +32 -0
  42. package/dist/src/mcp/validators.js +104 -0
  43. package/dist/src/security/policy.d.ts +48 -0
  44. package/dist/src/security/policy.js +155 -0
  45. package/docs/BLUEPRINT.md +596 -0
  46. package/extension-dist/background.js +567 -0
  47. package/extension-dist/manifest.json +12 -0
  48. package/extension-dist/options.html +32 -0
  49. package/extension-dist/options.js +37 -0
  50. package/package.json +69 -0
  51. package/scripts/postinstall.js +50 -0
@@ -0,0 +1,422 @@
1
+ "use strict";
2
+ /**
3
+ * src/executor/cdp-executor.ts — the Playwright-driven fallback Executor.
4
+ *
5
+ * Used when no extension is paired. Two acquisition modes:
6
+ * - 'connect': attach over CDP to a Chrome we did NOT launch (never closed by
7
+ * us — its lifecycle is the user's).
8
+ * - 'launch': spawn a dedicated persistent Chromium under a profile that is
9
+ * NOT the user's real one (would conflict with the extension-driven Chrome).
10
+ *
11
+ * Ported from linkedin-mcp/src/driver/browser.ts: the stale-profile-lock
12
+ * recovery (a SIGKILLed MCP child orphans `SingletonLock`), connect-once reuse,
13
+ * stealth init on the launch path only, and tab resolution. `tabId`s are stamped
14
+ * `cdp:<sessionId>:<n>` so a handle never mis-routes across a backend switch.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.CdpExecutor = void 0;
18
+ const node_child_process_1 = require("node:child_process");
19
+ const node_fs_1 = require("node:fs");
20
+ const node_path_1 = require("node:path");
21
+ const node_crypto_1 = require("node:crypto");
22
+ const playwright_1 = require("playwright");
23
+ const download_1 = require("../../shared/download");
24
+ const types_1 = require("./types");
25
+ const SINGLETON_FILES = ['SingletonLock', 'SingletonSocket', 'SingletonCookie'];
26
+ const STEALTH = `Object.defineProperty(navigator,'webdriver',{get:()=>undefined});`;
27
+ const NON_CONTENT = /^(file|data|devtools|chrome|about):/i;
28
+ // --- lock recovery helpers (ported) ---------------------------------------
29
+ function inspectProfileLock(profileDir) {
30
+ try {
31
+ const lockPath = (0, node_path_1.join)(profileDir, 'SingletonLock');
32
+ if (!(0, node_fs_1.existsSync)(lockPath))
33
+ return { alive: false, pid: null };
34
+ const target = (0, node_fs_1.readlinkSync)(lockPath);
35
+ const pid = Number.parseInt(target.slice(target.lastIndexOf('-') + 1), 10);
36
+ if (!Number.isInteger(pid) || pid <= 0)
37
+ return { alive: false, pid: null };
38
+ try {
39
+ process.kill(pid, 0);
40
+ return { alive: true, pid };
41
+ }
42
+ catch (e) {
43
+ return { alive: e.code === 'EPERM', pid };
44
+ }
45
+ }
46
+ catch {
47
+ return { alive: false, pid: null };
48
+ }
49
+ }
50
+ function clearProfileLocks(profileDir) {
51
+ for (const name of SINGLETON_FILES) {
52
+ try {
53
+ (0, node_fs_1.rmSync)((0, node_path_1.join)(profileDir, name), { force: true });
54
+ }
55
+ catch {
56
+ /* best effort */
57
+ }
58
+ }
59
+ }
60
+ function isChromiumProcess(pid) {
61
+ try {
62
+ const out = (0, node_child_process_1.execFileSync)('ps', ['-p', String(pid), '-o', 'command='], {
63
+ encoding: 'utf8',
64
+ stdio: ['ignore', 'pipe', 'ignore'],
65
+ });
66
+ return /chrom(e|ium)|Chrome for Testing/i.test(out);
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ class CdpExecutor {
73
+ opts;
74
+ backend = 'cdp';
75
+ sessionId = (0, node_crypto_1.randomUUID)();
76
+ profileDir;
77
+ cdpBrowser = null;
78
+ context = null;
79
+ launching = null;
80
+ /** Whether WE own (launched) the browser — only then may we close it. */
81
+ launched = false;
82
+ tabs = new Map();
83
+ seq = 0;
84
+ constructor(opts) {
85
+ this.opts = opts;
86
+ this.profileDir = (0, node_path_1.join)(opts.userDataDir, 'cdp-profile');
87
+ }
88
+ status() {
89
+ return {
90
+ ready: this.context !== null,
91
+ backend: this.backend,
92
+ activeTabId: null,
93
+ extensionConnected: false,
94
+ cdpAttached: this.context !== null,
95
+ };
96
+ }
97
+ async ensureReady() {
98
+ await this.getContext();
99
+ }
100
+ async ping() {
101
+ return this.context !== null;
102
+ }
103
+ async dispose() {
104
+ const ctx = this.context;
105
+ this.context = null;
106
+ this.tabs.clear();
107
+ if (this.opts.mode === 'connect' || !this.launched)
108
+ return; // never close the user's Chrome
109
+ try {
110
+ await ctx?.close();
111
+ }
112
+ catch {
113
+ /* ignore */
114
+ }
115
+ this.launched = false;
116
+ }
117
+ // -- acquisition --------------------------------------------------------
118
+ async getContext() {
119
+ if (this.context)
120
+ return this.context;
121
+ if (this.launching)
122
+ return this.launching;
123
+ this.launching = this.opts.mode === 'connect' ? this.doConnect() : this.doLaunch();
124
+ try {
125
+ return await this.launching;
126
+ }
127
+ finally {
128
+ this.launching = null;
129
+ }
130
+ }
131
+ async doConnect() {
132
+ if (!this.opts.cdpEndpoint)
133
+ throw new types_1.ExecutorError('LAUNCH_FAILED', 'connect mode requires a cdpEndpoint');
134
+ if (!this.cdpBrowser || !this.cdpBrowser.isConnected()) {
135
+ this.cdpBrowser = await playwright_1.chromium.connectOverCDP(this.opts.cdpEndpoint);
136
+ this.cdpBrowser.on('disconnected', () => {
137
+ this.cdpBrowser = null;
138
+ this.context = null;
139
+ this.tabs.clear();
140
+ });
141
+ }
142
+ const ctx = this.cdpBrowser.contexts()[0];
143
+ if (!ctx)
144
+ throw new types_1.ExecutorError('LAUNCH_FAILED', 'connected browser exposed no context');
145
+ this.context = ctx;
146
+ this.launched = false;
147
+ return ctx;
148
+ }
149
+ async doLaunch() {
150
+ (0, node_fs_1.mkdirSync)(this.profileDir, { recursive: true });
151
+ const args = ['--disable-blink-features=AutomationControlled'];
152
+ let ctx;
153
+ try {
154
+ ctx = await this.launchWithLockRecovery(args);
155
+ }
156
+ catch (err) {
157
+ throw new types_1.ExecutorError('LAUNCH_FAILED', `failed to launch Chromium: ${err.message}`);
158
+ }
159
+ try {
160
+ await ctx.addInitScript(STEALTH);
161
+ }
162
+ catch {
163
+ /* non-fatal */
164
+ }
165
+ this.context = ctx;
166
+ this.launched = true;
167
+ ctx.on('close', () => {
168
+ if (this.context === ctx) {
169
+ this.context = null;
170
+ this.tabs.clear();
171
+ }
172
+ });
173
+ return ctx;
174
+ }
175
+ async launchWithLockRecovery(args) {
176
+ const launchOpts = {
177
+ headless: this.opts.headless ?? true,
178
+ args,
179
+ acceptDownloads: true,
180
+ };
181
+ try {
182
+ return await playwright_1.chromium.launchPersistentContext(this.profileDir, launchOpts);
183
+ }
184
+ catch (err) {
185
+ const msg = err.message ?? '';
186
+ if (!/already in use|existing browser session|ProcessSingleton|SingletonLock|profile.*in use/i.test(msg)) {
187
+ throw err;
188
+ }
189
+ const owner = inspectProfileLock(this.profileDir);
190
+ if (owner.alive && owner.pid !== null) {
191
+ if (isChromiumProcess(owner.pid)) {
192
+ try {
193
+ process.kill(owner.pid, 'SIGKILL');
194
+ }
195
+ catch {
196
+ /* gone */
197
+ }
198
+ await new Promise((r) => setTimeout(r, 500));
199
+ }
200
+ else {
201
+ throw new types_1.ExecutorError('LAUNCH_FAILED', `profile lock held by pid ${owner.pid}, which is not our Chromium`);
202
+ }
203
+ }
204
+ clearProfileLocks(this.profileDir);
205
+ return await playwright_1.chromium.launchPersistentContext(this.profileDir, launchOpts);
206
+ }
207
+ }
208
+ // -- tab resolution -----------------------------------------------------
209
+ idFor(page) {
210
+ for (const [id, p] of this.tabs)
211
+ if (p === page)
212
+ return id;
213
+ const id = `cdp:${this.sessionId}:${++this.seq}`;
214
+ this.tabs.set(id, page);
215
+ return id;
216
+ }
217
+ contentPages(ctx) {
218
+ return ctx.pages().filter((p) => {
219
+ const u = p.url();
220
+ return !!u && !NON_CONTENT.test(u) && u !== 'about:blank';
221
+ });
222
+ }
223
+ async resolveTab(tabId) {
224
+ const ctx = await this.getContext();
225
+ if (tabId) {
226
+ const p = this.tabs.get(tabId);
227
+ if (!p || p.isClosed()) {
228
+ if (tabId.startsWith('cdp:') && !tabId.startsWith(`cdp:${this.sessionId}:`)) {
229
+ throw new types_1.ExecutorError('STALE_TAB', 'tab handle is from a previous session; call tabs_list again');
230
+ }
231
+ throw new types_1.ExecutorError('TAB_NOT_FOUND', `no such tab: ${tabId}`);
232
+ }
233
+ return p;
234
+ }
235
+ const content = this.contentPages(ctx);
236
+ const page = content[0] ?? ctx.pages()[0] ?? (await ctx.newPage());
237
+ return page;
238
+ }
239
+ locator(page, t) {
240
+ if ('selector' in t && t.selector !== undefined)
241
+ return page.locator(t.selector).first();
242
+ throw new types_1.ExecutorError('SELECTOR_NOT_FOUND', 'the CDP fallback supports selectors, not refs (use the extension backend)');
243
+ }
244
+ // -- tabs ---------------------------------------------------------------
245
+ async tabsList() {
246
+ const ctx = await this.getContext();
247
+ const pages = this.contentPages(ctx);
248
+ const out = [];
249
+ for (let i = 0; i < pages.length; i++) {
250
+ const p = pages[i];
251
+ out.push({ tabId: this.idFor(p), url: p.url(), title: await p.title().catch(() => ''), active: i === 0, index: i });
252
+ }
253
+ return out;
254
+ }
255
+ async tabSelect(tabId) {
256
+ const p = await this.resolveTab(tabId);
257
+ await p.bringToFront().catch(() => undefined);
258
+ return { tabId, url: p.url(), title: await p.title().catch(() => ''), active: true, index: 0 };
259
+ }
260
+ async tabNew(url) {
261
+ const ctx = await this.getContext();
262
+ const p = await ctx.newPage();
263
+ if (url)
264
+ await p.goto(url, { waitUntil: 'load' }).catch(() => undefined);
265
+ return { tabId: this.idFor(p), url: p.url(), title: await p.title().catch(() => ''), active: true, index: 0 };
266
+ }
267
+ async tabClose(tabId) {
268
+ const p = await this.resolveTab(tabId);
269
+ await p.close();
270
+ this.tabs.delete(tabId);
271
+ return { closed: true, tabId };
272
+ }
273
+ // -- navigation ---------------------------------------------------------
274
+ toState(w) {
275
+ return w ?? 'load';
276
+ }
277
+ async navigate(args) {
278
+ const p = await this.resolveTab(args.tabId);
279
+ const resp = await p.goto(args.url, { waitUntil: this.toState(args.waitUntil) });
280
+ return { url: p.url(), title: await p.title().catch(() => ''), httpStatus: resp?.status() };
281
+ }
282
+ async back(tabId) {
283
+ const p = await this.resolveTab(tabId);
284
+ await p.goBack().catch(() => undefined);
285
+ return { url: p.url(), title: await p.title().catch(() => '') };
286
+ }
287
+ async forward(tabId) {
288
+ const p = await this.resolveTab(tabId);
289
+ await p.goForward().catch(() => undefined);
290
+ return { url: p.url(), title: await p.title().catch(() => '') };
291
+ }
292
+ async reload(args) {
293
+ const p = await this.resolveTab(args?.tabId);
294
+ await p.reload({ waitUntil: this.toState(args?.waitUntil) });
295
+ return { url: p.url(), title: await p.title().catch(() => '') };
296
+ }
297
+ // -- interaction --------------------------------------------------------
298
+ ok = { ok: true };
299
+ async click(t, opts) {
300
+ const p = await this.resolveTab(opts?.tabId);
301
+ await this.locator(p, t).click({ button: opts?.button, clickCount: opts?.clickCount });
302
+ return this.ok;
303
+ }
304
+ async type(t, text, opts) {
305
+ const p = await this.resolveTab(opts?.tabId);
306
+ const loc = this.locator(p, t);
307
+ if (opts?.clear)
308
+ await loc.fill('');
309
+ if (opts?.keyEvents)
310
+ await loc.pressSequentially(text);
311
+ else
312
+ await loc.fill((opts?.clear ? '' : '') + text);
313
+ if (opts?.pressEnter)
314
+ await loc.press('Enter');
315
+ return this.ok;
316
+ }
317
+ async fill(t, value, opts) {
318
+ const p = await this.resolveTab(opts?.tabId);
319
+ await this.locator(p, t).fill(value);
320
+ return this.ok;
321
+ }
322
+ async press(key, opts) {
323
+ const p = await this.resolveTab(opts?.tabId);
324
+ const combo = [...(opts?.modifiers ?? []), key].join('+');
325
+ await p.keyboard.press(combo);
326
+ return this.ok;
327
+ }
328
+ async hover(t, opts) {
329
+ const p = await this.resolveTab(opts?.tabId);
330
+ await this.locator(p, t).hover();
331
+ return this.ok;
332
+ }
333
+ async scroll(opts) {
334
+ const p = await this.resolveTab(opts.tabId);
335
+ if (opts.target)
336
+ await this.locator(p, opts.target).scrollIntoViewIfNeeded();
337
+ else if (opts.x !== undefined || opts.y !== undefined)
338
+ await p.evaluate(([x, y]) => window.scrollTo(x ?? 0, y ?? 0), [opts.x, opts.y]);
339
+ else
340
+ await p.mouse.wheel(opts.deltaX ?? 0, opts.deltaY ?? 0);
341
+ return this.ok;
342
+ }
343
+ // -- read ---------------------------------------------------------------
344
+ async getText(t, opts) {
345
+ const p = await this.resolveTab(opts?.tabId);
346
+ const text = t ? await this.locator(p, t).innerText() : await p.locator('body').innerText();
347
+ return { text };
348
+ }
349
+ async getHtml(t, opts) {
350
+ const p = await this.resolveTab(opts?.tabId);
351
+ if (!t)
352
+ return { html: await p.content() };
353
+ const loc = this.locator(p, t);
354
+ const html = opts?.outer ? await loc.evaluate((el) => el.outerHTML) : await loc.innerHTML();
355
+ return { html };
356
+ }
357
+ async screenshot(opts) {
358
+ const p = await this.resolveTab(opts?.tabId);
359
+ const buf = opts?.target
360
+ ? await this.locator(p, opts.target).screenshot()
361
+ : await p.screenshot({ fullPage: opts?.fullPage });
362
+ const size = p.viewportSize() ?? { width: 0, height: 0 };
363
+ return { dataBase64: buf.toString('base64'), mimeType: 'image/png', width: size.width, height: size.height, truncated: false };
364
+ }
365
+ async eval(expression, opts) {
366
+ const p = await this.resolveTab(opts?.tabId);
367
+ try {
368
+ const value = await p.evaluate((expr) => {
369
+ // eslint-disable-next-line no-eval
370
+ return (0, eval)(expr);
371
+ }, expression);
372
+ return { ok: true, value, type: typeof value };
373
+ }
374
+ catch (err) {
375
+ return { ok: false, error: err.message };
376
+ }
377
+ }
378
+ async waitFor(opts) {
379
+ const p = await this.resolveTab(opts.tabId);
380
+ const timeout = opts.timeoutMs ?? 30_000;
381
+ const start = Date.now();
382
+ try {
383
+ if (opts.selector) {
384
+ await p.locator(opts.selector).first().waitFor({ state: opts.gone ? 'detached' : 'visible', timeout });
385
+ }
386
+ else if (opts.textContains) {
387
+ await p.waitForFunction((needle) => document.body?.innerText.includes(needle) ?? false, opts.textContains, { timeout });
388
+ }
389
+ return { matched: true, waitedMs: Date.now() - start };
390
+ }
391
+ catch {
392
+ return { matched: false, waitedMs: Date.now() - start };
393
+ }
394
+ }
395
+ // -- privileged ---------------------------------------------------------
396
+ async download(args) {
397
+ const ctx = await this.getContext();
398
+ const dir = this.opts.downloadDir ?? (0, node_path_1.join)(this.opts.userDataDir, 'downloads');
399
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
400
+ const safeName = (0, download_1.sanitizeDownloadName)(args.suggestedName);
401
+ const dest = (0, node_path_1.join)(dir, safeName);
402
+ if (args.url) {
403
+ const resp = await ctx.request.get(args.url);
404
+ const body = await resp.body();
405
+ if (!(0, download_1.isWithinSizeCap)(body.length)) {
406
+ throw new types_1.ExecutorError('DOWNLOAD_FAILED', `download exceeds the ${download_1.MAX_DOWNLOAD_BYTES}-byte cap`);
407
+ }
408
+ (0, node_fs_1.writeFileSync)(dest, body);
409
+ return { path: dest, backend: this.backend, bytes: body.length, mimeType: resp.headers()['content-type'] };
410
+ }
411
+ // Element-triggered download.
412
+ const p = await this.resolveTab(args.tabId);
413
+ const target = args.target;
414
+ if (!target)
415
+ throw new types_1.ExecutorError('DOWNLOAD_FAILED', 'provide a url or a target');
416
+ const [dl] = await Promise.all([p.waitForEvent('download'), this.locator(p, target).click()]);
417
+ await dl.saveAs(dest);
418
+ return { path: dest, backend: this.backend, bytes: 0, suggestedName: dl.suggestedFilename() };
419
+ }
420
+ }
421
+ exports.CdpExecutor = CdpExecutor;
422
+ //# sourceMappingURL=cdp-executor.js.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * src/executor/extension-executor.ts — the Executor backed by the MV3 extension.
3
+ *
4
+ * Every method is a thin translation into a single `bridge.sendCommand(method,
5
+ * params, {tabId, timeoutMs})` round-trip; the extension does the real work over
6
+ * `chrome.debugger`. The "operate-on" tab travels in the frame's `tabId`;
7
+ * method-specific arguments travel in `params`. Results are trusted shapes
8
+ * produced by the extension router (validated there).
9
+ */
10
+ import { type ActionOk, type BackendKind, type DownloadResult, type EvalResult, type Executor, type ExecutorStatus, type KeyModifier, type MouseButton, type NavResult, type ScreenshotResult, type TabId, type TabInfo, type Target, type WaitResult, type WaitUntil } from './types';
11
+ import type { BridgeServer } from '../bridge/server';
12
+ export declare class ExtensionExecutor implements Executor {
13
+ private readonly bridge;
14
+ readonly backend: BackendKind;
15
+ constructor(bridge: BridgeServer);
16
+ private send;
17
+ status(): ExecutorStatus;
18
+ ensureReady(): Promise<void>;
19
+ ping(deadlineMs?: number): Promise<boolean>;
20
+ dispose(): Promise<void>;
21
+ tabsList(): Promise<TabInfo[]>;
22
+ tabSelect(tabId: TabId): Promise<TabInfo>;
23
+ tabNew(url?: string): Promise<TabInfo>;
24
+ tabClose(tabId: TabId): Promise<{
25
+ closed: true;
26
+ tabId: TabId;
27
+ }>;
28
+ navigate(args: {
29
+ url: string;
30
+ tabId?: TabId;
31
+ waitUntil?: WaitUntil;
32
+ }): Promise<NavResult>;
33
+ back(tabId?: TabId): Promise<NavResult>;
34
+ forward(tabId?: TabId): Promise<NavResult>;
35
+ reload(args?: {
36
+ tabId?: TabId;
37
+ waitUntil?: WaitUntil;
38
+ }): Promise<NavResult>;
39
+ click(t: Target, opts?: {
40
+ tabId?: TabId;
41
+ button?: MouseButton;
42
+ clickCount?: number;
43
+ }): Promise<ActionOk>;
44
+ type(t: Target, text: string, opts?: {
45
+ tabId?: TabId;
46
+ clear?: boolean;
47
+ pressEnter?: boolean;
48
+ keyEvents?: boolean;
49
+ }): Promise<ActionOk>;
50
+ fill(t: Target, value: string, opts?: {
51
+ tabId?: TabId;
52
+ }): Promise<ActionOk>;
53
+ press(key: string, opts?: {
54
+ tabId?: TabId;
55
+ modifiers?: KeyModifier[];
56
+ }): Promise<ActionOk>;
57
+ hover(t: Target, opts?: {
58
+ tabId?: TabId;
59
+ }): Promise<ActionOk>;
60
+ scroll(opts: {
61
+ tabId?: TabId;
62
+ x?: number;
63
+ y?: number;
64
+ deltaX?: number;
65
+ deltaY?: number;
66
+ target?: Target;
67
+ }): Promise<ActionOk>;
68
+ getText(t?: Target, opts?: {
69
+ tabId?: TabId;
70
+ }): Promise<{
71
+ text: string;
72
+ ref?: string;
73
+ }>;
74
+ getHtml(t?: Target, opts?: {
75
+ tabId?: TabId;
76
+ outer?: boolean;
77
+ }): Promise<{
78
+ html: string;
79
+ }>;
80
+ screenshot(opts?: {
81
+ tabId?: TabId;
82
+ fullPage?: boolean;
83
+ target?: Target;
84
+ }): Promise<ScreenshotResult>;
85
+ eval(expression: string, opts?: {
86
+ tabId?: TabId;
87
+ awaitPromise?: boolean;
88
+ }): Promise<EvalResult>;
89
+ waitFor(opts: {
90
+ tabId?: TabId;
91
+ selector?: string;
92
+ textContains?: string;
93
+ gone?: boolean;
94
+ timeoutMs?: number;
95
+ }): Promise<WaitResult>;
96
+ download(args: {
97
+ url?: string;
98
+ target?: Target;
99
+ tabId?: TabId;
100
+ suggestedName?: string;
101
+ }): Promise<DownloadResult>;
102
+ }
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /**
3
+ * src/executor/extension-executor.ts — the Executor backed by the MV3 extension.
4
+ *
5
+ * Every method is a thin translation into a single `bridge.sendCommand(method,
6
+ * params, {tabId, timeoutMs})` round-trip; the extension does the real work over
7
+ * `chrome.debugger`. The "operate-on" tab travels in the frame's `tabId`;
8
+ * method-specific arguments travel in `params`. Results are trusted shapes
9
+ * produced by the extension router (validated there).
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ExtensionExecutor = void 0;
13
+ /** Flatten a Target into the params a wire command carries. */
14
+ function targetParams(t) {
15
+ if (!t)
16
+ return {};
17
+ return 'selector' in t && t.selector !== undefined ? { selector: t.selector } : { ref: t.ref };
18
+ }
19
+ class ExtensionExecutor {
20
+ bridge;
21
+ backend = 'extension';
22
+ constructor(bridge) {
23
+ this.bridge = bridge;
24
+ }
25
+ send(method, params, opts) {
26
+ return this.bridge.sendCommand(method, params, opts);
27
+ }
28
+ status() {
29
+ const connected = this.bridge.hasActiveExtension();
30
+ return {
31
+ ready: connected,
32
+ backend: this.backend,
33
+ activeTabId: null, // not known synchronously
34
+ extensionConnected: connected,
35
+ cdpAttached: false,
36
+ };
37
+ }
38
+ async ensureReady() {
39
+ // The bridge owns connectivity; nothing to launch here. The selector has
40
+ // already confirmed an extension is paired + responsive before picking us.
41
+ }
42
+ async ping(deadlineMs = 800) {
43
+ if (!this.bridge.hasActiveExtension())
44
+ return false;
45
+ try {
46
+ await this.send('ping_probe', {}, { timeoutMs: deadlineMs });
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ async dispose() {
54
+ // Never close the user's Chrome.
55
+ }
56
+ // -- tabs ---------------------------------------------------------------
57
+ async tabsList() {
58
+ return (await this.send('tabs_list', {}));
59
+ }
60
+ async tabSelect(tabId) {
61
+ return (await this.send('tab_select', {}, { tabId }));
62
+ }
63
+ async tabNew(url) {
64
+ return (await this.send('tab_new', { url }));
65
+ }
66
+ async tabClose(tabId) {
67
+ return (await this.send('tab_close', {}, { tabId }));
68
+ }
69
+ // -- navigation ---------------------------------------------------------
70
+ async navigate(args) {
71
+ return (await this.send('navigate', { url: args.url, waitUntil: args.waitUntil }, { tabId: args.tabId }));
72
+ }
73
+ async back(tabId) {
74
+ return (await this.send('back', {}, { tabId }));
75
+ }
76
+ async forward(tabId) {
77
+ return (await this.send('forward', {}, { tabId }));
78
+ }
79
+ async reload(args) {
80
+ return (await this.send('reload', { waitUntil: args?.waitUntil }, { tabId: args?.tabId }));
81
+ }
82
+ // -- interaction --------------------------------------------------------
83
+ async click(t, opts) {
84
+ return (await this.send('click', { ...targetParams(t), button: opts?.button, clickCount: opts?.clickCount }, { tabId: opts?.tabId }));
85
+ }
86
+ async type(t, text, opts) {
87
+ return (await this.send('type', { ...targetParams(t), text, clear: opts?.clear, pressEnter: opts?.pressEnter, keyEvents: opts?.keyEvents }, { tabId: opts?.tabId }));
88
+ }
89
+ async fill(t, value, opts) {
90
+ // No dedicated wire method: a cleared insertText is the fill primitive.
91
+ return (await this.send('type', { ...targetParams(t), text: value, clear: true, keyEvents: false }, { tabId: opts?.tabId }));
92
+ }
93
+ async press(key, opts) {
94
+ return (await this.send('press', { key, modifiers: opts?.modifiers }, { tabId: opts?.tabId }));
95
+ }
96
+ async hover(t, opts) {
97
+ return (await this.send('hover', { ...targetParams(t) }, { tabId: opts?.tabId }));
98
+ }
99
+ async scroll(opts) {
100
+ return (await this.send('scroll', { x: opts.x, y: opts.y, deltaX: opts.deltaX, deltaY: opts.deltaY, ...targetParams(opts.target) }, { tabId: opts.tabId }));
101
+ }
102
+ // -- read ---------------------------------------------------------------
103
+ async getText(t, opts) {
104
+ return (await this.send('get_text', { ...targetParams(t) }, { tabId: opts?.tabId }));
105
+ }
106
+ async getHtml(t, opts) {
107
+ return (await this.send('get_html', { ...targetParams(t), outer: opts?.outer }, { tabId: opts?.tabId }));
108
+ }
109
+ async screenshot(opts) {
110
+ return (await this.send('screenshot', { fullPage: opts?.fullPage, ...targetParams(opts?.target) }, { tabId: opts?.tabId }));
111
+ }
112
+ async eval(expression, opts) {
113
+ return (await this.send('eval', { expression, awaitPromise: opts?.awaitPromise }, { tabId: opts?.tabId }));
114
+ }
115
+ async waitFor(opts) {
116
+ return (await this.send('wait_for', { selector: opts.selector, textContains: opts.textContains, gone: opts.gone, timeoutMs: opts.timeoutMs }, { tabId: opts.tabId, timeoutMs: opts.timeoutMs ? opts.timeoutMs + 5_000 : undefined }));
117
+ }
118
+ // -- privileged ---------------------------------------------------------
119
+ async download(args) {
120
+ return (await this.send('download_file', { url: args.url, ...targetParams(args.target), suggestedName: args.suggestedName }, { tabId: args.tabId }));
121
+ }
122
+ }
123
+ exports.ExtensionExecutor = ExtensionExecutor;
124
+ //# sourceMappingURL=extension-executor.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * src/executor/manager.ts — owns the process-global active Executor and the
3
+ * `withReadyExecutor()` accessor every tool routes through (the `withReadyDriver`
4
+ * analog from the LinkedIn repo).
5
+ *
6
+ * Phase 1 holds a single Executor produced by an injected factory (the Stub).
7
+ * Later phases replace the factory with real selection logic (extension-if-
8
+ * responsive else CDP), but the surface the tools see — `ensureReady()` +
9
+ * `policy` — does not change.
10
+ */
11
+ import { type Executor } from './types';
12
+ import type { Policy } from '../security/policy';
13
+ export interface ManagerOptions {
14
+ policy: Policy;
15
+ /** Lazily construct a single backend on first use (Phase 1: the Stub). */
16
+ makeExecutor?: () => Executor;
17
+ /** Phase 3+ selection strategy, re-evaluated each `ensureReady()`. Wins over
18
+ * `makeExecutor` when both are present. Returns the chosen (not-yet-ready)
19
+ * Executor; the manager calls `ensureReady()` on it. */
20
+ select?: () => Promise<Executor>;
21
+ }
22
+ export declare class ExecutorManager {
23
+ private readonly opts;
24
+ private current;
25
+ private readying;
26
+ constructor(opts: ManagerOptions);
27
+ get policy(): Policy;
28
+ /** Inject a ready-made executor (tests, or a later phase's selector). */
29
+ setExecutor(ex: Executor): void;
30
+ /**
31
+ * Return a ready Executor, constructing it lazily and single-flight-guarding
32
+ * concurrent callers. Throws `NO_BACKEND` when nothing is configured.
33
+ */
34
+ ensureReady(): Promise<Executor>;
35
+ private doEnsure;
36
+ dispose(): Promise<void>;
37
+ }
38
+ /** Install (or replace) the global manager. Returns it. */
39
+ export declare function configureManager(opts: ManagerOptions): ExecutorManager;
40
+ export declare function getManager(): ExecutorManager;
41
+ /** Tear down the singleton (tests). */
42
+ export declare function resetManagerForTesting(): void;
43
+ export declare function withReadyExecutor(): Promise<Executor>;