dsh-codex-subscription 1.2.2 → 1.3.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/lib/index.js CHANGED
@@ -496,7 +496,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0 } = {
496
496
  }
497
497
  //#endregion
498
498
  //#region src/version.js
499
- const PACKAGE_VERSION = "1.2.2";
499
+ const PACKAGE_VERSION = "1.3.0";
500
500
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
501
501
  //#endregion
502
502
  //#region src/codex-search.js
@@ -505,7 +505,7 @@ const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
505
505
  const DEFAULT_MODEL = "gpt-5.6-luna";
506
506
  const MAX_OUTPUT_TOKENS = 4096;
507
507
  const MAX_SOURCE_DATE = 64;
508
- const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
508
+ const record$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
509
509
  const nonEmpty$1 = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
510
510
  const displayText = (value) => {
511
511
  const text = nonEmpty$1(value)?.replace(/\s+/gu, " ").trim();
@@ -518,7 +518,7 @@ const boundedDisplayText = (value, maximum) => {
518
518
  return `${text.slice(0, maximum - 1)}…`;
519
519
  };
520
520
  function sourceOf(value) {
521
- if (!record$2(value)) return void 0;
521
+ if (!record$3(value)) return void 0;
522
522
  const url = nonEmpty$1(value.url);
523
523
  if (url === void 0) return void 0;
524
524
  let parsed;
@@ -539,7 +539,7 @@ function sourceOf(value) {
539
539
  };
540
540
  }
541
541
  function parseSearchResponse(value) {
542
- if (!record$2(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
542
+ if (!record$3(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
543
543
  const seen = /* @__PURE__ */ new Set();
544
544
  const sources = [];
545
545
  for (const result of value.results ?? []) {
@@ -631,7 +631,7 @@ const PNG_SIGNATURE = Buffer.from([
631
631
  26,
632
632
  10
633
633
  ]);
634
- const record$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
634
+ const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
635
635
  const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
636
636
  function encodedLimit(decodedBytes) {
637
637
  return Math.ceil(decodedBytes / 3) * 4;
@@ -732,7 +732,7 @@ function imageOutputSchema() {
732
732
  }
733
733
  function responseMetadata(value) {
734
734
  const data = Array.isArray(value?.data) ? value.data[0] : void 0;
735
- const encoded = record$1(data) ? data.b64_json : void 0;
735
+ const encoded = record$2(data) ? data.b64_json : void 0;
736
736
  if (typeof encoded !== "string") throw new Error("Codex returned no image data");
737
737
  return {
738
738
  encoded,
@@ -865,11 +865,11 @@ async function createSubscriptionDiagnostics({ auth, preferences }) {
865
865
  //#region src/usage.js
866
866
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
867
867
  const DEFAULT_TTL_MS = 6e4;
868
- const DEFAULT_TIMEOUT_MS = 15e3;
869
- const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
868
+ const DEFAULT_TIMEOUT_MS$1 = 15e3;
869
+ const record$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
870
870
  function windowOf(value) {
871
871
  if (value === void 0 || value === null) return void 0;
872
- if (!record(value)) throw new Error("Codex returned a malformed rate-limit window");
872
+ if (!record$1(value)) throw new Error("Codex returned a malformed rate-limit window");
873
873
  const used = value.used_percent;
874
874
  const seconds = value.limit_window_seconds;
875
875
  if (!Number.isFinite(used) || used < 0 || used > 100) throw new Error("Codex returned an invalid used percentage");
@@ -884,7 +884,7 @@ function windowOf(value) {
884
884
  }
885
885
  function limitOf(id, name, value) {
886
886
  if (value === void 0 || value === null) return void 0;
887
- if (!record(value)) throw new Error("Codex returned malformed rate-limit details");
887
+ if (!record$1(value)) throw new Error("Codex returned malformed rate-limit details");
888
888
  const windows = [windowOf(value.primary_window), windowOf(value.secondary_window)].filter(Boolean);
889
889
  return windows.length === 0 ? void 0 : {
890
890
  id,
@@ -903,7 +903,7 @@ function epochSeconds(value, label) {
903
903
  }
904
904
  function creditsOf(value) {
905
905
  if (value === void 0 || value === null) return void 0;
906
- if (!record(value) || typeof value.has_credits !== "boolean" || typeof value.unlimited !== "boolean") throw new Error("Codex returned malformed credit details");
906
+ if (!record$1(value) || typeof value.has_credits !== "boolean" || typeof value.unlimited !== "boolean") throw new Error("Codex returned malformed credit details");
907
907
  if (!value.has_credits) return void 0;
908
908
  return {
909
909
  unlimited: value.unlimited,
@@ -912,10 +912,10 @@ function creditsOf(value) {
912
912
  }
913
913
  function individualOf(value) {
914
914
  if (value === void 0 || value === null) return void 0;
915
- if (!record(value)) throw new Error("Codex returned malformed spend control");
915
+ if (!record$1(value)) throw new Error("Codex returned malformed spend control");
916
916
  const item = value.individual_limit;
917
917
  if (item === void 0 || item === null) return void 0;
918
- if (!record(item) || !Number.isFinite(item.remaining_percent) || item.remaining_percent < 0 || item.remaining_percent > 100) throw new Error("Codex returned an invalid individual-limit percentage");
918
+ if (!record$1(item) || !Number.isFinite(item.remaining_percent) || item.remaining_percent < 0 || item.remaining_percent > 100) throw new Error("Codex returned an invalid individual-limit percentage");
919
919
  const resetsAt = epochSeconds(item.reset_at, "individual-limit reset time");
920
920
  return {
921
921
  limit: decimal(item.limit, "individual limit"),
@@ -926,19 +926,19 @@ function individualOf(value) {
926
926
  }
927
927
  function spendControlReachedOf(value) {
928
928
  if (value === void 0 || value === null) return void 0;
929
- if (!record(value)) throw new Error("Codex returned malformed spend control");
929
+ if (!record$1(value)) throw new Error("Codex returned malformed spend control");
930
930
  if (value.reached === void 0 || value.reached === null) return void 0;
931
931
  if (typeof value.reached !== "boolean") throw new Error("Codex returned an invalid spend-control state");
932
932
  return value.reached;
933
933
  }
934
934
  function resetCreditsOf(value) {
935
935
  if (value === void 0 || value === null) return void 0;
936
- if (!record(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
936
+ if (!record$1(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0) throw new Error("Codex returned malformed reset credit details");
937
937
  return { availableCount: value.available_count };
938
938
  }
939
939
  /** Reduce the provider payload to a browser-safe quota projection. */
940
940
  function parseCodexUsage(value) {
941
- if (!record(value)) throw new Error("Codex returned a malformed usage response");
941
+ if (!record$1(value)) throw new Error("Codex returned a malformed usage response");
942
942
  const rateLimits = [];
943
943
  const seenLimitIds = /* @__PURE__ */ new Set();
944
944
  const addLimit = (limit) => {
@@ -949,7 +949,7 @@ function parseCodexUsage(value) {
949
949
  addLimit(limitOf("codex", "Codex", value.rate_limit));
950
950
  if (value.additional_rate_limits !== void 0 && value.additional_rate_limits !== null && !Array.isArray(value.additional_rate_limits)) throw new Error("Codex returned malformed additional rate limits");
951
951
  for (const entry of value.additional_rate_limits ?? []) {
952
- if (!record(entry) || typeof entry.metered_feature !== "string" || entry.metered_feature.length === 0) throw new Error("Codex returned a malformed additional rate limit");
952
+ if (!record$1(entry) || typeof entry.metered_feature !== "string" || entry.metered_feature.length === 0) throw new Error("Codex returned a malformed additional rate limit");
953
953
  if (entry.limit_name !== void 0 && entry.limit_name !== null && typeof entry.limit_name !== "string") throw new Error("Codex returned an invalid additional rate-limit name");
954
954
  addLimit(limitOf(entry.metered_feature, entry.limit_name || void 0, entry.rate_limit));
955
955
  }
@@ -966,7 +966,7 @@ function parseCodexUsage(value) {
966
966
  ...resetCredits === void 0 ? {} : { resetCredits }
967
967
  };
968
968
  }
969
- const requestSignal = (signal, timeoutMs) => {
969
+ const requestSignal$1 = (signal, timeoutMs) => {
970
970
  const timeout = AbortSignal.timeout(timeoutMs);
971
971
  return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
972
972
  };
@@ -981,7 +981,7 @@ function createCodexUsageReader(options) {
981
981
  const fetchUsage = options.fetch ?? fetch;
982
982
  const now = options.now ?? Date.now;
983
983
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
984
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
984
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
985
985
  let cached;
986
986
  let inFlight;
987
987
  let generation = 0;
@@ -1001,7 +1001,7 @@ function createCodexUsageReader(options) {
1001
1001
  "cache-control": "no-store",
1002
1002
  "user-agent": USER_AGENT
1003
1003
  },
1004
- signal: requestSignal(signal, timeoutMs)
1004
+ signal: requestSignal$1(signal, timeoutMs)
1005
1005
  });
1006
1006
  if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT usage request failed (HTTP ${response.status})`);
1007
1007
  let value;
@@ -1037,6 +1037,185 @@ function createCodexUsageReader(options) {
1037
1037
  });
1038
1038
  }
1039
1039
  //#endregion
1040
+ //#region src/reset-credits.js
1041
+ const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
1042
+ const CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
1043
+ const RESET_CONFIRM_PHRASE = "USE RESET";
1044
+ const DEFAULT_CONFIRM_DELAY_MS = 5e3;
1045
+ const DEFAULT_CHALLENGE_TTL_MS = 6e4;
1046
+ const DEFAULT_TIMEOUT_MS = 15e3;
1047
+ const MAX_COPY_LENGTH = 240;
1048
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1049
+ const requestSignal = (signal, timeoutMs) => {
1050
+ const timeout = AbortSignal.timeout(timeoutMs);
1051
+ return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
1052
+ };
1053
+ function safeCopy(value) {
1054
+ return typeof value === "string" && value.length > 0 ? value.slice(0, MAX_COPY_LENGTH) : void 0;
1055
+ }
1056
+ function credentialsOf(auth, credential) {
1057
+ const access = auth?.auth?.apiKey;
1058
+ const accountId = credential?.type === "oauth" ? credential.accountId : void 0;
1059
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
1060
+ return {
1061
+ access,
1062
+ accountId
1063
+ };
1064
+ }
1065
+ function parseDetails(value, now) {
1066
+ if (!record(value) || !Number.isSafeInteger(value.available_count) || value.available_count < 0 || !Array.isArray(value.credits)) throw new Error("ChatGPT returned malformed quota reset details");
1067
+ if (value.available_count === 0) throw new Error("No quota reset is available");
1068
+ const available = value.credits.filter((credit) => record(credit) && typeof credit.id === "string" && credit.id.length > 0 && credit.id.length <= 256 && typeof credit.status === "string" && credit.status.toLowerCase() === "available").map((credit) => {
1069
+ const expiresAt = credit.expires_at === void 0 || credit.expires_at === null ? void 0 : credit.expires_at * 1e3;
1070
+ if (expiresAt !== void 0 && (!Number.isSafeInteger(credit.expires_at) || credit.expires_at <= 0)) throw new Error("ChatGPT returned malformed quota reset details");
1071
+ return {
1072
+ credit,
1073
+ expiresAt
1074
+ };
1075
+ }).filter(({ expiresAt }) => expiresAt === void 0 || expiresAt > now).sort((a, b) => (a.expiresAt ?? Number.MAX_SAFE_INTEGER) - (b.expiresAt ?? Number.MAX_SAFE_INTEGER));
1076
+ if (available.length === 0) throw new Error("No usable quota reset is available");
1077
+ return {
1078
+ availableCount: value.available_count,
1079
+ creditId: available[0].credit.id,
1080
+ title: safeCopy(available[0].credit.title),
1081
+ description: safeCopy(available[0].credit.description),
1082
+ creditExpiresAt: available[0].expiresAt
1083
+ };
1084
+ }
1085
+ function parseConsumeResult(value) {
1086
+ if (!record(value) || ![
1087
+ "reset",
1088
+ "nothing_to_reset",
1089
+ "no_credit",
1090
+ "already_redeemed"
1091
+ ].includes(value.code)) throw new Error("ChatGPT returned an unreadable quota reset response");
1092
+ const windowsReset = Array.isArray(value.windows_reset) ? value.windows_reset.filter((item) => typeof item === "string").slice(0, 16) : [];
1093
+ return {
1094
+ code: value.code,
1095
+ windowsReset
1096
+ };
1097
+ }
1098
+ function quotaIsExhausted(usage) {
1099
+ return Array.isArray(usage?.rateLimits) && usage.rateLimits.some((limit) => limit?.id !== "code_review" && Array.isArray(limit?.windows) && limit.windows.some((window) => Number.isFinite(window?.usedPercent) && window.usedPercent >= 100));
1100
+ }
1101
+ /**
1102
+ * Host-only reset redemption. The browser receives an opaque, short-lived
1103
+ * challenge; account ids, bearer tokens, credit ids, and idempotency keys stay
1104
+ * in memory on the host.
1105
+ */
1106
+ function createCodexResetCreditService(options) {
1107
+ const getAuth = options.getAuth;
1108
+ const readCredential = options.readCredential;
1109
+ const usageReader = options.usageReader;
1110
+ const fetchReset = options.fetch ?? fetch;
1111
+ const now = options.now ?? Date.now;
1112
+ const randomUUID$1 = options.randomUUID ?? randomUUID;
1113
+ const confirmDelayMs = options.confirmDelayMs ?? DEFAULT_CONFIRM_DELAY_MS;
1114
+ const challengeTtlMs = options.challengeTtlMs ?? DEFAULT_CHALLENGE_TTL_MS;
1115
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1116
+ const challenges = /* @__PURE__ */ new Map();
1117
+ const resolveCredentials = async (signal) => credentialsOf(await getAuth({ signal }), await readCredential({ signal }));
1118
+ return Object.freeze({
1119
+ async prepare({ signal } = {}) {
1120
+ const { access, accountId } = await resolveCredentials(signal);
1121
+ const response = await fetchReset(CODEX_RESET_CREDITS_URL, {
1122
+ method: "GET",
1123
+ redirect: "error",
1124
+ headers: {
1125
+ authorization: `Bearer ${access}`,
1126
+ "chatgpt-account-id": accountId,
1127
+ accept: "application/json",
1128
+ "cache-control": "no-store",
1129
+ "user-agent": USER_AGENT
1130
+ },
1131
+ signal: requestSignal(signal, timeoutMs)
1132
+ });
1133
+ if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT quota reset request failed (HTTP ${response.status})`);
1134
+ let raw;
1135
+ try {
1136
+ raw = await response.json();
1137
+ } catch {
1138
+ throw new Error("ChatGPT returned unreadable quota reset details");
1139
+ }
1140
+ const details = parseDetails(raw, now());
1141
+ const preparedAt = now();
1142
+ const readyAt = preparedAt + confirmDelayMs;
1143
+ const expiresAt = Math.min(preparedAt + challengeTtlMs, details.creditExpiresAt ?? Number.MAX_SAFE_INTEGER);
1144
+ if (expiresAt <= readyAt) throw new Error("The available quota reset expires too soon");
1145
+ const challengeId = randomUUID$1();
1146
+ challenges.set(challengeId, {
1147
+ state: "prepared",
1148
+ accountId,
1149
+ creditId: details.creditId,
1150
+ redeemRequestId: randomUUID$1(),
1151
+ readyAt,
1152
+ expiresAt
1153
+ });
1154
+ return {
1155
+ challengeId,
1156
+ availableCount: details.availableCount,
1157
+ confirmPhrase: RESET_CONFIRM_PHRASE,
1158
+ readyAt,
1159
+ expiresAt,
1160
+ ...details.title === void 0 ? {} : { title: details.title },
1161
+ ...details.description === void 0 ? {} : { description: details.description }
1162
+ };
1163
+ },
1164
+ async consume({ challengeId, phrase, signal } = {}) {
1165
+ const challenge = typeof challengeId === "string" ? challenges.get(challengeId) : void 0;
1166
+ if (challenge === void 0) throw new Error("This quota reset confirmation is no longer valid");
1167
+ if (challenge.state === "pending") throw new Error("This quota reset is already in progress");
1168
+ if (now() < challenge.readyAt) throw new Error("Wait before confirming this quota reset");
1169
+ if (now() > challenge.expiresAt) {
1170
+ challenges.delete(challengeId);
1171
+ throw new Error("This quota reset confirmation is no longer valid");
1172
+ }
1173
+ if (phrase !== "USE RESET") throw new Error("The quota reset confirmation phrase does not match");
1174
+ challenge.state = "pending";
1175
+ try {
1176
+ if (!quotaIsExhausted(await usageReader.read({
1177
+ force: true,
1178
+ signal
1179
+ }))) throw new Error("The current Codex quota is not exhausted");
1180
+ const { access, accountId } = await resolveCredentials(signal);
1181
+ if (accountId !== challenge.accountId) throw new Error("The signed-in ChatGPT account changed");
1182
+ const response = await fetchReset(CODEX_RESET_CONSUME_URL, {
1183
+ method: "POST",
1184
+ redirect: "error",
1185
+ headers: {
1186
+ authorization: `Bearer ${access}`,
1187
+ "chatgpt-account-id": accountId,
1188
+ accept: "application/json",
1189
+ "content-type": "application/json",
1190
+ "cache-control": "no-store",
1191
+ "user-agent": USER_AGENT
1192
+ },
1193
+ body: JSON.stringify({
1194
+ redeem_request_id: challenge.redeemRequestId,
1195
+ credit_id: challenge.creditId
1196
+ }),
1197
+ signal: requestSignal(signal, timeoutMs)
1198
+ });
1199
+ if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT quota reset request failed (HTTP ${response.status})`);
1200
+ let raw;
1201
+ try {
1202
+ raw = await response.json();
1203
+ } catch {
1204
+ throw new Error("ChatGPT returned an unreadable quota reset response");
1205
+ }
1206
+ const result = parseConsumeResult(raw);
1207
+ usageReader.clear();
1208
+ return result;
1209
+ } finally {
1210
+ challenges.delete(challengeId);
1211
+ }
1212
+ },
1213
+ clear() {
1214
+ challenges.clear();
1215
+ }
1216
+ });
1217
+ }
1218
+ //#endregion
1040
1219
  //#region src/index.js
1041
1220
  const name = "codex-subscription";
1042
1221
  const inject = [
@@ -1066,7 +1245,7 @@ const publicError = (code, message) => ({
1066
1245
  details: { issues: [] }
1067
1246
  }
1068
1247
  });
1069
- function createSubscriptionRpcHandler({ authHandler, usageReader, preferences, diagnosticsReader }) {
1248
+ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, diagnosticsReader }) {
1070
1249
  return async (endpoint, payload, signal) => {
1071
1250
  if (endpoint === "diagnostics") try {
1072
1251
  signal.throwIfAborted();
@@ -1120,8 +1299,40 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, preferences, d
1120
1299
  const message = error instanceof Error && known.has(error.message) ? error.message : "Could not read ChatGPT usage";
1121
1300
  return publicError("internal", message);
1122
1301
  }
1302
+ if (endpoint === "reset-credit/prepare" || endpoint === "reset-credit/consume") try {
1303
+ signal.throwIfAborted();
1304
+ return {
1305
+ ok: true,
1306
+ value: endpoint === "reset-credit/prepare" ? await resetCreditService.prepare({ signal }) : await resetCreditService.consume({
1307
+ challengeId: payload?.challengeId,
1308
+ phrase: payload?.phrase,
1309
+ signal
1310
+ })
1311
+ };
1312
+ } catch (error) {
1313
+ if (signal.aborted) throw error;
1314
+ const known = /* @__PURE__ */ new Set([
1315
+ "ChatGPT subscription is not signed in",
1316
+ "ChatGPT sign-in needs to be renewed",
1317
+ "No quota reset is available",
1318
+ "No usable quota reset is available",
1319
+ "The available quota reset expires too soon",
1320
+ "This quota reset confirmation is no longer valid",
1321
+ "This quota reset is already in progress",
1322
+ "Wait before confirming this quota reset",
1323
+ "The quota reset confirmation phrase does not match",
1324
+ "The current Codex quota is not exhausted",
1325
+ "The signed-in ChatGPT account changed"
1326
+ ]);
1327
+ const fallback = endpoint === "reset-credit/prepare" ? "Could not prepare a quota reset" : "Could not use the quota reset";
1328
+ const message = error instanceof Error && known.has(error.message) ? error.message : fallback;
1329
+ return publicError("internal", message);
1330
+ }
1123
1331
  const result = await authHandler(endpoint, payload, signal);
1124
- if (endpoint === "logout" && result.ok === true) usageReader.clear();
1332
+ if (endpoint === "logout" && result.ok === true) {
1333
+ usageReader.clear();
1334
+ resetCreditService.clear();
1335
+ }
1125
1336
  return result;
1126
1337
  };
1127
1338
  }
@@ -1231,9 +1442,15 @@ function apply(ctx) {
1231
1442
  getAuth: resolveAuth,
1232
1443
  readCredential: (options) => store.read(PROVIDER, options)
1233
1444
  });
1445
+ const resetCreditService = createCodexResetCreditService({
1446
+ getAuth: resolveAuth,
1447
+ readCredential: (options) => store.read(PROVIDER, options),
1448
+ usageReader
1449
+ });
1234
1450
  const handler = createSubscriptionRpcHandler({
1235
1451
  authHandler: createCodexRpcHandler(coordinator, { openExternal: openCodexAuthUrl }),
1236
1452
  usageReader,
1453
+ resetCreditService,
1237
1454
  preferences,
1238
1455
  diagnosticsReader: () => createSubscriptionDiagnostics({
1239
1456
  auth,
@@ -1243,4 +1460,4 @@ function apply(ctx) {
1243
1460
  ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "codex-subscription: loopback account RPC");
1244
1461
  }
1245
1462
  //#endregion
1246
- export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, openCodexAuthUrl, parseCodexUsage };
1463
+ export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, RESET_CONFIRM_PHRASE, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, openCodexAuthUrl, parseCodexUsage };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.2.2",
4
- "description": "ChatGPT and Codex subscriptions for DeepSeek Harness with OAuth login, image generation, quota, and subscription web search",
3
+ "version": "1.3.0",
4
+ "description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "exports": {
@@ -14,7 +14,7 @@
14
14
  "lib/*.js",
15
15
  "cordis.patch.yml",
16
16
  "README.md",
17
- "README.en.md",
17
+ "README.zh-CN.md",
18
18
  "DIRECTORY.md",
19
19
  "AGENTS.md",
20
20
  "compatibility.json",
@@ -52,6 +52,8 @@
52
52
  "chatgpt-plus",
53
53
  "subscription",
54
54
  "quota",
55
+ "quota-reset",
56
+ "fast-mode",
55
57
  "usage",
56
58
  "oauth",
57
59
  "web-search",
package/README.en.md DELETED
@@ -1,210 +0,0 @@
1
- <div align="center">
2
-
3
- # DSH Codex Subscription
4
-
5
- **Use your ChatGPT / Codex subscription directly in DeepSeek Harness**
6
-
7
- No OpenAI API key or Codex CLI. Models, search, quota, and image generation stay inside DSH.
8
-
9
- [![CI](https://github.com/WSL043/dsh-codex-subscription/actions/workflows/ci.yml/badge.svg)](https://github.com/WSL043/dsh-codex-subscription/actions/workflows/ci.yml)
10
- [![npm](https://img.shields.io/npm/v/dsh-codex-subscription?logo=npm&label=npm)](https://www.npmjs.com/package/dsh-codex-subscription)
11
- [![npm downloads](https://img.shields.io/npm/dt/dsh-codex-subscription?logo=npm&label=downloads)](https://www.npmjs.com/package/dsh-codex-subscription)
12
- [![MIT](https://img.shields.io/badge/license-MIT-111111.svg)](LICENSE)
13
- [![Star](https://img.shields.io/github/stars/WSL043/dsh-codex-subscription?style=flat&logo=github&label=Star)](https://github.com/WSL043/dsh-codex-subscription/stargazers)
14
-
15
- [Three-step start](#three-step-start) · [Agent install](#let-an-agent-install-it-recommended) · [Update and uninstall](#update-and-uninstall) · [简体中文](https://github.com/WSL043/dsh-codex-subscription/blob/main/README.md)
16
-
17
- </div>
18
-
19
- <p align="center">
20
- <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/readme-hero-en.webp" width="900" alt="Your Codex subscription inside DSH: subscription models, web search, live quota, and image generation">
21
- </p>
22
-
23
- ## Three-step start
24
-
25
- 1. **Install the plugin.** On Windows, open PowerShell and run the line below. Existing `dsh` and DSH-Portable users can use the standard command that follows.
26
-
27
- ```powershell
28
- irm 'https://github.com/WSL043/dsh-codex-subscription/releases/latest/download/dsh-codex-setup.ps1' | iex
29
- ```
30
-
31
- 2. **Sign in.** Restart DSH yourself, open **Settings -> Codex**, and choose browser sign-in. No Codex CLI and no pasted token are required.
32
- 3. **Use Codex.** Select a Codex model. Quota, subscription search, image generation, and Fast mode remain inside DSH.
33
-
34
- With an existing `dsh` command, install with:
35
-
36
- ```sh
37
- dsh plugin --profile web add dsh-codex-subscription
38
- ```
39
-
40
- From a DSH-Portable product folder, run `./dsh plugin ...` (`.\dsh.exe plugin ...` in Windows PowerShell). See below for the complete official npm, Agent, update, and uninstall routes.
41
-
42
- ## Why this plugin
43
-
44
- | Capability | What you get |
45
- | --- | --- |
46
- | **Subscription models** | Sign in to ChatGPT and use Codex without an OpenAI API key or Codex CLI |
47
- | **Recoverable and diagnosable** | Sign-in state reconciles automatically; Settings can create a support report without credentials or account identifiers |
48
- | **Visible quota** | Keep backend-provided standard Codex, Spark, and other limits separate |
49
- | **Subscription search** | Explicitly choose DSH default search or Codex subscription search |
50
- | **Codex image generation** | Describe an image in a DSH conversation and view the generated result in that session |
51
- | **Fast mode (Beta)** | Switch between Standard and Fast directly in the composer |
52
-
53
- These capabilities reuse the same local ChatGPT sign-in. Subscription routing failures stay visible and never silently switch to another paid route.
54
-
55
- ## Product screen
56
-
57
- <p align="center">
58
- <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/settings-focus-en.png" width="820" alt="Codex subscription settings in DeepSeek Harness">
59
- </p>
60
-
61
- <details>
62
- <summary>View the complete settings screen</summary>
63
-
64
- ![Complete Codex subscription settings in DeepSeek Harness](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/settings-en.png)
65
-
66
- </details>
67
-
68
- ## Prepare DSH
69
-
70
- This plugin is currently compatible through DeepSeek Harness `0.1.1-rc.2` and requires a ChatGPT account that currently has Codex access.
71
-
72
- - Do not want to configure Node.js? Use [DSH-Portable](https://github.com/WSL043/DSH-Portable), a community desktop distribution with portable and installed editions for Windows plus desktop packages for macOS and Linux.
73
- - Prefer the official route? Follow the [DeepSeek Harness run guide](https://github.com/deepseek-ai/deepseek-harness#run).
74
-
75
- ## Install
76
-
77
- ### Let an Agent install it (recommended)
78
-
79
- Send this link directly to your Agent:
80
-
81
- **[Agent install, update, and uninstall guide](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/AGENTS.md)**
82
-
83
- ```text
84
- https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/AGENTS.md
85
- ```
86
-
87
- The guide includes verification steps and tells the Agent to preserve the DSH profile, sign-in, and other plugins.
88
-
89
- ### Manual Windows install
90
-
91
- Open PowerShell and paste this one line:
92
-
93
- ```powershell
94
- irm 'https://github.com/WSL043/dsh-codex-subscription/releases/latest/download/dsh-codex-setup.ps1' | iex
95
- ```
96
-
97
- The lightweight setup checks the current folder, the system command, common locations, and any running official DSH or
98
- [DSH-Portable](https://github.com/WSL043/DSH-Portable), then invokes the official `plugin add`
99
- operation once. It does not recursively scan disks, install pnpm, create a resident command, snapshot a
100
- profile, or download the plugin twice. It needs no administrator access and never restarts DSH. Only
101
- when no existing DSH is found does it use the official npm route pinned to `0.1.1-rc.2`; the setup warns
102
- that first-time dependency resolution can take a while.
103
-
104
- <details>
105
- <summary>Official npm route (Node.js installed)</summary>
106
-
107
- The official `npx @deepseek-ai/dsh web` command does not create a global `dsh` command. Keep the full `npx` prefix when installing the plugin:
108
-
109
- ```sh
110
- npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-codex-subscription
111
- npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web list dsh-codex-subscription --depth 0
112
- npx -y @deepseek-ai/dsh@0.1.1-rc.2 --profile web --dump-config
113
- ```
114
-
115
- </details>
116
-
117
- <details>
118
- <summary>An existing <code>dsh</code> command or a DSH-Portable folder</summary>
119
-
120
- ```sh
121
- dsh plugin --profile web add dsh-codex-subscription
122
- dsh plugin --profile web list dsh-codex-subscription --depth 0
123
- dsh --profile web --dump-config
124
- ```
125
-
126
- For DSH-Portable, replace `dsh` with `./dsh` (`.\dsh.exe` in PowerShell). The plugin list should contain
127
- one `dsh-codex-subscription`, and the config should contain one `codex-subscription` entry.
128
-
129
- </details>
130
-
131
- Restart DSH manually after installation, then:
132
-
133
- 1. Open **Settings -> Codex**.
134
- 2. Sign in with a ChatGPT account that has Codex access.
135
- 3. Choose a search source.
136
- 4. Select a Codex model.
137
-
138
- ## Features
139
-
140
- - ChatGPT OAuth sign-in with credentials kept on the host;
141
- - Codex models and image generation directly inside DSH conversations;
142
- - A clear choice between DSH default search and Codex subscription search;
143
- - Actual backend-provided quota, reset time, and freshness;
144
- - Separate standard Codex, Codex-Spark, Credits, and other independent limits;
145
- - Optional composer quota for the selected Codex model (Beta, off by default);
146
- - Standard or Fast mode for supported Codex models directly in the composer (Beta);
147
- - A copyable support report in Settings that excludes OAuth credentials, account identifiers, and authorization timestamps;
148
- - Visible errors when subscription routing is unavailable, with no silent paid fallback.
149
-
150
- ### Composer quota
151
-
152
- <p align="center">
153
- <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/composer-quota-en.png" width="800" alt="Codex quota inside the composer">
154
- </p>
155
-
156
- The compact percentage appears only for a selected Codex model. Standard Codex uses the lowest remaining
157
- window returned by the service; Spark uses its independent quota. The plugin does not hard-code a
158
- “5-hour + weekly” layout or invent Credits and spending caps that the service did not return.
159
-
160
- ### Composer speed (Beta)
161
-
162
- With a supported Codex model selected, open the composer's model menu to choose Standard or Fast.
163
- Standard adds no icon; only Fast shows a lightning icon before the model name. Spark does not show the speed entry. Fast mode increases speed and uses more Credits;
164
- see the [OpenAI Codex Speed documentation](https://learn.chatgpt.com/docs/agent-configuration/speed) for the current rules.
165
-
166
- ## Update and uninstall
167
-
168
- On Windows, rerun the one-line setup above to update. For the official npm route, use:
169
-
170
- ```sh
171
- npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web update dsh-codex-subscription
172
- npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web list dsh-codex-subscription --depth 0
173
- npx -y @deepseek-ai/dsh@0.1.1-rc.2 --profile web --dump-config
174
- npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web remove dsh-codex-subscription
175
- ```
176
-
177
- These operations preserve the DSH profile, other plugins, and saved sign-in.
178
-
179
- <details>
180
- <summary>Update or uninstall with an existing <code>dsh</code> command</summary>
181
-
182
- ```sh
183
- dsh plugin --profile web update dsh-codex-subscription
184
- dsh plugin --profile web list dsh-codex-subscription --depth 0
185
- dsh --profile web --dump-config
186
- dsh plugin --profile web remove dsh-codex-subscription
187
- ```
188
-
189
- </details>
190
-
191
- From a DSH-Portable folder, replace `dsh` above with `.\dsh.exe`.
192
-
193
- ## Troubleshooting
194
-
195
- - **`dsh` is not recognized:** the official npm route does not create a global `dsh` command; use the complete `npx -y @deepseek-ai/dsh@0.1.1-rc.2 ...` command above;
196
- - **`dsh.exe` is not recognized:** that file is not in the current folder; enter the DSH-Portable folder first or use the one-line Windows setup;
197
- - **DSH-Portable is not found:** enter its folder and rerun setup, or run `.\dsh.exe plugin --profile web add dsh-codex-subscription` directly;
198
- - **More than one DSH exists:** the one-line setup combines running copies with Portable folders detected in common locations; enter its number. For an Agent, use the intended folder or pass `-DshPath` explicitly;
199
- - **Setup still fails:** send the Agent guide above to an Agent. Do not delete the profile or change the system PATH to force an install.
200
- - **Need to report a problem:** generate a **Support diagnostics** report at the bottom of Settings, then open the [bug report form](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml). The form collects exact DSH/plugin versions and reproduction steps. The generated report excludes credentials and account identifiers; still do not attach sign-in URLs, authorization codes, or browser callback addresses.
201
-
202
- The ChatGPT Codex backend and DSH can change independently. This community project is not affiliated with or endorsed by DeepSeek or OpenAI.
203
-
204
- Use the [bug report form](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml) for project feedback.
205
- For DSH plugin discussion, visit [DeepSeek Harness Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions).
206
- Read [SECURITY.md](SECURITY.md) before reporting sensitive issues.
207
-
208
- If this project is useful, the [Star button](https://github.com/WSL043/dsh-codex-subscription/stargazers) helps more DSH users find it.
209
-
210
- [简体中文](README.md) · [MIT](LICENSE)