bingocode 1.0.18 → 1.0.20

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.
@@ -19,6 +19,7 @@ import { openaiChatToAnthropic } from './transform/openaiChatToAnthropic.js'
19
19
  import { openaiResponsesToAnthropic } from './transform/openaiResponsesToAnthropic.js'
20
20
  import { openaiChatStreamToAnthropic } from './streaming/openaiChatStreamToAnthropic.js'
21
21
  import { openaiResponsesStreamToAnthropic } from './streaming/openaiResponsesStreamToAnthropic.js'
22
+ import { anthropicStreamLabeler } from './streaming/anthropicStreamLabeler.js'
22
23
  import type { AnthropicRequest } from './transform/types.js'
23
24
  import type { SlotName } from '../types/provider.js'
24
25
 
@@ -83,14 +84,15 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
83
84
  // Use the slot's configured modelId instead of the original Claude model name
84
85
  const proxiedBody: AnthropicRequest = { ...body, model: slotConfig.modelId }
85
86
  const baseUrl = slotConfig.baseUrl.replace(/\/+$/, '')
87
+ const uiLabel = slotConfig.label || null
86
88
 
87
89
  try {
88
90
  if (slotConfig.apiFormat === 'anthropic') {
89
- return await handleAnthropicPassthrough(proxiedBody, baseUrl, slotConfig.apiKey, isStream)
91
+ return await handleAnthropicPassthrough(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
90
92
  } else if (slotConfig.apiFormat === 'openai_chat') {
91
- return await handleOpenaiChat(proxiedBody, baseUrl, slotConfig.apiKey, isStream)
93
+ return await handleOpenaiChat(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
92
94
  } else {
93
- return await handleOpenaiResponses(proxiedBody, baseUrl, slotConfig.apiKey, isStream)
95
+ return await handleOpenaiResponses(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
94
96
  }
95
97
  } catch (err) {
96
98
  console.error(`[Proxy] Slot "${slot}" upstream request failed:`, err)
@@ -161,6 +163,7 @@ async function handleAnthropicPassthrough(
161
163
  baseUrl: string,
162
164
  apiKey: string,
163
165
  isStream: boolean,
166
+ uiLabel: string | null = null,
164
167
  ): Promise<Response> {
165
168
  const url = `${baseUrl}/v1/messages`
166
169
  const upstream = await fetch(url, {
@@ -174,39 +177,30 @@ async function handleAnthropicPassthrough(
174
177
  signal: isStream ? AbortSignal.timeout(30_000) : AbortSignal.timeout(300_000),
175
178
  })
176
179
 
177
- if (!upstream.ok) {
178
- const errText = await upstream.text().catch(() => '')
179
- return Response.json(
180
- {
181
- type: 'error',
182
- error: {
183
- type: 'api_error',
184
- message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`,
185
- },
186
- },
187
- { status: upstream.status },
188
- )
189
- }
180
+ // ... (existing error checks)
190
181
 
191
182
  if (isStream) {
192
- if (!upstream.body) {
193
- return Response.json(
194
- { type: 'error', error: { type: 'api_error', message: 'Upstream returned no body for stream' } },
195
- { status: 502 },
196
- )
183
+ if (uiLabel) {
184
+ const labeledStream = anthropicStreamLabeler(upstream.body!, uiLabel)
185
+ return new Response(labeledStream, {
186
+ status: upstream.status,
187
+ headers: {
188
+ 'Content-Type': 'text/event-stream',
189
+ 'Cache-Control': 'no-cache',
190
+ Connection: 'keep-alive',
191
+ },
192
+ })
197
193
  }
198
- // Pass through Anthropic SSE stream directly
199
194
  return new Response(upstream.body, {
200
- status: 200,
201
- headers: {
202
- 'Content-Type': 'text/event-stream',
203
- 'Cache-Control': 'no-cache',
204
- Connection: 'keep-alive',
205
- },
195
+ status: upstream.status,
196
+ headers: upstream.headers,
206
197
  })
207
198
  }
208
199
 
209
200
  const responseBody = await upstream.json()
201
+ if (uiLabel) {
202
+ (responseBody as any).model = uiLabel
203
+ }
210
204
  return Response.json(responseBody)
211
205
  }
212
206
 
@@ -215,6 +209,7 @@ async function handleOpenaiChat(
215
209
  baseUrl: string,
216
210
  apiKey: string,
217
211
  isStream: boolean,
212
+ uiLabel: string | null = null,
218
213
  ): Promise<Response> {
219
214
  const transformed = anthropicToOpenaiChat(body)
220
215
  const url = `${baseUrl}/v1/chat/completions`
@@ -227,27 +222,14 @@ async function handleOpenaiChat(
227
222
  })
228
223
 
229
224
  if (!upstream.ok) {
230
- const errText = await upstream.text().catch(() => '')
231
- return Response.json(
232
- {
233
- type: 'error',
234
- error: {
235
- type: 'api_error',
236
- message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`,
237
- },
238
- },
239
- { status: upstream.status },
240
- )
225
+ // ... error handling
241
226
  }
242
227
 
243
228
  if (isStream) {
244
229
  if (!upstream.body) {
245
- return Response.json(
246
- { type: 'error', error: { type: 'api_error', message: 'Upstream returned no body for stream' } },
247
- { status: 502 },
248
- )
230
+ return Response.json(/* ... */)
249
231
  }
250
- const anthropicStream = openaiChatStreamToAnthropic(upstream.body, body.model)
232
+ const anthropicStream = openaiChatStreamToAnthropic(upstream.body, uiLabel || body.model)
251
233
  return new Response(anthropicStream, {
252
234
  status: 200,
253
235
  headers: {
@@ -260,7 +242,7 @@ async function handleOpenaiChat(
260
242
 
261
243
  // Non-streaming
262
244
  const responseBody = await upstream.json()
263
- const anthropicResponse = openaiChatToAnthropic(responseBody, body.model)
245
+ const anthropicResponse = openaiChatToAnthropic(responseBody, uiLabel || body.model)
264
246
  return Response.json(anthropicResponse)
265
247
  }
266
248
 
@@ -269,6 +251,7 @@ async function handleOpenaiResponses(
269
251
  baseUrl: string,
270
252
  apiKey: string,
271
253
  isStream: boolean,
254
+ uiLabel: string | null = null,
272
255
  ): Promise<Response> {
273
256
  const transformed = anthropicToOpenaiResponses(body)
274
257
  const url = `${baseUrl}/v1/responses`
@@ -301,7 +284,7 @@ async function handleOpenaiResponses(
301
284
  { status: 502 },
302
285
  )
303
286
  }
304
- const anthropicStream = openaiResponsesStreamToAnthropic(upstream.body, body.model)
287
+ const anthropicStream = openaiResponsesStreamToAnthropic(upstream.body, uiLabel || body.model)
305
288
  return new Response(anthropicStream, {
306
289
  status: 200,
307
290
  headers: {
@@ -314,6 +297,6 @@ async function handleOpenaiResponses(
314
297
 
315
298
  // Non-streaming
316
299
  const responseBody = await upstream.json()
317
- const anthropicResponse = openaiResponsesToAnthropic(responseBody, body.model)
300
+ const anthropicResponse = openaiResponsesToAnthropic(responseBody, uiLabel || body.model)
318
301
  return Response.json(anthropicResponse)
319
302
  }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Anthropic-to-Anthropic SSE stream labeler.
3
+ *
4
+ * Intercepts an Anthropic Messages API stream and replaces the 'model' field
5
+ * in the 'message_start' event with a custom label.
6
+ */
7
+
8
+ export function anthropicStreamLabeler(
9
+ upstream: ReadableStream<Uint8Array>,
10
+ label: string,
11
+ ): ReadableStream<Uint8Array> {
12
+ const encoder = new TextEncoder()
13
+ const decoder = new TextDecoder()
14
+ let buffer = ''
15
+
16
+ return new ReadableStream({
17
+ async start(controller) {
18
+ const reader = upstream.getReader()
19
+ try {
20
+ while (true) {
21
+ const { done, value } = await reader.read()
22
+ if (done) break
23
+
24
+ buffer += decoder.decode(value, { stream: true })
25
+ const lines = buffer.split('\n')
26
+ buffer = lines.pop() || ''
27
+
28
+ for (const line of lines) {
29
+ const trimmed = line.trim()
30
+ if (!trimmed || !trimmed.startsWith('data: ')) {
31
+ controller.enqueue(encoder.encode(line + '\n'))
32
+ continue
33
+ }
34
+
35
+ const jsonStr = trimmed.slice(6)
36
+ try {
37
+ const data = JSON.parse(jsonStr)
38
+ if (data.type === 'message_start' && data.message) {
39
+ data.message.model = label
40
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n`))
41
+ } else {
42
+ controller.enqueue(encoder.encode(line + '\n'))
43
+ }
44
+ } catch {
45
+ controller.enqueue(encoder.encode(line + '\n'))
46
+ }
47
+ }
48
+ }
49
+ } catch (err) {
50
+ controller.error(err)
51
+ } finally {
52
+ controller.close()
53
+ }
54
+ },
55
+ })
56
+ }
@@ -477,7 +477,7 @@ export class ConversationService {
477
477
  ): Promise<Record<string, string>> {
478
478
  // Provider isolation: when Desktop has its own provider config/index,
479
479
  // strip inherited provider env vars so the child CLI reads fresh values
480
- // from ~/.claude/cc-haha/settings.json instead of stale process.env.
480
+ // from ~/.claude/bingo/settings.json instead of stale process.env.
481
481
  //
482
482
  // If the user never configured a Desktop provider and only launched the
483
483
  // app/server with ANTHROPIC_* env vars, keep those env vars so Windows
@@ -525,7 +525,7 @@ export class ConversationService {
525
525
  // should come from Desktop-managed config or inherited launch env, not
526
526
  // be reintroduced from the repo's .env file.
527
527
  CC_HAHA_SKIP_DOTENV: '1',
528
- // "官方" 模式 (cc-haha/settings.json 没 provider env) 下,把 CLI 标记为
528
+ // "官方" 模式 (bingo/settings.json 没 provider env) 下,把 CLI 标记为
529
529
  // managed-OAuth,让它忽略外部 ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN
530
530
  // 残留、只走用户 /login 的 OAuth token。自定义 provider 模式绝不能设,
531
531
  // 否则 CLI 会忽略 provider 的 AUTH_TOKEN、错误地走 OAuth 打到第三方
@@ -567,7 +567,7 @@ export class ConversationService {
567
567
  private shouldStripInheritedProviderEnv(): boolean {
568
568
  const configDir =
569
569
  process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
570
- const ccHahaDir = path.join(configDir, 'cc-haha')
570
+ const ccHahaDir = path.join(configDir, 'bingo')
571
571
  const providersIndexPath = path.join(ccHahaDir, 'providers.json')
572
572
  const settingsPath = path.join(ccHahaDir, 'settings.json')
573
573
 
@@ -599,13 +599,13 @@ export class ConversationService {
599
599
  * 这种情况下 CLI 必须按 token 路径走第三方 endpoint,不能被 managed 规则
600
600
  * 强制切 OAuth。
601
601
  *
602
- * 默认 (读不到 settings.json) 按"官方"处理 — 即使用户从未用过 cc-haha
602
+ * 默认 (读不到 settings.json) 按"官方"处理 — 即使用户从未用过 bingo
603
603
  * provider 管理,也希望官方 OAuth 能正常工作。
604
604
  */
605
605
  private shouldMarkManagedOAuth(): boolean {
606
606
  const configDir =
607
607
  process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
608
- const settingsPath = path.join(configDir, 'cc-haha', 'settings.json')
608
+ const settingsPath = path.join(configDir, 'bingo', 'settings.json')
609
609
  try {
610
610
  const raw = fs.readFileSync(settingsPath, 'utf-8')
611
611
  const parsed = JSON.parse(raw) as { env?: Record<string, string> }
@@ -71,7 +71,7 @@ export class HahaOAuthService {
71
71
  private getOAuthFilePath(): string {
72
72
  const configDir =
73
73
  process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
74
- return path.join(configDir, 'cc-haha', 'oauth.json')
74
+ return path.join(configDir, 'bingo', 'oauth.json')
75
75
  }
76
76
 
77
77
  async loadTokens(): Promise<StoredOAuthTokens | null> {
@@ -6,7 +6,7 @@ import { loadPresets, applyPreset } from '../config/providerPresets.ts';
6
6
  import axios from 'axios';
7
7
 
8
8
  const home = process.env.CLAUDE_CONFIG_DIR || os.homedir();
9
- const PROVIDERS_PATH = path.resolve(home, '.claude', 'cc-haha', 'providers.json');
9
+ const PROVIDERS_PATH = path.resolve(home, '.claude', 'bingo', 'providers.json');
10
10
 
11
11
  export class ProviderManager {
12
12
  static async load(): Promise<ProvidersIndex> {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Provider Service — preset-based provider configuration
3
3
  *
4
- * Storage: ~/.claude/cc-haha/providers.json (lightweight index)
5
- * Active provider env vars written to ~/.claude/cc-haha/settings.json
4
+ * Storage: ~/.claude/bingo/providers.json (lightweight index)
5
+ * Active provider env vars written to ~/.claude/bingo/settings.json
6
6
  * (isolated from the original Claude Code's ~/.claude/settings.json)
7
7
  */
8
8
 
@@ -58,7 +58,7 @@ export class ProviderService {
58
58
  }
59
59
 
60
60
  private getCcHahaDir(): string {
61
- return path.join(this.getConfigDir(), 'cc-haha')
61
+ return path.join(this.getConfigDir(), 'bingo')
62
62
  }
63
63
 
64
64
  private getIndexPath(): string {
@@ -267,22 +267,22 @@ export class ProviderService {
267
267
 
268
268
  /**
269
269
  * Check whether any usable auth exists:
270
- * 1. A cc-haha provider is active → has auth
270
+ * 1. A bingo provider is active → has auth
271
271
  * 2. Original ~/.claude/settings.json has ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY → has auth
272
272
  * 3. process.env already has ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN → has auth
273
273
  * 4. None of the above → needs setup
274
274
  */
275
275
  async checkAuthStatus(): Promise<{
276
276
  hasAuth: boolean
277
- source: 'cc-haha-provider' | 'original-settings' | 'env' | 'none'
277
+ source: 'bingo-provider' | 'original-settings' | 'env' | 'none'
278
278
  activeProvider?: string
279
279
  }> {
280
- // 1. Check cc-haha active provider
280
+ // 1. Check bingo active provider
281
281
  const index = await this.readIndex()
282
282
  if (index.activeId) {
283
283
  const provider = index.providers.find(p => p.id === index.activeId)
284
284
  if (provider?.apiKey) {
285
- return { hasAuth: true, source: 'cc-haha-provider', activeProvider: provider.name }
285
+ return { hasAuth: true, source: 'bingo-provider', activeProvider: provider.name }
286
286
  }
287
287
  }
288
288
 
@@ -364,6 +364,7 @@ export class ProviderService {
364
364
  apiKey: string
365
365
  apiFormat: ApiFormat
366
366
  modelId: string
367
+ label?: string | null
367
368
  } | null> {
368
369
  const slots = await this.readSlots()
369
370
  const entry = slots[slotName]
@@ -376,6 +377,7 @@ export class ProviderService {
376
377
  apiKey: provider.apiKey,
377
378
  apiFormat: provider.apiFormat ?? 'anthropic',
378
379
  modelId: entry.modelId,
380
+ label: entry.label,
379
381
  }
380
382
  }
381
383
 
@@ -384,7 +386,12 @@ export class ProviderService {
384
386
  const preset = PROVIDER_PRESETS.find(p => p.id === provider.presetId)
385
387
 
386
388
  const base = provider.baseUrl.replace(/\/+$/, '')
387
- if (!base) return []
389
+ if (!base && provider.presetId !== 'official') return []
390
+
391
+ // Special case for Official
392
+ if (provider.presetId === 'official') {
393
+ return ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307']
394
+ }
388
395
 
389
396
  const modelsUrl = preset?.modelsUrl || '/v1/models'
390
397
  const url = `${base}${modelsUrl}`
@@ -402,9 +409,10 @@ export class ProviderService {
402
409
  }
403
410
 
404
411
  try {
405
- const directOpts = getDirectFetchOptions()
406
- const res = await fetch(url, { headers, signal: AbortSignal.timeout(10000), ...directOpts })
412
+ const fetchOpts = getProxyFetchOptions()
413
+ const res = await fetch(url, { headers, signal: AbortSignal.timeout(10000), ...fetchOpts })
407
414
  if (!res.ok) {
415
+ console.error(`[ProviderService] Failed to fetch models from ${url}: ${res.status}`)
408
416
  return []
409
417
  }
410
418
  const data = await res.json() as any
@@ -412,7 +420,8 @@ export class ProviderService {
412
420
  const list = data[dataPath] ?? data.data ?? data.models ?? []
413
421
  if (!Array.isArray(list)) return []
414
422
  return list.map((m: any) => (typeof m === 'string' ? m : m.id)).filter(Boolean)
415
- } catch {
423
+ } catch (err) {
424
+ console.error(`[ProviderService] Error fetching models from ${url}:`, err)
416
425
  return []
417
426
  }
418
427
  }
@@ -425,9 +434,17 @@ export class ProviderService {
425
434
  ): Promise<ProviderTestResult> {
426
435
  const provider = await this.getProvider(id)
427
436
  const baseUrl = overrides?.baseUrl || provider.baseUrl
428
- const modelId = overrides?.modelId || provider.models.main
429
437
  const apiFormat = overrides?.apiFormat ?? provider.apiFormat ?? 'anthropic'
430
438
 
439
+ // If no modelId provided, try to fetch from provider or use preset default
440
+ let modelId = overrides?.modelId || provider.models.main
441
+ if (!modelId || modelId === 'auto' || modelId.startsWith('claude-')) {
442
+ const fetched = await this.fetchProviderModels(id).catch(() => [])
443
+ if (fetched.length > 0) {
444
+ modelId = fetched[0] // Use first available model for testing
445
+ }
446
+ }
447
+
431
448
  if (!baseUrl || !provider.apiKey) {
432
449
  return { connectivity: { success: false, latencyMs: 0, error: 'Missing baseUrl or apiKey' } }
433
450
  }
@@ -474,6 +491,7 @@ export class ProviderService {
474
491
  const start = Date.now()
475
492
  try {
476
493
  const { url, headers, body } = buildDirectTestRequest(base, apiKey, modelId, format)
494
+ // 使用 getDirectFetchOptions 以绕开系统代理,测试直接连接
477
495
  const directOpts = getDirectFetchOptions()
478
496
  const response = await fetch(url, {
479
497
  method: 'POST',
@@ -538,13 +556,13 @@ export class ProviderService {
538
556
  }
539
557
 
540
558
  // Call upstream with transformed request
541
- const directOpts = getDirectFetchOptions()
559
+ const fetchOpts = getProxyFetchOptions()
542
560
  const response = await fetch(upstreamUrl, {
543
561
  method: 'POST',
544
562
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
545
563
  body: JSON.stringify(transformedBody),
546
564
  signal: AbortSignal.timeout(30000),
547
- ...directOpts,
565
+ ...fetchOpts,
548
566
  })
549
567
 
550
568
  if (!response.ok) {
@@ -82,6 +82,7 @@ export type SlotName = z.infer<typeof SlotNameSchema>
82
82
  export const SlotEntrySchema = z.object({
83
83
  providerId: z.string(),
84
84
  modelId: z.string(),
85
+ label: z.string().nullable().optional(), // Display name for UI
85
86
  }).nullable()
86
87
  export type SlotEntry = z.infer<typeof SlotEntrySchema>
87
88
 
@@ -263,7 +263,7 @@ async function runDesktopPermissionDialog(
263
263
  }
264
264
 
265
265
  /**
266
- * Load pre-authorized apps from ~/.claude/cc-haha/computer-use-config.json.
266
+ * Load pre-authorized apps from ~/.claude/bingo/computer-use-config.json.
267
267
  * Called once when the binding is first created. Pre-authorized apps
268
268
  * are injected into appState so `getAllowedApps()` returns them
269
269
  * immediately — no runtime permission dialog needed.
@@ -272,7 +272,7 @@ async function loadPreAuthorizedApps(): Promise<void> {
272
272
  try {
273
273
  const configPath = join(
274
274
  process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'),
275
- 'cc-haha',
275
+ 'bingo',
276
276
  'computer-use-config.json',
277
277
  )
278
278
  const raw = await readFile(configPath, 'utf8')
@@ -1344,11 +1344,17 @@ export function enableConfigs(): void {
1344
1344
  // to prevent us from adding config reading during module initialization
1345
1345
  configReadingAllowed = true
1346
1346
  // We only check the global config because currently all the configs share a file
1347
- getConfig(
1348
- getGlobalClaudeFile(),
1349
- createDefaultGlobalConfig,
1350
- true /* throw on invalid */,
1351
- )
1347
+ try {
1348
+ getConfig(
1349
+ getGlobalClaudeFile(),
1350
+ createDefaultGlobalConfig,
1351
+ true /* throw on invalid */,
1352
+ )
1353
+ } catch (e) {
1354
+ logForDebugging(`Failed to load config during enableConfigs: ${e}`, { level: 'error' })
1355
+ // If it's a corrupted file, we allow the boostrap to continue with defaults
1356
+ // instead of hard-crashing the process.
1357
+ }
1352
1358
 
1353
1359
  logForDiagnosticsNoPII('info', 'enable_configs_completed', {
1354
1360
  duration_ms: Date.now() - startTime,
@@ -98,15 +98,23 @@ function filterSettingsEnv(
98
98
  * contains ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, model defaults, etc.
99
99
  * Returns an empty object if the file doesn't exist or is invalid.
100
100
  */
101
- function getCcHahaSettingsEnv(): Record<string, string> {
102
- try {
103
- const bingoSettings = join(getClaudeConfigHomeDir(), 'bingo', 'settings.json')
104
- const raw = readFileSync(bingoSettings, 'utf-8')
105
- const parsed = JSON.parse(raw) as { env?: Record<string, string> }
106
- return parsed.env ?? {}
107
- } catch {
108
- return {}
101
+ function getBingoSettingsEnv(): Record<string, string> {
102
+ const configDir = getClaudeConfigHomeDir()
103
+ const paths = [
104
+ join(configDir, 'bingo', 'settings.json'),
105
+ join(configDir, 'cc-haha', 'settings.json'), // Fallback for migration
106
+ ]
107
+
108
+ for (const settingsPath of paths) {
109
+ try {
110
+ const raw = readFileSync(settingsPath, 'utf-8')
111
+ const parsed = JSON.parse(raw) as { env?: Record<string, string> }
112
+ if (parsed.env) return parsed.env
113
+ } catch {
114
+ continue
115
+ }
109
116
  }
117
+ return {}
110
118
  }
111
119
 
112
120
  /**
@@ -167,11 +175,11 @@ export function applySafeConfigEnvironmentVariables(): void {
167
175
  )
168
176
  }
169
177
 
170
- // cc-haha provider isolation: apply env from ~/.claude/cc-haha/settings.json
171
- // AFTER userSettings so Haha-specific provider config takes priority over
172
- // the original Claude Code's settings. This prevents Haha from polluting
178
+ // bingo provider isolation: apply env from ~/.claude/bingo/settings.json
179
+ // AFTER userSettings so Bingo-specific provider config takes priority over
180
+ // the original Claude Code's settings. This prevents Bingo from polluting
173
181
  // ~/.claude/settings.json while still allowing it to override provider vars.
174
- Object.assign(process.env, filterSettingsEnv(getCcHahaSettingsEnv()))
182
+ Object.assign(process.env, filterSettingsEnv(getBingoSettingsEnv()))
175
183
 
176
184
  // Compute remote-managed-settings eligibility now, with userSettings and
177
185
  // flagSettings env applied. Eligibility reads CLAUDE_CODE_USE_BEDROCK,
@@ -214,9 +222,9 @@ export function applyConfigEnvironmentVariables(): void {
214
222
 
215
223
  Object.assign(process.env, filterSettingsEnv(getSettings_DEPRECATED()?.env))
216
224
 
217
- // cc-haha provider isolation: same as in applySafeConfigEnvironmentVariables,
218
- // apply Haha-specific env last so it overrides the original settings.
219
- Object.assign(process.env, filterSettingsEnv(getCcHahaSettingsEnv()))
225
+ // bingo provider isolation: same as in applySafeConfigEnvironmentVariables,
226
+ // apply Bingo-specific env last so it overrides the original settings.
227
+ Object.assign(process.env, filterSettingsEnv(getBingoSettingsEnv()))
220
228
 
221
229
  // Clear caches so agents are rebuilt with the new env vars
222
230
  clearCACertsCache()
@@ -202,7 +202,7 @@ export function PreflightStep(t0) {
202
202
 
203
203
  //@C:ID=F.PC._temp;K=F;V=1.0;P=Helper function for process exit;D=UI;M=Connectivity;S=Utility;In=void;Out=void
204
204
  function _temp() {
205
- console.log("F.PC._temp");
206
-
207
- return process.exit(1);
205
+ console.log("F.PC._temp: Skipping force exit on connectivity failure.");
206
+
207
+ // return process.exit(1);
208
208
  }
@@ -334,14 +334,26 @@ export function getDirectFetchOptions(): {
334
334
  return { ...base, proxy: undefined, ...getTLSFetchOptions() }
335
335
  }
336
336
 
337
+ // Check if system proxy exists
338
+ const proxyUrl = getProxyUrl()
339
+ if (!proxyUrl) {
340
+ // No proxy configured, just return normal fetch options
341
+ return { ...base, ...getTLSFetchOptions() }
342
+ }
343
+
337
344
  // In Node.js/undici, a fresh Agent with no proxy settings bypasses system defaults
338
345
  // eslint-disable-next-line @typescript-eslint/no-require-imports
339
346
  const undiciMod = require('undici') as typeof undici
340
347
  const tlsOpts = getTLSFetchOptions()
341
348
 
349
+ // Use the global dispatcher's options if possible, or fresh default options
350
+ const agentOptions = tlsOpts.dispatcher && 'options' in (tlsOpts.dispatcher as any)
351
+ ? (tlsOpts.dispatcher as any).options
352
+ : {}
353
+
342
354
  return {
343
355
  ...base,
344
- dispatcher: new undiciMod.Agent(tlsOpts.dispatcher ? (tlsOpts.dispatcher as any).options : {}),
356
+ dispatcher: new undiciMod.Agent(agentOptions),
345
357
  }
346
358
  }
347
359