@centerforagenticai/pi-multi-account 0.1.1
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/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +999 -0
- package/config/models/pi-multi-account.v1.json +32 -0
- package/config/subscription-plans.v1.json +122 -0
- package/package.json +76 -0
- package/packages/pi-anthropic-oauth/LICENSE +21 -0
- package/packages/pi-anthropic-oauth/package.json +54 -0
- package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
- package/packages/pi-anthropic-oauth/src/context.ts +116 -0
- package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
- package/packages/pi-anthropic-oauth/src/index.ts +37 -0
- package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
- package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
- package/packages/pi-antigravity/LICENSE +21 -0
- package/packages/pi-antigravity/package.json +77 -0
- package/packages/pi-antigravity/src/auth/index.ts +14 -0
- package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
- package/packages/pi-antigravity/src/client/client.ts +561 -0
- package/packages/pi-antigravity/src/client/index.ts +1 -0
- package/packages/pi-antigravity/src/context.ts +110 -0
- package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/packages/pi-antigravity/src/image/image.ts +336 -0
- package/packages/pi-antigravity/src/image/index.ts +1 -0
- package/packages/pi-antigravity/src/index.ts +280 -0
- package/packages/pi-antigravity/src/models/discovery.ts +154 -0
- package/packages/pi-antigravity/src/models/grouping.ts +424 -0
- package/packages/pi-antigravity/src/models/index.ts +3 -0
- package/packages/pi-antigravity/src/models/models.ts +500 -0
- package/packages/pi-antigravity/src/stream/index.ts +1 -0
- package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
- package/packages/pi-antigravity/src/types/enums.ts +42 -0
- package/packages/pi-antigravity/src/types/index.ts +2 -0
- package/packages/pi-antigravity/src/types/types.ts +292 -0
- package/packages/pi-antigravity/src/usage/index.ts +1 -0
- package/packages/pi-antigravity/src/usage/usage.ts +416 -0
- package/packages/pi-antigravity/src/utils/http.ts +91 -0
- package/packages/pi-antigravity/src/utils/index.ts +3 -0
- package/packages/pi-antigravity/src/utils/security.ts +73 -0
- package/packages/pi-antigravity/src/utils/util.ts +132 -0
- package/scripts/multi-account.mjs +44 -0
- package/src/account-labels.ts +223 -0
- package/src/account-plan-assignment.ts +340 -0
- package/src/account-rate-history.ts +372 -0
- package/src/anthropic-adaptive-stream.ts +531 -0
- package/src/anthropic-alias-stream.ts +140 -0
- package/src/anthropic-context-compat.ts +80 -0
- package/src/api-pricing.ts +579 -0
- package/src/bounded-file-lines.ts +97 -0
- package/src/catalog-rebinding.ts +177 -0
- package/src/catalog-registration-probe.ts +111 -0
- package/src/codex-adapter.ts +345 -0
- package/src/codex-model-defaults.ts +785 -0
- package/src/command-completions.ts +404 -0
- package/src/commands.ts +2000 -0
- package/src/compaction.ts +14 -0
- package/src/config.ts +1317 -0
- package/src/continuation.ts +569 -0
- package/src/cooldowns.ts +110 -0
- package/src/cost-digest-store.ts +332 -0
- package/src/cost-digest.ts +1044 -0
- package/src/cost-history.ts +251 -0
- package/src/cost-period-closer.ts +160 -0
- package/src/cost-report-json.ts +318 -0
- package/src/cost-report-reader.ts +368 -0
- package/src/cost-report-render.ts +207 -0
- package/src/cost-report.ts +1104 -0
- package/src/coverage-attestation.ts +397 -0
- package/src/credential-lifecycle.ts +169 -0
- package/src/credential-refresh.ts +248 -0
- package/src/declaration-notice-marker.ts +238 -0
- package/src/diagnostic-store.ts +276 -0
- package/src/diagnostics.ts +309 -0
- package/src/discovery.ts +471 -0
- package/src/duration.ts +13 -0
- package/src/error-classification.ts +256 -0
- package/src/fuzzy.ts +15 -0
- package/src/group-policy.ts +81 -0
- package/src/history-store.ts +897 -0
- package/src/index.ts +5572 -0
- package/src/lifecycle.ts +378 -0
- package/src/logical-dispatch.ts +279 -0
- package/src/logical-model-selector.ts +254 -0
- package/src/logical-model-switcher.ts +430 -0
- package/src/logical-provider-attribution.ts +544 -0
- package/src/logical-provider.ts +1237 -0
- package/src/logical-route-indicator.ts +215 -0
- package/src/machine-lease.ts +445 -0
- package/src/model-support.ts +66 -0
- package/src/models-declaration.ts +1091 -0
- package/src/openai-adapter.ts +117 -0
- package/src/openrouter-budget.ts +304 -0
- package/src/openrouter-fallback.ts +146 -0
- package/src/period-boundaries.ts +376 -0
- package/src/pi-anthropic-oauth.d.ts +6 -0
- package/src/preflight.ts +253 -0
- package/src/pricing-cache.ts +235 -0
- package/src/project-identity.ts +100 -0
- package/src/provider-registration.ts +942 -0
- package/src/rate-formula.ts +163 -0
- package/src/recovery-engine.ts +853 -0
- package/src/recovery-output.ts +837 -0
- package/src/recovery-plan.ts +239 -0
- package/src/report-range.ts +203 -0
- package/src/route-resolver.ts +789 -0
- package/src/routing-config-transaction.ts +232 -0
- package/src/routing.ts +1163 -0
- package/src/runtime-state.ts +630 -0
- package/src/session-account-groups.ts +284 -0
- package/src/session-restore.ts +287 -0
- package/src/shared-usage.ts +1392 -0
- package/src/standalone-cli.ts +720 -0
- package/src/status-view.ts +578 -0
- package/src/subscription-plan-catalog.ts +346 -0
- package/src/tier-model-resolver.ts +46 -0
- package/src/upstream-anthropic.ts +315 -0
- package/src/upstream-antigravity.ts +327 -0
- package/src/usage-fetch.ts +1634 -0
- package/src/usage.ts +1026 -0
- package/src/vendor.ts +87 -0
- package/src/warmer.ts +231 -0
- package/src/watchdog.ts +219 -0
- package/src/window-history.ts +270 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import type { ContentBlockParam, MessageParam, ToolUnion } from "@anthropic-ai/sdk/resources/messages.js";
|
|
2
|
+
|
|
3
|
+
type ToolResultContentBlock =
|
|
4
|
+
| { type: "text"; text: string }
|
|
5
|
+
| {
|
|
6
|
+
type: "image";
|
|
7
|
+
source: {
|
|
8
|
+
type: "base64";
|
|
9
|
+
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
10
|
+
data: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
import type {
|
|
14
|
+
Api,
|
|
15
|
+
ImageContent,
|
|
16
|
+
Message,
|
|
17
|
+
Model,
|
|
18
|
+
TextContent,
|
|
19
|
+
ThinkingContent,
|
|
20
|
+
Tool,
|
|
21
|
+
ToolCall,
|
|
22
|
+
ToolResultMessage,
|
|
23
|
+
} from "@earendil-works/pi-ai";
|
|
24
|
+
import { sanitizeSurrogates } from "./prompt.js";
|
|
25
|
+
|
|
26
|
+
export type IndexedBlock =
|
|
27
|
+
| (TextContent & { index: number })
|
|
28
|
+
| (ThinkingContent & { index: number; thinkingSignature?: string })
|
|
29
|
+
| (ToolCall & { index: number; partialJson: string });
|
|
30
|
+
|
|
31
|
+
const claudeCodeTools = [
|
|
32
|
+
"Read",
|
|
33
|
+
"Write",
|
|
34
|
+
"Edit",
|
|
35
|
+
"Bash",
|
|
36
|
+
"Grep",
|
|
37
|
+
"Glob",
|
|
38
|
+
"AskUserQuestion",
|
|
39
|
+
"TodoWrite",
|
|
40
|
+
"WebFetch",
|
|
41
|
+
"WebSearch",
|
|
42
|
+
] as const;
|
|
43
|
+
const claudeCodeToolLookup = new Map(claudeCodeTools.map((name) => [name.toLowerCase(), name]));
|
|
44
|
+
|
|
45
|
+
// Anthropic's OAuth classifier rejects tool definitions with names outside
|
|
46
|
+
// Claude Code's own tool set with a 400 "Third-party apps now draw from your
|
|
47
|
+
// extra usage" error. Claude Code exposes MCP tools as `mcp__<server>__<tool>`,
|
|
48
|
+
// so unknown tools are namespaced under `mcp__pi__`.
|
|
49
|
+
const CLAUDE_CODE_MCP_TOOL_PREFIX = "mcp__pi__";
|
|
50
|
+
|
|
51
|
+
function sanitizeCustomToolName(name: string): string {
|
|
52
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function toClaudeCodeToolName(name: string): string {
|
|
56
|
+
return (
|
|
57
|
+
claudeCodeToolLookup.get(name.toLowerCase()) ??
|
|
58
|
+
`${CLAUDE_CODE_MCP_TOOL_PREFIX}${sanitizeCustomToolName(name)}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function fromClaudeCodeToolName(name: string, tools?: Tool[]): string {
|
|
63
|
+
if (name.startsWith(CLAUDE_CODE_MCP_TOOL_PREFIX)) {
|
|
64
|
+
const stripped = name.slice(CLAUDE_CODE_MCP_TOOL_PREFIX.length);
|
|
65
|
+
return tools?.find((tool) => sanitizeCustomToolName(tool.name) === stripped)?.name ?? stripped;
|
|
66
|
+
}
|
|
67
|
+
const lower = name.toLowerCase();
|
|
68
|
+
return tools?.find((tool) => tool.name.toLowerCase() === lower)?.name ?? name;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function convertPiMessagesToAnthropic(
|
|
72
|
+
messages: Message[],
|
|
73
|
+
isOAuth: boolean,
|
|
74
|
+
model: Pick<Model<Api>, "provider" | "api" | "id">,
|
|
75
|
+
): MessageParam[] {
|
|
76
|
+
const params: MessageParam[] = [];
|
|
77
|
+
const toolIdMap = new Map<string, string>();
|
|
78
|
+
const usedToolIds = new Set<string>();
|
|
79
|
+
|
|
80
|
+
const getAnthropicToolId = (id: string): string => {
|
|
81
|
+
const existing = toolIdMap.get(id);
|
|
82
|
+
if (existing) return existing;
|
|
83
|
+
|
|
84
|
+
let base = sanitizeSurrogates(id).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
85
|
+
if (!base) base = "tool";
|
|
86
|
+
let candidate = base;
|
|
87
|
+
let suffix = 1;
|
|
88
|
+
while (usedToolIds.has(candidate)) {
|
|
89
|
+
candidate = `${base}_${suffix++}`;
|
|
90
|
+
}
|
|
91
|
+
usedToolIds.add(candidate);
|
|
92
|
+
toolIdMap.set(id, candidate);
|
|
93
|
+
return candidate;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Anthropic requires every `tool_use` block to be IMMEDIATELY followed by a
|
|
97
|
+
// user message containing a matching `tool_result` for the same id. Aborted
|
|
98
|
+
// / errored assistant turns or partial histories can leave orphan tool_uses
|
|
99
|
+
// behind, which causes the API to reject the next request with:
|
|
100
|
+
// `tool_use` ids were found without `tool_result` blocks immediately after
|
|
101
|
+
// We track outstanding tool_use ids from the last assistant message and
|
|
102
|
+
// synthesize "No result provided" results for any that aren't satisfied by
|
|
103
|
+
// the time we emit the next user message (or finish the history).
|
|
104
|
+
let pendingToolUseIds: string[] = [];
|
|
105
|
+
|
|
106
|
+
const flushPendingToolResults = () => {
|
|
107
|
+
if (pendingToolUseIds.length === 0) return;
|
|
108
|
+
const syntheticBlocks: ContentBlockParam[] = pendingToolUseIds.map((id) => ({
|
|
109
|
+
type: "tool_result",
|
|
110
|
+
tool_use_id: id,
|
|
111
|
+
content: "No result provided",
|
|
112
|
+
is_error: true,
|
|
113
|
+
}));
|
|
114
|
+
params.push({ role: "user", content: syntheticBlocks });
|
|
115
|
+
pendingToolUseIds = [];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < messages.length; i++) {
|
|
119
|
+
const message = messages[i];
|
|
120
|
+
|
|
121
|
+
if (message.role === "user") {
|
|
122
|
+
flushPendingToolResults();
|
|
123
|
+
if (typeof message.content === "string") {
|
|
124
|
+
if (message.content.trim()) params.push({ role: "user", content: sanitizeSurrogates(message.content) });
|
|
125
|
+
} else {
|
|
126
|
+
const blocks: ContentBlockParam[] = message.content.map((item) =>
|
|
127
|
+
item.type === "text"
|
|
128
|
+
? { type: "text", text: sanitizeSurrogates(item.text) }
|
|
129
|
+
: {
|
|
130
|
+
type: "image",
|
|
131
|
+
source: { type: "base64", media_type: item.mimeType as never, data: item.data },
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
if (blocks.length > 0) params.push({ role: "user", content: blocks });
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (message.role === "assistant") {
|
|
140
|
+
// Skip aborted / errored assistant turns entirely. Their content may
|
|
141
|
+
// include partial tool_use blocks that will never get a matching
|
|
142
|
+
// tool_result, which would poison every subsequent request.
|
|
143
|
+
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Defensive: if the previous assistant turn had unresolved tool_uses and
|
|
148
|
+
// the next message is another assistant (shouldn't normally happen),
|
|
149
|
+
// patch them up before emitting the new assistant turn.
|
|
150
|
+
flushPendingToolResults();
|
|
151
|
+
|
|
152
|
+
const blocks: ContentBlockParam[] = [];
|
|
153
|
+
const emittedToolUseIds: string[] = [];
|
|
154
|
+
const isSameModel =
|
|
155
|
+
message.provider === model.provider &&
|
|
156
|
+
message.api === model.api &&
|
|
157
|
+
message.model === model.id;
|
|
158
|
+
for (const block of message.content) {
|
|
159
|
+
if (block.type === "text" && block.text.trim()) {
|
|
160
|
+
blocks.push({ type: "text", text: sanitizeSurrogates(block.text) });
|
|
161
|
+
} else if (block.type === "thinking") {
|
|
162
|
+
const thinkingSignature = block.thinkingSignature?.trim();
|
|
163
|
+
if (block.redacted) {
|
|
164
|
+
if (isSameModel && thinkingSignature) {
|
|
165
|
+
blocks.push({
|
|
166
|
+
type: "redacted_thinking",
|
|
167
|
+
data: block.thinkingSignature!,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} else if (isSameModel && thinkingSignature) {
|
|
171
|
+
blocks.push({
|
|
172
|
+
type: "thinking",
|
|
173
|
+
thinking: sanitizeSurrogates(block.thinking),
|
|
174
|
+
signature: block.thinkingSignature!,
|
|
175
|
+
});
|
|
176
|
+
} else if (block.thinking.trim()) {
|
|
177
|
+
blocks.push({
|
|
178
|
+
type: "text",
|
|
179
|
+
text: sanitizeSurrogates(block.thinking),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
} else if (block.type === "toolCall") {
|
|
183
|
+
const anthropicId = getAnthropicToolId(block.id);
|
|
184
|
+
blocks.push({
|
|
185
|
+
type: "tool_use",
|
|
186
|
+
id: anthropicId,
|
|
187
|
+
name: isOAuth ? toClaudeCodeToolName(block.name) : block.name,
|
|
188
|
+
input: block.arguments ?? {},
|
|
189
|
+
});
|
|
190
|
+
emittedToolUseIds.push(anthropicId);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (blocks.length > 0) {
|
|
194
|
+
params.push({ role: "assistant", content: blocks });
|
|
195
|
+
pendingToolUseIds = emittedToolUseIds;
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (message.role === "toolResult") {
|
|
201
|
+
const toolResults: ContentBlockParam[] = [];
|
|
202
|
+
const satisfiedIds = new Set<string>();
|
|
203
|
+
|
|
204
|
+
const firstId = getAnthropicToolId(message.toolCallId);
|
|
205
|
+
toolResults.push({
|
|
206
|
+
type: "tool_result",
|
|
207
|
+
tool_use_id: firstId,
|
|
208
|
+
content: convertToolResultContentToAnthropic(message.content),
|
|
209
|
+
is_error: message.isError,
|
|
210
|
+
});
|
|
211
|
+
satisfiedIds.add(firstId);
|
|
212
|
+
|
|
213
|
+
let j = i + 1;
|
|
214
|
+
while (j < messages.length && messages[j]?.role === "toolResult") {
|
|
215
|
+
const nextMessage = messages[j] as ToolResultMessage;
|
|
216
|
+
const nextId = getAnthropicToolId(nextMessage.toolCallId);
|
|
217
|
+
toolResults.push({
|
|
218
|
+
type: "tool_result",
|
|
219
|
+
tool_use_id: nextId,
|
|
220
|
+
content: convertToolResultContentToAnthropic(nextMessage.content),
|
|
221
|
+
is_error: nextMessage.isError,
|
|
222
|
+
});
|
|
223
|
+
satisfiedIds.add(nextId);
|
|
224
|
+
j++;
|
|
225
|
+
}
|
|
226
|
+
i = j - 1;
|
|
227
|
+
|
|
228
|
+
// Synthesize "No result provided" for any tool_use from the last
|
|
229
|
+
// assistant message that wasn't covered by the toolResult run. They
|
|
230
|
+
// must live in the SAME user message as the real tool_results so that
|
|
231
|
+
// every tool_use is immediately followed by matching results.
|
|
232
|
+
for (const id of pendingToolUseIds) {
|
|
233
|
+
if (!satisfiedIds.has(id)) {
|
|
234
|
+
toolResults.push({
|
|
235
|
+
type: "tool_result",
|
|
236
|
+
tool_use_id: id,
|
|
237
|
+
content: "No result provided",
|
|
238
|
+
is_error: true,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
pendingToolUseIds = [];
|
|
243
|
+
|
|
244
|
+
params.push({ role: "user", content: toolResults });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const last = params.at(-1);
|
|
249
|
+
if (last?.role === "user" && Array.isArray(last.content) && last.content.length > 0) {
|
|
250
|
+
const lastBlock = last.content[last.content.length - 1] as { cache_control?: { type: string } };
|
|
251
|
+
lastBlock.cache_control = { type: "ephemeral" };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return params;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function convertPiToolsToAnthropic(tools: Tool[], isOAuth: boolean): ToolUnion[] {
|
|
258
|
+
return tools.map((tool) => ({
|
|
259
|
+
name: isOAuth ? toClaudeCodeToolName(tool.name) : tool.name,
|
|
260
|
+
description: tool.description,
|
|
261
|
+
input_schema: {
|
|
262
|
+
type: "object" as const,
|
|
263
|
+
properties: (tool.parameters as { properties?: Record<string, unknown> }).properties ?? {},
|
|
264
|
+
required: (tool.parameters as { required?: string[] }).required ?? [],
|
|
265
|
+
},
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function convertToolResultContentToAnthropic(
|
|
270
|
+
content: (TextContent | ImageContent)[],
|
|
271
|
+
): string | ToolResultContentBlock[] {
|
|
272
|
+
const hasImages = content.some((block) => block.type === "image");
|
|
273
|
+
if (!hasImages) {
|
|
274
|
+
return sanitizeSurrogates(
|
|
275
|
+
content
|
|
276
|
+
.filter((block): block is TextContent => block.type === "text")
|
|
277
|
+
.map((block) => block.text)
|
|
278
|
+
.join("\n"),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const blocks = content.map((block) => {
|
|
283
|
+
if (block.type === "text") return { type: "text" as const, text: sanitizeSurrogates(block.text) };
|
|
284
|
+
return {
|
|
285
|
+
type: "image" as const,
|
|
286
|
+
source: {
|
|
287
|
+
type: "base64" as const,
|
|
288
|
+
media_type: block.mimeType as ToolResultContentBlock extends { type: "image"; source: infer S }
|
|
289
|
+
? S extends { media_type: infer M }
|
|
290
|
+
? M
|
|
291
|
+
: never
|
|
292
|
+
: never,
|
|
293
|
+
data: block.data,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (!blocks.some((block) => block.type === "text")) {
|
|
299
|
+
blocks.unshift({ type: "text", text: "(see attached image)" });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return blocks;
|
|
303
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ProviderConfig,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { OAuthCredentials } from "@earendil-works/pi-ai";
|
|
9
|
+
import { loginAnthropic, refreshAnthropicToken } from "./auth.js";
|
|
10
|
+
import { streamAnthropicOAuth } from "./stream.js";
|
|
11
|
+
|
|
12
|
+
function ensureClaudeCodeSymlink() {
|
|
13
|
+
const target = join(homedir(), ".pi");
|
|
14
|
+
const link = join(homedir(), ".Claude Code");
|
|
15
|
+
if (existsSync(target) && !existsSync(link)) {
|
|
16
|
+
try {
|
|
17
|
+
symlinkSync(target, link);
|
|
18
|
+
} catch { }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function (pi: ExtensionAPI) {
|
|
23
|
+
ensureClaudeCodeSymlink();
|
|
24
|
+
|
|
25
|
+
pi.registerProvider("anthropic", {
|
|
26
|
+
baseUrl: "https://api.anthropic.com",
|
|
27
|
+
api: "anthropic-messages",
|
|
28
|
+
oauth: {
|
|
29
|
+
name: "Claude Pro/Max",
|
|
30
|
+
usesCallbackServer: true,
|
|
31
|
+
login: loginAnthropic,
|
|
32
|
+
refreshToken: refreshAnthropicToken,
|
|
33
|
+
getApiKey: (credentials: OAuthCredentials) => credentials.access,
|
|
34
|
+
} as unknown as ProviderConfig["oauth"],
|
|
35
|
+
streamSimple: streamAnthropicOAuth,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const CLAUDE_CODE_IDENTITY =
|
|
2
|
+
"You are Claude Code, Anthropic's official CLI for Claude.";
|
|
3
|
+
const PI_REMOVAL_ANCHORS = [
|
|
4
|
+
"pi-coding-agent",
|
|
5
|
+
"@earendil-works/pi-coding-agent",
|
|
6
|
+
"badlogic/pi-mono",
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
export const PI_REWRITE_MODE_ENV = "PI_ANTHROPIC_OAUTH_REWRITE_MODE";
|
|
10
|
+
export const PI_REWRITE_PATTERN_ENV = "PI_ANTHROPIC_OAUTH_REWRITE_PATTERN";
|
|
11
|
+
|
|
12
|
+
export type PiRewriteMode = "aggressive" | "path-safe" | "technical-safe" | "custom";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PI_REWRITE_MODE: PiRewriteMode = "aggressive";
|
|
15
|
+
const PI_REWRITE_PATTERN_SOURCES: Record<Exclude<PiRewriteMode, "custom">, string> = {
|
|
16
|
+
aggressive: String.raw`\b[Pp]i\b`,
|
|
17
|
+
"path-safe": String.raw`(?<![/\\])\b[Pp]i\b`,
|
|
18
|
+
"technical-safe": String.raw`(?<![/\\.@:_-])\b[Pp]i\b(?![/\\.@:_-])`,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PI_REWRITE_MODES = new Set<PiRewriteMode>([
|
|
22
|
+
"aggressive",
|
|
23
|
+
"path-safe",
|
|
24
|
+
"technical-safe",
|
|
25
|
+
"custom",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
type MessageContentBlock = {
|
|
29
|
+
type: string;
|
|
30
|
+
text?: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function sanitizeSurrogates(text: string): string {
|
|
35
|
+
// Replace only *unpaired* surrogates: a high surrogate not followed by a low
|
|
36
|
+
// surrogate, or a low surrogate not preceded by a high surrogate. Valid
|
|
37
|
+
// surrogate pairs (non-BMP characters such as emoji) must pass through
|
|
38
|
+
// untouched. Equivalent to String.prototype.toWellFormed(), which requires
|
|
39
|
+
// Node 20+ while this package supports Node >=18.
|
|
40
|
+
return text.replace(
|
|
41
|
+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
|
|
42
|
+
"\uFFFD",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildAnthropicSystemPrompt(
|
|
47
|
+
systemPrompt: string | undefined,
|
|
48
|
+
isOAuth: boolean,
|
|
49
|
+
): MessageContentBlock[] | undefined {
|
|
50
|
+
const blocks: MessageContentBlock[] = [];
|
|
51
|
+
|
|
52
|
+
if (isOAuth) {
|
|
53
|
+
blocks.push({
|
|
54
|
+
type: "text",
|
|
55
|
+
text: CLAUDE_CODE_IDENTITY,
|
|
56
|
+
cache_control: { type: "ephemeral" },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sanitized = systemPrompt ? sanitizeSystemText(systemPrompt) : "";
|
|
61
|
+
if (sanitized) {
|
|
62
|
+
blocks.push({
|
|
63
|
+
type: "text",
|
|
64
|
+
text: sanitized,
|
|
65
|
+
cache_control: { type: "ephemeral" },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return blocks.length > 0 ? blocks : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function sanitizeSystemText(
|
|
73
|
+
text: string,
|
|
74
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
75
|
+
): string {
|
|
76
|
+
const paragraphs = text.split(/\n\n+/);
|
|
77
|
+
const filtered = paragraphs.filter((paragraph) => {
|
|
78
|
+
const lower = paragraph.toLowerCase();
|
|
79
|
+
if (lower.includes("you are pi")) return false;
|
|
80
|
+
return !PI_REMOVAL_ANCHORS.some((anchor) => paragraph.includes(anchor));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return filtered
|
|
84
|
+
.join("\n\n")
|
|
85
|
+
.replace(resolvePiRewritePattern(env), "Claude Code")
|
|
86
|
+
.trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolvePiRewritePattern(env: NodeJS.ProcessEnv): RegExp {
|
|
90
|
+
const mode = parsePiRewriteMode(env[PI_REWRITE_MODE_ENV]);
|
|
91
|
+
if (mode === "custom") return compileCustomPiRewritePattern(env[PI_REWRITE_PATTERN_ENV]);
|
|
92
|
+
return new RegExp(PI_REWRITE_PATTERN_SOURCES[mode], "g");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parsePiRewriteMode(value: string | undefined): PiRewriteMode {
|
|
96
|
+
if (!value?.trim()) return DEFAULT_PI_REWRITE_MODE;
|
|
97
|
+
const mode = value.trim().toLowerCase();
|
|
98
|
+
if (PI_REWRITE_MODES.has(mode as PiRewriteMode)) return mode as PiRewriteMode;
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Invalid ${PI_REWRITE_MODE_ENV}: ${value}. Expected one of ${Array.from(PI_REWRITE_MODES).join(", ")}.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function compileCustomPiRewritePattern(value: string | undefined): RegExp {
|
|
105
|
+
const pattern = value?.trim();
|
|
106
|
+
if (!pattern) {
|
|
107
|
+
throw new Error(`${PI_REWRITE_PATTERN_ENV} must be set when ${PI_REWRITE_MODE_ENV}=custom.`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parsed = parseRegexLiteral(pattern) ?? { source: pattern, flags: "" };
|
|
111
|
+
const flags = parsed.flags.includes("g") ? parsed.flags : `${parsed.flags}g`;
|
|
112
|
+
try {
|
|
113
|
+
return new RegExp(parsed.source, flags);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
throw new Error(`Invalid ${PI_REWRITE_PATTERN_ENV}: ${message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseRegexLiteral(value: string): { source: string; flags: string } | undefined {
|
|
121
|
+
if (!value.startsWith("/")) return undefined;
|
|
122
|
+
|
|
123
|
+
for (let index = value.length - 1; index > 0; index--) {
|
|
124
|
+
if (value[index] !== "/" || isEscaped(value, index)) continue;
|
|
125
|
+
return { source: value.slice(1, index), flags: value.slice(index + 1) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isEscaped(value: string, index: number): boolean {
|
|
132
|
+
let slashCount = 0;
|
|
133
|
+
for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor--) {
|
|
134
|
+
slashCount++;
|
|
135
|
+
}
|
|
136
|
+
return slashCount % 2 === 1;
|
|
137
|
+
}
|