pi-plus 0.1.8 → 0.1.10

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 ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ - Compiled `pi-plus` CLI artifact: `build.mjs` bundles `pp` (pi + the plus override layer) into `dist/npm/`, a self-contained staging tree for `npm link` / `npm publish`. Exposes a single `pi-plus` bin — never `pi`, and not `pp` (collides with Homebrew nss's `pp` on macOS).
8
+ - The plus config wrapper now resolves package assets (themes, HTML export template, interactive assets) from the staged bundle layout.
9
+ - The version check against pi's release train (pi.dev) is skipped.
package/README.md CHANGED
@@ -1,172 +1,76 @@
1
- # pi-plus
2
-
3
- Claude Code-exact context usage detection for [pi](https://github.com/earendil-works/pi).
4
-
5
- This extension measures pi's context usage with the **same arithmetic as Claude Code** —
6
- usage-based prefix counting plus rough tail estimation, CC's context-window resolution,
7
- and CC's warning thresholds. It replaces pi's built-in footer so its context field
8
- shows the plugin's CC-exact usage measured against the auto-compact threshold.
9
- Optionally, it can also take over auto-compaction and fire `ctx.compact()` at CC's
10
- auto-compact threshold.
11
-
12
- ## What it replicates (from Claude Code's source)
13
-
14
- | Behavior | CC source | Here |
15
- |---|---|---|
16
- | Context window resolution (`[1m]` suffix → capability ≥100k → 200k default) | `utils/context.ts` | `src/model-config.ts` |
17
- | Max output tokens per model + 8k slot-reservation cap | `utils/context.ts`, `services/api/claude.ts` | `src/model-config.ts` |
18
- | Effective window = window − min(maxOutput, 20k) | `services/compact/autoCompact.ts` | `src/model-config.ts` |
19
- | Auto-compact threshold = effective − 13,000 | `services/compact/autoCompact.ts` | `src/model-config.ts` |
20
- | Context measurement: last API usage (input+cacheWrite+cacheRead+output) + chars/4 tail estimate; images/PDFs flat 2000 | `utils/tokens.ts`, `services/tokenEstimation.ts` | `src/usage.ts` |
21
- | Statusline percentage excludes output tokens; raw window denominator | `utils/context.ts` | `src/percentages.ts` |
22
- | Warning/error/blocking thresholds (20k/20k/3k buffers) | `services/compact/autoCompact.ts` | `src/percentages.ts` |
23
- | Footer replication: cwd/branch line, token stats, model right side, extension-status line | `modes/interactive/components/footer.ts` | `src/footer-render.ts` |
24
-
25
- This plugin supports **no Claude Code environment variables**
26
- (`CLAUDE_CODE_*`, `DISABLE_COMPACT`, … are all ignored). All configuration is
27
- done through the settings system described below; the only env vars honored
28
- are the plugin's own kill-switches, `PI_PLUS_NO_AUTO_COMPACT` and
29
- `PI_PLUS_NO_LOG`.
30
-
31
- On a real session the plugin's token count matches pi's own `ctx.getContextUsage().tokens`
32
- exactly for the Anthropic-style usage shape (pi's `cacheWrite` already includes the 1h
33
- cache subset and `reasoning` ⊂ `output`, so the field-for-field sums coincide).
34
-
35
- ## Install
1
+ # plus
36
2
 
37
- ```bash
38
- # quick try
39
- pi -e ./src/index.ts
3
+ All override/custom logic lives here. Direct updates to any other folder under `packages/` are forbidden — upstream packages stay pristine so they can be synced/updated without merge conflicts.
40
4
 
41
- # permanent: symlink into pi's extension directory
42
- ln -s "$PWD/src" ~/.pi/agent/extensions/pi-plus
43
- # (or copy src/ there)
44
- ```
5
+ ## What this layer does
45
6
 
46
- ## Custom footer (default on)
7
+ Ports Claude Code semantics (reference: `../free-code/`, CC v2.1.87) onto pi's context detection, compaction, and reasoning — with pi-style env var names:
47
8
 
48
- By default the plugin takes over pi's footer via `ctx.ui.setFooter()` and
49
- replicates the built-in rendering — cwd/git-branch line, token stats
50
- (`↑ ↓ R W CH $`), right-aligned model/thinking level, and the extension-status
51
- line — with one substitution: **the context field shows the plugin's CC-exact
52
- usage measured against the auto-compact threshold**, e.g. `17%/187k (auto)`.
9
+ 1. **Context usage detection** (`src/context/detection.ts`) — effective context window = `contextWindow − min(maxTokens, 20000)`; auto-compact threshold = effective window − 13k; warning/error = threshold ∓ 20k; blocking limit = effective window − 3k; 3-failure circuit breaker.
10
+ 2. **Compaction** (`src/compaction/`) — single full-conversation summary using the CC 9-section prompt (`prompt.ts`); prompt-too-long retries drop the oldest turn (`compact.ts`); post-compact re-injection of up to 5 recently read files (50k token budget).
11
+ 3. **Reasoning effort** (`src/reasoning/effort.ts`) — CC effort levels (`low`/`medium`/`high`/`max`, `max` → `xhigh` → `high` downgrading), adaptive thinking default, `ultrathink` keyword → high effort for that turn, thinking budgets from env.
12
+ 4. **Hub profiles** (`src/coding-agent/main.ts` wrapper, backed by `@earendil-works/pi-hub` = `packages/hub`) — named pi profiles (provider/models/thinking/token/base URL) stored in `~/.pi/profiles.json`, materialized into isolated agent dirs under `~/.pi/pi-hub/profiles/<name>/`. Adds `pp profile …`, `pp use` / `pp unuse`, the deprecated `pp run`, and the `pp --profile <name>` flag. The wrapper resolves the profile, sets `PI_CODING_AGENT_DIR` in-process (read lazily by `getAgentDir()`), and delegates to the original `main`.
53
13
 
54
- Semantics differ deliberately from pi's native `1.8%/1.0M` field:
14
+ The CLI identifies as **`pp`** (pi-plus) via the `config.ts` wrapper (`APP_NAME`/`APP_TITLE` shadow). Scope is the display name only — config dir (`.pi`), `PI_`-prefixed env vars, and session layout stay pi's.
55
15
 
56
- - **Numerator** is the same measurement both sides already share (last valid
57
- API usage + chars/4 tail estimation).
58
- - **Denominator is the threshold** (effective window − 13k, settings-overridable),
59
- not the raw registry window — so the percentage reaches 100% exactly when
60
- the plugin's auto-compact fires. It rises as context fills, like pi's field.
61
- - ` (auto)` is appended when auto-compact is enabled; right after `/compact`
62
- the field shows `?/187k` until the next assistant response (pi's own
63
- convention for unknown post-compaction usage).
64
- - Colors follow the plugin's warning state: warning color inside the 20k
65
- warning buffer, error past the error threshold or blocking limit.
16
+ ## Mechanism
66
17
 
67
- Two built-in details are not reproducible from the extension API and are
68
- omitted: the `• xp` experimental-features marker, and the `(sub)` suffix for
69
- subscription providers other than `kimi-coding`.
18
+ - `loader/hooks.mjs` — Node module customization hook. Two jobs: (a) map `@earendil-works/*` specifiers to `packages/*/src` (mirrors root tsconfig paths), (b) redirect selected upstream modules to the wrapper modules below. Importers inside `packages/plus/` are never redirected, so wrappers can import the true original via relative path without a loop.
19
+ - `loader/run-plus.mjs` + `pp` — entry point. Runs pi from TypeScript sources on Node's native type stripping (the repo is `erasableSyntaxOnly`). tsx is intentionally not used: its load hook silently produces empty modules when another customization hook is registered alongside it (Node 25).
20
+ - `src/<upstream-path>.ts` — wrapper modules. Each does `export * from "<relative path to original>"` and redefines selected exports; per ESM spec, explicit named exports shadow star exports, so everything not overridden passes through untouched.
21
+ - Redirect map: `loader/redirects.mjs` (7 modules: coding-agent compaction/model-resolver/defaults/config/main, agent agent.ts, agent harness compaction).
70
22
 
71
- **Conflict warning:** `setFooter` is last-caller-wins — pi-plus must be the
72
- only footer-taking extension. Another extension calling `setFooter` (e.g.
73
- `better-claude-code-ui`) replaces this footer entirely. Plain `setStatus`
74
- extensions are fine; they appear on the status line as usual.
23
+ ## Env vars (pi-style names)
75
24
 
76
- Set `"customFooter": "false"` (below) and `/reload` (or start a new session)
77
- to restore pi's built-in footer.
25
+ | Var | Values | Effect |
26
+ | ----------------------------- | ----------------------------------- | --------------------------------------------------------------- |
27
+ | `PI_MAX_CONTEXT_TOKENS` | number | Overrides `model.contextWindow` after model resolution |
28
+ | `PI_AUTO_COMPACT_WINDOW` | number | Caps the window used for threshold math |
29
+ | `PI_AUTOCOMPACT_PCT_OVERRIDE` | 1–100 | Auto-compact threshold as % of effective window (capped at default) |
30
+ | `PI_BLOCKING_LIMIT_OVERRIDE` | number | Blocking limit override |
31
+ | `PI_DISABLE_COMPACT` | truthy | Disables all compaction (incl. manual) |
32
+ | `PI_DISABLE_AUTO_COMPACT` | truthy | Disables threshold-triggered auto-compaction |
33
+ | `PI_EFFORT_LEVEL` | `off`/`auto`/`low`/`medium`/`high`/`max` | Per-request reasoning effort override |
34
+ | `PI_MAX_THINKING_TOKENS` | number (`0` disables) | Thinking budget (enables thinking at `high` when > 0) |
35
+ | `PI_DISABLE_THINKING` | truthy | Disables thinking entirely |
36
+ | `PI_DISABLE_ADAPTIVE_THINKING`| truthy | Forces the budget-based thinking path |
78
37
 
79
- ## Auto-compact takeover (optional)
38
+ ## Conventions
80
39
 
81
- pi's built-in auto-compaction fires at `contextWindow − 16,384` tokens, which is
82
- *earlier* than CC's threshold (`effective window − 13,000`, i.e. `window − 21,000`
83
- with the 8k output cap). For the CC threshold to actually govern, disable pi's
84
- built-in trigger in `~/.pi/agent/settings.json`:
40
+ - Mirror the upstream layout when overriding: e.g. logic overriding `packages/coding-agent/src/core/compaction/compaction.ts` lives in `src/coding-agent/core/compaction/compaction.ts`.
41
+ - Prefer extension points over patching: pi's extension API (see `packages/coding-agent/docs/extensions.md`), prompt templates, skills, and settings come first; reach for behavior overrides only when those cannot express the change.
42
+ - Each override module must re-export or wrap the upstream implementation instead of copying it, so upstream fixes propagate.
43
+ - Overrides are wired in only through the loader; nothing under `packages/plus` may be imported by upstream code.
85
44
 
86
- ```json
87
- { "compaction": { "enabled": false } }
88
- ```
45
+ ## Compiled artifact (npm)
89
46
 
90
- The plugin checks the overflow against CC's threshold on **every tool task
91
- finish** (`tool_execution_end`) and calls `ctx.compact()` the moment the
92
- threshold is crossed — pi performs the compaction between agent steps and
93
- continues the turn with the compacted context (the same window pi's own
94
- auto-compact and CC's autocompact use). The trigger is edge-triggered: it
95
- fires once per crossing and re-arms after post-compact usage drops below the
96
- threshold. Manual `/compact` keeps working.
97
-
98
- Set `PI_PLUS_NO_AUTO_COMPACT=1` to disable the plugin's trigger (footer field only).
99
-
100
- ## Configuration
101
-
102
- Settings are read through [pi-extension-settings](https://github.com/juanibiapina/pi-extension-settings)
103
- (the pi ecosystem's standard settings helpers — `getSetting()`), stored under a
104
- `pi-plus` section. Global values live in `~/.pi/agent/settings-extensions.json`,
105
- project values in `.pi/settings-extensions.json` (project wins per key). Values
106
- are strings; invalid or absent values fall back to the hardcoded CC-derived
107
- defaults. If `@juanibiapina/pi-extension-settings` is installed, the settings
108
- also appear in its `/extension-settings` UI.
109
-
110
- ```json
111
- {
112
- "pi-plus": {
113
- "autoCompact": "true",
114
- "contextWindow": "256000",
115
- "autoCompactThreshold": "235000",
116
- "autoCompactPercent": "50",
117
- "autoCompactWindowCap": "200000",
118
- "maxOutputTokens": "8192",
119
- "disableOutputTokenCap": "false",
120
- "blockingLimit": "190000",
121
- "customFooter": "true"
122
- }
123
- }
124
- ```
47
+ `build.mjs` bundles `pp` into `dist/npm/` — a self-contained staging tree that `npm link` /
48
+ `npm publish` operate on (this package itself stays private). The published artifact is
49
+ **`pi-plus`** on npmjs; it installs a single global command, **`pi-plus`** — deliberately
50
+ not `pi` (pi-plus must not own pi's command) and not `pp` (collides with Homebrew nss's
51
+ `pp` on macOS). The CLI itself still identifies as `pp` everywhere (help text, hub
52
+ commands); add `alias pp=pi-plus` to your shell rc for the short name.
125
53
 
126
- | Setting | Effect | Default |
127
- |---|---|---|
128
- | `autoCompact` | Master switch for the plugin's auto-compact trigger (`true`/`false`) | `true` |
129
- | `customFooter` | Replace pi's built-in footer with the plugin's (see above); takes effect on next session/`/reload` | `true` |
130
- | `contextWindow` | Override the model-derived context window (tokens) | model default (200k) |
131
- | `autoCompactThreshold` | Absolute token threshold for auto-compact; wins over everything below | effective window − 13k |
132
- | `autoCompactPercent` | Threshold as % of effective window (1–100, for testing) | off |
133
- | `autoCompactWindowCap` | Cap the window before threshold math (tokens) | off |
134
- | `maxOutputTokens` | Override max output tokens (bounded by the model's limit) | 8k-capped model default |
135
- | `disableOutputTokenCap` | Use the model's native default max output instead of the 8k slot cap | `false` |
136
- | `blockingLimit` | Absolute token usage that blocks further work | effective window − 3k |
137
-
138
- ## Usage-detection log
139
-
140
- Every usage check is appended as one JSON line to `pi-plus.log` inside pi's
141
- agent directory (`$PI_CODING_AGENT_DIR` or `~/.pi/agent`). Each line records
142
- the event (`usage_check` from the indicator or the auto-compact trigger,
143
- plus `auto_compact_fired` / `auto_compact_error`), the model, the measured
144
- token count, and the threshold/percent state:
145
-
146
- ```json
147
- {"ts":"2026-09-17T13:06:11.672Z","event":"usage_check","source":"indicator","model":"kimi-k2","tokens":9977,"percentLeft":94,"aboveWarning":false,"aboveError":false,"aboveAutoCompact":false,"atBlockingLimit":false}
54
+ ```bash
55
+ npm run build # in packages/plus; requires `npm run build:offline` at the repo root first
56
+ npm link # in packages/plus/dist/npm — global `pp`
57
+ npm publish --access public --ignore-scripts # in packages/plus/dist/npm
148
58
  ```
149
59
 
150
- Logging is best-effort — a write failure never affects the host agent.
151
- Set `PI_PLUS_NO_LOG=1` to disable it.
60
+ The bundle applies `loader/redirects.mjs` at bundle time (esbuild plugin) and keeps each
61
+ module a singleton by funneling every `packages/*/src/**` import with a compiled `dist/`
62
+ counterpart to that dist file. `--no-env` is preserved via a banner scrub. Provider
63
+ credentials, config dir (`~/.pi`), and session layout are identical to source-mode `pp`.
152
64
 
153
- ## Known limits
65
+ Known limitations of the compiled artifact:
154
66
 
155
- - With `customFooter` off (or replaced by another extension), pi's built-in
156
- footer keeps showing its own `percent%` (raw-window denominator — a much
157
- larger number for models like `kimi-for-coding` whose registry window is 1M);
158
- the plugin then has no on-screen display, only the log and the auto-compact
159
- trigger.
160
- - pi core owns the compaction cut point (keeps last ~20k tokens) — only the trigger
161
- threshold is CC-exact.
162
- - Exactness is guaranteed for Anthropic-style usage fields; other providers may map
163
- usage differently.
164
- - CC's `percentLeft` is not clamped to 100; neither is this port.
67
+ - `pp server` / `pp client` experimental subcommands (`PI_EXPERIMENTAL=1`) spawn sibling JS
68
+ files that are not emitted next to the bundle; use source-mode `./pp` for those.
69
+ - `docs/` and `examples/` are not shipped, so `/docs`-style paths into them are absent.
165
70
 
166
- ## Development
71
+ ## Tests
167
72
 
168
- ```bash
169
- npm install
170
- npm test # vitest — pure-function ports against CC fixtures
171
- npm run typecheck
73
+ ```
74
+ npx vitest run # from packages/plus; unit tests under test/ (faux streamFn, no real APIs)
75
+ ./pp --no-env # run pp (pi-plus) from sources with the override layer active
172
76
  ```
package/anthropic.js ADDED
@@ -0,0 +1,82 @@
1
+ import { createRequire as __piCreateRequire } from "node:module"; const require = __piCreateRequire(import.meta.url);
2
+ {
3
+ const __noEnvIndex = process.argv.indexOf("--no-env");
4
+ if (__noEnvIndex !== -1) {
5
+ process.argv.splice(__noEnvIndex, 1);
6
+ for (const __key of ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "OPENAI_API_KEY", "GEMINI_API_KEY", "GROQ_API_KEY", "CEREBRAS_API_KEY", "XAI_API_KEY", "OPENROUTER_API_KEY", "ZAI_API_KEY", "MISTRAL_API_KEY", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", "AI_GATEWAY_API_KEY", "OPENCODE_API_KEY", "COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN", "HF_TOKEN", "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT", "GOOGLE_CLOUD_LOCATION", "AWS_PROFILE", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_BEARER_TOKEN_BEDROCK", "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "AWS_CONTAINER_CREDENTIALS_FULL_URI", "AWS_WEB_IDENTITY_TOKEN_FILE", "AZURE_OPENAI_API_KEY", "AZURE_OPENAI_BASE_URL", "AZURE_OPENAI_RESOURCE_NAME"]) delete process.env[__key];
7
+ }
8
+ }
9
+ var __require=(x=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(x,{get:(a,b)=>(typeof require<"u"?require:a)[b]}):x)(function(x){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+x+'" is not supported')});var procEnvCache=null;function getBunSandboxEnvValue(name){if(!(typeof process>"u"||!process.versions?.bun||Object.keys(process.env).length>0)){if(procEnvCache===null){procEnvCache=new Map;try{let{readFileSync}=__require("node:fs"),data=readFileSync("/proc/self/environ","utf-8");for(let entry of data.split("\0")){let idx=entry.indexOf("=");idx>0&&procEnvCache.set(entry.slice(0,idx),entry.slice(idx+1))}}catch{}}return procEnvCache.get(name)}}function getProviderEnvValue(name,env){return env?.[name]||(typeof process<"u"?process.env[name]:void 0)||getBunSandboxEnvValue(name)||void 0}var LOGO_SVG='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" aria-hidden="true"><path fill="#fff" fill-rule="evenodd" d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"/><path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/></svg>';function escapeHtml(value){return value.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;")}function renderPage(options){let title=escapeHtml(options.title),heading=escapeHtml(options.heading),message=escapeHtml(options.message),details=options.details?escapeHtml(options.details):void 0;return`<!doctype html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8" />
13
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
14
+ <title>${title}</title>
15
+ <style>
16
+ :root {
17
+ --text: #fafafa;
18
+ --text-dim: #a1a1aa;
19
+ --page-bg: #09090b;
20
+ --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
21
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
22
+ }
23
+ * { box-sizing: border-box; }
24
+ html { color-scheme: dark; }
25
+ body {
26
+ margin: 0;
27
+ min-height: 100vh;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ padding: 24px;
32
+ background: var(--page-bg);
33
+ color: var(--text);
34
+ font-family: var(--font-sans);
35
+ text-align: center;
36
+ }
37
+ main {
38
+ width: 100%;
39
+ max-width: 560px;
40
+ display: flex;
41
+ flex-direction: column;
42
+ align-items: center;
43
+ justify-content: center;
44
+ }
45
+ .logo {
46
+ width: 72px;
47
+ height: 72px;
48
+ display: block;
49
+ margin-bottom: 24px;
50
+ }
51
+ h1 {
52
+ margin: 0 0 10px;
53
+ font-size: 28px;
54
+ line-height: 1.15;
55
+ font-weight: 650;
56
+ color: var(--text);
57
+ }
58
+ p {
59
+ margin: 0;
60
+ line-height: 1.7;
61
+ color: var(--text-dim);
62
+ font-size: 15px;
63
+ }
64
+ .details {
65
+ margin-top: 16px;
66
+ font-family: var(--font-mono);
67
+ font-size: 13px;
68
+ color: var(--text-dim);
69
+ white-space: pre-wrap;
70
+ word-break: break-word;
71
+ }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <main>
76
+ <div class="logo">${LOGO_SVG}</div>
77
+ <h1>${heading}</h1>
78
+ <p>${message}</p>
79
+ ${details?`<div class="details">${details}</div>`:""}
80
+ </main>
81
+ </body>
82
+ </html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading:"Authentication failed",message,details})}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 nodeApis=null,nodeApisPromise=null,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}`,SCOPES="org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";async function getNodeApis(){if(nodeApis)return nodeApis;if(!nodeApisPromise){if(typeof process>"u"||!process.versions?.node&&!process.versions?.bun)throw new Error("Anthropic OAuth is only available in Node.js environments");nodeApisPromise=import("node:http").then(httpModule=>({createServer:httpModule.createServer}))}return nodeApis=await nodeApisPromise,nodeApis}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 startCallbackServer(expectedState){let{createServer}=await getNodeApis();return new Promise((resolve,reject)=>{let settleWait,waitForCodePromise=new Promise(resolveWait=>{let settled=!1;settleWait=value=>{settled||(settled=!0,resolveWait(value))}}),server=createServer((req,res)=>{try{let url=new URL(req.url||"","http://localhost");if(url.pathname!==CALLBACK_PATH){res.writeHead(404,{"Content-Type":"text/html; charset=utf-8"}),res.end(oauthErrorHtml("Callback route not found."));return}let code=url.searchParams.get("code"),state=url.searchParams.get("state"),error=url.searchParams.get("error");if(error){res.writeHead(400,{"Content-Type":"text/html; charset=utf-8"}),res.end(oauthErrorHtml("Anthropic authentication did not complete.",`Error: ${error}`));return}if(!code||!state){res.writeHead(400,{"Content-Type":"text/html; charset=utf-8"}),res.end(oauthErrorHtml("Missing code or state parameter."));return}if(state!==expectedState){res.writeHead(400,{"Content-Type":"text/html; charset=utf-8"}),res.end(oauthErrorHtml("State mismatch."));return}res.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),res.end(oauthSuccessHtml("Anthropic authentication completed. You can close this window.")),settleWait?.({code,state})}catch{res.writeHead(500,{"Content-Type":"text/plain; charset=utf-8"}),res.end("Internal error")}});server.on("error",err=>{reject(err)}),server.listen(CALLBACK_PORT,CALLBACK_HOST,()=>{resolve({server,redirectUri:REDIRECT_URI,cancelWait:()=>{settleWait?.(null)},waitForCode:()=>waitForCodePromise})})})}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(),server=await startCallbackServer(verifier),manualAbort=new AbortController,onAbort=()=>server.cancelWait();interaction.signal.addEventListener("abort",onAbort,{once:!0}),interaction.signal.aborted&&onAbort();let code,state,manualInput,manualError;try{let authParams=new URLSearchParams({code:"true",client_id:CLIENT_ID,response_type:"code",redirect_uri: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. If the browser is on another machine, paste the final redirect URL here."});let manualPromise=interaction.prompt({type:"manual_code",message:"Complete login in your browser, or paste the authorization code / redirect URL here:",placeholder:REDIRECT_URI,signal:manualAbort.signal}).then(input=>{manualInput=input,server.cancelWait()}).catch(error=>{manualError=error instanceof Error?error:new Error(String(error)),server.cancelWait()}),result=await server.waitForCode();if(manualError)throw manualError;if(result?.code)code=result.code,state=result.state;else if(manualInput){let parsed=parseAuthorizationInput(manualInput);if(parsed.state&&parsed.state!==verifier)throw new Error("OAuth state mismatch");code=parsed.code,state=parsed.state??verifier}if(!code){if(await manualPromise,manualError)throw manualError;if(manualInput){let parsed=parseAuthorizationInput(manualInput);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");if(!state)throw new Error("Missing OAuth state");return interaction.notify({type:"progress",message:"Exchanging authorization code for tokens..."}),exchangeAuthorizationCode(code,state,verifier,REDIRECT_URI,interaction.signal)}finally{interaction.signal.removeEventListener("abort",onAbort),manualAbort.abort(),server.server.close()}}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,login:loginAnthropic,refresh:(credential,signal)=>refreshAnthropicToken(credential.refresh,signal),async toAuth(credential){return{apiKey:credential.access}}};export{anthropicOAuth};