dsh-plugin-exe 0.1.1 → 0.2.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 CHANGED
@@ -61,6 +61,28 @@ exe trust <调用方地址>
61
61
  没有沙箱也没有确认;每次调用都是一次中继往返,能一条命令做完就别拆成十条;
62
62
  **状态为 `lost` 的调用可能已经执行过,不要盲目重试**。
63
63
 
64
+ ## 侧边栏页面
65
+
66
+ 装好后 dsh 的侧边栏会多一行 **eXe**(图标是一台设备的提示符),点开是一页:
67
+
68
+ - **本机调用方地址** — 这台 harness 签名用的那把公钥地址,大字显示、可复制,下面直接给出
69
+ `exe trust <地址>`,抄到任意设备上执行即可。钥匙来源与中继入口也在这一行。
70
+ - **本机作为设备** — 如果这台机器自己也跑着 `exe`(有 `config.toml`),显示它的地址,
71
+ 以及**谁能在这台机器上执行命令**:信任列表逐条可取消,也可以加新地址(要填两遍,防手滑)。
72
+ 设备每 2 秒热读配置,改完即生效,不用重启。
73
+ - **可访问的设备** — 这台 harness 记得的设备(`mcp.toml` 的 `[devices]`),可加可删。
74
+ 设备那侧还要把上面的调用方地址加进它的信任列表,两边都成立才通。
75
+
76
+ 页面读写的就是 `exe mcp` 和 `exe` 用的那两个文件(`~/.config/exe/mcp.toml`、
77
+ `~/.config/exe/config.toml`),所以 `exe status`、`exe trust` 与这个页面永远说同一件事。
78
+ 写入是规范化的 TOML 重写(和 CLI 的行为一致,行内注释会丢),旧文件留在 `<文件名>.bak`。
79
+
80
+ 技术上它是这个包的第二半:`package.json` 里的 `dsh.client` 声明 + `./client` 导出,
81
+ 由 dsh 的模块系统作为浏览器插件加载;页面数据来自插件在 harness 自己的 web server 上
82
+ 注册的两条只读/单写路由(`/exe/api/state`、`/exe/api/edit`)。这两条路由**只接受同源请求**
83
+ 且**只接受 JSON**,跨源写一律 403——它们写的是你的配置,不是随便一个网页能碰的东西。
84
+ 没有 web server 的 profile(headless/tui)不会注册路由,三个工具照常工作。
85
+
64
86
  ## 配置
65
87
 
66
88
  装好后零配置即可用。需要改的时候,在自己的 profile 的 `cordis.patch.yml` 里覆盖,
