dsh-grok-subscription 1.0.4 → 1.0.5

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/README.en.md CHANGED
@@ -25,7 +25,7 @@ Models, reasoning effort, and weekly quota all stay inside DSH.
25
25
 
26
26
  ## Three-step start
27
27
 
28
- 1. **Install the plugin.** Run the command below; to select a candidate version, enter the full `package@version`, for example `dsh-grok-subscription@1.0.4`.
28
+ 1. **Install the plugin.** Run the command below; to select a candidate version, enter the full `package@version`, for example `dsh-grok-subscription@1.0.5`.
29
29
 
30
30
  ```sh
31
31
  dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
@@ -47,6 +47,7 @@ Restart `dsh web` after installing or upgrading, otherwise the old `lib/` keeps
47
47
  | **Usage panel (experimental)** | Settings shows the used and remaining percentages the backend reports, and never invents numbers when the read fails |
48
48
  | **Credentials stay local** | Only the short-lived access token from `~/.grok/auth.json` is read on the host; no token is handed to browser RPC |
49
49
  | **Sign-in state refreshes the UI** | Login, pull, and logout emit `llm/adapters-updated`, so the chat model list updates with them |
50
+ | **Automatic token renewal** | The access token only lives 6 hours; near expiry, or on a 401, the plugin runs the official `grok` CLI to refresh `auth.json` and retries once, so you never re-login by hand |
50
51
  | **Failures stay visible** | When subscription routing is unavailable it reports an error instead of silently using another paid route |
51
52
 
52
53
  These capabilities reuse the same local Grok Build sign-in.
@@ -89,7 +90,7 @@ dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
89
90
  You can also install the version published on npm:
90
91
 
91
92
  ```sh
92
- dsh plugin --profile web add dsh-grok-subscription@1.0.4
93
+ dsh plugin --profile web add dsh-grok-subscription@1.0.5
93
94
  ```
94
95
 
95
96
  DSH handles target selection, the profile lock, dependency resolution, and bundle activation.
@@ -142,7 +143,8 @@ Restart DSH manually afterwards, then:
142
143
  - The model menu offers `low` / `medium` / `high` / `xhigh` reasoning effort, defaulting to `high`;
143
144
  - A weekly remaining quota badge sits beside the model picker in the composer, with remaining percentage and reset time on hover or click;
144
145
  - Settings can show the used and remaining percentages the backend reports, and says so plainly instead of guessing when the read fails;
145
- - Supports CLI login, device-code login, pull from the Grok CLI, and logout; the chat model list refreshes whenever sign-in state changes;
146
+ - Supports CLI login, device-code login, pull from the Grok CLI, and logout;
147
+ - Renews the access token automatically: near the end of its 6-hour life, or on a 401, it runs the official `grok` CLI and retries once, so a session never lapses mid-use; the chat model list refreshes whenever sign-in state changes;
146
148
  - When subscription routing is unavailable it reports an error instead of silently switching to another paid route.
147
149
 
148
150
  ### Composer quota
@@ -198,6 +200,7 @@ npx -y @deepseek-ai/dsh@0.1.5-rc.2 plugin --profile web remove dsh-grok-subscrip
198
200
  - **Nothing changed after upgrading**: the plugin keeps running the old `lib/` — reinstall the plugin, restart `dsh web`, and hard-refresh (Ctrl+Shift+R);
199
201
  - **It reports `auth.json` permissions**: apply the `chmod 600` above; the plugin refuses symlinks and group- or other-readable files;
200
202
  - **"No reply" after sending a message**: upgrade to `1.0.1` or later. `1.0.0` had a defect where tool-calling turns aborted with a non-serializable stream chunk, leaving no reply in the UI at all.
203
+ - **Chat or usage reports 401 "Invalid or expired credentials"**: a Grok access token only lives about 6 hours, and once it lapses chat and usage fail together. From `1.0.5` the plugin renews it through the official CLI automatically; on older versions run any `grok` command (for example `grok models`) to refresh `~/.grok/auth.json`, then click **Pull from Grok CLI**.
201
204
 
202
205
  ## Scope and support
203
206
 
package/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  ## 三步开始
27
27
 
28
- 1. **安装插件**:在终端运行下面的命令;指定候选版本时填写完整的 `包名@版本`,例如 `dsh-grok-subscription@1.0.4`。
28
+ 1. **安装插件**:在终端运行下面的命令;指定候选版本时填写完整的 `包名@版本`,例如 `dsh-grok-subscription@1.0.5`。
29
29
 
30
30
  ```sh
