dsh-moli 1.1.14 → 1.1.15

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
@@ -311,10 +311,13 @@ capabilities always visible to the model.
311
311
  2. **Binary output is copied byte for byte.** PowerShell's `>` operator re-encodes a native
312
312
  program's output as UTF-16, inserting a `00` after every byte, which corrupts a screenshot.
313
313
  Bytes here are written verbatim, so the PNG signature stays `89504e470d0a1a0a`.
314
- 3. **No external dependencies.** A profile plugin is loaded from its real path on disk (a `link:`
314
+ 3. **One optional external dependency.** A profile plugin is loaded from its real path on disk (a `link:`
315
315
  dependency stays a symlink target), so a bare specifier such as `@deepseek-ai/schemastery`
316
- resolves against the plugin directory instead of the profile's `node_modules` and the plugin
317
- fails to load. This plugin imports only `node:` builtins and its own files.
316
+ resolves against the plugin directory instead of the profile's `node_modules`. The import is
317
+ therefore guarded: without schemastery (a `link:` install) the settings form does not appear and
318
+ the plugin degrades to plain patch-layer configuration, while `moli_fetch` and `moli_probe` keep
319
+ registering. With it (a normal npm install, which brings the dependency along) the settings page
320
+ is available.
318
321
 
319
322
  ## Platform support
320
323
 
@@ -358,6 +361,8 @@ exercised the proxy download route as well as the engine paths.
358
361
  ## Configuration
359
362
 
360
363
  Override any field under the plugin's `config` entry in the profile patch layer. All are optional.
364
+ The same fields are also editable from the **Moli 安装与抓取** settings section on the plugin's page in
365
+ the Plugins manager (a `plugins.bundle.config` form registered by the browser half).
361
366
 
362
367
  ```yaml
363
368
  - insert:
@@ -379,6 +384,22 @@ Without `exePath`, the plugin probes `exePath` → `MOLI_BIN` → `%LOCALAPPDATA
379
384
  `%USERPROFILE%\.local\bin\moli.exe`, then scans inside `%LOCALAPPDATA%\Moli` for a `moli.exe`, so an
380
385
  installation under a versioned subdirectory is still found.
381
386
 
387
+ ### Engine lifecycle from the settings page
388
+
389
+ The settings section also carries an **engine** row that detects the installed engine and lets you
390
+ install or uninstall it without touching the profile:
391
+
392
+ - **state** — `engineState` is `installed` (with `engineVersion`), `missing`, or a short error text.
393
+ - **install** — the form writes `engineAction: "install"`; the host downloads and runs the official
394
+ installer asynchronously, then reports the outcome back through `engineState` / `engineVersion`.
395
+ - **uninstall** — `engineAction: "uninstall"` removes the engine. On Windows only the installer-owned
396
+ `%LOCALAPPDATA%\Moli` tree is deleted; without one, nothing is touched. On POSIX the located binary
397
+ is removed and the now-empty install directories are pruned.
398
+
399
+ These three fields (`engineAction`, `engineState`, `engineVersion`) are part of the live Config schema;
400
+ `engineAction` is a `none`/`install`/`uninstall` union. They are meant to be driven by the settings
401
+ form, so prefer editing the four fields above for routine configuration.
402
+
382
403
  ## Known limitations
383
404
 
384
405
  - **macOS is untested, and Linux was tested by the contributor rather than the maintainer.** The
@@ -416,8 +437,14 @@ installation under a versioned subdirectory is still found.
416
437
 
417
438
  ## Verification
418
439
 
419
- Four suites live in [`verify/`](verify/), and all are runnable against a working tree:
440
+ Eight suites live in [`verify/`](verify/), and all are runnable against a working tree:
420
441
 
442
+ - `config-verify.mjs` checks the settings surface and engine lifecycle added by the plugin's browser
443
+ half: the volatile `Config` schema (every declared field, `engineAction` as a `none`/`install`/
444
+ `uninstall` union), `apply` reading both a plain config and a volatile `.get()` reference, the
445
+ `settings/document-updated` wiring, `uninstallMoli` (platform-aware: POSIX removes the located
446
+ binary, Windows removes only the installer-owned `%LOCALAPPDATA%\Moli` tree), and the client.js
447
+ shape (`plugins.bundle.config` registration, zh/en dictionaries, raw `true`/`false` select labels).
421
448
  - `schema-verify.mjs` checks the tool definitions with the registry's own validator, including
422
449
  controls that assert the two invalid schema shapes still fail.
423
450
  - `install-verify.mjs` drives the install pipeline against a loopback HTTP server: route ordering,
@@ -459,6 +486,12 @@ through a proxy, that proxy too. Useful where a network is available, but a suit
459
486
  because a website changed is worse than no suite, so it stays a local check. If you run it and see
460
487
  failures in sections 4–6 while sections 1–3 pass, suspect the network before the code.
461
488
 
489
+ ### Prerequisites for the suites
490
+
491
+ The plugin has one optional runtime dependency — [`@deepseek-ai/schemastery`](https://www.npmjs.com/package/@deepseek-ai/schemastery) — used by the live `Config` schema that registers the settings namespace (optional because the import is guarded, so a `link:` install without it still loads and registers the tools). Suites that import the plugin (`config-verify`, `schema-verify`, `empty-verify`, `report-verify`, `http-verify`) need it resolvable from the working tree, so run `npm install` first.
492
+
493
+ `schema-verify` additionally imports the registry's own validator out of the npm-global `@deepseek-ai/dsh` install (`@deepseek-ai/dsh-tools`); `report-verify` and the engine suites need a real Moli engine, and `empty-verify` needs a C compiler to build its stub engines. On Windows, `@deepseek-ai/schemastery` can be resolved through a directory junction into the fork's `node_modules` from the npm-global DSH tree, the same way the suites resolve `dsh-tools`.
494
+
462
495
  Beyond those, `lib/moli.js` and the tool layer were exercised end to end against Moli 1.1.16 on
463
496
  Windows: registration, probe, Markdown and JSON dumps, `eval` (including a tight-timeout regression
464
497
  guard for the `-t` argument position), PNG and PDF artifacts with valid signatures, truncation, every
package/README.zh.md CHANGED
@@ -289,9 +289,10 @@ Moli 仓库自带官方 agent skill:[`moli-webfetch`](https://github.com/lexmo
289
289
  重定向到临时文件再读回。
290
290
  2. **二进制按原始字节落盘。** PowerShell 的 `>` 会把原生程序输出按 UTF-16 重编码(每字节后插
291
291
  `00`),截图因此损坏。这里直接写字节,PNG 签名保持 `89504e470d0a1a0a`。
292
- 3. **零外部依赖。** profile 插件按磁盘真实路径加载(`link:` 依赖仍是符号链接目标),任何裸包名
293
- 导入(例如 `@deepseek-ai/schemastery`)都会解析到插件目录而非 profile 的 `node_modules`,导致
294
- 插件加载失败。本插件只导入 `node:` 内置模块和自己的文件。
292
+ 3. **一个可选的外部依赖。** profile 插件按磁盘真实路径加载(`link:` 依赖仍是符号链接目标),任何裸包名
293
+ 导入(例如 `@deepseek-ai/schemastery`)都会解析到插件目录而非 profile 的 `node_modules`。因此该导入
294
+ 是**受保护的**:没有 schemastery(`link:` 安装)时设置界面不出现、插件退化为纯补丁层配置,而
295
+ `moli_fetch` 与 `moli_probe` 照常注册;有它(普通 npm 安装会自动带上)时设置页面可用。
295
296
 
296
297
  ## 平台支持
297
298
 
@@ -328,7 +329,7 @@ cookie 驱动的查询参数。那次实测同时覆盖了代理下载路由与
328
329
 
329
330
  ## 配置
330
331
 
331
- 在 profile 的补丁层里覆盖 `config`,全部可选:
332
+ 在 profile 的补丁层里覆盖 `config`,全部可选。也可以在图 **Moli 设置** 界面里编辑(见下方「设置页入口」)。
332
333
 
333
334
  ```yaml
334
335
  - insert:
@@ -349,6 +350,32 @@ profile。无论生效的是哪个代理,loopback 地址一律绕过它。
349
350
  → `%USERPROFILE%\.local\bin\moli.exe`,随后还会在 `%LOCALAPPDATA%\Moli` 内递归查找 `moli.exe`,
350
351
  所以装在带版本号的子目录里也能找到。
351
352
 
353
+ ### 设置页入口
354
+
355
+ 插件自带一个**设置界面**,挂在插件管理页的对应插件条目上(`plugins.bundle.config` 槽位,由浏览器端
356
+ `client.js` 注册)。两种方式进入:
357
+
358
+ 1. **插件管理页**:打开 DSH Web 界面的插件管理页,找到 **dsh-moli** 条目,其配置区即是本设置界面;
359
+ 引擎状态、安装/卸载按钮、以及下方所有配置字段都在这里编辑。
360
+ 2. **直接改 profile**:按上面的 YAML 改 `cordis.patch.yml` 同样生效,两边是同一份 `config`,保存即同步。
361
+
362
+ 设置界面跟随 DSH 当前语言(内置 `zh` / `en`,英语兜底),布尔字段(如 `insecureTls`)以下拉选择
363
+ **原始值**(`true` / `false`)显示,不做本地化翻译。
364
+
365
+ ### 引擎生命周期(设置页的「引擎」行)
366
+
367
+ 设置界面顶部有一条**引擎**状态行,自动检测已安装的引擎,并可一键安装/卸载,无需动 profile:
368
+
369
+ - **状态** — `engineState` 为 `installed`(同时给出 `engineVersion`)、`missing`,或一段简短错误文本。
370
+ - **安装** — 点「安装引擎」后表单写入 `engineAction: "install"`,主机异步下载并运行官方安装器,
371
+ 完成后通过 `engineState` / `engineVersion` 回写结果。
372
+ - **卸载** — 「卸载引擎」写入 `engineAction: "uninstall"`。Windows 上只删除安装器所属的
373
+ `%LOCALAPPDATA%\Moli` 目录树;没有该目录树时**不触碰任何文件**。POSIX 上删除定位到的二进制,
374
+ 并清理随之变空的安装目录。
375
+
376
+ `engineAction` / `engineState` / `engineVersion` 三个字段属于动态 Config schema(`engineAction` 是
377
+ `none` / `install` / `uninstall` 三选一),设计上由设置界面驱动;日常改配置请优先用上面的四个字段。
378
+
352
379
  ## 已知限制
353
380
 
354
381
  - **macOS 未经验证,Linux 是由贡献者而非维护者验证的。** 维护者是一名**没有 macOS / Linux 电脑**的
@@ -376,8 +403,13 @@ profile。无论生效的是哪个代理,loopback 地址一律绕过它。
376
403
 
377
404
  ## 验证情况
378
405
 
379
- [`verify/`](verify/) 下有四个可直接对工作区运行的测试:
406
+ [`verify/`](verify/) 下有八个可直接对工作区运行的测试:
380
407
 
408
+ - `config-verify.mjs` 检查设置面与引擎生命周期:动态 `Config` schema(每个已声明字段、`engineAction`
409
+ 为 `none`/`install`/`uninstall` 三选一)、`apply` 同时接受普通配置与动态 `.get()` 引用、
410
+ `settings/document-updated` 接线、`uninstallMoli`(平台感知:POSIX 删除定位到的二进制,Windows
411
+ 只删除安装器所属的 `%LOCALAPPDATA%\Moli` 目录树)、以及 client.js 的形状(`plugins.bundle.config`
412
+ 注册、zh/en 词典、`true`/`false` 下拉显示原始值)。
381
413
  - `schema-verify.mjs` 用注册表自己的校验器检查工具定义,并带有断言"两种非法 schema 形状仍会被拒"的对照组。
382
414
  - `install-verify.mjs` 用本地回环 HTTP 服务器驱动安装链路:路由顺序、被提供字节的 SHA256、校验不匹配时
383
415
  拒绝执行、匹配时真正执行、以及全部路径失败的诊断。
@@ -400,9 +432,21 @@ profile。无论生效的是哪个代理,loopback 地址一律绕过它。
400
432
  ——`curl.exe` 连 `127.0.0.1` 会超时,而 Node 宿主进程可以——所以它用 `example.com` 下的一个路径,网络不可用
401
433
  时跳过。
402
434
 
403
- `.github/workflows/verify.yml` 在 Linux、Windows、macOS 上跑 `schema-verify`、`empty-verify` 与
404
- `scroll-verify`(无需引擎、无需网络),在 Linux 与 Windows 上尝试依赖引擎的几套;若 runner 上没有引擎,
405
- 则报告为跳过而不是失败。
435
+ ### 测试前置条件
436
+
437
+ 插件有一个可选的运行期依赖——[`@deepseek-ai/schemastery`](https://www.npmjs.com/package/@deepseek-ai/schemastery),
438
+ 由动态 `Config` schema(注册设置命名空间的那份)使用。凡是 import 插件本体的套件(`config-verify`、
439
+ `schema-verify`、`empty-verify`、`report-verify`、`http-verify`)都需要它能从工作区解析,所以先跑
440
+ `npm install`。
441
+
442
+ `schema-verify` 额外需要从 npm 全局安装的 `@deepseek-ai/dsh` 里 import 注册表自己的校验器
443
+ (`@deepseek-ai/dsh-tools`);`report-verify` 与引擎类套件需要真实 Moli 引擎,`empty-verify` 需要
444
+ C 编译器来构建它的桩引擎。在 Windows 上,`@deepseek-ai/schemastery` 可以通过目录 junction 指到
445
+ npm 全局 DSH 树来解析(与套件解析 `dsh-tools` 的方式相同)。
446
+
447
+ `.github/workflows/verify.yml` 在 Linux、Windows、macOS 上跑 `schema-verify`、`empty-verify`、
448
+ `scroll-verify` 与 `http-verify`(无需引擎、无需网络),在 Linux 与 Windows 上尝试依赖引擎的几套;
449
+ 若 runner 上没有引擎,则报告为跳过而不是失败。
406
450
 
407
451
  `report-verify.mjs` **故意不放进 CI**:其中三项会抓 `example.com`,还有一项抓一个具体的论坛帖子,因此它
408
452
  需要能上外网——如果这台机器的流量走代理,还需要那个代理可用。有网络时它是有用的,但**一个因为网站改版就变
package/client.js ADDED
@@ -0,0 +1,347 @@
1
+ /**
2
+ * dsh-moli — browser half.
3
+ *
4
+ * Settings section (Moli install & fetch): the plugin's configuration —
5
+ * engine lifecycle (install / uninstall), forward proxy, per-call timeout,
6
+ * inline truncation budget, the TLS no-verify switch, and the engine path —
7
+ * edited on the plugin's page in the Plugins manager.
8
+ *
9
+ * Hand-written ModuleLoader bundle — no build step required.
10
+ */
11
+ window.__ModuleLoader__.load({
12
+ id: "dsh-moli",
13
+ factory: (require) => {
14
+ var module = { exports: {} };
15
+ var exports = module.exports;
16
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
17
+ var react = require("react");
18
+ var h = react.createElement;
19
+
20
+ // ── CSS (theme tokens, same vocabulary as dsh-soul-md) ────────────────
21
+ var CSS = ".__moli_root{max-width:640px;display:flex;flex-direction:column;gap:10px}" +
22
+ ".__moli_field{display:flex;flex-direction:column;gap:4px}" +
23
+ ".__moli_label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px}" +
24
+ ".__moli_hint{font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
25
+ ".__moli_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 10px;font-size:13px;box-sizing:border-box;width:100%}" +
26
+ ".__moli_actions{display:flex;gap:8px;align-items:center;margin-top:4px}" +
27
+ ".__moli_btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 14px;font:inherit;font-size:13px;cursor:pointer}" +
28
+ ".__moli_btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}" +
29
+ ".__moli_btn:disabled{opacity:.5;cursor:default}" +
30
+ ".__moli_btnPrimary{border-color:var(--dsw-alias-state-business-primary, #3964fe);background:var(--dsw-alias-state-business-primary, #3964fe);color:#fff}" +
31
+ ".__moli_btnDanger{border-color:var(--dsw-alias-state-error-primary, #f85149);color:var(--dsw-alias-state-error-primary, #f85149)}" +
32
+ ".__moli_status{font-size:12px;color:var(--dsw-alias-label-tertiary)}" +
33
+ ".__moli_error{font-size:12px;color:var(--dsw-alias-state-error-primary)}" +
34
+ ".__moli_unavailable{font-size:13px;color:var(--dsw-alias-label-tertiary)}" +
35
+ ".__moli_group{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}" +
36
+ ".__moli_engineRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}" +
37
+ ".__moli_engineState{font-size:13px;color:var(--dsw-alias-label-primary);font-weight:600}" +
38
+ ".__moli_engineDetail{font-size:11px;color:var(--dsw-alias-label-tertiary)}";
39
+ if (typeof document !== "undefined") {
40
+ var style = document.createElement("style");
41
+ style.textContent = CSS;
42
+ style.setAttribute("data-dsh-moli", "");
43
+ if (!document.head.querySelector("style[data-dsh-moli]")) document.head.appendChild(style);
44
+ }
45
+
46
+ // ── locale ────────────────────────────────────────────────────────────
47
+ var zh = {
48
+ nav: "Moli 设置",
49
+ sectionTitle: "Moli 安装与抓取",
50
+ unavailable: "配置不可用(可能当前 profile 未启用本插件)。",
51
+ loading: "正在读取配置…",
52
+ saved: "已保存。",
53
+ save: "保存",
54
+ error: "保存失败",
55
+ notApplied: "写入被宿主拒绝,表单已按宿主实际状态刷新。",
56
+ engineSection: "引擎",
57
+ engineInstalled: "已安装",
58
+ engineMissing: "未安装",
59
+ engineUnknown: "检测中…",
60
+ engineWorking: "正在处理…",
61
+ engineActionInstall: "安装引擎",
62
+ engineActionUninstall: "卸载引擎",
63
+ engineVersion: "版本 {version}",
64
+ engineInstallHint: "未检测到 Moli 引擎。点击「安装引擎」下载官方安装器并运行(需联网,可能耗时数分钟)。",
65
+ engineUninstallHint: "点击「卸载引擎」将删除本机安装的 Moli 二进制。",
66
+ fieldProxy: "转发代理",
67
+ hintProxy: '每次调用的代理覆盖项;留空表示直连。例如 "http://127.0.0.1:7890"。回环地址总是绕过代理。',
68
+ fieldDefaultTimeoutMs: "默认超时(毫秒)",
69
+ hintDefaultTimeoutMs: "单次调用默认超时,默认 30000(30 秒)。",
70
+ fieldMaxChars: "内联文本截断(字符)",
71
+ hintMaxChars: "内联文本最多返回的字符数,默认 200000;超出时保留头尾并注明省略量,传 writeTo 可保留全文。",
72
+ fieldInsecureTls: "跳过 TLS 验证(insecureTls)",
73
+ hintInsecureTls: '本网络证书吊销应答不可达时,GitHub 等 HTTPS 主机可能报错;开启后透传 moli 的 no-verify 标志。默认开启。',
74
+ fieldExePath: "引擎路径(exePath)",
75
+ hintExePath: "可选。指定 moli 可执行文件的绝对路径;留空则自动在常见安装位置查找。",
76
+ placeholderProxy: "http://127.0.0.1:7890",
77
+ placeholderExePath: "/home/user/.local/bin/moli",
78
+ };
79
+ var en = {
80
+ nav: "Moli settings",
81
+ sectionTitle: "Moli install & fetch",
82
+ unavailable: "Configuration unavailable (this profile may not enable the plugin).",
83
+ loading: "Loading configuration…",
84
+ saved: "Saved.",
85
+ save: "Save",
86
+ error: "Save failed",
87
+ notApplied: "The write was refused by the host; the form was reloaded to what the host actually holds.",
88
+ engineSection: "Engine",
89
+ engineInstalled: "Installed",
90
+ engineMissing: "Not installed",
91
+ engineUnknown: "Detecting…",
92
+ engineWorking: "Working…",
93
+ engineActionInstall: "Install engine",
94
+ engineActionUninstall: "Uninstall engine",
95
+ engineVersion: "version {version}",
96
+ engineInstallHint: "No Moli engine found. Click “Install engine” to download and run the official installer (needs network; may take a few minutes).",
97
+ engineUninstallHint: "Click “Uninstall engine” to remove the locally installed Moli binary.",
98
+ fieldProxy: "Forward proxy",
99
+ hintProxy: 'Per-call proxy override; leave empty for direct. E.g. "http://127.0.0.1:7890". Loopback URLs always bypass it.',
100
+ fieldDefaultTimeoutMs: "Default timeout (ms)",
101
+ hintDefaultTimeoutMs: "Per-call default timeout; 30000 (30 s) by default.",
102
+ fieldMaxChars: "Inline truncation budget (chars)",
103
+ hintMaxChars: "Max characters returned inline; 200000 by default. Beyond it the head and tail are kept with an omitted-count note; pass writeTo to keep everything.",
104
+ fieldInsecureTls: "Skip TLS verification (insecureTls)",
105
+ hintInsecureTls: "When this network's CRL/OCSP responder is unreachable, HTTPS hosts like GitHub fail; enabling passes moli's no-verify flag. On by default.",
106
+ fieldExePath: "Engine path (exePath)",
107
+ hintExePath: "Optional. Absolute path to the moli executable; leave empty to search the usual install locations.",
108
+ placeholderProxy: "http://127.0.0.1:7890",
109
+ placeholderExePath: "/home/user/.local/bin/moli",
110
+ };
111
+
112
+ // ── write helper (same contract as dsh-soul-md) ───────────────────────
113
+ function settingsOpsApplied(snapshot, ops) {
114
+ var v = snapshot && snapshot.status === "ready" ? snapshot.value : void 0;
115
+ if (v === void 0) return false;
116
+ return ops.every(function (op) {
117
+ if (op.op === "unset") return v[op.path[0]] === void 0;
118
+ return v[op.path[0]] === op.value;
119
+ });
120
+ }
121
+ function commitSettingsOps(scope, ops) {
122
+ if (ops.length === 0) return Promise.resolve(true);
123
+ var run;
124
+ if (typeof scope.mutate === "function") {
125
+ run = scope.mutate(ops, scope.getSnapshot().revision);
126
+ } else {
127
+ run = ops.reduce(function (chain, op) {
128
+ return chain.then(function () {
129
+ return op.op === "unset" ? scope.unset(op.path[0]) : scope.set(op.path[0], op.value);
130
+ });
131
+ }, Promise.resolve());
132
+ }
133
+ return Promise.resolve(run).then(function () {
134
+ return settingsOpsApplied(scope.getSnapshot(), ops);
135
+ });
136
+ }
137
+
138
+ // ── field descriptors ─────────────────────────────────────────────────
139
+ // `select` fields store booleans (single option pair); the draft holds the
140
+ // string option and the ops map it back to a boolean.
141
+ var FIELDS = [
142
+ { key: "proxy", label: "fieldProxy", type: "text", hint: "hintProxy", placeholder: "placeholderProxy" },
143
+ { key: "defaultTimeoutMs", label: "fieldDefaultTimeoutMs", type: "number", min: 1000, hint: "hintDefaultTimeoutMs" },
144
+ { key: "maxChars", label: "fieldMaxChars", type: "number", min: 100, hint: "hintMaxChars" },
145
+ { key: "insecureTls", label: "fieldInsecureTls", type: "select", options: [["true", "true"], ["false", "false"]], hint: "hintInsecureTls" },
146
+ { key: "exePath", label: "fieldExePath", type: "text", hint: "hintExePath", placeholder: "placeholderExePath" },
147
+ ];
148
+ function toDraft(value) {
149
+ var out = {};
150
+ for (var i = 0; i < FIELDS.length; i++) {
151
+ var f = FIELDS[i];
152
+ var v = value && value[f.key];
153
+ if (f.type === "number") out[f.key] = typeof v === "number" ? String(v) : "";
154
+ else if (f.type === "select") out[f.key] = v ? "true" : "false";
155
+ else out[f.key] = typeof v === "string" ? v : "";
156
+ }
157
+ return out;
158
+ }
159
+ function draftOps(draft) {
160
+ var ops = [];
161
+ for (var i = 0; i < FIELDS.length; i++) {
162
+ var f = FIELDS[i];
163
+ if (f.type === "number") {
164
+ var n = Number(draft[f.key]);
165
+ if (draft[f.key] !== "" && Number.isFinite(n)) ops.push({ op: "set", path: [f.key], value: n });
166
+ } else if (f.type === "select") {
167
+ ops.push({ op: "set", path: [f.key], value: draft[f.key] === "true" });
168
+ } else {
169
+ ops.push({ op: "set", path: [f.key], value: typeof draft[f.key] === "string" ? draft[f.key] : "" });
170
+ }
171
+ }
172
+ return ops;
173
+ }
174
+
175
+ // ── settings section ──────────────────────────────────────────────────
176
+ function MoliConfigSection(props) {
177
+ useLocale(props.locale);
178
+ var t = props.t;
179
+ var scope = props.scope;
180
+ var [snapshot, setSnapshot] = react.useState(function () { return scope.getSnapshot(); });
181
+ var ready = snapshot.status === "ready" && snapshot.value !== void 0;
182
+ var [draft, setDraft] = react.useState(null);
183
+ var [busy, setBusy] = react.useState(false);
184
+ var [notice, setNotice] = react.useState(null);
185
+ var [error, setError] = react.useState(null);
186
+ var writePending = react.useRef(false);
187
+
188
+ react.useEffect(function () {
189
+ var alive = true;
190
+ var sync = function () { if (alive) setSnapshot(scope.getSnapshot()); };
191
+ var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
192
+ // Bound once per plugin, shared across mounts: unmount only unsubscribes,
193
+ // never disposes, so a later remount does not find a dead write queue.
194
+ return function () { alive = false; if (un) un(); };
195
+ }, [scope]);
196
+
197
+ if (snapshot.status === "unavailable") {
198
+ return h("p", { className: "__moli_unavailable" }, t("unavailable"));
199
+ }
200
+ if (!ready) return h("p", { className: "__moli_status" }, t("loading"));
201
+
202
+ var value = snapshot.value;
203
+ var currentDraft = draft === null ? toDraft(value) : draft;
204
+ var valueKey = JSON.stringify(toDraft(value));
205
+ var draftKey = JSON.stringify(currentDraft);
206
+ var dirty = valueKey !== draftKey;
207
+ var saving = busy;
208
+
209
+ function setField(f, v) { setNotice(null); setError(null); setDraft(function (p) { return Object.assign({}, p, { [f.key]: v }); }); }
210
+
211
+ function onSave() {
212
+ if (writePending.current) return;
213
+ writePending.current = true;
214
+ setBusy(true); setNotice(null); setError(null);
215
+ commitSettingsOps(scope, draftOps(currentDraft)).then(function (ok) {
216
+ writePending.current = false;
217
+ setBusy(false);
218
+ setSnapshot(scope.getSnapshot());
219
+ if (!ok) { setError(t("notApplied")); return; }
220
+ setNotice(t("saved"));
221
+ setDraft(null);
222
+ }).catch(function (e) {
223
+ writePending.current = false;
224
+ setBusy(false);
225
+ setSnapshot(scope.getSnapshot());
226
+ setError(String(e && e.message || e));
227
+ });
228
+ }
229
+
230
+ // Engine lifecycle: the Host reacts to `engineAction` and reports back
231
+ // through `engineState` / `engineVersion`. While the action is pending the
232
+ // form shows "working…"; scope updates (including the reset to `none`)
233
+ // drive the display back to the resulting state.
234
+ var engineState = typeof value.engineState === "string" ? value.engineState : "unknown";
235
+ var engineVersion = typeof value.engineVersion === "string" ? value.engineVersion : "";
236
+ var engineAction = typeof value.engineAction === "string" ? value.engineAction : "none";
237
+ var engineWorking = engineAction !== "none";
238
+ var engineLabel = engineWorking ? t("engineWorking")
239
+ : engineState === "installed" ? t("engineInstalled")
240
+ : engineState === "missing" ? t("engineMissing")
241
+ : t("engineUnknown");
242
+
243
+ function onEngineAction(action) {
244
+ if (engineWorking) return;
245
+ if (action === "uninstall" && !window.confirm(t("engineUninstallHint"))) return;
246
+ setNotice(null); setError(null);
247
+ commitSettingsOps(scope, [{ op: "set", path: ["engineAction"], value: action }]).then(function (ok) {
248
+ setSnapshot(scope.getSnapshot());
249
+ if (!ok) setError(t("notApplied"));
250
+ }).catch(function (e) {
251
+ setSnapshot(scope.getSnapshot());
252
+ setError(String(e && e.message || e));
253
+ });
254
+ }
255
+
256
+ return h("div", { className: "__moli_root" },
257
+ // Engine lifecycle group.
258
+ h("div", { className: "__moli_group" },
259
+ h("div", { className: "__moli_label" }, t("engineSection")),
260
+ h("div", { className: "__moli_engineRow" },
261
+ h("span", { className: "__moli_engineState" }, engineLabel),
262
+ engineVersion && engineState === "installed"
263
+ ? h("span", { className: "__moli_engineDetail" }, t("engineVersion").replace("{version}", engineVersion))
264
+ : null,
265
+ ),
266
+ engineState !== "installed" && !engineWorking
267
+ ? h("span", { className: "__moli_engineDetail" }, t("engineInstallHint"))
268
+ : engineState === "installed" && !engineWorking
269
+ ? h("span", { className: "__moli_engineDetail" }, t("engineUninstallHint"))
270
+ : null,
271
+ h("div", { className: "__moli_actions" },
272
+ engineState === "installed"
273
+ ? h("button", { className: "__moli_btn __moli_btnDanger", disabled: engineWorking, onClick: function () { onEngineAction("uninstall"); } }, t("engineActionUninstall"))
274
+ : h("button", { className: "__moli_btn __moli_btnPrimary", disabled: engineWorking, onClick: function () { onEngineAction("install"); } }, t("engineActionInstall")),
275
+ ),
276
+ ),
277
+ // Configuration fields group.
278
+ h("div", { className: "__moli_group" },
279
+ FIELDS.map(function (f) {
280
+ if (f.type === "select") {
281
+ return h("label", { key: f.key, className: "__moli_field" },
282
+ h("span", { className: "__moli_label" }, t(f.label)),
283
+ h("select", {
284
+ className: "__moli_input",
285
+ value: currentDraft[f.key],
286
+ onChange: function (e) { setField(f, e.target.value); },
287
+ }, f.options.map(function (o) {
288
+ // Option labels are the raw stored values ("true"/"false"),
289
+ // never translated; only the field label and hint are locale-bound.
290
+ return h("option", { key: o[0], value: o[0] }, o[1]);
291
+ })),
292
+ f.hint ? h("span", { className: "__moli_hint" }, t(f.hint)) : null,
293
+ );
294
+ }
295
+ return h("label", { key: f.key, className: "__moli_field" },
296
+ h("span", { className: "__moli_label" }, t(f.label)),
297
+ h("input", {
298
+ className: "__moli_input",
299
+ type: f.type === "number" ? "number" : "text",
300
+ min: f.min,
301
+ value: currentDraft[f.key],
302
+ placeholder: f.placeholder ? t(f.placeholder) : void 0,
303
+ onChange: function (e) { setField(f, e.target.value); },
304
+ }),
305
+ f.hint ? h("span", { className: "__moli_hint" }, t(f.hint)) : null,
306
+ );
307
+ }),
308
+ h("div", { className: "__moli_actions" },
309
+ h("button", { className: "__moli_btn __moli_btnPrimary", disabled: saving || !dirty, onClick: onSave }, t("save")),
310
+ notice ? h("span", { className: "__moli_status" }, notice) : null,
311
+ error ? h("span", { className: "__moli_error" }, error) : null,
312
+ ),
313
+ ),
314
+ );
315
+ }
316
+
317
+ var NS = "dsh-moli";
318
+
319
+ // Keep the section re-rendering when the active locale changes.
320
+ function useLocale(locale) {
321
+ var refresh = react.useState(0)[1];
322
+ react.useEffect(function () {
323
+ if (!locale || typeof locale.subscribe !== "function") return;
324
+ return locale.subscribe(function () { refresh(function (revision) { return revision + 1; }); });
325
+ }, [locale]);
326
+ }
327
+
328
+ function apply(ctx) {
329
+ var t = ctx.locale.bind(NS);
330
+ ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-moli: dictionaries");
331
+ var scope = ctx.configForms.get("dsh-moli");
332
+ ctx.slots.inject("plugins.bundle.config", function () {
333
+ return ctx.slots.register({
334
+ name: "plugins.bundle.config",
335
+ key: "dsh-moli",
336
+ locale: NS
337
+ }, function (props) {
338
+ return h(MoliConfigSection, Object.assign({}, props, { scope: scope, t: t, locale: ctx.locale }));
339
+ });
340
+ });
341
+ }
342
+
343
+ exports.apply = apply;
344
+ exports.inject = ["slots", "locale", "configForms"];
345
+ return module.exports;
346
+ }
347
+ });
package/index.js CHANGED
@@ -1,19 +1,27 @@
1
1
  /** Host half of dsh-moli: expose the local `moli` headless browser as callable tools. */
2
- import { MANUAL_INSTALL, installMoli } from './lib/install.js';
3
- import { findMoli, moliCandidates, probe, runFetch } from './lib/moli.js';
2
+ import { MANUAL_INSTALL, installMoli, uninstallMoli } from './lib/install.js';
3
+ import { findMoli, moliCandidates, moliVersion, probe, runFetch } from './lib/moli.js';
4
4
 
5
5
  export const name = 'dsh-moli';
6
6
  export const inject = ['tools'];
7
7
 
8
8
  /**
9
- * This plugin deliberately imports nothing but `node:` builtins and its own
10
- * files. A profile plugin is loaded from its real path on disk (a `link:`
11
- * dependency stays a symlink target), so a bare specifier such as
12
- * `@deepseek-ai/schemastery` would resolve against the plugin directory
13
- * instead of the profile's `node_modules` and fail to load. The registry
14
- * validates parameters and results itself, so `defineTool` and a Schema-based
15
- * config are not needed; configuration is read defensively from the patch layer.
9
+ * `@deepseek-ai/schemastery` powers the live Config schema and therefore the
10
+ * settings namespace. A profile plugin is loaded from its real path on disk (a
11
+ * `link:` dependency stays a symlink target), so a bare specifier must resolve
12
+ * against the plugin's own `node_modules` chain — which does not exist when the
13
+ * plugin is installed as a `link:`/junction (the README's first install route).
14
+ * The import is therefore optional: without schemastery the settings form does
15
+ * not appear and the plugin degrades to plain patch-layer configuration, while
16
+ * `moli_fetch` / `moli_probe` keep registering — those are the plugin's reason
17
+ * to exist, and losing them would be worse than losing the form.
16
18
  */
19
+ let z;
20
+ try {
21
+ z = (await import('@deepseek-ai/schemastery')).default;
22
+ } catch {
23
+ z = undefined;
24
+ }
17
25
 
18
26
  /** Deployment configuration defaults; override via the plugin's `config` entry. */
19
27
  const DEFAULTS = {
@@ -27,6 +35,35 @@ const DEFAULTS = {
27
35
  insecureTls: true,
28
36
  };
29
37
 
38
+ /**
39
+ * Live Config schema. Declaring it (and exporting it) makes DSH register the
40
+ * `dsh-moli` settings namespace, which the browser half's `plugins.bundle.config`
41
+ * form edits. `.volatile()` marks the whole object live so the namespace appears
42
+ * in the settings describe mirror and the form's writes are applied via the
43
+ * ConfigEditor / volatile HMR path.
44
+ *
45
+ * `undefined` when `@deepseek-ai/schemastery` cannot be resolved (a `link:`
46
+ * install) — then no settings namespace is registered and the engine lifecycle
47
+ * is skipped, but the tools still register.
48
+ */
49
+ export const Config = z
50
+ ? z.object({
51
+ defaultTimeoutMs: z.number().default(DEFAULTS.defaultTimeoutMs),
52
+ maxChars: z.number().default(DEFAULTS.maxChars),
53
+ insecureTls: z.boolean().default(DEFAULTS.insecureTls),
54
+ proxy: z.string().default(''),
55
+ exePath: z.string().default(''),
56
+ // Engine lifecycle, driven by the settings form. The form writes an action
57
+ // (`install` / `uninstall`) and the Host executes it asynchronously, then
58
+ // reports the outcome back through `engineState` / `engineVersion` and resets
59
+ // `engineAction` to `none`. `engineState` is `installed`, `missing`, or a
60
+ // short error text.
61
+ engineAction: z.union([z.const('none'), z.const('install'), z.const('uninstall')]).default('none'),
62
+ engineState: z.string().default('unknown'),
63
+ engineVersion: z.string().default(''),
64
+ }).volatile()
65
+ : undefined;
66
+
30
67
  /**
31
68
  * Parameter schema of `moli_fetch`.
32
69
  *
@@ -265,7 +302,10 @@ function renderProbe(_args, value) {
265
302
  * @param config - deployment configuration for paths, timeout, and output size.
266
303
  */
267
304
  export function apply(ctx, config) {
268
- const options = config ?? {};
305
+ // The Config schema is declared volatile, so DSH hands it as a live reference
306
+ // whose `.get()` reads the current values; a deployment that mounts an older
307
+ // loader without volatile wiring passes a plain object instead. Read either.
308
+ const options = typeof config?.get === 'function' ? config.get() : (config ?? {});
269
309
  // Mutable: a successful install resolves a path this process can use at once,
270
310
  // without waiting for a restart to re-run discovery.
271
311
  let cachedExe = findMoli(options.exePath);
@@ -384,5 +424,167 @@ export function apply(ctx, config) {
384
424
  },
385
425
  });
386
426
 
427
+ // ── engine lifecycle via the settings form ─────────────────────────────
428
+ // The browser half's `plugins.bundle.config` form drives install/uninstall
429
+ // through the `engineAction` field and reads the outcome from
430
+ // `engineState`/`engineVersion`. State is written back only when it changes,
431
+ // and a finished action always resets `engineAction` to `none`, so the
432
+ // resulting `settings/document-updated` re-entry lands in the plain sync
433
+ // branch instead of looping the action.
434
+ //
435
+ // Everything here depends on the settings namespace, which exists only when
436
+ // schemastery resolved (see the `Config` export): without it there is no form
437
+ // to drive the lifecycle, so the whole block is skipped and the plugin is
438
+ // pure tool registration.
439
+ if (Config) {
440
+ const NS = 'dsh-moli';
441
+ let engineBusy = false;
442
+ const currentConfig = () => (typeof config.get === 'function' ? config.get() : (config ?? {}));
443
+ // Write settings outside the current HMR transaction. `settings.update`
444
+ // routes through dsh-hmr's runExclusive, which rejects when it is called
445
+ // from inside an active transaction — the settings/document-updated event
446
+ // handler runs in one, so a direct update throws
447
+ // "HMR transactions cannot be nested" and the write-back is silently lost
448
+ // (the form then stays on "working…" forever). setImmediate defers the
449
+ // update until the event transaction has exited.
450
+ const writeSettings = (settings, patch) =>
451
+ new Promise((resolve, reject) => {
452
+ setImmediate(() => {
453
+ settings.update(NS, patch).then(resolve, reject);
454
+ });
455
+ });
456
+ const syncEngineState = async (settings, current) => {
457
+ const exe = findMoli(current.exePath);
458
+ const state = exe === undefined ? 'missing' : 'installed';
459
+ const version = exe === undefined ? '' : (moliVersion(exe) ?? '');
460
+ if (current.engineState === state && current.engineVersion === version) return;
461
+ try {
462
+ await writeSettings(settings, { engineState: state, engineVersion: version });
463
+ } catch (error) {
464
+ console.error(`[dsh-moli] engine-state sync failed: ${String(error?.message ?? error)}`);
465
+ ctx.logger?.warn?.('dsh-moli: engine-state sync failed', String(error?.message ?? error));
466
+ }
467
+ };
468
+ const runEngineAction = async (settings, current, action) => {
469
+ if (engineBusy) return;
470
+ engineBusy = true;
471
+ try {
472
+ if (action === 'install') {
473
+ const outcome = await installMoli({
474
+ exePath: current.exePath,
475
+ timeoutMs: 180000,
476
+ proxy: current.proxy,
477
+ locate: findMoli,
478
+ candidates: moliCandidates(current.exePath),
479
+ });
480
+ cachedExe = outcome.ok === true ? (outcome.exe ?? cachedExe) : cachedExe;
481
+ await writeSettings(settings, {
482
+ engineAction: 'none',
483
+ engineState: outcome.ok === true ? 'installed' : `install failed: ${String(outcome.error ?? 'unknown error').slice(0, 200)}`,
484
+ engineVersion: outcome.ok === true ? String(outcome.version ?? '') : '',
485
+ });
486
+ } else if (action === 'uninstall') {
487
+ const removed = uninstallMoli(current.exePath);
488
+ cachedExe = undefined;
489
+ await writeSettings(settings, {
490
+ engineAction: 'none',
491
+ engineState: removed === undefined ? 'uninstall failed: nothing to remove' : 'missing',
492
+ engineVersion: '',
493
+ });
494
+ }
495
+ } catch (error) {
496
+ // console (not ctx.logger) so the failure reaches the service journal
497
+ console.error(`[dsh-moli] engine action failed: ${String(error?.message ?? error)}`);
498
+ ctx.logger?.error?.('dsh-moli: engine action failed', String(error?.message ?? error));
499
+ try {
500
+ await writeSettings(settings, {
501
+ engineAction: 'none',
502
+ engineState: `action failed: ${String(error?.message ?? error).slice(0, 200)}`,
503
+ });
504
+ } catch (error2) {
505
+ console.error(`[dsh-moli] engine action failure report could not be written back: ${String(error2?.message ?? error2)}`);
506
+ ctx.logger?.error?.('dsh-moli: engine action failure report could not be written back', String(error2?.message ?? error2));
507
+ }
508
+ } finally {
509
+ engineBusy = false;
510
+ }
511
+ };
512
+ // Dispatch an engineAction if one is pending, otherwise sync state. Shared by
513
+ // the settings update handler and the boot-time initial sync, so an action
514
+ // whose `settings/document-updated` event was lost (a crash between the
515
+ // form's write and the handler) is still picked up on the next boot instead
516
+ // of leaving the form stuck on "processing".
517
+ // The event fires inside a dsh-hmr transaction. That transaction propagates
518
+ // through AsyncLocalStorage into anything the handler spawns — including
519
+ // setTimeout/setImmediate callbacks — so a deferred settings.update still
520
+ // nests and is rejected ("HMR transactions cannot be nested"). dsh-hmr
521
+ // itself escapes its own transaction with `executing.exit(...)` (watchConfig
522
+ // does this); running the dispatch through the same exit clears the ALS
523
+ // store so the write-back opens its own outermost transaction. The poller
524
+ // below stays as a defensive fallback (exit missing, or an event lost
525
+ // between the form's write and the handler).
526
+ const dispatchOutsideTransaction = (settings) => {
527
+ const hmr = ctx.get?.('hmr');
528
+ const run = () => {
529
+ const current = currentConfig();
530
+ const action = current.engineAction;
531
+ if (action === 'install' || action === 'uninstall') void runEngineAction(settings, current, action);
532
+ else void syncEngineState(settings, current);
533
+ };
534
+ if (hmr && typeof hmr.executing?.exit === 'function') {
535
+ hmr.executing.exit(run);
536
+ } else {
537
+ run();
538
+ }
539
+ };
540
+ // Actions are driven by an independent poller as a fallback, and by the
541
+ // transaction-escaped dispatch on the event path. A setInterval callback
542
+ // runs in the timer phase, outside any transaction, so the action and its
543
+ // write-back are safe there too.
544
+ let pollTimer = null;
545
+ let pendingCheck = false;
546
+ const pollCheck = () => {
547
+ const settings = ctx.get('settings');
548
+ if (!settings) return;
549
+ const current = currentConfig();
550
+ const action = current.engineAction;
551
+ if (action === 'install' || action === 'uninstall') {
552
+ void runEngineAction(settings, current, action);
553
+ } else {
554
+ void syncEngineState(settings, current);
555
+ }
556
+ };
557
+ ctx.on?.('settings/document-updated', (id) => {
558
+ if (id !== NS) return;
559
+ const settings = ctx.get('settings');
560
+ if (!settings) return;
561
+ pendingCheck = true;
562
+ dispatchOutsideTransaction(settings);
563
+ });
564
+ pollTimer = setInterval(() => {
565
+ if (pendingCheck) {
566
+ pendingCheck = false;
567
+ pollCheck();
568
+ }
569
+ }, 500);
570
+ pollTimer.unref?.();
571
+ // Initial state once settings is live. `ctx.get('settings')` may be null in a
572
+ // boot-time effect (the service mounts later), so retry briefly; the poller
573
+ // also picks up any pending engineAction written before the service settled.
574
+ let syncAttempts = 0;
575
+ const tryInitialSync = () => {
576
+ const settings = ctx.get('settings');
577
+ if (!settings) return false;
578
+ pollCheck();
579
+ return true;
580
+ };
581
+ if (!tryInitialSync()) {
582
+ const timer = setInterval(() => {
583
+ if (tryInitialSync() || ++syncAttempts >= 40) clearInterval(timer);
584
+ }, 250);
585
+ timer.unref?.();
586
+ }
587
+ }
588
+
387
589
  ctx.logger?.info?.(`dsh-moli: registered moli_fetch and moli_probe (${cachedExe ?? 'moli missing'})`);
388
590
  }
package/lib/install.js CHANGED
@@ -36,10 +36,10 @@
36
36
  */
37
37
  import { spawn, spawnSync } from 'node:child_process';
38
38
  import { createHash } from 'node:crypto';
39
- import { closeSync, existsSync, mkdtempSync, openSync, readFileSync, rmSync, statSync } from 'node:fs';
39
+ import { closeSync, existsSync, mkdtempSync, openSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
40
40
  import { createConnection } from 'node:net';
41
41
  import { tmpdir } from 'node:os';
42
- import { join } from 'node:path';
42
+ import { dirname, join } from 'node:path';
43
43
  import { findMoli } from './moli.js';
44
44
 
45
45
  /** Official installer for the latest release. */
@@ -673,7 +673,7 @@ export async function installMoli(options = {}) {
673
673
  return {
674
674
  ok: false,
675
675
  error: `installer checksum mismatch: expected ${expected}, got ${download.sha256}`,
676
- sha256: download.sha256,
676
+ installerSha256: download.sha256,
677
677
  expectedSha256: expected,
678
678
  method: download.method,
679
679
  note: 'Refusing to execute the installer.',
@@ -759,3 +759,37 @@ export async function installMoli(options = {}) {
759
759
  rmSync(workDir, { recursive: true, force: true });
760
760
  }
761
761
  }
762
+
763
+ /**
764
+ * Remove an installed Moli engine. Windows deletes the installer-owned
765
+ * `%LOCALAPPDATA%\Moli` tree; POSIX removes the located binary and prunes the
766
+ * install directories it sits in when they end up empty.
767
+ *
768
+ * @param override - explicit path from configuration, when set.
769
+ * @returns the removed path, or undefined when nothing was installed.
770
+ */
771
+ export function uninstallMoli(override) {
772
+ const exe = findMoli(override);
773
+ if (exe === undefined) return undefined;
774
+ if (process.platform === 'win32') {
775
+ const root = process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, 'Moli') : undefined;
776
+ if (root !== undefined && existsSync(root)) {
777
+ rmSync(root, { recursive: true, force: true });
778
+ return root;
779
+ }
780
+ // No installer-owned tree: nothing to remove. Returning here keeps the
781
+ // uninstall from falling through to the POSIX branch and deleting whatever
782
+ // findMoli located — which could be a user-provided path or a test fixture.
783
+ return undefined;
784
+ }
785
+ rmSync(exe, { force: true });
786
+ const dir = dirname(exe);
787
+ for (const parent of [dir, dirname(dir)]) {
788
+ try {
789
+ if (existsSync(parent) && readdirSync(parent).length === 0) rmSync(parent, { force: true });
790
+ } catch {
791
+ // A non-empty or protected parent is fine; the binary is already gone.
792
+ }
793
+ }
794
+ return exe;
795
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-moli",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "type": "module",
5
5
  "description": "DeepSeek Harness tools for the local Moli headless browser: rendered fetches, markdown, semantic trees, screenshots, and PDFs.",
6
6
  "license": "MIT",
@@ -32,14 +32,19 @@
32
32
  "registry": "https://registry.npmjs.org",
33
33
  "access": "public"
34
34
  },
35
+ "dependencies": {
36
+ "@deepseek-ai/schemastery": "^3.18.4"
37
+ },
35
38
  "exports": {
36
39
  ".": "./index.js",
40
+ "./client": "./client.js",
37
41
  "./cordis.patch.yml": "./cordis.patch.yml",
38
42
  "./package.json": "./package.json"
39
43
  },
40
44
  "files": [
41
45
  "index.js",
42
46
  "lib",
47
+ "client.js",
43
48
  "cordis.patch.yml",
44
49
  "README.md",
45
50
  "README.zh.md",
@@ -48,6 +53,9 @@
48
53
  "dsh": {
49
54
  "bundle": {
50
55
  "patch": "./cordis.patch.yml"
56
+ },
57
+ "client": {
58
+ "platform": "web"
51
59
  }
52
60
  }
53
61
  }