@mars-sea/dsh-commandcode-provider 0.10.2 → 0.10.4

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
@@ -220,16 +220,38 @@ var CommandCodeAccountPool = class {
220
220
  until: 0
221
221
  });
222
222
  }
223
- /** One account's key: literal → credential seam → auth file (default slot). */
223
+ /**
224
+ * One account's key: literal → credential seam → auth file (default slot).
225
+ *
226
+ * Every source is normalized here, at the single point where a slot's key
227
+ * enters the pool. The adapter sends the key through the harness's
228
+ * `assertUsableApiKey()`, which trims it — a stored key from the credentials
229
+ * seam, a `.env` line, or a shell export all pick up surrounding whitespace
230
+ * — and reports that trimmed form back to `markRejected()`. Returning the
231
+ * raw value would file every 429/401 mark under a key no later lookup can
232
+ * find: rotation would re-offer the same account, the account card would show
233
+ * no mark, and the usage endpoints would 401 while chat kept working.
234
+ * Normalizing once makes resolution, probing, marking, and the request path
235
+ * agree on one string.
236
+ */
224
237
  async resolveSlotKey(slot) {
225
- if (slot.literal !== void 0 && slot.literal !== "") return slot.literal;
238
+ if (slot.literal !== void 0) {
239
+ const literal = normalizeResolvedKey(slot.literal);
240
+ if (literal !== void 0) return literal;
241
+ }
226
242
  if (slot.ref !== void 0) {
227
243
  const hit = await this.deps.resolveRef(slot.ref);
228
- if (hit !== void 0 && hit !== "") return hit;
244
+ if (hit !== void 0) {
245
+ const resolved = normalizeResolvedKey(hit);
246
+ if (resolved !== void 0) return resolved;
247
+ }
229
248
  }
230
249
  if (slot.allowAuthFile) {
231
250
  const fromFile = this.deps.authFileKey();
232
- if (fromFile !== void 0 && fromFile !== "") return fromFile;
251
+ if (fromFile !== void 0) {
252
+ const fileKey = normalizeResolvedKey(fromFile);
253
+ if (fileKey !== void 0) return fileKey;
254
+ }
233
255
  }
234
256
  }
235
257
  /** Hand out the chosen account's key. */
@@ -240,6 +262,16 @@ var CommandCodeAccountPool = class {
240
262
  };
241
263
  }
242
264
  };
265
+ /**
266
+ * Normalize one resolved credential to the form every consumer sees. Trim
267
+ * only: a blank-after-trim value means "no key" (the slot is omitted and the
268
+ * caller reports `MISSING_CREDENTIAL`), while characters an HTTP header cannot
269
+ * carry are left for `assertUsableApiKey()` to reject with its own message.
270
+ */
271
+ function normalizeResolvedKey(value) {
272
+ const trimmed = value.trim();
273
+ return trimmed === "" ? void 0 : trimmed;
274
+ }
243
275
  //#endregion
244
276
  //#region src/capabilities.ts
245
277
  /**
@@ -338,6 +370,11 @@ const KNOWN_EFFORTS = {
338
370
  "high",
339
371
  "max"
340
372
  ],
373
+ "deepseek/deepseek-v4.1-flash": [
374
+ "low",
375
+ "high",
376
+ "max"
377
+ ],
341
378
  "deepseek/deepseek-v4-flash": ["high", "max"],
342
379
  "deepseek/deepseek-v4-flash-vision-exp": ["high", "max"],
343
380
  "deepseek/deepseek-v4-pro": ["high", "max"],
@@ -485,6 +522,11 @@ const KNOWN_EFFORTS = {
485
522
  "high",
486
523
  "xhigh",
487
524
  "max"
525
+ ],
526
+ "MiniMaxAI/MiniMax-M3": [
527
+ "low",
528
+ "medium",
529
+ "high"
488
530
  ]
489
531
  };
490
532
  /**
@@ -520,6 +562,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
520
562
  "claude-sonnet-4-6",
521
563
  "claude-sonnet-5",
522
564
  "deepseek/deepseek-v4-flash-vision-exp",
565
+ "deepseek/deepseek-v4.1-flash",
523
566
  "google/gemini-3.1-flash-lite",
524
567
  "google/gemini-3.5-flash",
525
568
  "google/gemini-3.5-flash-lite",
@@ -554,7 +597,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
554
597
  "z-ai/glm-5.3-flash"
555
598
  ]);
556
599
  /**
557
- * Models the official CLI's model table (command-code@1.50.0) marks
600
+ * Models the official CLI's model table (command-code@1.53.0) marks
558
601
  * `reasoning:!0` but defines no selectable `reasoning_effort` levels — they
559
602
  * think automatically, with Command Code driving the depth. This is the
560
603
  * authoritative "thinks, effort not adjustable" set: `KNOWN_EFFORTS` (which
@@ -562,7 +605,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
562
605
  * effort levels, and this snapshot is not surfaced in the picker's compact
563
606
  * description — it exists for programmatic consumers.
564
607
  *
565
- * Source: the command-code@1.50.0 bundled model table (dist/cli.mjs),
608
+ * Source: the command-code@1.53.0 bundled model table (dist/cli.mjs),
566
609
  * cross-checked with https://commandcode.ai/docs/reference/cli/models.
567
610
  * (`stealth/ox-alpha` left this set in command-code@1.32.1, which gave it
568
611
  * selectable `['low', 'high', 'max']` efforts; the preview then ended in
@@ -575,11 +618,16 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
575
618
  * to `KNOWN_EFFORTS`. command-code@1.42.0 added `meituan/LongCat-2.0:free`
576
619
  * (reasoning:!0, no efforts). command-code@1.45.0 gave the Muse Spark family
577
620
  * (1.1, 1.2, 1.2-contributor, 1.3, 1.3-contributor) selectable
578
- * `['low', 'medium', 'high', 'xhigh']` efforts — they moved to `KNOWN_EFFORTS`.)
621
+ * `['low', 'medium', 'high', 'xhigh']` efforts — they moved to `KNOWN_EFFORTS`.
622
+ * command-code@1.51.3 gave `MiniMaxAI/MiniMax-M3` selectable
623
+ * `['low', 'medium', 'high']` efforts — it moved to `KNOWN_EFFORTS` too.
624
+ * command-code@1.52.0 added `inclusionai/ling-3.0-flash-sante:free`
625
+ * (reasoning:!0, no efforts).)
626
+ * command-code@1.53.0 added `deepseek/deepseek-v4.1-flash` with selectable
627
+ * ['low', 'high', 'max'] efforts, so it lives in `KNOWN_EFFORTS`, not here.)
579
628
  * Keep in sync via the dsh-commandcode-upstream skill.
580
629
  */