31
31
  dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
@@ -47,6 +47,7 @@
47
47
  | **用量面板(实验性)** | 设置页显示服务端返回的已用与剩余百分比,读取失败时不猜数字、不虚构额度 |
48
48
  | **凭据留在本机** | 只在主机侧读取 `~/.grok/auth.json` 的短期 access token;不会把 token 交给浏览器 RPC |
49
49
  | **登录状态会刷新界面** | 登录、拉取或登出后自动派发 `llm/adapters-updated`,Chat 模型列表随之更新 |
50
+ | **token 自动续期** | access token 只有 6 小时有效期;临近过期或遇到 401 时,插件会调用官方 `grok` CLI 刷新 `auth.json` 并自动重试一次,不需要手动重新登录 |
50
51
  | **失败可见** | 订阅路由不可用时明确报错,不会静默改用其他付费路由 |
51
52
 
52
53
  这些能力共用同一份本机 Grok Build 登录。
@@ -89,7 +90,7 @@ dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
89
90
  也可以按 npm 上的已发布版本安装:
90
91
 
91
92
  ```sh
92
- dsh plugin --profile web add dsh-grok-subscription@1.0.4
93
+ dsh plugin --profile web add dsh-grok-subscription@1.0.5
93
94
  ```
94
95
 
95
96
  目标选择、profile 锁、依赖解析和 bundle 激活均由 DSH 负责。
@@ -143,6 +144,7 @@ dsh --profile web --dump-config
143
144
  - 输入框模型选择器旁显示每周剩余额度徽章,悬停或点击查看剩余比例与重置时间;
144
145
  - 设置页可查看服务端返回的用量与剩余百分比,失败时明确提示而不是显示猜测值;
145
146
  - 支持 CLI 登录、设备码登录、从 Grok CLI 拉取和登出;登录状态变化后 Chat 模型列表自动刷新;
147
+ - access token 到期自动续期:临近 6 小时有效期或收到 401 时,调用官方 `grok` CLI 刷新会话并重试一次,不会中途要求手动重新登录;
146
148
  - 订阅路由不可用时明确报错,不会静默切换到其他付费路由。
147
149
 
148
150
  ### 输入框额度
@@ -198,6 +200,7 @@ npx -y @deepseek-ai/dsh@0.1.5-rc.2 plugin --profile web remove dsh-grok-subscrip
198
200
  - **升级后界面没变化**:插件会继续运行旧的 `lib/`,请重新安装插件并重启 `dsh web`,然后硬刷新(Ctrl+Shift+R);
199
201
  - **提示 `auth.json` 权限不正确**:按上面的 `chmod 600` 处理;插件拒绝读取符号链接或组/其他用户可读的文件;
200
202
  - **出现「没有回复」**:请升级到 `1.0.1` 或更高版本。`1.0.0` 存在一个缺陷:工具调用轮次会因流片段无法无损序列化而整轮中止,界面上完全没有回复。
203
+ - **聊天或用量提示 401「Invalid or expired credentials」**:Grok 的 access token 只有约 6 小时有效期,过期后聊天和用量会一起失败。`1.0.5` 起插件会自动调用官方 CLI 续期;更早版本请先运行任意 `grok` 命令(如 `grok models`)刷新 `~/.grok/auth.json`,再点面板里的「从 Grok CLI 拉取」。
201
204
 
202
205
  ## 边界与支持
203
206
 
