ask-pro 0.1.51 → 0.1.58

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
@@ -1,8 +1,8 @@
1
1
  # ask-pro
2
2
 
3
3
  `ask-pro` gives coding agents a focused ChatGPT Pro second opinion through a
4
- human-logged-in browser. The calling agent supplies the context and owns the
5
- result; ask-pro never applies generated code.
4
+ human-logged-in browser. The calling agent supplies the question and evidence,
5
+ and owns the result; ask-pro never applies generated code.
6
6
 
7
7
  ## Install
8
8
 
@@ -71,8 +71,10 @@ Ask for a recoverable repo consult:
71
71
  ask-pro --no-temporary --prompt-file question.md --files src --files tests
72
72
  ```
73
73
 
74
- ask-pro selects `Latest` with `Pro` intelligence. It has no repo or conversation
75
- context unless you provide it, so keep the prompt and file bundle focused.
74
+ ask-pro selects `Latest` when available, otherwise the highest numbered available
75
+ GPT model, with `Pro` intelligence. Supply the caller's context explicitly.
76
+ Use a focused file bundle for local code; connected GitHub can provide remote
77
+ repository context when available (see below).
76
78
 
77
79
  Request generated files only when needed; treat them as data and never execute
78
80
  them automatically:
@@ -86,6 +88,40 @@ Use `ask-pro --help` for the full CLI. For multiline prompts, use
86
88
  `--prompt-file`. Prefer `--no-temporary` when recovery matters; `--temporary`
87
89
  requires Temporary Chat. The [skill](skills/ask-pro/SKILL.md) has agent guidance.
88
90
 
91
+ ## Optional GitHub context
92
+
93
+ Use `--github` to check the GitHub connection in ask-pro's signed-in ChatGPT
94
+ account before sending any prompt or files. It defaults to normal ChatGPT for
95
+ recovery; an explicit `--temporary` still requires Temporary Chat.
96
+
97
+ If GitHub is disconnected or its connection cannot be confirmed, ask-pro opens
98
+ GitHub's ChatGPT page and returns `connect_github_then_resume`. Install the
99
+ plugin if needed, connect your account yourself, and review repository access.
100
+ Select only the repositories you want available; an organization may require
101
+ approval. Then run the emitted `--resume` command. It checks the connection again
102
+ before submitting. ChatGPT login alone does not pass this GitHub check.
103
+
104
+ Then explicitly request repository evidence in the prompt:
105
+
106
+ ```powershell
107
+ ask-pro --github "Read owner/repo and explain its request authentication flow."
108
+ ```
109
+
110
+ Use `--files` for local changes or a precise snapshot that GitHub cannot retrieve.
111
+ For committed repository code, prefer `--github` with the repository, ref, paths,
112
+ and question; source-file uploads are usually unnecessary when retrieval works.
113
+ The flag requests read-only work and citations, but **does not enforce read-only
114
+ GitHub permissions**. The live GitHub plugin advertises write capabilities;
115
+ review the actual grant even though the [Help Center](https://help.openai.com/en/articles/11145903-connecting-github-to-chatgpt)
116
+ describes a read-only app. [Allow read actions](https://help.openai.com/en/articles/20001495-managing-app-permissions-in-chatgpt)
117
+ controls approval prompts, not the underlying access grant.
118
+
119
+ A connected account does not prove access to a particular repository, revision,
120
+ or Pro experience. ask-pro does not select an app in the composer, approve app
121
+ actions, or verify the answer's sources. The opt-in [GitHub smoke](docs/manual-tests.md#connected-github-context)
122
+ checks actual retrieval. GitHub authorization stays with ChatGPT; ask-pro needs
123
+ no GitHub token.
124
+
89
125
  ## Development
90
126
 
91
127
  ```powershell
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import "dotenv/config";
3
3
  import { AssistantStoppedError } from "../src/browser/errors.js";
4
+ import { GITHUB_SETUP_URL, GitHubConnectionRequiredError } from "../src/browser/actions/github.js";
4
5
  import fs from "node:fs/promises";
5
6
  import path from "node:path";
6
7
  import { Command, Option } from "commander";
@@ -20,6 +21,12 @@ program
20
21
  .option("--prompt-file <path>", "read the question from a UTF-8 file; use - for stdin")
21
22
  .option("--artifacts", "ask Pro for ask-pro-response.zip plus markdown fallback")
22
23
  .option("--response-zip", "alias for --artifacts")
