@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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +29 -0
  3. package/README.md +999 -0
  4. package/config/models/pi-multi-account.v1.json +32 -0
  5. package/config/subscription-plans.v1.json +122 -0
  6. package/package.json +76 -0
  7. package/packages/pi-anthropic-oauth/LICENSE +21 -0
  8. package/packages/pi-anthropic-oauth/package.json +54 -0
  9. package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
  10. package/packages/pi-anthropic-oauth/src/context.ts +116 -0
  11. package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
  12. package/packages/pi-anthropic-oauth/src/index.ts +37 -0
  13. package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
  14. package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
  15. package/packages/pi-antigravity/LICENSE +21 -0
  16. package/packages/pi-antigravity/package.json +77 -0
  17. package/packages/pi-antigravity/src/auth/index.ts +14 -0
  18. package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
  19. package/packages/pi-antigravity/src/client/client.ts +561 -0
  20. package/packages/pi-antigravity/src/client/index.ts +1 -0
  21. package/packages/pi-antigravity/src/context.ts +110 -0
  22. package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  23. package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
  24. package/packages/pi-antigravity/src/image/image.ts +336 -0
  25. package/packages/pi-antigravity/src/image/index.ts +1 -0
  26. package/packages/pi-antigravity/src/index.ts +280 -0
  27. package/packages/pi-antigravity/src/models/discovery.ts +154 -0
  28. package/packages/pi-antigravity/src/models/grouping.ts +424 -0
  29. package/packages/pi-antigravity/src/models/index.ts +3 -0
  30. package/packages/pi-antigravity/src/models/models.ts +500 -0
  31. package/packages/pi-antigravity/src/stream/index.ts +1 -0
  32. package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
  33. package/packages/pi-antigravity/src/types/enums.ts +42 -0
  34. package/packages/pi-antigravity/src/types/index.ts +2 -0
  35. package/packages/pi-antigravity/src/types/types.ts +292 -0
  36. package/packages/pi-antigravity/src/usage/index.ts +1 -0
  37. package/packages/pi-antigravity/src/usage/usage.ts +416 -0
  38. package/packages/pi-antigravity/src/utils/http.ts +91 -0
  39. package/packages/pi-antigravity/src/utils/index.ts +3 -0
  40. package/packages/pi-antigravity/src/utils/security.ts +73 -0
  41. package/packages/pi-antigravity/src/utils/util.ts +132 -0
  42. package/scripts/multi-account.mjs +44 -0
  43. package/src/account-labels.ts +223 -0
  44. package/src/account-plan-assignment.ts +340 -0
  45. package/src/account-rate-history.ts +372 -0
  46. package/src/anthropic-adaptive-stream.ts +531 -0
  47. package/src/anthropic-alias-stream.ts +140 -0
  48. package/src/anthropic-context-compat.ts +80 -0
  49. package/src/api-pricing.ts +579 -0
  50. package/src/bounded-file-lines.ts +97 -0
  51. package/src/catalog-rebinding.ts +177 -0
  52. package/src/catalog-registration-probe.ts +111 -0
  53. package/src/codex-adapter.ts +345 -0
  54. package/src/codex-model-defaults.ts +785 -0
  55. package/src/command-completions.ts +404 -0
  56. package/src/commands.ts +2000 -0
  57. package/src/compaction.ts +14 -0
  58. package/src/config.ts +1317 -0
  59. package/src/continuation.ts +569 -0
  60. package/src/cooldowns.ts +110 -0
  61. package/src/cost-digest-store.ts +332 -0
  62. package/src/cost-digest.ts +1044 -0
  63. package/src/cost-history.ts +251 -0
  64. package/src/cost-period-closer.ts +160 -0
  65. package/src/cost-report-json.ts +318 -0
  66. package/src/cost-report-reader.ts +368 -0
  67. package/src/cost-report-render.ts +207 -0
  68. package/src/cost-report.ts +1104 -0
  69. package/src/coverage-attestation.ts +397 -0
  70. package/src/credential-lifecycle.ts +169 -0
  71. package/src/credential-refresh.ts +248 -0
  72. package/src/declaration-notice-marker.ts +238 -0
  73. package/src/diagnostic-store.ts +276 -0
  74. package/src/diagnostics.ts +309 -0
  75. package/src/discovery.ts +471 -0
  76. package/src/duration.ts +13 -0
  77. package/src/error-classification.ts +256 -0
  78. package/src/fuzzy.ts +15 -0
  79. package/src/group-policy.ts +81 -0
  80. package/src/history-store.ts +897 -0
  81. package/src/index.ts +5572 -0
  82. package/src/lifecycle.ts +378 -0
  83. package/src/logical-dispatch.ts +279 -0
  84. package/src/logical-model-selector.ts +254 -0
  85. package/src/logical-model-switcher.ts +430 -0
  86. package/src/logical-provider-attribution.ts +544 -0
  87. package/src/logical-provider.ts +1237 -0
  88. package/src/logical-route-indicator.ts +215 -0
  89. package/src/machine-lease.ts +445 -0
  90. package/src/model-support.ts +66 -0
  91. package/src/models-declaration.ts +1091 -0
  92. package/src/openai-adapter.ts +117 -0
  93. package/src/openrouter-budget.ts +304 -0
  94. package/src/openrouter-fallback.ts +146 -0
  95. package/src/period-boundaries.ts +376 -0
  96. package/src/pi-anthropic-oauth.d.ts +6 -0
  97. package/src/preflight.ts +253 -0
  98. package/src/pricing-cache.ts +235 -0
  99. package/src/project-identity.ts +100 -0
  100. package/src/provider-registration.ts +942 -0
  101. package/src/rate-formula.ts +163 -0
  102. package/src/recovery-engine.ts +853 -0
  103. package/src/recovery-output.ts +837 -0
  104. package/src/recovery-plan.ts +239 -0
  105. package/src/report-range.ts +203 -0
  106. package/src/route-resolver.ts +789 -0
  107. package/src/routing-config-transaction.ts +232 -0
  108. package/src/routing.ts +1163 -0
  109. package/src/runtime-state.ts +630 -0
  110. package/src/session-account-groups.ts +284 -0
  111. package/src/session-restore.ts +287 -0
  112. package/src/shared-usage.ts +1392 -0
  113. package/src/standalone-cli.ts +720 -0
  114. package/src/status-view.ts +578 -0
  115. package/src/subscription-plan-catalog.ts +346 -0
  116. package/src/tier-model-resolver.ts +46 -0
  117. package/src/upstream-anthropic.ts +315 -0
  118. package/src/upstream-antigravity.ts +327 -0
  119. package/src/usage-fetch.ts +1634 -0
  120. package/src/usage.ts +1026 -0
  121. package/src/vendor.ts +87 -0
  122. package/src/warmer.ts +231 -0
  123. package/src/watchdog.ts +219 -0
  124. 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
+ }