ask-pro 0.2.87 → 0.2.93
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/SKILL.md +3 -1
- package/dist/src/ask-pro/loginPage.js +3 -2
- package/dist/src/ask-pro/remoteLogin.js +26 -2
- package/dist/src/browser/chromeLifecycle.js +5 -1
- package/dist/src/browser/config.js +1 -1
- package/dist/src/browser/index.js +2 -2
- package/dist/src/browser/linuxChrome.js +82 -0
- package/package.json +2 -1
package/SKILL.md
CHANGED
|
@@ -11,7 +11,9 @@ 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
|
|
14
|
+
On a display-less Linux host, install Xvfb (`sudo apt install xvfb`); ask-pro
|
|
15
|
+
manages a private display for headed Chrome during login and queries. Set
|
|
16
|
+
`CHROME_PATH` if Chrome is not discovered, and
|
|
15
17
|
`ASK_PRO_LOGIN_HOST` to the host's private Tailscale IP. Before the first query,
|
|
16
18
|
run `ask-pro --login` and give the human its `login_url`. Keep that command alive
|
|
17
19
|
while they sign in from their desktop or phone. The link expires after 15 minutes
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// The browser relays human input only. Never persist input
|
|
1
|
+
// The browser relays human input only. Never persist input or frames.
|
|
2
|
+
// Keep the short-lived access key in the URL fragment so reload preserves access.
|
|
2
3
|
export const loginPage = `<!doctype html>
|
|
3
4
|
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
4
5
|
<title>ask-pro sign in</title>
|
|
@@ -8,7 +9,7 @@ export const loginPage = `<!doctype html>
|
|
|
8
9
|
<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
10
|
<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
11
|
<script>
|
|
11
|
-
const token=location.hash.slice(1);
|
|
12
|
+
const token=location.hash.slice(1);
|
|
12
13
|
const screen=document.querySelector('#screen'),tabs=document.querySelector('#tabs'),status=document.querySelector('#status'),text=document.querySelector('#text');
|
|
13
14
|
let stopped=false,frameUrl,inputQueue=Promise.resolve();
|
|
14
15
|
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;}
|
|
@@ -80,7 +80,29 @@ export async function startLoginServer(options) {
|
|
|
80
80
|
}
|
|
81
81
|
if (req.method === "GET" && route.pathname === "/frame") {
|
|
82
82
|
const c = await connect(route.searchParams.get("target") ?? "");
|
|
83
|
-
|
|
83
|
+
// Reload cancels the image request. Release its pending CDP capture so
|
|
84
|
+
// it cannot hold the shared request queue and freeze the new viewer.
|
|
85
|
+
const cancel = () => {
|
|
86
|
+
if (!res.writableEnded) {
|
|
87
|
+
if (client === c) {
|
|
88
|
+
client = undefined;
|
|
89
|
+
targetId = undefined;
|
|
90
|
+
}
|
|
91
|
+
void c.close().catch(() => undefined);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
if (res.destroyed) {
|
|
95
|
+
cancel();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
res.once("close", cancel);
|
|
99
|
+
let frame;
|
|
100
|
+
try {
|
|
101
|
+
frame = await c.Page.captureScreenshot({ format: "jpeg", quality: 70 });
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
res.removeListener("close", cancel);
|
|
105
|
+
}
|
|
84
106
|
res.setHeader("Content-Type", "image/jpeg");
|
|
85
107
|
res.end(Buffer.from(frame.data, "base64"));
|
|
86
108
|
return;
|
|
@@ -222,7 +244,9 @@ export async function runRemoteLogin(onReady, savedProfile) {
|
|
|
222
244
|
if (peers !== 0)
|
|
223
245
|
throw Error("Browser busy. Wait for ask-pro runs to finish before --login.");
|
|
224
246
|
const chrome = (await maybeReuseRunningChrome(profile, () => { })) ??
|
|
225
|
-
(await launchChrome(resolveBrowserConfig({ manualLogin: true
|
|
247
|
+
(await launchChrome(resolveBrowserConfig({ manualLogin: true }), profile, () => { }, {
|
|
248
|
+
ignoreLockId: lock.lockId,
|
|
249
|
+
}));
|
|
226
250
|
releaseChromeProcessHandle(chrome);
|
|
227
251
|
if (chrome.pid)
|
|
228
252
|
await writeChromePid(profile, chrome.pid);
|
|
@@ -12,6 +12,8 @@ import { clearChromeLaunchStarting, clearIdleBrowserProfileCaches, cleanupStaleP
|
|
|
12
12
|
import { delay } from "./utils.js";
|
|
13
13
|
import { formatElapsed } from "./format.js";
|
|
14
14
|
import { withTimeout } from "./reattachHelpers.js";
|
|
15
|
+
import { isHeadlessLinux } from "./config.js";
|
|
16
|
+
import { spawnChromeWithVirtualDisplay } from "./linuxChrome.js";
|
|
15
17
|
const execFileAsync = promisify(execFile);
|
|
16
18
|
const CPU_THROTTLING_OVERRIDE_FLAGS = new Set([
|
|
17
19
|
"--disable-backgrounding-occluded-windows",
|
|
@@ -747,7 +749,9 @@ export function spawnWindowsProcessOutsideJob(command, args, options) {
|
|
|
747
749
|
}
|
|
748
750
|
const spawnManagedChrome = ((command, args, options) => process.platform === "win32"
|
|
749
751
|
? spawnWindowsProcessOutsideJob(command, args, options)
|
|
750
|
-
:
|
|
752
|
+
: isHeadlessLinux() && !args.some((arg) => arg.startsWith("--headless"))
|
|
753
|
+
? spawnChromeWithVirtualDisplay(command, args, options)
|
|
754
|
+
: spawn(command, args, options));
|
|
751
755
|
async function launchManagedChrome({ chromeFlags, chromePath, userDataDir, host, requestedPort, }) {
|
|
752
756
|
const launcher = new Launcher({
|
|
753
757
|
chromePath: chromePath ?? undefined,
|
|
@@ -79,7 +79,7 @@ export function resolveBrowserConfig(config) {
|
|
|
79
79
|
cookieSyncWaitMs: config?.cookieSyncWaitMs ?? DEFAULT_BROWSER_CONFIG.cookieSyncWaitMs,
|
|
80
80
|
inlineCookies: config?.inlineCookies ?? DEFAULT_BROWSER_CONFIG.inlineCookies,
|
|
81
81
|
inlineCookiesSource: config?.inlineCookiesSource ?? DEFAULT_BROWSER_CONFIG.inlineCookiesSource,
|
|
82
|
-
headless: config?.headless ??
|
|
82
|
+
headless: config?.headless ?? false,
|
|
83
83
|
keepBrowser: config?.keepBrowser ?? DEFAULT_BROWSER_CONFIG.keepBrowser,
|
|
84
84
|
hideWindow: config?.hideWindow ?? DEFAULT_BROWSER_CONFIG.hideWindow,
|
|
85
85
|
startMinimized: config?.startMinimized ?? DEFAULT_BROWSER_CONFIG.startMinimized,
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import CDP from "chrome-remote-interface";
|
|
6
|
-
import { DEFAULT_BROWSER_CONFIG, resolveBrowserConfig } from "./config.js";
|
|
6
|
+
import { DEFAULT_BROWSER_CONFIG, resolveBrowserConfig, isHeadlessLinux } from "./config.js";
|
|
7
7
|
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToRemoteChrome, connectWithNewTab, closeTab, closeRemoteChromeTarget, closeChromeGracefully, listRemoteChromeTargets, maybeReuseRunningChrome, releaseChromeProcessHandle, restoreChromeWindowByPid, shouldLaunchChromeMinimized, } from "./chromeLifecycle.js";
|
|
8
8
|
import { syncCookies } from "./cookies.js";
|
|
9
9
|
import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
|
|
@@ -1346,7 +1346,7 @@ export async function runBrowserMode(options) {
|
|
|
1346
1346
|
? "ChatGPT login required; leaving browser open so you can sign in."
|
|
1347
1347
|
: "Cloudflare challenge detected; leaving browser open so you can complete the check.");
|
|
1348
1348
|
logger(`Reuse this browser profile with: ${reuseProfileHint}`);
|
|
1349
|
-
if (isLoginRequired && manualLogin) {
|
|
1349
|
+
if (isLoginRequired && manualLogin && !isHeadlessLinux()) {
|
|
1350
1350
|
const recoveryRuntime = client?.Runtime;
|
|
1351
1351
|
if (!recoveryRuntime) {
|
|
1352
1352
|
throw normalizedError;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
// A separate supervisor outlives the controller when a browser is kept for resume.
|
|
8
|
+
// Its PID represents the managed browser lifetime, including its private display.
|
|
9
|
+
export function spawnChromeWithVirtualDisplay(command, args, options) {
|
|
10
|
+
const check = spawnSync("Xvfb", ["-help"], { stdio: "ignore" });
|
|
11
|
+
if (check.error)
|
|
12
|
+
throw Error("Xvfb is required on Linux without a display. Run: sudo apt install xvfb");
|
|
13
|
+
return spawn(process.execPath, [fileURLToPath(import.meta.url), "--supervise", command, ...args], options);
|
|
14
|
+
}
|
|
15
|
+
async function supervise() {
|
|
16
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "ask-pro-display-"));
|
|
17
|
+
const authority = path.join(directory, "Xauthority");
|
|
18
|
+
const cookie = randomBytes(16);
|
|
19
|
+
const writeAuthority = async (number) => {
|
|
20
|
+
const fields = [
|
|
21
|
+
Buffer.alloc(0),
|
|
22
|
+
Buffer.from(number),
|
|
23
|
+
Buffer.from("MIT-MAGIC-COOKIE-1"),
|
|
24
|
+
cookie,
|
|
25
|
+
];
|
|
26
|
+
const records = fields.map((field) => {
|
|
27
|
+
const length = Buffer.alloc(2);
|
|
28
|
+
length.writeUInt16BE(field.length);
|
|
29
|
+
return Buffer.concat([length, field]);
|
|
30
|
+
});
|
|
31
|
+
await writeFile(authority, Buffer.concat([Buffer.from([255, 255]), ...records]), {
|
|
32
|
+
mode: 0o600,
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
await writeAuthority("");
|
|
36
|
+
const display = spawn("Xvfb", ["-displayfd", "3", "-screen", "0", "1280x800x24", "-nolisten", "tcp", "-auth", authority], {
|
|
37
|
+
stdio: ["ignore", "ignore", "inherit", "pipe"],
|
|
38
|
+
});
|
|
39
|
+
let chrome;
|
|
40
|
+
const stop = () => {
|
|
41
|
+
if (chrome)
|
|
42
|
+
chrome.kill("SIGTERM");
|
|
43
|
+
else
|
|
44
|
+
display.kill();
|
|
45
|
+
};
|
|
46
|
+
process.on("SIGTERM", stop);
|
|
47
|
+
process.on("SIGINT", stop);
|
|
48
|
+
let timer;
|
|
49
|
+
try {
|
|
50
|
+
const number = await new Promise((resolve, reject) => {
|
|
51
|
+
timer = setTimeout(() => reject(Error("Xvfb did not start within 10 seconds")), 10_000);
|
|
52
|
+
let output = "";
|
|
53
|
+
display.once("error", reject);
|
|
54
|
+
display.once("exit", () => reject(Error("Xvfb exited before Chrome started")));
|
|
55
|
+
display.stdio[3].on("data", (chunk) => {
|
|
56
|
+
output += String(chunk);
|
|
57
|
+
if (/^\d+\n$/.test(output))
|
|
58
|
+
resolve(output.trim());
|
|
59
|
+
});
|
|
60
|
+
}).finally(() => clearTimeout(timer));
|
|
61
|
+
await writeAuthority(number);
|
|
62
|
+
chrome = spawn(process.argv[3], process.argv.slice(4), {
|
|
63
|
+
env: { ...process.env, DISPLAY: `:${number}`, XAUTHORITY: authority },
|
|
64
|
+
stdio: "inherit",
|
|
65
|
+
});
|
|
66
|
+
display.once("exit", () => chrome?.kill("SIGTERM"));
|
|
67
|
+
process.exitCode = await new Promise((resolve, reject) => {
|
|
68
|
+
chrome.once("error", reject);
|
|
69
|
+
chrome.once("exit", (code) => resolve(code ?? 1));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
display.kill();
|
|
74
|
+
await rm(directory, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (process.argv[2] === "--supervise") {
|
|
78
|
+
await supervise().catch((error) => {
|
|
79
|
+
console.error(error.message);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
});
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ask-pro",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.93",
|
|
4
4
|
"description": "Browser-backed ChatGPT Pro escalation for hard engineering questions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"dist/src/browser/attachRunning.js",
|
|
30
30
|
"dist/src/browser/chatgptModelCatalog.js",
|
|
31
31
|
"dist/src/browser/chromeLifecycle.js",
|
|
32
|
+
"dist/src/browser/linuxChrome.js",
|
|
32
33
|
"dist/src/browser/config.js",
|
|
33
34
|
"dist/src/browser/constants.js",
|
|
34
35
|
"dist/src/browser/cookies.js",
|