mu-agent 0.2.2 → 0.2.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.4] - 2026-10-24
6
+
7
+ **0.2.4 brings Gemini 4 Argon through Google Antigravity and fixes third-party OpenAI-compatible providers that turned away the `developer` role.**
8
+
9
+ ### Fixed
10
+
11
+ - Signed in with Google Antigravity, mu now lists Gemini 4 Argon (`gemini-4-argon-low`, `-medium` and `-high`) and the Gemini 3.8 Flash tiers, and can use them. mu introduced itself to Antigravity as its IDE, version 1.107.0 (`antigravity/1.107.0 darwin/arm64`). Google lists and serves those models only to the Antigravity command line from 1.3.0, and answered 404 for them. mu now introduces itself as the command line (`antigravity/cli/1.4.0`; `MU_ANTIGRAVITY_VERSION` still raises the version). Gemini 4 Argon thinks at low, medium or high, never off. A placeholder for a model the account may not use, such as `gemini-4-argon-disabled`, is left out. The context Antigravity gives a model (1 million tokens for Claude Opus and Sonnet 5.5) is now used.
12
+ - An account that was already signed in to Antigravity gets the new models without signing in again. Its list was read only at sign-in; now the terminal and the desktop app also read it in the background as a session starts. Print and JSON runs don't, nor does `PI_OFFLINE`. Signing out still drops the account's models, these included.
13
+ - A custom OpenAI-compatible provider (`"api": "openai-completions"` in `models.json`) no longer fails with "unknown role developer" for a thinking model. Only `api.openai.com`, Azure OpenAI and Xiaomi MiMo get `developer` messages and `max_completion_tokens`; other endpoints get `system`, `max_tokens` and no `store`. When an endpoint still rejects the `developer` role, the max tokens field, `reasoning_effort`, `stream_options`, `store` or the prompt cache fields with a 400 (or a gateway's 5xx wrapping one), mu sends the request once more without it, before any output, and remembers that for the model until it restarts. `compat` in `models.json` overrides the defaults.
14
+
15
+ ## [0.2.3] - 2026-10-24
16
+
17
+ **0.2.3 adds `mu judge model` for decision models from Hugging Face, makes mu decide when a mid-run message reaches the work, and makes Sign in with ChatGPT cope with a busy port 1455.**
18
+
19
+ ### Added
20
+
21
+ - `mu judge model`: other decision models from Hugging Face, next to mude 1.0. `mu judge model add <link or owner/name>[@revision]` looks the model up on Hugging Face and pins the commit and every file (sha256 for files in LFS, the git hash for the others). It then downloads the model with progress and checks every file, resuming after a stop like `mu judge mude install`. It sets the model up in mude's Python environment, starts its server on 127.0.0.1 and makes it the judge, with the judges from before behind it unless `--local-only`. `list`, `use <name>` (`use mude` switches back), `start`, `stop`, `status`, `disable` and `remove` manage them, and `--json` gives one JSON object per line, as for `mu judge mude`. A model must be packaged like mude 1.0's MLX edition, with its own `d3_server_mac.py`; a repository without one is refused before anything is downloaded. One decision model runs at a time: switching stops the other's server, and mu starts the one that is the judge before a session, as it does mude.
22
+ - `mu judge mude uninstall` keeps the Python environment when models added with `mu judge model` still use it.
23
+ - `mu auth login <provider> --paste` signs in without listening on the callback port: you sign in in the browser, then paste the address it ends on. That works whichever program holds the port.
24
+
25
+ ### Changed
26
+
27
+ - Less small print: `mu --help` no longer says "Formerly KYRN", `mu setup` drops its header line, the sign-in location sentence and the closing footer, and its judge question fits on one line; `mu ledger` drops its help footer, and the kernel part of the in-session `/help` keeps only the Esc and expand keys.
28
+ - mu now decides when a message you send during a run reaches the work: `input.interjection` is active by default (it was in shadow). Enter still sends a steer and Alt+Enter a follow-up, and the judge can move the message. A correction reaches the model at its next step, even when you queued it. Extra work and unrelated questions wait until the run is done, even when you sent them with Enter. Anything else goes the way you sent it. The message waits up to 1.5 s for the verdict (`features.interjection.waitMs`). It goes as sent when the verdict is late, when the judge fails or is off, and when the run ends during the wait. Messages you send in quick succession arrive in the order you sent them, however long each verdict takes. 34 of the 40 decision points now act from the start. To keep the old behavior, put `"input.interjection": "shadow"` under `modes` in `~/.mu/agent/mu.json`.
29
+
30
+ ### Fixed
31
+
32
+ - Sign in with ChatGPT (`/login` and `mu auth login openai`, which the desktop app runs) no longer fails with "Port 1455 is in use" when the port is held by another sign-in of mu or pi. Port 1455 is fixed by OpenAI, and a sign-in keeps it until it ends. Before, a second sign-in started while the first still waited (two in a row from the app, a `/login` left open in a terminal) failed with that message. Now the new sign-in asks the earlier one to let go: the earlier one stops with "Sign-in stopped: a newer sign-in started and took over port 1455", and the new one listens. The Codex CLI's own login takes the port over the same way, with the request it already sends to an older login server.
33
+ - When another program keeps port 1455 (the Codex CLI or the Codex app, while its own login waits), the sign-in now names it: "Port 1455 is in use by codex (process 4321), probably for its own sign-in … Finish or cancel that sign-in, then try again". That program is left alone. `mu auth` passes this on with `code: "port_in_use"`, the port and the program, so the app can say it in its own words. The program is looked up with `lsof` (or `ss`, or `netstat` on Windows); where none answers, the message says "another program".
34
+ - Every way a ChatGPT sign-in ends now gives port 1455 back at once: success, an error, a cancel (Escape in `/login`, `cancel` or the input closing for `mu auth`), SIGTERM to `mu auth` (the app quitting or ending it), and a prompt that never answers. A sign-in nobody finishes stops listening after 15 minutes. The older ChatGPT sign-in through the Codex login (`openai-codex`) takes the port over from an earlier sign-in of mu the same way. When another program keeps the port, it still falls back to the pasted address, and now says which program it is.
35
+ - A mid-run message that the judge moves to the other queue is now expanded like any other: a prompt template or a skill (`/name …`) reaches the model expanded, not as you typed it.
36
+ - A session in RPC mode (each conversation of the desktop app) now checks `get_available_models` and `set_model` against the credential store, so a sign-in or sign-out made by another process counts at once: `mu auth logout`, or `/logout` in another terminal. Before, a signed-out subscription's models stayed listed until the session restarted.
37
+ - `/logout` moves the session off the signed-out provider's model, to the default model (else a provider's default, else the first model available), and says so: "Logged out of ChatGPT. Switched to openai/gpt-5.5." Before, the next prompt failed with "No API key". With no model left, it says to use `/login`.
38
+ - `mu auth logout google-antigravity` also drops the list of models the account gave from `models-store.json`, so the next sign-in starts from its own.
39
+
5
40
  ## [0.2.2] - 2026-10-24
