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 +13 -2
- package/README.md +13 -2
- package/lib/client.js +37 -2
- package/lib/index.js +263 -32
- 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.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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
42
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
38
43
|
import { spawn } from "node:child_process";
|
|
39
|
-
import { join as
|
|
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
|
-
|
|
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
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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")
|
|
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
|
|
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);
|