ask-pro 0.2.80 → 0.2.87

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
@@ -27,8 +27,8 @@ Both installs run the same bundled launcher. The `ask-pro` npm package
27
27
  (`npm i -g ask-pro`) still works too. All three share the same state.
28
28
 
29
29
  You need Node.js 24+, npm, Chrome, and a ChatGPT account with Pro access. The
30
- browser runtime is developed and tested on Windows; other operating systems
31
- need validation.
30
+ browser runtime supports Windows and Linux. Linux without `DISPLAY` or
31
+ `WAYLAND_DISPLAY` uses headless Chrome. See [Linux setup and remote sign-in](docs/linux.md).
32
32
 
33
33
  **Upgrading from an earlier version:** nothing to do. On its first run,
34
34
  ask-pro moves your signed-in browser profiles from `~/.codex/state/ask-pro` to
package/SKILL.md CHANGED
@@ -11,6 +11,15 @@ The calling agent still owns the work. Use Pro for judgment, architecture, risk
11
11
 
12
12
  ## Running ask-pro
13
13
 
14
+ On a headless Linux host, set `CHROME_PATH` if Chrome is not discovered, and
15
+ `ASK_PRO_LOGIN_HOST` to the host's private Tailscale IP. Before the first query,
16
+ run `ask-pro --login` and give the human its `login_url`. Keep that command alive
17
+ while they sign in from their desktop or phone. The link expires after 15 minutes
18
+ or Done. Never drive the login page or capture its frames during real sign-in.
19
+ After the human confirms the ChatGPT composer is visible, resume the saved
20
+ session or start the requested query. A later `needs_auth` result includes a
21
+ `login` command for the same profile. See [Linux setup](docs/linux.md).
22
+
14
23
  This skill folder contains ask-pro itself; nothing else needs installing. In
15
24
  the commands below, `ask-pro` stands for:
16
25
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import "dotenv/config";
3
+ import { runRemoteLogin } from "../src/ask-pro/remoteLogin.js";
3
4
  import { AssistantStoppedError } from "../src/browser/errors.js";
4
5
  import { AssistantQuestionsError } from "../src/browser/actions/questions.js";
5
6
  import { readAskProQuestions } from "../src/ask-pro/questions.js";
@@ -8,7 +9,7 @@ import fs from "node:fs/promises";
8
9
  import path from "node:path";
9
10
  import { Command, Option } from "commander";
10
11
  import { createAskProSession, getAskProSessionPaths, pruneExpiredAskProSessions, readAskProAnswer, readAskProStatus, updateAskProResumeCommand, updateAskProStatus, } from "../src/ask-pro/session.js";
11
- import { AskProNeedsAuthError, isSuspiciousPreambleAnswer, resumeAskProBrowserSession, runAskProBrowserSession, } from "../src/ask-pro/browserRunner.js";
12
+ import { AskProNeedsAuthError, resolveAskProLoginProfile, isSuspiciousPreambleAnswer, resumeAskProBrowserSession, runAskProBrowserSession, } from "../src/ask-pro/browserRunner.js";
12
13
  import { renderToonRecord } from "../src/ask-pro/toon.js";
13
14
  import { askProAgentIdForLegacyBrowserProfileDir, askProAgentIdForManagedBrowserProfileDir, defaultAskProBrowserProfileDir, isLegacyAskProManagedBrowserProfileDir, } from "../src/browser/profilePaths.js";
14
15
  import { migrateLegacyAskProProfiles } from "../src/browser/profileMigration.js";
@@ -20,6 +21,20 @@ program
20
21
  .version(getCliVersion())
21
22
  .argument("[question...]", "question to send to ChatGPT Pro")
22
23
  .option("--dry-run", "prepare the session and context bundle without opening the browser")
24
+ .addOption(new Option("--login [session-id]", "open a private remote sign-in link for headless Chrome").conflicts([
25
+ "dryRun",
26
+ "files",
27
+ "promptFile",
28
+ "answers",
29
+ "artifacts",
30
+ "github",
31
+ "responseZip",
32
+ "resume",
33
+ "status",
34
+ "harvest",
35
+ "copy",
36
+ "temporary",
37
+ ]))
23
38
  .option("--files <pattern>", "include files or globs in the context bundle", collectFiles, [])
