pi-provider-qoder-cn 0.2.9 → 0.2.17

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/dist/index.js CHANGED
@@ -93,6 +93,14 @@ var init_cn_naming = __esm({
93
93
  });
94
94
 
95
95
  // src/cosy.ts
96
+ var cosy_exports = {};
97
+ __export(cosy_exports, {
98
+ QODER_CLIENT_TYPE: () => QODER_CLIENT_TYPE,
99
+ QODER_GATEWAY_COSY_VERSION: () => QODER_GATEWAY_COSY_VERSION,
100
+ QODER_OPENAPI_COSY_VERSION: () => QODER_OPENAPI_COSY_VERSION,
101
+ buildAuthHeaders: () => buildAuthHeaders,
102
+ getMachineId: () => getMachineId
103
+ });
96
104
  import crypto from "node:crypto";
97
105
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
98
106
  import { homedir } from "node:os";
@@ -379,6 +387,9 @@ function getQoderUsageURL(mode) {
379
387
  function getQoderRefreshURL(mode) {
380
388
  return `${getQoderOpenApiUrl(mode)}/api/v1/jobToken/refresh`;
381
389
  }
390
+ function getQoderDeviceRefreshURL(mode) {
391
+ return `${getQoderOpenApiUrl(mode)}/api/v1/deviceToken/refresh`;
392
+ }
382
393
  function getQoderDeviceLoginURL(codeChallenge, machineID, nonce) {
383
394
  const baseUrl = getQoderRegionConfig("global").deviceLoginUrl;
384
395
  if (!baseUrl) throw new Error("Qoder browser login URL is not configured");
@@ -428,7 +439,7 @@ var init_region = __esm({
428
439
  supportsBrowserLogin: false
429
440
  }
430
441
  };
431
- QODER_PROVIDER_MODES = ["cn"];
442
+ QODER_PROVIDER_MODES = ["cn", "global"];
432
443
  }
433
444
  });
434
445
 
@@ -1099,6 +1110,38 @@ var init_catalog = __esm({
1099
1110
  }
1100
1111
  });
1101
1112
 
1113
+ // src/auth/expiry.ts
1114
+ function resolveTokenExpiryMs(expiresAt, expiresIn, now = Date.now()) {
1115
+ if (expiresAt !== void 0 && expiresAt !== null && expiresAt !== "") {
1116
+ if (typeof expiresAt === "number" && Number.isFinite(expiresAt) && expiresAt > 0) {
1117
+ return expiresAt < 1e12 ? expiresAt * 1e3 : expiresAt;
1118
+ }
1119
+ const parsed = Date.parse(String(expiresAt));
1120
+ if (!Number.isNaN(parsed)) return parsed;
1121
+ const numeric = Number.parseInt(String(expiresAt), 10);
1122
+ if (!Number.isNaN(numeric) && numeric > 0) {
1123
+ return numeric < 1e12 ? numeric * 1e3 : numeric;
1124
+ }
1125
+ }
1126
+ if (typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0) {
1127
+ return now + (expiresIn > ONE_DAY_SECONDS ? expiresIn : expiresIn * 1e3);
1128
+ }
1129
+ return now + FALLBACK_TTL_MS;
1130
+ }
1131
+ function toEpochMs(value, now = Date.now()) {
1132
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
1133
+ const ms = value < 1e12 ? value * 1e3 : value;
1134
+ return ms > now + 365 * 864e5 ? now + FALLBACK_TTL_MS : ms;
1135
+ }
1136
+ var ONE_DAY_SECONDS, FALLBACK_TTL_MS;
1137
+ var init_expiry = __esm({
1138
+ "src/auth/expiry.ts"() {
1139
+ "use strict";
1140
+ ONE_DAY_SECONDS = 86400;
1141
+ FALLBACK_TTL_MS = 24 * 36e5;
1142
+ }
1143
+ });
1144
+
1102
1145
  // src/auth/pat.ts
1103
1146
  var pat_exports = {};
