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,349 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import zlib from "node:zlib";
4
+ import { atomicWriteFile } from "./atomicWrite.js";
5
+ const REQUIRED_RESPONSE_FILES = [
6
+ "IMPLEMENTATION_PLAN.md",
7
+ "TASKS.json",
8
+ "TEST_PLAN.md",
9
+ "RISK_REGISTER.md",
10
+ "FILES_TO_EDIT.md",
11
+ "REPO_CONTEXT_USED.md",
12
+ ];
13
+ export async function harvestLatestAssistantZip({ runtime, page, input, sessionDir, }) {
14
+ const downloadsDir = path.join(sessionDir, "downloads");
15
+ await fs.mkdir(downloadsDir, { recursive: true });
16
+ const artifact = await fetchLatestAssistantZip(runtime);
17
+ if (artifact.status !== "downloaded") {
18
+ const clicked = await harvestAssistantZipDownloadButton({ runtime, page, input, sessionDir });
19
+ return clicked ?? processResponseZip({ sessionDir, notes: artifact.notes });
20
+ }
21
+ const fileName = sanitizeZipFileName(artifact.fileName);
22
+ const downloadPath = path.join(downloadsDir, fileName);
23
+ await fs.writeFile(downloadPath, Buffer.from(artifact.base64, "base64"));
24
+ return processResponseZip({ sessionDir, preferredZipPath: downloadPath, notes: artifact.notes });
25
+ }
26
+ export async function harvestAssistantZipDownloadButton({ runtime, page, input, sessionDir, }) {
27
+ if (!page)
28
+ return null;
29
+ const downloadsDir = path.join(sessionDir, "downloads");
30
+ await fs.mkdir(downloadsDir, { recursive: true });
31
+ const pageWithDownloads = page;
32
+ if (typeof pageWithDownloads.setDownloadBehavior !== "function") {
33
+ return null;
34
+ }
35
+ await pageWithDownloads.setDownloadBehavior({ behavior: "allow", downloadPath: downloadsDir });
36
+ const { result } = await runtime.evaluate({
37
+ expression: buildFindAssistantZipButtonExpression(),
38
+ awaitPromise: true,
39
+ returnByValue: true,
40
+ });
41
+ const value = result?.value;
42
+ if (!value?.found) {
43
+ return null;
44
+ }
45
+ const notes = value.notes ?? [
46
+ `Clicked response zip download button: ${(value.text ?? "download").slice(0, 120)}`,
47
+ ];
48
+ if (input &&
49
+ typeof value.x === "number" &&
50
+ Number.isFinite(value.x) &&
51
+ typeof value.y === "number" &&
52
+ Number.isFinite(value.y)) {
53
+ await input.dispatchMouseEvent({ type: "mouseMoved", x: value.x, y: value.y });
54
+ await input.dispatchMouseEvent({
55
+ type: "mousePressed",
56
+ x: value.x,
57
+ y: value.y,
58
+ button: "left",
59
+ clickCount: 1,
60
+ });
61
+ await input.dispatchMouseEvent({
62
+ type: "mouseReleased",
63
+ x: value.x,
64
+ y: value.y,
65
+ button: "left",
66
+ clickCount: 1,
67
+ });
68
+ }
69
+ else {
70
+ await runtime.evaluate({
71
+ expression: buildDispatchAssistantZipButtonClickExpression(),
72
+ awaitPromise: true,
73
+ returnByValue: true,
74
+ });
75
+ }
76
+ const zipPath = await waitForDownloadedZip(downloadsDir, 20_000);
77
+ if (!zipPath) {
78
+ return processResponseZip({
79
+ sessionDir,
80
+ notes,
81
+ });
82
+ }
83
+ return processResponseZip({
84
+ sessionDir,
85
+ preferredZipPath: zipPath,
86
+ notes,
87
+ });
88
+ }
89
+ export async function processResponseZip({ sessionDir, preferredZipPath, notes = [], }) {
90
+ const downloadsDir = path.join(sessionDir, "downloads");
91
+ const zipPath = preferredZipPath ?? (await findPreferredZip(downloadsDir));
92
+ if (!zipPath) {
93
+ return {
94
+ schemaVersion: 1,
95
+ responseZip: {
96
+ status: "unavailable",
97
+ actualFileName: null,
98
+ downloadPath: null,
99
+ extractPath: null,
100
+ requiredFilesPresent: false,
101
+ notes: notes.length ? notes : ["No generated response zip was found."],
102
+ },
103
+ };
104
+ }
105
+ const extractPath = path.join(sessionDir, "pro-output");
106
+ try {
107
+ const entries = await readZipEntries(zipPath);
108
+ await fs.rm(extractPath, { recursive: true, force: true });
109
+ await fs.mkdir(extractPath, { recursive: true });
110
+ for (const entry of entries) {
111
+ if (entry.name.endsWith("/"))
112
+ continue;
113
+ const target = safeExtractPath(extractPath, entry.name);
114
+ await fs.mkdir(path.dirname(target), { recursive: true });
115
+ await fs.writeFile(target, entry.data);
116
+ }
117
+ const names = new Set(entries.map((entry) => normalizeZipPath(entry.name)));
118
+ const requiredFilesPresent = REQUIRED_RESPONSE_FILES.every((name) => names.has(name));
119
+ return {
120
+ schemaVersion: 1,
121
+ responseZip: {
122
+ status: requiredFilesPresent ? "downloaded" : "invalid",
123
+ actualFileName: path.basename(zipPath),
124
+ downloadPath: zipPath,
125
+ extractPath,
126
+ requiredFilesPresent,
127
+ notes: requiredFilesPresent
128
+ ? notes
129
+ : [...notes, "Response zip is missing one or more required files."],
130
+ },
131
+ };
132
+ }
133
+ catch (error) {
134
+ return {
135
+ schemaVersion: 1,
136
+ responseZip: {
137
+ status: "error",
138
+ actualFileName: path.basename(zipPath),
139
+ downloadPath: zipPath,
140
+ extractPath,
141
+ requiredFilesPresent: false,
142
+ notes: [...notes, error instanceof Error ? error.message : String(error)],
143
+ },
144
+ };
145
+ }
146
+ }
147
+ export async function writeResponseZipManifest(sessionDir, manifest) {
148
+ await atomicWriteFile(path.join(sessionDir, "PRO_OUTPUT_MANIFEST.json"), `${JSON.stringify(manifest, null, 2)}\n`);
149
+ }
150
+ async function fetchLatestAssistantZip(runtime) {
151
+ const { result } = await runtime.evaluate({
152
+ expression: buildFetchLatestAssistantZipExpression(),
153
+ awaitPromise: true,
154
+ returnByValue: true,
155
+ });
156
+ const value = result?.value;
157
+ const notes = Array.isArray(value?.notes) ? value.notes : [];
158
+ if (value?.status === "downloaded" && value.fileName && value.base64) {
159
+ return { status: "downloaded", fileName: value.fileName, base64: value.base64, notes };
160
+ }
161
+ return { status: "unavailable", notes: notes.length ? notes : ["No zip link found."] };
162
+ }
163
+ function buildFetchLatestAssistantZipExpression() {
164
+ return `(async () => {
165
+ const notes = [];
166
+ const assistantTurns = Array.from(document.querySelectorAll(
167
+ '[data-message-author-role="assistant"], [data-testid*="conversation-turn"][data-message-author-role="assistant"], article'
168
+ ));
169
+ const scope = assistantTurns.length ? assistantTurns[assistantTurns.length - 1] : document;
170
+ const anchors = Array.from(scope.querySelectorAll('a[href], a[download]'));
171
+ const candidates = anchors.map((anchor) => {
172
+ const href = anchor.href || anchor.getAttribute('href') || '';
173
+ const download = anchor.getAttribute('download') || '';
174
+ const text = anchor.textContent || '';
175
+ const name = download || text || href.split('/').pop() || 'ask-pro-response.zip';
176
+ return { href, name, text: [download, text, href].join(' ') };
177
+ }).filter((candidate) => /\\.zip(?:$|[?#])/i.test(candidate.href) || /\\.zip\\b/i.test(candidate.text));
178
+ candidates.sort((a, b) => {
179
+ const score = (candidate) => /ask-pro|response|implementation|plan/i.test(candidate.text) ? 0 : 1;
180
+ return score(a) - score(b);
181
+ });
182
+ const candidate = candidates[0];
183
+ if (!candidate?.href) {
184
+ return { status: 'unavailable', notes: ['No zip link found in latest assistant response.'] };
185
+ }
186
+ const response = await fetch(candidate.href, { credentials: 'include' });
187
+ if (!response.ok) {
188
+ return { status: 'unavailable', notes: ['Zip link fetch failed with HTTP ' + response.status + '.'] };
189
+ }
190
+ const blob = await response.blob();
191
+ const bytes = new Uint8Array(await blob.arrayBuffer());
192
+ let binary = '';
193
+ const chunkSize = 0x8000;
194
+ for (let index = 0; index < bytes.length; index += chunkSize) {
195
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
196
+ }
197
+ return {
198
+ status: 'downloaded',
199
+ fileName: candidate.name || 'ask-pro-response.zip',
200
+ base64: btoa(binary),
201
+ notes,
202
+ };
203
+ })()`;
204
+ }
205
+ function buildFindAssistantZipButtonExpression() {
206
+ return `(() => {
207
+ const assistantTurns = Array.from(document.querySelectorAll(
208
+ '[data-message-author-role="assistant"], [data-testid*="conversation-turn"][data-message-author-role="assistant"], article'
209
+ ));
210
+ const scope = assistantTurns.length ? assistantTurns[assistantTurns.length - 1] : document;
211
+ const candidates = Array.from(scope.querySelectorAll('button,[role="button"],a,span'))
212
+ .map((node) => {
213
+ const text = (node.innerText || node.textContent || '').trim();
214
+ const clickable = node.closest('button,[role="button"],a') || node;
215
+ return { node, clickable, text };
216
+ })
217
+ .filter((candidate) => /\\.zip\\b/i.test(candidate.text));
218
+ const candidate =
219
+ candidates.find((item) => /download|ask-pro|response/i.test(item.text)) || candidates[0];
220
+ if (!candidate) {
221
+ return { found: false, notes: ['No response zip download button found.'] };
222
+ }
223
+ candidate.clickable.scrollIntoView({ block: 'center' });
224
+ const rect = candidate.clickable.getBoundingClientRect();
225
+ return {
226
+ found: true,
227
+ text: candidate.text,
228
+ x: rect.left + rect.width / 2,
229
+ y: rect.top + rect.height / 2,
230
+ notes: ['Clicked response zip download button: ' + candidate.text.slice(0, 120)],
231
+ };
232
+ })()`;
233
+ }
234
+ function buildDispatchAssistantZipButtonClickExpression() {
235
+ return `(() => {
236
+ const assistantTurns = Array.from(document.querySelectorAll(
237
+ '[data-message-author-role="assistant"], [data-testid*="conversation-turn"][data-message-author-role="assistant"], article'
238
+ ));
239
+ const scope = assistantTurns.length ? assistantTurns[assistantTurns.length - 1] : document;
240
+ const candidates = Array.from(scope.querySelectorAll('button,[role="button"],a,span'))
241
+ .map((node) => {
242
+ const text = (node.innerText || node.textContent || '').trim();
243
+ const clickable = node.closest('button,[role="button"],a') || node;
244
+ return { clickable, text };
245
+ })
246
+ .filter((candidate) => /\\.zip\\b/i.test(candidate.text));
247
+ const candidate =
248
+ candidates.find((item) => /download|ask-pro|response/i.test(item.text)) || candidates[0];
249
+ if (!candidate) return false;
250
+ candidate.clickable.dispatchEvent(
251
+ new MouseEvent('click', { bubbles: true, cancelable: true, view: window }),
252
+ );
253
+ return true;
254
+ })()`;
255
+ }
256
+ async function waitForDownloadedZip(downloadsDir, timeoutMs) {
257
+ const deadline = Date.now() + timeoutMs;
258
+ while (Date.now() < deadline) {
259
+ const zip = await findPreferredZip(downloadsDir);
260
+ if (zip)
261
+ return zip;
262
+ await new Promise((resolve) => setTimeout(resolve, 300));
263
+ }
264
+ return null;
265
+ }
266
+ async function findPreferredZip(downloadsDir) {
267
+ let entries;
268
+ try {
269
+ entries = await fs.readdir(downloadsDir);
270
+ }
271
+ catch {
272
+ return null;
273
+ }
274
+ const zips = entries
275
+ .filter((entry) => entry.toLowerCase().endsWith(".zip"))
276
+ .sort((a, b) => {
277
+ const score = (value) => /ask-pro|response|implementation|plan/i.test(value) ? 0 : 1;
278
+ return score(a) - score(b) || a.localeCompare(b);
279
+ });
280
+ return zips[0] ? path.join(downloadsDir, zips[0]) : null;
281
+ }
282
+ async function readZipEntries(zipPath) {
283
+ const buffer = await fs.readFile(zipPath);
284
+ if (buffer.length < 4 || buffer.readUInt32LE(0) !== 0x04034b50) {
285
+ throw new Error("Response file is not a zip archive.");
286
+ }
287
+ const centralDirectory = findCentralDirectory(buffer);
288
+ const entries = [];
289
+ let offset = centralDirectory.offset;
290
+ for (let index = 0; index < centralDirectory.totalEntries; index += 1) {
291
+ if (buffer.readUInt32LE(offset) !== 0x02014b50) {
292
+ throw new Error("Invalid zip central directory.");
293
+ }
294
+ const method = buffer.readUInt16LE(offset + 10);
295
+ const compressedSize = buffer.readUInt32LE(offset + 20);
296
+ const nameLength = buffer.readUInt16LE(offset + 28);
297
+ const extraLength = buffer.readUInt16LE(offset + 30);
298
+ const commentLength = buffer.readUInt16LE(offset + 32);
299
+ const localHeaderOffset = buffer.readUInt32LE(offset + 42);
300
+ const name = normalizeZipPath(buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8"));
301
+ entries.push({
302
+ name,
303
+ data: inflateZipEntry(buffer, localHeaderOffset, compressedSize, method),
304
+ });
305
+ offset += 46 + nameLength + extraLength + commentLength;
306
+ }
307
+ return entries;
308
+ }
309
+ function findCentralDirectory(buffer) {
310
+ const minOffset = Math.max(0, buffer.length - 0xffff - 22);
311
+ for (let offset = buffer.length - 22; offset >= minOffset; offset -= 1) {
312
+ if (buffer.readUInt32LE(offset) === 0x06054b50) {
313
+ return {
314
+ totalEntries: buffer.readUInt16LE(offset + 10),
315
+ offset: buffer.readUInt32LE(offset + 16),
316
+ };
317
+ }
318
+ }
319
+ throw new Error("Zip end-of-central-directory record was not found.");
320
+ }
321
+ function inflateZipEntry(buffer, localHeaderOffset, compressedSize, method) {
322
+ if (buffer.readUInt32LE(localHeaderOffset) !== 0x04034b50) {
323
+ throw new Error("Invalid zip local header.");
324
+ }
325
+ const nameLength = buffer.readUInt16LE(localHeaderOffset + 26);
326
+ const extraLength = buffer.readUInt16LE(localHeaderOffset + 28);
327
+ const dataStart = localHeaderOffset + 30 + nameLength + extraLength;
328
+ const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
329
+ if (method === 0)
330
+ return Buffer.from(compressed);
331
+ if (method === 8)
332
+ return zlib.inflateRawSync(compressed);
333
+ throw new Error(`Unsupported zip compression method ${method}.`);
334
+ }
335
+ function safeExtractPath(root, zipEntryName) {
336
+ const target = path.resolve(root, normalizeZipPath(zipEntryName));
337
+ const rootWithSeparator = path.resolve(root) + path.sep;
338
+ if (target !== path.resolve(root) && !target.startsWith(rootWithSeparator)) {
339
+ throw new Error(`Unsafe zip entry path: ${zipEntryName}`);
340
+ }
341
+ return target;
342
+ }
343
+ function normalizeZipPath(value) {
344
+ return value.replace(/\\/g, "/").replace(/^\/+/, "");
345
+ }
346
+ function sanitizeZipFileName(value) {
347
+ const base = path.basename(value).replace(/[^a-zA-Z0-9._-]+/g, "-");
348
+ return base.toLowerCase().endsWith(".zip") ? base : "ask-pro-response.zip";
349
+ }