ask-pro 0.1.61 → 0.1.69
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 +29 -36
- package/SKILL.md +229 -0
- package/dist/src/ask-pro/browserRunner.js +6 -21
- package/dist/src/browser/actions/thinkingStatus.js +5 -3
- package/dist/src/browser/profilePaths.js +3 -213
- package/dist/src/browser/profileState.js +1 -48
- package/package.json +2 -4
- package/skills/ask-pro/SKILL.md +4 -229
- package/scripts/refresh-local-plugin.mjs +0 -179
- package/scripts/refresh-local-plugin.ps1 +0 -93
package/README.md
CHANGED
|
@@ -6,47 +6,39 @@ and owns the result; ask-pro never applies generated code.
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
Both options need Node.js 24+, Chrome, and a ChatGPT account with Pro access.
|
|
10
|
-
Browser login is manual.
|
|
11
|
-
|
|
12
|
-
### Codex plugin marketplace
|
|
13
|
-
|
|
14
9
|
```powershell
|
|
15
|
-
|
|
16
|
-
codex plugin add ask-pro@ask-pro
|
|
10
|
+
npx skills add JJLiebig/ask-pro -g
|
|
17
11
|
```
|
|
18
12
|
|
|
19
|
-
This
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Update with `codex plugin marketplace upgrade ask-pro`, then restart Codex to
|
|
24
|
-
load the updated skill.
|
|
25
|
-
|
|
26
|
-
### Skills CLI (Codex and other supported agents)
|
|
13
|
+
This installs the `ask-pro` skill for Codex, Claude Code, and the other agents
|
|
14
|
+
the [skills CLI](https://github.com/vercel-labs/skills) supports. The skill
|
|
15
|
+
contains ask-pro itself; its first run installs dependencies and builds, so no
|
|
16
|
+
separate CLI install is needed. Update with `npx skills update`.
|
|
27
17
|
|
|
28
|
-
|
|
18
|
+
Codex users can install it as a plugin instead:
|
|
29
19
|
|
|
30
20
|
```powershell
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
codex plugin marketplace add JJLiebig/ask-pro
|
|
22
|
+
codex plugin add ask-pro@ask-pro
|
|
33
23
|
```
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
Update with `codex plugin marketplace upgrade ask-pro`, then restart Codex.
|
|
26
|
+
Both installs run the same bundled launcher and share the same state.
|
|
27
|
+
|
|
28
|
+
You need Node.js 24+, npm, Chrome, and a ChatGPT account with Pro access. The
|
|
29
|
+
browser runtime is developed and tested on Windows; other operating systems
|
|
30
|
+
need validation.
|
|
39
31
|
|
|
40
|
-
|
|
32
|
+
**Upgrading from an earlier version:** ask-pro state moved from
|
|
33
|
+
`~/.codex/state/ask-pro` to `~/.agents/state/ask-pro`. Before the first run
|
|
34
|
+
after upgrading, and while no ask-pro run is active, copy your signed-in browser
|
|
35
|
+
state once:
|
|
41
36
|
|
|
42
37
|
```powershell
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
New-Item -ItemType Directory -Force "$HOME\.agents\state" | Out-Null
|
|
39
|
+
Copy-Item -Recurse "$HOME\.codex\state\ask-pro" "$HOME\.agents\state\ask-pro"
|
|
45
40
|
```
|
|
46
41
|
|
|
47
|
-
The browser runtime is developed and tested on Windows. Other operating systems
|
|
48
|
-
need validation.
|
|
49
|
-
|
|
50
42
|
## Browser and session state
|
|
51
43
|
|
|
52
44
|
ask-pro never asks for, types, reads, or logs passwords, MFA codes, recovery
|
|
@@ -54,10 +46,9 @@ codes, session cookies, or raw auth tokens. A new browser profile may need one
|
|
|
54
46
|
manual ChatGPT login. On Windows, ordinary managed Chrome runs start minimized
|
|
55
47
|
and are hidden; login and recovery restore the window for human action.
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
for copy and recovery details.
|
|
49
|
+
ask-pro keeps its state in `~/.agents/state/ask-pro/`: the persistent browser
|
|
50
|
+
profile in `browser-profile/` and the builds of the installed skill in
|
|
51
|
+
`runtimes/`. Set `ASK_PRO_STATE_DIR` to use a different folder.
|
|
61
52
|
|
|
62
53
|
Project sessions live under `.ask-pro/sessions/<id>/`. Add `.ask-pro/` to your
|
|
63
54
|
project's `.gitignore`. At each invocation, ask-pro deletes entire session
|
|
@@ -65,7 +56,9 @@ directories at least seven days old, regardless of state.
|
|
|
65
56
|
|
|
66
57
|
## Use
|
|
67
58
|
|
|
68
|
-
|
|
59
|
+
Agents run ask-pro through the installed skill, as
|
|
60
|
+
`node <skill-folder>/scripts/ask-pro.mjs`; `ask-pro` below stands for that
|
|
61
|
+
command. Ask for a recoverable repo consult:
|
|
69
62
|
|
|
70
63
|
```powershell
|
|
71
64
|
ask-pro --no-temporary --prompt-file question.md --files src --files tests
|
|
@@ -86,7 +79,7 @@ ask-pro --harvest <session-id>
|
|
|
86
79
|
|
|
87
80
|
Use `ask-pro --help` for the full CLI. For multiline prompts, use
|
|
88
81
|
`--prompt-file`. Prefer `--no-temporary` when recovery matters; `--temporary`
|
|
89
|
-
requires Temporary Chat. The [skill](
|
|
82
|
+
requires Temporary Chat. The [skill](SKILL.md) has agent guidance.
|
|
90
83
|
|
|
91
84
|
When Pro asks native questions, ask-pro returns `awaiting_answers`, prints each
|
|
92
85
|
question and its numbered choices, and saves `QUESTIONS.json`. The calling agent
|
|
@@ -154,8 +147,8 @@ pnpm run format:check
|
|
|
154
147
|
pnpm pack --dry-run
|
|
155
148
|
```
|
|
156
149
|
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
To try local changes as an installed skill, run
|
|
151
|
+
`npx skills add <path-to-checkout> -g`. Live browser tests are opt-in; see
|
|
159
152
|
[manual tests](docs/manual-tests.md).
|
|
160
153
|
|
|
161
154
|
After the build matrix passes on `main`, [CI](.github/workflows/ci.yml) publishes
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ask-pro
|
|
3
|
+
description: Escalate hard engineering questions to ChatGPT Pro through browser automation with focused repo context. Use when an agent needs a stronger external review, architecture plan, migration strategy, production-debugging second opinion, or when the user explicitly asks to use $ask-pro.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# $ask-pro
|
|
7
|
+
|
|
8
|
+
Use `$ask-pro` to ask ChatGPT Pro for a focused second opinion on hard engineering work.
|
|
9
|
+
|
|
10
|
+
The calling agent still owns the work. Use Pro for judgment, architecture, risk review, or implementation planning when the decision is consequential enough to justify a browser run.
|
|
11
|
+
|
|
12
|
+
## Running ask-pro
|
|
13
|
+
|
|
14
|
+
This skill folder contains ask-pro itself; nothing else needs installing. In
|
|
15
|
+
the commands below, `ask-pro` stands for:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
node <skill-folder>/scripts/ask-pro.mjs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`<skill-folder>` is the directory containing this `SKILL.md`. Run it from the
|
|
22
|
+
target repo root. The first run after an install or update installs
|
|
23
|
+
dependencies and builds, which can take a few minutes; wait for it. The CLI's
|
|
24
|
+
emitted `resume` and `harvest` commands already contain the full launcher path;
|
|
25
|
+
run them as printed. Node.js 24+, npm, and Chrome are required.
|
|
26
|
+
|
|
27
|
+
## Trigger
|
|
28
|
+
|
|
29
|
+
Use this skill when the user explicitly asks for `$ask-pro`, or when a second opinion would materially reduce risk for:
|
|
30
|
+
|
|
31
|
+
- backend architecture
|
|
32
|
+
- schema or data migrations
|
|
33
|
+
- auth, sessions, permissions, or billing
|
|
34
|
+
- queues, workers, idempotency, caching, scaling, or latency
|
|
35
|
+
- production debugging and observability
|
|
36
|
+
- ambiguous implementation paths where a second opinion would reduce risk
|
|
37
|
+
|
|
38
|
+
Do not use it for trivial syntax fixes, formatting, obvious dependency updates, or small bugs with a clear cause.
|
|
39
|
+
|
|
40
|
+
## Critical Reminders
|
|
41
|
+
|
|
42
|
+
- Long waits are normal. ask-pro can take a super long time to run; do not
|
|
43
|
+
close or kill the browser window/run until at least 3 hours have passed unless
|
|
44
|
+
the CLI has completed, failed, or explicitly asks for human action.
|
|
45
|
+
|
|
46
|
+
## Workflow
|
|
47
|
+
|
|
48
|
+
When invoked:
|
|
49
|
+
|
|
50
|
+
1. Inspect the repo and the relevant files.
|
|
51
|
+
2. Identify the exact decision Pro should answer.
|
|
52
|
+
3. For committed code on GitHub, prefer `--github` and give Pro the repository,
|
|
53
|
+
ref, relevant paths, and question. Let it retrieve the code instead of assembling
|
|
54
|
+
a source bundle. Add a small `--files` bundle only for uncommitted changes,
|
|
55
|
+
local-only evidence, or repository content the connection cannot access.
|
|
56
|
+
4. Write the prompt yourself using the Prompt Shape below.
|
|
57
|
+
5. Run the smallest useful command: `ask-pro --github "<prompt>"` for GitHub
|
|
58
|
+
evidence, or `ask-pro --no-temporary --files "<glob>" "<prompt>"` for a file bundle.
|
|
59
|
+
For multiline prompts, write a temporary prompt file and use
|
|
60
|
+
`ask-pro --no-temporary --prompt-file <path> --files "<glob>"`; do not rely
|
|
61
|
+
on shell multiline quoting.
|
|
62
|
+
6. If auth is required, stop and ask the human to log in in the opened browser.
|
|
63
|
+
7. Read the CLI's compact `ask_pro` record and run the emitted `resume` or
|
|
64
|
+
`harvest` command when that is the next action.
|
|
65
|
+
8. Treat the answer as advisory; turn it into your own plan before editing code.
|
|
66
|
+
|
|
67
|
+
`ask-pro` selects `Latest` when available, otherwise the highest numbered available
|
|
68
|
+
GPT model, then `Pro` intelligence. Do not require a
|
|
69
|
+
different dated model label or a separate thinking-effort control in the prompt
|
|
70
|
+
or workflow.
|
|
71
|
+
|
|
72
|
+
Fresh runs try ChatGPT Temporary Chat by default and automatically fall back to
|
|
73
|
+
normal ChatGPT if the current account/UI does not expose Pro there. For repo
|
|
74
|
+
advisories, large bundles, review rounds, or anything where recovery matters,
|
|
75
|
+
prefer `--no-temporary` from the start. Add `--temporary` only when Temporary
|
|
76
|
+
Chat is required and falling back would be wrong. Temporary Chat is less
|
|
77
|
+
recoverable after browser/tab loss.
|
|
78
|
+
|
|
79
|
+
On Windows, ordinary managed Chrome runs start minimized. First login,
|
|
80
|
+
resume/recovery, and stale-auth paths stay visible or are restored for human
|
|
81
|
+
action. Local managed Chrome guards browser input while Pro is answering.
|
|
82
|
+
If login, MFA, a browser challenge, or incomplete-answer debugging needs human
|
|
83
|
+
attention, ask-pro should restore or retain the browser and emit the next
|
|
84
|
+
action.
|
|
85
|
+
|
|
86
|
+
Do not set `ASK_PRO_AGENT_ID` for ordinary or concurrent use; the shared
|
|
87
|
+
`ask-pro` browser profile under `~/.agents/state/ask-pro/` handles concurrent
|
|
88
|
+
agents automatically. Set `ASK_PRO_AGENT_ID` only when explicitly testing an
|
|
89
|
+
isolated profile or when the human requests a separate browser login. Use a
|
|
90
|
+
stable reusable lowercase id like `review-t1`, not a one-off task slug, because
|
|
91
|
+
each new id creates a new Chrome profile and may require the human to log in
|
|
92
|
+
again. Example:
|
|
93
|
+
`ASK_PRO_AGENT_ID=review-t1 ask-pro ...`.
|
|
94
|
+
|
|
95
|
+
## Prompt Shape
|
|
96
|
+
|
|
97
|
+
Assume Pro has no caller context or local checkout. Include each material fact and
|
|
98
|
+
instruction once: the goal, current state, hard constraints, evidence, success
|
|
99
|
+
criteria, and required output. Do not rely on the agent's conversation context, repo
|
|
100
|
+
folklore, prior ask-pro runs, branch names, or unstated user preferences.
|
|
101
|
+
Keep advisory design consults as plain answer requests. Start advisory prompts
|
|
102
|
+
with:
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
Return final Markdown only, with no preamble or implementation package.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The CLI wrapper already tells Pro to read `CONTEXT.zip` and call out missing or
|
|
109
|
+
conflicting context. Do not repeat that instruction. Add task-specific output
|
|
110
|
+
requirements only when needed. For risk or review work, ask Pro to rank
|
|
111
|
+
findings by severity. For architecture or design consults, ask directly for the
|
|
112
|
+
recommendation and tradeoffs.
|
|
113
|
+
|
|
114
|
+
Use `--artifacts` only when the standard implementation package is needed. The
|
|
115
|
+
wrapper supplies the zip name, standard files, and markdown fallback; add only
|
|
116
|
+
task-specific deliverables not covered by that package. Keep advisory consults
|
|
117
|
+
inline by default.
|
|
118
|
+
Keep bundles focused: source files under review, focused tests, relevant docs,
|
|
119
|
+
known recent changes, and validation status. Avoid whole-repo bundles unless the
|
|
120
|
+
question is explicitly architectural.
|
|
121
|
+
|
|
122
|
+
## Connected GitHub (optional)
|
|
123
|
+
|
|
124
|
+
Prefer `--github` for remote repository evidence. It checks
|
|
125
|
+
the connection before uploading or submitting, and defaults to normal ChatGPT.
|
|
126
|
+
Do not require GitHub for file-based consults. Availability varies by account,
|
|
127
|
+
workspace, model, and ChatGPT experience.
|
|
128
|
+
|
|
129
|
+
If the CLI returns `needs_github_connection` / `connect_github_then_resume`,
|
|
130
|
+
direct the human to the opened GitHub page in ask-pro's browser. They install the
|
|
131
|
+
plugin if needed, complete sign-in, review permissions, and select repositories;
|
|
132
|
+
organization approval may be needed. Wait for them to finish, then run the
|
|
133
|
+
emitted resume command without adding `--github`. The requirement is saved with
|
|
134
|
+
the session and checked again. Do not start a second session or submit through
|
|
135
|
+
the browser yourself. Never collect GitHub credentials or automate authorization.
|
|
136
|
+
|
|
137
|
+
If available, GitHub's **Allow read actions** permission can avoid read approval
|
|
138
|
+
prompts. The human chooses that setting. It controls approval behavior, not the
|
|
139
|
+
underlying permissions of a write-capable app; see
|
|
140
|
+
[app permissions](https://help.openai.com/en/articles/20001495-managing-app-permissions-in-chatgpt).
|
|
141
|
+
The live GitHub plugin advertises write capabilities despite the Help Center's
|
|
142
|
+
read-only description. `--github` requests read-only work; it does not restrict
|
|
143
|
+
the provider's permissions or prove that writes are impossible.
|
|
144
|
+
|
|
145
|
+
If GitHub is already connected, proceed without a setup question or source-file
|
|
146
|
+
bundle. For the consult, name the repository (`owner/repo`), relevant paths, and
|
|
147
|
+
requested branch or commit in the prompt. Explicitly ask Pro
|
|
148
|
+
to answer the specific question. The wrapper requests the connected GitHub app,
|
|
149
|
+
read-only work, citations, the revision inspected, and access gaps. Do not
|
|
150
|
+
infer repository access from ChatGPT login or a successful CLI exit.
|
|
151
|
+
|
|
152
|
+
Keep `--files` for local/uncommitted changes, non-GitHub evidence, and snapshots
|
|
153
|
+
that the app cannot retrieve. Distinguish
|
|
154
|
+
uploaded evidence from GitHub evidence, and ask Pro to flag revision conflicts.
|
|
155
|
+
If the app or requested revision is unavailable, offer a new consult with a
|
|
156
|
+
focused file bundle and
|
|
157
|
+
disclose that limitation. Do not silently switch away from Pro. The CLI submits
|
|
158
|
+
text; it does not select an app in the composer or handle app approval prompts.
|
|
159
|
+
Until a live run demonstrates retrieval, describe this as an available setup
|
|
160
|
+
path, not a verified connection. Confirm material claims against the repo.
|
|
161
|
+
|
|
162
|
+
## Output
|
|
163
|
+
|
|
164
|
+
Normal `ask-pro` stdout is compact TOON-style telemetry. Use `state`, `action`,
|
|
165
|
+
`resume`, and `harvest` to decide the next command. Browser progress may appear
|
|
166
|
+
on stderr and can be ignored unless diagnosing a stuck run.
|
|
167
|
+
|
|
168
|
+
`AWAITING_ANSWERS` / `answer_questions` means Pro is asking native interactive
|
|
169
|
+
questions. Read the printed questions/options or `QUESTIONS.json`, answer from
|
|
170
|
+
the caller's context, and ask the human only for decisions you cannot infer.
|
|
171
|
+
Write UTF-8 JSON `{"requestId":"<request_id>","answers":[2,3,"free text"]}`
|
|
172
|
+
with one answer per question in order, then run the emitted
|
|
173
|
+
`ask-pro --resume <session-id> --answers <answers.json>` command. Numbers select
|
|
174
|
+
1-based choices; strings enter the native free-text field. `--answers -` accepts
|
|
175
|
+
piped JSON. Do not submit defaults or treat the question preamble as the answer.
|
|
176
|
+
Repeat for further question rounds, then harvest the final answer normally.
|
|
177
|
+
|
|
178
|
+
When present, use `profile`, `profile_path`, `chrome`, and `language` only as
|
|
179
|
+
diagnostic hints. They tell you whether the run used the shared profile, an
|
|
180
|
+
isolated agent profile, saved DevTools state, and English browser steering.
|
|
181
|
+
When present, `conversation_url` is a recoverable non-temporary ChatGPT
|
|
182
|
+
conversation URL.
|
|
183
|
+
|
|
184
|
+
`ask-pro --harvest <session-id>` prints the raw markdown answer only for
|
|
185
|
+
answer-bearing states such as `COMPLETED` or `HARVESTED`.
|
|
186
|
+
For pending/incomplete sessions it prints compact status/action instead. For
|
|
187
|
+
sessions run with `--artifacts`, any provided `ask-pro-response.zip` is
|
|
188
|
+
extracted under the session's `pro-output/` directory and described in
|
|
189
|
+
`PRO_OUTPUT_MANIFEST.json`. Inline-default sessions should not expect a zip.
|
|
190
|
+
|
|
191
|
+
`INCOMPLETE_ANSWER` / `stopped_without_answer` means ChatGPT stopped without
|
|
192
|
+
an answer after ask-pro attempted one automatic `continue`. Present the caller
|
|
193
|
+
with the emitted resume command or a full retry of the original request in a new
|
|
194
|
+
chat, and wait for their choice. Do not automatically resume or retry this state.
|
|
195
|
+
An explicit `--resume` allows one more continuation attempt.
|
|
196
|
+
|
|
197
|
+
`COMPLETED` means harvest now; the run browser may already be closed. If the
|
|
198
|
+
state is `INCOMPLETE_ANSWER` / `preamble_without_artifacts`, do not treat
|
|
199
|
+
`ANSWER.md` as final. Try resume/harvest if recoverable; otherwise rerun with
|
|
200
|
+
`--no-temporary`, a tighter bundle, and a more direct prompt.
|
|
201
|
+
|
|
202
|
+
## Commands
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
ask-pro "Review the async billing webhook migration plan and return an implementation plan."
|
|
206
|
+
ask-pro --no-temporary --prompt-file question.md --files src --files tests
|
|
207
|
+
ask-pro --github "In owner/repo at <commit>, inspect src/auth and explain token validation."
|
|
208
|
+
ask-pro --temporary "Review this sensitive migration plan, and fail if Temporary Chat cannot use Pro."
|
|
209
|
+
ask-pro --no-temporary "Review this in normal ChatGPT instead of Temporary Chat."
|
|
210
|
+
ask-pro --prompt-file question.md --files .\src
|
|
211
|
+
ask-pro --artifacts --prompt-file implementation-plan.md --files src
|
|
212
|
+
ask-pro --files "src/api/stripe/**" --files "prisma/**" --files "src/lib/billing/**" \
|
|
213
|
+
"Review whether this Stripe webhook flow should use a queue or transactional outbox."
|
|
214
|
+
ask-pro --dry-run "Prepare the Pro handoff but do not open the browser."
|
|
215
|
+
ask-pro --resume <session-id>
|
|
216
|
+
ask-pro --harvest <session-id>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`--files` must be inside the target repo. Use repo-relative paths.
|
|
220
|
+
|
|
221
|
+
## Safety
|
|
222
|
+
|
|
223
|
+
Never ask for, read, store, type, or log passwords, MFA codes, recovery codes, session cookies, or raw auth tokens.
|
|
224
|
+
|
|
225
|
+
Browser auth is human-controlled. Continue only after the human says the ChatGPT composer is visible.
|
|
226
|
+
|
|
227
|
+
After submit, avoid interacting with any retained Chrome run window while Pro is
|
|
228
|
+
thinking. ask-pro guards input after submit, but the safest agent behavior is to
|
|
229
|
+
let the run finish or resume/harvest from CLI telemetry.
|
|
@@ -5,7 +5,7 @@ import { AssistantStoppedError, BrowserAutomationError } from "../browser/errors
|
|
|
5
5
|
import { AssistantQuestionsError } from "../browser/actions/questions.js";
|
|
6
6
|
import { readAskProQuestions, parseQuestionAnswers, writeAskProQuestions } from "./questions.js";
|
|
7
7
|
import { GitHubConnectionRequiredError } from "../browser/actions/github.js";
|
|
8
|
-
import {
|
|
8
|
+
import { askProAgentIdForManagedBrowserProfileDir, askProBrowserProfileDirForAgentId, isAskProManagedBrowserProfileDir, resolveAskProAgentId, } from "../browser/profilePaths.js";
|
|
9
9
|
import { runBrowserMode } from "../browserMode.js";
|
|
10
10
|
import { closeTab } from "../browser/chromeLifecycle.js";
|
|
11
11
|
import { resumeBrowserSession } from "../browser/reattach.js";
|
|
@@ -28,7 +28,7 @@ async function runAskProBrowserSessionWithLease({ cwd, sessionId, temporary: req
|
|
|
28
28
|
const temporary = requestedTemporary ?? (sessionStatus.github ? false : undefined);
|
|
29
29
|
const artifactsRequested = sessionStatus.artifacts === true;
|
|
30
30
|
const agentId = agentIdOverride !== undefined ? agentIdOverride : resolveAskProAgentId();
|
|
31
|
-
const browserProfile = browserProfileDir ?? (
|
|
31
|
+
const browserProfile = browserProfileDir ?? askProBrowserProfileDirForAgentId(agentId);
|
|
32
32
|
const metadata = await readBrowserMetadata(paths.browser).catch(() => null);
|
|
33
33
|
const chatgptUrl = chatgptUrlOverride ??
|
|
34
34
|
(temporary === true
|
|
@@ -317,7 +317,7 @@ async function resumeAskProBrowserSessionWithLease({ cwd, sessionId, temporary,
|
|
|
317
317
|
: effectiveTemporary === false
|
|
318
318
|
? ASK_PRO_CHATGPT_URL
|
|
319
319
|
: (metadata.url ?? ASK_PRO_TEMPORARY_CHATGPT_URL);
|
|
320
|
-
const fallbackProfile =
|
|
320
|
+
const fallbackProfile = resolveResumeBrowserProfile(metadata);
|
|
321
321
|
if (sessionStatus.status === "NEEDS_GITHUB_CONNECTION" ||
|
|
322
322
|
(sessionStatus.github && metadata.runtime?.githubCheckPending === true)) {
|
|
323
323
|
await closeSessionBrowserTab(cwd, sessionId, logger);
|
|
@@ -716,41 +716,26 @@ async function writeTerminalBrowserMetadata(cwd, sessionId, status, reason) {
|
|
|
716
716
|
},
|
|
717
717
|
});
|
|
718
718
|
}
|
|
719
|
-
|
|
719
|
+
function resolveResumeBrowserProfile(metadata) {
|
|
720
720
|
const agentProfile = resolveStoredAgentProfile(metadata.agentId);
|
|
721
721
|
const profileDir = metadata.profileDir;
|
|
722
722
|
const profileAgentId = profileDir ? askProAgentIdForManagedBrowserProfileDir(profileDir) : null;
|
|
723
|
-
const legacyAgentId = profileDir ? askProAgentIdForLegacyBrowserProfileDir(profileDir) : null;
|
|
724
723
|
if (profileAgentId && profileAgentId !== metadata.agentId) {
|
|
725
724
|
throw new Error("Stored ask-pro agent profile does not match stored agent id.");
|
|
726
725
|
}
|
|
727
|
-
if (legacyAgentId && legacyAgentId !== metadata.agentId) {
|
|
728
|
-
throw new Error("Stored ask-pro agent profile does not match stored agent id.");
|
|
729
|
-
}
|
|
730
726
|
if (profileAgentId && agentProfile) {
|
|
731
727
|
return profileDir;
|
|
732
728
|
}
|
|
733
|
-
if (legacyAgentId && agentProfile) {
|
|
734
|
-
return ensureAskProBrowserProfileDir(metadata.agentId);
|
|
735
|
-
}
|
|
736
729
|
if (profileDir &&
|
|
737
730
|
!profileAgentId &&
|
|
738
|
-
!legacyAgentId &&
|
|
739
731
|
!agentProfile &&
|
|
740
732
|
isAskProManagedBrowserProfileDir(profileDir)) {
|
|
741
|
-
return
|
|
742
|
-
}
|
|
743
|
-
if (profileDir &&
|
|
744
|
-
!profileAgentId &&
|
|
745
|
-
!legacyAgentId &&
|
|
746
|
-
!agentProfile &&
|
|
747
|
-
isLegacyAskProManagedBrowserProfileDir(profileDir)) {
|
|
748
|
-
return ensureAskProBrowserProfileDir(null);
|
|
733
|
+
return askProBrowserProfileDirForAgentId(null);
|
|
749
734
|
}
|
|
750
735
|
if (profileDir) {
|
|
751
736
|
throw new Error("Stored ask-pro profile path is invalid.");
|
|
752
737
|
}
|
|
753
|
-
return
|
|
738
|
+
return askProBrowserProfileDirForAgentId(metadata.agentId);
|
|
754
739
|
}
|
|
755
740
|
function resolveStoredAgentProfile(agentId) {
|
|
756
741
|
if (!agentId)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatElapsed } from "../format.js";
|
|
2
|
-
import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR } from "../constants.js";
|
|
2
|
+
import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, STOP_BUTTON_SELECTOR, } from "../constants.js";
|
|
3
3
|
const THINKING_STALE_HINT_MS = 10 * 60_000;
|
|
4
4
|
export function startThinkingStatusMonitor(Runtime, logger, options = {}) {
|
|
5
5
|
const intervalMs = resolveThinkingStatusInterval(options.intervalMs);
|
|
@@ -200,13 +200,15 @@ function buildThinkingStatusExpression() {
|
|
|
200
200
|
[node.textContent, node.getAttribute?.('aria-label'), node.getAttribute?.('title')]
|
|
201
201
|
.some((label) => normalize(label) === expected),
|
|
202
202
|
);
|
|
203
|
-
|
|
203
|
+
const stopActive = hasControl('stop answering') ||
|
|
204
|
+
Array.from(document.querySelectorAll(${JSON.stringify(STOP_BUTTON_SELECTOR)})).some(isVisible);
|
|
205
|
+
if (hasControl('answer now') && stopActive) {
|
|
204
206
|
return {
|
|
205
207
|
message: 'pro gate active',
|
|
206
208
|
source: 'inline',
|
|
207
209
|
};
|
|
208
210
|
}
|
|
209
|
-
if (
|
|
211
|
+
if (stopActive) {
|
|
210
212
|
return { message: 'active', source: 'inline' };
|
|
211
213
|
}
|
|
212
214
|
const looksLikeThinking = (node) => {
|
|
@@ -1,144 +1,13 @@
|
|
|
1
|
-
import { createHash
|
|
2
|
-
import fs from "node:fs/promises";
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import { acquireProfileRunLock, isBrowserProfileInUse, isProcessAlive, releaseProfileRunLock, } from "./profileState.js";
|
|
6
4
|
const RESOLVED_AGENT_ID_PATTERN = /^[a-z0-9._-]+-[a-f0-9]{10}$/;
|
|
7
|
-
const MIGRATION_MARKER = ".ask-pro-profile-migration";
|
|
8
5
|
export function defaultAskProBrowserProfileDir() {
|
|
9
6
|
return askProBrowserProfileDirForAgentId(null);
|
|
10
7
|
}
|
|
11
8
|
export function askProBrowserProfileDirForAgentId(agentId, env = process.env) {
|
|
12
9
|
return profileDirUnder(askProStateDir(env), agentId);
|
|
13
10
|
}
|
|
14
|
-
export function legacyAskProBrowserProfileDirForAgentId(agentId, homeDir = os.homedir()) {
|
|
15
|
-
return profileDirUnder(path.join(homeDir, ".agents", "skills", "ask-pro"), agentId);
|
|
16
|
-
}
|
|
17
|
-
export async function ensureAskProBrowserProfileDir(agentId, options = {}) {
|
|
18
|
-
const target = askProBrowserProfileDirForAgentId(agentId, options.env);
|
|
19
|
-
const legacy = legacyAskProBrowserProfileDirForAgentId(agentId, options.homeDir);
|
|
20
|
-
const [targetExists, legacyExists] = await Promise.all([exists(target), exists(legacy)]);
|
|
21
|
-
if (targetExists && legacyExists) {
|
|
22
|
-
const recordedIdentity = await retainedLegacyIdentity(target);
|
|
23
|
-
if (recordedIdentity) {
|
|
24
|
-
const currentIdentity = await legacyProfileIdentity(legacy);
|
|
25
|
-
if (currentIdentity && sameLegacyIdentity(currentIdentity, recordedIdentity)) {
|
|
26
|
-
if (await waitForConcurrentMigration(target, legacy))
|
|
27
|
-
return target;
|
|
28
|
-
if (await isBrowserProfileInUse(legacy)) {
|
|
29
|
-
throw new Error("Legacy ask-pro browser profile is in use; retry after its browser run exits.");
|
|
30
|
-
}
|
|
31
|
-
return target;
|
|
32
|
-
}
|
|
33
|
-
throw new Error(`Both current and legacy ask-pro browser profiles exist; refusing to merge ${legacy} into ${target}.`);
|
|
34
|
-
}
|
|
35
|
-
if (await waitForConcurrentMigration(target, legacy))
|
|
36
|
-
return target;
|
|
37
|
-
if ((await exists(target)) && !(await exists(legacy)))
|
|
38
|
-
return target;
|
|
39
|
-
throw new Error(`Both current and legacy ask-pro browser profiles exist; refusing to merge ${legacy} into ${target}.`);
|
|
40
|
-
}
|
|
41
|
-
if (targetExists || !legacyExists)
|
|
42
|
-
return target;
|
|
43
|
-
if (await isBrowserProfileInUse(legacy)) {
|
|
44
|
-
throw new Error("Legacy ask-pro browser profile is in use; retry after its browser run exits.");
|
|
45
|
-
}
|
|
46
|
-
let migrationLock;
|
|
47
|
-
try {
|
|
48
|
-
migrationLock = await acquireProfileRunLock(legacy, {
|
|
49
|
-
timeoutMs: 300_000,
|
|
50
|
-
pollMs: 100,
|
|
51
|
-
requireExistingProfile: true,
|
|
52
|
-
staleLockMode: "fail",
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
if ((await exists(target)) && !(await exists(legacy)))
|
|
57
|
-
return target;
|
|
58
|
-
throw new Error("Legacy ask-pro browser profile could not be claimed; retry after other browser runs exit.", { cause: error });
|
|
59
|
-
}
|
|
60
|
-
if (!migrationLock)
|
|
61
|
-
throw new Error("Could not claim the legacy ask-pro browser profile.");
|
|
62
|
-
let releasePath = migrationLock.path;
|
|
63
|
-
try {
|
|
64
|
-
if (await matchesRetainedLegacy(target, legacy))
|
|
65
|
-
return target;
|
|
66
|
-
if (await isBrowserProfileInUse(legacy, { ignoreLockId: migrationLock.lockId })) {
|
|
67
|
-
throw new Error("Legacy ask-pro browser profile is in use; retry after its browser run exits.");
|
|
68
|
-
}
|
|
69
|
-
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
70
|
-
let identityRequired = false;
|
|
71
|
-
try {
|
|
72
|
-
await fs.rename(legacy, target);
|
|
73
|
-
releasePath = path.join(target, path.basename(migrationLock.path));
|
|
74
|
-
return target;
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
const code = error.code;
|
|
78
|
-
if (code !== "EXDEV" && !isSharingDenied(error)) {
|
|
79
|
-
if ((await exists(target)) && !(await exists(legacy)))
|
|
80
|
-
return target;
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
identityRequired = code !== "EXDEV";
|
|
84
|
-
}
|
|
85
|
-
const legacyIdentity = await legacyProfileIdentity(legacy);
|
|
86
|
-
if (identityRequired && !legacyIdentity) {
|
|
87
|
-
throw new Error("Legacy ask-pro browser profile identity is unavailable; refusing to copy.");
|
|
88
|
-
}
|
|
89
|
-
const staging = `${target}.migrating-${process.pid}-${randomUUID()}`;
|
|
90
|
-
try {
|
|
91
|
-
await fs.cp(legacy, staging, { recursive: true, errorOnExist: true });
|
|
92
|
-
await fs.writeFile(path.join(staging, MIGRATION_MARKER), JSON.stringify({
|
|
93
|
-
pid: process.pid,
|
|
94
|
-
createdAt: Date.now(),
|
|
95
|
-
migrationLockId: migrationLock.lockId,
|
|
96
|
-
...(legacyIdentity ? { legacyIdentity } : {}),
|
|
97
|
-
}));
|
|
98
|
-
await fs.rename(staging, target);
|
|
99
|
-
releasePath = path.join(target, path.basename(migrationLock.path));
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
await fs.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
103
|
-
if ((await exists(target)) && !(await exists(legacy)))
|
|
104
|
-
return target;
|
|
105
|
-
if (await matchesRetainedLegacy(target, legacy))
|
|
106
|
-
return target;
|
|
107
|
-
if (await waitForConcurrentMigration(target, legacy))
|
|
108
|
-
return target;
|
|
109
|
-
if ((await exists(target)) && !(await exists(legacy)))
|
|
110
|
-
return target;
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
await fs.rm(legacy, { recursive: true });
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
await releaseProfileRunLock(migrationLock.path, migrationLock.lockId);
|
|
118
|
-
if (isSharingDenied(error) && legacyIdentity)
|
|
119
|
-
return target;
|
|
120
|
-
await fs.rm(path.join(target, MIGRATION_MARKER), { force: true });
|
|
121
|
-
if (isSharingDenied(error)) {
|
|
122
|
-
throw new Error("Legacy ask-pro browser profile could not be removed and has no reliable identity; refusing to retain it.", { cause: error });
|
|
123
|
-
}
|
|
124
|
-
throw error;
|
|
125
|
-
}
|
|
126
|
-
await fs.rm(path.join(target, MIGRATION_MARKER), { force: true });
|
|
127
|
-
return target;
|
|
128
|
-
}
|
|
129
|
-
finally {
|
|
130
|
-
await releaseProfileRunLock(releasePath, migrationLock.lockId);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
export function askProAgentIdForLegacyBrowserProfileDir(profileDir) {
|
|
134
|
-
return agentIdForProfileDir(profileDir, path.join(os.homedir(), ".agents", "skills", "ask-pro"));
|
|
135
|
-
}
|
|
136
|
-
export function isLegacyAskProManagedBrowserProfileDir(profileDir) {
|
|
137
|
-
const resolved = normalizeProfilePath(profileDir);
|
|
138
|
-
if (resolved === normalizeProfilePath(legacyAskProBrowserProfileDirForAgentId(null)))
|
|
139
|
-
return true;
|
|
140
|
-
return askProAgentIdForLegacyBrowserProfileDir(profileDir) !== null;
|
|
141
|
-
}
|
|
142
11
|
function profileDirUnder(root, agentId) {
|
|
143
12
|
if (!agentId)
|
|
144
13
|
return path.join(root, "browser-profile");
|
|
@@ -176,87 +45,8 @@ function agentIdForProfileDir(profileDir, stateRoot) {
|
|
|
176
45
|
return RESOLVED_AGENT_ID_PATTERN.test(agentId) ? agentId : null;
|
|
177
46
|
}
|
|
178
47
|
function askProStateDir(env = process.env) {
|
|
179
|
-
const
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
async function exists(filePath) {
|
|
183
|
-
return fs
|
|
184
|
-
.stat(filePath)
|
|
185
|
-
.then(() => true)
|
|
186
|
-
.catch((error) => {
|
|
187
|
-
if (error.code === "ENOENT")
|
|
188
|
-
return false;
|
|
189
|
-
throw error;
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
async function legacyProfileIdentity(profileDir) {
|
|
193
|
-
const info = await fs.stat(profileDir, { bigint: true }).catch(() => null);
|
|
194
|
-
if (!info?.isDirectory() || info.dev <= 0n || info.ino <= 0n || info.birthtimeNs <= 0n) {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
dev: info.dev.toString(),
|
|
199
|
-
ino: info.ino.toString(),
|
|
200
|
-
birthtimeNs: info.birthtimeNs.toString(),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
async function retainedLegacyIdentity(target) {
|
|
204
|
-
const recorded = await fs
|
|
205
|
-
.readFile(path.join(target, MIGRATION_MARKER), "utf8")
|
|
206
|
-
.then((raw) => JSON.parse(raw).legacyIdentity)
|
|
207
|
-
.catch(() => null);
|
|
208
|
-
if (!recorded ||
|
|
209
|
-
![recorded.dev, recorded.ino, recorded.birthtimeNs].every((value) => /^[1-9]\d*$/.test(value))) {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
return recorded;
|
|
213
|
-
}
|
|
214
|
-
async function matchesRetainedLegacy(target, legacy) {
|
|
215
|
-
const [recorded, current] = await Promise.all([
|
|
216
|
-
retainedLegacyIdentity(target),
|
|
217
|
-
legacyProfileIdentity(legacy),
|
|
218
|
-
]);
|
|
219
|
-
return recorded !== null && current !== null && sameLegacyIdentity(recorded, current);
|
|
220
|
-
}
|
|
221
|
-
function sameLegacyIdentity(left, right) {
|
|
222
|
-
return left.dev === right.dev && left.ino === right.ino && left.birthtimeNs === right.birthtimeNs;
|
|
223
|
-
}
|
|
224
|
-
function isSharingDenied(error) {
|
|
225
|
-
return ["EACCES", "EBUSY", "EPERM"].includes(error.code ?? "");
|
|
226
|
-
}
|
|
227
|
-
async function waitForConcurrentMigration(target, legacy) {
|
|
228
|
-
const marker = path.join(target, MIGRATION_MARKER);
|
|
229
|
-
const owner = await fs
|
|
230
|
-
.readFile(marker, "utf8")
|
|
231
|
-
.then((raw) => JSON.parse(raw))
|
|
232
|
-
.catch(() => null);
|
|
233
|
-
if (!owner?.pid ||
|
|
234
|
-
!Number.isFinite(owner.pid) ||
|
|
235
|
-
!owner.createdAt ||
|
|
236
|
-
!Number.isFinite(owner.createdAt) ||
|
|
237
|
-
!owner.migrationLockId) {
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
while (isProcessAlive(owner.pid) &&
|
|
241
|
-
Date.now() - owner.createdAt < 300_000 &&
|
|
242
|
-
(await legacyHasMigrationLock(legacy, owner.migrationLockId))) {
|
|
243
|
-
if (!(await exists(legacy))) {
|
|
244
|
-
await fs.rm(marker, { force: true }).catch(() => undefined);
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
248
|
-
}
|
|
249
|
-
if (await exists(legacy))
|
|
250
|
-
return false;
|
|
251
|
-
await fs.rm(marker, { force: true }).catch(() => undefined);
|
|
252
|
-
return exists(target);
|
|
253
|
-
}
|
|
254
|
-
async function legacyHasMigrationLock(legacy, lockId) {
|
|
255
|
-
const activeLockId = await fs
|
|
256
|
-
.readFile(path.join(legacy, "ask-pro-automation.lock"), "utf8")
|
|
257
|
-
.then((raw) => JSON.parse(raw).lockId)
|
|
258
|
-
.catch(() => null);
|
|
259
|
-
return activeLockId === lockId;
|
|
48
|
+
const override = env.ASK_PRO_STATE_DIR?.trim();
|
|
49
|
+
return override ? path.resolve(override) : path.join(os.homedir(), ".agents", "state", "ask-pro");
|
|
260
50
|
}
|
|
261
51
|
export function resolveAskProAgentId(env = process.env) {
|
|
262
52
|
const value = env.ASK_PRO_AGENT_ID;
|
|
@@ -168,42 +168,6 @@ function parseProfileRunLock(payload) {
|
|
|
168
168
|
return null;
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
-
export async function isBrowserProfileInUse(userDataDir, options = {}) {
|
|
172
|
-
const lockPath = path.join(userDataDir, ASK_PRO_PROFILE_LOCK_FILENAME);
|
|
173
|
-
let lockPayload;
|
|
174
|
-
try {
|
|
175
|
-
lockPayload = await readFile(lockPath, "utf8");
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
if (error.code !== "ENOENT")
|
|
179
|
-
return true;
|
|
180
|
-
lockPayload = null;
|
|
181
|
-
}
|
|
182
|
-
let lock = parseProfileRunLock(lockPayload);
|
|
183
|
-
if (lockPayload !== null && !lock) {
|
|
184
|
-
await delay(200);
|
|
185
|
-
try {
|
|
186
|
-
lockPayload = await readFile(lockPath, "utf8");
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
if (error.code !== "ENOENT")
|
|
190
|
-
return true;
|
|
191
|
-
lockPayload = null;
|
|
192
|
-
}
|
|
193
|
-
lock = parseProfileRunLock(lockPayload);
|
|
194
|
-
}
|
|
195
|
-
if (lock && lock.lockId !== options.ignoreLockId && isProcessAlive(lock.pid))
|
|
196
|
-
return true;
|
|
197
|
-
const pid = await readChromePid(userDataDir);
|
|
198
|
-
const port = await readDevToolsPort(userDataDir);
|
|
199
|
-
const [owner, devTools] = await Promise.all([
|
|
200
|
-
isChromeUsingUserDataDir(userDataDir),
|
|
201
|
-
port ? verifyDevToolsReachable({ port, attempts: 1 }) : null,
|
|
202
|
-
]);
|
|
203
|
-
if (owner === true || (pid && isProcessAlive(pid) && devTools?.ok))
|
|
204
|
-
return true;
|
|
205
|
-
return owner === null;
|
|
206
|
-
}
|
|
207
171
|
export async function acquireProfileRunLock(userDataDir, options) {
|
|
208
172
|
const timeoutMs = options.timeoutMs;
|
|
209
173
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
@@ -224,12 +188,7 @@ export async function acquireProfileRunLock(userDataDir, options) {
|
|
|
224
188
|
createdAt: new Date().toISOString(),
|
|
225
189
|
sessionId: options.sessionId,
|
|
226
190
|
};
|
|
227
|
-
|
|
228
|
-
await stat(userDataDir);
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
232
|
-
}
|
|
191
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
233
192
|
await writeFile(lockPath, JSON.stringify(payload), { encoding: "utf8", flag: "wx" });
|
|
234
193
|
options.logger?.(`Acquired ask-pro profile lock at ${lockPath}`);
|
|
235
194
|
return {
|
|
@@ -249,18 +208,12 @@ export async function acquireProfileRunLock(userDataDir, options) {
|
|
|
249
208
|
await delay(200);
|
|
250
209
|
existing = parseProfileRunLock(await readFile(lockPath, "utf8").catch(() => null));
|
|
251
210
|
if (!existing) {
|
|
252
|
-
if (options.staleLockMode === "fail") {
|
|
253
|
-
throw new Error("ask-pro profile lock is unreadable; refusing stale lock removal.");
|
|
254
|
-
}
|
|
255
211
|
options.logger?.("ask-pro profile lock unreadable; deleting lockfile.");
|
|
256
212
|
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
257
213
|
continue;
|
|
258
214
|
}
|
|
259
215
|
}
|
|
260
216
|
if (!existing || !isProcessAlive(existing.pid)) {
|
|
261
|
-
if (options.staleLockMode === "fail") {
|
|
262
|
-
throw new Error("ask-pro profile lock owner is not alive; refusing stale lock removal.");
|
|
263
|
-
}
|
|
264
217
|
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
265
218
|
continue;
|
|
266
219
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ask-pro",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
4
4
|
"description": "Browser-backed ChatGPT Pro escalation for hard engineering questions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -51,10 +51,9 @@
|
|
|
51
51
|
"dist/src/browser/providers/chatgptDomProvider.js",
|
|
52
52
|
"dist/src/browserMode.js",
|
|
53
53
|
"dist/src/version.js",
|
|
54
|
-
"scripts/refresh-local-plugin.mjs",
|
|
55
|
-
"scripts/refresh-local-plugin.ps1",
|
|
56
54
|
".codex-plugin/plugin.json",
|
|
57
55
|
"assets/ask-pro_logo.png",
|
|
56
|
+
"SKILL.md",
|
|
58
57
|
"skills/ask-pro/SKILL.md",
|
|
59
58
|
"README.md",
|
|
60
59
|
"LICENSE"
|
|
@@ -72,7 +71,6 @@
|
|
|
72
71
|
"format:diff": "oxfmt --write . && git --no-pager diff",
|
|
73
72
|
"lint": "pnpm run typecheck && oxlint",
|
|
74
73
|
"lint:fix": "oxlint --fix && pnpm run format",
|
|
75
|
-
"plugin:refresh": "pnpm run build && node scripts/refresh-local-plugin.mjs",
|
|
76
74
|
"test": "pnpm run test:ask-pro",
|
|
77
75
|
"test:ask-pro": "vitest run tests/ask-pro tests/browser",
|
|
78
76
|
"test:coverage": "vitest run --coverage",
|
package/skills/ask-pro/SKILL.md
CHANGED
|
@@ -5,232 +5,7 @@ description: Escalate hard engineering questions to ChatGPT Pro through browser
|
|
|
5
5
|
|
|
6
6
|
# $ask-pro
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Trigger
|
|
13
|
-
|
|
14
|
-
Use this skill when the user explicitly asks for `$ask-pro`, or when a second opinion would materially reduce risk for:
|
|
15
|
-
|
|
16
|
-
- backend architecture
|
|
17
|
-
- schema or data migrations
|
|
18
|
-
- auth, sessions, permissions, or billing
|
|
19
|
-
- queues, workers, idempotency, caching, scaling, or latency
|
|
20
|
-
- production debugging and observability
|
|
21
|
-
- ambiguous implementation paths where a second opinion would reduce risk
|
|
22
|
-
|
|
23
|
-
Do not use it for trivial syntax fixes, formatting, obvious dependency updates, or small bugs with a clear cause.
|
|
24
|
-
|
|
25
|
-
## Critical Reminders
|
|
26
|
-
|
|
27
|
-
- Long waits are normal. ask-pro can take a super long time to run; do not
|
|
28
|
-
close or kill the browser window/run until at least 3 hours have passed unless
|
|
29
|
-
the CLI has completed, failed, or explicitly asks for human action.
|
|
30
|
-
|
|
31
|
-
## Workflow
|
|
32
|
-
|
|
33
|
-
When invoked:
|
|
34
|
-
|
|
35
|
-
1. Inspect the repo and the relevant files.
|
|
36
|
-
2. Identify the exact decision Pro should answer.
|
|
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.
|
|
41
|
-
4. Write the prompt yourself using the Prompt Shape below.
|
|
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.
|
|
44
|
-
For multiline prompts, write a temporary prompt file and use
|
|
45
|
-
`ask-pro --no-temporary --prompt-file <path> --files "<glob>"`; do not rely
|
|
46
|
-
on shell multiline quoting.
|
|
47
|
-
If installed through the skills CLI and `ask-pro` is not on `PATH`, install
|
|
48
|
-
the standalone CLI from the [README](https://github.com/JJLiebig/ask-pro#skills-cli-codex-and-other-supported-agents)
|
|
49
|
-
first. For Codex plugin installs without `ask-pro` on `PATH`, use the cached
|
|
50
|
-
plugin runner. Locate it under the installed plugin cache,
|
|
51
|
-
usually
|
|
52
|
-
`~/.codex/plugins/cache/<marketplace-name>/ask-pro/<version>/scripts/run-cached-cli.mjs`,
|
|
53
|
-
then call it with:
|
|
54
|
-
`node <cached-runner> -- --cwd <target-repo-root> --no-temporary --prompt-file <path> --files "<repo-relative-glob>"`.
|
|
55
|
-
On Git marketplace installs, the first cached-runner call may bootstrap the
|
|
56
|
-
content-addressed runtime under `$CODEX_HOME/plugin-runtimes/ask-pro/` by
|
|
57
|
-
installing dependencies and building `dist`; wait for that to finish. The
|
|
58
|
-
installed plugin cache stays immutable.
|
|
59
|
-
6. If auth is required, stop and ask the human to log in in the opened browser.
|
|
60
|
-
7. Read the CLI's compact `ask_pro` record and run the emitted `resume` or
|
|
61
|
-
`harvest` command when that is the next action.
|
|
62
|
-
8. Treat the answer as advisory; turn it into your own plan before editing code.
|
|
63
|
-
|
|
64
|
-
`ask-pro` selects `Latest` when available, otherwise the highest numbered available
|
|
65
|
-
GPT model, then `Pro` intelligence. Do not require a
|
|
66
|
-
different dated model label or a separate thinking-effort control in the prompt
|
|
67
|
-
or workflow.
|
|
68
|
-
|
|
69
|
-
Fresh runs try ChatGPT Temporary Chat by default and automatically fall back to
|
|
70
|
-
normal ChatGPT if the current account/UI does not expose Pro there. For repo
|
|
71
|
-
advisories, large bundles, review rounds, or anything where recovery matters,
|
|
72
|
-
prefer `--no-temporary` from the start. Add `--temporary` only when Temporary
|
|
73
|
-
Chat is required and falling back would be wrong. Temporary Chat is less
|
|
74
|
-
recoverable after browser/tab loss.
|
|
75
|
-
|
|
76
|
-
On Windows, ordinary managed Chrome runs start minimized. First login,
|
|
77
|
-
resume/recovery, and stale-auth paths stay visible or are restored for human
|
|
78
|
-
action. Local managed Chrome guards browser input while Pro is answering.
|
|
79
|
-
If login, MFA, a browser challenge, or incomplete-answer debugging needs human
|
|
80
|
-
attention, ask-pro should restore or retain the browser and emit the next
|
|
81
|
-
action.
|
|
82
|
-
|
|
83
|
-
Do not set `ASK_PRO_AGENT_ID` for ordinary or concurrent use; the shared
|
|
84
|
-
`ask-pro` browser profile under `$CODEX_HOME/state/ask-pro/` handles concurrent
|
|
85
|
-
agents automatically. Set `ASK_PRO_AGENT_ID` only when explicitly testing an
|
|
86
|
-
isolated profile or when the human requests a separate browser login. Use a
|
|
87
|
-
stable reusable lowercase id like `review-t1`, not a one-off task slug, because
|
|
88
|
-
each new id creates a new Chrome profile and may require the human to log in
|
|
89
|
-
again. Example:
|
|
90
|
-
`ASK_PRO_AGENT_ID=review-t1 ask-pro ...`.
|
|
91
|
-
|
|
92
|
-
## Prompt Shape
|
|
93
|
-
|
|
94
|
-
Assume Pro has no caller context or local checkout. Include each material fact and
|
|
95
|
-
instruction once: the goal, current state, hard constraints, evidence, success
|
|
96
|
-
criteria, and required output. Do not rely on the agent's conversation context, repo
|
|
97
|
-
folklore, prior ask-pro runs, branch names, or unstated user preferences.
|
|
98
|
-
Keep advisory design consults as plain answer requests. Start advisory prompts
|
|
99
|
-
with:
|
|
100
|
-
|
|
101
|
-
```text
|
|
102
|
-
Return final Markdown only, with no preamble or implementation package.
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
The CLI wrapper already tells Pro to read `CONTEXT.zip` and call out missing or
|
|
106
|
-
conflicting context. Do not repeat that instruction. Add task-specific output
|
|
107
|
-
requirements only when needed. For risk or review work, ask Pro to rank
|
|
108
|
-
findings by severity. For architecture or design consults, ask directly for the
|
|
109
|
-
recommendation and tradeoffs.
|
|
110
|
-
|
|
111
|
-
Use `--artifacts` only when the standard implementation package is needed. The
|
|
112
|
-
wrapper supplies the zip name, standard files, and markdown fallback; add only
|
|
113
|
-
task-specific deliverables not covered by that package. Keep advisory consults
|
|
114
|
-
inline by default.
|
|
115
|
-
Keep bundles focused: source files under review, focused tests, relevant docs,
|
|
116
|
-
known recent changes, and validation status. Avoid whole-repo bundles unless the
|
|
117
|
-
question is explicitly architectural.
|
|
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
|
-
|
|
159
|
-
## Output
|
|
160
|
-
|
|
161
|
-
Normal `ask-pro` stdout is compact TOON-style telemetry. Use `state`, `action`,
|
|
162
|
-
`resume`, and `harvest` to decide the next command. Browser progress may appear
|
|
163
|
-
on stderr and can be ignored unless diagnosing a stuck run.
|
|
164
|
-
|
|
165
|
-
`AWAITING_ANSWERS` / `answer_questions` means Pro is asking native interactive
|
|
166
|
-
questions. Read the printed questions/options or `QUESTIONS.json`, answer from
|
|
167
|
-
the caller's context, and ask the human only for decisions you cannot infer.
|
|
168
|
-
Write UTF-8 JSON `{"requestId":"<request_id>","answers":[2,3,"free text"]}`
|
|
169
|
-
with one answer per question in order, then run the emitted
|
|
170
|
-
`ask-pro --resume <session-id> --answers <answers.json>` command. Numbers select
|
|
171
|
-
1-based choices; strings enter the native free-text field. `--answers -` accepts
|
|
172
|
-
piped JSON. Do not submit defaults or treat the question preamble as the answer.
|
|
173
|
-
Repeat for further question rounds, then harvest the final answer normally.
|
|
174
|
-
|
|
175
|
-
When present, use `profile`, `profile_path`, `chrome`, and `language` only as
|
|
176
|
-
diagnostic hints. They tell you whether the run used the shared profile, an
|
|
177
|
-
isolated agent profile, saved DevTools state, and English browser steering.
|
|
178
|
-
When present, `conversation_url` is a recoverable non-temporary ChatGPT
|
|
179
|
-
conversation URL.
|
|
180
|
-
|
|
181
|
-
`ask-pro --harvest <session-id>` prints the raw markdown answer only for
|
|
182
|
-
answer-bearing states such as `COMPLETED` or `HARVESTED`.
|
|
183
|
-
For pending/incomplete sessions it prints compact status/action instead. For
|
|
184
|
-
sessions run with `--artifacts`, any provided `ask-pro-response.zip` is
|
|
185
|
-
extracted under the session's `pro-output/` directory and described in
|
|
186
|
-
`PRO_OUTPUT_MANIFEST.json`. Inline-default sessions should not expect a zip.
|
|
187
|
-
|
|
188
|
-
`INCOMPLETE_ANSWER` / `stopped_without_answer` means ChatGPT stopped without
|
|
189
|
-
an answer after ask-pro attempted one automatic `continue`. Present the caller
|
|
190
|
-
with the emitted resume command or a full retry of the original request in a new
|
|
191
|
-
chat, and wait for their choice. Do not automatically resume or retry this state.
|
|
192
|
-
An explicit `--resume` allows one more continuation attempt.
|
|
193
|
-
|
|
194
|
-
`COMPLETED` means harvest now; the run browser may already be closed. If the
|
|
195
|
-
state is `INCOMPLETE_ANSWER` / `preamble_without_artifacts`, do not treat
|
|
196
|
-
`ANSWER.md` as final. Try resume/harvest if recoverable; otherwise rerun with
|
|
197
|
-
`--no-temporary`, a tighter bundle, and a more direct prompt.
|
|
198
|
-
|
|
199
|
-
## Commands
|
|
200
|
-
|
|
201
|
-
```bash
|
|
202
|
-
ask-pro "Review the async billing webhook migration plan and return an implementation plan."
|
|
203
|
-
ask-pro --no-temporary --prompt-file question.md --files src --files tests
|
|
204
|
-
ask-pro --github "In owner/repo at <commit>, inspect src/auth and explain token validation."
|
|
205
|
-
ask-pro --temporary "Review this sensitive migration plan, and fail if Temporary Chat cannot use Pro."
|
|
206
|
-
ask-pro --no-temporary "Review this in normal ChatGPT instead of Temporary Chat."
|
|
207
|
-
ask-pro --prompt-file question.md --files .\src
|
|
208
|
-
ask-pro --artifacts --prompt-file implementation-plan.md --files src
|
|
209
|
-
ask-pro --files "src/api/stripe/**" --files "prisma/**" --files "src/lib/billing/**" \
|
|
210
|
-
"Review whether this Stripe webhook flow should use a queue or transactional outbox."
|
|
211
|
-
ask-pro --dry-run "Prepare the Pro handoff but do not open the browser."
|
|
212
|
-
ask-pro --resume <session-id>
|
|
213
|
-
ask-pro --harvest <session-id>
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
If the binary is not on `PATH`, skills CLI installs need the standalone CLI from
|
|
217
|
-
the README. Codex plugin installs can use the cached plugin runner. Do not run
|
|
218
|
-
the plugin from a mutable development checkout; it may contain in-flight changes
|
|
219
|
-
that have not been synced for agents.
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
node <cached-runner> -- --cwd /path/to/repo --no-temporary --prompt-file question.md --files src
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
In cached-runner fallback mode, `--files` must be inside `--cwd`. Use
|
|
226
|
-
repo-relative `--files`; do not point at files outside the target repo cwd.
|
|
227
|
-
|
|
228
|
-
## Safety
|
|
229
|
-
|
|
230
|
-
Never ask for, read, store, type, or log passwords, MFA codes, recovery codes, session cookies, or raw auth tokens.
|
|
231
|
-
|
|
232
|
-
Browser auth is human-controlled. Continue only after the human says the ChatGPT composer is visible.
|
|
233
|
-
|
|
234
|
-
After submit, avoid interacting with any retained Chrome run window while Pro is
|
|
235
|
-
thinking. ask-pro guards input after submit, but the safest agent behavior is to
|
|
236
|
-
let the run finish or resume/harvest from CLI telemetry.
|
|
8
|
+
This is the Codex plugin entry for ask-pro. The full instructions are in
|
|
9
|
+
`SKILL.md` at the plugin root, two folders above this file. Read that file and
|
|
10
|
+
follow it. Its `<skill-folder>` is the plugin root, so the launcher is
|
|
11
|
+
`node <plugin-root>/scripts/ask-pro.mjs`.
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
const args = parseArgs(process.argv.slice(2));
|
|
8
|
-
const pluginName = args.pluginName ?? "ask-pro";
|
|
9
|
-
const codexHome = args.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
|
10
|
-
const marketplacePath = args.marketplacePath ?? (await findCodexMarketplace(codexHome, pluginName));
|
|
11
|
-
|
|
12
|
-
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const repoRoot = path.resolve(scriptDir, "..");
|
|
14
|
-
const marketplaceFile = path.resolve(marketplacePath);
|
|
15
|
-
const marketplace = JSON.parse(await fs.readFile(marketplaceFile, "utf8"));
|
|
16
|
-
const marketplaceName = String(marketplace.name ?? "").trim();
|
|
17
|
-
if (!marketplaceName) {
|
|
18
|
-
throw new Error("Marketplace file must include a top-level name.");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const plugin = Array.isArray(marketplace.plugins)
|
|
22
|
-
? marketplace.plugins.find((entry) => entry?.name === pluginName)
|
|
23
|
-
: null;
|
|
24
|
-
if (!plugin) {
|
|
25
|
-
throw new Error(`Plugin '${pluginName}' was not found in ${marketplaceFile}.`);
|
|
26
|
-
}
|
|
27
|
-
if (plugin.source?.source === "local") {
|
|
28
|
-
const configuredSourcePath = plugin.source?.path;
|
|
29
|
-
if (typeof configuredSourcePath !== "string" || !configuredSourcePath.trim()) {
|
|
30
|
-
throw new Error(`Plugin '${pluginName}' local source must include a path.`);
|
|
31
|
-
}
|
|
32
|
-
const configuredSourceRoot = resolveConfiguredSourceRoot(configuredSourcePath, marketplaceFile);
|
|
33
|
-
if (!samePath(configuredSourceRoot, repoRoot)) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
`Plugin '${pluginName}' marketplace path resolves to ${configuredSourceRoot}, not this checkout ${repoRoot}.`,
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
} else if (plugin.source?.source !== "url") {
|
|
39
|
-
throw new Error(`Plugin '${pluginName}' must come from a local or URL marketplace source.`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const manifestPath = path.join(repoRoot, ".codex-plugin", "plugin.json");
|
|
43
|
-
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
44
|
-
if (manifest.name !== pluginName) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
`Plugin manifest name '${manifest.name}' does not match requested plugin '${pluginName}'.`,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const cacheRoot = path.resolve(codexHome, "plugins", "cache");
|
|
51
|
-
const pluginCacheRoot = path.resolve(cacheRoot, marketplaceName, pluginName);
|
|
52
|
-
const cacheVersion = plugin.source?.source === "local" ? "local" : manifest.version;
|
|
53
|
-
if (typeof cacheVersion !== "string" || !cacheVersion.trim()) {
|
|
54
|
-
throw new Error(`Plugin '${pluginName}' manifest must include a version.`);
|
|
55
|
-
}
|
|
56
|
-
const targetRoot = path.resolve(pluginCacheRoot, cacheVersion);
|
|
57
|
-
assertInside(
|
|
58
|
-
pluginCacheRoot,
|
|
59
|
-
cacheRoot,
|
|
60
|
-
"Resolved plugin cache path is outside Codex plugin cache",
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const requiredDistEntry = path.join(repoRoot, "dist", "bin", "ask-pro-cli.js");
|
|
64
|
-
if (!(await exists(requiredDistEntry))) {
|
|
65
|
-
throw new Error("dist is missing. Run `pnpm run build` before refreshing the plugin cache.");
|
|
66
|
-
}
|
|
67
|
-
const repoNodeModules = path.join(repoRoot, "node_modules");
|
|
68
|
-
if (!(await exists(repoNodeModules))) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
"node_modules is missing. Run `pnpm install` before refreshing the plugin cache.",
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
await removeNodeModulesLink(path.join(targetRoot, "node_modules"));
|
|
75
|
-
await fs.rm(targetRoot, { recursive: true, force: true });
|
|
76
|
-
await fs.mkdir(targetRoot, { recursive: true });
|
|
77
|
-
|
|
78
|
-
for (const item of [
|
|
79
|
-
".codex-plugin",
|
|
80
|
-
"assets",
|
|
81
|
-
"skills",
|
|
82
|
-
"references",
|
|
83
|
-
"README.md",
|
|
84
|
-
"LICENSE",
|
|
85
|
-
"package.json",
|
|
86
|
-
"pnpm-lock.yaml",
|
|
87
|
-
"pnpm-workspace.yaml",
|
|
88
|
-
"dist",
|
|
89
|
-
]) {
|
|
90
|
-
const source = path.join(repoRoot, item);
|
|
91
|
-
if (!(await exists(source))) continue;
|
|
92
|
-
await fs.cp(source, path.join(targetRoot, item), { recursive: true, force: true });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
await fs.mkdir(path.join(targetRoot, "scripts"), { recursive: true });
|
|
96
|
-
await fs.cp(
|
|
97
|
-
path.join(repoRoot, "scripts", "run-cached-cli.mjs"),
|
|
98
|
-
path.join(targetRoot, "scripts", "run-cached-cli.mjs"),
|
|
99
|
-
{ force: true },
|
|
100
|
-
);
|
|
101
|
-
await fs.symlink(repoNodeModules, path.join(targetRoot, "node_modules"), "junction");
|
|
102
|
-
|
|
103
|
-
console.log("Refreshed local Codex plugin cache:");
|
|
104
|
-
console.log(` source: ${repoRoot}`);
|
|
105
|
-
console.log(` target: ${targetRoot}`);
|
|
106
|
-
console.log("");
|
|
107
|
-
console.log("Restart or reload Codex to pick up refreshed plugin skills.");
|
|
108
|
-
|
|
109
|
-
function parseArgs(argv) {
|
|
110
|
-
const result = {};
|
|
111
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
112
|
-
const arg = argv[i];
|
|
113
|
-
const value = argv[i + 1];
|
|
114
|
-
if (!arg.startsWith("--") || !value || value.startsWith("--")) {
|
|
115
|
-
throw new Error(`Expected --name value argument, got '${arg}'.`);
|
|
116
|
-
}
|
|
117
|
-
i += 1;
|
|
118
|
-
if (arg === "--marketplace-path") result.marketplacePath = value;
|
|
119
|
-
else if (arg === "--codex-home") result.codexHome = value;
|
|
120
|
-
else if (arg === "--plugin-name") result.pluginName = value;
|
|
121
|
-
else throw new Error(`Unknown argument '${arg}'.`);
|
|
122
|
-
}
|
|
123
|
-
return result;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function assertInside(child, parent, message) {
|
|
127
|
-
const relative = path.relative(parent, child);
|
|
128
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
129
|
-
throw new Error(`${message}: ${child}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function exists(filePath) {
|
|
134
|
-
try {
|
|
135
|
-
await fs.access(filePath);
|
|
136
|
-
return true;
|
|
137
|
-
} catch {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function removeNodeModulesLink(filePath) {
|
|
143
|
-
try {
|
|
144
|
-
const stat = await fs.lstat(filePath);
|
|
145
|
-
if (stat.isSymbolicLink()) {
|
|
146
|
-
await fs.unlink(filePath);
|
|
147
|
-
}
|
|
148
|
-
} catch {
|
|
149
|
-
// Missing or non-removable links are handled by the full target cleanup.
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function findCodexMarketplace(codexHome, pluginName) {
|
|
154
|
-
const codexMarketplace = path.join(
|
|
155
|
-
codexHome,
|
|
156
|
-
".tmp",
|
|
157
|
-
"marketplaces",
|
|
158
|
-
pluginName,
|
|
159
|
-
".agents",
|
|
160
|
-
"plugins",
|
|
161
|
-
"marketplace.json",
|
|
162
|
-
);
|
|
163
|
-
if (await exists(codexMarketplace)) return codexMarketplace;
|
|
164
|
-
return path.join(os.homedir(), ".agents", "plugins", "marketplace.json");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function resolveConfiguredSourceRoot(sourcePath, marketplaceFile) {
|
|
168
|
-
const candidates = [
|
|
169
|
-
path.resolve(path.dirname(marketplaceFile), sourcePath),
|
|
170
|
-
path.resolve(os.homedir(), sourcePath),
|
|
171
|
-
];
|
|
172
|
-
return candidates.find((candidate) => samePath(candidate, repoRoot)) ?? candidates[0];
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function samePath(left, right) {
|
|
176
|
-
const a = path.resolve(left);
|
|
177
|
-
const b = path.resolve(right);
|
|
178
|
-
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
179
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
param(
|
|
2
|
-
[string]$MarketplacePath = "",
|
|
3
|
-
[string]$CodexHome = $(if ($env:CODEX_HOME) { $env:CODEX_HOME } else { "$HOME/.codex" }),
|
|
4
|
-
[string]$PluginName = "ask-pro"
|
|
5
|
-
)
|
|
6
|
-
|
|
7
|
-
$ErrorActionPreference = "Stop"
|
|
8
|
-
|
|
9
|
-
function Resolve-StrictPath([string]$Path) {
|
|
10
|
-
return [System.IO.Path]::GetFullPath((Resolve-Path -LiteralPath $Path).Path)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function Join-And-Normalize([string]$Base, [string[]]$Parts) {
|
|
14
|
-
return [System.IO.Path]::GetFullPath((Join-Path -Path $Base -ChildPath ([System.IO.Path]::Combine($Parts))))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
$repoRoot = Resolve-StrictPath (Join-Path $PSScriptRoot "..")
|
|
18
|
-
if ([string]::IsNullOrWhiteSpace($MarketplacePath)) {
|
|
19
|
-
$codexMarketplace = Join-And-Normalize $CodexHome @(".tmp", "marketplaces", $PluginName, ".agents", "plugins", "marketplace.json")
|
|
20
|
-
if (Test-Path -LiteralPath $codexMarketplace) {
|
|
21
|
-
$MarketplacePath = $codexMarketplace
|
|
22
|
-
} else {
|
|
23
|
-
$MarketplacePath = "$HOME/.agents/plugins/marketplace.json"
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
$marketplaceFile = Resolve-StrictPath $MarketplacePath
|
|
27
|
-
$marketplace = Get-Content -LiteralPath $marketplaceFile -Raw | ConvertFrom-Json
|
|
28
|
-
$marketplaceName = [string]$marketplace.name
|
|
29
|
-
if ([string]::IsNullOrWhiteSpace($marketplaceName)) {
|
|
30
|
-
throw "Marketplace file must include a top-level name."
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
$plugin = @($marketplace.plugins) | Where-Object { $_.name -eq $PluginName } | Select-Object -First 1
|
|
34
|
-
if (-not $plugin) {
|
|
35
|
-
throw "Plugin '$PluginName' was not found in $marketplaceFile."
|
|
36
|
-
}
|
|
37
|
-
if (($plugin.source.source -ne "local") -and ($plugin.source.source -ne "url")) {
|
|
38
|
-
throw "Plugin '$PluginName' must come from a local or URL marketplace source."
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
$sourceRoot = Resolve-StrictPath $repoRoot
|
|
42
|
-
$manifestPath = Join-Path $sourceRoot ".codex-plugin/plugin.json"
|
|
43
|
-
if (-not (Test-Path -LiteralPath $manifestPath)) {
|
|
44
|
-
throw "Missing plugin manifest at $manifestPath."
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
|
48
|
-
if ($manifest.name -ne $PluginName) {
|
|
49
|
-
throw "Plugin manifest name '$($manifest.name)' does not match requested plugin '$PluginName'."
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
$codexHomePath = [System.IO.Path]::GetFullPath($CodexHome)
|
|
53
|
-
$cacheRoot = Join-And-Normalize $codexHomePath @("plugins", "cache")
|
|
54
|
-
$pluginCacheRoot = Join-And-Normalize $cacheRoot @($marketplaceName, $PluginName)
|
|
55
|
-
$cacheVersion = $(if ($plugin.source.source -eq "local") { "local" } else { [string]$manifest.version })
|
|
56
|
-
if ([string]::IsNullOrWhiteSpace($cacheVersion)) {
|
|
57
|
-
throw "Plugin '$PluginName' manifest must include a version."
|
|
58
|
-
}
|
|
59
|
-
$targetRoot = Join-And-Normalize $pluginCacheRoot @($cacheVersion)
|
|
60
|
-
|
|
61
|
-
$cacheRootWithSeparator = $cacheRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar
|
|
62
|
-
if (-not $pluginCacheRoot.StartsWith($cacheRootWithSeparator, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
63
|
-
throw "Resolved plugin cache path is outside Codex plugin cache: $pluginCacheRoot"
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (Test-Path -LiteralPath $targetRoot) {
|
|
67
|
-
Remove-Item -LiteralPath $targetRoot -Recurse -Force
|
|
68
|
-
}
|
|
69
|
-
New-Item -ItemType Directory -Path $targetRoot -Force | Out-Null
|
|
70
|
-
|
|
71
|
-
$itemsToCopy = @(
|
|
72
|
-
".codex-plugin",
|
|
73
|
-
"skills",
|
|
74
|
-
"references",
|
|
75
|
-
"README.md",
|
|
76
|
-
"LICENSE",
|
|
77
|
-
"package.json",
|
|
78
|
-
"dist"
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
foreach ($item in $itemsToCopy) {
|
|
82
|
-
$source = Join-Path $sourceRoot $item
|
|
83
|
-
if (-not (Test-Path -LiteralPath $source)) {
|
|
84
|
-
continue
|
|
85
|
-
}
|
|
86
|
-
Copy-Item -LiteralPath $source -Destination $targetRoot -Recurse -Force
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
Write-Host "Refreshed local Codex plugin cache:"
|
|
90
|
-
Write-Host " source: $sourceRoot"
|
|
91
|
-
Write-Host " target: $targetRoot"
|
|
92
|
-
Write-Host ""
|
|
93
|
-
Write-Host "Restart or reload Codex to pick up refreshed plugin skills."
|