24
39
  .option("--prompt-file <path>", "read the question from a UTF-8 file; use - for stdin")
25
40
  .option("--artifacts", "ask Pro for ask-pro-response.zip plus markdown fallback")
@@ -58,6 +73,21 @@ program
58
73
  await program.parseAsync(process.argv);
59
74
  async function runAskPro(question, options) {
60
75
  const cwd = resolveProjectCwd(options);
76
+ if (options.login) {
77
+ if (question.trim())
78
+ throw new Error("Use --login without a question.");
79
+ const profile = typeof options.login === "string"
80
+ ? await resolveAskProLoginProfile(cwd, options.login)
81
+ : undefined;
82
+ await runRemoteLogin((url) => writeToon("ask_pro", {
83
+ state: "needs_auth",
84
+ action: "open_login_url",
85
+ login_url: url,
86
+ expires_minutes: 15,
87
+ }), profile);
88
+ writeToon("ask_pro", { state: "login_closed", action: "confirm_composer_then_resume" });
89
+ return;
90
+ }
61
91
  if (options.answers && question.trim())
62
92
  throw new Error("Use --answers with --resume, without a question argument.");
63
93
  await pruneExpiredAskProSessions({ cwd });
@@ -347,6 +377,11 @@ async function printAuthInstructions(sessionId, options, cwd, error) {
347
377
  ...fallbackPreflight,
348
378
  ...browserPreflight,
349
379
  action: "human_login_then_resume",
380
+ ...(process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY
381
+ ? {
382
+ login: `${buildLauncherCommand()} --cwd ${quoteCommandArg(cwd)} --login ${quoteCommandArg(sessionId)}`,
383
+ }
384
+ : {}),
350
385
  resume: resumeCommand,
351
386
  });
352
387
  }
@@ -717,6 +717,10 @@ async function writeTerminalBrowserMetadata(cwd, sessionId, status, reason) {
717
717
  },
718
718
  });
719
719
  }
