@vanillagreen/pi-claude-bridge 2.0.0 → 4.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/src/models.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  // Canonical selection + display order for the model picker.
2
- // `resolveModelId` returns the first partial match, so `opus` resolves to the first-listed opus entry.
3
2
  // Extracted from index.ts so tests can import without activating the extension.
4
3
 
5
4
  export const FABLE_MODEL_ID = "claude-fable-5";
@@ -94,9 +93,3 @@ export function buildModels<T extends { id: string; [key: string]: any }>(piAiMo
94
93
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
95
94
  }));
96
95
  }
97
-
98
- export function resolveModelId(models: Array<{ id: string }>, input: string): string {
99
- const lower = input.toLowerCase();
100
- const match = models.find((m) => m.id === lower || m.id.includes(lower));
101
- return match ? match.id : input;
102
- }
@@ -56,11 +56,16 @@ export function buildNativeProvider(
56
56
  models: Array<Record<string, unknown>>,
57
57
  streamSimple: (...args: unknown[]) => unknown,
58
58
  env: NodeJS.ProcessEnv = process.env,
59
+ // Availability probe. Defaults to direct credential presence; the extension
60
+ // passes a probe that also accepts a companion account-router pool.
61
+ hasCredentials: () => boolean = () => hasClaudeCredentials(env),
59
62
  ): unknown {
60
63
  if (!supportsNativeProvider(piAi)) throw new Error(NATIVE_PROVIDER_UNSUPPORTED_MESSAGE);
61
64
  // The legacy config path stamped provider/api/baseUrl onto each model during
62
65
  // composition; createProvider passes models through verbatim, so stamp here.
63
- const stamped = models.map((model) => ({ api: "claude-bridge", baseUrl: "claude-bridge", provider: PROVIDER_ID, ...model }));
66
+ // Stamps win over any provider field the source model carries the models
67
+ // come from pi-ai's anthropic registry and must be re-homed under pi-claude.
68
+ const stamped = models.map((model) => ({ ...model, api: "claude-bridge", baseUrl: "claude-bridge", provider: PROVIDER_ID }));
64
69
  // The Claude Code subprocess router IS the implementation for both stream
65
70
  // entry points — there is no raw-API shape to dispatch to.
66
71
  const streams = {
@@ -69,7 +74,7 @@ export function buildNativeProvider(
69
74
  };
70
75
  return (piAi as { createProvider: (input: unknown) => unknown }).createProvider({
71
76
  id: PROVIDER_ID,
72
- name: "Claude (Claude Code)",
77
+ name: "Pi Claude",
73
78
  baseUrl: "claude-bridge",
74
79
  auth: {
75
80
  apiKey: {
@@ -77,8 +82,8 @@ export function buildNativeProvider(
77
82
  // check() exists so pi's availability pass never has to call
78
83
  // resolve(): both are existence-only, but check is the documented
79
84
  // side-effect-free probe.
80
- check: async () => (hasClaudeCredentials(env) ? { type: "api_key" as const, source: claudeAuthSourceLabel(env) } : undefined),
81
- resolve: async () => (hasClaudeCredentials(env)
85
+ check: async () => (hasCredentials() ? { type: "api_key" as const, source: claudeAuthSourceLabel(env) } : undefined),
86
+ resolve: async () => (hasCredentials()
82
87
  ? { auth: { apiKey: "not-used" }, source: claudeAuthSourceLabel(env) }
83
88
  : undefined),
84
89
  },
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
2
  import { dirname, join, resolve } from "path";
3
3
  import { isolatedFromEnv, piUserDir } from "./config.js";
4
+ import { debug } from "./debug.js";
4
5
 
5
6
  export interface PromptContextSettings {
6
7
  includeAppendSystemPromptMd?: boolean;
@@ -19,7 +20,10 @@ function readTrimmed(path: string): string | undefined {
19
20
  if (!existsSync(path)) return undefined;
20
21
  const content = readFileSync(path, "utf8").trim();
21
22
  return content.length > 0 ? content : undefined;
22
- } catch {
23
+ } catch (error) {
24
+ // The file exists but could not be read: the user opted into forwarding
25
+ // it, so a silent drop looks like the setting being ignored.
26
+ debug(`prompt-context: failed to read ${path}:`, error instanceof Error ? error.message : String(error));
23
27
  return undefined;
24
28
  }
25
29
  }
@@ -0,0 +1,183 @@
1
+ // Pure assembly of the Claude Agent SDK query options for one bridge query.
2
+ // Extracted from index.ts (pure move): no closures — reads config, env, and
3
+ // the provided context only.
4
+
5
+ import { type Model } from "@earendil-works/pi-ai";
6
+ import { createSdkMcpServer, type query, type EffortLevel, type SettingSource } from "@anthropic-ai/claude-agent-sdk";
7
+ import { accountSessionScope, subscriberProfileEnv, type ClaudeAccountRoute } from "./account-router.js";
8
+ import { extractAgentsAppend } from "./agents-md.js";
9
+ import { spawnClaudeCodeWithDiagnostics } from "./claude-executable.js";
10
+ import { normalizeEffortLevel, type Config } from "./config.js";
11
+ import { connectorQueryOptions, connectorWriteModeFor, connectorsEnabledFor, settingSourcesForQuery } from "./connectors.js";
12
+ import { connectorServersSnapshot } from "./connector-runtime.js";
13
+ import { PROVIDER_ID } from "./convert.js";
14
+ import { makeCliDebugOptions } from "./debug.js";
15
+ import { FABLE_MODEL_ID, fallbackModelForPrimaryModel } from "./models.js";
16
+ import { buildPromptContextAppend } from "./prompt-context.js";
17
+ import { extractSkillsBlock } from "./skills.js";
18
+
19
+ // --- Effort level mapping ---
20
+ // Pi reasoning levels → CC SDK effort levels
21
+
22
+ const REASONING_TO_EFFORT: Record<string, EffortLevel> = {
23
+ minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "max", max: "max",
24
+ };
25
+
26
+ function normalizeEffortOverrideModelKey(value: string): string {
27
+ const key = value.trim().toLowerCase();
28
+ return key.startsWith(`${PROVIDER_ID}/`) ? key.slice(PROVIDER_ID.length + 1) : key;
29
+ }
30
+
31
+ export function resolveConfiguredEffort(
32
+ modelId: string,
33
+ reasoningEffort: EffortLevel | undefined,
34
+ providerConfig?: Config["provider"],
35
+ ): EffortLevel | undefined {
36
+ const target = normalizeEffortOverrideModelKey(modelId);
37
+ for (const [key, rawEffort] of Object.entries(providerConfig?.modelEffortOverrides ?? {})) {
38
+ const normalizedKey = normalizeEffortOverrideModelKey(key);
39
+ if (normalizedKey !== "*" && normalizedKey !== target) continue;
40
+ const effort = normalizeEffortLevel(rawEffort) as EffortLevel | undefined;
41
+ if (effort) return effort;
42
+ }
43
+ return (normalizeEffortLevel(providerConfig?.forceEffort) as EffortLevel | undefined) ?? reasoningEffort;
44
+ }
45
+
46
+ export interface BuildClaudeQueryOptionsInput {
47
+ cwd: string;
48
+ /** The model Pi requested. */
49
+ requestedModel: Model<any>;
50
+ /** The model this attempt actually runs (router may substitute). */
51
+ queryModel: Model<any>;
52
+ account?: ClaudeAccountRoute;
53
+ bridgeConfig: Config;
54
+ systemPrompt?: string;
55
+ /** Pi reasoning level from the stream options, if any. */
56
+ reasoning?: string;
57
+ resumeSessionId: string | null;
58
+ mcpServers?: Record<string, ReturnType<typeof createSdkMcpServer>>;
59
+ claudeExecutable?: string;
60
+ }
61
+
62
+ export interface BuiltClaudeQueryOptions {
63
+ queryOptions: NonNullable<Parameters<typeof query>[0]["options"]>;
64
+ // Diagnostics-ish bits the caller's debug line reports.
65
+ enableCloudMcp: boolean;
66
+ appendSystemPrompt: boolean;
67
+ promptContextLabels: string[];
68
+ strictMcpConfigEnabled: boolean;
69
+ effort?: EffortLevel;
70
+ fallbackModel?: string;
71
+ }
72
+
73
+ export function buildClaudeQueryOptions(input: BuildClaudeQueryOptionsInput): BuiltClaudeQueryOptions {
74
+ const { cwd, requestedModel, queryModel, account, bridgeConfig, systemPrompt, reasoning, resumeSessionId, mcpServers, claudeExecutable } = input;
75
+ const providerSettings = bridgeConfig.provider ?? {};
76
+ const accountScope = accountSessionScope(account);
77
+ // Whether to expose the Claude account's claude.ai cloud MCP connectors
78
+ // (Gmail/Calendar/Drive). Enabled via env or config; drives setting-sources,
79
+ // tool isolation, and the ENABLE_CLAUDEAI_MCP_SERVERS child-env gate below.
80
+ const enableCloudMcp = connectorsEnabledFor(bridgeConfig);
81
+ // Connector WRITE control: read-only by default (writes denied); the one-shot
82
+ // approved-write executor sets CLAUDE_BRIDGE_CONNECTOR_WRITE=allow / config.
83
+ const connectorWriteMode = connectorWriteModeFor(bridgeConfig);
84
+ // Declare the account's connected connectors explicitly so `alwaysLoad` can
85
+ // hold startup until they attach — otherwise the turn-1 manifest is built
86
+ // before the CLI has fetched them (kendex#832).
87
+ const connectorServers = enableCloudMcp ? connectorServersSnapshot(accountScope.claudeConfigDir) : {};
88
+ const appendSystemPrompt = providerSettings.appendSystemPrompt !== false;
89
+ const agentsAppend = appendSystemPrompt ? extractAgentsAppend() : undefined;
90
+ const skillsAppend = appendSystemPrompt ? extractSkillsBlock(systemPrompt) : undefined;
91
+ const promptContextAppend = buildPromptContextAppend(systemPrompt, cwd, bridgeConfig.promptContext ?? {});
92
+ const appendParts = [agentsAppend, skillsAppend, promptContextAppend.text].filter((part): part is string => Boolean(part));
93
+ const systemPromptAppend = appendParts.length > 0 ? appendParts.join("\n\n") : undefined;
94
+
95
+ // MCP auto-loading suppression: with appendSystemPrompt=true (default), the
96
+ // SDK uses isolation mode and avoids filesystem settings. If users turn that
97
+ // off, load user/project settings but pass --strict-mcp-config so Claude Code
98
+ // ignores auto-discovered filesystem MCP servers while Pi owns tool execution.
99
+ // Connectors mode needs settings resolution ON but restricted to USER scope
100
+ // only — project/local settings files can smuggle `env`/`apiKeyHelper` from
101
+ // a hostile checkout (kendex#990). Full rationale on settingSourcesForQuery.
102
+ const settingSources: SettingSource[] | undefined = settingSourcesForQuery(
103
+ enableCloudMcp, appendSystemPrompt, providerSettings.settingSources);
104
+ const strictMcpConfigEnabled = !appendSystemPrompt && providerSettings.strictMcpConfig !== false;
105
+ // Prefer the model's own thinkingLevelMap when present (pi-ai 0.72+ ships
106
+ // per-model overrides — e.g. opus-4-7 wants xhigh→xhigh, not xhigh→max).
107
+ // Fall back to our generic table for older pi-ai or unmapped levels.
108
+ const requestedEffort = reasoning
109
+ ? ((queryModel as any).thinkingLevelMap?.[reasoning] as EffortLevel | undefined)
110
+ ?? REASONING_TO_EFFORT[reasoning]
111
+ : undefined;
112
+ const effort = resolveConfiguredEffort(queryModel.id, requestedEffort, providerSettings);
113
+
114
+ const extraArgs: Record<string, string | null> = {};
115
+ // Opus 4.7 defaults thinking.display to "omitted" (empty thinking text in stream).
116
+ // Force summarized so thinking_delta events arrive. See anthropics/claude-agent-sdk-python#830.
117
+ // Deliberately the raw flag, NOT the typed `thinking` option: every non-disabled
118
+ // ThinkingConfig also emits `--thinking adaptive` or `--max-thinking-tokens`
119
+ // (verified in sdk.mjs flag mapping), so the typed form cannot set display
120
+ // without overriding the model's thinking mode alongside our `--effort`.
121
+ if (effort) extraArgs["thinking-display"] = "summarized";
122
+ // With a managed Fable pool, let every account's model-scoped allowance run
123
+ // out (rotation) before changing models — the CLI's own Opus fallback would
124
+ // silently skip accounts whose Fable quota is still available. Once the
125
+ // router explicitly selects Opus, its normal Opus→4.8 safety fallback is
126
+ // back on.
127
+ const fallbackModel = account && requestedModel.id === FABLE_MODEL_ID && queryModel.id === requestedModel.id
128
+ ? undefined
129
+ : fallbackModelForPrimaryModel(queryModel.id);
130
+
131
+ // Suppress claude.ai cloud MCP servers (Figma/Canva/etc. auto-discovered via OAuth
132
+ // when the user is logged into Anthropic). These are a separate code path from
133
+ // filesystem MCP and are NOT blocked by --strict-mcp-config or settingSources=undefined.
134
+ // The native CC binary gates them on env var ENABLE_CLAUDEAI_MCP_SERVERS: setting it
135
+ // to "0"/"false"/"no"/"off" makes the loader return early before any cloud fetch.
136
+ // DISABLE_AUTO_COMPACT=1: pi owns context-management and propagates its own
137
+ // /compact via session_compact (see handler in the extension entry). Letting CC
138
+ // also autocompact would double-flush the prompt cache and races pi's
139
+ // threshold with CC's, including CC's anti-thrashing guard (issue #8).
140
+ // Manual /compact in CC still works (we never invoke it).
141
+ // When connectors are enabled, allow claude.ai cloud MCP servers so the
142
+ // authenticated account's Gmail/Calendar/Drive tools load. Default stays "0".
143
+ const childEnv = {
144
+ ...(account ? subscriberProfileEnv(account) : process.env),
145
+ ENABLE_CLAUDEAI_MCP_SERVERS: enableCloudMcp ? "1" : "0",
146
+ DISABLE_AUTO_COMPACT: "1",
147
+ };
148
+ const queryOptions: NonNullable<Parameters<typeof query>[0]["options"]> = {
149
+ cwd,
150
+ model: queryModel.id,
151
+ env: childEnv,
152
+ ...connectorQueryOptions(enableCloudMcp, connectorWriteMode),
153
+ permissionMode: "bypassPermissions",
154
+ includePartialMessages: true,
155
+ ...(fallbackModel ? { fallbackModel } : {}),
156
+ ...(providerSettings.fastMode ? { settings: { fastMode: true } } : {}),
157
+ systemPrompt: {
158
+ type: "preset", preset: "claude_code",
159
+ append: systemPromptAppend ? systemPromptAppend : undefined,
160
+ },
161
+ extraArgs,
162
+ ...(strictMcpConfigEnabled ? { strictMcpConfig: true } : {}),
163
+ ...(effort ? { effort } : {}),
164
+ ...(settingSources ? { settingSources } : {}),
165
+ ...(mcpServers || Object.keys(connectorServers).length > 0
166
+ ? { mcpServers: { ...(mcpServers ?? {}), ...connectorServers } as NonNullable<Parameters<typeof query>[0]["options"]>["mcpServers"] }
167
+ : {}),
168
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
169
+ ...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
170
+ spawnClaudeCodeProcess: spawnClaudeCodeWithDiagnostics,
171
+ ...makeCliDebugOptions("provider"),
172
+ };
173
+
174
+ return {
175
+ queryOptions,
176
+ enableCloudMcp,
177
+ appendSystemPrompt,
178
+ promptContextLabels: promptContextAppend.labels,
179
+ strictMcpConfigEnabled,
180
+ ...(effort ? { effort } : {}),
181
+ ...(fallbackModel ? { fallbackModel } : {}),
182
+ };
183
+ }