@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 CHANGED
@@ -1,10 +1,20 @@
1
1
  # pi-claude-bridge
2
+
2
3
  ![Claude bridge demo response](https://raw.githubusercontent.com/vanillagreencom/vstack/main/pi-extensions/pi-claude-bridge/assets/bridge-demo.png)
3
4
  ![Claude Bridge settings panel](https://raw.githubusercontent.com/vanillagreencom/vstack/main/pi-extensions/pi-claude-bridge/assets/settings-panel.png)
4
5
 
5
- Claude Code provider bridge for Pi. It registers `claude-bridge/*` models and routes Pi turns through the Claude Agent SDK while keeping Pi's tools in the Pi TUI.
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
- This package is a vstack fork of [`elidickinson/pi-claude-bridge`](https://github.com/elidickinson/pi-claude-bridge). The provider, MCP tool bridge, session sync, streaming, and Claude Code SDK plumbing come from that project; this fork removes the AskClaude tool and adds vstack-controlled prompt-context forwarding.
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 prompt context is **off by default** and can be enabled in `/extensions:settings` `Claude Bridge`:
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
- | Setting | Default | Effect |
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
- The bridge detects these hook additions from the effective Pi system prompt for the current turn and forwards only enabled recognized blocks.
46
+ ### General
48
47
 
49
- ## Settings
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
- | Setting | Default | Notes |
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
- Legacy config files are still read: `~/.pi/agent/claude-bridge.json` and `.pi/claude-bridge.json`. vstack extension-manager settings override them.
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
- ## Differences from upstream
72
+ ### Claude Code
61
73
 
62
- - No AskClaude tool; this package is provider-only.
63
- - vstack extension-manager settings for prompt forwarding and Claude Code options.
64
- - Bundled runtime dependencies for vstack installs; the bridge auto-detects the local Claude Code executable.
65
- - Opt-in delivery of `APPEND_SYSTEM.md` and recognized Pi `before_agent_start` prompt hooks.
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 (e2) {
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" || event.reason === "fork") restoreSharedSessionFromPi(ctx2);
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.0",
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 ] && . .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",
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 (e) {
77
- console.error(`claude-bridge: failed to parse ${path}: ${e}`);
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 restoreSharedSessionFromPi(ctx: { sessionManager?: unknown }): void {
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
- if (event.reason === "startup" || event.reason === "resume" || event.reason === "fork") restoreSharedSessionFromPi(ctx);
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) => {
@@ -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");