@eddyskywalker/dsh-chatgpt-subscription 0.38.0 → 0.39.0

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/README.md +48 -11
  3. package/lib/client.js +805 -151
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +2174 -700
  6. package/lib/types/client/CodexSubscriptionSection.d.ts +1 -1
  7. package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
  8. package/lib/types/client/api.d.ts +16 -1
  9. package/lib/types/client/api.d.ts.map +1 -1
  10. package/lib/types/client/cline-pass/ClinePassSection.d.ts.map +1 -1
  11. package/lib/types/client/cline-pass/api.d.ts +16 -1
  12. package/lib/types/client/cline-pass/api.d.ts.map +1 -1
  13. package/lib/types/client/cline-pass/locales.d.ts +144 -18
  14. package/lib/types/client/cline-pass/locales.d.ts.map +1 -1
  15. package/lib/types/client/cline-pass/styles.d.ts.map +1 -1
  16. package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
  17. package/lib/types/client/kimi-code/locales.d.ts +8 -17
  18. package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
  19. package/lib/types/client/locales-ru/cline-pass.d.ts.map +1 -1
  20. package/lib/types/client/locales-ru/hub.d.ts.map +1 -1
  21. package/lib/types/client/locales-ru/kimi-code.d.ts.map +1 -1
  22. package/lib/types/client/locales.d.ts +43 -1
  23. package/lib/types/client/locales.d.ts.map +1 -1
  24. package/lib/types/client/minimax-code/commands-contract.d.ts +14 -4
  25. package/lib/types/client/minimax-code/commands-contract.d.ts.map +1 -1
  26. package/lib/types/client/minimax-code/locales.d.ts +2 -2
  27. package/lib/types/client/progress-updates/settings-binding.d.ts +8 -1
  28. package/lib/types/client/progress-updates/settings-binding.d.ts.map +1 -1
  29. package/lib/types/client/progress-updates/settings-section.d.ts.map +1 -1
  30. package/lib/types/client/styles.d.ts.map +1 -1
  31. package/lib/types/compat.d.ts +33 -0
  32. package/lib/types/compat.d.ts.map +1 -1
  33. package/lib/types/host/agent-preset.d.ts +5 -0
  34. package/lib/types/host/agent-preset.d.ts.map +1 -1
  35. package/lib/types/host/antigravity/account-pool.d.ts.map +1 -1
  36. package/lib/types/host/antigravity/mapper.d.ts +0 -1
  37. package/lib/types/host/antigravity/mapper.d.ts.map +1 -1
  38. package/lib/types/host/antigravity/types.d.ts +1 -2
  39. package/lib/types/host/antigravity/types.d.ts.map +1 -1
  40. package/lib/types/host/claude/mapper.d.ts +5 -7
  41. package/lib/types/host/claude/mapper.d.ts.map +1 -1
  42. package/lib/types/host/claude/model-catalog.d.ts.map +1 -1
  43. package/lib/types/host/cline-pass/adapter.d.ts +1 -1
  44. package/lib/types/host/cline-pass/adapter.d.ts.map +1 -1
  45. package/lib/types/host/cline-pass/client.d.ts +16 -3
  46. package/lib/types/host/cline-pass/client.d.ts.map +1 -1
  47. package/lib/types/host/cline-pass/engine.d.ts +11 -3
  48. package/lib/types/host/cline-pass/engine.d.ts.map +1 -1
  49. package/lib/types/host/cline-pass/line.d.ts +1 -1
  50. package/lib/types/host/cline-pass/line.d.ts.map +1 -1
  51. package/lib/types/host/cline-pass/protocol.d.ts +18 -0
  52. package/lib/types/host/cline-pass/protocol.d.ts.map +1 -1
  53. package/lib/types/host/cline-pass/routes.d.ts +2 -0
  54. package/lib/types/host/cline-pass/routes.d.ts.map +1 -1
  55. package/lib/types/host/cline-pass/store.d.ts +2 -0
  56. package/lib/types/host/cline-pass/store.d.ts.map +1 -1
  57. package/lib/types/host/cline-pass/token-store.d.ts +11 -0
  58. package/lib/types/host/cline-pass/token-store.d.ts.map +1 -1
  59. package/lib/types/host/cline-pass/tools.d.ts.map +1 -1
  60. package/lib/types/host/cline-pass/types.d.ts +24 -2
  61. package/lib/types/host/cline-pass/types.d.ts.map +1 -1
  62. package/lib/types/host/codex-account-pool.d.ts +3 -0
  63. package/lib/types/host/codex-account-pool.d.ts.map +1 -1
  64. package/lib/types/host/common/account-pool.d.ts +59 -5
  65. package/lib/types/host/common/account-pool.d.ts.map +1 -1
  66. package/lib/types/host/common/credential-read-cache.d.ts +11 -1
  67. package/lib/types/host/common/credential-read-cache.d.ts.map +1 -1
  68. package/lib/types/host/common/credentials-compat.d.ts +73 -0
  69. package/lib/types/host/common/credentials-compat.d.ts.map +1 -0
  70. package/lib/types/host/common/pool-storage-settings.d.ts +41 -0
  71. package/lib/types/host/common/pool-storage-settings.d.ts.map +1 -0
  72. package/lib/types/host/common/pool-storage.d.ts +143 -0
  73. package/lib/types/host/common/pool-storage.d.ts.map +1 -0
  74. package/lib/types/host/credential-store-secret-service.d.ts +27 -0
  75. package/lib/types/host/credential-store-secret-service.d.ts.map +1 -1
  76. package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
  77. package/lib/types/host/kimi-code/cache-hint.d.ts +29 -15
  78. package/lib/types/host/kimi-code/cache-hint.d.ts.map +1 -1
  79. package/lib/types/host/kimi-code/mapper.d.ts +34 -31
  80. package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
  81. package/lib/types/host/kimi-code/model-catalog.d.ts +32 -0
  82. package/lib/types/host/kimi-code/model-catalog.d.ts.map +1 -1
  83. package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
  84. package/lib/types/host/kimi-code/token-store.d.ts +1 -11
  85. package/lib/types/host/kimi-code/token-store.d.ts.map +1 -1
  86. package/lib/types/host/kimi-code/types.d.ts +38 -14
  87. package/lib/types/host/kimi-code/types.d.ts.map +1 -1
  88. package/lib/types/host/oauth-service.d.ts +83 -1
  89. package/lib/types/host/oauth-service.d.ts.map +1 -1
  90. package/lib/types/host/preset-sync.d.ts +2 -2
  91. package/lib/types/host/preset-sync.d.ts.map +1 -1
  92. package/lib/types/host/qoder/account-pool.d.ts +4 -0
  93. package/lib/types/host/qoder/account-pool.d.ts.map +1 -1
  94. package/lib/types/host/qoder/client.d.ts +24 -2
  95. package/lib/types/host/qoder/client.d.ts.map +1 -1
  96. package/lib/types/host/qoder/oauth.d.ts +17 -0
  97. package/lib/types/host/qoder/oauth.d.ts.map +1 -1
  98. package/lib/types/host/qoder/routes.d.ts.map +1 -1
  99. package/lib/types/host/routes.d.ts +10 -0
  100. package/lib/types/host/routes.d.ts.map +1 -1
  101. package/lib/types/host/subagent-model-authorization.d.ts +20 -5
  102. package/lib/types/host/subagent-model-authorization.d.ts.map +1 -1
  103. package/lib/types/host/token-store-linux.d.ts.map +1 -1
  104. package/lib/types/host/token-store.d.ts +16 -0
  105. package/lib/types/host/token-store.d.ts.map +1 -1
  106. package/lib/types/index.d.ts +26 -1
  107. package/lib/types/index.d.ts.map +1 -1
  108. package/lib/types/shared/cline-pass-contracts.d.ts +48 -2
  109. package/lib/types/shared/cline-pass-contracts.d.ts.map +1 -1
  110. package/lib/types/shared/contracts.d.ts +86 -0
  111. package/lib/types/shared/contracts.d.ts.map +1 -1
  112. package/lib/types/shared/kimi-code-contracts.d.ts +0 -16
  113. package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
  114. package/package.json +37 -37
  115. package/presets/dispatch/agent.cordis.yml +25 -11
package/lib/index.js CHANGED
@@ -100,6 +100,37 @@ const OAUTH_SCOPE = "openid profile email offline_access";
100
100
  const CODEX_ORIGINATOR = "opencode";
101
101
  /** OAuth presents the same identity the request path does (see above). */
102
102
  const OAUTH_ORIGINATOR = CODEX_ORIGINATOR;
103
+ /**
104
+ * The device-code grant: the one sign-in path a headless host can finish.
105
+ *
106
+ * The browser flow redirects to a *fixed* `http://localhost:1455/auth/callback`
107
+ * registered against this OAuth client, so it cannot be completed from a server:
108
+ * nothing listens on that loopback port there, and an HTTPS page cannot reach it
109
+ * anyway (mixed content). The device grant takes the redirect out of the
110
+ * picture — the user types a short code on a page on their own machine and this
111
+ * process polls for the result.
112
+ *
113
+ * Same client id as the browser flow, because it is the same registration. These
114
+ * are that registration's device endpoints, verified against the live service.
115
+ */
116
+ const OAUTH_DEVICE_USERCODE_URL = `${CHATGPT_OAUTH_ISSUER}/api/accounts/deviceauth/usercode`;
117
+ const OAUTH_DEVICE_TOKEN_URL = `${CHATGPT_OAUTH_ISSUER}/api/accounts/deviceauth/token`;
118
+ const OAUTH_DEVICE_VERIFY_URL = `${CHATGPT_OAUTH_ISSUER}/codex/device`;
119
+ /**
120
+ * The redirect the device grant redeems its authorization code against.
121
+ *
122
+ * Nothing listens on it: the device endpoint hands the code to this process
123
+ * directly and the token endpoint only checks it against the grant.
124
+ */
125
+ const OAUTH_DEVICE_REDIRECT_URI = `${CHATGPT_OAUTH_ISSUER}/deviceauth/callback`;
126
+ /** Deadline used when a grant states no usable `expires_at` (the live one is ~15 min out). */
127
+ const OAUTH_DEVICE_GRANT_LIFETIME_MS = 15 * 6e4;
128
+ /**
129
+ * Floor for a fractional poll interval (a grant answering `"0.2"`), so it cannot
130
+ * turn the loop into a spin. `"0"`, a negative or an unparseable value never get
131
+ * this far: `parseGrantInterval` already replaced them with the 5 s default.
132
+ */
133
+ const OAUTH_DEVICE_MIN_POLL_INTERVAL_MS = 1e3;
103
134
  const TOKEN_REFRESH_MARGIN_MS = 6e4;
104
135
  const ROUTE_PREFIX$4 = "/api/dsh-chatgpt-subscription";
105
136
  const PLUGIN_VERSION = "0.1.0-alpha.0";
@@ -878,6 +909,61 @@ function asNumber$7(value) {
878
909
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
879
910
  }
880
911
  //#endregion
912
+ //#region src/host/token-store.ts
913
+ /**
914
+ * A stored document that cannot be read back as one.
915
+ *
916
+ * Distinct from a store that did not answer, on purpose. A corrupt document is
917
+ * something the NEXT WRITE repairs; an unreadable store is not, and recovering
918
+ * from it as if the pool were empty is how a transient keyring failure erases
919
+ * every account: the read comes back empty, the caller mutates the empty pool and
920
+ * saves it. Stores throw this class for their own "payload is invalid" case and
921
+ * leave availability failures as plain errors, which is the only distinction the
922
+ * pool core needs (see `AccountPoolCore.loadPoolData`).
923
+ */
924
+ var CorruptStoredDocumentError = class extends Error {
925
+ constructor(message, options) {
926
+ super(message, options);
927
+ this.name = "CorruptStoredDocumentError";
928
+ }
929
+ };
930
+ /** Whether a store failure means "this document does not parse", not "the store is unreachable". */
931
+ function isCorruptStoredDocument(error) {
932
+ return error instanceof CorruptStoredDocumentError;
933
+ }
934
+ function parseStoredCredentials(value) {
935
+ if (typeof value !== "object" || value === null) throw new Error("credential bundle is not an object");
936
+ const record = value;
937
+ if (typeof record.accessToken !== "string" || record.accessToken === "") throw new Error("access token is missing");
938
+ if (typeof record.refreshToken !== "string" || record.refreshToken === "") throw new Error("refresh token is missing");
939
+ if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt)) throw new Error("expiry is invalid");
940
+ const optional = (key) => {
941
+ const candidate = record[key];
942
+ if (candidate === void 0) return void 0;
943
+ if (typeof candidate !== "string") throw new Error(`${key} is invalid`);
944
+ return candidate;
945
+ };
946
+ const credentials = {
947
+ accessToken: record.accessToken,
948
+ refreshToken: record.refreshToken,
949
+ expiresAt: record.expiresAt,
950
+ idToken: optional("idToken"),
951
+ accountId: optional("accountId"),
952
+ email: optional("email"),
953
+ planType: optional("planType")
954
+ };
955
+ if (record.adopted === true) {
956
+ const source = optional("source");
957
+ if (source !== void 0) {
958
+ credentials.adopted = true;
959
+ credentials.source = source;
960
+ const sourcePath = optional("sourcePath");
961
+ if (sourcePath !== void 0) credentials.sourcePath = sourcePath;
962
+ }
963
+ }
964
+ return credentials;
965
+ }
966
+ //#endregion
881
967
  //#region src/host/common/credential-read-cache.ts
882
968
  /**
883
969
  * Identity a file-backed credential store reports when nothing is stored.
@@ -944,6 +1030,16 @@ var CredentialReadCache = class {
944
1030
  entry = null;
945
1031
  pending = null;
946
1032
  /**
1033
+ * Bumped by every {@link invalidate}.
1034
+ *
1035
+ * A read that was already in flight when the value changed must not fill the
1036
+ * snapshot: with a 15-second keyring identity bucket, its bytes are the ones
1037
+ * that were just replaced, and the next `load()` would answer with a refresh
1038
+ * token the pool has already spent — up to fifteen seconds of exactly the
1039
+ * double spend the single-flight path in `freshAccount` exists to prevent.
1040
+ */
1041
+ generation = 0;
1042
+ /**
947
1043
  * @param identity - cheap identity of the stored value; `null` disables
948
1044
  * caching for the read.
949
1045
  */
@@ -969,6 +1065,7 @@ var CredentialReadCache = class {
969
1065
  return null;
970
1066
  }
971
1067
  if (this.pending !== null && this.pending.identity === identity) return this.pending.promise;
1068
+ const generation = this.generation;
972
1069
  const promise = read();
973
1070
  this.pending = {
974
1071
  identity,
@@ -976,7 +1073,7 @@ var CredentialReadCache = class {
976
1073
  };
977
1074
  const settle = (value) => {
978
1075
  if (this.pending !== null && this.pending.promise === promise) this.pending = null;
979
- this.entry = {
1076
+ if (generation === this.generation) this.entry = {
980
1077
  identity,
981
1078
  value
982
1079
  };
@@ -986,8 +1083,9 @@ var CredentialReadCache = class {
986
1083
  });
987
1084
  return promise;
988
1085
  }
989
- /** Drop the snapshot; the next read consults storage again. */
1086
+ /** Drop the snapshot; the next read consults storage again and in-flight ones stop filling it. */
990
1087
  invalidate() {
1088
+ this.generation += 1;
991
1089
  this.entry = null;
992
1090
  this.pending = null;
993
1091
  }
@@ -996,198 +1094,6 @@ function isMissing$1(error) {
996
1094
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
997
1095
  }
998
1096
  //#endregion
999
- //#region src/host/token-store.ts
1000
- function parseStoredCredentials(value) {
1001
- if (typeof value !== "object" || value === null) throw new Error("credential bundle is not an object");
1002
- const record = value;
1003
- if (typeof record.accessToken !== "string" || record.accessToken === "") throw new Error("access token is missing");
1004
- if (typeof record.refreshToken !== "string" || record.refreshToken === "") throw new Error("refresh token is missing");
1005
- if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt)) throw new Error("expiry is invalid");
1006
- const optional = (key) => {
1007
- const candidate = record[key];
1008
- if (candidate === void 0) return void 0;
1009
- if (typeof candidate !== "string") throw new Error(`${key} is invalid`);
1010
- return candidate;
1011
- };
1012
- const credentials = {
1013
- accessToken: record.accessToken,
1014
- refreshToken: record.refreshToken,
1015
- expiresAt: record.expiresAt,
1016
- idToken: optional("idToken"),
1017
- accountId: optional("accountId"),
1018
- email: optional("email"),
1019
- planType: optional("planType")
1020
- };
1021
- if (record.adopted === true) {
1022
- const source = optional("source");
1023
- if (source !== void 0) {
1024
- credentials.adopted = true;
1025
- credentials.source = source;
1026
- const sourcePath = optional("sourcePath");
1027
- if (sourcePath !== void 0) credentials.sourcePath = sourcePath;
1028
- }
1029
- }
1030
- return credentials;
1031
- }
1032
- //#endregion
1033
- //#region src/host/token-store-windows.ts
1034
- const PROTECT_SCRIPT = String.raw`
1035
- $ErrorActionPreference = 'Stop'
1036
- [Console]::InputEncoding = [Text.Encoding]::UTF8
1037
- [Console]::OutputEncoding = [Text.Encoding]::UTF8
1038
- $OutputEncoding = [Text.Encoding]::UTF8
1039
- Add-Type -AssemblyName System.Security
1040
- $path = $env:DSH_CODEX_TOKEN_PATH
1041
- $plain = [Console]::In.ReadToEnd()
1042
- $bytes = [Text.Encoding]::UTF8.GetBytes($plain)
1043
- $cipher = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
1044
- $directory = [IO.Path]::GetDirectoryName($path)
1045
- [IO.Directory]::CreateDirectory($directory) | Out-Null
1046
- $temporary = $path + '.tmp-' + [Guid]::NewGuid().ToString('N')
1047
- try {
1048
- [IO.File]::WriteAllBytes($temporary, $cipher)
1049
- if ([IO.File]::Exists($path)) {
1050
- [IO.File]::Replace($temporary, $path, [System.Management.Automation.Language.NullString]::Value)
1051
- } else {
1052
- [IO.File]::Move($temporary, $path)
1053
- }
1054
- } finally {
1055
- if ([IO.File]::Exists($temporary)) { [IO.File]::Delete($temporary) }
1056
- }
1057
- `;
1058
- const UNPROTECT_SCRIPT = String.raw`
1059
- $ErrorActionPreference = 'Stop'
1060
- [Console]::InputEncoding = [Text.Encoding]::UTF8
1061
- [Console]::OutputEncoding = [Text.Encoding]::UTF8
1062
- $OutputEncoding = [Text.Encoding]::UTF8
1063
- Add-Type -AssemblyName System.Security
1064
- $path = $env:DSH_CODEX_TOKEN_PATH
1065
- if (-not [IO.File]::Exists($path)) { exit 3 }
1066
- $cipher = [IO.File]::ReadAllBytes($path)
1067
- $bytes = [Security.Cryptography.ProtectedData]::Unprotect($cipher, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
1068
- [Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))
1069
- `;
1070
- const CLEAR_SCRIPT = String.raw`
1071
- $ErrorActionPreference = 'Stop'
1072
- $path = $env:DSH_CODEX_TOKEN_PATH
1073
- if ([IO.File]::Exists($path)) { [IO.File]::Delete($path) }
1074
- `;
1075
- function defaultDpapiCredentialPath() {
1076
- return join(process.env.DSH_HOME?.trim() || join(homedir(), ".dsh"), "storages", "dsh-chatgpt-subscription", "oauth.dpapi");
1077
- }
1078
- var WindowsDpapiCredentialStore = class {
1079
- path;
1080
- parse;
1081
- storage = {
1082
- kind: "windows-dpapi",
1083
- encrypted: true
1084
- };
1085
- /**
1086
- * Decrypted snapshot, validated against the file's own mtime and size.
1087
- *
1088
- * Every uncached read is a spawned `powershell.exe` (~200 ms), and one
1089
- * settings request reads several credentials, so the snapshot is what keeps a
1090
- * tab switch from costing seconds.
1091
- */
1092
- cache = new CredentialReadCache(() => fileCredentialIdentity(this.path));
1093
- constructor(path, parse) {
1094
- this.path = path;
1095
- this.parse = parse;
1096
- if (process.platform !== "win32") throw new Error("Windows DPAPI storage requires Windows");
1097
- if (dirname(path) === path) throw new Error("invalid DPAPI credential path");
1098
- }
1099
- async load() {
1100
- return this.cache.load(async () => {
1101
- const result = await runPowerShell(UNPROTECT_SCRIPT, this.path, "");
1102
- if (result.code === 3) return null;
1103
- if (result.code !== 0) throw new Error("DPAPI credential read failed");
1104
- try {
1105
- return this.parse(JSON.parse(result.stdout));
1106
- } catch {
1107
- throw new Error("DPAPI credential payload is invalid");
1108
- }
1109
- });
1110
- }
1111
- async save(value) {
1112
- if ((await runPowerShell(PROTECT_SCRIPT, this.path, JSON.stringify(value))).code !== 0) throw new Error("DPAPI credential write failed");
1113
- this.cache.invalidate();
1114
- }
1115
- async clear() {
1116
- if ((await runPowerShell(CLEAR_SCRIPT, this.path, "")).code !== 0) throw new Error("DPAPI credential deletion failed");
1117
- this.cache.invalidate();
1118
- }
1119
- };
1120
- var WindowsDpapiTokenStore = class extends WindowsDpapiCredentialStore {
1121
- constructor(path = defaultDpapiCredentialPath()) {
1122
- super(path, parseStoredCredentials);
1123
- }
1124
- };
1125
- /**
1126
- * How long a helper may run before it is killed and the read fails.
1127
- *
1128
- * A healthy spawn costs ~200 ms (see the cache note on this class), so the guard
1129
- * exists only for a child that never exits, and it was set at 10 s - fifty times
1130
- * the healthy cost. That turned out to be too tight for one real environment: a
1131
- * cold Windows runner whose freshly-installed dependency tree is being scanned
1132
- * on first spawn, where the helper legitimately took longer than 10 s and the
1133
- * kill turned a working credential read into `DPAPI helper timed out`.
1134
- *
1135
- * The guard is kept, and only its headroom widened: 30 s is still bounded, still
1136
- * far above anything a warm machine needs, and a hung helper now blocks one
1137
- * credential read for 30 s rather than 10 - a worse wait in a case that is already
1138
- * a failure, in exchange for not failing a machine that is merely cold.
1139
- */
1140
- const POWERSHELL_HELPER_TIMEOUT_MS = 3e4;
1141
- function runPowerShell(script, path, stdin) {
1142
- return new Promise((resolve, reject) => {
1143
- const child = spawn("powershell.exe", [
1144
- "-NoLogo",
1145
- "-NoProfile",
1146
- "-NonInteractive",
1147
- "-Command",
1148
- script
1149
- ], {
1150
- env: {
1151
- ...process.env,
1152
- DSH_CODEX_TOKEN_PATH: path
1153
- },
1154
- stdio: [
1155
- "pipe",
1156
- "pipe",
1157
- "pipe"
1158
- ],
1159
- windowsHide: true
1160
- });
1161
- let stdout = "";
1162
- let stderrLength = 0;
1163
- const timer = setTimeout(() => {
1164
- child.kill();
1165
- reject(/* @__PURE__ */ new Error("DPAPI helper timed out"));
1166
- }, POWERSHELL_HELPER_TIMEOUT_MS);
1167
- child.stdout.setEncoding("utf8");
1168
- child.stdout.on("data", (chunk) => {
1169
- stdout += chunk;
1170
- if (stdout.length > 1 << 20) child.kill();
1171
- });
1172
- child.stderr.on("data", (chunk) => {
1173
- stderrLength += chunk.length;
1174
- if (stderrLength > 1 << 20) child.kill();
1175
- });
1176
- child.once("error", (error) => {
1177
- clearTimeout(timer);
1178
- reject(error);
1179
- });
1180
- child.once("close", (code) => {
1181
- clearTimeout(timer);
1182
- resolve({
1183
- code: code ?? 1,
1184
- stdout
1185
- });
1186
- });
1187
- child.stdin.end(stdin);
1188
- });
1189
- }
1190
- //#endregion
1191
1097
  //#region src/host/token-store-macos.ts
1192
1098
  const DEFAULT_SERVICE = "dsh-chatgpt-subscription";
1193
1099
  const DEFAULT_ACCOUNT = "oauth";
@@ -1242,7 +1148,7 @@ var MacKeychainCredentialStore = class {
1242
1148
  try {
1243
1149
  return this.parse(parseSecurityPayload(result.stdout));
1244
1150
  } catch {
1245
- throw new Error("Keychain credential payload is invalid");
1151
+ throw new CorruptStoredDocumentError("Keychain credential payload is invalid");
1246
1152
  }
1247
1153
  });
1248
1154
  }
