dsh-moli 1.0.6 → 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 +19 -3
- package/README.zh.md +15 -2
- package/index.js +9 -9
- package/lib/install.js +25 -19
- package/lib/moli.js +31 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -255,6 +255,17 @@ 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
|
+
- **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
|
+
|
|
258
269
|
## Configuration
|
|
259
270
|
|
|
260
271
|
Override any field under the plugin's `config` entry in the profile patch layer. All are optional.
|
|
@@ -277,9 +288,14 @@ installation under a versioned subdirectory is still found.
|
|
|
277
288
|
|
|
278
289
|
## Known limitations
|
|
279
290
|
|
|
280
|
-
- **
|
|
281
|
-
|
|
282
|
-
|
|
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.
|
|
283
299
|
- **Moli itself is not bundled.** Install it separately or let `moli_probe action=install` fetch it.
|
|
284
300
|
- **Only `fetch` is wrapped.** `moli serve` (CDP/WebDriver server) and `moli import` (session
|
|
285
301
|
import) are not exposed as tools yet.
|
package/README.zh.md
CHANGED
|
@@ -236,6 +236,15 @@ 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
|
+
- **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
|
+
|
|
239
248
|
## 配置
|
|
240
249
|
|
|
241
250
|
在 profile 的补丁层里覆盖 `config`,全部可选:
|
|
@@ -258,8 +267,12 @@ Moli 仓库自带官方 agent skill:[`moli-webfetch`](https://github.com/lexmo
|
|
|
258
267
|
|
|
259
268
|
## 已知限制
|
|
260
269
|
|
|
261
|
-
-
|
|
262
|
-
|
|
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)**——那是这些路径能被打通的唯一途径。
|
|
263
276
|
- **不包含 Moli 本体。** 需自行安装,或让 `moli_probe action=install` 替你拉取。
|
|
264
277
|
- **只封装了 `fetch`。** `moli serve`(CDP/WebDriver 服务器)与 `moli import`(会话导入)尚未做成工具。
|
|
265
278
|
- **不支持单次取消。** 引擎是同步执行的,被中断的回合无法中途终止抓取;已在进行的调用会跑完并
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Host half of dsh-moli: expose the local `moli
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
|
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 =
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
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
|
-
|
|
49
|
-
process.env.
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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",
|