dsh-packycode-api 1.0.4 → 1.0.6

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/lib/index.js CHANGED
@@ -41,8 +41,17 @@ const DEFAULT_API_HOST = "https://cf.api.fan";
41
41
  /**
42
42
  * Host carrying `/api/usage/token/`. `cf.api.fan` answers 404 for the whole
43
43
  * `/api/*` family, so per-key quota is read from the PackyAPI host instead.
44
+ * PackyAPI now runs on `.ai` (its site and docs moved there); the `.com` name
45
+ * is DNS-poisoned on mainland networks, so a host whose fetch does not go
46
+ * through a proxy reads `fetch failed` from it.
44
47
  */
45
- const DEFAULT_STATUS_HOST = "https://www.packyapi.com";
48
+ const DEFAULT_STATUS_HOST = "https://www.packyapi.ai";
49
+ /**
50
+ * Earlier default status hosts. A profile that saved one keeps working, but
51
+ * the current default is tried before it, so an unreachable legacy name
52
+ * cannot hold a read up for its whole timeout first.
53
+ */
54
+ const LEGACY_STATUS_HOSTS = Object.freeze(["https://www.packyapi.com"]);
46
55
  /** PackyAPI quotes balances in quota units of 1/500000 USD (new-api convention). */
47
56
  const QUOTA_PER_USD = 5e5;
48
57
  /** Balance cache lifetime; a forced read bypasses it. */
@@ -61,7 +70,8 @@ const DEFAULT_REASONING_LEVEL = "high";
61
70
  * the account. Only the second family exists on the model host (`cf.api.fan`
62
71
  * answers 404 for `/api/*`), so a read tries the configured host first and
63
72
  * falls back to the PackyAPI host — the same fallback the PackyCode status
64
- * tooling uses.
73
+ * tooling uses. A saved legacy status host (`www.packyapi.com`, DNS-poisoned
74
+ * on mainland networks) is tried after the current default, not before it.
65
75
  *
66
76
  * Credentials and response bodies stay host-side; the browser receives only
67
77
  * the projection built here.
