dsh-grok-subscription 1.0.5 → 1.0.7

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.5`.
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.7`.
29
29
 
30
30
  ```sh
31
31
  dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
@@ -43,6 +43,7 @@ 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 |
@@ -90,7 +91,7 @@ dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
90
91
  You can also install the version published on npm:
91
92
 
92
93
  ```sh
93
- dsh plugin --profile web add dsh-grok-subscription@1.0.5
94
+ dsh plugin --profile web add dsh-grok-subscription@1.0.7
94
95
  ```
95
96
 
96
97
  DSH handles target selection, the profile lock, dependency resolution, and bundle activation.
@@ -155,6 +156,16 @@ Restart DSH manually afterwards, then:
155
156
 
156
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.
157
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
+ **Both paths support it**: when the host's official `pi-ai` adapter is available it handles images, and otherwise the bundled fallback adapter reads the attachment bytes itself and inlines them as `input_image`. Image support therefore does not depend on resolving the host's packages. The **Active path** row in Settings shows which one is live and whether image input is available — check it first when a model rejects an image.
164
+
165
+ > If it says "bundled fallback" and images are rejected, `dsh web` most likely was not restarted after updating: the client UI refreshes from disk, but the host adapter is loaded at process start.
166
+
167
+ `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.
168
+
158
169
  ### Reasoning effort
159
170
 
160
171
  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.
package/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  ## 三步开始
27
27
 
28
- 1. **安装插件**:在终端运行下面的命令;指定候选版本时填写完整的 `包名@版本`,例如 `dsh-grok-subscription@1.0.5`。
28
+ 1. **安装插件**:在终端运行下面的命令;指定候选版本时填写完整的 `包名@版本`,例如 `dsh-grok-subscription@1.0.7`。
29
29
 
30
30
  ```sh
31
31
  dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
@@ -43,6 +43,7 @@
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 |
@@ -90,7 +91,7 @@ dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
90
91
  也可以按 npm 上的已发布版本安装:
91
92
 
92
93
  ```sh