6
41
 
7
42
  **0.2.2 makes `mu judge mude install` cope with a full disk, a stop at any step and two installs at once.**
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **A coding agent with a judgment kernel.** It hands its routine decisions to a small, fast judge model, so the main model can keep its attention on the work.
4
4
 
5
- > Source, docs and the desktop app: [github.com/qybaihe/mu](https://github.com/qybaihe/mu) (README in English, 简体中文, 繁體中文, 日本語 and 한국어).
5
+ > Source, docs and the desktop app: [github.com/qybaihe/mu](https://github.com/qybaihe/mu).
6
6
 
7
7
  ## Install
8
8
 
@@ -13,7 +13,7 @@ mu
13
13
 
14
14
  You need Node.js 22.19 or newer. The package is `mu-agent`; the command is `mu`. It runs on macOS, Linux, Windows and WSL.
15
15
 
16
- `mu setup` connects a model from your shell. Paste an API key and mu detects the service from the key's format (OpenAI, Anthropic, Google Gemini, xAI, OpenRouter, OpenCode Zen, DeepSeek, Qwen, Kimi, GLM, SiliconFlow, StepFun), or pick Ollama or your own endpoint. It checks the key with one request, picks a starting model and sets up the judge. To use a ChatGPT, Grok or Google subscription instead, choose it there and mu starts on `/login`. If no model is set up yet, the first `mu` run offers to start setup.
16
+ `mu setup` connects a model from your shell. Paste an API key and mu detects the service from the key's format (OpenAI, Anthropic, Google Gemini, xAI, OpenRouter, OpenCode Zen, DeepSeek, Qwen, Kimi, GLM, SiliconFlow, StepFun), or pick Ollama or your own endpoint. It checks the key with one request, picks a starting model and sets up the judge. To use a ChatGPT, Grok or Google subscription instead, choose it there and mu starts on `/login`.
17
17
 
18
18
  Inside mu, `/login` signs in to a provider or a subscription and `/model` switches models. `/help` lists everything. `mu doctor` checks your setup from the shell.
19
19
 
@@ -36,7 +36,7 @@ Out of the box, the judge is Jev 1.13, TypeSafe's hosted judge model, free on Op
36
36
  - **Laya** (local, macOS on Apple silicon): `mu judge setup` installs it into `~/.mu/local-judge`. It downloads about 930 MB, and only when you run that command.
37
37
  - **Any model you already use:** `/mu judge llm:<provider>/<model>`.
38
38
 
39
- 33 of the 40 decision points act on their verdicts from the start (`active`); `context.compact` and `output.drift` belong to features that are off until you turn them on, `capability.disclosure` and `swarm.patch` are `off`, and `notify.routing`, `input.interjection` and `turn.drift` are in `shadow`. To only record what the judge would do, run `/mu mode default shadow`, or put `"modes": { "default": "shadow" }` in `~/.mu/agent/mu.json`.
39
+ 34 of the 40 decision points act on their verdicts from the start (`active`). To only record what the judge would do, run `/mu mode default shadow`, or put `"modes": { "default": "shadow" }` in `~/.mu/agent/mu.json`.
40
40
 
41
41
  ## Commands
42
42
 
@@ -58,10 +58,6 @@ mu help | version show this help | print the version
58
58
 
59
59
  Everything else goes to pi unchanged: `mu --help` also shows pi's options. mu keeps its files in `~/.mu`, separate from a regular pi install in `~/.pi`.
60
60
 
61
- ## Themes
62
-
63
- In the terminal, mu uses its own themes, `mu-dark` or `mu-light` to match your terminal, until you pick a theme in `/settings`. Choose `system` there to use your terminal's own palette.
64
-
65
61
  ## Update and remove
66
62
 
67
63
  ```bash
@@ -83,4 +83,4 @@ var __require=(x=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(x,{get:(
83
83
  ${details?`<div class="details">${details}</div>`:""}
84
84
  </main>
85
85
  </body>
86
- </html>`}function oauthSuccessHtml(message){return renderPage({heading:"Signed in",message})}function oauthErrorHtml(message,details){return renderPage({heading:"Sign-in did not complete",message,details})}function sendPage(response,status,html){response.writeHead(status,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),response.end(html)}async function startOAuthCallbackServer(options){let{providerName,signal}=options;if(signal?.aborted)throw new Error("Login cancelled");let resolveWait=()=>{},rejectWait=()=>{},waitPromise=new Promise((resolve,reject)=>{resolveWait=resolve,rejectWait=reject});waitPromise.catch(()=>{});let claimed=!1,settled=!1,timer,onAbort=()=>finish({error:new Error("Login cancelled")}),finish=result=>{settled||(settled=!0,timer&&clearTimeout(timer),signal?.removeEventListener("abort",onAbort),"error"in result?rejectWait(result.error):resolveWait(result.value))},server=createServer((request,response)=>{(async()=>{let url=new URL(request.url??"/","http://localhost");if(request.method!=="GET"||url.pathname!==options.path){sendPage(response,404,oauthErrorHtml("Callback route not found."));return}if(options.state!==void 0&&url.searchParams.get("state")!==options.state){sendPage(response,400,oauthErrorHtml("State mismatch."));return}if(claimed||settled){sendPage(response,409,oauthErrorHtml("This sign-in has already been handled."));return}let error=url.searchParams.get("error");if(error){let description=url.searchParams.get("error_description")??error;sendPage(response,400,oauthErrorHtml(`${providerName} authorization failed.`,description)),finish({error:new Error(`${providerName} authorization failed: ${description}`)});return}let code=url.searchParams.get("code");if(!code){sendPage(response,400,oauthErrorHtml("Missing authorization code."));return}claimed=!0;try{let value=await options.complete(code);sendPage(response,200,oauthSuccessHtml(`Signed in to ${providerName}. You may now close this page.`)),finish({value})}catch(error2){let failure=error2 instanceof Error?error2:new Error(String(error2));sendPage(response,502,oauthErrorHtml(`${providerName} sign-in failed.`,failure.message)),finish({error:failure})}})()});await new Promise((resolve,reject)=>{server.once("error",reject),server.listen(options.port,options.host,()=>{server.off("error",reject),resolve()})});let address=server.address();if(!address||typeof address=="string")throw server.close(),new Error("OAuth callback server did not bind to TCP");server.on("error",error=>finish({error})),signal?.addEventListener("abort",onAbort,{once:!0}),options.timeoutMs!==void 0&&(timer=setTimeout(()=>finish({error:new Error(`${providerName} sign-in timed out`)}),options.timeoutMs));let redirectHost=options.redirectHost??options.host;return{redirectUri:`http://${redirectHost.includes(":")?`[${redirectHost}]`:redirectHost}:${address.port}${options.path}`,wait:()=>waitPromise,cancel:()=>{claimed||finish({value:void 0})},close:()=>{finish({error:new Error("OAuth callback server closed")}),server.close()}}}async function waitForCallbackOrManualInput(interaction,callback,prompt){let manualAbort=new AbortController,manualError,manual=interaction.prompt({type:"manual_code",...prompt,signal:manualAbort.signal}).then(input=>(callback?.cancel(),input)).catch(error=>{manualError=error instanceof Error?error:new Error(String(error)),callback?.cancel()});try{let value=await callback?.wait();if(manualError)throw manualError;if(value!==void 0)return{type:"callback",value};let input=await manual;if(manualError)throw manualError;return{type:"manual",input:input??""}}finally{manualAbort.abort()}}function base64urlEncode(bytes){let binary="";for(let byte of bytes)binary+=String.fromCharCode(byte);return btoa(binary).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function generatePKCE(){let verifierBytes=new Uint8Array(32);crypto.getRandomValues(verifierBytes);let verifier=base64urlEncode(verifierBytes),data=new TextEncoder().encode(verifier),hashBuffer=await crypto.subtle.digest("SHA-256",data),challenge=base64urlEncode(new Uint8Array(hashBuffer));return{verifier,challenge}}var decode=s=>atob(s),CLIENT_ID=decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl"),AUTHORIZE_URL="https://claude.ai/oauth/authorize",TOKEN_URL="https://platform.claude.com/v1/oauth/token",CALLBACK_HOST=getProviderEnvValue("PI_OAUTH_CALLBACK_HOST")||"127.0.0.1",CALLBACK_PORT=53692,CALLBACK_PATH="/callback",REDIRECT_URI=`http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`,COPY_CODE_REDIRECT_URI="https://platform.claude.com/oauth/code/callback",ANTHROPIC_BROWSER_LOGIN_METHOD="browser",ANTHROPIC_COPY_CODE_LOGIN_METHOD="copy_code",SCOPES="org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";function parseAuthorizationInput(input){let value=input.trim();if(!value)return{};try{let url=new URL(value);return{code:url.searchParams.get("code")??void 0,state:url.searchParams.get("state")??void 0}}catch{}if(value.includes("#")){let[code,state]=value.split("#",2);return{code,state}}if(value.includes("code=")){let params=new URLSearchParams(value);return{code:params.get("code")??void 0,state:params.get("state")??void 0}}return{code:value}}function formatErrorDetails(error){if(error instanceof Error){let details=[`${error.name}: ${error.message}`],errorWithCode=error;return errorWithCode.code&&details.push(`code=${errorWithCode.code}`),typeof errorWithCode.errno<"u"&&details.push(`errno=${String(errorWithCode.errno)}`),typeof error.cause<"u"&&details.push(`cause=${formatErrorDetails(error.cause)}`),error.stack&&details.push(`stack=${error.stack}`),details.join("; ")}return String(error)}async function postJson(url,body,signal){let response=await fetch(url,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(body),signal:AbortSignal.any([signal,AbortSignal.timeout(3e4)])}),responseBody=await response.text();if(!response.ok)throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`);return responseBody}async function exchangeAuthorizationCode(code,state,verifier,redirectUri,signal){let responseBody;try{responseBody=await postJson(TOKEN_URL,{grant_type:"authorization_code",client_id:CLIENT_ID,code,state,redirect_uri:redirectUri,code_verifier:verifier},signal)}catch(error){throw new Error(`Token exchange request failed. url=${TOKEN_URL}; redirect_uri=${redirectUri}; response_type=authorization_code; details=${formatErrorDetails(error)}`)}let tokenData;try{tokenData=JSON.parse(responseBody)}catch(error){throw new Error(`Token exchange returned invalid JSON. url=${TOKEN_URL}; body=${responseBody}; details=${formatErrorDetails(error)}`)}return{type:"oauth",refresh:tokenData.refresh_token,access:tokenData.access_token,expires:Date.now()+tokenData.expires_in*1e3-300*1e3}}async function loginAnthropic(interaction){let{verifier,challenge}=await generatePKCE(),startCallbackServer=port=>startOAuthCallbackServer({providerName:"Anthropic",host:CALLBACK_HOST,port,path:CALLBACK_PATH,redirectHost:"localhost",state:verifier,complete:async code=>code,signal:interaction.signal}),callback=await startCallbackServer(CALLBACK_PORT).catch(()=>startCallbackServer(0)).catch(()=>{}),redirectUri=callback?.redirectUri??REDIRECT_URI;try{let authParams=new URLSearchParams({code:"true",client_id:CLIENT_ID,response_type:"code",redirect_uri:redirectUri,scope:SCOPES,code_challenge:challenge,code_challenge_method:"S256",state:verifier});interaction.notify({type:"auth_url",url:`${AUTHORIZE_URL}?${authParams.toString()}`,instructions:"Complete login in your browser. If the browser is on another machine, paste the final redirect URL here."});let result=await waitForCallbackOrManualInput(interaction,callback,{message:"Complete login in your browser, or paste the authorization code / redirect URL here:",placeholder:redirectUri}),code,state=verifier;if(result.type==="callback")code=result.value;else{let parsed=parseAuthorizationInput(result.input);if(parsed.state&&parsed.state!==verifier)throw new Error("OAuth state mismatch");code=parsed.code,state=parsed.state??verifier}if(!code)throw new Error("Missing authorization code");return interaction.notify({type:"progress",message:"Exchanging authorization code for tokens..."}),await exchangeAuthorizationCode(code,state,verifier,redirectUri,interaction.signal)}finally{callback?.close()}}async function loginAnthropicCopyCode(interaction){let{verifier,challenge}=await generatePKCE(),authParams=new URLSearchParams({code:"true",client_id:CLIENT_ID,response_type:"code",redirect_uri:COPY_CODE_REDIRECT_URI,scope:SCOPES,code_challenge:challenge,code_challenge_method:"S256",state:verifier});interaction.notify({type:"auth_url",url:`${AUTHORIZE_URL}?${authParams.toString()}`,instructions:"Complete login in your browser, then copy the code Anthropic shows and paste it here."});let input=await interaction.prompt({type:"manual_code",message:"Paste the code Anthropic shows after you sign in:",placeholder:"code#state",signal:interaction.signal}),parsed=parseAuthorizationInput(input);if(parsed.state&&parsed.state!==verifier)throw new Error("OAuth state mismatch");if(!parsed.code)throw new Error("Missing authorization code");return interaction.notify({type:"progress",message:"Exchanging authorization code for tokens..."}),await exchangeAuthorizationCode(parsed.code,parsed.state??verifier,verifier,COPY_CODE_REDIRECT_URI,interaction.signal)}async function refreshAnthropicToken(refreshToken,signal){let responseBody;try{responseBody=await postJson(TOKEN_URL,{grant_type:"refresh_token",client_id:CLIENT_ID,refresh_token:refreshToken},signal)}catch(error){throw new Error(`Anthropic token refresh request failed. url=${TOKEN_URL}; details=${formatErrorDetails(error)}`)}let data;try{data=JSON.parse(responseBody)}catch(error){throw new Error(`Anthropic token refresh returned invalid JSON. url=${TOKEN_URL}; body=${responseBody}; details=${formatErrorDetails(error)}`)}return{type:"oauth",refresh:data.refresh_token,access:data.access_token,expires:Date.now()+data.expires_in*1e3-300*1e3}}var anthropicOAuth={name:"Anthropic (Claude Pro/Max)",isSubscription:!0,async login(interaction){let method=await interaction.prompt({type:"select",message:"Select Anthropic login method:",options:[{id:ANTHROPIC_BROWSER_LOGIN_METHOD,label:"Browser login (default)"},{id:ANTHROPIC_COPY_CODE_LOGIN_METHOD,label:"Copy code login (headless)"}]});if(method===ANTHROPIC_COPY_CODE_LOGIN_METHOD)return loginAnthropicCopyCode(interaction);if(method!==ANTHROPIC_BROWSER_LOGIN_METHOD)throw new Error(`Unknown Anthropic login method: ${method}`);return loginAnthropic(interaction)},refresh:(credential,signal)=>refreshAnthropicToken(credential.refresh,signal),async toAuth(credential){return{apiKey:credential.access}}};export{anthropicOAuth};
86
+ </html>`}function oauthSuccessHtml(message){return renderPage({heading:"Signed in",message})}function oauthErrorHtml(message,details){return renderPage({heading:"Sign-in did not complete",message,details})}import{execFile}from"node:child_process";import{request}from"node:http";var LOGIN_PROBE_PATH="/pi-login",LOGIN_TAKE_OVER_PATH="/pi-login/take-over",CODEX_CANCEL_PATH="/cancel",MARKER_HEADER="x-pi-login",OLD_PI_NOT_FOUND="Callback route not found.",SETTLE_MS=2500,RETRY_MS=100,REQUEST_MS=1e3,LOOKUP_MS=2e3;function describeHolder(holder){let pid=holder?.pid?`process ${holder.pid}`:void 0;return holder?.command?pid?`${holder.command} (${pid})`:holder.command:pid??"another program"}var CallbackPortInUseError=class extends Error{code="port_in_use";port;holder;constructor(port,holder){super(holder?.pi?`Port ${port} is held by an unfinished sign-in in another pi or mu session (${describeHolder(holder)}), which did not hand it over. Finish or cancel that sign-in, then try again.`:`Port ${port} is in use by ${describeHolder(holder)}, probably for its own sign-in (the Codex CLI and the Codex app use this port). Finish or cancel that sign-in, then try again.`),this.name="CallbackPortInUseError",this.port=port,this.holder=holder}},LoginTakenOverError=class extends Error{code="login_taken_over";constructor(port,by){super(by==="codex"?`Sign-in stopped: the Codex CLI started its own sign-in on port ${port}.`:`Sign-in stopped: a newer sign-in started and took over port ${port}.`),this.name="LoginTakenOverError"}};function answerLoginControl(request2,response,takeOver){let path=new URL(request2.url??"/","http://localhost").pathname,fromBrowser=request2.headers.origin!==void 0||request2.headers["sec-fetch-site"]!==void 0;if(request2.method==="GET"&&path===LOGIN_PROBE_PATH)return response.writeHead(200,{"content-type":"application/json","cache-control":"no-store",[MARKER_HEADER]:"pi"}),response.end(JSON.stringify({pi:!0,pid:process.pid})),!0;let ours=request2.method==="POST"&&path===LOGIN_TAKE_OVER_PATH&&request2.headers[MARKER_HEADER]==="take-over"&&!fromBrowser,codex=request2.method==="GET"&&path===CODEX_CANCEL_PATH&&!fromBrowser;return!ours&&!codex?!1:(response.once("close",()=>takeOver(ours?"pi":"codex")),response.writeHead(200,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),response.end("Login cancelled"),!0)}function closeCallbackServer(server){server.close(),server.closeAllConnections()}function localRequest(host,port,method,path,headers){return new Promise(resolve=>{let outgoing=request({host,port,method,path,headers,timeout:REQUEST_MS,agent:!1},incoming=>{let body="";incoming.setEncoding("utf8"),incoming.on("data",chunk=>{body.length<16384&&(body+=chunk)}),incoming.on("end",()=>resolve({status:incoming.statusCode??0,headers:incoming.headers,body})),incoming.on("error",()=>resolve({status:incoming.statusCode??0,headers:incoming.headers,body}))});outgoing.on("timeout",()=>outgoing.destroy(new Error("timeout"))),outgoing.on("error",error=>resolve({error:error.code??error.message})),outgoing.end()})}async function probeHolder(host,port){let answer=await localRequest(host,port,"GET",LOGIN_PROBE_PATH,{[MARKER_HEADER]:"probe"});if("error"in answer)return answer.error==="ECONNREFUSED"?{kind:"none"}:{kind:"other"};if(answer.status===200&&answer.headers[MARKER_HEADER]==="pi")try{let pid=JSON.parse(answer.body).pid;return{kind:"pi",pid:typeof pid=="number"?pid:void 0}}catch{return{kind:"pi"}}return answer.status===404&&answer.body.includes(OLD_PI_NOT_FOUND)?{kind:"old-pi"}:{kind:"other"}}function tryListen(server,port,host){return new Promise(resolve=>{let onError=error=>{server.off("listening",onListening),resolve(error)},onListening=()=>{server.off("error",onError),resolve(void 0)};server.once("error",onError),server.once("listening",onListening),server.listen(port,host)})}function cancelled(){return new Error("Login cancelled")}function pause(ms,signal){return new Promise((resolve,reject)=>{if(signal?.aborted){reject(cancelled());return}let onAbort=()=>{clearTimeout(timer),reject(cancelled())},timer=setTimeout(()=>{signal?.removeEventListener("abort",onAbort),resolve()},ms);signal?.addEventListener("abort",onAbort,{once:!0})})}async function listenOnLoginPort(server,options){let{port,host,signal}=options,settleMs=options.settleMs??SETTLE_MS,deadline=Date.now()+settleMs,askedToLetGo=!1,probe;for(;;){if(signal?.aborted)throw cancelled();let error=await tryListen(server,port,host);if(!error){if(signal?.aborted)throw closeCallbackServer(server),cancelled();return}if(error.code!=="EADDRINUSE")throw error;if(!askedToLetGo&&probe?.kind!=="other"&&probe?.kind!=="old-pi"&&(probe=await probeHolder(host,port),probe.kind==="pi"&&(await localRequest(host,port,"POST",LOGIN_TAKE_OVER_PATH,{[MARKER_HEADER]:"take-over"}),askedToLetGo=!0,deadline=Date.now()+settleMs)),Date.now()>=deadline)break;await pause(RETRY_MS,signal)}let found=await(options.findHolder??findPortHolder)(port).catch(()=>{}),holder={...found,pid:found?.pid??(probe?.kind==="pi"?probe.pid:void 0),...probe?.kind==="pi"||probe?.kind==="old-pi"?{pi:!0}:{}};throw new CallbackPortInUseError(port,holder.pid||holder.command||holder.pi?holder:void 0)}function run(command,args){return new Promise(resolve=>{execFile(command,args,{timeout:LOOKUP_MS,windowsHide:!0,encoding:"utf8"},(error,stdout)=>{resolve(error&&error.code==="ENOENT"?void 0:String(stdout??""))})})}function parseLsof(output){let holder;for(let line of output.split(/\r?\n/))if(line.startsWith("p")){if(holder)break;let pid=Number(line.slice(1));holder=Number.isInteger(pid)&&pid>0?{pid}:{}}else line.startsWith("c")&&holder&&(holder.command=line.slice(1));return holder}function parseSs(output){let match=/users:\(\("([^"]*)",pid=(\d+)/.exec(output);return match?{command:match[1],pid:Number(match[2])}:void 0}function parseNetstat(output,port){for(let line of output.split(/\r?\n/)){let columns=line.trim().split(/\s+/);if(columns.length<5||columns[0]?.toUpperCase()!=="TCP"||!columns[1]?.endsWith(`:${port}`)||columns[3]?.toUpperCase()!=="LISTENING")continue;let pid=Number(columns[4]);if(Number.isInteger(pid)&&pid>0)return pid}}function parseTasklist(output){return/^"([^"]+)"/.exec(output.trim())?.[1]?.replace(/\.exe$/i,"")}async function findPortHolder(port){if(process.platform==="win32"){let pid=parseNetstat(await run("netstat",["-ano","-p","TCP"])??"",port);if(!pid)return;let command=parseTasklist(await run("tasklist",["/FI",`PID eq ${pid}`,"/FO","CSV","/NH"])??"");return command?{pid,command}:{pid}}for(let lsof of["lsof","/usr/sbin/lsof","/usr/bin/lsof"]){let output=await run(lsof,["-nP",`-iTCP:${port}`,"-sTCP:LISTEN","-Fpc"]);if(output!==void 0)return parseLsof(output)}if(process.platform==="linux")return parseSs(await run("ss",["-Hltnp",`sport = :${port}`])??"")}function sendPage(response,status,html){response.writeHead(status,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),response.end(html)}async function startOAuthCallbackServer(options){let{providerName,signal}=options;if(signal?.aborted)throw new Error("Login cancelled");let resolveWait=()=>{},rejectWait=()=>{},waitPromise=new Promise((resolve,reject)=>{resolveWait=resolve,rejectWait=reject});waitPromise.catch(()=>{});let claimed=!1,settled=!1,timer,onAbort=()=>finish({error:new Error("Login cancelled")}),finish=result=>{settled||(settled=!0,timer&&clearTimeout(timer),signal?.removeEventListener("abort",onAbort),"error"in result?rejectWait(result.error):resolveWait(result.value))},server=createServer((request2,response)=>{(async()=>{let takeOver=by=>{finish({error:new LoginTakenOverError(options.port,by)}),closeCallbackServer(server)};if(options.sharedPort&&answerLoginControl(request2,response,takeOver))return;let url=new URL(request2.url??"/","http://localhost");if(request2.method!=="GET"||url.pathname!==options.path){sendPage(response,404,oauthErrorHtml("Callback route not found."));return}if(options.state!==void 0&&url.searchParams.get("state")!==options.state){sendPage(response,400,oauthErrorHtml("State mismatch."));return}if(claimed||settled){sendPage(response,409,oauthErrorHtml("This sign-in has already been handled."));return}let error=url.searchParams.get("error");if(error){let description=url.searchParams.get("error_description")??error;sendPage(response,400,oauthErrorHtml(`${providerName} authorization failed.`,description)),finish({error:new Error(`${providerName} authorization failed: ${description}`)});return}let code=url.searchParams.get("code");if(!code){sendPage(response,400,oauthErrorHtml("Missing authorization code."));return}claimed=!0;try{let value=await options.complete(code);sendPage(response,200,oauthSuccessHtml(`Signed in to ${providerName}. You may now close this page.`)),finish({value})}catch(error2){let failure=error2 instanceof Error?error2:new Error(String(error2));sendPage(response,502,oauthErrorHtml(`${providerName} sign-in failed.`,failure.message)),finish({error:failure})}})()});options.sharedPort?await listenOnLoginPort(server,{port:options.port,host:options.host,signal}):await new Promise((resolve,reject)=>{server.once("error",reject),server.listen(options.port,options.host,()=>{server.off("error",reject),resolve()})});let address=server.address();if(!address||typeof address=="string")throw server.close(),new Error("OAuth callback server did not bind to TCP");server.on("error",error=>finish({error})),signal?.addEventListener("abort",onAbort,{once:!0}),options.timeoutMs!==void 0&&(timer=setTimeout(()=>finish({error:new Error(`${providerName} sign-in timed out`)}),options.timeoutMs));let redirectHost=options.redirectHost??options.host;return{redirectUri:`http://${redirectHost.includes(":")?`[${redirectHost}]`:redirectHost}:${address.port}${options.path}`,wait:()=>waitPromise,cancel:()=>{claimed||finish({value:void 0})},close:()=>{finish({error:new Error("OAuth callback server closed")}),options.sharedPort?closeCallbackServer(server):server.close()}}}async function waitForCallbackOrManualInput(interaction,callback,prompt){let manualAbort=new AbortController,manualError,manual=interaction.prompt({type:"manual_code",...prompt,signal:manualAbort.signal}).then(input=>(callback?.cancel(),input)).catch(error=>{manualError=error instanceof Error?error:new Error(String(error)),callback?.cancel()});try{let value=await callback?.wait();if(manualError)throw manualError;if(value!==void 0)return{type:"callback",value};let input=await manual;if(manualError)throw manualError;return{type:"manual",input:input??""}}finally{manualAbort.abort()}}function base64urlEncode(bytes){let binary="";for(let byte of bytes)binary+=String.fromCharCode(byte);return btoa(binary).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function generatePKCE(){let verifierBytes=new Uint8Array(32);crypto.getRandomValues(verifierBytes);let verifier=base64urlEncode(verifierBytes),data=new TextEncoder().encode(verifier),hashBuffer=await crypto.subtle.digest("SHA-256",data),challenge=base64urlEncode(new Uint8Array(hashBuffer));return{verifier,challenge}}var decode=s=>atob(s),CLIENT_ID=decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl"),AUTHORIZE_URL="https://claude.ai/oauth/authorize",TOKEN_URL="https://platform.claude.com/v1/oauth/token",CALLBACK_HOST=getProviderEnvValue("PI_OAUTH_CALLBACK_HOST")||"127.0.0.1",CALLBACK_PORT=53692,CALLBACK_PATH="/callback",REDIRECT_URI=`http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`,COPY_CODE_REDIRECT_URI="https://platform.claude.com/oauth/code/callback",ANTHROPIC_BROWSER_LOGIN_METHOD="browser",ANTHROPIC_COPY_CODE_LOGIN_METHOD="copy_code",SCOPES="org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";function parseAuthorizationInput(input){let value=input.trim();if(!value)return{};try{let url=new URL(value);return{code:url.searchParams.get("code")??void 0,state:url.searchParams.get("state")??void 0}}catch{}if(value.includes("#")){let[code,state]=value.split("#",2);return{code,state}}if(value.includes("code=")){let params=new URLSearchParams(value);return{code:params.get("code")??void 0,state:params.get("state")??void 0}}return{code:value}}function formatErrorDetails(error){if(error instanceof Error){let details=[`${error.name}: ${error.message}`],errorWithCode=error;return errorWithCode.code&&details.push(`code=${errorWithCode.code}`),typeof errorWithCode.errno<"u"&&details.push(`errno=${String(errorWithCode.errno)}`),typeof error.cause<"u"&&details.push(`cause=${formatErrorDetails(error.cause)}`),error.stack&&details.push(`stack=${error.stack}`),details.join("; ")}return String(error)}async function postJson(url,body,signal){let response=await fetch(url,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(body),signal:AbortSignal.any([signal,AbortSignal.timeout(3e4)])}),responseBody=await response.text();if(!response.ok)throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`);return responseBody}async function exchangeAuthorizationCode(code,state,verifier,redirectUri,signal){let responseBody;try{responseBody=await postJson(TOKEN_URL,{grant_type:"authorization_code",client_id:CLIENT_ID,code,state,redirect_uri:redirectUri,code_verifier:verifier},signal)}catch(error){throw new Error(`Token exchange request failed. url=${TOKEN_URL}; redirect_uri=${redirectUri}; response_type=authorization_code; details=${formatErrorDetails(error)}`)}let tokenData;try{tokenData=JSON.parse(responseBody)}catch(error){throw new Error(`Token exchange returned invalid JSON. url=${TOKEN_URL}; body=${responseBody}; details=${formatErrorDetails(error)}`)}return{type:"oauth",refresh:tokenData.refresh_token,access:tokenData.access_token,expires:Date.now()+tokenData.expires_in*1e3-300*1e3}}async function loginAnthropic(interaction){let{verifier,challenge}=await generatePKCE(),startCallbackServer=port=>startOAuthCallbackServer({providerName:"Anthropic",host:CALLBACK_HOST,port,path:CALLBACK_PATH,redirectHost:"localhost",state:verifier,complete:async code=>code,signal:interaction.signal}),callback=await startCallbackServer(CALLBACK_PORT).catch(()=>startCallbackServer(0)).catch(()=>{}),redirectUri=callback?.redirectUri??REDIRECT_URI;try{let authParams=new URLSearchParams({code:"true",client_id:CLIENT_ID,response_type:"code",redirect_uri:redirectUri,scope:SCOPES,code_challenge:challenge,code_challenge_method:"S256",state:verifier});interaction.notify({type:"auth_url",url:`${AUTHORIZE_URL}?${authParams.toString()}`,instructions:"Complete login in your browser. If the browser is on another machine, paste the final redirect URL here."});let result=await waitForCallbackOrManualInput(interaction,callback,{message:"Complete login in your browser, or paste the authorization code / redirect URL here:",placeholder:redirectUri}),code,state=verifier;if(result.type==="callback")code=result.value;else{let parsed=parseAuthorizationInput(result.input);if(parsed.state&&parsed.state!==verifier)throw new Error("OAuth state mismatch");code=parsed.code,state=parsed.state??verifier}if(!code)throw new Error("Missing authorization code");return interaction.notify({type:"progress",message:"Exchanging authorization code for tokens..."}),await exchangeAuthorizationCode(code,state,verifier,redirectUri,interaction.signal)}finally{callback?.close()}}async function loginAnthropicCopyCode(interaction){let{verifier,challenge}=await generatePKCE(),authParams=new URLSearchParams({code:"true",client_id:CLIENT_ID,response_type:"code",redirect_uri:COPY_CODE_REDIRECT_URI,scope:SCOPES,code_challenge:challenge,code_challenge_method:"S256",state:verifier});interaction.notify({type:"auth_url",url:`${AUTHORIZE_URL}?${authParams.toString()}`,instructions:"Complete login in your browser, then copy the code Anthropic shows and paste it here."});let input=await interaction.prompt({type:"manual_code",message:"Paste the code Anthropic shows after you sign in:",placeholder:"code#state",signal:interaction.signal}),parsed=parseAuthorizationInput(input);if(parsed.state&&parsed.state!==verifier)throw new Error("OAuth state mismatch");if(!parsed.code)throw new Error("Missing authorization code");return interaction.notify({type:"progress",message:"Exchanging authorization code for tokens..."}),await exchangeAuthorizationCode(parsed.code,parsed.state??verifier,verifier,COPY_CODE_REDIRECT_URI,interaction.signal)}async function refreshAnthropicToken(refreshToken,signal){let responseBody;try{responseBody=await postJson(TOKEN_URL,{grant_type:"refresh_token",client_id:CLIENT_ID,refresh_token:refreshToken},signal)}catch(error){throw new Error(`Anthropic token refresh request failed. url=${TOKEN_URL}; details=${formatErrorDetails(error)}`)}let data;try{data=JSON.parse(responseBody)}catch(error){throw new Error(`Anthropic token refresh returned invalid JSON. url=${TOKEN_URL}; body=${responseBody}; details=${formatErrorDetails(error)}`)}return{type:"oauth",refresh:data.refresh_token,access:data.access_token,expires:Date.now()+data.expires_in*1e3-300*1e3}}var anthropicOAuth={name:"Anthropic (Claude Pro/Max)",isSubscription:!0,async login(interaction){let method=await interaction.prompt({type:"select",message:"Select Anthropic login method:",options:[{id:ANTHROPIC_BROWSER_LOGIN_METHOD,label:"Browser login (default)"},{id:ANTHROPIC_COPY_CODE_LOGIN_METHOD,label:"Copy code login (headless)"}]});if(method===ANTHROPIC_COPY_CODE_LOGIN_METHOD)return loginAnthropicCopyCode(interaction);if(method!==ANTHROPIC_BROWSER_LOGIN_METHOD)throw new Error(`Unknown Anthropic login method: ${method}`);return loginAnthropic(interaction)},refresh:(credential,signal)=>refreshAnthropicToken(credential.refresh,signal),async toAuth(credential){return{apiKey:credential.access}}};export{anthropicOAuth};