dsh-grok-subscription 1.0.3 → 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 +6 -3
- package/README.md +5 -2
- package/lib/client.js +45 -10
- package/lib/index.js +129 -17
- package/package.json +10 -7
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.
|
|
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.
|
|
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;
|
|
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.
|
|
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.
|
|
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
|
@@ -20,8 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/client.jsx
|
|
21
21
|
var client_exports = {};
|
|
22
22
|
__export(client_exports, {
|
|
23
|
+
GrokSubscriptionPanel: () => GrokSubscriptionPanel,
|
|
23
24
|
GrokSubscriptionSection: () => GrokSubscriptionSection,
|
|
24
25
|
RPC_CALL_TIMEOUT_MS: () => RPC_CALL_TIMEOUT_MS,
|
|
26
|
+
SectionBoundary: () => SectionBoundary,
|
|
27
|
+
UsagePanel: () => UsagePanel,
|
|
25
28
|
apply: () => apply,
|
|
26
29
|
callRpc: () => callRpc,
|
|
27
30
|
inject: () => inject,
|
|
@@ -34,7 +37,7 @@ var import_react2 = require("react");
|
|
|
34
37
|
var zh = {
|
|
35
38
|
nav: "Grok \u8BA2\u9605",
|
|
36
39
|
title: "Grok \u8BA2\u9605",
|
|
37
|
-
subtitle: "\u7528 SuperGrok / X Premium\uFF08Grok Build\uFF09\u4F1A\u8BDD\uFF0C\u800C\u4E0D\u662F XAI_API_KEY\u3002\xB7 1.0.
|
|
40
|
+
subtitle: "\u7528 SuperGrok / X Premium\uFF08Grok Build\uFF09\u4F1A\u8BDD\uFF0C\u800C\u4E0D\u662F XAI_API_KEY\u3002\xB7 1.0.5",
|
|
38
41
|
signedIn: "\u5DF2\u767B\u5F55",
|
|
39
42
|
signedOut: "\u672A\u767B\u5F55",
|
|
40
43
|
account: "\u8D26\u6237",
|
|
@@ -54,6 +57,7 @@ var zh = {
|
|
|
54
57
|
pullHint: "\u5B89\u5168\u8BFB\u53D6 ~/.grok/auth.json\uFF08\u62D2\u7EDD\u7B26\u53F7\u94FE\u63A5\u4E0E\u7EC4/\u5176\u4ED6\u4EBA\u53EF\u8BFB\uFF09\uFF0C\u53EA\u628A\u77ED\u671F access token \u5199\u5165 DSH\u3002",
|
|
55
58
|
loginHelp: "\u767B\u5F55\u65B9\u5F0F\u8BF4\u660E",
|
|
56
59
|
refreshCatalog: "\u5237\u65B0\u6A21\u578B\u76EE\u5F55",
|
|
60
|
+
renderError: "\u754C\u9762\u6E32\u67D3\u5931\u8D25",
|
|
57
61
|
busy: "\u5904\u7406\u4E2D\u2026",
|
|
58
62
|
busyCatalog: "\u6B63\u5728\u5237\u65B0\u6A21\u578B\u76EE\u5F55\u2026",
|
|
59
63
|
busyUsage: "\u6B63\u5728\u5237\u65B0\u7528\u91CF\u2026",
|
|
@@ -83,7 +87,7 @@ var zh = {
|
|
|
83
87
|
var en = {
|
|
84
88
|
nav: "Grok Subscription",
|
|
85
89
|
title: "Grok Subscription",
|
|
86
|
-
subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY. \xB7 1.0.
|
|
90
|
+
subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY. \xB7 1.0.5",
|
|
87
91
|
signedIn: "Signed in",
|
|
88
92
|
signedOut: "Signed out",
|
|
89
93
|
account: "Account",
|
|
@@ -103,6 +107,7 @@ var en = {
|
|
|
103
107
|
pullHint: "Reads ~/.grok/auth.json securely (refuses symlinks and group/other-readable files) and stores only the short-lived access token in DSH.",
|
|
104
108
|
loginHelp: "How signing in works",
|
|
105
109
|
refreshCatalog: "Refresh model catalog",
|
|
110
|
+
renderError: "Failed to render this panel",
|
|
106
111
|
busy: "Working\u2026",
|
|
107
112
|
busyCatalog: "Refreshing model catalog\u2026",
|
|
108
113
|
busyUsage: "Refreshing usage\u2026",
|
|
@@ -614,6 +619,32 @@ async function callRpc(rpc, endpoint, payload = {}) {
|
|
|
614
619
|
if (timer) clearTimeout(timer);
|
|
615
620
|
}
|
|
616
621
|
}
|
|
622
|
+
var SectionBoundary = class extends import_react2.Component {
|
|
623
|
+
constructor(props) {
|
|
624
|
+
super(props);
|
|
625
|
+
this.state = { error: void 0 };
|
|
626
|
+
}
|
|
627
|
+
static getDerivedStateFromError(error) {
|
|
628
|
+
return { error };
|
|
629
|
+
}
|
|
630
|
+
render() {
|
|
631
|
+
const { error } = this.state;
|
|
632
|
+
if (!error) return this.props.children;
|
|
633
|
+
const t = this.props.t;
|
|
634
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: "grokSubscription", children: [
|
|
635
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: SETTINGS_STYLE }),
|
|
636
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gsHead", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: t("title") }) }),
|
|
637
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gsCard", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "gsStatus gsStatus--error", children: [
|
|
638
|
+
t("renderError"),
|
|
639
|
+
": ",
|
|
640
|
+
error instanceof Error ? error.message : String(error)
|
|
641
|
+
] }) })
|
|
642
|
+
] });
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
function GrokSubscriptionPanel(props) {
|
|
646
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SectionBoundary, { t: props.t, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GrokSubscriptionSection, { ...props }) });
|
|
647
|
+
}
|
|
617
648
|
function Chevron() {
|
|
618
649
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "gsChevron", width: "12", height: "12", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 6l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" }) });
|
|
619
650
|
}
|
|
@@ -625,12 +656,16 @@ function Chip({ tone = "off", children }) {
|
|
|
625
656
|
}
|
|
626
657
|
function UsagePanel({ usage, t, usageBusy, onRefresh, signedIn }) {
|
|
627
658
|
const ok = usage?.status === "ok";
|
|
628
|
-
const
|
|
629
|
-
const
|
|
659
|
+
const usedNumber = ok && Number.isFinite(usage.usedPercent) ? usage.usedPercent : void 0;
|
|
660
|
+
const remainingNumber = ok && Number.isFinite(usage.remainingPercent) ? usage.remainingPercent : void 0;
|
|
661
|
+
const used = usedNumber !== void 0 ? formatPercent(usedNumber) : void 0;
|
|
662
|
+
const remaining = remainingNumber !== void 0 ? formatPercent(remainingNumber) : void 0;
|
|
630
663
|
const resetLabel = usage?.periodEndLocal || usage?.periodEnd;
|
|
631
|
-
const
|
|
632
|
-
const
|
|
633
|
-
const
|
|
664
|
+
const gaugeNumber = remainingNumber ?? (usedNumber !== void 0 ? 100 - usedNumber : void 0);
|
|
665
|
+
const gaugeValue = gaugeNumber !== void 0 ? formatPercent(gaugeNumber) : void 0;
|
|
666
|
+
const gaugeLabel = remainingNumber !== void 0 ? t("usageRemaining") : t("usageUsed");
|
|
667
|
+
const fill = gaugeNumber === void 0 ? 0 : Math.min(100, Math.max(0, gaugeNumber));
|
|
668
|
+
const products = Array.isArray(usage?.productUsage) ? usage.productUsage : [];
|
|
634
669
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsCard", children: [
|
|
635
670
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gsCardHead", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { children: t("usageTitle") }) }),
|
|
636
671
|
ok && (used !== void 0 || remaining !== void 0) ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsGauge", children: [
|
|
@@ -660,11 +695,11 @@ function UsagePanel({ usage, t, usageBusy, onRefresh, signedIn }) {
|
|
|
660
695
|
t("usageUnavailable"),
|
|
661
696
|
usage?.reason ? `: ${usage.reason}` : ""
|
|
662
697
|
] }),
|
|
663
|
-
|
|
698
|
+
products.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gsRows", children: products.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsRow", children: [
|
|
664
699
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: row.name ?? "\u2014" }),
|
|
665
700
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gsRowValue", children: typeof row.usedPercent === "number" ? `${formatPercent(row.usedPercent)}%` : "\u2014" })
|
|
666
701
|
] }, `${row.name ?? "row"}-${index}`)) }) : null,
|
|
667
|
-
usage
|
|
702
|
+
usage?.fetchedAt ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "gsHint", children: [
|
|
668
703
|
t("usageFetchedAt"),
|
|
669
704
|
": ",
|
|
670
705
|
usage.fetchedAt
|
|
@@ -879,7 +914,7 @@ function apply(ctx) {
|
|
|
879
914
|
label: () => t("nav"),
|
|
880
915
|
locale: LOCALE_NS,
|
|
881
916
|
inject: () => ({ rpc, t })
|
|
882
|
-
},
|
|
917
|
+
}, GrokSubscriptionPanel));
|
|
883
918
|
const installDirectorySlots = (scope) => {
|
|
884
919
|
let modelDirectories;
|
|
885
920
|
try {
|
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
|
-
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "node scripts/build.mjs",
|
|
42
|
+
"pretest": "npm run build",
|
|
42
43
|
"test": "node --test tests/*.test.mjs",
|
|
43
44
|
"check": "npm test && npm run build"
|
|
44
45
|
},
|
|
@@ -55,8 +56,12 @@
|
|
|
55
56
|
"node": "^22.19.0 || >=24.0.0"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
59
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.5-rc.2",
|
|
58
60
|
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
|
|
59
61
|
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "^0.1.5-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
|
|
60
65
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
|
|
61
66
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
|
|
62
67
|
"@deepseek-ai/dsh-credentials": "^0.1.5-rc.2",
|
|
@@ -66,11 +71,7 @@
|
|
|
66
71
|
"@deepseek-ai/dsh-web": "^0.1.5-rc.2",
|
|
67
72
|
"@earendil-works/pi-ai": "^0.82.1",
|
|
68
73
|
"react": "^18.2.0",
|
|
69
|
-
"react-dom": "^18.2.0"
|
|
70
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.5-rc.2",
|
|
71
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
|
|
72
|
-
"@deepseek-ai/dsh-client-ui-model-selection": "^0.1.5-rc.2",
|
|
73
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2"
|
|
74
|
+
"react-dom": "^18.2.0"
|
|
74
75
|
},
|
|
75
76
|
"peerDependenciesMeta": {
|
|
76
77
|
"@deepseek-ai/dsh-client-connection": {
|
|
@@ -123,7 +124,9 @@
|
|
|
123
124
|
}
|
|
124
125
|
},
|
|
125
126
|
"devDependencies": {
|
|
126
|
-
"esbuild": "^0.25.0"
|
|
127
|
+
"esbuild": "^0.25.0",
|
|
128
|
+
"react": "^18.3.1",
|
|
129
|
+
"react-dom": "^18.3.1"
|
|
127
130
|
},
|
|
128
131
|
"author": "BaronCyrus",
|
|
129
132
|
"repository": {
|