581
630
  const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
582
- "MiniMaxAI/MiniMax-M3",
583
631
  "Qwen/Qwen3.6-Max-Preview",
584
632
  "Qwen/Qwen3.6-Plus",
585
633
  "Qwen/Qwen3.7-Flash",
@@ -594,7 +642,8 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
594
642
  "thinkingmachines/inkling",
595
643
  "thinkingmachines/inkling-small",
596
644
  "poolside/laguna-s-2.1-free",
597
- "meituan/LongCat-2.0:free"
645
+ "meituan/LongCat-2.0:free",
646
+ "inclusionai/ling-3.0-flash-sante:free"
598
647
  ]);
599
648
  /**
600
649
  * The minimum subscription plan a model is included in, per the official plan
@@ -609,7 +658,9 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
609
658
  * command-code@1.41.0 added `Qwen/Qwen3.8-Max-0902` (Go) and 1.42.0 added
610
659
  * `meituan/LongCat-2.0:free` (Go, free promo); command-code@1.43.0 added
611
660
  * `google/gemini-3.8-flash` (GOAT) and 1.44.0 added `meta/muse-spark-1.3`
612
- * (GOAT) plus its Contributor sibling (Go).
661
+ * (GOAT) plus its Contributor sibling (Go); command-code@1.52.0 added the
662
+ * free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
663
+ * added `deepseek/deepseek-v4.1-flash` (Go).
613
664
  *
614
665
  * The Provider API exposes no plan metadata, so this snapshot is the source of
615
666
  * truth for the picker's plan annotation — it answers "which plan do I need to
@@ -633,11 +684,13 @@ const KNOWN_PLANS = {
633
684
  "Qwen/Qwen3.8-Max": "go",
634
685
  "Qwen/Qwen3.8-Max-0902": "go",
635
686
  "deepseek/deepseek-v4-flash-fast": "go",
687
+ "deepseek/deepseek-v4.1-flash": "go",
636
688
  "deepseek/deepseek-v4-flash": "go",
637
689
  "deepseek/deepseek-v4-flash-vision-exp": "go",
638
690
  "deepseek/deepseek-v4-pro": "go",
639
691
  "gpt-5.6-luna": "go",
640
692
  "meituan/LongCat-2.0:free": "go",
693
+ "inclusionai/ling-3.0-flash-sante:free": "go",
641
694
  "meta/muse-spark-1.2-contributor": "go",
642
695
  "meta/muse-spark-1.3-contributor": "go",
643
696
  "moonshotai/Kimi-K2.5": "go",
@@ -734,8 +787,8 @@ function compareByPlan(a, b) {
734
787
  }
735
788
  /**
736
789
  * Subscription plan table, synced from the official CLI bundle's plan maps
737
- * (located by the `"individual-go"` key in command-code@1.50.0 `dist/cli.mjs`,
738
- * re-verified unchanged through 1.50.0): subscription `planId`
790
+ * (located by the `"individual-go"` key in command-code@1.53.0 `dist/cli.mjs`,
791
+ * re-verified unchanged through 1.53.0): subscription `planId`
739
792
  * prefix → display name and the plan's monthly credit total. This is the
740
793
  * account's own subscription (from `/alpha/billing/subscriptions`) — distinct
741
794
  * from {@link KNOWN_PLANS}, which maps catalog models to their minimum tier.
@@ -828,45 +881,69 @@ const KNOWN_DEALS = {
828
881
  "meituan/LongCat-2.0:free": {
829
882
  label: "FREE",
830
883
  free: true
884
+ },
885
+ "inclusionai/ling-3.0-flash-sante:free": {
886
+ label: "FREE",
887
+ free: true
831
888
  }
832
889
  };
833
890
  /**
834
891
  * Models with time-of-day (peak/off-peak) pricing, per the official pricing
835
892
  * page (`/docs/resources/pricing-limits`). Since 2026-08-16 16:00 UTC, DeepSeek
836
- * charges by the hour: peak hours are 01:00–04:00 and 06:00–10:00 UTC (7h/day,
837
- * full price); the other 17 hours are off-peak at half price. The V4 Flash
838
- * Vision (exp) variant (command-code@1.32.0) shares the V4 Flash windows and
839
- * peak prices ($0.44/$1.32) — each row's hover annotation states exactly 2×
840
- * that row's displayed off-peak prices. The picker shows the
893
+ * charges by the hour: peak hours are 01:00–04:00 and 06:00–10:00 UTC (7h per
894
+ * weekday, full price) **Monday to Friday only**; the other 17 hours of a
895
+ * weekday and every hour of Saturday/Sunday (UTC) are off-peak at half price.
896
+ * The V4 Flash Vision (exp) variant (command-code@1.32.0) shares the V4 Flash
897
+ * windows and peak prices ($0.44/$1.32) — each row's hover annotation states
898
+ * exactly 2× that row's displayed off-peak prices. The picker shows the
841
899
  * *current* state as a compact
842
900
  * label (`Peak`/`Half`) matching the English noun style of the other markers
843
901
  * (`Image`, `FREE`), so a developer can tell at a glance whether calling the
844
902
  * model right now is cheap or expensive.
845
903
  *
846
- * Extraction caution: in the page's HTML each annotation div sits inside its
847
- * OWN row's container, immediately before the NEXT row starts — flattening
848
- * the page to text makes every annotation look like it belongs to the model
849
- * printed after it. Verify membership against the enclosing row and the 2×
850
- * price relation, not the flat-text neighbor.
904
+ * Authoritative extraction: the pricing page embeds a model JSON array whose
905
+ * hourly-priced entries carry a `timeOfDay` block
906
+ * (`{ windows: "01–04 & 06–10 UTC, Mon–Fri", peakHoursPerDay: 7,
907
+ * offPeakHoursPerDay: 17, peak: {...}, offPeak: {...} }`). Exactly four models
908
+ * carry it: V4 Pro, V4 Flash, V4 Flash Vision (exp), and V4.1 Flash (added in
909
+ * command-code@1.53.0 at $0.15/$0.60 off-peak, $0.30/$1.20 peak — the same
910
+ * schedule as the other three).
911
+ *
912
+ * Extraction caution: the rendered HTML rows are a trap. Each annotation div
913
+ * sits inside its OWN row's container, immediately before the NEXT row starts,
914
+ * so flattening the page to text makes every annotation look like it belongs
915
+ * to the model printed after it — that is how `deepseek/deepseek-v4-flash-fast`
916
+ * was wrongly added here (its row is flat-priced at $0.28/$0.56/$0.07 and has
917
+ * no `timeOfDay` block). Trust the embedded JSON's `timeOfDay` membership and
918
+ * the 2× price relation, never the flat-text neighbor.
851
919
  *
852
- * Keep in sync with the official pricing page when the model set or the peak
853
- * windows change (see the dsh-commandcode-upstream skill).
920
+ * Keep in sync with the official pricing page when the model set, the peak
921
+ * windows, or the weekday rule change (see the dsh-commandcode-upstream skill).
854
922
  */