package/lib/client.js CHANGED
@@ -37,7 +37,7 @@ var import_react2 = require("react");
37
37
  var zh = {
38
38
  nav: "Grok \u8BA2\u9605",
39
39
  title: "Grok \u8BA2\u9605",
40
- subtitle: "\u7528 SuperGrok / X Premium\uFF08Grok Build\uFF09\u4F1A\u8BDD\uFF0C\u800C\u4E0D\u662F XAI_API_KEY\u3002\xB7 1.0.4",
40
+ subtitle: "\u7528 SuperGrok / X Premium\uFF08Grok Build\uFF09\u4F1A\u8BDD\uFF0C\u800C\u4E0D\u662F XAI_API_KEY\u3002\xB7 1.0.5",
41
41
  signedIn: "\u5DF2\u767B\u5F55",
42
42
  signedOut: "\u672A\u767B\u5F55",
43
43
  account: "\u8D26\u6237",
@@ -87,7 +87,7 @@ var zh = {
87
87
  var en = {
88
88
  nav: "Grok Subscription",
89
89
  title: "Grok Subscription",
90
- subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY. \xB7 1.0.4",
90
+ subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY. \xB7 1.0.5",
91
91
  signedIn: "Signed in",
92
92
  signedOut: "Signed out",
93
93
  account: "Account",
package/lib/index.js CHANGED
@@ -22,6 +22,8 @@ var AUTH_FILE_MAX_BYTES = 1048576;
22
22
  var CATALOG_TIMEOUT_MS = 15e3;
23
23
  var USAGE_TIMEOUT_MS = 8e3;
24
24
  var STORE_TOKEN_TIMEOUT_MS = 5e3;
25
+ var TOKEN_EXPIRY_SKEW_MS = 12e4;
26
+ var CLI_REFRESH_TIMEOUT_MS = 2e4;
25
27
  var STREAM_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
26
28
  var MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
27
29
  var REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
@@ -1068,22 +1070,38 @@ function createDuckAdapter(session) {
1068
1070
  yield { type: "finish", reason: { kind: "error", failure: { message: "Unknown provider", code: "NO_ADAPTER" } } };
1069
1071
  return;
1070
1072
  }
1071
- const token = await session.currentToken();
1073
+ let token = await session.currentToken();
1072
1074
  if (!token) {
1073
1075
  yield { type: "finish", reason: { kind: "error", failure: { message: "Grok subscription is not signed in", code: "MISSING_CREDENTIAL" } } };
1074
1076
  return;
1075
1077
  }
1076
- try {
1077
- yield* streamResponses(options, token);
1078
- } catch (error) {
1079
- const aborted = options.signal?.aborted === true;
1080
- const failure = {
1081
- message: error instanceof Error ? error.message : "Grok Build request failed",
1082
- code: aborted ? "ABORTED" : "PROVIDER"
1083
- };
1084
- const status = finiteOr(error?.status, void 0);
1085
- if (status !== void 0) failure.status = status;
1086
- yield { type: "finish", reason: { kind: aborted ? "aborted" : "error", failure } };
1078
+ for (let attempt = 0; ; attempt++) {
1079
+ let emitted = false;
1080
+ try {
1081
+ for await (const chunk of streamResponses(options, token)) {
1082
+ emitted = true;
1083
+ yield chunk;
1084
+ }
1085
+ return;
1086
+ } catch (error) {
1087
+ const aborted = options.signal?.aborted === true;
1088
+ const status = finiteOr(error?.status, void 0);
1089
+ const canRetry = !aborted && !emitted && attempt === 0 && status === 401 && typeof session.refreshToken === "function";
1090
+ if (canRetry) {
1091
+ const renewed = await session.refreshToken();
1092
+ if (renewed && renewed !== token) {
1093
+ token = renewed;
1094
+ continue;
1095
+ }
1096
+ }
1097
+ const failure = {
1098
+ message: status === 401 ? "Grok Build session is expired or unauthorized (HTTP 401). Run grok login, then Pull from Grok CLI." : error instanceof Error ? error.message : "Grok Build request failed",
1099
+ code: aborted ? "ABORTED" : status === 401 ? "UNAUTHORIZED" : "PROVIDER"
1100
+ };
1101
+ if (status !== void 0) failure.status = status;
1102
+ yield { type: "finish", reason: { kind: aborted ? "aborted" : "error", failure } };
1103
+ return;
1104
+ }
1087
1105
  }
1088
1106
  }
1089
1107
  };
@@ -1310,6 +1328,52 @@ function spawnGrokLogin(options = {}) {
1310
1328
  });
1311
1329
  });
