dsh-moli 1.0.5 → 1.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 CHANGED
@@ -58,17 +58,57 @@ dsh plugin --profile web add github:27painter/dsh-moli
58
58
  ```
59
59
 
60
60
  > [!IMPORTANT]
61
- > **This package is the plugin only — it does not include the Moli engine.** It is 26 kB of
62
- > JavaScript; `moli.exe` is a separate ~101 MB Rust binary with its own release process. Installing the
63
- > plugin does not install Moli, and `moli_fetch` reports that the engine is missing until you install
64
- > it.
61
+ > **The npm package is the plugin only — it does not contain the Moli engine.**
65
62
  >
66
- > **There is no npm package for the engine.** The registry names `moli` and `moli-cli` belong to
63
+ > The npm tarball is 26 kB: eight JavaScript and text files. `moli.exe` is a separate ~101 MB compiled
64
+ > Rust binary, built and released by the Moli project on its own schedule.
65
+ >
66
+ > **Why it cannot be bundled:**
67
+ >
68
+ > - **Size.** The engine is roughly 4,000× the plugin. Every install and every plugin update would
69
+ > carry a browser.
70
+ > - **Ownership.** Moli is `lexmount/moli` under its own MIT copyright. Redistributing its binary
71
+ > through this package would mean re-publishing someone else's artifact as our own.
72
+ > - **Platforms.** The engine ships per OS and per architecture — Windows/Linux/macOS × x64/arm64.
73
+ > Bundling would pin one build into every user's install.
74
+ > - **Versions.** The plugin must work with whatever Moli version a user has, including Moli releases
75
+ > that appear after the plugin was published. Bundling would lock the pair together.
76
+ >
77
+ > **There is no npm package for the engine either.** The registry names `moli` and `moli-cli` belong to
67
78
  > unrelated projects, and `@lexmount/moli` does not exist. Moli ships as prebuilt binaries on
68
79
  > [GitHub Releases](https://github.com/lexmount/moli/releases/latest) — do not install the engine by
69
80
  > guessing a package name.
70
81
 
71
- Either route ends the same way. Restart DSH, then check the engine and fetch a page:
82
+ ## The two situations after installing
83
+
84
+ The two download routes above install the same thing. What differs is whether Moli is already on the
85
+ machine — and that decides whether you are finished or have one more step.
86
+
87
+ ### Situation A — Moli is already installed: nothing else to do
88
+
89
+ The plugin discovers `moli.exe` on its own and both tools work immediately. Just restart DSH and
90
+ confirm:
91
+
92
+ ```
93
+ moli_probe
94
+ ```
95
+
96
+ If the reported version looks right, you are done.
97
+
98
+ ### Situation B — Moli is not installed yet: fetch the engine too
99
+
100
+ `moli_fetch` will keep reporting a missing engine until you install one. Do that with either of:
101
+
102
+ ```
103
+ moli_probe action=install # let the plugin fetch it
104
+ ```
105
+
106
+ ```powershell
107
+ # or run Moli's own installer yourself
108
+ powershell -ExecutionPolicy ByPass -c "irm https://github.com/lexmount/moli/releases/latest/download/moli-installer.ps1 | iex"
109
+ ```
110
+
111
+ Then confirm with `moli_probe` and fetch a page:
72
112
 
73
113
  ```
74
114
  moli_probe
@@ -215,6 +255,17 @@ capabilities always visible to the model.
215
255
  resolves against the plugin directory instead of the profile's `node_modules` and the plugin
216
256
  fails to load. This plugin imports only `node:` builtins and its own files.
217
257
 