1104
1147
  __export(pat_exports, {
@@ -1145,13 +1188,7 @@ async function exchangeJobToken(pat, mode) {
1145
1188
  if (!data.token) {
1146
1189
  throw new Error("Qoder PAT exchange returned no job token");
1147
1190
  }
1148
- let expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
1149
- if (data.expires_at) {
1150
- const parsed = Date.parse(data.expires_at);
1151
- if (!Number.isNaN(parsed)) expiresAt = parsed;
1152
- } else if (data.expires_in) {
1153
- expiresAt = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
1154
- }
1191
+ const expiresAt = resolveTokenExpiryMs(data.expires_at, data.expires_in);
1155
1192
  return {
1156
1193
  jobToken: data.token,
1157
1194
  jobRefreshToken: data.refresh_token || "",
@@ -1204,6 +1241,7 @@ var init_pat = __esm({
1204
1241
  "use strict";
1205
1242
  init_cosy();
1206
1243
  init_region();
1244
+ init_expiry();
1207
1245
  UA = "pi-provider-qoder-cn";
1208
1246
  PAT_REFRESH_PREFIX = "pat";
1209
1247
  }
@@ -1235,16 +1273,7 @@ function generatePKCE() {
1235
1273
  return { codeVerifier, codeChallenge };
1236
1274
  }
1237
1275
  function parseExpiresAt(s, expiresInSeconds) {
1238
- if (s) {
1239
- const t = Date.parse(s);
1240
- if (!Number.isNaN(t)) return t;
1241
- const ms = Number.parseInt(s, 10);
1242
- if (!Number.isNaN(ms) && ms > 0) return ms;
1243
- }
1244
- if (expiresInSeconds && expiresInSeconds > 0) {
1245
- return Date.now() + expiresInSeconds * 1e3;
1246
- }
1247
- return Date.now() + 30 * 24 * 60 * 60 * 1e3;
1276
+ return resolveTokenExpiryMs(s, expiresInSeconds);
1248
1277
  }
1249
1278
  async function confirmCnEndpoint(callbacks, mode) {
1250
1279
  if (mode !== "cn") return;
@@ -1415,6 +1444,9 @@ async function runDeviceFlow(callbacks) {
1415
1444
  if (!tokenData.token) {
1416
1445
  throw new Error("Device token poll returned empty access token");
1417
1446
  }
1447
+ if (!tokenData.refresh_token) {
1448
+ throw new Error("Device token poll returned no refresh token");
1449
+ }
1418
1450
  const expireMs = parseExpiresAt(tokenData.expires_at, tokenData.expires_in);
1419
1451
  getProgress(callbacks)?.("Fetching user profile...");
1420
1452
  let email = "";
@@ -1462,6 +1494,7 @@ var init_login = __esm({
1462
1494
  init_cosy();
1463
1495
  init_region();
1464
1496
  init_vpc();
1497
+ init_expiry();
1465
1498
  init_pat();
1466
1499
  }
1467
1500
  });
@@ -1478,10 +1511,9 @@ __export(oauth_exports, {
1478
1511
  resolveQoderIdentity: () => resolveQoderIdentity,
1479
1512
  saveCredentialsToAuthFile: () => saveCredentialsToAuthFile
1480
1513
  });
1481
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1514
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, rmSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
1482
1515
  import { homedir as homedir4 } from "node:os";
1483
1516
  import { dirname as dirname3, join as join4 } from "node:path";
1484
- import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
1485
1517
  function getHomeDir4() {
1486
1518
  return process.env.HOME || process.env.USERPROFILE || homedir4();
1487
1519
  }
@@ -1494,17 +1526,17 @@ function clearQoderAuthMemCache() {
1494
1526
  }
1495
1527
  function readAuthFileCached() {
1496
1528
  const authPath = getAuthFilePath();
1497
- if (authFileMem !== void 0) {
1498
- if (authFileMem === null) return null;
1499
- if (authFileMem.path === authPath) return authFileMem.data;
1500
- }
1501
1529
  if (!existsSync4(authPath)) {
1502
1530
  authFileMem = null;
1503
1531
  return null;
1504
1532
  }
1505
1533
  try {
1534
+ const stat = statSync(authPath);
1535
+ if (authFileMem && authFileMem.path === authPath && authFileMem.mtimeMs === stat.mtimeMs) {
1536
+ return authFileMem.data;
1537
+ }
1506
1538
  const data = JSON.parse(readFileSync4(authPath, "utf-8"));
1507
- authFileMem = { path: authPath, data };
1539
+ authFileMem = { path: authPath, data, mtimeMs: stat.mtimeMs };
1508
1540
  return data;
1509
1541
  } catch {
1510
1542
  authFileMem = null;
@@ -1533,8 +1565,25 @@ function saveCredentialsToAuthFile(providerID, credentials) {
1533
1565
  auth = cached ? { ...cached } : {};
1534
1566
  }
1535
1567
  auth[providerID] = { type: "oauth", ...credentials };
1536
- writeFileSync4(authPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1537
- authFileMem = { path: authPath, data: auth };
1568
+ const tmp = `${authPath}.${process.pid}.${Date.now()}.tmp`;
1569
+ writeFileSync4(tmp, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1570
+ let published = false;
1571
+ for (let attempt = 0; attempt < 3 && !published; attempt++) {
1572
+ try {
1573
+ renameSync(tmp, authPath);
1574
+ published = true;
1575
+ } catch (err) {
1576
+ if (attempt === 2) {
1577
+ console.error(`[pi-provider-qoder] rename failed for ${authPath}, falling back to direct write:`, err);
1578
+ writeFileSync4(authPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1579
+ published = true;
1580
+ } else {
1581
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25 * (attempt + 1));
1582
+ }
1583
+ }
1584
+ }
1585
+ rmSync(tmp, { force: true });
1586
+ authFileMem = { path: authPath, data: auth, mtimeMs: statSync(authPath).mtimeMs };
1538
1587
  const q = credentials;
1539
1588
  if (q.access && q.userID) {
1540
1589
  identityCache.set(`${providerID}:${q.access}`, q);
@@ -1547,23 +1596,14 @@ async function autoLoginQoderFromEnvironment(providerID, mode) {
1547
1596
  const pat = getQoderPatForMode(mode);
1548
1597
  if (!pat) return;
1549
1598
  const credentials = await credentialsFromPat(pat, mode);
1550
- if (typeof AuthStorage2?.create === "function") {
1551
- try {
1552
- const authStorage = AuthStorage2.create();
1553
- authStorage.set(providerID, { type: "oauth", ...credentials });
1554
- } catch {
1555
- saveCredentialsToAuthFile(providerID, credentials);
1556
- }
1557
- } else {
1558
- saveCredentialsToAuthFile(providerID, credentials);
1559
- }
1599
+ saveCredentialsToAuthFile(providerID, credentials);
1560
1600
  const qCreds = credentials;
1561
1601
  await updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode);
1562
1602
  }
1563
1603
  function getCachedCredentials(_accessToken, providerID = "qoder") {
1564
1604
  const auth = readAuthFileCached();
1565
1605
  if (!auth) return null;
1566
- const creds = auth[providerID] || (providerID === "qoder" ? auth.qoder : null);
1606
+ const creds = auth[providerID];
1567
1607
  if (creds?.userID || creds?.access) {
1568
1608
  if (creds.access && creds.userID) {
1569
1609
  identityCache.set(`${providerID}:${creds.access}`, creds);
@@ -1595,7 +1635,9 @@ async function resolveQoderIdentity(accessToken, providerID, mode) {
1595
1635
  expires: stored?.expires || 0
1596
1636
  };
1597
1637
  identityCache.set(cacheKey, creds);
1598
- saveCredentialsToAuthFile(providerID, creds);
1638
+ if (stored?.refresh && info.userID && (!stored.access || stored.access === accessToken)) {
1639
+ saveCredentialsToAuthFile(providerID, creds);
1640
+ }
1599
1641
  return creds;
1600
1642
  }
1601
1643
  async function loginQoderForMode(callbacks, mode) {
@@ -1615,65 +1657,93 @@ async function loginQoderForMode(callbacks, mode) {
1615
1657
  const creds = await interactiveLogin(callbacks, mode);
1616
1658
  try {
1617
1659
  const qCreds = creds;
1618
- updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
1619
- });
1660
+ await updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode);
1620
1661
  } catch {
1621
1662
  }
1622
1663
  saveCredentialsToAuthFile(providerID, creds);
1623
1664
  return creds;
1624
1665
  }
1625
- async function refreshQoderTokenForMode(credentials, mode) {
1666
+ async function refreshQoderTokenForMode(credentials, mode, signal) {
1667
+ const providerID = getQoderRegionConfig(mode).providerID;
1668
+ const existing = inFlightRefreshes.get(providerID);
1669
+ if (existing) return existing;
1670
+ const promise = (async () => {
1671
+ try {
1672
+ return await executeRefreshForMode(credentials, mode, signal);
1673
+ } finally {
1674
+ inFlightRefreshes.delete(providerID);
1675
+ }
1676
+ })();
1677
+ inFlightRefreshes.set(providerID, promise);
1678
+ return promise;
1679
+ }
1680
+ async function executeRefreshForMode(credentials, mode, signal) {
1681
+ const region = getQoderRegionConfig(mode);
1682
+ const providerID = region.providerID;
1626
1683
  if (isPatRefresh(credentials.refresh)) {
1627
1684
  const { pat } = decodePatRefresh(credentials.refresh);
1628
1685
  if (pat) {
1629
1686
  try {
1630
1687
  const refreshed = await credentialsFromPat(pat, mode);
1631
1688
  const qCreds = refreshed;
1689
+ saveCredentialsToAuthFile(providerID, refreshed);
1632
1690
  updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
1633
1691
  });
1634
1692
  return refreshed;
1635
1693
  } catch (error) {
1636
1694
  const detail = error instanceof Error ? error.message : String(error);
1637
1695
  throw new Error(
1638
- `Qoder CN credential refresh failed (PAT re-exchange): ${detail}. If this persists, the PAT may be revoked \u2014 run /login qoder-cn.`
1696
+ `${region.loginName} credential refresh failed (PAT re-exchange): ${detail}. If this persists, the PAT may be revoked \u2014 run /login ${providerID}.`
1639
1697
  );
1640
1698
  }
1641
1699
  }
1642
1700
  throw new Error(
1643
- "Qoder CN credential refresh failed: the stored refresh chain carries no PAT. Run /login qoder-cn."
1701
+ `${region.loginName} credential refresh failed: the stored refresh chain carries no PAT. Run /login ${providerID}.`
1644
1702
  );
1645
1703
  }
1646
1704
  const parts = credentials.refresh.split("|");
1647
1705
  const refreshToken = parts[0] || "";
1706
+ if (!refreshToken) {
1707
+ throw new Error(
1708
+ `${region.loginName} credential has no stored refresh chain (written by an older version). Run /login ${providerID} once.`
1709
+ );
1710
+ }
1648
1711
  const userID = parts[1] || "";
1649
1712
  const machineID = parts[2] || getMachineId();
1650
1713
  const prev = credentials;
1651
1714
  const prevName = prev.name || "";
1652
1715
  const prevEmail = prev.email || "";
1653
- const refreshURL = getQoderRefreshURL(mode);
1654
- let lastError = "unknown error";
1655
- try {
1656
- const response = await fetch(refreshURL, {
1657
- method: "POST",
1658
- headers: {
1659
- "Content-Type": "application/json",
1660
- Authorization: `Bearer ${credentials.access}`,
1661
- Accept: "application/json",
1662
- "User-Agent": "pi-provider-qoder-cn"
1663
- },
1664
- body: JSON.stringify({ refresh_token: refreshToken })
1665
- });
1716
+ const isDevice = refreshToken.startsWith("drt-");
1717
+ const refreshURL = isDevice ? getQoderDeviceRefreshURL(mode) : getQoderRefreshURL(mode);
1718
+ const body = JSON.stringify({ refresh_token: refreshToken });
1719
+ const failures = [];
1720
+ for (const withAuth of [true, false]) {
1721
+ let response;
1722
+ try {
1723
+ response = await fetch(refreshURL, {
1724
+ method: "POST",
1725
+ headers: {
1726
+ "Content-Type": "application/json",
1727
+ ...withAuth ? { Authorization: `Bearer ${credentials.access}` } : {},
1728
+ Accept: "application/json",
1729
+ "User-Agent": "pi-provider-qoder-cn"
1730
+ },
1731
+ body,
1732
+ signal
1733
+ });
1734
+ } catch (error) {
1735
+ if (signal?.aborted) throw error;
1736
+ failures.push(error instanceof Error ? error.message : String(error));
1737
+ break;
1738
+ }
1666
1739
  if (response.ok) {
1667
1740
  const data = await response.json();
1668
- const newAccess = data.token;
1669
- const newRefresh = data.refresh_token || refreshToken;
1670
- let expireMs = Date.now() + 30 * 24 * 60 * 60 * 1e3;
1671
- if (data.expires_at) {
1672
- const parsed = Date.parse(data.expires_at);
1673
- if (!Number.isNaN(parsed)) expireMs = parsed;
1674
- } else if (data.expires_in) {
1675
- expireMs = Date.now() + (data.expires_in > 86400 ? data.expires_in : data.expires_in * 1e3);
1741
+ const newAccess = typeof data.token === "string" && data.token ? data.token : data.access_token;
1742
+ if (typeof newAccess !== "string" || !newAccess) {
1743
+ throw new Error("token refresh response carried no access token");
1676
1744
  }
1745
+ const newRefresh = data.refresh_token || refreshToken;
1746
+ const expireMs = resolveTokenExpiryMs(data.expires_at, data.expires_in);
1677
1747
  const refreshed = {
1678
1748
  ...credentials,
1679
1749
  refresh: `${newRefresh}|${userID}|${machineID}`,
@@ -1684,27 +1754,29 @@ async function refreshQoderTokenForMode(credentials, mode) {
1684
1754
  name: prevName,
1685
1755
  machineID
1686
1756
  };
1757
+ saveCredentialsToAuthFile(providerID, refreshed);
1687
1758
  updateQoderModelsCache(newAccess, userID, prevName, prevEmail, mode).catch(() => {
1688
1759
  });
1689
1760
  return refreshed;
1690
1761
  }
1691
- lastError = `HTTP ${response.status} ${response.statusText}`;
1692
- } catch (error) {
1693
- lastError = error instanceof Error ? error.message : String(error);
1762
+ const errText = await response.text();
1763
+ failures.push(`${response.status} ${response.statusText}: ${errText.slice(0, 120)}`);
1764
+ if (response.status !== 401 && response.status !== 403 || !withAuth) break;
1694
1765
  }
1695
- throw new Error(`Qoder CN token refresh failed (${lastError}). Run /login qoder-cn.`);
1766
+ throw new Error(`${region.loginName} token refresh failed (${failures.join(" | ")}). Run /login ${providerID}.`);
1696
1767
  }
1697
- var AuthStorage2, identityCache, authFileMem;
1768
+ var identityCache, authFileMem, inFlightRefreshes;
1698
1769
  var init_oauth = __esm({
1699
1770
  "src/auth/oauth.ts"() {
1700
1771
  "use strict";
1701
1772
  init_catalog();
1702
1773
  init_cosy();
1703
1774
  init_region();
1775
+ init_expiry();
1704
1776
  init_login();
1705
1777
  init_pat();
1706
- AuthStorage2 = PiCodingAgent.AuthStorage;
1707
1778
  identityCache = /* @__PURE__ */ new Map();
1779
+ inFlightRefreshes = /* @__PURE__ */ new Map();
1708
1780
  }
1709
1781
  });
1710
1782
 
@@ -1765,7 +1837,400 @@ async function fetchQoderUsageForMode(credentials, mode) {
1765
1837
  // src/index.ts
1766
1838
  init_catalog();
1767
1839
 
1840
+ // src/commands/claim.ts
1841
+ init_expiry();
1842
+ init_cosy();
1843
+ init_region();
1844
+ import { spawnSync } from "node:child_process";
1845
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
1846
+ import { homedir as homedir5, hostname } from "node:os";
1847
+ import { basename, dirname as dirname4, join as join5 } from "node:path";
1848
+ import { fileURLToPath } from "node:url";
1849
+ function piAgentDir2() {
1850
+ const home = process.env.HOME || process.env.USERPROFILE || homedir5();
1851
+ return join5(home, ".pi", "agent");
1852
+ }
1853
+ function getCheckinCachePath(mode = "cn") {
1854
+ const file = mode === "cn" ? "qoder-cn-checkin.json" : "qoder-checkin.json";
1855
+ return join5(piAgentDir2(), file);
1856
+ }
1857
+ function readCheckinCache(mode = "cn") {
1858
+ try {
1859
+ const file = getCheckinCachePath(mode);
1860
+ if (!existsSync5(file)) return null;
1861
+ return JSON.parse(readFileSync5(file, "utf8"));
1862
+ } catch {
1863
+ return null;
1864
+ }
1865
+ }
1866
+ function writeCheckinCache(info, mode = "cn") {
1867
+ try {
1868
+ const file = getCheckinCachePath(mode);
1869
+ const dir = dirname4(file);
1870
+ if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true });
1871
+ writeFileSync5(file, JSON.stringify(info, null, 2), "utf8");
1872
+ return true;
1873
+ } catch {
1874
+ return false;
1875
+ }
1876
+ }
1877
+ var ANSI = {
1878
+ reset: "\x1B[0m",
1879
+ bold: "\x1B[1m",
1880
+ green: "\x1B[32m",
1881
+ yellow: "\x1B[33m",
1882
+ red: "\x1B[31m",
1883
+ cyan: "\x1B[36m"
1884
+ };
1885
+ function paint(text, code) {
1886
+ return code && text ? `${code}${text}${ANSI.reset}` : text;
1887
+ }
1888
+ function msUntilBeijing10AM(now = Date.now()) {
1889
+ const beijingOffset = 8 * 36e5;
1890
+ const beijingTime = now + beijingOffset;
1891
+ const beijingDate = new Date(beijingTime);
1892
+ let targetUtc = Date.UTC(beijingDate.getUTCFullYear(), beijingDate.getUTCMonth(), beijingDate.getUTCDate(), 10, 0, 0);
1893
+ if (beijingTime >= targetUtc) {
1894
+ targetUtc += 24 * 36e5;
1895
+ }
1896
+ return targetUtc - beijingTime;
1897
+ }
1898
+ function formatCountdownBeijing(ms) {
1899
+ const totalMinutes = Math.max(1, Math.round(ms / 6e4));
1900
+ const hours = Math.floor(totalMinutes / 60);
1901
+ const minutes = totalMinutes % 60;
1902
+ return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
1903
+ }
1904
+ function formatDateTime(isoOrMs) {
1905
+ if (!isoOrMs) return "n/a";
1906
+ const d = new Date(isoOrMs);
1907
+ if (Number.isNaN(d.getTime())) return String(isoOrMs);
1908
+ return new Intl.DateTimeFormat("zh-CN", {
1909
+ timeZone: "Asia/Shanghai",
1910
+ year: "numeric",
1911
+ month: "2-digit",
1912
+ day: "2-digit",
1913
+ hour: "2-digit",
1914
+ minute: "2-digit",
1915
+ second: "2-digit",
1916
+ hour12: false
1917
+ }).format(d);
1918
+ }
1919
+ var printableAscii = (v) => typeof v === "string" && v.trim().length > 0 && /^[\x20-\x7e]+$/.test(v.trim());
1920
+ var identityMemCache = /* @__PURE__ */ new Map();
1921
+ var IDENTITY_TTL_MS = 30 * 6e4;
1922
+ function candidateUmidExes() {
1923
+ const custom = process.env.QODER_UMID_EXE;
1924
+ if (custom) return [custom];
1925
+ const candidates = [];
1926
+ if (process.platform === "win32") {
1927
+ const moduleDir = dirname4(fileURLToPath(import.meta.url));
1928
+ const pkgRoot = basename(moduleDir) === "commands" ? dirname4(dirname4(moduleDir)) : dirname4(moduleDir);
1929
+ candidates.push(join5(pkgRoot, "bin", "runtime-info.exe"));
1930
+ }
1931
+ const localAppData = process.env.LOCALAPPDATA || join5(process.env.HOME || process.env.USERPROFILE || homedir5(), "AppData", "Local");
1932
+ for (const r of [join5(localAppData, "Programs", "Qoder"), "C:\\Program Files\\Qoder", "D:\\Qoder"]) {
1933
+ candidates.push(join5(r, "resources", "umid", "runtime-info.exe"));
1934
+ }
1935
+ return candidates;
1936
+ }
1937
+ function runUmidExe(exe, environment, accountId) {
1938
+ const res = spawnSync(exe, [String(environment), "--account-stdin"], {
1939
+ input: JSON.stringify({ account: accountId }),
1940
+ timeout: 1e4,
1941
+ windowsHide: true,
1942
+ encoding: "utf8",
1943
+ maxBuffer: 1024 * 1024
1944
+ });
1945
+ if (res.error) throw res.error;
1946
+ if (res.status !== 0) throw new Error(`runtime-info exited ${res.status}: ${String(res.stderr || "").slice(0, 120)}`);
1947
+ return String(res.stdout || "");
1948
+ }
1949
+ async function resolveSashMachineIdentity(mode, accountId) {
1950
+ if (!accountId) return null;
1951
+ const key = `${mode}:${accountId}`;
1952
+ const hit = identityMemCache.get(key);
1953
+ if (hit && hit.expiresAt > Date.now()) return hit.identity;
1954
+ const environment = mode === "global" ? 3 : 0;
1955
+ for (const exe of candidateUmidExes()) {
1956
+ if (!existsSync5(exe)) continue;
1957
+ try {
1958
+ const stdout = await runUmidExe(exe, environment, accountId);
1959
+ const parsed = JSON.parse(stdout);
1960
+ if (!printableAscii(parsed.machineToken) || !printableAscii(parsed.machineCode) || !printableAscii(parsed.machineType)) {
1961
+ continue;
1962
+ }
1963
+ const identity = {
1964
+ machineToken: parsed.machineToken.trim(),
1965
+ machineCode: parsed.machineCode.trim(),
1966
+ machineType: parsed.machineType.trim()
1967
+ };
1968
+ identityMemCache.set(key, { identity, expiresAt: Date.now() + IDENTITY_TTL_MS });
1969
+ return identity;
1970
+ } catch {
1971
+ }
1972
+ }
1973
+ return null;
1974
+ }
1975
+ function buildSashHeaders(accessToken, machineID, identity) {
1976
+ const headers = {
1977
+ Authorization: `Bearer ${accessToken}`,
1978
+ Accept: "application/json",
1979
+ "Content-Type": "application/json",
1980
+ "Cosy-ClientType": "10",
1981
+ "Cosy-Version": "1.13.3",
1982
+ "Cosy-MachineId": machineID,
1983
+ "Cosy-MachineOS": "x86_64_windows",
1984
+ "Cosy-MachineHostname": hostname(),
1985
+ "User-Agent": "Qoder"
1986
+ };
1987
+ if (identity) {
1988
+ headers["Cosy-MachineToken"] = identity.machineToken;
1989
+ headers["Cosy-MachineCode"] = identity.machineCode;
1990
+ headers["Cosy-MachineType"] = identity.machineType;
1991
+ }
1992
+ return headers;
1993
+ }
1994
+ async function fetchQoderCampaigns(accessToken, machineID, mode, identity) {
1995
+ const url = `${getQoderOpenApiUrl(mode)}/sash/api/v1/me/campaigns?forceRefresh=true`;
1996
+ const response = await fetch(url, {
1997
+ method: "GET",
1998
+ headers: buildSashHeaders(accessToken, machineID, identity),
1999
+ signal: AbortSignal.timeout(1e4)
2000
+ });
2001
+ const text = await response.text();
2002
+ let data;
2003
+ try {
2004
+ data = JSON.parse(text);
2005
+ } catch {
2006
+ data = void 0;
2007
+ }
2008
+ if (!response.ok) {
2009
+ const detail = data?.message || text.slice(0, 200);
2010
+ throw new Error(`Failed to query Qoder campaigns (${response.status}): ${detail}`);
2011
+ }
2012
+ if (!data) throw new Error("Qoder campaigns response was not JSON.");
2013
+ return data;
2014
+ }
2015
+ async function claimQoderCampaign(accessToken, machineID, campaignId, mode, identity) {
2016
+ const url = `${getQoderOpenApiUrl(mode)}/sash/api/v1/me/campaigns/${encodeURIComponent(campaignId)}/claim`;
2017
+ const response = await fetch(url, {
2018
+ method: "POST",
2019
+ headers: buildSashHeaders(accessToken, machineID, identity),
2020
+ body: JSON.stringify({}),
2021
+ signal: AbortSignal.timeout(1e4)
2022
+ });
2023
+ const text = await response.text();
2024
+ let data;
2025
+ try {
2026
+ data = JSON.parse(text);
2027
+ } catch {
2028
+ data = void 0;
2029
+ }
2030
+ if (!response.ok) {
2031
+ const detail = data?.message || text.slice(0, 200);
2032
+ throw new Error(`Failed to claim Qoder benefit (${response.status}): ${detail}`);
2033
+ }
2034
+ if (!data) throw new Error("Qoder claim response was not JSON.");
2035
+ return data;
2036
+ }
2037
+ async function fetchCheckinGrantInfo(accessToken, machineID, mode, identity) {
2038
+ const cached = readCheckinCache(mode);
2039
+ if (cached?.claimed && cached.expiresAt && cached.cacheUntil && Date.now() < cached.cacheUntil) {
2040
+ return cached;
2041
+ }
2042
+ try {
2043
+ const campaignsData = await fetchQoderCampaigns(accessToken, machineID, mode, identity);
2044
+ const campaigns = campaignsData.campaigns || [];
2045
+ const dailyCreditCampaign = campaigns.find(
2046
+ (c) => c.actionType === "CLAIM_BENEFIT" && (c.benefit?.kind === "CREDITS" || c.benefit?.amount === 100)
2047
+ ) || campaigns.find((c) => c.actionType === "CLAIM_BENEFIT");
2048
+ if (!dailyCreditCampaign) return null;
2049
+ const title = dailyCreditCampaign.placements?.[0]?.content?.zh?.title || "\u6BCF\u5929\u9886 100 Credits";
2050
+ const amount = dailyCreditCampaign.benefit?.amount || 100;
2051
+ const validityDays = dailyCreditCampaign.benefit?.validity?.days || 30;
2052
+ const endAtMs = toEpochMs(dailyCreditCampaign.endAt);
2053
+ const cacheUntil = endAtMs && endAtMs > Date.now() ? endAtMs : Date.now() + msUntilBeijing10AM();
2054
+ let info;
2055
+ if (dailyCreditCampaign.claimStatus === "CLAIMED") {
2056
+ try {
2057
+ const claimRes = await claimQoderCampaign(
2058
+ accessToken,
2059
+ machineID,
2060
+ dailyCreditCampaign.campaignId,
2061
+ mode,
2062
+ identity
2063
+ );
2064
+ info = {
2065
+ claimed: true,
2066
+ amount: claimRes.benefit?.amount || amount,
2067
+ claimedAt: claimRes.claimedAt || cached?.claimedAt,
2068
+ expiresAt: claimRes.expiresAt || cached?.expiresAt,
2069
+ validityDays: claimRes.benefit?.validity?.days || validityDays,
2070
+ campaignId: dailyCreditCampaign.campaignId,
2071
+ campaignTitle: title,
2072
+ cacheUntil
2073
+ };
2074
+ } catch {
2075
+ info = {
2076
+ claimed: true,
2077
+ amount,
2078
+ claimedAt: cached?.claimedAt,
2079
+ expiresAt: cached?.expiresAt,
2080
+ validityDays,
2081
+ campaignId: dailyCreditCampaign.campaignId,
2082
+ campaignTitle: title,
2083
+ cacheUntil
2084
+ };
2085
+ }
2086
+ } else {
2087
+ info = {
2088
+ claimed: false,
2089
+ amount,
2090
+ validityDays,
2091
+ campaignId: dailyCreditCampaign.campaignId,
2092
+ campaignTitle: title,
2093
+ cacheUntil
2094
+ };
2095
+ }
2096
+ writeCheckinCache(info, mode);
2097
+ return info;
2098
+ } catch {
2099
+ return readCheckinCache(mode);
2100
+ }
2101
+ }
2102
+ async function resolveCredentials(providerID, ctx) {
2103
+ let accessToken;
2104
+ try {
2105
+ accessToken = await ctx?.modelRegistry?.getApiKeyForProvider(providerID) || void 0;
2106
+ } catch {
2107
+ }
2108
+ const { getCachedCredentials: getCachedCredentials2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
2109
+ const stored = getCachedCredentials2("", providerID);
2110
+ accessToken = accessToken || stored?.access || void 0;
2111
+ if (!accessToken) return null;
2112
+ const machineID = stored?.machineID || getMachineId();
2113
+ const userID = stored?.userID || "";
2114
+ return { accessToken, machineID, userID };
2115
+ }
2116
+ async function runClaimCommand(mode, args, ctx) {
2117
+ const region = getQoderRegionConfig(mode);
2118
+ const providerID = region.providerID;
2119
+ const wantsRaw = ["json", "raw", "debug"].includes((args || "").trim().toLowerCase());
2120
+ try {
2121
+ const creds = await resolveCredentials(providerID, ctx);
2122
+ if (!creds) {
2123
+ const msg = mode === "cn" ? `\u672A\u627E\u5230 ${providerID} \u767B\u5F55\u51ED\u636E\uFF0C\u8BF7\u5148\u8FD0\u884C /login ${providerID}` : `No ${providerID} credentials found. Run /login ${providerID} first.`;
2124
+ ctx?.ui?.notify(msg, "warning");
2125
+ if (!ctx?.ui) console.warn(msg);
2126
+ return;
2127
+ }
2128
+ const { accessToken, machineID, userID } = creds;
2129
+ const identity = await resolveSashMachineIdentity(mode, userID).catch(() => null);
2130
+ const campaignsData = await fetchQoderCampaigns(accessToken, machineID, mode, identity);
2131
+ const campaigns = campaignsData.campaigns || [];
2132
+ const dailyCreditCampaign = campaigns.find(
2133
+ (c) => c.actionType === "CLAIM_BENEFIT" && (c.benefit?.kind === "CREDITS" || c.benefit?.amount === 100)
2134
+ ) || campaigns.find((c) => c.actionType === "CLAIM_BENEFIT");
2135
+ if (!dailyCreditCampaign) {
2136
+ if (wantsRaw) {
2137
+ const rawJson = JSON.stringify(campaignsData, null, 2);
2138
+ ctx?.ui?.notify(rawJson, "info");
2139
+ if (!ctx?.ui) console.log(rawJson);
2140
+ return;
2141
+ }
2142
+ const msg = identity === null ? mode === "cn" ? "\u26A0\uFE0F \u672A\u627E\u5230 Qoder \u684C\u9762\u7AEF\u8BBE\u5907\u6307\u7EB9\u7EC4\u4EF6\uFF08runtime-info.exe\uFF09\uFF0C\u670D\u52A1\u7AEF\u56E0\u6B64\u4E0D\u4E0B\u53D1\u53EF\u9886\u53D6\u7684\u7B7E\u5230\u6D3B\u52A8\u3002\u8BF7\u5B89\u88C5\u5E76\u6253\u5F00 Qoder \u684C\u9762\u7AEF\uFF0C\u6216\u8BBE\u7F6E QODER_UMID_EXE \u6307\u5411\u5176 resources/umid/runtime-info.exe \u540E\u91CD\u8BD5\u3002" : "\u26A0\uFE0F Daily check-in campaigns are hidden without desktop device attestation, and the Qoder desktop UMID component (runtime-info.exe) was not found on this machine.\n- Install/launch the Qoder desktop app, or set QODER_UMID_EXE to its resources/umid/runtime-info.exe, then retry." : mode === "cn" ? "\u2139\uFE0F \u5F53\u524D\u6682\u65E0\u53EF\u9886\u53D6\u7684\u7B7E\u5230\u6D3B\u52A8\uFF08\u6BCF\u65E5 10:00 UTC+8 \u5F00\u653E\u5237\u65B0\uFF09" : "\u2139\uFE0F Device verified, but no claimable check-in campaign is offered for this account/region right now (the campaign list carries banners only; claims reset at 10:00 UTC+8 when offered).";
2143
+ ctx?.ui?.notify(msg, identity === null ? "warning" : "info");
2144
+ if (!ctx?.ui) console.log(msg);
2145
+ return;
2146
+ }
2147
+ const content = mode === "cn" ? dailyCreditCampaign.placements?.[0]?.content?.zh || dailyCreditCampaign.placements?.[0]?.content?.en : dailyCreditCampaign.placements?.[0]?.content?.en || dailyCreditCampaign.placements?.[0]?.content?.zh;
2148
+ const campaignTitle = content?.title || (mode === "cn" ? "\u6BCF\u5929\u9886 100 Credits" : "Claim 100 Credits Daily");
2149
+ const amount = dailyCreditCampaign.benefit?.amount || 100;
2150
+ const validityDays = dailyCreditCampaign.benefit?.validity?.days || 30;
2151
+ const endAtMs = toEpochMs(dailyCreditCampaign.endAt);
2152
+ const remainingToReset = endAtMs && endAtMs > Date.now() ? endAtMs - Date.now() : msUntilBeijing10AM();
2153
+ const countdown = formatCountdownBeijing(remainingToReset);
2154
+ if (dailyCreditCampaign.claimStatus === "CLAIMED") {
2155
+ const cached = readCheckinCache(mode);
2156
+ writeCheckinCache(
2157
+ {
2158
+ claimed: true,
2159
+ amount,
2160
+ validityDays,
2161
+ campaignId: dailyCreditCampaign.campaignId,
2162
+ campaignTitle,
2163
+ claimedAt: cached?.claimedAt,
2164
+ expiresAt: cached?.expiresAt,
2165
+ cacheUntil: Date.now() + remainingToReset
2166
+ },
2167
+ mode
2168
+ );
2169
+ if (wantsRaw) {
2170
+ const rawJson = JSON.stringify(dailyCreditCampaign, null, 2);
2171
+ ctx?.ui?.notify(rawJson, "info");
2172
+ if (!ctx?.ui) console.log(rawJson);
2173
+ return;
2174
+ }
2175
+ const lines2 = mode === "cn" ? [
2176
+ paint(`\u2139\uFE0F \u4ECA\u65E5 ${amount} Credits \u5DF2\u7ECF\u9886\u53D6\u8FC7\uFF0C\u65E0\u9700\u91CD\u590D\u64CD\u4F5C`, ANSI.cyan),
2177
+ `- \u6D3B\u52A8\u540D\u79F0\uFF1A${campaignTitle}`,
2178
+ `- \u989D\u5EA6\u8BF4\u660E\uFF1A${amount} Credits\uFF08\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF0C${validityDays} \u5929\u6709\u6548\uFF09`,
2179
+ `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2180
+ ] : [
2181
+ paint(`\u2139\uFE0F Today's ${amount} Credits already claimed.`, ANSI.cyan),
2182
+ `- Campaign: ${campaignTitle}`,
2183
+ `- Benefit: ${amount} Credits (${validityDays}-day validity)`,
2184
+ `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2185
+ ];
2186
+ const output2 = lines2.join("\n");
2187
+ ctx?.ui?.notify(output2, "info");
2188
+ if (!ctx?.ui) console.log(output2);
2189
+ return;
2190
+ }
2191
+ const claimRes = await claimQoderCampaign(accessToken, machineID, dailyCreditCampaign.campaignId, mode, identity);
2192
+ const grantAmount = claimRes.benefit?.amount || amount;
2193
+ const grantDays = claimRes.benefit?.validity?.days || validityDays;
2194
+ const expiresText = claimRes.expiresAt ? formatDateTime(claimRes.expiresAt) : `${grantDays} \u5929\u540E`;
2195
+ writeCheckinCache(
2196
+ {
2197
+ claimed: true,
2198
+ amount: grantAmount,
2199
+ claimedAt: claimRes.claimedAt,
2200
+ expiresAt: claimRes.expiresAt,
2201
+ validityDays: grantDays,
2202
+ campaignId: dailyCreditCampaign.campaignId,
2203
+ campaignTitle,
2204
+ cacheUntil: Date.now() + remainingToReset
2205
+ },
2206
+ mode
2207
+ );
2208
+ const lines = mode === "cn" ? [
2209
+ paint(`\u{1F389} \u6210\u529F\u9886\u53D6\u4ECA\u65E5 ${grantAmount} Credits\uFF01`, `${ANSI.green}${ANSI.bold}`),
2210
+ `- \u989D\u5EA6\u7C7B\u578B\uFF1A\u5168\u6A21\u578B\u901A\u7528\u8D44\u6E90\u5305\uFF08Add-on Credits\uFF09`,
2211
+ `- \u6709\u6548\u671F\u9650\uFF1A${grantDays} \u5929\uFF08\u6709\u6548\u671F\u81F3 ${expiresText}\uFF09`,
2212
+ `- \u9886\u53D6\u6D41\u6C34\uFF1A${claimRes.grantId || "ok"}`,
2213
+ `- \u4E0B\u6B21\u5237\u65B0\uFF1A\u660E\u65E5 10:00 UTC+8\uFF08\u8DDD\u5237\u65B0\u7EA6 ${countdown}\uFF09`
2214
+ ] : [
2215
+ paint(`\u{1F389} Successfully claimed ${grantAmount} Credits!`, `${ANSI.green}${ANSI.bold}`),
2216
+ `- Type: Universal Add-on Credits`,
2217
+ `- Validity: ${grantDays} days (expires ${expiresText})`,
2218
+ `- Grant ID: ${claimRes.grantId || "ok"}`,
2219
+ `- Next reset: Daily at 10:00 UTC+8 (in ~${countdown})`
2220
+ ];
2221
+ const output = lines.join("\n");
2222
+ ctx?.ui?.notify(output, "info");
2223
+ if (!ctx?.ui) console.log(output);
2224
+ } catch (error) {
2225
+ const message = error instanceof Error ? error.message : String(error);
2226
+ const errText = mode === "cn" ? `\u274C \u6BCF\u65E5\u7B7E\u5230\u9886\u53D6\u5931\u8D25: ${message}` : `\u274C Daily check-in claim failed: ${message}`;
2227
+ ctx?.ui?.notify(errText, "error");
2228
+ if (!ctx?.ui) console.error(errText);
2229
+ }
2230
+ }
2231
+
1768
2232
  // src/protocol/errors.ts
2233
+ init_vpc();
1769
2234
  function parseQoderErrorPayload(payload) {
1770
2235
  if (!payload) return null;
1771
2236
  let parsed = payload;
@@ -1820,12 +2285,21 @@ function getBeijingDailyResetCountdown(now = Date.now()) {
1820
2285
  const minutes = totalMinutes % 60;
1821
2286
  return hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206\u949F` : `${minutes}\u5206\u949F`;
1822
2287
  }
1823
- function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
2288
+ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "cn") {
1824
2289
  const parsed = parseQoderErrorPayload(rawBody);
1825
2290
  const code = parsed?.code;
1826
2291
  const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
1827
2292
  const countdown = getBeijingDailyResetCountdown(now);
2293
+ const isCn = mode !== "global";
1828
2294
  if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
2295
+ if (!isCn) {
2296
+ return [
2297
+ `[Qoder quota limit] Daily call allowance exhausted (Billing daily count exceeded, code 110)`,
2298
+ `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2299
+ `- Reason: the account is on the free personal standard plan or tripped the daily frequency fuse. Add-on credits stay unspendable while the daily cap is active.`,
2300
+ `- Fix: wait for the reset; switch provider with /model; or upgrade at https://qoder.com/pricing to lift the daily limit.`
2301
+ ].join("\n");
2302
+ }
1829
2303
  return [
1830
2304
  `[Qoder CN \u989D\u5EA6\u9650\u5236] \u4ECA\u65E5\u8C03\u7528\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 (Billing daily count exceeded, \u9519\u8BEF\u7801 110)`,
1831
2305
  `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
@@ -1834,44 +2308,71 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
1834
2308
  ].join("\n");
1835
2309
  }
1836
2310
  if (code === "117" || /team member credits exhausted/i.test(message)) {
1837
- return [
2311
+ return isCn ? [
1838
2312
  `[Qoder CN \u4F01\u4E1A\u989D\u5EA6\u9650\u5236] \u4F01\u4E1A\u5206\u914D\u7ED9\u60A8\u7684\u4E2A\u4EBA Credits \u989D\u5EA6\u5DF2\u7528\u5C3D (\u9519\u8BEF\u7801 117)`,
1839
2313
  `- \u9650\u5236\u539F\u56E0\uFF1A\u7BA1\u7406\u5458\u5728\u4F01\u4E1A\u63A7\u5236\u53F0\u5206\u914D\u7ED9\u60A8\u4E2A\u4EBA\u7684\u53EF\u7528 Credits \u989D\u5EA6\u5DF2\u8017\u5C3D\u3002`,
1840
2314
  `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u8054\u7CFB\u4F01\u4E1A\u7BA1\u7406\u5458\u5728 Qoder \u56E2\u961F\u7BA1\u7406\u540E\u53F0\u4E3A\u60A8\u589E\u52A0\u6210\u5458 Credits \u914D\u989D\u3002`
2315
+ ].join("\n") : [
2316
+ `[Qoder enterprise quota] Your member credit allowance is exhausted (code 117)`,
2317
+ `- Reason: the admin-assigned per-member credit cap has been consumed.`,
2318
+ `- Fix: ask your organization admin to raise your member quota in the team console.`
1841
2319
  ].join("\n");
1842
2320
  }
1843
2321
  if (code === "116" || /team administrator credits exhausted/i.test(message)) {
1844
- return [
2322
+ return isCn ? [
1845
2323
  `[Qoder CN \u4F01\u4E1A\u989D\u5EA6\u9650\u5236] \u4F01\u4E1A/\u56E2\u961F\u7BA1\u7406\u5458\u7684 Credits \u603B\u4F59\u989D\u5DF2\u8017\u5C3D (\u9519\u8BEF\u7801 116)`,
1846
2324
  `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u4F01\u4E1A\u7EC4\u7EC7\u8D26\u6237\u7684 Credits \u989D\u5EA6\u5DF2\u5168\u90E8\u7528\u5B8C\u3002`,
1847
2325
  `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u8054\u7CFB\u4F01\u4E1A\u7BA1\u7406\u5458\u5728\u63A7\u5236\u53F0\u4E3A\u7EC4\u7EC7\u8D26\u6237\u5145\u503C\u6216\u7EED\u671F\u3002`
2326
+ ].join("\n") : [
2327
+ `[Qoder enterprise quota] The organization's credit pool is exhausted (code 116)`,
2328
+ `- Reason: every credit in the organization account has been consumed.`,
2329
+ `- Fix: ask your organization admin to purchase or renew the org pool.`
1848
2330
  ].join("\n");
1849
2331
  }
1850
2332
  if (code === "122" || /billing-group credits limit reached/i.test(message)) {
1851
- return [
2333
+ return isCn ? [
1852
2334
  `[Qoder CN \u4F01\u4E1A\u8BA1\u8D39\u7EC4\u9650\u5236] \u60A8\u6240\u5728\u7684\u4F01\u4E1A\u8BA1\u8D39\u7EC4\u5DF2\u8FBE\u5230\u672C\u671F\u652F\u51FA\u4E0A\u9650 (\u9519\u8BEF\u7801 122)`,
1853
2335
  `- \u9650\u5236\u539F\u56E0\uFF1A\u8BA1\u8D39\u7EC4\u5468\u671F\u5185\u5DF2\u6D88\u8017\u5B8C\u7BA1\u7406\u5458\u8BBE\u5B9A\u7684\u4E0A\u9650\u989D\u5EA6\u3002`,
1854
2336
  `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u8054\u7CFB\u8BA1\u8D39\u7BA1\u7406\u5458\u6216\u4F01\u4E1A\u7BA1\u7406\u5458\u8C03\u6574\u8BE5\u8BA1\u8D39\u7EC4\u7684\u5468\u671F\u652F\u51FA\u4E0A\u9650\u3002`
2337
+ ].join("\n") : [
2338
+ `[Qoder billing group] The billing group reached its period spend cap (code 122)`,
2339
+ `- Reason: the admin-set limit for this billing group is consumed for the period.`,
2340
+ `- Fix: ask the billing administrator to raise the group's period cap.`
1855
2341
  ].join("\n");
1856
2342
  }
1857
2343
  if (code === "119" || /free usage limit for the selected model reached/i.test(message)) {
1858
- return [
2344
+ return isCn ? [
1859
2345
  `[Qoder CN \u6A21\u578B\u9650\u514D\u989D\u5EA6\u5DF2\u6EE1] \u5F53\u524D\u6A21\u578B\u7684\u514D\u8D39\u4F53\u9A8C\u989D\u5EA6\u5DF2\u7528\u5B8C (\u9519\u8BEF\u7801 119)`,
1860
2346
  `- \u5237\u65B0\u65F6\u95F4\uFF1A\u5C06\u5728\u5317\u4EAC\u65F6\u95F4 00:00 \u91CD\u7F6E\uFF08\u7EA6 ${countdown}\u540E\uFF09`,
1861
2347
  `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u53EF\u4F7F\u7528 /model \u5207\u6362\u81F3\u5176\u4ED6\u4ED8\u8D39\u6A21\u578B\uFF08\u5982 deepseek-v4-pro / glm-5.3\uFF09\u6D88\u8017 Credits \u989D\u5EA6\uFF0C\u6216\u6B21\u65E5\u91CD\u7F6E\u540E\u7EE7\u7EED\u4F7F\u7528\u3002`
2348
+ ].join("\n") : [
2349
+ `[Qoder model free tier] This model's free daily trial allowance is used up (code 119)`,
2350
+ `- Reset: at 00:00 UTC+8 (in ${countdown})`,
2351
+ `- Fix: switch to a paid model via /model to spend credits, or wait for the reset.`
1862
2352
  ].join("\n");
1863
2353
  }
1864
2354
  if (code === "113" || code === "118" || /quota exhausted/i.test(message) || /credits exhausted/i.test(message)) {
1865
- return [
2355
+ const store = isCn ? QODER_CN_OFFICIAL.manageUrl : "https://qoder.com";
2356
+ return isCn ? [
1866
2357
  `[Qoder CN \u989D\u5EA6\u8017\u5C3D] \u8D26\u6237 Credits \u4F59\u989D\u5DF2\u5168\u90E8\u7528\u5C3D (\u9519\u8BEF\u7801 ${code || 113})`,
1867
2358
  `- \u9650\u5236\u539F\u56E0\uFF1A\u5F53\u524D\u8BA2\u9605\u5957\u9910\u53CA\u8D44\u6E90\u5305\u5185\u7684\u53EF\u7528\u989D\u5EA6\u5DF2\u5168\u90E8\u6263\u51CF\u5B8C\u6BD5\u3002`,
1868
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u524D\u5F80 qoder.com.cn \u8D2D\u4E70\u8D44\u6E90\u5305/\u52A0\u6CB9\u5305\uFF0C\u6216\u7B49\u5F85\u4E0B\u4E00\u8BA1\u8D39\u5468\u671F\u5237\u65B0\u3002`
2359
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u524D\u5F80 ${store} \u8D2D\u4E70\u8D44\u6E90\u5305/\u52A0\u6CB9\u5305\uFF0C\u6216\u7B49\u5F85\u4E0B\u4E00\u8BA1\u8D39\u5468\u671F\u5237\u65B0\u3002`
2360
+ ].join("\n") : [
2361
+ `[Qoder credits exhausted] The account has no remaining credits (code ${code || 113})`,
2362
+ `- Reason: the plan allowance and every add-on pack are consumed.`,
2363
+ `- Fix: purchase a credit pack at ${store}, or wait for the next billing cycle.`
1869
2364
  ].join("\n");
1870
2365
  }
1871
2366
  if (code === "105" || /token expired/i.test(message) || /login expired/i.test(message)) {
2367
+ if (isCn) {
2368
+ return [
2369
+ `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
2370
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u91CD\u65B0\u8FD0\u884C /login qoder-cn\uFF0C\u6216\u66F4\u65B0\u73AF\u5883\u53D8\u91CF\u4E2D\u7684\u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C\uFF08PAT\uFF09\u3002`
2371
+ ].join("\n");
2372
+ }
1872
2373
  return [
1873
- `[Qoder CN \u51ED\u8BC1\u5931\u6548] \u767B\u5F55\u6001\u5DF2\u8FC7\u671F\u6216 Token \u5931\u6548 (\u9519\u8BEF\u7801 105)`,
1874
- `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u8BF7\u91CD\u65B0\u8FD0\u884C /login qoder-cn\uFF0C\u6216\u66F4\u65B0\u73AF\u5883\u53D8\u91CF\u4E2D\u7684\u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C\uFF08PAT\uFF09\u3002`
2374
+ `[Qoder \u51ED\u8BC1\u5931\u6548 | Credential expired] The login session or token has expired (\u9519\u8BEF\u7801 105)`,
2375
+ `- Next step: run /login qoder. You can also refresh credentials by exporting QODER_PERSONAL_ACCESS_TOKEN (PAT).`
1875
2376
  ].join("\n");
1876
2377
  }
1877
2378
  if (rawBody === void 0 || rawBody === null) {
@@ -1888,7 +2389,33 @@ function isCredentialExpiredError(error) {
1888
2389
 
1889
2390
  // src/commands/usage.ts
1890
2391
  init_region();
1891
- var ANSI = {
2392
+ function estimateRollingAddOnExpiry(addOn, latestExpiryMs, now = Date.now()) {
2393
+ if (!addOn || !addOn.total || addOn.total <= 0 || !addOn.remaining || addOn.remaining <= 0) {
2394
+ return null;
2395
+ }
2396
+ const packSize = 100;
2397
+ const totalPacks = Math.max(1, Math.round(addOn.total / packSize));
2398
+ if (totalPacks > 50) return null;
2399
+ const used = Math.max(0, addOn.used ?? 0);
2400
+ const consumedPacks = Math.min(totalPacks - 1, Math.floor(used / packSize));
2401
+ const earliestActiveIndex = consumedPacks;
2402
+ const daysAgoClaimed = totalPacks - 1 - earliestActiveIndex;
2403
+ const anchorExpiryMs = latestExpiryMs && Number.isFinite(latestExpiryMs) ? latestExpiryMs : now + 30 * 864e5;
2404
+ const earliestMs = anchorExpiryMs - daysAgoClaimed * 864e5;
2405
+ const daysRemaining = Math.max(0, Math.round((earliestMs - now) / 864e5));
2406
+ const earliestRemaining = Math.min(addOn.remaining, packSize - used % packSize);
2407
+ const d = new Date(earliestMs);
2408
+ const earliestDate = d.toISOString().slice(0, 10);
2409
+ return {
2410
+ earliestDate,
2411
+ earliestMs,
2412
+ earliestRemaining,
2413
+ totalPacks,
2414
+ consumedPacks,
2415
+ daysRemaining
2416
+ };
2417
+ }
2418
+ var ANSI2 = {
1892
2419
  reset: "\x1B[0m",
1893
2420
  bold: "\x1B[1m",
1894
2421
  dim: "\x1B[2m",
@@ -1901,15 +2428,15 @@ var EMPTY_CHAR = "\u2591";
1901
2428
  var UNKNOWN_CHAR = "?";
1902
2429
  var BAR_WIDTH = 20;
1903
2430
  var COLUMN_GAP = " ";
1904
- function paint(text, code) {
1905
- return code && text ? `${code}${text}${ANSI.reset}` : text;
2431
+ function paint2(text, code) {
2432
+ return code && text ? `${code}${text}${ANSI2.reset}` : text;
1906
2433
  }
1907
2434
  function usageColor(percentage, danger = false) {
1908
- if (danger) return `${ANSI.red}${ANSI.bold}`;
2435
+ if (danger) return `${ANSI2.red}${ANSI2.bold}`;
1909
2436
  if (typeof percentage !== "number" || !Number.isFinite(percentage)) return void 0;
1910
- if (percentage >= 85) return `${ANSI.red}${ANSI.bold}`;
1911
- if (percentage >= 60) return ANSI.yellow;
1912
- return ANSI.green;
2437
+ if (percentage >= 85) return `${ANSI2.red}${ANSI2.bold}`;
2438
+ if (percentage >= 60) return ANSI2.yellow;
2439
+ return ANSI2.green;
1913
2440
  }
1914
2441
  function usageBar(percentage, options = {}) {
1915
2442
  const width = options.width ?? BAR_WIDTH;
@@ -1918,7 +2445,7 @@ function usageBar(percentage, options = {}) {
1918
2445
  const fill = known ? FILLED_CHAR.repeat(filled) : "";
1919
2446
  const rest = known ? EMPTY_CHAR.repeat(width - filled) : UNKNOWN_CHAR.repeat(width);
1920
2447
  if (!options.color) return `[${fill}${rest}]`;
1921
- return paint("[", ANSI.dim) + paint(fill, usageColor(percentage, options.danger)) + paint(rest, ANSI.dim) + paint("]", ANSI.dim);
2448
+ return paint2("[", ANSI2.dim) + paint2(fill, usageColor(percentage, options.danger)) + paint2(rest, ANSI2.dim) + paint2("]", ANSI2.dim);
1922
2449
  }
1923
2450
  function formatAmount(value, unit) {
1924
2451
  if (typeof value !== "number" || !Number.isFinite(value)) return "?";
@@ -1929,6 +2456,10 @@ function formatPercent(value) {
1929
2456
  if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
1930
2457
  return Math.round(value * 10) / 10;
1931
2458
  }
2459
+ function normalizePercent(value) {
2460
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
2461
+ return value >= 0 && value <= 1 ? value * 100 : value;
2462
+ }
1932
2463
  function formatDailyResetCountdown(now = Date.now()) {
1933
2464
  const diffMs = msUntilBeijingMidnight(now);
1934
2465
  const totalMinutes = Math.max(1, Math.round(diffMs / 6e4));
@@ -1955,7 +2486,7 @@ function isBucketRow(row) {
1955
2486
  return typeof row.used === "number" || typeof row.total === "number";
1956
2487
  }
1957
2488
  function rightCell(text, width, code) {
1958
- return " ".repeat(Math.max(0, width - text.length)) + paint(text, code);
2489
+ return " ".repeat(Math.max(0, width - text.length)) + paint2(text, code);
1959
2490
  }
1960
2491
  function renderUsageRows(rows, notes, color) {
1961
2492
  const buckets = rows.filter(isBucketRow);
@@ -1980,16 +2511,13 @@ function renderUsageRows(rows, notes, color) {
1980
2511
  ].join(COLUMN_GAP);
1981
2512
  });
1982
2513
  for (const note of notes) {
1983
- lines.push(`${note.label.padEnd(labelWidth)}${COLUMN_GAP}${color ? paint(note.value, note.color) : note.value}`);
2514
+ lines.push(`${note.label.padEnd(labelWidth)}${COLUMN_GAP}${color ? paint2(note.value, note.color) : note.value}`);
1984
2515
  }
1985
2516
  return lines.map((line) => line.replace(/ +$/, ""));
1986
2517
  }
1987
2518
  function bucketRow(label, bucket, danger) {
1988
- const rawPercent = bucket?.percentage;
1989
2519
  const derivedPercent = typeof bucket?.used === "number" && typeof bucket?.total === "number" && bucket.total > 0 ? bucket.used / bucket.total * 100 : void 0;
1990
- const percent = formatPercent(
1991
- typeof rawPercent === "number" && Number.isFinite(rawPercent) ? rawPercent : derivedPercent
1992
- );
2520
+ const percent = formatPercent(derivedPercent ?? normalizePercent(bucket?.percentage));
1993
2521
  return {
1994
2522
  label,
1995
2523
  percent,
@@ -2019,7 +2547,9 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
2019
2547
  const org = pickBucket(raw, "orgResourcePackage") ?? pickBucket(raw, "sharedQuota");
2020
2548
  const showOrg = hasBucket(org);
2021
2549
  const orgPercent = typeof org?.used === "number" && typeof org?.cap === "number" && org.cap > 0 ? formatPercent(org.used / org.cap * 100) : void 0;
2022
- const overallPercent = formatPercent(showOrg ? raw?.totalUsagePercentage || orgPercent : raw?.totalUsagePercentage);
2550
+ const overallPercent = formatPercent(
2551
+ showOrg ? normalizePercent(raw?.totalUsagePercentage) || orgPercent : normalizePercent(raw?.totalUsagePercentage)
2552
+ );
2023
2553
  const rows = [{ label: "Overall", percent: overallPercent, tail: "used", danger }];
2024
2554
  if (showUserQuota) rows.push(bucketRow("Plan quota", pickBucket(raw, "userQuota"), danger));
2025
2555
  if (showAddOnQuota) rows.push(bucketRow("Add-on", pickBucket(raw, "addOnQuota"), danger));
@@ -2028,7 +2558,7 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
2028
2558
  const isPersonalStandard = raw?.userType === "personal_standard" || raw?.userType === "free";
2029
2559
  const notes = [];
2030
2560
  if (danger) {
2031
- notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
2561
+ notes.push({ label: "Status", value: "quota exceeded", color: ANSI2.red });
2032
2562
  }
2033
2563
  if (!showUserQuota && !showAddOnQuota && !showOrg) notes.push({ label: "Buckets", value: "none returned" });
2034
2564
  if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
@@ -2039,13 +2569,48 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
2039
2569
  if (isPersonalStandard || !showOrg && formatResetTime(raw?.expiresAt, now) === "never") {
2040
2570
  notes.push({ label: "Daily reset", value: formatDailyResetCountdown(now) });
2041
2571
  }
2572
+ const checkin = options.checkin;
2573
+ if (checkin) {
2574
+ if (checkin.claimed && checkin.expiresAt) {
2575
+ const rel = formatResetTime(Date.parse(checkin.expiresAt), now);
2576
+ notes.push({
2577
+ label: "Today checkin",
2578
+ value: `${checkin.amount} Credits claimed (expires ${rel})`,
2579
+ color: color ? ANSI2.green : void 0
2580
+ });
2581
+ } else if (checkin.claimed) {
2582
+ notes.push({
2583
+ label: "Today checkin",
2584
+ value: `${checkin.amount} Credits claimed (expires in ~30d)`,
2585
+ color: color ? ANSI2.green : void 0
2586
+ });
2587
+ } else {
2588
+ notes.push({
2589
+ label: "Today checkin",
2590
+ value: `${checkin.amount} Credits available (run /${region.providerID}.claim)`,
2591
+ color: color ? ANSI2.yellow : void 0
2592
+ });
2593
+ }
2594
+ }
2595
+ const addOn = pickBucket(raw, "addOnQuota");
2596
+ if (showAddOnQuota && (addOn?.remaining ?? 0) > 0 && checkin) {
2597
+ const latestExpiryMs = checkin.expiresAt ? Date.parse(checkin.expiresAt) : void 0;
2598
+ const estimate = estimateRollingAddOnExpiry(addOn, latestExpiryMs, now);
2599
+ if (estimate) {
2600
+ const earliestText = estimate.totalPacks > 1 ? `earliest active pack ~${estimate.earliestDate} \xB7 in ~${estimate.daysRemaining}d (~${formatAmount(estimate.earliestRemaining)} credits)` : `${estimate.earliestDate} (in ${estimate.daysRemaining}d)`;
2601
+ notes.push({
2602
+ label: "Add-on expiry",
2603
+ value: `Rolling 30d (${earliestText})`
2604
+ });
2605
+ }
2606
+ }
2042
2607
  notes.push({ label: "Expires", value: formatResetTime(raw?.expiresAt, now) });
2043
2608
  const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
2044
2609
  if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
2045
2610
  const title = `${region.usageTitle}${userType}`;
2046
2611
  return {
2047
2612
  title: region.usageTitle,
2048
- lines: [paint(title, color ? ANSI.bold : void 0), ...renderUsageRows(rows, notes, color)]
2613
+ lines: [paint2(title, color ? ANSI2.bold : void 0), ...renderUsageRows(rows, notes, color)]
2049
2614
  };
2050
2615
  }
2051
2616
  async function fetchQoderQuota(accessToken, mode) {
@@ -2055,7 +2620,8 @@ async function fetchQoderQuota(accessToken, mode) {
2055
2620
  Authorization: `Bearer ${accessToken}`,
2056
2621
  Accept: "application/json",
2057
2622
  "User-Agent": "pi-provider-qoder-cn"
2058
- }
2623
+ },
2624
+ signal: AbortSignal.timeout(1e4)
2059
2625
  });
2060
2626
  const text = await response.text();
2061
2627
  let data;
@@ -2096,7 +2662,7 @@ async function resolveUsageIdentity(providerID, mode, ctx, withIdentity = true)
2096
2662
  } catch {
2097
2663
  }
2098
2664
  }
2099
- return { access, name, email };
2665
+ return { access, name, email, userID: stored?.userID || (storedMatches ? stored?.userID : void 0) };
2100
2666
  }
2101
2667
  var FORMAT_ARGS = /* @__PURE__ */ new Set(["json", "raw", "debug"]);
2102
2668
  var PLAIN_ARGS = /* @__PURE__ */ new Set(["plain", "no-color", "nocolor"]);
@@ -2118,9 +2684,15 @@ async function runUsageCommand(mode, args, ctx) {
2118
2684
  ctx?.ui?.notify(`No ${providerID} credentials. Run /login ${providerID} first.`, "warning");
2119
2685
  return;
2120
2686
  }
2121
- const raw = await fetchQoderQuota(accessToken, mode);
2687
+ const { getMachineId: getMachineId2 } = await Promise.resolve().then(() => (init_cosy(), cosy_exports));
2688
+ const machineID = getMachineId2();
2689
+ const deviceIdentity = wantsRaw ? null : await resolveSashMachineIdentity(mode, identity?.userID).catch(() => null);
2690
+ const [raw, checkinInfo] = await Promise.all([
2691
+ fetchQoderQuota(accessToken, mode),
2692
+ wantsRaw ? Promise.resolve(null) : fetchCheckinGrantInfo(accessToken, machineID, mode, deviceIdentity).catch(() => null)
2693
+ ]);
2122
2694
  const color = !wantsRaw && shouldColorize(args, { hasUI: Boolean(ctx?.ui) });
2123
- const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color, user: identity }).lines.join("\n");
2695
+ const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color, user: identity, checkin: checkinInfo }).lines.join("\n");
2124
2696
  ctx?.ui?.notify(output, "info");
2125
2697
  if (!ctx?.ui) console.log(output);
2126
2698
  } catch (error) {
@@ -2580,7 +3152,7 @@ function transformMessagesForQoder(messages) {
2580
3152
  }
2581
3153
 
2582
3154
  // src/protocol/stream.ts
2583
- var lastForcedHealAt = 0;
3155
+ var healCooldowns = /* @__PURE__ */ new Map();
2584
3156
  var HEAL_COOLDOWN_MS = 6e4;
2585
3157
  function stableHash(prefix, ...inputs) {
2586
3158
  const hash = crypto3.createHash("sha256");
@@ -2668,6 +3240,25 @@ async function peekQueueNotice(reader, decoder) {
2668
3240
  return null;
2669
3241
  }
2670
3242
  }
3243
+ async function peekCredentialExpired(reader, decoder) {
3244
+ const text = await reader.peekLine(decoder);
3245
+ const firstLine = text.split("\n")[0]?.trim() ?? "";
3246
+ if (!firstLine.startsWith("data:")) return false;
3247
+ const payload = firstLine.slice(5).trim();
3248
+ if (!payload || payload === "[DONE]") return false;
3249
+ try {
3250
+ const envelope = JSON.parse(payload);
3251
+ if (envelope.isQueued === true) return false;
3252
+ if (typeof envelope.statusCodeValue === "number" && envelope.statusCodeValue !== 200) {
3253
+ const parsed = parseQoderErrorPayload(envelope.body);
3254
+ const message = parsed?.message ?? (typeof envelope.body === "string" ? envelope.body : "");
3255
+ return parsed?.code === "105" || /token expired|login expired/i.test(message);
3256
+ }
3257
+ return false;
3258
+ } catch {
3259
+ return false;
3260
+ }
3261
+ }
2671
3262
  function resolveRequestContext(piAi, context) {
2672
3263
  const {
2673
3264
  collapseSystemMessages,
@@ -2717,6 +3308,8 @@ function streamQoder(model, context, options) {
2717
3308
  (async () => {
2718
3309
  try {
2719
3310
  const providerMode = model.provider === "qoder-cn" ? "cn" : "global";
3311
+ let reader;
3312
+ let started = false;
2720
3313
  const region = getQoderRegionConfig(providerMode);
2721
3314
  const runAttempt = async (apiKey) => {
2722
3315
  const accessToken = apiKey ?? options?.apiKey;
@@ -2831,7 +3424,9 @@ function streamQoder(model, context, options) {
2831
3424
  });
2832
3425
  const modelSource = modelConfig.source || "system";
2833
3426
  let queuedAttempt = 0;
2834
- let reader;
3427
+ await reader?.cancel().catch(() => {
3428
+ });
3429
+ reader = void 0;
2835
3430
  let decoder;
2836
3431
  let buffer = "";
2837
3432
  let bufferStart = 0;
@@ -2852,7 +3447,7 @@ function streamQoder(model, context, options) {
2852
3447
  });
2853
3448
  if (!response.ok) {
2854
3449
  const errText = await response.text();
2855
- throw new Error(formatQoderStreamError(response.status, errText));
3450
+ throw new Error(formatQoderStreamError(response.status, errText, Date.now(), providerMode));
2856
3451
  }
2857
3452
  const body = response.body;
2858
3453
  if (!body) throw new Error("No response body");
@@ -2873,6 +3468,13 @@ function streamQoder(model, context, options) {
2873
3468
  await sleep(queue.retryAfterMs, options?.signal);
2874
3469
  continue;
2875
3470
  }
3471
+ if (await peekCredentialExpired(reader, decoder)) {
3472
+ await reader.cancel().catch(() => {
3473
+ });
3474
+ throw new Error(
3475
+ formatQoderStreamError(403, '{"code":"105","message":"Login expired"}', Date.now(), providerMode)
3476
+ );
3477
+ }
2876
3478
  break;
2877
3479
  }
2878
3480
  if (!reader || !decoder) throw new Error("No response body");
@@ -2882,6 +3484,11 @@ function streamQoder(model, context, options) {
2882
3484
  const toolCallsState = [];
2883
3485
  const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
2884
3486
  const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
3487
+ if (!started) {
3488
+ started = true;
3489
+ } else {
3490
+ output.content.length = 0;
3491
+ }
2885
3492
  stream.push({ type: "start", partial: output });
2886
3493
  let sawDone = false;
2887
3494
  while (!sawDone) {
@@ -2906,7 +3513,9 @@ function streamQoder(model, context, options) {
2906
3513
  try {
2907
3514
  const envelope = JSON.parse(dataStr);
2908
3515
  if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
2909
- throw new Error(formatQoderStreamError(envelope.statusCodeValue, envelope.body));
3516
+ throw new Error(
3517
+ formatQoderStreamError(envelope.statusCodeValue, envelope.body, Date.now(), providerMode)
3518
+ );
2910
3519
  }
2911
3520
  const innerStr = envelope.body;
2912
3521
  if (innerStr === "[DONE]") {
@@ -3090,15 +3699,32 @@ function streamQoder(model, context, options) {
3090
3699
  try {
3091
3700
  await runAttempt(void 0);
3092
3701
  } catch (error) {
3093
- if (model.provider !== "qoder-cn" || !isCredentialExpiredError(error) || Date.now() - lastForcedHealAt < HEAL_COOLDOWN_MS) {
3702
+ if (!["qoder", "qoder-cn"].includes(model.provider) || !isCredentialExpiredError(error)) {
3094
3703
  throw error;
3095
3704
  }
3096
- lastForcedHealAt = Date.now();
3097
3705
  const stored = getCachedCredentials("", model.provider);
3098
3706
  if (!stored) throw error;
3099
- const healed = await refreshQoderTokenForMode(stored, providerMode);
3707
+ const attemptedAccess = options?.apiKey;
3708
+ if (stored.access && attemptedAccess && stored.access !== attemptedAccess) {
3709
+ await runAttempt(stored.access);
3710
+ return;
3711
+ }
3712
+ const lastHeal = healCooldowns.get(model.provider) ?? 0;
3713
+ if (Date.now() - lastHeal < HEAL_COOLDOWN_MS) throw error;
3714
+ healCooldowns.set(model.provider, Date.now());
3715
+ let healed;
3716
+ try {
3717
+ healed = await refreshQoderTokenForMode(stored, providerMode, options?.signal);
3718
+ } catch (healError) {
3719
+ if (healError === error) throw error;
3720
+ healCooldowns.delete(model.provider);
3721
+ throw healError;
3722
+ }
3100
3723
  saveCredentialsToAuthFile(model.provider, healed);
3101
3724
  await runAttempt(healed.access);
3725
+ } finally {
3726
+ await reader?.cancel().catch(() => {
3727
+ });
3102
3728
  }
3103
3729
  } catch (e) {
3104
3730
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
@@ -3142,7 +3768,7 @@ function createQoderOAuth(mode) {
3142
3768
  return {
3143
3769
  name: region.loginName,
3144
3770
  login: (callbacks) => loginQoderForMode(callbacks, mode),
3145
- refreshToken: (credentials) => refreshQoderTokenForMode(credentials, mode),
3771
+ refreshToken: (credentials, signal) => refreshQoderTokenForMode(credentials, mode, signal),
3146
3772
  getApiKey: (cred) => cred.access,
3147
3773
  // NOTE: no `modifyModels` hook on purpose. OMP (Bun) does a whole-catalog
3148
3774
  // structuredClone before invoking it, and its bundled catalog contains a
@@ -3212,11 +3838,29 @@ async function index_default(pi) {
3212
3838
  }
3213
3839
  function registerCommands(pi) {
3214
3840
  pi.registerCommand("qoder-cn.usage", {
3215
- description: "Show qoder-cn quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)",
3841
+ description: "Show Qoder CN quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)",
3216
3842
  handler: async (args, ctx) => {
3217
3843
  await runUsageCommand("cn", args, ctx);
3218
3844
  }
3219
3845
  });
3846
+ pi.registerCommand("qoder-cn.claim", {
3847
+ description: "Claim Qoder CN daily 100 free Credits reward (resets daily at 10:00 UTC+8)",
3848
+ handler: async (args, ctx) => {
3849
+ await runClaimCommand("cn", args, ctx);
3850
+ }
3851
+ });
3852
+ pi.registerCommand("qoder.usage", {
3853
+ description: "Show Qoder Global quota: plan + add-on credits and check-in status (append 'json' for raw payload)",
3854
+ handler: async (args, ctx) => {
3855
+ await runUsageCommand("global", args, ctx);
3856
+ }
3857
+ });
3858
+ pi.registerCommand("qoder.claim", {
3859
+ description: "Claim Qoder Global daily 100 free Credits reward (resets daily at 10:00 UTC+8)",
3860
+ handler: async (args, ctx) => {
3861
+ await runClaimCommand("global", args, ctx);
3862
+ }
3863
+ });
3220
3864
  }
3221
3865
  export {
3222
3866
  index_default as default