@vanillagreen/pi-claude-bridge 1.1.0 → 1.1.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 +43 -32
- package/bundle/index.js +32 -9
- package/package.json +2 -2
- package/src/config.ts +3 -2
- package/src/index.ts +45 -2
- package/src/prompt-context.ts +1 -5
package/README.md
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
# pi-claude-bridge
|
|
2
|
+
|
|
2
3
|

|
|
3
4
|

|
|
4
5
|
|
|
5
|
-
Claude Code
|
|
6
|
+
Run Claude Code as a Pi provider. Adds `claude-bridge/*` models to `/model` and routes Pi turns through the Claude Agent SDK while keeping Pi's tools and TUI.
|
|
7
|
+
|
|
8
|
+
Forked from [`elidickinson/pi-claude-bridge`](https://github.com/elidickinson/pi-claude-bridge). The provider, MCP bridge, session sync, and SDK plumbing come from upstream; this fork removes the AskClaude tool and adds opt-in forwarding for Pi prompt context.
|
|
9
|
+
|
|
10
|
+
## Highlights
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
- `claude-bridge/claude-opus-4-7`, Sonnet, and Haiku in `/model`.
|
|
13
|
+
- Pi tool calls run on Pi; Claude Code handles reasoning.
|
|
14
|
+
- Session continuity across normal turns, `/compact`, tree navigation, and abort recovery.
|
|
15
|
+
- Thinking-level forwarding with summarized Opus thinking display.
|
|
16
|
+
- MCP isolation and Claude cloud-MCP suppression to keep tokens lean.
|
|
17
|
+
- Opt-in forwarding of `APPEND_SYSTEM.md` and recognized Pi prompt hooks.
|
|
8
18
|
|
|
9
19
|
## Install
|
|
10
20
|
|
|
@@ -23,47 +33,48 @@ vstack add vanillagreencom/vstack --pi-extension pi-claude-bridge --harness pi -
|
|
|
23
33
|
|
|
24
34
|
Restart Pi after installation.
|
|
25
35
|
|
|
26
|
-
## What it provides
|
|
27
|
-
|
|
28
|
-
- `claude-bridge/claude-opus-4-7`, Sonnet, and Haiku models in `/model`.
|
|
29
|
-
- Pi tool calls bridged to Claude Code through a local MCP server.
|
|
30
|
-
- Session reuse/rebuild so Claude Code follows Pi history across normal turns, `/compact`, forks, tree navigation, and abort recovery.
|
|
31
|
-
- Thinking-level forwarding, summarized Opus thinking display, MCP isolation, and Claude cloud-MCP suppression to reduce token overhead.
|
|
32
|
-
- Optional forwarding of Pi-only context that upstream does not pass to Claude Code.
|
|
33
|
-
|
|
34
36
|
## Prompt context
|
|
35
37
|
|
|
36
38
|
Default behavior matches upstream: append `AGENTS.md` plus Pi's skills block to Claude Code's `claude_code` preset prompt.
|
|
37
39
|
|
|
38
|
-
Extra Pi
|
|
40
|
+
Extra Pi context is off by default. Enable per item in the extension manager when you want Claude Code to see prompt blocks that other Pi extensions add to your session.
|
|
41
|
+
|
|
42
|
+
## Settings
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
| --- | --- | --- |
|
|
42
|
-
| `includeAppendSystemPromptMd` | off | Forward project/global `APPEND_SYSTEM.md`, including `.pi/APPEND_SYSTEM.md`. |
|
|
43
|
-
| `includeProjectAgentsHook` | off | Forward recognized `pi-agents-tmux` `before_agent_start` additions such as the Project Agents/Subagents list. |
|
|
44
|
-
| `includeTaskPanelHook` | off | Forward recognized `pi-task-panel` workflow reminders. |
|
|
45
|
-
| `includeCavemanHook` | off | Forward recognized `pi-caveman` response-style prompt additions. |
|
|
44
|
+
All settings live in the extension manager under **Claude Bridge**.
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
### General
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
| Setting | What it does |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| Enable Claude bridge provider | Register `claude-bridge/*` models. Reload required. |
|
|
51
|
+
|
|
52
|
+
### Base prompt
|
|
53
|
+
|
|
54
|
+
| Setting | What it does |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| Forward AGENTS.md + skills | Append AGENTS.md and Pi's skills block. |
|
|
57
|
+
|
|
58
|
+
### Pi prompt context
|
|
59
|
+
|
|
60
|
+
| Setting | What it does |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| Forward APPEND_SYSTEM.md | Forward project/global `APPEND_SYSTEM.md` content. |
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
| --- | --- | --- |
|
|
53
|
-
| `enabled` | on | Register the Claude bridge provider; reload required. |
|
|
54
|
-
| `appendSystemPrompt` | on | Forward `AGENTS.md` + skills. |
|
|
55
|
-
| `strictMcpConfig` | on | When Claude Code filesystem settings are loaded, block filesystem MCP auto-loads; Pi owns tool execution. |
|
|
56
|
-
| `pathToClaudeCodeExecutable` | auto | Explicit `claude` binary path; empty auto-detects `claude` or `claude-code` on PATH. |
|
|
64
|
+
### Pi prompt hooks
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
| Setting | What it does |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| Forward project agents hook | Forward `pi-agents-tmux` Project Agents/Subagents list. |
|
|
69
|
+
| Forward task panel hook | Forward `pi-task-panel` workflow reminders. |
|
|
70
|
+
| Forward caveman hook | Forward `pi-caveman` response-style directives. |
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
### Claude Code
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- Keeps upstream provider fixes: ID-based tool-result matching, compact/session rebuild handling, abort recovery, skill read-tool rewriting, strict MCP config, cloud MCP suppression, and Opus 4.7 thinking display forcing.
|
|
74
|
+
| Setting | What it does |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| Strict MCP config | Block filesystem MCP auto-loads; Pi owns tools. |
|
|
77
|
+
| Claude executable path | Explicit `claude` binary path; empty auto-detects. |
|
|
67
78
|
|
|
68
79
|
## Debugging
|
|
69
80
|
|
package/bundle/index.js
CHANGED
|
@@ -19186,6 +19186,7 @@ function readSession(jsonlPath, projectPath) {
|
|
|
19186
19186
|
// src/index.ts
|
|
19187
19187
|
import { createHash } from "crypto";
|
|
19188
19188
|
import { accessSync, appendFileSync as appendFileSync3, constants as fsConstants, mkdirSync as mkdirSync3, realpathSync as realpathSync3, statSync as statSync3 } from "fs";
|
|
19189
|
+
import { resolve as pathResolve } from "path";
|
|
19189
19190
|
import { homedir as homedir5 } from "os";
|
|
19190
19191
|
import { delimiter, dirname as dirname5, join as join5 } from "path";
|
|
19191
19192
|
|
|
@@ -19596,8 +19597,7 @@ function tryParseJson(path) {
|
|
|
19596
19597
|
if (!existsSync3(path)) return {};
|
|
19597
19598
|
try {
|
|
19598
19599
|
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
19599
|
-
} catch
|
|
19600
|
-
console.error(`claude-bridge: failed to parse ${path}: ${e2}`);
|
|
19600
|
+
} catch {
|
|
19601
19601
|
return {};
|
|
19602
19602
|
}
|
|
19603
19603
|
}
|
|
@@ -19797,11 +19797,7 @@ ${taskReminder}`);
|
|
|
19797
19797
|
}
|
|
19798
19798
|
}
|
|
19799
19799
|
if (settings.includeCavemanHook) {
|
|
19800
|
-
const caveman = extractBlockByMarkers(systemPrompt, [
|
|
19801
|
-
/^Caveman communication mode active/m,
|
|
19802
|
-
/^Token efficiency mode: terse smart caveman/m,
|
|
19803
|
-
/^Caveman mode is active/m
|
|
19804
|
-
]);
|
|
19800
|
+
const caveman = extractBlockByMarkers(systemPrompt, [/^You MUST respond in caveman /m]);
|
|
19805
19801
|
if (caveman) {
|
|
19806
19802
|
parts.push(`### before_agent_start: caveman
|
|
19807
19803
|
|
|
@@ -34544,9 +34540,34 @@ function claudeSessionExists(sessionId, cwd) {
|
|
|
34544
34540
|
return false;
|
|
34545
34541
|
}
|
|
34546
34542
|
}
|
|
34543
|
+
function canonicalize(p4) {
|
|
34544
|
+
if (!p4) return void 0;
|
|
34545
|
+
try {
|
|
34546
|
+
return realpathSync3.native(p4);
|
|
34547
|
+
} catch {
|
|
34548
|
+
return pathResolve(p4);
|
|
34549
|
+
}
|
|
34550
|
+
}
|
|
34551
|
+
function shouldRestorePersistedBridgeEntry(persisted, currentPiSessionId, currentCwd) {
|
|
34552
|
+
if (!persisted.piSessionId) return "missing piSessionId";
|
|
34553
|
+
if (currentPiSessionId && persisted.piSessionId !== currentPiSessionId) {
|
|
34554
|
+
return `piSessionId mismatch (persisted=${persisted.piSessionId} current=${currentPiSessionId})`;
|
|
34555
|
+
}
|
|
34556
|
+
if (currentCwd && canonicalize(persisted.cwd) !== canonicalize(currentCwd)) {
|
|
34557
|
+
return `cwd mismatch (persisted=${persisted.cwd} current=${currentCwd})`;
|
|
34558
|
+
}
|
|
34559
|
+
return void 0;
|
|
34560
|
+
}
|
|
34547
34561
|
function restoreSharedSessionFromPi(ctx2) {
|
|
34548
34562
|
const persisted = latestPersistedBridgeSession(ctx2.sessionManager);
|
|
34549
34563
|
if (!persisted) return;
|
|
34564
|
+
const currentPiSessionId = typeof ctx2.sessionManager?.getSessionId === "function" ? ctx2.sessionManager.getSessionId() : void 0;
|
|
34565
|
+
const currentCwd = typeof ctx2.sessionManager?.getCwd === "function" ? ctx2.sessionManager.getCwd() : ctx2.cwd;
|
|
34566
|
+
const rejection = shouldRestorePersistedBridgeEntry(persisted, currentPiSessionId, currentCwd);
|
|
34567
|
+
if (rejection) {
|
|
34568
|
+
debug(`restoreSharedSession: ${rejection} \u2014 forcing rebuild`);
|
|
34569
|
+
return;
|
|
34570
|
+
}
|
|
34550
34571
|
const built = readBuiltSessionContext(ctx2.sessionManager);
|
|
34551
34572
|
if (!built) return;
|
|
34552
34573
|
const cursor = Math.max(0, Math.min(persisted.cursor, built.messages.length));
|
|
@@ -35322,7 +35343,7 @@ function index_default(pi) {
|
|
|
35322
35343
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
35323
35344
|
clearSession(`session_start:${event.reason}`);
|
|
35324
35345
|
}
|
|
35325
|
-
if (event.reason === "startup" || event.reason === "resume"
|
|
35346
|
+
if (event.reason === "startup" || event.reason === "resume") restoreSharedSessionFromPi(ctx2);
|
|
35326
35347
|
});
|
|
35327
35348
|
pi.on("session_shutdown", () => clearSession("session_shutdown"));
|
|
35328
35349
|
pi.on("message_end", (event, ctx2) => {
|
|
@@ -35356,5 +35377,7 @@ export {
|
|
|
35356
35377
|
CLAUDE_BRIDGE_TOOL_ISOLATION,
|
|
35357
35378
|
DISALLOWED_BUILTIN_TOOLS,
|
|
35358
35379
|
index_default as default,
|
|
35359
|
-
mapToolName
|
|
35380
|
+
mapToolName,
|
|
35381
|
+
restoreSharedSessionFromPi,
|
|
35382
|
+
shouldRestorePersistedBridgeEntry
|
|
35360
35383
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-claude-bridge",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Pi provider bridge that runs Claude Code through the Claude Agent SDK, with opt-in forwarding for Pi prompt context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=bundle/index.js --external:@earendil-works/pi-ai --external:@earendil-works/pi-coding-agent",
|
|
120
120
|
"prepack": "npm run build",
|
|
121
121
|
"test:unit": "node --import tsx --test tests/unit-*.mjs",
|
|
122
|
-
"test": "set -a && [ -f .env.test ] && .
|
|
122
|
+
"test": "set -a && [ -f .env.test ] && . ./.env.test; set +a && npm run test:unit && tests/int-smoke.sh && tests/int-multi-turn.sh && tests/int-cache.sh && node --import tsx --test tests/int-*.mjs",
|
|
123
123
|
"test:usage": "tests/usage-test.sh",
|
|
124
124
|
"typecheck": "tsc --noEmit"
|
|
125
125
|
},
|
package/src/config.ts
CHANGED
|
@@ -73,8 +73,9 @@ export function tryParseJson(path: string): Partial<Config> {
|
|
|
73
73
|
if (!existsSync(path)) return {};
|
|
74
74
|
try {
|
|
75
75
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
76
|
-
} catch
|
|
77
|
-
|
|
76
|
+
} catch {
|
|
77
|
+
// Malformed optional config should not write raw terminal diagnostics;
|
|
78
|
+
// stdout/stderr output can corrupt active Pi TUI widgets.
|
|
78
79
|
return {};
|
|
79
80
|
}
|
|
80
81
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Base64ImageSource, ContentBlockParam, MessageParam } from "@anthro
|
|
|
6
6
|
import { createSession, deleteSession, openSession, repairToolPairing } from "cc-session-io";
|
|
7
7
|
import { createHash } from "crypto";
|
|
8
8
|
import { accessSync, appendFileSync, constants as fsConstants, mkdirSync, realpathSync, statSync } from "fs";
|
|
9
|
+
import { resolve as pathResolve } from "path";
|
|
9
10
|
import { homedir } from "os";
|
|
10
11
|
import { delimiter, dirname, join } from "path";
|
|
11
12
|
import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
|
|
@@ -238,9 +239,47 @@ function claudeSessionExists(sessionId: string, cwd: string): boolean {
|
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
function
|
|
242
|
+
function canonicalize(p: string | undefined): string | undefined {
|
|
243
|
+
if (!p) return undefined;
|
|
244
|
+
try { return realpathSync.native(p); } catch { return pathResolve(p); }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Decides whether a persisted bridge-session marker is safe to restore.
|
|
248
|
+
//
|
|
249
|
+
// The fork case is the load-bearing one: pi/core's createBranchedSession copies
|
|
250
|
+
// every non-label entry from root→leaf into the new session file. That includes
|
|
251
|
+
// our claude-bridge-session markers from the parent. Restoring from them would
|
|
252
|
+
// --resume parent's Claude jsonl on the fork's first turn, leaking conversation
|
|
253
|
+
// past the fork point.
|
|
254
|
+
//
|
|
255
|
+
// Returns undefined when the entry is safe to use, or a short rejection reason
|
|
256
|
+
// for diagnostic logging. Old entries without piSessionId always reject, which
|
|
257
|
+
// degrades safely to the rebuild path.
|
|
258
|
+
export function shouldRestorePersistedBridgeEntry(
|
|
259
|
+
persisted: { piSessionId?: string; cwd: string },
|
|
260
|
+
currentPiSessionId: string | undefined,
|
|
261
|
+
currentCwd: string | undefined,
|
|
262
|
+
): string | undefined {
|
|
263
|
+
if (!persisted.piSessionId) return "missing piSessionId";
|
|
264
|
+
if (currentPiSessionId && persisted.piSessionId !== currentPiSessionId) {
|
|
265
|
+
return `piSessionId mismatch (persisted=${persisted.piSessionId} current=${currentPiSessionId})`;
|
|
266
|
+
}
|
|
267
|
+
if (currentCwd && canonicalize(persisted.cwd) !== canonicalize(currentCwd)) {
|
|
268
|
+
return `cwd mismatch (persisted=${persisted.cwd} current=${currentCwd})`;
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function restoreSharedSessionFromPi(ctx: { sessionManager?: unknown; cwd?: string }): void {
|
|
242
274
|
const persisted = latestPersistedBridgeSession(ctx.sessionManager);
|
|
243
275
|
if (!persisted) return;
|
|
276
|
+
const currentPiSessionId = typeof (ctx.sessionManager as any)?.getSessionId === "function" ? (ctx.sessionManager as any).getSessionId() : undefined;
|
|
277
|
+
const currentCwd = typeof (ctx.sessionManager as any)?.getCwd === "function" ? (ctx.sessionManager as any).getCwd() : ctx.cwd;
|
|
278
|
+
const rejection = shouldRestorePersistedBridgeEntry(persisted, currentPiSessionId, currentCwd);
|
|
279
|
+
if (rejection) {
|
|
280
|
+
debug(`restoreSharedSession: ${rejection} — forcing rebuild`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
244
283
|
const built = readBuiltSessionContext(ctx.sessionManager);
|
|
245
284
|
if (!built) return;
|
|
246
285
|
const cursor = Math.max(0, Math.min(persisted.cursor, built.messages.length));
|
|
@@ -1292,7 +1331,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1292
1331
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
1293
1332
|
clearSession(`session_start:${event.reason}`);
|
|
1294
1333
|
}
|
|
1295
|
-
|
|
1334
|
+
// Note: "fork" intentionally omitted from restoration. createBranchedSession
|
|
1335
|
+
// copies the parent's persisted bridge entries into the fork; restoring from
|
|
1336
|
+
// them would --resume the parent's Claude jsonl and leak conversation past the
|
|
1337
|
+
// fork point. Letting the first fork turn rebuild is the correct path.
|
|
1338
|
+
if (event.reason === "startup" || event.reason === "resume") restoreSharedSessionFromPi(ctx);
|
|
1296
1339
|
});
|
|
1297
1340
|
pi.on("session_shutdown", () => clearSession("session_shutdown"));
|
|
1298
1341
|
pi.on("message_end", (event, ctx) => {
|
package/src/prompt-context.ts
CHANGED
|
@@ -115,11 +115,7 @@ export function buildPromptContextAppend(systemPrompt: string | undefined, cwd:
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
if (settings.includeCavemanHook) {
|
|
118
|
-
const caveman = extractBlockByMarkers(systemPrompt, [
|
|
119
|
-
/^Caveman communication mode active/m,
|
|
120
|
-
/^Token efficiency mode: terse smart caveman/m,
|
|
121
|
-
/^Caveman mode is active/m,
|
|
122
|
-
]);
|
|
118
|
+
const caveman = extractBlockByMarkers(systemPrompt, [/^You MUST respond in caveman /m]);
|
|
123
119
|
if (caveman) {
|
|
124
120
|
parts.push(`### before_agent_start: caveman\n\n${caveman}`);
|
|
125
121
|
labels.push("caveman hook");
|