ask-pro 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 (55) hide show
  1. package/.codex-plugin/plugin.json +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +231 -0
  4. package/assets/ask-pro_logo.png +0 -0
  5. package/dist/bin/ask-pro-cli.js +507 -0
  6. package/dist/scripts/run-cli.js +27 -0
  7. package/dist/src/ask-pro/atomicWrite.js +26 -0
  8. package/dist/src/ask-pro/browserRunner.js +796 -0
  9. package/dist/src/ask-pro/responseZip.js +349 -0
  10. package/dist/src/ask-pro/session.js +662 -0
  11. package/dist/src/ask-pro/sessionControllerLease.js +64 -0
  12. package/dist/src/ask-pro/toon.js +26 -0
  13. package/dist/src/ask-pro/zip.js +85 -0
  14. package/dist/src/browser/actions/assistantResponse.js +1245 -0
  15. package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
  16. package/dist/src/browser/actions/attachments.js +1720 -0
  17. package/dist/src/browser/actions/composerSendReadiness.js +369 -0
  18. package/dist/src/browser/actions/domEvents.js +31 -0
  19. package/dist/src/browser/actions/inputGuard.js +52 -0
  20. package/dist/src/browser/actions/modelPickerDom.js +68 -0
  21. package/dist/src/browser/actions/modelSelection.js +576 -0
  22. package/dist/src/browser/actions/navigation.js +510 -0
  23. package/dist/src/browser/actions/promptComposer.js +824 -0
  24. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  25. package/dist/src/browser/actions/thinkingStatus.js +408 -0
  26. package/dist/src/browser/actions/thinkingTime.js +635 -0
  27. package/dist/src/browser/actions/windowState.js +47 -0
  28. package/dist/src/browser/attachRunning.js +31 -0
  29. package/dist/src/browser/chatgptModelCatalog.js +321 -0
  30. package/dist/src/browser/chromeLifecycle.js +807 -0
  31. package/dist/src/browser/config.js +110 -0
  32. package/dist/src/browser/constants.js +85 -0
  33. package/dist/src/browser/cookies.js +191 -0
  34. package/dist/src/browser/detect.js +337 -0
  35. package/dist/src/browser/domDebug.js +72 -0
  36. package/dist/src/browser/errors.js +20 -0
  37. package/dist/src/browser/format.js +16 -0
  38. package/dist/src/browser/index.js +2631 -0
  39. package/dist/src/browser/language.js +97 -0
  40. package/dist/src/browser/liveTabs.js +434 -0
  41. package/dist/src/browser/modelStrategy.js +13 -0
  42. package/dist/src/browser/pageActions.js +5 -0
  43. package/dist/src/browser/profilePaths.js +282 -0
  44. package/dist/src/browser/profileState.js +413 -0
  45. package/dist/src/browser/providerDomFlow.js +17 -0
  46. package/dist/src/browser/providers/chatgptDomProvider.js +50 -0
  47. package/dist/src/browser/reattach.js +534 -0
  48. package/dist/src/browser/reattachHelpers.js +387 -0
  49. package/dist/src/browser/utils.js +122 -0
  50. package/dist/src/browserMode.js +1 -0
  51. package/dist/src/version.js +39 -0
  52. package/package.json +114 -0
  53. package/scripts/refresh-local-plugin.mjs +179 -0
  54. package/scripts/refresh-local-plugin.ps1 +93 -0
  55. package/skills/ask-pro/SKILL.md +181 -0
@@ -0,0 +1,337 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { Launcher } from "chrome-launcher";
5
+ export async function detectChromeBinary() {
6
+ const envPath = (process.env.CHROME_PATH ?? "").trim();
7
+ if (envPath) {
8
+ const ok = await isExecutable(envPath);
9
+ if (ok) {
10
+ return { path: envPath };
11
+ }
12
+ }
13
+ const launcherDetected = Launcher.getFirstInstallation();
14
+ if (launcherDetected) {
15
+ return { path: launcherDetected };
16
+ }
17
+ const candidates = platformChromeCandidates(process.platform, os.homedir());
18
+ for (const candidate of candidates.absolutePaths) {
19
+ if (await isExecutable(candidate)) {
20
+ return { path: candidate };
21
+ }
22
+ }
23
+ const fromPath = await findOnPath(candidates.binaryNames);
24
+ if (fromPath) {
25
+ return { path: fromPath };
26
+ }
27
+ return { path: null };
28
+ }
29
+ export async function detectChromeCookieDb({ profile, }) {
30
+ const profileName = profile?.trim() ? profile.trim() : "Default";
31
+ if (process.platform === "win32") {
32
+ return null;
33
+ }
34
+ const roots = resolveAttachRunningProfileRoots();
35
+ for (const root of roots) {
36
+ const dir = path.join(root.root, profileName);
37
+ const direct = path.join(dir, "Cookies");
38
+ if (await isFile(direct))
39
+ return direct;
40
+ const network = path.join(dir, "Network", "Cookies");
41
+ if (await isFile(network))
42
+ return network;
43
+ }
44
+ return null;
45
+ }
46
+ export function resolveAttachRunningProfileRoots(platform = process.platform, homeDir = os.homedir()) {
47
+ if (platform === "darwin") {
48
+ return [
49
+ {
50
+ family: "chrome",
51
+ root: path.join(homeDir, "Library", "Application Support", "Google", "Chrome"),
52
+ },
53
+ {
54
+ family: "chromium",
55
+ root: path.join(homeDir, "Library", "Application Support", "Chromium"),
56
+ },
57
+ {
58
+ family: "edge",
59
+ root: path.join(homeDir, "Library", "Application Support", "Microsoft Edge"),
60
+ },
61
+ {
62
+ family: "brave",
63
+ root: path.join(homeDir, "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
64
+ },
65
+ ];
66
+ }
67
+ if (platform === "linux") {
68
+ return [
69
+ { family: "chrome", root: path.join(homeDir, ".config", "google-chrome") },
70
+ { family: "chromium", root: path.join(homeDir, ".config", "chromium") },
71
+ { family: "edge", root: path.join(homeDir, ".config", "microsoft-edge") },
72
+ {
73
+ family: "brave",
74
+ root: path.join(homeDir, ".config", "BraveSoftware", "Brave-Browser"),
75
+ },
76
+ { family: "chromium", root: path.join(homeDir, "snap", "chromium", "common", "chromium") },
77
+ { family: "chromium", root: path.join(homeDir, "snap", "chromium", "current", "chromium") },
78
+ ];
79
+ }
80
+ if (platform === "win32") {
81
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local");
82
+ return [
83
+ {
84
+ family: "chrome",
85
+ root: path.join(localAppData, "Google", "Chrome", "User Data"),
86
+ },
87
+ {
88
+ family: "chromium",
89
+ root: path.join(localAppData, "Chromium", "User Data"),
90
+ },
91
+ {
92
+ family: "edge",
93
+ root: path.join(localAppData, "Microsoft", "Edge", "User Data"),
94
+ },
95
+ {
96
+ family: "brave",
97
+ root: path.join(localAppData, "BraveSoftware", "Brave-Browser", "User Data"),
98
+ },
99
+ ];
100
+ }
101
+ return [];
102
+ }
103
+ export function resolveDevToolsActivePortDiscoveryRoots(platform = process.platform, homeDir = os.homedir()) {
104
+ if (platform === "darwin") {
105
+ return [path.join(homeDir, "Library", "Application Support")];
106
+ }
107
+ if (platform === "linux") {
108
+ return [path.join(homeDir, ".config"), path.join(homeDir, "snap")];
109
+ }
110
+ if (platform === "win32") {
111
+ return [process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local")];
112
+ }
113
+ return [];
114
+ }
115
+ export function inferAttachRunningBrowserFamily(chromePath) {
116
+ const normalized = chromePath?.trim().toLowerCase();
117
+ if (!normalized) {
118
+ return null;
119
+ }
120
+ if (normalized.includes("microsoft edge") || normalized.includes("msedge")) {
121
+ return "edge";
122
+ }
123
+ if (normalized.includes("brave")) {
124
+ return "brave";
125
+ }
126
+ if (normalized.includes("chromium")) {
127
+ return "chromium";
128
+ }
129
+ if (normalized.includes("chrome")) {
130
+ return "chrome";
131
+ }
132
+ return null;
133
+ }
134
+ export function parseDevToolsActivePort(raw, options = {}) {
135
+ const host = formatWebSocketHost(options.host ?? "127.0.0.1");
136
+ const [rawPort, rawBrowserPath] = raw.split(/\r?\n/u);
137
+ const port = Number.parseInt(rawPort?.trim() ?? "", 10);
138
+ if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
139
+ throw new Error("DevToolsActivePort did not contain a valid port.");
140
+ }
141
+ const browserPath = rawBrowserPath?.trim() || "/devtools/browser";
142
+ const normalizedPath = browserPath.startsWith("/") ? browserPath : `/${browserPath}`;
143
+ return {
144
+ port,
145
+ browserWSEndpoint: `ws://${host}:${port}${normalizedPath}`,
146
+ };
147
+ }
148
+ export async function readDevToolsActivePortInfo(profileRoot, options = {}) {
149
+ const candidates = [
150
+ path.join(profileRoot, "DevToolsActivePort"),
151
+ path.join(profileRoot, "Default", "DevToolsActivePort"),
152
+ ];
153
+ for (const candidate of candidates) {
154
+ try {
155
+ const raw = await fs.readFile(candidate, "utf8");
156
+ const parsed = parseDevToolsActivePort(raw, options);
157
+ return { ...parsed, path: candidate };
158
+ }
159
+ catch {
160
+ // ignore missing/unreadable candidates
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+ export async function discoverDevToolsActivePortCandidates(options = {}) {
166
+ const { host, platform = process.platform, homeDir = os.homedir(), maxDepth = 6 } = options;
167
+ const roots = resolveDevToolsActivePortDiscoveryRoots(platform, homeDir);
168
+ const candidates = [];
169
+ const seenPaths = new Set();
170
+ for (const root of roots) {
171
+ await walkForDevToolsActivePort(root, maxDepth, async (candidatePath, stat) => {
172
+ if (seenPaths.has(candidatePath)) {
173
+ return;
174
+ }
175
+ seenPaths.add(candidatePath);
176
+ try {
177
+ const raw = await fs.readFile(candidatePath, "utf8");
178
+ const parsed = parseDevToolsActivePort(raw, { host });
179
+ candidates.push({
180
+ ...parsed,
181
+ path: candidatePath,
182
+ profileRoot: deriveDevToolsProfileRoot(candidatePath),
183
+ mtimeMs: Number(stat.mtimeMs),
184
+ });
185
+ }
186
+ catch {
187
+ // ignore unreadable or malformed DevToolsActivePort files
188
+ }
189
+ });
190
+ }
191
+ return candidates;
192
+ }
193
+ function platformChromeCandidates(platform = process.platform, homeDir = os.homedir()) {
194
+ if (platform === "linux") {
195
+ return {
196
+ binaryNames: [
197
+ "google-chrome",
198
+ "google-chrome-stable",
199
+ "chromium",
200
+ "chromium-browser",
201
+ "brave-browser",
202
+ "microsoft-edge",
203
+ "microsoft-edge-stable",
204
+ ],
205
+ absolutePaths: [
206
+ "/usr/bin/google-chrome",
207
+ "/usr/bin/google-chrome-stable",
208
+ "/usr/bin/google-chrome-beta",
209
+ "/usr/bin/google-chrome-unstable",
210
+ "/usr/bin/chromium",
211
+ "/usr/bin/chromium-browser",
212
+ "/usr/bin/brave-browser",
213
+ "/usr/bin/microsoft-edge",
214
+ "/usr/bin/microsoft-edge-stable",
215
+ "/snap/bin/chromium",
216
+ "/snap/bin/brave",
217
+ "/snap/bin/brave-browser",
218
+ "/snap/bin/microsoft-edge",
219
+ "/opt/google/chrome/chrome",
220
+ ],
221
+ };
222
+ }
223
+ if (platform === "darwin") {
224
+ return {
225
+ binaryNames: [],
226
+ absolutePaths: [
227
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
228
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
229
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
230
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
231
+ ],
232
+ };
233
+ }
234
+ if (platform === "win32") {
235
+ const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
236
+ const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
237
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local");
238
+ return {
239
+ binaryNames: [],
240
+ absolutePaths: [
241
+ path.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
242
+ path.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
243
+ path.join(localAppData, "Google", "Chrome", "Application", "chrome.exe"),
244
+ path.join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
245
+ path.join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
246
+ ],
247
+ };
248
+ }
249
+ return { binaryNames: [], absolutePaths: [] };
250
+ }
251
+ async function isExecutable(candidate) {
252
+ try {
253
+ const stat = await fs.stat(candidate);
254
+ if (!stat.isFile())
255
+ return false;
256
+ if (process.platform === "win32")
257
+ return true;
258
+ // eslint-disable-next-line no-bitwise
259
+ return (stat.mode & 0o111) !== 0;
260
+ }
261
+ catch {
262
+ return false;
263
+ }
264
+ }
265
+ async function isFile(candidate) {
266
+ try {
267
+ const stat = await fs.stat(candidate);
268
+ return stat.isFile();
269
+ }
270
+ catch {
271
+ return false;
272
+ }
273
+ }
274
+ async function findOnPath(names) {
275
+ const rawPath = process.env.PATH ?? "";
276
+ const dirs = rawPath.split(path.delimiter).filter(Boolean);
277
+ for (const name of names) {
278
+ for (const dir of dirs) {
279
+ const candidate = path.join(dir, name);
280
+ if (await isExecutable(candidate)) {
281
+ return candidate;
282
+ }
283
+ }
284
+ }
285
+ return null;
286
+ }
287
+ function deriveDevToolsProfileRoot(activePortPath) {
288
+ const parentDir = path.dirname(activePortPath);
289
+ if (path.basename(parentDir).toLowerCase() === "default") {
290
+ return path.dirname(parentDir);
291
+ }
292
+ return parentDir;
293
+ }
294
+ function formatWebSocketHost(host) {
295
+ if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) {
296
+ return `[${host}]`;
297
+ }
298
+ return host;
299
+ }
300
+ async function walkForDevToolsActivePort(root, maxDepth, onFile) {
301
+ const stack = [{ dir: root, depth: 0 }];
302
+ while (stack.length > 0) {
303
+ const current = stack.pop();
304
+ if (!current) {
305
+ continue;
306
+ }
307
+ let entries;
308
+ try {
309
+ entries = await fs.readdir(current.dir, { withFileTypes: true });
310
+ }
311
+ catch {
312
+ continue;
313
+ }
314
+ for (const entry of entries) {
315
+ const candidatePath = path.join(current.dir, entry.name);
316
+ if (entry.isSymbolicLink()) {
317
+ continue;
318
+ }
319
+ if (entry.isFile()) {
320
+ if (entry.name !== "DevToolsActivePort") {
321
+ continue;
322
+ }
323
+ try {
324
+ const stat = await fs.stat(candidatePath);
325
+ await onFile(candidatePath, stat);
326
+ }
327
+ catch {
328
+ // ignore unreadable candidates
329
+ }
330
+ continue;
331
+ }
332
+ if (entry.isDirectory() && current.depth < maxDepth) {
333
+ stack.push({ dir: candidatePath, depth: current.depth + 1 });
334
+ }
335
+ }
336
+ }
337
+ }
@@ -0,0 +1,72 @@
1
+ const MAX_CONTROLS = 32;
2
+ const MAX_JSON_BYTES = 8 * 1024;
3
+ export function buildDomControlInventoryExpression() {
4
+ return `(() => {
5
+ const selector = [
6
+ 'button', 'input', 'select', 'textarea',
7
+ '[role="button"]', '[role="combobox"]', '[role="dialog"]', '[role="listbox"]',
8
+ '[role="menu"]', '[role="menuitem"]', '[role="option"]', '[role="radio"]',
9
+ '[role="switch"]', '[role="tab"]', '[aria-expanded]', '[aria-haspopup]'
10
+ ].join(',');
11
+ const allowed = (value, values) => values.includes(value) ? value : null;
12
+ const state = (node, name) => allowed(node.getAttribute(name), ['true', 'false', 'mixed']);
13
+ const nodes = Array.from(document.querySelectorAll(selector));
14
+ const controls = nodes.map((node, index) => {
15
+ const style = getComputedStyle(node);
16
+ const visible = node.getClientRects().length > 0 && style.display !== 'none' && style.visibility !== 'hidden';
17
+ const focused = document.activeElement === node;
18
+ const inOverlay = Boolean(node.closest('[role="dialog"], dialog, [aria-modal="true"]'));
19
+ let depth = 0;
20
+ for (let parent = node.parentElement; parent && depth < 12; parent = parent.parentElement) depth += 1;
21
+ const control = {
22
+ index,
23
+ tag: allowed(node.tagName.toLowerCase(), ['button', 'input', 'select', 'textarea']) ?? 'other',
24
+ role: allowed(node.getAttribute('role'), ['button', 'combobox', 'dialog', 'listbox', 'menu', 'menuitem', 'option', 'radio', 'switch', 'tab']),
25
+ type: allowed(node.getAttribute('type'), ['button', 'checkbox', 'file', 'radio', 'reset', 'submit', 'text']),
26
+ visible,
27
+ disabled: node.disabled === true || node.getAttribute('aria-disabled') === 'true',
28
+ focused,
29
+ inOverlay,
30
+ expanded: state(node, 'aria-expanded'),
31
+ pressed: state(node, 'aria-pressed'),
32
+ checked: state(node, 'aria-checked'),
33
+ selected: state(node, 'aria-selected'),
34
+ popup: allowed(node.getAttribute('aria-haspopup'), ['true', 'false', 'menu', 'listbox', 'tree', 'grid', 'dialog']),
35
+ modal: state(node, 'aria-modal'),
36
+ depth,
37
+ childControls: Math.min(node.querySelectorAll(selector).length, ${MAX_CONTROLS}),
38
+ };
39
+ return { control, score: (focused ? 4 : 0) + (inOverlay ? 2 : 0) + (visible ? 1 : 0) };
40
+ }).sort((a, b) => b.score - a.score || a.control.index - b.control.index)
41
+ .slice(0, ${MAX_CONTROLS}).map(({ control }) => control);
42
+ return { matchedControls: nodes.length, truncated: nodes.length > ${MAX_CONTROLS}, controls };
43
+ })()`;
44
+ }
45
+ export async function logDomFailure(Runtime, logger, context) {
46
+ if (!logger?.verbose)
47
+ return;
48
+ try {
49
+ const { result } = await Runtime.evaluate({
50
+ expression: buildDomControlInventoryExpression(),
51
+ returnByValue: true,
52
+ });
53
+ const value = result.value;
54
+ const inventory = {
55
+ context: context.slice(0, 64),
56
+ matchedControls: Number.isSafeInteger(value?.matchedControls) ? value.matchedControls : 0,
57
+ truncated: value?.truncated === true,
58
+ controls: Array.isArray(value?.controls) ? value.controls.slice(0, MAX_CONTROLS) : [],
59
+ };
60
+ let json = JSON.stringify(inventory);
61
+ while (new TextEncoder().encode(json).byteLength > MAX_JSON_BYTES &&
62
+ inventory.controls.length) {
63
+ inventory.controls.pop();
64
+ inventory.truncated = true;
65
+ json = JSON.stringify(inventory);
66
+ }
67
+ logger(json);
68
+ }
69
+ catch {
70
+ // Diagnostics must not replace the original browser failure.
71
+ }
72
+ }
@@ -0,0 +1,20 @@
1
+ export class AssistantStoppedError extends Error {
2
+ turnIndex;
3
+ constructor(turnIndex) {
4
+ super("ChatGPT stopped without an answer. Choose another resume or a full retry.");
5
+ this.turnIndex = turnIndex;
6
+ this.name = "AssistantStoppedError";
7
+ }
8
+ }
9
+ export class BrowserAutomationError extends Error {
10
+ category = "browser-automation";
11
+ details;
12
+ constructor(message, details, cause) {
13
+ super(message);
14
+ this.name = "BrowserAutomationError";
15
+ this.details = details;
16
+ if (cause) {
17
+ this.cause = cause;
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,16 @@
1
+ export function formatElapsed(ms) {
2
+ if (ms >= 60 * 60 * 1000) {
3
+ const hours = Math.floor(ms / (60 * 60 * 1000));
4
+ const minutes = Math.floor((ms % (60 * 60 * 1000)) / (60 * 1000));
5
+ return `${hours}h ${minutes}m`;
6
+ }
7
+ if (ms >= 60 * 1000) {
8
+ const minutes = Math.floor(ms / (60 * 1000));
9
+ const seconds = Math.floor((ms % (60 * 1000)) / 1000);
10
+ return `${minutes}m ${seconds}s`;
11
+ }
12
+ if (ms >= 1000) {
13
+ return `${Math.floor(ms / 1000)}s`;
14
+ }
15
+ return `${Math.round(ms)}ms`;
16
+ }