258
+ ## Platform support
259
+
260
+ Works on **Windows, Linux and macOS** (x64 / arm64).
261
+
262
+ - **Windows** — the engine is located at the official installer paths (`%LOCALAPPDATA%\Moli\bin\moli.exe`,
263
+ `%USERPROFILE%\.local\bin\moli.exe`) with a recursive scan of `%LOCALAPPDATA%\Moli` as a fallback.
264
+ `moli_probe action=install` runs the official PowerShell installer.
265
+ - **Linux / macOS** — the engine is found in the standard locations (`~/.local/bin/moli`,
266
+ `~/.local/share/moli/bin/moli`, `~/.local/share/moli/moli`, `~/.moli/bin/moli`) and on `$PATH`.
267
+ `moli_probe action=install` runs the official `moli-installer.sh` via `sh`.
268
+
218
269
  ## Configuration
219
270
 
220
271
  Override any field under the plugin's `config` entry in the profile patch layer. All are optional.
@@ -237,9 +288,14 @@ installation under a versioned subdirectory is still found.
237
288
 
238
289
  ## Known limitations
239
290
 
240
- - **Windows-only targeting.** Developed and tested on Windows; the executable discovery, the
241
- file-descriptor workaround, and the Schannel notes are Windows-specific. Nothing prevents a
242
- POSIX port, but it is untested.
291
+ - **Linux and macOS are implemented but not tested by the author.** The POSIX paths were contributed
292
+ in [PR #1](https://github.com/27painter/dsh-moli/pull/1) and reviewed, but the person who maintains
293
+ this repository is a student with **no macOS or Linux machine**, so those code paths have never been
294
+ run against a real POSIX host. Windows is the only platform verified here: the executable discovery,
295
+ the file-descriptor workaround and the Schannel notes are Windows-specific observations.
296
+ **If something fails on Linux or macOS, please open an
297
+ [issue](https://github.com/27painter/dsh-moli/issues) with the error output** — that is the only way
298
+ these paths get fixed.
243
299
  - **Moli itself is not bundled.** Install it separately or let `moli_probe action=install` fetch it.
244
300
  - **Only `fetch` is wrapped.** `moli serve` (CDP/WebDriver server) and `moli import` (session
245
301
  import) are not exposed as tools yet.
package/README.zh.md CHANGED
@@ -53,14 +53,53 @@ dsh plugin --profile web add github:27painter/dsh-moli
53
53
  ```
54
54
 
55
55
  > [!IMPORTANT]
56
- > **这个包只有插件,不含 Moli 引擎。** 它是 26 kB 的 JavaScript;`moli.exe` 是另一个约 101 MB 的 Rust
57
- > 二进制、有自己的发布流程。装插件**不会**顺带装上 Moli,在你装引擎之前 `moli_fetch` 会一直报"引擎缺失"。
56
+ > **npm 包只有插件,不含 Moli 引擎。**
58
57
  >
59
- > **引擎没有 npm 包。** registry 上的 `moli` 和 `moli-cli` 属于无关项目,`@lexmount/moli` 并不存在。
60
- > Moli 以预编译二进制形式发布在
58
+ > npm tarball 是 26 kB:八个 JavaScript 与文本文件。`moli.exe` 是另外的东西——约 101 MB 的 Rust 编译
59
+ > 产物,由 Moli 项目按自己的节奏构建和发布。
60
+ >
61
+ > **为什么不能打包进来:**
62
+ >
63
+ > - **体积。** 引擎约为插件的 4,000 倍。每次安装、每次插件更新都要顺带搬运一个浏览器。
64
+ > - **归属。** Moli 是 `lexmount/moli`,版权属于它自己的 MIT 署名。把它家的二进制经由本包再分发,等于
65
+ > 把别人的产物当自己的发布。
66
+ > - **平台。** 引擎按操作系统与架构分别发布——Windows/Linux/macOS × x64/arm64。打包进来等于把某一个
67
+ > 构建方式钉死给所有用户。
68
+ > - **版本。** 插件要能配合用户手上**任意** Moli 版本工作,包括插件发布之后才出现的 Moli 新版本。
69
+ > 打包会把两者的版本绑死。
70
+ >
71
+ > **引擎同样没有 npm 包。** registry 上的 `moli` 和 `moli-cli` 属于无关项目,`@lexmount/moli` 并不
72
+ > 存在。Moli 以预编译二进制形式发布在
61
73
  > [GitHub Releases](https://github.com/lexmount/moli/releases/latest)——**不要靠猜包名去装引擎**。
62
74
 
63
- 两种方式都要重启 DSH,然后自检并抓一个页面:
75
+ ## 装好之后的两种情形
76
+
77
+ 上面两条下载路线装的是同一个东西。区别在于**你的机器上有没有 Moli**——这决定你是就此完事,还是还差一步。
78
+
79
+ ### 情形 A —— 已经装了 Moli:不用再做别的
80
+
81
+ 插件自己会找到 `moli.exe`,两个工具立即可用。重启 DSH 然后确认:
82
+
83
+ ```
84
+ moli_probe
85
+ ```
86
+
87
+ 如果报出来的版本正常,就结束了。
88
+
89
+ ### 情形 B —— 还没装 Moli:把引擎也拉下来
90
+
91
+ 在你装上引擎之前,`moli_fetch` 会一直报"引擎缺失"。用下面任一种装上:
92
+
93
+ ```
94
+ moli_probe action=install # 让插件替你拉
95
+ ```
96
+
97
+ ```powershell
98
+ # 或者自己跑 Moli 官方的安装器
99
+ powershell -ExecutionPolicy ByPass -c "irm https://github.com/lexmount/moli/releases/latest/download/moli-installer.ps1 | iex"
100
+ ```
101
+
102
+ 然后用 `moli_probe` 确认并抓一个页面:
64
103
 
65
104
  ```
66
105
  moli_probe
@@ -197,6 +236,15 @@ Moli 仓库自带官方 agent skill:[`moli-webfetch`](https://github.com/lexmo
197
236
  导入(例如 `@deepseek-ai/schemastery`)都会解析到插件目录而非 profile 的 `node_modules`,导致
198
237
  插件加载失败。本插件只导入 `node:` 内置模块和自己的文件。
199
238
 
239
+ ## 平台支持
240
+
241
+ 支持 **Windows、Linux、macOS**(x64 / arm64)。
242
+
243
+ - **Windows** —— 引擎在官方安装位置查找(`%LOCALAPPDATA%\Moli\bin\moli.exe` 与 `%USERPROFILE%\.local\bin\moli.exe`),
244
+ 并以 `%LOCALAPPDATA%\Moli` 递归扫描兜底;`moli_probe action=install` 走官方 PowerShell 安装器。
245
+ - **Linux / macOS** —— 引擎在标准位置(`~/.local/bin/moli`、`~/.local/share/moli/bin/moli`、
246
+ `~/.local/share/moli/moli`、`~/.moli/bin/moli`)与 `$PATH` 中查找;`moli_probe action=install` 通过 `sh` 执行官方 `moli-installer.sh`。
247
+
200
248
  ## 配置
201
249
 
202
250
  在 profile 的补丁层里覆盖 `config`,全部可选:
@@ -219,8 +267,12 @@ Moli 仓库自带官方 agent skill:[`moli-webfetch`](https://github.com/lexmo
219
267
 
220
268
  ## 已知限制
221
269
 
222
- - **面向 Windows。** 开发与验证都在 Windows 上;可执行文件探测、文件描述符绕坑、Schannel 说明都
223
- 与 Windows 相关。移植到 POSIX 没有障碍,但未经验证。
270
+ - **Linux 与 macOS 已实现,但作者没有测过。** POSIX 相关代码来自
271
+ [PR #1](https://github.com/27painter/dsh-moli/pull/1),已做代码审查,但本仓库维护者是一名**没有
272
+ macOS / Linux 电脑**的大学生,所以这些分支从未在真实的 POSIX 主机上运行过。这里唯一验证过的平台是
273
+ Windows:可执行文件探测、文件描述符绕坑与 Schannel 说明都是 Windows 上的观察。
274
+ **如果在 Linux 或 macOS 上出问题,请带上报错信息开
275
+ [issue](https://github.com/27painter/dsh-moli/issues)**——那是这些路径能被打通的唯一途径。
224
276
  - **不包含 Moli 本体。** 需自行安装,或让 `moli_probe action=install` 替你拉取。
225
277
  - **只封装了 `fetch`。** `moli serve`(CDP/WebDriver 服务器)与 `moli import`(会话导入)尚未做成工具。
226
278
  - **不支持单次取消。** 引擎是同步执行的,被中断的回合无法中途终止抓取;已在进行的调用会跑完并
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /** Host half of dsh-moli: expose the local `moli.exe` headless browser as callable tools. */
1
+ /** Host half of dsh-moli: expose the local `moli` headless browser as callable tools. */
2
2
  import { MANUAL_INSTALL, installMoli } from './lib/install.js';
3
3
  import { findMoli, moliCandidates, probe, runFetch } from './lib/moli.js';
4
4
 
@@ -198,7 +198,7 @@ function renderProbe(_args, value) {
198
198
  return [{ type: 'text', text: lines.join('\n') }];
199
199
  }
200
200
  if (value.installed !== true) {
201
- const lines = ['moli.exe was not found. Call moli_probe with action "install", or set the plugin config field `exePath`.'];
201
+ const lines = ['moli was not found. Call moli_probe with action "install", or set the plugin config field `exePath`.'];
202
202
  lines.push(`Manual install — PowerShell: ${MANUAL_INSTALL.powershell}`);
203
203
  lines.push(`Manual install — Linux/macOS: ${MANUAL_INSTALL.shell}`);
204
204
  return [{ type: 'text', text: lines.join('\n') }];
@@ -239,7 +239,7 @@ export function apply(ctx, config) {
239
239
  const insecureTls = options.insecureTls ?? DEFAULTS.insecureTls;
240
240
 
241
241
  if (cachedExe === undefined) {
242
- ctx.logger?.warn?.('dsh-moli: moli.exe not found; moli_probe can install it, or set config.exePath');
242
+ ctx.logger?.warn?.('dsh-moli: moli not found; moli_probe can install it, or set config.exePath');
243
243
  }
244
244
 
245
245
  ctx.tools.register({
@@ -247,7 +247,7 @@ export function apply(ctx, config) {
247
247
  description: 'Fetch a URL with the local Moli headless browser: rendered HTML or Markdown, the semantic'
248
248
  + ' tree, a JSON envelope with status and final_url, a JavaScript expression result, a PNG screenshot, or a'
249
249
  + ' PDF. Use it for pages that need real rendering or JavaScript; prefer web_fetch for plain text. Screenshot'
250
- + ' and pdf dumps require writeTo. If moli.exe is missing, run moli_probe with action "install" first.',
250
+ + ' and pdf dumps require writeTo. If moli is missing, run moli_probe with action "install" first.',
251
251
  parameters: FETCH_PARAMETERS,
252
252
  output: { schema: FETCH_OUTPUT_SCHEMA, render: renderFetch },
253
253
  isConcurrencySafe: args => args.writeTo === undefined || args.writeTo === '',
@@ -258,9 +258,9 @@ export function apply(ctx, config) {
258
258
  return {
259
259
  ok: false,
260
260
  url: args.url,
261
- error: 'moli.exe was not found',
261
+ error: 'moli was not found',
262
262
  sandboxTlsBlock: false,
263
- hint: 'Call moli_probe with action "install" to download it, or set the dsh-moli config field `exePath` to the absolute moli.exe path.',
263
+ hint: 'Call moli_probe with action "install" to download it, or set the dsh-moli config field `exePath` to the absolute moli path.',
264
264
  manualInstall: MANUAL_INSTALL,
265
265
  };
266
266
  }
@@ -279,7 +279,7 @@ export function apply(ctx, config) {
279
279
  description: 'Report the local Moli install: version, executable path, and whether HTTPS fetching works from'
280
280
  + ' this process. With action "install" it downloads and runs the official Moli installer instead, then'
281
281
  + ' reports the executable and the SHA256 of the installer it ran. Use "install" only when moli_fetch'
282
- + ' reports that moli.exe is missing; it downloads and executes third-party code.',
282
+ + ' reports that moli is missing; it downloads and executes third-party code.',
283
283
  parameters: {
284
284
  type: 'object',
285
285
  additionalProperties: false,
@@ -321,7 +321,7 @@ export function apply(ctx, config) {
321
321
 
322
322
  const exe = engine();
323
323
  if (exe === undefined) {
324
- return { installed: false, detail: 'moli.exe not found. Re-run with action "install" to download it.', manualInstall: MANUAL_INSTALL };
324
+ return { installed: false, detail: 'moli not found. Re-run with action "install" to download it.', manualInstall: MANUAL_INSTALL };
325
325
  }
326
326
  const result = probe(exe, 'https://example.com', insecureTls);
327
327
  return {
@@ -336,5 +336,5 @@ export function apply(ctx, config) {
336
336
  },
337
337
  });
338
338
 
339
- ctx.logger?.info?.(`dsh-moli: registered moli_fetch and moli_probe (${cachedExe ?? 'moli.exe missing'})`);
339
+ ctx.logger?.info?.(`dsh-moli: registered moli_fetch and moli_probe (${cachedExe ?? 'moli missing'})`);
340
340
  }
package/lib/install.js CHANGED
@@ -30,7 +30,9 @@ import { join } from 'node:path';
30
30
  import { findMoli, moliVersion } from './moli.js';
31
31
 
32
32
  /** Official installer for the latest release. */
33
- export const INSTALLER_URL = 'https://github.com/lexmount/moli/releases/latest/download/moli-installer.ps1';
33
+ export const INSTALLER_URL = process.platform === 'win32'
34
+ ? 'https://github.com/lexmount/moli/releases/latest/download/moli-installer.ps1'
35
+ : 'https://github.com/lexmount/moli/releases/latest/download/moli-installer.sh';
34
36
 
35
37
  /** Moli's own README instructions, for the manual path. */
36
38
  export const MANUAL_INSTALL = {
@@ -136,7 +138,7 @@ export async function downloadInstaller(options) {
136
138
  const url = options.installerUrl ?? INSTALLER_URL;
137
139
  const configured = typeof options.proxy === 'string' && options.proxy.length > 0 ? options.proxy : undefined;
138
140
  const timeoutMs = options.timeoutMs ?? 60000;
139
- const powershell = options.powershell ?? 'powershell.exe';
141
+ const powershell = options.powershell ?? (process.platform === 'win32' ? 'powershell.exe' : 'powershell');
140
142
 
141
143
  // Never hand a loopback URL to a proxy — with a stale system proxy setting the
142
144
  // request would go to a dead port and wait out the connect timeout. Windows
@@ -152,20 +154,22 @@ export async function downloadInstaller(options) {
152
154
  `${TLS_PRELUDE}${bypass} Invoke-WebRequest -UseBasicParsing ${extra} -Uri '${url}' -OutFile $env:MOLI_DL_TARGET -TimeoutSec ${String(Math.ceil(timeoutMs / 1000))}`,
153
155
  ];
154
156
 
155
- const routes = [
156
- {
157
- method: 'Invoke-WebRequest (direct)',
158
- command: powershell,
159
- args: iwr(''),
160
- },
161
- ];
157
+ const routes = process.platform === 'win32'
158
+ ? [
159
+ {
160
+ method: 'Invoke-WebRequest (direct)',
161
+ command: powershell,
162
+ args: iwr(''),
163
+ },
164
+ ]
165
+ : [];
162
166
 
163
167
  // Only attempt proxy routes when something is actually listening there.
164
168
  let proxyAlive = false;
165
169
  if (configured !== undefined) {
166
170
  proxyAlive = await proxyListens(configured);
167
171
  }
168
- if (configured !== undefined && proxyAlive) {
172
+ if (process.platform === 'win32' && configured !== undefined && proxyAlive) {
169
173
  routes.push({
170
174
  method: `Invoke-WebRequest (proxy ${configured})`,
171
175
  command: powershell,
@@ -173,14 +177,14 @@ export async function downloadInstaller(options) {
173
177
  });
174
178
  }
175
179
  routes.push({
176
- method: 'curl.exe (direct)',
177
- command: 'curl.exe',
180
+ method: 'curl (direct)',
181
+ command: process.platform === 'win32' ? 'curl.exe' : 'curl',
178
182
  args: ['-sSL', ...curlBypass, '--max-time', String(Math.ceil(timeoutMs / 1000)), '-o', target, url],
179
183
  });
180
184
  if (configured !== undefined && proxyAlive) {
181
185
  routes.push({
182
- method: `curl.exe (proxy ${configured})`,
183
- command: 'curl.exe',
186
+ method: `curl (proxy ${configured})`,
187
+ command: process.platform === 'win32' ? 'curl.exe' : 'curl',
184
188
  args: ['-sSL', ...curlBypass, '--max-time', String(Math.ceil(timeoutMs / 1000)), '--proxy', configured, '-o', target, url],
185
189
  });
186
190
  }
@@ -242,7 +246,7 @@ export async function downloadInstaller(options) {
242
246
  export async function installMoli(options = {}) {
243
247
  const timeoutMs = options.timeoutMs ?? 180000;
244
248
  const workDir = mkdtempSync(join(tmpdir(), 'dsh-moli-install-'));
245
- const target = join(workDir, 'moli-installer.ps1');
249
+ const target = join(workDir, process.platform === 'win32' ? 'moli-installer.ps1' : 'moli-installer.sh');
246
250
  try {
247
251
  const download = await downloadInstaller({
248
252
  target,
@@ -279,9 +283,11 @@ export async function installMoli(options = {}) {
279
283
  }
280
284
  }
281
285
 
282
- const run = spawnSync(options.powershell ?? 'powershell.exe',
283
- ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', target],
284
- { stdio: ['ignore', 'ignore', 'ignore'], timeout: timeoutMs, windowsHide: true });
286
+ const run = process.platform === 'win32'
287
+ ? spawnSync(options.powershell ?? 'powershell.exe',
288
+ ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', target],
289
+ { stdio: ['ignore', 'ignore', 'ignore'], timeout: timeoutMs, windowsHide: true })
290
+ : spawnSync('sh', [target], { stdio: ['ignore', 'ignore', 'ignore'], timeout: timeoutMs });
285
291
  const runFailed = run.error !== undefined && run.error !== null;
286
292
  const runStatus = runFailed ? null : (run.status ?? 0);
287
293
 
@@ -294,7 +300,7 @@ export async function installMoli(options = {}) {
294
300
  ok: false,
295
301
  error: runFailed
296
302
  ? `running the installer failed: ${run.error.message}`
297
- : `the installer ${runStatus === 0 ? 'reported success' : `exited ${String(runStatus)}`}, but no working moli.exe was found afterwards`,
303
+ : `the installer ${runStatus === 0 ? 'reported success' : `exited ${String(runStatus)}`}, but no working moli binary was found afterwards`,
298
304
  installerSha256: download.sha256,
299
305
  installerBytes: download.bytes,
300
306
  method: download.method,
package/lib/moli.js CHANGED
@@ -16,7 +16,7 @@
16
16
  import { spawnSync } from 'node:child_process';
17
17
  import { createHash } from 'node:crypto';
18
18
  import { closeSync, existsSync, mkdtempSync, openSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
19
- import { tmpdir } from 'node:os';
19
+ import { homedir, tmpdir } from 'node:os';
20
20
  import { dirname, join } from 'node:path';
21
21
 
22
22
  /** Output formats that are bytes, not text. */
@@ -42,27 +42,44 @@ const WAIT_STAGES = new Set(['domcontentloaded', 'load', 'networkidle', 'domstab
42
42
  * @returns candidate paths, in probe order.
43
43
  */
44
44
  export function moliCandidates(override) {
45
+ const isWin = process.platform === 'win32';
46
+ const home = process.env.HOME ?? homedir();
45
47
  const listed = [
46
48
  override,
47
49
  process.env.MOLI_BIN,
48
- process.env.LOCALAPPDATA === undefined ? undefined : join(process.env.LOCALAPPDATA, 'Moli', 'bin', 'moli.exe'),
49
- process.env.USERPROFILE === undefined ? undefined : join(process.env.USERPROFILE, '.local', 'bin', 'moli.exe'),
50
+ // Windows: official installer location.
51
+ isWin && process.env.LOCALAPPDATA !== undefined ? join(process.env.LOCALAPPDATA, 'Moli', 'bin', 'moli.exe') : undefined,
52
+ isWin && process.env.USERPROFILE !== undefined ? join(process.env.USERPROFILE, '.local', 'bin', 'moli.exe') : undefined,
53
+ // Linux/macOS: installer.sh default ~/.local/bin/moli, plus versioned/shared dirs.
54
+ !isWin ? join(home, '.local', 'bin', 'moli') : undefined,
55
+ !isWin ? join(home, '.local', 'share', 'moli', 'bin', 'moli') : undefined,
56
+ !isWin ? join(home, '.local', 'share', 'moli', 'moli') : undefined,
57
+ !isWin ? join(home, '.moli', 'bin', 'moli') : undefined,
50
58
  ].filter(candidate => typeof candidate === 'string' && candidate.length > 0);
51
59
 
52
60
  const discovered = [];
53
- const root = process.env.LOCALAPPDATA === undefined ? undefined : join(process.env.LOCALAPPDATA, 'Moli');
54
- if (root !== undefined && existsSync(root)) {
55
- let entries = [];
56
- try {
57
- entries = readdirSync(root, { recursive: true, withFileTypes: true });
58
- } catch {
59
- entries = [];
60
- }
61
- for (const entry of entries) {
62
- if (entry.isFile() && entry.name.toLowerCase() === 'moli.exe') {
63
- discovered.push(join(entry.parentPath ?? root, entry.name));
61
+ if (isWin) {
62
+ const root = process.env.LOCALAPPDATA === undefined ? undefined : join(process.env.LOCALAPPDATA, 'Moli');
63
+ if (root !== undefined && existsSync(root)) {
64
+ let entries = [];
65
+ try {
66
+ entries = readdirSync(root, { recursive: true, withFileTypes: true });
67
+ } catch {
68
+ entries = [];
69
+ }
70
+ for (const entry of entries) {
71
+ if (entry.isFile() && entry.name.toLowerCase() === 'moli.exe') {
72
+ discovered.push(join(entry.parentPath ?? root, entry.name));
73
+ }
64
74
  }
65
75
  }
76
+ } else {
77
+ const seen = new Set(listed);
78
+ for (const dir of String(process.env.PATH ?? '').split(':')) {
79
+ if (dir === '') continue;
80
+ const candidate = join(dir, 'moli');
81
+ if (existsSync(candidate) && !seen.has(candidate)) discovered.push(candidate);
82
+ }
66
83
  }
67
84
  return [...listed, ...discovered];
68
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-moli",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
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",
@@ -26,7 +26,7 @@
26
26
  "url": "https://github.com/27painter/dsh-moli/issues"
27
27
  },
28
28
  "engines": {
29
- "node": ">=20"
29
+ "node": ">=20.12"
30
30
  },
31
31
  "publishConfig": {
32
32
  "registry": "https://registry.npmjs.org",