@saccolabs/pi-claude-cli 0.4.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 Rebecca Chernoff
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,54 @@
1
+ # pi-claude-cli
2
+
3
+ > **This is a maintenance fork.** The upstream project's last commit was in March 2026, and it had stopped working against current [pi](https://github.com/earendil-works/pi). This fork updates it for compatibility with the current pi version, and folds in three open upstream pull requests that hadn't been merged:
4
+ >
5
+ > - **[#25](https://github.com/rchern/pi-claude-cli/pull/25)** — don't resume a Claude CLI session that was never created (fixes empty replies when switching to this provider mid-conversation), and surface CLI errors instead of silently returning nothing.
6
+ > - **[#26](https://github.com/rchern/pi-claude-cli/pull/26)** — fix a false "not authenticated" warning on Claude Code 2.x, and correct the outdated login instructions.
7
+ > - **[#29](https://github.com/rchern/pi-claude-cli/pull/29)** — let all models use the full thinking-effort range (up to `max`), not just Opus.
8
+ >
9
+ > Together these resolve the widely-reported problem where prompting a `pi-claude-cli` model just returned an empty response ([#3](https://github.com/rchern/pi-claude-cli/issues/3)). Credit for the three fixes goes to their original PR authors.
10
+
11
+ A [pi](https://github.com/earendil-works/pi) extension that routes LLM calls through the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) as a subprocess. Use your Claude Pro/Max subscription as the LLM backend — no API key, no separate billing.
12
+
13
+ ## How it works
14
+
15
+ The extension registers as a custom pi provider exposing all Claude models. Each request spawns a `claude -p` subprocess using the stream-json wire protocol, with `--resume` on follow-up turns to reuse the CLI's session state instead of replaying full history. Claude proposes tool calls, pi executes them natively. Custom pi tools are exposed to Claude via a schema-only MCP server.
16
+
17
+ ## Requirements
18
+
19
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated (`claude` on PATH)
20
+ - A Claude Pro or Max subscription
21
+ - [pi](https://github.com/earendil-works/pi) or [GSD](https://github.com/gsd-build/gsd-2)
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pi install npm:@saccolabs/pi-claude-cli
27
+ ```
28
+
29
+ Or declare it in `~/.pi/agent/settings.json` (global) or `.pi/settings.json` (project):
30
+
31
+ ```json
32
+ {
33
+ "packages": ["npm:@saccolabs/pi-claude-cli"]
34
+ }
35
+ ```
36
+
37
+ Then select a Claude model via `/model` in the interactive UI. All Claude models appear under the `pi-claude-cli` provider.
38
+
39
+ Requires the `claude` binary on your login-shell PATH (`npm install -g @anthropic-ai/claude-code`), authenticated with your Claude Pro/Max account.
40
+
41
+ ## Features
42
+
43
+ - Streams text, thinking, and tool call tokens in real-time
44
+ - Maps tool names and arguments bidirectionally between Claude and pi
45
+ - Exposes custom pi tools to Claude via MCP (schema-only, no execution)
46
+ - Break-early pattern prevents Claude CLI from auto-executing tools
47
+ - Session resume via `--resume` eliminates history replay on follow-up turns
48
+ - Configurable thinking effort across the full ladder (low to max) for all models, with elevated mapping for Opus
49
+ - Cross-platform subprocess management (Windows, macOS, Linux)
50
+ - Inactivity timeout and process registry for cleanup
51
+
52
+ ## License
53
+
54
+ MIT
package/index.ts ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Pi extension entry point for pi-claude-cli.
3
+ *
4
+ * Registers a custom provider that routes LLM calls through the Claude Code CLI
5
+ * subprocess using stream-json NDJSON protocol.
6
+ */
7
+
8
+ import { getBuiltinModels as getModels } from "@earendil-works/pi-ai/providers/all";
9
+ import { registerApiProvider } from "@earendil-works/pi-ai/compat";
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import { streamViaCli } from "./src/provider.js";
12
+ import {
13
+ validateCliPresence,
14
+ validateCliAuth,
15
+ killAllProcesses,
16
+ } from "./src/process-manager.js";
17
+ import { getCustomToolDefs, writeMcpConfig } from "./src/mcp-config.js";
18
+
19
+ // Kill all active Claude subprocesses on process exit to prevent orphans
20
+ process.on("exit", killAllProcesses);
21
+
22
+ const PROVIDER_ID = "pi-claude-cli";
23
+
24
+ let mcpConfigPath: string | undefined;
25
+ let mcpConfigResolved = false;
26
+
27
+ /**
28
+ * Lazily generate MCP config on first request (not at load time).
29
+ * pi.getAllTools() fails during extension loading; this defers it
30
+ * until the pi runtime is fully initialized.
31
+ *
32
+ * Only locks (sets mcpConfigResolved) when getAllTools() returns a
33
+ * real array — if it returns undefined/null (registry not ready),
34
+ * we retry on the next request. Once the registry is ready we
35
+ * commit to the result even if there are zero custom tools.
36
+ *
37
+ * Uses warn-don't-block: failure logs a warning but does not
38
+ * prevent the provider from functioning (built-ins still work).
39
+ */
40
+ function ensureMcpConfig(pi: ExtensionAPI): string | undefined {
41
+ if (mcpConfigResolved) return mcpConfigPath;
42
+ try {
43
+ const allTools = pi.getAllTools();
44
+
45
+ // Registry not ready yet — don't lock, retry on next call
46
+ if (!Array.isArray(allTools)) {
47
+ return mcpConfigPath;
48
+ }
49
+
50
+ // Registry is ready — lock regardless of whether custom tools exist
51
+ mcpConfigResolved = true;
52
+
53
+ const toolDefs = getCustomToolDefs(pi);
54
+ if (toolDefs.length > 0) {
55
+ mcpConfigPath = writeMcpConfig(toolDefs);
56
+ console.error(
57
+ `[pi-claude-cli] MCP config generated with ${toolDefs.length} custom tool(s)`,
58
+ );
59
+ }
60
+ } catch (err) {
61
+ console.warn(
62
+ "[pi-claude-cli] MCP config generation failed, custom tools unavailable:",
63
+ err,
64
+ );
65
+ }
66
+ return mcpConfigPath;
67
+ }
68
+
69
+ export default function (pi: ExtensionAPI) {
70
+ try {
71
+ // Startup validation
72
+ validateCliPresence(); // throws if CLI not on PATH
73
+ validateCliAuth(); // warns if not authenticated
74
+
75
+ const models = getModels("anthropic").map((model) => ({
76
+ id: model.id,
77
+ name: model.name,
78
+ reasoning: model.reasoning,
79
+ input: model.input,
80
+ cost: model.cost,
81
+ contextWindow: model.contextWindow,
82
+ maxTokens: model.maxTokens,
83
+ // pi's thinking selector only offers xhigh/max when the model's
84
+ // thinkingLevelMap declares them (getSupportedThinkingLevels in pi-ai);
85
+ // without this, every model is capped at "high" in the UI. The mapped
86
+ // values are unused by this provider — effort is derived from
87
+ // options.reasoning in mapThinkingEffort.
88
+ thinkingLevelMap: { xhigh: "xhigh", max: "max" },
89
+ }));
90
+
91
+ // Ensure all registered tools are active so pi can execute them.
92
+ // Some tools (find, grep, ls) are registered but not activated by default.
93
+ pi.on("session_start", async () => {
94
+ const allTools = pi.getAllTools();
95
+ if (Array.isArray(allTools)) {
96
+ pi.setActiveTools(allTools.map((t: any) => t.name));
97
+ }
98
+ });
99
+
100
+ const streamFn = (
101
+ model: Parameters<typeof streamViaCli>[0],
102
+ context: Parameters<typeof streamViaCli>[1],
103
+ options?: Parameters<typeof streamViaCli>[2],
104
+ ) => {
105
+ const configPath = ensureMcpConfig(pi);
106
+ return streamViaCli(model, context, {
107
+ ...options,
108
+ mcpConfigPath: configPath,
109
+ });
110
+ };
111
+
112
+ // pi.registerProvider() feeds pi's provider composer, but pi 0.84's
113
+ // default stream fn (pi-agent-core setDefaultStreamFn) resolves
114
+ // model.api against pi-ai's global api registry instead — print mode
115
+ // and nested agent loops take that path and would throw
116
+ // "No API provider registered for api: pi-claude-cli" (#32).
117
+ // Register the custom api id there too.
118
+ registerApiProvider(
119
+ { api: PROVIDER_ID as any, stream: streamFn, streamSimple: streamFn },
120
+ PROVIDER_ID,
121
+ );
122
+
123
+ pi.registerProvider(PROVIDER_ID, {
124
+ baseUrl: "pi-claude-cli",
125
+ apiKey: "unused",
126
+ api: "pi-claude-cli",
127
+ models,
128
+ streamSimple: streamFn,
129
+ });
130
+ } catch (err) {
131
+ console.error(`[pi-claude-cli] Failed to register provider:`, err);
132
+ }
133
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@saccolabs/pi-claude-cli",
3
+ "version": "0.4.0",
4
+ "description": "Pi coding agent extension that routes LLM calls through the Claude Code CLI",
5
+ "main": "index.ts",
6
+ "keywords": [
7
+ "pi-package"
8
+ ],
9
+ "pi": {
10
+ "extensions": [
11
+ "index.ts"
12
+ ]
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/agustinsacco/pi-claude-cli"
17
+ },
18
+ "license": "MIT",
19
+ "peerDependencies": {
20
+ "@earendil-works/pi-ai": "*",
21
+ "@earendil-works/pi-coding-agent": "*"
22
+ },
23
+ "dependencies": {
24
+ "cross-spawn": "^7.0.6"
25
+ },
26
+ "devDependencies": {
27
+ "@earendil-works/pi-ai": "0.84.2",
28
+ "@earendil-works/pi-coding-agent": "0.84.2",
29
+ "@eslint/js": "^9.39.4",
30
+ "@types/cross-spawn": "^6.0.6",
31
+ "@types/node": "^22.0.0",
32
+ "@vitest/coverage-v8": "^3.2.4",
33
+ "eslint": "^9.39.4",
34
+ "eslint-config-prettier": "^10.1.8",
35
+ "husky": "^9.1.7",
36
+ "lint-staged": "^16.4.0",
37
+ "prettier": "^3.8.1",
38
+ "typescript": "^5.7.0",
39
+ "typescript-eslint": "^8.57.0",
40
+ "vitest": "^3.0.0"
41
+ },
42
+ "lint-staged": {
43
+ "*.{ts,js,cjs,mjs}": [
44
+ "eslint --fix",
45
+ "prettier --write"
46
+ ],
47
+ "*.{json,md,yml,yaml}": [
48
+ "prettier --write"
49
+ ]
50
+ },
51
+ "scripts": {
52
+ "test": "vitest run --reporter=verbose",
53
+ "test:coverage": "vitest run --coverage",
54
+ "typecheck": "tsc --noEmit",
55
+ "lint": "eslint .",
56
+ "format:check": "prettier --check .",
57
+ "prepare": "husky || true",
58
+ "test:e2e": "bash scripts/e2e-stub.sh"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "engines": {
64
+ "node": ">=22"
65
+ },
66
+ "files": [
67
+ "index.ts",
68
+ "src/",
69
+ "README.md",
70
+ "LICENSE"
71
+ ]
72
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Control protocol handler for Claude CLI stream-json communication.
3
+ *
4
+ * Processes control_request messages from Claude CLI stdout and writes
5
+ * control_response messages to stdin.
6
+ *
7
+ * - Custom MCP tools (mcp__custom-tools__*): DENIED — pi executes these
8
+ * - Everything else (user MCP tools, internal tools): ALLOWED — Claude handles
9
+ */
10
+
11
+ import type { ClaudeControlRequest } from "./types";
12
+ import { CUSTOM_TOOLS_MCP_PREFIX } from "./tool-mapping.js";
13
+
14
+ export const TOOL_EXECUTION_DENIED_MESSAGE =
15
+ "Tool execution is unavailable in this environment.";
16
+
17
+ /** Prefix for MCP (Model Context Protocol) tool names. */
18
+ export const MCP_PREFIX = "mcp__";
19
+
20
+ interface ControlResponse {
21
+ type: "control_response";
22
+ request_id: string;
23
+ response: {
24
+ subtype: "success";
25
+ response: {
26
+ behavior: "allow" | "deny";
27
+ message?: string;
28
+ };
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Handle a control_request from the Claude CLI.
34
+ *
35
+ * Denies custom MCP tools (mcp__custom-tools__*) so pi can execute them.
36
+ * Allows everything else (user MCP tools, internal Claude tools).
37
+ *
38
+ * @returns true if the tool was allowed, false if denied
39
+ */
40
+ export function handleControlRequest(
41
+ msg: ClaudeControlRequest,
42
+ stdin: NodeJS.WritableStream,
43
+ ): boolean {
44
+ if (!msg.request_id || !msg.request) {
45
+ console.error(
46
+ "[pi-claude-cli] Malformed control_request: missing request_id or request object",
47
+ msg,
48
+ );
49
+ return false;
50
+ }
51
+
52
+ const toolName = msg.request?.tool_name ?? "";
53
+ const isCustomTool = toolName.startsWith(CUSTOM_TOOLS_MCP_PREFIX);
54
+
55
+ const response: ControlResponse = {
56
+ type: "control_response",
57
+ request_id: msg.request_id,
58
+ response: {
59
+ subtype: "success",
60
+ response: isCustomTool
61
+ ? { behavior: "deny", message: TOOL_EXECUTION_DENIED_MESSAGE }
62
+ : { behavior: "allow" },
63
+ },
64
+ };
65
+
66
+ stdin.write(JSON.stringify(response) + "\n");
67
+ return !isCustomTool;
68
+ }