@@ -139,22 +149,71 @@ function parseBillingUsage(payload) {
139
149
  function statusMessage(status) {
140
150
  if (status === 401) return "this API key was rejected (401)";
141
151
  if (status === 402 || status === 403) return "the account cannot be read with this key (402/403)";
152
+ if (status === 429) return "the gateway is rate limiting balance reads (429)";
142
153
  return `the gateway answered HTTP ${status}`;
143
154
  }
155
+ /**
156
+ * Say why a request never got an answer. Node's fetch reports every transport
157
+ * fault as a bare `fetch failed`; the cause carries the part a reader can act
158
+ * on (a DNS failure, a refused or reset connection, a certificate problem).
159
+ */
160
+ function transportMessage(error, timeoutSignal, timeoutMs) {
161
+ if (timeoutSignal.aborted) return `no answer within ${Math.round(timeoutMs / 1e3)}s`;
162
+ const cause = error?.cause;
163
+ const code = typeof cause?.code === "string" ? cause.code : typeof error?.code === "string" ? error.code : void 0;
164
+ const message = error instanceof Error ? error.message : String(error);
165
+ return code === void 0 ? message : `${message} (${code})`;
166
+ }
144
167
  async function getJson(url, key, { fetchImpl, timeoutMs, signal }) {
145
168
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
146
169
  const requestSignal = signal === void 0 ? timeoutSignal : AbortSignal.any([signal, timeoutSignal]);
147
- const response = await fetchImpl(url, {
148
- headers: {
149
- Authorization: `Bearer ${key}`,
150
- Accept: "application/json"
151
- },
152
- redirect: "error",
153
- signal: requestSignal
154
- });
170
+ let response;
171
+ try {
172
+ response = await fetchImpl(url, {
173
+ headers: {
174
+ Authorization: `Bearer ${key}`,
175
+ Accept: "application/json"
176
+ },
177
+ redirect: "error",
178
+ signal: requestSignal
179
+ });
180
+ } catch (error) {
181
+ if (signal?.aborted === true) throw error;
182
+ throw new Error(transportMessage(error, timeoutSignal, timeoutMs), { cause: error });
183
+ }
155
184
  if (!response.ok) throw new Error(statusMessage(response.status));
156
185
  return response.json();
157
186
  }
187
+ /**
188
+ * The status hosts one per-key read tries, in order: an explicit override,
189
+ * the current default, the legacy defaults, then the model hosts.
190
+ * @param hosts - the resolved host settings.
191
+ * @returns unique origins.
192
+ */
193
+ function statusHostOrder(hosts) {
194
+ const configured = hosts?.statusHost;
195
+ const override = configured === void 0 || LEGACY_STATUS_HOSTS.includes(configured) ? [] : [configured];
196
+ return [...new Set([
197
+ ...override,
198
+ DEFAULT_STATUS_HOST,
199
+ ...LEGACY_STATUS_HOSTS,
200
+ hosts?.apiHost,
201
+ DEFAULT_API_HOST
202
+ ].filter((host) => typeof host === "string" && host !== ""))];
203
+ }
204
+ /**
205
+ * The hosts the account read tries: the model hosts first (they serve the
206
+ * billing endpoints), then the status hosts.
207
+ * @param hosts - the resolved host settings.
208
+ * @returns unique origins.
209
+ */
210
+ function accountHostOrder(hosts) {
211
+ return [...new Set([
212
+ hosts?.apiHost,
213
+ DEFAULT_API_HOST,
214
+ ...statusHostOrder(hosts)
215
+ ].filter((host) => typeof host === "string" && host !== ""))];
216
+ }
158
217
  async function withFallback(hosts, path, key, options) {
159
218
  const tried = [];
160
219
  for (const host of hosts) try {
@@ -169,6 +228,12 @@ async function withFallback(hosts, path, key, options) {
169
228
  }
170
229
  /**
171
230
  * Cached, single-flight balance reader.
231
+ *
232
+ * A read that fails for one key — the gateway throttled it, a host was
233
+ * unreachable — keeps showing the last balance that key did report, marked
234
+ * `stale` with when it was read and why this read failed, instead of blanking
235
+ * the row. The same holds for the account figures. These last-good values
236
+ * live in the host process: a page refresh keeps them, a restart does not.
172
237
  * @param options - key listing, host resolution, and transport knobs.
173
238
  * @returns `{ read({force, signal}), invalidate() }`.
174
239
  */
@@ -178,13 +243,11 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = fetch, now = Date
178
243
  let cached;
179
244
  let inFlight;
180
245
  let generation = 0;
246
+ /** Last successful per-key usage, by key id, and the last account figures. */
247
+ const lastGood = /* @__PURE__ */ new Map();
248
+ let lastAccount;
181
249
  const loadKey = async (entry, hosts, signal) => {
182
- const { payload } = await withFallback([.../* @__PURE__ */ new Set([
183
- hosts.statusHost,
184
- DEFAULT_STATUS_HOST,
185
- hosts.apiHost,
186
- DEFAULT_API_HOST
187
- ])], "/api/usage/token/", entry.key, {
250
+ const { payload } = await withFallback(statusHostOrder(hosts), "/api/usage/token/", entry.key, {
188
251
  fetchImpl,
189
252
  timeoutMs,
190
253
  signal
@@ -207,12 +270,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = fetch, now = Date
207
270
  ...keys.filter((entry) => entry.disabled !== true).map((entry) => entry.id)
208
271
  ])].map((id) => keys.find((entry) => entry.id === id)).filter((entry) => entry !== void 0);
209
272
  if (candidates.length === 0) throw new Error("add a PackyCode API key to read the account balance");
210
- const hostsToTry = [.../* @__PURE__ */ new Set([
211
- hosts.apiHost,
212
- DEFAULT_API_HOST,
213
- hosts.statusHost,
214
- DEFAULT_STATUS_HOST
215
- ])];
273
+ const hostsToTry = accountHostOrder(hosts);
216
274
  const options = {
217
275
  fetchImpl,
218
276
  timeoutMs,
@@ -239,6 +297,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = fetch, now = Date
239
297
  signal?.throwIfAborted();
240
298
  const hosts = getHosts();
241
299
  const keys = await listKeys();
300
+ for (const id of lastGood.keys()) if (!keys.some((entry) => entry.id === id)) lastGood.delete(id);
242
301
  const entries = [];
243
302
  const usableIds = [];
244
303
  for (const entry of keys) {
@@ -248,23 +307,50 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = fetch, now = Date
248
307
  disabled: entry.disabled === true
249
308
  };
250
309
  try {
310
+ const usage = await loadKey(entry, hosts, signal);
311
+ lastGood.set(entry.id, {
312
+ usage,
313
+ fetchedAt: now()
314
+ });
251
315
  entries.push({
252
316
  ...base,
253
- usage: await loadKey(entry, hosts, signal)
317
+ usage
254
318
  });
255
319
  usableIds.push(entry.id);
256
320
  } catch (error) {
257
- entries.push({
321
+ signal?.throwIfAborted();
322
+ const message = error instanceof Error ? error.message : String(error);
323
+ const previous = lastGood.get(entry.id);
324
+ entries.push(previous === void 0 ? {
325
+ ...base,
326
+ error: message
327
+ } : {
258
328
  ...base,
259
- error: error instanceof Error ? error.message : String(error)
329
+ usage: previous.usage,
330
+ stale: {
331
+ fetchedAt: previous.fetchedAt,
332
+ error: message
333
+ }
260
334
  });
261
335
  }
262
336
  }
263
337
  let account;
264
338
  try {
265
339
  account = await loadAccount(keys, hosts, signal, usableIds);
340
+ lastAccount = {
341
+ account,
342
+ fetchedAt: now()
343
+ };
266
344
  } catch (error) {
267
- account = { error: error instanceof Error ? error.message : String(error) };
345
+ signal?.throwIfAborted();
346
+ const message = error instanceof Error ? error.message : String(error);
347
+ account = lastAccount === void 0 || !keys.some((entry) => entry.id === lastAccount.account.keyId) ? { error: message } : {
348
+ ...lastAccount.account,
349
+ stale: {
350
+ fetchedAt: lastAccount.fetchedAt,
351
+ error: message
352
+ }
353
+ };
268
354
  }
269
355
  return {
270
356
  keys: entries,
@@ -302,6 +388,147 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = fetch, now = Date
302
388
  });
303
389
  }
304
390
  //#endregion
391
+ //#region src/failures.js
392
+ /**
393
+ * Failure classification for the two conditions that move the key ring along.
394
+ *
395
+ * The gateway reports an exhausted key and a rejected key in Chinese, and the
396
+ * harness classifies failures by fixed English/status wording it recognizes
397
+ * (`isQuotaExceededError`, `\b401\b`). Normalizing here is what makes the
398
+ * harness's retry policy re-run the step — and therefore what lets the retry
399
+ * pick up the next key. Rewriting is honest about what happened: this plugin
400
+ * knows the key's condition from the gateway's own words plus its balance
401
+ * reader, so the sentence it substitutes says no more than that.
402
+ *
403
+ * @module dsh-packycode-api/failures
404
+ */
405
+ /** Terminal quota, balance, or credit wording, in the gateway's own languages. */
406
+ const QUOTA_WORDING = /额度|余额|配额|欠费|insufficient[\s_-]+(?:quota|balance|credits?)|quota[\s_-]+(?:exceeded|exhausted|reached)|out[\s_-]+of[\s_-]+(?:credits?|budget)/iu;
407
+ /** A rejected key names itself; a bare 403 does not, and must not disable one. */
408
+ const INVALID_KEY_WORDING = /无效的令牌|令牌无效|无效密钥|密钥无效|invalid[\s_-]+api[\s_-]*key|invalid_authentication|api key is invalid/iu;
409
+ const INVALID_KEY_STATUS = /\b401\b/;
410
+ /**
411
+ * A gateway group that only serves its vendor's official client. These arrive
412
+ * as 403, which the harness reads as an authentication failure and renders as
413
+ * an invalid-API-key problem — the opposite of what happened, and a condition
414
+ * no retry can clear.
415
+ */
416
+ const CLIENT_POLICY_WORDING = /only accessible via the official|only accepts requests from the official|official\s+claude(?:\s+code)?\s+cli|官方\s*claude(?:\s*code)?\s*cli|请使用标准\s*codex\s*客户端|客户端存在异常|二次分发|API\s*转接/iu;
417
+ /** The key's quota is exhausted; move to the next key. */
418
+ const QUOTA_FAILURE = "quota";
419
+ /** The gateway rejected the key itself; move to the next key. */
420
+ const INVALID_KEY_FAILURE = "invalid-key";
421
+ /**
422
+ * Whether the gateway is refusing this client rather than this credential.
423
+ * @param message - provider failure text, as pi-ai carries it.
424
+ * @returns true when the route is gated to a vendor's official client.
425
+ */
426
+ function classifyClientPolicy(message) {
427
+ return CLIENT_POLICY_WORDING.test(typeof message === "string" ? message : "");
428
+ }
429
+ /**
430
+ * Classify a gateway failure message.
431
+ *
432
+ * Quota wording wins over a status token: an exhausted balance is often
433
+ * reported as 402/403, and only the key's own rejection may read as 401. A
434
+ * client-policy refusal is not a key fault and is classified separately.
435
+ * @param message - provider failure text, as pi-ai carries it.
436
+ * @returns the failure kind, or undefined when the key is not at fault.
437
+ */
438
+ function classifyGatewayFailure(message) {
439
+ const text = typeof message === "string" ? message : "";
440
+ if (text === "") return void 0;
441
+ if (classifyClientPolicy(text)) return void 0;
442
+ if (QUOTA_WORDING.test(text)) return QUOTA_FAILURE;
443
+ if (INVALID_KEY_WORDING.test(text)) return INVALID_KEY_FAILURE;
444
+ if (INVALID_KEY_STATUS.test(text)) return INVALID_KEY_FAILURE;
445
+ }
446
+ /**
447
+ * Rewrite a failure message so the harness classifies it as quota or auth.
448
+ *
449
+ * A bare status token is neutralized first: an exhausted balance arrives as
450
+ * 402 or 403 on some gateway routes, and the harness's auth rule matches
451
+ * 401/403 before it reads any wording, which would send the failure into a
452
+ * category the retry policy does not re-run.
453
+ * @param message - the original provider failure text.
454
+ * @param kind - the classification of {@link classifyGatewayFailure}.
455
+ * @returns text the harness routes to QUOTA or AUTH.
456
+ */
457
+ /**
458
+ * Explain a client-policy refusal so the user reads the real cause.
459
+ *
460
+ * The sentence must not read as any other failure the harness knows: it
461
+ * deliberately carries no HTTP status and none of the words the classifier
462
+ * matches for auth, quota, rate limits, timeouts, or transport faults. That
463
+ * keeps the failure out of the retry policy — no retry can talk a group into
464
+ * serving an API client — and lets the sentence reach the user verbatim, in
465
+ * place of the "API key is invalid" the bare 403 would have produced.
466
+ * @param keyName - the account name the balance reader saw, when known.
467
+ * @returns the replacement sentence.
468
+ */
469
+ function explainClientPolicyFailure() {
470
+ return "PackyCode 拒绝了这个客户端:所选分组只允许厂商官方客户端调用(Claude 分组要求官方 Claude CLI,Codex 分组要求标准 Codex 客户端)。这不是 API Key 失效,重试也不会成功。请改用允许 API 调用的分组(例如 cc-sale 或 claude-officially),或改用 PackyCode 的 gpt-6.1-sol / deepseek-flash。 / This group only serves the vendor’s official client; the API key is fine and retrying cannot help.";
471
+ }
472
+ /**
473
+ * The gateway is out of capacity for the moment: its Claude pool's
474
+ * per-account wait queue is full, or it throttled or shed the request. The
475
+ * CLI retries these on its own; this matches what is left when it gave up.
476
+ */
477
+ const CAPACITY_WORDING = /concurrency_queue_full|并发排队已满|wait queues? (?:are|is) full|负载已饱和|overloaded|rate[_\s-]?limit|API Error:\s*(?:429|502|503|504|529)\b/iu;
478
+ /**
479
+ * Whether a CLI failure is the gateway being busy rather than a fault of the
480
+ * key, the client, or the request.
481
+ * @param message - the CLI failure text.
482
+ * @returns true for throttling and capacity refusals.
483
+ */
484
+ function classifyCapacity(message) {
485
+ return CAPACITY_WORDING.test(typeof message === "string" ? message : "");
486
+ }
487
+ /**
488
+ * Explain a CLI turn that failed because the gateway stayed busy.
489
+ * @param message - the CLI failure text.
490
+ * @param retries - how many retries the CLI reported before giving up.
491
+ * @returns the replacement sentence, with the gateway's own words kept.
492
+ */
493
+ function explainCliCapacityFailure(message, retries) {
494
+ const detail = (typeof message === "string" ? message : "").replace(/^Claude Code CLI failed:\s*/u, "").slice(0, 600);
495
+ return `PackyCode 的 Claude 上游暂时繁忙(并发排队已满或被限流)。${typeof retries === "number" && retries > 0 ? `Claude Code CLI 已自动重试 ${retries} 次仍未成功。` : ""}这不是 API Key 或插件的问题,稍后再发一次即可;着急的话可以先切到 gpt-6.1-sol 或 deepseek-flash。 / PackyCode's Claude upstream is busy right now; retry shortly. Gateway said: ${detail}`;
496
+ }
497
+ /**
498
+ * Explain a client-policy refusal of a request the local Claude Code CLI made.
499
+ *
500
+ * The request did come from the official CLI, so the HTTP-route advice above
501
+ * (switch to a CLI) does not apply. The gateway's own words are kept, without
502
+ * their HTTP status, so a support request can quote the request ids.
503
+ * @param message - the CLI failure text.
504
+ * @returns the replacement sentence.
505
+ */
506
+ function explainCliClientPolicyFailure(message) {
507
+ return `PackyCode 的上游拒绝了这次 Claude Code CLI 请求,认为它不是官方 Claude Code CLI 发出的。这不是 API Key 失效。请先把插件和 Claude Code CLI 都更新到最新版本再试;仍然失败时,请把下面的原始错误(含 request id)发给 PackyCode 客服,确认所用分组是否支持 Claude Code 的 -p(非交互)模式。 / The gateway refused this Claude Code CLI request as not coming from the official CLI; the API key is fine. Gateway said: ${(typeof message === "string" ? message : "").replace(/^Claude Code CLI failed:\s*/u, "").replace(/\b(?:401|403)\b/gu, "HTTP").slice(0, 600)}`;
508
+ }
509
+ /**
510
+ * Rewrite a failure message so the harness classifies it as quota or auth.
511
+ *
512
+ * A bare status token is neutralized first: an exhausted balance arrives as
513
+ * 402 or 403 on some gateway routes, and the harness's auth rule matches
514
+ * 401/403 before it reads any wording, which would send the failure into a
515
+ * category the retry policy does not re-run.
516
+ * @param message - the original provider failure text.
517
+ * @param kind - the classification of {@link classifyGatewayFailure}.
518
+ * @param options - optional account name for the policy explanation.
519
+ * @returns text the harness routes to QUOTA or AUTH, or a policy explanation.
520
+ */
521
+ function normalizeFailureMessage(message, kind, options = {}) {
522
+ const text = typeof message === "string" && message.length > 0 ? message : "PackyCode request failed";
523
+ if (kind === "client-policy") return explainClientPolicyFailure(options.keyName);
524
+ if (kind === "quota") return `${neutralizeAuthStatus(text)} — PackyCode: insufficient quota for this API key; trying the next key`;
525
+ if (kind === "invalid-key") return `${neutralizeAuthStatus(text)} — PackyCode: 401 invalid_authentication for this API key; trying the next key`;
526
+ return text;
527
+ }
528
+ function neutralizeAuthStatus(text) {
529
+ return text.replace(/\b(?:401|403)\b/gu, "HTTP");
530
+ }
531
+ //#endregion
305
532
  //#region src/claude-adapter.js
306
533
  /**
307
534
  * Route Claude models through the local Claude Code CLI.
@@ -314,9 +541,12 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = fetch, now = Date
314
541
  * A CLI-backed turn is a *delegation*: the CLI runs its own agent loop with
315
542
  * its own tools and returns one answer. The CLI cannot call the harness's
316
543
  * tools, so `options.tools` is deliberately not forwarded, and the harness's
317
- * system prompt is dropped in favour of the CLI's own. While it works, its
318
- * thinking and tool activity stream into a reasoning block and its answer
319
- * streams as text.
544
+ * system prompt is dropped in favour of the CLI's own. The delegation
545
+ * instructions travel as `--append-system-prompt`; the recent conversation
546
+ * travels as one user message whose images are real image blocks, read from
547
+ * the attachment store, so the CLI's model sees them as images. While it
548
+ * works, its thinking and tool activity stream into a reasoning block and its
549
+ * answer streams as text.
320
550
  *
321
551
  * A session-title request is not a delegation: it runs on the small model,
322
552
  * read-only and outside the workspace, with the title instructions appended,
@@ -335,15 +565,344 @@ const TITLE_MODEL_ID = "claude-haiku-5-5";
335
565
  /** How much of the conversation a delegated prompt carries. */
336
566
  const MAX_PROMPT_CHARS$1 = 48e3;
337
567
  const MAX_BLOCK_CHARS = 8e3;
338
- /** Render one content block as text a CLI prompt can carry. */
339
- function renderBlock(block) {
340
- if (block === null || typeof block !== "object") return "";
341
- if (block.type === "text" && typeof block.text === "string") return block.text;
342
- if (block.type === "image") return "[图片]";
343
- if (block.type === "tool-call" || block.type === "tool-call-delta") return `[调用工具 ${block.name ?? block.id ?? ""}]`;
344
- if (block.type === "tool-result") return `[工具结果] ${(typeof block.content === "string" ? block.content : "").slice(0, MAX_BLOCK_CHARS)}`;
345
- if (block.type === "reasoning") return "";
346
- return "";
568
+ const TRUNCATED_MARK = "…(较早对话已截断)";
569
+ /**
570
+ * Image bounds for one delegated turn. Each image is re-encoded by the
571
+ * attachment store to the same request version the HTTP route sends (at most
572
+ * 2048×2048 pixels, about 1 MiB); the oldest images are left out once the turn
573
+ * would carry more than 20 of them or 20 MiB of base64.
574
+ */
575
+ const CLI_IMAGE_LIMITS = Object.freeze({
576
+ maxPixels: 2048 * 2048,
577
+ maxBytesPerImage: 1024 * 1024,
578
+ maxTotalBase64Bytes: 20 * 1024 * 1024,
579
+ maxImages: 20
580
+ });
581
+ /** The vendor's per-image cap on base64 data; a fallback read above it is not sent. */
582
+ const MAX_IMAGE_BASE64_BYTES = 5 * 1024 * 1024;
583
+ const base64Length = (bytes) => Math.ceil(bytes / 3) * 4;
584
+ /** A short name for an image or file a reader can match to the chat. */
585
+ function attachmentName(ref) {
586
+ if (typeof ref?.name === "string" && ref.name !== "") return ref.name;
587
+ const id = String(ref?.attachmentId ?? "");
588
+ return id.startsWith("sha256:") ? id.slice(7, 15) : id.slice(0, 8);
589
+ }
590
+ /** The host path of a stored attachment, when the store is file-backed. */
591
+ function hostPath(lookup) {
592
+ try {
593
+ const value = lookup();
594
+ return typeof value === "string" && value !== "" ? value : void 0;
595
+ } catch {
596
+ return;
597
+ }
598
+ }
599
+ /**
600
+ * Flatten one message's content into ordered parts: text, image, and file.
601
+ * Tool calls and results become short text so a turn that started on another
602
+ * model still reads coherently; reasoning is not carried.
603
+ */
604
+ function collectParts(blocks, parts) {
605
+ for (const block of Array.isArray(blocks) ? blocks : []) {
606
+ if (block === null || typeof block !== "object") continue;
607
+ switch (block.type) {
608
+ case "text":
609
+ if (typeof block.text === "string" && block.text !== "") parts.push({
610
+ kind: "text",
611
+ text: block.text
612
+ });
613
+ break;
614
+ case "image":
615
+ if (block.attachment !== void 0) parts.push({
616
+ kind: "image",
617
+ ref: block.attachment
618
+ });
619
+ else parts.push({
620
+ kind: "text",
621
+ text: "[图片]"
622
+ });
623
+ break;
624
+ case "file":
625
+ if (block.attachment !== void 0) parts.push({
626
+ kind: "file",
627
+ ref: block.attachment
628
+ });
629
+ break;
630
+ case "tool-call":
631
+ case "tool-call-delta":
632
+ parts.push({
633
+ kind: "text",
634
+ text: `[调用工具 ${block.name ?? block.id ?? ""}]`
635
+ });
636
+ break;
637
+ case "tool-result": {
638
+ if (typeof block.content === "string") {
639
+ parts.push({
640
+ kind: "text",
641
+ text: `[工具结果] ${block.content.slice(0, MAX_BLOCK_CHARS)}`
642
+ });
643
+ break;
644
+ }
645
+ const nested = [];
646
+ collectParts(block.content, nested);
647
+ let budget = MAX_BLOCK_CHARS;
648
+ parts.push({
649
+ kind: "text",
650
+ text: "[工具结果]"
651
+ });
652
+ for (const part of nested) {
653
+ if (part.kind !== "text") {
654
+ parts.push(part);
655
+ continue;
656
+ }
657
+ if (budget <= 0) continue;
658
+ parts.push({
659
+ kind: "text",
660
+ text: part.text.slice(0, budget)
661
+ });
662
+ budget -= part.text.length;
663
+ }
664
+ break;
665
+ }
666
+ default: break;
667
+ }
668
+ }
669
+ return parts;
670
+ }
671
+ const textLength = (parts) => parts.reduce((sum, part) => sum + (part.kind === "text" ? part.text.length : 0), 0);
672
+ /**
673
+ * The conversation as labelled turns, newest last, trimmed to the prompt
674
+ * budget by dropping whole older turns; a single newest turn that is larger
675
+ * than the budget keeps its tail.
676
+ */
677
+ function delegatedTurns(options) {
678
+ const messages = Array.isArray(options?.messages) ? options.messages : [];
679
+ const turns = [];
680
+ for (const message of messages) {
681
+ if (message === null || typeof message !== "object" || message.role === "system") continue;
682
+ const parts = collectParts(message.content, []);
683
+ if (parts.length === 0 || textLength(parts) === 0 && !parts.some((part) => part.kind !== "text")) continue;
684
+ const label = message.role === "user" ? "用户" : message.role === "assistant" ? "助手" : String(message.role);
685
+ turns.push({
686
+ label,
687
+ parts
688
+ });
689
+ }
690
+ const kept = [];
691
+ let used = 0;
692
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
693
+ const turn = turns[index];
694
+ const size = textLength(turn.parts) + turn.label.length + 3;
695
+ if (used + size <= MAX_PROMPT_CHARS$1) {
696
+ kept.unshift(turn);
697
+ used += size;
698
+ continue;
699
+ }
700
+ if (kept.length === 0) kept.unshift({
701
+ label: turn.label,
702
+ parts: tailOf(turn.parts, MAX_PROMPT_CHARS$1 - turn.label.length - 3),
703
+ clipped: true
704
+ });
705
+ break;
706
+ }
707
+ return {
708
+ turns: kept,
709
+ truncated: kept.length < turns.length || kept[0]?.clipped === true
710
+ };
711
+ }
712
+ /** Keep the last `budget` characters of a turn's text, and every part after them. */
713
+ function tailOf(parts, budget) {
714
+ const result = [];
715
+ let remaining = budget;
716
+ for (let index = parts.length - 1; index >= 0; index -= 1) {
717
+ const part = parts[index];
718
+ if (part.kind !== "text") {
719
+ result.unshift(part);
720
+ continue;
721
+ }
722
+ if (remaining <= 0) continue;
723
+ result.unshift({
724
+ kind: "text",
725
+ text: part.text.length > remaining ? part.text.slice(-remaining) : part.text
726
+ });
727
+ remaining -= part.text.length;
728
+ }
729
+ return result;
730
+ }
731
+ /** How a part reads when it is carried as text only. */
732
+ function partText(part, describe) {
733
+ if (part.kind === "text") return part.text;
734
+ return describe(part);
735
+ }
736
+ /** Render turns as one transcript, with non-text parts described by `describe`. */
737
+ function renderTurns({ turns, truncated }, describe) {
738
+ const transcript = turns.map((turn) => `${turn.label}:${turn.parts.map((part) => partText(part, describe)).join("\n").trim()}`).join("\n\n");
739
+ return truncated ? `${TRUNCATED_MARK}\n\n${transcript}` : transcript;
740
+ }
741
+ const plainDescription = (part) => part.kind === "image" ? `[图片 ${attachmentName(part.ref)}]` : `[文件 ${attachmentName(part.ref)}]`;
742
+ /**
743
+ * The delegation instructions, appended to the CLI's own system prompt.
744
+ *
745
+ * The harness system prompt describes tools this model cannot call, so only
746
+ * its opening is passed on, marked as context.
747
+ * @param options - the assembled request.
748
+ * @param cwd - the working directory the CLI will run in, when known.
749
+ * @returns the instructions text.
750
+ */
751
+ function delegationInstructions(options, cwd) {
752
+ const system = typeof options?.system === "string" && options.system.trim() !== "" ? options.system.trim() : "";
753
+ return [
754
+ "本回合由 DeepSeek Harness 委派给 Claude Code 执行。用户消息是 DeepSeek Harness 会话的近期对话记录,最后一条是当前请求;其中的图片已作为图片随消息附上,「只读副本」路径也可以用 Read 工具读取。请用你自己的工具完成任务,直接输出最终结果;不要反问、不要复述流程。",
755
+ cwd === void 0 ? void 0 : `工作目录:${cwd}`,
756
+ system === "" ? void 0 : `补充说明(来自 DeepSeek Harness,其中提到的工具你未必拥有):${system.slice(0, 2e3)}`
757
+ ].filter((part) => part !== void 0).join("\n");
758
+ }
759
+ /**
760
+ * The recent conversation as one text transcript. Images and files are named
761
+ * in place; {@link buildDelegatedContent} is what carries the images.
762
+ * @param options - the assembled request.
763
+ * @returns the transcript text.
764
+ */
765
+ function renderDelegatedPrompt(options) {
766
+ return renderTurns(delegatedTurns(options), plainDescription).trim();
767
+ }
768
+ /**
769
+ * The recent conversation as content blocks for the CLI's stream-json input:
770
+ * the transcript, with every image that fits the turn's bounds as an image
771
+ * block at the place it was sent, labelled with its read-only host copy.
772
+ * An image that is left out, or that cannot be read, is described in text
773
+ * instead; it never fails the turn.
774
+ * @param options - the assembled request.
775
+ * @param context - the attachment store (`ctx.attachments`), image bounds,
776
+ * and cancellation.
777
+ * @returns text and image blocks, in conversation order.
778
+ */
779
+ async function buildDelegatedContent(options, { attachments, limits = CLI_IMAGE_LIMITS, signal } = {}) {
780
+ const shape = delegatedTurns(options);
781
+ const images = shape.turns.flatMap((turn) => turn.parts.filter((part) => part.kind === "image"));
782
+ const pathOf = (ref) => hostPath(() => attachments?.imageHostPath?.(ref));
783
+ const filePathOf = (ref) => hostPath(() => attachments?.fileHostPath?.(ref));
784
+ const carried = /* @__PURE__ */ new Map();
785
+ const reasons = /* @__PURE__ */ new Map();
786
+ const reads = /* @__PURE__ */ new Map();
787
+ let total = 0;
788
+ let count = 0;
789
+ let full = false;
790
+ for (const part of [...images].reverse()) {
791
+ if (attachments === void 0 || attachments === null) {
792
+ reasons.set(part, "unavailable");
793
+ continue;
794
+ }
795
+ if (full) {
796
+ reasons.set(part, "limit");
797
+ continue;
798
+ }
799
+ const id = String(part.ref.attachmentId);
800
+ try {
801
+ if (!reads.has(id)) reads.set(id, readRequestImage(attachments, part.ref, limits, signal));
802
+ const image = await reads.get(id);
803
+ const size = base64Length(image.data.byteLength);
804
+ if (size > MAX_IMAGE_BASE64_BYTES) {
805
+ reasons.set(part, "too-large");
806
+ continue;
807
+ }
808
+ if (count + 1 > limits.maxImages || total + size > limits.maxTotalBase64Bytes) {
809
+ full = true;
810
+ reasons.set(part, "limit");
811
+ continue;
812
+ }
813
+ count += 1;
814
+ total += size;
815
+ carried.set(part, image);
816
+ } catch (error) {
817
+ if (signal?.aborted === true) throw error;
818
+ reasons.set(part, "unreadable");
819
+ }
820
+ }
821
+ const blocks = [];
822
+ let text = shape.truncated ? `${TRUNCATED_MARK}\n\n` : "";
823
+ const flush = () => {
824
+ if (text !== "") blocks.push({
825
+ type: "text",
826
+ text
827
+ });
828
+ text = "";
829
+ };
830
+ for (const [turnIndex, turn] of shape.turns.entries()) {
831
+ text += `${turnIndex === 0 ? "" : "\n\n"}${turn.label}:`;
832
+ for (const [partIndex, part] of turn.parts.entries()) {
833
+ const separator = partIndex === 0 ? "" : "\n";
834
+ if (part.kind === "text") {
835
+ text += separator + part.text;
836
+ continue;
837
+ }
838
+ if (part.kind === "file") {
839
+ const path = filePathOf(part.ref);
840
+ text += `${separator}[文件 ${attachmentName(part.ref)}${path === void 0 ? "" : `;只读副本:${path}`}]`;
841
+ continue;
842
+ }
843
+ const path = pathOf(part.ref);
844
+ const copy = path === void 0 ? "" : `;只读副本:${path}`;
845
+ const image = carried.get(part);
846
+ if (image === void 0) {
847
+ text += `${separator}[图片 ${attachmentName(part.ref)}:${imageOmission(reasons.get(part))}${copy}]`;
848
+ continue;
849
+ }
850
+ text += `${separator}[图片 ${attachmentName(part.ref)}(${image.width}x${image.height}px)${copy}]`;
851
+ flush();
852
+ blocks.push({
853
+ type: "image",
854
+ source: {
855
+ type: "base64",
856
+ media_type: image.mediaType,
857
+ data: Buffer.from(image.data).toString("base64")
858
+ }
859
+ });
860
+ }
861
+ }
862
+ flush();
863
+ return blocks;
864
+ }
865
+ function imageOmission(reason) {
866
+ switch (reason) {
867
+ case "limit": return "超出本回合的图片上限,未随消息附上,需要时请用 Read 工具读取";
868
+ case "too-large": return "图片过大,未随消息附上,需要时请用 Read 工具读取";
869
+ case "unavailable": return "当前运行环境无法读取附件";
870
+ default: return "读取失败,未随消息附上";
871
+ }
872
+ }
873
+ /**
874
+ * One request-sized version of a stored image: the store's own request
875
+ * encoder when it has one, otherwise the stored normalized bytes.
876
+ */
877
+ async function readRequestImage(attachments, ref, limits, signal) {
878
+ if (typeof attachments.readImageRequest === "function") {
879
+ const version = await attachments.readImageRequest(ref, {
880
+ maxPixels: limits.maxPixels,
881
+ maxBytes: limits.maxBytesPerImage
882
+ }, signal);
883
+ return {
884
+ data: version.data,
885
+ mediaType: version.mediaType ?? ref.mediaType,
886
+ width: version.width ?? ref.width,
887
+ height: version.height ?? ref.height
888
+ };
889
+ }
890
+ const stored = await attachments.readImage(ref, signal);
891
+ return {
892
+ data: stored.data,
893
+ mediaType: stored.mediaType ?? ref.mediaType,
894
+ width: ref.width,
895
+ height: ref.height
896
+ };
897
+ }
898
+ /**
899
+ * The prompt of a one-shot auxiliary request: its messages' text, verbatim.
900
+ * The caller frames its own input, so nothing is added around it.
901
+ * @param options - the assembled request.
902
+ * @returns the prompt text.
903
+ */
904
+ function renderAuxiliaryPrompt(options) {
905
+ return (Array.isArray(options?.messages) ? options.messages : []).filter((message) => message !== null && typeof message === "object" && message.role !== "system").flatMap((message) => Array.isArray(message.content) ? message.content : []).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n").trim();
347
906
  }
348
907
  /**
349
908
  * Emit text and reasoning as well-formed harness blocks.
@@ -430,43 +989,9 @@ function turnUsage(lastRequest, finished) {
430
989
  cacheWriteTokens: finished?.cacheWriteTokens ?? 0
431
990
  };
432
991
  }
433
- /**
434
- * Turn an assembled request into one CLI prompt.
435
- *
436
- * The leading system message carries the harness's system prompt, which
437
- * describes tools this model cannot call; the CLI brings its own instead.
438
- * @param options - the assembled request.
439
- * @param cwd - the working directory the CLI will run in, when known.
440
- * @returns the prompt text.
441
- */
442
- function renderDelegatedPrompt(options, cwd) {
443
- const messages = Array.isArray(options?.messages) ? options.messages : [];
444
- const turns = [];
445
- for (const message of messages) {
446
- if (message === null || typeof message !== "object") continue;
447
- if (message.role === "system") continue;
448
- const text = (Array.isArray(message.content) ? message.content : []).map(renderBlock).filter((part) => part !== "").join("\n").trim();
449
- if (text === "") continue;
450
- const label = message.role === "user" ? "用户" : message.role === "assistant" ? "助手" : String(message.role);
451
- turns.push(`${label}:${text}`);
452
- }
453
- let transcript = turns.join("\n\n");
454
- if (transcript.length > MAX_PROMPT_CHARS$1) transcript = `…(较早对话已截断)\n\n${transcript.slice(-48e3)}`;
455
- const system = typeof options?.system === "string" && options.system.trim() !== "" ? options.system.trim() : "";
456
- return `${[
457
- "[本回合由 DeepSeek Harness 委派给 Claude Code CLI 执行。请用你自己的工具完成任务,直接输出最终结果;不要反问、不要复述流程。]",
458
- cwd === void 0 ? void 0 : `[工作目录:${cwd}]`,
459
- system === "" ? void 0 : `[补充说明:${system.slice(0, 2e3)}]`
460
- ].filter((part) => part !== void 0).join("\n")}\n\n${transcript}`.trim();
461
- }
462
- /**
463
- * The prompt of a one-shot auxiliary request: its messages' text, verbatim.
464
- * The caller frames its own input, so nothing is added around it.
465
- * @param options - the assembled request.
466
- * @returns the prompt text.
467
- */
468
- function renderAuxiliaryPrompt(options) {
469
- return (Array.isArray(options?.messages) ? options.messages : []).filter((message) => message !== null && typeof message === "object" && message.role !== "system").flatMap((message) => Array.isArray(message.content) ? message.content : []).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n").trim();
992
+ /** One line describing a retry the CLI is making on its own. */
993
+ function retryNote(event) {
994
+ return `网关返回 ${event.status === void 0 ? "出错" : `HTTP ${event.status}`}${typeof event.error === "string" && event.error !== "" ? `(${event.error})` : ""},CLI 自动重试${event.attempt === void 0 ? "" : event.maxRetries === void 0 ? ` ${event.attempt}` : ` ${event.attempt}/${event.maxRetries}`}${event.delayMs === void 0 ? "" : `,${(event.delayMs / 1e3).toFixed(1)} 秒后`}`;
470
995
  }
471
996
  /**
472
997
  * How one request runs on the CLI: a title request on the small model with
@@ -475,7 +1000,7 @@ function renderAuxiliaryPrompt(options) {
475
1000
  * The CLI's own system prompt and tool set stay in place even for a title:
476
1001
  * they are part of what the gateway sees from the official client.
477
1002
  */
478
- function cliCall(options, { titleModel, cwdOf, modeOf }) {
1003
+ async function cliCall(options, { titleModel, cwdOf, modeOf, attachmentsOf, imageLimits }) {
479
1004
  if (options?.purpose === "session-title") return {
480
1005
  prompt: renderAuxiliaryPrompt(options),
481
1006
  model: titleModel,
@@ -485,7 +1010,12 @@ function cliCall(options, { titleModel, cwdOf, modeOf }) {
485
1010
  };
486
1011
  const cwd = cwdOf();
487
1012
  return {
488
- prompt: renderDelegatedPrompt(options, cwd),
1013
+ content: await buildDelegatedContent(options, {
1014
+ attachments: attachmentsOf(),
1015
+ limits: imageLimits,
1016
+ signal: options?.signal
1017
+ }),
1018
+ appendSystemPrompt: delegationInstructions(options, cwd),
489
1019
  model: options.model,
490
1020
  mode: modeOf(),
491
1021
  cwd
@@ -495,26 +1025,39 @@ function cliCall(options, { titleModel, cwdOf, modeOf }) {
495
1025
  * Wrap an adapter so the CLI-backed models run through the local CLI.
496
1026
  *
497
1027
  * @param options - the inner adapter, the CLI runner, the CLI-backed model ids,
498
- * the model session titles run on, and the mode and working directory each
499
- * turn runs with.
1028
+ * the model session titles run on, the mode and working directory each
1029
+ * turn runs with, and the attachment store images are read from.
500
1030
  * @returns an adapter with the same provider surface.
501
1031
  */
502
- function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS, titleModel = TITLE_MODEL_ID, resolveMode, resolveCwd }) {
1032
+ function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS, titleModel = TITLE_MODEL_ID, resolveMode, resolveCwd, resolveAttachments, imageLimits = CLI_IMAGE_LIMITS }) {
503
1033
  if (inner === void 0 || inner === null) throw new Error("the CLI adapter needs an inner adapter");
504
1034
  if (runner === void 0 || runner === null) throw new Error("the CLI adapter needs a runner");
505
1035
  const cliModels = new Set(modelIds);
506
1036
  /** The mode is read per turn so a settings change applies without a reload. */
507
1037
  const modeOf = () => typeof resolveMode === "function" ? resolveMode() : void 0;
508
1038
  const cwdOf = () => typeof resolveCwd === "function" ? resolveCwd() : void 0;
1039
+ const attachmentsOf = () => {
1040
+ try {
1041
+ return typeof resolveAttachments === "function" ? resolveAttachments() ?? void 0 : void 0;
1042
+ } catch {
1043
+ return;
1044
+ }
1045
+ };
509
1046
  async function* cliStream(options) {
1047
+ const quiet = options?.purpose === "session-title";
1048
+ const blocks = createBlockWriter();
1049
+ if (!quiet) yield* blocks.note("▸ 启动 Claude Code CLI…\n");
510
1050
  let call;
511
1051
  try {
512
- call = cliCall(options, {
1052
+ call = await cliCall(options, {
513
1053
  titleModel,
514
1054
  cwdOf,
515
- modeOf
1055
+ modeOf,
1056
+ attachmentsOf,
1057
+ imageLimits
516
1058
  });
517
1059
  } catch (error) {
1060
+ yield* blocks.close();
518
1061
  yield {
519
1062
  type: "finish",
520
1063
  reason: {
@@ -527,7 +1070,6 @@ function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS
527
1070
  };
528
1071
  return;
529
1072
  }
530
- const blocks = createBlockWriter();
531
1073
  const seen = /* @__PURE__ */ new Set();
532
1074
  const firstSighting = (key) => {
533
1075
  if (key.endsWith(":")) return true;
@@ -535,6 +1077,7 @@ function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS
535
1077
  seen.add(key);
536
1078
  return true;
537
1079
  };
1080
+ let retries = 0;
538
1081
  try {
539
1082
  let finished;
540
1083
  let lastRequest;
@@ -542,6 +1085,16 @@ function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS
542
1085
  ...call,
543
1086
  signal: options.signal
544
1087
  })) switch (event.type) {
1088
+ case "started":
1089
+ if (!quiet) {
1090
+ const detail = [event.version, event.model].filter((part) => typeof part === "string" && part !== "").join(" · ");
1091
+ yield* blocks.note(`▸ Claude Code CLI 已启动${detail === "" ? "" : `(${detail})`}\n`);
1092
+ }
1093
+ break;
1094
+ case "retry":
1095
+ retries = Math.max(retries, event.attempt ?? retries + 1);
1096
+ if (!quiet) yield* blocks.note(` ↻ ${retryNote(event)}\n`);
1097
+ break;
545
1098
  case "block":
546
1099
  blocks.boundary(event.kind);
547
1100
  break;
@@ -575,13 +1128,15 @@ function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS
575
1128
  reason: { kind: "stop" }
576
1129
  };
577
1130
  } catch (error) {
1131
+ yield* blocks.close();
1132
+ const raw = String(error?.message ?? error);
578
1133
  yield {
579
1134
  type: "finish",
580
1135
  reason: {
581
1136
  kind: "error",
582
1137
  failure: {
583
1138
  code: "PI_AI_ERROR",
584
- message: String(error?.message ?? error)
1139
+ message: classifyClientPolicy(raw) ? explainCliClientPolicyFailure(raw) : classifyCapacity(raw) ? explainCliCapacityFailure(raw, retries) : raw
585
1140
  }
586
1141
  }
587
1142
  };
@@ -745,8 +1300,32 @@ const DEFAULT_CLAUDE_MODE = "edit";
745
1300
  /** Isolated CLI home; the plugin owns the endpoint and key it runs with. */
746
1301
  const DEFAULT_CLAUDE_CONFIG_DIR = join(homedir(), ".dsh", "packycode-claude-cli");
747
1302
  const DEFAULT_CLAUDE_TIMEOUT_MS = 1800 * 1e3;
748
- /** Argument lists travel through argv, which the kernel bounds. */
1303
+ /**
1304
+ * How long a streaming run may stay silent after starting. The CLI prints its
1305
+ * `system/init` line within a second or two once it runs; one that prints
1306
+ * nothing for this long is stuck before its first request and is stopped, so
1307
+ * the turn reports it instead of spinning until the overall timeout.
1308
+ */
1309
+ const DEFAULT_CLAUDE_STARTUP_TIMEOUT_MS = 120 * 1e3;
1310
+ /**
1311
+ * Upper bound on one run's prompt text. The prompt travels over stdin, so this
1312
+ * is a sanity cap rather than an OS limit.
1313
+ */
749
1314
  const MAX_PROMPT_CHARS = 1e5;
1315
+ /** Upper bound on the images one run carries. */
1316
+ const MAX_INPUT_IMAGES = 20;
1317
+ /**
1318
+ * Instructions a run carries when its caller supplies none.
1319
+ *
1320
+ * In print mode the official CLI chooses its own identity line from whether
1321
+ * it was given appended instructions: with them it introduces itself to the
1322
+ * model as "Claude Code, Anthropic's official CLI for Claude, running within
1323
+ * the Claude Agent SDK"; without them as "a Claude agent, built on Anthropic's
1324
+ * Claude Agent SDK". PackyCode routes Claude only to Claude Code, and a run
1325
+ * without instructions was refused upstream as not coming from it, so every
1326
+ * run states what it was delegated to do.
1327
+ */
1328
+ const DEFAULT_APPEND_SYSTEM_PROMPT = "This task was delegated to Claude Code by DeepSeek Harness. Complete it with your own tools and reply with the final result.";
750
1329
  const KILL_GRACE_MS = 3e3;
751
1330
  /** The npm package the official CLI ships as. */
752
1331
  const CLAUDE_NPM_PACKAGE = "@anthropic-ai/claude-code";
@@ -862,34 +1441,74 @@ function resolveSpawnCommand(binary, { platform = process.platform, exists = exi
862
1441
  }
863
1442
  /**
864
1443
  * Build the CLI arguments for one delegated task.
865
- * @param options - the prompt, the permission mode, an optional model, whether
866
- * the caller consumes the streaming protocol, and optional instructions
867
- * appended to the CLI's own system prompt. The CLI's system prompt and tool
868
- * set are never replaced: they are part of what the official client sends.
1444
+ *
1445
+ * The prompt is not among them: it travels over stdin (see
1446
+ * {@link buildClaudeInput}). An argv is bounded — about 32K UTF-16 units on
1447
+ * Windows — and cannot carry an image; stdin carries both.
1448
+ * @param options - the permission mode, an optional model, whether the caller
1449
+ * consumes the streaming protocol, and instructions appended to the CLI's
1450
+ * own system prompt ({@link DEFAULT_APPEND_SYSTEM_PROMPT} when blank). The
1451
+ * CLI's system prompt and tool set are never replaced: they are part of
1452
+ * what the official client sends.
869
1453
  * @returns argv after the binary name.
870
1454
  */
871
- function buildClaudeArgs({ prompt, mode = DEFAULT_CLAUDE_MODE, model, stream = false, appendSystemPrompt } = {}) {
872
- const text = typeof prompt === "string" ? prompt : "";
873
- if (text.trim() === "") throw new Error("claude_code needs a non-empty prompt");
874
- if (text.length > MAX_PROMPT_CHARS) throw new Error(`claude_code prompt is too long (${text.length} > ${MAX_PROMPT_CHARS} characters)`);
1455
+ function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, appendSystemPrompt } = {}) {
875
1456
  const spec = CLAUDE_CLI_MODES[mode];
876
1457
  if (spec === void 0) throw new Error(`claude_code mode must be one of ${Object.keys(CLAUDE_CLI_MODES).join(", ")}`);
877
- const args = [
1458
+ const args = stream ? [
878
1459
  "-p",
879
- text,
1460
+ "--input-format",
1461
+ "stream-json",
880
1462
  "--output-format",
881
- stream ? "stream-json" : "json"
1463
+ "stream-json",
1464
+ "--include-partial-messages",
1465
+ "--verbose"
1466
+ ] : [
1467
+ "-p",
1468
+ "--output-format",
1469
+ "json"
882
1470
  ];
883
- if (stream) args.push("--include-partial-messages", "--verbose");
884
1471
  const named = typeof model === "string" ? model.trim() : "";
885
1472
  if (named !== "") args.push("--model", named);
886
1473
  const appended = typeof appendSystemPrompt === "string" ? appendSystemPrompt.trim() : "";
887
- if (appended !== "") args.push("--append-system-prompt", appended);
1474
+ args.push("--append-system-prompt", appended === "" ? DEFAULT_APPEND_SYSTEM_PROMPT : appended);
888
1475
  if (spec.skipPermissions === true) args.push("--dangerously-skip-permissions");
889
1476
  else args.push("--allowedTools", ...spec.allowedTools);
890
1477
  return args;
891
1478
  }
892
1479
  /**
1480
+ * Build what the CLI reads from stdin for one run.
1481
+ *
1482
+ * A streaming run reads one `stream-json` user message, whose content may mix
1483
+ * text and Anthropic image blocks; the CLI forwards those images to the model
1484
+ * as images. A non-streaming run reads plain prompt text.
1485
+ * @param options - the prompt text, or `content` blocks (`text` and
1486
+ * `image` with a base64 `source`), and whether the run streams.
1487
+ * @returns the stdin payload.
1488
+ */
1489
+ function buildClaudeInput({ prompt, content, stream = false } = {}) {
1490
+ const blocks = Array.isArray(content) ? content.filter((block) => block?.type === "text" && typeof block.text === "string" && block.text !== "" || block?.type === "image") : [{
1491
+ type: "text",
1492
+ text: typeof prompt === "string" ? prompt : ""
1493
+ }];
1494
+ const text = blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
1495
+ const images = blocks.filter((block) => block.type === "image");
1496
+ if (text.trim() === "" && images.length === 0) throw new Error("claude_code needs a non-empty prompt");
1497
+ if (text.length > MAX_PROMPT_CHARS) throw new Error(`claude_code prompt is too long (${text.length} > ${MAX_PROMPT_CHARS} characters)`);
1498
+ if (images.length > MAX_INPUT_IMAGES) throw new Error(`claude_code carries at most ${MAX_INPUT_IMAGES} images per run (got ${images.length})`);
1499
+ if (!stream) {
1500
+ if (images.length > 0) throw new Error("claude_code images need the streaming CLI input");
1501
+ return text;
1502
+ }
1503
+ return `${JSON.stringify({
1504
+ type: "user",
1505
+ message: {
1506
+ role: "user",
1507
+ content: blocks
1508
+ }
1509
+ })}\n`;
1510
+ }
1511
+ /**
893
1512
  * The child's environment: this plugin's endpoint and key, an isolated config
894
1513
  * home, and the CLI's own network-noise switches off.
895
1514
  * @param options - credential, config home, and the parent environment.
@@ -1048,6 +1667,25 @@ function readClaudeStreamEvents(event) {
1048
1667
  id: String(block.tool_use_id ?? ""),
1049
1668
  text: oneLine(toolResultText(block.content))
1050
1669
  }));
1670
+ if (event.type === "system") {
1671
+ if (event.subtype === "init") return [{
1672
+ type: "started",
1673
+ version: typeof event.claude_code_version === "string" ? event.claude_code_version : "",
1674
+ model: typeof event.model === "string" ? event.model : ""
1675
+ }];
1676
+ if (event.subtype === "api_retry") {
1677
+ const count = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
1678
+ return [{
1679
+ type: "retry",
1680
+ attempt: count(event.attempt),
1681
+ maxRetries: count(event.max_retries),
1682
+ delayMs: count(event.retry_delay_ms),
1683
+ status: count(event.error_status),
1684
+ error: typeof event.error === "string" ? event.error : ""
1685
+ }];
1686
+ }
1687
+ return [];
1688
+ }
1051
1689
  if (event.type === "result") return [{
1052
1690
  type: "result",
1053
1691
  result: event
@@ -1058,24 +1696,29 @@ function readClaudeStreamEvents(event) {
1058
1696
  * A Claude Code CLI runner.
1059
1697
  * @param options - binary (a path, or a function resolving one per run so a
1060
1698
  * CLI installed after startup is picked up), config home, credential
1061
- * resolution, and process seams.
1062
- * @returns `{ run({ prompt, mode, model, cwd, signal }) }`.
1699
+ * resolution, the overall and (streaming) startup time limits, and process
1700
+ * seams.
1701
+ * @returns `{ run(options), stream(options) }`.
1063
1702
  */
1064
- function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_CLAUDE_CONFIG_DIR, resolveCredential, spawnImpl = spawn, timeoutMs = DEFAULT_CLAUDE_TIMEOUT_MS, platform = process.platform } = {}) {
1703
+ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_CLAUDE_CONFIG_DIR, resolveCredential, spawnImpl = spawn, timeoutMs = DEFAULT_CLAUDE_TIMEOUT_MS, startupTimeoutMs = DEFAULT_CLAUDE_STARTUP_TIMEOUT_MS, platform = process.platform } = {}) {
1065
1704
  if (typeof resolveCredential !== "function") throw new Error("the Claude Code CLI runner needs resolveCredential");
1066
1705
  /**
1067
1706
  * Spawn one CLI process and own its lifetime: capture both pipes, enforce the
1068
1707
  * timeout, and kill the process — with a grace period — when the caller
1069
1708
  * aborts. Both public entry points share it.
1070
1709
  */
1071
- const start = async ({ prompt, mode, model, appendSystemPrompt, cwd, signal, stream, onStdout }) => {
1710
+ const start = async ({ prompt, content, mode, model, appendSystemPrompt, cwd, signal, stream, onStdout }) => {
1072
1711
  const args = buildClaudeArgs({
1073
- prompt,
1074
1712
  mode,
1075
1713
  model,
1076
1714
  stream,
1077
1715
  appendSystemPrompt
1078
1716
  });
1717
+ const input = buildClaudeInput({
1718
+ prompt,
1719
+ content,
1720
+ stream
1721
+ });
1079
1722
  const { baseUrl, token } = await resolveCredential();
1080
1723
  const program = typeof binary === "function" ? await binary() : binary;
1081
1724
  const command = resolveSpawnCommand(program, { platform });
@@ -1088,16 +1731,20 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
1088
1731
  cwd,
1089
1732
  env,
1090
1733
  stdio: [
1091
- "ignore",
1734
+ "pipe",
1092
1735
  "pipe",
1093
1736
  "pipe"
1094
1737
  ],
1095
1738
  windowsHide: true
1096
1739
  });
1740
+ child.stdin?.on?.("error", () => void 0);
1741
+ child.stdin?.end?.(input);
1097
1742
  const state = {
1098
1743
  stdout: "",
1099
1744
  stderr: "",
1100
1745
  timedOut: false,
1746
+ silent: false,
1747
+ sawOutput: false,
1101
1748
  settled: false
1102
1749
  };
1103
1750
  const kill = () => {
@@ -1114,17 +1761,30 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
1114
1761
  kill();
1115
1762
  }, timeoutMs) : void 0;
1116
1763
  timer?.unref?.();
1764
+ let startupTimer = stream && startupTimeoutMs > 0 ? setTimeout(() => {
1765
+ if (state.sawOutput) return;
1766
+ state.silent = true;
1767
+ kill();
1768
+ }, startupTimeoutMs) : void 0;
1769
+ startupTimer?.unref?.();
1770
+ const clearStartup = () => {
1771
+ if (startupTimer !== void 0) clearTimeout(startupTimer);
1772
+ startupTimer = void 0;
1773
+ };
1117
1774
  const onAbort = () => kill();
1118
1775
  signal?.addEventListener("abort", onAbort, { once: true });
1119
1776
  const release = () => {
1120
1777
  if (state.settled) return;
1121
1778
  state.settled = true;
1122
1779
  if (timer !== void 0) clearTimeout(timer);
1780
+ clearStartup();
1123
1781
  signal?.removeEventListener("abort", onAbort);
1124
1782
  };
1125
1783
  child.stdout?.setEncoding?.("utf8");
1126
1784
  child.stderr?.setEncoding?.("utf8");
1127
1785
  child.stdout?.on("data", (chunk) => {
1786
+ state.sawOutput = true;
1787
+ clearStartup();
1128
1788
  const text = String(chunk);
1129
1789
  state.stdout = (state.stdout + text).slice(-4194304);
1130
1790
  onStdout?.(text);
@@ -1143,6 +1803,11 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
1143
1803
  reject(/* @__PURE__ */ new Error(`claude_code timed out after ${Math.round(timeoutMs / 1e3)}s`));
1144
1804
  return;
1145
1805
  }
1806
+ if (state.silent) {
1807
+ const detail = state.stderr.trim().split("\n").slice(-4).join(" ").slice(0, 400);
1808
+ reject(/* @__PURE__ */ new Error(`Claude Code CLI printed nothing within ${Math.round(startupTimeoutMs / 1e3)}s of starting and was stopped${detail === "" ? "" : `: ${detail}`}`));
1809
+ return;
1810
+ }
1146
1811
  if (signal?.aborted === true) {
1147
1812
  reject(/* @__PURE__ */ new Error("claude_code was aborted"));
1148
1813
  return;
@@ -1167,12 +1832,13 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
1167
1832
  * @param options - prompt, mode, model, working directory, and cancellation.
1168
1833
  * @returns the CLI's answer plus its own accounting.
1169
1834
  */
1170
- async run({ prompt, mode = DEFAULT_CLAUDE_MODE, model, cwd, signal } = {}) {
1835
+ async run({ prompt, mode = DEFAULT_CLAUDE_MODE, model, appendSystemPrompt, cwd, signal } = {}) {
1171
1836
  const startedAt = Date.now();
1172
1837
  const { state, closed } = await start({
1173
1838
  prompt,
1174
1839
  mode,
1175
1840
  model,
1841
+ appendSystemPrompt,
1176
1842
  cwd,
1177
1843
  signal,
1178
1844
  stream: false
@@ -1196,18 +1862,19 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
1196
1862
  * Run one delegated task, yielding its answer as the CLI produces it.
1197
1863
  * The final `result` event carries the CLI's own accounting, summed over
1198
1864
  * every request the CLI made.
1199
- * @param options - as {@link run}, plus optional `appendSystemPrompt`
1200
- * instructions.
1865
+ * @param options - as {@link run}, plus optional `content` blocks (text
1866
+ * and base64 images) that replace `prompt`.
1201
1867
  * @returns an async generator of {@link readClaudeStreamEvents} events and
1202
1868
  * one closing `result` event.
1203
1869
  */
1204
- async *stream({ prompt, mode = DEFAULT_CLAUDE_MODE, model, appendSystemPrompt, cwd, signal } = {}) {
1870
+ async *stream({ prompt, content, mode = DEFAULT_CLAUDE_MODE, model, appendSystemPrompt, cwd, signal } = {}) {
1205
1871
  const startedAt = Date.now();
1206
1872
  let buffer = "";
1207
1873
  let sawResult;
1208
1874
  const lines = [];
1209
1875
  const { state, closed, kill } = await start({
1210
1876
  prompt,
1877
+ content,
1211
1878
  mode,
1212
1879
  model,
1213
1880
  appendSystemPrompt,
@@ -2354,9 +3021,9 @@ function createPackyCodePluginManager({ fetchImpl = ambientFetch, runCommand = s
2354
3021
  *
2355
3022
  * Effort levels come from each vendor's documentation, because an effort the
2356
3023
  * model does not support is refused before the request is sent: Claude Opus
2357
- * 5.5 (platform.claude.com effort docs), GPT-6.1 Sol `reasoning.effort`
2358
- * (developers.openai.com model page), DeepSeek thinking mode (`thinking`
2359
- * toggle plus `low|high|max` effort).
3024
+ * 5.5 (platform.claude.com effort docs), GPT-6.1 Sol and GPT-6 Astra
3025
+ * `reasoning.effort` (developers.openai.com model pages), DeepSeek thinking
3026
+ * mode (`thinking` toggle plus `low|high|max` effort).
2360
3027
  *
2361
3028
  * @module dsh-packycode-api/catalog
2362
3029
  */
@@ -2385,7 +3052,7 @@ const DEEPSEEK_LEVELS = Object.freeze({
2385
3052
  max: "max"
2386
3053
  });
2387
3054
  /**
2388
- * Claude 5.5 models and GPT-6.1 Sol cannot disable reasoning, so `off` is
3055
+ * Claude 5.5 models, GPT-6.1 Sol, and GPT-6 Astra cannot disable reasoning, so `off` is
2389
3056
  * pinned unsupported. The Claude models run through the CLI, which picks its
2390
3057
  * own effort, so for them these levels only shape the picker.
2391
3058
  */
@@ -2431,6 +3098,19 @@ const PACKYCODE_MODELS = Object.freeze([
2431
3098
  maxTokens: 128e3,
2432
3099
  thinkingLevelMap: ALWAYS_THINKING_LEVELS
2433
3100
  }),
3101
+ Object.freeze({
3102
+ id: "gpt-6-astra",
3103
+ name: "GPT-6 Astra",
3104
+ api: "openai-responses",
3105
+ provider: PROVIDER,
3106
+ baseUrl: RESPONSES_BASE,
3107
+ reasoning: true,
3108
+ input: Object.freeze(["text", "image"]),
3109
+ cost: NO_COST,
3110
+ contextWindow: 105e4,
3111
+ maxTokens: 128e3,
3112
+ thinkingLevelMap: ALWAYS_THINKING_LEVELS
3113
+ }),
2434
3114
  Object.freeze({
2435
3115
  id: "claude-opus-5-5",
2436
3116
  name: "Claude Opus 5.5",
@@ -2486,110 +3166,6 @@ const PACKYCODE_MODELS = Object.freeze([
2486
3166
  /** Every model id this catalog advertises, in picker order. */
2487
3167
  const PACKYCODE_MODEL_IDS = Object.freeze(PACKYCODE_MODELS.map((model) => model.id));
2488
3168
  //#endregion
2489
- //#region src/failures.js
2490
- /**
2491
- * Failure classification for the two conditions that move the key ring along.
2492
- *
2493
- * The gateway reports an exhausted key and a rejected key in Chinese, and the
2494
- * harness classifies failures by fixed English/status wording it recognizes
2495
- * (`isQuotaExceededError`, `\b401\b`). Normalizing here is what makes the
2496
- * harness's retry policy re-run the step — and therefore what lets the retry
2497
- * pick up the next key. Rewriting is honest about what happened: this plugin
2498
- * knows the key's condition from the gateway's own words plus its balance
2499
- * reader, so the sentence it substitutes says no more than that.
2500
- *
2501
- * @module dsh-packycode-api/failures
2502
- */
2503
- /** Terminal quota, balance, or credit wording, in the gateway's own languages. */
2504
- const QUOTA_WORDING = /额度|余额|配额|欠费|insufficient[\s_-]+(?:quota|balance|credits?)|quota[\s_-]+(?:exceeded|exhausted|reached)|out[\s_-]+of[\s_-]+(?:credits?|budget)/iu;
2505
- /** A rejected key names itself; a bare 403 does not, and must not disable one. */
2506
- const INVALID_KEY_WORDING = /无效的令牌|令牌无效|无效密钥|密钥无效|invalid[\s_-]+api[\s_-]*key|invalid_authentication|api key is invalid/iu;
2507
- const INVALID_KEY_STATUS = /\b401\b/;
2508
- /**
2509
- * A gateway group that only serves its vendor's official client. These arrive
2510
- * as 403, which the harness reads as an authentication failure and renders as
2511
- * an invalid-API-key problem — the opposite of what happened, and a condition
2512
- * no retry can clear.
2513
- */
2514
- const CLIENT_POLICY_WORDING = /only accessible via the official|official claude cli|官方\s*claude\s*cli|请使用标准\s*codex\s*客户端|客户端存在异常|二次分发|API\s*转接/iu;
2515
- /** The key's quota is exhausted; move to the next key. */
2516
- const QUOTA_FAILURE = "quota";
2517
- /** The gateway rejected the key itself; move to the next key. */
2518
- const INVALID_KEY_FAILURE = "invalid-key";
2519
- /**
2520
- * Whether the gateway is refusing this client rather than this credential.
2521
- * @param message - provider failure text, as pi-ai carries it.
2522
- * @returns true when the route is gated to a vendor's official client.
2523
- */
2524
- function classifyClientPolicy(message) {
2525
- return CLIENT_POLICY_WORDING.test(typeof message === "string" ? message : "");
2526
- }
2527
- /**
2528
- * Classify a gateway failure message.
2529
- *
2530
- * Quota wording wins over a status token: an exhausted balance is often
2531
- * reported as 402/403, and only the key's own rejection may read as 401. A
2532
- * client-policy refusal is not a key fault and is classified separately.
2533
- * @param message - provider failure text, as pi-ai carries it.
2534
- * @returns the failure kind, or undefined when the key is not at fault.
2535
- */
2536
- function classifyGatewayFailure(message) {
2537
- const text = typeof message === "string" ? message : "";
2538
- if (text === "") return void 0;
2539
- if (classifyClientPolicy(text)) return void 0;
2540
- if (QUOTA_WORDING.test(text)) return QUOTA_FAILURE;
2541
- if (INVALID_KEY_WORDING.test(text)) return INVALID_KEY_FAILURE;
2542
- if (INVALID_KEY_STATUS.test(text)) return INVALID_KEY_FAILURE;
2543
- }
2544
- /**
2545
- * Rewrite a failure message so the harness classifies it as quota or auth.
2546
- *
2547
- * A bare status token is neutralized first: an exhausted balance arrives as
2548
- * 402 or 403 on some gateway routes, and the harness's auth rule matches
2549
- * 401/403 before it reads any wording, which would send the failure into a
2550
- * category the retry policy does not re-run.
2551
- * @param message - the original provider failure text.
2552
- * @param kind - the classification of {@link classifyGatewayFailure}.
2553
- * @returns text the harness routes to QUOTA or AUTH.
2554
- */
2555
- /**
2556
- * Explain a client-policy refusal so the user reads the real cause.
2557
- *
2558
- * The sentence must not read as any other failure the harness knows: it
2559
- * deliberately carries no HTTP status and none of the words the classifier
2560
- * matches for auth, quota, rate limits, timeouts, or transport faults. That
2561
- * keeps the failure out of the retry policy — no retry can talk a group into
2562
- * serving an API client — and lets the sentence reach the user verbatim, in
2563
- * place of the "API key is invalid" the bare 403 would have produced.
2564
- * @param keyName - the account name the balance reader saw, when known.
2565
- * @returns the replacement sentence.
2566
- */
2567
- function explainClientPolicyFailure() {
2568
- return "PackyCode 拒绝了这个客户端:所选分组只允许厂商官方客户端调用(Claude 分组要求官方 Claude CLI,Codex 分组要求标准 Codex 客户端)。这不是 API Key 失效,重试也不会成功。请改用允许 API 调用的分组(例如 cc-sale 或 claude-officially),或改用 PackyCode 的 gpt-6.1-sol / deepseek-flash。 / This group only serves the vendor’s official client; the API key is fine and retrying cannot help.";
2569
- }
2570
- /**
2571
- * Rewrite a failure message so the harness classifies it as quota or auth.
2572
- *
2573
- * A bare status token is neutralized first: an exhausted balance arrives as
2574
- * 402 or 403 on some gateway routes, and the harness's auth rule matches
2575
- * 401/403 before it reads any wording, which would send the failure into a
2576
- * category the retry policy does not re-run.
2577
- * @param message - the original provider failure text.
2578
- * @param kind - the classification of {@link classifyGatewayFailure}.
2579
- * @param options - optional account name for the policy explanation.
2580
- * @returns text the harness routes to QUOTA or AUTH, or a policy explanation.
2581
- */
2582
- function normalizeFailureMessage(message, kind, options = {}) {
2583
- const text = typeof message === "string" && message.length > 0 ? message : "PackyCode request failed";
2584
- if (kind === "client-policy") return explainClientPolicyFailure(options.keyName);
2585
- if (kind === "quota") return `${neutralizeAuthStatus(text)} — PackyCode: insufficient quota for this API key; trying the next key`;
2586
- if (kind === "invalid-key") return `${neutralizeAuthStatus(text)} — PackyCode: 401 invalid_authentication for this API key; trying the next key`;
2587
- return text;
2588
- }
2589
- function neutralizeAuthStatus(text) {
2590
- return text.replace(/\b(?:401|403)\b/gu, "HTTP");
2591
- }
2592
- //#endregion
2593
3169
  //#region src/provider.js
2594
3170
  /**
2595
3171
  * The PackyCode pi-ai provider, built in code so the user configures no route.
@@ -2848,6 +3424,8 @@ const name = PLUGIN_ID;
2848
3424
  * blocking on a service it will never see.
2849
3425
  */
2850
3426
  const inject = ["llm", "credentials"];
3427
+ /** What a `claude_code` tool run is told about where its task came from. */
3428
+ const CLAUDE_TOOL_INSTRUCTIONS = "This task was delegated to Claude Code by another agent in DeepSeek Harness through its claude_code tool. The task text is self-contained; complete it with your own tools and reply with the final result only.";
2851
3429
  /** Host overrides; a profile that never installs this section keeps the defaults. */
2852
3430
  const Config = z.object({
2853
3431
  apiHost: z.string().default(DEFAULT_API_HOST),
@@ -3072,7 +3650,8 @@ function apply(ctx, config) {
3072
3650
  }),
3073
3651
  runner: claudeRunner,
3074
3652
  resolveMode: resolveCliMode,
3075
- resolveCwd: resolveCliCwd
3653
+ resolveCwd: resolveCliCwd,
3654
+ resolveAttachments: () => ctx.get?.("attachments") ?? ctx.attachments
3076
3655
  });
3077
3656
  ctx.llm.registerAdapter([PROVIDER], adapter);
3078
3657
  ctx.llm.registerConfigurableProviders([{
@@ -3192,6 +3771,7 @@ function apply(ctx, config) {
3192
3771
  prompt: args.prompt,
3193
3772
  mode: capClaudeMode(args.mode, cliModeCeiling(exec?.agent?.session)) ?? "edit",
3194
3773
  model: args.model,
3774
+ appendSystemPrompt: CLAUDE_TOOL_INSTRUCTIONS,
3195
3775
  cwd,
3196
3776
  signal: exec?.signal
3197
3777
  });
@@ -3200,4 +3780,4 @@ function apply(ctx, config) {
3200
3780
  });
3201
3781
  }
3202
3782
  //#endregion
3203
- export { AUTO_CLAUDE_MODE, CHANNEL, CLAUDE_CLI_MODES, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLI_BACKED_MODEL_IDS, Config, DEFAULT_API_HOST, DEFAULT_STATUS_HOST, DISPLAY_NAME, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SETTINGS_NAMESPACE, TITLE_MODEL_ID, apply, assertApiKey, buildClaudeArgs, buildClaudeEnvironment, capClaudeMode, classifyClientPolicy, classifyGatewayFailure, claudeBinaryCandidates, claudeModeFor, claudeModeForSession, createBalanceReader, createClaudeCliAdapter, createClaudeCliManager, createClaudeRunner, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, explainClientPolicyFailure, explainNpmFailure, findClaudeBinary, guardKeyRotation, inject, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, npmCandidates, parseBillingSubscription, parseBillingUsage, parseClaudeResult, parseClaudeVersion, parseTokenUsage, readClaudeStreamEvents, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, summarizeToolUse };
3783
+ export { AUTO_CLAUDE_MODE, CHANNEL, CLAUDE_CLI_MODES, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLI_BACKED_MODEL_IDS, CLI_IMAGE_LIMITS, Config, DEFAULT_API_HOST, DEFAULT_APPEND_SYSTEM_PROMPT, DEFAULT_STATUS_HOST, DISPLAY_NAME, LEGACY_STATUS_HOSTS, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SETTINGS_NAMESPACE, TITLE_MODEL_ID, accountHostOrder, apply, assertApiKey, buildClaudeArgs, buildClaudeEnvironment, buildClaudeInput, buildDelegatedContent, capClaudeMode, classifyClientPolicy, classifyGatewayFailure, claudeBinaryCandidates, claudeModeFor, claudeModeForSession, createBalanceReader, createClaudeCliAdapter, createClaudeCliManager, createClaudeRunner, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, delegationInstructions, explainCliClientPolicyFailure, explainClientPolicyFailure, explainNpmFailure, findClaudeBinary, guardKeyRotation, inject, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, npmCandidates, parseBillingSubscription, parseBillingUsage, parseClaudeResult, parseClaudeVersion, parseTokenUsage, readClaudeStreamEvents, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse };