@gotgenes/pi-anthropic-auth 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # pi-anthropic-auth
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@gotgenes/pi-anthropic-auth?style=flat&logo=npm&logoColor=white)](https://www.npmjs.com/package/@gotgenes/pi-anthropic-auth)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/gotgenes/pi-anthropic-auth/ci.yml?style=flat&logo=github&label=CI)](https://github.com/gotgenes/pi-anthropic-auth/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
+ [![pnpm](https://img.shields.io/badge/pnpm-%3E%3D9-F69220?style=flat&logo=pnpm&logoColor=white)](https://pnpm.io/)
8
+ [![Pi Package](https://img.shields.io/badge/Pi-Package-6366F1?style=flat)](https://pi.mariozechner.at/)
9
+
3
10
  Minimal Pi extension package for Anthropic Claude Pro/Max OAuth compatibility.
4
11
 
5
12
  ## Purpose
@@ -28,53 +35,63 @@ The extension currently does the following:
28
35
 
29
36
  The extension does not currently replace Pi's built-in Anthropic streaming transport.
30
37
 
31
- ## Requirements
38
+ ## Install
32
39
 
33
- 1. `pnpm`
34
- 2. a local `pi` installation
35
- 3. Anthropic OAuth credentials configured through Pi
40
+ ```bash
41
+ pi install npm:@gotgenes/pi-anthropic-auth
42
+ ```
36
43
 
37
- ## Install Dependencies
44
+ To try it without permanently installing:
38
45
 
39
46
  ```bash
40
- pnpm install
47
+ pi -e npm:@gotgenes/pi-anthropic-auth
41
48
  ```
42
49
 
50
+ ## Usage Notes
51
+
52
+ 1. `/login anthropic` should continue using Pi's native Anthropic UX.
53
+ 2. API-key Anthropic behavior should remain the baseline behavior.
54
+ 3. The extension's compatibility logic is intended to affect only Anthropic OAuth requests.
55
+
43
56
  ## Development
44
57
 
45
- Typecheck:
58
+ ### Requirements
46
59
 
47
- ```bash
48
- pnpm run build
49
- ```
60
+ 1. `pnpm`
61
+ 2. a local `pi` installation
62
+ 3. Anthropic OAuth credentials configured through Pi
50
63
 
51
- Run tests:
64
+ ### Install Dependencies
52
65
 
53
66
  ```bash
54
- pnpm test
67
+ pnpm install
55
68
  ```
56
69
 
57
- Run both:
70
+ ### Typecheck
58
71
 
59
72
  ```bash
60
73
  pnpm run check
61
74
  ```
62
75
 
63
- ## Load In Pi
76
+ ### Run Tests
64
77
 
65
- You can load the extension directly from the local source file:
78
+ ```bash
79
+ pnpm test
80
+ ```
81
+
82
+ ### Build
66
83
 
67
84
  ```bash
68
- pi -e /absolute/path/to/pi-anthropic-auth/src/index.ts
85
+ pnpm run build
69
86
  ```
70
87
 
71
- Example:
88
+ ### Load Local Build In Pi
72
89
 
73
90
  ```bash
74
- pi -e /Users/chris/development/pi-anthropic-auth/src/index.ts
91
+ pi -e /absolute/path/to/pi-anthropic-auth/dist/index.js
75
92
  ```
76
93
 
77
- ## Fast Repro Loop
94
+ ### Fast Repro Loop
78
95
 
79
96
  The most useful non-interactive repro loop is:
80
97
 
@@ -83,7 +100,7 @@ pi \
83
100
  --model anthropic/claude-sonnet-4-6 \
84
101
  --no-session \
85
102
  --tools read,grep,find,ls \
86
- -e /Users/chris/development/pi-anthropic-auth/src/index.ts \
103
+ -e /Users/chris/development/pi-anthropic-auth/dist/index.js \
87
104
  -p "How many lines are in @AGENTS.md ?"
88
105
  ```
89
106
 
@@ -95,12 +112,6 @@ That path was used to validate:
95
112
  4. structured output
96
113
  5. expired-token refresh
97
114
 
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
115
  ## Project Skills
105
116
 
106
117
  Project-local skills live in `.agents/skills/`:
@@ -120,6 +131,10 @@ Project-local skills live in `.agents/skills/`:
120
131
  7. `AGENTS.md`
121
132
  8. `.agents/skills/`
122
133
 
134
+ ## Acknowledgments
135
+
136
+ This project was inspired by [opencode-anthropic-auth](https://github.com/ex-machina-co/opencode-anthropic-auth/), which solved the same Anthropic OAuth compatibility problem for [OpenCode](https://opencode.ai/).
137
+
123
138
  ## License
124
139
 
125
140
  MIT
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Prefix of Pi's built-in default system prompt preamble.
3
+ *
4
+ * Used to detect whether a system block contains Pi's original verbose
5
+ * preamble so it can be replaced with the minimal neutral prompt.
6
+ */
7
+ export declare const PI_DEFAULT_PROMPT_PREFIX = "You are an expert coding assistant operating inside pi, a coding agent harness.";
8
+ /**
9
+ * Prefix of the minimal neutral Anthropic OAuth system prompt.
10
+ *
11
+ * Used as a detection marker in request shaping to identify system blocks
12
+ * that have already been shaped. Must match the first line of
13
+ * MINIMAL_ANTHROPIC_OAUTH_PROMPT.
14
+ */
15
+ export declare const MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX = "You are an expert coding assistant.";
16
+ /**
17
+ * Minimal neutral system prompt used for Anthropic OAuth requests.
18
+ *
19
+ * Replaces Pi's verbose default preamble to avoid prompt fingerprinting
20
+ * while preserving any project context that follows.
21
+ */
22
+ export declare const MINIMAL_ANTHROPIC_OAUTH_PROMPT: string;
23
+ /**
24
+ * Prefix of Claude Code's identity injection block.
25
+ *
26
+ * Used to detect OAuth Anthropic payloads built by Pi's built-in Anthropic
27
+ * provider, which injects a "You are Claude Code, Anthropic's official CLI"
28
+ * system block for OAuth sessions.
29
+ */
30
+ export declare const CLAUDE_CODE_IDENTITY_PREFIX = "You are Claude Code, Anthropic's official CLI";
31
+ /**
32
+ * Claude Code version string embedded in the billing header.
33
+ *
34
+ * **Must be kept in sync with the current Claude Code release.**
35
+ * Update this value when a new Claude Code version ships. If it drifts
36
+ * too far from what Anthropic expects, OAuth requests may be rejected or
37
+ * counted incorrectly.
38
+ */
39
+ export declare const CLAUDE_CODE_VERSION = "2.1.87";
40
+ /** Salt used in the billing header suffix hash. */
41
+ export declare const BILLING_HEADER_SALT = "59cf53e54c78";
42
+ /** Character positions sampled from the first user message for the billing hash. */
43
+ export declare const BILLING_HEADER_POSITIONS: readonly [4, 7, 20];
44
+ /** Entrypoint identifier included in the billing header. */
45
+ export declare const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Prefix of Pi's built-in default system prompt preamble.
3
+ *
4
+ * Used to detect whether a system block contains Pi's original verbose
5
+ * preamble so it can be replaced with the minimal neutral prompt.
6
+ */
7
+ export const PI_DEFAULT_PROMPT_PREFIX = "You are an expert coding assistant operating inside pi, a coding agent harness.";
8
+ /**
9
+ * Prefix of the minimal neutral Anthropic OAuth system prompt.
10
+ *
11
+ * Used as a detection marker in request shaping to identify system blocks
12
+ * that have already been shaped. Must match the first line of
13
+ * MINIMAL_ANTHROPIC_OAUTH_PROMPT.
14
+ */
15
+ export const MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX = "You are an expert coding assistant.";
16
+ /**
17
+ * Minimal neutral system prompt used for Anthropic OAuth requests.
18
+ *
19
+ * Replaces Pi's verbose default preamble to avoid prompt fingerprinting
20
+ * while preserving any project context that follows.
21
+ */
22
+ export const MINIMAL_ANTHROPIC_OAUTH_PROMPT = [
23
+ MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX,
24
+ "Be concise and helpful.",
25
+ "Use the available tools to answer the user's request.",
26
+ "Show file paths clearly when working with files.",
27
+ ].join("\n");
28
+ /**
29
+ * Prefix of Claude Code's identity injection block.
30
+ *
31
+ * Used to detect OAuth Anthropic payloads built by Pi's built-in Anthropic
32
+ * provider, which injects a "You are Claude Code, Anthropic's official CLI"
33
+ * system block for OAuth sessions.
34
+ */
35
+ export const CLAUDE_CODE_IDENTITY_PREFIX = "You are Claude Code, Anthropic's official CLI";
36
+ // ---------------------------------------------------------------------------
37
+ // Billing header constants
38
+ //
39
+ // These values are used to build the x-anthropic-billing-header injected into
40
+ // OAuth requests. They must match the values Anthropic's backend expects for
41
+ // the current Claude Code release.
42
+ //
43
+ // CLAUDE_CODE_VERSION must be updated when Anthropic ships a new Claude Code
44
+ // version. There is no upstream source to import it from; check the current
45
+ // version at https://github.com/anthropics/claude-code or in a working Claude
46
+ // Code installation (`claude --version`).
47
+ // ---------------------------------------------------------------------------
48
+ /**
49
+ * Claude Code version string embedded in the billing header.
50
+ *
51
+ * **Must be kept in sync with the current Claude Code release.**
52
+ * Update this value when a new Claude Code version ships. If it drifts
53
+ * too far from what Anthropic expects, OAuth requests may be rejected or
54
+ * counted incorrectly.
55
+ */
56
+ export const CLAUDE_CODE_VERSION = "2.1.87";
57
+ /** Salt used in the billing header suffix hash. */
58
+ export const BILLING_HEADER_SALT = "59cf53e54c78";
59
+ /** Character positions sampled from the first user message for the billing hash. */
60
+ export const BILLING_HEADER_POSITIONS = [4, 7, 20];
61
+ /** Entrypoint identifier included in the billing header. */
62
+ export const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
package/dist/index.js CHANGED
@@ -1,15 +1,9 @@
1
1
  import { anthropicOAuthOverride } from "./anthropic-oauth.js";
2
2
  import { shapeAnthropicOAuthPayload } from "./request-shaping.js";
3
- import { shapeAnthropicOAuthSystemPrompt } from "./system-prompt-shaping.js";
4
3
  export default function (pi) {
5
4
  pi.registerProvider("anthropic", {
6
5
  oauth: anthropicOAuthOverride,
7
6
  });
8
- pi.on("before_agent_start", (event) => {
9
- return {
10
- systemPrompt: shapeAnthropicOAuthSystemPrompt(event.systemPrompt),
11
- };
12
- });
13
7
  pi.on("before_provider_request", (event) => {
14
8
  return shapeAnthropicOAuthPayload(event.payload);
15
9
  });
@@ -1,11 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { BILLING_HEADER_POSITIONS, BILLING_HEADER_SALT, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY_PREFIX, CLAUDE_CODE_VERSION, MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX, } from "./constants.js";
3
+ import { shapeSystemBlocks } from "./system-prompt-shaping.js";
2
4
  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
5
  function isRecord(value) {
10
6
  return value !== null && typeof value === "object" && !Array.isArray(value);
11
7
  }
@@ -29,13 +25,7 @@ function hasOAuthAnthropicSystemMarker(block) {
29
25
  }
30
26
  return (block.text.includes(CLAUDE_CODE_IDENTITY_PREFIX) ||
31
27
  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));
28
+ block.text.startsWith(MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX));
39
29
  }
40
30
  function getFirstUserText(messages) {
41
31
  const firstUserMessage = messages.find((message) => message.role === "user");
@@ -78,7 +68,7 @@ function normalizeSystemBlock(block) {
78
68
  if (isRecord(block) && typeof block.text === "string") {
79
69
  return {
80
70
  ...block,
81
- type: block.type === "text" ? "text" : "text",
71
+ type: "text",
82
72
  text: block.text,
83
73
  };
84
74
  }
@@ -107,6 +97,15 @@ function mergeAnthropicBetas(betaHeader) {
107
97
  .filter(Boolean);
108
98
  return [...new Set([...ANTHROPIC_OAUTH_BETAS, ...existing])].join(",");
109
99
  }
100
+ /**
101
+ * Splits assistant messages that interleave text and tool_use blocks.
102
+ *
103
+ * The Anthropic API rejects assistant turns where non-tool_use blocks follow
104
+ * a tool_use block. Pi's serializer can produce this ordering, so we split
105
+ * the message into two consecutive assistant turns: one with text blocks and
106
+ * one with tool_use blocks. The reordering is safe because the text and
107
+ * tool_use blocks are semantically independent within a single turn.
108
+ */
110
109
  function splitAssistantToolUseTrailingContent(messages) {
111
110
  return messages.flatMap((message) => {
112
111
  if (message.role !== "assistant" || !Array.isArray(message.content)) {
@@ -137,10 +136,13 @@ export function shapeAnthropicOAuthPayload(payload) {
137
136
  return payload;
138
137
  }
139
138
  const normalizedMessages = splitAssistantToolUseTrailingContent(messages);
139
+ const shapedSystem = Array.isArray(payload.system)
140
+ ? shapeSystemBlocks(payload.system)
141
+ : payload.system;
140
142
  const shapedPayload = {
141
143
  ...payload,
142
144
  messages: normalizedMessages,
143
- system: prependBillingHeader(payload.system, normalizedMessages),
145
+ system: prependBillingHeader(shapedSystem, normalizedMessages),
144
146
  };
145
147
  if (typeof payload["anthropic-beta"] === "string") {
146
148
  shapedPayload["anthropic-beta"] = mergeAnthropicBetas(payload["anthropic-beta"]);
@@ -1 +1,22 @@
1
+ /**
2
+ * Shape a system prompt string for Anthropic OAuth compatibility.
3
+ *
4
+ * Replaces Pi's verbose default preamble with a minimal neutral prompt while
5
+ * preserving any project context that follows. Returns the original string
6
+ * unchanged when Pi's default preamble is not detected.
7
+ */
1
8
  export declare function shapeAnthropicOAuthSystemPrompt(systemPrompt: string): string;
9
+ type TextBlock = {
10
+ type: "text";
11
+ text: string;
12
+ [key: string]: unknown;
13
+ };
14
+ /**
15
+ * Apply system prompt shaping to an array of Anthropic system text blocks.
16
+ *
17
+ * Finds the first block containing Pi's default prompt preamble and replaces
18
+ * its text in-place (returning a new array). Blocks without the preamble are
19
+ * passed through unchanged.
20
+ */
21
+ export declare function shapeSystemBlocks(blocks: TextBlock[]): TextBlock[];
22
+ export {};
@@ -1,14 +1,15 @@
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");
1
+ import { MINIMAL_ANTHROPIC_OAUTH_PROMPT, PI_DEFAULT_PROMPT_PREFIX, } from "./constants.js";
8
2
  function findProjectContextStart(systemPrompt) {
9
3
  const marker = "\n\n# Project Context\n\n";
10
4
  return systemPrompt.indexOf(marker);
11
5
  }
6
+ /**
7
+ * Shape a system prompt string for Anthropic OAuth compatibility.
8
+ *
9
+ * Replaces Pi's verbose default preamble with a minimal neutral prompt while
10
+ * preserving any project context that follows. Returns the original string
11
+ * unchanged when Pi's default preamble is not detected.
12
+ */
12
13
  export function shapeAnthropicOAuthSystemPrompt(systemPrompt) {
13
14
  if (!systemPrompt.includes(PI_DEFAULT_PROMPT_PREFIX)) {
14
15
  return systemPrompt;
@@ -19,3 +20,19 @@ export function shapeAnthropicOAuthSystemPrompt(systemPrompt) {
19
20
  }
20
21
  return `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}${systemPrompt.slice(projectContextStart)}`;
21
22
  }
23
+ /**
24
+ * Apply system prompt shaping to an array of Anthropic system text blocks.
25
+ *
26
+ * Finds the first block containing Pi's default prompt preamble and replaces
27
+ * its text in-place (returning a new array). Blocks without the preamble are
28
+ * passed through unchanged.
29
+ */
30
+ export function shapeSystemBlocks(blocks) {
31
+ return blocks.map((block) => {
32
+ if (block.type !== "text" ||
33
+ !block.text.includes(PI_DEFAULT_PROMPT_PREFIX)) {
34
+ return block;
35
+ }
36
+ return { ...block, text: shapeAnthropicOAuthSystemPrompt(block.text) };
37
+ });
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-anthropic-auth",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Pi extension package for Anthropic OAuth compatibility",
5
5
  "author": {
6
6
  "name": "Chris Lasher"
@@ -32,28 +32,31 @@
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsc -p tsconfig.build.json",
35
- "check": "tsc -p tsconfig.json --noEmit",
35
+ "check": "tsc -p tsconfig.check.json --noEmit",
36
36
  "lint": "biome check .",
37
37
  "lint:fix": "biome check --write .",
38
38
  "lint:md": "markdownlint-cli2 '*.md'",
39
39
  "lint:all": "pnpm run lint && pnpm run lint:md",
40
40
  "format": "biome format --write .",
41
- "test": "tsx --test test/**/*.test.ts"
41
+ "test": "tsx --test test/**/*.test.ts",
42
+ "prepublishOnly": "pnpm run check && pnpm run test && pnpm run build"
42
43
  },
43
44
  "pi": {
44
45
  "extensions": [
45
46
  "./dist/index.js"
46
47
  ]
47
48
  },
48
- "dependencies": {
49
- "@anthropic-ai/sdk": "^0.52.0",
50
- "@mariozechner/pi-ai": "^0.68.0",
51
- "@mariozechner/pi-coding-agent": "^0.68.0"
49
+ "peerDependencies": {
50
+ "@mariozechner/pi-ai": "*",
51
+ "@mariozechner/pi-coding-agent": "*"
52
52
  },
53
53
  "devDependencies": {
54
+ "@anthropic-ai/sdk": "^0.52.0",
54
55
  "@biomejs/biome": "^2.4.12",
56
+ "@mariozechner/pi-ai": "^0.68.0",
57
+ "@mariozechner/pi-coding-agent": "^0.68.0",
55
58
  "markdownlint-cli2": "^0.22.0",
56
59
  "tsx": "^4.20.6",
57
- "typescript": "^5.9.3"
60
+ "typescript": "^6.0.3"
58
61
  }
59
62
  }