package/lib/client.js ADDED
@@ -0,0 +1,415 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-plugin-exe", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/client/index.tsx
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ PANEL_ID: () => PANEL_ID,
25
+ apply: () => apply,
26
+ inject: () => inject
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/client/icon.tsx
31
+ var import_jsx_runtime = require("react/jsx-runtime");
32
+ function ExeIcon(props) {
33
+ const size = props.size ?? 18;
34
+ const opacity = props.active === false ? 0.72 : 1;
35
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { opacity }, children: [
36
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "3", y: "4.5", width: "18", height: "15", rx: "3", stroke: "currentColor", strokeWidth: "1.6" }),
37
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
38
+ "path",
39
+ {
40
+ d: "M7.5 10l2.6 2.1-2.6 2.1M12.4 14.4h4.1",
41
+ stroke: "currentColor",
42
+ strokeWidth: "1.6",
43
+ strokeLinecap: "round",
44
+ strokeLinejoin: "round"
45
+ }
46
+ )
47
+ ] });
48
+ }
49
+
50
+ // src/client/panel.tsx
51
+ var import_react2 = require("react");
52
+
53
+ // src/client/view.tsx
54
+ var import_react = require("react");
55
+ var import_jsx_runtime2 = require("react/jsx-runtime");
56
+ var mono = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
57
+ var border = "1px solid color-mix(in srgb, currentColor 16%, transparent)";
58
+ var s = {
59
+ page: {
60
+ height: "100%",
61
+ overflowY: "auto",
62
+ padding: "24px 28px 48px",
63
+ fontSize: 13,
64
+ lineHeight: 1.55
65
+ },
66
+ header: {
67
+ display: "flex",
68
+ alignItems: "baseline",
69
+ gap: 12,
70
+ marginBottom: 4
71
+ },
72
+ title: { fontSize: 20, fontWeight: 600, margin: 0 },
73
+ subtitle: { opacity: 0.6 },
74
+ toolbar: { marginLeft: "auto", display: "flex", gap: 8 },
75
+ card: {
76
+ border,
77
+ borderRadius: 10,
78
+ padding: "16px 18px",
79
+ marginTop: 18,
80
+ display: "grid",
81
+ gap: 10
82
+ },
83
+ cardTitle: { fontSize: 13, fontWeight: 600, margin: 0, letterSpacing: "0.01em" },
84
+ hint: { opacity: 0.65, margin: 0 },
85
+ address: { fontFamily: mono, fontSize: 15, wordBreak: "break-all", letterSpacing: "0.01em" },
86
+ code: {
87
+ fontFamily: mono,
88
+ fontSize: 12.5,
89
+ background: "color-mix(in srgb, currentColor 7%, transparent)",
90
+ borderRadius: 6,
91
+ padding: "6px 8px",
92
+ overflowX: "auto",
93
+ whiteSpace: "pre"
94
+ },
95
+ row: {
96
+ display: "flex",
97
+ alignItems: "center",
98
+ gap: 10,
99
+ padding: "6px 0",
100
+ borderTop: "1px solid color-mix(in srgb, currentColor 10%, transparent)"
101
+ },
102
+ label: { minWidth: 96, fontWeight: 500 },
103
+ rowAddress: { fontFamily: mono, fontSize: 12, opacity: 0.85, wordBreak: "break-all", flex: 1 },
104
+ form: { display: "flex", flexWrap: "wrap", gap: 8, marginTop: 2 },
105
+ input: {
106
+ flex: "1 1 160px",
107
+ minWidth: 120,
108
+ padding: "6px 9px",
109
+ borderRadius: 6,
110
+ border,
111
+ background: "transparent",
112
+ color: "inherit",
113
+ font: "inherit"
114
+ },
115
+ button: {
116
+ padding: "5px 11px",
117
+ borderRadius: 6,
118
+ border,
119
+ background: "transparent",
120
+ color: "inherit",
121
+ font: "inherit",
122
+ cursor: "pointer"
123
+ },
124
+ primary: {
125
+ padding: "5px 12px",
126
+ borderRadius: 6,
127
+ border,
128
+ background: "color-mix(in srgb, currentColor 10%, transparent)",
129
+ color: "inherit",
130
+ font: "inherit",
131
+ cursor: "pointer"
132
+ },
133
+ danger: {
134
+ padding: "3px 9px",
135
+ borderRadius: 6,
136
+ border: "1px solid color-mix(in srgb, #d33 45%, transparent)",
137
+ background: "transparent",
138
+ color: "inherit",
139
+ font: "inherit",
140
+ cursor: "pointer"
141
+ },
142
+ banner: {
143
+ marginTop: 14,
144
+ border: "1px solid color-mix(in srgb, #d33 45%, transparent)",
145
+ borderRadius: 8,
146
+ padding: "9px 12px",
147
+ whiteSpace: "pre-wrap"
148
+ },
149
+ badge: { fontSize: 11, opacity: 0.6, fontFamily: mono }
150
+ };
151
+ function CopyButton({ value, label = "\u590D\u5236" }) {
152
+ const [copied, setCopied] = (0, import_react.useState)(false);
153
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
154
+ "button",
155
+ {
156
+ type: "button",
157
+ style: s.button,
158
+ onClick: () => {
159
+ void navigator.clipboard?.writeText(value).then(() => {
160
+ setCopied(true);
161
+ setTimeout(() => setCopied(false), 1500);
162
+ }).catch(() => void 0);
163
+ },
164
+ children: copied ? "\u5DF2\u590D\u5236" : label
165
+ }
166
+ );
167
+ }
168
+ function AddForm(props) {
169
+ const [first, setFirst] = (0, import_react.useState)("");
170
+ const [second, setSecond] = (0, import_react.useState)("");
171
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
172
+ "form",
173
+ {
174
+ style: s.form,
175
+ onSubmit: (event) => {
176
+ event.preventDefault();
177
+ if (first.trim().length === 0 || second.trim().length === 0) return;
178
+ props.onSubmit(first.trim(), second.trim());
179
+ setFirst("");
180
+ setSecond("");
181
+ },
182
+ children: [
183
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
184
+ "input",
185
+ {
186
+ style: s.input,
187
+ placeholder: props.first,
188
+ value: first,
189
+ disabled: props.busy,
190
+ onChange: (event) => setFirst(event.target.value),
191
+ "aria-label": props.first
192
+ }
193
+ ),
194
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
195
+ "input",
196
+ {
197
+ style: s.input,
198
+ placeholder: props.second,
199
+ value: second,
200
+ disabled: props.busy,
201
+ onChange: (event) => setSecond(event.target.value),
202
+ "aria-label": props.second
203
+ }
204
+ ),
205
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: s.primary, type: "submit", disabled: props.busy, children: props.action })
206
+ ]
207
+ }
208
+ );
209
+ }
210
+ function Address({ value }) {
211
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { style: s.address, children: value });
212
+ }
213
+ function CallerCard(props) {
214
+ const { caller } = props.state;
215
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { style: s.card, children: [
216
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { style: s.cardTitle, children: "\u672C\u673A\u8C03\u7528\u65B9\u5730\u5740" }),
217
+ caller.address === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
218
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: s.hint, children: "\u8FD8\u6CA1\u6709\u8C03\u7528\u65B9\u94A5\u5319\uFF0C\u5176\u5B83\u8BBE\u5907\u6682\u65F6\u65E0\u5904\u53EF\u4FE1\u4EFB\u3002" }),
219
+ caller.error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: s.banner, children: caller.error }),
220
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("pre", { style: s.code, children: "exe mcp init" })
221
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
222
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: s.hint, children: "\u628A\u8FD9\u884C\u5730\u5740\u52A0\u5230\u6BCF\u53F0\u8BBE\u5907\u4E0A\uFF08\u5728\u8BBE\u5907\u4E0A\u6267\u884C\uFF09\uFF0C\u5B83\u4EEC\u4FBF\u63A5\u53D7\u8FD9\u53F0 harness \u7684\u6307\u4EE4\u3002" }),
223
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: s.row, children: [
224
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Address, { value: caller.address }),
225
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CopyButton, { value: caller.address })
226
+ ] }),
227
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("pre", { style: s.code, children: `exe trust ${caller.address}` }),
228
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: s.badge, children: [
229
+ "\u94A5\u5319\u6765\u6E90\uFF1A",
230
+ caller.source ?? "\u2014",
231
+ " \xB7 \u4E2D\u7EE7\uFF1A",
232
+ caller.endpoints.join(" , ") || "\u2014"
233
+ ] })
234
+ ] })
235
+ ] });
236
+ }
237
+ function DeviceCard(props) {
238
+ const { device } = props.state;
239
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { style: s.card, children: [
240
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { style: s.cardTitle, children: "\u672C\u673A\u4F5C\u4E3A\u8BBE\u5907" }),
241
+ !device.available ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
242
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { style: s.hint, children: [
243
+ "\u8FD9\u53F0\u673A\u5668\u8FD8\u4E0D\u662F eXe \u8BBE\u5907\uFF08\u6CA1\u6709 ",
244
+ device.path,
245
+ "\uFF09\u3002"
246
+ ] }),
247
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("pre", { style: s.code, children: "exe" })
248
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
249
+ device.address !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
250
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: s.hint, children: "\u5B83\u7684\u5730\u5740\u2014\u2014\u5176\u5B83\u8C03\u7528\u65B9\u8981 reach \u5B83\uFF0C\u5C31\u586B\u8FD9\u4E32\u3002" }),
251
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: s.row, children: [
252
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Address, { value: device.address }),
253
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CopyButton, { value: device.address })
254
+ ] })
255
+ ] }),
256
+ device.error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: s.banner, children: device.error }),
257
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { style: s.hint, children: [
258
+ "\u4FE1\u4EFB\u7684\u8C03\u7528\u65B9\uFF08",
259
+ device.trust.length,
260
+ "\uFF09\uFF1A\u53EA\u6709\u8FD9\u91CC\u7684\u5730\u5740\u80FD\u5728\u8FD9\u53F0\u673A\u5668\u4E0A\u6267\u884C\u547D\u4EE4\u3002\u8BBE\u5907\u6BCF 2 \u79D2\u70ED\u8BFB\u914D\u7F6E\uFF0C\u6539\u5B8C\u5373\u751F\u6548\u3002"
261
+ ] }),
262
+ device.trust.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: s.badge, children: "\u8FD8\u6CA1\u6709\u4FE1\u4EFB\u4EFB\u4F55\u8C03\u7528\u65B9" }),
263
+ device.trust.map((address) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: s.row, children: [
264
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: s.rowAddress, children: address }),
265
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
266
+ "button",
267
+ {
268
+ type: "button",
269
+ style: s.danger,
270
+ disabled: props.busy,
271
+ onClick: () => props.onEdit({ op: "trust.remove", address }),
272
+ children: "\u53D6\u6D88\u4FE1\u4EFB"
273
+ }
274
+ )
275
+ ] }, address)),
276
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
277
+ AddForm,
278
+ {
279
+ busy: props.busy,
280
+ first: "\u8C03\u7528\u65B9\u5730\u5740\uFF08base58\uFF09",
281
+ second: "\u518D\u586B\u4E00\u6B21\u4EE5\u786E\u8BA4",
282
+ action: "\u4FE1\u4EFB",
283
+ onSubmit: (address, confirm) => {
284
+ if (address !== confirm) return;
285
+ props.onEdit({ op: "trust.add", address });
286
+ }
287
+ }
288
+ )
289
+ ] })
290
+ ] });
291
+ }
292
+ function DevicesCard(props) {
293
+ const { devices } = props.state;
294
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { style: s.card, children: [
295
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { style: s.cardTitle, children: "\u53EF\u8BBF\u95EE\u7684\u8BBE\u5907" }),
296
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: s.hint, children: "\u8FD9\u53F0 harness \u8BB0\u5F97\u7684\u8BBE\u5907\uFF08\u5199\u6210\u6807\u7B7E\uFF0C\u6A21\u578B\u8C03\u7528\u65F6\u7528\u5B83\uFF09\u3002\u8BBE\u5907\u90A3\u4FA7\u8FD8\u8981\u628A\u8FD9\u4E2A\u9875\u9762\u4E0A\u7684\u8C03\u7528\u65B9\u5730\u5740\u52A0\u8FDB\u5B83\u7684\u4FE1\u4EFB\u5217\u8868\u3002" }),
297
+ devices.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: s.badge, children: "\u8FD8\u6CA1\u6709\u914D\u7F6E\u8BBE\u5907" }),
298
+ devices.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: s.row, children: [
299
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: s.label, children: row.label }),
300
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: s.rowAddress, children: row.address }),
301
+ row.source === "config" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: s.badge, children: "\u6765\u81EA\u63D2\u4EF6\u914D\u7F6E" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
302
+ "button",
303
+ {
304
+ type: "button",
305
+ style: s.danger,
306
+ disabled: props.busy,
307
+ onClick: () => props.onEdit({ op: "device.remove", label: row.label }),
308
+ children: "\u79FB\u9664"
309
+ }
310
+ )
311
+ ] }, row.label)),
312
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
313
+ AddForm,
314
+ {
315
+ busy: props.busy,
316
+ first: "\u6807\u7B7E\uFF08\u5982 macmini\uFF09",
317
+ second: "\u8BBE\u5907\u5730\u5740\uFF08base58\uFF09",
318
+ action: "\u6DFB\u52A0",
319
+ onSubmit: (label, address) => props.onEdit({ op: "device.set", label, address })
320
+ }
321
+ )
322
+ ] });
323
+ }
324
+ function ExeView(props) {
325
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: s.page, children: [
326
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { style: s.header, children: [
327
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { style: s.title, children: "eXe" }),
328
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: s.subtitle, children: "\u8FD9\u53F0 harness \u7684\u94A5\u5319\uFF0C\u4E0E\u5B83\u591F\u5F97\u7740\u7684\u8BBE\u5907" }),
329
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: s.toolbar, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", style: s.button, onClick: props.onReload, disabled: props.busy, children: props.busy ? "\u5904\u7406\u4E2D\u2026" : "\u5237\u65B0" }) })
330
+ ] }),
331
+ props.error !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: s.banner, children: props.error }),
332
+ props.state === null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: s.hint, children: "\u8F7D\u5165\u4E2D\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
333
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CallerCard, { state: props.state, busy: props.busy }),
334
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DeviceCard, { state: props.state, busy: props.busy, onEdit: props.onEdit }),
335
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DevicesCard, { state: props.state, busy: props.busy, onEdit: props.onEdit })
336
+ ] })
337
+ ] });
338
+ }
339
+
340
+ // src/client/panel.tsx
341
+ var import_jsx_runtime3 = require("react/jsx-runtime");
342
+ var STATE_URL = "/exe/api/state";
343
+ var EDIT_URL = "/exe/api/edit";
344
+ function reason(error) {
345
+ return error instanceof Error ? error.message : String(error);
346
+ }
347
+ async function failure(response) {
348
+ try {
349
+ const body = await response.json();
350
+ if (typeof body.error === "string" && body.error.length > 0) return body.error;
351
+ } catch {
352
+ }
353
+ return `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${response.status}\uFF09`;
354
+ }
355
+ function ExePanel() {
356
+ const [state, setState] = (0, import_react2.useState)(null);
357
+ const [error, setError] = (0, import_react2.useState)(null);
358
+ const [busy, setBusy] = (0, import_react2.useState)(false);
359
+ const load = (0, import_react2.useCallback)(async () => {
360
+ try {
361
+ const response = await fetch(STATE_URL, { cache: "no-store", headers: { accept: "application/json" } });
362
+ if (!response.ok) throw new Error(await failure(response));
363
+ setState(await response.json());
364
+ setError(null);
365
+ } catch (loadError) {
366
+ setError(reason(loadError));
367
+ }
368
+ }, []);
369
+ (0, import_react2.useEffect)(() => {
370
+ void load();
371
+ }, [load]);
372
+ const edit = (0, import_react2.useCallback)((op) => {
373
+ void (async () => {
374
+ setBusy(true);
375
+ try {
376
+ const response = await fetch(EDIT_URL, {
377
+ method: "POST",
378
+ headers: { "content-type": "application/json", accept: "application/json" },
379
+ body: JSON.stringify(op)
380
+ });
381
+ if (!response.ok) throw new Error(await failure(response));
382
+ setState(await response.json());
383
+ setError(null);
384
+ } catch (editError) {
385
+ setError(reason(editError));
386
+ } finally {
387
+ setBusy(false);
388
+ }
389
+ })();
390
+ }, []);
391
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
392
+ ExeView,
393
+ {
394
+ state,
395
+ error,
396
+ busy,
397
+ onEdit: edit,
398
+ onReload: () => {
399
+ void load();
400
+ }
401
+ }
402
+ );
403
+ }
404
+
405
+ // src/client/index.tsx
406
+ var inject = ["slots"];
407
+ var PANEL_ID = "exe";
408
+ function apply(ctx) {
409
+ ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: PANEL_ID }, ExePanel));
410
+ ctx.slots.inject(
411
+ "sidebar.panellist",
412
+ () => ctx.slots.register({ name: "sidebar.panellist", id: PANEL_ID, order: 40, label: "eXe" }, ExeIcon)
413
+ );
414
+ }
415
+ return module.exports; } });
package/lib/index.js CHANGED
@@ -765,7 +765,190 @@ function parse(toml, options = {}) {
765
765
  }