1312
1330
  }
1331
+ function spawnGrokRefresh(options = {}) {
1332
+ const spawnFn = options.spawn ?? spawn;
1333
+ const bin = resolveGrokBin(options.env, options.exists);
1334
+ const timeoutMs = typeof options.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : CLI_REFRESH_TIMEOUT_MS;
1335
+ return new Promise((resolve, reject) => {
1336
+ let child;
1337
+ try {
1338
+ child = spawnFn(bin, ["models"], {
1339
+ stdio: options.stdio ?? "ignore",
1340
+ env: options.env ?? process.env
1341
+ });
1342
+ } catch (error) {
1343
+ reject(new Error(`Could not start grok CLI (${bin})`, { cause: error }));
1344
+ return;
1345
+ }
1346
+ let settled = false;
1347
+ const timer = setTimeout(() => {
1348
+ if (settled) return;
1349
+ settled = true;
1350
+ try {
1351
+ child.kill("SIGKILL");
1352
+ } catch {
1353
+ }
1354
+ resolve({ ok: false, reason: "timeout" });
1355
+ }, timeoutMs);
1356
+ child.on("error", (error) => {
1357
+ if (settled) return;
1358
+ settled = true;
1359
+ clearTimeout(timer);
1360
+ reject(new Error(`Could not run grok CLI (${bin})`, { cause: error }));
1361
+ });
1362
+ child.on("exit", (code) => {
1363
+ if (settled) return;
1364
+ settled = true;
1365
+ clearTimeout(timer);
1366
+ resolve({ ok: true, code });
1367
+ });
1368
+ });
1369
+ }
1370
+ async function defaultRenewSession(options = {}) {
1371
+ const env = options.env ?? process.env;
1372
+ if (!grokCliAvailable(env)) return void 0;
1373
+ await spawnGrokRefresh({ env });
1374
+ const parsed = (options.readAuth ?? readGrokAuthSession)();
1375
+ return parsed?.session;
1376
+ }
1313
1377
  async function defaultCredentialRefOf() {
1314
1378
  try {
1315
1379
  const mod = await optionalImport("@deepseek-ai/dsh-credentials");
@@ -1344,12 +1408,15 @@ function createSessionService({
1344
1408
  readAuth = readGrokAuthSession,
1345
1409
  storeTokenTimeoutMs = STORE_TOKEN_TIMEOUT_MS,
1346
1410
  credentialsIoTimeoutMs = CREDENTIALS_IO_TIMEOUT_MS,
1347
- credentialRefOf = defaultCredentialRefOf
1411
+ credentialRefOf = defaultCredentialRefOf,
1412
+ renewSession = defaultRenewSession,
1413
+ now = () => Date.now()
1348
1414
  } = {}) {
1349
1415
  let catalog = { models: [], source: "signed-out", error: void 0 };
1350
1416
  let lastPublic = publicSessionView(void 0);
1351
1417
  let lastUsage = unavailable("Not fetched yet");
1352
1418
  let memoryAccessToken;
1419
+ let memoryTokenExpiresAt;
1353
1420
  let memorySessionTouched = false;
1354
1421
  const resolveTimeoutMs = () => {
1355
1422
  return typeof storeTokenTimeoutMs === "number" && storeTokenTimeoutMs > 0 ? storeTokenTimeoutMs : STORE_TOKEN_TIMEOUT_MS;
@@ -1357,6 +1424,48 @@ function createSessionService({
1357
1424
  const resolveIoTimeoutMs = () => {
1358
1425
  return typeof credentialsIoTimeoutMs === "number" && credentialsIoTimeoutMs > 0 ? credentialsIoTimeoutMs : CREDENTIALS_IO_TIMEOUT_MS;
1359
1426
  };
1427
+ const epochMs = (value) => {
1428
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1429
+ if (typeof value === "string" && value.trim()) {
1430
+ const parsed = Date.parse(value.trim());
1431
+ if (Number.isFinite(parsed)) return parsed;
1432
+ }
1433
+ return void 0;
1434
+ };
1435
+ const adoptSession = (session) => {
1436
+ if (!session?.accessToken) return void 0;
1437
+ memoryAccessToken = session.accessToken;
1438
+ memoryTokenExpiresAt = epochMs(session.expiresAt);
1439
+ memorySessionTouched = true;
1440
+ lastPublic = publicSessionView(session);
1441
+ return memoryAccessToken;
1442
+ };
1443
+ const memoryTokenNeedsRenewal = () => {
1444
+ if (typeof memoryAccessToken !== "string" || memoryAccessToken.length === 0) return false;
1445
+ if (typeof memoryTokenExpiresAt !== "number") return false;
1446
+ return memoryTokenExpiresAt - TOKEN_EXPIRY_SKEW_MS <= now();
1447
+ };
1448
+ let renewalInFlight;
1449
+ const renewAccessToken = async () => {
1450
+ if (renewalInFlight) return renewalInFlight;
1451
+ renewalInFlight = (async () => {
1452
+ try {
1453
+ const session = await renewSession();
1454
+ const token = adoptSession(session);
1455
+ if (token) notifyCatalogChange();
1456
+ return token;
1457
+ } catch (error) {
1458
+ logger?.warn?.(
1459
+ "Grok subscription session renewal failed: %s",
1460
+ error instanceof Error ? error.message : "unknown"
1461
+ );
1462
+ return void 0;
1463
+ } finally {
1464
+ renewalInFlight = void 0;
1465
+ }
1466
+ })();
1467
+ return renewalInFlight;
1468
+ };
1360
1469
  const notifyCatalogChange = () => {
1361
1470
  scheduleDeferred(() => {
1362
1471
  try {
@@ -1371,7 +1480,9 @@ function createSessionService({
1371
1480
  };
1372
1481
  const readStoredToken = async () => {
1373
1482
  if (typeof memoryAccessToken === "string" && memoryAccessToken.length > 0) {
1374
- return memoryAccessToken;
1483
+ if (!memoryTokenNeedsRenewal()) return memoryAccessToken;
1484
+ const renewed = await renewAccessToken();
1485
+ return renewed ?? memoryAccessToken;
1375
1486
  }
1376
1487
  if (memorySessionTouched) return void 0;
1377
1488
  if (!credentials?.resolve) return void 0;
@@ -1485,6 +1596,7 @@ function createSessionService({
1485
1596
  }
1486
1597
  const accessToken = parsed.session.accessToken;
1487
1598
  memoryAccessToken = accessToken;
1599
+ memoryTokenExpiresAt = epochMs(parsed.session.expiresAt);
1488
1600
  memorySessionTouched = true;
1489
1601
  lastPublic = publicSessionView(parsed.session);
1490
1602
  scheduleDeferred(() => {
@@ -1507,9 +1619,7 @@ function createSessionService({
1507
1619
  try {
1508
1620
  const parsed = readAuth();
1509
1621
  if (parsed.session) {
1510
- memoryAccessToken = parsed.session.accessToken;
1511
- memorySessionTouched = true;
1512
- lastPublic = publicSessionView(parsed.session);
1622
+ adoptSession(parsed.session);
1513
1623
  return { account: lastPublic, catalog, usage: lastUsage, cliAvailable: grokCliAvailable(), authPath: authJsonPath() };
1514
1624
  }
1515
1625
  } catch (error) {
@@ -1562,6 +1672,8 @@ function createSessionService({
1562
1672
  login,
1563
1673
  logout,
1564
1674
  currentToken: readStoredToken,
1675
+ /** Force a CLI-backed renewal; used when the provider answers 401. */
1676
+ refreshToken: () => renewAccessToken(),
1565
1677
  models: () => catalog.models,
1566
1678
  catalog: () => catalog,
1567
1679
  usage: () => lastUsage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-grok-subscription",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Use a SuperGrok or X Premium Grok Build subscription in DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",