pi-anthropic-oauth 0.1.14 → 0.1.16
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 +50 -10
- package/package.json +3 -2
- package/src/prompt.ts +74 -3
- package/src/stream.ts +44 -9
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# pi-anthropic-oauth
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/pi-anthropic-oauth)
|
|
3
|
+
[](https://www.npmjs.com/package/pi-anthropic-oauth)
|
|
4
4
|
|
|
5
5
|
Use Claude Pro/Max in Pi with browser OAuth.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- Claude Pro/Max login from `/login
|
|
9
|
+
- Claude Pro/Max login from `/login`
|
|
10
10
|
- Automatic token refresh
|
|
11
11
|
- Claude Code-compatible OAuth headers and prompt shaping
|
|
12
|
-
- No API key
|
|
12
|
+
- No Anthropic API key needed
|
|
13
13
|
- Uses Pi's Anthropic model registry
|
|
14
14
|
- Adds Claude Opus 4.8 by default
|
|
15
15
|
- Auto-creates `~/.Claude Code` → `~/.pi` symlink when missing
|
|
@@ -20,10 +20,10 @@ Use Claude Pro/Max in Pi with browser OAuth.
|
|
|
20
20
|
pi install npm:pi-anthropic-oauth
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Start Pi, then run:
|
|
23
|
+
Start Pi, then run Pi's login command:
|
|
24
24
|
|
|
25
25
|
```text
|
|
26
|
-
/login
|
|
26
|
+
/login
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Choose:
|
|
@@ -32,12 +32,47 @@ Choose:
|
|
|
32
32
|
Claude Pro/Max
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Complete the browser login. After login, select an Anthropic model in Pi.
|
|
36
|
+
|
|
37
|
+
Update with:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pi update npm:pi-anthropic-oauth
|
|
41
|
+
```
|
|
37
42
|
|
|
38
43
|
> [!NOTE]
|
|
39
44
|
> Anthropic auth changes are closely monitored for quick compatibility updates.
|
|
40
45
|
|
|
46
|
+
## System prompt rewriting
|
|
47
|
+
|
|
48
|
+
When using Claude Pro/Max OAuth, the extension prepends Claude Code identity text and rewrites standalone `Pi` / `pi` references in Pi's system prompt to `Claude Code`. The rewrite mode defaults to `aggressive`:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
PI_ANTHROPIC_OAUTH_REWRITE_MODE=aggressive
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Other modes are opt-in and avoid rewriting some technical strings such as paths, package names, or repository names. If OAuth starts failing, switch back to `aggressive`.
|
|
55
|
+
|
|
56
|
+
Available modes:
|
|
57
|
+
|
|
58
|
+
| Mode | Behavior |
|
|
59
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
60
|
+
| `aggressive` | Default. Replaces standalone `Pi` / `pi` wherever the word-boundary pattern matches. |
|
|
61
|
+
| `path-safe` | Avoids replacing `Pi` / `pi` immediately after `/` or `\`, preserving path segments like `/srv/dev/pi-foo`. |
|
|
62
|
+
| `technical-safe` | Avoids replacing `Pi` / `pi` next to common technical-token separators: `/`, `\`, `.`, `@`, `:`, `_`, `-`. |
|
|
63
|
+
| `custom` | Uses `PI_ANTHROPIC_OAUTH_REWRITE_PATTERN` as the replacement regex. |
|
|
64
|
+
|
|
65
|
+
Custom patterns can be a regex source or a JavaScript-style regex literal. The `g` flag is added automatically when omitted:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
PI_ANTHROPIC_OAUTH_REWRITE_MODE=custom
|
|
69
|
+
PI_ANTHROPIC_OAUTH_REWRITE_PATTERN='(?<![/\\])\b[Pp]i\b'
|
|
70
|
+
# or
|
|
71
|
+
PI_ANTHROPIC_OAUTH_REWRITE_PATTERN='/\bpi\b/gi'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
There is no separate off mode. If you need to disable rewriting entirely, use a custom pattern that never matches, such as `(?!)`. Disabling or weakening the rewrite may affect OAuth compatibility.
|
|
75
|
+
|
|
41
76
|
## Extra models
|
|
42
77
|
|
|
43
78
|
To add another Anthropic model, create `~/.pi/agent/models.json`:
|
|
@@ -47,7 +82,7 @@ To add another Anthropic model, create `~/.pi/agent/models.json`:
|
|
|
47
82
|
"providers": {
|
|
48
83
|
"anthropic": {
|
|
49
84
|
"baseUrl": "https://api.anthropic.com",
|
|
50
|
-
"apiKey": "
|
|
85
|
+
"apiKey": "unused",
|
|
51
86
|
"api": "anthropic-messages",
|
|
52
87
|
"models": [
|
|
53
88
|
{
|
|
@@ -61,12 +96,17 @@ To add another Anthropic model, create `~/.pi/agent/models.json`:
|
|
|
61
96
|
```
|
|
62
97
|
|
|
63
98
|
> [!NOTE]
|
|
64
|
-
>
|
|
99
|
+
> Custom models are not needed for Claude Opus 4.8; this extension registers `claude-opus-4-8` automatically.
|
|
100
|
+
>
|
|
101
|
+
> Pi requires `baseUrl`, `apiKey`, and `api` when defining custom models in `models.json`. With this extension, requests normally authenticate through Claude Pro/Max OAuth after `/login`, so `apiKey` is only a placeholder to satisfy Pi's config requirements and does not need to be a valid Anthropic API key.
|
|
102
|
+
>
|
|
103
|
+
> Do not use a fake `sk-ant-oat...` value as the placeholder. If OAuth login has not completed, Pi may try to use that fake token and Anthropic will return `401 Invalid bearer token`. Use a harmless placeholder such as `"unused"`, then run `/login` and choose `Claude Pro/Max`.
|
|
65
104
|
|
|
66
105
|
## Troubleshooting
|
|
67
106
|
|
|
68
|
-
-
|
|
107
|
+
- Run `/login` with no arguments, then choose `Claude Pro/Max`.
|
|
69
108
|
- If local callback login does not complete, paste the final callback URL or `code#state` when prompted
|
|
109
|
+
- If you see `401 Invalid bearer token`, remove any fake `sk-ant-oat...` placeholder from `~/.pi/agent/models.json` and log in again
|
|
70
110
|
- If something breaks, please open an issue with your Pi version, extension version, and error output
|
|
71
111
|
|
|
72
112
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anthropic-oauth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Claude Pro/Max OAuth extension for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
28
|
+
"test": "node -e \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\" && tsc -p tsconfig.json --outDir .test-dist && node --test test/*.test.mjs",
|
|
28
29
|
"pack:check": "npm pack --dry-run",
|
|
29
|
-
"prepublishOnly": "npm run check && npm run pack:check"
|
|
30
|
+
"prepublishOnly": "npm run check && npm test && npm run pack:check"
|
|
30
31
|
},
|
|
31
32
|
"pi": {
|
|
32
33
|
"extensions": [
|
package/src/prompt.ts
CHANGED
|
@@ -6,6 +6,25 @@ const PI_REMOVAL_ANCHORS = [
|
|
|
6
6
|
"badlogic/pi-mono",
|
|
7
7
|
] as const;
|
|
8
8
|
|
|
9
|
+
export const PI_REWRITE_MODE_ENV = "PI_ANTHROPIC_OAUTH_REWRITE_MODE";
|
|
10
|
+
export const PI_REWRITE_PATTERN_ENV = "PI_ANTHROPIC_OAUTH_REWRITE_PATTERN";
|
|
11
|
+
|
|
12
|
+
export type PiRewriteMode = "aggressive" | "path-safe" | "technical-safe" | "custom";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PI_REWRITE_MODE: PiRewriteMode = "aggressive";
|
|
15
|
+
const PI_REWRITE_PATTERN_SOURCES: Record<Exclude<PiRewriteMode, "custom">, string> = {
|
|
16
|
+
aggressive: String.raw`\b[Pp]i\b`,
|
|
17
|
+
"path-safe": String.raw`(?<![/\\])\b[Pp]i\b`,
|
|
18
|
+
"technical-safe": String.raw`(?<![/\\.@:_-])\b[Pp]i\b(?![/\\.@:_-])`,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PI_REWRITE_MODES = new Set<PiRewriteMode>([
|
|
22
|
+
"aggressive",
|
|
23
|
+
"path-safe",
|
|
24
|
+
"technical-safe",
|
|
25
|
+
"custom",
|
|
26
|
+
]);
|
|
27
|
+
|
|
9
28
|
type MessageContentBlock = {
|
|
10
29
|
type: string;
|
|
11
30
|
text?: string;
|
|
@@ -42,7 +61,10 @@ export function buildAnthropicSystemPrompt(
|
|
|
42
61
|
return blocks.length > 0 ? blocks : undefined;
|
|
43
62
|
}
|
|
44
63
|
|
|
45
|
-
function sanitizeSystemText(
|
|
64
|
+
export function sanitizeSystemText(
|
|
65
|
+
text: string,
|
|
66
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
67
|
+
): string {
|
|
46
68
|
const paragraphs = text.split(/\n\n+/);
|
|
47
69
|
const filtered = paragraphs.filter((paragraph) => {
|
|
48
70
|
const lower = paragraph.toLowerCase();
|
|
@@ -52,7 +74,56 @@ function sanitizeSystemText(text: string): string {
|
|
|
52
74
|
|
|
53
75
|
return filtered
|
|
54
76
|
.join("\n\n")
|
|
55
|
-
.replace(
|
|
56
|
-
.replace(/\bPi\b/g, "Claude Code")
|
|
77
|
+
.replace(resolvePiRewritePattern(env), "Claude Code")
|
|
57
78
|
.trim();
|
|
58
79
|
}
|
|
80
|
+
|
|
81
|
+
function resolvePiRewritePattern(env: NodeJS.ProcessEnv): RegExp {
|
|
82
|
+
const mode = parsePiRewriteMode(env[PI_REWRITE_MODE_ENV]);
|
|
83
|
+
if (mode === "custom") return compileCustomPiRewritePattern(env[PI_REWRITE_PATTERN_ENV]);
|
|
84
|
+
return new RegExp(PI_REWRITE_PATTERN_SOURCES[mode], "g");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parsePiRewriteMode(value: string | undefined): PiRewriteMode {
|
|
88
|
+
if (!value?.trim()) return DEFAULT_PI_REWRITE_MODE;
|
|
89
|
+
const mode = value.trim().toLowerCase();
|
|
90
|
+
if (PI_REWRITE_MODES.has(mode as PiRewriteMode)) return mode as PiRewriteMode;
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Invalid ${PI_REWRITE_MODE_ENV}: ${value}. Expected one of ${Array.from(PI_REWRITE_MODES).join(", ")}.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function compileCustomPiRewritePattern(value: string | undefined): RegExp {
|
|
97
|
+
const pattern = value?.trim();
|
|
98
|
+
if (!pattern) {
|
|
99
|
+
throw new Error(`${PI_REWRITE_PATTERN_ENV} must be set when ${PI_REWRITE_MODE_ENV}=custom.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parsed = parseRegexLiteral(pattern) ?? { source: pattern, flags: "" };
|
|
103
|
+
const flags = parsed.flags.includes("g") ? parsed.flags : `${parsed.flags}g`;
|
|
104
|
+
try {
|
|
105
|
+
return new RegExp(parsed.source, flags);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
throw new Error(`Invalid ${PI_REWRITE_PATTERN_ENV}: ${message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseRegexLiteral(value: string): { source: string; flags: string } | undefined {
|
|
113
|
+
if (!value.startsWith("/")) return undefined;
|
|
114
|
+
|
|
115
|
+
for (let index = value.length - 1; index > 0; index--) {
|
|
116
|
+
if (value[index] !== "/" || isEscaped(value, index)) continue;
|
|
117
|
+
return { source: value.slice(1, index), flags: value.slice(index + 1) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isEscaped(value: string, index: number): boolean {
|
|
124
|
+
let slashCount = 0;
|
|
125
|
+
for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor--) {
|
|
126
|
+
slashCount++;
|
|
127
|
+
}
|
|
128
|
+
return slashCount % 2 === 1;
|
|
129
|
+
}
|
package/src/stream.ts
CHANGED
|
@@ -24,7 +24,12 @@ import { buildAnthropicSystemPrompt } from "./prompt.js";
|
|
|
24
24
|
const REQUIRED_BETAS = [
|
|
25
25
|
"claude-code-20250219",
|
|
26
26
|
"oauth-2025-04-20",
|
|
27
|
-
|
|
27
|
+
// fine-grained-tool-streaming removed: it ships the model's raw, unvalidated
|
|
28
|
+
// tool-input JSON. For large edits full of quotes/newlines the streamed
|
|
29
|
+
// string escaping breaks, so a field (e.g. edit.oldText) swallows the rest of
|
|
30
|
+
// the structure — surfacing as either a hard JSON.parse crash or a wrong-shape
|
|
31
|
+
// schema-validation failure. Default streaming has the server validate/buffer
|
|
32
|
+
// tool JSON, guaranteeing well-formed, correctly-structured input.
|
|
28
33
|
"interleaved-thinking-2025-05-14",
|
|
29
34
|
] as const;
|
|
30
35
|
|
|
@@ -43,6 +48,14 @@ function mapStopReason(reason: string | null | undefined): StopReason {
|
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
function headersToRecord(headers: Headers): Record<string, string> {
|
|
52
|
+
const record: Record<string, string> = {};
|
|
53
|
+
headers.forEach((value, key) => {
|
|
54
|
+
record[key] = value;
|
|
55
|
+
});
|
|
56
|
+
return record;
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
function makeDefaultHeaders(
|
|
47
60
|
isOAuth: boolean,
|
|
48
61
|
options?: SimpleStreamOptions,
|
|
@@ -57,10 +70,7 @@ function makeDefaultHeaders(
|
|
|
57
70
|
headers["user-agent"] = USER_AGENT;
|
|
58
71
|
headers["x-app"] = "cli";
|
|
59
72
|
} else {
|
|
60
|
-
headers["anthropic-beta"] = [
|
|
61
|
-
"fine-grained-tool-streaming-2025-05-14",
|
|
62
|
-
"interleaved-thinking-2025-05-14",
|
|
63
|
-
].join(",");
|
|
73
|
+
headers["anthropic-beta"] = ["interleaved-thinking-2025-05-14"].join(",");
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
if (options?.headers) {
|
|
@@ -118,7 +128,9 @@ export function streamAnthropicOAuth(
|
|
|
118
128
|
try {
|
|
119
129
|
const apiKey = options?.apiKey;
|
|
120
130
|
if (!apiKey) {
|
|
121
|
-
throw new Error(
|
|
131
|
+
throw new Error(
|
|
132
|
+
"No Anthropic auth available. Run /login and choose Claude Pro/Max.",
|
|
133
|
+
);
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
const isOAuth = isClaudeOAuthAccessToken(apiKey);
|
|
@@ -170,9 +182,32 @@ export function streamAnthropicOAuth(
|
|
|
170
182
|
};
|
|
171
183
|
}
|
|
172
184
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
185
|
+
// Raw stream instead of the MessageStream helper: MessageStream
|
|
186
|
+
// accumulates tool_use input and JSON.parses it on content_block_stop,
|
|
187
|
+
// which throws under fine-grained-tool-streaming (input may be invalid
|
|
188
|
+
// mid-flight) and aborts the turn. The raw stream yields the same
|
|
189
|
+
// RawMessageStreamEvents; tool args are already parsed leniently below.
|
|
190
|
+
const { data: anthropicStream, response: httpResponse } =
|
|
191
|
+
await client.messages
|
|
192
|
+
.create(params, {
|
|
193
|
+
signal: options?.signal,
|
|
194
|
+
})
|
|
195
|
+
.withResponse();
|
|
196
|
+
|
|
197
|
+
if (options?.onResponse) {
|
|
198
|
+
try {
|
|
199
|
+
await options.onResponse(
|
|
200
|
+
{
|
|
201
|
+
status: httpResponse.status,
|
|
202
|
+
headers: headersToRecord(httpResponse.headers),
|
|
203
|
+
},
|
|
204
|
+
model,
|
|
205
|
+
);
|
|
206
|
+
} catch {
|
|
207
|
+
// Response hooks are best-effort and should not break streaming.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
176
211
|
stream.push({ type: "start", partial: output });
|
|
177
212
|
|
|
178
213
|
const blocks = output.content as IndexedBlock[];
|