855
923
  const KNOWN_PEAK_PRICING = /* @__PURE__ */ new Set([
856
924
  "deepseek/deepseek-v4-pro",
857
925
  "deepseek/deepseek-v4-flash",
858
926
  "deepseek/deepseek-v4-flash-vision-exp",
859
- "deepseek/deepseek-v4-flash-fast"
927
+ "deepseek/deepseek-v4.1-flash"
860
928
  ]);
861
- /** Peak hours (UTC, hour-of-day range end-exclusive): 01–03 and 06–09. */
929
+ /**
930
+ * Peak hours (UTC, hour-of-day range end-exclusive): 01–03 and 06–09.
931
+ * Weekday-only — see `peakPricingState()`; weekends are fully off-peak.
932
+ */
862
933
  const PEAK_HOUR_RANGES = [[1, 4], [6, 10]];
863
934
  /**
864
- * Whether `now` (defaults to `Date.now()`) falls in a peak-pricing hour for
865
- * time-of-day-priced models. `undefined` for models outside the snapshot.
935
+ * Whether `now` (defaults to `Date.now()`) falls in a peak-pricing window for
936
+ * time-of-day-priced models. Peak rates apply Monday–Friday (UTC) only: the
937
+ * official rule charges Saturday and Sunday completely off-peak for all 24
938
+ * hours, so a weekend timestamp is off-peak even inside `PEAK_HOUR_RANGES`.
939
+ * `undefined` for models outside the snapshot.
866
940
  */
867
941
  function peakPricingState(modelId, now = Date.now()) {
868
942
  if (!KNOWN_PEAK_PRICING.has(modelId)) return void 0;
869
- const hour = new Date(now).getUTCHours();
943
+ const at = new Date(now);
944
+ const day = at.getUTCDay();
945
+ if (day === 0 || day === 6) return "off-peak";
946
+ const hour = at.getUTCHours();
870
947
  return PEAK_HOUR_RANGES.some(([start, end]) => hour >= start && hour < end) ? "peak" : "off-peak";
871
948
  }
872
949
  /**
@@ -947,7 +1024,7 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
947
1024
  * and API key or subscription, and Command Code's terms apply.
948
1025
  *
949
1026
  * Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
950
- * re-verified against command-code@1.50.0 — endpoints, request shape, and
1027
+ * re-verified against command-code@1.53.0 — endpoints, request shape, and
951
1028
  * stream events unchanged):
952
1029
  * POST {apiBase}/alpha/generate
953
1030
  * body: { config, memory, taste, skills, params: { model, messages, tools,
@@ -960,7 +1037,7 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
960
1037
  * per-request options thunk and an API-key resolver from the plugin entry
961
1038
  * (src/index.ts), so a settings change reaches the very next request.
962
1039
  */
963
- const COMMAND_CODE_CLI_VERSION = "1.50.0";
1040
+ const COMMAND_CODE_CLI_VERSION = "1.53.0";
964
1041
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
965
1042
  const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
966
1043
  const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