766
766
 
767
767
  // ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/stringify.js
768
+ var BARE_KEY = /^[a-z0-9-_]+$/i;
768
769
  var HAS_WELLFORMED = !!"".isWellFormed;
770
+ function extendedTypeOf(obj) {
771
+ let type = typeof obj;
772
+ if (type === "object") {
773
+ if (Array.isArray(obj))
774
+ return "array";
775
+ if (typeof obj.getUTCDate === "function" && obj instanceof Date)
776
+ return "date";
777
+ if (globalThis.Temporal) {
778
+ if (obj.until) {
779
+ if (obj instanceof Temporal.ZonedDateTime)
780
+ return "temporal/tz+uc";
781
+ if (obj instanceof Temporal.PlainDateTime || obj instanceof Temporal.PlainDate)
782
+ return "temporal/uc";
783
+ if (obj instanceof Temporal.PlainTime || obj instanceof Temporal.Instant)
784
+ return "temporal";
785
+ if (obj instanceof Temporal.PlainYearMonth)
786
+ return "temporal/x";
787
+ } else if (obj.toPlainDate && obj instanceof Temporal.PlainMonthDay || obj.negated && obj instanceof Temporal.Duration) {
788
+ return "temporal/x";
789
+ }
790
+ }
791
+ }
792
+ return type;
793
+ }
794
+ function isArrayOfTables(obj) {
795
+ for (let i = 0; i < obj.length; i++) {
796
+ if (extendedTypeOf(obj[i]) !== "object")
797
+ return false;
798
+ }
799
+ return obj.length != 0;
800
+ }
801
+ function formatWellFormedStringUnchecked(s) {
802
+ return JSON.stringify(s).replaceAll("\x7F", "\\u007f");
803
+ }
804
+ function formatString(s) {
805
+ return formatWellFormedStringUnchecked(HAS_WELLFORMED ? s.toWellFormed() : s);
806
+ }
807
+ function formatKey(s) {
808
+ if (BARE_KEY.test(s))
809
+ return s;
810
+ if (HAS_WELLFORMED && !s.isWellFormed())
811
+ throw new RangeError("key contains illegal lone surrogates");
812
+ return formatWellFormedStringUnchecked(s);
813
+ }
814
+ function stringifyValue(val, type, depth, numberAsFloat, strictTemporal) {
815
+ if (depth === 0) {
816
+ throw new Error("Could not stringify the object: maximum object depth exceeded");
817
+ }
818
+ switch (type) {
819
+ // @ts-expect-error -- intentional fallthrough case
820
+ case "number":
821
+ if (isNaN(val))
822
+ return "nan";
823
+ if (val === Infinity)
824
+ return "inf";
825
+ if (val === -Infinity)
826
+ return "-inf";
827
+ if (Number.isInteger(val) && (numberAsFloat || !Number.isSafeInteger(val)))
828
+ return val.toFixed(1);
829
+ case "bigint":
830
+ case "boolean":
831
+ case "temporal":
832
+ return val.toString();
833
+ case "string":
834
+ return formatString(val);
835
+ case "date":
836
+ if (isNaN(val.getTime()))
837
+ throw new TypeError("cannot serialize invalid date");
838
+ return val.toISOString();
839
+ case "object":
840
+ return stringifyInlineTable(val, depth, numberAsFloat, strictTemporal);
841
+ case "array":
842
+ return stringifyArray(val, depth, numberAsFloat, strictTemporal);
843
+ // @ts-expect-error -- intentional fallthrough case
844
+ case "temporal/tz+uc":
845
+ if (strictTemporal) {
846
+ let tz = val.timeZoneId;
847
+ let tzc = tz.charCodeAt(0);
848
+ if (
849
+ // Classic offset
850
+ tzc !== 43 && tzc !== 45 && // Fast pre-check pass; see below for the actually accepted values
851
+ (tzc !== 85 && tzc !== 71 && tzc !== 90 && tzc !== 69 || // UTC and its aliases; Temporal implementations don't all canonicalise unfortunately
852
+ tz !== "UTC" && tz !== "UCT" && tz !== "Universal" && tz !== "Zulu" && // GMT is a TZ but it's for all intents and purposes equivalent to UTC. Safe to downgrade.
853
+ !tz.startsWith("GMT") && tz !== "Greenwich" && // Etc/* are all safe to downgrade to offset (either UTC, GMT, or offset)
854
+ !tz.startsWith("Etc/"))
855
+ ) {
856
+ throw new TypeError("Temporal objects with an IANA timezone are not allowed in Temporal strict mode");
857
+ }
858
+ }
859
+ case "temporal/uc":
860
+ if (strictTemporal && val.calendarId !== "iso8601")
861
+ throw new TypeError("Temporal objects with a non-default calendar are not allowed in Temporal strict mode");
862
+ return val.toString({
863
+ calendarName: "never",
864
+ timeZoneName: "never"
865
+ });
866
+ case "temporal/x":
867
+ throw new TypeError("Unsupported " + val[Symbol.toStringTag]);
868
+ }
869
+ }
870
+ function stringifyInlineTable(obj, depth, numberAsFloat, strictTemporal) {
871
+ let keys = Object.keys(obj);
872
+ if (keys.length === 0)
873
+ return "{}";
874
+ let res = "{ ";
875
+ for (let i = 0; i < keys.length; i++) {
876
+ let k = keys[i];
877
+ if (i)
878
+ res += ", ";
879
+ res += formatKey(k) + " = " + stringifyValue(obj[k], extendedTypeOf(obj[k]), depth - 1, numberAsFloat, strictTemporal);
880
+ }
881
+ return res + " }";
882
+ }
883
+ function stringifyArray(array, depth, numberAsFloat, strictTemporal) {
884
+ if (array.length === 0)
885
+ return "[]";
886
+ let res = "[ ";
887
+ for (let i = 0; i < array.length; i++) {
888
+ if (i)
889
+ res += ", ";
890
+ if (array[i] === null || array[i] === void 0) {
891
+ throw new TypeError("arrays cannot contain null or undefined values");
892
+ }
893
+ res += stringifyValue(array[i], extendedTypeOf(array[i]), depth - 1, numberAsFloat, strictTemporal);
894
+ }
895
+ return res + " ]";
896
+ }
897
+ function stringifyArrayTable(array, key, depth, numberAsFloat, strictTemporal) {
898
+ if (depth === 0) {
899
+ throw new Error("Could not stringify the object: maximum object depth exceeded");
900
+ }
901
+ let res = "";
902
+ for (let i = 0; i < array.length; i++) {
903
+ res += `${res && "\n"}[[${key}]]
904
+ `;
905
+ res += stringifyTable(0, array[i], key, depth, numberAsFloat, strictTemporal);
906
+ }
907
+ return res;
908
+ }
909
+ function stringifyTable(tableKey, obj, prefix, depth, numberAsFloat, strictTemporal) {
910
+ if (depth === 0) {
911
+ throw new Error("Could not stringify the object: maximum object depth exceeded");
912
+ }
913
+ let preamble = "";
914
+ let tables = "";
915
+ let keys = Object.keys(obj);
916
+ for (let i = 0; i < keys.length; i++) {
917
+ let k = keys[i];
918
+ if (obj[k] !== null && obj[k] !== void 0) {
919
+ let type = extendedTypeOf(obj[k]);
920
+ if (type === "symbol" || type === "function") {
921
+ throw new TypeError(`cannot serialize values of type '${type}'`);
922
+ }
923
+ let key = formatKey(k);
924
+ if (type === "array" && isArrayOfTables(obj[k])) {
925
+ tables += (tables && "\n") + stringifyArrayTable(obj[k], prefix ? `${prefix}.${key}` : key, depth - 1, numberAsFloat, strictTemporal);
926
+ } else if (type === "object") {
927
+ let tblKey = prefix ? `${prefix}.${key}` : key;
928
+ tables += (tables && "\n") + stringifyTable(tblKey, obj[k], tblKey, depth - 1, numberAsFloat, strictTemporal);
929
+ } else {
930
+ preamble += key;
931
+ preamble += " = ";
932
+ preamble += stringifyValue(obj[k], type, depth, numberAsFloat, strictTemporal);
933
+ preamble += "\n";
934
+ }
935
+ }
936
+ }
937
+ if (tableKey && (preamble || !tables))
938
+ preamble = preamble ? `[${tableKey}]
939
+ ${preamble}` : `[${tableKey}]`;
940
+ return preamble && tables ? `${preamble}
941
+ ${tables}` : preamble || tables;
942
+ }
943
+ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false, strictTemporal = false } = {}) {
944
+ if (extendedTypeOf(obj) !== "object") {
945
+ throw new TypeError("stringify can only be called with an object");
946
+ }
947
+ let str = stringifyTable(0, obj, "", maxDepth, numbersAsFloat, strictTemporal);
948
+ if (str[str.length - 1] !== "\n")
949
+ return str + "\n";
950
+ return str;
951
+ }
769
952
 
