@zgltyq/pi-provider-claude 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +92 -0
  3. package/index.ts +283 -0
  4. package/package.json +49 -0
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leechael
4
+
5
+ Based on @benvargas/pi-claude-code-use (MIT License, Copyright (c) Ben Vargas).
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @zgltyq/pi-provider-claude
2
+
3
+ Claude (Anthropic **OAuth / subscription**) compatibility layer for
4
+ [pi-coding-agent](https://pi.dev). Keeps **all** your extension tools usable
5
+ under the Claude subscription.
6
+
7
+ It is a self-contained fork of the tool-handling half of
8
+ [`@benvargas/pi-claude-code-use`](https://www.npmjs.com/package/@benvargas/pi-claude-code-use)
9
+ (MIT) — credit to Ben Vargas for the original approach.
10
+
11
+ ## Problem
12
+
13
+ Anthropic's OAuth (subscription) request path appears to fingerprint tool names.
14
+ Tools that are **not** part of the Claude Code core set and are **not** prefixed
15
+ `mcp__` can be classified as *extra usage*. The upstream extension defends against
16
+ this by **dropping every unknown flat-named tool** before the request is sent —
17
+ which silently hides legitimate extension tools from Claude:
18
+
19
+ `ask_user_question`, `todo`, `subagent`, `ast_grep_search`, `lsp_diagnostics`,
20
+ `ctx_*`, `web_search`, `ralph_*`, `preview_export`, … all disappear, even though
21
+ they work fine for non-Anthropic models.
22
+
23
+ ## What this does
24
+
25
+ Instead of dropping unknown flat tools, it **renames them in place on the wire**
26
+ to `mcp__pi__<name>`:
27
+
28
+ - ✅ `mcp__*` shape passes Anthropic's classifier → **no extra-usage charge**
29
+ - ✅ the tool stays **visible to Claude** → it can actually call it
30
+ - ✅ on the way back, calls to `mcp__pi__*` are rewritten to the original flat
31
+ name **before Pi executes them**, so the real tool (and its closure-bound
32
+ state) runs unchanged
33
+
34
+ Untouched: native Anthropic tools (objects with a `type` field, e.g.
35
+ `web_search`), anything already `mcp__`-prefixed, and the Claude Code core tools.
36
+ The extension only activates for **Anthropic + OAuth** — API-key auth and
37
+ non-Anthropic providers pass through completely unchanged.
38
+
39
+ It also applies the upstream system-prompt rewrite (`pi itself` → `the cli
40
+ itself`, etc.) on the OAuth path.
41
+
42
+ ## Why it's lightweight
43
+
44
+ The tool's full JSON schema already rides inside the outbound payload, so the fix
45
+ is a pure rename — **no jiti capture, no tool re-registration, no typebox**. The
46
+ only import is a TypeScript type (erased at runtime), so the extension has **zero
47
+ runtime dependencies**.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pi install npm:@zgltyq/pi-provider-claude
53
+ ```
54
+
55
+ Then remove the upstream one so they don't both transform the payload:
56
+
57
+ ```bash
58
+ pi remove npm:@benvargas/pi-claude-code-use
59
+ ```
60
+
61
+ Restart Pi (or `/reload`) and continue using the normal `anthropic` provider with
62
+ your OAuth login.
63
+
64
+ ## Verify
65
+
66
+ ```bash
67
+ PI_CLAUDE_PROVIDER_DEBUG_LOG=/tmp/claude-provider.log pi
68
+ # send one message, then quit
69
+ ```
70
+
71
+ In `/tmp/claude-provider.log`, the `"after"` payload's `tools[]` should show your
72
+ extension tools as `mcp__pi__<name>` and **nothing dropped**.
73
+
74
+ ## Environment variables
75
+
76
+ | Variable | Effect |
77
+ |---|---|
78
+ | `PI_CLAUDE_PROVIDER_DEBUG_LOG=/path` | Append `before`/`after` payloads for debugging. |
79
+ | `PI_CLAUDE_PROVIDER_DISABLE=1` | Pass all tools through with flat names (no renaming). Debug escape hatch. |
80
+
81
+ ## How it differs from `@benvargas/pi-claude-code-use`
82
+
83
+ | | upstream | this fork |
84
+ |---|---|---|
85
+ | Unknown flat tools | **dropped** | **renamed** `mcp__pi__<name>` (kept) |
86
+ | Alias scheme | per-tool config + jiti capture + re-registration | generic in-place rename (deterministic prefix) |
87
+ | Runtime deps | `@mariozechner/jiti` | none |
88
+ | Scope | tool filtering + companion aliases + system-prompt rewrite | tool renaming + system-prompt rewrite |
89
+
90
+ ## License
91
+
92
+ MIT. Based on `@benvargas/pi-claude-code-use` (MIT).
package/index.ts ADDED
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @zgltyq/pi-provider-claude
3
+ * ────────────────────────────────────────────────────────────────────────────
4
+ * Claude (Anthropic OAuth / subscription) compatibility layer for pi-coding-agent.
5
+ * Self-contained fork of the tool-handling half of `@benvargas/pi-claude-code-use`
6
+ * (MIT). Credit to Ben Vargas for the original approach.
7
+ *
8
+ * WHY THIS EXISTS
9
+ * Anthropic's OAuth (subscription) request path appears to fingerprint tool
10
+ * names: tools that are neither part of the Claude Code core set nor prefixed
11
+ * `mcp__` can be classified as "extra usage". The upstream extension defends
12
+ * against this by DROPPING every unknown flat-named tool — which silently
13
+ * hides legitimate extension tools (ask_user_question, todo, subagent,
14
+ * ast_grep_search, ctx_*, web_search, …) from Claude.
15
+ *
16
+ * WHAT THIS DOES DIFFERENTLY (the "map all my tools" fix)
17
+ * Instead of dropping unknown flat tools, it RENAMES them in place on the
18
+ * wire to `mcp__pi__<name>` so they pass the classifier (subscription-safe)
19
+ * AND remain visible to Claude. On the way back, tool calls to `mcp__pi__*`
20
+ * are rewritten to their original flat name before Pi executes them, so the
21
+ * real tool (and its closure-bound state) runs unchanged.
22
+ *
23
+ * DESIGN NOTES
24
+ * - The tool's full JSON schema already rides inside the outbound payload,
25
+ * so we just rename — NO jiti capture, NO tool re-registration, NO typebox.
26
+ * The only import is a TYPE (erased at runtime), so this extension has zero
27
+ * runtime dependencies and minimal chance of a load failure.
28
+ * - Native Anthropic tools (objects with a `type` field, e.g. web_search) and
29
+ * anything already `mcp__`-prefixed are passed through untouched. Core
30
+ * Claude Code tools are passed through untouched.
31
+ * - Only ACTIVATES for Anthropic + OAuth. API-key and non-Anthropic providers
32
+ * are left completely unchanged.
33
+ *
34
+ * ENV (optional)
35
+ * PI_CLAUDE_PROVIDER_DEBUG_LOG=/path → append before/after payloads
36
+ * PI_CLAUDE_PROVIDER_DISABLE=1 → pass everything through flat (debug)
37
+ */
38
+
39
+ import { appendFileSync } from "node:fs";
40
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
41
+
42
+ // Prefix used to disguise flat tools as MCP tools. Kept short so
43
+ // `mcp__pi__<name>` stays well under Anthropic's 64-char tool-name limit.
44
+ const ALIAS_PREFIX = "mcp__pi__";
45
+
46
+ // Core Claude Code tool names that always pass through (lowercased).
47
+ // Mirrors Pi core's `claudeCodeTools` in packages/ai/src/providers/anthropic.ts.
48
+ const CORE_TOOL_NAMES = new Set([
49
+ "read",
50
+ "write",
51
+ "edit",
52
+ "bash",
53
+ "grep",
54
+ "glob",
55
+ "askuserquestion",
56
+ "enterplanmode",
57
+ "exitplanmode",
58
+ "killshell",
59
+ "notebookedit",
60
+ "skill",
61
+ "task",
62
+ "taskoutput",
63
+ "todowrite",
64
+ "webfetch",
65
+ "websearch",
66
+ ]);
67
+
68
+ const lower = (s: string): string => s.toLowerCase();
69
+ const isPlainObject = (v: unknown): v is Record<string, unknown> =>
70
+ typeof v === "object" && v !== null && !Array.isArray(v);
71
+
72
+ // ─── System prompt rewrite (parity with upstream) ────────────────────────────
73
+ // Keep Pi's identity out of the system prompt on the OAuth path.
74
+ function rewritePromptText(text: string): string {
75
+ return text
76
+ .replaceAll("pi itself", "the cli itself")
77
+ .replaceAll("pi .md files", "cli .md files")
78
+ .replaceAll("pi packages", "cli packages");
79
+ }
80
+
81
+ function rewriteSystemField(system: unknown): unknown {
82
+ if (typeof system === "string") return rewritePromptText(system);
83
+ if (!Array.isArray(system)) return system;
84
+ return system.map((block) => {
85
+ if (
86
+ !isPlainObject(block) ||
87
+ block.type !== "text" ||
88
+ typeof block.text !== "string"
89
+ )
90
+ return block;
91
+ const rewritten = rewritePromptText(block.text);
92
+ return rewritten === block.text ? block : { ...block, text: rewritten };
93
+ });
94
+ }
95
+
96
+ // True for tools that must NOT be renamed: native typed tools, core tools,
97
+ // already-mcp__ tools, or nameless entries.
98
+ function shouldRename(tool: Record<string, unknown>): boolean {
99
+ if (typeof tool.type === "string" && tool.type.trim().length > 0)
100
+ return false; // native (web_search, …)
101
+ const name = typeof tool.name === "string" ? tool.name : "";
102
+ if (!name) return false;
103
+ const lc = lower(name);
104
+ if (CORE_TOOL_NAMES.has(lc)) return false;
105
+ if (lc.startsWith("mcp__")) return false;
106
+ return true;
107
+ }
108
+
109
+ // Transform tools[]; returns the surviving tools plus the flat→alias map of
110
+ // exactly what was renamed (so tool_choice / message history stay consistent).
111
+ function transformTools(tools: unknown[]): {
112
+ tools: unknown[];
113
+ renamed: Map<string, string>;
114
+ } {
115
+ const renamed = new Map<string, string>(); // lower(flat) → alias
116
+ const emitted = new Set<string>();
117
+ const result: unknown[] = [];
118
+
119
+ for (const tool of tools) {
120
+ if (!isPlainObject(tool)) continue;
121
+
122
+ if (!shouldRename(tool)) {
123
+ const key =
124
+ typeof tool.name === "string"
125
+ ? lower(tool.name)
126
+ : `__native_${result.length}`;
127
+ if (emitted.has(key)) continue;
128
+ emitted.add(key);
129
+ result.push(tool);
130
+ continue;
131
+ }
132
+
133
+ const name = tool.name as string;
134
+ const alias = ALIAS_PREFIX + name;
135
+ renamed.set(lower(name), alias);
136
+ if (emitted.has(lower(alias))) continue;
137
+ emitted.add(lower(alias));
138
+ result.push({ ...tool, name: alias }); // preserves schema + cache_control
139
+ }
140
+
141
+ return { tools: result, renamed };
142
+ }
143
+
144
+ // Rewrite tool_use block names in message history to match the renamed tools.
145
+ function remapMessages(
146
+ messages: unknown[],
147
+ renamed: Map<string, string>,
148
+ ): unknown[] {
149
+ if (renamed.size === 0) return messages;
150
+ let changed = false;
151
+ const next = messages.map((msg) => {
152
+ if (!isPlainObject(msg) || !Array.isArray(msg.content)) return msg;
153
+ let blockChanged = false;
154
+ const content = msg.content.map((block) => {
155
+ if (
156
+ !isPlainObject(block) ||
157
+ block.type !== "tool_use" ||
158
+ typeof block.name !== "string"
159
+ )
160
+ return block;
161
+ const alias = renamed.get(lower(block.name));
162
+ if (!alias || alias === block.name) return block;
163
+ blockChanged = true;
164
+ return { ...block, name: alias };
165
+ });
166
+ if (!blockChanged) return msg;
167
+ changed = true;
168
+ return { ...msg, content };
169
+ });
170
+ return changed ? next : messages;
171
+ }
172
+
173
+ function remapToolChoice(
174
+ toolChoice: Record<string, unknown>,
175
+ renamed: Map<string, string>,
176
+ ): Record<string, unknown> {
177
+ if (toolChoice.type !== "tool" || typeof toolChoice.name !== "string")
178
+ return toolChoice;
179
+ const alias = renamed.get(lower(toolChoice.name));
180
+ return alias ? { ...toolChoice, name: alias } : toolChoice;
181
+ }
182
+
183
+ function transformPayload(
184
+ raw: Record<string, unknown>,
185
+ disable: boolean,
186
+ ): Record<string, unknown> {
187
+ const payload = JSON.parse(JSON.stringify(raw)) as Record<string, unknown>;
188
+
189
+ // 1. System prompt rewrite always applies.
190
+ if (payload.system !== undefined)
191
+ payload.system = rewriteSystemField(payload.system);
192
+
193
+ if (disable) return payload;
194
+
195
+ // 2. Rename unknown flat tools → mcp__pi__<name>.
196
+ if (Array.isArray(payload.tools)) {
197
+ const { tools, renamed } = transformTools(payload.tools as unknown[]);
198
+ payload.tools = tools;
199
+
200
+ // 3. Keep tool_choice consistent.
201
+ if (isPlainObject(payload.tool_choice)) {
202
+ payload.tool_choice = remapToolChoice(payload.tool_choice, renamed);
203
+ }
204
+
205
+ // 4. Keep historical tool_use names consistent with the renamed tools.
206
+ if (Array.isArray(payload.messages)) {
207
+ payload.messages = remapMessages(payload.messages, renamed);
208
+ }
209
+ }
210
+
211
+ return payload;
212
+ }
213
+
214
+ // ─── Reverse mapping on execution ────────────────────────────────────────────
215
+ // Rewrite mcp__pi__<name> tool calls back to <name> in the finalized assistant
216
+ // message, BEFORE Pi resolves which tool to run. Only touches our own prefix,
217
+ // so foreign mcp__ tools (other extensions) are untouched.
218
+ function unaliasToolCalls(message: unknown): unknown {
219
+ if (
220
+ !isPlainObject(message) ||
221
+ message.role !== "assistant" ||
222
+ !Array.isArray(message.content)
223
+ )
224
+ return undefined;
225
+ let changed = false;
226
+ const content = message.content.map((block) => {
227
+ if (
228
+ !isPlainObject(block) ||
229
+ block.type !== "toolCall" ||
230
+ typeof block.name !== "string"
231
+ )
232
+ return block;
233
+ if (!block.name.startsWith(ALIAS_PREFIX)) return block;
234
+ changed = true;
235
+ return { ...block, name: block.name.slice(ALIAS_PREFIX.length) };
236
+ });
237
+ return changed ? { ...message, content } : undefined;
238
+ }
239
+
240
+ // ─── Debug logging (optional) ─────────────────────────────────────────────────
241
+ const debugLogPath = process.env.PI_CLAUDE_PROVIDER_DEBUG_LOG;
242
+ function writeDebugLog(payload: unknown): void {
243
+ if (!debugLogPath) return;
244
+ try {
245
+ appendFileSync(
246
+ debugLogPath,
247
+ `${new Date().toISOString()}\n${JSON.stringify(payload, null, 2)}\n---\n`,
248
+ "utf-8",
249
+ );
250
+ } catch {
251
+ /* logging must never break a request */
252
+ }
253
+ }
254
+
255
+ export default function piProviderClaude(pi: ExtensionAPI): void {
256
+ // mcp__pi__* → flat, before the agent loop resolves the tool.
257
+ pi.on("message_end", async (event, _ctx) => {
258
+ const rewritten = unaliasToolCalls(event.message);
259
+ if (!rewritten) return undefined;
260
+ return { message: rewritten as typeof event.message };
261
+ });
262
+
263
+ pi.on("before_provider_request", (event, ctx) => {
264
+ const model = ctx.model;
265
+ if (
266
+ !model ||
267
+ model.provider !== "anthropic" ||
268
+ !ctx.modelRegistry.isUsingOAuth(model)
269
+ ) {
270
+ return undefined; // only Anthropic OAuth
271
+ }
272
+ if (!isPlainObject(event.payload)) return undefined;
273
+
274
+ writeDebugLog({ stage: "before", payload: event.payload });
275
+ const disable = process.env.PI_CLAUDE_PROVIDER_DISABLE === "1";
276
+ const transformed = transformPayload(
277
+ event.payload as Record<string, unknown>,
278
+ disable,
279
+ );
280
+ writeDebugLog({ stage: "after", payload: transformed });
281
+ return transformed;
282
+ });
283
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@zgltyq/pi-provider-claude",
3
+ "version": "1.0.0",
4
+ "description": "Claude (Anthropic OAuth/subscription) compatibility layer for pi-coding-agent — keeps ALL extension tools usable under the Claude subscription by mapping unknown flat tool names to mcp__pi__<name> on the wire instead of dropping them. Self-contained fork of the tool half of @benvargas/pi-claude-code-use.",
5
+ "author": "Leechael",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/ZGltYQ/pi-provider-claude"
10
+ },
11
+ "homepage": "https://github.com/ZGltYQ/pi-provider-claude#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/ZGltYQ/pi-provider-claude/issues"
14
+ },
15
+ "keywords": [
16
+ "pi-package",
17
+ "pi-extension",
18
+ "pi-provider",
19
+ "pi",
20
+ "pi-coding-agent",
21
+ "pi-mono",
22
+ "coding-agent",
23
+ "anthropic",
24
+ "claude",
25
+ "claude-code",
26
+ "oauth",
27
+ "subscription",
28
+ "tools",
29
+ "fork"
30
+ ],
31
+ "type": "module",
32
+ "files": [
33
+ "index.ts",
34
+ "package.json",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "pi": {
39
+ "extensions": [
40
+ "./index.ts"
41
+ ]
42
+ },
43
+ "peerDependencies": {
44
+ "@earendil-works/pi-coding-agent": ">=0.74.0"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }