@simonyea/holysheep-cli 1.7.130 → 1.7.132

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.7.130",
3
+ "version": "1.7.132",
4
4
  "description": "Claude Code/Cursor/Cline API relay for China — ¥1=$1, WeChat/Alipay payment, no credit card, no VPN. One command setup for all AI coding tools.",
5
5
  "keywords": [
6
6
  "openai-china",
@@ -68,22 +68,19 @@ async function runClaude(args = []) {
68
68
  claudeCodeTool.writeSettings(settings)
69
69
  }
70
70
 
71
- const { server, port, sessionId } = await startProcessProxy({})
72
- const proxyUrl = getLocalProxyUrl(port)
73
71
  const runtime = typeof claudeCodeTool.detectClaudeRuntime === 'function'
74
72
  ? claudeCodeTool.detectClaudeRuntime()
75
73
  : { kind: 'unknown', launchMode: 'env-proxy' }
74
+ const { server, port, sessionId } = await startProcessProxy({})
75
+ const proxyUrl = getLocalProxyUrl(port)
76
76
  const launchMode = runtime.launchMode === 'node-inject'
77
77
  ? 'local-api + connect-fallback + node-inject'
78
- : 'local-api + connect-fallback'
78
+ : 'whole-process-proxy + local-api'
79
79
 
80
80
  const env = {
81
81
  ...process.env,
82
82
  ANTHROPIC_API_KEY: undefined,
83
83
  ANTHROPIC_AUTH_TOKEN: apiKey,
84
- // Route ALL Anthropic API traffic exclusively through ANTHROPIC_BASE_URL.
85
- // HTTP(S)_PROXY must NOT be set: it causes Claude Code to open CONNECT
86
- // tunnels to api.anthropic.com, bypassing CRS multi-account scheduling.
87
84
  ANTHROPIC_BASE_URL: proxyUrl,
88
85
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
89
86
  HOLYSHEEP_CLAUDE_PROCESS_PROXY: '1',
@@ -91,10 +88,12 @@ async function runClaude(args = []) {
91
88
  HS_PROXY_URL: proxyUrl,
92
89
  HOLYSHEEP_CLAUDE_RUNTIME_KIND: runtime.kind || 'unknown',
93
90
  HOLYSHEEP_CLAUDE_LAUNCH_MODE: launchMode,
94
- HTTP_PROXY: undefined,
95
- HTTPS_PROXY: undefined,
96
- ALL_PROXY: undefined,
97
- NO_PROXY: undefined,
91
+ HTTP_PROXY: proxyUrl,
92
+ HTTPS_PROXY: proxyUrl,
93
+ ALL_PROXY: proxyUrl,
94
+ NO_PROXY: mergeNoProxy(process.env.NO_PROXY, ['127.0.0.1', 'localhost']),
95
+ ANTHROPIC_MAX_RETRIES: '0',
96
+ MAX_RETRIES: '0',
98
97
  }
99
98
 
100
99
  // 不再写 settings.json — 只用 env 变量,避免 Claude Code 从两个来源
@@ -47,6 +47,39 @@ function getControlPlaneUrl(config) {
47
47
 
48
48
  const leaseCache = new Map()
49
49
  const MAX_PROXY_RETRIES = 3
50
+ const SLOW_PATH_LOG_MS = Number(process.env.HS_CLAUDE_SLOW_PATH_LOG_MS) || 5000
51
+
52
+ function sanitizeUrl(value) {
53
+ if (!value) return ''
54
+ try {
55
+ const url = new URL(String(value))
56
+ return `${url.protocol}//${url.host}${url.pathname}`
57
+ } catch {
58
+ return String(value)
59
+ }
60
+ }
61
+
62
+ function logProxyTiming(event, details = {}) {
63
+ const payload = Object.fromEntries(
64
+ Object.entries(details).filter(([, value]) => value !== undefined && value !== null && value !== '')
65
+ )
66
+ console.error(`[hs-claude-proxy] ${event} ${JSON.stringify(payload)}`)
67
+ }
68
+
69
+ function createForwardTrace({ clientReq, targetUrl, nodeProxyUrl, sessionId, lease, attempt, isDirect }) {
70
+ return {
71
+ requestId: crypto.randomUUID().slice(0, 8),
72
+ sessionId,
73
+ nodeId: lease?.nodeId || '',
74
+ attempt,
75
+ isDirect,
76
+ method: clientReq?.method || '',
77
+ target: sanitizeUrl(targetUrl),
78
+ nodeProxy: sanitizeUrl(nodeProxyUrl),
79
+ leaseOpenMs: lease?._hsLeaseOpenMs,
80
+ leaseAgeMs: lease?._hsLeaseOpenedAt ? Date.now() - lease._hsLeaseOpenedAt : undefined,
81
+ }
82
+ }
50
83
 
51
84
  function createForwardError(statusCode, body) {
52
85
  const error = new Error(`HTTP ${statusCode}: ${String(body || '').slice(0, 200)}`)
@@ -55,6 +88,16 @@ function createForwardError(statusCode, body) {
55
88
  return error
56
89
  }
57
90
 
91
+ function createClientValidationErrorBody(message) {
92
+ return JSON.stringify({
93
+ type: 'error',
94
+ error: {
95
+ type: 'client_validation_error',
96
+ message,
97
+ },
98
+ })
99
+ }
100
+
58
101
  function shouldRefreshLeaseAfterError(err) {
59
102
  const statusCode = Number(err?.statusCode || 0)
60
103
  const body = String(err?.body || err?.message || '')
@@ -87,6 +130,8 @@ function isRetryableNodeLeaseError(err) {
87
130
  'HTTP 503',
88
131
  'HTTP 504',
89
132
  'client_validation_error',
133
+ 'upstream response timeout',
134
+ 'upstream stream stalled',
90
135
  '不可用'
91
136
  ].some((token) => message.includes(token))
92
137
  }
@@ -107,6 +152,7 @@ async function fetchFreshLease(config, sessionId, options = {}) {
107
152
  const controlPlaneUrl = getControlPlaneUrl(config)
108
153
  if (!controlPlaneUrl) throw new Error('Claude relay control plane is not configured')
109
154
 
155
+ const startedAt = Date.now()
110
156
  const response = await fetch(`${controlPlaneUrl}/session/open`, {
111
157
  method: 'POST',
112
158
  headers: { 'content-type': 'application/json' },
@@ -124,6 +170,17 @@ async function fetchFreshLease(config, sessionId, options = {}) {
124
170
  if (!response.ok || !payload?.success || !payload?.data?.ticket) {
125
171
  throw new Error(payload?.error?.message || `Failed to open Claude session (HTTP ${response.status})`)
126
172
  }
173
+ const openedAt = Date.now()
174
+ payload.data._hsLeaseOpenMs = openedAt - startedAt
175
+ payload.data._hsLeaseOpenedAt = openedAt
176
+ if (payload.data._hsLeaseOpenMs >= SLOW_PATH_LOG_MS) {
177
+ logProxyTiming('lease.open', {
178
+ sessionId,
179
+ nodeId: payload.data.nodeId || '',
180
+ leaseOpenMs: payload.data._hsLeaseOpenMs,
181
+ forceReassign: options.forceReassign === true,
182
+ })
183
+ }
127
184
  leaseCache.set(sessionId, payload.data)
128
185
  return payload.data
129
186
  }
@@ -158,14 +215,110 @@ function deriveNodeProxyUrl(lease) {
158
215
  return upstream.toString().replace(/\/+$/, '')
159
216
  }
160
217
 
161
- function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, extraHeaders = {} }) {
218
+ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, extraHeaders = {}, trace = null }) {
162
219
  const upstream = new URL(nodeProxyUrl)
163
220
  return new Promise((resolve, reject) => {
164
- const forwardReq = http.request({
221
+ const requestStartedAt = Date.now()
222
+ const resolvedTrace = trace || createForwardTrace({
223
+ clientReq,
224
+ targetUrl,
225
+ nodeProxyUrl,
226
+ sessionId: clientReq.headers['x-hs-session-id'] || '',
227
+ lease: null,
228
+ attempt: 0,
229
+ isDirect: !String(clientReq.url || '').startsWith('http'),
230
+ })
231
+ let settled = false
232
+ let responseTimer = null
233
+ let stallTimer = null
234
+ let sawUpstreamResponse = false
235
+ let sawUpstreamData = false
236
+ let forwardReq = null
237
+ let upstreamAssignedAt = null
238
+ let firstByteAt = null
239
+
240
+ const clearResponseTimer = () => {
241
+ if (responseTimer) {
242
+ clearTimeout(responseTimer)
243
+ responseTimer = null
244
+ }
245
+ }
246
+
247
+ const clearStallTimer = () => {
248
+ if (stallTimer) {
249
+ clearTimeout(stallTimer)
250
+ stallTimer = null
251
+ }
252
+ }
253
+
254
+ const clearTimers = () => {
255
+ clearResponseTimer()
256
+ clearStallTimer()
257
+ }
258
+
259
+ const finish = () => {
260
+ if (settled) return
261
+ settled = true
262
+ clearTimers()
263
+ const finishedAt = Date.now()
264
+ const totalMs = finishedAt - requestStartedAt
265
+ if (totalMs >= SLOW_PATH_LOG_MS) {
266
+ logProxyTiming('request.complete', {
267
+ ...resolvedTrace,
268
+ totalMs,
269
+ connectMs: upstreamAssignedAt ? upstreamAssignedAt - requestStartedAt : undefined,
270
+ firstByteMs: firstByteAt ? firstByteAt - requestStartedAt : undefined,
271
+ streamMs: firstByteAt ? finishedAt - firstByteAt : undefined,
272
+ bytesStarted: sawUpstreamData,
273
+ })
274
+ }
275
+ resolve()
276
+ }
277
+
278
+ const failWithAnthropicError = (message) => {
279
+ if (settled) return
280
+ settled = true
281
+ clearTimers()
282
+ const failedAt = Date.now()
283
+ logProxyTiming('request.fail', {
284
+ ...resolvedTrace,
285
+ totalMs: failedAt - requestStartedAt,
286
+ connectMs: upstreamAssignedAt ? upstreamAssignedAt - requestStartedAt : undefined,
287
+ firstByteMs: firstByteAt ? firstByteAt - requestStartedAt : undefined,
288
+ bytesStarted: sawUpstreamData,
289
+ error: message,
290
+ })
291
+ if (forwardReq && !forwardReq.destroyed) {
292
+ forwardReq.destroy()
293
+ }
294
+ const error = createForwardError(400, createClientValidationErrorBody(message))
295
+ error.message = message
296
+ reject(error)
297
+ }
298
+
299
+ const fail = (error) => {
300
+ const err = error instanceof Error ? error : new Error(String(error || 'Proxy error'))
301
+ return failWithAnthropicError(err.message || 'Proxy error')
302
+ }
303
+
304
+ const armResponseTimer = () => {
305
+ if (settled || sawUpstreamResponse || responseTimer) return
306
+ responseTimer = setTimeout(() => failWithAnthropicError('upstream response timeout'), RESPONSE_TIMEOUT_MS)
307
+ }
308
+
309
+ const armStallTimer = () => {
310
+ if (settled) return
311
+ sawUpstreamData = true
312
+ clearStallTimer()
313
+ stallTimer = setTimeout(() => failWithAnthropicError('upstream stream stalled'), STALL_TIMEOUT_MS)
314
+ }
315
+
316
+ forwardReq = http.request({
165
317
  host: upstream.hostname,
166
318
  port: Number(upstream.port || 80),
167
319
  method: clientReq.method,
168
320
  path: targetUrl.toString(),
321
+ agent: false,
169
322
  headers: {
170
323
  ...clientReq.headers,
171
324
  ...extraHeaders,
@@ -173,22 +326,68 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
173
326
  connection: 'close',
174
327
  },
175
328
  }, (forwardRes) => {
329
+ sawUpstreamResponse = true
330
+ clearResponseTimer()
331
+
176
332
  const status = forwardRes.statusCode || 502
177
333
  if (status === 403 || status === 502 || status === 503) {
178
- // 收集响应体用于错误消息,不透传给客户端
179
334
  const chunks = []
180
335
  forwardRes.on('data', (c) => chunks.push(c))
181
336
  forwardRes.on('end', () => {
337
+ if (settled) return
338
+ settled = true
339
+ clearTimers()
182
340
  const body = Buffer.concat(chunks).toString('utf8')
183
- reject(createForwardError(status, body))
341
+ const error = createForwardError(status, body)
342
+ logProxyTiming('request.upstream_error', {
343
+ ...resolvedTrace,
344
+ status,
345
+ totalMs: Date.now() - requestStartedAt,
346
+ body: String(body || '').slice(0, 200),
347
+ })
348
+ reject(error)
184
349
  })
350
+ forwardRes.on('error', fail)
185
351
  return
186
352
  }
353
+
354
+ upstreamAssignedAt = upstreamAssignedAt || Date.now()
187
355
  clientRes.writeHead(status, forwardRes.headers)
356
+ forwardRes.on('data', () => {
357
+ if (!firstByteAt) {
358
+ firstByteAt = Date.now()
359
+ logProxyTiming('request.first_byte', {
360
+ ...resolvedTrace,
361
+ firstByteMs: firstByteAt - requestStartedAt,
362
+ })
363
+ }
364
+ armStallTimer()
365
+ })
366
+ forwardRes.once('end', () => {
367
+ clearStallTimer()
368
+ finish()
369
+ })
370
+ forwardRes.once('close', () => {
371
+ clearStallTimer()
372
+ finish()
373
+ })
374
+ forwardRes.once('error', fail)
188
375
  forwardRes.pipe(clientRes)
189
- resolve()
190
376
  })
191
- forwardReq.once('error', reject)
377
+
378
+ forwardReq.on('socket', (socket) => {
379
+ upstreamAssignedAt = upstreamAssignedAt || Date.now()
380
+ socket.once('connect', () => {
381
+ upstreamAssignedAt = upstreamAssignedAt || Date.now()
382
+ })
383
+ })
384
+ forwardReq.setTimeout(RESPONSE_TIMEOUT_MS, () => {
385
+ if (!sawUpstreamResponse) failWithAnthropicError('upstream response timeout')
386
+ })
387
+ forwardReq.once('error', fail)
388
+ clientReq.once('aborted', () => finish())
389
+
390
+ armResponseTimer()
192
391
  clientReq.pipe(forwardReq)
193
392
  })
194
393
  }
@@ -202,6 +401,7 @@ function createConnectTunnel(proxyUrl, target, headers) {
202
401
  method: 'CONNECT',
203
402
  path: target,
204
403
  headers,
404
+ agent: false,
205
405
  timeout: 15000, // CONNECT 握手 15 秒超时
206
406
  })
207
407
 
@@ -221,11 +421,11 @@ function createConnectTunnel(proxyUrl, target, headers) {
221
421
  })
222
422
  }
223
423
 
224
- // 等上游返回 first byte 的最大时间。Claude 长上下文(>200K tokens)的 prefill 时间
225
- // 可以达到 30-90 秒,120 秒留了充足裕量。默认 120 秒,可通过环境变量
226
- // HS_CLAUDE_RESPONSE_TIMEOUT_MS 覆盖。旧值 300 秒导致异常时用户感知卡 5 分钟。
227
- const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) || 120000
228
- // 上游 stream 已开始后,相邻两个 chunk 之间的最大间隔(覆盖 extended thinking 间歇)。
424
+ // 等上游返回 first byte 的最大时间。
425
+ // 这里不能默认等 5 分钟,否则上游/代理链路卡死时,用户会感觉每次下一条请求都被“挂住”。
426
+ // 默认快速失败为 30 秒,仍可通过环境变量覆盖。
427
+ const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) || 30000
428
+ // 上游 stream 已开始后,相邻两个 chunk 之间的最大间隔。
229
429
  const STALL_TIMEOUT_MS = Number(process.env.HS_CLAUDE_STALL_TIMEOUT_MS) || 120000
230
430
 
231
431
  function pipeWithCleanup(a, b) {
@@ -233,49 +433,61 @@ function pipeWithCleanup(a, b) {
233
433
  if (typeof sock.setKeepAlive === 'function') sock.setKeepAlive(true, 10000)
234
434
  }
235
435
 
236
- // 双阶段超时:
237
- // 1. 客户端发了数据,上游 RESPONSE_TIMEOUT_MS 没回第一个字节 → 断开(node proxy 挂了)
238
- // 2. 上游在流数据突然停了 STALL_TIMEOUT_MS → 断开(stream 中断)
239
436
  let timer = null
437
+ let closed = false
240
438
  let streaming = false
241
- const kill = (reason) => {
242
- if (timer) clearTimeout(timer)
243
- a.destroy(new Error(reason))
244
- b.destroy(new Error(reason))
245
- }
246
- a.on('data', () => {
247
- // 客户端发了数据(请求),等上游响应
248
- if (!streaming) {
249
- if (timer) clearTimeout(timer)
250
- timer = setTimeout(() => kill('upstream response timeout'), RESPONSE_TIMEOUT_MS)
439
+
440
+ const clearTimer = () => {
441
+ if (timer) {
442
+ clearTimeout(timer)
443
+ timer = null
251
444
  }
252
- })
253
- b.on('data', () => {
254
- // 上游回数据了,切换到 stream 模式
445
+ }
446
+
447
+ const destroySocket = (sock, err) => {
448
+ if (!sock || sock.destroyed) return
449
+ if (err) sock.destroy(err)
450
+ else sock.destroy()
451
+ }
452
+
453
+ const close = (reason) => {
454
+ if (closed) return
455
+ closed = true
456
+ clearTimer()
457
+ const err = reason ? new Error(reason) : null
458
+ destroySocket(a, err)
459
+ destroySocket(b, err)
460
+ }
461
+
462
+ const armResponseTimer = () => {
463
+ if (streaming || closed || timer) return
464
+ timer = setTimeout(() => close('upstream response timeout'), RESPONSE_TIMEOUT_MS)
465
+ }
466
+
467
+ const armStallTimer = () => {
468
+ if (closed) return
255
469
  streaming = true
256
- // 每次收到数据重置 stall 超时
257
- if (timer) clearTimeout(timer)
258
- timer = setTimeout(() => kill('upstream stream stalled'), STALL_TIMEOUT_MS)
259
- })
470
+ clearTimer()
471
+ timer = setTimeout(() => close('upstream stream stalled'), STALL_TIMEOUT_MS)
472
+ }
473
+
474
+ b.on('data', armStallTimer)
260
475
 
476
+ armResponseTimer()
261
477
  a.pipe(b)
262
478
  b.pipe(a)
263
- const close = () => {
264
- if (timer) clearTimeout(timer)
265
- a.destroy()
266
- b.destroy()
267
- }
268
- a.once('error', close)
269
- b.once('error', close)
270
- a.once('close', close)
271
- b.once('close', close)
479
+
480
+ a.once('error', () => close())
481
+ b.once('error', () => close())
482
+ a.once('close', () => close())
483
+ b.once('close', () => close())
272
484
  }
273
485
 
274
- function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
486
+ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH, allowAnthropicConnect = false }) {
275
487
  const server = http.createServer(async (clientReq, clientRes) => {
276
488
  const isDirect = !clientReq.url.startsWith('http')
277
489
 
278
- const doForward = async (lease) => {
490
+ const doForward = async (lease, attempt) => {
279
491
  const config = readConfig(configPath)
280
492
  const nodeProxyUrl = deriveNodeProxyUrl(lease)
281
493
 
@@ -288,19 +500,38 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
288
500
  clientReq,
289
501
  clientRes,
290
502
  extraHeaders: buildAuthHeaders(config, lease),
503
+ trace: createForwardTrace({
504
+ clientReq,
505
+ targetUrl: target,
506
+ nodeProxyUrl,
507
+ sessionId,
508
+ lease,
509
+ attempt,
510
+ isDirect,
511
+ }),
291
512
  })
292
513
  }
293
514
 
515
+ const targetUrl = new URL(clientReq.url)
294
516
  const headers = {
295
517
  ...buildAuthHeaders(config, lease),
296
- host: new URL(clientReq.url).host,
518
+ host: targetUrl.host,
297
519
  }
298
520
  return forwardViaNodeProxy({
299
521
  nodeProxyUrl,
300
- targetUrl: new URL(clientReq.url),
522
+ targetUrl,
301
523
  clientReq,
302
524
  clientRes,
303
525
  extraHeaders: headers,
526
+ trace: createForwardTrace({
527
+ clientReq,
528
+ targetUrl,
529
+ nodeProxyUrl,
530
+ sessionId,
531
+ lease,
532
+ attempt,
533
+ isDirect,
534
+ }),
304
535
  })
305
536
  }
306
537
 
@@ -308,7 +539,7 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
308
539
  for (let attempt = 0; attempt <= MAX_PROXY_RETRIES; attempt++) {
309
540
  try {
310
541
  if (attempt === 0) {
311
- await doForward(getCachedLease(sessionId))
542
+ await doForward(getCachedLease(sessionId), attempt)
312
543
  } else {
313
544
  const config = readConfig(configPath)
314
545
  leaseCache.delete(sessionId)
@@ -318,12 +549,19 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
318
549
  const freshLease = await fetchFreshLease(config, sessionId, {
319
550
  forceReassign: shouldRefreshLeaseAfterError(lastError),
320
551
  })
321
- await doForward(freshLease)
552
+ await doForward(freshLease, attempt)
322
553
  }
323
554
  lastError = null
324
555
  break
325
556
  } catch (err) {
326
557
  lastError = err
558
+ logProxyTiming('request.retry', {
559
+ sessionId,
560
+ attempt,
561
+ error: String(err?.message || err),
562
+ retryable: isRetryableNodeLeaseError(err),
563
+ forceReassign: shouldRefreshLeaseAfterError(err),
564
+ })
327
565
  if (clientRes.headersSent) return
328
566
  if (!isRetryableNodeLeaseError(err) && attempt > 0) break
329
567
  }
@@ -341,7 +579,7 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
341
579
 
342
580
  // Hosts that must NOT be tunneled directly — they must go through CRS via
343
581
  // the HTTP path (ANTHROPIC_BASE_URL), not a CONNECT tunnel that bypasses it.
344
- const BLOCKED_CONNECT = new Set(['api.anthropic.com'])
582
+ const BLOCKED_CONNECT = allowAnthropicConnect ? new Set() : new Set(['api.anthropic.com'])
345
583
 
346
584
  server.on('connect', async (req, clientSocket, head) => {
347
585
  const target = String(req.url || '').trim()
@@ -408,7 +646,7 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
408
646
  return server
409
647
  }
410
648
 
411
- async function startProcessProxy({ port = null, sessionId = null, configPath = CONFIG_PATH } = {}) {
649
+ async function startProcessProxy({ port = null, sessionId = null, configPath = CONFIG_PATH, allowAnthropicConnect = false } = {}) {
412
650
  const config = readConfig(configPath)
413
651
  const preferredPort = port || getProcessProxyPort(config)
414
652
  const effectiveSessionId = sessionId || crypto.randomUUID()
@@ -416,7 +654,7 @@ async function startProcessProxy({ port = null, sessionId = null, configPath = C
416
654
  // 启动时拿一次 lease,之后靠被动重试维持,不再主动续约
417
655
  await fetchFreshLease(config, effectiveSessionId)
418
656
 
419
- const server = createProcessProxyServer({ sessionId: effectiveSessionId, configPath })
657
+ const server = createProcessProxyServer({ sessionId: effectiveSessionId, configPath, allowAnthropicConnect })
420
658
 
421
659
  return new Promise((resolve, reject) => {
422
660
  const tryListen = (p) => {