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,507 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { AssistantStoppedError } from "../src/browser/errors.js";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { Command, Option } from "commander";
7
+ import { createAskProSession, getAskProSessionPaths, pruneExpiredAskProSessions, readAskProAnswer, readAskProStatus, updateAskProResumeCommand, updateAskProStatus, } from "../src/ask-pro/session.js";
8
+ import { AskProNeedsAuthError, isSuspiciousPreambleAnswer, resumeAskProBrowserSession, runAskProBrowserSession, } from "../src/ask-pro/browserRunner.js";
9
+ import { renderToonRecord } from "../src/ask-pro/toon.js";
10
+ import { askProAgentIdForManagedBrowserProfileDir, defaultAskProBrowserProfileDir, } from "../src/browser/profilePaths.js";
11
+ import { getCliVersion } from "../src/version.js";
12
+ const program = new Command();
13
+ program
14
+ .name("ask-pro")
15
+ .description("Browser-backed ChatGPT Pro escalation for hard engineering questions.")
16
+ .version(getCliVersion())
17
+ .argument("[question...]", "question to send to ChatGPT Pro")
18
+ .option("--dry-run", "prepare the session and context bundle without opening the browser")
19
+ .option("--files <pattern>", "include files or globs in the context bundle", collectFiles, [])
20
+ .option("--prompt-file <path>", "read the question from a UTF-8 file; use - for stdin")
21
+ .option("--artifacts", "ask Pro for ask-pro-response.zip plus markdown fallback")
22
+ .option("--response-zip", "alias for --artifacts")
23
+ .option("--resume [session-id]", "resume a prepared or waiting ask-pro session")
24
+ .option("--status [session-id]", "show ask-pro session status")
25
+ .option("--harvest [session-id]", "print harvested ANSWER.md for a session")
26
+ .option("--copy [session-id]", "print the copy target for a session")
27
+ .option("--temporary", "require ChatGPT Temporary Chat; default runs already try it first")
28
+ .option("--no-temporary", "retry a session outside ChatGPT Temporary Chat")
29
+ .addOption(new Option("--cwd <path>", "project working directory").hideHelp())
30
+ .option("--verbose", "print browser automation diagnostics")
31
+ .action(async (questionParts, options) => {
32
+ try {
33
+ await runAskPro(questionParts.join(" "), options);
34
+ }
35
+ catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ writeToon("ask_pro_error", {
38
+ code: classifyCliError(message),
39
+ message,
40
+ action: "inspect_session",
41
+ });
42
+ process.exitCode = 1;
43
+ }
44
+ });
45
+ await program.parseAsync(process.argv);
46
+ async function runAskPro(question, options) {
47
+ const cwd = resolveProjectCwd(options);
48
+ await pruneExpiredAskProSessions({ cwd });
49
+ if (options.status !== undefined) {
50
+ const { status } = await readAskProStatus({ cwd, sessionId: optionSessionId(options.status) });
51
+ printStatusRecord(status, {
52
+ ...(await readBrowserPreflight(cwd, status)),
53
+ ...answerExtraForStatus(status, status.sessionId),
54
+ });
55
+ return;
56
+ }
57
+ if (options.harvest !== undefined) {
58
+ const { status } = await readAskProStatus({
59
+ cwd,
60
+ sessionId: optionSessionId(options.harvest),
61
+ });
62
+ if (!isAnswerBearingStatus(status)) {
63
+ const recoverable = await readRecoverableCapturedAnswer(cwd, status);
64
+ if (recoverable !== null) {
65
+ await writeStdout(recoverable);
66
+ try {
67
+ await updateAskProStatus({ cwd, sessionId: status.sessionId, status: "HARVESTED" });
68
+ }
69
+ catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ console.error(`ask-pro harvest status update failed: ${message}`);
72
+ }
73
+ return;
74
+ }
75
+ printStatusRecord(status, await readBrowserPreflight(cwd, status));
76
+ return;
77
+ }
78
+ const result = await readAskProAnswer({ cwd, sessionId: status.sessionId });
79
+ await writeStdout(result.answer);
80
+ try {
81
+ if (status.status !== "HARVESTED") {
82
+ await updateAskProStatus({ cwd, sessionId: result.sessionId, status: "HARVESTED" });
83
+ }
84
+ }
85
+ catch (error) {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ console.error(`ask-pro harvest status update failed: ${message}`);
88
+ }
89
+ return;
90
+ }
91
+ if (options.copy !== undefined) {
92
+ const { dir, status } = await readAskProStatus({
93
+ cwd,
94
+ sessionId: optionSessionId(options.copy),
95
+ });
96
+ if (isAnswerBearingStatus(status)) {
97
+ writeToon("ask_pro", {
98
+ session: status.sessionId,
99
+ state: normalizeState(status.status),
100
+ target: path.join(dir, "ANSWER.md"),
101
+ action: "copy_target",
102
+ });
103
+ }
104
+ else {
105
+ printStatusRecord(status, await readBrowserPreflight(cwd, status));
106
+ }
107
+ return;
108
+ }
109
+ if (options.resume !== undefined) {
110
+ const { status } = await readAskProStatus({ cwd, sessionId: optionSessionId(options.resume) });
111
+ const effectiveOptions = mergeStatusOptions(options, status);
112
+ const resumeCommand = buildResumeCommand(status.sessionId, effectiveOptions, cwd);
113
+ const harvestCommand = buildHarvestCommand(status.sessionId, cwd);
114
+ if (resumeCommand !== status.resumeCommand || harvestCommand !== status.harvestCommand) {
115
+ await updateAskProResumeCommand({
116
+ cwd,
117
+ sessionId: status.sessionId,
118
+ resumeCommand,
119
+ harvestCommand,
120
+ temporary: effectiveOptions.temporary,
121
+ });
122
+ }
123
+ await submitOrResumeBrowserSession(cwd, status.sessionId, effectiveOptions);
124
+ return;
125
+ }
126
+ const dryRun = options.dryRun === true;
127
+ const resolvedQuestion = await resolveQuestion(question, options, cwd);
128
+ const artifacts = options.artifacts === true || options.responseZip === true;
129
+ const session = await createAskProSession({
130
+ cwd,
131
+ question: resolvedQuestion,
132
+ filePatterns: options.files ?? [],
133
+ dryRun,
134
+ artifacts,
135
+ });
136
+ const resumeCommand = buildResumeCommand(session.id, options, cwd);
137
+ const harvestCommand = buildHarvestCommand(session.id, cwd);
138
+ let currentStatus = session.status;
139
+ if (resumeCommand !== session.status.resumeCommand ||
140
+ harvestCommand !== session.status.harvestCommand) {
141
+ currentStatus = await updateAskProResumeCommand({
142
+ cwd,
143
+ sessionId: session.id,
144
+ resumeCommand,
145
+ harvestCommand,
146
+ temporary: options.temporary,
147
+ });
148
+ }
149
+ if (dryRun) {
150
+ printStatusRecord(currentStatus, { files: session.manifest.includedFiles.length });
151
+ return;
152
+ }
153
+ await submitOrResumeBrowserSession(cwd, session.id, options);
154
+ }
155
+ function collectFiles(value, previous) {
156
+ return previous.concat(value);
157
+ }
158
+ async function resolveQuestion(question, options, cwd) {
159
+ if (!options.promptFile) {
160
+ return question;
161
+ }
162
+ if (question.trim()) {
163
+ throw new Error("Use either a question argument or --prompt-file, not both.");
164
+ }
165
+ if (options.promptFile === "-") {
166
+ if (process.stdin.isTTY) {
167
+ throw new Error("--prompt-file - requires piped stdin.");
168
+ }
169
+ return readStdin();
170
+ }
171
+ return fs.readFile(path.resolve(cwd, options.promptFile), "utf8");
172
+ }
173
+ async function readStdin() {
174
+ const chunks = [];
175
+ for await (const chunk of process.stdin) {
176
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
177
+ }
178
+ return Buffer.concat(chunks).toString("utf8");
179
+ }
180
+ function optionSessionId(value) {
181
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
182
+ }
183
+ async function submitOrResumeBrowserSession(cwd, sessionId, options) {
184
+ const { status } = await readAskProStatus({ cwd, sessionId });
185
+ if (status.status === "COMPLETED" || status.status === "HARVESTED") {
186
+ printStatusRecord(status, {
187
+ ...(await readBrowserPreflight(cwd, status)),
188
+ ...answerExtraForStatus(status, sessionId),
189
+ });
190
+ return;
191
+ }
192
+ if (status.status === "WAITING" ||
193
+ status.status === "WAIT_TIMED_OUT" ||
194
+ status.status === "INCOMPLETE_ANSWER" ||
195
+ status.status === "NEEDS_USER_AUTH") {
196
+ try {
197
+ await resumeAskProBrowserSession({
198
+ cwd,
199
+ sessionId,
200
+ temporary: options.temporary,
201
+ verbose: options.verbose,
202
+ });
203
+ }
204
+ catch (error) {
205
+ if (error instanceof AssistantStoppedError) {
206
+ const { status: stopped } = await readAskProStatus({ cwd, sessionId });
207
+ printStatusRecord(stopped, await readBrowserPreflight(cwd, stopped));
208
+ return;
209
+ }
210
+ if (error instanceof AskProNeedsAuthError) {
211
+ await printAuthInstructions(sessionId, options, cwd, error);
212
+ return;
213
+ }
214
+ throw error;
215
+ }
216
+ const { status: completed } = await readAskProStatus({ cwd, sessionId });
217
+ printStatusRecord(completed, {
218
+ ...(await readBrowserPreflight(cwd, completed)),
219
+ ...answerExtraForStatus(completed, sessionId),
220
+ });
221
+ return;
222
+ }
223
+ try {
224
+ await runAskProBrowserSession({
225
+ cwd,
226
+ sessionId,
227
+ temporary: options.temporary,
228
+ verbose: options.verbose,
229
+ });
230
+ const { status: completed } = await readAskProStatus({ cwd, sessionId });
231
+ printStatusRecord(completed, {
232
+ ...(await readBrowserPreflight(cwd, completed)),
233
+ ...answerExtraForStatus(completed, sessionId),
234
+ });
235
+ }
236
+ catch (error) {
237
+ if (error instanceof AssistantStoppedError) {
238
+ const { status: stopped } = await readAskProStatus({ cwd, sessionId });
239
+ printStatusRecord(stopped, await readBrowserPreflight(cwd, stopped));
240
+ return;
241
+ }
242
+ if (error instanceof AskProNeedsAuthError) {
243
+ await printAuthInstructions(sessionId, options, cwd, error);
244
+ return;
245
+ }
246
+ throw error;
247
+ }
248
+ }
249
+ function buildResumeCommand(sessionId, options, cwd) {
250
+ const flags = [
251
+ options.temporary === true ? "--temporary" : null,
252
+ options.temporary === false ? "--no-temporary" : null,
253
+ ];
254
+ return buildSessionCommand(cwd, [...flags, "--resume", sessionId]);
255
+ }
256
+ function buildHarvestCommand(sessionId, cwd) {
257
+ return buildSessionCommand(cwd, ["--harvest", sessionId]);
258
+ }
259
+ function buildSessionCommand(cwd, args) {
260
+ const launcher = buildLauncherCommand();
261
+ const flags = [
262
+ needsExplicitCwd(launcher) ? "--cwd" : null,
263
+ needsExplicitCwd(launcher) ? quoteCommandArg(cwd) : null,
264
+ ...args,
265
+ ].filter(Boolean);
266
+ return `${launcher} ${flags.join(" ")}`;
267
+ }
268
+ function buildLauncherCommand() {
269
+ const sourceLauncher = process.env.ASK_PRO_SOURCE_CHECKOUT_LAUNCHER?.trim();
270
+ if (sourceLauncher) {
271
+ return sourceLauncher;
272
+ }
273
+ return "ask-pro";
274
+ }
275
+ function needsExplicitCwd(launcher) {
276
+ return launcher !== "ask-pro";
277
+ }
278
+ function resolveProjectCwd(options) {
279
+ if (options.cwd) {
280
+ return path.resolve(options.cwd);
281
+ }
282
+ if (process.env.ASK_PRO_SOURCE_CHECKOUT_LAUNCHER && process.env.INIT_CWD) {
283
+ return path.resolve(process.env.INIT_CWD);
284
+ }
285
+ return process.cwd();
286
+ }
287
+ async function printAuthInstructions(sessionId, options, cwd, error) {
288
+ const resumeCommand = buildResumeCommand(sessionId, options, cwd);
289
+ const fallbackPreflight = {
290
+ profile: profileMode(error.browserProfile),
291
+ profile_path: collapseHome(error.browserProfile),
292
+ };
293
+ const browserPreflight = await readBrowserPreflightForSession(cwd, sessionId);
294
+ writeToon("ask_pro", {
295
+ session: sessionId,
296
+ state: "needs_auth",
297
+ reason: error.reason,
298
+ ...fallbackPreflight,
299
+ ...browserPreflight,
300
+ action: "human_login_then_resume",
301
+ resume: resumeCommand,
302
+ });
303
+ }
304
+ function quoteCommandArg(value) {
305
+ if (process.platform !== "win32") {
306
+ return `'${value.replace(/'/g, "'\\''")}'`;
307
+ }
308
+ return `"${value.replace(/"/g, '""')}"`;
309
+ }
310
+ function mergeStatusOptions(options, status) {
311
+ const temporary = options.temporary !== undefined ? options.temporary : status.temporary;
312
+ return {
313
+ ...options,
314
+ temporary,
315
+ };
316
+ }
317
+ function printStatusRecord(status, extra = {}) {
318
+ writeToon("ask_pro", {
319
+ session: status.sessionId,
320
+ state: normalizeState(status.status),
321
+ reason: status.reason,
322
+ temporary: normalizeTemporary(status.temporary),
323
+ action: actionForStatus(status),
324
+ resume: shouldPrintResume(status) ? status.resumeCommand : undefined,
325
+ harvest: shouldPrintHarvest(status) ? status.harvestCommand : undefined,
326
+ retry: status.reason === "stopped_without_answer"
327
+ ? "Run the original request again without --resume to start a new chat."
328
+ : undefined,
329
+ ...extra,
330
+ });
331
+ }
332
+ function writeToon(name, fields) {
333
+ process.stdout.write(`${renderToonRecord(name, fields)}\n`);
334
+ }
335
+ async function writeStdout(value) {
336
+ await new Promise((resolve, reject) => {
337
+ process.stdout.write(value, (error) => {
338
+ if (error) {
339
+ reject(error);
340
+ }
341
+ else {
342
+ resolve();
343
+ }
344
+ });
345
+ });
346
+ }
347
+ function normalizeState(status) {
348
+ if (status === "NEEDS_USER_AUTH")
349
+ return "needs_auth";
350
+ return status.toLowerCase();
351
+ }
352
+ function normalizeTemporary(temporary) {
353
+ if (temporary === true)
354
+ return "strict";
355
+ if (temporary === false)
356
+ return "off";
357
+ return "default";
358
+ }
359
+ function actionForStatus(status) {
360
+ if (status.reason === "stopped_without_answer")
361
+ return "choose_resume_or_retry";
362
+ switch (status.status) {
363
+ case "DRY_RUN_COMPLETE":
364
+ case "INCOMPLETE_ANSWER":
365
+ case "READY_TO_SUBMIT":
366
+ case "WAIT_TIMED_OUT":
367
+ case "FAILED":
368
+ return "resume";
369
+ case "NEEDS_USER_AUTH":
370
+ return "human_login_then_resume";
371
+ case "COMPLETED":
372
+ return "harvest";
373
+ case "HARVESTED":
374
+ return "read_answer";
375
+ case "WAITING":
376
+ case "BROWSER_STARTING":
377
+ return "wait";
378
+ }
379
+ }
380
+ function shouldPrintResume(status) {
381
+ return [
382
+ "BROWSER_STARTING",
383
+ "DRY_RUN_COMPLETE",
384
+ "INCOMPLETE_ANSWER",
385
+ "WAITING",
386
+ "READY_TO_SUBMIT",
387
+ "NEEDS_USER_AUTH",
388
+ "WAIT_TIMED_OUT",
389
+ "FAILED",
390
+ ].includes(status.status);
391
+ }
392
+ function shouldPrintHarvest(status) {
393
+ return status.status === "COMPLETED";
394
+ }
395
+ function answerPath(sessionId) {
396
+ return `.ask-pro/sessions/${sessionId}/ANSWER.md`;
397
+ }
398
+ function answerExtraForStatus(status, sessionId) {
399
+ return isAnswerBearingStatus(status) ? { answer: answerPath(sessionId) } : {};
400
+ }
401
+ function isAnswerBearingStatus(status) {
402
+ return ["COMPLETED", "HARVESTED"].includes(status.status);
403
+ }
404
+ async function readRecoverableCapturedAnswer(cwd, status) {
405
+ if (status.status === "INCOMPLETE_ANSWER") {
406
+ return null;
407
+ }
408
+ try {
409
+ const { answer } = await readAskProAnswer({ cwd, sessionId: status.sessionId });
410
+ return isPlaceholderAnswer(answer) || isSuspiciousPreambleAnswer(answer) ? null : answer;
411
+ }
412
+ catch {
413
+ return null;
414
+ }
415
+ }
416
+ function isPlaceholderAnswer(answer) {
417
+ const normalized = answer.trim().toLowerCase();
418
+ return (normalized.length === 0 ||
419
+ normalized === "# dry run\n\nno browser submission was performed." ||
420
+ normalized === "# pending\n\nbrowser submission is not wired in this slice.");
421
+ }
422
+ async function readBrowserPreflight(cwd, status) {
423
+ return readBrowserPreflightForSession(cwd, status.sessionId);
424
+ }
425
+ async function readBrowserPreflightForSession(cwd, sessionId) {
426
+ const metadata = await readBrowserMetadata(cwd, sessionId);
427
+ if (!metadata)
428
+ return {};
429
+ const profileDir = typeof metadata.profileDir === "string" ? metadata.profileDir : undefined;
430
+ const profile = profileMode(profileDir);
431
+ return compactFields({
432
+ profile,
433
+ profile_path: profile && profileDir ? collapseHome(profileDir) : undefined,
434
+ chrome: chromeMode(metadata),
435
+ language: typeof metadata.acceptLanguage === "string" ? metadata.acceptLanguage : undefined,
436
+ conversation_url: recoverableConversationUrl(metadata),
437
+ });
438
+ }
439
+ async function readBrowserMetadata(cwd, sessionId) {
440
+ try {
441
+ const paths = getAskProSessionPaths(cwd, sessionId);
442
+ const raw = await fs.readFile(paths.browser, "utf8");
443
+ return JSON.parse(raw);
444
+ }
445
+ catch {
446
+ return null;
447
+ }
448
+ }
449
+ function profileMode(profileDir) {
450
+ if (!profileDir)
451
+ return undefined;
452
+ if (askProAgentIdForManagedBrowserProfileDir(profileDir))
453
+ return "agent";
454
+ if (path.resolve(profileDir) === path.resolve(defaultAskProBrowserProfileDir()))
455
+ return "shared";
456
+ return undefined;
457
+ }
458
+ function chromeMode(metadata) {
459
+ if (typeof metadata.chromeMode === "string")
460
+ return metadata.chromeMode;
461
+ return undefined;
462
+ }
463
+ function recoverableConversationUrl(metadata) {
464
+ if (metadata.temporary === true)
465
+ return undefined;
466
+ const runtime = browserRuntimeMetadata(metadata.runtime);
467
+ const candidates = [runtime.tabUrl, metadata.url].filter((value) => typeof value === "string");
468
+ return candidates.find(isConversationUrl);
469
+ }
470
+ function browserRuntimeMetadata(value) {
471
+ return value !== null && typeof value === "object" ? value : {};
472
+ }
473
+ function isConversationUrl(value) {
474
+ return /^https:\/\/chatgpt\.com\/c\/[a-z0-9-]+/i.test(value);
475
+ }
476
+ function compactFields(fields) {
477
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
478
+ }
479
+ function collapseHome(filePath) {
480
+ const home = process.env.HOME || process.env.USERPROFILE;
481
+ if (!home)
482
+ return filePath;
483
+ const resolvedHome = path.resolve(home);
484
+ const resolvedPath = path.resolve(filePath);
485
+ if (resolvedPath === resolvedHome)
486
+ return "~";
487
+ if (resolvedPath.startsWith(`${resolvedHome}${path.sep}`)) {
488
+ return `~${path.sep}${path.relative(resolvedHome, resolvedPath)}`;
489
+ }
490
+ return filePath;
491
+ }
492
+ function classifyCliError(message) {
493
+ const normalized = message.toLowerCase();
494
+ if (normalized.includes("requires a question") ||
495
+ normalized.includes("no ask-pro sessions") ||
496
+ normalized.includes("use either a question argument or --prompt-file") ||
497
+ normalized.includes("--prompt-file - requires piped stdin")) {
498
+ return "usage";
499
+ }
500
+ if (normalized.includes("auth") || normalized.includes("login")) {
501
+ return "auth_required";
502
+ }
503
+ if (normalized.includes("browser") || normalized.includes("chatgpt")) {
504
+ return "browser_failed";
505
+ }
506
+ return "failed";
507
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const rawArgs = process.argv.slice(2);
6
+ const args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
7
+ const here = path.dirname(fileURLToPath(import.meta.url));
8
+ const repoRoot = path.basename(path.dirname(here)) === "dist"
9
+ ? path.resolve(here, "../..")
10
+ : path.resolve(here, "..");
11
+ const cliEntry = path.join(here, "../bin/ask-pro-cli.js");
12
+ const child = spawn(process.execPath, ["--", cliEntry, ...args], {
13
+ env: {
14
+ ...process.env,
15
+ ASK_PRO_SOURCE_CHECKOUT_LAUNCHER: `npm exec --yes pnpm@11.19.0 -- --dir ${quoteCommandArg(repoRoot)} start --`,
16
+ },
17
+ stdio: "inherit",
18
+ });
19
+ child.on("exit", (code) => {
20
+ process.exit(code ?? 0);
21
+ });
22
+ function quoteCommandArg(value) {
23
+ if (process.platform !== "win32") {
24
+ return `'${value.replace(/'/g, "'\\''")}'`;
25
+ }
26
+ return `"${value.replace(/"/g, '""')}"`;
27
+ }
@@ -0,0 +1,26 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ const RETRYABLE_RENAME_ERRORS = new Set(["EBUSY", "EPERM"]);
6
+ export async function atomicWriteFile(filePath, data) {
7
+ const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
8
+ try {
9
+ await fs.writeFile(temporaryPath, data);
10
+ for (let attempt = 0;; attempt += 1) {
11
+ try {
12
+ await fs.rename(temporaryPath, filePath);
13
+ break;
14
+ }
15
+ catch (error) {
16
+ const code = error.code ?? "";
17
+ if (attempt === 2 || !RETRYABLE_RENAME_ERRORS.has(code))
18
+ throw error;
19
+ await delay(50);
20
+ }
21
+ }
22
+ }
23
+ finally {
24
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
25
+ }
26
+ }