93
- dsh plugin --profile web add dsh-grok-subscription@1.0.5
94
+ dsh plugin --profile web add dsh-grok-subscription@1.0.7
94
95
  ```
95
96
 
96
97
  目标选择、profile 锁、依赖解析和 bundle 激活均由 DSH 负责。
@@ -155,6 +156,16 @@ dsh --profile web --dump-config
155
156
 
156
157
  仅当当前会话的 provider 为 `grok-build` 且用量读取成功时显示徽章;悬停或点击可查看「每周额度 剩余 N% · 重置于 M/D HH:mm」。徽章只反映服务端返回的每周额度;读取失败时徽章不显示,聊天不受影响。
157
158
 
159
+ ### 图片输入
160
+
161
+ `grok-4.7`、`grok-4.7-fast` 与 `grok-4.6` 支持图片输入:在输入框粘贴或附加图片即可提问。图片由宿主的附件服务按像素与体积预算归一化后发送,超预算时降级为文本说明,而不是静默丢弃。
162
+
163
+ **两条通路都支持**:宿主官方 `pi-ai` 适配器可用时走官方实现,否则内置回退适配器自己读取附件字节并内联成 `input_image`。因此贴图能力不依赖宿主包的解析结果。设置页的「运行通路」会显示当前生效的是哪一条,以及图片输入是否可用 —— 遇到「当前模型不支持图片」时先看这一行。
164
+
165
+ > 若显示「内置回退」且贴图被拒,通常是 `dsh web` 更新后没有重启:客户端界面会随刷新更新,但主机端适配器是进程启动时加载的。
166
+
167
+ `grok-4.5` **不在支持列表内**。实测给它一张纯红图片并询问颜色,它回答 "green",还把黄色方块描述成「带黑边的圆」——它在编造而不是看图。插件因此不为它声明图片能力,避免输出看起来自信却完全错误的答案。
168
+
158
169
  ### 推理档位
159
170
 
160
171
  选择 Grok 模型后,模型菜单里会出现推理档位子菜单:`low` / `medium` / `high` / `xhigh`,默认 `high`。档位通过 `model.reasoning`(`efforts` + `defaultEffort`)元数据提供,因此与 Codex 在 DSH 中的交互一致;具体可用档位以账号模型目录为准。
package/lib/client.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/client.jsx
21
21
  var client_exports = {};
22
22
  __export(client_exports, {
23
+ DiagnosticsRows: () => DiagnosticsRows,
23
24
  GrokSubscriptionPanel: () => GrokSubscriptionPanel,
24
25
  GrokSubscriptionSection: () => GrokSubscriptionSection,
25
26
  RPC_CALL_TIMEOUT_MS: () => RPC_CALL_TIMEOUT_MS,
@@ -37,7 +38,7 @@ var import_react2 = require("react");
37
38
  var zh = {
38
39
  nav: "Grok \u8BA2\u9605",
39
40
  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.5",
41
+ subtitle: "\u7528 SuperGrok / X Premium\uFF08Grok Build\uFF09\u4F1A\u8BDD\uFF0C\u800C\u4E0D\u662F XAI_API_KEY\u3002\xB7 1.0.7",
41
42
  signedIn: "\u5DF2\u767B\u5F55",
42
43
  signedOut: "\u672A\u767B\u5F55",
43
44
  account: "\u8D26\u6237",
@@ -58,6 +59,13 @@ var zh = {
58
59
  loginHelp: "\u767B\u5F55\u65B9\u5F0F\u8BF4\u660E",
59
60
  refreshCatalog: "\u5237\u65B0\u6A21\u578B\u76EE\u5F55",
60
61
  renderError: "\u754C\u9762\u6E32\u67D3\u5931\u8D25",
62
+ diagnostics: "\u8FD0\u884C\u901A\u8DEF",
63
+ adapterPiAi: "\u5B98\u65B9 pi-ai",
64
+ adapterFallback: "\u5185\u7F6E\u56DE\u9000",
65
+ imageInput: "\u56FE\u7247\u8F93\u5165",
66
+ imageInputOn: "\u53EF\u7528",
67
+ imageInputOff: "\u4E0D\u53EF\u7528\uFF08\u672A\u89E3\u6790\u5230\u5BBF\u4E3B\u9644\u4EF6\u670D\u52A1\uFF09",
68
+ adapterHint: "\u82E5\u663E\u793A\u300C\u5185\u7F6E\u56DE\u9000\u300D\u4E14\u8D34\u56FE\u88AB\u62D2\uFF0C\u8BF7\u91CD\u542F dsh web \u8BA9\u65B0\u7248\u4E3B\u673A\u7AEF\u751F\u6548\u3002",
61
69
  busy: "\u5904\u7406\u4E2D\u2026",
62
70
  busyCatalog: "\u6B63\u5728\u5237\u65B0\u6A21\u578B\u76EE\u5F55\u2026",
63
71
  busyUsage: "\u6B63\u5728\u5237\u65B0\u7528\u91CF\u2026",
@@ -87,7 +95,7 @@ var zh = {
87
95
  var en = {
88
96
  nav: "Grok Subscription",
89
97
  title: "Grok Subscription",
90
- subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY. \xB7 1.0.5",
98
+ subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY. \xB7 1.0.7",
91
99
  signedIn: "Signed in",
92
100
  signedOut: "Signed out",
93
101
  account: "Account",
@@ -108,6 +116,13 @@ var en = {
108
116
  loginHelp: "How signing in works",
109
117
  refreshCatalog: "Refresh model catalog",
110
118
  renderError: "Failed to render this panel",
119
+ diagnostics: "Active path",
120
+ adapterPiAi: "official pi-ai",
121
+ adapterFallback: "bundled fallback",
122
+ imageInput: "Image input",
123
+ imageInputOn: "available",
124
+ imageInputOff: "unavailable (host attachment service not resolved)",
125
+ adapterHint: 'If this says "bundled fallback" and images are rejected, restart dsh web so the new host bundle loads.',
111
126
  busy: "Working\u2026",
112
127
  busyCatalog: "Refreshing model catalog\u2026",
113
128
  busyUsage: "Refreshing usage\u2026",
@@ -173,6 +188,11 @@ var MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
173
188
  var REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
174
189
  var REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
175
190
  var FALLBACK_MODEL_IDS = Object.freeze(["grok-4.7", "grok-4.6", "grok-4.5"]);
191
+ var IMAGE_INPUT_MODEL_IDS = Object.freeze([
192
+ "grok-4.7",
193
+ "grok-4.7-build-fast",
194
+ "grok-4.6"
195
+ ]);
176
196
 
177
197
  // src/client-settings-style.js
178
198
  var SETTINGS_STYLE = `
@@ -654,6 +674,19 @@ function Chip({ tone = "off", children }) {
654
674
  children
655
675
  ] });
