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 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.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.4
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; the chat model list refreshes whenever sign-in state changes;
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.4`。
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.4
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.4",
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.4",
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 existsSync2 } from "node:fs";
42
+ import { existsSync as existsSync3 } from "node:fs";
36
43
  import { spawn } from "node:child_process";
37
- import { join as join2 } from "node:path";
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 importFn = typeof options.importFn === "function" ? options.importFn : (id) => import(id);
726
- try {
727
- return await withImportTimeout(
728
- Promise.resolve(importFn(specifier)),
729
- timeoutMs,
730
- `Import timed out after ${timeoutMs}ms: ${specifier}`
731
- );
732
- } catch {
733
- return void 0;
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
- const token = await session.currentToken();
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
- 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 } };
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 = existsSync2) {
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 = join2(grokHome(env), "bin", "grok");
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 = existsSync2) {
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(join2(dir, bin)));
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
- memoryAccessToken = parsed.session.accessToken;
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);
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.6",
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",