@eddyskywalker/dsh-chatgpt-subscription 0.38.1 → 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.
- package/CHANGELOG.md +130 -1
- package/README.md +45 -8
- package/lib/client.js +805 -151
- package/lib/client.js.map +1 -1
- package/lib/index.js +1908 -532
- package/lib/types/client/CodexSubscriptionSection.d.ts +1 -1
- package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +16 -1
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/cline-pass/ClinePassSection.d.ts.map +1 -1
- package/lib/types/client/cline-pass/api.d.ts +16 -1
- package/lib/types/client/cline-pass/api.d.ts.map +1 -1
- package/lib/types/client/cline-pass/locales.d.ts +144 -18
- package/lib/types/client/cline-pass/locales.d.ts.map +1 -1
- package/lib/types/client/cline-pass/styles.d.ts.map +1 -1
- package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
- package/lib/types/client/kimi-code/locales.d.ts +8 -17
- package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
- package/lib/types/client/locales-ru/cline-pass.d.ts.map +1 -1
- package/lib/types/client/locales-ru/hub.d.ts.map +1 -1
- package/lib/types/client/locales-ru/kimi-code.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +43 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/minimax-code/commands-contract.d.ts +14 -4
- package/lib/types/client/minimax-code/commands-contract.d.ts.map +1 -1
- package/lib/types/client/minimax-code/locales.d.ts +2 -2
- package/lib/types/client/progress-updates/settings-binding.d.ts +8 -1
- package/lib/types/client/progress-updates/settings-binding.d.ts.map +1 -1
- package/lib/types/client/progress-updates/settings-section.d.ts.map +1 -1
- package/lib/types/client/styles.d.ts.map +1 -1
- package/lib/types/compat.d.ts +33 -0
- package/lib/types/compat.d.ts.map +1 -1
- package/lib/types/host/agent-preset.d.ts +5 -0
- package/lib/types/host/agent-preset.d.ts.map +1 -1
- package/lib/types/host/antigravity/account-pool.d.ts.map +1 -1
- package/lib/types/host/antigravity/mapper.d.ts +0 -1
- package/lib/types/host/antigravity/mapper.d.ts.map +1 -1
- package/lib/types/host/antigravity/types.d.ts +1 -2
- package/lib/types/host/antigravity/types.d.ts.map +1 -1
- package/lib/types/host/claude/mapper.d.ts +5 -7
- package/lib/types/host/claude/mapper.d.ts.map +1 -1
- package/lib/types/host/claude/model-catalog.d.ts.map +1 -1
- package/lib/types/host/cline-pass/adapter.d.ts +1 -1
- package/lib/types/host/cline-pass/adapter.d.ts.map +1 -1
- package/lib/types/host/cline-pass/client.d.ts +16 -3
- package/lib/types/host/cline-pass/client.d.ts.map +1 -1
- package/lib/types/host/cline-pass/engine.d.ts +11 -3
- package/lib/types/host/cline-pass/engine.d.ts.map +1 -1
- package/lib/types/host/cline-pass/line.d.ts +1 -1
- package/lib/types/host/cline-pass/line.d.ts.map +1 -1
- package/lib/types/host/cline-pass/protocol.d.ts +18 -0
- package/lib/types/host/cline-pass/protocol.d.ts.map +1 -1
- package/lib/types/host/cline-pass/routes.d.ts +2 -0
- package/lib/types/host/cline-pass/routes.d.ts.map +1 -1
- package/lib/types/host/cline-pass/store.d.ts +2 -0
- package/lib/types/host/cline-pass/store.d.ts.map +1 -1
- package/lib/types/host/cline-pass/token-store.d.ts +11 -0
- package/lib/types/host/cline-pass/token-store.d.ts.map +1 -1
- package/lib/types/host/cline-pass/tools.d.ts.map +1 -1
- package/lib/types/host/cline-pass/types.d.ts +24 -2
- package/lib/types/host/cline-pass/types.d.ts.map +1 -1
- package/lib/types/host/codex-account-pool.d.ts +3 -0
- package/lib/types/host/codex-account-pool.d.ts.map +1 -1
- package/lib/types/host/common/account-pool.d.ts +59 -5
- package/lib/types/host/common/account-pool.d.ts.map +1 -1
- package/lib/types/host/common/credential-read-cache.d.ts +11 -1
- package/lib/types/host/common/credential-read-cache.d.ts.map +1 -1
- package/lib/types/host/common/credentials-compat.d.ts +73 -0
- package/lib/types/host/common/credentials-compat.d.ts.map +1 -0
- package/lib/types/host/common/pool-storage-settings.d.ts +41 -0
- package/lib/types/host/common/pool-storage-settings.d.ts.map +1 -0
- package/lib/types/host/common/pool-storage.d.ts +143 -0
- package/lib/types/host/common/pool-storage.d.ts.map +1 -0
- package/lib/types/host/credential-store-secret-service.d.ts +27 -0
- package/lib/types/host/credential-store-secret-service.d.ts.map +1 -1
- package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
- package/lib/types/host/kimi-code/cache-hint.d.ts +29 -15
- package/lib/types/host/kimi-code/cache-hint.d.ts.map +1 -1
- package/lib/types/host/kimi-code/mapper.d.ts +6 -12
- package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
- package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
- package/lib/types/host/kimi-code/token-store.d.ts +1 -11
- package/lib/types/host/kimi-code/token-store.d.ts.map +1 -1
- package/lib/types/host/kimi-code/types.d.ts +0 -13
- package/lib/types/host/kimi-code/types.d.ts.map +1 -1
- package/lib/types/host/oauth-service.d.ts +83 -1
- package/lib/types/host/oauth-service.d.ts.map +1 -1
- package/lib/types/host/preset-sync.d.ts +2 -2
- package/lib/types/host/preset-sync.d.ts.map +1 -1
- package/lib/types/host/qoder/account-pool.d.ts +4 -0
- package/lib/types/host/qoder/account-pool.d.ts.map +1 -1
- package/lib/types/host/qoder/client.d.ts +24 -2
- package/lib/types/host/qoder/client.d.ts.map +1 -1
- package/lib/types/host/qoder/oauth.d.ts +17 -0
- package/lib/types/host/qoder/oauth.d.ts.map +1 -1
- package/lib/types/host/qoder/routes.d.ts.map +1 -1
- package/lib/types/host/routes.d.ts +10 -0
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/host/subagent-model-authorization.d.ts +20 -5
- package/lib/types/host/subagent-model-authorization.d.ts.map +1 -1
- package/lib/types/host/token-store-linux.d.ts.map +1 -1
- package/lib/types/host/token-store.d.ts +16 -0
- package/lib/types/host/token-store.d.ts.map +1 -1
- package/lib/types/index.d.ts +26 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/shared/cline-pass-contracts.d.ts +48 -2
- package/lib/types/shared/cline-pass-contracts.d.ts.map +1 -1
- package/lib/types/shared/contracts.d.ts +86 -0
- package/lib/types/shared/contracts.d.ts.map +1 -1
- package/lib/types/shared/kimi-code-contracts.d.ts +0 -16
- package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
- package/package.json +37 -37
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
1655
|
-
return
|
|
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.
|
|
1676
|
-
|
|
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
|
-
|
|
1700
|
-
|
|
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
|
-
/**
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2633
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
@@ -24588,10 +25487,6 @@ function maxOutputTokensFor$4(modelId, contextWindow) {
|
|
|
24588
25487
|
const capped = (contextWindow ?? kimiCodeModelDef(modelId)?.contextWindow ?? 262144) - CONTEXT_HEADROOM_TOKENS$1;
|
|
24589
25488
|
return Math.max(declared, capped);
|
|
24590
25489
|
}
|
|
24591
|
-
/** Whether a stored preference names a tier this line can send. */
|
|
24592
|
-
function isKimiCacheTtl(value) {
|
|
24593
|
-
return value === "5m" || value === "1h";
|
|
24594
|
-
}
|
|
24595
25490
|
/**
|
|
24596
25491
|
* Reduce the requested output cap so prompt + output fit the window.
|
|
24597
25492
|
*
|
|
@@ -24641,8 +25536,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
24641
25536
|
enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$4],
|
|
24642
25537
|
catalogModels: [],
|
|
24643
25538
|
contextWindowOverrides: {},
|
|
24644
|
-
defaultReasoningEffort: null
|
|
24645
|
-
cacheTtl: null
|
|
25539
|
+
defaultReasoningEffort: null
|
|
24646
25540
|
};
|
|
24647
25541
|
fallbackStore.read().then((stored) => {
|
|
24648
25542
|
snapshot = stored;
|
|
@@ -24659,12 +25553,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
24659
25553
|
enabled: z.boolean().default(true),
|
|
24660
25554
|
enabledModelIds: z.array(z.string()).default([...DEFAULT_ENABLED_MODEL_IDS$4]),
|
|
24661
25555
|
contextWindowOverrides: z.dict(z.number()).default({}),
|
|
24662
|
-
defaultReasoningEffort: z.union([...KIMI_CODE_REASONING_EFFORTS.map((effort) => z.const(effort)), z.const(null)]).default(null)
|
|
24663
|
-
cacheTtl: z.union([
|
|
24664
|
-
z.const("5m"),
|
|
24665
|
-
z.const("1h"),
|
|
24666
|
-
z.const(null)
|
|
24667
|
-
]).default(null)
|
|
25556
|
+
defaultReasoningEffort: z.union([...KIMI_CODE_REASONING_EFFORTS.map((effort) => z.const(effort)), z.const(null)]).default(null)
|
|
24668
25557
|
}));
|
|
24669
25558
|
return {
|
|
24670
25559
|
status: () => {
|
|
@@ -24674,8 +25563,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
24674
25563
|
enabledModelIds: value.enabledModelIds,
|
|
24675
25564
|
catalogModels: [],
|
|
24676
25565
|
contextWindowOverrides: value.contextWindowOverrides,
|
|
24677
|
-
defaultReasoningEffort: value.defaultReasoningEffort
|
|
24678
|
-
cacheTtl: isKimiCacheTtl(value.cacheTtl) ? value.cacheTtl : null
|
|
25566
|
+
defaultReasoningEffort: value.defaultReasoningEffort
|
|
24679
25567
|
};
|
|
24680
25568
|
},
|
|
24681
25569
|
update: async (patch) => {
|
|
@@ -24684,8 +25572,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
24684
25572
|
enabled: patch.enabled !== void 0 ? patch.enabled : current.enabled !== false,
|
|
24685
25573
|
enabledModelIds: patch.enabledModelIds ?? current.enabledModelIds,
|
|
24686
25574
|
contextWindowOverrides: patch.contextWindowOverrides ? mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) : current.contextWindowOverrides,
|
|
24687
|
-
defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort
|
|
24688
|
-
cacheTtl: patch.cacheTtl !== void 0 ? patch.cacheTtl : current.cacheTtl
|
|
25575
|
+
defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort
|
|
24689
25576
|
};
|
|
24690
25577
|
await scope.update(normalized);
|
|
24691
25578
|
fallbackStore.updateSettings(patch).catch(() => void 0);
|
|
@@ -24879,8 +25766,7 @@ var FileModelSettingsStore$3 = class {
|
|
|
24879
25766
|
enabledModelIds,
|
|
24880
25767
|
catalogModels,
|
|
24881
25768
|
contextWindowOverrides,
|
|
24882
|
-
defaultReasoningEffort
|
|
24883
|
-
cacheTtl: isKimiCacheTtl(record.cacheTtl) ? record.cacheTtl : null
|
|
25769
|
+
defaultReasoningEffort
|
|
24884
25770
|
};
|
|
24885
25771
|
}
|
|
24886
25772
|
} catch {}
|
|
@@ -24889,8 +25775,7 @@ var FileModelSettingsStore$3 = class {
|
|
|
24889
25775
|
enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$4],
|
|
24890
25776
|
catalogModels: [],
|
|
24891
25777
|
contextWindowOverrides: {},
|
|
24892
|
-
defaultReasoningEffort: null
|
|
24893
|
-
cacheTtl: null
|
|
25778
|
+
defaultReasoningEffort: null
|
|
24894
25779
|
};
|
|
24895
25780
|
}
|
|
24896
25781
|
async write(settings) {
|
|
@@ -24906,8 +25791,7 @@ var FileModelSettingsStore$3 = class {
|
|
|
24906
25791
|
...patch.enabled !== void 0 ? { enabled: patch.enabled } : {},
|
|
24907
25792
|
...patch.enabledModelIds !== void 0 ? { enabledModelIds: patch.enabledModelIds } : {},
|
|
24908
25793
|
...patch.contextWindowOverrides !== void 0 ? { contextWindowOverrides: mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) } : {},
|
|
24909
|
-
...patch.defaultReasoningEffort !== void 0 ? { defaultReasoningEffort: patch.defaultReasoningEffort } : {}
|
|
24910
|
-
...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 } : {}
|
|
24911
25795
|
};
|
|
24912
25796
|
await this.write(next);
|
|
24913
25797
|
return next;
|
|
@@ -24950,20 +25834,6 @@ async function persistRegion(region) {
|
|
|
24950
25834
|
await fs.writeFile(file, region, "utf8");
|
|
24951
25835
|
} catch {}
|
|
24952
25836
|
}
|
|
24953
|
-
//#endregion
|
|
24954
|
-
//#region src/host/kimi-code/cache-hint.ts
|
|
24955
|
-
/**
|
|
24956
|
-
* How long an entry written at each tier survives.
|
|
24957
|
-
*
|
|
24958
|
-
* The tier is LOCKED when the prefix is first written, so this is the lifetime
|
|
24959
|
-
* of the entry a request actually created rather than of the tier as a name.
|
|
24960
|
-
*/
|
|
24961
|
-
const CACHE_TTL_MS = {
|
|
24962
|
-
"5m": 300 * 1e3,
|
|
24963
|
-
"1h": 3600 * 1e3
|
|
24964
|
-
};
|
|
24965
|
-
/** Tier assumed when the conversation states none. */
|
|
24966
|
-
const DEFAULT_TIER = "5m";
|
|
24967
25837
|
/**
|
|
24968
25838
|
* Last time each session did something, epoch ms.
|
|
24969
25839
|
*
|
|
@@ -25008,9 +25878,8 @@ function evaluateCacheHint(input) {
|
|
|
25008
25878
|
if (lastActiveAt === void 0 || totalTokens === void 0) return { kind: "skip" };
|
|
25009
25879
|
if (!Number.isFinite(lastActiveAt) || !Number.isFinite(totalTokens)) return { kind: "skip" };
|
|
25010
25880
|
if (totalTokens < 1e5) return { kind: "skip" };
|
|
25011
|
-
const tier = input.cacheTtl ?? DEFAULT_TIER;
|
|
25012
25881
|
const idleMs = now - lastActiveAt;
|
|
25013
|
-
if (idleMs <=
|
|
25882
|
+
if (idleMs <= 18e5) return { kind: "skip" };
|
|
25014
25883
|
return {
|
|
25015
25884
|
kind: "hint",
|
|
25016
25885
|
idleMs,
|
|
@@ -27253,39 +28122,6 @@ function thinkingBudgetFor(effort, maxTokens) {
|
|
|
27253
28122
|
const budget = Math.min(effort === "low" ? 2048 : effort === "high" ? 8192 : 16384, maxTokens - 1024);
|
|
27254
28123
|
return budget >= 1024 ? budget : void 0;
|
|
27255
28124
|
}
|
|
27256
|
-
/**
|
|
27257
|
-
* Media a single request build may carry.
|
|
27258
|
-
*
|
|
27259
|
-
* Passed as one object so adding a media kind never grows a positional
|
|
27260
|
-
* signature the existing callers already bind.
|
|
27261
|
-
*/
|
|
27262
|
-
/**
|
|
27263
|
-
* The cache field for the Anthropic-compatible wire.
|
|
27264
|
-
*
|
|
27265
|
-
* TOP LEVEL, not per block: the service documents that a `cache_control` inside
|
|
27266
|
-
* a message is ignored, and omitting the field means "read a 5m entry but do
|
|
27267
|
-
* not write one" — which is why this is only attached when the caller asked
|
|
27268
|
-
* for a tier.
|
|
27269
|
-
*/
|
|
27270
|
-
function kimiAnthropicCacheControl(ttl) {
|
|
27271
|
-
return { cache_control: {
|
|
27272
|
-
type: "ephemeral",
|
|
27273
|
-
ttl
|
|
27274
|
-
} };
|
|
27275
|
-
}
|
|
27276
|
-
/**
|
|
27277
|
-
* The cache field for the OpenAI-compatible wire.
|
|
27278
|
-
*
|
|
27279
|
-
* `mode: 'implicit'` is the only mode the service supports — it identifies the
|
|
27280
|
-
* prefix itself, so this is not a request to break the cache but a request to
|
|
27281
|
-
* write it at a chosen TTL.
|
|
27282
|
-
*/
|
|
27283
|
-
function kimiOpenAICacheOptions(ttl) {
|
|
27284
|
-
return { prompt_cache_options: {
|
|
27285
|
-
mode: "implicit",
|
|
27286
|
-
ttl
|
|
27287
|
-
} };
|
|
27288
|
-
}
|
|
27289
28125
|
function textOf$6(content) {
|
|
27290
28126
|
if (typeof content === "string") return sanitizeText$6(content);
|
|
27291
28127
|
if (!Array.isArray(content)) return "";
|
|
@@ -27672,7 +28508,6 @@ function buildOpenAIRequest(options, images = NO_RESOLVED_IMAGES$3, preserveThin
|
|
|
27672
28508
|
stream: true,
|
|
27673
28509
|
stream_options: { include_usage: true },
|
|
27674
28510
|
max_completion_tokens: maxTokens,
|
|
27675
|
-
...media.cacheTtl === null || media.cacheTtl === void 0 ? {} : kimiOpenAICacheOptions(media.cacheTtl),
|
|
27676
28511
|
...stopSequences(options.stop).length > 0 ? { stop: stopSequences(options.stop) } : {},
|
|
27677
28512
|
...options.tools && options.tools.length > 0 ? {
|
|
27678
28513
|
tools: options.tools.map((tool) => ({
|
|
@@ -27807,7 +28642,6 @@ function mergeAnthropicMessages$2(entries) {
|
|
|
27807
28642
|
}
|
|
27808
28643
|
/** Build one `/v1/messages` body. */
|
|
27809
28644
|
function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {}) {
|
|
27810
|
-
const cacheTtl = media.cacheTtl ?? null;
|
|
27811
28645
|
const toolCallIds = new ToolCallIdNormalizer();
|
|
27812
28646
|
const carriers = options.messages.filter((message) => message.role === "system" && messageToolsOf(message) !== void 0);
|
|
27813
28647
|
const unsentDeclarations = carriers.reduce((total, message) => total + (messageToolsOf(message)?.length ?? 0), 0);
|
|
@@ -27839,7 +28673,6 @@ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {
|
|
|
27839
28673
|
model: options.model,
|
|
27840
28674
|
max_tokens: maxTokens,
|
|
27841
28675
|
messages: mergeAnthropicMessages$2(entries),
|
|
27842
|
-
...cacheTtl === null || cacheTtl === void 0 ? {} : kimiAnthropicCacheControl(cacheTtl),
|
|
27843
28676
|
...system === void 0 ? {} : { system },
|
|
27844
28677
|
...options.temperature === void 0 || budget !== void 0 ? {} : { temperature: options.temperature },
|
|
27845
28678
|
...options.stop && options.stop.length > 0 ? { stop_sequences: options.stop } : {},
|
|
@@ -29120,8 +29953,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
29120
29953
|
const media = {
|
|
29121
29954
|
videos,
|
|
29122
29955
|
videoAccepted: inputModalitiesForEntry(options.model, catalog).includes("video"),
|
|
29123
|
-
messageTools: dynamicToolsForEntry(options.model, catalog)
|
|
29124
|
-
cacheTtl: settings.cacheTtl
|
|
29956
|
+
messageTools: dynamicToolsForEntry(options.model, catalog)
|
|
29125
29957
|
};
|
|
29126
29958
|
const contextWindow = this.contextWindowFor(options.model, entry, settings.contextWindowOverrides);
|
|
29127
29959
|
const requestedMax = options.maxTokens ?? maxOutputTokensFor$4(options.model, contextWindow);
|
|
@@ -29666,12 +30498,11 @@ async function getKimiCodeWebStatus(store, modelSettings, preferences, options =
|
|
|
29666
30498
|
lastFetchedAt: quota?.fetchedAt ?? null,
|
|
29667
30499
|
credentialsRejected: active !== null && active !== void 0 && isRefreshTokenRejected$1(active.refreshToken),
|
|
29668
30500
|
cache: cacheStatsOrNull(options.sessionId),
|
|
29669
|
-
cacheHint: cacheHintOrNull(options.sessionId
|
|
30501
|
+
cacheHint: cacheHintOrNull(options.sessionId),
|
|
29670
30502
|
preserveThinking: preserveThinkingEnabled(),
|
|
29671
30503
|
models,
|
|
29672
30504
|
contextWindowOverrides: settings.contextWindowOverrides,
|
|
29673
30505
|
defaultReasoningEffort: settings.defaultReasoningEffort,
|
|
29674
|
-
cacheTtl: settings.cacheTtl,
|
|
29675
30506
|
loginRegion: region,
|
|
29676
30507
|
serving: readOption$5(options.serving, true),
|
|
29677
30508
|
conflict: readOption$5(options.conflict, null)
|
|
@@ -29706,13 +30537,12 @@ function cacheStatsOrNull(sessionId) {
|
|
|
29706
30537
|
* the hint describes the conversation the card is actually showing rather than
|
|
29707
30538
|
* some global figure.
|
|
29708
30539
|
*/
|
|
29709
|
-
function cacheHintOrNull(sessionId
|
|
30540
|
+
function cacheHintOrNull(sessionId) {
|
|
29710
30541
|
const lastActiveAt = sessionLastActiveAt(sessionId);
|
|
29711
30542
|
const decision = evaluateCacheHint({
|
|
29712
30543
|
now: Date.now(),
|
|
29713
30544
|
lastActiveAt,
|
|
29714
|
-
totalTokens: getCacheStats(sessionId).cachedTokens + getCacheStats(sessionId).freshTokens
|
|
29715
|
-
cacheTtl
|
|
30545
|
+
totalTokens: getCacheStats(sessionId).cachedTokens + getCacheStats(sessionId).freshTokens
|
|
29716
30546
|
});
|
|
29717
30547
|
if (decision.kind !== "hint") return null;
|
|
29718
30548
|
return {
|
|
@@ -29936,10 +30766,6 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
|
|
|
29936
30766
|
const effort = body.defaultReasoningEffort;
|
|
29937
30767
|
if (effort === null || isKimiCodeEffort(effort)) patch.defaultReasoningEffort = effort;
|
|
29938
30768
|
}
|
|
29939
|
-
if (body.cacheTtl !== void 0) {
|
|
29940
|
-
const ttl = body.cacheTtl;
|
|
29941
|
-
if (ttl === null || isKimiCacheTtl(ttl)) patch.cacheTtl = ttl;
|
|
29942
|
-
}
|
|
29943
30769
|
if (preferences) await preferences.update(patch);
|
|
29944
30770
|
else await modelSettings.updateSettings(patch);
|
|
29945
30771
|
if (patch.enabledModelIds !== void 0 || patch.enabled !== void 0) ctx.emit?.("llm/adapters-updated");
|
|
@@ -40300,7 +41126,7 @@ const CLAUDE_MODELS = Object.freeze([
|
|
|
40300
41126
|
"xhigh",
|
|
40301
41127
|
"max"
|
|
40302
41128
|
],
|
|
40303
|
-
canDisableThinking:
|
|
41129
|
+
canDisableThinking: true
|
|
40304
41130
|
},
|
|
40305
41131
|
{
|
|
40306
41132
|
id: "claude-opus-5-5",
|
|
@@ -40324,7 +41150,7 @@ const CLAUDE_MODELS = Object.freeze([
|
|
|
40324
41150
|
{
|
|
40325
41151
|
id: "claude-sonnet-4-5",
|
|
40326
41152
|
name: "Claude Sonnet 4.5 (latest)",
|
|
40327
|
-
contextWindow:
|
|
41153
|
+
contextWindow: 2e5,
|
|
40328
41154
|
maxTokens: 64e3,
|
|
40329
41155
|
supportsImage: true,
|
|
40330
41156
|
supportsTemperature: true,
|
|
@@ -40339,7 +41165,7 @@ const CLAUDE_MODELS = Object.freeze([
|
|
|
40339
41165
|
{
|
|
40340
41166
|
id: "claude-sonnet-4-5-20250929",
|
|
40341
41167
|
name: "Claude Sonnet 4.5",
|
|
40342
|
-
contextWindow:
|
|
41168
|
+
contextWindow: 2e5,
|
|
40343
41169
|
maxTokens: 64e3,
|
|
40344
41170
|
supportsImage: true,
|
|
40345
41171
|
supportsTemperature: true,
|
|
@@ -42600,23 +43426,19 @@ function thinkingRequestedOff(effort) {
|
|
|
42600
43426
|
* This is what decides whether the response ceiling is inflated by the level's
|
|
42601
43427
|
* budget, and getting it wrong in the generous direction is not harmless: a
|
|
42602
43428
|
* non-thinking model whose ceiling was raised by a budget it never had would
|
|
42603
|
-
* ask the server for tokens it cannot produce.
|
|
42604
|
-
* caller asked for
|
|
42605
|
-
* 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.
|
|
42606
43431
|
*/
|
|
42607
43432
|
function claudeRequestThinks(model, effort) {
|
|
42608
|
-
|
|
42609
|
-
if (mode === "none") return false;
|
|
42610
|
-
if (mode === "mid-convo") return true;
|
|
43433
|
+
if (claudeThinkingMode(model) === "none") return false;
|
|
42611
43434
|
return !(thinkingRequestedOff(effort) && claudeModelCanDisableThinking(model));
|
|
42612
43435
|
}
|
|
42613
43436
|
/**
|
|
42614
43437
|
* Dispatch the four thinking cases for one model and effort.
|
|
42615
43438
|
*
|
|
42616
|
-
* The
|
|
42617
|
-
*
|
|
42618
|
-
*
|
|
42619
|
-
* 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.
|
|
42620
43442
|
*
|
|
42621
43443
|
* @param model - exact model id, resolved through the frozen catalog.
|
|
42622
43444
|
* @param effort - the caller's raw effort, passed through as-is.
|
|
@@ -42629,7 +43451,7 @@ function claudeThinking(model, effort, level, maxTokens, customBudgets) {
|
|
|
42629
43451
|
const mode = claudeThinkingMode(model);
|
|
42630
43452
|
if (mode === "none") return {};
|
|
42631
43453
|
const namedEffort = effort !== void 0 && effort !== "" && !thinkingRequestedOff(effort) ? effort : void 0;
|
|
42632
|
-
|
|
43454
|
+
if (thinkingRequestedOff(effort) && claudeModelCanDisableThinking(model)) return { thinking: { type: "disabled" } };
|
|
42633
43455
|
if (mode === "mid-convo") return {
|
|
42634
43456
|
thinking: {
|
|
42635
43457
|
type: "adaptive",
|
|
@@ -42638,7 +43460,6 @@ function claudeThinking(model, effort, level, maxTokens, customBudgets) {
|
|
|
42638
43460
|
},
|
|
42639
43461
|
outputConfig: { effort: namedEffort ?? "high" }
|
|
42640
43462
|
};
|
|
42641
|
-
if (off && claudeModelCanDisableThinking(model)) return { thinking: { type: "disabled" } };
|
|
42642
43463
|
if (mode === "adaptive") return {
|
|
42643
43464
|
thinking: {
|
|
42644
43465
|
type: "adaptive",
|
|
@@ -58919,6 +59740,20 @@ const REASONING_EFFORTS = [
|
|
|
58919
59740
|
* configured request budget rather than taken as the budget itself.
|
|
58920
59741
|
*/
|
|
58921
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
|
+
},
|
|
58922
59757
|
"cline-pass/deepseek-v4.1-flash": {
|
|
58923
59758
|
name: "DeepSeek V4.1 Flash",
|
|
58924
59759
|
contextWindow: 1e6,
|
|
@@ -59081,13 +59916,34 @@ const DEFAULT_MODELS = Object.freeze(Object.keys(MODEL_CATALOG));
|
|
|
59081
59916
|
function catalogEntry(model) {
|
|
59082
59917
|
return MODEL_CATALOG[model];
|
|
59083
59918
|
}
|
|
59084
|
-
/** The default
|
|
59085
|
-
function
|
|
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 = "") {
|
|
59086
59941
|
return {
|
|
59087
59942
|
upstreams: [],
|
|
59088
59943
|
exclude: [],
|
|
59089
59944
|
pinMode: "strict",
|
|
59090
|
-
sort: ""
|
|
59945
|
+
sort: "",
|
|
59946
|
+
has: defaultGatewayHas(model)
|
|
59091
59947
|
};
|
|
59092
59948
|
}
|
|
59093
59949
|
//#endregion
|
|
@@ -59135,6 +59991,16 @@ function jwtExpiryMs(token) {
|
|
|
59135
59991
|
return;
|
|
59136
59992
|
}
|
|
59137
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
|
+
}
|
|
59138
60004
|
function credentialPath$1() {
|
|
59139
60005
|
return path.join(dshHomeDir(), "storages", "cline-pass-credentials.json");
|
|
59140
60006
|
}
|
|
@@ -59228,14 +60094,15 @@ var FileCredentialStore$1 = class {
|
|
|
59228
60094
|
}
|
|
59229
60095
|
};
|
|
59230
60096
|
/** One pin as stored, validated; a malformed row falls back to the default. */
|
|
59231
|
-
function readPin(value) {
|
|
59232
|
-
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);
|
|
59233
60099
|
const record = value;
|
|
59234
60100
|
return {
|
|
59235
60101
|
upstreams: Array.isArray(record.upstreams) ? record.upstreams.filter((entry) => typeof entry === "string" && entry.length > 0) : [],
|
|
59236
60102
|
exclude: Array.isArray(record.exclude) ? record.exclude.filter((entry) => typeof entry === "string" && entry.length > 0) : [],
|
|
59237
60103
|
pinMode: record.pinMode === "preferred" ? "preferred" : "strict",
|
|
59238
|
-
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)
|
|
59239
60106
|
};
|
|
59240
60107
|
}
|
|
59241
60108
|
function readPins(value) {
|
|
@@ -59243,7 +60110,7 @@ function readPins(value) {
|
|
|
59243
60110
|
const pins = {};
|
|
59244
60111
|
for (const [model, pin] of Object.entries(value)) {
|
|
59245
60112
|
if (typeof model !== "string" || model.length === 0) continue;
|
|
59246
|
-
pins[model] = readPin(pin);
|
|
60113
|
+
pins[model] = readPin(pin, model);
|
|
59247
60114
|
}
|
|
59248
60115
|
return pins;
|
|
59249
60116
|
}
|
|
@@ -59270,6 +60137,7 @@ function readCatalogModels$1(value) {
|
|
|
59270
60137
|
if (typeof record.reasoning === "boolean") model.reasoning = record.reasoning;
|
|
59271
60138
|
if (Array.isArray(record.input)) model.input = record.input.filter((item) => typeof item === "string");
|
|
59272
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;
|
|
59273
60141
|
models.push(model);
|
|
59274
60142
|
}
|
|
59275
60143
|
return models;
|
|
@@ -59278,18 +60146,41 @@ function positiveInteger$1(value, fallback, allowZero = false) {
|
|
|
59278
60146
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) return fallback;
|
|
59279
60147
|
return value;
|
|
59280
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
|
+
}
|
|
59281
60170
|
/** Parse one whole settings document. */
|
|
59282
60171
|
function parseClinePassModelSettings(value) {
|
|
59283
60172
|
const record = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
59284
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);
|
|
59285
60176
|
return {
|
|
59286
60177
|
enabled: record.enabled !== false,
|
|
59287
60178
|
baseURL: typeof record.baseURL === "string" && record.baseURL.trim() !== "" ? record.baseURL.trim() : DEFAULT_BASE_URL,
|
|
59288
60179
|
enabledModelIds,
|
|
59289
|
-
catalogModels: readCatalogModels$1(record.catalogModels),
|
|
59290
|
-
pins
|
|
60180
|
+
catalogModels: promoteInUseCatalogRows(readCatalogModels$1(record.catalogModels), enabledModelIds, pins, contextWindowOverrides),
|
|
60181
|
+
pins,
|
|
59291
60182
|
hiddenModels: Array.isArray(record.hiddenModels) ? record.hiddenModels.filter((id) => typeof id === "string") : [],
|
|
59292
|
-
contextWindowOverrides
|
|
60183
|
+
contextWindowOverrides,
|
|
59293
60184
|
defaultContextWindow: positiveInteger$1(record.defaultContextWindow, DEFAULT_CONTEXT_WINDOW$1),
|
|
59294
60185
|
defaultMaxTokens: positiveInteger$1(record.defaultMaxTokens, DEFAULT_MAX_TOKENS$1),
|
|
59295
60186
|
maxConcurrentRequests: positiveInteger$1(record.maxConcurrentRequests, 16, true),
|
|
@@ -60059,6 +60950,83 @@ function resolveModelInfo$1(model, options) {
|
|
|
60059
60950
|
};
|
|
60060
60951
|
}
|
|
60061
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
|
|
60062
61030
|
//#region src/host/cline-pass/protocol.ts
|
|
60063
61031
|
/**
|
|
60064
61032
|
* Cline Pass wire protocol: pipeline detection, pin injection, failover
|
|
@@ -60108,22 +61076,97 @@ function parseRouting(json) {
|
|
|
60108
61076
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
60109
61077
|
const first = isRecord$11(choices[0]) ? choices[0] : {};
|
|
60110
61078
|
const message = isRecord$11(first.message) ? first.message : {};
|
|
60111
|
-
const
|
|
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) ?? {};
|
|
60112
61085
|
const gateway = isRecord$11(metadata.gateway) ? metadata.gateway : {};
|
|
60113
|
-
const routing = isRecord$11(gateway.routing) ? gateway.routing : {};
|
|
60114
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;
|
|
60115
61091
|
return {
|
|
60116
|
-
content: typeof message.content === "string" ? message.content : null,
|
|
60117
|
-
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 : [],
|
|
60118
61094
|
usage: payload.usage ?? null,
|
|
60119
61095
|
pipeline: typeof routing.finalProvider === "string" ? "planner" : direct === null ? null : "direct",
|
|
60120
|
-
canonicalSlug
|
|
60121
|
-
finalProvider
|
|
61096
|
+
canonicalSlug,
|
|
61097
|
+
finalProvider,
|
|
60122
61098
|
finalProviderName: typeof routing.finalProvider === "string" ? routing.finalProvider : direct,
|
|
60123
61099
|
fallbacks: Array.isArray(routing.fallbacksAvailable) ? routing.fallbacksAvailable.map(String) : [],
|
|
60124
|
-
plan: typeof routing.planningReasoning === "string" ? routing.planningReasoning : ""
|
|
61100
|
+
plan: typeof routing.planningReasoning === "string" ? routing.planningReasoning : "",
|
|
61101
|
+
fingerprint: huntFingerprint(payload),
|
|
61102
|
+
anonymous: isClinePassAnonymousUpstream(finalProvider ?? "", canonicalSlug ?? "")
|
|
60125
61103
|
};
|
|
60126
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
|
+
}
|
|
60127
61170
|
/** Lowercase a display name into an upstream slug. */
|
|
60128
61171
|
function slugify(value) {
|
|
60129
61172
|
return String(value).toLowerCase().replace(/\s+/g, "-");
|
|
@@ -60212,6 +61255,7 @@ function injectPrefs(body, meta, attempt) {
|
|
|
60212
61255
|
const excludeList = attempt.excludeList ?? [];
|
|
60213
61256
|
const strict = attempt.strict !== false;
|
|
60214
61257
|
const sort = normalizeSort(attempt.sort);
|
|
61258
|
+
const has = capGatewayHas((attempt.has ?? []).filter((name) => typeof name === "string"));
|
|
60215
61259
|
const excluded = excludeList.filter((name) => normalizeSlug(name) !== normalizeSlug(upstream ?? ""));
|
|
60216
61260
|
const excludedIds = new Set(excluded.map(normalizeSlug));
|
|
60217
61261
|
const spell = (names, pipeline) => names.map((name) => channelNameFor(name, pipeline));
|
|
@@ -60221,7 +61265,7 @@ function injectPrefs(body, meta, attempt) {
|
|
|
60221
61265
|
const both = attempt.spelling !== "detected";
|
|
60222
61266
|
const useGateway = both || pipeline === "planner" || pipeline === null;
|
|
60223
61267
|
const useOpenRouter = both || pipeline === "direct" || pipeline === null;
|
|
60224
|
-
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);
|
|
60225
61269
|
const openRouterUsable = useOpenRouter && (upstream !== null || sort !== null || excluded.length > 0 || openRouterAllow !== null && openRouterAllow.length > 0);
|
|
60226
61270
|
if (!gatewayUsable && !openRouterUsable) return next;
|
|
60227
61271
|
if (gatewayUsable) {
|
|
@@ -60233,6 +61277,7 @@ function injectPrefs(body, meta, attempt) {
|
|
|
60233
61277
|
}
|
|
60234
61278
|
else if (gatewayAllow !== null && gatewayAllow.length > 0) gateway.only = spell(gatewayAllow, "planner");
|
|
60235
61279
|
if (sort !== null) gateway.sort = sort;
|
|
61280
|
+
if (has.length > 0) gateway.has = [...has];
|
|
60236
61281
|
const providerOptions = isRecord$11(next.providerOptions) ? next.providerOptions : {};
|
|
60237
61282
|
const currentGateway = isRecord$11(providerOptions.gateway) ? providerOptions.gateway : {};
|
|
60238
61283
|
next.providerOptions = {
|
|
@@ -60290,13 +61335,16 @@ function isAdherenceOk(adherence) {
|
|
|
60290
61335
|
*/
|
|
60291
61336
|
function pinWarnings(attempt, meta) {
|
|
60292
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}`);
|
|
60293
61342
|
const excluded = (attempt.excludeList ?? []).filter((name) => normalizeSlug(name) !== normalizeSlug(upstream ?? ""));
|
|
60294
61343
|
const excludedIds = new Set(excluded.map(normalizeSlug));
|
|
60295
|
-
if (excluded.length === 0) return
|
|
60296
|
-
|
|
60297
|
-
if (known.length === 0) return [`
|
|
60298
|
-
|
|
60299
|
-
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;
|
|
60300
61348
|
}
|
|
60301
61349
|
/**
|
|
60302
61350
|
* Expand one model's pin configuration into the ordered failover candidates.
|
|
@@ -60316,6 +61364,8 @@ function buildAttempts(config) {
|
|
|
60316
61364
|
sort: normalizeSort(config.sort),
|
|
60317
61365
|
excludeList: exclude
|
|
60318
61366
|
};
|
|
61367
|
+
const has = (config.has ?? []).filter((name) => typeof name === "string" && name.trim().length > 0);
|
|
61368
|
+
if (has.length > 0) base.has = [...has];
|
|
60319
61369
|
if (typeof config.spelling === "string" && config.spelling.length > 0) base.spelling = config.spelling;
|
|
60320
61370
|
if (wanted.length > 0) return wanted.map((upstream, index) => ({
|
|
60321
61371
|
...base,
|
|
@@ -60969,8 +62019,7 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
60969
62019
|
*/
|
|
60970
62020
|
async availableModelIds(settings) {
|
|
60971
62021
|
const ids = new Set(DEFAULT_MODELS);
|
|
60972
|
-
for (const model of settings
|
|
60973
|
-
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);
|
|
60974
62023
|
const hidden = new Set(settings.hiddenModels);
|
|
60975
62024
|
const enabled = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
|
|
60976
62025
|
return [...ids].filter((id) => !hidden.has(id) && (enabled === null || enabled.has(id)));
|
|
@@ -61149,7 +62198,9 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
61149
62198
|
const observed = {
|
|
61150
62199
|
provider: null,
|
|
61151
62200
|
canonical: null,
|
|
61152
|
-
pipeline: null
|
|
62201
|
+
pipeline: null,
|
|
62202
|
+
fingerprint: null,
|
|
62203
|
+
anonymous: false
|
|
61153
62204
|
};
|
|
61154
62205
|
const iterator = translate(tapRouting(parseServerSentEvents$1(tapChunks$1(response.body, () => {
|
|
61155
62206
|
activity?.pulse();
|
|
@@ -61221,6 +62272,9 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
61221
62272
|
upstream: attempt.upstream,
|
|
61222
62273
|
excludeList: attempt.excludeList
|
|
61223
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`);
|
|
61224
62278
|
const ms = Date.now() - attemptStarted;
|
|
61225
62279
|
trace.push(attemptLabel);
|
|
61226
62280
|
this.store.record({
|
|
@@ -61236,10 +62290,16 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
61236
62290
|
code: finishFailure?.code ?? "",
|
|
61237
62291
|
account: account.name,
|
|
61238
62292
|
adherence,
|
|
62293
|
+
fingerprint: observed.fingerprint ?? "",
|
|
62294
|
+
anonymous: observed.anonymous,
|
|
61239
62295
|
warnings,
|
|
61240
62296
|
...usage === void 0 ? {} : { usage },
|
|
61241
62297
|
effort: options.reasoningEffort === void 0 ? "" : String(options.reasoningEffort)
|
|
61242
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
|
+
});
|
|
61243
62303
|
this.accountPool.recordModel(account.id, options.model);
|
|
61244
62304
|
if (finishFailure === null && attempt.upstream !== null && adherence === "adopted") this.store.learnUpstream(options.model, attempt.upstream, "ok", "", ms);
|
|
61245
62305
|
return;
|
|
@@ -61337,8 +62397,10 @@ async function* tapRouting(payloads, observed) {
|
|
|
61337
62397
|
if (routing.finalProvider !== null) {
|
|
61338
62398
|
observed.provider = routing.finalProvider;
|
|
61339
62399
|
observed.pipeline = routing.pipeline;
|
|
62400
|
+
observed.anonymous = routing.anonymous;
|
|
61340
62401
|
if (routing.canonicalSlug !== null) observed.canonical = routing.canonicalSlug;
|
|
61341
62402
|
}
|
|
62403
|
+
if (routing.fingerprint !== null) observed.fingerprint = routing.fingerprint;
|
|
61342
62404
|
}
|
|
61343
62405
|
yield payload;
|
|
61344
62406
|
}
|
|
@@ -61466,7 +62528,7 @@ function effortsFromOptions(options) {
|
|
|
61466
62528
|
const ordered = REASONING_EFFORTS.map((effort) => effort.id).filter((id) => levels.has(id));
|
|
61467
62529
|
return ordered.length > 0 ? ordered : void 0;
|
|
61468
62530
|
}
|
|
61469
|
-
/** List the gateway's own catalog
|
|
62531
|
+
/** List the gateway's own catalog; empty when the call fails. */
|
|
61470
62532
|
async function fetchGatewayCatalog({ fetchFn, baseURL, apiKey, signal, timeoutMs = 3e4 }) {
|
|
61471
62533
|
try {
|
|
61472
62534
|
const result = await sendJSON(fetchFn, `${trimBase(baseURL)}/models`, {
|
|
@@ -61474,9 +62536,24 @@ async function fetchGatewayCatalog({ fetchFn, baseURL, apiKey, signal, timeoutMs
|
|
|
61474
62536
|
signal,
|
|
61475
62537
|
timeoutMs
|
|
61476
62538
|
});
|
|
61477
|
-
|
|
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
|
+
};
|
|
61478
62552
|
} catch {
|
|
61479
|
-
return
|
|
62553
|
+
return {
|
|
62554
|
+
ids: [],
|
|
62555
|
+
models: {}
|
|
62556
|
+
};
|
|
61480
62557
|
}
|
|
61481
62558
|
}
|
|
61482
62559
|
/**
|
|
@@ -61727,6 +62804,7 @@ var ClinePassEngine = class {
|
|
|
61727
62804
|
exclude: this.pinOf(model).exclude,
|
|
61728
62805
|
pinMode: this.pinOf(model).pinMode,
|
|
61729
62806
|
sort: this.pinOf(model).sort,
|
|
62807
|
+
has: this.pinOf(model).has,
|
|
61730
62808
|
spelling: this.options.pinFor(model).spelling
|
|
61731
62809
|
};
|
|
61732
62810
|
}
|
|
@@ -61737,7 +62815,8 @@ var ClinePassEngine = class {
|
|
|
61737
62815
|
upstreams: (pin.upstreams ?? []).map(String),
|
|
61738
62816
|
exclude: (pin.exclude ?? []).map(String),
|
|
61739
62817
|
pinMode: pin.pinMode === "preferred" ? "preferred" : "strict",
|
|
61740
|
-
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)
|
|
61741
62820
|
};
|
|
61742
62821
|
}
|
|
61743
62822
|
/**
|
|
@@ -61814,7 +62893,9 @@ var ClinePassEngine = class {
|
|
|
61814
62893
|
if (attempt.upstream === null && attempt.excludeList.length > 0 && outcome.error !== "") this.learnFromError(model, outcome.error);
|
|
61815
62894
|
}
|
|
61816
62895
|
}
|
|
61817
|
-
const
|
|
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;
|
|
61818
62899
|
return {
|
|
61819
62900
|
ok: false,
|
|
61820
62901
|
result: last,
|
|
@@ -61876,21 +62957,26 @@ var ClinePassEngine = class {
|
|
|
61876
62957
|
lastProvider: "",
|
|
61877
62958
|
upstreams: [],
|
|
61878
62959
|
availableProviders: [],
|
|
61879
|
-
tier0: []
|
|
62960
|
+
tier0: [],
|
|
62961
|
+
fingerprint: "",
|
|
62962
|
+
anonymous: false
|
|
61880
62963
|
};
|
|
61881
62964
|
}
|
|
61882
62965
|
const routing = parseRouting(payload);
|
|
61883
62966
|
const pipeline = routing.pipeline;
|
|
61884
62967
|
const otherPipeline = pipeline === "planner" ? "direct" : pipeline === "direct" ? "planner" : null;
|
|
62968
|
+
const has = this.pinOf(model).has;
|
|
61885
62969
|
let harvested = null;
|
|
61886
62970
|
if (pipeline !== null) harvested = await this.harvest(model, pipeline, account, {
|
|
61887
62971
|
signal,
|
|
61888
|
-
timeoutMs: this.options.probeTimeoutMs
|
|
62972
|
+
timeoutMs: this.options.probeTimeoutMs,
|
|
62973
|
+
has
|
|
61889
62974
|
});
|
|
61890
62975
|
let otherHarvested = null;
|
|
61891
62976
|
if (otherPipeline !== null) otherHarvested = await this.harvest(model, otherPipeline, account, {
|
|
61892
62977
|
signal,
|
|
61893
|
-
timeoutMs: this.options.probeTimeoutMs
|
|
62978
|
+
timeoutMs: this.options.probeTimeoutMs,
|
|
62979
|
+
has
|
|
61894
62980
|
});
|
|
61895
62981
|
let endpoints = [];
|
|
61896
62982
|
let openrouterSlug = null;
|
|
@@ -61930,6 +63016,7 @@ var ClinePassEngine = class {
|
|
|
61930
63016
|
staleAt: combined.length === 0 && upstreams.length > 0 ? Date.now() : 0,
|
|
61931
63017
|
tier0,
|
|
61932
63018
|
lastProvider: routing.finalProvider ?? previous.lastProvider ?? null,
|
|
63019
|
+
lastFingerprint: routing.fingerprint ?? previous.lastFingerprint ?? null,
|
|
61933
63020
|
lastMs: ms,
|
|
61934
63021
|
probedAt: Date.now(),
|
|
61935
63022
|
lastError: ""
|
|
@@ -61945,7 +63032,9 @@ var ClinePassEngine = class {
|
|
|
61945
63032
|
lastProvider: meta.lastProvider ?? "",
|
|
61946
63033
|
upstreams: meta.upstreams ?? [],
|
|
61947
63034
|
availableProviders: meta.availableProviders ?? [],
|
|
61948
|
-
tier0: meta.tier0 ?? []
|
|
63035
|
+
tier0: meta.tier0 ?? [],
|
|
63036
|
+
fingerprint: meta.lastFingerprint ?? "",
|
|
63037
|
+
anonymous: isClinePassAnonymousUpstream(meta.lastProvider ?? "", meta.canonicalSlug ?? "")
|
|
61949
63038
|
};
|
|
61950
63039
|
}
|
|
61951
63040
|
/**
|
|
@@ -61953,7 +63042,7 @@ var ClinePassEngine = class {
|
|
|
61953
63042
|
* cannot exist: the router fails before spending a token and names every
|
|
61954
63043
|
* provider it could have used.
|
|
61955
63044
|
*/
|
|
61956
|
-
async harvest(model, pipeline, account, { signal, timeoutMs }) {
|
|
63045
|
+
async harvest(model, pipeline, account, { signal, timeoutMs, has = [] }) {
|
|
61957
63046
|
const base = {
|
|
61958
63047
|
model,
|
|
61959
63048
|
messages: [{
|
|
@@ -61964,7 +63053,10 @@ var ClinePassEngine = class {
|
|
|
61964
63053
|
};
|
|
61965
63054
|
const body = pipeline === "planner" ? {
|
|
61966
63055
|
...base,
|
|
61967
|
-
providerOptions: { gateway: {
|
|
63056
|
+
providerOptions: { gateway: {
|
|
63057
|
+
only: ["__probe__"],
|
|
63058
|
+
...has.length > 0 ? { has: [...has] } : {}
|
|
63059
|
+
} }
|
|
61968
63060
|
} : {
|
|
61969
63061
|
...base,
|
|
61970
63062
|
provider: { only: ["__probe__"] }
|
|
@@ -61978,7 +63070,12 @@ var ClinePassEngine = class {
|
|
|
61978
63070
|
signal,
|
|
61979
63071
|
timeoutMs: timeoutMs ?? this.options.probeTimeoutMs
|
|
61980
63072
|
});
|
|
61981
|
-
|
|
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;
|
|
61982
63079
|
} catch {
|
|
61983
63080
|
return null;
|
|
61984
63081
|
}
|
|
@@ -61993,6 +63090,7 @@ var ClinePassEngine = class {
|
|
|
61993
63090
|
async validate(model, { signal, timeoutMs } = {}) {
|
|
61994
63091
|
const meta = this.store.metaOf(model);
|
|
61995
63092
|
const list = Array.isArray(meta.upstreams) ? meta.upstreams : [];
|
|
63093
|
+
const has = this.pinOf(model).has;
|
|
61996
63094
|
const results = [];
|
|
61997
63095
|
for (let index = 0; index < list.length; index += 5) {
|
|
61998
63096
|
if (signal?.aborted === true) break;
|
|
@@ -62004,7 +63102,8 @@ var ClinePassEngine = class {
|
|
|
62004
63102
|
strict: true,
|
|
62005
63103
|
sort: null,
|
|
62006
63104
|
orderRest: [],
|
|
62007
|
-
excludeList: []
|
|
63105
|
+
excludeList: [],
|
|
63106
|
+
has
|
|
62008
63107
|
};
|
|
62009
63108
|
const body = injectPrefs({
|
|
62010
63109
|
model,
|
|
@@ -62083,13 +63182,14 @@ var ClinePassEngine = class {
|
|
|
62083
63182
|
* Send one small chat through a (possibly temporary) pin configuration and
|
|
62084
63183
|
* report what served it. Nothing is persisted.
|
|
62085
63184
|
*/
|
|
62086
|
-
async test(model, { upstreams, exclude, signal } = {}) {
|
|
63185
|
+
async test(model, { upstreams, exclude, has, signal } = {}) {
|
|
62087
63186
|
const stored = this.pinOf(model);
|
|
62088
63187
|
const pin = {
|
|
62089
63188
|
upstreams: Array.isArray(upstreams) ? upstreams : stored.upstreams,
|
|
62090
63189
|
exclude: Array.isArray(exclude) ? exclude : stored.exclude,
|
|
62091
63190
|
pinMode: stored.pinMode,
|
|
62092
63191
|
sort: stored.sort,
|
|
63192
|
+
has: Array.isArray(has) ? has : stored.has,
|
|
62093
63193
|
spelling: this.options.pinFor(model).spelling
|
|
62094
63194
|
};
|
|
62095
63195
|
const started = Date.now();
|
|
@@ -62135,6 +63235,9 @@ var ClinePassEngine = class {
|
|
|
62135
63235
|
adopted: false,
|
|
62136
63236
|
adherence: outcome.result?.adherence ?? "",
|
|
62137
63237
|
plan: routing?.plan ?? "",
|
|
63238
|
+
fingerprint: routing?.fingerprint ?? "",
|
|
63239
|
+
anonymous: isClinePassAnonymousUpstream(routing?.finalProvider ?? "", routing?.canonicalSlug ?? ""),
|
|
63240
|
+
has: [...pin.has ?? []],
|
|
62138
63241
|
warnings
|
|
62139
63242
|
};
|
|
62140
63243
|
}
|
|
@@ -62166,6 +63269,9 @@ var ClinePassEngine = class {
|
|
|
62166
63269
|
adopted: outcome.result?.adherence === "adopted",
|
|
62167
63270
|
adherence: outcome.result?.adherence ?? "",
|
|
62168
63271
|
plan: routing?.plan ?? "",
|
|
63272
|
+
fingerprint: routing?.fingerprint ?? "",
|
|
63273
|
+
anonymous: isClinePassAnonymousUpstream(routing?.finalProvider ?? "", routing?.canonicalSlug ?? ""),
|
|
63274
|
+
has: [...pin.has ?? []],
|
|
62169
63275
|
warnings
|
|
62170
63276
|
};
|
|
62171
63277
|
}
|
|
@@ -62229,7 +63335,7 @@ var ClinePassEngine = class {
|
|
|
62229
63335
|
};
|
|
62230
63336
|
}
|
|
62231
63337
|
}
|
|
62232
|
-
/** Refresh the gateway's own catalog ids. */
|
|
63338
|
+
/** Refresh the gateway's own catalog: ids plus published metadata. */
|
|
62233
63339
|
async gatewayCatalog({ signal, timeoutMs } = {}) {
|
|
62234
63340
|
const account = await this.resolveAccount();
|
|
62235
63341
|
return await fetchGatewayCatalog({
|
|
@@ -62264,66 +63370,6 @@ var ClinePassEngine = class {
|
|
|
62264
63370
|
}
|
|
62265
63371
|
};
|
|
62266
63372
|
//#endregion
|
|
62267
|
-
//#region src/shared/cline-pass-contracts.ts
|
|
62268
|
-
/**
|
|
62269
|
-
* Wire contracts for the Cline Pass line's settings API.
|
|
62270
|
-
*
|
|
62271
|
-
* The line is API-key based: there is no OAuth flow, no token expiry, and no
|
|
62272
|
-
* per-account identity beyond the key and the alias a user gives it. What the
|
|
62273
|
-
* card does render beyond the shared account facts is the routing observation
|
|
62274
|
-
* this plugin makes about each model (pipeline, discovered channels, per-channel
|
|
62275
|
-
* verdicts) and the per-model pin the user stores. None of it is a credential.
|
|
62276
|
-
*/
|
|
62277
|
-
const CLINE_PASS_PROVIDER_ID = "cline-pass";
|
|
62278
|
-
const CLINE_PASS_PROVIDER_NAME = "Cline Pass";
|
|
62279
|
-
const CLINE_PASS_ROUTE_PREFIX = "/cline-pass/api";
|
|
62280
|
-
/** Whether a submitted pin mode names one of the two accepted modes. */
|
|
62281
|
-
function isClinePassPinMode(value) {
|
|
62282
|
-
return value === "strict" || value === "preferred";
|
|
62283
|
-
}
|
|
62284
|
-
/** Human label for the gateway's quota-window spelling. */
|
|
62285
|
-
function clinePassWindowLabel(type) {
|
|
62286
|
-
switch (type) {
|
|
62287
|
-
case "five_hour": return "5 小时";
|
|
62288
|
-
case "weekly": return "每周";
|
|
62289
|
-
case "monthly": return "每月";
|
|
62290
|
-
default: return type;
|
|
62291
|
-
}
|
|
62292
|
-
}
|
|
62293
|
-
/** Window length in minutes for the shared AccountQuota formatter. */
|
|
62294
|
-
function clinePassWindowDurationMins(type) {
|
|
62295
|
-
switch (type) {
|
|
62296
|
-
case "five_hour": return 300;
|
|
62297
|
-
case "weekly": return 7 * 1440;
|
|
62298
|
-
case "monthly": return 30 * 1440;
|
|
62299
|
-
default: return;
|
|
62300
|
-
}
|
|
62301
|
-
}
|
|
62302
|
-
/**
|
|
62303
|
-
* Project one account's Cline Pass usage reading into the shared
|
|
62304
|
-
* {@link PoolAccountQuotaDto} shape that AccountPoolSection renders inside the
|
|
62305
|
-
* account row. A failed reading returns undefined so the card does not draw
|
|
62306
|
-
* fabricated zero bars.
|
|
62307
|
-
*/
|
|
62308
|
-
function clinePassAccountQuota(reading) {
|
|
62309
|
-
if (reading === void 0 || !reading.ok) return void 0;
|
|
62310
|
-
const windows = reading.windows.map((window) => {
|
|
62311
|
-
const duration = clinePassWindowDurationMins(window.type);
|
|
62312
|
-
const parsedReset = window.resetsAt === "" ? NaN : Date.parse(window.resetsAt);
|
|
62313
|
-
const resetsAt = Number.isFinite(parsedReset) && parsedReset > 0 ? parsedReset : null;
|
|
62314
|
-
return {
|
|
62315
|
-
label: window.label || clinePassWindowLabel(window.type),
|
|
62316
|
-
usedPercent: window.percentUsed,
|
|
62317
|
-
...duration === void 0 ? {} : { windowDurationMins: duration },
|
|
62318
|
-
resetsAt
|
|
62319
|
-
};
|
|
62320
|
-
});
|
|
62321
|
-
return {
|
|
62322
|
-
fetchedAt: reading.fetchedAt,
|
|
62323
|
-
windows
|
|
62324
|
-
};
|
|
62325
|
-
}
|
|
62326
|
-
//#endregion
|
|
62327
63373
|
//#region src/host/cline-pass/routes.ts
|
|
62328
63374
|
function sendJson$2(response, status, body) {
|
|
62329
63375
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
@@ -62373,23 +63419,40 @@ function formatModelCapacity$1(tokens) {
|
|
|
62373
63419
|
if (tokens >= 1e3 && tokens % 1e3 === 0) return tokens / 1e3 + "k";
|
|
62374
63420
|
return String(tokens);
|
|
62375
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
|
+
}
|
|
62376
63440
|
function formatModelHint$1(contextWindow, input, reasoning) {
|
|
62377
63441
|
return formatModelCapacity$1(contextWindow) + " · " + input.join("+") + (reasoning ? " · reasoning" : "");
|
|
62378
63442
|
}
|
|
62379
|
-
function uniqueModels$1(settings
|
|
63443
|
+
function uniqueModels$1(settings) {
|
|
62380
63444
|
const ids = new Set(DEFAULT_MODELS);
|
|
62381
|
-
for (const model of settings
|
|
62382
|
-
if (settings.exposeCatalog) for (const id of catalogIds) ids.add(id);
|
|
63445
|
+
for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
|
|
62383
63446
|
return [...ids];
|
|
62384
63447
|
}
|
|
62385
63448
|
/** Every model the card should render, whether or not it is advertised. */
|
|
62386
63449
|
function projectModels$1(settings, store) {
|
|
62387
|
-
const known = uniqueModels$1(settings
|
|
63450
|
+
const known = uniqueModels$1(settings);
|
|
62388
63451
|
const hidden = new Set(settings.hiddenModels);
|
|
62389
63452
|
const enabledSet = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
|
|
62390
63453
|
return known.map((id) => {
|
|
62391
63454
|
const meta = store.metaOf(id);
|
|
62392
|
-
const pin = settings.pins[id] ?? defaultPin();
|
|
63455
|
+
const pin = settings.pins[id] ?? defaultPin(id);
|
|
62393
63456
|
const base = resolveModelInfo$1(id, {
|
|
62394
63457
|
displayName: "Cline Pass",
|
|
62395
63458
|
synced: settings.catalogModels,
|
|
@@ -62426,6 +63489,7 @@ function projectModels$1(settings, store) {
|
|
|
62426
63489
|
excluded: pin.exclude,
|
|
62427
63490
|
pinMode: normalizePinMode(pin.pinMode),
|
|
62428
63491
|
sort: pin.sort,
|
|
63492
|
+
has: [...pin.has],
|
|
62429
63493
|
upstreams: (meta.upstreams ?? []).map(String),
|
|
62430
63494
|
upstreamStatus: verdicts,
|
|
62431
63495
|
lastProvider: String(meta.lastProvider ?? ""),
|
|
@@ -62478,7 +63542,7 @@ async function getClinePassWebStatus(options, quotaRefresh) {
|
|
|
62478
63542
|
pool,
|
|
62479
63543
|
models,
|
|
62480
63544
|
catalogSynced: settings.catalogModels.length > 0,
|
|
62481
|
-
catalogCount: settings.
|
|
63545
|
+
catalogCount: listedClinePassCatalogModels(settings).length,
|
|
62482
63546
|
usage: store.usage().accounts.length === 0 ? null : store.usage(),
|
|
62483
63547
|
quotaRefreshing: quotaRefresh?.refreshing === true,
|
|
62484
63548
|
historySize: store.historySize(),
|
|
@@ -62490,7 +63554,6 @@ async function getClinePassWebStatus(options, quotaRefresh) {
|
|
|
62490
63554
|
maxConcurrentRequests: settings.maxConcurrentRequests,
|
|
62491
63555
|
streamIdleTimeoutMs: settings.streamIdleTimeoutMs,
|
|
62492
63556
|
historyLimit: settings.historyLimit,
|
|
62493
|
-
exposeCatalog: settings.exposeCatalog,
|
|
62494
63557
|
pinSpelling: settings.pinSpelling
|
|
62495
63558
|
}
|
|
62496
63559
|
};
|
|
@@ -62513,6 +63576,8 @@ const CLINE_PASS_ROUTES = {
|
|
|
62513
63576
|
"models/visibility-all": "POST",
|
|
62514
63577
|
"models/context-window": "POST",
|
|
62515
63578
|
"catalog/refresh": "POST",
|
|
63579
|
+
"catalog/search": "GET",
|
|
63580
|
+
"catalog/add": "POST",
|
|
62516
63581
|
usage: "GET|POST",
|
|
62517
63582
|
history: "GET"
|
|
62518
63583
|
};
|
|
@@ -62532,6 +63597,27 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62532
63597
|
ctx.emit?.("llm/adapters-updated");
|
|
62533
63598
|
};
|
|
62534
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
|
+
/**
|
|
62535
63621
|
* Read every usable account's official quota and replace the snapshot.
|
|
62536
63622
|
*
|
|
62537
63623
|
* Shared by the explicit refresh action and the background one a status read
|
|
@@ -62841,12 +63927,13 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62841
63927
|
ok: false,
|
|
62842
63928
|
error: "this action needs a model id"
|
|
62843
63929
|
});
|
|
62844
|
-
const current = (await settingsStore.read()).pins[model] ?? defaultPin();
|
|
63930
|
+
const current = (await settingsStore.read()).pins[model] ?? defaultPin(model);
|
|
62845
63931
|
const next = {
|
|
62846
63932
|
upstreams: Array.isArray(body.upstreams) ? body.upstreams.map(String) : current.upstreams,
|
|
62847
63933
|
exclude: Array.isArray(body.exclude) ? body.exclude.map(String) : current.exclude,
|
|
62848
63934
|
pinMode: isClinePassPinMode(body.pinMode) ? body.pinMode : current.pinMode,
|
|
62849
|
-
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
|
|
62850
63937
|
};
|
|
62851
63938
|
await settingsStore.setPin(model, next);
|
|
62852
63939
|
const meta = store.metaOf(model);
|
|
@@ -62915,7 +64002,9 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62915
64002
|
lastProvider: result.lastProvider,
|
|
62916
64003
|
upstreams: result.upstreams,
|
|
62917
64004
|
availableProviders: result.availableProviders,
|
|
62918
|
-
tier0: result.tier0
|
|
64005
|
+
tier0: result.tier0,
|
|
64006
|
+
fingerprint: result.fingerprint,
|
|
64007
|
+
anonymous: result.anonymous
|
|
62919
64008
|
},
|
|
62920
64009
|
status: await status()
|
|
62921
64010
|
}
|
|
@@ -62973,7 +64062,8 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62973
64062
|
});
|
|
62974
64063
|
const result = await engine.test(model, {
|
|
62975
64064
|
upstreams: Array.isArray(body.upstreams) ? body.upstreams.map(String) : void 0,
|
|
62976
|
-
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
|
|
62977
64067
|
});
|
|
62978
64068
|
return sendJson$2(response, 200, {
|
|
62979
64069
|
ok: true,
|
|
@@ -62995,6 +64085,9 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62995
64085
|
adopted: result.adopted,
|
|
62996
64086
|
adherence: result.adherence,
|
|
62997
64087
|
plan: firstLine$2(result.plan, 300),
|
|
64088
|
+
fingerprint: result.fingerprint,
|
|
64089
|
+
anonymous: result.anonymous,
|
|
64090
|
+
has: result.has,
|
|
62998
64091
|
warnings: result.warnings,
|
|
62999
64092
|
trace: result.trace
|
|
63000
64093
|
},
|
|
@@ -63010,7 +64103,7 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
63010
64103
|
});
|
|
63011
64104
|
const body = await readRequestJson$2(request).catch(() => ({}));
|
|
63012
64105
|
const current = await settingsStore.read();
|
|
63013
|
-
const known = uniqueModels$1(current
|
|
64106
|
+
const known = uniqueModels$1(current);
|
|
63014
64107
|
const hidden = new Set(current.hiddenModels);
|
|
63015
64108
|
if (path === "models/visibility") {
|
|
63016
64109
|
const model = typeof body.model === "string" ? body.model.trim() : "";
|
|
@@ -63063,6 +64156,94 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
63063
64156
|
value: await status()
|
|
63064
64157
|
});
|
|
63065
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
|
+
}
|
|
63066
64247
|
if (path === "catalog/refresh") {
|
|
63067
64248
|
if (method !== "POST") return sendMethodNotAllowed$2(response);
|
|
63068
64249
|
if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
|
|
@@ -63083,6 +64264,7 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
63083
64264
|
const published = scan.catalog[id] ?? {};
|
|
63084
64265
|
existing.set(id, {
|
|
63085
64266
|
id,
|
|
64267
|
+
origin: "official",
|
|
63086
64268
|
...published.name === void 0 ? {} : { name: published.name },
|
|
63087
64269
|
...published.contextWindow === void 0 ? {} : { contextWindow: published.contextWindow },
|
|
63088
64270
|
...published.maxTokens === void 0 ? {} : { maxTokens: published.maxTokens },
|
|
@@ -63091,17 +64273,18 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
63091
64273
|
...published.efforts === void 0 ? {} : { efforts: published.efforts }
|
|
63092
64274
|
});
|
|
63093
64275
|
}
|
|
63094
|
-
const gateway =
|
|
63095
|
-
|
|
63096
|
-
|
|
63097
|
-
|
|
63098
|
-
|
|
63099
|
-
}
|
|
63100
|
-
await settingsStore.storeCatalog(merged);
|
|
64276
|
+
const gateway = await engine.gatewayCatalog().catch(() => ({
|
|
64277
|
+
ids: [],
|
|
64278
|
+
models: {}
|
|
64279
|
+
}));
|
|
64280
|
+
await settingsStore.storeCatalog([...existing.values()]);
|
|
63101
64281
|
store.setCatalog({
|
|
63102
|
-
ids: [.../* @__PURE__ */ new Set([...scan.models, ...gateway])],
|
|
63103
|
-
sources: scan.sources,
|
|
63104
|
-
models:
|
|
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
|
+
}
|
|
63105
64288
|
});
|
|
63106
64289
|
emitModelsUpdated();
|
|
63107
64290
|
return sendJson$2(response, 200, {
|
|
@@ -63175,6 +64358,8 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
63175
64358
|
error: entry.error,
|
|
63176
64359
|
attempts: entry.attempts,
|
|
63177
64360
|
adherence: entry.adherence,
|
|
64361
|
+
fingerprint: entry.fingerprint,
|
|
64362
|
+
anonymous: entry.anonymous,
|
|
63178
64363
|
warnings: entry.warnings
|
|
63179
64364
|
}));
|
|
63180
64365
|
return sendJson$2(response, 200, {
|
|
@@ -63295,6 +64480,8 @@ function createClinePassStore({ historyLimit = 100 } = {}) {
|
|
|
63295
64480
|
error: entry.error ?? "",
|
|
63296
64481
|
attempts: entry.attempts ?? [],
|
|
63297
64482
|
adherence: entry.adherence ?? "",
|
|
64483
|
+
fingerprint: entry.fingerprint ?? "",
|
|
64484
|
+
anonymous: entry.anonymous === true,
|
|
63298
64485
|
warnings: entry.warnings ?? [],
|
|
63299
64486
|
...entry.code === void 0 ? {} : { code: entry.code }
|
|
63300
64487
|
};
|
|
@@ -63478,6 +64665,11 @@ const MODEL_SCHEMA = {
|
|
|
63478
64665
|
type: "string",
|
|
63479
64666
|
required: true
|
|
63480
64667
|
},
|
|
64668
|
+
has: {
|
|
64669
|
+
type: "array",
|
|
64670
|
+
required: true,
|
|
64671
|
+
items: { type: "string" }
|
|
64672
|
+
},
|
|
63481
64673
|
pinned: {
|
|
63482
64674
|
type: "array",
|
|
63483
64675
|
required: true,
|
|
@@ -63553,6 +64745,7 @@ function projectModel(status, model) {
|
|
|
63553
64745
|
hidden: !entry.enabled,
|
|
63554
64746
|
pinMode: normalizePinMode(entry.pinMode),
|
|
63555
64747
|
sort: entry.sort,
|
|
64748
|
+
has: entry.has,
|
|
63556
64749
|
pinned: entry.pinned,
|
|
63557
64750
|
excluded: entry.excluded,
|
|
63558
64751
|
upstreams: entry.upstreams,
|
|
@@ -63576,7 +64769,7 @@ function projectModel(status, model) {
|
|
|
63576
64769
|
function registerClinePassTools(ctx, options) {
|
|
63577
64770
|
const { settings, accountPool, store, engine, status, serving } = options;
|
|
63578
64771
|
const disposers = [];
|
|
63579
|
-
const pinOf = async (model) => (await settings.read()).pins[model] ?? defaultPin();
|
|
64772
|
+
const pinOf = async (model) => (await settings.read()).pins[model] ?? defaultPin(model);
|
|
63580
64773
|
disposers.push(ctx.tools.register(defineTool({
|
|
63581
64774
|
name: "cline_pass_status",
|
|
63582
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.",
|
|
@@ -63741,6 +64934,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
63741
64934
|
const published = scan.catalog[id] ?? {};
|
|
63742
64935
|
merged.push({
|
|
63743
64936
|
id,
|
|
64937
|
+
origin: "official",
|
|
63744
64938
|
...published.name === void 0 ? {} : { name: published.name },
|
|
63745
64939
|
...published.contextWindow === void 0 ? {} : { contextWindow: published.contextWindow },
|
|
63746
64940
|
...published.maxTokens === void 0 ? {} : { maxTokens: published.maxTokens },
|
|
@@ -63750,10 +64944,17 @@ function registerClinePassTools(ctx, options) {
|
|
|
63750
64944
|
});
|
|
63751
64945
|
}
|
|
63752
64946
|
await settings.storeCatalog(merged);
|
|
64947
|
+
const gateway = await engine.gatewayCatalog().catch(() => ({
|
|
64948
|
+
ids: [],
|
|
64949
|
+
models: {}
|
|
64950
|
+
}));
|
|
63753
64951
|
store.setCatalog({
|
|
63754
|
-
ids: scan.models,
|
|
63755
|
-
sources: scan.sources,
|
|
63756
|
-
models:
|
|
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
|
+
}
|
|
63757
64958
|
});
|
|
63758
64959
|
}
|
|
63759
64960
|
const current = await settings.read();
|
|
@@ -63779,7 +64980,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
63779
64980
|
const catalog = store.catalog();
|
|
63780
64981
|
return {
|
|
63781
64982
|
models,
|
|
63782
|
-
catalogCount:
|
|
64983
|
+
catalogCount: listedClinePassCatalogModels(await settings.read()).length,
|
|
63783
64984
|
catalogSources: catalog.sources,
|
|
63784
64985
|
catalogFetchedAt: Math.round(catalog.fetchedAt),
|
|
63785
64986
|
added
|
|
@@ -63995,6 +65196,11 @@ function registerClinePassTools(ctx, options) {
|
|
|
63995
65196
|
type: "array",
|
|
63996
65197
|
items: { type: "string" },
|
|
63997
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."
|
|
63998
65204
|
}
|
|
63999
65205
|
},
|
|
64000
65206
|
timeoutMs: 6e5,
|
|
@@ -64035,6 +65241,11 @@ function registerClinePassTools(ctx, options) {
|
|
|
64035
65241
|
type: "string",
|
|
64036
65242
|
required: true
|
|
64037
65243
|
},
|
|
65244
|
+
has: {
|
|
65245
|
+
type: "array",
|
|
65246
|
+
required: true,
|
|
65247
|
+
items: { type: "string" }
|
|
65248
|
+
},
|
|
64038
65249
|
warnings: {
|
|
64039
65250
|
type: "array",
|
|
64040
65251
|
required: true,
|
|
@@ -64062,6 +65273,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
64062
65273
|
const result = await engine.test(model, {
|
|
64063
65274
|
upstreams: Array.isArray(args.upstreams) ? args.upstreams.map(String) : void 0,
|
|
64064
65275
|
exclude: Array.isArray(args.exclude) ? args.exclude.map(String) : void 0,
|
|
65276
|
+
has: Array.isArray(args.has) ? args.has.map(String) : void 0,
|
|
64065
65277
|
signal: exec.signal
|
|
64066
65278
|
});
|
|
64067
65279
|
return {
|
|
@@ -64073,6 +65285,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
64073
65285
|
pipeline: result.pipeline,
|
|
64074
65286
|
account: result.account,
|
|
64075
65287
|
adherence: result.adherence,
|
|
65288
|
+
has: result.has,
|
|
64076
65289
|
warnings: result.warnings,
|
|
64077
65290
|
trace: result.trace.map((entry) => ({
|
|
64078
65291
|
upstream: entry.upstream,
|
|
@@ -64116,6 +65329,11 @@ function registerClinePassTools(ctx, options) {
|
|
|
64116
65329
|
"none"
|
|
64117
65330
|
],
|
|
64118
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."
|
|
64119
65337
|
}
|
|
64120
65338
|
},
|
|
64121
65339
|
timeoutMs: 3e4,
|
|
@@ -64150,6 +65368,11 @@ function registerClinePassTools(ctx, options) {
|
|
|
64150
65368
|
type: "string",
|
|
64151
65369
|
required: true
|
|
64152
65370
|
},
|
|
65371
|
+
has: {
|
|
65372
|
+
type: "array",
|
|
65373
|
+
required: true,
|
|
65374
|
+
items: { type: "string" }
|
|
65375
|
+
},
|
|
64153
65376
|
warnings: {
|
|
64154
65377
|
type: "array",
|
|
64155
65378
|
required: true,
|
|
@@ -64161,7 +65384,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
64161
65384
|
`pinned ${value.model}`,
|
|
64162
65385
|
`upstreams: ${value.pinned.length === 0 ? "(auto)" : value.pinned.join(" > ")}`,
|
|
64163
65386
|
`exclude: ${value.excluded.length === 0 ? "(none)" : value.excluded.join(", ")}`,
|
|
64164
|
-
`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(",")}`,
|
|
64165
65388
|
...value.warnings.map((warning) => `⚠ ${warning}`)
|
|
64166
65389
|
].join("\n"))
|
|
64167
65390
|
},
|
|
@@ -64172,7 +65395,8 @@ function registerClinePassTools(ctx, options) {
|
|
|
64172
65395
|
upstreams: Array.isArray(args.upstreams) ? args.upstreams.map(String) : current.upstreams,
|
|
64173
65396
|
exclude: Array.isArray(args.exclude) ? args.exclude.map(String) : current.exclude,
|
|
64174
65397
|
pinMode: args.pinMode !== void 0 ? args.pinMode === "preferred" ? "preferred" : "strict" : current.pinMode,
|
|
64175
|
-
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
|
|
64176
65400
|
};
|
|
64177
65401
|
await settings.setPin(model, next);
|
|
64178
65402
|
const meta = store.metaOf(model);
|
|
@@ -64194,6 +65418,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
64194
65418
|
excluded: next.exclude,
|
|
64195
65419
|
pinMode: normalizePinMode(next.pinMode),
|
|
64196
65420
|
sort: next.sort,
|
|
65421
|
+
has: next.has,
|
|
64197
65422
|
warnings
|
|
64198
65423
|
};
|
|
64199
65424
|
}
|
|
@@ -64426,16 +65651,15 @@ function registerClinePassTools(ctx, options) {
|
|
|
64426
65651
|
/** Every id the visibility toggle may act on, including shipped ones. */
|
|
64427
65652
|
function currentModelsForVisibility(settings) {
|
|
64428
65653
|
const ids = new Set(DEFAULT_MODELS);
|
|
64429
|
-
for (const model of settings
|
|
65654
|
+
for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
|
|
64430
65655
|
return [...ids];
|
|
64431
65656
|
}
|
|
64432
65657
|
//#endregion
|
|
64433
65658
|
//#region src/host/cline-pass/line.ts
|
|
64434
65659
|
/** Every id the line knows, before the visibility selection is applied. */
|
|
64435
|
-
function clinePassKnownModelIds(settings,
|
|
65660
|
+
function clinePassKnownModelIds(settings, _catalogIds = []) {
|
|
64436
65661
|
const ids = new Set(DEFAULT_MODELS);
|
|
64437
|
-
for (const model of settings
|
|
64438
|
-
if (settings.exposeCatalog) for (const id of catalogIds) ids.add(id);
|
|
65662
|
+
for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
|
|
64439
65663
|
return [...ids];
|
|
64440
65664
|
}
|
|
64441
65665
|
/** Every id the line advertises right now. */
|
|
@@ -64459,7 +65683,7 @@ function createClinePassLine(ctx, options) {
|
|
|
64459
65683
|
accountPool,
|
|
64460
65684
|
store,
|
|
64461
65685
|
pinFor: (model) => ({
|
|
64462
|
-
...modelSettings.status().pins[model] ?? defaultPin(),
|
|
65686
|
+
...modelSettings.status().pins[model] ?? defaultPin(model),
|
|
64463
65687
|
spelling: modelSettings.status().pinSpelling
|
|
64464
65688
|
}),
|
|
64465
65689
|
baseURL: () => modelSettings.status().baseURL,
|
|
@@ -66458,13 +67682,44 @@ async function fetchQoderUserInfo(accessToken, region, options = {}) {
|
|
|
66458
67682
|
};
|
|
66459
67683
|
}
|
|
66460
67684
|
/**
|
|
66461
|
-
*
|
|
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":…}\`。
|
|
66462
67710
|
*
|
|
66463
67711
|
* PAT 不能直接鉴权,必须先兑换(docs.qoder.com/cli/authentication + pi-provider pat.ts)。
|
|
66464
|
-
*
|
|
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` 的形状在实测里仍可能被服务端接受,但没有理由不发这个官方字段。
|
|
66465
67718
|
*/
|
|
66466
67719
|
async function exchangeQoderJobToken(personalToken, region, options = {}) {
|
|
66467
|
-
const
|
|
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, {
|
|
66468
67723
|
method: "POST",
|
|
66469
67724
|
headers: {
|
|
66470
67725
|
"content-type": "application/json",
|
|
@@ -66473,12 +67728,15 @@ async function exchangeQoderJobToken(personalToken, region, options = {}) {
|
|
|
66473
67728
|
"cosy-clienttype": "5",
|
|
66474
67729
|
"user-agent": USER_AGENT
|
|
66475
67730
|
},
|
|
66476
|
-
body: JSON.stringify({
|
|
67731
|
+
body: JSON.stringify({
|
|
67732
|
+
personal_token: personalToken,
|
|
67733
|
+
machine_id: resolveQoderMachineId(env)
|
|
67734
|
+
}),
|
|
66477
67735
|
signal: timeoutSignal(options.signal, DISCOVERY_TIMEOUT_MS)
|
|
66478
67736
|
});
|
|
66479
67737
|
if (!response.ok) {
|
|
66480
67738
|
const detail = qoderErrorDetail(await response.text().catch(() => ""), 160);
|
|
66481
|
-
throw new
|
|
67739
|
+
throw new QoderPatExchangeError("Qoder(订阅) PAT 兑换失败(HTTP " + response.status + ")" + (detail === "" ? "" : ":" + detail), response.status, region);
|
|
66482
67740
|
}
|
|
66483
67741
|
const payload = await response.json().catch(() => void 0);
|
|
66484
67742
|
if (!isRecord$7(payload)) throw new Error("Qoder(订阅) PAT 兑换响应不是 JSON 对象");
|
|
@@ -67921,11 +69179,42 @@ async function qoderCredentialsFromDevicePoll(ready, machineId, options = {}) {
|
|
|
67921
69179
|
* 兑换 + userinfo 补齐;**PAT 本体留在凭据里**(host-only),因为 PAT 的「刷新」
|
|
67922
69180
|
* 就是拿它重新兑换一把 job token(pi-provider 把 PAT 编进 refresh 字段,
|
|
67923
69181
|
* 本线路把它放进一个具名字段——同一件事,更好读)。
|
|
69182
|
+
*
|
|
69183
|
+
* ---------------------------------------------------------------------------
|
|
69184
|
+
* 自动补认区域
|
|
69185
|
+
* ---------------------------------------------------------------------------
|
|
69186
|
+
* 区域是**凭据的属性**,但 PAT 字符串本身不自述区域,而卡片上的区域选择器默认停在
|
|
69187
|
+
* 全球区。中国站的 PAT 拿去全球区端点兑换,上游回的是 400 `BadRequest`——没有
|
|
69188
|
+
* 任何一句说明「你打错了区」,用户看到的只有一串 requestId。这一步因此不是
|
|
69189
|
+
* 「多试一次」,而是**把选错区域的这个必然失败翻译成一次成功的登录**。
|
|
69190
|
+
*
|
|
69191
|
+
* 只在明确的 token 被拒状态下换区({@link qoderPatRejectionStatus}):HTTP 429、
|
|
69192
|
+
* 5xx、网络错误都不换——那些是瞬时故障,换区重发只会把一个可重试的问题变成两次
|
|
69193
|
+
* 打上游。首次命中的区域写进凭据,之后刷新照它走。
|
|
67924
69194
|
*/
|
|
67925
69195
|
async function qoderCredentialsFromPat(personalToken, region, options = {}) {
|
|
67926
69196
|
const pat = personalToken.trim();
|
|
67927
69197
|
if (pat === "") throw new Error("Qoder(订阅) 的 PAT 是空的。");
|
|
67928
|
-
|
|
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
|
+
}
|
|
67929
69218
|
let uid = null;
|
|
67930
69219
|
let email = null;
|
|
67931
69220
|
let name = null;
|
|
@@ -67933,7 +69222,7 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
|
|
|
67933
69222
|
let organizationTags = [];
|
|
67934
69223
|
let dataPolicyAgreed = false;
|
|
67935
69224
|
try {
|
|
67936
|
-
const info = await fetchQoderUserInfo(exchanged.token,
|
|
69225
|
+
const info = await fetchQoderUserInfo(exchanged.token, resolved, options);
|
|
67937
69226
|
uid = info.userID === "" ? null : info.userID;
|
|
67938
69227
|
email = info.email === "" ? null : info.email;
|
|
67939
69228
|
name = info.name === "" ? null : info.name;
|
|
@@ -67945,7 +69234,7 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
|
|
|
67945
69234
|
accessToken: exchanged.token,
|
|
67946
69235
|
refreshToken: exchanged.refreshToken,
|
|
67947
69236
|
expiresAt: exchanged.expiresAt,
|
|
67948
|
-
region,
|
|
69237
|
+
region: resolved,
|
|
67949
69238
|
origin: "pat",
|
|
67950
69239
|
uid,
|
|
67951
69240
|
email,
|
|
@@ -67957,6 +69246,10 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
|
|
|
67957
69246
|
authenticatedAt: Date.now()
|
|
67958
69247
|
};
|
|
67959
69248
|
}
|
|
69249
|
+
/** 区域的短名(只用于拼给人看的一句话,卡片另有自己的文案)。 */
|
|
69250
|
+
function regionLabel(region) {
|
|
69251
|
+
return REGION_HOSTS$1[region].title;
|
|
69252
|
+
}
|
|
67960
69253
|
/**
|
|
67961
69254
|
* 刷新一份凭据(号池 refresh 钩子的实现)。
|
|
67962
69255
|
*
|
|
@@ -67968,6 +69261,11 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
|
|
|
67968
69261
|
*
|
|
67969
69262
|
* 失败带上 \`status\`(有 HTTP 状态时),号池据此分类「这个账号要重登」还是
|
|
67970
69263
|
* 「瞬时故障,别判死」。
|
|
69264
|
+
*
|
|
69265
|
+
* PAT 分支走的是 {@link qoderCredentialsFromPat},因此**也和首次登录一样会补认区域**:
|
|
69266
|
+
* 一份区域记错了的旧凭据,会在下一次刷新时自己搬到真正认它的那一区(区域本来就是
|
|
69267
|
+
* 凭据的属性,而 PAT 是唯一能问出它归属的原料)。这是修好「区域选错 → 永远 400」
|
|
69268
|
+
* 的第二半——只修首次登录的话,落盘里已有的错区账号会一直刷不上。
|
|
67971
69269
|
*/
|
|
67972
69270
|
async function refreshQoderCredential(credentials, fetchFn = fetch) {
|
|
67973
69271
|
if (credentials.origin === "pat" && credentials.personalToken !== null) return {
|
|
@@ -68254,8 +69552,13 @@ function qoderPoolQuota(snapshot) {
|
|
|
68254
69552
|
*
|
|
68255
69553
|
* 401/403 → \`'expired'\`(上游明说这把 token 不行了,这个账号要重登);
|
|
68256
69554
|
* 其余(网络故障、5xx、形状变了)→ undefined:瞬时故障不该把一个账号判死。
|
|
69555
|
+
*
|
|
69556
|
+
* PAT 来源的刷新是「拿存下的 PAT 重兑换」,它的 400 同样是终局:上游读完了
|
|
69557
|
+
* `personal_token` 才拒绝(见 client.ts 的 {@link QoderPatExchangeError}),
|
|
69558
|
+
* 重试不会变好,只有重新生成 PAT 能救这个账号。
|
|
68257
69559
|
*/
|
|
68258
69560
|
function qoderRefreshFailureStatus(error) {
|
|
69561
|
+
if (error instanceof QoderPatExchangeError && qoderPatRejectionStatus(error.status)) return "expired";
|
|
68259
69562
|
const status = error?.status;
|
|
68260
69563
|
if (status === 401 || status === 403) return "expired";
|
|
68261
69564
|
const message = error instanceof Error ? error.message : "";
|
|
@@ -68729,7 +70032,13 @@ function patchFromBody(body) {
|
|
|
68729
70032
|
if (body.selectedAccountId !== void 0) patch.selectedAccountId = typeof body.selectedAccountId === "string" ? body.selectedAccountId : null;
|
|
68730
70033
|
return patch;
|
|
68731
70034
|
}
|
|
68732
|
-
/**
|
|
70035
|
+
/**
|
|
70036
|
+
* 请求体里的区域。
|
|
70037
|
+
*
|
|
70038
|
+
* 认不出时回落全球区(它是唯一支持设备流的,且是卡片默认值),但**只对设备流成立**:
|
|
70039
|
+
* 设备流本来就只有全球区。PAT 粘贴的兜底不靠这一层——`qoderCredentialsFromPat`
|
|
70040
|
+
* 会把「区域猜错」这件事自己修好(见该函数的注释),所以这里不需要猜得更聪明。
|
|
70041
|
+
*/
|
|
68733
70042
|
function regionFromBody(body) {
|
|
68734
70043
|
return isQoderRegion(body.region) ? body.region : "global";
|
|
68735
70044
|
}
|
|
@@ -74325,9 +75634,7 @@ function effectiveAllowedRoutes(agent, preference, sessions, scope = "session")
|
|
|
74325
75634
|
return authorizedRoutesFor(agent, sessions) ?? (scope === "preference" ? preference?.allowedModels ?? [] : []);
|
|
74326
75635
|
}
|
|
74327
75636
|
/**
|
|
74328
|
-
*
|
|
74329
|
-
* `list_subagent_models` result when the calling Session is governed by an
|
|
74330
|
-
* allowlist.
|
|
75637
|
+
* State a Session's routing posture in the `list_subagent_models` result.
|
|
74331
75638
|
*
|
|
74332
75639
|
* Calling `list_subagent_models({})` upstream returns only provider names
|
|
74333
75640
|
* (`minimax-code — MiniMax Code`), and calling it with `provider` prints
|
|
@@ -74336,18 +75643,38 @@ function effectiveAllowedRoutes(agent, preference, sessions, scope = "session")
|
|
|
74336
75643
|
* here lets even a single `list_subagent_models({})` call hand the model every
|
|
74337
75644
|
* authorized route in the form `subagent` expects, eliminating any reason to
|
|
74338
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.
|
|
74339
75653
|
* @param output - Original text returned by `list_subagent_models`.
|
|
74340
|
-
* @param allowed - Routes the calling Session authorizes.
|
|
74341
|
-
* @returns the output with the Session
|
|
75654
|
+
* @param allowed - Routes the calling Session authorizes, empty when none is in force.
|
|
75655
|
+
* @returns the output with the Session routing posture appended.
|
|
74342
75656
|
*/
|
|
74343
75657
|
function enrichListSubagentModelsOutput(output, allowed) {
|
|
74344
|
-
if (allowed.length === 0)
|
|
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
|
+
}
|
|
74345
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):";
|
|
74346
75663
|
if (output.includes(header)) return output;
|
|
74347
75664
|
const suffix = `${header}\n${allowed.map((route) => `- ${route.provider}/${route.model} -> provider: "${route.provider}", model: "${route.model}"`).join("\n")}`;
|
|
74348
75665
|
return output.trim().length === 0 ? suffix : `${output}\n\n${suffix}`;
|
|
74349
75666
|
}
|
|
74350
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
|
+
/**
|
|
74351
75678
|
* Rewrite the upstream `list_subagent_models` and `subagent` tool/parameter
|
|
74352
75679
|
* descriptions when the calling Session enforces an allowlist.
|
|
74353
75680
|
*
|
|
@@ -74548,7 +75875,6 @@ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
|
|
|
74548
75875
|
const decision = await next();
|
|
74549
75876
|
if (exec.name !== "list_subagent_models" || result.isError === true || decision?.kind !== "accept") return decision;
|
|
74550
75877
|
const allowed = effectiveAllowedRoutes(exec.agent, subagentModelSelectionPreference(settings), sessions, scope);
|
|
74551
|
-
if (allowed.length === 0) return decision;
|
|
74552
75878
|
const currentValue = decision.value !== void 0 ? decision.value : result.value;
|
|
74553
75879
|
if (typeof currentValue === "string") {
|
|
74554
75880
|
const enriched = enrichListSubagentModelsOutput(currentValue, allowed);
|
|
@@ -74719,15 +76045,25 @@ function candidatePackageRoots(entryScript = process.argv[1]) {
|
|
|
74719
76045
|
return [...roots];
|
|
74720
76046
|
}
|
|
74721
76047
|
/**
|
|
74722
|
-
* Whether a package is installed in any candidate node_modules tree.
|
|
74723
|
-
*
|
|
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.
|
|
74724
76050
|
*/
|
|
74725
76051
|
function packageInstalled(specifier, roots = candidatePackageRoots()) {
|
|
74726
76052
|
const pkg = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
76053
|
+
const subpath = specifier.slice(pkg.length);
|
|
74727
76054
|
for (const root of roots) {
|
|
74728
76055
|
let dir = root;
|
|
74729
76056
|
for (;;) {
|
|
74730
|
-
|
|
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
|
+
}
|
|
74731
76067
|
const parent = dirname(dir);
|
|
74732
76068
|
if (parent === dir) break;
|
|
74733
76069
|
dir = parent;
|
|
@@ -75082,6 +76418,12 @@ function installBundledPresets(host, install) {
|
|
|
75082
76418
|
*/
|
|
75083
76419
|
/** The row package whose presence marks a harness that takes presets from a row. */
|
|
75084
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";
|
|
75085
76427
|
/** The id the `dispatch` preset is selectable under. */
|
|
75086
76428
|
const DISPATCH_PRESET_ID = "dispatch";
|
|
75087
76429
|
/**
|
|
@@ -75129,7 +76471,7 @@ Criterion: whether spawn_teammate and wait_agent exist (in PTC mode: tools.spawn
|
|
|
75129
76471
|
## R0 · Complexity Triage (Triage First, Then Choose the Workflow)
|
|
75130
76472
|
Upon receiving a task, determine its tier first, write one line stating "Tier + Rationale", and then decide which phases to run.
|
|
75131
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.
|
|
75132
|
-
- L1 Moderate:
|
|
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.
|
|
75133
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.
|
|
75134
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.
|
|
75135
76477
|
|
|
@@ -75146,21 +76488,25 @@ For L2 tasks, never execute work yourself outside these six categories: (1) Plan
|
|
|
75146
76488
|
For L0/L1 tasks, execute directly—do not spawn subagents just to look like you are orchestrating.
|
|
75147
76489
|
|
|
75148
76490
|
## R2 · Model Routing (Check Availability First, Then Decide How to Dispatch)
|
|
75149
|
-
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
|
|
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.
|
|
75150
76492
|
|
|
75151
|
-
[
|
|
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.
|
|
75152
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.
|
|
75153
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.
|
|
75154
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.
|
|
75155
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.
|
|
75156
76498
|
|
|
75157
|
-
[
|
|
75158
|
-
-
|
|
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.
|
|
75159
76505
|
- Dispatch and verify per R3–R6 as usual, simply omitting the route: line.
|
|
75160
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.
|
|
75161
76507
|
|
|
75162
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.
|
|
75163
|
-
It has no provider/model parameters, so the explicit-route rule under [
|
|
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.
|
|
75164
76510
|
Choose models based on task nature and model capability; keep model selection consistent across tasks of the same kind within a batch.
|
|
75165
76511
|
|
|
75166
76512
|
## R3 · Subagent Autonomy
|
|
@@ -75170,21 +76516,26 @@ A subagent has full authority to plan, execute, and self-verify within its assig
|
|
|
75170
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.
|
|
75171
76517
|
|
|
75172
76518
|
## R5 · Dispatch Discipline
|
|
75173
|
-
Every task brief must contain
|
|
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.
|
|
75174
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.
|
|
75175
76523
|
Precondition: never dispatch implementation subagents before the clarification interview has converged.
|
|
75176
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.
|
|
75177
76525
|
|
|
75178
76526
|
## R6 · Review & Acceptance
|
|
75179
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.
|
|
75180
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.
|
|
75181
|
-
Re-dispatch
|
|
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.
|
|
75182
76532
|
|
|
75183
76533
|
## R-W · Turn Boundary (Waiting / Goals / Questions)
|
|
75184
76534
|
|
|
75185
76535
|
All three rules govern the boundary of a turn: after dispatching, before continuing, and before stopping.
|
|
75186
76536
|
|
|
75187
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.
|
|
75188
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.
|
|
75189
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.
|
|
75190
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.
|
|
@@ -75200,11 +76551,12 @@ All three rules govern the boundary of a turn: after dispatching, before continu
|
|
|
75200
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.
|
|
75201
76552
|
|
|
75202
76553
|
## R7 · Documentation
|
|
75203
|
-
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.
|
|
75204
76556
|
|
|
75205
76557
|
## R8 · Fallback
|
|
75206
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.
|
|
75207
|
-
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
|
|
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.
|
|
75208
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.
|
|
75209
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.
|
|
75210
76562
|
|
|
@@ -75212,7 +76564,7 @@ Reset: fallback applies only to the current task. When the next task begins, alw
|
|
|
75212
76564
|
1. Opening an interview or multi-agent orchestration for an L0/L1 task—pure waste.
|
|
75213
76565
|
2. Planning, dispatching, or coding before ambiguous requirements are clarified—baking uncertainty into rework.
|
|
75214
76566
|
3. Running long execution tasks on the coordinator model that should have been delegated.
|
|
75215
|
-
4. Omitting provider/model
|
|
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).
|
|
75216
76568
|
5. Directly editing a module assigned to a subagent.
|
|
75217
76569
|
6. Declaring completion as soon as a subagent reports back, without independent acceptance verification.
|
|
75218
76570
|
7. Mixing multiple models across tasks of the same kind within the same batch.
|
|
@@ -75225,6 +76577,10 @@ Reset: fallback applies only to the current task. When the next task begins, alw
|
|
|
75225
76577
|
14. Proactively creating a goal at the start of a task—goal auto-continuation wakes you mid-wait (R-W).
|
|
75226
76578
|
15. Ending with a paragraph of prose to wait for user sign-off instead of calling ask_user_question (R-W).
|
|
75227
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]).
|
|
75228
76584
|
`
|
|
75229
76585
|
}
|
|
75230
76586
|
},
|
|
@@ -75431,15 +76787,26 @@ Reset: fallback applies only to the current task. When the next task begins, alw
|
|
|
75431
76787
|
* `unknown prompt variable "{{cwd}}"` at render time. On 0.1.7–0.2.0 that package
|
|
75432
76788
|
* is absent and `{{cwd}}` is still the only way the persona states the workspace,
|
|
75433
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.
|
|
75434
76795
|
* @param resolves - whether a bare specifier resolves in this installation.
|
|
75435
76796
|
* @returns the preset definition for the running harness generation.
|
|
75436
76797
|
*/
|
|
75437
76798
|
function resolveDispatchPreset(resolves = resolvesFromHere) {
|
|
75438
|
-
|
|
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;
|
|
75439
76802
|
return {
|
|
75440
76803
|
...DISPATCH_PRESET,
|
|
75441
76804
|
plugins: DISPATCH_PRESET.plugins.map((row) => {
|
|
75442
|
-
if (row.id
|
|
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;
|
|
75443
76810
|
const { suffix: _omitted, ...rest } = row.config;
|
|
75444
76811
|
return {
|
|
75445
76812
|
...row,
|
|
@@ -77841,6 +79208,7 @@ const Config = z.object({
|
|
|
77841
79208
|
reasoningCollapseGuard: z.union([z.object({}), z.const(false)]).default(false),
|
|
77842
79209
|
noAnswerResume: z.union([z.object({}), z.const(false)]).default({}),
|
|
77843
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 凭据库,服务器推荐)。"),
|
|
77844
79212
|
progressUpdates: z.boolean().default(true).volatile().description("启用进展更新:允许 agent 用 progress_update 工具主动汇报多步任务的进展,并在侧栏展示。"),
|
|
77845
79213
|
progressUpdatesNudgeThreshold: z.number().step(1).min(1).max(50).default(8).volatile().description("催促阈值:连续这么多轮带工具调用却没有进展更新时,提醒 agent 发一条进展。"),
|
|
77846
79214
|
progressUpdatesLanguage: z.union([
|
|
@@ -77859,6 +79227,14 @@ const inject = [
|
|
|
77859
79227
|
];
|
|
77860
79228
|
function apply(ctx, pluginConfig = {}) {
|
|
77861
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
|
+
});
|
|
77862
79238
|
if (pluginConfig.syncAgentPresets !== false) ctx.effect(() => installBundledPresets(ctx, installDispatchPreset(ctx)), "dsh-chatgpt-subscription: agent preset sync");
|
|
77863
79239
|
const store = createPlatformTokenStore();
|
|
77864
79240
|
const preferences = registerPreferenceStore(ctx.settings);
|
|
@@ -78230,7 +79606,7 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
78230
79606
|
}))
|
|
78231
79607
|
});
|
|
78232
79608
|
};
|
|
78233
|
-
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 });
|
|
78234
79610
|
const routable = (accounts) => accounts.some((account) => account.authStatus === void 0 || account.authStatus === "ok");
|
|
78235
79611
|
const disposeHubOverview = registerHubOverviewRoutes(ctx, [
|
|
78236
79612
|
{
|