ask-pro 0.1.53 → 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 +39 -4
- package/dist/bin/ask-pro-cli.js +30 -1
- package/dist/src/ask-pro/browserRunner.js +30 -3
- package/dist/src/ask-pro/session.js +5 -4
- package/dist/src/browser/actions/github.js +67 -0
- package/dist/src/browser/index.js +36 -3
- package/package.json +1 -1
- package/skills/ask-pro/SKILL.md +48 -4
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
|
|
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
|
|
|
@@ -72,8 +72,9 @@ ask-pro --no-temporary --prompt-file question.md --files src --files tests
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
ask-pro selects `Latest` when available, otherwise the highest numbered available
|
|
75
|
-
GPT model, with `Pro` intelligence.
|
|
76
|
-
|
|
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).
|
|
77
78
|
|
|
78
79
|
Request generated files only when needed; treat them as data and never execute
|
|
79
80
|
them automatically:
|
|
@@ -87,6 +88,40 @@ Use `ask-pro --help` for the full CLI. For multiline prompts, use
|
|
|
87
88
|
`--prompt-file`. Prefer `--no-temporary` when recovery matters; `--temporary`
|
|
88
89
|
requires Temporary Chat. The [skill](skills/ask-pro/SKILL.md) has agent guidance.
|
|
89
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
|
+
|
|
90
125
|
## Development
|
|
91
126
|
|
|
92
127
|
```powershell
|
package/dist/bin/ask-pro-cli.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
}
|
|
@@ -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
package/skills/ask-pro/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
|
40
|
-
`ask-pro --no-temporary --files "<glob>" "<prompt>"` for
|
|
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.
|
|
@@ -88,7 +91,7 @@ again. Example:
|
|
|
88
91
|
|
|
89
92
|
## Prompt Shape
|
|
90
93
|
|
|
91
|
-
Assume Pro has no caller or
|
|
94
|
+
Assume Pro has no caller context or local checkout. Include each material fact and
|
|
92
95
|
instruction once: the goal, current state, hard constraints, evidence, success
|
|
93
96
|
criteria, and required output. Do not rely on the agent's conversation context, repo
|
|
94
97
|
folklore, prior ask-pro runs, branch names, or unstated user preferences.
|
|
@@ -113,6 +116,46 @@ Keep bundles focused: source files under review, focused tests, relevant docs,
|
|
|
113
116
|
known recent changes, and validation status. Avoid whole-repo bundles unless the
|
|
114
117
|
question is explicitly architectural.
|
|
115
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
|
+
|
|
116
159
|
## Output
|
|
117
160
|
|
|
118
161
|
Normal `ask-pro` stdout is compact TOON-style telemetry. Use `state`, `action`,
|
|
@@ -148,6 +191,7 @@ state is `INCOMPLETE_ANSWER` / `preamble_without_artifacts`, do not treat
|
|
|
148
191
|
```bash
|
|
149
192
|
ask-pro "Review the async billing webhook migration plan and return an implementation plan."
|
|
150
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."
|
|
151
195
|
ask-pro --temporary "Review this sensitive migration plan, and fail if Temporary Chat cannot use Pro."
|
|
152
196
|
ask-pro --no-temporary "Review this in normal ChatGPT instead of Temporary Chat."
|
|
153
197
|
ask-pro --prompt-file question.md --files .\src
|