770
953
  // ../../node_modules/.pnpm/@noble+hashes@2.4.0/node_modules/@noble/hashes/_u64.js
771
954
  var U32_MASK64 = /* @__PURE__ */ (() => BigInt(2 ** 32 - 1))();
@@ -4322,12 +4505,18 @@ function openResult(input) {
4322
4505
 
4323
4506
  // src/config.ts
4324
4507
  var DEFAULT_ENDPOINTS = ["https://exe.ntnl.io", "https://exe.ntnl.top"];
4325
- function defaultConfigPath(env = process.env) {
4508
+ function configDir(env = process.env) {
4326
4509
  const override = env.EXE_CONFIG_DIR?.trim();
4327
- if (override) return join(override, "mcp.toml");
4328
- if (process.platform === "win32" && env.APPDATA?.trim()) return join(env.APPDATA.trim(), "exe", "mcp.toml");
4510
+ if (override) return override;
4511
+ if (process.platform === "win32" && env.APPDATA?.trim()) return join(env.APPDATA.trim(), "exe");
4329
4512
  const home = env.HOME?.trim() || env.USERPROFILE?.trim() || homedir();
4330
- return join(home, ".config", "exe", "mcp.toml");
4513
+ return join(home, ".config", "exe");
4514
+ }
4515
+ function defaultConfigPath(env = process.env) {
4516
+ return join(configDir(env), "mcp.toml");
4517
+ }
4518
+ function deviceConfigPath(env = process.env) {
4519
+ return join(configDir(env), "config.toml");
4331
4520
  }
4332
4521
  function splitEndpoints(value) {
4333
4522
  const entries = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
@@ -4628,6 +4817,233 @@ var Relay = class {
4628
4817
  }
4629
4818
  };
4630
4819
 
4820
+ // src/routes.ts
4821
+ var ROUTE_PREFIX = "/exe/api";
4822
+ var MAX_BODY_BYTES = 64 * 1024;
4823
+ function send(res, status, body) {
4824
+ const text2 = JSON.stringify(body);
4825
+ res.writeHead(status, {
4826
+ "content-type": "application/json; charset=utf-8",
4827
+ "content-length": Buffer.byteLength(text2),
4828
+ "cache-control": "no-store"
4829
+ });
4830
+ res.end(text2);
4831
+ }
4832
+ function sameOrigin(req) {
4833
+ const origin = req.headers.origin;
4834
+ if (origin === void 0) return true;
4835
+ const host = req.headers.host;
4836
+ if (host === void 0) return false;
4837
+ try {
4838
+ return new URL(origin).host === host;
4839
+ } catch {
4840
+ return false;
4841
+ }
4842
+ }
4843
+ async function readJson(req) {
4844
+ const chunks = [];
4845
+ let size = 0;
4846
+ for await (const chunk of req) {
4847
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4848
+ size += buffer.length;
4849
+ if (size > MAX_BODY_BYTES) throw new Error("request body is too large");
4850
+ chunks.push(buffer);
4851
+ }
4852
+ const text2 = Buffer.concat(chunks).toString("utf8").trim();
4853
+ if (text2.length === 0) throw new Error("empty request body");
4854
+ try {
4855
+ return JSON.parse(text2);
4856
+ } catch (error) {
4857
+ throw new Error(`body is not JSON: ${error instanceof Error ? error.message : String(error)}`);
4858
+ }
4859
+ }
4860
+ function registerExeRoutes(webServer, store) {
4861
+ const handler = async (req, res) => {
4862
+ try {
4863
+ if (!sameOrigin(req)) {
4864
+ send(res, 403, { error: "cross-origin request refused" });
4865
+ return;
4866
+ }
4867
+ const url = new URL(req.url ?? "/", "http://localhost");
4868
+ if (url.pathname === `${ROUTE_PREFIX}/state`) {
4869
+ if (req.method !== "GET") {
4870
+ send(res, 405, { error: "use GET" });
4871
+ return;
4872
+ }
4873
+ send(res, 200, store.state());
4874
+ return;
4875
+ }
4876
+ if (url.pathname === `${ROUTE_PREFIX}/edit`) {
4877
+ if (req.method !== "POST") {
4878
+ send(res, 405, { error: "use POST" });
4879
+ return;
4880
+ }
4881
+ if (!(req.headers["content-type"] ?? "").includes("application/json")) {
4882
+ send(res, 415, { error: "send application/json" });
4883
+ return;
4884
+ }
4885
+ const body = await readJson(req);
4886
+ if (typeof body !== "object" || body === null || typeof body.op !== "string") {
4887
+ send(res, 400, { error: "an edit names its `op`" });
4888
+ return;
4889
+ }
4890
+ send(res, 200, store.edit(body));
4891
+ return;
4892
+ }
4893
+ send(res, 404, { error: `no route for ${req.method} ${url.pathname}` });
4894
+ } catch (error) {
4895
+ send(res, 400, { error: error instanceof Error ? error.message : String(error) });
4896
+ }
4897
+ };
4898
+ return webServer.register({ kind: "prefix", path: ROUTE_PREFIX, handler });
4899
+ }
4900
+
4901
+ // src/state.ts
4902
+ import { copyFileSync, existsSync as existsSync2, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
4903
+ function reason2(error) {
4904
+ return error instanceof Error ? error.message : String(error);
4905
+ }
4906
+ function readToml(path) {
4907
+ if (!existsSync2(path)) return void 0;
4908
+ try {
4909
+ return parse(readFileSync2(path, "utf8"));
4910
+ } catch (error) {
4911
+ throw new Error(`${path} is not valid TOML: ${reason2(error)}`);
4912
+ }
4913
+ }
4914
+ function writeToml(path, value) {
4915
+ const text2 = stringify(value);
4916
+ const temporary = `${path}.tmp`;
4917
+ writeFileSync(temporary, text2, { mode: 384 });
4918
+ if (existsSync2(path)) copyFileSync(path, `${path}.bak`);
4919
+ renameSync(temporary, path);
4920
+ }
4921
+ function labelOf(value) {
4922
+ const label = typeof value === "string" ? value.trim() : "";
4923
+ if (label.length === 0 || label.length > 64 || /[\u0000-\u001f]/.test(label)) {
4924
+ throw new Error("a device label is 1 to 64 printable characters");
4925
+ }
4926
+ return label;
4927
+ }
4928
+ function addressOfText(value) {
4929
+ const address = typeof value === "string" ? value.trim() : "";
4930
+ if (!isAddress(address)) {
4931
+ throw new Error(`not an eXe address: ${JSON.stringify(address)}`);
4932
+ }
4933
+ return address;
4934
+ }
4935
+ function trustOf(file) {
4936
+ const trust = file?.trust;
4937
+ if (trust === void 0) return [];
4938
+ if (!Array.isArray(trust)) throw new Error("`trust` in the device config must be a list of addresses");
4939
+ return trust.filter((entry) => typeof entry === "string");
4940
+ }
4941
+ function createStateStore(options = {}, env = process.env) {
4942
+ const callerPath = options.configPath?.trim() || defaultConfigPath(env);
4943
+ const devicePath = deviceConfigPath(env);
4944
+ const inline = options.devices ?? {};
4945
+ const callerView = () => {
4946
+ const view = { endpoints: [], configPath: callerPath };
4947
+ try {
4948
+ const file = readToml(callerPath);
4949
+ const inlineSecret = options.secret?.trim();
4950
+ const secret = inlineSecret ?? (typeof file?.secret === "string" ? file.secret.trim() : void 0);
4951
+ if (secret === void 0 || secret.length === 0) {
4952
+ view.error = `no caller key yet: run \`exe mcp init\`, or set \`secret\` in this plugin's config`;
4953
+ return view;
4954
+ }
4955
+ view.address = addressOf(decodeSecret(secret));
4956
+ view.source = inlineSecret === void 0 ? callerPath : "plugin config";
4957
+ view.endpoints = splitEndpoints(options.endpoints).length > 0 ? splitEndpoints(options.endpoints) : splitEndpoints(file?.endpoints).length > 0 ? splitEndpoints(file?.endpoints) : splitEndpoints(file?.cloud);
4958
+ } catch (error) {
4959
+ view.error = reason2(error);
4960
+ }
4961
+ return view;
4962
+ };
4963
+ const devicesView = () => {
4964
+ const rows = [];
4965
+ try {
4966
+ const table = readToml(callerPath)?.devices;
4967
+ if (table !== void 0 && (typeof table !== "object" || table === null || Array.isArray(table))) {
4968
+ throw new Error("`devices` in the caller config must be a table of label = address");
4969
+ }
4970
+ for (const [label, address] of Object.entries(table ?? {})) {
4971
+ if (typeof address === "string") rows.push({ label, address, source: "file" });
4972
+ }
4973
+ } catch {
4974
+ }
4975
+ for (const [label, address] of Object.entries(inline)) {
4976
+ if (rows.some((row) => row.label === label)) continue;
4977
+ rows.push({ label, address, source: "config" });
4978
+ }
4979
+ return rows.sort((left, right) => left.label.localeCompare(right.label));
4980
+ };
4981
+ const deviceView = () => {
4982
+ const view = { available: false, path: devicePath, endpoints: [], trust: [] };
4983
+ if (!existsSync2(devicePath)) return view;
4984
+ view.available = true;
4985
+ try {
4986
+ const file = readToml(devicePath);
4987
+ const secret = typeof file?.secret === "string" ? file.secret.trim() : "";
4988
+ if (secret.length === 0) throw new Error(`${devicePath} has no secret`);
4989
+ view.address = addressOf(decodeSecret(secret));
4990
+ view.endpoints = splitEndpoints(file?.endpoints).length > 0 ? splitEndpoints(file?.endpoints) : splitEndpoints(file?.cloud);
4991
+ view.trust = trustOf(file);
4992
+ } catch (error) {
4993
+ view.error = reason2(error);
4994
+ }
4995
+ return view;
4996
+ };
4997
+ const state = () => ({ caller: callerView(), devices: devicesView(), device: deviceView() });
4998
+ const edit = (op) => {
4999
+ switch (op.op) {
5000
+ case "device.set": {
5001
+ const label = labelOf(op.label);
5002
+ const address = addressOfText(op.address);
5003
+ if (Object.hasOwn(inline, label)) {
5004
+ throw new Error(`\`${label}\` is set in this plugin's own config; change it there, not in ${callerPath}`);
5005
+ }
5006
+ const file = readToml(callerPath) ?? {};
5007
+ const table = file.devices;
5008
+ const devices = typeof table === "object" && table !== null && !Array.isArray(table) ? { ...table } : {};
5009
+ devices[label] = address;
5010
+ writeToml(callerPath, { ...file, devices });
5011
+ break;
5012
+ }
5013
+ case "device.remove": {
5014
+ const label = labelOf(op.label);
5015
+ if (Object.hasOwn(inline, label)) {
5016
+ throw new Error(`\`${label}\` is set in this plugin's own config; remove it there, not in ${callerPath}`);
5017
+ }
5018
+ const file = readToml(callerPath);
5019
+ if (file === void 0) throw new Error(`no caller config at ${callerPath}`);
5020
+ const devices = { ...file.devices ?? {} };
5021
+ delete devices[label];
5022
+ writeToml(callerPath, { ...file, devices });
5023
+ break;
5024
+ }
5025
+ case "trust.add":
5026
+ case "trust.remove": {
5027
+ const address = addressOfText(op.address);
5028
+ const file = readToml(devicePath);
5029
+ if (file === void 0) {
5030
+ throw new Error(`this machine is not an eXe device: ${devicePath} does not exist (run \`exe\`)`);
5031
+ }
5032
+ const trust = trustOf(file);
5033
+ const next = op.op === "trust.add" ? [.../* @__PURE__ */ new Set([...trust, address])] : trust.filter((entry) => entry !== address);
5034
+ writeToml(devicePath, { ...file, trust: next });
5035
+ break;
5036
+ }
5037
+ default: {
5038
+ const never = op;
5039
+ throw new Error(`unknown edit ${JSON.stringify(never)}`);
5040
+ }
5041
+ }
5042
+ return state();
5043
+ };
5044
+ return { state, edit };
5045
+ }
5046
+
4631
5047
  // src/answers.ts
4632
5048
  function whole(outcome) {
4633
5049
  if (outcome.attachment !== void 0) {
@@ -4728,7 +5144,7 @@ var Config = Schema.object({
4728
5144
  function shQuote(value) {
4729
5145
  return `'${value.replaceAll("'", `'\\''`)}'`;
4730
5146
  }
4731
- function reason2(error) {
5147
+ function reason3(error) {
4732
5148
  return error instanceof Error ? error.message : String(error);
4733
5149
  }
4734
5150
  function mediaTypeOf(bytes, path, allowed) {
@@ -4823,7 +5239,7 @@ function apply(ctx, config) {
4823
5239
  if (status.last_seen_at !== void 0) row.last_seen_at = status.last_seen_at;
4824
5240
  return row;
4825
5241
  } catch (error) {
4826
- return { device: label, address, error: reason2(error) };
5242
+ return { device: label, address, error: reason3(error) };
4827
5243
  }
4828
5244
  })
4829
5245
  );
@@ -4975,6 +5391,11 @@ function apply(ctx, config) {
4975
5391
  }
4976
5392
  })
4977
5393
  );
5394
+ ctx.inject(["webServer"], (scoped) => {
5395
+ const webServer = scoped.webServer;
5396
+ const store = createStateStore(config);
5397
+ scoped.effect(() => registerExeRoutes(webServer, store), "dsh-plugin-exe: panel routes");
5398
+ });
4978
5399
  }
4979
5400
  export {
4980
5401
  Config,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-plugin-exe",
3
- "version": "0.1.1",
4
- "description": "Operate your own devices from DeepSeek Harness through eXe: a shell on remote machines, screenshots as images",
3
+ "version": "0.2.0",
4
+ "description": "eXe for DeepSeek Harness: shell and screenshots on your own devices, plus a sidebar page that carries the caller key other devices must trust",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "dsh-plugin",
@@ -18,16 +18,22 @@
18
18
  "directory": "plugins/dsh"
19
19
  },
20
20
  "exports": {
21
- ".": "./lib/index.js"
21
+ ".": "./lib/index.js",
22
+ "./client": "./lib/client.js"
22
23
  },
23
24
  "files": [
24
25
  "lib/index.js",
26
+ "lib/client.js",
25
27
  "cordis.patch.yml",
26
28
  "README.md"
27
29
  ],
28
30
  "dsh": {
29
31
  "bundle": {
30
32
  "patch": "./cordis.patch.yml"
33
+ },
34
+ "client": {
35
+ "platform": "web",
36
+ "inject": []
31
37
  }
32
38
  },
33
39
  "engines": {
@@ -35,7 +41,7 @@
35
41
  },
36
42
  "scripts": {
37
43
  "build": "node scripts/build.mjs",
38
- "typecheck": "tsc --noEmit -p tsconfig.json",
44
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
39
45
  "test": "node --experimental-strip-types --test tests/*.test.ts",
40
46
  "check": "pnpm build && pnpm typecheck && pnpm test"
41
47
  },
@@ -58,7 +64,11 @@
58
64
  "@noble/curves": "^2.4.0",
59
65
  "@noble/hashes": "^2.4.0",
60
66
  "@types/node": "^24.0.0",
67
+ "@types/react": "^18.3.0",
68
+ "@types/react-dom": "^18.3.0",
61
69
  "esbuild": "^0.28.0",
70
+ "react": "^18.3.1",
71
+ "react-dom": "^18.3.1",
62
72
  "smol-toml": "^1.9.1",
63
73
  "typescript": "^7.0.2"
64
74
  }