@vanillagreen/pi-claude-bridge 1.1.4 → 1.2.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 +5 -0
- package/bundle/index.js +14715 -12015
- package/package.json +11 -2
- package/src/config.ts +3 -0
- package/src/index.ts +160 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-claude-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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": [
|
|
@@ -85,6 +85,15 @@
|
|
|
85
85
|
"category": "Claude Code",
|
|
86
86
|
"apply": "live"
|
|
87
87
|
},
|
|
88
|
+
{
|
|
89
|
+
"key": "allowExtraUsage",
|
|
90
|
+
"label": "Allow extra usage helper",
|
|
91
|
+
"description": "Allow claude-bridge to launch Claude Code's /extra-usage flow when rate limits require extra usage. Billing/admin approval still happens in Claude's browser page.",
|
|
92
|
+
"type": "boolean",
|
|
93
|
+
"default": false,
|
|
94
|
+
"category": "Claude Code",
|
|
95
|
+
"apply": "live"
|
|
96
|
+
},
|
|
88
97
|
{
|
|
89
98
|
"key": "pathToClaudeCodeExecutable",
|
|
90
99
|
"label": "Claude executable path",
|
|
@@ -98,7 +107,7 @@
|
|
|
98
107
|
}
|
|
99
108
|
},
|
|
100
109
|
"dependencies": {
|
|
101
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.
|
|
110
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.141",
|
|
102
111
|
"@anthropic-ai/sdk": "^0.73.0",
|
|
103
112
|
"cc-session-io": "^0.3.1",
|
|
104
113
|
"change-case": "^5.4.4"
|
package/src/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface Config {
|
|
|
14
14
|
/** Low-level Claude Agent SDK plumbing. Most users won't need these. */
|
|
15
15
|
provider?: {
|
|
16
16
|
appendSystemPrompt?: boolean;
|
|
17
|
+
allowExtraUsage?: boolean;
|
|
17
18
|
settingSources?: SettingSource[];
|
|
18
19
|
strictMcpConfig?: boolean;
|
|
19
20
|
pathToClaudeCodeExecutable?: string;
|
|
@@ -111,6 +112,8 @@ function managerToConfig(raw: SettingsRecord): Partial<Config> {
|
|
|
111
112
|
|
|
112
113
|
const appendSystemPrompt = boolFrom(raw, "appendSystemPrompt");
|
|
113
114
|
if (appendSystemPrompt !== undefined) provider.appendSystemPrompt = appendSystemPrompt;
|
|
115
|
+
const allowExtraUsage = boolFrom(raw, "allowExtraUsage");
|
|
116
|
+
if (allowExtraUsage !== undefined) provider.allowExtraUsage = allowExtraUsage;
|
|
114
117
|
const strictMcpConfig = boolFrom(raw, "strictMcpConfig");
|
|
115
118
|
if (strictMcpConfig !== undefined) provider.strictMcpConfig = strictMcpConfig;
|
|
116
119
|
const claudePath = stringFrom(raw, "pathToClaudeCodeExecutable");
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { MCP_SERVER_NAME, MCP_TOOL_PREFIX, extractSkillsBlock } from "./skills.j
|
|
|
16
16
|
import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
|
|
17
17
|
import { extractAllToolResults as _extractAllToolResults, type McpResult } from "./extract-tool-results.js";
|
|
18
18
|
import { QueryContext, ctx, stackDepth, pushContext, popContext } from "./query-state.js";
|
|
19
|
-
import { loadConfig } from "./config.js";
|
|
19
|
+
import { loadConfig, type Config } from "./config.js";
|
|
20
20
|
import { extractAgentsAppend } from "./agents-md.js";
|
|
21
21
|
import { buildPromptContextAppend } from "./prompt-context.js";
|
|
22
22
|
import { jsonSchemaToZodShape } from "./typebox-to-zod.js";
|
|
@@ -365,6 +365,7 @@ function diagDump(label: string, data: Record<string, unknown>) {
|
|
|
365
365
|
// On session_shutdown (including /reload), clearSession() resets this so a fresh
|
|
366
366
|
// registration can occur for the next session.
|
|
367
367
|
const ACTIVE_STREAM_SIMPLE_KEY = Symbol.for("claude-bridge:activeStreamSimple");
|
|
368
|
+
const COMMANDS_REGISTERED_KEY = Symbol.for("claude-bridge:commandsRegistered");
|
|
368
369
|
|
|
369
370
|
const SDK_TO_PI_TOOL_NAME: Record<string, string> = {
|
|
370
371
|
read: "read", write: "write", edit: "edit", bash: "bash",
|
|
@@ -422,6 +423,82 @@ interface SessionState {
|
|
|
422
423
|
|
|
423
424
|
let sharedSession: SessionState | null = null;
|
|
424
425
|
let extensionApi: ExtensionAPI | undefined;
|
|
426
|
+
let piUI: ExtensionUIContext | undefined;
|
|
427
|
+
let extraUsageHelperInFlight: Promise<string> | null = null;
|
|
428
|
+
|
|
429
|
+
export function isExtraUsageRequiredMessage(value: unknown): boolean {
|
|
430
|
+
let text: string;
|
|
431
|
+
if (typeof value === "string") text = value;
|
|
432
|
+
else if (value instanceof Error) text = value.message;
|
|
433
|
+
else {
|
|
434
|
+
try { text = JSON.stringify(value ?? ""); }
|
|
435
|
+
catch { text = String(value); }
|
|
436
|
+
}
|
|
437
|
+
return /extra[-\s]?usage|overage|extra usage billing|extra usage credits|1M context/i.test(text);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function extraUsageAllowed(config: Config): boolean {
|
|
441
|
+
return config.provider?.allowExtraUsage === true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function sdkTextFromMessage(message: SDKMessage): string | undefined {
|
|
445
|
+
if (message.type === "result") return (message as any).result;
|
|
446
|
+
if (message.type === "assistant") {
|
|
447
|
+
const content = (message as any).message?.content;
|
|
448
|
+
if (!Array.isArray(content)) return undefined;
|
|
449
|
+
return content
|
|
450
|
+
.map((block) => block?.type === "text" && typeof block.text === "string" ? block.text : "")
|
|
451
|
+
.filter(Boolean)
|
|
452
|
+
.join("\n");
|
|
453
|
+
}
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function runExtraUsageHelper(cwd: string, config = loadConfig(cwd)): Promise<string> {
|
|
458
|
+
const providerSettings = config.provider ?? {};
|
|
459
|
+
const claudeExecutable = resolveClaudeExecutable(providerSettings.pathToClaudeCodeExecutable);
|
|
460
|
+
if (claudeExecutable) preflightClaudeExecutable(claudeExecutable, cwd);
|
|
461
|
+
|
|
462
|
+
const helperQuery = query({
|
|
463
|
+
prompt: "/extra-usage",
|
|
464
|
+
options: {
|
|
465
|
+
cwd,
|
|
466
|
+
env: { ...process.env, ENABLE_CLAUDEAI_MCP_SERVERS: "0", DISABLE_AUTO_COMPACT: "1" },
|
|
467
|
+
maxTurns: 1,
|
|
468
|
+
...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
|
|
469
|
+
spawnClaudeCodeProcess: spawnClaudeCodeWithDiagnostics,
|
|
470
|
+
...makeCliDebugOptions("extra-usage"),
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
const outputs: string[] = [];
|
|
474
|
+
try {
|
|
475
|
+
for await (const message of helperQuery) {
|
|
476
|
+
const text = sdkTextFromMessage(message)?.trim();
|
|
477
|
+
if (text && outputs[outputs.length - 1] !== text) outputs.push(text);
|
|
478
|
+
}
|
|
479
|
+
} finally {
|
|
480
|
+
helperQuery.close();
|
|
481
|
+
}
|
|
482
|
+
return outputs.join("\n").trim() || "Claude Code /extra-usage completed.";
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function launchExtraUsageHelperIfAllowed(cwd: string, config: Config, reason: string): boolean {
|
|
486
|
+
if (!extraUsageAllowed(config)) return false;
|
|
487
|
+
if (extraUsageHelperInFlight) return true;
|
|
488
|
+
extraUsageHelperInFlight = runExtraUsageHelper(cwd, config)
|
|
489
|
+
.then((message) => {
|
|
490
|
+
piUI?.notify(`Claude extra usage helper: ${message}`, "info");
|
|
491
|
+
return message;
|
|
492
|
+
})
|
|
493
|
+
.catch((error) => {
|
|
494
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
495
|
+
piUI?.notify(`Claude extra usage helper failed after ${reason}: ${message}`, "error");
|
|
496
|
+
throw error;
|
|
497
|
+
})
|
|
498
|
+
.finally(() => { extraUsageHelperInFlight = null; });
|
|
499
|
+
void extraUsageHelperInFlight.catch(() => {});
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
425
502
|
|
|
426
503
|
const BRIDGE_SESSION_CUSTOM_TYPE = "claude-bridge-session";
|
|
427
504
|
|
|
@@ -851,9 +928,6 @@ function mapToolArgs(
|
|
|
851
928
|
// them without activating the extension. `ctx()`, `pushContext()`, `popContext()`
|
|
852
929
|
// are imported at the top of this file.
|
|
853
930
|
|
|
854
|
-
// Global (not query state):
|
|
855
|
-
let piUI: ExtensionUIContext | null = null;
|
|
856
|
-
|
|
857
931
|
function resolveMcpTools(context: Context, excludeToolName?: string): {
|
|
858
932
|
mcpTools: Tool[];
|
|
859
933
|
customToolNameToSdk: Map<string, string>;
|
|
@@ -1156,6 +1230,8 @@ async function consumeQuery(
|
|
|
1156
1230
|
sdkQuery: ReturnType<typeof query>,
|
|
1157
1231
|
customToolNameToPi: Map<string, string>,
|
|
1158
1232
|
model: Model<any>,
|
|
1233
|
+
cwd: string,
|
|
1234
|
+
bridgeConfig: Config,
|
|
1159
1235
|
wasAborted: () => boolean,
|
|
1160
1236
|
): Promise<{ capturedSessionId?: string }> {
|
|
1161
1237
|
let capturedSessionId: string | undefined;
|
|
@@ -1180,6 +1256,14 @@ async function consumeQuery(
|
|
|
1180
1256
|
ctx().currentPiStream?.push({ type: "text_start", contentIndex: idx, partial: ctx().turnOutput });
|
|
1181
1257
|
ctx().currentPiStream?.push({ type: "text_delta", contentIndex: idx, delta: text, partial: ctx().turnOutput });
|
|
1182
1258
|
ctx().currentPiStream?.push({ type: "text_end", contentIndex: idx, content: text, partial: ctx().turnOutput });
|
|
1259
|
+
} else if (message.subtype !== "success" && isExtraUsageRequiredMessage(message)) {
|
|
1260
|
+
const errors = Array.isArray((message as any).errors) ? (message as any).errors.join("\n") : message.subtype;
|
|
1261
|
+
const openedExtraUsage = launchExtraUsageHelperIfAllowed(cwd, bridgeConfig, "result error");
|
|
1262
|
+
ctx().turnOutput.stopReason = "error";
|
|
1263
|
+
ctx().turnOutput.errorMessage = `${errors}${openedExtraUsage ? "\n\nOpened Claude Code /extra-usage helper. Complete billing/admin flow in the browser, then retry the prompt." : "\n\nRun /claude-bridge:extra, or enable Allow extra usage helper in settings."}`;
|
|
1264
|
+
ctx().currentPiStream?.push({ type: "error", reason: "error", error: ctx().turnOutput });
|
|
1265
|
+
ctx().currentPiStream?.end();
|
|
1266
|
+
ctx().currentPiStream = null;
|
|
1183
1267
|
}
|
|
1184
1268
|
break;
|
|
1185
1269
|
case "system":
|
|
@@ -1194,7 +1278,9 @@ async function consumeQuery(
|
|
|
1194
1278
|
debug("consumeQuery: rate_limit_event", JSON.stringify(info).slice(0, 300));
|
|
1195
1279
|
if (info?.status === "rejected") {
|
|
1196
1280
|
const resetsAt = info.resetsAt ? new Date(info.resetsAt).toLocaleTimeString() : "unknown";
|
|
1197
|
-
|
|
1281
|
+
const reason = `${info.rateLimitType ?? "unknown"} rate limit`;
|
|
1282
|
+
const launchedExtraUsage = isExtraUsageRequiredMessage(info) && launchExtraUsageHelperIfAllowed(cwd, bridgeConfig, reason);
|
|
1283
|
+
piUI?.notify(`Claude rate limited (${reason}) — resets at ${resetsAt}${launchedExtraUsage ? "; opened /extra-usage helper" : ""}`, "warning");
|
|
1198
1284
|
} else if (info?.status === "allowed_warning") {
|
|
1199
1285
|
piUI?.notify(`Claude rate limit warning: ${Math.round(info.utilization ?? 0)}% used (${info.rateLimitType ?? ""})`, "warning");
|
|
1200
1286
|
}
|
|
@@ -1432,7 +1518,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1432
1518
|
}
|
|
1433
1519
|
|
|
1434
1520
|
// Background consumer — runs until query ends
|
|
1435
|
-
consumeQuery(sdkQuery, customToolNameToPi, model, () => wasAborted)
|
|
1521
|
+
consumeQuery(sdkQuery, customToolNameToPi, model, cwd, bridgeConfig, () => wasAborted)
|
|
1436
1522
|
.then(async ({ capturedSessionId }) => {
|
|
1437
1523
|
debug(`provider: consumeQuery completed, stopReason=${ctx().turnOutput?.stopReason}, error=${ctx().turnOutput?.errorMessage}, aborted=${wasAborted}`);
|
|
1438
1524
|
|
|
@@ -1481,7 +1567,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1481
1567
|
debug(`provider: continuation query, model=${model.id}, resume=${resumeId.slice(0, 8)}, prompt=${steerPrompt.slice(0, 60)}`);
|
|
1482
1568
|
|
|
1483
1569
|
try {
|
|
1484
|
-
const { capturedSessionId: contSid } = await consumeQuery(contQuery, customToolNameToPi, model, () => wasAborted);
|
|
1570
|
+
const { capturedSessionId: contSid } = await consumeQuery(contQuery, customToolNameToPi, model, cwd, bridgeConfig, () => wasAborted);
|
|
1485
1571
|
const sid = contSid ?? sharedSession?.sessionId;
|
|
1486
1572
|
if (sid) {
|
|
1487
1573
|
sharedSession = { sessionId: sid, cursor: sharedSession?.cursor ?? 0, cwd };
|
|
@@ -1502,6 +1588,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1502
1588
|
})
|
|
1503
1589
|
.catch((error) => {
|
|
1504
1590
|
debug(`provider: query error, model=${model.id}, aborted=${Boolean(options?.signal?.aborted)}, error=`, error);
|
|
1591
|
+
const openedExtraUsage = isExtraUsageRequiredMessage(error) && launchExtraUsageHelperIfAllowed(cwd, bridgeConfig, "query error");
|
|
1505
1592
|
if ((wasAborted || options?.signal?.aborted) && sharedSession) {
|
|
1506
1593
|
sharedSession = { ...sharedSession, needsRebuild: true, forceRotate: true };
|
|
1507
1594
|
} else {
|
|
@@ -1510,7 +1597,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1510
1597
|
ctx().deferredUserMessages = [];
|
|
1511
1598
|
if (ctx().turnOutput) {
|
|
1512
1599
|
ctx().turnOutput.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
1513
|
-
ctx().turnOutput.errorMessage = error instanceof Error ? error.message : String(error)
|
|
1600
|
+
ctx().turnOutput.errorMessage = `${error instanceof Error ? error.message : String(error)}${openedExtraUsage ? "\n\nOpened Claude Code /extra-usage helper. Complete billing/admin flow in the browser, then retry the prompt." : ""}`;
|
|
1514
1601
|
}
|
|
1515
1602
|
ctx().currentPiStream?.push({ type: "error", reason: (ctx().turnOutput?.stopReason ?? "error") as "aborted" | "error", error: ctx().turnOutput! });
|
|
1516
1603
|
ctx().currentPiStream?.end();
|
|
@@ -1536,6 +1623,70 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1536
1623
|
return stream;
|
|
1537
1624
|
}
|
|
1538
1625
|
|
|
1626
|
+
function commandCwd(ctx: unknown): string {
|
|
1627
|
+
const value = (ctx as { cwd?: unknown })?.cwd;
|
|
1628
|
+
return typeof value === "string" && value.length > 0 ? value : process.cwd();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
async function tryOpenExtensionManagerSettings(ctx: { ui: ExtensionUIContext }): Promise<boolean> {
|
|
1632
|
+
const host = globalThis as unknown as Record<PropertyKey, unknown>;
|
|
1633
|
+
const openQuickSettings = host[Symbol.for("vstack.pi.extension-manager.open-quick-settings")];
|
|
1634
|
+
if (typeof openQuickSettings !== "function") return false;
|
|
1635
|
+
try {
|
|
1636
|
+
await (openQuickSettings as (ctx: unknown, hint?: string) => Promise<void>)(ctx, "@vanillagreen/pi-claude-bridge");
|
|
1637
|
+
return true;
|
|
1638
|
+
} catch {
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function showBridgeStatus(ctx: { ui: ExtensionUIContext; cwd?: string }): void {
|
|
1644
|
+
const config = loadConfig(commandCwd(ctx));
|
|
1645
|
+
ctx.ui.notify([
|
|
1646
|
+
`Claude bridge: ${config.enabled === false ? "disabled" : "enabled"}`,
|
|
1647
|
+
`Extra usage auto-helper: ${extraUsageAllowed(config) ? "on" : "off"} (settings)`,
|
|
1648
|
+
`Use /claude-bridge:extra to run Claude Code /extra-usage now.`,
|
|
1649
|
+
].join("\n"), "info");
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function registerBridgeCommands(pi: ExtensionAPI): void {
|
|
1653
|
+
const guard = pi as unknown as Record<PropertyKey, unknown>;
|
|
1654
|
+
if (guard[COMMANDS_REGISTERED_KEY]) return;
|
|
1655
|
+
guard[COMMANDS_REGISTERED_KEY] = true;
|
|
1656
|
+
|
|
1657
|
+
const runExtraUsage = async (ctx: { ui: ExtensionUIContext; cwd?: string }) => {
|
|
1658
|
+
const cwd = commandCwd(ctx);
|
|
1659
|
+
if (extraUsageHelperInFlight) {
|
|
1660
|
+
ctx.ui.notify("Claude extra usage helper already running.", "info");
|
|
1661
|
+
await extraUsageHelperInFlight.catch(() => undefined);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
ctx.ui.notify("Claude extra usage helper starting…", "info");
|
|
1666
|
+
extraUsageHelperInFlight = runExtraUsageHelper(cwd)
|
|
1667
|
+
.finally(() => { extraUsageHelperInFlight = null; });
|
|
1668
|
+
const message = await extraUsageHelperInFlight;
|
|
1669
|
+
ctx.ui.notify(`Claude extra usage helper: ${message}`, "info");
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1672
|
+
ctx.ui.notify(`Claude extra usage helper failed: ${message}`, "error");
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
pi.registerCommand("claude-bridge", {
|
|
1677
|
+
description: "Open Claude bridge settings/status",
|
|
1678
|
+
handler: async (args: string, ctx) => {
|
|
1679
|
+
if (args.trim()) ctx.ui.notify("Unknown /claude-bridge argument. Use /claude-bridge:extra to run Claude Code /extra-usage.", "warning");
|
|
1680
|
+
if (await tryOpenExtensionManagerSettings(ctx)) return;
|
|
1681
|
+
showBridgeStatus(ctx);
|
|
1682
|
+
},
|
|
1683
|
+
});
|
|
1684
|
+
pi.registerCommand("claude-bridge:extra", {
|
|
1685
|
+
description: "Run Claude Code /extra-usage through claude-bridge",
|
|
1686
|
+
handler: async (_args: string, ctx) => runExtraUsage(ctx),
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1539
1690
|
// --- Extension registration ---
|
|
1540
1691
|
|
|
1541
1692
|
export default function (pi: ExtensionAPI) {
|
|
@@ -1545,6 +1696,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1545
1696
|
|
|
1546
1697
|
const config = loadConfig(process.cwd());
|
|
1547
1698
|
debug("loadConfig:", JSON.stringify(config));
|
|
1699
|
+
registerBridgeCommands(pi);
|
|
1548
1700
|
if (config.enabled === false) {
|
|
1549
1701
|
debug("provider: disabled by configuration");
|
|
1550
1702
|
return;
|