720
+ export async function resolveAskProLoginProfile(cwd, sessionId) {
721
+ const paths = getAskProSessionPaths(cwd, sessionId);
722
+ return resolveResumeBrowserProfile(await readBrowserMetadata(paths.browser));
723
+ }
720
724
  async function resolveResumeBrowserProfile(metadata) {
721
725
  const agentProfile = resolveStoredAgentProfile(metadata.agentId);
722
726
  const profileDir = metadata.profileDir;
@@ -0,0 +1,29 @@
1
+ // The browser relays human input only. Never persist input, frames, or the access key.
2
+ export const loginPage = `<!doctype html>
3
+ <html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4
+ <title>ask-pro sign in</title>
5
+ <style>
6
+ :root{font:16px/1.5 system-ui;color:#182421;background:#f5f7f6}body{margin:0}main{max-width:1280px;margin:auto;padding:20px}header{display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin-bottom:16px}h1{font-size:22px;margin:0 auto 0 0}button,input,select{font:inherit;padding:10px;border:1px solid #62716b;border-radius:6px;background:white;color:inherit}button{cursor:pointer}button:hover{background:#e0ebe5}:focus-visible{outline:3px solid #197451;outline-offset:2px}#screen{display:block;width:100%;background:white;touch-action:manipulation}form{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px}input{flex:1;min-width:150px}#status{min-height:24px;margin:8px 0;color:#344b41}button:disabled{opacity:.6;cursor:wait}
7
+ </style><main><header><h1>Sign in to ChatGPT</h1><select id="tabs" aria-label="Browser tab"></select><button id="refresh">Refresh tabs</button><button id="done">Done</button></header>
8
+ <p id="status" role="status">Connecting…</p><img id="screen" tabindex="0" alt="Remote browser. Click a field, then type, or use the text box below.">
9
+ <form><input id="text" type="password" autocomplete="off" aria-label="Text for selected browser field" placeholder="Type into selected field"><button>Send text</button><button type="button" id="enter">Enter</button><button type="button" id="tab">Tab</button><button type="button" id="backspace">Backspace</button><button type="button" id="up">Scroll up</button><button type="button" id="down">Scroll down</button></form></main>
10
+ <script>
11
+ const token=location.hash.slice(1);history.replaceState(null,'',location.pathname);
12
+ const screen=document.querySelector('#screen'),tabs=document.querySelector('#tabs'),status=document.querySelector('#status'),text=document.querySelector('#text');
13
+ let stopped=false,frameUrl,inputQueue=Promise.resolve();
14
+ async function api(route,body){const r=await fetch(route,{method:body===undefined?'GET':'POST',headers:{Authorization:'Bearer '+token,'Content-Type':'application/json'},body:body===undefined?undefined:JSON.stringify(body),cache:'no-store'});if(!r.ok)throw Error(r.status===401?'Link expired. Start ask-pro --login again.':'Connection failed. Retry or refresh tabs.');return r;}
15
+ function report(e){status.textContent=e.message;}
16
+ function input(body){const target=tabs.value;const request=inputQueue.then(()=>api('/input',{target,...body}));inputQueue=request.catch(report);return request;}
17
+ async function refresh(){try{const list=await(await api('/tabs')).json();const previous=tabs.value;tabs.replaceChildren(...list.map(t=>new Option(t.name,t.id)));if(list.some(t=>t.id===previous))tabs.value=previous;status.textContent='Click the browser to sign in. Select Done when the ChatGPT composer is visible.';}catch(e){report(e);}}
18
+ async function frame(){if(stopped)return;try{if(tabs.value){const r=await api('/frame?target='+encodeURIComponent(tabs.value));const url=URL.createObjectURL(await r.blob());screen.src=url;if(frameUrl)URL.revokeObjectURL(frameUrl);frameUrl=url;}}catch(e){report(e);}setTimeout(frame,350);}
19
+ screen.onclick=e=>{const r=screen.getBoundingClientRect();input({kind:'click',x:(e.clientX-r.left)*screen.naturalWidth/r.width,y:(e.clientY-r.top)*screen.naturalHeight/r.height});screen.focus();};
20
+ screen.onkeydown=e=>{if(e.ctrlKey||e.metaKey||e.altKey)return;if(e.key.length===1){e.preventDefault();input({kind:'text',text:e.key});}else if(['Enter','Tab','Backspace','Delete','ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Escape','Home','End'].includes(e.key)){e.preventDefault();input({kind:'key',key:e.key});}};
21
+ screen.onwheel=e=>{e.preventDefault();const r=screen.getBoundingClientRect();input({kind:'scroll',x:(e.clientX-r.left)*screen.naturalWidth/r.width,y:(e.clientY-r.top)*screen.naturalHeight/r.height,deltaY:e.deltaY});};
22
+ document.querySelector('form').onsubmit=async e=>{e.preventDefault();const value=text.value;try{await input({kind:'text',text:value});if(text.value===value)text.value='';}catch(e){report(e);}};
23
+ for(const key of ['Enter','Tab','Backspace'])document.querySelector('#'+key.toLowerCase()).onclick=()=>input({kind:'key',key});
24
+ document.querySelector('#up').onclick=()=>input({kind:'scroll',x:100,y:100,deltaY:-400});
25
+ document.querySelector('#down').onclick=()=>input({kind:'scroll',x:100,y:100,deltaY:400});
26
+ document.querySelector('#refresh').onclick=refresh;
27
+ document.querySelector('#done').onclick=async()=>{try{await api('/done',{});stopped=true;screen.removeAttribute('src');text.value='';status.textContent='Sign-in window closed. You can return to your agent.';}catch(e){report(e);}};
28
+ refresh().then(frame);
29
+ </script></html>`;
@@ -0,0 +1,254 @@
1
+ import http from "node:http";
2
+ import { randomBytes } from "node:crypto";
3
+ import os from "node:os";
4
+ import CDP from "chrome-remote-interface";
5
+ import { resolveBrowserConfig } from "../browser/config.js";
6
+ import { prepareAskProBrowserProfile } from "../browser/profileMigration.js";
7
+ import { resolveAskProAgentId } from "../browser/profilePaths.js";
8
+ import { acquireProfileRunLock, createManagedChromeRunLease, releaseManagedChromeRunLeaseAndCountPeers, writeChromePid, writeDevToolsActivePort, } from "../browser/profileState.js";
9
+ import { launchChrome, maybeReuseRunningChrome, releaseChromeProcessHandle, } from "../browser/chromeLifecycle.js";
10
+ import { loginPage } from "./loginPage.js";
11
+ const keys = new Set([
12
+ "Enter",
13
+ "Tab",
14
+ "Backspace",
15
+ "Delete",
16
+ "ArrowLeft",
17
+ "ArrowRight",
18
+ "ArrowUp",
19
+ "ArrowDown",
20
+ "Escape",
21
+ "Home",
22
+ "End",
23
+ ]);
24
+ /** Narrow human-operated relay: no cookies, DOM, evaluation, or arbitrary CDP endpoint. */
25
+ export async function startLoginServer(options) {
26
+ const token = randomBytes(32).toString("hex");
27
+ let client;
28
+ let targetId;
29
+ let queue = Promise.resolve();
30
+ let finish;
31
+ const done = new Promise((resolve) => {
32
+ finish = resolve;
33
+ });
34
+ const targets = async () => (await CDP.List({ port: options.chromePort })).filter((t) => t.type === "page" && /^(https?:|about:blank)/.test(t.url));
35
+ async function connect(id) {
36
+ if (!(await targets()).some((t) => t.id === id))
37
+ throw Error("Unavailable tab");
38
+ if (targetId !== id || !client) {
39
+ await client?.close().catch(() => undefined);
40
+ client = await CDP({ port: options.chromePort, target: id });
41
+ targetId = id;
42
+ client.on("disconnect", () => {
43
+ if (targetId === id) {
44
+ client = undefined;
45
+ targetId = undefined;
46
+ }
47
+ });
48
+ }
49
+ return client;
50
+ }
51
+ const server = http.createServer((req, res) => {
52
+ res.setHeader("Cache-Control", "no-store");
53
+ res.setHeader("Referrer-Policy", "no-referrer");
54
+ res.setHeader("X-Frame-Options", "DENY");
55
+ res.setHeader("X-Content-Type-Options", "nosniff");
56
+ res.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src blob:; connect-src 'self'; frame-ancestors 'none'; form-action 'none'");
57
+ if (req.method === "GET" && req.url === "/") {
58
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
59
+ res.end(loginPage);
60
+ return;
61
+ }
62
+ if (req.headers.authorization !== "Bearer " + token) {
63
+ res.writeHead(401).end();
64
+ return;
65
+ }
66
+ // Reject cross-origin browser requests even when a caller has the link.
67
+ if (req.headers.origin && req.headers.origin !== "http://" + req.headers.host) {
68
+ res.writeHead(403).end();
69
+ return;
70
+ }
71
+ const route = new URL(req.url ?? "/", "http://localhost");
72
+ const run = async () => {
73
+ if (req.method === "GET" && route.pathname === "/tabs") {
74
+ res.setHeader("Content-Type", "application/json");
75
+ res.end(JSON.stringify((await targets()).map((t) => ({
76
+ id: t.id,
77
+ name: t.url === "about:blank" ? "Blank tab" : new URL(t.url).hostname,
78
+ }))));
79
+ return;
80
+ }
81
+ if (req.method === "GET" && route.pathname === "/frame") {
82
+ const c = await connect(route.searchParams.get("target") ?? "");
83
+ const frame = await c.Page.captureScreenshot({ format: "jpeg", quality: 70 });
84
+ res.setHeader("Content-Type", "image/jpeg");
85
+ res.end(Buffer.from(frame.data, "base64"));
86
+ return;
87
+ }
88
+ if (req.method !== "POST" || !["/input", "/done"].includes(route.pathname)) {
89
+ res.writeHead(404).end();
90
+ return;
91
+ }
92
+ let raw = "";
93
+ for await (const chunk of req) {
94
+ raw += chunk;
95
+ if (Buffer.byteLength(raw) > 16384) {
96
+ res.writeHead(413).end();
97
+ return;
98
+ }
99
+ }
100
+ if (route.pathname === "/done") {
101
+ res.end();
102
+ setImmediate(() => {
103
+ void close();
104
+ });
105
+ return;
106
+ }
107
+ const body = JSON.parse(raw);
108
+ const c = await connect(typeof body.target === "string" ? body.target : "");
109
+ await c.Page.bringToFront();
110
+ const coordinate = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
111
+ if (body.kind === "text" && typeof body.text === "string")
112
+ await c.Input.insertText({ text: body.text });
113
+ else if (body.kind === "key" && typeof body.key === "string" && keys.has(body.key)) {
114
+ const key = body.key;
115
+ const code = {
116
+ Enter: 13,
117
+ Tab: 9,
118
+ Backspace: 8,
119
+ Delete: 46,
120
+ Escape: 27,
121
+ ArrowLeft: 37,
122
+ ArrowUp: 38,
123
+ ArrowRight: 39,
124
+ ArrowDown: 40,
125
+ Home: 36,
126
+ End: 35,
127
+ }[key];
128
+ await c.Input.dispatchKeyEvent({
129
+ type: "keyDown",
130
+ key,
131
+ code: key,
132
+ windowsVirtualKeyCode: code,
133
+ ...(key === "Enter" ? { text: "\r" } : {}),
134
+ });
135
+ await c.Input.dispatchKeyEvent({
136
+ type: "keyUp",
137
+ key,
138
+ code: key,
139
+ windowsVirtualKeyCode: code,
140
+ });
141
+ }
142
+ else if ((body.kind === "click" || body.kind === "scroll") &&
143
+ coordinate(body.x) &&
144
+ coordinate(body.y)) {
145
+ const x = body.x, y = body.y;
146
+ if (body.kind === "click") {
147
+ await c.Input.dispatchMouseEvent({
148
+ type: "mousePressed",
149
+ x,
150
+ y,
151
+ button: "left",
152
+ clickCount: 1,
153
+ });
154
+ await c.Input.dispatchMouseEvent({
155
+ type: "mouseReleased",
156
+ x,
157
+ y,
158
+ button: "left",
159
+ clickCount: 1,
160
+ });
161
+ }
162
+ else if (typeof body.deltaY === "number" && Number.isFinite(body.deltaY))
163
+ await c.Input.dispatchMouseEvent({
164
+ type: "mouseWheel",
165
+ x,
166
+ y,
167
+ deltaX: 0,
168
+ deltaY: body.deltaY,
169
+ });
170
+ else {
171
+ res.writeHead(400).end();
172
+ return;
173
+ }
174
+ }
175
+ else {
176
+ res.writeHead(400).end();
177
+ return;
178
+ }
179
+ res.end();
180
+ };
181
+ queue = queue.then(run).catch(() => {
182
+ if (!res.headersSent)
183
+ res.writeHead(400);
184
+ res.end();
185
+ });
186
+ });
187
+ let closed = false;
188
+ async function close() {
189
+ if (closed)
190
+ return;
191
+ closed = true;
192
+ clearTimeout(timer);
193
+ server.close();
194
+ server.closeAllConnections();
195
+ await client?.close().catch(() => undefined);
196
+ finish();
197
+ }
198
+ await new Promise((resolve, reject) => {
199
+ server.once("error", reject);
200
+ server.listen(options.port ?? 0, options.host, resolve);
201
+ });
202
+ const timer = setTimeout(() => {
203
+ void close();
204
+ }, options.ttlMs ?? 15 * 60_000);
205
+ const address = server.address();
206
+ const hostname = options.host === "0.0.0.0" ? os.hostname() : options.host;
207
+ return { url: "http://" + hostname + ":" + address.port + "/#" + token, done, close };
208
+ }
209
+ export async function runRemoteLogin(onReady, savedProfile) {
210
+ const host = process.env.ASK_PRO_LOGIN_HOST?.trim();
211
+ if (!host)
212
+ throw Error("Set ASK_PRO_LOGIN_HOST to this machine’s private network IP (for example its Tailscale IP), then run --login.");
213
+ const profile = savedProfile ?? (await prepareAskProBrowserProfile(resolveAskProAgentId()));
214
+ const lock = await acquireProfileRunLock(profile, { timeoutMs: 1000 });
215
+ if (!lock)
216
+ throw Error("Browser busy. Wait for ask-pro runs to finish before --login.");
217
+ const lease = await createManagedChromeRunLease(profile);
218
+ let relay;
219
+ try {
220
+ // Count live automation leases while holding the launch lock; login never races a query.
221
+ const peers = await releaseManagedChromeRunLeaseAndCountPeers(profile, lease);
222
+ if (peers !== 0)
223
+ throw Error("Browser busy. Wait for ask-pro runs to finish before --login.");
224
+ const chrome = (await maybeReuseRunningChrome(profile, () => { })) ??
225
+ (await launchChrome(resolveBrowserConfig({ manualLogin: true, headless: true }), profile, () => { }, { ignoreLockId: lock.lockId }));
226
+ releaseChromeProcessHandle(chrome);
227
+ if (chrome.pid)
228
+ await writeChromePid(profile, chrome.pid);
229
+ await writeDevToolsActivePort(profile, chrome.port);
230
+ const pages = await CDP.List({ port: chrome.port });
231
+ if (!pages.some((t) => t.type === "page" && /^https:\/\/(chatgpt\.com|auth\.openai\.com)(\/|$)/.test(t.url))) {
232
+ await CDP.New({ port: chrome.port, url: "https://chatgpt.com/" });
233
+ }
234
+ relay = await startLoginServer({ chromePort: chrome.port, host });
235
+ const stop = () => {
236
+ void relay?.close();
237
+ };
238
+ process.once("SIGINT", stop);
239
+ process.once("SIGTERM", stop);
240
+ try {
241
+ onReady(relay.url);
242
+ await relay.done;
243
+ }
244
+ finally {
245
+ process.removeListener("SIGINT", stop);
246
+ process.removeListener("SIGTERM", stop);
247
+ }
248
+ }
249
+ finally {
250
+ await relay?.close();
251
+ await releaseManagedChromeRunLeaseAndCountPeers(profile, lease);
252
+ await lock.release();
253
+ }
254
+ }
@@ -2,6 +2,9 @@ import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./con
2
2
  import { normalizeBrowserModelStrategy } from "./modelStrategy.js";
3
3
  import { defaultAskProBrowserProfileDir } from "./profilePaths.js";
4
4
  import { normalizeChatgptUrl } from "./utils.js";
5
+ export function isHeadlessLinux(platform = process.platform, env = process.env) {
6
+ return platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY;
7
+ }
5
8
  export const DEFAULT_BROWSER_CONFIG = {
6
9
  chromeProfile: null,
7
10
  chromePath: null,
@@ -76,14 +79,14 @@ export function resolveBrowserConfig(config) {
76
79
  cookieSyncWaitMs: config?.cookieSyncWaitMs ?? DEFAULT_BROWSER_CONFIG.cookieSyncWaitMs,
77
80
  inlineCookies: config?.inlineCookies ?? DEFAULT_BROWSER_CONFIG.inlineCookies,
78
81
  inlineCookiesSource: config?.inlineCookiesSource ?? DEFAULT_BROWSER_CONFIG.inlineCookiesSource,
79
- headless: config?.headless ?? DEFAULT_BROWSER_CONFIG.headless,
82
+ headless: config?.headless ?? isHeadlessLinux(),
80
83
  keepBrowser: config?.keepBrowser ?? DEFAULT_BROWSER_CONFIG.keepBrowser,
81
84
  hideWindow: config?.hideWindow ?? DEFAULT_BROWSER_CONFIG.hideWindow,
82
85
  startMinimized: config?.startMinimized ?? DEFAULT_BROWSER_CONFIG.startMinimized,
83
86
  desiredModel,
84
87
  modelStrategy,
85
88
  chromeProfile: config?.chromeProfile ?? DEFAULT_BROWSER_CONFIG.chromeProfile,
86
- chromePath: config?.chromePath ?? DEFAULT_BROWSER_CONFIG.chromePath,
89
+ chromePath: config?.chromePath ?? process.env.CHROME_PATH ?? DEFAULT_BROWSER_CONFIG.chromePath,
87
90
  chromeCookiePath: config?.chromeCookiePath ?? DEFAULT_BROWSER_CONFIG.chromeCookiePath,
88
91
  attachRunning: config?.attachRunning ?? DEFAULT_BROWSER_CONFIG.attachRunning,
89
92
  browserTabRef: config?.browserTabRef ?? DEFAULT_BROWSER_CONFIG.browserTabRef,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ask-pro",
3
- "version": "0.2.80",
3
+ "version": "0.2.87",
4
4
  "description": "Browser-backed ChatGPT Pro escalation for hard engineering questions.",
5
5
  "keywords": [
6
6
  "agents",