@vanillagreen/pi-claude-bridge 1.2.0 → 1.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/README.md +25 -2
- package/bundle/index.js +7304 -6333
- package/package.json +38 -3
- package/src/config.ts +67 -1
- package/src/convert.ts +77 -20
- package/src/index.ts +590 -56
- package/src/models.ts +1 -1
- package/src/query-state.ts +187 -3
- package/src/tool-pairing-audit.ts +61 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-claude-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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": [
|
|
@@ -94,6 +94,41 @@
|
|
|
94
94
|
"category": "Claude Code",
|
|
95
95
|
"apply": "live"
|
|
96
96
|
},
|
|
97
|
+
{
|
|
98
|
+
"key": "fastMode",
|
|
99
|
+
"label": "Fast mode",
|
|
100
|
+
"description": "Enable Claude Code fast mode for bridge requests when the selected model supports it. Off by default.",
|
|
101
|
+
"type": "boolean",
|
|
102
|
+
"default": false,
|
|
103
|
+
"category": "Claude Code",
|
|
104
|
+
"apply": "live"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"key": "forceEffort",
|
|
108
|
+
"label": "Force Claude effort",
|
|
109
|
+
"description": "Override Pi's thinking-level mapping for all claude-bridge requests. Use max to reach Claude Code max effort even though Pi has no max thinking level. none keeps Pi's selected level.",
|
|
110
|
+
"type": "enum",
|
|
111
|
+
"enumValues": [
|
|
112
|
+
"none",
|
|
113
|
+
"low",
|
|
114
|
+
"medium",
|
|
115
|
+
"high",
|
|
116
|
+
"xhigh",
|
|
117
|
+
"max"
|
|
118
|
+
],
|
|
119
|
+
"default": "none",
|
|
120
|
+
"category": "Claude Code",
|
|
121
|
+
"apply": "live"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"key": "modelEffortOverrides",
|
|
125
|
+
"label": "Model effort overrides",
|
|
126
|
+
"description": "Optional JSON object mapping claude-bridge model ids to Claude Code effort levels, e.g. {\"claude-opus-4-8\":\"max\"}. Keys may also use claude-bridge/<id> or *.",
|
|
127
|
+
"type": "string",
|
|
128
|
+
"default": "{}",
|
|
129
|
+
"category": "Claude Code",
|
|
130
|
+
"apply": "live"
|
|
131
|
+
},
|
|
97
132
|
{
|
|
98
133
|
"key": "pathToClaudeCodeExecutable",
|
|
99
134
|
"label": "Claude executable path",
|
|
@@ -107,8 +142,8 @@
|
|
|
107
142
|
}
|
|
108
143
|
},
|
|
109
144
|
"dependencies": {
|
|
110
|
-
"@anthropic-ai/claude-agent-sdk": "0.
|
|
111
|
-
"@anthropic-ai/sdk": "
|
|
145
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.158",
|
|
146
|
+
"@anthropic-ai/sdk": "0.93.0",
|
|
112
147
|
"cc-session-io": "^0.3.1",
|
|
113
148
|
"change-case": "^5.4.4"
|
|
114
149
|
},
|
package/src/config.ts
CHANGED
|
@@ -9,12 +9,22 @@ import { dirname, join, resolve } from "path";
|
|
|
9
9
|
|
|
10
10
|
export const PACKAGE_ID = "@vanillagreen/pi-claude-bridge";
|
|
11
11
|
|
|
12
|
+
export type BridgeEffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
|
|
13
|
+
|
|
14
|
+
const VALID_EFFORT_LEVELS = new Set<BridgeEffortLevel>(["low", "medium", "high", "xhigh", "max"]);
|
|
15
|
+
|
|
12
16
|
export interface Config {
|
|
13
17
|
enabled?: boolean;
|
|
14
18
|
/** Low-level Claude Agent SDK plumbing. Most users won't need these. */
|
|
15
19
|
provider?: {
|
|
16
20
|
appendSystemPrompt?: boolean;
|
|
17
21
|
allowExtraUsage?: boolean;
|
|
22
|
+
/** Enable Claude Code fast mode for bridge requests. */
|
|
23
|
+
fastMode?: boolean;
|
|
24
|
+
/** Force this Claude Code effort level for every bridge request. */
|
|
25
|
+
forceEffort?: BridgeEffortLevel;
|
|
26
|
+
/** Per-model Claude Code effort overrides keyed by model id (e.g. claude-opus-4-8). */
|
|
27
|
+
modelEffortOverrides?: Record<string, BridgeEffortLevel>;
|
|
18
28
|
settingSources?: SettingSource[];
|
|
19
29
|
strictMcpConfig?: boolean;
|
|
20
30
|
pathToClaudeCodeExecutable?: string;
|
|
@@ -106,6 +116,53 @@ function stringFrom(raw: SettingsRecord, key: string): string | undefined {
|
|
|
106
116
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
107
117
|
}
|
|
108
118
|
|
|
119
|
+
function hasOwn(raw: SettingsRecord, key: string): boolean {
|
|
120
|
+
return Object.prototype.hasOwnProperty.call(raw, key);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeEffortLevel(value: unknown): BridgeEffortLevel | undefined {
|
|
124
|
+
if (typeof value !== "string") return undefined;
|
|
125
|
+
const normalized = value.trim().toLowerCase();
|
|
126
|
+
if (normalized === "" || normalized === "none" || normalized === "auto" || normalized === "default") return undefined;
|
|
127
|
+
return VALID_EFFORT_LEVELS.has(normalized as BridgeEffortLevel) ? normalized as BridgeEffortLevel : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function normalizeModelEffortOverrides(value: unknown): Record<string, BridgeEffortLevel> | undefined {
|
|
131
|
+
let source: unknown = value;
|
|
132
|
+
if (typeof source === "string") {
|
|
133
|
+
const trimmed = source.trim();
|
|
134
|
+
if (!trimmed || trimmed === "{}") return undefined;
|
|
135
|
+
try {
|
|
136
|
+
source = JSON.parse(trimmed);
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const record = asRecord(source);
|
|
142
|
+
if (!record) return undefined;
|
|
143
|
+
|
|
144
|
+
const out: Record<string, BridgeEffortLevel> = {};
|
|
145
|
+
for (const [modelId, rawEffort] of Object.entries(record)) {
|
|
146
|
+
const key = modelId.trim();
|
|
147
|
+
const effort = normalizeEffortLevel(rawEffort);
|
|
148
|
+
if (key && effort) out[key] = effort;
|
|
149
|
+
}
|
|
150
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeProviderConfig(provider: Config["provider"] | undefined): Config["provider"] {
|
|
154
|
+
if (!provider) return {};
|
|
155
|
+
const raw = provider as SettingsRecord;
|
|
156
|
+
const out: Config["provider"] = { ...provider };
|
|
157
|
+
const forceEffort = normalizeEffortLevel(raw.forceEffort);
|
|
158
|
+
if (forceEffort) out.forceEffort = forceEffort;
|
|
159
|
+
else delete out.forceEffort;
|
|
160
|
+
const modelEffortOverrides = normalizeModelEffortOverrides(raw.modelEffortOverrides);
|
|
161
|
+
if (modelEffortOverrides) out.modelEffortOverrides = modelEffortOverrides;
|
|
162
|
+
else delete out.modelEffortOverrides;
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
109
166
|
function managerToConfig(raw: SettingsRecord): Partial<Config> {
|
|
110
167
|
const provider: Config["provider"] = {};
|
|
111
168
|
const promptContext: Config["promptContext"] = {};
|
|
@@ -114,6 +171,14 @@ function managerToConfig(raw: SettingsRecord): Partial<Config> {
|
|
|
114
171
|
if (appendSystemPrompt !== undefined) provider.appendSystemPrompt = appendSystemPrompt;
|
|
115
172
|
const allowExtraUsage = boolFrom(raw, "allowExtraUsage");
|
|
116
173
|
if (allowExtraUsage !== undefined) provider.allowExtraUsage = allowExtraUsage;
|
|
174
|
+
const fastMode = boolFrom(raw, "fastMode");
|
|
175
|
+
if (fastMode !== undefined) provider.fastMode = fastMode;
|
|
176
|
+
if (hasOwn(raw, "forceEffort")) {
|
|
177
|
+
provider.forceEffort = normalizeEffortLevel(raw.forceEffort);
|
|
178
|
+
}
|
|
179
|
+
if (hasOwn(raw, "modelEffortOverrides")) {
|
|
180
|
+
provider.modelEffortOverrides = normalizeModelEffortOverrides(raw.modelEffortOverrides);
|
|
181
|
+
}
|
|
117
182
|
const strictMcpConfig = boolFrom(raw, "strictMcpConfig");
|
|
118
183
|
if (strictMcpConfig !== undefined) provider.strictMcpConfig = strictMcpConfig;
|
|
119
184
|
const claudePath = stringFrom(raw, "pathToClaudeCodeExecutable");
|
|
@@ -139,9 +204,10 @@ export function loadConfig(cwd: string): Config {
|
|
|
139
204
|
const global = tryParseJson(join(piUserDir(), "claude-bridge.json"));
|
|
140
205
|
const project = tryParseJson(join(cwd, ".pi", "claude-bridge.json"));
|
|
141
206
|
const manager = managerToConfig(readManagerConfig(cwd));
|
|
207
|
+
const provider = normalizeProviderConfig({ ...global.provider, ...project.provider, ...manager.provider });
|
|
142
208
|
return {
|
|
143
209
|
enabled: manager.enabled ?? project.enabled ?? global.enabled ?? true,
|
|
144
|
-
provider
|
|
210
|
+
provider,
|
|
145
211
|
promptContext: { ...global.promptContext, ...project.promptContext, ...manager.promptContext },
|
|
146
212
|
};
|
|
147
213
|
}
|
package/src/convert.ts
CHANGED
|
@@ -80,6 +80,34 @@ function assistantProvenancePrefix(msg: PiMessage): string | undefined {
|
|
|
80
80
|
return `[Prior Pi assistant response from ${provider ?? api ?? "unknown-provider"}${model ? `/${model}` : ""}]\n`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function userMessageToAnthropic(msg: PiMessage): SessionMessage {
|
|
84
|
+
if (typeof msg.content === "string") return { role: "user", content: msg.content || "[empty]" };
|
|
85
|
+
if (Array.isArray(msg.content)) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
for (const block of msg.content) {
|
|
88
|
+
if (block.type === "text" && block.text) parts.push({ type: "text", text: block.text });
|
|
89
|
+
else if (block.type === "image" && block.data && block.mimeType) parts.push(imageBlockToAnthropic(block));
|
|
90
|
+
}
|
|
91
|
+
const kept = parts.filter(Boolean) as ContentBlock[];
|
|
92
|
+
return { role: "user", content: kept.length ? kept : "[image]" };
|
|
93
|
+
}
|
|
94
|
+
return { role: "user", content: "[empty]" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toolResultToAnthropicBlock(msg: PiMessage, sanitizedIds: Map<string, string>): ContentBlock {
|
|
98
|
+
const content = toolResultContentToAnthropic(msg.content as string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>);
|
|
99
|
+
return {
|
|
100
|
+
type: "tool_result",
|
|
101
|
+
tool_use_id: sanitizeToolId((msg as { toolCallId: string }).toolCallId, sanitizedIds),
|
|
102
|
+
content: content || "",
|
|
103
|
+
is_error: (msg as { isError?: boolean }).isError,
|
|
104
|
+
} as ContentBlock;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function hasToolUse(msg: PiMessage): boolean {
|
|
108
|
+
return msg.role === "assistant" && Array.isArray(msg.content) && msg.content.some((block) => block.type === "toolCall");
|
|
109
|
+
}
|
|
110
|
+
|
|
83
111
|
/** Convert pi message array to Anthropic API format. */
|
|
84
112
|
export function convertPiMessages(
|
|
85
113
|
messages: PiMessage[],
|
|
@@ -88,22 +116,26 @@ export function convertPiMessages(
|
|
|
88
116
|
const anthropicMessages = [];
|
|
89
117
|
const sanitizedIds = new Map();
|
|
90
118
|
|
|
91
|
-
|
|
119
|
+
const pushToolResultGroup = (toolMessages: PiMessage[]): void => {
|
|
120
|
+
if (toolMessages.length === 0) return;
|
|
121
|
+
anthropicMessages.push({
|
|
122
|
+
role: "user",
|
|
123
|
+
content: toolMessages.map((toolMsg) => {
|
|
124
|
+
const content = toolResultContentToAnthropic(toolMsg.content as string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>);
|
|
125
|
+
return {
|
|
126
|
+
type: "tool_result",
|
|
127
|
+
tool_use_id: sanitizeToolId((toolMsg as { toolCallId: string }).toolCallId, sanitizedIds),
|
|
128
|
+
content: content || "",
|
|
129
|
+
is_error: (toolMsg as { isError?: boolean }).isError,
|
|
130
|
+
};
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < messages.length; i++) {
|
|
136
|
+
const msg = messages[i];
|
|
92
137
|
if (msg.role === "user") {
|
|
93
|
-
|
|
94
|
-
anthropicMessages.push({ role: "user", content: msg.content || "[empty]" });
|
|
95
|
-
} else if (Array.isArray(msg.content)) {
|
|
96
|
-
const parts = [];
|
|
97
|
-
for (const block of msg.content) {
|
|
98
|
-
if (block.type === "text" && block.text) parts.push({ type: "text", text: block.text });
|
|
99
|
-
else if (block.type === "image" && block.data && block.mimeType) {
|
|
100
|
-
parts.push(imageBlockToAnthropic(block));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
anthropicMessages.push({ role: "user", content: parts.filter(Boolean).length ? parts.filter(Boolean) as ContentBlock[] : "[image]" });
|
|
104
|
-
} else {
|
|
105
|
-
anthropicMessages.push({ role: "user", content: "[empty]" });
|
|
106
|
-
}
|
|
138
|
+
anthropicMessages.push(userMessageToAnthropic(msg));
|
|
107
139
|
} else if (msg.role === "assistant") {
|
|
108
140
|
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
109
141
|
const blocks = [];
|
|
@@ -125,12 +157,37 @@ export function convertPiMessages(
|
|
|
125
157
|
}
|
|
126
158
|
if (!blocks.length) blocks.push({ type: "text", text: "[incompatible content omitted]" });
|
|
127
159
|
anthropicMessages.push({ role: "assistant", content: blocks });
|
|
160
|
+
|
|
161
|
+
// Pi may inject steer/followUp user messages between parallel tool
|
|
162
|
+
// results, while runtime extraction treats every toolResult after the
|
|
163
|
+
// assistant (until the next assistant) as one turn. Claude history must
|
|
164
|
+
// put all tool_result blocks immediately after the tool_use assistant;
|
|
165
|
+
// replay interleaved user text only after that grouped result message.
|
|
166
|
+
if (hasToolUse(msg)) {
|
|
167
|
+
const toolMessages: PiMessage[] = [];
|
|
168
|
+
const interleavedUsers: PiMessage[] = [];
|
|
169
|
+
let j = i + 1;
|
|
170
|
+
for (; j < messages.length; j++) {
|
|
171
|
+
const next = messages[j];
|
|
172
|
+
if (next.role === "assistant") break;
|
|
173
|
+
if (next.role === "toolResult") toolMessages.push(next);
|
|
174
|
+
else if (next.role === "user") interleavedUsers.push(next);
|
|
175
|
+
else break;
|
|
176
|
+
}
|
|
177
|
+
if (toolMessages.length > 0) {
|
|
178
|
+
pushToolResultGroup(toolMessages);
|
|
179
|
+
for (const userMsg of interleavedUsers) anthropicMessages.push(userMessageToAnthropic(userMsg));
|
|
180
|
+
i = j - 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
128
183
|
} else if (msg.role === "toolResult") {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
const blocks: ContentBlock[] = [];
|
|
185
|
+
for (; i < messages.length; i++) {
|
|
186
|
+
const toolMsg = messages[i];
|
|
187
|
+
if (toolMsg.role !== "toolResult") { i--; break; }
|
|
188
|
+
blocks.push(toolResultToAnthropicBlock(toolMsg, sanitizedIds));
|
|
189
|
+
}
|
|
190
|
+
anthropicMessages.push({ role: "user", content: blocks });
|
|
134
191
|
}
|
|
135
192
|
}
|
|
136
193
|
|