dsh-moli 1.0.6 → 1.1.1

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
@@ -255,6 +255,32 @@ capabilities always visible to the model.
255
255
  resolves against the plugin directory instead of the profile's `node_modules` and the plugin
256
256
  fails to load. This plugin imports only `node:` builtins and its own files.
257
257
 
258
+ ## Platform support
259
+
260
+ Works on **Windows, Linux and macOS** (x64 / arm64).
261
+
262
+ > **Linux and macOS support was contributed by [@nd002723](https://github.com/nd002723)** in
263
+ > [PR #1](https://github.com/27painter/dsh-moli/pull/1): engine discovery in the standard POSIX
264
+ > locations, the `sh` installer route, and the per-platform gating of the PowerShell paths.
265
+
266
+ | Platform | Status |
267
+ |---|---|
268
+ | **Windows** | Verified by the maintainer |
269
+ | **Linux** | Verified by the contributor on Ubuntu 24.04 LTS, Node v24.21.0, DSH 0.2.1-alpha.2, dsh-moli 1.1.0, Moli 1.1.17 |
270
+ | **macOS** | Implemented, not yet verified on a real host |
271
+
272
+ - **Windows** — the engine is located at the official installer paths (`%LOCALAPPDATA%\Moli\bin\moli.exe`,
273
+ `%USERPROFILE%\.local\bin\moli.exe`) with a recursive scan of `%LOCALAPPDATA%\Moli` as a fallback.
274
+ `moli_probe action=install` runs the official PowerShell installer.
275
+ - **Linux / macOS** — the engine is found in the standard locations (`~/.local/bin/moli`,
276
+ `~/.local/share/moli/bin/moli`, `~/.local/share/moli/moli`, `~/.moli/bin/moli`) and on `$PATH`.
277
+ `moli_probe action=install` runs the official `moli-installer.sh` via `sh`.
278
+
279
+ On Linux, `moli_probe` discovered `~/.local/bin/moli` unaided, and `moli_fetch` rendered a
280
+ region-gated storefront end to end — through a local forward proxy, with a Netscape `cookieFile` —
281
+ returning a 500+ KB page whose final URL carried the cookie-driven query parameters. That run
282
+ exercised the proxy download route as well as the engine paths.
283
+
258
284
  ## Configuration
259
285
 
260
286
  Override any field under the plugin's `config` entry in the profile patch layer. All are optional.
@@ -277,9 +303,12 @@ installation under a versioned subdirectory is still found.
277
303
 
278
304
  ## Known limitations
279
305
 
280
- - **Windows-only targeting.** Developed and tested on Windows; the executable discovery, the
281
- file-descriptor workaround, and the Schannel notes are Windows-specific. Nothing prevents a
282
- POSIX port, but it is untested.
306
+ - **macOS is untested, and Linux was tested by the contributor rather than the maintainer.** The
307
+ maintainer is a student with no macOS or Linux machine, so Linux was verified independently by
308
+ [@nd002723](https://github.com/nd002723) on Ubuntu 24.04 (see [Platform support](#platform-support));
309
+ macOS has not been run on a real host at all. **If something fails on either, please open an
310
+ [issue](https://github.com/27painter/dsh-moli/issues) with the error output** — that is the only way
311
+ these paths get fixed.
283
312
  - **Moli itself is not bundled.** Install it separately or let `moli_probe action=install` fetch it.
284
313
  - **Only `fetch` is wrapped.** `moli serve` (CDP/WebDriver server) and `moli import` (session
285
314
  import) are not exposed as tools yet.
package/README.zh.md CHANGED
@@ -236,6 +236,29 @@ Moli 仓库自带官方 agent skill:[`moli-webfetch`](https://github.com/lexmo
236
236
  导入(例如 `@deepseek-ai/schemastery`)都会解析到插件目录而非 profile 的 `node_modules`,导致
237
237
  插件加载失败。本插件只导入 `node:` 内置模块和自己的文件。
238
238
 
239
+ ## 平台支持
240
+
241
+ 支持 **Windows、Linux、macOS**(x64 / arm64)。
242
+
243
+ > **Linux 与 macOS 支持由 [@nd002723](https://github.com/nd002723) 贡献**,见
244
+ > [PR #1](https://github.com/27painter/dsh-moli/pull/1):POSIX 标准位置的引擎探测、`sh` 安装器路径,
245
+ > 以及把 PowerShell 相关路径按平台门控。
246
+
247
+ | 平台 | 状态 |
248
+ |---|---|
249
+ | **Windows** | 由维护者验证 |
250
+ | **Linux** | 由贡献者在 Ubuntu 24.04 LTS、Node v24.21.0、DSH 0.2.1-alpha.2、dsh-moli 1.1.0、Moli 1.1.17 上验证通过 |
251
+ | **macOS** | 已实现,尚未在真机验证 |
252
+
253
+ - **Windows** —— 引擎在官方安装位置查找(`%LOCALAPPDATA%\Moli\bin\moli.exe` 与 `%USERPROFILE%\.local\bin\moli.exe`),
254
+ 并以 `%LOCALAPPDATA%\Moli` 递归扫描兜底;`moli_probe action=install` 走官方 PowerShell 安装器。
255
+ - **Linux / macOS** —— 引擎在标准位置(`~/.local/bin/moli`、`~/.local/share/moli/bin/moli`、
256
+ `~/.local/share/moli/moli`、`~/.moli/bin/moli`)与 `$PATH` 中查找;`moli_probe action=install` 通过 `sh` 执行官方 `moli-installer.sh`。
257
+
258
+ 在 Linux 上,`moli_probe` 无需干预就找到了 `~/.local/bin/moli`;`moli_fetch` 端到端渲染了一个地区限制的
259
+ 电商站——经由本地正向代理、并带上 Netscape 格式的 `cookieFile`——返回 500+ KB 的页面,最终 URL 带上了
260
+ cookie 驱动的查询参数。那次实测同时覆盖了代理下载路由与引擎探测路径。
261
+
239
262
  ## 配置
240
263
 
241
264
  在 profile 的补丁层里覆盖 `config`,全部可选:
@@ -258,8 +281,10 @@ Moli 仓库自带官方 agent skill:[`moli-webfetch`](https://github.com/lexmo
258
281
 
259
282
  ## 已知限制
260
283
 
261
- - **面向 Windows。** 开发与验证都在 Windows 上;可执行文件探测、文件描述符绕坑、Schannel 说明都
262
- 与 Windows 相关。移植到 POSIX 没有障碍,但未经验证。
284
+ - **macOS 未经验证,Linux 是由贡献者而非维护者验证的。** 维护者是一名**没有 macOS / Linux 电脑**的
285
+ 大学生;Linux 由 [@nd002723](https://github.com/nd002723) 在 Ubuntu 24.04 上独立验证通过(见
286
+ [平台支持](#平台支持)),而 macOS 从未在真机运行过。**任一平台出问题,都请带上报错信息开
287
+ [issue](https://github.com/27painter/dsh-moli/issues)**——那是这些路径能被打通的唯一途径。
263
288
  - **不包含 Moli 本体。** 需自行安装,或让 `moli_probe action=install` 替你拉取。
264
289
  - **只封装了 `fetch`。** `moli serve`(CDP/WebDriver 服务器)与 `moli import`(会话导入)尚未做成工具。
265
290
  - **不支持单次取消。** 引擎是同步执行的,被中断的回合无法中途终止抓取;已在进行的调用会跑完并
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.6",
3
+ "version": "1.1.1",
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",