@mars-sea/dsh-commandcode-provider 0.11.8 → 0.11.10
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/CHANGELOG.md +55 -1
- package/README.md +35 -5
- package/README.zh-CN.md +35 -5
- package/icon.svg +1 -0
- package/lib/client.js +548 -23
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +182 -6
- package/lib/index.js +1705 -204
- package/lib/index.js.map +1 -1
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +43 -38
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { homedir, tmpdir } from "node:os";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
@@ -6,7 +6,7 @@ import * as dshLlm from "@deepseek-ai/dsh-llm";
|
|
|
6
6
|
import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, assertUsableApiKey, attributionHeaders, errorChain, isContextWindowExceededError, offloadedImageText, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
|
|
7
7
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
8
8
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
9
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
10
10
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
11
11
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
12
12
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
@@ -284,6 +284,27 @@ const KNOWN_EFFORTS = {
|
|
|
284
284
|
"xhigh",
|
|
285
285
|
"max"
|
|
286
286
|
],
|
|
287
|
+
"claude-opus-5-5": [
|
|
288
|
+
"low",
|
|
289
|
+
"medium",
|
|
290
|
+
"high",
|
|
291
|
+
"xhigh",
|
|
292
|
+
"max"
|
|
293
|
+
],
|
|
294
|
+
"gpt-6-luna": [
|
|
295
|
+
"low",
|
|
296
|
+
"medium",
|
|
297
|
+
"high",
|
|
298
|
+
"xhigh",
|
|
299
|
+
"max"
|
|
300
|
+
],
|
|
301
|
+
"gpt-6-sol": [
|
|
302
|
+
"low",
|
|
303
|
+
"medium",
|
|
304
|
+
"high",
|
|
305
|
+
"xhigh",
|
|
306
|
+
"max"
|
|
307
|
+
],
|
|
287
308
|
"MiniMaxAI/MiniMax-M3": [
|
|
288
309
|
"low",
|
|
289
310
|
"medium",
|
|
@@ -320,6 +341,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
320
341
|
"claude-haiku-4-5-20251001",
|
|
321
342
|
"claude-opus-4-7",
|
|
322
343
|
"claude-opus-4-8",
|
|
344
|
+
"claude-opus-5-5",
|
|
323
345
|
"claude-opus-5",
|
|
324
346
|
"claude-sonnet-4-6",
|
|
325
347
|
"claude-sonnet-5",
|
|
@@ -339,6 +361,8 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
339
361
|
"gpt-5.6-sol",
|
|
340
362
|
"gpt-5.6-terra",
|
|
341
363
|
"gpt-6-astra",
|
|
364
|
+
"gpt-6-luna",
|
|
365
|
+
"gpt-6-sol",
|
|
342
366
|
"meta/muse-spark-1.1",
|
|
343
367
|
"meta/muse-spark-1.2",
|
|
344
368
|
"meta/muse-spark-1.2-contributor",
|
|
@@ -365,6 +389,69 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
365
389
|
"z-ai/glm-5.3-flashx"
|
|
366
390
|
]);
|
|
367
391
|
/**
|
|
392
|
+
* Models WITHOUT a zero-data-retention upstream, per the official CLI's own
|
|
393
|
+
* registry (`command-code@1.64.0` `dist/cli.mjs`: `modelSupportsZdr(id)` is
|
|
394
|
+
* exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
|
|
395
|
+
* carries the same membership in the sibling route table — the union is this
|
|
396
|
+
* set). The official docs (commandcode.ai/docs/resources/zdr) put it in prose
|
|
397
|
+
* — "99% of our models have ZDR-capable upstreams … only a small handful of
|
|
398
|
+
* models are affected" — so the CLI's exclusion list is the only per-model
|
|
399
|
+
* evidence there is; a ZDR request naming one of these fails with HTTP 422
|
|
400
|
+
* `cmd_zdr_no_providers` instead of routing through a provider that retains.
|
|
401
|
+
*
|
|
402
|
+
* Why a NEGATIVE set, and why "not listed" answers TRUE: 99% of the catalog is
|
|
403
|
+
* covered, so the maintained difference is the exception list. This helper is
|
|
404
|
+
* informational; the adapter sends the ZDR header for EVERY request when the
|
|
405
|
+
* switch is on. The provider remains the routing authority and refuses an
|
|
406
|
+
* unsupported model rather than silently dropping the privacy guarantee.
|
|
407
|
+
*
|
|
408
|
+
* `minimax/minimax-m3-free` is the one entry the public catalog
|
|
409
|
+
* (`/provider/v1/models`) does not serve (it is CLI/pricing-visible and hidden
|
|
410
|
+
* from the picker); it stays listed because the CLI carries it and a Go-plan
|
|
411
|
+
* request can still name it.
|
|
412
|
+
*
|
|
413
|
+
* Keep in sync via the dsh-commandcode-upstream skill: the CLI's registry data
|
|
414
|
+
* (its `zdr:{only:[…]}` provider routes and the per-provider `zdr`/`noTraining`
|
|
415
|
+
* flags) is upstream-internal routing, not a per-model contract, so this table
|
|
416
|
+
* is the snapshot of the exclusion set and nothing more. The 1.62.0 → 1.64.0
|
|
417
|
+
* diff of that union is EMPTY: both anchors were extracted from both bundles
|
|
418
|
+
* during the 2026-09-23 check and each carries the same 20 members — which is
|
|
419
|
+
* how rare a change here is expected to be. `meituan/LongCat-2.0` is the one
|
|
420
|
+
* member the two anchors disagree about (it is in `modelSupportsZdr`'s set in
|
|
421
|
+
* both releases and in neither `knownModelSupportsZdr` set), so the union is
|
|
422
|
+
* what this table follows; reading only the sibling route table would drop it.
|
|
423
|
+
*/
|
|
424
|
+
const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
|
|
425
|
+
"MiniMaxAI/MiniMax-M3",
|
|
426
|
+
"Qwen/Qwen3.8-Max-0902",
|
|
427
|
+
"meituan/LongCat-2.0",
|
|
428
|
+
"meta/muse-spark-1.1",
|
|
429
|
+
"meta/muse-spark-1.2",
|
|
430
|
+
"meta/muse-spark-1.2-contributor",
|
|
431
|
+
"meta/muse-spark-1.3",
|
|
432
|
+
"meta/muse-spark-1.3-contributor",
|
|
433
|
+
"minimax/minimax-m3-free",
|
|
434
|
+
"poolside/laguna-s-2.1-free",
|
|
435
|
+
"sakana/fugu-ultra",
|
|
436
|
+
"stepfun/Step-3.7-Flash",
|
|
437
|
+
"stepfun/Step-5-Preview",
|
|
438
|
+
"xai/grok-4.5",
|
|
439
|
+
"xai/grok-4.6",
|
|
440
|
+
"xiaomi/mimo-v2.6-flash",
|
|
441
|
+
"xiaomi/mimo-v2.6-pro",
|
|
442
|
+
"xiaomi/mimo-v2.6-pro-ultraspeed",
|
|
443
|
+
"z-ai/glm-5.3-flashx",
|
|
444
|
+
"zai-org/GLM-5.2-Fast"
|
|
445
|
+
]);
|
|
446
|
+
/**
|
|
447
|
+
* Whether the CLI snapshot lists a ZDR-capable upstream for `modelId`. This is
|
|
448
|
+
* informational, never a reason to omit the header when ZDR is enabled: the
|
|
449
|
+
* provider may add coverage or lack capacity after this snapshot was taken.
|
|
450
|
+
*/
|
|
451
|
+
function supportsZeroDataRetention(modelId) {
|
|
452
|
+
return !KNOWN_NON_ZDR_MODELS.has(modelId);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
368
455
|
* Models the official CLI's model table (command-code@1.53.0) marks
|
|
369
456
|
* `reasoning:!0` but defines no selectable `reasoning_effort` levels — they
|
|
370
457
|
* think automatically, with Command Code driving the depth. This is the
|
|
@@ -508,6 +595,7 @@ const KNOWN_PLANS = {
|
|
|
508
595
|
"deepseek/deepseek-v4-flash-vision-exp": "go",
|
|
509
596
|
"deepseek/deepseek-v4-pro": "go",
|
|
510
597
|
"gpt-5.6-luna": "go",
|
|
598
|
+
"gpt-6-luna": "go",
|
|
511
599
|
"meituan/LongCat-2.0": "go",
|
|
512
600
|
"inclusionai/ling-3.0-flash-sante:free": "go",
|
|
513
601
|
"meta/muse-spark-1.2-contributor": "go",
|
|
@@ -558,11 +646,13 @@ const KNOWN_PLANS = {
|
|
|
558
646
|
"gpt-5.4-mini": "pro",
|
|
559
647
|
"gpt-5.5": "pro",
|
|
560
648
|
"gpt-5.6-terra": "pro",
|
|
649
|
+
"gpt-6-sol": "pro",
|
|
561
650
|
"meta/muse-spark-1.1": "pro",
|
|
562
651
|
"claude-fable-5-1": "provider",
|
|
563
652
|
"claude-fable-5": "provider",
|
|
564
653
|
"claude-opus-4-7": "provider",
|
|
565
654
|
"claude-opus-4-8": "provider",
|
|
655
|
+
"claude-opus-5-5": "provider",
|
|
566
656
|
"claude-opus-5": "provider",
|
|
567
657
|
"gpt-6-astra": "provider",
|
|
568
658
|
"sakana/fugu-ultra": "provider"
|
|
@@ -955,6 +1045,15 @@ const MODEL_PRICE_ROWS = [
|
|
|
955
1045
|
6.25
|
|
956
1046
|
]
|
|
957
1047
|
},
|
|
1048
|
+
{
|
|
1049
|
+
id: "claude-opus-5-5",
|
|
1050
|
+
rates: [
|
|
1051
|
+
4,
|
|
1052
|
+
20,
|
|
1053
|
+
.2,
|
|
1054
|
+
5
|
|
1055
|
+
]
|
|
1056
|
+
},
|
|
958
1057
|
{
|
|
959
1058
|
id: "claude-sonnet-4-6",
|
|
960
1059
|
rates: [
|
|
@@ -1274,6 +1373,52 @@ const MODEL_PRICE_ROWS = [
|
|
|
1274
1373
|
25
|
|
1275
1374
|
] }]
|
|
1276
1375
|
},
|
|
1376
|
+
{
|
|
1377
|
+
id: "gpt-6-luna",
|
|
1378
|
+
rates: [
|
|
1379
|
+
.1,
|
|
1380
|
+
.5,
|
|
1381
|
+
.01,
|
|
1382
|
+
.125
|
|
1383
|
+
],
|
|
1384
|
+
contextTiers: [{
|
|
1385
|
+
"maxContext": 272e3,
|
|
1386
|
+
"rates": [
|
|
1387
|
+
.1,
|
|
1388
|
+
.5,
|
|
1389
|
+
.01,
|
|
1390
|
+
.125
|
|
1391
|
+
]
|
|
1392
|
+
}, { "rates": [
|
|
1393
|
+
.2,
|
|
1394
|
+
.75,
|
|
1395
|
+
.02,
|
|
1396
|
+
.25
|
|
1397
|
+
] }]
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
id: "gpt-6-sol",
|
|
1401
|
+
rates: [
|
|
1402
|
+
2,
|
|
1403
|
+
10,
|
|
1404
|
+
.2,
|
|
1405
|
+
2.5
|
|
1406
|
+
],
|
|
1407
|
+
contextTiers: [{
|
|
1408
|
+
"maxContext": 272e3,
|
|
1409
|
+
"rates": [
|
|
1410
|
+
2,
|
|
1411
|
+
10,
|
|
1412
|
+
.2,
|
|
1413
|
+
2.5
|
|
1414
|
+
]
|
|
1415
|
+
}, { "rates": [
|
|
1416
|
+
4,
|
|
1417
|
+
15,
|
|
1418
|
+
.4,
|
|
1419
|
+
5
|
|
1420
|
+
] }]
|
|
1421
|
+
},
|
|
1277
1422
|
{
|
|
1278
1423
|
id: "grok-4.5",
|
|
1279
1424
|
rates: [
|
|
@@ -1856,11 +2001,11 @@ function costGroupKey(group, table) {
|
|
|
1856
2001
|
//#endregion
|
|
1857
2002
|
//#region src/cost-projection.ts
|
|
1858
2003
|
const record$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
1859
|
-
const finite = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
2004
|
+
const finite$1 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
1860
2005
|
function tokensOf(value) {
|
|
1861
2006
|
const u = record$2(value);
|
|
1862
|
-
if (!u || !finite(u.inputTokens) || !finite(u.outputTokens)) return void 0;
|
|
1863
|
-
if (u.cacheReadTokens !== void 0 && !finite(u.cacheReadTokens) || u.cacheWriteTokens !== void 0 && !finite(u.cacheWriteTokens)) return void 0;
|
|
2007
|
+
if (!u || !finite$1(u.inputTokens) || !finite$1(u.outputTokens)) return void 0;
|
|
2008
|
+
if (u.cacheReadTokens !== void 0 && !finite$1(u.cacheReadTokens) || u.cacheWriteTokens !== void 0 && !finite$1(u.cacheWriteTokens)) return void 0;
|
|
1864
2009
|
return {
|
|
1865
2010
|
uncachedInputTokens: u.inputTokens,
|
|
1866
2011
|
outputTokens: u.outputTokens,
|
|
@@ -1886,13 +2031,13 @@ function usageOf(type, data) {
|
|
|
1886
2031
|
}
|
|
1887
2032
|
function parseState(value) {
|
|
1888
2033
|
const s = record$2(value), facts = record$2(s?.facts);
|
|
1889
|
-
if (!s || !facts || !finite(facts.pricingKey) || !Array.isArray(facts.groups)) throw new TypeError("invalid commandCodeCost state");
|
|
2034
|
+
if (!s || !facts || !finite$1(facts.pricingKey) || !Array.isArray(facts.groups)) throw new TypeError("invalid commandCodeCost state");
|
|
1890
2035
|
const group = (v) => {
|
|
1891
2036
|
const g = record$2(v), t = record$2(g?.tokens);
|
|
1892
|
-
return !!g && typeof g.provider === "string" && typeof g.model === "string" && (g.at === null || finite(g.at)) && finite(g.contextTokens) && !!t && COST_TOKEN_KEYS.every((k) => finite(t[k]));
|
|
2037
|
+
return !!g && typeof g.provider === "string" && typeof g.model === "string" && (g.at === null || finite$1(g.at)) && finite$1(g.contextTokens) && !!t && COST_TOKEN_KEYS.every((k) => finite$1(t[k]));
|
|
1893
2038
|
};
|
|
1894
2039
|
const sel = record$2(s.selection), last = record$2(s.last);
|
|
1895
|
-
if (!(s.selection === null || sel && typeof sel.provider === "string" && typeof sel.model === "string") || !(s.at === null || finite(s.at)) || !facts.groups.every(group) || !(s.last === null || last && finite(last.turn) && finite(last.step) && group(last.group))) throw new TypeError("invalid commandCodeCost state");
|
|
2040
|
+
if (!(s.selection === null || sel && typeof sel.provider === "string" && typeof sel.model === "string") || !(s.at === null || finite$1(s.at)) || !facts.groups.every(group) || !(s.last === null || last && finite$1(last.turn) && finite$1(last.step) && group(last.group))) throw new TypeError("invalid commandCodeCost state");
|
|
1896
2041
|
return value;
|
|
1897
2042
|
}
|
|
1898
2043
|
/** Optional registry uses only synchronous parse/view; no new runtime module is required. */
|
|
@@ -1926,11 +2071,11 @@ function createCostProjection(table = modelPriceTable()) {
|
|
|
1926
2071
|
}
|
|
1927
2072
|
if (event.type === "step/start" || event.type === "llm/retry-started") return {
|
|
1928
2073
|
...state,
|
|
1929
|
-
at: finite(event.time) ? event.time : null,
|
|
2074
|
+
at: finite$1(event.time) ? event.time : null,
|
|
1930
2075
|
last: null
|
|
1931
2076
|
};
|
|
1932
2077
|
const tokens = usageOf(event.type, data);
|
|
1933
|
-
if (tokens === void 0 || !finite(data.turn) || !finite(data.step)) return state;
|
|
2078
|
+
if (tokens === void 0 || !finite$1(data.turn) || !finite$1(data.step)) return state;
|
|
1934
2079
|
const previous = state.last?.turn === data.turn && state.last.step === data.step ? state.last.group : void 0;
|
|
1935
2080
|
const group = {
|
|
1936
2081
|
provider: state.selection?.provider ?? "",
|
|
@@ -2652,6 +2797,181 @@ function scaleBy(size, factor) {
|
|
|
2652
2797
|
};
|
|
2653
2798
|
}
|
|
2654
2799
|
//#endregion
|
|
2800
|
+
//#region src/stream-trace.ts
|
|
2801
|
+
/**
|
|
2802
|
+
* Opt-in raw-stream trace for the Command Code transports.
|
|
2803
|
+
*
|
|
2804
|
+
* Why this exists: this route's failure mode of record is a response that ENDS
|
|
2805
|
+
* EARLY. The gateway, an intermediary proxy, or the network closes the socket
|
|
2806
|
+
* mid-generation, so the adapter's read loop sees `done` without ever seeing
|
|
2807
|
+
* the transport's terminal event. From inside the adapter those two endings are
|
|
2808
|
+
* indistinguishable — the provider cut the stream, a proxy timed out, or the
|
|
2809
|
+
* socket was dropped — and the only artifact that separates them is the raw
|
|
2810
|
+
* wire: which events actually arrived, when the last one landed, how long the
|
|
2811
|
+
* socket then sat silent, and how it ended. Without that, a truncated answer is
|
|
2812
|
+
* a report with no evidence attached.
|
|
2813
|
+
*
|
|
2814
|
+
* Enable it per process:
|
|
2815
|
+
*
|
|
2816
|
+
* ```sh
|
|
2817
|
+
* DSH_COMMANDCODE_TRACE=/tmp/cc-stream.jsonl dsh web
|
|
2818
|
+
* DSH_COMMANDCODE_TRACE=1 dsh web # -> $TMPDIR/dsh-commandcode-stream.jsonl
|
|
2819
|
+
* ```
|
|
2820
|
+
*
|
|
2821
|
+
* Any value that is not a flag is a path, and the explicit off-spellings
|
|
2822
|
+
* (`0`, `false`, `no`, `off`) are recognized as "off" rather than being taken
|
|
2823
|
+
* for a file name.
|
|
2824
|
+
*
|
|
2825
|
+
* Every model request appends one JSONL record per raw chunk plus lifecycle
|
|
2826
|
+
* records (`response`, `eof`, `idle-timeout`, `read-error`, `end`,
|
|
2827
|
+
* `stream-close`), each stamped with `at` — milliseconds since that request's
|
|
2828
|
+
* first byte. The chunk records carry the provider's own payload verbatim
|
|
2829
|
+
* (bounded per chunk), because a truncated answer is exactly what is being
|
|
2830
|
+
* diagnosed: treat the file as conversation content, not as a log safe to
|
|
2831
|
+
* share. It never carries the request body, an API key, or any request header.
|
|
2832
|
+
*
|
|
2833
|
+
* Deliberate trade-offs:
|
|
2834
|
+
* - OFF costs one `process.env` read per request.
|
|
2835
|
+
* - ON appends synchronously, one file open per chunk. The trace must survive
|
|
2836
|
+
* a host killed mid-diagnosis, and the microseconds it costs are irrelevant
|
|
2837
|
+
* beside the seconds-scale silences it is there to measure.
|
|
2838
|
+
* - A trace that cannot be written turns itself off instead of failing the
|
|
2839
|
+
* request it is observing: a diagnostic must never become the outage.
|
|
2840
|
+
*
|
|
2841
|
+
* @module
|
|
2842
|
+
*/
|
|
2843
|
+
/** Environment variable that turns the trace on. */
|
|
2844
|
+
const STREAM_TRACE_ENV = "DSH_COMMANDCODE_TRACE";
|
|
2845
|
+
/** File used when the variable is set to a bare flag instead of a path. */
|
|
2846
|
+
const STREAM_TRACE_DEFAULT_FILE = "dsh-commandcode-stream.jsonl";
|
|
2847
|
+
/** One raw chunk is recorded verbatim up to this many characters. */
|
|
2848
|
+
const STREAM_TRACE_MAX_TEXT = 16384;
|
|
2849
|
+
/** Values of {@link STREAM_TRACE_ENV} that mean "trace to the default file". */
|
|
2850
|
+
const FLAG_VALUES = /* @__PURE__ */ new Set([
|
|
2851
|
+
"1",
|
|
2852
|
+
"true",
|
|
2853
|
+
"yes",
|
|
2854
|
+
"on"
|
|
2855
|
+
]);
|
|
2856
|
+
/**
|
|
2857
|
+
* Values that explicitly mean OFF.
|
|
2858
|
+
*
|
|
2859
|
+
* A user turning the trace off writes `false` as readily as they write `1` to
|
|
2860
|
+
* turn it on, and without this set every one of these spellings fell through to
|
|
2861
|
+
* "…so it must be a path" — the trace switched ON and wrote the conversation to
|
|
2862
|
+
* a file NAMED `false` (or `0`, or `off`) in the process's working directory.
|
|
2863
|
+
*/
|
|
2864
|
+
const OFF_VALUES = /* @__PURE__ */ new Set([
|
|
2865
|
+
"0",
|
|
2866
|
+
"false",
|
|
2867
|
+
"no",
|
|
2868
|
+
"off"
|
|
2869
|
+
]);
|
|
2870
|
+
/** The inert trace used whenever the switch is off. */
|
|
2871
|
+
const OFF = {
|
|
2872
|
+
enabled: false,
|
|
2873
|
+
record: () => void 0,
|
|
2874
|
+
close: () => void 0
|
|
2875
|
+
};
|
|
2876
|
+
/**
|
|
2877
|
+
* Resolve the trace file for this process.
|
|
2878
|
+
*
|
|
2879
|
+
* @param raw - the variable's raw value; defaults to the live environment.
|
|
2880
|
+
* @returns the file to append to, or undefined when the trace is off.
|
|
2881
|
+
*/
|
|
2882
|
+
function resolveStreamTracePath(raw = process.env[STREAM_TRACE_ENV]) {
|
|
2883
|
+
const value = raw?.trim();
|
|
2884
|
+
if (value === void 0 || value === "") return void 0;
|
|
2885
|
+
const lower = value.toLowerCase();
|
|
2886
|
+
if (OFF_VALUES.has(lower)) return void 0;
|
|
2887
|
+
if (FLAG_VALUES.has(lower)) return join(tmpdir(), STREAM_TRACE_DEFAULT_FILE);
|
|
2888
|
+
return value;
|
|
2889
|
+
}
|
|
2890
|
+
/**
|
|
2891
|
+
* Clamp one recorded payload so a single chunk cannot consume the whole cap.
|
|
2892
|
+
*
|
|
2893
|
+
* @param text - the text to record.
|
|
2894
|
+
* @param max - characters to keep, defaulting to {@link STREAM_TRACE_MAX_TEXT}.
|
|
2895
|
+
* @returns `text` unchanged, or its head plus a marker naming what was dropped.
|
|
2896
|
+
*/
|
|
2897
|
+
function boundTraceText(text, max = STREAM_TRACE_MAX_TEXT) {
|
|
2898
|
+
if (text.length <= max) return text;
|
|
2899
|
+
return `${text.slice(0, max)}…[+${text.length - max} chars]`;
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Open one request's trace.
|
|
2903
|
+
*
|
|
2904
|
+
* @param scope - what the records describe (the `provider/model` route).
|
|
2905
|
+
* @param options - `path` overrides the environment (tests pass a temp file);
|
|
2906
|
+
* `now` overrides the clock the `at` stamps are measured with.
|
|
2907
|
+
* @returns a writer that never throws and never fails the request.
|
|
2908
|
+
*/
|
|
2909
|
+
function openStreamTrace(scope, options = {}) {
|
|
2910
|
+
const path = options.path ?? resolveStreamTracePath();
|
|
2911
|
+
if (path === void 0) return OFF;
|
|
2912
|
+
const now = options.now ?? Date.now;
|
|
2913
|
+
const startedAt = now();
|
|
2914
|
+
let written = 0;
|
|
2915
|
+
let stopped = false;
|
|
2916
|
+
let capped = false;
|
|
2917
|
+
const terminals = /* @__PURE__ */ new Set();
|
|
2918
|
+
try {
|
|
2919
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2920
|
+
} catch {
|
|
2921
|
+
return OFF;
|
|
2922
|
+
}
|
|
2923
|
+
const write = (record) => {
|
|
2924
|
+
if (stopped) return;
|
|
2925
|
+
const terminal = record.event === "end" || record.event === "stream-close";
|
|
2926
|
+
if (capped && !terminal) return;
|
|
2927
|
+
if (terminal && terminals.has(String(record.event))) return;
|
|
2928
|
+
let line;
|
|
2929
|
+
try {
|
|
2930
|
+
line = `${JSON.stringify({
|
|
2931
|
+
at: now() - startedAt,
|
|
2932
|
+
scope,
|
|
2933
|
+
...record
|
|
2934
|
+
})}\n`;
|
|
2935
|
+
} catch {
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
if (terminal) {
|
|
2939
|
+
if (Buffer.byteLength(line) > 16384) return;
|
|
2940
|
+
terminals.add(String(record.event));
|
|
2941
|
+
} else if (written + Buffer.byteLength(line) > 4194304) {
|
|
2942
|
+
capped = true;
|
|
2943
|
+
line = `${JSON.stringify({
|
|
2944
|
+
at: now() - startedAt,
|
|
2945
|
+
scope,
|
|
2946
|
+
event: "trace-capped",
|
|
2947
|
+
bytes: written
|
|
2948
|
+
})}\n`;
|
|
2949
|
+
}
|
|
2950
|
+
try {
|
|
2951
|
+
appendFileSync(path, line);
|
|
2952
|
+
written += Buffer.byteLength(line);
|
|
2953
|
+
} catch {
|
|
2954
|
+
stopped = true;
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
write({ event: "stream-open" });
|
|
2958
|
+
return {
|
|
2959
|
+
enabled: true,
|
|
2960
|
+
record(event, fields) {
|
|
2961
|
+
write(fields === void 0 ? { event } : {
|
|
2962
|
+
event,
|
|
2963
|
+
...fields
|
|
2964
|
+
});
|
|
2965
|
+
},
|
|
2966
|
+
close() {
|
|
2967
|
+
write({
|
|
2968
|
+
event: "stream-close",
|
|
2969
|
+
bytes: written
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
//#endregion
|
|
2655
2975
|
//#region src/adapter.ts
|
|
2656
2976
|
/**
|
|
2657
2977
|
* DeepSeek Harness LLM adapter for the Command Code Provider API.
|
|
@@ -2674,7 +2994,7 @@ function scaleBy(size, factor) {
|
|
|
2674
2994
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
2675
2995
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
2676
2996
|
*/
|
|
2677
|
-
const COMMAND_CODE_CLI_VERSION = "1.
|
|
2997
|
+
const COMMAND_CODE_CLI_VERSION = "1.64.0";
|
|
2678
2998
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
2679
2999
|
const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
|
|
2680
3000
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
@@ -2767,15 +3087,21 @@ function periodEndValue(value) {
|
|
|
2767
3087
|
* Terminal stream-error markers from the official CLI (`Xw` in command-code's
|
|
2768
3088
|
* cli.mjs): these always mean "retrying cannot succeed", so the adapter must
|
|
2769
3089
|
* not classify them as transient server errors.
|
|
3090
|
+
*
|
|
3091
|
+
* Written with spaces rather than the CLI's underscores, because
|
|
3092
|
+
* {@link hasTerminalStreamMarker} normalizes the separator before matching:
|
|
3093
|
+
* a JSON body usually carries `premium credits exhausted` while the CLI's own
|
|
3094
|
+
* list is written `premium_credits_exhausted`, and both are the same refusal
|
|
3095
|
+
* (the pre-stream classifier normalizes identically).
|
|
2770
3096
|
*/
|
|
2771
3097
|
const TERMINAL_STREAM_ERROR_MARKERS = [
|
|
2772
|
-
"
|
|
2773
|
-
"
|
|
3098
|
+
"premium credits exhausted",
|
|
3099
|
+
"model not in plan",
|
|
2774
3100
|
"insufficient credits"
|
|
2775
3101
|
];
|
|
2776
3102
|
function hasTerminalStreamMarker(message) {
|
|
2777
|
-
const
|
|
2778
|
-
return TERMINAL_STREAM_ERROR_MARKERS.some((marker) =>
|
|
3103
|
+
const normalized = message.toLowerCase().replaceAll("_", " ");
|
|
3104
|
+
return TERMINAL_STREAM_ERROR_MARKERS.some((marker) => normalized.includes(marker));
|
|
2779
3105
|
}
|
|
2780
3106
|
/**
|
|
2781
3107
|
* Context-window wording the provider uses to reject a request that outgrew
|
|
@@ -2792,6 +3118,19 @@ function isContextOverflowDetail(detail) {
|
|
|
2792
3118
|
return isContextWindowExceededError(detail) || CLI_CONTEXT_OVERFLOW_PATTERN.test(detail);
|
|
2793
3119
|
}
|
|
2794
3120
|
/**
|
|
3121
|
+
* Whether one rejection says "ZDR requested, no ZDR-capable upstream": the
|
|
3122
|
+
* provider spells the code `cmd_zdr_no_providers` in JSON error bodies and
|
|
3123
|
+
* `CMD_ZDR_NO_PROVIDERS` in plain-text ones (the official CLI's own
|
|
3124
|
+
* classifier checks both spellings, and its error text tells the user to
|
|
3125
|
+
* unset `CMD_ZDR`). Underscore-separated matching normalizes the separator, so
|
|
3126
|
+
* one comparison covers the variants — the same normalization the pre-stream
|
|
3127
|
+
* rejection classifier and `hasTerminalStreamMarker` already use.
|
|
3128
|
+
*/
|
|
3129
|
+
const ZDR_NO_PROVIDERS_PATTERN = /cmd[_\s-]?zdr[_\s-]?no[_\s-]?providers/i;
|
|
3130
|
+
function isZdrNoProviders(providerCode, providerDetail, errText) {
|
|
3131
|
+
return ZDR_NO_PROVIDERS_PATTERN.test(providerCode ?? "") || ZDR_NO_PROVIDERS_PATTERN.test(providerDetail) || ZDR_NO_PROVIDERS_PATTERN.test(errText);
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
2795
3134
|
* Classify one in-band stream `error` payload into the failure the caller
|
|
2796
3135
|
* throws. Shared by both transports — the CLI transport delivers it as an
|
|
2797
3136
|
* `error` event (`{ type: 'error', error }`), the Provider API transport as a
|
|
@@ -2814,7 +3153,10 @@ function isContextOverflowDetail(detail) {
|
|
|
2814
3153
|
* (issue #39).
|
|
2815
3154
|
* - Credits/plan wording is terminal, so an exhausted balance or a model
|
|
2816
3155
|
* outside the plan is not retried as if it were a transient rate limit.
|
|
2817
|
-
* - Only then does the status/`isRetryable` pair decide
|
|
3156
|
+
* - Only then does the status/`isRetryable` pair decide — and an explicit
|
|
3157
|
+
* refusal (`isRetryable: false`) or a terminal marker OUTRANKS a status, so a
|
|
3158
|
+
* 5xx carrying "insufficient credits" stays terminal instead of entering the
|
|
3159
|
+
* retry cadence as `SERVER`.
|
|
2818
3160
|
*/
|
|
2819
3161
|
function streamErrorToLlmError(value, fallbackMessage) {
|
|
2820
3162
|
const record = isRecord(value) ? value : void 0;
|
|
@@ -2829,7 +3171,7 @@ function streamErrorToLlmError(value, fallbackMessage) {
|
|
|
2829
3171
|
const statusOption = statusCode !== void 0 ? { status: statusCode } : void 0;
|
|
2830
3172
|
if (isContextOverflowDetail(detail)) return new LlmError(`Command Code stream error: ${message};Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, statusOption);
|
|
2831
3173
|
const terminal = hasTerminalStreamMarker(detail);
|
|
2832
|
-
if (!(isRetryable === true || (statusCode !== void 0 ? statusCode !== void 0 && (statusCode === 429 || statusCode >= 500) :
|
|
3174
|
+
if (!(isRetryable === true || (isRetryable === false || terminal ? false : statusCode !== void 0 ? statusCode !== void 0 && (statusCode === 429 || statusCode >= 500) : true))) return new LlmError(`Command Code stream error: ${message}`, "PROVIDER_STREAM_ERROR", statusOption);
|
|
2833
3175
|
return new LlmError(`Command Code stream error: ${message}`, "SERVER", statusOption);
|
|
2834
3176
|
}
|
|
2835
3177
|
function recordOrEmpty(value) {
|
|
@@ -3033,42 +3375,32 @@ async function writeModelsCache(cachePath, models) {
|
|
|
3033
3375
|
}
|
|
3034
3376
|
}
|
|
3035
3377
|
/**
|
|
3036
|
-
*
|
|
3037
|
-
*
|
|
3038
|
-
*
|
|
3039
|
-
*
|
|
3040
|
-
*
|
|
3041
|
-
*
|
|
3042
|
-
*
|
|
3043
|
-
*
|
|
3044
|
-
*
|
|
3045
|
-
*
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
* `ToolMessageSource`), and the fallback is not cosmetic: `pairedToolCalls()`
|
|
3059
|
-
* counts a `tool-result` block from ANY message, so classifying such a
|
|
3060
|
-
* message as a plain user message would drop it at emission while its call
|
|
3061
|
-
* still counted as paired — leaving the assistant's tool call unanswered on
|
|
3062
|
-
* the wire, which the gateway rejects outright. FIRST block, not "any block":
|
|
3063
|
-
* the tool-result branch reads `content[0]` and skips anything else, so
|
|
3064
|
-
* claiming a mixed message as a tool result would discard its other blocks
|
|
3065
|
-
* instead of sending them.
|
|
3066
|
-
*/
|
|
3067
|
-
function isToolResultMessage(message) {
|
|
3068
|
-
if (message.role !== "user") return false;
|
|
3378
|
+
* >=0.1.7 puts toolCallId/isError on a role:'tool' message and carries raw
|
|
3379
|
+
* content blocks. Older engines wrap those fields in the first tool-result
|
|
3380
|
+
* block of a role:'user' message. Read structurally so the bundle still builds
|
|
3381
|
+
* and loads against older peers. Pairing and emission MUST use this same view:
|
|
3382
|
+
* otherwise a dropped result either drops its call too (making the model
|
|
3383
|
+
* forget completed work), or leaves an unanswered call on the wire.
|
|
3384
|
+
*
|
|
3385
|
+
* For the legacy envelope a present source tag remains authoritative; an
|
|
3386
|
+
* absent tag falls back to the first block (issue #47). The modern role is
|
|
3387
|
+
* itself the discriminator, as it is for the Harness's own serializers.
|
|
3388
|
+
*/
|
|
3389
|
+
function toolResultOf(message) {
|
|
3390
|
+
const current = message;
|
|
3391
|
+
if (current.role === "tool") {
|
|
3392
|
+
if (typeof current.toolCallId !== "string" || current.toolCallId === "") return void 0;
|
|
3393
|
+
return {
|
|
3394
|
+
toolCallId: current.toolCallId,
|
|
3395
|
+
isError: current.isError ?? false,
|
|
3396
|
+
content: current.content
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
if (message.role !== "user") return void 0;
|
|
3069
3400
|
const kind = message.source?.kind;
|
|
3070
|
-
if (kind !== void 0
|
|
3071
|
-
|
|
3401
|
+
if (kind !== void 0 && kind !== "tool") return void 0;
|
|
3402
|
+
const block = message.content[0];
|
|
3403
|
+
return block?.type === "tool-result" ? block : void 0;
|
|
3072
3404
|
}
|
|
3073
3405
|
/**
|
|
3074
3406
|
* Collect the tool calls that have a paired tool result, plus each call's
|
|
@@ -3081,12 +3413,13 @@ function pairedToolCalls(messages) {
|
|
|
3081
3413
|
const callIds = /* @__PURE__ */ new Set();
|
|
3082
3414
|
const names = /* @__PURE__ */ new Map();
|
|
3083
3415
|
const resultIds = /* @__PURE__ */ new Set();
|
|
3084
|
-
for (const message of messages)
|
|
3085
|
-
|
|
3416
|
+
for (const message of messages) {
|
|
3417
|
+
const result = toolResultOf(message);
|
|
3418
|
+
if (result) resultIds.add(result.toolCallId);
|
|
3419
|
+
for (const block of message.content) if (message.role === "assistant" && block.type === "tool-call") {
|
|
3086
3420
|
callIds.add(block.id);
|
|
3087
3421
|
names.set(block.id, block.name);
|
|
3088
3422
|
}
|
|
3089
|
-
if (block.type === "tool-result") resultIds.add(block.toolCallId);
|
|
3090
3423
|
}
|
|
3091
3424
|
return {
|
|
3092
3425
|
ids: new Set([...callIds].filter((id) => resultIds.has(id))),
|
|
@@ -3394,7 +3727,8 @@ async function messagesToCC(messages, readImage) {
|
|
|
3394
3727
|
};
|
|
3395
3728
|
for (const message of messages) {
|
|
3396
3729
|
if (message.role === "system") continue;
|
|
3397
|
-
|
|
3730
|
+
const result = toolResultOf(message);
|
|
3731
|
+
if (message.role === "user" && !result) {
|
|
3398
3732
|
flushPendingImages();
|
|
3399
3733
|
const parts = [];
|
|
3400
3734
|
for (const block of message.content) {
|
|
@@ -3437,18 +3771,17 @@ async function messagesToCC(messages, readImage) {
|
|
|
3437
3771
|
});
|
|
3438
3772
|
continue;
|
|
3439
3773
|
}
|
|
3440
|
-
if (
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
const
|
|
3444
|
-
const wireToolCallId = wireIds.get(block.toolCallId) ?? block.toolCallId;
|
|
3774
|
+
if (result) {
|
|
3775
|
+
if (!paired.has(result.toolCallId)) continue;
|
|
3776
|
+
const media = toolResultMedia(result);
|
|
3777
|
+
const wireToolCallId = wireIds.get(result.toolCallId) ?? result.toolCallId;
|
|
3445
3778
|
out.push({
|
|
3446
3779
|
role: "tool",
|
|
3447
3780
|
content: [{
|
|
3448
3781
|
type: "tool-result",
|
|
3449
3782
|
toolCallId: wireToolCallId,
|
|
3450
|
-
toolName: toolNames.get(
|
|
3451
|
-
output:
|
|
3783
|
+
toolName: toolNames.get(result.toolCallId) || "unknown",
|
|
3784
|
+
output: result.isError ? {
|
|
3452
3785
|
type: "error-text",
|
|
3453
3786
|
value: toolResultTextForWire(media)
|
|
3454
3787
|
} : {
|
|
@@ -3506,7 +3839,8 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
3506
3839
|
};
|
|
3507
3840
|
for (const message of messages) {
|
|
3508
3841
|
if (message.role === "system") continue;
|
|
3509
|
-
|
|
3842
|
+
const result = toolResultOf(message);
|
|
3843
|
+
if (message.role === "user" && !result) {
|
|
3510
3844
|
flushPendingImages();
|
|
3511
3845
|
const parts = [];
|
|
3512
3846
|
for (const block of message.content) {
|
|
@@ -3552,11 +3886,10 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
3552
3886
|
out.push(assistant);
|
|
3553
3887
|
continue;
|
|
3554
3888
|
}
|
|
3555
|
-
if (
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
const
|
|
3559
|
-
const wireToolCallId = wireIds.get(block.toolCallId) ?? block.toolCallId;
|
|
3889
|
+
if (result) {
|
|
3890
|
+
if (!paired.has(result.toolCallId)) continue;
|
|
3891
|
+
const media = toolResultMedia(result);
|
|
3892
|
+
const wireToolCallId = wireIds.get(result.toolCallId) ?? result.toolCallId;
|
|
3560
3893
|
out.push({
|
|
3561
3894
|
role: "tool",
|
|
3562
3895
|
tool_call_id: wireToolCallId,
|
|
@@ -3761,6 +4094,7 @@ async function connectGenerate(deps, key, protocol, body) {
|
|
|
3761
4094
|
if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
|
|
3762
4095
|
};
|
|
3763
4096
|
let response;
|
|
4097
|
+
const zdr = connection.zdr === true;
|
|
3764
4098
|
const headers = protocol === "cli" ? {
|
|
3765
4099
|
"Content-Type": "application/json",
|
|
3766
4100
|
Authorization: `Bearer ${key}`,
|
|
@@ -3769,11 +4103,13 @@ async function connectGenerate(deps, key, protocol, body) {
|
|
|
3769
4103
|
"x-project-slug": projectSlugFromPath(connection.workingDir),
|
|
3770
4104
|
"x-taste-learning": "true",
|
|
3771
4105
|
"x-co-flag": "false",
|
|
4106
|
+
...zdr ? { "x-cmd-zdr": "1" } : {},
|
|
3772
4107
|
...attributionHeaders()
|
|
3773
4108
|
} : {
|
|
3774
4109
|
"Content-Type": "application/json",
|
|
3775
4110
|
Authorization: `Bearer ${key}`,
|
|
3776
4111
|
Accept: "text/event-stream",
|
|
4112
|
+
...zdr ? { "x-cmd-zdr": "1" } : {},
|
|
3777
4113
|
...attributionHeaders()
|
|
3778
4114
|
};
|
|
3779
4115
|
try {
|
|
@@ -3817,6 +4153,7 @@ function createBlockAssembler() {
|
|
|
3817
4153
|
reasoningIndex: -1,
|
|
3818
4154
|
reasoningContent: "",
|
|
3819
4155
|
sawContent: false,
|
|
4156
|
+
finishReason: void 0,
|
|
3820
4157
|
openAiToolCalls: []
|
|
3821
4158
|
};
|
|
3822
4159
|
}
|
|
@@ -4036,8 +4373,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4036
4373
|
/** The headers every authenticated account endpoint shares. */
|
|
4037
4374
|
async accountHeaders(apiKey) {
|
|
4038
4375
|
const connection = this.deps.options();
|
|
4376
|
+
const raw = apiKey ?? await this.deps.resolveApiKey(connection);
|
|
4377
|
+
const named = connection.apiKeyEnv;
|
|
4039
4378
|
return {
|
|
4040
|
-
Authorization: `Bearer ${
|
|
4379
|
+
Authorization: `Bearer ${assertUsableApiKey(raw, "llm-commandcode", typeof named === "string" && named !== "" ? named : "the stored credential")}`,
|
|
4041
4380
|
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
4042
4381
|
"x-cli-environment": "production",
|
|
4043
4382
|
...attributionHeaders()
|
|
@@ -4049,11 +4388,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4049
4388
|
* failure accounting: the billing probe fails open silently, the usage
|
|
4050
4389
|
* report books failures per endpoint. Non-2xx and non-record bodies come
|
|
4051
4390
|
* back without a record; only a transport throw propagates to the caller.
|
|
4391
|
+
*
|
|
4392
|
+
* `timeoutMs` is the caller's budget for this one request. The default is the
|
|
4393
|
+
* CATALOG probe's short cap, which fits the picker's fail-open reads; the
|
|
4394
|
+
* usage report deliberately passes the connection's own request budget so an
|
|
4395
|
+
* account query is not held to a stricter (and invisible) limit than chat.
|
|
4052
4396
|
*/
|
|
4053
|
-
async fetchEndpointJson(url, headers) {
|
|
4397
|
+
async fetchEndpointJson(url, headers, timeoutMs = MODELS_TIMEOUT_MS) {
|
|
4054
4398
|
const response = await this.fetchImpl(url, {
|
|
4055
4399
|
headers,
|
|
4056
|
-
signal: AbortSignal.timeout(
|
|
4400
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4057
4401
|
});
|
|
4058
4402
|
if (!response.ok) return { status: response.status };
|
|
4059
4403
|
const parsed = await response.json();
|
|
@@ -4211,15 +4555,39 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4211
4555
|
* view. Requires a usable API key (throws `MISSING_CREDENTIAL` otherwise).
|
|
4212
4556
|
* Pass `apiKey` to report on a specific account of a multi-account pool;
|
|
4213
4557
|
* the default resolves the currently active account.
|
|
4558
|
+
*
|
|
4559
|
+
* Two failure modes are NOT "the network is down", and the report must say
|
|
4560
|
+
* so rather than let {@link classifyTotalFailure} blame the connection:
|
|
4561
|
+
*
|
|
4562
|
+
* - A key that no HTTP header can carry (a stray newline from a paste, a
|
|
4563
|
+
* full-width character) makes `fetch` throw a `TypeError` **before any
|
|
4564
|
+
* I/O**, for every endpoint at once. The chat path refuses such a key
|
|
4565
|
+
* through `assertUsableApiKey`; this path must too, and it answers
|
|
4566
|
+
* `blocked: 'invalid-key'` instead of four phantom transport failures.
|
|
4567
|
+
* - The four endpoints get the SAME per-request budget as a chat call
|
|
4568
|
+
* (`connection.requestTimeoutMs`, default 60 s), not the catalog's 10 s
|
|
4569
|
+
* probe budget: on a slow link a 10 s cap made every account query time
|
|
4570
|
+
* out while chat kept working, which is exactly a "check your network"
|
|
4571
|
+
* banner that never clears.
|
|
4214
4572
|
*/
|
|
4215
4573
|
async getUsage(apiKey) {
|
|
4216
|
-
const
|
|
4217
|
-
const
|
|
4574
|
+
const connection = this.deps.options();
|
|
4575
|
+
const base = connection.apiBase;
|
|
4576
|
+
let headers;
|
|
4577
|
+
try {
|
|
4578
|
+
headers = await this.accountHeaders(apiKey);
|
|
4579
|
+
} catch (error) {
|
|
4580
|
+
if (error instanceof LlmError && error.code === "INVALID_CREDENTIAL") return {
|
|
4581
|
+
failures: [error.message],
|
|
4582
|
+
blocked: "invalid-key"
|
|
4583
|
+
};
|
|
4584
|
+
throw error;
|
|
4585
|
+
}
|
|
4218
4586
|
const failures = [];
|
|
4219
4587
|
const failedStatuses = [];
|
|
4220
4588
|
const getJson = async (path) => {
|
|
4221
4589
|
try {
|
|
4222
|
-
const { status, record } = await this.fetchEndpointJson(`${base}${path}`, headers);
|
|
4590
|
+
const { status, record } = await this.fetchEndpointJson(`${base}${path}`, headers, connection.requestTimeoutMs);
|
|
4223
4591
|
if (record === void 0) {
|
|
4224
4592
|
failures.push(`${path}: HTTP ${status}`);
|
|
4225
4593
|
failedStatuses.push(status);
|
|
@@ -4305,7 +4673,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4305
4673
|
reported = true;
|
|
4306
4674
|
if (!window.exceeded) continue;
|
|
4307
4675
|
exceeded = true;
|
|
4308
|
-
resetAt = Math.max(resetAt, window.resetAt);
|
|
4676
|
+
if (window.resetAt > 0 && window.resetAt - Date.now() <= MAX_TRUSTED_RESET_MS) resetAt = Math.max(resetAt, window.resetAt);
|
|
4309
4677
|
}
|
|
4310
4678
|
if (!reported) return void 0;
|
|
4311
4679
|
return {
|
|
@@ -4413,6 +4781,19 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4413
4781
|
const reader = response.body.getReader();
|
|
4414
4782
|
const decoder = new TextDecoder();
|
|
4415
4783
|
let buffer = "";
|
|
4784
|
+
const trace = openStreamTrace(`${options.provider}/${options.model}`);
|
|
4785
|
+
const streamStartedAt = Date.now();
|
|
4786
|
+
let chunkCount = 0;
|
|
4787
|
+
let totalBytes = 0;
|
|
4788
|
+
let lastChunkAt = streamStartedAt;
|
|
4789
|
+
trace.record("response", {
|
|
4790
|
+
protocol,
|
|
4791
|
+
endpoint: protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`,
|
|
4792
|
+
status: response.status,
|
|
4793
|
+
attempts: tried.size,
|
|
4794
|
+
maxTokens,
|
|
4795
|
+
...reasoningEffort === void 0 ? {} : { reasoningEffort }
|
|
4796
|
+
});
|
|
4416
4797
|
let idleTimer;
|
|
4417
4798
|
let idleFired = false;
|
|
4418
4799
|
const armIdle = () => {
|
|
@@ -4429,9 +4810,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4429
4810
|
}
|
|
4430
4811
|
};
|
|
4431
4812
|
const asm = createBlockAssembler();
|
|
4432
|
-
|
|
4813
|
+
let finish;
|
|
4814
|
+
let usage;
|
|
4815
|
+
let usageEmitted = false;
|
|
4816
|
+
let endRecorded = false;
|
|
4817
|
+
function* handle(event) {
|
|
4818
|
+
for (const chunk of handleEvent(asm, protocol, event)) if (chunk.type === "finish") finish = chunk;
|
|
4819
|
+
else if (chunk.type === "usage") usage = chunk.usage;
|
|
4820
|
+
else yield chunk;
|
|
4821
|
+
}
|
|
4433
4822
|
try {
|
|
4434
|
-
let finished = false;
|
|
4435
4823
|
for (;;) {
|
|
4436
4824
|
let read;
|
|
4437
4825
|
armIdle();
|
|
@@ -4439,44 +4827,104 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4439
4827
|
read = await reader.read();
|
|
4440
4828
|
} catch (error) {
|
|
4441
4829
|
if (options.signal?.aborted) throw error;
|
|
4830
|
+
trace.record("read-error", {
|
|
4831
|
+
chunks: chunkCount,
|
|
4832
|
+
bytes: totalBytes,
|
|
4833
|
+
silentMs: Date.now() - lastChunkAt,
|
|
4834
|
+
message: errorChain(error)
|
|
4835
|
+
});
|
|
4442
4836
|
throw new LlmError(`Command Code API stream from ${connection.apiBase} failed while reading: ${errorChain(error)};Command Code API 流式响应中途断开——网络波动所致,重试通常可恢复`, "TRANSPORT", { cause: error });
|
|
4443
4837
|
} finally {
|
|
4444
4838
|
clearIdle();
|
|
4445
4839
|
}
|
|
4446
4840
|
const { done, value } = read;
|
|
4447
4841
|
if (done) {
|
|
4842
|
+
trace.record("eof", {
|
|
4843
|
+
chunks: chunkCount,
|
|
4844
|
+
bytes: totalBytes,
|
|
4845
|
+
silentMs: Date.now() - lastChunkAt,
|
|
4846
|
+
totalMs: Date.now() - streamStartedAt,
|
|
4847
|
+
finishSeen: finish !== void 0,
|
|
4848
|
+
idleFired,
|
|
4849
|
+
buffered: buffer.length
|
|
4850
|
+
});
|
|
4448
4851
|
if (idleFired) throw new LlmError(`Command Code API stream from ${connection.apiBase} was idle for ${connection.streamIdleTimeoutMs}ms (no events) and was treated as a dead connection;Command Code API 流式响应已 ${connection.streamIdleTimeoutMs} 毫秒无任何事件,被判定为死连接——长思考模型可在设置中调大流空闲超时`, "TIMEOUT");
|
|
4449
|
-
if (buffer.trim())
|
|
4450
|
-
yield chunk;
|
|
4451
|
-
if (chunk.type === "finish") finished = true;
|
|
4452
|
-
}
|
|
4852
|
+
if (buffer.trim()) yield* handle(parseStreamEventLine(buffer));
|
|
4453
4853
|
break;
|
|
4454
4854
|
}
|
|
4455
|
-
|
|
4855
|
+
const text = decoder.decode(value, { stream: true });
|
|
4856
|
+
buffer += text;
|
|
4857
|
+
chunkCount += 1;
|
|
4858
|
+
totalBytes += value.byteLength;
|
|
4859
|
+
lastChunkAt = Date.now();
|
|
4860
|
+
trace.record("chunk", {
|
|
4861
|
+
n: chunkCount,
|
|
4862
|
+
bytes: value.byteLength,
|
|
4863
|
+
text: boundTraceText(text)
|
|
4864
|
+
});
|
|
4456
4865
|
const lines = buffer.split("\n");
|
|
4457
4866
|
buffer = lines.pop() ?? "";
|
|
4458
|
-
for (const line of lines)
|
|
4459
|
-
|
|
4460
|
-
for (const chunk of chunks) {
|
|
4461
|
-
yield chunk;
|
|
4462
|
-
if (chunk.type === "finish") finished = true;
|
|
4463
|
-
}
|
|
4464
|
-
}
|
|
4465
|
-
if (finished) break;
|
|
4867
|
+
for (const line of lines) yield* handle(parseStreamEventLine(line));
|
|
4868
|
+
if (finish !== void 0) break;
|
|
4466
4869
|
}
|
|
4467
|
-
if (
|
|
4468
|
-
|
|
4469
|
-
yield* closeReasoning(asm);
|
|
4470
|
-
if (protocol === "openai") yield* emitOpenAiToolCalls(asm);
|
|
4471
|
-
if (!asm.sawContent) throw new LlmError("Command Code returned an empty response;Command Code 返回了空响应,重试通常可恢复", "EMPTY_RESPONSE");
|
|
4870
|
+
if (usage !== void 0) {
|
|
4871
|
+
usageEmitted = true;
|
|
4472
4872
|
yield {
|
|
4473
|
-
type: "
|
|
4474
|
-
|
|
4873
|
+
type: "usage",
|
|
4874
|
+
usage
|
|
4475
4875
|
};
|
|
4476
4876
|
}
|
|
4877
|
+
if (finish !== void 0) {
|
|
4878
|
+
const failure = asm.sawContent ? void 0 : emptyCompletionError(asm.finishReason, maxTokens, usage);
|
|
4879
|
+
trace.record("end", {
|
|
4880
|
+
outcome: failure === void 0 ? "finished" : failure.code === "OUTPUT_TOKEN_LIMIT" ? "output-token-limit" : "empty",
|
|
4881
|
+
finishReason: asm.finishReason,
|
|
4882
|
+
...failure === void 0 ? {} : { code: failure.code },
|
|
4883
|
+
maxTokens,
|
|
4884
|
+
...usage === void 0 ? {} : { usage },
|
|
4885
|
+
chunks: chunkCount,
|
|
4886
|
+
bytes: totalBytes,
|
|
4887
|
+
totalMs: Date.now() - streamStartedAt,
|
|
4888
|
+
sawContent: asm.sawContent
|
|
4889
|
+
});
|
|
4890
|
+
endRecorded = true;
|
|
4891
|
+
if (failure !== void 0) throw failure;
|
|
4892
|
+
yield finish;
|
|
4893
|
+
} else {
|
|
4894
|
+
const elapsed = Date.now() - streamStartedAt;
|
|
4895
|
+
const detail = `${chunkCount} chunk(s), ${totalBytes} bytes, ${elapsed}ms`;
|
|
4896
|
+
trace.record("end", {
|
|
4897
|
+
outcome: asm.sawContent ? "stream-closed" : "empty",
|
|
4898
|
+
chunks: chunkCount,
|
|
4899
|
+
bytes: totalBytes,
|
|
4900
|
+
totalMs: elapsed,
|
|
4901
|
+
sawContent: asm.sawContent
|
|
4902
|
+
});
|
|
4903
|
+
endRecorded = true;
|
|
4904
|
+
if (!asm.sawContent) throw new LlmError("Command Code returned an empty response;Command Code 返回了空响应,重试通常可恢复", "EMPTY_RESPONSE");
|
|
4905
|
+
throw new LlmError(`Command Code API stream from ${connection.apiBase} ended before its finish event (${detail}) — the response was closed mid-generation, so this is not the model stopping by itself;Command Code API 流式响应在结束事件之前被关闭(生成中途断流),并非模型主动结束——请重试;若频繁出现,可用 ${STREAM_TRACE_ENV}=<文件路径> 抓取原始流以定位断流原因`, "STREAM_CLOSED");
|
|
4906
|
+
}
|
|
4907
|
+
} catch (error) {
|
|
4908
|
+
if (!usageEmitted && usage !== void 0) yield {
|
|
4909
|
+
type: "usage",
|
|
4910
|
+
usage
|
|
4911
|
+
};
|
|
4912
|
+
if (!endRecorded) trace.record("end", {
|
|
4913
|
+
outcome: options.signal?.aborted ? "aborted" : "error",
|
|
4914
|
+
code: error instanceof LlmError ? error.code : "unknown",
|
|
4915
|
+
finishReason: asm.finishReason,
|
|
4916
|
+
maxTokens,
|
|
4917
|
+
...usage === void 0 ? {} : { usage },
|
|
4918
|
+
sawContent: asm.sawContent,
|
|
4919
|
+
chunks: chunkCount,
|
|
4920
|
+
bytes: totalBytes,
|
|
4921
|
+
totalMs: Date.now() - streamStartedAt
|
|
4922
|
+
});
|
|
4923
|
+
throw error;
|
|
4477
4924
|
} finally {
|
|
4478
4925
|
clearIdle();
|
|
4479
4926
|
cleanup();
|
|
4927
|
+
trace.close();
|
|
4480
4928
|
await reader.cancel().catch(() => void 0);
|
|
4481
4929
|
reader.releaseLock();
|
|
4482
4930
|
}
|
|
@@ -4517,6 +4965,7 @@ function generateHttpError(status, errText, retryAfterMs) {
|
|
|
4517
4965
|
const detail = providerCode ?? `HTTP ${status}`;
|
|
4518
4966
|
const overflowDetail = providerDetail !== "" ? providerDetail : errText.slice(0, 500);
|
|
4519
4967
|
if (status < 500 && isContextOverflowDetail(overflowDetail)) return new LlmError(`Command Code API error ${status}: the request exceeds the model's context window — this session is being compacted and the request retried;Command Code API 返回 ${status}:请求内容超出模型上下文窗口——正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, { status });
|
|
4968
|
+
if (status === 422 && isZdrNoProviders(providerCode, providerDetail, errText)) return new LlmError(`Command Code API error 422 (${detail}): zero data retention is enforced but no ZDR-capable upstream is available for this model — turn the ZDR setting off (or use another model);Command Code API 返回 422:已开启零数据保留(ZDR),但该模型当前没有可用的 ZDR 上游——请关闭 ZDR 开关,或改用其他模型`, "PROVIDER_HTTP_ERROR", { status: 422 });
|
|
4520
4969
|
if (status === 401) return new LlmError(`Command Code API error 401 (${detail}): the API key is missing or invalid — check the key stored for COMMANDCODE_API_KEY (Models page) or the auth file;Command Code API 返回 401:API 密钥缺失或无效——请在设置页检查 COMMANDCODE_API_KEY 存储的密钥,或检查 auth 文件`, "INVALID_CREDENTIAL", { status: 401 });
|
|
4521
4970
|
if (status === 413) return new LlmError("Command Code API error 413: the request body exceeds the provider's size limit (the cap is undocumented, measured at about 50 MB) — the session history is too large to send, usually because of accumulated image attachments. Start a new session, or drop the image-heavy part of this one, and retry;Command Code API 返回 413:请求体超过服务端的体积上限(官方未公开,实测约 50 MB)——会话历史过大,通常是历史图片累积所致。请新建会话,或移除本会话中图片较多的部分后重试", "PROVIDER_HTTP_ERROR", { status: 413 });
|
|
4522
4971
|
return new LlmError(`Command Code API error ${status}${detail === `HTTP ${status}` ? "" : ` (${detail})`}: ${errText.slice(0, 500)}`, httpErrorCode(status), {
|
|
@@ -4567,7 +5016,7 @@ function handleCliEvent(asm, event) {
|
|
|
4567
5016
|
}
|
|
4568
5017
|
const delta = stringValue(event.text) ?? "";
|
|
4569
5018
|
asm.textContent += delta;
|
|
4570
|
-
asm.sawContent = true;
|
|
5019
|
+
if (delta.trim() !== "") asm.sawContent = true;
|
|
4571
5020
|
chunks.push({
|
|
4572
5021
|
type: "text-delta",
|
|
4573
5022
|
index: asm.textIndex,
|
|
@@ -4630,6 +5079,7 @@ function handleCliEvent(asm, event) {
|
|
|
4630
5079
|
break;
|
|
4631
5080
|
}
|
|
4632
5081
|
case "finish": {
|
|
5082
|
+
asm.finishReason = stringValue(event.finishReason);
|
|
4633
5083
|
chunks.push(...closeText(asm), ...closeReasoning(asm));
|
|
4634
5084
|
const usage = isRecord(event.totalUsage) ? event.totalUsage : void 0;
|
|
4635
5085
|
if (usage) {
|
|
@@ -4643,6 +5093,8 @@ function handleCliEvent(asm, event) {
|
|
|
4643
5093
|
cacheReadTokens: cacheRead,
|
|
4644
5094
|
cacheWriteTokens: cacheWrite
|
|
4645
5095
|
};
|
|
5096
|
+
const reasoningTokens = numberValue((isRecord(usage.outputTokenDetails) ? usage.outputTokenDetails : void 0)?.reasoningTokens) ?? numberValue(usage.reasoningTokens);
|
|
5097
|
+
if (reasoningTokens !== void 0) tokenUsage.reasoningTokens = reasoningTokens;
|
|
4646
5098
|
chunks.push({
|
|
4647
5099
|
type: "usage",
|
|
4648
5100
|
usage: tokenUsage
|
|
@@ -4707,39 +5159,38 @@ function handleOpenAIEvent(asm, event) {
|
|
|
4707
5159
|
});
|
|
4708
5160
|
}
|
|
4709
5161
|
asm.textContent += contentDelta;
|
|
4710
|
-
asm.sawContent = true;
|
|
5162
|
+
if (contentDelta.trim() !== "") asm.sawContent = true;
|
|
4711
5163
|
chunks.push({
|
|
4712
5164
|
type: "text-delta",
|
|
4713
5165
|
index: asm.textIndex,
|
|
4714
5166
|
text: contentDelta
|
|
4715
5167
|
});
|
|
4716
5168
|
}
|
|
4717
|
-
if (Array.isArray(delta.tool_calls)) {
|
|
5169
|
+
if (Array.isArray(delta.tool_calls)) for (const rawCall of delta.tool_calls) {
|
|
5170
|
+
if (!isRecord(rawCall)) continue;
|
|
4718
5171
|
asm.sawContent = true;
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
if (name !== void 0 && existing.name === "") existing.name = name;
|
|
4738
|
-
if (argDelta !== void 0) existing.arguments += argDelta;
|
|
4739
|
-
}
|
|
5172
|
+
const callIndex = numberValue(rawCall.index) ?? 0;
|
|
5173
|
+
let existing = asm.openAiToolCalls.find((call) => call.index === callIndex);
|
|
5174
|
+
const fn = isRecord(rawCall.function) ? rawCall.function : void 0;
|
|
5175
|
+
const id = stringValue(rawCall.id);
|
|
5176
|
+
const name = fn === void 0 ? void 0 : stringValue(fn.name);
|
|
5177
|
+
const argDelta = fn === void 0 ? void 0 : stringValue(fn.arguments) ?? (fn.arguments === void 0 ? "" : JSON.stringify(fn.arguments));
|
|
5178
|
+
if (!existing) {
|
|
5179
|
+
existing = {
|
|
5180
|
+
index: callIndex,
|
|
5181
|
+
name: name ?? "",
|
|
5182
|
+
arguments: argDelta ?? ""
|
|
5183
|
+
};
|
|
5184
|
+
if (id !== void 0) existing.id = id;
|
|
5185
|
+
asm.openAiToolCalls.push(existing);
|
|
5186
|
+
} else {
|
|
5187
|
+
if (id !== void 0 && existing.id === void 0) existing.id = id;
|
|
5188
|
+
if (name !== void 0 && existing.name === "") existing.name = name;
|
|
5189
|
+
if (argDelta !== void 0) existing.arguments += argDelta;
|
|
4740
5190
|
}
|
|
4741
5191
|
}
|
|
4742
5192
|
if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
|
|
5193
|
+
asm.finishReason = stringValue(choice.finish_reason);
|
|
4743
5194
|
chunks.push(...closeText(asm), ...closeReasoning(asm), ...emitOpenAiToolCalls(asm));
|
|
4744
5195
|
if (event.usage !== void 0) chunks.push({
|
|
4745
5196
|
type: "usage",
|
|
@@ -4787,6 +5238,13 @@ function mapFinishReason(reason) {
|
|
|
4787
5238
|
if (reason === "length" || reason === "max_tokens" || reason === "max-tokens" || reason === "max_output_tokens") return { kind: "max-tokens" };
|
|
4788
5239
|
return { kind: "stop" };
|
|
4789
5240
|
}
|
|
5241
|
+
/** A terminal marker without text or tools is a failed attempt, not a completed turn. */
|
|
5242
|
+
function emptyCompletionError(reason, maxTokens, usage) {
|
|
5243
|
+
const detail = `finish_reason=${reason ?? "unknown"}, max_tokens=${maxTokens}, outputTokens=${usage?.outputTokens ?? "unknown"}, reasoningTokens=${usage?.reasoningTokens ?? "unknown"}`;
|
|
5244
|
+
if (mapFinishReason(reason).kind === "max-tokens") return new LlmError(`Command Code reached the output token limit without producing answer text or a tool call (${detail});Command Code 已达到输出 token 上限,但没有生成正文或工具调用;请调整思考强度,或在模型和接口上限内增加输出预算`, "OUTPUT_TOKEN_LIMIT");
|
|
5245
|
+
if (reason === "content_filter" || reason === "content-filter") return new LlmError(`Command Code filtered the response (${detail});Command Code 拦截了响应,未生成正文或工具调用`, "CONTENT_FILTER");
|
|
5246
|
+
return new LlmError(`Command Code finished without producing answer text or a tool call (${detail});Command Code 返回了空响应(可能只有思考内容),重试通常可恢复`, "EMPTY_RESPONSE");
|
|
5247
|
+
}
|
|
4790
5248
|
/**
|
|
4791
5249
|
* True when a Provider API pre-stream rejection is Command Code's Go-plan
|
|
4792
5250
|
* gate (`upgrade_required`). Only this exact class of rejection should fall
|
|
@@ -4831,8 +5289,25 @@ function mapFinishReason(reason) {
|
|
|
4831
5289
|
* or a model outside its plan (`MODEL_NOT_IN_PLAN`). Rotating must NOT mark
|
|
4832
5290
|
* the account: the fact is about the model or the balance, it can change
|
|
4833
5291
|
* without the key changing, and a `:free` model can still be served by an
|
|
4834
|
-
* account with an empty balance.
|
|
5292
|
+
* account with an empty balance. These codes are recognized on EVERY
|
|
5293
|
+
* status — a 5xx that proxies the provider's own credits/plan body is
|
|
5294
|
+
* still an account fact, and only the prose scan is confined to 4xx.
|
|
4835
5295
|
*/
|
|
5296
|
+
/**
|
|
5297
|
+
* The provider's structured "this account cannot serve this request" codes.
|
|
5298
|
+
*
|
|
5299
|
+
* Status-independent on purpose, mirroring the CLI's classifier, which reads
|
|
5300
|
+
* the code before any status guard. A 5xx that carries one of these must not
|
|
5301
|
+
* be retried as a transient provider failure: the same request against the
|
|
5302
|
+
* same account cannot succeed, so the caller rotates past it (without marking
|
|
5303
|
+
* the key) instead of replaying it on the harness's retry cadence.
|
|
5304
|
+
*/
|
|
5305
|
+
const ACCOUNT_UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
|
|
5306
|
+
"USAGE_EXCEEDED",
|
|
5307
|
+
"INSUFFICIENT_CREDITS",
|
|
5308
|
+
"PREMIUM_CREDITS_EXHAUSTED",
|
|
5309
|
+
"MODEL_NOT_IN_PLAN"
|
|
5310
|
+
]);
|
|
4836
5311
|
function classifyAccountRejection(status, errText) {
|
|
4837
5312
|
if (status === 429 || readProviderErrorCode(errText)?.toUpperCase() === "RATE_LIMITED") {
|
|
4838
5313
|
const evidence = readWindowLimitEvidence(errText);
|
|
@@ -4843,10 +5318,11 @@ function classifyAccountRejection(status, errText) {
|
|
|
4843
5318
|
};
|
|
4844
5319
|
}
|
|
4845
5320
|
if (status === 401) return { reason: "invalid-credential" };
|
|
5321
|
+
const code = readProviderErrorCode(errText)?.toUpperCase();
|
|
5322
|
+
if (code !== void 0 && ACCOUNT_UNAVAILABLE_CODES.has(code)) return { reason: "unavailable" };
|
|
4846
5323
|
if (status < 400 || status >= 500) return void 0;
|
|
4847
5324
|
const lower = errText.toLowerCase().replaceAll("_", " ");
|
|
4848
|
-
|
|
4849
|
-
if (code === "USAGE_EXCEEDED" || code === "INSUFFICIENT_CREDITS" || code === "PREMIUM_CREDITS_EXHAUSTED" || code === "MODEL_NOT_IN_PLAN" || lower.includes("insufficient credits") || lower.includes("premium credits exhausted") || lower.includes("model not in plan") || /insufficient (credit|balance)/.test(lower) || /out of credit/.test(lower) || /not (available|included)[^.]{0,40}plan/.test(lower) || lower.includes("upgrade your plan")) return { reason: "unavailable" };
|
|
5325
|
+
if (lower.includes("insufficient credits") || lower.includes("premium credits exhausted") || lower.includes("model not in plan") || /insufficient (credit|balance)/.test(lower) || /out of credit/.test(lower) || /not (available|included)[^.]{0,40}plan/.test(lower) || lower.includes("upgrade your plan")) return { reason: "unavailable" };
|
|
4850
5326
|
}
|
|
4851
5327
|
/** The machine-readable `error.code`/`error.type` of a pre-stream failure body. */
|
|
4852
5328
|
function readProviderErrorCode(errText) {
|
|
@@ -4860,6 +5336,16 @@ function readProviderErrorCode(errText) {
|
|
|
4860
5336
|
}
|
|
4861
5337
|
}
|
|
4862
5338
|
/**
|
|
5339
|
+
* How far ahead a provider-published reset may be and still be trusted.
|
|
5340
|
+
*
|
|
5341
|
+
* Generous on purpose: the weekly window is the longest this provider meters,
|
|
5342
|
+
* and the value is a plausibility bound rather than a policy. Beyond it the
|
|
5343
|
+
* reset is dropped rather than clamped, which leaves the account's mark
|
|
5344
|
+
* `unknown` — the state the pool's probe pass re-checks — instead of a
|
|
5345
|
+
* cooldown that never expires.
|
|
5346
|
+
*/
|
|
5347
|
+
const MAX_TRUSTED_RESET_MS = 2592e6;
|
|
5348
|
+
/**
|
|
4863
5349
|
* The usage-window evidence carried by a rejected request, mirroring the CLI's
|
|
4864
5350
|
* `parseWindowLimitError` → `resolveWindowLabel`/`extractResetAtMs` pair.
|
|
4865
5351
|
*
|
|
@@ -4893,7 +5379,8 @@ function readWindowLimitEvidence(errText) {
|
|
|
4893
5379
|
const window = named === "fiveHour" || named === "weekly" || named === "daily" ? named : /usage limit for your plan/i.test(message) || /usage limit for your plan/i.test(errText) ? /weekly/i.test(message) ? "weekly" : "fiveHour" : void 0;
|
|
4894
5380
|
const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(message)?.[1] ?? /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(errText)?.[1];
|
|
4895
5381
|
const stamped = stamp === void 0 ? void 0 : Date.parse(stamp);
|
|
4896
|
-
const
|
|
5382
|
+
const published = seconds !== void 0 && seconds > 0 ? Math.round(seconds * 1e3) : stamped !== void 0 && !Number.isNaN(stamped) ? stamped : void 0;
|
|
5383
|
+
const resetAtMs = published !== void 0 && Number.isFinite(published) && published - Date.now() <= MAX_TRUSTED_RESET_MS ? published : void 0;
|
|
4897
5384
|
return {
|
|
4898
5385
|
...window === void 0 ? {} : { window },
|
|
4899
5386
|
...resetAtMs === void 0 ? {} : { resetAtMs }
|
|
@@ -5693,7 +6180,10 @@ function isCallbackCredentials(value) {
|
|
|
5693
6180
|
/**
|
|
5694
6181
|
* One browser-login attempt machine. Single-flight by design: `begin()` while
|
|
5695
6182
|
* waiting returns the live attempt's status instead of starting a second one;
|
|
5696
|
-
* a
|
|
6183
|
+
* a start that is still binding its port is REJOINED through
|
|
6184
|
+
* {@link CommandCodeLoginFlow.begin}'s in-flight promise, because the status
|
|
6185
|
+
* face cannot say `waiting` until that bind settles; a terminal state makes the
|
|
6186
|
+
* next `begin()` start fresh.
|
|
5697
6187
|
*/
|
|
5698
6188
|
var CommandCodeLoginFlow = class {
|
|
5699
6189
|
deps;
|
|
@@ -5711,6 +6201,17 @@ var CommandCodeLoginFlow = class {
|
|
|
5711
6201
|
* the user cancelled and flipping the page back to success.
|
|
5712
6202
|
*/
|
|
5713
6203
|
attemptSeq = 0;
|
|
6204
|
+
/**
|
|
6205
|
+
* The start currently binding a port, if any. Every `begin()` that arrives
|
|
6206
|
+
* before it settles joins it: two independent starts would each bind their
|
|
6207
|
+
* own loopback server, and only the LAST one is reachable by
|
|
6208
|
+
* {@link CommandCodeLoginFlow.teardown} — the orphan keeps listening and
|
|
6209
|
+
* answering `/callback` for the process's lifetime, and ten of them exhaust
|
|
6210
|
+
* the port window so browser login dies until the Host restarts.
|
|
6211
|
+
*/
|
|
6212
|
+
starting;
|
|
6213
|
+
/** A `cancel()` that arrived while a start was still binding (see {@link CommandCodeLoginFlow.cancel}). */
|
|
6214
|
+
cancelPending = false;
|
|
5714
6215
|
disposed = false;
|
|
5715
6216
|
constructor(deps) {
|
|
5716
6217
|
this.deps = deps;
|
|
@@ -5732,6 +6233,22 @@ var CommandCodeLoginFlow = class {
|
|
|
5732
6233
|
async begin() {
|
|
5733
6234
|
if (this.disposed) throw new Error("login flow has been disposed");
|
|
5734
6235
|
if (this.statusValue.state === "waiting" && this.server !== void 0) return this.statusValue;
|
|
6236
|
+
if (this.starting !== void 0) return this.starting;
|
|
6237
|
+
this.cancelPending = false;
|
|
6238
|
+
const starting = this.startAttempt();
|
|
6239
|
+
this.starting = starting;
|
|
6240
|
+
try {
|
|
6241
|
+
return await starting;
|
|
6242
|
+
} finally {
|
|
6243
|
+
if (this.starting === starting) this.starting = void 0;
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
/**
|
|
6247
|
+
* The binding half of {@link CommandCodeLoginFlow.begin}: one attempt, one
|
|
6248
|
+
* server, one published `waiting` status. Callers reach it only through
|
|
6249
|
+
* `begin()`'s single-flight fence.
|
|
6250
|
+
*/
|
|
6251
|
+
async startAttempt() {
|
|
5735
6252
|
this.teardown();
|
|
5736
6253
|
const attempt = ++this.attemptSeq;
|
|
5737
6254
|
const port = await this.findPort();
|
|
@@ -5743,6 +6260,16 @@ var CommandCodeLoginFlow = class {
|
|
|
5743
6260
|
};
|
|
5744
6261
|
});
|
|
5745
6262
|
await this.bindServer(port, expectedState);
|
|
6263
|
+
if (this.disposed || this.cancelPending) {
|
|
6264
|
+
const cancelled = !this.disposed;
|
|
6265
|
+
this.cancelPending = false;
|
|
6266
|
+
this.teardown();
|
|
6267
|
+
if (cancelled) this.setStatus({
|
|
6268
|
+
state: "failed",
|
|
6269
|
+
reason: "cancelled"
|
|
6270
|
+
});
|
|
6271
|
+
return this.statusValue;
|
|
6272
|
+
}
|
|
5746
6273
|
const apiBase = this.readApiBase();
|
|
5747
6274
|
this.setStatus({
|
|
5748
6275
|
state: "waiting",
|
|
@@ -5764,19 +6291,29 @@ var CommandCodeLoginFlow = class {
|
|
|
5764
6291
|
settled.then((credentials) => this.complete(attempt, credentials), (failure) => this.failFrom(attempt, failure));
|
|
5765
6292
|
return this.statusValue;
|
|
5766
6293
|
}
|
|
5767
|
-
/**
|
|
6294
|
+
/**
|
|
6295
|
+
* Cancel a waiting attempt; terminal states are untouched. A start that is
|
|
6296
|
+
* still binding a port is flagged instead of ignored — it cannot be torn
|
|
6297
|
+
* down from here (its server does not exist yet), so `startAttempt` observes
|
|
6298
|
+
* the flag once the bind settles and retires it.
|
|
6299
|
+
*/
|
|
5768
6300
|
cancel() {
|
|
5769
|
-
if (this.disposed
|
|
5770
|
-
this.
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
6301
|
+
if (this.disposed) return;
|
|
6302
|
+
if (this.statusValue.state === "waiting") {
|
|
6303
|
+
this.teardown();
|
|
6304
|
+
this.setStatus({
|
|
6305
|
+
state: "failed",
|
|
6306
|
+
reason: "cancelled"
|
|
6307
|
+
});
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
if (this.starting !== void 0) this.cancelPending = true;
|
|
5775
6311
|
}
|
|
5776
6312
|
/** Stop everything; a waiting attempt ends cancelled. Idempotent. */
|
|
5777
6313
|
dispose() {
|
|
5778
6314
|
if (this.disposed) return;
|
|
5779
6315
|
this.disposed = true;
|
|
6316
|
+
this.cancelPending = false;
|
|
5780
6317
|
const wasWaiting = this.statusValue.state === "waiting";
|
|
5781
6318
|
this.teardown();
|
|
5782
6319
|
if (wasWaiting) this.setStatus({
|
|
@@ -5812,8 +6349,12 @@ var CommandCodeLoginFlow = class {
|
|
|
5812
6349
|
const server = createServer((request, response) => this.handleCallback(request, response, expectedState));
|
|
5813
6350
|
this.server = server;
|
|
5814
6351
|
server.once("error", (error) => {
|
|
5815
|
-
if (this.server !== server)
|
|
6352
|
+
if (this.server !== server) {
|
|
6353
|
+
closeServer(server);
|
|
6354
|
+
return;
|
|
6355
|
+
}
|
|
5816
6356
|
this.server = void 0;
|
|
6357
|
+
closeServer(server);
|
|
5817
6358
|
const tagged = new LoginSettleError("error", `Could not bind the login callback server on port ${port}: ${error.code ?? error.message}`);
|
|
5818
6359
|
if (binding) {
|
|
5819
6360
|
binding = false;
|
|
@@ -5982,11 +6523,17 @@ var CommandCodeLoginFlow = class {
|
|
|
5982
6523
|
/** Close the server and watchdog without touching the published status. */
|
|
5983
6524
|
teardown() {
|
|
5984
6525
|
this.clearTimer();
|
|
5985
|
-
this.server
|
|
6526
|
+
closeServer(this.server);
|
|
5986
6527
|
this.server = void 0;
|
|
5987
6528
|
this.settle = void 0;
|
|
5988
6529
|
}
|
|
5989
6530
|
};
|
|
6531
|
+
/** Close one loopback server without letting "not running"/"already closed" escape. */
|
|
6532
|
+
function closeServer(server) {
|
|
6533
|
+
try {
|
|
6534
|
+
server?.close();
|
|
6535
|
+
} catch {}
|
|
6536
|
+
}
|
|
5990
6537
|
/** A tagged settle failure carrying the stable copy reason. */
|
|
5991
6538
|
var LoginSettleError = class extends Error {
|
|
5992
6539
|
reason;
|
|
@@ -6060,7 +6607,8 @@ function selectCommandCodeSearchProvider(web, enable) {
|
|
|
6060
6607
|
function commandCodeSearchSelection() {
|
|
6061
6608
|
return {
|
|
6062
6609
|
owner: false,
|
|
6063
|
-
displaced: void 0
|
|
6610
|
+
displaced: void 0,
|
|
6611
|
+
preexisting: false
|
|
6064
6612
|
};
|
|
6065
6613
|
}
|
|
6066
6614
|
/**
|
|
@@ -6078,8 +6626,14 @@ function commandCodeSearchSelection() {
|
|
|
6078
6626
|
* auto-select — already says what the user wants.
|
|
6079
6627
|
* - When the field already reads `commandcode` at first touch (e.g. a
|
|
6080
6628
|
* surviving runtime the plugin did not set, or a manual
|
|
6081
|
-
* `searchProvider: commandcode` pin), `displaced` stays undefined
|
|
6082
|
-
* later disable is a no-op rather than a guess
|
|
6629
|
+
* `searchProvider: commandcode` pin), `displaced` stays undefined and
|
|
6630
|
+
* `preexisting` is set, so the later disable is a no-op rather than a guess
|
|
6631
|
+
* at the factory default. A "no-op" means the field is left ALONE: writing
|
|
6632
|
+
* that `undefined` back would destroy the user's own pin, and dsh-web reads
|
|
6633
|
+
* a cleared `searchProviderId` as auto-select — where a second usable
|
|
6634
|
+
* provider (the shipped `deepseek-official` is usable whenever a DeepSeek
|
|
6635
|
+
* key resolves) makes EVERY later search throw
|
|
6636
|
+
* `WEB_PROVIDER_AMBIGUOUS`.
|
|
6083
6637
|
*
|
|
6084
6638
|
* Never throws: like the low-level rewrite, a hardened runtime shape degrades
|
|
6085
6639
|
* to registered-but-unselected.
|
|
@@ -6093,14 +6647,16 @@ function applyCommandCodeSearchSelection(web, state, enable) {
|
|
|
6093
6647
|
return;
|
|
6094
6648
|
}
|
|
6095
6649
|
const prior = field.searchProviderId;
|
|
6096
|
-
state.
|
|
6650
|
+
state.preexisting = prior === COMMANDCODE_SEARCH_PROVIDER_ID;
|
|
6651
|
+
state.displaced = state.preexisting ? void 0 : prior;
|
|
6097
6652
|
field.searchProviderId = COMMANDCODE_SEARCH_PROVIDER_ID;
|
|
6098
6653
|
state.owner = true;
|
|
6099
6654
|
return;
|
|
6100
6655
|
}
|
|
6101
6656
|
if (state.owner) {
|
|
6102
6657
|
state.owner = false;
|
|
6103
|
-
field.searchProviderId = state.displaced;
|
|
6658
|
+
if (!state.preexisting) field.searchProviderId = state.displaced;
|
|
6659
|
+
state.preexisting = false;
|
|
6104
6660
|
return;
|
|
6105
6661
|
}
|
|
6106
6662
|
} catch {}
|
|
@@ -6131,7 +6687,7 @@ function toSource(result) {
|
|
|
6131
6687
|
...snippet !== void 0 && snippet.length > 0 ? { snippet } : {}
|
|
6132
6688
|
};
|
|
6133
6689
|
}
|
|
6134
|
-
function isAbortError(error) {
|
|
6690
|
+
function isAbortError$1(error) {
|
|
6135
6691
|
return error instanceof DOMException && error.name === "AbortError";
|
|
6136
6692
|
}
|
|
6137
6693
|
/** Build the provider's stable cancellation error while retaining the caller's reason. */
|
|
@@ -6185,7 +6741,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6185
6741
|
...signal !== void 0 ? { signal } : {}
|
|
6186
6742
|
});
|
|
6187
6743
|
} catch (error) {
|
|
6188
|
-
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6744
|
+
if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
|
|
6189
6745
|
throw new WebError(`Command Code web search request failed: ${error instanceof Error ? error.message : String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
6190
6746
|
}
|
|
6191
6747
|
if (!response.ok) {
|
|
@@ -6200,7 +6756,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6200
6756
|
if (typeof code === "string" || typeof inner === "string") message += `: ${typeof code === "string" ? code : ""}${typeof code === "string" && typeof inner === "string" ? " — " : ""}${typeof inner === "string" ? inner : ""}`;
|
|
6201
6757
|
}
|
|
6202
6758
|
} catch (error) {
|
|
6203
|
-
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6759
|
+
if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
|
|
6204
6760
|
const slice = (await response.text().catch(() => "")).trim().slice(0, 200);
|
|
6205
6761
|
if (slice !== "") message += `: ${slice}`;
|
|
6206
6762
|
}
|
|
@@ -6210,7 +6766,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6210
6766
|
try {
|
|
6211
6767
|
payload = await response.json();
|
|
6212
6768
|
} catch (error) {
|
|
6213
|
-
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6769
|
+
if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
|
|
6214
6770
|
throw new WebError("Command Code web search returned an unparseable response body", "WEB_PROVIDER_ERROR", { cause: error });
|
|
6215
6771
|
}
|
|
6216
6772
|
const results = payload?.results;
|
|
@@ -6234,7 +6790,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6234
6790
|
try {
|
|
6235
6791
|
key = await this.deps.resolveKey();
|
|
6236
6792
|
} catch (error) {
|
|
6237
|
-
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6793
|
+
if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
|
|
6238
6794
|
if (error instanceof Error && typeof error.code === "string") {
|
|
6239
6795
|
const code = error.code;
|
|
6240
6796
|
if (code === "MISSING_CREDENTIAL") throw new WebError(error.message, "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
|
|
@@ -6402,8 +6958,8 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
6402
6958
|
label: "API key",
|
|
6403
6959
|
descriptions: { zh: "API 密钥" },
|
|
6404
6960
|
secret: { ref },
|
|
6405
|
-
hint: `Stored in the credential store as ${ref}, never in
|
|
6406
|
-
hintDescriptions: { zh: `保存在凭据库(${ref}
|
|
6961
|
+
hint: `Stored in the credential store as ${ref}, never in configuration files.`,
|
|
6962
|
+
hintDescriptions: { zh: `保存在凭据库(${ref}),不会写入配置文件。` }
|
|
6407
6963
|
},
|
|
6408
6964
|
{
|
|
6409
6965
|
path: ["apiBase"],
|
|
@@ -6422,6 +6978,70 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
6422
6978
|
};
|
|
6423
6979
|
}
|
|
6424
6980
|
},
|
|
6981
|
+
{
|
|
6982
|
+
path: ["commandGuard"],
|
|
6983
|
+
group: "advanced",
|
|
6984
|
+
kind: "boolean",
|
|
6985
|
+
label: "AI command guard",
|
|
6986
|
+
descriptions: { zh: "AI 命令安全预判" },
|
|
6987
|
+
hint: "typesafe/jev judges whether a shell command is safe and auto-approves it; everything else asks as usual. Off by default.",
|
|
6988
|
+
hintDescriptions: { zh: "由 typesafe/jev 判断 shell 命令是否安全,安全则自动放行,其余照常弹窗。默认关闭。" },
|
|
6989
|
+
format: (value) => value === true ? "true" : "false",
|
|
6990
|
+
parse: (text) => ({
|
|
6991
|
+
kind: "set",
|
|
6992
|
+
value: text.trim() === "true"
|
|
6993
|
+
})
|
|
6994
|
+
},
|
|
6995
|
+
{
|
|
6996
|
+
path: ["commandGuardThreshold"],
|
|
6997
|
+
group: "advanced",
|
|
6998
|
+
kind: "number",
|
|
6999
|
+
label: "Auto-approve threshold (0.5-1)",
|
|
7000
|
+
descriptions: { zh: "自动放行阈值(0.5-1)" },
|
|
7001
|
+
hint: "Approve when typesafe/jev rates the command \"safe\" at least this likely; default 0.9.",
|
|
7002
|
+
hintDescriptions: { zh: "typesafe/jev 判定\"安全\"的概率达到该值即放行,默认 0.9。" },
|
|
7003
|
+
parse: (text) => {
|
|
7004
|
+
const trimmed = text.trim();
|
|
7005
|
+
if (trimmed === "") return { kind: "clear" };
|
|
7006
|
+
const parsed = Number(trimmed);
|
|
7007
|
+
return Number.isFinite(parsed) && parsed >= .5 && parsed <= 1 ? {
|
|
7008
|
+
kind: "set",
|
|
7009
|
+
value: parsed
|
|
7010
|
+
} : void 0;
|
|
7011
|
+
}
|
|
7012
|
+
},
|
|
7013
|
+
{
|
|
7014
|
+
path: ["commandGuardTimeoutMs"],
|
|
7015
|
+
group: "advanced",
|
|
7016
|
+
kind: "number",
|
|
7017
|
+
label: "Judgement timeout (ms, 200-10000)",
|
|
7018
|
+
descriptions: { zh: "判定超时(毫秒,200-10000)" },
|
|
7019
|
+
hint: "If typesafe/jev does not answer in time, the normal prompt appears; default 1500.",
|
|
7020
|
+
hintDescriptions: { zh: "typesafe/jev 超过该时间未返回就照常弹窗,默认 1500。" },
|
|
7021
|
+
parse: (text) => {
|
|
7022
|
+
const trimmed = text.trim();
|
|
7023
|
+
if (trimmed === "") return { kind: "clear" };
|
|
7024
|
+
const parsed = Number(trimmed);
|
|
7025
|
+
return Number.isFinite(parsed) && parsed >= 200 && parsed <= 1e4 ? {
|
|
7026
|
+
kind: "set",
|
|
7027
|
+
value: parsed
|
|
7028
|
+
} : void 0;
|
|
7029
|
+
}
|
|
7030
|
+
},
|
|
7031
|
+
{
|
|
7032
|
+
path: ["zdr"],
|
|
7033
|
+
group: "advanced",
|
|
7034
|
+
kind: "boolean",
|
|
7035
|
+
label: "Zero data retention (ZDR)",
|
|
7036
|
+
descriptions: { zh: "零数据保留(ZDR)" },
|
|
7037
|
+
hint: "Route requests only through upstreams that retain nothing and never train on prompts (the CLI's CMD_ZDR=1). Models without an available ZDR upstream fail instead of losing protection; ZDR is usually billed at higher pass-through rates. Off by default.",
|
|
7038
|
+
hintDescriptions: { zh: "请求只经由不留存数据、也不用于训练的上游(等价于 CLI 的 CMD_ZDR=1)。没有可用 ZDR 上游的模型会报错,不会降级为非 ZDR 请求;ZDR 通常按更高的透传价计费。默认关闭。" },
|
|
7039
|
+
format: (value) => value === true ? "true" : "false",
|
|
7040
|
+
parse: (text) => ({
|
|
7041
|
+
kind: "set",
|
|
7042
|
+
value: text.trim() === "true"
|
|
7043
|
+
})
|
|
7044
|
+
},
|
|
6425
7045
|
{
|
|
6426
7046
|
path: ["filterModelsByPlan"],
|
|
6427
7047
|
group: "models",
|
|
@@ -6681,18 +7301,810 @@ function transportBudgetMessage(failureMessage, maxRetries) {
|
|
|
6681
7301
|
return `${failureMessage};Command Code API 连接在连续重试后仍失败——已停止重试以免整轮卡死。插件已自动重试 ${maxRetries} 次。通常说明当前网络到 api.commandcode.ai 的连接不通,常见原因是代理未生效:DSH 只读取环境变量 HTTPS_PROXY/HTTP_PROXY(系统代理/PAC 不会被读取),请确认启动 DSH 的终端里已导出代理,或先关闭代理直连后重试。网络恢复后直接重发即可 — Command Code API transport still failed after ${maxRetries} automatic retries, which were stopped so the turn could not stall for minutes. This is a connectivity problem between this machine and api.commandcode.ai, not a context-window or request-size rejection. Common cause: the proxy is not reaching the harness — DSH reads HTTPS_PROXY/HTTP_PROXY from the environment only (a Windows system proxy/PAC setting is not consulted), so export it in the launching shell, or turn the proxy off and retry. Resend when the network recovers; the budget is per request and has been reset`;
|
|
6682
7302
|
}
|
|
6683
7303
|
//#endregion
|
|
6684
|
-
//#region src/
|
|
7304
|
+
//#region src/systemone.ts
|
|
6685
7305
|
/**
|
|
6686
|
-
*
|
|
6687
|
-
*
|
|
7306
|
+
* Host-side client for Command Code's System One decision endpoint
|
|
7307
|
+
* (`POST {apiBase}/provider/v1/systemone`).
|
|
6688
7308
|
*
|
|
6689
|
-
*
|
|
6690
|
-
*
|
|
6691
|
-
*
|
|
7309
|
+
* `typesafe/jev` is **not a chat model**: it takes a `state` plus typed
|
|
7310
|
+
* questions and answers with probabilities, never text, and it never streams.
|
|
7311
|
+
* It is therefore absent from `/provider/v1/models` (so the picker can never
|
|
7312
|
+
* offer it) and must never be registered as a model route — this plugin uses it
|
|
7313
|
+
* as an internal advisor instead: the command guard in `./command-guard.ts`
|
|
7314
|
+
* asks it whether a shell command is safe to auto-approve.
|
|
7315
|
+
*
|
|
7316
|
+
* The three question types are the whole vocabulary (command-code@1.64.0
|
|
7317
|
+
* `systemOneRequestSchema`):
|
|
7318
|
+
*
|
|
7319
|
+
* - `noul` — "is this true?", optional `criteria: { true, false }` describing
|
|
7320
|
+
* each verdict. Answers with `noul`, the probability of *yes*.
|
|
7321
|
+
* - `choice` — "which one?", `criteria` maps each option key to its
|
|
7322
|
+
* description. Answers with `choice`, plus optional `confidence` and
|
|
7323
|
+
* `probabilities`.
|
|
7324
|
+
* - `score` — "where on this rubric?", `criteria` is an array of at least two
|
|
7325
|
+
* levels, lowest first. Answers with `score`, `legend` and `probabilities`.
|
|
7326
|
+
*
|
|
7327
|
+
* A request carries at most 20 questions and every answer is keyed by the
|
|
7328
|
+
* question id it belongs to. `usage` is reported per request, in tokens.
|
|
7329
|
+
*
|
|
7330
|
+
* Billing: free through 2026-09-24T23:59:59Z, then $0.042 per 1M input tokens
|
|
7331
|
+
* with output free. The model has **no ZDR-capable upstream**, so a request
|
|
7332
|
+
* carrying `x-cmd-zdr: 1` is refused with HTTP 422 `cmd_zdr_no_providers` —
|
|
7333
|
+
* this client never sends that header, and it must stay that way even though
|
|
7334
|
+
* the plugin now ships a ZDR switch for chat (`Config.zdr` → the `x-cmd-zdr: 1`
|
|
7335
|
+
* header per model, gated on `KNOWN_NON_ZDR_MODELS`): the decision endpoint is
|
|
7336
|
+
* reached through THIS module's own request path, never through the adapter's
|
|
7337
|
+
* chat connection, so the two cannot drift — and a deployment that later widens
|
|
7338
|
+
* ZDR must exclude this endpoint rather than route it through the switch.
|
|
7339
|
+
*
|
|
7340
|
+
* Auth is the plugin's ordinary chain (the multi-account pool, the credentials
|
|
7341
|
+
* seam, the CLI auth file) through the same `Authorization: Bearer <key>` and
|
|
7342
|
+
* `x-command-code-version` headers every other endpoint uses.
|
|
7343
|
+
*
|
|
7344
|
+
* @module dsh-commandcode-provider/systemone
|
|
7345
|
+
*/
|
|
7346
|
+
/** Path of the decision endpoint, relative to `apiBase`. */
|
|
7347
|
+
const SYSTEMONE_ROUTE = "/provider/v1/systemone";
|
|
7348
|
+
/** Milliseconds a decision may take before the caller gives up on it. */
|
|
7349
|
+
const SYSTEMONE_DEFAULT_TIMEOUT_MS = 1500;
|
|
7350
|
+
/** The one error type this client throws. Callers fail closed on it. */
|
|
7351
|
+
var SystemOneError = class extends Error {
|
|
7352
|
+
kind;
|
|
7353
|
+
status;
|
|
7354
|
+
constructor(message, kind, status, options) {
|
|
7355
|
+
super(message, options);
|
|
7356
|
+
this.kind = kind;
|
|
7357
|
+
this.status = status;
|
|
7358
|
+
this.name = "SystemOneError";
|
|
7359
|
+
}
|
|
7360
|
+
};
|
|
7361
|
+
/**
|
|
7362
|
+
* Combine the caller's signal with a timeout, keeping the two distinguishable:
|
|
7363
|
+
* a timeout is this client's own budget expiring (the guard delegates to the
|
|
7364
|
+
* user) while a caller abort means the approval was already withdrawn (answer
|
|
7365
|
+
* nothing) — both fail closed, but the diagnosis must not lie about which.
|
|
7366
|
+
*
|
|
7367
|
+
* The timer is a plain `setTimeout` rather than `AbortSignal.timeout()`, whose
|
|
7368
|
+
* timer Node unrefs: with nothing else pending (a test double, a socket that
|
|
7369
|
+
* never opened) an unref'd timer lets the process exit while the promise is
|
|
7370
|
+
* still pending, which reads as a hang rather than as the timeout it is.
|
|
7371
|
+
*/
|
|
7372
|
+
function deadlineOf(signal, timeoutMs) {
|
|
7373
|
+
const controller = new AbortController();
|
|
7374
|
+
let timedOut = false;
|
|
7375
|
+
const timer = setTimeout(() => {
|
|
7376
|
+
timedOut = true;
|
|
7377
|
+
controller.abort();
|
|
7378
|
+
}, timeoutMs);
|
|
7379
|
+
const onCallerAbort = () => controller.abort();
|
|
7380
|
+
if (signal !== void 0) {
|
|
7381
|
+
if (signal.aborted) controller.abort();
|
|
7382
|
+
else signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
7383
|
+
}
|
|
7384
|
+
return {
|
|
7385
|
+
signal: controller.signal,
|
|
7386
|
+
callerAborted: () => signal?.aborted === true,
|
|
7387
|
+
timedOut: () => timedOut,
|
|
7388
|
+
dispose: () => {
|
|
7389
|
+
clearTimeout(timer);
|
|
7390
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
7391
|
+
}
|
|
7392
|
+
};
|
|
7393
|
+
}
|
|
7394
|
+
/** Whether a caught value is an abort-shaped failure. */
|
|
7395
|
+
function isAbortError(error) {
|
|
7396
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
7397
|
+
}
|
|
7398
|
+
/** Stop waiting for an operation that does not accept an AbortSignal. */
|
|
7399
|
+
function withinDeadline(operation, deadline, timeoutMs) {
|
|
7400
|
+
return new Promise((resolve, reject) => {
|
|
7401
|
+
const onAbort = () => {
|
|
7402
|
+
reject(deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout"));
|
|
7403
|
+
};
|
|
7404
|
+
deadline.signal.addEventListener("abort", onAbort, { once: true });
|
|
7405
|
+
operation.then((value) => {
|
|
7406
|
+
deadline.signal.removeEventListener("abort", onAbort);
|
|
7407
|
+
resolve(value);
|
|
7408
|
+
}, (error) => {
|
|
7409
|
+
deadline.signal.removeEventListener("abort", onAbort);
|
|
7410
|
+
reject(error);
|
|
7411
|
+
});
|
|
7412
|
+
if (deadline.signal.aborted) onAbort();
|
|
7413
|
+
});
|
|
7414
|
+
}
|
|
7415
|
+
/** A finite number in `[0, 1]`, or undefined. */
|
|
7416
|
+
function probability(value) {
|
|
7417
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
|
|
7418
|
+
}
|
|
7419
|
+
/** A finite number, or undefined. */
|
|
7420
|
+
function finite(value) {
|
|
7421
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
7422
|
+
}
|
|
7423
|
+
/** A record of finite numbers (probabilities), or undefined when the shape is off. */
|
|
7424
|
+
function numberRecord(value) {
|
|
7425
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
7426
|
+
const out = {};
|
|
7427
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
7428
|
+
const parsed = finite(entry);
|
|
7429
|
+
if (parsed === void 0) return void 0;
|
|
7430
|
+
out[key] = parsed;
|
|
7431
|
+
}
|
|
7432
|
+
return out;
|
|
7433
|
+
}
|
|
7434
|
+
/**
|
|
7435
|
+
* Parse one answer entry strictly by its own `type`. A body whose answer does
|
|
7436
|
+
* not carry the member its type promises is rejected rather than defaulted:
|
|
7437
|
+
* an absent probability is exactly the case the caller must not read as "safe".
|
|
7438
|
+
*
|
|
7439
|
+
* @param value - one raw entry from the response's `answers` object.
|
|
7440
|
+
* @returns the parsed answer, or undefined when the entry is malformed.
|
|
7441
|
+
*/
|
|
7442
|
+
function parseSystemOneAnswer(value) {
|
|
7443
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
7444
|
+
const raw = value;
|
|
7445
|
+
if (raw.type === "noul") {
|
|
7446
|
+
const noul = probability(raw.noul);
|
|
7447
|
+
return noul === void 0 ? void 0 : {
|
|
7448
|
+
type: "noul",
|
|
7449
|
+
noul
|
|
7450
|
+
};
|
|
7451
|
+
}
|
|
7452
|
+
if (raw.type === "choice") {
|
|
7453
|
+
if (typeof raw.choice !== "string" || raw.choice === "") return void 0;
|
|
7454
|
+
const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
|
|
7455
|
+
if (raw.confidence !== void 0 && confidence === void 0) return void 0;
|
|
7456
|
+
const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
|
|
7457
|
+
if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
|
|
7458
|
+
return {
|
|
7459
|
+
type: "choice",
|
|
7460
|
+
choice: raw.choice,
|
|
7461
|
+
...confidence === void 0 ? {} : { confidence },
|
|
7462
|
+
...probabilities === void 0 ? {} : { probabilities }
|
|
7463
|
+
};
|
|
7464
|
+
}
|
|
7465
|
+
if (raw.type === "score") {
|
|
7466
|
+
const score = finite(raw.score);
|
|
7467
|
+
if (score === void 0) return void 0;
|
|
7468
|
+
const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
|
|
7469
|
+
if (raw.confidence !== void 0 && confidence === void 0) return void 0;
|
|
7470
|
+
const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
|
|
7471
|
+
if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
|
|
7472
|
+
return {
|
|
7473
|
+
type: "score",
|
|
7474
|
+
score,
|
|
7475
|
+
...confidence === void 0 ? {} : { confidence },
|
|
7476
|
+
...probabilities === void 0 ? {} : { probabilities }
|
|
7477
|
+
};
|
|
7478
|
+
}
|
|
7479
|
+
}
|
|
7480
|
+
/**
|
|
7481
|
+
* Parse a decision response body. Every answer entry must be well formed — one
|
|
7482
|
+
* malformed entry fails the whole response, because a partially understood
|
|
7483
|
+
* decision is not a decision. Unknown extra members are ignored so a server-side
|
|
7484
|
+
* addition cannot break the guard.
|
|
7485
|
+
*
|
|
7486
|
+
* @param value - the decoded JSON body.
|
|
7487
|
+
* @returns the parsed response, or undefined when the body is not one.
|
|
7488
|
+
*/
|
|
7489
|
+
function parseSystemOneResponse(value) {
|
|
7490
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
7491
|
+
const raw = value;
|
|
7492
|
+
const rawAnswers = raw.answers;
|
|
7493
|
+
if (typeof rawAnswers !== "object" || rawAnswers === null || Array.isArray(rawAnswers)) return void 0;
|
|
7494
|
+
const entries = Object.entries(rawAnswers);
|
|
7495
|
+
if (entries.length === 0) return void 0;
|
|
7496
|
+
const answers = {};
|
|
7497
|
+
for (const [id, entry] of entries) {
|
|
7498
|
+
const answer = parseSystemOneAnswer(entry);
|
|
7499
|
+
if (answer === void 0) return void 0;
|
|
7500
|
+
answers[id] = answer;
|
|
7501
|
+
}
|
|
7502
|
+
const usage = raw.usage === void 0 ? void 0 : parseUsage(raw.usage);
|
|
7503
|
+
if (raw.usage !== void 0 && usage === void 0) return void 0;
|
|
7504
|
+
return {
|
|
7505
|
+
...typeof raw.model === "string" ? { model: raw.model } : {},
|
|
7506
|
+
answers,
|
|
7507
|
+
...usage === void 0 ? {} : { usage }
|
|
7508
|
+
};
|
|
7509
|
+
}
|
|
7510
|
+
/** Parse the optional `usage` member; an unreadable one is a malformed body. */
|
|
7511
|
+
function parseUsage(value) {
|
|
7512
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
7513
|
+
const raw = value;
|
|
7514
|
+
const inputTokens = raw.input_tokens === void 0 ? void 0 : finite(raw.input_tokens);
|
|
7515
|
+
if (raw.input_tokens !== void 0 && inputTokens === void 0) return void 0;
|
|
7516
|
+
const outputTokens = raw.output_tokens === void 0 ? void 0 : finite(raw.output_tokens);
|
|
7517
|
+
if (raw.output_tokens !== void 0 && outputTokens === void 0) return void 0;
|
|
7518
|
+
return {
|
|
7519
|
+
...inputTokens === void 0 ? {} : { inputTokens },
|
|
7520
|
+
...outputTokens === void 0 ? {} : { outputTokens }
|
|
7521
|
+
};
|
|
7522
|
+
}
|
|
7523
|
+
/** Validate the outgoing request where the server would only reject it with a 400. */
|
|
7524
|
+
function assertRequest(request) {
|
|
7525
|
+
const ids = Object.keys(request.questions);
|
|
7526
|
+
if (ids.length === 0) throw new SystemOneError("a decision request needs at least one question", "invalid-request");
|
|
7527
|
+
if (ids.length > 20) throw new SystemOneError(`a decision request carries at most 20 questions (got ${ids.length})`, "invalid-request");
|
|
7528
|
+
for (const id of ids) if (id.trim() === "") throw new SystemOneError("every decision question needs a non-empty id", "invalid-request");
|
|
7529
|
+
}
|
|
7530
|
+
/** Read the endpoint's error body into a one-line diagnosis. */
|
|
7531
|
+
async function httpFailure(response) {
|
|
7532
|
+
let detail = "";
|
|
7533
|
+
try {
|
|
7534
|
+
const slice = (await response.text()).trim().slice(0, 200);
|
|
7535
|
+
if (slice !== "") detail = `: ${slice}`;
|
|
7536
|
+
} catch {}
|
|
7537
|
+
return new SystemOneError(`Command Code decision request failed (HTTP ${response.status})${detail}`, "http", response.status);
|
|
7538
|
+
}
|
|
7539
|
+
/**
|
|
7540
|
+
* Run one decision request.
|
|
7541
|
+
*
|
|
7542
|
+
* @param deps - connection facts, credential chain and fetch seam.
|
|
7543
|
+
* @param request - the state and typed questions.
|
|
7544
|
+
* @param options - caller cancellation and the per-call budget.
|
|
7545
|
+
* @returns the parsed decision.
|
|
7546
|
+
* @throws {SystemOneError} on every failure path; callers fail closed.
|
|
7547
|
+
*/
|
|
7548
|
+
async function runSystemOne(deps, request, options = {}) {
|
|
7549
|
+
assertRequest(request);
|
|
7550
|
+
const apiBase = deps.apiBase();
|
|
7551
|
+
if (!URL.canParse(apiBase)) throw new SystemOneError(`Command Code decision request is misconfigured: apiBase ${JSON.stringify(apiBase)} is not a valid URL`, "invalid-request");
|
|
7552
|
+
if (options.signal?.aborted === true) throw new SystemOneError("decision request aborted", "aborted");
|
|
7553
|
+
const timeoutMs = options.timeoutMs ?? 1500;
|
|
7554
|
+
const deadline = deadlineOf(options.signal, timeoutMs);
|
|
7555
|
+
try {
|
|
7556
|
+
let key;
|
|
7557
|
+
try {
|
|
7558
|
+
key = await withinDeadline(Promise.resolve().then(() => deps.resolveApiKey()), deadline, timeoutMs);
|
|
7559
|
+
} catch (error) {
|
|
7560
|
+
if (error instanceof SystemOneError && (error.kind === "aborted" || error.kind === "timeout")) throw error;
|
|
7561
|
+
throw new SystemOneError(`Command Code decision request has no usable credential: ${error instanceof Error ? error.message : String(error)}`, "credential", void 0, { cause: error });
|
|
7562
|
+
}
|
|
7563
|
+
const body = {
|
|
7564
|
+
model: request.model ?? "typesafe/jev",
|
|
7565
|
+
state: request.state,
|
|
7566
|
+
questions: request.questions
|
|
7567
|
+
};
|
|
7568
|
+
if (deadline.signal.aborted) throw deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout");
|
|
7569
|
+
let response;
|
|
7570
|
+
try {
|
|
7571
|
+
response = await (deps.fetchImpl ?? fetch)(`${apiBase.replace(/\/$/, "")}${SYSTEMONE_ROUTE}`, {
|
|
7572
|
+
method: "POST",
|
|
7573
|
+
headers: {
|
|
7574
|
+
"Content-Type": "application/json",
|
|
7575
|
+
Authorization: `Bearer ${key}`,
|
|
7576
|
+
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
7577
|
+
...attributionHeaders()
|
|
7578
|
+
},
|
|
7579
|
+
body: JSON.stringify(body),
|
|
7580
|
+
signal: deadline.signal
|
|
7581
|
+
});
|
|
7582
|
+
} catch (error) {
|
|
7583
|
+
if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
|
|
7584
|
+
if (deadline.timedOut() || isAbortError(error)) throw new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout", void 0, { cause: error });
|
|
7585
|
+
throw new SystemOneError(`Command Code decision request failed: ${error instanceof Error ? error.message : String(error)}`, "network", void 0, { cause: error });
|
|
7586
|
+
}
|
|
7587
|
+
if (!response.ok) throw await httpFailure(response);
|
|
7588
|
+
let payload;
|
|
7589
|
+
try {
|
|
7590
|
+
payload = await response.json();
|
|
7591
|
+
} catch (error) {
|
|
7592
|
+
if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
|
|
7593
|
+
throw new SystemOneError("Command Code returned an unparseable decision body", "invalid-response", void 0, { cause: error });
|
|
7594
|
+
}
|
|
7595
|
+
const parsed = parseSystemOneResponse(payload);
|
|
7596
|
+
if (parsed === void 0) throw new SystemOneError("Command Code returned a decision body this plugin cannot read", "invalid-response");
|
|
7597
|
+
return parsed;
|
|
7598
|
+
} finally {
|
|
7599
|
+
deadline.dispose();
|
|
7600
|
+
}
|
|
7601
|
+
}
|
|
7602
|
+
//#endregion
|
|
7603
|
+
//#region src/command-guard.ts
|
|
7604
|
+
/** Tool names whose `arguments.command` this guard judges. */
|
|
7605
|
+
const COMMAND_GUARD_SHELL_TOOLS = ["bash", "pwsh"];
|
|
7606
|
+
/** Question id the safety verdict is keyed by. */
|
|
7607
|
+
const COMMAND_GUARD_QUESTION_ID = "safe";
|
|
7608
|
+
/** Decision budget; the approval prompt must not wait longer than this. */
|
|
7609
|
+
const COMMAND_GUARD_DEFAULT_TIMEOUT_MS = SYSTEMONE_DEFAULT_TIMEOUT_MS;
|
|
7610
|
+
/** Bounds of the configurable probability, mirrored by the settings schemas. */
|
|
7611
|
+
const COMMAND_GUARD_MIN_THRESHOLD = .5;
|
|
7612
|
+
const COMMAND_GUARD_MAX_TIMEOUT_MS = 1e4;
|
|
7613
|
+
/**
|
|
7614
|
+
* Longest command the guard will send for judgement. A longer one is delegated
|
|
7615
|
+
* instead of truncated: a verdict on the first N characters of a command is a
|
|
7616
|
+
* verdict on a different command.
|
|
7617
|
+
*/
|
|
7618
|
+
const COMMAND_GUARD_MAX_COMMAND_CHARS = 6e3;
|
|
7619
|
+
/**
|
|
7620
|
+
* Hard denylist: commands the guard never auto-approves, and never even sends
|
|
7621
|
+
* for judgement. Each entry is a deliberate, narrow reading of one destructive
|
|
7622
|
+
* family — the goal is not to classify every dangerous command (that is the
|
|
7623
|
+
* model's job) but to make sure the classics cannot be talked into a grant by a
|
|
7624
|
+
* persuasive-looking command string.
|
|
7625
|
+
*
|
|
7626
|
+
* Every pattern is allowed to OVER-match: a false positive costs one ordinary
|
|
7627
|
+
* human prompt, while a false negative would hand a destructive command to a
|
|
7628
|
+
* model that has to guess. That asymmetry is why `sudo` is matched anywhere in
|
|
7629
|
+
* the line rather than only in command position, and why `fdisk` is listed even
|
|
7630
|
+
* though `fdisk -l` only lists partitions.
|
|
7631
|
+
*/
|
|
7632
|
+
const COMMAND_GUARD_DENY_LIST = [
|
|
7633
|
+
{
|
|
7634
|
+
id: "rm-root",
|
|
7635
|
+
pattern: /\brm\s+(?:-[a-zA-Z]*[rR][a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*[rR][a-zA-Z]*|--recursive[^\n]*--force|--force[^\n]*--recursive)\s+(?:\/|\/\*|~|~\/\*|\$HOME(?:\/\*)?|\*|\.\/\*|\.)(?:\s|$)/
|
|
7636
|
+
},
|
|
7637
|
+
{
|
|
7638
|
+
id: "rm-home",
|
|
7639
|
+
pattern: /\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*\s+(?:~|\$HOME)\//
|
|
7640
|
+
},
|
|
7641
|
+
{
|
|
7642
|
+
id: "filesystem-write",
|
|
7643
|
+
pattern: /\b(?:mkfs(?:\.\w+)?|wipefs|fdisk|parted|shred)\b/
|
|
7644
|
+
},
|
|
7645
|
+
{
|
|
7646
|
+
id: "raw-device-write",
|
|
7647
|
+
pattern: /\bdd\b[^\n]*\bof=\/dev\//
|
|
7648
|
+
},
|
|
7649
|
+
{
|
|
7650
|
+
id: "device-redirect",
|
|
7651
|
+
pattern: /(?:^|[^0-9\w])>{1,2}\s*\/dev\/(?:sd|disk|nvme|hd)/
|
|
7652
|
+
},
|
|
7653
|
+
{
|
|
7654
|
+
id: "fork-bomb",
|
|
7655
|
+
pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/
|
|
7656
|
+
},
|
|
7657
|
+
{
|
|
7658
|
+
id: "privilege-escalation",
|
|
7659
|
+
pattern: /\b(?:sudo|doas)\b/
|
|
7660
|
+
},
|
|
7661
|
+
{
|
|
7662
|
+
id: "su",
|
|
7663
|
+
pattern: /\bsu\s/
|
|
7664
|
+
},
|
|
7665
|
+
{
|
|
7666
|
+
id: "chmod-world-write",
|
|
7667
|
+
pattern: /\bchmod\b[^\n]*\b(?:777|a\+w|o\+w)\b/
|
|
7668
|
+
},
|
|
7669
|
+
{
|
|
7670
|
+
id: "chown-root",
|
|
7671
|
+
pattern: /\bchown\b[^\n]*\b(?:root|0):/
|
|
7672
|
+
},
|
|
7673
|
+
{
|
|
7674
|
+
id: "git-push",
|
|
7675
|
+
pattern: /\bgit\s+push\b/
|
|
7676
|
+
},
|
|
7677
|
+
{
|
|
7678
|
+
id: "git-history-rewrite",
|
|
7679
|
+
pattern: /\bgit\s+(?:filter-branch|filter-repo)\b/
|
|
7680
|
+
},
|
|
7681
|
+
{
|
|
7682
|
+
id: "git-hard-reset",
|
|
7683
|
+
pattern: /\bgit\s+reset\s+--hard\b/
|
|
7684
|
+
},
|
|
7685
|
+
{
|
|
7686
|
+
id: "git-clean-force",
|
|
7687
|
+
pattern: /\bgit\s+clean\b[^\n]*\s-[a-zA-Z]*f/
|
|
7688
|
+
},
|
|
7689
|
+
{
|
|
7690
|
+
id: "git-discard-worktree",
|
|
7691
|
+
pattern: /\bgit\s+(?:checkout|restore)\s+(?:--\s+)?\.(?:\s|$)/
|
|
7692
|
+
},
|
|
7693
|
+
{
|
|
7694
|
+
id: "git-stash-drop",
|
|
7695
|
+
pattern: /\bgit\s+stash\s+(?:drop|clear)\b/
|
|
7696
|
+
},
|
|
7697
|
+
{
|
|
7698
|
+
id: "publish",
|
|
7699
|
+
pattern: /\b(?:npm|pnpm|yarn|bun)\s+publish\b/
|
|
7700
|
+
},
|
|
7701
|
+
{
|
|
7702
|
+
id: "release-upload",
|
|
7703
|
+
pattern: /\b(?:gh\s+release|twine\s+upload)\b/
|
|
7704
|
+
},
|
|
7705
|
+
{
|
|
7706
|
+
id: "docker-push",
|
|
7707
|
+
pattern: /\bdocker\s+push\b/
|
|
7708
|
+
},
|
|
7709
|
+
{
|
|
7710
|
+
id: "cloud-upload",
|
|
7711
|
+
pattern: /\b(?:aws\s+s3|gsutil|gcloud\s+storage|az\s+storage)\b[^\n]*\b(?:cp|sync|mv|rm|rb)\b/
|
|
7712
|
+
},
|
|
7713
|
+
{
|
|
7714
|
+
id: "remote-copy",
|
|
7715
|
+
pattern: /\b(?:scp|rsync)\b[^\n]*(?::|@)/
|
|
7716
|
+
},
|
|
7717
|
+
{
|
|
7718
|
+
id: "pipe-to-shell",
|
|
7719
|
+
pattern: /\b(?:curl|wget|fetch)\b[^\n|]*\|\s*(?:sudo\s+)?(?:sh|bash|zsh|dash|ksh|python\d*|node|ruby|perl)\b/
|
|
7720
|
+
},
|
|
7721
|
+
{
|
|
7722
|
+
id: "power",
|
|
7723
|
+
pattern: /\b(?:shutdown|reboot|halt|poweroff)\b/
|
|
7724
|
+
},
|
|
7725
|
+
{
|
|
7726
|
+
id: "kill-everything",
|
|
7727
|
+
pattern: /\bkill\s+(?:-9\s+)?-1\b|\b(?:killall|pkill)\s+-9\b/
|
|
7728
|
+
},
|
|
7729
|
+
{
|
|
7730
|
+
id: "credential-write",
|
|
7731
|
+
pattern: /(?:>{1,2}|tee|cp|mv|rm|chmod|chown)\s[^\n]*(?:\.ssh\b|\.aws\b|\.gnupg\b|\.commandcode\/auth\.json|id_rsa|id_ed25519)/
|
|
7732
|
+
},
|
|
7733
|
+
{
|
|
7734
|
+
id: "history-clear",
|
|
7735
|
+
pattern: /\bhistory\s+-c\b|\bunset\s+HISTFILE\b/
|
|
7736
|
+
},
|
|
7737
|
+
{
|
|
7738
|
+
id: "global-install",
|
|
7739
|
+
pattern: /\b(?:npm|pnpm|yarn|bun)\s+(?:i|install|add)\s+(?:-g\b|--global\b)/
|
|
7740
|
+
},
|
|
7741
|
+
{
|
|
7742
|
+
id: "system-install",
|
|
7743
|
+
pattern: /\b(?:apt|apt-get|dnf|yum|zypper|apk|pacman|brew)\s+(?:install|remove|purge|uninstall|upgrade)\b/
|
|
7744
|
+
},
|
|
7745
|
+
{
|
|
7746
|
+
id: "language-install",
|
|
7747
|
+
pattern: /\b(?:pip3?|pipx|gem|cargo)\s+(?:install|add)\b/
|
|
7748
|
+
}
|
|
7749
|
+
];
|
|
7750
|
+
/**
|
|
7751
|
+
* Why the guard refuses to judge a command, when it does.
|
|
7752
|
+
*
|
|
7753
|
+
* @param command - the raw command text.
|
|
7754
|
+
* @returns the matching denylist id, or undefined.
|
|
7755
|
+
*/
|
|
7756
|
+
function denyReasonOf(command) {
|
|
7757
|
+
for (const entry of COMMAND_GUARD_DENY_LIST) if (entry.pattern.test(command)) return entry.id;
|
|
7758
|
+
}
|
|
7759
|
+
/** A non-empty string member, or undefined. */
|
|
7760
|
+
function stringMember(value, key) {
|
|
7761
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
7762
|
+
const member = value[key];
|
|
7763
|
+
return typeof member === "string" && member.trim() !== "" ? member : void 0;
|
|
7764
|
+
}
|
|
7765
|
+
/**
|
|
7766
|
+
* Read a shell command out of a tool execution, or undefined when this call is
|
|
7767
|
+
* not one the guard judges (a different tool, or arguments without a command).
|
|
7768
|
+
*
|
|
7769
|
+
* @param name - the executed tool's name.
|
|
7770
|
+
* @param args - the execution's parsed `arguments`.
|
|
7771
|
+
* @returns the fact to judge, without the asker's reason (that arrives later).
|
|
7772
|
+
*/
|
|
7773
|
+
function shellCommandOf(name, args) {
|
|
7774
|
+
if (!COMMAND_GUARD_SHELL_TOOLS.includes(name)) return void 0;
|
|
7775
|
+
const command = stringMember(args, "command");
|
|
7776
|
+
if (command === void 0) return void 0;
|
|
7777
|
+
const description = stringMember(args, "description");
|
|
7778
|
+
const workdir = stringMember(args, "workdir");
|
|
7779
|
+
const sandboxPermissions = stringMember(args, "sandbox_permissions");
|
|
7780
|
+
return {
|
|
7781
|
+
tool: name,
|
|
7782
|
+
command,
|
|
7783
|
+
...description === void 0 ? {} : { description },
|
|
7784
|
+
...workdir === void 0 ? {} : { workdir },
|
|
7785
|
+
...sandboxPermissions === void 0 ? {} : { sandboxPermissions }
|
|
7786
|
+
};
|
|
7787
|
+
}
|
|
7788
|
+
/**
|
|
7789
|
+
* Build the decision request for one command.
|
|
7790
|
+
*
|
|
7791
|
+
* The question is deliberately one `noul`: a single probability is a decision
|
|
7792
|
+
* rule that can be explained, thresholded and logged in one line, and the
|
|
7793
|
+
* criteria carry the whole distinction between "reads, or writes reversibly
|
|
7794
|
+
* inside the project" and "everything else". The agent's own `description` is
|
|
7795
|
+
* included as context, but the criteria say the command text itself decides — a
|
|
7796
|
+
* model-written description is a claim, not evidence.
|
|
7797
|
+
*
|
|
7798
|
+
* @param fact - the command and its context.
|
|
7799
|
+
* @returns the request to send.
|
|
7800
|
+
*/
|
|
7801
|
+
function buildSafetyRequest(fact) {
|
|
7802
|
+
const lines = [
|
|
7803
|
+
`Tool: ${fact.tool}`,
|
|
7804
|
+
"Command:",
|
|
7805
|
+
fact.command,
|
|
7806
|
+
...fact.description === void 0 ? [] : ["", `The agent describes this command as: ${fact.description}`],
|
|
7807
|
+
...fact.workdir === void 0 ? [] : ["", `Working directory: ${fact.workdir}`],
|
|
7808
|
+
...fact.sandboxPermissions === void 0 ? [] : ["", `The harness is asking because this call requests a wider sandbox mode (${fact.sandboxPermissions}) than the one currently in force.`],
|
|
7809
|
+
...fact.reason === void 0 ? [] : ["", `The asker's reason for needing approval: ${fact.reason}`]
|
|
7810
|
+
];
|
|
7811
|
+
const question = {
|
|
7812
|
+
type: "noul",
|
|
7813
|
+
instructions: "Is it safe to run this command right now WITHOUT asking the user first? Judge the command text itself; a description is only a claim about it.",
|
|
7814
|
+
criteria: {
|
|
7815
|
+
true: "Read-only, or its effects stay inside the current project and are trivially reversible: no data leaves the machine, nothing outside the project is modified, and no credentials, git history or remote state are touched.",
|
|
7816
|
+
false: "Anything else. Deleting or overwriting files outside the project, rewriting or publishing git history, uploading data anywhere, installing system or global packages, changing permissions, ownership or credentials, killing processes, running as another user or as root, piping a download into a shell, or a command obfuscated enough that its effect cannot be read."
|
|
7817
|
+
}
|
|
7818
|
+
};
|
|
7819
|
+
return {
|
|
7820
|
+
state: lines.join("\n"),
|
|
7821
|
+
questions: { [COMMAND_GUARD_QUESTION_ID]: question }
|
|
7822
|
+
};
|
|
7823
|
+
}
|
|
7824
|
+
/**
|
|
7825
|
+
* Read the safety probability out of a decision response.
|
|
7826
|
+
*
|
|
7827
|
+
* @param response - the parsed response.
|
|
7828
|
+
* @param questionId - the question id to read (defaults to the guard's own).
|
|
7829
|
+
* @returns the probability of "safe", or undefined when the answer is missing
|
|
7830
|
+
* or is not a `noul` verdict.
|
|
7831
|
+
*/
|
|
7832
|
+
function safeProbabilityOf(response, questionId = COMMAND_GUARD_QUESTION_ID) {
|
|
7833
|
+
const answer = response.answers[questionId];
|
|
7834
|
+
return answer !== void 0 && answer.type === "noul" ? answer.noul : void 0;
|
|
7835
|
+
}
|
|
7836
|
+
/**
|
|
7837
|
+
* One-line, length-bounded excerpt of a command for the log. The full text is
|
|
7838
|
+
* already in the session's tool call and was already sent to the decision
|
|
7839
|
+
* endpoint, so this adds no exposure — it is what makes "why did this run
|
|
7840
|
+
* without asking?" answerable from the log alone.
|
|
7841
|
+
*/
|
|
7842
|
+
function commandExcerpt(command, limit = 160) {
|
|
7843
|
+
const flat = command.replace(/\s+/g, " ").trim();
|
|
7844
|
+
return flat.length <= limit ? flat : `${flat.slice(0, limit - 1)}…`;
|
|
7845
|
+
}
|
|
7846
|
+
/**
|
|
7847
|
+
* The guard's stateful half: what was executed (by call id) and what has
|
|
7848
|
+
* already been decided (by agent and complete decision context), both bounded.
|
|
7849
|
+
*
|
|
7850
|
+
* The execution cache is the bridge between the two seams — `tools/pre-execute`
|
|
7851
|
+
* knows the arguments, `approval/request` knows the call id — and it is keyed by
|
|
7852
|
+
* call id with an agent-identity check when read: an entry for an execution
|
|
7853
|
+
* that never asked is never read, so there is nothing to clean up on the result
|
|
7854
|
+
* side (and the ask happens before the result anyway).
|
|
7855
|
+
*/
|
|
7856
|
+
var CommandGuard = class {
|
|
7857
|
+
deps;
|
|
7858
|
+
executions = /* @__PURE__ */ new Map();
|
|
7859
|
+
decisions = /* @__PURE__ */ new WeakMap();
|
|
7860
|
+
constructor(deps) {
|
|
7861
|
+
this.deps = deps;
|
|
7862
|
+
}
|
|
7863
|
+
/**
|
|
7864
|
+
* Remember one execution's command. Never throws, never changes a decision.
|
|
7865
|
+
*
|
|
7866
|
+
* @param execution - the execution about to run.
|
|
7867
|
+
*/
|
|
7868
|
+
noteExecution(execution) {
|
|
7869
|
+
const callId = execution?.callId;
|
|
7870
|
+
if (typeof callId !== "string" || callId === "") return;
|
|
7871
|
+
const fact = shellCommandOf(execution.name, execution.arguments);
|
|
7872
|
+
if (fact === void 0) return;
|
|
7873
|
+
this.executions.delete(callId);
|
|
7874
|
+
this.executions.set(callId, {
|
|
7875
|
+
fact,
|
|
7876
|
+
agent: execution.agent
|
|
7877
|
+
});
|
|
7878
|
+
while (this.executions.size > 64) {
|
|
7879
|
+
const oldest = this.executions.keys().next();
|
|
7880
|
+
if (oldest.done === true) break;
|
|
7881
|
+
this.executions.delete(oldest.value);
|
|
7882
|
+
}
|
|
7883
|
+
}
|
|
7884
|
+
/** Forget every remembered execution and decision (disposal, tests). */
|
|
7885
|
+
clear() {
|
|
7886
|
+
this.executions.clear();
|
|
7887
|
+
this.decisions = /* @__PURE__ */ new WeakMap();
|
|
7888
|
+
}
|
|
7889
|
+
/**
|
|
7890
|
+
* Judge one approval request.
|
|
7891
|
+
*
|
|
7892
|
+
* @param request - the pending approval.
|
|
7893
|
+
* @returns the verdict; `approve` is the only outcome that grants anything.
|
|
7894
|
+
*/
|
|
7895
|
+
async judge(request) {
|
|
7896
|
+
const settings = this.deps.settings();
|
|
7897
|
+
if (!settings.enabled) return {
|
|
7898
|
+
kind: "delegate",
|
|
7899
|
+
reason: "guard disabled"
|
|
7900
|
+
};
|
|
7901
|
+
const callId = request.callId;
|
|
7902
|
+
if (callId === void 0) return {
|
|
7903
|
+
kind: "delegate",
|
|
7904
|
+
reason: "approval carries no call id"
|
|
7905
|
+
};
|
|
7906
|
+
const observed = this.executions.get(callId);
|
|
7907
|
+
if (observed === void 0 || observed.agent !== request.agent) return {
|
|
7908
|
+
kind: "delegate",
|
|
7909
|
+
reason: "the guard never saw this call for this agent"
|
|
7910
|
+
};
|
|
7911
|
+
const fact = observed.fact;
|
|
7912
|
+
const excerpt = commandExcerpt(fact.command);
|
|
7913
|
+
if (!COMMAND_GUARD_SHELL_TOOLS.includes(request.toolName)) return {
|
|
7914
|
+
kind: "delegate",
|
|
7915
|
+
reason: `not a shell tool (${request.toolName})`
|
|
7916
|
+
};
|
|
7917
|
+
if (fact.command.length > 6e3) return {
|
|
7918
|
+
kind: "delegate",
|
|
7919
|
+
reason: `command longer than ${COMMAND_GUARD_MAX_COMMAND_CHARS} characters`,
|
|
7920
|
+
command: excerpt
|
|
7921
|
+
};
|
|
7922
|
+
const denied = denyReasonOf(fact.command);
|
|
7923
|
+
if (denied !== void 0) return {
|
|
7924
|
+
kind: "delegate",
|
|
7925
|
+
reason: `denylisted command (${denied})`,
|
|
7926
|
+
command: excerpt
|
|
7927
|
+
};
|
|
7928
|
+
const full = {
|
|
7929
|
+
...fact,
|
|
7930
|
+
...request.reason === void 0 ? {} : { reason: request.reason }
|
|
7931
|
+
};
|
|
7932
|
+
const memoKey = JSON.stringify(full);
|
|
7933
|
+
const now = this.deps.now?.() ?? Date.now();
|
|
7934
|
+
const agentDecisions = request.agent === void 0 ? void 0 : this.decisions.get(request.agent);
|
|
7935
|
+
const memo = agentDecisions?.get(memoKey);
|
|
7936
|
+
if (memo !== void 0) {
|
|
7937
|
+
if (now - memo.at < 6e5) return this.verdict(memo.probability, settings.threshold, "memo", excerpt);
|
|
7938
|
+
agentDecisions?.delete(memoKey);
|
|
7939
|
+
}
|
|
7940
|
+
let response;
|
|
7941
|
+
try {
|
|
7942
|
+
response = await this.deps.decide(buildSafetyRequest(full), {
|
|
7943
|
+
...request.signal === void 0 ? {} : { signal: request.signal },
|
|
7944
|
+
timeoutMs: settings.timeoutMs
|
|
7945
|
+
});
|
|
7946
|
+
} catch (error) {
|
|
7947
|
+
return {
|
|
7948
|
+
kind: "delegate",
|
|
7949
|
+
reason: `decision unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
7950
|
+
command: excerpt
|
|
7951
|
+
};
|
|
7952
|
+
}
|
|
7953
|
+
const probability = safeProbabilityOf(response);
|
|
7954
|
+
if (probability === void 0) return {
|
|
7955
|
+
kind: "delegate",
|
|
7956
|
+
reason: "the decision carried no safe verdict",
|
|
7957
|
+
command: excerpt
|
|
7958
|
+
};
|
|
7959
|
+
if (request.agent !== void 0) {
|
|
7960
|
+
const decisions = agentDecisions ?? /* @__PURE__ */ new Map();
|
|
7961
|
+
decisions.set(memoKey, {
|
|
7962
|
+
probability,
|
|
7963
|
+
at: now
|
|
7964
|
+
});
|
|
7965
|
+
while (decisions.size > 64) {
|
|
7966
|
+
const oldest = decisions.keys().next();
|
|
7967
|
+
if (oldest.done === true) break;
|
|
7968
|
+
decisions.delete(oldest.value);
|
|
7969
|
+
}
|
|
7970
|
+
this.decisions.set(request.agent, decisions);
|
|
7971
|
+
}
|
|
7972
|
+
return this.verdict(probability, settings.threshold, "model", excerpt);
|
|
7973
|
+
}
|
|
7974
|
+
/** Apply the threshold to one probability. */
|
|
7975
|
+
verdict(probability, threshold, source, command) {
|
|
7976
|
+
return probability >= threshold ? {
|
|
7977
|
+
kind: "approve",
|
|
7978
|
+
probability,
|
|
7979
|
+
source,
|
|
7980
|
+
command
|
|
7981
|
+
} : {
|
|
7982
|
+
kind: "delegate",
|
|
7983
|
+
reason: `safe probability ${probability.toFixed(3)} below ${threshold}`,
|
|
7984
|
+
probability,
|
|
7985
|
+
command
|
|
7986
|
+
};
|
|
7987
|
+
}
|
|
7988
|
+
};
|
|
7989
|
+
/**
|
|
7990
|
+
* Register the guard on a context that has the approval seam.
|
|
7991
|
+
*
|
|
7992
|
+
* Call this from inside `ctx.inject(['approval'], …)`: the seam's presence is
|
|
7993
|
+
* what makes the feature meaningful (no approval service means no asks), and
|
|
7994
|
+
* tying the listeners to that injected fiber is what makes them go away with it.
|
|
7995
|
+
*
|
|
7996
|
+
* @param ctx - the injected context (or a test double with `on`).
|
|
7997
|
+
* @param deps - settings, the decision client, clock and logger.
|
|
7998
|
+
* @returns a disposer for both listeners.
|
|
7999
|
+
*/
|
|
8000
|
+
function applyCommandGuard(ctx, deps) {
|
|
8001
|
+
const guard = new CommandGuard(deps);
|
|
8002
|
+
const events = ctx;
|
|
8003
|
+
const offPreExecute = events.on("tools/pre-execute", (execution, next) => {
|
|
8004
|
+
guard.noteExecution(execution);
|
|
8005
|
+
return next();
|
|
8006
|
+
}, { prepend: true });
|
|
8007
|
+
const offApproval = events.on("approval/request", async (request, next) => {
|
|
8008
|
+
try {
|
|
8009
|
+
if (!deps.settings().enabled) return next();
|
|
8010
|
+
} catch (error) {
|
|
8011
|
+
deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
8012
|
+
return next();
|
|
8013
|
+
}
|
|
8014
|
+
let verdict;
|
|
8015
|
+
try {
|
|
8016
|
+
verdict = await guard.judge(request);
|
|
8017
|
+
} catch (error) {
|
|
8018
|
+
deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
8019
|
+
return next();
|
|
8020
|
+
}
|
|
8021
|
+
if (verdict.kind === "approve") {
|
|
8022
|
+
deps.log?.(`command guard auto-approved a ${request.toolName} call (safe probability ${verdict.probability.toFixed(3)}, ${verdict.source}): ${verdict.command ?? "(command unknown)"}`);
|
|
8023
|
+
return "allowed-once";
|
|
8024
|
+
}
|
|
8025
|
+
deps.log?.(`command guard delegated a ${request.toolName} call: ${verdict.reason}${verdict.command === void 0 ? "" : ` — ${verdict.command}`}`);
|
|
8026
|
+
return next();
|
|
8027
|
+
}, { prepend: true });
|
|
8028
|
+
return () => {
|
|
8029
|
+
guard.clear();
|
|
8030
|
+
for (const off of [offPreExecute, offApproval]) if (typeof off === "function") off();
|
|
8031
|
+
};
|
|
8032
|
+
}
|
|
8033
|
+
//#endregion
|
|
8034
|
+
//#region src/config-volatile.ts
|
|
8035
|
+
/**
|
|
8036
|
+
* Mark one schema field volatile when the installed schemastery supports it.
|
|
8037
|
+
* @param schema - The field schema (any generation).
|
|
8038
|
+
* @returns The schema carrying `meta.volatile`, or the schema itself on an
|
|
8039
|
+
* engine whose schemastery predates `.volatile()`.
|
|
8040
|
+
*/
|
|
8041
|
+
function markVolatile(schema) {
|
|
8042
|
+
const candidate = schema;
|
|
8043
|
+
if (typeof candidate.volatile !== "function") return schema;
|
|
8044
|
+
return candidate.volatile();
|
|
8045
|
+
}
|
|
8046
|
+
/**
|
|
8047
|
+
* Mark every field of a Config field dict volatile, except the named
|
|
8048
|
+
* composition-only secrets (`apiKey` here, which no settings surface writes).
|
|
8049
|
+
*
|
|
8050
|
+
* The result is a NEW dict of schema instances: callers that also need an
|
|
8051
|
+
* UNMARKED schema for the legacy settings generation must build the fields
|
|
8052
|
+
* twice (`configFields()`), never share one dict — `.volatile()` clones per
|
|
8053
|
+
* call, and a shared instance would carry the mark into the legacy schema.
|
|
8054
|
+
*
|
|
8055
|
+
* @param fields - Freshly built field schemas.
|
|
8056
|
+
* @param skip - Field names that must stay unmarked.
|
|
8057
|
+
* @returns The field dict with every non-skipped field marked.
|
|
8058
|
+
*/
|
|
8059
|
+
function markVolatileFields(fields, skip = []) {
|
|
8060
|
+
return Object.fromEntries(Object.entries(fields).map(([name, schema]) => [name, skip.includes(name) ? schema : markVolatile(schema)]));
|
|
8061
|
+
}
|
|
8062
|
+
/**
|
|
8063
|
+
* Whether a resolved config value is a volatile reference.
|
|
8064
|
+
*
|
|
8065
|
+
* Duck-typed on the two properties `createVolatile` produces (frozen object
|
|
8066
|
+
* with a `get` method) rather than cosmokit's private `write` symbol, because
|
|
8067
|
+
* cosmokit is not a peer of this bundle. Config values are plain parsed JSON
|
|
8068
|
+
* (or `!!jsExpr` wrappers), which are never frozen, so the test cannot
|
|
8069
|
+
* misfire on an ordinary field.
|
|
8070
|
+
*/
|
|
8071
|
+
function isVolatileRef(value) {
|
|
8072
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.isFrozen(value) && typeof value.get === "function";
|
|
8073
|
+
}
|
|
8074
|
+
/**
|
|
8075
|
+
* Read one config object as PLAIN values across generations.
|
|
8076
|
+
*
|
|
8077
|
+
* Each top-level field found to be a volatile reference is read through
|
|
8078
|
+
* `.get()` — freshly on every call, so a live update lands on the very next
|
|
8079
|
+
* read. When no field is a reference the input object is returned as-is,
|
|
8080
|
+
* keeping reference identity stable for identity-keyed memos on engines that
|
|
8081
|
+
* hand `apply()` plain config.
|
|
8082
|
+
*/
|
|
8083
|
+
function unwrapVolatileConfig(config) {
|
|
8084
|
+
const source = config;
|
|
8085
|
+
if (typeof source !== "object" || source === null) return config;
|
|
8086
|
+
let found = false;
|
|
8087
|
+
const plain = {};
|
|
8088
|
+
for (const [field, value] of Object.entries(source)) if (isVolatileRef(value)) {
|
|
8089
|
+
plain[field] = value.get();
|
|
8090
|
+
found = true;
|
|
8091
|
+
} else plain[field] = value;
|
|
8092
|
+
return found ? plain : config;
|
|
8093
|
+
}
|
|
8094
|
+
//#endregion
|
|
8095
|
+
//#region src/index.ts
|
|
8096
|
+
/**
|
|
8097
|
+
* dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
|
|
8098
|
+
* Code (unofficial; ported from pi-commandcode-provider@0.5.1).
|
|
8099
|
+
*
|
|
8100
|
+
* Registers the `commandcode` provider route on `ctx.llm` and declares it in
|
|
8101
|
+
* the configurable-provider directory, so the web Models page shows a
|
|
8102
|
+
* "Command Code" card with an API-key field and the model picker lists the
|
|
6692
8103
|
* live Command Code model catalog. Connection facts resolve per request over
|
|
6693
|
-
* the
|
|
6694
|
-
*
|
|
6695
|
-
*
|
|
8104
|
+
* the plugin's live config — the `llm-commandcode` settings section through
|
|
8105
|
+
* dsh 0.1.6, profile Config (volatile fields, unwrapped per read) from 0.1.7
|
|
8106
|
+
* — and the credential seam, so a changed key, endpoint, or cache path
|
|
8107
|
+
* reaches the next request without a restart.
|
|
6696
8108
|
*
|
|
6697
8109
|
* ```yaml
|
|
6698
8110
|
* - id: llm-commandcode
|
|
@@ -6717,41 +8129,88 @@ const DEFAULT_API_KEY_ENV = "COMMANDCODE_API_KEY";
|
|
|
6717
8129
|
const PROVIDER = "commandcode";
|
|
6718
8130
|
/** Default models cache path (mirrors the pi plugin's on-disk cache). */
|
|
6719
8131
|
const DEFAULT_MODELS_CACHE_PATH = join(homedir(), ".commandcode", "models-cache.json");
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
8132
|
+
/**
|
|
8133
|
+
* The Config schema's fields, freshly built on every call.
|
|
8134
|
+
*
|
|
8135
|
+
* A FACTORY, not a shared dict, and that is load-bearing: the exported
|
|
8136
|
+
* {@link Config} marks every field volatile while {@link LegacySettingsSchema}
|
|
8137
|
+
* must carry no mark at all, and schemastery's schema instances are
|
|
8138
|
+
* single-purpose objects — sharing one dict would leak the marks into the
|
|
8139
|
+
* legacy registration (see `config-volatile.ts` for why that is a boot-time
|
|
8140
|
+
* `ValidationError` on ≤0.1.6 engines whose schemastery resolved to ≥3.18.3).
|
|
8141
|
+
*
|
|
8142
|
+
* @returns One fresh schema instance per Config field.
|
|
8143
|
+
*/
|
|
8144
|
+
function configFields() {
|
|
8145
|
+
return {
|
|
8146
|
+
apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
8147
|
+
apiKey: z.string().role("secret"),
|
|
8148
|
+
apiBase: z.string(),
|
|
8149
|
+
workingDir: z.string(),
|
|
8150
|
+
modelsCachePath: z.string(),
|
|
8151
|
+
requestTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
8152
|
+
streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
8153
|
+
transportMaxRetries: z.number().min(0).max(50),
|
|
8154
|
+
filterModelsByPlan: z.boolean(),
|
|
8155
|
+
visibleModels: z.array(z.string()),
|
|
8156
|
+
/**
|
|
8157
|
+
* Per-model visibility overrides for the terminal settings page's checkbox
|
|
8158
|
+
* list, keyed by catalog id. dsh-TUI addresses a staged edit by its field
|
|
8159
|
+
* PATH, so two checkboxes sharing one path would overwrite each other's
|
|
8160
|
+
* draft and the section's last model would decide every write; a map gives
|
|
8161
|
+
* each checkbox a path of its own. An id listed here wins over
|
|
8162
|
+
* {@link Config.visibleModels}; ids absent here keep following it.
|
|
8163
|
+
*/
|
|
8164
|
+
modelVisibility: z.dict(z.boolean()),
|
|
8165
|
+
webSearch: z.boolean().default(true),
|
|
8166
|
+
showSidebarQuota: z.boolean().default(false),
|
|
8167
|
+
accounts: z.array(z.object({
|
|
8168
|
+
label: z.string(),
|
|
8169
|
+
apiKeyEnv: z.string().role("credential-ref"),
|
|
8170
|
+
/** Literal key for one extra slot; see the top-level `apiKey` secret note. */
|
|
8171
|
+
apiKey: z.string().role("secret")
|
|
8172
|
+
})),
|
|
8173
|
+
activeAccount: z.string(),
|
|
8174
|
+
modelAccountRules: z.array(z.object({
|
|
8175
|
+
models: z.array(z.string()),
|
|
8176
|
+
account: z.string()
|
|
8177
|
+
})),
|
|
8178
|
+
commandGuard: z.boolean().default(false),
|
|
8179
|
+
commandGuardThreshold: z.number().min(COMMAND_GUARD_MIN_THRESHOLD).max(1),
|
|
8180
|
+
commandGuardTimeoutMs: z.number().min(200).max(COMMAND_GUARD_MAX_TIMEOUT_MS),
|
|
8181
|
+
zdr: z.boolean().default(false),
|
|
8182
|
+
lang: z.string().pattern(/^(zh|en)$/).default("zh")
|
|
8183
|
+
};
|
|
8184
|
+
}
|
|
8185
|
+
/**
|
|
8186
|
+
* The 0.1.7-generation schema: every field volatile except the
|
|
8187
|
+
* composition-only `apiKey` secret.
|
|
8188
|
+
*
|
|
8189
|
+
* dsh 0.1.7's settings forms are projected from the schema's `meta.volatile`
|
|
8190
|
+
* nodes, so an unmarked field would be invisible to AND unwritable from the
|
|
8191
|
+
* settings page and refused by form-edit path validation — and on that
|
|
8192
|
+
* generation the loader hands `apply()` a live reference per marked field,
|
|
8193
|
+
* committing later writes without remounting this fiber (the
|
|
8194
|
+
* `loader/volatile-update` listener at the bottom of `apply` covers the facts
|
|
8195
|
+
* that are not re-derived per read). The mark is inert on engines whose
|
|
8196
|
+
* schemastery predates `.volatile()`.
|
|
8197
|
+
*/
|
|
8198
|
+
const Config = z.object(markVolatileFields(configFields(), ["apiKey"]));
|
|
8199
|
+
/**
|
|
8200
|
+
* The ≤0.1.6-generation registration schema: the same fields, NO volatile
|
|
8201
|
+
* marks.
|
|
8202
|
+
*
|
|
8203
|
+
* The legacy `installSection(owner, ns, schema, entry, hooks)` re-validates the
|
|
8204
|
+
* `entry` it is handed (`register()` → `resolve()` → `schema(mergeLayers(base,
|
|
8205
|
+
* section))`) and its `describe()` structuredClones that same `entry`. Since
|
|
8206
|
+
* schemastery ≥3.18.3 creates references at parse time on EVERY generation —
|
|
8207
|
+
* `dsh-settings` through 0.1.6 declares `schemastery: ^3.18.2`, so old engines
|
|
8208
|
+
* freshly installed today resolve 3.18.3 — a marked schema plus the raw
|
|
8209
|
+
* reference-carrying `config` is a boot-time `ValidationError` there. This
|
|
8210
|
+
* schema is therefore unmarked, and `apply` pairs it with
|
|
8211
|
+
* `unwrapVolatileConfig(config)`.
|
|
8212
|
+
*/
|
|
8213
|
+
const LegacySettingsSchema = z.object(configFields());
|
|
6755
8214
|
/**
|
|
6756
8215
|
* The one explicit resolve step from raw config to validated connection
|
|
6757
8216
|
* facts. Programmatic construction may bypass Schemastery normalization, so
|
|
@@ -6768,7 +8227,8 @@ function resolveAdapterOptions(config) {
|
|
|
6768
8227
|
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
|
|
6769
8228
|
filterModelsByPlan: config.filterModelsByPlan ?? true,
|
|
6770
8229
|
visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0,
|
|
6771
|
-
modelVisibility: readModelVisibility(config.modelVisibility)
|
|
8230
|
+
modelVisibility: readModelVisibility(config.modelVisibility),
|
|
8231
|
+
zdr: config.zdr === true
|
|
6772
8232
|
};
|
|
6773
8233
|
}
|
|
6774
8234
|
/**
|
|
@@ -6785,7 +8245,8 @@ function readModelVisibility(raw) {
|
|
|
6785
8245
|
}
|
|
6786
8246
|
function apply(ctx, config) {
|
|
6787
8247
|
installCostProjection(ctx);
|
|
6788
|
-
let
|
|
8248
|
+
let source = () => config;
|
|
8249
|
+
const current = () => unwrapVolatileConfig(source());
|
|
6789
8250
|
let lastRaw;
|
|
6790
8251
|
let lastGood;
|
|
6791
8252
|
const options = () => {
|
|
@@ -6981,6 +8442,26 @@ function apply(ctx, config) {
|
|
|
6981
8442
|
applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
|
|
6982
8443
|
}, "dsh-commandcode-provider: web search selection");
|
|
6983
8444
|
});
|
|
8445
|
+
const commandGuardSettings = () => {
|
|
8446
|
+
const raw = current();
|
|
8447
|
+
return {
|
|
8448
|
+
enabled: raw.commandGuard === true,
|
|
8449
|
+
threshold: raw.commandGuardThreshold ?? .9,
|
|
8450
|
+
timeoutMs: raw.commandGuardTimeoutMs ?? COMMAND_GUARD_DEFAULT_TIMEOUT_MS
|
|
8451
|
+
};
|
|
8452
|
+
};
|
|
8453
|
+
ctx.inject(["approval"], (approvalCtx) => {
|
|
8454
|
+
applyCommandGuard(approvalCtx, {
|
|
8455
|
+
settings: commandGuardSettings,
|
|
8456
|
+
decide: (request, decideOptions) => runSystemOne({
|
|
8457
|
+
apiBase: () => options().apiBase,
|
|
8458
|
+
resolveApiKey: () => resolveApiKey(options())
|
|
8459
|
+
}, request, decideOptions),
|
|
8460
|
+
log: (message) => {
|
|
8461
|
+
ctx.logger.info(`llm-commandcode: ${message}`);
|
|
8462
|
+
}
|
|
8463
|
+
});
|
|
8464
|
+
});
|
|
6984
8465
|
let refreshTuiSettings;
|
|
6985
8466
|
ctx.inject(["tuiSettingsSections"], (tuiCtx) => {
|
|
6986
8467
|
refreshTuiSettings = applyCommandCodeTuiSettings(tuiCtx, {
|
|
@@ -6998,18 +8479,38 @@ function apply(ctx, config) {
|
|
|
6998
8479
|
}, "dsh-commandcode-provider: tui settings handle");
|
|
6999
8480
|
});
|
|
7000
8481
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
7001
|
-
settingsCtx.settings
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
8482
|
+
const service = settingsCtx.settings;
|
|
8483
|
+
if (typeof service.installSection === "function") {
|
|
8484
|
+
service.installSection(ctx, NS, LegacySettingsSchema, unwrapVolatileConfig(config), {
|
|
8485
|
+
setSource: (next) => {
|
|
8486
|
+
source = next;
|
|
8487
|
+
},
|
|
8488
|
+
onChange: () => {
|
|
8489
|
+
applySearchSelection(current().webSearch ?? true);
|
|
8490
|
+
refreshTuiSettings?.();
|
|
8491
|
+
}
|
|
8492
|
+
});
|
|
8493
|
+
return;
|
|
8494
|
+
}
|
|
8495
|
+
if (typeof service.configure === "function") settingsCtx.effect(() => {
|
|
8496
|
+
const dispose = service.configure?.({ auto: false }, ctx.fiber);
|
|
8497
|
+
return () => {
|
|
8498
|
+
dispose?.();
|
|
8499
|
+
};
|
|
8500
|
+
}, "dsh-commandcode-provider: settings auto-form policy");
|
|
7010
8501
|
});
|
|
8502
|
+
const loaderEvents = ctx;
|
|
8503
|
+
ctx.effect(() => {
|
|
8504
|
+
const off = loaderEvents.on("loader/volatile-update", () => {
|
|
8505
|
+
applySearchSelection(current().webSearch ?? true);
|
|
8506
|
+
refreshTuiSettings?.();
|
|
8507
|
+
});
|
|
8508
|
+
return () => {
|
|
8509
|
+
if (typeof off === "function") off();
|
|
8510
|
+
};
|
|
8511
|
+
}, "dsh-commandcode-provider: volatile config updates");
|
|
7011
8512
|
}
|
|
7012
8513
|
//#endregion
|
|
7013
|
-
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 };
|
|
8514
|
+
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_NON_ZDR_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, LegacySettingsSchema, 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, supportsZeroDataRetention, usageReportSchema, validateCommandApiKey };
|
|
7014
8515
|
|
|
7015
8516
|
//# sourceMappingURL=index.js.map
|