@huanlin/dsh-plugin-interpreters 0.1.0

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.md ADDED
@@ -0,0 +1,69 @@
1
+ # dsh-interpreters
2
+
3
+ DSH 插件:暴露 `run_python` 和 `run_node` 两个模型可调用工具,通过 stdin 执行代码并返回 stdout/stderr/exit code。在设置页「插件配置」分区提供配置卡片,让用户设置 Python 和 Node.js 解释器的可执行文件路径,工具描述中会告知模型解释器位置。
4
+
5
+ ## 架构
6
+
7
+ - **工具**:`run_python` / `run_node`,通过 `spawn(executable, ['-'])` 执行代码,代码经 stdin 传入(无命令行长度限制)
8
+ - **设置持久化**:通过 `ctx.settings` 命名空间 `interpreters` 持久化到 `$DSH_HOME/settings.yaml`
9
+ - **动态 description**:工具的 `description` 在注册时根据配置计算,包含解释器路径;设置变更时通过 `bridge.onChange()` 自动重新注册
10
+ - **配置暴露(TypertRemoteService 模式)**:DSH 的 settings RPC 域(api-proxy)只向配置客户端提供白名单命名空间(`interpreters` 不在其中),所以浏览器通过 host 内置的 typertRemote `/api` RPC 通道读写 `interpreters` 命名空间——`InterpretersConfigGateway` 继承 `TypertRemoteService` 并用 `@Remote('get')` / `@Remote('set')` 标记方法,typertRemote 的 SRC discovery 自动 claim `/api/interpreters/get` 和 `/api/interpreters/set` 端点。gateway 在 host 进程内 in-process 调 `ctx.settings.update(ns, patch)`,绕过 wire-layer allowlist
11
+ - **客户端 bundle**:配置卡片通过 `settings.plugin.item` slot 注册(「插件配置」分区),卡片用 `connection.rpc.call('/api', 'interpreters/get'|'set', { args: {...} })` 读写,不依赖 `settingsScope`(因为 `interpreters` 不在白名单中);`connection/reset` 事件触发卡片重载
12
+
13
+ ## 开发
14
+
15
+ ```sh
16
+ pnpm install # 安装依赖(link: 指向 ~/.dsh/source/current/)
17
+ pnpm run typecheck # tsc --noEmit
18
+ pnpm test # vitest run
19
+ pnpm run build # tsdown + tsc(生成 lib/index.js, lib/client.js, lib/types/*.d.ts)
20
+ ```
21
+
22
+ ### 目录结构
23
+
24
+ ```
25
+ src/
26
+ ├── index.ts # Host 入口:name, inject, apply(实例化 bridge + gateway + 工具注册)
27
+ ├── config.ts # Config schema (schemastery), ResolvedConfig, resolveConfig
28
+ ├── settings.ts # installInterpretersSettings: 注册 namespace,返回 bridge (source() + onChange())
29
+ ├── gateway.ts # InterpretersConfigGateway: TypertRemoteService + @Remote('get'|'set')
30
+ ├── tools.ts # registerTools: 注册 run_python + run_node
31
+ ├── runner.ts # runCode: spawn + stdin + stdout/stderr 收集
32
+ └── client/
33
+ ├── index.ts # Client 入口:slots.inject('settings.plugin.item') + connection/reset
34
+ ├── store.ts # 卡片响应式 store(通过 connection.rpc 读写 /api/interpreters/*)
35
+ ├── InterpretersCard.tsx # 配置卡片组件
36
+ └── locales.ts # i18n (zh + en)
37
+ ```
38
+
39
+ ## 运行
40
+
41
+ ### 本地安装
42
+
43
+ ```sh
44
+ dsh plugin --profile web add "link:D:/Projects/deepseek-harness/dsh-interpreters"
45
+ ```
46
+
47
+ ### 配置
48
+
49
+ 默认配置(`cordis.patch.yml`):
50
+
51
+ ```yaml
52
+ pythonPath: 'python' # Python 可执行文件路径
53
+ nodePath: 'node' # Node.js 可执行文件路径
54
+ timeoutMs: 30000 # 执行超时(毫秒)
55
+ ```
56
+
57
+ 运行时通过设置页「插件配置」分区的卡片修改,持久化到 `$DSH_HOME/settings.yaml`。
58
+
59
+ ## 检查
60
+
61
+ ```sh
62
+ pnpm run typecheck && pnpm test && pnpm run build
63
+ ```
64
+
65
+ 验证 `lib/` 产物:
66
+ - `lib/index.js` — host bundle(ESM,external: `@deepseek-ai/dsh-typert-protocol`, `@deepseek-ai/dsh-settings`, `@deepseek-ai/dsh-tools`, `schemastery`)
67
+ - `lib/client.js` — client bundle(CJS,`window.__ModuleLoader__.load` 包裹,external: `react`)
68
+ - `lib/types/` — TypeScript 声明文件
69
+ - `cordis.patch.yml` — bundle 配置层
@@ -0,0 +1,11 @@
1
+ # dsh-interpreters bundle layer: inserts the plugin row.
2
+ # Loaded by `dsh plugin --profile <name> add link:./dsh-interpreters`.
3
+ # The `name` is the package entry; Loader resolves it from profile node_modules.
4
+ # Defaults: python/node from PATH, 30s timeout.
5
+ - insert:
6
+ - id: dsh-interpreters
7
+ name: '@huanlin/dsh-plugin-interpreters'
8
+ config:
9
+ pythonPath: 'python'
10
+ nodePath: 'node'
11
+ timeoutMs: 30000
package/lib/client.js ADDED
@@ -0,0 +1,457 @@
1
+ window.__ModuleLoader__.load({ id: "@huanlin/dsh-plugin-interpreters", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ "use strict";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
23
+
24
+ // src/client/index.ts
25
+ var index_exports = {};
26
+ __export(index_exports, {
27
+ apply: () => apply,
28
+ inject: () => inject
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+ var import_dsh_client_web_react = require("@deepseek-ai/dsh-client-web-react");
32
+
33
+ // src/client/InterpretersCard.tsx
34
+ var import_react = require("react");
35
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
36
+
37
+ // src/client/store.ts
38
+ var import_client = require("@deepseek-ai/dsh-client-runtime/client");
39
+ function initialState() {
40
+ return {
41
+ status: "idle",
42
+ loaded: false,
43
+ available: false,
44
+ writable: false,
45
+ draft: {},
46
+ dirty: false,
47
+ applyState: { kind: "idle" }
48
+ };
49
+ }
50
+ function formatNumber(value) {
51
+ return typeof value === "number" ? String(value) : "";
52
+ }
53
+ function formatText(value) {
54
+ return typeof value === "string" ? value : "";
55
+ }
56
+ var InterpretersCardController = class {
57
+ constructor() {
58
+ __publicField(this, "store");
59
+ /** True after the first successful load; gates `connection/reset` refreshes. */
60
+ __publicField(this, "loaded", false);
61
+ __publicField(this, "generation", 0);
62
+ __publicField(this, "staged", /* @__PURE__ */ new Map());
63
+ this.store = (0, import_client.createSnapshotStore)(initialState());
64
+ void this.load();
65
+ }
66
+ /**
67
+ * Read the resolved config from the Host HTTP route and publish it.
68
+ * @returns settlement after the read.
69
+ */
70
+ async load() {
71
+ const gen = ++this.generation;
72
+ this.store.update((s) => {
73
+ s.status = "loading";
74
+ });
75
+ let config;
76
+ try {
77
+ const response = await fetch("/interpreters/api/get", {
78
+ method: "POST",
79
+ headers: { "content-type": "application/json" },
80
+ body: "{}"
81
+ });
82
+ if (response.ok) {
83
+ const parsed = await response.json().catch(() => null);
84
+ if (parsed?.ok === true && parsed.value !== void 0) {
85
+ config = parsed.value.config;
86
+ }
87
+ }
88
+ } catch {
89
+ }
90
+ if (gen !== this.generation) return;
91
+ if (config === void 0) {
92
+ this.store.update((s) => {
93
+ s.status = "ready";
94
+ s.available = false;
95
+ s.writable = false;
96
+ });
97
+ return;
98
+ }
99
+ this.loaded = true;
100
+ this.staged.clear();
101
+ this.store.update((s) => {
102
+ s.status = "ready";
103
+ s.available = true;
104
+ s.writable = true;
105
+ s.draft = { ...config };
106
+ s.dirty = false;
107
+ s.applyState = { kind: "idle" };
108
+ });
109
+ }
110
+ /** Stage draft text for one field. */
111
+ edit(field, text) {
112
+ this.staged.set(field, text);
113
+ this.store.update((s) => {
114
+ s.draft = { ...s.draft, [field]: text };
115
+ s.dirty = true;
116
+ s.applyState = { kind: "idle" };
117
+ });
118
+ }
119
+ /** Drop every staged edit. */
120
+ discard() {
121
+ if (this.staged.size === 0) {
122
+ this.store.update((s) => {
123
+ s.applyState = { kind: "idle" };
124
+ });
125
+ return;
126
+ }
127
+ this.staged.clear();
128
+ void this.load();
129
+ }
130
+ /** Write every staged edit, then re-seed from what the Host accepted. */
131
+ save() {
132
+ void this.doSave();
133
+ }
134
+ async doSave() {
135
+ const gen = ++this.generation;
136
+ const patch = this.patchOf();
137
+ if (Object.keys(patch).length === 0) {
138
+ this.staged.clear();
139
+ this.store.update((s) => {
140
+ s.dirty = false;
141
+ s.applyState = { kind: "idle" };
142
+ });
143
+ return;
144
+ }
145
+ this.store.update((s) => {
146
+ s.applyState = { kind: "saving" };
147
+ });
148
+ try {
149
+ const response = await fetch("/interpreters/api/set", {
150
+ method: "POST",
151
+ headers: { "content-type": "application/json" },
152
+ body: JSON.stringify({ patch })
153
+ });
154
+ if (gen !== this.generation) return;
155
+ if (!response.ok) {
156
+ const parsed2 = await response.json().catch(() => null);
157
+ const message = parsed2?.error?.message ?? `HTTP ${response.status}`;
158
+ this.store.update((s) => {
159
+ s.applyState = { kind: "error", message };
160
+ });
161
+ return;
162
+ }
163
+ const parsed = await response.json().catch(() => null);
164
+ if (parsed?.ok !== true || parsed.value === void 0) {
165
+ const message = parsed?.error?.message ?? "unknown error";
166
+ this.store.update((s) => {
167
+ s.applyState = { kind: "error", message };
168
+ });
169
+ return;
170
+ }
171
+ const next = parsed.value.config;
172
+ this.staged.clear();
173
+ this.store.update((s) => {
174
+ s.draft = { ...next };
175
+ s.dirty = false;
176
+ s.applyState = { kind: "saved" };
177
+ });
178
+ } catch (error) {
179
+ if (gen !== this.generation) return;
180
+ this.store.update((s) => {
181
+ s.applyState = { kind: "error", message: error instanceof Error ? error.message : String(error) };
182
+ });
183
+ }
184
+ }
185
+ /** The staged edits as one patch (only changed fields). */
186
+ patchOf() {
187
+ const patch = {};
188
+ for (const [field, text] of this.staged) {
189
+ const value = parseField(field, text);
190
+ if (value === void 0) continue;
191
+ patch[field] = value;
192
+ }
193
+ return patch;
194
+ }
195
+ };
196
+ function parseField(field, text) {
197
+ const trimmed = text.trim();
198
+ if (trimmed === "") return "";
199
+ if (field === "timeoutMs") {
200
+ const parsed = Number(trimmed);
201
+ return Number.isFinite(parsed) ? parsed : void 0;
202
+ }
203
+ return trimmed;
204
+ }
205
+ function refreshIfLoaded(controller) {
206
+ if (controller.loaded) void controller.load();
207
+ }
208
+ var formatFieldText = formatText;
209
+ var formatFieldNumber = formatNumber;
210
+
211
+ var css = "._3Vx1tq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}._3Vx1tq_card:hover{border-color:var(--dsw-alias-label-dimmed)}._3Vx1tq_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}._3Vx1tq_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}._3Vx1tq_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}._3Vx1tq_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}._3Vx1tq_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}._3Vx1tq_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}._3Vx1tq_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}._3Vx1tq_chevronOpen{transform:rotate(180deg)}._3Vx1tq_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}._3Vx1tq_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}._3Vx1tq_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}._3Vx1tq_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}._3Vx1tq_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}._3Vx1tq_savedNotice{min-width:0;color:var(--dsw-alias-state-success-primary);flex:1;margin:0;font-size:12px;line-height:1.5}._3Vx1tq_discard,._3Vx1tq_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}._3Vx1tq_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}._3Vx1tq_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}._3Vx1tq_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}._3Vx1tq_discard:disabled,._3Vx1tq_save:disabled{opacity:.4;cursor:default}._3Vx1tq_discard:focus-visible,._3Vx1tq_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}._3Vx1tq_notice{color:var(--dsw-alias-state-warn-label);margin:12px 0 0;font-size:12px;line-height:18px}._3Vx1tq_form{flex-direction:column;gap:12px;padding:12px 0 0;display:flex}._3Vx1tq_field{flex-direction:column;gap:6px;display:flex}._3Vx1tq_fieldLabel{color:var(--dsw-alias-label-secondary);align-items:center;gap:10px;font-size:12px;font-weight:500;line-height:18px;display:inline-flex}._3Vx1tq_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px;line-height:22px}._3Vx1tq_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}._3Vx1tq_input::placeholder{color:var(--dsw-alias-label-dimmed)}._3Vx1tq_input:disabled{opacity:.6;cursor:default}._3Vx1tq_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}";
212
+ var tagId = "@huanlin/dsh-plugin-interpreters/InterpretersCard.module.css";
213
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
214
+ const tag = document.createElement("style");
215
+ tag.dataset.plugin = "@huanlin/dsh-plugin-interpreters";
216
+ tag.dataset.pluginCss = tagId;
217
+ tag.textContent = css;
218
+ document.head.appendChild(tag);
219
+ }
220
+ var InterpretersCard_module_css_default = { "body": "_3Vx1tq_body", "card": "_3Vx1tq_card", "cardOpen": "_3Vx1tq_cardOpen", "chevron": "_3Vx1tq_chevron", "chevronOpen": "_3Vx1tq_chevronOpen", "description": "_3Vx1tq_description", "discard": "_3Vx1tq_discard", "failed": "_3Vx1tq_failed", "field": "_3Vx1tq_field", "fieldLabel": "_3Vx1tq_fieldLabel", "footer": "_3Vx1tq_footer", "form": "_3Vx1tq_form", "headText": "_3Vx1tq_headText", "header": "_3Vx1tq_header", "hint": "_3Vx1tq_hint", "input": "_3Vx1tq_input", "name": "_3Vx1tq_name", "notice": "_3Vx1tq_notice", "pending": "_3Vx1tq_pending", "readOnly": "_3Vx1tq_readOnly", "save": "_3Vx1tq_save", "savedNotice": "_3Vx1tq_savedNotice" };
221
+
222
+ // src/client/InterpretersCard.tsx
223
+ var import_jsx_runtime = require("react/jsx-runtime");
224
+ function InterpretersCard(props) {
225
+ const { controller, useSnapshot, t } = props;
226
+ const state = useSnapshot((snapshot) => snapshot);
227
+ if (state.status === "idle") void controller.load();
228
+ const [userOpen, setUserOpen] = (0, import_react.useState)(false);
229
+ const degraded = state.status === "ready" && !state.available;
230
+ const open = userOpen || degraded;
231
+ const title = t("title");
232
+ const header = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
233
+ "button",
234
+ {
235
+ type: "button",
236
+ className: InterpretersCard_module_css_default.header,
237
+ "aria-expanded": open,
238
+ "aria-label": `${t(open ? "collapse" : "expand")}: ${title}`,
239
+ onClick: () => {
240
+ if (!degraded) setUserOpen(!userOpen);
241
+ },
242
+ children: [
243
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: InterpretersCard_module_css_default.headText, children: [
244
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: InterpretersCard_module_css_default.name, children: title }),
245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: InterpretersCard_module_css_default.description, children: t("intro") })
246
+ ] }),
247
+ state.dirty ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: InterpretersCard_module_css_default.pending, children: t("unsaved") }) : null,
248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
249
+ import_dsh_client_ui_primitives.IconChevronDownOutline14,
250
+ {
251
+ className: open ? `${InterpretersCard_module_css_default.chevron} ${InterpretersCard_module_css_default.chevronOpen}` : InterpretersCard_module_css_default.chevron
252
+ }
253
+ )
254
+ ]
255
+ }
256
+ );
257
+ let body;
258
+ if (degraded) {
259
+ body = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: InterpretersCard_module_css_default.body, children: [
260
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: InterpretersCard_module_css_default.notice, role: "status", children: t("namespaceUnavailable") }),
261
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: InterpretersCard_module_css_default.footer, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
262
+ "button",
263
+ {
264
+ type: "button",
265
+ className: InterpretersCard_module_css_default.discard,
266
+ onClick: () => {
267
+ void controller.load();
268
+ },
269
+ children: t("retry")
270
+ }
271
+ ) })
272
+ ] });
273
+ } else if (state.status === "ready") {
274
+ const { draft, writable, applyState } = state;
275
+ const saving = applyState.kind === "saving";
276
+ const busy = !writable || saving;
277
+ const saveDisabled = !state.dirty || saving || !writable;
278
+ const discardDisabled = !state.dirty || saving;
279
+ const errorText = applyState.kind === "error" ? applyState.message : void 0;
280
+ body = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: InterpretersCard_module_css_default.body, children: [
281
+ !writable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: InterpretersCard_module_css_default.readOnly, role: "status", children: t("readOnly") }) : null,
282
+ applyState.kind === "saved" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: InterpretersCard_module_css_default.savedNotice, role: "status", children: t("save") }) : null,
283
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: InterpretersCard_module_css_default.form, children: [
284
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
285
+ Field,
286
+ {
287
+ id: "plugin-config-interpreters-python",
288
+ label: t("pythonPath"),
289
+ hint: t("pythonHelp"),
290
+ text: formatFieldText(draft.pythonPath),
291
+ disabled: busy,
292
+ onEdit: (text) => {
293
+ controller.edit("pythonPath", text);
294
+ }
295
+ }
296
+ ),
297
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
298
+ Field,
299
+ {
300
+ id: "plugin-config-interpreters-node",
301
+ label: t("nodePath"),
302
+ hint: t("nodeHelp"),
303
+ text: formatFieldText(draft.nodePath),
304
+ disabled: busy,
305
+ onEdit: (text) => {
306
+ controller.edit("nodePath", text);
307
+ }
308
+ }
309
+ ),
310
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
311
+ Field,
312
+ {
313
+ id: "plugin-config-interpreters-timeout",
314
+ label: t("timeoutMs"),
315
+ hint: t("timeoutHelp"),
316
+ text: formatFieldNumber(draft.timeoutMs),
317
+ numeric: true,
318
+ disabled: busy,
319
+ onEdit: (text) => {
320
+ controller.edit("timeoutMs", text);
321
+ }
322
+ }
323
+ )
324
+ ] }),
325
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: InterpretersCard_module_css_default.footer, children: [
326
+ errorText === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: InterpretersCard_module_css_default.failed, role: "status", children: errorText }),
327
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
328
+ "button",
329
+ {
330
+ type: "button",
331
+ className: InterpretersCard_module_css_default.discard,
332
+ disabled: discardDisabled,
333
+ onClick: () => {
334
+ controller.discard();
335
+ },
336
+ children: t("discard")
337
+ }
338
+ ),
339
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
340
+ "button",
341
+ {
342
+ type: "button",
343
+ className: InterpretersCard_module_css_default.save,
344
+ disabled: saveDisabled,
345
+ onClick: () => {
346
+ controller.save();
347
+ },
348
+ children: t(saving ? "saving" : "save")
349
+ }
350
+ )
351
+ ] })
352
+ ] });
353
+ } else {
354
+ body = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: InterpretersCard_module_css_default.body });
355
+ }
356
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: open ? `${InterpretersCard_module_css_default.card} ${InterpretersCard_module_css_default.cardOpen}` : InterpretersCard_module_css_default.card, children: [
357
+ header,
358
+ open ? body : null
359
+ ] });
360
+ }
361
+ function Field(props) {
362
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: InterpretersCard_module_css_default.field, children: [
363
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: InterpretersCard_module_css_default.fieldLabel, htmlFor: props.id, children: props.label }),
364
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
365
+ "input",
366
+ {
367
+ id: props.id,
368
+ className: InterpretersCard_module_css_default.input,
369
+ type: props.numeric ? "number" : "text",
370
+ ...props.numeric ? { inputMode: "numeric" } : {},
371
+ value: props.text,
372
+ disabled: props.disabled,
373
+ onChange: (event) => {
374
+ props.onEdit(event.target.value);
375
+ }
376
+ }
377
+ ),
378
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: InterpretersCard_module_css_default.hint, children: props.hint })
379
+ ] });
380
+ }
381
+
382
+ // src/client/locales.ts
383
+ var NS = "interpreters";
384
+ var zh = {
385
+ title: "\u89E3\u91CA\u5668\u8DEF\u5F84",
386
+ intro: "\u914D\u7F6E run_python / run_node \u5DE5\u5177\u4F7F\u7528\u7684\u89E3\u91CA\u5668\u8DEF\u5F84\u3002",
387
+ pythonPath: "Python \u53EF\u6267\u884C\u6587\u4EF6\u8DEF\u5F84",
388
+ pythonHelp: "\u6A21\u578B\u901A\u8FC7 run_python \u5DE5\u5177\u8C03\u7528\u8BE5\u8DEF\u5F84\u6267\u884C Python \u4EE3\u7801\u3002\u7559\u7A7A\u4F7F\u7528\u7CFB\u7EDF\u9ED8\u8BA4 python\u3002",
389
+ nodePath: "Node.js \u53EF\u6267\u884C\u6587\u4EF6\u8DEF\u5F84",
390
+ nodeHelp: "\u6A21\u578B\u901A\u8FC7 run_node \u5DE5\u5177\u8C03\u7528\u8BE5\u8DEF\u5F84\u6267\u884C Node.js \u4EE3\u7801\u3002\u7559\u7A7A\u4F7F\u7528\u7CFB\u7EDF\u9ED8\u8BA4 node\u3002",
391
+ timeoutMs: "\u6267\u884C\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09",
392
+ timeoutHelp: "\u8D85\u8FC7\u8BE5\u65F6\u957F\u8FDB\u7A0B\u5C06\u88AB\u5F3A\u5236\u7EC8\u6B62\u3002",
393
+ save: "\u4FDD\u5B58",
394
+ saving: "\u4FDD\u5B58\u4E2D\u2026",
395
+ discard: "\u653E\u5F03\u4FEE\u6539",
396
+ unsaved: "\u672A\u4FDD\u5B58",
397
+ saveFailed: "\u672C\u90E8\u7F72\u6CA1\u6709\u63A5\u53D7\u8FD9\u4E9B\u503C\uFF0C\u5DF2\u4FDD\u7559\u4F9B\u4F60\u4FEE\u6539\u3002",
398
+ readOnly: "\u672C\u90E8\u7F72\u7684\u8BBE\u7F6E\u4E3A\u53EA\u8BFB\u3002",
399
+ expand: "\u5C55\u5F00\u8BBE\u7F6E",
400
+ collapse: "\u6536\u8D77\u8BBE\u7F6E",
401
+ namespaceUnavailable: "\u89E3\u91CA\u5668\u914D\u7F6E\u901A\u9053\u5F53\u524D\u4E0D\u53EF\u7528\u3002\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
402
+ retry: "\u91CD\u8BD5"
403
+ };
404
+ var en = {
405
+ title: "Interpreter paths",
406
+ intro: "Configure the interpreter executables used by the run_python / run_node tools.",
407
+ pythonPath: "Python executable path",
408
+ pythonHelp: "The model uses this path to execute Python code via the run_python tool. Leave empty to use the system default python.",
409
+ nodePath: "Node.js executable path",
410
+ nodeHelp: "The model uses this path to execute Node.js code via the run_node tool. Leave empty to use the system default node.",
411
+ timeoutMs: "Execution timeout (ms)",
412
+ timeoutHelp: "The process is killed after this duration.",
413
+ save: "Save",
414
+ saving: "Saving\u2026",
415
+ discard: "Discard",
416
+ unsaved: "Unsaved",
417
+ saveFailed: "The deployment did not accept these values; they were left for you to correct.",
418
+ readOnly: "This deployment stores settings read-only.",
419
+ expand: "Show settings",
420
+ collapse: "Hide settings",
421
+ namespaceUnavailable: "The interpreter configuration channel is unavailable. Please retry later.",
422
+ retry: "Retry"
423
+ };
424
+
425
+ // src/client/index.ts
426
+ var inject = ["slots", "locale", "connection"];
427
+ function apply(ctx) {
428
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-interpreters: dictionaries");
429
+ const controller = new InterpretersCardController();
430
+ const useSnapshot = (0, import_dsh_client_web_react.bindSnapshotSelector)(controller.store);
431
+ ctx.effect(() => {
432
+ let pending = false;
433
+ const refresh = () => {
434
+ if (pending) return;
435
+ pending = true;
436
+ queueMicrotask(() => {
437
+ pending = false;
438
+ refreshIfLoaded(controller);
439
+ });
440
+ };
441
+ const disposers = [ctx.on("connection/reset", refresh)];
442
+ return () => {
443
+ for (const dispose of disposers) dispose();
444
+ };
445
+ }, "dsh-interpreters: pushed invalidations");
446
+ ctx.slots.inject("settings.plugin.item", function* () {
447
+ yield ctx.slots.register({
448
+ name: "settings.plugin.item",
449
+ id: "dsh-interpreters",
450
+ order: 50,
451
+ // bash 0 / agent-loop 10 / web-search 20 / advisor 30 / interpreters 50
452
+ locale: NS,
453
+ inject: () => ({ controller, useSnapshot })
454
+ }, InterpretersCard);
455
+ });
456
+ }
457
+ return module.exports; } });
package/lib/config.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * config.ts — composition-layer schema, resolved config shape, and resolver.
3
+ *
4
+ * The composition `Config` (cordis.patch.yml) is the first-boot seed; the
5
+ * settings user layer composes on top of it at runtime. `resolveConfig`
6
+ * normalises any combination of partial source values (composition, user
7
+ * layer, or both) into a fully-populated {@link ResolvedConfig} the tool
8
+ * registration and gateway can consume.
9
+ *
10
+ * @module dsh-interpreters/config
11
+ */
12
+ import z from 'schemastery';
13
+ /** Schemastery schema for the composition entry and the `interpreters` settings namespace. */
14
+ export const Config = z.object({
15
+ pythonPath: z.string().default('python').description('Path to the Python interpreter executable.'),
16
+ nodePath: z.string().default('node').description('Path to the Node.js interpreter executable.'),
17
+ timeoutMs: z.number().default(30000).description('Maximum execution time in milliseconds before the process is killed.'),
18
+ });
19
+ /**
20
+ * Resolve config with fallbacks for missing / invalid values.
21
+ * @param config - raw config from cordis.yml or settings scope.
22
+ * @returns a fully-populated {@link ResolvedConfig}.
23
+ */
24
+ export function resolveConfig(config) {
25
+ const pythonPath = typeof config.pythonPath === 'string' && config.pythonPath !== '' ? config.pythonPath : 'python';
26
+ const nodePath = typeof config.nodePath === 'string' && config.nodePath !== '' ? config.nodePath : 'node';
27
+ const timeoutMs = typeof config.timeoutMs === 'number' && config.timeoutMs > 0 ? config.timeoutMs : 30000;
28
+ return { pythonPath, nodePath, timeoutMs };
29
+ }