24
+ .addOption(new Option("--github", "use connected GitHub; pause for setup before sending").conflicts([
25
+ "resume",
26
+ "status",
27
+ "harvest",
28
+ "copy",
29
+ ]))
23
30
  .option("--resume [session-id]", "resume a prepared or waiting ask-pro session")
24
31
  .option("--status [session-id]", "show ask-pro session status")
25
32
  .option("--harvest [session-id]", "print harvested ANSWER.md for a session")
@@ -124,6 +131,8 @@ async function runAskPro(question, options) {
124
131
  return;
125
132
  }
126
133
  const dryRun = options.dryRun === true;
134
+ if (options.github && options.temporary === undefined)
135
+ options.temporary = false;
127
136
  const resolvedQuestion = await resolveQuestion(question, options, cwd);
128
137
  const artifacts = options.artifacts === true || options.responseZip === true;
129
138
  const session = await createAskProSession({
@@ -132,6 +141,7 @@ async function runAskPro(question, options) {
132
141
  filePatterns: options.files ?? [],
133
142
  dryRun,
134
143
  artifacts,
144
+ github: options.github,
135
145
  });
136
146
  const resumeCommand = buildResumeCommand(session.id, options, cwd);
137
147
  const harvestCommand = buildHarvestCommand(session.id, cwd);
@@ -192,7 +202,8 @@ async function submitOrResumeBrowserSession(cwd, sessionId, options) {
192
202
  if (status.status === "WAITING" ||
193
203
  status.status === "WAIT_TIMED_OUT" ||
194
204
  status.status === "INCOMPLETE_ANSWER" ||
195
- status.status === "NEEDS_USER_AUTH") {
205
+ status.status === "NEEDS_USER_AUTH" ||
206
+ status.status === "NEEDS_GITHUB_CONNECTION") {
196
207
  try {
197
208
  await resumeAskProBrowserSession({
198
209
  cwd,
@@ -202,6 +213,11 @@ async function submitOrResumeBrowserSession(cwd, sessionId, options) {
202
213
  });
203
214
  }
204
215
  catch (error) {
216
+ if (error instanceof GitHubConnectionRequiredError) {
217
+ const { status: paused } = await readAskProStatus({ cwd, sessionId });
218
+ printStatusRecord(paused);
219
+ return;
220
+ }
205
221
  if (error instanceof AssistantStoppedError) {
206
222
  const { status: stopped } = await readAskProStatus({ cwd, sessionId });
207
223
  printStatusRecord(stopped, await readBrowserPreflight(cwd, stopped));
@@ -234,6 +250,11 @@ async function submitOrResumeBrowserSession(cwd, sessionId, options) {
234
250
  });
235
251
  }
236
252
  catch (error) {
253
+ if (error instanceof GitHubConnectionRequiredError) {
254
+ const { status: paused } = await readAskProStatus({ cwd, sessionId });
255
+ printStatusRecord(paused);
256
+ return;
257
+ }
237
258
  if (error instanceof AssistantStoppedError) {
238
259
  const { status: stopped } = await readAskProStatus({ cwd, sessionId });
239
260
  printStatusRecord(stopped, await readBrowserPreflight(cwd, stopped));
@@ -320,6 +341,11 @@ function printStatusRecord(status, extra = {}) {
320
341
  state: normalizeState(status.status),
321
342
  reason: status.reason,
322
343
  temporary: normalizeTemporary(status.temporary),
344
+ github: status.github || undefined,
345
+ setup_url: status.status === "NEEDS_GITHUB_CONNECTION" ? GITHUB_SETUP_URL : undefined,
346
+ help: status.status === "NEEDS_GITHUB_CONNECTION"
347
+ ? "In the opened ChatGPT page, install GitHub if needed, connect your account, review repository access, then resume."
348
+ : undefined,
323
349
  action: actionForStatus(status),
324
350
  resume: shouldPrintResume(status) ? status.resumeCommand : undefined,
325
351
  harvest: shouldPrintHarvest(status) ? status.harvestCommand : undefined,
@@ -360,6 +386,8 @@ function actionForStatus(status) {
360
386
  if (status.reason === "stopped_without_answer")
361
387
  return "choose_resume_or_retry";
362
388
  switch (status.status) {
389
+ case "NEEDS_GITHUB_CONNECTION":
390
+ return "connect_github_then_resume";
363
391
  case "DRY_RUN_COMPLETE":
364
392
  case "INCOMPLETE_ANSWER":
365
393
  case "READY_TO_SUBMIT":
@@ -385,6 +413,7 @@ function shouldPrintResume(status) {
385
413
  "WAITING",
386
414
  "READY_TO_SUBMIT",
387
415
  "NEEDS_USER_AUTH",
416
+ "NEEDS_GITHUB_CONNECTION",
388
417
  "WAIT_TIMED_OUT",
389
418
  "FAILED",
390
419
  ].includes(status.status);
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { DEFAULT_CHATGPT_BROWSER_MODEL_LABEL } from "../browser/chatgptModelCatalog.js";
4
4
  import { AssistantStoppedError, BrowserAutomationError } from "../browser/errors.js";
5
+ import { GitHubConnectionRequiredError } from "../browser/actions/github.js";
5
6
  import { askProAgentIdForLegacyBrowserProfileDir, askProAgentIdForManagedBrowserProfileDir, askProBrowserProfileDirForAgentId, ensureAskProBrowserProfileDir, isLegacyAskProManagedBrowserProfileDir, isAskProManagedBrowserProfileDir, resolveAskProAgentId, } from "../browser/profilePaths.js";
6
7
  import { runBrowserMode } from "../browserMode.js";
7
8
  import { closeTab } from "../browser/chromeLifecycle.js";
@@ -18,10 +19,11 @@ export async function runAskProBrowserSession(options) {
18
19
  const { dir } = getAskProSessionPaths(options.cwd, options.sessionId);
19
20
  return withSessionControllerLease(dir, () => runAskProBrowserSessionWithLease(options));
20
21
  }
21
- async function runAskProBrowserSessionWithLease({ cwd, sessionId, temporary, chatgptUrl: chatgptUrlOverride, browserProfileDir, agentId: agentIdOverride, allowStartMinimized = true, verbose, }) {
22
+ async function runAskProBrowserSessionWithLease({ cwd, sessionId, temporary: requestedTemporary, chatgptUrl: chatgptUrlOverride, browserProfileDir, agentId: agentIdOverride, allowStartMinimized = true, verbose, }) {
22
23
  const paths = getAskProSessionPaths(cwd, sessionId);
23
24
  const prompt = await readAskProPrompt({ cwd, sessionId });
24
25
  const { status: sessionStatus } = await readAskProStatus({ cwd, sessionId });
26
+ const temporary = requestedTemporary ?? (sessionStatus.github ? false : undefined);
25
27
  const artifactsRequested = sessionStatus.artifacts === true;
26
28
  const agentId = agentIdOverride !== undefined ? agentIdOverride : resolveAskProAgentId();
27
29
  const browserProfile = browserProfileDir ?? (await ensureAskProBrowserProfileDir(agentId));
@@ -62,6 +64,7 @@ async function runAskProBrowserSessionWithLease({ cwd, sessionId, temporary, cha
62
64
  },
63
65
  ],
64
66
  config: {
67
+ github: sessionStatus.github,
65
68
  url: chatgptUrl,
66
69
  manualLogin: true,
67
70
  attachRunning: false,
@@ -163,7 +166,7 @@ async function runAskProBrowserSessionWithLease({ cwd, sessionId, temporary, cha
163
166
  temporary,
164
167
  })) {
165
168
  await appendAskProLog(cwd, sessionId, "Temporary Chat did not expose the Pro model; retrying in normal ChatGPT.");
166
- await closeFallbackTemporaryTab(cwd, sessionId, logger);
169
+ await closeSessionBrowserTab(cwd, sessionId, logger);
167
170
  await updateAskProStatus({
168
171
  cwd,
169
172
  sessionId,
@@ -206,6 +209,16 @@ async function runAskProBrowserSessionWithLease({ cwd, sessionId, temporary, cha
206
209
  verbose,
207
210
  });
208
211
  }
212
+ if (error instanceof GitHubConnectionRequiredError) {
213
+ await writeTerminalBrowserMetadata(cwd, sessionId, "needs_github_connection", error.reason);
214
+ await updateAskProStatus({
215
+ cwd,
216
+ sessionId,
217
+ status: "NEEDS_GITHUB_CONNECTION",
218
+ reason: error.reason,
219
+ });
220
+ throw error;
221
+ }
209
222
  if (isAuthGateError(error)) {
210
223
  const currentMetadata = await readBrowserMetadata(paths.browser).catch(() => ({}));
211
224
  await writeAskProBrowserMetadata({
@@ -282,6 +295,20 @@ async function resumeAskProBrowserSessionWithLease({ cwd, sessionId, temporary,
282
295
  ? ASK_PRO_CHATGPT_URL
283
296
  : (metadata.url ?? ASK_PRO_TEMPORARY_CHATGPT_URL);
284
297
  const fallbackProfile = await resolveResumeBrowserProfile(metadata);
298
+ if (sessionStatus.status === "NEEDS_GITHUB_CONNECTION" ||
299
+ (sessionStatus.github && metadata.runtime?.githubCheckPending === true)) {
300
+ await closeSessionBrowserTab(cwd, sessionId, logger);
301
+ await runAskProBrowserSessionWithLease({
302
+ cwd,
303
+ sessionId,
304
+ temporary: effectiveTemporary,
305
+ browserProfileDir: fallbackProfile,
306
+ agentId: metadata.agentId ?? null,
307
+ allowStartMinimized: false,
308
+ verbose,
309
+ });
310
+ return;
311
+ }
285
312
  let capturedFinalStatus = null;
286
313
  if (metadata.runtime) {
287
314
  metadata.runtime = normalizeResumeRuntime(metadata.runtime, metadata.profileDir, fallbackProfile);
@@ -494,7 +521,7 @@ function withNoTemporaryResumeCommand(command) {
494
521
  }
495
522
  return withoutStrictTemporary.replace(/\s--resume(?=\s|$)/, " --no-temporary --resume");
496
523
  }
497
- async function closeFallbackTemporaryTab(cwd, sessionId, logger) {
524
+ async function closeSessionBrowserTab(cwd, sessionId, logger) {
498
525
  const paths = getAskProSessionPaths(cwd, sessionId);
499
526
  const metadata = await readBrowserMetadata(paths.browser).catch(() => null);
500
527
  const runtime = metadata?.runtime;
@@ -20,7 +20,7 @@ const DEFAULT_EXCLUDES = [
20
20
  ];
21
21
  const SESSION_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,127}$/;
22
22
  const SESSION_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
23
- export async function createAskProSession({ cwd, question, filePatterns, dryRun, artifacts = false, }) {
23
+ export async function createAskProSession({ cwd, question, filePatterns, dryRun, artifacts = false, github = false, }) {
24
24
  const trimmedQuestion = question.trim();
25
25
  if (!trimmedQuestion) {
26
26
  throw new Error("ask-pro requires a question.");
@@ -56,8 +56,9 @@ export async function createAskProSession({ cwd, question, filePatterns, dryRun,
56
56
  harvestCommand: `ask-pro --harvest ${sessionId}`,
57
57
  dryRun,
58
58
  artifacts,
59
+ ...(github ? { github: true } : {}),
59
60
  };
60
- const submittedPrompt = renderSubmittedPrompt(question, artifacts);
61
+ const submittedPrompt = renderSubmittedPrompt(question, artifacts, github);
61
62
  const manifestMarkdown = renderManifestMarkdown(manifest);
62
63
  const browserMetadata = {
63
64
  schemaVersion: 1,
@@ -621,11 +622,11 @@ function redactSecretsForLog(message) {
621
622
  const findings = [];
622
623
  return redactSecrets(message, "log", findings);
623
624
  }
624
- function renderSubmittedPrompt(question, artifacts) {
625
+ function renderSubmittedPrompt(question, artifacts, github) {
625
626
  const artifactRequest = artifacts
626
627
  ? "\nIf file generation is available, also create a downloadable zip named ask-pro-response.zip. It should contain IMPLEMENTATION_PLAN.md, TASKS.json, TEST_PLAN.md, RISK_REGISTER.md, FILES_TO_EDIT.md, and REPO_CONTEXT_USED.md. If you cannot create a zip, return the same content in markdown sections.\n"
627
628
  : "";
628
- return `${question}
629
+ return `${question}${github ? "\n\nUse the connected GitHub app for the requested repository evidence. Keep the work read-only: do not change files, create issues or pull requests, or post comments. Cite the files and revision actually retrieved, and report missing access or revision conflicts." : ""}
629
630
 
630
631
  Read MANIFEST.md in CONTEXT.zip first. Treat the context files it lists as authoritative evidence only for the scope they cover, and call out material gaps or conflicts.
631
632
  ${artifactRequest}
@@ -0,0 +1,67 @@
1
+ import { BrowserAutomationError } from "../errors.js";
2
+ import { delay } from "../utils.js";
3
+ const GITHUB_PLUGIN_ID = "plugin_connector_1p_1a69035c238881919c4190932b2df699";
4
+ const GITHUB_APP_ID = "connector_76869538009648d5b282a4bb21c3d157";
5
+ export const GITHUB_SETUP_URL = `https://chatgpt.com/plugins/${GITHUB_PLUGIN_ID}`;
6
+ const GITHUB_SETTINGS_URL = `https://chatgpt.com/settings/plugins-settings/${GITHUB_PLUGIN_ID}/apps/${GITHUB_APP_ID}`;
7
+ export class GitHubConnectionRequiredError extends BrowserAutomationError {
8
+ reason;
9
+ constructor(reason) {
10
+ super("Connect GitHub in the opened ChatGPT page, review repository access, then resume.", {
11
+ stage: "github-connection-required",
12
+ });
13
+ this.reason = reason;
14
+ }
15
+ }
16
+ // Read only visible connection controls, never account identifiers or OAuth state.
17
+ function readGitHubConnectionState(accountSectionId) {
18
+ const visible = (element) => {
19
+ const rect = element.getBoundingClientRect();
20
+ const style = getComputedStyle(element);
21
+ return (rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden");
22
+ };
23
+ const heading = Array.from(document.querySelectorAll("h1")).some((element) => visible(element) && element.textContent?.trim() === "GitHub");
24
+ if (!heading)
25
+ return "unknown";
26
+ const section = document.getElementById(accountSectionId);
27
+ if (!section || !visible(section))
28
+ return "unknown";
29
+ const buttons = Array.from(section.querySelectorAll("button")).filter(visible);
30
+ if (buttons.some((button) => /^reconnect\b/i.test(button.textContent?.trim() ?? ""))) {
31
+ return "disconnected";
32
+ }
33
+ if (buttons.some((button) => button.getAttribute("aria-label")?.startsWith("Actions for "))) {
34
+ return "connected";
35
+ }
36
+ return buttons.some((button) => /^connect\b/i.test(button.textContent?.trim() ?? ""))
37
+ ? "disconnected"
38
+ : "unknown";
39
+ }
40
+ export async function ensureGitHubConnection(Page, Runtime, logger, timeoutMs) {
41
+ const { result: location } = await Runtime.evaluate({
42
+ expression: "location.href",
43
+ returnByValue: true,
44
+ });
45
+ const returnUrl = location.value;
46
+ await Page.navigate({ url: GITHUB_SETTINGS_URL });
47
+ const deadline = Date.now() + timeoutMs;
48
+ let state = "unknown";
49
+ do {
50
+ const result = await Runtime.evaluate({
51
+ expression: `(${readGitHubConnectionState.toString()})(${JSON.stringify(`plugin-connected-accounts-${GITHUB_APP_ID}`)})`,
52
+ returnByValue: true,
53
+ }).catch(() => undefined);
54
+ state = result?.result.value ?? "unknown";
55
+ if (state !== "unknown")
56
+ break;
57
+ if (Date.now() < deadline)
58
+ await delay(300);
59
+ } while (Date.now() < deadline);
60
+ if (state !== "connected") {
61
+ // Stop on ChatGPT before the human starts OAuth; do not observe consent or callback URLs.
62
+ await Page.navigate({ url: GITHUB_SETUP_URL });
63
+ throw new GitHubConnectionRequiredError(state === "disconnected" ? "github_connection_required" : "github_connection_unverified");
64
+ }
65
+ logger("GitHub connected. Repository access depends on the selected repositories.");
66
+ await Page.navigate({ url: returnUrl });
67
+ }
@@ -269,7 +269,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
269
269
  return { status: 'already-selected', label };
270
270
  }
271
271
  const buttonMatchesTarget = () => {
272
- // The numbered pill cannot prove that the rolling Latest option is selected.
272
+ // The composer pill cannot prove that the newest available model is selected.
273
273
  if (normalizedTarget === 'latest') return false;
274
274
  const normalizedLabel = normalize(getButtonLabel());
275
275
  if (!normalizedLabel) return false;
@@ -446,14 +446,24 @@ function buildModelSelectionExpression(targetModel, strategy) {
446
446
  testid.toLowerCase().includes('submenu') ||
447
447
  option.getAttribute('data-has-submenu') !== null ||
448
448
  option.getAttribute('aria-haspopup') === 'menu';
449
- const baseScore = scoreOption(normalizedText, testid);
449
+ if (normalizedTarget === 'latest') {
450
+ const rect = option.getBoundingClientRect();
451
+ if (isSubmenu || rect.width <= 0 || rect.height <= 0 ||
452
+ option.getAttribute('aria-disabled') === 'true' || option.hasAttribute('disabled')) continue;
453
+ }
454
+ // Read the raw label: normalization removes decimal separators.
455
+ const modelVersion = normalizedTarget === 'latest'
456
+ ? text.trim().match(/^gpt[-\\s]+(\\d+(?:\\.\\d+)*)(?![.\\d])/i)?.[1] ?? ''
457
+ : '';
458
+ const baseScore = modelVersion ? 500 : scoreOption(normalizedText, testid);
450
459
  if (baseScore <= 0) {
451
460
  continue;
452
461
  }
453
462
  const score = baseScore + (isSubmenu ? 0 : 1);
454
463
  const label = getOptionLabel(option);
455
- if (!bestMatch || score > bestMatch.score) {
456
- bestMatch = { node: option, label, score, testid, normalizedText, isSubmenu };
464
+ if (!bestMatch || score > bestMatch.score ||
465
+ (score === bestMatch.score && modelVersion.localeCompare(bestMatch.modelVersion, 'en', { numeric: true }) > 0)) {
466
+ bestMatch = { node: option, label, score, testid, normalizedText, isSubmenu, modelVersion };
457
467
  }
458
468
  }
459
469
  }
@@ -10,6 +10,7 @@ import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked,
10
10
  import { INPUT_SELECTORS } from "./constants.js";
11
11
  import { uploadAttachmentViaDataTransfer } from "./actions/remoteFileTransfer.js";
12
12
  import { ensureThinkingTime } from "./actions/thinkingTime.js";
13
+ import { ensureGitHubConnection, GitHubConnectionRequiredError } from "./actions/github.js";
13
14
  import { startThinkingStatusMonitor } from "./actions/thinkingStatus.js";
14
15
  import { createAssistantContinuation } from "./actions/assistantResponse.js";
15
16
  import { createPostSubmitInputGuard } from "./actions/inputGuard.js";
@@ -278,9 +279,10 @@ export async function runBrowserMode(options) {
278
279
  logger.sessionLog = options.log.sessionLog;
279
280
  }
280
281
  const runtimeHintCb = options.runtimeHintCb;
282
+ let githubCheckPending = config.github;
281
283
  let lastTargetId;
282
284
  let lastUrl;
283
- const emitRuntimeHint = async () => {
285
+ const emitRuntimeHint = async (required = false) => {
284
286
  if (!runtimeHintCb || !chrome?.port) {
285
287
  return;
286
288
  }
@@ -294,11 +296,14 @@ export async function runBrowserMode(options) {
294
296
  conversationId,
295
297
  userDataDir,
296
298
  controllerPid: process.pid,
299
+ githubCheckPending,
297
300
  };
298
301
  try {
299
302
  await runtimeHintCb(hint);
300
303
  }
301
304
  catch (error) {
305
+ if (required)
306
+ throw error;
302
307
  const message = error instanceof Error ? error.message : String(error);
303
308
  logger(`Failed to persist runtime hint: ${message}`);
304
309
  }
@@ -517,6 +522,7 @@ export async function runBrowserMode(options) {
517
522
  });
518
523
  client = connection.client;
519
524
  isolatedTargetId = connection.targetId ?? null;
525
+ lastTargetId = connection.targetId ?? undefined;
520
526
  if (!isolatedTargetId) {
521
527
  launchTargetIds = [];
522
528
  }
@@ -702,6 +708,12 @@ export async function runBrowserMode(options) {
702
708
  }));
703
709
  if (manualLogin)
704
710
  manualLoginRecoveryInProgress = false;
711
+ if (config.github) {
712
+ await raceWithDisconnect(ensureGitHubConnection(Page, Runtime, logger, config.inputTimeoutMs));
713
+ githubCheckPending = false;
714
+ // Persist this before upload/send so a failed save cannot cause a duplicate retry.
715
+ await emitRuntimeHint(true);
716
+ }
705
717
  if (config.url !== baseUrl) {
706
718
  await raceWithDisconnect(navigateToPromptReadyWithFallback(Page, Runtime, {
707
719
  url: config.url,
@@ -1280,6 +1292,12 @@ export async function runBrowserMode(options) {
1280
1292
  }
1281
1293
  catch (error) {
1282
1294
  let normalizedError = error instanceof Error ? error : new Error(String(error));
1295
+ if (normalizedError instanceof GitHubConnectionRequiredError) {
1296
+ preserveBrowserOnError = true;
1297
+ await revealAuthenticatedWindow("github-connection-required");
1298
+ await emitRuntimeHint();
1299
+ throw normalizedError;
1300
+ }
1283
1301
  if (manualLoginRecoveryInProgress && isWebSocketClosureError(normalizedError)) {
1284
1302
  const livePort = await readDevToolsPort(userDataDir);
1285
1303
  const endpoint = livePort
@@ -1358,6 +1376,9 @@ export async function runBrowserMode(options) {
1358
1376
  isolatedTargetId = recovered.ownsTarget ? recovered.targetId : null;
1359
1377
  ownsTarget = recovered.ownsTarget;
1360
1378
  connectionClosedUnexpectedly = false;
1379
+ // The restarted run owns persistence; the outer hook would save stale onboarding state.
1380
+ removeTerminationHooks?.();
1381
+ removeTerminationHooks = null;
1361
1382
  const restartedResult = await runBrowserMode(config.browserTabRef
1362
1383
  ? {
1363
1384
  ...options,
@@ -1771,9 +1792,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1771
1792
  let expectedConversationUrl;
1772
1793
  let expectedConversationId;
1773
1794
  let attachedExistingTab = false;
1795
+ let githubCheckPending = config.github;
1796
+ let githubSetupRequired = false;
1774
1797
  let ownsTarget = true;
1775
1798
  const runtimeHintCb = options.runtimeHintCb;
1776
- const emitRuntimeHint = async () => {
1799
+ const emitRuntimeHint = async (required = false) => {
1777
1800
  if (!runtimeHintCb)
1778
1801
  return;
1779
1802
  try {
@@ -1787,9 +1810,12 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1787
1810
  tabUrl: lastUrl,
1788
1811
  conversationId,
1789
1812
  controllerPid: process.pid,
1813
+ githubCheckPending,
1790
1814
  });
1791
1815
  }
1792
1816
  catch (error) {
1817
+ if (required)
1818
+ throw error;
1793
1819
  const message = error instanceof Error ? error.message : String(error);
1794
1820
  logger(`Failed to persist runtime hint: ${message}`);
1795
1821
  }
@@ -1924,6 +1950,12 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1924
1950
  await raceWithDisconnect(ensureLoggedIn(Runtime, logger, { remoteSession: true }));
1925
1951
  await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
1926
1952
  }
1953
+ if (config.github) {
1954
+ await raceWithDisconnect(ensureGitHubConnection(Page, Runtime, logger, config.inputTimeoutMs));
1955
+ githubCheckPending = false;
1956
+ await emitRuntimeHint(true);
1957
+ await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
1958
+ }
1927
1959
  logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
1928
1960
  try {
1929
1961
  const { result } = await Runtime.evaluate({
@@ -2312,6 +2344,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2312
2344
  }
2313
2345
  catch (error) {
2314
2346
  const normalizedError = error instanceof Error ? error : new Error(String(error));
2347
+ githubSetupRequired = normalizedError instanceof GitHubConnectionRequiredError;
2315
2348
  const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
2316
2349
  connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
2317
2350
  if (!socketClosed) {
@@ -2344,7 +2377,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2344
2377
  // ignore
2345
2378
  }
2346
2379
  removeDialogHandler?.();
2347
- if (ownsTarget) {
2380
+ if (ownsTarget && !githubSetupRequired) {
2348
2381
  await closeRemoteChromeTarget(host, port, remoteTargetId ?? undefined, logger);
2349
2382
  }
2350
2383
  // Don't kill remote Chrome - it's not ours to manage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ask-pro",
3
- "version": "0.1.51",
3
+ "version": "0.1.58",
4
4
  "description": "Browser-backed ChatGPT Pro escalation for hard engineering questions.",
5
5
  "keywords": [
6
6
  "agents",
@@ -34,10 +34,13 @@ When invoked:
34
34
 
35
35
  1. Inspect the repo and the relevant files.
36
36
  2. Identify the exact decision Pro should answer.
37
- 3. Choose a small, high-signal file bundle with `--files`.
37
+ 3. For committed code on GitHub, prefer `--github` and give Pro the repository,
38
+ ref, relevant paths, and question. Let it retrieve the code instead of assembling
39
+ a source bundle. Add a small `--files` bundle only for uncommitted changes,
40
+ local-only evidence, or repository content the connection cannot access.
38
41
  4. Write the prompt yourself using the Prompt Shape below.
39
- 5. Run the smallest useful command, usually
40
- `ask-pro --no-temporary --files "<glob>" "<prompt>"` for repo advisories.
42
+ 5. Run the smallest useful command: `ask-pro --github "<prompt>"` for GitHub
43
+ evidence, or `ask-pro --no-temporary --files "<glob>" "<prompt>"` for a file bundle.
41
44
  For multiline prompts, write a temporary prompt file and use
42
45
  `ask-pro --no-temporary --prompt-file <path> --files "<glob>"`; do not rely
43
46
  on shell multiline quoting.
@@ -58,7 +61,8 @@ When invoked:
58
61
  `harvest` command when that is the next action.
59
62
  8. Treat the answer as advisory; turn it into your own plan before editing code.
60
63
 
61
- `ask-pro` selects `Latest`, then `Pro` intelligence. Do not require a
64
+ `ask-pro` selects `Latest` when available, otherwise the highest numbered available
65
+ GPT model, then `Pro` intelligence. Do not require a
62
66
  different dated model label or a separate thinking-effort control in the prompt
63
67
  or workflow.
64
68
 
@@ -87,7 +91,7 @@ again. Example:
87
91
 
88
92
  ## Prompt Shape
89
93
 
90
- Assume Pro has no caller or repository context. Include each material fact and
94
+ Assume Pro has no caller context or local checkout. Include each material fact and
91
95
  instruction once: the goal, current state, hard constraints, evidence, success
92
96
  criteria, and required output. Do not rely on the agent's conversation context, repo
93
97
  folklore, prior ask-pro runs, branch names, or unstated user preferences.
@@ -112,6 +116,46 @@ Keep bundles focused: source files under review, focused tests, relevant docs,
112
116
  known recent changes, and validation status. Avoid whole-repo bundles unless the
113
117
  question is explicitly architectural.
114
118
 
119
+ ## Connected GitHub (optional)
120
+
121
+ Prefer `--github` for remote repository evidence. It checks
122
+ the connection before uploading or submitting, and defaults to normal ChatGPT.
123
+ Do not require GitHub for file-based consults. Availability varies by account,
124
+ workspace, model, and ChatGPT experience.
125
+
126
+ If the CLI returns `needs_github_connection` / `connect_github_then_resume`,
127
+ direct the human to the opened GitHub page in ask-pro's browser. They install the
128
+ plugin if needed, complete sign-in, review permissions, and select repositories;
129
+ organization approval may be needed. Wait for them to finish, then run the
130
+ emitted resume command without adding `--github`. The requirement is saved with
131
+ the session and checked again. Do not start a second session or submit through
132
+ the browser yourself. Never collect GitHub credentials or automate authorization.
133
+
134
+ If available, GitHub's **Allow read actions** permission can avoid read approval
135
+ prompts. The human chooses that setting. It controls approval behavior, not the
136
+ underlying permissions of a write-capable app; see
137
+ [app permissions](https://help.openai.com/en/articles/20001495-managing-app-permissions-in-chatgpt).
138
+ The live GitHub plugin advertises write capabilities despite the Help Center's
139
+ read-only description. `--github` requests read-only work; it does not restrict
140
+ the provider's permissions or prove that writes are impossible.
141
+
142
+ If GitHub is already connected, proceed without a setup question or source-file
143
+ bundle. For the consult, name the repository (`owner/repo`), relevant paths, and
144
+ requested branch or commit in the prompt. Explicitly ask Pro
145
+ to answer the specific question. The wrapper requests the connected GitHub app,
146
+ read-only work, citations, the revision inspected, and access gaps. Do not
147
+ infer repository access from ChatGPT login or a successful CLI exit.
148
+
149
+ Keep `--files` for local/uncommitted changes, non-GitHub evidence, and snapshots
150
+ that the app cannot retrieve. Distinguish
151
+ uploaded evidence from GitHub evidence, and ask Pro to flag revision conflicts.
152
+ If the app or requested revision is unavailable, offer a new consult with a
153
+ focused file bundle and
154
+ disclose that limitation. Do not silently switch away from Pro. The CLI submits
155
+ text; it does not select an app in the composer or handle app approval prompts.
156
+ Until a live run demonstrates retrieval, describe this as an available setup
157
+ path, not a verified connection. Confirm material claims against the repo.
158
+
115
159
  ## Output
116
160
 
117
161
  Normal `ask-pro` stdout is compact TOON-style telemetry. Use `state`, `action`,
@@ -147,6 +191,7 @@ state is `INCOMPLETE_ANSWER` / `preamble_without_artifacts`, do not treat
147
191
  ```bash
148
192
  ask-pro "Review the async billing webhook migration plan and return an implementation plan."
149
193
  ask-pro --no-temporary --prompt-file question.md --files src --files tests
194
+ ask-pro --github "In owner/repo at <commit>, inspect src/auth and explain token validation."
150
195
  ask-pro --temporary "Review this sensitive migration plan, and fail if Temporary Chat cannot use Pro."
151
196
  ask-pro --no-temporary "Review this in normal ChatGPT instead of Temporary Chat."
152
197
  ask-pro --prompt-file question.md --files .\src