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 +9 -0
- package/README.md +55 -151
- package/anthropic.js +82 -0
- package/bedrock-converse-stream.js +90 -0
- package/core/export-html/template.css +1066 -0
- package/core/export-html/template.html +55 -0
- package/core/export-html/template.js +1864 -0
- package/core/export-html/vendor/highlight.min.js +1213 -0
- package/core/export-html/vendor/marked.min.js +78 -0
- package/github-copilot.js +9 -0
- package/image-resize-worker.js +9 -0
- package/kimi-coding.js +9 -0
- package/modes/interactive/assets/clankolas.png +0 -0
- package/modes/interactive/theme/dark.json +91 -0
- package/modes/interactive/theme/light.json +90 -0
- package/modes/interactive/theme/theme-schema.json +356 -0
- package/openai-codex.js +82 -0
- package/openrouter.js +82 -0
- package/package.json +23 -43
- package/pp.js +2040 -0
- package/radius.js +82 -0
- package/xai.js +9 -0
- package/LICENSE +0 -21
- package/src/auto-compact.ts +0 -43
- package/src/context-usage.ts +0 -91
- package/src/footer-render.ts +0 -248
- package/src/footer.ts +0 -139
- package/src/index.ts +0 -172
- package/src/logger.ts +0 -56
- package/src/model-config.ts +0 -184
- package/src/percentages.ts +0 -99
- package/src/settings.ts +0 -101
- package/src/usage.ts +0 -201
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
ln -s "$PWD/src" ~/.pi/agent/extensions/pi-plus
|
|
43
|
-
# (or copy src/ there)
|
|
44
|
-
```
|
|
5
|
+
## What this layer does
|
|
45
6
|
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
(
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
##
|
|
38
|
+
## Conventions
|
|
80
39
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
{ "compaction": { "enabled": false } }
|
|
88
|
-
```
|
|
45
|
+
## Compiled artifact (npm)
|
|
89
46
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
65
|
+
Known limitations of the compiled artifact:
|
|
154
66
|
|
|
155
|
-
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
##
|
|
71
|
+
## Tests
|
|
167
72
|
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
|
|
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("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}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};
|