@@ -1370,9 +1276,9 @@ function envelopeFrom(parsed) {
1370
1276
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
1371
1277
  const marker = parsed[ENVELOPE_KEY];
1372
1278
  if (marker === void 0) return null;
1373
- if (typeof marker !== "object" || marker === null) throw new Error(INVALID);
1279
+ if (typeof marker !== "object" || marker === null) throw new CorruptStoredDocumentError(INVALID);
1374
1280
  const { gen, chunks, bytes } = marker;
1375
- if (typeof gen !== "string" || gen === "" || typeof chunks !== "number" || !Number.isInteger(chunks) || chunks < 1 || chunks > MAX_SHARDS || typeof bytes !== "number" || !Number.isInteger(bytes) || bytes < 0) throw new Error(INVALID);
1281
+ if (typeof gen !== "string" || gen === "" || typeof chunks !== "number" || !Number.isInteger(chunks) || chunks < 1 || chunks > MAX_SHARDS || typeof bytes !== "number" || !Number.isInteger(bytes) || bytes < 0) throw new CorruptStoredDocumentError(INVALID);
1376
1282
  return {
1377
1283
  gen,
1378
1284
  chunks,
@@ -1390,6 +1296,18 @@ var SecretServiceCredentialStore = class {
1390
1296
  this.account = account;
1391
1297
  this.parse = parse;
1392
1298
  }
1299
+ /**
1300
+ * Label the desktop keyring manager shows for this entry.
1301
+ *
1302
+ * Derived from the entry's own `service` attribute: it used to be a hardcoded
1303
+ * 'DSH Antigravity OAuth' for every caller, so the ChatGPT, Claude, Qoder and
1304
+ * Minimax pools all appeared in seahorse under Antigravity's name and a user
1305
+ * could not tell which secret they were looking at. The label is display-only,
1306
+ * so entries stored under the old one keep working.
1307
+ */
1308
+ get label() {
1309
+ return `DSH ${this.service}`;
1310
+ }
1393
1311
  attributes() {
1394
1312
  return [
1395
1313
  "service",
@@ -1411,6 +1329,31 @@ var SecretServiceCredentialStore = class {
1411
1329
  `${this.account}#${gen}#${index}`
1412
1330
  ];
1413
1331
  }
1332
+ /**
1333
+ * Whether this host has a usable Secret Service at all.
1334
+ *
1335
+ * One lookup is the whole question, and its ANSWER is not the point: an entry
1336
+ * that is not there exits 1 with empty stderr, which still proves the daemon
1337
+ * answered. A host with no `secret-tool`, no session bus, no
1338
+ * `org.freedesktop.secrets` or a locked keyring fails instead — by refusing the
1339
+ * spawn, or by exiting non-zero with a message. Never throws, because it is
1340
+ * asked by a fallback chain whose next step is the answer to "no".
1341
+ */
1342
+ async ping() {
1343
+ try {
1344
+ const result = await runSecretTool(["lookup", ...this.attributes()]);
1345
+ if (result.code === 0 || result.code === 1 && !result.hasStderr) return { available: true };
1346
+ return {
1347
+ available: false,
1348
+ reason: UNAVAILABLE
1349
+ };
1350
+ } catch (error) {
1351
+ return {
1352
+ available: false,
1353
+ reason: error instanceof Error ? error.message : String(error)
1354
+ };
1355
+ }
1356
+ }
1414
1357
  async load() {
1415
1358
  return this.cache.load(async () => {
1416
1359
  const result = await runSecretTool(["lookup", ...this.attributes()]);
@@ -1420,18 +1363,18 @@ var SecretServiceCredentialStore = class {
1420
1363
  try {
1421
1364
  parsed = JSON.parse(result.stdout);
1422
1365
  } catch {
1423
- throw new Error(INVALID);
1366
+ throw new CorruptStoredDocumentError(INVALID);
1424
1367
  }
1425
1368
  const envelope = envelopeFrom(parsed);
1426
1369
  if (envelope === null) try {
1427
1370
  return this.parse(parsed);
1428
1371
  } catch {
1429
- throw new Error(INVALID);
1372
+ throw new CorruptStoredDocumentError(INVALID);
1430
1373
  }
1431
1374
  const pieces = [];
1432
1375
  for (let index = 0; index < envelope.chunks; index += 1) {
1433
1376
  const shard = await runSecretTool(["lookup", ...this.shardAttributes(envelope.gen, index)]);
1434
- if (shard.code !== 0) throw new Error(INVALID);
1377
+ if (shard.code !== 0) throw new CorruptStoredDocumentError(INVALID);
1435
1378
  pieces.push(shard.stdout);
1436
1379
  }
1437
1380
  try {
@@ -1439,7 +1382,7 @@ var SecretServiceCredentialStore = class {
1439
1382
  if (decoded.length !== envelope.bytes) throw new Error("shard byte count mismatch");
1440
1383
  return this.parse(JSON.parse(decoded.toString("utf8")));
1441
1384
  } catch {
1442
- throw new Error(INVALID);
1385
+ throw new CorruptStoredDocumentError(INVALID);
1443
1386
  }
1444
1387
  });
1445
1388
  }
@@ -1450,7 +1393,7 @@ var SecretServiceCredentialStore = class {
1450
1393
  if (bytes < SECRET_TOOL_STDIN_LIMIT) {
1451
1394
  if ((await runSecretTool([
1452
1395
  "store",
1453
- "--label=DSH Antigravity OAuth",
1396
+ "--label=" + this.label,
1454
1397
  ...this.attributes()
1455
1398
  ], payload)).code !== 0) throw new Error(UNAVAILABLE);
1456
1399
  } else await this.saveChunked(payload, bytes);
@@ -1473,7 +1416,7 @@ var SecretServiceCredentialStore = class {
1473
1416
  const piece = encoded.slice(index * SHARD_PAYLOAD_SIZE, (index + 1) * SHARD_PAYLOAD_SIZE);
1474
1417
  if ((await runSecretTool([
1475
1418
  "store",
1476
- "--label=DSH Antigravity OAuth",
1419
+ "--label=" + this.label,
1477
1420
  ...this.shardAttributes(gen, index)
1478
1421
  ], piece)).code !== 0) {
1479
1422
  await this.clearShards(gen, index);
@@ -1487,7 +1430,7 @@ var SecretServiceCredentialStore = class {
1487
1430
  };
1488
1431
  if ((await runSecretTool([
1489
1432
  "store",
1490
- "--label=DSH Antigravity OAuth",
1433
+ "--label=" + this.label,
1491
1434
  ...this.attributes()
1492
1435
  ], JSON.stringify({ [ENVELOPE_KEY]: envelope }))).code !== 0) {
1493
1436
  await this.clearShards(gen, chunks);
@@ -1582,14 +1525,491 @@ function runSecretTool(args, stdin = "") {
1582
1525
  child.stdin.end(stdin);
1583
1526
  });
1584
1527
  }
1528
+ //#endregion
1529
+ //#region src/host/token-store-windows.ts
1530
+ const PROTECT_SCRIPT = String.raw`
1531
+ $ErrorActionPreference = 'Stop'
1532
+ [Console]::InputEncoding = [Text.Encoding]::UTF8
1533
+ [Console]::OutputEncoding = [Text.Encoding]::UTF8
1534
+ $OutputEncoding = [Text.Encoding]::UTF8
1535
+ Add-Type -AssemblyName System.Security
1536
+ $path = $env:DSH_CODEX_TOKEN_PATH
1537
+ $plain = [Console]::In.ReadToEnd()
1538
+ $bytes = [Text.Encoding]::UTF8.GetBytes($plain)
1539
+ $cipher = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
1540
+ $directory = [IO.Path]::GetDirectoryName($path)
1541
+ [IO.Directory]::CreateDirectory($directory) | Out-Null
1542
+ $temporary = $path + '.tmp-' + [Guid]::NewGuid().ToString('N')
1543
+ try {
1544
+ [IO.File]::WriteAllBytes($temporary, $cipher)
1545
+ if ([IO.File]::Exists($path)) {
1546
+ [IO.File]::Replace($temporary, $path, [System.Management.Automation.Language.NullString]::Value)
1547
+ } else {
1548
+ [IO.File]::Move($temporary, $path)
1549
+ }
1550
+ } finally {
1551
+ if ([IO.File]::Exists($temporary)) { [IO.File]::Delete($temporary) }
1552
+ }
1553
+ `;
1554
+ const UNPROTECT_SCRIPT = String.raw`
1555
+ $ErrorActionPreference = 'Stop'
1556
+ [Console]::InputEncoding = [Text.Encoding]::UTF8
1557
+ [Console]::OutputEncoding = [Text.Encoding]::UTF8
1558
+ $OutputEncoding = [Text.Encoding]::UTF8
1559
+ Add-Type -AssemblyName System.Security
1560
+ $path = $env:DSH_CODEX_TOKEN_PATH
1561
+ if (-not [IO.File]::Exists($path)) { exit 3 }
1562
+ $cipher = [IO.File]::ReadAllBytes($path)
1563
+ $bytes = [Security.Cryptography.ProtectedData]::Unprotect($cipher, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
1564
+ [Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))
1565
+ `;
1566
+ const CLEAR_SCRIPT = String.raw`
1567
+ $ErrorActionPreference = 'Stop'
1568
+ $path = $env:DSH_CODEX_TOKEN_PATH
1569
+ if ([IO.File]::Exists($path)) { [IO.File]::Delete($path) }
1570
+ `;
1571
+ function defaultDpapiCredentialPath() {
1572
+ return join(process.env.DSH_HOME?.trim() || join(homedir(), ".dsh"), "storages", "dsh-chatgpt-subscription", "oauth.dpapi");
1573
+ }
1574
+ var WindowsDpapiCredentialStore = class {
1575
+ path;
1576
+ parse;
1577
+ storage = {
1578
+ kind: "windows-dpapi",
1579
+ encrypted: true
1580
+ };
1581
+ /**
1582
+ * Decrypted snapshot, validated against the file's own mtime and size.
1583
+ *
1584
+ * Every uncached read is a spawned `powershell.exe` (~200 ms), and one
1585
+ * settings request reads several credentials, so the snapshot is what keeps a
1586
+ * tab switch from costing seconds.
1587
+ */
1588
+ cache = new CredentialReadCache(() => fileCredentialIdentity(this.path));
1589
+ constructor(path, parse) {
1590
+ this.path = path;
1591
+ this.parse = parse;
1592
+ if (process.platform !== "win32") throw new Error("Windows DPAPI storage requires Windows");
1593
+ if (dirname(path) === path) throw new Error("invalid DPAPI credential path");
1594
+ }
1595
+ async load() {
1596
+ return this.cache.load(async () => {
1597
+ const result = await runPowerShell(UNPROTECT_SCRIPT, this.path, "");
1598
+ if (result.code === 3) return null;
1599
+ if (result.code !== 0) throw new Error("DPAPI credential read failed");
1600
+ try {
1601
+ return this.parse(JSON.parse(result.stdout));
1602
+ } catch {
1603
+ throw new CorruptStoredDocumentError("DPAPI credential payload is invalid");
1604
+ }
1605
+ });
1606
+ }
1607
+ async save(value) {
1608
+ if ((await runPowerShell(PROTECT_SCRIPT, this.path, JSON.stringify(value))).code !== 0) throw new Error("DPAPI credential write failed");
1609
+ this.cache.invalidate();
1610
+ }
1611
+ async clear() {
1612
+ if ((await runPowerShell(CLEAR_SCRIPT, this.path, "")).code !== 0) throw new Error("DPAPI credential deletion failed");
1613
+ this.cache.invalidate();
1614
+ }
1615
+ };
1616
+ var WindowsDpapiTokenStore = class extends WindowsDpapiCredentialStore {
1617
+ constructor(path = defaultDpapiCredentialPath()) {
1618
+ super(path, parseStoredCredentials);
1619
+ }
1620
+ };
1621
+ /**
1622
+ * How long a helper may run before it is killed and the read fails.
1623
+ *
1624
+ * A healthy spawn costs ~200 ms (see the cache note on this class), so the guard
1625
+ * exists only for a child that never exits, and it was set at 10 s - fifty times
1626
+ * the healthy cost. That turned out to be too tight for one real environment: a
1627
+ * cold Windows runner whose freshly-installed dependency tree is being scanned
1628
+ * on first spawn, where the helper legitimately took longer than 10 s and the
1629
+ * kill turned a working credential read into `DPAPI helper timed out`.
1630
+ *
1631
+ * The guard is kept, and only its headroom widened: 30 s is still bounded, still
1632
+ * far above anything a warm machine needs, and a hung helper now blocks one
1633
+ * credential read for 30 s rather than 10 - a worse wait in a case that is already
1634
+ * a failure, in exchange for not failing a machine that is merely cold.
1635
+ */
1636
+ const POWERSHELL_HELPER_TIMEOUT_MS = 3e4;
1637
+ function runPowerShell(script, path, stdin) {
1638
+ return new Promise((resolve, reject) => {
1639
+ const child = spawn("powershell.exe", [
1640
+ "-NoLogo",
1641
+ "-NoProfile",
1642
+ "-NonInteractive",
1643
+ "-Command",
1644
+ script
1645
+ ], {
1646
+ env: {
1647
+ ...process.env,
1648
+ DSH_CODEX_TOKEN_PATH: path
1649
+ },
1650
+ stdio: [
1651
+ "pipe",
1652
+ "pipe",
1653
+ "pipe"
1654
+ ],
1655
+ windowsHide: true
1656
+ });
1657
+ let stdout = "";
1658
+ let stderrLength = 0;
1659
+ const timer = setTimeout(() => {
1660
+ child.kill();
1661
+ reject(/* @__PURE__ */ new Error("DPAPI helper timed out"));
1662
+ }, POWERSHELL_HELPER_TIMEOUT_MS);
1663
+ child.stdout.setEncoding("utf8");
1664
+ child.stdout.on("data", (chunk) => {
1665
+ stdout += chunk;
1666
+ if (stdout.length > 1 << 20) child.kill();
1667
+ });
1668
+ child.stderr.on("data", (chunk) => {
1669
+ stderrLength += chunk.length;
1670
+ if (stderrLength > 1 << 20) child.kill();
1671
+ });
1672
+ child.once("error", (error) => {
1673
+ clearTimeout(timer);
1674
+ reject(error);
1675
+ });
1676
+ child.once("close", (code) => {
1677
+ clearTimeout(timer);
1678
+ resolve({
1679
+ code: code ?? 1,
1680
+ stdout
1681
+ });
1682
+ });
1683
+ child.stdin.end(stdin);
1684
+ });
1685
+ }
1686
+ //#endregion
1687
+ //#region src/host/common/credentials-compat.ts
1688
+ /**
1689
+ * One `<scope>/<id>` segment, exactly as the seam's own `credentialKey` requires.
1690
+ *
1691
+ * Copied from the shipped builder (`@deepseek-ai/dsh-credentials`,
1692
+ * `KEY_SEGMENT_PATTERN`) rather than invented: a key that this plugin builds and
1693
+ * the seam rejects is worse than no seam at all — the write succeeds, the record
1694
+ * document stops parsing on the next start, and every credential on the host
1695
+ * goes with it.
1696
+ */
1697
+ const KEY_SEGMENT_PATTERN = /^[a-z][a-z0-9-]*$/;
1698
+ /** Whether one segment of a record key could be handed to the seam at all. */
1699
+ function isCredentialKeySegment(value) {
1700
+ return KEY_SEGMENT_PATTERN.test(value);
1701
+ }
1702
+ /**
1703
+ * The seam, when `ctx.get('credentials')` returned something that is one.
1704
+ *
1705
+ * Takes the resolved SERVICE rather than the context: on a Cordis context a
1706
+ * service outside `inject` may only be read through `ctx.get`, so the lookup
1707
+ * belongs to the plugin entry (`src/index.ts`) and this function only decides
1708
+ * whether what came back is the record half the pool can use.
1709
+ *
1710
+ * Verified by shape, not by `!== undefined`: a host may carry a name that is not
1711
+ * this seam, and a service answering half the calls would fail in the middle of a
1712
+ * save — after the token exchange, which is exactly the failure the `poolStorage`
1713
+ * option exists to remove.
1714
+ */
1715
+ function asCredentialsSeam(service) {
1716
+ if (typeof service !== "object" || service === null) return void 0;
1717
+ const candidate = service;
1718
+ if (typeof candidate.readRecord !== "function" || typeof candidate.modifyRecord !== "function" || typeof candidate.deleteRecord !== "function") return;
1719
+ return candidate;
1720
+ }
1721
+ /**
1722
+ * Address of one pool document inside the seam's record space.
1723
+ *
1724
+ * Scoped by plugin rather than by provider line: a record's payload is written in
1725
+ * its owner's format, so two lines of this plugin may share the scope and stay
1726
+ * apart by id.
1727
+ *
1728
+ * Both segments are validated HERE, by the seam's own rule, because the seam's
1729
+ * write path does not validate them: a key it would reject on the next start
1730
+ * (`parseCredentialKey` runs in `parseRecords`, and an invalid one fails the
1731
+ * provider's activation) is a key that must never be written at all. A caller
1732
+ * that cannot build a legal id gets a `TypeError` now, in the line that named it,
1733
+ * instead of a credential document that stops loading after a restart.
1734
+ */
1735
+ function credentialsRecordKey(scope, id) {
1736
+ for (const segment of [scope, id]) if (!isCredentialKeySegment(segment)) throw new TypeError(`credential key segment "${segment}" must match ${String(KEY_SEGMENT_PATTERN)}`);
1737
+ return `${scope}/${id}`;
1738
+ }
1739
+ //#endregion
1740
+ //#region src/host/common/pool-storage.ts
1741
+ /**
1742
+ * Which store holds a provider line's account pool.
1743
+ *
1744
+ * The pool document is one JSON object per line. The store used to be decided by
1745
+ * `process.platform` alone: DPAPI on Windows, Keychain on macOS, and on Linux the
1746
+ * Secret Service — which a headless server does not have. There the token exchange
1747
+ * succeeded, saving failed, and the account could not be added at all; the only
1748
+ * workaround was running `gnome-keyring` unlocked with an empty password, which
1749
+ * protects the keyring file by permissions and nothing else.
1750
+ *
1751
+ * So the store becomes a choice with exactly two values:
1752
+ *
1753
+ * - `keyring` (default) — the platform store: DPAPI on Windows, the Keychain on
1754
+ * macOS, the Secret Service on Linux. Every existing install has this, and
1755
+ * nobody who never touches the setting sees a change.
1756
+ * - `credentials` — DSH's own credential store, through `ctx.credentials`. The
1757
+ * option a server install should use: no external daemon, one store for every
1758
+ * secret the host holds.
1759
+ *
1760
+ * There is deliberately NO automatic mode. An `auto` that picks by probing would
1761
+ * let one pool move between two stores depending on whether a daemon happened to
1762
+ * answer, and a pool that moves looks exactly like a pool whose accounts were
1763
+ * lost. A wrong choice is reported instead — the card names the store in use, says
1764
+ * why the configured one is unusable and offers the switch — and the accounts that
1765
+ * stayed behind in the other store are still there.
1766
+ */
1767
+ /** Store a deployment gets when no `poolStorage` is configured anywhere. */
1768
+ const DEFAULT_POOL_STORAGE_MODE = "keyring";
1769
+ /** Record scope the pool document occupies in DSH's credential store. */
1770
+ const POOL_RECORD_SCOPE = "dsh-chatgpt-subscription";
1771
+ /**
1772
+ * Record id prefix for one pool: `<scope>/pool-<digest>`.
1773
+ *
1774
+ * The prefix is load-bearing, not decoration. The seam requires each key segment
1775
+ * to match `/^[a-z][a-z0-9-]*$/` while a pool's id is a sha256 hex digest, and
1776
+ * 62.5% of digests start with a digit — those keys are rejected on the NEXT start,
1777
+ * when the record document is parsed, and an invalid key fails the whole provider's
1778
+ * activation: the DeepSeek key, the Telegram token and every other secret in
1779
+ * `$DSH_HOME/.credentials.yaml` become unreadable. `pool-` puts a letter first and
1780
+ * leaves the digest untouched, so the address stays stable and the Secret Service
1781
+ * entry (which uses the bare digest as its account attribute) is unaffected.
1782
+ */
1783
+ function poolRecordKey(account) {
1784
+ return credentialsRecordKey(POOL_RECORD_SCOPE, `pool-${account}`);
1785
+ }
1786
+ let defaults = { mode: () => DEFAULT_POOL_STORAGE_MODE };
1787
+ /**
1788
+ * Revision of the deployment-wide choice.
1789
+ *
1790
+ * Every resolved pool remembers the revision it resolved under and re-resolves
1791
+ * when this moves. Without it, switching the store in the settings card would keep
1792
+ * serving the old backend until the host restarted: the resolution is deliberately
1793
+ * cached — the decision must not be retaken per request — so the switch has to
1794
+ * invalidate it explicitly.
1795
+ */
1796
+ let revision = 0;
1797
+ /** Record the deployment-wide storage choice; call once, before any pool is built. */
1798
+ function configurePoolStorage(next) {
1799
+ defaults = next.mode === void 0 ? {
1800
+ ...next,
1801
+ mode: () => DEFAULT_POOL_STORAGE_MODE
1802
+ } : next;
1803
+ revision += 1;
1804
+ }
1805
+ /**
1806
+ * Announce that the stored choice changed.
1807
+ *
1808
+ * Called by the settings route after the new value is on disk; every pool drops
1809
+ * the backend it cached and resolves again on its next read.
1810
+ */
1811
+ function poolStorageChanged() {
1812
+ revision += 1;
1813
+ }
1814
+ /** The revision the current choice stands at. */
1815
+ function poolStorageRevision() {
1816
+ return revision;
1817
+ }
1818
+ const BACKEND_LABELS = {
1819
+ keyring: "desktop keyring (Secret Service)",
1820
+ credentials: "DSH credential store",
1821
+ dpapi: "Windows DPAPI",
1822
+ keychain: "macOS Keychain"
1823
+ };
1824
+ /** Whether the backend encrypts the document at rest. */
1825
+ function poolStorageEncrypted(backend) {
1826
+ return backend === "keyring" || backend === "dpapi" || backend === "keychain";
1827
+ }
1828
+ /**
1829
+ * The pool document as a record in DSH's own credential store.
1830
+ *
1831
+ * Stored through the seam's record half (see `credentials-compat.ts`): the
1832
+ * document is this plugin's own format, and a `grant` payload is exactly the slot
1833
+ * the seam keeps for a value only its owner can interpret.
1834
+ *
1835
+ * Writes are LAST-WRITER-WINS, and honestly so. `modifyRecord` is the seam's only
1836
+ * write path and it does hand back the record as it stands under its lock, but a
1837
+ * whole pool document is written from one in-memory copy: a second process that
1838
+ * saved between this process's read and this write is overwritten. The lock makes
1839
+ * the write atomic, not the transaction. This is the same guarantee the keyring
1840
+ * backend has always given, and exactly what the `keyring` option gives — one
1841
+ * process is the normal case (one `DSH_HOME`, one host), and it is stated here
1842
+ * rather than implied away.
1843
+ */
1844
+ var CredentialsRecordStore = class {
1845
+ seam;
1846
+ key;
1847
+ parse;
1848
+ constructor(seam, key, parse) {
1849
+ this.seam = seam;
1850
+ this.key = key;
1851
+ this.parse = parse;
1852
+ }
1853
+ /** The `<scope>/<id>` address this pool occupies; a location, never a credential. */
1854
+ recordKey() {
1855
+ return this.key;
1856
+ }
1857
+ async load() {
1858
+ const record = await this.seam.readRecord(this.key);
1859
+ if (record === void 0) return null;
1860
+ if (record.kind !== "grant") throw new CorruptStoredDocumentError("pool record is not a grant");
1861
+ try {
1862
+ return this.parse(record.payload);
1863
+ } catch (error) {
1864
+ throw new CorruptStoredDocumentError("DSH credential store pool payload is invalid", { cause: error });
1865
+ }
1866
+ }
1867
+ async save(value) {
1868
+ const payload = JSON.parse(JSON.stringify(value));
1869
+ await this.seam.modifyRecord(this.key, async () => ({
1870
+ kind: "grant",
1871
+ payload
1872
+ }));
1873
+ }
1874
+ async clear() {
1875
+ await this.seam.deleteRecord(this.key);
1876
+ }
1877
+ };
1878
+ /**
1879
+ * A backend that cannot be used on this host, kept so the failure is reported with
1880
+ * its reason instead of being replaced by a silent substitution.
1881
+ *
1882
+ * `poolStorage` is an instruction. Answering it with a different store would make
1883
+ * the settings card describe a backend the user did not choose, and — worse —
1884
+ * would spread one deployment's accounts across two stores without saying so.
1885
+ * Every operation therefore fails with the sentence the card also shows, and
1886
+ * `unavailable` tells the card which reason code to translate.
1887
+ */
1888
+ var UnusablePoolStore = class {
1889
+ message;
1890
+ constructor(message) {
1891
+ this.message = message;
1892
+ }
1893
+ async load() {
1894
+ throw new Error(this.message);
1895
+ }
1896
+ async save() {
1897
+ throw new Error(this.message);
1898
+ }
1899
+ async clear() {
1900
+ throw new Error(this.message);
1901
+ }
1902
+ };
1903
+ /** What the user is told when `keyring` is configured and there is no keyring. */
1904
+ const KEYRING_UNREACHABLE = "poolStorage is \"keyring\", but this host has no reachable desktop keyring: the Secret Service does not answer (no secret-tool, no session bus, or a locked keyring). Switch \"Account storage\" to the DSH credential store, or set poolStorage: \"credentials\".";
1905
+ /** What the user is told when `credentials` is configured and the harness has no such seam. */
1906
+ const NO_CREDENTIALS_SERVICE = "poolStorage is \"credentials\", but this harness has no ctx.credentials service, so there is no DSH credential store to write into. Switch \"Account storage\" to the desktop keyring, or set poolStorage: \"keyring\".";
1907
+ /**
1908
+ * Resolve the pool store for one line.
1909
+ *
1910
+ * Async because `credentials` availability is a runtime fact and the configured
1911
+ * value may still be loading from disk; callers hold the promise rather than the
1912
+ * store (see `AccountPoolCore`).
1913
+ */
1914
+ async function resolvePoolStorage(request) {
1915
+ const platform = request.platform ?? process.platform;
1916
+ const logger = request.logger ?? defaults.logger ?? console;
1917
+ await defaults.ready?.();
1918
+ const mode = request.mode ?? defaults.mode?.() ?? "keyring";
1919
+ const seam = request.credentials ?? defaults.credentials?.();
1920
+ if (mode === "credentials") {
1921
+ if (seam === void 0) {
1922
+ logger.warn(`[dsh-chatgpt-subscription] ${NO_CREDENTIALS_SERVICE}`);
1923
+ return unusable(mode, "credentials", "no-credentials-service", NO_CREDENTIALS_SERVICE);
1924
+ }
1925
+ const store = new CredentialsRecordStore(seam, poolRecordKey(request.account), request.parsePoolData);
1926
+ logger.info(`[dsh-chatgpt-subscription] account pool stored in ${BACKEND_LABELS.credentials}`);
1927
+ return resolved(store, mode, "credentials");
1928
+ }
1929
+ if (platform === "win32" || platform === "darwin") {
1930
+ const backend = platform === "win32" ? "dpapi" : "keychain";
1931
+ const store = request.desktopStore ?? (platform === "win32" ? new WindowsDpapiCredentialStore(`${request.poolFile}.dpapi`, request.parsePoolData) : new MacKeychainCredentialStore(request.keychainService, request.account, request.parsePoolData));
1932
+ logger.info(`[dsh-chatgpt-subscription] account pool stored in ${BACKEND_LABELS[backend]}`);
1933
+ return resolved(store, mode, backend);
1934
+ }
1935
+ if (platform !== "linux") throw new Error(`${request.displayName} pool storage requires Windows, macOS, or Linux.`);
1936
+ const keyring = request.keyring ?? new SecretServiceCredentialStore(request.keychainService, request.account, request.parsePoolData);
1937
+ const availability = await probeKeyring(keyring);
1938
+ if (!availability.available) {
1939
+ const reason = availability.reason === void 0 ? KEYRING_UNREACHABLE : `${KEYRING_UNREACHABLE} (${availability.reason})`;
1940
+ logger.warn(`[dsh-chatgpt-subscription] ${reason}`);
1941
+ return unusable(mode, "keyring", "keyring-unreachable", reason);
1942
+ }
1943
+ logger.info(`[dsh-chatgpt-subscription] account pool stored in ${BACKEND_LABELS.keyring}`);
1944
+ return resolved(keyring, mode, "keyring");
1945
+ }
1946
+ /**
1947
+ * The store a caller-supplied backend stands in for, on this platform.
1948
+ *
1949
+ * `AccountPoolHooks.backend` replaces the STORE, not the choice, and adoption
1950
+ * harnesses use it without a `storage` to go with it. Falling back to
1951
+ * `{ backend: 'keyring', encrypted: false }` there described a backend that cannot
1952
+ * exist (the platform keyring is encrypted on every platform it exists on) and the
1953
+ * wrong one on Windows or macOS.
1954
+ */
1955
+ function poolStorageBackendForPlatform(platform) {
1956
+ if (platform === "win32") return "dpapi";
1957
+ if (platform === "darwin") return "keychain";
1958
+ return "keyring";
1959
+ }
1960
+ function resolved(store, mode, backend) {
1961
+ return {
1962
+ store,
1963
+ storage: {
1964
+ mode,
1965
+ backend,
1966
+ encrypted: poolStorageEncrypted(backend)
1967
+ }
1968
+ };
1969
+ }
1970
+ function unusable(mode, backend, unavailable, message) {
1971
+ return {
1972
+ store: new UnusablePoolStore(`ChatGPT account pool storage is unavailable: ${message}`),
1973
+ storage: {
1974
+ mode,
1975
+ backend,
1976
+ encrypted: poolStorageEncrypted(backend),
1977
+ unavailable
1978
+ }
1979
+ };
1980
+ }
1981
+ /** Whether the keyring answers at all; a backend without a probe is assumed to. */
1982
+ async function probeKeyring(keyring) {
1983
+ if (typeof keyring.ping !== "function") return { available: true };
1984
+ return keyring.ping();
1985
+ }
1986
+ /**
1987
+ * Human description of where a resolved pool lives, for diagnostics.
1988
+ *
1989
+ * A location, never a credential: the sentence the pool printed before, now
1990
+ * derived from the backend that actually won.
1991
+ */
1992
+ function describePoolStoragePath(platform, poolFile, storage, keychainService, account) {
1993
+ if (storage.backend === "dpapi") return `${poolFile}.dpapi`;
1994
+ if (storage.backend === "credentials") return `credentials://${poolRecordKey(account)}`;
1995
+ return `${platform === "darwin" ? "Keychain" : "Secret Service"}: ${keychainService}/${account}`;
1996
+ }
1585
1997
  /** Shortest cooldown a 429 may impose; a provider asking for less is clamped. */
1586
1998
  const MIN_COOLDOWN_MS = 1e4;
1587
- /** Stable keychain account name for one pool file, isolated per `DSH_HOME`. */
1588
1999
  /** Accept a stored rotation strategy, or fall back to sequential. */
1589
2000
  function normalizeRotationStrategy(value) {
1590
2001
  return value === "round-robin" || value === "sticky" ? value : "sequential";
1591
2002
  }
1592
- function poolCredentialAccount$1(filePath) {
2003
+ /**
2004
+ * Stable keychain account name for one pool file, isolated per `DSH_HOME`.
2005
+ *
2006
+ * A bare sha256 hex digest: that is what the Secret Service entry, the DPAPI file
2007
+ * suffix and the Keychain account all address, and what survives a restart. It is
2008
+ * NOT a legal credential-record id on its own — see `poolRecordKey` in
2009
+ * `pool-storage.ts`, which prefixes it — so nothing may hand this value to the
2010
+ * `ctx.credentials` seam directly.
2011
+ */
2012
+ function poolCredentialAccount(filePath) {
1593
2013
  return createHash("sha256").update(path.resolve(filePath)).digest("hex");
1594
2014
  }
1595
2015
  const poolOperations = /* @__PURE__ */ new Map();
@@ -1614,7 +2034,11 @@ var PoolAccountUnavailableError = class extends LlmError {};
1614
2034
  var AccountPoolCore = class {
1615
2035
  hooks;
1616
2036
  filePath;
1617
- backend;
2037
+ /** The resolved backend, and the promise that resolves it (see {@link backendStore}). */
2038
+ backendRef = {};
2039
+ backendPromise;
2040
+ /** Choice revision {@link backendPromise} was resolved under. */
2041
+ backendRevision = -1;
1618
2042
  /**
1619
2043
  * Bumped by every write that can move which account serves a request.
1620
2044
  *
@@ -1629,7 +2053,6 @@ var AccountPoolCore = class {
1629
2053
  constructor(hooks) {
1630
2054
  this.hooks = hooks;
1631
2055
  this.filePath = hooks.poolFile;
1632
- this.backend = hooks.backend ?? createPoolBackend(hooks);
1633
2056
  const resolved = path.resolve(this.filePath);
1634
2057
  this.operationKey = process.platform === "win32" ? resolved.toLowerCase() : resolved;
1635
2058
  }
@@ -1649,10 +2072,71 @@ var AccountPoolCore = class {
1649
2072
  currentIdentityRevision() {
1650
2073
  return poolIdentityRevisions.get(this.operationKey) ?? 0;
1651
2074
  }
1652
- /** Human description of where this pool's credentials live. */
2075
+ /**
2076
+ * Human description of where this pool's credentials live.
2077
+ *
2078
+ * Derived from the backend that was resolved, not from the platform: on a
2079
+ * server that answered `poolStorage` with the credential store or a file, the
2080
+ * old sentence named a keyring entry that does not exist.
2081
+ */
1653
2082
  path() {
1654
- if (process.platform === "win32") return `${this.filePath}.dpapi`;
1655
- return `${process.platform === "darwin" ? "Keychain" : "Secret Service"}: ${this.hooks.keychainService}/${poolCredentialAccount$1(this.filePath)}`;
2083
+ const selected = this.backendRef.current;
2084
+ return selected === void 0 ? defaultPoolStoragePath(process.platform, this.filePath, this.hooks.keychainService) : describePoolStoragePath(process.platform, this.filePath, selected.storage, this.hooks.keychainService, poolCredentialAccount(this.filePath));
2085
+ }
2086
+ /**
2087
+ * The backend holding this pool, and why it was chosen, for the settings surface.
2088
+ *
2089
+ * Resolves the store on first call, exactly as a read does: a card opened before
2090
+ * any request has to be able to answer "where are my accounts?" as well.
2091
+ */
2092
+ async poolStorage() {
2093
+ return (await this.backendStore()).storage;
2094
+ }
2095
+ /**
2096
+ * The pool store: resolved once, and re-resolved only when the setting changes.
2097
+ *
2098
+ * Lazy because the choice is a question about the host — does the keyring answer
2099
+ * — and a pool is built from a constructor, which cannot await. CACHED for the
2100
+ * life of the process on purpose: a read that re-detected the store on every
2101
+ * model request would pay a `secret-tool` launch per request, and could hand two
2102
+ * requests of one turn two different stores. The only thing that invalidates the
2103
+ * cache is {@link poolStorageRevision} moving, which happens when the settings
2104
+ * card writes a new choice.
2105
+ */
2106
+ backendStore() {
2107
+ const revision = poolStorageRevision();
2108
+ if (this.backendPromise === void 0 || this.backendRevision !== revision) {
2109
+ this.backendRevision = revision;
2110
+ this.backendPromise = this.resolveBackend();
2111
+ }
2112
+ return this.backendPromise;
2113
+ }
2114
+ async resolveBackend() {
2115
+ if (this.hooks.backend !== void 0) {
2116
+ const backend = poolStorageBackendForPlatform(this.hooks.platform ?? process.platform);
2117
+ const resolution = {
2118
+ store: this.hooks.backend,
2119
+ storage: this.hooks.storage ?? {
2120
+ mode: this.hooks.poolStorage ?? "keyring",
2121
+ backend,
2122
+ encrypted: poolStorageEncrypted(backend)
2123
+ }
2124
+ };
2125
+ this.backendRef.current = resolution;
2126
+ return resolution;
2127
+ }
2128
+ const resolution = await resolvePoolStorage({
2129
+ displayName: this.hooks.displayName,
2130
+ ...this.hooks.poolStorage === void 0 ? {} : { mode: this.hooks.poolStorage },
2131
+ poolFile: this.filePath,
2132
+ keychainService: this.hooks.keychainService,
2133
+ account: poolCredentialAccount(this.filePath),
2134
+ parsePoolData: this.hooks.parsePoolData,
2135
+ ...this.hooks.platform === void 0 ? {} : { platform: this.hooks.platform },
2136
+ ...this.hooks.keyring === void 0 ? {} : { keyring: this.hooks.keyring }
2137
+ });
2138
+ this.backendRef.current = resolution;
2139
+ return resolution;
1656
2140
  }
1657
2141
  /**
1658
2142
  * Queue one storage transaction for this pool file.
@@ -1672,8 +2156,12 @@ var AccountPoolCore = class {
1672
2156
  }
1673
2157
  async saveVerified(data) {
1674
2158
  const normalized = this.hooks.parsePoolData(data);
1675
- await this.backend.save(normalized);
1676
- if (!isDeepStrictEqual(await this.backend.load(), normalized)) throw new Error(`${this.hooks.displayName} pool encrypted verification failed`);
2159
+ const { store } = await this.backendStore();
2160
+ await store.load().catch((error) => {
2161
+ if (!isCorruptStoredDocument(error)) throw error;
2162
+ });
2163
+ await store.save(normalized);
2164
+ if (!isDeepStrictEqual(await store.load(), normalized)) throw new Error(`${this.hooks.displayName} pool storage verification failed`);
1677
2165
  }
1678
2166
  /**
1679
2167
  * Read the pool.
@@ -1695,9 +2183,13 @@ var AccountPoolCore = class {
1695
2183
  */
1696
2184
  async loadPoolData() {
1697
2185
  let current = null;
2186
+ let resolution;
1698
2187
  try {
1699
- current = await this.backend.load();
1700
- } catch {}
2188
+ resolution = await this.backendStore();
2189
+ current = await resolution.store.load();
2190
+ } catch (error) {
2191
+ if (isCorruptStoredDocument(error)) {} else if (resolution?.storage.unavailable === void 0) throw error;
2192
+ }
1701
2193
  if (current !== null && Array.isArray(current.accounts) && current.accounts.length > 0) return current;
1702
2194
  try {
1703
2195
  const legacy = await this.hooks.legacyAccount?.();
@@ -2203,13 +2695,16 @@ var AccountPoolCore = class {
2203
2695
  };
2204
2696
  }
2205
2697
  };
2206
- /** Build the encrypted platform backend one pool file uses. */
2207
- function createPoolBackend(hooks) {
2208
- const filePath = hooks.poolFile;
2209
- if (process.platform === "win32") return new WindowsDpapiCredentialStore(`${filePath}.dpapi`, hooks.parsePoolData);
2210
- if (process.platform === "darwin") return new MacKeychainCredentialStore(hooks.keychainService, poolCredentialAccount$1(filePath), hooks.parsePoolData);
2211
- if (process.platform === "linux") return new SecretServiceCredentialStore(hooks.keychainService, poolCredentialAccount$1(filePath), hooks.parsePoolData);
2212
- throw new Error(`${hooks.displayName} pool encrypted storage requires Windows, macOS, or Linux.`);
2698
+ /**
2699
+ * Where the pool would live on this platform before anything is resolved.
2700
+ *
2701
+ * Only used for the diagnostic sentence while the backend is still being
2702
+ * resolved; {@link AccountPoolCore.path} switches to the real answer afterwards.
2703
+ */
2704
+ function defaultPoolStoragePath(platform, filePath, keychainService) {
2705
+ if (platform === "win32") return `${filePath}.dpapi`;
2706
+ if (platform === "darwin") return `Keychain: ${keychainService}/${poolCredentialAccount(filePath)}`;
2707
+ return `Secret Service: ${keychainService}/${poolCredentialAccount(filePath)}`;
2213
2708
  }
2214
2709
  //#endregion
2215
2710
  //#region src/host/token-store-linux.ts
@@ -2252,7 +2747,7 @@ var LinuxFileTokenStore = class {
2252
2747
  const payload = await handle.readFile({ encoding: "utf8" });
2253
2748
  return parseStoredCredentials(JSON.parse(payload));
2254
2749
  } catch (error) {
2255
- throw new Error("Linux credential payload is invalid or insecure", { cause: error });
2750
+ throw new CorruptStoredDocumentError("Linux credential payload is invalid or insecure", { cause: error });
2256
2751
  } finally {
2257
2752
  await handle.close();
2258
2753
  }
@@ -2546,6 +3041,18 @@ var OAuthService = class {
2546
3041
  loginEvents = /* @__PURE__ */ new Map();
2547
3042
  listeners = /* @__PURE__ */ new Map();
2548
3043
  activeLogin = null;
3044
+ /**
3045
+ * True between taking the login slot and having something to put in
3046
+ * {@link activeLogin}.
3047
+ *
3048
+ * Both starts have work to do before the login exists (a storage read, the
3049
+ * device grant), and that work is awaited. A check made before the first
3050
+ * `await` and an assignment made after it do not exclude each other: two
3051
+ * overlapping calls both pass the check, the second overwrites the first, and
3052
+ * the first flow's `AbortController` becomes unreachable — Cancel stops the
3053
+ * wrong flow and the abandoned one keeps polling until its grant expires.
3054
+ */
3055
+ startPending = false;
2549
3056
  refreshPromise = null;
2550
3057
  lastLoginError;
2551
3058
  disposed = false;
@@ -2593,19 +3100,55 @@ var OAuthService = class {
2593
3100
  accounts: [],
2594
3101
  rotationStrategy: "sequential"
2595
3102
  };
2596
- const [accounts, data] = await Promise.all([pool.listAccounts().catch(() => []), pool.read().catch(() => null)]);
3103
+ let readFailed = false;
3104
+ const [accounts, data, poolStorage] = await Promise.all([
3105
+ pool.listAccounts().catch(() => {
3106
+ readFailed = true;
3107
+ return [];
3108
+ }),
3109
+ pool.read().catch(() => {
3110
+ readFailed = true;
3111
+ return null;
3112
+ }),
3113
+ pool.poolStorage().catch(() => void 0)
3114
+ ]);
3115
+ const reason = readFailed && poolStorage !== void 0 && poolStorage.unavailable === void 0 ? {
3116
+ ...poolStorage,
3117
+ unavailable: "store-unreadable"
3118
+ } : poolStorage;
2597
3119
  return {
2598
3120
  accounts,
2599
3121
  ...data?.activeAccountId === void 0 ? {} : { activeAccountId: data.activeAccountId },
2600
- rotationStrategy: data?.rotationStrategy ?? "sequential"
3122
+ rotationStrategy: data?.rotationStrategy ?? "sequential",
3123
+ ...reason === void 0 ? {} : { poolStorage: reason }
2601
3124
  };
2602
3125
  }
3126
+ /**
3127
+ * Take the one login slot, or say that it is taken.
3128
+ *
3129
+ * Called synchronously, before the first `await` of the start it belongs to:
3130
+ * nothing can suspend between the check and the flag, so two overlapping
3131
+ * starts cannot both get through. The caller's `finally` releases the flag once
3132
+ * the login itself is in {@link activeLogin} (or the start has failed).
3133
+ */
3134
+ beginStart() {
3135
+ if (this.startPending || this.activeLogin !== null) throw new OAuthServiceError("login-active", "A ChatGPT sign-in is already in progress.");
3136
+ this.startPending = true;
3137
+ }
2603
3138
  async startLogin() {
2604
3139
  this.assertAvailable();
3140
+ this.beginStart();
3141
+ try {
3142
+ return await this.startBrowserLogin();
3143
+ } finally {
3144
+ this.startPending = false;
3145
+ }
3146
+ }
3147
+ /** The body of {@link startLogin}, with the slot already taken. */
3148
+ async startBrowserLogin() {
2605
3149
  await this.store.load().catch(() => {
2606
3150
  throw new OAuthServiceError("storage-failed", "Secure credential storage is unavailable. Fix its ownership or permissions before signing in.");
2607
3151
  });
2608
- if (this.activeLogin !== null) throw new OAuthServiceError("login-active", "A ChatGPT sign-in is already in progress.");
2609
3152
  this.lastLoginError = void 0;
2610
3153
  const loginId = this.random(24).toString("base64url");
2611
3154
  const verifier = this.random(48).toString("base64url");
@@ -2629,8 +3172,10 @@ var OAuthService = class {
2629
3172
  this.activeLogin = {
2630
3173
  id: loginId,
2631
3174
  expiresAt,
2632
- server,
2633
- timeout
3175
+ stop: () => {
3176
+ clearTimeout(timeout);
3177
+ server.cancel(new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled."));
3178
+ }
2634
3179
  };
2635
3180
  this.publish({
2636
3181
  type: "pending",
@@ -2652,6 +3197,57 @@ var OAuthService = class {
2652
3197
  if (this.activeLogin === null || this.activeLogin.id !== loginId) throw new OAuthServiceError("bad-request", "The requested sign-in is not active.");
2653
3198
  this.cancelActive(new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled."), "cancelled");
2654
3199
  }
3200
+ /**
3201
+ * Start a device-code sign-in, the path a headless host can finish.
3202
+ *
3203
+ * Answers as soon as the grant exists, with the code the user has to type and
3204
+ * the page to type it on; the wait for confirmation runs in the background and
3205
+ * reports through the same event stream the browser flow uses, so the card
3206
+ * needs no second mechanism and cancellation is the same call.
3207
+ */
3208
+ async startDeviceLogin() {
3209
+ this.assertAvailable();
3210
+ this.beginStart();
3211
+ try {
3212
+ return await this.startDeviceLoginInner();
3213
+ } finally {
3214
+ this.startPending = false;
3215
+ }
3216
+ }
3217
+ /** The body of {@link startDeviceLogin}, with the slot already taken. */
3218
+ async startDeviceLoginInner() {
3219
+ await this.store.load().catch(() => {
3220
+ throw new OAuthServiceError("storage-failed", "Secure credential storage is unavailable. Fix its ownership or permissions before signing in.");
3221
+ });
3222
+ this.lastLoginError = void 0;
3223
+ const controller = new AbortController();
3224
+ const grant = await this.requestDeviceGrant(controller.signal);
3225
+ const loginId = this.random(24).toString("base64url");
3226
+ const device = {
3227
+ userCode: grant.userCode,
3228
+ verificationUrl: OAUTH_DEVICE_VERIFY_URL,
3229
+ expiresAt: Math.floor(grant.expiresAt / 1e3),
3230
+ now: Math.floor(this.now() / 1e3)
3231
+ };
3232
+ this.activeLogin = {
3233
+ id: loginId,
3234
+ expiresAt: grant.expiresAt,
3235
+ stop: () => controller.abort(),
3236
+ device
3237
+ };
3238
+ this.publish({
3239
+ type: "pending",
3240
+ loginId
3241
+ });
3242
+ this.pollDeviceGrant(loginId, device, grant, controller.signal).catch((error) => {
3243
+ this.failLogin(loginId, error);
3244
+ });
3245
+ this.logger.info("[dsh-chatgpt-subscription] device-code login started");
3246
+ return {
3247
+ loginId,
3248
+ ...device
3249
+ };
3250
+ }
2655
3251
  subscribe(loginId, listener) {
2656
3252
  const current = this.loginEvents.get(loginId);
2657
3253
  if (current === void 0) return null;
@@ -2764,14 +3360,14 @@ var OAuthService = class {
2764
3360
  this.listeners.clear();
2765
3361
  this.loginEvents.clear();
2766
3362
  }
2767
- async exchangeCode(code, verifier, signal) {
3363
+ async exchangeCode(code, verifier, signal, redirectUri = OAUTH_REDIRECT_URI) {
2768
3364
  const response = await this.fetchFn(OAUTH_TOKEN_URL$1, {
2769
3365
  method: "POST",
2770
3366
  headers: { "content-type": "application/x-www-form-urlencoded" },
2771
3367
  body: new URLSearchParams({
2772
3368
  grant_type: "authorization_code",
2773
3369
  code,
2774
- redirect_uri: OAUTH_REDIRECT_URI,
3370
+ redirect_uri: redirectUri,
2775
3371
  client_id: CHATGPT_OAUTH_CLIENT_ID,
2776
3372
  code_verifier: verifier
2777
3373
  }).toString(),
@@ -2784,6 +3380,20 @@ var OAuthService = class {
2784
3380
  throw new OAuthServiceError("oauth-token-exchange-failed", `ChatGPT token exchange failed (${response.status}${detail === null ? "" : `, ${detail}`}).`);
2785
3381
  }
2786
3382
  const credentials = credentialsFromTokenResponse(await response.json(), this.now());
3383
+ await this.persistCredentials(credentials, signal);
3384
+ }
3385
+ /**
3386
+ * Write freshly redeemed credentials into the pool, unless the sign-in was
3387
+ * cancelled while the token answer was on its way.
3388
+ *
3389
+ * The abort check used to live only in the poll loop, *after* the exchange, so
3390
+ * a Cancel landing between the token `200` and the write left the host having
3391
+ * answered `{cancelled:true}` and published `cancelled` while the pool quietly
3392
+ * gained an account. The check belongs at the last moment before the write —
3393
+ * the narrower the window, the fewer writes it misses.
3394
+ */
3395
+ async persistCredentials(credentials, signal) {
3396
+ if (signal.aborted) throw new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled.");
2787
3397
  if (this.pool !== null) {
2788
3398
  await this.pool.addAccount(credentials).catch(() => {
2789
3399
  throw new OAuthServiceError("storage-failed", "ChatGPT credentials could not be saved securely.");
@@ -2795,6 +3405,104 @@ var OAuthService = class {
2795
3405
  });
2796
3406
  this.credentialRevision += 1;
2797
3407
  }
3408
+ /**
3409
+ * Ask for a device code, and normalise what the grant says about itself.
3410
+ *
3411
+ * `interval` arrives as a *string* (`"5"`) on the live service and
3412
+ * `expires_at` as an ISO timestamp, so both are parsed rather than trusted to
3413
+ * be the type they should be. A grant that states neither still works: the
3414
+ * defaults are the values the service actually uses.
3415
+ */
3416
+ async requestDeviceGrant(signal) {
3417
+ const response = await this.fetchFn(OAUTH_DEVICE_USERCODE_URL, {
3418
+ method: "POST",
3419
+ headers: { "content-type": "application/json" },
3420
+ body: JSON.stringify({ client_id: CHATGPT_OAUTH_CLIENT_ID }),
3421
+ signal
3422
+ }).catch(() => {
3423
+ throw new OAuthServiceError("oauth-token-exchange-failed", "The ChatGPT device-code request could not be reached.");
3424
+ });
3425
+ if (!response.ok) {
3426
+ const detail = await oauthErrorIdentifier(response);
3427
+ throw new OAuthServiceError("oauth-token-exchange-failed", `The ChatGPT device code could not be issued (${response.status}${detail === null ? "" : `, ${detail}`}).`, response.status);
3428
+ }
3429
+ const payload = await response.json().catch(() => null);
3430
+ const record = isRecord$49(payload) ? payload : {};
3431
+ const authId = nonEmptyString$10(record.device_auth_id);
3432
+ const userCode = nonEmptyString$10(record.user_code) ?? nonEmptyString$10(record.usercode);
3433
+ if (authId === void 0 || userCode === void 0) throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT returned no device code.");
3434
+ return {
3435
+ authId,
3436
+ userCode,
3437
+ interval: parseGrantInterval(record.interval),
3438
+ expiresAt: parseGrantDeadline(record.expires_at, this.now())
3439
+ };
3440
+ }
3441
+ /**
3442
+ * Wait for the user to confirm the code, then redeem the grant.
3443
+ *
3444
+ * Three rules, and each one is a way this loop goes wrong if it is dropped:
3445
+ * the wait between polls is the grant's own `interval` (clamped, see
3446
+ * {@link pollDelayMs}); no single wait runs past the deadline; and nothing is
3447
+ * accepted at or after the deadline, because a grant that expired upstream is
3448
+ * not a sign-in — redeeming it would surface the server's error as if the
3449
+ * user's code had failed.
3450
+ */
3451
+ async pollDeviceGrant(loginId, device, grant, signal) {
3452
+ const deadline = grant.expiresAt;
3453
+ const interval = grant.interval;
3454
+ for (;;) {
3455
+ if (signal.aborted) return;
3456
+ const remaining = deadline - this.now();
3457
+ if (remaining <= 0) throw new OAuthServiceError("login-expired", "The device code expired before it was confirmed. Start the sign-in again for a fresh code.");
3458
+ await sleep$6(Math.min(pollDelayMs(interval, deadline - this.now()), remaining), signal);
3459
+ if (signal.aborted) return;
3460
+ if (this.now() >= deadline) throw new OAuthServiceError("login-expired", "The device code expired before it was confirmed. Start the sign-in again for a fresh code.");
3461
+ const outcome = await this.pollDeviceToken(grant, signal);
3462
+ if (outcome === "pending") continue;
3463
+ if (signal.aborted) return;
3464
+ await this.exchangeCode(outcome.authorizationCode, outcome.codeVerifier, signal, OAUTH_DEVICE_REDIRECT_URI);
3465
+ if (signal.aborted) return;
3466
+ this.completeLogin(loginId);
3467
+ return;
3468
+ }
3469
+ }
3470
+ /**
3471
+ * One confirmation check.
3472
+ *
3473
+ * `403`/`404` (and any payload whose status says pending) mean "not yet" — the
3474
+ * user has not typed the code or has not confirmed it. Everything else is
3475
+ * terminal and carries the status with any error code the service named, so
3476
+ * the card can say what happened instead of polling until the deadline.
3477
+ */
3478
+ async pollDeviceToken(grant, signal) {
3479
+ const response = await this.fetchFn(OAUTH_DEVICE_TOKEN_URL, {
3480
+ method: "POST",
3481
+ headers: { "content-type": "application/json" },
3482
+ body: JSON.stringify({
3483
+ device_auth_id: grant.authId,
3484
+ user_code: grant.userCode
3485
+ }),
3486
+ signal
3487
+ }).catch(() => {
3488
+ if (signal.aborted) throw new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled.");
3489
+ throw new OAuthServiceError("oauth-token-exchange-failed", "The ChatGPT device confirmation could not be reached.");
3490
+ });
3491
+ const payload = await response.json().catch(() => null);
3492
+ if (isPendingDeviceAnswer(response, payload)) return "pending";
3493
+ if (!response.ok) {
3494
+ const detail = deviceErrorIdentifier(payload);
3495
+ throw new OAuthServiceError("oauth-token-exchange-failed", `The ChatGPT device confirmation was refused (${response.status}${detail === void 0 ? "" : `, ${detail}`}).`, response.status);
3496
+ }
3497
+ const record = isRecord$49(payload) ? payload : {};
3498
+ const authorizationCode = nonEmptyString$10(record.authorization_code);
3499
+ const codeVerifier = nonEmptyString$10(record.code_verifier);
3500
+ if (authorizationCode === void 0 || codeVerifier === void 0) throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT confirmed the device code but returned no authorization code.");
3501
+ return {
3502
+ authorizationCode,
3503
+ codeVerifier
3504
+ };
3505
+ }
2798
3506
  refreshCredentials(stored) {
2799
3507
  if (this.refreshPromise !== null) return this.refreshPromise;
2800
3508
  this.refreshPromise = this.performRefresh(stored).finally(() => {
@@ -2881,11 +3589,7 @@ var OAuthService = class {
2881
3589
  ...this.store.storage,
2882
3590
  available: storageAvailable
2883
3591
  },
2884
- login: {
2885
- active: active !== null,
2886
- loginId: active?.id ?? null,
2887
- expiresAt: active === null ? null : Math.floor(active.expiresAt / 1e3)
2888
- },
3592
+ login: this.loginStatus(active),
2889
3593
  ...this.lastLoginError === void 0 ? {} : { error: this.lastLoginError }
2890
3594
  };
2891
3595
  const identity = extractIdentity(credentials);
@@ -2902,17 +3606,32 @@ var OAuthService = class {
2902
3606
  ...this.store.storage,
2903
3607
  available: storageAvailable
2904
3608
  },
2905
- login: {
2906
- active: active !== null,
2907
- loginId: active?.id ?? null,
2908
- expiresAt: active === null ? null : Math.floor(active.expiresAt / 1e3)
2909
- },
3609
+ login: this.loginStatus(active),
2910
3610
  ...this.lastLoginError === void 0 ? {} : { error: this.lastLoginError }
2911
3611
  };
2912
3612
  }
3613
+ /**
3614
+ * The `login` slice of the status, shared by both branches above.
3615
+ *
3616
+ * A device flow also carries its code: the card is the only place the user can
3617
+ * read it, and a settings page opened a second time (or reloaded) would
3618
+ * otherwise show "waiting" with nothing to type anywhere on it.
3619
+ */
3620
+ loginStatus(active) {
3621
+ const device = active?.device;
3622
+ return {
3623
+ active: active !== null,
3624
+ loginId: active?.id ?? null,
3625
+ expiresAt: active === null ? null : Math.floor(active.expiresAt / 1e3),
3626
+ ...device === void 0 ? {} : { device: {
3627
+ ...device,
3628
+ now: Math.floor(this.now() / 1e3)
3629
+ } }
3630
+ };
3631
+ }
2913
3632
  completeLogin(loginId) {
2914
3633
  if (this.activeLogin?.id !== loginId) return;
2915
- clearTimeout(this.activeLogin.timeout);
3634
+ this.activeLogin.stop();
2916
3635
  this.activeLogin = null;
2917
3636
  this.lastLoginError = void 0;
2918
3637
  this.publish({
@@ -2923,7 +3642,7 @@ var OAuthService = class {
2923
3642
  }
2924
3643
  failLogin(loginId, error) {
2925
3644
  if (this.activeLogin?.id !== loginId) return;
2926
- clearTimeout(this.activeLogin.timeout);
3645
+ this.activeLogin.stop();
2927
3646
  this.activeLogin = null;
2928
3647
  const mapped = publicError(error, "oauth-callback-invalid");
2929
3648
  this.lastLoginError = mapped;
@@ -2937,9 +3656,8 @@ var OAuthService = class {
2937
3656
  cancelActive(error, outcome) {
2938
3657
  const active = this.activeLogin;
2939
3658
  if (active === null) return;
2940
- clearTimeout(active.timeout);
2941
3659
  this.activeLogin = null;
2942
- active.server.cancel(error);
3660
+ active.stop();
2943
3661
  this.publish(outcome === "cancelled" ? {
2944
3662
  type: "cancelled",
2945
3663
  loginId: active.id
@@ -3042,6 +3760,119 @@ function maskAccountId(accountId) {
3042
3760
  if (accountId === void 0) return null;
3043
3761
  return `…${accountId.slice(-4)}`;
3044
3762
  }
3763
+ /**
3764
+ * How long to wait before the next confirmation check.
3765
+ *
3766
+ * Two clamps, for two different bad values. A floor keeps a fractional interval
3767
+ * (a grant answering `"0.2"`) from turning the loop into a spin against the
3768
+ * service — `"0"`, a negative and anything unparseable never reach it, because
3769
+ * {@link parseGrantInterval} already turns those into the 5 s default. The
3770
+ * deadline keeps an absurd interval from sleeping past the point where the grant
3771
+ * dies, which would leave the card "waiting" for a code that can no longer be
3772
+ * redeemed. The normal value — `"5"` — passes through untouched.
3773
+ */
3774
+ function pollDelayMs(intervalSeconds, remainingMs) {
3775
+ const requested = intervalSeconds * 1e3;
3776
+ return Math.max(OAUTH_DEVICE_MIN_POLL_INTERVAL_MS, Math.min(requested, Math.max(remainingMs, 0)));
3777
+ }
3778
+ /** The grant's `interval`, which arrives as a string on the live service. */
3779
+ function parseGrantInterval(value) {
3780
+ const seconds = typeof value === "number" ? value : Number(value);
3781
+ return Number.isFinite(seconds) && seconds > 0 ? seconds : 5;
3782
+ }
3783
+ /**
3784
+ * The grant's `expires_at`, which the live service sends as an ISO timestamp.
3785
+ *
3786
+ * A number is accepted too, and its magnitude decides the unit: a Unix timestamp
3787
+ * in seconds and one in milliseconds differ by three orders of magnitude, so
3788
+ * guessing is safe here in a way it is not for a field that could be either by a
3789
+ * factor of a thousand. Anything unparseable falls back to the grant lifetime
3790
+ * this client expects — the deadline still exists, it just is not the grant's.
3791
+ */
3792
+ function parseGrantDeadline(value, now) {
3793
+ if (typeof value === "string" && value !== "") {
3794
+ const parsed = Date.parse(value);
3795
+ if (Number.isFinite(parsed)) return parsed;
3796
+ }
3797
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value < 0xe8d4a51000 ? value * 1e3 : value;
3798
+ return now + OAUTH_DEVICE_GRANT_LIFETIME_MS;
3799
+ }
3800
+ /**
3801
+ * Wait `ms`, or less if the sign-in is cancelled first.
3802
+ *
3803
+ * Resolves rather than rejects on abort: the caller already knows it was
3804
+ * cancelled and checks the signal, so a rejected promise here would only add an
3805
+ * unhandled path that says the same thing a second time.
3806
+ */
3807
+ function sleep$6(ms, signal) {
3808
+ return new Promise((resolve) => {
3809
+ if (signal.aborted) {
3810
+ resolve();
3811
+ return;
3812
+ }
3813
+ const done = () => {
3814
+ clearTimeout(timer);
3815
+ signal.removeEventListener("abort", done);
3816
+ resolve();
3817
+ };
3818
+ const timer = setTimeout(done, ms);
3819
+ timer.unref?.();
3820
+ signal.addEventListener("abort", done, { once: true });
3821
+ });
3822
+ }
3823
+ /** Status values that mean "the user has not confirmed yet", not a failure. */
3824
+ const DEVICE_PENDING_STATUSES = /* @__PURE__ */ new Set([
3825
+ "pending",
3826
+ "authorization_pending",
3827
+ "deviceauth_pending",
3828
+ "not_found"
3829
+ ]);
3830
+ /**
3831
+ * Whether one confirmation check was the grant saying "not yet".
3832
+ *
3833
+ * Two ways to say it, and the second is deliberately narrow. The live service
3834
+ * answers `403`/`404` while waiting, and those statuses mean "not yet" whatever
3835
+ * the body says. A body that merely *looks* pending counts only on a status that
3836
+ * could carry it — a `200`, or the `400` some builds answer — because honouring it
3837
+ * everywhere let a `401`/`429`/`500` that happened to mention `status: "pending"`
3838
+ * retry an invalid user code until the deadline, hiding a reason the user could
3839
+ * have been told at once. Every other status is a failure, read by its code.
3840
+ */
3841
+ function isPendingDeviceAnswer(response, payload) {
3842
+ if (response.status === 403 || response.status === 404) return true;
3843
+ if (!response.ok && response.status !== 400) return false;
3844
+ if (!isRecord$49(payload)) return false;
3845
+ const status = payload.status;
3846
+ if (typeof status === "string" && DEVICE_PENDING_STATUSES.has(status.toLowerCase())) return true;
3847
+ const error = payload.error;
3848
+ if (isRecord$49(error)) {
3849
+ const code = error.code;
3850
+ if (typeof code === "string" && DEVICE_PENDING_STATUSES.has(code.toLowerCase())) return true;
3851
+ }
3852
+ return false;
3853
+ }
3854
+ /**
3855
+ * The error code a device endpoint named, if it named one.
3856
+ *
3857
+ * A named `error.code` is read before the top-level `status`, so a status word
3858
+ * (`pending`) cannot be quoted as the reason when the body also says what went
3859
+ * wrong.
3860
+ */
3861
+ function deviceErrorIdentifier(payload) {
3862
+ if (!isRecord$49(payload)) return void 0;
3863
+ const candidates = [payload.code];
3864
+ const error = payload.error;
3865
+ if (typeof error === "string") candidates.push(error);
3866
+ else if (isRecord$49(error)) candidates.push(error.code, error.type);
3867
+ candidates.push(payload.status);
3868
+ for (const candidate of candidates) if (typeof candidate === "string" && /^[a-z0-9_.-]{1,64}$/i.test(candidate)) return candidate;
3869
+ }
3870
+ function nonEmptyString$10(value) {
3871
+ return typeof value === "string" && value !== "" ? value : void 0;
3872
+ }
3873
+ function isRecord$49(value) {
3874
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3875
+ }
3045
3876
  async function oauthErrorIdentifier(response) {
3046
3877
  const payload = await response.json().catch(() => null);
3047
3878
  if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
@@ -3692,6 +4523,7 @@ var CodexAccountPool = class extends AccountPoolCore {
3692
4523
  },
3693
4524
  emptyMessage: "未登录 ChatGPT 账号,请在「设置 → 订阅服务 → ChatGPT」中添加并登录账号。",
3694
4525
  ...options.backend === void 0 ? {} : { backend: options.backend },
4526
+ ...options.keyring === void 0 ? {} : { keyring: options.keyring },
3695
4527
  ...options.maxAccounts === void 0 ? {} : { maxAccounts: options.maxAccounts }
3696
4528
  };
3697
4529
  super(hooks);
@@ -4467,8 +5299,7 @@ const SCOPES = [
4467
5299
  const DEFAULT_CLIENT_ID = Buffer.from("MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==", "base64").toString("utf8");
4468
5300
  const DEFAULT_CLIENT_SECRET = Buffer.from("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=", "base64").toString("utf8");
4469
5301
  const ANTIGRAVITY_SYSTEM_INSTRUCTION = "You are Antigravity, a powerful agentic AI coding assistant designed by Google DeepMind. You are pair programming with a user to solve coding tasks. Be concise, practical, and tool-aware.";
4470
- const ANTIGRAVITY_NO_PREAMBLE_INSTRUCTION = "CRITICAL: NEVER output rule checks, formatting guidelines, constraint checklists, or thinking/personality preambles in the final response. Output only the final response. This forbids meta commentary only; it does not forbid the short progress lines the next rule requires before tool calls.";
4471
- const ANTIGRAVITY_PROGRESS_INSTRUCTION = "Progress and tool execution rule: before EVERY tool call, emit 1-2 short sentences of ordinary, visible assistant text stating what you are about to do and why, written as normal response text the user can read. Never write those sentences only inside your thinking/thought summary: the visible text part must exist, otherwise the progress is not delivered. These progress lines are required and take precedence over the no-preamble rule. Keep them brief, professional, and factual; do not restate the whole plan. Only present the comprehensive final answer and summary in the final turn, after all tool operations are completed.";
5302
+ const ANTIGRAVITY_NO_PREAMBLE_INSTRUCTION = "CRITICAL: NEVER output rule checks, formatting guidelines, constraint checklists, or thinking/personality preambles in the final response. Output only the final response.";
4472
5303
  const GEMINI_ROLE = {
4473
5304
  user: "user",
4474
5305
  model: "model"
@@ -6768,12 +7599,7 @@ function gemini25ThinkingBudget(runtimeModel, effort) {
6768
7599
  const max = isPro ? 32768 : 24576;
6769
7600
  return Math.min(max, Math.max(min, effort === "off" || effort === "none" ? 0 : effort === "high" || effort === "xhigh" ? max : effort === "medium" ? 16384 : 4096));
6770
7601
  }
6771
- function progressExplanationInstruction$1(tools) {
6772
- if (!tools?.length) return void 0;
6773
- return ANTIGRAVITY_PROGRESS_INSTRUCTION;
6774
- }
6775
7602
  function buildRequest$2(options, model, projectId, runtimeModel, effort, images = NO_RESOLVED_IMAGES$5, wire = {}) {
6776
- const progressInstruction = progressExplanationInstruction$1(options.tools);
6777
7603
  const request = {
6778
7604
  contents: convertMessages(options, model, runtimeModel, images),
6779
7605
  systemInstruction: {
@@ -6782,8 +7608,7 @@ function buildRequest$2(options, model, projectId, runtimeModel, effort, images
6782
7608
  { text: ANTIGRAVITY_SYSTEM_INSTRUCTION },
6783
7609
  { text: `Please ignore following [ignore]${ANTIGRAVITY_SYSTEM_INSTRUCTION}[/ignore]` },
6784
7610
  { text: ANTIGRAVITY_NO_PREAMBLE_INSTRUCTION },
6785
- ...options.system ? [{ text: sanitizeText$8(options.system) }] : [],
6786
- ...progressInstruction ? [{ text: progressInstruction }] : []
7611
+ ...options.system ? [{ text: sanitizeText$8(options.system) }] : []
6787
7612
  ]
6788
7613
  }
6789
7614
  };
@@ -7371,15 +8196,6 @@ function parseAntigravityPoolData(value) {
7371
8196
  if (activeAccountId) res.activeAccountId = activeAccountId;
7372
8197
  return res;
7373
8198
  }
7374
- function poolCredentialAccount(filePath) {
7375
- return createHash("sha256").update(path.resolve(filePath)).digest("hex");
7376
- }
7377
- function createPoolCredentialBackend(filePath) {
7378
- if (process.platform === "win32") return new WindowsDpapiCredentialStore(`${filePath}.dpapi`, parseAntigravityPoolData);
7379
- if (process.platform === "darwin") return new MacKeychainCredentialStore("dsh-antigravity-pool", poolCredentialAccount(filePath), parseAntigravityPoolData);
7380
- if (process.platform === "linux") return new SecretServiceCredentialStore("dsh-antigravity-pool", poolCredentialAccount(filePath), parseAntigravityPoolData);
7381
- throw new Error("Antigravity pool encrypted storage requires Windows, macOS, or Linux.");
7382
- }
7383
8199
  /**
7384
8200
  * One bucket's reset moment as Unix milliseconds, when it parses as a date.
7385
8201
  *
@@ -7441,7 +8257,7 @@ const MAX_DETAIL_CHARS = 500;
7441
8257
  var AccountPoolStore = class {
7442
8258
  core;
7443
8259
  legacyStore;
7444
- constructor(filePath = poolPath(), backend = createPoolCredentialBackend(filePath), legacyStore = new FileCredentialStore$3()) {
8260
+ constructor(filePath = poolPath(), backend, legacyStore = new FileCredentialStore$3()) {
7445
8261
  this.legacyStore = legacyStore;
7446
8262
  const hooks = {
7447
8263
  providerId: "antigravity",
@@ -7504,7 +8320,7 @@ var AccountPoolStore = class {
7504
8320
  emptyMessage: "未登录 Antigravity 账号,请在「设置 → Antigravity」中添加并登录账号。",
7505
8321
  allUnavailableMessage: (count, waitMinutes) => `全部 ${count} 个 Antigravity 账号均处于配额限制或冷却中 (429)。` + (waitMinutes === null ? "目前没有处于冷却中的账号。" : `最短预计在 ${waitMinutes} 分钟后解除冷却。`),
7506
8322
  allAuthFailedMessage: (count) => `全部 ${count} 个 Antigravity 账号均需要重新登录。`,
7507
- backend
8323
+ ...backend === void 0 ? {} : { backend }
7508
8324
  };
7509
8325
  this.core = new AccountPoolCore(hooks);
7510
8326
  }
@@ -12671,6 +13487,12 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
12671
13487
  value: await oauth.startLogin()
12672
13488
  });
12673
13489
  return;
13490
+ case `${ROUTE_PREFIX$4}/login/device/start`:
13491
+ json(response, {
13492
+ ok: true,
13493
+ value: await oauth.startDeviceLogin()
13494
+ });
13495
+ return;
12674
13496
  case `${ROUTE_PREFIX$4}/login/cancel`: {
12675
13497
  const loginId = field(body, "loginId");
12676
13498
  if (loginId === null) throw new Error("missing loginId");
@@ -12681,6 +13503,24 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
12681
13503
  });
12682
13504
  return;
12683
13505
  }
13506
+ case `${ROUTE_PREFIX$4}/storage`: {
13507
+ const storage = adopt?.storage;
13508
+ if (storage === void 0) {
13509
+ jsonError(response, 404, {
13510
+ code: "bad-request",
13511
+ message: "The account storage setting is not available on this host."
13512
+ });
13513
+ return;
13514
+ }
13515
+ const mode = field(body, "mode");
13516
+ if (mode !== "keyring" && mode !== "credentials") throw new PreferenceError("mode must be \"keyring\" or \"credentials\".");
13517
+ await storage.update(mode);
13518
+ json(response, {
13519
+ ok: true,
13520
+ value: await refreshedStatus()
13521
+ });
13522
+ return;
13523
+ }
12684
13524
  case `${ROUTE_PREFIX$4}/accounts`: {
12685
13525
  if (accountPool === void 0) throw new Error("The ChatGPT account pool is not installed.");
12686
13526
  const action = field(body, "action");
@@ -15695,6 +16535,65 @@ function registerAntigravityRoutes(ctx, store, modelSettings, preferences, fetch
15695
16535
  });
15696
16536
  }
15697
16537
  //#endregion
16538
+ //#region src/host/common/pool-storage-settings.ts
16539
+ /**
16540
+ * Where the account-pool storage choice is remembered between restarts.
16541
+ *
16542
+ * The profile's `poolStorage` is the deployment's default, and the settings card
16543
+ * writes the operator's own answer here instead of rewriting `cordis.yml`: that
16544
+ * file is the harness composition, and a card that edited it would be rewriting
16545
+ * the deployment to change a preference. The document lives beside the plugin's
16546
+ * other storage under the harness home, is written atomically, validated with the
16547
+ * same schema machinery as the preferences, and — because it stores `null` until
16548
+ * somebody chooses — the profile's value keeps winning until a card actually says
16549
+ * otherwise.
16550
+ * @module dsh-chatgpt-subscription/pool-storage-settings
16551
+ */
16552
+ /** Where the card's choice is kept. */
16553
+ function poolStorageSettingsPath() {
16554
+ return join(dshHomeDir(), "storages", "dsh-chatgpt-subscription-pool-storage.json");
16555
+ }
16556
+ const schema = z.object({ mode: z.union([
16557
+ z.const("keyring"),
16558
+ z.const("credentials"),
16559
+ z.const(null)
16560
+ ]).default(null) });
16561
+ /** The choice the settings card writes, and the pools read. */
16562
+ var PoolStorageSettings = class {
16563
+ store;
16564
+ constructor(filePath = poolStorageSettingsPath()) {
16565
+ this.store = new FilePreferencesStore(schema, filePath);
16566
+ }
16567
+ /** Load the document once; a missing or corrupt one counts as "nothing chosen". */
16568
+ hydrate() {
16569
+ return this.store.hydrate();
16570
+ }
16571
+ /** The chosen mode, or `null` while the profile's value stands. */
16572
+ chosen() {
16573
+ return this.store.get().mode;
16574
+ }
16575
+ /**
16576
+ * The mode to use: the card's choice, else the profile's value, else the default.
16577
+ *
16578
+ * @param configured - `poolStorage` from the profile, when it carries one.
16579
+ */
16580
+ effective(configured) {
16581
+ return this.chosen() ?? configured ?? "keyring";
16582
+ }
16583
+ /**
16584
+ * Persist one choice and tell the pools to resolve their backend again.
16585
+ *
16586
+ * The announcement is part of the write rather than the route's job: a caller
16587
+ * that stores a mode without invalidating the caches would leave every pool on
16588
+ * the old store until the host restarted, which is the one failure this whole
16589
+ * option exists to avoid — reading and writing in different places.
16590
+ */
16591
+ async update(mode) {
16592
+ await this.store.update({ mode });
16593
+ poolStorageChanged();
16594
+ }
16595
+ };
16596
+ //#endregion
15698
16597
  //#region src/host/common/context-budget-fetch.ts
15699
16598
  /**
15700
16599
  * Recover a rejected input+output budget without dropping model-visible history.
@@ -23621,166 +24520,6 @@ var OllamaAccountPool = class extends AccountPoolCore {
23621
24520
  }
23622
24521
  };
23623
24522
  //#endregion
23624
- //#region src/host/kimi-code/model-catalog.ts
23625
- /**
23626
- * The ids the subscription serves today.
23627
- *
23628
- * `kimi-for-coding` is the alias that never changes: Moonshot upgrades the model
23629
- * behind it in place (K2.7 Code became K2.8 Preview without a config change), so
23630
- * this entry is the durable default a user can leave selected.
23631
- */
23632
- const KIMI_CODE_MODELS = [
23633
- {
23634
- id: "k3",
23635
- name: "K3",
23636
- version: "K3",
23637
- contextWindow: 262144,
23638
- maxContextWindow: 1048576,
23639
- maxTokens: 32768,
23640
- inputModalities: [
23641
- "text",
23642
- "image",
23643
- "video"
23644
- ],
23645
- reasoningEfforts: [
23646
- "low",
23647
- "high",
23648
- "max"
23649
- ],
23650
- defaultReasoningEffort: "high",
23651
- minimumPlan: "Moderato",
23652
- contextPlan: "Allegretto",
23653
- description: "The most capable flagship coding model: 2.8T parameters, 1M context window. The 1M context consumes about twice the quota of k3-256k.",
23654
- supportsDynamicTools: true,
23655
- quotaMultiplier: 2,
23656
- speed: "regular"
23657
- },
23658
- {
23659
- id: "k3-256k",
23660
- name: "K3 (256K)",
23661
- version: "K3",
23662
- contextWindow: 262144,
23663
- maxContextWindow: null,
23664
- maxTokens: 32768,
23665
- inputModalities: ["text", "image"],
23666
- reasoningEfforts: [
23667
- "low",
23668
- "high",
23669
- "max"
23670
- ],
23671
- defaultReasoningEffort: "high",
23672
- minimumPlan: "Moderato",
23673
- contextPlan: null,
23674
- description: "The 256K context version of K3, available to every Moderato member and above. Costs about half the quota of k3 with 1M context, and does not accept video input.",
23675
- supportsDynamicTools: true,
23676
- quotaMultiplier: 1,
23677
- speed: "regular"
23678
- },
23679
- {
23680
- id: "kimi-for-coding",
23681
- name: "Kimi for Coding",
23682
- version: "K2.8 Preview",
23683
- contextWindow: 1048576,
23684
- maxContextWindow: null,
23685
- maxTokens: 32768,
23686
- inputModalities: [
23687
- "text",
23688
- "image",
23689
- "video"
23690
- ],
23691
- reasoningEfforts: [
23692
- "low",
23693
- "high",
23694
- "max"
23695
- ],
23696
- defaultReasoningEffort: "max",
23697
- minimumPlan: null,
23698
- contextPlan: null,
23699
- description: "Performance close to K3 with more efficient thinking, and up to 1M context on every plan. Good at code completion and routine development tasks.",
23700
- supportsDynamicTools: true,
23701
- quotaMultiplier: 1,
23702
- speed: "regular"
23703
- },
23704
- {
23705
- id: "kimi-for-coding-highspeed",
23706
- name: "Kimi for Coding HighSpeed",
23707
- version: "K2.7 Code HighSpeed",
23708
- contextWindow: 262144,
23709
- maxContextWindow: null,
23710
- maxTokens: 32768,
23711
- inputModalities: [
23712
- "text",
23713
- "image",
23714
- "video"
23715
- ],
23716
- reasoningEfforts: ["high"],
23717
- defaultReasoningEffort: "high",
23718
- minimumPlan: "Allegretto",
23719
- contextPlan: null,
23720
- description: "The high-speed version of K2.7 Code with the same coding ability and roughly 5-6x faster output, at 3x quota usage. Requires the Allegretto plan or above.",
23721
- supportsDynamicTools: false,
23722
- quotaMultiplier: 3,
23723
- speed: "highspeed"
23724
- },
23725
- {
23726
- id: "k3.1-pro",
23727
- name: "K3.1 Pro",
23728
- version: "K3.1 Pro",
23729
- contextWindow: 262144,
23730
- maxContextWindow: 1048576,
23731
- maxTokens: 32768,
23732
- inputModalities: [
23733
- "text",
23734
- "image",
23735
- "video"
23736
- ],
23737
- reasoningEfforts: [
23738
- "low",
23739
- "high",
23740
- "max"
23741
- ],
23742
- defaultReasoningEffort: "max",
23743
- thinkingType: "only",
23744
- minimumPlan: "Moderato",
23745
- contextPlan: "Allegretto",
23746
- description: "The K3.1 Pro flagship: 3T parameters, a 256K context window on Plus/Moderato and 1M on Pro/Allegretto and above. Thinking runs at low/high/max, defaults to max, and cannot be disabled.",
23747
- supportsDynamicTools: true,
23748
- quotaMultiplier: null,
23749
- speed: "regular"
23750
- },
23751
- {
23752
- id: "k3.1-pro-highspeed",
23753
- name: "K3.1 Pro Highspeed",
23754
- version: "K3.1 Pro Highspeed",
23755
- contextWindow: 1048576,
23756
- maxContextWindow: null,
23757
- maxTokens: 32768,
23758
- inputModalities: [
23759
- "text",
23760
- "image",
23761
- "video"
23762
- ],
23763
- reasoningEfforts: [
23764
- "low",
23765
- "high",
23766
- "max"
23767
- ],
23768
- defaultReasoningEffort: "max",
23769
- thinkingType: "only",
23770
- minimumPlan: "Allegretto",
23771
- contextPlan: null,
23772
- description: "The high-speed K3.1 Pro with a 1M context window and faster output, at about twice the quota of the regular version. Requires the Pro/Allegretto plan or above.",
23773
- supportsDynamicTools: true,
23774
- quotaMultiplier: null,
23775
- speed: "highspeed"
23776
- }
23777
- ];
23778
- const BY_ID$1 = new Map(KIMI_CODE_MODELS.map((model) => [model.id, model]));
23779
- /** Registry entry for one model id, or undefined when the id is unknown. */
23780
- function kimiCodeModelDef(modelId) {
23781
- return BY_ID$1.get(modelId.trim());
23782
- }
23783
- //#endregion
23784
24523
  //#region src/host/common/request-images.ts
23785
24524
  function isRecord$39(value) {
23786
24525
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -24174,6 +24913,194 @@ function imageBlockToInline$2(block, images) {
24174
24913
  } : void 0;
24175
24914
  }
24176
24915
  //#endregion
24916
+ //#region src/host/kimi-code/model-catalog.ts
24917
+ /**
24918
+ * Request-body ceiling the 1M-context tier defaults to.
24919
+ *
24920
+ * The 2 MB figure this line used to apply to every model comes from the gateway's
24921
+ * generic error example and was never measured against a 1M window, while a real
24922
+ * 1M session (3-4 MB of Chinese prose) hits it long before the window is full.
24923
+ * The official kimi-cli ships no byte guard at all, so the ceiling is this
24924
+ * plugin's own safety net rather than a client requirement.
24925
+ *
24926
+ * 8 MiB is sized so bytes never become the binding limit for a 1M session: that
24927
+ * window's body is roughly 4 MB of English or 4.5 MB of dense Chinese, so 8 MiB
24928
+ * leaves nearly double the headroom. What binds first is the model's own token
24929
+ * window — the service answers "Your request exceeded model token limit", which
24930
+ * common/context-overflow.ts already normalizes to CONTEXT_WINDOW_EXCEEDED, so
24931
+ * compaction and retry behave exactly as they did before.
24932
+ *
24933
+ * The number is still an UPPER BOUND pending measurement, not a documented
24934
+ * gateway limit: a deployment that has measured its own gateway pins the real
24935
+ * figure with DSH_KIMI_CODE_MAX_BODY_BYTES. The 256K tier keeps 2 MB, where the
24936
+ * old ceiling and the window line up.
24937
+ */
24938
+ const LONG_CONTEXT_MESSAGE_BODY_BYTES = 8 * 1024 * 1024;
24939
+ /**
24940
+ * The ids the subscription serves today.
24941
+ *
24942
+ * `kimi-for-coding` is the alias that never changes: Moonshot upgrades the model
24943
+ * behind it in place (K2.7 Code became K2.8 Preview without a config change), so
24944
+ * this entry is the durable default a user can leave selected.
24945
+ */
24946
+ const KIMI_CODE_MODELS = [
24947
+ {
24948
+ id: "k3",
24949
+ name: "K3",
24950
+ version: "K3",
24951
+ contextWindow: 262144,
24952
+ maxContextWindow: 1048576,
24953
+ maxTokens: 32768,
24954
+ maxRequestBodyBytes: LONG_CONTEXT_MESSAGE_BODY_BYTES,
24955
+ inputModalities: [
24956
+ "text",
24957
+ "image",
24958
+ "video"
24959
+ ],
24960
+ reasoningEfforts: [
24961
+ "low",
24962
+ "high",
24963
+ "max"
24964
+ ],
24965
+ defaultReasoningEffort: "high",
24966
+ minimumPlan: "Moderato",
24967
+ contextPlan: "Allegretto",
24968
+ description: "The most capable flagship coding model: 2.8T parameters, 1M context window. The 1M context consumes about twice the quota of k3-256k.",
24969
+ supportsDynamicTools: true,
24970
+ quotaMultiplier: 2,
24971
+ speed: "regular"
24972
+ },
24973
+ {
24974
+ id: "k3-256k",
24975
+ name: "K3 (256K)",
24976
+ version: "K3",
24977
+ contextWindow: 262144,
24978
+ maxContextWindow: null,
24979
+ maxTokens: 32768,
24980
+ maxRequestBodyBytes: MAX_MESSAGE_BODY_BYTES,
24981
+ inputModalities: ["text", "image"],
24982
+ reasoningEfforts: [
24983
+ "low",
24984
+ "high",
24985
+ "max"
24986
+ ],
24987
+ defaultReasoningEffort: "high",
24988
+ minimumPlan: "Moderato",
24989
+ contextPlan: null,
24990
+ description: "The 256K context version of K3, available to every Moderato member and above. Costs about half the quota of k3 with 1M context, and does not accept video input.",
24991
+ supportsDynamicTools: true,
24992
+ quotaMultiplier: 1,
24993
+ speed: "regular"
24994
+ },
24995
+ {
24996
+ id: "kimi-for-coding",
24997
+ name: "Kimi for Coding",
24998
+ version: "K2.8 Preview",
24999
+ contextWindow: 1048576,
25000
+ maxContextWindow: null,
25001
+ maxTokens: 32768,
25002
+ maxRequestBodyBytes: LONG_CONTEXT_MESSAGE_BODY_BYTES,
25003
+ inputModalities: [
25004
+ "text",
25005
+ "image",
25006
+ "video"
25007
+ ],
25008
+ reasoningEfforts: [
25009
+ "low",
25010
+ "high",
25011
+ "max"
25012
+ ],
25013
+ defaultReasoningEffort: "max",
25014
+ minimumPlan: null,
25015
+ contextPlan: null,
25016
+ description: "Performance close to K3 with more efficient thinking, and up to 1M context on every plan. Good at code completion and routine development tasks.",
25017
+ supportsDynamicTools: true,
25018
+ quotaMultiplier: 1,
25019
+ speed: "regular"
25020
+ },
25021
+ {
25022
+ id: "kimi-for-coding-highspeed",
25023
+ name: "Kimi for Coding HighSpeed",
25024
+ version: "K2.7 Code HighSpeed",
25025
+ contextWindow: 262144,
25026
+ maxContextWindow: null,
25027
+ maxTokens: 32768,
25028
+ maxRequestBodyBytes: MAX_MESSAGE_BODY_BYTES,
25029
+ inputModalities: [
25030
+ "text",
25031
+ "image",
25032
+ "video"
25033
+ ],
25034
+ reasoningEfforts: ["high"],
25035
+ defaultReasoningEffort: "high",
25036
+ minimumPlan: "Allegretto",
25037
+ contextPlan: null,
25038
+ description: "The high-speed version of K2.7 Code with the same coding ability and roughly 5-6x faster output, at 3x quota usage. Requires the Allegretto plan or above.",
25039
+ supportsDynamicTools: false,
25040
+ quotaMultiplier: 3,
25041
+ speed: "highspeed"
25042
+ },
25043
+ {
25044
+ id: "k3.1-pro",
25045
+ name: "K3.1 Pro",
25046
+ version: "K3.1 Pro",
25047
+ contextWindow: 262144,
25048
+ maxContextWindow: 1048576,
25049
+ maxTokens: 32768,
25050
+ maxRequestBodyBytes: LONG_CONTEXT_MESSAGE_BODY_BYTES,
25051
+ inputModalities: [
25052
+ "text",
25053
+ "image",
25054
+ "video"
25055
+ ],
25056
+ reasoningEfforts: [
25057
+ "low",
25058
+ "high",
25059
+ "max"
25060
+ ],
25061
+ defaultReasoningEffort: "max",
25062
+ thinkingType: "only",
25063
+ minimumPlan: "Moderato",
25064
+ contextPlan: "Allegretto",
25065
+ description: "The K3.1 Pro flagship: 3T parameters, a 256K context window on Plus/Moderato and 1M on Pro/Allegretto and above. Thinking runs at low/high/max, defaults to max, and cannot be disabled.",
25066
+ supportsDynamicTools: true,
25067
+ quotaMultiplier: null,
25068
+ speed: "regular"
25069
+ },
25070
+ {
25071
+ id: "k3.1-pro-highspeed",
25072
+ name: "K3.1 Pro Highspeed",
25073
+ version: "K3.1 Pro Highspeed",
25074
+ contextWindow: 1048576,
25075
+ maxContextWindow: null,
25076
+ maxTokens: 32768,
25077
+ maxRequestBodyBytes: LONG_CONTEXT_MESSAGE_BODY_BYTES,
25078
+ inputModalities: [
25079
+ "text",
25080
+ "image",
25081
+ "video"
25082
+ ],
25083
+ reasoningEfforts: [
25084
+ "low",
25085
+ "high",
25086
+ "max"
25087
+ ],
25088
+ defaultReasoningEffort: "max",
25089
+ thinkingType: "only",
25090
+ minimumPlan: "Allegretto",
25091
+ contextPlan: null,
25092
+ description: "The high-speed K3.1 Pro with a 1M context window and faster output, at about twice the quota of the regular version. Requires the Pro/Allegretto plan or above.",
25093
+ supportsDynamicTools: true,
25094
+ quotaMultiplier: null,
25095
+ speed: "highspeed"
25096
+ }
25097
+ ];
25098
+ const BY_ID$1 = new Map(KIMI_CODE_MODELS.map((model) => [model.id, model]));
25099
+ /** Registry entry for one model id, or undefined when the id is unknown. */
25100
+ function kimiCodeModelDef(modelId) {
25101
+ return BY_ID$1.get(modelId.trim());
25102
+ }
25103
+ //#endregion
24177
25104
  //#region src/host/kimi-code/types.ts
24178
25105
  const PROVIDER_ID$8 = "kimi-code";
24179
25106
  const PROVIDER_NAME$7 = "Kimi Code";
@@ -24274,9 +25201,22 @@ function openAIUrl(suffix, region = "mainland-cn") {
24274
25201
  * spanning both tiers could not express "relax the text ceiling, leave the video
24275
25202
  * one alone", and a value chosen for a 2 MB conversation would collapse the video
24276
25203
  * tier with it. MiniMax's video budget is likewise a bare constant.
25204
+ *
25205
+ * The text/image tier is now graded by model: this constant is what the 256K
25206
+ * models keep and what an unknown model falls back to, while the 1M models
25207
+ * declare their own, larger ceiling in the registry. {@link maxRequestBodyBytesFor}
25208
+ * is the resolver that applies that grade.
24277
25209
  */
24278
25210
  const DEFAULT_MAX_MESSAGE_BODY_BYTES$1 = MAX_MESSAGE_BODY_BYTES;
24279
- /** Ceiling this process enforces, honouring the deployment override. */
25211
+ /**
25212
+ * Ceiling this process enforces when the caller has no model to key on.
25213
+ *
25214
+ * Kept alongside {@link maxRequestBodyBytesFor} rather than folded into it: this
25215
+ * is the answer for "what does this deployment allow at all", which the video
25216
+ * budget and the settings reporting paths need without naming a model. A request
25217
+ * that DOES name one goes through the per-model resolver below, so a 1M-tier
25218
+ * session gets its own tier instead of this floor.
25219
+ */
24280
25220
  function maxMessageBodyBytes$1(env = process.env) {
24281
25221
  const raw = (env.DSH_KIMI_CODE_MAX_BODY_BYTES ?? "").trim();
24282
25222
  if (raw === "") return DEFAULT_MAX_MESSAGE_BODY_BYTES$1;
@@ -24285,6 +25225,35 @@ function maxMessageBodyBytes$1(env = process.env) {
24285
25225
  return Math.floor(parsed);
24286
25226
  }
24287
25227
  /**
25228
+ * Request-body ceiling for one model, in precedence order.
25229
+ *
25230
+ * The middleware gateway that rejects a body is shared and the sessions are not:
25231
+ * a 1M-tier session produces several megabytes of conversation the 256K tier
25232
+ * cannot, so one number for both either starves the long tier or leaves the
25233
+ * short one unguarded. Hence three sources, highest priority first:
25234
+ *
25235
+ * 1. DSH_KIMI_CODE_MAX_BODY_BYTES — a measured figure for THIS deployment, so it
25236
+ * wins over every per-model default. It still has to be a positive integer:
25237
+ * a typo falls through to the model's own default rather than switching the
25238
+ * guard off, which is the one failure this variable must never cause;
25239
+ * 2. the model's {@link KimiCodeCatalogModel.maxRequestBodyBytes};
25240
+ * 3. {@link DEFAULT_MAX_MESSAGE_BODY_BYTES}, for a model the registry does not
25241
+ * know — an unknown id keeps the most conservative ceiling rather than
25242
+ * inheriting a long-context allowance nothing has vouched for.
25243
+ *
25244
+ * @param model the registry row for the model being called, or undefined when
25245
+ * the id is not one this route ships.
25246
+ * @param env environment the override is read from, injectable for tests.
25247
+ */
25248
+ function maxRequestBodyBytesFor(model, env = process.env) {
25249
+ const raw = (env.DSH_KIMI_CODE_MAX_BODY_BYTES ?? "").trim();
25250
+ if (raw !== "") {
25251
+ const parsed = Number(raw);
25252
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
25253
+ }
25254
+ return model?.maxRequestBodyBytes ?? DEFAULT_MAX_MESSAGE_BODY_BYTES$1;
25255
+ }
25256
+ /**
24288
25257
  * Bytes one METERED token is assumed to occupy when a deployment asks this route
24289
25258
  * to fold its published window against the byte ceiling.
24290
25259
  *
@@ -24518,10 +25487,6 @@ function maxOutputTokensFor$4(modelId, contextWindow) {
24518
25487
  const capped = (contextWindow ?? kimiCodeModelDef(modelId)?.contextWindow ?? 262144) - CONTEXT_HEADROOM_TOKENS$1;
24519
25488
  return Math.max(declared, capped);
24520
25489
  }
24521
- /** Whether a stored preference names a tier this line can send. */
24522
- function isKimiCacheTtl(value) {
24523
- return value === "5m" || value === "1h";
24524
- }
24525
25490
  /**
24526
25491
  * Reduce the requested output cap so prompt + output fit the window.
24527
25492
  *
@@ -24571,8 +25536,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
24571
25536
  enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$4],
24572
25537
  catalogModels: [],
24573
25538
  contextWindowOverrides: {},
24574
- defaultReasoningEffort: null,
24575
- cacheTtl: null
25539
+ defaultReasoningEffort: null
24576
25540
  };
24577
25541
  fallbackStore.read().then((stored) => {
24578
25542
  snapshot = stored;
@@ -24589,12 +25553,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
24589
25553
  enabled: z.boolean().default(true),
24590
25554
  enabledModelIds: z.array(z.string()).default([...DEFAULT_ENABLED_MODEL_IDS$4]),
24591
25555
  contextWindowOverrides: z.dict(z.number()).default({}),
24592
- defaultReasoningEffort: z.union([...KIMI_CODE_REASONING_EFFORTS.map((effort) => z.const(effort)), z.const(null)]).default(null),
24593
- cacheTtl: z.union([
24594
- z.const("5m"),
24595
- z.const("1h"),
24596
- z.const(null)
24597
- ]).default(null)
25556
+ defaultReasoningEffort: z.union([...KIMI_CODE_REASONING_EFFORTS.map((effort) => z.const(effort)), z.const(null)]).default(null)
24598
25557
  }));
24599
25558
  return {
24600
25559
  status: () => {
@@ -24604,8 +25563,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
24604
25563
  enabledModelIds: value.enabledModelIds,
24605
25564
  catalogModels: [],
24606
25565
  contextWindowOverrides: value.contextWindowOverrides,
24607
- defaultReasoningEffort: value.defaultReasoningEffort,
24608
- cacheTtl: isKimiCacheTtl(value.cacheTtl) ? value.cacheTtl : null
25566
+ defaultReasoningEffort: value.defaultReasoningEffort
24609
25567
  };
24610
25568
  },
24611
25569
  update: async (patch) => {
@@ -24614,8 +25572,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
24614
25572
  enabled: patch.enabled !== void 0 ? patch.enabled : current.enabled !== false,
24615
25573
  enabledModelIds: patch.enabledModelIds ?? current.enabledModelIds,
24616
25574
  contextWindowOverrides: patch.contextWindowOverrides ? mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) : current.contextWindowOverrides,
24617
- defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort,
24618
- cacheTtl: patch.cacheTtl !== void 0 ? patch.cacheTtl : current.cacheTtl
25575
+ defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort
24619
25576
  };
24620
25577
  await scope.update(normalized);
24621
25578
  fallbackStore.updateSettings(patch).catch(() => void 0);
@@ -24809,8 +25766,7 @@ var FileModelSettingsStore$3 = class {
24809
25766
  enabledModelIds,
24810
25767
  catalogModels,
24811
25768
  contextWindowOverrides,
24812
- defaultReasoningEffort,
24813
- cacheTtl: isKimiCacheTtl(record.cacheTtl) ? record.cacheTtl : null
25769
+ defaultReasoningEffort
24814
25770
  };
24815
25771
  }
24816
25772
  } catch {}
@@ -24819,8 +25775,7 @@ var FileModelSettingsStore$3 = class {
24819
25775
  enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$4],
24820
25776
  catalogModels: [],
24821
25777
  contextWindowOverrides: {},
24822
- defaultReasoningEffort: null,
24823
- cacheTtl: null
25778
+ defaultReasoningEffort: null
24824
25779
  };
24825
25780
  }
24826
25781
  async write(settings) {
@@ -24836,8 +25791,7 @@ var FileModelSettingsStore$3 = class {
24836
25791
  ...patch.enabled !== void 0 ? { enabled: patch.enabled } : {},
24837
25792
  ...patch.enabledModelIds !== void 0 ? { enabledModelIds: patch.enabledModelIds } : {},
24838
25793
  ...patch.contextWindowOverrides !== void 0 ? { contextWindowOverrides: mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) } : {},
24839
- ...patch.defaultReasoningEffort !== void 0 ? { defaultReasoningEffort: patch.defaultReasoningEffort } : {},
24840
- ...patch.cacheTtl !== void 0 && patch.cacheTtl !== null ? { cacheTtl: isKimiCacheTtl(patch.cacheTtl) ? patch.cacheTtl : current.cacheTtl } : patch.cacheTtl === null ? { cacheTtl: null } : {}
25794
+ ...patch.defaultReasoningEffort !== void 0 ? { defaultReasoningEffort: patch.defaultReasoningEffort } : {}
24841
25795
  };
24842
25796
  await this.write(next);
24843
25797
  return next;
@@ -24880,20 +25834,6 @@ async function persistRegion(region) {
24880
25834
  await fs.writeFile(file, region, "utf8");
24881
25835
  } catch {}
24882
25836
  }
24883
- //#endregion
24884
- //#region src/host/kimi-code/cache-hint.ts
24885
- /**
24886
- * How long an entry written at each tier survives.
24887
- *
24888
- * The tier is LOCKED when the prefix is first written, so this is the lifetime
24889
- * of the entry a request actually created rather than of the tier as a name.
24890
- */
24891
- const CACHE_TTL_MS = {
24892
- "5m": 300 * 1e3,
24893
- "1h": 3600 * 1e3
24894
- };
24895
- /** Tier assumed when the conversation states none. */
24896
- const DEFAULT_TIER = "5m";
24897
25837
  /**
24898
25838
  * Last time each session did something, epoch ms.
24899
25839
  *
@@ -24938,9 +25878,8 @@ function evaluateCacheHint(input) {
24938
25878
  if (lastActiveAt === void 0 || totalTokens === void 0) return { kind: "skip" };
24939
25879
  if (!Number.isFinite(lastActiveAt) || !Number.isFinite(totalTokens)) return { kind: "skip" };
24940
25880
  if (totalTokens < 1e5) return { kind: "skip" };
24941
- const tier = input.cacheTtl ?? DEFAULT_TIER;
24942
25881
  const idleMs = now - lastActiveAt;
24943
- if (idleMs <= CACHE_TTL_MS[tier]) return { kind: "skip" };
25882
+ if (idleMs <= 18e5) return { kind: "skip" };
24944
25883
  return {
24945
25884
  kind: "hint",
24946
25885
  idleMs,
@@ -27183,39 +28122,6 @@ function thinkingBudgetFor(effort, maxTokens) {
27183
28122
  const budget = Math.min(effort === "low" ? 2048 : effort === "high" ? 8192 : 16384, maxTokens - 1024);
27184
28123
  return budget >= 1024 ? budget : void 0;
27185
28124
  }
27186
- /**
27187
- * Media a single request build may carry.
27188
- *
27189
- * Passed as one object so adding a media kind never grows a positional
27190
- * signature the existing callers already bind.
27191
- */
27192
- /**
27193
- * The cache field for the Anthropic-compatible wire.
27194
- *
27195
- * TOP LEVEL, not per block: the service documents that a `cache_control` inside
27196
- * a message is ignored, and omitting the field means "read a 5m entry but do
27197
- * not write one" — which is why this is only attached when the caller asked
27198
- * for a tier.
27199
- */
27200
- function kimiAnthropicCacheControl(ttl) {
27201
- return { cache_control: {
27202
- type: "ephemeral",
27203
- ttl
27204
- } };
27205
- }
27206
- /**
27207
- * The cache field for the OpenAI-compatible wire.
27208
- *
27209
- * `mode: 'implicit'` is the only mode the service supports — it identifies the
27210
- * prefix itself, so this is not a request to break the cache but a request to
27211
- * write it at a chosen TTL.
27212
- */
27213
- function kimiOpenAICacheOptions(ttl) {
27214
- return { prompt_cache_options: {
27215
- mode: "implicit",
27216
- ttl
27217
- } };
27218
- }
27219
28125
  function textOf$6(content) {
27220
28126
  if (typeof content === "string") return sanitizeText$6(content);
27221
28127
  if (!Array.isArray(content)) return "";
@@ -27602,7 +28508,6 @@ function buildOpenAIRequest(options, images = NO_RESOLVED_IMAGES$3, preserveThin
27602
28508
  stream: true,
27603
28509
  stream_options: { include_usage: true },
27604
28510
  max_completion_tokens: maxTokens,
27605
- ...media.cacheTtl === null || media.cacheTtl === void 0 ? {} : kimiOpenAICacheOptions(media.cacheTtl),
27606
28511
  ...stopSequences(options.stop).length > 0 ? { stop: stopSequences(options.stop) } : {},
27607
28512
  ...options.tools && options.tools.length > 0 ? {
27608
28513
  tools: options.tools.map((tool) => ({
@@ -27737,7 +28642,6 @@ function mergeAnthropicMessages$2(entries) {
27737
28642
  }
27738
28643
  /** Build one `/v1/messages` body. */
27739
28644
  function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {}) {
27740
- const cacheTtl = media.cacheTtl ?? null;
27741
28645
  const toolCallIds = new ToolCallIdNormalizer();
27742
28646
  const carriers = options.messages.filter((message) => message.role === "system" && messageToolsOf(message) !== void 0);
27743
28647
  const unsentDeclarations = carriers.reduce((total, message) => total + (messageToolsOf(message)?.length ?? 0), 0);
@@ -27769,7 +28673,6 @@ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {
27769
28673
  model: options.model,
27770
28674
  max_tokens: maxTokens,
27771
28675
  messages: mergeAnthropicMessages$2(entries),
27772
- ...cacheTtl === null || cacheTtl === void 0 ? {} : kimiAnthropicCacheControl(cacheTtl),
27773
28676
  ...system === void 0 ? {} : { system },
27774
28677
  ...options.temperature === void 0 || budget !== void 0 ? {} : { temperature: options.temperature },
27775
28678
  ...options.stop && options.stop.length > 0 ? { stop_sequences: options.stop } : {},
@@ -27789,12 +28692,40 @@ function buildRequest(options, wire, images = NO_RESOLVED_IMAGES$3, preserveThin
27789
28692
  recordDriftCause(trackPrefixStability(options), options.sessionId);
27790
28693
  return wire === "anthropic" ? buildAnthropicRequest(options, images, media) : buildOpenAIRequest(options, images, preserveThinking, media);
27791
28694
  }
27792
- function assertRequestBodyFits$1(body, carriesVideo = false, context) {
28695
+ /**
28696
+ * Reject a request the service would refuse for its size.
28697
+ *
28698
+ * This is the most frequently reported 400 on the coding endpoint, and it is
28699
+ * worth catching locally for two reasons: the message can name the actual
28700
+ * remedy (DSH's compaction), and a request that cannot succeed should not be
28701
+ * sent at all. The measured size is the real serialized body, so it accounts
28702
+ * for tool schemas and inlined images the caller cannot easily estimate.
28703
+ *
28704
+ * The ceiling is the caller's when it passes one, because the text/image tier is
28705
+ * graded by model and only the caller knows which model the turn is for; omitted,
28706
+ * the guard resolves the model-less default (or the video tier) itself.
28707
+ *
28708
+ * The error code is CONTEXT_OVERFLOW_CODE, not PROVIDER_ERROR, because naming the
28709
+ * remedy in prose is not the same as being able to act on it: DSH forces a
28710
+ * compaction and retries the same step only for that one code, and every other
28711
+ * 400-shaped failure ends the turn. A byte ceiling is answered by the same
28712
+ * compression a token ceiling is — the conversation shrinks either way — so the
28713
+ * body of the message stays as specific as it was while the code becomes one the
28714
+ * host can recover from.
28715
+ *
28716
+ * @param body the request body about to be serialized and sent.
28717
+ * @param carriesVideo whether the request attaches video, which has its own tier.
28718
+ * @param context what the body-fit loop already did, when it ran.
28719
+ * @param limit the ceiling already resolved for this request's model.
28720
+ * @returns the serialized body, so a caller that is about to send it does not
28721
+ * serialize a body of up to tens of megabytes a second time.
28722
+ */
28723
+ function assertRequestBodyFits$1(body, carriesVideo = false, context, limit) {
27793
28724
  const serialized = JSON.stringify(body);
27794
28725
  const bytes = Buffer.byteLength(serialized, "utf8");
27795
- const limit = carriesVideo ? MAX_VIDEO_MESSAGE_BODY_BYTES : maxMessageBodyBytes$1();
27796
- if (bytes <= limit) return serialized;
27797
- throw new LlmError$1(oversizedBodyMessage(bytes, limit, body, context), CONTEXT_OVERFLOW_CODE);
28726
+ const ceiling = limit ?? (carriesVideo ? 67108864 : maxMessageBodyBytes$1());
28727
+ if (bytes <= ceiling) return serialized;
28728
+ throw new LlmError$1(oversizedBodyMessage(bytes, ceiling, body, context), CONTEXT_OVERFLOW_CODE);
27798
28729
  }
27799
28730
  function walkJson(value, visit) {
27800
28731
  if (Array.isArray(value)) {
@@ -29022,8 +29953,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
29022
29953
  const media = {
29023
29954
  videos,
29024
29955
  videoAccepted: inputModalitiesForEntry(options.model, catalog).includes("video"),
29025
- messageTools: dynamicToolsForEntry(options.model, catalog),
29026
- cacheTtl: settings.cacheTtl
29956
+ messageTools: dynamicToolsForEntry(options.model, catalog)
29027
29957
  };
29028
29958
  const contextWindow = this.contextWindowFor(options.model, entry, settings.contextWindowOverrides);
29029
29959
  const requestedMax = options.maxTokens ?? maxOutputTokensFor$4(options.model, contextWindow);
@@ -29034,7 +29964,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
29034
29964
  maxTokens: clampOutputToContext$1(requestedMax, promptLimit, estimatedInputTokens(requestOptions))
29035
29965
  };
29036
29966
  const carriesVideo = requestHasVideo(requestOptions);
29037
- const bodyLimit = carriesVideo ? MAX_VIDEO_MESSAGE_BODY_BYTES : maxMessageBodyBytes$1();
29967
+ const bodyLimit = carriesVideo ? MAX_VIDEO_MESSAGE_BODY_BYTES : maxRequestBodyBytesFor(kimiCodeModelDef(entry?.id ?? options.model), process.env);
29038
29968
  let buildOptions = boundedOptions;
29039
29969
  let body = "";
29040
29970
  for (let attempt = 0;; attempt++) {
@@ -29048,12 +29978,12 @@ var KimiCodeAdapter = class extends LlmAdapter {
29048
29978
  excessBytes: bytes - bodyLimit
29049
29979
  });
29050
29980
  if (attempt > MAX_BODY_FIT_ATTEMPTS || !requestHasSendableImage(buildOptions, images)) {
29051
- assertRequestBodyFits$1(built, carriesVideo, context());
29981
+ assertRequestBodyFits$1(built, carriesVideo, context(), bodyLimit);
29052
29982
  throw new Error("unreachable: assertRequestBodyFits returns or throws");
29053
29983
  }
29054
29984
  const drop = dropOldestImagesUntilFits(buildOptions, images, bytes - bodyLimit, requestOptions);
29055
29985
  if (drop.dropped === 0) {
29056
- assertRequestBodyFits$1(built, carriesVideo, context());
29986
+ assertRequestBodyFits$1(built, carriesVideo, context(), bodyLimit);
29057
29987
  throw new Error("unreachable: assertRequestBodyFits returns or throws");
29058
29988
  }
29059
29989
  buildOptions = drop.options;
@@ -29568,12 +30498,11 @@ async function getKimiCodeWebStatus(store, modelSettings, preferences, options =
29568
30498
  lastFetchedAt: quota?.fetchedAt ?? null,
29569
30499
  credentialsRejected: active !== null && active !== void 0 && isRefreshTokenRejected$1(active.refreshToken),
29570
30500
  cache: cacheStatsOrNull(options.sessionId),
29571
- cacheHint: cacheHintOrNull(options.sessionId, settings.cacheTtl),
30501
+ cacheHint: cacheHintOrNull(options.sessionId),
29572
30502
  preserveThinking: preserveThinkingEnabled(),
29573
30503
  models,
29574
30504
  contextWindowOverrides: settings.contextWindowOverrides,
29575
30505
  defaultReasoningEffort: settings.defaultReasoningEffort,
29576
- cacheTtl: settings.cacheTtl,
29577
30506
  loginRegion: region,
29578
30507
  serving: readOption$5(options.serving, true),
29579
30508
  conflict: readOption$5(options.conflict, null)
@@ -29608,13 +30537,12 @@ function cacheStatsOrNull(sessionId) {
29608
30537
  * the hint describes the conversation the card is actually showing rather than
29609
30538
  * some global figure.
29610
30539
  */
29611
- function cacheHintOrNull(sessionId, cacheTtl) {
30540
+ function cacheHintOrNull(sessionId) {
29612
30541
  const lastActiveAt = sessionLastActiveAt(sessionId);
29613
30542
  const decision = evaluateCacheHint({
29614
30543
  now: Date.now(),
29615
30544
  lastActiveAt,
29616
- totalTokens: getCacheStats(sessionId).cachedTokens + getCacheStats(sessionId).freshTokens,
29617
- cacheTtl
30545
+ totalTokens: getCacheStats(sessionId).cachedTokens + getCacheStats(sessionId).freshTokens
29618
30546
  });
29619
30547
  if (decision.kind !== "hint") return null;
29620
30548
  return {
@@ -29838,10 +30766,6 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
29838
30766
  const effort = body.defaultReasoningEffort;
29839
30767
  if (effort === null || isKimiCodeEffort(effort)) patch.defaultReasoningEffort = effort;
29840
30768
  }
29841
- if (body.cacheTtl !== void 0) {
29842
- const ttl = body.cacheTtl;
29843
- if (ttl === null || isKimiCacheTtl(ttl)) patch.cacheTtl = ttl;
29844
- }
29845
30769
  if (preferences) await preferences.update(patch);
29846
30770
  else await modelSettings.updateSettings(patch);
29847
30771
  if (patch.enabledModelIds !== void 0 || patch.enabled !== void 0) ctx.emit?.("llm/adapters-updated");
@@ -40202,7 +41126,7 @@ const CLAUDE_MODELS = Object.freeze([
40202
41126
  "xhigh",
40203
41127
  "max"
40204
41128
  ],
40205
- canDisableThinking: false
41129
+ canDisableThinking: true
40206
41130
  },
40207
41131
  {
40208
41132
  id: "claude-opus-5-5",
@@ -40226,7 +41150,7 @@ const CLAUDE_MODELS = Object.freeze([
40226
41150
  {
40227
41151
  id: "claude-sonnet-4-5",
40228
41152
  name: "Claude Sonnet 4.5 (latest)",
40229
- contextWindow: 1e6,
41153
+ contextWindow: 2e5,
40230
41154
  maxTokens: 64e3,
40231
41155
  supportsImage: true,
40232
41156
  supportsTemperature: true,
@@ -40241,7 +41165,7 @@ const CLAUDE_MODELS = Object.freeze([
40241
41165
  {
40242
41166
  id: "claude-sonnet-4-5-20250929",
40243
41167
  name: "Claude Sonnet 4.5",
40244
- contextWindow: 1e6,
41168
+ contextWindow: 2e5,
40245
41169
  maxTokens: 64e3,
40246
41170
  supportsImage: true,
40247
41171
  supportsTemperature: true,
@@ -42502,23 +43426,19 @@ function thinkingRequestedOff(effort) {
42502
43426
  * This is what decides whether the response ceiling is inflated by the level's
42503
43427
  * budget, and getting it wrong in the generous direction is not harmless: a
42504
43428
  * non-thinking model whose ceiling was raised by a budget it never had would
42505
- * ask the server for tokens it cannot produce. mid-convo thinks whatever the
42506
- * caller asked for; the other forms think unless the disable request is going
42507
- * to be sent and the model allows one.
43429
+ * ask the server for tokens it cannot produce. Every form thinks unless the
43430
+ * caller asked for thinking off on a model that accepts a disabled request.
42508
43431
  */
42509
43432
  function claudeRequestThinks(model, effort) {
42510
- const mode = claudeThinkingMode(model);
42511
- if (mode === "none") return false;
42512
- if (mode === "mid-convo") return true;
43433
+ if (claudeThinkingMode(model) === "none") return false;
42513
43434
  return !(thinkingRequestedOff(effort) && claudeModelCanDisableThinking(model));
42514
43435
  }
42515
43436
  /**
42516
43437
  * Dispatch the four thinking cases for one model and effort.
42517
43438
  *
42518
- * The guard order is the catalog's and is load-bearing: mid-convo is checked
42519
- * first and unconditionally, so a caller asking for thinking off on a managed
42520
- * effort model still gets the adaptive form with its block_binding. See the
42521
- * module doc for what each form is and why.
43439
+ * The disable request is checked first. A mid-convo model that cannot disable
43440
+ * still gets the adaptive form with its block_binding; one that can disable
43441
+ * takes that form without an effort override. See the module doc for the cases.
42522
43442
  *
42523
43443
  * @param model - exact model id, resolved through the frozen catalog.
42524
43444
  * @param effort - the caller's raw effort, passed through as-is.
@@ -42531,7 +43451,7 @@ function claudeThinking(model, effort, level, maxTokens, customBudgets) {
42531
43451
  const mode = claudeThinkingMode(model);
42532
43452
  if (mode === "none") return {};
42533
43453
  const namedEffort = effort !== void 0 && effort !== "" && !thinkingRequestedOff(effort) ? effort : void 0;
42534
- const off = thinkingRequestedOff(effort);
43454
+ if (thinkingRequestedOff(effort) && claudeModelCanDisableThinking(model)) return { thinking: { type: "disabled" } };
42535
43455
  if (mode === "mid-convo") return {
42536
43456
  thinking: {
42537
43457
  type: "adaptive",
@@ -42540,7 +43460,6 @@ function claudeThinking(model, effort, level, maxTokens, customBudgets) {
42540
43460
  },
42541
43461
  outputConfig: { effort: namedEffort ?? "high" }
42542
43462
  };
42543
- if (off && claudeModelCanDisableThinking(model)) return { thinking: { type: "disabled" } };
42544
43463
  if (mode === "adaptive") return {
42545
43464
  thinking: {
42546
43465
  type: "adaptive",
@@ -58821,6 +59740,20 @@ const REASONING_EFFORTS = [
58821
59740
  * configured request budget rather than taken as the budget itself.
58822
59741
  */
58823
59742
  const MODEL_CATALOG = Object.freeze({
59743
+ "cline-pass/deepseek-v4.1-flash-1017": {
59744
+ name: "DeepSeek V4.1 Flash 1017",
59745
+ contextWindow: 1048576,
59746
+ maxTokens: 384e3,
59747
+ reasoning: true,
59748
+ efforts: [
59749
+ "none",
59750
+ "low",
59751
+ "high",
59752
+ "max"
59753
+ ],
59754
+ input: ["text", "image"],
59755
+ gatewayHas: ["vision", "implicit-caching"]
59756
+ },
58824
59757
  "cline-pass/deepseek-v4.1-flash": {
58825
59758
  name: "DeepSeek V4.1 Flash",
58826
59759
  contextWindow: 1e6,
@@ -58983,13 +59916,34 @@ const DEFAULT_MODELS = Object.freeze(Object.keys(MODEL_CATALOG));
58983
59916
  function catalogEntry(model) {
58984
59917
  return MODEL_CATALOG[model];
58985
59918
  }
58986
- /** The default pin: automatic routing, no exclusions. */
58987
- function defaultPin() {
59919
+ /** The capability filter a model needs by default; empty for all others. */
59920
+ function defaultGatewayHas(model) {
59921
+ return [...catalogEntry(model)?.gatewayHas ?? []];
59922
+ }
59923
+ /**
59924
+ * Bound one pin's capability filter, the way channels are bounded.
59925
+ *
59926
+ * Applied on read and again on the wire: `has` is copied into every chat request
59927
+ * body, so a filter list is request payload on every turn, not a private note.
59928
+ */
59929
+ function capGatewayHas(names) {
59930
+ const capped = [];
59931
+ for (const name of names) {
59932
+ const trimmed = name.trim();
59933
+ if (trimmed === "" || trimmed.length > 64) continue;
59934
+ if (!capped.includes(trimmed)) capped.push(trimmed);
59935
+ if (capped.length >= 16) break;
59936
+ }
59937
+ return capped;
59938
+ }
59939
+ /** The default pin: automatic routing, no exclusions, model-default capabilities. */
59940
+ function defaultPin(model = "") {
58988
59941
  return {
58989
59942
  upstreams: [],
58990
59943
  exclude: [],
58991
59944
  pinMode: "strict",
58992
- sort: ""
59945
+ sort: "",
59946
+ has: defaultGatewayHas(model)
58993
59947
  };
58994
59948
  }
58995
59949
  //#endregion
@@ -59037,6 +59991,16 @@ function jwtExpiryMs(token) {
59037
59991
  return;
59038
59992
  }
59039
59993
  }
59994
+ /** Whether a catalog row belongs in the compact, built-in + official + added list. */
59995
+ function isListedClinePassCatalogModel(model) {
59996
+ if (model.origin === "official" || model.origin === "custom") return true;
59997
+ if (model.origin === "gateway") return false;
59998
+ return model.id.startsWith("cline-pass/");
59999
+ }
60000
+ /** The catalog rows to display/advertise, never the whole gateway search index. */
60001
+ function listedClinePassCatalogModels(settings) {
60002
+ return settings.catalogModels.filter(isListedClinePassCatalogModel);
60003
+ }
59040
60004
  function credentialPath$1() {
59041
60005
  return path.join(dshHomeDir(), "storages", "cline-pass-credentials.json");
59042
60006
  }
@@ -59130,14 +60094,15 @@ var FileCredentialStore$1 = class {
59130
60094
  }
59131
60095
  };
59132
60096
  /** One pin as stored, validated; a malformed row falls back to the default. */
59133
- function readPin(value) {
59134
- if (typeof value !== "object" || value === null || Array.isArray(value)) return defaultPin();
60097
+ function readPin(value, model) {
60098
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return defaultPin(model);
59135
60099
  const record = value;
59136
60100
  return {
59137
60101
  upstreams: Array.isArray(record.upstreams) ? record.upstreams.filter((entry) => typeof entry === "string" && entry.length > 0) : [],
59138
60102
  exclude: Array.isArray(record.exclude) ? record.exclude.filter((entry) => typeof entry === "string" && entry.length > 0) : [],
59139
60103
  pinMode: record.pinMode === "preferred" ? "preferred" : "strict",
59140
- sort: typeof record.sort === "string" ? record.sort : ""
60104
+ sort: typeof record.sort === "string" ? record.sort : "",
60105
+ has: Array.isArray(record.has) ? capGatewayHas(record.has.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())) : defaultGatewayHas(model)
59141
60106
  };
59142
60107
  }
59143
60108
  function readPins(value) {
@@ -59145,7 +60110,7 @@ function readPins(value) {
59145
60110
  const pins = {};
59146
60111
  for (const [model, pin] of Object.entries(value)) {
59147
60112
  if (typeof model !== "string" || model.length === 0) continue;
59148
- pins[model] = readPin(pin);
60113
+ pins[model] = readPin(pin, model);
59149
60114
  }
59150
60115
  return pins;
59151
60116
  }
@@ -59172,6 +60137,7 @@ function readCatalogModels$1(value) {
59172
60137
  if (typeof record.reasoning === "boolean") model.reasoning = record.reasoning;
59173
60138
  if (Array.isArray(record.input)) model.input = record.input.filter((item) => typeof item === "string");
59174
60139
  if (Array.isArray(record.efforts)) model.efforts = record.efforts.filter((item) => typeof item === "string");
60140
+ if (record.origin === "official" || record.origin === "custom" || record.origin === "gateway") model.origin = record.origin;
59175
60141
  models.push(model);
59176
60142
  }
59177
60143
  return models;
@@ -59180,18 +60146,41 @@ function positiveInteger$1(value, fallback, allowZero = false) {
59180
60146
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) return fallback;
59181
60147
  return value;
59182
60148
  }
60149
+ /**
60150
+ * Keep a legacy gateway row the user is demonstrably using.
60151
+ *
60152
+ * Rows written before `origin` existed carry none, and the compact list hides
60153
+ * every unmarked non-`cline-pass/*` row. A row that is enabled, pinned or given
60154
+ * a context override is one the user already selected, so hiding it would drop
60155
+ * a working model out of the picker on upgrade; those rows become `custom`
60156
+ * instead. Everything else stays hidden as a search-index row.
60157
+ */
60158
+ function promoteInUseCatalogRows(models, enabledModelIds, pins, contextWindowOverrides) {
60159
+ const inUse = /* @__PURE__ */ new Set([
60160
+ ...enabledModelIds === null ? models.map((model) => model.id) : enabledModelIds,
60161
+ ...Object.keys(pins),
60162
+ ...Object.keys(contextWindowOverrides)
60163
+ ]);
60164
+ if (inUse.size === 0) return models;
60165
+ return models.map((model) => model.origin !== void 0 || model.id.startsWith("cline-pass/") || !inUse.has(model.id) ? model : {
60166
+ ...model,
60167
+ origin: "custom"
60168
+ });
60169
+ }
59183
60170
  /** Parse one whole settings document. */
59184
60171
  function parseClinePassModelSettings(value) {
59185
60172
  const record = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
59186
60173
  const enabledModelIds = record.enabledModelIds === null ? null : Array.isArray(record.enabledModelIds) ? record.enabledModelIds.filter((id) => typeof id === "string") : null;
60174
+ const pins = readPins(record.pins);
60175
+ const contextWindowOverrides = readNumberOverrides$1(record.contextWindowOverrides);
59187
60176
  return {
59188
60177
  enabled: record.enabled !== false,
59189
60178
  baseURL: typeof record.baseURL === "string" && record.baseURL.trim() !== "" ? record.baseURL.trim() : DEFAULT_BASE_URL,
59190
60179
  enabledModelIds,
59191
- catalogModels: readCatalogModels$1(record.catalogModels),
59192
- pins: readPins(record.pins),
60180
+ catalogModels: promoteInUseCatalogRows(readCatalogModels$1(record.catalogModels), enabledModelIds, pins, contextWindowOverrides),
60181
+ pins,
59193
60182
  hiddenModels: Array.isArray(record.hiddenModels) ? record.hiddenModels.filter((id) => typeof id === "string") : [],
59194
- contextWindowOverrides: readNumberOverrides$1(record.contextWindowOverrides),
60183
+ contextWindowOverrides,
59195
60184
  defaultContextWindow: positiveInteger$1(record.defaultContextWindow, DEFAULT_CONTEXT_WINDOW$1),
59196
60185
  defaultMaxTokens: positiveInteger$1(record.defaultMaxTokens, DEFAULT_MAX_TOKENS$1),
59197
60186
  maxConcurrentRequests: positiveInteger$1(record.maxConcurrentRequests, 16, true),
@@ -59961,6 +60950,83 @@ function resolveModelInfo$1(model, options) {
59961
60950
  };
59962
60951
  }
59963
60952
  //#endregion
60953
+ //#region src/shared/cline-pass-contracts.ts
60954
+ /**
60955
+ * Wire contracts for the Cline Pass line's settings API.
60956
+ *
60957
+ * The line is API-key based: there is no OAuth flow, no token expiry, and no
60958
+ * per-account identity beyond the key and the alias a user gives it. What the
60959
+ * card does render beyond the shared account facts is the routing observation
60960
+ * this plugin makes about each model (pipeline, discovered channels, per-channel
60961
+ * verdicts) and the per-model pin the user stores. None of it is a credential.
60962
+ */
60963
+ const CLINE_PASS_PROVIDER_ID = "cline-pass";
60964
+ const CLINE_PASS_PROVIDER_NAME = "Cline Pass";
60965
+ const CLINE_PASS_ROUTE_PREFIX = "/cline-pass/api";
60966
+ /**
60967
+ * The gateway's anonymous contributor-route label.
60968
+ *
60969
+ * When Cline routes a model id onto the VMC fallback net, the serving channel
60970
+ * reports itself as `openai-compatible-private` and the resolved slug moves
60971
+ * under `private/…-contributor`. Answers from it may be used for training and
60972
+ * carry no zero-data-retention guarantee, so a pin landing there is a fact the
60973
+ * user must see, not a channel like any other.
60974
+ */
60975
+ const CLINE_PASS_ANONYMOUS_UPSTREAMS = Object.freeze(["openai-compatible-private"]);
60976
+ /** Whether a channel name or resolved slug names the anonymous contributor route. */
60977
+ function isClinePassAnonymousUpstream(name, canonicalSlug) {
60978
+ const value = String(name ?? "").toLowerCase();
60979
+ if (CLINE_PASS_ANONYMOUS_UPSTREAMS.includes(value)) return true;
60980
+ const slug = String(canonicalSlug ?? "").toLowerCase();
60981
+ return slug.startsWith("private/") || slug.includes("-contributor");
60982
+ }
60983
+ /** Whether a submitted pin mode names one of the two accepted modes. */
60984
+ function isClinePassPinMode(value) {
60985
+ return value === "strict" || value === "preferred";
60986
+ }
60987
+ /** Human label for the gateway's quota-window spelling. */
60988
+ function clinePassWindowLabel(type) {
60989
+ switch (type) {
60990
+ case "five_hour": return "5 小时";
60991
+ case "weekly": return "每周";
60992
+ case "monthly": return "每月";
60993
+ default: return type;
60994
+ }
60995
+ }
60996
+ /** Window length in minutes for the shared AccountQuota formatter. */
60997
+ function clinePassWindowDurationMins(type) {
60998
+ switch (type) {
60999
+ case "five_hour": return 300;
61000
+ case "weekly": return 7 * 1440;
61001
+ case "monthly": return 30 * 1440;
61002
+ default: return;
61003
+ }
61004
+ }
61005
+ /**
61006
+ * Project one account's Cline Pass usage reading into the shared
61007
+ * {@link PoolAccountQuotaDto} shape that AccountPoolSection renders inside the
61008
+ * account row. A failed reading returns undefined so the card does not draw
61009
+ * fabricated zero bars.
61010
+ */
61011
+ function clinePassAccountQuota(reading) {
61012
+ if (reading === void 0 || !reading.ok) return void 0;
61013
+ const windows = reading.windows.map((window) => {
61014
+ const duration = clinePassWindowDurationMins(window.type);
61015
+ const parsedReset = window.resetsAt === "" ? NaN : Date.parse(window.resetsAt);
61016
+ const resetsAt = Number.isFinite(parsedReset) && parsedReset > 0 ? parsedReset : null;
61017
+ return {
61018
+ label: window.label || clinePassWindowLabel(window.type),
61019
+ usedPercent: window.percentUsed,
61020
+ ...duration === void 0 ? {} : { windowDurationMins: duration },
61021
+ resetsAt
61022
+ };
61023
+ });
61024
+ return {
61025
+ fetchedAt: reading.fetchedAt,
61026
+ windows
61027
+ };
61028
+ }
61029
+ //#endregion
59964
61030
  //#region src/host/cline-pass/protocol.ts
59965
61031
  /**
59966
61032
  * Cline Pass wire protocol: pipeline detection, pin injection, failover
@@ -60010,22 +61076,97 @@ function parseRouting(json) {
60010
61076
  const choices = Array.isArray(payload.choices) ? payload.choices : [];
60011
61077
  const first = isRecord$11(choices[0]) ? choices[0] : {};
60012
61078
  const message = isRecord$11(first.message) ? first.message : {};
60013
- const metadata = isRecord$11(message.provider_metadata) ? message.provider_metadata : isRecord$11(payload.provider_metadata) ? payload.provider_metadata : {};
61079
+ const delta = isRecord$11(first.delta) ? first.delta : {};
61080
+ const metadata = [
61081
+ message,
61082
+ delta,
61083
+ payload
61084
+ ].map(providerMetadataOf).find((entry) => entry !== null) ?? {};
60014
61085
  const gateway = isRecord$11(metadata.gateway) ? metadata.gateway : {};
60015
- const routing = isRecord$11(gateway.routing) ? gateway.routing : {};
60016
61086
  const direct = typeof payload.provider === "string" ? payload.provider : null;
61087
+ let routing = isRecord$11(gateway.routing) ? gateway.routing : {};
61088
+ if (typeof routing.finalProvider !== "string") routing = huntRouting(payload) ?? routing;
61089
+ const finalProvider = typeof routing.finalProvider === "string" ? routing.finalProvider : direct === null ? null : slugify(direct);
61090
+ const canonicalSlug = typeof routing.canonicalSlug === "string" ? routing.canonicalSlug : typeof payload.model === "string" && payload.model.includes("/") ? payload.model : null;
60017
61091
  return {
60018
- content: typeof message.content === "string" ? message.content : null,
60019
- toolCalls: Array.isArray(message.tool_calls) ? message.tool_calls : [],
61092
+ content: typeof message.content === "string" ? message.content : typeof delta.content === "string" ? delta.content : null,
61093
+ toolCalls: Array.isArray(message.tool_calls) ? message.tool_calls : Array.isArray(delta.tool_calls) ? delta.tool_calls : [],
60020
61094
  usage: payload.usage ?? null,
60021
61095
  pipeline: typeof routing.finalProvider === "string" ? "planner" : direct === null ? null : "direct",
60022
- canonicalSlug: typeof routing.canonicalSlug === "string" ? routing.canonicalSlug : typeof payload.model === "string" && payload.model.includes("/") ? payload.model : null,
60023
- finalProvider: typeof routing.finalProvider === "string" ? routing.finalProvider : direct === null ? null : slugify(direct),
61096
+ canonicalSlug,
61097
+ finalProvider,
60024
61098
  finalProviderName: typeof routing.finalProvider === "string" ? routing.finalProvider : direct,
60025
61099
  fallbacks: Array.isArray(routing.fallbacksAvailable) ? routing.fallbacksAvailable.map(String) : [],
60026
- plan: typeof routing.planningReasoning === "string" ? routing.planningReasoning : ""
61100
+ plan: typeof routing.planningReasoning === "string" ? routing.planningReasoning : "",
61101
+ fingerprint: huntFingerprint(payload),
61102
+ anonymous: isClinePassAnonymousUpstream(finalProvider ?? "", canonicalSlug ?? "")
60027
61103
  };
60028
61104
  }
61105
+ /**
61106
+ * Nodes one payload walk may visit before giving up.
61107
+ *
61108
+ * The hunts run per streamed frame, and a frame is small, but a pathological
61109
+ * payload must not turn a cheap re-read into an unbounded walk.
61110
+ */
61111
+ const HUNT_NODE_BUDGET = 4e3;
61112
+ /** `provider_metadata` of one node, when present. */
61113
+ function providerMetadataOf(node) {
61114
+ return isRecord$11(node.provider_metadata) ? node.provider_metadata : null;
61115
+ }
61116
+ /**
61117
+ * Depth-bounded hunt for the gateway's routing block anywhere in one payload.
61118
+ *
61119
+ * The fast paths (`message`/`delta`/`payload.provider_metadata.gateway`) are
61120
+ * checked by the caller first; this walk covers the nestings between the shapes
61121
+ * seen so far, so a frame the fast path does not know still yields its routing.
61122
+ */
61123
+ function huntRouting(node, depth = 0, budget = { left: HUNT_NODE_BUDGET }) {
61124
+ if (depth > 8 || budget.left <= 0 || node === null || typeof node !== "object") return null;
61125
+ budget.left -= 1;
61126
+ if (Array.isArray(node)) {
61127
+ for (const entry of node) {
61128
+ const found = huntRouting(entry, depth + 1, budget);
61129
+ if (found !== null) return found;
61130
+ }
61131
+ return null;
61132
+ }
61133
+ const record = node;
61134
+ const gateway = isRecord$11(record.gateway) ? record.gateway : null;
61135
+ const routing = gateway !== null && isRecord$11(gateway.routing) ? gateway.routing : isRecord$11(record.routing) ? record.routing : null;
61136
+ if (routing !== null && typeof routing.finalProvider === "string") return routing;
61137
+ for (const value of Object.values(record)) {
61138
+ const found = huntRouting(value, depth + 1, budget);
61139
+ if (found !== null) return found;
61140
+ }
61141
+ return null;
61142
+ }
61143
+ /**
61144
+ * Read the first-party DeepSeek fingerprint wherever `provider_metadata`
61145
+ * carries it. Absent means the answer did not prove a first-party upstream —
61146
+ * the anonymous contributor route carries no `deepseek` namespace at all.
61147
+ */
61148
+ function huntFingerprint(node, depth = 0, budget = { left: HUNT_NODE_BUDGET }) {
61149
+ if (depth > 8 || budget.left <= 0 || node === null || typeof node !== "object") return null;
61150
+ budget.left -= 1;
61151
+ if (Array.isArray(node)) {
61152
+ for (const entry of node) {
61153
+ const found = huntFingerprint(entry, depth + 1, budget);
61154
+ if (found !== null) return found;
61155
+ }
61156
+ return null;
61157
+ }
61158
+ const record = node;
61159
+ const metadata = providerMetadataOf(record);
61160
+ if (metadata !== null) {
61161
+ const deepseek = isRecord$11(metadata.deepseek) ? metadata.deepseek : null;
61162
+ if (deepseek !== null && typeof deepseek.systemFingerprint === "string" && deepseek.systemFingerprint !== "") return deepseek.systemFingerprint;
61163
+ }
61164
+ for (const value of Object.values(record)) {
61165
+ const found = huntFingerprint(value, depth + 1, budget);
61166
+ if (found !== null) return found;
61167
+ }
61168
+ return null;
61169
+ }
60029
61170
  /** Lowercase a display name into an upstream slug. */
60030
61171
  function slugify(value) {
60031
61172
  return String(value).toLowerCase().replace(/\s+/g, "-");
@@ -60114,6 +61255,7 @@ function injectPrefs(body, meta, attempt) {
60114
61255
  const excludeList = attempt.excludeList ?? [];
60115
61256
  const strict = attempt.strict !== false;
60116
61257
  const sort = normalizeSort(attempt.sort);
61258
+ const has = capGatewayHas((attempt.has ?? []).filter((name) => typeof name === "string"));
60117
61259
  const excluded = excludeList.filter((name) => normalizeSlug(name) !== normalizeSlug(upstream ?? ""));
60118
61260
  const excludedIds = new Set(excluded.map(normalizeSlug));
60119
61261
  const spell = (names, pipeline) => names.map((name) => channelNameFor(name, pipeline));
@@ -60123,7 +61265,7 @@ function injectPrefs(body, meta, attempt) {
60123
61265
  const both = attempt.spelling !== "detected";
60124
61266
  const useGateway = both || pipeline === "planner" || pipeline === null;
60125
61267
  const useOpenRouter = both || pipeline === "direct" || pipeline === null;
60126
- const gatewayUsable = useGateway && (upstream !== null || sort !== null || gatewayAllow !== null && gatewayAllow.length > 0);
61268
+ const gatewayUsable = useGateway && (upstream !== null || sort !== null || has.length > 0 || gatewayAllow !== null && gatewayAllow.length > 0);
60127
61269
  const openRouterUsable = useOpenRouter && (upstream !== null || sort !== null || excluded.length > 0 || openRouterAllow !== null && openRouterAllow.length > 0);
60128
61270
  if (!gatewayUsable && !openRouterUsable) return next;
60129
61271
  if (gatewayUsable) {
@@ -60135,6 +61277,7 @@ function injectPrefs(body, meta, attempt) {
60135
61277
  }
60136
61278
  else if (gatewayAllow !== null && gatewayAllow.length > 0) gateway.only = spell(gatewayAllow, "planner");
60137
61279
  if (sort !== null) gateway.sort = sort;
61280
+ if (has.length > 0) gateway.has = [...has];
60138
61281
  const providerOptions = isRecord$11(next.providerOptions) ? next.providerOptions : {};
60139
61282
  const currentGateway = isRecord$11(providerOptions.gateway) ? providerOptions.gateway : {};
60140
61283
  next.providerOptions = {
@@ -60192,13 +61335,16 @@ function isAdherenceOk(adherence) {
60192
61335
  */
60193
61336
  function pinWarnings(attempt, meta) {
60194
61337
  const upstream = attempt.upstream ?? null;
61338
+ const warnings = [];
61339
+ if (upstream !== null && upstream !== void 0 && String(upstream).length > 0 && isClinePassAnonymousUpstream(upstream)) warnings.push(`anonymousUpstream: ${upstream} is the gateway's anonymous contributor route — answers from it may be used for training and carry no zero-data-retention guarantee; pin it only when that is acceptable`);
61340
+ const known = Array.isArray(meta?.upstreams) ? meta.upstreams : [];
61341
+ if (upstream !== null && upstream !== void 0 && String(upstream).length > 0 && !isClinePassAnonymousUpstream(upstream) && known.length > 0 && known.every((name) => isClinePassAnonymousUpstream(name))) warnings.push(`pinLikelyIgnored: every channel discovered for this model is the anonymous contributor route (${known.join(", ")}), which silently drops a pin and answers anyway — the request will likely be served by it regardless of ${upstream}`);
60195
61342
  const excluded = (attempt.excludeList ?? []).filter((name) => normalizeSlug(name) !== normalizeSlug(upstream ?? ""));
60196
61343
  const excludedIds = new Set(excluded.map(normalizeSlug));
60197
- if (excluded.length === 0) return [];
60198
- const known = Array.isArray(meta?.upstreams) ? meta.upstreams : [];
60199
- if (known.length === 0) return [`excludeUnresolved: this model has no channel list yet, so ${excluded.join(", ")} could not be compiled into an allow-list — run cline_pass_probe for it`];
60200
- if (known.filter((name) => !excludedIds.has(normalizeSlug(name))).length === 0 && upstream === null) return [`PIN_UNRESOLVED: every known channel is excluded (${known.join(", ")}), so the request falls back to the gateway's own routing`];
60201
- return [];
61344
+ if (excluded.length === 0) return warnings;
61345
+ if (known.length === 0) return [...warnings, `excludeUnresolved: this model has no channel list yet, so ${excluded.join(", ")} could not be compiled into an allow-list — run cline_pass_probe for it`];
61346
+ if (known.filter((name) => !excludedIds.has(normalizeSlug(name))).length === 0 && upstream === null) return [...warnings, `PIN_UNRESOLVED: every known channel is excluded (${known.join(", ")}), so the request falls back to the gateway's own routing`];
61347
+ return warnings;
60202
61348
  }
60203
61349
  /**
60204
61350
  * Expand one model's pin configuration into the ordered failover candidates.
@@ -60218,6 +61364,8 @@ function buildAttempts(config) {
60218
61364
  sort: normalizeSort(config.sort),
60219
61365
  excludeList: exclude
60220
61366
  };
61367
+ const has = (config.has ?? []).filter((name) => typeof name === "string" && name.trim().length > 0);
61368
+ if (has.length > 0) base.has = [...has];
60221
61369
  if (typeof config.spelling === "string" && config.spelling.length > 0) base.spelling = config.spelling;
60222
61370
  if (wanted.length > 0) return wanted.map((upstream, index) => ({
60223
61371
  ...base,
@@ -60871,8 +62019,7 @@ var ClinePassAdapter = class extends LlmAdapter {
60871
62019
  */
60872
62020
  async availableModelIds(settings) {
60873
62021
  const ids = new Set(DEFAULT_MODELS);
60874
- for (const model of settings.catalogModels) ids.add(model.id);
60875
- if (settings.exposeCatalog === true) for (const id of this.store.catalog().ids) ids.add(id);
62022
+ for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
60876
62023
  const hidden = new Set(settings.hiddenModels);
60877
62024
  const enabled = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
60878
62025
  return [...ids].filter((id) => !hidden.has(id) && (enabled === null || enabled.has(id)));
@@ -61051,7 +62198,9 @@ var ClinePassAdapter = class extends LlmAdapter {
61051
62198
  const observed = {
61052
62199
  provider: null,
61053
62200
  canonical: null,
61054
- pipeline: null
62201
+ pipeline: null,
62202
+ fingerprint: null,
62203
+ anonymous: false
61055
62204
  };
61056
62205
  const iterator = translate(tapRouting(parseServerSentEvents$1(tapChunks$1(response.body, () => {
61057
62206
  activity?.pulse();
@@ -61123,6 +62272,9 @@ var ClinePassAdapter = class extends LlmAdapter {
61123
62272
  upstream: attempt.upstream,
61124
62273
  excludeList: attempt.excludeList
61125
62274
  }, meta);
62275
+ if (adherence === "not-adopted") warnings.push(`pinNotAdopted: the gateway served ${observed.provider ?? "(unknown)"} instead of the pinned ${attempt.upstream ?? "(auto)"}`);
62276
+ else if (adherence === "violated") warnings.push(`pinViolated: an excluded channel served this request: ${observed.provider ?? "(unknown)"}`);
62277
+ if (observed.anonymous) warnings.push(`anonymousServed: this answer came from the gateway's anonymous contributor route (${observed.provider ?? "openai-compatible-private"}) — it may be used for training and carries no zero-data-retention guarantee`);
61126
62278
  const ms = Date.now() - attemptStarted;
61127
62279
  trace.push(attemptLabel);
61128
62280
  this.store.record({
@@ -61138,10 +62290,16 @@ var ClinePassAdapter = class extends LlmAdapter {
61138
62290
  code: finishFailure?.code ?? "",
61139
62291
  account: account.name,
61140
62292
  adherence,
62293
+ fingerprint: observed.fingerprint ?? "",
62294
+ anonymous: observed.anonymous,
61141
62295
  warnings,
61142
62296
  ...usage === void 0 ? {} : { usage },
61143
62297
  effort: options.reasoningEffort === void 0 ? "" : String(options.reasoningEffort)
61144
62298
  });
62299
+ if (observed.fingerprint !== null || observed.provider !== null) this.store.learn(options.model, {
62300
+ ...observed.provider === null ? {} : { lastProvider: observed.provider },
62301
+ ...observed.fingerprint === null ? {} : { lastFingerprint: observed.fingerprint }
62302
+ });
61145
62303
  this.accountPool.recordModel(account.id, options.model);
61146
62304
  if (finishFailure === null && attempt.upstream !== null && adherence === "adopted") this.store.learnUpstream(options.model, attempt.upstream, "ok", "", ms);
61147
62305
  return;
@@ -61239,8 +62397,10 @@ async function* tapRouting(payloads, observed) {
61239
62397
  if (routing.finalProvider !== null) {
61240
62398
  observed.provider = routing.finalProvider;
61241
62399
  observed.pipeline = routing.pipeline;
62400
+ observed.anonymous = routing.anonymous;
61242
62401
  if (routing.canonicalSlug !== null) observed.canonical = routing.canonicalSlug;
61243
62402
  }
62403
+ if (routing.fingerprint !== null) observed.fingerprint = routing.fingerprint;
61244
62404
  }
61245
62405
  yield payload;
61246
62406
  }
@@ -61368,7 +62528,7 @@ function effortsFromOptions(options) {
61368
62528
  const ordered = REASONING_EFFORTS.map((effort) => effort.id).filter((id) => levels.has(id));
61369
62529
  return ordered.length > 0 ? ordered : void 0;
61370
62530
  }
61371
- /** List the gateway's own catalog ids; an empty array when the call fails. */
62531
+ /** List the gateway's own catalog; empty when the call fails. */
61372
62532
  async function fetchGatewayCatalog({ fetchFn, baseURL, apiKey, signal, timeoutMs = 3e4 }) {
61373
62533
  try {
61374
62534
  const result = await sendJSON(fetchFn, `${trimBase(baseURL)}/models`, {
@@ -61376,9 +62536,24 @@ async function fetchGatewayCatalog({ fetchFn, baseURL, apiKey, signal, timeoutMs
61376
62536
  signal,
61377
62537
  timeoutMs
61378
62538
  });
61379
- return (isRecord$10(result.json) && Array.isArray(result.json.data) ? result.json.data : []).map((model) => isRecord$10(model) ? model.id : void 0).filter((id) => typeof id === "string");
62539
+ const data = isRecord$10(result.json) && Array.isArray(result.json.data) ? result.json.data : [];
62540
+ const ids = [];
62541
+ const models = {};
62542
+ for (const model of data) {
62543
+ if (!isRecord$10(model) || typeof model.id !== "string") continue;
62544
+ ids.push(model.id);
62545
+ const meta = modelMetadata(model);
62546
+ if (Object.keys(meta).length > 0) models[model.id] = meta;
62547
+ }
62548
+ return {
62549
+ ids,
62550
+ models
62551
+ };
61380
62552
  } catch {
61381
- return [];
62553
+ return {
62554
+ ids: [],
62555
+ models: {}
62556
+ };
61382
62557
  }
61383
62558
  }
61384
62559
  /**
@@ -61629,6 +62804,7 @@ var ClinePassEngine = class {
61629
62804
  exclude: this.pinOf(model).exclude,
61630
62805
  pinMode: this.pinOf(model).pinMode,
61631
62806
  sort: this.pinOf(model).sort,
62807
+ has: this.pinOf(model).has,
61632
62808
  spelling: this.options.pinFor(model).spelling
61633
62809
  };
61634
62810
  }
@@ -61639,7 +62815,8 @@ var ClinePassEngine = class {
61639
62815
  upstreams: (pin.upstreams ?? []).map(String),
61640
62816
  exclude: (pin.exclude ?? []).map(String),
61641
62817
  pinMode: pin.pinMode === "preferred" ? "preferred" : "strict",
61642
- sort: typeof pin.sort === "string" ? pin.sort : ""
62818
+ sort: typeof pin.sort === "string" ? pin.sort : "",
62819
+ has: Array.isArray(pin.has) ? pin.has.map(String) : defaultGatewayHas(model)
61643
62820
  };
61644
62821
  }
61645
62822
  /**
@@ -61716,7 +62893,9 @@ var ClinePassEngine = class {
61716
62893
  if (attempt.upstream === null && attempt.excludeList.length > 0 && outcome.error !== "") this.learnFromError(model, outcome.error);
61717
62894
  }
61718
62895
  }
61719
- const error = last === null ? "no candidate succeeded" : last.ok !== true ? last.error : last.adherence === "violated" ? `an excluded channel served the request: ${last.routing.finalProvider ?? "(unknown)"}` : `the gateway ignored the pin: served by ${last.routing.finalProvider ?? "(unknown)"}`;
62896
+ const failure = last === null ? "no candidate succeeded" : last.ok !== true ? last.error : last.adherence === "violated" ? `an excluded channel served the request: ${last.routing.finalProvider ?? "(unknown)"}` : `the gateway ignored the pin: served by ${last.routing.finalProvider ?? "(unknown)"}`;
62897
+ const has = (pinConfig.has ?? []).filter((name) => typeof name === "string" && name.trim() !== "");
62898
+ const error = has.length > 0 && /provider|capabilit|filter/i.test(failure) ? `${failure} (this pin also requires capabilities [${has.join(", ")}]; clear them in the pin editor to widen routing)` : failure;
61720
62899
  return {
61721
62900
  ok: false,
61722
62901
  result: last,
@@ -61778,21 +62957,26 @@ var ClinePassEngine = class {
61778
62957
  lastProvider: "",
61779
62958
  upstreams: [],
61780
62959
  availableProviders: [],
61781
- tier0: []
62960
+ tier0: [],
62961
+ fingerprint: "",
62962
+ anonymous: false
61782
62963
  };
61783
62964
  }
61784
62965
  const routing = parseRouting(payload);
61785
62966
  const pipeline = routing.pipeline;
61786
62967
  const otherPipeline = pipeline === "planner" ? "direct" : pipeline === "direct" ? "planner" : null;
62968
+ const has = this.pinOf(model).has;
61787
62969
  let harvested = null;
61788
62970
  if (pipeline !== null) harvested = await this.harvest(model, pipeline, account, {
61789
62971
  signal,
61790
- timeoutMs: this.options.probeTimeoutMs
62972
+ timeoutMs: this.options.probeTimeoutMs,
62973
+ has
61791
62974
  });
61792
62975
  let otherHarvested = null;
61793
62976
  if (otherPipeline !== null) otherHarvested = await this.harvest(model, otherPipeline, account, {
61794
62977
  signal,
61795
- timeoutMs: this.options.probeTimeoutMs
62978
+ timeoutMs: this.options.probeTimeoutMs,
62979
+ has
61796
62980
  });
61797
62981
  let endpoints = [];
61798
62982
  let openrouterSlug = null;
@@ -61832,6 +63016,7 @@ var ClinePassEngine = class {
61832
63016
  staleAt: combined.length === 0 && upstreams.length > 0 ? Date.now() : 0,
61833
63017
  tier0,
61834
63018
  lastProvider: routing.finalProvider ?? previous.lastProvider ?? null,
63019
+ lastFingerprint: routing.fingerprint ?? previous.lastFingerprint ?? null,
61835
63020
  lastMs: ms,
61836
63021
  probedAt: Date.now(),
61837
63022
  lastError: ""
@@ -61847,7 +63032,9 @@ var ClinePassEngine = class {
61847
63032
  lastProvider: meta.lastProvider ?? "",
61848
63033
  upstreams: meta.upstreams ?? [],
61849
63034
  availableProviders: meta.availableProviders ?? [],
61850
- tier0: meta.tier0 ?? []
63035
+ tier0: meta.tier0 ?? [],
63036
+ fingerprint: meta.lastFingerprint ?? "",
63037
+ anonymous: isClinePassAnonymousUpstream(meta.lastProvider ?? "", meta.canonicalSlug ?? "")
61851
63038
  };
61852
63039
  }
61853
63040
  /**
@@ -61855,7 +63042,7 @@ var ClinePassEngine = class {
61855
63042
  * cannot exist: the router fails before spending a token and names every
61856
63043
  * provider it could have used.
61857
63044
  */
61858
- async harvest(model, pipeline, account, { signal, timeoutMs }) {
63045
+ async harvest(model, pipeline, account, { signal, timeoutMs, has = [] }) {
61859
63046
  const base = {
61860
63047
  model,
61861
63048
  messages: [{
@@ -61866,7 +63053,10 @@ var ClinePassEngine = class {
61866
63053
  };
61867
63054
  const body = pipeline === "planner" ? {
61868
63055
  ...base,
61869
- providerOptions: { gateway: { only: ["__probe__"] } }
63056
+ providerOptions: { gateway: {
63057
+ only: ["__probe__"],
63058
+ ...has.length > 0 ? { has: [...has] } : {}
63059
+ } }
61870
63060
  } : {
61871
63061
  ...base,
61872
63062
  provider: { only: ["__probe__"] }
@@ -61880,7 +63070,12 @@ var ClinePassEngine = class {
61880
63070
  signal,
61881
63071
  timeoutMs: timeoutMs ?? this.options.probeTimeoutMs
61882
63072
  });
61883
- return extractAvailableProviders(errorText$1(unwrapEnvelope(result.json).error) || result.text, pipeline);
63073
+ const payload = unwrapEnvelope(result.json);
63074
+ const listed = extractAvailableProviders(errorText$1(payload.error) || result.text, pipeline);
63075
+ if (listed !== null && listed.length > 0) return listed;
63076
+ const routing = parseRouting(payload);
63077
+ if (routing.finalProvider !== null && routing.finalProvider !== "") return [routing.finalProvider];
63078
+ return listed;
61884
63079
  } catch {
61885
63080
  return null;
61886
63081
  }
@@ -61895,6 +63090,7 @@ var ClinePassEngine = class {
61895
63090
  async validate(model, { signal, timeoutMs } = {}) {
61896
63091
  const meta = this.store.metaOf(model);
61897
63092
  const list = Array.isArray(meta.upstreams) ? meta.upstreams : [];
63093
+ const has = this.pinOf(model).has;
61898
63094
  const results = [];
61899
63095
  for (let index = 0; index < list.length; index += 5) {
61900
63096
  if (signal?.aborted === true) break;
@@ -61906,7 +63102,8 @@ var ClinePassEngine = class {
61906
63102
  strict: true,
61907
63103
  sort: null,
61908
63104
  orderRest: [],
61909
- excludeList: []
63105
+ excludeList: [],
63106
+ has
61910
63107
  };
61911
63108
  const body = injectPrefs({
61912
63109
  model,
@@ -61985,13 +63182,14 @@ var ClinePassEngine = class {
61985
63182
  * Send one small chat through a (possibly temporary) pin configuration and
61986
63183
  * report what served it. Nothing is persisted.
61987
63184
  */
61988
- async test(model, { upstreams, exclude, signal } = {}) {
63185
+ async test(model, { upstreams, exclude, has, signal } = {}) {
61989
63186
  const stored = this.pinOf(model);
61990
63187
  const pin = {
61991
63188
  upstreams: Array.isArray(upstreams) ? upstreams : stored.upstreams,
61992
63189
  exclude: Array.isArray(exclude) ? exclude : stored.exclude,
61993
63190
  pinMode: stored.pinMode,
61994
63191
  sort: stored.sort,
63192
+ has: Array.isArray(has) ? has : stored.has,
61995
63193
  spelling: this.options.pinFor(model).spelling
61996
63194
  };
61997
63195
  const started = Date.now();
@@ -62037,6 +63235,9 @@ var ClinePassEngine = class {
62037
63235
  adopted: false,
62038
63236
  adherence: outcome.result?.adherence ?? "",
62039
63237
  plan: routing?.plan ?? "",
63238
+ fingerprint: routing?.fingerprint ?? "",
63239
+ anonymous: isClinePassAnonymousUpstream(routing?.finalProvider ?? "", routing?.canonicalSlug ?? ""),
63240
+ has: [...pin.has ?? []],
62040
63241
  warnings
62041
63242
  };
62042
63243
  }
@@ -62068,6 +63269,9 @@ var ClinePassEngine = class {
62068
63269
  adopted: outcome.result?.adherence === "adopted",
62069
63270
  adherence: outcome.result?.adherence ?? "",
62070
63271
  plan: routing?.plan ?? "",
63272
+ fingerprint: routing?.fingerprint ?? "",
63273
+ anonymous: isClinePassAnonymousUpstream(routing?.finalProvider ?? "", routing?.canonicalSlug ?? ""),
63274
+ has: [...pin.has ?? []],
62071
63275
  warnings
62072
63276
  };
62073
63277
  }
@@ -62131,7 +63335,7 @@ var ClinePassEngine = class {
62131
63335
  };
62132
63336
  }
62133
63337
  }
62134
- /** Refresh the gateway's own catalog ids. */
63338
+ /** Refresh the gateway's own catalog: ids plus published metadata. */
62135
63339
  async gatewayCatalog({ signal, timeoutMs } = {}) {
62136
63340
  const account = await this.resolveAccount();
62137
63341
  return await fetchGatewayCatalog({
@@ -62166,66 +63370,6 @@ var ClinePassEngine = class {
62166
63370
  }
62167
63371
  };
62168
63372
  //#endregion
62169
- //#region src/shared/cline-pass-contracts.ts
62170
- /**
62171
- * Wire contracts for the Cline Pass line's settings API.
62172
- *
62173
- * The line is API-key based: there is no OAuth flow, no token expiry, and no
62174
- * per-account identity beyond the key and the alias a user gives it. What the
62175
- * card does render beyond the shared account facts is the routing observation
62176
- * this plugin makes about each model (pipeline, discovered channels, per-channel
62177
- * verdicts) and the per-model pin the user stores. None of it is a credential.
62178
- */
62179
- const CLINE_PASS_PROVIDER_ID = "cline-pass";
62180
- const CLINE_PASS_PROVIDER_NAME = "Cline Pass";
62181
- const CLINE_PASS_ROUTE_PREFIX = "/cline-pass/api";
62182
- /** Whether a submitted pin mode names one of the two accepted modes. */
62183
- function isClinePassPinMode(value) {
62184
- return value === "strict" || value === "preferred";
62185
- }
62186
- /** Human label for the gateway's quota-window spelling. */
62187
- function clinePassWindowLabel(type) {
62188
- switch (type) {
62189
- case "five_hour": return "5 小时";
62190
- case "weekly": return "每周";
62191
- case "monthly": return "每月";
62192
- default: return type;
62193
- }
62194
- }
62195
- /** Window length in minutes for the shared AccountQuota formatter. */
62196
- function clinePassWindowDurationMins(type) {
62197
- switch (type) {
62198
- case "five_hour": return 300;
62199
- case "weekly": return 7 * 1440;
62200
- case "monthly": return 30 * 1440;
62201
- default: return;
62202
- }
62203
- }
62204
- /**
62205
- * Project one account's Cline Pass usage reading into the shared
62206
- * {@link PoolAccountQuotaDto} shape that AccountPoolSection renders inside the
62207
- * account row. A failed reading returns undefined so the card does not draw
62208
- * fabricated zero bars.
62209
- */
62210
- function clinePassAccountQuota(reading) {
62211
- if (reading === void 0 || !reading.ok) return void 0;
62212
- const windows = reading.windows.map((window) => {
62213
- const duration = clinePassWindowDurationMins(window.type);
62214
- const parsedReset = window.resetsAt === "" ? NaN : Date.parse(window.resetsAt);
62215
- const resetsAt = Number.isFinite(parsedReset) && parsedReset > 0 ? parsedReset : null;
62216
- return {
62217
- label: window.label || clinePassWindowLabel(window.type),
62218
- usedPercent: window.percentUsed,
62219
- ...duration === void 0 ? {} : { windowDurationMins: duration },
62220
- resetsAt
62221
- };
62222
- });
62223
- return {
62224
- fetchedAt: reading.fetchedAt,
62225
- windows
62226
- };
62227
- }
62228
- //#endregion
62229
63373
  //#region src/host/cline-pass/routes.ts
62230
63374
  function sendJson$2(response, status, body) {
62231
63375
  response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
@@ -62275,23 +63419,40 @@ function formatModelCapacity$1(tokens) {
62275
63419
  if (tokens >= 1e3 && tokens % 1e3 === 0) return tokens / 1e3 + "k";
62276
63420
  return String(tokens);
62277
63421
  }
63422
+ /** `cline-pass/*` is the subscription catalogue; every other id is marketplace credit billing. */
63423
+ function catalogBilling(id) {
63424
+ return id.startsWith("cline-pass/") ? "subscription" : "credits";
63425
+ }
63426
+ /** Published metadata for an id the store already scanned, so an added row keeps its real facts. */
63427
+ function publishedCatalogMeta(store, id) {
63428
+ const entry = store.catalog().models[id];
63429
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return {};
63430
+ const record = entry;
63431
+ const meta = {};
63432
+ if (typeof record.name === "string") meta.name = record.name;
63433
+ if (typeof record.contextWindow === "number") meta.contextWindow = record.contextWindow;
63434
+ if (typeof record.maxTokens === "number") meta.maxTokens = record.maxTokens;
63435
+ if (typeof record.reasoning === "boolean") meta.reasoning = record.reasoning;
63436
+ if (Array.isArray(record.input)) meta.input = record.input.filter((item) => typeof item === "string");
63437
+ if (Array.isArray(record.efforts)) meta.efforts = record.efforts.filter((item) => typeof item === "string");
63438
+ return meta;
63439
+ }
62278
63440
  function formatModelHint$1(contextWindow, input, reasoning) {
62279
63441
  return formatModelCapacity$1(contextWindow) + " · " + input.join("+") + (reasoning ? " · reasoning" : "");
62280
63442
  }
62281
- function uniqueModels$1(settings, catalogIds) {
63443
+ function uniqueModels$1(settings) {
62282
63444
  const ids = new Set(DEFAULT_MODELS);
62283
- for (const model of settings.catalogModels) ids.add(model.id);
62284
- if (settings.exposeCatalog) for (const id of catalogIds) ids.add(id);
63445
+ for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
62285
63446
  return [...ids];
62286
63447
  }
62287
63448
  /** Every model the card should render, whether or not it is advertised. */
62288
63449
  function projectModels$1(settings, store) {
62289
- const known = uniqueModels$1(settings, store.catalog().ids);
63450
+ const known = uniqueModels$1(settings);
62290
63451
  const hidden = new Set(settings.hiddenModels);
62291
63452
  const enabledSet = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
62292
63453
  return known.map((id) => {
62293
63454
  const meta = store.metaOf(id);
62294
- const pin = settings.pins[id] ?? defaultPin();
63455
+ const pin = settings.pins[id] ?? defaultPin(id);
62295
63456
  const base = resolveModelInfo$1(id, {
62296
63457
  displayName: "Cline Pass",
62297
63458
  synced: settings.catalogModels,
@@ -62328,6 +63489,7 @@ function projectModels$1(settings, store) {
62328
63489
  excluded: pin.exclude,
62329
63490
  pinMode: normalizePinMode(pin.pinMode),
62330
63491
  sort: pin.sort,
63492
+ has: [...pin.has],
62331
63493
  upstreams: (meta.upstreams ?? []).map(String),
62332
63494
  upstreamStatus: verdicts,
62333
63495
  lastProvider: String(meta.lastProvider ?? ""),
@@ -62380,7 +63542,7 @@ async function getClinePassWebStatus(options, quotaRefresh) {
62380
63542
  pool,
62381
63543
  models,
62382
63544
  catalogSynced: settings.catalogModels.length > 0,
62383
- catalogCount: settings.catalogModels.length,
63545
+ catalogCount: listedClinePassCatalogModels(settings).length,
62384
63546
  usage: store.usage().accounts.length === 0 ? null : store.usage(),
62385
63547
  quotaRefreshing: quotaRefresh?.refreshing === true,
62386
63548
  historySize: store.historySize(),
@@ -62392,7 +63554,6 @@ async function getClinePassWebStatus(options, quotaRefresh) {
62392
63554
  maxConcurrentRequests: settings.maxConcurrentRequests,
62393
63555
  streamIdleTimeoutMs: settings.streamIdleTimeoutMs,
62394
63556
  historyLimit: settings.historyLimit,
62395
- exposeCatalog: settings.exposeCatalog,
62396
63557
  pinSpelling: settings.pinSpelling
62397
63558
  }
62398
63559
  };
@@ -62415,6 +63576,8 @@ const CLINE_PASS_ROUTES = {
62415
63576
  "models/visibility-all": "POST",
62416
63577
  "models/context-window": "POST",
62417
63578
  "catalog/refresh": "POST",
63579
+ "catalog/search": "GET",
63580
+ "catalog/add": "POST",
62418
63581
  usage: "GET|POST",
62419
63582
  history: "GET"
62420
63583
  };
@@ -62434,6 +63597,27 @@ function registerClinePassRoutes(ctx, options) {
62434
63597
  ctx.emit?.("llm/adapters-updated");
62435
63598
  };
62436
63599
  /**
63600
+ * The full gateway catalog, cached as the search index for manual add.
63601
+ *
63602
+ * The metadata is kept alongside the ids: an id the official scan never saw
63603
+ * has no other source, and `catalog/add` copies what this call learned so the
63604
+ * added row is sized by the gateway rather than by the 128k default.
63605
+ */
63606
+ const searchCatalogIds = async () => {
63607
+ const cached = store.catalog().ids;
63608
+ if (cached.length > 0) return cached;
63609
+ const gateway = await engine.gatewayCatalog();
63610
+ store.setCatalog({
63611
+ ids: gateway.ids,
63612
+ sources: ["gateway"],
63613
+ models: {
63614
+ ...store.catalog().models,
63615
+ ...gateway.models
63616
+ }
63617
+ });
63618
+ return gateway.ids;
63619
+ };
63620
+ /**
62437
63621
  * Read every usable account's official quota and replace the snapshot.
62438
63622
  *
62439
63623
  * Shared by the explicit refresh action and the background one a status read
@@ -62743,12 +63927,13 @@ function registerClinePassRoutes(ctx, options) {
62743
63927
  ok: false,
62744
63928
  error: "this action needs a model id"
62745
63929
  });
62746
- const current = (await settingsStore.read()).pins[model] ?? defaultPin();
63930
+ const current = (await settingsStore.read()).pins[model] ?? defaultPin(model);
62747
63931
  const next = {
62748
63932
  upstreams: Array.isArray(body.upstreams) ? body.upstreams.map(String) : current.upstreams,
62749
63933
  exclude: Array.isArray(body.exclude) ? body.exclude.map(String) : current.exclude,
62750
63934
  pinMode: isClinePassPinMode(body.pinMode) ? body.pinMode : current.pinMode,
62751
- sort: body.sort === void 0 ? current.sort : body.sort === "none" ? "" : String(body.sort)
63935
+ sort: body.sort === void 0 ? current.sort : body.sort === "none" ? "" : String(body.sort),
63936
+ has: Array.isArray(body.has) ? body.has.map(String).map((name) => name.trim()).filter((name) => name !== "") : current.has
62752
63937
  };
62753
63938
  await settingsStore.setPin(model, next);
62754
63939
  const meta = store.metaOf(model);
@@ -62817,7 +64002,9 @@ function registerClinePassRoutes(ctx, options) {
62817
64002
  lastProvider: result.lastProvider,
62818
64003
  upstreams: result.upstreams,
62819
64004
  availableProviders: result.availableProviders,
62820
- tier0: result.tier0
64005
+ tier0: result.tier0,
64006
+ fingerprint: result.fingerprint,
64007
+ anonymous: result.anonymous
62821
64008
  },
62822
64009
  status: await status()
62823
64010
  }
@@ -62875,7 +64062,8 @@ function registerClinePassRoutes(ctx, options) {
62875
64062
  });
62876
64063
  const result = await engine.test(model, {
62877
64064
  upstreams: Array.isArray(body.upstreams) ? body.upstreams.map(String) : void 0,
62878
- exclude: Array.isArray(body.exclude) ? body.exclude.map(String) : void 0
64065
+ exclude: Array.isArray(body.exclude) ? body.exclude.map(String) : void 0,
64066
+ has: Array.isArray(body.has) ? body.has.map(String).map((name) => name.trim()).filter((name) => name !== "") : void 0
62879
64067
  });
62880
64068
  return sendJson$2(response, 200, {
62881
64069
  ok: true,
@@ -62897,6 +64085,9 @@ function registerClinePassRoutes(ctx, options) {
62897
64085
  adopted: result.adopted,
62898
64086
  adherence: result.adherence,
62899
64087
  plan: firstLine$2(result.plan, 300),
64088
+ fingerprint: result.fingerprint,
64089
+ anonymous: result.anonymous,
64090
+ has: result.has,
62900
64091
  warnings: result.warnings,
62901
64092
  trace: result.trace
62902
64093
  },
@@ -62912,7 +64103,7 @@ function registerClinePassRoutes(ctx, options) {
62912
64103
  });
62913
64104
  const body = await readRequestJson$2(request).catch(() => ({}));
62914
64105
  const current = await settingsStore.read();
62915
- const known = uniqueModels$1(current, store.catalog().ids);
64106
+ const known = uniqueModels$1(current);
62916
64107
  const hidden = new Set(current.hiddenModels);
62917
64108
  if (path === "models/visibility") {
62918
64109
  const model = typeof body.model === "string" ? body.model.trim() : "";
@@ -62965,6 +64156,94 @@ function registerClinePassRoutes(ctx, options) {
62965
64156
  value: await status()
62966
64157
  });
62967
64158
  }
64159
+ if (path === "catalog/search") {
64160
+ if (method !== "GET") return sendMethodNotAllowed$2(response);
64161
+ const query = (url.searchParams.get("q") ?? "").trim().toLowerCase();
64162
+ if (query === "") return sendJson$2(response, 200, {
64163
+ ok: true,
64164
+ value: { results: [] }
64165
+ });
64166
+ let ids;
64167
+ try {
64168
+ ids = await searchCatalogIds();
64169
+ } catch (error) {
64170
+ return sendJson$2(response, 502, {
64171
+ ok: false,
64172
+ error: firstLine$2(error instanceof Error ? error.message : error, 240)
64173
+ });
64174
+ }
64175
+ const current = await settingsStore.read();
64176
+ const listed = /* @__PURE__ */ new Set([...DEFAULT_MODELS, ...listedClinePassCatalogModels(current).map((entry) => entry.id)]);
64177
+ return sendJson$2(response, 200, {
64178
+ ok: true,
64179
+ value: { results: ids.filter((id) => id.toLowerCase().includes(query)).slice(0, 30).map((id) => ({
64180
+ id,
64181
+ added: listed.has(id),
64182
+ billing: catalogBilling(id)
64183
+ })) }
64184
+ });
64185
+ }
64186
+ if (path === "catalog/add") {
64187
+ if (method !== "POST") return sendMethodNotAllowed$2(response);
64188
+ if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
64189
+ ok: false,
64190
+ error: "Cross-origin request rejected."
64191
+ });
64192
+ const body = await readRequestJson$2(request).catch(() => ({}));
64193
+ const model = typeof body.model === "string" ? body.model.trim() : "";
64194
+ if (model === "") return sendJson$2(response, 400, {
64195
+ ok: false,
64196
+ error: "this action needs a model id"
64197
+ });
64198
+ let ids;
64199
+ try {
64200
+ ids = await searchCatalogIds();
64201
+ } catch (error) {
64202
+ return sendJson$2(response, 502, {
64203
+ ok: false,
64204
+ error: firstLine$2(error instanceof Error ? error.message : error, 240)
64205
+ });
64206
+ }
64207
+ if (!ids.includes(model)) return sendJson$2(response, 404, {
64208
+ ok: false,
64209
+ error: `${model} is not in the gateway catalog; refresh or search again.`
64210
+ });
64211
+ const billing = catalogBilling(model);
64212
+ if (billing === "credits" && body.acknowledgeBilling !== true) return sendJson$2(response, 409, {
64213
+ ok: false,
64214
+ code: "credits_billing",
64215
+ billing,
64216
+ error: `${model} is a marketplace model billed against the Cline Credits balance, not the Cline Pass plan. Retry with acknowledgeBilling:true to add it anyway.`
64217
+ });
64218
+ const current = await settingsStore.read();
64219
+ const published = publishedCatalogMeta(store, model);
64220
+ const exists = current.catalogModels.some((entry) => entry.id === model);
64221
+ const catalogModels = exists ? current.catalogModels.map((entry) => entry.id === model ? {
64222
+ ...entry,
64223
+ ...published,
64224
+ origin: "custom"
64225
+ } : entry) : [...current.catalogModels, {
64226
+ id: model,
64227
+ ...published,
64228
+ origin: "custom"
64229
+ }];
64230
+ const enabledModelIds = current.enabledModelIds === null ? null : [.../* @__PURE__ */ new Set([...current.enabledModelIds, model])];
64231
+ const hiddenModels = current.hiddenModels.filter((id) => id !== model);
64232
+ await settingsStore.update({
64233
+ catalogModels,
64234
+ enabledModelIds,
64235
+ hiddenModels
64236
+ });
64237
+ emitModelsUpdated();
64238
+ return sendJson$2(response, 200, {
64239
+ ok: true,
64240
+ value: {
64241
+ added: !exists,
64242
+ billing,
64243
+ status: await status()
64244
+ }
64245
+ });
64246
+ }
62968
64247
  if (path === "catalog/refresh") {
62969
64248
  if (method !== "POST") return sendMethodNotAllowed$2(response);
62970
64249
  if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
@@ -62985,6 +64264,7 @@ function registerClinePassRoutes(ctx, options) {
62985
64264
  const published = scan.catalog[id] ?? {};
62986
64265
  existing.set(id, {
62987
64266
  id,
64267
+ origin: "official",
62988
64268
  ...published.name === void 0 ? {} : { name: published.name },
62989
64269
  ...published.contextWindow === void 0 ? {} : { contextWindow: published.contextWindow },
62990
64270
  ...published.maxTokens === void 0 ? {} : { maxTokens: published.maxTokens },
@@ -62993,17 +64273,18 @@ function registerClinePassRoutes(ctx, options) {
62993
64273
  ...published.efforts === void 0 ? {} : { efforts: published.efforts }
62994
64274
  });
62995
64275
  }
62996
- const gateway = current.exposeCatalog ? await engine.gatewayCatalog().catch(() => []) : [];
62997
- let merged = [...existing.values()];
62998
- if (gateway.length > 0) {
62999
- const known = new Set(merged.map((entry) => entry.id));
63000
- merged = [...merged, ...gateway.filter((id) => !known.has(id)).map((id) => ({ id }))];
63001
- }
63002
- await settingsStore.storeCatalog(merged);
64276
+ const gateway = await engine.gatewayCatalog().catch(() => ({
64277
+ ids: [],
64278
+ models: {}
64279
+ }));
64280
+ await settingsStore.storeCatalog([...existing.values()]);
63003
64281
  store.setCatalog({
63004
- ids: [.../* @__PURE__ */ new Set([...scan.models, ...gateway])],
63005
- sources: scan.sources,
63006
- models: scan.catalog
64282
+ ids: [.../* @__PURE__ */ new Set([...scan.models, ...gateway.ids])],
64283
+ sources: [...scan.sources, ...gateway.ids.length > 0 ? ["gateway"] : []],
64284
+ models: {
64285
+ ...gateway.models,
64286
+ ...scan.catalog
64287
+ }
63007
64288
  });
63008
64289
  emitModelsUpdated();
63009
64290
  return sendJson$2(response, 200, {
@@ -63077,6 +64358,8 @@ function registerClinePassRoutes(ctx, options) {
63077
64358
  error: entry.error,
63078
64359
  attempts: entry.attempts,
63079
64360
  adherence: entry.adherence,
64361
+ fingerprint: entry.fingerprint,
64362
+ anonymous: entry.anonymous,
63080
64363
  warnings: entry.warnings
63081
64364
  }));
63082
64365
  return sendJson$2(response, 200, {
@@ -63197,6 +64480,8 @@ function createClinePassStore({ historyLimit = 100 } = {}) {
63197
64480
  error: entry.error ?? "",
63198
64481
  attempts: entry.attempts ?? [],
63199
64482
  adherence: entry.adherence ?? "",
64483
+ fingerprint: entry.fingerprint ?? "",
64484
+ anonymous: entry.anonymous === true,
63200
64485
  warnings: entry.warnings ?? [],
63201
64486
  ...entry.code === void 0 ? {} : { code: entry.code }
63202
64487
  };
@@ -63380,6 +64665,11 @@ const MODEL_SCHEMA = {
63380
64665
  type: "string",
63381
64666
  required: true
63382
64667
  },
64668
+ has: {
64669
+ type: "array",
64670
+ required: true,
64671
+ items: { type: "string" }
64672
+ },
63383
64673
  pinned: {
63384
64674
  type: "array",
63385
64675
  required: true,
@@ -63455,6 +64745,7 @@ function projectModel(status, model) {
63455
64745
  hidden: !entry.enabled,
63456
64746
  pinMode: normalizePinMode(entry.pinMode),
63457
64747
  sort: entry.sort,
64748
+ has: entry.has,
63458
64749
  pinned: entry.pinned,
63459
64750
  excluded: entry.excluded,
63460
64751
  upstreams: entry.upstreams,
@@ -63478,7 +64769,7 @@ function projectModel(status, model) {
63478
64769
  function registerClinePassTools(ctx, options) {
63479
64770
  const { settings, accountPool, store, engine, status, serving } = options;
63480
64771
  const disposers = [];
63481
- const pinOf = async (model) => (await settings.read()).pins[model] ?? defaultPin();
64772
+ const pinOf = async (model) => (await settings.read()).pins[model] ?? defaultPin(model);
63482
64773
  disposers.push(ctx.tools.register(defineTool({
63483
64774
  name: "cline_pass_status",
63484
64775
  description: "Report the Cline Pass provider state: route, accounts and whether each key is stored, account mode, model and pin counts. Read-only and free. Check this first when a Cline Pass model fails.",
@@ -63643,6 +64934,7 @@ function registerClinePassTools(ctx, options) {
63643
64934
  const published = scan.catalog[id] ?? {};
63644
64935
  merged.push({
63645
64936
  id,
64937
+ origin: "official",
63646
64938
  ...published.name === void 0 ? {} : { name: published.name },
63647
64939
  ...published.contextWindow === void 0 ? {} : { contextWindow: published.contextWindow },
63648
64940
  ...published.maxTokens === void 0 ? {} : { maxTokens: published.maxTokens },
@@ -63652,10 +64944,17 @@ function registerClinePassTools(ctx, options) {
63652
64944
  });
63653
64945
  }
63654
64946
  await settings.storeCatalog(merged);
64947
+ const gateway = await engine.gatewayCatalog().catch(() => ({
64948
+ ids: [],
64949
+ models: {}
64950
+ }));
63655
64951
  store.setCatalog({
63656
- ids: scan.models,
63657
- sources: scan.sources,
63658
- models: scan.catalog
64952
+ ids: [.../* @__PURE__ */ new Set([...scan.models, ...gateway.ids])],
64953
+ sources: [...scan.sources, ...gateway.ids.length > 0 ? ["gateway"] : []],
64954
+ models: {
64955
+ ...gateway.models,
64956
+ ...scan.catalog
64957
+ }
63659
64958
  });
63660
64959
  }
63661
64960
  const current = await settings.read();
@@ -63681,7 +64980,7 @@ function registerClinePassTools(ctx, options) {
63681
64980
  const catalog = store.catalog();
63682
64981
  return {
63683
64982
  models,
63684
- catalogCount: catalog.ids.length,
64983
+ catalogCount: listedClinePassCatalogModels(await settings.read()).length,
63685
64984
  catalogSources: catalog.sources,
63686
64985
  catalogFetchedAt: Math.round(catalog.fetchedAt),
63687
64986
  added
@@ -63897,6 +65196,11 @@ function registerClinePassTools(ctx, options) {
63897
65196
  type: "array",
63898
65197
  items: { type: "string" },
63899
65198
  description: "Upstream channels to exclude for this call only."
65199
+ },
65200
+ has: {
65201
+ type: "array",
65202
+ items: { type: "string" },
65203
+ description: "Vercel gateway capability filter for this call only."
63900
65204
  }
63901
65205
  },
63902
65206
  timeoutMs: 6e5,
@@ -63937,6 +65241,11 @@ function registerClinePassTools(ctx, options) {
63937
65241
  type: "string",
63938
65242
  required: true
63939
65243
  },
65244
+ has: {
65245
+ type: "array",
65246
+ required: true,
65247
+ items: { type: "string" }
65248
+ },
63940
65249
  warnings: {
63941
65250
  type: "array",
63942
65251
  required: true,
@@ -63964,6 +65273,7 @@ function registerClinePassTools(ctx, options) {
63964
65273
  const result = await engine.test(model, {
63965
65274
  upstreams: Array.isArray(args.upstreams) ? args.upstreams.map(String) : void 0,
63966
65275
  exclude: Array.isArray(args.exclude) ? args.exclude.map(String) : void 0,
65276
+ has: Array.isArray(args.has) ? args.has.map(String) : void 0,
63967
65277
  signal: exec.signal
63968
65278
  });
63969
65279
  return {
@@ -63975,6 +65285,7 @@ function registerClinePassTools(ctx, options) {
63975
65285
  pipeline: result.pipeline,
63976
65286
  account: result.account,
63977
65287
  adherence: result.adherence,
65288
+ has: result.has,
63978
65289
  warnings: result.warnings,
63979
65290
  trace: result.trace.map((entry) => ({
63980
65291
  upstream: entry.upstream,
@@ -64018,6 +65329,11 @@ function registerClinePassTools(ctx, options) {
64018
65329
  "none"
64019
65330
  ],
64020
65331
  description: "Reorder candidates by this metric; \"none\" clears it."
65332
+ },
65333
+ has: {
65334
+ type: "array",
65335
+ items: { type: "string" },
65336
+ description: "Vercel gateway capability filter; omit to keep the stored value, or pass an empty array to clear it."
64021
65337
  }
64022
65338
  },
64023
65339
  timeoutMs: 3e4,
@@ -64052,6 +65368,11 @@ function registerClinePassTools(ctx, options) {
64052
65368
  type: "string",
64053
65369
  required: true
64054
65370
  },
65371
+ has: {
65372
+ type: "array",
65373
+ required: true,
65374
+ items: { type: "string" }
65375
+ },
64055
65376
  warnings: {
64056
65377
  type: "array",
64057
65378
  required: true,
@@ -64063,7 +65384,7 @@ function registerClinePassTools(ctx, options) {
64063
65384
  `pinned ${value.model}`,
64064
65385
  `upstreams: ${value.pinned.length === 0 ? "(auto)" : value.pinned.join(" > ")}`,
64065
65386
  `exclude: ${value.excluded.length === 0 ? "(none)" : value.excluded.join(", ")}`,
64066
- `pinMode=${value.pinMode} sort=${value.sort === "" ? "none" : value.sort}`,
65387
+ `pinMode=${value.pinMode} sort=${value.sort === "" ? "none" : value.sort} has=${value.has.length === 0 ? "(none)" : value.has.join(",")}`,
64067
65388
  ...value.warnings.map((warning) => `⚠ ${warning}`)
64068
65389
  ].join("\n"))
64069
65390
  },
@@ -64074,7 +65395,8 @@ function registerClinePassTools(ctx, options) {
64074
65395
  upstreams: Array.isArray(args.upstreams) ? args.upstreams.map(String) : current.upstreams,
64075
65396
  exclude: Array.isArray(args.exclude) ? args.exclude.map(String) : current.exclude,
64076
65397
  pinMode: args.pinMode !== void 0 ? args.pinMode === "preferred" ? "preferred" : "strict" : current.pinMode,
64077
- sort: args.sort === void 0 ? current.sort : args.sort === "none" ? "" : String(args.sort)
65398
+ sort: args.sort === void 0 ? current.sort : args.sort === "none" ? "" : String(args.sort),
65399
+ has: Array.isArray(args.has) ? args.has.map(String).map((name) => name.trim()).filter((name) => name !== "") : current.has
64078
65400
  };
64079
65401
  await settings.setPin(model, next);
64080
65402
  const meta = store.metaOf(model);
@@ -64096,6 +65418,7 @@ function registerClinePassTools(ctx, options) {
64096
65418
  excluded: next.exclude,
64097
65419
  pinMode: normalizePinMode(next.pinMode),
64098
65420
  sort: next.sort,
65421
+ has: next.has,
64099
65422
  warnings
64100
65423
  };
64101
65424
  }
@@ -64328,16 +65651,15 @@ function registerClinePassTools(ctx, options) {
64328
65651
  /** Every id the visibility toggle may act on, including shipped ones. */
64329
65652
  function currentModelsForVisibility(settings) {
64330
65653
  const ids = new Set(DEFAULT_MODELS);
64331
- for (const model of settings.catalogModels) ids.add(model.id);
65654
+ for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
64332
65655
  return [...ids];
64333
65656
  }
64334
65657
  //#endregion
64335
65658
  //#region src/host/cline-pass/line.ts
64336
65659
  /** Every id the line knows, before the visibility selection is applied. */
64337
- function clinePassKnownModelIds(settings, catalogIds) {
65660
+ function clinePassKnownModelIds(settings, _catalogIds = []) {
64338
65661
  const ids = new Set(DEFAULT_MODELS);
64339
- for (const model of settings.catalogModels) ids.add(model.id);
64340
- if (settings.exposeCatalog) for (const id of catalogIds) ids.add(id);
65662
+ for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
64341
65663
  return [...ids];
64342
65664
  }
64343
65665
  /** Every id the line advertises right now. */
@@ -64361,7 +65683,7 @@ function createClinePassLine(ctx, options) {
64361
65683
  accountPool,
64362
65684
  store,
64363
65685
  pinFor: (model) => ({
64364
- ...modelSettings.status().pins[model] ?? defaultPin(),
65686
+ ...modelSettings.status().pins[model] ?? defaultPin(model),
64365
65687
  spelling: modelSettings.status().pinSpelling
64366
65688
  }),
64367
65689
  baseURL: () => modelSettings.status().baseURL,
@@ -66360,13 +67682,44 @@ async function fetchQoderUserInfo(accessToken, region, options = {}) {
66360
67682
  };
66361
67683
  }
66362
67684
  /**
66363
- * POST {openapi}/api/v1/jobToken/exchange,体 \`{"personal_token":"<pt-...>"}\`。
67685
+ * 一次 PAT 兑换的失败,带 HTTP 状态(调用方据此分类「token 被拒」与「瞬时故障」)。
67686
+ */
67687
+ var QoderPatExchangeError = class extends Error {
67688
+ status;
67689
+ /** 兑换打的是哪个区(区域猜错是这类 400 最常见的真因)。 */
67690
+ region;
67691
+ constructor(message, status, region) {
67692
+ super(message);
67693
+ this.name = "QoderPatExchangeError";
67694
+ this.status = status;
67695
+ this.region = region;
67696
+ }
67697
+ };
67698
+ /**
67699
+ * 上游对这个状态的回答是否在说「这把 token 不认」。
67700
+ *
67701
+ * 四个区共用一套语义:400 BadRequest 是服务端读完了 `personal_token` 才发现它
67702
+ * 不被接受(缺字段、写错字段名、空体也是 400,但那种请求来自别的实现,不来自这里);
67703
+ * 401/403 是鉴权拒绝;404 是这套部署上没有这个 token。
67704
+ */
67705
+ function qoderPatRejectionStatus(status) {
67706
+ return status === 400 || status === 401 || status === 403 || status === 404;
67707
+ }
67708
+ /**
67709
+ * POST {openapi}/api/v1/jobToken/exchange,体 \`{"personal_token":"<pt-...>","machine_id":…}\`。
66364
67710
  *
66365
67711
  * PAT 不能直接鉴权,必须先兑换(docs.qoder.com/cli/authentication + pi-provider pat.ts)。
66366
- * 失败抛错(登录路径要如实告诉用户 PAT 被拒)。
67712
+ * 失败抛 {@link QoderPatExchangeError}(登录路径要如实告诉用户 PAT 被拒)。
67713
+ *
67714
+ * **`machine_id` 是官方 CLI 1.1.74 的请求体字段**(qodercli2api `docs/oauth.md` §3:
67715
+ * `getMachineIdentityRequestFields` 为 exchange/refresh 补上既有 machine_id,
67716
+ * 没有 UMID 时不补 machine_token,也不拿 machine_id 冒充它)。早先按 pi-provider
67717
+ * 只发 `personal_token` 的形状在实测里仍可能被服务端接受,但没有理由不发这个官方字段。
66367
67718
  */
66368
67719
  async function exchangeQoderJobToken(personalToken, region, options = {}) {
66369
- const response = await (options.fetchFn ?? fetch)(openapiBaseForRegion(region, options.env ?? process.env) + JOB_TOKEN_EXCHANGE_PATH, {
67720
+ const fetchFn = options.fetchFn ?? fetch;
67721
+ const env = options.env ?? process.env;
67722
+ const response = await fetchFn(openapiBaseForRegion(region, env) + JOB_TOKEN_EXCHANGE_PATH, {
66370
67723
  method: "POST",
66371
67724
  headers: {
66372
67725
  "content-type": "application/json",
@@ -66375,12 +67728,15 @@ async function exchangeQoderJobToken(personalToken, region, options = {}) {
66375
67728
  "cosy-clienttype": "5",
66376
67729
  "user-agent": USER_AGENT
66377
67730
  },
66378
- body: JSON.stringify({ personal_token: personalToken }),
67731
+ body: JSON.stringify({
67732
+ personal_token: personalToken,
67733
+ machine_id: resolveQoderMachineId(env)
67734
+ }),
66379
67735
  signal: timeoutSignal(options.signal, DISCOVERY_TIMEOUT_MS)
66380
67736
  });
66381
67737
  if (!response.ok) {
66382
67738
  const detail = qoderErrorDetail(await response.text().catch(() => ""), 160);
66383
- throw new Error("Qoder(订阅) PAT 兑换失败(HTTP " + response.status + ")" + (detail === "" ? "" : ":" + detail));
67739
+ throw new QoderPatExchangeError("Qoder(订阅) PAT 兑换失败(HTTP " + response.status + ")" + (detail === "" ? "" : ":" + detail), response.status, region);
66384
67740
  }
66385
67741
  const payload = await response.json().catch(() => void 0);
66386
67742
  if (!isRecord$7(payload)) throw new Error("Qoder(订阅) PAT 兑换响应不是 JSON 对象");
@@ -67823,11 +69179,42 @@ async function qoderCredentialsFromDevicePoll(ready, machineId, options = {}) {
67823
69179
  * 兑换 + userinfo 补齐;**PAT 本体留在凭据里**(host-only),因为 PAT 的「刷新」
67824
69180
  * 就是拿它重新兑换一把 job token(pi-provider 把 PAT 编进 refresh 字段,
67825
69181
  * 本线路把它放进一个具名字段——同一件事,更好读)。
69182
+ *
69183
+ * ---------------------------------------------------------------------------
69184
+ * 自动补认区域
69185
+ * ---------------------------------------------------------------------------
69186
+ * 区域是**凭据的属性**,但 PAT 字符串本身不自述区域,而卡片上的区域选择器默认停在
69187
+ * 全球区。中国站的 PAT 拿去全球区端点兑换,上游回的是 400 `BadRequest`——没有
69188
+ * 任何一句说明「你打错了区」,用户看到的只有一串 requestId。这一步因此不是
69189
+ * 「多试一次」,而是**把选错区域的这个必然失败翻译成一次成功的登录**。
69190
+ *
69191
+ * 只在明确的 token 被拒状态下换区({@link qoderPatRejectionStatus}):HTTP 429、
69192
+ * 5xx、网络错误都不换——那些是瞬时故障,换区重发只会把一个可重试的问题变成两次
69193
+ * 打上游。首次命中的区域写进凭据,之后刷新照它走。
67826
69194
  */
67827
69195
  async function qoderCredentialsFromPat(personalToken, region, options = {}) {
67828
69196
  const pat = personalToken.trim();
67829
69197
  if (pat === "") throw new Error("Qoder(订阅) 的 PAT 是空的。");
67830
- const exchanged = await exchangeQoderJobToken(pat, region, options);
69198
+ if (!pat.startsWith("pt-")) throw new Error("Qoder(订阅) 的 PAT 格式不对:它必须以 `pt-` 开头,粘贴时不要带引号或多余空白。令牌在 https://qoder.com/account/integrations(中国站是 https://qoder.com.cn/account/integrations)生成。");
69199
+ const candidates = [region, ...QODER_REGIONS.filter((other) => other !== region)];
69200
+ let resolved = region;
69201
+ let exchanged = null;
69202
+ let rejection = null;
69203
+ for (const candidate of candidates) try {
69204
+ exchanged = await exchangeQoderJobToken(pat, candidate, options);
69205
+ resolved = candidate;
69206
+ rejection = null;
69207
+ break;
69208
+ } catch (error) {
69209
+ rejection = error;
69210
+ if (!(error instanceof QoderPatExchangeError) || !qoderPatRejectionStatus(error.status)) throw error;
69211
+ }
69212
+ if (exchanged === null) {
69213
+ const primary = rejection instanceof Error ? rejection.message : String(rejection);
69214
+ const tried = candidates.length > 1 ? regionLabel(candidates[0]) + "与" + regionLabel(candidates[1]) + "两个区域都试过" : regionLabel(region) + "区域试过";
69215
+ const status = rejection instanceof QoderPatExchangeError ? rejection.status : 400;
69216
+ throw new QoderPatExchangeError("这把 PAT 被上游拒了(" + tried + ",都会在兑换时被拒)。最可能的原因是它已经被撤销或过期——请到 Integrations 页重新生成一个再粘贴。技术详情:" + primary, status, region);
69217
+ }
67831
69218
  let uid = null;
67832
69219
  let email = null;
67833
69220
  let name = null;
@@ -67835,7 +69222,7 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
67835
69222
  let organizationTags = [];
67836
69223
  let dataPolicyAgreed = false;
67837
69224
  try {
67838
- const info = await fetchQoderUserInfo(exchanged.token, region, options);
69225
+ const info = await fetchQoderUserInfo(exchanged.token, resolved, options);
67839
69226
  uid = info.userID === "" ? null : info.userID;
67840
69227
  email = info.email === "" ? null : info.email;
67841
69228
  name = info.name === "" ? null : info.name;
@@ -67847,7 +69234,7 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
67847
69234
  accessToken: exchanged.token,
67848
69235
  refreshToken: exchanged.refreshToken,
67849
69236
  expiresAt: exchanged.expiresAt,
67850
- region,
69237
+ region: resolved,
67851
69238
  origin: "pat",
67852
69239
  uid,
67853
69240
  email,
@@ -67859,6 +69246,10 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
67859
69246
  authenticatedAt: Date.now()
67860
69247
  };
67861
69248
  }
69249
+ /** 区域的短名(只用于拼给人看的一句话,卡片另有自己的文案)。 */
69250
+ function regionLabel(region) {
69251
+ return REGION_HOSTS$1[region].title;
69252
+ }
67862
69253
  /**
67863
69254
  * 刷新一份凭据(号池 refresh 钩子的实现)。
67864
69255
  *
@@ -67870,6 +69261,11 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
67870
69261
  *
67871
69262
  * 失败带上 \`status\`(有 HTTP 状态时),号池据此分类「这个账号要重登」还是
67872
69263
  * 「瞬时故障,别判死」。
69264
+ *
69265
+ * PAT 分支走的是 {@link qoderCredentialsFromPat},因此**也和首次登录一样会补认区域**:
69266
+ * 一份区域记错了的旧凭据,会在下一次刷新时自己搬到真正认它的那一区(区域本来就是
69267
+ * 凭据的属性,而 PAT 是唯一能问出它归属的原料)。这是修好「区域选错 → 永远 400」
69268
+ * 的第二半——只修首次登录的话,落盘里已有的错区账号会一直刷不上。
67873
69269
  */
67874
69270
  async function refreshQoderCredential(credentials, fetchFn = fetch) {
67875
69271
  if (credentials.origin === "pat" && credentials.personalToken !== null) return {
@@ -68156,8 +69552,13 @@ function qoderPoolQuota(snapshot) {
68156
69552
  *
68157
69553
  * 401/403 → \`'expired'\`(上游明说这把 token 不行了,这个账号要重登);
68158
69554
  * 其余(网络故障、5xx、形状变了)→ undefined:瞬时故障不该把一个账号判死。
69555
+ *
69556
+ * PAT 来源的刷新是「拿存下的 PAT 重兑换」,它的 400 同样是终局:上游读完了
69557
+ * `personal_token` 才拒绝(见 client.ts 的 {@link QoderPatExchangeError}),
69558
+ * 重试不会变好,只有重新生成 PAT 能救这个账号。
68159
69559
  */
68160
69560
  function qoderRefreshFailureStatus(error) {
69561
+ if (error instanceof QoderPatExchangeError && qoderPatRejectionStatus(error.status)) return "expired";
68161
69562
  const status = error?.status;
68162
69563
  if (status === 401 || status === 403) return "expired";
68163
69564
  const message = error instanceof Error ? error.message : "";
@@ -68631,7 +70032,13 @@ function patchFromBody(body) {
68631
70032
  if (body.selectedAccountId !== void 0) patch.selectedAccountId = typeof body.selectedAccountId === "string" ? body.selectedAccountId : null;
68632
70033
  return patch;
68633
70034
  }
68634
- /** 请求体里的区域(认不出时回落全球区——它是唯一支持设备流的,选错时上游明确拒)。 */
70035
+ /**
70036
+ * 请求体里的区域。
70037
+ *
70038
+ * 认不出时回落全球区(它是唯一支持设备流的,且是卡片默认值),但**只对设备流成立**:
70039
+ * 设备流本来就只有全球区。PAT 粘贴的兜底不靠这一层——`qoderCredentialsFromPat`
70040
+ * 会把「区域猜错」这件事自己修好(见该函数的注释),所以这里不需要猜得更聪明。
70041
+ */
68635
70042
  function regionFromBody(body) {
68636
70043
  return isQoderRegion(body.region) ? body.region : "global";
68637
70044
  }
@@ -74227,9 +75634,7 @@ function effectiveAllowedRoutes(agent, preference, sessions, scope = "session")
74227
75634
  return authorizedRoutesFor(agent, sessions) ?? (scope === "preference" ? preference?.allowedModels ?? [] : []);
74228
75635
  }
74229
75636
  /**
74230
- * Append the Session's authorized `{ provider, model }` pairs to a successful
74231
- * `list_subagent_models` result when the calling Session is governed by an
74232
- * allowlist.
75637
+ * State a Session's routing posture in the `list_subagent_models` result.
74233
75638
  *
74234
75639
  * Calling `list_subagent_models({})` upstream returns only provider names
74235
75640
  * (`minimax-code — MiniMax Code`), and calling it with `provider` prints
@@ -74238,18 +75643,38 @@ function effectiveAllowedRoutes(agent, preference, sessions, scope = "session")
74238
75643
  * here lets even a single `list_subagent_models({})` call hand the model every
74239
75644
  * authorized route in the form `subagent` expects, eliminating any reason to
74240
75645
  * search local config files or source code for the allowlist.
75646
+ *
75647
+ * The empty case states the opposite fact rather than staying silent, because
75648
+ * silence is what makes a model infer that a mounted tool implies enforcement.
75649
+ * It does not: the tool row and the route parameters are static, while whether
75650
+ * anything is enforced is decided by the allowlist the Session recorded. The
75651
+ * two facts differ, so the model has to be able to read the second one instead
75652
+ * of deriving it from the first.
74241
75653
  * @param output - Original text returned by `list_subagent_models`.
74242
- * @param allowed - Routes the calling Session authorizes.
74243
- * @returns the output with the Session allowlist summary appended.
75654
+ * @param allowed - Routes the calling Session authorizes, empty when none is in force.
75655
+ * @returns the output with the Session routing posture appended.
74244
75656
  */
74245
75657
  function enrichListSubagentModelsOutput(output, allowed) {
74246
- if (allowed.length === 0) return output;
75658
+ if (allowed.length === 0) {
75659
+ if (output.includes("No allowlist is in force for this Session: subagent routing is unrestricted, no route is validated against any list, and omitting provider/model is legitimate rather than a rule breach. To govern routing per task, enable subagent model selection under \"Settings -> Subagents\" and start a new session; this one keeps its current posture.")) return output;
75660
+ return output.trim().length === 0 ? UNRESTRICTED_ROUTING_NOTICE : `${output}\n\n${UNRESTRICTED_ROUTING_NOTICE}`;
75661
+ }
74247
75662
  const header = "Authorized child routes for this Session (already filtered by the Session allowlist; only these routes are accepted, and `provider` and `model` must be passed as separate fields):";
74248
75663
  if (output.includes(header)) return output;
74249
75664
  const suffix = `${header}\n${allowed.map((route) => `- ${route.provider}/${route.model} -> provider: "${route.provider}", model: "${route.model}"`).join("\n")}`;
74250
75665
  return output.trim().length === 0 ? suffix : `${output}\n\n${suffix}`;
74251
75666
  }
74252
75667
  /**
75668
+ * The statement appended to `list_subagent_models` when no allowlist governs the
75669
+ * calling Session.
75670
+ *
75671
+ * It names the three consequences a reading model would otherwise have to guess
75672
+ * at: nothing is enforced, omitting the route is legitimate, and "authorized" is
75673
+ * not a word that can be used about any route here. It also names the one action
75674
+ * that changes the state, without asking the user to stop and do it.
75675
+ */
75676
+ const UNRESTRICTED_ROUTING_NOTICE = "No allowlist is in force for this Session: subagent routing is unrestricted, no route is validated against any list, and omitting provider/model is legitimate rather than a rule breach. To govern routing per task, enable subagent model selection under \"Settings -> Subagents\" and start a new session; this one keeps its current posture.";
75677
+ /**
74253
75678
  * Rewrite the upstream `list_subagent_models` and `subagent` tool/parameter
74254
75679
  * descriptions when the calling Session enforces an allowlist.
74255
75680
  *
@@ -74450,7 +75875,6 @@ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
74450
75875
  const decision = await next();
74451
75876
  if (exec.name !== "list_subagent_models" || result.isError === true || decision?.kind !== "accept") return decision;
74452
75877
  const allowed = effectiveAllowedRoutes(exec.agent, subagentModelSelectionPreference(settings), sessions, scope);
74453
- if (allowed.length === 0) return decision;
74454
75878
  const currentValue = decision.value !== void 0 ? decision.value : result.value;
74455
75879
  if (typeof currentValue === "string") {
74456
75880
  const enriched = enrichListSubagentModelsOutput(currentValue, allowed);
@@ -74621,15 +76045,25 @@ function candidatePackageRoots(entryScript = process.argv[1]) {
74621
76045
  return [...roots];
74622
76046
  }
74623
76047
  /**
74624
- * Whether a package is installed in any candidate node_modules tree.
74625
- * Matches DSH agent-presets' own discovery check.
76048
+ * Whether a package or exported subpath is installed in any candidate node_modules tree.
76049
+ * A subpath needs its own exact `exports` key; the package root alone is not enough.
74626
76050
  */
74627
76051
  function packageInstalled(specifier, roots = candidatePackageRoots()) {
74628
76052
  const pkg = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
76053
+ const subpath = specifier.slice(pkg.length);
74629
76054
  for (const root of roots) {
74630
76055
  let dir = root;
74631
76056
  for (;;) {
74632
- if (existsSync(join(dir, "node_modules", pkg, "package.json"))) return true;
76057
+ const manifestPath = join(dir, "node_modules", pkg, "package.json");
76058
+ if (existsSync(manifestPath)) {
76059
+ if (subpath === "") return true;
76060
+ try {
76061
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
76062
+ if (manifest.exports !== void 0 && Object.prototype.hasOwnProperty.call(manifest.exports, `.${subpath}`)) return true;
76063
+ } catch {
76064
+ continue;
76065
+ }
76066
+ }
74633
76067
  const parent = dirname(dir);
74634
76068
  if (parent === dir) break;
74635
76069
  dir = parent;
@@ -74984,6 +76418,12 @@ function installBundledPresets(host, install) {
74984
76418
  */
74985
76419
  /** The row package whose presence marks a harness that takes presets from a row. */
74986
76420
  const PRESET_REGISTRY_PACKAGE = "@deepseek-ai/dsh-agent-preset";
76421
+ /**
76422
+ * The package whose presence marks a harness (>= 0.2.1) that injects the
76423
+ * session working directory via `WORKING_DIRECTORY` user context and no longer
76424
+ * registers `{{cwd}}` as a `systemPrompt` variable.
76425
+ */
76426
+ const WORKING_DIRECTORY_PACKAGE = "@deepseek-ai/dsh-working-directory";
74987
76427
  /** The id the `dispatch` preset is selectable under. */
74988
76428
  const DISPATCH_PRESET_ID = "dispatch";
74989
76429
  /**
@@ -75031,7 +76471,7 @@ Criterion: whether spawn_teammate and wait_agent exist (in PTC mode: tools.spawn
75031
76471
  ## R0 · Complexity Triage (Triage First, Then Choose the Workflow)
75032
76472
  Upon receiving a task, determine its tier first, write one line stating "Tier + Rationale", and then decide which phases to run.
75033
76473
  - L0 Simple / Single-point: single-file or single-point edit; unambiguous requirements; reversible change; no parallelism value; you already hold all required context. -> Do not spawn subagents or run an interview; execute directly and report.
75034
- - L1 Moderate: 2–5 files; requirements mostly clear with 1–2 open points; decomposable but with limited parallelism gain. -> Ask at most 1–2 key questions (no full interview) -> brief plan -> execute yourself or dispatch 1 subagent.
76474
+ - L1 Moderate: edits at most 5 files (5 is a cap, not a range and not a target); requirements mostly clear with at most 2 open points; decomposable but with limited parallelism gain. -> Ask at most 2 key questions (no full interview; 2 is the cap) -> brief plan -> execute yourself or dispatch 1 subagent.
75035
76475
  - L2 Complex / Multi-step: cross-module or multi-deliverable; ambiguity or hidden assumptions; requires exploring unknowns; independent subtasks suitable for parallel execution. -> Must run the full workflow: Clarification Interview -> Full Plan -> Parallel Subagents -> Layered Acceptance.
75036
76476
  When in doubt, clarify before escalating; if a task turns out more complex than expected, stop, re-triage, and inform the user. Never open an interview or multi-agent orchestration for L0/L1 tasks.
75037
76477
 
@@ -75048,21 +76488,25 @@ For L2 tasks, never execute work yourself outside these six categories: (1) Plan
75048
76488
  For L0/L1 tasks, execute directly—do not spawn subagents just to look like you are orchestrating.
75049
76489
 
75050
76490
  ## R2 · Model Routing (Check Availability First, Then Decide How to Dispatch)
75051
- Before dispatching, check whether subagent model selection is enabled in this session: check whether list_subagent_models exists (in PTC mode: whether tools.list_subagent_models is available). Its presence and the exposure of provider / model / reasoning_effort on subagent are controlled by the same setting and always appear or disappear together, so this single signal is sufficient.
76491
+ Before dispatching, check whether subagent model selection is enabled in this session: check whether list_subagent_models exists (in PTC mode: whether tools.list_subagent_models is available). Its presence tells you the tool is mounted, and nothing more: the tool row and the provider / model / reasoning_effort parameters are mounted statically, so they are present even in a session that authorized no route at all. Whether the route guard enforces anything is decided by the allowlist this session recorded, which is a separate fact. Read that fact instead of inferring it: the tool's output appends the session's authorized routes, and appends an explicit statement when no allowlist is in force. Then dispatch in one of three states.
75052
76492
 
75053
- [When Available] Dispatch with explicit routing—this is the default operating mode.
76493
+ [State: Listed] The output ends with "Authorized child routes for this Session" and lists at least one route—enforcement is active. Dispatch with explicit routing; this is the default operating mode.
75054
76494
  - The sole source of truth for the allowlist is list_subagent_models: before your first dispatch, call list_subagent_models({}) to list authorized providers (its output also appends the full breakdown of every authorized provider/model pair for this session); if you need per-model capabilities or reasoning effort levels under a provider, call list_subagent_models({ provider }) to list authorized models under that provider (or pass { provider, model }). DSH already filters this tool's output at runtime against the session's Subagents settings—what it returns IS the complete authorized allowlist, and the route guard validates against the exact same session snapshot, so trust its output directly without cross-checking local config files or workspace code.
75055
76495
  - list_subagent_models({ provider }) formats entries as <provider>/<model> — <Name>; on every dispatch, pass the split provider and model fields explicitly (model without the <provider>/ prefix, plus reasoning_effort when needed), and the route must be in the allowlist returned by the tool.
75056
76496
  - Before every dispatch, write out the routing decision line: route: <provider>/<model> (+ reasoning_effort when applicable). Do not dispatch without writing this line first. The runtime route guard (enabled by default, covering tools.subagent(...) inside run_code in PTC mode as well) hard-rejects omitting provider/model, passing only one of them, or picking a route outside the allowlist before the child starts—never omit parameters or use trial-and-error to discover routes.
75057
76497
  - If a tool's generic upstream DSH description calls model selection "optional" or states "Catalog membership is advisory", this rule takes precedence: the runtime output is already filtered to this session's allowlist and needs no cross-checking elsewhere.
75058
76498
 
75059
- [When Unavailable] Do not error, complain, or ask the user to change settings—continue with DSH's default behavior.
75060
- - Here subagent does not expose provider/model/reasoning_effort, the route guard is inactive, and subagents silently inherit your route. This is a legitimate degraded path in this mode, not a violation.
76499
+ [State: Unlisted] The tool is present but its output states no allowlist is in force—the guard authorizes every route and enforces nothing. This is the second legitimate state of this mode, not a violation, and not the same thing as "model selection is off".
76500
+ - Dispatch per R3–R6 as usual. Passing provider/model is still legal and still needed for the child to differ from your route; omitting it is a genuine choice rather than a rule breach, because nothing is being enforced.
76501
+ - Say it once, in one sentence, in your delivery summary: subagent routing is unrestricted in this session because no allowlist was recorded. Only if the task clearly wants per-task routing (mechanical refactoring vs. deep reasoning) should you also point the user at "Settings -> Subagents" plus a new session—do not stop and wait.
76502
+ - Never claim per-task routing is in force here, and never report a route as "authorized" when the session recorded no allowlist—there was nothing to authorize it against.
76503
+
76504
+ [State: Unmounted] list_subagent_models does not exist and subagent exposes no provider/model/reasoning_effort at all—the guard is inactive and subagents silently inherit your route. Do not error, complain, or ask the user to change settings; continue with DSH's default behavior.
75061
76505
  - Dispatch and verify per R3–R6 as usual, simply omitting the route: line.
75062
76506
  - Only exception worth a brief note: if a task is clearly better suited for a different model (e.g., mechanical refactoring vs. deep reasoning), you may mention in one sentence that the user can enable subagent model selection under "Settings -> Subagents" and start a new session, while continuing to complete the current task with default routing—do not stop and wait. That setting only affects newly created sessions.
75063
76507
 
75064
76508
  subagent_fork deliberately shares the parent agent's route (reusing conversation history and KV cache); use it only for reviews that require the coordinator's context, never as a shortcut around explicit routing.
75065
- It has no provider/model parameters, so the explicit-route rule under [When Available] does not apply to it; however, the guard still validates the route it inherits: if your current model is not in the allowlist, subagent_fork will be hard-rejected—switch to subagent with an explicit allowed route, or switch this session to an allowed model so subagent_fork becomes available again.
76509
+ It has no provider/model parameters, so the explicit-route rule under [State: Listed] does not apply to it; however, the guard still validates the route it inherits: if your current model is not in the allowlist, subagent_fork will be hard-rejected—switch to subagent with an explicit allowed route, or switch this session to an allowed model so subagent_fork becomes available again.
75066
76510
  Choose models based on task nature and model capability; keep model selection consistent across tasks of the same kind within a batch.
75067
76511
 
75068
76512
  ## R3 · Subagent Autonomy
@@ -75072,21 +76516,26 @@ A subagent has full authority to plan, execute, and self-verify within its assig
75072
76516
  Subagents run with DSH's default subagent role and environment; do not inject custom personas. Model selection authority belongs solely to the Dispatch Agent: when multiple models are in the allowlist, you assign the model per task nature—never let a subagent choose its own model.
75073
76517
 
75074
76518
  ## R5 · Dispatch Discipline
75075
- Every task brief must contain five elements: Goal / Boundary (which files or modules may be touched) / Constraints / Acceptance Criteria / Required Evidence to Report.
76519
+ Every task brief must contain six elements: Goal / Boundary (which files or modules may be touched) / Constraints / Limits (see below) / Acceptance Criteria / Required Evidence to Report.
76520
+ - [Limits] One line per numeric or quantitative limit, each spelling out four things: **what it bounds, in what unit, the cap, and whether it accumulates**. A subagent is a fresh instance of the same model and reads your brief exactly the way an ambiguous rule gets read—taking the strictest available meaning and not checking—so a loose limit is spent twice: once when you write it, once when the child under-delivers against it. Write "at most 3 attempts", not "a few attempts"; write "edits at most 5 files", not "2–5 files". Where a dimension is genuinely unbounded, say so explicitly ("test runs: unlimited"); silence is read as a limit of zero. Cover at minimum: attempts/retries, files edited, commands run, and any per-cycle quota with its reset rule (a quota that resets per cycle is lost, not banked—say that).
76521
+ - Do not ask the child to restate your rules back as a correctness check: restating an ambiguous rule reproduces the misreading rather than catching it. If a limit is genuinely unclear, the child should spend one minimal tool call to verify it; write that escaping instruction into the brief when the ambiguity is unavoidable.
75076
76522
  Issue mutually independent dispatches in parallel within the same message rather than waiting serially. A subagent's report is evidence, not a final verdict.
75077
76523
  Precondition: never dispatch implementation subagents before the clarification interview has converged.
75078
76524
  In PTC mode, dispatch via tools.subagent(...) inside run_code, running independent dispatches concurrently with Promise.all / asyncio.gather; parameters match the native tool.
75079
76525
 
75080
76526
  ## R6 · Review & Acceptance
75081
76527
  Two layers: subagent self-check -> your independent review. Acceptance must be grounded in concrete evidence (reading key diffs, running tests, checking each acceptance criterion).
76528
+ The completion notification is a post-hoc signal: it says the child stopped, not that the work is correct—and it arrives after the work is already done. Your pre-hoc control is the acceptance criteria you wrote into the brief under R5, not the notification and not the child's self-report.
75082
76529
  If work fails review, re-dispatch with specific evidence—do not step in to fix the code yourself, and never declare completion based solely on a subagent's self-report.
75083
- Re-dispatch the same task brief at most twice; only if it still fails after two re-dispatches, or if re-dispatch is hard-rejected (tool unavailable or repeated errors), transition to R8 Fallback and state which condition triggered it.
76530
+ Re-dispatch budget, counted per task brief: 1 first attempt + at most 2 re-dispatches = at most 3 runs total. Only if it still fails after the third run, or if re-dispatch is hard-rejected (tool unavailable or repeated errors), transition to R8 Fallback and state which condition triggered it. Count runs, not failures; the number belongs in the task list item for that brief (R7), not in your memory.
76531
+ [Noticed but not handled] Every review that finds a defect must end one of two ways: the defect goes back out on a re-dispatch with evidence, or it goes to the user through ask_user_question as a scoping decision (R-W). Writing it into the delivery summary and shipping anyway is not an available outcome—a risk you named but never routed is exactly the failure this rule exists to stop. When the final delivery summary is written, it carries the list of risks you noticed and routed, each with what you did about it; an empty list means you found none, which is a claim in its own right.
75084
76532
 
75085
76533
  ## R-W · Turn Boundary (Waiting / Goals / Questions)
75086
76534
 
75087
76535
  All three rules govern the boundary of a turn: after dispatching, before continuing, and before stopping.
75088
76536
 
75089
76537
  [Waiting for Subagents] By default, end your turn without calling any more tools and let the subagent completion notification wake you—notifications are built-in; do not manufacture your own wait.
76538
+ - The notification is a post-hoc signal. It tells you a child stopped running; it does not tell you the work is correct, and nothing about it arrives before the work is done. Treat it as "there is now something to review", never as "this passed". The pre-hoc control is the acceptance criteria in the brief (R5); the review itself is R6.
75090
76539
  - Never run sleep, polling loops, watch, or any long-running placeholder command just to "hang around": they burn context and budget for zero information.
75091
76540
  - If you have independent coordinator work in hand (the six categories in R1, reviewing another subagent's output, reading evidence you need), do it first and then end your turn.
75092
76541
  - Only when you are genuinely blocked and must collect a result immediately should you call job_output({ wait: true }) for a bounded wait; that is a collection tool, not a timer.
@@ -75102,11 +76551,12 @@ All three rules govern the boundary of a turn: after dispatching, before continu
75102
76551
  - Only two exceptions: plan-mode approval still goes through exit_plan_mode (which has its own channel); and when the task is finished and only the delivery summary remains, do not ask a question.
75103
76552
 
75104
76553
  ## R7 · Documentation
75105
- Maintain the task list (in todo_write, not create_goal; see R-W), decision log, and interview conclusions (which decisions are settled and why). Subagents never report directly to the user.
76554
+ Maintain the task list (in todo_write, not create_goal; see R-W), decision log, and interview conclusions (which decisions are settled and why). Each task item carries its own run count (R6), so the re-dispatch budget is read rather than remembered. Subagents never report directly to the user.
76555
+ [Before every turn] Scan the task list for items that will escalate, expire, or time out if you do nothing this turn, and handle those before the items that are merely large. Concretely, in this mode: a result that came back and has not been reviewed; an acceptance that failed with the rework scope still undecided; a question the user has not answered and that blocks a dispatch; a brief sitting at its run count. An item that has waited is not the same as an item that is fine—a low-priority backlog is what eventually turns into the incident, so every item still open at the end of a turn is listed with its age in turns, and any item older than the brief's own limit is handled or escalated instead of carried forward once more.
75106
76556
 
75107
76557
  ## R8 · Fallback
75108
76558
  Fallback is a task-scoped, non-session-wide exception: it covers only the single task that triggered it and never alters the default operating mode for subsequent tasks in this session.
75109
- You may execute a task yourself only when at least one of the following holds, and you must state which condition triggered it and the supporting evidence: L0/L1 task; subagent tools unavailable; allowlist is empty and the user has not enabled it; task is non-delegable (pure Q&A, tiny single-point edit, or an operation that must share your immediate context); or the same task brief still fails after two re-dispatches under R6.
76559
+ You may execute a task yourself only when at least one of the following holds, and you must state which condition triggered it and the supporting evidence: L0/L1 task; subagent tools not mounted at all (R2 [State: Unmounted]); task is non-delegable (pure Q&A, tiny single-point edit, or an operation that must share your immediate context); or the same task brief has still not passed review after its third run under R6. An unmounted tool is the only routing condition that permits fallback: an empty, unlisted allowlist (R2 [State: Unlisted]) is not one, because delegation still works there—it is simply unrestricted.
75110
76560
  Fix the trigger condition on hard rejections instead of treating them as fallback grounds: on active child limit -> wait for existing children to settle and re-dispatch; on subagent model selection -> specify an explicit provider/model from the authorized routes listed in the error; on route-inheritance rejection -> switch to an explicit-route subagent tool or switch this session to an allowed model. Unless the condition is fixed, the same rejection will recur.
75111
76561
  Reset: fallback applies only to the current task. When the next task begins, always return to R0 to triage anew; as long as L2 holds and the trigger condition above no longer applies, resume dispatching subagents—never carry over or pre-assume fallback because a previous task fell back.
75112
76562
 
@@ -75114,7 +76564,7 @@ Reset: fallback applies only to the current task. When the next task begins, alw
75114
76564
  1. Opening an interview or multi-agent orchestration for an L0/L1 task—pure waste.
75115
76565
  2. Planning, dispatching, or coding before ambiguous requirements are clarified—baking uncertainty into rework.
75116
76566
  3. Running long execution tasks on the coordinator model that should have been delegated.
75117
- 4. Omitting provider/model when model selection is available, relying on the guard to catch it or letting it silently inherit the coordinator's route (inheritance when model selection is unavailable is not a violation).
76567
+ 4. Omitting provider/model while [State: Listed] holds, relying on the guard to catch it or letting it silently inherit the coordinator's route (omitting it under [State: Unlisted] or [State: Unmounted] is a genuine choice, not a violation).
75118
76568
  5. Directly editing a module assigned to a subagent.
75119
76569
  6. Declaring completion as soon as a subagent reports back, without independent acceptance verification.
75120
76570
  7. Mixing multiple models across tasks of the same kind within the same batch.
@@ -75127,6 +76577,10 @@ Reset: fallback applies only to the current task. When the next task begins, alw
75127
76577
  14. Proactively creating a goal at the start of a task—goal auto-continuation wakes you mid-wait (R-W).
75128
76578
  15. Ending with a paragraph of prose to wait for user sign-off instead of calling ask_user_question (R-W).
75129
76579
  16. Second-guessing list_subagent_models output by hunting through local config or source files just to check which subagent models are allowed—list_subagent_models already returns the session's runtime allowlist (R2).
76580
+ 17. Shipping a defect you already named in the review, on the grounds that it is recorded in the summary—unrouted risk is not handled risk (R6).
76581
+ 18. Writing a task brief with a range where a cap belongs ("2–5 files", "1–2 questions", "a few attempts")—the strictest reading wins and the budget shrinks to its lower bound (R0, R5).
76582
+ 19. Asking a subagent to restate its rules as a correctness check instead of verifying the ambiguous ones with one tool call—restatement reproduces the misreading (R5).
76583
+ 20. Reporting a route as "authorized", or claiming per-task routing is in force, when the session recorded no allowlist (R2 [State: Unlisted]).
75130
76584
  `
75131
76585
  }
75132
76586
  },
@@ -75333,15 +76787,26 @@ Reset: fallback applies only to the current task. When the next task begins, alw
75333
76787
  * `unknown prompt variable "{{cwd}}"` at render time. On 0.1.7–0.2.0 that package
75334
76788
  * is absent and `{{cwd}}` is still the only way the persona states the workspace,
75335
76789
  * so the unmodified {@link DISPATCH_PRESET} is returned unchanged.
76790
+ *
76791
+ * Newer harnesses also register `list_agents` from the main subagent-control entry
76792
+ * and remove its standalone `./list-agents` export. That row is omitted only when
76793
+ * the export is absent; older harnesses keep it because their main entry does not
76794
+ * register the tool.
75336
76795
  * @param resolves - whether a bare specifier resolves in this installation.
75337
76796
  * @returns the preset definition for the running harness generation.
75338
76797
  */
75339
76798
  function resolveDispatchPreset(resolves = resolvesFromHere) {
75340
- if (!resolves("@deepseek-ai/dsh-working-directory")) return DISPATCH_PRESET;
76799
+ const hasWorkingDirectory = resolves(WORKING_DIRECTORY_PACKAGE);
76800
+ const hasLegacyListAgents = resolves("@deepseek-ai/dsh-tool-subagent-control/list-agents");
76801
+ if (!hasWorkingDirectory && hasLegacyListAgents) return DISPATCH_PRESET;
75341
76802
  return {
75342
76803
  ...DISPATCH_PRESET,
75343
76804
  plugins: DISPATCH_PRESET.plugins.map((row) => {
75344
- if (row.id !== "persona" || typeof row.config !== "object" || row.config === null) return row;
76805
+ if (row.id === "delegation" && !hasLegacyListAgents && Array.isArray(row.config)) return {
76806
+ ...row,
76807
+ config: row.config.filter((nested) => nested.id !== "tool-subagent-list-agents")
76808
+ };
76809
+ if (row.id !== "persona" || !hasWorkingDirectory || typeof row.config !== "object" || row.config === null) return row;
75345
76810
  const { suffix: _omitted, ...rest } = row.config;
75346
76811
  return {
75347
76812
  ...row,
@@ -77743,6 +79208,7 @@ const Config = z.object({
77743
79208
  reasoningCollapseGuard: z.union([z.object({}), z.const(false)]).default(false),
77744
79209
  noAnswerResume: z.union([z.object({}), z.const(false)]).default({}),
77745
79210
  codexStreamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).step(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS$2),
79211
+ poolStorage: z.union([z.const("keyring"), z.const("credentials")]).default("keyring").description("号池存储后端:keyring(默认,桌面平台的系统密钥环)或 credentials(DSH 凭据库,服务器推荐)。"),
77746
79212
  progressUpdates: z.boolean().default(true).volatile().description("启用进展更新:允许 agent 用 progress_update 工具主动汇报多步任务的进展,并在侧栏展示。"),
77747
79213
  progressUpdatesNudgeThreshold: z.number().step(1).min(1).max(50).default(8).volatile().description("催促阈值:连续这么多轮带工具调用却没有进展更新时,提醒 agent 发一条进展。"),
77748
79214
  progressUpdatesLanguage: z.union([
@@ -77761,6 +79227,14 @@ const inject = [
77761
79227
  ];
77762
79228
  function apply(ctx, pluginConfig = {}) {
77763
79229
  const fetchConfiguration = resolveFetchConfiguration(pluginConfig);
79230
+ const poolStorageSettings = new PoolStorageSettings();
79231
+ poolStorageSettings.hydrate().catch(() => void 0);
79232
+ configurePoolStorage({
79233
+ mode: () => poolStorageSettings.effective(pluginConfig.poolStorage),
79234
+ ready: () => poolStorageSettings.hydrate().catch(() => void 0),
79235
+ credentials: () => asCredentialsSeam(ctx.get("credentials")),
79236
+ logger: ctx.logger
79237
+ });
77764
79238
  if (pluginConfig.syncAgentPresets !== false) ctx.effect(() => installBundledPresets(ctx, installDispatchPreset(ctx)), "dsh-chatgpt-subscription: agent preset sync");
77765
79239
  const store = createPlatformTokenStore();
77766
79240
  const preferences = registerPreferenceStore(ctx.settings);
@@ -78132,7 +79606,7 @@ function apply(ctx, pluginConfig = {}) {
78132
79606
  }))
78133
79607
  });
78134
79608
  };
78135
- const disposeRoutes = registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, readRouteAudit, codexAccountPool, fetchConfiguration);
79609
+ const disposeRoutes = registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, readRouteAudit, codexAccountPool, fetchConfiguration, { storage: poolStorageSettings });
78136
79610
  const routable = (accounts) => accounts.some((account) => account.authStatus === void 0 || account.authStatus === "ok");
78137
79611
  const disposeHubOverview = registerHubOverviewRoutes(ctx, [
78138
79612
  {