656
676
  }
677
+ function DiagnosticsRows({ diagnostics, t }) {
678
+ if (!diagnostics) return null;
679
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsRows", children: [
680
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsRow", children: [
681
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("diagnostics") }),
682
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gsRowValue", children: diagnostics.adapter === "pi-ai" ? t("adapterPiAi") : t("adapterFallback") })
683
+ ] }),
684
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsRow", children: [
685
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("imageInput") }),
686
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gsRowValue", children: diagnostics.imageInput ? t("imageInputOn") : t("imageInputOff") })
687
+ ] })
688
+ ] });
689
+ }
657
690
  function UsagePanel({ usage, t, usageBusy, onRefresh, signedIn }) {
658
691
  const ok = usage?.status === "ok";
659
692
  const usedNumber = ok && Number.isFinite(usage.usedPercent) ? usage.usedPercent : void 0;
@@ -830,6 +863,8 @@ function GrokSubscriptionSection({ rpc, t }) {
830
863
  error
831
864
  ] }) : null,
832
865
  status?.cliAvailable === false ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsStatus gsStatus--warn", children: t("cliMissing") }) : null,
866
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DiagnosticsRows, { diagnostics: status?.diagnostics, t }),
867
+ status?.diagnostics && !status.diagnostics.imageInput ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsHint", children: t("adapterHint") }) : null,
833
868
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("details", { className: "gsDisclosure", children: [
834
869
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("summary", { children: [
835
870
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Chevron, {}),
package/lib/index.js CHANGED
@@ -29,14 +29,19 @@ var MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
29
29
  var REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
30
30
  var REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
31
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
+ ]);
32
37
  var IMPORT_TIMEOUT_MS = 2500;
33
38
  var CREDENTIALS_IO_TIMEOUT_MS = 1500;
34
39
  var RPC_HANDLER_TIMEOUT_MS = 8e3;
35
40
 
36
41
  // src/session.js
37
- import { existsSync as existsSync2 } from "node:fs";
42
+ import { existsSync as existsSync3 } from "node:fs";
38
43
  import { spawn } from "node:child_process";
39
- import { join as join2 } from "node:path";
44
+ import { join as join3 } from "node:path";
40
45
 
41
46
  // src/auth-file.js
42
47
  import { lstatSync, openSync, readFileSync, closeSync, constants as fsConstants } from "node:fs";
@@ -337,6 +342,9 @@ function mergeCatalog(live) {
337
342
  }
338
343
  return live.map((model) => ({ ...model, source: "live" }));
339
344
  }
345
+ function supportsImageInput(id) {
346
+ return typeof id === "string" && IMAGE_INPUT_MODEL_IDS.includes(id);
347
+ }
340
348
  function toPiModels(models) {
341
349
  return models.map((model) => {
342
350
  const efforts = model.reasoningEfforts ?? ["low", "medium", "high", "xhigh"];
@@ -357,7 +365,7 @@ function toPiModels(models) {
357
365
  baseUrl: PROXY_BASE_URL,
358
366
  reasoning: model.reasoning !== false,
359
367
  thinkingLevelMap,
360
- input: ["text"],
368
+ input: supportsImageInput(model.id) ? ["text", "image"] : ["text"],
361
369
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
362
370
  contextWindow: model.contextWindow ?? 5e5,
363
371
  maxTokens: model.maxTokens ?? 5e5,
@@ -381,7 +389,9 @@ function toLlmModels(models) {
381
389
  provider: PROVIDER_ID,
382
390
  id: model.id,
383
391
  name: model.name,
384
- inputModalities: ["text"]
392
+ // The bundled adapter inlines images through the host attachment store,
393
+ // so it advertises the same modality map as the official pi-ai path.
394
+ inputModalities: supportsImageInput(model.id) ? ["text", "image"] : ["text"]
385
395
  };
386
396
  const reasoning = reasoningInfoOf(model);
387
397
  if (reasoning) info.reasoning = reasoning;
@@ -711,6 +721,11 @@ async function fetchBillingUsage(accessToken, options = {}) {
711
721
  }
712
722
 
713
723
  // src/adapter.js
724
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
725
+ import { createRequire } from "node:module";
726
+ import { homedir as homedir2 } from "node:os";
727
+ import { dirname, join as join2 } from "node:path";
728
+ import { pathToFileURL } from "node:url";
714
729
  function withImportTimeout(promise, ms, message) {
715
730
  let timer;
716
731
  return Promise.race([
@@ -722,18 +737,132 @@ function withImportTimeout(promise, ms, message) {
722
737
  if (timer) clearTimeout(timer);
723
738
  });
724
739
  }
740
+ function hostModuleRoots(env = process.env, argv1 = process.argv[1]) {
741
+ const roots = [];
742
+ const push = (dir) => {
743
+ if (typeof dir === "string" && dir && !roots.includes(dir)) roots.push(dir);
744
+ };
745
+ const override = env.DSH_GROK_PEER_ROOT;
746
+ if (typeof override === "string" && override.trim()) push(override.trim());
747
+ if (typeof argv1 === "string" && argv1) {
748
+ let dir = dirname(argv1);
749
+ for (let depth = 0; depth < 6 && dir && dir !== dirname(dir); depth++) {
750
+ push(join2(dir, "node_modules"));
751
+ dir = dirname(dir);
752
+ }
753
+ }
754
+ const prefix = env.NPM_CONFIG_PREFIX;
755
+ if (typeof prefix === "string" && prefix.trim()) push(join2(prefix.trim(), "lib", "node_modules"));
756
+ if (typeof process.execPath === "string" && process.execPath) {
757
+ push(join2(dirname(process.execPath), "..", "lib", "node_modules"));
758
+ }
759
+ push(join2("/opt/homebrew", "lib", "node_modules"));
760
+ push(join2("/usr/local", "lib", "node_modules"));
761
+ for (const segment of ["Library/pnpm", ".local/share/pnpm", ".pnpm-global"]) {
762
+ push(join2(homedir2(), segment, "node_modules"));
763
+ }
764
+ if (typeof env.DSH_PORTABLE_HOME === "string" && env.DSH_PORTABLE_HOME.trim()) {
765
+ push(join2(env.DSH_PORTABLE_HOME.trim(), "node_modules"));
766
+ }
767
+ push(join2(homedir2(), ".local", "lib", "node_modules"));
768
+ const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME.trim() ? env.DSH_HOME.trim() : join2(homedir2(), ".dsh");
769
+ push(join2(dshHome, "profiles", "node_modules"));
770
+ return roots;
771
+ }
772
+ function splitSpecifier(specifier) {
773
+ const parts = specifier.split("/");
774
+ const take = specifier.startsWith("@") ? 2 : 1;
775
+ return {
776
+ name: parts.slice(0, take).join("/"),
777
+ subpath: parts.length > take ? `./${parts.slice(take).join("/")}` : "."
778
+ };
779
+ }
780
+ function pickExportTarget(value) {
781
+ if (typeof value === "string") return value;
782
+ if (!value || typeof value !== "object") return void 0;
783
+ for (const condition of ["import", "module", "default", "node"]) {
784
+ if (condition in value) {
785
+ const picked = pickExportTarget(value[condition]);
786
+ if (picked) return picked;
787
+ }
788
+ }
789
+ return void 0;
790
+ }
791
+ function resolveExportsTarget(exportsField, subpath) {
792
+ if (typeof exportsField === "string") return subpath === "." ? exportsField : void 0;
793
+ if (!exportsField || typeof exportsField !== "object") return void 0;
794
+ const keys = Object.keys(exportsField);
795
+ const isSubpathMap = keys.some((key) => key === "." || key.startsWith("./"));
796
+ if (!isSubpathMap) return subpath === "." ? pickExportTarget(exportsField) : void 0;
797
+ if (subpath in exportsField) return pickExportTarget(exportsField[subpath]);
798
+ for (const key of keys) {
799
+ const star = key.indexOf("*");
800
+ if (star === -1) continue;
801
+ const prefix = key.slice(0, star);
802
+ const suffix = key.slice(star + 1);
803
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue;
804
+ const matched = subpath.slice(prefix.length, subpath.length - suffix.length);
805
+ const target = pickExportTarget(exportsField[key]);
806
+ if (target) return target.replace("*", matched);
807
+ }
808
+ return void 0;
809
+ }
810
+ function resolveHostSpecifierManually(root, specifier) {
811
+ const { name: name2, subpath } = splitSpecifier(specifier);
812
+ const candidates = [
813
+ join2(root, name2),
814
+ join2(root, "node_modules", name2),
815
+ join2(root, "@deepseek-ai", "dsh", "node_modules", name2),
816
+ join2(root, "node_modules", "@deepseek-ai", "dsh", "node_modules", name2)
817
+ ];
818
+ for (const pkgDir of candidates) {
819
+ const manifest = join2(pkgDir, "package.json");
820
+ if (!existsSync2(manifest)) continue;
821
+ let pkg;
822
+ try {
823
+ pkg = JSON.parse(readFileSync3(manifest, "utf8"));
824
+ } catch {
825
+ continue;
826
+ }
827
+ const target = pkg.exports !== void 0 ? resolveExportsTarget(pkg.exports, subpath) : subpath === "." ? pkg.module ?? pkg.main ?? "index.js" : void 0;
828
+ if (typeof target !== "string" || !target) continue;
829
+ const file = join2(pkgDir, target);
830
+ if (existsSync2(file)) return file;
831
+ }
832
+ return void 0;
833
+ }
834
+ function resolveHostSpecifier(specifier, roots = hostModuleRoots()) {
835
+ for (const root of roots) {
836
+ try {
837
+ const resolved = createRequire(join2(root, "noop.js")).resolve(specifier);
838
+ if (typeof resolved === "string" && resolved) return resolved;
839
+ } catch {
840
+ }
841
+ const manual = resolveHostSpecifierManually(root, specifier);
842
+ if (manual) return manual;
843
+ }
844
+ return void 0;
845
+ }
725
846
  async function optionalImport(specifier, options = {}) {
726
847
  const timeoutMs = typeof options.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : IMPORT_TIMEOUT_MS;
727
- const importFn = typeof options.importFn === "function" ? options.importFn : (id) => import(id);
728
- try {
729
- return await withImportTimeout(
730
- Promise.resolve(importFn(specifier)),
731
- timeoutMs,
732
- `Import timed out after ${timeoutMs}ms: ${specifier}`
733
- );
734
- } catch {
735
- return void 0;
848
+ const injected = typeof options.importFn === "function" ? options.importFn : void 0;
849
+ const importFn = injected ?? ((id) => import(id));
850
+ const candidates = [specifier];
851
+ if (!injected && !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("file:")) {
852
+ const resolved = resolveHostSpecifier(specifier, options.hostRoots ?? hostModuleRoots());
853
+ if (resolved) candidates.push(pathToFileURL(resolved).href);
854
+ }
855
+ for (const candidate of candidates) {
856
+ try {
857
+ return await withImportTimeout(
858
+ Promise.resolve(importFn(candidate)),
859
+ timeoutMs,
860
+ `Import timed out after ${timeoutMs}ms: ${specifier}`
861
+ );
862
+ } catch {
863
+ }
736
864
  }
865
+ return void 0;
737
866
  }
738
867
  function textOf(content) {
739
868
  if (typeof content === "string") return content;
@@ -748,7 +877,66 @@ function textOf(content) {
748
877
  return "";
749
878
  }).join("");
750
879
  }
751
- function responsesInput(options) {
880
+ function imagePlaceholderText(ref) {
881
+ const name2 = typeof ref?.name === "string" && ref.name.trim() ? ` ${JSON.stringify(ref.name.trim())}` : "";
882
+ return `[attached image${name2} could not be included in this request]`;
883
+ }
884
+ async function collectImageParts(options, resolveAttachments, policy = {}) {
885
+ const parts = /* @__PURE__ */ new Map();
886
+ const refs = [];
887
+ for (const message of options.messages ?? []) {
888
+ for (const block of message?.content ?? []) {
889
+ if (block?.type !== "image" || !block.attachment) continue;
890
+ const id = block.attachment.attachmentId;
891
+ if (typeof id !== "string" || !id || parts.has(id)) continue;
892
+ parts.set(id, void 0);
893
+ refs.push(block.attachment);
894
+ }
895
+ }
896
+ if (refs.length === 0) return parts;
897
+ let store;
898
+ try {
899
+ store = resolveAttachments?.();
900
+ } catch {
901
+ store = void 0;
902
+ }
903
+ if (!store?.readImageRequest) return parts;
904
+ const request = {
905
+ maxPixels: policy.maxPixels ?? REQUEST_IMAGE_PIXEL_BUDGET,
906
+ maxBytes: policy.maxBytes ?? REQUEST_IMAGE_MAX_BYTES
907
+ };
908
+ await Promise.all(refs.map(async (ref) => {
909
+ try {
910
+ const version = await store.readImageRequest(ref, request);
911
+ if (!version?.data || typeof version.mediaType !== "string" || !version.mediaType) return;
912
+ parts.set(ref.attachmentId, {
913
+ mediaType: version.mediaType,
914
+ base64: Buffer.from(version.data).toString("base64")
915
+ });
916
+ } catch {
917
+ }
918
+ }));
919
+ return parts;
920
+ }
921
+ function imageContentParts(blocks, imageParts) {
922
+ const parts = [];
923
+ const text = textOf(blocks);
924
+ if (text) parts.push({ type: "input_text", text });
925
+ for (const block of blocks ?? []) {
926
+ if (block?.type !== "image" || !block.attachment) continue;
927
+ const resolved = imageParts?.get(block.attachment.attachmentId);
928
+ if (resolved) {
929
+ parts.push({
930
+ type: "input_image",
931
+ image_url: `data:${resolved.mediaType};base64,${resolved.base64}`
932
+ });
933
+ } else {
934
+ parts.push({ type: "input_text", text: imagePlaceholderText(block.attachment) });
935
+ }
936
+ }
937
+ return parts;
938
+ }
939
+ function responsesInput(options, imageParts) {
752
940
  const input = [];
753
941
  const system = typeof options.system === "string" && options.system ? options.system : void 0;
754
942
  if (system) input.push({ role: "system", content: system });
@@ -779,6 +967,12 @@ function responsesInput(options) {
779
967
  if (text2) input.push({ role: "assistant", content: text2 });
780
968
  continue;
781
969
  }
970
+ const hasImage = (message.content ?? []).some((block) => block?.type === "image");
971
+ if (role === "user" && hasImage) {
972
+ const parts = imageContentParts(message.content, imageParts);
973
+ if (parts.length > 0) input.push({ role, content: parts });
974
+ continue;
975
+ }
782
976
  const text = textOf(message.content);
783
977
  if (!text) continue;
784
978
  input.push({ role, content: text });
@@ -827,7 +1021,7 @@ async function* streamResponsesUnsafe(options, token) {
827
1021
  }
828
1022
  const body = {
829
1023
  model: options.model,
830
- input: responsesInput(options),
1024
+ input: responsesInput(options, await collectImageParts(options, options.resolveAttachments)),
831
1025
  stream: true
832
1026
  };
833
1027
  if (typeof options.maxTokens === "number") body.max_output_tokens = options.maxTokens;
@@ -1028,7 +1222,7 @@ function visiblePiModels(session) {
1028
1222
  if (session.publicAccount()?.signedIn !== true) return [];
1029
1223
  return toPiModels(session.models()).map((model) => model.provider === PROVIDER_ID ? model : { ...model, provider: PROVIDER_ID });
1030
1224
  }
1031
- function createDuckAdapter(session) {
1225
+ function createDuckAdapter(session, adapterOptions = {}) {
1032
1226
  const providerInfo = () => ({ id: PROVIDER_ID, name: DISPLAY_NAME });
1033
1227
  const list = () => {
1034
1228
  const signedIn = session.publicAccount()?.signedIn === true;
@@ -1070,6 +1264,7 @@ function createDuckAdapter(session) {
1070
1264
  yield { type: "finish", reason: { kind: "error", failure: { message: "Unknown provider", code: "NO_ADAPTER" } } };
1071
1265
  return;
1072
1266
  }
1267
+ const callOptions = typeof adapterOptions.resolveAttachments === "function" ? { ...options, resolveAttachments: adapterOptions.resolveAttachments } : options;
1073
1268
  let token = await session.currentToken();
1074
1269
  if (!token) {
1075
1270
  yield { type: "finish", reason: { kind: "error", failure: { message: "Grok subscription is not signed in", code: "MISSING_CREDENTIAL" } } };
@@ -1078,7 +1273,7 @@ function createDuckAdapter(session) {
1078
1273
  for (let attempt = 0; ; attempt++) {
1079
1274
  let emitted = false;
1080
1275
  try {
1081
- for await (const chunk of streamResponses(options, token)) {
1276
+ for await (const chunk of streamResponses(callOptions, token, callOptions.resolveAttachments)) {
1082
1277
  emitted = true;
1083
1278
  yield chunk;
1084
1279
  }
@@ -1177,7 +1372,7 @@ function wrapAsHostAdapter(candidate, dshLlm) {
1177
1372
  return new GrokBuildAdapter();
1178
1373
  }
1179
1374
  function createGrokBuildAdapterSync(session, options = {}) {
1180
- const duck = createDuckAdapter(session);
1375
+ const duck = createDuckAdapter(session, { resolveAttachments: options.resolveAttachments });
1181
1376
  const dshLlm = options.dshLlm;
1182
1377
  return {
1183
1378
  adapter: wrapAsHostAdapter(duck, dshLlm),
@@ -1192,7 +1387,7 @@ async function createGrokBuildAdapter(session, options = {}) {
1192
1387
  optionalImport("@deepseek-ai/dsh-llm-pi-ai", importOpts),
1193
1388
  optionalImport("@deepseek-ai/dsh-llm", importOpts)
1194
1389
  ]);
1195
- const duck = createDuckAdapter(session);
1390
+ const duck = createDuckAdapter(session, { resolveAttachments: options.resolveAttachments });
1196
1391
  const asHostAdapter = (candidate) => wrapAsHostAdapter(candidate, dshLlm);
1197
1392
  if (!piAi?.createProvider || !dshPi?.PiAiAdapter) {
1198
1393
  return {
@@ -1286,7 +1481,11 @@ async function createGrokBuildAdapter(session, options = {}) {
1286
1481
  env: async () => void 0,
1287
1482
  fileExists: async () => false
1288
1483
  })
1289
- })
1484
+ }),
1485
+ // The host's durable attachment store. Without it PiAiAdapter rejects any
1486
+ // request carrying an image ("requires the durable attachment service"),
1487
+ // and with it images are normalized to the configured budget for us.
1488
+ resolveAttachments: typeof options.resolveAttachments === "function" ? () => options.resolveAttachments() : void 0
1290
1489
  });
1291
1490
  return {
1292
1491
  adapter,
@@ -1298,17 +1497,17 @@ async function createGrokBuildAdapter(session, options = {}) {
1298
1497
  }
1299
1498
 
1300
1499
  // src/session.js
1301
- function resolveGrokBin(env = process.env, exists = existsSync2) {
1500
+ function resolveGrokBin(env = process.env, exists = existsSync3) {
1302
1501
  if (typeof env.DSH_GROK_BIN === "string" && env.DSH_GROK_BIN.trim()) return env.DSH_GROK_BIN.trim();
1303
- const local = join2(grokHome(env), "bin", "grok");
1502
+ const local = join3(grokHome(env), "bin", "grok");
1304
1503
  if (exists(local)) return local;
1305
1504
  return "grok";
1306
1505
  }
1307
- function grokCliAvailable(env = process.env, exists = existsSync2) {
1506
+ function grokCliAvailable(env = process.env, exists = existsSync3) {
1308
1507
  const bin = resolveGrokBin(env, exists);
1309
1508
  if (bin.includes("/") || bin.includes("\\")) return exists(bin);
1310
1509
  const delimiter = process.platform === "win32" ? ";" : ":";
1311
- return String(env.PATH ?? "").split(delimiter).some((dir) => dir && exists(join2(dir, bin)));
1510
+ return String(env.PATH ?? "").split(delimiter).some((dir) => dir && exists(join3(dir, bin)));
1312
1511
  }
1313
1512
  function spawnGrokLogin(options = {}) {
1314
1513
  const spawnFn = options.spawn ?? spawn;
@@ -1764,8 +1963,17 @@ function sanitizeUsage(usage) {
1764
1963
  }
1765
1964
  return out;
1766
1965
  }
1767
- async function dispatch(session, endpoint) {
1768
- if (endpoint === "status") return publicResult(stripSecrets(await session.status()));
1966
+ async function dispatch(session, endpoint, diagnostics) {
1967
+ if (endpoint === "status") {
1968
+ const status = await session.status();
1969
+ return publicResult(stripSecrets({
1970
+ ...status,
1971
+ // Which adapter is actually serving this route: the host's official
1972
+ // pi-ai implementation or the bundled fallback. Diagnosing "model does
1973
+ // not support images" starts here.
1974
+ diagnostics: typeof diagnostics === "function" ? diagnostics() : diagnostics
1975
+ }));
1976
+ }
1769
1977
  if (endpoint === "pull") return publicResult(stripSecrets(await session.pull()));
1770
1978
  if (endpoint === "logout") return publicResult(stripSecrets(await session.logout()));
1771
1979
  if (endpoint === "catalog/refresh") {
@@ -1788,7 +1996,7 @@ function createRpcHandler(session, options = {}) {
1788
1996
  return async function handle(endpoint, _payload, _signal) {
1789
1997
  try {
1790
1998
  return await withTimeout(
1791
- dispatch(session, endpoint),
1999
+ dispatch(session, endpoint, options.diagnostics),
1792
2000
  timeoutMs,
1793
2001
  `Grok subscription RPC "${endpoint}" timed out after ${timeoutMs}ms`,
1794
2002
  { unref: false }
@@ -1897,6 +2105,15 @@ function softService(ctx, key) {
1897
2105
  }
1898
2106
  return void 0;
1899
2107
  }
2108
+ function attachmentResolver(ctx) {
2109
+ return () => {
2110
+ try {
2111
+ return typeof ctx.get === "function" ? ctx.get("attachments") : void 0;
2112
+ } catch {
2113
+ return void 0;
2114
+ }
2115
+ };
2116
+ }
1900
2117
  function apply(ctx, options = {}) {
1901
2118
  let active = true;
1902
2119
  if (typeof ctx.effect === "function") {
@@ -1924,11 +2141,21 @@ function apply(ctx, options = {}) {
1924
2141
  logger: ctx.logger,
1925
2142
  onCatalogChange: notifyCatalogChange
1926
2143
  });
1927
- const handler = createRpcHandler(session);
2144
+ const adapterState = { kind: "custom-mvp", upgraded: false };
2145
+ const handler = createRpcHandler(session, {
2146
+ diagnostics: () => ({
2147
+ adapter: adapterState.upgraded ? "pi-ai" : "fallback",
2148
+ adapterKind: adapterState.kind,
2149
+ imageInput: typeof resolveAttachments()?.readImageRequest === "function",
2150
+ hostPeersResolved: adapterState.upgraded
2151
+ })
2152
+ });
2153
+ const resolveAttachments = attachmentResolver(ctx);
1928
2154
  const createSync = options.createSync ?? createGrokBuildAdapterSync;
1929
2155
  try {
1930
- const created = createSync(session);
2156
+ const created = createSync(session, { resolveAttachments });
1931
2157
  if (created.note) ctx.logger?.debug?.(created.note);
2158
+ adapterState.kind = created.kind;
1932
2159
  ctx.llm.registerAdapter([PROVIDER_ID], created.adapter);
1933
2160
  notifyCatalogChange();
1934
2161
  } catch (error) {
@@ -1939,14 +2166,14 @@ function apply(ctx, options = {}) {
1939
2166
  }
1940
2167
  const defer = options.defer ?? scheduleDeferred2;
1941
2168
  defer(() => {
1942
- void deferredBoot(ctx, session, notifyCatalogChange, options);
2169
+ void deferredBoot(ctx, session, notifyCatalogChange, options, adapterState);
1943
2170
  });
1944
2171
  ctx.inject(["connection"], (connectionContext) => connectionContext.effect(
1945
2172
  () => registerSubscriptionTransport(connectionContext.connection, handler),
1946
2173
  "grok-subscription: host-only account RPC"
1947
2174
  ));
1948
2175
  }
1949
- async function deferredBoot(ctx, session, notifyCatalogChange, options = {}) {
2176
+ async function deferredBoot(ctx, session, notifyCatalogChange, options = {}, adapterState) {
1950
2177
  if (!options.skipSettings) {
1951
2178
  await tryRegisterSettings(ctx);
1952
2179
  }
@@ -1974,10 +2201,14 @@ async function deferredBoot(ctx, session, notifyCatalogChange, options = {}) {
1974
2201
  if (options.skipUpgrade) return;
1975
2202
  const createAsync = options.createAsync ?? createGrokBuildAdapter;
1976
2203
  try {
1977
- const created = await createAsync(session);
2204
+ const created = await createAsync(session, { ...options.adapterOptions, resolveAttachments: attachmentResolver(ctx) });
1978
2205
  if (created.kind === "pi-ai") {
1979
2206
  if (created.note) ctx.logger?.warn?.(created.note);
1980
2207
  ctx.llm.registerAdapter([PROVIDER_ID], created.adapter);
2208
+ if (adapterState) {
2209
+ adapterState.kind = created.kind;
2210
+ adapterState.upgraded = true;
2211
+ }
1981
2212
  notifyCatalogChange?.();
1982
2213
  } else if (created.note) {
1983
2214
  ctx.logger?.debug?.(created.note);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-grok-subscription",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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",