@gotgenes/pi-anthropic-auth 0.4.0 → 0.4.2

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
@@ -4,7 +4,7 @@
4
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
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT)
6
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/)
7
+ [![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-F69220?style=flat&logo=pnpm&logoColor=white)](https://pnpm.io/)
8
8
  [![Pi Package](https://img.shields.io/badge/Pi-Package-6366F1?style=flat)](https://pi.mariozechner.at/)
9
9
 
10
10
  A [Pi](https://pi.mariozechner.at/) extension that improves compatibility with Anthropic Claude Pro/Max OAuth (i.e., your Claude subscription) while preserving Pi's normal Anthropic behavior.
@@ -57,6 +57,34 @@ pnpm run build # compile
57
57
  pi -e /absolute/path/to/pi-anthropic-auth/dist/index.js
58
58
  ```
59
59
 
60
+ ### Debug Logging
61
+
62
+ Set `PI_ANTHROPIC_AUTH_DEBUG` to enable structured debug logs from the OAuth shaping layer.
63
+
64
+ Modes:
65
+
66
+ - `PI_ANTHROPIC_AUTH_DEBUG=all` — log all Anthropic OAuth shaping events
67
+ - `PI_ANTHROPIC_AUTH_DEBUG=tool-use` — log only requests that include `tool_use`
68
+
69
+ Example:
70
+
71
+ ```bash
72
+ PI_ANTHROPIC_AUTH_DEBUG=tool-use \
73
+ pi \
74
+ --model anthropic/claude-haiku-4-5 \
75
+ --no-session \
76
+ --tools read,grep,find,ls \
77
+ -e /absolute/path/to/pi-anthropic-auth/src/index.ts \
78
+ -p "How many lines are in @AGENTS.md ?"
79
+ ```
80
+
81
+ ## Similar Projects
82
+
83
+ - [opencode-anthropic-auth](https://github.com/ex-machina-co/opencode-anthropic-auth/) — Anthropic OAuth compatibility work for [OpenCode](https://opencode.ai/).
84
+ - [pi-anthropic-oauth](https://github.com/leohenon/pi-anthropic-oauth) — a Pi extension that takes a fuller provider-override approach.
85
+
86
+ For notes on how this project compares to similar work, see [docs/comparison-to-similar-projects.md](docs/comparison-to-similar-projects.md).
87
+
60
88
  ## Acknowledgments
61
89
 
62
90
  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/).
@@ -5,6 +5,14 @@
5
5
  * preamble so it can be replaced with the minimal neutral prompt.
6
6
  */
7
7
  export declare const PI_DEFAULT_PROMPT_PREFIX = "You are an expert coding assistant operating inside pi, a coding agent harness.";
8
+ /**
9
+ * Final line of Pi's built-in default system prompt preamble.
10
+ *
11
+ * Used to replace the entire Pi-generated preamble body with the minimal
12
+ * neutral Anthropic OAuth prompt while preserving anything appended after the
13
+ * preamble (project context, skills, and date/cwd footer).
14
+ */
15
+ export declare const PI_DEFAULT_PROMPT_TERMINATOR = "- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)";
8
16
  /**
9
17
  * Prefix of the minimal neutral Anthropic OAuth system prompt.
10
18
  *
@@ -36,7 +44,7 @@ export declare const CLAUDE_CODE_IDENTITY_PREFIX = "You are Claude Code, Anthrop
36
44
  * too far from what Anthropic expects, OAuth requests may be rejected or
37
45
  * counted incorrectly.
38
46
  */
39
- export declare const CLAUDE_CODE_VERSION = "2.1.108";
47
+ export declare const CLAUDE_CODE_VERSION = "2.1.112";
40
48
  /** Salt used in the billing header suffix hash. */
41
49
  export declare const BILLING_HEADER_SALT = "59cf53e54c78";
42
50
  /** Character positions sampled from the first user message for the billing hash. */
package/dist/constants.js CHANGED
@@ -5,6 +5,14 @@
5
5
  * preamble so it can be replaced with the minimal neutral prompt.
6
6
  */
7
7
  export const PI_DEFAULT_PROMPT_PREFIX = "You are an expert coding assistant operating inside pi, a coding agent harness.";
8
+ /**
9
+ * Final line of Pi's built-in default system prompt preamble.
10
+ *
11
+ * Used to replace the entire Pi-generated preamble body with the minimal
12
+ * neutral Anthropic OAuth prompt while preserving anything appended after the
13
+ * preamble (project context, skills, and date/cwd footer).
14
+ */
15
+ export const PI_DEFAULT_PROMPT_TERMINATOR = "- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)";
8
16
  /**
9
17
  * Prefix of the minimal neutral Anthropic OAuth system prompt.
10
18
  *
@@ -53,7 +61,7 @@ export const CLAUDE_CODE_IDENTITY_PREFIX = "You are Claude Code, Anthropic's off
53
61
  * too far from what Anthropic expects, OAuth requests may be rejected or
54
62
  * counted incorrectly.
55
63
  */
56
- export const CLAUDE_CODE_VERSION = "2.1.108";
64
+ export const CLAUDE_CODE_VERSION = "2.1.112";
57
65
  /** Salt used in the billing header suffix hash. */
58
66
  export const BILLING_HEADER_SALT = "59cf53e54c78";
59
67
  /** Character positions sampled from the first user message for the billing hash. */
@@ -0,0 +1,3 @@
1
+ export declare function isDebugEnabled(): boolean;
2
+ export declare function isToolUseOnlyDebugEnabled(): boolean;
3
+ export declare function debugLog(scope: string, data: unknown): void;
package/dist/debug.js ADDED
@@ -0,0 +1,32 @@
1
+ function getDebugMode(value) {
2
+ if (!value)
3
+ return "off";
4
+ switch (value.trim().toLowerCase()) {
5
+ case "1":
6
+ case "true":
7
+ case "yes":
8
+ case "on":
9
+ case "debug":
10
+ case "all":
11
+ return "all";
12
+ case "tool":
13
+ case "tools":
14
+ case "tool-use":
15
+ case "tool_use":
16
+ return "tool-use";
17
+ default:
18
+ return "off";
19
+ }
20
+ }
21
+ export function isDebugEnabled() {
22
+ return getDebugMode(process.env.PI_ANTHROPIC_AUTH_DEBUG) !== "off";
23
+ }
24
+ export function isToolUseOnlyDebugEnabled() {
25
+ return getDebugMode(process.env.PI_ANTHROPIC_AUTH_DEBUG) === "tool-use";
26
+ }
27
+ export function debugLog(scope, data) {
28
+ if (!isDebugEnabled()) {
29
+ return;
30
+ }
31
+ console.error(`[pi-anthropic-auth][debug] ${scope} ${JSON.stringify(data)}`);
32
+ }
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
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 { debugLog, isToolUseOnlyDebugEnabled } from "./debug.js";
3
4
  import { shapeSystemBlocks } from "./system-prompt-shaping.js";
4
5
  function isRecord(value) {
5
6
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -119,6 +120,36 @@ function splitAssistantToolUseTrailingContent(messages) {
119
120
  ];
120
121
  });
121
122
  }
123
+ function getToolDefinitionNames(payload) {
124
+ const tools = payload.tools;
125
+ if (!Array.isArray(tools)) {
126
+ return [];
127
+ }
128
+ return tools
129
+ .map((tool) => isRecord(tool) && typeof tool.name === "string" ? tool.name : undefined)
130
+ .filter((name) => typeof name === "string");
131
+ }
132
+ function getToolUseNames(messages) {
133
+ return messages.flatMap((message) => {
134
+ if (!Array.isArray(message.content)) {
135
+ return [];
136
+ }
137
+ return message.content
138
+ .map((block) => block?.type === "tool_use" && typeof block.name === "string"
139
+ ? block.name
140
+ : undefined)
141
+ .filter((name) => typeof name === "string");
142
+ });
143
+ }
144
+ function countAssistantMessages(messages) {
145
+ return messages.filter((message) => message.role === "assistant").length;
146
+ }
147
+ function shouldLogRequestDebug(messages) {
148
+ if (!isToolUseOnlyDebugEnabled()) {
149
+ return true;
150
+ }
151
+ return getToolUseNames(messages).length > 0;
152
+ }
122
153
  export function shapeAnthropicOAuthPayload(payload) {
123
154
  if (!isAnthropicMessagesPayload(payload)) {
124
155
  return payload;
@@ -131,9 +162,28 @@ export function shapeAnthropicOAuthPayload(payload) {
131
162
  const shapedSystem = Array.isArray(payload.system)
132
163
  ? shapeSystemBlocks(payload.system)
133
164
  : payload.system;
165
+ const finalSystem = prependBillingHeader(shapedSystem, normalizedMessages);
166
+ const toolUseNamesBefore = getToolUseNames(messages);
167
+ const toolUseNamesAfter = getToolUseNames(normalizedMessages);
168
+ if (shouldLogRequestDebug(messages)) {
169
+ debugLog("before-provider-request", {
170
+ model: payload.model,
171
+ systemBlockCountBefore: Array.isArray(payload.system)
172
+ ? payload.system.length
173
+ : 0,
174
+ systemBlockCountAfter: Array.isArray(finalSystem)
175
+ ? finalSystem.length
176
+ : 0,
177
+ assistantMessagesBefore: countAssistantMessages(messages),
178
+ assistantMessagesAfter: countAssistantMessages(normalizedMessages),
179
+ toolDefinitions: getToolDefinitionNames(payload),
180
+ toolUseNamesBefore,
181
+ toolUseNamesAfter,
182
+ });
183
+ }
134
184
  return {
135
185
  ...payload,
136
186
  messages: normalizedMessages,
137
- system: prependBillingHeader(shapedSystem, normalizedMessages),
187
+ system: finalSystem,
138
188
  };
139
189
  }
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Reset the one-time terminator-missing warning latch. Exposed for tests.
3
+ */
4
+ export declare function _resetShapingWarnings(): void;
5
+ export type SanitizedSystemTextReport = {
6
+ text: string;
7
+ removedParagraphs: Array<{
8
+ anchor: string;
9
+ preview: string;
10
+ }>;
11
+ replacementMatches: string[];
12
+ };
1
13
  /**
2
14
  * Sanitize system prompt text by removing paragraphs containing known
3
15
  * Pi-specific anchor strings and applying inline text replacements for
@@ -9,19 +21,20 @@
9
21
  * string still appears somewhere in the paragraph, removal works regardless
10
22
  * of how the surrounding text changes.
11
23
  */
24
+ export declare function sanitizeSystemTextWithReport(text: string): SanitizedSystemTextReport;
12
25
  export declare function sanitizeSystemText(text: string): string;
13
26
  /**
14
27
  * Shape a system prompt string for Anthropic OAuth compatibility.
15
28
  *
16
- * Uses an anchor-driven sanitizer to remove Pi-specific paragraphs
17
- * (identity, documentation references, filler) while preserving
18
- * extension-contributed content (tool snippets, guidelines, appended
19
- * content, project context, skills, and date/cwd footer).
20
- *
21
- * Prepends a minimal neutral prompt to replace the removed Pi identity.
29
+ * For the normal upstream Pi prompt shape, sanitize only the known preamble
30
+ * span and replace its identity paragraph with the minimal neutral prompt.
31
+ * This preserves downstream configuration/extension points embedded in the
32
+ * preamble (tool snippets and guideline bullets) while still stripping the
33
+ * Pi-specific identity, filler, and documentation paragraphs.
22
34
  *
23
- * If the Pi default prompt prefix is not present, the prompt is returned
24
- * unchanged this gates shaping so non-Pi prompts pass through.
35
+ * If Pi's known preamble terminator drifts upstream, we fall back to slicing
36
+ * from `# Project Context`. If that section is also absent, we return the
37
+ * minimal prompt only.
25
38
  */
26
39
  export declare function shapeAnthropicOAuthSystemPrompt(systemPrompt: string): string;
27
40
  type TextBlock = {
@@ -1,4 +1,30 @@
1
- import { MINIMAL_ANTHROPIC_OAUTH_PROMPT, PARAGRAPH_REMOVAL_ANCHORS, PI_DEFAULT_PROMPT_PREFIX, TEXT_REPLACEMENTS, } from "./constants.js";
1
+ import { MINIMAL_ANTHROPIC_OAUTH_PROMPT, PARAGRAPH_REMOVAL_ANCHORS, PI_DEFAULT_PROMPT_PREFIX, PI_DEFAULT_PROMPT_TERMINATOR, TEXT_REPLACEMENTS, } from "./constants.js";
2
+ import { debugLog, isToolUseOnlyDebugEnabled } from "./debug.js";
3
+ let warnedTerminatorMissing = false;
4
+ function warnTerminatorMissingOnce() {
5
+ if (warnedTerminatorMissing) {
6
+ return;
7
+ }
8
+ warnedTerminatorMissing = true;
9
+ console.warn("[pi-anthropic-auth] Pi default preamble terminator not found; falling back to '# Project Context' anchor. " +
10
+ "Upstream Pi may have reworded its preamble — update PI_DEFAULT_PROMPT_TERMINATOR.");
11
+ }
12
+ /**
13
+ * Reset the one-time terminator-missing warning latch. Exposed for tests.
14
+ */
15
+ export function _resetShapingWarnings() {
16
+ warnedTerminatorMissing = false;
17
+ }
18
+ function previewParagraph(paragraph) {
19
+ return paragraph.replace(/\s+/g, " ").trim().slice(0, 140);
20
+ }
21
+ function shouldLogPromptDebug(report) {
22
+ if (!isToolUseOnlyDebugEnabled()) {
23
+ return true;
24
+ }
25
+ return (report.removedParagraphs.length === 0 &&
26
+ report.replacementMatches.length > 0);
27
+ }
2
28
  /**
3
29
  * Sanitize system prompt text by removing paragraphs containing known
4
30
  * Pi-specific anchor strings and applying inline text replacements for
@@ -10,43 +36,97 @@ import { MINIMAL_ANTHROPIC_OAUTH_PROMPT, PARAGRAPH_REMOVAL_ANCHORS, PI_DEFAULT_P
10
36
  * string still appears somewhere in the paragraph, removal works regardless
11
37
  * of how the surrounding text changes.
12
38
  */
13
- export function sanitizeSystemText(text) {
39
+ export function sanitizeSystemTextWithReport(text) {
14
40
  const paragraphs = text.split(/\n\n+/);
41
+ const removedParagraphs = [];
15
42
  const filtered = paragraphs.filter((paragraph) => {
16
43
  for (const anchor of PARAGRAPH_REMOVAL_ANCHORS) {
17
- if (paragraph.includes(anchor))
18
- return false;
44
+ if (!paragraph.includes(anchor)) {
45
+ continue;
46
+ }
47
+ removedParagraphs.push({
48
+ anchor,
49
+ preview: previewParagraph(paragraph),
50
+ });
51
+ return false;
19
52
  }
20
53
  return true;
21
54
  });
22
55
  let result = filtered.join("\n\n");
56
+ const replacementMatches = [];
23
57
  for (const rule of TEXT_REPLACEMENTS) {
58
+ if (result.includes(rule.match)) {
59
+ replacementMatches.push(rule.match);
60
+ }
24
61
  result = result.replaceAll(rule.match, rule.replacement);
25
62
  }
26
- return result.trim();
63
+ return {
64
+ text: result.trim(),
65
+ removedParagraphs,
66
+ replacementMatches,
67
+ };
68
+ }
69
+ export function sanitizeSystemText(text) {
70
+ return sanitizeSystemTextWithReport(text).text;
71
+ }
72
+ function findProjectContextStart(systemPrompt) {
73
+ const marker = "\n\n# Project Context\n\n";
74
+ return systemPrompt.indexOf(marker);
27
75
  }
28
76
  /**
29
77
  * Shape a system prompt string for Anthropic OAuth compatibility.
30
78
  *
31
- * Uses an anchor-driven sanitizer to remove Pi-specific paragraphs
32
- * (identity, documentation references, filler) while preserving
33
- * extension-contributed content (tool snippets, guidelines, appended
34
- * content, project context, skills, and date/cwd footer).
79
+ * For the normal upstream Pi prompt shape, sanitize only the known preamble
80
+ * span and replace its identity paragraph with the minimal neutral prompt.
81
+ * This preserves downstream configuration/extension points embedded in the
82
+ * preamble (tool snippets and guideline bullets) while still stripping the
83
+ * Pi-specific identity, filler, and documentation paragraphs.
35
84
  *
36
- * Prepends a minimal neutral prompt to replace the removed Pi identity.
37
- *
38
- * If the Pi default prompt prefix is not present, the prompt is returned
39
- * unchanged — this gates shaping so non-Pi prompts pass through.
85
+ * If Pi's known preamble terminator drifts upstream, we fall back to slicing
86
+ * from `# Project Context`. If that section is also absent, we return the
87
+ * minimal prompt only.
40
88
  */
41
89
  export function shapeAnthropicOAuthSystemPrompt(systemPrompt) {
42
- if (!systemPrompt.includes(PI_DEFAULT_PROMPT_PREFIX)) {
90
+ const prefixIdx = systemPrompt.indexOf(PI_DEFAULT_PROMPT_PREFIX);
91
+ if (prefixIdx === -1) {
43
92
  return systemPrompt;
44
93
  }
45
- const sanitized = sanitizeSystemText(systemPrompt);
46
- if (!sanitized) {
94
+ const terminatorIdx = systemPrompt.indexOf(PI_DEFAULT_PROMPT_TERMINATOR, prefixIdx);
95
+ if (terminatorIdx !== -1) {
96
+ const terminatorEnd = terminatorIdx + PI_DEFAULT_PROMPT_TERMINATOR.length;
97
+ const preamble = systemPrompt.slice(prefixIdx, terminatorEnd);
98
+ const report = sanitizeSystemTextWithReport(preamble);
99
+ const shapedPreamble = report.text
100
+ ? `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}\n\n${report.text}`
101
+ : MINIMAL_ANTHROPIC_OAUTH_PROMPT;
102
+ if (shouldLogPromptDebug(report)) {
103
+ debugLog("system-prompt-shaping", {
104
+ mode: "terminator",
105
+ originalLength: systemPrompt.length,
106
+ preambleLength: preamble.length,
107
+ sanitizedPreambleLength: report.text.length,
108
+ removedParagraphCount: report.removedParagraphs.length,
109
+ removedAnchors: report.removedParagraphs.map((entry) => entry.anchor),
110
+ removedParagraphPreviews: report.removedParagraphs.map((entry) => entry.preview),
111
+ replacementMatches: report.replacementMatches,
112
+ });
113
+ }
114
+ return (systemPrompt.slice(0, prefixIdx) +
115
+ shapedPreamble +
116
+ systemPrompt.slice(terminatorEnd));
117
+ }
118
+ warnTerminatorMissingOnce();
119
+ if (!isToolUseOnlyDebugEnabled()) {
120
+ debugLog("system-prompt-shaping", {
121
+ mode: "project-context-fallback",
122
+ originalLength: systemPrompt.length,
123
+ });
124
+ }
125
+ const projectContextStart = findProjectContextStart(systemPrompt);
126
+ if (projectContextStart === -1) {
47
127
  return MINIMAL_ANTHROPIC_OAUTH_PROMPT;
48
128
  }
49
- return `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}\n\n${sanitized}`;
129
+ return `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}${systemPrompt.slice(projectContextStart)}`;
50
130
  }
51
131
  /**
52
132
  * Apply system prompt shaping to an array of Anthropic system text blocks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-anthropic-auth",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Pi extension package for Anthropic OAuth compatibility",
5
5
  "author": {
6
6
  "name": "Chris Lasher"