@gotgenes/pi-anthropic-auth 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christopher D. Lasher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # pi-anthropic-auth
2
+
3
+ Minimal Pi extension package for Anthropic Claude Pro/Max OAuth compatibility.
4
+
5
+ ## Purpose
6
+
7
+ This package minimally overrides Pi's built-in `anthropic` provider to improve Claude Pro/Max OAuth compatibility while preserving Pi's normal Anthropic behavior wherever possible.
8
+
9
+ The design goal is to preserve:
10
+
11
+ 1. built-in provider name: `anthropic`
12
+ 2. built-in model list
13
+ 3. normal Anthropic API-key behavior
14
+ 4. native `/login anthropic` UX
15
+
16
+ ## Current Behavior
17
+
18
+ The extension currently does the following:
19
+
20
+ 1. re-registers the built-in `anthropic` provider with only an `oauth` override
21
+ 2. reuses Pi's native Anthropic login and refresh helpers from `@mariozechner/pi-ai/oauth`
22
+ 3. preserves the old refresh token when refresh responses omit `refresh_token`
23
+ 4. shapes OAuth Anthropic request payloads through `before_provider_request`
24
+ 5. prepends the Anthropic billing/content-consistency header block to `system[]`
25
+ 6. avoids adding `cache_control` to the injected billing block
26
+ 7. normalizes assistant `[tool_use..., text]` ordering for Anthropic OAuth payloads
27
+ 8. replaces Pi's default prompt body with a minimal neutral prompt for Anthropic OAuth when the default Pi harness prompt is detected, while preserving project context
28
+
29
+ The extension does not currently replace Pi's built-in Anthropic streaming transport.
30
+
31
+ ## Requirements
32
+
33
+ 1. `pnpm`
34
+ 2. a local `pi` installation
35
+ 3. Anthropic OAuth credentials configured through Pi
36
+
37
+ ## Install Dependencies
38
+
39
+ ```bash
40
+ pnpm install
41
+ ```
42
+
43
+ ## Development
44
+
45
+ Typecheck:
46
+
47
+ ```bash
48
+ pnpm run build
49
+ ```
50
+
51
+ Run tests:
52
+
53
+ ```bash
54
+ pnpm test
55
+ ```
56
+
57
+ Run both:
58
+
59
+ ```bash
60
+ pnpm run check
61
+ ```
62
+
63
+ ## Load In Pi
64
+
65
+ You can load the extension directly from the local source file:
66
+
67
+ ```bash
68
+ pi -e /absolute/path/to/pi-anthropic-auth/src/index.ts
69
+ ```
70
+
71
+ Example:
72
+
73
+ ```bash
74
+ pi -e /Users/chris/development/pi-anthropic-auth/src/index.ts
75
+ ```
76
+
77
+ ## Fast Repro Loop
78
+
79
+ The most useful non-interactive repro loop is:
80
+
81
+ ```bash
82
+ pi \
83
+ --model anthropic/claude-sonnet-4-6 \
84
+ --no-session \
85
+ --tools read,grep,find,ls \
86
+ -e /Users/chris/development/pi-anthropic-auth/src/index.ts \
87
+ -p "How many lines are in @AGENTS.md ?"
88
+ ```
89
+
90
+ That path was used to validate:
91
+
92
+ 1. simple prompts
93
+ 2. tool use
94
+ 3. multi-turn continuation
95
+ 4. structured output
96
+ 5. expired-token refresh
97
+
98
+ ## Usage Notes
99
+
100
+ 1. `/login anthropic` should continue using Pi's native Anthropic UX.
101
+ 2. API-key Anthropic behavior should remain the baseline behavior.
102
+ 3. The extension's compatibility logic is intended to affect only Anthropic OAuth requests.
103
+
104
+ ## Project Skills
105
+
106
+ Project-local skills live in `.agents/skills/`:
107
+
108
+ 1. `anthropic`: Anthropic OAuth debugging workflow and lessons learned
109
+ 2. `pi-cli-repro`: repeatable `pi -p ... -e ...` repro workflow
110
+ 3. `frontmatter`: Pi skill frontmatter template and rules
111
+
112
+ ## Key Files
113
+
114
+ 1. `src/index.ts`
115
+ 2. `src/anthropic-oauth.ts`
116
+ 3. `src/request-shaping.ts`
117
+ 4. `src/system-prompt-shaping.ts`
118
+ 5. `docs/plans/minimal-anthropic-override.md`
119
+ 6. `docs/plans/gap-analysis-and-next-steps.md`
120
+ 7. `AGENTS.md`
121
+ 8. `.agents/skills/`
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,8 @@
1
+ import type { OAuthCredentials, OAuthLoginCallbacks } from "@mariozechner/pi-ai";
2
+ export declare function mergeRefreshedCredentials(credentials: OAuthCredentials, refreshed: Partial<OAuthCredentials>): OAuthCredentials;
3
+ export declare const anthropicOAuthOverride: {
4
+ readonly name: "Anthropic (Claude Pro/Max)";
5
+ readonly login: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials>;
6
+ readonly refreshToken: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
7
+ readonly getApiKey: (credentials: OAuthCredentials) => string;
8
+ };
@@ -0,0 +1,24 @@
1
+ import { loginAnthropic, refreshAnthropicToken, } from "@mariozechner/pi-ai/oauth";
2
+ export function mergeRefreshedCredentials(credentials, refreshed) {
3
+ return {
4
+ ...credentials,
5
+ ...refreshed,
6
+ refresh: typeof refreshed.refresh === "string" &&
7
+ refreshed.refresh.trim().length > 0
8
+ ? refreshed.refresh
9
+ : credentials.refresh,
10
+ };
11
+ }
12
+ export const anthropicOAuthOverride = {
13
+ name: "Anthropic (Claude Pro/Max)",
14
+ login(callbacks) {
15
+ return loginAnthropic(callbacks);
16
+ },
17
+ async refreshToken(credentials) {
18
+ const refreshed = await refreshAnthropicToken(credentials.refresh);
19
+ return mergeRefreshedCredentials(credentials, refreshed);
20
+ },
21
+ getApiKey(credentials) {
22
+ return credentials.access;
23
+ },
24
+ };
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export default function (pi: ExtensionAPI): void;
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import { anthropicOAuthOverride } from "./anthropic-oauth.js";
2
+ import { shapeAnthropicOAuthPayload } from "./request-shaping.js";
3
+ import { shapeAnthropicOAuthSystemPrompt } from "./system-prompt-shaping.js";
4
+ export default function (pi) {
5
+ pi.registerProvider("anthropic", {
6
+ oauth: anthropicOAuthOverride,
7
+ });
8
+ pi.on("before_agent_start", (event) => {
9
+ return {
10
+ systemPrompt: shapeAnthropicOAuthSystemPrompt(event.systemPrompt),
11
+ };
12
+ });
13
+ pi.on("before_provider_request", (event) => {
14
+ return shapeAnthropicOAuthPayload(event.payload);
15
+ });
16
+ }
@@ -0,0 +1 @@
1
+ export declare function shapeAnthropicOAuthPayload(payload: unknown): unknown;
@@ -0,0 +1,149 @@
1
+ import { createHash } from "node:crypto";
2
+ const ANTHROPIC_OAUTH_BETAS = ["claude-code-20250219", "oauth-2025-04-20"];
3
+ const BILLING_HEADER_SALT = "59cf53e54c78";
4
+ const BILLING_HEADER_POSITIONS = [4, 7, 20];
5
+ const CLAUDE_CODE_VERSION = "2.1.87";
6
+ const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
7
+ const CLAUDE_CODE_IDENTITY_PREFIX = "You are Claude Code, Anthropic's official CLI";
8
+ const PI_MINIMAL_ANTHROPIC_PROMPT_PREFIX = "You are an expert coding assistant.";
9
+ function isRecord(value) {
10
+ return value !== null && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+ function isAnthropicMessagesPayload(payload) {
13
+ return (isRecord(payload) &&
14
+ typeof payload.model === "string" &&
15
+ Array.isArray(payload.messages) &&
16
+ typeof payload.stream === "boolean");
17
+ }
18
+ function isOAuthAnthropicPayload(payload) {
19
+ if (!Array.isArray(payload.system)) {
20
+ return false;
21
+ }
22
+ return payload.system.some(hasOAuthAnthropicSystemMarker);
23
+ }
24
+ function hasOAuthAnthropicSystemMarker(block) {
25
+ if (!isRecord(block) ||
26
+ block.type !== "text" ||
27
+ typeof block.text !== "string") {
28
+ return false;
29
+ }
30
+ return (block.text.includes(CLAUDE_CODE_IDENTITY_PREFIX) ||
31
+ block.text.includes("x-anthropic-billing-header:") ||
32
+ block.text.startsWith(PI_MINIMAL_ANTHROPIC_PROMPT_PREFIX));
33
+ }
34
+ function _hasClaudeCodeIdentity(block) {
35
+ return (isRecord(block) &&
36
+ block.type === "text" &&
37
+ typeof block.text === "string" &&
38
+ block.text.includes(CLAUDE_CODE_IDENTITY_PREFIX));
39
+ }
40
+ function getFirstUserText(messages) {
41
+ const firstUserMessage = messages.find((message) => message.role === "user");
42
+ if (!firstUserMessage)
43
+ return "";
44
+ if (typeof firstUserMessage.content === "string") {
45
+ return firstUserMessage.content;
46
+ }
47
+ if (!Array.isArray(firstUserMessage.content)) {
48
+ return "";
49
+ }
50
+ const firstTextBlock = firstUserMessage.content.find((block) => block?.type === "text" && typeof block.text === "string");
51
+ return typeof firstTextBlock?.text === "string" ? firstTextBlock.text : "";
52
+ }
53
+ function buildBillingHeaderValue(messages) {
54
+ const messageText = getFirstUserText(messages);
55
+ if (!messageText) {
56
+ return undefined;
57
+ }
58
+ const cch = createHash("sha256")
59
+ .update(messageText)
60
+ .digest("hex")
61
+ .slice(0, 5);
62
+ const sampledCharacters = BILLING_HEADER_POSITIONS.map((index) => messageText[index] || "0").join("");
63
+ const suffix = createHash("sha256")
64
+ .update(`${BILLING_HEADER_SALT}${sampledCharacters}${CLAUDE_CODE_VERSION}`)
65
+ .digest("hex")
66
+ .slice(0, 3);
67
+ return [
68
+ "x-anthropic-billing-header:",
69
+ `cc_version=${CLAUDE_CODE_VERSION}.${suffix};`,
70
+ `cc_entrypoint=${CLAUDE_CODE_ENTRYPOINT};`,
71
+ `cch=${cch};`,
72
+ ].join(" ");
73
+ }
74
+ function normalizeSystemBlock(block) {
75
+ if (typeof block === "string") {
76
+ return { type: "text", text: block };
77
+ }
78
+ if (isRecord(block) && typeof block.text === "string") {
79
+ return {
80
+ ...block,
81
+ type: block.type === "text" ? "text" : "text",
82
+ text: block.text,
83
+ };
84
+ }
85
+ return { type: "text", text: String(block ?? "") };
86
+ }
87
+ function prependBillingHeader(system, messages) {
88
+ const billingHeader = buildBillingHeaderValue(messages);
89
+ if (!billingHeader) {
90
+ return system;
91
+ }
92
+ const systemBlocks = Array.isArray(system)
93
+ ? system.map(normalizeSystemBlock)
94
+ : system == null
95
+ ? []
96
+ : [normalizeSystemBlock(system)];
97
+ if (systemBlocks.some((block) => block.text.includes("x-anthropic-billing-header:"))) {
98
+ return systemBlocks;
99
+ }
100
+ const billingBlock = { type: "text", text: billingHeader };
101
+ return [billingBlock, ...systemBlocks];
102
+ }
103
+ function mergeAnthropicBetas(betaHeader) {
104
+ const existing = (betaHeader ?? "")
105
+ .split(",")
106
+ .map((value) => value.trim())
107
+ .filter(Boolean);
108
+ return [...new Set([...ANTHROPIC_OAUTH_BETAS, ...existing])].join(",");
109
+ }
110
+ function splitAssistantToolUseTrailingContent(messages) {
111
+ return messages.flatMap((message) => {
112
+ if (message.role !== "assistant" || !Array.isArray(message.content)) {
113
+ return [message];
114
+ }
115
+ const firstToolUseIndex = message.content.findIndex((block) => block?.type === "tool_use");
116
+ if (firstToolUseIndex === -1) {
117
+ return [message];
118
+ }
119
+ const trailingBlocks = message.content.slice(firstToolUseIndex);
120
+ if (!trailingBlocks.some((block) => block?.type !== "tool_use")) {
121
+ return [message];
122
+ }
123
+ const nonToolUseBlocks = message.content.filter((block) => block?.type !== "tool_use");
124
+ const toolUseBlocks = message.content.filter((block) => block?.type === "tool_use");
125
+ return [
126
+ { ...message, content: nonToolUseBlocks },
127
+ { ...message, content: toolUseBlocks },
128
+ ];
129
+ });
130
+ }
131
+ export function shapeAnthropicOAuthPayload(payload) {
132
+ if (!isAnthropicMessagesPayload(payload)) {
133
+ return payload;
134
+ }
135
+ const messages = payload.messages;
136
+ if (!isOAuthAnthropicPayload(payload)) {
137
+ return payload;
138
+ }
139
+ const normalizedMessages = splitAssistantToolUseTrailingContent(messages);
140
+ const shapedPayload = {
141
+ ...payload,
142
+ messages: normalizedMessages,
143
+ system: prependBillingHeader(payload.system, normalizedMessages),
144
+ };
145
+ if (typeof payload["anthropic-beta"] === "string") {
146
+ shapedPayload["anthropic-beta"] = mergeAnthropicBetas(payload["anthropic-beta"]);
147
+ }
148
+ return shapedPayload;
149
+ }
@@ -0,0 +1 @@
1
+ export declare function shapeAnthropicOAuthSystemPrompt(systemPrompt: string): string;
@@ -0,0 +1,21 @@
1
+ const PI_DEFAULT_PROMPT_PREFIX = "You are an expert coding assistant operating inside pi, a coding agent harness.";
2
+ const MINIMAL_ANTHROPIC_OAUTH_PROMPT = [
3
+ "You are an expert coding assistant.",
4
+ "Be concise and helpful.",
5
+ "Use the available tools to answer the user's request.",
6
+ "Show file paths clearly when working with files.",
7
+ ].join("\n");
8
+ function findProjectContextStart(systemPrompt) {
9
+ const marker = "\n\n# Project Context\n\n";
10
+ return systemPrompt.indexOf(marker);
11
+ }
12
+ export function shapeAnthropicOAuthSystemPrompt(systemPrompt) {
13
+ if (!systemPrompt.includes(PI_DEFAULT_PROMPT_PREFIX)) {
14
+ return systemPrompt;
15
+ }
16
+ const projectContextStart = findProjectContextStart(systemPrompt);
17
+ if (projectContextStart === -1) {
18
+ return MINIMAL_ANTHROPIC_OAUTH_PROMPT;
19
+ }
20
+ return `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}${systemPrompt.slice(projectContextStart)}`;
21
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@gotgenes/pi-anthropic-auth",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension package for Anthropic OAuth compatibility",
5
+ "author": {
6
+ "name": "Chris Lasher"
7
+ },
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "keywords": [
20
+ "pi",
21
+ "pi-package",
22
+ "anthropic",
23
+ "oauth"
24
+ ],
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/gotgenes/pi-anthropic-auth.git"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "check": "tsc -p tsconfig.json --noEmit",
36
+ "lint": "biome check .",
37
+ "lint:fix": "biome check --write .",
38
+ "lint:md": "markdownlint-cli2 '*.md'",
39
+ "lint:all": "pnpm run lint && pnpm run lint:md",
40
+ "format": "biome format --write .",
41
+ "test": "tsx --test test/**/*.test.ts"
42
+ },
43
+ "pi": {
44
+ "extensions": [
45
+ "./dist/index.js"
46
+ ]
47
+ },
48
+ "dependencies": {
49
+ "@anthropic-ai/sdk": "^0.52.0",
50
+ "@mariozechner/pi-ai": "^0.68.0",
51
+ "@mariozechner/pi-coding-agent": "^0.68.0"
52
+ },
53
+ "devDependencies": {
54
+ "@biomejs/biome": "^2.4.12",
55
+ "markdownlint-cli2": "^0.22.0",
56
+ "tsx": "^4.20.6",
57
+ "typescript": "^5.9.3"
58
+ }
59
+ }