@vanillagreen/pi-claude-bridge 1.9.0 → 2.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.
- package/README.md +24 -11
- package/bundle/connector-inventory.js +10 -0
- package/bundle/index.js +2418 -1794
- package/package.json +6 -6
- package/src/assistant-stream.ts +313 -46
- package/src/auth-presence.ts +6 -50
- package/src/bridge-state.ts +43 -1
- package/src/connector-audit.ts +203 -0
- package/src/connector-cache.ts +118 -0
- package/src/connector-inventory.ts +52 -0
- package/src/connectors.ts +142 -1
- package/src/convert.ts +14 -0
- package/src/index.ts +330 -172
- package/src/native-provider.ts +89 -0
- package/src/query-state.ts +218 -9
- package/src/query-teardown.ts +45 -0
- package/src/rate-limit.ts +42 -10
- package/src/session-persistence.ts +7 -2
- package/src/tool-pairing-audit.ts +48 -0
- package/src/typebox-to-zod.ts +9 -3
package/README.md
CHANGED
|
@@ -23,6 +23,10 @@ Forked from [`elidickinson/pi-claude-bridge`](https://github.com/elidickinson/pi
|
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
26
|
+
Requires pi ≥ 0.81 (bridge 2.x registers through pi's native provider API, so pi shows the
|
|
27
|
+
Claude models only while a Claude account is actually connected). On older pi, install
|
|
28
|
+
`@vanillagreen/pi-claude-bridge@1.x` instead.
|
|
29
|
+
|
|
26
30
|
Via [npm](https://www.npmjs.com/package/@vanillagreen/pi-claude-bridge):
|
|
27
31
|
|
|
28
32
|
```bash
|
|
@@ -108,6 +112,20 @@ Turn this on and the model can use whatever your Claude account already has conn
|
|
|
108
112
|
|
|
109
113
|
Sessions are **read-only** by default: the model can look things up, but cannot send, post, or change anything unless you explicitly turn writes on below.
|
|
110
114
|
|
|
115
|
+
Connector tools run inside Claude Code rather than in Pi, so Pi shows the model's answer but no tool card for the lookup itself. (Before this was handled, Pi showed a card claiming `Tool … not found` for calls that had actually succeeded — so an answer built on real data looked invented.)
|
|
116
|
+
|
|
117
|
+
Each of those lookups is still recorded in the session file as a `claude-bridge-connector-call` entry — the tool name, whether it succeeded, and how many bytes came back, never the contents. So "did it really look that up?" has an answer even though nothing is drawn in the transcript.
|
|
118
|
+
|
|
119
|
+
That entry needs a pi session to be written into. A host that embeds the bridge **without** one — loading it through a bare resource loader, so `extensionApi` is undefined — gets no record at all, and nothing in the bridge can tell. Such a host can install its own destination:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { setConnectorCallAuditSink } from "@vanillagreen/pi-claude-bridge";
|
|
123
|
+
|
|
124
|
+
setConnectorCallAuditSink((record) => myOwnAuditTrail(record)); // pass undefined to clear
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The sink **adds** a destination; it never replaces the session entry. A session-backed host that installs one gets both, so turning it on can never cost you the record you already had. Same payload-free shape as the entry (`name`, `toolUseId`, `outcome`, and where known `byteSize` / `childSessionId` / `reason`), and the same never-fails-a-turn rule: a sink that throws is caught and dropped. It is process-global, like the bridge's other host handles, so a host running several conversations in one process must route by `childSessionId` itself.
|
|
128
|
+
|
|
111
129
|
Extension-manager settings use flat package-scoped keys:
|
|
112
130
|
|
|
113
131
|
```json
|
|
@@ -134,14 +152,11 @@ Legacy `.pi/claude-bridge.json` configuration keeps these options nested under `
|
|
|
134
152
|
|
|
135
153
|
For both, the env var wins over config. `connectorWriteMode` only matters when connectors are enabled. Any value other than exactly `allow` is treated as `deny` (fail-closed).
|
|
136
154
|
|
|
137
|
-
With `connectorWriteMode: "deny"` (the default), connector sessions are **read-only**: search/read/fetch/list tools stay available,
|
|
138
|
-
|
|
139
|
-
- **Model context:** the known write tools are passed as `disallowedTools` (exact tool ids), so the model does not see them. (Note: the CLI's MCP permission matcher only supports exact tool names or a whole-server `mcp__server__*` glob — partial tool-segment globs are inert — so exact ids are what actually removes today's writes.)
|
|
140
|
-
- **Runtime:** a `PreToolUse` hook blocks any connector tool classified as a write at call time, regardless of permission mode. Classification is **fail-closed** and covers the whole `mcp__claude_ai_<Server>__` space — a tool there is a write unless its name *begins* with a known read verb (`list`, `search`, `get`, `read`, `fetch`, …). The verb is matched as a word across naming styles, since connector servers differ: `search_threads` (Gmail), `slack_read_channel` (Slack, server-prefixed), `getJiraIssue` (Atlassian, camelCase) are all reads; a leading word that merely repeats the server name is skipped first. A name that opens with a read verb but also names a mutation (`getOrCreateChannel`) is a write, and so is a name that does not parse as `<server>__<tool>`. This covers both not-yet-known write tools (e.g. a future Gmail `send_message` or Drive `delete_file`) and connectors beyond the Google trio: claude.ai connectors attach account-wide, so a Slack/Atlassian/org-custom connector is visible in a connector session, and its writes are denied by the same rule.
|
|
155
|
+
With `connectorWriteMode: "deny"` (the default), connector sessions are **read-only**: search/read/fetch/list tools stay available, while mutating tools are denied twice — the known write tools are removed from the model's tool list, and a runtime hook blocks any connector tool classified as a write at call time, regardless of permission mode. Classification is fail-closed across every connector on the account: a connector tool counts as a write unless its name begins with a known read verb, so not-yet-known write tools and future connectors are denied by the same rule.
|
|
141
156
|
|
|
142
157
|
Set `allow` only for a one-shot write-executor session that has already obtained explicit user approval — never for an interactive connector chat.
|
|
143
158
|
|
|
144
|
-
> **`allow` is per-process, not global.**
|
|
159
|
+
> **`allow` is per-process, not global.** A host's approved-write executor should set `CLAUDE_BRIDGE_CONNECTOR_WRITE=allow` in the **child env of a dedicated one-shot process** that runs the single approved write and exits. Do not set `connectorWriteMode: "allow"` in persistent `settings.json` (or `allow` process-globally) for a shared/long-lived sidecar — that would make every connector session in that process write-capable, defeating the approval gate.
|
|
145
160
|
|
|
146
161
|
### Isolated mode (embedding hosts)
|
|
147
162
|
|
|
@@ -161,9 +176,7 @@ The bridge registers `claude-bridge/claude-fable-5`, `claude-bridge/claude-opus-
|
|
|
161
176
|
|
|
162
177
|
## Connector inventory
|
|
163
178
|
|
|
164
|
-
`/claude-bridge:connectors` lists the Claude account's installed claude.ai connectors by asking the account, not the model.
|
|
165
|
-
|
|
166
|
-
The older way to answer "does this account have Slack?" was a capability probe: a model turn that enumerated connectors via `ToolSearch`. A search returns what the search surfaced — a lower bound — and nothing in the result said so, so an account with Slack attached could produce an inventory without Slack and no failure signal (vstack#838). This command calls the account's connector list endpoint instead, so the answer is complete by construction.
|
|
179
|
+
`/claude-bridge:connectors` lists the Claude account's installed claude.ai connectors by asking the account, not the model, so the answer is complete by construction.
|
|
167
180
|
|
|
168
181
|
`listAccountConnectors()` is the programmatic form for host apps. Import it from the package's `./connector-inventory` entry point:
|
|
169
182
|
|
|
@@ -171,13 +184,13 @@ The older way to answer "does this account have Slack?" was a capability probe:
|
|
|
171
184
|
import { listAccountConnectors, resolveClaudeOAuth } from "@vanillagreen/pi-claude-bridge/connector-inventory";
|
|
172
185
|
```
|
|
173
186
|
|
|
174
|
-
|
|
187
|
+
The same functions are re-exported from the package root for consuming apps whose vendored `package.json` uses a closed exports map (`{".": "./bundle/index.js"}`), which blocks every subpath:
|
|
175
188
|
|
|
176
189
|
```ts
|
|
177
190
|
import { listAccountConnectors } from "@vanillagreen/pi-claude-bridge";
|
|
178
191
|
```
|
|
179
192
|
|
|
180
|
-
|
|
193
|
+
It returns a discriminated result: on success `{ ok: true, complete: true, connectors }`, and on any transport or protocol failure `{ ok: false, reason }`. An account with no connectors is a successful empty list; a failure is never reported as an empty inventory. Credentials resolve from `CLAUDE_CONFIG_DIR` before `$HOME`, so a host running one sidecar per Claude account reads the right account.
|
|
181
194
|
|
|
182
195
|
## Extra usage and rate limits
|
|
183
196
|
|
|
@@ -193,7 +206,7 @@ If Claude Code accepts a turn but produces no visible output, the bridge returns
|
|
|
193
206
|
|
|
194
207
|
Set `CLAUDE_BRIDGE_DEBUG=1` to write bridge logs to `<agent dir>/claude-bridge.log` and per-query Claude Code CLI logs under `<agent dir>/cc-cli-logs/`, where `<agent dir>` is `PI_CODING_AGENT_DIR` when set, else `~/.pi/agent`. Override the exact files with `CLAUDE_BRIDGE_DEBUG_PATH` / `CLAUDE_BRIDGE_DIAG_PATH`.
|
|
195
208
|
|
|
196
|
-
Tool-result integrity problems are surfaced even when debug logging is off. Pi shows an error notification
|
|
209
|
+
Tool-result integrity problems are surfaced even when debug logging is off. Pi shows an error notification, writes a diagnostic file to `<agent dir>/claude-bridge-diag.log`, and appends a `claude-bridge-integrity` custom entry to the pi session transcript (compact metadata only — never tool output), so lost or mismatched tool output stays analyzable from the session file alone.
|
|
197
210
|
|
|
198
211
|
Startup failures include the resolved Claude executable and working directory, which makes missing binaries and wrong launch directories easier to fix.
|
|
199
212
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// src/connector-inventory.ts
|
|
2
2
|
var CONNECTOR_NS_PREFIX = "mcp__claude_ai_";
|
|
3
3
|
var DEFAULT_API_BASE = "https://api.anthropic.com";
|
|
4
|
+
var DEFAULT_PROXY_BASE = "https://mcp-proxy.anthropic.com/v1/mcp";
|
|
4
5
|
var OAUTH_BETA_HEADER = "oauth-2025-04-20";
|
|
6
|
+
function connectorServerName(connectorName) {
|
|
7
|
+
return `claude.ai ${connectorName.trim()}`;
|
|
8
|
+
}
|
|
9
|
+
function connectorProxyUrl(installedServerId, proxyBase = DEFAULT_PROXY_BASE) {
|
|
10
|
+
return `${trimTrailingSlashes(proxyBase)}/${encodeURIComponent(installedServerId)}`;
|
|
11
|
+
}
|
|
5
12
|
function connectorServerNamespace(connectorName) {
|
|
6
13
|
return `${CONNECTOR_NS_PREFIX}${connectorName.trim().replace(/\s+/g, "_")}__`;
|
|
7
14
|
}
|
|
@@ -103,6 +110,7 @@ async function listAccountConnectors(deps) {
|
|
|
103
110
|
name,
|
|
104
111
|
installedServerId: nonEmptyString(entry?.installedServerId),
|
|
105
112
|
directoryUuid: nonEmptyString(entry?.directoryUuid),
|
|
113
|
+
installState: nonEmptyString(entry?.installState),
|
|
106
114
|
description: nonEmptyString(entry?.description),
|
|
107
115
|
isAuthless: typeof entry?.isAuthless === "boolean" ? entry.isAuthless : void 0
|
|
108
116
|
});
|
|
@@ -129,6 +137,8 @@ function errorText(error) {
|
|
|
129
137
|
return error instanceof Error ? error.message : String(error);
|
|
130
138
|
}
|
|
131
139
|
export {
|
|
140
|
+
connectorProxyUrl,
|
|
141
|
+
connectorServerName,
|
|
132
142
|
connectorServerNamespace,
|
|
133
143
|
connectorsListUrl,
|
|
134
144
|
credentialCandidatePaths,
|