dsh-grok-subscription 1.0.4 → 1.0.6
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 +13 -3
- package/README.md +12 -2
- package/lib/client.js +7 -2
- package/lib/index.js +268 -35
- package/package.json +1 -1
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.6`.
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
31
|
dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
|
|
@@ -43,10 +43,12 @@ Restart `dsh web` after installing or upgrading, otherwise the old `lib/` keeps
|
|
|
43
43
|
| **Direct subscription routing** | Reuse the official Grok Build CLI session — no `XAI_API_KEY` needed |
|
|
44
44
|
| **Real model catalog** | Read the models the account can actually use (such as `grok-4.7`, `grok-4.6`, `grok-4.5`) from `/v1/models-v2`; nothing is exposed while signed out |
|
|
45
45
|
| **Reasoning effort** | The model picker offers `low` / `medium` / `high` / `xhigh`, defaulting to `high`, matching the Codex interaction |
|
|
46
|
+
| **Image input** | `grok-4.7` / `grok-4.7-fast` / `grok-4.6` accept pasted images, normalized to the host's pixel and byte budget by its attachment service |
|
|
46
47
|
| **Composer quota** | A badge beside the model picker shows weekly remaining percentage; hover or click for "Weekly quota · N% left · resets M/D HH:mm" |
|
|
47
48
|
| **Usage panel (experimental)** | Settings shows the used and remaining percentages the backend reports, and never invents numbers when the read fails |
|
|
48
49
|
| **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
50
|
| **Sign-in state refreshes the UI** | Login, pull, and logout emit `llm/adapters-updated`, so the chat model list updates with them |
|
|
51
|
+
| **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
52
|
| **Failures stay visible** | When subscription routing is unavailable it reports an error instead of silently using another paid route |
|
|
51
53
|
|
|
52
54
|
These capabilities reuse the same local Grok Build sign-in.
|
|
@@ -89,7 +91,7 @@ dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
|
|
|
89
91
|
You can also install the version published on npm:
|
|
90
92
|
|
|
91
93
|
```sh
|
|
92
|
-
dsh plugin --profile web add dsh-grok-subscription@1.0.
|
|
94
|
+
dsh plugin --profile web add dsh-grok-subscription@1.0.6
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
DSH handles target selection, the profile lock, dependency resolution, and bundle activation.
|
|
@@ -142,7 +144,8 @@ Restart DSH manually afterwards, then:
|
|
|
142
144
|
- The model menu offers `low` / `medium` / `high` / `xhigh` reasoning effort, defaulting to `high`;
|
|
143
145
|
- A weekly remaining quota badge sits beside the model picker in the composer, with remaining percentage and reset time on hover or click;
|
|
144
146
|
- 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;
|
|
147
|
+
- Supports CLI login, device-code login, pull from the Grok CLI, and logout;
|
|
148
|
+
- 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
149
|
- When subscription routing is unavailable it reports an error instead of silently switching to another paid route.
|
|
147
150
|
|
|
148
151
|
### Composer quota
|
|
@@ -153,6 +156,12 @@ Restart DSH manually afterwards, then:
|
|
|
153
156
|
|
|
154
157
|
The badge appears only when the current session's provider is `grok-build` and the usage read succeeds; hover or click for "Weekly quota · N% left · resets M/D HH:mm". It reflects only the weekly quota the backend returns, and when the read fails the badge is simply hidden — chat is unaffected.
|
|
155
158
|
|
|
159
|
+
### Image input
|
|
160
|
+
|
|
161
|
+
`grok-4.7`, `grok-4.7-fast` and `grok-4.6` accept image input: paste or attach an image in the composer and ask about it. Images are normalized to the host's pixel and byte budget by its attachment service before they are sent, and an over-budget image degrades to a text description rather than being dropped silently.
|
|
162
|
+
|
|
163
|
+
`grok-4.5` is **deliberately excluded**. Given a solid red image and asked its colour it answered "green", and it described a yellow square as a "circle with a black outline" — it confabulates instead of looking. The plugin therefore does not advertise image input for it, so it cannot return confident nonsense.
|
|
164
|
+
|
|
156
165
|
### Reasoning effort
|
|
157
166
|
|
|
158
167
|
After selecting a Grok model, the model menu exposes a reasoning effort submenu: `low` / `medium` / `high` / `xhigh`, defaulting to `high`. The levels come from `model.reasoning` metadata (`efforts` + `defaultEffort`), so the interaction matches Codex in DSH; the levels actually available depend on the account's model catalog.
|
|
@@ -198,6 +207,7 @@ npx -y @deepseek-ai/dsh@0.1.5-rc.2 plugin --profile web remove dsh-grok-subscrip
|
|
|
198
207
|
- **Nothing changed after upgrading**: the plugin keeps running the old `lib/` — reinstall the plugin, restart `dsh web`, and hard-refresh (Ctrl+Shift+R);
|
|
199
208
|
- **It reports `auth.json` permissions**: apply the `chmod 600` above; the plugin refuses symlinks and group- or other-readable files;
|
|
200
209
|
- **"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.
|
|
210
|
+
- **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
211
|
|
|
202
212
|
## Scope and support
|
|
203
213
|
|
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.6`。
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
31
|
dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
|
|
@@ -43,10 +43,12 @@
|
|
|
43
43
|
| **订阅直连** | 复用官方 Grok Build CLI 会话,不需要 `XAI_API_KEY` |
|
|
44
44
|
| **真实模型目录** | 登录后从 `/v1/models-v2` 读取账号实际可用的模型(如 `grok-4.7`、`grok-4.6`、`grok-4.5`);未登录时不暴露任何模型 |
|
|
45
45
|
| **推理档位** | 模型选择器内置 `low` / `medium` / `high` / `xhigh` 子菜单,默认 `high`,与 Codex 的交互一致 |
|
|
46
|
+
| **图片输入** | `grok-4.7` / `grok-4.7-fast` / `grok-4.6` 可直接粘贴图片提问,图片由宿主的附件服务按像素与体积预算归一化 |
|
|
46
47
|
| **输入框额度** | 模型选择器旁的徽章直接显示每周剩余比例,悬停或点击查看「每周额度 剩余 N% · 重置于 M/D HH:mm」 |
|
|
47
48
|
| **用量面板(实验性)** | 设置页显示服务端返回的已用与剩余百分比,读取失败时不猜数字、不虚构额度 |
|
|
48
49
|
| **凭据留在本机** | 只在主机侧读取 `~/.grok/auth.json` 的短期 access token;不会把 token 交给浏览器 RPC |
|
|
49
50
|
| **登录状态会刷新界面** | 登录、拉取或登出后自动派发 `llm/adapters-updated`,Chat 模型列表随之更新 |
|
|
51
|
+
| **token 自动续期** | access token 只有 6 小时有效期;临近过期或遇到 401 时,插件会调用官方 `grok` CLI 刷新 `auth.json` 并自动重试一次,不需要手动重新登录 |
|
|
50
52
|
| **失败可见** | 订阅路由不可用时明确报错,不会静默改用其他付费路由 |
|
|
51
53
|
|
|
52
54
|
这些能力共用同一份本机 Grok Build 登录。
|
|
@@ -89,7 +91,7 @@ dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
|
|
|
89
91
|
也可以按 npm 上的已发布版本安装:
|
|
90
92
|
|
|
91
93
|
```sh
|
|
92
|
-
dsh plugin --profile web add dsh-grok-subscription@1.0.
|
|
94
|
+
dsh plugin --profile web add dsh-grok-subscription@1.0.6
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
目标选择、profile 锁、依赖解析和 bundle 激活均由 DSH 负责。
|
|
@@ -143,6 +145,7 @@ dsh --profile web --dump-config
|
|
|
143
145
|
- 输入框模型选择器旁显示每周剩余额度徽章,悬停或点击查看剩余比例与重置时间;
|
|
144
146
|
- 设置页可查看服务端返回的用量与剩余百分比,失败时明确提示而不是显示猜测值;
|
|
145
147
|
- 支持 CLI 登录、设备码登录、从 Grok CLI 拉取和登出;登录状态变化后 Chat 模型列表自动刷新;
|
|
148
|
+
- access token 到期自动续期:临近 6 小时有效期或收到 401 时,调用官方 `grok` CLI 刷新会话并重试一次,不会中途要求手动重新登录;
|
|
146
149
|
- 订阅路由不可用时明确报错,不会静默切换到其他付费路由。
|
|
147
150
|
|
|
148
151
|
### 输入框额度
|
|
@@ -153,6 +156,12 @@ dsh --profile web --dump-config
|
|
|
153
156
|
|
|
154
157
|
仅当当前会话的 provider 为 `grok-build` 且用量读取成功时显示徽章;悬停或点击可查看「每周额度 剩余 N% · 重置于 M/D HH:mm」。徽章只反映服务端返回的每周额度;读取失败时徽章不显示,聊天不受影响。
|
|
155
158
|
|
|
159
|
+
### 图片输入
|
|
160
|
+
|
|
161
|
+
`grok-4.7`、`grok-4.7-fast` 与 `grok-4.6` 支持图片输入:在输入框粘贴或附加图片即可提问。图片由宿主的附件服务按像素与体积预算归一化后发送,超预算时降级为文本说明,而不是静默丢弃。
|
|
162
|
+
|
|
163
|
+
`grok-4.5` **不在支持列表内**。实测给它一张纯红图片并询问颜色,它回答 "green",还把黄色方块描述成「带黑边的圆」——它在编造而不是看图。插件因此不为它声明图片能力,避免输出看起来自信却完全错误的答案。
|
|
164
|
+
|
|
156
165
|
### 推理档位
|
|
157
166
|
|
|
158
167
|
选择 Grok 模型后,模型菜单里会出现推理档位子菜单:`low` / `medium` / `high` / `xhigh`,默认 `high`。档位通过 `model.reasoning`(`efforts` + `defaultEffort`)元数据提供,因此与 Codex 在 DSH 中的交互一致;具体可用档位以账号模型目录为准。
|
|
@@ -198,6 +207,7 @@ npx -y @deepseek-ai/dsh@0.1.5-rc.2 plugin --profile web remove dsh-grok-subscrip
|
|
|
198
207
|
- **升级后界面没变化**:插件会继续运行旧的 `lib/`,请重新安装插件并重启 `dsh web`,然后硬刷新(Ctrl+Shift+R);
|
|
199
208
|
- **提示 `auth.json` 权限不正确**:按上面的 `chmod 600` 处理;插件拒绝读取符号链接或组/其他用户可读的文件;
|
|
200
209
|
- **出现「没有回复」**:请升级到 `1.0.1` 或更高版本。`1.0.0` 存在一个缺陷:工具调用轮次会因流片段无法无损序列化而整轮中止,界面上完全没有回复。
|
|
210
|
+
- **聊天或用量提示 401「Invalid or expired credentials」**:Grok 的 access token 只有约 6 小时有效期,过期后聊天和用量会一起失败。`1.0.5` 起插件会自动调用官方 CLI 续期;更早版本请先运行任意 `grok` 命令(如 `grok models`)刷新 `~/.grok/auth.json`,再点面板里的「从 Grok CLI 拉取」。
|
|
201
211
|
|
|
202
212
|
## 边界与支持
|
|
203
213
|
|
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.
|
|
40
|
+
subtitle: "\u7528 SuperGrok / X Premium\uFF08Grok Build\uFF09\u4F1A\u8BDD\uFF0C\u800C\u4E0D\u662F XAI_API_KEY\u3002\xB7 1.0.6",
|
|
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.
|
|
90
|
+
subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY. \xB7 1.0.6",
|
|
91
91
|
signedIn: "Signed in",
|
|
92
92
|
signedOut: "Signed out",
|
|
93
93
|
account: "Account",
|
|
@@ -173,6 +173,11 @@ var MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
|
173
173
|
var REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
|
|
174
174
|
var REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
|
|
175
175
|
var FALLBACK_MODEL_IDS = Object.freeze(["grok-4.7", "grok-4.6", "grok-4.5"]);
|
|
176
|
+
var IMAGE_INPUT_MODEL_IDS = Object.freeze([
|
|
177
|
+
"grok-4.7",
|
|
178
|
+
"grok-4.7-build-fast",
|
|
179
|
+
"grok-4.6"
|
|
180
|
+
]);
|
|
176
181
|
|
|
177
182
|
// src/client-settings-style.js
|
|
178
183
|
var SETTINGS_STYLE = `
|
package/lib/index.js
CHANGED
|
@@ -22,19 +22,26 @@ 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;
|
|
28
30
|
var REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
|
|
29
31
|
var FALLBACK_MODEL_IDS = Object.freeze(["grok-4.7", "grok-4.6", "grok-4.5"]);
|
|
32
|
+
var IMAGE_INPUT_MODEL_IDS = Object.freeze([
|
|
33
|
+
"grok-4.7",
|
|
34
|
+
"grok-4.7-build-fast",
|
|
35
|
+
"grok-4.6"
|
|
36
|
+
]);
|
|
30
37
|
var IMPORT_TIMEOUT_MS = 2500;
|
|
31
38
|
var CREDENTIALS_IO_TIMEOUT_MS = 1500;
|
|
32
39
|
var RPC_HANDLER_TIMEOUT_MS = 8e3;
|
|
33
40
|
|
|
34
41
|
// src/session.js
|
|
35
|
-
import { existsSync as
|
|
42
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
36
43
|
import { spawn } from "node:child_process";
|
|
37
|
-
import { join as
|
|
44
|
+
import { join as join3 } from "node:path";
|
|
38
45
|
|
|
39
46
|
// src/auth-file.js
|
|
40
47
|
import { lstatSync, openSync, readFileSync, closeSync, constants as fsConstants } from "node:fs";
|
|
@@ -335,6 +342,9 @@ function mergeCatalog(live) {
|
|
|
335
342
|
}
|
|
336
343
|
return live.map((model) => ({ ...model, source: "live" }));
|
|
337
344
|
}
|
|
345
|
+
function supportsImageInput(id) {
|
|
346
|
+
return typeof id === "string" && IMAGE_INPUT_MODEL_IDS.includes(id);
|
|
347
|
+
}
|
|
338
348
|
function toPiModels(models) {
|
|
339
349
|
return models.map((model) => {
|
|
340
350
|
const efforts = model.reasoningEfforts ?? ["low", "medium", "high", "xhigh"];
|
|
@@ -355,7 +365,7 @@ function toPiModels(models) {
|
|
|
355
365
|
baseUrl: PROXY_BASE_URL,
|
|
356
366
|
reasoning: model.reasoning !== false,
|
|
357
367
|
thinkingLevelMap,
|
|
358
|
-
input: ["text"],
|
|
368
|
+
input: supportsImageInput(model.id) ? ["text", "image"] : ["text"],
|
|
359
369
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
360
370
|
contextWindow: model.contextWindow ?? 5e5,
|
|
361
371
|
maxTokens: model.maxTokens ?? 5e5,
|
|
@@ -709,6 +719,11 @@ async function fetchBillingUsage(accessToken, options = {}) {
|
|
|
709
719
|
}
|
|
710
720
|
|
|
711
721
|
// src/adapter.js
|
|
722
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
723
|
+
import { createRequire } from "node:module";
|
|
724
|
+
import { homedir as homedir2 } from "node:os";
|
|
725
|
+
import { dirname, join as join2 } from "node:path";
|
|
726
|
+
import { pathToFileURL } from "node:url";
|
|
712
727
|
function withImportTimeout(promise, ms, message) {
|
|
713
728
|
let timer;
|
|
714
729
|
return Promise.race([
|
|
@@ -720,18 +735,115 @@ function withImportTimeout(promise, ms, message) {
|
|
|
720
735
|
if (timer) clearTimeout(timer);
|
|
721
736
|
});
|
|
722
737
|
}
|
|
738
|
+
function hostModuleRoots(env = process.env, argv1 = process.argv[1]) {
|
|
739
|
+
const roots = [];
|
|
740
|
+
const push = (dir) => {
|
|
741
|
+
if (typeof dir === "string" && dir && !roots.includes(dir)) roots.push(dir);
|
|
742
|
+
};
|
|
743
|
+
const override = env.DSH_GROK_PEER_ROOT;
|
|
744
|
+
if (typeof override === "string" && override.trim()) push(override.trim());
|
|
745
|
+
if (typeof argv1 === "string" && argv1) {
|
|
746
|
+
let dir = dirname(argv1);
|
|
747
|
+
for (let depth = 0; depth < 6 && dir && dir !== dirname(dir); depth++) {
|
|
748
|
+
push(join2(dir, "node_modules"));
|
|
749
|
+
dir = dirname(dir);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const prefix = env.NPM_CONFIG_PREFIX;
|
|
753
|
+
if (typeof prefix === "string" && prefix.trim()) push(join2(prefix.trim(), "lib", "node_modules"));
|
|
754
|
+
push(join2(homedir2(), ".local", "lib", "node_modules"));
|
|
755
|
+
const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME.trim() ? env.DSH_HOME.trim() : join2(homedir2(), ".dsh");
|
|
756
|
+
push(join2(dshHome, "profiles", "node_modules"));
|
|
757
|
+
return roots;
|
|
758
|
+
}
|
|
759
|
+
function splitSpecifier(specifier) {
|
|
760
|
+
const parts = specifier.split("/");
|
|
761
|
+
const take = specifier.startsWith("@") ? 2 : 1;
|
|
762
|
+
return {
|
|
763
|
+
name: parts.slice(0, take).join("/"),
|
|
764
|
+
subpath: parts.length > take ? `./${parts.slice(take).join("/")}` : "."
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function pickExportTarget(value) {
|
|
768
|
+
if (typeof value === "string") return value;
|
|
769
|
+
if (!value || typeof value !== "object") return void 0;
|
|
770
|
+
for (const condition of ["import", "module", "default", "node"]) {
|
|
771
|
+
if (condition in value) {
|
|
772
|
+
const picked = pickExportTarget(value[condition]);
|
|
773
|
+
if (picked) return picked;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return void 0;
|
|
777
|
+
}
|
|
778
|
+
function resolveExportsTarget(exportsField, subpath) {
|
|
779
|
+
if (typeof exportsField === "string") return subpath === "." ? exportsField : void 0;
|
|
780
|
+
if (!exportsField || typeof exportsField !== "object") return void 0;
|
|
781
|
+
const keys = Object.keys(exportsField);
|
|
782
|
+
const isSubpathMap = keys.some((key) => key === "." || key.startsWith("./"));
|
|
783
|
+
if (!isSubpathMap) return subpath === "." ? pickExportTarget(exportsField) : void 0;
|
|
784
|
+
if (subpath in exportsField) return pickExportTarget(exportsField[subpath]);
|
|
785
|
+
for (const key of keys) {
|
|
786
|
+
const star = key.indexOf("*");
|
|
787
|
+
if (star === -1) continue;
|
|
788
|
+
const prefix = key.slice(0, star);
|
|
789
|
+
const suffix = key.slice(star + 1);
|
|
790
|
+
if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue;
|
|
791
|
+
const matched = subpath.slice(prefix.length, subpath.length - suffix.length);
|
|
792
|
+
const target = pickExportTarget(exportsField[key]);
|
|
793
|
+
if (target) return target.replace("*", matched);
|
|
794
|
+
}
|
|
795
|
+
return void 0;
|
|
796
|
+
}
|
|
797
|
+
function resolveHostSpecifierManually(root, specifier) {
|
|
798
|
+
const { name: name2, subpath } = splitSpecifier(specifier);
|
|
799
|
+
for (const pkgDir of [join2(root, name2), join2(root, "node_modules", name2)]) {
|
|
800
|
+
const manifest = join2(pkgDir, "package.json");
|
|
801
|
+
if (!existsSync2(manifest)) continue;
|
|
802
|
+
let pkg;
|
|
803
|
+
try {
|
|
804
|
+
pkg = JSON.parse(readFileSync3(manifest, "utf8"));
|
|
805
|
+
} catch {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const target = pkg.exports !== void 0 ? resolveExportsTarget(pkg.exports, subpath) : subpath === "." ? pkg.module ?? pkg.main ?? "index.js" : void 0;
|
|
809
|
+
if (typeof target !== "string" || !target) continue;
|
|
810
|
+
const file = join2(pkgDir, target);
|
|
811
|
+
if (existsSync2(file)) return file;
|
|
812
|
+
}
|
|
813
|
+
return void 0;
|
|
814
|
+
}
|
|
815
|
+
function resolveHostSpecifier(specifier, roots = hostModuleRoots()) {
|
|
816
|
+
for (const root of roots) {
|
|
817
|
+
try {
|
|
818
|
+
const resolved = createRequire(join2(root, "noop.js")).resolve(specifier);
|
|
819
|
+
if (typeof resolved === "string" && resolved) return resolved;
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
const manual = resolveHostSpecifierManually(root, specifier);
|
|
823
|
+
if (manual) return manual;
|
|
824
|
+
}
|
|
825
|
+
return void 0;
|
|
826
|
+
}
|
|
723
827
|
async function optionalImport(specifier, options = {}) {
|
|
724
828
|
const timeoutMs = typeof options.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : IMPORT_TIMEOUT_MS;
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
829
|
+
const injected = typeof options.importFn === "function" ? options.importFn : void 0;
|
|
830
|
+
const importFn = injected ?? ((id) => import(id));
|
|
831
|
+
const candidates = [specifier];
|
|
832
|
+
if (!injected && !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("file:")) {
|
|
833
|
+
const resolved = resolveHostSpecifier(specifier, options.hostRoots ?? hostModuleRoots());
|
|
834
|
+
if (resolved) candidates.push(pathToFileURL(resolved).href);
|
|
835
|
+
}
|
|
836
|
+
for (const candidate of candidates) {
|
|
837
|
+
try {
|
|
838
|
+
return await withImportTimeout(
|
|
839
|
+
Promise.resolve(importFn(candidate)),
|
|
840
|
+
timeoutMs,
|
|
841
|
+
`Import timed out after ${timeoutMs}ms: ${specifier}`
|
|
842
|
+
);
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
734
845
|
}
|
|
846
|
+
return void 0;
|
|
735
847
|
}
|
|
736
848
|
function textOf(content) {
|
|
737
849
|
if (typeof content === "string") return content;
|
|
@@ -1068,22 +1180,38 @@ function createDuckAdapter(session) {
|
|
|
1068
1180
|
yield { type: "finish", reason: { kind: "error", failure: { message: "Unknown provider", code: "NO_ADAPTER" } } };
|
|
1069
1181
|
return;
|
|
1070
1182
|
}
|
|
1071
|
-
|
|
1183
|
+
let token = await session.currentToken();
|
|
1072
1184
|
if (!token) {
|
|
1073
1185
|
yield { type: "finish", reason: { kind: "error", failure: { message: "Grok subscription is not signed in", code: "MISSING_CREDENTIAL" } } };
|
|
1074
1186
|
return;
|
|
1075
1187
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1188
|
+
for (let attempt = 0; ; attempt++) {
|
|
1189
|
+
let emitted = false;
|
|
1190
|
+
try {
|
|
1191
|
+
for await (const chunk of streamResponses(options, token)) {
|
|
1192
|
+
emitted = true;
|
|
1193
|
+
yield chunk;
|
|
1194
|
+
}
|
|
1195
|
+
return;
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
const aborted = options.signal?.aborted === true;
|
|
1198
|
+
const status = finiteOr(error?.status, void 0);
|
|
1199
|
+
const canRetry = !aborted && !emitted && attempt === 0 && status === 401 && typeof session.refreshToken === "function";
|
|
1200
|
+
if (canRetry) {
|
|
1201
|
+
const renewed = await session.refreshToken();
|
|
1202
|
+
if (renewed && renewed !== token) {
|
|
1203
|
+
token = renewed;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const failure = {
|
|
1208
|
+
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",
|
|
1209
|
+
code: aborted ? "ABORTED" : status === 401 ? "UNAUTHORIZED" : "PROVIDER"
|
|
1210
|
+
};
|
|
1211
|
+
if (status !== void 0) failure.status = status;
|
|
1212
|
+
yield { type: "finish", reason: { kind: aborted ? "aborted" : "error", failure } };
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1087
1215
|
}
|
|
1088
1216
|
}
|
|
1089
1217
|
};
|
|
@@ -1268,7 +1396,11 @@ async function createGrokBuildAdapter(session, options = {}) {
|
|
|
1268
1396
|
env: async () => void 0,
|
|
1269
1397
|
fileExists: async () => false
|
|
1270
1398
|
})
|
|
1271
|
-
})
|
|
1399
|
+
}),
|
|
1400
|
+
// The host's durable attachment store. Without it PiAiAdapter rejects any
|
|
1401
|
+
// request carrying an image ("requires the durable attachment service"),
|
|
1402
|
+
// and with it images are normalized to the configured budget for us.
|
|
1403
|
+
resolveAttachments: typeof options.resolveAttachments === "function" ? () => options.resolveAttachments() : void 0
|
|
1272
1404
|
});
|
|
1273
1405
|
return {
|
|
1274
1406
|
adapter,
|
|
@@ -1280,17 +1412,17 @@ async function createGrokBuildAdapter(session, options = {}) {
|
|
|
1280
1412
|
}
|
|
1281
1413
|
|
|
1282
1414
|
// src/session.js
|
|
1283
|
-
function resolveGrokBin(env = process.env, exists =
|
|
1415
|
+
function resolveGrokBin(env = process.env, exists = existsSync3) {
|
|
1284
1416
|
if (typeof env.DSH_GROK_BIN === "string" && env.DSH_GROK_BIN.trim()) return env.DSH_GROK_BIN.trim();
|
|
1285
|
-
const local =
|
|
1417
|
+
const local = join3(grokHome(env), "bin", "grok");
|
|
1286
1418
|
if (exists(local)) return local;
|
|
1287
1419
|
return "grok";
|
|
1288
1420
|
}
|
|
1289
|
-
function grokCliAvailable(env = process.env, exists =
|
|
1421
|
+
function grokCliAvailable(env = process.env, exists = existsSync3) {
|
|
1290
1422
|
const bin = resolveGrokBin(env, exists);
|
|
1291
1423
|
if (bin.includes("/") || bin.includes("\\")) return exists(bin);
|
|
1292
1424
|
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
1293
|
-
return String(env.PATH ?? "").split(delimiter).some((dir) => dir && exists(
|
|
1425
|
+
return String(env.PATH ?? "").split(delimiter).some((dir) => dir && exists(join3(dir, bin)));
|
|
1294
1426
|
}
|
|
1295
1427
|
function spawnGrokLogin(options = {}) {
|
|
1296
1428
|
const spawnFn = options.spawn ?? spawn;
|
|
@@ -1310,6 +1442,52 @@ function spawnGrokLogin(options = {}) {
|
|
|
1310
1442
|
});
|
|
1311
1443
|
});
|
|
1312
1444
|
}
|
|
1445
|
+
function spawnGrokRefresh(options = {}) {
|
|
1446
|
+
const spawnFn = options.spawn ?? spawn;
|
|
1447
|
+
const bin = resolveGrokBin(options.env, options.exists);
|
|
1448
|
+
const timeoutMs = typeof options.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : CLI_REFRESH_TIMEOUT_MS;
|
|
1449
|
+
return new Promise((resolve, reject) => {
|
|
1450
|
+
let child;
|
|
1451
|
+
try {
|
|
1452
|
+
child = spawnFn(bin, ["models"], {
|
|
1453
|
+
stdio: options.stdio ?? "ignore",
|
|
1454
|
+
env: options.env ?? process.env
|
|
1455
|
+
});
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
reject(new Error(`Could not start grok CLI (${bin})`, { cause: error }));
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
let settled = false;
|
|
1461
|
+
const timer = setTimeout(() => {
|
|
1462
|
+
if (settled) return;
|
|
1463
|
+
settled = true;
|
|
1464
|
+
try {
|
|
1465
|
+
child.kill("SIGKILL");
|
|
1466
|
+
} catch {
|
|
1467
|
+
}
|
|
1468
|
+
resolve({ ok: false, reason: "timeout" });
|
|
1469
|
+
}, timeoutMs);
|
|
1470
|
+
child.on("error", (error) => {
|
|
1471
|
+
if (settled) return;
|
|
1472
|
+
settled = true;
|
|
1473
|
+
clearTimeout(timer);
|
|
1474
|
+
reject(new Error(`Could not run grok CLI (${bin})`, { cause: error }));
|
|
1475
|
+
});
|
|
1476
|
+
child.on("exit", (code) => {
|
|
1477
|
+
if (settled) return;
|
|
1478
|
+
settled = true;
|
|
1479
|
+
clearTimeout(timer);
|
|
1480
|
+
resolve({ ok: true, code });
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
async function defaultRenewSession(options = {}) {
|
|
1485
|
+
const env = options.env ?? process.env;
|
|
1486
|
+
if (!grokCliAvailable(env)) return void 0;
|
|
1487
|
+
await spawnGrokRefresh({ env });
|
|
1488
|
+
const parsed = (options.readAuth ?? readGrokAuthSession)();
|
|
1489
|
+
return parsed?.session;
|
|
1490
|
+
}
|
|
1313
1491
|
async function defaultCredentialRefOf() {
|
|
1314
1492
|
try {
|
|
1315
1493
|
const mod = await optionalImport("@deepseek-ai/dsh-credentials");
|
|
@@ -1344,12 +1522,15 @@ function createSessionService({
|
|
|
1344
1522
|
readAuth = readGrokAuthSession,
|
|
1345
1523
|
storeTokenTimeoutMs = STORE_TOKEN_TIMEOUT_MS,
|
|
1346
1524
|
credentialsIoTimeoutMs = CREDENTIALS_IO_TIMEOUT_MS,
|
|
1347
|
-
credentialRefOf = defaultCredentialRefOf
|
|
1525
|
+
credentialRefOf = defaultCredentialRefOf,
|
|
1526
|
+
renewSession = defaultRenewSession,
|
|
1527
|
+
now = () => Date.now()
|
|
1348
1528
|
} = {}) {
|
|
1349
1529
|
let catalog = { models: [], source: "signed-out", error: void 0 };
|
|
1350
1530
|
let lastPublic = publicSessionView(void 0);
|
|
1351
1531
|
let lastUsage = unavailable("Not fetched yet");
|
|
1352
1532
|
let memoryAccessToken;
|
|
1533
|
+
let memoryTokenExpiresAt;
|
|
1353
1534
|
let memorySessionTouched = false;
|
|
1354
1535
|
const resolveTimeoutMs = () => {
|
|
1355
1536
|
return typeof storeTokenTimeoutMs === "number" && storeTokenTimeoutMs > 0 ? storeTokenTimeoutMs : STORE_TOKEN_TIMEOUT_MS;
|
|
@@ -1357,6 +1538,48 @@ function createSessionService({
|
|
|
1357
1538
|
const resolveIoTimeoutMs = () => {
|
|
1358
1539
|
return typeof credentialsIoTimeoutMs === "number" && credentialsIoTimeoutMs > 0 ? credentialsIoTimeoutMs : CREDENTIALS_IO_TIMEOUT_MS;
|
|
1359
1540
|
};
|
|
1541
|
+
const epochMs = (value) => {
|
|
1542
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1543
|
+
if (typeof value === "string" && value.trim()) {
|
|
1544
|
+
const parsed = Date.parse(value.trim());
|
|
1545
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
1546
|
+
}
|
|
1547
|
+
return void 0;
|
|
1548
|
+
};
|
|
1549
|
+
const adoptSession = (session) => {
|
|
1550
|
+
if (!session?.accessToken) return void 0;
|
|
1551
|
+
memoryAccessToken = session.accessToken;
|
|
1552
|
+
memoryTokenExpiresAt = epochMs(session.expiresAt);
|
|
1553
|
+
memorySessionTouched = true;
|
|
1554
|
+
lastPublic = publicSessionView(session);
|
|
1555
|
+
return memoryAccessToken;
|
|
1556
|
+
};
|
|
1557
|
+
const memoryTokenNeedsRenewal = () => {
|
|
1558
|
+
if (typeof memoryAccessToken !== "string" || memoryAccessToken.length === 0) return false;
|
|
1559
|
+
if (typeof memoryTokenExpiresAt !== "number") return false;
|
|
1560
|
+
return memoryTokenExpiresAt - TOKEN_EXPIRY_SKEW_MS <= now();
|
|
1561
|
+
};
|
|
1562
|
+
let renewalInFlight;
|
|
1563
|
+
const renewAccessToken = async () => {
|
|
1564
|
+
if (renewalInFlight) return renewalInFlight;
|
|
1565
|
+
renewalInFlight = (async () => {
|
|
1566
|
+
try {
|
|
1567
|
+
const session = await renewSession();
|
|
1568
|
+
const token = adoptSession(session);
|
|
1569
|
+
if (token) notifyCatalogChange();
|
|
1570
|
+
return token;
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
logger?.warn?.(
|
|
1573
|
+
"Grok subscription session renewal failed: %s",
|
|
1574
|
+
error instanceof Error ? error.message : "unknown"
|
|
1575
|
+
);
|
|
1576
|
+
return void 0;
|
|
1577
|
+
} finally {
|
|
1578
|
+
renewalInFlight = void 0;
|
|
1579
|
+
}
|
|
1580
|
+
})();
|
|
1581
|
+
return renewalInFlight;
|
|
1582
|
+
};
|
|
1360
1583
|
const notifyCatalogChange = () => {
|
|
1361
1584
|
scheduleDeferred(() => {
|
|
1362
1585
|
try {
|
|
@@ -1371,7 +1594,9 @@ function createSessionService({
|
|
|
1371
1594
|
};
|
|
1372
1595
|
const readStoredToken = async () => {
|
|
1373
1596
|
if (typeof memoryAccessToken === "string" && memoryAccessToken.length > 0) {
|
|
1374
|
-
return memoryAccessToken;
|
|
1597
|
+
if (!memoryTokenNeedsRenewal()) return memoryAccessToken;
|
|
1598
|
+
const renewed = await renewAccessToken();
|
|
1599
|
+
return renewed ?? memoryAccessToken;
|
|
1375
1600
|
}
|
|
1376
1601
|
if (memorySessionTouched) return void 0;
|
|
1377
1602
|
if (!credentials?.resolve) return void 0;
|
|
@@ -1485,6 +1710,7 @@ function createSessionService({
|
|
|
1485
1710
|
}
|
|
1486
1711
|
const accessToken = parsed.session.accessToken;
|
|
1487
1712
|
memoryAccessToken = accessToken;
|
|
1713
|
+
memoryTokenExpiresAt = epochMs(parsed.session.expiresAt);
|
|
1488
1714
|
memorySessionTouched = true;
|
|
1489
1715
|
lastPublic = publicSessionView(parsed.session);
|
|
1490
1716
|
scheduleDeferred(() => {
|
|
@@ -1507,9 +1733,7 @@ function createSessionService({
|
|
|
1507
1733
|
try {
|
|
1508
1734
|
const parsed = readAuth();
|
|
1509
1735
|
if (parsed.session) {
|
|
1510
|
-
|
|
1511
|
-
memorySessionTouched = true;
|
|
1512
|
-
lastPublic = publicSessionView(parsed.session);
|
|
1736
|
+
adoptSession(parsed.session);
|
|
1513
1737
|
return { account: lastPublic, catalog, usage: lastUsage, cliAvailable: grokCliAvailable(), authPath: authJsonPath() };
|
|
1514
1738
|
}
|
|
1515
1739
|
} catch (error) {
|
|
@@ -1562,6 +1786,8 @@ function createSessionService({
|
|
|
1562
1786
|
login,
|
|
1563
1787
|
logout,
|
|
1564
1788
|
currentToken: readStoredToken,
|
|
1789
|
+
/** Force a CLI-backed renewal; used when the provider answers 401. */
|
|
1790
|
+
refreshToken: () => renewAccessToken(),
|
|
1565
1791
|
models: () => catalog.models,
|
|
1566
1792
|
catalog: () => catalog,
|
|
1567
1793
|
usage: () => lastUsage,
|
|
@@ -1861,8 +2087,15 @@ async function deferredBoot(ctx, session, notifyCatalogChange, options = {}) {
|
|
|
1861
2087
|
}
|
|
1862
2088
|
if (options.skipUpgrade) return;
|
|
1863
2089
|
const createAsync = options.createAsync ?? createGrokBuildAdapter;
|
|
2090
|
+
const resolveAttachments = () => {
|
|
2091
|
+
try {
|
|
2092
|
+
return typeof ctx.get === "function" ? ctx.get("attachments") : void 0;
|
|
2093
|
+
} catch {
|
|
2094
|
+
return void 0;
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
1864
2097
|
try {
|
|
1865
|
-
const created = await createAsync(session);
|
|
2098
|
+
const created = await createAsync(session, { ...options.adapterOptions, resolveAttachments });
|
|
1866
2099
|
if (created.kind === "pi-ai") {
|
|
1867
2100
|
if (created.note) ctx.logger?.warn?.(created.note);
|
|
1868
2101
|
ctx.llm.registerAdapter([PROVIDER_ID], created.adapter);
|