@@ -1162,8 +1239,65 @@ function wireToolCallIds(paired) {
1162
1239
  function blockText(block) {
1163
1240
  return block.type === "text" || block.type === "reasoning" ? block.text : "";
1164
1241
  }
1165
- function toolResultText(block) {
1166
- return block.content.map(blockText).filter(Boolean).join("\n");
1242
+ /**
1243
+ * Collect text and nested images of one tool result. The harness `read_image`
1244
+ * tool returns both, so dropping the image half is what made a tool-read image
1245
+ * invisible to a Vision model (issue #30). Deduplication keeps a result that
1246
+ * repeats one attachment from paying for the same pixels twice.
1247
+ */
1248
+ function toolResultMedia(block) {
1249
+ const chunks = [];
1250
+ const images = [];
1251
+ const seen = /* @__PURE__ */ new Set();
1252
+ const walk = (blocks) => {
1253
+ for (const nested of blocks) {
1254
+ if (nested.type === "text" || nested.type === "reasoning") {
1255
+ if (nested.text) chunks.push(nested.text);
1256
+ continue;
1257
+ }
1258
+ if (nested.type === "image") {
1259
+ if (!seen.has(nested.attachment.attachmentId)) {
1260
+ seen.add(nested.attachment.attachmentId);
1261
+ images.push(nested.attachment);
1262
+ }
1263
+ continue;
1264
+ }
1265
+ if (nested.type === "tool-result") walk(nested.content);
1266
+ }
1267
+ };
1268
+ walk(block.content);
1269
+ return {
1270
+ text: chunks.join("\n"),
1271
+ images
1272
+ };
1273
+ }
1274
+ /**
1275
+ * Result text for the wire's `role: 'tool'` message. A tool that returned only
1276
+ * an image (and possibly a nested image-only tool result) has no text at all,
1277
+ * and neither transport accepts an empty tool content string, so it gets a
1278
+ * descriptor pointing at the user message that follows with the pixels.
1279
+ */
1280
+ function toolResultTextForWire(media) {
1281
+ return media.text || (media.images.length > 0 ? "(image returned; see the attached image)" : "");
1282
+ }
1283
+ /** Leading line of the user message that carries a tool result's images. */
1284
+ const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result:";
1285
+ /**
1286
+ * The model-visible note introducing the images carried out of one tool
1287
+ * result. Neither transport merges them back into the tool result, so this
1288
+ * line is what tells the model the image it is about to see belongs to the
1289
+ * tool it just ran rather than to the user; it also leads the part list,
1290
+ * because an image-first content array is what some gateways reject. Count and
1291
+ * pixel dimensions are appended when the tool's own text does not already
1292
+ * state them (a result that returns the image and nothing else).
1293
+ */
1294
+ function toolResultImageNote(media) {
1295
+ const first = media.images[0];
1296
+ if (first === void 0) return TOOL_RESULT_IMAGE_TEXT;
1297
+ const dimensions = `${first.width}x${first.height} px`;
1298
+ if (first.width <= 0 || first.height <= 0 || media.text.includes(dimensions)) return TOOL_RESULT_IMAGE_TEXT;
1299
+ const count = media.images.length > 1 ? `${media.images.length} images, ` : "";
1300
+ return `${TOOL_RESULT_IMAGE_TEXT} ${count}${dimensions}`;
1167
1301
  }
1168
1302
  function hasImageContent(message) {
1169
1303
  const check = (blocks) => blocks.some((b) => b.type === "image" || b.type === "tool-result" && check(b.content));
@@ -1231,6 +1365,7 @@ async function messagesToCC(messages, readImage) {
1231
1365
  if (message.role === "user" && message.source.kind === "tool") {
1232
1366
  const block = message.content[0];
1233
1367
  if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
1368
+ const media = toolResultMedia(block);
1234
1369
  out.push({
1235
1370
  role: "tool",
1236
1371
  content: [{
@@ -1239,13 +1374,25 @@ async function messagesToCC(messages, readImage) {
1239
1374
  toolName: toolNames.get(block.toolCallId) || "unknown",
1240
1375
  output: block.isError ? {
1241
1376
  type: "error-text",
1242
- value: toolResultText(block)
1377
+ value: toolResultTextForWire(media)
1243
1378
  } : {
1244
1379
  type: "text",
1245
- value: toolResultText(block)
1380
+ value: toolResultTextForWire(media)
1246
1381
  }
1247
1382
  }]
1248
1383
  });
1384
+ if (media.images.length > 0) {
1385
+ if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
1386
+ const carried = [{
1387
+ type: "text",
1388
+ text: toolResultImageNote(media)
1389
+ }];
1390
+ for (const attachment of media.images) carried.push(await imageToCommandCode(attachment, readImage));
1391
+ out.push({
1392
+ role: "user",
1393
+ content: carried
1394
+ });
1395
+ }
1249
1396
  }
1250
1397
  }
1251
1398
  return out;
@@ -1324,11 +1471,24 @@ async function messagesToOpenAI(messages, readImage) {
1324
1471
  if (message.role === "user" && message.source.kind === "tool") {
1325
1472
  const block = message.content[0];
1326
1473
  if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
1474
+ const media = toolResultMedia(block);
1327
1475
  out.push({
1328
1476
  role: "tool",
1329
1477
  tool_call_id: wireIds.get(block.toolCallId) ?? block.toolCallId,
1330
- content: toolResultText(block)
1478
+ content: toolResultTextForWire(media)
1331
1479
  });
1480
+ if (media.images.length > 0) {
1481
+ if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
1482
+ const carried = [{
1483
+ type: "text",
1484
+ text: toolResultImageNote(media)
1485
+ }];
1486
+ for (const attachment of media.images) carried.push(await imageToOpenAI(attachment, readImage));
1487
+ out.push({
1488
+ role: "user",
1489
+ content: carried
1490
+ });
1491
+ }
1332
1492
  }
1333
1493
  }
1334
1494
  return out;
@@ -2514,6 +2674,14 @@ function tokensCompact(value) {
2514
2674
  if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
2515
2675
  return String(value);
2516
2676
  }
2677
+ /**
2678
+ * Format a success-rate percentage (already in percent units): at most two
2679
+ * decimals, trailing zeros trimmed — mirrors `formatSuccessRate` in
2680
+ * `./client/usage.ts`, which the settings card uses.
2681
+ */
2682
+ function successRateText(value) {
2683
+ return String(Number(value.toFixed(2)));
2684
+ }
2517
2685
  /** Format a millis timestamp as a local date; `n/a` when unset. */
2518
2686
  function resetLabel(ms) {
2519
2687
  if (ms <= 0) return "n/a";
@@ -2552,7 +2720,7 @@ function renderReport(report, locale, title) {
2552
2720
  }
2553
2721
  if (report.usage) {
2554
2722
  const u = report.usage;
2555
- lines.push(commandCopy(locale, "usageHeader"), commandCopy(locale, "requestsLine").replace("{n}", String(u.completedCount)).replace("{f}", String(u.failedCount)).replace("{r}", String(u.successRate)), commandCopy(locale, "costLine").replace("{money}", money(u.totalCost)).replace("{credits}", moneyShort(u.totalCredits)), commandCopy(locale, "tokensLine").replace("{in}", tokensCompact(u.totalTokensIn)).replace("{out}", tokensCompact(u.totalTokensOut)), "");
2723
+ lines.push(commandCopy(locale, "usageHeader"), commandCopy(locale, "requestsLine").replace("{n}", String(u.completedCount)).replace("{f}", String(u.failedCount)).replace("{r}", successRateText(u.successRate)), commandCopy(locale, "costLine").replace("{money}", money(u.totalCost)).replace("{credits}", moneyShort(u.totalCredits)), commandCopy(locale, "tokensLine").replace("{in}", tokensCompact(u.totalTokensIn)).replace("{out}", tokensCompact(u.totalTokensOut)), "");
2556
2724
  }
2557
2725
  if (report.credits) {
2558
2726
  const c = report.credits;
@@ -3069,6 +3237,14 @@ var CommandCodeLoginFlow = class {
3069
3237
  timer;
3070
3238
  /** Settle hooks of the live attempt's callback promise. */
3071
3239
  settle;
3240
+ /**
3241
+ * Attempt generation. A delivered callback keeps validating the key
3242
+ * asynchronously (`complete()`), and that window is open to a cancel or a
3243
+ * fresh `begin()`; the generation lets a late completion recognize that it
3244
+ * no longer owns the status face and stop instead of storing a credential
3245
+ * the user cancelled and flipping the page back to success.
3246
+ */
3247
+ attemptSeq = 0;
3072
3248
  disposed = false;
3073
3249
  constructor(deps) {
3074
3250
  this.deps = deps;
@@ -3089,8 +3265,9 @@ var CommandCodeLoginFlow = class {
3089
3265
  */
3090
3266
  async begin() {
3091
3267
  if (this.disposed) throw new Error("login flow has been disposed");
3092
- if (this.statusValue.state === "waiting") return this.statusValue;
3268
+ if (this.statusValue.state === "waiting" && this.server !== void 0) return this.statusValue;
3093
3269
  this.teardown();
3270
+ const attempt = ++this.attemptSeq;
3094
3271
  const port = await this.findPort();
3095
3272
  const expectedState = this.deps.randomToken?.(32) ?? randomBytes(32).toString("base64url");
3096
3273
  const settled = new Promise((resolve, reject) => {
@@ -3118,7 +3295,7 @@ var CommandCodeLoginFlow = class {
3118
3295
  });
3119
3296
  }, this.deps.timeoutMs ?? 12e4);
3120
3297
  this.timer.unref?.();
3121
- settled.then((credentials) => this.complete(credentials), (failure) => this.failFrom(failure));
3298
+ settled.then((credentials) => this.complete(attempt, credentials), (failure) => this.failFrom(attempt, failure));
3122
3299
  return this.statusValue;
3123
3300
  }
3124
3301
  /** Cancel a waiting attempt; terminal states are untouched. */
@@ -3235,6 +3412,13 @@ var CommandCodeLoginFlow = class {
3235
3412
  }
3236
3413
  if (typeof payload === "object" && payload !== null && "error" in payload) {
3237
3414
  const denial = payload;
3415
+ if (denial.state !== expectedState) {
3416
+ json(403, {
3417
+ success: false,
3418
+ error: "Invalid state token"
3419
+ });
3420
+ return;
3421
+ }
3238
3422
  const description = denial.error_description ?? denial.error;
3239
3423
  this.settleAttempt(json, 200, { success: true }, new LoginSettleError(denial.error === "access_denied" ? "denied" : "error", typeof description === "string" && description !== "" ? description : "Authorization failed"));
3240
3424
  return;
@@ -3266,10 +3450,20 @@ var CommandCodeLoginFlow = class {
3266
3450
  if (failure !== void 0) settle.reject(failure);
3267
3451
  else if (credentials !== void 0) settle.resolve(credentials);
3268
3452
  }
3269
- /** Post-validation completion: whoami check, then hand-off to storage. */
3270
- async complete(credentials) {
3271
- if (this.disposed || this.statusValue.state !== "waiting") return;
3453
+ /**
3454
+ * Post-validation completion: whoami check, then hand-off to storage.
3455
+ *
3456
+ * Every step re-checks {@link ownsAttempt} first: the whoami round-trip and
3457
+ * the credential write are awaits, and the user may cancel (or start another
3458
+ * attempt) while one is in flight. A completion that no longer owns the
3459
+ * attempt must not write the key or publish a status — otherwise cancel
3460
+ * would report "cancelled" while the credential landed anyway, and the page
3461
+ * would silently flip to success.
3462
+ */
3463
+ async complete(attempt, credentials) {
3464
+ if (!this.ownsAttempt(attempt)) return;
3272
3465
  const validation = await validateCommandApiKey(this.deps.fetchImpl ?? fetch, this.readApiBase(), credentials.apiKey);
3466
+ if (!this.ownsAttempt(attempt)) return;
3273
3467
  if (!validation.valid) {
3274
3468
  const reason = validation.error === "invalid_key" ? "invalid-key" : validation.error === "network_error" ? "network" : "error";
3275
3469
  this.setStatus({
@@ -3279,9 +3473,11 @@ var CommandCodeLoginFlow = class {
3279
3473
  });
3280
3474
  return;
3281
3475
  }
3476
+ if (!this.ownsAttempt(attempt)) return;
3282
3477
  try {
3283
3478
  await this.deps.storeKey(credentials);
3284
3479
  } catch (error) {
3480
+ if (!this.ownsAttempt(attempt)) return;
3285
3481
  this.setStatus({
3286
3482
  state: "failed",
3287
3483
  reason: "unavailable",
@@ -3289,7 +3485,7 @@ var CommandCodeLoginFlow = class {
3289
3485
  });
3290
3486
  return;
3291
3487
  }
3292
- if (this.disposed) return;
3488
+ if (!this.ownsAttempt(attempt)) return;
3293
3489
  this.clearTimer();
3294
3490
  this.setStatus({
3295
3491
  state: "success",
@@ -3297,10 +3493,14 @@ var CommandCodeLoginFlow = class {
3297
3493
  keyName: credentials.keyName
3298
3494
  });
3299
3495
  }
3496
+ /** Whether one attempt still owns the status face (not cancelled, replaced, or disposed). */
3497
+ ownsAttempt(attempt) {
3498
+ return !this.disposed && this.attemptSeq === attempt && this.statusValue.state === "waiting";
3499
+ }
3300
3500
  /** Map a tagged settle rejection onto the status face. */
3301
- failFrom(failure) {
3501
+ failFrom(attempt, failure) {
3302
3502
  if (!(failure instanceof LoginSettleError)) return;
3303
- if (this.disposed || this.statusValue.state !== "waiting") return;
3503
+ if (!this.ownsAttempt(attempt)) return;
3304
3504
  this.setStatus({
3305
3505
  state: "failed",
3306
3506
  reason: failure.reason,
@@ -3581,6 +3781,244 @@ var CommandCodeSearchProvider = class {
3581
3781
  }
3582
3782
  };
3583
3783
  //#endregion
3784
+ //#region src/tui-settings.ts
3785
+ /** The selector value meaning "no pinned account — follow rotation order". */
3786
+ const ACTIVE_ACCOUNT_AUTO = "auto";
3787
+ /** The selector value meaning "no language override — follow the shell locale". */
3788
+ const LANG_AUTO = "auto";
3789
+ /**
3790
+ * Build the section descriptor. Pure, so tests can pin the exact fields
3791
+ * without a dsh-TUI host.
3792
+ *
3793
+ * Field choices worth keeping: the two option-bearing fields (`activeAccount`,
3794
+ * `lang`) are `text` + `options` rather than `select`, because a `select`
3795
+ * cannot express "unset" — cycling only ever lands on a declared option, so a
3796
+ * `select` would strand the user on a pinned value with no way back to
3797
+ * automatic. The `auto` sentinel plus a `parse` that clears the path keeps the
3798
+ * unset state reachable. The two booleans format their EFFECTIVE default
3799
+ * (`filterModelsByPlan` unset means true at the adapter), so a fresh install
3800
+ * reads true instead of the screen's "(empty)".
3801
+ */
3802
+ function buildCommandCodeTuiSection(deps) {
3803
+ const ref = deps.apiKeyRef();
3804
+ const slots = deps.accountSlots();
3805
+ return {
3806
+ ns: deps.ns,
3807
+ title: deps.title ?? "Command Code",
3808
+ descriptions: { zh: "Command Code(非官方)" },
3809
+ groups: [
3810
+ {
3811
+ id: "connection",
3812
+ title: "Connection",
3813
+ descriptions: { zh: "连接" }
3814
+ },
3815
+ {
3816
+ id: "models",
3817
+ title: "Models",
3818
+ descriptions: { zh: "模型" }
3819
+ },
3820
+ {
3821
+ id: "advanced",
3822
+ title: "Advanced",
3823
+ descriptions: { zh: "高级" }
3824
+ }
3825
+ ],
3826
+ fields: [
3827
+ {
3828
+ path: ["apiKey"],
3829
+ group: "connection",
3830
+ kind: "text",
3831
+ label: "API key",
3832
+ descriptions: { zh: "API 密钥" },
3833
+ secret: { ref },
3834
+ hint: `Stored in the credential store as ${ref}, never in settings.yaml.`,
3835
+ hintDescriptions: { zh: `保存在凭据库(${ref}),不会写入 settings.yaml。` }
3836
+ },
3837
+ {
3838
+ path: ["apiBase"],
3839
+ group: "connection",
3840
+ kind: "text",
3841
+ label: "API base",
3842
+ descriptions: { zh: "API 地址" },
3843
+ placeholder: "https://api.commandcode.ai",
3844
+ hint: "Leave empty for the public Command Code Provider API.",
3845
+ hintDescriptions: { zh: "留空即使用官方 Command Code Provider API。" },
3846
+ parse: (text) => {
3847
+ const trimmed = text.trim();
3848
+ return trimmed === "" ? { kind: "clear" } : {
3849
+ kind: "set",
3850
+ value: trimmed
3851
+ };
3852
+ }
3853
+ },
3854
+ {
3855
+ path: ["filterModelsByPlan"],
3856
+ group: "models",
3857
+ kind: "boolean",
3858
+ label: "Hide out-of-plan models",
3859
+ descriptions: { zh: "隐藏套餐外的模型" },
3860
+ hint: "Keeps models above your subscription tier out of the picker. Fails open.",
3861
+ hintDescriptions: { zh: "在选择器里隐藏超出当前订阅档位的模型;判断不出来时全部显示。" },
3862
+ format: (value) => value === false ? "false" : "true",
3863
+ parse: (text) => ({
3864
+ kind: "set",
3865
+ value: text.trim() === "true"
3866
+ })
3867
+ },
3868
+ {
3869
+ path: ["visibleModels"],
3870
+ group: "models",
3871
+ kind: "text",
3872
+ label: "Visible models",
3873
+ descriptions: { zh: "显示的模型" },
3874
+ hint: "Comma-separated catalog ids; empty shows every model.",
3875
+ hintDescriptions: { zh: "以逗号分隔的模型 id;留空表示显示全部模型。" },
3876
+ format: (value) => (Array.isArray(value) ? value.filter((id) => typeof id === "string" && id !== "") : []).join(", "),
3877
+ parse: (text) => {
3878
+ const ids = text.split(",").map((id) => id.trim()).filter((id) => id !== "");
3879
+ return ids.length === 0 ? { kind: "clear" } : {
3880
+ kind: "set",
3881
+ value: ids
3882
+ };
3883
+ }
3884
+ },
3885
+ {
3886
+ path: ["activeAccount"],
3887
+ group: "advanced",
3888
+ kind: "text",
3889
+ label: "Active account",
3890
+ descriptions: { zh: "当前账号" },
3891
+ hint: "A pinned account id, or auto to follow the rotation order.",
3892
+ hintDescriptions: { zh: "固定使用某个账号的 id;auto 表示按轮换顺序自动选择。" },
3893
+ options: [{
3894
+ value: ACTIVE_ACCOUNT_AUTO,
3895
+ label: "Automatic (rotation order)",
3896
+ descriptions: { zh: "自动(按轮换顺序)" }
3897
+ }, ...slots.map((slot) => ({
3898
+ value: slot.id,
3899
+ label: slot.label,
3900
+ descriptions: { zh: slot.label }
3901
+ }))],
3902
+ format: (value) => typeof value === "string" && value.trim() !== "" ? value : ACTIVE_ACCOUNT_AUTO,
3903
+ parse: (text) => {
3904
+ const trimmed = text.trim();
3905
+ return trimmed === "" || trimmed === "auto" ? { kind: "clear" } : {
3906
+ kind: "set",
3907
+ value: trimmed
3908
+ };
3909
+ }
3910
+ },
3911
+ {
3912
+ path: ["lang"],
3913
+ group: "advanced",
3914
+ kind: "text",
3915
+ label: "Command language",
3916
+ descriptions: { zh: "命令语言" },
3917
+ hint: "Language of the /commandcode dashboard; auto follows the shell locale.",
3918
+ hintDescriptions: { zh: "/commandcode 用量面板的语言;auto 跟随终端 locale。" },
3919
+ options: [
3920
+ {
3921
+ value: LANG_AUTO,
3922
+ label: "Automatic (shell locale)",
3923
+ descriptions: { zh: "自动(跟随终端 locale)" }
3924
+ },
3925
+ {
3926
+ value: "zh",
3927
+ label: "中文"
3928
+ },
3929
+ {
3930
+ value: "en",
3931
+ label: "English"
3932
+ }
3933
+ ],
3934
+ format: (value) => value === "zh" || value === "en" ? value : LANG_AUTO,
3935
+ parse: (text) => {
3936
+ const trimmed = text.trim();
3937
+ return trimmed === "" || trimmed === "auto" ? { kind: "clear" } : {
3938
+ kind: "set",
3939
+ value: trimmed
3940
+ };
3941
+ }
3942
+ }
3943
+ ]
3944
+ };
3945
+ }
3946
+ /**
3947
+ * The `tuiSettingsSections` service, read defensively.
3948
+ *
3949
+ * The service name is declared by dsh-TUI's own module augmentation, which
3950
+ * this package deliberately does not import, so the typed `Context` has no
3951
+ * such property. The read goes through the REFLECTIVE `ctx.get` rather than a
3952
+ * bare property access: cordis refuses a property read for a service the fiber
3953
+ * never declared in `inject` (`cannot get property … without inject`), and an
3954
+ * unmeet seam must degrade to "no section" instead of throwing out of the
3955
+ * plugin's boot.
3956
+ */
3957
+ function tuiSettingsService(ctx) {
3958
+ let candidate;
3959
+ try {
3960
+ candidate = ctx.get("tuiSettingsSections");
3961
+ } catch {
3962
+ return;
3963
+ }
3964
+ if (typeof candidate !== "object" || candidate === null) return void 0;
3965
+ const register = candidate.register;
3966
+ if (typeof register !== "function") return void 0;
3967
+ return { register: register.bind(candidate) };
3968
+ }
3969
+ /**
3970
+ * Identity of everything in the section that can change at runtime: the
3971
+ * credential reference behind the API-key field and the account slots behind
3972
+ * the active-account selector. Re-registration is skipped while this matches,
3973
+ * so ordinary settings writes never churn the screen's section list.
3974
+ */
3975
+ function sectionSignature(section) {
3976
+ const active = section.fields.find((field) => field.path.join(".") === "activeAccount");
3977
+ return JSON.stringify({
3978
+ secret: section.fields.find((field) => field.secret !== void 0)?.secret?.ref ?? "",
3979
+ options: active?.options?.map((option) => option.value) ?? []
3980
+ });
3981
+ }
3982
+ /**
3983
+ * Register the Command Code section on a dsh-TUI host.
3984
+ *
3985
+ * @param ctx - the context of an activated `tuiSettingsSections` injection.
3986
+ * @param deps - plugin-owned facts the section reads.
3987
+ * @returns a refresh function that re-registers the section when a fact it
3988
+ * renders changed (the plugin entry calls it from its settings `onChange`
3989
+ * hook), or `undefined` when the seam is unusable. The returned function is
3990
+ * inert after the fiber is torn down.
3991
+ */
3992
+ function applyCommandCodeTuiSettings(ctx, deps) {
3993
+ const service = tuiSettingsService(ctx);
3994
+ if (service === void 0) return void 0;
3995
+ let disposed = false;
3996
+ let current;
3997
+ const refresh = () => {
3998
+ if (disposed) return;
3999
+ const section = buildCommandCodeTuiSection(deps);
4000
+ const signature = sectionSignature(section);
4001
+ if (current?.signature === signature) return;
4002
+ current?.dispose();
4003
+ current = void 0;
4004
+ try {
4005
+ current = {
4006
+ signature,
4007
+ dispose: service.register(section)
4008
+ };
4009
+ } catch (error) {
4010
+ ctx.logger?.warn(`llm-commandcode: could not register the dsh-TUI settings section: ${error instanceof Error ? error.message : String(error)}`);
4011
+ }
4012
+ };
4013
+ refresh();
4014
+ ctx.effect(() => () => {
4015
+ disposed = true;
4016
+ current?.dispose();
4017
+ current = void 0;
4018
+ }, "dsh-commandcode-provider: tui settings section");
4019
+ return refresh;
4020
+ }
4021
+ //#endregion
3584
4022
  //#region src/index.ts
3585
4023
  /**
3586
4024
  * dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
@@ -3619,7 +4057,7 @@ const PROVIDER = "commandcode";
3619
4057
  const DEFAULT_MODELS_CACHE_PATH = join(homedir(), ".commandcode", "models-cache.json");
3620
4058
  const Config = z.object({
3621
4059
  apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
3622
- apiKey: z.string(),
4060
+ apiKey: z.string().role("secret"),
3623
4061
  apiBase: z.string(),
3624
4062
  workingDir: z.string(),
3625
4063
  modelsCachePath: z.string(),
@@ -3631,7 +4069,8 @@ const Config = z.object({
3631
4069
  accounts: z.array(z.object({
3632
4070
  label: z.string(),
3633
4071
  apiKeyEnv: z.string().role("credential-ref"),
3634
- apiKey: z.string()
4072
+ /** Literal key for one extra slot; see the top-level `apiKey` secret note. */
4073
+ apiKey: z.string().role("secret")
3635
4074
  })),
3636
4075
  activeAccount: z.string(),
3637
4076
  modelAccountRules: z.array(z.object({
@@ -3821,6 +4260,20 @@ function apply(ctx, config) {
3821
4260
  applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
3822
4261
  }, "dsh-commandcode-provider: web search selection");
3823
4262
  });
4263
+ let refreshTuiSettings;
4264
+ ctx.inject(["tuiSettingsSections"], (tuiCtx) => {
4265
+ refreshTuiSettings = applyCommandCodeTuiSettings(tuiCtx, {
4266
+ ns: NS,
4267
+ apiKeyRef: () => current().apiKeyEnv ?? DEFAULT_API_KEY_ENV,
4268
+ accountSlots: () => slots().map((slot) => ({
4269
+ id: slot.id,
4270
+ label: slot.label
4271
+ }))
4272
+ });
4273
+ tuiCtx.effect(() => () => {
4274
+ refreshTuiSettings = void 0;
4275
+ }, "dsh-commandcode-provider: tui settings handle");
4276
+ });
3824
4277
  ctx.inject(["settings"], (settingsCtx) => {
3825
4278
  settingsCtx.settings.installSection(ctx, NS, Config, config, {
3826
4279
  setSource: (source) => {
@@ -3828,11 +4281,12 @@ function apply(ctx, config) {
3828
4281
  },
3829
4282
  onChange: () => {
3830
4283
  applySearchSelection(current().webSearch ?? true);
4284
+ refreshTuiSettings?.();
3831
4285
  }
3832
4286
  });
3833
4287
  });
3834
4288
  }
3835
4289
  //#endregion
3836
- export { BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommands, applyUsageRemote, buildCommandAuthUrl, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, selectCommandCodeSearchProvider, studioBaseForApiBase, subscriptionPlanInfo, usageReportSchema, validateCommandApiKey };
4290
+ export { ACTIVE_ACCOUNT_AUTO, BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LANG_AUTO, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommandCodeTuiSettings, applyCommands, applyUsageRemote, buildCommandAuthUrl, buildCommandCodeTuiSection, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, selectCommandCodeSearchProvider, studioBaseForApiBase, subscriptionPlanInfo, usageReportSchema, validateCommandApiKey };
3837
4291
 
3838
4292
  //# sourceMappingURL=index.js.map